OD/Windbg 二、从C/C++到汇编 (持续更新)

各位读者,总结如有错误请指正,谢谢。

2.1 常见指令

2.1.1堆栈相关命令
Push:把一个32位操作压入堆栈中。Esp -4,我们认为栈顶是最小的区域;
Pop:与push相反,一个数据出栈;
sub:减法:第一个是被减数的寄存器,第二个参数是减数;
Add:加法:
Ret:返回,自动的弹出返回地址;
Call:调用函数。将下一条指令的地址压入堆栈中,然后指向它调用的函数的开头;
区别:call相当于Push 下条指令 + jump 进入函数;ret相当于 pop + jump;
2.1.2数据传输指令
Mov: 数据移动,第一个是目的,第二个是来源;
Xor:异或,xor eax,eax 常用来代替mov eax,0. 
Lea: 取地址(第二个参数)后放入到前面的寄存器(第一个参数)中。
因此:lea    edi,[ebp - 0cch]  相当于 mov    edi,ebp - 0cch 但是mov不支持后一个操作数写成寄存器减去数字,但是lea支持,所以用lea 代替了mov。
Stos:串存储指令,处理4个字节;将eax中的数据存放如edi所指的地址中,同时edi会增加4字节;
Stow:处理word 2个字节;
Stob:处理byte 1个字节;
Rep:指指令重复执行ecx 中填写的次数。

2.1.2数据传输指令:
Mov: 数据移动,第一个是目的,第二个是来源;
Xor:异或,xor eax,eax 常用来代替mov eax,0. 
Lea: 取地址(第二个参数)后放入到前面的寄存器(第一个参数)中。
因此:lea    edi,[ebp - 0cch]  相当于 mov    edi,ebp - 0cch 但是mov不支持后一个操作数写成寄存器减去数字,但是lea支持,所以用lea 代替了mov。
Stos:串存储指令,处理4个字节;将eax中的数据存放如edi所指的地址中,同时edi会增加4字节;
Stow:处理word 2个字节;
Stob:处理byte 1个字节;
Rep:指指令重复执行ecx 中填写的次数。

其他命令:
Shr: 逻辑右移  相反的shl
Sar: 算数右移
两者区别:SAR右移的时候保留操作数的符号,即使用符号位来补足,而SHR右移时总是用0来补足;我们在编码的过程中,右移动时高位补零还是补1是变量的类型决定的;

2.2 例子: 局部变量赋值对应的汇编代码

#include “stdafx.h”

int main(int argc, _TCHAR* argv[])

{

       int Var1  = 0;

       return 0;

}

对应的汇编:

逐句汇编的解释:
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
{
00D81370  push        ebp                      //保存ebp,
00D81371  mov         ebp,esp                 //esp 栈顶位置赋值给ebp;ebp = esp
ebp保存这个函数执行之前的esp
00D81373  sub         esp,0CCh                  //分配栈空间,esp - 0xcc保存局部变量
00D81379  push        ebx                      //保存ebx
00D8137A  push        esi                      //保存esi
00D8137B  push        edi                      //保存edi
00D8137C  lea         edi,[ebp-0CCh]             //edi = ebp -0xcc ,其实edi = esp 
                                            本来是用mov,但是mov不支持-操作。
00D81382  mov         ecx,33h                 //ecx = 33  分配33个4字节空间。
00D81387  mov         eax,0CCCCCCCCh             //eax = 0xCCCCCCCC  每四字节赋值为OXcccccccc
00D8138C  rep stos    dword ptr es:[edi]         //rep重复33次,写入occh指令(中断指令)
    int Var1 = 0;
00D8138E  mov         dword ptr [Var1],0         //变量赋值

    return 0;
00D81395  xor         eax,eax                 //异或清零
}
00D81397  pop         edi                      //恢复edi、esi、ebx
00D81398  pop         esi      
00D81399  pop         ebx                      
00D8139A  mov         esp,ebp                 //函数执行完后,用ebp来恢复esp
00D8139C  pop         ebp                      
00D8139D  ret  
总结:
1、从这里可以看出函数调用是压入了好多寄存器值,返回的时候做逆向操作。
2、先做保存现场,再做堆栈的初始化(这里初始化为CC),然后再去做局部变量的复制操作,最后退出来;
3、堆栈的初始化过程,是先esp - 0xcc(栈顶向上减0xCC),然后用rep来操作edi (esp的地址)向下每四个字节初始化为0xCCCCCCCC(eax) ,初始化33次(ecx).
小技巧:函数头饰固定的,这个可以作为我们在IDA中确定函数的头;
 

2.3 C语言流程到对应的汇编

2.3.1 for循环

对应的反汇编:
    for(i = 0; i < 50 ;i++)
00192DD7  mov         dword ptr [i],0             //mov初始化变量i
00192DDE  jmp         myfunction2+39h (192DE9h)     //jmp跳过修改循环变量的代码
00192DE0  mov         eax,dword ptr [i]             //局部变量
00192DE3  add         eax,1                     //i++
00192DE6  mov         dword ptr [i],eax             //eax的值赋值给局部变量i
00192DE9  cmp         dword ptr [i],32h             //cmp 实现条件判断I 与 50比较
00192DED  jge         myfunction2+4Ah (192DFAh)     //大于等于50就跳转到结束,否者顺序执行
    {
        c = c + i;
00192DEF  mov         eax,dword ptr [c]             
00192DF2  add         eax,dword ptr [i]             //
00192DF5  mov         dword ptr [c],eax             //改变局部变量C的值
    }
00192DF8  jmp         myfunction2+30h (192DE0h) 
    return c;
00192DFA  mov         eax,dword ptr [c]
基本结构图为:

总结:使用jmp来实现跳转循环;
 

2.3.2 do循环

反汇编为:
    do
    {
        c = c +i;
001C17EE  mov         eax,dword ptr [c] 
001C17F1  add         eax,dword ptr [i] 
001C17F4  mov         dword ptr [c],eax 
    }while(c < 50);
001C17F7  cmp         dword ptr [c],32h 
001C17FB  jl          myfunctionDo+2Eh (1C17EEh)

2.3.3 while 循环

对应的反汇编:
    while(c < 50)
00B42E3E  cmp         dword ptr [c],32h 
00B42E42  jge         myfunctionWhile+3Fh (0B42E4Fh) 
    {
        c = c +i;
00B42E44  mov         eax,dword ptr [c] 
00B42E47  add         eax,dword ptr [i] 
00B42E4A  mov         dword ptr [c],eax 
    }
00B42E4D  jmp         myfunctionWhile+2Eh (0B42E3Eh)
    return c;
00B42E4F  mov         eax,dword ptr [c]
 

2.3.4 if -else

对应的反汇编:
    if (c == 10 && c < 11)
00A43067  cmp         dword ptr [c],0Ah 
00A4306B  jne         myfunctionIfElse+40h (0A43080h) 
00A4306D  cmp         dword ptr [c],0Bh 
00A43071  jge         myfunctionIfElse+40h (0A43080h) 
    {
        c = c/2;
00A43073  mov         eax,dword ptr [c] 
00A43076  cdq              
00A43077  sub         eax,edx 
00A43079  sar         eax,1 
00A4307B  mov         dword ptr [c],eax 
00A4307E  jmp         myfunctionIfElse+5Ch (0A4309Ch) 
    }
    else if (c == 8 || c == 6)
00A43080  cmp         dword ptr [c],8 
00A43084  je          myfunctionIfElse+4Ch (0A4308Ch) 
00A43086  cmp         dword ptr [c],6 
00A4308A  jne         myfunctionIfElse+55h (0A43095h) 
    {
        c = 4/2;
00A4308C  mov         dword ptr [c],2 
00A43093  jmp         myfunctionIfElse+5Ch (0A4309Ch) 
    }
    else c = 100;
00A43095  mov         dword ptr [c],64h 
        
    return c;
00A4309C  mov         eax,dword ptr [c]
总结:
1、    If判断是使用了cmp 再加上跳转指令;
2、    在开始的地方都有一条无条件跳转指令,跳转到判断的结束处,阻止情面的分支执行结束后,直接进入到这个分支的可能;(黄色的部分)
3、    Else 则是在jmp之后直接执行操作。

2.3.5 switch case

反编译结果:
    int d,e;
    switch(c)
00E93477  mov         eax,dword ptr [c] 
00E9347A  mov         dword ptr [ebp-0E8h],eax 
00E93480  cmp         dword ptr [ebp-0E8h],0 
00E93487  je          myfunctionSwitchCase+44h (0E93494h) 
00E93489  cmp         dword ptr [ebp-0E8h],1 
00E93490  je          myfunctionSwitchCase+55h (0E934A5h) 
00E93492  jmp         myfunctionSwitchCase+64h (0E934B4h) 
    {
    case 0:
        d = c++;
00E93494  mov         eax,dword ptr [c] 
00E93497  mov         dword ptr [d],eax 
00E9349A  mov         ecx,dword ptr [c] 
00E9349D  add         ecx,1 
00E934A0  mov         dword ptr [c],ecx 
        break;
00E934A3  jmp         myfunctionSwitchCase+6Dh (0E934BDh)     //如果有break,那么就多一条jmp
    case 1:
        e = ++c;
00E934A5  mov         eax,dword ptr [c] 
00E934A8  add         eax,1 
00E934AB  mov         dword ptr [c],eax 
00E934AE  mov         ecx,dword ptr [c] 
00E934B1  mov         dword ptr [e],ecx 
    default:
        c = c+1;
00E934B4  mov         eax,dword ptr [c] 
00E934B7  add         eax,1 
00E934BA  mov         dword ptr [c],eax 
    };

    return c;
00E934BD  mov         eax,dword ptr [c]

总结:
1、    switch 的特点是有多个判断,且因为switch 只能用je 做判断;
2、    连续的比较与条件就让人联想到switch;
 

2.3.6 C语言的数组和结构体访问

对应的汇编语句是:
    unsigned char *buf[100];
    mystruct *strs = (mystruct *) buf;
008A34EE  lea         eax,[buf]                     
008A34F4  mov         dword ptr [strs],eax 
    int i;
    for(i =0;i<5;i++)
008A34FA  mov         dword ptr [i],0 
008A3504  jmp         myfunctionStruct+45h (8A3515h) 
008A3506  mov         eax,dword ptr [i] 
008A350C  add         eax,1 
008A350F  mov         dword ptr [i],eax 
008A3515  cmp         dword ptr [i],5 
008A351C  jge         myfunctionStruct+94h (8A3564h) 
    {
        strs[i].a = 0;
008A351E  mov         eax,dword ptr [i]                 //目的是把i*0ch放入到eax中。
008A3524  imul        eax,eax,0Ch                     //i*och 是结构的大小,由编译器生成
008A3527  mov         ecx,dword ptr [strs]             //把strs 的地址(基址)存入到ecx中 
008A352D  mov         dword ptr [ecx+eax],0             //计算得到strs[i]的地址,并赋0
        strs[i].b = 1;
008A3534  mov         eax,dword ptr [i] 
008A353A  imul        eax,eax,0Ch 
008A353D  mov         ecx,dword ptr [strs] 
008A3543  mov         dword ptr [ecx+eax+4],1             //这里增加偏移量获取b的地址;
        strs[i].c = 2;
008A354B  mov         eax,dword ptr [i] 
008A3551  imul        eax,eax,0Ch 
008A3554  mov         ecx,dword ptr [strs] 
008A355A  mov         dword ptr [ecx+eax+8],2 
    }
008A3562  jmp         myfunctionStruct+36h (8A3506h)     //典型的循环结束
    return 0;
008A3564  xor         eax,eax

总结:
1、imul指令让人联想到结构体数组,这是个特征;
    某个元素的起始地址 = 下标*单个元素的长度 + 数组的起始地址;
2、常量很重要;结构体的大小可以通过常量来确定;

2.3.7 共用体和枚举

其实和结构体访问是类似的; 

2.4 函数约定:C函数的参数传递 到汇编

C语言程序通过堆栈把参数从函数外部传入到函数内部;在堆栈中划分区域来容纳函数的内部变量;
使用指令:Push、Pop
7.3.1 C语言的调用方式:堆栈总是调用方把参数反序(从右往左)地压入堆栈中,被调用方把堆栈复原。Call指令和ret指令只是为了调用的方便而已,绝对不是函数存在的绝对证据,即使我们仅仅使用jmp 并自己操作堆栈,一样可以实现函数的功能,一般有三种方式:
_cdecl C调用规则:
1、    参数从右往左进入堆栈;
2、    函数返回后,调用者要负责清除堆栈,所以这种调用方式常会生成较大的可执行程序;
适合可变参数函数:例如printf

_stdcall 又称为WINAPI:
1、    参数从右往左进入堆栈;
2、    函数返回后,被调用者要负责清除堆栈(栈平衡),所以生成 的代码比cdecl 小;
适合参数固定的函数,体积小,速度快;
最后一句一定是:ret 12;返回并在栈区清除12字节;

_fastcal  快速调用方式:….
    是stdcal的一种变种,传递给函数的前两个参数存于寄存器    Ecx、edx  中,剩余的按照stdcal的方式来进行入栈;也是由被调用者负责清除堆栈;

_thiscall    方式(C++的调用方式):…
        与_stdcall大致相同,Ecx 传递this指针;
This指针作为任何非静态成员的第一个隐含参数;

Pascall:
1、    参数从左往右进入堆栈;
2、    函数返回后,被调用者要负责清除堆栈
3、    不支持可变参数的函数调用;
注意:pascal 在Win16里面的规则如上,但是在Win32里面和stdCall调用方式一样;

重要:调用方式在写Hook工具 和 IDA识别约定错误 时候是十分要注意的;

总结注意点:
1、    在windows中,不管哪种调用方式都是返回值放在eax中,然后放返回,外部从eax中获取;
2、    重点观察涉及call、ret、push、pop、ebp、esp 的指令,就能看到C语言函数的调用过程;
3、    定位函数头:必有存储环境,初始化的内容;

 

猜你喜欢

转载自blog.csdn.net/yuqian123455/article/details/82556889