在C语言中,我们知道当函数返回时,其栈上的内存会随着函数出栈而释放,但是我们有时需要返回一块函数内部可以处理,而函数外面仍然有效的内存。大体来说有如下几种方法:
1)在函数内部通过malloc在堆上分配内存,然后把这块内存返回。但是这将带来潜在的安全隐患,如内存泄露或多次释放导致程序崩溃。
2)由函数外部传入一块内存,函数内部的数据处理可以在该内存块上完成。让内存由外部程序维护,比较简显直观,且相对安全,但稍显麻烦。
3)函数内部定义static变量,即便函数返回仍然有效。既不用使用堆上的内存,也不需用户传入buffer和其长度,故简洁易用。
这里,我想对第三种方法进行一些讨论。使用static内存这个方法看似不错,但是它有让你想象不到的陷阱。见如下代码:
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- static char *st(int n)
- {
- static char buf[20];
- memset(buf, 0, sizeof buf);
- sprintf(buf, "n = %d\n", n);
- return buf;
- }
- int main(void)
- {
- printf("%s%s", st(10), st(20));
- return 0;
- }
运行结果有点出乎意料,显示两个 n = 10。接下来,分析一下为何会如此。
想象一下C的运行机理,对于main中的printf调用,一共有三个参数,通常从右往左入栈,即先计算st(20)的值入栈,再计算st(10)的值入栈,最后将字符串"%s%s"地址入栈,然后call printf完成调用,如果是这样那就明白了,因为两次st调用都返回的是st内部static变量buf的地址,因此在printf的调用中入栈的前两个参数都是相同的地址,而且第二次调用st即st(10)会修改缓冲区覆盖前一次的修改结果st(20),这样最后就会显示出两个 n = 10。如果还觉得不踏实,我们可以从汇编层面来分析其内部机理。
将生成的可执行文件反汇编后如下
080483e4 :
80483e4: 55 push %ebp
80483e5: 89 e5 mov %esp,%ebp
80483e7: 83 ec 18 sub $0x18,%esp
80483ea: b8 20 85 04 08 mov $0x8048520,%eax
80483ef: 8b 55 08 mov 0x8(%ebp),%edx
80483f2: 89 54 24 08 mov %edx,0x8(%esp)
80483f6: 89 44 24 04 mov %eax,0x4(%esp)
80483fa: c7 04 24 20 a0 04 08 movl $0x804a020,(%esp)
8048401: e8 ea fe ff ff call 80482f0 <>
8048406: b8 20 a0 04 08 mov $0x804a020,%eax
804840b: c9 leave
804840c: c3 ret
0804840d :
804840d: 55 push %ebp
804840e: 89 e5 mov %esp,%ebp
8048410: 83 e4 f0 and $0xfffffff0,%esp
8048413: 53 push %ebx
8048414: 83 ec 1c sub $0x1c,%esp
8048417: c7 04 24 14 00 00 00 movl $0x14,(%esp)
804841e: e8 c1 ff ff ff call 80483e4
8048423: 89 c3 mov %eax,%ebx
8048425: c7 04 24 0a 00 00 00 movl $0xa,(%esp)
804842c: e8 b3 ff ff ff call 80483e4
8048431: ba 23 85 04 08 mov $0x8048523,%edx
8048436: 89 5c 24 08 mov %ebx,0x8(%esp)
804843a: 89 44 24 04 mov %eax,0x4(%esp)
804843e: 89 14 24 mov %edx,(%esp)
8048441: e8 da fe ff ff call 8048320 <>
8048446: b8 00 00 00 00 mov $0x0,%eax
804844b: 83 c4 1c add $0x1c,%esp
804844e: 5b pop %ebx
804844f: 89 ec mov %ebp,%esp
8048451: 5d pop %ebp
分析main函数,先看这一段
movl $0x14,(%esp)
call 80483e4
mov %eax,%ebx
即为将20入栈,调用st(20)并将函数返回值存放在ebx中。
同理,以下这一段
movl $0xa,(%esp)
call 80483e4
即为将10入栈,调用st(10)且函数返回值保存在eax中。
接下来将为printf调用准备参数,将前两次调用st的返回值ebx,eax分别入栈,最后将地址0x8048523入栈,然后调用printf。
mov $0x8048523,%edx
mov %ebx,0x8(%esp)
mov %eax,0x4(%esp)
mov %edx,(%esp)
call 8048320 <>
为了分析函数st的返回值,我们先看看函数st的汇编代码结尾的这两句
call 80482f0 <>
mov $0x804a020,%eax
即调用sprintf函数,然后将返回值0x804a020放入寄存器eax中,该返回值即为buf的地址。
由于st两次返回值都相同,所以最后main中print打印出来的结果是一样的。
以上最后入栈的0x8048523即为字符串"%s%s"的地址,我们可以用gdb确定一下
gcc test.c -g -o test
gdb -q test
Reading symbols from /home/dell/project/test/test...done.
(gdb) b main
Breakpoint 1 at 0x8048417: file test.c, line 13.
(gdb) r
Starting program: /home/dell/project/test/test
Breakpoint 1, main () at test.c:13
13 printf("result is %s, %s\n", str(10), str(20));
(gdb) x/s 0x804a020
0x804a020 : ""
(gdb) x/s 0x8048520
0x8048520: "n = %d\n"
(gdb) x/s 0x8048523
0x8048523: "%s%s"
(gdb)
同样的问题可能在很多地方都会遇到,如ctime函数利用内部静态存储的方式保存时间字符串并返回其地址,还有inet_ntoa函数同样如此,我们在调用类似的函数时需要注意一下,如果有必要,可以将其拷贝到另外一块缓冲区中再使用。
阅读(2555) | 评论(2) | 转发(1) |