专栏首页Linux内核及编程语言底层相关技术研究精致全景图 | 程序是如何运行起来的

精致全景图 | 程序是如何运行起来的

因为图片比较大,微信公众号上压缩的比较厉害,所以很多细节都看不清了,我单独传了一份到github上,想要原版图片的,可以点击下方的阅读原文,或者直接使用下面的链接,来访问github:

https://github.com/wangyuntao/linux-kernel-illustrated

另外,精致全景图系列文章,以及之后的linux内核分析文章,我都会整理到这个github仓库里,欢迎大家star收藏。


相信很多同学都会有疑问,一个程序是如何运行起来的,为什么我们在shell中执行了一个程序,它的main函数就会被调用呢?在main函数被调用之前及之后,又经历了什么呢?

今天我们就来详细的说下这个问题。

还是和之前一样,我画了一张程序运行的全景图,在上图中,一个程序运行所经历的代码段,我都标注了其所在的git仓库、源文件、及函数名,想要自己看源码的,可以参考下上图中的这些信息。

我们先从整体上讲一下这张图。

在linux下,我们一般都是通过shell来执行程序的。

shell其实也是一个普通的程序,它也有自己的main函数,它在正常运行后,会通过调用read_command函数,来等待用户输入命令。

在接收到用户输入的命令后,shell会先使用fork系统调用,创建一个子进程,然后再在这个子进程中,通过execve系统调用,执行最终的用户程序。

在子进程执行用户程序期间,shell主进程会调用waitpid函数,阻塞等待子进程的完成,子进程完成之后,waitpid从阻塞状态中返回,且status参数中会带着子进程的退出码,这个退出码会在后续的逻辑中被保存起来,供用户查询。

之后,shell主进程进入到下一次循环,继续等待用户输入命令并执行。

以上就是shell的主体逻辑,对应于上面全景图中的蓝色部分。

下面我们再来看下linux内核中有关execve系统调用的代码,也就是上面全景图中的绿色部分。

shell通过execve系统调用,告知linux内核,要在当前进程中执行目标程序,linux内核经过层层代码,最终到达load_elf_binary函数。

该函数是整个系统调用中最核心的一段逻辑,它主要用来为目标程序准备各种执行环境。

比如,映射代码区、数据区等到当前进程的虚拟地址空间,将程序名、环境变量、程序参数、及各种其他数据,有规律的压入到新分配的栈中,等等。

之后,load_elf_binary函数会调用start_thread,进而会调用start_thread_common函数。

在该函数里,会将返回到用户区之后,要执行的,用户区程序的起始地址,设置到regs->ip里,同时也会将上面新初始化好的,用户堆栈的栈顶地址,设置到regs->sp里。

当execve系统调用返回到用户区之后,regs->ip和regs->sp里的值,会分别赋值到rip和rsp寄存器里,这样指定的用户程序就可以继续执行了。

这一流程我们在之前的文章 精致全景图 | 系统调用是如何实现的 中讲过,这里就不再赘述。

不过这里还是有一点需要注意,就是设置到regs->ip中的地址,并不是我们自己程序的起始地址,而是动态链接器 /lib64/ld-linux-x86-64.so.2 的起始地址。

之所以要设置动态链接器的起始地址,是因为我们需要在返回到用户区之后,让其可以继续为我们的程序准备执行环境,比如,帮忙加载程序依赖的各种动态链接库等。

在动态链接器为我们的程序准备好执行环境之后,它会从进程堆栈的auxiliary vector区,取出最终用户程序的真正起始地址,并跳转到该位置开始执行。

auxiliary vector区存放的用户程序的起始地址,是上面linux内核初始化堆栈时设置的。

动态链接器相关的代码就是这些,它对应于上面全景图中紫色的部分。

在跳转到我们自己程序的起始地址后,首先执行的并不是我们写的main函数,而是glibc里名为_start的一段汇编代码。

这段汇编代码也比较简单,主要是从堆栈中获取main函数所需的argc,argv等参数,然后最终调用我们写的main函数。

当main函数返回之后,glibc里的后续代码,会将main函数的返回值,当作该进程的退出码,然后调用exit结束该进程。

这些代码对应于上面全景图中的粉色部分。

进程调用exit退出之后,shell主进程也会从waitpid的阻塞状态中返回,然后继续进行下一次循环。

以上就是程序完整的启动和结束流程。

下面我们来看下具体的源码实现。

注意,为了方便理解,很多代码我们都做了删减。

首先是shell部分,shell是一个普通的程序,它也有自己的main函数:

该函数里调用了reader_loop:

reader_loop的主体逻辑是,在while循环里不断的使用read_command函数读取用户输入的命令,然后使用execute_command执行该命令。

execute_command函数经过层层代码后,会使用下图中的fork,创建一个子进程:

然后在该子进程中,使用execve系统调用,告知linux内核,用当前子进程执行新的用户程序:

在shell主进程中,会调用waitpid函数,阻塞等待子进程的完成:

当子进程退出后,waitpid会从阻塞状态中返回,并在status里携带子进程的退出码,之后shell主进程又返回上面的read_command函数,继续等待用户下一条命令的输入。

以上就是bash的主体逻辑,对应于上面全景图中的蓝色部分。

下面我们继续看全景图中的绿色部分,也就是linux内核中有关execve的代码。

当shell的子进程执行execve函数时,linux内核中对应的系统调用被触发:

沿着函数的调用链,我们会找到一个名为do_execveat_common的函数,在该函数中,会将目标程序的文件名、环境变量、及各种程序参数等字符串,拷贝到新创建的用户堆栈区:

此时,新创建的堆栈区里内容,就如上面全景图中右下角的a1-a9, b1-b8部分构成的二维网格区域里所示的内容。

其中,黄色区域里存放的是程序参数 ./a.out hello world,蓝色区域里存放的是环境变量 SHLVL=2, HOME=/, TERM=linux, PWD=/,橘黄色区域里存放的是要执行的程序文件名 ./a.out。

这些内容和我们执行的测试程序,及其所处的环境也正好一样:

继续沿着内核函数调用链,我们最终会来到load_elf_binary函数,该函数是整个系统调用的核心。

由于linux上执行的程序基本上都是elf格式,所以内核选择的加载函数是load_elf_binary,看这个函数时,可以参考elf格式的man文档:

https://man.archlinux.org/man/elf.5

该函数比较复杂,我对其做了大量删减,并添加了很多注释:

该函数最后会调用start_thread函数,进而会调用start_thread_common函数:

这个函数重点需要注意的是对regs->ip和regs->sp的赋值,其作用在load_elf_binary函数的截图中已经注释过了,就是在返回到用户区之后,这两个字段的值会被分别拷贝到rip和rsp寄存器里,所以这里的赋值,就相当于在返回用户区之后,对rip和rsp寄存器的赋值,这个在 精致全景图 | 系统调用是如何实现的 有讲。

到这里内核部分的代码就都已经结束了。

由load_elf_binary函数截图中可见,regs->ip中设置的地址是elf_entry,即动态链接器的起始地址,而不是我们自己程序的起始地址。

原因是,我们还需要动态链接器继续帮我们准备执行环境,比如帮我们加载程序依赖的动态链接库等。

所以在execve系统调用返回到用户区之后,代码流程就进入到了动态链接器里的逻辑,即上面全景图中的紫色区域:

上图中的_start是动态链接器的起始执行地址,这个可以通过下面的方式来确认:

在_start函数中,先将rsp寄存器的值,即上面内核新初始化的堆栈的栈顶地址,赋值到rdi中,然后再使用call指令,调用_dl_start函数。

之所以要赋值到rdi寄存器中,是因为c语言的calling convention约定好的,用此方式来传递参数。

再看_dl_start函数:

该函数调用了_dl_start_final,返回一个地址,这个地址就是我们自己程序的起始地址。

再看_dl_start_final:

该函数又调用了_dl_sysdep_start:

在这里,动态链接器通过内核初始化的堆栈区中的auxiliary vector,找到最终用户程序的起始执行地址。

再之后,动态链接器的函数调用链依次退出,最终返回到上面的_start函数。

_start函数之后会顺序执行_dl_start_user,相关代码也在上面的_start函数的截图里。

其逻辑是,先将rax中的值,即_dl_start函数返回的最终用户程序的起始地址,赋值到r12寄存器中,然后再jmp到r12寄存器指向的地址,即开始执行最终的用户程序逻辑。

至于rax中的值,为什么是_dl_start函数返回的地址,这个其实也是 c calling convention 中的约定,感兴趣可以自己查下。

以上就是动态链接器的全部逻辑,其对应于全景图中的紫色部分。

最后,逻辑进入到了全景图中的粉色部分。

动态链接器从内核设置的auxiliary vector中,获取的用户程序的起始地址,还并不是我们的main函数,而是glibc中一段名为_start的代码,这个可以通过下面的方式确认:

该_start代码段内容如下:

它从堆栈中获取到argc和argv,然后调用__libc_start_main:

在__libc_start_main里,才真正的调用了我们写的main函数。

当main函数返回之后,__libc_start_main里用main函数返回的值,作为该进程的退出码,然后调用exit退出当前进程。

当该进程退出后,shell主进程也从waitpid的阻塞状态返回,并携带用户程序的退出码。

在上面全景图这个示例中,返回码为99:

之后,shell主进程又进入到下一次循环,继续等待用户命令并执行,也就是说,又进入到全景图中的蓝色部分。

至此,在linux上执行程序的流程,就形成了一个完整闭环。

你,学废了吗?

能看到这里的,都是真爱了,给个赞再走吧

另外,没有关注我公众号的也可以关注下,一起来探索linux内核里的神秘世界

本文分享自微信公众号 - Linux内核及JVM底层相关技术研究(ytcode),作者:KINGYT

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-02-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 精致全景图 | 系统调用是如何实现的

    这张图画了挺久的,主要是想让大家可以从全局角度,看下linux内核中系统调用的实现。

    KINGYT
  • dotnet core 应用是如何跑起来的 通过AppHost理解运行过程

    在 dotnet 的输出路径里面,可以看到有一个有趣的可执行文件,这个可执行文件是如何在框架发布和独立发布的时候,找到 dotnet 程序的运行时的,这个可执行...

    林德熙
  • dotnet core 应用是如何跑起来的 通过自己写一个 dotnet host 理解运行过程

    在上一篇博客是使用官方提供的 AppHost 跑起来整个 dotnet 程序。本文告诉大家在 dotnet 程序运行到托管代码之前,所需要的 Native 部分...

    林德熙
  • 干货 | OCR技术在携程业务中的应用

    袁秋龙,携程度假大数据AI研发团队实习生,专注于计算机视觉的研究和应用。在实习期间致力于度假图像智能化工作,OCR问题为实习期主要做的研究。

    携程技术
  • 场景几何约束在视觉定位中的探索

    视觉定位是自动驾驶和移动机器人领域的核心技术之一,旨在估计移动平台当前的全局位姿,为环境感知和路径规划等其他环节提供参考和指导。美团无人配送团队长期在该方面进行...

    美团无人配送
  • 知易Cocos2D-iPhone 游戏开发教程006

    在前一章中,我们谈到游戏的场景滚动主要包括3种类型:纵向、横向、纵横向。无论何种画面滚动方式,都需要实现主角在地图中的游历。在游历的过程中需要判断:  1) 是...

    全栈程序员站长
  • 最新综述 | 基于深度学习的SLAM方法:面向空间机器智能时代

    A Survey on Deep Learning for Localization and Mapping Towards the Age of Spatia...

    用户1150922
  • 场景几何约束在视觉定位中的探索

    视觉定位是自动驾驶和移动机器人领域的核心技术之一,旨在估计移动平台当前的全局位姿,为环境感知和路径规划等其他环节提供参考和指导。美团无人配送团队长期在该方面进行...

    计算机视觉
  • 论文翻译 | 多鱼眼相机的全景SLAM

    提出了一种基于特征的全景图像序列同时定位和建图系统,该系统是在宽基线移动建图系统中从多鱼眼相机平台获得的.首先,所开发的鱼眼镜头校准方法结合了等距投影模型和三角...

    3D视觉工坊
  • AR Mapping:高效快速的AR建图方案

    本文仅做学术分享,如有侵权,请联系删除。欢迎各位加入免费知识星球,获取PDF论文,欢迎转发朋友圈。内容如有错误欢迎评论留言,未经允许请勿转载!

    点云PCL博主
  • 机器视觉在 3D 动画中的应用

    每当在电影出现新技术的时候,电影制作人们都会讨论这项技术的原理,在电影《攻壳机动队》中,剑道战士或倒茶艺妓等人物的实景全息图被投放到城市上空。这种展现形式其实是...

    CV君
  • 快给你的用例做减法吧

    ? 01 ? 热身:数一数你的用例数 随着互联网时代节奏的日益加快,许多产品都会在版本迭代中对功能做加法,于是累计的测试用例似乎都无可避免地越来越多。从小编自...

    腾讯移动品质中心TMQ
  • 【腾讯TMQ】快给你的用例做减法吧

    随着互联网时代节奏的日益加快,许多产品都会在版本迭代中对功能做加法,于是累计的测试用例似乎都无可避免地越来越多。从小编自己的经验,作为测试人员,最开始设计测试用...

    腾讯移动品质中心TMQ
  • 快给你的用例做减法吧

    前言 生活的智慧,有时不在于多,而在于少。 同理适用于测试用例的管理中。 一. 热身:数一数你的用例数 随着互联网时代节奏的日益加快,许多产品都会在版本迭代中对...

    腾讯移动品质中心TMQ
  • VO视觉里程计

    VO(Visual Odometry)视觉里程计是通过车载摄像头或移动机器人的运动所引起的图像的变化,以逐步估计车辆姿态的过程。

    点云PCL博主
  • 数字化营销时代:企业如何从“推时代”进阶“拉时代”

    随着互联网经济形态由消费到产业的进阶迭代,业务场景及商业逻辑从“推营销”时代向“拉营销”时代转变,推时代即平台利用信息推送的方式来获取和维系客户,拉时代则是平台...

    盈鱼MA
  • AI 加码新基建,「中国数谷」缘何选中依图?

    6月30日,贵阳市政府在贵阳召开新闻发布会,正式官宣:脸行贵阳一期项目上线,暨轨交公交刷脸正式开通及一脸黔行APP上线。这也将作为贵阳数博会的重要部分。

    AI掘金志
  • 基于深度学习的视觉里程计算法

    近年来,视觉里程计广泛应用于机器人和自动驾驶等领域,传统方法求解视觉里程计需基于特征提取、特征 匹配和相机校准等复杂过程,同时各个模块之间要耦合在一起才能达到...

    苏州程序大白
  • 曹旭东7000字剖析:无人驾驶端到端的学习(end-to-end learning)靠谱吗?

    前天,雷锋网撰文《爆料:曹旭东创立自动驾驶公司Momenta 首次公开项目细节》,正式公布曹旭东及其创业项目Momenta,此项目致力于打造自动驾驶大脑,核心技...

    AI科技评论

扫码关注云+社区

领取腾讯云代金券