linux II: 程序加载与执行

2024-04-09

前言

想要了解linux的基础概念,可以先看看https://wsxk.github.io/linux_basic/

11. program加载与执行过程

当你在一个shell中执行一个程序时,你不会好奇:这个程序是如何被加载然后执行的吗?
以执行/bin/cat为例,程序会执行如下7个步骤

1. A process is created. 进程创建
2. Cat is loaded. Cat程序被加载
3. Cat is initialized. Cat程序被初始化
4. Cat is launched.  Cat程序被运行
5. Cat reads its arguments and environment. Cat程序读取参数和环境变量
6. Cat does its thing. Cat程序开始做正式工作
7. Cat terminates.  Cat程序结束运行

11.1 进程创建

linux系统中,进程都是通过分裂来进行传播的,具体而言,当我们在terminal(bash)中执行cat xxx时,父进程(bash)会通过系统调用fork()或者clone()创建跟父进程近乎一样的子进程,随后,子进程会通过系统调用execve()来把自身替换成其他进程,在这个例子中就是cat

11.2 进程加载

在执行execve()这个动作时,kernel会检测文件是否可执行,即executable权限,如果不可执行,那么execve()系统调用会失败
在确定文件是可执行的后,kernel还会为了确定加载什么内容而做如下图所示的检测

  • 1 首先判断文件是否以#!开头,如果是,kernel会提取该行接下来的内容,并将其当作解释器用来执行,原始的命令作为解释器的参数(直接跟在解释器后面)

例子:
在这个例子,文件以#!开头,kernel提取/bin/echo作为解释器,即运行这个程序
此时命令相当于:

/bin/echo ./some-script

因此some-script文件中的echo hi就不会打印出来
这个过程也可以是递归的,再看一个例子:
在这个例子中,some-script2中的解释器是some-script,在执行命令./some-script2时,相当于:

./some-script ./some-script2

而在运行./some-script ./some-script2相当于

/bin/echo ./some-script ./some-script2

十分神奇!

  • 2 如果文件的格式满足 /proc/sys/fs/binfmt_misc中的内容,kernel会执行特定格式对应的解释器,原始的命令作为解释器的参数

举个例子:
在这里,如果文件的开头是550d0d0a,那么就用/usr/bin/python3.8来执行这个文件

  • 3 如果文件是动态链接的elf文件,kernel会选定elf文件中loader定义的值来作为解释器,加载loader和原始文件,并让loader来进行控制

loader是很重要的,如果elf文件是动态链接的,loader会负责so的地址分配,符号解析,重定位,合并段….等等用途,保证elf能够顺利执行
loader可以通过 readelf -a /bin/cat | grep interpreter来查询
下面这张图也能体现动态链接elf的加载过程

例子又来了:
gcc -shared -o preload.so preload.c将其编译出so文件
我们可以用strace -E LD_PRELOAD=./preload.so ./cat cat.c 2>&1 | head -n 100来查看先后调用关系

如果用的是strace -E LD_LIBRARY_PATH=/some/lib ./cat cat.c来追踪的话:
其他的路径可以自行实验~

  • 4 如果文件是静态链接的elf,kernel会直接加载它

很直接,没有啥好说的

  • 5 其他遗留的文件格式会被检查

这个也很直接

11.3 进程初始化

Every ELF binary can specify constructors, which are functions that run before the program is actually launched.
即程序在运行前,可以执行一些构造函数
例子:
在二进制的实现:
我们可以发现,haha函数被放在了init_array中

11.4 程序被发起

其实通过逆向就可以知道,main函数并不是第一个被执行的程序
正常的elf程序都会自动调用libc 中的__libc_start_main()函数
其实11.3中的初始化的构造函数也会作为参数被放入__libc_start_main()函数中被执行

11.5 程序读取环境变量和参数

main函数的int main(int argc, void **argv, void **envp);中,argv是参数,envp是环境变量
PS: 如果你想要程序在一个没有环境变量的环境下运行,可以使用 env -i ./program

11.6 程序执行正常功能

执行正常功能没什么好说的,要提到的点是:

  1. elf文件中的导入符号必须通过动态库(libc)的导出符号来解析
  2. 几乎所有程序都要和外界交互,交互基本上要用到 system call(系统调用)
  3. 另一个用到的就是signals,即信号,需要用到sighandler_t signal(int signum, sighandler_t handler)来注册

    信号会让程序执行暂时中断,并运行handler函数 hanlder函数是一种 函数,只有一个参数: signal的值 没有特定的handler来处理信号,会默认调用kill来终止程序 signal 9(SIGKILL)和 signal 19(SIGSTOP)无法被handled

  4. 最后一个用到的是 共享内存(shared memory)用于不同进程通信,建立时需要system call,建立之后就不再需要用到system call

11.7 程序结束

程序只有两种情况会结束运行:

  1. 收到没有handler的signal
  2. 调用了 exit()这个 system call

注意:所有的程序在结束后,会保持僵尸状态被其父进程回收,如果父进程已经停止生命周期了,其会被PID 1回收