深入理解计算机---链接(二)

接上
GCC使用
gcc -S elf.c//生存汇编语言
5、符号和符号表
c中,任何声明带有static属性的全局变量或者函数都是模块私有的。
类似的,任何声明为不带static属性的全局变量和函数都是公共的,可被其他模块访问。尽可能使用static属性来保护你的变量和函数是很好的编程习惯

符号表是由汇编器构造的,使用编译器输出.s文件中的符号。.symtab节中包含ELF符号表。

6、符号解析
链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定符号定义联系起来。
当编译器遇到一个不是当前模块中定义的符号(变量或者函数名)时,它会假设该符号是在其他模块中定义的,生成一个链接器符号表表目。并把它交给链接器处理。如果链接器在它输入模块中找不到这个被引用的符号,就会终止。

void foo(void);
int main()
{
	foo();
	return 0;
}
//编译器通过,但链接器无法解析对foo的引用。终止
另外,还因为相同的符号会被多个目标文件定义。

在这里插入图片描述

  • 链接器如何解析多处定义的全局符号
    在编译时,编译器输出每个全局符号给汇编器,strong,weak,汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化全局变量是strong ,未初始化的全局变量是weak。
/*swap.c*/
extern int buf[];
int *bufp0=&buf[0];
int *bufp1;
void swap()
{
 int temp;
 bufp1=&buf[1];
 temp=*bufp0;
 *bufp0=*bufp1;
 *bufp1=temp;
}
/*main.c*/
void swap();
int buf[2]={1,2};
int main()
{
swap();
return 0;
}
// buf、bufp0、main和swap是强符号strong,bufp1是弱符号weak

根据强弱符号定义,unix 链接器使用如下规则处理多处定义的符号

规则1:不允许有多个强符号
规则2:如果有一个强符号和多个弱符号,那么选择强符号
规则3:如果有多个弱符号,那么从这些弱符号中任意选择一个
/*foo1.c*/
int main()
{
	return 0;
}
/*bar1.c*/
int main()
{
	return 0;
}
//gcc foo1.c barc.1
报错,因强符号strong main被定义多次 ,规则1


/*foo2.c*/
int x =15213;
int main()
{
	return 0;
}
/*bar2.c*/
int x;
void f()
{
}
gcc foo2.c bar2.c 正确 ,其中x 规则2 ,链接器选择初始化的x
若在bar2.c中,int x = 15213. 错误
 
/*foo3.c*/
#include <stdio.h>
void f(void);
int x =15213;
int main()
{
	f();
	printf("x=%d\n",x);
	return 0;
}

/*bar3.c*/
int x;
void f()
{
	x=10;
}
gcc -o foobar3 foo3.c bar3.c
./foobar3 
x=10   规则2

如果有两个弱定义:x
/*foo4.c*/
#include <stdio.h>
void f(void);
int x;
int main()
{
	x=15213;
	f();
	printf("x=%d\n",x);
	return 0;
}
/*bar4.c*/
int x;
void f()
{
	x=10;
}
或者:x
/*foo5.c*/
#include <stdio.h>
void f(void);
int x=15213;
int y=15212;
int main()
{
	f();
	printf("x=0x% y=0x%\n",x,y);
	return 0;
}
/*bar5.c*/
doubule x;
void f()
{
	x=-0.0;
}
gcc -o foobar5 foo5.c bar5.c
./foobar5
x=0x0 y=0x80000000
//foo5.c注释f() 
x=3b6d y=3b6c
因double 8字节,而int 4字节
注意此类错误,gcc 5.4.0 版本有warning
///usr/bin/ld: Warning: alignment 4 of symbol `x' in /tmp/ccE9WZqr.o is smaller than 8 in /tmp/cc9IxnHn.o
  • 与静态库链接
    上述,均假设链接器读取一组可重定位目标文件,并链接成为一个输出的执行文件。现在讨论,将所有相关目标模块打包为一个单独文件,称为静态库(static library),也可作为链接器的输入。当链接器构造一个输出的可执行文件时,它只拷贝静态库里被应用程序引用的目标模块。

    如果不使用静态库,编译器开发人员使用什么方法来向用户提供eg.atoi,printf,scanf,random,sin,cos,sqrt函数.
    1、 让编译器认出对标准函数的调用,并直接生成相应代码。但c标准定义大量标准函数,对编译器复杂度变高,修改标准函数,就需要一个新的编译器版本。
    2、将所有的标准c函数放在一个单独的可重定位目标模块中。缺点是,系统中每个可执行文件都包含一份标准函数几何的完全拷贝。另一个缺点是,对任何标准函数的修改,都要重新编译整个源文件。耗时,维护复杂。优点是,编译器的实现与标准函数的实现分离。
    另外,为每一个标注函数创建1个分离的可重定位文件,把它们放在一个共知的目录。然而,要求应用程序员显示地链接合适的目标模块到它们的可执行文件中,耗时易错。eg. gcc main,c /usr/lib/printf.o /usr/lib/scanf.o …
    静态库概念,相关函数可被编译为独立的目标模块,让后封装成一个单独的静态库文件。让后,应用程序可通过在命令行上指定单独的文件名字来使用这些在库中定义的函数。eg.使用标准c和数学库中函数的程序可以如; gcc main.c /usr/lib/libm.a /usr/lib/libc.a …
    在链接时,链接器只拷贝被程序引用的目标模块。减少存储大小。另一方面,程序员只需包含较少的库文件名字。

//addvec.c
void addvec(int *x,int *y,int *z,int n)
{
	int i;
	for(i=0;i<n;i++)
	{
		z[i]=x[i]+y[i];
	}
}
//multvec.c
void multvec(int *x,int *y,int *z,int n)
{
	int i;
	for(i=0;i<n;i++)
	{
		z[i]=x[i]*y[i];
	}
}
//vector.h
void addvec(int *x,int *y,int *z,int n);
void multvec(int *x,int *y,int *z,int n);

//gcc -c addvec.c multvec.c
//ar rcs libvector.a addvec.o multvec.o

//main2.c
#include<stdio.h>
#include"vector.h"
int x[2]={1,2};
int y[2]={3,4};
int z[2];
int main()
{
	addvec(x,y,z,2);
	printf("z=[%d,%d]\n",z[0],z[1]);
	return 0;
}

//gcc -O2 -c main2.c
//gcc -static -O p2 main2.o ./libvector.a
//-static 参数告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件,它可以加载到存储器运行,在加载时无须更进一步的链接。
当链接器运行时,拷贝addvec.o到可执行文件。 程序未引用multvec,即不会拷贝multvec.o定义的符号。
另外链接器还会从libc.a拷贝printf.o模块,以及其他c运行时系统的模块

在这里插入图片描述

  • 链接器如何使用静态库来解析引用
    疑惑:unix链接器使用静态库解析外部引用方式。在符号解析阶段,编译器驱动程序扫描可重定位目标文件和存档文件(unix中,静态库以一种称为存档’archive’ 的特殊文件格式存放在磁盘,.a后缀标识)。维持集合E:可重定位目标文件集合,这个集合中的文件会被合并起来形成可执行文件。集合U:未解析的符号集合,即引用了但尚未定义的符号。集合D:前面输入文件中已定义的符号集合。 初始时,集合E、U、D均空。
    1、 对于命令行上的每个输入文件 f,链接器会判断f是一个目标文件还是一个存档文件。如果f是一个目标文件,那么链接器把f添加到E,修改U和D来反映f中的符号定义和引用,并继续下一输入文件
    2、如果f是个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号。如果某个存档成员文件m,定义一个符号来解析U中的一个引用,那么就将m加到E中,并且链接器修改U和D来反映m中的符号定义和引用。对存档文件中所有的成员目标文件都反复进行这个过程,直到U和D都不在发生变化。此时,任何不被包含在E中的成员目标文件都被丢失,而链接器将继续到下一个输入文件。
    3、如果当链接器完成对命令行上输入文件的扫描,U是非空的,那么链接器就会输出一个错误并终止,否则,它会合并和重定位E中的目标文件,从而构建输出的可执行文件。
    这种,算法要求命令行上的库和目标文件的顺序非常重要。如果在命令行中,一个符号库出现在引用这个符号的目标文件前,那么引用不能被解析,链接失败。
顺序 如:
报错 //gcc -static ./libvector.a main2.c
在处理libvector.a时,U是空的,所以没有libvector.a中成员目标文件会添加到E中。因此对addvec的引用是绝不会被解析的,链接器报错终止
正确 //gcc -static -O p2 main2.o ./libvector.a
	一般准则是,库放在命令行末尾,如果各个库相互独立任何顺序均可。若库不是独立,必须排序。调用者放前面,被调用者放在后面。也可重复出现。

7、重定位
上述符号解析,把代码中的每个符号引用和确定的一个符号定义联系起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。
目的:合并输入模块,并为每个符号分配运行时地址。
步骤1:重定位节和符号定义。链接器将所有相同类型的节合并为同一类型的新的聚合节。并将运行时存储器地址赋给新的聚合节,赋给输入模块定义的每个节,以及定义的每个符号。此时,程序中的每个指令和全局变量都有惟一的运行时存储器地址了。
步骤2:重定位节中的符号引用。链接器修改代码节中对每个符号的引用,使得它们指向正确的运行时地址。此步依赖与 重定位表目(relocation entry)的可重定位目标模块中的数据结构。

  • 重定位表目
    汇编器生成这个目标模块时了不知道数据和代码最终存放在存储器中的位置,也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。就会生成一个重定位表目,告诉链接器在将目标文件合并成可执行文件时修改这个引用。
typedef struct{
	int offset;/*offset of the reference to relocate,   需要被修改的引用的节偏移*/
	int symbol:24,/*symbol the reference should point to, 标识被修改引用应该指向的符号*/
		type:8;/*relocation type,如何修改新的引用*/
}Elf32_Rel;
  • 重定位符号引用
  • 重定位PC相关的引用
  • 重定位绝对引用

8、可执行目标文件
前述,已经将多个目标模块合并成一个可执行目标文件。下图是一个典型的ELF可执行文件中的各类信息。 在这里插入图片描述
可执行目标文件格式类似可重定位目标文件格式。ELF头部描述文件总体格式。包括程序入口点(entry point)…text,.rodata和.data节和可重定位目标文件中的节是相似的。.init节定义一个小函数,程序的初始化代码会调用它。因可执行文件是完全链接的(已被重定位了),所以它不需要.relo节。
ELF可执行文件被设计为很容易加载到存储器,连续的可执行文件的组块(chunks)被映射到连续的存储器段。段头表描述了这种映射关系。

9、加载可执行文件
在这里插入图片描述
旁注:加载器实际上是如何工作的?
我们对于加载的描述从概念上来说是准确的,但也不是完全准确.为了理解加载实际是如何工作 的,必須理解进程、虚拟 存储器和存储器映射的概念。
概述:Unix系统中的每个程序都运行在一个进程上下文中,这个进程上下文有自己的虚拟地址空间.当shell运行一个程序时,父shell 进程生成一个子进程,它是父进程的一个复制品.子进程通过execve系统调用启动加载器,加载器刪除子进程已有的虚拟存储器段,并创建一组新的代码、数据、栈和堆段.新的栈和堆段被初始化为零. 通过将虚拟地址空间中的页映射到可执行文件的页大小的组块(chunks),新的代码和数据段被初始化 为可执行文件的内容. 最后,加载器跳转到_static地址,它最终会调用应用的main函数.除了一些头 部信息,在加载过程中没有任何从磁盘到存储器的數据拷贝。直到CPU引用一个被映射的虚拟页, 才会进行拷贝,此时,操作系统利用它的页面调度机制自动将页面从磁盘传送到存储器.

10、动态链接共享库
静态库缺陷1:如果应用程序需要一个更新的静态库,必须显示地将该静态库与程序重新链接
静态库缺陷2:几何标准I/O函数,如printf,scanf,在运行时,这些函数会被复制到每个运行的程序的文本中。
—> 共享库(shared library)致力于解决静态库缺陷。它是一个目标模块,在运行时,可以加载到任意存储器地址,并在存储器中和一个程序链接起来。此过程,被称为动态链接(dynamic linker);
又被称为共享目标(shared object),.so后缀表示。微软.dll.
共享表现在两方面:
1、任何给定的文件系统总,对于一个库只有一个.so文件。所有引用该库的可执行文件共享.so文件中的代码和数据,而不是如静态库的内容被拷贝和嵌入到引用它们的可执行的文件中。
2、一个共享的.text节只有一个副本可以被不同的正在运行的进程共享。
在这里插入图片描述
-fPIC选项 编译器 生成位置无关代码
-shared 创建一个共享的目标文件
链接 gcc -o p2 main2.c ./libvector.so
此时,没有任何libvector.so 的代码和数据被真的拷贝到可执行文件p2中。取而代之拷贝了一些重定位和符号信息,它们使的运行时可以解析对libvector.so中代码和数据的引用。
加载器加载和运行可执行文件p2时,利用节9讨论的技术,加载部分链接的可执行文件p2。接着,p2包含一个*.interp节,这个节包含动态链接器的路径名。加载器不再像通常那样将控制传递给应用,而是加载和运行这个动态链接器*。
动态链接器通过执行重定位完成链接任务:
1、重定位libc.so的文本和数据到某个存储器段。
2、重定位libvector.so的文本和数据到另一个存储器段
3、重定位p2中所有对由libc.so和libvectot.so定义的符号和引用。
最后,动态链接器将控制传递给应用程序。此刻,改动态库位置固定,并在程序执行的过程中不再改变。

11、从应用中加载和链接共享库
应用程序还可能在它运行时要求动态链接加载和链接任意共享库,而无需在编译时链接那些库到应用中。
更进一步,可在运行时,无需停止更新已存在的函数,以及添加新函数。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
12、*与位置无关的代码(PIC)
共享库的一个主要目的就是允许多个正在运行的进程共享存储器中相同的库代码。
多个进程时如何共享一个程序的拷贝的呢?一个方法是给每个共享库分配一个事先预备的专用的地址空间组块(chunk),然后要求加载器总是在这个地址加载共享库。地址空间效率低,难以管理。随着时间的进展,一个系统中由成百上千个库和各个版本,就很难避免地址空间分裂成大量晓得,未使用而又不能使用的小洞。
—> 位置无关代码(position-independent code,PIC)

  • PIC数据引用
    无论在存储器中的何处加载一个目标模块(包括共享目标模块),数据段总是分配未紧随代码段后面。因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时的常量,与代码段和数据段的绝对存储位置是无关的。
    利用上述特性,编译器在数据段开的地方创建一个表,叫做全局偏移量表(global offset table,GOT)。.
    GOT包含每个被这个目标模块引用的全局数据目标的表目。编译器还为GOT中每个表目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个表目,使得它包含正确的绝对地址。每个引用全局数据的目标模块都有一张自己的GOT。

  • PIC函数调用
    ELF编译系统使用延迟绑定。

13、 处理目标文件的工具

  • AR:创建静态库,插入、删除、列出和提取成员
  • STRINGS:列出一个目标文件中所有可打印的字符串
  • STRIP:从目标文件中删除符号表信息
  • NM:列出一个目标文件的符号表中定义的符号
  • SIZE:列出目标文件中节的名字和大小
  • READELF:显示一个目标文件的完整结构,包括ELF头中编码的所有信息。包含SIZE 和NM的功能
  • OBJDUMP:所有二进制工具之母。能够显示一个目标文件中所有信息。它最有用的功能是反汇编.text节中的二进制指令
    unix系统未操作共享库还提供ldd程序:
  • ldd:列出一个可执行文件在运行时所需要的共享库

小结:

  • 链接可在编译时由静态编译器来完成,也可在加载时和运行时由动态链接器来完成。链接器处理的的目标文件,有三种不同形式:可重定位的,可执行的和共享的。
  • 可重定位,由静态链接器组合成一个可执行的目标文件,它可以加载到存储器中 并执行
  • 共享目标文件(共享库)是在运行时由动态链接器链接和加载的,或者隐含地在调用程序被加载和开始执行时,或根据需要在程序调用dlopen库的函数时。
  • 链接器主要任务: 符号解析和重定位。 符号解析将目标文件中每个全局符号都绑定到一个惟一的定义,重定位确定每个符号的最终存储器地址,并修改对那些目标的引用。
  • 静态链接器是由像GCC这样的编译器调用的。它们将多个可重定位目标文件组合成一个单独的可执行目标文件。多个目标文件可以定义相同的符号,而链接器用来悄悄解析这些多处定义的规则可能在用户程序中引入的微妙错误。
  • 多个目标文件可被链接到一个单独的静态库。链接器用库来解析其他目标模块中的符号引用。许多链接器通过从左到右的顺序扫描来解析符号引用,顺序关键,容易错。
  • 加载器将可执行文件的内容映射到存储器,并运行这个程序。
  • 被编译为位置无关代码的共享库可以加载到任何地方,也可以运行时被多个进程共享。为了加载、链接和访问共享库的函数和数据,应用程序还可以在运行时使用动态链接器。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值