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. 变量定义语法与示例
-
单个变量与连续变量:
-
未初始化变量声明:
-
MASM 语法:使用
?表示未初始化。var3 DD ? -
NASM 语法:在
.bss段使用专门的保留指令。var3 resd 1(保留 1 个双字)
-
-
批量分配 (数组/缓冲区):
使用
DUP(Duplicate) 操作符可以快速填充大块内存。
三、 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运算符用于在编译期获取变量所在段的真实段基址。 -
获取段内偏移 (
OFFSET):OFFSET运算符用于获取变量在其所在段内部的相对偏移量。
3. 访问各个段内的变量 (段初始化与段超越)
核心铁律:x86 架构禁止将立即数直接传送到段寄存器,必须使用通用寄存器中转。
-
访问主数据段 (DS) 的变量:
必须先手动初始化
DS寄存器,然后 CPU 默认会去DS指向的段中寻找数据。 -
访问附加数据段 (ES) 的变量:
初始化
ES寄存器后,在指令中必须使用段超越前缀 (ES:),强制告诉 CPU 不要去DS找,去ES找。
四、 全局变量 vs 局部变量的底层实现
1. 全局变量 (内存绝对/相对地址)
-
存储位置:
.data或.bss段。 -
寻址方式:通过汇编阶段确定的 标签(地址偏移) 直接访问。
-
生命周期:随进程启动而创建,随进程终止而销毁。
-
示例 (32位):
MOV EAX, [GlobalVar](直接去数据段取值)
2. 局部变量 (栈帧偏移)
-
存储位置:线程栈 (Stack) 上的当前函数栈帧内部。
-
寻址方式:由于函数的调用深度动态变化,局部变量没有固定地址。必须通过栈底基址寄存器 (
EBP或RBP) 减去一个负偏移量来访问。 -
生命周期:函数被调用(建立栈帧)时分配,函数返回(销毁栈帧)时释放(变为无效数据)。
-
分配与访问示例:
五、 变量操作的核心指令与强制类型转换
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 的场景(立即数写入内存):
六、 架构演进对变量寻址的影响 (32位 \(\rightarrow\) 64位)
1. 32 位保护模式:绝对地址寻址
进入平坦模型后,所有段的基址通常均为 0,寻址空间达到 4GB。
- 全局变量表现:编译器会将全局变量直接翻译成一个 32 位的绝对内存地址(如
0x00403000)。
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)。
-