本文旨在阐述如何在Cortex-M系列CPU的C文件中插入汇编程序。通过观察C语言生成的汇编程序,在必要时刻知道如何写更为复杂的汇编代码。通过学习ARM工具链生成的汇编程序,可实现较高效的汇编指令学习,更重要的在于知道程序有多大的优化空间。特别地,理解数组处理方式,能更深刻地理解数组越界到底会有怎样的影响;理解文本池访问的方式,能更直观地知道常量以及const关键字在汇编中是如何体现的。
本文承接参考链接[3]和[7],故对一些概念没有再具体讲解。
目录
1 Cortex-M7程序生成
编译的整个过程为:预编译、编译、汇编、链接
1.1 基本概念
格式:
[标号] <指令|条件|S> <操作数>;[注释]
注:指令指伪指令或汇编指令;条件和S见“表5.3 Cortex-M 汇编语言的后缀”[1]。
1、在ARM工具链和GNU工具链的伪指令是不同的,即不同的编译器GNU的伪指令可能不同。一般GNU工具链的伪指令以“.”为开头。下面我将在Keil5中使用ARM工具链测试一些汇编指令,都是在.c文件中编写和调用。
2、汇编指令到具体CPU手册找,如[2]
2、操作数可以是下表的寄存器,也可以是立即数“#0x01”,或是定义的常量(在ARM工具链中,用“EQU”定义,用“=常量名”引用),或是指针的本质用法("[R3]")。
寄存器 | __asm中的字符串 |
APSR | "apsr" |
BASEPRI | "basepri" |
BASEPRI_MAX | "basepri_max" |
CONTROL | "control" |
EAPSR(EPSR+APSR) | "eapsr" |
EPSR | "epsr" |
FAULTMASK | "faultmask" |
IAPSR(IPSR+APSR) | "iapsr" |
IEPSSR(IPSR+EPSR) | "iepsr" |
IPSR | "ipsr" |
MSP | "msp" |
PRIMASK | "primask" |
PSP | "psp" |
PSR | "psr" |
r0~r12 | "r0"~"r12" |
r13 | "r13"或“sp” |
r14 | "r14"或"lr" |
r15 | "r15"或“oc” |
XPSR | "xpsr" |
1.2 书写规范[6]
a\所有标号必须在一行的顶格书写
b\所有的指令均不能顶格书写
c\标识符大小写敏感
d\ARM指令、伪指令、寄存器名可以全部大写字母,也可以全部小写字母,但不要大小写混合使用。
(如可以"BX lr”,但不能“Bx lr”)
e\语句间可以插入空行,也可以某行只有标号,以使得源代码的可读性更好。
1.3 嵌入汇编
__asm uint8_t myADD( uint8_t lhs,uint8_t rhs )
{
MOV r2,r0
ADDS r0,r2,r1
UXTB r0,r0
BX lr
}
注:容易出现“表面语法错误”,编译后并不会报错。
嵌入汇编提供一个汇编函数的空壳子,提供个入口,里面的所有操作都有程序员自己写,尤其是lhs和rhs不能在汇编中直接使用。在调用者调用时,lhs存在r0,rhs存在r1[7]。需要返回的值应该写在r0上。函数的返回也需要将LR写入CP寄存器来返回。中间的过程再补充一下就行。
调用者使用的C语言及发生的汇编如下:
remp = testAdd(10,20);
想要在汇编中直接使用lhs和rhs可用下一方法。
1.4 内联汇编
内联汇编的框架有C函数搭建,在内联汇编里即可以使用寄存器名,也可以使用“可见”的C变量。
uint8_t myADD1( uint8_t lhs,uint8_t rhs)
{
uint8_t rslt;
__asm
{
ADD rslt,lhs,rhs
}
return rslt;
}
uint8_t myADD2( uint8_t lhs,uint8_t rhs)
{
uint8_t rslt;
__asm("ADDS rslt,lhs,rhs");
return rslt;
}
两个函数的汇编结果相同,结果如下所示:
1.5 其他汇编形式
从汇编中调用C函数 | IMPORT my_add_c BL my_add_c |
从C程序中的汇编代码 | BL _cpp(my_add_c) |
从C中调用汇编函数 | EXPORT My_Add |
注:详见[1]中的第20章
2 C函数例子——基本运算
记得将优化等级改为最低。下面的代码可能有的复制不完整,特性是BX指令。
2.1 加法测试
uint64_t和int64_t没有在本文测试。
2.2 乘除减和移位
Void BHWTest2(void)
Void S_BHWTest2(void)
2.3 浮点测试
注意这里是用BX返回调用者的。
2.4 数组处理,SP相关寻址
a、为了在(向下增长的)栈中给数组留下空间,程序直接移动了栈指针。这个技术成为“SP相关寻址”
b、因为程序调用了子函数,因此使用“PUSH {lr}”和“POP {pc}”组合更高效。
c、调用ARM的标准库函数时,可以发现函数只把memset函数的第一个参数和第三个参数传入了,可能是它内部优化导致的吧。
2.5 多参数调用
调用者部分:
被调用者部分:
a\压入栈的有4个寄存器,每个寄存器4字节,于是栈地址减小了0x10.
b\比较特别的是,调用者知道多余的参数会“破坏”栈,所以提前把R3这个非被调用者保存寄存器保存起来,供第5个参数破坏。
c\显然这个函数效率很低,如果用SIMD中的UADD8,那速率就快多了。这也是汇编加速可操作的地方吧。但不确定3级或2级优化下,这里是否优化,感兴趣的读者可以自己试一下。
2.6 排他访问
自己还没试过,主要是没需求,另外就是[9] 基于ARM的排他访问原理及应用 写的太好了。
2.7文本池访问
实例1:当非零组数定义在函数内
由于里面有数组,所有使用了4.4节的相关寻址的概念,但不是这节的重点。值得一提的是,虽然数组的有效数只有0x4C的大小,sizeof(BASEtable2)==0x4C,但在数组前留了4个字节,或者说多压了4个字节入栈。
1、由于这里的数组时非零数组,于是隐式调用了memcpy(r0,r1,r2)函数。
2、而原始数据存储的地方是在代码段,定义局部数组,所有需要把代码段内的值赋值到栈中。
3、当真正运行到0x08005EEE行时,PC指针的值其实是0x08005EF0 ,即该行指令的下一个指令的地址(这一点需要特别注意)。
0x08005EF0+24=0x08005F08
这个地址正好是函数结束后的内存(考虑4字节对齐),没错这个就是0x081075C8,
实例2:当常量非零数组定义在函数外,
和实例1比,少了隐含数组复制。
实例3:单个常量
有意思,常量不是在Flash中的真常量,而是一个常变量,使用强制转换功能是改用改变这个“常变量”的值的。
3 参考资料
[1] Joseph Yiu. ARM Cortex-M3与Cortex-m4权威指南(第3版)[M]. 吴常玉,曹孟娟,王丽红,译. 北京:清华大学出版社,2015.
[2] ARM® Cortex®-M7 Devices Generic User Guide
[3]Cortex-M系列:ARM架构与汇编指令集 https://blog.csdn.net/NoDistanceY/article/details/104177163
[6] Thumb指令集及汇编格式 https://wenku.baidu.com/view/49ab91606fdb6f1aff00bed5b9f3f90f77c64d5a.html
[7] Cortex-M系列:非中断、特权模式下的汇编语言 https://blog.csdn.net/NoDistanceY/article/details/104003831
[8] Cortex-M for Beginners - 2017_EN_v2 https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/white-paper-cortex-m-for-beginners-an-overview-of-the-arm-cortex-m-processor-family-and-comparison
[9] 基于ARM的排他访问原理及应用 https://blog.csdn.net/tissar/article/details/83008719