链接器和装载器--符号管理
符号管理是链接器的关键功能,主要用于实现不同模块之间符号的关联关系管理。如果不能解决模块之间的符号引用问题,那么链接器的其他功能也就没有什么太大的用处了。
5.1 符号名绑定的解析
链接器处理各种类型的符号,还要处理各模块之间符号的相互引用,每个输入模块都有一张符号表,其中的符号包括。
1 当前模块中定义的全局符号
2 在本模块中引用但未被定义的全局符号
3 段名称,通常用于调试或崩溃转储crash dump 分析。链接过程中几乎不会被用到这些符号,但是有时候它们会和全局符号混在一起,在这种情况下,连接器可以识别并跳过他们,或者为他们在文件中创建一个单独的表,或者为他们的创建一个单独的调试信息文件。
4 行号信息,用于调试过程中建立目标代码与源代码之间的对应关系。
链接器读入输入文件中所有的符号表,并提取出有用的信息,也就是链接过程中的重要的信息,然后将他们汇集在一起建立一个面向链接过程的符号表,最后,链接器会将部分或全部的符号表信息放置到输出文件中,具体存储哪些信息是由输出文件的格式要求决定的。
某些格式会在一个文件中存在多张符号表,例如ELF共享库会有一个动态链接所需要的信息的符号表,以及一个更加详细的符号表用以支持调试和重链接。这种设计思路其实非常有效,动态链接过程所用到的符号,通常仅仅占整个文件全部符号的很少一部分,因此为他们创建一个独立的表可以加快动态链接的速度,也就是运行的速度,毕竟相比运行而言,调试或重链接一个库的机会还是很少的。
5.2 符号表的格式
链接器使用的符号表与编译器使用的相近,由于链接器用到的符号一般没有编译器的那么复杂,所以符号表通常也更简单一些。在链接器中会用到多张符号表,第一张符号表用于记录输入文件和库模块的文件信息,第二张符号表用于记录全局符号,也就是链接器需要在所有输入文件中进行查表,定位和解析的符号,第三个表用于记录模块内部的调试符号,尽管少数情况下链接器也会调试符号建立完整的符号表,但是通常只是将输入的调试符号直接传递到输出文件。
在链接器内部,符号表通常以数组形式来保存,每个符号的信息是一个表项,并通过一个哈希函数来定位表项,也可以是由指针组成的数组,用指针指向真正的符号信息项,同样使用哈希函数进行定位,当哈希值冲突时,所有冲突的项以链表形式依次串接在后面如图5-1.当需要在表中定位一个符号时,链接器根据符号名计算哈希值,将该值用桶的树木取模以定位到对应的桶,也就是说,当b为哈希值时,选取的桶时,然后遍历其中的符号链表找到所要的符号。
由哈希检索的符号表 表中每一项是一个指针。
符号 符号 符号
符号 符号
符号
struct sym* symbash()
struct sym {
struct sym *next;
int fullhash;
char *symname;
};
图5.1 符号表,符号可以直接组织成为一张表,也可以形成一张链表,节点的形式串接在表中,符号表通常使用哈希的方法进行检索。
传统的链接器仅能支持较短的符号名称,例如IBM主机系统的符号为8个符号,早期多数DEC系统上使用的UNIX系统的符号为6个符号,甚至出现过一些小型计算机的符号仅有2个字符,现代链接器支持的符号名称要长的多,一方面是由于开发人员倾向于使用更长的名称语言中虽然一直支持长符号名,但是要求使用符号名前8个符号就能够区分所有的符号,这同样会给开发人员造成困扰,另一方面是因为编译器会将符号名称进一步加长,把一些类型信息通过编码加入符号的名称中。
早期的链接器由于名称长度有限,在查找哈希链表时会对每一个符号名称进行自符串比较,直到找到匹配项或便利完毕。现在的程序经常会使用很多长符号名,仅在最后几个字符有区别,前面的符号都是相同的,这使得字符串比较的开销变大。一个简单的解决办法是将符号名的哈希值也保存在符号表中,并且只在哈希值相同的时候,才进行字符串比较。在符号解析的过程中,如果出现了。一个哈希表中无法找到符号,链接器可能会为它创建一个新的符号并加入相应的链中,可能会报错,具体的处理方式由符号解析的上下文决定