Skip to content

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 打印实现过程。

判断当前工程用哪个编译器,看构建日志第一行:

text
*** 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 报错 ​

c
// 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"
    );
}
text
../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 的函数体内联汇编只有一种形式:单条指令的字符串语句,且字符串里不能出现寄存器名之外的复杂内容:

c
__asm("wfi");        /* 可以: 无寄存器名的单指令 */
__asm("mov r0, r1"); /* 报 #1267 / #549: 隐式使用物理寄存器 */

也就是说,AC5 基本没法在函数体内安全地写多指令内联汇编。

1.3 正确出路 ​

多指令汇编一律放文件作用域的 __asm 函数里:

c
__asm void my_asm_func(void)
{
    MOV   r0, r1
    BX    lr
}

这是 AC5 的原生形式,armasm 全语法可用(含 IMPORT / PRESERVE8 等指令),也是 LV005 跳板采用的形式。

2. 坑二:函数体内的 __asm { } 块,报 #20 + #114 ​

既然字符串形式不行,很自然的想法是改用块形式:

c
// 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 的报错彻底暴露了它的解析方式:

text
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,却在汇编阶段报:

text
stm32f1xx_it.c(145): error: A1516E: Bad symbol 'hardfault_dump', not defined or external
        BL    hardfault_dump

1.1 原因 ​

armasm 处理 __asm 函数体时,需要一个「符号导入表」。实验发现两条规律:

  • 符号在本文件中从未定义时(只有原型),AC5 会自动生成 IMPORT,汇编通过;
  • 符号在本文件中已有定义时,AC5 反而不自动导入,汇编器认为它「既不是本汇编单元定义的,也不是 external」,报 A1516E。

1.2 解法 ​

在汇编函数体里显式写 IMPORT:

armasm
; 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 ​

汇编问题解决后,错误转移到了链接阶段:

text
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 解法 ​

两步配合:

armasm
; 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 作为第一条汇编指令:

c
// 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 验证汇编函数,会得到完全虚假的安全感。

bash
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)是否刷新。

五、 双编译器兼容写法总结 ​

把上面的结论收敛成一份可抄的模板:

c
#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 技能辅助生成