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

问题描述

当我使用GCC进行ARM操作系统开发时,由于堆栈未初始化,所以我不能使用局部变量,那么如何告诉编译器初始化SP?

解决方法

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

,

这是我在裸机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,x3"
    "\n\tadd     x0,x2"
    "\n\tadd     sp,x3"
    "\n\tb     enter"
    "\n\t.previous"
    "\n.align 10" ); // Alignment to avoid GPU overwriting code
,

由于您未指定目标,因此您的问题令人困惑,对于不同类型的ARM体系结构,有不同的答案。但是独立于gcc与此无关。 Gcc是C编译器,因此,理想情况下,您需要用某种其他语言编写的引导程序(否则它看起来很糟糕,无论如何您都在解决鸡和鸡蛋的问题)。通常以汇编语言完成。

对于进入armv7-a内核的armv4t,您具有不同的处理器模式,用户,系统,主管等。查看《体系结构参考手册》时,您会看到堆栈指针已存储,每种模式或每个模式都有一个。至少许多模式都有一个加一点点共享。这意味着您需要一种访问该寄存器的方法。对于这些内核,工作原理是您需要切换模式,设置堆栈指针切换模式,设置堆栈指针,直到拥有将要使用的所有设置为止(有关方面,请参阅Internet上成千上万的示例)如何执行此操作)。然后通常回到主管模式,然后启动到应用程序/内核中,无论您要调用它什么。

然后使用armv8-a,我也认为armv7-a也有一个虚拟机监控程序模式,这是不同的。当然是64位内核armv8-a(内部具有armv7-aarch32执行兼容的内核)。

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

reset:
    mov sp,=0x8000

或类似的东西。在早期的Pis上,您可以执行此操作,因为加载程序会将kernel.img设置为0x8000,除非另有指示,否则从入口点以下到ATAG的正上方是可用空间,并且在启动后使用这样,您就可以自由地访问ATAG条目到异常表(您需要进行设置),最简单的方法是让工具为您工作并生成地址,然后将它们简单地复制到它们的正确位置。 >

.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!,r9}
    ldmia r0!,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,#0x4000

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

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

armv8-m有一个例外表,但是例外的间隔如ARM文档中所示。

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

armv6-m,armv7-m和armv8-m(到目前为止彼此兼容)的皮质ms使用向量表。意味着众所周知的地址是向量,即处理程序的地址,而不是指令,因此您可以执行以下操作

.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向量表具有用于堆栈指针初始化的条目,因此您不必添加代码,只需将地址放在此处即可。复位时,逻辑从0x00000000读取,将该值放入堆栈指针,从0x00000004读取,检查并剥离lsbit并在该地址开始执行(lsbit需要在向量表中设置,请不要执行reset + 1操作,正确使用工具。)

注意_start实际上不是必需的,只是分散注意力,它们是裸机,因此没有加载程序需要知道入口点是什么,同样,理想情况下,您正在制作自己的引导程序和链接程序脚本,因此没有如果不将它放在链接脚本中,则需要_start。养成一个习惯比什么都重要,以后可以省一些问题。

在阅读体系结构参考手册时,您会注意到stm / push指令的描述是如何先进行递减然后存储的,因此,如果设置为0x20001000,则首先推送的地址是地址0x20000FFC,而不是0x20001000 ,对于非ARM来说不一定如此,因此一如既往先获取并阅读文档,然后开始编码。

您由裸机程序员全权负责芯片供应商实施中的内存映射。因此,如果从0x20000000到0x20010000有64KB的ram,则可以决定如何对其进行切片。传统堆栈从顶部向下,数据在底部,堆在中间是很容易的,尽管如果您要谈论的是MCU,为什么您可能会在MCU上堆呢?未指定)。因此,对于64K字节的ram cortex-m,您可能只想将0x20010000放在向量表的第一项中,就完成了堆栈指针初始化问题。有些人通常喜欢过于复杂的链接描述文件,由于某种原因,我无法理解,所以请在链接描述文件中定义堆栈。在这种情况下,您只需使用链接描述文件中定义的变量来指示堆栈顶部,然后将其用于cortex-m的向量表中,或用于全尺寸ARM的引导程序代码中。

在芯片实现范围内完全负责存储空间的一部分还意味着您需要设置链接程序脚本以进行匹配,您还需要了解已阅读文档中记录的异常或向量表众所周知的地址至此,是吗?

对于皮质,也许是这样的

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 Pi零可能是这样的:

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

,您可以从那里过度复杂化。

堆栈指针是引导程序的简单部分,您在设计内存映射时只需在其中选择一个数字即可。初始化.data和.bss更为复杂,尽管对于| Pi Zero,如果您知道自己在做什么,则链接脚本可以如上所述,并且引导程序可以很简单

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返回,如果您的软件设计是事件驱动的,并且在完成所有设置后就不需要前台了,那么对于像mcu这样的裸机系统来说,这是非常好的选择。大多数人认为您无法从主要人员回来。

gcc与此无关,gcc只是一个无法汇编它不能链接的编译器,它甚至不能编译,gcc是一个前端,它调用执行这些工作的其他工具作为解析器,编译器和汇编器,除非被告知,否则不要链接器。解析器和编译器是gcc的一部分。汇编器和链接器是名为binutils的不同程序包的一部分,该程序包具有许多二进制实用程序,并且恰好包括gnu汇编程序或gas。它还包括gnu链接器。汇编语言是特定于汇编程序而非目标的,链接器脚本特定于链接器,而内联汇编特定于编译器,因此,不假定这些内容是从一个工具链移植到另一工具链的。通常,使用内联汇编是不明智的,您必须非常绝望,最好使用实际汇编,也不使用根本汇编,这取决于真正的问题是什么。但是,可以,如果您确实有需要,可以使用gnu内联引导程序。

如果这是Raspberry Pi的问题,GPU引导加载程序会为您将ARM程序复制到ram中,因此整个过程都在ram中,这使其比其他裸机更加容易。对于逻辑控制单元,尽管逻辑只是使用记录的解决方案进行引导,但您需要负责初始化ram,因此,如果您有要初始化的任何.data或.bss,则必须在引导程序中进行。该信息必须位于非易失性ram中,因此您可以使用链接器做两件事:一是将此信息放入非易失性空间(rom / flash)中,并告诉它要在ram中放置它的位置您可以使用正确的工具,链接器会告诉您是否已将所有内容放入flash / ram中,然后可以使用程序以编程方式使用这些空间。 (当然要先调用main。)

由于这个原因,对于您负责.data和.bss的平台(以及使用链接器解决的其他复杂问题),引导程序和链接器脚本之间存在非常密切的关系。在使用内存映射设计指定.text,.data,.bss节将驻留的位置时,当然可以使用gnu,您可以在链接器脚本中创建变量以知道起点,终点和/或大小,这些变量是引导程序用来复制/初始化这些部分。由于asm和链接描述文件依赖于工具,因此它们并不期望可移植,因此您可能需要针对每个工具重做它(如果您不使用内联asm且不使用编译指示等,则C的移植性更高(不需要这些工具)无论如何)),因此解决方案越简单,如果您希望在不同的工具上尝试该应用程序,并希望为最终用户使用该应用程序等提供不同的工具,则移植的代码就越少。

带有aarch64的最新内核通常非常复杂,但是,特别是如果您想选择特定的模式,则可能需要编写非常精致的引导程序代码。令人高兴的是,对于存储寄存器,您可以直接从更高特权的模式直接访问它们,而不必进行类似armv4t之类的模式切换操作。与执行级别相比,节省不了多少,您需要了解的所有内容以及设置和维护的内容都非常详细。如果要创建操作系统,则在启动它们时包括每个执行层和应用程序的堆栈。