初始学《链接装载与库》

来源:互联网 发布:真丝枕巾 知乎 推荐 编辑:程序博客网 时间:2024/05/17 04:09

因为工作原因,很久没有写博客,近期初略看了一个《链接装载与库》,书写得很好,但因本人功力有现,好些地方还是不明白。做个小结先。相信以后再读此书会有更多收获。

 

 

 

第二章 编译和链接

从原文件到可执行文件可心为为几部:预编译,编译,汇编,静态链接。

预编译:由预编译器完成。主要处理代码中以“#”开始的预编译指令。比如“include”"define"等,删除所有注释,插入文件标识。(生成.i文件)

编译:对预处理完的文件进行词法,语法,语义分析及优化,生成相应的汇编代码文件(生成.cod,.s文件)。

汇编:将汇编代码转变为机器可执行的指令。(生成目标文件.obj,.o文件)

链接:把目标文件组装的过各就是链接。主要的工作是:地址空间分配,符号决议和重定位。

 

库的本质:他是一组打包存放的目标文件。

重定位:地址修正的过程也叫重定位,每个要修正的地方叫重定位入口。

 

每三章 目标文件里有什么

文件格式

OMF-对象模型文件(Object Module File)

OMF是一大群IT巨头在n年制定的一种格式,在Windows平台上很常见。大家喜欢的Borland公司现在使用的目标文件就是这种格式。

COFF – 通用对象文件格式(Common Object File Format)

MS和Intel在n年前用的也是OMF格式,现在都改投异侧,用COFF格式了。

1.PE(Portable Executable)格式,是微软Win32环境可执行文件的标准格式。

2.ELF-可执行及连接文件格式(Executable and Linking Format)

ELF格式,是linux通用格式,在非Windows平台上使用得比较多,在Windows平台基本上没见过。

其它格式:Unix a.out和MS-Dos.COM格式。

 

这些格式文件归类

1.可重定位文件。

用途:连接成为执行文件或共享目标文件

实例:Linux的.o,Windows的.obj

2.可执行文件(一般没有文件名)

3.共享目标文件

用途:1.作为可执行文件一部分运行生成可执行文件。2生成新的目标文件。

例:linus .so, windows dll

4.核心转储文件(略)

用于保存运行信息。Linux 下core dump

(目的文件,动态静态链接库,可执行文件都按可执行文件格式存储)

 

目标文件中的段:

ELF Header:描述文件的基本属性,如程序入口,文件版本等。(正是这样,病毒可能修改程序的入口地址)

Section Table:描述各个段的信息,如段句,升序,在文件中的偏移,读写权限等。

.test:指信段

.data段:已经初始化了的全局中静态变量和局部静态变量

.bss段:未初始化的全局变量和局部静态变量。

.rodata段: 存放只读据,const修饰的变量和字符串常量

.rel.text段:重定位表。如对printf的调用。在链接时使用重定位表对地址进行修正。

符号表:函数和变量统称符号,函数或变量名就是符号名。符号值就是函数或变量的地址。特殊符号:_executable_start,_etext或_etext或etext,_edata或edata,_end或end

字符串表

段表字符串表

.comment:编译器版本信息

 

符号修饰与函数签名

即函数的名称修改方法,用于识别不用的函数。C++为了实现以C的兼容,使用符号"extern "C""声明的符号会按C的方式进行符号修饰。

弱符号与强符号

规则:1.强符号不允许多次定义。2.如果一个符号在霜冻个目标文件中是强符号,在其它中是弱符号,那么选择强符号。3.如果都是弱符号,那么选择空间最大的一个(这样可能产生调用过程中的问题)。

弱引用与强引用

在链接时,没有找到符号定义就报错,称为强引用。

 

 

第四章 静态链接

连接器一般采用“两步链接”的方法进行链接。第一步进行空间和地址分配,第二步进行符号解析和重定位。

空间和地址分配

空间分配:这里的空间分配只关心虚拟地址的空间分配,而不关心可执行文件的空间分配。空间分配采用相似段合并的方法,合并后某个段内部的布局是有输入文件的顺序决定的,当然收到连接脚本的控制。

地址分配:确定各个段的虚拟地址,包括BSS段。

空间分配需要的信息包括段的长度、属性和位置(偏移)。

空间和地址分配遍结束后:有一个全局符号表,记录所有目标文件的符号信息,包括符号定义和符号引用。合并后各个段的起始地址和长度确定

符号解析与重定位

符号解析:确定各个符号的地址。根据段的地址和符号在段中的偏移以及相同段的合并顺序,可以确定各个符号的准确地址,从而更新符号表。每个要重定位的段都有一个重定位表。所有被引用的符号都不能是未定义的。

重定位:根据各个目标文件的重定位表信息,可以准确的定位文件中哪些地方需要进行重定位。

重定位表的结构如下:

Typedef struct{

          Elf32_Addr                r_offset;               //从定位入口的偏移,相对于作用段的偏移

           Elf32_Word               r_info;                 //入口的类型和符号

}Elf32_Rel;

符号解析结束,找到重定位入口,下一步做的就是指令修改。根据指令的寻址方式进行修改。

 

 

第六章 可执行文件装载与进程

32位虚拟地址空间0到2的32次方减1(0到0xFFFFFFFF).

 

从操作系统看可执行文件装载

进程的建立:一个进程的建立最关键的特征是拥有独立的虚拟地址空间。进程的建立分三步:

1 创建独立的虚拟地址空间:创建虚拟地址空间实际上是建立虚拟地址空间到物理空间的映射,在i386的linux实际上就是创建一个页目录,或者称为页表。

2 读取可执行文件头,建立虚拟空间与可执行文件的映射关系。当程序发生页错误时,操作系统将从内存中分配一个物理页,并将该缺页从磁盘读入内存。然后设置页的映射关系。显然,当缺页时,操作系统需要知道程序当前需要的页在磁盘中的哪个位置,此时指令的虚拟地址是知道的,这就需要建立一个虚拟地址到可执行文件之间的映射。这样的一种数据结构称为VMA,它记录了各个段对应的虚拟空间,并记录该段在文件中的偏移。

3 CPU指令寄存器设置成可执行程序入口,启动。

页错误:当CPU执行某条指令,如果该指令所在页是空页,则发生段错误。操作系统捕获,找到该指令所在的VMA,并计算出该页在文件中的偏移,分配一个物理页,将文件内容读入内存,设置虚拟地址也物理地址的映射关系,即页表。将控制权还给程序。

文件的链接视图和执行视图

对于相同的权限的段,合并到一起当作一个段进行映射。称为"Segment",和前面的section不一样。一个是从装的角度看(Segment),一个是链接的角度看(Section)。

 

段地址对齐

最简单的情况是,各个Segment在物理内存中都分别映射,Segment起始地址都是4096整数倍(每页4K)。这样容易造成物理空间的浪费。

 

 

第七章 动态链接

为什么要动态链接

.节省内存和磁盘空间(共享代码,但私用数据会有复本)。2.程序的升级。3.有利于程序的可扩展和兼容性

 

动态库参与链接

gcc  –o  Program1  Program1.c  ./lib.so

Program1.c引用到lib.so中的foobar()函数,这里foobar定义在共享对象中,连接器会将这个符号的引用标记为一个动态链接的符号,不对其进行重定位,留到装载时再进行。动态库lib.so保存了完整的符号信息,把./lib.so作为连接的输入符号之一,连接器在解析符号时就知道foobar是定义在lib.so中的动态符号。如果foobar是定义在其他目标文件中的函数,链接器会按照静态链接的规则进行重定位。

 

固定装载地址的困扰

静态共享库:操作系统在某些特定的地区划分一些地址块,为那些已知的模块预留足够的空间。

装载时重定位(任意地址加载,方法一)

装载时重定位是首先想到的解决共享对象任意地址装载问题。在链接时,对所有绝对地址的引用都不尽兴重定位,而是把这一步推迟到装载时进行。

连接时重定位 vs 装载时重定位(基址重置)这种方法对共享对象并不是很合适,因为这种方法需要修改指令,而指令部分被多个进程共享,没法做到一份指令被多个进程共享,因为指令被重定位后对每个进程都是不同地。

但这种方法可以修改共享对象的数据部分,因为数据部分都是进程私有的。

 地址无关代码(任意地址加载,方法二)

    要想实现指令部分的进程共享,解决共享对象指令中对绝对地址的重定位问题,方法是将指令中那些需要被修改的部分分离出来,放在数据部分。这种方案称为地址无关代码(PIC, Position-Independent Code)技术。

 模块中四种类型的地址引用:

(1)模块内部的函数调用、跳转

(2)模块内部的数据访问,全局变量?、静态变量

(3)模块外部的函数调用、跳转

(4)模块外部的数据访问,如其他模块定义的全局变量

 

延迟绑定

动态链接速度慢的原因有:

(1)对于全局变量和静态数据的访问都要通过GOT表定位,再间接寻址。

(2)对于模块间的调用也要先定位GOT,然后再进行间接跳转。

(3)动态链接的链接工作在运行时完成。

主要的原因是(2)和(3),因为模块间的全局变量访问比较少,多了耦合性就强。

 延迟绑定是指函数在第一次用到时才进行绑定,包括符号查找、重定位等。如果没有用到则不进行绑定。这样就节省了(3)的时间。

ELF采用PLT(Procedure Linkage Table)的方法来实现延迟绑定。这种方法是在GOT之上增加一层间接跳转来实现的。每个外部函数在PLT中都有一个对应的项,调用函数并不是直接跳转的GOT中进行定位,而是先跳转到PLT中的对应项,如果是第一次调用该函数,PLT则先进行符号绑定,填充GOT表,再跳转过去。否则直接跳转到GOT进行定位。

 

 

第9章Windows下的动态链接

Windes PE采用了与ELF不同的办法,它采用的是装载时重定位的方法。

 

 

第10章 内存

栈向低地址增长,堆向高地址增长,直到预留的空间被用完。

 

对于windows来说,每个线程默认栈大小是1MB,可以在createThread时指定。

堆栈帧一般包含以下几个内容:

(1)传入的参数

(2)返回的地址

(3)保存的寄存器(上下文)

(4)临时变量

返回值<=4字节,值用eax返回,5-5字节,用eax和edx联合返回(eax低4,edx高4),大于8字节:为返回值开一个临时空间,把地址传给被调用函数,被调用函数为它赋值并用eax返回它的地址。(如果调用函数要用到返回值,就用eax找到临时对象,复制值)

 

1.malloc是一个运行库的功能,系统“批发”给进程一块较大的空间,程序通过malloc进行内存的管理。在linux上用mmap和windows中的VirtualAlloc相似,向系统申请空间。对为x86来说,申请的大小必须是4096字节的整数倍。

2.winows堆管理函数:heapcreate,heapalloc,heapfree,heapdestroy.每个进程都有一个默认堆,默认大小为1M.

3.堆在内存中是通过空闲链表,位图,对象池等方式组织起来的。

 

 

第11章 运行库

入口函数和程序初始化

程序的入口点实际上是一个程序的初始化和结束部分,它往往是运行库的一部分。典型的程序运行步骤:

1.系统创建进程,把控制权交到程序入口,入口往往是运行库的某个入口函数。

2.穰函数对运行库和运行环境进行初始化,包括堆,I/O,线程,全局中变量构造等等。

3.初始完后,调用main,执行程序主体。

4.返回到入口函数,进行清理工作,包括全局变量,堆销毁,关闭I/O,然后系统调用结束进程。

 

MSVC CRT入口函数

MSVC CRT默认的入口函数名为mainCRTStartup, 它的总体流程是:

1.初始化和OS版本有关的全局变量。

2.初始化堆。alloc.

3.初始化I/O.

4.获取命令行参数和环境变量。

5.初始化C库一些数据。

6.调用main并记录返回值。

7.检查并并main返回值返回。

 

exit函数和return函数的主要区别是:

1)exit用于在程序运行的过程中随时结束程序,其参数是返回给OS的。也可以这么讲:exit函数是退出应用程序,并将应用程序的一个状态返回给OS,这个状态标识了应用程序的一些运行信息。

main函数结束时也会隐式地调用exit函数,exit函数运行时首先会执行由atexit()函数登记的函数,然后会做一些自身的清理工作,同时刷新所有输出流、关闭所有打开的流并且关闭通过标准I/O函数tmpfile()创建的临时文件。

exit是系统调用级别的,它表示了一个进程的结束,它将删除进程使用的内存空间,同时把错误信息返回父进程。通常情况:exit(0)表示程序正常, exit(1)和exit(-1)表示程序异常退出,exit(2)表示系统找不到指定的文件。在整个程序中,只要调用exit就结束。

 

运行库与I/O

现代系统系统对系统资源都有严格的控制。以文件操作举例:设置文件句柄可以防止用户写操作系统内核的文件对象,文件句柄总是和内核的文件对象相关联的。内核可以通过句柄来计算出内核文件对象的地址。

在内核中,每一个进程都有一个私有的”打开文件表“,这个表是一个数组,每一个元素都指向一个崔的打开文件对象。而打开文件得到的fd是这个表的下标。(p329)

 

C/C++语言运行库

支撑c语言运行的一系列函数所构成的集合称为运行时库。C语言运行库大致包含:启动与退出,C标准函数,堆,语言实现调试。

参数格式:int printf(const char* format, ...);

cdecl调用惯例保证参数的正确清除,它是由调用方负责清除堆栈。

局部跳转

C语言中一个争议机制,它可以实现从一个函数休向别一个函数体跳转。(p340)

 

glibc与MSVC CRT

C语言的运行库是c程序和和不同操作系统平台之间的抽象层,将不同的API抽象成相同的库函数。但象线程和谐这样的功能并不是标准c运行库的一部分,glibc有pthread库的pthread_create创建,而MSVCRT有_beginthread创建。所以事实上他们是标准C语言运行库的超集。

 

层次关系图(P402)

 

 

第12章 系统调用与API

系统调用原理

用户态的程序一般是通过中断来从用户态切换到内核态。windows中的运行库中通过调用windows api实现系统调用,windows API实质上是以DLL函数函数的形式暴露给应用程序开发者的。

SDK是Windows API DLL导出函数的声明头文件、导出库、相关文件和工具的集合。Windows在API之上建立了很多应用模块,如winine.dll。之所以引入API,是为了隔离硬件结构的不同而导致的程序的兼容性问题。

 

原创粉丝点击