linux中的静态库、动态库详解

linux中的静态库、动态库详解

要说linux中的库使用,首先还得从gcc的编译流程说起。不然,总是说不清,理还乱,读完似懂非懂不如不看。

可执行文件的编译过程

在使用gcc编译程序时,编译过程可以被细分为4个阶段:

  1. 预处理(Pre-Processing):
  2. 编译(Compiling)
  3. 汇编(Assembling)
  4. 链接(Linking)

而我们通常使用gcc -o hello hello.c编译程序实际是包含了(hello.c->hello.i–>hello.s–>hello.o–>hello)上边的4个步骤

  1. 关于预处理,读者可以使用gcc -E -o hello.i hello.c预处理后的文件.
  2. gcc -S -o hello.s hello.i生成汇编文件
  3. gcc -c -o hello.o hello.s生成机器码.尽管已经得到了机器码,但这个文件却还是不可以运行的,必须要经过链接才能运行
  4. gcc hello.o。得到可执行文件。

由于链接与库的生成和使用相关,所以链接是我们要讨论的重点。

在第三阶段汇编中生成的二进制文件main.o我们通常叫它可重定位文件.而在第四阶段生成的二进制文件main我们叫它可执行目标文件。一般我们的程序会包含很多个.c文件吧,这些.c文件经过编译,汇编后就会变成一组可重定位文件。然后通过链接器将他们生成一个可执行目标文件(也就是我们最终的运行程序)。当然链接器也可以将库作为输入,和可重定位文件一起生成我们所要的可执行目标文件

在链接器生成可执行目标文件过程中根据链接器链接的行为将其分为静态链接动态链接

  1. 静态链接:在生成可执行目标文件时,将程序用到的模块从(比如函数printf())复制到可执行目标文件中。之后在程序的加载、执行过程中与此库再无关系。
    1. 优点:生成的可执行文件没有依赖,在运行时不存在缺少依赖库的问题。
    2. 缺点:生成的可执行文件大,浪费存储空间。此外,当库发生更新时,此程序需要重新编译链接。不方便。
  2. 动态链接: 为解决静态链接的缺点,提出了动态链接。在生成可执行目标文件时,将程序用到库的模块只进行简单的标记。之后在程序的加载执行过程中与程序进行链接。
    1. 优点: 生成的可执行文件小。由于文件中并不包含库中模块的实体函数,所以当库发生更新时,不需要对程序进行重新编译、链接。
    2. 缺点: 存在库依赖的问题。

我们经常使用的gcc -o helloworld helloworld.o生成的可执行文件helloworld是动态链接的。gcc默认使用动态链接。使用-static进行静态链接.如gcc -static -o helloworld helloworld.o.它俩生成的文件可以做一个大小比较,会发现动态生成的要比静态的小很多。对于动态链接生成的文件,我们可以使用ldd命令查看它的所有依赖库,如ldd ./helloword.

那么什么是库呢?其实就是将我们用来完成某些功能的程序块被打包成一个特定格式的文件。相应的,静态链接时用到的库格式我们叫静态库,后缀名为.a。而动态链接时用到的库格式我们叫动态库(共享库),后缀名为.so。动态库又分为加载时的动态链接库(程序在加载时进行库链接)和运行时的动态链接库(程序在运行时进行库链接)。千呼万唤始出来,终于见到我们的主角:静态库动态库了。

linux使用lib+库名.后缀名的格式给库命名。比如名叫mytest的静态库应该命名为libmytest.a.相应的动态库则命名为libmytest.so.除此之外,常在动态库后缀名后边加版本号.x.y.z.其中x为主版本号,y为次版本号,z为发行版本号。如libmytest.so.1.2.3。一般的,linux系统中的库都存放在/usr/lib/(存放系统相关的库)和/usr/local/lib/(存放用户相关的库)下。

静态库的生成与使用

使用find /usr/lib/ -name *.a命令可以查看/usr/lib/下的静态库。我们最常用的printf()等函数都包含在libc.a的静态库中。这些是系统自带的静态库,当然我们可以生成自己的静态库。用下面的程序举个创建例程:

//建立三个文件 main.c hello.c hello.h,其中我们用hello.c创建我们的静态库libhello.a.用main.c创建我们的可执行文件main,而hello.h为libhello.a的接口。在main.c中调用了libhello.a中的模块HelloPrintf().程序如下:

//-------------------main.c文件内容
#include "hello.h"

void main(void)
{
    HelloPrintf();
}

//-------------------hello.c文件内容
#include <stdio.h>
#include "hello.h"

void HelloPrintf(void)
{
    printf("hello word\n");
}

//-------------------hello.h文件内容
#ifndef __HELLO__H__
#define __HELLO__H__

void HelloPrintf(void);

#endif

程序已经在上面创建好,我们现在就演示其库的生成和使用步骤:

  1. 使用命令gcc -c -o hello.o hello.c生成可重定位文件hello.o,用于后面生成libhello.a静态库。
  2. 使用命令ar rcs libhello.a hello.o生成libhello.a静态库。
    1. 命令中rcs的含义:rcs中的r表示向库中添加模块,若模块已存在则替换,c表示创建库文件,s表示生成一个目标文件索引。
  3. 使用命令gcc -o main main.c ./libhello.a生成可执行文件main.这是一个综合命令,其实是gcc先将main.c编译、汇编成一个可重定位文件,然后将这个可重定位文件和libhello.a链接,进而生成可执行文件main。
  4. 执行命令./main来运行程序。控制台打印出hello word.

上面的1,2步为静态库的生成。3,4步为静态库的使用。我们思考一个问题:上面生成的可执行文件main是静态链接的呢,还是动态链接的呢?答案是动态链接的(用ldd main命令可以验证)。所以我们必须要清楚一个概念:main与libhello.a之间的链接是静态链接(用nm main可以看到HelloPrintf符号为T,代表文本段的意思。说明HelloPrintf被复制到了可执行文件main的文本段中,所以说它俩之间是静态链接)。但是,main还有一些隐式的链接(这些隐式的链接是链接器自己完成的,用于程序的启动加载等。用nm main可以看到属性为U的符号,如__libc_start_main,U代表未定义的符号,说明此函数不在main的文本段中,所以说是动态链接的)。我们前面也说了gcc默认是动态编译的。所以我们可是使用gcc -static -o main main.c ./libhello.a来创建完全静态链接的可执行程序main。再次使用nm main查看就会发现找不到属性为U的符号了。说明程序是全静态编译。刚才提到的nm命令用于列出一个目标文件的符号表中定义的符号,具体使用可以用man nm查询。

当然的,并非一个静态库只能由一个.c文件生成。可以是多个.c文件生成一个。如使用ar rcs libhello.a hello.o hello2.o hello3.o等。除此之外,ar命令还有很多的功能,具体请参考man ar.

动态库的生成与使用

上面也提到了,动态库又分为加载时的动态链接库(程序在加载时进行库链接)和运行时的动态链接库(程序在运行时进行库链接).不要将动态链接的库与动态加载的库混淆。前者只在程序启动时加载一次。然而,后者可以在程序执行期间的任何时候加载,它们也被称为插件。所以它们的生成和使用也存在不同,下面分别叙述:

加载时的动态链接库

我们还是用上面的程序例程来说动态库的创建与使用步骤:

  1. 使用命令gcc -shared -fpic -o libhello.so hello.c变可生成libhello.so动态库。
    1. -shared: 生成共享库时必要的gcc选项。
    2. -fpic: pic(position-Independent code)表示编译为位置无关代码.共享库编译时总是使用此选项。位置无关代码简单的理解就是可以被加载器加载到内存的任意位置。
  2. 使用命令gcc -o main main.c ./libhello.so生成可执行文件main.当然这也是一个综合命令,其实是gcc先将main.c编译、汇编成一个可重定位文件,然后将这个可重定位文件和libhello.so链接,进而生成可执行文件main。
  3. 执行命令./main来运行程序。控制台打印出hello word.

上面的第1步为动态库的生成。2,3步为动态库的使用。毋庸置疑的,生成的可执行文件main为动态链接的。可以使用nm main查看与上边的不同处(会发现此文件的HelloPrintf符号的属性为U,表示未定义,可见是动态链接)。现在,我们可以将刚生成的可执行文件main复制到其它目录,会发现它在执行是会报错(说加载时找不到共享库libhello.so)。当然你也可以试试上边静态库生成的main,发现在其它的目录下依旧可以执行。这就引入了一个我们现在要谈论的共享库路径问题。

解决共享库路径问题的方式如下:

  1. 将自己生成好的库放入gcc可以查找到的位置,有下面几种方式:
    1. 第一种:直接加入到系统默认的库存放路径。如/usr/lib//usr/local/lib/
    2. 第二种:修改配置文件/etc/ld.so.conf.将自己的库路径加入其中。
    3. 第三种:修改LD_LIBRARY_PATH环境变量。如export LD_LIBRARY_PATH=xxx/mylibPath
  2. 重新链接生成可执行文件
    1. 对于上面的第一、二种库路径。直接使用gcc -o main main.c -lhello就可以生成。并且在其他目录下也可执行。
      1. -lhello为小写的L+name,其中name为libhello.so去掉前缀和后缀。它是用于指示链接器搜索此库。
    2. 对于第三种库路径的配置。使用gcc -o main main.c -L/libPath -lhello.
      1. -L/libPath为libhello.so的路径。
  3. 在其他目录下使用./main执行。发现上面的路径问题解决了。

运行时的动态链接库

运行时的链接库类似用户在程序中open一个文件,然后操作文件,最后在操作完成后关闭文件。下面是相关的函数.调用下面函数需要包含头文件#include <dlfcn.h>.

  1. void *dlopen(const char *filename, int flags)
    1. 描述: 加载一个动态库,并返回它的操作指针。
    2. filename:共享库名称。如/usr/lib/libhello.so
    3. flags:
      1. RTLD_LAZY:标志指示连接器推迟符号解析直到执行来自库中的代码。
      2. RTLD_NOW:立即解析对外部符号的引用。
    4. return:操作指针。
  2. void *dlsym(void *handle, const char *symbol)
    1. 描述:查找符号(简单说就是我们库中的函数等)并返回符号指针。
    2. handle:dlopen()返回的操作指针。
    3. symbol:要查找的符号。
    4. return:符号指针。
  3. int dlclose(void *handle)
    1. 描述:卸载打开的共享库。
    2. return: 0:ok -1:error.
  4. char *dlerror(void)
    1. 描述:返回一个调用dlxxx()函数时最近发生的错误。
    2. return:一个描述错误原因的字符串。

下面用一个具体例子举例说明:

#include "hello.h"
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{

    void *phandle;
    void (*pHelloPrintf)(void);
    char *error;

    //打开libhello.so库
    phandle = dlopen("libhello.so",RTLD_LAZY);
    if(!phandle){
        printf("%s\n",dlerror());
        exit(1);
    }

    //获取到libhello.so中的HelloPrintf()函数指针
    pHelloPrintf = dlsym(phandle,"HelloPrintf");
    error = dlerror();
    if(error != NULL){
        printf("%s\n",error);
        exit(1);
    }

    //调用此函数
    pHelloPrintf();

    //使用完卸载
    int res = dlclose(phandle);
    if(res < 0){
        printf("%s\n",dlerror());
        exit(1);
    }

    return 0;
}

编写好程序后,下面是使用步骤:

  1. 使用命令gcc -rdynamic -o main main.c -ldl编译上面的main.c程序.
  2. 使用./main运行,终端打印hello word.

在介绍加载时的动态链接库时已经介绍了共享库路径问题。而运行时的动态链接库需要在此基础上再加一个步骤:在修改或者在库路径中加入新库时,使用ldconfig命令进行更新/etc/ld.so.cache缓冲文件。

运行时的链接库的优点很明显,读者细细品吧。

关于技术交流

此处后的文字已经和题目内容无关,可以不看。
qq群:825695030
微信公众号:嵌入式的日常
如果上面的文章对你有用,欢迎打赏、点赞、评论。二维码

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

theboynoName

感谢鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值