内联汇编 —— 在 C++ 里直接写 ARM64 指令
什么是内联汇编
内联汇编(Inline Assembly)就是在 C/C++ 代码里直接嵌入汇编指令,编译器会把这些指令原样插入到生成的机器码中。
典型用途:
- 性能极致优化(核心循环用汇编手写)
- 特殊硬件指令(比如 ARM 的加密指令)
- 操作系统底层开发
- 逆向分析学习
Cxxdroid 官方明确宣传「支持 Clang 语法的内联汇编」——这是它和其他移动端 C/C++ 工具的差异点之一。
Clang 语法 vs GCC 语法
GCC 风格的内联汇编(最常见)长这样:
asm volatile("add %0, %1, #1" : "=r"(result) : "r"(x));
Clang 也支持这种格式(Clang 兼容 GCC 内联汇编),所以大部分 GCC 风格内联汇编代码可以直接在 Cxxdroid 里编译。
一个简单的内联汇编示例
在 Cxxdroid(ARM64 架构)里实现一个加法函数:
int add_one(int x) {
int result;
asm volatile(
"add %0, %1, #1\n"
: "=r"(result) // 输出操作数:result 映射到任意可用寄存器
: "r"(x) // 输入操作数:x 映射到任意可用寄存器
: // clobber list:无额外需要声明的寄存器
);
return result;
}
翻译成人话:
"add %0, %1, #1\n":汇编指令,把 %1(x 的值)加 1,结果存入 %0"=r"(result):=r 表示输出操作数,r 表示让编译器选任意通用寄存器"r"(x):输入操作数,同样让编译器选寄存器- clobber list 空着,表示除了 %0 和 %1 外没有其他被修改的寄存器
clobber list 是什么
如果内联汇编修改了某个未声明的寄存器,必须在 clobber list 里告诉编译器——否则编译器可能以为那个寄存器还存着之前的值,导致后续代码读错数据。
asm volatile(
"mov x0, #42\n"
:
:
: "x0" // 声明修改了 x0 寄存器
);
约束字符速查
最常用的几个约束字符(适用于 ARM64):
| 字符 | 含义 |
|---|---|
r | 任意通用寄存器 |
g | 允许寄存器、内存或立即数 |
m | 仅允许内存操作数 |
=r | 输出操作数(= 表示写) |
+r | 读写操作数(+ 表示先读后写) |
在 Cxxdroid 里验证内联汇编
- 新建 C++ 单文件项目
- 粘贴上面的 add_one 函数 + main 调用它
- 编译运行
- 如果输出正确的结果(输入 5 输出 6),说明内联汇编工作正常
注意事项
volatile vs 不 volatile
asm("nop"); // 编译器可能优化掉
asm volatile("nop"); // 编译器不能优化掉
用 volatile 告诉编译器「这段汇编有副作用,不能删掉、不能重排」。大多数内联汇编都应该加 volatile。
语法错误提示
Clang 对内联汇编的语法错误提示很清晰——会告诉你「哪个约束字符用错了」或「寄存器名无效」。如果报 invalid operand for inline asm constraint,检查一下约束字符是不是符合当前架构(ARM64 vs x86 的约束不完全一样)。
Cxxdroid 是 ARM64
重要:Cxxdroid 编译出来的可执行文件是 ARM64 架构的——所以你写的内联汇编必须是 AArch64 汇编语法,不能是 x86/x64 语法。
典型的 ARM64 vs x86 差异:
- 寄存器名:ARM64 用 x0-x30、w0-w31(32 位);x86 用 eax/rax、ebx/rbx 等
- 指令格式:ARM64 大多数指令三个操作数(目的, 源1, 源2);x86 大多数两个操作数
- 立即数语法:ARM64 用
#42;x86 用$42(AT&T 语法)或42(Intel 语法)
进阶:Extended Inline Assembly
如果内联汇编里有多条指令,用 asm goto 或 Extended 格式可以更清晰地组织代码。不过对于学习和实验来说,Basic Inline Assembly(上面的例子)已经够用了。