GNU/Linux >> LINUX-Kenntnisse >  >> Linux

Welche Teile dieses HelloWorld-Assemblercodes sind wesentlich, wenn ich das Programm in Assembler schreiben würde?

Das absolute Minimum, das auf der Plattform funktioniert, die dies zu sein scheint, ist

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

Dies verstößt jedoch gegen eine Reihe von ABI-Anforderungen. Das Minimum für ein ABI-konformes Programm ist

        .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"

Alles andere in Ihrer Objektdatei ist entweder der Compiler, der den Code nicht so streng wie möglich optimiert, oder optional Anmerkungen, die in die Objektdatei geschrieben werden sollen.

Der .cfi_* insbesondere Direktiven sind optionale Anmerkungen. Sie sind notwendig wenn und nur wenn die Funktion auf dem Call-Stack sein könnte, wenn eine C++-Ausnahme ausgelöst wird, sie aber nützlich sind in jedem Programm, aus dem Sie einen Stack-Trace extrahieren möchten. Wenn Sie nicht trivialen Code von Hand in Assemblersprache schreiben, lohnt es sich wahrscheinlich zu lernen, wie man ihn schreibt. Leider sind sie sehr schlecht dokumentiert; Ich finde derzeit nichts, was meiner Meinung nach einen Link wert wäre.

Die Linie

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

ist auch wichtig zu wissen, ob Sie Assembler von Hand schreiben; es ist eine weitere optionale Anmerkung, aber eine wertvolle, denn sie bedeutet "nichts in dieser Objektdatei erfordert, dass der Stack ausführbar ist". Wenn alle Objektdateien in einem Programm diese Anmerkung haben, macht der Kernel den Stack nicht ausführbar, was die Sicherheit ein wenig verbessert.

(Um anzuzeigen, dass Sie tun Damit der Stack ausführbar ist, geben Sie "x" ein statt "" . GCC kann dies tun, wenn Sie seine Erweiterung "nested function" verwenden. (Tu das nicht.))

Es ist wahrscheinlich erwähnenswert, dass es in der (standardmäßig) von GCC und GNU-Binutils verwendeten Assembly-Syntax "AT&T" drei Arten von Zeilen gibt:Eine Zeile mit einem einzelnen Token darauf, die mit einem Doppelpunkt endet, ist ein Label. (Ich erinnere mich nicht an die Regeln, welche Zeichen in Labels erscheinen dürfen.) Eine Zeile, deren first Token beginnt mit einem Punkt und nicht mit einem Doppelpunkt enden, ist eine Art Anweisung an den Assembler. Alles andere ist eine Montageanleitung.


Verwandt:Wie entferne ich "Rauschen" aus der GCC/Clang-Assembly-Ausgabe? Die .cfi Direktiven sind für Sie nicht direkt nützlich, und das Programm würde ohne sie funktionieren. (Es sind Stack-Unwind-Informationen, die für die Ausnahmebehandlung und Backtraces benötigt werden, also -fomit-frame-pointer kann standardmäßig aktiviert werden. Und ja, gcc gibt das sogar für C aus.)

Was die Anzahl der asm-Quellzeilen betrifft, die benötigt werden, um ein brauchbares Hello-World-Programm zu erstellen, wollen wir offensichtlich libc-Funktionen verwenden, um mehr Arbeit für uns zu erledigen.

Die Antwort von @Zwol hat die kürzeste Implementierung Ihres ursprünglichen C-Codes.

Hier ist, was Sie von Hand tun könnten , wenn Sie sich nicht um den Exit-Status Ihres Programms kümmern, nur dass es Ihren String ausgibt.

# 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

Das äquivalente C (Sie haben gerade nach dem kürzesten Hello World gefragt, nicht nach einem mit identischer Semantik):

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

Sein Exit-Status ist implementierungsdefiniert, wird aber definitiv gedruckt. puts(3) gibt "eine nicht negative Zahl" zurück, die außerhalb des Bereichs 0..255 liegen könnte, daher können wir nichts darüber sagen, dass der Exit-Status des Programms unter Linux 0 / ungleich Null ist (wobei der Exit-Status des Prozesses die niedrige 8 ist Bits der Ganzzahl, die an exit_group() übergeben werden Systemaufruf (in diesem Fall durch den CRT-Startcode, der main() aufgerufen hat).

Die Verwendung von JMP zum Implementieren des Tail-Aufrufs ist eine Standardpraxis und wird häufig verwendet, wenn eine Funktion nichts tun muss, nachdem eine andere Funktion zurückgegeben wurde. puts() wird schließlich zu der Funktion zurückkehren, die main() aufgerufen hat , genau wie wenn puts() zu main() zurückgekehrt wäre und dann main() zurückgekehrt wäre. Der Aufrufer von main() muss sich immer noch mit den Argumenten befassen, die er für main() auf den Stack gelegt hat, weil sie immer noch da sind (aber modifiziert, und wir dürfen das).

gcc und clang generieren keinen Code, der den arg-übergebenden Speicherplatz auf dem Stapel ändert. Es ist jedoch absolut sicher und ABI-konform:Funktionen "besitzen" ihre Argumente auf dem Stack, selbst wenn sie const wären . Wenn Sie eine Funktion aufrufen, können Sie nicht davon ausgehen, dass die Argumente, die Sie auf den Stack gelegt haben, noch vorhanden sind. Um einen weiteren Anruf mit denselben oder ähnlichen Argumenten zu tätigen, müssen Sie sie alle erneut speichern.

Beachten Sie auch, dass dies puts() aufruft mit derselben Stapelausrichtung, die wir beim Eintritt in main() hatten , also sind wir wieder ABI-konform, indem wir die 16B-Ausrichtung beibehalten, die von der modernen Version des x86-32 alias i386 System V ABI (von Linux verwendet) benötigt wird.

.string beendet Strings mit Null, genauso wie .asciz , aber ich musste es nachschlagen, um es zu überprüfen. Ich würde empfehlen, einfach .ascii zu verwenden oder .asciz um sicherzustellen, dass Sie sich darüber im Klaren sind, ob Ihre Daten ein abschließendes Byte haben oder nicht. (Sie brauchen keinen, wenn Sie ihn mit Funktionen mit expliziter Länge wie write() verwenden )

In x86-64 System V ABI (und Windows) werden Argumente in Registern übergeben. Dies macht die Tail-Call-Optimierung viel einfacher, da Sie Argumente neu anordnen oder mehr übergeben können args (solange die Register nicht ausgehen). Dies macht Compiler bereit, dies in der Praxis zu tun. (Weil sie, wie gesagt, derzeit keinen Code generieren möchten, der den eingehenden Argumentraum auf dem Stapel ändert, obwohl die ABI klar ist, dass sie das dürfen, und vom Compiler generierte Funktionen davon ausgehen, dass aufgerufene ihre Stapelargumente verstopfen .)

clang oder gcc -O3 führt diese Optimierung für x86-64 durch, wie Sie im Godbolt-Compiler-Explorer sehen können :

#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"

Statische Datenadressen passen immer in die unteren 31 Bit des Adressraums, und ausführbare Dateien brauchen keinen positionsunabhängigen Code, sonst mov wäre lea .LC0(%rip), %rdi . (Sie erhalten dies von gcc, wenn es mit --enable-default-pie konfiguriert wurde um positionsunabhängige ausführbare Dateien zu erstellen.)

Wie man die Adresse einer Funktion oder eines Labels in das Register in GNU Assembler lädt

Hallo Welt mit 32-Bit-x86-Linux int 0x80 Systemaufrufe direkt, keine libc

Siehe Hallo, Welt in Assemblersprache mit Linux-Systemaufrufen? Meine Antwort dort wurde ursprünglich für SO Docs geschrieben und dann hierher verschoben, um sie abzulegen, als SO Docs geschlossen wurde. Es gehörte nicht wirklich hierher, also habe ich es in eine andere Frage verschoben.

verwandt:Ein Wirbelwind-Tutorial zum Erstellen wirklich winziger ausführbarer ELF-Dateien für Linux. Die kleinste Binärdatei, die Sie ausführen können und die nur einen Systemaufruf exit() durchführt. Dabei geht es darum, die Binärgröße zu minimieren, nicht die Quellgröße oder auch nur die Anzahl der Anweisungen, die tatsächlich ausgeführt werden.


Linux
  1. Was sind die Hauptunterschiede zwischen Bsd und Gnu/Linux Userland?

  2. Was sind die verschiedenen Arten von Shells in Linux?

  3. Was sind die Vorteile von CloudLinux?

  4. Welche Arten von DNS-Servern gibt es?

  5. Was sind Bash-Exit-Codes in Linux

Was ist Intel SGX und was sind die Vorteile?

pfSense vs Netgear Router:Was sind die Hauptunterschiede?

Fedora vs. Ubuntu:Was sind die Hauptunterschiede?

useradd vs. adduser:Was sind die Unterschiede?

Kann GDB den Assemblercode eines laufenden Programms ändern?

Was sind die Vorteile des Logical Volume Managers