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+ 编程手册
建议按以下顺序排查:
- 查看构建日志中报错对应的汇编行,确认是
LDR R3, testReg,还是某条LDR ..., =常量。 - 使用项目原有编译选项生成
.s文件,检查局部变量如何寻址,以及常量池实际位于哪里。 - 先改为通过 extended asm 操作数传参,移除跨越 C 代码的
PUSH、POP和隐式寄存器依赖。 - 如果确实需要手写常量池,再检查
.ltorg的位置、对齐和控制流。
若目标是检测指定寄存器的硬件故障,应另行设计完整的汇编测试例程,并遵守调用约定、保存必要的寄存器。当前示例主要验证测试模式的写入与读回,测试范围应据此界定。
备注:内容仅供参考。
