Beispiel x86-64 Programm
Ich habe das folgende x86-Programm geschrieben, um sicherzustellen, dass ich beim Aufrufen einer Funktion und beim Beenden des Betriebssystems die richtigen Vorgehensweisen befolge:
.globl _start
_start:
# Calculate 2*3 + 7*9 = 6 + 63 = 69
# The multiplication will be done with a separate function call
# Parameters passed in System V ABI
# The first 6 integer/pointer arguments are passed in:
# %rdi, %rsi, %rdx, %rcx, %r8, and %r9
# The return value is passed in %rax
# multiply(2, 3)
# Part 1 --> Call the parameters
mov $2, %rdi mov $3, %rsi
# Part 2 --> Call the function (`push` return address onto stack and `jmp` to function label)
call multiply
# Part 3 --> Handle the return value from %rax (here we'll just push it to the stack as a test)
push %rax
# multiply(7, 9)
mov $7, %rdi mov $9, %rsi
call multiply
# Add the two together
# Restore from stack onto rdi for the first function
pop %rdi
# The previous value from multiply(7,9) is already in rax, so just add to rbx
add %rax, %rdi
# for the 64-bit calling convention, do syscall instead of int 0x80
# use %rdi instead of %rbx for the exit arg
# use $60 instead of 1 for the exit code movq $60, %rax # use the `_exit` [fast] syscall
# rdi contains out exit code
syscall # make syscall
multiply:
mov %rdi, %rax
imul %rsi, %rax
ret
Entspricht das oben Gesagte den x86-64-Konventionen ordnungsgemäß? Ich weiß, dass es wahrscheinlich so einfach ist wie es kommt, aber was kann hier verbessert werden?
Antworten
Um näher auf einige Kommentare , die Sie auf der SO - Version dieser Frage bekam, Hauptsache du bist fehlt , ist Stack Ausrichtung , eine Anforderung an die SysV ABI Aufrufkonventionen , die oft wird von Anfängern übersehen.
Die Anforderung ist (ABI 3.2.2):
Das Ende des Eingabeargumentbereichs muss an einer 16- Byte-Grenze (32 oder 64, wenn
__m256oder__m512auf einem Stapel übergeben wird) ausgerichtet sein.
Das bedeutet, dass callder Stapelzeiger zu dem Zeitpunkt, bevor Sie eine Anweisung ausführen %rsp, ein Vielfaches von 16 sein muss. In Ihrem Fall haben Sie push8 Bytes ohne ein popZwischenzeichen zwischen Ihren beiden Aufrufen an multiply, sodass nicht beide Bytes vorhanden sind korrekte Ausrichtung.
Einige Falten werden hier durch die Tatsache eingeführt, dass Ihre übergeordnete Funktion _startanstelle einer mainoder einer anderen Funktion ist, die vom C-Code aufgerufen wird:
Die Bedingungen für die Einreise in
_startsind in 3.4 des ABI beschrieben. Insbesondere wird der Stapel zu dem Zeitpunkt, an dem er die_startKontrolle erhält, auf 16 Bytes ausgerichtet . Da Sie nicht von zurückkehren können_start(es gibt keine Rücksprungadresse auf dem Stapel), müssen Sie wie gewohnt mit einem Systemaufruf beenden, sodass keine Register für den Aufrufer gespeichert werden müssen.Für
mainoder eine andere Funktion wäre der Stapel vor dem Aufruf Ihrer Funktion auf 16 Byte ausgerichtet worden. Die zusätzlichen 8 Byte für die Rücksprungadresse bedeuten also, dass der Stapel beim Eintritt in Ihre Funktion jetzt "falsch ausgerichtet" ist, dh der Wert vonrspist 8 mehr oder weniger als ein Vielfaches von 16. (Da man den Stapel normalerweise nur in 8-Byte-Schritten manipulieren würde, ist es wirklich immer nur in zwei möglichen Zuständen, die ich "ausgerichtet" und "falsch ausgerichtet" nennen werde.) Auch In solchen Funktionen müssten Sie den Inhalt der von Angerufenen gespeicherten Register beibehalten%rbx, %rbp, %r12-r15.
So wie es aussieht, hat Ihr erster Aufruf von multiplyeine korrekte Stapelausrichtung, Ihr zweiter jedoch nicht. Natürlich ist es in diesem Fall nur von akademischem Interesse, weil multiplyes nichts tut, was eine Stapelausrichtung erfordert (es verwendet den Stapel überhaupt nicht), aber es ist eine gute Praxis, es richtig zu machen.
Eine Möglichkeit, dies zu beheben, besteht darin, vor dem zweiten Aufruf weitere 8 Bytes vom Stapelzeiger zu subtrahieren, entweder mit sub $8, %rspoder (effizienter) durch einfaches pushZufügen eines beliebigen 64-Bit-Registers. Aber warum sollten wir uns überhaupt die Mühe machen, den Stapel zu verwenden, um diesen Wert zu speichern? Wir könnten es einfach in eines der von Angerufenen gespeicherten Register schreiben %rbx, von denen wir wissen, dass sie aufbewahrt werden multiplymüssen. Normalerweise müssten wir den Inhalt dieses Registers speichern und wiederherstellen, aber da wir uns im Sonderfall befinden _start, müssen wir das nicht.
Ein separater Kommentar ist, dass Sie viele Anweisungen haben, z. B. mov $7, %rdiwo Sie mit 64-Bit-Registern arbeiten. Dies wäre besser zu schreiben als mov $7, %edi. Denken Sie daran, dass bei jedem Schreibvorgang in ein 32-Bit-Register die obere Hälfte des entsprechenden 64-Bit-Registers auf Null gesetzt wird. Der Effekt ist also der gleiche, solange Ihre Konstante 32 Bit ohne Vorzeichen ist und die Codierung von mov $7, %ediein Byte kürzer ist als dies nicht der Fall ist Ich brauche kein REX-Präfix.
Also würde ich Ihren Code als überarbeiten
.globl _start
_start:
# Calculate 2*3 + 7*9 = 6 + 63 = 69
# The multiplication will be done with a separate function call
# Parameters passed in System V ABI
# The first 6 integer/pointer arguments are passed in:
# %rdi, %rsi, %rdx, %rcx, %r8, and %r9
# The return value is passed in %rax
# multiply(2, 3)
# Part 1 --> Load the parameters
mov $2, %edi mov $3, %esi
# Part 2 --> Call the function (`push` return address onto stack and `jmp` to function label)
call multiply
# Part 3 --> Save the return value
mov %rax, %rbx # could also do mov %ebx, %eax if you know the result fits in 32 bits
# multiply(7, 9)
mov $7, %edi mov $9, %esi
call multiply
# Add the two together
add %rbx, %rax
mov %rax, %rdi
# for the 64-bit calling convention, do syscall instead of int 0x80
# use %rdi instead of %rbx for the exit arg
# use $60 instead of 1 for the exit code mov $60, %eax # use the `_exit` [fast] syscall
# rdi contains out exit code
syscall # make syscall
multiply:
mov %rdi, %rax
imul %rsi, %rax
ret
Wenn Sie auf das Ergebnis verlassen wollen von multiplyin 32 - Bit - Montage, könnten Sie ersetzen mov %rax, %rbxmit mov %eax, %ebxeinem Byte zu speichern. Ebenso könnte das "Add the two together" stattdessen 32-Bit-Anweisungen verwenden, um zwei weitere Bytes zu speichern.
Schließlich gibt es einen stilistischen Punkt, ob die Suffixe für die Operandengröße der AT & T-Syntax wie addqVersus verwendet werden sollen add. Sie sind optional, wenn ein Operand ein Register ist, da die Operandengröße aus der Größe dieses Registers abgeleitet werden kann (z. B. 32 Bit für %eax, 64 Bit für %raxusw.). Meine persönliche Präferenz ist es, sie immer zu verwenden, um zu bestätigen, dass Sie wirklich schreiben, was Sie meinen, aber es ist auch üblich und in Ordnung, sie wegzulassen, wie Sie es (meistens) getan haben. Sei einfach konsequent. Sie hatten eine Instanz, in der movq $60, %raxes nicht benötigt wurde, daher habe ich aus Gründen der Konsistenz das Suffix dort weggelassen. (Ich habe es auch %eaxaus den oben genannten Gründen geändert .)