3.5 链接的接口–符号
在链接中,我们将函数名和变量统称为符号,函数名和变量名就是符号名。
每一个目标文件都有一个符号表,里面记录了目标文件中所有用到的符号。
每一个符号都有一个对应的值,叫做符号值。对于变量和函数来说,符号值就是地址。
符号分为5类。
**(1)本目标文件中定义的全局符号。可以被其他目标文件引用。如fun1、main、global_init_var
(2)不在本目标文件中定义的全局符号。称为外部符号。如printf**
(3)段名。由编译器产生,它的值就是该段的起始地址。如.text、.data等。
(4)局部符号。只在编译单元内可见。如static_var、static_var2
(5)行号信息。即目标文件指令与源代码中代码行的对应关系,可选。
其中,(1)(2)最为重要。
使用:$nm SimpleSection.o查看符号
nm命令用来列出一个目标文件中的各种符号。
T:Text段的符号。
D:已初始化的变量符号
C:该符号为common。common symbol是未初始化的的数据段。只在链接时被分配。
U:在当前文件中未定义,定义在别的文件中。
3.5.1 ELF符号表结构
符号表是一个Elf32_Sym结构的数组,一个Elf32_Sym对应一个符号。
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;
利用readelf查看ELF文件的符号:
解释:
- 第6列vis段未使用,忽略。
- printf在本文件中未定义,因此它的Ndx是SHN_UNDEF。
- static_var.1488和staic_var2.1489是两个静态变量,绑定属性是STB_LOCAL在编译单元内部可见。用到了”符号修饰“
(1)符号类型和绑定信息
高28位表示符号绑定信息;第4位表示符号类型。
(2)符号所在的段
如果符号定义在本目标文件中,则表示符号所在段在段表中的下标;如果不在或有些特殊符号,则如表所示:
如:
(3)符号值
分下列3种情况:
- 如果时符号的定义且符号不是”COMMON块“类型的,则表示该符号在段中的偏移。???
- 如果是”COMMON块“类型的,则表示该符号的对齐属性。如global_uninit_var.
- 在可执行文件中,表示符号的虚拟地址。该地址对动态链接器十分有用。
3.5.2 特殊符号
有些符号没有在程序中定义,但可以直接声明并引用,我们称为特殊符号。链接器会在链接时将其解释为正确的值。
几个代表性的特殊符号如下:
- __executabel_start:程序起始地址。注意不是入口地址,是程序最开始地址
- __etext、_etext、etext:代码段结束地址
- _edata、edata:数据段结束地址
- _end、end:程序结束地址
以上都是虚拟地址。
使用这些符号:
#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\n",end,_end);
return 0;
}
编译并运行:
3.5.3 符号修饰与函数签名
为防止库函数与自定义函数的命名冲突,采取了符号修饰来避免。例如:C语言代码中所有全局变量和函数经过编译后,会在符号名前加上”_”;C++增加了名称空间来解决符号冲突问题。
例如:
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);
};
};
这6个func都是不同的,因为它们的函数签名不同。函数签名包含了一个函数的信息,包括函数名、参数类型、所在的类和名称空间。由于它们的参数类型、所处的类、名称空间的不同,因此函数签名是不同的。
3.5.4 extern “C”
C++为了与C兼容,在符号的管理上,C++有一个用来声明或定义一个C的符号的“extern ”C”“关键字用法。如
extern "C"{
int func(int);
int var;
}
C++编译器会将extern “C”的大括号内部的代码当作C语言代码处理。
考虑以下情况:
C语言代码或C++包含了用C语言编写的函数或全局变量,如C语言库函数中的string.h中声明了memset这个函数,它的原型如下:
void memset(void ,int,size_t);
当C语言用到这个函数时,会正确处理;但是在C++语言中,编译器会认为memset是一个C++函数,于是将memset的符号修饰成_Z6memsetPvii,这样链接器就无法与C语言库中的memset符号进行链接。为了解决这个问题,我们使用C++的宏”__cplusplus“,C++编译器在编译C++的程序时默认定义这个宏,我们可以使用这个宏判断当前编译单元是不是C++代码。具体代码如下:
#ifdef __cplusplus
extern "C"{
#endif
void *memset(void *,int,size_t);
#ifdef __cplusplus
}
#endif
如果当前编译单元是C++代码,那么memset会在extern “C”里面被声明;
如果是C代码,就直接声明。
这个技巧很重要!几乎在所有的系统头文件中都被用到。
3.5.5 弱符号与强符号
1、对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。
可以通过GCC的”attribute((weak))“来将强符号定义为弱符号。
注意:强符号和弱符号都是针对定义来说,不是针对引用
链接器对全局符号的处理规则:
- 不允许强符号被多次定义
- 如果一个符号在某个目标文件中是强符号,在其他文件是弱符号,则选择强符号。
- 如果一个符号在所有目标文件中是弱符号,那么选择其中占用空间最大的那个。
2、强引用与弱引用
强引用:对外部文件的符号引用在目标文件被最终链接成可执行文件时,需要被正确决议,如果没有找到该符号的定义,链接器就会报未定义错误。
弱引用:如果该符号有定义,链接器将正确处理;如果未定义,链接器不报错。
对于未定义的弱引用,链接器将其默认为0,或一个特殊的值,以便于程序代码能够识别。
如下面这段代码:
__attribute__((weakref)) void foo();
int main()
{
foo();
}
当将其编译成一个可执行文件,GCC并不会报链接错误。但是当我们运行这个可执行文件时,会发送错误。因为foo函数的地址是0,找不到。对其进行改进:
__attribute__((weakref)) void foo();
int main
{
if(foo) foo();
}
弱符号和弱引用对库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号覆盖,从而使得程序可以使用自定义版本的库函数。或者程序将某些功能模块的引用定义为弱引用,当我们将扩展模块与程序链接在一起时,功能模块就可以正常使用。
3.6 调试信息
如果我们在GCC编译时加上-g参数,编译器就会在产生的目标文件里面加上调试的信息。
这些段中保存的就是调试信息。现在的ELF文件采用一个叫DWARF的标准的调试信息格式。
可以使用strip命令去掉ELF文件中的调试信息。