Linux静态链接和动态链接浅析

  每当谈到静态链接和动态链接的时候脑子就会犯晕,到底什么是静态链接?什么是动态链接?它俩有什么联系?在静态链接和动态链接两种情况下程序发生了什么?以及它们的区别是什么?这些问题在对一个程序员来说肯定是要了解的,在学习完<<程序员的自我修养>>一书之后决定就这个问题做一些个人的总结。下面就带大家简单来看看程序在编译过程中发生了什么?ELF文件是什么东西等等........(此文要在看完<<程序员的自我修养>>一书后才可能会看懂,如有不足之处,欢迎大家指正!)

  我们知道一个程序是一组有序且有穷的计算机指令在一定数据集上的一次运算. 那么对程序来讲最关键的两个部分就是数据和指令, 程序要运行就得知道入口地址,从入口地址运行后如果遇到函数调用,那么这个被调用的函数的地址就要被调用者知道,在调用函数的时候参数要入栈,调用结束之后函数返回,参数出栈。在程序运行过程中如果遇到变量的访问,就要知道变量的地址。如果是这些变量是程序模块内的全局变量(全局未初始化变量和局部未初始化静态变量存放在.bss段,因为未初始化全局变量被规定为弱符号,它刚开始是没有存放在.bss段,等到它被确定的时候才存放到.bss段),如果是局部非静态变量,则编译器通过mov 0x12 -0x24(%ebp)的形式将它存放在栈中。

  链接的本质就是将各个子模块拼接成一个功能完整的能独立完成一次运算的大模块,为了完成链接,就必须知道完成各个子模块功能的函数的位置(这里我们可以认为子模块其实也是有许许多多的函数构成),无论是静态链接还是动态链接最关键的一点就是符号的重定位问题,局部变量符号不存在重定位问题,所以总的来说需要重定位的符号含有以下几种:

A. 模块内部和模块外部的函数

B. 模块内部的全局变量和局部静态变量(存放在.bss段和.data段)

C.模块外部的变量(一般是全部变量非静态变量,因为无论是静态全局变量还是静态局部变量,它们的作用域都在模块内部,前者在模块内部都可以访问,后着存放在.data段或是.bss段,在定义它的函数调用结束后该变量依然有效).

针对以上的疑问我们将从汇编代码入手分析静态链接和动态链接的实质,首先开看看静态链接。

测试代码如下:

1.静态链接

  静态链接最大的特点就是所有模块组合成一个完整的可执行文件,并将这个完整的科执行文件进行运行,节约时间,速度快,因为静态链接在程序载入内存之前,各个功能模块中函数的地址都已经确定,在运行的时候无需再定位。在验证过程中,我们的测试代码如下:

#include<stdio.h>
void swap(int &a,int &b){
	int temp=a;
	a=b;
	b=temp;
}
int main(){
	int a=1;
	int b=2;
	swap(a,b);
	printf("%d,%d\n",a,b);
	return 0;
}
程序很简单,输出结果为2,1。a和b的值通过引用传递发生交换。让我们来看看可执行问文件中main函数的汇编码:

我们看到:

 80484c0:    e8 a6 ff ff ff           call   804846b <_Z4swapRiS_>

是对swap函数的调用,从swap到_Z4swapRiS_的改变是编译器符号修饰规则的问题我们不必深究。我们看到call 0x804846b是调用swap函数,那我们来看看这个0x804846b地址上的东西是什么?

我们看到在0x084846b这个位置上确实是swap这个函数的地址,它和main函数被编译器链接到了同一个ELF(Executable and Linkable Format,预知这个文件的内容建议参考《程序员的自我修养》一书)文件内。至于怎么将call 后面的地址定位成功,其实中间还有很多步骤,这里省略。

2.动态链接

  在动态链接里我们做了如下测试:

main.c文件如下:

#include<stdio.h>
extern int global;
extern void swap(int &,int &);
int main(){
	int a=1;
	int b=2;
	swap(a,b);
	printf("%d,%d\n",a,b);
	printf("the value of global is :%d\n",global);
	return 0;
}
swap.c文件如下:

int global=10;
void swap(int &a,int &b){
	int temp=a;
	a=b;
	b=temp;
}
我们通过g++ -fPIC -shared swap.c -o swap.so将swap.c编译成地址无关(position independent code)的共享文件swap.so文件用于动态链接,

让我们看看这个可执行文件1中main函数的内容:

080485eb <main>:
 80485eb:	8d 4c 24 04          	lea    0x4(%esp),%ecx
 80485ef:	83 e4 f0             	and    $0xfffffff0,%esp
 80485f2:	ff 71 fc             	pushl  -0x4(%ecx)
 80485f5:	55                   	push   %ebp
 80485f6:	89 e5                	mov    %esp,%ebp
 80485f8:	51                   	push   %ecx
 80485f9:	83 ec 14             	sub    $0x14,%esp
 80485fc:	65 a1 14 00 00 00    	mov    %gs:0x14,%eax
 8048602:	89 45 f4             	mov    %eax,-0xc(%ebp)
 8048605:	31 c0                	xor    %eax,%eax
 8048607:	c7 45 ec 01 00 00 00 	movl   $0x1,-0x14(%ebp)
 804860e:	c7 45 f0 02 00 00 00 	movl   $0x2,-0x10(%ebp)
 8048615:	83 ec 08             	sub    $0x8,%esp
 8048618:	8d 45 f0             	lea    -0x10(%ebp),%eax
 804861b:	50                   	push   %eax
 804861c:	8d 45 ec             	lea    -0x14(%ebp),%eax
 804861f:	50                   	push   %eax
 8048620:	e8 9b fe ff ff       	call   80484c0 <_Z4swapRiS_@plt>
 8048625:	83 c4 10             	add    $0x10,%esp
 8048628:	8b 55 f0             	mov    -0x10(%ebp),%edx
 804862b:	8b 45 ec             	mov    -0x14(%ebp),%eax
 804862e:	83 ec 04             	sub    $0x4,%esp
 8048631:	52                   	push   %edx
 8048632:	50                   	push   %eax
 8048633:	68 00 87 04 08       	push   $0x8048700
 8048638:	e8 63 fe ff ff       	call   80484a0 <printf@plt>
 804863d:	83 c4 10             	add    $0x10,%esp
 8048640:	a1 24 a0 04 08       	mov    0x804a024,%eax
 8048645:	83 ec 08             	sub    $0x8,%esp
 8048648:	50                   	push   %eax
 8048649:	68 07 87 04 08       	push   $0x8048707
 804864e:	e8 4d fe ff ff       	call   80484a0 <printf@plt>
 8048653:	83 c4 10             	add    $0x10,%esp
 8048656:	b8 00 00 00 00       	mov    $0x0,%eax
 804865b:	8b 4d f4             	mov    -0xc(%ebp),%ecx
 804865e:	65 33 0d 14 00 00 00 	xor    %gs:0x14,%ecx
 8048665:	74 05                	je     804866c <main+0x81>
 8048667:	e8 44 fe ff ff       	call   80484b0 <__stack_chk_fail@plt>
 804866c:	8b 4d fc             	mov    -0x4(%ebp),%ecx
 804866f:	c9                   	leave  
 8048670:	8d 61 fc             	lea    -0x4(%ecx),%esp
 8048673:	c3                   	ret    
 8048674:	66 90                	xchg   %ax,%ax
 8048676:	66 90                	xchg   %ax,%ax
 8048678:	66 90                	xchg   %ax,%ax
 804867a:	66 90                	xchg   %ax,%ax
 804867c:	66 90                	xchg   %ax,%ax
 804867e:	66 90                	xchg   %ax,%ax
并且我们在可执行文件1中并未找到swap和printf函数的具体代码内容,与之相关的是:

080484a0 <printf@plt>:
 80484a0:	ff 25 0c a0 04 08    	jmp    *0x804a00c
 80484a6:	68 00 00 00 00       	push   $0x0
 80484ab:	e9 e0 ff ff ff       	jmp    8048490 <_init+0x30>

080484c0 <_Z4swapRiS_@plt>:
 80484c0:	ff 25 14 a0 04 08    	jmp    *0x804a014
 80484c6:	68 10 00 00 00       	push   $0x10
 80484cb:	e9 c0 ff ff ff       	jmp    8048490 <_init+0x30>
在main函数中相关的三条指令是:

 8048620:	e8 9b fe ff ff       	call   80484c0 <_Z4swapRiS_@plt>
 8048638:	e8 63 fe ff ff       	call   80484a0 <printf@plt>
 804864e:	e8 4d fe ff ff       	call   80484a0 <printf@plt>
由此可以看到swap和printf其实都不在可执行文件中,也就是说它们的代码并没有转载到可执行文件的虚拟地址空间,在可执行文件中多了两个间接跳转,这个jmp是跳转到.got.plt段的相关位置。因为在动态链接中,swap和printf模块在程序执行是才装载到进程的虚拟地址空间。在程序运行的时候,swap模块被加载到内存中,然后由动态链接器将其映射到进程的虚拟地址空间,这个时候,.got.plt的相关位置被写入为swap函数的地址,就如同jmp *0x804a00c那样,0x804a00c其实是.got.plt段中存放swap函数地址的地方,×0x804a00c就可以得到映射之后函数的绝对地址.

        可以看出正式借助.got.plt段的间接跳转,才不需要在程序运行之前就将所有模块链接成一个大的完整功能模块,这样我们的swap.so文件在内存和磁盘中都只有一份,大大节省了空间;

 那么对于这个外部全局变量global的处理是怎样的呢?让我们开看看一下两条指令:

 8048640:	a1 24 a0 04 08       	mov    0x804a024,%eax
 8048648:	50                   	push   %eax   

下面我们来看看每个段的内容:

可以看到0x804a024在可执行文件1中其实是.bss段的地址保存到eax寄存器;

再看看.got.plt段的内容:

080484e0 <.plt.got>:
 80484e0:	ff 25 fc 9f 04 08    	jmp    *0x8049ffc
 80484e6:	66 90                	xchg   %ax,%ax
其中0x8049ffc是.got段的地址,其中<<程序员的自我修养>>一书对.got段的解释如下:


对于这这一点,我的猜想是可执行文件在执行时,共享模块被转载到进程的虚拟地址空间,那么global变量的实际地址被填入到.got段,那么这个时候可以通过一次间接跳转进行访问.至于为什么是

08048460 <_init>:
 8048460:	53                   	push   %ebx
 8048461:	83 ec 08             	sub    $0x8,%esp
 8048464:	e8 b7 00 00 00       	call   8048520 <__x86.get_pc_thunk.bx>
 8048469:	81 c3 97 1b 00 00    	add    $0x1b97,%ebx
 804846f:	8b 83 fc ff ff ff    	mov    -0x4(%ebx),%eax
 8048475:	85 c0                	test   %eax,%eax
 8048477:	74 05                	je     804847e <_init+0x1e>
 8048479:	e8 62 00 00 00       	call   80484e0 <__libc_start_main@plt+0x10>
 804847e:	83 c4 08             	add    $0x8,%esp
 8048481:	5b                   	pop    %ebx
 8048482:	c3                   	ret    

080484e0 <.plt.got>:
 80484e0:	ff 25 fc 9f 04 08    	jmp    *0x8049ffc
 80484e6:	66 90                	xchg   %ax,%ax
再到

 8048640:    a1 24 a0 04 08           mov    0x804a024,%eax

再到

  [23] .got              PROGBITS        08049ffc 000ffc 000004 04  WA  0   0  4

  [26] .bss              NOBITS          0804a024 001024 000008 00  WA  0   0  4

还有待探究,望高人指点!!!!!!!!!

总的来说,动态链接最大的好处就是实时更新方便,节省空间。假设某公司开发了一块软件产品,该产品由20个模块构成,总大小为20M,每当其中的任意一个模块更新时,用户不等不重新下载整个软件,这是一件很费网络流量和带宽的事情,但是在动态链接中,我们只需要下载被更新的模块即可,只要接口没变,这个更新的模块依然可以和其它子模块拼接成一个完整的功能模块.

以上纯属个人见解,有些问题还待解决,不足之处还望大牛指正!!!!!!!!!!!!!

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值