进程:公司接这么多项目,如何管
- 软件平台:运行于VMware Workstation 12 Player下UbuntuLTS16.04_x64 系统
- 开发环境:Linux-4.19-rc3内核,glibc-2.9
1、编译—程序的二进制格式(.o ELF文件的第一种格式)
1.1 生成方式
执行gcc -o xxx.o xxx.c
,最终生成的.o
文件——这就是ELF的第一种类型,可重定位文件(Relocatable File)。
1.2 文件格式(存储方式)
如下图所示:由ELF头,多个节(Section)与节头部表(Section Header Table)构成
-
对于一开始的**
ELF Header
文件头,其用于描述整个文件**,在内核中有struct elf32_hdr
与struct elf64_hdr
结构体描述,形式如下:-
位置:
include\uapi\linux\elf.h
-
源码:可以看到,32位机与64位机在文字描述上无任何区别,由于位数的增加,结构体中每项变量的大小的有所区别
有一项
e_entry
,也是个虚拟地址,是程序运行的入口。/* 32-bit ELF base types. */ typedef unsigned int Elf32_Addr; typedef unsigned short Elf32_Half; typedef unsigned int Elf32_Off; typedef signed int Elf32_Sword; typedef unsigned int Elf32_Word; #define EI_NIDENT 16 typedef struct elf32_hdr{ unsigned char e_ident[EI_NIDENT]; //开始的16个字节 Elf32_Half e_type; //文件类型 Elf32_Half e_machine; //运行的机器类型 Elf32_Word e_version; //版本 Elf32_Addr e_entry; //程序入口地址 Elf32_Off e_phoff; //程序头表在文件中的偏移 Elf32_Off e_shoff; //节头表在文件中的偏移 Elf32_Word e_flags; //标记 Elf32_Half e_ehsize; //elf文件头大小 Elf32_Half e_phentsize; //程序头表项的大小 Elf32_Half e_phnum; //程序头表中表项项的个数 Elf32_Half e_shentsize; //节头表项大小 Elf32_Half e_shnum; //节头表中表项的个数 Elf32_Half e_shstrndx; //节头表的字符串节所在节头表中下标 } Elf32_Ehdr; /* 64-bit ELF base types. */ typedef unsigned long long Elf64_Addr; typedef unsigned short Elf64_Half; typedef signed short Elf64_SHalf; typedef unsigned long long Elf64_Off; typedef signed int Elf64_Sword; typedef unsigned int Elf64_Word; typedef unsigned long long Elf64_Xword; typedef signed long long Elf64_Sxword; typedef struct elf64_hdr { unsigned char e_ident[EI_NIDENT]; //开始的16个字节 Elf64_Half e_type; //文件类型 Elf64_Half e_machine; //运行的机器类型 Elf64_Word e_version; //版本 Elf64_Addr e_entry; //程序入口地址 Elf64_Off e_phoff; //程序头表在文件中的偏移 Elf64_Off e_shoff; //节头表在文件中的偏移 Elf64_Word e_flags; //标记 Elf64_Half e_ehsize; //elf文件头大小 Elf64_Half e_phentsize; //程序头表项的大小 Elf64_Half e_phnum; //程序头表中表项项的个数 Elf64_Half e_shentsize; //节头表项大小 Elf64_Half e_shnum; //节头表中表项的个数 Elf64_Half e_shstrndx; //节头表的字符串节所在节头表中下标 } Elf64_Ehdr;
-
-
对于其他的项,我们称其为节(Section),可以看到其有多个节,那么怎么定位到每个节呢?由最底下的节头部表(Section Header Table)来定位,在这个表里面,每一个节都有一项,其在内核中由
struct elf32_shdr
和struct elf64_shdr
结构体描述。(在上述的ELF头,有描述这个文件的节头部表的位置,有多少个表项等等信息)。-
位置:
include\uapi\linux\elf.h
-
源码:32位机与64位机在文字描述上无任何区别,由于位数的增加,结构体中每项变量的大小的有所区别
typedef struct elf32_shdr { Elf32_Word sh_name; /* 节的名字,在符号表中的下标 */ Elf32_Word sh_type; /* 节的类型,描述符号,代码,数据,重定位等 */ Elf32_Word sh_flags; /* 读写执行标记 */ Elf32_Addr sh_addr; /* 节在执行时的虚拟地址 */ Elf32_Off sh_offset; /* 节在文件中的偏移量 */ Elf32_Word sh_size; /* 节的大小 */ Elf32_Word sh_link; /* 其它节的索引 */ Elf32_Word sh_info; /* 节的其它信息 */ Elf32_Word sh_addralign; /* 节对齐 */ Elf32_Word sh_entsize; /* 节拥有固定大小项的大小 */ } Elf32_Shdr; typedef struct elf64_shdr { Elf64_Word sh_name; /* 节的名字,在符号表中的下标 */ Elf64_Word sh_type; /* 节的类型,描述符号,代码,数据,重定位等 */ Elf64_Xword sh_flags; /* 读写执行标记 */ Elf64_Addr sh_addr; /* 节在执行时的虚拟地址 */ Elf64_Off sh_offset; /* 节在文件中的偏移量 */ Elf64_Xword sh_size; /* 节的大小 */ Elf64_Word sh_link; /* 其它节的索引 */ Elf64_Word sh_info; /* 节的其它信息 */ Elf64_Xword sh_addralign; /* 节对齐 */ Elf64_Xword sh_entsize; /* 节拥有固定大小项的大小 */ } Elf64_Shdr;
-
1.3 每个节的含义
刚刚介绍了ELF头表与节头部表,下面来介绍下每个节/段中具体存储的信息
.text
:放编译好的二进制可执行代码.data
:已经初始化好的全局变量与静态变量.rodata
:只读数据,例如字符串常量、const的变量.bss
:未初始化全局变量与静态变量,运行时会置0.symtab
(符号表):记录的则是函数和变量.rel.text
:.text节中需要重定位的信息。一般,任何调用外部函数或者引用全局变量的指令都需要被修改,但是,如果指令调用本地函数,则不需要被修改。重定位信息在可执行文件中可以不需要。.rel.data
:重定位任何被这个模块定义和引用的全局变量信息。通常在一个全局变量的值是另一个全局变量地址或外部函数地址的情况下需要重新修改这个值。.strtab
(字符串表):字符串常量和变量名
2、静态链接生成的可执行文件(ELF第二种格式)
2.1 生成方式
-
静态链接库生成方式
执行
ar -rc xxx.a xxx.o xxx.o
,可以生成一个静态链接库xxx.a
,这个静态链接库可以由一个或多个.o
文件组织起来。 -
可执行文件生成方式
执行
gcc -o xxx xxx.o -L. -lxxx
,可以根据命令,调用静态链接库,与.o
文件生成一个可执行的文件。其中**
-L
参数跟着的是库文件所在的目录名**;-l
参数用来指定程序要链接的库,即库名,例如:-lstaticprocess
,即静态链接库为libstaticprocess.a
,其到一个自动补全的功能。
2.2 文件格式(存储方式)
如下图所示,由ELF头、段头部表(Segment Header Table)与每个节组成的段构成
-
对于ELF头,其组成方式如上面结构的一样
-
对于段头部表(Segment Header Table)
因为此时文件已经是马上就可以加载到内存里面执行的文件了,因而这些section被分成了需要加载到内存里面的代码段、数据段和不需要加载到内存里面的部分,将小的section合成了大的段segment,并且在最前面加一个段头表(Segment Header Table)。
代码段 由
.text
和rodata
组成,会被加载到内存中;数据段 由
.data
和.bss
组成,会被加载到内存中;不加载到内存的段 由
.symtab
、。strtab
与Section Header Table
组成;其在内核中由
struct elf32_phdr
和struct elf64_phdr
结构体表示-
位置:
include\uapi\linux\elf.h
-
源码:32位机与64位机在文字描述上有顺序的区别,内容无区别,由于位数的增加,结构体中每项变量的大小的有所区别
最重要的是**
p_vaddr
,这个是这个段加载到内存的虚拟地址**。typedef struct elf32_phdr{ Elf32_Word p_type; /* 段的类型,LOAD,DYNAMIC等 */ Elf32_Off p_offset; /* 段在文件中的偏移量 */ Elf32_Addr p_vaddr; /* 段的物理地址 */ Elf32_Addr p_paddr; /* 段的虚拟地址 */ Elf32_Word p_filesz; /* 段在文件中的大小 */ Elf32_Word p_memsz; /* 段在内存中的大小 */ Elf32_Word p_flags; /* 读写执行标记 */ Elf32_Word p_align; /* 段的对齐 */ } Elf32_Phdr; typedef struct elf64_phdr { Elf64_Word p_type; /* 段的类型,LOAD,DYNAMIC等 */ Elf64_Word p_flags; /* 读写执行标记 */ Elf64_Off p_offset; /* 段在文件中的偏移量 */ Elf64_Addr p_vaddr; /* 段的物理地址 */ Elf64_Addr p_paddr; /* 段的虚拟地址 */ Elf64_Xword p_filesz; /* 段在文件中的大小 */ Elf64_Xword p_memsz; /* 段在内存中的大小 */ Elf64_Xword p_align; /* 段的对齐 */ } Elf64_Phdr;
-
-
对于其他节,其介绍也和上述说明一致。
2.3 静态链接库的缺点
根据上面可以知道,由于静态链接库,会根据调用关系,把代码重新组织构成可执行文件,其中会把静态链接库的源代码“内嵌”到内存中。
当多个文件使用同一个静态链接库时,会重复的加载相同的代码到内存中,若静态链接库更新后,需要重新编译链接代码,否则使用的可执行文件中调用的代码为之前加载的静态链接库的代码。
3、动态链接库——共享对象文件(Shared Object,ELF文件的第三种类型)
对于动态链接库,是通过一定的方式把所需要的段的地址存储到可执行文件中,在运行时才会跳到地址的代码中执行,类似下图。
3.1 生成方式
执行gcc -fpic -shared xxx.c -o libxxx.so
,生成libxxx.so
动态链接库;
注意,此时若需要调用该动态链接库,在链接时候需要指定动态链接库的路径或把动态链接库放入到指定目录中,否则在运行时会报错,这是为什么呢?
原因:当一个动态链接库被链接到一个程序文件中的时候,最后的程序文件并不包括动态链接库中的代码,而仅仅包括对动态链接库的引用,并且不保存动态链接库的全路径,仅仅保存动态链接库的名称。
3.2 文件格式(存储方式)
在上述可执行文件的格式基础上,有以下的不同:
-
多了一个**
.interp
的Segment**,这里面是ld-linux.so
,这是动态链接器,也就是说,运行时的链接动作都是它做的。 -
多了两个section,一个是**.plt**,过程链接表(Procedure Linkage Table,PLT),一个是**.got.plt**,全局偏移量表(Global Offset Table,GOT)
3.3 如何加载动态链接库到进程空间
这个时候就需要上述的**.plt
与.got.plt
**,下面会举例子说明,
假设如下场景:
- 可执行程序:
createprocess
- 动态库:
libcprocess.so
- 执行情况:
createprocess
执行时,需要调用libcprocess.so
中的create_process()
由于在程序编译时,create_process()
所在的位置是不知道,在程序运行时会有如下步骤:
-
建立本地代理
在PLT里面建立一项PLT[x],在二进制程序里面,而是调用PLT[x]里面的代理代码,这个代理代码会在运行的时候找真正的
create_process
函数。 -
获取代理代码
在GOT里面也会为
create_proces
s函数创建一项GOT[y]。这一项是运行时create_process
函数在内存中真正的地址。 -
获取真正函数地址
一开始,GOT[y]会回调PLT(PLT[x]里面的代理代码来找我要
create_process
函数的真实地址)然后,PLT会转而调用PLT[0],也即第一项
之后,PLT[0]转而调用GOT[2],这里面是
ld-linux.so
的入口函数,这个函数会找到加载到内存中的libdynamicprocess.so
里面的create_process
函数的地址最后,把这个地址放在GOT[y]里面。下次,PLT[x]的代理函数就能够直接调用了。
4、可执行文件如何加载到内存
-
在
include\linux\binfmts.h
中有这样一个结构体linux_binfmt
,其中的load_binary()
函数是用于加载二进制文件到内存的定义。/* * This structure defines the functions that are used to load the binary formats that * linux accepts. */ 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;
-
在
fs\binfmt_elf.c
文件中,对于elf文件格式,有如下结构体实现elf_format
。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, };
其中的
load_elf_binary
就是把elf格式文件加载到内存中的函数,在之前其调用顺序为:do_execve
-->do_execveat_common
-->__do_execve_file
-->exec_binprm
– >search_binary_handler
-->search_binary_handler
-->fmt->load_binary(bprm);
-
那么谁调用
do_execve
呢?通过source insight的搜索可以知道:在
fs\exec.c
文件中有如下定义,通过对系统调用的理解,可以知道是exec
这个系统调用最终调用的load_elf_binary
SYSCALL_DEFINE3(execve, const char __user *, filename, const char __user *const __user *, argv, const char __user *const __user *, envp) { return do_execve(getname(filename), argv, envp); }
5、exec系统调用简单介绍
exec
比较特殊,它是一组函数:
- 包含p的函数(
execvp
,execlp
)会在PATH路径下面寻找程序; - 不包含p的函数需要输入程序的全路径;
- 包含v的函数(
execv
,execvp
,execve
)以数组的形式接收参数; - 包含l的函数(
execl
,execlp
,execle
)以列表的形式接收参数; - 包含e的函数(
execve
,execle
)以数组的形式接收环境变量。
6、对于Linux操作系统,其进程树的大致结构
7、总结
用图表示一个进程从代码到二进制到运行时的一个过程:
- 通过图右边的文件编译过程,生成so文件和可执行文件,放在硬盘上。
- 左边的用户态的进程A执行fork,创建进程B,在进程B的处理逻辑中,执行exec系列系统调用。
- 这个系统调用会通过
load_elf_binary
方法,将刚才生成的可执行文件,加载到进程B的内存中执行。