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

How to find the function memory map in ARM ELF file?

我正在尝试使用 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 的内存位置地址,以便仅在执行此类基本块期间访问?换句话说,函数和它使用的内存地址范围之间是否存在映射?

您问的问题令人困惑。首先,为什么 linker 会努力随机化事物?也许有人故意让输出不重复table。但是 linker 只是一个程序,通常会按顺序处理命令行上的项目,然后从头到尾处理每个对象……不是随机的。

到目前为止,其余部分似乎非常简单,只需使用工具即可。您的评论暗示 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

现在每个函数都有自己的部分名称。

所以其余部分严重依赖 linking 并且没有任何 linker 脚本,程序员直接或间接选择以及如何构建最终二进制文件 (elf) 是直接的该选择的结果。

如果你有这样的东西

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

与这些功能无关,那么它们都将落入此定义中,但 linker 脚本或 linker 的说明可能表明其他原因导致一个或多个降落在自己的 space.

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      <----

这仅仅是因为文件中有一个符号table

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 文件格式有点简单,您可以轻松编写代码来解析它,您不需要库或类似的东西。并且通过像这样的简单实验可以很容易地理解这些工具是如何工作的。

How can I get the initial memory used by this function ?

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

Are there memory segments for every function to access or is it random ?

如上所示,您稍后在评论中提到的命令行选项(应该在问题中,您应该编辑问题以确保完整性)确实为每个函数创建了一个自定义部分名称。 (如果在两个或多个对象中有相同的非全局函数名称会发生​​什么情况?您可以自己轻松解决这个问题)

这里没有什么是随机的,为了安全或其他原因,您需要有理由将事物随机化,通常更喜欢工具每次使用相同的输入输出相同或至少相似的结果(某些工具将在文件中嵌入一个版本 date/time,并且可能因一个版本而异。

如果您不使用 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 等进行检查。显然反汇编是行不通的,但有些工具可以。

如果这不是您要问的问题,那么您需要重写或编辑您的问题,以便我们了解您的实际问题。

编辑

I would like to know if there is a map between a function and the range of memory address used by it ?

一般没有。术语函数暗示但不限于高级语言,如 C 等。机器代码显然没有线索,也不应该有线索,优化好的代码不一定有函数的单一出口点,更不用说 return 标记结束。对于像各种 arm 指令集这样的架构,return 指令不是“函数”的结尾,后面可能有池数据。

但让我们看看 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

一个参考资料说这是为了让 linker 可以删除不使用的功能。我在游戏中看到了这个功能,很高兴知道(你可以像我一样轻松地查找它)

所以在那种情况下,信息就在那里,你可以提取它(reader 的课程)。

然后如果你使用你提到的这个 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 一起具有此功能,您可以查看它是否与其他语言或工具一起使用。假设答案是否定的,那么如果你恰好找到了方法,那就好了。

Does the compiler saves a memory segment to be only accessed by a certain function ?

我不知道这是什么意思。编译器不会像 linker 那样创建内存段。如何将二进制文件放入内存映像是 linker 的事情,而不是初学者的编译器事情。段只是工具之间通信的一种方式,这些字节用于起始代码(理想情况下只读)、初始化数据或未初始化数据。也许扩展到只读数据,然后组成你自己的类型。

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

我们似乎知道的第一件事是 OBJECT 包含此信息,因此 linker 功能可以删除未使用的函数以减小大小。但这并不意味着 linker 的输出二进制文件也包含此信息。您需要找到此 .size 在对象中的位置,然后在最终的二进制文件中查找它。

编译器将一种语言转换为另一种语言,通常是从较高级别到较低级别,但并不总是取决于编译器和 input/output 语言。 C 到汇编或 C 到机器代码,或者 Verilog 到 C++ 的模拟是更高还是更低?术语 .text、.data、.bss 不是语言的一部分,而是更多的基于学习经验的习惯,如前所述有助于与 linker 交流,以便可以针对各种目标更好地控制输出二进制文件.通常如编译器上方所示,在这种情况下为 gcc,因为在这一领域不能对所有工具和语言甚至所有 C 或 C++ 工具进行概括,源文件中所有函数的所有代码都位于一个 .text 段中默认。你必须做额外的工作才能得到不同的东西。因此,编译器通常不会为每个……创建一个“段”或“内存段”。您似乎已经解决了您的问题,方法是使用命令行选项将每个函数转换为它自己的段,现在您可以更好地控制大小和位置等。

只需使用文件格式 and/or 工具。这道题或者说系列题归结起来就是去看看elf文件格式。这不是 Stack Overflow 问题,因为寻求外部信息建议的问题不适用于此站点。


Does the compiler for example save a memory location address from 0x20 to 0x1234 to be only accessed during the execution of such basic block ? In another word, Is there a map between a function and the range of memory address used by it ?

“保存”?编译器不会 link link 或者 link。该内存是否“仅”在该块的执行期间访问?好吧,在纯教科书理论中是的,但实际上分支预测和预取或缓存行填充也可以访问该“内存”。

除非进行自我修改代码或以有趣的方式使用 mmu,否则您不会为应用程序中的多个函数重复使用地址 space。一般来说。所以函数 foo() 在某处实现,而 bar() 在其他地方实现。从过去的美好时光手写的 asm 你可能有 foo() 分支到 bar() 的中间以节省 space,获得更好的性能或使代码更难逆向工程或其他什么。但是编译器效率不高,他们会尽力将函数之类的概念首先转换为函数式(相当于高级代码),然后,如果需要,相对于语言之间的直接蛮力转换更小或更快或两者兼而有之。因此,除了内联和尾部(叶?,我称之为尾部)优化等,可以说在某个地址处有一些字节数定义了一个编译函数。但是由于处理器技术的性质,您不能假设这些字节仅在执行该功能时才由 processor/chip/system 总线访问。