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.