一.libc.so
相信很多人都知道编译一个c或者一个c++程序的时候都知道要经过预处理,编译,汇编,链接这四步操作。大家都知道printf这个函数在stdio.h 这个头文件中,原来我在windows里面的devc++看到了printf的定义,但是当我在linux里面打开stdio.h的时候并没有看到函数的定义,有的只是函数的声明,这大大激发了我对知识的渴望。后来我知道了printf这个函数是动态链接过来的。我们来看一下这段代码。
1 #include <stdio.h>
2
3 int main(void)
4 {
5 printf("hello world");
6 while (1)
7 ;
8 return 0;
9 }
这段代码用了个printf函数。我们编译然后执行以下操作。
lonelyeagle@myarch ~ ./a.out&
[1] 3937
lonelyeagle@myarch /proc/3937 cat maps
56504cc3b000-56504cc3c000 r--p 00000000 103:09 1836512 /home/lonelyeagle/a.out
56504cc3c000-56504cc3d000 r-xp 00001000 103:09 1836512 /home/lonelyeagle/a.out
56504cc3d000-56504cc3e000 r--p 00002000 103:09 1836512 /home/lonelyeagle/a.out
56504cc3e000-56504cc3f000 r--p 00002000 103:09 1836512 /home/lonelyeagle/a.out
56504cc3f000-56504cc40000 rw-p 00003000 103:09 1836512 /home/lonelyeagle/a.out
56504e481000-56504e4a2000 rw-p 00000000 00:00 0 [heap]
7f5782648000-7f578264a000 rw-p 00000000 00:00 0
7f578264a000-7f5782670000 r--p 00000000 103:08 1838455 /usr/lib/libc-2.33.so
7f5782670000-7f57827bb000 r-xp 00026000 103:08 1838455 /usr/lib/libc-2.33.so
7f57827bb000-7f5782807000 r--p 00171000 103:08 1838455 /usr/lib/libc-2.33.so
7f5782807000-7f578280a000 r--p 001bc000 103:08 1838455 /usr/lib/libc-2.33.so
7f578280a000-7f578280d000 rw-p 001bf000 103:08 1838455 /usr/lib/libc-2.33.so
7f578280d000-7f5782818000 rw-p 00000000 00:00 0
7f5782839000-7f578283a000 r--p 00000000 103:08 1838444 /usr/lib/ld-2.33.so
7f578283a000-7f578285e000 r-xp 00001000 103:08 1838444 /usr/lib/ld-2.33.so
7f578285e000-7f5782867000 r--p 00025000 103:08 1838444 /usr/lib/ld-2.33.so
7f5782867000-7f5782869000 r--p 0002d000 103:08 1838444 /usr/lib/ld-2.33.so
7f5782869000-7f578286b000 rw-p 0002f000 103:08 1838444 /usr/lib/ld-2.33.so
7ffd24920000-7ffd24941000 rw-p 00000000 00:00 0 [stack]
7ffd249c0000-7ffd249c4000 r--p 00000000 00:00 0 [vvar]
7ffd249c4000-7ffd249c6000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
这样我们就可以看到整个进程的地址空间的文件的映射。其中的 /usr/lib/libc-2.33.so 就是我们c程序在linux上运行时需要的动态链接库。/usr/lib/ld-2.33.so就是linux下的动态连接器,所以printf这个函数从那里来这个问题的答案已经显而易见了。
二.elf文件的格式
这里不得不说的就是elf文件的格式。可执行文件,动态链接库,静态链接库文件都按照elf文件格式存储的,所以学习elf文件格式对我们理解一个程序非常的有用。
我们先来看看下面这段代码
1 int printf(const char* format, ... );
2
3 int global_init_var = 84;
4 int global_uninit_var;
5
6 void funcl(int i)
7 {
8 printf("%d\n", i);
9 }
10
11 int main(void)
12 {
13 static int static_var = 85;
14 static int static_var2;
15 int a = 1;
16 int b;
17 funcl(static_var + static_var2 + a + b);
18 return 0;
19 }
gcc -c 测试.c 这个命令只编译不链接
我们把这个目标文件的基本信息打印出来
lonelyeagle@myarch ~/程序员的自我修养 objdump -h 测试.o
测试.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .text 0000005c 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 实际存在
1 .data 00000008 0000000000000000 0000000000000000 0000009c 2**2
CONTENTS, ALLOC, LOAD, DATA 实际存在
2 .bss 00000008 0000000000000000 0000000000000000 000000a4 2**2
ALLOC
3 .rodata 00000004 0000000000000000 0000000000000000 000000a4 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA 实际存在
4 .comment 00000013 0000000000000000 0000000000000000 000000a8 2**0
CONTENTS, READONLY 实际存在
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000bb 2**0
CONTENTS, READONLY 但是size 为0
6 .note.gnu.property 00000030 0000000000000000 0000000000000000 000000c0 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA 实际存在
7 .eh_frame 00000058 0000000000000000 0000000000000000 000000f0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA 实际存在
不同的目标文件可以拥有不同数量及不同类型的“段”
程序源代码编译后的机器指令被放在代码段,代码段常见的名字有“.code",".text",已初始化全局变量和局部静态变量数据经常放在数据段,一般的名字都叫".data".未初始化的全局变量和局部静态变量一般放在一个叫"bss"的段里。
第2行中的contents ,alloc等表示段的属性。contents表示该段在文件中存在内容,.bss段没有contents,表示他实际上在elf文件中不存在内容。
我们对这个目标文件反汇编
下面的是目标文件中各个段的内容,有contents的就表示存在内容,所以只显示了下面几个段的内容。
最左边的是偏移量,中间的那些是内容,我们可以看到,一行分为4列,一列8个数字,也就是4个字节,所以一行16个字节。最右边是内容用ascll码翻译出来的内容。
lonelyeagle@myarch ~/程序员的自我修养 objdump -s -d 测试.o
测试.o: 文件格式 elf64-x86-64
Contents of section .text:
0000 554889e5 4883ec10 897dfc8b 45fc89c6 UH..H....}..E...
0010 488d0500 00000048 89c7b800 000000e8 H......H........
0020 00000000 90c9c355 4889e548 83ec10c7 .......UH..H....
0030 45f80100 00008b15 00000000 8b050000 E...............
0040 000001c2 8b45f801 c28b45fc 01d089c7 .....E....E.....
0050 e8000000 00b80000 0000c9c3 ............
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata:
0000 25640a00 %d..
Contents of section .comment:
0000 00474343 3a202847 4e552920 31312e31 .GCC: (GNU) 11.1
0010 2e3000 .0.
Contents of section .note.gnu.property:
0000 04000000 20000000 05000000 474e5500 .... .......GNU.
0010 020001c0 04000000 00000000 00000000 ................
0020 010001c0 04000000 01000000 00000000 ................
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 27000000 00410e10 8602430d ....'....A....C.
0030 06620c07 08000000 1c000000 3c000000 .b..........<...
0040 00000000 35000000 00410e10 8602430d ....5....A....C.
0050 06700c07 08000000 .p......
这个是反汇编的结果
Disassembly of section .text:
0000000000000000 <funcl>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 8b 45 fc mov -0x4(%rbp),%eax
e: 89 c6 mov %eax,%esi
10: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # 17 <funcl+0x17>
17: 48 89 c7 mov %rax,%rdi
1a: b8 00 00 00 00 mov $0x0,%eax
1f: e8 00 00 00 00 call 24 <funcl+0x24>
24: 90 nop
25: c9 leave
26: c3 ret
0000000000000027 <main>:
27: 55 push %rbp
28: 48 89 e5 mov %rsp,%rbp
2b: 48 83 ec 10 sub $0x10,%rsp
2f: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
36: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 3c <main+0x15>
3c: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 42 <main+0x1b>
42: 01 c2 add %eax,%edx
44: 8b 45 f8 mov -0x8(%rbp),%eax
47: 01 c2 add %eax,%edx
49: 8b 45 fc mov -0x4(%rbp),%eax
4c: 01 d0 add %edx,%eax
4e: 89 c7 mov %eax,%edi
50: e8 00 00 00 00 call 55 <main+0x2e>
55: b8 00 00 00 00 mov $0x0,%eax
5a: c9 leave
5b: c3 ret
1.data段
.data段保存的是那些已经初始化了的全局静态变量和局部静态变量。前面的代码里面一共有两个这样的变量,分别是global_init_varabal与static_var。这两个变量每个都是4字节,一共8个字节,所以.data这个段的大小是8字节
我们看到".data"段里的前4个字节,从低到高分别为 0x54,0x00,0x00,0x00,这个值刚好是global_init_varabal,即十进制的84。
2.rodata段
.rodata段存放的是只读数据,一般是程序里面的只读变量(如const修饰的变量)和字符串常量。对这个段的任何修改操作都会作为非法操作处理,保证了程序的安全性。printf函数里面用到了一个字符串常量,"%d\n",它是一种只读数据,所以他被放在了.rodata段。
3.bss段
.bss 段存放的是未初始化的全局变量和局部静态变量。上述代码中global_unint_var 和 static_var2 就是存放.bss段,更准确的说法是.bss 段为他们预留了空间。
4.文件头
lonelyeagle@myarch ~/程序员的自我修养 readelf -h 测试.o
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 1064 (bytes into file)
标志: 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: 14
Section header string table index: 13
文件头,它包含了描述整个文件的基本属性,比如elf文件版本,目标机器型号,程序入口地址等。
magic的16个字节用来标识ELF文件的平台属性。
5.段表
lonelyeagle@myarch ~/程序员的自我修养 readelf -S 测试.o
There are 14 section headers, starting at offset 0x428:
节头:
[号] 名称 类型(type) 地址 偏移量
大小 全体大小 旗标(flg) 链接(lk) 信息(inf) 对齐(al)
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000005c 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000308
0000000000000078 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 0000009c
0000000000000008 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 000000a4
0000000000000008 0000000000000000 WA 0 0 4
[ 5] .rodata PROGBITS 0000000000000000 000000a4
0000000000000004 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 000000a8
0000000000000013 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000bb
0000000000000000 0000000000000000 0 0 1
[ 8] .note.gnu.pr[...] NOTE 0000000000000000 000000c0
0000000000000030 0000000000000000 A 0 0 8
[ 9] .eh_frame PROGBITS 0000000000000000 000000f0
0000000000000058 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 00000380
0000000000000030 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 00000148
0000000000000150 0000000000000018 12 8 8
[12] .strtab STRTAB 0000000000000000 00000298
000000000000006f 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 000003b0
0000000000000074 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
段表就是保存这些段的基本属性的结构比如每个段的段名,段的长度,在文件中的偏移,读写权力及段的其他属性。elf文件的段结构就是由段表决定的,编译器,链接器,和装载器都是依靠段表来定位和访问各个段的属性的。
对于我们的代码来说,段表就是有14个元素的数组,elf段表的这个数组的第一个元素是无效的段描述符,他的类型是null,除此之外每个段描述符都对应一个段。也就是说我们的代码一共有13个段。另外,objdump -h 只是把elf文件中最关键的段显示了出来,而省略了其他辅助性的段。
1.段的类型相关常量
常量 | 含义 | 值 |
---|---|---|
NULL | 无效段 | 0 |
PROGBITS | 程序段,代码段,数据段都是这种类型 | 1 |
SYMTAB | 表示该段的内容为符号表 | 2 |
STRTAB | 表示该段的内容为字符串表 | 3 |
RELA | 重定位表,该段包含了重定位信息,这个段跟静态链接和动态链接有关 | 4 |
HASH | 符号表的哈希表 | 5 |
DYNAMIC | 动态链接信息 | 6 |
NOTE | 提示性信息 | 7 |
NOBITS | 表示该段在文件中没有内容,比如.BSS段 | 8 |
REL | 包含了重定位信息 | 9 |
SHLIB | 保留 | 10 |
DNYSYM | 动态链接的符号表 | 11 |
2. 段的标志位(也就是上面图里面的旗帜)
也可以参考图里面 Key to Flags 里面的内容。
WRITE | 表示该段在进程中可写 |
---|---|
ALLOC | 表示这段在进程空间中需要分配空间,有些包含指示或控制信息的段不需要在进程空间中被分配空间,他们一般不会有这个标志,像代码段,数据段和.bss段都会有这个标志位 |
EXECINSTR | 表示该段在进程空间中可以被执行,一般指代码段 |
3.段的链接信息
如果段的类型是与链接相关的,比如重定位表,符号表等,那么sh_link和sh_info这两个成员包含的意义如下面的表所比示,对于其他的段,sh_link 和 sh_info 没有意义
type | link(lk) | info(inf) |
---|---|---|
DYNAMIC | 该段使用的字符串在段表中的下标 | 0 |
HASH | 该段所使用的符号表在段表中的下标 | 0 |
REL 和 RELA | 该段所使用的相应符号表在段表中的下标 | 该重定位表所作用的段在段表中的下标 |
6.重定位表
在测试.o中有一个.rel.text的段,他的类型为rela,也就是说他是一个重定位表,因为链接器在处理目标文件时,需要对目标文件中某些部件进行重定位,即代码段和数据段中那些绝对地址的引用的位置,这些重定位信息都记录在elf文件的重定位表里面,对于每个需要重定位的代码段或数据段,都会有一个相应的重定位表,比如.rel.text就是.text的重定位表,.text中有printf函数的调用。.data段中没有对绝对地址的引用,它只是包含了几个常量。所以没有.rel.data这个段。
一个重定位表,它的"link"表示符号表的下标,他的"info"表示他作用于那个段。比如,“.rel.text"作用于作用于".text"段,而".text"段的下标为"1",那么".rel.text"的"info"为1,在上图中,我们可以观察到符号表的下标是11。不只是.rel.text是这样,.rela.eh_frame的sh_link也是11。
7.符号表
lonelyeagle@myarch ~/程序员的自我修养 readelf -s 测试.o
Symbol table '.symtab' contains 14 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS ��.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: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 static_var.1
7: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 static_var2.0
8: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var
9: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 global_uninit_var
10: 0000000000000000 39 FUNC GLOBAL DEFAULT 1 funcl
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
13: 0000000000000027 53 FUNC GLOBAL DEFAULT 1 main
1.符号绑定(Bind)
宏定义名 | 值 | 说明 |
---|---|---|
local | 0 | 局部符号,对于目标文件的外表不可见 |
global | 1 | 全局符号,外部可见 |
weak | 2 | 弱引用 |
强符号与弱符号
我们经常在编程中碰到一种情况叫符号重复定义。多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候将会出现符号重复定义的错误。
对于c/c++来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。
注意:强符号和弱符号都是针对定义来说的,不是针对符号的引用。
1 extern int ext;
2
3 int weak;
4 int strong = 1;
5 __attribute__((week)) week2 = 2; //弱引用
6
7 int main()
8 {
9 return 0;
上面这段程序,"week"和"week2"是弱符号,因为没有初始化。"strong"和"main"是强符号。”ext"即非强符号也非弱符号,它是一个外部符号的引用。我们这里只是针对强符号和弱符号来说的。
三大规则
1.不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号);如果有多个强符号,则链接器报符号重复定义错误。
2.如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。
3.如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。
1 #include <stdio.h>
2 int week; // 弱引用
3 int week = 10; // 强引用
4
5 int main(void)
6 {
7 printf("%d\n",week);
8 return 0;
9 }
lonelyeagle@myarch ~ gcc 测试.c
lonelyeagle@myarch ~ ./a.out
10
弱引用和强引用
目前我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,他们需要被正确决议。如果没有找到该符号的定义,链接器就会报符号为定义错误,这种被称为强引用与之对应还有一种弱引用,在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议,如果该符号未被定义,则链接器对于该引用不报错。 链接器处理强引用和弱引用的过程几乎一样,只是对于未被定义的弱引用,链接器对于该引用不报错。弱符号和弱引用主要用户库的链接过程。 这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数,或者程序可以对某些扩展功能模块的引用定义为弱引用,当我们将扩展模块与程序链接在一起时,功能模块就可以正常使用,如果我们去掉了某些功能模块,那么程序也可以正常链接。
3.符号类型(Type)
宏定义名 | 值 | 说明 |
---|---|---|
NOTYPE | 0 | 说明 |
OBJECT | 1 | 该符号是一个数据对象,比如变量,数组 |
FUNC | 2 | 该符号是一个函数或者其他可执行代码 |
SECTION | 3 | 该符号表示一个段 |
FILE | 4 | 该符号表示文件名 |
3.符号所在段(Ndx)
如果符号定义在本目标文件中,那么这个成员表示符号所在的段在段表中的下标。对于上图Num为0 的那个符号,永远都是一个为定义的符号。我们的printf函数因为没有被定义,所以他的Ndx是UND,意思是没有定义。
4.符号值
也就是value, 表示的是符号所对应的函数或者变量位于Ndx指定的段,偏移Value的位置。举个例子,因为main和func1都定义在代码里面,所以他们所在的位置都为代码段,所以Ndx的值为1,对应的段就是.text。这个printf函数,因为只是带代码里面被引用,没有被定义,所以他的Ndx为und,也就是没有被定义的意思。
三.总结
由于篇幅的原因,这里只说了主要的几个段,其实还有很多的东西可以讲,学习elf文件格式对于我们编程可能没有太直接的用处,但是对于提升我们对于一个程序的理解会有很大的提升。