soNiliCs

Game! Game! Game!
随笔 - 2, 文章 - 2, 评论 - 0, 引用 - 0
数据加载中……

崩溃分析用到的一些汇编语言技巧

常用寄存器

寄存器
名称
常见用途(未完)
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加上条件跳转指令。
cmp <条件>
jle 
<下一个分支>
所以开始的反汇编为:
    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内核编程》

 

 

 


posted on 2011-01-07 20:19 sonilics 阅读(825) 评论(0)  编辑 收藏 引用 所属分类: Debugging


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理