读书人

C++内嵌汇编(1):反汇编分析C++代码

发布时间: 2012-12-27 10:17:10 作者: rapoo

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才可以用)。现在,你可以用上面说的方法自己分析一下他们各自的特点了。

?

?

?

读书人网 >C++

热点推荐