Linux环境下的编译-链接与库的使用.doc_第1页
Linux环境下的编译-链接与库的使用.doc_第2页
Linux环境下的编译-链接与库的使用.doc_第3页
Linux环境下的编译-链接与库的使用.doc_第4页
Linux环境下的编译-链接与库的使用.doc_第5页
已阅读5页,还剩22页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

Linux环境下的编译,链接与库的使用From: /为什么使用ullib有时会出现 undefined reference error 的错误?为什么在动态链接库里ul_log会把日志输出到屏幕上?为什么用-static 编译有时候会报warning?我们在使用基础库或者第三方库的时候,经常遇到这样那样的问题,本文结合公司目前的主要环境,说明库的原理,使用的注意事项。从程序到可执行文件从hello world 说起include int main() printf(“hello worldn”); return 0; 上面这一段程序任何一个学过C语言的同学都是闭着眼睛都可以写出来,但是对于将这样一个源代码编译成为一个可执行文件的过程却不一定有所了解。 上面的程序如果要编译,很简单gcc hello.c 然后./a.out就可以运行,但是在这个简单的命令后面隐藏了许多复杂的过程一般来说,可以把这样的过程分成4个: 预编译, 编译, 汇编和链接。预编译:这个过程包括了下面的步骤宏定义展开,所有的#define 在这个阶段都会被展开 预编译命令的处理,包括#if #ifdef 一类的命令 展开#include 的文件,像上面hello world 中的stdio.h , 把stdio.h中的所有代码合并到hello.c中 去掉注释 gcc的预编译 采用的是预编译器cpp, 我们可以通过-E参数来看预编译的结果,如: gcc -E hello.c -o hello.i 生 成的 hello.i 就是经过了预编译的结果 在预编译的过程中不会太多的检查与预编译无关的语法(#ifdef 之类的还是需要检查, #include文件路径需要检查), 但是对于一些诸如 ; 漏掉的语法错误,在这个阶段都是看不出来的。 写过makefile的人都知道, 我们需要加上-Ipath 一系列的参数来标示gcc对头文件的查找路径小提示:1.在一些程序中由于宏的原因导致编译错误,可以通过-E把宏展开再检查错误 , 这个在编写 PHP扩展, python扩展这些大量需要使用宏的地方对于查错误很有帮助。2.如果在头文件中,#include 的时候带上路径在这个阶段有时候是可以省不少事情, 比如 #include , 这样在gcc的-I参数只需要指定一个路径,不会由于不小心导致,文件名正好相同出现冲突的麻烦事情. 不过公司由于早期出现了lib2和lib2-64两个目录, 以及头文件输出在include 目录下, 静态发布等一些历史原因, 有些时候使用带完整路径名的方式不是那么合适( 比如 #include 中间有一个include 显的很别扭). 不过个人认为所有的#include 都应该是尽量采用从cvs 根路径下开始写完整路径名的方式进行预编译的过程,只是受限于公司原有习惯和历史问题而显的不合适, 当然带路径的方式要多写一些代码,也是麻烦的事情, 路径由外部指定相对也会灵活一些.编译:这个过程才是进行语法分析和词法分析的地方, 他们将我们的C/C+代码翻译成为 汇编代码, 这也是一个编译器最复杂的地方使用命令gcc -S hello.i -o hello.s 可 以看到gcc编译出来的汇编代码, 现代gcc编译器一般是把预编译和编译合在一起,使用cc1 的程序来完成这个过程,在我们的开发机上有些时候一些同学编译大文件的时候可以用top命令看一个cc1的进程一直在占用时间,这个时候就是程序在执行编 译过程. 后面提到的编译过程都是指 cc1的处理包括了预编译与编译.汇编: 现在C/C+代码已经成为汇编代码了,直接使用汇编代码的编译器把汇编变成机器码(注意还不是可执行的) .gcc -c hello.c -o hello.o 这里的hello.o就是最后的机器码, 如果作为一个静态库到这里可以所已经完成了,不需要后面的过程.对于静态库, 比如ullib, COM提供的是libullib.a, 这里的.a文件其实是多个.o 通过ar命令打包起来的, 仅仅是为了方便使用,抛开.a 直接使用.o 也是一样的小提示:1. gcc 采用as 进行汇编的处理过程,as 由于接收的是gcc生成的标准汇编, 在语法检查上存在不少缺陷,如果是我们自己写的汇编代码给as去处理,经常会出现很多莫名奇妙的错误. 链接: 链接的过程,本质上来说是一个把所有的机器码文件组合成一个可执行的文件 上面汇编的结果得到一个.o文件, 但是这个.o要生成二执行文件只靠它自己是不行的, 它还需要一堆辅助的机器码,帮它处理与系统底层打交道的事情.gcc -o hello hello.o 这样就把一个.o文件链接成为了一个二进制可执行文件. 我们提供的各种库头文件在编译期使用,到了链接期就需要用-l, -L的方式来指定我们到底需要哪些库。 对于glibc中的strlen之类常用的东西编译器会帮助你去加上可以不需要手动指定。这个地方也是本文讨论的重点, 在后面会有更详细的说明小提示:有些程序在编译的时候会出现 “linker input file unused because linking not done” 的提示(虽然gcc不认为是错误,这个提示还是会出现的), 这里就是把 编译和链接 使用的参数搞混了,比如g+ -c test.cpp -I././ullib/include -L././ullib/lib/ -lullib 这样的写法就会导致上面的提示, 因为在编译的过程中是不需要链接的, 它们两个过程其实是独立的静态链接链接的过程 这里先介绍一下,链接器所做的工作,其实链接做的工作分两块: 符号解析和重定位符号解析符号包括了我们的程序中的被定义和引用的函数和变量信息在命令行上使用 nm ./testtest 是用户的二进制程序,包括可以把在二进制目标文件中符号表输出00000000005009b8 A bss_start00000000004004cc t call_gmon_start00000000005009b8 b completed.10000000000500788 d CTOR_END0000000000500780 d CTOR_LIST00000000005009a0 D data_start00000000005009a0 W data_start0000000000400630 t do_global_ctors_aux00000000004004f0 t do_global_dtors_aux00000000005009a8 D dso_handle0000000000500798 d DTOR_END0000000000500790 d DTOR_LIST00000000005007a8 D DYNAMIC00000000005009b8 A edata00000000005009c0 A end0000000000400668 T fini0000000000500780 A fini_array_end0000000000500780 A fini_array_start0000000000400530 t frame_dummy0000000000400778 r FRAME_END0000000000500970 D GLOBAL_OFFSET_TABLE w gmon_start U gxx_personality_v0CXXABI_1.30000000000400448 T _init0000000000500780 A _init_array_end 当然上面由nm输出的符号表可以通过编译命令去除,让人不能直接看到。链接器解析符号引用的方式是将每一个引用的符号与其它的目标文件(.o)的符号表中一个符号的定义联系起来,对于那些和引用定义在相同模块的本地符号(注:static修饰的),编译器在编译期就可以发现问题,但是对于那些全局的符号引用就比较麻烦了下面来看一个最简单程序:includeint foo();int main() foo(); return 0; 我们把文件命名为test.cpp, 采用下面的方式进行编译 g+ -c test.cppg+ -o test test.o 第一步正常结束,并且生成了test.o文件,到第二步的时候报了如下的错误test.o(.text+0x5): In function main: undefined reference tofoo()collect2: ld returned 1 exit status 由于foo 是全局符号, 在编译的时候不会报错,等到链接的时候,发现没有找到对应的符号,就会报出上面的错误。但是如果我们把上面的写法改成下面这样include/注意这里的static static int foo();int main() foo(); return 0; 在运行 g+ -c test.cpp, 马上就报出下面的错误:test.cpp:19: error: int foo() used but never defined 在编译器就发现foo 无法生成目标文件的符号表,可以马上报错,对于一些本地使用的函数使用static一方面可以避免符号污染,另一方面也可以让编译器尽快的发现错误在我们的基础库中提供的都是一系列的.a文件,这些.a文件其实是一批的目标文件(.o)的打包结果这样的目的是可以方便的使用已有代码生成的结果,一般情况下是一个.c/.cpp文件生成一个.o文件,在编译的时候如果带上一堆的.o文件显的很不方便,像:g+ -o main main.cpp a.o b.o c.o 这样大量的使用.o也很容易出错,在linux下使用archive来将这些.o存档和打包所以我们就可以把编译参数写成g+ -o main main.cpp ./libullib.a 我们可以使用./libullib.a 直接使用libullib.a这个库,不过gcc提供了另外的方式来使用:g+ -o main main.cpp -L./ -lullib -L指定需要查找的库文件的路径, -l 选择需要使用的库名字,不过库的名字需要用lib+name的方式命名,才会被gcc认出来 不过上面的这种方式存在一个问题就是不区分动态库和静态库,这个问题在后面介绍动态库的时候还会提到当存在多个.a ,并且在库之间也存在依赖关系,这个时候情况就比较复杂如果我们要使用lib2-64/dict, dict又依赖ullib, 这个时候需要写成类似下面的形式g+ -o main main.cpp -L./lib2-64/dict/lib -L./lib2-64/ullib/lib -ldict -lullib -lullib 需要写在-ldict的后面,这是由于在默认情况对于符号表的解析和查找工作是由后往前(内部实现是一个类似堆栈的尾递归) 所以当所使用的库本身存在依赖关系的时候,越是基础的库就越是需要放到后面否则如果上面把-ldict -lulib的位置换一下,可能就会出现 undefined reference toxxx 的错误 一般来说对于基础库的依赖关系可以在平台上获取, 若存在一些第三方的依赖,就只有参考相关的帮助说明了当然gcc提供了另外的方式的来解决这个问题g+ -o main main.cpp -L./lib2-64/dict/lib -L./lib2-64/ullib/lib-Xlinker “-(” -ldict -lullib-Xlinker “-)” 可以看到我们需要的库被-Xlinker “-(“和-Xlinker “-)” 包含起来,gcc在这里处理的时候会循环自动查找依赖关系,不过这样的代价就是延长gcc的编译时间,如果使用的库非常的多时候,对编译的耗时影响还是非常大.-Xlinker有时候也简写成”-Wl, “,它的意思是 它后面的参数是给链接器使用的-Xlinker 和 -Wl 的区别是一个后面跟的参数是用空格,另一个是用”,”我们通过nm命令查看目标文件,可以看到类似下面的结果/lib2-64/dict/lib/x.html 1 0000000000009740 T Z11ds_syn_loadPcS 2 0000000000009c62 T Z11ds_syn_seekP16Sdict_search_synPcS1_i 3 0000000000007928 T Z11dsur_searchPcS_S 4 &nbs p; U Z11ul_readfilePcS_Pvi 5 &nbs p; U Z11ul_writelogiPKcz 6 00000000000000a2 T Z12creat_sign32Pc其中用U标示的符号_Z11ul_readfilePcS_Pvi(其实是ullib中的 ul_readfile) ,表示在dict的目标文件中没有找到ul_readfile函数在链接的时候,链接器就会去其他的目标文件中查找_Z11ul_readfilePcS_Pvi的符号小提示:编译的时候采用-Lxxx -lyyy的形式使用库,-L和-l这个参数并没有配对的关系,我们的一些Makefile 为了维护方便把他们写成配对的形式,给一些同学造成了误解 其实我们完全可以写成-Lpath1, -Lpath2, -Lpath3, -llib1 这样的形式在具体链接的时候,gcc是以.o文件为单位, 编译的时候如果写g+ -o main main.cpp libx.o 那么无论main.cpp中是否使用到libx.o,libx.o中的所有符号都会被载入到main函数中但是如果是针对.a,写成g+ -o main main.cpp -L./ -lx, 这个时候gcc在链接的时候只会链接有被用到.o, 如果出现libx.a中的某个.o文件中没有任何一个符号被main用到,那么这个.o就不会被链接到main中gcc编译.c文件的时候和g+ 有一个不一样的地方, 就是在g+ 中对于一个函数必须要先定在再使用,比如上面的例子中需要先定义foo()才能被使用,但对于gcc编译的.c(如果是.cpp会自动换成C+编译) 文件, 可以不需要先定义, 而直接使用. 但这样会出现问题, 如果没有其他地方使用和这个函数同名的函数那么链接的时候会找不到这个函数. 但是如果碰巧在另外的地方存在一个同名函数,那么链接的时候就会被直接连接到这个函数上, 万一使用的时候偏偏传入参数或返回值的类型不对,那么这个时候就可能出现莫名奇妙的错误. 不过我们还是可以用-Wmissing-declarations参数打开这个检查重定位经过上面的符号解析后,所有的符号都可以找到它所对应的实际位置(U表示的链接找到具体的符号位置)as 汇编生成一个目标模块的时候,它不知道数据和代码在最后具体的位置,同时也不知道任何外部定义的符号的具体位置,所以as在生成目标代码的时候,对于位置未知的符号,它会生成一个重定位表目,告诉链接器在将目标文件合并成可执行文件时候如何修改地址成最终的位置g+和gcc 采用gcc 和g+ 在编译的时候产生的符号有所不同在C+中由于要支持函数重载,命名空间等特性,g+会把函数参数(可能还有命名空间),把函数命变成一个特殊并且唯一的符号名例如:int foo(int a); 在gcc编译后,在符号表中的名字就是函数名foo, 但是在g+编译后名字可能就变成了_Z3fooi, 我们可以使用c+filt命令把一个符号还原成它原本的样子,比如c+filt _Z3fooi 运行的结果可以得到foo(int)由于在C+和纯C环境中,符号表存在不兼容问题,程序不能直接调用C+编译出来的库,C+程序也不能直接调用C编译出来的库为了解决这个问题C+中引入了extern “C”的方式extern “C” int foo(int a); 这样在用g+编译的时候, c+的编译器会自动把上面的 int foo(int a)当做C的接口进行符号转化这样在纯C里面就可以认出这些符号不过这里存在一个问题,extern “C” 是C+支持的,gcc并不认识,所有在实际中一般采用下面的方式使用c+#ifdef cplusplus extern “C” #endifint foo(int a); #ifdef cplusplus #endif这样这个头文件中的接口即可以给gcc使用也可以给g+使用, 当然在extern “C” 中的接口是不支持重载,默认参数等特性在我们的64位编译环境中如果有gcc的程序,使用上面方式g+编译出来的库,需要加上-lstdc+, 这是因为,对于我们位环境下g+编译出来的库,需要使用到一个gxx_personality_v0的符号,它所在的位置是/usr /lib64/libstdc+.so.6 (C+的标准库iostream都在里面,C+程序都需要的). 但是在我们的32位2.96 g+编译器中是不需要gxx_personality_v0,所有编译可以不加上 -lstdc+小提示:在linux gcc 中,只有在源代码使用.c做后缀,并且使用gcc编译才会被编译成纯C的结果,其他情况像g+编译.c文件,或者gcc 编译.cc, .cpp文件都会被当作C+程序编译成C+的目标文件, gcc和g+唯一的不同在于gcc不会主动链接-lstdc+ 在 extern “C” 中如果存在默认参数的接口,在g+编译的时候不会出现问题,但是gcc使用的时候会报错因为对于函数重载,接口的符号表还是和不用默认参数的时候是 一样的 编译器版本问题 目前公司内部使用的gcc版本主要分两种32位gcc 2.96 64位 gcc 3.4.4 (这是编译机的版本号,我们的开发机多数是gcc 3.4.5, 小版本号的差异,目前看来不会对程序会带来影响) 有时候在32位环境中经常会出现undefined reference error”的错误,这个问题多数是由于 gcc 的版本问题造成的,我们许多的32位机器上的编译器都是3.x的版本,gcc 从2到3做了很大的改动,c+的符号表的表现有所区别,导致gcc3的编译器不能链接由gcc2.96编译出来的库我们的基础库在lib2下的都是采用静态发布(直接发布最后的二进制库,而不是在需要的时候重新编译)不过在gcc3的glibc中考虑了向下兼容性使的可以正常运行由gcc 2.96上编译出来的二进制程序 我们现在有一种方式是在gcc2.96环境下编译出来的二进制程序放到64位机器上去运行,如果我们是一个新的 64位机器环境上运行程序,实际上这是无法运行的,我们的程序之所以可以这样做,是由于在我们的位机器上装上32位程序运行的环境,包括载入32位程 序的载入器,对应的各种动态库,可以在64位机器上/usr/lib/rh80目录下看所使用各种动态库,不过这些库的版本与我们的开发机编译机上版本有 所不同,有些时候我们会发现如果64位机器上的32位程序运行出core, 把core文件放到开发机上进行调试会看到出现在glibc的动态库的函数都core在一些很奇怪的位置,根本不是我们程序中调用的位置,这里很重要的原因就在于动态库的版本不一样符号表冲突 我们在编译程序的时候时常会遇到类似于multiple definition of foo() 的错误这些错误的产生都是由于所使用的.o文件中存在了相同的符号造成的比如:libx.cppint foo() return 30; liby.cppint foo() return 20; 将libx.cpp, liby.cpp编译成libx.o和liby.o两个文件g+ -o main main.cpp libx.o liby.o 这个时候就会报出multiple definition of foo()的错误但是如果把libx.o和liby.o分别打包成libx.a和liby.a用下面的方式编译g+ -o main main.cpp -L./ -lx -ly 这个时候编译不会报错,它会选择第一个出现的库,上面的例子中会选择libx中的foo. 但是注意不是所有的情况都是这样的,由于链接是以.o为单位的,完全可以不用某个.o的时候才不会出错误,否则依然会出现multipe的错误, 这种情况下的建议是查看一下这些函数的行为是什么样子,是否是一致的,如果不一致,还是想办法规避, 如果是一致的话可以用 -Wl,allow-multiple-definition 强制编译过去,这样会使用第一个碰到的库,但不推荐这样做.可以通过g+ -o main main.cpp -L./ -lx -ly-Wl,trace-symbol=_Z3foov的命令查看符号具体是链接到哪个库中,g+ -o main main.cpp -L./ -lx -ly-Wl,cref可以把所有的符号链接都输出(无论是否最后被使用) 小提示:对于一些定义在头文件中的全局常量,gcc和g+有不同的行为,g+中const也同时是static的,但gcc不是例如: foo.h 中存在一个constint INTVALUE = 2000; 的全局常量有两个库 a和b, 他们在生成的时候有使用到了INTVALUE,如果有一个程序main同时使用到了a库和b库,在链接的时候gcc编译的结果就会报错,但如果a和b都是g+编译的话结果却一切正常这个原因主要是在g+中会把INTVALUE 这种const常量当做static的,这样就是一个局部变量,不会导致冲突,但是如果是gcc编译的话,这个地方INTVALUE会被认为是一个对外的全局常量是非static的,这个时候就会造成链接错误小提示 上说了对于a库和b库出现同样符号的情况会有冲突, 但是在实际中有这么一种情况, a库定义的foo的接口,在有b库的情况下是一种行为,在没有b库的情况下又想要一种行为。为解决这个问题引入了弱连接的机制, 前面我们看到nm后,有些符号前面有T标志,这个表示的是这个符号是一个强连接。 如果看有W的表示,那么就表示这个符号是弱连接。如果有一个同名的库也有相同的符号并且是强连接,那么就可以替代掉他(如果也是弱连接,会存在先后顺序用 谁的问题)。 glibc中的符号都是弱连接, 我们可以在我们的程序中编写 open, write之类的函数去替换掉glibc中的实现。如果我们要自己写弱连接的函数可以采用gcc扩展attribute(weak) const int func();来表示一个符号是弱连接/lib2-64/dict/lib/x.html 1 0000000000009740 T Z11ds_syn_loadPcS 2 0000000000009c62 T Z11ds_syn_seekP16Sdict_search_synPcS1_i 3 0000000000007928 T Z11dsur_searchPcS_S 4 &nbs p; U Z11ul_readfilePcS_Pvi 5 &nbs p; U Z11ul_writelogiPKcz 6 00000000000000a2 T Z12creat_sign32Pc动态链接对于静态库的使用,有下面两个问题当我们需要对某一个库进行更新的时候,我们必须把一个可执行文件再完整的进行一些重新编译 在程序运行的时候代码是会被载入机器的内存中,如果采用静态库就会出现一个库需要被copy到多个内存程序中,这个一方面占用了一定的内存,另一方面对于 的cache不够友好 链接的控制,从前面的介绍中可以看到静态库的连接行为我们不好控制,做不到在运行期替换使用的库,编译后的程序就是二进制代码,有些代码它们涉及到不同的机器和环境,假设在A 机器上编译了一个程序X, 把它直接放到B机器上去运行,由于A和B环境存在差异,直接运行X程序可能存在问题,这个时候如果把和机器相关的这部分做成动态库C,并且保证接口一致, 编译X程序的时候只调用C的对外接口对于一般的用户态的X程序而言,就可以简单的从环境放到环境中但如果是静态编译,就可能做不到这点,需要在机器上重新编译一次动态链接库在linux被称为共享库(shared library,下文提到的共享库和动态链接库都是指代shared library),它主要是为了解决上面列出静态库的缺点而提出的目前在公司内部许多产品线也开始逐步采用这种方式。 共享库的使用 共享库的使用主要有两种方式,一种方式和.a的静态库类似由编译器来控制,其实质和二进制程序一样都是由系统中的载入器(ld-linux.so)载入, 另一种是写在代码中,由我们自己的代码来控制还是以前面的例子为例:g+ -shared -fPIC -o libx.so libx.cpp 编译的时候和静态库类似,只是加上了 -shared 和 -fPIC, 将输出命名改为.so然后和可执行文件链接.a一样,都是g+ -o main main.cpp -L./ -lx 这样main就是调用 libx.so, 在运行的时候可能会出现找不到libx.so的错误,这个原因是由于动态的库查找路径的问题,动态库默认的查找路径是由/etc /ld.so.conf文件来指定,在运行可执行文件的时候,按照顺会去这些目录下查找需要的共享库。我们可以通过 环境变量 LD_LIBRARY_PATH来指定共享库的查找路径(注:LD_LIBRARY_PATH的优先级比ld.so.conf要高).命令上运行 ldd ./main 我们可以看到这个二进制程序在运行的时候需要使用的动态库,例如: libx.so = /home/bnh/tmp/test/libx.so (0x003cb000) libstdc+.so.6 = /usr/lib/libstdc+.so.6 (0x00702000) libm.so.6 = /lib/tls/libm.so.6 (0x00bde000) libgcc_s.so.1 = /lib/libgcc_s.so.1 (0x00c3e000) libc.so.6 = /lib/tls/libc.so.6 (0x00aab000)这里列出了main所需要的动态库, 如果有看类似 libx.so=no found的错误,就意味着路径不对,需要设置LD_LIBRARY_PATH来指定路径小提示: 有一个特殊的环境变量LD_PRELOAD, 可以强行替换共享库中运行的符号。 export LD_PRELOAD= “xxx.so”, 如果你程序运行过程中遇到了和xxx.so同名的符号,这个时候程序会使用到xxx.so中的符号手动载入共享库 除了采用类型于静态库的方式来使用动态库,我们还可以通过由代码来控制动态库的使用。这种方式允许应用程序在运行时加载和链接共享库,主要有下面的四个接口载入动态链接库 void dlopen(constchar filename, int flag); 获取动态库中的符号 void dlsym(void handle, char symbol); 关闭动态链接库 void dlclose(void handle); 输出错误信息 constchar *dlerror(void); 看下面的例子:typedefint foo_t();foo_t * foo = (foo_t*) dlsym(handle, “foo”); 通过上面的方式我们可以载入符号”foo”所对应的地址,然后通过强制类型转换给一个函数指针,当然这里函数指针的类型需要和符号的原型类型保持一致,这些一般是由共享库所对应的头文件提供这 里要注意一个问题,在dlsym中载入的符号表示是和我们使用nm 库文件所看到符号表要保持一致,这里就有一个前面提到的 gcc和g+符号表的不同,一个 int foo(), 如果是g+编译,并且没有extern “C”导出接口,那么用dlsym载入的时候需要用dlsym(handle, “_Z3foov”) 方式才可以载入函数 int foo(),所以建议所以的共享库对外接口都采用extern “C”的方式导出 纯C接口对外使用,这样在使用上也会比较方便dlopen 的flag 标志可以选择RTLD_GLOBAL , RTLD_NOW, RTLD_LAZY. RTLD_NOW, RTLD_LAZY只是表示载入的符号是一开始就被载入还等到使用的时候被载入,对于多数应用而言没有什么特别的影响这两个标志都可以通过| 和RTLD_GLOBAL一起连用这里主要是说明RTLD_GLOBAL的功能,考虑这样的一个情况:我们有一个 main.cpp ,调用了两个动态libA, 和libB,假设A中有一个对外接口叫做testA,在main.cpp可以通过dlsym获取到testA的指针,进行使用但是对于libB 中的接口,它是看到不libA的接口,使用testA 是不能调用到libA中的testA的,但是如果在dlopen 打开libA.so的时候,设置了RTLD_GLOBAL这个选项,就可以把libA.so中的接口升级为全局可见, 这样在libB中就可以直接调用libA中的testA,如果在多个共享库都有相同的符号,并且有RTLD_GLOBAL选项,那么会优先选择第一个。另 外这里注意到一个问题,RTLD_GLOBAL使的动态库之间的对外接口是可见的,但是动态库是不能调用主程序中的全局符号,为了解决这个问题, gcc引入了一个参数-rdynamic,在编译载入共享库的可执行程序的时候最后在链接的时候加上-rdynamic,会把可执行文件中所有的符号变成 全局可见,对于这个可执行程序而言,它载入的动态库在运行中可以直接调用主程序中的全局符号,而且如果共享库(自己或者另外的共享库 RTLD_GLOBAL) 加中有同名的符号,会选择可执行文件中使用的符号,这在一些情况下可能会带来一些莫名其妙的运行错误。小提示:/usr/sbin/lsof -p pid 可以查看到由pid在运行期所载入的所有共享库 共享库无论是通过dlopen方式载入还是载入器载入,实质都是通过 mmap的方式把共享库映射到内存空间中去。mmap的参数MAP_DENYWRITE可以在修改已经被载入某个进程文件的时候阻止对于内存数据的修改, 由于现在内核中已经禁用这个参数,直接导致的结果就是如果对mmap的文件进行修改,这个时候的修改会被直接反映到已经被mmap映射的空间上。由于内核 的不支持,使得共享库不能在运行期进行热切换,共享库在更新的时候需要由载入的程序通过一些外部的方式来判断,主动使用dlclose,并且dlopen 重新载入共享库,如果是载入器载入那么需要重启程序。另外这里的热切换指的是直接copy覆盖原有的共享库,如果是采用mv或者软连接的方式那么还是安全 的,共享库被mv后不会影响原来的已经载入它的程序。 g+ 加上 -rdynamic 参数实质上相当于ld链接的时候加上-E或者export-dynamic参数,效果与g+ -Wl,-E或者g+ -Wl,export-dynamic的效果是一样的。静态库和动态库的混合编译 目前我们多数的库都是以静态库的方式提供,但是现在有许多地方出于运维和升级的考虑使用了许多动态链接库,这样不可避免的出现了大量的静态库与动态库的混合使用,经常会出现一些奇怪的错误,使用的时候需要有所关注对于一般情况下,只要静态库与共享库之间没有依赖关系,没有使用全局变量(包括static变量),不会出现太多的问题,下面以出现的问题作例子来说明使用的注意事项。baidugz与zlib的冲突具体的说明可以参看wiki LibBaidugz baidugz 是百度早期用来解压压缩网页,可以自动识别多数的网页压缩格式具有一定的容错性,但是由于baidugz是早期zlib版本直接修改而来,出现与系统中版本不一致的时候就可能导致问题。在 /usr/lib64/ 下可以看到 libz.so, 我们在直接使用系统zlib的时候多是在链接的时候加上 -lz 就可以了。程序在运行的时候会直接到系统的目录下去寻找libz.so,并且在运行期被载入。早 期的zlib代码中有一部分函数和变量,虽然没有通过zlib.h对外公开,但是还是采用了extern的方式被其他的.c文件使用(这里涉及到一个问题 就是一个源码中的变量或接口要被同一个库中其它地方使用,只能被extern,但extern 后就意味着可以被其它任意使用这个库的程序看到和使用, 无论是否在对外接口中声明), 还有个别接口可以使用static但没有使用static。 这部分对内公开(实际上对外也公开了)的接口, 在baidugz的修改过程中没有被修改,在后来升级64位版本的时候,由于系统中的zlib与baidugz使用的zlib相差过大,zlib在本身的 升级过程中也没有过多的考虑这个问题(它假设不会有并存的情况), 导致在链接的过程出现错误.在编写动态库的过程中,可以static的函数即使没有暴露在头文件也需要尽量static,避免和外界冲突。那种没有对外公开接口就无所谓加不加static的观点是存在一定风险的.小提示:有 些程序使用 using namespace 这样的匿名命名空间来规避冲突的问题,从编译器角度而言,在代码中使用确实不会产生冲突。 不过采用dlopen的方式却还是可以通过强制获取符号的方式运行在共享库中使用using namespace 包含起来的函数,但static的函数是不能被dlopen方式强制获取的。地址无关代码在64位下编译动态库的时候,经常会遇到下面的错误/usr/bin/ld: /tmp/ccQ1dkqh.o: relocation R_X86_64_32 against a local symbol can not be used when making a shared object; recompile with -fPIC 提示说需要-fPIC编译,然后在链接动态库的地方加上-fPIC的参数编译结果还是报错,需要把共享库所用到的所有静态库都采用-fPIC编译一边才可 以成功的在64位环境下编译出动态库。这里的-fPIC指的是地址无关代码这里首先先说明一下装载时重定位的问题,一个程序如果没有用到任何动态库,那么由于已经知道了所有的代码,那么装载器在把程序载入内存的过程中就可以直接安装静态库在链接的时候定好的代码段位置直接加载进内存中的对应位置就可以了。但是在面对动态的库的时候 ,这种方式就不行了。假设需要载入共享库A,但是在编译链接的时候使用的共享库和最后运行的不一定是同一个库,在编译期就没办法知道具体的库长度,在链接的时候就没办法确定它或者其他动态库的具体位置。另一个方面动态库中也会用到一些全局的符号,这些符号可能是来自其他的动态库,这在编译器是没办法假设的 (如果可以假设那就全是静态库了)基于上面的原因,就要求在载入动态库的时候对于使用到的符号地址实现重定位。在实现上在编译链接的时候不做重定位操作,地址都采用相对地址,一但到了需要载入的时候,根据相对地址的偏移计算出最后的绝对地址载入内存中。但是这种采用装载时重定位的方式存在一个问题就是相同的库代码(不包括数据部分)不能在多个进程间共享(每个代码都放到了它自己的进程空间中),这个失去了动态库节省内存的优势。为了解决这个问题,ELF中的做法是在数据段中建立一个指向那些需要被使用(内部的位置无关简单采用相对地址访问就可以实现)的地址列表(也被称为全局偏移表,Global offset table, GOT). 可以通过GOT相对应的位置进行间接引用.对于我们的32位环境来说, 编译时是否加上-fPIC, 都不会对链接产生影响, 只是一份代码的在内存中有几个副本的问题(而且对于静态库而言结果都是一样的).但在64位的环境下装载时重定位的方式存在一个问题就是在我们的64位环 境下用来进行位置偏移定位的cpu指令只支持32位的偏移, 但实际中位置的偏移是完全可能超过64位的,所以在这种情况下编译器要求用户必须采用fPIC的方式进行编译的程序才可以在共享库中使用从理论上来说-fPIC由于多一次内存取址的调用,在性能上会有所损失.不过从目前的一些测试中还无法明显的看出加上-fPIC后对库的性能有多大的损失,这个可能和我们现在使用的机器缓存以及大量寄存器的存在相关.小提示:-fPIC与-fpic 上面的介绍可以看到,gcc要使用地址无关代码加上-fPIC即可,但是在gcc的手册中我们可以看到一个-fpic(区别在一个大写一个小写)的参数, 从功能上来说它们都是一样的。-fpic在一些特定的环境中(包括硬件环境)可以有针对性的进行优化,产生更小更快的代码, 但是由于受到平台的限制,像我们的编译环境,开发环境,运行环境都不完全统一的情况下面使用fpic有一定未知的风险,所有决大多数情况下我们使用 -fPIC来产生地址无关代码。 共享内存效率 共享内存在只读的情况下性能和读普通内存是一样的(如果不算第一载入的消耗),而且由于是多个进程共享对cpu cache还显的相对友好。 可以参见mmap性能 同时存在静态库和动态库 前 面提到编译动态库的时候有提到编译动态库可以像编译静态库那样采用-Lpath -lxx的方式进行, 但这里存在一个问题,如果在path目录下既有动态库又有静态库的时候的行为又是什么样地? 事实上在这种情下, 链接器优先选择采用动态库的方式进行编译.比如在同一目录下存在 libx.a 和 libx.so, 那么在链接的时候会优先选择libx.so进行链接. 这也是为什么在com组维护的第三方库(third, third-64)中绝大多数库的产出物中只有.a的存在, 主要就是为了避免在默认情况下使用到.so的库, 导致在上线的时候出现麻烦(特别是一些系统中存在,但又与我们需要使用的版本有出入的库).为了能够控制动态库和静态库的编译, 有下面的几种方式直接使用要编译的库在前面也提到了在编译静态库的时候有三种方式: 目标文件.o 直接使用 静态库文件.a 直接编译 采用 -L -l方式进行编译 编译的时候如果不采用-Lpath -lxx的方式进行编译, 而且直接写上 path/libx.a 或者 path/libx.so 进行编译,那么在链接的时候就是使用我们指定的 .a 或者 .so进行编译不会出现 所谓的动态库优先还是静态库优先的问题. 但这个方案需要知道编译库的路径,一些情况下并不适合使用。 static参数在gcc的编译的时候加上static参数, 这样在编译的时候就会优先选择静态库进行编译,而不是按照默认的情况选择动态库进行编译.不过使用static参数会带来另外的问题,不推荐使用,主要会带来下面的问题如果只有动态库,而不存在同名的静态库,链接的时候也不会报错,但在运行的时候可能会出现错误 /lib/ld64.so.1: bad ELF interpreter: 由于我们程序本身在运行的需要系统中一些库的支持,包括libc, libm, phtread等库,在采用static编译方式之后,链接的就是这些库的静态编译版本(glibc还是提供了静态编译的版本),我们等于使用的是编 译机上的库,但是我们的运行环境可能和编译机有所不同,glibc这些动态库的存在本身的目的就是为了能让在一台机器上编译好的库能够比较方便的移到另外 的机器上,程序本身只需要关注接口,至于从接口到底层的部分由每台机器上的.so来处理不过这个问题也不是那么绝对,在一些特殊情况下(比如 glibc, gcc存在大版本差异的时候,主要是gcc2到gcc3有些地方没有做好,abi不兼容的问题比较突出,真遇到这些情况其实需要换编译器了) static编译反倒可以正常的运行但是还是不推荐使用, 这些是可以采用其它方法规范在后面的第6点中有说明另外就是glibc static编译可能会产生下面的warning: warning: Using getservbyport_r in statically linked applications requires at runtime the shared libraries from the glibc version used for linking 这个主要原因是由于getservbyport_r这样的接口还是需要动态库的支持才可以运行,许多glibc的函数都存在这样的问题, 特别是网络编程的接口中是很常见的对一些第三方工具不友好,类似valgrind检查内存泄露为了不在一些特殊的情况下误报(最典型的就是strlen可以参考valgrind的 wikiValgrind运行的程序不能够使用-static来进行链接中的case3), 它需要用动态库的方式替换glibc中的函数,如果静态编译那么valgrind就无法替换这些函数,产生误报甚至无法报错 tcmalloc在这种情况下也不能支持. 我们目前64位环境中使用的pthread库,如果是使用的是动态库那么采用的是ntpl库,如果是静态库采用的linuxthread库,使用 static 会导致性能下降(可以参考32/64位性能调研) static之后会导致代码大小变大,对cpu代码cache不友好,浪费内存空间,不过对于小代码问题也不大 早期使用static的一个原因是需要使用一些第三方面库, 但是最后

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论