预编译、编译、汇编、链接
链接过程:
地址和空间分配,符号决议(又被称为符号绑定,名称绑定,名称决议,地址绑定等等),重定位;
目标文件:源代码编译过后但是还没有链接,如windows平台下的.obj,Linux平台下的.o(可在linux平台下采用file指令查看文件格式)

历史小插曲,pe文件和elf文件都源自于coff文件
ELF文件解析(.o文件格式):

利用命令查看文件格式:
.text 代码段
.data已初始化的全局变量和局部静态变量数据段
.bss 未初始化的全局变量和局部静态变量数据段(
.bss段只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。
程序运行的时候它们的确是要占内存空
间的,并且可执行文件必须记录所有未初始化的全局变量和局部静态变
量的大小总和,记为
.bss
段。
)
.comment注释段
.note.GNU-stack 堆栈提示段

属性信息:CONTENTS属性表示该段存在,File off和Size是最容易理解的。
比如.text的File off 是00000040,那么ELF Header的大小就是00000039;
代码段:

左边是偏移量,右侧是具体代码
数据段:
源文件中有两个静态变量,所以.data的大小是8字节

00000054(8进制)刚好是84
.rodata
“.rodata”
段存放的是只读数据,一般是程序里面的只读变量(如
const
修饰的变量)和字符串量(不同的编译器放的位置可能不一样)。单独设立“.rodata”
段有很多好处,不光是在 语义上支持了C++
的
const
关键字,而且操作系统在加载的时候可以 将“.rodata”
段的属性映射成只读,这样对于这个段的任何修改操作都会 作为非法操作处理,保证了程序的安全性。另外在某些嵌入式平台下, 有些存储区域是采用只读存储器的,如ROM
,这样将
“.rodata”
段放在该 存储区域中就可以保证程序访问存储器的正确性。)
.bss段
.bss
段存放的是未初始化的全局变量和局部静态变量,如上述代码中 global_uninit_var 和
static_var2 就是被存放在.bss
段,其实更准确的说法 是.bss
段为它们预留了空间。但是我们可以看到该段的大小只有
4
个字 节,这与 global_uninit_var 和 static_var2 的大小的8
个字节不符。
其实我们可以通过符号表(
Symbol Table,下图
)看 到,只有 static_var2 被存放在了.bss
段,而 global_uninit_var 却没有被存放 在任何段,只是一个未定义的“COMMON
符号
”
。这其实是跟不同的语 言与不同的编译器实现有关,有些编译器会将全局的未初始化变量存放 在目标文件.bss
段,有些则不存放,只是预留一个未定义的全局变量符 号,等到最终链接成可执行文件的时候再在.bss
段分配空间。
static int x1=0;
static int x2=1;
x1会被放在.bss段被当作未初始化的,可以节省磁盘空间
符号表(1):
其他段:

ELF文件描述:
文件头(是一个结构体):

书中的是32版本,而我们这个是64版本的,贴一下书中对32位版本各个字段的解释:

ELF魔数的解释:
文件头其他字段的解释:
Type字段:
Machine:

Section Header Table
(段表,类似于14个Elf64_Shdr的数组,数组第一个值为null):

段描述符
(这里贴的是32位机器的):

我们这里的每个段表项的大小是64字节,从文件头可以看出来。段表在文件中的偏移是1192字节,即段表是从1193字节处开始的。
段表项的各个字段的解释:
sh_type:
段的类型( sh_type
) 正如前面所说的,段的名字只是在链接和编译过程 中有意义,但它不能真正地表示段的类型。我们也可以将一个数据段命 名为“.text”
,对于编译器和链接器来说,主要决定段的属性的是段的类
型(sh_type )和段的标志位( sh_flags )。段的类型相关常量以SHT_
开
头

sh_flag:
段的标志位
sh_flag
) 段的标志位表示该段在进程虚拟地址空间中的属 性,比如是否可写,是否可执行等。相关常量以SHF_
开头

一些段的sh_type字段和sh_flag字段属性:

段的链接信息:sh_linkmsh_info
如果段的类型是与链接相关的(不论
是动态链接或静态链接),比如重定位表、符号表等,那么 sh_link 和
sh_
info
这两个成员所包含的意义如表
3-11
所示。对于其他类型的段,这 两个成员没有意义。
重定位表
.rela.text是对.text段的重定位表,一个重定位表也是ELF的一个段,这个段的类型就是sh_type就是SHT_REL类型(显然这里我的机器上叫RELA),这里的32机器上,sh_link表示符号表的下标,sh_info表示其作用于哪个段。
字符串表
常见段名为:.strtab(保留普通的字符串)或者.shstrtab(段表中用到的字符串,如段的名称)
符号表(2)
段名为:.symtab
补充:
在链接中,我们将函数和变量统称为符号(
Symbol
),函数名或变
量名就是符号名(
Symbol Name
).
符号的分类:
(1)定义在本目标文件的全局符号,可以被其他目标文件引用。比如
SimpleSection.o
里面的
“
func1 和main ”和
“ global_init_var ”。
(2)在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫
做外部符号(
External Symbol
),也就是我们前面所讲的符号引用。比
如
SimpleSection.o
里面的
“printf”
。
(3)段名,这种符号往往由编译器产生,它的值就是该段的起始地址。比如
SimpleSection.o
里面的
“.text”
、
“.data”
等。
(4)局部符号,这类符号只在编译单元内部可见。比如
SimpleSection.o
里面 的“
static_var ”和
“ static_var2 ”。调试器可以使用这些符号来分析程序或崩
溃时的核心转储文件。这些局部符号对于链接过程没有作用,链接器往
往也忽略它们。
(5)
行号信息,即目标文件指令与源代码中代码行的对应关系,它也是可选
的。
符号表的结构:
这里给的是32位的:
这是对其字段的解释:
st_info
的解释,该成员低四位表示符号的类型,高28位表示符号绑定信息:

st_shndx:
如果符号定义在本目标文件内,那么这个成员表示符号所在段在段表中的下标;如果符号不是定义在本目标文件中,或者对于某些特殊符号,则其值有些特殊:

这里几个字段(符号值)的配合使用:
在目标文件中,如果是符号的定义并且该符号不是
“COMMON
块
”
类型 的(即st_shndx
不为
SHN_COMMON
,具体请参照
“
深入静态链接
”
一章 中的“COMMON
块
”
),则
st_value
表示该符号在段中的偏移。即符号所
对应的函数或变量位于由st_shndx
指定的段,偏移
st_value
的位置。这也
是目标文件中定义全局变量的符号的最常见情况,比如
SimpleSection.o中的“ func1
”
、
“ main”和
“ global_init_var ”。在目标文件中,如果符号是“COMMON
块
”
类型的(即 st_shndx
为 SHN_COMMON),则
st_value
表示该符号的对齐属性。比如 SimpleSection.o中的
“
global_uninit_var ”。
示例如下:

func1 和 main
函数都是定义在
SimpleSection.c
里面的,它们所在的位置都
为代码段,所以
Ndx
为
1
,即
SimpleSection.o
里面,
.text
段的下标为
1
。这
一点可以通过
readelf –a
或
objdump –x
得到验证。它们是函数,所以类型
是STT_FUNC ;它们是全局可见的,所以是 STB_GLOBAL
Size
表示函数指令所
占的字节数;
Value
表示函数相对于代码段起始位置的偏移量。
再来看
printf
这个符号,该符号在
SimpleSection.c
里面被引用,但是没有 被定义。所以它的Ndx
是
SHN_UNDEF
。
global_init_var 是已初始化的全局变量,它被定义在.bss
段,即下标为
3
。
global_uninit_var 是未初始化的全局变量,它是一个 SHN_COMMON
类型的符 号,它本身并没有存在于BSS
段;关于未初始化的全局变量具体请参 见“COMMON
块
”
。
static_var.1533
和
static_var2.1534
是两个静态变量,它们的绑定属性是
STB_LOCAL,即只是编译单元内部可见。
对于那些
STT_SECTION
类型的符号,它们表示下标为
Ndx
的段的段
名。它们的符号名没有显示,其实它们的符号名即它们的段名。比如
2
号符号的
Ndx
为
1
,那么它即表示
.text
段的段名,该符号的符号名应该就
是
“.text”
。如果我们使用
“objdump –t”
就可以清楚地看到这些段名符号。
“SimpleSection.c”
这个符号表示编译单元的源文件名
特殊符号:
链接器定义好的:如__executable_start表示程序起始地址.测试过该代码无法运行,留个坑--
符号修饰:防止命名冲突
函数签名:
函数签名用于识别不同的函数,就像签名用于识别不同的人一样,函数的名字只是函数签名的一部
分。
函数签名包含了一个函数的信息,包括函数名、它的参数
类型、它所在的类和名称空间及其他信息。
一个防止C++和C链接冲突的小tip:
#ifdef __cplusplus
extern "C"{
#endif
void *memset(void*,int,size_t);
#ifdef _cplusplus
}
#endif
弱符号和强符号:
对于
C/C++
语言来说,编译器默 认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符 号。我们也可以通过GCC
的 “__attribute__((weak))” 来定义任何一个强符号为弱符号。