x64架構下Linux系統函數調用

原文鏈接://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文檔地址//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

五、 參考資料

Tags: