AT&T 汇编语言 — 总览

AT&T 语法是 GNU 工具链(GAS 汇编器 as、GCC 内联汇编、objdump 反汇编)默认使用的汇编语法。本知识库按模块组织:

学习路径建议:先读模块 0–4 掌握语法与指令基础 → 用模块 5–7 写出第一个完整程序 → 模块 8–9 提升工程能力 → 模块 10–13 按需深入专题 → 模块 14/19 学会调试、模块 16 学会 C 互操作;模块 17、18 是速查与排障附录,随用随查。

AT&T 与 Intel 语法对比

特性 AT&T Intel
操作数顺序 源, 目的 目的, 源
寄存器前缀 %(如 %eax
立即数前缀 $(如 $4
指令大小后缀 有(b/w/l/q 无(靠操作数推断)
内存操作数 disp(base,idx,scale) [base + idx*scale + disp]

指令大小后缀

后缀 含义 位数 示例
b byte 8 movb
w word 16 movw
l long 32 movl
q quad 64 movq

x87 浮点指令有自己的精度后缀 s/l/t(单/双/扩展精度),见模块十。跳转、syscall 等指令无后缀。

数值与字符字面量

写法 含义
0x10 十六进制 16
0b1010 二进制 10(GAS 支持)
010 八进制 8!前导 0 不是十进制
-5 负数
'A' 字符常量(= 0x41)

八进制陷阱是 GAS 新手最常见的坑之一:movb $010, %al 装入的是 8 而不是 10,反汇编可见 mov $0x8,%al

示例(32 位):

movl $1, %eax        # 把立即数 1 放入 eax(源在左,目的在右)
movl %eax, %ebx      # ebx = eax
addl $4, %eax        # eax = eax + 4

示例(64 位):

movq $1, %rax
movq %rax, %rbx
addq $8, %rax

语句格式

每条语句占一行,由可选的标号、指令、操作数和注释组成:

[标号:] 指令  [源, 目的]   # 注释
.globl _start
_start:
    movl $1, %eax        # 标号、指令、注释三者俱全
  • 注释以 # 开头(也支持 C 风格 /* ... */)。
  • 标号以 : 结尾,用作跳转目标或地址符号。
  • 局部数字标号 1:2: 可重复使用,配合 1b(向后找最近的 1:)/ 2f(向前找)引用,见模块九
loop_start:
    decl %ecx
    jnz loop_start

三类操作数(最重要的基础概念)

每条指令的操作数必属以下三类之一:

类型 写法 示例 含义
立即数 $常数 movl $42, %eax 常数 42 本身
寄存器 %名字 movl %eax, %ebx 寄存器里存放的值
内存 disp(base,idx,scale) movl 8(%rbp), %eax 内存地址处的值

"取变量 msg地址"是把地址当作一个数算出来(并不读内存):

leaq msg(%rip), %rdi      # 推荐:RIP 相对算地址
movq $msg, %rdi           # 绝对地址(仅非 PIE 程序可用)
movq msg(%rip), %rdi      # 注意!这是读 msg 处 8 字节内容,不是取地址

leamov 的区别贯穿全书,见模块四模块三

第一个程序(5 分钟上手)

# hello.s
.section .rodata
msg:    .ascii "Hello, AT&T asm!\n"
.set len, . - msg          # len = 当前位置 - msg = 字符串长度

.section .text
.globl _start              # 导出入口符号(ld 默认从 _start 开始执行)
_start:
    movq $1, %rax          # 系统调用号:write
    movq $1, %rdi          # 参数 1:fd = 1(stdout)
    leaq msg(%rip), %rsi   # 参数 2:缓冲区地址
    movq $len, %rdx        # 参数 3:长度
    syscall                # 陷入内核

    movq $60, %rax         # 系统调用号:exit
    xorq %rdi, %rdi        # 参数:退出码 0
    syscall
as hello.s -o hello.o && ld hello.o -o hello && ./hello
# 输出:Hello, AT&T asm!

逐行讲解、系统调用表见模块七

_start 还是 mainas + ld、不链接 libc 时入口是 _start(系统调用全部手写);用 gcc 链接 libc 时入口是 main(C 启动代码 crt0 会先初始化,然后调用你的 main,返回后替你 exit)。两种风格本书都用,注意区分。

编译与查看

# 汇编 + 链接(64 位)
as hello.s -o hello.o
ld hello.o -o hello

# 多文件链接(ld 默认入口符号是 _start,可用 -e 指定)
ld a.o b.o -o prog
ld -e my_entry a.o -o prog

# 经 gcc 驱动(.s 直接汇编;.S 大写会先经 C 预处理器)
gcc -no-pie -nostdlib hello.s -o hello

# 命令行定义符号(配合 .ifdef 做条件汇编)
as --defsym DEBUG=1 foo.s -o foo.o

# 指定 32 位
as --32 hello.s -o hello.o && ld -m elf_i386 hello.o -o hello

# 反汇编查看(默认 AT&T 语法)
objdump -d hello.o

# 反汇编并转换为 Intel 语法
objdump -d -M intel hello.o

AT&T / Intel 语法切换

GAS 允许在源文件中随时切换两种语法(.intel_syntax noprefixnoprefix 表示寄存器不带 %):

.intel_syntax noprefix
mov rax, rbx          # 该段按 Intel 语法(目的在左)
.att_syntax
movq %rbx, %rax       # 切回 AT&T

阅读混合风格的代码(如内核、旧代码)时常会用到;本知识库统一使用 AT&T。

若链接时出现 missing .note.GNU-stack section implies executable stack 警告,在源文件加一行关闭可执行栈标记即可,见模块二

常见陷阱(务必先读)

  1. 操作数顺序:AT&T 是 源, 目的movl %eax, %ebxebx = eax,与 Intel 语法相反,是看反汇编时最常见的误解来源。
  2. 立即数必须加 $movl $0x10, %eax 把常数 0x10 放入 eax;而 movl 0x10, %eax读内存地址 0x10 处的值!二者语义完全不同。
  3. 寄存器必须加 %:漏写 % 时汇编器会把寄存器名当作符号,报 undefined reference。
  4. 大小后缀尽量显式写:只有寄存器操作数时 GAS 能推断(mov %eax,%ebx 可汇编);一旦涉及内存或立即数,GAS 只给警告并套用默认 32 位大小("using default"),结果可能不是你要的——mov $1, (%rax) 被当作 32 位。养成显式写 b/w/l/q 的习惯。
  5. 写 32 位寄存器自动清零高 32 位movl $0, %eax 等价于把整个 %rax 清零(且更短);但写 8/16 位寄存器(如 %al)不影响其余位。详见模块一
  6. 内存到内存不允许movl (%eax), (%ebx) 非法,必须经寄存器中转。
  7. 只有 mov 接受 64 位立即数movq $0x1122334455667788, %rax 合法(汇编成 movabs);其他指令的立即数最多 32 位(符号扩展)。
  8. lea 不算内存leaq (%rdi,%rsi,4), %rax 只把地址算出来放进 rax,不访问内存movq (%rdi,%rsi,4), %rax 才会读内存。
  9. 比较方向cmp b, a 计算 a - b,条件跳转据此判断,注意别把 jg/jl 用反。详见模块四模块五
  10. 栈对齐call 指令执行前 %rsp 必须 16 字节对齐,详见模块六