C语言再学习3-编译&链接_PE加载

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编译的时候
其中有很多个步骤

  1. 生成 调试信息
    在c语言工程的根目录里有个Debug文件夹,生成如下文件:
    xx.ilk(链接文件),xx.pbd(Windbg调试符号)
    在这里插入图片描述
    当我们使用16进制编辑器查看文件内容的时候发现:

其实ilk里面并没有东西,而pdb则存放了0环调试器,调试我们程序所需要的调试文件,里面包含了我们程序的各种说明

  1. 正式编译代码, 把c语言代码转换成汇编代码,当然我们的汇编遵循着cpu提供的硬编码格式
    在和c语言工程根目录里面有个和工程同名的文件夹,里面还有一个Debug文件夹,生成如下文件:
    BuildLog.htm(调试信息日志)
    xx.pdb(单个cpp的调试符号而不是整个工程)
    xx.obj(我们最重要的文件,存放汇编代码)
    在这里插入图片描述
    当我们使用16进制编辑器查看内容的时候发现,这个文件里面包含了节表的内容,通过链接程序,链接所需要的库文件等等,给汇编文件加上PE头,即可运行

  2. 最后在根目录下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        @__CheckForDebuggerJustMyCode@4 (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        @__CheckForDebuggerJustMyCode@4 (0A2120Dh)  
}

仔细观察,我们会发现这不是一个空函数,我们的空函数反汇编里面生成了很多代码,让我们再看看函数原型

void add()
{

}

我们的函数什么都没做,但是为什么我们的函数生成了这么多的汇编代码?再观察一下汇编代码

  1. 保存栈底
  2. 提升栈底
  3. 提升栈顶
  4. 保存寄存器
  5. 填充缓冲区
  6. 降低堆栈
  7. RET
    在这里插入图片描述

仅仅是一个空函数就做了这么多事情,其实在编译器眼里就是这样,我们可以手写空函数,真正的空函数

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跑起来分为了几个步骤

  1. 系统初始化(堆栈准备,虚拟内存分配)
  2. CrtStartup(创建和检查进程线程,在内核句柄表添加我们的窗口,…)
  3. main函数启动(我们所写的代码)

小记:

我们的代码想要变成可执行EXE,会经历编译链接的过程,在底层被转换成2进制代码,通常我们看的汇编就是机器码的表述

在这里插入图片描述

当我们编译好的可执行EXE,运行的时候,系统受到我们的运行请求,为程序检查PE结构信息,分配堆栈空间,分配堆内存,(分配逻辑地址,线性地址)创建进程和线程,在内核句柄表创建句柄和内核对象,最后初始化进程,根据ImageBase分配程序的实例句柄(0x400000),根据导入表加载各种dll到对应的位置并且修复自身,然后重定位全局变量,修复IAT表,把EIP指向PE文件的OEP入口点,程序才开始真正的运行起来!


以下为WIN32扩展类型,看不懂可以以后再看!

  1. 注册窗口样式,创建实例结构体,赋值实例样式,应用程序实例句柄,窗实例图标和实例样式(可选),实例样式,背景色,主消息函数,实例名字,菜单名字(可选)
  2. 创建窗口句柄,窗口类名(自己注册的窗口样式),窗口名字,窗口外观样式,相对坐标和绝对坐标,父句柄,菜单句柄(可选),应用程序实例句柄,附加数据(一般为空)
  3. 显示窗口,更新窗口
  4. 取出消息,创建消息循环(不处理消息,加工后转发回操作系统,调用自定义消息处理函数)
  5. 自定义函数的窗口回调函数,对消息进行处理

猜你喜欢

转载自blog.csdn.net/qq_35425243/article/details/82782405