programma x86 per eseguire un esponente

Sep 11 2020

Per continuità, la mia ultima domanda era con l'aggiunta di numeri insieme a una chiamata di funzione: Esempio di programma x86-64 . In questo ho cercato di applicare le lezioni indicate nella risposta precedente (allineamento dello stack, utilizzando edxinvece di rdxdove si adatteranno i risultati). Ho provato a commentare in linea con il codice:

# We are going to calculate 7^2 + 2^4 = 49 + 16 = 65
# The exponent function supports whole numbers (long) >= 0

.section .data
base_1: .long   7
exp_1:  .long   2
base_2: .long   2
exp_2:  .long   4

.section .text
.globl _start
_start:


    # We will do the first function call, 7^2
    mov base_1(%rip),   %edi
    mov exp_1(%rip),    %esi
    call exp

    # needs to be 16-byte aligned before the next function call so do two pushes
    pushq $0
    pushq %rax

    # Now do the second function call
    mov base_2(%rip),   %edi
    mov exp_2(%rip),    %esi
    call exp

    # We have the return value in %eax so let's add this with our previous function's value
    popq %rdi
    
    add %eax,   %edi
    mov $60,    %eax
    syscall

exp:
    # Initialize %eax to 1
    mov $1,     %eax

exp_op:
    cmp $0,     %esi
    je exp_ret
    imul %edi,  %eax
    dec %esi
    jmp exp_op

exp_ret:
    ret

Ho alcune domande specifiche sul codice:

  • Sta facendo un raw push/ popcomune? O è la convenzione sempre di fare push rbp mov rsp rbp... pop rbp. Perché un metodo è preferito rispetto all'altro?
  • E se volessi memorizzare il risultato del primo calcolo in un registro: ci sono registri di cui è garantito il mantenimento tra le chiamate di funzione? O è per questo che il push...popmetodo viene utilizzato così tanto?
  • Infine, il mio expsembra ok? Sembra un po 'strano avere tre etichette in quello che equivale a una funzione. Come potrebbe essere migliorato?

Risposte

3 Quuxplusone Sep 11 2020 at 13:45

Non sono un esperto, ma che diamine, ecco un commento sulla recensione:

# needs to be 16-byte aligned before the next function call so do two pushes
pushq $0
pushq %rax

Questo commento è buono, ma lo riformulerei. Due push di otto byte fanno 16 byte e non cambiano l'allineamento dello stack. Quindi deduco che una di queste spinte sia significativa e l'altra insignificante , ma il tuo commento non mi dice quale sia! Quindi potresti dire invece

# one extra push to preserve 16-byte stack alignment
pushq $0

# push the result of `exp`
pushq %rax

È possibile ridurre il codice generato eliminando la costante insignificante $0:

# push the result of `exp`, plus one extra push to preserve 16-byte stack alignment
pushq %rax
pushq %rax

Ora il lettore non ha nemmeno bisogno di capire quale spinta è quella significativa, perché entrambe le spinte fanno la stessa cosa!


Ma perché è importante preservare l'allineamento a 16 byte sulle chiamate? Non è un requisito della macchina . Sembra che tu stia cercando di seguire qualche ABI specifico , come forse per l'interoperabilità con C o C ++. La tua documentazione esterna dovrebbe essere più chiara su ciò che ABI stai cercando di seguire.

E poi, se si sta cercando di interagire con il codice C, si potrebbe migliorare il codice indicando quali delle sue etichette sono da intendersi come entry point esterni e quali sono le etichette locali appena interne. Sembra che tu abbia intenzione expdi essere chiamato da un altro codice - è un punto di ingresso - ma ad esempio exp_opnon è richiamabile, ed exp_retè tecnicamente richiamabile ma funge solo da no-op. Potresti contrassegnarli in qualche modo come "dettagli di implementazione locale, non per consumo esterno".

Sì, tecnicamente lo fai già esportando .globl _starte non .globl exp, ma c'è ancora una grande differenza tra la funzione richiamabile expe l'etichetta locale exp_opche non si riflette nel tuo schema di denominazione. Se lo stessi facendo, aggiungerei .globl expe rinominerei exp_op, exp_retin qualcosa come Lexp1, Lexp2o L1_looptop, L2_loopend.