LV010-Keil AC5汇编踩坑记录
在 Keil MDK(ARM Compiler 5)里为 LV005 的跳板写内联汇编,先后撞上四个报错:两个发生在编译期(#20、#114),两个发生在汇编与链接期(A1516E、L6238E)。本文按踩坑顺序逐个讲透成因与解法,最后收敛成双编译器兼容模板。
一、 背景:AC5 与 AC6 是两门方言
Keil MDK 有两代编译器,内联汇编的语法完全不兼容:
| 写法 | ARM Compiler 5(V5.x) | ARM Compiler 6(V6.x) |
|---|---|---|
__asm volatile("mov r0, r1\n") | 不支持 | 支持 |
函数体内 __asm { ... } 块 | 不支持 | 不支持 |
文件作用域 __asm void f(void){ ... } | 支持 | 不支持 |
__attribute__((naked)) + 字符串汇编 | 警告后忽略 | 支持 |
两个「不支持」是本文的重点。AC5 遇到 GCC 风格写法不会友好地报「语法不支持」,而是把汇编字符串当成 C 代码继续解析,报出一堆和真实原因毫不相干的错误。下面按踩坑顺序记录四个报错,全部来自一次真实的 HardFault 打印实现过程。
判断当前工程用哪个编译器,看构建日志第一行:
*** Using Compiler 'V5.06 update 7 (build 960)', folder: 'C:\LenovoSoft\Keil_v5\ARM\ARMCC\Bin'V5. 开头是 AC5,V6. 开头是 AC6。也可以在代码里用 __ARMCC_VERSION 区分:AC5 是 5xxxxxx(如 5060960),AC6 ≥ 6010050。要切换编译器,在 μVision 里打开 Options for Target → Target 页 → ARM Compiler 下拉框选择。
二、 编译期的坑:函数体内写不了多指令汇编
下面两个坑同根同源:AC5 把函数体内出现的汇编统统当成 C 代码解析,报错信息因此极具误导性。
1. 坑一:GCC 风格字符串汇编,报 #20 identifier "lr" is undefined
1.1 报错
// Core/Src/stm32f1xx_it.c(错误写法)
void HardFault_Handler(void)
{
__asm volatile
(
"TST lr, #4 \n"
"ITE EQ \n"
"MRSEQ r0, MSP \n"
"MRSNE r0, PSP \n"
"B hardfault_dump \n"
);
}../Core/Src/stm32f1xx_it.c(148): error: #20: identifier "lr" is undefined
"TST lr, #4 \n"1.2 原因
错误信息极具误导性:它把字符串的内容当 C 标识符在报错。真相是 AC5 不认 __asm volatile(...) 这种形式——注意 volatile 夹在 __asm 和括号之间,AC5 的内联汇编语句只接受紧跟 __asm 的字符串。
AC5 的函数体内联汇编只有一种形式:单条指令的字符串语句,且字符串里不能出现寄存器名之外的复杂内容:
__asm("wfi"); /* 可以: 无寄存器名的单指令 */
__asm("mov r0, r1"); /* 报 #1267 / #549: 隐式使用物理寄存器 */也就是说,AC5 基本没法在函数体内安全地写多指令内联汇编。
1.3 正确出路
多指令汇编一律放文件作用域的 __asm 函数里:
__asm void my_asm_func(void)
{
MOV r0, r1
BX lr
}这是 AC5 的原生形式,armasm 全语法可用(含 IMPORT / PRESERVE8 等指令),也是 LV005 跳板采用的形式。
2. 坑二:函数体内的 __asm { } 块,报 #20 + #114
既然字符串形式不行,很自然的想法是改用块形式:
// Core/Src/stm32f1xx_it.c(错误写法)
void HardFault_Handler(void)
{
__asm
{
TST lr, #4
ITE EQ
MRSEQ r0, MSP
MRSNE r0, PSP
B hardfault_dump
}
}AC5 的报错彻底暴露了它的解析方式:
stm32f1xx_it.c(162): error: #20: identifier "lr" is undefined
TST lr, #4
stm32f1xx_it.c(164): warning: #1267-D: Implicit physical register R0 should be defined as a variable
MRSEQ r0, MSP
stm32f1xx_it.c(166): error: #114: label "hardfault_dump" was referenced but not defined
B hardfault_dump#114: label "hardfault_dump" was referenced but not defined 是 C 编译器对 goto 未定义标签的报错——也就是说 AC5 把整个 __asm { } 块当成 C 复合语句在解析。__asm 在 AC5 里是声明说明符(用于声明汇编函数),不是语句关键字;写在函数体内,后面的花括号块就是普通 C 代码。
结论与坑一相同:AC5 的函数体内没有可用的多指令汇编入口,必须走文件作用域 __asm 函数。
三、 汇编期与链接期的坑
编译过了不代表完——换成文件作用域 __asm 函数后,错误转移到了汇编器和链接器,各有一个。
1. 坑三:A1516E: Bad symbol, not defined or external
换成文件作用域 __asm 函数后,编译过了 armcc,却在汇编阶段报:
stm32f1xx_it.c(145): error: A1516E: Bad symbol 'hardfault_dump', not defined or external
BL hardfault_dump1.1 原因
armasm 处理 __asm 函数体时,需要一个「符号导入表」。实验发现两条规律:
- 符号在本文件中从未定义时(只有原型),AC5 会自动生成
IMPORT,汇编通过; - 符号在本文件中已有定义时,AC5 反而不自动导入,汇编器认为它「既不是本汇编单元定义的,也不是 external」,报 A1516E。
1.2 解法
在汇编函数体里显式写 IMPORT:
; Core/Src/stm32f1xx_it.c
__asm void hardfault_trampoline(void)
{
IMPORT hardfault_dump /* 显式声明外部符号 */
MRS r0, MSP
BL hardfault_dump
}【注意】
前面加 C 原型声明(void hardfault_dump(uint32_t msp);)对这个问题无效——实测加了原型照样报 A1516E。IMPORT 是唯一可靠解法。
2. 坑四:L6238E: invalid call from ~PRES8 to REQ8
汇编问题解决后,错误转移到了链接阶段:
atk-f103.axf: Error: L6238E: stm32f1xx_it.o(.emb_text) contains invalid call from
'~PRES8 (The user did not require code to preserve 8-byte aligment of 8-byte data
objects)' function to 'REQ8 (Code was permitted to depend on the 8-byte aligment of
8-byte data items)' function hardfault_dump.2.1 原因
AAPCS 要求函数入口处 SP 8 字节对齐。armlink 会做静态检查:调用方必须标记为 PRES8(承诺保持对齐),才能调用 REQ8 的被调方(默认依赖对齐)。
AC5 编译出的 C 函数默认是 REQ8;而 __asm 函数默认是 ~PRES8——链接器看不到汇编里的对齐代码,只能保守地认为「这个汇编函数可能传进来一个 4 mod 8 的 SP」,于是拒绝这次调用。
2.2 解法
两步配合:
; Core/Src/stm32f1xx_it.c
__asm void hardfault_trampoline(void)
{
PRESERVE8 /* 声明本函数保持 8 字节栈对齐 */
IMPORT hardfault_dump
MRS r0, MSP
ADD r0, r0, #4
MOV r1, sp /* 无条件把 SP 垫到 8 字节对齐 */
BIC r1, r1, #7
MOV sp, r1
BL hardfault_dump
}PRESERVE8 是对链接器的承诺,MOV/BIC/MOV 是对齐的事实——两者必须同时存在,只写 PRESERVE8 而不真对齐是把风险藏进 printf 内部,只对齐而不声明则过不了链接检查。
AC6 分支对应的写法是把 .preserve8 作为第一条汇编指令:
// Core/Src/stm32f1xx_it.c(AC6 分支)
__attribute__((naked)) void hardfault_trampoline(void)
{
__asm volatile
(
".preserve8 \n"
"MRS r0, MSP \n"
/* ... */
);
}四、 验证方法上的两个教训
排这一串错时浪费时间最多的不是错误本身,而是两个验证手段的盲区:
1. armcc -S 不跑汇编器
为了快速看生成代码,一直用 -S(只编译,产出 .s)。它不调用 armasm,所以 A1516E、L6238E 这类汇编/链接期错误在 -S 下永远是「通过」。用 -S 验证汇编函数,会得到完全虚假的安全感。
armcc -c ... stm32f1xx_it.c -o it.o # 对: 跑 armasm, 能抓汇编错
armcc -S ... stm32f1xx_it.c -o it.s # 错: 只到编译, 汇编错看不见验证汇编函数一律用 -c 起步,再看整体链接。
2. 构建脚本的日志流会混入上一次的内容
工程脚本边构建边流式打印 build.log,而脚本启动 UV4 前日志文件里还留着上一次构建的内容。于是控制台里新旧输出交错,出现过「上一轮 0 Error 的成功输出」和「本轮 1 Error 的失败输出」同时出现的灵异现象,行号还对不上当前文件。
判断构建结果只认两个权威来源:
- 构建结束后再读一次
build.log的最终内容; - 看产物时间戳(
.axf/.hex的LastWriteTime)是否刷新。
五、 双编译器兼容写法总结
把上面的结论收敛成一份可抄的模板:
#if defined(__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050)
/* AC6: naked 函数 + 字符串内联汇编 */
__attribute__((naked)) void trampoline(void)
{
__asm volatile
(
".preserve8 \n"
"MRS r0, MSP \n"
/* ... */
);
}
#else
/* AC5: 文件作用域 __asm 函数 */
__asm void trampoline(void)
{
PRESERVE8
IMPORT target_c_function
MRS r0, MSP
/* ... */
}
#endif三条铁律:
(1)AC5 的函数体内不写多指令汇编,一律文件作用域 __asm 函数。
(2)__asm 函数引用 C 符号必须 IMPORT。
(3)__asm 函数调用 C 函数必须 PRESERVE8 + 真实对齐。
本文档由 markdowncli 技能辅助生成