您的位置:首页 > 运维架构 > Linux

C基础----返回局部变量的地址&栈(linux)

2012-10-15 23:01 239 查看
近来是校园招聘高峰期啊... ...

局部变量之局部指的是一个变量的作用范围,例如一个函数内定义的变量。这里我们要说的就是这种变量。

很多书上或者说面试宝典呐之类的,都说不要返回一个局部变量的地址以供外部使用。怎么说呢,这种说法应该是基于编码习惯软件工程角度来说的,这种做法确实很容易出问题,但不是一定会出问题。下面我们来看一个简单的C代码:

1 #include <stdio.h>
  2 
  3 int* foo_a()
  4 {
  5     int a = 0xff;
  6     return &a;
  7 }
  8 
  9 void foo_b()
 10 {
 11     int a = 0xffff;
 12 }
 13 
 14 int main()
 15 {
 16     int* p = NULL;
 17 
 18     p = foo_a();
 19     printf("0x%x\n", *p);
 20 
 21     foo_b();
 22     printf("0x%x\n", *p);
 23 
 24     return 0;
 25 }
这段代码所表达的意思是很清晰的,我们在函数foo_a中定义了一个变量a,其值为0xff,然后将其地址返回,注意返回的是变量a的地址。

函数foo_b更简单,仅仅是在函数体内定义了一个变量b,其值为0xffff。

问题是,两次printf的输出是什么?答案是:

0xff
0xffff
如果答案没有出乎你的所料,那么下面你可以不用看了 :) 否则,请看下去。

疑问1:为什么第一次说出的值是0xff?

第一个输出的是0xff,这个绝对不是巧合,这个值是foo_a留下的。这就像昨晚下了一场大雪,一大早的你出去踩新雪了,留下了很多脚印。如果,在你去过之后没有人再去的话,那么那些脚印还是在那的,除非雪化了!

我们先来看看main函数的反汇编结果,然后我们把每一步执行后的结果画出来。

331 08048405 <main>:
332  8048405:   55                      push   %ebp
333  8048406:   89 e5                   mov    %esp,%ebp
334  8048408:   83 e4 f0                and    $0xfffffff0,%esp
335  804840b:   83 ec 20                sub    $0x20,%esp
336  804840e:   c7 44 24 1c 00 00 00    movl   $0x0,0x1c(%esp)
337  8048415:   00
338  8048416:   e8 c9 ff ff ff          call   80483e4 <foo_a>
339  804841b:   89 44 24 1c             mov    %eax,0x1c(%esp)
340  804841f:   8b 44 24 1c             mov    0x1c(%esp),%eax
341  8048423:   8b 10                   mov    (%eax),%edx
342  8048425:   b8 30 85 04 08          mov    $0x8048530,%eax
343  804842a:   89 54 24 04             mov    %edx,0x4(%esp)
344  804842e:   89 04 24                mov    %eax,(%esp)
345  8048431:   e8 ca fe ff ff          call   8048300 <printf@plt>
346  8048436:   e8 bb ff ff ff          call   80483f6 <foo_b>
347  804843b:   8b 44 24 1c             mov    0x1c(%esp),%eax
348  804843f:   8b 10                   mov    (%eax),%edx
349  8048441:   b8 30 85 04 08          mov    $0x8048530,%eax
350  8048446:   89 54 24 04             mov    %edx,0x4(%esp)
351  804844a:   89 04 24                mov    %eax,(%esp)
352  804844d:   e8 ae fe ff ff          call   8048300 <printf@plt>
353  8048452:   b8 00 00 00 00          mov    $0x0,%eax
354  8048457:   c9                      leave
355  8048458:   c3                      ret
对照上面的汇编,我们画一个图。这一步我们只搞到函数foo_a调用之前。



main函数的栈帧目前只能看到指针p,其值为NULL,即0(可以找一下#define NULL 0;)。

call foo_a的时候会默认把下一条指令的地址入栈,从上面的反汇编推测应该是0x0804841B。call同时会将改变pc的值,应该是0x080483e4,即函数foo_a的地址,也就是说进入函数foo_a执行。

下面我们把函数foo_a的反汇编代码,并把执行结果画在图上:

314 080483e4 <foo_a>:
315  80483e4:   55                      push   %ebp
316  80483e5:   89 e5                   mov    %esp,%ebp
317  80483e7:   83 ec 10                sub    $0x10,%esp
318  80483ea:   c7 45 fc ff 00 00 00    movl   $0xff,-0x4(%ebp)
319  80483f1:   8d 45 fc                lea    -0x4(%ebp),%eax
320  80483f4:   c9                      leave
321  80483f5:   c3                      ret


对照上面的汇编,我们把foo_a执行以及执行后的栈的情况画出来。





汇编代码执行到lea -0x4(%ebp), %eax后,其实返回值已经被设置,函数的功能已经实现。后面就是函数的返回要做的事情。
leave指令相当于:movl %ebp %esp和pop %ebp。这两条执行和函数开始出的push %ebp和%movl %esp %ebp是相对应的。执行完这两条指令后,ebp和esp的值均被更改为进入foo_a时的值。也就是说foo_a的栈帧在逻辑上不在存在。然而foo_a的栈帧中的数据并不会被释放,也就是说栈空间(虚拟区域,vma)映射的物理内存这时候不会被回收。那么,存储在物理内存上的数据当然是在那的。就像前面打的比喻,你的脚印仍然在雪中,除非雪化了(物理内存被回收)。

那么,为什么函数返回时,那个函数的栈帧对应的物理内存不会被回收?其实我们可以反问一下,怎么被回收?怎么去触发回收?这里你是找不到回收的依据的。

所以说,foo_a返回后,输出的*p的值是0xff。

前面我们依据汇编画了foo_a的栈结构,下面我们简要的把foo_b的反汇编和栈结构画出来:

323 080483f6 <foo_b>:
324  80483f6:   55                      push   %ebp
325  80483f7:   89 e5                   mov    %esp,%ebp
326  80483f9:   83 ec 10                sub    $0x10,%esp
327  80483fc:   c7 45 fc ff ff 00 00    movl   $0xffff,-0x4(%ebp)
328  8048403:   c9                      leave
329  8048404:   c3                      ret



也就是说,函数foo_b执行完成以后把foo_a中a的地址处的值置成了0xffff。所以说,foo_b返回后,输出的*p的值是0xffff。

以上代码使用gcc 无优化编译连接,反汇编使用objdump -D。

如有错误请不吝赐教,谢谢!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: