2 链接命令行_编译原理——链接过程(下)

0b77be9426270601d8aed53d2d8de0bf.png

在上一篇文章中,介绍了链接过程,本质上就是符号的解析、重定位。在上一步汇编的结果,生成一个可重定位目标文件。符号表、代码节、data节,还有重定位信息...。因为每个可重定位目标文件对应一个cpp,所以该cpp文件去调用其他cpp的函数,或者访问其他cpp的变量(非局部变量)时,汇编程序就会生成相关重定位信息。在链接过程中,就会根据这些重定位信息,去符号表找对应的符号的地址,调用的地方就会修改为符号的地址。

Chdy:编译原理——链接过程(上)​zhuanlan.zhihu.com
61271534945ab3a076ca12362c3dc69b.png

有了链接的过程,慢慢的就衍生出了静态链接、动态链接。程序中有很多重复的工作,比如:数学函数、显示等等功能。人们就想到用链接来复用这些高重复的功能。下面我们就来介绍静态链接和动态链接。

静态链接(.a.lib文件)

1.创建静态链接库

$gcc -c file1.c file2.c
$ar rcs mylib.a file1.o file2.o

2.使用静态链接库

$gcc -c main.c
$gcc -static -o output main.o ./mylib.a

3.链接过程

在静态链接过程中,链接器创建E、U、D三个集合。

  • E组成可执行文件的所有目标文件的集合;
  • U当前所有未解析的引用符号集合;
  • D当前所有定义符号的集合;

开始E、U、D为空,首先扫描main.o,把它加入E,同时把引用符号(myFunc)加入U,main加入D。接着扫描mylib.a,将U中的所有符号与mylib.a中所有目标模块依次匹配,如发现file1.o中定义了myFunc,故file1.o加入到E,myFunc从U转移到D。在file1.o中发现还有未解析的符号printf,将其加到U。file2.o中没有匹配的符号,因而它将被丢弃。printf从C标准静态库(libc.a->printf.o指令中无需明显指出)获得匹配。到最后,如果U还有符号,则出现链接错误。

这里需要特别注意的是,链接器是根据命令行给出的顺序扫描。如果,符号的引用模块在后面,符号的定义在前面,就会出现链接错误。所以,好的做法,将静态库放到命令行最后。

4.与动态库比较的缺点:

  • 因为静态库函数(如printf)被包含在每个进程的代码段中,对于并发运行上百个进程的系统,造成极大的主存资源浪费
  • 因为静态库函数被合并到可执行目标中国,磁盘放着数千个可执行文件,造成磁盘空间浪费
  • 静态库有新版本出现,须定期下载、重新编译和链接

动态链接(共享库 .so/.dll文件)

共享库包含目标模板的文件,每个模板包含有代码和数据。共享库,在磁盘和内存中只有一份备份,可以在装入时运行时动态的被加载并链接。共享库升级时,被自动加载到内存,并和程序动态链接。可分模块,并独立的用不同的编程语言进行开发,第三方开发的共享库可作为程序插件,使程序功能易于扩展。

  • 第一次加载运行时运行(load-time linking):通常由动态链接器(ld-linux.so)自动处理。标准C库(libc.so)通常按这种方式被链接
  • 在已经开始运行后进行(run-time linking):通过调用dlopen()等接口实现。通常用于分发软件包、构建高性能Web服务器等。

1.创建动态链接库

$gcc -c file1.c file2.c
$gcc -shared -fPIC -o mylib.so file1.o file2.o

2.使用动态链接库

$gcc -c main.c
$gcc -o outfile main.o ./mylib.so

9e2bc51c90a0f658f57d9b3e95be17af.png
加载时动态链接过程

a18c978b186c884d616c1b7762345499.png
运行时动态链接

3.位置无关代码(PIC)

动态链接用到一个重要概念位置无关代码。GCC选项-fPIC指示生成PIC代码。要实现动态链接,必须生成PIC代码。共享库代码被加载的位置可以是不确定的,即使共享库代码长度发生变化,也不影响调用它的程序。引入PIC的目的,无需修改程序代码即可将共享库加载到任意地址运行。以下是应用。

特别注意:以下是可执行文件的内容,是通过工具objdump逆向形成,可执行文件只包括了,汇编指令所对应的二进制数。最左边的虚拟空间地址值和最右边的汇编指令,是工具生成,便于查看分析。

5a55d1f87af4ecf02757c44f1ce219c9.png
Call指令的功能,将下一条指令所在地址入栈;并将子程序的起始地址(使用相对寻址方式)送入PC,于是CPU就去执行子程序,当子程序执行RET,RET指令的功能就是一条从栈中取出一条数据送入PC。于是执行完子程序,继续执行Call指令的下一条指令。

46fcbc29fcd20356e62b4f7da374fa78.png
模块被加载到内存的地址可以是不确定的,所以使用相对位置寻址方式

在链接过程中,可以通过重定位信息来重新定义调用其他模块函数的地址。但是,在可执行文件中,已经没有重定位信息了,要怎么链接呢?这时候,就引入了GOT(全局偏移表)来解决这个问题。GOT是一个指针数组(设置在.data节开始处,可读写),加载时动态链接器对GOT中各项进行重定位,填入所引用的地址。

2d88e0f978c70059443b50f0cc2100a7.png
Call指令,将下一条执行指令的地址0x35c压栈,并跳转到0x35c执行。popl出栈,相当于把0x35c放到ebx寄存器。再加上0x1180偏移量,就获得变量b在GOT所在的地址位置,里面的值就是变量b的地址。

49fb3cba533b1d0ed9d5c8a82f155be1.png
模块间的调用,与前面的汇编类似。只不过将movl语句换成了call语句。

637879a883c92e27a85aa7c8545dda6e.png

call指令,跳转到804834cPLT[1]的首地址,接着跳到GOT[3](0x8049590的值所对应的地址0x8048352),即跳转到下一条指令。接着将常量0x0(标识ext函数)压栈。执行下一条指令,跳转到0x804833cPLT[0]。将0x8049588GOT[1]里面的内容压栈(4000a9f8动态链接器的标识信息)。执行下一条指令跳转到0x804958cGOT[2]里面的内容所对应的地址。GOT[2]为动态链接器延迟绑定代码的入口地址。开始执行动态链接,根据GOT[1]和ID确定ext地址并填入GOT[3],并转ext执行。以后调用ext,就不需要重复跳转了,因为GOT[3]已经修改为ext的地址了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值