分类目录

展开|收起

看你喜欢

(1) (1) (42) (1) (1) (1) (16) (2) (1) (1) (4) (1) (2) (7) (4) (1) (1) (1) (1) (3) (1) (5) (1) (1) (1) (1) (1) (2) (1) (4) (4) (3) (1) (1) (2) (1) (37) (2) (1) (5) (3) (1) (4) (1) (1) (11) (3) (1) (9) (3) (1) (23) (2) (1) (2) (1) (1) (1) (1)

最新精华

函数调用汇编分析【3】- 实例分析1(值传递)

下面我们结合具体的代码来进行直观地分析,例子都是在VC下编译的。

3.1 值传递

(1)源程序

函数用值方式传递两个int参数,并返回一个int参数。

int add(int c , int d)
{
    int e;
    e=c+d;
    return e;
}

void main()
{
    int a = 1;
    int b= 2;
    int c= 0;
    c=add(a,b);
    while (1)
    {
    }
}

(2)汇编代码

1:    int add(int c , int d)
2:    {
00401020   push        ebp     // 把main的ebp入栈
00401021   mov         ebp,esp  // 设置add的ebp,可以看出[ebp]==main的ebp值
00401023   sub         esp,44h  // 保留44h个字节大小的栈空间,
用于存放局部变量,
                                  实际本例只有一个内部变量e
00401026   push        ebx
00401027   push        esi
00401028   push        edi     // 在保留的栈空间以外(更低的地址)
                                保存ebx,esi和edi寄存器。
00401029   lea         edi,[ebp-44h]
0040102C   mov         ecx,11h    // 44h/4=11h,因为是按dword来初始化
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]  // 初始化保留的栈空间,用0xcc来填充
                                     这个是有目的的,见后说明。
3:        int e;
4:        e=c+d;
00401038   mov         eax,dword ptr [ebp+8] // 取第一个入参,即行参c。
因为d先入栈
0040103B   add         eax,dword ptr [ebp+0Ch]   // c+d
0040103E   mov         dword ptr [ebp-4],eax     // e=c+d
5:        return e;
00401041   mov         eax,dword ptr [ebp-4]     // e通过eax寄存器返回
6:
7:    }
00401044   pop         edi      // 恢复edi
00401045   pop         esi      // 恢复esi
00401046   pop         ebx     // 恢复 ebx,可以看出和入栈的次序相反
00401047   mov         esp,ebp  // 恢复caller即main的esp
00401049   pop         ebp     // 恢复main的esp
                                 此语句执行后,栈顶就是返回地址了
0040104A   ret                  // 函数返回,即EIP=0040B4DA

8:
9:
10:   void main()
11:   {
0040B4A0   push        ebp      // 把main的调用者的ebp入栈
                                   main函数也是callee,呵呵
0040B4A1   mov         ebp,esp   //  设置main的ebp
0040B4A3   sub         esp,4Ch   //  以下几条语句意义参见add中说明
0040B4A6   push        ebx
0040B4A7   push        esi
0040B4A8   push        edi
0040B4A9   lea         edi,[ebp-4Ch]
0040B4AC   mov         ecx,13h
0040B4B1   mov         eax,0CCCCCCCCh
0040B4B6   rep stos    dword ptr [edi]
12:   int a = 1;
0040B4B8   mov         dword ptr [ebp-4],1    // 栈中保存a ,注意偏移量是-4
13:   int b= 2;
0040B4BF   mov         dword ptr [ebp-8],2    // 栈中保存b
14:   int c= 0;
0040B4C6   mov         dword ptr [ebp-0Ch],0  // 栈中保存c
15:
16:   c=add(a,b);
0040B4CD   mov         eax,dword ptr [ebp-8]    //  取b的值
0040B4D0   push        eax                    //  入栈作为第二个参数
0040B4D1   mov         ecx,dword ptr [ebp-4]
0040B4D4   push        ecx                      // a的值入栈作为第一个参数
0040B4D5   call        @ILT+10(test) (0040100f)     // 实际就是调用add
0040B4DA   add         esp,8                    // 调整堆栈指针,因为有两个int型参数(实参),共8字节
0040B4DD   mov         dword ptr [ebp-0Ch],eax  // add返回值赋给c
17:   while (1)
0040B4E0   mov         edx,1
0040B4E5   test        edx,edx
0040B4E7   je          main+4Bh (0040b4eb)
18:   {
19:   }
0040B4E9   jmp         main+40h (0040b4e0)
20:
21:   }
0040B4EB   pop         edi
0040B4EC   pop         esi
0040B4ED   pop         ebx
0040B4EE   add         esp,4Ch       // 堆栈指针回退4Ch,即开始保留的栈空间
0040B4F1   cmp         ebp,esp       // 比较调整后的esp是否和ebp相同
0040B4F3   call        __chkesp (00401050)  //堆栈检查
0040B4F8   mov         esp,ebp
0040B4FA   pop         ebp
0040B4FB   ret

@ILT+0(?test@@YAHHH@Z):
00401005   jmp         add (00401020)
@ILT+5(_main):
0040100A   jmp         main (0040b4a0)
0040100F   jmp         add (00401020)  // 这里是调转到add,不是函数调用
00401014   int         3              //  INT 3指令,断点指令,
是调试器故意填充的,实际就是值0xCC
正常是不会走到这里的,
走到这里就会触发断点异常,下同。
00401015   int         3
00401016   int         3
00401017   int         3
00401018   int         3
00401019   int         3
0040101A   int         3
0040101B   int         3
0040101C   int         3
0040101D   int         3
0040101E   int         3
0040101F   int         3

__chkesp:
00401050   jne         __chkesp+3 (00401053)  // 如果堆栈指针错误,说明有内存泄露或越界
00401052   ret             // 堆栈指针正确,直接返回
00401053   push        ebp
00401054   mov         ebp,esp
00401056   sub         esp,0
00401059   push        eax
0040105A   push        edx
0040105B   push        ebx
0040105C   push        esi
0040105D   push        edi
0040105E   push        offset string "The value of ESP was not properl"... (0041fe60)
00401063   push        offset string "" (0041fc20)
00401068   push        2Ah
0040106A   push        offset string "i386\\chkesp.c" (0041fe50)
0040106F   push        1
00401071   call        _CrtDbgReport (00407180)  // 调用错误处理函数
00401076   add         esp,14h   // 调整堆栈指针
00401079   cmp         eax,1     // 返回值和1比较
0040107C   jne         __chkesp+2Fh (0040107f)  // 返回值不等于1,说明正确,跳转到返回代码
0040107E   int         3     // 返回值为1,说明出错,触发3号中断
                              调试器已经为之提供中断服务程序,
                              VC中会弹出一个对话框,提示用户代码异常
0040107F   pop         edi
00401080   pop         esi
00401081   pop         ebx
00401082   pop         edx
00401083   pop         eax
00401084   mov         esp,ebp
00401086   pop         ebp
00401087   ret
00401088   int         3
00401089   int         3
0040108A   int         3
0040108B   int         3
0040108C   int         3
0040108D   int         3
0040108E   int         3
0040108F   int         3

说明:

  • 调试版本

add和main函数里第三条指令调整栈指针是VC编译调试版本特意加的,主要是为了调试时的栈检查。大家肯定比较奇怪,为何要把这个预留的用于存放局部变量的空间用0xCC来初始化呢?是有目的的。还有一个有趣的现象:当我们用VC6进行调试时,常常会观察到一块刚分配的内存或字符串数组里面被填充满了‘CC’。如果是在中文环境下,因为0xCCCC恰好是汉字‘烫’字的简码,所以会观察到很多‘烫烫烫烫烫烫…’,CC正好是INT3指令的机器码,这是偶然的么?答案是否定的。因为这是有意为之。为了辅助调试,调试版本的运行库会用0xCC来填充刚刚分配的缓冲区。这样如果因为缓冲区或堆栈溢出时程序指针意外指向了这些区域,那么便会因为遇到这些自动填充的INT 3指令而马上中断到调试器。另一方面,编译器也经常用INT 3指令来填充函数或代码段末尾的空闲区域。这也可以解释为什么有时我们没有手工插入任何对INT 3的调用,但是也会遇到下图所示的对话框。
user-breakpoint

这个对话框提示触发了一个用户断点,这个我们在跑VC工程的时候或许碰过,现在应该知道是怎么出来的了吧。它说明我们的程序跑到不该去的地方了,这些地方被VC编译器事先填充了0xCC,即INT3指令的机器码。我们可以自己试验一下,用嵌入汇编直接插入一个INT3指令,也会触发这个异常。

int main(INT argc, char* argv[])
{
    _asm INT 3   // 嵌入汇编直接插入INT3指令
    printf("Hello INT 3!\n");
    return 0;
}

知道了这个,我们后面分析时就可以忽略这段内容,这些只是VC编译调试版本特意加的,实际release版本和我们前台用gcc编译的版本中没有这些。

另:INT 3触发的是一种软件异常,前面介绍的PageFault也是一种软件异常,CPU都有相应的中断向量和之对应。其它的还有除0,单步执行等,这样调试器就可以捕获这些异常,从而和用户交互,即实现调试过程。

  • main调用add后的栈帧结构main调用add后的栈帧结构

(3)小结

可以看出汇编代码完全符合C调用规范,参数从右至左入栈,caller调整栈指针。函数调用时传入的所谓“实参”实际是原参数的一个副本,在栈中占用独立的空间,从代码看出,“形参”也是指这些地方,相当于把“实参”赋给了“形参”。可以想象,如果callee中改变了这些副本的值,是不会影响到原来的参数a和b的。看到这里,是不是对值形式传递参数的函数调用有了更直观的认识了呢?这个的确比C语言中用“实参”赋给“形参”这样的描述更清楚。或许这里对“实参”和“形参”的字面描述不够准确,但要说的意思就是这样的。

不过简单的值传递很简单,怎么说都好理解,后面将会涉及到指针和引用,大家看过以后,也许会对自己理解的C中的相应描述有个重新的认识。后面的例子略去了通用的代码如构造栈帧框架,__chkesp以及int 3指令的解释。

  打分:5.0/5 (共1人投票)
(浏览总计: 90 次)
Add Comment Register



发表回复

  

  

  

您可以使用这些HTML标签

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>