反汇编即把目标二进制机器码转为汇编代码的过程,该技术常用于软件破解、外挂技术、病毒分析、逆向工程、软件汉化等领域,学习和理解反汇编对软件调试、系统漏洞挖掘、内核原理及理解高级语言代码都有相当大的帮助,软件一切神秘的运行机制全在反汇编代码里面。
函数是任何一个高级语言中必须要存在的一个东西,使用函数式编程可以让程序可读性更高,充分发挥了模块化设计思想的精髓,今天我将带大家一起来探索函数的实现机理,探索编译器到底是如何对函数这个关键字进行实现的,从而更好地理解编译行为。
先来研究函数,函数是任何一门编程语言中都存在的关键字,使用函数式编程可以让程序可读性更高,充分发挥模块化设计思想的精髓,而函数传参的底层实现就是通过堆栈来实现的,首先我们来理解一下堆栈.
当有参函数被执行时,通常会根据不同的调用约定来对参数进行压栈存储
以STDcall约定为例,栈的调用原则是先进后出,最先被push到堆栈中的数据会被最后释放出来,而CPU中有两个寄存器专门用于维护堆栈的变化,ESP栈顶寄存器,EBP栈底寄存器(基址),这两个寄存器就像是好基友,两个寄存器相互配合,来让堆栈有条不乱.
栈帧:就是ESP -> EBP 之间的空间,通常是调用函数时,函数的参数,从一个函数切换到另一个函数上,栈帧也会发生变化,当函数调用结束后,则需要平栈帧,不然会发生访问冲突,平栈帧的过程都是有编译器来解决的。
函数与堆栈的基础: 下面一个简单的函数调用案例,我们来看看汇编格式是怎样的.
#include <stdio.h>
int VoidFunction()
{
printf("hello lyshark\n");
return 0;
}
int main(int argc, char* argv[])
{
VoidFunction();
return 0;
}
编译上面的这段代码,首先我们找到main函数的位置,然后会看到call 0x4110E1
这条汇编指令就是在调用VoidFunction()
函数,观察函数能发现函数下方并没有add esp,xxx
这样的指令,则说明平栈操作是在函数的内部完成的,我们直接跟进去看看函数内部到底做了什么见不得人的事情.
0041142C | 8DBD 40FFFFFF | lea edi,dword ptr ss:[ebp-0xC0] |
00411432 | B9 30000000 | mov ecx,0x30 |
00411437 | B8 CCCCCCCC | mov eax,0xCCCCCCCC |
0041143C | F3:AB | rep stosd |
0041143E | E8 9EFCFFFF | call 0x4110E1 | 调用VoidFunction()
00411443 | 33C0 | xor eax,eax | main.c:13
00411445 | 5F | pop edi | main.c:14, edi:"閉\n"
00411446 | 5E | pop esi | esi:"閉\n"
00411447 | 5B | pop ebx |
此时我们直接跟进call 0x4110E1
这个函数中,分析函数内部是如何平栈的,进入函数以后首先使用push ebp
保存当前EBP指针位置,然后调用mov ebp,esp
这条指令来将当前的栈帧付给EBP也就是当基址使用,sub esp,0xC0
则是分配局部变量,接着是push ebx,esi,edi
则是因为我们需要用到这几个寄存器所以应该提前将原始值保存起来,最后用完了就需要pip edi,esi,ebx
恢复这些寄存器的原始状态,并执行add esp,0xC0
对局部变量进行恢复,最后mov esp,ebp
还原到原始的栈顶指针位置,首尾呼应.
004113C0 | 55 | push ebp | 保存栈底指针 ebp
004113C1 | 8BEC | mov ebp,esp | 将当前栈指针给ebp
004113C3 | 81EC C0000000 | sub esp,0xC0 | 抬高栈顶esp,开辟局部空间
004113C9 | 53 | push ebx | 保存 ebx
004113CA | 56 | push esi | 保存 esi
004113CB | 57 | push edi | 保存 edi
004113CC | 8DBD 40FFFFFF | lea edi,dword ptr ss:[ebp-0xC0] | 取出次函数可用栈空间首地址
004113D2 | B9 30000000 | mov ecx,0x30 | ecx:"閉\n", 30:'0'
004113D7 | B8 CCCCCCCC | mov eax,0xCCCCCCCC |
004113DC | F3:AB | rep stosd |
004113DE | 8BF4 | mov esi,esp | main.c:5
004113E0 | 68 58584100 | push consoleapplication1.415858 | 415858:"hello lyshark\n"
004113E5 | FF15 14914100 | call dword ptr ds:[<&printf>] | 调用printf
004113EB | 83C4 04 | add esp,0x4 | 降低栈顶esp,释放printf局部空间
004113EE | 3BF4 | cmp esi,esp | 检测堆栈是否平衡,ebp!=esp则不平衡
004113F0 | E8 46FDFFFF | call 0x41113B | 堆栈检测函数:检测平衡,不平衡则报错
004113F5 | 33C0 | xor eax,eax | main.c:6
004113F7 | 5F | pop edi | 还原寄存器edi
004113F8 | 5E | pop esi | 还原寄存器esi
004113F9 | 5B | pop ebx | 还原寄存器ebx
004113FA | 81C4 C0000000 | add esp,0xC0 | 恢复esp,还原局部变量
00411400 | 3BEC | cmp ebp,esp |
00411402 | E8 34FDFFFF | call 0x41113B |
00411407 | 8BE5 | mov esp,ebp | 还原原始的ebp指针
00411409 | 5D | pop ebp |
0041140A | C3 | ret |
上方的代码其实默认走的是STDCALL的调用约定,一般情况下在Win32环境默认遵循的就是STDCALL,而在Win64环境下使用的则是FastCALL,在Linux系统上则遵循SystemV的约定,这里我整理了他们之间的异同点.
这里我们来演示CDECL的调用约定,其实我们使用的Printf()
函数就是在遵循__cdecl()
约定,由于Printf函数可以有多个参数传递,所以只能使用__cdecl()
约定来传递参数,该约定的典型特点就是平栈不在被调用函数内部完成,而是在外部通过使用一条add esp,0x4
这种方式来平栈的.
004113E0 | 68 58584100 | push consoleapplication1.415858 | 415858:"hello lyshark\n"
004113E5 | FF15 14914100 | call dword ptr ds:[<&printf>] |
004113EB | 83C4 04 | add esp,0x4 | 平栈
004113EE | 3BF4 | cmp esi,esp |
004113F0 | E8 46FDFFFF | call 0x41113B |
004113F5 | 8BF4 | mov esi,esp | main.c:6
004113F7 | 68 58584100 | push consoleapplication1.415858 | 415858:"hello lyshark\n"
004113FC | FF15 14914100 | call dword ptr ds:[<&printf>] | 平栈
00411402 | 83C4 04 | add esp,0x4 |
在使用Release版对其进行优化的话,此段代码将会采取复写传播优化,将每次参数平衡的操作进行归并,一次性平衡栈顶指针esp,从而可以大大的提高程序的执行效率,汇编代码如下:
004113E0 | 68 58584100 | push consoleapplication1.415858 | 415858:"hello lyshark\n"
004113E5 | FF15 14914100 | call dword ptr ds:[<&printf>] |
004113F7 | 68 58584100 | push consoleapplication1.415858 | 415858:"hello lyshark\n"
004113FC | FF15 14914100 | call dword ptr ds:[<&printf>] |
00411402 | 83C4 04 | add esp,0x8 | 一次性平栈加上0x8,平了前面的2个push
通过以上分析发现_cdecl
与_stdcall
两者只在参数平衡上有所不同,其余部分都一样,但经过优化后_cdecl
调用方式的函数在同一作用域内多次使用,会在效率上比_stdcall
髙,这是因为_cdecl
可以使用复写传播,而_stdcall
的平栈都是在函数内部完成的,无法使用复写传播这种优化方式.
除了前面的两种调用约定以外_fastcall
调用方式的效率最髙,其他两种调用方式都是通过栈传递参数,唯独_fastcall
可以利用寄存器传递参数,但由于寄存器数目很少,而参数相比可以很多,只能量力而行,故在Windows环境中_fastcall
的调用方式只使用了ECX和EDX寄存器,分别传递第1个参数和第2个参数,其余参数传递则依然使用堆栈传递.
#include <stdio.h>
void _fastcall VoidFunction(int x,int y,int z,int a)
{
printf("%d%d%d%d\n", x, y, z, a);
}
int main(int argc, char* argv[])
{
VoidFunction(1,2,3,4);
return 0;
}
反汇编后观察代码发现call 0x4110E6
就是在调用我们的VoidFunction()
函数在调用之前分别将参数压入了不同的寄存器和堆栈中,接着我们继续跟进到call函数内部,看它是如何取出参数的.
0041145E | 6A 04 | push 0x4 | 第四个参数使用堆栈传递
00411460 | 6A 03 | push 0x3 | 第三个参数使用堆栈传递
00411462 | BA 02000000 | mov edx,0x2 | 第二个参数使用edx传递
00411467 | B9 01000000 | mov ecx,0x1 | 第一个参数使用ecx传递
0041146C | E8 75FCFFFF | call 0x4110E6 |
00411471 | 33C0 | xor eax,eax | main.c:11
进入call 0x4110E6
这个函数中,观察发现首先会通过mov指令将前两个参数提取出来,然后再从第四个参数开始依次将参数取出来并压栈,最后让Printf函数成功调用到.
004113E0 | 8955 EC | mov dword ptr ss:[ebp-0x14],edx | edx => 提取出第二个参数
004113E3 | 894D F8 | mov dword ptr ss:[ebp-0x8],ecx | ecx => 提取出第一个参数
004113E6 | 8BF4 | mov esi,esp | main.c:5
004113E8 | 8B45 0C | mov eax,dword ptr ss:[ebp+0xC] | 保存第四个参数
004113EB | 50 | push eax |
004113EC | 8B4D 08 | mov ecx,dword ptr ss:[ebp+0x8] | 保存第三个参数
004113EF | 51 | push ecx |
004113F0 | 8B55 EC | mov edx,dword ptr ss:[ebp-0x14] | 保存第二个参数
004113F3 | 52 | push edx |
004113F4 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | 保存第一个参数
004113F7 | 50 | push eax |
004113F8 | 68 58584100 | push consoleapplication1.415858 | 415858:"%d%d%d%d\n"
004113FD | FF15 14914100 | call dword ptr ds:[<&printf>] |
00411403 | 83C4 14 | add esp,0x14 | 平栈
定义并使用有参函数: 我们给函数传递些参数,然后分析其反汇编代码,观察代码的展示形式.
#include <stdio.h>
int Function(int x,float y,double z)
{
if (x = 100)
{
x = x + 100;
y = y + 100;
z = z + 100;
}
return (x);
}
int main(int argc, char* argv[])
{
int ret = 0;
ret = Function(100, 2.5, 10.245);
printf("返回值: %d\n", ret);
return 0;
}
下方的反汇编代码就是调用函数ret = Function()
的过程,该过程中可看出压栈顺序遵循的是从后向前压入的.
0041145E | C745 F8 00000000 | mov dword ptr ss:[ebp-0x8],0x0 | main.c:17
00411465 | 83EC 08 | sub esp,0x8 | main.c:18
00411468 | F2:0F1005 70584100 | movsd xmm0,qword ptr ds:[<__real@40247d70a3d70a3d>] | 将10.245放入XMM0寄存器
00411470 | F2:0F110424 | movsd qword ptr ss:[esp],xmm0 | 取出XMM0中内容,并放入堆栈
00411475 | 51 | push ecx |
00411476 | F3:0F1005 68584100 | movss xmm0,dword ptr ds:[<__real@40200000>] | 将2.5放入XMM0
0041147E | F3:0F110424 | movss dword ptr ss:[esp],xmm0 | 同理
00411483 | 6A 64 | push 0x64 | 最后一个参数100
00411485 | E8 51FDFFFF | call 0x4111DB | 调用Function函数
0041148A | 83C4 10 | add esp,0x10 |
0041148D | 8945 F8 | mov dword ptr ss:[ebp-0x8],eax | 将返回值压栈
00411490 | 8BF4 | mov esi,esp | main.c:19
00411492 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] |
00411495 | 50 | push eax |
00411496 | 68 58584100 | push consoleapplication1.415858 | 415858:"返回值: %d\n"
0041149B | FF15 14914100 | call dword ptr ds:[<&printf>] | 输出结果
004114A1 | 83C4 08 | add esp,0x8 |
压栈完成以后我们可以继续跟进call 0x4111DB
这个关键CALL,此处就是运算数据的关键函数,跟进去以后,可发现其对浮点数的运算,完全是依靠XMM寄存器实现的.
004113F1 | 8945 08 | mov dword ptr ss:[ebp+0x8],eax |
004113F4 | F3:0F1045 0C | movss xmm0,dword ptr ss:[ebp+0xC] | main.c:8
004113F9 | F3:0F5805 8C584100 | addss xmm0,dword ptr ds:[<__real@42c80000>] |
00411401 | F3:0F1145 0C | movss dword ptr ss:[ebp+0xC],xmm0 |
00411406 | F2:0F1045 10 | movsd xmm0,qword ptr ss:[ebp+0x10] | main.c:9
0041140B | F2:0F5805 80584100 | addsd xmm0,qword ptr ds:[<__real@4059000000000000>] |
00411413 | F2:0F1145 10 | movsd qword ptr ss:[ebp+0x10],xmm0 |
00411418 | 8B45 08 | mov eax,dword ptr ss:[ebp+0x8] | main.c:11
向函数传递数组/指针: 这里我们以一维数组为例,二维数组的传递其实和一维数组是相通的,只不过在寻址方式上要使用二维数组的寻址公式,此外传递数组其实本质上就是传递指针,所以数组与指针的传递方式也是相通的.
#include <stdio.h>
void Function(int Array[], int size)
{
for (int i = 0; i<size; ++i)
{
printf("输出元素: %d \n", Array[i]);
}
}
int main(int argc, char* argv[])
{
int ary[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
Function(ary, 10);
return 0;
}
以下代码就是Function(ary,10)
函数的调用代码,首先压栈传递0A也就是10,接着传递ary首地址,最后调用call指令.
004114B4 | 6A 0A | push 0xA | 10
004114B6 | 8D45 D4 | lea eax,dword ptr ss:[ebp-0x2C] | ary 首地址
004114B9 | 50 | push eax | push eax
004114BA | E8 63FCFFFF | call 0x411122 | 调用Function()
004114BF | 83C4 08 | add esp,0x8 | 堆栈修复
函数中返回指针,其实就是返回一个内存地址,我们可以打印出这个内存地址具体的值,如下是一段测试代码,这里的原理于上方都是相通的,此处就不在浪费篇幅了.
#include <stdio.h>
int GetAddr(int number)
{
int nAddr;
nAddr = *(int*)(&number-1);
return nAddr;
}
int main(int argc, char* argv[])
{
int address = 0;
address = GetAddr(100);
printf("%x\n",address);
return 0;
}
函数的参数传递就到此结束了,其实其他的参数传递无外乎就是上面的这几种传递形式,只是在某些实现细节上略有差异,但大体上也就是这些东西,在真正的逆向过程中还需要考虑编译器的版本等具体细节,每一个编译器在实现参数传递上都略微不同,这也就是编译特性所影响的,我们应该灵活运用这些知识,才能更好地分析这些字节码.
接着我们来研究一下变量的作用域,在C语言中作用域可分为局部变量与全局变量,两种变量又分为静态变量和动态变量,接下来我们将通过反汇编学习研究他们之间的异同点.
探索全局变量的奥秘: 全局变量与常量有很多相似的地方,两者都是在程序执行前就存在的,这是因为编译器在编译时就将其写入到的程序文件里,但是在PE文件中的只读数据节里,常量的节属性被修饰为不可写入,而全局变量和静态变量的属性为可读可写,PE文件加载器在加载可执行文件时,会率先装载这些常量与全局变量,然后才会运行程序入口代码,因此这些全局变量可以不受作用域的影响,在程序中的任何位置都可以被访问和使用,来看一段C代码:
#include <stdio.h>
int number1 = 1;
int number2 = 2;
int main(int argc, char* argv[])
{
scanf("%d", &number1);
printf("您输入的数字: %d\n", number1);
number2 = 100;
return 0;
}
如下反汇编代码可以看出,全局变量的访问是直接通过立即数push consoleapplication1.415858
访问的,此立即数是通过编译器编译时就写入到了程序中的,所以也就可以直接进行访问了.
004113E0 | 68 00804100 | push <consoleapplication1._number1> | 此处的压栈参数就是全局变量
004113E5 | 68 58584100 | push consoleapplication1.415858 | 415858:"%d"
004113EA | FF15 10914100 | call dword ptr ds:[<&scanf>] |
004113F0 | 83C4 08 | add esp,0x8 | 保存第二个参数
004113F3 | 3BF4 | cmp esi,esp |
004113F5 | E8 41FDFFFF | call 0x41113B |
004113FA | 8BF4 | mov esi,esp | main.c:9
004113FC | A1 00804100 | mov eax,dword ptr ds:[<_number1>] |
00411401 | 50 | push eax |
00411402 | 68 5C584100 | push consoleapplication1.41585C | 41585C:"您输入的数字: %d\n"
00411407 | FF15 18914100 | call dword ptr ds:[<&printf>] |
0041140D | 83C4 08 | add esp,0x8 |
00411410 | 3BF4 | cmp esi,esp |
00411412 | E8 24FDFFFF | call 0x41113B |
00411417 | C705 04804100 64000000 | mov dword ptr ds:[<_number2>],0x64 | main.c:11, 64:'d'
00411421 | 33C0 | xor eax,eax | main.c:12
探索局部变量的奥秘: 局部变量的访问是通过栈指针相对间接访问,也就是说局部变量是程序动态创建的,通常是调用某个函数或过程时动态生成的,局部变量作用域也仅限于函数内部,且其地址也是一个未知数,编译器无法预先计算.
#include <stdio.h>
int main(int argc, char* argv[])
{
int num1 = 0;
int num2 = 1;
scanf("%d", &num1);
printf("%d", num1);
num2 = 10;
return 0;
}
反汇编代码,局部变量就是通过mov dword ptr ss:[ebp-0x8],0x0
动态开辟的空间,其作用域就是在本函数退出时消亡.
004113DE | C745 F8 00000000 | mov dword ptr ss:[ebp-0x8],0x0 | 申请局部变量
004113E5 | C745 EC 01000000 | mov dword ptr ss:[ebp-0x14],0x1 | main.c:6
004113EC | 8BF4 | mov esi,esp | main.c:8
004113EE | 8D45 F8 | lea eax,dword ptr ss:[ebp-0x8] |
004113F1 | 50 | push eax |
004113F2 | 68 58584100 | push consoleapplication1.415858 | 415858:"%d"
004113F7 | FF15 10914100 | call dword ptr ds:[<&scanf>] |
说到局部变量,不得不提起局部静态变量,局部静态变量的声明只需要使用static
关键字声明,该变量比较特殊,他不会随作用域的结束而消亡,并且也是在未进入作用域之前就已经存在了,其实局部静态变量也是全局变量,只不过它的作用域被限制在了某一个函数内部而已,所以它本质上还是全局变量,来一段代码验证一下:
#include <stdio.h>
int main(int argc, char* argv[])
{
static int g_number = 0;
for (int x = 0; x <= 10; x++)
{
g_number = x;
printf("输出: %d\n", g_number);
}
return 0;
}
观察这段反汇编代码,你能够清晰的看出,同样是使用mov eax,dword ptr ds:[<g_number>]
从全局数据区取数据的,这说明局部变量声明为静态属性以后,就和全局变量变成了一家人了.
004113DE | C745 F8 00000000 | mov dword ptr ss:[ebp-0x8],0x0 | main.c:7
004113E5 | EB 09 | jmp 0x4113F0 |
004113E7 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] |
004113EA | 83C0 01 | add eax,0x1 |
004113ED | 8945 F8 | mov dword ptr ss:[ebp-0x8],eax |
004113F0 | 837D F8 0A | cmp dword ptr ss:[ebp-0x8],0xA | A:'\n'
004113F4 | 7F 27 | jg 0x41141D |
004113F6 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | main.c:9
004113F9 | A3 30814100 | mov dword ptr ds:[<g_number>],eax |
004113FE | 8BF4 | mov esi,esp | main.c:10
00411400 | A1 30814100 | mov eax,dword ptr ds:[<g_number>] | 与全局变量是一家人
00411405 | 50 | push eax |
00411406 | 68 58584100 | push consoleapplication1.415858 | 415858:"输出: %d\n"
0041140B | FF15 14914100 | call dword ptr ds:[<&printf>] |
00411411 | 83C4 08 | add esp,0x8 |
00411414 | 3BF4 | cmp esi,esp |
00411416 | E8 1BFDFFFF | call 0x411136 |
0041141B | EB CA | jmp 0x4113E7 | main.c:11
0041141D | 33C0 | xor eax,eax | main.c:12
探索堆变量的奥秘: 堆变量是最容易识别的一种变量类型,因为分配堆区的函数就几个calloc/malloc/new
等,所以这类变量往往能被调试器直接补货到,这种变量同样属于局部变量的范畴,因为它也是通过函数动态申请的一段内存空间,这里只给出一个案例吧,反编译大家可以自己研究,这一个是很简单的了.
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char* argv[])
{
int *pMalloc = (int*)malloc(10);
printf("变量地址: %x", pMalloc);
free(pMalloc);
return 0;
}
针对C语言的反汇编,就剩一个结构体与共用体了,这里的内容比较少,我就不再新的文章里写了,直接在这里把它给写完,C语言的反汇编就到此结束。
C语言提供给我们了一些由系统定义的数据类型,我们也可以自己定义这样的数据类型,结构体与共用体就是用来定义一些比较复杂的数据结构的这么一个方法,定义结构很简单只需要使用struct
关键字即可,定义共用体则使用union
来实现,接下来将分别演示它们之间的反汇编状态.
首先我们来定义tag
结构体,假设结构体中的当前数据成员类型长度为M,指定对其值为N,那么实际对其值为Q = min(M,N)
,其成员的地址将被编译器安排在Q的倍数上,例如默认8字节对齐,则需要安排在8,16,24,32
字节之间,如下结构体.
struct tag{
short sShort; // 占用2字节的空间
int nInt; // 占用4字节的空间
double dDouble; // 占用8字节的空间
}
在VS编译器中默认数据块的对其值是8字节,上方定义的tag
结构中sShort
占用2个字节的空间,而nInt
则占用4字节的空间,dDouble
则占用8字节的存储空间,那么结构体成员的总长度8+4+2=14bytes
按照默认的对其值8字节来对其,结构体分配空间需要被8整除,也就是最低要分配16字节的空间给tag
这个结构,那么编译器会自动在14字节的基础上增加2字节的垫片,来保证tag
结构体内被系统更好的接受.
默认情况下编译器会自动找出最大的变量值double dDouble
使用它的字节长度来充当数据块对齐尺寸,例如上方代码中最大值是double 8字节,那么相应的对齐尺寸就应该是8字节,不足8字节的变量编译器会自动补充垫片字节,当然我们也可以通过预编译指令#pragma pack(N)
来手动调整对齐大小.
定义结构体成员: 首先定义Student结构,然后动态的赋值,观察其参数的变换.
需要注意的是,结构体类型与结构体变量是不同的概念,通常结构体类型的定义并不会分配空间,只有结构体变量被赋值后编译器才会在编译时对其进行处理,结构体类型与结构体变量,其在内存中的表现形式都是普通变量,而结构则是编译器对语法进行的一种处理,编译时会将其转为普通的变量来对待.
#include <stdio.h>
struct Student
{
long int number;
char name[20];
char sex;
};
int main(int argc, char* argv[])
{
struct Student num1;
scanf("%d", &num1.number);
scanf("%s", &num1.name);
scanf("%c", &num1.sex);
printf("编号: %d 姓名: %s 性别: %c", num1.number, num1.name, num1.sex);
return 0;
}
为了验证上面的猜测,我们将其反汇编,观察代码会发现结构体之间的变化,通过0x20-0x1c
可得到第一个结构的大小,同理0x1c-0x08
得到的则是第二个结构以此类推,就可推测出部分结构成员的类型.
004113E0 | 8D45 E0 | lea eax,dword ptr ss:[ebp-0x20] | 第一个结构
004113E3 | 50 | push eax |
004113E4 | 68 58584100 | push consoleapplication1.415858 | 415858:"%d"
004113E9 | FF15 10914100 | call dword ptr ds:[<&scanf>] |
004113EF | 83C4 08 | add esp,0x8 |
004113F2 | 3BF4 | cmp esi,esp |
004113F4 | E8 42FDFFFF | call 0x41113B |
004113F9 | 8BF4 | mov esi,esp | main.c:14
004113FB | 8D45 E4 | lea eax,dword ptr ss:[ebp-0x1C] | 第二个结构
004113FE | 50 | push eax |
004113FF | 68 5C584100 | push consoleapplication1.41585C | 41585C:"%s"==L"猥"
00411404 | FF15 10914100 | call dword ptr ds:[<&scanf>] |
0041140A | 83C4 08 | add esp,0x8 |
0041140D | 3BF4 | cmp esi,esp |
0041140F | E8 27FDFFFF | call 0x41113B |
00411414 | 8BF4 | mov esi,esp | main.c:15
00411416 | 8D45 F8 | lea eax,dword ptr ss:[ebp-0x8] | 第三个结构
00411419 | 50 | push eax |
0041141A | 68 60584100 | push consoleapplication1.415860 | 415860:"%c"==L"挥"
0041141F | FF15 10914100 | call dword ptr ds:[<&scanf>] |
00411425 | 83C4 08 | add esp,0x8 |
定义结构体数组: 结构体数组中每个数组元素都是一个结构体类型的数据,他们都分别包括各个成员项.
#include <stdio.h>
#include <string.h>
struct Student
{
char name[20];
int count;
};
int main(int argc, char* argv[])
{
int x, y;
char leader_name[20];
struct Student leader[3] = { "admin", 0, "lyshark", 0, "guest", 0 };
for (x = 0; x <= 10; x++)
{
scanf("%s", leader_name);
for (y = 0; y < 3; y++)
{
if (strcmp(leader_name, leader[y].name) == 0)
leader[y].count++;
}
}
for (int z = 0; z < 3; z++)
{
printf("用户名: %5s 出现次数: %d\n", leader[z].name, leader[z].count);
}
system("pause");
return 0;
}
逆向上方这段代码,我们主要观察它的寻址方式,你会发现其本质上就是数组寻址,并没有任何的特别的.
004114F9 | 83BD 74FFFFFF 03 | cmp dword ptr ss:[ebp-0x8C],0x3 | 指定循环次数 3
00411500 | 7D 31 | jge 0x411533 |
00411502 | 6B85 74FFFFFF 18 | imul eax,dword ptr ss:[ebp-0x8C],0x18 | 每次递增0x18 => char name[20] + int count = 24
00411509 | 8BF4 | mov esi,esp |
0041150B | 8B4C05 C8 | mov ecx,dword ptr ss:[ebp+eax-0x38] | 找到 count
0041150F | 51 | push ecx | ecx:"guest"
00411510 | 6B95 74FFFFFF 18 | imul edx,dword ptr ss:[ebp-0x8C],0x18 |
00411517 | 8D4415 B4 | lea eax,dword ptr ss:[ebp+edx-0x4C] | 找到 name[20]
0041151B | 50 | push eax |
0041151C | 68 78584100 | push consoleapplication1.415878 | 415878:"用户名: %5s 出现次数: %d\n"
00411521 | FF15 20914100 | call dword ptr ds:[<&printf>] |
00411527 | 83C4 0C | add esp,0xC |
指向结构体数组的指针: 结构体指针就是指向结构体变量的指针,结构体变量的前4字节就是该结构体的指针,将该指针存放到一个指针变量中,那么这个指针变量就可以叫做结构指针变量,结构体指针定义如下.
#include <stdio.h>
#include <string.h>
struct Student
{
int number;
char name[20];
};
struct Student stu[3] = { { 1, "admin" }, { 2, "lyshark" }, { 3, "guest" } };
int main(int argc, char* argv[])
{
struct Student *structPTR;
for (structPTR = stu; structPTR < stu + 3; structPTR++)
{
printf("编号: %d 名字: %s \n", (*structPTR).number, structPTR->name);
}
system("pause");
return 0;
}
观察以下这段反汇编代码,你会发现其实和前面的指针数组寻址一个道理,并没有什么野路子.
004113DE | C745 F8 00804100 | mov dword ptr ss:[ebp-0x8],0x418000 | 此处获取结构体指针 => structPTR = stu
004113E5 | EB 09 | jmp 0x4113F0 |
004113E7 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | [ebp-8]:_stu
004113EA | 83C0 18 | add eax,0x18 | 递增 structPTR++ 每次递增一个结构
004113ED | 8945 F8 | mov dword ptr ss:[ebp-0x8],eax | 将递增后的指针回写
004113F0 | 817D F8 48804100 | cmp dword ptr ss:[ebp-0x8],0x418048 | 对比指正是否结束
004113F7 | 73 26 | jae 0x41141F |
004113F9 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | main.c:18, [ebp-8]:_stu
004113FC | 83C0 04 | add eax,0x4 | eax:"admin"
004113FF | 8BF4 | mov esi,esp |
00411401 | 50 | push eax | 将 structPTR->name 压栈
00411402 | 8B4D F8 | mov ecx,dword ptr ss:[ebp-0x8] | [ebp-8]:_stu
00411405 | 8B11 | mov edx,dword ptr ds:[ecx] | 取出计数地址
00411407 | 52 | push edx |
00411408 | 68 58584100 | push consoleapplication1.415858 | 415858:"编号: %d 名字: %s \n"
0041140D | FF15 18914100 | call dword ptr ds:[<&printf>] | 输出结果
00411413 | 83C4 0C | add esp,0xC |
向函数内传递结构体: 将函数的形参列表定义为结构体参数,该函数就可以接收一个结构体列表了,收到列表后我们可以取出里面的最大值并返回.
#include <stdio.h>
#include <string.h>
struct Student
{
int number;
char name[20];
float aver;
};
struct Student stud[3] = { { 1, "admin" ,89}, { 2, "lyshark" ,76}, { 3, "guest",98 }};
int GetMaxID(struct Student stu[])
{
int x , item = 0;
for (x = 0; x < 3; x++)
{
if (stu[x].aver > stu[item].aver)
item = x;
}
return stu[item].number;
}
int main(int argc, char* argv[])
{
int item;
item = GetMaxID(stud);
printf("成绩最高的学生编号: %d", item);
system("pause");
return 0;
}
这里不啰嗦,直接看反汇编代码能发现在主函数调用call 0x4110e6
之前是将push <console._stud>
结构体的首地址传入了函数内部执行的.
0041146C | 8DBD 34FFFFFF | lea edi,dword ptr ss:[ebp-0xCC] |
00411472 | B9 33000000 | mov ecx,0x33 | 33:'3'
00411477 | B8 CCCCCCCC | mov eax,0xCCCCCCCC |
0041147C | F3:AB | rep stosd |
0041147E | 68 00804100 | push <console._stud> | 将结构体首地址传递到call内部
00411483 | E8 5EFCFFFF | call 0x4110E6 |
00411488 | 83C4 04 | add esp,0x4 |
最后一段C代码是实现了返回结构体的结构,就是说将处理好的结构体返回给上层调用,其原理也是利用了指针,这里只把代码放出来,自己分析一下吧.
#include <stdio.h>
struct tag{
int x;
int y;
char z;
};
tag RetStruct()
{
tag temp;
temp.x = 10;
temp.y = 20;
temp.z = 'A';
return temp;
}
int main(int argc, char* argv[])
{
tag temp;
temp = RetStruct();
printf("%d \n",temp.x);
printf("%d \n",temp.y);
printf("%d \n",temp.z);
return 0;
}
定义并使用共用体类型: 有时候我们想要使用同一段内存数据来表示不同的数据类型,那么我们就可以使用共用体类型.
结构体与共用体的定义形式相似,但他们的含义完全不同,结构体变量所占用的内存长度是各成员占的内存长度之和,每个成员分别占有其自己的内存单元,而共用体变量所占用的内存长度则等于共用体中的最长的成员的长度,首先我们先来研究C代码.
#include <stdio.h>
union Date
{
int num;
char ch;
float f;
}dat;
int main(int argc, char* argv[])
{
dat.num = 97;
printf("以整数形式输出: %d\n", dat.num);
printf("以字符形式输出: %c\n", dat.ch);
printf("以浮点数形式输出: %f\n", dat.f);
system("pause");
return 0;
}
以上代码我们通过dat.num = 97;
给共用体赋予了整数类型的初始值,后面则是按照不同的形式输出这段内存,其反汇编代码如下,观察代码可发现共用体仅仅储存一份变量数据在程序的常量区,当我们调用不同类型的共用体是则进行相应的转换,其实这些都是编译器为我们做的,本质上共用体其实也是一个个普通的变量.
004113DE | C705 48854100 61000000 | mov dword ptr ds:[<_dat>],0x61 | main.c:12, 00418548:L"a", 61:'a'
004113E8 | 8BF4 | mov esi,esp | main.c:13
004113EA | A1 48854100 | mov eax,dword ptr ds:[<_dat>] | 使用整数方式输出
004113EF | 50 | push eax |
004113F0 | 68 58584100 | push consoleapplication1.415858 | 415858:"以整数形式输出: %d\n"
004113F5 | FF15 18914100 | call dword ptr ds:[<&printf>] |
004113FB | 83C4 08 | add esp,0x8 |
004113FE | 3BF4 | cmp esi,esp |
00411400 | E8 36FDFFFF | call 0x41113B |
00411405 | 0FBE05 48854100 | movsx eax,byte ptr ds:[<_dat>] | 输出字符
0041140C | 8BF4 | mov esi,esp |
0041140E | 50 | push eax |
0041140F | 68 70584100 | push consoleapplication1.415870 | 415870:"以字符形式输出: %c\n"
00411414 | FF15 18914100 | call dword ptr ds:[<&printf>] |
0041141A | 83C4 08 | add esp,0x8 |
0041141D | 3BF4 | cmp esi,esp |
0041141F | E8 17FDFFFF | call 0x41113B |
00411424 | F3:0F5A05 48854100 | cvtss2sd xmm0,dword ptr ds:[<_dat>] | 输出浮点数
0041142C | 8BF4 | mov esi,esp |
0041142E | 83EC 08 | sub esp,0x8 |
00411431 | F2:0F110424 | movsd qword ptr ss:[esp],xmm0 |
00411436 | 68 88584100 | push consoleapplication1.415888 | 415888:"以浮点数形式输出: %f\n"
0041143B | FF15 18914100 | call dword ptr ds:[<&printf>] |
00411441 | 83C4 0C | add esp,0xC |
既然了解了共用体的结构类型,那不妨编译以下代码然后逆向分析它的寻址方式,观察与数组指针是否一致呢?
#include <stdio.h>
struct
{
char job; // s=学生 t=老师
union
{
int clas; // 学生学号
char position[20]; // 老师职务
}category;
}person[2];
int main(int argc, char* argv[])
{
for (int x = 0; x < 2; x++)
{
scanf("%c", &person[x].job); // 输入人物类型
if (person[x].job == 't')
{
scanf("%s", &person[x].category.position); // 如果是老师则输入职务
}
else if (person[x].job == 's')
{
scanf("%d", &person[x].category.clas); // 如果是学生则输入学号
}
}
for (int y = 0; y < 2; y++)
{
if (person[y].job == 's')
printf("学生学号: %d\n", person[y].category.clas);
else if (person[y].job == 't')
printf("老师职务: %s\n", person[y].category.position);
}
system("pause");
return 0;
}
定义并使用枚举类型: 如果一个变量只有几种可能,那么我们就可以定义一个枚举字典,通过循环的方式枚举元素,编译以下代码观察变化,其中的枚举{red,yellow,blue,white,black}
会被编译器在编译时替换为{0,1,2,3,4}
等数字,所以反汇编以下代码你回范县并没有出现字符串,而是使用数字来代替了.
#include <stdio.h>
int main(int argc, char* argv[])
{
enum Color {red,yellow,blue,white,black};
enum Color x;
for (x = red; x <= black; x++)
{
printf("元素值: %d\n",x);
switch (x)
{
case red: printf("red 出现了\n"); break;
case blue: printf("blue 出现了\n"); break;
}
}
system("pause");
return 0;
}
至此,我们的C语言反汇编的内容就结束了,接下来我们将领略C++ 的反汇编技巧,C++ 是重头戏,其中的类,构造析构函数,等都是重点,不过C++ 在识别上其实更加的容易,因为其封装的更加彻底,对C语言的封装。