【转】编译,链接与库的使用(1)

来源:互联网 发布:python cmdb开发实例 编辑:程序博客网 时间:2024/05/29 13:17

背景

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

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

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

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

从程序到可执行文件

从hello world 说起

#includeint main() { printf("hello world\n"); return 0; } 
上面这一段程序任何一个学过C语言的同学都是闭着眼睛都可以写出来,但是对于将这样一个源代码编译成为一个可执行文件的过程却不一定有所了解。

上面的程序如果要编译,很简单

gcc hello.c

然后./a.out就可以运行,但是在这个简单的命令后面隐藏了许多复杂的过程

一般来说,可以把这样的过程分成4个, 预编译, 编译, 汇编和链接

预编译

这个过程包括了下面的步骤

  1. 宏定义展开,所有的#define 在这个阶段都会被展开
  2. 预编译命令的处理,包括#if #ifdef 一类的命令
  3. 展开#include 的文件,像上面hello world 中的stdio.h , 把stdio.h中的所有代码合并到hello.c中
  4. 去掉注释
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 ./test

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

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

00000000005009b8 A __bss_start00000000004004cc t call_gmon_start00000000005009b8 b completed.10000000000500788 d __CTOR_END__0000000000500780 d __CTOR_LIST__00000000005009a0 D __data_start00000000005009a0 W data_start0000000000400630 t __do_global_ctors_aux00000000004004f0 t __do_global_dtors_aux00000000005009a8 D __dso_handle0000000000500798 d __DTOR_END__0000000000500790 d __DTOR_LIST__00000000005007a8 D _DYNAMIC00000000005009b8 A _edata00000000005009c0 A _end0000000000400668 T _fini0000000000500780 A __fini_array_end0000000000500780 A __fini_array_start0000000000400530 t frame_dummy0000000000400778 r __FRAME_END__0000000000500970 D _GLOBAL_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 to `foo()'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 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中的所有符号都会被载入到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++

小提示

  1. 在linux gcc 中, 只有在源代码使用 .c做后缀,并且使用gcc编译才会被编译成纯C的结果,其他情况像 g++ 编译.c文件,或者gcc 编译.cc, .cpp文件都会被当作C++程序编译成C++的目标文件, gcc和g++唯一的不同在于gcc不会主动链接-lstdc++
  2. 在 extern "C" { }中如果存在默认参数的接口,在g++编译的时候不会出现问题,但是gcc使用的时候会报错.因为对于函数重载,接口的符号表还是和不用默认参数的时候是一样的.

编译器版本问题

目前公司内部使用的gcc版本主要分两种

  1. 32位 gcc 2.96
  2. 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 T _Z12creat_sign32Pc

动态链接

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

  1. 当我们需要对某一个库进行更新的时候,我们必须把一个可执行文件再完整的进行一些重新编译
  2. 在程序运行的时候代码是会被载入机器的内存中,如果采用静态库就会出现一个库需要被copy到多个内存程序中,这个一方面占用了一定的内存,另一方面对于CPU的cache不够友好
  3. 链接的控制,从前面的介绍中可以看到静态库的连接行为我们不好控制,做不到在运行期替换使用的库
  4. 编译后的程序就是二进制代码,有些代码它们涉及到不同的机器和环境,假设在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) 加中有同名的符号,会选择可执行文件中使用的符号,这在一些情况下可能会带来一些莫名其妙的运行错误。

小提示:

  1. /usr/sbin/lsof -p pid 可以查看到由pid在运行期所载入的所有共享库
  2. 共享库无论是通过dlopen方式载入还是载入器载入,实质都是通过 mmap的方式把共享库映射到内存空间中去。mmap的参数MAP_DENYWRITE可以在修改已经被载入某个进程文件的时候阻止对于内存数据的修改, 由于现在内核中已经禁用这个参数,直接导致的结果就是如果对mmap的文件进行修改,这个时候的修改会被直接反映到已经被mmap映射的空间上。由于内核 的不支持,使得共享库不能在运行期进行热切换,共享库在更新的时候需要由载入的程序通过一些外部的方式来判断,主动使用dlclose,并且dlopen 重新载入共享库,如果是载入器载入那么需要重启程序。另外这里的热切换指的是直接copy覆盖原有的共享库,如果是采用mv或者软连接的方式那么还是安全 的,共享库被mv后不会影响原来的已经载入它的程序。
  3. g++ 加上 -rdynamic 参数实质上相当于ld链接的时候加上-E或者--export-dynamic参数,效果与g++ -Wl,-E或者g++ -Wl,--export-dynamic的效果是一样的。
0 0
原创粉丝点击