1. 内嵌汇编介绍
内嵌汇编是代码优化时的常见手段,它是指在 C代码 中嵌入汇编代码,从而使得代码更加紧凑,避免一些无效操作,有时能够满足一些特殊的代码需求,这也是为后面的neon优化做基础准备。笔者觉得掌握内嵌汇编是一名嵌入式工程师应该必备的技能,进行优化代码,退能看汇编调bug,实属码农居家必备良技
PS:本文需要有一定的裸机汇编基础才能阅读
2. 内嵌汇编语法
不同的C编译器内联汇编代码时,它们的写法是各不相同的,下面为gcc写法
内嵌汇编语法如下,其中汇编语句模板必不可少,其他三部分可选,如果使用了后面的部分,而前面部分为空,也需要用: (冒号)
格开,相应部分内容为空
语法如下:
asm(
(asm code)
:(output)
:(input)
:(clobber)
)
asm: __asm__或asm用来声明一个内联汇编表达式,所以任何一个内联汇编表达式都以它开头,是必不可少的。
asm code: 由汇编语句序列组成,语句之间使用“;”分开,指令中的操作数可以使用占位符引用C语言变量,操作数占位符最多10个,名称如下:%0,%1…,%9
output: 输出部分描述输出操作数,不同的操作数描述符之间用逗号格开,每个操作数描述符由限定字符串和C语言变量组成。每个输出操作数的限定字符串必须包含“=”表示他是一个输出操作数。
input: 输入部分描述输入操作数,不同的操作数描述符之间使用逗号格开,每个操作数描述符由限定字符串和C语言表达式或者C语言变量组成。
clobber: 这部分常常以“memory”为约束条件,以表示操作完成后内存中的内容已有改变,如果原来某个寄存器的内容来自内存,那么现在内存中这个单元的内容已经改变(参考3.3章节)
需要注意的是,使用 asm 关键字声明一段汇编代码后, 可以使用 volatile 向GCC声明不允许对该内联汇编优化,否则,当使用优化选项(-o)进行编译时GCC会根据字自己的判断决定是否将内联汇编表达式的指令优化掉。
下面会通过简单的例子来解释
3. 内嵌汇编要素
3.1 操作符
操作符通常用于声明内嵌汇编中变量的属性
r: 通用寄存器r0-r15,表示需要将变量与某个通用寄存器相关联,先将变量的值读入寄存器,然后在指令中使用相应寄存器
m: 一个有效的内存地址,表示操作数是在内存中,在编译时将直接使用内存中的变量,而不会像"r"操作符一样将其读入寄存器,一般是使用对内存进行操作的指令时会才使用
I: 数据处理指令中的立即数
X: 被修饰的操作符只能用作输出
3.2 修饰符
修饰符通常用于声明操作符支持的属性
无修饰符:此类操作符是只读的,输入部分必须为read-only,C编译器是没有能力做这个检查
=: 被修饰的操作符只写, 输出部分必须为write-only,相应C表达式的值必须是左值,C编译器是没有能力做这个检查
+: 被修饰的操作符具有可读可写的属性
&: 被修饰的操作符能被作为输出
3.3 clobber
clobber 简单来说就是声明了哪些变量或者寄存器被修改了,从而告知编译器的编译代码的时候不要使用该寄存器或者变量, 避免发生不可预知的错误
比如我不想寄存器使用r0,则在clobber中声明r0,那么编译器则会选择其他寄存器
常用的 clobber 如下:
memory: 表示修改了内存中的数据,这将强迫编译器在执行汇编代码前存储所有缓存的值,然后在执行完汇编代码后重新加载该值
rn: 表示修改了寄存器rn(n寄存器标号)
cc: 表示修改了cpsr等标志类寄存器,它用来向编译器指明,内嵌汇编指令改变了内存中的值。这将强迫编译器在执行汇编代码前存储所有缓存的值,然后在执行完汇编代码后重新加载该值
4. 内嵌汇编例子讲解
4.1 mem_to_mem
通过一个简单的例子来理解上面的语法, 该例子的作用是使用汇编将变量值在内存中移动
void asm_mem_to_mem(void)
{
unsigned int var_1 = 0;
unsigned int var_2 = 999;
unsigned int var_3 = 0;
__asm__ __volatile__
(
"mov %0, #20;" //asm code
"mov %2, %1;"
: "=r" (var_1), "=r"(var_3)
: "r" (var_2)
: "memory"
);
}
- asm code : 其中%0、%1等均表示变量
- output: 声明输出变量,操作符 r 表示使用任何寄存器,修饰符 = 号 表示只写,如果不添加 = 号,编译会提示
output operand constraint lacks ‘=’
- input : 声明输出变量,操作符 r 表示使用任何寄存器来传递变量
- clobber:memory 表示修改了内存
使用操作符r来传递变量的意思指内存中变量的值如何进入汇编中进行运算,因为 arm 汇编的所有计算都是在寄存器中进行的,不能够直接从内存中进行计算,所以需要先将 变量 从内存中读取到寄存器中,而传递变量的意思是使用 ldr 指令时,需要指定寄存器来装载变量,那么使用 操作符r 来声明该变量可以使用任何寄存器来装载
4.2 register_to_mem
读取寄存器中的值到内存
void asm_register_to_mem(int param_0, int param_1, int param_2)
{
int var_0 = 0;
int var_1 = 0;
int var_2 = 0;
/* 1. 错误例子,没有声明寄存器被修改 */
__asm__ __volatile__
(
"str r0, [%0];" //r0 -> var_0
"str r1, [%1];" //r1 -> var_1
"str r2, [%2];" //r2 -> var_2
:
:"r"(&var_0), "r"(&var_1), "r"(&var_2)
:"memory"
);
/*
0x000103c8 <+4>: add r7, sp, #0
0x000103ca <+6>: str r0, [r7, #12]
0x000103cc <+8>: str r1, [r7, #8]
0x000103ce <+10>: str r2, [r7, #4]
0x000103d0 <+12>: movs r3, #0
0x000103d2 <+14>: str r3, [r7, #28]
0x000103d4 <+16>: movs r3, #0
0x000103d6 <+18>: str r3, [r7, #24]
0x000103d8 <+20>: movs r3, #0
0x000103da <+22>: str r3, [r7, #20]
0x000103dc <+24>: add.w r3, r7, #28
0x000103e0 <+28>: add.w r2, r7, #24//r2被修改
0x000103e4 <+32>: add.w r1, r7, #20//r1被修改
0x000103e8 <+36>: str r0, [r3, #0]
0x000103ea <+38>: str r1, [r2, #0]
0x000103ec <+40>: str r2, [r1, #0]
从汇编代码可知, 因为没有在 clobber 中声明寄存器被修改
所以编译器在汇编代码时使用了装有 变量 的寄存器 r1、r2,导致 r1、r2 中的值被修改,从而得不到正确值
*/
/* 2. 正确例子 */
__asm__ __volatile__
(
"str r0, [%0];" //r0 -> var_0
"str r1, [%1];" //r1 -> var_1
"str r2, [%2];" //r2 -> var_2
:
:"r"(&var_0), "r"(&var_1), "r"(&var_2)
:"memory", "r0", "r1", "r2"
);
/*
0x0001046c <+4>: add r7, sp, #8
0x0001046e <+6>: str r0, [r7, #12]
0x00010470 <+8>: str r1, [r7, #8]
0x00010472 <+10>: str r2, [r7, #4]
0x00010474 <+12>: movs r3, #0
0x00010476 <+14>: str r3, [r7, #28]
0x00010478 <+16>: movs r3, #0
0x0001047a <+18>: str r3, [r7, #24]
0x0001047c <+20>: movs r3, #0
0x0001047e <+22>: str r3, [r7, #20]
/* r0没有被修改,编译器转而使用r3来进行操作 */
0x00010480 <+24>: add.w r3, r7, #28
/* r1没有被修改,编译器转而使用r4来进行操作 */
0x00010484 <+28>: add.w r4, r7, #24
/* r2没有被修改,编译器转而使用r5来进行操作 */
0x00010488 <+32>: add.w r5, r7, #20
/* 将r0的值装入内存中的变量 */
0x0001048c <+36>: str r0, [r3, #0]
/* 将r1的值装入内存中的变量 */
0x0001048e <+38>: str r1, [r4, #0]
/* 将r2的值装入内存中的变量 */
0x00010490 <+40>: str r2, [r5, #0]
*/
/* 3. 错误例子,没有声明寄存器被修改*/
__asm__ __volatile__
(
"mov %0, r0;" //r0 -> var_0
"mov %1, r1;" //r1 -> var_1
"mov %2, r2;" //r2 -> var_2
:"=r"(var_0), "=r"(var_1), "=r"(var_2) //输入部分,表示变量var输入到r0-r15中的一个寄存器
:
:"memory"
);
/*
0x00010470 <+8>: str r1, [r7, #8]
0x00010472 <+10>: str r2, [r7, #4]
0x00010474 <+12>: movs r3, #0
0x00010476 <+14>: str r3, [r7, #28]
0x00010478 <+16>: movs r3, #0
0x0001047a <+18>: str r3, [r7, #24]
0x0001047c <+20>: movs r3, #0
0x0001047e <+22>: str r3, [r7, #20]
0x00010480 <+24>: mov r1, r0 //r0被修改
0x00010482 <+26>: mov r2, r1 //r1被修改
0x00010484 <+28>: mov r3, r2 //r2被修改
0x00010486 <+30>: str r1, [r7, #28]
0x00010488 <+32>: str r2, [r7, #24]
0x0001048a <+34>: str r3, [r7, #20]
0x0001048c <+36>: ldr r3, [r7, #20]
0x0001048e <+38>: str r3, [sp, #4]
0x00010490 <+40>: ldr r3, [r7, #24]
0x00010492 <+42>: str r3, [sp, #0]
0x00010494 <+44>: ldr r3, [r7, #28]
*/
/* 4. 正确例子*/
__asm__ __volatile__
(
"mov %0, r0;" //r0 -> var_0
"mov %1, r1;" //r1 -> var_1
"mov %2, r2;" //r2 -> var_2
:"=r"(var_0), "=r"(var_1), "=r"(var_2) //输入部分,表示变量var输入到r0-r15中的一个寄存器
:
/* 告诉编译器,寄存器r0, r1, r2已经被修改了。不要将r0, r1, r2用于操作 */
:"memory", "r0", "r1", "r2"
);
/*
0x000103c6 <+2>: sub sp, #36 ; 0x24
0x000103c8 <+4>: add r7, sp, #0
0x000103ca <+6>: str r0, [r7, #12]
0x000103cc <+8>: str r1, [r7, #8]
0x000103ce <+10>: str r2, [r7, #4]
0x000103d0 <+12>: movs r3, #0
0x000103d2 <+14>: str r3, [r7, #28]
0x000103d4 <+16>: movs r3, #0
0x000103d6 <+18>: str r3, [r7, #24]
0x000103d8 <+20>: movs r3, #0
0x000103da <+22>: str r3, [r7, #20]
0x000103dc <+24>: mov r5, r0
0x000103de <+26>: mov r4, r1 //r1没有被修改,编译器转而使用r4来进行操作
0x000103e0 <+28>: mov r3, r2 //r2没有被修改,编译器转而使用r3来进行操作
0x000103e2 <+30>: str r5, [r7, #28]
0x000103e4 <+32>: str r4, [r7, #24]
0x000103e6 <+34>: str r3, [r7, #20]
*/
}
4.2 mem_to_cpsr
修改 cpsr寄存器 中的值
void asm_mem_to_cpsr(void)
{
int cpsr_status = 0x80;
__asm__ __volatile__
(
"msr cpsr,%0"
:
: "r" (cpsr_status)//声明输入
: "cc"
);
/* 汇编代码
0x000103d6 <+2>: sub sp, #12
0x000103d8 <+4>: add r7, sp, #0
0x000103da <+6>: movs r3, #128 ; 0x80
0x000103dc <+8>: str r3, [r7, #4]
0x000103de <+10>: ldr r3, [r7, #4]
0x000103e0 <+12>: msr CPSR_fc, r3
0x000103e4 <+16>: nop
*/
}
4.3 cpsr_to_mem
读取 cpsr寄存器 中的值
void asm_cpsr_to_mem(void)
{
int cpsr_status = 0;
asm
(
"mrs %0, cpsr;"
:"=r" (cpsr_status)//声明输出
:
: "memory"//告诉编译器内存已经被修改了
);
}
4.4 内存屏障
# define barrier() _asm__volatile_("": : :"memory")
内存屏障向GCC声明,内存做了改动,GCC在编译的时候,会将此因素考虑进去。在访问IO端口和IO内存时,会用到内存屏障。它就是防止编译器对读写IO端口和IO内存指令的优化而实际的错误。
其原理是保留程序的执行顺序,因为在使用了带有 memory clobber 的 asm 声明后,所有变量的内容都是不可预测的。编译器将按顺序编译语句
5. 调试流程
笔者使用的是经典的调试器 gdb 进行调试, 其步骤如下
设置断点在main函数 b main
将程序运行到 asm_test_input 处 run
汇编级执行 stepi n(n是运行的步数,如果不写则默认1步)
打印内存 x /nxw addr (笔者使用命令来打印堆栈中的数值,其中n是打印个数,具体请参靠gdb的命令说明)
打印寄存器 info register
打印汇编 disassemble
6. 参考资料
ARM嵌入式开发中的GCC内联汇编asm:https://www.cnblogs.com/fengliu-/p/7667892.html
asm volatile内嵌汇编用法简述:https://blog.csdn.net/geekcome/article/details/6216436
ARM汇编-从内嵌汇编开始:https://blog.csdn.net/u011298001/article/details/83864516
ARM GCC 内嵌(inline)汇编手册:http://blog.chinaunix.net/uid-20543672-id-3194385.html