本文分析说明编译优化对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
32void 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
60x600056fa <+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
50x600056fa <+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
12Dump 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
9void boot_jump_run(void)
{
pfnImage reset_handle;
...
//执行跳转
reset_handle();
}
reset_handle
刚好是boot_jump_run最后一个调用函数,编译器就做了上面流程的优化,叫做尾调用优化.
尾调用优化
什么是尾调用
尾调用是指某个函数的最后一步是调用另一个函数,下面的goo是一个典型的尾调用1
2
3
4int foo(int x){
x++;
return goo(x);
}
注意尾调用只要求是函数的最后一步而并不一定是出现在函数尾部,例如下面的moo和noo都是尾调用1
2
3
4
5
6int 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
12Dump 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
26void 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
11void 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的优化等级这里记录一下
- 可指定优化等级:-O0、-O1、-O2、-O3、-Og、-Os、-Ofast。
- 未指定时则默认为 -O0,无优化。O后面数字越大优化程度越高
- -Og在-O1的基础上,去掉影响调试的优化,最终是为了调试程序,可以使用这个参数。
- -Os在-O2的基础上,去掉会导致最终可执行程序增大的优化,生成小尺寸的可执行文件。
- -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-