如何让gcc在裸机环境下生成堆栈?

How to make gcc generate stack in bare-metal environment?

当我使用 GCC 进行 ARM 操作系统开发时,我无法使用局部变量,因为堆栈未初始化,那么我如何告诉编译器初始化 SP?

我的经验是使用 Cortex-M,正如 @n-pronouns-m 所说,“设置”堆栈的是链接器,而不是编译器或汇编器。所需要做的就是将初始堆栈指针值放在程序存储器中的位置 0x0 处。这通常是(最高 RAM 地址 + 4)。由于不同的处理器具有不同数量的 RAM,因此正确的地址取决于处理器并且通常是链接器文件中的文字。

你的问题很混乱,因为你没有指定目标,对于不同风格的 ARM 架构有不同的答案。但是独立于那个 gcc 与此无关。 Gcc 是一个 C 编译器,因此你需要一个用其他语言编写的 bootstrap 理想情况下(否则它看起来很糟糕而且你正在与鸡和鸡蛋的问题作斗争)。一般用汇编语言完成。

对于 armv4t 到 armv7-a 内核,您有不同的处理器模式、用户、系统、管理程序等。当您查看 Architectural Reference Manual 时,您会看到堆栈指针已存储,每个对应一个模式或至少许多模式都有他们的一加一点共享。这意味着您需要有一种方法来访问该寄存器。对于那些核心,它是如何工作的,你需要切换模式设置堆栈指针切换模式设置堆栈指针,直到你拥有所有你要使用的设置(参见互联网上数万到数十万个示例如何做到这一点)。然后经常回到管理员模式,然后启动到 application/kernel 随便你怎么称呼它。

然后使用 armv8-a,我认为 armv7-a 也有不同的管理程序模式。当然还有 armv8-a,它是 64 位内核(内部有一个 armv7-a 兼容内核,用于 aarch32 执行)。

尽管您需要在代码中设置堆栈指针,但上述所有内容

reset:
    mov sp,=0x8000

或类似的东西。在早期的 Pis 上,这是你可以做的事情,因为加载器会将你的 kernel.img 放在 0x8000 除非另有指示,所以从入口点下方到 ATAG 上方是免费的 space 和启动后,如果你使用 ATAG 条目,那么你可以自由地进入异常 table(你需要设置,最简单的方法是让工具为你工作并生成地址,然后简单地将它们复制到他们的适当的位置。这种东西。

.globl _start
_start:
    ldr pc,reset_handler
    ldr pc,undefined_handler
    ldr pc,swi_handler
    ldr pc,prefetch_handler
    ldr pc,data_handler
    ldr pc,unused_handler
    ldr pc,irq_handler
    ldr pc,fiq_handler
reset_handler:      .word reset
undefined_handler:  .word hang
swi_handler:        .word hang
prefetch_handler:   .word hang
data_handler:       .word hang
unused_handler:     .word hang
irq_handler:        .word irq
fiq_handler:        .word hang

reset:
    mov r0,#0x8000
    mov r1,#0x0000
    ldmia r0!,{r2,r3,r4,r5,r6,r7,r8,r9}
    stmia r1!,{r2,r3,r4,r5,r6,r7,r8,r9}
    ldmia r0!,{r2,r3,r4,r5,r6,r7,r8,r9}
    stmia r1!,{r2,r3,r4,r5,r6,r7,r8,r9}


    ;@ (PSR_IRQ_MODE|PSR_FIQ_DIS|PSR_IRQ_DIS)
    mov r0,#0xD2
    msr cpsr_c,r0
    mov sp,#0x8000

    ;@ (PSR_FIQ_MODE|PSR_FIQ_DIS|PSR_IRQ_DIS)
    mov r0,#0xD1
    msr cpsr_c,r0
    mov sp,#0x4000

    ;@ (PSR_SVC_MODE|PSR_FIQ_DIS|PSR_IRQ_DIS)
    mov r0,#0xD3
    msr cpsr_c,r0
    mov sp,#0x8000000

    ;@ SVC MODE, IRQ ENABLED, FIQ DIS
    ;@mov r0,#0x53
    ;@msr cpsr_c, r0

armv8-m 有一个例外 table 但例外是 space 如 ARM 文档所示。

以上由 ARM 记录的众所周知的地址是一个入口点,代码从那里开始执行,因此您需要将指令放在那里,然后如果它是重置处理程序,通常您将在其中添加代码以设置堆栈指针、复制 .data、零 .bss 和输入 C 代码之前需要的任何其他 bootstrapping。

cortex-ms 是 armv6-m、armv7-m 和 armv8-m(到目前为止与其中一个兼容)使用矢量 table。这意味着众所周知的地址是向量,处理程序的地址,而不是指令,所以你会做这样的事情

.thumb

.globl _start
_start:
.word 0x20001000
.word reset
.word loop
.word loop
.word loop

.thumb_func
reset:
    bl main
    b .
.thumb_func
loop:
    b .

正如 ARM 所记录的那样,cortex-m 向量 table 有一个堆栈指针初始化的入口,因此您不必添加代码,只需将地址放在那里。复位时,逻辑从 0x00000000 读取,将该值放入堆栈指针,从 0x00000004 读取,检查并剥离 lsbit 并在该地址开始执行(lsbit 需要在向量 table 中设置,请不要进行复位+ 1 件事,正确使用工具)。

注意 _start 实际上不是必需的,它只是分散注意力这些 bare-metal 所以没有加载器需要知道入口点是什么,同样你最好自己制作 bootstrap 和 linker 脚本,因此如果您不将它放在 linker 脚本中,则不需要 _start。养成习惯就好了,省得以后有问题了。

当你阅读架构参考手册时,你会注意到 stm/push 指令的描述是如何先递减然后存储,所以如果你设置 0x20001000 那么第一个被压入的是地址0x20000FFC,而不是 0x20001000,non-ARMs 不一定正确,所以总是先获取并阅读文档,然后开始编码。

您 bare-metal 程序员全权负责芯片供应商实施中的内存映射。所以如果从 0x20000000 到 0x20010000 有 64KBytes 的 ram,你决定如何分割它。传统的堆栈从顶部向下,数据在底部,堆在中间是非常容易的,尽管如果你正在谈论的是一个 mcu,为什么你可能会在 mcu 上有一个堆(你做了不指定)。因此,对于 64K 字节的 ram cortex-m,您可能只想将 0x20010000 放入向量 table 的第一个条目中,堆栈指针初始化问题已完成。总的来说,有些人喜欢严重 over-complicate linker 脚本,出于某种我无法理解的原因,在 linker 脚本中定义堆栈。在那种情况下,您只需使用 linker 脚本中定义的变量来指示堆栈顶部,然后在向量 table 中使用它来表示 cortex-m 或 bootstrap全尺寸 ARM 的代码。

另外部分完全负责内存 space 在芯片实现的限制内意味着你设置 linker 脚本来匹配,您需要知道异常或矢量 table 众所周知的地址,如您此时已阅读的文档中所述是吗?

对于 cortex-m 可能是这样的

MEMORY
{
    /* rom : ORIGIN = 0x08000000, LENGTH = 0x1000 *//*AXIM*/
    rom : ORIGIN = 0x00200000, LENGTH = 0x1000 /*ITCM*/
    ram : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
    .text   : { *(.text*)   } > rom
    .rodata : { *(.rodata*) } > rom
    .bss    : { *(.bss*)    } > ram
}

对于 Pi Zero 可能是这样的:

MEMORY
{
    ram : ORIGIN = 0x8000, LENGTH = 0x1000
}
SECTIONS
{
    .text : { *(.text*) } > ram
    .rodata : { *(.rodata*) } > ram
    .bss : { *(.bss*) } > ram
    .data : { *(.data*) } > ram
}

你可以从那里把它复杂化。

堆栈指针是 bootstrap 中最简单的部分,您只需在设计内存映射时选择一个数字即可。初始化 .data 和 .bss 更复杂,尽管对于 |Pi Zero 如果您知道自己在做什么,linker 脚本可以如上,bootstrap 可以如此简单

reset:
    ldr sp,=0x8000
    bl main
hang: b hang

如果不改变模式,不使用argc/argv。你可以从那里把它复杂化。

对于 cortex-m 你可以让它变得更简单

reset:
    bl main
hang: b hang

或者如果您不使用 .data 或 .bss 或者不需要初始化它们,您可以在技术上这样做:

.word 0x20001000
.word main
.word handler
.word handler
...

但除我以外的大多数人都依赖 .bss 为零,.data 被初始化。您也不能从 main return,如果您的软件设计是事件驱动的并且在设置所有内容后不需要前台,这对于像 mcu 这样的 bare-metal 系统来说非常好。大多数人认为您不能 return 来自 main.

gcc 与这些无关,gcc 只是一个编译器,它不能 assemble 它不能 link,它甚至不能编译,gcc 是一个调用其他工具的前端做这些工作一个解析器一个编译器一个 assembler 和一个 linker 除非被告知不要这样做。解析器和编译器是 gcc 的一部分。 assembler 和 linker 是一个名为 binutils 的不同包的一部分,它有许多二进制实用程序,也恰好包括 gnu assembler 或 gas。它还包括 gnu linker。汇编语言特定于 assembler 而不是目标,linker 脚本特定于 linker,内联汇编特定于编译器,因此这些东西不假定为端口从一个工具链到另一个工具链。使用内联汇编通常是不明智的,你必须非常绝望,最好使用真正的汇编,也不要 none,这取决于真正的问题是什么。但是是的,如果你真的觉得有必要,你可以使用 gnu 内联 bootstrap。

如果这是一个 Raspberry Pi 问题,GPU 引导加载程序会为您将 ARM 程序复制到 ram,因此整个过程都在 ram 中,与其他裸机相比,这要容易得多。对于 mcu,虽然逻辑只是使用记录的解决方案启动,但您负责初始化 ram,因此如果您有任何想要初始化的 .data 或 .bss,则必须在 bootstrap 中执行此操作。该信息需要在 non-volatile ram 中,因此您使用 linker 做两件事,一是将此信息放入 non-volatile space (rom/flash) 作为以及告诉它你将把它放在 ram 中的什么地方,如果你使用正确的工具,linker 会告诉你它是否将每个东西都放在 flash/ram 中,然后你可以以编程方式使用变量初始化那些space秒。 (当然是在调用 main 之前)。

bootstrap 和 linker 脚本之间存在非常密切的关系,因此对于您负责 .data 和 .bss 的平台(以及您创建的其他并发症)你用linker来解决)。当然,对于 gnu,当您使用内存映射设计来指定 .text、.data、.bss 部分的位置时,您在 linker 脚本中创建变量以了解起点、终点 and/or 大小,并且这些变量由 bootstrap 到 copy/init 那些部分使用。由于 asm 和 linker 脚本依赖于工具,因此预计它们不会被 portable 所以你必须为每个工具重做它(如果你的 C 更 portable不使用内联 asm 和 pragma 等(无论如何都不需要这些))所以解决方案越简单,如果您希望在不同的工具上尝试应用程序希望为最终用户支持不同的工具,则必须移植的代码越少使用应用等

带有 aarch64 的最新内核通常相当复杂,但特别是如果您想选择特定模式,您可能需要编写非常精细的 bootstrap 代码。好处是,对于分组寄存器,您可以直接从更高特权模式访问它们,而不必像 armv4t 等那样进行模式切换。执行级别并没有节省多少,您需要了解、设置和维护的所有内容都非常详细。如果您正在创建操作系统,则包括每个执行层和启动应用程序时的堆栈。

这是我在裸机 C 代码 aarch64、Pi3 中在全局级别使用的代码的变体。它调用一个名为 enter 的 C 函数,它设置了一个简单的堆栈,给定一个变量 stacks 和每个内核所需的堆栈大小 STACK_SIZE(不能使用 sizeof) .

asm (
    "\n.global  _start"
    "\n.type    _start, %function"
    "\n.section .text"
    "\n_start:"
    "\n\tmrs     x0, mpidr_el1"
    "\n\ttst     x0, #0x40000000"
    "\n\tand     x1, x0, #0xff"
    "\n\tcsel    x1, x1, xzr, eq" // core
    "\n\tadr     x0, stacks"
    "\n\tmov     x3, #"STACK_SIZE                                                                                       
    "\n\tmul     x2, x1, x3"
    "\n\tadd     x0, x0, x2"
    "\n\tadd     sp, x0, x3"
    "\n\tb     enter"
    "\n\t.previous"
    "\n.align 10" ); // Alignment to avoid GPU overwriting code