c 重置数组_从汇编语言看C/C++函数调用

76cce4cee22df72f9262a07a84c7fa66.png

从汇编角度理解C/C++函数调用,能够加深我们对代码的理解,提升调试能力。本文主要讲函数调用栈的建立和销毁、call与ret指令的本质、栈变量的申请、函数参数-返回地址-ebp在栈上的相对位置。

1.前言

//main.c
int bar(int c, int d)
{
    int e = c + d;
    return e;
}
int foo(int a, int b)
{
    return bar(a, b);
}
int main(void)
{
    foo(2, 5);
    return 0;
}

编译这段代码,进入gdb,然后对main函数进行反汇编(disassemble main),得到如下汇编代码.

(gdb) disassemble main
Dump of assembler code for function main:
   0x0000000000400506 <+0>: push   %rbp
   0x0000000000400507 <+1>: mov    %rsp,%rbp //以上两句,建立了main函数的栈
=> 0x000000000040050a <+4>: mov    $0x5,%esi 
   0x000000000040050f <+9>: mov    $0x2,%edi // 将实参放入寄存器
   0x0000000000400514 <+14>:    callq  0x4004e7 <foo>
   0x0000000000400519 <+19>:    mov    $0x0,%eax
   0x000000000040051e <+24>:    pop    %rbp // 销毁main函数栈
   0x000000000040051f <+25>:    retq

此时,我们查看对应寄存器,得到如下结果. 我们可以看到,此时rbp和rsp指向同一个位置,这是在gdb中执行start命令之后,指令所在的位置。

(gdb) info registers rbp rsp
rbp            0x7fffffffe380   0x7fffffffe380
rsp            0x7fffffffe380   0x7fffffffe380

接下来我们将用这个例子来详细探索函数调用过程中的参数传递、控制转移call、新函数栈的建立、新函数栈的销毁、控制转移的恢复ret.

2.进入被调用函数的准备

40050a~40050f两条指令为函数调用传递参数的过程,我们可以看到是函数参数是倒序传入的:先传入第N个参数,再传入第N-1个参数。(注意,目前64位的机器上,函数参数的传递很少是通过栈进行的。)

接下来的三行汇编,对应的是实参的传递和对foo函数的调用。下面,我们接着执行几条汇编(si 3),进入foo函数内部,具体执行如下与解释如下.

(gdb) si 3
(gdb) disassemble foo
=> 0x00000000004004e7 <+0>: push   %rbp //
   0x00000000004004e8 <+1>: mov    %rsp,%rbp //新的函数栈的建立
   0x00000000004004eb <+4>: sub    $0x8,%rsp //申请a和b对应的栈空间
   0x00000000004004ef <+8>: mov    %edi,-0x4(%rbp) 
   0x00000000004004f2 <+11>:    mov    %esi,-0x8(%rbp) //将传入参数,存放到当前函数栈
   0x00000000004004f5 <+14>:    mov    -0x8(%rbp),%edx
   0x00000000004004f8 <+17>:    mov    -0x4(%rbp),%eax
   0x00000000004004fb <+20>:    mov    %edx,%esi
   0x00000000004004fd <+22>:    mov    %eax,%edi //将局部变量,放入寄存器
   0x00000000004004ff <+24>:    callq  0x4004cd <bar>
   0x0000000000400504 <+29>:    leaveq
   0x0000000000400505 <+30>:    retq

call指令

注意当指令走到0x00000000004004e7的时候(尚未执行),也就是callq执行刚刚执行完毕,对应寄存器的值如下。

(gdb) info registers rbp rsp
rbp            0x7fffffffe380   0x7fffffffe380
rsp            0x7fffffffe378   0x7fffffffe378

和上一次我们查看rsp/rbp相比,rsp向低地址移动了8位(有数据入栈),结合上下文,这一寄存器的变化只会出现在callq 0x4004e7之中。进一步,我们看一下入栈的数据是什么,也就是原来rsp被存放了什么数值。从下面的代码中我们可以看到,原来的rsp上方存放的数值是callq之后的指令的地址。

(gdb) x/8 0x7fffffffe370 
0x7fffffffe370: 0xffffe460 0x00007fff 0x00400519 0x00000000 //注意0x00400519这个值,它是main函数中对应指令的地址
0x7fffffffe380: 0x00000000 0x00000000 0xf7a30445 0x00007fff

总结一下:call指令的实际用途:1.push IP 2.通过设置 IP实现指令之间的跳转

3.新函数栈的建立与销毁

函数栈的建立

对应4004e7~4004e8处的汇编指令:重新设置rsprbp.

函数中局部变量的申请与赋值

对应的代码位于4004eb~4004f2. 从这里我们可以看出1.同一个函数内部,栈变量的申请是同时发生的,但是赋值是逐条执行。2.栈变量的申请,仅仅涉及rsp指针的移动,不会导致segment fault,但是变量的读写,会具体访问到对应的内存,将会触发segment fault。这里我们来看一个例子,猜一猜,这段代码会crash到哪一行?答案是会crash在func()这一行。因为数组a对应的空间申请发生在func()之前,而调用func的时候会将IP入栈,而此时,栈已经溢出了。

int func(void) {
  int b; 
  b = 1; 
  return b; 
} 
int main(){ 
  func(); 
  int a[1024*1024*8]; 
  a[0] = 1; 
  a[1024*1024*8 -1] = 1; 
  return 0; 
}

函数栈的销毁

函数栈的销毁涉及两方面:栈上空间的释放与rsp和rbp的重置。我们来看看foo函数末尾对应的汇编指令

0x0000000000400504 <+29>: leaveq

继续用si来逐条执行对应指令,同时查看这条指令前后rpb和rsp的值如下

(gdb) disassemble foo
Dump of assembler code for function foo:
   0x00000000004004e7 <+0>: push   %rbp
   0x00000000004004e8 <+1>: mov    %rsp,%rbp
   0x00000000004004eb <+4>: sub    $0x8,%rsp
   0x00000000004004ef <+8>: mov    %edi,-0x4(%rbp)
   0x00000000004004f2 <+11>:    mov    %esi,-0x8(%rbp)
   0x00000000004004f5 <+14>:    mov    -0x8(%rbp),%edx
   0x00000000004004f8 <+17>:    mov    -0x4(%rbp),%eax
   0x00000000004004fb <+20>:    mov    %edx,%esi
   0x00000000004004fd <+22>:    mov    %eax,%edi
   0x00000000004004ff <+24>:    callq  0x4004cd <bar>
=> 0x0000000000400504 <+29>:    leaveq
   0x0000000000400505 <+30>:    retq
End of assembler dump.
(gdb) info registers rbp rsp
rbp            0x7fffffffe370   0x7fffffffe370
rsp            0x7fffffffe368   0x7fffffffe368
(gdb) si
0x0000000000400505  9   } //对应retq语句
(gdb) info registers rbp rsp
rbp            0x7fffffffe380   0x7fffffffe380
rsp            0x7fffffffe378   0x7fffffffe378
(gdb) disassemble foo
Dump of assembler code for function foo:
   0x00000000004004e7 <+0>: push   %rbp
   0x00000000004004e8 <+1>: mov    %rsp,%rbp
   0x00000000004004eb <+4>: sub    $0x8,%rsp
   0x00000000004004ef <+8>: mov    %edi,-0x4(%rbp)
   0x00000000004004f2 <+11>:    mov    %esi,-0x8(%rbp)
   0x00000000004004f5 <+14>:    mov    -0x8(%rbp),%edx
   0x00000000004004f8 <+17>:    mov    -0x4(%rbp),%eax
   0x00000000004004fb <+20>:    mov    %edx,%esi
   0x00000000004004fd <+22>:    mov    %eax,%edi
   0x00000000004004ff <+24>:    callq  0x4004cd <bar>
   0x0000000000400504 <+29>:    leaveq
=> 0x0000000000400505 <+30>:    retq

从理论上来说,leaveq 应该正好是入栈的逆向过程mov %rbp %rsp; pop %rbp.

控制转移的恢复ret/retq

最后我们看看retq执行完毕之后,寄存器前后的变化。

(gdb) info registers rbp rsp rip 
rbp 0x7fffffffe380 0x7fffffffe380 
rsp 0x7fffffffe378 0x7fffffffe378 
rip 0x400505 0x400505 <foo+30> 
(gdb) disassemble foo 
Dump of assembler code for function foo: 
0x00000000004004e7 <+0>: push %rbp 
0x00000000004004e8 <+1>: mov %rsp,%rbp 
0x00000000004004eb <+4>: sub $0x8,%rsp 
0x00000000004004ef <+8>: mov %edi,-0x4(%rbp) 
0x00000000004004f2 <+11>: mov %esi,-0x8(%rbp) 
0x00000000004004f5 <+14>: mov -0x8(%rbp),%edx 
0x00000000004004f8 <+17>: mov -0x4(%rbp),%eax 
0x00000000004004fb <+20>: mov %edx,%esi 
0x00000000004004fd <+22>: mov %eax,%edi 
0x00000000004004ff <+24>: callq 0x4004cd <bar> 0x0000000000400504 <+29>: leaveq 
=> 0x0000000000400505 <+30>: retq End of assembler dump. 
(gdb) si 
main () at main.c:13 
13 return 0; 
(gdb) info registers rbp rsp rip 
rbp 0x7fffffffe380 0x7fffffffe380 
rsp 0x7fffffffe380 0x7fffffffe380 
rip 0x400519 0x400519 <main+19>

这里我们得到几点结论: 1. retq指令的调用,导致出栈了一个8位的数,这个就是调用者的下一条指令。 2. retq调用之前,已经处在调用者的栈帧。retq的调用,仅仅是一个栈上保存的地址存放到rip寄存器。 3. 对比mainfoo函数的末尾,我们可以看到有细小的差别:main函数栈的销毁仅仅有push %rbp一句,但foo函数的结尾是leaveq。导致这一差别的原因是main函数建立调用栈之后,并没有移动rsp.

总结

函数调用过程中栈的变化如图

1434d3d98de87a3873ef7a26fa25a876.png

我们可以看到子程序调用之前和之后(1&6),函数栈是没有任何变化的,有变化的在于rip等相关寄存器的值;call指令执行之后与ret指令执行之前,函数栈也是相同的,而ret指令之所以能转交控制权,是因为ip的值被保存到栈上。 这里我们可以看到,栈不仅仅有保存的局部变量数据,也有对控制转移指令至关重要的寄存器临时存储。一旦栈被写坏,控制转移指令就无法正常执行。下一节,我们将讲解与栈相关的控制转移指令被写坏的场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值