c语言可以通过malloc在栈上,C语言内部静态成员陷阱

在C语言中,我们知道当函数返回时,其栈上的内存会随着函数出栈而释放,但是我们有时需要返回一块函数内部可以处理,而函数外面仍然有效的内存。大体来说有如下几种方法:

1)在函数内部通过malloc在堆上分配内存,然后把这块内存返回。但是这将带来潜在的安全隐患,如内存泄露或多次释放导致程序崩溃。

2)由函数外部传入一块内存,函数内部的数据处理可以在该内存块上完成。让内存由外部程序维护,比较简显直观,且相对安全,但稍显麻烦。

3)函数内部定义static变量,即便函数返回仍然有效。既不用使用堆上的内存,也不需用户传入buffer和其长度,故简洁易用。

这里,我想对第三种方法进行一些讨论。使用static内存这个方法看似不错,但是它有让你想象不到的陷阱。见如下代码:

#include

#include

#include

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值