0%

手动编写C函数的汇编代码

在前面的文章里已经清楚计算机是只认识0和1的,那平时编写的程序到运行中间又经历了什么?

这个过程用下面一张图就足以说明所有的问题了

稍微解释一下其中的一些含义

目标文件和可执行文件都是由机器语言指令组成的
目标文件只包含你写的代码所翻译的机器语言代码
可执行文件还包含你写的代码中使用的库函数和启动代码的机器语言代码(启动代码充当着程序和操作系统之间的接口)

编译器到底生成了什么

多说无益,这里用一个空白的C语言函数来看看编译器生成了哪些东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

void test() {
00ED1E70 push ebp
00ED1E71 mov ebp,esp
00ED1E73 sub esp,0C0h
00ED1E79 push ebx
00ED1E7A push esi
00ED1E7B push edi
00ED1E7C lea edi,[ebp-0C0h]
00ED1E82 mov ecx,30h
00ED1E87 mov eax,0CCCCCCCCh
00ED1E8C rep stos dword ptr es:[edi]
00ED1E8E mov ecx,offset [email protected] (0EDC000h)
00ED1E93 call @[email protected] (0ED1208h)

}
00ED1E98 pop edi
00ED1E99 pop esi

}
00ED1E9A pop ebx
00ED1E9B add esp,0C0h
00ED1EA1 cmp ebp,esp
00ED1EA3 call __RTC_CheckEsp (0ED1212h)
00ED1EA8 mov esp,ebp
00ED1EAA pop ebp
00ED1EAB ret

中间的检查堆栈平衡等函数我们可以省略,仔细看看其中的汇编代码,很容易可以看出这其中所进行的操作就是上一篇文章所画的堆栈图,堆栈图也是后面进行分析的关键,手写这段程序的代码也是一件很重要的事情,如果所有的操作都交给编译器去做,那你所有的操作就都是很明确的,又怎么去跟别人进行对抗。

手动编写

这里就需要引入裸函数的概念了,裸函数就是编译器不帮你生成一行代码,所有的代码都必须你自己去手动编写

1
2
3
void __declspec(naked) Function(){

}

在正常情况下,我们写一个空函数是不会出现报错的情况的,但是裸函数则不然,直接用上面的方式写,会跳到一个程序不认识的地方,如果对上一篇文章的堆栈图足够了解,就会知道造成这个情况的原因是什么。

这是因为函数在汇编语言中是通过call来调用的,这个操作包含了两个步骤,一步是把下一条指令的地址push到堆栈中,一步是跳转到函数所要执行的地址,如果是一个空函数,它会再跳回到call指令的下一条地址,但是裸函数不会,因为编译器没有给我们生成任何一条指令,所以要想让一个空的裸函数正常运行, 就需要我们手动添加一段指令,让程序回到原来要执行的位置,那就是添加ret指令,所以可以运行的空的裸函数如下

1
2
3
4
5
6
7
void __declspec(naked) Function()
{
__asm
{
ret
}
}

对于手动编写要特别注意对于相关数据的调用,需要明确它们所处的位置在哪里,为了把所有的情况都包含在内,用了下面的这个例子作为说明

1
2
3
4
5
6
int plus(int x, int y, int z) {
int a = 1;
int b = 2;
int c = 3;
return x + y + z + a + b + c;
}

其中x、y、z和a、b、c在内存中所存在的位置是完全不同的,想要分清楚这个内容,上一篇文章的堆栈图就特别的关键了,不清楚的去看上一篇文章的说明。

下面直接给出最终的代码,跟编译器所生成的肯定是有差别的,但是在功能实现方面已经足够了,想要看懂其中的含义,堆栈图是必须的,堆栈图是必须的,堆栈图是必须的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

int plus(int x, int y, int z) {
int a = 1;
int b = 2;
int c = 3;
return x + y + z + a + b + c;
}

int __declspec(naked) plus1(int x, int y, int z) {
__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 dword ptr ds:[ebp-0x8],1
mov dword ptr ds:[ebp-0xC],2
mov dword ptr ds:[ebp-0x10],3

mov eax,dword ptr ds:[ebp+0x8]
add eax,dword ptr ds:[ebp+0xC]
add eax,dword ptr ds:[ebp+0x10]
add eax,dword ptr ds:[ebp-0x8]
add eax,dword ptr ds:[ebp-0xC]
add eax,dword ptr ds:[ebp-0x10]

//恢复现场
push edi
push esi
push ebx

//恢复堆栈
mov esp,ebp
pop ebp

//返回
ret
}
}
void test() {

}

int main(int argc, char* argv[]) {
//test();
//plus(1, 2, 3);
plus1(1, 2, 3);
return 0;
}

不再做过多的解释,如果看不明白含义的,请看上一篇文章对堆栈图的讲解