PAYATHON 2026

ARM Cortex-M0+ 的 C 与汇编混合函数如何使用 literal pool?

数据小许

结论

C 与内联汇编混合的函数可以使用 literal pool。这段代码需要先修正 C 与汇编之间的数据传递,单独添加 .ltorg 不能解决所有问题。

下面假设项目使用 GCC 和 GNU assembler(GAS)。局部变量应通过 GCC extended asm 的操作数传入 &testReg;常量可以交给编译器装入寄存器。如果需要精确安排 literal pool 或测试指定的 CPU 寄存器,使用独立的 .S 函数更容易控制。

原代码为什么有问题

LDR R3, testReg 不会取得局部变量地址

原代码的注释希望把 testReg 的地址放入 R3:

LDR R3, testReg

这里有两个问题:

  • LDR R3, label 从标签对应的内存位置读取一个字,不会取得标签地址。
  • 栈上的 C 局部变量通常没有可以直接写进汇编字符串的同名汇编符号,其位置由编译器决定。

因此,即使把指令改成 LDR R3, =testReg,也不能据此访问这个局部变量。对于确实存在的全局汇编符号,LDR R3, =symbol 可以用于装入其地址;局部变量应通过操作数传入。

LDR R0, =0x55555555 是装入常量的伪指令。当目标指令无法直接生成该常量时,GAS 会使用 literal pool,并生成 PC-relative load。GNU assembler 的 LDR 伪指令说明

多个 basic asm 没有描述寄存器和内存依赖

原代码把 PUSH、LDR 和 STR 分别放在多个 asm("...") 中,又在它们之间执行 C 条件判断。编译器不知道这些字符串如何修改 R0、R3,也不知道后续汇编依赖之前保留在 R3 中的地址。

即使手动保存了 R0 和 R1,编译器仍然不知道这些依赖关系。分散的汇编语句也不能保证在最终代码中连续出现。GCC basic asm 文档

此外,PUSH 会改变 SP。编译器生成的局部变量访问可能使用相对 SP 的偏移,如果在两个汇编块之间保持额外的压栈状态,这些访问就可能出错。

return 后面不是可靠的常量池位置

return(rc);
asm(".LTORG");

这条汇编位于不可达的 C 代码中,不能依赖它出现在最终汇编里,也不能据此认定常量池会紧跟机器级返回指令。

GAS 文档使用的指令是 .ltorg。它在当前位置输出当前 section/subsection 中尚未输出的 literal pool,并进行字对齐,但不会为 C 局部变量创建地址符号。如果常量池放在可顺序执行的位置,还必须确保控制流跳过池中的数据。GNU assembler 的 .ltorg 说明

推荐修改:通过操作数传递地址和值

如果目标是让汇编把测试模式写入局部变量,再由 C 检查,可以这样写:

#include <stdint.h>

static inline void storePattern(volatile uint32_t *addr,
                                uint32_t value)
{
    __asm__ volatile (
        "str %[value], [%[addr]]"
        :
        : [addr] "l" (addr),
          [value] "l" (value)
        : "memory"
    );
}

uint32_t registerTest(void)
{
    volatile uint32_t testReg;

    storePattern(&testReg, 0x55555555u);
    if (testReg != 0x55555555u)
        return 0x55555555u;

    storePattern(&testReg, 0xAAAAAAAAu);
    if (testReg != 0xAAAAAAAAu)
        return 0xAAAAAAAAu;

    return 0u;
}

编译器会计算局部变量地址,并把地址和值放入寄存器。内联汇编只执行 STR,无需手动维护跨越 C 语句的寄存器状态,也无需手动压栈。

"l" 在 ARM 的 Thumb 状态下约束操作数使用 R0 至 R7,适合这里的 Cortex-M0+ 寄存器间接存储指令。GCC ARM 机器约束

"memory" 告知编译器,汇编会通过指针访问未列为显式内存操作数的内存。__asm__ volatile 用于保留该汇编操作,但它本身不能替代内存依赖声明,也不是硬件内存屏障。GCC extended asm 文档

这个版本由编译器负责常量装载,因此无需在内联汇编中显式安排 literal pool。它保留了原代码的两次写入和检查流程,但不保证使用指定的 R0 或 R3,也不能作为完整的 CPU 寄存器自检。

需要显式 literal pool 时,使用独立的 .S 函数

下面的最小示例展示了如何安全放置常量池:

.syntax unified
.cpu cortex-m0plus
.thumb
.text
.p2align 2

.global write_55555555
.type write_55555555, %function
.thumb_func
write_55555555:
    ldr r1, =0x55555555
    str r1, [r0]
    bx lr
    .ltorg
.size write_55555555, .-write_55555555

C 端声明为:

void write_55555555(volatile uint32_t *addr);

按通常的 ARM 调用约定,指针参数通过 R0 传入。函数使用 R1 装载常量并执行存储,随后由 BX LR 返回。常量池位于返回指令之后,正常调用不会顺序执行池中的数据;它又紧邻 LDR,便于满足寻址范围要求。

独立汇编函数可以明确控制指令、返回位置和常量池布局。

如何定位 0xFFFFFFFC 报错

0xFFFFFFFC 按 32 位有符号数解释为 -4,但仅凭这个数值,不能确定究竟是哪条指令或哪个符号导致错误。

Cortex-M0+ 的 PC-relative LDR 有有限的正向寻址范围,目标需要字对齐,编码偏移为 0 至 1020,且是 4 的倍数。负偏移、目标过远或符号用法错误,都需要结合实际汇编输出判断。STM32 Cortex-M0+ 编程手册

建议按以下顺序排查:

  1. 查看构建日志中报错对应的汇编行,确认是 LDR R3, testReg,还是某条 LDR ..., =常量。
  2. 使用项目原有编译选项生成 .s 文件,检查局部变量如何寻址,以及常量池实际位于哪里。
  3. 先改为通过 extended asm 操作数传参,移除跨越 C 代码的 PUSH、POP 和隐式寄存器依赖。
  4. 如果确实需要手写常量池,再检查 .ltorg 的位置、对齐和控制流。

若目标是检测指定寄存器的硬件故障,应另行设计完整的汇编测试例程,并遵守调用约定、保存必要的寄存器。当前示例主要验证测试模式的写入与读回,测试范围应据此界定。

备注:内容仅供参考。