每当谈到静态链接和动态链接的时候脑子就会犯晕,到底什么是静态链接?什么是动态链接?它俩有什么联系?在静态链接和动态链接两种情况下程序发生了什么?以及它们的区别是什么?这些问题在对一个程序员来说肯定是要了解的,在学习完<<程序员的自我修养>>一书之后决定就这个问题做一些个人的总结。下面就带大家简单来看看程序在编译过程中发生了什么?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,每当其中的任意一个模块更新时,用户不等不重新下载整个软件,这是一件很费网络流量和带宽的事情,但是在动态链接中,我们只需要下载被更新的模块即可,只要接口没变,这个更新的模块依然可以和其它子模块拼接成一个完整的功能模块.
以上纯属个人见解,有些问题还待解决,不足之处还望大牛指正!!!!!!!!!!!!!