Logo
ELF之符号表、字符串表和哈希表之间的关系

符号表、字符串表和哈希表在 ELF 文件中扮演着不同的角色,但它们相互关联,共同为程序的链接和执行提供支持。

符号表

字符串表

哈希表

三者之间的关系

总结

当我们引用函数 invoker 时:

  1. 编译器会将 invoker 加入到当前模块的符号表中。
  2. invoker 的名字 “invoker” 会被添加到字符串表中。
  3. invoker 的哈希值会被计算出来,并存储在哈希表中。
  4. 链接器在链接时,会通过哈希表快速找到 invoker 在符号表中的位置,从而确定其地址。

形象地比喻:

举例说明:

假设我们有一个 C 程序,其中定义了一个函数 invoker:

void invoker() {
    // 函数体
}

编译器在编译这个文件时,会生成一个 .o 文件,其中包含:

当链接器将多个 .o 文件链接成一个可执行文件时,会将所有的符号表合并,并创建一个全局的哈希表。这样,当程序运行时,动态链接器就可以通过哈希表快速找到所需的符号。

总结:

符号表、字符串表和哈希表是 ELF 文件中非常重要的组成部分,它们共同保证了程序的正确链接和执行。理解它们之间的关系有助于我们更好地理解编译链接的过程。

  struct Elf64_Phdr {
    PT p_type;            //段的类型,如 PT_LOAD 表示可加载段
    PF p_flags;          //段的属性,例如可读、可写、可执行
    Elf64_Off p_offset;  // 段在文件中的偏移
    Elf64_Addr p_vaddr;   //段在虚拟内存中的起始地址
    Elf64_Addr p_paddr;   //段在物理内存中的起始地址(一般与虚拟地址相同)
    Elf64_Xword p_filesz; //段在文件中的大小
    Elf64_Xword p_memsz;  //段在内存中的大小
    Elf64_Xword p_align;  //段的对齐要求
    
    if (p_offset >= 0 && p_filesz > 0 && (p_offset + p_filesz) <= std::mem::size() && p_filesz <= std::mem::size())
        u8 p_data[p_filesz] @ p_offset [[sealed]];
};
  enum SHT : Elf32_Word {
    NULL                   = 0x00, // 空段,通常作为占位符。
    PROGBITS               = 0x01, // 程序数据,包含可执行代码、已初始化数据或其他程序数据。
    SYMTAB                 = 0x02, // 符号表,包含定义和使用的符号信息。
    STRTAB                 = 0x03, // 字符串表,包含其他段使用的字符串。
    RELA                   = 0x04, // 带有附加信息的重定位条目,用于动态链接。
    HASH                   = 0x05, // 符号哈希表,用于高效的符号查找。
    DYNAMIC                = 0x06, // 动态链接信息,用于加载和链接共享库。
    NOTE                   = 0x07, // 包含任意形式的注释数据
    NOBITS                 = 0x08, // 没有内容的段,但在文件中占位。
    REL                    = 0x09, // 不带附加信息的重定位条目。
    SHLIB                  = 0x0A, // 预留给共享库使用
    DYNSYM                 = 0x0B, // 动态符号表,用于动态链接。
    UNKNOWN12              = 0x0C,
    UNKNOWN13              = 0x0D,
    INIT_ARRAY             = 0x0E, //程序初始化或终止时调用的函数指针数组
    FINI_ARRAY             = 0x0F, //程序初始化或终止时调用的函数指针数组
    PREINIT_ARRAY          = 0x10, //程序初始化或终止时调用的函数指针数组
    GROUP                  = 0x11, //段分组信息。
    SYMTAB_SHNDX           = 0x12, //符号表中节名称的索引。
    GNU_INCREMENTAL_INPUTS = 0x6FFF4700,
    GNU_ATTRIBUTES         = 0x6FFFFFF5,
    GNU_HASH               = 0x6FFFFFF6,
    GNU_LIBLIST            = 0x6FFFFFF7,
    CHECKSUM               = 0x6FFFFFF8,
    SUNW_move              = 0x6FFFFFFA,
    SUNW_COMDAT            = 0x6FFFFFFB,
    SUNW_syminfo           = 0x6FFFFFFC,
    GNU_verdef             = 0x6FFFFFFD,
    GNU_verneed            = 0x6FFFFFFE,
    GNU_versym             = 0x6FFFFFFF,
    ARM_EXIDX              = 0x70000001,
    ARM_PREEMPTMAP         = 0x70000002,
    ARM_ATTRIBUTES         = 0x70000003,
    ARM_DEBUGOVERLAY       = 0x70000004,
    ARM_OVERLAYSECTION     = 0x70000005,
};

  enum PT : Elf32_Word {
    NULL         = 0x00, // 无效段,通常用作占位符
    LOAD         = 0x01, // 可加载段。这个段会被加载到内存中,并可以被执行或读写。代码段和数据段通常都是 LOAD 类型。
    DYNAMIC      = 0x02, // 动态链接信息段。包含了动态链接器所需的信息,如动态符号表、重定位信息等
    INTERP       = 0x03, //  解释器段。指定了动态链接器的路径。
    NOTE         = 0x04, //  附注段。包含了一些额外的信息,比如构建信息、版权信息等。
    SHLIB        = 0x05, //  预留给共享库使用。
    PHDR         = 0x06, //  程序头表段。包含了程序头表本身的信息。
    TLS          = 0x07, //  线程局部存储段。用于存储线程私有的数据。
    LOOS         = 0x60000000, //  系统特定的段类型,范围从 LOOS 到 HIOS。
    HIOS         = 0x6FFFFFFF,
    GNU_EH_FRAME = PT::LOOS + 0x474E550, //用于异常处理的段。
    GNU_STACK    = PT::LOOS + 0x474E551, //指示栈的增长方向和属性。
    GNU_RELRO    = PT::LOOS + 0x474E552, //  只读重定位段。
    GNU_PROPERTY = PT::LOOS + 0x474E553, //属性段。包含一些额外的属性信息。
    SUNWBSS      = 0x6FFFFFFA, //SunOS 系统特定的段类型。
    SUNWSTACK    = 0x6FFFFFFB, //SunOS 系统特定的段类型。
    ARM_ARCHEXT  = 0x70000000, // ARM 架构特定的段类型。
    ARM_UNWIND   = 0x70000001, // ARM 架构特定的段类型。
};