程序员的自我修养(1)

预编译、编译、汇编、链接

链接过程:

地址和空间分配,符号决议(又被称为符号绑定,名称绑定,名称决议,地址绑定等等),重定位;

目标文件:源代码编译过后但是还没有链接,如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))” 来定义任何一个强符号为弱符号。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值