Örnek x86-64 programı

Aug 31 2020

Bir işlevi çağırırken ve ardından işletim sisteminden çıkarken doğru uygulamaları izlediğimden emin olmak için aşağıdaki x86 programını yazdım:

.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

Yukarıdakiler x86-64 kurallarına uygun mu? Muhtemelen geldiği kadar basit olduğunu biliyorum, ama burada ne geliştirilebilir?

Yanıtlar

3 NateEldredge Aug 31 2020 at 21:40

Bu sorunun SO versiyonu hakkında aldığınız bazı yorumları detaylandırmak için, kaçırdığınız ana şey , yeni başlayanlar tarafından genellikle gözden kaçan SysV ABI çağrı kurallarının bir gereği olan yığın hizalamasıdır .

Gereksinim (ABI 3.2.2):

Giriş bağımsız değişken alanının sonu, 16 ( yığın üzerinde iletilirse __m256veya __m512iletilirse 32 veya 64 ) bayt sınırı üzerinde hizalanmalıdır .

Bu, bir calltalimatı yürütmeden hemen önce , yığın işaretçisinin %rsp16'nın katı olması gerektiği anlamına gelir . Sizin durumunuzda , iki çağrınız arasında push8 baytlık bir popdeğeriniz olduğundan multiply, ikisinde birden doğru hizalama.

Burada bazı kırışıklıklar, ana işlevinizin _startyerine mainveya C kodu ile adlandırılan başka bir işlev olması nedeniyle ortaya çıkar :

  • _startAlanına giriş koşulları ABI 3.4'te açıklanmıştır. Özellikle, yığın _startkontrol anında 16 bayta hizalanır . Ayrıca, geri dönemeyeceğiniz için _start(yığında dönüş adresi yoktur), yaptığınız gibi bir sistem çağrısıyla çıkmanız gerekir ve böylece arayan için herhangi bir kayıt kaydetmenize gerek kalmaz.

  • İçin mainbaşka bir fonksiyon ya, yığın 16 bayt hizalanmış olurdu önce sizin işlevi çağrıldı iade adresi ortalama fazladan 8 bayt fonksiyonunuza girişinde, yığın artık "yanlış hizalanmış" böylece yani, değerini rsp8, 16'nın bir katından daha fazla veya daha azdır. (Biri normalde yığını yalnızca 8 baytlık artışlarla değiştireceğinden, gerçekten her zaman iki olası durumda, ben buna "hizalı" ve "yanlış hizalanmış" diyeceğim.) , bu tür işlevlerde, aranan cihazda kaydedilen kayıtların içeriğini korumanız gerekir %rbx, %rbp, %r12-r15.

Bu durumda, ilk çağrınız multiplydoğru yığın hizalamasına sahip, ancak ikinciniz yok. Tabii ki, bu durumda bu sadece akademik ilgi, çünkü multiplyyığın hizalaması gerektiren hiçbir şey yapmaz (yığını hiç kullanmaz), ancak doğru yapmak iyi bir uygulamadır.

Bunu düzeltmenin bir yolu , herhangi bir rastgele 64-bit yazmaçla sub $8, %rspveya (daha verimli bir şekilde) ikinci çağrıdan önce yığın göstericisinden 8 bayt daha çıkarmaktır push. Ama neden bu değeri kurtarmak için yığını kullanmalıyız? Saklaması gerektiğini %rbxbildiğimiz , aranan uçta kaydedilen kayıtlardan birine koyabiliriz multiply. Normalde bu, bu kütüğün içeriğini kaydetmemizi ve geri _startyüklememizi gerektirir, ancak varlığın özel durumunda olduğumuz için , buna gerek yoktur.


Ayrı bir yorum, mov $7, %rdi64 bitlik kayıtlarda nerede çalıştığınız gibi birçok talimatınızın olmasıdır . Bunu olarak yazmak daha iyi olur mov $7, %edi. 32 bitlik bir sicile her yazmanın, karşılık gelen 64 bitlik yazmacın üst yarısını sıfırlayacağını hatırlayın , bu nedenle sabitiniz işaretsiz 32 bit olduğu sürece efekt aynıdır ve kodlaması mov $7, %edibir bayt daha kısadır. REX önekine gerek yok.

Bu yüzden kodunuzu şu şekilde gözden geçirirdim:

.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

multiply32 bit sığdırmanın sonucuna güvenmek istiyorsanız , bir bayt tasarruf etmek için mov %rax, %rbxile değiştirebilirsiniz mov %eax, %ebx. Ve benzer şekilde, "İkisini bir araya topla", iki bayt daha kaydetmek için 32-bit komutları kullanabilir.

Son olarak, gibi, AT & T sözdizimi işlenen boyutu ekleri kullanmak konusunda bir stil bir nokta vardır addqkarşı add. Bir işlenen bir yazmaç olduğunda, bunlar isteğe bağlıdır, çünkü işlenen boyutu bu yazmacın boyutundan çıkarılabilir (örneğin 32 bit için %eax, 64 bit için %rax, vb.). Kişisel tercihim onları her zaman kullanmaktır, gerçekten ne demek istediğinizi gerçekten yazdığınızı biraz daha doğrulamak için, ancak (çoğunlukla) yaptığınız gibi onları atlamak da yaygın ve iyidir; sadece tutarlı olun. movq $60, %raxİhtiyaç duyulmadığına dair bir örneğiniz vardı, bu yüzden tutarlılık için orada son eki atladım. (Ayrıca %eaxyukarıda belirtilen nedenlerden dolayı olarak değiştirdim .)