linux内核进程的创建

以C语言为例,我们在Linux下编写C语言代码,然后通过gcc编译和链接生成可执行文件后直接执行即可完成一个进程的创建和工作。下面将详细展开介绍这个创建进程的过程。在 Linux 下面,二进制的程序也要有严格的格式,这个格式我们称为 ELF(Executable and Linkable Format,可执行与可链接格式)。这个格式可以根据编译的结果不同,分为不同的格式。主要包括

  1. 可重定位的对象文件(Relocatable file),由汇编器汇编生成的 .o 文件

  2. 可执行的对象文件(Executable file),可执行应用程序

  3. 可被共享的对象文件(Shared object file),动态库文件,也即 .so 文件

下面在进程创建过程中会详细说明三种文件。

编译

写完C程序后第一步就是程序编译(其实还有IDE的预编译,那些属于编辑器操作这里不表)。编译指令如下所示

    gcc -c -fPIC xxxx.c

-c表示编译、汇编指定的源文件,不进行链接。-fPIC表示生成与位置无关(Position-Independent Code)代码,即采用相对地址而非绝对地址,从而满足共享库加载需求。在编译的时候,先做预处理工作,例如将头文件嵌入到正文中,将定义的宏展开,然后就是真正的编译过程,最终编译成为.o 文件,这就是 ELF 的第一种类型,可重定位文件(Relocatable File)。之所以叫做可重定位文件,是因为对于编译好的代码和变量,将来加载到内存里面的时候,都是要加载到一定位置的。比如说,调用一个函数,其实就是跳到这个函数所在的代码位置执行;再比如修改一个全局变量,也是要到变量的位置那里去修改。但是现在这个时候,还是.o 文件,不是一个可以直接运行的程序,这里面只是部分代码片段。因此.o 里面的位置是不确定的,但是必须是可重新定位的以适应需求。

linux内核进程的创建

ELF 文件的头是用于描述整个文件的。这个文件格式在内核中有定义,分别为 struct elf32_hdr 和struct elf64_hdr。其他各个section作用如下所示

  • .text:放编译好的二进制可执行代码
  • .rodata:只读数据,例如字符串常量、const 的变量
  • .data:已经初始化好的全局变量
  • .bss:未初始化全局变量,运行时会置 0
  • .symtab:符号表,记录的则是函数和变量
  • .rel.text: .text部分的重定位表
  • .rel.data:.data部分的重定位表
  • .strtab:字符串表、字符串常量和变量名

这些节的元数据信息也需要有一个地方保存,就是最后的节头部表(Section Header Table)。在这个表里面,每一个 section 都有一项,在代码里面也有定义 struct elf32_shdr和struct elf64_shdr。在 ELF 的头里面,有描述这个文件的节头部表的位置,有多少个表项等等信息。

链接

链接分为静态链接动态链接。静态链接库会和目标文件通过链接生成一个可执行文件,而动态链接则会通过链接形成动态连接器,在可执行文件执行的时候动态的选择并加载其中的部分或全部函数。二者的各自优缺点如下所示

  • 静态链接库的优点

(1) 代码装载速度快,执行速度略比动态链接库快;

(2) 只需保证在开发者的计算机中有正确的.LIB文件,在以二进制形式发布程序时不需考虑在用户的计算机上.LIB文件是否存在及版本问题,可避免DLL地狱等问题。

  • 静态链接库的缺点

使用静态链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费

  • 动态链接库的优点

(1) 更加节省内存并减少页面交换;

(2) DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性;

(3) 不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数;

(4)适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。

  • 动态链接库的缺点

使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。而使用运行时动态链接,系统不会终止,但由于DLL中的导出函数不可用,程序会加载失败;速度比静态链接慢。当某个模块更新后,如果新模块与旧的模块不兼容,那么那些需要该模块才能运行的软件均无法执行。这在早期Windows中很常见。

下面分别介绍静态链接和动态链接

静态链接

静态链接库.a文件(Archives)的执行指令如下

    ar cr libXXX.a XXX.o XXXX.o

当需要使用该静态库的时候,会将.o文件从.a文件中依次抽取并链接到程序中,指令如下

    gcc -o XXXX XXX.O -L. -lsXXX

-L表示在当前目录下找.a 文件,-lsXXXX会自动补全文件名,比如加前缀 lib,后缀.a,变成libXXX.a,找到这个.a文件后,将里面的 XXXX.o 取出来,和 XXX.o 做一个链接,形成二进制执行文件XXXX。在这里,重定位会从.o中抽取函数并和.a中的文件抽取的函数进行合并,找到实际的调用位置,形成最终的可执行文件(Executable file),即ELF的第二种格式文件。

linux内核进程的创建

对比ELF第一种格式可重定位文件,这里可执行文件略去了重定位表相关段落。此处将ELF文件分为了代码段、数据段和不加载到内存中的部分,并加上了段头表(Segment Header Table)用以记录管理,在代码中定义为struct elf32_phdr和 struct elf64_phdr,这里面除了有对于段的描述之外,最重要的是 p_vaddr,这个是这个段加载到内存的虚拟地址。这部分会在内存篇章详细介绍。

动态链接

动态链接库(Shared Libraries)的作用主要是为了解决静态链接大量使用会造成空间浪费的问题,因此这里设计成了可以被多个程序共享的形式,其执行命令如下

    gcc -shared -fPIC -o libXXX.so XXX.o

当一个动态链接库被链接到一个程序文件中的时候,最后的程序文件并不包括动态链接库中的代码,而仅仅包括对动态链接库的引用,并且不保存动态链接库的全路径,仅仅保存动态链接库的名称。

    gcc -o XXX XXX.O -L. -lXXX

当运行这个程序的时候,首先寻找动态链接库,然后加载它。默认情况下,系统在 /lib 和/usr/lib 文件夹下寻找动态链接库。如果找不到就会报错,我们可以设定 LD_LIBRARY_PATH环境变量,程序运行时会在此环境变量指定的文件夹下寻找动态链接库。动态链接库,就是 ELF 的第三种类型,共享对象文件(Shared Object)。

动态链接的ELF相对于静态链接主要多了以下部分

  • .interp段,里面是ld-linux.so,负责运行时的链接动作
  • .plt(Procedure Linkage Table),过程链接表
  • .got.plt(Global Offset Table),全局偏移量表

当程序编译时,会对每个函数在PLT中建立新的项,如PLT[n],而动态库中则存有该函数的实际地址,记为GOT[m]。整体寻址过程如下所示

  • PLT[n]向GOT[m]寻求地址
  • GOT[m]初始并无地址,需要采取以下方式获取地址
    • 回调PLT[0]
    • PLT[0]调用GOT[2],即ld-linux.so
    • ld-linux.so查找所需函数实际地址并存放在GOT[m]中

由此,我们建立了PLT[n]到GOT[m]的对应关系,从而实现了动态链接。

加载运行

完成了上述的编译、汇编、链接,我们最终形成了可执行文件,并加载运行。在内核中,有这样一个数据结构,用来定义加载二进制文件的方法。

struct linux_binfmt {
    struct list_head lh;
    struct module *module;
    int (*load_binary)(struct linux_binprm *);
    int (*load_shlib)(struct file *);
    int (*core_dump)(struct coredump_params *cprm);
    unsigned long min_coredump;     /* minimal dump size */
} __randomize_layout;

对于ELF文件格式,其对应实现为

static struct linux_binfmt elf_format = {
    .module         = THIS_MODULE,
    .load_binary    = load_elf_binary,
    .load_shlib     = load_elf_library,
    .core_dump      = elf_core_dump,
    .min_coredump   = ELF_EXEC_PAGESIZE,
};

其中加载的函数指针指向的函数和内核镜像加载是同一份函数,实际上通过exec函数完成调用。exec 比较特殊,它是一组函数:

  • 包含 p 的函数(execvp, execlp)会在 PATH 路径下面寻找程序;不包含 p 的函数需要输入程序的全路径;
  • 包含 v 的函数(execv, execvp, execve)以数组的形式接收参数;
  • 包含 l 的函数(execl, execlp, execle)以列表的形式接收参数;
  • 包含 e 的函数(execve, execle)以数组的形式接收环境变量。

当我们通过shell运行可执行文件或者通过fork派生子类,均是通过该类函数实现加载。

酷客网相关文章:

赞(0)

评论 抢沙发

评论前必须登录!