赞
踩
此处so指Shared Object,即动态链接库,本文将从so文件格式开始讲述,在了解完so文件格式的必要知识后,接下来最简概述so的生成,即编译器的静态链接,然后便是so的加载与动态链接,以及动态链接库的依赖动态链接库。
so的文件格式为ELF(Executable and Linkable Format),ELF由Unix System Laboratories开发,已经成为标准。常见的动态链接库(so), 静态库(a), 编译目标文件(o), 可执行文件, CoreDump文件的格式均为ELF。
ELF文件由ELF Header, Program header table, Section Content, Section header table组成,示例图如下:
-------------------------
| ELF Header |
-------------------------
| Program Header Table |
-------------------------
| Section Content |
| (.text) |
| (.data) |
| (.bss) |
| (...) |
-------------------------
| Section Header Table |
-------------------------
如下在Windows中采用Android NDK 22.0.7026061版本中arm64-v8a架构下的libc++_shared.so进行分析。
完整路径:22.0.7026061/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so
所有ELF文件最开始都为ELF Header。通过readelf -h可以查看文件头信息(Android NDK自带readelf.exe)。
e:\huchao>readelf -h libc++_shared.so 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: DYN (Shared object file) Machine: AArch64 Version: 0x1 Entry point address: 0x6c15c Start of program headers: 64 (bytes into file) Start of section headers: 7809600 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 9 Size of section headers: 64 (bytes) Number of section headers: 36 Section header string table index: 34
ELF Header为Elf32_Ehdr/Elf64_Ehdr结构体(sizeof(Elf64_Ehdr) = 64):
看完文件头后,再查看Section Header Table,其位于本ELF文件的末尾,中文名为段表,其描述了段名,长度、偏移、权限等属性,段表主要描述编译的各个段信息是如何存储在文件中的,专注于文件存储。文件头看到Start of section headers为7809600,而本文件有36个Section Header Table,所以计算出libc++_shared.so的文件大小为7809600 + 36 * 64 = 7811904。通过readelf -S可以查看Section Header表信息(原本输出内容做了折行,不太好阅读,如下内容已经格式化)
e:\huchao>readelf -S libc++_shared.so There are 36 section headers, starting at offset 0x772a40: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .note.android.ide NOTE 0000000000000238 00000238 0000000000000098 0000000000000000 A 0 0 4 [ 2] .note.gnu.build-i NOTE 00000000000002d0 000002d0 0000000000000024 0000000000000000 A 0 0 4 [ 3] .dynsym DYNSYM 00000000000002f8 000002f8 000000000000e448 0000000000000018 A 8 1 8 [ 4] .gnu.version VERSYM 000000000000e740 0000e740 0000000000001306 0000000000000002 A 3 0 2 [ 5] .gnu.version_r VERNEED 000000000000fa48 0000fa48 0000000000000040 0000000000000000 A 8 2 4 [ 6] .gnu.hash GNU_HASH 000000000000fa88 0000fa88 0000000000003c84 0000000000000000 A 3 0 8 [ 7] .hash HASH 000000000001370c 0001370c 0000000000004c20 0000000000000004 A 3 0 4 [ 8] .dynstr STRTAB 000000000001832c 0001832c 000000000001c277 0000000000000000 A 0 0 1 [ 9] .rela.dyn RELA 00000000000345a8 000345a8 00000000000131b8 0000000000000018 A 3 0 8 [10] .rela.plt RELA 0000000000047760 00047760 0000000000002c10 0000000000000018 A 3 22 8 [11] .rodata PROGBITS 000000000004a370 0004a370 00000000000065c5 0000000000000000 AMS 0 0 16 [12] .gcc_except_table PROGBITS 0000000000050938 00050938 0000000000005c48 0000000000000000 A 0 0 4 [13] .eh_frame_hdr PROGBITS 0000000000056580 00056580 00000000000040bc 0000000000000000 A 0 0 4 [14] .eh_frame PROGBITS 000000000005a640 0005a640 0000000000011b1c 0000000000000000 A 0 0 8 [15] .text PROGBITS 000000000006c15c 0006c15c 000000000007b2c0 0000000000000000 AX 0 0 4 [16] .plt PROGBITS 00000000000e7420 000e7420 0000000000001d80 0000000000000000 AX 0 0 16 [17] .data.rel.ro PROGBITS 00000000000ea1a0 000e91a0 00000000000070f0 0000000000000000 WA 0 0 8 [18] .fini_array FINI_ARRAY 00000000000f1290 000f0290 0000000000000010 0000000000000008 WA 0 0 8 [19] .init_array INIT_ARRAY 00000000000f12a0 000f02a0 0000000000000008 0000000000000000 WA 0 0 8 [20] .dynamic DYNAMIC 00000000000f12a8 000f02a8 00000000000001b0 0000000000000010 WA 8 0 8 [21] .got PROGBITS 00000000000f1458 000f0458 0000000000000570 0000000000000000 WA 0 0 8 [22] .got.plt PROGBITS 00000000000f19c8 000f09c8 0000000000000ec8 0000000000000000 WA 0 0 8 [23] .data PROGBITS 00000000000f3890 000f1890 0000000000000118 0000000000000000 WA 0 0 8 [24] .bss NOBITS 00000000000f39c0 000f19a8 00000000000072f0 0000000000000000 WA 0 0 64 [25] .comment PROGBITS 0000000000000000 000f19a8 000000000000011b 0000000000000001 MS 0 0 1 [26] .debug_loc PROGBITS 0000000000000000 000f1ac3 000000000023ab8a 0000000000000000 0 0 1 [27] .debug_abbrev PROGBITS 0000000000000000 0032c64d 0000000000010ef6 0000000000000000 0 0 1 [28] .debug_info PROGBITS 0000000000000000 0033d543 000000000021203a 0000000000000000 0 0 1 [29] .debug_ranges PROGBITS 0000000000000000 0054f57d 0000000000074b40 0000000000000000 0 0 1 [30] .debug_str PROGBITS 0000000000000000 005c40bd 00000000000d07cc 0000000000000001 MS 0 0 1 [31] .debug_line PROGBITS 0000000000000000 00694889 0000000000084e0b 0000000000000000 0 0 1 [32] .debug_aranges PROGBITS 0000000000000000 00719694 0000000000000170 0000000000000000 0 0 1 [33] .symtab SYMTAB 0000000000000000 00719808 0000000000028560 0000000000000018 35 4450 8 [34] .shstrtab STRTAB 0000000000000000 00741d68 0000000000000178 0000000000000000 0 0 1 [35] .strtab STRTAB 0000000000000000 00741ee0 0000000000030b59 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), p (processor specific)
libc++_shared.so文件中有36个Section Header Table,每一项称为一个Section,其为Elf32_Shdr/Elf64_Shdr结构体(sizeof(Elf64_Shdr) = 64)
typedef struct {
uint32_t sh_name;
uint32_t sh_type;
uint64_t sh_flags;
Elf64_Addr sh_addr;
Elf64_Off sh_offset;
uint64_t sh_size;
uint32_t sh_link;
uint32_t sh_info;
uint64_t sh_addralign;
uint64_t sh_entsize;
} Elf64_Shdr;
// 哈希表结构 ------------------ | Bucket Count | ------------------ | Chain Count | ------------------ | Bucket[0] | | ... | | Bucket[n - 1] | ------------------ | Chain[0] | | ... | | Chain[n - 1] | ------------------ // 哈希函数 unsigned long elf_Hash(const unsigned char *name) { unsigned long h = 0, g; while (*name) { h = (h << 4) + *name++; if (g = h & 0xf0000000) h ^= g >> 24; h &= ~g; } return h; }
上面讲完so文件的格式后,接下来看so文件是怎样被生成出来的。
做过C/C++开发的都知道so是先通过编译,然后链接生成的,编译各源文件生成目标文件(.o),然后链接各目标文件生成最终的产物,产物可以是so或可执行文件等。
本节讨论so文件的生成,那么我们暂时忽略编译过程,核心讨论链接过程。前面讲了ELF文件结构,实际上目标文件(.o)也是ELF结构,链接就是将多个ELF结构的目标文件通过一定的规则合并成一个总的ELF文件,然后修改必要的字段的过程。
前面提到的.data, .text, .strtab等段,一般编译器在合并目标文件时,会选择合并名称相同的段,重定位需要的段,合并通常分两步进行:
我们知道C/C++的源文件都是单独编译成目标文件的,假设A目标文件调用B目标文件中的foobar函数,那么A如何知道foobar的信息呢?实际上在编译阶段,A目标不知道foobar的更多信息,所以只能将其地址写为0,以待后续链接时再行处理。链接器经过空间与地址分配后,会在全局表中记录foobar的符号信息(名称与地址),然后查找全局表,获取关于foobar的相关信息,最后再对目标A中的符号进行重定位,如果此时链接器没找到匹配的foobar符号,那么就将报链接错误。
由于C++语言的特性,链接器还会做如下两个操作:
最后,拿Android平台举个例子,对于同一份C/C++代码,编译生成so时,armeabi-v7a/arm64-v8a/x86等也均需完全重新编译,也需要选择匹配平台、指令、ABI中的静态库进行链接,因为C/C++编译器是特定于机器架构,不同的指令与ABI均需重新编译。
在最终生成产物时,应用编写者是可以通过链接控制脚本对产物进行控制的,如:自定义入口,这样能在默认入口之前再执行些特定的操作。
上面讲完so文件的生成后,接下来继续看看静态库.a文件是怎样被生成出来的。
实际上.a文件生成非常简单,因为.a就是一个archive压缩包,其中包含的内容就是目标文件(.o),由于是静态链接库,所以在最终链接时,解压其中所需的目标文件,静态链接即可。可以通过命令查看
e:\huchao>ar -t libc++_static.a
algorithm.o
any.o
atomic.o
barrier.o
bind.o
charconv.o
chrono.o
condition_variable.o
.../
ELF64 Header的大小为64Bytes,libc++_shared.so从第64Bytes开始便为Program Header表(如上的Start of program headers中为64),Program Header是用于描述自己将如何被加载到系统中的,专注于加载过程。通过readelf -l可以查看Program Header表信息(原本输出内容做了折行,不太好阅读,如下内容已经格式化)
e:\huchao>readelf -l libc++_shared.so Elf file type is DYN (Shared object file) Entry point 0x6c15c There are 9 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 0x00000000000001f8 0x00000000000001f8 R 8 LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x00000000000e91a0 0x00000000000e91a0 R E 1000 LOAD 0x00000000000e91a0 0x00000000000ea1a0 0x00000000000ea1a0 0x00000000000086f0 0x00000000000086f0 RW 1000 LOAD 0x00000000000f1890 0x00000000000f3890 0x00000000000f3890 0x0000000000000118 0x0000000000007420 RW 1000 DYNAMIC 0x00000000000f02a8 0x00000000000f12a8 0x00000000000f12a8 0x00000000000001b0 0x00000000000001b0 RW 8 GNU_RELRO 0x00000000000e91a0 0x00000000000ea1a0 0x00000000000ea1a0 0x00000000000086f0 0x0000000000008e60 R 1 GNU_EH_FRAME 0x0000000000056580 0x0000000000056580 0x0000000000056580 0x00000000000040bc 0x00000000000040bc R 4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0 NOTE 0x0000000000000238 0x0000000000000238 0x0000000000000238 0x00000000000000bc 0x00000000000000bc R 4 Section to Segment mapping: Segment Sections... 00 01 .note.android.ident .note.gnu.build-id .dynsym .gnu.version .gnu.version_r .gnu.hash .hash .dynstr .rela.dyn .rela.plt .rodata .gcc_except_table .eh_frame_hdr .eh_frame .text .plt 02 .data.rel.ro .fini_array .init_array .dynamic .got .got.plt 03 .data .bss 04 .dynamic 05 .data.rel.ro .fini_array .init_array .dynamic .got .got.plt 06 .eh_frame_hdr 07 08 .note.android.ident .note.gnu.build-id
libc++_shared.so文件中有9个Program Header,每一项称为一个Segment,其为Elf32_Phdr/Elf64_Phdr结构体(sizeof(Elf64_Phdr) = 56)
typedef struct {
uint32_t p_type;
uint32_t p_flags;
Elf64_Off p_offset;
Elf64_Addr p_vaddr;
Elf64_Addr p_paddr;
uint64_t p_filesz;
uint64_t p_memsz;
uint64_t p_align;
} Elf64_Phdr;
最终发布release程序时,通常会对so进行strip操作,strip能够减少so文件的大小,上面讨论过ELF File Header、Program Header Table、Section Header Table,我们从这3者的角度看看strip做了什么。
strip可通过命令行执行:
strip libc++_shared.so -o libc++_shared_stripped.so
strip后的so由7811904 Bytes减少为991880 Bytes,文件减少将近90%。如下对比一下ELF的各个Header,看具体减少了哪些东西。
libc++_shared_stripped.so的ELF Header(读者可以通过文本比较工具对比一下)
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: DYN (Shared object file) Machine: AArch64 Version: 0x1 Entry point address: 0x6c15c Start of program headers: 64 (bytes into file) Start of section headers: 990152 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 9 Size of section headers: 64 (bytes) Number of section headers: 27 Section header string table index: 26
可以看到主要改变了3个字段:
libc++_shared_stripped.so的ELF Section Header Table
There are 27 section headers, starting at offset 0xf1bc8: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .note.android.ide NOTE 0000000000000238 00000238 0000000000000098 0000000000000000 A 0 0 4 [ 2] .note.gnu.build-i NOTE 00000000000002d0 000002d0 0000000000000024 0000000000000000 A 0 0 4 [ 3] .dynsym DYNSYM 00000000000002f8 000002f8 000000000000e448 0000000000000018 A 8 1 8 [ 4] .gnu.version VERSYM 000000000000e740 0000e740 0000000000001306 0000000000000002 A 3 0 2 [ 5] .gnu.version_r VERNEED 000000000000fa48 0000fa48 0000000000000040 0000000000000000 A 8 2 4 [ 6] .gnu.hash GNU_HASH 000000000000fa88 0000fa88 0000000000003c84 0000000000000000 A 3 0 8 [ 7] .hash HASH 000000000001370c 0001370c 0000000000004c20 0000000000000004 A 3 0 4 [ 8] .dynstr STRTAB 000000000001832c 0001832c 000000000001c277 0000000000000000 A 0 0 1 [ 9] .rela.dyn RELA 00000000000345a8 000345a8 00000000000131b8 0000000000000018 A 3 0 8 [10] .rela.plt RELA 0000000000047760 00047760 0000000000002c10 0000000000000018 AI 3 22 8 [11] .rodata PROGBITS 000000000004a370 0004a370 00000000000065c5 0000000000000000 AMS 0 0 16 [12] .gcc_except_table PROGBITS 0000000000050938 00050938 0000000000005c48 0000000000000000 A 0 0 4 [13] .eh_frame_hdr PROGBITS 0000000000056580 00056580 00000000000040bc 0000000000000000 A 0 0 4 [14] .eh_frame PROGBITS 000000000005a640 0005a640 0000000000011b1c 0000000000000000 A 0 0 8 [15] .text PROGBITS 000000000006c15c 0006c15c 000000000007b2c0 0000000000000000 AX 0 0 4 [16] .plt PROGBITS 00000000000e7420 000e7420 0000000000001d80 0000000000000000 AX 0 0 16 [17] .data.rel.ro PROGBITS 00000000000ea1a0 000e91a0 00000000000070f0 0000000000000000 WA 0 0 8 [18] .fini_array FINI_ARRAY 00000000000f1290 000f0290 0000000000000010 0000000000000008 WA 0 0 8 [19] .init_array INIT_ARRAY 00000000000f12a0 000f02a0 0000000000000008 0000000000000008 WA 0 0 8 [20] .dynamic DYNAMIC 00000000000f12a8 000f02a8 00000000000001b0 0000000000000010 WA 8 0 8 [21] .got PROGBITS 00000000000f1458 000f0458 0000000000000570 0000000000000000 WA 0 0 8 [22] .got.plt PROGBITS 00000000000f19c8 000f09c8 0000000000000ec8 0000000000000000 WA 0 0 8 [23] .data PROGBITS 00000000000f3890 000f1890 0000000000000118 0000000000000000 WA 0 0 8 [24] .bss NOBITS 00000000000f39c0 000f19a8 00000000000072f0 0000000000000000 WA 0 0 64 [25] .comment PROGBITS 0000000000000000 000f19a8 000000000000011b 0000000000000001 MS 0 0 1 [26] .shstrtab STRTAB 0000000000000000 000f1ac3 0000000000000104 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), p (processor specific)
可以看到strip主要删除了.symtab段, .debug开头的调试段,更新了.strtab等字符串表。
读者也可以打印Program Header Table,然后对比一下,最终可见Program Header Table没有任何变更,所以也可以证明strip不影响加载与运行。
动态链接库被操作系统加载并执行需要完成几个步骤。动态链接实际上是把静态链接的步骤延迟到加载时才来做,gcc静态链接器为可执行程序ld,Linux动态连接器为ld-x.x.so,动态连接器也是一个so,这个so被可执行程序加载,在运行时完成动态链接过程。
整个动态链接过程分3个步骤:
可执行程序可能依赖于其他so(如:libc.so),而这些so是动态链接的,其符号地址是未被决议的,所以需要一个库来专门做符号地址决议事情,这个库就是ld-x.x.so,由于这个库是其他所有库决议的root,所以其不能依赖于任何其他库,并且不能依赖于全局变量与静态变量。
通过ldd查看到ld-2.31.so时静态链接的。
huchao@ubuntu:~/Documents$ ldd /lib/x86_64-linux-gnu/ld-2.31.so
statically linked
通过Program Header章节了解到,从加载视角来看,so是按照Segment来加载的,其实只有Type为LOAD类型的Segment才会加载到内存,加载到内存后,最终展现为一个VMA(Virtual Memory Address),通过cat /proc/[pid]/maps便可以查看某个进程的VMA。如下为某个进程加载libc++_shared.so后的VMA(节选)
7c1d340000-7c1d42a000 r-xp 00000000 fc:0a 1205828 /data/app/~~_aQ_wKdfg1jJICW6DWQKtw==/com.huchao.myapplication-OBLuyt_hiZ5qxD-6lY_J8A==/lib/arm64/libc++_shared.so
7c1d42a000-7c1d433000 r--p 000e9000 fc:0a 1205828 /data/app/~~_aQ_wKdfg1jJICW6DWQKtw==/com.huchao.myapplication-OBLuyt_hiZ5qxD-6lY_J8A==/lib/arm64/libc++_shared.so
7c1d433000-7c1d434000 rw-p 000f1000 fc:0a 1205828 /data/app/~~_aQ_wKdfg1jJICW6DWQKtw==/com.huchao.myapplication-OBLuyt_hiZ5qxD-6lY_J8A==/lib/arm64/libc++_shared.so
7c2104c000-7c2154c000 rw-p 00000000 00:00 0 [anon:libc_malloc]
7d13f15000-7d13f16000 r--p 00000000 07:80 34 /apex/com.android.runtime/lib64/bionic/libdl.so
7d15b30000-7d15b31000 r-xp 00000000 00:00 0 [vdso]
7fe02e0000-7fe0adf000 rw-p 00000000 00:00 0 [stack]
可以看到libc++_shared.so被映射了3次,分别对应Segment Header中的3个Type为LOAD的Segment。本次加载在内存中的VMA的顺序与文件中的Program Header顺序一致,并且3次映射一一对应。
以第一次映射举例:
从最后一行存在一些有意义的路径,举例说几个:
so可能会被各种程序加载到内存中,那么内存地址是不固定的,所以直接使用是显然不行的(前面讲so生成时提到“链接时重定位”,实际上是指链接器在静态链接时的重定位,因为最终产物是固定的),这里是动态链接,对应的可以采用“装载时重定位”的方式,也就是加载到内存时,再对符号地址进行重定位。
装载时重定位似乎解决了so动态加载的问题,但实际上还有更优策略。前面讲so从磁盘加载到内存时,LOAD类型的Program Header会被映射到内存,而libc.so之类的so是非常常用的,几乎所有程序都需要加载,而libc.so的代码又是一致的,并且系统只存在一份,那么可以考虑内存复用,也就是说对于libc.so相同的部分,在内存中只存在一份,所有进程共享这一份代码段数据,各自有的数据段再各自存储。
在共享代码段内存时,有个主要的问题就是地址的引用,如:有些代码引用了变量的绝对地址,这在静态链接中是没问题的,因为最终链接时会进行重定位,并且也无需内存共享。动态链接加载时,如果对绝对地址做重定位,那么意味着代码段的内容将不同,也就无法共享内存了,这需要对so的地址引用做特殊处理,GCC编译器有地址无关代码(Position-independent Code)技术,也就是把需要引用的地址单独拿出来,作为.got(Global Offset Table)段,代码段再引用.got段内容来解决。gcc提供了-fPIC参数用于开启地址无关代码,如有需要,最终的so将会生成.got段。
相较于静态链接,动态链接在加载时需要对符号地址做重定位、还需要PIC(地址无关代码)技术,那么也就意味着,动态链接的效率是低于静态链接的。实际上可以通过延迟绑定技术(将dlopen的第二个参数填为RTLD_LAZY)来优化部分性能,延迟绑定(PLT:Procedure Linkage Table)通过将模块外部的函数地址写成单独的段(.got.plt),在so加载时不做绑定,等到调用时再进行。
在重定位完成后,如果so有.init段,那么动态链接器将会执行之,进行初始化,同样,如果存在.fini段,在进程退出时也会执行。
程序加载某libA.so时,libA.so可能还依赖于libB.so, libC.so,这些静态依赖是通过.dynamic这个段来实现的。
e:\huchao>readelf -d libc++_shared.so Dynamic section at offset 0xf02a8 contains 27 entries: Tag Type Name/Value 0x0000000000000001 (NEEDED) Shared library: [libc.so] 0x0000000000000001 (NEEDED) Shared library: [libdl.so] 0x000000000000000e (SONAME) Library soname: [libc++_shared.so] 0x000000000000001e (FLAGS) BIND_NOW 0x000000006ffffffb (FLAGS_1) Flags: NOW 0x0000000000000007 (RELA) 0x345a8 0x0000000000000008 (RELASZ) 78264 (bytes) 0x0000000000000009 (RELAENT) 24 (bytes) 0x000000006ffffff9 (RELACOUNT) 1288 0x0000000000000017 (JMPREL) 0x47760 0x0000000000000002 (PLTRELSZ) 11280 (bytes) 0x0000000000000003 (PLTGOT) 0xf19c8 0x0000000000000014 (PLTREL) RELA 0x0000000000000006 (SYMTAB) 0x2f8 0x000000000000000b (SYMENT) 24 (bytes) 0x0000000000000005 (STRTAB) 0x1832c 0x000000000000000a (STRSZ) 115319 (bytes) 0x000000006ffffef5 (GNU_HASH) 0xfa88 0x0000000000000004 (HASH) 0x1370c 0x0000000000000019 (INIT_ARRAY) 0xf12a0 0x000000000000001b (INIT_ARRAYSZ) 8 (bytes) 0x000000000000001a (FINI_ARRAY) 0xf1290 0x000000000000001c (FINI_ARRAYSZ) 16 (bytes) 0x000000006ffffff0 (VERSYM) 0xe740 0x000000006ffffffe (VERNEED) 0xfa48 0x000000006fffffff (VERNEEDNUM) 2 0x0000000000000000 (NULL) 0x0
libc++_shared.so有27个动态链接信息,其为Elf64_Dyn结构体。此处我们只关心Type为NEEDED类型的,其表示依赖的共享对象。也就是说如果加载libc++_shared.so,那么必须先加载libc.so与libdl.so。
如果系统中有多个libc.x.y.z.so,[libc.so]意味着加载最新的,[libcurl.so.4]意味这加载Major版本为4,而Minor, Patch版本为最新的,如:libcurl.so.4.6.0。在绝大部分Linux某些系统中,[libc.so.6]对应着libc-2.xx.so,这是因为1 ~ 5版本被占用了,感兴趣的可以搜索libc SO-NAME相关内容看看。
先上粗略的结论,通常可以认为两者在功能上没啥区别,一个ELF既可以是可执行文件,又可以是动态链接库。然后再来细化分析一下。
上面提到动态连接器so,可以被执行文件加载到内存中,因此是个动态链接库。但这也是一个可执行文件,可以在Terminal中运行之。
huchao@ubuntu:~/Documents$ /lib/x86_64-linux-gnu/ld-2.31.so
Usage: ld.so [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...]
You have invoked `ld.so', the helper program for shared library executables.
This program usually lives in the file `/lib/ld.so', and special directives
......
接下来通过前面的知识来详细分析一下动态链接库与可执行文件到底有什么区别,先上源码(main.cpp)
#include <stdio.h>
int main() {
printf("Hello World!\n");
return 0;
}
huchao@ubuntu:~/Documents/tmp/maint$ gcc main.cpp -o main huchao@ubuntu:~/Documents/tmp/maint$ gcc -c -fPIC main.cpp huchao@ubuntu:~/Documents/tmp/maint$ gcc -shared -fPIC -o main.so main.o huchao@ubuntu:~/Documents/tmp/maint$ ls -al total 52 drwxrwxr-x 2 huchao huchao 4096 Mar 26 11:06 . drwxrwxr-x 4 huchao huchao 4096 Mar 26 11:06 .. -rwxrwxr-x 1 huchao huchao 16696 Mar 26 11:05 main -rw-rw-r-- 1 huchao huchao 79 Mar 26 11:04 main.cpp -rw-rw-r-- 1 huchao huchao 1688 Mar 26 11:05 main.o -rwxrwxr-x 1 huchao huchao 16200 Mar 26 11:06 main.so huchao@ubuntu:~/Documents/tmp/maint$ ./main Hello World! huchao@ubuntu:~/Documents/tmp/maint$ readelf -h main main.so ...... huchao@ubuntu:~/Documents/tmp/maint$ readelf -S main main.so ...... huchao@ubuntu:~/Documents/tmp/maint$ readelf -l main main.so ...... huchao@ubuntu:~/Documents/tmp/maint$ nm main | grep main U __libc_start_main@@GLIBC_2.2.5 0000000000001149 T main huchao@ubuntu:~/Documents/tmp/maint$ nm main.so | grep main 0000000000001119 T main
生成了可执行文件main,动态链接库main.so。限于篇幅,如上内容省略了部分输出,读者可以自己试试。
通过打印ELF File Header看到,main.so比main多了一个Section Header,少了两个Program Header。
通过打印Section Header看到,比起main.so,main中多出两个段.interp, .note.ABI-tag,少了一个段.got.plt。
通过打印Program Header看到,比起main.so,main中多出了PHDR、INTERP两个Segment
通过nm打印main, main.so的符号看到,这两者都由main这个符号,并且地址都是一致的,意味着可以当做动态链接库加载,可以定位到main地址处,然后开始执行。
通过如上的分析可以看到,如果仅从运行的指令上来看,main, main.so几乎是一致的,但main.so中存在.got.plt段,其用于延迟加载的优化,也就是说通过dlopen延迟加载main是没有优化的,需要对所有符号进行决议,性能较差。
实际上在编译过程中可以通过语言关键字、链接器脚本添加需要的Section,修改程序入口等,做到main, main.so在二进制层面都几乎一致。感兴趣的朋友可以当做小游戏自行尝试。
https://man7.org/linux/man-pages/man5/elf.5.html
https://refspecs.linuxfoundation.org/LSB_3.0.0/LSB-PDA/LSB-PDA.junk/generic-elf.html
https://martin.uy/blog/understanding-the-gcc_except_table-section-in-elf-binaries-gcc/
https://stevens.netmeister.org/631/elf.html
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。