近来是校园招聘高峰期啊... ...
局部变量之局部指的是一个变量的作用范围,例如一个函数内定义的变量。这里我们要说的就是这种变量。
很多书上或者说面试宝典呐之类的,都说不要返回一个局部变量的地址以供外部使用。怎么说呢,这种说法应该是基于编码习惯软件工程角度来说的,这种做法确实很容易出问题,但不是一定会出问题。下面我们来看一个简单的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。
如有错误请不吝赐教,谢谢!