动态创建线程
通过一段时间与Zephyr的接触,逐渐发现其使用上与其他实时操作系统有些差异,例如没有动态创建线程和同步对象的函数,当一个函数需要被多个线程运行且线程数量不确定的情况下,静态创建TCB和栈的方式不能满足我们的需求。 下面是一个尝试动态创建线程的例子:
struct k_thread* tcb = NULL ;
k_thread_stack_t * stack = NULL ;
tcb = k_malloc ( sizeof ( struct k_thread) ) ;
stack = k_malloc ( 2048 ) ;
if ( tcb && stack)
{
k_thread_create ( tcb, stack, 2048 , ( k_thread_entry_t) entry, parameter, NULL , NULL , priority, 0 , K_NO_WAIT) ;
}
在没有使用MPU时该程序可以正常运行,但是如果使能了MPU,程序可能在运行时触发写保护,而我在使用过程中刚好打开了MPU进行硬件栈保护,下面是程序运行结果:
动态创建线程触发MPU写保护
看到这个运行结果之后,通过查找错误提示字符,发现是MPU写保护被触发,但是并不清楚具体的原因,加上对Zephyr中MPU的工作机制并不了解,只能将 k_malloc 返回值打印出来,下面是增加打印后的结果: 可以看到tcb的首地址是0x20002488,栈的首地址为0x20002510,在这两个值之间的部分便属于tcb,出错的位置是在写入tcb时触发了写保护。 为找到问题,又对程序进行反汇编,查找0x80027d0对应的指令:
080027 c8 < arch_swap> :
* as BASEPRI is not available.
* /
int arch_swap ( unsigned int key)
{
_current-> arch. basepri = key;
80027 c8: 4 a09 ldr r2, [ pc, #36 ] ; ( 80027f 0 < arch_swap+ 0x28 > )
_current-> arch. swap_return_value = _k_neg_eagain;
80027 ca: 490 a ldr r1, [ pc, #40 ] ; ( 80027f 4 < arch_swap+ 0x2c > )
_current-> arch. basepri = key;
80027 cc: 6893 ldr r3, [ r2, #8 ]
_current-> arch. swap_return_value = _k_neg_eagain;
80027 ce: 6809 ldr r1, [ r1, #0 ]
80027 d0: 6799 str r1, [ r3, #120 ] ; 0x78
#if defined(CONFIG_CPU_CORTEX_M)
SCB-> ICSR | = SCB_ICSR_PENDSVSET_Msk;
80027 d2: 4909 ldr r1, [ pc, #36 ] ; ( 80027f 8 < arch_swap+ 0x30 > )
_current-> arch. basepri = key;
80027 d4: 6758 str r0, [ r3, #116 ] ; 0x74
SCB-> ICSR | = SCB_ICSR_PENDSVSET_Msk;
80027 d6: 684 b ldr r3, [ r1, #4 ]
80027 d8: f043 5380 orr. w r3, r3, #268435456 ; 0x10000000
80027 dc: 604 b str r3, [ r1, #4 ]
80027 de: 2300 movs r3, #0
80027e0 : f383 8811 msr BASEPRI, r3
80027e4 : f3bf 8f 6f isb sy
#endif
return _current-> arch. swap_return_value;
80027e8 : 6893 ldr r3, [ r2, #8 ]
}
从上述代码可看出,该函数是在对 _current->arch.swap_return_value 进行赋值时触发了异常,当我把任务控制块所使用的内存由 k_malloc 改为静态局部变量之后,程序能够正常运行,通过上面的简单排查误以为是内核不能操作用户空间的数据,所以才出现了异常,于是在文件中定义了一个 tcb_buf 数组和一个 tcb_used 变量,代码如下图所示:
static int tcb_used = 0 ;
static struct k_thread tcb_buf[ AT_CLIENT_NUM_MAX] = { 0 } ;
struct k_thread* tcb = NULL ;
k_thread_stack_t * stack = NULL ;
tcb = tcb_buf + tcb_used ;
stack = k_malloc ( 2048 ) ;
if ( tcb && stack)
{
k_thread_create ( tcb, stack, 2048 , ( k_thread_entry_t) entry, parameter, NULL , NULL , priority, 0 , K_FOREVER) ;
tcb_used++ ;
}
当需要动态创建线程时从数组中取出一个tcb用于创建线程,从此处得出的初步结论是内核访问heap中的数据可能存在某种保护,导致程序异常,但是没有足够的证据,便进一步对MPU在Zephyr中的作用进行深入了解。
ARMv7m MPU
简介
MPU在ARMv7m架构中是一个可选的内存保护组件,提供了以下功能:
提供8个或者16个内存保护区域 优先级依次上升的可重叠保护区域
当存在16个保护区域时最高优先级为15 当存在8个保护区域时最高优先级为7 最低优先级为0 访问权限设置 向系统输出存储器属性 通过MPU可以实施访问规则,特权规则,分离进程。
寄存器
下面是MPU相关寄存器: MPU_TYPE 寄存器的 8-15 位用于表示该处理器支持多少个内存保护区域。 MPU_CTRL bit0 用于使能MPU,bit1 用于决定是否在优先级低于0的中断中使用MPU。 MPU_RNR 用于设置 MPU_RBAR 和 MPU_RASR 当前所使用的区域,需要在访问MPU_RBAR 和 MPU_RASR前进行设置,仅bit0-bit7有效。 MPU_RBAR 区域基地址寄存器,该寄存器用于设置保护区域的基地址,仅 bit5 - bit31 有效,也就意味着保护的基地址是32的整倍数,如果传入的地址未按照32位对齐,那么实际设置的基地址可能要比期望的地址要小,在正常进行数据访问时出现故障。 MPU_RASR 区域属性和大小寄存器,用于设置保护区域的属性和保护的长度。
保护区域的实际保护长度为2^(SIZE + 1),其值最小为4也就意味着保护的最小范围为32字节。 访问属性在这里不做叙述,感兴趣的可以查看ARMv7架构参考手册 以上截图均出自ARMv7m架构参考手册。
MPU 在Zephyr中的作用
存储器权限
下面是arm_mu_regions.c中对MPU存储器的设置,以 ARMv7-m 为例:
static const struct arm_mpu_region mpu_regions[ ] = {
MPU_REGION_ENTRY ( "FLASH_0" ,
CONFIG_FLASH_BASE_ADDRESS,
REGION_FLASH_ATTR ( REGION_FLASH_SIZE) ) ,
MPU_REGION_ENTRY ( "SRAM_0" ,
CONFIG_SRAM_BASE_ADDRESS,
REGION_RAM_ATTR ( REGION_SRAM_SIZE) ) ,
LINKER_DT_REGION_MPU ( ARM_MPU_REGION_INIT)
} ;
const struct arm_mpu_config mpu_config = {
. num_regions = ARRAY_SIZE ( mpu_regions) ,
. mpu_regions = mpu_regions,
} ;
#define REGION_RAM_ATTR(size) \
{ \
(NORMAL_OUTER_INNER_WRITE_BACK_WRITE_READ_ALLOCATE_NON_SHAREABLE | \
MPU_RASR_XN_Msk | size | P_RW_U_NA_Msk) \
}
#if defined(CONFIG_MPU_ALLOW_FLASH_WRITE)
#define REGION_FLASH_ATTR(size) \
{ \
(NORMAL_OUTER_INNER_WRITE_THROUGH_NON_SHAREABLE | size | \
P_RW_U_RO_Msk) \
}
#else
#define REGION_FLASH_ATTR(size) \
{ \
(NORMAL_OUTER_INNER_WRITE_THROUGH_NON_SHAREABLE | size | RO_Msk) \
}
#endif
默认情况下MPU开启后会将Flash设置为只读可执行区域,将RAM设置为可读写区域,但是禁止在RAM中执行程序,否则就会触发异常。 除此之外还可以在设备树对其他区域的权限进行设置,例如外部SRAM,以及FSMC映射的区域。
硬件栈保护
在进行上下文切换时会将栈底的一部分区域设置为只读,如果向其中写入数据会触发写保护,其中z_arm_configure_dynamic_mpu_regions 函数就是在上下文中用于配置MPU的函数,下面是 z_arm_configure_dynamic_mpu_regions 函数删除无关代码之后的部分细节:
void z_arm_configure_dynamic_mpu_regions ( struct k_thread * thread)
{
static struct z_arm_mpu_partition
dynamic_regions[ _MAX_DYNAMIC_MPU_REGIONS_NUM] ;
uint8_t region_num = 0U ;
#if defined(CONFIG_MPU_STACK_GUARD)
uintptr_t guard_start;
size_t guard_size = MPU_GUARD_ALIGN_AND_SIZE;
{
guard_start = thread-> stack_info. start - guard_size;
}
__ASSERT ( region_num < _MAX_DYNAMIC_MPU_REGIONS_NUM,
"Out-of-bounds error for dynamic region map." ) ;
dynamic_regions[ region_num] . start = guard_start;
dynamic_regions[ region_num] . size = guard_size;
dynamic_regions[ region_num] . attr = K_MEM_PARTITION_P_RO_U_NA;
region_num++ ;
#endif
arm_core_mpu_configure_dynamic_mpu_regions ( dynamic_regions,
region_num) ;
}
从代码中可以知道该函数将栈起始地址减去0x40之后作为保护区域,默认情况下通过 k_malloc 申请的地址就是作为栈的起始地址 thread->stack_info.start 不是栈空间的起始地址,而是代表栈底,当程序运行超过该地址会触发异常,下面是线程初始化时栈的初始化函数:
static char * setup_thread_stack ( struct k_thread * new_thread,
k_thread_stack_t * stack, size_t stack_size)
{
size_t stack_obj_size, stack_buf_size;
char * stack_ptr, * stack_buf_start;
size_t delta = 0 ;
{
stack_obj_size = Z_KERNEL_STACK_SIZE_ADJUST ( stack_size) ;
stack_buf_start = Z_KERNEL_STACK_BUFFER ( stack) ;
stack_buf_size = stack_obj_size - K_KERNEL_STACK_RESERVED;
}
stack_ptr = ( char * ) stack + stack_obj_size;
LOG_DBG ( "stack %p for thread %p: obj_size=%zu buf_start=%p "
" buf_size %zu stack_ptr=%p" ,
stack, new_thread, stack_obj_size, ( void * ) stack_buf_start,
stack_buf_size, ( void * ) stack_ptr) ;
#ifdef CONFIG_INIT_STACKS
memset ( stack_buf_start, 0xaa , stack_buf_size) ;
#endif
#if CONFIG_STACK_POINTER_RANDOM
delta + = random_offset ( stack_buf_size) ;
#endif
delta = ROUND_UP ( delta, ARCH_STACK_PTR_ALIGN) ;
#ifdef CONFIG_THREAD_STACK_INFO
new_thread-> stack_info. start = ( uintptr_t) stack_buf_start;
new_thread-> stack_info. size = stack_buf_size;
new_thread-> stack_info. delta = delta;
#endif
stack_ptr - = delta;
return stack_ptr;
}
经过上面的分析可得出,MPU保护的区域实际上是栈空间的一部分,将栈空间前64字节设置为保护区域,剩余的部分为线程栈,当程序过度运行超过栈底,将会触发MPU写保护,通过这样的方式来实现硬件栈保护。
问题分析
回过头再来看看看这张图片 首先我们通过k_malloc申请的地址并没有进行32字节对齐。 其次 MMFAR Address 的值可以看出实际设置的保护基地址要比栈起始地址要小,以至于将 tcb 的部分区域设置成了写保护,当进行上下文切换时,就有可能触发写保护。 如前面所说,由于MPU基地址时只取高27位,低5位为0,如果没有进行32字节对齐,低5位会被忽略掉,导致设置的保护基地址要比期望的要小,导致MPU写保护被错误触发。 前面的程序中将 tcb 改为静态数组后,由于bss段中该数组的地址和堆空间中申请的栈首地址间隔较大,所以写保护未被触发,但仍有概率触发写保护。
Zephyr动态创建线程
ARMv7m架构以32字节进行对齐,因此动态申请的栈空间首地址也必须是32字节对齐的,但是此处并没有采用32字节对齐,而是采用64字节对齐。 因为保护区域必须包含足够的空间,用于捕获区域下方的异常帧,因为堆栈错误最终会将异常数据(0x20 字节)存储到堆栈指针引用的任何位置的堆栈上,即使该堆栈在保护区域内也是如此,因此我们通过将其设置为 0x40 来确保该区域可保存异常帧。
#if defined(CONFIG_MPU_STACK_GUARD)
#if CONFIG_ARM_MPU_REGION_MIN_ALIGN_AND_SIZE <= 0x20
#define MPU_GUARD_ALIGN_AND_SIZE 0x40
#else
#define MPU_GUARD_ALIGN_AND_SIZE CONFIG_ARM_MPU_REGION_MIN_ALIGN_AND_SIZE
#endif
#else
#define MPU_GUARD_ALIGN_AND_SIZE 0
#endif
k_thread_t tcb = NULL ;
k_thread_stack_t * stack = NULL ;
stack = k_aligned_alloc ( Z_KERNEL_STACK_OBJ_ALIGN, Z_KERNEL_STACK_SIZE_ADJUST ( 2048 ) ) ;
tcb = k_malloc ( sizeof ( struct k_thread) ) ;
memset ( tcb, 0 , sizeof ( struct k_thread) ) ;
if ( tcb && stack)
{
k_thread_create ( tcb, stack, 2048 , ( k_thread_entry_t) entry, parameter, NULL , NULL , priority, 0 , K_NO_WAIT) ;
}
为了申请到对齐的栈空间,改用 k_aligned_alloc 函数,该函数可以按照输入的对齐值分配空间。 Z_KERNEL_STACK_OBJ_ALIGN 是内核中表示不同芯片栈对齐值的一个宏,根据使用的芯片,以及是否使能MPU,其值会变化,例如在ARMv7m架构中,如果没有使用MPU,该值即为4,使用了MPU之后,该值为64。 Z_KERNEL_STACK_SIZE_ADJUST 宏源自 K_KERNEL_STACK_DEFINE, 下面是其展开后的部分内容,通过 Z_KERNEL_STACK_SIZE_ADJUST 便可计算出对齐后的实际栈空间。
#define K_KERNEL_STACK_DEFINE(sym, size) \
Z_KERNEL_STACK_DEFINE_IN(sym, size, __kstackmem)
#define Z_KERNEL_STACK_DEFINE_IN(sym, size, lsect) \
struct z_thread_stack_element lsect \
__aligned(Z_KERNEL_STACK_OBJ_ALIGN) \
sym[Z_KERNEL_STACK_SIZE_ADJUST(size)]
运行效果
总结
Zephyr 中可以使用堆空间创建tcb和线程栈,但是为了保险起见,不应该直接使用k_malloc,而应该使用 k_aligned_alloc ,同时需要根据对应的规则进行对齐,否则在MPU工作时,程序就会被异常终止。