Linux下程序的加载、运行和终止流程

本文详细介绍了Linux系统下程序加载、运行和终止的流程,包括动态链接器的作用、程序加载时的重定位过程、用户程序的初始化与终止执行等。通过分析,揭示了操作系统、动态链接器和编译器在支持程序正确运行背后的复杂工作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

用户在编写程序时都要定义一个main()函数作为程序运行的入口。程序开始 执行时就从这个函数开始。当这个函数返回时就表明程序运行结束了。可是用户编写的 程序要能正确运行远不是这么简单。比如,我们不禁要问main()是由谁调用 的呢?当从main()返回后又运行到哪里去了呢?C++程序中定义的全局对象 是如何构造的呢?又是如何析构的呢?如果程序是动态链接的,它所依赖的共享库是 如何加载进内存的?更复杂的是,共享对象中的全局对象是如何构造的和析构的呢? 要回答这些问题,就不得不弄清程序加载、运行和终止的整个流程,从中也可以知道 系统软件(包括操作系统、动态链接器、链接编辑器和编译器)为了支持用户程序的 正确运行做了多么复杂的工作。

为了支持用户程序的正确运行需要解决以下几个重要问题:

  • 加载用户程序以及它所依赖的所有共享对象;
  • 对用户程序和共享对象进行符号解析重定 位
  • 向用户程序传递环境变量和命令行参数。
  • 根据C++标准的规定,全局对象(包括用户程序和共享库中定义的)必须 在main()执行前初始化,并在程序结束时以相反的顺序析构。

 

为了理清这些问题,下面我们来分析Linux系统下程序的运行流程。

术语

程序头(Program Header)
程序头在[gabi]的 Program Header一节中定义,是ELF文件执行视图的重 要部分。它规定了ELF文件中的哪些部分段需要加载以及加载的地址以及是否需要动 态链接器等信息。若需要动态链接器,程序头中的 PT_INTERP指定了动态 链接器的路径
初始化代码和终止代码(Initialization and Termination code)
每个可执行文件和共享对象都有初始化代码和终止代码。初始化代码在用户程 序开始执行前执行。所有的共享对象的初始化代码在可执行文件获得控制权之前执 行。终止代码则在进程退出时执行,顺序与初始化代码执行的顺序相反。共享对象 的初始化代码和终止代码由动态连接器负责执行。( Initialization and Termination Functions, [gabi])
加载时重定位(Load-time Relocation)和运行时重定位(Run-time Relocation)
加载时重定位指在动态链接器加载对象文件后就进行的重定位,而运行时重定位 是指在用户程序已开始运行后在需要的情况下进行的重定位。PLT表的重定位就属于 运行时重定位。在PLT表的帮助下,当第一次调用一个函数时进行重定位,以后再调 用时就不用重定位了。若这个函数不被调用则不需要重定位,这可省去加载时重定 位的时间。详见[abi386-4]的 Procedure Linkage Table一节。

程序运行的基本流程

首先给出一个大致的流程。

  1. 操作系统运行用户程序时将其映射到内存中;
  2. 当它看到可执行文件中的PT_INERP时,操作系统 将PT_INTERP指定的动态链接器映射进内存,并通过栈向其传递它所需要 的参数,并跳到动态链接器的入口处开始执行;
  3. 动态链接器开始自举(Bootstrap),对自己进行重定位,并开始构造符号 表;
  4. 自举完成后,动态链接器根据可执行文件.dynamic段中的DT_NEEDED元素开始加 载依赖的共享对象,并加入它的符号表。如果这个共享对象依赖其它的共享对象, 动态链接器也会加载它们。当这个过程结束时,所有需要的共享对象都已加载进内 存,动态链接器也具有了程序和所有共享库的符号表。
  5. 这时,动态链接器重新遍历共享库,并进行加载时重定位(注意加载时重定位 采用依赖图的后序遍历顺序进行。也就是说如果A对象依赖B对象,则先处理B对象再 处理A对象)。加载时重定位包括:
    • 对数据的引用,在.rel.dyn段中,需要初始化一个 GOT(在.got中)项为一个 全局符号的地址;
    • 对代码的引用(在.rel.plt段中),需要初始化一个 GOT(在.got.plt)项为PLT表中第二条指令的地址(Procedure Linkage Table, [abi386-4])。
    如果共享对象有初始化代码(在.init中,全局对象的初始化就是这样实 现的),动态链接器会执行它,并将终止代码(在.fini中,全局对象的 析构就是这样实现的)记录下来以便退出时执行。动态链接器不会执行用户程序的 初始化代码,它由用户程序的启动代码自己执行。这个过程完成后,所有的共享对 象都已重定位并初始化,动态链接器跳到用户程序的入口处开始执行。注意,为了 能在程序退出时让动态链接器有机会调用共享对象的终止代码,动态链接器会传递 一个终止函数(用以调用共享对象的终止代码)给用户程序。
  6. 用户程序开始执行。首先它注册动态链接器的终止函数和它自己的终止函数, 然后调用用户程序的初始化代码,然后调用用户定义的main()函数。 main()函数返回后,以注册的相反顺序调用终止函数(也就是说先调用 用户程序的终止函数,再调用动态链接器的终止函数),最后调 用_exit()退出进程。

详见[Levine]第10章。

下面结合实际代码给出Linux下详细的运行流程。

程序的加载

程序的加载是通过执行exec(3)系统调用实现的。当在命令行上执行一个程序 或在图形界面系统中双击一个可执行文件时最终都是通过这个系统调用来执行程序的。 执行这个系统调用后,陷入操作系统内核,由操作系统负责加载该程序文件。在操作系 统确认相关参数后,然后通过内存映射方式加载进内存。若该ELF文件是动态链接的可执行文件 (程序头中存在PT_INTERP)需要动态连接器的支持,操作系统则将该动态连 接器映射进内存,并准备好相应的环境,将控制权转移给动态连接器。若ELF文件是静 态链接的,则操作系统准备好环境后直接转移到ELF文件的入口点开始执行。详细过程 如下:

  1. 执行exec(3)调用后陷入操作系统内核,检查参数,并判断可执行文件 的类型。因为Linux支持的可执行文件不止一种类型,加载不同类型的文件方法不一 样。下面假设文件类型为ELF。
  2. 检查ELF文件格式的有效性,读入程序头(Program Header),并检查是否存在 PT_INTERP项。存在的话说明该文件是动态链接的可执行文件,需要动态 连接器的支持。
  3. 根据ELF文件程序头的信息对ELF文件进行映射,通常包括两个段:代码段和数 据段。
  4. 初始化进程运行的堆栈环境,在栈中存储环境变量、命令行参数以及需要传给 动态连接器的一些附加参数(Auxiliary Vector)。(见[abi386-4]的图3-31)
  5. 若ELF文件是静态链接的可执行文件,跳转到用户程序入口点(由其程序头定义)开始 执行;若ELF文件是动态链接的可执行文件,映射动态连接器,并跳转到动态连接器 的入口处开始执行。

 

另见[abi386-4]的第5节,[gabi]的Dynamic Linking.

运行动态连接器

对于动态链接的可执行文件,还需要动态连接器为其加载可执行文件依赖的共享对象文 件并进行符号重定位才可以执行。动态连接器的位置存储在可执行文件程序头 的PT_INTERP元素中(见[gabi]Program Header一节)。动态连接器 的运行过程如下:

  1. 动态连接器的入口是_start, 在glibc/sysdeps/i386/dl-machine.h中的RTLD_START宏中定义。它首先调 用_dl_start() (glibc/elf/rtld.c)
  2. _dl_start()首先对动态连接器自己进行重定位,最后调 用_dl_start_final() (glibc/elf/rtld.c)收集一些基本的运行时信息后 调用_dl_sysdep_start() (glibc/elf/dl-sysdep.c)
  3. _dl_sysdep_start()首先处理由操作系统建立的环境信息(Figure 3-31, p.28, [abi386-4]),设置相关参数(_dl_argc: 命令行参数的个 数,_dl_argv: 命令行参数数组,_environ: 环境数 组,_dl_auxv: 传递给动态连接器的附加参数数组),在读 入_dl_auxv数组存储的信息,最后调 用_dl_main() (glibc/elf/rtld.c)进行动态连接器的主要任务。
  4. _dl_main()非常长,主要工作是加载可执行文件依赖的所有共享对 象,构造符号表,并进行加载时重定位(有些重定位可以延迟到需要时再进行,称为 运行时重定位)。考虑到R_386_COPY(见[abi386-4]的78页)重定位类 型,要特别加载时重定位的顺序。下面是摘自_dl_main()中的一段注释。
    /* Now we have all the objects loaded. Relocate them all except for the dynamic linker itself. We do this in reverse order so that copy relocs of earlier objects overwrite the data written by later objects. We do not re-relocate the dynamic linker itself in this loop because that could result in the GOT entries for functions we call being changed, and that would break us. It is safe to relocate the dynamic linker out of order because it has no copy relocs (we know that because it is self-contained). */
    简单地说,先重定位一个对象文件所依赖的所有对象文件再重定位这个对象文件。 重定位完成后返回到_dl_sysdep_start(),然后返回 到_dl_start_final(),然后再返回到_dl_start(),继续返回 到_start
  5. _start调用动态连接器的初始化函数(以调用每个共享对象的初始化 代码),并把动态连接器的终止函数(以调用每个共享对象的终止代码)地址存 入EDX寄存器以传给可执行文件,然后跳转到可执行文件的入口处开始执行。

 

动态连接器任务完成后将控制权转移给用户程序,此时用户程序才正是开始执行。

用户程序的执行

不管用户程序是静态的还是动态的可执行文件,它们的入口处都在 _start (glibc/sysdeps/i386/elf/Start.S)。它首先设置好一些寄 存器后调用__libc_start_main() (glibc/csu/libc-start.c)__libc_start_main()主要进行以下工作:

  1. 调用__cxa_atexit() (glibc/stdlib/cxa_atexit.c)注册动态连接器通过EDX寄存器传过来的终 止函数。
  2. 调用__cxa_atexit()注册用户程序的终止函数
  3. 调用用户程序的初始化函数
  4. 调用用户提供的main()函数
  5. main()返回后调用exit() (glibc/stdlib/exit.c)exit()以注册的相反 顺序调用atexit() (glibc/stdlib/atexit.c)__cxa_atexit()注册的函数,然后调 用_exit()结束进程。

 

参考文献

  • [abi386-4] System V Application Binary Interface: Intel386 Architecture Processor Supplement, Fourth Edition.
  • [gabi4] System V Application Binary Interface, 2001.
  • [Levine] John R. Levine, Linkers and Loaders.
  • Glibc源代码

Updated: 2011-03-06 22:04:53

### 如何确保 Linux 系统中的程序不断开 SSH 连接后仍能持续运行 为了使程序在关闭 SSH 会话之后依旧能够正常运行,有几种不同的方法可供选择。 #### 方法一:使用 `nohup` 命令配合重定向输出文件 当通过 SSH 登录到远程主机并希望启动的应用能在退出终端后继续工作时,可以采用 nohup 工具。此工具允许指定的任务忽略 SIGHUP 信号(默认情况下会在用户注销时发送给所有子进程),从而防止这些任务因用户的登出而终止[^1]。具体操作如下: ```bash nohup command > output.log 2>&1 & ``` 这里 `command` 是要执行的具体指令;`>` 符号用于将标准输出重定向至名为 `output.log` 的日志文件中保存;`2>&1` 表示把错误信息也写入同一文件内;最后面的 `&` 则表示该作业将在后台被执行。 然而,在某些环境中仅依靠 nohup 可能不足以完全解决问题,因为尽管它可以保护进程免受 HUP 信号的影响,但如果网络连接意外中断,则可能会导致其他类型的信号被传递给目标进程进而造成其异常结束的情况发生。 #### 方法二:利用 screen 或 tmux 创建持久化会话 对于更稳定的解决方案来说,推荐使用像 GNU Screen 或 Tmux 这样的多路复用器创建独立于当前登录会话之外的新 shell 会话。这类软件可以在本地建立一个新的虚拟控制台环境,并将其与实际物理设备解耦合起来,即使客户端突然掉线也不会影响正在其中运行的各种应用程序服务的状态。 以 **tmux** 为例: ```bash # 开始新的 session 并命名为 mysession tmux new-session -s mysession # 执行所需命令... # 脱离当前 session (按 Ctrl+b 键后再按 d) Ctrl+b d ``` 此时即可安全地断开端口而不必担心任何正在进行的工作会被迫停止。稍后可以通过下面这条语句重新接入之前留下的位置: ```bash tmux attach-session -t mysession ``` 这种方法不仅解决了由于意外断网所引发的一系列麻烦事端,同时也提供了更多实用特性比如水平/垂直分割窗格、复制粘贴模式等功能来提升工作效率[^4]。 #### 方法三:设置系统级别的守护进程管理机制 如果计划长期部署某项服务的话,那么最理想的方式莫过于借助 systemd 单元定义文件的形式注册成为正式的服务单元了。这样做不仅可以实现开机自启的效果,而且还能享受到诸如自动重启失败实例之类的高级功能保障业务连续性不受干扰。 编写一个简单的 .service 文件放置于 `/etc/systemd/system/` 下: ```ini [Unit] Description=My Long Running Service [Service] ExecStart=/path/to/executable Restart=always [Install] WantedBy=multi-user.target ``` 完成配置编辑以后记得刷新 daemon 配置缓存以便加载最新的修改内容: ```bash sudo systemctl daemon-reload ``` 随后就可以按照常规流程启用以及查询对应 service 的状态啦! ```bash sudo systemctl start myservicename.service sudo systemctl status myservicename.service ``` 综上所述,针对不同场景需求可以选择合适的技术手段达成目的——短期临时性的任务适合选用前两种方案之一;而对于那些期望获得更高稳定性可靠度的关键型应用而言则建议优先考虑第三种方式即集成进操作系统自带的服务管理体系当中去[^2]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值