一覽
本文目的
之前,我本想觀察函數調用時,棧的細節。順便驗證一下,口口相傳的,C/C++從右向左入棧。詳情參考之前的文章:C++函數調用棧細節(gdb調試)結果發現了些出乎意料的情況。
在x86-64 gcc 5.4.0 沒有優化選項時
代碼:
1 #include<stdio.h>
2
3 class A {
4 public:
5 int f;
6 int s;
7 };
8
9 int sum(int l,int r)
10 {
11 int res =0;
12 res = l+r;
13 return res;
14 }
15 int main ()
16 {
17 A a;
18 a.f = 0x55;
19 a.s = 0x66;
20 int res = sum(a.f,a.s);
21 printf("sum: %d",res);
22 return 0;
23 }
出現了:
- 調用函數前參數沒有入棧,而是按照從右向左的順序存放在了寄存器。
- 進入調用函數後,參數從寄存器存放進了棧中,但是按照從左到右的順序。
- 進入調用函數後rsp棧頂指針沒有增長,第2條裏的參數存放在了棧頂之外。
以上三條可以在C++函數調用棧細節(gdb調試)中看到調試的具體情況。
x86-64下打印參數地址判斷參數入棧順序的做法不合理
之前看過很多判斷參數入棧順序,通過打印參數的地址,根據右邊參數爲高地址,左邊參數爲低地址,就得出參數是從右向左入棧。但是在x86-64下,這樣行不通。
1 #include<stdio.h>
2
3 void foo(int x, int y)
4 {
5 printf("x = %d at [%X]\n", x, &x);
6 printf("y = %d at [%X]\n", y, &y);
7 }
8
9 int main ()
10 {
11 foo(100,300);
12 return 0;
13 }
不添加優化選項編譯
結果:
左邊參數地址高於右邊參數地址。
難道這就說明了 參數從左向右入棧的?有些偏頗。
情況1:函數調用參數從右向左存放到寄存器,又從左到右存放到棧
0000000000400526 <foo>:
400526: 55 push %rbp
400527: 48 89 e5 mov %rsp,%rbp
40052a: 48 83 ec 10 sub $0x10,%rsp 棧頂有增長
40052e: 89 7d fc mov %edi,-0x4(%rbp) edi即左邊參數100,先存放在高地址(靠近棧底)
400531: 89 75 f8 mov %esi,-0x8(%rbp) esi即右邊參數300,後存放在低地址
400534: 8b 45 fc mov -0x4(%rbp),%eax
400537: 48 8d 55 fc lea -0x4(%rbp),%rdx
40053b: 89 c6 mov %eax,%esi
40053d: bf 14 06 40 00 mov $0x400614,%edi
400542: b8 00 00 00 00 mov $0x0,%eax
400547: e8 b4 fe ff ff callq 400400 <printf@plt>
40054c: 8b 45 f8 mov -0x8(%rbp),%eax
40054f: 48 8d 55 f8 lea -0x8(%rbp),%rdx
400553: 89 c6 mov %eax,%esi
400555: bf 24 06 40 00 mov $0x400624,%edi
40055a: b8 00 00 00 00 mov $0x0,%eax
40055f: e8 9c fe ff ff callq 400400 <printf@plt>
400564: 90 nop
400565: c9 leaveq
400566: c3 retq
0000000000400567 <main>:
400567: 55 push %rbp
400568: 48 89 e5 mov %rsp,%rbp
40056b: be 2c 01 00 00 mov $0x12c,%esi 先是右邊參數0x12c 即300存放在esi內
400570: bf 64 00 00 00 mov $0x64,%edi 然後左邊參數0x64 即100存放在edi內
400575: e8 ac ff ff ff callq 400526 <foo>
40057a: b8 00 00 00 00 mov $0x0,%eax
40057f: 5d pop %rbp
400580: c3 retq
400581: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
400588: 00 00 00
40058b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
這裏發現函數調用前,函數的參數是先按照從右向左的順序放入寄存器中,而沒有采取直接入棧的策略。
雖然後來是從左向右存放在棧中,但是要知道從右向左是爲了支持C++可變參數的特性。而開始從右向左放入寄存器,就已經支持了這個特性。後面在函數內從寄存器搬移再存放順序已經不影響了。
不過在foo內棧頂有增長了,這與我前一篇觀察到的情況不同。懷疑因爲foo內部又調用了printf,所以棧頂增長。參數存放在棧頂內。我們接下來嘗試函數內部沒有調用其他函數的情況。
情況2:棧頂不增長,參數存放在棧頂之外
foo內不再調用其他函數
1 #include<stdio.h>
2
3 void foo(int x, int y)
4 {
5 int z = x+y;
6 }
7
8 int main ()
9 {
10 foo(100,300);
11 return 0;
12 }
void foo(int x, int y)
{
4004d6: 55 push %rbp
4004d7: 48 89 e5 mov %rsp,%rbp rsp棧頂指針沒有提前增長?
4004da: 89 7d ec mov %edi,-0x14(%rbp)
4004dd: 89 75 e8 mov %esi,-0x18(%rbp)
int z = x+y;
4004e0: 8b 55 ec mov -0x14(%rbp),%edx
4004e3: 8b 45 e8 mov -0x18(%rbp),%eax
4004e6: 01 d0 add %edx,%eax
4004e8: 89 45 fc mov %eax,-0x4(%rbp)
}
4004eb: 90 nop
4004ec: 5d pop %rbp
4004ed: c3 retq
這裏rsp沒有提前增長,但是後面
4004da: 89 7d ec mov %edi,-0x14(%rbp)
4004dd: 89 75 e8 mov %esi,-0x18(%rbp)
rsp是不是會在這兩步增長呢?我們用gdb調試觀察一下。
執行前,rbp和rsp保持一致。
執行mov -0x14(%rbp),%edx後,rsp沒有增長 參數被存放在棧頂之外。
執行完 mov -0x18(%rbp),%eax,棧頂依舊沒有增長
後來我從淘寶的這篇博客X86-64寄存器和棧幀,(原鏈接無法訪問,因此是這裏貼出百度文庫的鏈接)讀到:
確實,這種手段減少了rsp的操作。不過要注意,這種情況不光是上圖所說的128字節的範圍,而且根據我的實驗,還要保證函數內部參數不能傳入其他函數。
參數多於寄存器數量,參數會入棧
雖然之前觀察到,參數從右向左存入寄存器。但是因此就說這種手段代替了以前的從右向左入棧的方式,似乎不能說服自己。我又看到了知乎的這篇x86-64 下函數調用及棧幀原理發現存放函數參數的寄存器是有限的,當參數超過寄存器的數量,超出部分的函數參數還是會從右向左入棧。
並根據寄存器的Caller Save” 和 ”Callee Save決定是調用者把這些參數入棧,還是被調用者入棧。這其中的詳情可以仔細看一下知乎中的那篇。
測試
代碼更改如下:
1#include<stdio.h>
2
3 void foo(int x, int y,int z,int j,int k,int l,int a,int b,int c)
4 {
5 int res = x+y+j+k+a+b+z+c+l;
6
7 }
8
9 int main ()
10 {
11 foo(100,200,300,400,500,600,700,800,900);
12 return 0;
13 }
彙編如下:
void foo(int x, int y,int z,int j,int k,int l,int a,int b,int c)
{
4004d6: 55 push %rbp
4004d7: 48 89 e5 mov %rsp,%rbp
rsp沒有增長,之前入棧的三個參數應該是由main棧管理
4004da: 89 7d ec mov %edi,-0x14(%rbp)
4004dd: 89 75 e8 mov %esi,-0x18(%rbp)
4004e0: 89 55 e4 mov %edx,-0x1c(%rbp)
4004e3: 89 4d e0 mov %ecx,-0x20(%rbp)
4004e6: 44 89 45 dc mov %r8d,-0x24(%rbp)
4004ea: 44 89 4d d8 mov %r9d,-0x28(%rbp)
int res = x+y+j+k+a+b+z+c+l;
4004ee: 8b 55 ec mov -0x14(%rbp),%edx
4004f1: 8b 45 e8 mov -0x18(%rbp),%eax
4004f4: 01 c2 add %eax,%edx
4004f6: 8b 45 e0 mov -0x20(%rbp),%eax
4004f9: 01 c2 add %eax,%edx
4004fb: 8b 45 dc mov -0x24(%rbp),%eax
4004fe: 01 c2 add %eax,%edx
從下面可以看出,那三個參數確實在main棧上,mov 0x10(%rbp),%eax
因爲從當前rbp向高地址方向的16字節處,也就是棧底以下的16字節處取得參數的值。
400500: 8b 45 10 mov 0x10(%rbp),%eax
400503: 01 c2 add %eax,%edx
400505: 8b 45 18 mov 0x18(%rbp),%eax
400508: 01 c2 add %eax,%edx
40050a: 8b 45 e4 mov -0x1c(%rbp),%eax
40050d: 01 c2 add %eax,%edx
40050f: 8b 45 20 mov 0x20(%rbp),%eax
400512: 01 c2 add %eax,%edx
400514: 8b 45 d8 mov -0x28(%rbp),%eax
400517: 01 d0 add %edx,%eax
400519: 89 45 fc mov %eax,-0x4(%rbp)
}
40051c: 90 nop
40051d: 5d pop %rbp
40051e: c3 retq
000000000040051f <main>:
int main ()
{
40051f: 55 push %rbp
400520: 48 89 e5 mov %rsp,%rbp
foo(100,200,300,400,500,600,700,800,900);
#這裏三個參數0x384,0x320,0x2bc即900,800,700從右向左執行pushq入棧!
400523: 68 84 03 00 00 pushq $0x384
400528: 68 20 03 00 00 pushq $0x320
40052d: 68 bc 02 00 00 pushq $0x2bc
其餘參數剛好放入6個寄存器
400532: 41 b9 58 02 00 00 mov $0x258,%r9d
400538: 41 b8 f4 01 00 00 mov $0x1f4,%r8d
40053e: b9 90 01 00 00 mov $0x190,%ecx
400543: ba 2c 01 00 00 mov $0x12c,%edx
400548: be c8 00 00 00 mov $0xc8,%esi
40054d: bf 64 00 00 00 mov $0x64,%edi
400552: e8 7f ff ff ff callq 4004d6 <foo>
400557: 48 83 c4 18 add $0x18,%rsp
return 0;
40055b: b8 00 00 00 00 mov $0x0,%eax
}
400560: c9 leaveq
400561: c3 retq
400562: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
400569: 00 00 00
40056c: 0f 1f 40 00 nopl 0x0(%rax)
總結
- x86-64下的gcc在參數小於等於6個時,在函數調用前會用寄存器按照從右到左的順序保存參數,在函數調用後,從左到右放入棧中。
- 超過6個時,超出的部分會從右向左入棧。
- 函數內部沒有調用其他函數且需要入棧的少於128字節時,棧頂不會增長,數據被存放在棧頂之外。
尾語
上面這些,基本解決了我遇到的疑惑,不過還能往更深追究,但是對於函數調用時,棧發生的事情來說,作爲了解已經足夠了。
以上