一、代码编译过程图
二、代码编译及编译文件分析
比如有如下的测试代码:
process1.c
#include <stdio.h>
void local_func1(){
printf("local_func1\n");
}
void static local_func2(){
printf("local_func2\n");
}
void func1(){
printf("func1\n");
local_func1();
local_func2();
}
process2.c
#include <stdio.h>
void func2(){
printf("func2\n");
}
对process1.c和process2.c进行编译:
gcc -fPIC -o process1.o -c process1.c
gcc -fPIC -o process2.o -c process2.c
process*.o文件头结构类似于可执行文件结构(见上一篇)
其中:
1、由于只是编译,未进行链接,文件中的符号位置没固定,所以程序入口位置(Entry point address)为0
[work@bogon 10]$ readelf -h process1.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1080 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 12
下面看一下文件中section部分:
其中:
.text部分为代码部分
.rela.text为重定位节部分,包含重定位偏移位置,重定位类型(这样在链接的时候,根据该重定位表信息,重定位.text部分,同理.rela.data)
.data为全局初始化变量
.bss为全局未初始化变量,默认初始化为0
.rodata为只读数据,包括字符串常量,const变量,如下:
.comment是版本控制信息如下:
.symtab是符号表信息,为本程序导出的符号信息,如下:
.strtab符号的字符串信息,一般为符号表的字符串名称
.shstrtab section字符串信息,一般为所有的section字符串名称
[work@bogon 10]$ readelf -S process1.o
There are 13 section headers, starting at offset 0x438:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000004a 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 000002e0
00000000000000a8 0000000000000018 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 0000008a
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 0000008a
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 0000008a
000000000000001e 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 000000a8
000000000000002e 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000d6
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 000000d8
0000000000000078 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 00000388
0000000000000048 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 00000150
0000000000000150 0000000000000018 11 10 8
[11] .strtab STRTAB 0000000000000000 000002a0
000000000000003f 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 000003d0
0000000000000061 0000000000000000 0 0 1
三1.静态库.a生成及生成可执行程序
静态库里若只压一个.o,其实和.o没有区别,例如:
ar cr libprocess1.a process1.o
更多情况下,我们是把多个.o文件压到一个静态库中的,如下:
ar cr libprocess.a process1.o process2.o
我们看一下libprocess.a中内容:
发现静态库其实就是直接把.o组合在一起的
下面编译一下main.c文件
其中Type字段为NOTYPE信息的,需要在链接的时候从其他的文件中链接过来
gcc -fPIC -o main.o -c main.c
readelf -s main.o
Symbol table '.symtab' contains 19 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 6
9: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_var_init
10: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_val_uninit
11: 0000000000000000 104 FUNC GLOBAL DEFAULT 1 main
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND malloc
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND memcpy
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND free
17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND func1
18: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND func2
此时main.o也是可定位的文件,里面的虚拟地址也未指定,如下:
(e8为函数跳转指令)
下面进行静态库链接:
gcc -o main main.o -L . -lprocess
链接过程是根据可重定位文件中的可重定位表进行重定位,此时对符号进行了定位,分配了虚拟地址:
三2.动态库.so生成及生成可执行程序
gcc -shared -fPIC -o libprocess.so process1.o process2.o
因为生成.so的时候是使用到连接器的,所有分配了虚拟地址,若要使用.so生成可执行文件,需要对.so中符号进行重定位,所有需要使用-fPIC命令。
动态库中的结构基本上和.o文件类似。
下面看一下动态库中的动态符号表:
动态符号表中导出的函数是可以被外部文件调用的。
某些时候,我们不希望把我们所有的函数 或者 存在不希望外面文件调用的函数,或者内部存在函数名称和外部文件中函数名称一致,为了避免版本冲突,可以不把该函数加到符号表内,这样外部文件链接的时候就不会找到该函数了,使用如下命令:
gcc -shared -o libprocess2.so process1.o process2.o -fPIC -Wl,--version-script=./process.map
[work@bogon 10]$ cat process.map
{
global:
func1;
func2;
local:*;
};
链接:
gcc -o main main.o ./libprocess2.so
四,可执行程序加载准备
1、对于单个进程来说,可操作的是虚拟内存地址,所有的虚拟内存空间在该进程来看都是独享的
2、对于32位机器来说,寻址范围为4G,内存管理把整个寻址空间分为3G给用户进程空间,1G给内核进程空间
首先基于动态库创建的可执行程序,有些不同,多了一个.interp段:
[work@bogon 10]$ readelf -S main
There are 30 section headers, starting at offset 0x1a90:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
查看一下这个段里面的内容:
[work@bogon 10]$ readelf -x 1 main
Hex dump of section '.interp':
0x00400238 2f6c6962 36342f6c 642d6c69 6e75782d /lib64/ld-linux-
0x00400248 7838362d 36342e73 6f2e3200 x86-64.so.2.
发现这个里面是个动态链接器,也就是程序运行时的链接是它做的。
/下面时引用自极客时间刘超老师课程/
另外,ELF 文件中还多了两个 section,一个是.plt,过程链接表(Procedure Linkage Table,PLT),一个是.got.plt,全局偏移量表(Global Offset Table,GOT)。
main这个程序要调用 libprocess.so 里的函数。由于是运行时才去找,编译的时候,不知道这个函数在哪里,所以就在 PLT 里面建立一项 PLT[x]。这一项也是一些代码,有点像一个本地的代理,在二进制程序里面,不直接调用 func1 函数,而是调用 PLT[x]里面的代理代码,这个代理代码会在运行的时候找真正的func1函数。去哪里找代理代码呢?这就用到了 GOT,这里面也会为func1 函数创建一项 GOT[y]。这一项是运行时 func1 函数在内存中真正的地址。如果这个地址在 main调用 PLT[x]里面的代理代码,代理代码调用 GOT 表中对应项 GOT[y],调用的就是加载到内存中的 libprocess.so 里面的 func1 函数了。但是 GOT 怎么知道的呢?对于 func1 函数,GOT 一开始就会创建一项 GOT[y],但是这里面没有真正的地址,因为它也不知道,但是它有办法,它又回调 PLT,告诉它,你里面的代理代码来找我要 func1函数的真实地址,我不知道,你想想办法吧。PLT 这个时候会转而调用 PLT[0],也即第一项,PLT[0]转而调用 GOT[2],这里面是 ld-linux.so 的入口函数,这个函数会找到加载到内存中的 libcprocess.so 里面的func1 函数的地址,然后把这个地址放在 GOT[y]里面。下次,PLT[x]的代理函数就能够直接调用了。
我理解的是,程序加载的时候,会加载这个链接器程序,当程序运行时,调用链接器来查找函数,PLT(里面有函数的名字),链接器根据GOT(这里面有动态库.so的地址),然后加载对应的.so,.so加载后,获取指定func的真实虚拟地址返回给代码。
五、可执行程序加载过程和运行内存图
shell中执行可执行程序加载过程(进程fork再执行,类似):
1、shell判断可执行文件类型。(shell脚本?可执行程序?)
2、可执行程序的话,fork子进程,执行execve系统调用
3、execve系统调用执行do_execve函数
4、do_execve->do_execveat_common->__do_execve_file
5、创建linux_binprm->根据本用户命名空间创建内存管理结构bprm_mm_init
6、根据inode读取二进制文件的前128字节到linux_binprm结构体的file字段(kernel_read)
7、search_binary_handler
8、search_binary_handle->load_binary
9、load_elf_binary加载二进制的elf程序,把可执行程序需要加载的段,加载到指定的虚拟内存地址中。
10、exec_binprm执行
11、
下面是我测试程序main运行时,内存空间分布: