GNU/Linux >> Znalost Linux >  >> Linux

Které části tohoto kódu sestavení HelloWorld jsou nezbytné, pokud bych měl psát program v assembleru?

Absolutní minimum, které bude fungovat na platformě, jakou se zdá, je

        .globl main
main:
        pushl   $.LC0
        call    puts
        addl    $4, %esp
        xorl    %eax, %eax
        ret
.LC0:
        .string "Hello world"

To však porušuje řadu požadavků ABI. Minimum pro program kompatibilní s ABI je

        .globl  main
        .type   main, @function
main:
        subl    $24, %esp
        pushl   $.LC0
        call    puts
        xorl    %eax, %eax
        addl    $28, %esp
        ret
        .size main, .-main
        .section .rodata
.LC0:
        .string "Hello world"

Všechno ostatní ve vašem objektovém souboru je buď kompilátor, který neoptimalizuje kód co nejpřesněji, nebo volitelné anotace, které mají být zapsány do souboru objektu.

.cfi_* nepovinnými anotacemi jsou zejména směrnice. Jsou nezbytné tehdy a jen tehdy, když funkce může být v zásobníku volání, když je vyvolána výjimka C++, ale jsou užitečné v libovolném programu, ze kterého byste mohli chtít extrahovat trasování zásobníku. Pokud budete psát netriviální kód ručně v jazyce symbolických instrukcí, pravděpodobně bude stát za to naučit se je psát. Bohužel jsou velmi špatně zdokumentovány; Momentálně nenacházím nic, o čem si myslím, že by stálo za to odkazovat.

Linka

.section    .note.GNU-stack,"",@progbits

je také důležité vědět, zda píšete jazyk symbolických instrukcí ručně; je to další volitelná anotace, ale cenná, protože to znamená "nic v tomto objektovém souboru nevyžaduje, aby byl zásobník spustitelný." Pokud všechny objektové soubory v programu mají tuto anotaci, jádro neudělá zásobník spustitelným, což trochu zlepšuje zabezpečení.

(Pro označení, že děláte potřebujete, aby byl zásobník spustitelný, vložte "x" místo "" . GCC to může udělat, pokud použijete jeho rozšíření "vnořená funkce". (Nedělejte to.))

Pravděpodobně stojí za zmínku, že v syntaxi sestavení „AT&T“, kterou (standardně) používají GCC a GNU binutils, existují tři druhy řádků:Řádek s jedním tokenem, který končí dvojtečkou, je štítek. (Nepamatuji si pravidla pro to, jaké znaky se mohou objevit v popiscích.) Řádek, jehož první token začíná tečkou a není konec dvojtečkou, je nějaký druh směrnice pro assembler. Cokoli jiného je návod k sestavení.


související:Jak odstranit „šum“ z výstupu sestavy GCC/clang? .cfi direktivy pro vás nejsou přímo užitečné a program by bez nich fungoval. (Jsou to informace o odvíjení zásobníku potřebné pro zpracování výjimek a zpětné sledování, takže -fomit-frame-pointer lze ve výchozím nastavení povolit. A ano, gcc to vydává i pro C.)

Pokud jde o počet zdrojových řádků asm potřebných k vytvoření hodnotného programu Hello World, samozřejmě chceme použít funkce libc, aby za nás udělaly více práce.

Odpověď @Zwol má nejkratší implementaci vašeho původního kódu C.

Zde je to, co můžete udělat ručně , pokud vás nezajímá stav ukončení vašeho programu, jen to, že vypíše váš řetězec.

# Hand-optimized asm, not compiler output
    .globl main            # necessary for the linker to see this symbol
main:
    # main gets two args: argv and argc, so we know we can modify 8 bytes above our return address.
    movl    $.LC0, 4(%esp)     # replace our first arg with the string
    jmp     puts               # tail-call puts.

# you would normally put the string in .rodata, not leave it in .text where the linker will mix it with other functions.
.section .rodata
.LC0:
    .asciz "Hello world"     # asciz zero-terminates

Ekvivalent C (právě jste požádali o nejkratší Hello World, nikoli o ten, který měl stejnou sémantiku):

int main(int argc, char **argv) {
    return puts("Hello world");
}

Jeho výstupní stav je definován implementací, ale rozhodně se vytiskne. puts(3) vrací "nezáporné číslo", které by mohlo být mimo rozsah 0..255, takže nemůžeme říci nic o tom, že stav ukončení programu je 0 / nenulový v Linuxu (kde je stav ukončení procesu nízkých 8 bitů celého čísla předávaného do exit_group() systémové volání (v tomto případě spouštěcím kódem CRT, který volal main()).

Použití JMP k implementaci tail-call je standardní praxí a běžně se používá, když funkce po návratu jiné funkce nemusí nic dělat. puts() se nakonec vrátí k funkci, která volala main() , stejně jako kdyby se puts() vrátilo k main() a pak se vrátilo main(). Volající main() se stále musí vypořádat s argumenty, které vložil do zásobníku pro main(), protože tam stále jsou (ale upravené a my to můžeme udělat).

gcc a clang negenerují kód, který by upravoval prostor pro předávání argumentů na zásobníku. Je však naprosto bezpečný a kompatibilní s ABI:funkce „vlastní“ své argumenty v zásobníku, i když byly const . Pokud zavoláte funkci, nemůžete předpokládat, že argumenty, které jste vložili do zásobníku, tam stále jsou. Chcete-li uskutečnit další hovor se stejnými nebo podobnými argumenty, musíte je všechny znovu uložit.

Všimněte si také, že to volá puts() se stejným zarovnáním zásobníku, jaké jsme měli při vstupu do main() , takže jsme opět v souladu s ABI při zachování zarovnání 16B, které vyžaduje moderní verze x86-32 aka i386 System V ABI (používaná Linuxem).

.string řetězce končící nulou, stejně jako .asciz , ale musel jsem si to prověřit. Doporučil bych použít pouze .ascii nebo .asciz abyste se ujistili, že máte jasno v tom, zda vaše data mají ukončovací bajt nebo ne. (Nepotřebujete jej, pokud jej používáte s funkcemi s explicitní délkou, jako je write() )

V x86-64 System V ABI (a Windows) jsou argumenty předávány v registrech. Díky tomu je optimalizace tail-call mnohem jednodušší, protože můžete změnit uspořádání argumentů nebo předat více args (pokud vám nedojdou registry). Díky tomu jsou kompilátoři ochotni to dělat v praxi. (Protože, jak jsem řekl, v současné době neradi generují kód, který upravuje příchozí argový prostor na zásobníku, i když je ABI jasné, že to mají povoleno, a funkce generované kompilátorem předpokládají, že volaní blokují jejich argumenty zásobníku. .)

clang nebo gcc -O3 provede tuto optimalizaci pro x86-64, jak můžete vidět v průzkumníku kompilátoru Godbolt :

#include <stdio.h>
int main() { return puts("Hello World"); }

# clang -O3 output
main:                               # @main
    movl    $.L.str, %edi
    jmp     puts                    # TAILCALL

 # Godbolt strips out comment-only lines and directives; there's actually a .section .rodata before this
.L.str:
    .asciz  "Hello World"

Statické datové adresy se vždy vejdou do nízkých 31 bitů adresního prostoru a spustitelný soubor nepotřebuje kód nezávislý na pozici, jinak mov bude lea .LC0(%rip), %rdi . (To získáte z gcc, pokud byl nakonfigurován s --enable-default-pie vytvořit spustitelné soubory nezávislé na pozici.)

Jak načíst adresu funkce nebo štítku do registru v GNU Assembler

Hello World pomocí 32bitového x86 Linuxu int 0x80 systémová volání přímo, žádná knihovna libc

Viz Hello, world in assembler with Linux system calls? Moje odpověď tam byla původně napsána pro SO Docs, pak se sem přesunula jako místo, kam ji umístit, když SO Docs skončil. Ve skutečnosti to sem nepatřilo, takže jsem to přesunul na jinou otázku.

související:Výukový program Whirlwind o vytváření spustitelných souborů Really Teensy ELF pro Linux. Nejmenší binární soubor, který můžete spustit a který pouze provede systémové volání exit(). Jde o minimalizaci binární velikosti, nikoli velikosti zdroje nebo dokonce pouze počtu instrukcí, které se skutečně spustí.


Linux
  1. MySQL vs. MariaDB:Jaké jsou hlavní rozdíly mezi nimi

  2. Linux – Jaké jsou hlavní rozdíly mezi operačními systémy založenými na Bsd a Linux?

  3. Co je na tomto kódu C zranitelné?

  1. Linux – proprietární nebo uzavřené části jádra?

  2. Router pfSense vs Netgear:Jaké jsou hlavní rozdíly?

  3. Fedora vs Ubuntu:Jaké jsou klíčové rozdíly?

  1. Jaké jsou typy serverů DNS

  2. Jaký je ukončovací kód programu, když selžeasses()?

  3. Jaké jsou výchozí začleněné adresáře GCC?