C语言中的静/动态库文件

默认分类,C 2019-05-03 5448 字 1456 浏览 点赞

起因

之前一直对 .a .so 文件的存在犯迷糊。现在想来,之所以当时会有不解,是因为从没用 C 语言写过项目,顶多只是语法的测试,以及一些算法实现。现在略微搞明白些了,于是有了这篇总结。

编译

当尝试在终端敲入 gcc -o test test.c 时,其实就是告诉编译器,请把我的代码(test.c)编译成一个可执行文件(test)。在这个短短过程中,代码经历了四个阶段,分别是:1. 预处理阶段 2. 编译阶段 3. 汇编阶段 4. 链接阶段。

不同阶段有不同的事儿要做,从头至尾如下:

  • 预处理阶段:1. 删除注释 2. 宏替换 3. 条件编译 4. 将头文件放到 .c 文件中,最后生成 .i 文件;(gcc -E -o test.i test.c
  • 编译阶段:将 C 语言翻译成汇编语言,生成 .s 文件;(gcc -S -o test.s test.i
  • 汇编阶段:将汇编翻译成机器语言,生成 .o 文件;(gcc -o test.o -c test.s
  • 链接阶段:将 .o 文件链接生成可执行文件。(gcc -o test test.o

在上述四个阶段中,.i 和 .s 都是文本文件,你可以打开来看看里面都写着啥。后两个阶段是二进制文件,你想打开看,那就看一堆乱码。也并非束手无措,可以用 objdump 命令反汇编。像下面这样:

root@hui:make$ objdump -xd test.o

test.o:     file format elf64-x86-64
test.o
architecture: i386:x86-64, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x0000000000000000
...

函数库

前面唠叨那么多,就是想引出 .o 文件。

函数库,顾名思义:里边存放了一堆供程序员使用的函数。其实不但有函数名、函数对应的实现代码,还有链接过程中所需的重定位信息。

函数库分为静态库(.a 文件)和动态库(.so)文件。Linux 中,标准的 C 函数库放在 /usr/lib 目录下;而用户,也可以根据自生需求,建立自己的用户函数库。

这里需要注意,由于函数库来自于 .o 文件,也就是说,是一堆二进制文件构成,你看不到里面的库函数代码。所以库函数该怎么用呢?这就体现了头文件中的重要性。所以 .h 文件与 .c 最好是要分开写。

静态库

静态库是一些 .o 目标文件的集合,以 .a 结尾,在程序链接的时候使用。链接器会将程序中使用到的函数代码从库文件中拷贝到可执行文件里边,完成链接后,可执行文件不需要静态库也能独立执行。

静态库的创建方式:

$ ar rcs lib库文件名.a 目标文件1.o 目标文件2.o 目标文件3.o ...

参数说明:

  • r 表示将 .o 的目的文件加入到静态库中;
  • c 表示创建静态库;
  • s 表示生产索引。

创建的库文件名字前面最好加上前缀 lib ,方便编译器找到它。(你可以认为这是一种规范)

静态库的使用方式:

$ gcc -o 可执行文件 源码.c -I[头文件目录] -L[库文件目录] -l[库文件]

$ gcc -o 可执行文件 目标文件.o -L[库文件目录] -l[库文件]

参数说明:

  • -L 表示将指定库文件所在目录加入到库搜索路径中去,默认库搜索路径为 /usr/lib 目录;
  • -l(小写 L) 表示库搜索路径下的某个库文件。需要注意,如果你的库文件名以 lib 开头,如:libguan.a ,那么配合该参数时直接 -lguan 即可;如果不是 lib 开头,则不能用 -l 参数,而是直接指明 .a 文件的路径。
  • -I(大写 i),表示指明头文件的所在目录。

举例简单说明一下用法。

这里有目录 demo ,目录结构如下:

root@hui:test$ tree demo/
demo/
├── include
│   └── guan.h
└── src
    ├── guan.c
    └── main.c

2 directories, 3 files

guan.h 文件提供函数声明,内容如下:

// root@hui:demo$ cat include/guan.h 
#ifndef __GUAN_H__
#define __GUAN_H__

#include <stdio.h>
#include <stdarg.h>

void print(char *fmt, ...);

#endif

guan.c 源码文件存放函数 print 的定义:

// root@hui:demo$ cat src/guan.c 
#include "guan.h"

void
print(char *fmt, ...) {
    
    char msg[1024 * 4] = {};
    va_list sp;
    
    va_start(sp, fmt);
    vsprintf(msg, fmt, sp);
    va_end(sp); 

    printf("%s:", "Guan");
    printf("%s\n", msg);
}

在主程序 main.c 文件中,调用 print() 函数:

// root@hui:demo$ cat src/main.c 
#include <stdio.h>
#include "guan.h"  // 注意,需要映入头文件

int main()
{
    print("%s", "怀念我跑进雨天的冒险。");
}

现在,我们先在当前目录下创建一个 lib 目录,用来存放生成的 .a 静态库文件。在这之前,我们还需要生成 .o 结尾的目标文件。操作如下:

root@hui:demo$ gcc -o src/guan.o -c src/guan.c -Iinclude  # 生成 .o 的目标文件
root@hui:demo$ tree src/
src/
├── guan.c
├── guan.o  # 文件生成
└── main.c

0 directories, 3 files
root@hui:demo$ mkdir lib  # 创建 lib 目录,用于存放 .a 文件
root@hui:demo$ ls
include  lib  src
root@hui:demo$ ar rcs lib/libguan.a src/guan.o  # 生成 .a 文件
root@hui:demo$ tree lib/
lib/
└── libguan.a  # 文件生成

0 directories, 1 file
root@hui:demo$ mkdir bin  # 创建 bin 目录,用来存放可执行文件
root@hui:demo$ ls
bin  include  lib  src
root@hui:demo$ gcc -o bin/app src/main.c -Iinclude -Llib -lguan  # 生成可执行文件 app
root@hui:demo$ bin/app  # 运行可执行文件
Guan:怀念我跑进雨天的冒险。  # 可执行文件的输出结果

如果生成 .a 文件时,没有用 lib 作为前缀,那么编译 main.c 时命令应该如下这样:

gcc -o bin/app2 src/main.c -Iinclude lib/guan.a

前面说静态库的使用时,有两种使用方式,第一种上面已经说过了。第二种,就是先把 main.c 生成 main.o 的目标文件,这样一下,生成执行文件时,就不需要指定头文件的目录了。

root@hui:demo$ gcc -o src/main.o -c src/main.c -Iinclude   # 生成 main.o 文件
root@hui:demo$ ls src/ |grep main
main.c
main.o
root@hui:demo$ gcc -o bin/app3 src/main.o -Llib -lguan  # 生成可执行文件 app3
root@hui:demo$ bin/app3  # 执行可执行文件
Guan:怀念我跑进雨天的冒险。

动态库

动态库又叫作共享库,在 linux 中以 .so 结尾。与静态库不同在于,动态库不会直接把调用的代码拷贝到执行程序中去,而是在执行程序中作下标记,等到程序运行时才去加载需要的函数。这就意味着执行程序在运行时需要动态库做支持,自己不能独立执行。这样做的好处是:如果某个模块修改了代码,不需要将整个程序都重新编译,而是编译一部分,生成新的 .so 文件即可。

同时,动态库链接出来的执行文件会比静态库要小得多。

动态库的创建方式:

# 第一种方式
gcc -fPIC -o 目标文件1.o -c 非主程序源码1.c
...
gcc -shared -o lib库文件名.so 目标文件1.o 目标文件2.o ...


# 第二种方式
gcc -fPIC -shared -o lib库文件名.so 非主程序源码.c

参数说明:

  • -shared 表示使用共享库(动态库);
  • -fpci 表示创建产生独立目标代码。

动态库文件的生成稍稍比静态库文件麻烦些。先生成 .o 的目标文件时,一定要加上 -fpic 这个参数,然后生成 .so 文件时,不要忘了 -shared 参数。也可以把 .c 到 .so 这个过程合并起来,也就是上边说的第二种方法。如果仍以之前的目录结构为例,命令就应该像下面这样:

gcc -shared -fpic -o lib/libguan.so src/guan.c -Iinclude  # 生成动态库文件

动态库的使用方式:

gcc -o 可执行文件 源码.c -I[头文件目录] -L[库文件目录] -l[库文件]

gcc -o 可执行文件 目标文件.o -L[库文件目录] -l[库文件]

这同使用静态库一样。但需要注意的是,由于链接动态库的执行程序在运行过程中会用到里边的库函数,那么执行程序必须知道动态库在哪里。怎样看执行程序知不知道动态库在哪里呢?执行一下咯!

倒也不是开玩笑,看程序是否能够正常执行可以鉴别它知不知道动态库在哪里。但不执行程序,也可以知道。这里需要用到命令:ldd

root@hui:demo$ ldd bin/app 
        linux-vdso.so.1 =>  (0x00007ffe403a2000)
        libguan.so => not found  # 注意,没找到动态库
        libc.so.6 => /lib64/libc.so.6 (0x00007f1faa60e000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f1faa9db000)

解决方案有三种:

  • 第一种:将动态库文件放到 /usr/lib 目录下(但要注意,我上面输出 libc.so.6 那一行是在 /lib64 目录下找到的,所以我的环境里需要把动态库文件放到 /usr/lib64 这个目录下);
  • 第二种:使用命令:export LD_LIBRARY_PATH=库文件目录 (只在声明时的所在终端有效)
  • 第三种:vim /etc/ld.so.conf;在配置文件中加入 lib 目录的绝对路径,保存退出;执行命令 ldconfig

如果程序运行过程中动态库文件被删除,程序就不能正常运行下去。

感谢

  • 参考 海同网校王正平老师的 C 语言之多模块软件的编译和链接


本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论