尝试了解 ARM 二进制映像中的加载内存地址 (LMA) 和二进制文件偏移量

Trying to understand the load memory address (LMA) and the binary file offset in an ARM binary image

我在 ARM Cortex M4 (STM32F4xxxx) 中工作,我试图了解二进制文件 (*.elf*.bin) 是在内存中构建和闪存的,特别是关于内存位置。具体来说,我不明白的是 LMA 如何从实际的二进制文件偏移量中获取 'translated' 。让我用一个例子来解释:

我有一个 *.elf 文件,其(相关)部分如下:(从 objdump -h 获得)

my_file.elf:     file format elf32-littlearm

Sections:
Idx Name              Size      VMA       LMA       File off  Algn
  0 .text             000001c4  08010000  08010000  00020000  2**0
                      CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .bootloader       00004000  08000000  08000000  00010000  2**0
                      CONTENTS, ALLOC, LOAD, DATA

根据该文件,VMA 和 LMA 是 0x80000000x8010000,这很好,因为它们是在链接描述文件中以这种方式定义的。此外,根据该报告,这些部分的偏移量分别为 0x100000x20000。接下来,我执行以下命令来转储对应于 .bootloader 的内存:

xxd -s 0x10000 -l 16 my_file.elf
00010000: b007 c0de b007 c0de b007 c0de b007 c0de  ................ 

现在,创建要闪存到内存中的二进制文件:

arm-none-eabi-objcopy -O binary --gap-fill 0xFF -S my_file.elf my_file.bin 

根据上面提供的信息,据我了解,生成的二进制文件应该有 .bootloader 部分位于 0x8000000。我知道这不是它实际工作的方式,因为文件会变得非常大,所以 bootloader 放在文件的开头,所以地址 0x0 (检查两个内存块是相同的,即使它们位于不同的地址):

xxd -s 0x00000 -l 16 my_file.bin
00000000: b007 c0de b007 c0de b007 c0de b007 c0de  ................

据我所知,当提到的二进制文件闪入内存时,bootloader 将位于地址 0x0,考虑到有问题的 MCU 跳转,这完全没问题当它开始工作时到地址 0x4(从 0x0 获取 SP 后),正如我在此处检查的那样(第 26 页):https://www.st.com/content/ccc/resource/technical/document/application_note/76/f9/c8/10/8a/33/4b/f0/DM00115714.pdf/files/DM00115714.pdf/jcr:content/translations/en.DM00115714.pdf

最后,我的问题是:

bootloader 真的会放在 0x0 吗?如果是这样,在链接器文件中定义内存扇区的目的是什么?

是不是因为0x0属于flash memory,MCU启动时将flash全部copy到地址0x8000000RAM?如果是这样,bootloader 是否会从闪存中执行,而所有其余代码会从 RAM 中执行?

考虑到上面的问题,如果我什么都不懂,LMAFile offset之间的relation/difference是什么?

不,引导加载程序将位于 08000000,如 elf 文件中所定义。

映像将被烧录到该地址的闪存中并直接从那里执行(不会复制到其他地方)。

有些未记录的行为,在生成二进制图像时跳过实际数据之前的单元化区域。正如 BFDlib 源代码中的注释所述 (https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;a=blob;f=bfd/binary.c;h=37f5f9f7363e7349612cdfc8bc579369bbabbc0c;hb=HEAD#l238)

/* The lowest section LMA sets the virtual address of the start
   of the file.  We use this to set the file position of all the
   sections.  */

最低部分 (.bootloader) LMA 在您的 .elf 中是 08000000,因此二进制文件将从该地址开始。
在确定图像中的地址时,应考虑此地址并将其添加到文件偏移量中。

Sections:
Idx Name              Size      VMA       LMA       File off  Algn
  0 .text             000001c4  08010000  08010000  00020000  2**0
    /*                                    ^^^^^^^^              */
    /* this section will be at offset 10000 in image            */

                      CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .bootloader       00004000  08000000  08000000  00010000  2**0
    /*                                    ^^^^^^^^              */
    /* this is the lowest LMA in your case it will be used      */
    /* as a start of an image, and this section will be placed  */
    /* directly at start of the image                           */
                      CONTENTS, ALLOC, LOAD, DATA

Memory layout:     Bin. image layout:
000000000                    \ skipped
...       ________________   /
080000000 .bootloader         0
...       ________________
080004000   <gap>          4000
...       ________________
080010000 .text           10000
...       ________________
0800101C4                 101C4

ldscript 中定义的地址,因此二进制图像应该从固定位置开始。但是,在处理 ldscrips 和二进制图像时,您应该注意这种行为。

总结构建和烧写过程:

  1. 链接时,起始地址在 ldscript 中定义,elf 中的第一部分位于此处。
  2. 转换为二进制时,起始地址由 LMA 确定,二进制图像从该地址开始。
  3. 当刷入图像时,给flasher的相同地址作为参数,所以图像被放置在正确的位置(在ldscript中定义)。

更新:STM32F4xxx启动过程。

从地址 0 开始的地址区域对于那些 MCU 来说是特殊的。它可以配置为映射其他区域,即闪存、SRAM 或系统 ROM。它们由引脚 BOOTSELx 选择。 从 CPU 一侧看,闪存(SRAM 或系统 ROM)的第二个副本出现在地址 0。

当CPU启动时,首先从地址0读取初始SP,从地址4读取初始PC。实际上是从闪存中读取。 如果代码从实际闪存位置链接到 运行,则初始 PC 将指向那里。在这种情况下,执行从实际闪存地址开始。

----- Mapped area (mimics contents as flash) ---
       0:          (02001000)         ;
       4:          (0800ABCD) ----.   ; CPU reads PC here
....                              |   ; (it points to flash)
----- FLASH -----                 |
 8000000:           20001000      |   ; initial stack pointer
 8000004:           0800ABCD --.  |   ; address of _start in flash
....                           |  |   
 800ABCD: <_start:> movw ... <-'<-'   ; Code execution starts here

(注意:这不适用于十六进制图像(如 intel hex 或 s-record),因为此类格式明确定义了加载地址,并且按原样使用)。

文档非常清楚地址 space 是 stm32 应用程序代码的位置,即 0x08000000(竞争供应商如 0x01000000,等等)。并且当以某种模式启动时,0x08000000 被映射到地址 0x00000000,这可以通过调试器轻松看到(在两个 spaces 中)。

映射到 0x08000000 的 0x00000000 处的地址 space 小于 0x08000000 处的潜在地址 space,具体取决于芯片。因此,构建和使用 0x08000000 而不是 0x00000000 是明智的,但对于小程序,您可以选择其中之一。

因为 cortex-m 是向量 table 机器,当逻辑读取地址 0x00000004,在正常启动模式下映射到 0x08000004,它会看到 0x080xxxxx,然后离开 0x00000000 内存 space, 避免任何限制。

当你使用 boot0/boot1 带引脚时,你可以改为将 0x00000000 映射到烧入引导加载程序所在的其他地方。该引导加载程序当然可以轻松读取 0x08000000 并通过分支轻松模拟重置,或者它可以更改逻辑并实际重置(如果您要求,虽然我不知道该引导加载程序是否真的支持 运行ning 程序)。谁知道我们是否在那里工作我们不一定会说。很有可能它总是引导到引导加载程序,然后根据表带更改映射。

类似于 mmu,但解码地址和别名要简单得多。如果 boot0 == 0 和 address[31:16] = 0x0000 那么 address[31:16]=0x0800 并且内存系统在不同的地址对其进行解码,就像用 C 语言编写一样容易,在 HDL 中也很容易如果不是更容易。

这在微控制器和其他微控制器中并不罕见,但由于微控制器通常从 flash/rom 启动,但在某些架构上相同的启动 space 也是向量或异常 table 一个 rtos 可能想要操作 有时你会看到 ram 可以交换到那个 space 所以 cpu "sees" 一些 ram 在控制寄存器被更改后在引导它的位置"saw" 闪存上的矢量 table。那个或者你有闪存分支上的代码到 ram 中的某个地方用于非重置向量然后 rtos 或任何其他关心这样做的应用程序可以对实际获得的代码进行 运行time 更改 运行 对于那些异常或中断。

ARM 对代码可以执行的位置和数据可以存在的位置以及您可能希望从哪里开始外设地址 space 以及 arm 保留的地址 space 施加了地址 space 规则用于核心内的资源。所以你有时会看到 ram 在较低地址处有一个别名,这意味着如果你想 运行 ram 中的程序你想使用较低地址执行但可以使用任一地址将代码复制到那里。

由芯片设计人员决定制作它的简单程度或复杂程度。对于 ST,它非常简单,然后在封装上有一个或多个引导引脚,至少可以让您在应用程序和片上引导加载程序之间进行选择,到目前为止,我看到的所有 stm32 应用程序闪存 space 都被认为是活的在 0x08000000 并且是 mapped/aliased 到 0x00000000 对于其中一种启动模式。当有两个引导引脚暴露时,最多可以存在四种可能的引导条件,其中一种是 0x00000000 别名为 0x08000000 的应用程序。

至于如何将位放入闪存,因工具而异。像 gnu 这样的工具链肯定会构建一个 .bin 文件,其中文件的第一个字节是我们希望在 0x08000000 处拥有的精灵的第一个字节(如果以这种方式构建,如果您为 0x02000000 构建它仍然是第一个字节,并且该代码可能无法正常工作)。有一些工具,您当然可以编写自己的工具,知道可以在 0x08000000 的所需位置加载 .bin 文件,或者您也可以在正确的模式下写入地址 0x00000000,以适应不太大且具有它仍然落在正确的位置以在重置时执行。同样,有或者您可以编写可以解析 .elf 文件、intel hex、motorola srecord 等的工具,并根据这些二进制文件中的信息将数据加载到您想要的地址 space,假设一切都是错误免费。

您可能试图将它过于复杂化,它没有任何魔力,工具需要做理智的事情,而理智的事情是从编译器中获取二进制文件并将其放入我们想要的芯片中。我们负责链接器脚本等 bootstrap code/vector table 当然,但如果我们这样做正确,如果设计正确,工具会将这些位放在正确的位置芯片,如果芯片按照文档设计正确,那么它将启动并且 运行.

Will the bootloader actually be placed at 0x0? If so, what's the purpose of defining the memory sectors in the linker file?

理想情况下,您希望您调用的应用程序或引导加载程序位于处理器地址 space 中的地址 0x08000000。在某些引导模式 (boot0/boot1) 中,该地址也被别名为 0x00000000,因此您可以同时在两个位置看到向量 table。如果您没有处于正确的启动模式,那么只有 0x08000000 会显示您的代码。

Is this because 0x0 belongs to flash memory, and when the MCU starts, all the flash is copied into RAM at address 0x8000000? If so, will the bootloader be executed from flash memory and all the rest of the code from RAM?

芯片中的逻辑旨在获取处理器放在其地址总线上的地址,并在应用程序闪存上有多个地址区,如果应用程序闪存是 16Kbyte 闪存,例如它的应用程序闪存不在 0x08000000当你访问 0x08001234 时,它只得到一个从 0x0000 到 0xFFFF 的地址,它实际上将 0x1234 发送到闪存控制器,或者如果闪存控制器知道它应该处理该请求,它就会切断顶部。 0x00000000、0x08000000 是地址 space 的处理器视图,实际情况是高位被解码并将请求路由到它所属的任何人,最终处理程序最终查看低位以确定正在寻址的内容。

就像当你投递一封信时,它有名字和姓氏、街道地址、城市国家邮政编码。一旦到达正确州的正确 post 办公室,街道地址对 post 其他人来说就很重要了。一旦到达正确的房子,通常名字就是最重要的,其余的都可以忽略。这里没有区别。地址的某些部分(通常)变得无关紧要,因为负责检查该地址的逻辑将请求指向正确的一方。

Taking into account the above questions, if I have not understood anything, what's the relation/difference between the LMA and the File offset?

elf 文件格式是通用的,对于微控制器的工作来说太过分了,但得到了很好的支持并且易于使用,为什么不呢。加载内存地址是我们程序员希望该代码相对于处理器世界观存在的位置。从 readelf 的角度来看,文件中的偏移量是 elf 文件中该信息的偏移量,它只是工具放置它的地方,没有其他有趣的关系。或者至少不需要。 Objcopy 会将数据从文件中删除,对于 -O binary,将其放入一种内存映像文件中,其中复制出的最低地址是该文件中的偏移量 0,大小由总地址决定 space对于所有可加载块(除非您使用更多命令行参数)。正如你所暗示的那样,但是如果你考虑一下并且有一个链接器脚本错误,如果你在 0x08000000 处有一条指令,在 0x20000000 处有一个字节的 .data 但没有做 AT > 事情那么你的文件尽管只有三个相关字节的长度为 0x20000001 - 0x08000000 字节。 (在 -O 二进制文件之后)在调试链接描述文件之前不要将 objcopy 放入 make 文件中是个好主意。想象一下,假设一个目标闪存为 0x00000000,内存为 0xE0000000,相当大的 .bin 文件,直到您整理出链接描述文件。