文章目录
  1. 目标文件的格式
  2. 目标文件的内容
    1. 整体结构
    2. 代码段
    3. 只读数据段
    4. 数据段
    5. 其他段
  3. ELF文件结构
    1. 文件头
    2. 段表
    3. 重定位表
    4. 字符串表
  4. 符号
    1. 符号表结构
    2. 特殊符号
    3. 符号修饰与函数签名
    4. extern “C”
    5. 弱符号与强符号

目标文件的格式

不仅是目标文件(.o与.obj)按照可执行文件(.elf与.exe)格式存储,动态链接库(.so与.dll)与静态链接库(.a与.lib)也按照可执行文件格式存储。对于静态链接库,稍有不同的是它将多个目标文件拼在一起,再加上一些索引形成一个文件。ELF文件标准将系统中采用的ELF格式的文件分为如下4类。

 

在Ubuntu上可使用file命令查看一个文件的格式。

目标文件的内容

可执行文件主要有.text代码段(存放执行指令),.data数据段(已初始化的全局变量与局部静态变量),.bss段(未初始化的全局变量与局部静态变量)。.bss段为未初始化的全局变量与局部静态变量预留位置,在文件中不占据空间。

Q:分段的好处?

A:安全性(数据段可读,代码段可执行),共享(当系统运行程序的多个副本,内存中只需要保存一份程序的指令部分,即代码段),良好的局部性。

整体结构

使用objdump工具来查看目标文件的结构与内容。

1
2
3
4
# -c参数指定只编译,不链接
gcc -c simple_section.c
# -h参数指定只打印各个段的基本信息
objdump -h simple_section.o

其中simple_section.c内容如下。

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
#include <stdio.h>

unsigned int global_init_var=84;

unsigned int global_uninit_var;

int func(unsigned int);

int func(unsigned int N){
unsigned int X,Y;
X=N;
Y=N+1;
while(X){
Y+=X;
X--;
}
return Y;
}

int main(char * * argv, int argc){
unsigned int ret;
ret=func(global_init_var);
printf("func(global_init_var)=%d\n", ret);
return 0;
}

分析结果如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
simple_section.o:     file format elf64-x86-64

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000069 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000004 0000000000000000 0000000000000000 000000ac 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 000000b0 2**0
ALLOC
3 .rodata 0000001a 0000000000000000 0000000000000000 000000b0 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000002a 0000000000000000 0000000000000000 000000ca 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000f4 2**0
CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 000000f8 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

可以看到simple_section.o有如下段:.text、.data、.bss、.rodata、.comment、.note.GNU-stack、.eh_frame。各段的分布大致如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
      |        |
|--------|0xf4
| |
0x2a |.comment|
| |
| |
|--------|0xca
| |
0x1a |.rodata |
| |
|--------|0xb0
0x04 |.data |
|------- |0xac
|------- |0xa9
| |
| |
0x69 | |
|.text |
| |
| |
|--------|0x40

此外,readelf工具可对ELF格式文件进行分析。

代码段

使用objdump也可以查看代码段。

1
2
#-s 参数指定以16进制打印数据,-d 参数将指令部分反汇编
objdump -s -d simple_section.o

只读数据段

在.rodata段中存放只读数据,一般是程序中的字符串常量与const修饰的变量。

数据段

数据段信息如下。

1
2
Contents of section .data:
0000 54000000 T...

从这里可以看出:x86架构的CPU使用的小端字节序(高位字节存放在高地址,低位字节存放在低地址),与之对应的还有大端字节序(网络设备传输字节使用大端字节序,高位字节存放在低地址,最先传输)。

其他段

其他段的说明如下。

 

GCC提供扩展机制使程序编写者可指定变量所处段:__attribute__((section("name")))

ELF文件结构

 

文件头

使用readelf工具来查看ELF的文件头。

1
readelf -h simple_section.o

段表

段表描述ELF文件各个段的信息,包括每个段的名称、长度、偏移、读写权限等。

用readelf工具来查看段表的内容。

1
readelf -S simple_section.o

段表中的元素是段描述符,第一个元素为NULL,无效的段描述符。

重定位表

链接器在链接目标文件时,需要对来自其他模块的变量或函数进行重定位。需要重定位的变量或函数以及重定位所需的信息保存在重定位表中。

字符串表

由于字符串的长度不固定,为了用固定的长度表示字符串,常见的做法为:将字符串集中存放到一个字符串表,然后使用字符串在表中偏移来引用字符串。

符号

在链接时,函数与变量统称为符号,函数名或变量名即为符号名。目标文件中都有一个符号表,记录了目标文件中所有符号及其相关信息(符号名、符号值、符号大小、符号类型与绑定信息、符号所在段)。链接器重点关注每个目标文件中的定义全局符号与被引用的全局符号。

符号表结构

符号表在目标文件中通常作为一个段——.symtab,使用readelf工具来查看目标文件的符号表。

1
readelf -s simple_section.o

特殊符号

链接器在链接目标文件时会定义很多特殊符号,虽然未在源代码中定义,但是可以直接声明并引用它们。这就是特殊符号。如下例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

extern char __executable_start[];
extern char etext[], _etext[], __etext[];
extern char edata[], _edata[];
extern char end[], _end[];

int main()
{
printf("Executable start %X\n", __executable_start);
printf("Text end %X %X %X\n", etext, _etext, __etext);
printf("Data end %X %X\n", edata, _edata);
printf("Executable end %X %X\n", end, _end);
return 0;
}

符号修饰与函数签名

为了防止符号名冲突,Unix下C语言规定:源代码中所有全局变量与函数经过编译后在符号名前加”_”。C++为支持重载,编译器增加了符号修饰机制。修饰后的名称因函数名、参数类型、所在类与名称空间等性质的不同而不同。

extern “C”

C++编译器会将extern "C"的大括号内部的代码当作C语言处理。相应地,若C++源代码中要包含C语言库,就存在符号修饰不一致的问题,为解决这个问题,借助宏__cplusplus,C++编译器在编译C++源代码时会定义这个宏,于是可以通过这个宏是否被定义判断当前编译的语言是C语言还是C++。而如果是C++,则包含C语言库的语句要用extern "C"括号包含。

弱符号与强符号

对C语言与C++,编译器默认函数与初始化了的全局变量为强符号,未初始化的全局变量为弱符号。使用__attribute__((weak))在GCC编译时将强符号修饰为弱符号。

在链接时,强符号不允许被多次定义;若一个符号在某个目标文件中是强符号,而在其他文件中是弱符号,链接器选择链接强符号;若一个符号在所有目标文件中均为弱符号,链接器选择链接占用空间最大的那个。

弱引用与强引用:对一个强引用的符号,若链接器在链接时找不到该符号的定义,则会报符号未定义错误,而对于弱引用符号,则不会报错。使用__attribute__((weakref))可将对一个函数的引用声明为弱引用。