目标文件的 ELF 结构

实验环境:

Linux version 5.11.0-46-generic (buildd@lgw01-amd64-010)
Ubuntu 20.04.3 LTS
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for Ubuntu) 2.34
Arch x86_64

ELF

ELF 为 可执行可链接文件格式Executable Linkable Format 的缩写. 可重定位文件Relocatable File (目标文件), 可执行文件Executable File, 共享目标文件Shared Object File 等文件类型都以 ELF 格式储存.

就是目标文件, Linux 下后缀为 .o (Object). 这一类文件是编译后链接前的中间文件. 其中, 代码里定义在其他模块(如其他 .c 文件)里的变量暂时没有地址, 在目标文件中用全 0 占位. 链接器链接文件时, 将其地址替换为变量真正的地址, 这个过程叫 重定位.

gcc 选项 -c 表示只编译不链接. 可以用如下命令把源代码编译为目标文件:

1
2
	gcc -m32 -c SimpleSection.c -o SimpleSection.o
	file SimpleSection.o
SimpleSection.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

不解释

即动态链接库, Linux 下后缀为 .so (Shared Object) 动态连接器可以与可执行文件结合, 作为程序进程的一部分来运行.

1
	file /lib/i386-linux-gnu/ld-2.31.so 
/lib/i386-linux-gnu/ld-2.31.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, BuildID[sha1]=c6d33dc0bb4a6e9fed4fa72af98b59b8d0fec3f1, stripped

目标文件的 ELF 结构

由于目标文件和可执行文件都是 ELF 结构, 而目标文件没有链接过程, 所以我们分析目标文件, 来窥探 ELF 的大致结构. ELF 由三个部分组成, 分别是 文件头Header, 一系列 Section, 节表Section Header Table程序段表Program Segment Header Table.

在目标文件中, 不需要程序段这个东西. 而节, 是目标文件中最重要的内容.

注意
«程序员的自我修养» 在这部分介绍的时候, 并没有区分段和节的概念, 而是把他们等同起来. 而这里则严格按照概念, 避免之后的学习出现混淆.

目标文件包含编译后的机器代码和数据, 以及一些链接需要的信息. 这些信息按照不同的属性存在不同的节里. objdump 可以查看目标文件的各个节.

下面以程序 SimpleSection.c 编译成的 SimpleSecton.o 为例, 源代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
int printf(const char* format, ...);		// 由于预编译会将 #include<stdio.h> 给展开, 引入更多东西, 不便于分析(虽然暂时不知道在.o里会是什么样子的, 但我猜作者肯定考虑了这一点qaq)

int global_init_var = 84;
int global_uninit_var;

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

int main() {
	static int static_var = 85;
	static int static_var2;

	int a = 1;
	int b;

	func1(static_var + static_var2 + a + b);

	return a;
}

命令 objdump -h 可以查看目标文件的结构和内容. (当然也可以查看可执行文件的结构和内容)

1
	objdump -h SimpleSection.o
SimpleSection.o:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
0 .group        00000008  00000000  00000000  00000034  2**2
              	CONTENTS, READONLY, GROUP, LINK_ONCE_DISCARD
1 .text         00000087  00000000  00000000  0000003c  2**0
              	CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
2 .data         00000008  00000000  00000000  000000c4  2**2
              	CONTENTS, ALLOC, LOAD, DATA
3 .bss          00000004  00000000  00000000  000000cc  2**2
              	ALLOC
4 .rodata       00000004  00000000  00000000  000000cc  2**0
              	CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .text.__x86.get_pc_thunk.ax 00000004  00000000  00000000  000000d0  2**0
              	CONTENTS, ALLOC, LOAD, READONLY, CODE
6 .comment      0000002b  00000000  00000000  000000d4  2**0
              	CONTENTS, READONLY
7 .note.GNU-stack 00000000  00000000  00000000  000000ff  2**0
              	CONTENTS, READONLY
8 .note.gnu.property 0000001c  00000000  00000000  00000100  2**2
              	CONTENTS, ALLOC, LOAD, READONLY, DATA
9 .eh_frame     0000007c  00000000  00000000  0000011c  2**2
              	CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

(和书上的不同, 可能是编译器和机器版本不同的原因, 以及开没开一些选项?)

可以看到, 文件格式是 elf32-i386, 即 32 位 Intel 80386 处理器下的 ELF 文件. 一共有 9 个非辅助性的节 (一些辅助性质的节, 如 .symtab 等"表"被省略掉了. 要查看所有的节, 可以使用 readelf -S). (其中有一堆我不知道是啥, 之后知识面广了再回来看QAQ.)

几个比较重要的节是 .text, 包含程序的 代码 (机器指令); .data 包含 已初始化的全局变量和静态局部变量; .bss 包含 未初始化的全局变量和静态局部变量, .rodata (read only data) 包含只读变量如 const 修饰的变量和字符串常量. 其他段如 .note 开头的段包含一些提示信息, .commment 段包含注释信息, (.group 书上没有网上查到的东西我暂时也看不懂, 先不理它.) .text.__x86.get_pc_thunk.ax 包含一个 (系统用于函数调用栈的?) 函数的代码, .eh_frame 包含一些调用栈信息的记录.

表格中一个节有两行, 第一行是一些数据, 比较重要的是 Size (大小[单位字节]) 和 File off (偏移量, 即开始虚拟地址), 可以发现, .group 从 0x34 开始, 大小为 0x08 字节, .text 从 0x3c 开始, 大小为 0x87 字节. 可以发现, .group 和 .text 是连着的. .group 之前 0x34 大小的数据是 ELF 文件头大小. 有些相邻节之间差了一两个字节, 是因为这个节需要对齐

第二行是各个节的属性, CONTENTS 表示是 ELF 的内容, 也即 存在 ELF 文件中; DATA 表示该节存放数据, CODE 表示该节存放代码, READONLY 表示该节只读. (ALLOC LOAD RELOC 啥的看不懂, 书上没讲, 留坑)

objdump 参数 -s 将节内容以十六进制打印, -d 反汇编代码并打印. 所以我们可以用这两个参数来查看 .text 详细内容.

1
	objdump -s -d SimpleSection.o
SimpleSection.o:     file format elf32-i386

Contents of section .group:
 0000 01000000 07000000                    ........        
Contents of section .text:
 0000 f30f1efb 5589e553 83ec04e8 fcffffff  ....U..S........
 0010 05010000 0083ec08 ff75088d 90000000  .........u......
 0020 005289c3 e8fcffff ff83c410 908b5dfc  .R............].
 0030 c9c3f30f 1efb8d4c 240483e4 f0ff71fc  .......L$.....q.
 0040 5589e551 83ec14e8 fcffffff 05010000  U..Q............
 0050 00c745f0 01000000 8b900400 00008b80  ..E.............
 0060 00000000 01c28b45 f001c28b 45f401d0  .......E....E...
 0070 83ec0c50 e8fcffff ff83c410 8b45f08b  ...P.........E..
 0080 4dfcc98d 61fcc3                      M...a..         
Contents of section .data:
 0000 54000000 55000000                    T...U...        
Contents of section .rodata:
 0000 25640a00                             %d..            
Contents of section .text.__x86.get_pc_thunk.ax:
 0000 8b0424c3                             ..$.            
Contents of section .comment:
 0000 00474343 3a202855 62756e74 7520392e  .GCC: (Ubuntu 9.
 0010 332e302d 31377562 756e7475 317e3230  3.0-17ubuntu1~20
 0020 2e303429 20392e33 2e3000             .04) 9.3.0.     
Contents of section .note.gnu.property:
 0000 04000000 0c000000 05000000 474e5500  ............GNU.
 0010 020000c0 04000000 03000000           ............    
Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 017c0801  .........zR..|..
 0010 1b0c0404 88010000 20000000 1c000000  ........ .......
 0020 00000000 32000000 00450e08 8502420d  ....2....E....B.
 0030 05448303 66c5c30c 04040000 28000000  .D..f.......(...
 0040 40000000 32000000 55000000 00480c01  @...2...U....H..
 0050 00471005 02750043 0f03757c 067e0c01  .G...u.C..u|.~..
 0060 0041c543 0c040400 10000000 6c000000  .A.C........l...
 0070 00000000 04000000 00000000           ............    

Disassembly of section .text:

00000000 <func1>:
   0:	f3 0f 1e fb          	endbr32 
   4:	55                   	push   %ebp
   5:	89 e5                	mov    %esp,%ebp
   7:	53                   	push   %ebx
   8:	83 ec 04             	sub    $0x4,%esp
   b:	e8 fc ff ff ff       	call   c <func1+0xc>
  10:	05 01 00 00 00       	add    $0x1,%eax
  15:	83 ec 08             	sub    $0x8,%esp
  18:	ff 75 08             	pushl  0x8(%ebp)
  1b:	8d 90 00 00 00 00    	lea    0x0(%eax),%edx
  21:	52                   	push   %edx
  22:	89 c3                	mov    %eax,%ebx
  24:	e8 fc ff ff ff       	call   25 <func1+0x25>
  29:	83 c4 10             	add    $0x10,%esp
  2c:	90                   	nop
  2d:	8b 5d fc             	mov    -0x4(%ebp),%ebx
  30:	c9                   	leave  
  31:	c3                   	ret    

00000032 <main>:
  32:	f3 0f 1e fb          	endbr32 
  36:	8d 4c 24 04          	lea    0x4(%esp),%ecx
  3a:	83 e4 f0             	and    $0xfffffff0,%esp
  3d:	ff 71 fc             	pushl  -0x4(%ecx)
  40:	55                   	push   %ebp
  41:	89 e5                	mov    %esp,%ebp
  43:	51                   	push   %ecx
  44:	83 ec 14             	sub    $0x14,%esp
  47:	e8 fc ff ff ff       	call   48 <main+0x16>
  4c:	05 01 00 00 00       	add    $0x1,%eax
  51:	c7 45 f0 01 00 00 00 	movl   $0x1,-0x10(%ebp)
  58:	8b 90 04 00 00 00    	mov    0x4(%eax),%edx
  5e:	8b 80 00 00 00 00    	mov    0x0(%eax),%eax
  64:	01 c2                	add    %eax,%edx
  66:	8b 45 f0             	mov    -0x10(%ebp),%eax
  69:	01 c2                	add    %eax,%edx
  6b:	8b 45 f4             	mov    -0xc(%ebp),%eax
  6e:	01 d0                	add    %edx,%eax
  70:	83 ec 0c             	sub    $0xc,%esp
  73:	50                   	push   %eax
  74:	e8 fc ff ff ff       	call   75 <main+0x43>
  79:	83 c4 10             	add    $0x10,%esp
  7c:	8b 45 f0             	mov    -0x10(%ebp),%eax
  7f:	8b 4d fc             	mov    -0x4(%ebp),%ecx
  82:	c9                   	leave  
  83:	8d 61 fc             	lea    -0x4(%ecx),%esp
  86:	c3                   	ret    

Disassembly of section .text.__x86.get_pc_thunk.ax:

00000000 <__x86.get_pc_thunk.ax>:
   0:	8b 04 24             	mov    (%esp),%eax
   3:	c3                   	ret    

其中, Contents of section .text: 这一段中间部分是以十六进制打印的指令, 左边是偏移, 右边是十六进制的 ASCII 码, 对照下面 Disassembly of section .text:, 左边是机器码, 右边是反汇编指令, 可以看到 .text 的指令和十六进制机器指令是一一对应的, 如开始四个字节 f30f1efb 就是反汇编后的第一行的机器码. 同理最后一行的 ret(c3), 也是 .text 最后一个字节. 并且可以看到, .text 一共 0x87 个字节, 这和 objdump -h 得到的是一样的.

.data 包含 已初始化的全局变量和静态局部变量, 而 .rodata 里存放的是只读变量, 如 const 修饰的变量, 字符串常量等. .data 里的变量的值可以被改变, 而 .rodata 是只读数据, 里面的变量不能被改变.

依旧使用 objdump -s -d 查看各个节的详细信息, 可以看到:

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

.data 有 8 个字节, 这正好是全局变量 global_init_var 和 静态局部变量 static_var 的大小(一个 int 4 个字节, 两个 int 8 个字节), 同时, 0x54000000 是十进制数 84 小端序的存储方式, 0x55000000 是十进制数 85 小端序的存储方式.

.rodata 一共有 4 个字节, 这里存放的是字符串常量 "%d\n" (就是 printf 的第一个参数), 数据 0x25 是字符 % 的 ASCII 码, 0x64 是字符 d 的 ASCII 码, 而 0x0a 是 \n 的 ASCII 码. 末尾的 0x00 是字符串结尾符号 \0.

bss 全称 Block Started By Symbol, 是为符号预留的空间. 也就是说, .bss 并不占用磁盘的空间, 而是在 程序运行后再分配空间. 但是根据 objdump -h 的结果可以看到, .bss 段只有 4 个字节, 而程序里有两个 int 未初始化的全局变量和局部静态变量: global_uninit_varabalstatic_var2.

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
3 .bss          00000004  00000000  00000000  000000cc  2**2
              	ALLOC

这是因为有些编译器会将目标文件中的未初始化的全局变量放在 COMMON 块, 链接成可执行文件后才会放在可执行文件的 .bss 节.

命令 objdump -x 可以查看符号表, 看到各个变量的存放位置.

查看目标文件的符号表, 可以看到如下内容:

SYMBOL TABLE:
00000000 l    df *ABS*	00000000 SimpleSection.c
00000000 l    d  .text	00000000 .text
00000000 l    d  .data	00000000 .data
00000000 l    d  .bss	00000000 .bss
00000000 l    d  .rodata	00000000 .rodata
00000004 l     O .data	00000004 static_var.1512
00000000 l     O .bss	00000004 static_var2.1513
00000000 l    d  .text.__x86.get_pc_thunk.ax	00000000 .text.__x86.get_pc_thunk.ax
00000000 l    d  .note.GNU-stack	00000000 .note.GNU-stack
00000000 l    d  .note.gnu.property	00000000 .note.gnu.property
00000000 l    d  .eh_frame	00000000 .eh_frame
00000000 l    d  .comment	00000000 .comment
00000000 l    d  .group	00000000 .group
00000000 g     O .data	00000004 global_init_var
00000004       O *COM*	00000004 global_uninit_var
00000000 g     F .text	00000032 func1
00000000 g     F .text.__x86.get_pc_thunk.ax	00000000 .hidden __x86.get_pc_thunk.ax
00000000         *UND*	00000000 _GLOBAL_OFFSET_TABLE_
00000000         *UND*	00000000 printf
00000032 g     F .text	00000055 main

和变量有关的符号是这四行:

00000004 l     O .data	00000004 static_var.1512
00000000 l     O .bss	00000004 static_var2.1513
00000000 g     O .data	00000004 global_init_var
00000004       O *COM*	00000004 global_uninit_var

可以看到, static_varglobal_init_var 被放在了 .data 节, static_var2 被放在了 .bss 节, 而 global_uninit_var 在 COMMON 里.

如果将程序链接, 得到可执行文件, 再用 objdump 查看 global_uninit_var 符号表, 可以发现确实是被放在了 .bss 节里:

1
	objdump -x SimpleSection | grep global_uninit_var
00004018 g     O .bss	00000004              global_uninit_var

有一个值得注意的点是, 如果已初始化的全局变量或者局部静态变量的初始值是 0, 那么他将被放在 .bss 节里. 因为 .bss 节里存放的未初始化的全局变量和局部静态变量的默认值就是 0, 这并不会改变内容, 而 .bss 节是不占用磁盘空间的.

我们可以写一个程序验证一下:

1
2
3
4
int main() {
	static int x = 0;
	static int y = 1;
}

还是编译到目标文件不链接, 然后查看符号表:

1
2
	gcc -m32 -c test.c -o test.o
	objdump -x test.o
test.o:     file format elf32-i386
test.o
architecture: i386, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x00000000

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
0 .group        00000008  00000000  00000000  00000034  2**2
              	CONTENTS, READONLY, GROUP, LINK_ONCE_DISCARD
1 .text         00000018  00000000  00000000  0000003c  2**0
              	CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
2 .data         00000004  00000000  00000000  00000054  2**2
              	CONTENTS, ALLOC, LOAD, DATA
3 .bss          00000004  00000000  00000000  00000058  2**2
              	ALLOC
4 .text.__x86.get_pc_thunk.ax 00000004  00000000  00000000  00000058  2**0
              	CONTENTS, ALLOC, LOAD, READONLY, CODE
5 .comment      0000002b  00000000  00000000  0000005c  2**0
              	CONTENTS, READONLY
6 .note.GNU-stack 00000000  00000000  00000000  00000087  2**0
              	CONTENTS, READONLY
7 .note.gnu.property 0000001c  00000000  00000000  00000088  2**2
              	CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .eh_frame     0000004c  00000000  00000000  000000a4  2**2
              	CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
SYMBOL TABLE:
00000000 l    df *ABS*	00000000 test.c
00000000 l    d  .text	00000000 .text
00000000 l    d  .data	00000000 .data
00000000 l    d  .bss	00000000 .bss
00000000 l     O .data	00000004 y.1505
00000000 l     O .bss	00000004 x.1504
00000000 l    d  .text.__x86.get_pc_thunk.ax	00000000 .text.__x86.get_pc_thunk.ax
00000000 l    d  .note.GNU-stack	00000000 .note.GNU-stack
00000000 l    d  .note.gnu.property	00000000 .note.gnu.property
00000000 l    d  .eh_frame	00000000 .eh_frame
00000000 l    d  .comment	00000000 .comment
00000000 l    d  .group	00000000 .group
00000000 g     F .text	00000018 main
00000000 g     F .text.__x86.get_pc_thunk.ax	00000000 .hidden __x86.get_pc_thunk.ax
00000000         *UND*	00000000 _GLOBAL_OFFSET_TABLE_


RELOCATION RECORDS FOR [.text]:
OFFSET   TYPE              VALUE 
00000008 R_386_PC32        __x86.get_pc_thunk.ax
0000000d R_386_GOTPC       _GLOBAL_OFFSET_TABLE_


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET   TYPE              VALUE 
00000020 R_386_PC32        .text
00000040 R_386_PC32        .text.__x86.get_pc_thunk.ax

可以看到这两行:

00000000 l     O .data	00000004 y.1505
00000000 l     O .bss	00000004 x.1504

确实 变量 x 放在了 .bss 节, 而变量 y 放在了 .data 节.

ELF 文件头共 52 个字节, 使用十六进制编辑器可以看到, SimpleSection.o 的文件头如下:

00000000:  7f45 4c46 0101 0100 0000 0000 0000 0000 0100 0300 0100 0000  :.ELF....................
00000018:  0000 0000 0000 0000 7404 0000 0000 0000 3400 0000 0000 2800  :........t.......4.....(.
00000030:  1000 0f00                                                    :....

使用 readelf -h 可以查看 ELF 文件头信息:

1
	readelf -h SimpleSection.o 
ELF Header:
Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
Class:                             ELF32
Data:                              2's complement, little endian
Version:                           1 (current)
OS/ABI:                            UNIX - System V
ABI Version:                       0
Type:                              REL (Relocatable file)
Machine:                           Intel 80386
Version:                           0x1
Entry point address:               0x0
Start of program headers:          0 (bytes into file)
Start of section headers:          1140 (bytes into file)
Flags:                             0x0
Size of this header:               52 (bytes)
Size of program headers:           0 (bytes)
Number of program headers:         0
Size of section headers:           40 (bytes)
Number of section headers:         16
Section header string table index: 15

我们可以对照 /usr/include/elf.h 中的定义来看这些数据的含义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* The ELF file header.  This appears at the start of every ELF file.  */

#define EI_NIDENT (16)

typedef struct
{
  unsigned char	e_ident[EI_NIDENT];	/* Magic number and other info */
  Elf32_Half	e_type;			/* Object file type */
  Elf32_Half	e_machine;		/* Architecture */
  Elf32_Word	e_version;		/* Object file version */
  Elf32_Addr	e_entry;		/* Entry point virtual address */
  Elf32_Off	e_phoff;		/* Program header table file offset */
  Elf32_Off	e_shoff;		/* Section header table file offset */
  Elf32_Word	e_flags;		/* Processor-specific flags */
  Elf32_Half	e_ehsize;		/* ELF header size in bytes */
  Elf32_Half	e_phentsize;		/* Program header table entry size */
  Elf32_Half	e_phnum;		/* Program header table entry count */
  Elf32_Half	e_shentsize;		/* Section header table entry size */
  Elf32_Half	e_shnum;		/* Section header table entry count */
  Elf32_Half	e_shstrndx;		/* Section header string table index */
} Elf32_Ehdr;

并且可以看到 elf.h 中有很多宏定义, 并且带了注释, 可以很方便了解每个变量值的含义.

文件头的前 16 个字节是 魔数Magic, 其中, 1-4 字节是 ELF 文件的标识码, 0x454c46 是 ‘ELF’ 的 ASCII 码. 第五个字节是文件类型, 0x01 表示 32 位, 0x02 表示 64 位. 第六个字节是大小端序, 0x01 是小端序, 0x02 是大端序. 第七个字节是 ELF 版本号, 一般为 0x01, 后面 9 个字节没有定义, 一般为 0. 有些平台会使用其作为拓展标志.

关于这些定义, 都可以在 elf.h 中找到:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/* Fields in the e_ident array.  The EI_* macros are indices into the
## 	array.  The macros under each EI_* macro are the values the byte
## 	may have.  */

#define EI_MAG0		0		/* File identification byte 0 index */
#define ELFMAG0		0x7f		/* Magic number byte 0 */

#define EI_MAG1		1		/* File identification byte 1 index */
#define ELFMAG1		'E'		/* Magic number byte 1 */

#define EI_MAG2		2		/* File identification byte 2 index */
#define ELFMAG2		'L'		/* Magic number byte 2 */

#define EI_MAG3		3		/* File identification byte 3 index */
#define ELFMAG3		'F'		/* Magic number byte 3 */

/* Conglomeration of the identification bytes, for easy testing as a word.  */
#define	ELFMAG		"\177ELF"
#define	SELFMAG		4

#define EI_CLASS	4		/* File class byte index */
#define ELFCLASSNONE	0		/* Invalid class */
#define ELFCLASS32	1		/* 32-bit objects */
#define ELFCLASS64	2		/* 64-bit objects */
#define ELFCLASSNUM	3

#define EI_DATA		5		/* Data encoding byte index */
#define ELFDATANONE	0		/* Invalid data encoding */
#define ELFDATA2LSB	1		/* 2's complement, little endian */
#define ELFDATA2MSB	2		/* 2's complement, big endian */
#define ELFDATANUM	3

#define EI_VERSION	6		/* File version byte index */
					/* Value must be EV_CURRENT */

#define EI_OSABI	7		/* OS ABI identification */
#define ELFOSABI_NONE		0	/* UNIX System V ABI */
#define ELFOSABI_SYSV		0	/* Alias.  */
#define ELFOSABI_HPUX		1	/* HP-UX */
#define ELFOSABI_NETBSD		2	/* NetBSD.  */
#define ELFOSABI_GNU		3	/* Object uses GNU ELF extensions.  */
#define ELFOSABI_LINUX		ELFOSABI_GNU /* Compatibility alias.  */
#define ELFOSABI_SOLARIS	6	/* Sun Solaris.  */
#define ELFOSABI_AIX		7	/* IBM AIX.  */
#define ELFOSABI_IRIX		8	/* SGI Irix.  */
#define ELFOSABI_FREEBSD	9	/* FreeBSD.  */
#define ELFOSABI_TRU64		10	/* Compaq TRU64 UNIX.  */
#define ELFOSABI_MODESTO	11	/* Novell Modesto.  */
#define ELFOSABI_OPENBSD	12	/* OpenBSD.  */
#define ELFOSABI_ARM_AEABI	64	/* ARM EABI */
#define ELFOSABI_ARM		97	/* ARM */
#define ELFOSABI_STANDALONE	255	/* Standalone (embedded) application */

#define EI_ABIVERSION	8		/* ABI version */

#define EI_PAD		9		/* Byte index of padding bytes */

可以看到, 16 个字节之后, 接下来两个字节 (Elf32_Half 是 2 个字节, 可以在 elf.h 中找到定义) 表示文件类型, 并且可以找到变量值的含义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/* 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 */
#define	ET_NUM		5		/* Number of defined types */
#define ET_LOOS		0xfe00		/* OS-specific range start */
#define ET_HIOS		0xfeff		/* OS-specific range end */
#define ET_LOPROC	0xff00		/* Processor-specific range start */
#define ET_HIPROC	0xffff		/* Processor-specific range end */

很容易发现, 0x0100 表示可重定位文件, 0x0200 是可执行文件, 0x0300 是共享目标文件, 0x0400 是内核文件. (注意这里是小端序).

对照上方十六进制内容, 这两个位置是 0x0100, 也就是说, SimpleSection.o 是可重定位文件. 当然这些信息都在 readelf -h 中可以很方便地查看.

ELF 文件头的其他内容都可以用如上的方法来查看了解信息.

目标文件中有很多个节, 这些节的信息由 节表Section Header Table 描述. 节表也被存储在 ELF 文件中, 且文件头中的 e_shoff 表述了节表的偏移量, 也就是节表从哪个地方开始. 节表用一种顺序的结构来存储, 就像是数组一样, 但是没有明着写成数组的形式, 因为节的个数是可变的, 而每个节的信息 (包括节名, 类型, 大小等) 的大小是固定的. 这样只需要用一个 “指针”, 从节表开始地址出发, 跳相应的倍数大小, 就可以找到各个表的信息了. (暂时还不清楚由于对齐导致的偏移, 这个 “指针” 会如何处理. 留个坑.)

我们可以根据上述方法, 找到 SimpleSection.o 的节表偏移, 第 32 个字节开始往后 4 个字节, 是 e_shoff 的值, 查看十六进制, 为 7404 0000 0000 0000, 也就是开始地址为 0x0474 (还是注意小端序), 这和 readelf -h 得到的十进制数 1140 是一样的.

使用命令 readelf -S 来查看节表的内容, 也就是各个节的信息. objdump -h 也能查看各个节, 不过 readelf -S 看到的才是所有节, 比 objdump -h 多了一些信息表, 如重定向表 .rel.text, .rel.eh_frame, 符号表 .symtab, 字符串表 .strtab, 节名字符串表 .shstrtab.

1
	readelf -S SimpleSection.o
There are 16 section headers, starting at offset 0x474:

Section Headers:
[Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .group            GROUP           00000000 000034 000008 04     13  17  4
  [ 2] .text             PROGBITS        00000000 00003c 000087 00  AX  0   0  1
  [ 3] .rel.text         REL             00000000 00037c 000048 08   I 13   2  4
  [ 4] .data             PROGBITS        00000000 0000c4 000008 00  WA  0   0  4
  [ 5] .bss              NOBITS          00000000 0000cc 000004 00  WA  0   0  4
  [ 6] .rodata           PROGBITS        00000000 0000cc 000004 00   A  0   0  1
  [ 7] .text.__x86.get_p PROGBITS        00000000 0000d0 000004 00 AXG  0   0  1
  [ 8] .comment          PROGBITS        00000000 0000d4 00002b 01  MS  0   0  1
  [ 9] .note.GNU-stack   PROGBITS        00000000 0000ff 000000 00      0   0  1
  [10] .note.gnu.propert NOTE            00000000 000100 00001c 00   A  0   0  4
  [11] .eh_frame         PROGBITS        00000000 00011c 00007c 00   A  0   0  4
  [12] .rel.eh_frame     REL             00000000 0003c4 000018 08   I 13  11  4
  [13] .symtab           SYMTAB          00000000 000198 000150 10     14  14  4
  [14] .strtab           STRTAB          00000000 0002e8 000092 00      0   0  1
  [15] .shstrtab         STRTAB          00000000 0003dc 000095 00      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),
  p (processor specific)

当然这里也可以用之前的人工查看十六进制的方法, 找到各个表的所有信息 — 不过这样很蠢.

至此, 我们根据文件头信息, 各个节的信息, 节表的信息, 就可以把目标文件 SimpleSection.o 的整个结构理清了 (有点长 QAQ):

SimpleSection.o
SimpleSection.o

这里就是整个文件的所有数据了. 整个文件大小 1780 个字节, 也就是上图所示的 0x000006f4.

仔细观察, 可以发现, 有一些内容中间有 1 个字节或者 3 个字节的空隙 (也可能是 2 个字节), 这是因为, 有一些变量需要对齐.

CPU 在读写数据的时候, 是以字为单位的. 也就是说, 在 32 位机器下, CPU 一次读写的数据大小是 4 个字节. 举个例子, 现在有一个 8 个字节的 double 存在了 0x00 - 0x08 处, 那么 CPU 可以取 0x00 - 0x04 和 0x04 - 0x08, 得到 double 的值. 但是, 如果这个 double 存在了 0x02 - 0x0a 处, 那么 CPU 就需要读取 0x00 - 0x04, 0x04 - 0x08, 0x08 - 0x0c 的数据, 这需要多一次的数据交换! 这样显然不利于 CPU 高效处理数据. 于是设计了对齐这种 用空间换时间 的方法. 简单来说, 就是强制要求 double 或者其他类型的数据, 存在能够用最少次数取出来的地址上. 用数学语言描述, 就是存放的地址必须是 4 的整数倍. 如果是 16 位机器, 那么就是 2 的整数倍, 64 位机器就是 8 的整数倍.

阅读 elf.h, 能够找到节的定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* 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;

这里先解释一下一些简单的变量含义. sh_name 是节的名字, 所有节名以字符串的形式被存在 .shstrtab 中, sh_name 的值是这个节名在 .shstrtab 中的偏移. sh_addr 留坑. sh_offset 是该节在文件中的偏移 (不在文件中的节如 .bss 这个值就没有意义了). sh_entsize 留坑. sh_addralign 是地址是否需要对齐标志, 为 0 或 1 的时候表示不需要对齐, 其他则表示地址需要对齐到 $2^{sh\_addralign}$.

接下来再解释几个比较重要的变量.

节的类型, 可以在 elf.h 中查看详细信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/* 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 */

对照这些定义, 再回去看 readelf -S 查看到的内容, 可以发现, .data, .text, .rodata, .comment, .eh_frame 等节, 都是 PROGBITS 程序数据类型. .rel.text.rel.eh_frameREL 重定位表类型. 重定位表是链接时一个重要的内容. .symtab 是 SYMTAB 符号类型, .strtab 和 .shstrtab 是 STRTAB 字符串表类型.

其中, 节表第 0 个位置, 有一个 NULL 型, 解释是没有使用的项. 这里的数据全是 0. 查看其位置 (即节表偏移) 十六进制以验证:

1
	hexdump -s 0x474 SimpleSection.o -n 40
0000474 0000 0000 0000 0000 0000 0000 0000 0000
*
000049c

-s 0x474 表示从偏移 0x474 开始输出, -n 40 表示输出 40 个字节的内容 (一个节占用空间是 40 个字节), 第二行的 * 表示都是 0, 直到 0x00049c

当我看到这个节的时候, 感觉这就是在浪费空间! 书上没有讲为什么会有一个空的节, 网上能够查到的也不多 (没有深入去搜索), 在 Oracle 关于 “链接与库 (Linker and Libraries Guide)” 的文档 里找到了这样的一句话:

引用
Should the number of sections or program headers exceed the ELF header data sizes, elements of section header 0 are used to define extended ELF header attributes. The following table shows the values.

大致意思就是说, 这里的空间可以作拓展的一个节, 或者 ELF 文件头的拓展数据. 具体什么情况会使用到暂时不清楚, 留坑.

节的标志, 表示节在虚拟地址空间中的属性, 比如可写, 可执行等. 同样在 elf.h 中可以看到所有属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/* 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_COMPRESSED       (1 << 11)  /* Section with compressed 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          (1U << 31) /* Section is excluded unless
                                           referenced or allocated (Solaris).*/

对照 readelf -S, 就可以看哪些节有哪些标志了.

其中, 执行过程中需要占用空间的节会有一个标志叫 ALLOC. 代码节, 数据节, .bss 等节, 程序运行时会载入到内存中, 所以他们有 ALLOC 标志. 而一些信息, 如 .comment, 其中的内容不需要载入内存, 所以没有 ALLOC 这个标志

这两个变量是链接相关的, 暂时跳过.

ELF 文件中的字符串用单独的表存起来, 使用的时候只需要一个下标就能找到字符串了.

.shstrtab 是 节表字符串表Section Header String Table, 用来保存节表中用到的字符串, 如节名 sh_name. 而 .strtab 是 字符串表String Table, 保存其他地方用到的字符串.

ELF 文件头中有一个 e_shstrndx, 表示节表字符串表在节表中的下标. readelf -h 可以查看:

1
	readelf -h SimpleSection.o | grep "Section header string table index"
Section header string table index: 15

可以看到, Section header string table index 的值为 15. 对比 readelf -S:

1
	readelf -S SimpleSection.o | grep -E ".shstrtab|Nr"
	[Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
	[15] .shstrtab         STRTAB          00000000 0003dc 000095 00      0   0  1

如果在编译的时候加上 -g 选项, 则会生成调试信息. 调试信息包含了代码及其对应的行号等等.

1
2
	gcc -m32 -c SimpleSection.c -o SimpleSectionDebug.o -g
	readelf -S SimpleSectionDebug.o  | grep -E "deb|Nr"
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 8] .debug_info       PROGBITS        00000000 0000d4 0000ca 00      0   0  1
  [ 9] .rel.debug_info   REL             00000000 0006ec 000090 08   I 21   8  4
  [10] .debug_abbrev     PROGBITS        00000000 00019e 00009d 00      0   0  1
  [11] .debug_aranges    PROGBITS        00000000 00023b 000020 00      0   0  1
  [12] .rel.debug_arange REL             00000000 00077c 000010 08   I 21  11  4
  [13] .debug_line       PROGBITS        00000000 00025b 00005e 00      0   0  1
  [14] .rel.debug_line   REL             00000000 00078c 000008 08   I 21  13  4
  [15] .debug_str        PROGBITS        00000000 0002b9 0000f4 01  MS  0   0  1
注意
这里是和链接有密切关系的节, 具体内容可能要等学了链接再来补, 这里只了解一个大概.

当用 readelf -S 查看目标文件时, 可以发现, 有两个打 .rel 开头的节:

1
readelf -S SimpleSection.o | grep -E "\.rel|Nr"
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 3] .rel.text         REL             00000000 00037c 000048 08   I 13   2  4
  [12] .rel.eh_frame     REL             00000000 0003c4 000018 08   I 13  11  4

可以看到, 他的类型是 REL. 翻阅 elf.h 可以找到:

1
#define SHT_REL           9             /* Relocation entries, no addends */

REL 表示 重定位条目. 链接的时候, 有些节需要重定位, 而这里就存放了重定位的信息. 对于每个需要重定位的数据节或者代码节, 都会有相应的重定位条目. 具体什么时候需要重定位, 暂时没学到, 留坑.

注意
书上叫 重定位表, 感觉他没有"表"的概念, 我更倾向于称它为"条目".

当一个节是重定位条目时, 它会有一些附加信息. 首先它需要知道作用于哪个节, 比如 .rel.text 作用于 .text, 所以 .rel.text 这个节中的 sh_info 会保存 .text 节的下标. 而 sh_link 中会保存符号表的下标(因为链接的时候要用?).

对照 readelf -S 的结果来看, .rel.text 的 sh_info 是 2, 也就是 .text 的下标; .rel.eh_frame 的 sh_info 是 11, 也就是 .eh_frame 的下标. 二者的 sh_link 都是 13, 也就是符号表 .symtab 的下标.

.symtab 节是符号表Symbol Table, 保存各个符号Symbol的信息. 所谓符号, 一般就是函数, 变量等, 也有一些特殊符号. 总之, 符号是为链接服务的, 可以看作是 “地址的标签”. 连接器正是根据符号才能进行正确的链接. 举个例子, 如果在某一个文件中定义了一个函数, 另一个文件中使用这个函数, 由于使用这个函数的文件没有该函数的代码, 目标文件中会创建一个函数的符号, 待链接时, 从其他文件中找到相同的符号, 然后链接起来.

查看目标文件的符号表可以用如下命令:

1
	readelf -s SimpleSection.o
Symbol table '.symtab' contains 21 entries:
 Num:    Value  Size Type    Bind   Vis      Ndx Name
   0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
   1: 00000000     0 FILE    LOCAL  DEFAULT  ABS SimpleSection.c
   2: 00000000     0 SECTION LOCAL  DEFAULT    2 
   3: 00000000     0 SECTION LOCAL  DEFAULT    4 
   4: 00000000     0 SECTION LOCAL  DEFAULT    5 
   5: 00000000     0 SECTION LOCAL  DEFAULT    6 
   6: 00000004     4 OBJECT  LOCAL  DEFAULT    4 static_var.1512
   7: 00000000     4 OBJECT  LOCAL  DEFAULT    5 static_var2.1513
   8: 00000000     0 SECTION LOCAL  DEFAULT    7 
   9: 00000000     0 SECTION LOCAL  DEFAULT    9 
  10: 00000000     0 SECTION LOCAL  DEFAULT   10 
  11: 00000000     0 SECTION LOCAL  DEFAULT   11 
  12: 00000000     0 SECTION LOCAL  DEFAULT    8 
  13: 00000000     0 SECTION LOCAL  DEFAULT    1 
  14: 00000000     4 OBJECT  GLOBAL DEFAULT    4 global_init_var
  15: 00000004     4 OBJECT  GLOBAL DEFAULT  COM global_uninit_var
  16: 00000000    50 FUNC    GLOBAL DEFAULT    2 func1
  17: 00000000     0 FUNC    GLOBAL HIDDEN     7 __x86.get_pc_thunk.ax
  18: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
  19: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
  20: 00000032    85 FUNC    GLOBAL DEFAULT    2 main

其中表中的第 0 个位置不存放信息(暂时不知道为啥), 并把符号类型设为 NOTYPE. readelf 没有显示节符号的名字, 这个符号名其实就是节名, 如 .text

查看 elf.h, 可以找到符号表的定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* 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;

其中, st_name 是符号名称, 实际上是一个下标, 通过这个下标去字符串表中索引真正的名称. st_info 包含了符号的信息, 低四位表示符号类型Symbol Type, 高 28 位表示符号绑定信息Symbol Binding.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/* 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 */
#define STT_FILE        4               /* Symbol's name is file name */
#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 */

符号的绑定信息有如下主要的三种: 局部符号, 全局符号, 弱引用. 局部符号对外部文件不可见, 全局符号对外部文件可见. 弱引用在讲符号的时候再说. 观察 readelf 的输出可以看到, 局部变量, 节, 都是 LOCAL 的. 全局变量和函数都是 GLOBAL.

符号类型有对象object(即变量), 函数funcation, section, 文件file等. 表示该符号是什么样的符号. 根据符号类型的不同, st_value 也有不同的含义. 如果是函数或者对象, 并且不是 COMMON 块 (COMMON 块是啥还没学到), 这个值 在目标文件中 则表示该对象或函数的地址 相对包含该符号的节起始位置 的偏移. 而在可执行文件中, 则是符号的虚拟地址.

st_size 是符号的大小, 如一个 int 变量的大小是 4 个字节, 那么这个值就是 4. 一个函数的 st_size 也是该函数指令所占的字节数. st_shndx 是包含该符号的节在节表中的下标. 符号如果不在当前目标文件中定义, 则这个值有特殊的含义. 比如 COMMON 指的是 COMMON 的符号 (如未初始化的全局变量), ABS 指的是绝对符号, 如文件类型的符号就是一个绝对符号. UND 指的是未定义的符号, 也就是引用了这个符号, 需要后续链接器去找到其他文件中该符号的定义.