0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

一个程序是如何运行起来的

Linux阅码场 来源:卯时卯刻 作者:KINGYT 2021-10-12 17:48 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

相信很多同学都会有疑问,一个程序是如何运行起来的,为什么我们在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函数:

372bd54e-22a0-11ec-82a8-dac502259ad0.png

该函数里调用了reader_loop:

373c6454-22a0-11ec-82a8-dac502259ad0.png

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

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

3788521a-22a0-11ec-82a8-dac502259ad0.png

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

37c7b0b8-22a0-11ec-82a8-dac502259ad0.png

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

37d9e45e-22a0-11ec-82a8-dac502259ad0.png

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

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

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

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

37eb0efa-22a0-11ec-82a8-dac502259ad0.png

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

3822e884-22a0-11ec-82a8-dac502259ad0.png

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

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

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

3857cfe0-22a0-11ec-82a8-dac502259ad0.png

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

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

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

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

387b14fa-22a0-11ec-82a8-dac502259ad0.png

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

38a84222-22a0-11ec-82a8-dac502259ad0.png

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

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

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

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

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

38b7ad3e-22a0-11ec-82a8-dac502259ad0.png

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

38fd1e50-22a0-11ec-82a8-dac502259ad0.png

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

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

再看_dl_start函数:

391105f0-22a0-11ec-82a8-dac502259ad0.png

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

再看_dl_start_final:

393b5134-22a0-11ec-82a8-dac502259ad0.png

该函数又调用了_dl_sysdep_start:

3974678a-22a0-11ec-82a8-dac502259ad0.png

在这里,动态链接器通过内核初始化的堆栈区中的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的代码,这个可以通过下面的方式确认:

39c36312-22a0-11ec-82a8-dac502259ad0.png

该_start代码段内容如下:

39f2140a-22a0-11ec-82a8-dac502259ad0.png

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

3a222078-22a0-11ec-82a8-dac502259ad0.png

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

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

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

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

3a58d370-22a0-11ec-82a8-dac502259ad0.png

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

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

你,学废了吗?

责任编辑:haq

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 程序
    +关注

    关注

    117

    文章

    3836

    浏览量

    84745
  • Shell
    +关注

    关注

    1

    文章

    373

    浏览量

    25164

原文标题:精致全景图 | 程序是如何运行起来的

文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    MCU代码需要搬到RAM中才能运行吗?不这样做会有什么不妥嘛?

    大部分单片机的代码直接在nor flash中运行,少部分需要加载到ram中。 nor flash可以直接寻址一个字节,可以找到指令的具体地址,因此可以直接
    发表于 12-04 07:39

    Linux 下交叉编译实战:跑起来你的第一个 STM32 程序

    起来你的第一个STM32程序、准备工作在开始之前,需要准备:1、Linux开发环境Ubuntu、Debian或其他主流发行版都可以。2、ARMGCC交叉编译工具
    的头像 发表于 11-24 19:04 225次阅读
    Linux 下交叉编译实战:跑<b class='flag-5'>起来</b>你的第<b class='flag-5'>一个</b> STM32 <b class='flag-5'>程序</b>

    如何自己设计基于RISC-V的SoC架构,最后可以在FPGA上跑起来

    如何自己设计基于RISC-V的SoC架构,最后可以在FPGA上跑起来
    发表于 11-11 08:03

    大数组程序无法运行怎么解决?

    主控是103,程序中定义const类型 128k只读数组,放在flash上,程序无法运行,堆栈都初始化不了,在keil编译下正常,在rt
    发表于 09-15 06:21

    同样的代码在官方开发板上运行正常,在自己板子上就跑不起来,怎么办?

    UART交互,CLI代码所在目录为:SDK安装目录examplesperipheralcli。如果这2程序运行正常,说明你的焊接问题
    的头像 发表于 05-12 15:26 574次阅读
    同样的代码在官方开发板上<b class='flag-5'>运行</b>正常,在自己板子上就跑不<b class='flag-5'>起来</b>,怎么办?

    如何在 树莓派 上编写和运行 C 语言程序

    在本教程中,我将讨论C编程语言是什么,C编程的用途,以及如何在RaspberryPi上编写和运行C程序。本文的目的是为您介绍在RaspberryPi上进行C编程的基础知识。如果您想深入了解C编程
    的头像 发表于 03-25 09:28 934次阅读
    如何在 树莓派 上编写和<b class='flag-5'>运行</b> C 语言<b class='flag-5'>程序</b>?

    零基础入门:如何在树莓派上编写和运行Python程序

    在这篇文章中,我将为你简要介绍Python程序是什么、Python程序可以用来做什么,以及如何在RaspberryPi上编写和运行简单的
    的头像 发表于 03-25 09:27 1509次阅读
    零基础入门:如何在树莓派上编写和<b class='flag-5'>运行</b>Python<b class='flag-5'>程序</b>?

    STM32F103RBT6开发板每次程序都得重新烧录才能正常运行,如何解决?

    每次板子都得重新烧录程序才能运行起来。第次烧录完程序后,能正常运行,等我把开关断开再打开,
    发表于 03-11 07:40

    FLASHXIP程序跑不起来是怎么回事?

    1、环境是: Nuclei Studio IDE for C/C++ Developers Version: 2023-10 2、开发板是正点原子达芬奇 这是跑不起来程序,不知道是什么原因还望高手指导
    发表于 03-07 14:20

    用Labview写电子称的485串口程序

    关键词:Labview + 串口程序 232、485串口通讯是最常见的仪器仪表通讯方式之,本文详细介绍,用Labview编写电子秤的485串口
    的头像 发表于 03-06 09:54 1479次阅读
    用Labview写<b class='flag-5'>一</b><b class='flag-5'>个</b>电子称的485串口<b class='flag-5'>程序</b>

    用DLPA3000+DLPC3478+MSP430+SII1161(HDMI Reciever)设计了产品,如何将板子运行起来呢?

    SII1161上有EEPROM, 里面的文件我们可以自己搞定; 2. MSP430 上带了SPI FLASH,这里面的程序从那里获取呢?不烧录是不是板子就运行
    发表于 02-28 06:39

    KS-Soft:站式集合20TCP/IP实用程序

    IP-Tools 在程序中提供了许多 TCP/IP 实用程序。 这个屡获殊荣的程序可以在Windows XP/7/8/10,Window
    的头像 发表于 02-11 11:09 580次阅读
    KS-Soft:<b class='flag-5'>一</b>站式集合20<b class='flag-5'>个</b>TCP/IP实用<b class='flag-5'>程序</b>

    用5509A写用MCBSP和AIC23采集和播放音频的程序,在运行的时候发出了很大的杂音,为什么?

    AIC23提供。(经过测试,FSR=FSX=44.1K) 3.音频采样率为44.1K,DSP MODE,一个字长16bit。 4.用BYPASS方式运行,没有任何杂音,很清晰。 5.LINEIN不接电
    发表于 02-05 07:12

    使用msp430运行程序时,出现“Error initializing emulator: No USB FET was found”怎么解决?

    我使用msp430运行程序时,出现“Error initializing emulator: No USB FET was found”,请问这个该怎么解决呢? 之前在ti.co
    发表于 01-16 07:59

    不停的malloc程序会异常吗

    用完,程序应该会异常退出。 于是我找了环境,把程序运行起来试下。 先用虚拟机,跑的
    的头像 发表于 01-14 09:17 840次阅读