目录

从源文件生成可执行文件(书中第2章)

1.Preprocessing预处理——预处理器cpp

2.Compilation编译——编译器cll

ps:vs中优化选项设置

3.Assembly汇编——汇编器as

ps:vs中汇编输出文件设置

4.Linking链接——链接器ld

符号

模块,库

链接过程——链接器

链接过程

1.简单链接的例子

2.链接过程

3.地址和空间分配,符号决议,重定位

4.C++中链接相关的问题

5.使用静态库链接——gcc -static

6.实现一个静态链接的最“小”的helloword程序

7.VS链接器link.exe的符号解析过程

动态链接(第七章)

动态链接 vs 静态链接

共享对象so

装载时重定位——在装载时修正指令中对绝对地址的引用

代码段:编译生成地址无关“代码”——fPIC

数据段:数据段拥有绝对地址引用时,也会生成重定位表

使用动态链接的程序

动态链接下的装载

Linux共享库的组织

共享库的兼容性:

共享库的版本机制演化

1)共享库的文件命名规则:libxxx.so.x.y.z

2)使用SO-NAME来记录共享库的依赖关系

3)基于符号的版本机制p236-p240

系统指定的共享库目录

ldconfig程序

影响动态链接器行为的一些环境变量

1)LD_LIBRARY_PATH——临时改变动态链接器装载共享库路径的方法

2)LD_PRELOAD——利用全局符号介入测试某些函数

3)LD_DEBUG——打印装载过程的一些信息。

gcc和ld的编译链接命令

Windows下的共享库


书:《程序员的自我修养-链接装载库》 大部分分析的是C语言程序编译链接过程,linux下gcc。

也介绍了一些共享库的内容。

从源文件生成可执行文件(书中第2章)

( 在开发过程中我们一般使用IDE(集成开发环境),在IDE中,一般将编译链接一步完成,称为构建(Build)。比如用过的VS和Xcode。对于一些小型的实验,有时候就直接用命令行编译会更加方便)

使用gcc命令行“编译”源代码(hello.c)如下:

gcc hello.c

执行完会直接生成一个a.out可执行文件,这条命令其实是经过了编译和链接的过程了。

其中hello.c的内容

运行可执行文件,如下:

hello.c 到 a.out

源文件.c可执行文件.out,具体经历了4个过程:

Preprocessing预处理-》生成.i文本文件。

Compilation编译-》生成.s文件汇编指令的文件,也是文本文件也可以查看。

Assembly汇编--》.o目标文件。应该是二进制文件吧

Linking链接--》可执行文件。

(链接是比较重要的,平时开发中出现的复杂问题大都在这一步。)

GCC编译过程图解:

gcc hello.c

注:这里的cpp即C preprocessorC预处理器的简称。

下面介绍从源文件到可执行文件的4个过程:

1.Preprocessing预处理——预处理器cpp

如果想要一个源文件只进行预处理过程那么使用命令:

gcc -E hello.c -o hello.i

(原理是:gcc根据-E这个参数去调用cpp,相当于执行 cpp hello.c > hello.i  。所以用这个命 cpp hello.c > hello.i令也一样的)

可以用文本编辑器查看生成的hello.i,多了很多内容,大小也变大了很多:

预处理的处理过程如下:(主要涉及到以#开头的预处理指令)

1)处理#define 宏定义——删除所有的#define,收集有哪些宏定义(在vs的配置中可以直接定义有什么宏),展开宏定义。

2)处理#if #ifdef #elif #else #endif 条件指令 ——既然宏定义已经展开了,已经知道了定义了哪些宏,这里就可以相应的做出判断了。

3)处理#include 头文件包含指令 ——将被包含的文件内容插入到该位置,递归进行。

4)删除注释

5)为代码添加 行号和文件名标识和标志位,标识下面的代码来自于哪个文件的哪一行———给编译器用的,编译器产生调试用的行号以及编译错误的时候要给出提示哪一行错了。(可以看下hello.i的内容,结合 https://gcc.gnu.org/onlinedocs/cpp/Preprocessor-Output.html)

6)保留#pragma编译器指令——后面的编译器需要这些信息。

总结:对每一个源文件进行一下预处理过程,删除除了#pragma以外的所有的#开头的跟宏定义有关的,删除注释,增加行号信息。

2.Compilation编译——编译器cll

编译的过程很复杂,经历词法分析--》语法分析--语义分析--》中间代码生成--》目标代码生成(优化)——生成汇编指令的文件。

图示编译过程

在vs的IDE中经常有到优化的设置,应该是涉及到最后SourceCodeOptimizerCodeOptimizer这两个过程(书中2.2.1-2.2.5)。

在gcc中我们使用 -fno-builtin 这个来关闭内置函数(比如上述例子中会把hello.c中的printf会优化替换成puts函数来提高运行速度。优化选项,不关的话会进行一些函数的替换。

编译命令:

gcc -S hello.i -o hello.s

也可以直接从源文件生成汇编文件

gcc -S hello.c  -o hello.s

(gcc调用的是ccl,是c语言的预处理和编译程序。c++的预处理和编译程序是cllplus

最后输出的是汇编指令的文件

vim hello.s

总结:主要是将代码生成汇编语句,还有一些其他的信息。

ps:vs中优化选项设置

在不同的配置中,release 和 debug,默认的优化选项也不一样。

优化选项不一样,最后生成的机器码也不一样。

根据需要可以进行自行配置

选择启用优化的话,针对一些没有用的代码,就会被优化掉,也就是目标文件中就不会有对应的机器码了。这里有介绍过应用。

ps:伪指令

在生成的汇编指令的文件中,包含的是 汇编指令+伪指令。

伪指令包含一些段名标号啊,段的开始和结束啊,程序的结束啊,需要进一步汇编编译器翻译的。

汇编指令是真正有机器码的指令。

汇编的源代码要通过汇编编译器+链接器,生成最终的可执行文件。

3.Assembly汇编——汇编器as

汇编语句转化成机器语句,根据汇编指令机器指令的对照表一一翻译,生成最终的目标文件

汇编命令:

gcc -c hello.s  -o hello.o

(gcc调用的是汇编器as,相当于执行 as hello.s -o hello.o  所以用这个命令也一样的)

或者直接从源文件生成目标文件

gcc -c hello.c  -o hello.o

(-c表示只编译不链接)

注:目标文件中除了源代码编译以后的机器指令外(代码段),还包括的全局变量局部静态变量数据(数据段

除此之外,还包括了链接时需要用到的信息,比如符号表,调试信息,字符串等(符号表,字符串表等)。

这些都被划分成一个个段,涉及到目标文件的文件格式。后面会讲。可以想象成,可执行文件=描述信息+指令

总结:一般我们说的编译过程是包括上述的 Preprocessing预处理,Compilation编译 和 Assembly汇编。一个源文件,经过预处理,编译,汇编以后,变成一个目标文件

ps:vs中汇编输出文件设置——包含Compilation编译和Assembly汇编过程

选择汇编程序输出,默认选择的是 无列表【C/C++-》输出文件-》汇编输出程序】

其他值的含义:

/FA      仅输出汇编到文件, 文件默认扩展名是 .asm。

/FAc    输出汇编和相应的机器码到文件,文件默认扩展名是 .cod。

/FAs    输出汇编和相应的源代码到文件,文件默认扩展名是 .asm

/FAcs     输出汇编机器码源代码到文件,文件默认扩展名是 .cod。

一般如要选的话,就选择最后一个FAcs,可以通过这个辅助查找崩溃的具体行。具体可以查看https://blog.csdn.net/wind19/article/details/40614745

可以本地查看一下。一般是设置在Intermediate文件夹下,中间文件都是。

4.Linking链接——链接器ld

链接过程就是把编译好的目标文件和其他的一些目标文件和库链接在一起,形成最终的可执行文件。

下图是链接过程示意图:(下图中libc.a 是c语言的静态运行时库,crt1.o是见https://blog.csdn.net/u012138730/article/details/82805675)

下面先介绍一些常用术语:符号模块等 。

符号

符号其实就是一个地址,函数或者变量的地址。

远古时代,【jmp 目标地址

有了汇编语言以后,【jmp 符号】,使用符号来标记位置。所以符号编译阶段就用到了。

当符号在别的文件定义时,编译期间没法知道其地址,都要在等到链接过程中处理,寻找出 符号 的地址,然后对所有引用到这个符号的指令都修正为正确的地址

模块,库

在现代的软件代码中模块是一个重要的概念。

在C语言中,一个.c源文件就是一个模块(一个.o目标文件)。

在高级语言java中,一个类就是一个模块若干个类组成一个包若个干包组成一个最终程序

(:C/C++编译器在编译时并没有把函数签名保存到目标文件、可执行对象、和共享对象中,所以我们在运行时只能获取函数的地址。而java .net里面的反射功能可以实现运行时获得函数的额外信息,参数和返回类型,从这一点来说他们更加“高级”。)

在C语言中,一个模块就是指的一个目标文件

我们经常听到运行时库C、C++标准库动态、静态库,这些名词都是从不同的维度定义的。

运行时库(runtime Library)支持程序运行基本函数集合。(https://blog.csdn.net/u012138730/article/details/90377865)

标准库是实现了语言标准的诸如输入/输出处理、字符串处理、内存管理等的内容。标准库中的函数很多都是对系统调用的封装,比如C语言的标准库中有printf函数(当然也有不是的对系统调用的封装,比如strlen())——printf在Linux下封装的是一个write系统调用(系统调用 是程序与操作系统内核交互的中介),在Windows下是一个WriteConsole系统api。也就是说程序通过标准库进行一些系统调用,优点是:有了标准库,程序就可以忽略系统调用,直接使用标准库中的函数即可。

每个平台都有其自己的标准库实现libc.a就是Linux中平台中最常用静态的C语言标准库libc.so是动态库。

静态库其实就是目标文件的集合,是一组目标文件包/集合(nm xxx.a 可以看到很多个.o下面的符号,通常人们用ar压缩程序对一些列目标文件进行压缩,并对其进行编号和索引,便于查找)

静态库查看工具——ar命令

ps:如果用ar提示说这是一个a fat file,是这个.a中包含多个cpu框架的,要先进行分离才能使用下面的命令 lipo xx. a -thin armv7 -output xx_armv7.a

  • ar -t XXX.a——查看包含哪些目标文件

会输出很多目标文件

  • objdump -t xxx.a———打印每个目标文件下都有哪些符号

会输出每个目标文件下的每个符号

  • ar   -x xxx.a——把所有目标文件解压到当前目录

动态库其实也可以执行,跟可执行文件很接近了。需要在程序运行过程中当作一个模块装载到内存中。(等回顾完 https://blog.csdn.net/u012138730/article/details/82805675 和 这篇文章再来补充)

链接过程——链接器

链接要解决的问题之一模块之间的符号间的引用

在编译器的时候对不知道的符号的地址,指令会设置成0或其他的值。

链接器怎么知道哪些指令需要修正呢?——编译以后的目标文件有重定位表,记录着每一个要被修正的地方(重定位入口)。

链接过程中还链接了一些必须的运行时库和启动文件,比如上面图中libc.a crt1.o等等

链接的过程会产生map文件 vs中,调试用。


链接过程

1.简单链接的例子

a.cb.c分别生成a.ob.o,然后链接成可执行文件ab

两个源代码如下:

生成32位两个目标文件(-m32 表示生成32位,-c表示只编译不链接):

链接目标文件生成32位可执行文件ab(-m elf_i386 表示32位),指定程序入口为main函数(-e main。其实不需要指定,默认就是main)(这里使用的是动态链接,静态链接需要指定 -static,后面会讲):

2.链接过程

大概分成以下四步,通过ld链接器来完成以下的链接过程:

1)第一步:读取输入的多个目标文件(编译后生成的)读取段信息,合并相同的段或类似的段(段内容详情请看elf文件格式中的段表的内容,具体合并哪些相同的段其实有个默认的链接脚本指示的,后面会说道),确定各个合并后的段的虚拟地址(VMA)

首先,看下目标文件的头可以看到没有虚拟地址即VMA的值,VMA这一列均为0:

然后,看下可执行文件的头VMA的值(记住text段和data段的VMA值,下面会说到)

2)第二步:收集各个输入目标文件的符号(符号表)用来建立全局表(全局表是全局可见的符号,不是符号表,因为符号表里有不是全局的符号)

符号表各个列的含义详情请看elf文件格式中的段表的内容: 这里没有就关心下面圈起来的这几个符号

3)第三步:因为第一步中的已经确定了合并段的地址VMAtext段和data段)了,所以可以确定全局符号的虚拟地址(函数text段中,变量data段中)了。

链接以后的可执行文件符号表,Value列就是符号的虚拟地址了,Ndx表示在哪一段

可以对比1)中的可执行文件中data段和text段的VMA的值

4)第四步:重定位——修正代码指令中的符号的地址。在链接时,通过看目标文件重定位表,知道有哪些地方需要重定位(即那些包含外部符号的指令。因为上一步已经知道了外部符号虚拟地址,就可以修正代码指令中的符号的地址。

在编译器生成.o的过程中,由于有些符号是在外部定义的,所以当汇编那条代码的时候找不到该符号,就会在符号表中把符号标记为U,然后对应的指令地址标记为0或者其他地址,并在重定位表标记这个位置

比如a.c中对shared的地址的引用的对应的指令地址标记为0:(因为在该目标文件中找不到shared符号的定义,符号是未定义的)—— -d 查看反汇编

使用objdump查看a.o重定位表,可以看到需要重定位的地方有两个,text段的15和21字节开始,分别需要修正地址为符号sharedswap的地址。—— -r 查看重定位表

如果在合并后的全局表中找不到该符号的地址 链接器就报错,找到了就修正指令地址。下图就是重定位以后指令了,shared的地址已经变成真正的地址0804a000。至于怎么修正的下一部分会介绍。

3.地址和空间分配,符号决议,重定位

总结一下链接过程中涉及到的内容

  • 地址和空间分配(Address and storage)

1.多个输入目标文件如何合并成一个输出文件输出文件中的空间如何分配给输入文件———现在链接器都是采用相似段合并的方法。(可以自行定义链接脚本,定义合并规则。详细看书本4.6.3使用ld链接脚本中。)

2.我们在elf文件格式的文章中看到过,段表中有个属性,就是虚拟空间中的地址-sh_addr(VMA)————目标文件都为0,可执行文件就有值了。

  • 符号决议(Symbol Resolution)

对外部符号的解析。

符号决议也叫做符号绑定(Symbol Binding)

或者名称决议(Name Resolution),名称绑定(Name Binding)

或者地址绑定(Address Binding),指令绑定(Instruction Binding)。从细节上区分,在静态链接中多叫 决议 ,在动态链接中多叫 绑定

一个特殊的段COMMON段:

COMMON段弱符号的存放位置。没有未初始化的全局变量就是典型的弱符号。这个例子中没有未初始化全局变量,没有common段。

(介绍elf文件中中的符号表的时候符号所在的段st_shndx值有SHN_COMMON的,说明这个是弱符号,如下:)

现代链接器处理弱符号的规则是:

在生成全局表的时候,

如果同名符号中,一个强符号,其他都是弱符号,那么就是就用强符号。此时如果弱符号size大小大于强符号会给出一个warning。

如果同名符号中,都是弱符号,那么就选占用空间最大的那个弱符号。

弱符号为什么不在bss段,而在单独的common段:

通过上述的规则我们知道,之所以不在bss段就是他还不确定占用的空间多大,要等到链接以后才能确定。

  • 重定位(Relocation)——指令修正

前面看到过,-r 可以查看重定位表。

重新计算符号的地址过程被叫做重定位首先介绍一下elf文件中的重定位表段结构和含义

/usr/include/elf.h

对于不同的文件类型(可重定位文件(就是目标文件.o) ,可执行文件(.out或者没有) ,共享文件(.so)) ,r_offsetr_info两个变量的含义不同

(怎么看到可执行文件共享文件重定位表 使用 -R 参数)

a.o可重定位文件的重定位表,

OFFSET就是r_offset,

TYPE就是r_info的低8位,

VALUE就是r_info的高24位:

主要看下重定位类型

cpu的指令:

转移跳转指令(jmp)

子程序调用指令(call)

数据传送指令(mov)每种指令都有很多种不同的寻址方式,这里的重定位类型就是寻址方式的类型。

对于32位x86平台下elf文件的重定位入口所修正的指令寻址方式只有两种:

绝对近址32位寻址(R_386_32,变量shared

相对近址32位寻址(R_386_PC32,函数swap),32位即被修正的地址的占用4个字节,

上述例子中:

变量sharedR_386_32,绝对近址寻址,修正后的地址就是符号的绝对地址((S+A),由于A是0,所以就是S,就是实际地址。shared在可重定位文件中指令的地址是0

函数swapR_386_PC32,相对近址寻址,修正后的地址是符号距离被修正位置的地址差,正好是7个字节(00000007)(S-P+A)。

4.C++中链接相关的问题

相关的问题有:

重复代码消除-模版虚函数表,外部内联函数默认构造函数,默认拷贝构造函数和赋值操作符

函数级别链接 VS中 C/C++ 代码生成 /Gy

全局构造和析构

C++和ABI

p113遇到的是再看

5.使用静态库链接——gcc -static

--verbose参数是显示整个过程,图中的过程大概分成以下三步:

1.使用ccl编译成一个.s文件

2.使用as汇编成目标文件.o文件

3.使用collect2进行链接。collect2是ld的一个封装,他会调用ld来完成链接以及完成一些程序初始化结构的过程。

gcc默认是动态链接的,使用-static参数进行静态链接

静态链接动态链接的文件大小大很大,因为静态链接中每个程序的可执行文件本身内部都保留着诸如printf,scanf,strlen标准库函数以及系统库等等(静态链接和动态链接的区别。动态链接时,可执行程序的内部的系统函数标准库函数这些都只是一个符号而已,没有实际内容,实际内容在动态库中,运行是加载动态库)。

而程序中使用标准库和系统库都是同样一份代码,其实只需要存在一份,所以静态链接会比较浪费内存空间和磁盘空间。

下图中的a.out是静态链接的,adongtai.out是动态链接。可以通过查看:

nm a.out 和

nm.adongtai.out 看看符号

6.实现一个静态链接的最“小”的helloword程序

TinyHelloWorld.c:自己进行write系统调打印,而不是用C语言标准库里的。自己调用EXIT系统调用结束进程

编译生成32位目标文件(32位程序加的参数 编译的时候 -m32):

进行静态链接生成32位可执行文件(32位程序加的参数 链接的时候 -m elf_i386):

  • asm就是指示里面是汇编指令,那么print里面的汇编指令的含义:

(就是调用了write系统调用, write系统调用C语言描述就是write(int filedesc, char* buffer, int size);)

movl $13, %%edx 将立即数13(字符串str的长度)传递给第三个参数size
movl %0, %%ecx 将指针str(%0指代str变量)传递给第二个参数buff
movl $0, %%ebx 将立即数0(默认终端的文件描述符为0)传递给第一个参数filedesc
movl $4, %%eax 使用寄存器eax用来存放系统调用号,write调用号为4,见unistd_32.h
"r"(str)表示由编译器决定使用哪个通用寄存器来存放str变量,"edx", "ecx", "ebx",告知编译器汇编代码会修改这几个寄存器的值

  • 链接的时候指定入口函数-e nomain

把程序的入口函数设置为nomain,elf文件格式中的 Elf32_Ehdr的 e_entry 成员的值(目标文件没有这个值,只有可执行文件有,不知道动态库有没有)

如果不指定得话 就是默认得main,如果是main得话就会有是语言库中得一个 _start函数进行完初始化工作以后,调用main函数,最后再调用一个exit函数(https://blog.csdn.net/u012138730/article/details/82805675 中有 _start函数介绍

这里我们指定得是自己的nomain作为入口函数,所以也需要自己调用exit函数了。

  • 同样是静态链接,依不依赖库大小相差很多(因为标准库中还包含了其他的函数)

  • 如果不设置 -e nomain 链接这个会怎么样(why,也能运行,但是会出错)

7.VS链接器link.exe的符号解析过程

在符号解析 (symbol resolution) 阶段,链接器按照所有目标文件和库文件出现在命令行中的顺序(配置属性-链接器-命令行)从左至右把他们放入输入文件列表中,然后依次扫描它们,在此期间它要维护若干个集合 :

(1) 集合 E 是将被合并到一起组成可执行文件的所有目标文件集合

(2) 集合 U 是未解析符号 (unresolvedsymbols ,比如已经被引用但是还未被定义的符号 ) 的集合

(3) 集合 D 是所有之前已被加入到 E 的目标文件定义的符号集合

一开始,这三个集合都是空的。 链接器的工作过程:

(1) 对命令行中的每一个输入文件 f ,链接器确定它是目标文件还是库文件:

如果f它是目标文件,就把 f 加入到 E ,并把 f 中未解析的符号和已定义的符号分别加入到 U 、 D 集合中,然后处理下一个输入文件。(如果加入D中的符号,已经在U中存在,肯定要删了U中的符号吧)

如果f它是库文件,链接器会尝试把 U 中的所有未解析符号f 中各目标模块定义的符号进行匹配。如果某个目标模块 m 定义了一个 U 中的未解析符号,那么就把 m 加入到 E 中,并把 m 中未解析的符号已定义的符号分别加入到 U 、 D 集合中。(应该要U中找到的给删除吧)。不断地对 f 中的所有目标模块重复这个过程直至到达一个不动点 (fixed point) ,此时 U 和 D 不再变化。而那些未加入到 E 中的f 里的目标模块就被简单地丢弃。

链接器继续处理下一输入文件。

(3) 如果处理过程中往 D 加入一个已存在的符号,或者当扫描完所有输入文件时 U 非空,链接器报错并停止动作。否则,它把 E 中的所有目标文件合并在一起生成可执行文件。

在链接的时候有三个规则:关于强符号和弱符号的,上述的过程应该没有考虑这个因素

规则 1: 不允许强符号多次定义 ( 即不同的目标文件中不能有同名的强符号 ) ;

规则 2: 如果一个符号在某个目标文件中是强符号,在其它文件中都是弱符号,那么选择强符号

规则 3: 如果一个符号在所有目标文件中都是弱符号,那么选择其中任意一个


动态链接(第七章)

注意一下,这里讨论的是动态链接和静态链接,并不是动态库和静态库。

动态链接 vs 静态链接

1.链接的时机不同:

动态链接真正的链接过程是在装载时进行的;

静态是编译链接期间,生成了可执行文件载前就链接好了。

从一个程序角度来看,静态链接下,程序,就是可执行文件,是一个整体;动态链接下的程序是有多个模块的——可执行文件主模块和她所依赖的共享对象(so)。

2.链接器的不同:

动态链接是动态链接器ld.so进行链接,

静态链接是ld链接器进行的

3.动态链接参与链接的是动态共享库(.so),

(还有静态共享库吗,有的,静态共享库的装载地址是固定的,现在都是不怎么用了,在一些旧的系统中还有)

4.动态链接的优缺点:

缺点:动态链接由于是在装载时进行链接的,在性能上会有一点损失,优化有——延迟绑定

优点:1)更加有效利用内存和磁盘空间。2)更加方便的维护升级程序。3)让程序的重用变得更加可行和有效。

共享对象so

一个例子Lib.h Lib.c如下:

编译成一个共享对象Lib.so

gcc -fPIC -shared -o Lib.so Lib.c

注:

-shared——表示产生共享对象

-fPIC    ——表示Position-idependent Code技术 地址无关代码(后面介绍)(-fpic 相比于-fPIC产生的代码较小。-fPIC在对硬件平台没有限制 -fpic有。-fPIC更加具有兼容性

注:如果不用-fPIC会报错,报错如下: gcc -shared -o Lib.so Lib.c

/usr/bin/ld: /tmp/ccULU6Ci.o: relocation R_X86_64_32 against `.rodata' can not be used when making a shared object; recompile with -fPIC

查看Lib.so的装载属性

查看程序头以及section到segment的映射(readelf -l 执行视图)

有各个section,有各个程序头

so共享对象执行文件都差不多内容。

装载时重定位——在装载时修正指令中对绝对地址的引用

共享对象的装载地址是不确定的,而可执行文件的装载地址是可以定的,因为他一般是第一个被装上的

因为共享对象需要在任意位置被加载,所以共享对象需要在装载时候进行重定位,根据最终装载到某个地址上了以后,再对指令中的那些对绝对地址的引用进行重定位到真正的绝对地址上。

通过objdump -R xxx.so ——可以看到共享对象so文件的重定位信息(如果是动态链接的可执行文件也可以查看-R

(而可执行文件是在链接期间,多个.o链接的时候,就对所有的需要重定位的地方(.o的重定位表,-r)都会进行一个重定位的过程)

缺点:对指令中的地址进行修正的话,共享库就失去了共享的意义了,因为指令代码中的地址跟具体某个装载地址有关系了。

而共享库在不同的程序的装载地址是任意的,不是同一个。

可执行文件中代码段可以不是地址无关代码

共享文件中的代码需要是地址无关代码,具体如何实现地址无关分成四种情况看下文

代码段:编译生成地址无关“代码”——fPIC

解决方法:把代码指令中需要被修改的地方剥离出来放到数据部分数据部分是每个进程单独一份副本的,不是共享的。

比如有个pic.c源文件,其代码之间的符号调用分成以下四个类型:

1)《Type2》对于模块内的数据访问:我们只需要用相对寻址,而因为在同一个模块中,所以可以获得指令和数据之间相对地址,是确定的。虽然在现代的体系结构中,数据的相对寻址往往没有直接相对于当前指令地址(PC)的寻址方式。我们首先获得当前指令地址即可

2)《Type4》对于模块间的数据访问:因为我们不知道另外一个模块相对于自己这个模块的距离,要等到装载的时候才能确定,所以必然是需要进行修改的。

我们把修改的部分放在在数据部分——在数据段里面建立一个GOT——全局偏移表,把这个当作一个中介。

在模块中的指令引用就引用GOT的相对地址(这相当于模块内的数据访问),然后GOT对应于真正的外部模块的变量,对应关系等到真正装载的时候再进行修改,GOT就是一个指向这些外部变量的指针数组。

查看so文件GOT的位置:(例子来源于a.c b.c --->ab.so)

objdump -h ab.so

查看GOT中的内容

objdump -R ab.so

3)《Type1》对于模块内的函数调用,本身就有相对地址调用

4)《Type3》对于模块间的函数调用,使用类似于模块间的数据访问,只不过GOT中存的是外部模块的函数地址,而不是数据

总结:

对于数据访问,需要把Type4转化为Type2。指令中不能包含数据的绝对地址。

对于函数调用,需要把Type3转化为类似于Type2,只不是存的是外部的函数地址指令中不能包含目标函数的绝对地址。

全局符号介入:

实际上生成的pic.so中的bar()也是用的GOT,跟ext()一样,因为考虑到可能有全局符号介入的情况存在

全局符号介入(global symbol interpose):一个共享对象里面的全局符号被另外一个共享对象中的同名全局符号覆盖的现象。

linux下的动态链接器处理全局符号介入:当一个符号被加入全局符号表时,已存在同名的,则忽略后加入的。

因为bar()全局符号

所以调用处就是按《Type3》类型3来的跟ext一样:

 

为了提高模块内部函数调用的效率,有一个办法是把bar变成编译单元私有的,使用static关键字定义bar函数,在这种情况下就是真正按类型1进行:

PIC的DSO(动态库)是不会包含任何代码段的重定位表的地址(TEXTREF段)

readelf -d xxx.so| grep TEXTREF

有任何输出就说明xxx.so不是PIC的因为PIC是不会包含任何代码段的重定位的,PIC是地址无关代码

PIC与PIE

地址无关代码,即指令代码是跟地址无关的。

地址无关可执行文件,一个以地址无关的方式编译的可执行文件

定义在共享对象中的全局变量——通过GOT来实现对变量的访问,类型四Type4

module.c可能是共享对象的一个源文件,还是可执行对象的一个源文件。

对于定义global的共享对象(定义的,和使用的的编译都会按照2)对于模块间的数据访问中进行编译,即有GOT (在-fPIC下)。

原因:

因为global全局对象,他是可以在可执行文件中被引用的。一旦他在可执行文件中被引用,那么可执行文件编译的代码可以不是地址无关的,他会在指令中会直接使用global的绝对地址,所以global会在可执行文件的bss段有一个副本。

(在默认情况下,动态链接下,gcc会默认对可执行文件也进行使用PIC技术生成地址无关的代码,可以查看动态链接中可执行文件中有got这样的段)

类型四《Type4》指的是 2)对于模块间的数据访问。

注:

后面会讲到:

共享数据段

线程私有存储 

数据段:数据段拥有绝对地址引用时,也会生成重定位表

static in a;

static int* p = &a;

指针p的地址是一个绝对地址=a的地址,也就是数据段拥有绝对地址引用

因为a的地址会随着装载地址的改变而改变,所以需要在装载时对p进行重定位

当拥有绝对地址引用时编译器链接器会生成重定位表,类型R_386_RELATIVE的重定位入口,

当动态链接器装载拥有这样的重定位入口的共享对象的时候就会进行重定位了

注:虽然书中说可以不使用-fpic生成so,但是实际测试会报错。

使用动态链接的程序

例子Program2.c

生成可执行文件

gcc -o Program2 Program2.c ./Lib.so

查看进程的虚拟地址空间分布:

ld-2.17.so动态链接器

libc-2.17.soc语言标准动态库

Lib.so就是我们创建的动态共享库 在运行时被加载进去了

ldd——查看程序或者共享库依赖哪些共享库:

后面的地址是装载地址

linux-vdso.so.1是一个内核虚拟共享对象,不存在于文件系统中 

动态链接下的装载

可执行文件中的相关的段:https://blog.csdn.net/u012138730/article/details/82805675

动态链接下可执行文件中的装载:https://blog.csdn.net/u012138730/article/details/82805675

动态链接比静态链接慢的原因:

1)模块间的数据和函数调用要进行复杂的GOT定位间接寻址

2)动态链接器在程序开始时,寻找和装载共享对象,进行符号查找重定位解决模块之间的函数引用等。

针对原因2)进行优化,采用延时绑定(采用plt,见https://blog.csdn.net/u012138730/article/details/82805675),在函数第一次被用到的时候才进行绑定(即进行符号查找,重定位),用来加快程序的启动速度。


Linux共享库的组织

共享库共享对象没什么区别,linux下共享库就是普通的elf共享对象

共享库的兼容性:

1)兼容更新:原有接口不变,新增接口。

2)不兼容更新:改变了原有的接口

这里接口指的是ABI——包括函数调用的堆栈结构,符号命名,参数规则,数据结构的内存分布,成员的对齐方式等。

不同的语言对ABI兼容性不一样。

对于C语言来说:p231

对于C++语言来说:p232 和 p115

共享库的版本机制演化

1)共享库的文件命名规则:libxxx.so.x.y.z

正常情况下:

例外:

glibc有许多组件,c语言库只是其中的一个,动态链接器也是其中的一个。

2)使用SO-NAME来记录共享库的依赖关系

正常:

共享库的文件名去掉次版本号发布版本号,留下主版本号即为共享库SO_NAME

例外:

红框是例外,绿框是正常

命令行——编译链接用的链接名

3)基于符号的版本机制p236-p240

1》Solaris系统 的版本机制和范围机制

2》linux系统下 的符号版本机制扩展

系统指定的共享库目录

  • /lib
  • /usr/lib
  • /usr/local/lib

ldconfig程序

很多软件包的安装程序系统里安装了共享库以后都会调用ldconfig,因为需要这个程序做一下事情:

1)为共享库目录下的各个共享库创建,删除或更新相应的SO-NAME

2)收集共享库SO-NAME,集中存放在/etc/ld.so.cache,方便动态链接器查找共享库,而不用遍历所有的共享库目录

影响动态链接器行为的一些环境变量

1)LD_LIBRARY_PATH——临时改变动态链接器装载共享库路径的方法

因为动态链接器固定搜索路径的第一个查找共享路径就是这个环境变量中设置的路径,所以可以临时改变这个。

比如ls肯定是依赖libc的,我们测试一个新版本的libc库的放置在/home/usr下,然后让ls去加载新版本的libc。

ls程序是一个可执行程序,依赖的共享库有:红框是libc库绿框是动态链接器。

默认情况下LD_LIBRARY_PATH是空的

设置LD_LIBRARY_PATH/home/usr,执行ls

相当于使用动态链接器(-library-path)执行:

gcc编译的时候也会用到LD_LIBRARY_PATH中的设置的路径进行查找动态库(相当于gcc -L),所以不要随意修改LD_LIBRARY_PATH并导出到全局范围

2)LD_PRELOAD——利用全局符号介入测试某些函数

动态链接器固定搜索路径之前

会加载所有LD_PRELOAD中设置的共享库或目标文件,

不管是否用到。

3)LD_DEBUG——打印装载过程的一些信息。

除了设置LD_DEBUG=files ,还可以设置成其他的值

gcc和ld的编译链接命令

1)gcc -wl,-soname,xxxx      《==》ld -soname xxx

2)gcc -wl,-rpath,xxx            《==》ld -rpath xxx

3)gcc -wl,-export-dynamic  《==》ld -export-dynamic

4)gcc -wl,-s/S                     《==》ld -s/S:剔除符号信息

5)gcc -L :动态库查找路径


Windows下的共享库

待补充

C/C++的编译和链接过程相关推荐

  1. 程序的编译和链接过程

    一.虚拟机.linux简介 简单介绍一下虚拟机还有就是各种操作系统,比如centos,Ubuntu 操作系统:linux(centos.Ubuntu.redhat),Android,Windows(x ...

  2. C++主流预处理,编译和链接过程

    在C++的程序的编写过程中,基本上都碰到过LNK2005的错误吧,下面就针对这个问题详细分析: 首先,预处理阶段: 这一过程,主要针对#include和#define进行处理,具体过程如下: 对于cp ...

  3. 【C语言关键知识点1】C语言的预处理、编译和链接过程

    1 引言   再一次回顾C语言的关键基础知识,今天带大家深刻的剖析一下C语言的预处理.编译(汇编)和链接的过程,以加深对C语言及编程本质的理解!   学习C语言首先要理解的就是如何将程序员输入的源代码 ...

  4. C/C++编译和链接过程详解 概述 (重定向表,导出符号表,未解决符号表)

    详解link  有 些人写C/C++(以下假定为C++)程序,对unresolved external link或者duplicated external simbol的错误信息不知所措(因为这样的错 ...

  5. 描述C,C++编译和链接过程

    为什么80%的码农都做不了架构师?>>>    详解link 有 些人写C/C++(以下假定为C++)程序,对unresolved external link或者duplicated ...

  6. C语言编译、链接过程探究

    编译器基本构成: C语言编译基本流程图解: 预处理: 处理所有的注释,以空格代替 将所有的#define删除,并且展开左右的宏定义 处理条件编译指令#if,#ifdef ,#elif,#else , ...

  7. u-boot移值(九)-u-boot的编译、链接过程

    上一篇文章<u-boot的配置>了解了u-boot的配置过程,配置完成后,我们只需要一条简单的指令: make all 就能实现对u-boot的编译,Makefile也类似于C编程,先包含 ...

  8. 对AngularJS的编译和链接过程讲解一步到位的文章

    内容摘抄于 http://stackoverflow.com/questions/12164138/what-is-the-difference-between-compile-and-link-fu ...

  9. 编译/链接过程如何工作?

    编译和链接过程如何工作? (注意:这本来是Stack Overflow的C ++ FAQ的条目.如果您想批评以这种形式提供FAQ的想法,那么在所有这些都开始的meta上的张贴将是这样做的地方.该问题在 ...

最新文章

  1. 实战:使用TCP/IP筛选保护服务器安全
  2. Makefile写法入门心得
  3. javaScript实现字符串首字母大写
  4. linux系统是否支持gpt分区,Linux下进行GPT分区
  5. 【竞赛方案】2020腾讯广告算法大赛:高分进阶
  6. 大数据(1) - 虚拟机集群搭建
  7. SAP C4C url mashup跳转原理 - C4C UI到Mashup的参数传递是如何进行的
  8. 钓鱼(信息学奥赛一本通-T1431)
  9. php ci controller,Codeigniter – CI_Controller与控制器
  10. DataGridView绑定list的注意事项
  11. mysql8.0.15免安装版配置_Win10配置MySQL8.0.15免安装版教程
  12. 【转载】PHP.INI配置:Session配置详细说明教程
  13. mysql的数据类型5---enum与set类型
  14. 基于netty实现socketio的聊天室
  15. 【元胞自动机】基于matlab元胞自动机模拟SEIR传播模型【含Matlab源码 2156期】
  16. 可达性分析算法代码举例
  17. 使用Python(OCR)收集体温打卡截图,并自动发消息提醒没交的人。
  18. 利用雅可比方法求线性方程组C语言_工程项目经济评价的基本方法
  19. 礼物帮手-论文(不全)
  20. 使用列表推导式生成一个[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]的列表

热门文章

  1. 宽带813有连接,连接不上。
  2. 给你8个接私活的网站,保证你月薪轻松上W
  3. C语言之标准(KRC 、c89、c99、c11)
  4. Rockchip RK3588获取芯片的实时温度
  5. 个人电脑日常必备软件推荐,无广告、好用、持续更新
  6. IDEA中的翻译插件
  7. 我们一起完成插件框架的设计与实现
  8. Intel伽利略官方文档整理
  9. 计算机毕业设计Python+uniapp基于微信小程序的校园快递代取平台(小程序+源码+LW)
  10. FBAR滤波器的工作原理及制备方法