80x86汇编编程-2(内存管理)

官方信息技术老师·22 页·深入(追求细节与边界)·0 次浏览·2 天前
内存管理寻址机制实模式保护模式

80x86汇编内存管理

实模式与保护模式:从逻辑地址到物理地址的完整寻址链路

按 空格/→ 演示下一步

1 / 22 页

全部页面点击任意一页,跳回舞台从这页播放

内存管理寻址机制实模式保护模式

80x86汇编内存管理

实模式与保护模式:从逻辑地址到物理地址的完整寻址链路

1第 1 页 · 80x86汇编内存管理

内存地址与数据

CPU 干活时,指令和数据都躺在内存里。但 CPU 怎么精确定位某个字节?一次又能搬回多少字节?这就要靠三组总线协作。

地址总线
CPU 单向向外发送地址,n 位地址线可访问 2^n 个内存单元
数据总线
双向传输数据,位数等于单次搬运的数据位宽,又称机器字长
控制总线
传递读/写、片选等控制信号,决定本次访问的方向与时序
总线宽决定 CPU 位数
数据总线 16 位 → 16 位 CPU;32 位 → 32 位 CPU;二者一般对齐
公寓楼门牌号与快递包裹对应 →地址总线与数据总线

门牌号位数定房间总数=寻址空间;包裹大小定一次能送多少=传输位宽

220=1MB (8086);232=4GB (80386)2^{20}=1\text{MB (8086)};\quad 2^{32}=4\text{GB (80386)}
2第 2 页 · 内存地址与数据

内存寻址模式

上页我们看到 CPU 拿到的是线性地址,但汇编指令要操作数据,得先说清「去哪儿找」。这一步就是寻址——它把指令里的字段翻译成真正要访问的位置。

立即数寻址
操作数就是数据本身:MOV AX, 100,数据 100 直接写在指令里。
寄存器寻址
操作数在 CPU 寄存器中:MOV AX, BX,值就在 BX 里,无需访存。
直接寻址
指令中写死偏移:MOV AX, [2000H],有效地址 EA=2000H。
寄存器间接寻址
寄存器装的是地址:[BX] 表示 EA=BX,常用 BX/SI/DI。
基址变址寻址
[BX+SI+8]:基址寄存器 + 变址寄存器 + 位移,三者合成 EA。
快递柜取件对应 →五种寻址模式

立即数=自带货;寄存器=A号格;直接=柜号0123;间接=凭码找柜;基址变址=B区SI-08号

EA=基址+变址+位移\text{EA} = \text{基址} + \text{变址} + \text{位移}
3第 3 页 · 内存寻址模式

CPU与内存的数据流

三条总线各管一件事:地址管去向,控制管动作,数据管内容。

图解渲染中…
D数据总线双向,读写共用;宽度=一次能传几位K控制总线含读/写、内存选通等多根信号线A地址总线单向,由CPU驱动,宽度=最大寻址空间
4第 4 页 · CPU与内存的数据流

变量的声明语法

前面我们看到内存像一排编号货架,现在要把数据真正放上货架——汇编用三类伪指令声明变量,它们在编译时就决定了每个变量占几个格子。

DB 定义字节
Define Byte,每个值占 1 字节(8 位),可放字符或 0~255 整数
DW 定义字
Define Word,每个值占 2 字节(16 位),是 8086 的自然处理单位
DD 定义双字
Define Doubleword,每个值占 4 字节(32 位),可存 32 位整数或远指针
初始化与重复
多个值用逗号分隔;重复模式用 DUP,如 100 DUP(?);未初始化也用 ?
小端字节序
多字节值的低字节存低地址、高字节存高地址——x86 硬性约定,反汇编时务必看清
仓库里三种尺寸的储物格对应 →DB / DW / DD 声明的变量

小格放小件、大格放大件,格子尺寸在声明时就定死,标签名就是变量名

5第 5 页 · 变量的声明语法

声明各种类型变量

x86asm

用 MASM 语法展示 BYTE/WORD/DWORD 三种类型变量的声明与取值范围。

代码高亮加载中…

DB/DW/DD 后缀直接决定存储宽度(1/2/4 字节),h 后缀只是值的另一种书写方式,内存中的比特完全相同。

6第 6 页 · 声明各种类型变量

数据的存储方式

上页声明32位变量赋了 0x12345678 这种值,我们当一个整体处理。但内存按字节存取,这个32位数拆成四字节后,按什么顺序塞进连续地址?x86 的选择和大多数人直觉相反。

字节序问题
多字节数据拆成字节后,按什么顺序放进连续内存地址
小端序规则
低位字节进低地址,高位字节进高地址(word、dword、qword 都遵循)
存储实例
0x12345678 从地址A起依次为:A=0x78, A+1=0x56, A+2=0x34, A+3=0x12
调试时的反直觉
内存窗口看到的字节顺序,与你写的十六进制字面值方向相反
手写多位数字1234对应 →x86存0x12345678的内存布局

写数字时最左是最高位1;存字节时最左(低地址)是最低位78——同样自左向右走,权重方向相反

7第 7 页 · 数据的存储方式

小端序 vs 大端序

对比字节在内存中的排列顺序

小端序 vs 大端序
对比字节在内存中的排列顺序
8第 8 页 · 小端序 vs 大端序

直接寻址

前面我们梳理了内存寻址的多种模式,现在挑最直白的一种——直接寻址:把内存地址直接当立即数写在指令里。CPU 看到它,不用拐弯,直接去那个地址取数。

地址即立即数
指令中直接给出偏移地址,如 MOV AX,[2000H] 中的 2000H
默认走 DS 段
CPU 默认用 DS 段寄存器,可用段前缀临时改用 ES/SS/CS
编译期固定
地址在汇编/编译时确定,运行时不变,不经寄存器间接取
16位偏移上限
8086 实模式下偏移仅 16 位(0~FFFFH),386 后可扩展到 32 位
对应变量名
C 等高级语言中的全局/静态变量,编译后即映射为直接地址
快递按门牌号直送对应 →直接寻址

门牌号就是指令里的偏移量,CPU 不绕弯,直接到该地址取数据

物理地址=DS×16+偏移地址\text{物理地址} = \text{DS} \times 16 + \text{偏移地址}
9第 9 页 · 直接寻址

寄存器间接寻址

直接寻址把地址写死在指令里,间接寻址则把地址放进寄存器——相当于「门牌号随身带」,想改就改。

方括号语义
[]是「按寄存器里的地址去内存取数」,不是把寄存器值搬过来
BX 基址寄存器
默认搭配 DS,指向数据段,常作数组基地址
SI 源变址寄存器
串操作中默认指向源操作数,常配合 DS
DI 目的变址寄存器
串操作中默认指向目的操作数,常配合 ES
与直接寻址对比
直接寻址地址是常量;间接寻址地址在寄存器,可动态改变
酒店房卡对应 →寄存器间接寻址

房卡=寄存器,存着房间号;刷卡进门=[],按卡内号码找房间

物理地址=DS×16+BX\text{物理地址} = DS \times 16 + BX
10第 10 页 · 寄存器间接寻址

基址变址寻址

上页用一个寄存器装地址,就像只知道行号;再加一个寄存器就同时拿到行列号——这就是基址变址寻址:两个寄存器联手定位内存单元。

两寄存器相加
有效地址 = 基址寄存器 + 变址寄存器,二者内容相加作偏移
四种合法组合
[BX+SI]、[BX+DI]、[BP+SI]、[BP+DI],基址与变址才能配对
默认段规则
含BX默认DS段;含BP默认SS段,与单寄存器时一致
可叠加位移
还能加disp8/disp16,如[BX+SI+5]、MOV AX,[BP+DI+100H]
电影院对号入座对应 →基址变址寻址

BX是排号,SI是座号;两数相加才得到具体座位

EA=BX+SI+dispEA = \text{BX} + \text{SI} + \text{disp}
11第 11 页 · 基址变址寻址

寻址方式代码对比

nasm

4 条 mov ax 看似不同,其实都从同一个内存单元取数;区别只在地址怎么算出来。

代码高亮加载中…

4 条 mov ax 都从 arr+4 取 30h,区别仅在偏移 4 的来源:硬编码、由 BX 提供、BX+SI 相加、BP+位移在栈帧里取。运行时地址需要变化就选②或③。

12第 12 页 · 寻址方式代码对比

堆栈的基本机制

想象自助餐厅的弹簧盘架:每次把新盘子压到最上面,弹簧就会被压缩、所有旧盘子向下退一格——CPU 堆栈也是这个『向下退』的逻辑。

SS:SP 指向栈顶
SS 给段地址、SP 给偏移地址,两者组合指向当前栈顶所在的内存单元
PUSH:先减后存
执行时先把 SP 减 2,再把数据写入 SS:SP 指向的新位置
栈向低地址生长
高地址端是『栈底』,新元素不断压向更低地址
字操作为何减 2
16 位下 word 占 2 字节,SP 以字节为单位计数
弹簧盘架往下压对应 →PUSH 入栈机制

压盘子=把数据写入栈;弹簧收缩=SP 减 2;盘架最上层永远对应 SP 当前指向的栈顶

13第 13 页 · 堆栈的基本机制

堆栈的增长方向

纵轴是地址高低,虚线箭头表示 PUSH 一次后 ESP 减 4,栈顶向低地址下移。

图解渲染中…
EESP 指向当前栈顶,初始位于段的高地址端F未用区越大,剩余栈深度越多;越接近 L 越危险
14第 14 页 · 堆栈的增长方向

PUSH/POP指令对

x86asm

用PUSH把两个值压栈,再用POP按相反顺序取出,无需临时寄存器即可完成交换。

代码高亮加载中…

栈是LIFO(后进先出)结构,所以PUSH顺序与POP目标必须"错开":先压AX再压BX,弹出时栈顶的BX先出,正好填入AX,AX再出来填入BX。

15第 15 页 · PUSH/POP指令对

子程序调用与返回

前面学过PUSH/POP可以往堆栈存取数据。但你有没有想过:调用子程序时,CPU是怎么记住「等下要回到哪一行」的?答案就藏在CALL和RET这一对搭档里。

CALL指令
把「下一条指令的地址」压栈,再跳转到子程序入口
返回地址
CALL后面那条指令的地址,由CPU自动压入栈顶
RET指令
从栈顶弹出返回地址,然后跳转回去继续执行
near与far
同段调用只压IP;跨段调用要同时压CS和IP
看书夹书签对应 →CALL/RET机制

CALL=夹书签(压返回地址)+翻到目标页;RET=取回书签(弹出地址)+跳回继续读

16第 16 页 · 子程序调用与返回

调用子程序时堆栈变化

CALL时栈状态完整时序图

调用子程序时堆栈变化
CALL时栈状态完整时序图
17第 17 页 · 调用子程序时堆栈变化

段寄存器与内存分段

上一页堆栈能自动存取,是因为有专门的段寄存器在管。80x86 共有 4 个段寄存器,每个对应一片内存区域、各管一摊事。

CS 代码段
指向指令所在区域,CPU 取指令时只能从这里读
DS 数据段
程序数据的默认基址,访问变量默认相对它寻址
SS 堆栈段
堆栈所在区域,PUSH/POP 时 SP/BP 自动相对它
ES 附加段
字符串指令(MOVS/CMPS/STOS)目的操作数的默认段
工厂四个分区对应 →四个段寄存器

CS=生产线(CPU 只从这里取指令);DS=原料库;SS=中转台;ES=备用通道

物理地址=段地址×16+偏移地址\text{物理地址} = \text{段地址} \times 16 + \text{偏移地址}
18第 18 页 · 段寄存器与内存分段

逻辑地址到物理地址

从逻辑地址「段:偏移」出发,经两次变换得到20位物理地址。

图解渲染中…
a2段寄存器里的16位数值,乘以16才得到段基址a4×16 等价于十六进制左移一位(二进制左移4位)a3段内偏移量,范围 0000H~FFFFHa6最终送上CPU地址总线、用来寻址内存的20位地址
19第 19 页 · 逻辑地址到物理地址

内存复制与串操作

nasm

Linux x86-64 下用 REP 串指令完成复制、填充、逐字节读取,并验证输出。

代码高亮加载中…

CLD 令地址递增;MOVSB 从 RSI 读、写入 RDI,STOSB 将 AL 写入 RDI,LODSB 读入 AL。REP 由 ECX 决定次数,相关指针随后递增。

20第 20 页 · 内存复制与串操作

内存管理自测

点击作答

在 8086 实模式下,DS=0x1000,CS=0x2000,指令 MOV AX,[BX] 中 BX=0x0050。该指令访问的物理地址是?

21第 21 页 · 内存管理自测

课后思考

先自己想,再点开参考答案——问题没有标准答案,只有思考路径。

1为什么8086要采用段地址:偏移地址的寻址方式,而不是直接用单一地址?

参考答案8086数据线16位,纯偏移寻址只能覆盖64KB;引入段寄存器把地址空间扩到1MB。分段既是被硬件逼出来的解法,也成了后来保护模式的雏形。

2子程序里PUSH了几个寄存器,RET前却忘了POP,程序会怎么表现?

参考答案RET仍能正确弹出返回地址,程序不会立刻崩溃。但堆栈会不断"长高",反复调用后耗尽栈空间,触发不可预测的崩溃——这是汇编里的"内存泄漏"。

3假设80x86改成大端序,哪些指令会受影响,哪些完全不需要改?

参考答案MOV读写多字节数据时字节序翻转,PUSH/POP对字/双字的组装也需调整。但ADD、SUB这类纯算术指令只关心数值,与字节序无关,保持不变。

22第 22 页 · 课后思考