博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

程序的链接和加载基础

Posted on 2012-08-30 11:14  刘乐  阅读(218)  评论(0)    收藏  举报

1. 程序的链接

 

程序链接的核心是找“符号”,即函数或者变量定义的位置。“符号”可能位于其它程序文件,静态库,动态库,如果没找到,编译器可能报错,还可能帮你生成一个。

 


1.1 窥豹一斑 c++的extern关键字

 

c++编写的so,希望在c中使用。因为某种原因,c中只能用gcc编译器。

 

$ cat e.cc

const char *ecc(const char *x) { return x; }

$ gcc -o libecc.so -shared -fPIC -Wall e.cc

 

 

注意这里,我用了gcc编译,而不是g++,它的影响在1.2中可以看到。

 

 

$ cat main.cc            

int main() { ecc("Hello World"); return 0; }

$ cat main.c

int main() { ecc("Hello World"); return 0; }

 

 

main.c 和 main.cc 文件内容相同,文件名不同。

 

 

$ gcc -o main-cc main.cc -Wall

main.cc: In function 'int main()':

main.cc:1: error: 'ecc' was not declared in this scope

 

$ gcc -o main-c main.c -Wall 

main.c: In function 'main':

main.c:1: warning: implicit declaration of function 'ecc'

/tmp/ccwO2sQc.o: In function `main':

main.c:(.text+0xf): undefined reference to `ecc'

collect2: ld returned 1 exit status

 

 

同样的程序,报错完全不同。main.cc 报编译错误,找不到ecc的声明。main.c 报链接错误,没有定义ecc,但是给出了警告,说没有声明ecc。稍后讲为什么。

 

 

$ cat main.cc

struct A { A(const char *s) { } };

extern int ecc(const A &);

int main() { ecc("Hello World"); return 0; }

 

$ gcc -o main-cc main.cc -Wall

/tmp/ccq4LsaY.o: In function `main':

main.cc:(.text+0x1b): undefined reference to `ecc(A const&)'

/tmp/ccq4LsaY.o:(.eh_frame+0x12): undefined reference to `__gxx_personality_v0'

collect2: ld returned 1 exit status

 

 

现在没有编译错误了,这里提供了函数声明,虽然这个声明和我们想要链接的函数一点不搭边,但是还是编译通过了。编译时只认函数原型,这里const char* 被隐式转成了A。现在报链接错误,注意这里报找不到ecc(A const&),而不是ecc(const char *)。

 

 

$ cat main.cc

extern int ecc(const char *);

int main() { ecc("Hello World"); return 0; }

 

$ g++ -o main-cc main.cc -Wall -L. -lecc

 

 

现在编译,链接都没有问题。注意,虽然提供了错误的函数声明(返回值不对),但是编译和链接都不在乎这个,所以编译,链接都没有问题。这种三不管,会导致问题。

 

 

再来看 main.c

$ gcc -o main-c main.c -Wall -L. -lecc

main.c: In function 'main':

main.c:1: warning: implicit declaration of function 'ecc'

/tmp/ccADJnMJ.o: In function `main':

main.c:(.text+0xf): undefined reference to `ecc'

./libecc.so: undefined reference to `__gxx_personality_v0'

collect2: ld returned 1 exit status

 

 

已经用 -L 和 -l 指明了连接的库,还是找不到符号,为什么?

 

$ nm libecc.so | grep ecc

000000000000051c T _Z3eccPKc

 

的确没有ecc这个符号,因为libecc.so 是c++编译出来的so(虽然,我们用的是gcc),而c++为了支持namespace和函数重载,做了名字混淆。

 

仲么办?

 

$ cat main.c

int main() { _Z3eccPKc("Hello World"); return 0; }

$ gcc -o main-c main.c -Wall -L. -lecc -lstdc++

main.c: In function 'main':

main.c:1: warning: implicit declaration of function '_Z3eccPKc'

 

 

现在虽然有一个警告,但是编译链接通过。我们显式指定了libecc.so中ecc对应的名字。从这一步可以看出,链接器很好骗(要是女人也这么好骗就行了:)),它要的只是一个符号的名字。

这种方法,无疑非常的丑陋,但是如果不修改 libecc.so 的源文件,这是唯一的方法了。

 

 

$ cat e.cc

extern "C"

const char *ecc(const char *x) { return x; }

$ cat main.c

int main() { ecc("Hello World"); return 0; }

 

$ nm libecc.so | grep ecc

000000000000050c T ecc

 

 

通过 extern "C" 显式的告诉gcc,这个函数使用c语言的方式生成函数名字。这次main.c可以编译通过,但是main.cc又不可以了。因为main.cc要找的是_Z3eccPKc,而不是ecc。

 

 

$ cat main.cc 

extern "C" int ecc(const char *);

int main() { int i = ecc("Hello World"); return i; }

 

 

这里显式告诉gcc,找ecc这个符号时,不要做函数名混淆。


 

从上面的演示过程可以看出,链接时,所需的就是符号的名字(无论是ecc,还是_Z3eccPKc),所以碰到链接错误,只要考虑能不能找到这个名字。

 


1.2 gcc 和 g++ 的差别

 

我们通常所说的编译,其实分四个阶段,预处理(-E)编译(-c)汇编(-S)链接。一直到到链接,使用gcc和g++是没有区别的。其实一直到链接,它们也是没有区别的。前面的链错误里面有

./libecc.so: undefined reference to `__gxx_personality_v0',后来为什么没了?因为gcc 加了 -lstdc++。不用我说,你也猜得到,libstdc++.so 里定义了 __gxx_personality_v0。

 

 

objdump -d /usr/lib/libstdc++.so.6 | grep "__gxx_personality_v0"

00d580c0 <__gxx_personality_v0>:

 

 

因为c++增加了对象的构造和析构,所以需要编译器生成额外的代码,这个过程也是找“符号”而已。让gcc链接c++的so,还要显示链接libstdc++.so 是不是觉得怪怪的,没关系,如果我在生成libecc.so时使用g++而不是gcc,那么在连接main.c时就不需要-lstdc++了。

 

 

$ g++ -o libecc.so -shared -fPIC -Wall e.cc

$ readelf -d libecc.so 

 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]

 


1.3 静态库和动态库的编译

 

$ g++ -o libecc.so -shared -fPIC -Wall e.cc 

$ g++ -o ecc.o -c e.cc -Wall -fPIC; ar -r libecc.a ecc.o 

$ ls libecc.*

libecc.a  libecc.so

 

 

使用静态库要注意,libecc.a 要放到.c 或者.o 文件的后面

$ gcc -o main-c main.c -L. -lecc -Wall

$ gcc -o main-c main.c libecc.a -Wall -lstdc++

 

 

还可以用链接动态库的方式连接静态库,假如当前目录有libecc.a,以这种方式链接,需要静态库的命名,符合动态库的命名规则,lib+name+.a。再加上libecc.a不包含任何动态链接信息(libecc.so包含动态链接信息,这里的libstdc++只是个例子,如果在libecc.a中使用了openssl,那么使用静态库时也必须-lssl),所以,必须-lstdc++。

 

$ gcc -o main-c main.c -L. -lecc -Wall -lstdc++

 

 

假设当前目录同时有libecc.a和libecc.so,则优先链接动态库。通过-static参数强制使用库的静态版本,注意此时(所有)需要链接的库必须有静态库版本存在。没有办法以静态方式链接动态库。


1.4 编写c++动态库常用的手法

 

* 提供c语言包装函数,extern "C" { /* function define code */ }

* 提供c语言的头文件,__cplusplus 保证了c和c++都可以使用

#ifdef __cplusplus

extern "C" {

#endif

/* function declare code */

#ifdef __cplusplus

}

#endif

 


1.5 c 和 c++ 对待函数原型的问题

 

在上文,main.c 和 main.cc 都没有声明ecc函数原型(也没有-l指定动态库),main.c 报链接错误,main.cc 报编译错误,为什么?

 

$ gcc -o main-cc main.cc

main.cc:1: error: 'ecc' was not declared in this scope

 

$ gcc -o main-c main.c

main.c:(.text+0xf): undefined reference to `ecc'

 

 

这是c和c++不同的地方,在c++中,编译时必须看到函数声明(原型),而在c中则不然。在C99之前,如果编译过程中找不到函数原型,会使用默认函数原型,返回值类型为int,参数类型为传递的参数类型。并且在满足下面4个条件是,默认函数原型工作正确。

1. 如果函数返回int。

2. 如果函数函数有固定个数的参数。

3. 参数类型是类型提升后的。(例如int是好的,char不行,因为int是类型提升后的)。

4. 传递的参数个数正确。

 

 

$ cat main.c 

#include 

int main() { printf("%d\n", (int) atof("1234.3")); return 0; }

$ gcc main.c -o main; ./main

858993459

 

 

正确的输出应该是1234,但是这里输出了858993459。因为atof在stdlib.h中声明,而我们没有包含这个头文件,程序编译使用原型是 int atof(const char *nptr); 


1.7 符号查找路径

 

通常在.h文件中声明函数,在.c/.cc文件(静态库/动态库)中定义函数。上面的演示中,在需要函数声明的地方,都是手工声明的。

 

头文件的查找路径,如果#include "header",那么header在当前路径查找,如果#include <header>,那么header在/usr/include 和 /usr/local/include 路径查找。动态库和静态库在/lib,/usr/lib 和 /usr/local/lib。

除去这些标准路径,还可以通过-I参数指定头文件的查找路径(具体哪个头文件在源代码里指定了),这个是预处理过程(CPPFLAGS)。通过-L参数指定库的位置(LDFLAGS),-l参数指定具体连接那个库(LIBS),这个是链接过程。

 

需要注意的是:无论是标准路径还是-L指定的路径,都需要用-l指定库的名字。例如-lmysqlclient,会在libmysqlclient.so中查找符号。


1.8 库的版本问题

 

$ gcc -o libtx.so -shared -fPIC tx.c

$ gcc -o libty.so.1.0 -shared -fPIC -Wl,-soname -Wl,libty.so.1 ty.c

$ gcc -o libty.so.2.0 -shared -fPIC -Wl,-soname -Wl,libty.so.2 ty.c

$ ls -l libt*

libtx.so

libty.so.1.0

libty.so.2.0

 

# echo $PWD > /etc/ld.so.conf.d/libt.conf

$ ls -l libt*

libtx.so

libty.so.1 -> libty.so.1.0

libty.so.1.0

libty.so.2 -> libty.so.2.0

libty.so.2.0

 

$ readelf -d libty.so.1.0 

 0x000000000000000e (SONAME)             Library soname: [libty.so.1]

ldconfig 读取soname,如果soname和文件名不同,和soname同名的软连接到文件。

 

$ gcc -o main main.c -ltx -lty -L.

/usr/bin/ld: cannot find -lty

collect2: ld returned 1 exit status

$ ln -sf libty.so.2 libty.so

$ gcc -o main main.c -ltx -lty -L.

$ readelf -d main

 0x0000000000000001 (NEEDED)             Shared library: [libty.so.2]

 

假如,我想链接libty.so.1仲么办?笨办法是把libty.so.1复制到新的目录,创建libty.so 到 libty.so.1的软连接。其实还可以这样。

$ gcc -o main main.c -ltx -L. libty.so.1

$ readelf -d main

 0x0000000000000001 (NEEDED)             Shared library: [libty.so.1]

(注意,libty.so.1前面的空格)不使用-l参数,而是直接把so的全路径写出来。想想你是怎么链接静态库的你就明白了。


1.9 动态库/静态库和可执行文件的区别

 

动态库/静态库本质上只编译,不链接,就算库里面出现的未决(找不到定义,找符号的定义是链接和加载的事情)符号,也不会报错。

$ cat e.cc

const char *lx(const char *x);

const char *ecc(const char *x) { return lx(x); }

$ gcc -o libecc.so -shared -fPIC -Wall e.cc

$ nm libecc.so | grep -P "lx|ecc"

                 U _Z2lxPKc

000000000000055c T _Z3eccPKc

 

nm 查看 U 是未定义符号的意思(lx未定义),T 表明符号定义在代码段(ecc)。

 

$ cat e.cc

static const char *lx(const char *x);

const char *ecc(const char *x) { return lx(x); }

$ gcc -o libecc.so -shared -fPIC -Wall e.cc

e.cc:1: error: 'const char* lx(const char*)' used but never defined

 

编译器处理static函数的方式不一样,因为static函数的作用域是文件。

 

$ cat e.cc

namespace { const char *lx(const char *x); }

const char *ecc(const char *x) { return lx(x); }

$ g++ -o libecc.so -shared -fPIC -Wall e.cc

/usr/bin/ld: /tmp/ccINDBqF.o: relocation R_X86_64_PC32 against `(anonymous namespace)::lx(char const*)' can not be used when making a shared object; recompile with -fPIC

/usr/bin/ld: final link failed: Bad value

collect2: ld returned 1 exit status

 

编译器处理匿名namespace(c++设计用来代替static函数的机制)也不允许未定义,它的处理方式也很不一样,做了链接处理。


2. 程序的加载
2.1 指定运行时库的加载地址

 

$ gcc -o main main.c -ltx -lty  -L. -Wl,-rpath -Wl,./lib

readelf -d main

 0x000000000000000f (RPATH)              Library rpath: [./lib]

 

这里用-rpath 指定运行时,优先从哪个个路径(多个路径用冒号分隔)加载动态库。该选项适用于程序有自己的so,但是又不想将该so安装到标准路径。

注意,这个路径和运行是加载有关,和链接时无关。


2.2 运行时库的加载顺序

 

* 编译时用rpath制定的路径

* LD_LIBRARY_PATH 环境变量指定的路径

* /etc/ld.so.cache 中指定的路径

* /lib 和 /usr/lib 标准路径


2.3 ldconfig 维护 /etc/ld.so.cache

 

$ cat /etc/ld.so.conf

include ld.so.conf.d/*.conf

/usr/local/lib

 

ldconfig 根据 ld.so.conf 的配置,生成类似库名到库地址的列表,并将结果保存在/etc/ld.so.cache中。例如

$ /sbin/ldconfig -p | grep stdc++

        libstdc++.so.6 (libc6,x86-64) => /usr/lib64/libstdc++.so.6

        libstdc++.so.6 (libc6) => /usr/lib/libstdc++.so.6

 

程序启动加载动态库时,查找/etc/ld.so.cache文件,如果找到对应的库,则去响应的路径加载。这也是很多时候,我们安装了库,需要执行ldconfig的原因。


3. 常用的工具

 

* nm 查看目标文件的符号,适合.o,.a,.so,exe。

* objdump 查看目标文件的信息。比nm更强大。

* readelf 查看ELF格式的可执行文件。

* addr2line

* ldd 查看依赖的共享库

* ldconfig

 

这些工具的使用详解man手册,我也只是知道几个选项。