从实践理解《程序员的自我修养》(1)

从实践理解《程序员的自我修养》(1)

前言

这篇文档主要从实践的角度充分理解《程序员的自我修养》一书中提到的细节。书中提到的各种机制、数据结构,我都将在实际系统中找到并理解它们。
环境:Ubuntu 20.04

目标文件里有什么

示例代码

# a.c
#include <stdio.h>

extern int shared;

int main()
{
    int a = 100;
    printf("hello, World.");
    swap(&a, &shared);
}
# b.c
int shared = 1;

void swap(int *a, int *b)
{
    *a ^= *b ^= *a ^= *b;
}

可执行文件ab为a.o和b.o链接后得到。接下来查看的ELF文件都是ab。

ELF文件的结构

ELF文件头
readelf -h ab
ELF 头:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              DYN (共享目标文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:               0x1080
  程序头起点:          64 (bytes into file)
  Start of section headers:          14848 (bytes into file)
  标志:             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
Sections(节)

我尽量不用去描述Section这个概念,因为后面还有Segment的定义,容易混淆。

readelf -t ab
There are 31 section headers, starting at offset 0x3a00:

节头:
  [号] 名称
       类型              地址              偏移量             链接
       大小              全体大小          信息               对齐
       旗标
  [ 0] 
       NULL             0000000000000000  0000000000000000  0
       0000000000000000 0000000000000000  0                 0
       [0000000000000000]: 
  [ 1] .interp
       PROGBITS         0000000000000318  0000000000000318  0
       000000000000001c 0000000000000000  0                 1
       [0000000000000002]: ALLOC
  ...
  [16] .text
       PROGBITS         0000000000001080  0000000000001080  0
       0000000000000215 0000000000000000  0                 16
       [0000000000000006]: ALLOC, EXEC
  ...
  [25] .data
       PROGBITS         0000000000004000  0000000000003000  0
       0000000000000014 0000000000000000  0                 8
       [0000000000000003]: WRITE, ALLOC
  [26] .bss
       NOBITS           0000000000004014  0000000000003014  0
       0000000000000004 0000000000000000  0                 1
       [0000000000000003]: WRITE, ALLOC
  [27] .comment
       PROGBITS         0000000000000000  0000000000003014  0
       000000000000002a 0000000000000001  0                 1
       [0000000000000030]: MERGE, STRINGS
  [28] .symtab
       SYMTAB           0000000000000000  0000000000003040  29
       0000000000000678 0000000000000018  47                8
       [0000000000000000]: 
  [29] .strtab
       STRTAB           0000000000000000  00000000000036b8  0
       000000000000022d 0000000000000000  0                 1
       [0000000000000000]: 
  [30] .shstrtab
       STRTAB           0000000000000000  00000000000038e5  0
       000000000000011a 0000000000000000  0                 1
       [0000000000000000]: 
Section header table

我们之前读取的section信息应该都是通过Section header table得到的。文件的主体应该是一个个section,它们关系到程序的具体功能,但是如果只想了解程序的大致结构信息,也许Section header table就足够概括这些section的特性了。

书中给出了Section header table在Linux中的实现,它是一个结构体数组,结构体名称为Elf32_Shdr,也就是所谓Section header。我使用的机器是64位的,所以我的/usr/include/elf.h里还有64位的Elf64_Shdr。

/* 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;

可以看到我们之前用readelf -t ab命令读取到的信息都能在这里的结构体上对应到。32位和64位的成员定义除了类型几乎没有区别。

重点Section

字符串表

.strtab是字符串表(STRING TABLE)
.shstrtab是段表字符串表(Section Header String Table),针对段表
.symtab是符号表,一般是变量、函数

shstrtab及symtab经常引用strtab中的字符串。

readelf -x 29 ab

“.strtab”节的十六进制输出:
  0x00000000 00637274 73747566 662e6300 64657265 .crtstuff.c.dere
  0x00000010 67697374 65725f74 6d5f636c 6f6e6573 gister_tm_clones
  0x00000020 005f5f64 6f5f676c 6f62616c 5f64746f .__do_global_dto
  0x00000030 72735f61 75780063 6f6d706c 65746564 rs_aux.completed
  ...
  0x00000210 66696e61 6c697a65 4040474c 4942435f finalize@@GLIBC_
  0x00000220 322e322e 35007368 61726564 00       2.2.5.shared.
  
  
readelf -p 29 ab

String dump of section '.strtab':
  [     1]  crtstuff.c
  [     c]  deregister_tm_clones
  [    21]  __do_global_dtors_aux
  [    37]  completed.8060
  [    46]  __do_global_dtors_aux_fini_array_entry
  [    6d]  frame_dummy
  [    79]  __frame_dummy_init_array_entry
  [    98]  a.c
  [    9c]  b.c
  [    a0]  __FRAME_END__
  [    ae]  __init_array_end
  [    bf]  _DYNAMIC
  [    c8]  __init_array_start
  [    db]  __GNU_EH_FRAME_HDR
  [    ee]  _GLOBAL_OFFSET_TABLE_
  [   104]  __libc_csu_fini
  [   114]  _ITM_deregisterTMCloneTable
  [   130]  _edata
  [   137]  __stack_chk_fail@@GLIBC_2.4
  [   153]  printf@@GLIBC_2.2.5
  [   167]  __libc_start_main@@GLIBC_2.2.5
  [   186]  __data_start
  [   193]  __gmon_start__
  [   1a2]  __dso_handle
  [   1af]  _IO_stdin_used
  [   1be]  __libc_csu_init
  [   1ce]  __bss_start
  [   1da]  main
  [   1df]  __TMC_END__
  [   1eb]  _ITM_registerTMCloneTable
  [   205]  swap
  [   20a]  __cxa_finalize@@GLIBC_2.2.5
  [   226]  shared

ELF文件中,有些Sectoin会被装载,有些不会,在之前的Sections信息中可以看到.strtab并没有装载地址信息,说明它确实不会被装载。那么它应该只有在链接等环节才会起作用。

字符串表所谓的字符串并不包含代码中出现的字符串,甚至变量名也不会出现在其中,不过函数名倒是可以在其中看到。

重定位表

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

readelf -r a.o

重定位节 '.rela.text' at offset 0x2a8 contains 3 entries:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000000029  000a00000002 R_X86_64_PC32     0000000000000000 shared - 4
000000000036  000c00000004 R_X86_64_PLT32    0000000000000000 swap - 4
00000000004f  000d00000004 R_X86_64_PLT32    0000000000000000 __stack_chk_fail - 4

重定位节 '.rela.eh_frame' at offset 0x2f0 contains 1 entry:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000000020  000200000002 R_X86_64_PC32     0000000000000000 .text + 0
readelf -x 2 a.o

“.rela.text”节的十六进制输出:
  0x00000000 29000000 00000000 02000000 0a000000 )...............
  0x00000010 fcffffff ffffffff 36000000 00000000 ........6.......
  0x00000020 04000000 0c000000 fcffffff ffffffff ................
  0x00000030 4f000000 00000000 04000000 0d000000 O...............
  0x00000040 fcffffff ffffffff                   ........

具体功能后面再说。

符号表
readelf -s ab

Symbol table '.dynsym' contains 8 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4 (2)
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (3)
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (3)
     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     6: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     7: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (3)

Symbol table '.symtab' contains 69 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000318     0 SECTION LOCAL  DEFAULT    1 
     2: 0000000000000338     0 SECTION LOCAL  DEFAULT    2 
     3: 0000000000000358     0 SECTION LOCAL  DEFAULT    3 
     4: 000000000000037c     0 SECTION LOCAL  DEFAULT    4 
     5: 00000000000003a0     0 SECTION LOCAL  DEFAULT    5 
     6: 00000000000003c8     0 SECTION LOCAL  DEFAULT    6 
     7: 0000000000000488     0 SECTION LOCAL  DEFAULT    7 
     8: 0000000000000528     0 SECTION LOCAL  DEFAULT    8 
     9: 0000000000000538     0 SECTION LOCAL  DEFAULT    9 
    10: 0000000000000568     0 SECTION LOCAL  DEFAULT   10 
    11: 0000000000000628     0 SECTION LOCAL  DEFAULT   11 
    12: 0000000000001000     0 SECTION LOCAL  DEFAULT   12 
    13: 0000000000001020     0 SECTION LOCAL  DEFAULT   13 
    14: 0000000000001050     0 SECTION LOCAL  DEFAULT   14 
    15: 0000000000001060     0 SECTION LOCAL  DEFAULT   15 
    16: 0000000000001080     0 SECTION LOCAL  DEFAULT   16 
    17: 0000000000001298     0 SECTION LOCAL  DEFAULT   17 
    18: 0000000000002000     0 SECTION LOCAL  DEFAULT   18 
    19: 0000000000002014     0 SECTION LOCAL  DEFAULT   19 
    20: 0000000000002060     0 SECTION LOCAL  DEFAULT   20 
    21: 0000000000003db0     0 SECTION LOCAL  DEFAULT   21 
    22: 0000000000003db8     0 SECTION LOCAL  DEFAULT   22 
    23: 0000000000003dc0     0 SECTION LOCAL  DEFAULT   23 
    24: 0000000000003fb0     0 SECTION LOCAL  DEFAULT   24 
    25: 0000000000004000     0 SECTION LOCAL  DEFAULT   25 
    26: 0000000000004014     0 SECTION LOCAL  DEFAULT   26 
    27: 0000000000000000     0 SECTION LOCAL  DEFAULT   27 
    28: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    29: 00000000000010b0     0 FUNC    LOCAL  DEFAULT   16 deregister_tm_clones
    30: 00000000000010e0     0 FUNC    LOCAL  DEFAULT   16 register_tm_clones
    31: 0000000000001120     0 FUNC    LOCAL  DEFAULT   16 __do_global_dtors_aux
    32: 0000000000004014     1 OBJECT  LOCAL  DEFAULT   26 completed.8060
    33: 0000000000003db8     0 OBJECT  LOCAL  DEFAULT   22 __do_global_dtors_aux_fin
    34: 0000000000001160     0 FUNC    LOCAL  DEFAULT   16 frame_dummy
    35: 0000000000003db0     0 OBJECT  LOCAL  DEFAULT   21 __frame_dummy_init_array_
    36: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS a.c
    37: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS b.c
    38: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    39: 0000000000002184     0 OBJECT  LOCAL  DEFAULT   20 __FRAME_END__
    40: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 
    41: 0000000000003db8     0 NOTYPE  LOCAL  DEFAULT   21 __init_array_end
    42: 0000000000003dc0     0 OBJECT  LOCAL  DEFAULT   23 _DYNAMIC
    43: 0000000000003db0     0 NOTYPE  LOCAL  DEFAULT   21 __init_array_start
    44: 0000000000002014     0 NOTYPE  LOCAL  DEFAULT   19 __GNU_EH_FRAME_HDR
    45: 0000000000003fb0     0 OBJECT  LOCAL  DEFAULT   24 _GLOBAL_OFFSET_TABLE_
    46: 0000000000001000     0 FUNC    LOCAL  DEFAULT   12 _init
    47: 0000000000001290     5 FUNC    GLOBAL DEFAULT   16 __libc_csu_fini
    48: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
    49: 0000000000004000     0 NOTYPE  WEAK   DEFAULT   25 data_start
    50: 0000000000004014     0 NOTYPE  GLOBAL DEFAULT   25 _edata
    51: 0000000000001298     0 FUNC    GLOBAL HIDDEN    17 _fini
    52: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@@GLIBC_2
    53: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@@GLIBC_2.2.5
    54: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
    55: 0000000000004000     0 NOTYPE  GLOBAL DEFAULT   25 __data_start
    56: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    57: 0000000000004008     0 OBJECT  GLOBAL HIDDEN    25 __dso_handle
    58: 0000000000002000     4 OBJECT  GLOBAL DEFAULT   18 _IO_stdin_used
    59: 0000000000001220   101 FUNC    GLOBAL DEFAULT   16 __libc_csu_init
    60: 0000000000004018     0 NOTYPE  GLOBAL DEFAULT   26 _end
    61: 0000000000001080    47 FUNC    GLOBAL DEFAULT   16 _start
    62: 0000000000004014     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
    63: 0000000000001169   102 FUNC    GLOBAL DEFAULT   16 main
    64: 0000000000004018     0 OBJECT  GLOBAL HIDDEN    25 __TMC_END__
    65: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
    66: 00000000000011cf    79 FUNC    GLOBAL DEFAULT   16 swap
    67: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@@GLIBC_2.2
    68: 0000000000004010     4 OBJECT  GLOBAL DEFAULT   25 shared

筛选出我们自己定义或者关心的符号:

    36: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS a.c
    66: 00000000000011cf    79 FUNC    GLOBAL DEFAULT   16 swap
    68: 0000000000004010     4 OBJECT  GLOBAL DEFAULT   25 shared

符号表的C实现也是结构体数组,结构体为Elf32_Sym(或Elf64_Sym):

/* 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;
小实验

做一个关于符号表和字符串表的小实验。首先选取符号:

    66: 00000000000011cf    79 FUNC    GLOBAL DEFAULT   16 swap

符号名是swap,根据文档,st_name其实是swap这个字符串在字符串表里的下标。我们看之前的字符串表,知道swap的下标(字节为单位)是

  [   205]  swap

直接查看.symtab对应位置的hex dump。

05020000(st_name) 12(st_info)00(st_other)1000(st_shndx:.text) cf110000 00000000(st_value) 4f000000 00000000(st_size)

可以看到uint32类型的st_name对应0x05020000,对其应用小端序也就是0x205,与字符串表下标一致。

最后验证一下st_value(在可执行文件中,这个表示虚拟地址):

objdump -D ab
...
0000000011cf <swap>:
    11cf:	f3 0f 1e fa          	endbr64 
    11d3:	55                   	push   %rbp
    11d4:	48 89 e5             	mov    %rsp,%rbp
    11d7:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
    11db:	48 89 75 f0          	mov    %rsi,-0x10(%rbp)
...

静态链接

完成两项任务:

  • 空间与地址分配

    虚拟地址空间是针对可执行文件来说的,目标文件更多使用文件偏移或段偏移来定位。所以这一步其实是将几个目标文件的内容先按照一定的规则(相似段合并)编排,再塞进一个虚拟地址空间,这样每个符号都有了属于自己的虚拟地址。

  • 符号解析与重定位

    前一步已经形成了一个全局的符号表,那么每个目标文件中的符号与全局符号表是怎样的对应关系就需要在这里搞清楚。并且因为每个符号都拥有了新的地址,所以之前所有的绝对地址引用都要被修改,也就是重定位。

空间与地址分配

示例代码:

// a.c
#include <stdio.h>

extern int shared;
extern int global_uinit;

int main()
{
    int a = 100;
    static int static_init = 20;
    static int static_uinit;
    
    global_uinit = 20;

    printf("hello, World.");
    swap(&a, &shared);
}
// b.c
int shared = 1;
int global_uinit;

void swap(int *a, int *b)
{
    *a ^= *b ^= *a ^= *b;
}

下面对a.o、b.o、ab三个ELF文件进行考察,考察的对象是几个主要的符号shared、global_uinit、static_init、static_uinit、swap

readelf -s a.o

Symbol table '.symtab' contains 19 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
	...
	// 下面两个都对外部文件不可见,在链接时几乎不用考虑
	// Ndx = 4 对应了.bss,因为.bss无实际空间,所以偏移value为0
	// Ndx = 3 对应了.data,它的十六进制输出:0x00000000 14000000,对应value为0
     6: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    4 static_uinit.2319
     7: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    3 static_init.2318
	...
    12: 0000000000000000   112 FUNC    GLOBAL DEFAULT    1 main
    // Ndx = UND表示引用外部定义符号,需要在链接时进行重定位
    13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND global_uinit
    ...
    16: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND shared
    17: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND swap
    ...
readelf -s b.o

Symbol table '.symtab' contains 12 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     ...
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    2 shared
     // Ndx = COM表示未初始化的全局符号
    10: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM global_uinit
    11: 0000000000000000    79 FUNC    GLOBAL DEFAULT    1 swap

注:

COMMON--------------未初始化的全局变量
.bss ---------------------未初始化的静态变量,以及初始化为0的全局变量或静态变量

查看a.o对这几处符号是如何引用的。

objdump -D a.o

...
// global_uinit和shared的位置上就是0
22:	c7 05 00 00 00 00 1e 	movl   $0x1e,0x0(%rip)        # 2c <main+0x2c>
...
41:	48 8d 35 00 00 00 00 	lea    0x0(%rip),%rsi        # 48 <main+0x48>
...
// swap的地址也是0,但是根据观察,目标文件的代码部分对所有函数(包括文件内函数)地址的引用好像都是0
ab:	e8 00 00 00 00       	callq  b0 <main+0x78>

查看ab的符号表。

readelf -s ab

...
    // Ndx = 26 对应了.bss,static_uinit和global_uinit都被归入.bss
    // Ndx = 25 对应了.data
    37: 000000000000401c     4 OBJECT  LOCAL  DEFAULT   26 static_uinit.2319
    38: 0000000000004010     4 OBJECT  LOCAL  DEFAULT   25 static_init.2318
    ...
    // Ndx = 16 对应了.text
    65: 0000000000001169   112 FUNC    GLOBAL DEFAULT   16 main
    ...
    68: 00000000000011d9    79 FUNC    GLOBAL DEFAULT   16 swap
    69: 0000000000004020     4 OBJECT  GLOBAL DEFAULT   26 global_uinit
    ...
    71: 0000000000004014     4 OBJECT  GLOBAL DEFAULT   25 shared

每个符号都有了虚拟地址,说明完成了空间与地址分配。

符号解析与重定位

参与符号解析与重定位的主要有swap、global_uinit、shared,它们三者在a.o中引用时都是全0,查看一下它们在ab中的引用。

objdump -D ab

...
  118b:	c7 05 8b 2e 00 00 1e 	movl   $0x1e,0x2e8b(%rip)        # 4020 <global_uinit>
  ...
  11aa:	48 8d 35 63 2e 00 00 	lea    0x2e63(%rip),%rsi        # 4014 <shared>
  ...
  11b9:	e8 1b 00 00 00       	callq  11d9 <swap>
...

这些符号都被修正到了正确的地址上,那么问题是链接过程中如何完成寻址?

从代码来看,重定位基本上就是修正a.o中的外部符号引用,查看a.o的代码段重定位表。

在此之前,看一下重定位表的C实现:

/* 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 ,符号的第一个字节相对段的偏移,uint64_t*/
  Elf64_Xword	r_info;			/* Relocation type and symbol index,uint64_t  */
} 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,这两者的差别主要在于多了个成员变量r_addend,下面简单说明一下两者的区别。

As specified previously, only Elf32_Rela and Elf64_Rela entries contain an explicit addend. Entries of type Elf32_Rel and Elf64_Rel store an implicit addend in the location to be modified. Depending on the processor architecture, one form or the other might be necessary or more convenient. Consequently, an implementation for a particular machine may use one form exclusively or either form depending on context.

也就是说,r_addend其实就是Rel形式中被修改位置保存的值,它们的原理一致。

objdump -r a.o

a.o:     文件格式 elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
// VALUE
0000000000000024 R_X86_64_PC32     global_uinit-0x0000000000000008
...
0000000000000044 R_X86_64_PC32     shared-0x0000000000000004
0000000000000051 R_X86_64_PLT32    swap-0x0000000000000004
...

重定位表为我们提供的线索总结来说就是:

  1. 待重定位的外部符号是什么?r_info高32位指出它在符号表的下标。
  2. 待重定位的外部符号在哪里?r_offset指出。
  3. 重定位方法是什么?r_info低32位指出。
  4. 重定位计算中需要用到的VALUE是多少?r_addend指出。

那么实际重定位的时候肯定已经完成了虚拟地址分配,每个符号有了自己的虚拟地址,也就可以开始重定位的计算了。

  • R_386_32:值1,绝对寻址修正S+A

    R_X86_64_32

  • R_386_PC32:值2,相对寻址修正S+A-P

    R_X86_64_PC32

    R_X86_64_PLT32

A:被修正位置的值。在Rela形式中,就是r_addend的值。对应objdump -r命令查出的VALUE值。

P:被修正的位置。可以通过相对于段开始的偏移量,也就是r_offset加上该段第一个函数在可执行文件中的虚拟地址计算出来。(在实验中,a.o的代码段起始就是main函数,所以r_offset就是相对于main来说的,但是链接之后的代码段起始就不一定是main函数了,那么直接用.text的虚拟地址加r_offset就无法定位到shared)

S:符号的实际虚拟地址。r_info高32位指出它在目标文件符号表的下标,链接时可以换算到可执行文件符号表的下标,进而可以得到VMA。

根据上文的重定位类型,知道是相对寻址修正,所以分别计算A,P,S。

以shared为例:A = -4h,P = 44h + 1169h = 11ADh,S = 4014h。

S + A - P = 2E63h,所以原本shared的位置上应该由0变成2E63h。

objdump -d ab

...
    11aa:	48 8d 35 63 2e 00 00 	lea    0x2e63(%rip),%rsi        # 4014 <shared>
...

其他

之前我认为需要重定位的都是外部符号,后来实验中发现一些本地函数以及本地定义的全局变量、局部静态变量同样需要重定位,它们在目标文件中的引用也是0,只有在可执行文件中才修改为相对寻址,我认为这样做的原因是目标文件中如果就使用了相对寻址,那么在链接的时候,如果代码段数据段等等被重排,之前的相对寻址就可能失效。

强弱符号

在C语言中,编译器默认函数和初始化了的全局变量为强符号(Strong Symbol),未初始化的全局变量为弱符号(Weak Symbol)。强符号之所以强,是因为它们拥有确切的数据,变量有值,函数有函数体;弱符号之所以弱,是因为它们还未被初始化,没有确切的数据。

链接器会按照如下的规则处理被多次定义的强符号和弱符号:

  1. 不允许强符号被多次定义,也即不同的目标文件中不能有同名的强符号;如果有多个强符号,那么链接器会报符号重复定义错误。
  2. 如果一个符号在某个目标文件中是强符号,在其他文件中是弱符号,那么选择强符号。
  3. 如果一个符号在所有的目标文件中都是弱符号,那么选择其中占用空间最大的一个。

现在我们再回头总结性地思考关于未初始化的全局变量的问题:在目标文件中,编译器为什么不直接把未初始化的全局变量也当作未初始化的局部静态变量样处理,为它在 BSS段分配空间,而是将其标记为一个COMMON类型的变量?

通过了解链接器处理多个弱符号的过程,我们可以想到,当编译器将一个编译单元编译成目标文件的时候,如果该编译单元包含了弱符号(未初始化的全局变量就是典型的弱符号),那么该弱符号最终所占空间的大小在此时是未知的,因为有可能其他编译单元中该符号所占的空间比本编译单元该符号所占的空间要大。所以编译器此时无法为该弱符号在.BSS段分配空间,因为所须要空间的大小未知。但是链接器在链接过程中可以确定弱符号的大小,因为当链接器读取所有输入目标文件以后,任何一个弱符号的最终大小都可以确定了,所以它可以在最终输出文件的 BSS 段为其分配空间。所以总体来看,未初始化全局变量最终还是被放在BSS 段的。

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值