【问题标题】:How to get an entry point address in binary image file generated by objcopy?如何在objcopy生成的二进制图像文件中获取入口点地址?
【发布时间】:2020-12-08 15:22:22
【问题描述】:

我正在为自己的教育目的构建 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

从精灵目标生成二值图像:

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)。 文件的其余部分用零填充。

我卡住的地方我想不通:

  1. 我应该在哪个偏移量处将此 bin 映像文件加载到我的虚拟 CPU 的内存中?我不认为在 0x0 地址加载它是可以的,因为图像的开头有有用的信息,我认为程序从地址 0x0 读取它并不酷。
  2. 程序加载后,我需要将CPU执行转移到我程序的入口点(_start)。如何找出哪个地址是入口点,以便在启动 CPU 周期之前将此地址放入pc 寄存器?它显然不在图像的开头(那里有人类可读的字符串)。
  3. 有没有办法使这个入口点地址稳定,所以我编写的所有程序都将具有相同的入口点地址,这样我就不必对我编译的每个程序进行调整?

当我使用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

我知道程序中缺少一些重要的东西,比如初始化堆栈指针寄存器。我打算在弄清楚加载逻辑后处理这个问题

【问题讨论】:

  • 关于(3),我不这么认为,因为一开始有一个静态内存,其中包含您的程序使用的所有静态数据(正如您提到的 - 字符串文字,以及作为一些额外的)
  • 您能否用链接描述文件的确切内容来扩充您的问题?
  • 只是一些想法:我会尝试使用 objdump 从 ELF 文件中获取信息。纯二进制文件没有任何元数据。 -- 如果你有链接描述文件,你可以尝试复制和编辑它以获得一个固定的入口地址。 -- 为了更深入的检查,您可以比较 ELF 和二进制文件的反汇编。
  • 弗兰特,我在他的问题中添加了链接器参数,我希望这就是你所要求的。它由 rustc 编译器自动生成。
  • 忙碌的蜜蜂,我用objdump -dobjdump -f 做了一些实验。它告诉我 start address 0x000111d8 并且与 objdump 反汇编 _start 函数位置相匹配。问题是这个地址不在我的由objcopy(大约5kb)生成的二进制图像之外。 objcopy 文档说“内存转储将从复制到输出文件的最低部分的加载地址开始。” - 我怎样才能知道“复制到输出文件的最低部分”的地址是什么?我认为这会告诉我在哪个偏移量处加载我的二进制文件。

标签: rust embedded riscv objcopy


【解决方案1】:

免责声明:我对 Rust 不熟悉,但您的问题更多与 ELF 文件格式和可以理解它的工具有关——我的两分钱。

  1. 您应该将二进制文件加载到哪个偏移量应该由 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 处加载。

您正在使用的工具链应该有一个等价物。

  1. 您可以使用了解 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 0000000042000000my-emulator -s 0000000042000000

b) 由于您掌握了模拟器将加载的图像格式,您可以召集系统地将起始地址添加到 objcopy 生成的二进制文件中:这样,您将读取二进制文件的前 4 或 8 个字节首先文件,获取你的起始地址,然后读取剩余的字节。

一个简单的方法是使用xxdcat

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

您当然可以定义一个更复杂的标题,可能包含一些其他重要的符号,或者为您的模拟器添加更多命令行选项 - 基本原理保持不变。

  1. 是的,您可能会强制编译器将_start() 函数放入特定的链接器部分,如here 所述,使用link_section 指令/pragma:

程序:

#[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 部分。

我希望我没有偏离那个轨道......

【讨论】:

  • 弗兰特,我要感谢您的详细解释。我遵循了前半部分,并且能够生成一个似乎在 .bin 文件开头有入口点的二进制文件,我假设如果我用偏移量加载它,你建议它也会给我正确的入口点!您链接的 riscv_rt crate 似乎对对齐 ELF 文件中的内容具有必要的魔力,然后我可以使用您的脚本使用 nm 提取偏移量。我不完全理解 riscv_rt 正在做什么来实现这一点,并且必须了解更多:)
  • 不客气。忘了提一下,如果您的模拟器将在小端系统上运行,则必须将二进制文件前面的起始地址转换为小端,这应该是这种情况。使用像 bswap_64()bswap_32() 这样的函数就可以了。
猜你喜欢
  • 1970-01-01
  • 2013-10-01
  • 1970-01-01
  • 2021-12-04
  • 2019-11-25
  • 2011-07-11
  • 1970-01-01
  • 2020-04-10
  • 1970-01-01
相关资源
最近更新 更多