88、【OS】【Nuttx】【启动】栈溢出保护:volatile 关键字(修饰内联汇编)
【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
背景
接之前 blog
【OS】【Nuttx】【启动】栈溢出保护:volatile 关键字(修饰变量)
分析了 volatile 关键字对变量的修饰作用,下面再来看 volatile 关键字的下一个用法,也是之前分析栈溢出时语句的用法
volatile 关键字
还是回到这张图
上篇 blog 分析了 volatile 关键字修饰变量,表示这个寄存器变量的值可能会在任何时刻被意想不到的方式改变,在每次访问该变量时都要从内存中重新读取其值,而不是用存储在寄存器中的副本,那么上面这张图就是 volatile 关键字的另一个用法,修饰内联汇编语句
修饰内联汇编
来看下 gcc 官方文档对这块的描述
有几个点:
- 扩展汇编语句的典型用途是对输入值进行处理,生成输出值,比如将 C 变量与汇编指令中的寄存器绑定
- 但有些汇编语句也可能产生副作用,这些典型的副作用包括修改硬件状态,写入寄存器,改变内存,触发中断等,这些副作用是编译器感知不到,或者说判断不了是否有用的,此时编译器对这些汇编语句可能就会产生优化行为,比如删掉这段汇编代码
- 此时就需要用 volatile 关键字来禁用掉编译器的某些优化,这些优化包括删除看起来没用的汇编语句,重排指令顺序,合并重复操作等等,尤其在汇编语句没有输出(但有副作用,比如写硬件寄存器)的情况下,编译器可能认为这条语句没用(注意这里是可能,不是一定,取决于编译器优化的保守程度),而把它优化掉,此时 volatile 关键字就会告诉编译器:这段汇编很重要,不要优化,一定要按顺序执行
下面对上面三个点再详细展开举例下
扩展汇编的典型用途
典型用途:将 C 变量与汇编指令中的寄存器绑定
这种用法比那种只有副作用的汇编语句相对来说更“安全”,因为编译器此时能感知到 C 变量在其中起作用了,但也只是相对的,取决于编译器的优化程度和保守程度,最佳的编程实践还是所有内联汇编语句都加上 volatile,明确告诉编译器不要优化
比如下面这段代码,就是 C 变量与内联汇编混用的典型场景
// main.c
#include <stdio.h>int main(void) {int src = 1;int dst; __asm__ volatile("mov %1, %0\n\t" : "=r" (dst) : "r" (src));printf("%d\n", dst);
}
带有副作用的内联汇编
典型副作用:修改硬件状态,写入寄存器,改变内存,触发中断等
这些副作用对编译器来说相当于是未定义的,编译器对这些汇编语句可能产生优化行为,从用户使用角度来说,肯定是希望同一份代码能支持不同编译器,别编译器 A 编译后能保留这些汇编语句,换了编译器 B 这些汇编语句可能就被优化掉,所以最佳的实践还是都加上 volatile,明确告诉编译器不要优化
比如下面这段代码,不关联任何 C 变量,只对通用寄存器进行操作
// main.c
#include <stdio.h>int main(void) {__asm__ volatile ("sub r10, sp, %0" : :"r"(64) :);return 0;
}
在终端 bash 中输入命令(这里 --specs=nosys.specs 表示暂时不需要系统调用功能,告诉链接器忽略相关未定义的符号)
arm-none-eabi-gcc -mthumb -O3 -mcpu=cortex-m4 --specs=nosys.specs main.c -o main
再反汇编,终端输入
arm-none-eabi-objdump -d main > main.s
可以看到汇编语句中,可以看到最终的汇编语句明确保留了下来(不加 volatile 不是说一定会被优化,对 gcc 编译器来说,gcc 对 asm 处理比较保守,特别是当不能确定是否会有副作用时,对用户来说,加上 volatile 肯定是最佳的编程实践,就像家庭电气回路里的保险丝一样,你不知道它啥时候出现电流过载,加上是最保险的)
先到这里,下篇 blog 回到栈溢出继续