文章目录
前言
在多核处理器环境中,每个CPU核心都需要获得独立的栈空间,从而在并行运行时避免栈空间冲突和竞争。
为每个cpu分配堆栈空间仍是由TF-A中入口函数的第一个宏el3_entrypoint_common
完成的,分配堆栈空间除了设置栈的起始地址和大小,还将栈指针sp(Stack Pointer)寄存器设置为指向该栈的顶部。
本文详细地分析了TF-A分配堆栈空间的过程。
本文可以独立阅读。也可以作为前面两篇入口分析文章的续篇。
《TF-A中入口函数的第一个宏el3_entrypoint_common
详细分析之一(TF-A入口分析一)》
《TF-A中入口函数的第一个宏el3_entrypoint_common
详细分析之二(TF-A入口分析二)》
我们调试的tf-a源码的版本为v2.8,为了使用qemu调试TF-A,选择fvp模拟器作为硬件平台。编译指令如下:
make CROSS_COMPILE=aarch64-linux-gnu- PLAT=fvp CPU=cortex-a57 TARGET=all DEBUG=1
1、分配堆栈空间
分配堆栈空间仍然是由TF-A中入口函数的第一个宏el3_entrypoint_common
完成的,调用过程如下:
/* ---------------------------------------------------------------------
* 使用 SP_EL0 作为 C 运行时堆栈
* ---------------------------------------------------------------------
*/
msr spsel, #0
/* ---------------------------------------------------------------------
* 为一个栈分配内存,该内存将在启用MMU(内存管理单元)时被标记为Normal-IS-WBWA。
* 启用MMU后,没有读取过期栈内存的风险,因为此时只有主CPU在运行。
* ---------------------------------------------------------------------
*/
bl plat_set_my_stack
首先执行msr spsel, #0
指令,将SPSel(栈指针选择器)寄存器的值初始化为0。这样做的结果,会使得处理器正确地使用SP_EL3寄存器来设置和管理我们准备分配的栈空间。
接下来调用plat_set_my_stack
函数,目的是为当前CPU分配一个栈内存。栈内存在启用MMU(内存管理单元)后将被标记为Normal-IS-WBWA。这里的Normal-IS-WBWA表示内存类型是Normal,具有Inner Shareable属性(IS,内部可共享),并采用写回(Write-Back)和写分配(Write-Allocate)的缓存策略。后面我们会详细分析plat_set_my_stack
函数涉及所有宏和函数。
2、设置堆栈指针–plat_set_my_stack
函数
函数位于\arm-trusted-firmware\plat\common\aarch64\platform_mp_stack.S
/* -----------------------------------------------------
* void plat_set_my_stack ()
* 该函数用于当前 CPU,将堆栈指针设置为在正常内存中分配的堆栈。
* -----------------------------------------------------
*/
func plat_set_my_stack
mov x9, x30 // 保存返回地址到寄存器x9(链接寄存器x30保存着函数的返回地址)
bl plat_get_my_stack // 调用plat_get_my_stack函数来获取栈的起始地址,该地址将存储在x0寄存器中
mov sp, x0 // 把寄存器x0中的地址赋值给栈指针(sp)
ret x9 // 使用寄存器x9中保存的返回地址返回到调用者
endfunc plat_set_my_stack
plat_set_my_stack
函数功能:为当前正在运行的CPU设置栈指针,将栈指针指向在普通内存中分配的栈空间。
以下是逐句分析:
- mov x9, x30:将寄存器x30(链接寄存器,存储返回地址)的值移动到寄存器x9中,以便稍后恢复。
- bl plat_get_my_stack:调用plat_get_my_stack函数。此函数的目的是获取当前CPU对应的栈的起始地址,并将其返回值存储在寄存器x0中。
- mov sp, x0:将寄存器x0(plat_get_my_stack的返回值)的值移动到栈指针寄存器sp中,将栈指针设置为当前CPU对应的栈的起始地址。
- ret x9:将x9寄存器的值(之前保存的链接寄存器x30的值)作为返回地址,跳转回调用者并结束该函数。
在ARM汇编程序中,通常会将函数的返回地址放在寄存器x30中。这是一种惯例,因为在许多情况下,x30被定义为链接寄存器(Link Register),用于存储函数的返回地址。
3、获取栈顶地址–plat_get_my_stack函数
函数位于\arm-trusted-firmware\plat\common\aarch64\platform_mp_stack.S
/* -----------------------------------------------------
* uintptr_t plat_get_my_stack ()
*
* 对于当前的CPU,此函数返回在设备内存中分配的堆栈的堆栈指针。
* -----------------------------------------------------
*/
func plat_get_my_stack
mov x10, x30 // 保存返回地址到寄存器x10(链接寄存器x30保存着函数的返回地址)
get_my_mp_stack platform_normal_stacks, PLATFORM_STACK_SIZE // 调用get_my_mp_stack宏,传入两个参数:platform_normal_stacks和PLATFORM_STACK_SIZE
ret x10 // 使用寄存器x10中保存的返回地址返回到调用者
endfunc plat_get_my_stack
plat_get_my_stack()函数功能:为当前CPU返回在设备内存中分配的栈的栈指针。
逐句分析如下:
-
mov x10, x30:将寄存器x30(链接寄存器,存储返回地址)的值移动到寄存器x10中,以便稍后恢复。
-
调用名为get_my_mp_stack的宏,并传递两个参数:platform_normal_stacks和PLATFORM_STACK_SIZE。该宏计算当前CPU对应的栈的起始地址,并将其返回值存储在寄存器x0中。
-
ret x10:将x10寄存器的值(之前保存的链接寄存器x30的值)作为返回地址,跳转回调用者并结束该函数。
4、计算栈顶地址–get_my_mp_stack
宏
宏的定义文件:\arm-trusted-firmware\include\arch\aarch64\asm_macros.S
/*
* 本宏根据plat_my_core_pos()索引、栈存储的名称和每个栈的大小来计算当前CPU的MP栈(多处理器栈)的基地址。
* 输出:X0 = 栈基址的物理地址
* 可能改变的寄存器:X30, X1, X2
*/
.macro get_my_mp_stack _name, _size
bl plat_my_core_pos
adrp x2, (\_name + \_size)
add x2, x2, :lo12:(\_name + \_size)
mov x1, #\_size
madd x0, x0, x1, x2
.endm
这个宏(get_my_mp_stack)是用于计算当前CPU的多处理器(MP)堆栈的基地址的。它通过调用plat_my_core_pos函数获取当前CPU的索引,然后使用该索引、存储堆栈的名称和每个堆栈的大小计算堆栈的基地址。
这个宏的代码流程如下:
- bl plat_my_core_pos:调用plat_my_core_pos函数,获取当前CPU的索引。
- adrp x2, (_name + _size):使用adrp指令计算指向堆栈存储区域的高位地址部分,并将其加载到寄存器x2中。
- add x2, x2, :lo12:(_name + _size):使用add指令合并高位地址和低位地址,以获得完整的地址。
- mov x1, #_size:将堆栈的大小存储到寄存器x1中。
- madd x0, x0, x1, x2:使用madd指令计算当前CPU的MP堆栈的基地址,并将其存储到寄存器x0中。
这个宏的输出为:X0 = physical address of stack base,即堆栈的基地址。它会使用(占用)寄存器X30、X1、X2。
5、 获取当前CPU索引–plat_my_core_pos
函数
函数位于 \arm-trusted-firmware\plat\arm\common\aarch64\arm_helpers.S
/* -----------------------------------------------------
* unsigned int plat_my_core_pos(void)
* 这个函数使用plat_arm_calc_core_pos定义来获取调用该函数的CPU的索引。
* -----------------------------------------------------
*/
func plat_my_core_pos
mrs x0, mpidr_el1
b plat_arm_calc_core_pos
endfunc plat_my_core_pos
plat_my_core_pos的函数,它的功能是使用plat_arm_calc_core_pos定义来获取调用CPU的索引。以下是逐句分析:
- mrs x0, mpidr_el1:从MPIDR_EL1(多处理器仿真器ID寄存器)中读取当前CPU的ID,并将其存储在寄存器x0中。
b plat_arm_calc_core_pos:分支到plat_arm_calc_core_pos标签。这个负责根据传入的CPU ID(存储在寄存器x0中)计算CPU的索引。计算结果将存储在寄存器x0中。
MPIDR_EL1寄存器中包含了关于CPU集群和核心编号的信息,这些信息可用于唯一标识一个CPU。通过读取这个寄存器,函数可以获取到当前正在执行该代码的CPU的ID。
6、 计算cpu核的索引–plat_arm_calc_core_pos函数
函数位置:\arm-trusted-firmware\plat\arm\board\fvp\aarch64\fvp_helpers.S
函数功能:用于计算当前Fixed Virtual Platforms(FVP)上正在运行的cpu核的线性位置。函数接收一个参数mpidr,这是一个ARMv8-A架构中用于表示处理器亲和性的寄存器。可以从mpidr中提取需要的亲和性字段:ClusterId、CPUId 和ThreadId。
简化公式:((ClusterId * FVP_MAX_CPUS_PER_CLUSTER + CPUId) * FVP_MAX_PE_PER_CPU) + ThreadId,用于计算当前运行的CPU核在整个CPU架构中的线性位置。这个线性位置是一个唯一的索引,可用于区分不同的CPU核并确定它们在系统中的位置。例如,在分配和管理多处理器栈时,这个线性位置可以帮助找到每个CPU核专属的栈空间。
- FVP_MAX_PE_PER_CPU :单个处理器上的最大可用 PE(物理执行单元) 数;
- FVP_MAX_CPUS_PER_CLUSTER :单个处理器集中可以使用的最大 CPU 数量;
- ClusterId: 是指处理器集群的编号。处理器集群是多个处理器组成的单元,每个集群都有唯一的编号。
- CPUId :是指单个处理器的编号。每个处理器都属于某个集群,并且有一个在集群内唯一的编号。
- ThreadId :是指在处理器上运行的线程的编号。多核处理器可以在单个处理器上同时运行多个线程,每个线程都有一个在处理器内唯一的编号。
最后,函数将计算得到的线性位置放入寄存器x0,并返回。
/* ---------------------------------------------------------------------
* unsigned int plat_arm_calc_core_pos(u_register_t mpidr)
*
* 函数功能:计算FVP(Fixed Virtual Platforms)上当前正在工作的核位置。
* (ClusterId * FVP_MAX_CPUS_PER_CLUSTER * FVP_MAX_PE_PER_CPU) +
* (CPUId * FVP_MAX_PE_PER_CPU) +
* ThreadId
* 计算公式可以简化为:
* ((ClusterId * FVP_MAX_CPUS_PER_CLUSTER + CPUId) * FVP_MAX_PE_PER_CPU)
* + ThreadId
* ---------------------------------------------------------------------
*/
func plat_arm_calc_core_pos
/*
* 检查MPIDR中的MT位。如果未设置,将MPIDR左移,使其看起来像多线程实现。
*/
tst x0, #MPIDR_MT_MASK
lsl x3, x0, #MPIDR_AFFINITY_BITS
csel x3, x3, x0, eq
/* MPIDR中提取单独的亲和性字段 */
ubfx x0, x3, #MPIDR_AFF0_SHIFT, #MPIDR_AFFINITY_BITS
ubfx x1, x3, #MPIDR_AFF1_SHIFT, #MPIDR_AFFINITY_BITS
ubfx x2, x3, #MPIDR_AFF2_SHIFT, #MPIDR_AFFINITY_BITS
/* 计算线性位置 */
mov x4, #FVP_MAX_CPUS_PER_CLUSTER
madd x1, x2, x4, x1
mov x5, #FVP_MAX_PE_PER_CPU
madd x0, x1, x5, x0
ret
endfunc plat_arm_calc_core_pos
7、 MPIDR(Multi-Processor ID Register)寄存器简介
亲和性是指处理器与存储设备(例如内存)配合度。处理器和存储设备之间的亲和性可以影响数据存储和加载的速度和效率。如果处理器和存储设备之间的亲和性较高,则读写数据的速度可以提高,并且可以降低系统的延迟和加载时间。
在ARMv8-A架构中,MPIDR(Multi-Processor ID Register)寄存器用于唯一标识处理器核心并提供关于处理器核心拓扑结构的信息。MPIDR寄存器是一个64位寄存器,其中一些位用于表示不同级别的亲和性(affinity),这些亲和性信息可用于识别处理器在多级拓扑结构中的位置。以下是MPIDR寄存器中可以提取的参数:
Aff0(亲和性级别0,位0-7):表示处理器核心在其所属CPU内的位置。
Aff1(亲和性级别1,位8-15):表示处理器核心所在的CPU或核心在其所属集群内的位置。
Aff2(亲和性级别2,位16-23):表示处理器核心所在的集群在其所属系统(如SoC)内的位置。
Aff3(亲和性级别3,位32-39):表示处理器核心所在系统在更大规模系统内的位置。这一级别在大多数实现中不使用,但在特定场景下可能有用。
MT(位24):表示多线程支持。当MT位被设置时,表示处理器实现支持多线程。
U(位30):表示MPIDR寄存器的值是唯一的。当U位被设置时,表示所有处理器核心的MPIDR寄存器值都是唯一的。
MPIDR寄存器的主要功能是:
提供关于处理器核心拓扑结构的信息,以便软件可以在多核环境中进行任务调度和管理。
唯一标识处理器核心,以便在多核处理器环境中区分各个核心。
通过MPIDR寄存器中的亲和性信息,软件可以了解处理器核心在多级拓扑结构中的位置,从而进行任务分配、调度和核心间通信。
堆栈分配过程总结
宏el3_entrypoint_common
调用plat_set_my_stack
函数不仅给出了栈的起始指针和大小,还将栈指针sp(Stack Pointer)寄存器设置为指向该栈的顶部。步骤如下:
- 第一步:plat_my_core_pos函数,从MPIDR_EL1寄存器读取当前运行CPU核的亲和性信息(包括ClusterId、CPUId和ThreadId等)并存储在寄存器X0中。
- 第二步:plat_arm_calc_core_pos函数,根据寄存器X0中保存的亲和性信息计算出当前运行的CPU核在整个处理器架构中的唯一索引。
- 第三步:get_my_mp_stack宏,使用计算出的当前CPU索引来确定当前CPU核的栈空间起始地址。
- 第四步:plat_get_my_stack函数和get_my_mp_stack宏,根据当前CPU核的栈空间起始地址和栈大小来计算栈顶地址。
- 第五步:将计算出的栈顶地址赋值给栈指针(sp)寄存器。
以上概括了tf-a源码在设置堆栈方面的逻辑。这些步骤确保了在多核处理器环境中,每个CPU核心都能获得独立的栈空间,从而在并行运行时避免栈空间冲突和竞争。这对于实现任务隔离和提高多核处理器性能非常重要。