一段汇编必须同时说明目标指令集、执行状态和汇编器语法。助记符相似不代表文件可以互换:下面区分 GNU assembler 的 x86 Intel 语法、Arm A32 与 Cortex-M3 的 Thumb/T32 语法。
ISA 与文本语法不是同一件事
跳转到“ISA 与文本语法不是同一件事”| 范围 | 指令与编码特点 | 阅读边界 |
|---|---|---|
| x86/x86-64 | 可变长度编码,单条指令最长15字节;存在复杂寻址和字符串操作 | Intel 语法通常写“目标,源”;AT&T 语法通常相反,不能只看 CPU 名称判断顺序 |
| Arm A32 | 32位指令编码,常见于支持 Arm 状态的32位 A/R-profile 处理器 | 许多数据处理指令可带条件码;并非所有 Arm 处理器都能执行 A32 |
| Thumb/T32 | 16位与32位指令编码并存,具体支持集依架构版本 | Cortex-M3 执行 Thumb 指令;不能把 Thumb 统一说成只有16位 |
| AArch64/A64 | 另一套寄存器与指令模型,常见指令为32位编码 | 本页的 R0~R15 示例不能直接用于 X0~X30 |
CISC/RISC 可以帮助描述指令集设计取向,但不能推出“一条指令只做一个动作”“一定只用一个周期”或“某一类 CPU 必然更快”。编码、微架构和程序行为需要分别分析。
GNU x86 Intel 语法可用 .intel_syntax noprefix 选择,行注释使用 #。GNU Arm 语法中 @ 开始行注释,; 可以分隔语句,# 通常标记立即数;不能把另一种汇编器的分号注释原样搬来。GNU Arm 特殊字符说明给出了这些规则。
数据传送与寻址
跳转到“数据传送与寻址”下表是分别属于所标架构的指令片段,不是可直接连在一起运行的程序。var、地址寄存器和标签需要在实际程序中定义,内存必须有效。
| 意图 | x86 Intel 语法示例 | Arm A32 示例 |
|---|---|---|
| 常数写入寄存器 | mov eax, 5 | mov r0, #5 |
| 寄存器复制 | mov ebx, eax | mov r1, r0 |
| 写内存 | mov [var], eax | str r1, [r0] |
| 从地址读取 | mov eax, [ebx + ecx*4 + 8],这里使用32位地址寄存器 | ldr r1, [r0] |
| 基址加偏移读取 | 按具体地址表达式形成有效地址 | ldr r0, [r1, #4] |
| 装入常数或地址的伪指令 | 取决于汇编器及重定位写法 | ldr r0, =0x1000 将常数 0x1000 装入 R0 |
ldr r0, =0x1000 不表示读取地址0x1000处的数据。这是汇编器伪指令,可能变成装立即数的指令或文字池读取;只有后续 ldr r1, [r0] 才会使用 R0 中的地址访问内存。参见 GNU Arm LDR 伪指令。
算术与位逻辑
跳转到“算术与位逻辑”| 操作 | x86 Intel 片段 | Arm A32 片段 | 含义 |
|---|---|---|---|
| 加法 | add eax, 2 | add r0, r0, r1 | 把加法结果写入目标寄存器 |
| 减法 | sub eax, 1 | sub r0, r0, #1 | 目标值减去指定值 |
| 加一、减一 | inc eax、dec eax | 可用相应 add、sub | 标志位行为不一定与其他写法完全相同 |
| 按位与 | and eax, 0xFF | and r0, r0, r1 | 保留双方都为1的位 |
| 按位或 | or eax, 0x01 | orr r0, r0, r1 | 合并置位 |
| 按位异或 | xor eax, eax | eor r0, r0, r1 | 同位不同则为1;自身异或得到0 |
| 按位取反 | not eax | mvn r0, r0 | 在操作数宽度内逐位取反 |
不能只依据结果相同就认定标志位效果相同。例如 x86 的 INC/DEC 保留 CF,而 ADD/SUB 会更新它;Arm 的标志更新与具体指令、编码及 S 后缀有关。寄存器宽度、溢出解释和后续条件跳转也必须一起考虑。
比较、跳转、调用与返回
跳转到“比较、跳转、调用与返回”| 意图 | x86 Intel | Arm A32 |
|---|---|---|
| 无条件分支 | jmp label | b label |
| 比较并更新条件状态 | cmp eax, ebx | cmp r0, r1 |
| 相等分支 | je label | beq label |
| 不等分支 | jne label | bne label |
| 普通子程序调用 | call subroutine | bl subroutine |
| 普通子程序返回 | ret | 常见为 bx lr,取决于函数保存与恢复方式 |
x86 的普通 call/ret 通过栈处理返回地址;Arm 的 bl 把链接信息写入 LR。继续调用另一个函数会改变 LR,不能据一条 bx lr 就认定嵌套调用的返回过程完整。
A32 中,cmp r0, r1 后可用 addne r0, r0, r2 表示在“不相等”条件下相加。Cortex-M3 的 Thumb 代码应按 T32 的分支或 IT 规则编写,不能把所有 A32 条件后缀直接复制过去。中断/异常返回另有规则,不能简单等同普通函数返回。
一个相同运算的两种完整函数
跳转到“一个相同运算的两种完整函数”以下函数都计算 (value & 0xFF) | 1。第一份针对 GNU/Linux x86-64 System V ABI:首个整数参数在 EDI,32位返回值在 EAX。
.intel_syntax noprefix.text.globl low_byte_odd.type low_byte_odd, @functionlow_byte_odd: mov eax, edi and eax, 255 or eax, 1 ret.size low_byte_odd, .-low_byte_odd.section .note.GNU-stack,"",@progbits第二份针对 Cortex-M3 Thumb 与 AAPCS32:参数和结果都使用 R0。这里没有嵌套调用,也没有改变需要由被调用方保存的寄存器。
.syntax unified.cpu cortex-m3.thumb.text.global low_byte_odd.type low_byte_odd, %function.thumb_funclow_byte_odd: and r0, r0, #255 orr r0, r0, #1 bx lr.size low_byte_odd, .-low_byte_odd宿主测试程序可以保存为 bits-main.c:
#include <assert.h>#include <inttypes.h>#include <stdint.h>#include <stdio.h>
extern uint32_t low_byte_odd(uint32_t value);
int main(void) { const uint32_t inputs[] = {0, 0xFF, 0x12345678, UINT32_MAX}; for (size_t i = 0; i < sizeof inputs / sizeof inputs[0]; ++i) { const uint32_t result = low_byte_odd(inputs[i]); assert(result == ((inputs[i] & UINT32_C(0xFF)) | UINT32_C(1))); printf("%08" PRIx32 " -> %08" PRIx32 "\n", inputs[i], result); } return 0;}在 x86-64 GNU/Linux 下编译链接并运行第一份:
gcc -std=c11 -Wall -Wextra -Werror bits-main.c bits-x86.S -o bits-demo./bits-demo对 Cortex-M3 示例只生成目标文件:
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -c bits-m3.S -o bits-m3.oarm-none-eabi-objdump -d bits-m3.o目标文件可汇编、可反汇编并不表示已经有板级启动、链接地址或可运行固件。Windows x64 的参数寄存器也与第一份不同,不能仅修改文件扩展名就复用它。