了解 STM32F103C8T6 的链接描述文件

Understanding the linker scripts for STM32F103C8T6

最近开始接触STM32F103C8T6裸机编程,链接描述文件的实现好像有点乱。我在网上找到了两个版本的链接器脚本,令人惊讶的是,尽管它们的内容存在巨大差异,但它们都能按预期工作。

版本 1,已使用 here

SECTIONS
{
    .  = 0x0;         /* From 0x00000000 */
    .text : 
    {
        *(isr_vector) /* Interrupt Service Routine Vector table */
        *(.text)      /* Program code */
    }
}

生成的固件用 flash write_bank 0 add.bin 0

刷新

版本 2,已使用 here

MEMORY {
    FLASH (rw): ORIGIN = 0x8000000, LENGTH = 64K
    RAM (rwx): ORIGIN = 0x20000000, LENGTH = 20K
}

ENTRY(Reset_Handler)
SECTIONS
{   .text : {
        KEEP(* (.isr_vector))
        * (.text*)
    } > FLASH

    __StackTop = ORIGIN(RAM) + LENGTH(RAM);
}

生成的固件用 flash write_image erase main.bin 0x8000000

刷新

如您所见,在链接器脚本和用于刷新固件的 OpenOCD 命令中,版本 1 将 .text 刷新为 0x00000000,而版本 2 将刷新为 0x8000000。首先,我不确定这些地址指的是什么:它们是 LMA 还是 VMA?其次,为什么刷入不同地址效果一样?

我做了一些研究,但是 Programming manual 似乎没有解决我的困惑。

处理器启动寻找矢量 table,地址为 0x00000000,地址为 space。 ST 已经实现了他们的部分,因此应用程序闪存位于 arm 地址 space.

中的地址 0x08000000

根据引导模式,如果您将内置引导加载程序镜像到地址 0x00000000 或应用程序,ST 可以。这样 arm 对地址 0x00000000 的访问将 return 具有在闪存中找到的值。如果镜像应用程序,则 0x00000000 和 0x08000000 将从同一物理闪存设备读取值,并 return 它们。这里没有魔法,ARM 总线有一个地址和它要读取的数据量,逻辑有掩码和匹配来确定它是哪个地址 space,那么如果 0x00000000 到某个千字节数,那么如果strap 是一种从一个 flash bank 读取的方式,另一种方式是从另一个 flash bank 读取。写入也是如此。

在部件的参考手册中搜索 BOOT0。

理想情况下,您希望 link 用于 0x08000000(或 0x00200000 用于某些部分但不是全部),以便向量 table 条目在 0x00000004、0x00000008 处读取,依此类推 return 0x0800xxxx 地址,这样只有向量 table 实际上是从 0x00000000 读取的,然后是应用程序地址 space 中的程序的其余部分。您会在文档中看到,对于具有大量闪存的部件,0x00000000 地址 space 不支持闪存的整个大小,因此如果您 link 0x00000000

现在这些 linker 脚本很有趣,首先,如果你在向量中看到这个 table for a cortex-m

.word       _start + 1
.word       _nmi_handler + 1
.word       _hard_fault + 1

再找一个例子。

.thumb

.section isr_vector
.word 0x20001000
.word one
.word two

.text
.thumb_func
one:
    b one
.thumb_func
two:
    b two
.thumb_func

第一个 linker 脚本

Disassembly of section .text:

00000000 <one-0xc>:
   0:   20001000    andcs   r1, r0, r0
   4:   0000000d    andeq   r0, r0, sp
   8:   0000000f    andeq   r0, r0, pc

0000000c <one>:
   c:   e7fe        b.n c <one>

0000000e <two>:
   e:   e7fe        b.n e <two>

在这种情况下,当 arm 读取向量 table 以在地址 0x00000004 处找到复位向量时,ST 部分将 return 应用闪存第二个字中的值(认为地址 0x08000004)

在这种情况下,它找到 0000000d,这意味着从地址 0x0000000c 开始获取指令。

第二个 linker 脚本

.thumb

.section .isr_vector
.globl _isr_vector
_isr_vector:
.word 0x20001000
.word Reset_Handler

.text
.globl Reset_Handler
.thumb_func
Reset_Handler:
    b .


Disassembly of section .text:

08000000 <_isr_vector>:
 8000000:   20001000    andcs   r1, r0, r0
 8000004:   08000009    stmdaeq r0, {r0, r3}

08000008 <Reset_Handler>:
 8000008:   e7fe        b.n 8000008 <Reset_Handler>

在这种情况下,当 arm 在 0x00000004 处寻找复位向量时,它将得到 0x08000009,这意味着在地址 0x08000008 处获取第一条指令,该地址位于应用程序闪存部分的地址 space 中,这是首选。

你会发现一些 ST 部件在 0x00200000 处有一个小的 window 可以更快地读取一些闪存(它是 ITCM 与 AXIM 的手臂,阅读 cortex-m7 文档)。

通过 msp432,我认为应用程序闪存是 0x01000000 相同的镜像,只是地址不同。

.thumb_func
.align  2
.global Reset_Handler
.type   Reset_Handler, %function
Reset_Handler:

请注意,在第二个示例中,作者有额外的代码,.thumb_func 和 .type 都将该标签标记为函数(它使用 1 进行 ORR 以设置地址的 lsbit,因此您不需要不必拥有丑陋的矢量 table,使用工具)

例如:

.thumb

.section .isr_vector
.globl _isr_vector
_isr_vector:
.word 0x20001000
.word Reset_Handler
.word Something_Else

.text
.globl Reset_Handler
.thumb_func
Reset_Handler:
    b .

.type Something_Else, %function
Something_Else:
    b .
    

Disassembly of section .text:

08000000 <_isr_vector>:
 8000000:   20001000    andcs   r1, r0, r0
 8000004:   0800000d    stmdaeq r0, {r0, r2, r3}
 8000008:   0800000f    stmdaeq r0, {r0, r1, r2, r3}

0800000c <Reset_Handler>:
 800000c:   e7fe        b.n 800000c <Reset_Handler>

0800000e <Something_Else>:
 800000e:   e7fe        b.n 800000e <Something_Else>

都可以,看个人喜好

.thumb_func

干净简单,不需要匹配标签,但它依赖于位置,下一个标签是标记为函数的标签。也没有 .arm_func

.type labelname, %function

适用于 arm 和 thumb 代码它可以说是多打字,你必须匹配标签名称,但这样做有好处,因为你清楚地说明了你想要被识别为函数地址的标签这个习惯对手臂和拇指模式都有效。

两位作者(还是同一个人两次?)都创作了不必要的作品。

考虑这些

so.s

.thumb
.word 0x20001000
.word one
.word two

.thumb_func
one:
    b .

.thumb_func
two:
    b .
    

next.s

add r1,r2,r3
add r2,r3,r4
add r3,r4,r5

so.ld

MEMORY
{
    xyz : ORIGIN = 0x08000000, LENGTH = 0x1000
}
SECTIONS
{
    .text : { *(.text*) } > xyz
}

arm-none-eabi-as so.s -o so.o
arm-none-eabi-as next.s -o next.o
arm-none-eabi-ld -T so.ld so.o next.o -o so.elf
arm-none-eabi-objdump -D so.elf

so.elf:     file format elf32-littlearm


Disassembly of section .text:

08000000 <one-0xc>:
 8000000:   20001000    andcs   r1, r0, r0
 8000004:   0800000d    stmdaeq r0, {r0, r2, r3}
 8000008:   0800000f    stmdaeq r0, {r0, r1, r2, r3}

0800000c <one>:
 800000c:   e7fe        b.n 800000c <one>

0800000e <two>:
 800000e:   e7fe        b.n 800000e <two>
 8000010:   e0821003    add r1, r2, r3
 8000014:   e0832004    add r2, r3, r4
 8000018:   e0843005    add r3, r4, r5

很好,但是如果你

arm-none-eabi-ld -T so.ld next.o so.o -o so.elf
arm-none-eabi-objdump -D so.elf

so.elf:     file format elf32-littlearm


Disassembly of section .text:

08000000 <one-0x18>:
 8000000:   e0821003    add r1, r2, r3
 8000004:   e0832004    add r2, r3, r4
 8000008:   e0843005    add r3, r4, r5
 800000c:   20001000    andcs   r1, r0, r0
 8000010:   08000019    stmdaeq r0, {r0, r3, r4}
 8000014:   0800001b    stmdaeq r0, {r0, r1, r3, r4}

08000018 <one>:
 8000018:   e7fe        b.n 8000018 <one>

0800001a <two>:
 800001a:   e7fe        b.n 800001a <two>

太糟糕了。

如果未在 linker 脚本中调用,则 link 的命令行位置决定文件中 .text 项目的顺序。在提供示例或制作项目时,您不应该有一个 makefile 或构建说明来与代码一起使用吗?您真的需要在 linker 脚本中做额外的工作吗? YMMV.

还要注意 linker 脚本中的内存标签只是连接 MEMORY 和 SECTIONS 之间的点的标签。您可以在限制范围内使用任何您想要的字符串,但有一些例外。

我曾经使用 (rw) 的东西,但它变得有问题不得不重新设计我的少即是多 linker 脚本在两个版本的 binutils 之间。 (在我看来那个例子有一个错误)如果 linker 抱怨缺少部分 (.rodata) 只需添加它。

裸机的美妙之处在于你可以自由选择你想要的方式,除非你使用库然后你必须在他们的沙盒中玩。通常,您会发现过度设计的 linker 脚本和 bootstraps 等试图涵盖各种可能的用例和功能。只做一个涵盖您的用例的。

您可以编写不需要将 .bss 归零或复制 .data 的代码,或者您可以选择支持 .data 但不支持 .bss,或者您可以通过相对简单的方式完全支持两者 linker 脚本和 bootstrap。您拥有此 mcu 上的 space 您真的需要将堆栈设计到 linker 脚本中吗?你知道这部分有多大只是让堆栈从顶部下来。 (当然,如果您使用 linker 魔法,您只需输入一次尺寸)您的选择。如果你出于某种原因想要支持堆(为什么你会在 MCU 上有堆?这不是一件好事)你也可以将其添加到 linker 脚本中。

再次强调裸机的美妙之处,只要它能正常工作,你就可以随心所欲地做任何你想做的事。我建议超越这些人所做的,更好地学习工具(主要是在汇编语言方面),这样您就可以有更多选择。

为什么两者都有效是因为该部分将 0x08000000 臂地址 space 的 some/all 镜像到 0x00000000。所以 link 两者都将在 window.

内工作

你可以选择用什么来标记这些我使用术语arm地址space,我也会使用术语物理地址,如果这些内核中的任何一个都有虚拟内存space,那么很少没有意义。来自 ARM IP 的 ARM 总线(arm 不制造芯片,它制造像 ST 这样的公司购买的核心,然后将他们自己或其他人的逻辑放在周围制造芯片)将发送一个带有这些物理地址之一的读取,如何芯片的响应基于他们的设计方式。向量 table 条目位于已知地址,这意味着逻辑将使用已知地址读取每个项目,并且芯片和程序员需要做出响应,如果存在可以更改地址的 mmu space,如果处理程序逻辑通过 mmu,向量 table 仍然需要位于这些物理地址之一。

所以考虑到多项选择测试我会说逻辑内存地址,LMA。

这里接下来的事情是openocd支持写入0x00000000。 openocd 完全支持闪存写入是一个好处,因为它是特定于芯片的,并且必须有人投入时间。由于这些部件的 0x00000000 的性质以及你如何给它加电(带引脚),你可能会把它绑错,and/or openocd 实现有一个假设或定义总是将 0x00000000 变成 0x08000000 或类似的东西,可能在我最近为您查看的 .cfg 文件本身中,但没有查找此详细信息。