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

一、符号、符号表

符号:通俗地说,就是前面跟着类型(如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的所有目标文件中定义符号的集合(定义符号)

 

开始EUD为空,符号解析过程如下:(扫描文件的顺序:按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的后面!

 

参与评论 您还未登录,请先 登录 后发表或查看评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:游动-白 设计师:我叫白小胖 返回首页

打赏作者

STEVEN_QDU

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值