Linux环境下的编译,链接与库的使用

来源:互联网 发布:matlab怎么读取usb数据 编辑:程序博客网 时间:2024/05/23 01:24

为什么使用ullib有时会出现 undefined reference error 的错误?

为什么在动态链接库里ul_log会把日志输出到屏幕上?

为什么用-static 编译有时候会报warning?

我们在使用基础库或者第三方库的时候,经常遇到这样那样的问题,本文结合公司目前的主要环境,说明库的原理,使用的注意事项。

从程序到可执行文件

从hello world 说起

includeint 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扩展这些大量需要使用宏的地方对于查错误很有帮助。

  1. 如果在头文件中,#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 ./test

test 是用户的二进制程序,包括

可以把在二进制目标文件中符号表输出

00000000005009b8 A bss_start00000000004004cc t call_gmon_start00000000005009b8 b completed.10000000000500788 dCTOR_END0000000000500780 d CTOR_LIST00000000005009a0 D data_start00000000005009a0 W data_start0000000000400630 tdo_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 DGLOBAL_OFFSET_TABLE w gmon_start U gxx_personality_v0@@CXXABI_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 to xxx 的错误. 一般来说对于基础库的依赖关系可以在平台上获取, 若存在一些第三方的依赖,就只有参考相关的帮助说明了

当然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 TZ12creat_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中的所有符号都会被载入到mian函数中.但是如果是针对.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编译出来的库.为了解决这个问题C++中引入了 extern “C” 的方式.

extern “C” int foo(int a); 这样在用g++编译的时候, c++的编译器会自动把上面的 int foo(int a)当做C的接口进行符号转化.这样在纯C里面就可以认出这些符号.

不过这里存在一个问题,extern “C” 是C++支持的,gcc并不认识,所有在实际中一般采用下面的方式使用++

ifdef cplusplus extern “C” { #endifint foo(int a); #ifdef cplusplus } #endif

这样这个头文件中的接口即可以给gcc使用也可以给g++使用, 当然在extern “C” { } 中的接口是不支持重载,默认参数等特性

在 我们的64位编译环境中如果有gcc的程序使用上面方式g++编译出来的库,需要加上-lstdc++, 这是因为,对于我们64位环境下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位机器环境上运行程序,实际上这是无法运行的,我们的程序之所以可以这样做,是由于在我们的64位机器上装上32位程序运行的环境,包括载入32位程 序的载入器,对应的各种动态库,可以在64位机器上/usr/lib/rh80目录下看所使用各种动态库,不过这些库的版本与我们的开发机编译机上版本有 所不同,有些时候我们会发现如果64位机器上的32位程序运行出core, 把core文件放到开发机上进行调试会看到出现在glibc的动态库的函数都core在一些很奇怪的位置,根本不是我们程序中调用的位置,这里很重要的原 因就在于动态库的版本不一样

符号表冲突 我们在编译程序的时候时常会遇到类似于

multiple definition of `foo()’ 的错误.

这些错误的产生都是由于所使用的.o文件中存在了相同的符号造成的.

比如:

libx.cpp

int foo() { return 30; } liby.cpp

int 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 TZ12creat_sign32Pc


动态链接

对于静态库的使用,有下面两个问题

当我们需要对某一个库进行更新的时候,我们必须把一个可执行文件再完整的进行一些重新编译 在程序运行的时候代码是会被载入机器的内存中,如果采用静态库就会出现一个库需要被copy到多个内存程序中,这个一方面占用了一定的内存,另一方面对于CPU的cache不够友好 链接的控制,从前面的介绍中可以看到静态库的连接行为我们不好控制,做不到在运行期替换使用的库 编译后的程序就是二进制代码,有些代码它们涉及到不同的机器和环境,假设在A 机器上编译了一个程序X, 把它直接放到B机器上去运行,由于A和B环境存在差异,直接运行X程序可能存在问题,这个时候如果把和机器相关的这部分做成动态库C,并且保证接口一致, 编译X程序的时候只调用C的对外接口.对于一般的用户态的X程序而言,就可以简单的从A环境放到B环境中.但如果是静态编译,就可能做不到这点,需要在B 机器上重新编译一次. 动态链接库在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)

这里列出了mian所需要的动态库, 如果有看类似 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的一个原因是需要使用一些第三方面库, 但是最后运行的线上机器和编译机的库存在一些行为不一致问题或者不兼容问题, 需要静态编译进去.对于这个问题,com的建议是采用third和third-64下的库, 通过我们自己编译的库来控制, com目前维护的第三方库相关资料见第三方库, 如果有新的第三方库需求可以联系com组. 采用我们自己编译的第三方库可以避免由于库底层和运行机器环境不兼容造成问题,同时也可以避免使用–static参数. 同时第三方库的编译参数也可以由我们根据实际情况进行定制, 只需要做到编译机编译的结果可以在线上机器(或者测试机)上正常运行. 早 期有一些测试表明在32位的环境下, 采用–static全部使用静态库可以使程序性能有1%~3%的提高,这个主要原因在于-fPIC产生的二次寻址问题导致(glibc那些库都是采用了 -fPIC的方式进行编译).但对于我们的很多程序本身调用glibc的地方就不是很多(如果这里会产生瓶颈,设法减少这一次函数调用效果是会更好的), 加上我们机器缓存大,代码小,这种的性能提高实在是很有限了. 在运行了多个程序的机器上反倒可能由于cache不友好有反效果

特别的是在ullib 3.1.27以后版本引入了comlog 输出网络日志,由于网络库的存在 会报出这样链接错误, 建议去除

小提示:

编译机差异 我们目前的环境的下, 编译机,开发机,线上机器都存在一些不同的地方 32位环境机器差异比较大, 开发机和编译机上是2.96 gcc, glibc版本 2.2.5 .线上32位机器现在已经很少了, 但我们经常把32位程序放到64位机器上运行, 64位机器下的32位环境中的glibc是2.2.93版本,其它各种库的版本也存在差异,使用动态库编译出问题的概率较大,特别使用了某些库的特殊接口 64位机器环境相对比较接近, 开发机上是gcc.3.4.5, 编译机是3.4.4 相差一个小版本号. 线上机器3.4.5和3.4.4都存在,内核版本也有略微不同 虽然编译机和线上机器环境存在略微的差异,不过这些差距并不大,目前的还没有因为这个原因造成问题. 对原来使用–static编译的老程序的建议 首先要确定使用–static编译的原因是什么, 根据不同的原因采用不同的方案 性能原因, 即使是经过测试认为–static可以提升性能, 提升的也是非常有限的,不建议为了这一点性能而使用–static, 另外注意就是原来在32位下有性能优势当升级到64位机器上可能就没有优势了 使用的第三方库比如-lz等, 如果出现不加–static, 会出现在线上机器不能运行. 这里建议使用由我们自己维护的第三方库第三方库,像基础库那样使用. 如果有不同版本和新库的需求请联系com组 存在动态库和静态库的混合应用,推荐使用下一章节的 -dn, -dy参数

Linux环境编译,链接与库的使用(四)

链接参数控制

链接器中提供了-dn -dy 参数来控制使用的是动态库还是静态库,-dn表示后面使用的是静态库,-dy表示使用的是动态库

例:

g++ -Lpath -Wl,-dn -lx -Wl,-dy -lpthread 这样如果在path路径下有libx.so和libx.a这个时候只会用到 libx.a.

注意在最后的地方如果没有-Wl,-dy 让后面的库都使用动态库,可能会报出 “cannot find -lgcc_s” 的错误,这是由于glibc的.a库和.so库名字不同,–static会自动处理,但是 -Wl,-dy却不会去识别这个问题.

小提示:

如果使用–static, 由于-dy的使用导致后面的库都是共享库(dy强制屏蔽了静态库),这个时候编译出来的程序和只有动态库的情况下强制使用–static编译一样都会报错

运行报错 ”undefined reference to `xxx()’ ” 对 于动态链接库,实际的符号定位是在运行期进行的.在编译.so的时候,如果没有把它需要的库和他一起进行联编,比如libx.so 需要使用uldict, 但是忘记在编译libx.so的时候加上-luldict的话,在编译libx.so的时候不会报错,因为这个时候libx.so被认为是一个库,它里面 存在一些不知道具体实现的符号是合法的,是可以在运行期指定或者编译另外的二进制程序的时候指定.

如果是采用 g++ -Lpath -lx 的方式进行编译,链接器会发现所需要的uldict的符号表找不到从而报错,但是如果是程序采用dlopen的方式载入,由于是运行期,这个程序在这个地 方就直接运行报错了. 另外还有一种情况就是一个对外的接口在动态库中已经声明定义了,但是忘记实现了,这个时候也会产生类似的错误.

如果在运行期报出这样的错误,就要注意是否是由于某些库没有链接进来或者某些接口没有实现的原因产生

日志库问题 其实不只是日志库存在这样的问题,其他需要同时被多个动态库以及主程序同时使用的函数其实都存在这样的问题.这里主要以日志库的问题为例来说明这些问题.

有 一个程序,它通过dlopen的方式调用了一个.so文件. 在这个主程序中和.so中都使用了日志库,主程序中使用ul_openlog打开了日志, 在.so中没有用ul_openlog打开日志.这个时候发现,主程序中的日志正常输出,但.so中的日志却直接输出到了标准出错.

这个问 题的原因在前面的其实已经提到了,在默认情况下主程序中使用的接口对于.so是不可见的,.so所在的代码空间与主程序的代码空间是隔离的,这个时 候.so调用的ul_writelog其实是没有经过ul_openlog的那块代码空间,由于ul_log库使用了一些static变量(如果是带 comlog的ul_openlog那还有全局符号),只有在ul_writelog, ul_openlog都是在同一块空间上的时候才会起作用.

这个问题的一个最简单的解决方案是

在主程序的链接的时候加入-rdynamic,仍然链接libullib.a库 编译动态链接库时,不加链接libullib.a库 其实动态库这里是否链了libullib.a已经不重要了,在有-rdynamic的情况下,.so中如果有与主程序同名的函数那么会优先调用主程序中的函数, 动态库不链接libullib.a倒是可以省点空间

但是这种方式在某些情况还是不能完全解决问题

假 设有A.so, B.so,主程序main, 在A.so中调用了ul_openlog, B.so中没有调用ul_openlog, 但调用了ul_writelog. 在主程序中没有调用ul_log中的任何接口和使用任何变量.这种情况下即时使用了-rdynamic还是会导致在 A.so中正常输出日志,但在B.so中却把日志输出到标准出错.

这个问题的主要原因在于,gcc在链接的时候是以.o为单位的,如果一 个.o中的符号没有被外部所使用,那么在链接的时候就不会把这个.o中的符号给链接进行.so或者二进制程序中.在上面的问题中主程序里面没有调用到日志 库中的任何符号,所以在链接的时候就不会把ullib中的ul_openglog和ul_writelog给链接进行主程序中,这个时候即使有 -rdynamic也是做不到让.so中的动态链接库都使用.

这个问题一般有下面几种方案:

载入A.so的时候使用RTLD_GLOBAL参数,把A.so中的所有的符号都变成对外可见,这样A.so和B.so的ul_writelog都在一块代码空间中了 编译主程序的时候链接ullib的地方由-lullib改为 -Wl,–whole-archive -lullib -Wl,–no-whole-archive, 同时加上-rdynamic. -Wl, –whole-archive是链接参数,它表示把ullib的中所有的符号都链接进主程序中,而不管主程序是否调用了ullib中的符 号.-Wl,–no-whole-archive表示后面的链接取消–whole-archive的行为,毕竟其他的库没有必要采用这种方式全部链接 进来. 在主程序中随便调一下ul_log中的符号,比如可以先随便ul_openlog一下,然后ul_closelog, 后面再进行动态库调用. 把libullib.a用 ar x 命令还原成多个.o文件,采用直接链接的方式使用ul_log.o. 上 面的几个问题的产生主要还在于静态链接和动态链接混用,而两种链接方式又存在不一样的地方.事实上如果我们把ullib库 采用动态链接库的方式编译成 libullib.so, 采用前面的方式在编译期链接libullib.so,并且设置LD_LIBRARY_PATH, 上面的2个问题都不会存在. 编译期使用的.so,是全局可见的,不需要-rdynamic也可以被dlopen的动态库所用到,由于是动态链接库,所以 包含了所有的符号,不会像静态库那样只包含了所用到的.o中的符号. 事实上这也是多数第三方程序的解决方案

主程序中使用-rdynamic会对后续的升级造成一些麻烦:

加上-rdynamic后,像日志库这样的基础如果需要升级, 那么就必须要升级主程序, 使用的.so无论如何升级所用到的ul_writelog都是主程序中的. 主程序中除了日志库,还会有其他库或者函数的存在, 这些函数如果不是static的就有可能与 dlopen打开的so中的函数混到一起,造成困惑 如果.so中需要打印它自己的日志, 那样需要comlog本身功能的支持才可以实现,而不能简单的使用ul_writelog来实现 但是如果主程序中没有使用-rdynamic,那么又有下面的这些麻烦 dlopen打开的动态库日志是打印自己的, 不能和主程序统一在一起. 如果.so的程序和主程序open的是同一个日志,这相当于多进程打日志, 那必须要comlog的支持,ullog本身不支持多进程打同一个日志. 如果主程序中用dlopen+RTLD_GLOBAL的打开了某个.so 日志的问题就可能影响到其他的.so中的调用. 同时对于一些老程序, 升级前后-rdynamic 可能也会产生影响,比如两次ul_openlog 这里对于类似日志库这种需要全局状态变量支持的库提出另外方案

  1. 编译一个专门的.so, 这个.so中包括了其它.so中所需要的所有和全局量相关的接口 2. 主程序不使用-rdynamic编译, 但打开上面的.so的时候,采用RTLD_GLOBAL方式,并且是第一个打开 3. 除了打开第一个 .so, 其它的.so都不使用RTLD_GLOBAL方式, 并且在编译的时候都不把和第一.so相关的库联编 4. 第一个.so的升级需要保证没有其它.so在运行才可以dlclose, 重新dlopen

这个问题首先需要明确需求, 到底是希望每个.so打自己独立的日志,还是和主线程统一

这 里要注意另外一个问题就是目前的ullib 日志库情况比老的ullog要复杂, 在comlog中引入一些extern 出来的全局变量 在采用dlopen的时候,对于一般符号,一般都是主程序和动态库在两块空间中, 但是对于使用extern出来的变量主程序和动态库都是在一块空间中(注:由于32位下不用-fPIC也可以编译so, 在没有-fPIC的情况也是分开的, 但是由于64位一定要-fPIC所以一定会出现同一块空间的问题), 对于这个问题的解决方案是在动态库链接的时候加上 -Wl,-Bsymbolic 参数将动态库的空间和主程序的空间强行分开。

上面有提到 编译动态链接库时,不加链接libullib.a库,但主程序使用-rdynamic, 这里主要是为了避免使用到了不同的ullib导致调用了一些不同的内部符号,导致出现另外的麻烦

对于动态库中的日志建议采用下面的几个方案:

  1. 动态库完全打自己的,日志,编译二进制程序不要用-rdynamic, 动态库链接的编译加上 -Wl,-Bsymbolic 参数 , 链接ullib, 在动态库中自己open,自己控制等级 1. 动态库不链接ullib, 编译二进制程序用-rdynamic , 这样可以正确的使用主程序中的日志库,也规避了版本不一致带来的问题, 但是这样失去了对于动态库日志的控制, 而且存在升级的不便, 日志的升级是由主程序控制的。

小提示:

有关动态库使用的例子还可以参考 SoTips 在运行期可以通过设置环境变量LD_DEBUG查看每个符号具体链接到了什么地方,每个符号具体的查找过程和绑定过程.可以这样使用 export LD_DEBUG=help 随便运行一个程序就可以看到对于LD_DEBUG的使用说明

export LD_DEBUG=files./main 可以看到整个装载过程

版本管理

系 统中存在了大量的动态和静态库,并且每个库都会随着库的升级和更新,形成各种的版本,这些版本之间又存在了各种各样的兼容或者不兼容的问题.linux中 是如何维护和管理这些库的?这里介绍了linux在这方面所作的一些工作.下面的这些都是基于我们现在使用的64位开发环境中的情况, 与32位的老版本存在了一定程度上的不兼容.

命名 在linux系统中对于一个共享库的命名一般是 libname.so.x.y.z

这个与百度目前通常使用的版本项目版本是类似,我们的版本号多了一个4位版本,作为开发过程中的小版本号,最后发布的其实也是按照3位版本号进行的.

不过在linux中 也有不少不遵守上面的命名的,比如glibc的动态库叫libc-2.3.5.so, 版本号在.so前面

这里又存在了另一个问题,由于版本号和命名捆绑在一起,那么当库升级的时候有怎么把版本号给对应上呢?

编译和载入的版本 我们先看一下pthread库在系统中的情况,(下面是以64位开发机为例,32位路径有所不同其它都一样)

在 /usr/lib64/ 中我们可以找到libpthread.so, 不过可以到libpthread.so其实很下,cat 一下可以看到这其实是一个文本文件

libpthread.so的内容:

/ GNU ld script Use the shared library, but some functions are only in the static library, so try that secondarily. /OUTPUT_FORMAT(elf64-x86-64)GROUP ( /lib64/libpthread.so.0 /usr/lib64/libpthread_nonshared.a ) 这 个libpthread.so 其实并不是什么共享库,它其实是一个ld的链接脚本,这个脚本的意思是,输出的是elf64-x86-64格式,使用的动 态库是/lib64/libc.so.6 静态库是/usr/lib64/libc_nonshared.a, 这样我们在编译的时候不用考虑使用的是哪一个版本的libpthread.so,也不需要靠它的动态库叫什么,静态库叫什么,只需要都是指定  -lpthread就可以了,具体实际是哪个版本,交给脚本去处理.

我们再看/lib64/libpthread.so.0, 其实这是一个软链接,它实际指向的是 libpthread-0.10.so, libpthread-0.10.so才是它真正使用的.so. pthread它通过这样的方式进行链接有什么作用呢?

我 先随便写个使用了pthread的程序,然后用readelf -d 查看,这时候可以看到pthread那一行指向的是 libpthread.so.0!, 而不是libpthread-0.10.so. 其实pthread这样处理主要是从版本的兼容性方面进行考虑.在我们编译的阶段通过ld脚本,统一了编译时候-l使用的命字,我们编译的时候不需要去考 虑什么.so的版本号问题.编译完了实际指向的是 libpthread.so.0,而不是最后的libpthread-0.10.so, 这样的好处在下面的几个方面:

如果pthread升级了, 比如升级后叫做libpthread-0.20.so,作为开发者可以保证它和libpthread-0.10.so是兼容的,那么我们可以大胆的把 libpthread.so.0的软链接指向libpthread-0.20.so,这不会出现什么问题. 如果libpthread-0.20.so与libpthread-0.10.so是不兼容的,那么我们可以新建立一个叫做 libpthread.so.1的符号链接指向libpthread-0.20.so,这样老的程序在运行由于它认为自己使用的是 libpthread.so.0而不会指向libpthread.so.1, 而用新版本编译出来的程序会自动依赖到libpthread.so.1,而不会出现依赖了老的libpthread-0.10.so而导致不兼容.需要依 赖老版本的程序在运行的过程中也不会出现因为依赖的库替换了不兼容的库导致出现问题. 符号版本 上面的这个过程很好的解决了在同一台机器上编译和使用动态库的更新问题.但是还有一些问题上面的方式无法完全解决

比如我现在的程序是使用了libpthread-0.20.so编译出来的程序,但是在运行的机器上只有libpthread-0.10.so, 这个时候有2种选择 1. 警报,但让程序继续运行,这有可能会出core 2. 直接禁止运行

现在的问题,很有可能我虽然用了新的库进行编译,但我只用到了老的接口,用libpthread-0.10.so就可以胜任我的工作, 但无论警报也好还是直接禁止运行也好这些都显的不合适.

为 了能解决这个问题, linux在符号中引入了版本的机制.我们可以用nm /lib64/libc.so.6 查看glibc中的符号表(我们32位环境中的glibc符号表被清空了,只能通过readelf进行查看). 我们可以看有不少接口被写成类似于 tmpfile@@GLIBC_2.2.5 的形式, 在符号中带有版本号信息, 这方式可以使程序在编译的时候记录下它编译的时候使用的共享库所对应的接口的版本号,这样如果共享库升级了, 当程序载入共享库的时候会检查编译时记录下来的版本是否与当前共享库接口的版本是否兼容,如果兼容那么就可以正常运行,如果有不兼容的情况就在载入库的时 候报错. 这种机制尽可能的考虑高版本与低版本的兼容性问题,如果我们打开glibc的源码包,我们可以到许多目录下都有一个叫Versions的文件,这个文件就 是用来描述各接口的兼容版本.

共享库可执行 我们直接运行 /lib64/tls/libc.so.6 , 可以看到,在运行后出现了一段的文字,表示了库的版本,作者, 编译时间等信息.这种方式提供了一种给用户确认和了解动态库的方式. 这种方式的实现也很简单, 下面是一个demo

include#include#include#ifdef cplusplus extern “C” { #endif#ifndef i386 / * @brief 使用的动态链接库 * */#define LD_SO_PATH “/lib64/ld-linux-x86-64.so.2” #else#define LD_SO_PATH “/lib/ld-linux.so.2” #endif#if defined( DATE) && defined( TIME) / * @brief 编译时间 * /#define BUILD_DATE ( DATE “” TIME) #else#define BUILD_DATE “unknown” #endif/** * @brief 设置入口位置 */constchar interp[] attribute((section(“.interp”))) = LD_SO_PATH; / * @brief .so文件运行的入口函数 * /void so_main() { //printf输出可以方便外部grep printf(“Ld.so : %sn”, LD_SO_PATH); //这个可以规避由于-O2开关导致,((section(“.interp”))) 被优化没的问题 printf(“Project : %sn”, “mc_pack php extension for mcpack2”); printf(“Version : %sn”, VERSION); printf(“CVS : %sn”, “public/php-ex/php-mcpack”); printf(“CVSTag : %sn”, CVSTAG); printf(“BuildDate : %sn”, BUILD_DATE); exit(0); }#ifdef cplusplus } #endif

LD_SO_PATH 表示了所使用的加载器,这里要注意32位和64位的区别,这里把过程简化了用宏__i386来判断32位与64位

section(“.interp”) 设置了运行使用的载入器

so_main是运行程序的地方,这里写上共享编译的信息, 当然这个地方可以换成别的什么名字, 在编译的时候需要加上 -Wl,-e,so_main, 指明了动态库的实际的入口位置(这里是so_name, 也可以换成xxx)

采用的编译参数

g++ xxx.cpp -fPIC -c -Wl, -e,so_main 在把生成的.o与共享的主程序链接到一起,就可以直接运行共享库了

总结和建议

上面介绍库(包括静态库和共享库)链接的过程,并对应其实现进行了分析,并且针对编译中出现的问题进行分析和解决.

这对于我们使用库是有一定的帮助的. 这里对于库的编写和使用提出一些建议

静态库 尽量不要使用–static参数, 对于一些特殊的必须要使用.so的情况,可以考虑使用-Wl,-dn 的方式进行.

64位环境中编译加上-fPIC 虽然是静态库,但很可能会被用作共享库的一部分

链接的时候注意链接的顺序,越是基础库越是在后面

共享库 开发

对外接口尽量使用基本类型,不要使用C++类, 如果一定需要建议采用指针的方式

对外接口采用extern “C”的方式提供, 不要直接使用一般的C++接口,这主要是从接口的方便性考虑,毕竟都不希望采用”_fooIV”这种形式的接口进行访问.

共享库的生成文件需要带上我们的4位版本号, 格式与linux的格式相同, 如 mcpack.so.1.1.0.0

共享库需要能够自运行, 运行需要输出版本号, 编译时间,库的简单说明, DEMO见上文中共享库可执行部分. 输出格式建议:

采用printf输出,考虑到以后可能可以用脚本分析 输出格式参考DEMO的样式

小心使用-rdynamic参数, 能够不使用就不要使用

尽量保证主程序的编译依赖与动态库的编译依赖相同,特别是在主程序代码中使用-rdynamic.

发布

动 态库发布到output下除了要有带版本号的.so,还需要同时有一个未带版本号的.so, 比如在output/lib目录需要有mcpack.so.1.1.0.0 和一个软链接 mcpack.so 指向mcpack.1.1.0.0, 这里的mcpack.so软链接主要是给其它程序编译期使用的.

上线

严禁对于.so采取直接copy覆盖的方式

更新.so, 可以采用两种方式:

软链接方式, 程序运行使用的是确定mcpack.so软链接,上线的时候采用的是把mcpack.so链接指向mcpack.so.1.1.0.0, 由于原来的程序还在,这样不会出现问题 mv方式, 旧的.so需要先mv成另外的文件名,然后再放入新的.so 需要确认.so已经被载入, 由于.so的使用本身有2种方式,需要考虑下面两种情况 dlopen方式,这种重新载入可以适用于热切换,这个要求RD根据代码逻辑来控制, 一般可以从日志看出 LD_LIBRARY_PATH指定路径,这种情况一般第三方库的情况比较多,这个时候需要把程序重启 所有操作完毕之后,需要用/usr/sbin/lsof -p pid 查看载入的动态库是否是我们需要的 不要在终端或者.bash_profile 等全局环境中加入LD_LIBRARY_PATH, 在启动需要.so的程序脚本中加入即可.

FAQ

这里收集了一些与编译,链接相关的问题, 有问题随时欢迎提问.

编译问题太复杂了,有没有简单的解决方案? 这里建议大家使用com组提供的Comake自动编译构建工具,来解决这些存在复杂依赖的情况, 在comake中 通过简单的描述就可以规避大量的编译链接问题

我是纯C程序,如何使用ullib这些用g++编译出来的库 上文已经介绍过了, 在g++的环境中直接编译的结果会导致符合表与gcc编译的结果不同导致不能混合编译.

gcc使用g++编译的库原则:

  1. g++编译库的时候需要把被外界使用的接口按照纯C++可以接受的方式用extern “C” 包起来,并且加上__cplusplus宏的判断,可以参考public/mcpack, public/nshead中的写法. 对于一些特殊情况,比如已经是g++编译出来的库又不适合修改,比如ullib, 分词库等,可以自己写一个 xxx.cpp的程序,在xxx.cpp对需要使用的接口再做一次纯C接口的封装,同时用extern “C”把纯C接口导出使用.使用g++编译,并且在链接的时候加上ullib等库即可. 2. gcc编译g++库在我们的64位环境中需要在最后加上-lstdc++

gcc使用g++编译的库多见于需要将基础库与php扩展,apache mod进行联编, 这可以参考 public/php-ex/php-wordseg中的实现

g++使用gcc编译出来的库: 这个比较简单,我们使用系统的库都是这种方式,只需要gcc编译的提供的头文件采用了extern “C”封装即可.

我在同样的环境下用同样的方式编译出来的程序md5是否都一样 如果环境完全一样包括编译路径,环境变量等都是一样的,一般情况下确实是一样的,但是许多环境的情况我们很难做到一样,比如程序使用一些DATA这样与时间相关宏就会导致每次编译的结果都是不一样的,有时候甚至内存的多少也会影响编译的结果

链接和运行的时候,静态库和动态库路径的查找顺序都是什么? 链接的时候查找顺序:

-L 指定的路径, 从左到右依次查找 由 环境变量 LIBRARY_PATH 指定的路径,使用”:”分割从左到右依次查找 /etc/ld.so.conf 指定的路径顺序 /lib 和 /usr/lib (64位下是/lib64和/usr/lib64) 动态库调用的查找顺序: ld的-rpath参数指定的路径, 这是写死在代码中的 ld脚本指定的路径 LD_LIBRARY_PATH 指定的路径 /etc/ld.so.conf 指定的路径 /lib和/usr/lib(64位下是/lib64和/usr/lib64) 一般情况链接的时候我们采用-L的方式指定查找路径, 调用动态链接库的时候采用LD_LIBRARY_PATH的方式指定链接路径. 另 外注意一个问题,就是只要查找到第一个就会返回,后面的不会再查找. 比如-L./A -L./B -lx 在A中有libx.a B中有libx.a和libx.so, 这个时候会使用在./A的libx.a 而不会遵循动态库优先的原则,因为./A是先找到的,并且没有同名动态库存在.

我们一般使用ullib库都是直接获取.a进行编译,但是public下的库都是需要我们自己手动编译后才可以使用,为什么要这样做? 这个地方涉及到一些历史问题, lib2和lib2-64下的库都是以库的形式发布这里有两个原因:

类似ullib这样的库到处都要用,每次都编译比较花时间 类似于wordseg这样的库有保密需求,不方便向所有工程师透露原代码 一般我们把发布到lib2和lib2-64下的库称为静态发布, 发布到public下的库都是实时发布,它们的区别就在于public下的库每次都会重新进行编译. 但是这种静态发布会给库的升级带来一些麻烦:

静态编译的库之间有依赖关系,一些接口性的升级会超成连锁反应,导致多个库都需要升级. 比如最简单的情况: ullib库中有一个叫foo(char str)的接口, 由于是老代码当时没有考虑到foo的接口上需要使用 const ,现在希望能够满足编程规范采用const接口变成foo(const charstr)的形式.这个时候假设其他静态发布的库libX也使用了这个接口. 在前面的介绍中我们提到了在c++中会根据接口类型不同导致符号不同,我们把foo(char str)改为foo(const charstr)导致现在编译出来的ullib中符好是const charstr的形式,而不是charstr的形式, 这样在联合编译libX和ullib的时候,libX就会说foo(char str)这个接口找不到, 这个时候一种解决方案就是libX也升一次级, 这样又带来另外的问题就是强迫大家都必须把相关的库都升上去,这个往往是不现实的, 因为某些原因这个过程往往需要分成几步走( 比如需要考虑新分词和老分词的策略区别, 版本相差太多的风险性). 目前这方面COM采用的解决方式是在.cpp文件中依然保留foo(char str)的接口,这样可以使得老库在依赖新的ullib的时候不会出问题. 但是这个方案还是不能本质解决问题, 比如新的uldict使用新的ullib编译出来, 现在需要使用一个新的dict和一个老的ullib联合编译,由于dict依赖了ullib中的const char的接口,但老的ullib又不存在const char这样的接口, 这个时候联编又会发现符号找不到.我们只能做到老的使用新的不出问题,但新的使用老的还是存在一些问题,目前只能建议用户升级到最新的ullib上.如果 升级存在困难请联系com组.

静态发布受到编译器和环境的限制 我们发布的是使用32位gcc 2.96和64位gcc3.4编译出来的库,如果换到另外的gcc环境中又需要重新编译,特别是存在有多个静态发布的库需要重新编译. 上面的这些问题,如果是在采用实时编译的话都可以不会出问题的,我们可以保证了代码级别上的兼容,但对于已经存在的符号表的兼容确实比较麻烦.

但是事实上这些库采用和public的类似在scmpf上进行实现编译其实也是可以做到,只是目前还是保留过去的编译方式,比较静态编译目前出现的问题都是可以规避的.

另外在third和third-64下的第三方库也是采用静态发布,这个主要考虑是第三方库的编译比较消耗时间.

使用cvs co libsrc/ullib 的方式可以直接获取到ulllib的原代码

哪些情况会出现 ”undefined reference error” 的错误? 这里再总结一下这个问题可能出现的场景:

没有指定对应的库(.o/.a/.so) 使用了库中定义的实体,但没有指定库(-lXXX)或者没有指定库路径(-LYYY),会导致该错误, 比如 使用uldict, 由于uldict中使用到了md5签名需要库crypto的支持,需要在-lullib之后加上-lcrypto. 连接库参数的顺序不对 在默认情况下,对于-l 使用库的要求是越是基础的库越要写在后面,无论是静态还动态,这里可以参考上文件中静态链接的章节. gcc/ld版本不匹配 gcc/ld的版本的兼容性问题,由于gcc2 到 gcc3大版本的兼容性存在问题(其实gcc3.2到3.4也一定程度上存在这样的问题) 当在高版本机器上使用低版本的机器就会导致这样的错误, 这个问题比较常见在我们32位的环境上, 许多32位的机器上是gcc3.2的编译环境,但我们提供的是有gcc2.96编译出来的结果,导致ullib库不能被编译器所认识. 另外就在32位环境不小心使用了64位的库或者反过来64位环境使用了32位的库. C/C++相互依赖和链接 gcc和g++编译结果的混用需要保证能够extern “C” 两边都可以使用的接口,在我们的64位环境中gcc链接g++的库还需要加上 -lstdc++,具体见前文对于混合编译的说明 运行期报错 这个问题基本上是由于程序使用了dlopen方式载入.so, 但.so没有把所有需要的库都链接上,具体参加上文中对于静态库和动态库混合使用的说明 可以把两个.o直接合并成一个.o文件吗? 可以,命令是 ld -r a.o b.o -o x.o, 不过不推荐这样做,这样做唯一的好处是静态库在链接的时候如果使用到了a.o中的符号也可以同时把b.o中的符号链接进来,可以避免–whole-archive的应用.

但是不推荐这样做,无形中增加了对源文件维护的麻烦

为什么我使用inline,并没有把代码inline进程序? 首先加了inline的函数是否可以被inline这个是由编译器决定,很多时候即时是指定了inline但还是无法被inline

另 外注意到我们的gcc中,只有在使用-O以上的优化后inline才会起作用,没有-O, -O2, -O3这些优化手段,无论是否加上了-finline-functions gcc都是不会进行inline优化的,这个时候的inline相当于一个普通函数(其实还是有一点区别,在符号表中表示是不一样的).我们许多程序在编 译的时候加上了-finline-functions 但如果没有-OX(X>=1)的配合, -finline-functions其实是无效的,不会起作用也不会报错

gcc里面为了能够支持在不加-OX(X>=1)的情况下能够将函数inline, 提供了一个扩展always_inline, 将函数写成下面这样

attribute((always_inline)) int foo() { … } 就可以在不加-OX(X>=1)的情况下把foo inline进程序,不过always_inline 这个扩展只在gcc3以后支持,我们32位环境中使用的2.96 gcc是不支持的.

64位机器上可以编译出32位程序吗? 理 论上是可以的, 在64位机器上的64位gcc中提供了-m32的参数,可以指定进行32位的编译。 但是编译问题虽然解决,但链接问题却还是存在, 我们部分64位机器上gcc2.96使用的程序覆盖了64位机器给32位程序使用的库导致链接失败, 如 果没有覆盖的机器是正常的。

我将-lub -lub_log这样连在一起使用是否有问题? 如 果确认所使用libub.a, libub_log.a它们的版本一致,这样使用是没有问题的,无论是libub中的ub_log的部分还是ub_log.a中的ub_log的部分,他 们都是属于同样的二进制代码,无论怎么链接都可以正常工作. 但是如果版本不同可能会有不可预知的后果(如最经典的baidugz和zlib的冲突).

为什么编写的动态链接库不能直接运行? 在共享库的总结中介绍了如何实现共享库可以自己运行,但是有些时候会出现undefined reference error的错误导致共享库不能被运行。

这 种情况产生的原因是:动态库中采用了类似 static int val = func(xxx);的写法, 其中val 是一个全局变量(或者静态全局变量)。 动态库被载入内存中使用的时候会直接先运行func这个函数,如果func是来自其他的库(比如一些情况下主程序使用-rdynamic编译,动态库使用 主程序的空间), 在编译动态链接的库的时候又没有被链接上, 这个时候就会出现这样的问题。

对于这样的问题主要考虑下面的解决方案:

  1. 不要采用static int val = func(xxx);这种写法

将使用的静态库链接进共享库, 但这里要注意-rdynamic的影响,必要的时候需要保证和主程序使用的库版本是相同的。 让共享库不可运行也是一种解决方案 是否可以在main函数开始前就执行程序? 如果在main函数开始前执行代码,我们一般有下面的两种方法

采用 int val = func(xxx)的方式,在func(xxx)中执行 声明一个class, 把需要运行的函数写在class. 并且定义一个全局(或者static)的类变量 在实现上,编译器把它们放到一个特殊的符号 _init 中,在程序被载入内存的时候被执行 但是这种方式我们不推荐使用,特别是在这些执行代码中存在库与库之间的依赖关系的时候, 比如下面的场景:

libA.cpp

class Aclass { public: Aclass() { int * u = Bfunc(); //这是另外一个库libB中的函数 int c = u[0]; } } static Aclass s_test; libB.cpp

staticint s_test = test_init(); //初始化s_testint Bfunc() { return s_test; } 上面的程序中有2个库,A库有一个static变量的构造函数依赖了 B库中的一个函数, B库中的这个函数又操作了一个由函数test_init初始化的static变量. 按照程序的要求我们必须要让test_init()这个函数在Aclass这个函数之前运行, 但是可惜的在某些情况我们很难做到这点, 这里涉及到链接器对库链接和初始化顺序的问题.

在默认情况下, test_init()和s_test的构造函数的执行顺序是按照链接的时候-l的顺序从右到左, 比如-lB -lA 那么Aclass的构造函数会在test_init()前执行,这个时候就会出现问题,需要保证-lA -lB的顺序才可以正常.

这 里又涉及到另外一个问题, 就是 正常情况既然A依赖B, 那么在链接的时候肯定需要 保证 -lA在-lB. 但是这里我们只能说需要把越基础的库放在越后面,而不是必需放在最后面.还是上面的例子. 如果这个时候有一个test.cpp 使用了 A库, 并且在test中没有直接使用到B库中的东西, 这个时候如果-lB放在-lA前面,链接器会报错, 因为符号在从左往右展开的时候, 由于test没有使用到B的东西,所以没有做任何展开, 从这个角度而言在链接A的时候就找不到符号. 但是如果在test中有使用到B中和test_init相关联的函数,那么这个时候如果把-lB放在-lA的前面展开B函数的时候会把test_init 导出, 这样导致A会认为已经存在了test_init, 从而不报编译错误. 但是这样的结果就是test_init的初始化顺序被放到Aclass之后, 那么在程序运行的时候就可能导致错误.

对这种问题解决,主要有几种考虑

采用 单例模式, 采用类似 if (NULL == ptr) ptr = new xxx; return ptr的方式通过用户态的判断来控制 了解依赖关系, 把-lB放到-lA的后面 不允许这种方式的存在. 在使用全局变量的时候 需要特别注意这种初始化的顺序问题. 小提示:

构造初始化等,是在init中处理, 另一个方面fini是存在在程序退出前的执行析构等操作

dlopen是否可以载入主程序的符号? dlopen除了可以通过指定文件名载入共享库中的符号其实也是可以载入主程序的符号,只不过要注意这个时候主程需要使用-rdynamic

在dlopen的时候使用dlopen(NULL, …) 的方式载入符号,这个时候可以载入此时运行程序中的所有全局符号,包括2个部分

dlopen具体共享的时候采用了RTLD_GLOBAL方式打开 主程序在链接的时候使用了-rdynamic参数 例: extern “C” { externint foo(int a, int b); } int foo(int a, int b) { fprintf(stderr, “%d %dn”, a, b); } typedefint (foo_t)(int, int);int main() { void handle = dlopen(NULL, RTLD_NOW); char *estr = dlerror(); if (estr != NULL) { fprintf(stderr, “myerror %sn”, estr); exit(-1); } foo_t fun = (foo_t)dlsym(handle, “foo”); estr = dlerror(); if (estr != NULL) { fprintf(stderr, “error %sn”, estr); exit(-1); } fun(3, 4); return 0; } 上面的程序采用g++ -o test test.cpp -ldl -rdynamic编译后, fun可以被正常执行

有些程序对于库的编译依赖版本不一致,会有问题吗? 一个例子:

一个程序依赖A库和C库, A库依赖B库的版本1, 但C库依赖B库的版本2

几个注意: 1. 程序具体依赖到那个库是在最后链接的时候决定的 2. 在平台上SCM会选择最高版本的那个B库进行链接 3. 在编译期,库的依赖只是头文件的依赖。

严格来说这样是有一定的风险, 对于lib2和public下的库,主要由com组在升级过程中会尽可能的规避这样的风险,让大家一般情况下都不需要关心这个问题。 一般来说, com组都会保证头文件的编译依赖的向下兼容

在scmpf平台上对于编译的问题是可以强制指定编译依赖的。

另 外的一个问题就是,由于在平台上只会针对一个版本进行编译,不至于出现。 但是在线下编译的时候,对于上面的情况可能还是会出现一些链接错误, 这个主要原因是由于A库由版本1的B库编译,然后升级了, C库由版本2个B库编译, 最后编程序的时候链接是版本2的B库造成的, 遇到这种问题 , 一般建议是把A库用版本2重新编译一边。因为向下兼容一般只能保证做到高版本支持低版本,在不重新编译的情况要做到兼容比较困难,另一方面意义也不大。



http://liuxun.org/blog/bian-yi--lian-jie-yu-ku-de-shi-yong-yi/点击打开链接

0 0
原创粉丝点击