一、说明
在前面对写好的程序代码装载到计算机内存的过程进行了分析和说明。这是继代码的编译链接后,从程序转为进程的重要一环。但其实,对于大多数的开发者来说,他们更关心的是程序如何启动,然后又是如何调用自己写得程序代码的整体流程。
或者这样说,开发者更重视的是一种直观的对自己代码在计算机中执行的显式的描述。这样,就更能够将代码与CPU的实际执行过程进行对比。从而更加明了代码编写处理逻辑与运行逻辑的相互关系。
二、程序加载后的执行
很多开发者认为,main函数(主函数)就是程序的入口,从这里开始,自己的Code开始执行,并进入相应的运行逻辑处理。但实际的情况却未必是如此。不同的操作系统平台,可能有着自己不同的程序启动执行的逻辑。它们一般都是在进程创建完成、各种资源准备完毕并且相关的校验通过后,才进入相关的可执行的入口点,进而调用main函数,并正式开始执行开发者编写的代码逻辑的。
三、Linux系统的执行
先看一下在Linux系统中,是如何开始执行程序代码流程的。其主要包括以下的步骤:
- 调用动态链接器入口
内核在进程准备完成后,不会直接中到主程序的入口,而调用动态链接器的入口。在继续准备相关的信息的同时,准备解析主程序入口。并最终将控制权交给主程序e_entry,它通常是_start - 调用主程序入口
_start往往是C运行库的启动文件,而不是习惯上认知的main。这个入口一般会继续获取参数信息(argc,argv等),进行ABI的处理以及调用__libc_start_main。初始化相关libc及线程及相关的安全状态。预初始化并初始化相关数组及对全局及静态变量进行构造,需要注意的是,前面提到的全局变量依赖不确定(静态初始化顺序问题)就是在这个阶段出现的。最终调用main函数 - 调用main入口
一般情况下,主线程会启动main函数。大多数的计算、函数调用及相关的内存操作等等,都是在用户态进行的。如果需要与内核进行交互,一般是通过libstdc++/libc库来进行系统调用(如线程切换等)。利用syscall来完成与内核的通信后再次回到用户态 - 运行
这个就好理解了,整个代码的各种执行逻辑。正常的情况下,会和开发者的开发逻辑是几乎保持一致的。但在细节的调度上可能有所出入,但不会影响运行的结果 - 退出
main执行完成,会调用std::exit来进行程序的退出。一般包括作用域内的对象的资源回收、atexit注册的回调、静态或全局对象的资源回收、IO的处理、动态链接器和运行库的结束处理,并最终调用exit_group系统调用结束代码的生命周期。当然,直接或间接的调用abort、Kill等等方式来退出语义也不尽相同,但大多是省略了上述的资源回收相关的正常流程,不再详细展开。
exit_group后,内核开始结束相关的线程、关闭回收文件资源、内存资源、同步和通信资源和状态并通知其父进程。由其进行最后的处理。
四、Windows系统的执行
不同的平台,整体的逻辑思路大抵是相通的。但在实际设计或实现时,还是有着很大的区别。Windows平台的执行步骤主要有:
- 加载器运行
ntdll.dll中的加载器进行用户进程的初始化。包括自身的初始化及PEB相关、解析exe文件的mport Table、定位并处理相关依赖及解析、配置各种异常及安全相关信息并最终执行相关模块的初始化 - DLL入口
在进入exe的入口前,加载器会调用DLL的入口,一般形式为DllMain这种表示。这一步一般由Windows加载器和依赖关系处理 - PE入口和CRT
完成上述的操作后,程序的控制权会转到PE Optional Header的AddressOfEntryPoint。对于MSVC程序来说,它通常是类似mainCRTStartup入口(不同的启动情况对应不同的类似的入口)。然后对CRT进行初始化,完成相关的c/c++的初始化表及相关的全局/静态对象的构造。并最终调用main(WinMain等)函数 - 调用main函数
在此函数中,可以进行大多数的程序代码的处理。如果需要和内核通信,则通过CRT调用相关的API,利用ntdll Native API实现syscall与内核通信。并在完成相关任务后重新返回用户态任务 - 运行
按照相关的开发逻辑进行执行,细节的控制不同,正常情况下不会影响运行的结果 - 退出
和Linux中的退出类似,回收各种资源对象、调用atexit/_onexit注册函数、处理IO资源、清理CRT状态并调用ExitProcess。Windows的异常退出相对较为复杂,但基本也是省略了资源的处理,此处不再展开。
Windows内核接收到终止信号后会对线程、内存、各种句柄资源和引用对象进行处理,特别是处理动态库的映射等并重置退出状态,处理与退出进程相关的各方的协调应对
五、总结
可执行文件执行,不是很多人简单理解的,编译好的代码装载到内存后就就调用开发者写的main入口。而是由操作系统首先创建进程进行动态加载器处理的相关资源、状态以及依赖与重定位等。并由C/C++运行库完成相关初始化,然后才会转入到开发者编写的入口函数。