如何解决如何在objcopy生成的二进制图像文件中获取入口点地址?
我出于自己的教育目的而构建了Risk-V CPU仿真器。我的POC工作量很小,想构建示例程序并在模拟器上对其进行测试。
我正在尝试在Rust中构建示例程序,似乎取得了一些不错的进步,但是当我不得不将编译后的程序加载到模拟器的内存中并将CPU执行转移到该程序时,我陷入了困境。
测试程序:
#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[no_mangle]
pub extern "C" fn _start() -> ! {
loop {
for i in 0..1000 {
unsafe {
let r = i as *mut u32;
// This can panic because (500 - i) can be 0
*r = 20000 % (500 - i);
}
}
}
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
内部版本:
$ cargo build --target riscv32i-unknown-none-elf --release
从elf目标生成二进制图像:
riscv32-unknown-linux-gnu-objcopy -g -O binary \
target/riscv32i-unknown-none-elf/release/sample1 \
target/riscv32i-unknown-none-elf/release/sample1.bin
到目前为止,它工作正常,并为我生成了5156字节的二进制文件。
我检查了.bin文件,它对我来说似乎是“合法二进制”。
我在文件的开头发现了一些可读的字符串(例如attempt to calculate the remainder with a divisor of zero
)-看起来它们与处理我在% 0
时可能发生的紧急情况的代码有关。
在文件末尾,我发现了一些看起来像riskv32i指令的内容(由于最低有效位为11
,因此容易注意到它们)。
文件的其余部分填充有零。
我无法确定的地方是卡住的:
- 我应该将此bin映像文件加载到虚拟CPU的内存中的偏移量是多少?我不认为可以将其加载到0x0地址,因为在图像的开头有有用的信息,而且我认为程序从地址0x0读取它并不酷。
- 程序加载后,我需要将CPU执行转移到程序的入口点(
_start
)。如何确定入口地址是哪个地址,以便在启动CPU周期之前将该地址放入pc
寄存器中?显然不是在图像的开头(那里有人类可读的字符串)。 - 是否有办法使此入口点地址稳定,所以我编写的所有程序都将具有相同的入口点地址,因此我不必对我编译的每个程序进行调整?
使用objcopy
时,我可能走错了路。如果是这种情况,请让我知道将ELF文件加载到自制CPU仿真器中的合适方法。
更新:链接器参数(由RUSTFLAGS="-Z print-link-args" cargo build --target riscv32i-unknown-none-elf --release --verbose
提供):
rust-lld \
-flavor \
gnu \
-L \
/home/kris/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/riscv32i-unknown-none-elf/lib \
/mnt/c/src/ws/cpu/sample1/target/riscv32i-unknown-none-elf/release/deps/sample1-4813691a581d1819.sample1.251h7tq6-cgu.0.rcgu.o \
/mnt/c/src/ws/cpu/sample1/target/riscv32i-unknown-none-elf/release/deps/sample1-4813691a581d1819.sample1.251h7tq6-cgu.1.rcgu.o -o \
/mnt/c/src/ws/cpu/sample1/target/riscv32i-unknown-none-elf/release/deps/sample1-4813691a581d1819 \
--gc-sections \
-L \
/mnt/c/src/ws/cpu/sample1/target/riscv32i-unknown-none-elf/release/deps \
-L \
/mnt/c/src/ws/cpu/sample1/target/release/deps \
-L \
/home/kris/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/riscv32i-unknown-none-elf/lib \
-Bstatic \
/home/kris/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/riscv32i-unknown-none-elf/lib/librustc_std_workspace_core-6d1cf467df9db3bb.rlib \
/home/kris/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/riscv32i-unknown-none-elf/lib/libcore-a1a0b4993598bfe4.rlib \
/home/kris/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/riscv32i-unknown-none-elf/lib/libcompiler_builtins-a229bbbccd019775.rlib \
-Bdynamic
我知道程序中缺少一些重要的东西,例如初始化堆栈指针寄存器。我计划在弄清楚加载逻辑之后会对此进行照顾
解决方法
免责声明:我对Rust并不熟悉,但是您的问题与ELF文件格式和可以理解它的工具更多相关-我的两分钱。
- 您应该在哪个偏移量处加载二进制文件,应该遵循rust-ldd使用的链接器设置。
例如,此documentation描述了文件memory.x,该文件定义了链接器使用的内存映射:
MEMORY
{
RAM : ORIGIN = 0x80000000,LENGTH = 16K
FLASH : ORIGIN = 0x20000000,LENGTH = 16M
}
REGION_ALIAS("REGION_TEXT",FLASH);
REGION_ALIAS("REGION_RODATA",FLASH);
REGION_ALIAS("REGION_DATA",RAM);
REGION_ALIAS("REGION_BSS",RAM);
REGION_ALIAS("REGION_HEAP",RAM);
REGION_ALIAS("REGION_STACK",RAM);
在此示例中,生成的二进制文件可能应该以偏移量0x20000000
加载。
您所使用的工具链应该等效。
- 您可以使用了解ELF文件格式的工具来找到
_start
。
例如,为Aarch64编译的我的一个可执行文件上的aarch64-none-elf-nm
将显示:
aarch64-none-elf-nm h5-example.elf
0000000042000078 t $d
0000000042000000 t $x
0000000042000080 t $x
00000000420001dc t $x
00000000420001f4 t $x
0000000042000230 B __bss_end__
0000000042000230 B __bss_start__
0000000042000080 T c_entry
000000004200022c D __copy_table_end__
0000000042000220 D __copy_table_start__
0000000042000230 D __data_end__
0000000042000230 D __data_start__
0000000042000230 ? __end__
0000000042000230 B __etext
0000000042000218 T __exidx_end
0000000042000218 T __exidx_start
0000000042000230 d __fini_array_end
0000000042000230 d __fini_array_start
0000000046000230 ? __HeapLimit
0000000004000000 A __HEAP_SIZE
0000000042000230 d __init_array_end
0000000042000230 d __init_array_start
00000000420001f4 T main
0000000042000000 A __RAM_BASE
000000000e000000 A __RAM_SIZE
0000000042000000 T Reset_Handler
0000000000000000 A __ROM_BASE
0000000000000000 A __ROM_SIZE
000000004c000000 ? __StackLimit
0000000004000000 A __STACK_SIZE
0000000050000000 ? __StackTop
00000000420001dc t system_read_CurrentEL
0000000042000230 B __zero_table_end__
0000000042000230 B __zero_table_start__
在我的情况下,要执行的第一条指令将在Reset_Handler
处执行。
我可以使用以下命令检索引用它的行:
aarch64-none-elf-nm h5-example-02.elf | grep ' Reset_Handler$'
0000000042000000 T Reset_Handler
及其使用十六进制的确切地址:
aarch64-none-elf-nm h5-example-02.elf | grep ' Reset_Handler$' | cut -d ' ' -f1
0000000042000000
RESET_HANDLER=$(aarch64-none-elf-nm h5-example-02.elf | grep ' Reset_Handler$' | cut -d ' ' -f1)
echo ${RESET_HANDLER}
当然会显示:
0000000042000000
现在知道起始地址,在DIY仿真器中将有几个使用它的选项。我想到的两个是:
a)将地址作为参数传递给模拟器,即:
my-emulator 0000000042000000
或my-emulator -s 0000000042000000
b)由于您掌握了仿真器将加载的映像的格式,因此可以召集系统地将起始地址添加到objcopy生成的二进制文件之前:这样,您将读取二进制文件的前4或8个字节文件,先获取您的起始地址,然后读取其余字节。
例如,使用xxd
和cat
的简单方法:
echo 0000000042000000 | xxd -r -p > final-image.bin
cat sample1.bin >> final-image.bin
使用包含“ ABCD”的示例文件,我们将得到:
printf "ABCD" > sample1.bin
hexdump -C sample1.bin
00000000 41 42 43 44 |ABCD|
00000004
echo 0000000042000000 | xxd -r -p > final-image.bin
hexdump -C final-image.bin
00000000 00 00 00 00 42 00 00 00 |....B...|
00000008
cat sample1.bin >> final-image.bin
hexdump -C final-image.bin
00000000 00 00 00 00 42 00 00 00 41 42 43 44 |....B...ABCD|
0000000c
您当然可以定义一个更复杂的标题,可能包含其他一些重要符号,或者为您的模拟器添加更多命令行选项-基本原理将保持不变。
- 是的,您可以使用
_start()
指令/编译指示,强制编译器将link_section
函数放入特定的链接器节,如here所述:
程序:
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
let _x = 42;
// can't return so we go into an infinite loop here
loop {}
}
// The reset vector,a pointer into the reset handler
#[link_section = ".vector_table.reset_vector"]
#[no_mangle]
pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;
链接描述文件:
/* Memory layout of the LM3S6965 microcontroller */
/* 1K = 1 KiBi = 1024 bytes */
MEMORY
{
FLASH : ORIGIN = 0x00000000,LENGTH = 256K
RAM : ORIGIN = 0x20000000,LENGTH = 64K
}
/* The entry point is the reset handler */
ENTRY(Reset);
EXTERN(RESET_VECTOR);
SECTIONS
{
.vector_table ORIGIN(FLASH) :
{
/* First entry: initial Stack Pointer value */
LONG(ORIGIN(RAM) + LENGTH(RAM));
/* Second entry: reset vector */
KEEP(*(.vector_table.reset_vector));
} > FLASH
.text :
{
*(.text .text.*);
} > FLASH
/DISCARD/ :
{
*(.ARM.exidx .ARM.exidx.*);
}
}
这样,_start()
函数的代码将始终放在.vector_table
节的开头,该节被定义为FLASH区域中的第一个。
因此,_start()
的地址将始终为0x00000000
,或者您将决定复位地址的任何地址都将在CPU中:您只需修改FLASH区域开始的地址来自。
该示例与Arm Cortex-M MCU有关,您可以将.vector_table
部分替换为自己的.startup
部分。
我希望我不会偏离那个方向...
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。