C语言--局部变量及其指针

1,291 阅读3分钟
原文链接: zhuanlan.zhihu.com

C语言的变量,分为全局变量、局部变量、静态全局变量(作用域在文件内、生命周期到进程结束)、静态局部变量(作用域在函数内、生命周期到进程结束)。


通常所说的局部变量,是在函数内定义的、不用static关键字修饰、分配在栈上的变量,其作用域在函数内,生命周期在函数调用的过程内,随着函数调用结束和栈的变化而被销毁。

因此,严禁返回局部变量的指针!!!

因为随着函数调用的结束和栈的变化,返回主调函数后,被调函数的局部变量已经被销毁了,其指针变成了野指针!!!

野指针的值是不确定的,结果也是不确定的,导致的BUG也远比空指针更危险,更难查。

x86 32位C程序的ABI(应用程序二进制接口),是通过栈传递调用参数,通过EAX寄存器返回调用结果。ARM 32位的C程序是前4个参数通过r0~r3传递,多于4个的参数通过栈传递,通过r0寄存器返回调用结果。64位的ABI没详细研究过,暂且不提。


以x86 32位为例,函数调用过程是这样的:

1,主调函数把参数压栈

2,主调函数使用call指令调用被调函数

3,被调函数保存ebp寄存器

4,被调函数把esp寄存器(指向栈顶)保存到ebp寄存器

5,被调函数更改esp寄存器,分配局部变量的内存!重点!

6,被调函数用"ebp寄存器加偏移"访问调用参数,并执行计算过程

7,被调函数用ebp寄存器回写esp寄存器,从而清理栈帧!

该步之后,局部变量的内存已经被清理,局部变量失效!

8,被调函数恢复第3步保存的ebp寄存器的值

9,返回主调函数,调用过程结束。


函数刚分配完局部变量后的栈帧结构(x86 32位,内存地址从上往下依次减小):

调用参数 -----ebp+8

返回地址 -----ebp+4

ebp寄存器原值 ---- ebp指向这里

局部变量 ------------ esp指向这里

汇编代码大概如下:

f:被调函数

pushl %ebp #保存ebp原值

movl %esp,%ebp #保存esp到ebp

subl $4,%esp #分配4字节的局部变量,假设就1个32位的局部变量

movl 8(%ebp), %eax #获取第1个调用参数

...... #计算过程,其结束时要把返回值写入eax寄存器

movl %ebp,%esp #恢复esp寄存器

popl %ebp #恢复ebp寄存器,此时esp指向返回地址

ret #弹出返回地址,返回主调函数


main:主调函数

......

movl $1,%eax #准备参数,假设就1个参数

pushl %eax #参数压栈

call f #调用函数f

addl $4,%esp #清理之前压栈的调用参数,此时eax存储被调函数的返回值

......

下图的代码在linux+gcc下是可以的,宏S直接在main函数内展开,其内部定义的变量s也是main函数的局部变量,可直接在main函数内打印其值,不属于"返回局部变量的指针"。

虽然能用函数的地方尽量不用宏,尤其是C也支持了inline之后,但在某些地方宏仍然是不能被替代的。

下图的用法,在某些需要把出错码转化为出错信息并打印时,可以用到。