由编译优化带来的boot跳转问题

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

本文分析说明编译优化对bootloader跳转的影响以及处理方法。

前两天刘兄发现一个隐藏在bootloader内的bug,并给出了最稳妥的解法。觉得该问题比较有意思,于是收集了相关资料进行分析供各位参考。

问题

Cortex-M7 由bootloader将引导xip image,在设置MSP后跳转前CPU挂掉。跳转代码如下

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
void boot_jump_run(void)
{
pfnImage reset_handle;


//一些状态通知代码
....

SCB_CleanDCache();

//取得要跳转的地址
reset_handle = (pfnImage)(*IMAGE_RESET_ADDRESS);
__ISB();
__disable_irq();

SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
/* Disable NVIC interrupts */
for (uint8_t i = 0; i < ARRAY_SIZE(NVIC->ICER); i++) {
NVIC->ICER[i] = 0xFFFFFFFF;
}
/* Clear pending NVIC interrupts */
for (uint8_t i = 0; i < ARRAY_SIZE(NVIC->ICPR); i++) {
NVIC->ICPR[i] = 0xFFFFFFFF;
}

//设置MSP
__set_MSP(*MSP_STORE_ADDRESS);
__set_CONTROL(0x00);

//执行跳转
reset_handle();
}

这段代码原本用来正常,这次将MSP修改到RAM的顶端就出现了异常,看下反汇编

1
2
3
4
5
6
0x600056fa <+166>:   msr     MSP, r3                         //设置MSP
0x600056fe <+170>: movs r3, #0
0x60005700 <+172>: msr CONTROL, r3
0x60005704 <+176>: mov r3, r4
0x60005706 <+178>: ldmia.w sp!, {r4, r5, r6, lr} //问题出在这里,跳转前在出栈
0x6000570a <+182>: bx r3 //跳转

问题就出在跳转前出栈,出栈时sp是递减,当MSP指向RAM顶端时,出栈sp递减访问范围将会超出RAM,无效地址导致CPU异常, 见下图2。
原本MSP不是指向RAM顶端,因此跳转没发现问题,加上原本的MSP指向内存的前面是有效内存,跳转后不会立即出问题,但在使用堆栈时会发生图1中阴影内存踩踏部分被入栈的数据改写,如果该部分已被其它地方使用将导致异常。

原因

原因分析

单从C语言看,执行reset_handle前没有什么特殊操作,为什么在编译后会插入一条出栈指令呢?基本上可以猜到是编译优化的问题,于是降低优化等级重新编译,果然就正常了

1
2
3
4
5
0x600056fa <+166>:   msr     MSP, r3     //设置MSP
0x600056fe <+170>: movs r3, #0
0x60005700 <+172>: msr CONTROL, r3
0x60005704 <+176>: blx r4 //跳转
0x60005706 <+178>: pop {r4, r5, r6, pc} //出栈在这里

是什么优化导致出栈被提前到跳转前呢?我们再看看boot_jump_run的全部反汇编

1
2
3
4
5
6
7
8
9
10
11
12
Dump of assembler code for function boot_jump_run:
0x60005654 <+0>: ldr r1, [pc, #176] ; (0x60005708 <boot_jump_run+180>)
0x60005656 <+2>: movs r0, #16
0x60005658 <+4>: push {r4, r5, r6, lr} #这里lr保存了boot_jump_run的返回地址,接下来调用report_status需要占用lr,因此要入栈保护
0x6000565a <+6>: bl 0x600059f0 <>
....
0x600056fa <+166>: msr MSP, r3 //设置MSP
0x600056fe <+170>: movs r3, #0
0x60005700 <+172>: msr CONTROL, r3
0x60005704 <+176>: mov r3, r4
0x60005706 <+178>: ldmia.w sp!, {r4, r5, r6, lr} //先出栈到lr,也就是boot_jump_run的返回地址
0x6000570a <+182>: bx r3 //再调用reset_handle

从汇编的注释分析我们知道在调用reset_handle前, lr内已经放的是boot_jump_run的返回地址,再继续看调用reset_handle汇编指令是bx,也就是说不会做pc+4->lr的动作,lr一直保存的是boot_jump_run的返回地址,因此在reset_handle执行完后使用指令ret返回时,跳到的时boot_jump_run的返回地址,再看看我们C的写法

1
2
3
4
5
6
7
8
9
void boot_jump_run(void)
{
pfnImage reset_handle;

...

//执行跳转
reset_handle();
}

reset_handle刚好是boot_jump_run最后一个调用函数,编译器就做了上面流程的优化,叫做尾调用优化.

尾调用优化

什么是尾调用

尾调用是指某个函数的最后一步是调用另一个函数,下面的goo是一个典型的尾调用

1
2
3
4
int foo(int x){
x++;
return goo(x);
}

注意尾调用只要求是函数的最后一步而并不一定是出现在函数尾部,例如下面的moo和noo都是尾调用

1
2
3
4
5
6
int foo(int x) {
if (x > 0) {
return moo(x)
}
return noo(x);
}

下面的情况都不是尾调用

1
2
3
4
5
6
7
8
9
10
// 情况一
int foo(int x){
int y = goo(x);
return y;
}

// 情况二
int foo(int x){
return goo(x) + 1;
}

尾调用优化有下面两个好处

  • 被调用函数执行完后,不需要再跳回调用函数
  • 由于被调用函数是最后一条指令,调用函数的堆栈不需要再保存,被调用函数直接用调用函数的栈,这样可以有效的节省内存空间,并且可以防止出现栈溢出

优化控制方式

在gcc中O1以上的优化就会启用尾调用优化,下面两个编译选项分别可以启用和关闭尾调用优化

1
2
-foptimize-sibling-calls
-fno-optimize-sibling-calls

原本只是知道尾调用优化会用到尾递归的代码写法用于节省堆栈,但在本案中对未递归也生效,但看看gcc文档的说明

-foptimize-sibling-calls
Optimize sibling and tail recursive calls.

支持同级的尾优化,做得够狠。

ARM的尾调用优化

对于尾调用优化的两个好处我们主要看重的是节省堆栈,但实际如果局部变量占用了堆栈反倒不进行同级尾调用优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
//测试代码
void boot_jump_run(void)
{
pfnImage reset_handle;
char test[20] = {0};

printf("%p", test);
....
__set_MSP(*MSP_STORE_ADDRESS);
__set_CONTROL(0x00);

reset_handle();
}

以上代码反汇编

1
2
3
4
5
6
7
8
9
10
11
12
Dump of assembler code for function boot_jump_run:
0x60005700 <+0>: push {r4, r5, r6, lr}
0x60005702 <+2>: movs r4, #0
0x60005704 <+4>: sub sp, #24 #堆栈上开辟局部变量数组test空间
0x60005706 <+6>: movs r2, #16
...
0x600057bc <+188>: msr MSP, r3
0x600057c0 <+192>: movs r3, #0
0x600057c2 <+194>: msr CONTROL, r3
0x600057c6 <+198>: blx r4 # 未进行尾调用优化, 如果进行,在这之前就应该放出堆栈
0x600057c8 <+200>: add sp, #24
0x600057ca <+202>: pop {r4, r5, r6, pc}

因此可见在cortex-m7下的同级尾调用优化是一个鸡肋,并没有获得多大好处

解决方法

知道了原因就容易解决了,下面几种解决方法

方法1:使用汇编精准控制

作为跳转,我们知道只是要跳转到指定位置执行,SP也被重新初始化,原来的堆栈已经没有意义,直接使用汇编控制跳转,这也是最安全没有歧义的方法,也是最推荐的方法.

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
void boot_jump_run(void)
{
pfnImage reset_handle;

SCB_CleanDCache();

reset_handle = (pfnImage)(*IMAGE_RESET_ADDRESS);
__ISB();
__disable_irq();

SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
/* Disable NVIC interrupts */
for (uint8_t i = 0; i < ARRAY_SIZE(NVIC->ICER); i++) {
NVIC->ICER[i] = 0xFFFFFFFF;
}
/* Clear pending NVIC interrupts */
for (uint8_t i = 0; i < ARRAY_SIZE(NVIC->ICPR); i++) {
NVIC->ICPR[i] = 0xFFFFFFFF;
}

__set_MSP(*MSP_STORE_ADDRESS);
__set_CONTROL(0x00);

//嵌入汇编跳到指定位置运行
__asm volatile ("BX %0" : : "r" (reset_handle) : );
}

方法2:降低优化等级

三种方式
1.可以在编译的时候指定使用Og或以下的优化等级,但这样打击面太广。

1
-Og

2.可以在编译的时候维持高的优化等级单独关闭尾调用优化。

1
-Os -fno-optimize-sibling-calls

3.另外可以进一步缩小范围,只让boot_jump_run不进行尾调用优化

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma GCC push_options
#pragma GCC optimize ("no-optimize-sibling-calls")
void boot_jump_run(void)
{
pfnImage reset_handle;
...

__set_MSP(*MSP_STORE_ADDRESS);
__set_CONTROL(0x00);

reset_handle();
}
//#pragma GCC pop_options

方法3:破坏尾调用

是尾调用引起的,我们就让跳转不是尾调用方式

1
2
3
4
5
6
7
8
9
10
11
void boot_jump_run(void)
{
pfnImage reset_handle;
...

__set_MSP(*MSP_STORE_ADDRESS);
__set_CONTROL(0x00);

reset_handle();
__builtin_unreachable();
}

总结

涉及到底层相关的流程为了不被编译器引入歧义,最好使用汇编进行精确控制, 因此推荐使用方法1。但一些情况下为了多架构的可移植性,想要保留C代码形式的跳转方式也会采用方法2,例如mcuboot就是使用的Og优化等级。

GCC优化等级

分析本案的时候,查了一下Gcc的优化等级这里记录一下

  1. 可指定优化等级:-O0、-O1、-O2、-O3、-Og、-Os、-Ofast。
  2. 未指定时则默认为 -O0,无优化。O后面数字越大优化程度越高
  3. -Og在-O1的基础上,去掉影响调试的优化,最终是为了调试程序,可以使用这个参数。
  4. -Os在-O2的基础上,去掉会导致最终可执行程序增大的优化,生成小尺寸的可执行文件。
  5. -Ofast在-O3的基础上,添加了一些非常规优化例如对数学函数等,达到提高执行速度的目的。
    使用 gcc -Q –help=optimizers 命令来查询具体做了哪些优化,例如:
    1
    gcc -Q --help=optimizers -O2

参考

https://zh.wikipedia.org/wiki/%E5%B0%BE%E8%B0%83%E7%94%A8
https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Optimize-Options
https://www.drdobbs.com/tackling-c-tail-calls/184401756
https://www.pspace.org/a/thesis/baueran_thesis.pdf
https://developer.arm.com/documentation/ddi0406/c/Application-Level-Architecture/Instruction-Details/Alphabetical-list-of-instructions/BX
https://developer.arm.com/documentation/ddi0406/c/Application-Level-Architecture/Instruction-Details/Alphabetical-list-of-instructions/BLX--register-