如何创建静态链接共享库

对于我的硕士论文,我正在尝试为ARM Cortex-M3嵌入式系统调整共享库方法。 由于我们的目标板没有MMU,我认为使用“普通”动态共享库是没有意义的。 因为.text是直接从flash执行的,并且.data在启动时被复制到RAM,所以我无法解决相对于代码的.data,因此GOT也是如此。 必须通过必须在链接时定义的绝对地址来访问GOT。 那么为什么不在链接时为所​​有符号分配固定的绝对地址……?

从“链接器和加载器”一书中我了解到“静态链接共享库,即库中程序和数据地址在链接时绑定到可执行文件的库”。 链接的章节描述了如何创建这样的库,并提供对Unix System V,BSD / OS的引用; 还提到了Linux和它的uselib()系统调用。 不幸的是,本书没有提供如何实际创建诸如工具和/或编译器/链接器开关之类的库的信息。 除了那本书之外,我几乎没有找到任何关于这些“野外”图书馆的信息。 我在这方面唯一发现的是Linux的预链接 。 但是,由于这是在“正常”动态库上运行,而不是我正在寻找的。

我担心这些库的使用非常具体,因此不存在创建它们的常用工具。 虽然在这种情况下提到的uselib()系统调用让我感到疑惑。 但是我想确保在开始破解我自己的链接器之前我没有忽略任何东西……;)那么有人能给我更多关于这些库的信息吗?

此外,我想知道是否有任何gcc / ld开关链接和重定位文件,但保留文件中的重定位条目 – 以便它可以重新重新定位? 我找到了“-r”选项,但完全跳过了重定位过程。 有没有人有想法?

编辑:

是的,我也知道链接器脚本。 使用gcc libfoo.c -o libfoo -nostdlib -e initLib -Ttext 0xdeadc0de我设法得到某种链接和重定位的目标文件。 但到目前为止,我还没有找到任何可能将主程序与此链接并将其用作共享库的可能性。 (链接动态共享库的“正常方式”将被链接器拒绝。)

概念

这种共享库可能的最小概念。

  • 相同的代码
  • 不同的数据

这有变化。 你支持图书馆之间的链接吗? 参考文献是DAG结构还是完全循环? 您想将代码放入ROM中,还是支持代码更新? 您是否希望在最初运行进程后加载库? 最后一个通常是静态共享库动态共享库之间的区别。 虽然许多人也会禁止图书馆之间的引用。

设备

最终,一切都将归结为处理器的寻址模式。 在这种情况下,ARM拇指。 加载器通常耦合到OS和使用中的二进制格式。 您的工具链(编译器和链接器)还必须支持二进制格式并可以生成所需的代码。

通过寄存器访问数据的支持是APCS (ARM过程调用标准)中固有的。 在这种情况下,通过sb (对于静态基础)访问数据,该寄存器是R9静态基础堆栈检查是可选function。 我相信您可能需要配置/编译GCC以启用或禁用这些选项。

选项-msingle-pic-base-mpic-register在GCC手册中 。 想法是OS最初将为每个库用户分配单独的数据,然后在上下文切换上加载/重新加载sb 。 当代码运行到库时,通过sb访问该实例数据的数据。

Gcc的arm.c代码有require_pic_register() ,它为共享库中的数据引用执行代码生成。 它可能对应于ARM ATPCS共享库机制。 见5.5节

您可以通过使用宏和内联汇编程序以及可能的函数注释来绕过工具链,例如nakedsection 。 但是,在这种情况下,库和可能的进程需要修改代码; 即,像EXPORT(myFunction)等非标准宏。

一种可能性

如果系统已完全指定(ROM映像),则可以设置偏移量,以便预先生成系统中每个库唯一的数据偏移量。 使用链接描述文件可以很容易地完成此操作。 使用NOLOAD并将库数据放在某些虚假部分中。 甚至可以使主程序成为静态共享库 。 例如,您正在制作具有四个以太网端口的网络设备。 主应用程序处理一个端口上的流量。 您可以使用不同的数据生成应用程序的四个实例,以指示正在处理哪个端口。

如果库类型的混合/匹配很大,则库数据的占地面积可能会变大。 在这种情况下,您需要在通过库的外部API上的包装函数进行调用时重新调整sb

  void *__wrap_malloc(size_t size) /* Wrapped version. */ { /* Locals on stack */ unsigned int new_sb = glob_libc; /* accessed via current sb. */ void * rval; unsigned int old_sb; volatile asm(" mov %0, sb\n" : "=r" (old_sb); volatile asm(" mov sb, %0\n" :: "r" (new_sb); rval = __real_malloc(size); volatile asm(" mov sb, %0\n" :: "r" (old_sb); return rval; } 

请参阅GNU ld –wrap选项。 如果您拥有更大的同类库集,则需要这种复杂性。 如果您的库只包含’libc / libsupc ++’,那么您可能不需要包装任何东西。

ARM ATPCS有编译器插入的代码,它们具有相同的function,

 LDR a4, [PC, #4] ; data address MOV SB, a4 LDR a4, [PC, #4] ; function-entry BX a4 DCD data-address DCD function-entry 

使用这种技术的库数据的大小是4k(可能是8k,但可能需要编译器修改)。 限制是通过ldr rN, [sb, #offset] ,ARM限制偏移到12位。 使用包装,每个库都有4k的限制。

如果原始应用程序构建时有多个未知的库,则需要将每个库包装起来并通过OS加载程序在主应用程序静态库中的固定位置放置GOT类型表。 每个应用程序都需要空间用于每个库的指针。 如果应用程序未使用该库,则操作系统不需要分配空间,并且该指针可以为NULL

可以通过.text已知位置,通过原始进程sb或通过堆栈的掩码访问库表 。 例如,如果所有进程都获得2K堆栈,则可以为库表保留低16个字。 sp & ~0x7ff将为所有任务提供隐式锚点。 操作系统也需要分配任务堆栈。

请注意,此机制与ATPCS不同,后者使用sb作为表来获取实际库数据的偏移量。 由于内存对Cortex-M3的描述相当有限,因此每个库不太可能需要使用超过4k的数据。 如果系统支持分配器,则可以解决此限制。

参考

  • Xflat技术概述 – Xflat作者的技术讨论; Xflat是一种支持共享库的uCLinux二进制格式。 一个非常好的阅读。
  • PLT和GOT上的联动表和GOT – SO。
  • ARM EABI – 正常的ARM二进制格式。
  • 汇集者和装载者 , David Solomon 。 特别是,pg262 A.3基本寄存器
  • ARM ATPCS ,特别是第5.5节,共享库 ,第18页。
  • bFLT是另一种支持共享库的uCLinux二进制格式。

你附加了多少内存? Cortex-M系统片上只有几十个kiB,其余的则需要外部SRAM。

我无法解决相对于代码的.data问题

你不必。 您可以将库符号跳转表放在固定位置的.data段(或行为相似的段)中。

因此也是GOT。 必须通过必须在链接时定义的绝对地址来访问GOT。 那么为什么不在链接时为所​​有符号分配固定的绝对地址……?

没有什么能阻止你在固定位置放置第二个GOT,这是可写的。 您必须指示链接器在何处以及如何创建它。 为此,您为链接器提供了一个所谓的“链接器脚本”,它是最终程序内存布局的模板蓝图。

在评论您的意图之前,我会尝试回答您的问题。

要在linux / solaris /任何使用ELF二进制文件的平台上编译文件:

 gcc -o libFoo.so.1.0.0 -shared -fPIC foo1.c foo2.c foo3.c ... -Wl,-soname=libFoo.so.1 

我将在下面解释所有选项:

 -o libFoo.so.1.0.0 

是链接后我们要给共享库文件的名称。

 -shared 

意味着您在末尾有一个共享对象文件,因此在编译和链接后可能会有未解决的引用,这将在后期绑定中解决。

 -fPIC 

指示编译器生成与位置无关的代码,因此库可以以可重定位的方式链接。

 -Wl,-soname=libFoo.so.1 

有两部分:第一, -Wl指示编译器将下一个选项(用逗号分隔)传递给链接器。 选项是-soname=libFoo.so.1 。 此选项告诉链接器用于此库的soname。 soname的确切值是自由样式字符串,但是有一个方便的自定义来使用库的名称和主要版本号。 这很重要,因为当您对共享库进行静态链接时,库的soname会粘在可执行文件上,因此只能加载具有该soname的库来协助此可执行文件。 传统上,当只更改库的实现时,我们只更改库的名称,而不更改soname部分,因为库的接口不会更改。 但是当您更改界面时,您正在构建一个新的,不兼容的界面,因此您必须更改soname部分,因为它不会与其他“版本”冲突。

链接到共享库与链接到静态库(具有.a作为扩展名的库)相同,只需将其放在命令文件中,如下所示:

 gcc -o bar bar.c libFoo.so.1.0.0 

通常,当您在系统中获得某个库时,您会在/ usr / lib目录中获得一个文件和一个或两个符号链接:

 /usr/lib/libFoo.so.1.0.0 /usr/lib/libFoo.so.1 --> /usr/lib/libFoo.so.1.0.0 /usr/lib/libFoo.so --> /usr/lib/libFoo.so.1 

第一个是执行程序时调用的实际库。 第二个是使用soname作为文件名的链接,只是为了能够进行后期绑定。 第三个是你必须要做的

 gcc -o bar bar.c -lFoo 

工作。 ( gcc和其他ELF编译器搜索libFoo.so ,然后搜索libFoo.a ,在/usr/lib目录中)

毕竟,对共享库的概念有一个解释,这可能会让您改变关于静态链接共享代码的图像。

动态库是多个程序共享它们function的一种方式(即代码,也可能是数据)。 我认为你有点迷失方向,因为我觉得你有点误解了静态链接共享库的意思。

静态链接是指程序与它启动之前将要使用的共享库的关联,因此程序与库中的所有符号之间存在硬连线。 启动程序后,链接过程开始,您将获得一个运行所有静态链接共享库的程序。 解析对共享库的引用,因为共享库在进程的虚拟内存映射中被赋予固定位置。 这就是必须使用-fPIC选项(可重定位代码)编译库的原因,因为它可以在每个程序的虚拟空间中以不同方式放置。

相反,共享库的动态链接是指使用一个库( libdl.so ),它允许您加载(一旦程序执行)一个共享库(甚至是之前未知的那个),搜索它的公共符号,解决引用,加载更多与此相关的库(并且可以像链接器那样递归地求解)并允许程序对其上的符号进行调用。 该程序甚至不需要知道库是否存在编译或链接时间。

共享库是与代码共享相关的概念。 很久以前,有UNIX,并且通过它的所有实例共享文本段(不能让程序修改自己的代码的惩罚)取得了很大的进步,所以你必须等待它第一次加载。 如今,代码共享的概念已扩展到库概念,您可以使用相同的库(可能是libc,libdl或libm)使用多个程序。内核对所有使用它的程序进行计数引用,以及它只是在没有其他程序正在使用它时被卸载。

使用共享库只有一个缺点:编译器必须创建可重定位代码以生成共享库,因为当我们尝试将其链接到另一个程序时,一个程序使用的空间可用于另一个库。 这通常会对要生成的操作码集产生限制或者强制使用一个/多个寄存器来处理代码的移动性(没有移动性,但有几个链接可以使它位于不同的位置)

相信我,使用静态代码只会导致您制作更大的可执行文件,因为您无法有效地共享代码,但使用共享库。