x64架構下Linux系統函數調用

原文鏈接: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

五、 參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章