(二)链接、装载与库 | 静态链接:目标文件


1. 目标文件格式

从结构上来讲,目标文件是经过编译后的可执行文件格式,只是还没有经过链接,其中有些符号或地址还没有被调整。当前,主流 PC 平台的可执行文件格式包括 Windows 下的 PE 和 Linux 下的 ELF,它们都是 COFF 格式变种。

ELF 文件标准把采用 ELF 格式的文件归为可重定位文件、可执行文件、共享目标文件和核心转储文件四类。在 Linux 下,可使用 file 命令查看相应的文件格式。可重定位文件:

$ file hello.o 
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

可执行文件:

$ file TinyHelloWorld
TinyHelloWorld: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped

共享目标文件:

$ file hello
hello: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, 
for GNU/Linux 3.2.0, BuildID[sha1]=847ece746b56cba30c2cca74ef5fb73245b351c5, not stripped

核心转储文件:

$ file core
core: ELF 64-bit LSB core file x86-64, version 1 (SYSV), SVR4-style, from './core_test', real uid: 0, 
effective uid: 0, real gid: 0, effective gid: 0, execfn: './core_test', platform: 'x86_64'

2. 目标文件内容

目标文件按照数据的属性,以段的形式存储各部分内容。源代码编译后的机器指令通常存放在代码段,命名为 .code 或 .text;全局变量和局部静态变量通常存放在数据段,命名为 .data。以下面程序为例,介绍各个段的内容:

/* SimpleSection.c */

int printf(const char* format, ...);
int global_init_var = 84;
int global_uninit_var;

void func1(int i)
{
    printf("%d\n", i);
}

int main(void)
{
    static int static_var = 85;
    static int static_var2;
    int a = 1;
    int b;
    func1(static_var + static_var2 + a + b);
    return a;
}

使用 gcc 编译上述代码,得到一个大小为 1936 字节的目标文件 SimpleSection.o。使用 objdump 工具查看该目标文件的内容:

$ objdump -h SimpleSection.o

SimpleSection.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000057  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  0000000000000000  0000000000000000  00000098  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000004  0000000000000000  0000000000000000  000000a0  2**2
                  ALLOC
  3 .rodata       00000004  0000000000000000  0000000000000000  000000a0  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      0000002a  0000000000000000  0000000000000000  000000a4  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000ce  2**0
                  CONTENTS, READONLY
  6 .eh_frame     00000058  0000000000000000  0000000000000000  000000d0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

由结果可知,该目标文件共有 6 个段。各项表示段的特性,Size 表示段大小,File off 项表示段偏移等。每个段的第二行显示了该段的属性,如 CONTENTS 表示段存在于目标文件中,READONLY 表示只读段等。

.bss 段不含 CONTENTS 表示该段不存在于目标文件。.note.GNU-stack 包含 CONTENTS 但是其 Size 为零,把它当作不存在于目标文件。根据上述内容,可以绘出各段在目标文件中的位置:

请添加图片描述

2.1 代码段

objdump 的 -s 选项将所有段的内容以十六进制的方式打印出来,-d 选项将所有包含指令的段反汇编。

objdump -s -d SimpleSection.o

将代码段 .text 部分提取出来:

Contents of section .text:
 0000 554889e5 4883ec10 897dfc8b 45fc89c6  UH..H....}..E...
 0010 488d3d00 000000b8 00000000 e8000000  H.=.............
 0020 0090c9c3 554889e5 4883ec10 c745f801  ....UH..H....E..
 0030 0000008b 15000000 008b0500 00000001  ................
 0040 c28b45f8 01c28b45 fc01d089 c7e80000  ..E....E........
 0050 00008b45 f8c9c3                      ...E...  

该十六进制对应于源代码中的 func1 部分和 main 部分:

0000000000000000 <func1>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 83 ec 10             sub    $0x10,%rsp
   8:   89 7d fc                mov    %edi,-0x4(%rbp)
   b:   8b 45 fc                mov    -0x4(%rbp),%eax
   e:   89 c6                   mov    %eax,%esi
  10:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # 17 <func1+0x17>
  17:   b8 00 00 00 00          mov    $0x0,%eax
  1c:   e8 00 00 00 00          callq  21 <func1+0x21>
  21:   90                      nop
  22:   c9                      leaveq 
  23:   c3                      retq   

0000000000000024 <main>:
  24:   55                      push   %rbp
  25:   48 89 e5                mov    %rsp,%rbp
  28:   48 83 ec 10             sub    $0x10,%rsp
  2c:   c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp)
  33:   8b 15 00 00 00 00       mov    0x0(%rip),%edx        # 39 <main+0x15>
  39:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 3f <main+0x1b>
  3f:   01 c2                   add    %eax,%edx
  41:   8b 45 f8                mov    -0x8(%rbp),%eax
  44:   01 c2                   add    %eax,%edx
  46:   8b 45 fc                mov    -0x4(%rbp),%eax
  49:   01 d0                   add    %edx,%eax
  4b:   89 c7                   mov    %eax,%edi
  4d:   e8 00 00 00 00          callq  52 <main+0x2e>
  52:   8b 45 f8                mov    -0x8(%rbp),%eax
  55:   c9                      leaveq 
  56:   c3                      retq  

左边标号为各指令的偏移地址,代码段大小为 0x56;中间部分为机器码;右边部分为汇编代码。

2.2 数据段

.data 段保存已经初始化了的全局静态变量和局部静态变量,即源代码中的 global_init_var 和 static_var。这两个变量均为整型,共 8 个字节,所以 .data 段大小为 8 字节。

调用 printf 中用到的 %d\n 是一种只读数据,它被放到 .rodata 段,大小为 4 字节。查看数据段的内容:

objdump -x -s -d SimpleSection.o

将数据段 .data 和 .rodata 部分提取出来:

Contents of section .data:
 0000 54000000 55000000                    T...U...        
Contents of section .rodata:
 0000 25640a00                             %d..    

54000000,0x00000054 对应于十进制的 84,刚好是 global_init_var 的值;55000000,0x00000055 对应于十进制的 85,刚好是 static_var 的值;25640a00,0x25 和 0x64为 %d ASCII 码,0x000a 为换行键的 ASCII 码。

2.3 BSS 段

.bss 段存放未初始化的全局变量和局部静态变量,如源代码中的 global_uninit_var 和 static_var2。但是该段大小仅有 4 字节,实际上只存放了 static_var2,而 global_uninit_var 是一个未定义的 COMMON 符号,没有被放在任何段。不同编译器对 .bss 存放内容的规定不同。

2.4 其他段

.comment 段存放编译器的版本信息,如字符串:GCC: (GNU) 7.5.0;.eh_frame 段用于程序调试等。


3. ELF 文件结构

3.1 文件头

ELF 目标文件的最前部是 ELF 文件头,其中包含了 ELF 文件版本、目标机器型号、程序入口地址等。紧接着是 ELF 文件的各个段。ELF 文件使用段表描述各个段的信息,如段名、段长度、在文件中的偏移、读写权限等。使用 readelf 命令查看 ELF 文件的文件头:

readelf -h SimpleSection.o
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

由结果可知,ELF 文件的文件头包含了 ELF 魔数(Magic 对应的 16 字节内容)、文件机器的字节长度(ELF 64)、数据存储方式(小端序)、版本、运行平台(UNIX)、ABI 版本(0)、文件类型(REL)等。

3.2 段表

ELF 文件的段表用于保存各段的基本属性。使用 readelf 命令查看 ELF 文件的段表:

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
  [ 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
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)

各项的含义如下:

typedef struct
{
  Elf64_Word	sh_name;		// 段名
  Elf64_Word	sh_type;		// 段类型
  Elf64_Xword	sh_flags;		// 段标志
  Elf64_Addr	sh_addr;		// 运行时段的虚拟地址
  Elf64_Off	sh_offset;		    // 段偏移
  Elf64_Xword	sh_size;		// 段大小(字节)
  Elf64_Word	sh_link;		// 链接到其他段
  Elf64_Word	sh_info;		// 段的其他信息
  Elf64_Xword	sh_addralign;	// 段对齐
  Elf64_Xword	sh_entsize;		/* Entry size if section holds table */
} Elf64_Shdr;

3.3 重定位表

ELF 文件的 .rela.text 段类型为 RELA,它是一个重定位表。链接器在处理目标文件时,须对目标文件中某些部位进行重定位,这些重定位的信息都记录在 ELF 文件的重定位表里。

3.4 字符串表

ELF 文件中用到了很多字符串,比如段名、变量名等。因为字符串的长度不固定,用固定结构存放较困难。常见做法是把字符串集中存放到一个表,然后使用偏移来表示字符串在表中的位置。比如:
请添加图片描述

使用 0、1、6 和 12 来引用空字符串、helloword、world 和 Myvariable。


4. 链接的接口——符号

在链接中,将函数和变量统称为符号。在 Linux 下使用 nm 查看符号:

nm SimpleSection.o
                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 T func1
0000000000000000 D global_init_var
0000000000000004 C global_uninit_var
0000000000000024 T main
                 U printf
0000000000000004 d static_var.1802
0000000000000000 b static_var2.1803

4.1 ELF 符号表结构

ELF 文件的 .symtab 段表示符号表,它是一个 Elf64_Sym 结构,定义如下:

typedef struct
{
  Elf64_Word	st_name;		// 符号名
  unsigned char	st_info;		// 符号类型和绑定信息
  unsigned char st_other;		// 0
  Elf64_Section	st_shndx;		// 符号所在段
  Elf64_Addr	st_value;		// 符号值
  Elf64_Xword	st_size;		// 符号大小
} Elf64_Sym;

4.2 符号修饰与函数签名

为了防止符号名冲突,C 语言规定全局变量和函数经过编译后,相应的符号名前加上下划线。但对于大型程序来说,这种方法不能从根本上解决问题。C++ 增加了命名空间的方法来解决多模块的符号冲突问题。

C++ 的符号修饰方法如下:所有的符号以 _Z 开头;对于嵌套(命名空间或类)的名字,后面紧跟 N,然后是类或命名空间的名字,每个名字前是字符串长度;再以 E 结尾。如 N::C::func 经过修饰后得到 _ZN1N1C4funcE。对于函数来说,它的参数列表紧跟在 E 后,如果是整型则为 i 等。使用 c++filt 工具可以用来解析被修饰过的名称:

$ c++filt _ZN1N1C4funcEi
N::C::func(int)

4.3 extern “C”

C++ 为了兼容 C,在符号的管理上,使用关键字 extern “C” 来声明或定义 C 的符号。C++ 编译器会将 extern “C” 大括号内部的代码当作 C 语言来处理。

extern "C"
{
	int func(int);
	int var;
}

4.4 弱符号和强符号

在符号的定义中,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。针对强弱符号的概念,链接器按如下规则选择多次被定义的全局符号:

  • 不允许强符号被多次定义,如果有多个强符号定义,则链接器报符号重复定义的错误。
  • 如果一个符号在某个目标文件中是强符号,则在其他文件中都是弱符号,且选择强符号。
  • 如果一个符号在所有目标文件中都是弱符号,那么选择内存占用较大的符号。

5. 总结

  • 目标文件 ELF 是 Linux 下的常见文件类型,主要包括可重定位文件、可执行文件、共享目标文件和核心转储文件四类。
  • 目标文件的内容以段的形式组织,各段内数据属性相似。
  • ELF 文件主要包括文件头、段表、重定位表和字符串表等。
  • 在链接过程中,符号用于耦合各目标文件,链接器根据符号类型选择合适的强符号或弱符号。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值