首页
学习
活动
专区
工具
TVP
发布

从kernel到Android

符号管理

本篇介绍

本篇介绍的是链接过程中的符号管理,符号管理对于链接十分重要而且基础,如果不能解决模块间符号的解析问题,那么将无法支持动态库等。本篇主要介绍下链接过程中的符号管理相关内容。

绑定和符号解析

链接器可以处理各种符号,包括一个模块对其他模块中符号的引用。每个输入模块包含一个符号表,这些符号可以分类如下:

定义在本模块的全局符号,可能被本模块引用,也可能被其他模块引用

在本模块引用,在其他模块定义的全局符号

段名字,这儿的段名字应该就是目标文件中的段名称,比如text,data等

非全局符号,也就是局部符号,用debugger和crash dump分析代码时会用到。这些符号在链接的过程中不需要,因此它们可以放到目标文件的单独一个符号表中,或者甚至放到单独的调试信息文件中

行号信息, 记录源码文件和目标文件的对应关系

链接器读取目标文件中的符号表,并解析出其中的有用内容,然后生成用来指导链接过程的符号表。不同的输出文件格式,符号表的生成策略也不一样。对于ELF文件,动态库符号由于在动态链接时会用,所以要生成到一个单独的符号表里面,对于调试和重新链接需要的符号可以放到另外一个符号表中,这样可以减小目标文件大小,加快动态链接速度。

符号表格式

链接器的符号表和编译后的符号表类似,不过相比后者会更简单一些,至少链接器需要的符号种类没有编译时的符号种类复杂。在链接器中,有一个符号表会列出输入文件和lib库,也有一个符号表会记录全局符号,在解析输入文件符号引用时用,还有一个符号表会记录模块间的调试信息。

对于链接器,一个符号表是以数组形式记录的,用hash函数值关联数组索引,数组的某个位置也是一个链表。符号表数据结构定义如下:

用符号表结构如下:

这样符号的查找流程如下,利用hash函数计算输入符号的hash值,然后用该值和符号表长度求余得到链表项索引,然后从该索引对应的符号表链表开始遍历查找。

这儿有个问题,对于旧的链接器来说,需要对变量的名字长度做限制,太长会造成符号表太大。现代链接器就放宽了对名字的长度限制,主要得益于名字修饰特性,后面会详细介绍。还有一点,在遍历符号表中某个链表的时候,如果名字长,每次比较都是最后几个字符不一样,会降低查找效率,因此添加了fullhash这一个字段,基于符号名字做hash,查找符号表的时候就可以先比较fullhash字段,fullhash一样的话再去比较字符串。

模块表

链接器需要跟踪链接时输入的每个模块,包括显式链接和从库中解析的模块。下面是GNU链接器生成a.out文件时需要的简化模块表:

从上面可以看出,模块表包含了符号表和字符串表,重定向表,text,data,bss段偏移。如果输入文件是一个库,那么每个库成员也会包含自己的模块表入口。

在第一次pass处理时,链接器会从每个输入文件中读取符号表,对于符号名字字符串在单独一个表的场景,链接器也会读取它到内存,并将符号表中的字符串偏移修改成指向内存中对应字符串的指针。

全局符号表

链接器会为所有的输入文件维护一个全局符号表。每次链接器读取一个文件后,会将该文件中所有的全局符号加入到符号表中,并就定义和引用信息。这样当第一次pass结束后,最简单的情况是每个全局符号应该正好有一处定义,并有零次或多次引用,对于UNIX目标文件,有种特利是会有零次定义的情况。看下全局符号表结构glosym:

当文件中的符号被添加到全局符号表中后,链接器需要将文件中的符号和全局符号表项对应起来。在单个目标文件中,重定位项是通过模块自己符号表的索引确定符号的,对于外部的引用,西药做一个修正。这儿是借助一个指针数组解决的。比如模块A的索引15对应符号fruit,模块B的索引14对应符号fruit,fruit在全局符号表中,这时候模块A和模块B分别添加一个数组指针,模块A的15指向的指针数组项和模块B的14指向的指针数组项都是全局符号表中fruit的地址。

在链接的第二次pass中,链接器需要解析符号引用,重定位项标识了程序对符号的引用。在最简单的场景中,链接器只需要将目标文件中的符号引用用绝对地址取代即可。真实场景会比较复杂,其中一个原因是有多种符号引用方式,另外一个原因是输出的目标文件也需要时可重定位的。

很多系统中,链接器定义了一些自己的特殊符号。比如UNIX 链接器中定义的etext,edata,end,分别表示text,data,bss段的结束地址。系统调用sbrk()将end作为运行时堆的开始地址,这儿可以看出,堆地址和data,bss地址是连续的。对于有构造器,析构器的程序,很多链接器会用类似于 的符号创建指向函数的函数指针表,这样就可以在程序开始和结束的时候调用相应的函数。

命名修饰

目标文件符号表里面的命名和源代码中的命名往往不一样,有三个原因,避免名字冲突,名字重载,类型检查时的问题。将源文件中的名字转成目标文件中名字的过程叫做命名修饰。 接下来看下C,Fortran,C++中的命名修饰。

C和Fortran

在最开始的时候是没有命名修饰的,可能是1970年以前,后来发现有个问题,当程序中命名和编译器和库函数中保留的命名一样时,就会因为命名冲突出现问题。在UNIX系统上,C程序会在名称前加作为命名修饰,比如main会被修饰成,Fortran会在名称前后添加作为命名修饰,比如calc会变成。可是这样也有个问题,会增大命名的长度限制,前面介绍过,链接器为了保证哈希表空间,会对命名长度做限制,这儿的修饰符也会占用长度,因此可用的命名长度就会变短。

还有另外一种方法,汇编器和链接器允许使用C和C++中禁止的字符,比如, $,这样只需要将运行时库命名中加入这些字符就可以避免命名冲突。不过后来UNIX被重写了,运行库也被扩展了,汇编器代码和编译器代码也被重写了,所以就没命名冲突问题了,现在的C代码已经前面不会加下划线了。

C++命名修饰

名称修饰可以起到标识作用域和类型信息的作用,这在C++,Ada中有用到。C++中同样名字的函数,变量可以在不同的作用域中存在,C++ 中也有全局变量,类静态变量,C++ 也支持函数重载,甚至操作符重载。C++ 最初被设计成cfront,也就是可以变成c代码,这样就可以使用现有的链接器了。这儿介绍下C++的命名修饰规则,C++类外的变量名字不做修饰,类外的函数名字需要加标识和入参类型标识,比如会变成, 类名是当成类型来处理的,在类名前会加类名的长度,比如Pair会变成4Pair。类里面也可以包含多层的内部类,比如First::Second::Third,这时候就会用字符Q作为内部类标识,后面跟嵌套层数,表示成Q35First6Second5Third。这样就会表示成. 类成员函数的修饰策略是,函数名字后加2个下划线,再加F和入参类型,比如会变成, 操作符也有对应的修饰字符串,比如。

这儿也有一些简化,对于函数入参类型一样的场景中,Tn表示和第n个入参一样,Nnm表示后面接n个参数,君和第m个参数一样,这样. 下面是一个类型和修饰字符对应表:

UNIX中可以使用c++filt 命令迅速反向修饰c++符号

链接时类型检查

链接时进行类型检查指的是每个全局符号都会有一个字符串表示该符号的类型或返回值,这样链接器在解析符号的时候,就会比较定义和引用地方的类型字符串,如果不匹配就会报错。在C++ 中类型检查也有用,因为命名修饰覆盖不了所有类型,比如返回值类型等。

弱引用

引用有强弱之分,对于强引用,如果找不到定义的地方就会报错,对于弱引用,找不到定义的地方也不会报错。一般链接器会把没有定义的符号,也就是弱符号的值初始化为零。

维护调试信息

现在的编译器都支持源代码调试,比如设置断点,单步调试等。编译器是通过在目标文件中添加源文件行号和目标文件的对应关系实现的。UNIX编译器中有两种不同的调试信息格式,一种是stab,主要在a.out,COFF,non-system V ELF 文件,一种是DWARF,用于system V ELF文件。

行号信息

行号信息可以让用户基于行号设置断点,单步调试,看调用栈。编译器在编译代码的时候,会在目标文件中以数组形式插入行号和代码开始的对应关系,如果目标文件中某行代码对应源文件中的两行代码,则选择行数较少的行作为对应代码位置。行号信息也需要用文件名字界定,这个只需要生成一个文件列表,并在行号前添加一个文件索引即可。

不过行号信息会收到编译优化的影响,导致目标文件的代码序列和源文件的代码序列对应不上。一些调试信息格式,比如DWARF,就会将每字节目标文件中的代码和行号信息对应起来。这用空间换取了精确度。

符号和变量信息

编译器也需要提供名字,类型,每个变量的位置信息。调试符号信息比命名修饰更加复杂,因为调试信息需要包含符号的结构信息,这样调试器才能正确的格式化变量的每个成员。符号信息本质上是一棵树,最上面是全局的变量,类型,函数,接下来是每个类型的结构成员,函数内的变量,在函数里,还包含代码块的起始位置信息。

符号信息中有一个技巧,对于大多数体系结构,函数调用是通过栈实现的,函数的多次调用就会涉及到不断地压栈,这时候就有栈指针随之变化,函数内的变量偏移也是相对于栈指针来的,对于没有局部栈变量的函数,一般的优化方法就是不调整栈指针,这时候调试器就需要直到如何解析到正确的条用栈。

实际问题

关于调试信息,有一些实际问题,主要是两个:

编译器会记录所有头文件的信息作为调试信息,如果一个相同的头文件被n个源文件包含,那么就会生成n份同样的信息,这样会导致待调试信息占用空间变大,通过去掉重复的调试可以可以提升链接和调试速度,也可以节省空间。

调试信息的存在会导致目标文件变大,对于产品,这些信息是不需要的,因此可以将发货产品中的调试信息去掉,UNIX上使用strip命令,研发保留带符号的目标文件,这样发货产品发生问题,就可以借助带符号的目标文件来定位分析。

本篇总结

本片介绍下链接过程中的符号管理,包括符号表格式,全局符号表,为了解决符号表中命名冲突出现的命名修饰,还有为了基于行号支持断点,单步的调试信息记录等。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20190205G0O3GI00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券