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

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

前几天,读者群里有小伙伴提问:从进程创建后,到底是怎么进入我写的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脚本,这个情况下将会将脚本解释器程序作为入口来启动

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

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

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

#include

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系统将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函数的了吗?有疑惑和不解的地方,欢迎留言交流。

我是Redis,MySQL大哥被我害惨了!

CPU明明8个核,网卡为啥拼命折腾一号核?

因为一个跨域请求,我差点丢了饭碗

完了!CPU一味求快出事儿了!

哈希表哪家强?几大编程语言吵起来了!

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

python创建虚拟环境(二):使用终端命令创建虚拟环境

python创建虚拟环境(二):使用终端命令创建虚拟环境 pip install virtualenv -i https://pypi.douban.com/simple/ (1)进入要存放虚拟环境的地址 这里我希望把要创建的虚拟环境放在D:\ebvs目录下 (2)创建虚拟环境 virtualenv 环境名 创建了一个名为pacho

FLASK环境安装,项目创建和启动

FLASK环境安装,项目创建和启动 FLAKSK目录 一: Flask了解: 二: 环境的安装: 三:基本使用: 四:FlASK的创建和启动: 0: Flask的模板导入: 1: Flask项目的创建: 2:老版本直接启动: 3: 新版--命令方式启动: 4: pycharm启动: 五:FLASK类的其他参数

九宫格游戏(java实现)

九宫格游戏(java实现) 创建了一个九宫格按钮类(继承了JButton),定义了一个九宫格类,先随机生成8个数,用网格排版放入9个格子,然后在游戏创建了3x3的矩阵用于存储状态,再对每个按键加监听器,当按下去时判断能否移动,能移动则交换空格和这个按钮的名字,

IDEA创建springBoot项目

IDEA创建springBoot项目 一、创建 二、配本地maven 三、更改合适的版本 一直使用2.0.4.RELEASE版本,其他版本有时候会出现数据库不兼容报错的现象。 四、引入常用的pom配置 dependencies dependency groupIdorg.springframework.boot/groupId artifactIdsprin

npm 使用(创建,上传,删除)

npm 使用(创建,上传,删除) 创建 当然啦,先要注册自己的npm账号 npm init 先cd 进你要放包的目录, 然后npm init 填写相关的资料 name:填写插件包名,默认是当前文件夹的名字(注意查看npm上有没有重复包名-- version:你需要发布包的版本,默认是1.0.0

AXIOS登录前端

AXIOS登录前端 1.创建一个Login.vue页面 在 views/components 下创建 Login.vue 页面 templatediv测试axios-{{title}}/div/templatescriptimport { login } from '@/http/apis';export default {data() {return {title: '测试axios',}},mounted() {},methods:

虚拟机创建流程-libvirt篇(上)

虚拟机创建流程-libvirt篇(上) 虚拟机创建流程-libvirt篇(上)-社区博客-网易数帆 libvirt的架构 libvirt是CS架构应用,用户通过client与server交互,server与client通过socket连接通信。基本架构图如下所示: libvirt分为client和deamon两个部分 libvirt

进程通信方式详解【管道、消息队列、共享内存、信号量】

进程通信方式详解【管道、消息队列、共享内存、信号量】 目录 进程间通信的目的: 常见的通信方式 按通信类型区分 详解 3.1 管道(无名管道) 3.2 命名管道 3.3消息队列 3.4 共享内存 3.5信号量 3.6 信号和信号集 进程间通信的目的: 1)数据传输:一个进程需

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

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

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

  2. java什么时候创建进程,Java创建进程

    Java创建进程 1 进程的概念 1 1.1 进程的概念 1 1.2 进程的特征 1 1.3 进程与线程区别 1 2 进程的创建 1 2.1 JAVA进程的创建 1 2.1.1 ProcessBuil ...

  3. C# 双保险进程监视器 lol 保证被监视的程序几乎永远运行. 关键字:进程操作 进程查看 创建进程

    假设现在有个程序 我希望它一直运行着 几年几十年不去管它. 可是有时候windows总是很梦幻的导致我的程序崩溃退出.所以我决定写一个监视器, 查看当前进程列表 里面如果有这个名字的进程,就sleep ...

  4. Linux系统【一】CPU+MMU+fork函数创建进程

    切板中的内容输出到文件### 进程相关概念 程序:编译好的二进制文件,在磁盘上,不占用系统资源(不包括磁盘).(剧本) 进程:占用系统资源,是程序的一次运行.(戏剧) 一个程序可以产生多个进程,一个进 ...

  5. Linux创建进程必须fork么,Linux - fork() 创建进程

    Linux - fork() 创建进程 Linux - fork() 创建进程 介绍创建进程之前,先简单地介绍一下 Linux 下的进程内存布局. Stack - 所有函数的 local variab ...

  6. 《Linux内核分析》 第六节 进程的描述和进程的创建

    <Linux内核分析> 第六节 进程的描述和进程的创建 20135307 张嘉琪 原创作品转载请注明出处 +<Linux内核分析>MOOC课程http://mooc.study ...

  7. Linux进程的创建图文教程,进程的创建和终止(超详细)

    大多数系统的进程能够并发执行,它们可以动态创建和删除.因此,操作系统必须提供机制,用于创建进程和终止进程. 进程创建 进程在执行过程中可能创建多个新的进程.创建进程称为父进程,而新的进程称为子进程.每 ...

  8. Android系统中的进程管理:进程的创建

    对于操作系统来说,进程管理是其最重要的职责之一. 考虑到这部分的内容较多,因此会拆分成几篇文章来讲解. 本文是进程管理系统文章的第一篇,会讲解Android系统中的进程创建. 本文适合Android平 ...

  9. 操作系统实验--进程的创建撤销和控制

    进程的创建撤销和控制 一.实验名称 进程的创建撤销和控制 二.实验内容 学生独立设计用于描述进程的数据结构,完成进程的创建.同步和互斥. 三.实验原理和设计思路 在Linux系统下用fork()函数创 ...

最新文章

  1. Python maketrans() 方法
  2. eplan接触器主触点怎么成一组_实物讲解接触器自锁电路的接线方法
  3. django第三次(转自刘江)
  4. MYSQL优化---hidba
  5. [LintCode] Reverse Integer
  6. 依赖注入底层反射原理_PHP反射机制实现自动依赖注入
  7. svn 验证位置失败 Authorization failed
  8. 优酷响应式在消费场景的落地之 iOS 篇
  9. Eclipse Code Template 设置自动加注释
  10. java程序 打包_Java程序打包方法最强集合
  11. iPhone各个机型屏幕尺寸
  12. VMware Fusion网络配置相关原理
  13. uniapp 实现识别图片二维码
  14. 总结:硬盘随机读写与顺序读写的性能差异
  15. 女生适合做产品经理吗?
  16. CentOS7+VMware 14的安装教程
  17. 移动手机用户目录下的证书至根目录下
  18. NBA常规赛总得分排行榜(数据截止至11年4月14日)
  19. 赛门铁克:深化本地化发展不是空谈
  20. 如何停止VBS永久循环

热门文章

  1. skype for linux 下载,开源周新闻:微软重大更新Skype for Linux
  2. 手把手教你如何搭建一个自己的安卓快速开发框架之带你做自己的APP(二)
  3. pycharm 新建项目时要勾选inherit global site-packages!pycharm 新建项目时要勾选inherit global site-packages
  4. fastjson中List和JSONArray的相互转换
  5. 批处理文件实现倒计时关机
  6. 北邮考研复试科目及参考书目
  7. 《逆向工程核心原理》学习笔记 破解abex' crackme #1
  8. 笔记本BIOS模式忘记密码与密码修改的解决办法
  9. Excel中逻辑函数和时间日期函数
  10. java定时任务增删改查_实现Quartz的动态增删改查