C++内嵌汇编(一):反汇编分析C++代码
#include "stdafx.h"int _tmain(int argc, _TCHAR* argv[]){ return 0;}
--- d:\my documents\visual studio 2008\projects\casmtest\casmtest\casmtest_main.cpp// CAsmTest.cpp : 定义控制台应用程序的入口点。#include "stdafx.h"int _tmain(int argc, _TCHAR* argv[]){00411370 push ebp 00411371 mov ebp,esp // 此后 ,ebp 寄存器中实际保存的是原来 esp 的内容00411373 sub esp,0C0h00411379 push ebx 0041137A push esi 0041137B push edi 0041137C lea edi,[ebp-0C0h] // 作用: 将 (ebp-0C0h) 这个数值放入 edi00411382 mov ecx,30h00411387 mov eax,0CCCCCCCCh0041138C rep stos dword ptr es:[edi] return 0;0041138E xor eax,eax // 嵌入汇编时,如果函数没有指定返回值,则 eax 为默认返回值}00411390 pop edi 00411391 pop esi 00411392 pop ebx 00411393 mov esp,ebp00411395 pop ebp 00411396 ret ?
// ASM_Test.cpp : Defines the entry point for the console application. ( 源代码 1 )//#include "stdafx.h"#include "stdlib.h"int _tmain(int argc, _TCHAR* argv[]){ int *pTest = new int(3); // 定义一个整型指针,并初始化为 3 printf( "*pTest = %d\r\n", *pTest ); // 调用库函数 printf 输出数据 delete []pTest; // 删除这个指针 return 0;}
?
--- f:\mysource\asm_test\asm_test\asm_test.cpp --------------------------------- ( 反汇编代码 1 )// ASM_Test.cpp : Defines the entry point for the console application.//#include "stdafx.h"#include "stdlib.h"int _tmain(int argc, _TCHAR* argv[]){; ( 1 )函数预处理部分004113C0 push ebp 004113C1 mov ebp,esp ; 保存堆栈的栈顶位置004113C3 sub esp,0E8h ; 要置为 0CCCCCCCCh 保留变量空间长度004113C9 push ebx ; 保存寄存器 ebx 、 esi 、 edi004113CA push esi 004113CB push edi 004113CC lea edi,[ebp-0E8h] ; 提出要置为 0CCCCCCCCh 的空间起始地址004113D2 mov ecx,3Ah ; 要置为 0CCCCCCCCh 的个数,每个占 4 个字节004113D7 mov eax,0CCCCCCCCh ; 于是 3Ah * 4 = 0E8h004113DC rep stos dword ptr es:[edi] ; 进行置为 0CCCCCCCCh 操作;( 2 )定义一个 int 型指针,分配空间后,并初始化为 3 , int *pTest = new int(3); // 定义一个整型指针,并初始化为 3004113DE push 4 ; 要分配的空间长度,会根据定义的数据类型而不同004113E0 call operator new (411186h) ; 分配空间,并把分配空间的起始地址放入 eax 中004113E5 add esp,4 ; 由于 new 与 delete 函数本身没有对栈进行弹出操作,所以,要编写者自己处理004113E8 mov dword ptr [ebp-0E0h],eax ; 比较分配的空间是否为 0 ,如果为 0004113EE cmp dword ptr [ebp-0E0h],0004113F5 je wmain+51h (411411h)004113F7 mov eax,dword ptr [ebp-0E0h] ; 对于分配的地址分配空间进行赋值为: 3004113FD mov dword ptr [eax],300411403 mov ecx,dword ptr [ebp-0E0h]00411409 mov dword ptr [ebp-0E8h],ecx ; 似乎用 [ebp - 0E0h] 和 [ebp - 0E8h] 作为了中间存储单元0041140F jmp wmain+5Bh (41141Bh)00411411 mov dword ptr [ebp-0E8h],0 ; 上面分配空间失败时的操作0041141B mov edx,dword ptr [ebp-0E8h]00411421 mov dword ptr [pTest],edx ; 数据最后送入 pTest 变量中; 调用 printf 函数进行数据输出 printf( "*pTest = %d\r\n", *pTest ); // 调用库函数 printf 输出数据00411424 mov esi,esp ; 用于调用 printf 后的 Esp 检测,不明白编译器为什么这样做00411426 mov eax,dword ptr [pTest] ; 提取要打印的数据,先是地址,下面一条是提取具体数据00411429 mov ecx,dword ptr [eax]0041142B push ecx ; 两个参数入栈0041142C push offset string "*pTest = %d\r\n" (41573Ch)00411431 call dword ptr [__imp__printf (4182C4h)] ; 调用函数00411437 add esp,8 ; 由于库函数无出栈管理操作,同 new 与 delete ,所以要加 8 ,进行堆栈处理0041143A cmp esi,esp ; 对堆栈的栈顶进行测试0041143C call @ILT+325(__RTC_CheckEsp) (41114Ah) ;进行指针变量的清理工作 delete []pTest; // 删除这个指针00411441 mov eax,dword ptr [pTest] ;[pTest] 中放入的是分配的地址,下面几条指令转悠一圈00411444 mov dword ptr [ebp-0D4h],eax ; 就是要把要清理的地址送入堆栈,然后调用 delete 函数0041144A mov ecx,dword ptr [ebp-0D4h]00411450 push ecx 00411451 call operator delete (411091h)00411456 add esp,4 ; 对堆栈进行处理,同 new 与 printf 函数;函数结束后,进行最终的清理工作 return 0;00411459 xor eax,eax ; 做相应的清理工作,堆栈中保存的变量送回原寄存器}0041145B pop edi 0041145C pop esi 0041145D pop ebx 0041145E add esp,0E8h ; 进行堆栈的栈顶判断00411464 cmp ebp,esp00411466 call @ILT+325(__RTC_CheckEsp) (41114Ah)0041146B mov esp,ebp0041146D pop ebp 0041146E ret --- No source file -------------------------; 后面不再是源代码 ?
#include<stdio.h>int fun(int a, int b) { a = 0x4455; b = 0x6677; return a + b;}int main(void){fun(0x8899,0x1100);return 0;}
反汇编后可以看到汇编代码如下:
?? 第一部分为main函数框架的汇编代码:
int main(void){002813F0 push ebp 002813F1 mov ebp,esp 002813F3 sub esp,0C0h 002813F9 push ebx 002813FA push esi 002813FB push edi 002813FC lea edi,[ebp-0C0h] 00281402 mov ecx,30h 00281407 mov eax,0CCCCCCCCh 0028140C rep stos dword ptr es:[edi] fun(0x8899,0x1100);0028140E push 1100h00281413 push 8899h 00281418 call fun (2811CCh) ;002811CCh地址处为指令: jmp fun(2813A0h),即fun函数的入口0028141D add esp,8 return 0;00281420 xor eax,eax }00281422 pop edi 00281423 pop esi 00281424 pop ebx 00281425 add esp,0C0h 0028142B cmp ebp,esp 0028142D call @ILT+310(__RTC_CheckEsp) (28113Bh) 00281432 mov esp,ebp 00281434 pop ebp 00281435 ret
?
?? 第二部分为fun函数的汇编代码:
int fun(int a, int b) {002813A0 push ebp 002813A1 mov ebp,esp ;注意:其实pop和push操作,只用到了esp指针002813A3 sub esp,0C0h 002813A9 push ebx 002813AA push esi 002813AB push edi 002813AC lea edi,[ebp-0C0h] 002813B2 mov ecx,30h 002813B7 mov eax,0CCCCCCCCh 002813BC rep stos dword ptr es:[edi] ;(重复) eax --放入--> es:[edi] a = 0x4455;002813BE mov dword ptr [a],4455h ;将0x00004455放入地址[a]中,即地址0x17493处 b = 0x6677;002813C5 mov dword ptr [b],6677h ;将0x00006677放入地址[b]中,即地址0x26231处 return a + b;002813CC mov eax,dword ptr [a] 002813CF add eax,dword ptr [b] }?002813D2 pop edi 002813D3 pop esi 002813D4 pop ebx 002813D5 mov esp,ebp 002813D7 pop ebp 002813D8 ret
?
FAQ:
1、在main()函数编译后的汇编代码中,call fun前将参数压入堆栈;可以再fun()函数编译后的汇编代码中怎么没见pop出这些参数呢?在fun()编译后的指令中通过什么来获得输入参数呢?
???? 回答:
???? A. 将esp内容传给ebp,通过ebp来获取输入参数
???? call fun指令背着我做了一件事情——将call fun的下一条语句地址压栈。fun()函数编译后的最后一条指令是ret,ret指令pop了push给他的地址,然后返回到这个地址==>“调用函数前的call”和“调用函数后的ret”一个push、一个pop,肯定不会让堆栈不平衡,老外叫做no stack unwinding。因此,如果在fun()函数汇编后的代码中上来就pop eax取出参数,就等于根ret抢返回地址了,这样fun()使用完就回不到主函数的断点了()
? ?? 那么怎么在被调用函数中获取参数呢?Easy! 既然参数已经在堆栈中,我们只要指定“esp+某个偏移”就可以访问了。
???? 然而,只要被调用函数中出现push或者pop指令,就会用到esp指针,esp就会变化,不方便定位传入的参数位置。其实,在调用过程中被调用函数的传入参数位置是固定的。于是,我们用ebp来获取他!
???? 地球人都知道,ebp指向栈底,故在fun()进入后要先将ebp内容压栈,fun()返回前将栈中“原来 ebp的值”pop到ebp中:)于是,才有了下面两句指令:
002813A0 push ebp 002813A1 mov ebp,esp
?? B. ebp如何获取输入参数
?? mov ebp, esp; 后堆栈应该变成这个样子:
/-------------------\? Higher Address
?| 参数2:? 0x1100h |?
?+-----------------+
?| 参数1:? 0x8899h |
?+-----------------+
?|?? 函数返回地址? |
?|??? 0x00401087?? |
?+-----------------+
?|?????? ebp?????? |
\-------------------/?? Lower Address <== stack pointer
& ebp all point to here, now
?? ∵一个int是32位,可以分析出,第一个参数的地址是ebp + 08h,第二个参数就是ebp + 0ch。主要到,此时ebp中的值就是esp的值!
?
2、一个简单的问题休息一下:
0028140E push 1100h00281413 push 8899h 00281418 call fun (2811CCh) ;002811CCh地址处为指令: jmp fun(2813A0h),即fun函数的入口0028141D add esp,8
这个代码中最后一行add esp, 8指令作用是什么?
??? 回答:平衡堆栈,相当于弹出(just throw out)传入被调用函数的参数。
?
关键点:
???? 注意:其实pop和push操作,只用到了esp指针!ebp指定了栈底,但并没有什么实际作用——在16位汇编和32位汇编中均如此!
?
3、calling convention 调用传统
这个历史遗留问题。如果认真思考过,你一定想函数的参数为什么偏用堆栈转递呢,寄存器不也可以传递吗?而且很快阿。参数的传递顺序不
一定要是由后到前的,从前到后传递也不会出现任何问题啊?再有为什么一定要等到函数返回了再处理堆栈平衡的问题呢,能否在函数返回前就让堆栈平衡呢?
所有上述提议都是绝对可行的,而他们之间不同的组合就造就了函数不同的调用方法。也就是你常看到或听到的stdcall,pascal,
fastcall,WINAPI,cdecl等等。这些不同的处理函数调用方式就叫做calling convention。
默认情况下C语言使用的是cdecl方式,也就是上面提到的。参数由右到左进栈,调用函数者处理堆栈平衡。如果你在我们刚才的程序中fun函数前加入
__stdcall,再来用上面的方法分析一下。
8:??????? fun(0x8899,0x1100);
00401058?? push??????? 1100h? ; <== 参数仍然是由右到左传递的
0040105D?? push??????? 8899h??
00401062?? call??????? fun (00401000)
;<== 这里没有了 add esp, 08h
1:??? int __stdcall fun(int a, int b) {
00401000?? push??????? ebp
00401001?? mov???????? ebp,esp
00401003?? sub???????? esp,40h
00401006?? push??????? ebx
00401007?? push??????? esi
00401008?? push??????? edi
00401009?? lea???????? edi,[ebp-40h]
0040100C?? mov???????? ecx,10h
00401011?? mov???????? eax,0CCCCCCCCh
00401016?? rep stos??? dword ptr [edi]
2:?????? a = 0x4455;
00401018?? mov???????? dword ptr [ebp+8],4455h
3:?????? b = 0x6677;
0040101F?? mov???????? dword ptr [ebp+0Ch],6677h
4:?????? return a + b;
00401026?? mov???????? eax,dword ptr [ebp+8]
00401029?? add???????? eax,dword ptr [ebp+0Ch]
5:??? }
0040102C?? pop???????? edi
0040102D?? pop???????? esi
0040102E?? pop???????? ebx
0040102F?? mov???????? esp,ebp
00401031?? pop???????? ebp
00401032?? ret???????? 8; <== ret 取出返回地址后,
?????????????????????? ; 给esp加上 8。看!堆栈平衡在函数内完成了。
?????????????????????? ; ret指令这个语法设计就是专门用来实现函数
?????????????????????? ; 内完成堆栈平衡的
于是得出结论,stdcall是由右到左传递参数,被调用函数恢复堆栈的calling convention. 其他几种calling convention的修饰关键词分别是__pascal,__fastcall,WINAPI(这个要包含windows.h才可以用)。现在,你可以用上面说的方法自己分析一下他们各自的特点了。
?
?
?