linux系统编译链接总结--高级c/c++编译技术读后总结(上)

最近学习了《高级c/c++编译技术》,收获良多,现总结如下:
1.编译环节是将c或c++程序编译成二进制文件,在linux中一般为.o文件,每个源文件编译为一个.o文件。链接:是根据包含关系将.o文件组合起来,合并成一个二进制文件,该文件可为可执行文件,也可以为静态库或者动态库文件。该二进制文件会分为很多节,下面几个节是必须支持的:
代码节(.text):包含了供cpu执行的机器指令。
数据节: 包含初始化数据节(.data节),未初始化数据节(.bss(Block Started by Symbol))和只读数据节(.rdata).
堆:动态内存分配区域。
栈:为各个函数提供独立的存储空间。
全局的未初始化变量存在于.bss段中,具体体现为一个占位符;全局的已初始化变量存于.data段中;而函数内的自动变量都在栈上分配空间。.bss是不占用.exe文件空间的,其内容由操作系统初始化(清零);而.data却需要占用,其内容由程序初始化,因此造成了上述情况。
bss段(未手动初始化的数据)并不给该段的数据分配空间,只是记录数据所需空间的大小。
data(已手动初始化的数据)段则为数据分配空间,数据保存在目标文件中。 数据段包含经过初始化的全局变量以及它们的值。BSS段的大小从可执行文件中得到 ,然后链接器得到这个大小的内存块,紧跟在数据段后面。当这个内存区进入程序的地址空间后全部清零。包含数据段和BSS段的整个区段此时通常称为数据区。
当链接完成后,整个binary就已经包含了程序运行时的内存映射布局的细节,这也是虚拟地址空间的一个好处,每个进程都独占整个内存空间,所以每个binary布局的时候就相当于独占整个内存空间,不用考虑实际占用的物理地址。
链接器创建了二进制文件的整体框架,链接器要对编译器生成的二进制文件进行合并,然后向各个内存映射节填充信息(代码和数据等信息)。
进程内存映射的初始化工作是由程序装载器这一系统工具完成的,装载器会打开二进制文件,读取节的相关信息,然后将这些信息载入到内存映射结构中。
2.编译的各个阶段:
编译主要分为 预处理,语言分析阶段,汇编,优化,代码生成。
预处理:
a.将include包含的文件和头文件包含到源代码中。
b.将define语句制定的值转换为常量。
c.将代码中的宏转换为代码
d.根据#if,#elif #endif等,剔除不需要的语句。
gcc -i intput_file -o output_file.i .i文件就是预处理之后的文件
语言分析:
整理代码,去除注释和空格等,检查代码是否满足语言规则。
汇编阶段:
编译器将标准的语言转换成特定cpu指令集的语言集合。不同的cpu有不同的指令集,所以 不同的cpu需要不同的编译器。
gcc -S input_file -o output_file.s .s文件就是一个汇编指令集文件。
优化阶段:
对代码进行优化。
代码生成阶段:将汇编指令转换为对应的机器码,并写入目标的特定位置。
从上面可以看出,编译完成的.o文件为机器码的二进制文件,不是汇编指令文件。反汇编可以将机器码转换为汇编语句来方便人们阅读。objdump命令可以来反汇编。
gcc -c input_file -o output_file.o 编译成.o文件。.o文件就是二进制文件,可以利用objdump来反汇编,将.o文件反汇编为汇编代码
objdump -D -M intel intput_file.o//反汇编为intel格式的汇编代码
3.目标文件的属性:
a.符号(symbol)和节(section)是目标文件的基本组成部分。符号表示的是程序中的内存地址,节就是上面介绍的各种节,如bss等。
b.链接的时候就是将各个.o文件组合成一个目标文件(可执行程序或者库),最终生成的二进制文件包含了多个相同类型的节,而这些节就是从各个.o文件中拼接得到的,链接的时候会确定程序内存映射中每个独立节的实际地址范围(当然这个地址是虚拟地址)。
4.在linux中一般编译成elf文件,elf文件为可链接可执行文件(executable and linkable format)
4.链接:
链接器的最终任务是将独立的节组合成最终的程序内存映射节,与此同时解析所有引用,比如有些全局变量是从其他源文件中export来的,并不在本文件中定义,或者引用其他文件中的函数,所以需要链接的时候才能解析出来。解析的时候就会搜索拼接到程序内存映射中的节,找到这些引用,计算出其精确的地址,最后将机器指令中的伪地址替换成程序内存映射的实际地址,这样就完成了引用的解析。
5.装载:
装载的第一个作用就是将链接好的几个具有相同属性的节合并为段,第二个作用就是加载的时候计算各个段的地址和大小,然后得出各个段的内存地址的映射,但这个时候没有真正将可执行文件加载到内存中,而是等到运行的时候,需要哪个段,再加载哪个段。
6.程序执行入口点:
我们一般认为,程序执行的入口点为main函数,但实际上,在执行main函数之前,还是做了一些其他的工作,下面介绍一下程序的启动过程。
1.程序装载完成后,装载器会查询elf文件的e_entry字段的值,通过反汇编二进制文件,我们可以看到该值包含了代码(.text)节的首地址,而这个首地址就是_start函数的首地址。
2._start函数接下来会调用libc_start_main函数,并为其准备需要的参数。__libc_start_main函数首先会启动程序的线程,然后调用init函数,在调用main函数之前完成必要的初始化工作(gcc编译器会利用__attribute((constructor))关键字对程序启动前的自定义操作提供支持)。然后注册__fini()
和rtld_fini()函数,这些函数会在程序终止时被调用(gcc编译器会利用attribute((destructor))关键字对程序结束时自定义操作提供支持)。当所有操作完成后,__libc_start_main调用main函数,开始执行。

7.静态库和动态库:
静态库就是为了便于管理,将多个二进制文件打包成一个库,链接的时候,链接器会将用到的函数等拷贝出来,组合到可执行文件中。静态库也可以动态的管理,可以接包成各个二进制文件,也可以替换某个二进制文件,都有工具可以实现上面的操作。
动态库也叫共享库,被链接的时候,连接器只会检查程序所使用的符号是否存在,并不会去检查具体实现的代码节,也不会像静态库一样将用到的节链接到可执行文件中,这些工作都等到加载的时候才会去做。动态库只会在内存中加载一份。链接动态库的时候,连接器只会检查二进制文件中所需的符号是否都能在动态库中找到。一旦找到了所有的符号,链接器就完成了任务。运行的时候进行动态库的装载和符号的解析,首先找到动态库文件的位置,检查动态库中的函数符号与链接的时候是否一致,运行时需要将可执行程序的符号解析到正确的地址上,这个地址是动态库映射到进程空间的地址,还会将动态库加载到内存中。
静态库和动态库的区别:
1.静态库只需要编译,只是将编译的二进制文件打包为一个.a库,但动态库需要编译和链接,所以动态库更像可执行文件,其实动态库仅仅比可执行文件少了启动代码,像libc等动态库,是可以直接运行的。动态库还可以像可执行文件一样链接其他动态库。
2.当二进制可执行文件链接静态库时,不会把整个静态库的内容链接进去,只会链接目标文件中必要的符号。所以二进制可执行文件大小随着引入静态库中相关代码数量的增加而增加。
动态库链接的时候由于不会将库实际链接进来,所以二进制可执行文件的大小基本不增长,仅仅多一些字节来保存使用到的符号信息。但是运行时,加载器会把整个动态库加载到内存中。
3.有时候想像使用动态库一样使用静态库的内容,这时候就需要使用到中介动态库,就是将静态库的接口设计成一个动态库,实际的实现还是在静态库中。这时候链接的时候,由于动态库没有使用静态库中的内容,所以不会将静态库的内容链接到动态库中,这时候就需要使用–whole-archive链接器选项,此选项会将该选项后面列出的库全部强制链接到二进制文件中。比如:
gcc -fPIC source_files -wl, –whole-archive -lstatic_libraries -o share_lib_filename.
这个编译命令就会将static_libraries全部链接到share_lib_file动态库中。注意选项-wl,(字母wl逗号),当通过gcc或者g++间接调用链接器的时候,都需要该选项,该选项之后跟链接器选项。
4.使用静态库时,二进制文件会比较大,但是完全独立的,不依赖于其他文件,因为其包含了所有使用到的代码。使用动态链接时,二进制文件比较小,但是其运行时需要依赖动态库,所以需要保证其运行的系统上部署了所需要的动态库,比如放在/usr/lib/目录下。
5.二进制复用的出现改变了软件的交付方式,现在一般交付就是包含一组二进制文件和一组导出头文件的软件包。这样就可以不用交付源码,便于知识产权的保护。

八.创建及使用静态库:
1.创建linux静态库:
静态库其实就是将编译的二进制文件归档为一个库,所以使用ar归档工具来生成静态库。
gcc -c first.c second.c
ar rcs libstaticlib.a first.o second.o
ar还可以完成从库文件中删除一个或多个目标文件,从库中替换一个或多个目标文件,从库中提取一个或多个目标文件。
2.将静态库转换为动态库:
ar -x static_lib.a 将静态库的目标文件都解析到当前目录
然后调用链接器将目标文件构建成动态库。(命令后面有)
3.静态库链接到共享库时,要求静态库需要用-fPIC或者-mcmodel-large编译器选项来构建。
gcc -fPIC -c first.c second.c

九.设计动态库
1.创建动态库:
构建动态库至少需要两个编译器选项:-fPIC -shared
gcc -fPIC -c first.c second.c
gcc -shared first.o second.o -o libdynamic_lib.so
-fPIC: Position independent code,即位置无关代码,这个是支持动态链接的关键技术。所以编译动态库时建议都需要此选项,编译静态库时,如果静态库时链接到可执行文件中,可以指定,也可以不指定,但是如果是链接到动态库中,则必须指定。
2.设计动态库:
动态库构建时与运行时之间应用程序二进制接口(ABI)不变性是动态链接成功的最基本要求。

3.控制动态库符号的可见性
因为linux动态库默认所有的符号对外可见,所以我们要控制只有某些符号可见,可以在编译器选项中通过设置-fvisibility=hidden来将所有符号对外隐藏(makefile中CFLAGS += -fvisibility=hidden),然后在要对外可见的符号前面利用编译属性修饰符attribute ((visibility(“default”)))来对外可见。实例如下:print_message就会对外可见。

#define FOR_EXPORT __attribute__ ((visibility("default")))
void FOR_EXPORT print_message(vid);

另外还可以通过脚本文件传递给编译器控制符号的可见性。
还有一些粗暴的方法,比如strip工具可以直接修改.so文件,将某些符号去掉。
4.动态库的加载:
大部分动态库都是在运行的时候有系统自动加载,除此之外,动态库还有一个灵活的特性,就是可以通过dlopen(),dlsym()等接口,在程序运行时根据用户的输入或者是用户的配置来选择加载的库,很多软件的plugin就是使用这种特性。程序运行的时候根据用户的配置文件来选择要加载的库,非常的灵活。

十 定位库文件
1.linux中,动态库文件名为:
lib+library_name+.so+version_information
version information : M.m.p 即主版本号.次版本号.补丁
动态库的soname就是lib+library_name+.so+M。
例如:libz.so.1.2.3, 则soname则是libz.so.1
动态库的soname通常由链接器嵌入二进制的elf字段中,链接的时候有特定的链接器选项来传给链接器
gcc -shared xx.o -wl,-soname,libfoo.so.1 -o libfoo.so.1.0.0
2.linux构建过程中库文件定位规则详解:
linux中使用L和l选项来指定库文件的路径和名称
a.目录路径添加到-L链接器选项后面,并传递给链接器
b.库文件名添加到-l参数后面,传递给链接器。

gcc main.o -L../shared_lib_dir -lworking_demo -o demo (链接的过程)

使用一次性编译的命令:

gcc -Wall -fPIC main.cpp -wl,-L../shared_lib_dir -lworking_demo -o demo

对于静态库来说,库文件的目录和名称怎么组合都可以,也就是说可以-L../ -lshared_lib_dir/working_demo,链接的时候连接器也会找到该库,并将用到的内容链接到二进制文件中,可以正常运行,但是对于动态库,则会出现问题,对于动态库-L只会在链接阶段起作用,但-l的内容会写到elf文件中,等到执行时加载时,会根据-l的库的名称去特定的目录寻找该库(一般是/lib, /usr/lib/等目录),如果-l后面跟了一些链接的时候的一些目录名称,同时在/usr/lib等目录中有没有该文件夹,则运行加载的时候就会出问题。所以对于动态库,目录和文件名一定要完全隔开。
-rpath-link:这个也是用于“链接”的时候的,例如你显示指定的需要 FOO.so,但是 FOO.so 本身是需要 BAR.so 的,后者你并没有指定,而是 FOO.so 引用到它,这个时候,会先从 -rpath-link 给的路径里找。
这里要注意,当链接生成一个库的时候,他只会检测这个库所依赖的库是否存在,所以这里需要用-L来指定库的位置,但他不会去检测依赖的库的依赖,也就是比如a.so依赖于b.so, b.so依赖于c.so,当编译a.so时,链接器只会检测b.so是否存在,但不会去检测c.so是否存在。所以编译a.so的时候,只需要用-L来指定b.so的位置。但是如果这是有一个可执行程序A依赖于a.so。这时,我们光用-L来指定a.so的位置是不够的,因为编译链接可执行程序的时候,链接器会寻找该可执行程序所有用到的依赖,包括b.so和c.so,所以这里就需要用 -rpath-link来指定b.so和c.so的位置。所以,-rpath-link只对编译可执行程序有效,对库的编译没有作用。
3.linux运行时动态库文件的定位规则:
A.预加载库,但这种方式不符合标准规范,所以这里就先不介绍了
B.rpath和run_path:
rpath很早就支持了,但现在一般被runpath替代了。rpath在elf文件中叫DT_RPATH,存储了二进制文件相关的搜索路径。
rpath和runpath现在都是支持的,但runpath在运行时拥有更高的优先级。只有在runpath(DT_RUNPATH)缺失的情况下,rpath才是最高优先级的。当设置了runpath,则链接器自动忽略rpath。
我们通常使用-R或者-rpath选项向链接器传递rpath,runpath也使用同样的方法,但需要加上–enable-new-dtags选项。

gcc -wl,-R/home/milan/projects -lmilanlibrary     //设置rpath
gcc -wl,-R/home/milan/projects -wl,--enable-new-dtags -lmilanlibrary  //设置runpath。

一般设置runpath的时候,rpath也会被设置为同一value。
也可以用LD_RUN_PATH环境变量来指定rpath:

export LD_RUN_PATH=/home/milan/projects:$LD_RUN_PATH

LD_LIBRARY_PATH环境变量:
如果没有设置rpath,该环境变量优先级最高(比runpath高),可以利用该环境变量来设置搜索路径,一般用于临时的测试验证等,不用于正式部署。关于优先级的总结,见下面。

export LD_LIBRARY_PATH=/home/milan/projects:$LD_LIBRARY_PATH

可以用chrpath工具来修改elf文件的rpath的值。
C.ldconfig缓存机制
一种标准的代码部署过程是基于linux的ldconfig工具,通常安装包安装完成后,利用该工具将库目录写入/etc/ld.so.conf文件中。系统会扫描该文件,将新加入的内容加入动态搜索列表中,该列表维护在文件/etc/ld.so.cache中。
D.默认库文件路径
/lib和/usr/lib是linux的默认库文件路径。

E.以上各种方案的优先级
如果指定了runpath,即DT_RUNPATH字段非空:
1)LD_LIBRARY_PATH
2)RUNPATH
3)ld.so.cache
4)默认搜索路径 /lib /usr/lib
如果没有指定runpath:
1)被加载库的rpath,然后是二进制文件的rpath
2)LD_LIBRARY_PATH
3) ld.so.cache
4) 默认库搜索路径 /lib /usr/lib

4.ldd命令可以查看elf和库文件的链接关系

你可能感兴趣的:(编译原理)