可可西

函数调用-汇编分析

0.寄存器

4个数据寄存器EAXEBXECXEDX

在用途方面,它们有各自默认的用途:
EAX - 用来保存函数的返回值;
ECX - 用来存放this指针。

2个指针寄存器EBPESP

EBP - 基址指针寄存器,作为堆栈数据存取操作的基本地址指针寄存器;
ESP - 栈指针寄存器,指示堆栈的当前偏移地址(堆栈顶部到堆栈起始位置的距离)。

2个变址寄存器ESIEDI

变址寄存器主要用于存放存储单元在段内的偏移量,用它们可实现多种存储器操作数的寻址方式,
为以不同的地址形式访问存储单元提供方便。 ESI - 源变址寄存器;  EDI - 目标变址寄存器

6个段寄存器ESCSSSDSFSGS

CS - 代码段寄存器(Code Segment Register),存放当前正在执行代码段的起始地址;
DS - 数据段寄存器(Data Segment Register),存放当前正在执行程序所用数据段的起始地址;
SS - 堆栈段寄存器(Stack Segment Register),存放当前正在执行程序暂时保留信息段的起始地址,用来指示堆栈起始位置的指针;
ES/FS/GS - 附加段寄存器(Extra Segment Register),存放程序的数据段的起始地址,为程序设计使用多个数据段带来方便。

-- 16位汇编,示意图如下(段寄存器中的值×16[2^4],得到段基址的值):

1个指令指针寄存器EIP/IP

指令指针EIP/IP(Instruction Pointer) - 存放下次将要执行的指令在代码段的偏移量。
当取出一条指令后,EIP/IP自动加上该指令的长度或者形成转移地址,又指向下一条指令的地址,
从而可以控制有序的执行程序。

1个标志寄存器EFlags/Flags

1. 栈(stack)

栈是由程序开辟的一片内存区域;先进后出;在windows下默认大小为1MB,当线程栈内存被用光,就会报“栈溢出”错误。

栈空间自动增长:CPU访问Guard页时会产生页错误并开始执行系统的内存管理函数,当内存管理函数检测到PAGE_GUARD属性后,会清除对应页面的PAGE_GUARD属性,

然后调用一个名为MiCheckForUserStackOverflow的系统函数,该函数会从当前线程的TEB中读取用户态栈的基本信息并检查导致异常的地址,

如果导致异常的被访问地址不属于栈空间范围,则返回STATUS_GUARD_PAGE_VIOLATION,否则MiCheckForUserStackOverflow函数会计算栈中是否还有足够的剩余空间可以创建一个新的栈保护页面。

如果有,则调用ZwAllocateVirtualMemory从保留的空间中在提交一个具有PAGE_GUARD属性的内存页。

新的栈保护页与原来的紧邻,经过这样的操作后,栈的保护页向低地址方向平移了一位,栈的可用空间增大了一个页面的大小。

栈溢出:当提交的栈空间被用完,且Guard页又被访问时;MiCheckForUserStackOverflow函数返回STATUS_STACK_OVERFLOW,触发栈溢出异常。

 

栈用于存放局部变量、函数调用参数、寄存器等内容。

总是向下增长的(位于高地址,向低地址进行分配)。示意图如下:

:函数调用点返回地址:告诉被调用者的return语句应该return到哪里去,通常指向该函数调用的下一条语句(代码段映射到内存中的绝对地址)

EBP指向函数活动记录的一个固定位置,又称为栈帧指针(Frame Pointer)。

通过EBP加上偏移值可以很好地访问函数参数和局部变量的值。

进入一个函数后,其EBP的值为:上层调用点的ESP-8(上层函数的EBP + 上层函数调用点的返回地址)即:上层调用点的ESP = EBP+8
当函数的返回值不为复合类型时,可通过EBP + 8 访问第一个参数,EBP + 8 + sizeof(第一个参数)来访问第二个参数,... ...

PUSH(压栈):ESP -= 4
Call函数调用:ESP -= 4
POP(弹栈):ESP += 4
函数返回(ret):ESP += 4
函数返回(ret 8):ESP += 8

2. 寻址方式

(1) 立即数寻址
mov eax,44332211h
(2) 寄存器寻址
mov eax,ebx
(3) 直接寻址
mov eax,[1234h] ;[]表示取地址1234h处的DWORD值
(4) 寄存器间接寻址
mov eax,[ebx]
(5) 寄存器相对寻址(带位移量的寄存器间接寻址)
mov eax,[ebx+80h]
(6) 基址变址寻址(基址的变址寻址)
mov eax,[ebx+esi]
(7) 相对基址变址寻址(基址的带位移量的变址寻址)
mov eax,[ebx+esi+80h]
(8) 带比例的变址寻址
mov eax,[esi*2]
(9) 基址的带比例的变址寻址
mov eax,[ebx+esi*4]
(10) 基址的带位移量的带比例的变址寻址
mov eax,[ebx+esi*8+80h]
(11) I/O端口的直接寻址
in eax,80h
(12) I/O端口的寄存器间接寻址
in eax,dx

16位和32位寻址时的4元素规定:

有效地址元素 16位寻址 32位寻址
基址寄存器 BX,BP 任何32位通用寄存器
变址寄存器 SI,DI 除ESP外的任何32位通用寄存器
比例因子 无(或1) 1,2,4,8
位移量 0,8,16位 0,8,32位

32位存储器寻址方式的组成公式为

32位有效地址 = 基址寄存器+(变址寄存器 × 比例)+位移量

其中的4个组成部分是:
· 基址寄存器——任何8个32位通用寄存器之一;
· 变址寄存器——除ESP之外的任何32位通用寄存器之一;
· 比例——可以是1∕2∕4∕8(因为操作数的长度可以是1∕2∕4∕8字节);
· 位移量——可以是8∕32位值。

16位存储器分段寻址计算方法:

存储段内的每个单元的物理地址PA(Physical Address),用“段基址 : 段内偏移地址”来表达。
段基址即段地址SA(Segment Address)是相应段的起始地址。
段内偏移地址即偏移地址或称有效地址EA(Effective Address),
它是该单元的物理地址PA到段地址SA的距离(字节数),即:EA = PA - SA。
由于实方式下段寄存器只有16位,EA由寄存器BX、BP、SI或DI的内容来计算,也只有16位,
因此,需要将段寄存器的内容左移4位后与EA相加来达到PA需要的20位地址(1MB空间)。
即:PA = SA + EA = 段寄存器的内容左移4位(×16) + EA

3. 常用汇编指令

指令由操作码+操作数组成。8086指令采用变长指令,由1-6字节组成。

参见:http://www.cnblogs.com/caoyawei/archive/2009/04/24/1442569.html

4. 示例分析(VC6反汇编)   注:汇编代码的注释符为 ;

/**************************main****************************/
       int main(int argc, char* argv[])
       {
004106B0   push        ebp            // 保存上一层函数栈底指针,esp -= 4
004106B1   mov         ebp,esp        // ebp = esp 设置当前函数栈底指针
004106B3   sub         esp,4Ch        // esp -= 4Ch  为临时变量开辟空间
004106B6   push        ebx            // 保存ebx到堆栈中,esp -= 4
004106B7   push        esi            // 保存esi到堆栈中,esp -= 4
004106B8   push        edi            // 保存edi到堆栈中,esp -= 4
004106B9   lea         edi,[ebp-4Ch]      // edi = ebp - 4Ch
004106BC   mov         ecx,13h            // ecx = 13h
004106C1   mov         eax,0CCCCCCCCh     // eax = 0CCCCCCCCh
004106C6   rep stos    dword ptr [edi]        // 将edi地址起的(13h*4=4Ch)大小的内存区域初始化为0CCh
            int a = 1;
004106C8   mov         dword ptr [ebp-4],1    // [ebp-4] = 1
            int b = 2;
004106CF   mov         dword ptr [ebp-8],2    // [ebp-8] = 2

            int c = sum(a, b);                  // 参数从右向左,依次压栈
004106D6   mov         eax,dword ptr [ebp-8]    // eax = [ebp-8]
004106D9   push        eax                      // 保存eax到堆栈中,[esp] = eax,esp -= 4
004106DA   mov         ecx,dword ptr [ebp-4]    // ecx = [ebp-4]
004106DD   push        ecx                      // 保存ecx到堆栈中,[esp] = ecx,esp -= 4
004106DE   call        @ILT+5(sum) (0040100a)   // 调用@ILT+5跳转,跳转到sum函数,esp -= 4
004106E3   add         esp,8                    // 调用方进行堆栈平衡  esp += 8(参数a、参数b)
004106E6   mov         dword ptr [ebp-0Ch],eax  // [ebp-0Ch] = eax(eax中存放的是返回值)
    
            return 0;
004106E9   xor         eax,eax        // eax = 0,效率更高
        }
004106EB   pop         edi            // 恢复edi,esp += 4
004106EC   pop         esi            // 恢复esi,esp += 4
004106ED   pop         ebx            // 恢复ebx,esp += 4
004106EE   add         esp,4Ch            // esp += 4Ch
004106F1   cmp         ebp,esp            // 检查栈是否平衡(即:ebp是否等于esp)
004106F3   call        __chkesp (00401060)// 对esp的值进行检查
004106F8   mov         esp,ebp            // esp = ebp
004106FA   pop         ebp                // 恢复上一层函数栈底指针
004106FB   ret                            // 返回,esp += 4
/*************************************************************/

/******************编译器生成的一个jmp**********************/
0040100A   jmp         sum (00401030)        // 跳转到sum函数的第1条指令
/*************************************************************/

/**************************sum函数***************************/
       int sum(int a, int b)
      {
00401030   push        ebp            // esp -= 4
00401031   mov         ebp,esp
00401033   sub         esp,40h //esp -= 0x40  为临时变量开辟64字节空间
00401036   push        ebx // esp -= 4
00401037   push        esi // esp -= 4
00401038   push        edi // esp -= 4
00401039   lea         edi,[ebp-40h] // edi=ebp-40h
0040103C   mov         ecx,10h
00401041   mov         eax,0CCCCCCCCh
00401046   rep stos    dword ptr [edi]
          return a+b;
00401048   mov         eax,dword ptr [ebp+8]    // eax = [ebp+8],参数a
0040104B   add         eax,dword ptr [ebp+0Ch]  // eax += [ebp+0Ch],参数b
       }
0040104E   pop         edi
0040104F   pop         esi
00401050   pop         ebx
00401051   mov         esp,ebp
00401053   pop         ebp
00401054   ret                // 返回,esp += 4
/*************************************************************/

/**************************esp检查***************************/
00401060   jne         __chkesp+3 (00401063)    // 若__chkesp+3不等于00401063,则跳转到00401063
00401062   ret
00401063   push        ebp            // 错误处理代码
00401064   mov         ebp,esp
00401066   sub         esp,0
00401069   push        eax
0040106A   push        edx
0040106B   push        ebx
0040106C   push        esi
0040106D   push        edi
0040106E   push        offset string "The value of ESP was not properl"... (00422030) //等价于push 00422030h
00401073   push        offset string "" (0042202c)    //将字符串""的首地址0042202c值压栈
00401078   push        2Ah
0040107A   push        offset string "i386\\chkesp.c" (0042201c)
0040107F   push        1
00401081   call        _CrtDbgReport (00401390)
00401086   add         esp,14h
00401089   cmp         eax,1
0040108C   jne         __chkesp+2Fh (004010df)
0040108E   int         3            // 软中断
0040108F   pop         edi
00401090   pop         esi
00401091   pop         ebx
00401092   pop         edx
00401093   pop         eax
00401094   mov         esp,ebp
00401096   pop         ebp
00401097   ret
/*************************************************************/

以上汇编中出现了@ILT(Incremental Link Table,Link配置:vc6--勾选“Link incrementally”,vs--“Enable Incremental Linking”设置成:“Yes(/INCREMENTAL)”)。

什么是Incremental Link Table呢?

想想如果我们自己要做编译器(compiler)和连接器(linker),当然希望编译连接运行得越快越好,
同时也希望产生的二进制代码也是又快又小,上帝是公平的,鱼与熊掌不可兼得,
所以我们自然想到用两种build方式,一种Release,编译慢一些,但是产生的二进制代码紧凑精悍,
一种Debug,编译运行快,产生的代码臃肿一点没关系,Debug版本嘛,就是指望程序员在开发的时候反复的build,
为了不浪费程序员的时候,要想尽办法让编译连接速度变快。
假如一个程序有连续两个foo和bar (所谓连续,就是他们编译连接之后函数体连续存放),
foo入口位置在0×0400,长度为0×200个字节,那么bar入口就应该在0×0600 = 0×0400+0×0200。
程序员在开发的时候总是频繁的修改code然后build,假如程序员在foo里面增加了一些内容,
现在foo函数体占0×300个字节了,bar的入口也就只好往后移0×100变成了0×0700,这样就有一个问题,
如果foo在程序中被调用了n次,那么linker不得不修改这n个函数调用点,虽然linker不嫌累,
但是link时间长了,程序员会觉得不爽。所以MSVC在Debug版的build,
不会让各个函数体之间这么紧凑,每个函数体后都有padding(全是汇编代码int 3,作用是引发中断,
这样因为古怪原因运行到不该运行的padding部分,会发生异常),有了这些padding,
就可以一定程度上缓解上面提到的问题,不过当函数增加内容太多超过padding,还是有问题,怎么办呢?
MSVC在Debug build中用上了Incremental Link Table, ILT其实就是一串jmp语句,每个jmp语句对应一个函数,
jmp的目的地就是函数的入口点,和没有ILT的区别是,现在对函数的调用不是直接call到函数入口点了,
而是call到ILT中对应的位置,而这个位置上什么也不做,直接jmp到函数中去。
这样的好处是,当一个函数入口地址改变时,只要修改ILT中对应值就搞定了,用不着修改每一个调用位置,
用一个冗余的ITL把时间复杂度从O(n)将为O(1),值得,当然Debug版的二进制文件会稍大稍慢,Release版不会用上ILT。

其他示例:http://blog.csdn.net/jltxgcy/article/details/8668666

5. 函数调用约定__cdecl/__stdcall/__fastcall/__thiscall)  

参见:http://blog.csdn.net/walkinginthewind/article/details/7580109

    http://www.cnblogs.com/Dah/archive/2006/11/29/576867.html

    http://hi.baidu.com/148332727/item/9e9b38ad9571afad29ce9dc0

    http://www.cnblogs.com/qinfengxiaoyue/archive/2013/02/04/2891908.html

6. 参考

http://jpkc.szpt.edu.cn/2006/IA32/ftp/address/frames/content/CHAPT3/index.htm

http://jpkc.zzu.edu.cn/hbyycai/courses/step.asp?id=13

posted on 2013-02-16 18:03  可可西  阅读(1928)  评论(0编辑  收藏  举报

导航