找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 3308|回复: 0

函数反汇编以及栈结构分析

[复制链接]

33

主题

1

回帖

561

积分

用户组: 大·技术宅

UID
580
精华
26
威望
28 点
宅币
341 个
贡献
0 次
宅之契约
0 份
在线时间
8 小时
注册时间
2014-12-8
发表于 2015-1-4 16:51:24 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×
  1. #include <iostream>
  2. using namespace std;

  3. int func(int a,int b);

  4. void main()
  5. {
  6.         int x = 1;
  7.         int y = 2;
  8.         int z = func(x,y);
  9. }

  10. int func(int a,int b)
  11. {
  12.         return a + b;
  13. }
复制代码
汇编解析:
  1. void main()
  2. {
  3. //这个函数头产生的汇编代码是:
  4. main:
  5. 013E13A0  push        ebp  
  6. 013E13A1  mov         ebp,esp  
  7. 013E13A3  sub         esp,0E4h  
  8. 013E13A9  push        ebx  
  9. 013E13AA  push        esi  
  10. 013E13AB  push        edi  
  11. 013E13AC  lea         edi,[ebp-0E4h]  
  12. 013E13B2  mov         ecx,39h  
  13. 013E13B7  mov         eax,0CCCCCCCCh  
  14. 013E13BC  rep stos    dword ptr es:[edi]  

  15. *******************************************************
  16.         int x = 1;
  17.         int y = 2;
  18. //这个的汇编代码是:
  19. 013E13BE  mov         dword ptr [x],1  
  20. 013E13C5  mov         dword ptr [y],2  
  21. *******************************************************
  22. //接下来就是函数的调用了,先是把参数压栈
  23. 013E13CC  mov         eax,dword ptr [y]  
  24. 013E13CF  push        eax  
  25. 013E13D0  mov         ecx,dword ptr [x]  
  26. 013E13D3  push        ecx  
  27. //参数压栈后就是调用函数
  28. 013E13D4  call        func (13E11D1h)
  29. // 在这个地方用F11调试步进,可以进入子函数func的汇编代码,如下:
  30. 013E11D1  jmp         func (13E35B0h)
  31. func:
  32. 013E35B0  push        ebp  
  33. 013E35B1  mov         ebp,esp  
  34. 013E35B3  sub         esp,0C0h  
  35. 013E35B9  push        ebx  
  36. 013E35BA  push        esi  
  37. 013E35BB  push        edi  
  38. 013E35BC  lea         edi,[ebp-0C0h]  
  39. 013E35C2  mov         ecx,30h  
  40. 013E35C7  mov         eax,0CCCCCCCCh  
  41. 013E35CC  rep stos    dword ptr es:[edi]  
  42. 013E35CE  mov         eax,dword ptr [a]  
  43. 013E35D1  add         eax,dword ptr [b]  
  44. 013E35D4  pop         edi  
  45. 013E35D5  pop         esi  
  46. 013E35D6  pop         ebx  
  47. 013E35D7  mov         esp,ebp  
  48. 013E35D9  pop         ebp  
  49. 013E35DA  ret  
  50. //func函数执行完毕后返回主函数中
  51. 013E13D9  add         esp,8  
  52. 013E13DC  mov         dword ptr [z],eax  
  53. 013E13DF  xor         eax,eax  
  54. 013E13E1  pop         edi  
  55. 013E13E2  pop         esi  
  56. 013E13E3  pop         ebx  
  57. 013E13E4  add         esp,0E4h  
  58. 013E13EA  cmp         ebp,esp  
  59. 013E13EC  call        @ILT+315(__RTC_CheckEsp) (13E1140h)  
  60. 013E13F1  mov         esp,ebp  
  61. 013E13F3  pop         ebp  
  62. 013E13F4  ret
复制代码
接下来再看看各个寄存器都是怎么变的,里面存的什么东西
1         首先进入main函数最开始:
1.png
再看看此时的寄存器的内容:
EAX = 00851D88 EBX = 7E0BC000 ECX = 00854A30 EDX = 00000001 ESI = 00000000 EDI = 00000000 EIP = 013E13A0 ESP = 009BF76C EBP = 009BF7B8 EFL = 00000202
2        再进行单步调试:
1.png
再观察一下寄存器的内容:
EAX = 00851D88 EBX = 7E0BC000 ECX = 00854A30 EDX = 00000001 ESI = 00000000 EDI = 00000000 EIP = 013E13A1 ESP = 009BF768 EBP = 009BF7B8 EFL = 00000202
我们发现esp的值少了4,这就说明了函数压栈时是从高字节到低字节递减,越是后压进来的越是字节低。
3         再把esp的值保存一下
1.png
再看一下寄存器的内容:
EAX = 00851D88 EBX = 7E0BC000 ECX = 00854A30 EDX = 00000001 ESI = 00000000 EDI = 00000000 EIP = 013E13A3 ESP = 009BF768 EBP = 009BF768 EFL = 00000202
Ebp就保存了esp的值
4        再步进到直到main函数调用func函数之前
1.png
我们再观察内存的情况
连续定义的变量,他们在内存中紧挨着,之间相差4个字节。其实在堆栈中每次内存偏移必须是4个字节的整数倍。这个在参数数目以及类型可变的函数中可以体会到字节对齐的这种思想。
我们再观察下此时的寄存器的内容:
EAX = 00000002 EBX = 7E0BC000 ECX = 00000001 EDX = 00000001 ESI = 00000000 EDI = 009BF768 EIP = 013E13D3 ESP = 009BF674 EBP = 009BF768 EFL = 00000206
我们可以发现实参x,y的值都是先传进寄存器,然后再压栈。当把参数全部压进去后才开始调用函数。
同时我们也可以发现EIP寄存器它里面总是放着下一条要执行的指令。
5        再调试函数到子函数func里面
1.png
观察此时对应的寄存器
EAX = 00000002 EBX = 7E0BC000 ECX = 00000001 EDX = 00000001 ESI = 00000000 EDI = 009BF768 EIP = 013E35B0 ESP = 009BF66C EBP = 009BF768 EFL = 00000206
其中的EBP = 009BF768是之前进入main函数时堆栈的栈顶指针。现在我们要把它保存,所以要push以下,放到堆栈的内存中。然后呢,因为进入main函数之后,直到进入func子函数时,我们又要维护这个在main函数中产生的栈顶指针,所以要把栈顶指针的值保存以下,保存在ebp中,所以此时要move以下。通俗的说就是,你这个子函数要在人家main函数的地盘进行栈操作,你就得负责人家main函数之前的栈(栈顶指针),这样在子函数调用结束后,main函数可以直接接着原来的栈干事。
同时子函数维护的还有ebx,esi,edi,凡是要在子函数中用到的寄存器,那么子函数就要保存调用它的函数中在这些寄存器中的值。
6        往后再看看函数的返回值,继续单步调试到:
1.png
再看相关的寄存器
EAX = 00000003 EBX = 7E0BC000 ECX = 00000000 EDX = 00000001 ESI = 00000000 EDI = 009BF668 EIP = 013E35D4 ESP = 009BF59C EBP = 009BF668 EFL = 00000206
可以看到两个数相加的结果保存在了寄存器EAX中。
函数体中的代码执行完毕就要释放栈空间,接下来就是一系列的pop了。
7        我们再看看pop完后的寄存器:
1.png
观察对应的寄存器:
EAX = 00000003 EBX = 7E0BC000 ECX = 00000000 EDX = 00000001 ESI = 00000000 EDI = 009BF768 EIP = 013E35DA ESP = 009BF66C EBP = 009BF768 EFL = 00000206
我们可以看到EBP变回了之前的 009BF66C(main函数中保存的先前函数的栈顶指针),ESP变回了之前的009BF66C(main函数进入func子函数栈顶指针)。这样就把之前保存在栈内存中的东西重新返回到寄存器中了。
再研究下从子函数返回后main函数中的代码
  1. 013E13D9  add         esp,8  //func函数有两个int参数,所以释放8个字节
  2. 013E13DC  mov        dword ptr [z],eax  //把函数的返回值赋给变量z
  3. 013E13DF  xor         eax,eax  //将eax清零,xor表示异或运算,自己跟自己异或肯定是0
  4. 013E13E1  pop         edi  
  5. 013E13E2  pop         esi  
  6. 013E13E3  pop         ebx  
  7. 013E13E4  add         esp,0E4h  
  8. 013E13EA  cmp         ebp,esp  
  9. 013E13EC  call        @ILT+315(__RTC_CheckEsp) (13E1140h)  
  10. 013E13F1  mov         esp,ebp  
  11. 013E13F3  pop         ebp  
  12. 013E13F4  ret  
复制代码
总结:
1:函数的返回值
通过这些例子我们也可以看出函数可以返回在函数中定义的局部变量的值,因为函数的返回值最后传递给了EAX 寄存器,所以即便函数调用结束后,栈中内存释放,但是寄存器却保存了返回值,但是如果返回一个指向栈内存的指针或者地址那就不行了,因为栈被释放了。
2:参数的获取
有些人或许要问,参数的值都是实参传递给形参的,这还需要什么获取,这话没有错误,但是如果函数时比较特殊的函数,例如:
int   func(…);
int   func(int,int);
int   func(int,…);
这种类型的呢,这些函数要么是可变参数,要么是参数名字没有给。在这种极端比较恶心的条件下,我们依然可以获取传递的实参的值。通过上面我们可以发现栈的结构是:
参数n
参数n-1

参数1
函数的返回地址(call 指令后面的那一句)
EBP
局部变量1
局部变量2

局部变量n-1
局部变量n
EBX
ESI
EDI
所以我们可以通过EBP寄存器通过偏移固定的位置就可以找到对应的参数,例如第一个参数只需要EBP的地址加上8得到。
3 关于
1.png
这里esp减值的问题
通过控制函数的形参个数,以及函数内部定义的局部变量我们可以发现有这样一个规律。
(1)        函数内部没有定义局部变量是,改变形参的个数不影响esp减去的值,此时esp减去的值都是0C0h。
(2)        函数内部每多定义一个int类型的局部变量,减去的值就会增加12,就是说多开辟12个字节。
(3)        函数内部每多定义一个double类型的局部变量,减去的值就会增加16,就是说多开辟16个字节
(4)        函数内部每多定义一个char类型的局部变量,减去的值就会增加12,就是说多开辟12个字节。
(5)        函数内部每多定义一个float类型的局部变量,减去的值就会增加12,就是说多开辟12个字节。
4 寄存器
4个数据寄存器(EAX、EBX、ECX和EDX)
2个变址和指针寄存器(ESI和EDI) 2个指针寄存器(ESP和EBP)
6个段寄存器(ES、CS、SS、DS、FS和GS)
1个指令指针寄存器(EIP) 1个标志寄存器(EFlags)
EBP :(Extended Base Pointer)基址指针寄存器,存放栈底指针
ESP :(Extended Stack Pointer)栈指针寄存器,存放栈顶指针
EAX :(Extended Accumulate )累加寄存器,用做加减乘除运算
EBX :(Extended Base)基址寄存器,可以用来存储指针
ECX :(Extended Count)计数寄存器,用在循环中控制循环的次数
EDX :(Extended Data)数据寄存器,在进行乘、除运算时,它可作为默认的操作数参与运算,通常用来保存余数。也 可用于存放I/O的端口地址
ESI:(Extended Source Index)源索引寄存器,可以存放任何数据,但是习惯把它存放指针。
EDI:(Extended Destination Index)目的索引寄存器,但是ESI和EDI虽然他俩经常配合,但是之间的区别还是比较大的。在字符串处理指令中,ESI和DS结合,DS:ESI构成全指针,在穿处理指令方面,ESI负责只读,EDI负责只写。关于为啥只能ESI和DS结合形参全指针,这是硬件构造决定的,没有选择性。ES:EDI,同样构成全指针。
EIP:(Extended Instruction Pointer)指令指针寄存器,要存放下一条执行指令的地址
EFL:(Extended Flag)标志寄存器,用来运算结果标志(进位,溢出等),状态控制标志(跟踪,中断允许等)。
CS——代码段寄存器(Code Segment Register),其值为代码段的段值;
DS——数据段寄存器(Data Segment Register),其值为数据段的段值;
ES——附加段寄存器(Extra Segment Register),其值为附加数据段的段值;
SS——堆栈段寄存器(Stack Segment Register),其值为堆栈段的段值;
FS——附加段寄存器(Extra Segment Register),其值为附加数据段的段值;
GS——附加段寄存器(Extra Segment Register),其值为附加数据段的段值。
5 个别语句解释
  1. 002E13F3  sub         esp,0CCh  ;        (1)
  2. 002E13F9  push        ebx  :                (2)
  3. 002E13FA  push        esi  ;                (3)
  4. 002E13FB  push        edi  ;                (4)
  5. 002E13FC  lea         edi,[ebp-0CCh]  ;        (5)
  6. 002E1402  mov         ecx,33h  ;        (6)
  7. 002E1407  mov         eax,0CCCCCCCCh;                (7)  
  8. 002E140C  rep stos    dword ptr es:[edi] ;        (8)
复制代码
(1)        表示栈指针减去occh,就是开辟0cch的栈内存空间来存放临时变量
(2)        保存ebx到栈内存
(3)        保存esi到栈内存
(4)        保存edi到栈内存
(5)        将临时变量的最低内存地址放到edi中
(6)        Ecx寄存器设置一个计数次数33h = 51(十进制)
(7)        将eax初始化为0CCCCCCCCh
(8)        这个语句有点复杂,它涉及到EAX,ECX,ES,EDI,这么多的寄存器。其中rep表示重复执行这个语句,重复的次数由ECX中数字决定;STOS 表示字符串保存指令,它把AL或者AX,或者EAX中的 数据复制到目标串ES:EDI。每重复一次,ECX中的数字减1,EDI中的值加4,直到EDI中值跟EBP的值相等(这个时候表示给临时变量分配的栈空间已经初始化完毕,都已经初始化为0CCCCCCCC)。
从中我们可以看出经常见到的我们自己未初始化的整形变量,编译器都会把它初始化为0CCCCCCCC。

回复

使用道具 举报

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-4-26 04:45 , Processed in 0.043652 second(s), 34 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表