int add(int x,int y)
{
return x+y;
}
这是一个标准的c语言函数,但是在计算机眼里只有2进制,并不认识这些函数与符号
当我们的c语言程序写完以后,就会编译、链接成为可执行文件(PE文件)·
通俗来讲
编译的过程就是把我们的代码转换成机器码(OPCODE)
链接的过程就是把我们的机器码链接到可执行文件上面
当我们在vs2017里按下F7编译的时候,我们只能看到如下 提示信息
1>------ 已启动生成: 项目: ConsoleApplication3, 配置: Debug Win32 ------
1>ConsoleApplication3.cpp
1>ConsoleApplication3.vcxproj -> C:\Users\30675\source\repos\ConsoleApplication3\Debug\ConsoleApplication3.exe
========== 生成: 成功 1 个,失败 0 个,最新 0 个,跳过 0 个 ==========
如果说我们在老版本的vs中
1>------ 已启动生成: 项目: dym, 配置: Debug Win32 ------
1>正在编译...
1>dym.cpp
1>正在链接...
1>LINK : warning LNK4076: 无效的增量状态文件“C:\Documents and Settings\admin\桌面\dym\Debug\dym.ilk”;正在非增量链接
1>正在嵌入清单...
1>生成日志保存在“file://c:\Documents and Settings\admin\桌面\dym\dym\Debug\BuildLog.htm”
1>dym - 0 个错误,1 个警告
========== 生成: 成功 1 个,失败 0 个,最新 0 个,跳过 0 个 ==========
我们的c语言/c++代码被写在xx.cpp这个文件里面,当我们按下F7编译的时候
其中有很多个步骤
-
生成 调试信息
在c语言工程的根目录里有个Debug文件夹,生成如下文件:
xx.ilk(链接文件),xx.pbd(Windbg调试符号)
当我们使用16进制编辑器查看文件内容的时候发现:C语言再学习3-编译&链接_PE加载
其实ilk里面并没有东西,而pdb则存放了0环调试器,调试我们程序所需要的调试文件,里面包含了我们程序的各种说明
-
正式编译代码, 把c语言代码转换成汇编代码,当然我们的汇编遵循着cpu提供的硬编码格式
在和c语言工程根目录里面有个和工程同名的文件夹,里面还有一个Debug文件夹,生成如下文件:
BuildLog.htm(调试信息日志)
xx.pdb(单个cpp的调试符号而不是整个工程)
xx.obj(我们最重要的文件,存放汇编代码)
当我们使用16进制编辑器查看内容的时候发现,这个文件里面包含了节表的内容,通过链接程序,链接所需要的库文件等等,给汇编文件加上PE头,即可运行C语言再学习3-编译&链接_PE加载 - 最后在根目录下Debug文件夹生成xx.exe
个人理解:当c代码被编译成xx.obj的时候,文件包含了完整的节表信息和目录表,节表中描述了所有的代码和数据,导出表描述了整个xx.cpp的所有功能,
当我们写多个cpp的时候,其实编译器就是帮我们把多个cpp的对应的所有的obj的节表与节合并(包括需要的dll等等),然后根据合并完成的信息生成PE头
,然后把合并完成的pe文件保存为xx.exe。就能看到我们运行的exe文件
我们的c程序被转换成汇编代码,xx.obj里包含了生成的完整代码,链接的过程就是把很多汇编代码组合到一起
int add(int x, int y)
{
012E1795 rol byte ptr [eax],0
012E1798 add byte ptr [ebx+56h],dl
012E179B push edi
012E179C lea edi,[ebp-0C0h]
012E17A2 mov ecx,30h
012E17A7 mov eax,0CCCCCCCCh
012E17AC rep stos dword ptr es:[edi]
012E17AE mov ecx,offset _0340B6FD_consoleapplication3.cpp (012EC008h)
012E17B3 call @[email protected] (012E120Dh)
return x + y;
012E17B8 mov eax,dword ptr [x]
012E17BB add eax,dword ptr [y]
}
012E17BE pop edi
012E17BF pop esi
012E17C0 pop ebx
012E17C1 add esp,0C0h
012E17C7 cmp ebp,esp
012E17C9 call __RTC_CheckEsp (012E1217h)
012E17CE mov esp,ebp
012E17D0 pop ebp
012E17D1 ret
我们的加法函数被编译成熟悉汇编代码!
mov eax,dword ptr [x]
add eax,dword ptr [y]
当我们调用函数的时候,在汇编层面其实就是CALL 0xB41181h
,
当我们写一个空函数的时候:
void add()
{
00A21790 push ebp
00A21791 mov ebp,esp
00A21793 sub esp,0C0h
00A21799 push ebx
00A2179A push esi
00A2179B push edi
00A2179C lea edi,[ebp-0C0h]
00A217A2 mov ecx,30h
00A217A7 mov eax,0CCCCCCCCh
00A217AC rep stos dword ptr es:[edi]
00A217AE mov ecx,offset _0340B6FD_consoleapplication3.cpp (0A2C008h)
00A217B3 call @[email protected] (0A2120Dh)
}
仔细观察,我们会发现这不是一个空函数,我们的空函数反汇编里面生成了很多代码,让我们再看看函数原型
void add()
{
}
我们的函数什么都没做,但是为什么我们的函数生成了这么多的汇编代码?再观察一下汇编代码
- 保存栈底
- 提升栈底
- 提升栈顶
- 保存寄存器
- 填充缓冲区
- 降低堆栈
- RET
C语言再学习3-编译&链接_PE加载
仅仅是一个空函数就做了这么多事情,其实在编译器眼里就是这样,我们可以手写空函数,真正的空函数
void __declspec(naked) add()
{
}
我们知道,汇编代码不同于c语言代码,如果调用一个函数以后不RET,就会造成不可预料的后果!
add:
008F1790 int 3
008F1791 int 3
008F1792 int 3
008F1793 int 3
008F1794 int 3
008F1795 int 3
.......
调用我们的函数以后会无限触发断点,在三环程序中触发断点,只会报一个普通异常
如果我们是零环程序,那么一个int 3 中断就会造成整个操作系统挂起(卡死),只能重启
所以我们的空函数一定要返回,让我们修改这个空函数
void __declspec(naked) add()
{
__asm
{
ret
}
}
再次查看反汇编
__asm
{
ret
002E1790 ret
}
我们会发现这个函数已经有了一个ret
再次单步执行以后发现,我们调用的空函数成功返回
总结:编译器会帮我们把普通的空函数生成一大堆代码,如果我们写真正的空函数,则只需要一个ret即可,我们完全可以在代码里写内联汇编来优化我们的程序!
再来看看我们的c语言程序到底是怎么启动的:
当然,这是一个win32应用程序的启动过程,我们一个exe跑起来分为了几个步骤
- 系统初始化(堆栈准备,虚拟内存分配)
- CrtStartup(创建和检查进程线程,在内核句柄表添加我们的窗口,…)
- main函数启动(我们所写的代码)
小记:
我们的代码想要变成可执行EXE,会经历编译链接的过程,在底层被转换成2进制代码,通常我们看的汇编就是机器码的表述
当我们编译好的可执行EXE,运行的时候,系统受到我们的运行请求,为程序检查PE结构信息,分配堆栈空间,分配堆内存,(分配逻辑地址,线性地址)创建进程和线程,在内核句柄表创建句柄和内核对象,最后初始化进程,根据ImageBase分配程序的实例句柄(0x400000),根据导入表加载各种dll到对应的位置并且修复自身,然后重定位全局变量,修复IAT表,把EIP指向PE文件的OEP入口点,程序才开始真正的运行起来!
以下为WIN32扩展类型,看不懂可以以后再看!
- 注册窗口样式,创建实例结构体,赋值实例样式,应用程序实例句柄,窗实例图标和实例样式(可选),实例样式,背景色,主消息函数,实例名字,菜单名字(可选)
- 创建窗口句柄,窗口类名(自己注册的窗口样式),窗口名字,窗口外观样式,相对坐标和绝对坐标,父句柄,菜单句柄(可选),应用程序实例句柄,附加数据(一般为空)
- 显示窗口,更新窗口
- 取出消息,创建消息循环(不处理消息,加工后转发回操作系统,调用自定义消息处理函数)
- 自定义函数的窗口回调函数,对消息进行处理