GCC链接器:在指定部分移动符号

Posted

技术标签:

【中文标题】GCC链接器:在指定部分移动符号【英文标题】:GCC linker: move a symbol in a specified section 【发布时间】:2015-05-27 13:35:41 【问题描述】:

可以将代码中的某些功能移动到特定的部分 在可执行文件上?如果有,怎么做?

对于使用 gcc 编译的应用程序,我们有更多的源文件,包括 X.C.每个对象都是从相关的源代码编译的(X.o 是从 X.c 获得的),并且链接器会生成一个大的可执行文件。

我需要 X.c 中的两个函数放在 可执行文件,比如 .magic_section。我想要这个的原因是 该部分将被加载到与其余部分不同的另一个内存区域中。

我的问题是我无法更改源 X.c,否则我会使用 一个特定的标志,例如__attribute__ ((section ("magic_section"))) 功能。

我在linker 的文档中阅读了一些内容并为链接器编写了一个自定义脚本,但我未能指定必须将特定符号放置在哪个部分。我只设法移动了整个部分。

【问题讨论】:

【参考方案1】:

假设您的 GCC 版本/架构支持这些选项,您可能会这样做(不是很好,但理论上应该可行)是使用 --function-sections--data-sections,然后手动调用所有功能& 需要通过链接描述文件进入给定文件的变量。

这会创建类似于 .text.function_name.data.variable_name 的部分。如果你熟悉通过 gcc 属性分配节,我假设你知道在链接器中做什么。

作为一个优势,如果您实际上不希望整个文件进入一个神奇的部分,那么它可以让您挑选函数。

【讨论】:

嗨!很棒的评论,我不知道 --data-sections 和 --function-sections 属性。当你不是编译代码的人时,你如何使用它? @IshayPeled - 我认为 MathPlayer 正在编译代码,只是不允许修改源代码。这种情况可能会出现,例如当您拥有用于调试目的的商业软件的完整源代码许可但“使保修无效”和/或在修改时违反使用条款。机器生成的源代码是避免修改文件的另一个常见原因。 @IshayPeled - 仅供参考,--function-sections 最常用于通过将其与 binutils 链接器 (ld) 选项 --gc-sections 配对来删除死代码。我从未见过它用于定位符号,但我看不出它为什么不起作用。 感谢您的意见!看起来这并不能解决这个特定问题,但我绝对可以看到自己将来会使用它。 @Brian McFarland 是的,我完全控制了编译代码,我不允许修改一些源代码,它们非常大,我只需要优化两个函数的调用,所以这个解决方案对我来说似乎很完美。【参考方案2】:

不幸的是,如果不修改二进制对象、动态链接器或动态加载器,您将无法完成此任务,无论如何,这是一项非常困难的任务。

选项 1 - ELF 操作

每个 ELF 可执行文件都由部分组成,其中包含实际的代码/数据/符号字符串/...以及帮助加载程序决定将代码加载到内存中的位置、此 ELF 公开哪些符号、哪些符号等它需要从其他位置,加载特定代码/数据的位置等。

您可以通过键入来观察二进制文件中的段

readelf -l [你的二进制文件]

输出将类似于以下内容(我选择 ls 作为二进制):

[ihaypeled@ihay-dev bin]$ readelf -l --wide ./ls

Elf file type is EXEC (Executable file)
Entry point 0x4048bf
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000400040 0x0000000000400040 0x0001f8 0x0001f8 R E 0x8
  INTERP         0x000238 0x0000000000400238 0x0000000000400238 0x00001c 0x00001c R   0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000400000 0x0000000000400000 0x01b694 0x01b694 R E 0x200000
  LOAD           0x01bdf0 0x000000000061bdf0 0x000000000061bdf0 0x000864 0x0016d0 RW  0x200000
  DYNAMIC        0x01be08 0x000000000061be08 0x000000000061be08 0x0001f0 0x0001f0 RW  0x8
  NOTE           0x000254 0x0000000000400254 0x0000000000400254 0x000044 0x000044 R   0x4
  GNU_EH_FRAME   0x01895c 0x000000000041895c 0x000000000041895c 0x00071c 0x00071c R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
  GNU_RELRO      0x01bdf0 0x000000000061bdf0 0x000000000061bdf0 0x000210 0x000210 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .dynamic .got 

现在让我们检查一下这个输出:

在第一个表(程序标题)中: [类型] - 段类型,这部分的目的是什么 [偏移量] - 文件中此段开始的偏移量 [VirtAddr] - 我们要在进程地址空间中加载此段的位置(如果应该加载此段,则并非全部都加载) [PhysAddr] - 与我遇到的所有现代操作系统的 VirtAddr 相同 [FileSiz] - 这部分的文件有多大。这是指向您的部分的链接 - 当前部分由 Offset 到 Offset+FileSiz 范围内的所有部分组成 [MemSiz] - 此部分在虚拟内存中有多大(这不必与文件中的大小相同!如果超出文件中的大小,则超出部分设置为 0) [Flg] - 权限标志,R-读 E-执行 W-写。 [Align] - 内存中需要的内存对齐。

您的重点是 LOAD (PT_LOAD) 类型的片段。这些段对段中的数据进行分组,指示加载程序将它们放在进程地址空间中的哪个位置,并确定指定它们的权限。

您可以在 Section to Segment 映射表中看到方便的 section to Segment 映射。

让我们观察两个 LOAD 段 2 和 3: 我们可以看到段 2 具有读取和执行权限,并且它跨越(以及其他).text 和 .rodata 部分。

所以,使用 ELF 操作来达到你的目的:

    在文件中找到生成函数的二进制数据(readelf 实用程序是您的朋友) 通过修改 ELF 标头(我不知道有任何工具可以自动执行此操作,您可能必须自己编写)将包含 .text 部分的段拆分为两个连续的 LOAD 段,省略您的函数代码 通过修改 ELF 标头创建一个仅包含您的两个函数的新 LOAD 段 将所有对旧函数位置的引用(如果有)更新到新的位置

如果您阅读到这里并理解了所有内容,您应该知道这对于现实生活中的案例来说是一项极其乏味且几乎不可能完成的任务。

选项 2 - 动态链接器操作 请注意上面示例中的 INTERP 段类型。这是一个 ASCII 字符串,用于指定您应该使用哪个动态链接器。 动态链接器的作用是解析段并执行所有动态操作,例如在运行时解析符号、从 .so 文件加载段等。

这里可能的操作是修改动态链接器代码(注意:这是系统范围的更改!)以将函数二进制数据加载到进程地址空间中的特定内存地址。请注意,这种方法有几个挫折:

    需要修改动态链接器 您仍需要更新 ELF 文件中对您的函数的所有引用

选项 3 - 动态加载器操作 很像选项 2,但修改 ld 库设施而不是动态链接器。

结论 正是你想做的事情非常困难,而且确实是一项乏味的任务。我目前正在开发一种允许将任意函数注入现有共享对象文件的工具,我保证这至少需要几个星期的工作。 您确定没有其他方法可以实现您想要的吗?为什么需要将这两个函数放在一个单独的地址中?也许有更简单的解决方案...

【讨论】:

我将另一个答案标记为已接受,因为对于我的具体问题,以这种方式解决它更简单。有趣的是,我前段时间做过一个类似的项目,我不得不重写一些符号并将新代码注入到现有对象中,我明白你为什么说这是一项乏味的任务。 是的,我不认为你可以编译代码,如果可以的话,指示链接器如何执行此操作要简单得多。

以上是关于GCC链接器:在指定部分移动符号的主要内容,如果未能解决你的问题,请参考以下文章

我可以让 gcc 链接器创建一个静态库吗?

gcc编译器选项-Wl,--no-undefined(告诉链接器在链接过程中不允许有未定义的符号)(gcc编译器和链接器是分离的工具,它们需要通过选项来进行通信)

gcc 链接器 - 将存档中的所有目标文件映射到某个部分

gcc 链接器如何获取函数的大小?

构建时的链接器符号算术计算错误的结果

为什么gcc在使用-l时动态链接?