newcenturymoon - 2009-5-24 16:07:00
本次开始就需要大家用一些调试工具积极跟着我一起调试程序了,用邓爷爷的一句名言:实践才是检验真理的唯一标准。希望大家不要因为代码表明看起来很繁琐而畏惧,也不要光看不操作,让我们一起把反汇编这种看起来晦涩难懂的东西学透学精!本次课程用到的工具:OD http://www.pediy.com/tools/Debuggers/ollydbg/OllyICE.rarIDA(可用可不用)http://bbs.pediy.com/showthread.php?t=55801需要用到的程序见附件Push ebpMov ebx,espSub esp,XXX看过一些汇编代码的人一定对这个有些印象,那么这是做了什么操作呢?里面有什么奥秘呢? 本次需要了解的两个指令sub(相当于减)sub eax,xxx 就是将eax减去 xxx与之对应的是add ,相加。首先我们先引入一个非常简单的小程序:#include <stdio.h>#include <string.h>void func1(int input1, int input2){
int j;
char c;
short k;
j = 0;
c = 'a';
k = 1;
printf("sum=%d\n", input1+input2);
return;}int main(){
char output[8] = "abcdef";
int i, j;
i=2;
j=3;
func1(i,j);
printf("%s\r\n", output);
return 0;}编译好了之后,用IDA打开该程序 定位到main函数.text:00401090 push ebp
.text:00401091 mov ebp, esp
.text:00401093 sub esp, 50h
.text:00401096 push ebx
.text:00401097 push esi
.text:00401098 push edi
.text:00401099 lea edi, [ebp+var_50]
.text:0040109C mov ecx, 14h
.text:004010A1 mov eax, 0CCCCCCCCh
.text:004010A6 rep stosd
.text:004010A8 mov eax, ds:dword_420030
.text:004010AD mov [ebp+var_8], eax
.text:004010B0 mov cx, ds:word_420034
.text:004010B7 mov [ebp+var_4], cx
.text:004010BB mov dl, ds:byte_420036
.text:004010C1 mov [ebp+var_2], dl
.text:004010C4 xor eax, eax
.text:004010C6 mov [ebp+var_1], al
.text:004010C9 mov [ebp+var_C], 2
.text:004010D0 mov [ebp+var_10], 3
.text:004010D7 mov ecx, [ebp+var_10]
.text:004010DA push ecx
.text:004010DB mov edx, [ebp+var_C]
.text:004010DE push edx
.text:004010DF call sub_401005 -------- 这里调用了func_1函数
.text:004010E4 add esp, 8
.text:004010E7 lea eax, [ebp+var_8]
.text:004010EA push eax
.text:004010EB push offset aS ; "%s\r\n"
.text:004010F0 call _printf
.text:004010F5 add esp, 8
.text:004010F8 xor eax, eax
.text:004010FA pop edi
.text:004010FB pop esi
.text:004010FC pop ebx
.text:004010FD add esp, 50h
.text:00401100 cmp ebp, esp
.text:00401102 call __chkesp
.text:00401107 mov esp, ebp
.text:00401109 pop ebp
.text:0040110A retn
.text:0040110A _main_0 endpFunc_1函数
.text:00401020
.text:00401020 push ebp
.text:00401021 mov ebp, esp
.text:00401023 sub esp, 4Ch
.text:00401026 push ebx
.text:00401027 push esi
.text:00401028 push edi
.text:00401029 lea edi, [ebp+var_4C]
.text:0040102C mov ecx, 13h
.text:00401031 mov eax, 0CCCCCCCCh
.text:00401036 rep stosd
.text:00401038 mov [ebp+var_4], 0
.text:0040103F mov [ebp+var_8], 61h
.text:00401043 mov [ebp+var_C], 1
.text:00401049 mov eax, [ebp+arg_0]
.text:0040104C add eax, [ebp+arg_4]
.text:0040104F push eax
.text:00401050 push offset Format ; "sum=%d\n"
.text:00401055 call _printf
.text:0040105A add esp, 8
.text:0040105D pop edi
.text:0040105E pop esi
.text:0040105F pop ebx
.text:00401060 add esp, 4Ch
.text:00401063 cmp ebp, esp
.text:00401065 call __chkesp
.text:0040106A mov esp, ebp
.text:0040106C pop ebp
.text:0040106D retn
.text:0040106D sub_401020 endp
本次我们不必关心这些代码的含义,我们目前只关心的是函数对栈是如何操作的。所以不要被这些东西吓到,我们要用的只是很少的一部分用户系统信息:Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; InfoPath.2; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 1.1.4322)附件:
需要用到的程序.rar
newcenturymoon - 2009-5-24 16:18:00
栈是如何建造的?
栈是一种经典的数据结构,它遵循“先进后出”的操作特性。
那么在实现栈时如何保证这种操作特性呢?我们来看一下栈的定义及相关概念。
栈的逻辑结构与线性表相同,但栈有其特殊的操作规则,所以说栈是运算受限的线性表。我们可以定义栈是限制在表的一端进行插入和删除的线性表,允许插入/删除的一端称为栈顶,另一端称为栈底。对于插入操作称为PUSH(入栈);删除操作称为POP(出栈)。
很明显,我们要对栈进行操作,需要且仅需要记录栈顶的位置。在Intel 80x86系统中,完成这一功能的就是寄存器ESP(Extended Stack Pointer)。所有的PUSH/POP操作都通过它来完成,因而寄存器ESP是经常变化。
聪明的读者这时会问:如何访问非栈顶的数据呢?Intel 80x86系统中解决的办法是设置另外一个寄存器EBP(Extended Base Pointer),我们用它来保存一个稳定不变的基准地址(基址),然后通过“基址+正负偏移量”的方式实现非栈顶的数据访问。也可以把EBP看作是保存栈底的位置,那么寄存EBP和ESP一起就组成一个栈。
寄存器是CPU的记忆细胞,非常重要。我们的分析也是依靠寄存器来进行的。更多的寄存器知识可以参考计算机组成原理和汇编语言的相关教程。下面说明几个后文我们需要特别关注的Intel 80x86系统的32位寄存器。
指令地址寄存器EIP:永远保存下一步将要执行的指令的地址。
基址寄存器EBP:保存当前函数层的栈内存基地址,要依靠它来完成栈内存的变量寻址。
栈顶指针寄存器ESP:保存栈顶地址(指针),系统依靠它来完成PUSH、POP操作。栈顶指针所指向的位置是存储有效数据的。故PUSH操作是先往低址移动栈顶指针,再存储数据;而POP操作是先读取数据,再往高址移动栈顶指针。
newcenturymoon - 2009-5-24 16:26:00
函数调用时候(call)发生了什么?我们用OD载入这个程序,通过IDA的主函数地址直接找到主函数 在主函数中直接看地址0x004010DF位置的call指令,如何直接在OD中找到这个函数位置呢?按下Ctrl+G组合键 此时 在”输入要跟随的表达式”的框中 输入004010DFOD接下来就会跳转到这个位置,然后在这条指令上按下F4 程序就会跑到这里下面我们来研究在函数调用(Call指令)的时候栈中发生了什么。
如图是还未进入0x00401005时候的情况, OD右下方显示的栈,且最上方指向的永远是栈顶也就是esp所在位置。现在我们看栈顶地址是0x0012FF1C
附件: 您所在的用户组无法下载或查看附件
newcenturymoon - 2009-5-24 16:30:00
好,按F7
跟进这个函数。注意看栈,大家看到了么与图1对比,可看出寄存ESP往低址偏移了4字节,现在地址是0012FF18,
附件: 您所在的用户组无法下载或查看附件
这说明其中执行了一个PUSH操作。再看函数调用时候刚刚被压入的这个数据,也就是目前esp中所存储的数据0x004010E4,再反过头来看我们上面的代码:
.text:004010DF
call
sub_401005 -------- 这里调用了func_1函数
.text:004010E4
add
esp, 8
在执行完0x00401005处的函数后,程序会返回到主函数中继续运行,这个调用后面的代码指令地址是0x004010e4,而上述实验中证明了在执行call指令的同时,它的返回地址被压入了栈,同时当前EIP位置(下一步将要执行的指令的地址)也跳转到了0x00401005处。
所以call指令相当于执行了一个push指令,一个jmp指令,push是在栈中压入了该函数执行后需要返回的地址,jmp则跳到要执行的函数地址中去,也就是call指令后面的地址。
newcenturymoon - 2009-5-24 16:33:00
函数入口做了什么? 按F8继续从刚才的地方往下走,这里有个疑问,0x401005处是一个jmp指令,为什么不直接是func_1函数呢?
这是因为我们现在使用的实验例子是DEBUG版的,在DEBUG版本中,VC汇编程序会产生一个函数跳转指令表,该表的每个表项存放一个函数的跳转指令。程序中的函数调用就是利用这个表来实现跳转到相应函数的入口地址。在DEBUG版本中增加函数跳转指令表,其目的是加快编译速度,当某函数的地址发生变化时,只需要修改相应表项即可,而不需要修改该函数的每一处引用。注意:在RELEASE版本中,不会生成这个表,也就是说call指令执行后0x00401005处的代码就是func_1函数而不是一个jmp 指令。
好了知道了这个,我们再往下走一步F8就到了真正的func_1函数了。
附件: 您所在的用户组无法下载或查看附件
newcenturymoon - 2009-5-24 16:36:00
我们结合示例一句一句看,push ebp这句是把当前栈的栈底地址压入栈保存起来。(也就是把main()函数层的栈内存基地址(由寄存器ebp指示)入栈保存起来。)这步走下去之后可以看到栈中被压入了0012FF80这个数据。同时一个push操作 栈顶也提升了push ebp的作用就是在栈中保存上一个函数栈帧的栈底。
附件: 您所在的用户组无法下载或查看附件
newcenturymoon - 2009-5-24 16:39:00
接着mov ebp,esp这句是把当前的栈顶地址(由寄存器esp指示)作为func1()函数层的栈内存基地址,即更新寄存器ebp为寄存器esp。也就是把现在的栈顶当作了栈底。这步走下去之后可以看到ebp和当前栈顶esp值相等。
附件: 您所在的用户组无法下载或查看附件
newcenturymoon - 2009-5-24 16:43:00
接着sub esp,4c然后,我们猛然看到,栈顶指针esp向低地址偏移76(0x4C)字节。这里相当于为func1()函数层分配了栈内存。当前esp变为了0x0012FEC8 还记得刚才的ebp么,0x0012FF14,现在的esp是0x0012FEC8 那么这两段之间的数据就是系统为func_1开辟的栈空间,用于存储func_1函数需要用的数据。
附件: 您所在的用户组无法下载或查看附件
newcenturymoon - 2009-5-24 16:44:00
我们不妨再往下看00401060
add
esp, 4Ch从这句可以看到esp又加了4c和sub做了相反的动作,这就是在函数执行完毕需要返回的时候,系统把这个函数开辟的栈空间收回。所以做了相反的动作。现在我们总结下函数调用过程中,栈中发生的一些故事吧:
假设有某个程序有main函数,func_A函数,func_B函数,在main函数中调用了func_A函数,在func_A函数中调用了func_B函数。
如图:
附件: 您所在的用户组无法下载或查看附件
如图所示,在函数调用的过程中,伴随的系统栈中的操作如下。
★在 main 函数调用func_A 的时候,首先在自己的栈帧中压入函数返回地址,然后为
func_A 创建新栈帧并压入系统栈。
★ 在 func_A 调用func_B 的时候,同样先在自己的栈帧中压入函数返回地址,然后为
func_B 创建新栈帧并压入系统栈。
★ 在 func_B 返回时,func_B 的栈帧被弹出系统栈,func_A 栈帧中的返回地址被“露”
在栈顶,此时处理器按照这个返回地址重新跳到func_A 代码区中执行。
总结起来就是,当某个函数被调用时,系统栈会为这个函数开辟一个新栈帧,并把它压入系统栈中。(也就是函数开始的push ebp…sub esp,xxx的过程)当函数返回时,系统栈会弹出该函数所对应的栈帧。
newcenturymoon - 2009-5-24 16:49:00
我们再用一个图表明ebp,esp和栈之间的关系
附件: 您所在的用户组无法下载或查看附件函数栈帧:ESP 和EBP 之间的内存空间为当前栈帧,EBP 标识了当前栈帧的底部,ESP标识了当前栈帧的顶部。
在函数栈帧中,一般包含以下几类重要信息。
(1)局部变量:为函数局部变量开辟的内存空间。
(2)栈帧状态值:保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶
部可以通过堆栈平衡计算得到),用于在本帧被弹出后恢复出上一个栈帧。(也就是那句push ebp的作用)
(3)函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位
置,以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令。
本次主要讲了 在调用函数时候栈中的变化,通过此次大家应该对esp,ebp有了更深的了解,下次我们将继续深入研究和栈有关的知识。
最硬的石头 - 2009-5-24 17:23:00
排队听课,学习了。:kaka12:
超级游戏迷 - 2009-5-24 18:23:00
真无语啊,没看懂……:kaka7:
vistalong - 2009-5-24 20:38:00
学习去
smallyou93 - 2009-5-24 21:04:00
:kaka7: 没看懂..
networkedition - 2009-5-25 10:20:00
排队慢慢看,不错,正想学习od呢:kaka12:
happysunday2003 - 2009-6-3 19:43:00
看的我都失去信心了
CPU_ring0 - 2009-6-3 23:19:00
该用户帖子内容已被屏蔽
ximo2006 - 2009-6-4 10:04:00
先膜拜,后学习。。。
6709的空 - 2009-6-5 16:11:00
拜读,学习
6709的空 - 2009-6-5 16:21:00
愁啊,加密与解密第三章看不懂
© 2000 - 2026 Rising Corp. Ltd.