Skip to content

04_变量

一、 变量的本质与内存区段划分

汇编程序通常通过 区段(Sections / Segments) 来组织变量。不同的区段决定了变量的物理存储位置和读写权限。

区段名称 伪指令声明 (MASM) 伪指令声明 (NASM) 变量类型 特征与权限
数据段 .data .data 已初始化的全局变量/静态变量 存放程序启动时就具有明确初始值的变量。占用可执行文件的磁盘空间。权限:可读可写。
未初始化数据段 .data? .bss 未初始化的全局变量 声明时不分配实际数据,仅记录所需内存大小。程序加载时由 OS 自动分配并在内存中清零。不占可执行文件磁盘空间。权限:可读可写。
只读数据段 .const .rdata (或 .rodata) 常量 (Constants)、字符串字面量 存放不可修改的变量。权限:只读。若强行写入会触发段错误 (Access Violation)。
附加数据段 (16位) N/A N/A 字符串操作目标区 16位时代的遗留产物,常用于 ES 寄存器配合的字符串操作(如 MOVSB 的目标地址)。
堆栈段 .stack N/A 局部变量、函数传参 存放函数内部定义的临时变量,随函数调用动态分配与销毁。

二、 变量的声明与内存分配 (伪指令)

汇编语言通过“数据定义伪指令”来分配内存。伪指令前通常放置一个标号(变量名)。

1. 数据大小与伪指令对照表

伪指令 (MASM) 伪指令 (NASM) 含义 字节数 (Bytes) 对应 C/C++ 类型
DB db, resb Define Byte (字节) 1 char, bool, int8_t
DW dw, resw Define Word (字) 2 short, int16_t
DD dd, resd Define Doubleword (双字) 4 int, float, int32_t
DQ dq, resq Define Quadword (四字) 8 long long, double,指针(64位)
DT dt, rest Define Tenbyte (十字节) 10 扩展精度浮点数 (80-bit FPU)

2. 变量定义语法与示例

  • 单个变量与连续变量:

    var1 DB 10         ; 定义一个 1 字节变量 var1,初始值为 10
    var2 DD 0xFFFFFFFF ; 定义一个 4 字节变量 var2
    arr1 DW 1, 2, 3    ; 定义一个 16 位数组,包含 3 个元素,共占用 6 字节内存
    
  • 未初始化变量声明:

    • MASM 语法:使用 ? 表示未初始化。var3 DD ?

    • NASM 语法:在 .bss 段使用专门的保留指令。var3 resd 1 (保留 1 个双字)

  • 批量分配 (数组/缓冲区):

    使用 DUP (Duplicate) 操作符可以快速填充大块内存。

    buffer DB 100 DUP(0) ; 分配 100 个字节,全部初始化为 0 (类似 char buffer[100] = {0})
    matrix DD 10 DUP(?)  ; 分配 10 个双字空间,不初始化
    

三、 16位实模式:段绑定的变量操作

在 16 位架构的实模式下,内存寻址严重依赖于物理分段机制。全局变量存在于特定的数据段中,访问它们需要处理“段地址:偏移地址”的二维坐标。

1. 不同段中变量的定义 (MASM 语法)

DATA SEGMENT         ; 定义主数据段
    var_data DW 1234H
DATA ENDS

EXTRA_DATA SEGMENT   ; 定义附加数据段
    var_extra DB 55H
EXTRA_DATA ENDS

STACK SEGMENT STACK  ; 定义堆栈段
    DB 100 DUP(0)    ; 预留 100 字节的堆栈空间
STACK ENDS

2. 获取变量的地址 (段地址与偏移地址)

在 16 位编程中,获取一个变量完整的物理位置需要两个独立的操作:

  • 获取段基址 (SEG):

    SEG 运算符用于在编译期获取变量所在段的真实段基址。

    MOV AX, SEG var_data  ; 将 var_data 所在段(DATA)的基址读入 AX
    
  • 获取段内偏移 (OFFSET):

    OFFSET 运算符用于获取变量在其所在段内部的相对偏移量。

    MOV BX, OFFSET var_data ; 获取 var_data 距离 DATA 段首的字节偏移,存入 BX
    

3. 访问各个段内的变量 (段初始化与段超越)

核心铁律:x86 架构禁止将立即数直接传送到段寄存器,必须使用通用寄存器中转。

  • 访问主数据段 (DS) 的变量:

    必须先手动初始化 DS 寄存器,然后 CPU 默认会去 DS 指向的段中寻找数据。

    MOV AX, SEG var_data ; 获取数据段基址
    MOV DS, AX           ; 【必须】初始化 DS 寄存器
    
    MOV CX, [var_data]   ; 此时 CPU 会自动使用 DS:var_data 的地址去读取数据
    
  • 访问附加数据段 (ES) 的变量:

    初始化 ES 寄存器后,在指令中必须使用段超越前缀 (ES:),强制告诉 CPU 不要去 DS 找,去 ES 找。

    MOV AX, SEG var_extra
    MOV ES, AX           ; 初始化 ES 寄存器
    
    MOV DL, ES:[var_extra] ; 使用 ES: 段超越前缀访问附加段的变量
    

四、 全局变量 vs 局部变量的底层实现

1. 全局变量 (内存绝对/相对地址)

  • 存储位置:.data 或 .bss 段。

  • 寻址方式:通过汇编阶段确定的 标签(地址偏移) 直接访问。

  • 生命周期:随进程启动而创建,随进程终止而销毁。

  • 示例 (32位):MOV EAX, [GlobalVar] (直接去数据段取值)

2. 局部变量 (栈帧偏移)

  • 存储位置:线程栈 (Stack) 上的当前函数栈帧内部。

  • 寻址方式:由于函数的调用深度动态变化,局部变量没有固定地址。必须通过栈底基址寄存器 (EBP 或 RBP) 减去一个负偏移量来访问。

  • 生命周期:函数被调用(建立栈帧)时分配,函数返回(销毁栈帧)时释放(变为无效数据)。

  • 分配与访问示例:

    ; 1. 分配空间:在栈上减去 8 字节,留给局部变量
    SUB ESP, 8
    
    ; 2. 变量赋值:[EBP - 4] 是第一个局部变量,[EBP - 8] 是第二个
    MOV DWORD PTR [EBP - 4], 10    ; 相当于 int a = 10;
    MOV DWORD PTR [EBP - 8], 20    ; 相当于 int b = 20;
    
    ; 3. 变量读取
    MOV EAX, [EBP - 4]
    

五、 变量操作的核心指令与强制类型转换

1. 地址与值的获取 (LEA vs MOV)

处理变量时,最常混淆的是“取变量的值”和“取变量的地址(指针)”。

  • 取变量的值:MOV EAX, [var] (将 var 地址里的内容读入 EAX)

  • 取变量的地址:

    • 方式一:MOV EAX, OFFSET var (在编译期将 var 的偏移地址作为立即数送入 EAX)

    • 方式二:LEA EAX, [var] (Load Effective Address,运行时计算有效地址存入 EAX。现代汇编中最常用的获取指针的方法)。

2. 强制类型指示符 (PTR)

由于汇编中的变量名只是个无类型的地址,当指令无法从寄存器推断出操作的数据大小时,必须使用 PTR 显式指定操作宽度。

  • 隐式推断:MOV EAX, [var] (因为 EAX 是 32 位,CPU 自动去 var 地址处读取 4 个字节,无需 PTR)。

  • 必须指定 PTR 的场景(立即数写入内存):

    MOV [var], 0          ; 【错误】编译失败!CPU 不知道该写 1 个、2 个还是 4 个字节的 0。
    MOV BYTE PTR [var], 0 ; 【正确】只将 var 处的 1 个字节清零。
    MOV DWORD PTR [var],0 ; 【正确】将 var 处的连续 4 个字节清零。
    

六、 架构演进对变量寻址的影响 (32位 \(\rightarrow\) 64位)

1. 32 位保护模式:绝对地址寻址

进入平坦模型后,所有段的基址通常均为 0,寻址空间达到 4GB。

  • 全局变量表现:编译器会将全局变量直接翻译成一个 32 位的绝对内存地址(如 0x00403000)。
MOV EAX, [0x00403000]  ; 32 位直接寻址获取变量

2. 64 位长模式:RIP 相对寻址 (PIC 基石)

现代 x64 操作系统为了安全(ASLR,地址随机化),不再允许将全局变量硬编码为绝对地址。

  • RIP-Relative Addressing:访问全局变量时,硬件原生使用当前指令指针 RIP 加上一个 32 位的相对偏移量。

  • 语法表现:

    • NASM 语法下,常显式写为:MOV RAX, [REL var1]

    • 实际机器码翻译为:MOV RAX, [RIP + 0x1A2B] (表示目标变量在当前指令之后 0x1A2B 字节处)。

    • 优势:无论包含该变量的模块(DLL/SO)被加载到内存的哪个基址,指令内部的相对距离永远不变,完美实现了位置无关代码 (Position Independent Code)。