Esempio di programma x86-64

Aug 31 2020

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

3 NateEldredge Aug 31 2020 at 21:40

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 di rspè 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.)