关于C语言静态链接的个人理解,欢迎指正

摘要:本篇主要介绍在静态链接中多个文件合并、地址确定、符号解析和重定位相关问题,以GCC编译器为例。

首先,链接器链接多个文件时,采用何种方式合并为一个文件?方式一,按序叠加,即多个文件依次叠加起来;方式二,相似段合并。采用何种方式就要看哪种方式利大于弊。

方式一:这种方式实现简单,链接速度快,基本不需要太多操作。但是,通常简单的东西往往是粗暴的。我们知道gcc编译后得到的可重定位目标文件是由各种段(section)组成的,这样简单叠加会产生大量零散的段,项目越大这样的段越多,而且还是大量相同名称的段。并且由于每个段都有地址和空间的对齐要求,这样做势必会浪费大量的内存空间(内部碎片)。所以这种方案并不好,可谓小甜头换大痛苦。

方式二:这种方式是将相似段合并,比如将多个不同文件的.text段合并为一个大.text段,类似的.data、.bss等等也是如此。最终得到的文件,在段的数量和类型上与原来各个小文件没有大的区别,只是每个段的大小变大了。当然这样做实现细节上肯定要复杂,也会牺牲一定的速度。但是这种付出是值得的。

对应方式二这种方式链接器一般采用两步链接,即分两步走。

  1. 空间与地址分配:(1)扫描各个输入文件获得各个段的长度、属性、位置等信息;(2)收集各个文件的符号表并建立统一的全局符号表。这一步将根据各个段的信息计算出合并后各个段的长度和位置,并建立映射关系(我的理解是更新段表的信息,段表描述ELF文件包含的所有段的信息,比如段名、长度、偏移量等)。
  2. 符号解析与重定位:这一步至关重要,由于上一步对相似段进行合并之后,原先的符号表信息已经过时,并且原先文件中代码的地址并没有映射到虚拟地址空间中,所以这一步要完成符号解析与重定位、调整代码中的地址等。

下面是对上面第二步的进一步解析。

首先调整代码位置这相对来说比较简单也易于理解,以Linux为例,在Linux下32bit ELF可执行文件默认地址从0x08048000开始分配,根据合并后各个段的位置做相对移位即可。如有下面示例:

代码b.c

1 int shared = 1;
2 void swap(int* a, int* b)
3 {
4    *a ^= *b ^= *a ^= *b;
5 }

代码a.c

1 extern int shared;
2 int main(int argc, char** argv)
3 {
4    int a = 100;
5    swap(&a, &shared);
6    return 0;
7 }
8 ~  

编译后输出a.o,b.o,cc -c a.c b.c,使用objdump查看a.o、b.o如下:

连接a.o b.o,的到可执行文件ab

可以看到a.o和b.o他们的起始地址都为0,而可执行文件ab的起始地址则是从0x0804000起(.text段之前还有文件头)。

重点和难点在于符号解析和重定位,即要更新文件合并后的总全局符号表,在构建全局符号表时即完成符号解析,重定位需要在符号解析后完成。在目标文件的结构中有一种section叫重定位表,各个段中如果有需要重定位的符号那么就会有相应的重定位表,如.text的重定位表是.rel.text。由于在a.c代码中用到了shared和swap这两个符号都属于b.c文件中的定义,链接时需要重定位,所以a.o中的的text段就会有相应的重定位表。同样用objdump可以查看目标文件的重定位表内容:

我们可以看到有两行是关于需要重定位符号shared和swap的描述,其中OFFSET表示它们在a.o文件中的偏移值,TYPE表示重定位时对指令的修正方式,下面是书中对其解释的相应的图表:

上面提到的r_offset和r_info是重定位表的结构中的变量,摘取书中的解释:

所以我的理解是A就是还未重定位时,符号shared和swap的地址,P即是在可执行文件ab中需要修改处的偏移值,而S是b.o和a.o合并后符号shared和swap的实际地址。具体该怎么算继续往下面看。

我们将a.o进行反汇编得到:

那么怎样找到代码a.c中对shared和swap的使用是上图哪两条指令呢?因为shared和swap是在b.c中定义的,在链接时a.o中对符号shared和swap必然要重定位,因此a.o中需要重定位的位置即是使用这两个符号的位置,所以由上面text段的重定位表中信息可知在偏移量为11和20处的两条指令即是使用它们地方(因为需要重定位的偏移量刚好在这两条指令中间)。

?我们来分析这两条指令,寄存器esp是专门用来作为栈顶指针使用的,所以mov $0x0, 0x4(%esp)是把0储存在偏移栈顶4个字节的位置,那这个0值又是什么呢?当然不是shared值啦,shared值是已知的只是不知道它存储在什么地方,为什么不知道它在什么地方呢?是因为在链接之前还没有对它重定位,所以这个0值应该是未重定位之前shared地址的默认值(这里这样解释只是我自认为的一种通俗表达,我的另一种理解是在编译层面和语言层面shared是不同的东西,在语言层面shared是一个变量,对它的操作是直接对它所在内存区域的操作,而在编译层面shared是内存中某块地址空间的引用,所以在符号表中shared的值是那块内存的地址,因此0值就是shared的值)。这个0又是怎么来的,注意上图是反汇编,所以汇编代码是由机器指令反编译得到的,偏移量11处的机器指令是0xc7 44 24 04 00 00 00 00,前4个字节是指令码,后四个字节是符号shared对应的值,即0。另外一条指令call 21 <...>,在汇编(其他语言也是)中函数名就是函数在内存中的起始地址,所以假设21就是swap的起始地址,那么call 21和call swap是等价的,现在是swap不在代码a.c中定义,这样的话就不能使用call swap了,而是给swap起始地址一个默认值(21),使用call 21。这个21又是怎样得到的呢?看与这条指令对应的机器指令,e8 fc ff ff ff(5个字节长度),书中对这条机器指令的解释是:0xe8是操作码,在Intel的IA-32体系中表示这是一条近址相对位移调用指令,操作码后面的四个字节就是被调用函数的相对于调用指令的下一条指令的偏移量,在没有重定位前默认为0xfc ff ff ff(小端字节表示法,代表-4的补码),所以21是(25-4)得来的,这是个假地址。

?现在再对链接a.o b.o 输出的可执行文件ab进行反汇编。

?由重定位表的偏移量计算可知,现在关于符号shared和swap的使用指令对应上图的偏移量为80480a5和80480b4两条指令。上图我们看到的结果是重定位后的,重定位时我们要修改的值是偏移量80480a5处的后四个字节和偏移量804800b4处后四个字节。根据上文提到的公式S+A和S+A-P,就可以算出重定位后的值。

?首先看符号shared重定位后的值怎么算。先查看可执行文件ab得到合并后变量shared的地址,如图

?    ?上图数据段在虚拟地址空间中起始地址是0x08049158,因为这个可执行文件中data段中就只有一个数据变量所以这个地址也是shared的地址,即S=0x08049158,重定位前地址是0x0,即A=0x0,所以S+A=0x08049158,在内存中以小端表示法存储时即为58 91 04 08。你可能会问如果不只一个全局变量时,我又怎么知道他们合并后的地址,注意合并后的地址都在符号解析后的全局符号表中,链接器是知道的。

?再看swap重定位后的值怎么算,在上面对可执行文件ab反汇编时我们看到函数swap的入口地址为0x080480c0,即S=0x080480c0,重定位前call汇编代码对应的机器指令后四个字节的值是ff ff ff fc(-4),即A=-4,P是被修正的位置,为0x080480b5,由公式S+A-P(c0-4-b5=7)得重定位后修改为07 00 00 00(小端表示法)。

?以上是我在看《程序员的自我修养--链接、装载与库》一书中第四章前2节的个人理解,限于个人水平问题,有些地方的理解可能有偏差,欢迎指正。

时间: 2024-11-06 08:24:17

关于C语言静态链接的个人理解,欢迎指正的相关文章

c语言静态链接库

1 获得lib文件 vc++ 6.0中 新建 Win32 Static Library项目,命名为libTest 新建lib.h文件,代码如下 #ifndef LIB_H #define LIB_H extern "C" int add(int x,int y); //声明为C编译.连接方式的外部函数 #endif 新建lib.cpp文件,代码如下 #include "lib.h" int add(int x,int y) { return x + y; } 编译后

C语言 之建立静态链接库

下面说一下建立静态链接库的方法 各个C语言编程软件都有它的方法,比如建立一个工程来共享文件,这就比较容易和简单了,现在我们选择使用难一点的linux系统,探究在linux系统下的建立静态链接库的方法. 首先,在linux系统我们在一个文件夹目录里面来建立一个 .h 头文件和一个 .c 文件,(比如我建立tiaoshen.c 和 mmc.h)然后打开它们. 在 .c 头文件里面我们写上自己创作的函数,比如下面我的例子: int add(int a,int b) { return a+b; } in

C语言编写静态链接库及其使用

本篇讲述使用C语言编写静态链接库,而且使用C和C++的方式来调用等. 一.静态库程序:执行时不独立存在,链接到可执行文件或者动态库中,目标程序的归档. 1.用C编写静态库步骤 a.建立项目(Win32 Static Library) b.加入库程序,源文件使用C文件 (Win32 Static Library) clib.c库源文件 <pre name="code" class="cpp">int CLib_add(int add1,int add2)

静态编译、动态编译、静态链接库和动态链接库理解

1.静态编译:编译器在编译可执行文件时,把需要用到的对应动态链接库(.so或.ilb)中的部分提取出来,链接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库. 2.动态编译: 动态编译的可执行文件需要附带一个的动态链接库,在执行时,需要调用其对应动态链接库中的命令.所以其优点一 方面是缩小了执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源.缺点一是哪怕是很简单的程序,只用到了链接 库中的一两条命令,也需要附带一个相对庞大的链接库:二是如果其他计算机上没有安装对应的运行库,

C语言之静态链接库和动态链接库

1:静态链接库 比较早出现的是静态链接库.静态库其实就是商业公司将自己的函数库源代码经过只编译不连接形成.o的目标文件,然后用ar工具将.o文件归档成.a的归档文件(.a的归档文件又叫静态链接库文件).商业公司通过发布.a库文件和.h头文件来提供静态库给客户使用:客户拿到.a和.h文件后,通过.h头文件得知库中的库函数的原型,然后在自己的.c文件中直接调用这些库文件,在连接的时候链接器会去.a文件中拿出被调用的那个函数的编译后的.o二进制代码段链接进去形成最终的可执行程序. 2:动态链接库 动态

静态链接库与动态链接库详解

转载: 关于静态链接库(Lib,.A)与动态链接库(DLL,.SO) (2011-10-10 21:04:26) 转载▼   分类: c.vc.cpp 在windows下一般可以看到后缀为dll和后缀为lib的文件,但这两种文件可以分为三种库,分别是动态链接库(Dynamic-Link Libraries),目标库(Object Libraries)和导入库(Import Libraries),下面一一解释这三种库. 目标库(Object Libraries) 目标库又叫静态链接库,是扩展名为.

C++ 动态链接库和静态链接库

概论 先来阐述一下DLL(Dynamic Linkable Library)的概念,你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用的变量.函数或类.在仓库的发展史上经历了"无库-静态链接库-动态链接库"的时代. 静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib中的指令都被直接包含在最终生成的EXE文件中了.但是若使用DLL,该DLL不必被包含在最终EXE文件中,EXE文件执行时可以"动态"地引用和卸载这个与EXE

dll和lib(包括静态链接库和与dll同时生成的lib)

转:http://blog.csdn.net/galaxy_li/article/details/7411956 1:神马是Dll和Lib,神马是静态链接和动态链接 大家都懂的,DLL就是动态链接库,LIB是静态链接库.DLL其实就是EXE,只不过没main. 动态链接是相对于静态链接而言的.所谓静态链接就是把函数或过程直接链接到可执行文件中,成为可执行程序中的一部分,当多个程序调用同样的函数时,内存里就会有这个函数的多个拷贝,浪费内存资源.而动态链接则是提供了一个函数的描述信息给可执行文件(并

计算机科学基础知识(三)静态库和静态链接

三.将relocatable object file静态链接成可执行文件 将relocatable object file链接成可执行文件分成两步,第一步是符号分析(symbol resolution),第二步是符号重新定位(Relocation).本章主要描述这两个过程,为了完整性,静态库的概念也会在本章提及. 1.为什么会提出静态库的概念? 程序逻辑有共同的需求,例如数学库.字符串库等,如果每个程序员在撰写这些代码逻辑的时候都需要自己重新写那么该是多么麻烦的事情,而且容易出错,如果有现成的,