当前位置: 首页 > article >正文

2-2-18-7 QNX 系统架构-动态链接

阅读前言

本文以QNX系统官方的文档英文原版资料为参考,翻译和逐句校对后,对QNX操作系统的相关概念进行了深度整理,旨在帮助想要了解QNX的读者及开发者可以快速阅读,而不必查看晦涩难懂的英文原文,这些文章将会作为一个或多个系列进行发布,从遵从原文的翻译,到针对某些重要概念的穿插引入,以及再到各个重要专题的梳理,大致分为这三个层次部分,分不同的文章进行发布,依据这样的原则进行组织,读者可以更好的查找和理解。


1. 动态链接

在一个典型的系统中,将运行许多程序。每个程序都依赖于许多函数,其中一些是“标准的”C库函数,比如printf(), malloc(), write()等。

如果每个程序都使用标准C库,则通常每个程序都会在其内部拥有该C库的唯一副本。不幸的是,这将会导致资源的浪费。由于C库是通用的,因此让每个程序引用该库的公共实例比让每个程序包含该库的副本更有意义。这种方法有几个优点,其中最重要的是节省了总的所需的系统内存。

在我们进一步讨论之前,我们应该先看看以下一些术语:

  • Linker

链接器,是一种工具,比如 ld,通常在编译程序后立即运行,以便组合对象文件【object files】和归档文件【archive files】,重新定位它们的数据,并解析符号引用【symbol reference】。

  • Runtime linker

运行时链接器,是一种在运行程序时查找并加载共享对象的工具。运行时链接器,也被称为动态链接器【dynamic linker】,但我们会使用运行时链接器【runtime linker】,而不是【dynamic linker】,从而避免与(非运行时)链接器所做的动态链接【dynamic linking】的概念相混淆。

运行时链接器的名称是ldd(同时ldd也是列举程序所需共享对象的 utility【实用工具】 的名称)。在ELF文件的.interp部分中,对于32位目标系统,它被称为/usr/lib/ldqx.so,对于64位目标系统,它被称为/usr/lib/ldqnx-64.so。您需要在OS映像中包含相应的版本;有关详细信息,请参阅Utilities Reference中的 mkifs 条目。

  • Statically linked

静态链接,表示程序和它所链接的特定的库,在链接时由链接器进行组合。

这意味着程序和特定库之间的绑定是固定的,并且在链接时就知道了(也就是在程序运行之前就知道了)。这也意味着我们不能改变这种绑定关系,除非我们用新版本的库重新链接程序。

如果您不确定库的正确版本是否在运行时可用,或者您正在测试库的新版本,而您还不想将其作为共享方式进行安装,则可以考虑静态链接该程序。

静态链接的程序是根据对象(库)的存档【archive】进行链接的,这些对象(库)的扩展名通常为.a。这种对象集合的一个例子是标准C库,libc.a。

  • Dynamically linked

动态链接,表示程序和它所引用的特定库,在链接时不会被链接器组合起来。

相反,链接器将信息放入可执行文件中,告诉加载器,代码位于哪个共享对象模块中,以及应该使用哪个运行时链接器来查找和绑定引用。这意味着程序和共享对象之间的绑定是在运行时完成的(在程序启动之前,找到并绑定适当的共享对象)。

这种类型的程序被称为部分绑定可执行程序【partially bound executable】,因为它不是完全解析的:链接器在链接时,并没有使程序中的所有引用符号与库中的特定代码相关联。相反,链接器只是说:“这个程序在一个特定的共享对象中调用一些函数,所以我将记录下这些函数在哪个共享对象中,然后继续。” 实际上,这将绑定操作延迟到运行时进行。

动态链接的程序,是针对具有.so扩展名的共享对象进行链接的。这种对象的一个例子是标准C库的共享对象版本,libc.so。

您可以使用编译器驱动程序 qcc 的命令行选项来告诉工具链,您是静态链接还是动态链接。然后该命令行选项就决定了所使用的扩展名(是.a还是.so)。

  • Augmenting code at runtime【在运行时扩充代码】

进一步来说,程序在运行之前可能并不知道需要调用哪些函数。虽然这初看有点奇怪(毕竟,一个程序怎么可能不知道它要调用什么函数呢?),但它确实是一个非常强大的特性。这是为什么。

比如我们来看一个“通用”磁盘驱动程序。启动,探测硬件,并检测到硬盘。然后,驱动程序动态加载 io-blk 代码来处理磁盘块,因为它找到了一个面向块【block-oriented】的设备。现在驱动程序开始以块级别访问磁盘,它发现磁盘上存在两个分区:一个DOS分区和一个Power-Safe分区。我们没有强制磁盘驱动程序,必须包含所有它可能遇到的,所有分区类型的文件系统驱动程序,而是尽量保持简单:它(磁盘驱动程序)没有任何的文件系统驱动程序!在运行时,磁盘驱动程序检测到两个分区,然后知道应该加载 fs-dos.so 和 fs-qnx6.so 文件系统代码,来处理这些分区。

通过推迟决定调用哪些函数,我们增强了磁盘驱动程序的灵活性(同时也减小了它的大小)。

1.1. 如何使用共享对象

为了理解程序如何使用共享对象,让我们首先看看可执行文件的格式,然后检查程序启动时发生的步骤。

ELF format

QNX Neutrino RTOS 使用 ELF(Executable and Linking Format)二进制格式。ELF不仅简化了创建共享库的任务,而且还增强了运行时模块的动态加载。

在下图中,我们展示了ELF文件的两个视图:链接视图和执行视图。链接视图在链接程序或链接库时使用,它处理对象文件中的节【sections】。节【sections】包含大量的对象文件信息:数据【data】、指令【instructions】、重定位信息【relocation information】、符号【symbols】、调试信息【debugging information】等。在程序运行时使用执行视图,执行视图则处理段【segments】。

在链接时,通过将具有相似属性的节【sections】合并为段【segments】来构建程序或库。通常,所有可执行数据节【executable data sections】和只读数据节【read-only data sections】,合并为单个文本段【text segment】,而 data 及 “BSS” 则合并为 data 段【data segment】。这些段称为加载段【load segments】,因为它们需要在进程创建时加载到内存中。其他部分,如符号信息【symbol information】和调试节【debugging sections】被合并到其他非加载段中。

对象文件格式:链接视图和执行视图
对象文件格式:链接视图和执行视图

ELF without COFF

大多数 ELF加载器 的实现,都派生自 COFF(Common Object File Format,通用对象文件格式)加载器;它们在加载时使用 ELF对象 的链接视图。这是低效的,因为程序加载器必须使用节加载可执行文件。一个典型的程序可能包含大量的节,每个节都必须位于程序中并分别加载到内存中。

然而,QNX Neutrino 完全不依赖于加载节【sections】的 COFF 技术。在开发我们的 ELF 实现时,我们直接根据 ELF 规范工作,并将效率放在了首位。该ELF加载器使用了程序的“执行视图”。通过这样做,加载程序的任务大大简化:它所要做的就是将程序或库的加载段【load segments】(通常是两个)复制到内存中。因此,进程创建和库的加载操作要比之前快得多。

1.2. 典型进程的内存布局

下图显示了一个典型进程的内存布局。进程加载段(对应于图中的 text段 和data段),在进程的基地址进行加载。主栈【main stack】位于正下方并向下扩展。任何被创建的其他线程都有它们自己的栈【stack】,位于主栈【main stack】的下方。每个栈【stack】由一个保护页【guard page】进行分隔,以检测栈溢出。堆【heap】位于进程加载段【load segments】上方并向上增长。(load segments,代表的是图中的data segment 和 text segment。)

在x86系统上的进程内存布局
在x86系统上的进程内存布局

在进程地址空间的中间,为共享对象保留了一个很大的区域。共享库位于地址空间的顶部并向下增长。

创建新进程时,进程管理器首先将可执行文件中的两个段映射到内存中。然后对程序的ELF头进行解码。如果程序头表明可执行文件链接到了共享库,则进程管理器将从程序头中提取动态解释器【dynamic interpreter】的名称。动态解释器指向包含了运行时链接器【runtime linker】代码的共享库【shared library】。进程管理器将在内存中加载此共享库【shared library】,然后将控制权传递给此共享库中的运行时链接器代码。

1.3. 运行时链接器

当启动指向共享对象的程序时,或程序请求动态加载共享对象时,将调用运行时链接器【runtime linker】。此链接器包含在C运行时库中。

运行时链接器【runtime linker】在加载共享库(.so文件)时,执行如下几个任务:

如果请求的共享库尚未加载到内存中,则运行时链接器将会加载它:

如果共享库名称是完全限定的【fully qualified】(即以斜杠开头,类似于“绝对路径”),则直接从指定位置加载它。如果在那里找不到,则不再执行进一步的搜索。

如果它不是一个完全限定的路径名,链接器将按照如下方式搜索它:

如果可执行文件的动态节【dynamic section】包含 DT_RPATH 标记,则搜索 DT_RPATH 指定的路径。

如果没有找到共享库,运行时链接器将在 LD_LIBRARY_PATH 指定的目录中搜索它。

如果仍然没有找到共享库,那么链接器将搜索由 LD_LIBRARY_PATH 环境变量 指定的默认库搜索路径(即 CS_LIBPATH 配置字符串)。如果没有指定,则默认库路径设置为映像文件系统的路径。

一旦找到请求的共享库,就将其加载到内存中。对于 ELF 共享库,运行时链接器只需要使用两次 mmap() 调用,来将两个加载段映射到内存中,是一个非常高效的操作。

然后将共享库添加到进程已加载的所有库的内部列表中。运行时链接器【runtime linker】负责维护这个列表。

然后运行时链接器【runtime linker】对共享对象的动态节【dynamic section】进行解码。

此动态节【dynamic section】向链接器提供有关此库所链接的其他库的信息。它还提供了有关需要应用的重定位信息和需要解析的外部符号的信息。运行时链接器,将会首先加载任何其他的所需共享库(这些共享库本身可能也会引用再其他的共享库)。然后,它将处理每个库的重定位。其中一些重定位是库的本地重定位,而另一些重定位则需要运行时链接器解析全局符号。在后一种情况下,链接器将在库列表中搜索该符号。在ELF文件中,使用哈希表进行符号查找,因此哈希表查找非常快。在库中搜索符号的顺序非常重要,我们将在后面的 “Symbol name resolution” 章节中看到这一点。

一旦应用了所有重定位,就会调用在共享库的 init section 中注册的所有初始化函数。在某些 c++ 实现中,也用于调用全局构造函数。

1.4. 在运行时加载共享库

进程可以通过使用 dlopen() 调用,在运行时加载共享库,该调用会指示运行时链接器【runtime linker】加载此库。一旦加载了此库,程序就可以通过使用 dlsym() 调用来确定其地址,从而调用该库中的任何函数。

程序还可以通过使用 dladdr() 调用来确定与给定地址相关联的符号。最后,当进程不再需要共享库时,它可以调用 dlclose() 从内存中卸载库。

1.5. 符号名称解析

当运行时链接器加载共享库时,必须解析该库中的符号。符号解析的顺序和范围很重要。如果一个共享库调用的函数恰好在程序加载的几个库中以相同的名称存在,那么在这些库中搜索该符号的顺序是至关重要的。这就是为什么OS会定义多个可以在加载库时所使用的选项。

所有具有全局作用域的对象(可执行文件【executables】和库【bibraries】)都存储在一个内部列表(全局列表)中。默认情况下,任何全局作用域对象都会使其所有符号对加载的任何共享库可用。全局列表最初包含了在程序启动时所加载的可执行文件和所有库。

默认情况下,当使用 dlopen() 调用加载新的共享库时,该库中的符号将通过按以下顺序搜索来进行解析:

由LD_PRELOAD环境变量 指定的库列表。您可以在运行程序时,使用此环境变量来添加或更改功能。

共享库

全局列表

共享库所引用的任何依赖对象(即,共享库链接到的任何其他库)

当使用dlopen()打开共享库时,运行时链接器【runtime linker】的作用域范围,可以通过两种方式改变:

当程序加载一个新库时,它可以通过将RTLD_GLOBAL标志传递给dlopen()调用的方式,来指示运行时链接器将库的符号放在全局列表中。这将使得该库的符号对随后加载的任何库都可用。

对共享库中符号进行解析时,会搜索对象列表,可以使用修改该对象列表的方式。如果将RTLD_GROUP标志传递给dlopen(),那么只有该库直接引用的对象才会在其中进行符号搜索。如果传递RTLD_WORLD标志,则只搜索全局列表中的对象。

2. 梳理理解与总结

在一个典型的系统中,将运行许多程序。每个程序都依赖于许多函数,其中一些是“标准的”C库函数,比如printf(), malloc(), write()等。

如果每个程序都使用标准C库,则通常每个程序都会在其内部拥有该C库的唯一副本。不幸的是,这将会导致资源的浪费。由于C库是通用的,因此让每个程序引用该库的公共实例比让每个程序包含该库的副本更有意义。这种方法有几个优点,其中最重要的是节省了总的所需的系统内存。

Linker

链接器,是一种工具,比如 ld,通常在编译程序后立即运行,以便组合对象文件【object files】和归档文件【archive files】,重新定位它们的数据,并解析符号引用【symbol reference】。

Runtime linker

运行时链接器,是一种在运行程序时查找并加载共享对象的工具。运行时链接器,也被称为动态链接器【dynamic linker】,但我们会使用运行时链接器【runtime linker】,而不是【dynamic linker】,从而避免与(非运行时)链接器所做的动态链接【dynamic linking】的概念相混淆。

运行时链接器的名称是ldd(同时ldd也是列举程序所需共享对象的 utility【实用工具】 的名称)。

Statically linked

静态链接,表示程序和它所链接的特定的库,在链接时由链接器进行组合。

这意味着程序和特定库之间的绑定是固定的,并且在链接时就知道了(也就是在程序运行之前就知道了)。这也意味着我们不能改变这种绑定关系,除非我们用新版本的库重新链接程序。

如果您不确定库的正确版本是否在运行时可用,或者您正在测试库的新版本,而您还不想将其作为共享方式进行安装,则可以考虑静态链接该程序。

静态链接的程序是根据对象(库)的存档【archive】进行链接的,这些对象(库)的扩展名通常为.a。这种对象集合的一个例子是标准C库,libc.a。

Dynamically linked

动态链接,表示程序和它所引用的特定库,在链接时不会被链接器组合起来。

相反,链接器将信息放入可执行文件中,告诉加载器,代码位于哪个共享对象模块中,以及应该使用哪个运行时链接器来查找和绑定引用。这意味着程序和共享对象之间的绑定是在运行时完成的(在程序启动之前,找到并绑定适当的共享对象)。

这种类型的程序被称为部分绑定可执行程序【partially bound executable】,因为它不是完全解析的:链接器在链接时,并没有使程序中的所有引用符号与库中的特定代码相关联。相反,链接器只是说:“这个程序在一个特定的共享对象中调用一些函数,所以我将记录下这些函数在哪个共享对象中,然后继续。” 实际上,这将绑定操作延迟到运行时进行。

动态链接的程序,是针对具有.so扩展名的共享对象进行链接的。这种对象的一个例子是标准C库的共享对象版本,libc.so。

您可以使用编译器驱动程序 qcc 的命令行选项来告诉工具链,您是静态链接还是动态链接。然后该命令行选项就决定了所使用的扩展名(是.a还是.so)。

2.1. 如何使用共享对象

ELF format

QNX Neutrino RTOS 使用 ELF(Executable and Linking Format)二进制格式。ELF不仅简化了创建共享库的任务,而且还增强了运行时模块的动态加载。

在下图中,我们展示了ELF文件的两个视图:链接视图和执行视图。链接视图在链接程序或链接库时使用,它处理对象文件中的节【sections】。节【sections】包含大量的对象文件信息:数据【data】、指令【instructions】、重定位信息【relocation information】、符号【symbols】、调试信息【debugging information】等。在程序运行时使用执行视图,执行视图则处理段【segments】。

在链接时,通过将具有相似属性的节【sections】合并为段【segments】来构建程序或库。通常,所有可执行数据节【executable data sections】和只读数据节【read-only data sections】,合并为单个文本段【text segment】,而 data 及 “BSS” 则合并为 data 段【data segment】。这些段称为加载段【load segments】,因为它们需要在进程创建时加载到内存中。其他部分,如符号信息【symbol information】和调试节【debugging sections】被合并到其他非加载段中。

对象文件格式:链接视图和执行视图
对象文件格式:链接视图和执行视图

2.2. 典型进程的内存布局

下图显示了一个典型进程的内存布局。进程加载段(对应于图中的 text段 和data段),在进程的基地址进行加载。主栈【main stack】位于正下方并向下扩展。任何被创建的其他线程都有它们自己的栈【stack】,位于主栈【main stack】的下方。每个栈【stack】由一个保护页【guard page】进行分隔,以检测栈溢出。堆【heap】位于进程加载段【load segments】上方并向上增长。

在x86系统上的进程内存布局

在进程地址空间的中间,为共享对象保留了一个很大的区域。共享库位于地址空间的顶部并向下增长。

创建新进程时,进程管理器首先将可执行文件中的两个段映射到内存中。然后对程序的ELF头进行解码。如果程序头表明可执行文件链接到共享库,进程管理器将从程序头中提取动态解释器的名称。动态解释器指向包含运行时链接器代码的共享库。进程管理器将在内存中加载此共享库,然后将控制权传递给此库中的运行时链接器代码。

2.3. 运行时链接器

当启动指向共享对象的程序时,或程序请求动态加载共享对象时,将调用运行时链接器【runtime linker】。此链接器包含在C运行时库中。

运行时链接器【runtime linker】在加载共享库(.so文件)过程请参考:- Runtime linker

2.4. 在运行时加载共享库

进程可以通过使用 dlopen() 调用,在运行时加载共享库,该调用会指示运行时链接器【runtime linker】加载此库。一旦加载了此库,程序就可以通过使用 dlsym() 调用来确定其地址,从而调用该库中的任何函数。

程序还可以通过使用 dladdr() 调用来确定与给定地址相关联的符号。最后,当进程不再需要共享库时,它可以调用 dlclose() 从内存中卸载库。

2.5. 动态链接库使用总结

两种动态链接库的使用方式:

2.5.1. 用程序编译时,通过编译工具链gcc指定和使用动态链接库

创建动态链接库

假设你有一个名为 libmylib.so 的动态链接库,它包含了一个函数 int add(int a, int b);。你需要编写这个库的源代码,例如 mylib.c:

然后,你可以使用 gcc 来编译和创建 .so 文件:

编写使用动态链接库的应用程序

现在,你有一个使用 libmylib.so 库的应用程序。你需要包含相应的头文件(如果有的话),并在编译时指定链接到该库。假设你没有为 add 函数创建头文件,但通常你会这样做。

app.c(使用动态链接库的应用程序):

注意:在实际开发中,你应该在头文件中声明函数 add,并在 app.c 中包含这个头文件。

编译应用程序并链接到动态链接库

编译应用程序时,你需要使用 -L 选项来指定库文件的搜索路径,并使用 -l 选项来指定库名(不包括前导的 lib 和后缀的 .so)。

这里 -L. 指定了当前目录为库文件的搜索路径(假设 libmylib.so 在当前目录下),而 -lmylib 指定了要链接的库名为 mylib。

运行应用程序

在运行应用程序之前,确保动态链接库 libmylib.so 在系统的库路径中,或者在运行应用程序时指定其位置。如果库在当前目录下,你可能需要设置 LD_LIBRARY_PATH 环境变量来包含当前目录:

2.5.2. 直接在应用程序代码中运行时动态加载共享对象(即.so文件)

假设你有一个共享对象文件 libmylib.so,它包含了一个函数 int add(int a, int b);。该函数的实现和创建 .so 文件的过程与前面的示例相同。

编写一个 C 应用程序,该程序在运行时动态加载 libmylib.so 并调用其中的 add 函数。

编译应用程序时,你需要链接 dl 库,因为 dlopen、dlsym 等函数都定义在这个库中。

执行./myapp运行你的应用程序。


http://www.kler.cn/a/418591.html

相关文章:

  • 【Linux】gdb / cgdb 调试 + 进度条
  • 机器学习算法(六)---逻辑回归
  • 【实验13】使用预训练ResNet18进行CIFAR10分类
  • 设计模式学习[10]---迪米特法则+外观模式
  • 神经网络中常见的激活函数Sigmoid、Tanh和ReLU
  • 网络安全法
  • Qt 详解QRubberBand
  • Python plotly库介绍
  • Qt中Socket网络编程
  • Redis高阶集群搭建+集群读写
  • FreeRTOS之vTaskStartScheduler实现分析
  • 电脑提示报错“Directx error”怎么解决?是什么原因导致的?游戏软件提示“Directx error”错误的解决方案
  • 龙蜥 Linux 安装 JDK
  • 华为仓颉编程环境搭建
  • PYNQ 框架 - OV5640驱动 + Linux 驱动分析
  • GPT(Generative Pre-trained Transformer) 和 Transformer的比较
  • 百度智能云千帆部署流程---语音识别和合成
  • 【前端】跨域问题与缓存
  • 泷羽sec- shell编程(8) until循环以及函数基本创建调用 学习笔记
  • 【OceanBase 诊断调优】—— 如何在 OceanBase 数据库 Oracle 模式中定位存储过程内的慢 SQL
  • CH32v20x单片机risc-v内核uint64_t类型移位后变量为0解决办法
  • 【热门主题】000075 探索嵌入式硬件设计的奥秘
  • js原型、原型链和继承
  • go语言的成神之路-筑基篇-gin框架渲染模板
  • 《datawhale2411组队学习 模型压缩技术7:NNI剪枝》
  • Angular v19 (三):增量水合特性详解 - 什么是水合过程?有哪些应用场景?与 Qwik 相比谁更胜一筹?- 哪个技术好我就学哪个,这就是吸心大法吧