Linux 第七章学习笔记
1:链接概述
链接(linking)是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储并执行。
编译系统提供的调用预处理器、编译器、汇编器和链接器来构造目标文件的。
2. 静态链接
为了构造可执行文件,链接器的两个主要任务
-
符号解析:目标文件定义和引用符号。符号解析的目的是将每个符号引用刚好和一个符号定义联系起来。
-
重定位:编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个存储器位置联系起来,然后修改所有对这些符号的引用,使得它们指向这个存储器位置,从而重定位这些节。
符号解析: 和引用定义在相同模块的本地符号;编译器只允许每个模块中每个本地符号有一个定义对于全局符号的解析。 当编译器遇到一个不是在当前模块中定义的符号时,它会假设该符号是在某个模块中定义的,生成一个链接器符号表条目然后交给链接器处理; 如果在链接器的任何模块中都找不到这个被引用的符号,它就输出一条错误信息然后终止 对于多重定义的全局符号; 函数和已经初始化的全局变量是强符号,未初始化的全局变量是弱符号 规则: 不允许有多个强符号 如果有一个强符号和多个弱符号,那么选择强符号 如果有多个弱符号,那么从其中任选一个
重定位 重定位节和符号定义:链接器将所有相同类型的节合并为同一类型的新的聚合节 重定位节中的符号引用:链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时的地址 重定位条目。
3. 目标文件
三种形式
可重定位目标文件:包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标件。
可执行目标文件:包含二进制代码和数据,其形式可以被直接拷贝到存储器并执行。
共享目标文件:一种特殊类型的可重定位目标文件,可以在加载或者运行地被动态地加载到存储器并链接。
4. 可重定位目标文件
-
一个典型的ELF可重定位目标文件的格式:
ELF文件已经是适应到某一种CPU体系结构的二进制兼容文件了。
默认的ELF头加载地址是0x8048000,头部大概要到0x48100处或者0x483000处,也就是可执行文件加载到内存之后
5. 符号和符号表
每个可重定位目标模块m都有一个符号表,它包含m所定义和引用的符号的信息。
链接器上下文中的三种不同符号
由m定义并能被其他模块引用的全局符号。
由其他模块定义并被模块m引用的全局符号。
只被模块m定义和引用的本地符号。
符号表
typedef struct{ int name; //字符串表中的字节偏移,指向符号的以NULL结尾的名字 int value; //符号的地址,对于可重定位的模块是距定义目标的节起始位置的偏移。 int size; //目标大小(单位:字节) char type:4, //数据或函数 binding:4; //本地LOCAL/全局GLOBAL char reserved; char section; //到节头部表的索引 }Elf_Symbol;
每个符号都和目标的某个节相关联,由section字段表示。
- ABS:不该被重定位的符号。
- UNDEF:未定义的符号,在本目标模块中引用,但在其他地方定义。
- COMMON:未被分配位置的未初始化数据目标。
6. 符号解析
与静态库链接
-
所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为静态库(Linux下是存档文件,Windows下是lib),只拷贝静态库里被应用程序引用的目标模块。
-
链接时加上-static参数告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件,它可以加载到存储器并执行,在加载时无需更进一步的链接。
如何处理多重定义的符号? 不允许有多个强符号 如果有一个强符号和多个弱符号,那么选择强符号 如果有多个弱符号,那么从这些弱符号中任意选择一个
7. 重定位
(1)重定位两步
重定位节和符号定义:链接器将所有相同类型的节合并为同一类型的新的聚合节 重定位节中的符号引用:
链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时的地址重定位条目。
链接器依赖于称为重定位条目的可重定位目标模块中的数据结构。
(2)重定位条目
ELF定义了11种不同的重定位类型。两种最基本的重定位类型:
*R_386_PC32 重定位一个使用32位PC相对地址的引用。
*R_386_32 重定位一个使用32位绝对地址的引用。
(3)重定位符号引用
链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址。
重定位PC相对引用
重定位绝对引用
8. 可执行目标文件及加载
(1)可执行目标文件
-
C程序开始时是一组ASCII文本文件,已经被转化为一个二进制文件,且这个二进制文件包含加载程序到存储器并运行它所需的所有信息。
-
段头部表:可执行文件的连续片被映射到连续的存储器段,段头部表描述了这种关系。
- 在32位Linux系统中,代码段总是从地址0x08048000处开始。
(2)加载可执行目标文件
加载器将可执行目标文件中的执行代码和数据从磁盘拷贝到存储器中,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序拷贝到存储器并运行的过程叫做加载。
要运行可执行目标文件p,可以在Unix外壳的命令行中输入它的名字:unix> ./p
将程序拷贝到存储器并运行的过程叫做加载。
用户栈总是最大的合法用户地址开始,向下增长的(向低存储器地址方向增长)。从栈的上部开始的段是为操作系统驻留存储器
的部分(内核)的代码和数据保留的。
9. 动态连接共享库
动态链接器通过执行下面的重定位完成链接任务:
- 重定位libc.so的文本和数据到某个存储器段
- 重定位libvector.so的文本和数据到另一个存储器段
- 重定位p2中所有对libc.so和libvector.so定义的符号的引用
- 最后动态链接器将控制传递给应用程序,此时共享库的位置已固定,并且在程序执行的过程中不会改变
(1)静态库的缺点
- 首先,静态库在更新时,使用该库的程序需要与更新的库进行重新链接。
- 其次,由于使用静态库的程序在链接时都会拷贝静态库里被应用程序引用的目标模块,像printf和scanf这样的函数的代码在运行时都会被复制到每个运行进程的文本段中,这造成了冗余,浪费了稀缺的存储器资源。
(2)共享库
共享库是一个目标模块,在运行时,可以加载到任意的存储器地址,并和一个在存储器中的程序链接起来。
这个过程称为动态链接,是由一个叫做动态链接器的程序来执行的。
共享库也称为共享目标,在Unix系统中通常用.so后缀来表示。微软的操作系统大量地利用了共享库,它们称为DLL(动态链接库)。
共享库是以两种不同的方式来“共享”的(在Windows中分别称为“隐式链接”和“显示链接”)。
1:在任何给定的文件系统中,对于一个库只有一个.so文件。所有引用该库的可执行目标文件共享这个.so文件中的代码和数据,
而不是像静态库的内容那样被拷贝和嵌入引用它们的可执行的文件中。 2:在存储器中,一个共享库的.text节 一个副本可以被不同的正在运行的进程共享。
10.从应用程序中加载和链接共享库
void *dlopen( const char *file, int mode );//将共享目标文件打开并且映射到内存中,并且返回句柄 void *dlsym( void *restrict handle, const char *restrict name );//回一个指向被请求入口点的指针 char *dlerror();//返回 NULL 或者一个指向描述最近错误的 ASCII 字符串的指针 char *dlclose( void *handle );//关闭句柄并且取消共享目标文件的映射
11. 处理目标文件的工具
AR:创建静态库,插入、删除、列出和提取成员。 STRINGS:列出一个目标文件中所有可打印的字符串。 STRIP:从目标文件中删除符号的信息。 NM:列出一个目标文件的符号表中定义的符号。 SIZE:目标文件中节的名字和大小。 READELF:显示一个目标文件的完整结构,包括ELF头中的编码的所有信息。包含SIZE和NM的功能。 OBJDUMP:所有二进制工具之母,能够显示一个目标文件中所有的信息。它最大的作用是反汇编.text节中的二进制指令。 LDD:列出一个可执行文件在运行时所需要的共享库。