Practical Assembly Language Examples for Beginners – wiki基地


汇编语言实战:新手入门示例详解

前言:揭开汇编语言的神秘面纱

在计算机科学的浩瀚宇宙中,汇编语言(Assembly Language)一直被视为一门神秘而深奥的艺术。它位于高级语言(如Python、Java、C++)和机器码之间,是直接与计算机硬件沟通的桥梁。对于许多初学者而言,汇编语言似乎是遥不可及的禁区,充斥着寄存器、内存地址和晦涩的指令。

然而,掌握汇编语言并非不可能的任务,反而是一次极其有益的探索。它能让你:
1. 深入理解计算机工作原理: 了解CPU如何执行指令、数据如何在内存中存储和操作。
2. 优化代码性能: 在某些对性能要求极高的场景下,汇编代码可以实现比高级语言更极致的优化。
3. 调试和逆向工程: 理解程序崩溃的深层原因,或分析已编译程序的行为。
4. 操作系统和嵌入式系统开发: 在这些领域,直接操作硬件是常态。
5. 培养底层思维: 提升解决问题的能力,对更高级别的抽象有更深刻的认识。

本文旨在为完全的初学者提供一系列实用且易于理解的汇编语言示例。我们将从最基础的概念开始,逐步深入到更复杂的程序结构,让你亲身体验汇编语言的魅力。我们将主要以 x86-64 架构NASM 汇编器语法 为例,因为它们在现代计算机中广泛应用,且NASM语法相对直观。

第一章:汇编语言基础概念速览

在深入实践之前,我们需要对一些核心概念有一个初步的认识。

1. 计算机体系结构概览

一台计算机的核心包括:
* 中央处理器(CPU):执行指令、进行计算。
* 内存(RAM):存储程序和数据。
* 总线(Bus):连接CPU、内存及其他设备的通信通道。

汇编语言就是直接向CPU“发号施令”的语言。

2. 寄存器(Registers)

寄存器是CPU内部极高速的小型存储单元,用于临时存放数据和地址。它们是CPU工作的基础。在x86-64架构中,常见的通用寄存器有:
* RAX, RBX, RCX, RDX:通用寄存器,通常用于算术运算、函数返回值(RAX)、循环计数(RCX)等。
* RSI, RDI:通常用作源索引和目标索引,在数据传输和字符串操作中很常见。
* RBP, RSP:基址指针(Base Pointer)和栈指针(Stack Pointer),用于管理函数调用栈。
* R8 – R15:更多的通用寄存器,扩展了64位程序的可用空间。
* RIP:指令指针(Instruction Pointer),指向下一条要执行的指令的内存地址。
* RFLAGS:标志寄存器,存放各种状态标志,如零标志(ZF)、进位标志(CF)、符号标志(SF)等,影响条件跳转指令。

3. 内存组织(Memory Organization)

内存被组织成一系列连续的字节,每个字节都有一个唯一的地址。程序运行时,内存通常分为几个区域:
* 代码段(.text):存放可执行的机器指令。
* 数据段(.data):存放已初始化且在程序运行期间保持不变的数据(如常量字符串)。
* BSS段(.bss):存放未初始化或初始化为零的数据。
* 栈(Stack):LIFO(后进先出)结构,用于存储局部变量、函数参数、返回地址等。由RSP和RBP管理。
* 堆(Heap):用于动态内存分配。

4. 指令集(Instruction Set)

指令是CPU能识别并执行的基本操作。每条指令通常由一个操作码(Opcode)和零个或多个操作数(Operands)组成。例如:
* MOV dest, src:将源操作数的值移动到目标操作数。
* ADD dest, src:将源操作数的值加到目标操作数。
* SUB dest, src:将源操作数的值从目标操作数减去。
* INC dest:将目标操作数的值加1。
* DEC dest:将目标操作数的值减1。
* CMP op1, op2:比较两个操作数,结果影响RFLAGS寄存器。
* JMP label:无条件跳转到指定标签。
* JE label:如果相等则跳转。
* CALL func:调用一个函数(将返回地址压栈并跳转)。
* RET:从函数返回(从栈中弹出返回地址并跳转)。

5. 寻址模式(Addressing Modes)

寻址模式是指CPU如何找到操作数所在的位置。常见的有:
* 立即数寻址(Immediate):操作数直接写在指令中(例如 MOV RAX, 10)。
* 寄存器寻址(Register):操作数是寄存器(例如 MOV RBX, RAX)。
* 直接寻址(Direct):操作数是内存中的一个固定地址(例如 MOV RAX, [my_variable])。
* 寄存器间接寻址(Register Indirect):操作数在内存中,其地址存放在一个寄存器中(例如 MOV RAX, [RBX])。
* 基址变址寻址(Base-Index):地址由基址寄存器 + 变址寄存器 + 比例因子 + 偏移量构成(例如 MOV RAX, [RBP + RSI*4 + 10h])。

6. 数据类型(Data Types)

汇编语言没有内置的复杂数据类型,通常只有字节大小的描述:
* BYTE:1 字节 (8 位)
* WORD:2 字节 (16 位)
* DWORD:4 字节 (32 位)
* QWORD:8 字节 (64 位)

第二章:环境搭建与工具链

要编写和运行汇编程序,你需要以下工具:
1. 汇编器(Assembler):将汇编源文件(.asm)转换为机器码(目标文件 .o)。
* NASM (Netwide Assembler):跨平台,语法简洁,推荐使用。
* MASM (Microsoft Macro Assembler):Windows平台,微软官方。
* GAS (GNU Assembler):Linux平台,GCC的默认汇编器,使用AT&T语法(与Intel语法有差异)。
2. 链接器(Linker):将目标文件(.o)与其他库文件链接起来,生成可执行文件。
* LD (GNU Linker):Linux平台。
* LINK (Microsoft Linker):Windows平台。
3. 文本编辑器/IDE:编写代码,如VS Code, Notepad++, Vim, Emacs等。
4. 调试器(Debugger):GDB (Linux), WinDbg/OllyDbg (Windows)。

本文示例将主要基于 Linux 环境和 NASM 汇编器。

安装 NASM (Linux):
bash
sudo apt update
sudo apt install nasm

编译和链接命令示例:
假设你的汇编文件名为 example.asm
1. 汇编: nasm -f elf64 example.asm -o example.o
* -f elf64:指定输出格式为64位ELF格式(Linux可执行文件)。
* -o example.o:指定输出的目标文件名为 example.o
2. 链接: ld example.o -o example
* example.o:输入的目标文件。
* -o example:指定输出的可执行文件名为 example
3. 执行: ./example

第三章:实践示例:从基础到进阶

现在,让我们通过一系列具体的代码示例来学习汇编语言。

示例 A: “Hello, World!” – 第一个程序

这是所有编程语言的起点。在汇编语言中,打印字符串需要调用操作系统提供的系统调用(System Call)。在 Linux x86-64 环境下,我们使用 syscall 指令,并通过特定寄存器传递参数。

hello.asm

“`assembly
; hello.asm – 打印 “Hello, World!” 到控制台

section .data ; 数据段 – 存放已初始化数据
msg db “Hello, World!”, 0x0A ; 要打印的字符串,0x0A是换行符
len equ $ – msg ; 字符串长度,equ是等值定义,$表示当前地址

section .text ; 代码段 – 存放可执行指令
global _start ; 声明 _start 为全局符号,程序入口点

_start:
; 调用 write 系统调用 (syscall number 1)
; Linux x86-64 syscall convention:
; rax: syscall number
; rdi: arg1 (file descriptor)
; rsi: arg2 (buffer address)
; rdx: arg3 (count/length)

mov     rax, 1      ; syscall number 1 (sys_write)
mov     rdi, 1      ; arg1: file descriptor 1 (stdout - 标准输出)
mov     rsi, msg    ; arg2: buffer address (字符串地址)
mov     rdx, len    ; arg3: count (字符串长度)
syscall             ; 执行系统调用

; 调用 exit 系统调用 (syscall number 60)
mov     rax, 60     ; syscall number 60 (sys_exit)
mov     rdi, 0      ; arg1: exit code 0 (成功退出)
syscall             ; 执行系统调用

“`

编译、链接与执行:

bash
nasm -f elf64 hello.asm -o hello.o
ld hello.o -o hello
./hello

预期输出:
Hello, World!

代码解释:
* section .datasection .text:定义了数据段和代码段。
* msg db "Hello, World!", 0x0A:在数据段定义了一个字节序列 msg,包含了字符串和换行符。db 表示 “define byte”。
* len equ $ - msg:计算 msg 的长度。$ 代表当前指令的地址。
* global _start:告诉链接器程序的入口点是 _start 标签。
* mov rax, 1:将系统调用号1(sys_write)放入 RAX 寄存器。
* mov rdi, 1:将文件描述符1(标准输出)放入 RDI 寄存器。
* mov rsi, msg:将字符串 msg 的地址放入 RSI 寄存器。
* mov rdx, len:将字符串长度 len 放入 RDX 寄存器。
* syscall:触发系统调用。
* mov rax, 60mov rdi, 0:设置 sys_exit 系统调用号和退出码0。
* 第二个 syscall:退出程序。

示例 B: 基本算术运算

我们将演示加法、减法、乘法和除法。

arithmetic.asm

“`assembly
; arithmetic.asm – 演示基本算术运算

section .data
msg_add db “Sum: “, 0
msg_sub db “Diff: “, 0
msg_mul db “Prod: “, 0
msg_div db “Quot: “, 0
msg_rem db “Rem: “, 0
newline db 0x0A, 0 ; 换行符

section .bss
; 用于存储数字的ASCII字符串表示
num_buffer resb 32

section .text
global _start

_start:
; — 加法: 10 + 5 —
mov rax, 10
add rax, 5 ; RAX = 15
call print_num ; 打印 RAX
call print_msg_nl ; 打印换行

; --- 减法: 15 - 7 ---
mov     rax, 15
sub     rax, 7          ; RAX = 8
call    print_num       ; 打印 RAX
call    print_msg_nl    ; 打印换行

; --- 乘法: 8 * 3 ---
mov     rax, 8          ; 被乘数在RAX
mov     rbx, 3          ; 乘数在RBX
mul     rbx             ; 无符号乘法。结果高64位在RDX,低64位在RAX。
                        ; (8 * 3 = 24)。 RAX = 24, RDX = 0
call    print_num       ; 打印 RAX
call    print_msg_nl    ; 打印换行

; --- 除法: 25 / 4 ---
mov     rax, 25         ; 被除数低64位在RAX
mov     rdx, 0          ; 被除数高64位在RDX (除法时要清零RDX)
mov     rbx, 4          ; 除数在RBX
div     rbx             ; 无符号除法。商在RAX (25/4=6),余数在RDX (25%4=1)。

call    print_num       ; 打印商 (RAX)
call    print_msg_nl    ; 打印换行

; 打印余数 (RDX)
mov     rax, rdx        ; 将余数移到RAX以便print_num函数处理
call    print_num
call    print_msg_nl    ; 打印换行

; 退出程序
mov     rax, 60
mov     rdi, 0
syscall

; — 辅助函数:将RAX中的数字转换为ASCII字符串并打印 —
; 需要一个将整数转换为ASCII字符串的函数,这里简化处理,假设只有一个数字,但我们为了通用性
; 仍然需要一个能够处理多位数字的函数。
; 注意:这个辅助函数通常会比较复杂,这里提供一个简化的版本,用于将RAX中的整数打印出来
; 实际项目中通常会使用库函数或者更健壮的转换逻辑。
; 为了文章长度,我们提供一个相对简单的但不完全通用的数字打印函数,
; 实际转换需要考虑数字的符号和多位数的转换,逆序存储再打印。
; 这里我们直接使用一个简化的,假设数字不是特别大,并且是正数。
print_num:
push rax ; 保存 rax (函数返回后需要恢复)
push rcx ; 保存 rcx
push rdx ; 保存 rdx
push rdi ; 保存 rdi

mov     rdi, num_buffer + 31 ; 指向缓冲区的末尾
mov     byte [rdi], 0   ; 写入字符串结束符
dec     rdi             ; 指向倒数第二个位置
mov     rcx, 0          ; 计数器,记录转换的位数

cmp     rax, 0
je      .print_zero     ; 如果数字为0,直接打印'0'

.loop_convert:
mov rdx, 0 ; 清空 RDX,为除法做准备
mov rbx, 10 ; 除数为10
div rbx ; RAX /= 10, RDX = RAX % 10 (余数)

add     dl, '0'         ; 将余数转换为ASCII字符
mov     byte [rdi], dl  ; 存储到缓冲区
dec     rdi             ; 向前移动一个字节
inc     rcx             ; 增加位数计数

cmp     rax, 0          ; 如果商为0,则转换完成
jne     .loop_convert

.print_num_string:
inc rdi ; rdi现在指向数字字符串的开头
mov rax, 1 ; sys_write
mov rsi, rdi ; 字符串地址
mov rdx, rcx ; 字符串长度
mov rdi, 1 ; stdout
syscall

pop     rdi             ; 恢复寄存器
pop     rdx
pop     rcx
pop     rax
ret

.print_zero:
mov byte [rdi], ‘0’
mov rcx, 1
jmp .print_num_string

; — 辅助函数:打印换行符 —
print_msg_nl:
push rax
push rdi
push rsi
push rdx
mov rax, 1
mov rdi, 1
mov rsi, newline
mov rdx, 1
syscall
pop rdx
pop rsi
pop rdi
pop rax
ret
“`

编译、链接与执行:

bash
nasm -f elf64 arithmetic.asm -o arithmetic.o
ld arithmetic.o -o arithmetic
./arithmetic

预期输出:
15
8
24
6
1

代码解释:
* section .bss:定义了一个未初始化数据段 num_buffer,用于存放转换后的数字字符串。
* mul rbx:执行无符号乘法。RAX 乘以 RBX。结果的低64位存储在 RAX,高64位存储在 RDX。对于小数字,RDX 通常为0。
* div rbx:执行无符号除法。RDX:RAX(128位)除以 RBX。商存储在 RAX,余数存储在 RDX
* print_num 辅助函数:这是一个重要的概念。由于汇编语言无法直接打印数字,需要将其转换为ASCII字符序列。
* 它通过反复除以10取余数的方法,将数字的每一位从低到高提取出来。
* 余数(0-9)加上 '0' 的ASCII值,就可以得到对应的字符。
* 由于余数是从低位到高位得到的,所以需要将字符存储在一个缓冲区中,从后往前填充,最后再从前往后打印。
* 注意函数内部保存和恢复了所有可能被修改的寄存器 (pushpop),这是良好汇编编程实践的一部分。

示例 C: 数据移动与交换

数据移动是汇编语言中最基本的操作。交换两个变量的值是常见的操作。

swap.asm

“`assembly
; swap.asm – 演示数据移动与内存交换

section .data
var1_val dq 1234567890ABCDh ; 64位十六进制数
var2_val dq 0EFCDA9876543210h ; 另一个64位十六进制数

msg_before db "Before swap: var1 = ", 0
msg_after  db "After swap:  var1 = ", 0
msg_var2   db ", var2 = ", 0
newline    db 0x0A, 0

section .bss
num_buffer resb 32

section .text
global _start

_start:
; 打印交换前的值
mov rsi, msg_before
call print_string_len ; 打印消息
mov rax, [var1_val] ; 加载 var1 的值到 RAX
call print_hex_num ; 打印 RAX
mov rsi, msg_var2
call print_string_len
mov rax, [var2_val] ; 加载 var2 的值到 RAX
call print_hex_num
call print_newline

; --- 交换 var1_val 和 var2_val ---
; 需要一个临时寄存器来辅助交换,因为不能直接交换两个内存地址的内容
mov     rax, [var1_val]  ; RAX = var1_val
mov     rbx, [var2_val]  ; RBX = var2_val

mov     [var1_val], rbx  ; var1_val = RBX (原 var2_val)
mov     [var2_val], rax  ; var2_val = RAX (原 var1_val)

; 打印交换后的值
mov     rsi, msg_after
call    print_string_len
mov     rax, [var1_val]
call    print_hex_num
mov     rsi, msg_var2
call    print_string_len
mov     rax, [var2_val]
call    print_hex_num
call    print_newline

; 退出程序
mov     rax, 60
mov     rdi, 0
syscall

; — 辅助函数:打印字符串,长度由循环计算 —
print_string_len:
push rax
push rdi
push rcx
push rdx
mov rdi, rsi ; rdi 指向字符串起始
mov rcx, 0 ; rcx 作为长度计数器
.count_loop:
cmp byte [rdi], 0 ; 检查是否遇到空字符
je .count_done
inc rdi
inc rcx
jmp .count_loop
.count_done:
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
; rsi 已经是字符串起始地址
mov rdx, rcx ; 字符串长度
syscall
pop rdx
pop rcx
pop rdi
pop rax
ret

; — 辅助函数:将RAX中的数字转换为HEX ASCII字符串并打印 —
; 类似于 print_num,但转换为十六进制
print_hex_num:
push rax
push rcx
push rdx
push rdi
push rsi ; 额外保存rsi,因为print_string_len会用到

mov     rdi, num_buffer + 31 ; 指向缓冲区的末尾
mov     byte [rdi], 0   ; 写入字符串结束符
dec     rdi             ; 指向倒数第二个位置
mov     rcx, 0          ; 计数器,记录转换的位数

cmp     rax, 0
je      .print_hex_zero ; 如果数字为0,直接打印'0'

.loop_hex_convert:
mov rdx, 0 ; 清空 RDX,为除法做准备
mov rbx, 16 ; 除数为16 (十六进制)
div rbx ; RAX /= 16, RDX = RAX % 16 (余数)

cmp     dl, 9
jg      .is_hex_char    ; 如果余数 > 9,是 A-F
add     dl, '0'         ; 0-9 转换为 ASCII '0'-'9'
jmp     .store_hex_char

.is_hex_char:
add dl, ‘A’ – 10 ; 10-15 转换为 ASCII ‘A’-‘F’
.store_hex_char:
mov byte [rdi], dl ; 存储到缓冲区
dec rdi ; 向前移动一个字节
inc rcx ; 增加位数计数

cmp     rax, 0          ; 如果商为0,则转换完成
jne     .loop_hex_convert

.print_hex_string:
inc rdi ; rdi现在指向数字字符串的开头
mov rax, 1 ; sys_write
mov rsi, rdi ; 字符串地址
mov rdx, rcx ; 字符串长度
mov rdi, 1 ; stdout
syscall

pop     rsi
pop     rdi
pop     rdx
pop     rcx
pop     rax
ret

.print_hex_zero:
mov byte [rdi], ‘0’
mov rcx, 1
jmp .print_hex_string

; — 辅助函数:打印换行符 —
print_newline:
push rax
push rdi
push rsi
push rdx
mov rax, 1
mov rdi, 1
mov rsi, newline
mov rdx, 1
syscall
pop rdx
pop rsi
pop rdi
pop rax
ret
“`

编译、链接与执行:

bash
nasm -f elf64 swap.asm -o swap.o
ld swap.o -o swap
./swap

预期输出:
Before swap: var1 = 1234567890ABCD, var2 = EFCDA9876543210
After swap: var1 = EFCDA9876543210, var2 = 1234567890ABCD

代码解释:
* var1_val dq ...dq 表示 “define quadword” (8字节/64位)。
* mov rax, [var1_val][var1_val] 表示从 var1_val 这个内存地址加载数据。这与 mov rax, var1_val 不同,后者是将 var1_val地址 放入 RAX
* 交换操作:直接交换两个内存位置的内容是不允许的,因为汇编指令通常只支持一个内存操作数。所以我们通过 RAXRBX 两个寄存器作为临时存储空间来完成交换。
* print_string_lenprint_hex_num:这两个是新增的辅助函数。print_string_len 遍历字符串直到找到空字符 (0) 来计算长度,然后打印。print_hex_num 类似于 print_num,但除数改为16,并且要处理十六进制数字 A-F 的转换。

示例 D: 条件判断 (IF/ELSE)

使用 CMP (比较) 和条件跳转指令 (JE, JNE, JG, JL 等) 来实现条件逻辑。

if_else.asm

“`assembly
; if_else.asm – 演示条件判断 (IF/ELSE)

section .data
val1 dq 10
val2 dq 20

msg_val1_gt_val2 db "val1 is greater than val2", 0x0A, 0
msg_val1_eq_val2 db "val1 is equal to val2", 0x0A, 0
msg_val1_lt_val2 db "val1 is less than val2", 0x0A, 0

section .text
global _start

_start:
mov rax, [val1] ; 加载 val1 到 RAX
mov rbx, [val2] ; 加载 val2 到 RBX

cmp     rax, rbx        ; 比较 RAX 和 RBX

jg      _val1_greater   ; 如果 RAX > RBX,跳转到 _val1_greater
je      _val1_equal     ; 如果 RAX == RBX,跳转到 _val1_equal

; 如果以上两个条件都不满足,则说明 RAX < RBX

_val1_less:
mov rsi, msg_val1_lt_val2
call print_string_len_syscall ; 打印 “val1 is less than val2”
jmp _exit_program ; 跳转到程序退出

_val1_greater:
mov rsi, msg_val1_gt_val2
call print_string_len_syscall ; 打印 “val1 is greater than val2”
jmp _exit_program

_val1_equal:
mov rsi, msg_val1_eq_val2
call print_string_len_syscall ; 打印 “val1 is equal to val2”
; 这里不再需要 jmp _exit_program,因为后面就是退出代码

_exit_program:
mov rax, 60 ; sys_exit
mov rdi, 0 ; exit code 0
syscall

; — 辅助函数:打印由空字符结尾的字符串 (封装了 syscall) —
; rsi 必须指向字符串地址
print_string_len_syscall:
push rax
push rdi
push rcx
push rdx
mov rdi, rsi ; rdi 指向字符串起始
mov rcx, 0 ; rcx 作为长度计数器
.count_loop:
cmp byte [rdi], 0 ; 检查是否遇到空字符
je .count_done
inc rdi
inc rcx
jmp .count_loop
.count_done:
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
; rsi 已经是字符串起始地址
mov rdx, rcx ; 字符串长度
syscall
pop rdx
pop rcx
pop rdi
pop rax
ret
“`

编译、链接与执行:

bash
nasm -f elf64 if_else.asm -o if_else.o
ld if_else.o -o if_else
./if_else

预期输出 (因为 val1=10, val2=20):
val1 is less than val2

代码解释:
* cmp rax, rbx:比较 RAXRBX。这个指令会根据比较结果设置 RFLAGS 寄存器中的标志位。
* jg _val1_greaterJump Greater。如果 RAX > RBX(即 cmp 结果显示 ZF=0SF=OF),则跳转。
* je _val1_equalJump Equal。如果 RAX == RBX(即 cmp 结果显示 ZF=1),则跳转。
* jmp _exit_program:无条件跳转到程序退出点,避免执行其他分支的代码。
* 通过标签 (_val1_greater, _val1_equal, _val1_less, _exit_program) 和跳转指令,我们构建了类似高级语言的 if-else if-else 结构。

示例 E: 循环结构 (FOR/WHILE)

汇编语言中的循环通常通过计数器、比较和条件跳转来实现。我们将演示一个计算 1 到 N 的和的循环。

loop_sum.asm

“`assembly
; loop_sum.asm – 演示循环结构 (计算 1 到 N 的和)

section .data
n_val dq 10 ; N 的值 (计算 1+2+…+10)
msg_sum_is db “Sum from 1 to N is: “, 0
newline db 0x0A, 0

section .bss
num_buffer resb 32

section .text
global _start

_start:
mov rax, 0 ; RAX 存储累加和 (初始为0)
mov rbx, 1 ; RBX 作为计数器 (从1开始)
mov rcx, [n_val] ; RCX 存储 N 的值,作为循环上限

.loop_start:
cmp rbx, rcx ; 比较计数器和上限
jg .loop_end ; 如果计数器 > 上限,跳出循环

add     rax, rbx        ; 将计数器值加到累加和
inc     rbx             ; 计数器加1

jmp     .loop_start     ; 无条件跳转回循环开始

.loop_end:
; 打印结果
mov rsi, msg_sum_is
call print_string_len_syscall ; 打印消息
mov rdi, rax ; 将和放入 RDI (print_num_from_rdi 需要)
call print_num_from_rdi ; 打印和
call print_newline

; 退出程序
mov     rax, 60
mov     rdi, 0
syscall

; — 辅助函数:打印由空字符结尾的字符串 (封装了 syscall) —
; rsi 必须指向字符串地址
print_string_len_syscall:
push rax
push rdi
push rcx
push rdx
mov rdi, rsi
mov rcx, 0
.count_loop:
cmp byte [rdi], 0
je .count_done
inc rdi
inc rcx
jmp .count_loop
.count_done:
mov rax, 1
mov rdi, 1
mov rdx, rcx
syscall
pop rdx
pop rcx
pop rdi
pop rax
ret

; — 辅助函数:将 RDI 中的数字转换为ASCII字符串并打印 —
; 与 print_num 类似,但输入在 RDI
print_num_from_rdi:
push rax
push rcx
push rdx
push rdi
push rsi ; print_string_len_syscall 会用到

mov     rsi, rdi        ; 将待打印数字移动到 RSI (原始 rdi 值)
mov     rdi, num_buffer + 31 ; 指向缓冲区的末尾
mov     byte [rdi], 0   ; 写入字符串结束符
dec     rdi             ; 指向倒数第二个位置
mov     rcx, 0          ; 计数器,记录转换的位数

cmp     rsi, 0          ; 比较原始 rdi 值 (待转换数字)
je      .print_zero_rdi ; 如果数字为0,直接打印'0'

.loop_convert_rdi:
mov rax, rsi ; 将待转换数字移到 RAX
mov rdx, 0 ; 清空 RDX
mov rbx, 10 ; 除数为10
div rbx ; RAX /= 10, RDX = RAX % 10

add     dl, '0'         ; 将余数转换为ASCII字符
mov     byte [rdi], dl  ; 存储到缓冲区
dec     rdi             ; 向前移动一个字节
inc     rcx             ; 增加位数计数

mov     rsi, rax        ; 更新待转换数字为商
cmp     rsi, 0          ; 如果商为0,则转换完成
jne     .loop_convert_rdi

.print_num_string_rdi:
inc rdi ; rdi现在指向数字字符串的开头
mov rax, 1 ; sys_write
mov rsi, rdi ; 字符串地址
mov rdx, rcx ; 字符串长度
mov rdi, 1 ; stdout
syscall

pop     rsi
pop     rdi
pop     rdx
pop     rcx
pop     rax
ret

.print_zero_rdi:
mov byte [rdi], ‘0’
mov rcx, 1
jmp .print_num_string_rdi

; — 辅助函数:打印换行符 —
print_newline:
push rax
push rdi
push rsi
push rdx
mov rax, 1
mov rdi, 1
mov rsi, newline
mov rdx, 1
syscall
pop rdx
pop rsi
pop rdi
pop rax
ret
“`

编译、链接与执行:

bash
nasm -f elf64 loop_sum.asm -o loop_sum.o
ld loop_sum.o -o loop_sum
./loop_sum

预期输出 (1到10的和是55):
Sum from 1 to N is: 55

代码解释:
* 我们用 RAX 存储累加和,RBX 作为循环计数器,RCX 存储循环的上限 N
* .loop_start 标签标记了循环的开始。
* cmp rbx, rcx:比较计数器和上限。
* jg .loop_end:如果计数器大于上限,跳出循环。
* add rax, rbxinc rbx:执行循环体内的操作(累加和计数器递增)。
* jmp .loop_start:无条件跳回循环开始,实现循环。
* print_num_from_rdi 辅助函数:为了使主程序代码更清晰,我们将数字打印功能封装成一个接受 RDI 中数字的函数。

示例 F: 过程/函数 (Procedures/Functions)

函数是组织代码的重要方式。在汇编中,函数被称为“过程”。它们通过 CALLRET 指令工作,并利用栈来传递参数和保存返回地址。

function_call.asm

“`assembly
; function_call.asm – 演示过程/函数调用

section .data
msg_add_result db “Addition result: “, 0
newline db 0x0A, 0

section .bss
num_buffer resb 32

section .text
global _start

_start:
; 调用 add_numbers 函数
; 参数传递 (x86-64 Linux System V ABI):
; arg1 in RDI, arg2 in RSI, arg3 in RDX, arg4 in RCX, arg5 in R8, arg6 in R9
; 返回值在 RAX

mov     rdi, 10         ; 第一个参数 num1 = 10
mov     rsi, 20         ; 第二个参数 num2 = 20
call    add_numbers     ; 调用函数

; 函数返回后,RAX 中是加法结果 (30)

; 打印结果
mov     rsi, msg_add_result
call    print_string_len_syscall
mov     rdi, rax        ; 将结果 (RAX) 放入 RDI 以便 print_num_from_rdi 使用
call    print_num_from_rdi
call    print_newline

; 退出程序
mov     rax, 60
mov     rdi, 0
syscall

; — 函数:add_numbers (num1, num2) —
; 输入: RDI = num1, RSI = num2
; 输出: RAX = num1 + num2
add_numbers:
; 通常函数需要保存一些被调用者需要保留的寄存器(callee-saved registers),
; 但对于这个简单函数,我们只修改了RAX,所以不需要保存RDI, RSI等
; 如果函数内部修改了 RBX, RBP, RSP, R12-R15,则必须保存它们。
; 这里我们只使用 RAX,并且它是caller-saved register,所以不需额外保存

mov     rax, rdi        ; 将 num1 (RDI) 移到 RAX
add     rax, rsi        ; 将 num2 (RSI) 加到 RAX
ret                     ; 返回到调用者 (从栈中弹出返回地址并跳转)

; — 以下是前面示例中用到的辅助函数,为保持完整性再次包含 —
print_string_len_syscall:
push rax ; 保存可能被使用的寄存器
push rdi
push rcx
push rdx
mov rdi, rsi
mov rcx, 0
.count_loop:
cmp byte [rdi], 0
je .count_done
inc rdi
inc rcx
jmp .count_loop
.count_done:
mov rax, 1
mov rdi, 1
mov rdx, rcx
syscall
pop rdx ; 恢复寄存器
pop rcx
pop rdi
pop rax
ret

print_num_from_rdi:
push rax
push rcx
push rdx
push rdi
push rsi

mov     rsi, rdi
mov     rdi, num_buffer + 31
mov     byte [rdi], 0
dec     rdi
mov     rcx, 0

cmp     rsi, 0
je      .print_zero_rdi

.loop_convert_rdi:
mov rax, rsi
mov rdx, 0
mov rbx, 10
div rbx

add     dl, '0'
mov     byte [rdi], dl
dec     rdi
inc     rcx

mov     rsi, rax
cmp     rsi, 0
jne     .loop_convert_rdi

.print_num_string_rdi:
inc rdi
mov rax, 1
mov rsi, rdi
mov rdx, rcx
mov rdi, 1
syscall

pop     rsi
pop     rdi
pop    rdx
pop     rcx
pop     rax
ret

.print_zero_rdi:
mov byte [rdi], ‘0’
mov rcx, 1
jmp .print_num_string_rdi

print_newline:
push rax
push rdi
push rsi
push rdx
mov rax, 1
mov rdi, 1
mov rsi, newline
mov rdx, 1
syscall
pop rdx
pop rsi
pop rdi
pop rax
ret
“`

编译、链接与执行:

bash
nasm -f elf64 function_call.asm -o function_call.o
ld function_call.o -o function_call
./function_call

预期输出:
Addition result: 30

代码解释:
* add_numbers 函数
* 根据 Linux x86-64 System V ABI 约定,前六个整数或指针参数分别通过 RDI, RSI, RDX, RCX, R8, R9 寄存器传递。函数返回值通过 RAX 传递。
* mov rdi, 10mov rsi, 20:在 _start 中设置参数。
* call add_numbers:执行函数调用。CALL 指令会将当前 RIP(下一条指令的地址,即返回地址)压入栈中,然后无条件跳转到 add_numbers 标签。
* ret:执行函数返回。RET 指令会从栈中弹出顶部值(即之前 CALL 压入的返回地址),然后无条件跳转到该地址。
* 寄存器保存约定:高级语言编译器遵循一套严格的寄存器保存约定。
* 调用者保存 (Caller-saved):RAX, RCX, RDX, RDI, RSI, R8-R11。调用者负责在 CALL 之前保存这些寄存器的值,如果它需要在 CALL 之后继续使用它们。被调用函数可以随意修改这些寄存器。
* 被调用者保存 (Callee-saved):RBX, RBP, RSP, R12-R15。被调用函数必须在修改这些寄存器之前将其值压栈保存,并在返回之前恢复它们。
* 在 add_numbers 中,我们只使用了 RAXRDIRSIRAX 是返回值寄存器,RDIRSI 是参数寄存器,它们都是调用者保存的,所以 add_numbers 函数本身不需要 pushpop 它们。

示例 G: 数组操作

数组在内存中是连续存放的。我们可以通过基址寄存器和索引寄存器来访问数组元素。

array_sum.asm

“`assembly
; array_sum.asm – 演示数组操作 (计算数组元素的和)

section .data
my_array dq 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ; 一个 QWORD (8字节) 数组
array_len equ ($ – my_array) / 8 ; 数组长度 (元素个数)

msg_sum_array db "Sum of array elements: ", 0
newline db 0x0A, 0

section .bss
num_buffer resb 32

section .text
global _start

_start:
mov rax, 0 ; RAX 存储累加和 (初始为0)
mov rdi, my_array ; RDI 存储数组的起始地址 (基址)
mov rcx, 0 ; RCX 作为索引计数器 (初始为0)
mov rbx, array_len ; RBX 存储数组的长度 (元素个数)

.loop_array_sum:
cmp rcx, rbx ; 比较索引和数组长度
je .loop_array_end ; 如果索引 == 长度,跳出循环

; 访问数组元素:[基址 + 索引 * 元素大小]
; 对于 QWORD 数组,元素大小是 8 字节
add     rax, [rdi + rcx * 8] ; 将当前元素的值加到 RAX

inc     rcx             ; 索引加1

jmp     .loop_array_sum ; 无条件跳转回循环开始

.loop_array_end:
; 打印结果
mov rsi, msg_sum_array
call print_string_len_syscall
mov rdi, rax ; 将和放入 RDI
call print_num_from_rdi
call print_newline

; 退出程序
mov     rax, 60
mov     rdi, 0
syscall

; — 以下是前面示例中用到的辅助函数,为保持完整性再次包含 —
print_string_len_syscall:
push rax
push rdi
push rcx
push rdx
mov rdi, rsi
mov rcx, 0
.count_loop:
cmp byte [rdi], 0
je .count_done
inc rdi
inc rcx
jmp .count_loop
.count_done:
mov rax, 1
mov rdi, 1
mov rdx, rcx
syscall
pop rdx
pop rcx
pop rdi
pop rax
ret

print_num_from_rdi:
push rax
push rcx
push rdx
push rdi
push rsi

mov     rsi, rdi
mov     rdi, num_buffer + 31
mov     byte [rdi], 0
dec     rdi
mov     rcx, 0

cmp     rsi, 0
je      .print_zero_rdi

.loop_convert_rdi:
mov rax, rsi
mov rdx, 0
mov rbx, 10
div rbx

add     dl, '0'
mov     byte [rdi], dl
dec     rdi
inc     rcx

mov     rsi, rax
cmp     rsi, 0
jne     .loop_convert_rdi

.print_num_string_rdi:
inc rdi
mov rax, 1
mov rsi, rdi
mov rdx, rcx
mov rdi, 1
syscall

pop     rsi
pop     rdi
pop     rdx
pop     rcx
pop     rax
ret

.print_zero_rdi:
mov byte [rdi], ‘0’
mov rcx, 1
jmp .print_num_string_rdi

print_newline:
push rax
push rdi
push rsi
push rdx
mov rax, 1
mov rdi, 1
mov rsi, newline
mov rdx, 1
syscall
pop rdx
pop rsi
pop rdi
pop rax
ret
“`

编译、链接与执行:

bash
nasm -f elf64 array_sum.asm -o array_sum.o
ld array_sum.o -o array_sum
./array_sum

预期输出 (1到10的和是55):
Sum of array elements: 55

代码解释:
* my_array dq 1, 2, ...:定义了一个包含10个QWORD(8字节)整数的数组。
* array_len equ ($ - my_array) / 8:计算数组的元素个数。($ - my_array) 得到数组的总字节数,除以每个元素的大小(8字节)得到元素个数。
* mov rdi, my_array:将数组的起始地址加载到 RDI(作为基址)。
* mov rcx, 0RCX 用作数组索引,从0开始。
* add rax, [rdi + rcx * 8]:这是访问数组元素的关键。
* rdi 是数组基址。
* rcx 是当前索引。
* 8 是比例因子,表示每个元素占8字节。
* [base + index * scale] 是 x86 处理器强大寻址模式的体现,它可以直接计算出要访问的内存地址。

示例 H: 位操作 (Bitwise Operations)

位操作是直接对二进制位进行操作,在低层编程、嵌入式和优化中非常有用。

bitwise.asm

“`assembly
; bitwise.asm – 演示位操作

section .data
initial_val dq 0xABCD1234EF0055AA ; 初始值
mask_val dq 0x0000FFFF0000FFFF ; 掩码值

msg_initial db "Initial value: ", 0
msg_and     db "AND result:    ", 0
msg_or      db "OR result:     ", 0
msg_xor     db "XOR result:    ", 0
msg_not     db "NOT result:    ", 0
msg_shl     db "SHL result:    ", 0
msg_shr     db "SHR result:    ", 0
msg_sar     db "SAR result:    ", 0
newline     db 0x0A, 0

section .bss
num_buffer resb 32

section .text
global _start

_start:
mov rax, [initial_val]
mov rsi, msg_initial
call print_string_len_syscall
call print_hex_num_from_rax ; 打印 RAX (需要新的辅助函数)
call print_newline

; --- AND 操作 ---
mov     rax, [initial_val]
and     rax, [mask_val]
mov     rsi, msg_and
call    print_string_len_syscall
call    print_hex_num_from_rax
call    print_newline

; --- OR 操作 ---
mov     rax, [initial_val]
or      rax, [mask_val]
mov     rsi, msg_or
call    print_string_len_syscall
call    print_hex_num_from_rax
call    print_newline

; --- XOR 操作 ---
mov     rax, [initial_val]
xor     rax, [mask_val]
mov     rsi, msg_xor
call    print_string_len_syscall
call    print_hex_num_from_rax
call    print_newline

; --- NOT 操作 ---
mov     rax, [initial_val]
not     rax             ; 位翻转 (0变1, 1变0)
mov     rsi, msg_not
call    print_string_len_syscall
call    print_hex_num_from_rax
call    print_newline

; --- SHL (逻辑左移) ---
mov     rax, 0x10       ; 初始值
shl     rax, 4          ; 左移4位 (0x10 << 4 = 0x100)
mov     rsi, msg_shl
call    print_string_len_syscall
call    print_hex_num_from_rax
call    print_newline

; --- SHR (逻辑右移) ---
mov     rax, 0x100      ; 初始值
shr     rax, 4          ; 右移4位 (0x100 >> 4 = 0x10)
mov     rsi, msg_shr
call    print_string_len_syscall
call    print_hex_num_from_rax
call    print_newline

; --- SAR (算术右移) ---
; 算术右移保留符号位
mov     rax, 0xFFFFFFFF00000000 ; 负数示例 (高位为1)
sar     rax, 4          ; 右移4位,高位补1,结果仍为负数
mov     rsi, msg_sar
call    print_string_len_syscall
call    print_hex_num_from_rax
call    print_newline

; 退出程序
mov     rax, 60
mov     rdi, 0
syscall

; — 辅助函数:将RAX中的数字转换为HEX ASCII字符串并打印 —
; 与 print_hex_num 类似,但输入在 RAX
print_hex_num_from_rax:
push rax
push rcx
push rdx
push rdi
push rsi

mov     rdi, num_buffer + 31 ; 指向缓冲区的末尾
mov     byte [rdi], 0   ; 写入字符串结束符
dec     rdi             ; 指向倒数第二个位置
mov     rcx, 0          ; 计数器,记录转换的位数

cmp     rax, 0
je      .print_hex_zero_rax ; 如果数字为0,直接打印'0'

mov     rsi, rax        ; 将待转换数字移动到 RSI

.loop_hex_convert_rax:
mov rax, rsi ; 复制到RAX进行除法
mov rdx, 0 ; 清空 RDX
mov rbx, 16 ; 除数为16
div rbx ; RAX /= 16, RDX = RAX % 16

cmp     dl, 9
jg      .is_hex_char_rax
add     dl, '0'
jmp     .store_hex_char_rax

.is_hex_char_rax:
add dl, ‘A’ – 10
.store_hex_char_rax:
mov byte [rdi], dl
dec rdi
inc rcx

mov     rsi, rax        ; 更新待转换数字为商
cmp     rsi, 0
jne     .loop_hex_convert_rax

.print_hex_string_rax:
inc rdi
mov rax, 1
mov rsi, rdi
mov rdx, rcx
mov rdi, 1
syscall

pop     rsi
pop     rdi
pop     rdx
pop     rcx
pop     rax
ret

.print_hex_zero_rax:
mov byte [rdi], ‘0’
mov rcx, 1
jmp .print_hex_string_rax

; — 其他辅助函数 (同上) —
print_string_len_syscall:
push rax
push rdi
push rcx
push rdx
mov rdi, rsi
mov rcx, 0
.count_loop:
cmp byte [rdi], 0
je .count_done
inc rdi
inc rcx
jmp .count_loop
.count_done:
mov rax, 1
mov rdi, 1
mov rdx, rcx
syscall
pop rdx
pop rcx
pop rdi
pop rax
ret

print_newline:
push rax
push rdi
push rsi
push rdx
mov rax, 1
mov rdi, 1
mov rsi, newline
mov rdx, 1
syscall
pop rdx
pop rsi
pop rdi
pop rax
ret
“`

编译、链接与执行:

bash
nasm -f elf64 bitwise.asm -o bitwise.o
ld bitwise.o -o bitwise
./bitwise

预期输出:
Initial value: ABCD1234EF0055AA
AND result: 00001234000055AA
OR result: ABCDFFFFEF00FFFF
XOR result: ABCDEDCBFF00AA55
NOT result: 4532EDCB10FFAC55
SHL result: 100
SHR result: 10
SAR result: FFFFFFFFF0000000

代码解释:
* and, or, xor, not:这些指令直接执行按位逻辑操作。
* shl rax, 4:逻辑左移 RAX 中的值 4 位。最右边的 4 位补零,最左边的 4 位移出并丢失。
* shr rax, 4:逻辑右移 RAX 中的值 4 位。最左边的 4 位补零,最右边的 4 位移出并丢失。
* sar rax, 4:算术右移 RAX 中的值 4 位。最左边的 4 位补符号位(如果原符号位是1,则补1;如果原符号位是0,则补0),最右边的 4 位移出并丢失。这对于带符号整数的除法很有用。
* print_hex_num_from_rax 辅助函数:为了更方便地打印寄存器中的十六进制值,我们又封装了一个辅助函数。

第四章:学习汇编语言的挑战与建议

学习汇编语言确实存在一些挑战,但通过正确的方法可以克服:

挑战:
1. 抽象度高: 需要直接操作寄存器和内存,远离高级语言的抽象。
2. 平台依赖性强: 不同的CPU架构(x86, ARM, RISC-V)有不同的指令集和寄存器。
3. 调试困难: 错误可能导致程序崩溃,且难以通过错误信息定位。
4. 代码量大: 实现相同功能,汇编代码通常比高级语言长得多。
5. 缺乏库支持: 大部分功能需要手动实现或通过系统调用完成。

建议:
1. 从小处着手,循序渐进: 从本文的示例开始,逐步增加复杂性。
2. 熟练掌握工具: 熟悉汇编器、链接器和调试器(尤其是GDB)。GDB是你的好朋友,学会查看寄存器、内存和单步执行。
3. 多实践,多动手: 理论知识再多,不如亲手写代码并运行。
4. 理解硬件原理: 寄存器、内存、栈、CPU指令周期等概念是基础。
5. 阅读优秀代码: 分析操作系统内核、引导加载程序或编译器生成的汇编代码。
6. 耐心和毅力: 汇编语言的学习曲线较陡峭,但坚持下去的回报丰厚。
7. 利用在线资源: 查阅指令集参考手册(Intel/AMD),阅读教程,参与社区讨论。

结论:汇编语言的持久魅力

虽然在日常开发中直接编写汇编代码的场景越来越少,但理解和掌握汇编语言的价值却从未减退。它赋予你对计算机系统最深层次的洞察力,让你能够理解高级语言代码编译后的真实面貌,以及操作系统如何与硬件交互。

从简单的“Hello, World!”到复杂的位操作,每一个汇编指令都是CPU执行任务的最小单位。通过本文的实战示例,希望你已经迈出了学习汇编语言的第一步,并感受到了它独特的魅力。继续探索,你将发现一个全新的、充满挑战与乐趣的计算机世界。祝你在汇编语言的学习之旅中一切顺利!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部