在windows下,一般可以通过文件的后缀名来识别文件的类型。在Linux下大致上也是可以的。但是要明确的一点是,在linux下,文件的后缀与文件的类型是没有必然的联系的。这只是约定俗称的习惯罢了。
在linux 下进行C/C++开发,一般都是使用的gcc编译器,所以本文的讲解以gcc为主。
-
.o文件,即目标文件。一般通过.c或者.cpp文件编译而来,相当于VC编译出来的obj文件
-
.so文件,shared object 共享库(对象),相当于windows下的dll。
-
.a文件,archive 归档包,即静态库。其实质是多个.o文件打包的结果,相当于VC下的.lib文件
-
.la文件,libtool archive 文件,是libtool自动生成的共享库文件。
下面对这四种文件进行逐个说明。
C/C++程序编译的过程
先说一下C/C++编译的几个过程。
-
预处理,展开头文件,宏定义,条件编译处理等。通过gcc -E source.c -o source.i或者cpp source.c生成。
-
编译。这里是一个狭义的编译意义,指的是将预处理后的文件翻译成汇编代码的过程。通过gcc -S source.i生成。默认生成source.s文件。
-
汇编。汇编即将上一步生成的汇编代码翻译成对应的二进制机器码的过程。通过gcc -c source.s来生成source.o文件。
-
链接。链接是将生成目标文件和其引用的各种符号等生成一个完整的可执行程序的过程。链接的时候会进行虚拟内存的重定向操作。
上面四个步骤就是C/C++程序编译的几个基本步骤。前面三个步骤都是很简单,大多时候会合并为一个步骤。只有第四个步骤链接是复杂一点的。很多时候我们编译比较大的项目,报错的往往是在链接的时候缺少某些库,或者某些符号找不到定义,重定义等
.o文件(目标文件)
.o文件就是C/C++源码编译的结果。即上面所说的C/C++编译过程中的前三步。一般开发中很少将这三步分开来做,通常的做法是一步生成。
这里举个例子,我们来写一个函数int atoi(const char* str)。
头文件atoi.h
.#ifndef ATOI_H
.#define ATOI_H
int atoi(const char* str);
.#endif //! ATOI_H
源文件atoi.c
.#include <stdio.h>
.#include "atoi.h"
int atoi(const char* str)
{
int ret = 0;
if(str != NULL){
sscanf(str,"%d",&ret);
}
return ret;
}
创建atoi.o
直接使用命令gcc -c atoi.c -o atoi.o或gcc -c atoi.c来生成目标文件。
上面我们函数中调用了sscanf这个C标准库中的函数,那么它在.o文件中会记录这个存在,我们可以使用readelf工具来查看一下。
o@Neo-kylin:~/lib_a_so$ ls
atoi.c atoi.h
o@Neo-kylin:~/lib_a_so$ gcc -c atoi.c
o@Neo-kylin:~/lib_a_so$ ll
总用量 20
drwxr-xr-x 2 o o 4096 10月 10 15:11 ./
drwxrwxr-x 5 1000 1000 4096 10月 10 14:32 ../
-rw-rw-r-- 1 o o 140 10月 10 15:07 atoi.c
-rw-rw-r-- 1 o o 75 10月 10 15:07 atoi.h
-rw-rw-r-- 1 o o 1536 10月 10 15:11 atoi.o
o@Neo-kylin:~/lib_a_so$ readelf -s atoi.o
Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS atoi.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 60 FUNC GLOBAL DEFAULT 1 atoi
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND __isoc99_sscanf
这就是.o文件了。它保存了编译的时候引用到的符号(函数,全局变量等),这些符号,在链接的时候需要使用到。
使用atoi.o
使用atoi.o的地方有很多,就不一一列举了。这里先不说怎么用,后面生成.a文件的时候用到了。
.a文件(静态库文件)
静态库是多个.o文件的打包的结果,前面已经说过了,其实不一定非要多个文件,一个.o文件也可以打包为.a文件。
这一步使用ar工具来操作。ar工具是用来创建, 修改和提取archives归档文件的工具,具体使用可以看manpages。
ar [emulation options] [-]{dmpqrstx}[abcfilNoPsSuvV] [member-name] [count] archive-file file...
这个工具的作用看起来很简单,但是其是很强大,且参数的设置很复杂的。这里不是为了介绍这个工具,不细说了。
创建atoi.a
我们先使用上面生成的atoi.o文件来生成一个atoi.a文件。
o@Neo-kylin:~/lib_a_so$ ls
atoi.c atoi.h atoi.o
o@Neo-kylin:~/lib_a_so$ ar -r atoi.a atoi.o
ar: creating atoi.a
o@Neo-kylin:~/lib_a_so$ ll
总用量 24
drwxr-xr-x 2 o o 4096 10月 10 15:35 ./
drwxrwxr-x 5 1000 1000 4096 10月 10 14:32 ../
-rw-rw-r-- 1 o o 1678 10月 10 15:35 atoi.a
-rw-rw-r-- 1 o o 140 10月 10 15:07 atoi.c
-rw-rw-r-- 1 o o 75 10月 10 15:07 atoi.h
-rw-rw-r-- 1 o o 1536 10月 10 15:11 atoi.o
-r参数的意思是替换已存在的或插入新的文件到archive包中。
使用atoi.a
创建了atoi.a文件后,我们就可以来使用它了。这里我们写一个main函数来调用atoi。
main.c文件
int main()
{
return atoi("5");
}
这一次我们先把main.c编译为main.o文件。
o@Neo-kylin:~/lib_a_so$ ls
atoi.a atoi.c atoi.h atoi.o main.c
o@Neo-kylin:~/lib_a_so$ gcc -c main.c
o@Neo-kylin:~/lib_a_so$ ls
atoi.a atoi.c atoi.h atoi.o main.c main.o
然后使用ld程序来链接main.o和atoi.a
o@Neo-kylin:~/lib_a_so$ ld main.o atoi.a -o main
ld: warning: cannot find entry symbol _start; defaulting to 00000000004000b0
atoi.a(atoi.o): In function `atoi':
atoi.c:(.text+0x33): undefined reference to `__isoc99_sscanf'`
上面报了一个错误,原因是在atoi函数中使用未定义的引用 __isoc99_sscanf,这个问题我们可以通过链接上libc.a或者libc.so来解决这个问题。通常的情况下,都是链接libc.so来解决的,如果使用glibc的静态库,那么你也必须将你的程序开源,不然这应该算是违反GPL协议的约定。
o@Neo-kylin:~/lib_a_so$ ld main.o atoi.a /lib64/libc.so.6 -o main
ld: warning: cannot find entry symbol _start; defaulting to 0000000000400288
这里又报了一个警告,是没有发现_start符号的意思。这是因为没有发现程序主入口点的原因。在C语言中,程序的入口函数是main,但是在汇编中,程序的主入口函数是_start。
这里我们可以把main.c文件中的main函数改为_start函数,然后再编译为main.o再链接就没有问题了。但是这不是正确的做法,这样做虽然使用ld来链接是不会报错了,但是程序是运行不了的。会报错误
o@Neo-kylin:~/lib_a_so$ ld main.o atoi.a /lib64/libc.so.6 -o main
o@Neo-kylin:~/lib_a_so$ ./main
-bash: ./main: /lib/ld64.so.1: bad ELF interpreter: 没有那个文件或目录
正确的做法是链接上crt0.o、crti.o、crtn.o等很多个文件就行了,不同的机器,需要链接的文件的位置可能不一样。这个参数可能非常长,普通人记不住。
这个是可以怎么得到呢?我肯定不知道这些文件都在什么位置,但是gcc编译环境知道,我们可以使用gcc来获取。
o@Neo-kylin:~/lib_a_so$ gcc -v -o main main.o atoi.a
使用内建 specs。
目标:x86_64-redhat-linux
配置为:../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-languages=c,c++,objc,obj-c++,java,fortran,ada --enable-java-awt=gtk --disable-dssi --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-1.5.0.0/jre --enable-libgcj-multifile --enable-java-maintainer-mode --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --disable-libjava-multilib --with-ppl --with-cloog --with-tune=generic --with-arch_32=i686 --build=x86_64-redhat-linux
线程模型:posix
gcc 版本 4.4.7 20120313 (Red Hat 4.4.7-16) (GCC)
COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/4.4.7/:/usr/libexec/gcc/x86_64-redhat-linux/4.4.7/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/:/usr/lib/gcc/x86_64-redhat-linux/:/usr/libexec/gcc/x86_64-redhat-linux/4.4.7/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/:/usr/lib/gcc/x86_64-redhat-linux/
LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/4.4.7/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-o' 'main' '-mtune=generic'
/usr/libexec/gcc/x86_64-redhat-linux/4.4.7/collect2 --eh-frame-hdr --build-id -m elf_x86_64 --hash-style=gnu -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o main /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crti.o /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtbegin.o -L/usr/lib/gcc/x86_64-redhat-linux/4.4.7 -L/usr/lib/gcc/x86_64-redhat-linux/4.4.7 -L/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../.. main.o atoi.a -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crtn.o
编译之后我们可以来看以下程序运行结果是否正确。
o@Neo-kylin:~/lib_a_so$ ./main
o@Neo-kylin:~/lib_a_so$ echo $?
5
结果为5,与预期一致。
.so文件(共享库文件)
共享库文件和windows下的dll文件(dynamic link library)的概念是一样的,都是在程序运行的时候进行动态链接,供程序调用的。
在linux 下可以使用ldd命令来查看某个可执行文件需要链接哪些共享库(动态库),并可以确定这些要链接的共享库在本机中的位置。
o@Neo-kylin:~/lib_a_so$ ldd main
linux-vdso.so.1 => (0x00007fffab1ff000)
libc.so.6 => /lib64/libc.so.6 (0x000000305d800000)
/lib64/ld-linux-x86-64.so.2 (0x000000305d000000)
这里要说以下动态库的查找路径。对于程序需要链接的动态库xxx.so,如果它在当前目录下有,那么链接当前目录下的。如果没有,那么就链接系统/etc/ld.so.cache(可通过ldconfig来更新)文件中查找xxx.so的路径,如果都没有,那么就会报错啦。
我们在当前目录创建一个libc.so.6文件,然后再使用ldd看一下。
o@Neo-kylin:~/lib_a_so$ touch libc.so.6 && chmod +x libc.so.6
o@Neo-kylin:~/lib_a_so$ ls -l libc.so.6
-rwxrwxr-x 1 o o 0 10月 10 17:15 libc.so.6
o@Neo-kylin:~/lib_a_so$ ldd main
./main: error while loading shared libraries: ./libc.so.6: file too short
可以看到,这时候是链接的当前目录下的libc.so.6这个文件,很可惜,出错了。
其实在链接的时候,我们可以通过-Wl,-rpath=sopath来指定运行时加载动态库的路径。这样做的好处是可以把一些动态库的位置信息不加入到/etc/ld.so.cache中,已经避免和系统已有动态库产生冲突的情况。(例如目标机器的glibc库版本太低,而编译程序的时候使用的高版本的,而出现”libc.so.6: version `GLIBC_2.14’ not found”类似的错误的情况)
注: -Wl: 表示后面的参数将传给link程序ld,gcc编译时候的链接实际上是调用ld进行的.
创建atoi.so
这里还是使用前面创建的atoi.c文件创建atoi.so文件。实际上我们这里创建atoi.so.1文件,文件名后面的.1代表的是版本号。动态库因为使用的时候是动态链接的,而不是直接链接到目标程序文件中的。所以可能同时存在多个版本的情况,一般都会指定版本号。
通常使用libxxx.so.主版本号.副版本号的形式来命名。
o@Neo-kylin:~/lib_a_so$ gcc -shared -o atoi.so.1 atoi.c
/usr/bin/ld: /tmp/ccLK0pLC.o: relocation R_X86_64_32 against `.rodata' can not be used when making a shared object; recompile with -fPIC
/tmp/ccLK0pLC.o: could not read symbols: Bad value
collect2: ld 返回 1
o@Neo-kylin:~/lib_a_so$ gcc -fPIC -shared -o atoi.so.1 atoi.c
o@Neo-kylin:~/lib_a_so$ ls
atoi.a atoi.c atoi.h atoi.o atoi.so.1 main.c main.o
-share该选项指定生成动态连接库(让连接器生成T类型的导出符号表,有时候也生成弱连接W类型的导出符号,后面介绍nm工具的时候再说),不用该标志外部程序无法连接。相当于一个可执行文件。
-fPIC表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。
第一次没有指定-fPIC的时候出错了,原因是针对可迁移R_X86_64_32平台,只读数据段’.rodata’不能创建成共享对象,原因是在动态链接动态库的时候,如果没有编译成位置无关代码,那么链接的时候可能因为某些代码的位置具有相关性,而在执行时出现错误。可执行文件在链接时就知道每一行代码、每一个变量会被放到线性地址空间的什么位置,因此这些地址可以都作为常数写到代码里面。对于动态库,只有加载的时候才知道。
如果代码中没有只读数据段,那么就不会有这个问题。例如
o@Neo-kylin:~/lib_a_so$ cat >val.c
int a= 100;
o@Neo-kylin:~/lib_a_so$ gcc -shared -o val.so val.c
使用atoi.so
使用.so文件的形式和使用.a也差不多,也是使用ld来进行链接。因为这过于复杂,还是使用gcc来做这个操作(实际上gcc也是使用的ld)。
o@Neo-kylin:~/lib_a_so$ gcc -o main main.o atoi.so.1
o@Neo-kylin:~/lib_a_so$ ldd main
linux-vdso.so.1 => (0x00007fff56eaf000)
atoi.so.1 => not found
libc.so.6 => /lib64/libc.so.6 (0x000000305d800000)
/lib64/ld-linux-x86-64.so.2 (0x000000305d000000)
o@Neo-kylin:~/lib_a_so$ ./main
./main: error while loading shared libraries: atoi.so.1: cannot open shared object file: No such file or directory
上面执行的时候报错,意思是找不到atoi.so.1这个文件。原因是共享库的查找目录没有当前目录,我们可以添加环境变量LD_LIBRARY_PATH来使系统动态载入器 (dynamic linker/loader)在当前目录也查找。
o@Neo-kylin:~/lib_a_so$ export LD_LIBRARY_PATH=.
o@Neo-kylin:~/lib_a_so$ ldd main
linux-vdso.so.1 => (0x00007fff08fff000)
atoi.so.1 => ./atoi.so.1 (0x00007f9ed7ac6000)
libc.so.6 => /lib64/libc.so.6 (0x000000305d800000)
/lib64/ld-linux-x86-64.so.2 (0x000000305d000000)
o@Neo-kylin:~/lib_a_so$ ./main
o@Neo-kylin:~/lib_a_so$ echo $?
5
还有一种办法,比添加环境变量更好使,也更具有可移植性,那就是编译的时候指定运行的时候共享库的加载路径。gcc使用-Wl,-rpath=sopath来指定,其中sopath是共享库放置的路径(可以是绝对路径,也可以是相对路径)。
o@Neo-kylin:~/lib_a_so$ gcc -o main main.o -Wl,-rpath=. atoi.so.1
o@Neo-kylin:~/lib_a_so$ ldd main
linux-vdso.so.1 => (0x00007fff457ff000)
atoi.so.1 => ./atoi.so.1 (0x00007fb946d56000)
libc.so.6 => /lib64/libc.so.6 (0x000000305d800000)
/lib64/ld-linux-x86-64.so.2 (0x000000305d000000)
o@Neo-kylin:~/lib_a_so$ ./main
o@Neo-kylin:~/lib_a_so$ echo $?
5
动态库还可以通过dlopen/dlsym等来使用,这里就不介绍了。