C语言内联汇编和调用协定
前面我们通过分析反汇编分析了C语言,现在我们来探究如何在C语言里直接自写汇编函数
这里就要引入C语言中裸函数的概念
裸函数
声明裸函数
裸函数与普通函数的区别在于在函数前多声明了
__declspec (naked)
以此来表面该函数是一个裸函数
裸函数作用
要讲裸函数的作用,就不得不提到裸函数与普通函数的区别
裸函数与普通函数区别
前面反汇编C语言的笔记里,我们可以得知一个普通空函数的反汇编代码并不少,保护现场、恢复现场等等都有,那么这些反汇编代码是如何产生的呢?
答案是:编译器
编译器会为我们产生这些反汇编代码
相比之下,只要普通函数加上裸函数前缀转化为裸函数,编译器就会知道这个函数无需额外生成上面所说的保护现场、恢复现场等反汇编代码,函数执行所需的反汇编代码由我们自己来实现
于是裸函数的作用呼之欲出:
当我们不希望编译器为我们生成函数里的汇编代码,而是想要自己实现函数内部的汇编代码时,就可以使用裸函数来告诉编译器不要去额外生成汇编代码
最简单的裸函数
void __declspec (naked) function(){
__asm{
ret
}
}
上面是一个最简单的裸函数,反汇编代码只有一行ret
与普通空函数相比,同样是什么都没做,但却要加上ret,为什么?
我们可以来看看这个最简单的裸函数的执行流程,并注意与普通空函数对比
分析最简单的裸函数
完整代码
先给出完整的代码
#include "stdafx.h"
void __declspec (naked) function(){
__asm{
ret
}
}
int main(int argc, char* argv[])
{
function();
return 0;
}
接下来进入反汇编的世界
函数外部
13: function();
00401078 call @ILT+10(function) (0040100f)
14: return 0;
0040107D xor eax,eax
15: }
0040107F pop edi
00401080 pop esi
00401081 pop ebx
00401082 add esp,40h
00401085 cmp ebp,esp
00401087 call __chkesp (004010e0)
0040108C mov esp,ebp
0040108E pop ebp
0040108F ret
函数内部
6: void __declspec (naked) function(){
00401030 ret
开始分析
00401078 call @ILT+10(function) (0040100f)
我们可以发现裸函数的调用和普通函数的调用并没有什么区别,都是call 函数地址
但接着进入函数内部就可以看到
00401030 ret
函数的内部有且仅有我们代码中用__asm所写的ret语句,没有任何其余的代码
执行完ret语句后,函数正常返回,接下来就和普通的空函数没有区别了
得出结论
于是我们可以知道,裸函数的内部汇编代码完全由我们自己来实现,之所以要写上一个ret也是为了让函数能够正常地返回
再来看看不添加ret语句时的反汇编代码情况
我们将裸函数内部的__asm删除
void __declspec (naked) function(){
}
然后再观察函数内部的汇编代码
我们可以看到函数内部为一堆 int 3,也就是CC即初始化堆栈的内容
程序执行到这里就会产生中断,无法正常执行,所以我们才要加上一条汇编ret语句,让函数能够正常执行返回
大致了解了裸函数后,我们就来使用内联汇编自己实现加法函数
内联汇编实现加法函数
自写加法函数
#include "stdafx.h"
int __declspec (naked) Plus(int x,int y){
__asm{
//保留调用前堆栈
push ebp
//提升堆栈
mov ebp,esp
sub esp,0x40
//保护现场
push ebx
push esi
push edi
//初始化提升的堆栈,填充缓冲区
mov eax,0xCCCCCCCC
mov ecx,0x10
lea edi,dword ptr ds:[ebp-0x40]
rep stosd
//函数核心功能
//取出参数
mov eax,dword ptr ds:[ebp+8]
//参数相加
add eax,dword ptr ds:[ebp+0xC]
//恢复现场
pop edi
pop esi
pop ebx
//降低堆栈
mov esp,ebp
pop ebp
//返回
ret
}
}
int main(int argc, char* argv[])
{
Plus(1,2);
return 0;
}
不难发现,其实我们自己实现的加法函数就是模拟了编译器为我们做的事情,此时进到函数内部也会看到
函数内部
6: int __declspec (naked) Plus(int x,int y){
00401030 push ebp
7: __asm{
8: //保留调用前堆栈
9: push ebp
10: //提升堆栈
11: mov ebp,esp
00401031 mov ebp,esp
12: sub esp,0x40
00401033 sub esp,40h
13: //保护现场
14: push ebx
00401036 push ebx
15: push esi
00401037 push esi
16: push edi
00401038 push edi
17: //初始化提升的堆栈,填充缓冲区
18: mov eax,0xCCCCCCCC
00401039 mov eax,0CCCCCCCCh
19: mov ecx,0x10
0040103E mov ecx,10h
20: lea edi,dword ptr ds:[ebp-0x40]
00401043 lea edi,ds:[ebp-40h]
21: rep stosd
00401047 rep stos dword ptr [edi]
22: //函数核心功能
23:
24: //取出参数
25: mov eax,dword ptr ds:[ebp+8]
00401049 mov eax,dword ptr ds:[ebp+8]
26: //参数相加
27: add eax,dword ptr ds:[ebp+0xC]
0040104D add eax,dword ptr ds:[ebp+0Ch]
28:
29:
30: //恢复现场
31: pop edi
00401051 pop edi
32: pop esi
00401052 pop esi
33: pop esi
00401053 pop esi
34:
35: //降低堆栈
36: mov esp,ebp
00401054 mov esp,ebp
37: pop ebp
00401056 pop ebp
38:
39: //返回
40: ret
执行的就是我们自己所写的代码,而非编译器所生成的,并且也能够实现加法函数的功能
函数返回后
我们可以发现函数返回后和普通函数并无差异
00401081 add esp,8
都有这一行平衡堆栈的语句,也就是堆栈外平衡,但如果我们想要在函数内部就平衡堆栈,也就是实现堆栈内平衡,也就是希望函数返回后没有这个外部的堆栈平衡语句,让堆栈的平衡工作由我们自己来处理,该如何做到?
这里就要引入C语言的调用协定这个概念了
调用协定
常见的几种调用协定:
调用协定 |
参数压栈顺序 |
平衡堆栈 |
__cdecl |
从右至左入栈 |
调用者清理栈 |
__stdcall |
从右至左入栈 |
自身清理堆栈 |
__fastcall |
ECX/EDX传送前两个 剩下:从右至左入栈 |
自身清理堆栈 |
其中__cdecl为C语言默认调用协定
接下来我们来比较一下这三种调用协定
int __cdecl Plus1(int x,int y){
return x+y;
}
int __stdcall Plus2(int x,int y){
return x+y;
}
int __fastcall Plus3(int x,int y){
return x+y;
}
同样都是一个简单的加法函数,分别采用了三种不同的调用协定,我们来用汇编来一察他们的区别
__cdecl
首先是我们最熟悉的__cdecl协定,和我们上一个笔记分析的简单加法函数不无区别
观察反汇编:
函数外部
函数内部
我们这里主要是关注三个地方:参数压栈、函数返回值、返回后执行语句
参数压栈:先push 2再 push 1
函数返回值:ret
返回后执行语句:add esp,8
__stdcall
函数外部
函数内部
接着关注三个地方:参数压栈、函数返回值、返回后执行语句
参数压栈:先push 2再 push 1
函数返回值:ret 8
返回后执行语句:xor eax,eax
__fastcall
函数外部
函数内部
依旧关注三个地方:参数压栈、函数返回值、返回后执行语句
参数压栈:先move edx,2再mov ecx,1
函数返回值:ret
返回后执行语句:xor eax,eax
对比三种协定
|
__cdecl |
__stdcall |
__fastcall |
参数压栈 |
push 2 push1 |
push2 push1 |
mov edx,2 mov ecx,1 |
函数返回值 |
ret |
ret 8 |
ret |
返回后执行语句 |
add esp,8 |
xor eax,eax |
xor eax,eax |
我们可以得出结论:
__cdecl是将参数压入栈中,然后在函数执行返回后再平衡堆栈,也就是堆栈外平衡
__stdcall也是将参数压入栈中,但是是在函数内部通过ret xxx来平衡堆栈,也就是堆栈内平衡
__fastcall则是在参数个数小于等于2时直接使用edx和ecx作为参数传递的载体,没用使用到堆栈,自然也就无须平衡堆栈,但是当参数个数大于2时,则多出来的那几个参数则按stdcall的方式来处理,也是采用堆栈内平衡
接下来再谈谈__stdcall中返回值的问题
我们可以看到,我们在上面的加法函数中push了两个立即数2和1,返回值是8
这是不是意味着ret xxxx中xxxx=参数个数*4?
并不是!!!这里ret xxxx里的xxxx和压入参数的数据宽度有关
我们这里压入的两个立即数的数据宽度都是4个字节=32bit,因此我们这里是ret 4+4=8
如果改成push ax,也就是压入2个字节=16bit时则应该ret 2
这里可以参考逆向基础笔记四 堆栈篇中堆栈相关汇编指令的push指令
了解了以上调用协定后,我们就可以修改之前的简单加法裸函数,将其改为堆栈内平衡
堆栈内平衡加法函数
__declspec (naked) __stdcall int Plus(int x,int y){
__asm{
//保留调用前堆栈
push ebp
//提升堆栈
mov ebp,esp
sub esp,0x40
//保护现场
push ebx
push esi
push edi
//初始化提升的堆栈,填充缓冲区
mov eax,0xCCCCCCCC
mov ecx,0x10
lea edi,dword ptr ds:[ebp-0x40]
rep stosd
//函数核心功能
//取出参数
mov eax,dword ptr ds:[ebp+8]
//参数相加
add eax,dword ptr ds:[ebp+0xC]
//恢复现场
pop edi
pop esi
pop ebx
//降低堆栈
mov esp,ebp
pop ebp
//返回
ret 8
}
}
int main(int argc, char* argv[])
{
Plus(1,2);
return 0;
}
与前面相比,修改ret 为ret 8,自己在函数内实现了堆栈内平衡