1. 程式人生 > >MSVC X64 函式中的 RSP, RBP 和 Calling Convention

MSVC X64 函式中的 RSP, RBP 和 Calling Convention

上一篇 博文提到了 X64 下 MSVC 如何傳遞引數,但是沒有涉及到當引數個數大於 4 的時候如何分配記憶體空間的問題,接下來我們來探究這個問題。

RSP 和 RBP

按照上面提到的博文,我們進行如下實驗:

  1. 所有引數都是 struct Arg, 並且 sizeof(Arg) == 16
  2. 實驗中,引數的個數 k = 6, 7, 8,先觀察彙編後 rsprbp 暫存器的變化。

實驗:
1. k = 6

; 28   : void Call() {

$LN3:
00000   40 55        push    rbp
00002   56       push    rsi
00003
57 push rdi 00004 48 81 ec f0 03 00 00 sub rsp, 1008 ; 000003f0H 0000b 48 8d 6c 24 30 lea rbp, QWORD PTR [rsp+48] 00010 48 8b fc mov rdi, rsp
  1. k = 7
; 28   : void Call() {
$LN3:
    00000   40 55        push    rbp
    00002   56       push    rsi
    00003   57       push    rdi
    00004   48
81 ec 60 04 00 00 sub rsp, 1120 ; 00000460H 0000b 48 8d 6c 24 40 lea rbp, QWORD PTR [rsp+64] 00010 48 8b fc mov rdi, rsp
  1. k = 8
; 28   : void CallFuck() {

$LN3:
    00000   40 55        push    rbp
    00002   56       push    rsi
    00003   57       push    rdi
    00004   48 81 ec c0 04
00 00 sub rsp, 1216 ; 000004c0H 0000b 48 8d 6c 24 40 lea rbp, QWORD PTR [rsp+64]

這裡我們需要明確一點,就是 rbp 的意義和 rsp 的意義是什麼。
rbp 是屬於當前函式的棧空間基地址,rsp 是包含當前函式為被呼叫函式準備的棧空間的基地址。

這點可以在彙編程式碼中看出來。

我們可以看出,當 k=6 的時候,MSVC 利用 lea rbp, QWORD PTR [rsp+48] 使得 rbp == rsp + 48
k = 7, 8 時 rbp == rsp + 64
根據 X64 下的傳參規則,當 sizeof(struct) 不為 8, 16, 32, 64 bits 時,將指向引數本體的指標放在對應的位置,因此,一個引數(這裡指這個指標,64 bits = 8 bytes)在棧上所佔用的記憶體應該為 M = 8 * (#arg - 4)
所以, k = 6, M = 16; k = 7, M = 24; k = 8, M = 32; 再加上分配的 32 bytes 影子空間,那麼應該是

k = 6, rbp == rsp + 48
k = 7, rbp == rsp + 56
k = 8, rbp == rsp + 64·

實際情況呢,k = 7 我們和編譯器結果不一樣,實際情況是 rbp == rsp + 64,原因在於

為這些聚合型別作為指標的傳遞 (包括__m128),呼叫方分配的臨時記憶體將是 16 位元組對齊。

因此這些指標雖然單個大小是 8,所以在奇數個的時候為了對齊要額外增加 8 !

Calling Convention

根據這個簡單的呼叫約定,我們可以畫出64位下函式呼叫時到底是個怎麼樣的記憶體結構。(時機應該是 call 指令執行完畢)
這裡寫圖片描述

這張圖片認真理解,就解決了所有呼叫時對於記憶體空間如何分配的疑問。

See also:

本篇基於實驗得出,除了某些叫法不同,和微軟的官方文件是一致的。