如何创建静态链接共享库

11
为了我的硕士论文,我正在尝试为ARM Cortex-M3嵌入式系统适应共享库方法。由于我们的目标板没有MMU,因此我认为使用“普通”的动态共享库是没有意义的。因为.text直接从flash中执行,.data在启动时被复制到RAM中,所以我无法相对于代码来寻址.data,也无法通过GOT来访问GOT必须通过绝对地址在链接时进行定义。那么为什么不在链接时为所有符号分配固定的绝对地址呢?
《链接器和装载机》这本书中,我了解了“静态链接共享库”,即库中程序和数据地址在链接时绑定到可执行文件中。该章节描述了如何创建此类库,还提到Unix System V、BSD/OS、Linux及其uselib()系统调用。不幸的是,该书没有提供有关实际创建此类库的工具和/或编译器/链接器开关的信息。除了这本书之外,在“野外”中几乎找不到有关这种库的其他信息。我在这方面找到的唯一一件事是针对Linux的prelink。但由于它操作的是“普通”的动态库,所以这并不是我要找的。
我担心使用这种类型的库非常特定,因此没有共用工具来创建它们。虽然在这种情况下提到的uselib()系统调用让我感到疑惑。但在开始自己的链接器之前,我希望确保我没有忽略任何东西...;)那么,有人可以给我更多关于此类库的信息吗?
此外,我想知道是否有gcc/ld开关可以将一个文件链接和重定位,但保留文件中的重定位项,以便重新定位?我找到了“-r”选项,但它完全跳过了重定位过程。有人有什么想法吗?

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


4
希望回答问题的人请注意:如果你想解释AR存档和DSO之间的区别,请立即停下来,仔细再次阅读问题。这与它们都不相关。请直接进行翻译即可。 - n. m.
@datenwolf 不,我看到这根本不是为共享库而设计的。 - n. m.
@n.m. 实际上这篇论文的总体目标是让板子上的库能够独立更新,并且在不同的板子上可以有任意排列组合的库。请参阅我对问题的编辑。 - Cryptkeeper
@ninjalj 目前我正在使用 ELF。我之前不知道 ELF-FDPIC 或 bFLT。我会对这些格式进行一些研究。 - Cryptkeeper
还可以参见可重定位代码和目标文件以及共享库(请参见Ian Lance Taylor的回复)。 - jww
显示剩余2条评论
3个回答

8

概念

共享库的最小概念。

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

这有所变化。您是否支持链接库之间的链接?引用是 DAG 结构还是完全循环?您想将代码放在 ROM 中,还是支持代码更新?您是否希望在进程初始运行后加载库?这通常是静态共享库和动态共享库之间的区别。尽管很多人也会禁止在库之间引用。

设施

最终,一切都将归结于处理器的寻址模式。在这种情况下,是 ARM Thumb。 加载程序 通常与操作系统和正在使用的二进制格式耦合。您的工具链(编译器和链接器)还必须支持二进制格式,并能生成所需的代码。

支持通过寄存器访问数据是APCS(ARM过程调用标准)中的内在特性。在这种情况下,数据通过sb(静态基址)访问,该寄存器为R9。静态基址和堆栈检查是可选功能。我认为您可能需要配置/编译GCC以启用或禁用这些选项。
选项-msingle-pic-base-mpic-registerGCC手册中。其思想是操作系统最初将为每个库用户分配单独的数据,然后在上下文切换时加载/重新加载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由编译器插入了等效的细木工连接件。
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二进制格式。非常值得一读。
  • 链接表和GOT - PLT和GOT上的SO。
  • ARM EABI - 普通的ARM二进制格式。
  • 汇编器和加载器,由David Solomon撰写。特别是第262页A.3基址寄存器
  • ARM ATPCS,特别是第5.5节,共享库,第18页。
  • bFLT是另一种支持共享库的uCLinux二进制格式。

Gcc的arm.c链接上面有误。 - artless noise
我相信u-boot使用静态基址方法来访问一些公共数据,以避免重定位。 - artless noise
从概念上讲,“静态共享库”可以视为C++对象和类。代码是“类”,而“数据”或静态基则代表“对象”。您可以将所有C++机制进行翻译。 C++编译器在优化方面取得了巨大的进步,语言更新着眼于维护并将性能接近非常好的‘C’代码。一个例外是异常。因此,如果您以“clean room”眼光看待这个问题,我会考虑将C++对象作为库的一个选择。 - artless noise
Eli Bendersky写的两篇博客,关于x86架构下的加载时重定位位置无关代码(PIC),值得一读。 - artless noise

2

你连接了多少RAM?Cortex-M系统只有几十KiB的片上RAM,其余需要外部SRAM。

我无法相对于代码地址访问.data

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

因此也涉及到GOT。GOT必须通过绝对地址访问,该地址必须在链接时定义。那么为什么不在链接时为所有符号分配固定的绝对地址呢...?

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


我们的板子有256 kiB闪存和只有48 kiB的RAM;没有外部SRAM。我不太确定你所说的第二个GOT是什么意思,但是:如果我链接一个库/程序来定位一个符号(可能是一个函数或固定位置的GOT)在一个板子上的固定地址,我可能需要重新链接它以使用另一个可用地址在另一个板子上。请参见上面关于总体目标的评论。如果我必须为每个板子重新链接库,则在链接时分配固定地址更有意义。 - Cryptkeeper
@Cryptkeeper:你将会遇到的问题是,没有任何绕过的方法,你必须间接地访问所有全局符号。比如说,一个替代库依赖于一些在.text段中的static const存储的数据。你要么在模块之间保留充足的空间(这很低效),要么将所有东西解压到RAM(.bss)中并从那里工作。坦白地说,我认为你(或你的顾问)正在错误的方向上努力。通常情况下,你必须重新编程整个闪存以进行任何固件更新,而且由于有限的大小,链接最终二进制文件几乎是“立即”发生的。 - datenwolf
@Cryptkeeper:实际上,上传新的固件映像所需的时间比整个编译和链接阶段要长,即使您完全发挥了Cortex-M的能力。根据我的经验,闪存映像的增量更新只会带来很多痛苦。实际上,每当我进行固件构建+闪存时,我的命令行看起来像 make clean; make -j6 flash - datenwolf
经过仔细思考,我同意全局符号必须间接访问 - 但是要按照链接的章节描述的方式进行,而不是像“普通”共享库那样以动态方式进行。此外,我考虑过在需要时直接在板上进行一些重定位的可能性,因此提出了一个保留重定位条目的ld开关的问题。 - Cryptkeeper
我之前没有看到这个对话。关于固件下载,bsdiff和Courgette可能会引起兴趣。然而,在现场修补等方面,不会与闪存很好地配合使用。此外,存在两个副本可以进行回滚。但这完全与“静态共享库”的初衷相反。它是具有不同数据的公共代码。 - artless noise

0

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

在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编译器在/usr/lib目录中搜索libFoo.so,然后搜索libFoo.a

最后,有一个关于共享库概念的解释,也许会让您改变对静态链接共享代码的看法。

动态库是几个程序共享它们的功能(也就是代码,也许还有数据)的一种方式。我认为您有点迷失方向,因为我感觉您对静态链接共享库的理解有些错误。

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

相反,共享库的动态链接是指使用一个库(libdl.so),允许您在程序执行时加载共享库(即使以前未知),搜索其公共符号,解决引用,加载与此相关的更多库(并像链接器一样递归地解决),并允许程序调用其中的符号。程序甚至不需要知道编译或链接时存在该库。
共享库是与代码共享相关的概念。很久以前,有UNIX,它通过所有实例共享程序的文本段(付出无法修改自己代码的代价)取得了巨大进步,因此您必须等待它仅在第一次加载。现在,代码共享的概念已扩展到库概念,并且您可以有多个程序使用同一个库(例如libc、libdl或libm)。内核对正在使用它的所有程序进行计数引用,并且只有在没有其他程序使用它时才会卸载它。

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

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


OP的目标是嵌入式系统(ARM Cortex-M)。很可能甚至没有运行必要支持动态链接的操作系统。 - datenwolf
2
“静态库”和“静态链接共享库”是完全不同的东西。请注意,在“静态链接共享库”中,每个单词都很重要。你可能没有听说过“静态链接共享库”,这项技术存在于Unix早期,并且几乎被动态共享库完全取代并遗忘了。 - n. m.
我知道,但我认为 Cryptkeeper 不知道。 - Luis Colorado

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接