QNX 动态链接

QNX 动态链接

  在一个典型的系统中,会运行许多程序。每个程序都依赖于一些函数,其中一些是标准的C库函数,如printf()、malloc()、write()等。
  如果每个程序都使用标准的C库,那么每个程序通常都有这个特定库的惟一副本。不幸的是,这导致了资源的浪费。由于C库是公共的,所以让每个程序引用该库的公共实例比让每个程序包含该库的副本更有意义。这种方法有几个优点,其中最重要的是节省了所需的系统总内存。

静态链接

  术语静态链接意味着程序和它所链接的特定库在链接时由链接器组合在一起。这意味着程序和特定库之间的绑定是固定的,并且在程序运行之前就已经知道了。这也意味着我们不能改变这个绑定,除非我们用库的新版本重新链接程序。
  如果您不确定某个库的正确版本在运行时是否可用,或者您正在测试某个库的新版本,但又不希望将其安装为共享的,那么您可以考虑静态地链接一个程序。
  静态链接的程序是针对对象(库)的归档文件链接的,这些对象(库)的扩展名通常是.a。这种对象集合的一个例子是标准C库libc.a

动态链接

  动态链接这个术语意味着程序和它引用的特定库在链接时不会被链接器组合在一起。相反,链接器将信息放入可执行文件中,告诉加载程序共享对象模块代码所在位置,以及应该使用哪个运行时链接器来查找和绑定引用。这意味着程序和共享库之间的绑定是在运行时完成的——在程序启动之前,找到并绑定适当的共享库。
  这种类型的程序称为部分绑定的可执行程序,因为它没有被完全解析——链接器在链接时并没有导致程序中所有引用的符号都与库中的特定代码相关联。相反,链接者只是简单地说:这个程序在一个特定的共享对象中调用了一些函数,所以我只需要记下这些函数在哪个共享对象中,然后继续。实际上,这将绑定延迟到运行时。
  动态链接的程序被链接到具有扩展名的共享对象上。此类对象的一个示例是标准C库的共享对象版本,即libc.so。
  您可以使用编译器驱动程序qcc的命令行选项来告诉工具链是静态链接还是动态链接。然后,此命令行选项确定所使用的扩展名(.a或.so)。

在运行时扩展代码

  更进一步说,程序在运行之前可能不知道需要调用哪些函数。虽然这一开始看起来有点奇怪(毕竟,一个程序怎么可能不知道它将调用什么函数呢?),但它确实是一个非常强大的功能。
  考虑一个通用的磁盘驱动程序。它启动、探测硬件并检测硬盘。然后驱动程序将动态加载io-blk代码来处理磁盘块,因为它发现了一个面向块的设备。现在驱动程序已经在块级别访问了磁盘,它发现磁盘上有两个分区:一个DOS分区和一个电力安全分区。我们没有强制磁盘驱动程序包含它可能遇到的所有可能的分区类型的文件系统驱动程序,而是保持简单:它没有任何文件系统驱动程序!在运行时,它检测到两个分区,然后知道应该加载fs-dos.so和fs-qnx6.so文件系统代码来处理这些分区。通过延迟决定哪个函数调用,我们增强了磁盘驱动程序的灵活性(并减少了它的大小).

如何使用共享库

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

ELF format

  QNX中微子RTOS使用ELF(可执行和链接格式)二进制格式,该格式目前在SVR4 Unix系统中使用。ELF不仅简化了创建共享库的任务,而且增强了模块在运行时的动态加载。
  在下面的关系图中,我们展示了ELF文件的两个视图:链接视图和执行视图。链接视图,当程序或库被链接时使用,涉及各种sections(包含object文件)。Section含大量的object文件信息:数据data、指令instruction、重定位信息relocation info、符号symbol、调试信息debug info等。执行视图,程序运行时使用,涉及各种segments。
  在链接时,程序或库是通过将具有相似属性的section合并到段Segments中来构建的。通常,所有可执行的和只读的数据section被合并到一个“text”段segments中,而数据和“BSS”被合并到“data”段segments中。这些段称为加载段,因为它们需要在进程创建时加载到内存中。其他部分(如符号信息和调试部分)被合并到其他非加载段中。
Figure 37: Object file format: linking view and execution view.

ELF without COFF

  ELF加载器的大多数实现都派生于COFF(Common Object File Format)加载器。它们在加载时使用ELF对象的链接视图。这是低效的,因为程序加载器必须使用节来加载可执行文件。一个典型的程序可能包含大量的节,每个节都必须位于程序中,并分别装入内存。
  然而,QNX中微子完全不依赖于COFF加载sections的技术。在开发我们的ELF实现时,我们直接按照ELF规范工作,并将效率放在首位。ELF加载器使用程序的“执行视图”。通过使用执行视图,加载器的任务大大简化了:它所要做的就是将程序或库的加载段(通常是两个)复制到内存中。因此,流程创建和库加载操作要快得多。

典型进程的内存布局

  下图显示了一个典型进程的内存布局。进程加载段(对应于图中的文本和数据)在进程的基本地址加载。主堆栈位于下面并向下扩展。创建的任何其他线程都有自己的堆栈,位于主堆栈之下。每个堆栈由一个保护页分隔,以检测堆栈溢出。堆位于进程之上,并向上增长。
Figure38

  在进程地址空间的中间,为共享对象保留了一个大区域。共享库位于地址空间的顶部,并向下扩展。创建新进程时,进程管理器首先将可执行文件中的两个段映射到内存中。然后对程序的ELF头进行解码。如果程序头指示可执行文件链接到共享库,则进程管理器将从程序头提取动态解释器的名称。动态解释器指向一个包含运行时链接共享库。进程管理器将在内存中加载这个共享库,然后将控制权传递给这个库中的代码。

Runtime linker

  当针对共享对象链接的程序启动时,或者当程序请求动态加载共享对象时,将调用运行时链接器。运行时链接器包含在C运行时库中。
  运行时链接器在加载共享库时执行几个任务(.so file):

  1. 如果请求的共享库还没有加载到内存中,运行时链接器会加载它:
  • 如果共享库名是完全限定的(即,以斜线开头),它直接从指定位置加载。如果在那里找不到,则不执行进一步的搜索。
  • 如果它不是一个完全限定的路径名,运行时链接器将按如下方式搜索它:
    1. 如果可执行文件的动态部分包含DT_RPATH标记,则搜索由DT_RPATH指定的路径。
    2. 如果没有找到共享库,则运行时链接器仅在程序未标记为setuid的情况下,在LD_LIBRARY_PATH指定的目录中搜索它。
    3. 如果仍然没有找到共享库,那么运行时链接器将根据LD_LIBRARY_PATH环境变量(为procnto指定的默认库搜索路径)(即 CS_LIBPATH配置字符串)。如果没有指定,则将默认的库路径设置为映像文件系统的路径。
  1. 一旦找到请求的共享库,它就会被加载到内存中。对于ELF共享库,这是一个非常有效的操作:运行时链接器只需要使用两次mmap()调用来将两个加载段映射到内存中。
  2. 然后,共享库被添加到进程已加载的所有库的内部列表中。运行时链接器维护这个列表。
  3. 运行时链接器然后解码共享对象的动态部分。
      此动态部分向链接器提供关于此库所链接的其他库的信息。它还提供了关于需要应用的重新定位和需要解析的外部符号的信息。运行时链接器将首先加载任何其他需要的共享库(它们本身可能引用其他共享库)。然后它将处理每个库的重新定位。其中一些重定位是库的本地重定位,而其他重定位则需要运行时链接器来解析全局符号。在后一种情况下,运行时链接器将在库列表中搜索此符号。在ELF文件中,散列表用于符号查找,因此速度非常快。查找符号库的顺序非常重要,我们将在下面的符号名称解析一节中看到。
      一旦应用了所有重定位,就会调用在共享库的init部分注册的任何初始化函数。这在c++的一些实现中用于调用全局构造函数。

在运行时加载共享库

  通过使用dlopen()调用,进程可以在运行时加载共享库,该调用指示运行时链接器加载该库。加载库之后,程序可以使用dlsym()调用来确定其地址,从而调用库中的任何函数。
Note:请记住:共享库只对动态链接的进程可用。
  该程序还可以使用dladdr()调用来确定与给定地址相关联的符号。最后,当进程不再需要共享库时,它可以调用dlclose()从内存中卸载该库。

符号名称解析

  当运行时链接器加载共享库时,必须解析该库中的符号。符号解析的顺序和范围很重要。如果共享库调用的函数恰好在程序加载的多个库中以相同的名称存在,则搜索这些库中此符号的顺序至关重要。这就是为什么OS定义了几个加载库时可以使用的选项。
  所有具有全局作用域的对象(可执行程序和库)都存储在一个内部列表(全局列表)中。默认情况下,任何全局作用域对象都将其所有符号提供给任何加载的共享库。全局列表最初包含可执行文件和在程序启动时加载的任何库。
  默认情况下,当使用dlopen()调用加载一个新的共享库时,该库中的符号通过以下顺序搜索解析:

  1. 加载的共享库
  2. LD_PRELOAD环境变量指定的库列表。您可以在运行程序时使用此环境变量来添加或更改功能。对于setuid或setgid ELF二进制文件,只加载包含在标准搜索目录中也setuid的库。
  3. 全局列表
  4. 共享库引用的任何依赖对象(即,共享库链接到的任何其他库)
      当dlopen()'ing一个共享库时,运行时链接器的作用域行为可以通过两种方式改变:
  5. 当程序加载一个新库时,它可以通过将RTLD_GLOBAL标志传递给dlopen()调用,指示运行时链接器将库的符号放在全局列表中。这将使库的符号对随后加载的任何库都可用。
  6. 可以修改解析共享库中的符号时搜索的对象列表。如果将RTLD_GROUP标志传递给dlopen(),则只搜索库直接引用的对象的符号。如果传递RTLD_WORLD标志,只搜索全局列表中的对象。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章