【计算机系统基础】符号表、符号解析(详解)

您所在的位置:网站首页 如何查看符号的代码信息内容 【计算机系统基础】符号表、符号解析(详解)

【计算机系统基础】符号表、符号解析(详解)

2024-07-16 06:42| 来源: 网络整理| 查看: 265

一、符号、符号表

符号:通俗地说,就是前面跟着类型(如int/void等)的函数名,或变量名。

          注意:这里的变量必须是除了非静态局部变量之外的其它变量。

所有符号载入史册——符号表。

分类:

①Global symbols(模块内部定义的全局符号,又称全局符号) – 由模块m定义并能被其他模块引用的全局符号。

例如,非static函数和非static的全局变量(指不带static的全局变量) 如,main.c 中的全局变量名buf

②External symbols(外部定义的全局符号,又称外部符号) – 由其他模块定义并被模块m引用的全局符号(标志是extern,extern用来修饰全局变量,即声明在“最外层”)(要体现引用,否则不会进入符号表) 如,main.c 中的函数名swap是一个外部符号。

i.它是一个声明,但是也仅仅是告诉链接器,这个swap实际上来自外部。

ii.还必须在模块m中引用,该符号才能出现在符号表里。对swap的引用,则体现在main函数中的swap();中。

当然,考试不会给出只定义/声明,但后面没有引用的例子。故了解即可。

【实际上,main里不写extern都可以,它是个弱符号,后续链接会自动把在main里写到的void swap的真正定义认为在swap.o中。只不过做题时,我们把extern int a这样的a称作外部符号(在外面有定义),int a这样的a称作全局符号(其在.bss)】

 Local symbols(本模块的局部符号,又称本地符号) – 仅由模块m定义并能被本模块引用的本地符号。例如,在模块m中定义的带static的函数和变量(无论定义变量的位置,是全局还是在函数中,只要变量前面有static,都算是(本地)符号!) 如,swap.c 中的static变量名bufp1

【注】不关心局部变量(当然,也不叫符号)。

如果函数的声明中带有关键字extern,则暗示这个函数可能在别的模块里定义,在此处只是一个声明而非定义(如下区分)。extern...可简单理解为是引用其他模块定义过的东西,其变量(函数)名属于外部符号。它在当前文件的符号表中,在真正定义模块的.data或.bss节中。

int a; // 定义一个变量, 不初始化 int b = 10; // 定义一个变量, 同时进行初始化 extern int c; // 声明一个外部extern的int型变量a void swap(); //函数的声明【而非定义】 void swap(){ //函数的定义 ...... } 【做题总结】找符号,就找前面带类型的&&(声明在最外层的 || 前面带extern的 || 前面带static的)

如图:红为内部(全局)符号,蓝为外部(全局)符号,绿为本地符号。

【符号表】

如图:符号的偏移量或虚拟地址一目了然!

st_size可能是函数(代码、指令占的字节数)的字节数,也可能是变量所占的字节数,视情况而定。

st_info比较重要,包含的信息很多!

【例如】

main.o

 本图中,swap是外部函数,它的信息许多未知

在swap.o中

同样,buf的相关信息,在swap.o中也是未知信息。而且,bufp1是未初始化变量,放在bss中(符号表中描述bufp1满足4字节对齐,占4个字节,可以理解为.o文件链接后映射到存储器(实则为主存)中,bufp1是4字节对齐,占4个字节)。

二、符号解析【重点!!!】

1、符号定义的实质:为函数名时,指代码所在区;为变量名时,指所占的静态数据区。

2、全局符号的解析比较复杂,内部符号解析较简单。所有定义符号的值,是其目标所在的首地址。

3、强符号/弱符号

接下来所说的全局变量/函数名包括:外部符号(即外部定义的全局符号)和全局符号(即模块内部定义的全局符号)

已初始化的全局变量名、全局函数名是强符号

未初始化的全局变量名和全局函数名【即光声明不写具体指令的函数(如前面的extern void swap();中的swap)】,是弱符号

【易错】本地(static)符号不参与强弱讨论。函数的“引用”不算强/弱符号。非静态局部变量也谈不上强/弱符号。

【练习】

p1中的var是强符号,p2中的var是弱符号。

【练习】指出强符号和弱符号

红框里面是强符号,蓝框里面是弱符号。

【做题总结】找强/弱符号,就找带类型的&&(声明在最外层的||前面带extern的)[注意:不包括前面带static的];然后看是否初始化 值 或 代码!

解析有如下规则:

Rule 1: 强符号不能多次定义

– 强符号只能被定义一次,否则链接错误

Rule 2: 若一个符号被定义为一次强符号和多次弱符号,则按强定义为准

– 对弱符号的引用被解析为其强定义符号

Rule 3: 若有多个弱符号定义,则任选其中一个(一般选第一个)

– 使用命令 gcc –fno-common链接时,会告诉链接器在遇 到多个弱定义的全局符号时输出一条警告信息。

【做题总结】找链接后符号的真正定义处,若是本地符号,则定义就在本地;若是全局符号(内部/外部),则根据上述规则找真正定义处。

如本题:红色是强符号,蓝色是弱符号。

y一次强定义,一次弱定义 z两次弱定义 p1一次强定义,一次弱定义 main一次强定义

没有两次强定义,因此:链接不会报错

但是,y和z的输出结果到底是谁?

p1.c中的p1函数,会将左端的y符号赋值为200(虽然y是用的main.c的强定义)

同理,z最后也会因为经过p1()函数,而变成2000.

【典例】

d最终输出结果如何?

单纯编译p1.c的时候,会认为把1.0给double d(汇编也是浮点指令)。但最终链接后,1.0作为浮点数,要放到int d中,因为main.c中d是强符号。具体如下:

d(1.0)的double浮点数为3FF0 0000 0000 0000H,它会把原来int d空间里的数据全部冲掉,换成0000 0000,把原来int x里的数据全部冲掉,变成3FF0 0000,为一个很大的数。如果在main.c中还定义了一个如short f的在.bss的变量,则无需考虑它。.data和.bss虽然说是连续的两个空间,且.data在低地址,.bss在高地址。但由于.bss的对齐原因,两者之间有很大的空隙,不会冲刷掉.bss空间的内容。

因此,链接可以通过是真的,但是程序结果出错了。

【规律】待用以赋值的数到底是什么类型的机器数,由本.c来决定(如double),因为这是在汇编及之前就完成的。而真正赋值给的对象,是强符号(同一名称的符号若都是弱符号,则任选其一)。

注意区分:

整个存储器镜像(右图)从低地址到高地址,是从下往上的(和栈帧一样)。因此,我们在画d和x的内容的时候,从下往上也应该是从低地址到高地址。对于.data节而言,先声明的变量占据低地址空间。

【经验教训】别用全局变量,用的时候也要赋初值!多用本地变量(static)。

三、库

1、避免所有函数在一个源文件中,也不要一个源文件只包括一个函数。要雨露均沾。

2、静态库 (.a archive files)

– 将所有相关的目标模块(.o)打包为一个单独的库文件(.a)【包含了许多.o文件】,称为静态库文件 ,也称存档文件(archive)

– 在构建可执行文件时只需指定库文件名,链接器会自动到库中寻找那些应用程序用到的目标模块,并只把用 到的模块从库中拷贝出来

– 在gcc命令行中无需明显指定C标准库libc.a(默认库)

如果要修改一个.c文件,只需要将对应新生成的.o文件覆盖到.a中,非常方便。

例子:

ar是一个归档程序。

$ ar rcs libc.a atoi.o printf.o … random.o

这样,就可以生成名为libc.a的静态库

然后,如$ gcc –static –o myproc main.o ./mylib.a,则就可以使用(假设自定义的静态库是)mylib.a了!

【注意】在链接的时候,无需特别地指出libc.a的标准库,链接器可以自动查询。

3、若干集合

E 将被合并到一起以组成可执行文件的所有目标文件集合(可执行文件+静态库文件)

U 未解析符号(未不对应定义符号关联的的引用符号)的集合 (符号)

比如说:在一个.o文件用了max(a,b),但是却没有这个max符号,这个max现在就称作未解析符号。

D 当前已被加入到E的所有目标文件中定义符号的集合(定义符号)

开始E、U、D为空,符号解析过程如下:(扫描文件的顺序:按gcc指令的顺序扫描)

①命令行中文件按照顺序出现,假如现在是f文件,链接器看它是什么文件:

是可重定位目标文件,就将f放到E中,并将f中未解析符号放到U,定义符号放到D。

是静态库文件:链接器尝试把U中所有未解析符号与f中各个目标模块中的定义的符号匹配。如果f中的某个m模块定义了U中的未解析符号x,就将:m放入E,x从U移到D,直到U和D不再变化。又假设f中的模块n,里面的定义符号都没在链接中用到,那么n就被扔了!(随扫随丢)库文件里也有可能存在那种未解析符号,那么就把静态库里的未解析符号也放到U里去。

如图例,处理main.o的时候,先把这个模块放到E中。发现d是定义符号,那么d就加入D,而main.c第8行的d已经被定义了,所以d不会被放到U里面。

②若往D中加入了一个已经存在的符号(双重定义),或扫描完所有文件时U非空,则连接器报错并停止。否则,链接器将生成.o文件,最终U中一定为空,D中符号唯一。

③最终还会自动检索默认库,也是匹配符号的过程。它不需要在 gcc -static 后明显指出。

【注意】链接器对外部引用的解析

①顺序扫描.o和.a,一般把静态库放到后面。如果静态库之间用相互引用关系,则必须按照引用关系在命令行中排列静态库文件,使得:对每个静态库目标模块中的外部引用(定义)的符号,包含其定义的静态库文件排在其后面。【每个静态库中,可能有外引符号,因此,为方便理解:可以把静态库中用到的模块,当成.o文件,理解成库内模块中的未定义符号也放到U。】

我们的目标是:每一个找不到家的孩子找到家,每一个未关联符号U都找到它的定义!

②最终U中一定是空集,D每个符号都是唯一的!

假设调用关系如下:  func.o → libx.a 和 liby.a 中的函数  libx.a → libz.a 中的函数  libx.a 和 liby.a 之间、liby.a 和 libz.a 相互独立  则以下几个命令行都是可行的:(共三种)

- gcc -static -o myfunc func.o (剩下三个库保证x在z的前面即可)

func.o → libx.a 和 liby.a 中的函数 libx.a → liby.a 同时 liby.a → libx.a

则下列都可以:(共两种)

– gcc -static –o myfunc func.o libx.a liby.a libx.a – gcc -static –o myfunc func.o liby.a libx.a liby.a

【例题】

(1)显然是$ gcc –static –o myproc p.o libx.a liby.a

(2)y和x库相互解析,故为$gcc -static -o myproc p.o libx.a liby.a (注意:y里有的符号需要x解析,故继续)  libx.a

(3)先顺着写:

$gcc -static -o myproc p.o libx.a liby.a libz.a

发现y也是调用x,x再调用z

那么直接写成:$gcc -static -o myproc p.o libx.a liby.a libx.a libz.a

【解读】总之就是被外引的.a一定要在外引的.a的后面!



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3