(2条消息) Linux so剖析

Linux so剖析

此处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 Header

所有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):

  1. 第1行Magic为魔数信息,结构体中的e_ident字段,占用16bit(2Bytes)

    • 第1bit恒等于0x7F
    • 第2 ~ 4bit为ELF的ASCII码
    • 第5bit为ELF架构信息,1表示32位,2表示64位
    • 第6bit为数据编码信息,1(ELFDATA2LSB)表示little-endian,2(ELFDATA2MSB)表示big-endian
    • 第7bit表示版本信息,恒等于1(EV_CURRENT)
    • 第8bit为操作系统ABI扩展信息,此值由可由操作系统指定,0表示没有扩展信息
    • 第9bit为ABI Version,0表示不指定版本
    • 第10 ~ 16bit为保留字段,全为0
  2. 第2行Class等同于Magic的第5bit
  3. 第3行Data等同于Magic的第6bit
  4. 第4行Version等同于Magic的第7bit
  5. 第5行OS/ABI等同于Magic的第8bit
  6. 第6行ABI Version等同于Magic的第9bit
  7. 第7行Type为文件类型,结构体中的e_type字段,占用2Bytes,DYN(Dynamic)表示共享库文件。
  8. 第8行Machine为机器类型,结构体中的e_machine字段,占用2Bytes,AArch64表示ARM64架构机器。
  9. 第9行Version为本文件版本,结构体中的e_version字段,占用4Bytes,恒等于1。
  10. 第10行Entry point address为程序的入口点,结构体中的e_entry字段,占用8Bytes,表示程序从此处开始执行。
  11. 第11行Start of program headers为Program Header在本文件中的偏移,结构体中的e_phoff字段,占用8Bytes。64刚好等于sizeof(Elf64_Ehdr),意味着本文件ELF Header之后便是Program Header内容。
  12. 第12行Start of section headers为Section Header在本文件中的偏移,结构体中的e_shoff字段,占用8Bytes。
  13. 第13行Flags恒等于0,结构体中的e_flags字段,占用4Bytes。
  14. 第14行Size of this header为本结构体大小(即:sizeof(Elf64_Ehdr)),结构体中的e_ehsize字段,占用2Bytes。
  15. 第15行Size of program headers为Program Header结构体大小(即:sizeof(Elf64_Phdr)),结构体中的e_phentsize字段,占用2Bytes。
  16. 第16行Number of program headers为Program Header的数目,结构体中的e_phnum字段,占用2Bytes。
  17. 第17行Size of section headers为Section Header的大小(即:sizeof(Elf64_Shdr)),结构体中的e_shentsize字段,占用2Bytes。
  18. 第18行Number of section headers为Section Header的数目,结构体中的e_shnum,占用2Bytes。
  19. 第19行Section header string table index为段表字符串表在段表中的索引(即:.shstrtab表在Section Header Table中的索引,在Section Header Table中看到其索引为34),结构体中的e_shstrndx,占用2Bytes。
    如上分析完了ELF Header信息,但又引入了Program Header Table、Section Header Table等内容,我们接下来继续分析。

Section Header Table

看完文件头后,再查看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;
  1. Name列对应sh_name字段,为段表名称在String Table中的索引,占用4Bytes
  2. Type列对应sh_type字段,为段表的类型与语义,占用4Bytes
    • NULL表示不活跃的段表,其所有字段均为0
    • .note开头的类型为NOTE,表示额外的信息字段,其内容为Elf64_Nhdr结构体列表(readelf -n),比如GNU Tools使用这个字段保存链接等信息,如:SHA1编码的Build ID,编译本so采用的NDK版本(可以查看到本so使用的版本为:r22 7026061)
    • .dynsym的类型为DYNSYM,表示动态链接符号,在so被操作系统加载到内存时需要重定位的内容,其内容为Elf64_Sym结构体列表(sizeof(Elf64_Sym) = 24)。此处.dynsym的大小为0xe448,相除得到动态链接符号表数目为2435。
    • .symtab的类型为SYMTAB,表示符号,其内容为Elf64_Sym结构体列表
    • .dynamic的类型为DYNAMIC,表示动态链接依赖的信息,即:依赖的其他动态链接库,其内容为Elf64_Dyn结构体列表(readelf -d)
    • .gnu.version的类型为VERSYM,表示符号的版本信息(Symbol Version Table),其内容为ELF64_Half内容(sizeof(ELF64_Half) = 2),个数跟DYNSYM相等。值为0表示外部不可见的本地符号,1表示全局可见。此处.gnu.version的大小为0x1306,相除得到符号版本信息表数目为2435,等同于.dynsym的个数。
    • .gnu.version_r的类型为VERNEED,表示需要的版本信息(Version Requirements),其内容为Elf64_Verneed结构体列表(sizeof(Elf64_Verneed) = 16)(readelf -V)
    • .hash的类型为HASH,为符号的哈希表,目的是为了快速查找符号表。此哈希表的格式为,前0 ~ 3Bytes为Bucket数目,4 ~ 7Bytes为Chain数目,Chain在本文件中的值为0x0983,必须等同于.dynsym的数目2435。
    // 哈希表结构
    ------------------
    |  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;
    }
    
    • .gnu.hash的类型为GNU_HASH,为gnu的一套符号哈希表,目的跟.hash类似,采用了Bloom filter算法。
    • .dynstr, .shstrtab, .strtab的类型为均为STRTAB,表示字符串表,字符串通过\0结尾,需要某字符串时,只需要给出字符首字节索引即可,可通过16进制查看器查看。.dynstr表示动态链接所需要的字符串,.strtab表示常规字符串,.shstrtab为段表字符串表,用来保存段表中用到的字符串。
    • .rela开头的类型为RELA,表示需要重新定位的信息
    • .rodata表示只读数据,其类型为PROGBITS,PROGBITS表示本段的意义由本程序定义
    • .data表示程序中已初始化过的数据段
    • .data.rel.ro表示程序初始化时的只读数据段
    • .gcc_except_table, .eh_frame, .eh_frame_hdr,表示GCC处理的异常信息,即:C++ try-catch-finally的处理内容。
    • .text表示可执行代码,即:代码逻辑部分
    • .plt为procedure linkage table,表示需要参与链接的信息。
    • .fini_array的类型为FINI_ARRAY,表示程序结束时的处理信息,如:全局对象的析构函数
    • .init_array的类型为INIT_ARRAY,表示程序初始化时的处理信息,如:全局对象的构造函数
    • .got表示全局偏移表
    • .got.plt表示只读的全局偏移表
    • .bss的类型为NOBITS,表示未初始化的数据(全局变量与静态局部变量),通常用于占位
    • .comment用于程序版本控制内容
    • .debug开头的段用于调试
  3. Flags列对应sh_flags字段,表示本段的属性信息(如:X为可执行,如上的.text, .plt为可执行的),占用8Bytes
  4. Address列对应sh_addr字段,表示如果此段被加载到内存,那么这是要被重定位的基地址,占用8Bytes
  5. Offset列对应sh_offset字段,表示本段内容在文件中的偏移(如:采用16进制编辑器打开libc++_shared.so,定位到.note.android.ide所在的0x00000238偏移,便可以看到Android NDK r22 7026061这些版本信息)。占用8Bytes
  6. Size列对应sh_size字段,表示这个段的大小,占用8Bytes
  7. Link列对应sh_link字段,如果段是与链接相关的,则表示段链接信息,否则无意义,占用4Bytes
  8. Info列对应sh_info字段,sh_link信息的补充信息,占用4Bytes
  9. Align列对应sh_addralign字段,表示段内存地址对齐,0或1表示无需对齐,占用8Bytes
  10. EntSize列对应sh_entsize字段,如果本段是固定大小,那么此值为其结构体的大小(如:.dynsym对应的结构体为Elf64_Sym,大小为0x18),如果本段非固定大小,则为0,占用8Bytes

so文件是怎样生成的

上面讲完so文件的格式后,接下来看so文件是怎样被生成出来的。
做过C/C++开发的都知道so是先通过编译,然后链接生成的,编译各源文件生成目标文件(.o),然后链接各目标文件生成最终的产物,产物可以是so或可执行文件等。

  • 【编译】编译器分为“前端”与“后端”,编译过程也是从前端到后端的过程。

    • 编译前端将源代码文件编译生成中间代码。将其拆开来,整个过程概述为:编译预处理 --> 词法分析 --> 语法分析 --> 语义分析 --> 中间代码
    • 编译后端将中间代码编译生成目标文件。将其拆开来,整个过程概述为:中间代码 --> 优化 --> 目标文件
  • 【链接】目标文件经过链接器进行链接,最终生成产物

本节讨论so文件的生成,那么我们暂时忽略编译过程,核心讨论链接过程。前面讲了ELF文件结构,实际上目标文件(.o)也是ELF结构,链接就是将多个ELF结构的目标文件通过一定的规则合并成一个总的ELF文件,然后修改必要的字段的过程。
前面提到的.data, .text, .strtab等段,一般编译器在合并目标文件时,会选择合并名称相同的段,重定位需要的段,合并通常分两步进行:

  1. 空间与地址分配。扫描所有目标文件,记录各个段的长度、属性等信息,并记录所有符号表中的信息,然后将其合并,并记录映射关系。
  2. 符号解析与重定位。使用上一步的信息,来进行符号的解析与重定位、调整代码中的地址等。此处重定位也叫链接时重定位,属于静态链接(将与下文动态链接中的装载时重定位对应)。

我们知道C/C++的源文件都是单独编译成目标文件的,假设A目标文件调用B目标文件中的foobar函数,那么A如何知道foobar的信息呢?实际上在编译阶段,A目标不知道foobar的更多信息,所以只能将其地址写为0,以待后续链接时再行处理。链接器经过空间与地址分配后,会在全局表中记录foobar的符号信息(名称与地址),然后查找全局表,获取关于foobar的相关信息,最后再对目标A中的符号进行重定位,如果此时链接器没找到匹配的foobar符号,那么就将报链接错误。

由于C++语言的特性,链接器还会做如下两个操作:

  1. 对于相同的模板、虚函数表等代码,最终链接到一个文件时,可能存在多份,所以链接器还会做重复代码消除操作
  2. 对于构造函数、析构函数,由于需要在main函数之前或之后执行,所以也会生成.init, .fini段。

最后,拿Android平台举个例子,对于同一份C/C++代码,编译生成so时,armeabi-v7a/arm64-v8a/x86等也均需完全重新编译,也需要选择匹配平台、指令、ABI中的静态库进行链接,因为C/C++编译器是特定于机器架构,不同的指令与ABI均需重新编译。

  • CPU指令:armv7, armv8, x86等均为不同的指令集
  • ABI:程序的二进制接口(Application Binary Interface)不同,是指内存分部、调用方式、修饰标准等。

在最终生成产物时,应用编写者是可以通过链接控制脚本对产物进行控制的,如:自定义入口,这样能在默认入口之前再执行些特定的操作。

.a文件是怎样生成的

上面讲完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
.../

Program Header Table

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;
  1. 第1列Type对应p_type字段,为本Program Header的类型,占用4Bytes

    • PHDR表示自己这个Program Header
    • LOAD表示操作系统将要加载到内存的Segment,最终展现为一个Virtual Memory Address。(某些Unix为了节省内存,加载时可能会合并Segment)
    • DYNAMIC表示加载到内存后,需要动态链接处理的Segment
    • GNU_RELRO表示加载到内存后,需要动态链接处理并且是只读的Segment
    • GNU_EH_FRAME表示Exception处理的调用栈内容
    • GNU_STACK表示调用栈内容
    • NOTE表示一些辅助的信息
  2. 第2列Offset对应p_offset字段,表示本Program Header在本文件中的偏移,占用8Bytes。比如:PHDR的偏移为0x40,等于文件头信息中的Start of program headers
  3. 第3列VirtAddr对应p_vaddr字段,表示本Program Header被加载到内存时,虚拟内存地址的起始位置,占用8Bytes。
  4. 第4列PhysAddr对应p_paddr字段,表示本Program Header被加载到内存时,物理内存地址的起始位置,一般不用关心,也等同于p_vaddr字段,占用8Bytes。在某些嵌入式设备中,需要手动指定内存时会有不同。
  5. 第5列FileSiz对应p_filesz字段,表示本Program Header在文件中的大小,占用8Bytes。
  6. 第6列MemSiz对应p_memsz字段,表示本Program Header在内存中将占用的大小,占用8Bytes。p_memsz >= p_filesz,一般情况下为等于,在某些初始化字段优化时,可能出现大于情况。
  7. 第7列Flags对应p_flags字段,表示本Program Header的权限,占用4Bytes。R(可读)、W(可写)、X(可执行)
  8. 第8列Align对应p_align字段,表示本Program Header加载到内存后的对齐,占用8Bytes。如:第二个LOAD类型的字段的对齐为0x1000(4096),即系统内存分页的大小。
    Program Header(加载视角)与Section Header(存储视角)共同用于描述ELF文件。索引为01, 02, 03的三个Segment类型为LOAD,表示本so的这3个Segment将加载到内存,并以映像的方式表示。通过Section to Segment mapping也可以看到,这3个Segment依次按顺序对应Section段中的内容。

strip做了什么

最终发布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个字段:

  • Start of section headers由7809600变为990152
  • Number of section headers由36变为27
  • Section header string table index由34变为26
    由此可见,strip主要是删掉了一些了Section,而Program Header Table则保持不变,也就是说strip与so的加载与运行无关,只与文件存储有关,我们接下来验证一下。

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个步骤:

  1. 动态连接器的Bootstrap
  2. 装载共享对象
  3. 重定位和初始化

动态连接器的Bootstrap

可执行程序可能依赖于其他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]
  1. 第1列为VMA的开始结束地址。
  2. 第2列为VMA的权限。r(可读)、w(可写)、x(可执行)、p(私有)
  3. 第3列为VMA的在映像文件中的偏移
  4. 第4列为VMA的映像文件所在设备的主次设备号
  5. 第5列为VMA的映像文件的节点号
  6. 第6列为VMA的映像文件路径

可以看到libc++_shared.so被映射了3次,分别对应Segment Header中的3个Type为LOAD的Segment。本次加载在内存中的VMA的顺序与文件中的Program Header顺序一致,并且3次映射一一对应。
以第一次映射举例:

  • 开始地址0x7c1d340000,结束地址0x7c1d42a000,差值为0xea000,这个值就是Segment Header中的p_memsz值0x00000000000e91a0按照0x1000对齐后的结果。权限为r-xp,意味着可读可执行私有,从Section to Segment mapping中可以看到,此Segment的内容主要是.eh_frame .text .plt等,这些Section是需要可读,因为内容需要执行,并且不允许写入,因为编译最终写死的代码不可修改。
  • 这个Segment在文件中的偏移为0x00000000,而ELF文件开头必然是ELF Header,所以可以这块内存的前64个字节内容为ELF Header。感兴趣的朋友可以打印内存做二进制对比试试。

从最后一行存在一些有意义的路径,举例说几个:

  • [anon:libc_malloc]为应用的堆内存区域,即:通过malloc/calloc/realloc调用申请的内存,anno表示匿名(anonymous),如果看到多个这种VMA,则有可能是导入的so用了不同的CRT版本,因为malloc的内存是CRT维护的。
  • /apex/com.android.runtime/lib64/bionic/libdl.so为上面提到的动态连接器so
  • [stack]为应用的栈内存区域
  • [vdso]Virtual Dynamic Shared Object为内核模块,进程可以通过访问这个VMA来跟内核进行通信
    由于so被加载到内存是按照映像方式进行的,所以so又叫做image(映像)文件(可执行文件也叫映像文件)。

重定位和初始化

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段,在进程退出时也会执行。

so依赖的其他so信息保存在哪里?

程序加载某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的动态链接库与可执行文件有什么区别?

先上粗略的结论,通常可以认为两者在功能上没啥区别,一个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。

  • .interp段在可执行文件main中用于指定动态连接器的位置,main.so本来就是动态链接库,所以不需要.interp。
  • .note.ABI-tag为GNU规定的可执行文件中必须包含的段,其包含了GNU的一些信息,在so中没有此规定。
  • .got.plt前面提到过,用于将模块外部的函数地址写成单独的段,在延迟加载时做重定位使用。由于可执行文件main本身不需要延迟加载,所以没有这个段。

通过打印Program Header看到,比起main.so,main中多出了PHDR、INTERP两个Segment

  • PHDR在可执行文件main中表示Segment表本身,main.so中不包含
  • INTERP跟Section Header中的.interp段完全一致

通过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

(0)

相关推荐