如何在ARM ELF文件中查找函数内存映射?

3

我正在使用Z3分析ARMV7m代码的循环边界,这是一个大型框架。 我想找到在.elf文件中由某个函数使用的内存地址, 例如,在函数foo()中,我有以下基本块:

ldr     r1, [r3, #0x20]
strb    r2, [r3, #6]  {__elf_header}
str     r2, [r3, #0x24]  {__elf_header}
str     r2, [r3, #0x20]  {__elf_header}
mov     r3, r1
cmp     r1, #0
bne     #0x89f6

如何获取此函数 [r3, #0x20] 使用的初始内存地址?每个函数是否都有可以访问的内存段,还是随机的? 给定上述基本块是一个循环。是否有办法在其执行期间知道将使用哪个内存地址? 例如,编译器是否保存从 0x20 到 0x1234 的内存位置地址,以便仅在执行此基本块时访问?换句话说,函数和所用内存地址范围之间是否有映射关系?

1
ELF 文件是否被完全重定位?是否涉及加载器?你的操作系统是否支持地址随机化,是否使用特殊标志进行编译,例如 -ffunction-section?有很多不太清楚的地方! - Klaus
.elf文件是使用-ffunction-sections标志为arm-none-eabi编译的裸机程序。 - Hazem Abaza
1个回答

5

您的问题让人感到困惑。首先,为什么任何链接器都要把精力放在随机化上?也许有一个链接器是故意做出输出不可重复的结果。但是,链接器只是一个程序,通常会按顺序处理命令行中的项目,然后从开始到结束处理每个对象...而不是随机处理。

到目前为止,其他部分似乎都很简单,只需使用工具即可。您的评论暗示了GNU工具?由于这在某种程度上是特定于工具的,因此您应该标记它,因为您不能真正在所有曾经创建的工具链之间进行概括。

unsigned int one ( void )
{
    return(1);
}
unsigned int two ( void )
{
    return(2);
}
unsigned int three ( void )
{
    return(3);
}

arm-none-eabi-gcc -O2 -c so.c -o so.o
arm-none-eabi-objdump -d so.o

so.o:     file format elf32-littlearm


Disassembly of section .text:

00000000 <one>:
   0:   e3a00001    mov r0, #1
   4:   e12fff1e    bx  lr

00000008 <two>:
   8:   e3a00002    mov r0, #2
   c:   e12fff1e    bx  lr

00000010 <three>:
  10:   e3a00003    mov r0, #3
  14:   e12fff1e    bx  lr

如图所示,它们都在.text中,非常简单。

arm-none-eabi-gcc -O2 -c -ffunction-sections so.c -o so.o
arm-none-eabi-objdump -d so.o

so.o:     file format elf32-littlearm


Disassembly of section .text.one:

00000000 <one>:
   0:   e3a00001    mov r0, #1
   4:   e12fff1e    bx  lr

Disassembly of section .text.two:

00000000 <two>:
   0:   e3a00002    mov r0, #2
   4:   e12fff1e    bx  lr

Disassembly of section .text.three:

00000000 <three>:
   0:   e3a00003    mov r0, #3
   4:   e12fff1e    bx  lr

现在每个函数都有自己的章节名称。

因此,余下的工作大量依赖链接,没有一个通用的链接脚本,由程序员直接或间接进行选择,最终二进制文件(elf)的构建结果也是这个选择的直接结果。

如果您有类似以下代码:

.text   : { *(.text*)   } > rom

如果这些函数在定义时没有其他约束,则所有这些函数都将归入此定义,但链接器脚本或链接器指令可以指示其他情况,使一个或多个函数归入自身的空间。

arm-none-eabi-ld -Ttext=0x1000 so.o -o so.elf
arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 0000000000001000
arm-none-eabi-objdump -d so.elf

so.elf:     file format elf32-littlearm


Disassembly of section .text:

00001000 <one>:
    1000:   e3a00001    mov r0, #1
    1004:   e12fff1e    bx  lr

00001008 <two>:
    1008:   e3a00002    mov r0, #2
    100c:   e12fff1e    bx  lr

00001010 <three>:
    1010:   e3a00003    mov r0, #3
    1014:   e12fff1e    bx  lr

当然,接下来

arm-none-eabi-nm -a so.elf
00000000 n .ARM.attributes
00011018 T __bss_end__
00011018 T _bss_end__
00011018 T __bss_start
00011018 T __bss_start__
00000000 n .comment
00011018 T __data_start
00011018 T _edata
00011018 T _end
00011018 T __end__
00011018 ? .noinit
00001000 T one      <----
00000000 a so.c
00080000 T _stack
         U _start
00001000 t .text
00001010 T three    <----
00001008 T two      <----

这是因为文件中有符号表。
Symbol table '.symtab' contains 22 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00001000     0 SECTION LOCAL  DEFAULT    1 
     2: 00000000     0 SECTION LOCAL  DEFAULT    2 
     3: 00000000     0 SECTION LOCAL  DEFAULT    3 
     4: 00011018     0 SECTION LOCAL  DEFAULT    4 
     5: 00000000     0 FILE    LOCAL  DEFAULT  ABS so.c
     6: 00001000     0 NOTYPE  LOCAL  DEFAULT    1 $a
     7: 00001008     0 NOTYPE  LOCAL  DEFAULT    1 $a
     8: 00001010     0 NOTYPE  LOCAL  DEFAULT    1 $a
     9: 00001008     8 FUNC    GLOBAL DEFAULT    1 two
    10: 00011018     0 NOTYPE  GLOBAL DEFAULT    1 _bss_end__
    11: 00011018     0 NOTYPE  GLOBAL DEFAULT    1 __bss_start__
    12: 00011018     0 NOTYPE  GLOBAL DEFAULT    1 __bss_end__
    13: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND _start
    14: 00011018     0 NOTYPE  GLOBAL DEFAULT    1 __bss_start
    15: 00011018     0 NOTYPE  GLOBAL DEFAULT    1 __end__
    16: 00001000     8 FUNC    GLOBAL DEFAULT    1 one
    17: 00011018     0 NOTYPE  GLOBAL DEFAULT    1 _edata
    18: 00011018     0 NOTYPE  GLOBAL DEFAULT    1 _end
    19: 00080000     0 NOTYPE  GLOBAL DEFAULT    1 _stack
    20: 00001010     8 FUNC    GLOBAL DEFAULT    1 three
    21: 00011018     0 NOTYPE  GLOBAL DEFAULT    1 __data_start

但是如果。
arm-none-eabi-strip so.elf
arm-none-eabi-nm -a so.elf
arm-none-eabi-nm: so.elf: no symbols
arm-none-eabi-objdump -d so.elf

so.elf:     file format elf32-littlearm


Disassembly of section .text:

00001000 <.text>:
    1000:   e3a00001    mov r0, #1
    1004:   e12fff1e    bx  lr
    1008:   e3a00002    mov r0, #2
    100c:   e12fff1e    bx  lr
    1010:   e3a00003    mov r0, #3
    1014:   e12fff1e    bx  lr

ELF文件格式相当简单,您可以轻松编写代码来解析它,不需要使用库或其他任何东西。通过像这样的简单实验,您可以轻松理解这些工具的工作方式。

如何获得此函数使用的初始内存?

假设您指的是未重定位的初始地址。您只需从文件中读取即可。简单吧。

每个函数都有一个内存段以供访问,还是随机的?

如上所示,您在评论中稍后提到的命令行选项(应该在问题中进行编辑以完整),正是每个函数创建自定义节名称的方式。(如果您在两个或多个对象中具有相同的非全局函数名称,会发生什么情况?您可以轻松地自行弄清楚)

这里没有任何随机性,您需要有一个安全或其他原因来随机化事物,更经常地,希望工具每次使用相同的输入输出相同或至少类似的结果(某些工具将构建日期/时间嵌入文件中,这可能会因每次构建而异)。

如果您没有使用gnu工具,则binutils仍然非常有用,可以对解析和显示ELF文件进行处理。

arm-none-eabi-nm so.elf
00011018 T __bss_end__
00011018 T _bss_end__
00011018 T __bss_start
00011018 T __bss_start__
00011018 T __data_start
00011018 T _edata
00011018 T _end
00011018 T __end__
00001000 T one
00080000 T _stack
         U _start
00001010 T three
00001008 T two

nm so.elf (x86 binutils not arm)
00001000 t $a
00001008 t $a
00001010 t $a
00011018 T __bss_end__
00011018 T _bss_end__
00011018 T __bss_start
00011018 T __bss_start__
00011018 T __data_start
00011018 T _edata
00011018 T _end
00011018 T __end__
00001000 T one
00080000 T _stack
         U _start
00001010 T three
00001008 T two

可以用clang进行构建,然后使用gnu等工具进行分析。显然,反汇编不起作用,但是一些工具可以使用。
通常情况下是不行的。术语“函数”意味着但不限于高级语言如C等,在这种情况下,机器代码显然不知道也不应该知道所使用的内存地址范围,而且经过优化的代码不一定有一个单独的从函数退出的点,更不用说标记结束的返回了。对于像各种ARM指令集这样的体系结构,返回指令并不代表“函数”的结束,可能还会跟随一些池数据。
但是让我们来看看gcc的处理方式。
unsigned int one ( unsigned int x )
{
    return(x+1);
}
unsigned int two ( void )
{
    return(one(2));
}
unsigned int three ( void )
{
    return(3);
}

arm-none-eabi-gcc -O2 -S so.c 
cat so.s
    .cpu arm7tdmi
    .eabi_attribute 20, 1
    .eabi_attribute 21, 1
    .eabi_attribute 23, 3
    .eabi_attribute 24, 1
    .eabi_attribute 25, 1
    .eabi_attribute 26, 1
    .eabi_attribute 30, 2
    .eabi_attribute 34, 0
    .eabi_attribute 18, 4
    .file   "so.c"
    .text
    .align  2
    .global one
    .arch armv4t
    .syntax unified
    .arm
    .fpu softvfp
    .type   one, %function
one:
    @ Function supports interworking.
    @ args = 0, pretend = 0, frame = 0
    @ frame_needed = 0, uses_anonymous_args = 0
    @ link register save eliminated.
    add r0, r0, #1
    bx  lr
    .size   one, .-one
    .align  2
    .global two
    .syntax unified
    .arm
    .fpu softvfp
    .type   two, %function
two:
    @ Function supports interworking.
    @ args = 0, pretend = 0, frame = 0
    @ frame_needed = 0, uses_anonymous_args = 0
    @ link register save eliminated.
    mov r0, #3
    bx  lr
    .size   two, .-two
    .align  2
    .global three
    .syntax unified
    .arm
    .fpu softvfp
    .type   three, %function
three:
    @ Function supports interworking.
    @ args = 0, pretend = 0, frame = 0
    @ frame_needed = 0, uses_anonymous_args = 0
    @ link register save eliminated.
    mov r0, #3
    bx  lr
    .size   three, .-three
    .ident  "GCC: (GNU) 10.2.0"

我们看到这被放置在文件中,但它是做什么用的?

    .size   three, .-three

有一种说法是这样使用的,以便链接器可以在函数未被使用时将其删除。我见过这个特性正在发挥作用,所以知道这一点很好(你和我一样可以轻松查找)。

因此,在那种情况下,信息是存在的,你可以提取它(读者应该记住这一点)。

然后,如果你使用了你提到的gcc编译器选项-ffunction-sections

Disassembly of section .text.one:

00000000 <one>:
   0:   e2800001    add r0, r0, #1
   4:   e12fff1e    bx  lr

Disassembly of section .text.two:

00000000 <two>:
   0:   e3a00003    mov r0, #3
   4:   e12fff1e    bx  lr

Disassembly of section .text.three:

00000000 <three>:
   0:   e3a00003    mov r0, #3
   4:   e12fff1e    bx  lr


[ 4] .text.one
       PROGBITS        00000000 000034 000008 00   0   0  4
       [00000006]: ALLOC, EXEC
  [ 5] .rel.text.one
       REL             00000000 0001a4 000008 08  12   4  4
       [00000040]: INFO LINK
  [ 6] .text.two
       PROGBITS        00000000 00003c 000008 00   0   0  4
       [00000006]: ALLOC, EXEC
  [ 7] .rel.text.two
       REL             00000000 0001ac 000008 08  12   6  4
       [00000040]: INFO LINK
  [ 8] .text.three
       PROGBITS        00000000 000044 000008 00   0   0  4
       [00000006]: ALLOC, EXEC
  [ 9] .rel.text.three
       REL             00000000 0001b4 000008 08  12   8  4
       [00000040]: INFO LINK

这给我们提供了每个部分的大小。

一般来说,就编译软件而言,或者特别是装配软件,假设一个函数没有边界。如上所述,一个函数被内联到另一个函数中,在另一个函数中内联的函数有多大?在二进制文件中有多少个函数实例?你想监视哪一个函数并知道其大小,性能等?Gnu在gcc中具有这个功能,您可以使用其他语言或工具查看是否存在。假设答案是否定的,然后如果您碰巧找到了一种方法,那就很好。

编译器是否将某个内存段保存为只能由某个特定函数访问?

我不知道这是什么意思。编译器并不会创建内存段,链接器会。如何将二进制文件放入内存映像是链接器的事情,而不是编译器的事情。段只是一种沟通方式,在工具之间传递信息,用于指明这些字节最初是代码(理想情况下为只读),已初始化数据或未初始化数据。也许可以扩展到只读数据,然后自己构建类型。

如果您的终极目标是通过使用gnu工具链查看elf二进制文件来找到表示内存中“函数”高级概念的字节(假设没有重定位等),那么这在理论上是可能的。

我们似乎首先要知道的是,对象包含此信息,以便链接器功能可以删除未使用的函数以减小大小。但是,这并不自动意味着链接器的输出二进制文件也包括此信息。您需要找到此 .size 在对象中的位置,然后在最终的二进制文件中查找。

编译器将一种语言转换为另一种语言,通常从更高级别到更低级别,但并非始终如此,这取决于编译器和输入/输出语言。例如,C到汇编语言或C到机器码,或者Verilog到C ++用于模拟,这是否更高级或更低级?.text、.data、.bss等术语不是语言的一部分,而是基于学习经验的一种习惯,并有助于与链接器沟通,以便针对各种目标控制输出二进制文件。通常情况下,如上所示,编译器(在此情况下是gcc)由于无法在所有工具和语言或甚至所有C或C ++工具中制定普遍规则,因此源文件中所有函数的所有代码都会默认落在一个 .text 段中。如果要得到不同的东西,需要额外的工作。所以通常情况下,编译器不为每个 ...一般情况下制作“段”或“内存段”。您似乎已经通过使用命令行选项将每个函数都变成自己的段来解决了问题,现在您对大小、位置等有更多的控制。

只需使用文件格式和/或工具即可。这个问题或者一系列问题可以简化为查看elf文件格式。这不是堆栈溢出问题,因为寻求外部信息的问题不适用于此网站。


编译器是否将内存位置地址从0x20到0x1234保存,以便仅在执行该基本块期间访问?换句话说,函数与其使用的内存地址范围之间是否存在映射关系?
“保存”?编译器不连接,链接器连接。该内存仅在执行该块时访问吗?在纯理论教科书中是这样的,但在现实中,分支预测、预取或缓存行填充也可以访问该“内存”。
除非使用自修改代码或以有趣的方式使用mmu,否则您不应在应用程序中为多个函数重复使用地址空间。通常情况下,foo()函数和bar()函数都在不同的地方实现。在过去的手写汇编语言中,您可能会使foo()直接跳转到bar()的中间部分以节省空间、获得更好的性能或使代码难以逆向工程等。但编译器并不那么高效,它们尽力将类似函数的概念转化为首先是功能(与高级代码相等),然后是相对于直接在语言之间进行粗暴转换而言更小或更快或两者兼而有之的东西。因此,除了内联和尾部(叶子?我称其为尾部)优化等之外,可以说在某个地址处有一些字节定义了已编译的函数。但由于处理器技术的性质,您不能假设这些字节仅在处理器/芯片/系统总线在执行该功能时访问。

谢谢您的回答,我已经编辑了问题,因为它似乎不太清楚。我想知道是否存在一个函数和其使用的内存地址范围之间的映射关系? 编译器是否保存一个内存段以仅由某个特定函数访问? - Hazem Abaza
这通常是不确定的。 - old_timer
有没有办法让它确定性地运行?或者把它置于控制之下? 例如,一个——>访问(0x20-0x40),两个——>访问(0x60-0x1000) - Hazem Abaza
-ffunction-section 在我看来是解决您问题的关键,而且您已经提到了它,所以我认为您基本上已经回答了自己的问题。如果您不理解我用工具展示的简单事情,无论您需要/想要这些信息的任务是什么,您都还没有准备好。您首先需要理解概念,然后检查文件格式以及它如何匹配加载的内容,然后再考虑如何以尚未讨论的方式隔离那些字节,不是所有处理器(如果有)都能真正“监视”一系列地址获取(外部SIM卡)。 - old_timer
2
老手:通常情况下,如果你对一个问题感到困惑,我建议先在评论中寻求澄清。如果足够明确的话,你就可以从理解问题的角度给出答案。请记住,这个网站的目的是产生对其他读者有用的问答。 - halfer

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