从创建进程到进入main函数,发生了什么?( 二 )


除了二进制的可执行文件 , 还支持shell脚本 , 这个情况下将会将脚本解释器程序作为入口来启动
从ELF入口到main函数上面交代了 , 一个新的进程 , 是如何执行到可执行文件的入口地址的 。
同时也留了一个问题 , 这个入口地址是什么?是我们的main函数吗?
这里有一个简单的C程序 , 运行起来后输出经典的hello world:
#include int main() {printf("hello, world!\n");return 0;}通过gcc编译后 , 生成了一个ELF可执行文件 , 通过readelf指令 , 可以实现对ELF文件的分析 , 这里可以看到ELF文件的入口地址是0x400430:
从创建进程到进入main函数,发生了什么?文章插图
随后 , 我们通过反汇编神器 , IDA打开分析这个文件 , 看一下位于0x400430入口的地方是什么函数?
从创建进程到进入main函数,发生了什么?文章插图
可以看到 , 入口地方是一个叫做 _start 的函数 , 并不是我们的main函数 。
在_start的结尾 , 调用了 __libc_start_main 函数 , 而这个函数 , 位于libc.so中 。
你可能疑惑 , 这个函数是哪里冒出来的 , 我们的代码中并没有用到它呢?
其实 , 在进入main函数之前 , 还有一个重要的工作要做 , 这就是:C/C++运行时库的初始化 。 上面的 __libc_start_main 就是在完成这一工作 。
在通过GCC进行编译时 , 编译器将自动完成运行时库的链接 , 将我们的main函数封装起来 , 由它来调用 。
glibc是开源的 , 我们可以在GitHub上找到这个项目的libc-start.c文件 , 一窥 __libc_start_main 的真面目 , 我们的main函数正是被它在调用 。
从创建进程到进入main函数,发生了什么?文章插图
完整流程到这里 , 我们梳理了 , 从进程创建fork , 到通过exec系列函数完成可执行文件的替换 , 再到执行流程进入到ELF文件的入口 , 再到我们的main函数的完整流程 。
从创建进程到进入main函数,发生了什么?文章插图
Windows上的一些区别下面简单介绍下Windows上这一流程的一些差异 。
首先是创建进程的环节 , Windows系统将fork+exec两步合并了一步 , 通过CreateProcess系列函数一步到位 , 在其参数中指定子进程的可执行文件路径 。
不同于Linux上进程和线程的边界模糊 , 在Windows操作系统上 , 内核是有明确的进程和线程概念定义 , 进程用EPROCESS结构表示 , 线程用ETHREAD结构表示 。
所以在Windows上 , 进程相关的工作准备就绪后 , 还需要单独创建一个参与内核调度的执行单元 , 也就是进程中的第一个线程:主线程 。 当然 , 这个工作也封装在了CreateProcess系列函数中了 。
新进程的主线程创建完成后 , 便开始参与系统调度了 。 主线程从哪里开始执行呢?内核在创建时就明确进行了指定:nt!KiThreadStartup , 这是一个内核函数 , 线程启动后就从这里开始执行 。
线程从这里启动后 , 再通过Windows的异步过程调用APC机制执行提前插入的APC , 进而将执行流程引入应用层 , 去执行Windows进程应用程序的初始化工作 , 比如一些核心DLL文件的加载(Kernel32.dll、ntdll.dll)等等 。
随后 , 再次通过APC机制 , 再转向去执行可执行文件的入口点 。
这后面和Linux上的机制类似 , 同样没有直接到main函数 , 而是需要先进行C/C++运行时库的初始化 , 这之后经过运行时函数的包装 , 才最终来到我们的main函数 。
下面是Windows上 , 从创建进程到我们的main函数的完整流程(高清大图:):
从创建进程到进入main函数,发生了什么?文章插图
现在你清楚 , 从进程启动是怎么一步步到你的main函数的了吗?有疑惑和不解的地方 , 欢迎留言交流 。
作者:轩辕之风
来源:编程技术宇宙(ID:xuanyuancoding)