现在位置: 首页 > 汇编语言 > 正文

汇编语言 - 宏

宏(Macro)是 NASM 提供的一种代码复用机制,它允许你在编译时展开代码模板,减少重复编写相似代码的需求。


什么是宏

宏(Macro) 是一种编译时文本替换机制,由汇编器的预处理器在编译前展开。

与过程(Procedure)不同,宏不会有 CALL/RET 开销——每次使用宏时,编译器直接将宏的内容复制到使用位置。

宏不是函数调用,没有栈帧开销;但可执行文件体积会更大,因为每次展开都会复制一份代码。


单行宏:%define

%define 是单行宏,语法简单:

实例

; 单行宏示例

; 定义常量
%define MAX_SIZE 256
%define APP_NAME 'runoob'

; 定义带参数的宏(宏函数)
%define mul_by_2(x) (x * 2)
%define sum3(a, b, c) ((a) + (b) + (c))

section .text
    global _start

_start:
    mov eax, MAX_SIZE            ; eax = 256
    mov eax, mul_by_2(10)        ; eax = (10 * 2) = 20
    mov eax, sum3(1, 2, 3)       ; eax = ((1)+(2)+(3)) = 6

    ; 重新定义(%define 可以改变)
    %define MAX_SIZE 512
    mov eax, MAX_SIZE            ; eax = 512

    ; 取消定义
    %undef MAX_SIZE
    ; mov eax, MAX_SIZE          ; 错误:MAX_SIZE 未定义

    mov eax, 1
    mov ebx, 0
    int 0x80

%define 宏展开时是纯文本替换。例如 mul_by_2(2+3) 展开为 (2+3 * 2),由于运算符优先级,结果不是 10 而是 8。这就是宏参数一定要加括号的原因。


多行宏:%macro / %endmacro

%macro 用于定义包含多条指令的复杂宏:

实例

; 文件路径:macro_multi.asm
; 多行宏示例

; 宏定义:退出程序
; 参数 argc:宏接受多少个参数
%macro exit_program 1
    mov eax, 1                   ; sys_exit
    mov ebx, %1                  ; 第 1 个参数作为退出码
    int 0x80
%endmacro

; 宏定义:打印字符串
; 接受 2 个参数:字符串地址、长度
%macro print_string 2
    push eax
    push ebx
    push ecx
    push edx
    mov eax, 4                   ; sys_write
    mov ebx, 1                   ; stdout
    mov ecx, %1                  ; 字符串地址
    mov edx, %2                  ; 字符串长度
    int 0x80
    pop edx
    pop ecx
    pop ebx
    pop eax
%endmacro

section .data
    msg db 'Hello, RUNOOB!', 0xA
    msg_len equ $ - msg

section .text
    global _start

_start:
    print_string msg, msg_len     ; 使用宏打印消息
    print_string msg, msg_len     ; 再次调用
    exit_program 0                ; 退出程序

宏参数的高级用法

NASM 宏支持默认参数、参数计数和条件展开:

实例

; 高级宏参数示例

; 带默认参数的宏(参数范围 2-3)
%macro debug_print 2-3 1        ; 至少 2 个参数,最多 3 个,第 3 个默认 = 1
    %if %3 = 1                   ; 如果第 3 个参数 = 1(debug 模式开启)
        push eax
        push ebx
        push ecx
        push edx
        mov eax, 4
        mov ebx, 1
        mov ecx, %1
        mov edx, %2
        int 0x80
        pop edx
        pop ecx
        pop ebx
        pop eax
    %endif
%endmacro

; 不定数量参数的宏
%macro push_registers 1-*        ; 1 到任意多个参数
    %rep %0                      ; %0 是参数个数
        push %1                  ; 展开第1个
        %rotate 1                ; 向左旋转参数列表
    %endrep
%endmacro

section .text
    global _start

_start:
    ; 使用 push_registers 保存多个寄存器
    push_registers eax, ebx, ecx, edx
    ; 展开为:
    ; push eax
    ; push ebx
    ; push ecx
    ; push edx

    ; 对应地弹出
    pop edx
    pop ecx
    pop ebx
    pop eax

    mov eax, 1
    mov ebx, 0
    int 0x80

宏中的局部标签

宏中使用 %%label 定义局部标签,避免多次展开时标签冲突:

实例

; 宏中的局部标签

; 比较两个值并设置最小值
%macro min_val 2
    mov eax, %1
    mov ebx, %2
    cmp eax, ebx
    jle %%skip                   ; ★ 局部标签,每次展开会生成唯一名
    mov eax, ebx
%%skip:
%endmacro

; 如果不使用局部标签,展开两次后会出现重复的 skip 标签
section .text
    global _start

_start:
    min_val 10, 5               ; eax = 5
    min_val eax, 3              ; eax = 3

    mov eax, 1
    mov ebx, 0
    int 0x80

普通标签在宏中展开两次会导致标签重名错误。局部标签 %%label 让 NASM 每次展开都生成唯一的标签名(如 ..@0001.skip),避免冲突。


宏与过程的对比

特性宏(Macro)过程(Procedure)
实现方式编译时文本展开运行时 CALL/RET
执行开销无调用开销(直接内联)有 CALL/RET 开销(约几个时钟周期)
代码大小每次展开增加体积一份代码多次调用
参数类型任意文本(寄存器、立即数、内存)运行时值
调试难(展开后无痕迹)易(有函数调用栈)
适用场景短小频繁调用、类型灵活的代码复杂逻辑、代码量大的函数

条件编译:%if / %elif / %else / %endif

宏预处理器支持条件编译,可以根据符号定义选择生成不同的代码:

实例

; 文件路径:cond_compile.asm
; 条件编译示例:调试和发布模式

%define DEBUG 1                  ; 1=调试模式, 0=发布模式

section .data
    msg db 'Program running...', 0xA
    msg_len equ $ - msg
    debug_msg db '[DEBUG] Entering function', 0xA
    debug_len equ $ - debug_msg

section .text
    global _start

_start:
    %if DEBUG = 1
        ; 调试模式:输出调试信息
        mov eax, 4
        mov ebx, 1
        mov ecx, debug_msg
        mov edx, debug_len
        int 0x80
    %endif

    ; 正常业务逻辑
    mov eax, 4
    mov ebx, 1
    mov ecx, msg
    mov edx, msg_len
    int 0x80

    mov eax, 1
    mov ebx, 0
    int 0x80

条件编译常用场景:

场景示例
调试/发布切换%if DEBUG / %else / %endif
平台适配%ifdef LINUX / %ifdef WINDOWS
功能开关通过符号存在与否控制特性

%rep 重复块

%rep 用于生成重复的代码或数据:

实例

; %rep 重复块示例

section .data
    ; 生成 256 字节的查找表
    ; 0, 1, 4, 9, 16, 25, ...(平方数表)
    %assign i 0
    ; 这里不能用 %rep 生成平方表来进行复杂计算
    ; 简单示例:生成 0-9 的 ASCII 表
    digits: db '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'

section .text
    global _start

_start:
    ; %rep 在代码中重复指令
    mov eax, 0
    %rep 5                      ; 重复 5 次
        inc eax                  ; eax 每次加 1
    %endrep
    ; eax = 5

    mov ebx, eax
    mov eax, 1
    int 0x80

过度使用宏会让代码难以阅读和调试。当宏体超过 10 行时,考虑改为过程调用。在性能敏感的热路径中,短小宏的内联效果才有明显好处。