裸函数
C源码(.c) →【预处理】→【编译】→【汇编】→【链接】→可执行程序
- 预处理 (cpp):展开头文件、宏替换、删掉注释,生成
.i文件(还是 C 语言文本) - 编译 (cc1):C → 汇编 (.s)
- 汇编 (as):汇编 (.s) → 机器码目标文件 (.o/.obj)
- 链接 (ld):多个.o + 库 → exe/elf 可执行文件
因此在我们的程序成为可执行文件之前,先转化为汇编语言,我们看到的函数里面的那些代码都死编译器自己生成的,push ebp等等,裸函数是编译器完全不生成函数 prologue (序言) /epilogue (尾声),你必须手写全部汇编。
需要注意的是 裸函数不能为空 而且不能出现C格式的代码
void __declspec(naked) Function()
{
_asm
}
{
汇编内容
}
列举三个常用的裸函数框架
无参数无返回值的框架
void __declspec(naked) Function()
{
__asm
{
push ebp
mov ebp,esp
sub esp,0x40
push ebx
push esi
push edi
lea edi,dword ptr ds:[ebp-0x40]
mov eax,0xCCCCCCCC
mov ecx,0x10
rep stosd
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
}
}
有参数有返回值的函数框架
int __declspec(naked) Function(int x,int y)
{
__asm
{
push ebp
mov ebp,esp
sub esp,0x40
push ebx
push esi
push edi
lea edi,dword ptr ds:[ebp-0x40]
mov eax,0xCCCCCCCC
mov ecx,0x10
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 __declspec(naked) Function(int x,int y)
{
__asm
{
push ebp
mov ebp,esp
sub esp,0x40
push ebx
push esi
push edi
lea edi,dword ptr ds:[ebp-0x40]
mov eax,0xCCCCCCCC
mov ecx,0x10
rep stosd
mov dword ptr ds:[ebp-4],2
mov dword ptr ds:[ebp-8],3
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
}
}
裸函数作业
练习:
1、用__declspec(naked)裸函数实现下面的功能
练习目的:
(1) 熟悉堆栈结构
(2) 参数、局部变量的位置
(3) 返回值存储的位置
int plus(int x,int y,int z) 010101010
{
int a = 2;
int b = 3;
int c = 4;
return x+y+z+a+b+c;
}
#include<stdio.h>
int plus1(int x, int y, int z)
{
int a = 2;
int b = 3;
int c = 4;
return a + b + c + x + y + z;
}
int _declspec(naked) plus(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-4],2
mov dword ptr ds : [ebp - 8] , 3
mov dword ptr ds : [ebp - 0Xc] , 4
// 核心计算功能
mov eax,dword ptr ds:[ebp-4]//第一个变量赋值给eax
add eax,dword ptr ds:[ebp - 8]//第一个变量赋值给eax
add eax,dword ptr ds:[ebp - 0XC]//第一个变量赋值给eax
add eax,dword ptr ds:[ebp+8]//第一个参数赋值给eax
add eax,dword ptr ds:[ebp+0xC]//第一个参数赋值给
add eax,dword ptr ds:[ebp+0x10]//第一个参数赋值给
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
}
}
extern "C" void test()
{
}
int main()
{
int res = plus(1, 2, 3);
printf("%d", res);
}
调用约定
常见的几种调用约定:
| 调用约定 | 参数压栈顺序 | 平衡堆栈 |
| __cdecl | 从右至左入栈 | 调用者清理栈 |
| __stdcall | 从右至左入栈 | 自身清理堆栈 |
| __fastcall | ECX/EDX传送前两个 剩下:从右至左入栈 | 自身清理堆栈 |
简单来说是__cdecl是外平栈 __stdcall __fastcall是内平栈
举例:
1、int __cdecl Plus(int a, int b)
{
return a+b;
}
push 2
push 1
call @ILT+15(Plus) (00401014)
add esp,8
2、int __stdcall Plus(int a, int b)
{
return a+b;
}
push 2
push 1
call @ILT+10(Plus) (0040100f)
函数内部:
ret 8
3、int __fastcall Plus(int a, int b)
{
return a+b;
}
mov edx,2
mov ecx,1
call @ILT+0(Plus) (00401005)
函数内部:
ret
4、int __fastcall Plus4(int a, int b,int c,int d)
{
return a+b+c+d;
}
push 4
push 3
mov edx,2
mov ecx,1
call @ILT+5(Plus) (0040100a)
函数内部:
ret 8
C语言数据类型

字符存储数据
以char类型举例
数据宽度 1个字节(8位)
存储格式分为有符号和无符号 有符号 -128 - 127 无符号255
需要注意的是有无符号是我们自己定义的 在内存中的存储的方式是完全一致的
作用域(变量属性,和数据类型无关)
变量可以被访问、使用的代码范围
局部变量、全局变量、静态局部变量
浮点类型
float i = 12.5f;
00401028 mov dword ptr [ebp-4],41480000h
float i = -8.25f;
00401028 mov dword ptr [ebp-4],0C1040000h
x86‑32 MSVC / MASM 汇编规则:
凡是以 A‑F 开头的十六进制常量,前面必须加一个前导数字
0
C1040000h 第一个字符是 C(字母),汇编器会分不清:
- 它是数字
0C1040000h - 还是标识符 / 变量名
C1040000
单浮点在内存的存储
8.25
1.首先把整数部分转换成二进制(除2 取余运算)
8/2 = 4···0
4/2 = 2···0
2/2 = 1···0
1/2 = 0···1
2.小数部分转换为二进制(乘以2直到没有小数部分)
0.25 x 2 = 0.5 0
0.5 x 2 = 1.0 1
8.25的二进制表示10000.01->转换成科学计数法1.00001 x 23
3.然后就可以开始拼接二进制
| 符号位 | 指数部分 | 尾数 |
| 0 | 10000010 | 00001000000000000000000 |
指数部分 = 科学技术法的指数部分-1 填入即可
| 0100 | 0001 | 0000 | 0100 | 0000 | 0000 | 0000 | 0000 |
| 4 | 1 | 0 | 4 | 0 | 0 | 0 | 0 |
验证:
float i = 8.25f;
00401028 mov dword ptr [ebp-4],41040000h 结果是正确的
调用约定作业
海哥用不同调用约定的函数嵌套构建了一个exe 作业内容需要根据汇编代码还原
如何找到程序入口(控制台程序)
1、程序的真正入口
(1) main 或WinMain 是“语法规定的用户入口”,而不是“应用程序入口”。应用程序入口通常是启动函数。
通过VC6堆栈调用查看
(2) mainCRTStartup 和 wmainCRTStartup 是控制台环境下多字节编码和Unicode 编码的启动函数.
而WinMainCRTStartup 和wWinMainCRTStartup 是windows 环境下多字节编码和Unicode 编码的启动函数.


设置完用户入口变成了test函数
main 函数的识别:
main 函数被调用前要先调用的函数如下:
GetVersion()
_heap_init()
GetCommandLineA()
_crtGetEnvironmentStringsA()
_setargv()
_setenvp()
_cinit()

这些函数调用结束后就会调用main 函数,根据main 函数调用的特征,将3 个参数压入栈内作为函数的参数。

启动函数

这里也可以看到GetVersion 是属于启动函数的 我们可以继续往下找一个三个参数的

三个参数

进去看一下发现就是我们要找的main函数
作业代码
#include<stdio.h>
int _stdcall Function2(int x,int y,int z)
{
return x + y + z;
}
int _cdecl Function3(int x,int y)
{
return x + y;
}
int _fastcall Function(int x, int y, int z, int c, int l)
{
int a = x;
int b = y;
int q = Function2(a,b,z);//ebp-0xc
int d = Function3(a, b);//ebp-0x10
int res = Function3(q, d);
return res;
}
int main()
{
int res = Function(1,3,4,6,7);
printf("eax = 0x%x", res);
}
结果是0XC