在线咨询
有事点这里
有事点这里
看不懂这篇文章?联系我们
("麦洛克菲"长期致力于内核与移动安全以及编程技术的推广与普及,我们更专业!)
如果想从事IT相关有竞争性的工作,欢迎报考麦洛克菲零基础班
作者:admin 时间:2016-10-31

第四章:汇编与C语句

4.1C语句与汇编

学习了汇编语言之后,就需要将常用的C语言代码结构与相应的汇编语言联系起来。这样就可以在分析汇编语言的时候,明白它的意思。C语言中函数过程的调用,循环语句,条件语句,结构体与数组的访问都有对应的特定的汇编语言结构。下面来分别分析这些特定的结构。

有些在C层面不好理解的代码,只要分析下它的汇编,就很容易明白其中的原理了,比如之前学习的函数的调用约定,函数的传参等。

4.1.1循环语句(for,while,do-while)与汇编

For循环语句的汇编代码:

C代码:

for(int i=0;i<10;i++)

{

}

汇编代码:

i=0;

jmp A;

B:

i++

A:

cmp i, 0ah

jge OUT;

.....

jmp B;

 

OUT:

While循环语句的汇编代码:

while(i<10)

{

       i++;

}

A:

cmp i,0ah

jge OUT;

......

i++;

 

jmp A;

OUT:

4.1.2if语句与汇编

if-else语句汇编代码:

if(i>0)

{

   

}

else

{

   

}

汇编代码:

cmp i,0

jle ELSE;

....

jmp OUT;

 

 

ELSE:

....

 

OUT:

Switch语句的汇编代码:

switch(value)

{

case 1:

   

    break;

case 2:

   

    break;

default:

 

}

switch:

cmp value, 1

je A;

cmp value, 2

je B;

jmp DEFAULT;

A:

...

jmp OUT

B:

...

jmp OUT

DEFAULT:

...

OUT:

4.1.3数组访问与汇编

数组访问的汇编代码如下:

 

int g_a[100]={100,99,98,97,0};

      

for(int i = 0; i<100;i++)

0093149E  mov         dword ptr [i],0

009314A5  jmp         wmain+30h (9314B0h)

009314A7  mov         eax,dword ptr [i]

009314AA  add         eax,1

009314AD  mov         dword ptr [i],eax

009314B0  cmp         dword ptr [i],64h

009314B4  jge         wmain+46h (9314C6h)

{

              g_a[i]=10;

009314B6  mov         eax,dword ptr [i]

009314B9  mov         dword ptr g_a (937000h)[eax*4],0Ah //数组首地址+i*4

}

4.1.4结构体访问与汇编

结构体中成员访问汇编代码如下:

typedef struct _S

{

       int val;

       char ch;

}S,*PS;

S g_s1;

 

g_s1.val=100;

0030149E  mov         dword ptr [g_s1 (307344h)],64h //结构体首地址+偏移量(0

 

g_s1.ch='A';

003014A8  mov         byte ptr [g_s1+4 (307348h)],41h//结构体首地址+偏移量(4)

4.1.5结构体数组与汇编

结构体数组成员访问汇编代码如下:

typedef struct _S

{

       int val;

       char ch;

}S,*PS;

S g_as[100]={{0}};

 

for(int i = 0;i<100;i++)

00FA149E  mov         dword ptr [i],0

00FA14A5  jmp         wmain+30h (0FA14B0h)

00FA14A7  mov         eax,dword ptr [i]

00FA14AA  add         eax,1

00FA14AD  mov         dword ptr [i],eax

00FA14B0  cmp         dword ptr [i],64h

00FA14B4  jge         wmain+51h (0FA14D1h)

{

              g_as[i].val=100;

00FA14B6  mov         eax,dword ptr [i]

00FA14B9  mov         dword ptr g_as (0FA7328h)[eax*8],64h //数组首地址+i*8+偏移(0

              g_as[i].ch='Z';

00FA14C4  mov         eax,dword ptr [i]

00FA14C7  mov         byte ptr g_as+4 (0FA732Ch)[eax*8],5Ah //数组首地址+i*8+偏移(4)

}

4.1.6 i++与汇编

C语句中的i++对应的汇编语句如下:

int i = 0;

00FC14D4 mov dword ptr [i],0

i++;

00FC14DB mov eax,dword ptr [i]

00FC14DE add eax,1

00FC14E1 mov dword ptr [i],eax

 

可见,对于在C语言中的一条语句,在汇编层也有可能是多条汇编指令组成。因此,即使在单核多线程环境下,i++也不是多线程安全的,因为它不是原子操作,因为一个线程执行了i++某一条汇编指令,CPU的时间片就有可能用完了,发生了切换,而另外一个线程切换进来之后,又开始从头开始执行i++的汇编指令,因此造成多线程不一致性。

 

因此,要保证i++语句的多线程安全,要么使用锁机制,要么使用原子操作:比如Windows平台的InterLockedIncrement()函数或者Linux平台的atomic_t类型以及相关的操作。

4.2函数调用与汇编

函数的调用约定(calling convention)有cdecl,standard,fastcall等几种形式。在函数调用的时候,参数从右往左依次入栈,然后是返回地址(eip寄存器中的值,指向下一条要执行的指令)入栈,ebp入栈,ebp指向esp(程序的release版本可能因为优化而没有ebp寄存器入栈),esp向上移动(与内存的方向相反,减去一个数值),分配一个局部空间,容纳局部变量。

栈上第一个参数的位置:ebp+8;第二个参数的位置:ebp+c;第一个局部变量的位置:ebp-4,第二个局部变量的位置:ebp-8,依次类推。

下面定义一个函数myFunc(),然后在main()函数中调用,依次为例来分析在函数调用过程中的汇编代码:

int myFunc(int a, int b)

{

     int c = a + b;

     return c;

}

int main(void)

{

     int a = 0;

     int b = 1;

     myFunc(a, b);

     return 0;

}

 

调试版汇编代码:

1)  调用者:

 

mov         dword ptr [ebp-4],0      //[ebp-4]即为aa = 0

mov         dword ptr [ebp-8],1      //[ebp-8]即为bb = 1

mov         eax,dword ptr [ebp-8]         // b入栈

push         eax

mov         ecx,dword ptr [ebp-4]         // a入栈

push         ecx

call          myFunc                      // 调用函数

add          esp,8                   // 恢复堆栈

 

2)被调用者:

 

push        ebp                           //ebp,ebx,esi,edi等入栈

mov         ebp,esp

sub         esp,44h

push        ebx

push        esi

push        edi

lea         edi,[ebp-44h]

mov         ecx,11h

//0xCC是调试中断(__asm int 3)的指令码,所以

//当程序错误的跳转到这个区域进行执行时将产

//生调试中断

mov          eax,0CCCCCCCCh

rep stos     dword ptr [edi]

mov          eax,dword ptr [ebp+8]        //获取参数aebp+8的位置为第一个参数

add          eax,dword ptr [ebp+0Ch]      //获取参数b并与a相加

mov          dword ptr [ebp-4],eax        //放入局部变量c

mov          eax,dword ptr [ebp-4]        //c作为返回值放如eax

pop          edi                          //恢复堆栈

pop          esi

pop          ebx

mov          esp,ebp

pop          ebp

ret

 

Release版:

1)调用者

 

push          1

push          0

call          MyFunc

add           esp,8

 

2)被调用者

 

mov           eax, dword ptr ss:[esp+8]

mov           ecx, dword ptr ss:[esp+4]

add           eax, ecx

ret

函数调用的汇编代码,在不同的调用约定下,会略有不同。因为与参数入栈的方式和栈平衡由谁来负责相关。

4.2.1cdecl调用汇编

调用约定cdecl又称为C调用约定,是C语言缺省的调用约定,它的定义语法是:

int func (int x ,int y)            //默认的C调用约定

int __cdecl func (int x,int y)  //明确指出C调用约定

该调用约定遵循下面的规则:

参数入栈顺序:从右到左

还原栈者:调用者修改栈

函数名:前加下划线:_func

由于每次函数调用都要由编译器产生还原栈的代码,所以使用__cdecl方式编译的程序比使用__stdcall方式编译的程序要大很多,但是 __cdecl调用方式是由函数调用者负责清除栈中的函数参数,所以这种方式支持可变参数,比如printf()WindowsAPI wsprintf()就是__cdecl调用方式。

由于参数按照从右向左顺序压栈,因此最开始的参数在最接近栈顶的位置,因此当采用不定个数参数时,第一个参数在栈中的位置肯定能知道,只要不定的参数个数能够根据第一个后者后续的明确的参数确定下来,就可以使用不定参数了。

int __cdecl func2(int x, int y)
{
    return x+y;
}

被调用者汇编代码:

int __cdecl func2(int x, int y)//采用cdecl调用约定 
{
0042D680 push ebp //ebp
入栈
0042D681 mov ebp,esp //ebp
指向esp
0042D683 sub esp,0C0h//esp
增长0xC0个空间
0042D689 push ebx //ebx,esi,edi
寄存器入栈
0042D68A push esi 
0042D68B push edi
0042D68C lea edi,[ebp-0C0h]// [ebp-0C0h]
即栈顶,将cccccccc拷贝到栈的局部空间中
0042D692 mov ecx,30h 
0042D697 mov eax,0CCCCCCCCh 
0040042D69C rep stos dword ptr es:[edi]//
拷贝开始
    return x+y;
0042D69E mov eax,dword ptr [x] 
0042D6A1 add eax,dword ptr [y] 
}
0042D6A4 pop edi//edi,esi,ebx
出栈
0042D6A5 pop esi
0042D6A6 pop ebx 
0042D6A7 mov esp,ebp//esp
收缩空间
0042D6A9 pop ebp//ebp
出栈
00000042D6AA ret//
被调用者直接返回,不用恢复栈平衡,由调用者负责

 

调用代码:

func2(1, 2);//采用cdecl调用约定,参数从右往左依次入栈,调用者负责栈平衡 
//0042D737 push 2//
参数从右往左依次入栈,2入栈 
//0042D739 push 1//
参数从右往左依次入栈,1入栈 
//0042D73B call func2 (42B3FCh)
//0042D740 add esp,8 //
调用者负责栈平衡,esp+8,等于2个入栈参数的长度

4.2.2stdcall调用汇编

stdcall调用约定声明的格式:

int __stdcall func(int x,int y)

stdcall的调用约定意味着:

参数入栈规则:参数从右向左压入栈

还原栈者:被调用函数自身修改栈

函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。在微软WindowsC/C++编译器中,常常用Pascal宏来声明这个调用约定,类似的宏还有WINAPICALLBACK

int __stdcall func1(int x, int y)
{
    return x+y;
}

//被调用者代码:

int __stdcall func1(int x, int y)//采用stdcall
{
42D640 push ebp 
0042D641 mov ebp,esp 
0042D643 sub esp,0C0h 
0042D649 push ebx 
0042D64A push esi
0042D64B push edi
0042D64C lea edi,[ebp-0C0h] 
0042D652 mov ecx,30h 
0042D657 mov eax,0CCCCCCCCh 
0042D65C rep stos dword ptr es:[edi] 
   return x+y;
0042D65E mov eax,dword ptr [x] 
0042D661 add eax,dword ptr [y]

} 
0042D664 pop edi 
0042D665 pop esi 
0042D666 pop ebx 
0042D667 mov esp,ebp //ebp
(调用前的栈顶)放入esp中,然后出栈,恢复老ebp
0042D669 pop ebp 
0042D66A ret 8 //
被调用者负责栈平衡,ret 8,esp += 8; 

 

//调用者代码

func1(1, 2); //采用stdcall,参数从右往左依次入栈,被调用者负责栈平衡
//0042D72E push 2 //
参数从右往左依次入栈,2入栈
//0042D730 push 1 //
参数从右往左依次入栈,1入栈
//0042D732 call func1 (42B6F4h)

4.2.3fastcall调用汇编

fastcall的声明语法为:

int fastcall func (int x,int y)

该调用约定遵循下面的规则:

参数入栈顺序:函数的第一个和第二个参数通过ecxedx传递,剩余参数从右到左入栈

还原栈者:被调用者修改栈

函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。以fastcall声明执行的函数,具有较快的执行速度,因为部分参数通过寄存器来进行传递的。

int __fastcall func3(int x, int y, int z)
{
    return x+y+z;
}

被调用者汇编代码:

int __fastcall func3(int x, int y, int z)//采用fastcall调用约定 
{
0042D6C0 push ebp 
0042D6C1 mov ebp,esp 
0042D6C3 sub esp,0D8h 
0042D6C9 push ebx 
0042D6CA push esi 
0042D6CB push edi
0042D6CC push ecx 
0042D6CD lea edi,[ebp-0D8h] 
0042D6D3 mov ecx,36h 
0042D6D8 mov eax,0CCCCCCCCh
0042D6DD rep stos dword ptr es:[edi] 
0042D6DF pop ecx 
0042D6E0 mov dword ptr [ebp-14h],edx //
2个参数放在了ecxedx
0040042D6E3 mov dword ptr [ebp-8],ecx//
2个参数放在了ecxedx
    return x+y+z;
0042D6E6 mov eax,dword ptr [x] 
0042D6E9 add eax,dword ptr [y] 
0042D6EC add eax,dword ptr [z] 
}
0042D6EF pop edi 
0042D6F0 pop esi
0042D6F1 pop ebx 
0042D6F2 mov esp,ebp 
0042D6F4 pop ebp 
0040042D6F5 ret 4 //
3个参数占4个字节,从栈上传递,所以栈平衡是弹出4个字节

 

调用者代码:

func3(1, 2, 3);//采用fastcall,前2个参数依次放入ecxedx寄存器,剩余参数从右往左依次入栈,被调用者负责栈平衡
//0042D743 push 3 //
剩余参数从右往左依次入栈,3入栈
//0042D745 mov edx,2 //
2个参数,分别送往ecxedx寄存器,2edx
//0042D74A mov ecx,1 //
2个参数,分别送往ecxedx寄存器,1ecx
//0042D74F call func3 (42B023h)23h)


x64默认的调用约定:fastcall

 

1,一个函数在调用时,前四个参数是从左至右依次存放于RCXRDXR8R9寄存器里面,剩下的参数从右至左顺序入栈;栈的增长方向为从高地址到低地址。

2,浮点前4个参数传入XMM0XMM1XMM2 XMM3 中。其他参数传递到堆栈中。

3,调用者负责在栈上分配32字节的“shadow space”,用于存放那四个存放调用参数的寄存器的值(亦即前四个调用参数);小于64(bit)的参数传递时高位并不填充零(例如只传递ecx),大于64位需要按照地址传递;

4,调用者负责栈平衡;

5,被调用函数的返回值是整数时,则返回值会被存放于RAX;浮点数返回在xmm0中

6,RAXRCXRDXR8R9R10R11是“易挥发”的,不用特别保护(所谓保护就是使用前要push备份),其余寄存器需要保护。(x86下只有eax, ecx, edx是易挥发的)

7,栈需要16字节对齐,“call”指令会入栈一个8字节的返回值(注:即函数调用前原来的RIP指令寄存器的值),这样一来,栈就对不齐了(因为RCXRDXR8R9四个寄存器刚好是32个字节,是16字节对齐的,现在多出来了8个字节)。所以,所有非叶子结点调用的函数,都必须调整栈RSP的地址为16n+8,来使栈对齐。比如sub rsp,28h

8,对于 R8R15 寄存器,我们可以使用 r8, r8d, r8w, r8b 分别代表 r8 寄存器的64位、低32位、低16位和低8位。

 

                                                                               x64 fastcall调用栈示意图

4.3 函数传参与汇编

  C语言中调用函数的时候,将实参数据传递给函数的方式,可以分为传值,传指针,以及C++中的传引用。这里所谓的传值,传指针和传引用都是针对实参来说的,也就是传实参的值,实参的指针,实参的引用。

4.3.1 传值

传值无法改变实参的值,因为传值的情况下,存放在栈上的形参只是实参值的一个拷贝,无法改变实参。

 

void func1(int x)

{

    x=1;

}

int a=0;

func1(a);

010414A5  mov       eax,dword ptr [a] //a的值放入eax中,然后把eax入栈

010414A8  push       eax

010414A9  call        func1 (10411D6h)

010414AE  add        esp,4

4.3.2 传地址

下面是传指针的函数定义方式,形参x是一个指向整数类型数据的地址。在函数内部通过*运算符来引用实参。

void func2(int *x)

{

    *x=2;

}

 

int a=0;

func2(&a);

010414B1  lea         eax,[a] //a的地址传入eax中,然后把eax入栈

010414B4  push        eax 

010414B5  call         func2 (104100Ah)

010414BA  add         esp,4

4.3.3 传引用

下面是在C++中的传引用调用方式,注意,形参部分使用的是&,而在函数内部,可以直接把形参当做实参来使用,此时形参就是对实参的一个引用。传引用实际上也是传的实参的地址,是可以直接修改实参的。下面是在main函数里直接通过传引用的方式调用函数的方式:

void func3(int &x)

{

    x=3;

}

 

int a=0;

func3(a);

010414BD  lea        eax,[a] //在汇编层与传指针的方法完全一样

010414C0  push       eax 

010414C1  call        func3 (104102Dh)

010414C6  add        esp,4

4.4反汇编引擎

    反汇编引擎就是将二进制程序代码还原成汇编代码的这样一种工具。因为程序编译完成之后,是以二进制存在的。二进制犹如天书,很难读懂。因此,可以使用反汇编引擎将二进制还原成可以阅读的汇编指令。在实际的HOOK,逆向,调试等方面,会经常使用到反汇编引擎。常见的反汇编引擎有:

XDE

BeaEngine(X32/x64)

Libdasm

下面这段代码,就是调用XDE反汇编引擎中的函数xde_disasm()将指针p所指内存中的二进制代码进行反汇编,还原成汇编指令的过程:

struct xde_instr instr;

while (totalLength < 5)

{

              length = xde_disasm(p+ totalLength, &instr);

              if (length == 0)

              {

                     return STATUS_UNSUCCESSFUL;

              }

              totalLength += length;

}

它的作用就是在p所指的位置,找到连续的汇编指令,使得这些指令长度之和大于等于5个字节,以便能容纳一条jmp 指令(5个字节),从而实现HOOK的指令替换。