目录
1)LD_LIBRARY_PATH——临时改变动态链接器装载共享库路径的方法
书:《程序员的自我修养-链接装载库》 大部分分析的是C语言程序的编译和链接过程,linux下gcc。
也介绍了一些共享库的内容。
C++ 引用Lib和Dll的方法总结_Bird鸟人的博客-CSDN博客
cl.exe和link.exe分别是visual studio 中的编译器和链接器
配置属性中的【c/c++】(设置编译的一些选项) 和 【链接器】选项页中,最后的那个命令行汇总了所有生效的设置,就是最终执行的命令行
配置属性中的VC++ 目录用于设置各个路径,相当于设置环境变量PATH(搜可执行文件的路径):
INCLUDE(搜include中文件的路径)
LIBPATH(搜使用using 引入文件的路径)
LIB(搜库文件的路径)
一般一个大的项目,修改完一个cpp以后,一般都先单独编译一下这个cpp,免得到整个工程生成的时候发现这个cpp的一些编译错误。把光标停留在某个cpp的页面中,然后【生成】--【编译】,或直接ctrl+F7
编译所做的事情:
对一个cpp进行预处理,将头文件加载进来,并且将各种#define信息代入,生成一个独立的编译单元,然后进行编译生成obj文件,一个cpp对应一个obj文件。
编译过程,先生成汇编语言,再生成机器语言(即汇编指令对应的机器码,cpu认识的指令)
1.工程属性==》配置属性==》C/C++下
1.选择编译为C代码: ==》高级:编译为
2.选择运行时库:==》代码生成:运行库
定义了不同的宏:
- 静态链接的多线程库 /MT: _MT 宏
静态链接的多线程库的目标代码也最终被编译在应用程序的二进制文件中,但是它可以在多线程应用程序中使用。通过/MT编译选项可以设置Visual c++ 使用静态链接库的多线程库。 - 动态链接的运行时库 /MD: _MT 和 _DLL
动态连接的运行时库将所有的c库函数保存在一个单独的动态链接库MSVCRxx.DLL中,其处理了多线程问题。使用/MD编译选项可以设置Visual c++使用动态链接的运行时库。 - /MDd、/MTd选项使用Debug runtime library(调试版本的运行时刻函数库, _DEBUG宏),与/MD、/MT对应为release版本。debug版本的Runtime Library 包含了调试信息,并采用了一些保护机制以帮助发现错误,将强了错误的检测,因此性能方面比不上release版本。
- 运行时库分成静态链接运行时库和动态链接运行时库。在vs中新建生成静态库的工程,编译生成成功后,只产生一个.lib文件,在vs中新建生成动态库的工程,编译成功后,产生一个.lib文件和一个.dll文件。静态库和动态库中的lib区别:
静态库中的lib:该LIB包含函数代码本身(即包括函数的索引,也包括实现),在编译时直接将代码加入程序当中
动态库中的lib:该LIB包含了函数所在的DLL文件和文件中函数位置的信息(索引),函数实现代码由运行时加载在进程空间中的DLL提供
总之,lib是编译时用到的,dll是运行时用到的。如果要完成源代码的编译,只需要lib;如果要使动态链接的程序运行起来,只需要dll。
3.设置warning等级:==》常规:警告等级
4.设置查找include<头文件>的路径:==》常规:附加包含目录
2.工程属性==》配置属性==》常规下
1.选择字符集: 字符集
多字节字符集 和 Unicode字符集,char和wchar string 和 wstring 等等
多字节字符集相当于定义了 _MBCS 宏,Unicode字符集为 _UNICODE 宏,有些函数参数类型是根据这个宏区别是否是宽字节类型。
(见 工程属性==》 配置属性==》C/C++==》预处理器定义==》继承的值)
3.工程属性==》配置属性==》链接器
1.添加需要链接的lib库的库名:工程属性==》配置属性==》链接器==》输入:附加依赖项
2.设置查找lib库的路径:工程属性==》配置属性==》链接器==》常规:附加库目录
或者 #pragma comment(lib, “**.lib”)
4.工具--》选项
1.设置字体:==》环境==》字体和颜色
2.显示行号:==》文本编辑器==》C/C++==》常规
3.与源代码不匹配:==》调试==》常规:要求与源文件与原始版本完全一样
4.自定义快捷键:==》环境==》键盘:查找然后分配
5.定位打开文件在项目中的位置==》
5.快捷键
1.{}匹配: 光标停在一侧括号前后,按 ctrl+] 或 ctrl+[
2.调试快捷键:
平时用的快捷键只有:F5 F10 F11 ctrl+F7
========
F5 开始调试
ctrl+F5 开始执行(不调试)
shift+F5 关闭执行
=======
F9 在本行 增加断点和删除断点 可切换状态
Ctrl+F9 在本行 禁用断点和启用断点 可切换状态
Ctrl+Shift+F9 删除所有断点
Alt+F9 禁用所有断点 (自己定义的) Shift+Alt+F9 启用所有断点 (自己定义的)
======
F7 生成解决方案
Ctrl+Alt+F7 重新生成解决方案
Ctrl+F7 编译本文件
======
F10 逐行
F11 逐语句
======书签相关:打开书签窗口 Ctrl+K,ctrl+w
Ctrl+F2 在本行 增加书签和删除书签 可切换状态
F2 下一个书签 Shift+F2 上一个书签
Ctrl+Shift+F9 删除所有书签
=====实用tricks
按住Alt 多行列处理模式
=====格式化
1.格式化,缩进(去掉行尾注释空格)
ctrl+k ctrl + d
2. 抽取方法
ctrl + R ctrl+M
ctrl+R ctrl+E 字段封装成属性
3.高亮
Alt+K
F3
shift+F3
ESC
4. 查找
Alt + 上下左右箭头
6.问题
1.打开.rc无法加载
VisualStudio命令提示行输入:
devenv /ResetSkipPkgs
从源文件生成可执行文件(书中第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的内容,结合 Preprocessor Output (The C Preprocessor))
6)保留#pragma编译器指令——后面的编译器需要这些信息。
总结:对每一个源文件进行一下预处理过程,删除除了#pragma以外的所有的#开头的跟宏定义有关的,删除注释,增加行号信息。
2.Compilation编译——编译器cll
编译的过程很复杂,经历词法分析--》语法分析--》语义分析--》中间代码生成--》目标代码生成(优化)——生成汇编指令的文件。
图示编译过程:
在vs的IDE中经常有到优化的设置,应该是涉及到最后SourceCodeOptimizer和CodeOptimizer这两个过程(书中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,可以通过这个辅助查找崩溃的具体行。具体可以查看windows下通过map,cod分析堆栈_wind19的博客-CSDN博客
可以本地查看一下。一般是设置在Intermediate文件夹下,中间文件都是。
4.Linking链接——链接器ld
链接过程就是把编译好的目标文件和其他的一些目标文件和库链接在一起,形成最终的可执行文件。
下图是链接过程示意图:(下图中libc.a 是c语言的静态运行时库,crt1.o是见剖析ELF文件格式的内容———文件头,段表,符号....(第三章)_elf文件解析_ivy_0709的博客-CSDN博客)
下面先介绍一些常用术语:符号,模块,库等 。
符号
符号其实就是一个地址,函数或者变量的地址。
远古时代,【jmp 目标地址】
有了汇编语言以后,【jmp 符号】,使用符号来标记位置。所以符号在编译阶段就用到了。
当符号在别的文件定义时,编译期间没法知道其地址,都要在等到链接过程中处理,寻找出 符号 的地址,然后对所有引用到这个符号的指令都修正为正确的地址。
模块,库
在现代的软件代码中模块是一个重要的概念。
在C语言中,一个.c源文件就是一个模块(一个.o目标文件)。
在高级语言java中,一个类就是一个模块,若干个类组成一个包,若个干包组成一个最终程序。
(:C/C++编译器在编译时并没有把函数签名保存到目标文件、可执行对象、和共享对象中,所以我们在运行时只能获取函数的地址。而java .net里面的反射功能可以实现运行时获得函数的额外信息,参数和返回类型,从这一点来说他们更加“高级”。)
在C语言中,一个模块就是指的一个目标文件。
我们经常听到运行时库,C、C++标准库,动态、静态库,这些名词都是从不同的维度定义的。
运行时库(runtime Library)是支持程序运行的基本函数的集合。(可执行文件的装载,进程和线程,运行时库的入口函数(第六章)_sizeof(eprocess)_ivy_0709的博客-CSDN博客)
标准库是实现了语言标准的诸如输入/输出处理、字符串处理、内存管理等的内容。标准库中的函数很多都是对系统调用的封装,比如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——把所有目标文件解压到当前目录
动态库其实也可以执行,跟可执行文件很接近了。需要在程序运行过程中当作一个模块装载到内存中。(等回顾完 剖析ELF文件格式的内容———文件头,段表,符号....(第三章)_elf文件解析_ivy_0709的博客-CSDN博客 和 这篇文章再来补充)
链接过程——链接器
链接要解决的问题之一是模块之间的符号间的引用。
在编译器的时候对不知道的符号的地址,指令会设置成0或其他的值。
链接器怎么知道哪些指令需要修正呢?——编译以后的目标文件有重定位表,记录着每一个要被修正的地方(重定位入口)。
链接过程中还链接了一些必须的运行时库和启动文件,比如上面图中libc.a crt1.o等等
链接的过程会产生map文件 vs中,调试用。
链接过程
1.简单链接的例子
a.c和b.c分别生成a.o和b.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)第三步:因为第一步中的已经确定了合并段的地址VMA(text段和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字节开始,分别需要修正地址为符号shared和swap的地址。—— -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_offset和r_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个字节,
上述例子中:
变量shared是R_386_32,绝对近址寻址,修正后的地址就是符号的绝对地址((S+A),由于A是0,所以就是S,就是实际地址。shared在可重定位文件中指令的地址是0)
函数swap是R_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的长度)传递给第三个参数sizemovl %0, %%ecx
将指针str(%0指代str变量)传递给第二个参数buffmovl $0, %%ebx
将立即数0(默认终端的文件描述符为0)传递给第一个参数filedescmovl $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函数(剖析ELF文件格式的内容———文件头,段表,符号....(第三章)_elf文件解析_ivy_0709的博客-CSDN博客 中有 _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.so是c语言标准动态库
Lib.so就是我们创建的动态共享库 在运行时被加载进去了
ldd——查看程序或者共享库依赖哪些共享库:
后面的地址是装载地址
linux-vdso.so.1是一个内核虚拟共享对象,不存在于文件系统中
动态链接下的装载
可执行文件中的相关的段:剖析ELF文件格式的内容———文件头,段表,符号....(第三章)_elf文件解析_ivy_0709的博客-CSDN博客
动态链接下可执行文件中的装载:剖析ELF文件格式的内容———文件头,段表,符号....(第三章)_elf文件解析_ivy_0709的博客-CSDN博客
动态链接比静态链接慢的原因:
1)模块间的数据和函数调用要进行复杂的GOT定位,间接寻址。
2)动态链接器在程序开始时,寻找和装载共享对象,进行符号查找,重定位以解决模块之间的函数引用等。
针对原因2)进行优化,采用延时绑定(采用plt,见剖析ELF文件格式的内容———文件头,段表,符号....(第三章)_elf文件解析_ivy_0709的博客-CSDN博客),在函数第一次被用到的时候才进行绑定(即进行符号查找,重定位),用来加快程序的启动速度。
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下的共享库
待补充