Zephyr下MPU问题分析

Creative Commons
本作品采用知识共享署名

本文记录在Zephyr下因开启CONFIG_HW_STACK_PROTECTION引起的MPU fault问题的分析调试过程。

最近Zephyr上rt系列的芯片板子默认开启了CONFIG_HW_STACK_PROTECTION,该功能是利用MPU在堆栈顶部加入写保护,防止线程堆栈溢出,基本原理可以参考Zephyr使用的堆栈保护技术中硬件保护小节。在开启该功能后,mm_swiftio板子上就出现了运行不起来,由于分析走了不少弯路花了不少时间,因此这里记录整个分析和和调试问题以帮助总结经验教训。注:本文运行的环境未引入userspace,因此所有分析的代码都移除userspace相关。

现象描述

配置CONFIG_HW_STACK_PROTECTION=y后,开机运行bootloader信息显示完后没有任何打印提示信息,串口shell无响应。

调试过程

1. 检查无响应原因

直线思维的失败

因为是开启了CONFIG_HW_STACK_PROTECTION后出的问题,所以基本可以将问题范围缩小在MPU内。但由于串口没有任何提示,只好使用DAPLink直接attached查看是挂在什么地方,方法参考Zephyr烧写调试环境切换到Windows,attach上后可以看到backtrace:

好吧,我承认看了个寂寞,只是一个fault, dump出来原因是0, 完全没帮助啊,何况都大概可以猜到是mpu保护出问题,肯定会进mpu fault。

2. 排除

既然知道是配置CONFIG_HW_STACK_PROTECTION导致的,哪就去找对用CONFIG_HW_STACK_PROTECTION做实际保护的代码:mpu保护的两个函数均在arch/arm/core/aarch32/cortex_m/mpu/arm_core_mpu.c中,分别是z_arm_configure_static_mpu_regionsz_arm_configure_dynamic_mpu_regions,由于我的运行环境的配置都不涉及到static region,因此排查重点是z_arm_configure_dynamic_mpu_regions, 先简单的移除了arch_switch_to_main_thread中的z_arm_configure_dynamic_mpu_regions,再运行一次,这次能看到一句log了:

* delaying boot 50ms (per build configuration) *
分析一下z_arm_configure_dynamic_mpu_regions代码,可以看到是对thread的stack进行guard保护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void z_arm_configure_dynamic_mpu_regions(struct k_thread *thread)
{
struct k_mem_partition *dynamic_regions[_MAX_DYNAMIC_MPU_REGIONS_NUM];

uint8_t region_num = 0U;

#if defined(CONFIG_MPU_STACK_GUARD)
/* Define a stack guard region for either the thread stack or the
* supervisor/privilege mode stack depending on the type of thread
* being mapped.
*/
struct k_mem_partition guard;

/* Privileged stack guard */
uint32_t guard_start;
uint32_t guard_size = MPU_GUARD_ALIGN_AND_SIZE; //stack guard大小


guard_start = thread->stack_info.start - guard_size; //stack guard开始地址

//创建只读保护region
guard = (const struct k_mem_partition)
{
guard_start,
guard_size,
K_MEM_PARTITION_P_RO_U_NA
};
dynamic_regions[region_num] = &guard;

region_num++;
#endif /* CONFIG_MPU_STACK_GUARD */

//配置MPU regin
arm_core_mpu_configure_dynamic_mpu_regions(
(const struct k_mem_partition **)dynamic_regions,
region_num);
}

既然移除了main thread的保护可以继续向下走,说明还有其它thread设置mpu guard保护导致,于是直接将所有调用z_arm_configure_dynamic_mpu_regions的地方都移除,另外一个地方在arch/arm/core/aarch32/swap_helper.S内z_arm_pendsv中调用,为了方便直接在z_arm_configure_dynamic_mpu_regions内进行return,不做后面的mpu配置,再运行一次,未出现MPU fault,运行正常。这里基本就可以确认是为线程stack guard MPU导致,分析到这里感觉之后比较容易了,只要在z_arm_configure_dynamic_mpu_regions中依次针对thread屏蔽设置MPU的动作,就可以知道是哪个thread导致的,可惜失算了,只要有一个thread在设置guard MPU都会出MPU fault。

获取thread保护信息

到这一步就开始怀疑是不是guard设置有问题,超过了实际的保护区域,在代码中添加打印出来对比,将z_arm_configure_dynamic_mpu_regions修改为如下,让其只打印堆栈信息和保护guard信息,不做实际的mpu设置
void z_arm_configure_dynamic_mpu_regions(struct k_thread *thread)

1
2
3
4
5
6
7
8
9
10
{
int key = irq_lock();
printk("%s stack %x(%x) protect %x(%x)\r\n", thread->name,
thread->stack_info.start,
thread->stack_info.size,
guard_start,
guard_size);
irq_unlock(key);
return;
}

运行时打印出来各个thread的保护信息如下

main stack 805132c0(40000) protect 805132a0(20)
shell_uart stack 804d32a0(40000) protect 804d3280(20)
idle 00 stack 805532e0(400) protect 805532c0(20)
使用下面命令查看zephyr中对应的stack的信息
nm -n build/zephyr/zephyr.elf
得到信息如下
804cf260 B timer_isr_stack
804d3280 b $d
804d3280 b shell_uart_stack
805132a0 b $d
805132a0 B z_main_stack
805532c0 b $d
805532c0 b z_idle_stacks
805536e0 b $d
805536e0 B z_interrupt_stacks
可以看到使用堆栈和保护区域也正常。

加大堆栈

到这里就怀疑是不是真的堆栈有移除,加大堆栈问题任然存在。

3. 对比

到上面的几步分析后就没有进一步头绪了,到这里突然想起我的程序是跑在sdram中,因为zephyr master上的配置都是跑flash,那么对比一下跑在flash如何,修改程序配置,让程序以XIP的方式运行,正常!!相同的代码只在SDRAM中运行有问题,哪应该是对sdram和flash的mpu保护导致,查看代码arch/arm/core/aarch32/cortex_m/mpu/arm_mpu.c中的arm_mpu_init 会对sdram/flash进行基本的mpu保护设置

1
2
3
4
5
6
7
8
static int arm_mpu_init(const struct device *arg)
{
arm_core_mpu_disable();
for (r_index = 0U; r_index < mpu_config.num_regions; r_index++) {
region_init(r_index, &mpu_config.mpu_regions[r_index]); //按照mpu_config的信息进行保护设置
}
arm_core_mpu_enable();
}

在soc/arm/common/cortex_m/arm_mpu_regions.c中有mpu_config的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static const struct arm_mpu_region mpu_regions[] = {
/* Region 0 */

MPU_REGION_ENTRY("FLASH_0",
CONFIG_FLASH_BASE_ADDRESS,
#if defined(CONFIG_ARMV8_M_BASELINE) || defined(CONFIG_ARMV8_M_MAINLINE)
REGION_FLASH_ATTR(CONFIG_FLASH_BASE_ADDRESS, \
CONFIG_FLASH_SIZE * 1024)),
#else
REGION_FLASH_ATTR(REGION_FLASH_SIZE)),
#endif

/* Region 1 */
MPU_REGION_ENTRY("SRAM_0",
CONFIG_SRAM_BASE_ADDRESS,
#if defined(CONFIG_ARMV8_M_BASELINE) || defined(CONFIG_ARMV8_M_MAINLINE)
REGION_RAM_ATTR(CONFIG_SRAM_BASE_ADDRESS, \
CONFIG_SRAM_SIZE * 1024)),
#else
REGION_RAM_ATTR(REGION_SRAM_SIZE)),
#endif

};

这里将这些regin信息移除,不做MPU保护,再在sdram中运行,没有问题!在include/arch/arm/aarch32/cortex_m/mpu/arm_mpu_v7m.h中查看REGION_RAM_ATTR定义确实没有可执行权限

1
2
3
#define REGION_RAM_ATTR	  {((MPU_REGION_SU_RW) | \
((UM_READ | UM_WRITE) << BM3_UM_SHIFT) | \
(BM4_PERMISSIONS))}

但这就诡异了在没有配置guard mpu保护时,SDRAM虽然没有被给予可执行权限,却可以执行,而一旦配置guard MPU就不行了这两者会有关联?又陷入无头绪中。

4. 查看所有MPU信息

打印配置

查看所有MPU信息,可以借助MPU模块本身的log:进行如下配置:

1
2
CONFIG_MPU_LOG_LEVEL_DBG=y
CONFIG_LOG_MINIMAL=y

CONFIG_MPU_LOG_LEVEL_DBG是开启Mpu本身的log信息,而使用CONFIG_LOG_MINIMAL是让log不通过thread转送,直接送到printk打印。
由于更关心MPU初始化阶段的情况,而默认情况下MPU的初始化早于console还没有初始化,因此不会有任何输出,在配置文件中做如下修改:

1
2
CONFIG_EARLY_CONSOLE=y
CONFIG_UART_CONSOLE_INIT_PRIORITY=39

其中CONFIG_EARLY_CONSOLE是让console在PRE_KERNEL_1阶段初始化,由于mpu的初始化也在该阶段,因此将CONFIG_UART_CONSOLE_INIT_PRIORITY设置为39, 让其小于CONFIG_KERNEL_INIT_PRIORITY_DEFAULT(40),保证能在mpu初始化之前完成

1
2
SYS_INIT(arm_mpu_init, PRE_KERNEL_1,
CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);

端倪初现

配置后未加任何额外打印,运行出的log如下,没什么特别的,就是在设置MPU regin 2的guard后挂掉的

D: total region count: 16
D: [0] 0x80000000 0x07020032
D: [1] 0x80000000 0x110b0030
D: [2] 0x805132e0 0x150b0008
E: * HARD FAULT *
E: Fault escalation (see below)
E: * MPU FAULT *
E: Instruction Access Violation
E: r0/a1: 0x801d2dd4 r1/a2: 0x00000008 r2/a3: 0x00000000
E: r3/a4: 0xe000ed90 r12/ip: 0x00000000 r14/lr: 0x8000d0ad
E: xpsr: 0x81000000
E: Faulting instruction address (r15/pc): 0x8001e838
E: >>> ZEPHYR FATAL ERROR 0: CPU exception on CPU 0
E: Current thread: 0x8020d0a0 (main)
E: Halting system
再仔细的看下诊断原因,是指令访问无效,和前面分析的信息也呼应了,配置guard后SDRAM就被MPU保护起来不允许执行。
这里就对实际设置MPU regin的过程比较好奇了,从thread guard呼叫流程如下:
z_arm_configure_dynamic_mpu_regions->arm_core_mpu_configure_dynamic_mpu_regions->mpu_configure_dynamic_mpu_regions,最后的函数在arch/arm/core/aarch32/cortex_m/mpu/arm_mpu_v7_internal.h内展开如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static int mpu_configure_dynamic_mpu_regions(const struct k_mem_partition
*dynamic_regions[], uint8_t regions_num)
{
int mpu_reg_index = static_regions_num;

printk("mpu_reg_index start %d\r\n", mpu_reg_index);

//配置实际的mpu regin
mpu_reg_index = mpu_configure_regions(dynamic_regions,
regions_num, mpu_reg_index, false);

printk("mpu_reg_index end %d\r\n", mpu_reg_index);

//关闭没有使用的regin
if (mpu_reg_index != -EINVAL) {
/* Disable the non-programmed MPU regions. */
for (int i = mpu_reg_index; i < get_num_regions(); i++) {
printk("clean %d\r\n", i);
ARM_MPU_ClrRegion(i);
}
}

printk("mpu_reg_index clear end %d\r\n", mpu_reg_index);
return mpu_reg_index;
}

然后允许看打印,结果就有点意思了:

mpu_reg_index start 2
D: Configure MPU region at index 0x2
D: Program MPU region at index 0x2
D: [2] 0x805132e0 0x150b0008
mpu_reg_index end 3
clean 3
clean 4
clean 5
clean 6
clean 7
E: * HARD FAULT *
最后一句mpu_reg_index clear end没出来,另外regin number总数为16, 清到7就fault了。MPU内的信息肯定有问题,凶手的尾巴已经漏出来了。

5. 定位

要继续分析,需要理解cortex-m7 MPU的一些基础知识:

MPU相关基础知识

这里只介绍和分析本问题相关的基础知识,后文要理解寄存器的值,可以参考<<Arm® Cortex®-M7 Devices
Generic User Guide>>的4.6 Optional Memory Protection Unit.

  1. MPU以regin对内存进行保护,支持8/16个regin(我用的rt1052有16个)
  2. 通过向MPU regin 寄存器写内存的起始地址,大小和属性,对内存进行保护
  3. 如果设置regin有重叠,regin index大的覆盖小的
  4. 读写某个regin的RBAR和RASR寄存器前,需要先用RNR指向该寄存器

定位问题点

前面知道MPU内的信息有问题,那么肯定要dump出来看一下,因此在mpu初始化前,使用下面代码读出MPU寄存器查看

1
2
3
4
5
6
7
8
9
10
11
12
static void dump_mpu(void)
{
#define PRINTF printk
PRINTF("MPU->TYPE 0x%x\r\n", MPU->TYPE);
PRINTF("MPU->CTRL 0x%x\r\n", MPU->CTRL);
PRINTF("MPU->RNR 0x%x\r\n", MPU->RNR);
for(int i = 0; i < get_num_regions(); i++){
MPU->RNR = i;
PRINTF("region %d, RBAR 0x%x RASR 0x%x\r\n",i,
MPU->RBAR, MPU->RASR);
}
}

查看log果然发现问题, 在zephyr尚未初始化MPU前,前面7个regin都已经被配置了,而且MPU是打开的

MPU->TYPE 0x1000
MPU->CTRL 0x5
MPU->RNR 0xf
region 0, RBAR 0x80000000 RASR 0x3100039
region 1, RBAR 0x60000001 RASR 0x3100039
region 2, RBAR 0x60000002 RASR 0x6030033
region 3, RBAR 0x3 RASR 0x310003b
region 4, RBAR 0x4 RASR 0x3030021
region 5, RBAR 0x20000005 RASR 0x3030021
region 6, RBAR 0x20200006 RASR 0x3030023
region 7, RBAR 0x80000007 RASR 0x3030031
region 8, RBAR 0x8 RASR 0x0
region 9, RBAR 0x9 RASR 0x0
region 10, RBAR 0xa RASR 0x0
region 11, RBAR 0xb RASR 0x0
region 12, RBAR 0xc RASR 0x0
region 13, RBAR 0xd RASR 0x0
region 14, RBAR 0xe RASR 0x0
region 15, RBAR 0xf RASR 0x0
这里基本就知道原因了,代码是由Bootloader从SD卡加载到SDRAM中再跳转运行的,在跳转前没有将MPU清干净,导致出现诡异问题。

问题现象解释

  1. 为什么在SDRAM中执行,Zephyr初始化没有给与在SDRAM区域可执行权限,也能执行代码
    因为bootloader残留的regin 7覆盖了regin 1的权限,中给与了SDRAM(0x8000000)了可执行代码的权限。
  2. 为什么设置guard后SDRAM没有可执行代码的权限
    设置guard是在 regin 2,Zephyr将2之后的regin做为不使用regin进行clean,这个动作将regin 7给禁用了,因此SDRAM失去的可执行权限,立马出现MPU Fault。

解决方法

  1. 在bootloader跳转前清除所有的regin信息,并关闭MPU。虽然以目前的配置跳转到zephyr后正确设置MPU也不会由问题,但bootloader跳转前清除现场是最安全的做法,也避免出现类似本文的问题,难以分析。
  2. 在Zephyr以非XIP运行,开启MPU的情况下,对text段进行可执行权限的MPU regin配置。

总结

  1. bootloader跳转前一定要清除所有现场。
  2. 出现MPU问题,第一步先在各个可疑点位dump所有MPU寄存器信息。