Linux中的动态库和静态库(.a/.la/.so/.o)

在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++编译的几个过程。

  1. 预处理,展开头文件,宏定义,条件编译处理等。通过gcc -E source.c -o source.i或者cpp source.c生成。

  2. 编译。这里是一个狭义的编译意义,指的是将预处理后的文件翻译成汇编代码的过程。通过gcc -S source.i生成。默认生成source.s文件。

  3. 汇编。汇编即将上一步生成的汇编代码翻译成对应的二进制机器码的过程。通过gcc -c source.s来生成source.o文件。

  4. 链接。链接是将生成目标文件和其引用的各种符号等生成一个完整的可执行程序的过程。链接的时候会进行虚拟内存的重定向操作。

上面四个步骤就是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等来使用,这里就不介绍了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值