常用寄存器
寄存器
|
名称
|
常见用途(未完)
|
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中。
;保存ebp,并把esp放入ebp中,此时ebp与esp都为这次函数调用的栈顶
push ebp
mov ebp,esp
3.在堆栈中预留一个区域用于保存局部变量。方法是将esp减少一个数值,这样就等于压入了一堆变量。要恢复的时候直接把esp回复成ebp保存的数据就可以了。
4.保存ebx、esi、edi到堆栈中,函数调用完成后恢复。
;把esp往下移动一个范围,等于在堆栈中预留一片新的空间来保存局部变量
sub esp,010h
push ebx
push esi
push edi
5.(debug版)把局部变量全部初始化为0xcccccccch.
;将保存局部变量的区域全部初始化为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 edi ;恢复edi、esi、ebx
pop esi
pop ebx
mov esp, ebp ;恢复原来的ebp和esp
pop ebp
ret
调用方:
mov eax,dword ptr [b]
push eax
move ecx,dword ptr [a]
push ecx
call myfunction
add esp,8 ;回复堆栈
常见的基础代码结构
for循环
for(int i = 0; i < 20; ++i )
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回到修改循环变量的代码进行下一次循环。大体结构如下:
mov <循环变量>,<初始值> ;给循环变量赋值
jmp A ;跳到第一次循环处
A: (改动循环变量) ;修改循环变量
B: cmp <循环变量>,<限制变量> ;检查循环变量
jge 跳出循环
(循环体)
jmp A ;跳回修改循环变量
do循环
int i = 0;
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循环就是用一个简单的条件比较指令跳转回去:
cmp <循环变量><限制变量>
jl <循环开始>
while循环
int i = 0;
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除了开始的时候判断循环条件之外,后面还要有一条无条件跳转指令:
A: cmp <循环变量>,<限制变量>
jge B
(循环体)
jmp A
B: (跳出循环)
if-else判断分支
int i = 0;
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
if 判断都是使用cmp加上条件跳转指令。
所以开始的反汇编为:
if ( i < 10 )
0040B94C cmp dword ptr [i],0Ah ;判断点
0040B950 jge wmain+3Bh (40B95Bh) ;跳转到下一个else if
else if和else的特点是,在开始的地方都有一条无条件跳转指令,跳转到判断结束处,阻止前面的分支执行结束后,直接进入这个分支的可能,这个分支执行的唯一条件为前面的判断不满足。else则在jmp之后直接执行操作,而else if则开始重复if之后的操作,用cmp比较,然后用条件质量进行跳转。
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
}
switch-case 判断分支
switch的特点是有多个判断。因为switch显然不会判断大于小于,所以都是je,分别跳转到每个case处,最有一个是无条件跳转,直接跳到default处。
对于break,会增加一个无条件跳转语句,跳转至结尾
int i = 0;
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
所以如果看到有多个连续的
cmp
je
标志着可能是swith语句
访问结构体数组成员
对于以下代码:
struct A
{
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循环中所对应的汇编为
ar[i].a = 0;
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.表达式的合并与控制流程的结合。
Reference:
学 Win32 汇编[29] - 串指令: MOVS*、CMPS*、SCAS*、LODS*、REP、REPE、REPNE 等
《天书夜读-从汇编语言到Windows内核编程》