汇编部分
1、call 的本质相当于push+jmp,ret的本质相当于pop+jmp。
2、Windows中,不管哪种调用方式都是返回值放在eax中,然后返回。外部从eax中得到值。
3、Ebp总是被我们用来保存这个函数执行之前的esp的值。
4、把局部变量区域初始化成全0cccccccch,0cch实际是int 3 指令的机器码,这是一个断点中断指令。
5、任何一段中间不加任何跳转,连续的mov和加减乘除指令一般都可以还原为一个C表达式。
如果有下面的代码段,说明可能是含有数组或结构体。
Mov eax,<数组下标>
Inul eax,eax,<结构大小>
Mov ecx, <结构数组开始的地址>
Mov eax, dword ptr [ecx + eax]
6、分析汇编指令时:
与堆栈操作相关的,call,ret等相关指令,我们叫做函数调用([函数])指令:F
流程控制代码,涉及判断和跳转指令:C
数据处理指令,其它一般为数据处理指令:D。常用寄存器
寄存器 | 名称 | 常见用途(未完) |
eax | 累加器(Accumulator) | 函数返回值 |
ebx | 基址寄存器(Base) | 可作为存储指针来使用 |
ecx | 计数器(Counter) | 在循环和字符串操作时,用来控制循环次数 __thiscall中传递this指针 |
edx | 数据寄存器(Data) | |
esp | 堆栈指针寄存器(Stack) | |
ebp | 基地址指针寄存器(Base) | |
esi | 源地址寄存器(Source Index) | |
edi | 目的地址寄存器(Destination) |
常用汇编指令
push | 把一个32位的操作数压入堆栈,这个操作会导致esp减4. |
pop | 与push相反,esp加4,一个数据出栈 |
call | 调用函数。将下一条指令的地址压栈,然后跳转到所调用函数的开始处,本质相当于push+jump |
ret | 与call相对应,跳转到栈顶数据所指的地址,本质相当于pop+jump。对于_cdecl 调用的函数,通常会在ret之后进行exp-[n],用于清理调用参数堆栈 |
xor | 异或,常用于清零操作,例如: xor eax eax |
lea | 取得地址(第二个参数)后放入前面的寄存器中。 |
stosw | 将eax中的数据传送给edi,之后edi+4。常与rep一起使用,用于初始化内存段 |
rep | 当eax>0时,重复后面的指令 |
jp,jl,jge | 根据eax中值与0的关系跳转 |
cmp | 比较指令,将结果放入eax中,往往是jp,jl,jge之类跳转指令的执行条件 |
函数调用规则
调用方式 | 简要说明 | 堆栈清理 | 参数传递规则 |
_cdecl | C 编译器的默认调用规则 | Caller | 从右到左 |
_stdcall | 又称为WINAPI | Callee | 从右到左 |
__thiscall | C++成员函数调用方式 | Callee | this放入ecx,其他从右到左 |
__fastcall | Callee | 前两个等于或者小于DWORD大小的参数放入ecx和edx,其他参数从右到左 |
_cdecl调用通常的asm代码:
被调用方:
1.保存ebp。ebp总是用来保存这个函数执行之前的esp值。执行完毕之后,我们用ebp回复esp;同时,调用此函数的上层函数也用ebp做同样的事情。
2.保存esp到ebp中。
push ebp
mov ebp,esp
3.在堆栈中预留一个区域用于保存局部变量。方法是将esp减少一个数值,这样就等于压入了一堆变量。要恢复的时候直接把esp回复成ebp保存的数据就可以了。
4.保存ebx、esi、edi到堆栈中,函数调用完成后恢复。
sub esp,010h
push ebx
push esi
push edi
5.(debug版)把局部变量全部初始化为0xcccccccch.
lea edi,[ebp- 010h]
mov ecx,33h
mov eax,0xcccccccch
rep stos dword ptr [edi]
6.然后执行函数的具体逻辑。传入参数的获取为:ebp+4为函数的返回地址;ebp+8为第一个参数,ebp+12为第二个参数,以此类推。
7.回复ebx、esi、edi、esp、ebp,最后返回。如果有返回值,在返回之前将保存在eax中,供调用方式用。
pop esi
pop ebx
mov esp, ebp ;恢复原来的ebp和esp
pop ebp
ret
调用方:
push eax
move ecx,dword ptr [a]
push ecx
call myfunction
add esp,8 ;回复堆栈
常见的基础代码结构
for循环
0040B93E mov dword ptr [i],0
0040B945 jmp wmain+ 30h (40B950h)
0040B947 mov eax,dword ptr [i]
0040B94A add eax,1
0040B94D mov dword ptr [i],eax
0040B950 cmp dword ptr [i],14h
0040B954 jge wmain+ 38h (40B958h)
{
}
0040B956 jmp wmain+27h (40B947h)
可以看到主循环主要由这么几条指令来实现:mov进行初始化;jmp跳过修改循环变量的代码;cmp实现跳转判断;jge根据条件跳转。用jmp回到修改循环变量的代码进行下一次循环。大体结构如下:
jmp A ;跳到第一次循环处
A: (改动循环变量) ;修改循环变量
B: cmp <循环变量>,<限制变量> ;检查循环变量
jge 跳出循环
(循环体)
jmp A ;跳回修改循环变量
do循环
0040B93E mov dword ptr [i],0
do
{
++ i;
0040B945 mov eax,dword ptr [i]
0040B948 add eax,1
0040B94B mov dword ptr [i],eax
} while (i<10 );
0040B94E cmp dword ptr [i],0Ah
0040B952 jl wmain+25h (40B945h)
上面的do循环就是用一个简单的条件比较指令跳转回去:
jl <循环开始>
while循环
0040B93E mov dword ptr [i],0
while (i<10 )
0040B945 cmp dword ptr [i],0Ah
0040B949 jge wmain+ 36h (40B956h)
{
++ i;
0040B94B mov eax,dword ptr [i]
0040B94E add eax,1
0040B951 mov dword ptr [i],eax
}
0040B954 jmp wmain+25h (40B945h)
while要复杂一些,因为wile除了开始的时候判断循环条件之外,后面还要有一条无条件跳转指令:
jge B
(循环体)
jmp A
B: (跳出循环)
if-else判断分支
0040B93E mov dword ptr [i],0
int j = 0 ;
0040B945 mov dword ptr [j],0
if ( i < 10 )
0040B94C cmp dword ptr [i],0Ah
0040B950 jge wmain+ 3Bh (40B95Bh)
{
j = 10 ;
0040B952 mov dword ptr [j],0Ah
0040B959 jmp wmain+ 51h (40B971h)
}
else if (i < 20 )
0040B95B cmp dword ptr [i],14h
0040B95F jge wmain+ 4Ah (40B96Ah)
{
j = 20 ;
0040B961 mov dword ptr [j],14h
}
else
0040B968 jmp wmain + 51h (40B971h)
{
j = 30 ;
0040B96A mov dword ptr [j],1Eh
}
return 0 ;
0040B971 xor eax,eax
jle <下一个分支>
0040B94C cmp dword ptr [i],0Ah ;判断点
0040B950 jge wmain+3Bh (40B95Bh) ;跳转到下一个else if
else则在jmp之后直接执行操作,而else if则开始重复if之后的操作,用cmp比较,然后用条件质量进行跳转。
}
else if (i < 20 )
0040B95B cmp dword ptr [i],14h
0040B95F jge wmain+ 4Ah (40B96Ah) ;比较,条件跳转,目标为下一个分支
{
j = 20 ;
0040B961 mov dword ptr [j],14h
}
switch-case 判断分支
switch的特点是有多个判断。因为switch显然不会判断大于小于,所以都是je,分别跳转到每个case处,最有一个是无条件跳转,直接跳到default处。
对于break,会增加一个无条件跳转语句,跳转至结尾
0040B93E mov dword ptr [i],0
int j = 0 ;
0040B945 mov dword ptr [j],0
switch (i)
0040B94C mov eax,dword ptr [i]
0040B94F mov dword ptr [ebp- 0DCh],eax
0040B955 cmp dword ptr [ebp-0DCh],0
0040B95C je wmain+49h (40B969h) ;判断case 1
0040B95E cmp dword ptr [ebp -0DCh],1
0040B965 je wmain+52h (40B972h) ;判断case 2
0040B967 jmp wmain + 59h (40B979h) ;跳转到default
{
case 0:
j = 0;
0040B969 mov dword ptr [j],0
break; ;跳转到结束
0040B970 jmp wmain+60h (40B980h)
case 1:
j = 1;
0040B972 mov dword ptr [j],1
default:
j = 3;
0040B979 mov dword ptr [j],3
}
return 0 ;
0040B980 xor eax,eax
je
标志着可能是swith语句
访问结构体数组成员
对于以下代码:
{
int a;
int b;
int c;
} ;
int wmain(int argc, wchar_t* argv[])
{
A ar[3];
for (int i=0;i<3;++i)
{
ar[i].a = 0;
ar[i].b = 0;
ar[i].c = 0;
}
return 0;
}
for循环中所对应的汇编为
0040B956 mov eax,dword ptr [i] ;访问第i个数据
0040B959 imul eax,eax,0Ch ;0ch为结构体的大小,这里得到访问第i个机构体的地址偏移
0040B95C mov dword ptr ar[eax],0 ;取得第i个结构体的第一个元素地址
ar[i].b = 0;
0040B964 mov eax,dword ptr [i]
0040B967 imul eax,eax,0Ch
0040B96A mov dword ptr [ebp+eax-24h],0
ar[i].c = 0;
0040B972 mov eax,dword ptr [i]
0040B975 imul eax,eax,0Ch
0040B978 mov dword ptr [ebp+eax-20h],0
对于结构体数组的访问有个很明显的特征:使用imul取得某个数组元素的地址偏移,然后在加上所要访问结构体成员的地址偏移。同时,大多数情况下结构的的大小都是在编译期决定的,imul的最后一个参数会是个常量。
阅读汇编代码的一些技巧
1.将指令分类:
首先F(function)类指令:是函数调用相关代码,这些代码用于函数或者作为一个函数数被调用。几乎凡是堆栈操作(备份集陈启或者压入参数)可全部归入此类。此外还有call指令、堆栈恢复。
然后C(control)类指令 :设计判断和跳转指令,以及对循环变量操作的指令。这些代码用于循环、判断语句。
剩余D(data)类指令:数据处理指令,应该不包含函数调用,多半不含有堆操作,也不会含有跳转。
2.翻译D类指令。
3.表达式的合并与控制流程的结合。