x86-64架構下函數調用棧與32位下的不同之處


本文目的

 
  之前,我本想觀察函數調用時,棧的細節。順便驗證一下,口口相傳的,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 }

出現了:

  1. 調用函數前參數沒有入棧,而是按照從右向左的順序存放在了寄存器。
  2. 進入調用函數後,參數從寄存器存放進了棧中,但是按照從左到右的順序。
  3. 進入調用函數後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    先是右邊參數0x12c300存放在esi內
  400570:	bf 64 00 00 00       	mov    $0x64,%edi	  然後左邊參數0x64100存放在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)


總結

  1. x86-64下的gcc在參數小於等於6個時,在函數調用前會用寄存器按照從右到左的順序保存參數,在函數調用後,從左到右放入棧中。
  2. 超過6個時,超出的部分會從右向左入棧。
  3. 函數內部沒有調用其他函數且需要入棧的少於128字節時,棧頂不會增長,數據被存放在棧頂之外。

尾語

  上面這些,基本解決了我遇到的疑惑,不過還能往更深追究,但是對於函數調用時,棧發生的事情來說,作爲了解已經足夠了。

以上

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