函数调用-汇编分析
0.寄存器
4个数据寄存器:EAX、EBX、ECX、EDX
在用途方面,它们有各自默认的用途:
EAX - 用来保存函数的返回值;
ECX - 用来存放this指针。
2个指针寄存器:EBP、ESP
EBP - 基址指针寄存器,作为堆栈数据存取操作的基本地址指针寄存器;
ESP - 栈指针寄存器,指示堆栈的当前偏移地址(堆栈顶部到堆栈起始位置的距离)。
2个变址寄存器:ESI、EDI
变址寄存器主要用于存放存储单元在段内的偏移量,用它们可实现多种存储器操作数的寻址方式,
为以不同的地址形式访问存储单元提供方便。 ESI - 源变址寄存器; EDI - 目标变址寄存器
6个段寄存器:ES、CS、SS、DS、FS和GS
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