逆向C++学习手写(1)
最近常遇到的尴尬是看着汇编代码却无法在脑中反应出正确的C、C++代码 。近日偶得一篇好文《reverse C++》,细读下来收获不少。遂打算在《reverse C++》的基础上扩展,从汇编角度来认识一些C、C++中的语法现象~先从一些简单的开始...
一、数组
数组是非常基本的数据结构,C++中的数组表现为一段连续可用的内存。注意这段内存地址是连续的,这与很多动态语言中的数组不一样,举例来说PHP中的数组实际上是一种hash表式的实现,因而肯定不会是一段连续的内存。
对于充当全局变量的数组,会自动为数组中的每个变量附上默认值;而对于声明在函数体内部的局部变量,则需要手动进行初始化。来看一段简单的代码:
#include <iostream.h> void main() { int arr[10]; arr[3] = 22; cout<<arr; }
这段C++代码在main函数中声明了一个长度为10的arr数组,不过并没有初始化,而是仅仅给数组中的一个单元赋予了初始值。在VC6 release下进行编译,优化选项为最快速度,得到的汇编代码如下:
00401000 sub esp, 0x28 //在栈上开辟40个字节的空间,每个int占4字节 00401003 lea eax, dword ptr [esp] //arr 指向栈顶,因此栈顶相当于arr[0] 00401007 mov ecx, 0040B9B8 //cout指针存入ecx,为方法调用做准备 0040100C push eax 0040100D mov dword ptr [esp+10], 16 //相当于arr[3]=22 00401015 call 00401020 //eax中存放的arr,执行cout<<ar 0040101A add esp, 28 //回收数组占用的40个字节的空间 0040101D retn
这里的汇编看着有点乱,比较理想的编译出来的代码应该是:
sub esp, 28 // 执行 int arr[10]
mov dword ptr [esp+C], 16 // 注意这里是esp+C ,esp 是 arr[0],所以 arr[3] = esp + C
lea eax, dword ptr [esp]
push eax
mov ecx, 0040B9B8
call 00401020
add esp, 28
retn
可能有人注意到了这里栈并不平衡,中间的push并没有对应的pop操作,这是因为调用cout对象的<<操作符相当于调用一个类得成员函数,需满足thiscall约定,当参数个数确定的时候,由被调用的call本身来清理堆栈(push的参数个数不确定时,才由调用方来清理)。
题外话,cout对象也是一个全局对象,必须要确保在main函数调用之前完成初始化,这样在main里才能直接使用。从Entry Point到main函数调用(5):_cinit 中介绍了cinit 函数,其实cout 对象的生成也是在cinit中完成的。准确的说,是在
/* * do C++ initializations */ _initterm( __xc_a, __xc_z )
中完成的。在CRT源码中的IOSTRINI.CPP 文件中有如下语句:
ostream_withassign cout(_new_crt filebuf(1));
可见cout 是 ostream_withassign 类型的一个实例。
1)数组初始化
还是从一段简单的示例代码说起:
void main() { int arr[10] = {22,99}; }
arr依然是局部变量, 这里只会将arr[0] 和 arr[1] 初始化为22、99,数组中其余的8个变量都会初始化为0。来具体看看main函数的汇编代码:
push ebp mov ebp, esp sub esp, 28 // 开辟40个字节空间当数组 push edi // 暂存edi的值, 因为下面要用到edi寄存器 mov dword ptr [ebp-28], 16 // arr[0] = 22 mov dword ptr [ebp-24], 63 // arr[1] = 99 mov ecx, 8 xor eax, eax lea edi, dword ptr [ebp-20] // 将arr[2] 的地址放入edi rep stos dword ptr es:[edi] // 该指令将arr[2] - arr[9] 全部清0 pop edi // 回复edi寄存器 mov esp, ebp pop ebp retn
rep stos dword ptr es:[edi] 会根据ECX的值重复执行,它所作的动作就是从EAX中取4个字节,然后传到EDI所指的内存地址,这个动作会不停重复直到ECX减到0为止。由于是将arr[2] - arr[9]全部置0,所以先将EAX清0。
2)全局数组
这里仅仅是验证一下全局变量并非存放在栈上。
int arr[10] = {1,2,3}; void main() { arr[5]=10; }
在PE文件的.data 区发现了:
数组arr 被放到了数据区 00406030 - 00406057 的区域。很显然,这里无法利用ESP + XXX 或者 EBP - XXX 来直接对数组中的内容进行访问,因为arr 并不是存在于栈区。因此在main函数中,对该数组的访问直接写成了硬编码的地址。一旦PE 被映射进内存,就可以直接操作写死的内存地址。这里的main 函数编译成如下:
mov dword ptr [406044], 1
retn
一般对数组的访问都是由基地址(数组首地址)+偏移量组成。
首地址形如: ESP+XXX 或者 EBP-XXX
偏移量形如: ElementSize*INDEX(ElementSize是数组中元素的大小,index放在eax或者ecx等寄存器中)
但是这里直接写死的地址406044,406044就表示arr[5],甚至连“基址 + 偏移量”都没用到。
还有就是留意一下不同编译器生成的代码的差异…
还有就是留意一下不同编译器生成的代码的差异…
内力不够深啊TT...我去瞅瞅那本书