全局变量和局部变量
在熟悉了汇编语言后,接下来我们来了解一下全局变量和局部变量,在函数外部定义的变量称为全局变量
,在函数内部定义的变量称为局部变量
,全局变量可以在任意函数中使用,局部变量只能在函数定义局部变量的内部使用。下面,我们就通过汇编语言来看一下全局变量和局部变量的不同之处。
下面定义的 C 语言代码分别定义了局部变量和全局变量,并且给各变量进行了赋值,我们先看一下源代码部分
// 定义被初始化的全局变量 int a1 = 1; int a2 = 2; int a3 = 3; int a4 = 4; int a5 = 5; // 定义没有初始化的全局变量 int b1,b2,b3,b4,b5; // 定义函数 void MyFunc(){ // 定义局部变量 int c1,c2,c3,c4,c5,c6,c7,c8,c9,c10; // 给局部变量赋值 c1 = 1; c2 = 2; c3 = 3; c4 = 4; c5 = 5; c6 = 6; c7 = 7; c8 = 8; c9 = 9; c10 = 10; // 把局部变量赋值给全局变量 a1 = c1; a2 = c2; a3 = c3; a4 = c4; a5 = c5; b1 = c6; b2 = c7; b3 = c8; b4 = c9; b5 = c10; }
上面的代码挺暴力的,不过没关系,能够便于我们分析其汇编源码就好,我们用 Borland C++ 编译后的汇编代码如下,编译完成后的源码比较长,这里我们只拿出来一部分作为分析使用(我们改变了一下段定义顺序,删除了部分注释)
_DATA segment dword public use32 'DATA' align 4 _a1 label dword dd 1 align 4 _a2 label dword dd 2 align 4 _a3 label dword dd 3 align 4 _a4 label dword dd 4 align 4 _a5 label dword dd 5 _DATA ends _BSS segment dword public use32 'BSS' align 4 _b1 label dword db 4 dup(?) align 4 _b2 label dword db 4 dup(?) align 4 _b3 label dword db 4 dup(?) align 4 _b4 label dword db 4 dup(?) align 4 _b5 label dword db 4 dup(?) _BSS ends _TEXT segment dword public use32 'CODE' _MyFunc proc near push ebp mov ebp,esp add esp,-20 push ebx push esi mov eax,1 mov edx,2 mov ecx,3 mov ebx,4 mov esi,5 mov dword ptr [ebp-4],6 mov dword ptr [ebp-8],7 mov dword ptr [ebp-12],8 mov dword ptr [ebp-16],9 mov dword ptr [ebp-20],10 mov dword ptr [_a1],eax mov dword ptr [_a2],edx mov dword ptr [_a3],ecx mov dword ptr [_a4],ebx mov dword ptr [_a5],esi mov eax,dword ptr [ebp-4] mov dword ptr [_b1],eax mov edx,dword ptr [ebp-8] mov dword ptr [_b2],edx mov ecx,dword ptr [ebp-12] mov dword ptr [_b3],ecx mov eax,dword ptr [ebp-16] mov dword ptr [_b4],eax mov edx,dword ptr [ebp-20] mov dword ptr [_b5],edx pop esi pop ebx mov esp,ebp pop ebp ret _MyFunc endp _TEXT ends
编译后的程序,会被归类到名为段定义的组。
- 初始化的全局变量,会汇总到名为 _DATA 的段定义中
_DATA segment dword public use32 'DATA' ... _DATA ends
- 没有初始化的全局变量,会汇总到名为 _BSS 的段定义中
_BSS segment dword public use32 'BSS' ... _BSS ends
- 被段定义 _TEXT 围起来的汇编代码则是 Borland C++ 的定义
_TEXT segment dword public use32 'CODE' _MyFunc proc near ... _MyFunc endp _TEXT ends
我们在分析上面汇编代码之前,先来认识一下更多的汇编指令,此表是对上面部分操作码及其功能的接续
操作码 | 操作数 | 功能 |
add | A,B | 把A和B的值相加,并把结果赋值给A |
call | A | 调用函数A |
cmp | A,B | 对A和B进行比较,比较结果会自动存入标志寄存器中 |
inc | A | 对A的值 + 1 |
ige | 标签名 | 和 cmp 命令组合使用。跳转到标签行 |
jl | 标签名 | 和 cmp 命令组合使用。跳转到标签行 |
jle | 标签名 | 和 cmp 命令组合使用。跳转到标签行 |
jmp | 标签名 | 和 cmp 命令组合使用。跳转到标签行 |
mov | A,B | 把 B 的值赋给 A |
pop | A | 从栈中读取数值并存入A |
push | A | 把A的值存入栈中 |
ret | 无 | 将处理返回到调用源 |
xor | A,B | A和B的位进行亦或比较,并将结果存入A中 |
我们首先来看一下 _DATA
段定义的内容。_a1 label dword
定义了 _a1
这个标签。标签表示的是相对于段定义起始位置的位置。由于_a1
在 _DATA 段
定义的开头位置,所以相对位置是0。_a1
就相当于是全局变量a1。编译后的函数名和变量名前面会加一个(_)
,这也是 Borland C++ 的规定。dd 1
指的是,申请分配了4字节的内存空间,存储着1这个初始值。 dd指的是 define double word
表示有两个长度为2的字节领域(word),也就是4字节的意思。
Borland C++ 中,由于int
类型的长度是4字节,因此汇编器就把 int a1 = 1 变换成了 _a1 label dword 和 dd 1
。同样,这里也定义了相当于全局变量的 a2 - a5 的标签 _a2 - _a5
,它们各自的初始值 2 - 5 也被存储在各自的4字节中。
接下来,我们来说一说 _BSS
段定义的内容。这里定义了相当于全局变量 b1 - b5 的标签 _b1 - _b5
。其中的db 4dup(?)
表示的是申请分配了4字节的领域,但值尚未确定(这里用 ? 来表示)的意思。db(define byte)
表示有1个长度是1字节的内存空间。因而,db 4 dup(?) 的情况下,就是4字节的内存空间。
注意:db 4 dup(?) 不要和 dd 4 混淆了,前者表示的是4个长度是1字节的内存空间。而 db 4 表示的则是双字节( = 4 字节) 的内存空间中存储的值是 4
临时确保局部变量使用的内存空间
我们知道,局部变量是临时保存在寄存器和栈中的。函数内部利用栈进行局部变量的存储,函数调用完成后,局部变量值被销毁,但是寄存器可能用于其他目的。所以,局部变量只是函数在处理期间临时存储在寄存器和栈中的。
回想一下上述代码是不是定义了10个局部变量?这是为了表示存储局部变量的不仅仅是栈,还有寄存器。为了确保 c1 - c10 所需的域,寄存器空闲的时候就会使用寄存器,寄存器空间不足的时候就会使用栈。
让我们继续来分析上面代码的内容。_TEXT
段定义表示的是 MyFunc
函数的范围。在 MyFunc 函数中定义的局部变量所需要的内存领域。会被尽可能的分配在寄存器中。大家可能认为使用高性能的寄存器来替代普通的内存是一种资源浪费,但是编译器不这么认为,只要寄存器有空间,编译器就会使用它。由于寄存器的访问速度远高于内存,所以直接访问寄存器能够高效的处理。局部变量使用寄存器,是 Borland C++ 编译器最优化的运行结果。
代码清单中的如下内容表示的是向寄存器中分配局部变量的部分
mov eax,1 mov edx,2 mov ecx,3 mov ebx,4 mov esi,5
仅仅对局部变量进行定义是不够的,只有在给局部变量赋值时,才会被分配到寄存器的内存区域。上述代码相当于就是给5个局部变量 c1 - c5 分别赋值为 1 - 5。eax、edx、ecx、ebx、esi 是 x86 系列32位 CPU 寄存器的名称。至于使用哪个寄存器,是由编译器
来决定的 。
x86 系列 CPU 拥有的寄存器中,程序可以操作的是十几,其中空闲的最多会有几个。因而,局部变量超过寄存器数量的时候,可分配的寄存器就不够用了,这种情况下,编译器就会把栈派上用场,用来存储剩余的局部变量。
在上述代码这一部分,给局部变量c1 - c5 分配完寄存器后,可用的寄存器数量就不足了。于是,剩下的5个局部变量c6 - c10 就被分配给了栈的内存空间。如下面代码所示
mov dword ptr [ebp-4],6 mov dword ptr [ebp-8],7 mov dword ptr [ebp-12],8 mov dword ptr [ebp-16],9 mov dword ptr [ebp-20],10
函数入口 add esp,-20
指的是,对栈数据存储位置的 esp 寄存器(栈指针)的值做减20的处理。为了确保内存变量 c6 - c10 在栈中,就需要保留5个 int 类型的局部变量(4字节 * 5 = 20 字节)所需的空间。mov ebp,esp
这行指令表示的意思是将 esp 寄存器的值赋值到 ebp 寄存器。之所以需要这么处理,是为了通过在函数出口处 mov esp ebp
这一处理,把 esp 寄存器的值还原到原始状态,从而对申请分配的栈空间进行释放,这时栈中用到的局部变量就消失了。这也是栈的清理处理。在使用寄存器的情况下,局部变量则会在寄存器被用于其他用途时自动消失,如下图所示。
用于局部变量的栈空间的申请分配和释放
mov dword ptr [ebp-4],6 mov dword ptr [ebp-8],7 mov dword ptr [ebp-12],8 mov dword ptr [ebp-16],9 mov dword ptr [ebp-20],10
这五行代码是往栈空间代入数值的部分,由于在向栈申请内存空间前,借助了 mov ebp, esp
这个处理,esp 寄存器的值被保存到了 esp 寄存器中,因此,通过使用[ebp - 4]、[ebp - 8]、[ebp - 12]、[ebp - 16]、[ebp - 20] 这样的形式,就可以申请分配20字节的栈内存空间切分成5个长度为4字节的空间来使用。例如,mov dword ptr [ebp-4],6
表示的就是,从申请分配的内存空间的下端(ebp寄存器指示的位置)开始向前4字节的地址([ebp - 4]) 中,存储着6这一4字节数据。