首页 > 其他分享 >操作系统之动态链接库

操作系统之动态链接库

时间:2023-07-27 16:03:24浏览次数:30  
标签:操作系统 符号 libdemo so 动态链接库 共享 链接 soname

操作系统之动态链接库

静态库

静态库也称为归档文件, 它是UNIX系统提供的第一种库。静态库带来以下好处:

  • 可以将一组经常被用到的目标文件组织进单个库文件, 这样可以使用它来构建多个可执行程序的时候无需重新编译原来的源代码文件。
  • 链接命令变得更简单了,在链接命令行中只需指定静态库名称即可, 而无需一个个列出目标文件了。链接器知道如何搜索静态库并将可执行程序需要的对象抽取出来。

创建和维护静态库

静态库的名称形式为libname.a
使用ar命令能够创建和维护静态库, 其通用形式如下所示。

ar options archive object-file...

options可选项:

  • r(替换): 将一个目标文件插入到归档文件中并取代同名的目标文件。如:

    ar r libdemo.a mod1.o mod2.o mod3.o

  • t(目标表):显示归档中的目标表。即显示归档文件中所有目标文件。添加v(verbose) 修饰后可显示权限、大小、修改时间等属性。
  • d(删除):从归档文件中删除一个模块, 如下面例子所示:

    ar d libdemo.a mod3.o

使用静态库

将程序与静态库链接起来存在两种方式。

  1. 在链接命令中指定静态库名称

    cc -g -c prog.c
    cc -g -o prog prog.o libdemo.a

  2. 将静态库放在链接器能搜索到的一个目录中(如/usr/lib), 然后使用-l选项指定库名称(即库文件名取出来lib前缀和.a后缀)。如果不是放在链接库默认的搜索目录中, 也可使用-L指定链接器搜索目录。

虽然静态库可以包含很多目标模块, 但链接器只会包含那些程序所需的模块。

共享库

上述的静态库存在哪些缺陷呢?链接静态库的每个可执行文件都存在目标模块的副本, 存在冗余, 这种冗余存在如下缺点:

  • 存储同一个目标模块的多个副本会浪费磁盘空间
  • 由于每个程序都存在相同的目标模块副本, 在其同时运行时,将耗费系统整体虚拟内存
  • 如果对静态库中某个目标模块进行更新,所有依赖这个目标模块的程序则需重新链接以合并这个变更。

共享库就是设计用来解决这些缺点的。 共享库的关键思想是目标模块的单个副本由所依赖的程序所共有。 目标模块不会被复制到链接过的可执行文件中。当共享库第一次被加载到内存后, 将会被所有依赖它的进程共有。

虽然共享库的代码是由多个进程共享的,但其中的变量却是每个使用库的进程所独有,即各自有副本。
共享库具备下列优势:

  • 整个程序大小变小了。 第一个加载共享库的程序启动时会花费更多时间, 用于查找共享库并加载到内存中。
  • 修改目标模块时, 无需重新链接程序就能看到变更。

相对应的缺点如下:

  • 共享库在编译时必须使用为止独立的代码, 这在大多数架构上都会带来性能开销, 因为它需要使用额外的寄存器。
  • 在运行时必须要执行符号重定位。在符号重定位期间, 需要将共享库中的每个符号(变量或函数)的引用修改成符号在虚拟内存中的实际运行时位置。所以会带来一些性能消耗。

创建共享库

gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
gcc -g -shared -o libdemo.so mod1.o mod2.o mod3.o

以上用编译和构建两个步骤创建动态库。 根据惯例, 共享库的前缀为lib, 后缀为.so(shared object)

位置独立的代码

gcc -fPIC选项指定编译器应该生成位置独立的代码, 这会改变编译器生成执行特定操作的代码的方式, 包括访问全局变量、静态和外部变量,访问字符串常量,以及获取函数的地址。这些变更使得代码可以在运行时被放置在任意一个虚拟地址处。这对共享库是必需的, 因为在链接时无法知道共享库代码位于内存的何处。

为了确定目标文件在编译时是否使用了-fPIC选项, 可使用如下命令来检查符号表中是否存在GLOBAL_OFFSET_TABLE

nm mod1.o | grep _GLOBAL_OFFSET_TABLE_
readelf -s mod1.o | grep _GLOBAL_OFFSET_TABLE_ 

而检查共享库则使用如下命令:

objdump --all-headers libdemo.so | grep TEXTREL
readelf -d libdemo.so | grep TEXTREL

如果以上命令存在任何输出, 则未指定-fPIC编译选项

控制函数的可见性

设计良好的共享库应该只公开那些构成其声明的应用程序二进制接口的符号(函数和变量)。理由如下:

  • 避免依赖库的调用者, 依赖了非标准接口
  • 共享库导出的符号可能会覆盖其他共享库导出的符号
  • 导出非必需的符号会增加在运行时虚假在动态符号表的大小

下列技术可控制符号的导出:

  • 使用static关键字,使得符号私有于一个源代码模块, 从而使得它无法被其他目标文件绑定
  • 利用GCC的特性声明:__attribute__((visibility("hidden")))
  • 版本脚本, 利用版本脚本可精细控制符号的可见性。其编译选项为-Wl,--version-script,scriptfile.map,其中的scriptfile.map即为版本脚本文件。
    如一个共享库默认导出符号: vis_common()、vis_f1()、vis_f2(), 但要求是不需要导出vis_common(), 其脚本如下:
    VER_1{
      global:
        vis_f1;
        vis_f2;
      local:
        *;
    }
    
    其中VER_1是一种版本标签。global标记出了以分号分隔的对库之外的程序可见的符号列表,local标记出了以分号分隔的对库之外的程序隐藏的符号列表。 以上用*表示除了global中列出的符号,其他都隐藏不可见。
    如果结合
    __asm__(".symver xyz_old, xyz@VER_1")
    __asm__(".symver xyz_new, xyz@VER_2")
    
    可实现符号版本化,可易于接口升级。
  • 动态加载库中提到的引用主程序函数

static 关键词将一个符号的可见性限制在单个源代码文件中, 而hidden特性使得符号对构成共享库的所有源代码文件可见,而对库之外的文件不可见。

初始化函数与终止函数

可以定义一个或多个在共享库被加载和卸载时自动执行的函数, 这样在使用共享库时就能完成一些初始化和终止工作了。无论是自动被加载还是使用dlopen()接口显式加载,初始化函数和终止函数都会被执行。

使用gcc的constructordestructor特性可定义初始化和终止函数。如:

void __attribute__((constructor)) some_name_load(void){
  /*Initialization code*/
}

void __attribute__((destructor)) some_name_unload(void){
  /*Finalization code*/
}

在共享库被加载时将会执行constructor修饰的函数, 相反,在卸载时会调用destructor修饰的函数。

当以上特性应用于可执行执行时, 具有相同的特性, 即在主程序初始化和销毁时被调用。

共享库的soname

soname即共享库的别名, 其存储在ELF文件的DT_SONAME标签中,如果共享库拥有一个soname,那么在静态链接阶段会将soname嵌入到可执行文件中,动态链接阶段也会使用soname来搜索库, 而不会使用真实名称

soname的目的是为了提供一层间接, 使可执行程序能够在运行时使用与链接时的库不同的共享库。

使用soname的第一步是在创建共享库时指定soname:

gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
gcc -g -shared -Wl,-soname,libtest.so -o libdemo.so mod1.o mod2.o mod3.o

上述例子通过编译选项-Wl,-soname指示了将libdemo.so取别名为libtest.so

通过如下命令可查看共享库的soname:

objdump -p libdemo.so | grep SONAME
readelf -d libdemo.so | grep SONAME

注意: 当给共享库取别名后,应建立别名共享库的软链接, 指向真实生成的共享库。如以上例子中应建立软链接libdemo.so,指向libtest.so

共享库版本和命名规则

明确版本的概念:

  • 次要版本
    版本不同但相互兼容的版本称为共享库的次要版本
  • 主要版本
    与上一个版本完全不兼容的版本即为主要版本

关于真实名称soname以及链接器名称

  • 真实名称的格式为: libname.so.major-id.minor-id
  • 共享库的soname包括相应的真实名称中的主要版本标识符, 但不包含次要版本标识符。因此其形式为:libname.so.major-id
  • 所谓链接器名称,即将可执行文件与共享库链接起来时会用到的名称。该名称不包含任何版本信息, 形如:libname.so
名称 格式 描述
真实名称 libname.so.maj.min 即真实的库文件
soname libname.so.maj 为一个软链接,库的每个主要版本都存在一个soname: 在链接时嵌入到可执行文件中,在运行时找出其指向的真实名称的库文件
链接器名称 libname.so 为一个软链接,指向soname

比如:

gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
gcc -g -shared -Wl,-soname,libdemo.so.1 -o libdemo.so.1.0.1 mod1.o mod2.o mod3.o

以上创建了一个真实名称为libdemo.so.1.0.1的共享库, 其soname为libdemo.so.1

接着为soname和链接器名称创建符号链接:

ln -s libdemo.so.1.0.1 libdemo.so.1
ln -s libdemo.so.1 libdemo.so

然后就可以通过链接器名称来构建可执行程序了:

gcc -g -Wall -o prog prog.c -L. -ldemo

使用共享库

由于可执行程序不再包含目标文件副本,所以需要某种机制找出所需运行时共享库。这是通过在链接阶段将共享库的名称嵌入可执行文件中来完成的。

在ELF中, 库依赖性是记录在可执行文件的DT_NEEDED标签中

如下命令可查看:

objdump --all-headers libdemo.so  | grep NEEDED
readelf -d  libdemo.so  | grep NEEDED

如下命令可将共享库名称嵌入到可执行文件中:

gcc -g -Wall -o prog prog.c libdemo.so

此时执行程序

./prog

会收到错误./prog: error while loading shared libraries: libdemo.so: cannot open shared object file: no such file or directory
解决这个问题需要做第二件事: 动态链接,即在运行时解析内嵌的库名。这个任务由动态链接器(运行时链接器)来完成。动态链接器本身也是一个共享库, 其名称为/lib/ld-linux.so.2, 所有使用共享库的ELF可执行文件都会用到这个共享库。

动态链接器会检查程序所需共享库清单并使用预先定义好的规则在文件系统上找出相关库文件。如默认会在/lib/usr/lib等标准目录中查找所需动态库。

通知动态链接器一个共享库位于非标准目录中的一种方法是将该目录添加到LD_LIBRARY_PATH环境变量中以分号间隔。

以上错误是因为libdemo.so文件在当前目录中,通过如下命令即可避免报错:

LD_LIBRARY_PATH=. ./prog

动态链接与静态链接

  • 静态链接发生在编译阶段, 即编译时,链接器ld将一个或多个编译过的目标文件组合成一个可执行文件。
  • 而动态链接发生在运行时,在运行时, 通过运行时链接器(/lib/ld-linux.so.2)找到所需库文件。

只有动态库才有动态链接, 而静态库不存在动态链接。

动态加载库

动态链接器会加载程序的动态以来列表中的所有共享库, 但有时候可延迟加载库。 动态链接器通过一组API来实现。 这组API通常被称为dlopen API

dlopen API能在程序运行时打开一个共享库,根据名称在库中搜索一个函数,然后调用这个函数。这个过程通常称为动态加载。其核心api如下:

  • dlopen()打开一个共享库, 返回其句柄。同一个库被打开多次,但加载进内存只会执行一次, 其他都是引用计数+1。当加载的库依赖其他库, 其依赖树会同时被加载。
  • dlsym()搜索一个符号, 返回其地址。dlsym可接受一个伪句柄, 如RTLD_DEFAULTRTLD_NEXT
  • dlclose()关闭打开的句柄。该调用会将引用计数-1,当引用计数为0时,dlclose()才会从内存中删除这个库。当进程终止时会隐式地对所有库执行dlclose()。
  • dlerror()返回一个错误消息字符串。
  • dladdr()获取与加载的符号相关的信息, 非标准接口需要使用宏_GNU_SOURCE

若想调用以上API, 必须依赖libdl库, 即在编译时需添加选项-ldl

假设使用dlopen()动态加载了一个共享库, 然后dlsym()获取到符号x(), 而在x()中调用了函数y(),通常情况下会在当前共享库或依赖的共享库中查找该符号, 如何让其调用主程序中的函数y()?

要达到上述要求, 需要在编译主程序时, 利用编译选项-Wl,--export-dynamic-export-dynamic,以允许动态加载的库访问主程序中的全局符号

预加载共享库:LD_PRELOAD

有时候可以选择覆盖一些正常情况下被动态链接器按照多个同名符号优先级问题找出的函数, 可定义环境变量LD_PRELOAD,该环境变量指示了多个由空格或冒号间隔的共享库。这些库会被优先加载。因为被优先加载, 在找某个函数名时,会优先找预加载的共享库, 因此会覆盖后续的同名函数。

利用/etc/ld.so.preload文件也可完成同样的效果。若两种方式都指定, LD_PRELOAD优先级更高。

许多监控软件或是分析软件, 利用了此特性用于分析程序。可监控C接口调用。

监控动态链接器:LD_DEBUG

有时候需要监控动态链接器的操作来弄清楚它在搜索哪些库、哪些符号, 可通过LD_DEBUG环境变量来完成。如:

LD_DEBUG=libs date

可以看到搜索依赖共享库的过程

共享库常用工具

ldd

ldd命令显示了一个程序运行所需的共享库。形如: ldd prog

objdump

命令形如: objdump -x <filename> | grep NEEDED

readelf

命令形如: readelf -d <filename> | grep NEEDED

nm

nm可以查看动态库是否定义了某个函数接口:nm -A libdemo.so 2> /dev/null | grep somefunction

ldconfig

ldconfig 解决了共享库两个潜在问题:

  1. 共享库位于各种目录中, 动态链接器搜索加载某个库将会比较慢
  2. 当安装了新版本的库或删除了旧版本的库, soname符号链接就不是最新的

ldconfig 主要用于解决以上两个问题, 针对第一个问题, ldconfig通过搜索标准库目录(/usr/lib、/lib、/usr/local/lib及x64对应目录)和/etc/ld.so.conf中列出的目录创建缓存文件/etc/ld.so.cache, 通过执行ldconfig -p可查看缓存的内容。动态链接器在运行时解析库名称会使用这个缓存文件

针对第二个问题,ldconfig会检查每个库的主要版本的最新次要版本以找出嵌入的soname, 然后再同一目录为每个soname创建(或更新)对应的符号链接。
为了避免生成缓存, 可附带-n选项,而为了阻止soname符号链接创建,可指定-X

每当安装了一个新的库,更新或删除了一个既有库, 以及/etc/ld.so.conf中的目录列表被修改之后, 都应该运行ldconfig。

其他方式定位共享库

  • 在静态编译阶段可以在可执行文件中插入一个在运行时搜索共享库的目录列表。需利用编译选项-Wl,-rpath,path1;path2;path3, 另一种替代方案即将多个目录放置到环境变量LD_RUN_PATH环境变量中。
    如何查看elf文件中的rpath, 通常使用命令objdump -p prog | grep PATHreadelf -d prog | grep PATH
  • 在rpath中使用$ORIGIN, 如gcc -Wl,-rpath, '$ORIGIN'/lib。这样可执行文件会在其lib目录中找依赖的共享库, 而不论可执行文件在哪个目录

共享库搜索

共享库搜索优先级如下:

  1. 在可执行文件中读取DT_RPATH条目, 搜索其指定的目录而排除DT_RUNPATH条目指定的目录(因DT_RUNPATH优先级低于LD_LIBRARY_PATH)。
  2. 搜索LD_LIBRARY_PATH指定的目录
  3. 搜索可执行文件中DT_RUNPATH条目包含的目录
  4. 搜索/etc/ld.so.cache指定的目录
  5. 搜索标准目录,如/lib、/lib64、/usr/lib等

多个同名符号优先级问题

  • 如果主程序与动态库中都存在同名符号func,那么会优先调用主程序的func
  • 而主程序依赖的多个共享库中存在同名接口, 则以以上搜索顺序找到的第一个共享库中的符号为准

标签:操作系统,符号,libdemo,so,动态链接库,共享,链接,soname
From: https://www.cnblogs.com/quenwaz/p/17564655.html

相关文章

  • cpu 操作系统 JVM(大白话)
    大白话直接描述下,cpu,操作系统和jvm:cpu就像健身房的跑步机硬件设备操作系统就是更有权威的大人(有很多权限)jvm及我们写的java应用,或其他用户程序,就像一群小朋友每个小朋友都想在跑步机上玩,大人就需要按某种规则安排(任务调度),只允许玩多久,每次换其他小朋友,都要记录下当前同学的......
  • 【运维】Cobbler原理与实战(自动安装操作系统类似网克)
    https://blog.csdn.net/weixin_46108954/article/details/105869201https://blog.csdn.net/weixin_47219818/article/details/107504402?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-1-107504402-blog-1058......
  • 分布式操作系统是操作系统的终极形态吗?
    昨天一位网友私信我,提出一个问题:“Laxcus分布式操作系统会不会是操作系统发展的终极形态?”。今天觉得有必要把这件事说一说,所以就忙里偷闲写下这篇文章。咱们先说结论:是也不是,需要具体情况具体分析。操作系统发展到今天,基本分为两种:面向个人工作的操作系统,和面向企业业务的操作系统......
  • 聚焦操作系统迁移实践与生态发展 | openEuler Meetup 长沙站圆满结束
    活动回顾12月23日,由openEuler社区和湖南欧拉生态创新中心主办,麒麟信安和湖南省鲲鹏生态创新中心协办的openEuler Meetup 长沙站举办。本次活动集聚社区开发者、用户、企业伙伴、操作系统爱好者,围绕操作系统搬迁进行交流和实践,正式发布《湖南欧拉生态发展白皮书》,并在活动上成立op......
  • 别错过!这场干货满满的操作系统产业峰会回顾来了
    12月28日操作系统产业峰会2022以线上直播的方式圆满举办作为操作系统产业界的年度盛会本次大会干货满满精彩纷呈!赶紧来一起回顾吧!25位重磅嘉宾出席4大系列重磅内容亮相2022年度openEuler领先商业实践奖项揭晓;中国科学院软件研究所联合多家机构和厂商发布基于openEuler的RISC-V......
  • centos 7配置ORACLE动态链接库
    随便在一个目录下解压instantclient-basiclite-linuxx64.zip(一般下最新的就好啦)然后在/etc/ld.so.conf文件添加解压完文件的目录 在执行ldconfig就好了。是不是很简单.ORACLE版本低的时候插入数据也许会报这个错cx_Oracle,cursor.execute(sql)执行的时候编码错误:UnicodeEn......
  • WINPE(Windows Preinstallation Environment)是一个基于Windows操作系统的轻量级预安装
    WINPE(WindowsPreinstallationEnvironment)是一个基于Windows操作系统的轻量级预安装环境。它主要用于系统部署、故障排除、数据恢复和维护等任务。以下是一些常见的WINPE版本:WindowsPE2.0:也称为Vista版,基于WindowsVista操作系统。具有较高的兼容性,并提供了各种工具和驱动程序......
  • .net平台如何切换国产操作系统
    .NET平台如何切换国产操作系统简介在某些特定的应用场景中,我们可能需要将已经开发好的应用程序迁移到国产操作系统上运行,比如麒麟操作系统。本文将介绍如何使用.NET平台切换到国产操作系统的方案,并提供代码示例作为参考。确认国产操作系统兼容性在开始切换操作系统之前,首先需要......
  • 操作系统
    1、操作系统启动过程:①执行BIOS,进行硬件自检并且去磁盘的0号块的0号扇区读取bootsect.s放入内存区域②执行bootsect.s把操作系统的后部分代码读入,并放在相邻位置。包括setup.s、system.s。③执行setup.s,初始化一些数据结构,用于管理硬件。④执行system2、系统调用:①系统调用......
  • VMware 客户机操作系统已禁止CPU。请关闭或重置虚拟机
    系统版本:Win11虚拟机版本:VM16.2.4从其他系统迁移过来的VM虚拟机,启动提示错误。搜好多都解决不了。(图片来源于网络)解决另外一个“无法运行虚拟机”问题时,无意中把这个问题解决了。解决方法:关闭系统安全选项https://blog.csdn.net/tianpeng666/article/details/1292683......