7-链接和加载选讲

本文详细探讨了Linux下的静态链接与动态链接的区别,动态链接如何节省空间,以及动态链接的实现原理。通过实例分析了动态链接过程中地址的解析、动态链接器的作用和延迟绑定的概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

静态链接

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
a.c

int foo(int a,int b){
	return a+b;
}

b.c

int x = 100,y = 200;

main.c

#include <stdio.h>
extern int x,y;
int foo(int a,int b);
int main(){
	printf("%d + %d = %d\n",x, y, foo(x, y));
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
readelf -d main.o(图1)
在这里插入图片描述

readelf -a main.o | less(图2)
在这里插入图片描述
可以看到y-4所在的地址是0a,对应于图1中0a的地址是保留着4字节的0的,这4个字节是为y的地址留的,之后会借助rela.text中的信息在链接的时候会填入
为什么是y-4而不是y呢?
解答:因为

   8:	8b 35 00 00 00 00    	mov    0x0(%rip),%esi        # e <main+0xe>

是一个位置无关代码,而rip总是指向下一条指令的地址,即0xe,而0xe-0xa=0x4,所以需要在0xa的位置填入y的地址-4才可以。归纳就可知道,-4的4就表示了需要填入的地址到下一个指令的距离。


这里,我们想知道执行a.out时的第一条指令在哪里
在这里插入图片描述
在这里插入图片描述
看main时可以发现所有待填的内容都已经填好了。
在这里插入图片描述
可以看到入口点地址为0x401c00,操作系统把这个可执行文件加载后,从这个入口地址开始执行。
回去查a.txt,得到:这个地址处是_start的代码
在这里插入图片描述


怎么从第一条指令开始调试这个程序?——gdb其实提供了这个功能
在这里插入图片描述
gdb ./a.out
starti ->
在这里插入图片描述
layout asm ->
在这里插入图片描述
_start之后又会进入__libc_start_main
在这里插入图片描述
之后经过很长的初始化以后才会进入到main。


在这里插入图片描述

在这里插入图片描述
ld链接时,说没有定义printf,这是因为我们没有链接libc.so。
我们把main.c的printf行注释掉。
在这里插入图片描述
这样就只剩下一个警告了。
在这里插入图片描述
这时候执行a.out会有一个段错误,什么原因呢,让我们用gdb调试吧
gdb ./a.out
starti
layout asm 可以看到一开始就执行main
在这里插入图片描述
经过两次si,准备执行ret的时候,查找rsp中的值为1。
在这里插入图片描述
再si执行ret指令,就触发了段错误,这是因为无法访问0x1的地址
在这里插入图片描述
所以直接用ld链接起来是不行的,并且我们给了一个小例子(如下),直接用三行汇编代码做一个系统调用,这样用ld是可以的。所以后面也会探寻gcc除了ld以外还做了哪些事(显然的有链接库函数)。


最小的可执行文件?
在这里插入图片描述

./a.out发现可以正常执行 echo $?看到返回值确实为99

a.S
在这里插入图片描述
可以用gdb去调试刚才这段代码
gdb ./a.out | starti | layout asm| si si si ->
在这里插入图片描述
程序就以0143,即99退出了。

所以整个计算机系统没有任何神秘的东西,所有的Linux世界,都是用这样的方式构造起来的。


在这里插入图片描述

man gcc中可以找到下图
在这里插入图片描述
-Wl,option 可以给gcc在调用链接器时传入额外的选项
-Wl,-verbose可以看到gcc的很多细节
在这里插入图片描述
等等等等。

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>

int x=100,y=200;
int foo(int a,int b){
        return a+b;
}

__attribute__((constructor)) void a(){//C标准库以外的,在main函数执行前就执行
        puts("Hello");
}

__attribute__((destructor)) void b(){//C标准库以外的,在main函数执行后执行
        puts("World");
}

int main(){
        atexit(b);//C标准库支持的,在程序退出前执行函数b
        printf("%d + %d = %d\n",x,y,foo(x,y));
}

在这里插入图片描述

动态链接与加载

静态链接 a.out文件的大小有87w+
在这里插入图片描述
而动态链接的a.out只有16k+
在这里插入图片描述
在这里插入图片描述
最早的时候,由于没有很多的程序,完全可以用静态链接。但随着bash中程序的飞速增加,共享的东西很多,静态链接会占用非常大的空间,动态链接是最好的选择。

在这里插入图片描述
libc代码,不仅在文件里,还是在物理内存里,都只有唯一的一份副本,而我们的一个a.out的进程启动以后,a.out有它自己的代码和数据,a.out还需要liibc,甚至还需要一些别的库,这个时候,我们的物理内存里只有一份libc代码,通过虚拟内存映射到这个同一个物理内存,这个libc加载到地址空间的哪里其实是完全没有关系的,加载到这里也可以,加载到那里也可以,只要按页面对齐就行。

默认会采用PIC,即所有的位置都是先用0(rip)占位,链接时再确定位置。如果想要不采用PIC,则用-no-pie编译。

在这里插入图片描述
ldd可以帮忙看共享库的映射
在这里插入图片描述
可以看到,将libc.so.6映射到0x7f58…ld-linux-x86-64.so.2映射到0x7fb5,且每次都是不一样的,说明加载时虚拟地址是不确定的。
在这里插入图片描述
/lib64/ld-linux-x86-64.so.2 ./a.out./a.out所做的事完全一样,通过strace看他们两者就是完全一样的。说明运行./a.out不是简单的执行一下,而是需要请操作系统运行/lib64/ld-linux-x86-64.so.2这个解释器,a.out就是这个解释器的参数,这个解释器加载好a.outa.out依赖的共享库,然后才正式运行。
在这里插入图片描述
可以通过readelf -a a.out看到里面写明了以/lib64/ld-linux-x86-64.so.2作为解释器。
在这里插入图片描述
直接vim a.out也可以看到这个
在这里插入图片描述
所以计算机科学里面没有任何的魔法。


动态链接的实现

在这里插入图片描述
如果让你来实现动态链接,你会怎么实现?
首先,比较容易想到的是,在每一次程序加载的时候重新做一次静态链接就好了。
以printf为例,call的时候一开始也是填着call $0x0(rip),然后将共享库加载到内存后做一次静态链接,修改那些0的地方,这样做确实是可以的。
但是有一些缺陷,如:
①如果在多个进程都运行同一个a.out,在运行时每个a.out都需要重新做一次静态链接,把0的地方修改了。
②如果一个程序很长很长,用到了几万个的库的函数调用,但是实际上只执行一点点代码就结束了,这样静态链接庞大的量会很没有效率

那么比较好的办法是什么呢?
在这里插入图片描述
很简单,查表即可。我们的需求很明确,就是call printf,但是我们现在不知道printf在哪,直接查表就好了,把printf这个函数所在的地址存放在一个给定的地址中,然后只需要修改printf的地址即可,不需要对中间的每个printf去修改,只需要在遇到printf时直接查这个表就可以了。

在这里插入图片描述
objdump -d main.o ->
在这里插入图片描述
objdump -d a.out
在这里插入图片描述
看10e4行中,是callq *…,10ea+2efe=3fe8,就是后面注释所写的地址,printf所在的地址存放在这个地址中的。


这边讲一下豪华版的查表——PLT(默认选项)
在这里插入图片描述
在这里插入图片描述
objdump -d a.out | less ->
在这里插入图片描述
对printf的调用直接变成跳转到一个确定的地址(这里是0x1080),这个地址是一个新的函数
在这里插入图片描述
这里汇编看不太懂,老师说什么老版本的gcc不是这样的…
反正就是该函数实际上做了两件事:

1)解析出printf的地址并将值填入这个表项中.
2)跳转执行真正的printf函数.
在第一次调用printf的时候,在3fc8的表项中还没有填入printf的地址,只有执行到这个__printf_chk@plt函数时,才会填入,称为延迟绑定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值