Chinaunix首页 | 论坛 | 博客
  • 博客访问: 291630
  • 博文数量: 134
  • 博客积分: 667
  • 博客等级: 上士
  • 技术积分: 770
  • 用 户 组: 普通用户
  • 注册时间: 2012-04-08 15:19
文章分类

全部博文(134)

文章存档

2012年(134)

分类:

2012-04-08 16:39:43

在C语言中,我们知道当函数返回时,其栈上的内存会随着函数出栈而释放,但是我们有时需要返回一块函数内部可以处理,而函数外面仍然有效的内存。大体来说有如下几种方法:
 
1)在函数内部通过malloc在堆上分配内存,然后把这块内存返回。但是这将带来潜在的安全隐患,如内存泄露或多次释放导致程序崩溃。
 
2)由函数外部传入一块内存,函数内部的数据处理可以在该内存块上完成。让内存由外部程序维护,比较简显直观,且相对安全,但稍显麻烦。
 
3)函数内部定义static变量,即便函数返回仍然有效。既不用使用堆上的内存,也不需用户传入buffer和其长度,故简洁易用。
 
这里,我想对第三种方法进行一些讨论。使用static内存这个方法看似不错,但是它有让你想象不到的陷阱。见如下代码:
  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <stdlib.h>

  4. static char *st(int n)
  5. {
  6.     static char buf[20];
  7.     memset(buf, 0, sizeof buf);
  8.     sprintf(buf, "n = %d\n", n);
  9.     return buf;
  10. }

  11. int main(void)
  12. {
  13.     printf("%s%s", st(10), st(20));
  14.     return 0;
  15. }

运行结果有点出乎意料,显示两个 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函数同样如此,我们在调用类似的函数时需要注意一下,如果有必要,可以将其拷贝到另外一块缓冲区中再使用。

阅读(925) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~