我是靠谱客的博主 奋斗手套,最近开发中收集的这篇文章主要介绍《程序员的自我修养——链接、装在与库》第三章《目标文件里有什么》读书笔记,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

一. 目标文件的概念

  1. 编译器编译源代码后生成的文件叫做目标文件。
  2. 从结构上讲,它是编译后的可执行文件格式,只是还没有经过链接过程。

二. 目标文件的格式

  1. PC平台上主流的可执行文件格式(Executable)主要有

    • Windows下的PE(Portable Executable)
    • Linux的ELF(Executable)

    它们都是COFF(Common file format)格式的变种。

  2. ELF分类

    ELF文件类型说明实例
    可重定位文件(Relocateable File)包含代码和数据,可以链接成可执行文件或共享目标文件Linux的 .o .a,Windows的.obj .lib
    可执行文件(Executeable File)包含可直接执行的程序,它的代表就是ELF可执行文件Linux的/bin/bash文件,Windows的.exe
    共享目标文件(Shared Object File)包含代码和数据。链接器可以使用它跟其他可重定位文件或共享目标文件链接,产生新的目标文件。 动态链接器可以将几个共享目标文件与可执行文件结合,作为进程映像的一部分来运行Linux的.so,Windows的.dll
    核心转存储文件(Core Dump File)当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转存储到核心转存储文件Linux下的core dump

三. 目标文件是什么样的

内容
File Header描述整个文件的属性,包括文件是否可执行,是静态链接还是动态链接以及入口地址(如果是位置相关的可执行文件),目标硬件,目标操作系统等信息,文件头还包括一个段表(Section Table)
.text(or .code) section代码段
.data section已初始化的全局变量和静态变量
.bss section未初始化的全局变量和静态变量

目标文件可分为程序指令和程序数据,这样做的好处有以下几个方面:

  1. 程序被装载后,数据和指令分别被映射到两个虚拟区域,数据区可读写,指令区只读,这样可以防止程序指令被改写。
  2. 现代CPU的缓存一般都被设计成数据缓存和指令缓存分离,所以程序的指令和数据分离对提高CPU的缓存命中率有好处。
  3. 副本进程之间可以共享程序指令。

四. 通过一个.o来了解目标文件的段

实验环境为:
GNU ld version 2.23.52.0.1-55.el7 20130226
gcc version 4.8.5 20150623 (Red Hat 4.8.5-4) (GCC)
CentOS Linux release 7.2.1511 (Core)

源代码如下:

int g_init_var = 84;
int g_unint_var;
void func1(int i) {
printf("%dn", i);
}
int main(void) {
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1(static_var2 + static_var + a + b);
return a;
}

执行

$ gcc -c SimpleSection.c 

编译结果为 SimpleSection.o。执行

$ objdump -h SimpleSection.o

查看段基本信息。

SimpleSection.o: 
file format elf64-x86-64
Sections:
Idx Name
Size
VMA
LMA
File off
Algn
0 .text
00000054
0000000000000000
0000000000000000
00000040
2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data
00000008
0000000000000000
0000000000000000
00000094
2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss
00000004
0000000000000000
0000000000000000
0000009c
2**2
ALLOC
3 .rodata
00000004
0000000000000000
0000000000000000
0000009c
2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment
0000002d
0000000000000000
0000000000000000
000000a0
2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000
0000000000000000
0000000000000000
000000cd
2**0
CONTENTS, READONLY
6 .eh_frame
00000058
0000000000000000
0000000000000000
000000d0
2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

每个段信息的第二行中的CONTENTS、ALLOC等表示了该段的属性,CONTENTS表示该段在ELF文件中存在。

4.1. 代码段

执行

$ objdump -s -d SimpleSection.o

看所有段信息,以及反汇编结果。

SimpleSection.o: 
file format elf64-x86-64
Sections:
Idx Name
Size
VMA
LMA
File off
Algn
0 .text
00000054
0000000000000000
0000000000000000
00000040
2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data
00000008
0000000000000000
0000000000000000
00000094
2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss
00000004
0000000000000000
0000000000000000
0000009c
2**2
ALLOC
3 .rodata
00000004
0000000000000000
0000000000000000
0000009c
2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment
0000002d
0000000000000000
0000000000000000
000000a0
2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000
0000000000000000
0000000000000000
000000cd
2**0
CONTENTS, READONLY
6 .eh_frame
00000058
0000000000000000
0000000000000000
000000d0
2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
[tiger@app-20-39 ~]$ objdump -s -d SimpleSection.o
SimpleSection.o:
file format elf64-x86-64
Contents of section .text:
0000 554889e5 4883ec10 897dfc8b 45fc89c6
UH..H....}..E...
0010 bf000000 00b80000 0000e800 000000c9
................
0020 c3554889 e54883ec 10c745fc 01000000
.UH..H....E.....
0030 8b150000 00008b05 00000000 01c28b45
...............E
0040 fc01c28b 45f801d0 89c7e800 0000008b
....E...........
0050 45fcc9c3
E...
Contents of section .data:
0000 54000000 55000000
T...U...
Contents of section .rodata:
0000 25640a00
%d..
Contents of section .comment:
0000 00474343 3a202847 4e552920 342e382e
.GCC: (GNU) 4.8.
0010 35203230 31353036 32332028 52656420
5 20150623 (Red
0020 48617420 342e382e 352d3429 00
Hat 4.8.5-4).
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001
.........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000
................
0020 00000000 21000000 00410e10 8602430d
....!....A....C.
0030 065c0c07 08000000 1c000000 3c000000
...........<...
0040 00000000 33000000 00410e10 8602430d
....3....A....C.
0050 066e0c07 08000000
.n......
Disassembly of section .text:
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:
bf 00 00 00 00
mov
$0x0,%edi
15:
b8 00 00 00 00
mov
$0x0,%eax
1a:
e8 00 00 00 00
callq
1f <func1+0x1f>
1f:
c9
leaveq
20:
c3
retq
0000000000000021 <main>:
21:
55
push
%rbp
22:
48 89 e5
mov
%rsp,%rbp
25:
48 83 ec 10
sub
$0x10,%rsp
29:
c7 45 fc 01 00 00 00
movl
$0x1,-0x4(%rbp)
30:
8b 15 00 00 00 00
mov
0x0(%rip),%edx
# 36 <main+0x15>
36:
8b 05 00 00 00 00
mov
0x0(%rip),%eax
# 3c <main+0x1b>
3c:
01 c2
add
%eax,%edx
3e:
8b 45 fc
mov
-0x4(%rbp),%eax
41:
01 c2
add
%eax,%edx
43:
8b 45 f8
mov
-0x8(%rbp),%eax
46:
01 d0
add
%edx,%eax
48:
89 c7
mov
%eax,%edi
4a:
e8 00 00 00 00
callq
4f <main+0x2e>
4f:
8b 45 fc
mov
-0x4(%rbp),%eax
52:
c9
leaveq
53:
c3
retq
  • -s 将所有段的内容以十六进制的方式打印出来
  • -d 将所有指令反汇编
  • 打印结果的最左边一列是偏移量,中间四列是十六进制内容,最右边一列是段内容的ASCII码

4.2 数据段和只读数据段

  1. 单独设立.rodata段的好处:
    • 语义上支持了C/C++的const关键字
    • 操作系统在加载的时候可以将.rodata段映射成只读,对于这个段任何修改操作都会作为非法操作处理,保证了程序的安全性
  2. 有些编译器会把常量字符串放到.data段

4.3 BSS段

有些编译器会将未初始化的变量存放在目标文件的.bss段,有些则不存放,只是预留一个未定义的全局变符号

4.4 其他段

常用段名说明
.rodata1Read Only Data,跟.rodata一样
.comment存放编译器版本信息,比如字符串: GCC:(GNU)4.9
.debug调试信息
.dynamic动态链接信息
.hash符号hash表
.line调试时的行号表,即源代码行号与编译后指令对应表
.note额外的编译器信息
.strtabString Table 字符串表,用于存储ELF文件中用到的各种字符串
.symtabSymbol Table 符号表
.shstrtabSection String Table 段名表
.plt .got动态链接的跳转表和全局入口表
.init .fini程序初始化与终结代码段

这些段名都是以 . 作为前缀,表示这些名字是系统保留的,应用程序也可以使用一些非系统保留的名字作为段名。比如可以在ELF文件中插入一个 music 段,里面存放一些MP3音乐(可以用gobjcopy实现),但是应用程序自定义的段名不能使用 . 作为前缀,否则容易跟系统保留段名冲突。

五. ELF文件结构描述

ELF总体结构如下:

ELF Header
.text
.data
.bss
other secitons
Section header table
String Tables
Symbol Tables

5.1 文件头

执行

$ 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:
400 (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: 10

ELF文件头的数据结构在 /usr/include/elf.h 中的 Elf64_Ehdr 定义。

typedef uint16_t Elf64_Half;
typedef uint32_t Elf64_Word;
typedef int32_t
Elf64_Sword;
typedef uint64_t Elf64_Xword;
typedef int64_t
Elf64_Sxword;
typedef uint64_t Elf64_Addr;
typedef uint64_t Elf64_Off;
typedef uint16_t Elf64_Section;
typedef Elf64_Half Elf64_Versym;
#define EI_NIDENT (16)
typedef struct
{
/**
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
*/
unsigned char e_ident[EI_NIDENT];
/* Magic number and other info */
/*Type:
REL (Relocatable file)*/
Elf64_Half
e_type;
/* Object file type */
/*Machine:
Advanced Micro Devices X86-64*/
Elf64_Half
e_machine;
/* Architecture */
/*Version:
0x1*/
Elf64_Word
e_version;
/* Object file version */
/*Entry point address:
0x0*/
Elf64_Addr
e_entry;
/* Entry point virtual address */
/*Start of program headers:
0 (bytes into file)*/
Elf64_Off
e_phoff;
/* Program header table file offset */
/*Start of section headers:
400 (bytes into file)*/
Elf64_Off
e_shoff;
/* Section header table file offset */
/*Flags:
0x0*/
Elf64_Word
e_flags;
/* Processor-specific flags */
/*Size of this header:
64 (bytes)*/
Elf64_Half
e_ehsize;
/* ELF header size in bytes */
/*Size of program headers:
0 (bytes)*/
Elf64_Half
e_phentsize;
/* Program header table entry size */
/*Number of program headers:
0*/
Elf64_Half
e_phnum;
/* Program header table entry count */
/*Size of section headers:
64 (bytes*/
Elf64_Half
e_shentsize;
/* Section header table entry size */
/*Number of section headers:
13*/
Elf64_Half
e_shnum;
/* Section header table entry count */
/*Section header string table index: 10*/
Elf64_Half
e_shstrndx;
/* Section header string table index */
} Elf64_Ehdr;
  1. 魔数(Magic number)
    7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
    • 这16个字节对应 Elf64_Ehdr 的 e_ident。
    • 最开始4个字节是所有ELF都必须相同的标识码,7f对应ASCII字符里的DEL控制符,45 4c 46对于ASCII字符是ELF。
    • 第五个字节 01 表示是32位的,如果是 02 则表示是64位的。
    • 第六个字节 01 是字节序,00 表示无效格式,01 表示小端,02表示大端。
    • 第七个字节 01 是ELF文件的主版本号,一般是 01,因为ELF标准自1.2版本以后再也没有更新过。
    • 后面九个字节ELF标准没有定义,一般填0。
  2. 文件类型(Type)
    e_type表示ELF文件类型,系统通过这个变量来判断ELF文件的类型,而不是扩展名。相关常量类型定义如下:

    /* Legal values for e_type (object file type).
    */
    #define ET_NONE
    0
    /* No file type */
    #define ET_REL
    1
    /* Relocatable file */
    #define ET_EXEC
    2
    /* Executable file */
    #define ET_DYN
    3
    /* Shared object file */
    #define ET_CORE
    4
    /* Core file */
    
  3. 机器类型(Machine)

    /* Legal values for e_machine (architecture).
    */
    #define EM_NONE
    0
    /* No machine */
    #define EM_M32
    1
    /* AT&T WE 32100 */
    #define EM_SPARC
    2
    /* SUN SPARC */
    #define EM_386
    3
    /* Intel 80386 */
    #define EM_68K
    4
    /* Motorola m68k family */
    #define EM_88K
    5
    /* Motorola m88k family */
    #define EM_860
    7
    /* Intel 80860 */
    #define EM_MIPS
    8
    /* MIPS R3000 big-endian */
    #define EM_S370
    9
    /* IBM System/370 */
    #define EM_MIPS_RS3_LE
    10
    /* MIPS R3000 little-endian */
    /*此省略若干个CPU*/
    

5.2 段表

段表描述了ELF的各个段的信息,比如段名、段的长度、段的偏移、段的读写权限以及其他属性。也就是说,ELF文件的段结构就是由段表决定的。执行

$ readelf -S SimpleSection.o

查看段表信息。

There are 13 section headers, starting at offset 0x190:
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 0000000000000054
0000000000000000
AX
0
0
4
[ 2] .rela.text
RELA
0000000000000000
000006b0 0000000000000078
0000000000000018
11
1
8
[ 3] .data
PROGBITS
0000000000000000
00000094 0000000000000008
0000000000000000
WA
0
0
4
[ 4] .bss
NOBITS
0000000000000000
0000009c 0000000000000004
0000000000000000
WA
0
0
4
[ 5] .rodata
PROGBITS
0000000000000000
0000009c 0000000000000004
0000000000000000
A
0
0
1
[ 6] .comment
PROGBITS
0000000000000000
000000a0 000000000000002d
0000000000000001
MS
0
0
1
[ 7] .note.GNU-stack
PROGBITS
0000000000000000
000000cd 0000000000000000
0000000000000000
0
0
1
[ 8] .eh_frame
PROGBITS
0000000000000000
000000d0 0000000000000058
0000000000000000
A
0
0
8
[ 9] .rela.eh_frame
RELA
0000000000000000
00000728 0000000000000030
0000000000000018
11
8
8
[10] .shstrtab
STRTAB
0000000000000000
00000128 0000000000000061
0000000000000000
0
0
1
[11] .symtab
SYMTAB
0000000000000000
000004d0 0000000000000180
0000000000000018
12
11
8
[12] .strtab
STRTAB
0000000000000000
00000650 000000000000005b
0000000000000000
0
0
1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

ELF段表数据结构在 elf.h 中的 Elf64_Shdr 定义。Elf64_Shdr又被称为段描述符(Section Descriptor)。

/* Section header.
*/
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;
  1. 段名(sh_name)
    段名 对于编译器、链接器是有意义的,对操作系统并没有实际意义。操作系统通过sh_type和sh_flags来决定如何处理该段。

  2. 段类型(sh_type)

    /* Legal values for sh_type (section type).
    */
    #define SHT_NULL
    0
    /* Section header table entry unused */
    #define SHT_PROGBITS
    1
    /* Program data */
    #define SHT_SYMTAB
    2
    /* Symbol table */
    #define SHT_STRTAB
    3
    /* String table */
    #define SHT_RELA
    4
    /* Relocation entries with addends */
    #define SHT_HASH
    5
    /* Symbol hash table */
    #define SHT_DYNAMIC
    6
    /* Dynamic linking information */
    #define SHT_NOTE
    7
    /* Notes */
    #define SHT_NOBITS
    8
    /* Program space with no data (bss) */
    #define SHT_REL
    9
    /* Relocation entries, no addends */
    #define SHT_SHLIB
    10
    /* Reserved */
    #define SHT_DYNSYM
    11
    /* Dynamic linker symbol table */
    #define SHT_INIT_ARRAY
    14
    /* Array of constructors */
    #define SHT_FINI_ARRAY
    15
    /* Array of destructors */
    #define SHT_PREINIT_ARRAY 16
    /* Array of pre-constructors */
    #define SHT_GROUP
    17
    /* Section group */
    #define SHT_SYMTAB_SHNDX
    18
    /* Extended section indeces */
    #define SHT_NUM
    19
    /* Number of defined types.
    */
    #define SHT_LOOS
    0x60000000
    /* Start OS-specific.
    */
    #define SHT_GNU_ATTRIBUTES 0x6ffffff5
    /* Object attributes.
    */
    #define SHT_GNU_HASH
    0x6ffffff6
    /* GNU-style hash table.
    */
    #define SHT_GNU_LIBLIST
    0x6ffffff7
    /* Prelink library list */
    #define SHT_CHECKSUM
    0x6ffffff8
    /* Checksum for DSO content.
    */
    #define SHT_LOSUNW
    0x6ffffffa
    /* Sun-specific low bound.
    */
    #define SHT_SUNW_move
    0x6ffffffa
    #define SHT_SUNW_COMDAT
    0x6ffffffb
    #define SHT_SUNW_syminfo
    0x6ffffffc
    #define SHT_GNU_verdef
    0x6ffffffd
    /* Version definition section.
    */
    #define SHT_GNU_verneed
    0x6ffffffe
    /* Version needs section.
    */
    #define SHT_GNU_versym
    0x6fffffff
    /* Version symbol table.
    */
    #define SHT_HISUNW
    0x6fffffff
    /* Sun-specific high bound.
    */
    #define SHT_HIOS
    0x6fffffff
    /* End OS-specific type */
    #define SHT_LOPROC
    0x70000000
    /* Start of processor-specific */
    #define SHT_HIPROC
    0x7fffffff
    /* End of processor-specific */
    #define SHT_LOUSER
    0x80000000
    /* Start of application-specific */
    #define SHT_HIUSER
    0x8fffffff
    /* End of application-specific */
    
  3. 段标志位(sh_flages)
    段标志位 表示段在进程空间中的属性。

    /* Legal values for sh_flags (section flags).
    */
    #define SHF_WRITE
    (1 << 0)
    /* Writable */
    #define SHF_ALLOC
    (1 << 1)
    /* Occupies memory during execution */
    #define SHF_EXECINSTR
    (1 << 2)
    /* Executable */
    #define SHF_MERGE
    (1 << 4)
    /* Might be merged */
    #define SHF_STRINGS
    (1 << 5)
    /* Contains nul-terminated strings */
    #define SHF_INFO_LINK
    (1 << 6)
    /* `sh_info' contains SHT index */
    #define SHF_LINK_ORDER
    (1 << 7)
    /* Preserve order after combining */
    #define SHF_OS_NONCONFORMING (1 << 8)
    /* Non-standard OS specific handling
    required */
    #define SHF_GROUP
    (1 << 9)
    /* Section is member of a group.
    */
    #define SHF_TLS
    (1 << 10)
    /* Section hold thread-local data.
    */
    #define SHF_MASKOS
    0x0ff00000 /* OS-specific.
    */
    #define SHF_MASKPROC
    0xf0000000 /* Processor-specific */
    #define SHF_ORDERED
    (1 << 30)
    /* Special ordering requirement
    (Solaris).
    */
    #define SHF_EXCLUDE
    (1 << 31)
    /* Section is excluded unless
    referenced or allocated (Solaris).*/
  4. 段链接信息(sh_link, sh_info)
    如果段的类型是链接相关的(包括动态链接和静态链接),比如重定位表、符号表等,那么段链接信息的含义如下表。否则段链接信息无意义(值为0)。

    sh_typesh_linksh_info
    SHT_DYNAMIC该段所使用的字符串在字符串表中的下标0
    SHT_HASH该段所使用的符号表在段表中的下标0
    SHT_REL, SHT_RELA该段使用的符号表在段表中的下标该重定位表所作用的段在段表中的下标
    SHT_SYSTAB, SHT_DYNSYM操作系统相关操作系统相关
  5. 段地址对齐(sh_addralign)
    有些段是有地址对齐要求的,比如我们假设有个段刚开始的位置包含了一个double变量(8个字节),因为Intel x86架构要求浮点数的存储地址必须是本身地址的整数倍,那么这个段的sh_addr就必须是8的整数倍。地址对齐的数量都是2的指数倍,sh_addralign即表示对齐数量中的指数。如果sh_addralign为0或1,则表示该段对地址对齐没有要求。


    [Nr] Name Type Address Offset Size EntSize Flags Link Info Align
    [12] .strtab STRTAB 0000000000000000 00000650 000000000000005b 0000000000000000 0 0 1
    [ 2] .rela.text RELA 0000000000000000 000006b0 0000000000000078 0000000000000018 11 1 8

    从上表可以看到.shsstrtab的结束地址为 0x650 + 0x5b = 0x6ab,这与.rela.text开始地址并不一致,这就是地址对齐造成的。

  6. 项的长度(sh_entsize)
    有些段包含了一些固定大小的项,比如符号表,sh_entsize就表示每个固定项的大小。

5.3 重定位表

 [Nr] Name
Type
Address
Offset
Size
EntSize
Flags
Link
Info
Align
[ 2] .rela.text
RELA
0000000000000000
000006b0 0000000000000078
0000000000000018
11
1
8

这个段的类型(sh_type)为SH_REL,是一个重定位表。连接器在处理ELF文件时,必须对ELF文件的代码段和数据段中那些对绝对地址引用的位置进行重定位。这些定位的信息都记录在ELF文件的重定位表里,对于每个需要重定位的代码段或数据段,都会有一个相应的重定位表。

[Nr] Name
Type
Address
Offset
Size
EntSize
Flags
Link
Info
Align [ 1] .text
PROGBITS
0000000000000000
00000040 0000000000000054
0000000000000000
AX
0
0
4
[11] .symtab
SYMTAB
0000000000000000
000004d0 0000000000000180
0000000000000018
12
11
8

.rela.text的Link(sh_link)表示符号表的下标,也就是是.symtab的下标11;它的Inf(sh_info)表示它作用于哪个段,显然它作用于.text段,该段对应的下标是1。

5.4 字符串表

ELF文件中用到了很多字符串,比如段名、变量名等。因为字符串长度往往是不定的,所以用固定的结构来表示它比较困难。所以ELF文件把字符串集中起来放到了一个表,然后使用表偏移来引用字符串。常见的段表为.strtab(String Tab)和.shstrtab(Section Header String Table)。.strtab保存普通的字符串,比如符号的名字;.shstrtab保存段表中用到的字符串,比如段名(sh_name)。
5.1 文件头中ELF文件头信息中有一个e_shstrndx(Section header string table index)值为10,正好对应.shstrtab的下标。

六. 链接的接口——符号

在链接中,我们将函数和变量统称为符号(Symbol),函数名活变量名就是符号名(Symbol Name)。符号分为以下几类:

  1. 定义在本目标文件的全局符号,可以被其他目标文件引用。比如SimpleSection.o中的func1、main、g_init_var。
  2. 在目标文件中引用,却未定义的全局符号,一般叫做外部符号(External Symbol)。比如SimpleSection.o中的printf。
  3. 段名,这种符号往往由编译器产生,它的值就是该段的起始地址。
  4. 局部符号,这类符号只在编译单元内部课件。比如SimpleSection.o中的static_var和static_var2。
  5. 行号信息。

1 2 对其他目标文件不可见的。3 4 5对其他目标文件是不可见的。可以用gnm查看符号。

$ nm SimpleSection.o
0000000000000000 T func1
0000000000000000 D g_init_var
0000000000000004 C g_unint_var
0000000000000021 T main
U printf
0000000000000004 d static_var.2184
0000000000000000 b static_var2.2185

6.1 ELF符号表结构

ELF文件中符号表往往是文件中的一个段,段名叫.symtab。其结构对应elf.h中的Elf64_Sym。

/* Symbol table entry.
*/
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;
  1. 符号类型和绑定信息(st_info)

    • st_info的低4位表示符号类型,高4位表示绑定信息

      /* How to extract and insert information held in the st_info field.
      */
      #define ELF32_ST_BIND(val)
      (((unsigned char) (val)) >> 4)
      #define ELF32_ST_TYPE(val)
      ((val) & 0xf)
      #define ELF32_ST_INFO(bind, type)
      (((bind) << 4) + ((type) & 0xf))
      /* Both Elf32_Sym and Elf64_Sym use the same one-byte st_info field.
      */
      #define ELF64_ST_BIND(val)
      ELF32_ST_BIND (val)
      #define ELF64_ST_TYPE(val)
      ELF32_ST_TYPE (val)
      #define ELF64_ST_INFO(bind, type)
      ELF32_ST_INFO ((bind), (type))
      
    • 符号绑定信息

      /* Legal values for ST_BIND subfield of st_info (symbol binding).
      */
      #define STB_LOCAL
      0
      /* Local symbol */
      #define STB_GLOBAL
      1
      /* Global symbol */
      #define STB_WEAK
      2
      /* Weak symbol */
      #define STB_NUM
      3
      /* Number of defined types.
      */
      #define STB_LOOS
      10
      /* Start of OS-specific */
      #define STB_GNU_UNIQUE
      10
      /* Unique symbol.
      */
      #define STB_HIOS
      12
      /* End of OS-specific */
      #define STB_LOPROC
      13
      /* Start of processor-specific */
      #define STB_HIPROC
      15
      /* End of processor-specific */
      
    • 符号类型

      /* Legal values for ST_TYPE subfield of st_info (symbol type).
      */
      #define STT_NOTYPE
      0
      /* Symbol type is unspecified */
      #define STT_OBJECT
      1
      /* Symbol is a data object *//*比如变量、数组等*/
      #define STT_FUNC
      2
      /* Symbol is a code object *//*函数或其他可执行代码*/
      #define STT_SECTION
      3
      /* Symbol associated with a section *//*必须是STB_LOCAL*/
      #define STT_FILE
      4
      /* Symbol's name is file name */
      /*一般都是源文件名,一点是STB_LOCAL,并且它的st_shndx一定是SHN_ABS*/
      #define STT_COMMON
      5
      /* Symbol is a common data object */
      #define STT_TLS
      6
      /* Symbol is thread-local data object*/
      #define STT_NUM
      7
      /* Number of defined types.
      */
      #define STT_LOOS
      10
      /* Start of OS-specific */
      #define STT_GNU_IFUNC
      10
      /* Symbol is indirect code object */
      #define STT_HIOS
      12
      /* End of OS-specific */
      #define STT_LOPROC
      13
      /* Start of processor-specific */
      #define STT_HIPROC
      15
      /* End of processor-specific */
      
  2. 符号所在段(st_stndx)
    如果符号定义在目标文件中,则表示符号所在段的下标;如果不是定义在目标文件中,或者对于有些特殊符号,sh_shndx的值有些特殊。

    /* Special section indices.
    */
    #define SHN_UNDEF
    0
    /* Undefined section */
    /*符号在目标文件中被引用到,但是定义在其他目标文件中。*/
    #define SHN_LORESERVE
    0xff00
    /* Start of reserved indices */
    #define SHN_LOPROC
    0xff00
    /* Start of processor-specific */
    #define SHN_BEFORE
    0xff00
    /* Order section before all others(Solaris). */
    #define SHN_AFTER
    0xff01
    /* Order section after all others(Solaris). */
    #define SHN_HIPROC
    0xff1f
    /* End of processor-specific */
    #define SHN_LOOS
    0xff20
    /* Start of OS-specific */
    #define SHN_HIOS
    0xff3f
    /* End of OS-specific */
    #define SHN_ABS
    0xfff1
    /* Associated symbol is absolute */
    /*表示该符号包含了一个绝对的值*/
    #define SHN_COMMON
    0xfff2
    /* Associated symbol is common */
    /*表示该符号是一个 COMMON块 类型的符号,
    一般来说,未初始化的全局符号就是这种类型的*/
    #define SHN_XINDEX
    0xffff
    /* Index is in extra table.
    */
    #define SHN_HIRESERVE
    0xffff
    /* End of reserved indices */
    

    COMMON块 现在的编译器和链接器都支持一种叫COMMON块(Common Block)的机制,这种叫法最早来源于Fortran,早期的Fortran没有动态分配空间的机制,程序员必须事先声明所需要使用的临时空间的大小。Fortran把这种空间叫COMMON块,当不同的目标文件需要的COMMON块大小不一致时,以最大的那块为准。

    现代链接机制在处理弱符号(未初始化的全局变量)的时候,采用的就是与COMMON块一样的机制。

  3. 符号可见性(st_other)

    /* Symbol visibility specification encoded in the st_other field.
    */
    #define STV_DEFAULT
    0
    /* Default symbol visibility rules */
    #define STV_INTERNAL
    1
    /* Processor specific hidden class */
    #define STV_HIDDEN
    2
    /* Sym unavailable in other modules */
    #define STV_PROTECTED
    3
    /* Not preemptible, not exported */
    
    • STV_DEFAULT 用它定义的符号将被导出。换句话说,它声明符号是到处可见的。
    • STV_INTERNAL 符号在当前可执行文件或共享库之外不可访问。
    • STV_HIDDEN 用它定义的符号将不被导出,并且不能从其他对象进行使用。这将不允许它们在库中被导出,但是可以在源文件之间共享。实际上,隐藏的符号将不会出现在动态符号表中,但是还被留在符号表中用于静态链接。
    • STV_PROTECED 符号在当前可执行文件或共享对象之外可见,但是不会被覆盖。换句话说,如果共享库中的一个受保护符号被该共享库中的另一个代码引用,那么此代码将总是引用共享库中的此符号,即便可执行文件定义了相同名称的符号。
  4. 符号值(st_value)
    符号值分类:

    • 如果符号有定义并且不是COMMON块类型,则st_value表示该符号在段中的偏移。
    • 如果符号是COMMON块类型的,则st_value表示该符号的对齐属性。
    • 在可执行程序中,st_value表示符号的虚拟地址。

    执行

    $ readelf -s SimpleSection.o

    查看符号表详细信息。

    Symbol table '.symtab' contains 16 entries:
    Num:
    Value
    Size Type
    Bind
    Vis
    Ndx Name
    0: 0000000000000000
    0 NOTYPE
    LOCAL
    DEFAULT
    UND
    1: 0000000000000000
    0 FILE
    LOCAL
    DEFAULT
    ABS SimpleSection.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 static_var2.2185
    7: 0000000000000004
    4 OBJECT
    LOCAL
    DEFAULT
    3 static_var.2184
    8: 0000000000000000
    0 SECTION LOCAL
    DEFAULT
    7
    9: 0000000000000000
    0 SECTION LOCAL
    DEFAULT
    8
    10: 0000000000000000
    0 SECTION LOCAL
    DEFAULT
    6
    11: 0000000000000000
    4 OBJECT
    GLOBAL DEFAULT
    3 g_init_var
    12: 0000000000000004
    4 OBJECT
    GLOBAL DEFAULT
    COM g_unint_var
    13: 0000000000000000
    33 FUNC
    GLOBAL DEFAULT
    1 func1
    14: 0000000000000000
    0 NOTYPE
    GLOBAL DEFAULT
    UND printf
    15: 0000000000000021
    51 FUNC
    GLOBAL DEFAULT
    1 main

    详解如下:

    • func1和main都是定义在SimpleSection.o中的函数,func1所处的位置为.text.func1段,下标为4(见5.1段表中的段表详细信息),所以Ndx值为4,main的情况一样。它们是函数,所以类型是STT_FUNC;它们是全局符号,所以绑定信息是STB_GLOBAL;Size表示函数所占的字节数;Value表示函数相对段起始位置的偏移量。
    • printf该符号在SimpleSection.o中被引用,但是没有被定义,所以Ndx是SHN_UNDEF。
    • g_init_var是已经初始化的全局变量,在.data段,所以下标是2。
    • g_uninit_var是为初始化的全局变量,它是COMMON块类型的符号,它本身并没有存在于.bss段。
    • 对于那些STT_SETION类型的符号,它们具体在哪个段是由Ndx决定的,即下标为Ndx的段。
    • SimpleSection.c 表示目标文件的源文件名。

5.3 特殊符号

特殊符号:当我们使用ld来链接生成可执行文件时,它会为我们定义很多特殊的符号,这些符号并没有在你的程序中定义,但是你可以直接声明并引用它。几个很具代表性的符号如下。

  • __executable_start 程序的起始地址,不是入口地址,是程序最开始的地方。
  • __etext或_etext或etext 代码段的结束地址。
  • _edata或edata 数据段结束地址。
  • _end或end 程序结束地址。

举个例子:

系统配置为:
GNU ld version 2.23.52.0.1-55.el7 20130226
gcc version 4.8.5 20150623 (Red Hat 4.8.5-4) (GCC)
CentOS Linux release 7.2.1511 (Core)
#include <stdio.h>
extern char __executable_start[];
extern char etext[], _etext[], __etext[];
extern char edata[], _edata[], __edata[];
extern char end[], _end[];
int main() {
printf("executable start %pn", __executable_start);
printf(".text section end %p %p %pn", etext, _etext, __etext);
printf(".data section end %p %pn", edata, _edata);
printf("executable end %p %pn", end, _end);
return 0;
}

执行

$ gcc SpecialSymbol.c -o SpecialSymbol
$ ./SpecialSymbol
executable start 0x400000
.text section end 0x40061d 0x40061d 0x40061d
.data section end 0x601034 0x601034
executable end 0x601038 0x601038

注:我们所使用的链接器没有定义__edata。

5.4 符号修饰和函数名

在C语言发明时,已经存在了相当多的使用汇编编写的库和目标文件。这就产生了一个问题,如果一个C程序要使用这些库的话,C语言中不可以使用汇编库中定义的函数和变量的名字作为符号名,否则会发跟汇编库发送冲突。
为了防止类似的符号名冲突,UNIX下C语言就规定,C语言中所有的全局变量和函数经过编译后都在相应的符号名前加上下划线。而Fortran语言经过编译后,所有的符号前后都加上下划线。(现在已经不这么玩了。)这个简单的方法一定范围内解决了符号冲突问题,但当程序很大时,还是可能会有符号冲突的问题。于是像C++这样的后来设计的语言增加了命名空间的方法来解决多模块的符号冲突问题。

  1. C++符号修饰

    int func(int);
    float func(float);
    class C {
    int func(int);
    class IC {
    int func(int);
    };
    };
    namespace M {
    int func(int);
    class C {
    int func(int);
    };
    }

    为了区分不同参数和不同名称空间的同名函数,引入函数签名的概念,函数签名包含了一个函数的信息,包括函数名、参数类型、所在的类和命名空间及其他信息。
    编译器和链接器在处理符号时,他们使用名称修饰的方法,使得每个函数签名对应一个修饰后的名称(Decorated Name)。上面6个func函数签名在GCC编译器下,相对应的修饰后名称如下:

    函数签名修饰后名称(符号名)
    int func(int)_Z4funci
    float func(float)_Z4funcf
    int C::func(int)_ZN1C4funcEi
    int C::IC::func(int)_ZN1C2IC4funcEi
    int M::func(int)_ZN1M4funcEi
    int M::C::func(int)_ZN1M1C4funcEi

    GCC修饰规则:

    • 所有的符号都以_Z开头
    • 对应嵌套的名字(符号外层有命名空间或类)后面紧跟N
    • 然后是各个空间、类的名字(如果有的话)和函数的名称
    • 在参数列表之前加E
    • 最后是参数列表

    binutils提供了一个 c++filt(Mac下是gc++filt)工具来解析被修饰过的名称。执行

    $c++filt _ZN1M4funcEii 

    输出结果

    M::func(int, int)

    签名和名称修饰机制同样适用全局变量和静态变量,比如一个命名空间foo中的全局变量bar,它修饰后的名字是_ZN3foo3barE。可以看到,变量的类型并没有被加入到修饰后的名称中,所以不论bar是int还是float类型的,它修饰后的名称都是一样的。

5.5 extern “C”

C++为了与C兼容,在符号管理上,C++有一个用来声明或定义一个C的符号的 extern “C” {} 的用法。C++编译器会将大括号内的代码当作C代码处理,这时C++的符号修饰机制将不再起作用。

5.6 弱符号与强符号

  1. 强符号与弱符号
    强符号 编译器默认函数和已初始化的全局变量为强符号。
    弱符号 未初始化的全局变量为弱符号。
    链接器按如下规则处理多次定义的全局符号:
    • 不允许强符号被多次定义,如果有多个强符号定义,链接器会报符号重复定义错误。
    • 如果一个符号在A目标文件中是强符号,在B目标文件中是弱符号,那么选择强符号。
    • 如果一个符号在所有目标文件中都是弱符号,那么选择占用空间最大的那个。
  2. 强引用和弱引用
    强引用 如果没有找到该符号的定义,链接器就会报符号未定义错误。
    弱引用 如果没有找到该符号的定义,链接器一般会默认其为0,或一个特殊的值。
    GCC可以使用 __attribute__((weakref)) 这个扩展关键字来声明对一个外部函数的引用为弱引用。

这种弱符号和弱引用使得程序功能更加容易剪裁和组合。比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义的版本的库函数。或者程序可以对某些扩展功能模块的引用定义为弱引用,当我们将扩展模块与程序链接在一起时,功能模块正常使用;如果我们去掉了某些功能模块,那么程序也可以正常链接,只是少了某些功能。
举个例子:

#include <stdio.h>
#include <pthread.h>
int pthread_create(pthread_t*, const pthread_attr_t*, void* (*)(void*), void*) __attribute__((weak));
int main() {
if (pthread_create) {
printf("This is multi-thread version!n");
} else {
printf("This is single-thread version!n");
}
}

执行

$ gcc pthread.c -o pt
$ ./pt
This is single-thread version!
$ gcc pthread.c -lpthread -o pt
$ ./pt
This is multi-thread version!

5.7 调试信息

几乎所有的现代编译器都支持源代码级别的调试。如果在GCC编译时加上 -g 参数,编译器就会在目标文件里加上很多调试信息。

[Nr] Name
Type
Address
Offset
Size
EntSize
Flags
Link
Info
Align
[ 6] .debug_info
PROGBITS
0000000000000000
000000a0
000000000000012c
0000000000000000
0
0
1
[ 7] .rela.debug_info
RELA
0000000000000000
00000d38
0000000000000288
0000000000000018
19
6
8
[ 8] .debug_abbrev
PROGBITS
0000000000000000
000001cc
000000000000009e
0000000000000000
0
0
1
[ 9] .debug_aranges
PROGBITS
0000000000000000
0000026a
0000000000000030
0000000000000000
0
0
1
[10] .rela.debug_arang RELA
0000000000000000
00000fc0
0000000000000030
0000000000000018
19
9
8
[11] .debug_line
PROGBITS
0000000000000000
0000029a
000000000000004a
0000000000000000
0
0
1
[12] .rela.debug_line
RELA
0000000000000000
00000ff0
0000000000000018
0000000000000018
19
11
8
[13] .debug_str
PROGBITS
0000000000000000
000002e4
0000000000000109
0000000000000001
MS
0
0
1

这些段中保存的就是调试信息。现在的ELF文件采用一个叫DWARF(Debug With arbitrary Record Format)的标准调试信息格式。
在Linux下,我们可以使用 strip 命令来去掉文件中的调试信息。

strip SimpleSection.o

可以运行

readelf -S SimpleSection.o

验证调试信息是否去除。

最后

以上就是奋斗手套为你收集整理的《程序员的自我修养——链接、装在与库》第三章《目标文件里有什么》读书笔记的全部内容,希望文章能够帮你解决《程序员的自我修养——链接、装在与库》第三章《目标文件里有什么》读书笔记所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(32)

评论列表共有 0 条评论

立即
投稿
返回
顶部