处理ELF重定位——理解重定位、符号、节数据以及它们的协同作用。

31

简而言之

我试图把这个问题简化,但它是一个复杂的问题,所以最终变得很长。如果你能回答其中任何一部分,或者给出任何建议、提示、资源或任何有用的信息,都将非常有帮助(即使你不能直接解决我的所有问题)。我现在正在头痛难忍。

以下是我遇到的具体问题。请继续阅读下面的内容了解更多信息。

我正在寻求关于如何处理重定位条目并更新节数据中未解析符号的指导。我对从重定位和节等提取的所有信息应该怎样处理感到困惑。
当链接器遇到重定位时,我也希望了解到具体发生了什么。尝试正确实现重定位方程式并以正确的方式使用所有正确的值是非常具有挑战性的。
当我遇到操作码、地址和符号等内容时,我需要了解如何处理它们。我感觉我漏掉了一些步骤。
我感觉我没有很好地掌握符号表条目与重定位之间的交互。我应该如何使用符号的绑定、可见性、值和大小信息呢?
最后,当我输出我的文件时,包括可执行文件使用的已解析数据和新的重定位条目时,数据都不正确。我不确定如何跟踪所有的重定位并提供所有必要的信息。可执行文件对我期望什么呢?

我的做法

我正在尝试创建一个特定的[未公开]专有格式的重定位文件,该文件严重基于ELF。我编写了一个工具,它接收一个ELF文件和一个部分链接文件(PLF),并将它们处理成输出完全解析的rel文件。这个rel文件用于根据需要加载/卸载数据以节省内存。平台是32位PPC。其中一个麻烦之处在于该工具是用c#在Windows上编写的,但数据是针对PPC的,因此需要注意字节序等问题。

我一直在尝试理解重定位是如何处理未解析的符号等问题的。到目前为止,我已经从PLF中复制了相关部分,并且对于每个相应的.rela部分,我解析条目并尝试修复部分数据并根据需要生成新的重定位条目。但这就是我的困难所在。我在这方面完全不擅长,这种事情通常由链接器和装载程序完成,因此没有太多好的示例可供参考。但我找到了一些有帮助的资料,包括这个
所以正在发生的是:
  1. 从PLF中复制部分数据以用于rel文件。我只关心.init(无数据)、.text、.ctors、.dtors、.rodata、.data、.bss(无数据)和另一个我们正在使用的自定义部分。
  2. 遍历PLF中的.rela部分并读取Elf32_Rela条目。
  3. 对于每个条目,我提取r_offset、r_info和r_addend字段,并从r_info中提取相关信息(符号和重定位类型)。
  4. 从PLF的符号表中,我可以获取symbolOffset、symbolSection和symbolValue。
  5. 从ELF中,我获取symbolSection的加载地址。
  6. 我计算int localAddress =(.relaSection.Offset + r_offset)。
  7. 我从symbolSection的内容在r_offset处获取uint relocValue。
  8. 现在我有了所有需要的信息,所以我根据重定位类型进行switch并处理数据。这些是我支持的类型:
    R_PPC_NONE
    R_PPC_ADDR32
    R_PPC_ADDR24
    R_PPC_ADDR16
    R_PPC_ADDR16_LO
    R_PPC_ADDR16_HI
    R_PPC_ADDR16_HA
    R_PPC_ADDR14
    R_PPC_ADDR14_BRTAKEN
    R_PPC_ADDR14_BRNTAKEN
    R_PPC_REL24
    R_PPC_REL14
    R_PPC_REL14_BRTAKEN
    R_PPC_REL14_BRNTAKEN
  9. 现在怎么办?我需要更新部分数据并构建伴随的重定位条目。但我不理解需要做什么以及如何做。
我这样做的原因是因为有一个旧的、已经过时的不支持使用自定义部分的工具,而这对于本项目来说是一个关键要求(基于内存的原因)。我们有一个包含大量初始化代码的自定义部分(总计约1兆字节),希望在启动后卸载它。现有的工具只会忽略该部分中所有数据。
所以,虽然制作一个支持自定义部分的工具是理想的,但如果有任何其他方法可以实现这个目标,我完全听取建议!我们已经提出了使用.dtor部分来存储我们的数据的想法,因为它几乎是空的。但这很麻烦,而且如果它阻止了干净的关闭可能也行不通。

重定位加示例代码

当我处理重定位时,我依据 ABI 文档 此处链接 中的方程和信息,以及我挖掘出来的许多其他代码示例和博客文章。但这一切都很令人困惑,没有很好地解释清楚,而我找到的所有代码都有些不同。

例如:

  • R_PPC_ADDR16_LO --> half16: #lo(S + A)
  • R_PPC_ADDR14_BRTAKEN --> low14*: (S + A) >> 2
  • 等等

所以,当我看到这种代码时,我该如何解读它?

以下是一个示例(来自 此源码

case ELF::R_PPC64_ADDR14 : {
    assert(((Value + Addend) & 3) == 0);
    // Preserve the AA/LK bits in the branch instruction
    uint8_t aalk = *(LocalAddress+3);
    writeInt16BE(LocalAddress + 2, (aalk & 3) | ((Value + Addend) & 0xfffc));
} break;

case ELF::R_PPC64_REL24 : {
    uint64_t FinalAddress = (Section.LoadAddress + Offset);
    int32_t delta = static_cast<int32_t>(Value - FinalAddress + Addend);
    if (SignExtend32<24>(delta) != delta)
        llvm_unreachable("Relocation R_PPC64_REL24 overflow");
    // Generates a 'bl <address>' instruction
    writeInt32BE(LocalAddress, 0x48000001 | (delta & 0x03FFFFFC));
} break;

这是另一个示例中的一些内容(这里

case R_PPC_ADDR32: /* word32 S + A */
    addr = elf_lookup(lf, symidx, 1);
    if (addr == 0)
        return -1;
    addr += addend;
    *where = addr;
    break;

case R_PPC_ADDR16_LO: /* #lo(S) */
    if (addend != 0) {
        addr = relocbase + addend;
    } else {
        addr = elf_lookup(lf, symidx, 1);
        if (addr == 0)
            return -1;
    }
    *hwhere = addr & 0xffff;
    break;

case R_PPC_ADDR16_HA: /* #ha(S) */
    if (addend != 0) {
        addr = relocbase + addend;
    } else {
        addr = elf_lookup(lf, symidx, 1);
        if (addr == 0)
            return -1;
    }
    *hwhere = ((addr >> 16) + ((addr & 0x8000) ? 1 : 0)) & 0xffff;
    break;

还有另一个例子(从这里开始

case R_PPC_ADDR16_HA:
    write_be16 (dso, rela->r_offset, (value + 0x8000) >> 16);
    break;
case R_PPC_ADDR24:
    write_be32 (dso, rela->r_offset, (value & 0x03fffffc) | (read_ube32 (dso, rela->r_offset) & 0xfc000003));
    break;
case R_PPC_ADDR14:
    write_be32 (dso, rela->r_offset, (value & 0xfffc) | (read_ube32 (dso, rela->r_offset) & 0xffff0003));
    break;
case R_PPC_ADDR14_BRTAKEN:
case R_PPC_ADDR14_BRNTAKEN:
    write_be32 (dso, rela->r_offset, (value & 0xfffc)
                                    | (read_ube32 (dso, rela->r_offset) & 0xffdf0003)
                                    | ((((GELF_R_TYPE (rela->r_info) == R_PPC_ADDR14_BRTAKEN) << 21)
                                    ^ (value >> 10)) & 0x00200000));
    break;
case R_PPC_REL24:
    write_be32 (dso, rela->r_offset, ((value - rela->r_offset) & 0x03fffffc) | (read_ube32 (dso, rela->r_offset) & 0xfc000003));
    break;
case R_PPC_REL32:
    write_be32 (dso, rela->r_offset, value - rela->r_offset);
    break;

我真的很想了解这些人在做什么以及为什么他们的代码看起来并不总是相同。我认为一些代码假设数据已经正确地屏蔽(对于分支等),而另一些代码则没有。但我完全不理解这一切。


跟随符号/数据/重定位等操作,当我在十六进制编辑器中查看数据时,到处都是“48 00 00 01”。我已经弄清楚这是一个操作码,并且需要更新重定位信息(这具体是针对'bl'分支和链接的),但我的工具并不适用于其中的绝大部分,而我更新的那些则有错误的值(与过时工具创建的示例相比)。显然我漏掉了某个步骤。
除了节数据之外,还需要将其他重定位条目添加到rel文件的末尾。这些包括内部和外部重定位,但我还没有弄清楚它们之间的区别以及何时使用其中之一。
如果你查看这个文件末尾的函数RuntimeDyldELF :: processRelocationRef,你会看到一些重定位条目被创建。它们还创建了存根函数。我怀疑这是我缺失的链接,但它就像泥一样浑浊,我一点也不明白。
当我输出每个重定位条目中的符号时,它们都有一个绑定/可见性[全局/弱/本地][函数/对象]和一个值、大小和一个部分。我知道部分是符号所在的位置,值是该部分中符号的偏移量(或者是虚拟地址吗?)。大小是符号的大小,但这很重要吗?也许全局/弱/本地对于确定它是内部重定位还是外部重定位有用?
也许我正在谈论创建的这个重定位表实际上是我的rel文件的符号表?也许这个表将符号值从虚拟地址更新为部分偏移量(因为在可重定位文件中,值是部分偏移量,在PLF中的符号表基本上是一个可执行文件)?

一些资源:

  1. 关于重定位的博客: http://eli.thegreenplace.net/2011/08/25/load-time-relocation-of-shared-libraries/
  2. 末尾提到操作码: http://wiki.netbsd.org/examples/elf_executables_for_powerpc/
  3. 我相关的未回答的问题: ELF Relocation reverse engineering

哇!那是一个非常棘手的问题。如果你看到这里了,恭喜你!:) 非常感谢您能给我任何帮助。


可能是重定位的概念的重复问题。详细的最小示例请参见:https://dev59.com/WGct5IYBdhLWcg3wZcmL#30507725。 - Ciro Santilli OurBigBook.com
朋友,这不是提问的正确方式——你不必讲述很多可以在互联网上找到的事实。直接提出你的问题——如果可能的话,一个帖子只提一个问题。 - Peter Teoh
3个回答

20

我偶然发现了这个问题并认为它值得回答。

确保有elf.h。你可以在互联网上找到它。

每个RELA节包含一个Elf32_Rela条目数组,正如你所知,但也与某个其他节相关联。r_offset是该节的偏移量(在这种情况下 - 对于共享库,它的工作方式不同)。您会发现节标头有一个称为sh_info的成员。这告诉你那是哪个部分。(它是一个索引,就像你所期望的一样,进入节标头表。)

“符号”,你从r_info得到的,实际上是一个索引,指向另一个部分中的符号表。在您的RELA部分标题中寻找成员sh_link。

符号表告诉您要查找的符号的名称,以Elf32_Sym的st_name成员的形式。st_name是一个偏移量,指向一个字符串部分。哪个部分是您从符号表的节标头的sh_link成员中获得的。如果这使您感到困惑,很抱歉。

Elf32_Shdr *sh_table = elf_image + ((Elf32_Ehdr *)elf_image)->e_shoff;
Elf32_Rela *relocs = elf_image + sh_table[relocation_section_index]->sh_offset;

unsigned section_to_modify_index = sh_table[relocation_section_index].sh_info;
char *to_modify = elf_image + sh_table[section_to_modify_index].sh_offset;

unsigned symbol_table_index = sh_table[relocation_section_index].sh_link;
Elf32_Sym *symbol_table = elf_image + sh_table[symbol_table_index].sh_offset;

unsigned string_table_index = sh_table[symbol_table].sh_link;
char *string_table = elf_image + sh_table[string_table_index].sh_offset;

假设我们正在处理迁移编号为i的情况。

Elf32_Rela *rel = &relocs[i];
Elf32_Sym *sym = &symbol_table[ELF32_R_SYM(rel->r_info)];
char *symbol_name = string_table + sym->st_name;

找到该符号的地址(假设符号名称为“printf”)。最终值将放在(to_modify + rel->r_offset)中。

至于您链接的pdf第79-83页上的表格,它告诉我们要在该地址处写入什么内容以及要写入多少字节。显然,我们刚刚得到的地址(在这种情况下是printf的地址)是其中大部分的一部分。它对应于表达式中的S。

r_addend只是A。我猜有时编译器需要将静态常量添加到重定位中。

B是共享对象的基地址,对于可执行程序来说是0,因为它们不会移动。

因此,如果ELF32_R_TYPE(rel->r_info)== R_PPC_ADDR32,则我们有S + A,并且字长为word32,因此我们将获得:

*(uint32_t *)(to_modify + rel->r_offset) = address_of_printf + rel->r_addend;

...我们已经成功地完成了搬迁。

当涉及到#lo、#hi等以及像low14这样的字大小时,我无法帮助您。我对PPC一无所知,但链接的PDF似乎足够合理。

我也不知道存根函数。在链接时(至少在动态链接时),通常不需要了解这些内容。

我不确定我是否回答了您所有的问题,但至少您现在应该能够看出您的示例代码是做什么的了。


2

仅回答问题:什么是重定位?

当您编写汇编程序时,如果它是位置相关的,则假定该程序将加载到内存的特定部分。当您将程序加载到另一个内存地址时,那么所有相对于内存的寻址数据都必须更新,以便从新的内存区域正确加载。

例如:

load A,B

有时B可能是地址,有时它只是纯常量数据。因此,只有编译器在生成汇编代码时才知道。编译器将构建包含偏移量及其原始加载相对地址(假定它被加载到固定全局地址)的所有条目的表。这称为重定位表。

它与符号表、PLT表、GOT等相关联:

enter image description here

http://blog.k3170makan.com/2018/10/introduction-to-elf-format-part-vi_18.html

https://reverseengineering.stackexchange.com/questions/1992/what-is-plt-got

什么是重定位的概念?

C++链接实践是如何工作的?


0

尝试查看ELF规范。它大约有60页,并且极大地澄清了事情。特别是第2部分,关于链接的部分。


或者更好的是,System V ABI(通用gABI和体系结构特定的补充)。我发现以下链接很有用:https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI(适用于x86 / x86-64)和http://www.sco.com/developers/gabi/。 - anon

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