ARM汇编指令学习

在学习操作系统课程的过程中,我接触到了ARM汇编语言。汇编语言是最接近机器码的编程语言,理解它对于掌握计算机体系结构、操作系统原理以及程序性能优化都至关重要。本文将通过一个快速排序算法的ARM64汇编实现,系统地介绍常用的ARM汇编指令及其应用场景。

一、ARM64架构基础

1.1 寄存器体系

ARM64(ARMv8-A)架构提供了31个通用寄存器(X0-X30)和一个特殊的零寄存器(XZR)。每个寄存器可以作为64位使用(X寄存器)或32位使用(W寄存器):

flowchart TB subgraph ARM64["ARM64 通用寄存器 (31个 + SP)"] direction TB subgraph G1["参数/返回值寄存器 - Caller-saved"] direction LR X0["X0
参数1/返回值"] X1["X1
参数2"] X2["X2
参数3"] X3["X3
参数4"] X4["X4
参数5"] X5["X5
参数6"] X6["X6
参数7"] X7["X7
参数8"] end subgraph G2["临时寄存器 - Caller-saved"] direction LR X8["X8
间接结果"] X9["X9-X15
临时变量"] X16["X16-X17
IP0/IP1"] X18["X18
平台保留"] end subgraph G3["Callee-saved 寄存器 - 函数必须保存"] direction LR X19["X19-X28
跨调用保持"] end subgraph G4["特殊用途寄存器"] direction LR X29["X29 (FP)
帧指针"] X30["X30 (LR)
返回地址"] SP["SP
栈指针"] XZR["XZR/WZR
零寄存器"] end end style G1 fill:#e3f2fd,stroke:#1976d2 style G2 fill:#fff3e0,stroke:#f57c00 style G3 fill:#e8f5e9,stroke:#388e3c style G4 fill:#fce4ec,stroke:#c2185b style X0 fill:#bbdefb style X29 fill:#f8bbd9 style X30 fill:#f8bbd9 style SP fill:#f8bbd9

64位 vs 32位: X0-X30 为64位寄存器,对应的 W0-W30 为其低32位。例如 W0 是 X0 的低32位。

寄存器用途说明:

  • X0-X7: 用于传递函数参数和返回值
  • X8: 间接结果寄存器
  • X9-X15: 临时寄存器(caller-saved)
  • X16-X17: 过程调用临时寄存器
  • X18: 平台寄存器(特殊用途)
  • X19-X28: callee-saved寄存器(被调用者保存)
  • X29: 帧指针(FP)
  • X30: 链接寄存器(LR,存储返回地址)
  • SP: 栈指针

1.2 调用约定

根据ARM的AAPCS64(Procedure Call Standard):

  • 前8个整型参数通过X0-X7传递
  • 返回值通过X0传递
  • X19-X28是callee-saved,函数必须保存和恢复
  • X9-X15是caller-saved,调用者负责保存

二、核心ARM汇编指令详解

2.1 数据传送指令

MOV - 数据移动

mov     x19, x0           // x19 = x0
mov     x2, #0            // x2 = 0

MOV指令用于在寄存器之间或将立即数传送到寄存器。

LDR - 加载数据

ldr     w5, [x19, x4, lsl #2]  // w5 = *(x19 + x4 * 4)
ldr     w6, [x19, x2, lsl #2]  // w6 = arr[i]

LDR从内存加载数据到寄存器。lsl #2表示左移2位(相当于乘以4),因为int类型占4字节。

寄存器位宽说明:

graph LR subgraph X0["X0 寄存器 (64位)"] direction LR HIGH["高32位"] --- LOW["W0 低32位"] end X0 --> |"用于"| ADDR["地址计算/指针操作"] LOW --> |"用于"| INT["int类型数据(32位)"] style HIGH fill:#f0f0f0 style LOW fill:#4CAF50,color:#fff style ADDR fill:#2196F3,color:#fff style INT fill:#FF9800,color:#fff

使用场景:

  • X寄存器: 用于地址计算、指针操作 (如 ldr w5, [X19, ...])
  • W寄存器: 用于int类型数据 (int占32位)

STR - 存储数据

str     w7, [x19, x2, lsl #2]  // *(x19 + x2 * 4) = w7

STR将寄存器的值存储到内存。

STP/LDP - 成对存储/加载

stp     x29, x30, [sp, #-32]!  // 预索引模式,先sp=sp-32,再存储
ldp     x19, x20, [sp, #16]    // 从sp+16处加载到x19和x20

STP和LDP用于同时操作两个寄存器,提高效率。感叹号!表示预索引模式。

2.2 算术和逻辑指令

ADD/SUB - 加法和减法

add     x2, x2, #1        // x2 = x2 + 1 (i++)
sub     x3, x1, #1        // x3 = x1 - 1 (j = n-1)

LSR - 逻辑右移

lsr     x4, x4, #1        // x4 = x4 >> 1 (除以2)

用于计算中点索引 n/2。

LSL - 逻辑左移

在索引计算中用于乘法:

lsl #2                    // 左移2位,相当于乘以4

2.3 比较和分支指令

CMP - 比较

cmp     x1, #1            // 比较 x1 和 1
cmp     x2, x3            // 比较 i 和 j
cmp     w6, w5            // 比较 arr[i] 和 pivot

CMP执行减法操作但不保存结果,只设置条件标志位(NZCV)。

条件标志位 (NZCV):

graph TB subgraph CPSR["CPSR 当前程序状态寄存器"] N["N (Negative)
结果为负"] Z["Z (Zero)
结果为零"] C["C (Carry)
进位/借位"] V["V (Overflow)
有符号溢出"] end style N fill:#ffcdd2 style Z fill:#c8e6c9 style C fill:#bbdefb style V fill:#fff9c4

示例: cmp x2, x3 (执行 x2 - x3)

情况x2x3结果NZ后续分支
情况153200b.gt 跳转 (5 > 3)
情况235-210b.gt 不跳转
情况333001b.eq 跳转 (相等)

条件分支指令

ble     L_ret             // 小于等于时跳转 (<=)
b.gt    L_done            // 大于时跳转 (>)
b.ge    L_right           // 大于等于时跳转 (>=)
b.le    L_swap            // 小于等于时跳转 (<=)
blt     L_no_left         // 小于时跳转 (<)

常用条件码:

  • EQ: 等于(Equal)
  • NE: 不等于(Not Equal)
  • GT: 大于(Greater Than)
  • GE: 大于等于(Greater or Equal)
  • LT: 小于(Less Than)
  • LE: 小于等于(Less or Equal)

B - 无条件跳转

b       L_left            // 无条件跳转到L_left标签

BL - 带链接的跳转(函数调用)

bl      _quicksort        // 调用函数,返回地址保存在X30(LR)

RET - 函数返回

ret                       // 返回到X30(LR)中保存的地址

2.4 汇编伪指令

.text                     // 代码段
.globl _quicksort         // 声明全局符号
.align 2                  // 4字节对齐(2^2=4)

三、实践案例:快速排序算法分析

3.0 快速排序算法流程图

在深入分析汇编代码之前,先通过流程图理解快速排序的整体逻辑:

flowchart TD A["quicksort(arr, n)"] --> B{"n <= 1 ?"} B -->|Yes| C["直接返回"] B -->|No| D["建立栈帧
保存 x19, x20, x29, x30"] D --> E["初始化
i=0, j=n-1
pivot=arr[n/2]"] E --> F["L_left: 左扫描
while arr[i] < pivot
i++"] F --> G["L_right: 右扫描
while arr[j] > pivot
j--"] G --> H{"i > j ?"} H -->|No| I["L_swap:
swap arr[i], arr[j]
i++, j--"] I --> F H -->|Yes| J["L_done: 分区完成
左: [0..j]
右: [i..n-1]"] J --> K["递归左半部分
quicksort(arr, j+1)"] K --> L["递归右半部分
quicksort(arr+i, n-i)"] L --> M["恢复寄存器并返回
x19, x20, x29, x30"] style A fill:#4CAF50,color:#fff style B fill:#FF9800,color:#fff style C fill:#9E9E9E,color:#fff style D fill:#2196F3,color:#fff style E fill:#2196F3,color:#fff style F fill:#9C27B0,color:#fff style G fill:#9C27B0,color:#fff style H fill:#FF9800,color:#fff style I fill:#E91E63,color:#fff style J fill:#00BCD4,color:#fff style K fill:#8BC34A,color:#fff style L fill:#8BC34A,color:#fff style M fill:#607D8B,color:#fff

3.1 函数框架

_quicksort:
    // 边界检查
    cmp     x1, #1
    ble     L_ret             // n <= 1 直接返回

    // 建立栈帧
    stp     x29, x30, [sp, #-32]!
    stp     x19, x20, [sp, #16]
    mov     x29, sp

关键点:

  1. 先检查递归终止条件,如果 n≤1 则直接返回,避免不必要的栈帧创建
  2. 使用STP一次保存两对寄存器,提高效率
  3. 总共分配32字节栈空间(保存4个64位寄存器)

3.2 初始化分区变量

    mov     x19, x0           // 保存数组基址
    mov     x20, x1           // 保存数组长度

    mov     x2, #0            // i = 0
    sub     x3, x1, #1        // j = n - 1

    // 选择中间元素作为pivot
    mov     x4, x1
    lsr     x4, x4, #1        // x4 = n / 2
    ldr     w5, [x19, x4, lsl #2]  // pivot = arr[n/2]

寄存器使用映射图:

寄存器保存内容说明
X0arr (初始)参数传递,后被X19替代
X1n (初始)参数传递,后被X20替代
X2i (左指针)左侧扫描索引
X3j (右指针)右侧扫描索引
X4n/2 (临时)计算pivot索引
W5pivot基准元素值
W6arr[i]左侧当前元素
W7arr[j]右侧当前元素
X19arr (保存)callee-saved,跨调用保持
X20n (保存)callee-saved,跨调用保持
X29FP帧指针
X30LR返回地址

寄存器生命周期可视化:

gantt title 寄存器生命周期 dateFormat X axisFormat %s section 参数寄存器 X0 arr参数 :a1, 0, 1 X1 n参数 :a2, 0, 1 section 循环变量 X2 i指针 :b1, 1, 4 X3 j指针 :b2, 1, 4 W5 pivot :b3, 1, 4 section Callee-saved X19 arr保存 :c1, 1, 5 X20 n保存 :c2, 1, 5

技巧:

  • 使用X19-X20保存需要跨函数调用保持的变量
  • 使用逻辑右移实现高效的除以2操作
  • 通过lsl #2实现数组索引计算: address = base + index * 4

3.3 双指针分区算法

L_left:
    cmp     x2, x3
    b.gt    L_done

    ldr     w6, [x19, x2, lsl #2]
    cmp     w6, w5
    b.ge    L_right           // arr[i] >= pivot
    add     x2, x2, #1
    b       L_left

L_right:
    cmp     x2, x3
    b.gt    L_done

    ldr     w7, [x19, x3, lsl #2]
    cmp     w7, w5
    b.le    L_swap            // arr[j] <= pivot
    sub     x3, x3, #1
    b       L_right

这段代码实现了经典的双指针分区算法:

  1. 左指针i向右扫描,找到 arr[i] ≥ pivot
  2. 右指针j向左扫描,找到 arr[j] ≤ pivot
  3. 交换两个元素,继续扫描

双指针分区过程可视化:

假设数组为 [9, 3, 7, 1, 8, 2, 6], pivot = 7 (中间元素)

flowchart TB subgraph Init["初始状态: pivot = arr[3] = 7"] direction LR I0["[0]
9
↑i"] ~~~ I1["[1]
3"] ~~~ I2["[2]
7"] ~~~ I3["[3]
1"] ~~~ I4["[4]
8"] ~~~ I5["[5]
2"] ~~~ I6["[6]
6
↑j"] end Init --> Step1 subgraph Step1["步骤1: arr[i]=9≥7, arr[j]=6≤7, 交换!"] direction LR S1_0["[0]
6
✓"] ~~~ S1_1["[1]
3
↑i"] ~~~ S1_2["[2]
7"] ~~~ S1_3["[3]
1"] ~~~ S1_4["[4]
8"] ~~~ S1_5["[5]
2
↑j"] ~~~ S1_6["[6]
9
✓"] end Step1 --> Step2 subgraph Step2["步骤2: arr[i]=3<7, i++; arr[i]=7≥7, arr[j]=2≤7, 交换!"] direction LR S2_0["[0]
6"] ~~~ S2_1["[1]
3"] ~~~ S2_2["[2]
2
✓"] ~~~ S2_3["[3]
1
↑i"] ~~~ S2_4["[4]
8
↑j"] ~~~ S2_5["[5]
7
✓"] ~~~ S2_6["[6]
9"] end Step2 --> Step3 subgraph Step3["步骤3: arr[i]=1<7, i++; arr[j]=8>7, j--; i>j 结束!"] direction LR S3_0["[0]
6"] ~~~ S3_1["[1]
3"] ~~~ S3_2["[2]
2"] ~~~ S3_3["[3]
1
↑j"] ~~~ S3_4["[4]
8
↑i"] ~~~ S3_5["[5]
7"] ~~~ S3_6["[6]
9"] end style I0 fill:#ffcdd2 style I6 fill:#bbdefb style S1_0 fill:#c8e6c9 style S1_6 fill:#c8e6c9 style S2_2 fill:#c8e6c9 style S2_5 fill:#c8e6c9 style Step3 fill:#fff9c4

分区步骤详解:

步骤ij操作说明数组状态
初始06arr[0]=9≥7, arr[6]=6≤7[9, 3, 7, 1, 8, 2, 6]
115交换后 i++, j–[6, 3, 7, 1, 8, 2, 9]
225arr[1]=3<7, 继续 i++[6, 3, 7, 1, 8, 2, 9]
334arr[2]=7≥7, arr[5]=2≤7, 交换[6, 3, 2, 1, 8, 7, 9]
443arr[3]=1<7, i++; arr[4]=8>7, j–[6, 3, 2, 1, 8, 7, 9]
结束43i > j, 分区完成左:[6,3,2,1] 右:[8,7,9]
flowchart LR subgraph Final["分区结果"] direction LR subgraph Left["左半部分 (≤ pivot)"] direction LR L0["6"] ~~~ L1["3"] ~~~ L2["2"] ~~~ L3["1"] end DIV["||
分界
||"] subgraph Right["右半部分 (≥ pivot)"] direction LR R0["8"] ~~~ R1["7"] ~~~ R2["9"] end Left --> DIV --> Right end Final --> Recurse["递归排序两个子数组"] style Left fill:#c8e6c9,stroke:#2e7d32 style Right fill:#ffcdd2,stroke:#c62828 style DIV fill:#fff,stroke:#666 style Recurse fill:#e3f2fd,stroke:#1565c0

3.4 元素交换

L_swap:
    str     w7, [x19, x2, lsl #2]  // arr[i] = w7
    str     w6, [x19, x3, lsl #2]  // arr[j] = w6

    add     x2, x2, #1
    sub     x3, x3, #1
    b       L_left

直接在内存中交换两个元素,利用之前已经加载到寄存器的值。

3.5 递归处理子数组

L_done:
    // 递归左半部分: [0 .. j]
    mov     x0, x19
    add     x1, x3, #1        // left_len = j + 1
    cmp     x1, #2
    blt     L_no_left
    bl      _quicksort

L_no_left:
    // 递归右半部分: [i .. n-1]
    add     x0, x19, x2, lsl #2   // base = arr + i
    mov     x1, x20
    sub     x1, x1, x2            // right_len = n - i
    cmp     x1, #2
    blt     L_no_right
    bl      _quicksort

L_no_right:

优化点:

  1. 递归前检查子数组长度,长度<2时不递归
  2. 使用BL指令调用函数,自动保存返回地址到X30
  3. 右半部分通过地址偏移直接传递子数组起始位置

3.6 栈帧清理和返回

    ldp     x19, x20, [sp, #16]
    ldp     x29, x30, [sp], #32

L_ret:
    ret

使用LDP成对恢复寄存器,后索引模式], #32表示先加载再调整SP。

四、内存寻址模式详解

ARM64支持多种灵活的寻址模式:

4.1 基址加偏移

ldr w5, [x19, x4, lsl #2]    // *(base + offset * scale)

可视化说明:

graph TB subgraph Memory["内存布局 (int数组, 每个元素4字节)"] direction LR M0["arr[0]
0x1000"] --- M1["arr[1]
0x1004"] --- M2["arr[2]
0x1008"] --- M3["arr[3]
0x100C"] --- M4["arr[4]
0x1010"] --- M5["arr[5]
0x1014"] end X19["X19 = 0x1000
(基址)"] --> M0 X4["X4 = 3
(索引)"] --> CALC["地址计算:
0x1000 + (3 << 2)
= 0x1000 + 12
= 0x100C"] CALC --> M3 style M3 fill:#4CAF50,color:#fff style X19 fill:#2196F3,color:#fff style X4 fill:#FF9800,color:#fff

计算过程: ldr w5, [x19, x4, lsl #2]

  • 目标地址 = x19 + (x4 « 2) = 0x1000 + (3 × 4) = 0x100C
  • 读取 arr[3] 的值到 w5

4.2 预索引模式

stp x29, x30, [sp, #-32]!    // sp = sp - 32; *(sp) = x29, x30

先修改基址寄存器,再进行内存操作。

栈操作可视化:

graph LR subgraph Before["操作前"] direction TB B1["..."] B2["← SP"] end Before -->|"stp x29, x30, [sp, #-32]!"| After subgraph After["操作后"] direction TB A1["..."] A2["x30 (LR)"] A3["x29 (FP)"] A4["← 新SP (sp-32)"] end style Before fill:#ffcdd2 style After fill:#c8e6c9

执行顺序:

  1. sp = sp - 32 (先调整栈指针)
  2. 将x29, x30存入新SP位置

4.3 后索引模式

ldp x29, x30, [sp], #32      // x29, x30 = *(sp); sp = sp + 32

先进行内存操作,再修改基址寄存器。

栈恢复可视化:

graph LR subgraph Before2["操作前"] direction TB C1["..."] C2["x30 (LR)"] C3["x29 (FP)"] C4["← SP"] end Before2 -->|"ldp x29, x30, [sp], #32"| After2 subgraph After2["操作后"] direction TB D1["..."] D2["← 新SP (sp+32)"] end style Before2 fill:#c8e6c9 style After2 fill:#e3f2fd

执行顺序:

  1. 从SP读取x29, x30 (先读取数据)
  2. sp = sp + 32 (再调整栈指针)

五、栈帧管理

栈是函数调用的核心机制。在ARM64中,栈向下增长(从高地址到低地址)。

5.1 快速排序的栈帧结构

graph LR subgraph S1["函数调用前"] direction TB A1["..."] A2["← SP旧"] A3[" "] end S1 -->|"分配32字节"| S2 subgraph S2["建立栈帧后"] direction TB B1["..."] B2["X30 (LR) SP+24"] B3["X29 (FP) SP+16"] B4["X20 SP+8"] B5["X19 ← SP新"] end S2 -->|"释放32字节"| S3 subgraph S3["函数返回后"] direction TB C1["..."] C2["← SP"] C3[" "] end style S1 fill:#fff3e0 style S2 fill:#e8f5e9 style S3 fill:#e3f2fd

栈帧内存布局:

偏移内容说明
SP+24X30 (LR)返回地址
SP+16X29 (FP)帧指针
SP+8X20保存的n值
SPX19保存的arr地址

5.2 栈帧操作详解

// 建立栈帧 (函数入口)
stp     x29, x30, [sp, #-32]!   // 1) sp=sp-32  2) 存储FP和LR
stp     x19, x20, [sp, #16]     // 在sp+16位置存储x19,x20
mov     x29, sp                 // 设置当前帧指针

// 清理栈帧 (函数出口)
ldp     x19, x20, [sp, #16]     // 从sp+16恢复x19,x20
ldp     x29, x30, [sp], #32     // 1) 恢复FP和LR  2) sp=sp+32
ret                             // 返回到LR地址

内存布局对应关系:

  • [sp] → X19 (第一个callee-saved寄存器)
  • [sp, #8] → X20 (第二个callee-saved寄存器)
  • [sp, #16] → X29/FP (帧指针)
  • [sp, #24] → X30/LR (返回地址)

关键要点:

  • 使用!的预索引模式,先调整SP再存储,确保栈对齐
  • 使用后索引模式恢复,先读取再调整SP
  • callee-saved寄存器(X19-X28)必须保存和恢复
  • 栈必须保持16字节对齐(AAPCS64要求)

六、性能优化技巧

通过这个快速排序实现,可以学到以下优化技巧:

6.1 优化技巧总结

优化技术实现方式性能提升
减少内存访问使用X19-X20保存arr和n避免重复从栈读取
批量操作STP/LDP成对存储/加载减少指令数,提高吞吐
早期返回n≤1时不建栈帧直接返回避免不必要的栈操作
位运算优化LSR实现除法,LSL实现乘法比算术指令快数倍
寄存器重用W6,W7在交换时直接使用减少LDR次数
指令合并索引计算与LDR合并减少ADD指令

6.2 优化实例对比

未优化版本 (假设的C语言直译):

// 计算 arr[i] 的地址 (5条指令)
mov     x10, #4
mul     x11, x2, x10      // x11 = i * 4
add     x12, x19, x11     // x12 = base + offset
ldr     w6, [x12]         // 加载

优化后版本 (实际代码,1条指令):

ldr     w6, [x19, x2, lsl #2]  // 地址计算+加载合并!

性能对比:

方法指令数周期数相对速度
未优化(分步)5~81x
优化(合并)1~32.7x

6.3 关键优化点分析

mindmap root((优化策略)) 寻址优化 索引缩放寻址 一条指令完成计算 寄存器分配 X19-X28 跨调用保存 X2-X3 循环变量 W5-W7 临时数据 分支优化 先检查边界条件 减少嵌套分支 批量操作 STP/LDP成对操作 减少指令数
  1. 使用索引缩放寻址模式

    [base, index, lsl #shift]  // 一条指令完成: base + (index << shift)
    
  2. 选择合适的寄存器

    • X19-X28: 存储跨函数调用需要保留的值(arr, n)
    • X2-X3: 存储循环变量(i, j),无需保存
    • W5-W7: 存储临时数据(pivot, arr[i], arr[j])
  3. 减少分支预测失败

    // 好的做法: 先检查边界,大概率情况
    cmp     x2, x3
    b.gt    L_done       // 大部分时间 i <= j
    
    // 避免: 复杂的嵌套条件分支
    

七、课后练习:测试代码

以下是配套的C语言测试程序:

#include <stdio.h>

void quicksort(int *arr, int n);

int main() {
    int a[] = {9, 3, 7, 1, 8, 2, 6, 4, 5, 0, 10};
    int n = sizeof(a) / sizeof(a[0]);

    quicksort(a, n);

    for (int i = 0; i < n; ++i)
        printf("%d ", a[i]);
    printf("\n");
    return 0;
}

qsort.s

    .text
    .globl _quicksort
    .align 2

// void quicksort(int *arr, int n);
// x0 = arr
// x1 = n
_quicksort:
    // if (n <= 1) return;
    cmp     x1, #1
    ble     L_ret             // 小于等于 1 直接返回(不建栈帧)

    // 建立栈帧,保存 callee-saved 寄存器
    stp     x29, x30, [sp, #-32]!
    stp     x19, x20, [sp, #16]
    mov     x29, sp

    mov     x19, x0           // x19 = arr base
    mov     x20, x1           // x20 = n

    // i = 0, j = n - 1
    mov     x2, #0            // x2 = i
    sub     x3, x1, #1        // x3 = j = n-1

    // pivot = arr[n/2]
    mov     x4, x1
    lsr     x4, x4, #1        // x4 = n / 2
    ldr     w5, [x19, x4, lsl #2] // w5 = pivot

// ===== 分区循环 =====
// 外层从左扫描开始
L_left:
    // while (i <= j && arr[i] < pivot) i++
    cmp     x2, x3
    b.gt    L_done            // i > j,分区结束

    ldr     w6, [x19, x2, lsl #2]  // w6 = arr[i]
    cmp     w6, w5
    b.ge    L_right           // arr[i] >= pivot,开始从右侧扫描
    add     x2, x2, #1        // i++
    b       L_left

// 从右侧扫描:while (i <= j && arr[j] > pivot) j--
L_right:
    cmp     x2, x3
    b.gt    L_done            // i > j,分区结束

    ldr     w7, [x19, x3, lsl #2]  // w7 = arr[j]
    cmp     w7, w5
    b.le    L_swap            // arr[j] <= pivot,交换
    sub     x3, x3, #1        // j--
    b       L_right

// 交换 arr[i] 和 arr[j]
L_swap:
    // 此时:
    // w6 = arr[i]
    // w7 = arr[j]
    str     w7, [x19, x2, lsl #2]
    str     w6, [x19, x3, lsl #2]

    add     x2, x2, #1        // i++
    sub     x3, x3, #1        // j--
    b       L_left            // 回到从左扫描

// ===== 分区完成,递归两边 =====
L_done:
    // 现在:
    // 左半部分索引范围 [0 .. j]
    // 右半部分索引范围 [i .. n-1]
    //
    // 左长度 left_len = j + 1
    // 右长度 right_len = n - i

    // ---- 递归左半部分 ----
    mov     x0, x19           // base = arr
    add     x1, x3, #1        // left_len = j + 1
    cmp     x1, #2            // 长度 <=1 则不递归
    blt     L_no_left
    bl      _quicksort
L_no_left:

    // ---- 递归右半部分 ----
    add     x0, x19, x2, lsl #2   // base = arr + i
    mov     x1, x20               // n
    sub     x1, x1, x2            // right_len = n - i
    cmp     x1, #2
    blt     L_no_right
    bl      _quicksort
L_no_right:

    // 恢复寄存器 & 返回
    ldp     x19, x20, [sp, #16]
    ldp     x29, x30, [sp], #32

L_ret:
    ret

编译和运行:

# macOS (ARM64)
gcc -c qsort.s -o qsort.o
gcc main.c qsort.o -o qsort
./qsort

# 输出: 0 1 2 3 4 5 6 7 8 9 10·

八、总结

graph TB subgraph Knowledge["ARM64汇编知识体系"] direction TB A["基础指令
MOV, LDR, STR
ADD, SUB"] B["控制流
CMP, B, BL, RET"] C["内存管理
栈帧, 寄存器保存"] D["寻址模式
基址, 索引, 预/后索引"] E["调用约定
AAPCS64"] F["优化技巧
位运算, 批量操作"] end A --> G["快速排序实现"] B --> G C --> G D --> G E --> G F --> G style A fill:#e3f2fd style B fill:#f3e5f5 style C fill:#e8f5e9 style D fill:#fff3e0 style E fill:#fce4ec style F fill:#e0f7fa style G fill:#4CAF50,color:#fff

通过实现快速排序算法,我们系统学习了:

  1. 基础指令: MOV, LDR, STR, ADD, SUB等数据处理指令
  2. 控制流: CMP, B, BL, RET等分支和跳转指令
  3. 内存管理: 栈帧创建、寄存器保存与恢复
  4. 寻址模式: 基址偏移、索引缩放、预/后索引
  5. 调用约定: 参数传递、寄存器使用规范
  6. 优化技巧: 减少内存访问、使用位运算

ARM汇编虽然语法简洁,但其强大的寻址模式和丰富的指令集使其能够编写出高效的底层代码。掌握汇编语言不仅能帮助我们理解编译器的工作原理,还能在性能关键场景下进行精细优化。

附录:ARM64汇编指令速查表

A. 数据传送指令

指令语法功能
MOVmov Xd, XnXd = Xn (寄存器复制)
MOVmov Xd, #immXd = 立即数
LDRldr Wd, [Xn]Wd = *(Xn) (从内存加载)
LDRldr Wd, [Xn, #off]Wd = *(Xn + off)
STRstr Wd, [Xn]*(Xn) = Wd (存储到内存)
STPstp Xd1, Xd2, [Xn]成对存储两个寄存器
LDPldp Xd1, Xd2, [Xn]成对加载两个寄存器

B. 算术和逻辑指令

指令语法功能
ADDadd Xd, Xn, XmXd = Xn + Xm
ADDadd Xd, Xn, #immXd = Xn + 立即数
SUBsub Xd, Xn, XmXd = Xn - Xm
MULmul Xd, Xn, XmXd = Xn * Xm
LSLlsl Xd, Xn, #immXd = Xn « imm (逻辑左移)
LSRlsr Xd, Xn, #immXd = Xn » imm (逻辑右移)
ANDand Xd, Xn, XmXd = Xn & Xm (按位与)
ORRorr Xd, Xn, XmXd = Xn | Xm (按位或)
EOReor Xd, Xn, XmXd = Xn ^ Xm (按位异或)

C. 比较和分支指令

指令语法功能
CMPcmp Xn, Xm比较 Xn 和 Xm,设置标志位
CMPcmp Xn, #imm比较 Xn 和立即数
Bb label无条件跳转到label
BLbl label调用函数,返回地址存入LR
B.EQb.eq label相等时跳转 (Z=1)
B.NEb.ne label不等时跳转 (Z=0)
B.GTb.gt label大于时跳转 (有符号)
B.GEb.ge label大于等于时跳转 (有符号)
B.LTb.lt label小于时跳转 (有符号)
B.LEb.le label小于等于时跳转 (有符号)
RETret返回 (跳转到LR)

D. 寻址模式速查

寻址模式示例含义
基址寻址[Xn]*(Xn)
偏移寻址[Xn, #16]*(Xn + 16)
索引寻址[Xn, Xm]*(Xn + Xm)
缩放索引[Xn, Xm, lsl #2](Xn + Xm4)
预索引[Xn, #16]!Xn += 16; *(Xn)
后索引[Xn], #16*(Xn); Xn += 16

E. 常用伪指令

伪指令功能
.text声明代码段
.data声明数据段
.globl symbol声明全局符号
.align n2^n 字节对齐
.word value定义4字节数据
.quad value定义8字节数据
.ascii "str"定义字符串(不含结束符)
.asciz "str"定义字符串(含\0结束符)