Based on Assembly Edition 4
Basic Part
Memory Segmentation in 8086 Microprocessor
存储器 、存储单元
存储器
也就是我们平时所说的内存
。 负责为CPU提供指令和数据 , 在一台PC机中内存的作用仅次于CPU,存储器被划分为若干个存储单元
, 每个存储单元从0开始顺序编号. 一般微型机的存储器的存储单元可以存储一个Byte
,即一个字节(8个二进制位) - 微机存储器容量的最小单位
。
一般应具有 存储数据 和 读写数据 的功能, 每个单元有一个地址,是一个整数 编码 ,可以表示为 二进制 整数。
磁盘不同于内存,磁盘上的数据或程序不读取到内存中就无法被CPU使用
CPU对存储器的读写
存储单元在存储器中顺序编号,这些编号可以看作存储单元在存储器中的地址。
计算机中 连接 CPU 与其他芯片的导线被称作是 "总线"
,
根据传输信息的不同, 其从逻辑上可分为三类 :
地址总线 (CPU通过地址总线来指定存储器单元)
地址总线的宽度
(位数)决定了CPU寻址的最大空间大小,假设一个CPU有10根地址总线,其寻址最大数为2^10 = 1024个
最小数为0,最大数为1023地址总线可寻到的内存单元构成了这个CPU的
内存地址空间
。控制总线
控制总线的宽度决定了CPU对外部器件的控制能力。
CPU通过地址总线来指定存储单元 ,控制总线上能传送信息的多少(控制总线的宽度),决定了CPU可以对多少个存储单元进行寻址。
数据总线
数据总线的宽度决定了
CPU与外界的数据传输速度
, 8根数据总线可以一次传送8位二进制数据 (即1Byte),计算机的
字长
即取决于数据总线的宽度.
内存地址空间
CPU操作存储器(RAM ROM 带有BIOS的ROM)时将他们看作内存来对待,将他们总的看作一个由若干个存储单元组成的逻辑存储器
. 这个逻辑存储器就是所说的内存空间地址.
每一个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间
,对于CPU来说,在这段地址空间中进行数据读写 , 实际也就是对对应的物理存储器进行读写.
因为内存空间地址与存储单元相关,其大小受到CPU地址总线宽度的限制,例如8086CPU的地址总线为20
,则其可以传送2^20
个不同的地址信息(0 ~ 2^20-1)
,也就是可以定位2^20
个内存单元,则其内存地址空间大小为 2^20 / 1024^2
在基于一个计算机硬件进行系统编程时,必须知道这个系统中内存地址的分配情况, 在对某类存储器进行数据读写时,必须知道它的第一个单元的地址和最后一个单元的地址,才能保证读写操作是在预期的存储器中进行的.
寄存器
CPU由运算器 \ 寄存器 \ 控制器 等器件组成,这些器件靠内部的总线相连..
内部总线实现CPU内部各个器件之间的联系外部总线实现CPU和主板上其他器件的联系
字在寄存器中的存储
字(记为word
) , 一个字由2个字节组成 这两个字节分别称为这个字的高位字节
和低位字节
.
一个十六进制数 = 4个二进制的四位
几条汇编指令与8086CPU给出物理地址的方式
在进行数据传送或运算时,要注意两个操作对象的位数应当是一致的. 高低位对应
1 | add ax,bx |
8086CPU
有20位地址总线,可以传送20位地址,寻址能力 2^20(bytes) / 1024(bytes)^2 = 1MB
内部采用2个16位地址合成的方法来形成一个20位的物理地址(一个称为段地址
, 一个称为偏移地址
)
段地址和偏移地址通过内部总线被送入一个称作地址加法器
的部件中,由它将两个16位的地址组合为20位的物理地址
地址加法器通过物理地址 = 段地址 × 16(d) + 偏移地址
的方式合成物理地址
段地址 × 16
常用说法 左移4位
(指二进制位) , 相当于 乘 2^4
关于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 \ DS \ SS \ ES
CS:IP 是8086CPU中最关键的俩个寄存器 , 它们指示了 CPU 当前要读取指令的地址
CS为代码段寄存器 (Code Section)
IP为指令指针寄存器 (Instruction Pointer)
CS * 16 + IP = 物理地址
关于 IP / CS 的修改
能改变IP / CS 内容的指令被统称为转移指令
, 目前可以用一个简单的指令来修改它们 :jmp 指令
指令形如 jmp 段地址:偏移地址
, 将CS修改为段地址, IP修改为偏移地址
或者使用 jmp 合法寄存器
来修改IP
的内容
e.g. jmp ax => mov IP,ax
jmp指令的功能类似于这样子
寄存器与内存访问
DS寄存器(数据段寄存器 , data segment register)
通常用来存储要访问数据的段地址
🚩 CPU提供的栈机制
栈 – 拥有特殊访问方式的存储空间 – LIFO 后进先出
, 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.
程序执行过程与跟踪
程序由Shell
(操作系统的外壳程序,用户使用Shell来操作计算机系统进行工作 (如DOS中的command.com
就是一个Shell) 加载入内存
之后Shell将设置CS:IP
来指向程序的入口点
, 然后暂停运行, 将程序交给CPU执行.
待任务执行完毕后, 控制返回到Shell , 屏幕显示由当前盘符和当前路径组成的提示符,等待用户输入.(返回到加载者)
📌 实验三 - 编程|编译|连接|跟踪 - - 分析
debug 跟踪以下程序执行过程 , 记录每步执行后相关寄存器中内存和栈顶内容
1 | assume cs:codesg |
✨
Base Register 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 |
Debug和汇编编译器MASM对指令的不同处理
若处理以[]
包含的指令时, 型如[idata]
,它们的处理结果如下
Debug 将[idata]
视作一个内存单元, 将内部idata
作为内存单元的偏移地址
编译器将[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~16
总共23(17H)
个字节
包含多个段的程序
在代码段中使用数据: 可以使用dw(define word)
定义字型数据.
利用end的另一个作用:通知编译器程序的入口点在何处
,可以使用Start
标号指明程序入口点
🍊 程序分析 6.3
1 | assume cs:code |
编写多段程序
利用assume
伪指令将定义的段和相关的寄存器联系起来 , 对于不同的段, 要有不同的段名
段名就相当于一个标号,它代表了一个段的段地址
, 编译器会把它处理为一个表示段地址的数值
注意在8086CPU不允许直接将数值传入段寄存器中
, 所以要将定义的段程序赋给特定的寄存器时需要一个寄存器作中转.
CPU如何处理所定义的段的内容, 当作指令执行或是数据访问, 亦或是栈空间… 完全取决于程序中具体的汇编指令 , 及汇编指令中对CS:IP
SS:SP
DS
等寄存器的设置决定
📌 实验五 - 编写与调试多段程序 - - 分析
编译链接并跟踪调试
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
E.G.
1 | mov ax,[bx+200] ; 将一个内存单元存入AX , 这个内存单元占2个字节, 存放一个字,偏移地址为 bx的值 + 200 , 段地址在ds中 |