汇编中的函数调用与寄存器

 
汇编中的函数调用与寄存器
 
 
  最近一直在看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还要调用其他函数,那么结构就类似了)
      到此,整个过程就介绍完了,我们可以看到,对于每个函数过程,系统都为其分配了单独的栈,也就是所谓的栈帧,仔细观察代码可以进一步发现,每个过程的汇编代码有几部分都是大致一样的,这就提示我们系统对每个过程分配方式都是统一的,也就是说先给你分配好了,至于怎么用再看具体情况,其实我们可以把代码看成三部分:(建立部分)作用是初始化栈帧;(主体部分)执行过程的实际计算,(结尾部分)恢复栈的状态和过程返回.至于怎么返回,内存如何释放,堆栈如何平衡等问题就是以上过程的反推.
       结语:我个人认为适当的掌握些硬件汇编知识对于我们学生来说很有帮助,这也是受我们学校一位老师的启发,以我为鉴,我想作为一个计算机学生可以适当的做些项目,但不要沉迷于某些项目的反复开发当中,这对自己知识结构的搭建没有太大好处,平时应该"做做学学,学学做做",最后还是应该回到书本当中,同时在不同的实践中深刻体会知识,检验自己,这样才会有提高.说实话写博客真很耗时啊,不过在这个过程中也算自己巩固下知识.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值