1.可执行文件的格式
- Windows平台:Portable Executable(PE)
- Linux平台:Executable Linkable Format(ELF)
它们都是COFF(Common File Format)的变种
2.目标文件的内容
- File Header:文件头。描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接、入口地址、目标硬件、目标操作系统等信息
- .text / .code:代码段。
- .data:数据段。存储全局变量和局部静态变量。
- .bss:(Block Started by Symbol)为未初始化的全局变量和局部静态变量预留位置。
- .rodata:只读数据段
- .comment:注释信息段
- .note.GNU-stack:堆栈段
- .strtab:字符串表。保存普通的字符串。
- .shstrtab:(Section Header String Table)段表字符串表,保存段表中用到的字符串,最常见的就是段名(sh_name)。
- .rel.text:重定位表(Relocation Table)。链接器在处理目标文件时,需要对目标文件中某些部位进行重定位。
3.链接的接口——符号
- 将函数和变量统称为符号,函数名或变量名统称为符号名。每个目标文件都会有一个相应的符号表,这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值。对于变量和函数而言,符号值就是它们的地址。
4.ELF符号表结构
typedef struct {
Elf32_Word st_name; //符号名,包含该符号名在符号表中的下标;
Elf32_Addr st_value; //符号值。
Elf32_Word st_size; //符号大小,值等于该数据类型的大小
unsigned char st_info; //符号类型和绑定信息
unsigned char st_other; //该成员目前为0,没用
Elf32_Half st_shndx; //表示符号所属的段
}Elf32_Sym
- 符号类型和绑定信息
1)STB_LOCAL:值为0。局部符号,对于目标文件的外部不可见。
2)STB_GLOBAL:值为1。全局符号,外部可见。
3)STB_WEAK:值为2。弱引用。 - 符号类型
1)STT_NOTYPE:值为0。未知类型符号。
2)STT_OBJECT:值为1。该符号是数据对象,如变量、数组。
3)STT_FUNC:值为2。该符号是函数或其他可执行代码。
4)STT_SECTION:值为3。该符号是一个段,这种符号必须是STB_LOCAL。
5)STT_FILE:值为4。该符号表示文件名,一般是该目标文件对应的源文件名,它一定是STB_LOCAL类型的,并且它的st_shndex一定是SHN_ABS。
5.特殊符号
特殊符号被定义在ld链接器的链接脚本中。
- __executable_start:程序起始地址,注意不是入口地址。
- __etext或_etext或etext:代码段最末尾的地址。
- _edata或edata:数据段最末尾的地址。
- _end或end:程序结束地址。
6.符号修饰与函数签名
- 符号修饰
为了防止类似的符号起冲突,有些编译器会为全局变量和函数名前后添加“—”。例如,foo 或_foo. - 函数签名
函数签名用于识别不同的函数,它包含了一个函数的信息,包括函数名、参数类型所在的类和命名空间及其他信息。
考虑如下函数重载的情况:
int func(int);
float func(float);
class C {
int func(int);
class C2 {
int func(int);
}
}
namespace N {
int func(int);
class C {
int func(int);
}
}
- 根据GCC的基本C++名称可得符号名如下。基本规则:所有的符号都以“_Z”开头,对于嵌套的名字后面紧跟“N”,然后是各个名称空间和类的名字,每个名字前是名字字符串长度,再以“E”结尾。对于一个函数来说,它的参数列表紧跟在“E”后面(对于int类型就是i,对于float类型就是f)。
函数前面 | 修饰后名称(符号名) |
---|---|
int func(int) | _Z4funci |
float func(float) | _Z4funcf |
int C::func(int) | _ZN1C4funcEi |
int C:: C2::func(int) | … |
int N::func(int) | … |
int N::C::func(int) | … |
7.extern “C”
- C++为了与C兼容,在符号的管理上。C++有一个用来声明或定义一个C的符号的“ extern “C” ”关键字用法。此外利用C++的宏“__cplusplus”。如果当前编译单元是C++代码,那么memset会在extern "C"里声明,否则直接声明。
#ifdef __cplusplus
extern "C" {
#endif
void *memset(void *, int, size_t);
#ifdef __cplusplus
}
#endif
8.弱符号与强符号
- 编译器将默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。同时,也可通过GCC的"__ attribute __((weak))"来定义任何一个强符号为弱符号。
- 弱符号机制允许同一个符号的定义存在于多个文件中。
extern int ext;
int weak;
int strong = 1;
__attibute__((weak)) weak2 = 2;
- weak 和 weak2 是弱符号。
3条规则:
1)强符号不可在不同模块被定义多次;
2)若全局变量在一个模块未被初始化,而在另一个模块被初始化了,那么链接器将选择在另一个模块中定义的强规则;
3)弱符号可以被多次定义。
- 规则1例子:这种情况链接器会报错
//foo2.c
int x = 111;
int main(int argc, char const *argv[])
{
return 0;
}
//bar2.c
int x = 222;
void f(){
}
- 规则2例子:这种情况函数f会将x 的值改为222。这种错误是不易察觉的。
//foo2.c
int x = 111;
int main(int argc, char const *argv[])
{
f();
return 0;
}
//bar2.c
int x;
void f(){
x = 222;
}