之前,我本想观察函数调用时,栈的细节。顺便验证一下,口口相传的,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 }
出现了:
以上三条可以在C++函数调用栈细节(gdb调试)中看到调试的具体情况。
之前看过很多判断参数入栈顺序,通过打印参数的地址,根据右边参数为高地址,左边参数为低地址,就得出参数是从右向左入栈。但是在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 }
难道这就说明了 参数从左向右入栈的?有些偏颇。
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,所以栈顶增长。参数存放在栈顶内。我们接下来尝试函数内部没有调用其他函数的情况。
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)
上面这些,基本解决了我遇到的疑惑,不过还能往更深追究,但是对于函数调用时,栈发生的事情来说,作为了解已经足够了。
以上