作者 | 轩辕之风O

来源 | 编程技术宇宙(ID:xuanyuancoding)

头图 |  CSDN 下载自东方IC

前几天,读者群里有小伙伴提问:从进程创建后,到底是怎么进入我写的 main 函数的?

今天这篇文章就来聊聊这个话题。

首先先划定一下这个问题的讨论范围:C/C++语言。

这篇文章主要讨论的是操作系统层面上对于进程、线程的创建初始化等行为,而像 Python、Java 等基于解释器、虚拟机的语言,如何进入到 main 函数执行,这背后的路径则更长(包含了解释器和虚拟机内部的执行流程),以后有机会再讨论。所以这里就重点关注 C/C++这类 native 语言的 main 函数是如何进入的。

本文会兼顾叙述 Linux 和 Windows 两个主要平台上的详细流程。

创建进程

第一步,创建进程。

在 Linux 上,我们要启动一个新的进程,一般通过 fork + exec 系列函数来实现,前者将当前进程“分叉”出一个孪生子进程,后者负责替换这个子进程的执行文件,来执行子进程的新程序文件。

这里的 fork、exec 系列函数,是操作系统提供给应用程序的 API 函数,在其内部最终都会通过系统调用,进入操作系统内核,通过内核中的进程管理机制,来完成一个进程的创建。

操作系统内核将负责进程的创建,主要有下面几个工作要做:

  • 创建内核中用于描述进程的数据结构,在Linux上是task_struct

  • 创建新进程的页目录、页表,用于构建新进程的内存地址空间

在 Linux 内核中,由于历史原因,Linux 内核早期并没有线程的概念,而是用任务:task_struct 来描述一个程序的执行实例:进程。

在内核中,一个任务对应就是一个 task_struct,也就是一个进程,内核的调度单元也是一个个的 task_struct。

后来,多线程的概念兴起,Linux 内核为了支持多线程技术,task_struct 实际上表示的变成了一个线程,通过将多个 task_struct 合并为一组(通过该结构内部的组 id 字段)再来描述一个进程。因此,Linux 上的线程,也称为轻量级进程。

系统调用 fork 的一个重要使命就是要去创建新进程的 task_struct 结构,创建完成后,进程就拥有了调度单元。随后将开始可以参与调度并有机会获得执行。

加载可执行文件

通过 fork 成功创建进程后,此时的子进程和父进程相当于一个细胞进行了有丝分裂,两个进程“几乎”是一模一样的。

而要想子进程执行新的程序,在子进程中还需要用到exec系列函数来实现对进程可执行程序的替换。

exec系列函数同样是系统调用的封装,通过调用它们,将进入内核sys_execve来执行真正的工作。

这个工作细节比较多,其中有一个重要的工作就是加载可执行文件到进程空间并对其进行分析,提取出可执行文件的入口地址。

我们使用 C、C++ 等高级语言编写的代码,最终通过编译器会编译生成可执行文件,在 Linux 上,是 ELF 格式,在 Windows 上,称之为 PE 文件。

无论是 ELF 文件还是 PE 文件,在各自的文件头中,都记录了这个可执行文件的指令入口地址,它指示了程序该从哪里开始执行。

这个入口指向哪里,是我们的 main 函数吗?这里卖一个关子,先来解决在这之前的一个问题:进程创建后,是如何来到这个入口地址的?

不管在 Windows 还是 Linux 上,应用线程都会经常在用户空间和内核空间来回穿梭,这可能出现在以下几种情况发生时:

  • 系统调用

  • 中断

  • 异常

从内核返回时,线程是如何知道自己从哪里进来的,该回到应用空间的哪里去继续执行呢?

答案是,在进入内核空间时,线程将自动保存上下文(其实就是一些寄存器的内容,比如指令寄存器EIP)到线程的堆栈上,记录自己从哪里来的,等到从内核返回时,再从堆栈上加载这些信息,回到原来的地方继续执行。

前面提到,子进程是通过sys_execve系统调用进入到内核中的,在后面完成可执行文件的分析后,拿到了ELF文件的入口地址,将会去修改原来保存在堆栈上的上下文信息,将EIP指向ELF文件的入口地址。这样等sys_execve系统调用结束时,返回到用户空间后,就能够直接转到新的程序入口开始执行代码。

所以,一个非常重要的特点是:exec系列函数正常情况下是不会返回的,一旦进入,完成使命后,执行流程就会转向新的可执行文件入口。

另外需要提一下的是,在Linux上,除了ELF文件,还支持一些其他格式的可执行文件,如MS-DOS、COFF。

除了二进制的可执行文件,还支持shell脚本,这个情况下将会将脚本解释器程序作为入口来启动。

从ELF入口到main函数

上面交代了,一个新的进程,是如何执行到可执行文件的入口地址的。

同时也留了一个问题,这个入口地址是什么?是我们的main函数吗?

这里有一个简单的C程序,运行起来后输出经典的hello world:

#include <stdio.h>int main() {    printf("hello, world!\n");    return 0;}

通过 gcc 编译后,生成了一个 ELF 可执行文件,通过 readelf 指令,可以实现对 ELF 文件的分析,这里可以看到 ELF 文件的入口地址是 0x400430:

随后,我们通过反汇编神器,IDA 打开分析这个文件,看一下位于0x400430入口的地方是什么函数?

可以看到,入口地方是一个叫做 _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 函数正是被它在调用。

完整流程

到这里,我们梳理了,从进程创建 fork,到通过 exec 系列函数完成可执行文件的替换,再到执行流程进入到 ELF 文件的入口,再到我们的 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函数的完整流程(高清大图:https://bbs.pediy.com/upload/attach/201604/501306_qz5f5hi1n3107kt.png):

现在你清楚,从进程启动是怎么一步步到你的main函数的了吗?有疑惑和不解的地方,欢迎留言交流。

更多精彩推荐
☞Unity “出圈”:游戏引擎的技术革新和跨界商机
☞大写的服!用耳朵也能写代码?盲人程序员自学编程成为全栈工程师
☞小霸王被申请破产重整;虎牙员工自曝被HR抬出公司;Office 2010被微软终止服务|极客头条
☞有了图分析,可解释的AI还远吗?☞移动云11.11,钜惠High不停!
☞提高警惕!国内虚拟货币犯罪形势日渐严峻
点分享点点赞点在看

从创建进程到进入 main 函数,发生了什么?相关推荐

  1. linux main是什么进程,从创建进程到进入main函数,发生了什么?

    从创建进程到进入main函数,发生了什么? 从创建进程到进入main函数,发生了什么? 前几天,读者群里有小伙伴提问:从进程创建后,到底是怎么进入我写的main函数的? 今天这篇文章就来聊聊这个话题. ...

  2. 关于linuxC语言中创建进程,利用execlp函数执行.c程序问题

    关于linuxC语言中创建进程,利用execlp函数执行.c程序问题 对于一个小白来说,这个系统调用真的太难了,完全摸不着头脑啊,一搜execlp函数,全是执行的命令,什么传参硬是没懂. 不过说到底, ...

  3. linux c之创建进程fork和vfork函数之间的区别

    1.函数简介 1).得到当前的进程id pid_t getpid(); 2).fork函数 要创建进程,必须调用fork函数, 系统调用fork用于派生一个进程,函数原型如下 #include< ...

  4. Linux0.11 创建进程的过程分析--fork函数的使用

    /* * linux/kernel/fork.c * * (C) 1991 Linus Torvalds */ /* 注意:signal.c和fork.c文件的编译选项内不能有vc变量优化选项/Og, ...

  5. linux创建新进程就分配空间,linux几种创建进程的方法

    在Linux中主要提供了fork.vfork.clone三个进程创建方法. 在linux源码中这三个调用的执行过程是执行fork(),vfork(),clone()时,通过一个系统调用表映射到sys_ ...

  6. Linux_进程控制(创建进程,等待进程,进程终止)

    文章目录 1.创建进程 1.1 fork()函数初识 1.2 fork()创建进程代码示例 2.等待进程 2.1 进程等待概念 2.2进程等待必要性 2.3 进程等待方法 2.3.1 wait 2.3 ...

  7. Linux:进程管理 | 进程创建 | 进程终止 | 进程等待 | 进程替换

    文章目录 进程创建 fork 写时拷贝机制 进程终止 退出码 进程退出方法 进程等待 阻塞.非阻塞的等待 进程替换 替换函数 exec l exec lp exec le exec ve exec v ...

  8. 进程替换exec系列函数

    进程替换,不是创建一个新进程,而是将一个运行的进程替换为其他进程执行:PCB使用原进程的,只进行修改.并且,进程替换是与进程复制配合进行的. 在进程复制时,我们根据fork()的返回值对父子进程进行区 ...

  9. 【Linux 内核】进程管理 ( 进程状态 | 进程创建 | 进程终止 | 调用 exit 系统调用函数主动退出 | main 函数返回自动退出 | kill 杀死进程 | 执行异常退出 )

    文章目录 一.进程状态 二.进程创建 三.进程终止 ( 调用 exit 系统调用函数主动退出 | main 函数返回自动退出 | kill 杀死进程 | 执行异常退出 ) 一.进程状态 Linux 进 ...

最新文章

  1. Ext智能提示 - Spket(Eclipse插件)
  2. shell脚本中的变量
  3. Nginx的location、root、alias指令用法和区别
  4. JedisConnectionException: java.net.SocketException: Broken pipe
  5. 一文带你了解隐马尔科夫模型
  6. thinkphp3.23开发的“二当家的”官网
  7. C#浅拷贝与深拷贝区别
  8. [转]Paul Adams:为社交设计
  9. XCode4.2 SVN设置
  10. Shiro系列-Shiro中Realm如何使用
  11. Modelsim-Altera仿真设置
  12. C语言读取图像像素坐标,OpenCL中读取image时的坐标
  13. 华为方舟编译器 下载 和 LiteOS Studio Setup 2019-04-16.exe SDK下载
  14. 07网络发展趋势:风险和机遇并存
  15. python保存图片到指定路径_[Python03] 5分钟学会3种方法给模块添加路径!
  16. jmeter压力测试+badboy脚本录制
  17. [Luogu P4630] [BZOJ 5463] [APIO2018] Duathlon 铁人两项
  18. 输入快递单号查询不到物流怎么办
  19. 无人驾驶13:PID控制器
  20. 西安交通大学MOOC C++期末1

热门文章

  1. 放射科医生选择AI供应商的10大标准
  2. 「模型训练」如何迁移学习一个小网络到移动端
  3. 5分钟带你读懂“语音识别”工作原理
  4. 比较好的中文分词方案汇总推荐
  5. 人工智能科普|极大似然估计——机器学习重要知识点
  6. 未来十年是AI的黄金发展期
  7. 图形卷积神经网络有多强大?一文让你熟练掌握GCN
  8. SAP-注入“AI基因” 打造全球第一款“智能ERP
  9. 机器学习常用激活函数
  10. Tensorflow— 递归神经网络RNN