一、局部变量
先来看一下函数内部定义变量在内存中的存放位置。首先要说明的是一个C程序被操作系统载入内存时分为两个部分:代码段和数据段。代码段中存放了程序运行的代码内容。数据段则又分为两个部分堆和栈。我们常常所说的堆栈其实在内存中是具有不同作用的两部分区域。在本节我们只学习栈的相关内容,而关于堆的内容我们会在后续章节中学习。如果在一个函数内部定义一些变量,它们在内存中就是存放在了栈中,并且符合栈操作原理,即先进后出,后进先出。下面来看一个简单的例子:
#include <stdio.h> int main(int argc, char **args) { int m = 0x11111111; int n = 0x22222222; int o = 0x33333333; int p = 0x44444444; return 0; }
来看一下这个程序的反汇编程序:
080483cb <main>: 80483cb: 55 push %ebp 80483cc: 89 e5 mov %esp,%ebp 80483ce: 83 ec 10 sub $0x10,%esp 80483d1: c7 45 fc 11 11 11 11 movl $0x11111111,-0x4(%ebp) 80483d8: c7 45 f8 22 22 22 22 movl $0x22222222,-0x8(%ebp) 80483df: c7 45 f4 33 33 33 33 movl $0x33333333,-0xc(%ebp) 80483e6: c7 45 f0 44 44 44 44 movl $0x44444444,-0x10(%ebp) 80483ed: b8 00 00 00 00 mov $0x0,%eax 80483f2: c9 leave 80483f3: c3 ret
在main函数被调用时首先将%ebp压栈(当然这里压栈的是%ebp,但在调用main函数的函数里%ebp已经被赋值为%esp,所以这个值实际上是调用函数的%esp,也就是这个函数的栈顶位置),然后%ebp被赋值为%esp,接下来向栈中申请0x10个字节(4个int型变量刚好是16个字节16==0x10),于是%ebp指向了main函数所使用栈可用空间的栈顶,最后将m、n、o、p这4个变量压栈。栈的数据图如下:
这就是函数内部局部变量在内存中的存放方式。接下来我们来看一下多个函数调用时它们的局部变量存放方式:
#include <stdio.h> void func_B(void) { int e = 0x99999999; int f = 0xaaaaaaaa; int g = 0xbbbbbbbb; int h = 0xcccccccc; } void func_A(void) { int a = 0x55555555; int b = 0x66666666; int c = 0x77777777; int d = 0x88888888; func_B(); } int main(int argc, char **args) { int m = 0x11111111; int n = 0x22222222; int o = 0x33333333; int p = 0x44444444; func_A(); return 0; }
看一下程序的反汇编代码:
080483cb <func_B>: 80483cb: 55 push %ebp 80483cc: 89 e5 mov %esp,%ebp 80483ce: 83 ec 10 sub $0x10,%esp 80483d1: c7 45 fc 99 99 99 99 movl $0x99999999,-0x4(%ebp) 80483d8: c7 45 f8 aa aa aa aa movl $0xaaaaaaaa,-0x8(%ebp) 80483df: c7 45 f4 bb bb bb bb movl $0xbbbbbbbb,-0xc(%ebp) 80483e6: c7 45 f0 cc cc cc cc movl $0xcccccccc,-0x10(%ebp) 80483ed: c9 leave 80483ee: c3 ret 080483ef <func_A>: 80483ef: 55 push %ebp 80483f0: 89 e5 mov %esp,%ebp 80483f2: 83 ec 10 sub $0x10,%esp 80483f5: c7 45 fc 55 55 55 55 movl $0x55555555,-0x4(%ebp) 80483fc: c7 45 f8 66 66 66 66 movl $0x66666666,-0x8(%ebp) 8048403: c7 45 f4 77 77 77 77 movl $0x77777777,-0xc(%ebp) 804840a: c7 45 f0 88 88 88 88 movl $0x88888888,-0x10(%ebp) 8048411: e8 b5 ff ff ff call 80483cb <func_B> 8048416: c9 leave 8048417: c3 ret 08048418 <main>: 8048418: 55 push %ebp 8048419: 89 e5 mov %esp,%ebp 804841b: 83 ec 10 sub $0x10,%esp 804841e: c7 45 fc 11 11 11 11 movl $0x11111111,-0x4(%ebp) 8048425: c7 45 f8 22 22 22 22 movl $0x22222222,-0x8(%ebp) 804842c: c7 45 f4 33 33 33 33 movl $0x33333333,-0xc(%ebp) 8048433: c7 45 f0 44 44 44 44 movl $0x44444444,-0x10(%ebp) 804843a: e8 b0 ff ff ff call 80483ef <func_A> 804843f: b8 00 00 00 00 mov $0x0,%eax 8048444: c9 leave 8048445: c3 ret
当main函数调用函数func_A()时首先将%eip压栈,函数func_A在执行时先将%ebp压栈(其实是main函数中的%esp),将%esp减0x10申请栈空间,再将a、b、c、d这4个变量压栈。当func_A()调用func_B()时也是同样的原理。整个调用过程内存图如下:
我们知道在函数内部定义的变量称作“局部变量”,它们的作用域只限于函数内部,原因是当一个函数执行结束之后就要执行ret指令来返回调用它的函数中继续执行,而在执行ret之前还要执行一个leave命令(相当于pop %ebp; mov %ebp, %esp;),也就是说要恢复调用函数的%esp。也就是说%esp已经恢复成了调用函数的栈区域,那么被调用函数的局部变量已经不能再使用了。
下面我们再来看另外一个函数调用的例子:
#include <stdio.h> void func_A(void) { int a = 0x11111111; int b = 0x22222222; int c = 0x33333333; int d = 0x44444444; printf("%x\n%x\n%x\n%x\n\n", a, b, c, d); } void func_B(void) { int e; int f; int g; int h; printf("%x\n%x\n%x\n%x\n\n", e, f, g, h); } int main(int argc, char **args) { func_A(); func_B(); return 0; }
运行结果:
11111111 22222222 33333333 44444444 11111111 22222222 33333333 44444444
很奇怪,为什么在函数func_B()中定义的局部变量e、f、g、h没有被初始化,也没有被赋值,但它们的值却与func_A()中的局部变量a、b、c、d一模一样呢?我们再来看一下在main函数分别调用func_A()、和func_B()时栈中的变量存储方式:
当main函数调用func_A()时,func_A将a、b、c、d初始化,并压栈;在func_A执行结束时出栈,%esp恢复到main函数的栈顶,而当main调用func_B()时,func_B将e、f、g、e压栈。值得注意的是func_A和func_B所使用的栈地址是一样的,执行压栈和出栈时只是修改了%esp的值而并不会修改栈中数据的内容。而在C语言中定义变量时如果不为其初始化,这些变量在内存中所在位置的数据是不会被清空的,也就是说这些内存地址中的值原来是多少就是多少,所以当func_B定义了4个变量e、f、g、h,并没有对它们初始化,但是它们的值就是原来使用它们这些地址时的值,也就是a、b、c、d的值。
所以说定义了变量之后要对其初始化,否则这些变量的值是无法预计的,在使用的过程中会存在非常严重的隐患问题。
二、参数传递
下面再来看一下带有参数的函数参数传递原理以及它们在栈中的存储方式:
#include <stdio.h> void func(int e, int f, int g, int h) { e = 0x55555555; f = 0x66666666; g = 0x77777777; h = 0x88888888; } int main(int argc, char **args) { int a = 0x11111111; int b = 0x22222222; int c = 0x33333333; int d = 0x44444444; func(a, b, c, d); return 0; }
看一下这个程序的反汇编代码:
080483cb <func>: 80483cb: 55 push %ebp 80483cc: 89 e5 mov %esp,%ebp 80483ce: c7 45 08 55 55 55 55 movl $0x55555555,0x8(%ebp) 80483d5: c7 45 0c 66 66 66 66 movl $0x66666666,0xc(%ebp) 80483dc: c7 45 10 77 77 77 77 movl $0x77777777,0x10(%ebp) 80483e3: c7 45 14 88 88 88 88 movl $0x88888888,0x14(%ebp) 80483ea: 5d pop %ebp 80483eb: c3 ret 080483ec <main>: 80483ec: 55 push %ebp 80483ed: 89 e5 mov %esp,%ebp 80483ef: 83 ec 10 sub $0x10,%esp 80483f2: c7 45 fc 11 11 11 11 movl $0x11111111,-0x4(%ebp) 80483f9: c7 45 f8 22 22 22 22 movl $0x22222222,-0x8(%ebp) 8048400: c7 45 f4 33 33 33 33 movl $0x33333333,-0xc(%ebp) 8048407: c7 45 f0 44 44 44 44 movl $0x44444444,-0x10(%ebp) 804840e: ff 75 f0 pushl -0x10(%ebp) 8048411: ff 75 f4 pushl -0xc(%ebp) 8048414: ff 75 f8 pushl -0x8(%ebp) 8048417: ff 75 fc pushl -0x4(%ebp) 804841a: e8 ac ff ff ff call 80483cb <func> 804841f: 83 c4 10 add $0x10,%esp 8048422: b8 00 00 00 00 mov $0x0,%eax 8048427: c9 leave 8048428: c3 ret
同样的,在带参数函数调用时也是使用栈机制来存放函数的参数,我们来看一下内存存储格式图:
可以看到在main中定义的4个变量a、b、c、d被倒序压栈,也就是上图中灰色部分的e、f、g、h,也就是函数func(int e, int f, int g, int h)中的4个参数,这种参数被称作“形式参数”简称“形参”。当func执行时修改e、f、g、h时,并没有影响到main函数中a、b、c、d这4个变量的值。所以说,在函数调用过程中被调用函数修改参数的值并不会影响到调用函数中的传入变量的值。
再来看下面的一个例子:
#include <stdio.h> void func_C(int e, int f, int g, int h) { int a = 0xcccccccc; int b = 0xdddddddd; int c = 0xeeeeeeee; int d = 0xffffffff; } void func_B(int e, int f, int g, int h) { int a = 0x88888888; int b = 0x99999999; int c = 0xaaaaaaaa; int d = 0xbbbbbbbb; func_C(a, b, c, d); } void func_A(int e, int f, int g, int h) { int a = 0x44444444; int b = 0x55555555; int c = 0x66666666; int d = 0x77777777; func_B(a, b, c, d); } int main(int argc, char **args) { int a = 0x00000000; int b = 0x11111111; int c = 0x22222222; int d = 0x33333333; func_A(a, b, c, d); return 0; }
这次我们直接看一下内存栈中的格式:
可以看到C语言中都是采用这种栈机制来实现局部变量和函数参数的。每一个函数所可以访问到的局部变量和形参被称作为C语言中函数的“栈帧”。
Copyright © 2015-2023 问渠网 辽ICP备15013245号