测试程序很简单如下:
libA.h
1
2
3
4
|
#ifndef __LIBA_H__
#define __LIBA_H__
void display();
#endif
|
libA.c
1
2
3
4
5
|
#include
#include "./libA.h"
void display() {
printf(
"%s\n",
"Hello libA");
}
|
编译选项:
1
2
|
gcc -c libA.c
ar r ./libA.a libA.o
|
main.c
1
2
3
4
5
6
7
8
9
|
#include
void display() {
printf(
"%s\n",
"Hello Main");
}
int main(
int argc,
const
char *argv[])
{
display();
return
0;
}
|
libA 和 main里面同时有个display函数.对 main.c进行编译 并连接如下:
1
|
gcc -L ./ main.c
-lA
|
成功编译!
运行输出: Hello Main
去掉main.c中的display()的实现.再次编译 ,输出为: Hello libA , 可以看出:若 main中实现了此函数,则会覆盖掉静态库中相同的函数.
从main.c这段程序开始:
1
2
3
4
5
6
|
#include
int argc,
const
char *argv[])
{
display();
return
0;
}
|
只生成目标文件也即gcc -c main.c
然后查看main.o的符号表信息:
1
2
|
U
display
T main
|
可以看出display函数是U的,也即未定义的!
把它和两个东西进行链接 1. libA.o 2.libA.a
连接结果是: 两者都可以成功连接 !
再把main.c的display的实现加进去. 然后查看main.o的符号表:
1
2
3
|
0000000000000000
T display
0000000000000010
T main
U puts
|
display函数也成为定义的了,但是如果我再和1. libA.o 2.libA.a 进行连接 会发生什么问题呢?
1.libA.o
1
2
3
4
5
|
163 ~/My-Codes/C-Codes/WelcomeC >>ld -lc main.o libA.o @crazybaby
libA.o: In function `display':
libA.c:(.text+0x0): multiple definition of `display'
main.o:main.c:(.text+0x0): first defined here
ld: warning: cannot find entry symbol _start; defaulting to 0000000000400260
|
连接libA.o的时候报 display多次定义.
再连接libA.a时就跟之前一样 是可以成功连接,并可以运行的.
这说明ld连接器在连接时对目标文件和静态链接库处理是不一样的. 为什么不一样呢? 其实目标文件在进行连接时会把相同性质的段合并到一起,而且收集所有目标文件的符号表中的符号定义和符号引用放到global table 中, 在这个过程中如果出现重复的 就会报刚才的错误了.,
2.libA.a
1
2
3
|
163 ~
/My-Codes/
C-
Codes/
WelcomeC >>ld -lc main.o libA.a -e main
@crazybaby
163 ~
/My-Codes/
C-
Codes/
WelcomeC >>ls
@crazybaby
a.out* lib/ libA.a main.c main.o
|
链接libgA.a时确能正常生成 a.out ,libA.a只不过是libA.o的打包,但是为什么可以正常编译成功呢? 其实这里面有个强弱符号的问题. 在连接器链接时 ,会进行符号解析, 符号解析时,符号表有强弱之分.对于同名强弱符号要符号下面3个规则:
1.只允许一个强符号存在
2.一个强符号和多个弱符号,则以强符号为主
3.若有多个弱符号,则任选其一.
另外,函数和已经初始化的全局变量为强符号,未初始化的全局变量为弱符号.
所以这就出现我们刚才遇到的情况main.o 和 libA.o一起链接时,是强符号的链接 ,所以会报错.而静态库是多个可重定位的目标文件的集合. 连接器在符号解析阶段, 会从左到右按照出现的先后顺序来加载可重定位目标文件和静态库,连接器会维护一个可重定位目标文件的集合E, 这个集合的文件会被合并起来形成可执行文件, 和一个未解析的符号(引用了,尚未定义的符号)集合U,和一个已经定义的符号D.
当ld链接每个文件时,它会从左到右开始链接,并且判断当且文件是目标文件还是静态库!
如果是目标文件,那么连接器会把它添加进E中,并用它来解析U和D,如果遇到两个相同符号则按照上面三个规则来处理. 这样就会出现和之前的例子多次定义的情况了!
如果是静态库,则连接器首先查看U中是否有未解析的符号和静态库中的目标文件的符号一致,如果有,则把那个静态库中的相应目标文件添加进E集合中.在同时修改U和D集合. 如果没有则丢弃目标文件.
好说到这,我们来看下面的例子: 开始的例子中main.c中的display已经定义 也就是说没有U集合.当它和静态库linA.a进行链接时,发现U集合中没有定义就把libA.a中的目标文件libA.o丢弃了. 假如我们在main.c中加入 未定义的display2(); 在libA中进行定义.这样会不会和我们所说的那样呢?
main.c
1
2
3
4
5
6
7
8
9
10
|
#include
void display() {
printf(
"%s\n",
"Hello Main");
}
int main(
int argc,
const
char *argv[])
{
display();
display2();
return
0;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
163 ~/My-Codes/C-Codes/WelcomeC >>readelf -s main.o @crazybaby
Symbol table
'.symtab' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
0:
0000000000000000
0 NOTYPE
LOCAL
DEFAULT UND
1:
0000000000000000
0 FILE
LOCAL
DEFAULT
ABS main.c
2:
0000000000000000
0 SECTION
LOCAL
DEFAULT
1
3:
0000000000000000
0 SECTION
LOCAL
DEFAULT
3
4:
0000000000000000
0 SECTION
LOCAL
DEFAULT
4
5:
0000000000000000
0 SECTION
LOCAL
DEFAULT
5
6:
0000000000000000
0 SECTION
LOCAL
DEFAULT
7
7:
0000000000000000
0 SECTION
LOCAL
DEFAULT
8
8:
0000000000000000
0 SECTION
LOCAL
DEFAULT
6
9:
0000000000000000
16 FUNC
GLOBAL
DEFAULT
1 display
10:
0000000000000000
0 NOTYPE
GLOBAL
DEFAULT UND puts
11:
0000000000000010
32 FUNC
GLOBAL
DEFAULT
1 main
12:
0000000000000000
0 NOTYPE
GLOBAL
DEFAULT UND display2
|
可以看出 display2是undefined的! puts是属于动态库的 动态调用glibc的
libA.c
1
2
3
4
5
6
7
8
|
#include
#include "./libA.h"
void display() {
printf(
"%s\n",
"Hello libA");
}
void display2() {
printf(
"%s\n",
"Hello libA");
}
|
进行链接libA文件:
1
2
3
4
|
>>ld -lc main.o libA.a -e main @crazybaby
libA.a(libA.o): In function
`display':
libA.c:(.text+0x0): multiple definition of
`display'
main.o:main.c:(.text+0x0): first defined here
|
果然如前文所说的那样.
我们继续,当连接器最后发现U集合为非空的,则连接器就会输出一个错误并且终止程序!
解决这种情况也很简单,只要在重定义的函数前加上static来限制它的作用域就可以了, 尤其在大型的工程构建中, 这种事经常发生, 对于C++ 可以使用namespace ,C的话可以定义好大家的命名规范 或者就用 static来解决.
我们再回过头来看看开始我同事出的问题,bug代码(重新还原bug原型,非工程代码)如下:
动态库的代码: libdy.so
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include "sofun.h"
#include
#include
using
namespace
std;
struct ServerConf {
int a;
string b ;
};
int helloworld() {
ServerConf m;
m.b =
"1111";
// 这里是重点
cout<<
"Hello"<
return
0;
}
|
.h
1
2
3
4
5
6
|
#pragma once
#include
#include
using
namespace
std;
extern
"C"
int helloworld();
|
编译选项:
1
|
>>
g++ -fpic -shared sofun.cpp -
o libA.
so
|
main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
#include
#include
#include
using
namespace
std;
class ServerConf {
public:
ServerConf(){};
~ServerConf(){};
};
int test() {
char buf[
100];
typedef
int (*fun)();
char *error;
void *handle = dlopen(
"./libA.so", RTLD_LAZY);
if (NULL == handle) {
error = dlerror();
cerr <<
"dlopen error"<
}
else {
fun f = (fun)dlsym(handle,
"helloworld");
f();
}
return
0;
}
int main(
int argc,
char **argv)
{
ServerConf conf;
test();
return
0;
}
|
编译选项:
1
|
g
+
+
main
.
cpp
-
rdynamic
-
ldl
|
然后直接运行:
1
2
|
>>./a.
out
@crazybaby
sh: segmentation fault (core dumped) ./a.
out
|
出现Core.
这是为什么呢? 其实主要是rdynamic这个选项出现的问题. 动态库在运行时加载时,rdynamic选项会把main.o里面的ServerConf变成全局可见的, 显然载入动态库时, 调用错了.
我们再次这样编译主程序:
1
|
g
+
+
main
.
cpp
-
ldl
|
然后也直接运行
1
2
|
>>./a.
out
@crazybaby
Hello
|
运行 OK!
来看下没加rdynamic的符号表.
1
2
3
4
5
|
>>readelf -s a.
out | grep ServerConf @crazybaby
60:
0000000000400aaa
10 FUNC
WEAK
DEFAULT
13 _ZN10ServerConfC1Ev
62:
0000000000400ab4
10 FUNC
WEAK
DEFAULT
13 _ZN10ServerConfD2Ev
77:
0000000000400ab4
10 FUNC
WEAK
DEFAULT
13 _ZN10ServerConfD1Ev
78:
0000000000400aaa
10 FUNC
WEAK
DEFAULT
13 _ZN10ServerConfC2Ev
|
再看下加rdynamic的符号表:
1
2
3
4
5
6
7
8
9
|
>>readelf -s a.
out | grep ServerConf @crazybaby
24:
0000000000400d94
10 FUNC
WEAK
DEFAULT
13 _ZN10ServerConfD1Ev
26:
0000000000400d8a
10 FUNC
WEAK
DEFAULT
13 _ZN10ServerConfC1Ev
27:
0000000000400d94
10 FUNC
WEAK
DEFAULT
13 _ZN10ServerConfD2Ev
29:
0000000000400d8a
10 FUNC
WEAK
DEFAULT
13 _ZN10ServerConfC2Ev
62:
0000000000400d8a
10 FUNC
WEAK
DEFAULT
13 _ZN10ServerConfC1Ev
64:
0000000000400d94
10 FUNC
WEAK
DEFAULT
13 _ZN10ServerConfD2Ev
77:
0000000000400d94
10 FUNC
WEAK
DEFAULT
13 _ZN10ServerConfD1Ev
78:
0000000000400d8a
10 FUNC
WEAK
DEFAULT
13 _ZN10ServerConfC2Ev
|
明显多了很多.
解决这种情况, 其实给main.cpp里面的ServerConf加上namespace就可以了,那动态库是否需要加呢? !
使用Cmake的童鞋要注意了,cmake编译时默认是加这个选项的, 我觉得有必要加. 但是这个又给不了解内部机制的童鞋造成莫名其妙的错误. 我建议还是去了解下.
另外再次请注意下dlopen的参数, 也就是flag标志,详细,请参见linux Man , 下面逐个详解各个flag参数, 只有知己知彼方能掌控.
RTLD_LAZY:
LAZY 也就是说,当动态库中声明了未定义的函数或变量,并且在调用的函数中使用了. 在dlopen时不会去解析这个未定义的符号的地址.
RTLD_NOW:
NOW 和LAZY相反,当执行dlopen时就会去解析动态库未定义的函数的地址.
举例:
1
2
3
4
5
6
7
8
9
10
|
#include "sofun.h"
#include
#include
using
namespace
std;
extern
int func();
int sofun()
{
func();
return
0;
}
|
这是动态库的程序,func()只是个声明. 如果main程序以lazy打开则不会报错,而以NOW打开则会dlopen error!
RTLD_GLOBAL:
GLOBAL 可以看出,所有解析出来的符号是全局的,任何链接库都可以使用. 刚才那个rdnamic是把可执行程序变为全局的,而这个GLOBAL是把动态库变为全局可见的 .
另外 ld 有个选项是 -export-dynamic: 其实就和g++的编译选项-rdnamic的意思一样的:
When creating a dynamically linked executable, add all symbols to the dynamic symbol table. The dynamic symbol table is the set of symbols which are visible from dynamic objects at run time. If you do not use this option, the dynamic symbol table will normally contain only those symbols which are referenced by some dynamic object mentioned in the link. If you usedlopento load a dynamic object which needs to refer back to the symbols defined by the program, rather than some other dynamic object, then you will probably need to use this option when linking the program itself.