x86-64架构下函数调用栈与32位下的不同之处

一览

  • 本文目的
    • x86-64下打印参数地址判断参数入栈顺序的做法不合理
    • 情况1:函数调用参数从右向左存放到寄存器,又从左到右存放到栈
    • 情况2:栈顶不增长,参数存放在栈顶之外
    • 参数多于寄存器数量,参数会入栈
      • 测试
  • 总结
  • 尾语


本文目的

 
  之前,我本想观察函数调用时,栈的细节。顺便验证一下,口口相传的,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调试观察一下。
x86-64架构下函数调用栈与32位下的不同之处_第1张图片
执行前,rbp和rsp保持一致。


x86-64架构下函数调用栈与32位下的不同之处_第2张图片
执行mov -0x14(%rbp),%edx后,rsp没有增长 参数被存放在栈顶之外。


x86-64架构下函数调用栈与32位下的不同之处_第3张图片
执行完 mov -0x18(%rbp),%eax,栈顶依旧没有增长


后来我从淘宝的这篇博客X86-64寄存器和栈帧,(原链接无法访问,因此是这里贴出百度文库的链接)读到:
x86-64架构下函数调用栈与32位下的不同之处_第4张图片
确实,这种手段减少了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字节时,栈顶不会增长,数据被存放在栈顶之外。

尾语

  上面这些,基本解决了我遇到的疑惑,不过还能往更深追究,但是对于函数调用时,栈发生的事情来说,作为了解已经足够了。

以上

你可能感兴趣的:(C++)