当我使用GCC进行ARM操作系统开发时,我不能使用局部变量,因为堆栈没有初始化,所以我如何告诉编译器初始化SP?
您的问题令人困惑,因为您没有指定目标,不同风格的ARM架构有不同的答案。但独立于此,gcc与此无关。Gcc是一个C编译器,因此您需要一个理想的用其他语言编写的引导程序(否则它看起来很糟糕,无论如何您都在与鸡和蛋的问题作斗争)。通常用汇编语言完成。
对于armv4t到armv7-a内核,您有不同的处理器模式、用户、系统、主管等。当您查看《体系结构参考手册》时,您会发现堆栈指针是堆叠的,每个模式一个,或者至少许多模式都有一个加一点共享。这意味着您需要有一种访问该寄存器的方法。对于这些核心,它的工作原理是你需要切换模式设置堆栈指针切换模式设置栈指针,直到你有了所有要使用的设置(请参阅互联网上关于如何做到这一点的数万到数十万个例子)。然后经常返回到监管模式,然后引导到应用程序/内核中,不管你想怎么称呼它
然后使用armv8-a,我认为armv7-a也有一个不同的hypervisor模式。当然,armv8-a是64位内核(内部有一个armv7-a兼容内核,用于执行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!,{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有一个异常表,但异常被隔开,如ARM文档中所示。
上面由ARM记录的众所周知的地址是一个入口点,代码在那里开始执行,所以你需要把指令放在那里,然后如果是重置处理程序,你通常会在那里添加代码来设置堆栈指针、复制.data、zero.bss和任何其他在输入C代码之前需要的引导。
皮层ms是armv6-m、armv7-m和armv8-m(到目前为止与其中一个兼容),使用向量表。这意味着众所周知的地址是向量,是处理程序的地址,而不是指令,所以你会做这样的
.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需要在矢量表中设置,请不要执行重置+1操作,正确使用工具)。
注意_start实际上并不是必要的,它只是分散注意力——这些都是裸露的金属,所以没有加载程序需要知道入口点是什么,同样,理想情况下,你正在制作自己的引导程序和链接器脚本,所以如果你不将_start放在链接器脚本中,就不需要它。这只是一种习惯,包括它比任何事情都重要,可以省去以后的问题。
当你阅读体系结构参考手册时,你会注意到stm/push指令的描述是如何先递减后存储的,所以如果你设置0x20001000,那么推送的第一个东西是在地址0x20000FFC,而不是0x20001000。对于非ARM,这不一定是真的,所以总是先获取和读取文档,然后开始编码。
裸金属程序员对芯片供应商实现中的内存映射负全部责任。因此,如果内存在0x20000000到0x20010000之间有64KB,您可以决定如何对其进行切片。传统的堆栈从上往下,数据在下,堆在中间,这是非常容易的,尽管如果这是一个你正在谈论的mcu(你没有具体说明),为什么你可能会在一个mcu上有一个堆。因此,对于64K字节的ram cortex-m,您可能只想在向量表的第一个条目中放入0x20010000,堆栈指针初始化问题完成。有些人通常喜欢过度复杂化链接器脚本,出于我无法理解的原因,在链接器脚本中定义堆栈。在这种情况下,您只需使用链接器脚本中定义的变量来指示堆栈顶部,并在cortex-m的矢量表中或在全尺寸ARM的引导程序代码中使用该变量。
此外,对芯片实现限制范围内的内存空间负全部责任意味着你要设置链接器脚本以匹配,你需要知道异常或向量表中的已知地址,正如你现在已经阅读的文档中所记录的那样,是吗?
对于一个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
}
你可以从那里把它搞得过于复杂。
堆栈指针是引导程序中最简单的部分,您只需在设计内存映射时在其中输入一个数字即可。初始化.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中,与其他裸机相比,它要容易得多。对于mcu,尽管逻辑只是使用文档化的解决方案引导,但您负责初始化ram,因此如果您有任何想要初始化的.data或.bss,则必须在引导程序中进行初始化。信息需要在非易失性ram中,所以你可以使用链接器做两件事:一是将这些信息放在非易易失性空间(rom/flash)中,二是告诉它你将把它放在ram的哪里,如果你使用正确的工具,链接器会告诉你它是把每件事都放在flash/ram中的,然后你可以通过编程将变量初始化这些空间。(当然是在呼叫main之前)。
由于这个原因,引导程序和链接器脚本之间有着非常密切的关系,因为您负责.data和.bss的平台(加上您使用链接器解决的其他复杂问题)。当然,使用gnu,当您使用内存映射设计来指定.text、.data和.bss部分的位置时,您可以在链接器脚本中创建变量来知道起点、终点和/或大小,引导程序使用这些变量来复制/初始化这些部分。由于asm和链接器脚本依赖于工具,因此它们不应该是可移植的,因此您必须为每个工具重做它(如果您不使用内联asm和pragma等,则C更具可移植性(无论如何都不需要这些)),因此解决方案越简单,如果您希望在不同的工具上尝试应用程序,则需要移植的代码就越少。如果您希望最终支持不同的工具用户使用应用程序等
aarch64的最新内核总体上相当复杂,但特别是如果你想选择一种特定的模式,你可能需要编写非常精细的引导程序代码。好的是,对于银行寄存器,您可以从更高特权的模式直接访问它们,而不必像armv4t之类的模式切换。执行级别并没有节省多少钱,您需要了解、设置和维护的所有内容都非常详细。包括每个执行层的堆栈,以及在创建操作系统时启动应用程序时的堆栈。
我的经验是使用Cortex-M,正如@n-promouns-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:"
"ntmrs x0, mpidr_el1"
"nttst x0, #0x40000000"
"ntand x1, x0, #0xff"
"ntcsel x1, x1, xzr, eq" // core
"ntadr x0, stacks"
"ntmov x3, #"STACK_SIZE
"ntmul x2, x1, x3"
"ntadd x0, x0, x2"
"ntadd sp, x0, x3"
"ntb enter"
"nt.previous"
"n.align 10" ); // Alignment to avoid GPU overwriting code