最近一直在看CS:APP,.看到程序的机器表示时感觉适当的了解寄存器与函数调用的关系对理解程序语言还是很有帮助的,便有了此文.
在高级语言中函数调用是很普通的了,但对于其汇编级别的掌握还是不太容易,文章中会涉及寄存器以及数据指令的相关汇编知识.在谈正文前先大概介绍下,
产生汇编代码的阶段:一个程序首先,C预处理器会扩展源代码,插入所有用#INCLUDE命令指定的文件,并扩展所有的宏.其次,编译器产生两个源文件的汇编代码,名字分别为P1.S和P2.S.接下来,汇编器会将汇编代码转化成二进制目标代码文件P1.O和P2.O.最后,连接器将两个目标文件与现实标准函数库的代码合并,并最后产生可执行的文件.
C预处理器----à编译器----à汇编器----à连接器
反汇编语言(Equivalent assembly language): 由于目标代码给阅读造成了障碍,而反汇编的格式与目标代码非常接近,因此对于汇编调试来说,看懂反汇编是很有价值的.
数据传送指令&寄存器:比如mov,lea等以及寄存器的使用规则;建议参考汇编语言书籍.注意不同的汇编器对于指令的定义有所不同,在VC中mov ebp esp 表示将寄存器esp值赋给寄存器ebp,以下代码均如此.
栈帧结构:IA32程序用栈来支持过程调用.栈用来传递参数,存储返回信息,保存寄存器一供以后恢复等,为单个过程分配的栈叫做栈帧.注意,对于每个过程系统都将为其单独分配一个独立的栈帧,而寄存器的使用是共享的.其最顶端是以两个指针定界的,寄存器%ebp作为帧指针,而%esp作为栈指针
远调用与近调用:子程序的调用指令分为近(near)调用和远(far)调用,这里我们记住一个结论,远调用的返回地址占8个字节,近调用的返回地址占4个字节.
注:这里涉及很多汇编知识,请大家多多参看
大家对以上知识有了了解后就可以开始下面的内容了,以下C代码展示了2个函数的调用:
-----------------------------------------------------------------------
int swap_add(int *xp,int *yp)
{
int x=*xp;
int y=*yp;
return x+y;
}
int caller()
{
int arg1=534;
int ary2=1057;
int sum=swap+add(&arg1,&arg2);
return 0
}
main()
{
caller();
}
-----------------------------------------------------------------------
上面caller()调用了函数swap_add(),并且通过地址传递参数.下面我们来看看系统是怎么传递参数、调用函数、寄存器使用以及是如何进栈出栈的.下面是VC6.0下的反汇编代码对应上面的源码.
-----------------------------------------------------------------------
10: int caller() //为便于理解,我将汇编代码的顺序按照函数调用的次序来展示的,真实应该按照前面的标号来
11: {
00401070 push ebp //将ebp入栈,大多数信息的访问都是相对于帧指针的,而esp是永远、
始终指向栈顶的,不要期望通过控制它的相对位置来访问其他位置
00401071 mov ebp,esp //将栈顶值赋给ebp,这时的ebp就指向栈顶了,当然esp仍然指向栈顶
这里仅仅是对ebp赋值而已
00401073 sub esp,4Ch //将esp减去48h,这时内存中将藤出一定空间用来为以后存放局部变
量,后头将会看到是如何存放的
00401076 push ebx //ebx,esi,edi是作为"被调用者保存"寄存器,也就是说在被调函数要访
00401077 push esi //问某个寄存器时,我们样避免被调者修改覆盖了主调用函数稍后会
00401078 push edi //使用的寄存器,我们必须在被调者覆盖前将其保存到起来,调用结束 后再恢复原来的值.当然这里所谓的"保存/恢复"就是压栈和弹栈
00401079 lea edi,[ebp-4Ch]
0040107C mov ecx,13h
00401081 mov eax,0CCCCCCCCh
00401086 rep stos dword ptr [edi]
12: int arg1 = 534;
00401088 mov dword ptr [ebp-4],216h //将216h(534)值放入相对于ebp-4的位置
13: int arg2 = 1057;
0040108F mov dword ptr [ebp-8],421h //将421h(1057)值放入相对于ebp-8的位置
14: int sum = swap_add( &arg1, &arg2);
00401096 lea eax,[ebp-8] //计算arg1的地址放入eax,并压栈
00401099 push eax
0040109A lea ecx,[ebp-4] //计算arg2的地址放入ecx,并压栈
0040109D push ecx
0040109E call @ILT+5(_swap_add) (0040100a) //调用swap_add函数,同时将call指令下面的地址作为返
回地址压入栈中,注意这两个动作是在一个call指令内同
时完成的 (到此系统开始调用swap_add函数)
004010A3 add esp,8
004010A6 mov dword ptr [ebp-0Ch],eax
15:
16: return 0;
004010A9 xor eax,eax
17:
18: }
004010AB pop edi
004010AC pop esi
004010AD pop ebx
004010AE add esp,4Ch
004010B1 cmp ebp,esp
004010B3 call __chkesp (00401110)
004010B8 mov esp,ebp
004010BA pop ebp
004010BB ret
---------------------------------------------------------------------------
1: #include <stdio.h>
2: int swap_add(int *xp, int *yp)
3: {
00401030 push ebp //ebp继续压栈,接到call()函数的返回地址压入的
00401031 mov ebp,esp
00401033 sub esp,48h
00401036 push ebx
00401037 push esi
00401038 push edi
00401039 lea edi,[ebp-48h]
0040103C mov ecx,12h
00401041 mov eax,0CCCCCCCCh
00401046 rep stos dword ptr [edi] //以上注释与call()原理相同,但请注意ebp寄存器值(相对位
置)的改变
4: int x = *xp;
00401048 mov eax,dword ptr [ebp+8] //这里的x=*xp显然是个赋值语句,如果只从C代码来看很容
易知道*xp指向的值是arg1(534),那么程序是怎么知道的
呢?这段汇编叫告诉了我们答案,子程序通过自己栈帧中的
ebp+8的偏移量来访问了存放在主调函数call()栈帧中
arg1,arg2的地址从而进一步访问到他们的具体值.那么,为
什么是加8的偏移量呢?在预备知识中有个叫远调用的东西,
因为返回地址占据了8个字节,推理,如果是近调用这里就应
该是加4个字节了.这个就是很常用的"寄存器传递参数",到
此,参数如何传递,寄存器如何使用就应该比较清楚了.图解1
0040104B mov ecx,dword ptr [eax]
0040104D mov dword ptr [ebp-4],ecx //将得到的具体值放如自己的栈帧中,相对位置在ebp-4,为
局部变量
5: int y = *yp;
00401050 mov edx,dword ptr [ebp+0Ch] //相当于EBP+12
00401053 mov eax,dword ptr [edx]
00401055 mov dword ptr [ebp-8],eax //同理
6:
7: return x + y;
00401058 mov eax,dword ptr [ebp-4]
0040105B add eax,dword ptr [ebp-8] //将最后的返回结果放入eax中
8: }
0040105E pop edi
0040105F pop esi
00401060 pop ebx
00401061 mov esp,ebp
00401063 pop ebp
00401064 ret //从过程调用中返回
--------------------------------------------------------------------------------------
内存是这样的.....
↓ ↓
内存地址 堆栈
┆ ┆
├──────┤
│ ebp │
├─┄┄┄┄─┤
│216h (arg1)│
├─┄┄┄┄─┤
│216h (arg1)│
├─┄┄┄┄─┤
┆ 4ch的空间 ┆
┆ ┆ 兰色为call()的栈帧结构
│ │
│ │
├─┄┄┄┄─┤
│ ebx/esi/edi│
├──────┤
│ &arg2 │
├──────┤
│ &arg1 │← --------|
├──────┤ |
│ 返回地址 │ | (ebp+8访问call()栈帧中的&arg1)图解1
├──────┤ | |
│ ebp │_________| |
├─┄┄┄┄─┤ |
│ x │←--------------------------|
├─┄┄┄┄─┤
│ y │
├─┄┄┄┄─┤
┆ 48h的空间 ┆
┆ ┆ 黑色swap_add的栈帧结构
│ │
│ │
├─┄┄┄┄─┤
│ ebx/esi/edi│
├──────┤
┆ . ┆
│ . │
│ . │
├─┄┄┄┄─┤ (如果swap_add还要调用其他函数,那么结构就类似了)
到此,整个过程就介绍完了,我们可以看到,对于每个函数过程,系统都为其分配了单独的栈,也就是所谓的栈帧,仔细观察代码可以进一步发现,每个过程的汇编代码有几部分都是大致一样的,这就提示我们系统对每个过程分配方式都是统一的,也就是说先给你分配好了,至于怎么用再看具体情况,其实我们可以把代码看成三部分:(建立部分)作用是初始化栈帧;(主体部分)执行过程的实际计算,(结尾部分)恢复栈的状态和过程返回.至于怎么返回,内存如何释放,堆栈如何平衡等问题就是以上过程的反推.
结语:我个人认为适当的掌握些硬件汇编知识对于我们学生来说很有帮助,这也是受我们学校一位老师的启发,以我为鉴,我想作为一个计算机学生可以适当的做些项目,但不要沉迷于某些项目的反复开发当中,这对自己知识结构的搭建没有太大好处,平时应该"做做学学,学学做做",最后还是应该回到书本当中,同时在不同的实践中深刻体会知识,检验自己,这样才会有提高.说实话写博客真很耗时啊,不过在这个过程中也算自己巩固下知识.