跳转到内容
新建笔记

x86 与 Arm 汇编语法:指令集、操作数与完整函数

一段汇编必须同时说明目标指令集、执行状态和汇编器语法。助记符相似不代表文件可以互换:下面区分 GNU assembler 的 x86 Intel 语法、Arm A32 与 Cortex-M3 的 Thumb/T32 语法。

ISA 与文本语法不是同一件事

跳转到“ISA 与文本语法不是同一件事”
范围指令与编码特点阅读边界
x86/x86-64可变长度编码,单条指令最长15字节;存在复杂寻址和字符串操作Intel 语法通常写“目标,源”;AT&T 语法通常相反,不能只看 CPU 名称判断顺序
Arm A3232位指令编码,常见于支持 Arm 状态的32位 A/R-profile 处理器许多数据处理指令可带条件码;并非所有 Arm 处理器都能执行 A32
Thumb/T3216位与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, 5mov r0, #5
寄存器复制mov ebx, eaxmov r1, r0
写内存mov [var], eaxstr 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, 2add r0, r0, r1把加法结果写入目标寄存器
减法sub eax, 1sub r0, r0, #1目标值减去指定值
加一、减一inc eax、dec eax可用相应 add、sub标志位行为不一定与其他写法完全相同
按位与and eax, 0xFFand r0, r0, r1保留双方都为1的位
按位或or eax, 0x01orr r0, r0, r1合并置位
按位异或xor eax, eaxeor r0, r0, r1同位不同则为1;自身异或得到0
按位取反not eaxmvn r0, r0在操作数宽度内逐位取反

不能只依据结果相同就认定标志位效果相同。例如 x86 的 INC/DEC 保留 CF,而 ADD/SUB 会更新它;Arm 的标志更新与具体指令、编码及 S 后缀有关。寄存器宽度、溢出解释和后续条件跳转也必须一起考虑。

比较、跳转、调用与返回

跳转到“比较、跳转、调用与返回”
意图x86 IntelArm A32
无条件分支jmp labelb label
比较并更新条件状态cmp eax, ebxcmp r0, r1
相等分支je labelbeq label
不等分支jne labelbne label
普通子程序调用call subroutinebl 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。

bits-x86.S
.intel_syntax noprefix
.text
.globl low_byte_odd
.type low_byte_odd, @function
low_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。这里没有嵌套调用,也没有改变需要由被调用方保存的寄存器。

bits-m3.S
.syntax unified
.cpu cortex-m3
.thumb
.text
.global low_byte_odd
.type low_byte_odd, %function
.thumb_func
low_byte_odd:
and r0, r0, #255
orr r0, r0, #1
bx lr
.size low_byte_odd, .-low_byte_odd

宿主测试程序可以保存为 bits-main.c:

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.o
arm-none-eabi-objdump -d bits-m3.o

目标文件可汇编、可反汇编并不表示已经有板级启动、链接地址或可运行固件。Windows x64 的参数寄存器也与第一份不同,不能仅修改文件扩展名就复用它。