Zephyr MPU栈保护

文章探讨了在Zephyr实时操作系统中动态创建线程遇到的问题,特别是在启用MPU(MemoryProtectionUnit)后触发的写保护异常。通过分析代码和MPU工作原理,发现由于栈空间地址未正确对齐,导致MPU设置的保护区域错误覆盖了TCB,从而在上下文切换时引发异常。解决方案是使用k_aligned_alloc确保栈空间32字节对齐,避免MPU写保护错误触发。
摘要由CSDN通过智能技术生成

动态创建线程

  • 通过一段时间与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对应的指令:
080027c8 <arch_swap>:
 * as BASEPRI is not available.
 */
int arch_swap(unsigned int key)
{
	/* store off key and return value */
	_current->arch.basepri = key;
 80027c8:	4a09      	ldr	r2, [pc, #36]	; (80027f0 <arch_swap+0x28>)
	_current->arch.swap_return_value = _k_neg_eagain;
 80027ca:	490a      	ldr	r1, [pc, #40]	; (80027f4 <arch_swap+0x2c>)
	_current->arch.basepri = key;
 80027cc:	6893      	ldr	r3, [r2, #8]
	_current->arch.swap_return_value = _k_neg_eagain;
 80027ce:	6809      	ldr	r1, [r1, #0]
 80027d0:	6799      	str	r1, [r3, #120]	; 0x78

#if defined(CONFIG_CPU_CORTEX_M)
	/* set pending bit to make sure we will take a PendSV exception */
	SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
 80027d2:	4909      	ldr	r1, [pc, #36]	; (80027f8 <arch_swap+0x30>)
	_current->arch.basepri = key;
 80027d4:	6758      	str	r0, [r3, #116]	; 0x74
	SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
 80027d6:	684b      	ldr	r3, [r1, #4]
 80027d8:	f043 5380 	orr.w	r3, r3, #268435456	; 0x10000000
 80027dc:	604b      	str	r3, [r1, #4]
 80027de:	2300      	movs	r3, #0
 80027e0:	f383 8811 	msr	BASEPRI, r3
 80027e4:	f3bf 8f6f 	isb	sy
#endif

	/* Context switch is performed here. Returning implies the
	 * thread has been context-switched-in again.
	 */
	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[] = {
	/* Region 0 */
	MPU_REGION_ENTRY("FLASH_0",
			 CONFIG_FLASH_BASE_ADDRESS,
			 REGION_FLASH_ATTR(REGION_FLASH_SIZE)),
	/* Region 1 */
	MPU_REGION_ENTRY("SRAM_0",
			 CONFIG_SRAM_BASE_ADDRESS,
			 REGION_RAM_ATTR(REGION_SRAM_SIZE)),
	/* DT-defined regions */
	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;
	/* MPU_GUARD_ALIGN_AND_SIZE 需要确保有足够的空间来捕获异常,此处编译后的值为0x40,即MPU保护区域大小 */
	size_t guard_size = MPU_GUARD_ALIGN_AND_SIZE;

	{
		/* stack_info.start为栈底地址,但是在栈底之前Zephyr还预留了一部分区域,该区域用于实现栈保护,如果将栈底的首地址减去0x40,
		得到的地址就是编译器分配的栈空间起始地址 */
		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 /* CONFIG_MPU_STACK_GUARD */

	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;

	{
		/* Z_KERNEL_STACK_SIZE_ADJUST(stack_size)用于将栈的实际大小进行对齐处理,使用宏定义栈也做了同样的处理,
		此处通过同样的计算方式可得到栈的实际大小,这样实际分配的栈空间应该大于等于用户需要的栈空间,空出的部分可能不会使用,
		但是对齐后分配新的栈时容易找到满足条件的地址 */
		stack_obj_size = Z_KERNEL_STACK_SIZE_ADJUST(stack_size);
		/* Z_KERNEL_STACK_BUFFER(stack) 用于将栈地址加上MPU保护长度之后的地址作为栈底地址 */
		stack_buf_start = Z_KERNEL_STACK_BUFFER(stack);
		/* K_KERNEL_STACK_RESERVED 其实就是MPU保护区域的大小,栈大小减去该部分之后作为栈实际可以空间 */
		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);

	/* 将栈全部设置为0xaa,可以用于计算出栈还有多少可用空间 */
#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
	/* 初始化TCB中栈的信息,
	stack_info.start 中保存的是栈起始地址加上保护区域大小后的地址
	stack_info.size 为栈大小
	 */
	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)
/* make sure there's more than enough space for an exception frame */
#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;
    
    /* MPU 开启后对栈底设置写保护,栈基地址必须是 Z_KERNEL_STACK_OBJ_ALIGN 整倍数,
    否则会造成写保护被错误触发,程序不能正常运行 */
    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工作时,程序就会被异常终止。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咕咚.萌西

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值