第三章:目标文件里有什么

发布时间 2023-07-17 13:11:39作者: 梦过无声

3.2 目标文件是什么样

总体来说,目标文件编译后主要分成两段:程序指令和程序数据. 代码(.text)属于指令段,而.data和.bss段属于程序数据

为什么要分开?

  1. 安全 程序映射到内存区域, 程序指令区域内存映射为只读,程序数据区域内存映射为可读写
  2. cpu缓存 现代CPU设计成数据缓存和指令缓存,对CPU命中缓存率提高有好处
  3. 最重要原因 系统运行多个该程序副本,他的指令都是一样,所以内存只需保存一份该程序指令 (尤其动态链接中)

3.3.2 数据段和只读数据段

.data段保存的是初始化了的全局静态变量和局部静态变量

.rodata段存放的是只读数据(const修饰的变量)

.bss段存放的是未初始化的全局变量和局部静态变量


3.3.4 其他段

.作为前缀,表示这些表的名字是系统保留的

自定义段,GCC提供扩展机制,使得程序可以指定变量所处段(自定义段名不能使用.命名)

__attribute__ ((section("FOO"))) int global = 42;
__attribute__ ((section("BAR"))) void foo(){};

3.4.3 ELF文件头

数据结构如下

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;

readelf -h查看elf文件头信息, 其中比较重要的信息(我这是elf64,书中为32)

REL(Relocatable file),代表这是一个重定位文件,对应e_type
Start of section headers: 1104(bytes into file) 是段表在文件的偏移,对应e_shoff

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:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          1104 (bytes into file)
  Flags:                             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:         13
  Section header string table index: 12

3.4.2 段表

段表数据结构

{
  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;


3.4.3 重定位表

链接器在处理目标文件时,需要对目标文件中的某些部分进行重定位,即代码段和数据段中那些绝对地址的引用位置,这些重定位信息记录在ELF文件的重定位表中, 对于每个需要重定位的代码段和数据段,都有个对应的重定位表

比如SimpleSection.o中的.rela.text就是针对.text的重定位表, 因为.text中至少有一个绝对地址的引用,那就是printf函数的调用,而.data则没有,它只包含几个常量


$ readelf -S SimpleSection.o 
There are 13 section headers, starting at offset 0x450:

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
       0000000000000057  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000340                
       0000000000000078  0000000000000018   I      10     1     8                   // link表示符号表下标为10 info表示作用与下标为1的段(这里也就是.text段)
  [ 3] .data             PROGBITS         0000000000000000  00000098
       0000000000000008  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  000000a0
       0000000000000004  0000000000000000  WA       0     0     4
  [ 5] .rodata           PROGBITS         0000000000000000  000000a0
       0000000000000004  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  000000a4
       000000000000002a  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000ce
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  000000d0
       0000000000000058  0000000000000000   A       0     0     8
  [ 9] .rela.eh_frame    RELA             0000000000000000  000003b8
       0000000000000030  0000000000000018   I      10     8     8
  [10] .symtab           SYMTAB           0000000000000000  00000128
       0000000000000198  0000000000000018          11    11     8
  [11] .strtab           STRTAB           0000000000000000  000002c0
       000000000000007c  0000000000000000           0     0     1
  [12] .shstrtab         STRTAB           0000000000000000  000003e8
       0000000000000061  0000000000000000           0     0     1

.rela.text段的sh_typeSHT_REL类型,常量为1. 表示为重定位表
sh_link表示符号表的下标, 上面的值为10 表示.symtab
sh_info表示作用于哪个段,上面值为1,表示作用与.text


3.4.4 字符串表

ELF中用到许多字符串,比如变量名global_init_var,函数名main,文件名SimpleSection.c,因为字符串长度往往不固定,固定结构表示比较苦难,常见是将字符串全部放到一个表中.

因此在ELF中引用字符串只需给出一个数字下标即可,在ELF文件中也是以段的形式保存,常见段名.strtab字符串表和.shstrtab段表字符串表

ps: 还记得段表嘛? 段表的名字就是在.shstrtab

实战 读取字符表和段表字符串表

参见3.4.2 ELF文件头

段表开始处在文件的偏移为1104,总共13个段,描述段表的结构体大小为64,因此我们可以用hexdump直接读取.strtab.shstrtab在文件的偏移量

.strtab段在文件 12 * 64 + 1104 = 1808处开始
.shstrtab段在文件 12 * 64 + 1104 = 1872处开始

$ hexdump -x -s 1808 -n 64 SimpleSection.o 
0000710    0009    0000    0003    0000    0000    0000    0000    0000
0000720    0000    0000    0000    0000    02c0    0000    0000    0000
0000730    007c    0000    0000    0000    0000    0000    0000    0000
0000740    0001    0000    0000    0000    0000    0000    0000    0000
0000750

$ hexdump -x -s 1872 -n 64 SimpleSection.o 
0000750    0011    0000    0003    0000    0000    0000    0000    0000
0000760    0000    0000    0000    0000    03e8    0000    0000    0000
0000770    0061    0000    0000    0000    0000    0000    0000    0000
0000780    0001    0000    0000    0000    0000    0000    0000    0000
0000790

sh_offset大小为8h, 在Elf64_Shdr结构体中的偏移为24字节, 表示字符串内容在文件的偏移处

因此字符串表.strtab的内容在文件的02c0处 , 段表字符串表在文件的.shstrtab在文件的03e8处,继续hexdump这两处,我们已经看到字符串表中的内容和段表字符串表的内容


3.5 链接的接口-符号

我们将函数和变量名统称为符号,函数名和变量名通称为符号名,符号看作是链接中的粘合剂,链接整个过程基于符号才能正确完成, ELF文件中符号表是一个段.symtab

每个目标文件中都有一个相应符号表,这个表中记录所有用到的符号(readelf -s可查看),每个符号有一个对应的值,叫做符号值,对于变量和函数来说,符号值就是他们的地址

符号分类:

3.5.1 符号表数据结构

符号表的数据结构

typedef struct
{
  Elf64_Word    st_name;        /* Symbol name (string tbl index) */    //对应字符串表的下标
  unsigned char st_info;        /* Symbol type and binding */   //高28位 绑定 低4 类型
  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;


3.5.3 符号修饰和函数签名

这节主要讲C++实现不同函数的重载,通过修饰符号, 例如void foo(int a)void foo(double a),编译器会生成不同的符号和函数签名已区分


3.5.4 extern "C"

memset(void *, int, size_t) 为例, 如果没有extern关键字,编译器会当作C++函数,因此查找符号的时候可能会查找不到

正确的写法

#ifdef __cplusplus
extern "C" {
#endif

void *memset(void *, int, size_t);

#ifdef __cplusplus
}
#endif


3.5.5 弱符号 强符号

gcc中使用__attribure__((wake))定义一个强符号为弱符号,例子:

extern int ext; //既非弱符号也非强符号
int wake; // 弱符号 未初始化
int stronge; //强符号

__attribure__((wake)) wake2 = 2; // 虽然初始化了,但是使用attr定义为弱符号

int main(){} //强符号
  1. 不允许强符号被多次定义
  2. 如果一个符号在不同文件中,分别为强符号和弱符号,那么选中强符号
  3. 如果一个符号在不同文件中都是弱符号,选择占用空间最大的
    比如int global 四字节 double global 8字节 链接后,符号global占8字节

gcc中使用__attribure__((wakeref))定义一个强引用为弱引用,例子:


//此时编译不会报错,因为foo是一个弱引用,foo地址为0
//但会出现运行时错误
__attribure__((wakeref)) void foo(); 

int main(){
    foo();
}