【问题标题】:How to 'link' object file to executable/compiled binary?如何将目标文件“链接”到可执行/编译的二进制文件?
【发布时间】:2024-01-16 11:19:01
【问题描述】:

问题

我希望将目标文件注入到现有的二进制文件中。作为一个具体的例子,考虑一个来源Hello.c

#include <stdlib.h>

int main(void)
{
    return EXIT_SUCCESS;
}

它可以编译成一个名为Hellogcc -std=gnu99 -Wall Hello.c -o Hello 的可执行文件。此外,现在考虑Embed.c

func1(void)
{
}

一个目标文件Embed.o可以通过gcc -c Embed.c创建。我的问题是如何将Embed.o 插入到Hello 中,以便执行必要的重定位,并正确修补适当的 ELF 内部表(例如符号表、PLT 等)?


假设

可以假设要嵌入的目标文件已经静态链接了它的依赖关系。可以假定任何动态依赖项(例如 C 运行时)也存在于目标可执行文件中。


目前的尝试/想法

  • 使用libbfd 将目标文件中的部分复制到二进制文件中。我在这方面取得的进展是我可以使用原始二进制文件中的部分和目标文件中的部分创建一个新对象。问题在于,由于目标文件是可重定位的,因此如果不先执行重定位,就无法将其部分正确复制到输出中。
  • 将二进制文件转换回目标文件并使用ld 重新链接。到目前为止,我尝试使用objcopy 执行转换objcopy --input elf64-x86-64 --output elf64-x86-64 Hello Hello.o。显然这并不像我想要的那样工作,因为ld -o Hello2 Embed.o Hello.o 将导致ld: error: Hello.o: unsupported ELF file type 2。我想这应该是可以预料的,因为Hello 不是目标文件。
  • 找到执行这种插入的现有工具?

基本原理(可选阅读)

我正在制作一个静态可执行编辑器,其愿景是允许将任意用户定义的例程检测到现有二进制文件中。这将分两步进行:

  1. 将目标文件(包含用户定义的例程)注入二进制文件。 这是一个强制性步骤,不能通过注入共享对象等替代方法来解决。
  2. 对新的二进制文件执行静态分析,并使用它静态地将例程从原始代码绕道到新添加的代码。

在大多数情况下,我已经完成了第 2 步所需的工作,但是我在注入目标文件时遇到了问题。考虑到其他工具使用相同的对象注入方法(例如EEL),这个问题肯定是可以解决的。

【问题讨论】:

  • 对问题的快速阅读留下了不理解运行时链接器和普通链接器之间的概念的感觉。运行时链接器/程序加载器仅在易于快速修复的格式上运行。 .o 不是其中之一 :-) 如果它具有最小的依赖项,例如编解码器,则使用最少的代码链接以使其成为 .so 听起来像逻辑路由
  • @MarcovandeVoort:感谢您的评论 :) 我松散地使用了“链接”一词,因为有人可能会使用“注入”,这就是我将其放在引号中的原因。我无法将其设为.so 的原因之一是应用程序可以破坏诸如LD_PRELOAD 之类的注入技巧。不仅如此,它还需要分发一个形成新环境的附加库。静态迂回还有其他各种优点(特别是出于本项目的目的),但正如我在问题和 cmets 中已经说过的那样,这不是我可以改变的设计决策 :)
  • 您是否尝试在 AIX(以及我所知道的其他任何地方)上执行类似 ld 的功能来重新链接只有一个目标文件已更改的可执行文件?
  • @evilotto:我想添加一个以前从未出现过的新目标文件。
  • 您是否介意分享一下 Rationale 下的 #2 如何成为可能?如果您现在知道 OP 的答案,我也会对此感到非常好奇。

标签: c linux linker


【解决方案1】:

如果是我,我希望将Embed.c 创建为共享对象libembed.so,如下所示:

gcc -Wall -shared -fPIC -o libembed.so Embed.c

这应该会从Embed.c 创建一个可重定位的共享对象。这样,您可以通过在运行时设置环境变量 LD_PRELOAD 来强制目标二进制文件加载此共享对象(请参阅更多信息 here):

LD_PRELOAD=/path/to/libembed.so Hello

这里的“诀窍”是弄清楚如何进行检测,尤其是考虑到它是一个静态可执行文件。在那里,我帮不了你,但这是让代码出现在进程的内存空间中的一种方法。您可能希望在构造函数中进行某种初始化,您可以使用属性来完成(至少如果您使用的是gcc):

void __attribute__ ((constructor)) my_init()
{
    // put code here!
}

【讨论】:

  • 是的,这是实现绕行的替代方案。关于如何实现补丁的问题,可以使用 __attribute__((constructor)) GCC 属性来完成,该属性允许在加载库时调用方法。可执行文件也可以被欺骗认为共享对象是一个依赖项。这是一个名为 LEEL 的现有工具使用的方法。
  • 不幸的是,运行时/动态迂回不是一个可接受的解决方案。这是项目开始时明确声明的要求。
【解决方案2】:

假设第一个可执行文件的源代码可用并且使用链接描述文件编译,该链接描述文件为以后的目标文件分配空间,则有一个相对简单的解决方案。由于我目前正在处理一个 ARM 项目,因此下面的示例是使用 GNU ARM 交叉编译器编译的。

主要源代码文件,hello.c

#include <stdio.h>

int main ()
{

   return 0;
}

使用简单的链接器脚本构建,为稍后嵌入的对象分配空间:

SECTIONS
{
    .text :
    {
        KEEP (*(embed)) ;

        *(.text .text*) ;
    }
}

喜欢:

arm-none-eabi-gcc -nostartfiles -Ttest.ld -o hello hello.c
readelf -s hello

Num:    Value  Size Type    Bind   Vis      Ndx Name
 0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
 1: 00000000     0 SECTION LOCAL  DEFAULT    1 
 2: 00000000     0 SECTION LOCAL  DEFAULT    2 
 3: 00000000     0 SECTION LOCAL  DEFAULT    3 
 4: 00000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
 5: 00000000     0 NOTYPE  LOCAL  DEFAULT    1 $a
 6: 00000000     0 FILE    LOCAL  DEFAULT  ABS 
 7: 00000000    28 FUNC    GLOBAL DEFAULT    1 main

现在让我们编译源在 embed.c 中的要嵌入的对象

void func1()
{
   /* Something useful here */
}

这次使用相同的链接描述文件重新编译,插入新符号​​:

arm-none-eabi-gcc -c embed.c
arm-none-eabi-gcc -nostartfiles -Ttest.ld -o new_hello hello embed.o

查看结果:

readelf -s new_hello
Num:    Value  Size Type    Bind   Vis      Ndx Name
 0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
 1: 00000000     0 SECTION LOCAL  DEFAULT    1 
 2: 00000000     0 SECTION LOCAL  DEFAULT    2 
 3: 00000000     0 SECTION LOCAL  DEFAULT    3 
 4: 00000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
 5: 00000000     0 NOTYPE  LOCAL  DEFAULT    1 $a
 6: 00000000     0 FILE    LOCAL  DEFAULT  ABS 
 7: 00000000     0 FILE    LOCAL  DEFAULT  ABS embed.c
 8: 0000001c     0 NOTYPE  LOCAL  DEFAULT    1 $a
 9: 00000000     0 FILE    LOCAL  DEFAULT  ABS 
10: 0000001c    20 FUNC    GLOBAL DEFAULT    1 func1
11: 00000000    28 FUNC    GLOBAL DEFAULT    1 main

【讨论】:

  • 我收到“你好:不支持的 ELF 文件类型 2”...(使用 arm-oe-linux-gnueabi/4.9.2/ 编译)
  • 您是否尝试过使用 arm-none-eabi-* 工具? developer.arm.com/tools-and-software/open-source-software/… 之类的工具链
  • 很抱歉无耻地提问,但如果对您有用,您也可以点赞这个答案。 :D
  • 当然。它很有用且有教育意义,尽管它在我的情况下不起作用(用于 oelinux 目标的 gcc 交叉编译器)。
【解决方案3】:

问题是 .o 还没有完全链接,而且大多数引用仍然是符号。二进制文件(共享库和可执行文件)离最终链接的代码更近了一步。

执行到共享库的链接步骤并不意味着您必须通过动态库加载器加载它。建议更多的是二进制或共享库的自己的加载器可能比 .o 更简单。

另一种可能性是自己自定义链接过程并调用链接器并将其链接到某个固定地址上。您还可以查看例如的准备工作。引导加载程序,它还包括一个基本的链接步骤来做到这一点(将一段代码固定到一个已知的加载地址)。

如果您不链接到固定地址,并且想要重定位运行时,则必须编写一个基本链接器来获取目标文件,并通过执行适当的修复将其重定位到目标地址。

我假设你已经拥有它,因为它是你的硕士论文,但是这本书:http://www.iecc.com/linker/ 是这方面的标准介绍。

【讨论】:

  • 我实际上也考虑过自定义链接过程,这就是我在这里的问题中提出的问题:*.com/questions/9508290/…。如果我能够链接某个地址的部分,我想我可以使用libbfd 将它们复制到可执行文件中。您是否知道允许您建议的工具或链接选项(将部分 - 而不是符号 - 链接到固定地址)?
  • 正如在另一个问题中已经说过的那样:链接器资源文件是要走的路。
【解决方案4】:

您必须通过扩展可执行文件文本段为可重定位代码腾出空间以适应可执行文件,就像病毒感染一样。然后在将可重定位代码写入该空间后,通过为该可重定位对象中的任何内容添加符号来更新符号表,然后应用必要的重定位计算。我编写的代码可以很好地使用 32 位 ELF。

【讨论】:

  • 欢迎来到 Stack Overflow。请演示您为解决此问题而编写的一些代码 - 这一切都很好地告诉我们您拥有它,但它现在没有帮助。
【解决方案5】:

你不能以任何实际的方式做到这一点。预期的解决方案是将该对象放入共享库,然后对其调用 dlopen。

【讨论】:

  • 感谢您的回答。请看我给 Dan Fego 的 cmets。具体来说,这是我无法更改的要求。我不确定它不能“以实际的方式”完成,因为现有的 EEL 工具可以做到这一点。
  • 我不知道什么疯子定义了你的要求,但坚持 .o 是可拉入的,而不是包含它的 .so 符合我对“疯子”的定义。我对“实用”的定义是“付出一定程度的努力”。如果您的管理层希望您花费大量时间来实现这一目标,我很同情您。
  • 你有我的同情。您的教授似乎很难从枯燥的基础设施中找出有趣的研究问题。
【解决方案6】:

你看过DyninstAPI吗?似乎最近添加了将 .o 链接到静态可执行文件的支持。

来自发布网站:

在 x86 和 x86_64 平台上对静态链接二进制文件的二进制重写器支持

【讨论】:

  • 感谢您提供此链接。我以前见过Dyninst,但不知道它也进行了静态二进制重写。我会看看这个并稍后更新。
最近更新 更多