学习了汇编语言之后,就需要将常用的C语言代码结构与相应的汇编语言联系起来。这样就可以在分析汇编语言的时候,明白它的意思。C语言中函数过程的调用,循环语句,条件语句,结构体与数组的访问都有对应的特定的汇编语言结构。下面来分别分析这些特定的结构。
有些在C层面不好理解的代码,只要分析下它的汇编,就很容易明白其中的原理了,比如之前学习的函数的调用约定,函数的传参等。
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:
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:
数组访问的汇编代码如下:
int g_a[100]={100,99,98,97,0};
for(int i = 0; i<100;i++)
0093149E mov
dword ptr [i],0
009314AA add eax,1
009314AD mov dword ptr [i],eax
009314B0 cmp dword ptr [i],64h
009314B4 jge
wmain+46h (
{
g_a[i]=10;
009314B6 mov eax,dword ptr [i]
009314B9 mov dword ptr g_a (937000h)[eax*4],0Ah //数组首地址+i*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';
结构体数组成员访问汇编代码如下:
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
00FA
00FA
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';
00FA
00FA
}
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类型以及相关的操作。
函数的调用约定(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]即为a,a = 0
mov
dword ptr [ebp-8],1 //[ebp-8]即为b,b = 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] //获取参数a,ebp+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
函数调用的汇编代码,在不同的调用约定下,会略有不同。因为与参数入栈的方式和栈平衡由谁来负责相关。
调用约定cdecl又称为C调用约定,是C语言缺省的调用约定,它的定义语法是:
int func (int x ,int y)
//默认的C调用约定
int __cdecl func (int x,int y) //明确指出C调用约定
该调用约定遵循下面的规则:
参数入栈顺序:从右到左
还原栈者:调用者修改栈
函数名:前加下划线:_func
由于每次函数调用都要由编译器产生还原栈的代码,所以使用__cdecl方式编译的程序比使用__stdcall方式编译的程序要大很多,但是
__cdecl调用方式是由函数调用者负责清除栈中的函数参数,所以这种方式支持可变参数,比如printf()和Windows的API 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,
0042D689 push ebx //ebx,esi,edi寄存器入栈
0042D
0042D68B push edi
0042D
0042D692 mov ecx,30h
0042D697 mov eax,0CCCCCCCCh
0040042D
return x+y;
0042D69E mov eax,dword ptr [x]
0042D
}
0042D
0042D
0042D
0042D
0042D
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个入栈参数的长度
stdcall调用约定声明的格式:
int __stdcall func(int x,int y)
stdcall的调用约定意味着:
参数入栈规则:参数从右向左压入栈
还原栈者:被调用函数自身修改栈
函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。在微软Windows的C/C++编译器中,常常用Pascal宏来声明这个调用约定,类似的宏还有WINAPI和CALLBACK。
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,
0042D649 push ebx
0042D
0042D64B push edi
0042D
0042D652 mov ecx,30h
0042D657 mov eax,0CCCCCCCCh
0042D
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
0042D
//调用者代码
func1(1, 2); //采用stdcall,参数从右往左依次入栈,被调用者负责栈平衡
//0042D72E push 2 //参数从右往左依次入栈,2入栈
//0042D730 push 1 //参数从右往左依次入栈,1入栈
//0042D732 call func1 (42B
fastcall的声明语法为:
int fastcall func (int x,int y)
该调用约定遵循下面的规则:
参数入栈顺序:函数的第一个和第二个参数通过ecx和edx传递,剩余参数从右到左入栈
还原栈者:被调用者修改栈
函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。以fastcall声明执行的函数,具有较快的执行速度,因为部分参数通过寄存器来进行传递的。
int __fastcall func3(int x, int y, int z)
{
return x+y+z;
}
被调用者汇编代码:
int __fastcall func3(int x, int y, int z)//采用fastcall调用约定
{
0042D
0042D
0042D
0042D
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个参数放在了ecx和edx中
0040042D6E3 mov dword ptr [ebp-8],ecx//前2个参数放在了ecx和edx中
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
0042D
0042D
0042D
0042D
0040042D
调用者代码:
func3(1, 2, 3);//采用fastcall,前2个参数依次放入ecx和edx寄存器,剩余参数从右往左依次入栈,被调用者负责栈平衡
//0042D743 push 3 //剩余参数从右往左依次入栈,3入栈
//0042D745 mov edx,2 //前2个参数,分别送往ecx和edx寄存器,2入edx
//0042D
//0042D
x64默认的调用约定:fastcall
1,一个函数在调用时,前四个参数是从左至右依次存放于RCX、RDX、R8、R9寄存器里面,剩下的参数从右至左顺序入栈;
2,浮点前4个参数传入XMM0、XMM1、XMM2 和
XMM3 中。其他参数传递到堆栈中。
3,调用者负责在栈上分配32字节的“shadow space”,用于存放那四个存放调用参数的寄存器的值(亦即前四个调用参数);小于64位(bit)的参数传递时高位并不填充零(例如只传递ecx),大于64位需要按照地址传递;
4,调用者负责栈平衡;
6,RAX,RCX,RDX,R8,R9,R10,R11是“易挥发”的,不用特别保护(所谓保护就是使用前要push备份),其余寄存器需要保护。(x86下只有eax, ecx, edx是易挥发的)
7,栈需要16字节对齐,“call”指令会入栈一个8字节的返回值(注:即函数调用前原来的RIP指令寄存器的值),这样一来,栈就对不齐了(因为RCX、RDX、R8、R9四个寄存器刚好是32个字节,是16字节对齐的,现在多出来了8个字节)。所以,所有非叶子结点调用的函数,都必须调整栈RSP的地址为16n+8,来使栈对齐。比如sub
rsp,28h
8,对于 R8~R15 寄存器,我们可以使用 r8, r8d, r8w, r8b 分别代表 r8 寄存器的64位、低32位、低16位和低8位。
在C语言中调用函数的时候,将实参数据传递给函数的方式,可以分为传值,传指针,以及C++中的传引用。这里所谓的传值,传指针和传引用都是针对实参来说的,也就是传实参的值,实参的指针,实参的引用。
传值无法改变实参的值,因为传值的情况下,存放在栈上的形参只是实参值的一个拷贝,无法改变实参。
void func1(int x)
{
x=1;
}
int a=0;
func1(a);
010414AE add esp,4
下面是传指针的函数定义方式,形参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
下面是在C++中的传引用调用方式,注意,形参部分使用的是&,而在函数内部,可以直接把形参当做实参来使用,此时形参就是对实参的一个引用。传引用实际上也是传的实参的地址,是可以直接修改实参的。下面是在main函数里直接通过传引用的方式调用函数的方式:
void func3(int &x)
{
x=3;
}
int a=0;
func3(a);
010414BD lea eax,[a] //在汇编层与传指针的方法完全一样
反汇编引擎就是将二进制程序代码还原成汇编代码的这样一种工具。因为程序编译完成之后,是以二进制存在的。二进制犹如天书,很难读懂。因此,可以使用反汇编引擎将二进制还原成可以阅读的汇编指令。在实际的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的指令替换。
Copyright 2011-2020 © MallocFree. All rights reserved.