跳转至

02-指令集与汇编语言

重要性:⭐⭐⭐⭐⭐ 实用度:⭐⭐⭐⭐ 学习时间:3天 必须掌握:是


为什么学这一章?

汇编语言是人与机器之间的桥梁。学习汇编能帮助你: - 理解C/C++代码的底层实现 - 调试复杂的程序崩溃问题 - 编写高性能的底层代码 - 理解编译器优化的原理

学完这一章,你将能够: - ✅ 阅读和理解x86-64汇编代码 - ✅ 编写简单的汇编程序 - ✅ 理解C代码与汇编的对应关系 - ✅ 使用汇编进行底层优化


📖 核心概念

1. 什么是汇编语言?

Text Only
┌─────────────────────────────────────────────────────────────────────┐
│                    从高级语言到机器码                                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  C/C++代码                                                          │
│  int add(int a, int b) {                                            │
│      return a + b;                                                  │
│  }                                                                  │
│      ↓ 编译器                                                        │
│  汇编代码                                                           │
│  add:                                                               │
│      mov  eax, edi      ; 将第一个参数(edi)移到eax                 │
│      add  eax, esi      ; 加上第二个参数(esi)                      │
│      ret                ; 返回(结果在eax中)                        │
│      ↓ 汇编器                                                        │
│  机器码(十六进制)                                                  │
│  89 F8 01 F0 C3                                                     │
│      ↓ CPU执行                                                       │
│  程序运行                                                           │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

汇编语言的特点: - 每条汇编指令对应一条或多条机器指令 - 直接操作寄存器和内存 - 与CPU架构紧密相关(x86, ARM, MIPS等) - 可读性比机器码好,但比高级语言差


2. x86-64汇编基础

基本语法格式

GAS
; AT&T语法(GCC默认)
movl $42, %eax      ; 将立即数42移到eax
addl %ebx, %eax     ; ebx加到eax
movl (%rdi), %eax   ; 从rdi指向的内存读取到eax

; Intel语法(Windows/MASM)
mov eax, 42         ; 将42移到eax
add eax, ebx        ; ebx加到eax
mov eax, [rdi]      ; 从rdi指向的内存读取到eax

本教程使用AT&T语法(Linux/GCC默认),特点: - 寄存器前加%(如%eax) - 立即数前加$(如$42) - 源操作数在前,目的操作数在后 - 内存寻址用()(如(%rdi)

常用指令分类

Text Only
┌─────────────────────────────────────────────────────────────────────┐
│                    x86-64常用指令分类                                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  1. 数据传送指令                                                     │
│  ┌───────────────────────────────────────────────────────────────┐ │
│  │  mov  src, dst    ; dst = src                                 │ │
│  │  push src         ; 压栈:rsp -= 8; [rsp] = src               │ │
│  │  pop  dst         ; 出栈:dst = [rsp]; rsp += 8               │ │
│  │  lea  src, dst    ; dst = &src(加载有效地址)                │ │
│  │  xchg src, dst    ; 交换src和dst                              │ │
│  └───────────────────────────────────────────────────────────────┘ │
│                                                                     │
│  2. 算术运算指令                                                     │
│  ┌───────────────────────────────────────────────────────────────┐ │
│  │  add  src, dst    ; dst += src                                │ │
│  │  sub  src, dst    ; dst -= src                                │ │
│  │  mul  src         ; rax *= src(无符号乘法)                   │ │
│  │  imul src         ; rax *= src(有符号乘法)                   │ │
│  │  div  src         ; rax /= src(无符号除法)                   │ │
│  │  idiv src         ; rax /= src(有符号除法)                   │ │
│  │  inc  dst         ; dst++                                     │ │
│  │  dec  dst         ; dst--                                     │ │
│  │  neg  dst         ; dst = -dst                                │ │
│  └───────────────────────────────────────────────────────────────┘ │
│                                                                     │
│  3. 逻辑运算指令                                                     │
│  ┌───────────────────────────────────────────────────────────────┐ │
│  │  and  src, dst    ; dst &= src                                │ │
│  │  or   src, dst    ; dst |= src                                │ │
│  │  xor  src, dst    ; dst ^= src                                │ │
│  │  not  dst         ; dst = ~dst                                │ │
│  │  shl  count, dst  ; dst <<= count(逻辑左移)                 │ │
│  │  shr  count, dst  ; dst >>= count(逻辑右移)                 │ │
│  │  sar  count, dst  ; dst >>= count(算术右移)                 │ │
│  └───────────────────────────────────────────────────────────────┘ │
│                                                                     │
│  4. 比较和跳转指令                                                   │
│  ┌───────────────────────────────────────────────────────────────┐ │
│  │  cmp  src, dst    ; 比较dst和src,设置标志位                   │ │
│  │  test src, dst    ; dst & src,设置标志位                      │ │
│  │  jmp  label       ; 无条件跳转到label                          │ │
│  │  je   label       ; 等于则跳转(ZF=1)                         │ │
│  │  jne  label       ; 不等于则跳转(ZF=0)                       │ │
│  │  jg   label       ; 大于则跳转(有符号)                       │ │
│  │  jl   label       ; 小于则跳转(有符号)                       │ │
│  │  ja   label       ; 大于则跳转(无符号)                       │ │
│  │  jb   label       ; 小于则跳转(无符号)                       │ │
│  │  call label       ; 调用函数                                   │ │
│  │  ret              ; 从函数返回                                 │ │
│  └───────────────────────────────────────────────────────────────┘ │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

3. 操作数类型

Text Only
┌─────────────────────────────────────────────────────────────────────┐
│                    x86-64操作数类型                                    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  1. 立即数(Immediate)                                              │
│     直接写在指令中的常数                                             │
│     mov $42, %eax      ; 将42移到eax                                │
│     add $10, %ebx      ; ebx加10                                    │
│                                                                     │
│  2. 寄存器(Register)                                               │
│     CPU内部的存储单元                                                │
│     mov %eax, %ebx     ; 将eax的值复制到ebx                         │
│     add %ecx, %edx     ; ecx加到edx                                 │
│                                                                     │
│  3. 内存(Memory)                                                   │
│     通过地址访问的内存数据                                           │
│                                                                     │
│     直接寻址:                                                       │
│     mov 0x1000, %eax   ; 从地址0x1000读取4字节到eax                 │
│                                                                     │
│     寄存器间接寻址:                                                  │
│     mov (%rbx), %eax   ; 从rbx指向的地址读取到eax                   │
│                                                                     │
│     基址+偏移寻址:                                                   │
│     mov 8(%rbx), %eax  ; 从rbx+8的地址读取到eax                     │
│                                                                     │
│     基址+索引+比例寻址:                                               │
│     mov (%rbx, %rcx, 2), %eax  ; 从rbx + rcx*2的地址读取            │
│                                                                     │
│     复杂寻址示例:                                                    │
│     mov 16(%rbx, %rcx, 4), %eax  ; 从rbx + rcx*4 + 16读取           │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

4. C代码与汇编对照

示例1:简单函数

C++
// C代码
int add(int a, int b) {
    return a + b;
}
GAS
# 汇编代码(AT&T语法)
add:
    push %rbp           # 保存旧的基址指针
    mov %rsp, %rbp      # 设置新的基址指针

    mov %edi, -4(%rbp)  # 保存参数a到栈
    mov %esi, -8(%rbp)  # 保存参数b到栈

    mov -4(%rbp), %edx  # 加载a到edx
    mov -8(%rbp), %eax  # 加载b到eax
    add %edx, %eax      # a + b,结果在eax

    pop %rbp            # 恢复基址指针
    ret                 # 返回

简化版本(优化后):

GAS
add:
    mov %edi, %eax      # 将第一个参数移到eax
    add %esi, %eax      # 加上第二个参数
    ret                 # 返回(结果在eax中)

示例2:条件判断

C++
// C代码
int max(int a, int b) {
    if (a > b) {
        return a;
    } else {
        return b;
    }
}
GAS
# 汇编代码
max:
    mov %edi, %eax      # eax = a
    cmp %esi, %edi      # 比较a和b
    jg .L_return_a      # 如果a > b,跳转到.L_return_a
    mov %esi, %eax      # 否则,eax = b
.L_return_a:
    ret                 # 返回(结果在eax中)

示例3:循环

C++
// C代码
int sum(int n) {
    int result = 0;
    for (int i = 1; i <= n; i++) {
        result += i;
    }
    return result;
}
GAS
# 汇编代码
sum:
    mov $0, %eax        # result = 0
    mov $1, %ecx        # i = 1
.L_loop:
    cmp %edi, %ecx      # 比较i和n
    jg .L_end           # 如果i > n,结束循环
    add %ecx, %eax      # result += i
    inc %ecx            # i++
    jmp .L_loop         # 继续循环
.L_end:
    ret                 # 返回result

5. 函数调用约定

System V AMD64 ABI(Linux/macOS)

Text Only
┌─────────────────────────────────────────────────────────────────────┐
│                    x86-64函数调用约定                                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  参数传递(前6个整数/指针参数):                                      │
│  ┌───────────────────────────────────────────────────────────────┐ │
│  │  第1参数:RDI                                                  │ │
│  │  第2参数:RSI                                                  │ │
│  │  第3参数:RDX                                                  │ │
│  │  第4参数:RCX                                                  │ │
│  │  第5参数:R8                                                   │ │
│  │  第6参数:R9                                                   │ │
│  │  第7+参数:通过栈传递                                           │ │
│  └───────────────────────────────────────────────────────────────┘ │
│                                                                     │
│  返回值:                                                            │
│  ├── 整数/指针:RAX                                                 │
│  ├── 128位整数:RAX(低64位)+ RDX(高64位)                         │
│  └── 浮点数:XMM0                                                   │
│                                                                     │
│  调用者保存的寄存器(Caller-saved):                                  │
│  ├── RAX, RCX, RDX, RSI, RDI, R8-R11                                │
│  └── 调用者需要在调用前保存这些寄存器                                 │
│                                                                     │
│  被调用者保存的寄存器(Callee-saved):                                │
│  ├── RBX, RBP, R12-R15                                              │
│  └── 被调用函数需要保存并在返回前恢复                                 │
│                                                                     │
│  栈对齐:                                                            │
│  └── 调用前RSP必须是16字节对齐                                       │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

函数调用示例

C++
// C代码
int callee(int a, int b, int c, int d, int e, int f, int g) {
    return a + b + c + d + e + f + g;
}

int caller() {
    return callee(1, 2, 3, 4, 5, 6, 7);
}
GAS
# 汇编代码
callee:
    push %rbp
    mov %rsp, %rbp

    # 前6个参数在寄存器中
    # a: %edi, b: %esi, c: %edx, d: %ecx, e: %r8d, f: %r9d

    # 第7个参数在栈上:16(%rbp)
    mov 16(%rbp), %eax  # g

    add %edi, %eax      # + a
    add %esi, %eax      # + b
    add %edx, %eax      # + c
    add %ecx, %eax      # + d
    add %r8d, %eax      # + e
    add %r9d, %eax      # + f

    pop %rbp
    ret

caller:
    push %rbp
    mov %rsp, %rbp

    sub $8, %rsp        # 栈对齐(16字节对齐)
    push $7             # 第7个参数压栈

    mov $6, %r9d        # 第6个参数
    mov $5, %r8d        # 第5个参数
    mov $4, %ecx        # 第4个参数
    mov $3, %edx        # 第3个参数
    mov $2, %esi        # 第2个参数
    mov $1, %edi        # 第1个参数

    call callee

    add $16, %rsp       # 清理栈
    pop %rbp
    ret

🧪 动手实验

实验1:编写第一个汇编程序

目的:学习汇编程序的基本结构

步骤

  1. 创建汇编文件

    GAS
    # hello.asm
    .section .data
        msg: .ascii "Hello, World!\n"
        len = . - msg
    
    .section .text
    .globl _start
    
    _start:
        # write(1, msg, len)
        mov $1, %rax        # syscall: write
        mov $1, %rdi        # fd: stdout
        mov $msg, %rsi      # buf: msg
        mov $len, %rdx      # count: len
        syscall
    
        # exit(0)
        mov $60, %rax       # syscall: exit
        mov $0, %rdi        # status: 0
        syscall
    

  2. 编译运行

    Bash
    as hello.asm -o hello.o
    ld hello.o -o hello
    ./hello
    

实验2:C与汇编混合编程

目的:学习C调用汇编函数

步骤

  1. 创建汇编文件

    GAS
    # add.asm
    .section .text
    .globl asm_add
    
    # int asm_add(int a, int b)
    asm_add:
        mov %edi, %eax      # eax = a
        add %esi, %eax      # eax += b
        ret
    

  2. 创建C文件

    C++
    // main.c
    #include <stdio.h>
    
    // 声明外部汇编函数
    extern int asm_add(int a, int b);
    
    int main() {
        int result = asm_add(10, 20);
        printf("10 + 20 = %d\n", result);
        return 0;
    }
    

  3. 编译链接

    Bash
    as add.asm -o add.o
    gcc -c main.c -o main.o
    gcc add.o main.o -o mixed_program
    ./mixed_program
    

实验3:观察编译器生成的汇编

目的:理解C代码如何翻译成汇编

步骤

  1. 创建C文件

    C++
    // test.c
    int factorial(int n) {
        if (n <= 1) return 1;
        return n * factorial(n - 1);
    }
    
    int main() {
        int result = factorial(5);
        return result;
    }
    

  2. 生成汇编代码(不同优化级别)

    Bash
    # 无优化
    gcc -S -O0 test.c -o test_O0.s
    
    # 中等优化
    gcc -S -O2 test.c -o test_O2.s
    
    # 最高优化
    gcc -S -O3 test.c -o test_O3.s
    

  3. 对比分析

    Bash
    # 对比文件大小
    ls -la test_*.s
    
    # 查看关键差异
    diff test_O0.s test_O3.s
    

实验4:使用内联汇编

目的:在C代码中嵌入汇编

步骤

  1. 创建C文件

    C++
    // inline_asm.c
    #include <stdio.h>  // 引入头文件
    
    int add_inline(int a, int b) {
        int result;
        __asm__ volatile (
            "add %1, %0\n\t"
            : "=r" (result)      // 输出操作数
            : "r" (a), "0" (b)   // "0" 约束使 b 与 result 共用同一寄存器
            :                    // 无破坏的寄存器
        );
        return result;
    }
    
    int main() {
        int result = add_inline(10, 20);
        printf("Result: %d\n", result);
        return 0;
    }
    

  2. 编译运行

    Bash
    gcc inline_asm.c -o inline_asm
    ./inline_asm
    


💡 核心要点总结

常用指令速查

指令 功能 示例
mov 数据传送 mov %eax, %ebx
push/pop 栈操作 push %rax / pop %rax
add/sub 加减 add %ebx, %eax
mul/imul 乘法 mul %ebx
div/idiv 除法 div %ebx
and/or/xor 逻辑运算 and %ebx, %eax
cmp 比较 cmp %ebx, %eax
jmp/je/jg 跳转 je label
call/ret 函数调用/返回 call func

寄存器用途速查

寄存器 用途
RAX 返回值、累加器
RBX 被调用者保存
RCX 第4参数、计数器
RDX 第3参数、数据
RSI 第2参数、源索引
RDI 第1参数、目的索引
RBP 基址指针
RSP 栈指针
R8-R9 第5-6参数
R10-R11 调用者保存
R12-R15 被调用者保存

C与汇编对应关系

C结构 汇编实现
函数 标签 + ret
参数 寄存器/栈传递
返回值 RAX寄存器
局部变量 栈空间
if/else cmp + 条件跳转
for/while 标签 + 条件跳转
函数调用 call + 参数设置

❓ 常见问题

Q1:AT&T语法和Intel语法有什么区别?

A:主要区别: - AT&T:源在前,目的在后;寄存器加%,立即数加$ - Intel:目的在前,源在后;无特殊前缀 - Linux/GCC默认AT&T,Windows/MASM使用Intel

Q2:为什么汇编代码中有那么多mov指令?

A:x86架构是CISC(复杂指令集),但现代CPU内部将复杂指令分解成简单操作。mov是最基本的操作,用于数据在寄存器和内存之间移动。

Q3:如何学习汇编编程?

A:建议步骤: 1. 先学习阅读汇编(从C代码生成汇编) 2. 理解函数调用约定 3. 尝试修改汇编代码 4. 最后尝试独立编写

Q4:汇编编程还有什么用?

A:现代应用: - 系统编程(操作系统、驱动) - 性能优化(关键路径) - 逆向工程和安全研究 - 嵌入式系统


📚 扩展阅读

  1. 《深入理解计算机系统》 - 第3章:程序的机器级表示
  2. 《x86-64汇编语言》 - 相关书籍
  3. Intel手册:Volume 2 - Instruction Set Reference
  4. 在线资源:x86asm.net

🎯 下一步

继续学习CPU与指令执行的后续内容,深入了解CPU微架构、流水线、分支预测和缓存机制。