如何在ARMELF文件中找到函数内存映射?
我正在尝试使用 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 的内存位置地址,以便仅在执行此类基本块期间访问?换句话说,函数和它使用的内存地址范围之间是否存在映射?
回答
你在问什么是令人困惑的。首先,为什么任何链接器都会努力随机化事物?也许有一个故意使输出不可重复。但是链接器只是一个程序,通常会按顺序处理命令行上的项目,然后从头到尾处理每个对象……不是随机的。
到目前为止,剩下的事情似乎很简单,只需使用工具即可。您的评论暗示了 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 文件格式有点简单,您可以轻松编写代码来解析它,您不需要库或类似的东西。通过像这样的简单实验,可以轻松了解这些工具的工作原理。
如何获得此函数使用的初始内存?
假设您的意思是假设未重新定位的初始地址。你只是从文件中读出它。简单的。
每个函数都有内存段可以访问还是随机的?
如上所示,您稍后在评论中提到的命令行选项(应该在问题中,您应该编辑问题以确保完整性)确实可以为每个函数创建自定义部分名称。(如果您在两个或多个对象中具有相同的非全局函数名称会发生什么?您可以自己轻松解决这个问题)
这里没有什么是随机的,出于安全或其他原因,您需要有理由将事物随机化,通常更喜欢工具每次使用相同的输入输出相同或至少相似的结果(某些工具会嵌入构建日期/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 等检查。显然反汇编不起作用,但有些工具可以。
如果这不是您要问的问题,那么您需要重新编写问题或对其进行编辑,以便我们了解您实际问的问题。
编辑
我想知道函数和它使用的内存地址范围之间是否有映射?
一般来说没有。术语函数意味着但不限于高级语言,如 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 二进制文件,找到代表内存中“函数”高级概念的字节(假设没有重定位等)。这在理论上是可能的。
我们似乎知道的第一件事是 OBJECT 包含此信息,以便链接器功能可以删除未使用的函数大小。但这并不意味着链接器的输出二进制文件也包含此信息。您需要找到这个 .size 在对象中的位置,然后在最终的二进制文件中查找。
编译器将一种语言转换为另一种语言,通常从较高级别到较低级别,但并不总是取决于编译器和输入/输出语言。C 到汇编或 C 到机器代码或 Verilog 到 C++ 的模拟是更高还是更低?术语 .text、.data、.bss 不是语言的一部分,而是更多基于学习经验的习惯,有助于与链接器进行通信,以便可以更好地控制各种目标的输出二进制文件。通常如上图所示,编译器,在这种情况下是 gcc,因为在这一领域无法在所有工具和语言甚至所有 C 或 C++ 工具中进行概括,源文件中所有函数的所有代码都位于一个 .text 段中默认情况下。你必须做额外的工作才能得到不同的东西。所以编译器一般不会做一个“
只需使用文件格式和/或工具。这个问题或一系列问题归结为只是去看看 elf 文件格式。这不是 Stack Overflow 问题,因为寻求外部信息建议的问题不适合本网站。
例如,编译器是否会保存从 0x20 到 0x1234 的内存位置地址,以便仅在执行此类基本块期间访问?换句话说,函数和它使用的内存地址范围之间是否存在映射?
“节省”?编译器不链接链接器链接。在执行该块期间是否“仅”访问该内存?在纯教科书理论中是的,但实际上分支预测和预取或缓存行填充也可以访问该“内存”。
除非进行自修改代码或以有趣的方式使用 mmu,否则您不会将地址空间重用于应用程序中的多个功能。一般来说。所以函数 foo() 在某处实现, bar() 在其他地方实现。从过去的美好时光开始手写 asm,您可能会将 foo() 分支直接放到 bar() 的中间,以节省空间、获得更好的性能或使代码更难逆向工程或其他什么。但是编译器效率不高,他们尽最大努力将函数等概念首先转换为函数式(相当于高级代码),然后,如果需要,相对于语言之间的直接蛮力转换,更小或更快或两者兼而有之。所以除非内联和尾部(叶?,我称之为尾部)优化等,可以说在某个地址有一定数量的字节定义了一个编译函数。但是由于处理器技术的性质,您不能假设只有在执行该功能时处理器/芯片/系统总线才能访问这些字节。
- old_timer: in general, if you are confused about a question, I would suggest seeking clarifications in the comments first. You can then give an answer from a position of understanding the problem if sufficient clarity can be obtained. Remember that the purpose of this site is to produce Q&A that shall be useful to other readers, not just one.