x64架構下Linux系統函式呼叫

orlion發表於2020-12-16

原文連結:https://blog.fanscore.cn/p/27/

一、 函式呼叫相關指令

關於棧可以看下我之前的這篇文章x86 CPU與IA-32架構

在開始函式呼叫約定之前我們需要先了解一下幾個相關的指令

1.1 push

pushq 立即數 # q/l是字尾,表示操作物件的大小
pushl 暫存器

push指令將資料壓棧。具體就是將esp(stack pointer)暫存器減去壓棧資料的大小,再將資料儲存到esp暫存器所指向的地址。

1.2 pop

popq 暫存器
popl 暫存器

pop指令將資料出棧並寫入暫存器。具體就是將資料從esp暫存器所指向的地址載入到指令的目標暫存器中,再將esp暫存器加上出棧的資料的大小。

1.3 call

call 立即數
call 暫存器
call 記憶體

call指令會呼叫由運算元所代表的地址指向的函式,一般都是call一個符號。call指令會將當前指令暫存器中的內容(即這條call指令下一條指令的地址,也就是函式執行完的返回地址)入棧,然後跳到函式對應的地址開始執行。

1.4 ret

ret指令用於從子函式中返回,ret指令會先彈出當前棧頂的資料,這個資料就是先前呼叫這個函式的call指令壓入的“下一條指令的地址”,然後跳轉到這個地址執行。

1.5 leave

leave相當於執行了movq %rbp, %rsp; popq %rbp,即釋放棧幀。

二、 函式呼叫約定

函式呼叫約定約定了caller如何傳參即將實參放到何處,應該按照何種順序儲存,以及callee如何返回返回值即將返回值放到何處。

x86的32位機器之上C語言一般是通過棧來傳遞引數,且一般都是倒序push,即先push最後一個引數再push倒數第二個引數,並通過ax暫存器返回結果,這稱為cdecl呼叫約定(C有三種呼叫約定,linux系統中使用cdecl),Go與之類似但是區別在於Go通過棧來返回結果,所以Go支援多個返回值。

x64架構中增加了8個通用暫存器,C語言採用了暫存器來傳遞引數,如果引數超過。在x64系統預設有System V AMD64Microsoft x64兩種C語言函式呼叫約定,System V AMD64實際是System V AMD64 ABI文件的一部分,類UNIX系統多采用System V的呼叫約定。

System V AMD64 ABI文件地址https://software.intel.com/sites/default/files/article/402129/mpx-linux64-abi.pdf

本文主要討論x64架構下Linux系統的函式呼叫約定即System V AMD64呼叫約定。

三、 x64架構下Linux系統函式呼叫

3.1 如何傳遞引數

System V AMD64呼叫約定規定了caller將第1-6個整型引數分別儲存到rdirsirdxrcxr8r9暫存器中,第7個及之後的整型引數從右往左倒序的壓入棧中。前8個浮點型別的引數放到xmm0-xmm7暫存器中,之後的浮點型別的引數從右往左倒序的壓入棧中。

3.2 如何返回返回值

對於整型返回值要儲存到rax暫存器中,浮點型返回值儲存到xmm0暫存器中。

3.3 棧的對齊問題

System V AMD64要求棧必須按照16位元組對齊,就是說在通過call指令呼叫目標函式之前棧頂指標即rsp指標必須是16的倍數。之所以要按照16位元組對齊是因為x64架構引入了SSE和AVX指令,這些指令要求必須從16的整數倍地址取數,為了兼顧這些指令所以就要求了16位元組對齊。

3.4 變長引數

這部分沒看懂,待後續發掘。

四、 實際案例分析

4.1 案例1

看下下面這段C程式碼

unsigned long long foo(unsigned long long param1, unsigned long long param2) {
    unsigned long long sum = param1 + param2;
    return sum;
}

int main(void) {
    unsigned long long sum = foo(8589934593, 8589934597);
    return 0;
}

uname -a: Linux xxx 3.10.0-514.26.2.el7.x86_64 #1 SMP Tue Jul 4 15:04:05 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
gcc -v: gcc 版本 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC)

轉為彙編程式碼,gcc -S call.c

    .file   "call.c"
    .text
    .globl  foo
    .type   foo, @function
foo:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -24(%rbp)
    movq    %rsi, -32(%rbp)
    movq    -32(%rbp), %rax
    movq    -24(%rbp), %rdx
    addq    %rdx, %rax
    movq    %rax, -8(%rbp)
    movq    -8(%rbp), %rax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   foo, .-foo
    .globl  main
    .type   main, @function
main:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movabsq $8589934597, %rsi
    movabsq $8589934593, %rdi
    call    foo
    movq    %rax, -8(%rbp)
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   main, .-main
    .ident  "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-39)"
    .section    .note.GNU-stack,"",@progbits

我們先看main函式的彙編程式碼,main函式中首先執行了三條指令:

pushq   %rbp # 將當前棧基底地址壓入棧中
movq    %rsp, %rbp # 將棧基底地址修改為棧頂地址
subq    $16, %rsp # 棧頂地址-16,棧擴容,這裡沒搞懂為什麼要擴容,有懂的同學歡迎評論區指點下

這三條指令是用來分配棧幀的,執行完成後棧變成下方的樣子:
image.png
繼續往下看:

movabsq $8589934597, %rsi # 先將第二個引數儲存到rsi暫存器
movabsq $8589934593, %rdi # 再將第一個引數儲存到rdi暫存器
call foo # 呼叫foo函式,這一步會將下一條指令的地址壓到棧上

執行完call foo指令後,棧的情況如下:
image.png

然後我們跳到foo函式中看下:

pushq   %rbp # 將當前棧基底地址壓入棧中
movq    %rsp, %rbp # 將棧基底地址修改為棧頂地址

開頭仍然是建立棧幀的指令,執行完成後,此時棧幀的樣子如下:
image.png

繼續往下看:

movq    %rdi, -24(%rbp)
movq    %rsi, -32(%rbp)
movq    -32(%rbp), %rax # 將第二個引數儲存到rax暫存器
movq    -24(%rbp), %rdx # 將第一個引數儲存到rdx暫存器
addq    %rdx, %rax # 執行加法並將結果儲存在rax暫存器
movq    %rax, -8(%rbp) 
movq    -8(%rbp), %rax # 將返回值儲存到rax暫存器

這裡沒搞懂為什麼需要先挪到記憶體中再儲存到rax暫存器上,可能是編譯器實現起來比較方便吧,有懂的同學歡迎評論區指點下

此時棧情況:
image.png
foo函式最後執行了以下兩條指令:

popq    %rbp # 將棧頂值pop出來儲存到rbp暫存器,即修改棧基底地址為當前棧頂值,同時棧頂指標-8
ret # 從子函式中返回到main函式中

最終結果如圖:
image.png

4.2 案例2

我們修改下函式foo,使它接收9個引數驗證下上面的理論。

unsigned long long foo(unsigned long long param1, unsigned long long param2, unsigned long long param3, unsigned long long param4, unsigned long long param5, unsigned long long param6, unsigned long long param7, unsigned long long param8, unsigned long long param9) {
    unsigned long long sum = param1 + param2;
    return sum;
}

int main(void) {
    unsigned long long sum = foo(8589934593, 8589934597, 3, 4,5,6,7,8,9);
    return 0;
}

編譯為彙編後:

foo:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -24(%rbp)
    movq    %rsi, -32(%rbp)
    movq    %rdx, -40(%rbp)
    movq    %rcx, -48(%rbp)
    movq    %r8, -56(%rbp)
    movq    %r9, -64(%rbp)
    movq    -32(%rbp), %rax
    movq    -24(%rbp), %rdx
    addq    %rdx, %rax
    movq    %rax, -8(%rbp)
    movq    -8(%rbp), %rax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   foo, .-foo
    .globl  main
    .type   main, @function
main:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $40, %rsp
    movq    $9, 16(%rsp) # 後6個引數放到棧上
    movq    $8, 8(%rsp)
    movq    $7, (%rsp)
    movl    $6, %r9d # 前6個引數分別使用rdi rsi rdx ecx r8 r9暫存器
    movl    $5, %r8d
    movl    $4, %ecx
    movl    $3, %edx
    movabsq $8589934597, %rsi
    movabsq $8589934593, %rdi 
    call    foo
    movq    %rax, -8(%rbp)
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret

五、 參考資料

相關文章