-
读取PE文件
当一个PE文件被执行时,Windows的创建进程函数(CreateProcess)首先被调用,负责为新进程创建虚拟地址空间。
操作系统从磁盘读取PE文件,将其头部内容(DOS头、PE头和节表)载入内存,以获取该文件的结构和装载信息。 -
检查PE文件有效性
操作系统首先会检查PE文件的合法性,比如验证DOS头中的 "MZ" 签名,接着检查PE头中的 "PE\0\0" 签名。
如果文件不符合PE格式要求,操作系统会中止加载并返回错误。 -
分配虚拟地址空间
根据PE头部的信息,操作系统会为进程分配所需的虚拟地址空间。其中包括程序的代码段、数据段、堆栈等。
装载基地址:PE文件头中包含一个“装载基地址”(ImageBase),指明程序希望被加载到哪个虚拟地址。如果该地址不可用(例如,冲突),则需要重定位。
操作系统使用分页内存管理机制来为程序分配虚拟内存区域,但此时并不会将所有PE文件的内容都立即加载到物理内存中。 -
加载各个段到虚拟内存
根据节表(Section Table)的描述,操作系统会将PE文件中的不同段(如代码段、数据段等)映射到进程的虚拟地址空间中:
代码段(.text)通常是只读和可执行的。
数据段(.data)是读写区域,存放全局变量和已初始化的数据。
未初始化数据段(.bss)通常被操作系统初始化为0。
导入表(.idata)、导出表(.edata)等特殊段也会映射到内存中,用于动态链接库的管理。
操作系统在加载时会将这些段映射到虚拟地址空间中,并且可能使用惰性加载(Lazy Loading)策略,即只有在段被实际访问时才会将其加载到物理内存中。 -
动态链接库(DLL)的加载与解析
如果PE文件有导入表(Import Table),它会列出该可执行文件所依赖的DLL文件及其函数。
操作系统会读取导入表中的每一个DLL名称,并尝试加载这些DLL文件。未加载的DLL文件将被操作系统载入到当前进程的虚拟地址空间中。
接着,操作系统会解析导入表中所引用的每个函数符号,并将它们映射到正确的内存地址上。如果函数是延迟绑定的(如使用 LoadLibrary 和 GetProcAddress 动态加载),则它们会在第一次被调用时加载。 -
地址重定位(Relocation)
如果PE文件的装载基地址被操作系统占用,操作系统会将PE文件加载到不同的地址空间。此时需要进行重定位,即调整PE文件中的指针和地址,使其指向新的基地址。
重定位信息存储在PE文件中的重定位表(Relocation Table)中,操作系统会根据该表修正所有与基地址相关的地址。 -
执行TLS回调函数
如果PE文件使用了线程局部存储(TLS),在加载过程中,操作系统会调用TLS回调函数。这些回调函数通常用于在进程或线程初始化时执行特定的任务,如全局变量初始化等。 -
设置初始栈和堆
操作系统为进程分配堆栈和堆。堆栈用于保存函数调用信息和局部变量,堆用于动态内存分配。
栈空间初始分配较小,随着函数调用的深入,栈空间会动态增加。 -
跳转到入口点执行
PE文件的PE头中有一个入口点(Entry Point),它是程序的主执行代码的起始地址。
操作系统通过加载器跳转到入口点并开始执行程序。对于普通应用程序,入口点通常是 main() 函数;对于DLL文件,则是 DllMain() 函数。 -
运行程序的主逻辑
从入口点开始,程序的执行逻辑正式运行,执行代码、访问数据,并与操作系统、其他进程或设备进行交互。
如果程序请求内存或与外部设备交互,会通过系统调用来请求操作系统的服务。
三、进程结束和卸载
当进程完成后,程序会通过系统调用 ExitProcess() 或 exit() 通知操作系统结束运行。
操作系统会清理该进程分配的虚拟地址空间,释放物理内存,并将该进程的所有资源(包括文件句柄、内存等)返还给系统。
如果是DLL文件,操作系统会根据需要调用 FreeLibrary() 函数卸载不再使用的DLL,并清理与该DLL相关的资源。