深入ELF文件结构

Last Updated: 2023-06-30 06:31:15 Friday

-- TOC --

Linux平台下二进制文件都是ELF格式,EFL格式的文件有4个部分:ELF Header,Program Headers,Sections,Section Headers。relocatable的.o文件没有Program Headers。

ELF格式标准历史

20世纪90年代,,一些厂商联合起来成立了一个委员会,起草并发布了一个ELF文件格式标准供公开使用,并希望所有人都能遵守这个标准并从中获益。1993年,委员会发布了ELF格式标准。当时参与该委员会的厂商主要是编译器厂商,如Watcom和Borland,CPU厂商,如IBM和Intel,操作系统厂商Windows。1995年,委员会发布了ELF1.2版本标准,自此委员会使命完成,不就就解散了。ELF标准定格在1.2,Windows参与了标准指定,但它却没有在其操作系统中使用ELF,而是用它自己的PE格式。

目标文件和可执行文件格式小历史

object file,executables, .so和.ko文件等,都是ELF格式!

玩转ELF格式,先玩转readelf命令

ELF文件头

ELF文件头给出了此二进制文件的类型,32bit或者64bit,以及用于解析或加载此二进制文件的其它必要数据。用readelf命令的-h参数,查看elf文件头信息。

$ readelf -h test
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:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x401070
  Start of program headers:          64 (bytes into file)
  Start of section headers:          23160 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 30

ELF file header structure defined in /usr/include/elf.h

/* The ELF file header.  This appears at the start of every ELF file.  */

#define EI_NIDENT (16)

typedef struct
{
  unsigned char e_ident[EI_NIDENT];     /* Magic number and other info */
  Elf32_Half    e_type;                 /* Object file type */
  Elf32_Half    e_machine;              /* Architecture */
  Elf32_Word    e_version;              /* Object file version */
  Elf32_Addr    e_entry;                /* Entry point virtual address */
  Elf32_Off     e_phoff;                /* Program header table file offset */
  Elf32_Off     e_shoff;                /* Section header table file offset */
  Elf32_Word    e_flags;                /* Processor-specific flags */
  Elf32_Half    e_ehsize;               /* ELF header size in bytes */
  Elf32_Half    e_phentsize;            /* Program header table entry size */
  Elf32_Half    e_phnum;                /* Program header table entry count */
  Elf32_Half    e_shentsize;            /* Section header table entry size */
  Elf32_Half    e_shnum;                /* Section header table entry count */
  Elf32_Half    e_shstrndx;             /* Section header string table index */
} Elf32_Ehdr;

typedef struct
{
  unsigned char e_ident[EI_NIDENT];     /* Magic number and other info */
  Elf64_Half    e_type;                 /* Object file type */
  Elf64_Half    e_machine;              /* Architecture */
  Elf64_Word    e_version;              /* Object file version */
  Elf64_Addr    e_entry;                /* Entry point virtual address */
  Elf64_Off     e_phoff;                /* Program header table file offset */
  Elf64_Off     e_shoff;                /* Section header table file offset */
  Elf64_Word    e_flags;                /* Processor-specific flags */
  Elf64_Half    e_ehsize;               /* ELF header size in bytes */
  Elf64_Half    e_phentsize;            /* Program header table entry size */
  Elf64_Half    e_phnum;                /* Program header table entry count */
  Elf64_Half    e_shentsize;            /* Section header table entry size */
  Elf64_Half    e_shnum;                /* Section header table entry count */
  Elf64_Half    e_shstrndx;             /* Section header string table index */
} Elf64_Ehdr;

仔细阅读elf.h文件,能够发现,_Half都是16bit,_Word都是32bit,_Addr和_Off与CPU位数有关,32或64。64位环境下ELF头大小为64bytes,32位环境下ELF头大小为52bytes,比32位环境下ELF头多12bytes。magic number的第5个字节不同。32位环境下的magic number为:7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 0045 4c 46就是ELF7fASCII的最后一个符号。64位环境第5个字节为02

e_version: Object file version,固定为1.

e_entry:Entry point virtual address,ELF程序的入口虚拟内存地址,操作系统加载程序后,从这个地址开始执行程序指令。Relocatable file不能直接运行,这个地址为0.

e_shoff: Section header table file offset,section header在文件中的偏移地址。section header并不是紧跟在elf header后面,如下图:

sections

elf文件就是由一个header + sections组成,各个section由section of section header定义!多个section合并成一个segment。

e_shnum: Section header table entry count,elf文件section的数量。

e_shstrndx: Section header string table index,这是一个index,指向一个section,此section内存放了一些字符串,这些字符串是section headers需要使用的。用readelf命令的-x-p参数,可以查看某个具体的section内容,如下:

$ readelf -x13 test.o

Hex dump of section '.shstrtab':
  0x00000000 002e7379 6d746162 002e7374 72746162 ..symtab..strtab
  0x00000010 002e7368 73747274 6162002e 72656c61 ..shstrtab..rela
  0x00000020 2e746578 74002e64 61746100 2e627373 .text..data..bss
  0x00000030 002e726f 64617461 002e636f 6d6d656e ..rodata..commen
  0x00000040 74002e6e 6f74652e 474e552d 73746163 t..note.GNU-stac
  0x00000050 6b002e6e 6f74652e 676e752e 70726f70 k..note.gnu.prop
  0x00000060 65727479 002e7265 6c612e65 685f6672 erty..rela.eh_fr
  0x00000070 616d6500                            ame.

$ readelf -p13 test.o

String dump of section '.shstrtab':
  [     1]  .symtab
  [     9]  .strtab
  [    11]  .shstrtab
  [    1b]  .rela.text
  [    26]  .data
  [    2c]  .bss
  [    31]  .rodata
  [    39]  .comment
  [    42]  .note.GNU-stack
  [    52]  .note.gnu.property
  [    65]  .rela.eh_frame

注意:没有.text字符串!

字符串一般长度不固定不统一,因此如果用固定的长度来表示字符串有困难。用上面这种方式保存字符串,就可以仅仅使用偏移地址,来定位到所有字符串。(字符串的结束符为0x00,这是C语言的选择)

.rodata放的是代码中写死的字符串或常量,比如printf调用中的格式字符串等。.comment内有编译器和编译平台信息。

program headers,请参考下面segment部分内容。

Section Header

如下是section header结构体的定义:

/* Section header.  */

typedef struct
{
  Elf32_Word    sh_name;                /* Section name (string tbl index) */
  Elf32_Word    sh_type;                /* Section type */
  Elf32_Word    sh_flags;               /* Section flags */
  Elf32_Addr    sh_addr;                /* Section virtual addr at execution */
  Elf32_Off     sh_offset;              /* Section file offset */
  Elf32_Word    sh_size;                /* Section size in bytes */
  Elf32_Word    sh_link;                /* Link to another section */
  Elf32_Word    sh_info;                /* Additional section information */
  Elf32_Word    sh_addralign;           /* Section alignment */
  Elf32_Word    sh_entsize;             /* Entry size if section holds table */
} Elf32_Shdr;

typedef struct
{
  Elf64_Word    sh_name;                /* Section name (string tbl index) */
  Elf64_Word    sh_type;                /* Section type */
  Elf64_Xword   sh_flags;               /* Section flags */
  Elf64_Addr    sh_addr;                /* Section virtual addr at execution */
  Elf64_Off     sh_offset;              /* Section file offset */
  Elf64_Xword   sh_size;                /* Section size in bytes */
  Elf64_Word    sh_link;                /* Link to another section */
  Elf64_Word    sh_info;                /* Additional section information */
  Elf64_Xword   sh_addralign;           /* Section alignment */
  Elf64_Xword   sh_entsize;             /* Entry size if section holds table */
} Elf64_Shdr;

_Xword是64位。elf头显示64位环境下,section header大小为64bytes,结合上面的结构体定义,算一下正好64bytes。

$ readelf -S test.o
There are 14 section headers, starting at offset 0x310:

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
       000000000000001b  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000250
       0000000000000030  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  0000005b
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  0000005b
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  0000005b
       000000000000000b  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  00000066
       000000000000002b  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  00000091
       0000000000000000  0000000000000000           0     0     1
  [ 8] .note.gnu.propert NOTE             0000000000000000  00000098
       0000000000000020  0000000000000000   A       0     0     8
  [ 9] .eh_frame         PROGBITS         0000000000000000  000000b8
       0000000000000038  0000000000000000   A       0     0     8
  [10] .rela.eh_frame    RELA             0000000000000000  00000280
       0000000000000018  0000000000000018   I      11     9     8
  [11] .symtab           SYMTAB           0000000000000000  000000f0
       0000000000000138  0000000000000018          12    10     8
  [12] .strtab           STRTAB           0000000000000000  00000228
       0000000000000028  0000000000000000           0     0     1
  [13] .shstrtab         STRTAB           0000000000000000  00000298
       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)

用readelf命令的-S参数,查看elf文件section headers,以及其offset!与elf header信息能够对应起来。这部分内容,与定义section header的结构体也能够对应起来。

Type 含义
PROGBITS 程序指令或数据
SYMTAB 符号表
STRTAB 字符串表
RELA 重定位表,包含了重定位信息
NOTE note info
NOBITS .bss段特有,没有内容

sh_name: Section name (string tbl index),e_shstrndx指向的section内的偏移,能够直接把section name取出来。

sh_entsize: Entry size if section holds table,section的内容是table,这个字段定义table的大小,正如elf header中定义了section header的大小一样,有这个size信息,C代码可以很方便的通过指针+1,来实现遍历。

类型为PROGBITS的段,有需要在链接的时候做重定位的,在object文件中,都有一个对应的.rela段。如上面的示例,有两个.rela段。此时,section header结构体中的link字段就不再是0,而是指向.symtab符号表section的编号,info字段指向此rela段作用于哪个段,比如上例,section2->section1,section10->section9。

从elf头获得section headers number和offset,还有固定的header size,以及section header需要的string table,配合section header structure结构,有了这些信息,就可以解析出每一个section!

Symbol Table

下面是符号表的结构体定义:

/* Symbol table entry.  */

typedef struct
{
  Elf32_Word    st_name;                /* Symbol name (string tbl index) */
  Elf32_Addr    st_value;               /* Symbol value */
  Elf32_Word    st_size;                /* Symbol size */
  unsigned char st_info;                /* Symbol type and binding */
  unsigned char st_other;               /* Symbol visibility */
  Elf32_Section st_shndx;               /* Section index */
} Elf32_Sym;

typedef struct
{
  Elf64_Word    st_name;                /* Symbol name (string tbl index) */
  unsigned char st_info;                /* Symbol type and binding */
  unsigned char st_other;               /* Symbol visibility */
  Elf64_Section st_shndx;               /* Section index */
  Elf64_Addr    st_value;               /* Symbol value */
  Elf64_Xword   st_size;                /* Symbol size */
} Elf64_Sym;

32位和64位的结构体,只是成员顺序不一样,仔细看看,内容都是一样的!st_name字段是指向.strtab段的指针,与section headers结构体中指向.shstrtab原理一样。

$ readelf -p12 test.o

String dump of section '.strtab':
  [     1]  test.c
  [     8]  cc.2319
  [    10]  bb.2318
  [    18]  g_var1
  [    1f]  g_var2
  [    26]  main
  [    2b]  _GLOBAL_OFFSET_TABLE_
  [    41]  puts
  [    46]  printf

查看文件符号表的命令有:readelf -s <filename>nm <filename>,当前,这两个命令也都有很多更复杂更具体的参数可以使用。(nm命令的man page内有很多有用的信息,它的输出更简洁)

$ readelf -s test.o

Symbol table '.symtab' contains 18 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS test.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     4 OBJECT  LOCAL  DEFAULT    4 cc.2319
     7: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 bb.2318
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT    9 
    11: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
    12: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 g_var1
    13: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM g_var2
    14: 0000000000000000    52 FUNC    GLOBAL DEFAULT    1 main
    15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    16: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts
    17: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

$ nm test.o
0000000000000004 d bb.2318
0000000000000000 b cc.2319
                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 D g_var1
0000000000000004 C g_var2
0000000000000000 T main
                 U printf
                 U puts

符号表的重要性不言而喻,链接过程就靠它。符号分为LOCALGLOBALWEAKGLOBAL是外部可见的全局符号,可被其它目标文件引用。符号类型有file,section,object,func,或notype。OBJECT类型是指各种类型的全局变量,FUNC是指函数,即代码。

st_shndx: Section index,符号所在section的index,对应Ndx这一列。ABS表示一个绝对值,值为0xFFF1。COMcommon块,未初始化的全局符号,值为0xFFF2。UND就是未定义,未在此目标文件中定义(外部符号),值为0。cc是个未定义的静态变量,被放在了section 4,bss段。bb是已初始化的静态变量,g_var1是已初始化的全局变量,都在section 3,data段。

st_value: Symbol value,在目标文件中,如果是非COM的GLOBAL符号,这个值表示符号所在段的offset。如果是COM的符号,这个值表示此符号的对齐属性,应该是此符号的size。在可执行文件中,这个value表示符号的虚拟地址!因此可以理解为,此value基本上就是符号的地址,要么是所在section的offset,要么是virtual address。

第1个符号,永远是一个未定义的符号!(我怀疑是为了让真正的符号的编号从1开始,故意在前面把0的位置占住)

bbcc是两个静态变量,但在符号表中,这两个符号后面有suffix,这是符号修饰(Name Decoration),目的是为了支持在不同函数中可以有相同名称的静态变量。链接过程不能出现相同名称的符号!C++的函数名重载,namespace等特性,在编译器层面,都要通过修饰的方式来使其具有不同的符号名称!C++的符号修饰和extern "C"

函数签名,Function Signature,包含函数名,参数列表,它所在的类或namespace,以及其它用于唯一辨识此函数的信息。函数签名用于识别不同的函数,就像签名用于识别不同的人一样,函数的名称还是签名的一部分。

符号的定义分为强符号和弱符号!符号的引用分强引用和弱引用。

GCC使用__attribute__((weak))使某个定义成为弱符号,使用__attribute__((weafref))使某个引用成为弱引用。默认的函数和全局变量都是强符号(gcc现在默认使用-fno-common),默认的引用都是强引用。符号和引用的强弱,体现在链接规则上:

  1. 不允许强符号被多次定义;
  2. 如果一个符号在某个文件中被定义为强符号,而在其它文件中被定义为弱符号,链接时选择强符号;
  3. 如果一个符号在所有文件中都是弱符号,链接时选择占用空间大的那个;(谁会这么用呢?不如报错?)
  4. 弱引用的符号如果没有定义,链接器不报错,但运行时会发生错误;一般对于未定义的弱引用,链接器默认其值为0,以便程序可以识别。

Relocation Section

每一个.text段,如果里面含有需要重定位的符号,就会有一个对应的.rela(或者.rel)段,包含了所有需要在链接的时候,重定位的符号信息。

/* Relocation table entry without addend (in section of type SHT_REL).  */

typedef struct
{
  Elf32_Addr    r_offset;               /* Address */
  Elf32_Word    r_info;                 /* Relocation type and symbol index */
} Elf32_Rel;

/* I have seen two different definitions of the Elf64_Rel and
   Elf64_Rela structures, so we'll leave them out until Novell (or
   whoever) gets their act together.  */
/* The following, at least, is used on Sparc v9, MIPS, and Alpha.  */

typedef struct
{
  Elf64_Addr    r_offset;               /* Address */
  Elf64_Xword   r_info;                 /* Relocation type and symbol index */
} Elf64_Rel;

/* Relocation table entry with addend (in section of type SHT_RELA).  */

typedef struct
{
  Elf32_Addr    r_offset;               /* Address */
  Elf32_Word    r_info;                 /* Relocation type and symbol index */
  Elf32_Sword   r_addend;               /* Addend */
} Elf32_Rela;

typedef struct
{
  Elf64_Addr    r_offset;               /* Address */
  Elf64_Xword   r_info;                 /* Relocation type and symbol index */
  Elf64_Sxword  r_addend;               /* Addend */
} Elf64_Rela;

Rel和Rela是两种不同的结构,后者多一个Addend。如果gcc用-m32编译,得到的符号表使用rel,如果是64位环境,得到的符号表使用rela。

r_offset是符号相对于它的.text段的偏移地址,r_info包含了符号类型和在symbol table中的index。目标文件在未链接时,引用的外部地址都是不确定的,一般编译器会用0占位,在链接过程中分配虚拟地址,此时各个符号的地址就都确定了!(动态链接除外)

$ readelf -r test.o

Relocation section '.rela.text' at offset 0x2e0 contains 10 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000006  000300000002 R_X86_64_PC32     0000000000000000 .data + 0
00000000000c  000400000002 R_X86_64_PC32     0000000000000000 .bss + 4
000000000013  00060000000a R_X86_64_32       0000000000000000 .rodata + 0
00000000001d  000c00000004 R_X86_64_PLT32    0000000000000000 printf - 4
00000000002a  000a00000002 R_X86_64_PC32     0000000000000000 c - 4
000000000030  000400000002 R_X86_64_PC32     0000000000000000 .bss + 0
000000000036  000900000002 R_X86_64_PC32     0000000000000000 a - 4
00000000003d  00060000000a R_X86_64_32       0000000000000000 .rodata + 7
000000000047  000c00000004 R_X86_64_PLT32    0000000000000000 printf - 4
000000000051  000b00000004 R_X86_64_PLT32    0000000000000000 f - 4

Info高位部分是符号的index(在符号表中的index),最后边的Sym. Name与index对应。低位部分表示Type,同时存在Type列,将数字和具体类型名称对应。Offset列是符号所在section的偏移。下面是对应的符号表:

$ readelf -s test.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 test.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 .data
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 .bss
     5: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    4 b
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 .rodata
     7: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 e.1
     8: 0000000000000008     4 OBJECT  LOCAL  DEFAULT    4 d.0
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    4 a
    10: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 c
    11: 0000000000000000    36 FUNC    GLOBAL DEFAULT    1 f
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    13: 0000000000000024    56 FUNC    GLOBAL DEFAULT    1 main

以上两段打印,对应的源码是:

#include <stdio.h>

int a;
static int b;
int c = 3;

void f(){
    static int d;
    static int e = 5;
    printf("%d %d\n", d, e);
}

int main(){
    printf("%d %d %d\n", a,b,c);
    f();
    return 0;
}

R_X86_64_PC32

在x64环境下,这是很常见的重定位方式,即基于PC指针(RIP)的重定位,32可能是表示需要修正位置的长度。

Relocation section '.rela.text' at offset 0x2e0 contains 10 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000006  000300000002 R_X86_64_PC32     0000000000000000 .data + 0
...

在链接时,在.text section的0006偏移的位置,填写.data+0-rip+4。链接器会确定这个.o文件的.text地址和.data地址,上面显示的是偏移(不是最后合并后的.text或.data地址,链接时相同section合并),但rip地址是最后分配后的地址。

R_X86_64_32

绝对地址,重定位时,-rip+4就没有了。

Debug Section

gcc编译时,加上-g参数,可以生成带有debug段的elf文件,debug段的类型为PROGBITS

ELF文件采用一种叫DWARF(Debug With Arbitrary Record Format)的标准的调试信息格式,2006年DWARF标准委员会发布了DWARF3版本。Microsoft使用自己的格式,叫做CodeView。

值得一提的是,debug段的size比正常代码段和数据段大好几倍。

理解Segment

前面的所有内容,都在说section,这是linking view。而segment,是execution view!

linking时,具有相同权限的section会被编排在一起,地址连续。此时,这些编排在一起的section,就形成了一个segment!链接时,链接器关心的是section,而当可执行程序在被OS加载到内存时,OS关心的是segment,OS按segment分配page。如果OS按section分配page,那么每个需要load进内存的section,必然会至少占用一个page,而如果按segment分配page,可以有效节省内存。

ELF文件头中的program headers,指向的就是segment。因此,.o文件的elf头看不到grogram header,只有可执行的elf文件才有。

$ readelf -l test

Elf file type is EXEC (Executable file)
Entry point 0x401040
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000002d8 0x00000000000002d8  R      0x8
  INTERP         0x0000000000000318 0x0000000000400318 0x0000000000400318
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000000510 0x0000000000000510  R      0x1000
  LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000
                 0x0000000000000191 0x0000000000000191  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000402000 0x0000000000402000
                 0x0000000000000104 0x0000000000000104  R      0x1000
  LOAD           0x0000000000002e10 0x0000000000403e10 0x0000000000403e10
                 0x000000000000021c 0x0000000000000230  RW     0x1000
  DYNAMIC        0x0000000000002e20 0x0000000000403e20 0x0000000000403e20
                 0x00000000000001d0 0x00000000000001d0  RW     0x8
  NOTE           0x0000000000000338 0x0000000000400338 0x0000000000400338
                 0x0000000000000040 0x0000000000000040  R      0x8
  NOTE           0x0000000000000378 0x0000000000400378 0x0000000000400378
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_PROPERTY   0x0000000000000338 0x0000000000400338 0x0000000000400338
                 0x0000000000000040 0x0000000000000040  R      0x8
  GNU_EH_FRAME   0x0000000000002024 0x0000000000402024 0x0000000000402024
                 0x0000000000000034 0x0000000000000034  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000002e10 0x0000000000403e10 0x0000000000403e10
                 0x00000000000001f0 0x00000000000001f0  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
   03     .init .plt .text .fini
   04     .rodata .eh_frame_hdr .eh_frame
   05     .init_array .fini_array .dynamic .got .got.plt .data .bss
   06     .dynamic
   07     .note.gnu.property
   08     .note.gnu.build-id .note.ABI-tag
   09     .note.gnu.property
   10     .eh_frame_hdr
   11
   12     .init_array .fini_array .dynamic .got

只有type为LOAD的segment,才会被OS加载。注意看上面展示的打印,Section to Segment mapping这部分,这里清晰的说明了哪些section被合并到了那个segment。

Dynamic Section

基本上现在编译的可执行程序,都是动态链接,又能有些程序私有的库是静态链接,但谁还会静态链接libc呢!EFL中定义了一个.dynamic section,用来存放动态链接相关信息。配套的还有.dynstr.dynsym.rela.dyn section。

/* Dynamic section entry.  */

typedef struct
{
  Elf32_Sword   d_tag;                  /* Dynamic entry type */
  union
    {
      Elf32_Word d_val;                 /* Integer value */
      Elf32_Addr d_ptr;                 /* Address value */
    } d_un;
} Elf32_Dyn;

typedef struct
{
  Elf64_Sxword  d_tag;                  /* Dynamic entry type */
  union
    {
      Elf64_Xword d_val;                /* Integer value */
      Elf64_Addr d_ptr;                 /* Address value */
    } d_un;
} Elf64_Dyn;

d_tag表示type,然后要么是d_val,要么是d_ptr。

如下是type list:

/* Legal values for d_tag (dynamic entry type).  */

#define DT_NULL         0               /* Marks end of dynamic section */
#define DT_NEEDED       1               /* Name of needed library */
#define DT_PLTRELSZ     2               /* Size in bytes of PLT relocs */
#define DT_PLTGOT       3               /* Processor defined value */
#define DT_HASH         4               /* Address of symbol hash table */
#define DT_STRTAB       5               /* Address of string table */
#define DT_SYMTAB       6               /* Address of symbol table */
#define DT_RELA         7               /* Address of Rela relocs */
#define DT_RELASZ       8               /* Total size of Rela relocs */
#define DT_RELAENT      9               /* Size of one Rela reloc */
#define DT_STRSZ        10              /* Size of string table */
#define DT_SYMENT       11              /* Size of one symbol table entry */
#define DT_INIT         12              /* Address of init function */
#define DT_FINI         13              /* Address of termination function */
#define DT_SONAME       14              /* Name of shared object */
#define DT_RPATH        15              /* Library search path (deprecated) */
#define DT_SYMBOLIC     16              /* Start symbol search here */
#define DT_REL          17              /* Address of Rel relocs */
#define DT_RELSZ        18              /* Total size of Rel relocs */
#define DT_RELENT       19              /* Size of one Rel reloc */
#define DT_PLTREL       20              /* Type of reloc in PLT */
#define DT_DEBUG        21              /* For debugging; unspecified */
#define DT_TEXTREL      22              /* Reloc might modify .text */
#define DT_JMPREL       23              /* Address of PLT relocs */
#define DT_BIND_NOW     24              /* Process relocations of object */
#define DT_INIT_ARRAY   25              /* Array with addresses of init fct */
#define DT_FINI_ARRAY   26              /* Array with addresses of fini fct */
#define DT_INIT_ARRAYSZ 27              /* Size in bytes of DT_INIT_ARRAY */
#define DT_FINI_ARRAYSZ 28              /* Size in bytes of DT_FINI_ARRAY */
#define DT_RUNPATH      29              /* Library search path */
#define DT_FLAGS        30              /* Flags for the object being loaded */
#define DT_ENCODING     32              /* Start of encoded range */
#define DT_PREINIT_ARRAY 32             /* Array with addresses of preinit fct*/
#define DT_PREINIT_ARRAYSZ 33           /* size in bytes of DT_PREINIT_ARRAY */
#define DT_SYMTAB_SHNDX 34              /* Address of SYMTAB_SHNDX section */
#define DT_NUM          35              /* Number used */
#define DT_LOOS         0x6000000d      /* Start of OS-specific */
#define DT_HIOS         0x6ffff000      /* End of OS-specific */
#define DT_LOPROC       0x70000000      /* Start of processor-specific */
#define DT_HIPROC       0x7fffffff      /* End of processor-specific */
#define DT_PROCNUM      DT_MIPS_NUM     /* Most used by any processor */

DT_NEEDED类型存放的值,是.dynstr section的偏移。

rpath

有的时候我们编译出来的代码,copy到别的机器上运行,会提示找不到某个动态链接库,可能就是rpath在作怪。gcc编译的时候,可以给ld传入rpath参数,但一般不会使用。(记忆中,在x64环境下,模拟运行risc-v程序的时候,需要指定rpath,因为需要使用完全不同的一套lib库)

#define DT_RPATH        15              /* Library search path (deprecated) */

elf.h文件中,已经在注释位置说明,deprecated,但实际上还有有可能会遇到。

我个人的经验是,如果elf中存在rpath,环境变量LD_LIBRARY_PATH就失效了,动态链接器只在rpath指定的路径中搜索。有个小工具,可以修改elf文件的rpath,chrpath

$ readelf -d libHSRCommon.so.1.0.0

Dynamic section at offset 0x47af0 contains 36 entries:
  标记        类型                         名称/值
...
 0x000000000000000e (SONAME)             Library soname: [libHSRCommon.so.1]
 0x000000000000001d (RUNPATH)            Library runpath: [/home/astute/work/HSR-Windows/Source/Config/../../Target/x86_64/release:/usr/local/qt5.15.2-x64/lib]
...

本文链接:https://cs.pynote.net/sf/c/cdm/202111232/

-- EOF --

-- MORE --