Basic Part
Memory Segmentation in 8086 Microprocessor
REF ….whatever..//
General-Purpose Registers (GPR | 通用寄存器 ) - 16-bit naming conventions
The 8 GPRs are as follows:
- Accumulator register (AX). Used in arithmetic operations
- Counter register (CX). Used in shift/rotate instructions and loops.
- Data register (DX). Used in arithmetic operations and I/O operations.
- Base register (BX). Used as a pointer to data (located in segment register DS, when in segmented mode).
- Stack Pointer register (SP). Pointer to the top of the stack.
- Stack Base Pointer register (BP). Used to point to the base of the stack. – The address in SS register is combined with the offset in BP to get the location of the parameter. BP can also be combined with DI and SI as base register for special addressing.
- Source Index register (SI). Used as a pointer to a source in stream operations.
- Destination Index register (DI). Used as a pointer to a destination in stream operations.
存储器 、存储单元
存储器 也就是我们平时所说的内存 。 负责为CPU提供指令和数据 , 在一台PC机中内存的作用仅次于CPU,存储器被划分为若干个存储单元, 每个存储单元从0开始顺序编号. 一般微型机的存储器的存储单元可以存储一个Byte,即一个字节(8个二进制位) - 微机存储器容量的最小单位。
一般应具有 存储数据 和 读写数据 的功能, 每个单元有一个地址,是一个整数 编码 ,可以表示为 二进制 整数。
磁盘不同于内存,磁盘上的数据或程序不读取到内存中就无法被CPU使用
CPU对存储器的读写 - 各总线概念解释
25.1.20 注: 通常情况下,CPU的字长会决定数据总线和地址总线的宽度。
存储单元在存储器中顺序编号,这些编号可以看作存储单元在存储器中的地址。
计算机中 连接 CPU 与其他芯片的导线被称作是 "总线" ,
根据传输信息的不同, 其从逻辑上可分为三类 :
地址总线 (CPU通过地址总线来指定存储器单元)
「传送地址信息,用于告诉内存或 IO:我要访问哪一个地址!」 e.g.1
2
3
4
5- CPU 把地址 `0x1000` 放上地址总线
- 同时发出控制信号(控制总线):我要读内存!
- 内存模块会在地址 `0x1000` 返回对应的数据CPU通过地址总线来指定存储单元 ,决定了CPU可以对多少个存储单元进行寻址。
地址总线的宽度
(位数)决定了CPU寻址的最大空间大小,假设一个CPU有10根地址总线,其寻址最大数为2^10 = 1024个最小数为0,最大数为1023
✨注: 在现代 64 位系统中(如 Win11 x64),虽然理论地址总线是 64 位,但操作系统/CPU 实际通常只启用了部分位数(例如 48 位物理地址线 → 可寻址 256 TB 内存)。
地址总线可寻到的内存单元构成了这个CPU的内存地址空间。
控制总线
控制总线的宽度决定了CPU对外部器件的控制能力。 [即位宽, 位宽指的是控制信号线的数量。这些控制信号线用于管理和协调CPU与其他组件之间的通信 ]
数据总线
数据总线的宽度决定了
CPU与外界的数据传输速度, 8根数据总线可以一次传送8位二进制数据 (即1Byte),后注: 8086CPU数据总线宽度为16 ,寄存器为16位, 即一次最大可从内存中读取或写入
一个字的数据大小注:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18🎯 数据总线是硬件层的传输能力:
- **64位数据总线**:代表 CPU 与内存之间一次最多可以传输 **64位 = 8字节** 的数据。
- 它决定了**单次读/写操作**中能够搬运多少“货物”。
- 类比:数据总线就像一辆卡车,一次能拉多少箱子是它的载重量。
---
🧠 而线程是软件层的逻辑控制:
- **线程**是 CPU 执行的指令序列。每个线程会使用自己的寄存器集、栈等,**共享**某些资源(比如内存)。
- 多线程 ≠ 数据总线变宽。
- 多线程 ≈ 多个搬运工可以排队用同一辆卡车。
稍稍拓展一下啦 , 如现在我的64位的CPU , 意味着其地址总线有64根,因此它的最大寻址能力是2^64。这意味着CPU可以直接访问的内存地址空间是2^64个字节,也就是16EB(艾字节)。但是,实际上,由于各种硬件和软件的限制,实际可用的内存通常远小于这个理论值。例如,Windows操作系统通常只支持到128GB或更少的物理内存。此外,某些地址被保留用于特殊用途,如内存映射的I/O设备,因此不能用于常规内存。
内存地址空间
CPU操作存储器(RAM(Random Access Memory 我们常说的内存条,临时存数据,断电就消失。) ROM 带有BIOS的ROM - (比如 BIOS 所在的存储芯片,是写好后不经常改动的程序,掉电也保留。))时将他们看作内存来对待,将他们总的看作一个由若干个存储单元组成的逻辑存储器. 这个逻辑存储器就是所说的内存空间地址.
附加理解 :
CPU 看它们(RAM ROM)不是“分开的”,而是“统一的地址空间”
- 在汇编或者底层编程中,CPU 并不关心具体数据存在哪块 RAM 或 ROM。
- 它通过一个统一的“地址空间”访问数据 —— 你给我个地址(比如 0x7FFEF000),我就去那里取/存数据。
🌐 这就叫做:将 RAM 和 ROM 抽象为一个“逻辑存储器”。
什么是逻辑存储器 ?
- 可以想象为:一个连续编号的巨大仓库(编号 = 地址),CPU 可以随机读取或写入任意位置。
- 每一个存储单元(最小粒度)通常就是 1字节(8位)。
- 所以说:内存空间是一个地址从 0 开始,每个位置对应一个字节的逻辑数组。
每一个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间,对于CPU来说,在这段地址空间中进行数据读写 , 实际也就是对对应的物理存储器进行读写.
因为内存空间地址与存储单元相关,其大小受到CPU地址总线宽度的限制,例如8086CPU的地址总线为20,则其可以传送2^20个不同的地址信息(0 ~ 2^20-1),也就是可以定位2^20个内存单元,则其内存地址空间大小为 2^20 bytes, 或 1024^2 bytes , 即 1MB
在基于一个计算机硬件进行系统编程时,必须知道这个系统中内存地址的分配情况, 在对某类存储器进行数据读写时,必须知道它的第一个单元的地址和最后一个单元的地址,才能保证读写操作是在预期的存储器中进行的.
寄存器
CPU由运算器 \ 寄存器 \ 控制器 等器件组成,这些器件靠内部的总线相连..
内部总线实现CPU内部各个器件之间的联系, 外部总线实现CPU和主板上其他器件的联系
🚩 字在寄存器中的存储
25.1.20 注: 字在寄存器中的存储和数据总线的宽度有关吧 , 比如64位cpu中 一个字64bit , 也就是 8个字节, 一个字(64位)的高位字节和低位字节分别占据4个字节 (即32位)
字(记为word) ,在8086CPU中 , 其数据单元宽度为16位, 即一个字由2个字节组成 这两个字节分别称为这个字的高位字节和低位字节.
对于64位CPU , 在计算机体系结构中,64位意味着一个字(word)的大小为64位,也就是8个字节。详细解释一下:
- 64位指的是数据总线的宽度,表明一次最大可从内存中读取或写入的数据大小
- 一个64位字由64个二进制位组成,等于8个字节。
- 64位CPU和算术逻辑单元(ALU)的设计基于64位大小的寄存器、地址总线或数据总线,支持64位宽度的整数运算和逻辑操作。
字单元,即存放一个字型数据(8086CPU - 16位)的内存单元,由两个地址连续的内存单元组成, 高地址内存单元中存放字型数据的高位字节 , 低地址内存单元中存放字型数据的低位字节.
由此可见8086CPU采取了小端模式
所谓的小端模式(Little-endian),是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致. 记忆: 地址的增长顺序与值的增长顺序相同
一个十六进制数 = 四位二进制数 , 遂 一个字节需要以两个十六进制数来表示
几条汇编指令与8086CPU给出物理地址的方式
在进行数据传送或运算时,要注意两个操作对象的位数应当是一致的. 高低位对应
1 | add ax,bx |
`8086CPU`有20位地址总线,可以传送20位地址,寻址能力` 2^20 (bytes) = 1024^2 (bytes) = 1024kb = 1MB` ,( BIT - BYTE - KB -MB - GB - TB - PB - EB -ZB - YB )
- Kilobyte (KB): 1024 Bytes
- Megabyte (MB): 1024 Kilobytes
- Gigabyte (GB): 1024 Megabytes
- Terabyte (TB): 1024 Gigabytes
- Petabyte (PB): 1024 Terabytes
- Exabyte (EB): 1024 Petabytes
- Zettabyte (ZB): 1024 Exabytes
- Yottabyte (YB): 1024 Zettabytes
以此类推。每个单位都是以1024倍递增,这样的命名方式来源于国际单位制的前缀。
内部采用2个16位地址合成的方法来形成一个20位的物理地址(一个称为段地址 , 一个称为偏移地址)
段地址和偏移地址通过内部总线被送入一个称作地址加法器的部件中,由它将两个16位的地址组合为20位的物理地址
地址加法器通过物理地址 = 段地址 × 16(d) + 偏移地址的方式合成物理地址(5位十六进制数 , 即20位二进制数)
段地址 × 16 常用说法 左移4位(指二进制位) , 相当于 乘 2^4
基础地址( 即将 段地址 x 16(d) 看作基础地址 ) + 偏移地址 = 物理地址
关于8086CPU的物理地址的五位十六进制数表示方式:
因为其寻址能力为1MB , 一位十六进制数相当于4位二进制数 , 所以五位十六进制数 = 4*5 = 20位 = 2^20 = 1MB 正好符合了8086CPU的寻址能力范围.
🔓 测验题解 - 2.2
有意思的题目…
有一数据存放在内存20000H单元中,现给定段地址为SA,若想用偏移地址寻到此单元。则SA应满足的条件是:最小为 ? 最大为 ?
最大值: 比较好求, 设SA为x, 则 EA为 0000H, x = 20000H / 16 = 2000H , 即二进制位右移四位
最小值: EA 最大 为FFFF, 则段地址为 2 0000H - FFFF 后右移4位(逆向) 结果 1000H,但是经过验算结果非20000H 而是 1FFFF,初见这我也觉得很奇怪… (我的计算器绝对没有问题.jpg) 但其实1FFFF即段地址1000H的最大寻址范围 , 与所需求内存单元就差 1 , 所以1000H + 1 = 1001H 这才是正确的值.也即是最小满足SA条件的值
好怪但雀氏
1 | (20000H-ffffH)/16 |
段寄存器
8086CPU有四个主要的段寄存器 : CS Code Segment \ DS Data Segment\ SS Stack Segment\ ES Extra Segment 附加段寄存器
CS:IP 是8086CPU中最关键的俩个寄存器 , 它们指示了 CPU 当前要读取指令的地址
CS为代码段寄存器 (Code Section) IP为指令指针寄存器 (Instruction Pointer)
CS * 16 + IP = 物理地址
关于 IP / CS 的修改
注: 8086CPU并不提供修改 CS IP的指令 , Debug中r指令可修改CS : IP , 这是通过调试手段实现
能改变IP / CS 内容的指令被统称为转移指令, 目前可以用一个简单的指令来修改它们 :jmp 指令
指令形如 jmp 段地址:偏移地址, 将CS修改为段地址, IP修改为偏移地址
或者使用 jmp 合法寄存器来修改IP的内容
e.g. jmp ax => mov IP,ax
寄存器与内存访问
DS寄存器(数据段寄存器 , data segment register)通常用来存储要访问数据的段地址
🚩 CPU提供的栈机制
栈 – 拥有特殊访问方式的存储空间 – LIFO | Last In First Out后进先出, CPU提供的最基本两个指令是 PUSH(入栈) 和 POP(出栈),它们的操作都是以字为单位进行的
SS \ SP 寄存器 – ( Stack Segement ) / ( Stack Pointer ),通过两者定义栈段,任意时刻 SS:SP都指向栈顶,CPU将从此处取得栈顶的地址 , 注意 CPU 只记录栈顶,栈空间的大小需要自己管理
pop / push 的执行过程
执行push时.CPU的两步操作是: 先改变SP, 后向SS:SP处传送, 执行 pop时, CPU的两步操作是:先读取SS:SP处的数据,后改变SP
⭐ 栈综述
将一段内存当作栈段, 这是我们自己在编程中的安排, 想要pop / push 等栈操作指令访问我们定义的栈段,就得将SS:SP指向我们定义的栈段
一个栈段的最大容量为64KB
栈满时继续压栈将导致**栈顶环绕 | 即 栈溢出**
- 当栈已满时,再次执行入栈操作会导致栈顶环绕,即数据从栈顶重新回到栈底。
- 栈顶环绕的影响:
- 栈顶环绕可能导致数据被覆盖。新的数据会覆盖最早入栈的数据。
- 这可能会导致程序错误、崩溃或不可预测的行为。
- 避免栈顶环绕:
- 为了避免栈顶环绕,程序员需要确保栈的容量足够大,或者在使用栈时谨慎处理数据。
- 在编写汇编程序或其他语言的代码时,需要注意栈的使用,以避免栈溢出问题。
❤️ 务必注意, 当栈空时, SS:SP将指向栈底的后一个内存单元.
When the stack is empty ,SP Point to the address of the next memory unit at the bottom of the stack , namely SP = At the bottom of the stack +1.
栈用于保存函数调用的现场(例如寄存器的值、局部变量 , 函数调用和返回地址等)。
当函数返回时,栈上的数据被弹出,恢复到调用函数之前的状态。
出栈和入栈操作不会影响到内存中的数据,只是在栈中进行了数据的移动和临时存储。
⭐ 关于32位架构中为什么不能以内存到内存的形式传递数据
1. **指令设计和硬件限制**: - 汇编语言的指令集是由硬件架构和设计决策所决定的。 - 大多数汇编指令都是为了从寄存器到内存、内存到寄存器、立即数到寄存器等进行数据传输而设计的。 - 直接从一个内存位置复制数据到另一个内存位置的指令并不常见,因为这样的操作通常需要更复杂的指令序列。 2. **间接传输**:The answer involves a fuller understanding of RAM. Simply stated, RAM can only be in two states, read mode or write mode. If you wish to copy one byte in ram to another location, you must have a temporary storage area outside of RAM as you switch from read to write.
It is certainly possible for the architecture to have such a RAM to RAM instruction , but it would be a high level instruction that in microcode would translate to copying of data from RAM to a register then back to RAM. Alternatively, it could be possible to extend the RAM controller to have such a temporary register just for this copying of data, but it wouldn’t provide much of a benefit for the added complexity of CPU/Hardware interaction.
- 虽然没有直接的内存到内存传输指令,但可以通过使用寄存器作为中介来实现间接的内存到内存传输。
- 例如,您可以将数据从一个内存位置加载到寄存器,然后将寄存器的内容存储到另一个内存位置。
性能和复杂性考虑:
直接的内存到内存传输可能涉及到更多的总线操作和更复杂的硬件电路。
这样的操作可能会影响性能,并增加指令的执行时间。
⭐ 关于在8086 CPU中,不能直接将立即数传递给段寄存器
段寄存器:
8086 CPU具有四个段寄存器:CS(代码段)、DS(数据段)、SS(堆栈段)和ES(附加段)。
这些段寄存器用于指示内存中不同段的起始地址。
立即数和段寄存器:
在8086汇编中,不能直接使用立即数来赋值给段寄存器,例如MOV DS, 1000H是错误的。
段寄存器的值必须通过其他方式获得,例如从其他寄存器、内存或其他操作数中加载。
不能直接将立即数传递给段寄存器的原因涉及到硬件设计和指令集的考虑。详见解释:
- 硬件设计:
- 8086 CPU的设计中,段寄存器的作用是指示内存中不同段的起始地址。这些段包括代码段、数据段、堆栈段等。
- 段寄存器的值必须是一个有效的内存地址,而不是一个立即数。
- 指令集设计:
- 8086的指令集并没有直接支持将立即数传递给段寄存器的操作。
- 指令集中提供了其他方式来加载段寄存器,例如使用其他寄存器、内存地址或其他操作数。
⭐ 关于汇编中 十六进制数据前加 ‘0’ 规则
如何做?
0~9开头的不用加, A~F开头的要加
主要是汇编语言编译器的设计者需要用户按这个规则编程, 否则编译器不知道你写的是什么。
因为标号不能以0~9的数字开头,但可以以字母开头,
导致无法区分数值与标记
⭐ 8086CPU的工作过程
- 从
CS:IP指向的内存单元读取指令 , 读取的指令进入指令缓冲器 IP指向下一条指令- 执行指令 ( 转到步骤 1 , 重复这个过程 )
程序执行过程与跟踪
程序由Shell(操作系统的外壳程序,用户使用Shell来操作计算机系统进行工作 (如DOS中的command.com就是一个Shell) 加载入内存
之后Shell将设置CS:IP来指向程序的入口点, 然后暂停运行, 将程序交给CPU执行.
待任务执行完毕后, 控制返回到Shell , 屏幕显示由当前盘符和当前路径组成的提示符,等待用户输入.(返回到加载者)
📌 实验三 - 编程|编译|连接|跟踪 - - 分析
debug 跟踪以下程序执行过程 , 记录每步执行后相关寄存器中内存和栈顶内容
1 | assume cs:codesg |
✨
Base Register[bx] And “Loop” instruction
[bx](base, 基址寄存器 , 常用于地址索引 )实际为一个偏移地址EA , 段地址SA默认存放在DS中
LOOP 指令格式 : loop 标号 , 通过loop指令实现循环功能 , 通过 CX ( count )寄存器控制循环次数
执行顺序 :
- (CX) = (CX) -1
- 判断 若 CX > 0 , 则向前转至
标号处执行 , 若 CX = 0 , 则继续向下执行
值得注意的是, 这里跳转到 标号 处,相当于高级语言中常见的 for \ while 等 循环中常见的 条件循环 , 汇编实现为 jle,jmp,jne等. 或许之后会详细提到
总体程序框架:
1 | mov cx,循环次数 |
Pseudo code
1 | CX = CX - 1 |
🍃更新:
loop所跳转的标号 , 实际表示的是一个偏移地址.
⚠️ Debug和汇编编译器MASM对指令的不同处理 | [idata]
若处理以[]包含的指令时, 型如[idata],它们的处理结果如下
Debug 将[idata]视作一个内存单元, 将内部idata作为内存单元的偏移地址 , 其段地址为 DS
编译器将[idata]解释作idata
如果要让编译器将其解释为一个 内存单元, 则必须在[]前显式的给出段地址DS
(Concept)段前缀
用于显式指明内存单元的段地址的"DS:","CS:","SS:","ES:", 在汇编语言中被称为 段前缀
📌 实验四 - [bx]与Loop的使用 (3) - - 分析
补全程序 , 将 mov ax,4c00H 之前的指令复制到内存 0:200中
1 | assume cs:code |
通过debug获取 程序的总字节量, 计算于 mov ax,4c00H之前的字节量.( 且注意debug中以十六进制表示的数据)
可以发现需要获取的数据在076C:0017之前,也就是从偏移地址0~16H总共23(17H)个字节

包含多个段的程序 \ dw \ end
dw | end
在代码段中使用数据: 可以使用**dw(define word)**定义字型数据.
利用end的一个作用是:通知编译器 程序的入口点在何处,可以使用Start 标号指明程序入口点
END START的含义是:程序结束,并且程序的入口点是START标签所指示的位置。这样的结尾是合理的,因为它明确指定了程序的入口点,并告知汇编器程序的结束位置。
🍊 程序分析 6.3
1 | assume cs:code |
编写多段程序
Assume | 段名
利用assume伪指令将定义的段和相关的寄存器联系起来 , 对于不同的段, 要有不同的**段名**
段名就相当于一个标号,它代表了一个段的段地址 , 编译器会把它处理为一个表示段地址的值
注意在8086CPU不允许直接将数值传入段寄存器中, 所以要将定义的段程序赋给特定的寄存器时需要一个寄存器作中转.
CPU如何处理所定义的段的内容, 当作指令执行或是数据访问, 亦或是栈空间… 完全取决于程序中具体的汇编指令 , 及汇编指令中对CS:IP SS:SP DS 等寄存器的设置决定
段定义伪指令
段使用 段名 + segment 关键字 来定义 , 代表段的开始
段名 + ends 关键字 , 代表段的结束
📌 实验五 - 编写与调试多段程序 - - 分析
编译链接并跟踪调试
1 | ; Experiment 5 |
Experiment 5 Quiz 1
程序返回前, data段中数据为 23 01 56 04… 共16个字节 , 剩下部分用00补全
程序返回前, CS = 076E SS = 076D DS = 076C
程序加载后, 设Code段的段地址为X , 则data 段的段地址为
X-2, stack段的段地址为X-1对于如下定义的段:
1 | name segment |
如果段中的数据占N个字节,则程序加载后 , 该段实际占有的空间为 (N / 16 + 1 )N小于16,则向下取整 * 16 个字节 , 若N大于16 , 则向上取整 …
- ⚠️ 若将伪指令 end start 改为 end (即不指明程序的入口点) . 程序均可正常执行 , 只是不再从指定的入口点开始执行,而是从程序开始的程序段处开始执行, 若此程序段为数据段或栈段, 编译器会将其处理为汇编指令并执行.
Experiment 5 Quiz 5
将a段和b段数据依次相加 , 结果存在cg段
1 | assume cs:code |
Experiment 5 Quiz 6
用push指令将 A 段中的前8个字型数据逆向存储到B段中
1 | ; E5q6 |

更灵活的定位内存地址的方法
如果一个问题的解决方案,使我们陷入一种矛盾之中。那么,很可能是我们考虑问题的出发点有了问题,或是说,我们起初运用的规律并不合适。
AND 与 OR 指令
与基本逻辑与 & 或 | 相同 , 只是汇编中皆按位进行运算
AND: 逻辑与指令, 按位进行与运算 , 通过该指令将操作对象相应位设为 0 ,其他位不变
OR: 逻辑或指令, 按位进行或运算 , 通过该指令将操作对象相应位设为 1 ,其他位不变
以字符形式给出的数据
在汇编程序中 , 可以使用形如 '....'的方式指明数据是以字符的形式给出的 , 编译器会将其转换为相应的 ASCII 码
E.G.
1 | db 'unIX' ; 相当于 db 75H,6EH,49H,58H 与字符的 ascii码相对应 |
[ BX + IDATA ]
可以以此方式来灵活的表明一个内存单元 , 即 [bx + idata]表示一个内存单元,偏移地址为 (bx) + idata , 段地址 于DS中
E.G.
1 | mov ax,[bx+200] ; 将一个内存单元存入AX , 这个内存单元占2个字节, 即存放一个字,偏移地址为 bx的值 + 200 , 段地址在ds中 |
SI (Source Index Register) & DI (Destination Index Register)
SI和DI都是变址寄存器(Index Register) , 且都是16位的寄存器
它们主要用于存放存储单元在段内的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。它们可作一般的存储器指针使用。在字符串操作指令的执行过程中,对它们有特定的要求,而且还具有特殊的功能。
不同寻址方式的灵活应用
不同的寻址方式:
[idata] 用一个常量来表示地址, 可用于直接定位一个内存单元.直接寻址
[bx]用一个变量来表示内存地址,可用于间接定位一个内存单元. 寄存器间接寻址
[bx+idata]用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元.寄存器相对寻址
[bx+si]用两个变量表示地址. 基址变址寻址
[bx+si+idata]用两个变量和一个常量表示地址。相对基址变址寻址
自顶向下逐渐可以用更灵活的寻址方式来定位一个内存单元的地址 。这使得数据在使用者的角度可以以结构化的角度看待。
关于数据的暂存
有时会遇到需要数据暂存的情况 , 如嵌套循环的CX , 作为计数器使用 . 由于寄存器的数量有限, 且每个程序使用的寄存器不一样 , 遂需要更为通用的方案
在不考虑使用寄存器的情况下
- 可以将要暂存的数据存放到内存空间中,需要的时候再从内存单元中恢复. 但是当数据量多的时候,这样的存储方式需要记住哪个数据放到 了哪个单元。容易引起程序混乱。
- 将需要暂存的数据存入栈中 , 利用压栈出栈与 栈顶指向 控制栈空间的数据
📌 实验六 - 灵活定位内存地址并修改值 - - 分析调试
将datasg段中的每个单词的前四个字母改为大写字母
1 | ; Experiment 6 |
✨ 数据处理的两个基本问题 何处? 多长?
- 要处理的数据在何处
- 要处理的的数据有多长
于汇编中数据位置的表达
- 立即数
idata,在汇编中直接给出的数据 - 寄存器 , 汇编指令中需给出相应的寄存器名
- 段地址(SA) 和 偏移地址(EA) , 汇编中可用
[x]形式给出EA , SA存在于某个段寄存器中
指明要处理的数据长度 - x ptr
8086CPU可处理两种长度的数据 Byte 和 Word ,在指令中需要指明需要处理的数据的尺寸.
- 可以通过寄存器名来指明数据的尺寸.
- 在没有寄存器名的情况下,可以使用形如
X ptr来指明内存单元的长度 ,X在汇编中可以为word或是byte.
如mov word ptr ds:[0],1inc word ptr [bx]这俩指令使用word ptr指明了指令访问的内存单元是一个字单元.
注意有的指令就不要指明内存单元的长度了 , 像是push \ pop,其只能进行字操作.
div Instruction 除法指令
语法
1 | div divisor |
除法指令 , 需要注意几个问题
除数 的两种情况 :
8bit&16bit, 在一个寄存器或是内存单元中被除数 默认放在
AX中 或是DX 和 AX中 , 情况如下- 若除数为 8位 , 被除数则为
16位, 此时被除数默认存放在AX中 - 若除数为16位 , 被除数则为
32位, 此时被除数被存放在DX和AX中 ,DX中存放高16位 , AX中存放低16位
- 若除数为 8位 , 被除数则为
对于计算结果的存储
- 若除数为 8位 , 则
AL将存储 除法操作的商,AH中存储的则是余数 - 若除数为 16位 , 则
AX中将存储 除法操作的商,DX中存储的是余数
- 若除数为 8位 , 则
一般寄存器的存储主要是关于被除数 和 结果.
✨ 拓展:
在x86汇编中,如果你使用的是8位的除数,那么被除数通常是16位的。同样,如果你使用的是16位的除数,那么被除数通常是32位的。这是因为在进行除法操作时,结果的商和余数都需要有足够的空间来存储。
在x86汇编语言中,如果你使用的是16位的除数,那么它的理论最大值可以是2^16,也就是65536。
在x86汇编语言中,被除数的最大理论长度确实可以达到32位。这意味着被除数的最大值可以是2^32,也就是4294967296。
实例:
1 | ; 1001 / 100 div calculation |
伪指令 - dd (define dword)
dd用来定义双字 型数据 , 占用32位 , 需要2个16位寄存器来分别存储其高低16为位
dup Operator
dup是一个操作符 , 和 db \ dw \ dd 一样, 其也是由编译器识别处理的符号 , 与他们(数据定义指令等伪指令)配合使用 , 用以进行数据的重复.
示例如下:
1 | db 3 dup (0) ; 定义了3个字节 , 他们的值都是 0 , 相当于db 0,0,0 |
可见,dup的使用格式如下:
db 重复的次数 dup(重复的字节型数据)dw 重复的次数 dup(重复的字型数据)dd 重复的次数 dup(重复的双字型数据)
📌 实验七 - 寻址方式在结构化数据访问中的应用 - - 分析调试
; 比较综合的一个实验, 融合之前的知识 可以结合C语言结构体进行类比.
将 data 段中的数据按照以下指定格式写入 table段

1 | assume cs:codesg |
✨ 转移指令及其原理
可以修改IP , 或同时修改 CS和IP的指令统称为 转移指令
针对8086CPU来说, 其转移行为有以下几类:
- 只修改IP时 , 为
段内转移 - 同时修改CS和 IP 时 , 为
段间转移
针对转移指令对IP修改范围的不同, 段内转移还分为: 短转移 (IP修改范围: -128 ~ 127)和 近转移 (IP修改范围: -32768 ~ 32767) (前后八位的区别吗 ( 字节范围和字范围 )) , 负数向前转移 , 正数向后转移
操作符 Offset
操作符offset在汇编语言中是由编译器处理的符号 , 它的功能是取得标号的偏移地址
Jmp 指令
Jmp指令为无条件转移指令 , 可以只修改IP , 或同时修改 CS和IP
此指令需要给出两种信息:
- 转移的距离 (
段间转移 \段内 短转移 \段内 近转移 ) - 转移的目的地址
✨ 依据位移(Displacement)进行转移的 JMP 指令 +. 补码
Jmp short 标号 ( 转到标号处执行指令 )
此格式的 JMP指令 实现的是段内短转移 , 对
IP的修改范围为 -128 ~ 127 , 即向前转移最多越过 128个字节 , 向后转移最多越过 127个字节 , “Short” 说明了其进行得是短转移 ,间接说明Short类型为8位 , “标号”存在于代码段中 , 是指令转移的目的地
注意 在jmp short 标号指令所对应的机器码中,并不包含转移的 目的地址,而包含的是转移
的位移, 此位移是编译器根据汇编指令中的 标号 计算出来的。

jmp short 标号的功能实际为 : (IP) = (IP) + 8位位移
- 8位位移 = 标号处的地址 ( 标号指令起始地址 ) - jmp指令后的第一个字节的地址( 指令起始地址 )
- 🌼
short指明此处的位移为8位位移 - 8位位移的范围为-128~127,用
补码表示 , 8位的最大最小值 ( 即IP最多向前移动128字节 , 向后移动 127字节, 若不在范围内会引发转移位移超界的问题 ) - 8位位移由编译程序在编译时算出。
可得在 jmp short指令的机器指令中 , 包含的是跳转到目标指令的相对位置 偏移量(即位移 , 以补码形式存储) , 而不是转移的目标地址
补码 Two’s Complement Review:
以8位数据表示 有符号数 , 其最高位表符号 , 1 为负 , 0 为正 . 用其他位表示数值
正数的原码反码补码均相同
补码的基本思想:先确定用 0000 0000b ~ 0111 1111b表示0 ~ 127,然后再用它们按位取反加1后的数据表示负数。
补码方案特性:
最高位为1表示负数
正数的二进制值(原码)取反加1后,为其对应的负数(相反数)的 二进制数 :负数的补码取反加1后,为其绝对值。
由于一个负数的补码不太容易看出它所表示的数据 , 但是利用补码的特性将其 按位取反再加1 后可得其 绝对值 的二进制数, 则负数的值取相反数即可.
补码可以让正数与负数之间进行加法运算 , 即所有减法都可转化为加一个负数的形式 :
A - B = A + (-B)
顺便一提 ,
计算机都是以补码的形式存储数据的
段内近转移: jmp near ptr 标号
- 16位位移 = 标号处的地址 - jmp指令后的第一个字节的地址
- 🌼
near指明此处的位移为16位位移 - 16位位移的范围为-32768~32767,用
补码表示 , 16位的最大最小值 - 16位位移由编译程序在编译时算出。
jmp far ptr 标号 实现的是段间转移 , 又称为远转移
far ptr 指明了指令用标号的段地址和偏移地址修改**CS和IP** , 在机器码中 , 高地址部分存放段地址 , 低地址存放偏移地址 , 顺序遵循小端存储模式
Recommend to refer : 汇编编译器(MASM.exe) 对 jmp 指令的相关处理
转移地址在寄存器中的JMP指令
格式 :
1 | jmp 16bit reg ; 格式 |
🛠️转移地址在内存中的JMP指令
两种格式 :
1 | 1. |
jcxz指令 (Jump if CX equals Zero)
所有的有条件跳转指令都属于[短转移](#依据位移进行转移的 JMP 指令 +. 补码)(段内短转移),其在机器码中包含的是转移的位移(位移这个概念还是很重要的Refer Chapter 9.9 ),而非目的地址
格式:
1 | jcxz flag |
loop 指令
循环指令, 注: 所有的循环指令都是 短转移 , 其机器码中 同样包含的是 相对目的地址的位移而非偏移地址
格式:
1 | loop 标号 |
📌 实验八 - 分析奇怪的程序 (jmp 与 位移) - - 分析调试
分析以下程序是否会正常返回
1 | assume cs:codesg |

📌 实验九 - 根据材料编程 (彩色字符显示) 分析调试 与 显示缓冲区


1 | assume cs:codesg,ds:data,ss:stacksg |
注意 : 每行 80列(即每行可存储80个字符 ) , 占 160个字节 (A0H) 每个字符占用2字节 ( 字符の ASCII码 + 字符属性 )
CALL 和 RET 指令
CALL和RET都是转移指令,常用来实现子程序的设计.
1. ret 和 retf
ret使用栈中的数据修改IP , 实现近转移
retf使用栈中的数据修改IP和cs , 实现远转移
以汇编语法解释这两条指令相当于:
1 | ret: |
LIFO, 同时注意压栈的顺序 , 先压栈CS,再是IP
2. call 指令
call的执行进行两步操作
- 将当前
IP或 将当前CS和IP压入栈中 - 转移
注意call无法实现短转移 , 其实现转移的方法和jmp指令原理相同 , 即位移
根据位移进行转移 , 语法:
1 | call 标号 |
转移的目的地址在指令中的CALL指令:
1 | call far ptr 标号 ; 实现的是段间转移 (同时改变了CS IP) |

转移目的地址在寄存器中 :
1 | call 16bitReg ; 以十六位寄存器内容作爲轉移目的IP |
相当于:
1 | push IP |
转移地址在内存中的CALL , 存在两种格式 ,
与 🛠️转移地址在内存中的jmp指令 类似 , 可参考
1:
1 | call word ptr 内存單元地址 ; 以内存單元処的字單元作爲目的地址IP |
2:
1 | call dword ptr 内存单元地址 ; 段间转移 ,低地址存放偏移地址 高地址存放段地址 |
mul Instruction 乘法指令
格式
1 | mul reg |
使用mul指令 , 需要注意两点:
1. 两乘数
两个相乘的数要么都是8位 , 要么都是16位
- 8位的情况下 , 一个乘数默认放在
AL中 , 另一个放在一个8位的reg中 或 内存字节单元中 - 16的情况下,默认的乘数将放在
AX中, 另一个放在一个16位的reg中 或 内存字单元中
(注意是reg与内存单元中而非立即数 , 不允许直接提供立即数(idata)进行运算)
2 结果
- 8位的情况下 , 结果默认存放在
AX中 - 16位的情况下, 结果的高位存放在
DX中 , 低位存放在AX中
对现实问题进行分析时,把它转化成为相互联系 \ 不同层次的子问题 , 是必须的解决方法
汇编模块化程序设计 - 以栈进行的参数传递
除了以寄存器和内存单元来进行对参数的存储外 , 也可使用栈存储
因为这种技术和高级语言编译器的工作原理密切相关, ( 很难不想到编译原理… ) 所以简单记录一下
其原理:
调用者将需要传递给子程序的参数压入栈中, 子程序从栈中取得参数.
指令ret n 的含义 , 以汇编指令描述为:
1 | pop ip |
子程序中 [ 寄存器冲突 ] 问题 解决方案
未避免子程序中寄存器冲突. 即在编写子程序时不需要考虑 调用者 使用了哪些寄存器
将使用以下子程序编写标准框架:
1 | 子程序开始:子程序中使用的寄存器入栈 |
📌 实验十 - 编写子程序
1 | ; 显示字符串 |
2略
3.

🔦 综合设计 1
:TODO
标志寄存器(Flag)
为16位寄存器 , 按位起作用, 每一位都有专门的含义, 记录特定的信息. 其中1\3\5\12-15 位是没有使用的标志位 , 不具有任何意义
ZF 标志(Zero Flag)
处于标志寄存器的第六位, 零标志位,记录相关指令执行后, 其结果是否为0 , 若结果为0 , 则ZF = 1, 反之 ZF = 0
使用某条指令的时候 , 要注意这条指令的全部功能 , 包括其执行结果对于标志寄存器的标志位造成的影响
PF 标志(Parity Flag)
处于标志寄存器的第二位, 奇偶标志位,记录相关指令执行后, 其结果所有bit位中1的个数是否为偶数 , 若1的个数为偶数 , 则PF = 1, 反之 PF = 0
SF 标志(Sign Flag)
Flag的第七位, 符号标志位 . 记录相关指令执行后 , 其结果是否为负 . 若为负 , 则 SF = 1, 反之 SF = 0
对于同一个二进制数据, 计算机可以将它当作无符号数据来运算, 也可以当作有符号数据来运算.
SF标志仅是对有符号数运算结果的记录 , 所以若是将数据作为有符号数 , 则 SF 有意义 , 若作为无符号数 , 则没有意义.(取决于程序功能实现, 见机行事)
CF 标志 (Carry Flag - Usually indicated as the C flag)
处于第零位, 进位标志位 , 在进行无符号数运算的时候, 它记录了运算结果的最高有效位向更高位的进位值(即加法) , 或是从更高位的借位值(减法情况)
一图胜千言.jpg

OF 标志 (Overflow Flag)
溢出标志位 , 处于第11位, 在进行有符号运算时, 如果结果超出了机器所能表示的范围即溢出 ,则OF为1 , 若无溢出 , 则为0
ADC and SBB instructions
这两个指令可用来处理任意大的数之间的加减运算 ,利用了进位与借位值 ,与CF的关系密不可分
指令格式分别与add与sub相同
cmp Instruction
cmp指令的执行将对标志寄存器产生影响 , 其他相关指令可以通过识别标志寄存器的值来得知比较的结果
指令格式 :
cmp 操作对象1,操作对象2
根据 操作对象1 - 操作对象2计算结果并对标志寄存器进行设置 , 不保存结果
检测比较结果的条件转移指令
“转移”指指令可修改IP , 条件即决定是否修改ip
所有转移指令的位移都是[-128,127]
大多条件转移指令都通过检测cmp指令所影响的标志位的检测结果来决定是否修改IP

DF标志( Direction Flag ) 和 串传送指令 move String instructions
DF标志是 Flag中的第10位 ,"方向标志位" 在串处理指令中 , 控制每次操作后 ( 即串指令执行完毕后 ) si与di的增减
df=0 每次操作后 si \ di 递增
df=1 每次操作后 si \ di 递减
数据单位与串传送指令相关 , 若为movsb (string byte) 则为1字节 , SW ( string word ) 则为一个字
cld & std Instructions
CLD : Clears the DF flag in the EFLAGS register. When the DF flag is set to 0, string operations increment the index registers (ESI and/or EDI)
STD : Sets the direction flag to 1, causing all subsequent string operations to decrement the index registers, (E)SI and/or (E)DI, used during the operation.
MOVS/MOVSB/MOVSW:
This instruction copies a byte(movsb) or a word(movsw) from a location in the data segment [DS:SI] to a location in the extra segment [ES:DI]. The offset in the data segment for the source is to be stored in the
SI(Source Index)register and the offset for the destination in the extra segment is to be stored in theDI(Destination Index)register.For multiple byte/word movement, the value stored in the
CX registerby the user functions as a counter. After each move, SI and DI are automatically adjusted to point to the next source and destination respectively.
以上指令通常和Rep指令(Repeat while equal)共同使用 , 类似的还有repnz (repeat while nonzero) or repz (repeat while zero) , 此类指令为Repeat String Operation
Description refer :
Use the rep (repeat while equal), repnz (repeat while nonzero) or repz (repeat while zero) prefixes in conjunction with string operations. Each prefix causes the associated string instruction to repeat until the count register (CX) or the zero flag (ZF) matches a tested condition.
PUSHF & POPF
前者将标志寄存器的值压入栈中 后者将栈中弹出数据并送入标志寄存器中
🔖 学习路径说明 - Explanation Update
以下 为 Chapter 12 - 17 , 12-13 为复习与巩固. 实际学习路径有变化 , 将跟随课程学习

Chap 14
移位指令
逻辑左移 Shift Logical Bit Left SHL OPR,CNT REF:SHL
循环左移 ROL OPR,CNT
注: less-significant bit (LSB) 最低位 \ The most-significant bit (MSB) 最高位
注意如果移动位数大于1的时候 , 则必须将移动位数放在 cl 中
+. Chap 16 🔖 描述内存单元的标号 - [ 数据标号 ] 与 地址标号
不同于只表示地址的地址标号 , 数据标号标记了存储数据的单元的地址和长度 , 加上段地址就可以表示一个内存地址
1 | ; e.g. |
注意 形如:
1 | a db 1,2 |
相当于
1 | c dw offset a,offset b |
存储的是 a \ b 标号所在的偏移地址 , ╰(°▽°)╯ 若说以上a , b 像指针 , 那 c 就有点类似与 C语言 中的二级指针…..
注意后面加有":"的地址标号只能在代码段(cs)中使用, 不能在其他段中使用
直接定址表
利用表(Table),在两个数据集合之间建立一种映射关系(mapping)使我们可以用查表的方法根据给出的数据(作索引)得到其在另一集合中的对应数据
空间换时间的做法(方案)….
其主要目的一般有:
- 为了算法的清晰和简洁
- 加快运算速度
- 是程序易于扩充
主要分为数据的直接定址表和 代码的直接定址表(与函数指针类似 , 能看见程序模块化的影子了 使程序结构清晰, 便于扩充 , 若要加入新的功能子程序 , 只需要在直接定址表中添加子程序的入口地址即可)
🏷️[ 标记起始点 ] 🔖 内中断
此处中断的意思是指:CPU不再接着(刚执行完的指令)向下执行,而是转去处理中断信息.
产生中断信息的事件,即中断信息的来源 简称为中断源 , 有着对应的中断类型码,为一个字节型数据 , 即可以表示256种中断信息的来源
<对中断的初见让我认为其和异常类似…实际上不然 , 中断属于异常的一类 REF , ..嗯..计组的内容 >
当CPU收到中断信息后需要对其进行处理 , 用来处理中断信息的程序为中断处理程序 , 一般需要对不同的中断信息编写不同的处理程序 , 要跳转到中断处理程序执行, 则需要得知对应的入口地址(即对应CS:IP, 程序第一条指令的地址)
中断向量表 Interrupt Vector Table
所谓中断向量,就是中断处理程序的入口地址.
则中断向量表 , 就是中断处理程序入口地址的列表 , 其在内存中保存
8086CPU规定内存0000:0000 ~ 0000:03FF的1024个单元中存放中断向量表 , 共1KB
向量表中可存放256个中断源对应的中断程序的入口 , 每个表项占2个字(高地址字存放段地址, 低地址字存放偏移地址) 256×2×2=1024b=1kb
中断处理程序和 iret 指令 (Interrupt Return)
中断处理程序的编写方法和子程序的比较相似,下面是常规的步骤:
保存用到的寄存器
处理中断:
恢复用到的寄存器:
用
iret指令返回。
iret指令的功能用汇编语法描述为:
1 | pop IP |
注意中断处理程序常驻内存 , 应存放在内存的确定位置
对于8086CPU , 由于系统实际要处理的中断事件远没有达到256个 , 如从EA(0200 - 02FF) 这段内存空间是中断向量表的空闲单元 , 可用以存储额外的中断处理程序
中断过程
中断过程由CPU硬件自动完成 : 由中断类型码找中断向量 , 用其设置CS:IP指向 。
设置中断向量
中断向量表表项index , 当完成对中断程序的安装后 , 则需要将中断程序的入口地址写入中断向量表中.
注意过程以字为单位进行 , - 段地址和偏移地址都是16位 , 低地址存放偏移地址 , 高地址存放段地址
Syntax:
1 | mov word ptr es:[index*4],EA |
单步中断 & TF Flag ( Trap Flag )
Ref to WiKi : A trap flag permits operation of a processor in single-step mode. If such a flag is available, debuggers can use it to step through the execution of a computer program.
CPU执行完一条指令后, 若检测到标志寄存器TF位为1 , 则产生单步中断(此中断类型码为1) , 并引发中断过程
CPU提供单步中断功能为单步跟踪程序的执行过程提供了实现机制.
INT instruction
INT is an assembly language instruction for x86 processors that generates a software interrupt.
int指令的格式 为 int n n 为中断类型码. 其功能为 引发中断过程
“ 实际int指令的功能和 call指令类似 , 都是调用一段程序 “
注意cs ip 的先后入栈顺序
端口
端口 可被 CPU 直接读写
对于端口的访问, CPU通过端口地址定位端口
与内存的读写指令( mov \ push \ pop )区分开 , in \ out 为端口读写指令 , 分別用于从端口读取(访问端口)或写入数据
端口地址和内存地址一樣 , 通过总线来传送
评论