Esempio di programma x86-64
Ho scritto il seguente programma x86 per assicurarmi di seguire le pratiche corrette nel chiamare una funzione e quindi uscire dal sistema operativo:
.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
Quanto sopra segue correttamente le convenzioni x86-64? So che probabilmente è così semplice come viene, ma cosa può essere migliorato qui?
Risposte
Per elaborare alcuni commenti che hai ottenuto sulla versione SO di questa domanda, la cosa principale che ti manca è l' allineamento dello stack , un requisito delle convenzioni di chiamata SysV ABI che spesso viene trascurato dai principianti.
Il requisito è (ABI 3.2.2):
La fine dell'area dell'argomento di input deve essere allineata su un confine di 16 byte (32 o 64, se
__m256o__m512viene passato allo stack).
Quindi questo significa che, nell'istante prima di eseguire callun'istruzione, il puntatore allo stack %rspdeve essere un multiplo di 16. Nel tuo caso hai un pushdi 8 byte senza un poptra le tue due chiamate a multiply, quindi non possono avere entrambi allineamento corretto.
Alcune rughe sono introdotte qui dal fatto che la tua funzione genitore è _startinvece di maino un'altra funzione chiamata dal codice C:
Le condizioni di ingresso a
_startsono descritte al punto 3.4 dell'ABI. In particolare, lo stack è allineato a 16 byte nell'istante in cui_startottiene il controllo. Inoltre, poiché non puoi tornare da_start(non c'è un indirizzo di ritorno nello stack), devi uscire con una chiamata di sistema come fai, e quindi non è necessario salvare alcun registro per il chiamante.Per
maino per qualsiasi altra funzione, lo stack sarebbe stato allineato a 16 byte prima che la funzione fosse chiamata, quindi gli 8 byte aggiuntivi per l'indirizzo di ritorno indicano che all'ingresso della funzione, lo stack è ora "disallineato", ovvero il valore dirspè 8 in più o in meno di un multiplo di 16. (Dato che normalmente si manipola lo stack solo con incrementi di 8 byte, è sempre e solo in due stati possibili, che chiamerò "allineato" e "disallineato".) Inoltre , in tali funzioni, è necessario preservare il contenuto dei registri salvati dal chiamato%rbx, %rbp, %r12-r15.
Così com'è, la tua prima chiamata a multiplyha il corretto allineamento dello stack, ma la tua seconda no. Ovviamente, in questo caso è solo di interesse accademico, perché multiplynon fa nulla che abbia bisogno di allineamento dello stack (non usa nemmeno lo stack), ma è buona pratica farlo bene.
Un modo per risolverlo sarebbe sottrarre altri 8 byte dal puntatore dello stack prima della seconda chiamata, con sub $8, %rspo (in modo più efficiente) semplicemente pushinserendo un registro casuale a 64 bit. Ma perché dovremmo preoccuparci di usare lo stack per salvare questo valore? Potremmo semplicemente metterlo in uno dei registri salvati dalle chiamate, diciamo %rbx, che sappiamo di multiplydover preservare. Normalmente questo ci richiederebbe di salvare e ripristinare il contenuto di questo registro, ma poiché siamo nel caso speciale dell'essere _start, non dobbiamo farlo.
Un commento a parte è che hai molte istruzioni come mov $7, %rdidove operi su registri a 64 bit. Sarebbe meglio scrivere come mov $7, %edi. Ricorda che ogni scrittura su un registro a 32 bit azzererà la metà superiore del corrispondente registro a 64 bit , quindi l'effetto è lo stesso fintanto che la tua costante è 32 bit senza segno e la codifica di mov $7, %ediè un byte più corta di quanto non lo sia non serve un prefisso REX.
Quindi rivederei il tuo codice come
.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
Se vuoi fare affidamento sul risultato multiplydell'adattamento a 32 bit, puoi sostituire mov %rax, %rbxcon mov %eax, %ebxper salvare un byte. Allo stesso modo, "Aggiungi i due insieme" potrebbe utilizzare invece istruzioni a 32 bit per salvare altri due byte.
Infine, c'è un punto stilistico sull'opportunità di utilizzare i suffissi delle dimensioni dell'operando della sintassi AT & T, come addqversus add. Sono opzionali quando un operando è un registro, poiché la dimensione dell'operando può essere dedotta dalla dimensione di quel registro (es. 32 bit per %eax, 64 bit per %rax, ecc.). La mia preferenza personale è di usarli sempre, come una piccola verifica in più che stai davvero scrivendo ciò che intendi, ma ometterli come hai fatto (principalmente) è anche comune e va bene; sii coerente. Hai avuto un'istanza in movq $60, %raxcui non era necessario, quindi per coerenza ho omesso il suffisso lì. (L'ho anche cambiato in %eaxper i motivi sopra indicati.)