本文最后更新于:2020年6月29日 晚上
* 好久都没更这个了。。。请叫我拖更大魔王。。。→_→ *
* 这一篇看下来,对符号的理解更深了。。。 *
在链接过程中,目标文件之间相互拼合实际上是目标文件之间对函数和变量的地址的引用。
在链接中,将函数和变量称为符号(Symbol),函数名和变量名为符号名(Symbol Name)。
符号表(Symbol Table)
每一个目标文件都有一个符号表(Symbol Table),表中记录目标文件中所用到的所有符号。每个定义的符号有一个对应的符号值(Symbol Value),对函数和变量来说,符号值就是其地址。
符号表中的所有符号分类:定义在本目标文件的全局变量,可以被其他目标文件引用
在本目标文件中引用的全局符号,没有定义在本目标文件,称为外部符号(External Symbol)
段名,由编译器产生,其值为该段的起始地址
局部符号,其只在编译单元内部可见
行号信息,可选
ELF符号表结构
ELf文件的符号表是文件中的”.symtab“段。其结构是一个Elf32_Sym结构的数组,每一个元素对应一个符号。这个数组下标为0的元素为无效的“未定义”符号。typedef struct { Elf32_Word st_name;//符号名。即该符号名在字符串表中的下标。 Elf32_Addr st_value;//符号相对应的值。不同符号对应的值不一样。 Elf32_Word st_size;//符号大小。包含数据的符号,该值为该数据类型大小。若该值为0,表示该符号大小为0或未知。 unsigned char st_info;//符号类型和绑定信息。低4位表示符号类型(Symbol Type),高28位表示符号绑定信息(Symbol Binding)。 unsigned char st_other;//目前为0,没用 //符号所在的段。如果符号定义在本目标文件中,其表示符号所在段在段表中的下标。若为0,表示在本目标文件中被引用,定义在其他目标文件中。 Elf32_Half at_shndx; }Elf32_Sym;
符号值(st_value)分类:
- 在目标文件中,如果符号定义不是“COMMON块”类型,则符号值表示该符号在段中的偏移。
- 在目标文件中,如果符号是“COMMON块”类型,则符号值表示该符号的对其属性。
- 在可执行文件中,符号值表示符号的虚拟地址。
- 符号修饰与函数签名
UNIX下的C语言规定,C语言代码源文件中的所有全局变量和函数经过编译后,相对应的符号名前加上下划线。
eg.Windows下的Visual C++编译器,C语言函数“foo”,编译后的符号名是“_foo”。Linux下的GCC编译器默认情况已经去掉了此方式。
C++中增加了名称空间(Namespace)解决多模块的符号冲突问题。
符号修饰(Name Decoration) /符号改编(Name Mangling)
C++中允许多个不同的参数类型的函数拥有相同的名字,即函数重载。
C++在语言级别支持名称空间,即允许在不同的名称空间中有多个同样名字的符号。函数签名(Function Signature)
其包含了一个函数的信息,包括函数名、参数类型、所在的类和名称空间及其他信息。
函数签名用于识别不同的函数,函数的名字是函数签名的一部分。
在编译器及链接器处理符号时,它们使用某种名称修饰方法,使的每个函数签名对应一个修饰后名称(Decorated Name)。即目标文件中的符号名是函数和变量的修饰后名称。
- 名称修饰机制也用在C++中的全局变量和静态变量。变量的类型并没有被加入到修饰后名称中。
extern “C”
C++为了与C兼容,在符号管理上,有一个用来声明或定义一个C的符号的“extern “C””关键字。//大括号内部的代码当作C语言代码处理,C++的名称修饰机制将不起作用 extern “C” { int func(int); int var; } //单独声明某个函数或变量为C语言的符号 extern "C" int func(int); extern "C" int var; //C语言函数库中的string.h中的memset()如何在.c或.cpp中正确使用 //C++中必须使用extern "C" //但C中不支持extern "C"语法 //因此利用C++中的宏”__cplusplus“,C++编译器在编译时默认定义这个宏 //使用条件宏自动判断选择哪种环境 #ifdef __cplusplus extern "C" { #endif void *memset(void *, int, size_t); #ifdef __cplusplus } #endif
强符号与弱符号
多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接时将会出现符号重复定义错误。
C/C++语言中,编译器默认函数和初始化了的全局变量为强符号(Strong Symbol),未初始化的全局变量为弱符号(Weak Symbol)。
通过GCC的”_ attribute _ ((weak))“定义任何一个强符号为弱符号。
强符号和弱符号针对定义,不是针对符号的引用。
链接器按照如下规则处理与选择被多次定义的全局符号
不允许强符号被多次定义(不同目标文件中不能有同名的强符号)。
如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。
如果一个符号在所有目标文件中都是弱符号,那么选择其中占用内存空间最大的一个。
- 强引用和若引用
链接时,如果没有找到引用符号的定义,链接器报符号未定义错误,这种被称为强引用(Strong Reference)。
如果声明为弱引用(Weak Reference),若引用的符号有定义,则链接器将该符号引用决议;若引用的符号未定义,链接器不报错,默认其为0或是一个特殊值。
在GCC中,使用”_ attribute _ ((weakref))”声明一个外部函数的引用为弱引用。
//GCC在链接时不会报错,但在运行可执行文件时,发生运行错误 //因为当要调用foo()时,其地址为0,发生非法地址访问的错误 __attribute__ ((weakref)) void foo(); int main() { foo();//改成if(foo) foo(); return 0; }
- COMMON块
弱符号机制允许同一个符号的定义存在于多个文件中。链接器本身不支持符号类型,只知道符号的名字。但当一个弱符号以不同的类型定义在多个目标文件中时,链接器如何区分?
多个符号有多个类型定义时:
两个或两个以上强符号类型不一致。(非法。符号重定义)。
有一个强符号,其他都是弱符号,出现类型不一致。
两个或两个以上弱符号类型不一致。
现在的编译器和链接器支持COMMON块(Common Block)机制。COMMON类型的链接规则是针对符号都是弱符号的情况。如果其中有一个强符号,最终符号所占空间欲强符号相同。如果有弱符号大小大于强符号,链接器报警告。
问题:为什么编译器把未初始化的全局变量标记为一个COMMON类型的变量,而不直接把它当作未初始化的局部静态变量,为其在BSS段分配空间。
答:
编译时,若目标文件中含有弱符号(比如未定义的全局变量),则该弱符号最终所占内存空间大小无法确定,因为有可能其他目标文件中该弱符号所占内存空间比本单元弱符号所占内存空间大,所以此时无法在.BSS段为该弱符号分配空间。
链接时,读取了所有目标文件,确定了任意一个弱符号的大小。这时才在最终输出文件的.BSS段中为其分配空间。
总体看来,未初始化的全局变量最终还是被放在.BSS段。
在GCC中使用”-fno-commom“或”_ attribute _((nocommom))“,使未初始化的全局变量不以COMMOM块的形式处理。即相当于一个强符号。
int global __attribute__((nocommom));
* 码字好累。。。下次再见喽。。。 *
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!