8086汇编-段寄存器
本帖最后由 啥都不会 于 2022-4-19 00:03 编辑> 本帖最后由 啥都不会 于 2022-4-18 21:49 编辑
> 本帖最后由 啥都不会 于 2022-4-18 21:34 编辑
> 本帖最后由 啥都不会 于 2022-4-18 21:34 编辑
> 本帖最后由 啥都不会 于 2022-4-18 21:32 编辑
# 前言:
这篇笔记主要解释计算机的内存单元、内存地址、段地址、偏移地址和物理地址相关知识点,并对上一篇的 "hello.asm" 代码进行调试分析。
> 这两天发的笔记都是存货,错误会少一些。后面的笔记会写得慢一些,如有问题欢迎大家指正。
# 编程语言:
汇编语言
# 以下为主题内容:
部分笔记参考 "王爽《汇编语言》"
# 存储单元
计算机中的存储器(逻辑上可称为内存)被划分成若干个存储(内存)单元,每个存储(内存)单元从 0 开始顺序编号。每个存储(内存)单元能存储 8bit = 1byte 的数据,因此计算机的最小存储单位是 1byte。一个存储器有128个存储单元,那么它可以存储 128 个 byte。
如下图所示:1字节表示一个内存单元。debug 调试器 d 命令一行显示 16 个字节
!(https://user-images.githubusercontent.com/25861639/160364967-25a25050-3b1f-4001-8240-32a709dac57c.jpg)
一个存储单元中存放的信息称为该存储器单元的内容。
!(https://user-images.githubusercontent.com/25861639/160364986-d8cd81d6-6b8d-4999-a754-d5509d20fb09.jpg)
一个字存放到存储器中要占用连续的两个存储单元。系统规定,当把一个字存放到存储器时,其低字节存放存放到地址较低的存储单元中,其高字节存放在地址较高的字节单元中。这种存储方式称为"高高低低"原则。
!(https://user-images.githubusercontent.com/25861639/160364991-cc4ab11a-0c50-4cc3-bb73-1aab178309f7.jpg)
四个连续的字节存储单元构成了一个双字单元,其存储方式也是使用"高高低低"原则存储。
以下存储的是 12345678
!(https://user-images.githubusercontent.com/25861639/160364994-900b8633-aea8-4660-92cd-a76b899174d3.jpg)
# 物理地址
1. CPU 在访问内存单元的时,要给出内存单元的地址。在内存地址空间中,每个内存单元在空间中都有一个唯一的地址,这个唯一的地址称为物理地址。
2. 要执行一个汇编指令,CPU 就必须要知道这个汇编指令的物理地址才能去执行指令。
3. 不同的 CPU 可以有不同的形成物理地址的方式。
# 总线
上一篇笔记有提到过计算机中有两个总线。相对于 CPU 是而言内部总线是将运算器、控制器、寄存器进行连接传送数据的;而外部总线是将 CPU 和主板上的其他器件进行联系的。总线从物理上来讲就是一根根导线的集合。
总线从逻辑上分为三种:数据总线、地址总线、控制总线。
如下图展示了 CPU 从内存中读取数据的过程
!(https://user-images.githubusercontent.com/25861639/160364951-059afbbe-2d9e-4cc7-8765-743fe3cd5e8f.jpg)
(1) 通过地址总线,形成逻辑上的物理地址,找到 3 号内存单元
(2) 通过数据总线将 3 号内存单元数据传到 CPU
(3) 通过控制总线执行命令,控制 CPU 向 3 号内存单元读取数据
## 地址总线
假设总线中有 10 根地址总线。我们知道一根导线可以传送的稳定状态只有两种,高电平和低电平。用二进制表示就是 0 和 1,10 根导线可以传送 10 位二进制数据,而10位二进制数可以表示 2的10次方个不同的数据,从 0~1024。也就是说这 1024 个地址组合,可以寻找到 1024 个内存单元的数据,大小是一共是1024byte = 1KB
!(https://user-images.githubusercontent.com/25861639/160364956-cf145b8f-f6b4-4936-bc6d-b0ba57726593.jpg)
## 数据总线
CPU 与内存或其他器件之间的数据传送是通过数据总线来进行的。数据总线的宽度决定了 CPU 和外界的数据传送速度。8根数据总线一次可传送一个 8 位二进制数据(即1个字节)。16根数据总线一次可传输2个字节。
8088CPU的数据总线宽度为8,8086CPU 的数据宽度为 16。下图演示这两个CPU是如何写入数据 89D8H 的。
!(https://user-images.githubusercontent.com/25861639/160364960-2c0eca31-58bb-4387-8ee4-2daf1aa9e421.jpg)
!(https://user-images.githubusercontent.com/25861639/160364964-8f584680-11e3-4dfe-93a3-76f163bd1dfc.jpg)
可以看到在硬件传输速率相同的情况下,8086 明显比 8088 读写数据速度更快。
## 控制总线
CPU 对外部器件的控制是通过控制总线来进行的。在这里控制总线是一个总称,控制总线是一些不同控制线的集合。有多少根控制总线,就意味着 CPU 提供了对外部器件的多少种控制。所以,控制总线的宽度决定了 CPU 对外部器件的控制能力。
# 16 位结构的 CPU
8086 是 16 位结构的 CPU,其具有下面几方面的结构特性。
- 运算器一次最多可以处理 16 位的数据
-
- 寄存器的最大宽度为 16 位
- 寄存器和运算器之间的通路为16位
上面的这些特性说明了 8086 内部,能够寻址 2^16 个的内存单元,一共能寻址到 64KB 的内存。能够一次处理、传输、暂时存储的信息最大长度是 16 位(2字节)。
# 段寄存器
前面提到过 8086 CPU 内部的地址总线是十六根,但是当时的 DOS 系统,外部的地址总线却是 20 根。此时出现了一个问题,8086 CPU 内部寻址总线只能寻到 2^16=64KB 的内存,而外部的地址总线 2^20 = 1M 的内存。也就是说,超出 64KB 的内存 8086 CPU 无法利用,那这个问题又该如何解决呢?
## 存储器的分段
为了解决上面的问题,设计人员想到用分段的方式来进行处理。也就是把 64KB 分为一段,我们只要寻 16 个段就可以把 1M 的内存空间都寻到。
!(https://user-images.githubusercontent.com/25861639/160364998-af80e042-366a-4b37-bcbf-929b9d6f3a33.jpg)
> 一个段的最大寻址范围是 64KB
## 物理地址地形成
8086 CPU 只能寻址到 64KB 的内存,对于当时而言 64KB 内存太小,如果程序大一些,计算机就无法运行该程序。为了解决这个问题,设计人员把外部的寻址总线扩展到了 20 根,这样计算机就能寻址到 1M 的内存空间。
为了解决这个问题设计人员想到,用分段的方式来解决这个问题,但这又要该如何实现呢?物理上肯定是无法解决,于是就只能从逻辑上解决这个问题。
如果内部地址总线生成的地址是 1234(0001 0010 0011 0100),外部地址总线生成的地址是 12341(0001 0010 0011 0100 0001)。
生成 20 位地址总线:
1. 1234 左移 4 位 = 12340
2. 12340+0001=12341(0001 0010 0011 0100 0001)
使用逻辑运算,算出 20 位地址总线,这样就能找到准确的地址,进行数据读写。其中 1234 使用段寄存器来存储,而 0001 是人为指定的偏移地址,我们后续只要修改段地址和偏移地址,就能在内存的任何位置进行数据读写。
在 CPU 内部使用器件"地址加法器"计算 20 位地址,如下图所示,展示了 CPU 运行过程
!(https://user-images.githubusercontent.com/25861639/160364971-6e8c872a-3cad-4e52-8285-cb91da71dbf5.jpg)
!(https://user-images.githubusercontent.com/25861639/160364973-25e26e5e-78cc-4e57-ba8e-685ba6526f82.jpg)
如下图所示 debug 调试器 u 命令最左侧显示的是每条汇编指令的逻辑地址
!(https://user-images.githubusercontent.com/25861639/160364943-c6b9518f-bafc-40ce-8b7a-04c230bc6d61.jpg)
> 物理地址就是:076C0、076C3、076C5......
## 段寄存器的引用
8086 CPU 在访问内存时要由相关部件提供内存单元的段地址和偏移地址,送入地址加法器合成物理地址。段地址在 8086CPU 的段寄存器中存放。8086CPU 有 4 个段寄存器:CS(Code Segment,代码段寄存器)、DS(Data Segment,数据段寄存器)、SS(Stack Segment,堆栈段寄存器)、ES(Extra Segment,附加段寄存器)。当 8086 CPU 要访问内存时由这 4 个段寄存器提供内存单元的段地址。
在取指令的时候,自动引用代码段寄存器CS,再加上IP所给出的16位偏移,得到要取指令的物理地址。
!(https://i.loli.net/2020/06/25/t3mAyXwDhxVb7jp.png)
1. 8086 CPU 当前状态:CS 中的内容为 2000H,IP 中的内容为 0000H;
2. 内存 20000H~20009H 单元存放着可执行的机器代码;
3. 内存 20000H~20009H 单元中存放的机器码对应的汇编指令如下。
地址:20000H~20002H,内容:B8 23 01,长度:3Byte,对应汇编指令:mov ax,0123H
地址:20003H~20005H,内容:BB 03 00,长度:3Byte,对应汇编指令:mov bx,0003H
地址: 20006H~20007H,内容:89 D8,长度:2Byte,对应的汇编指令:mov ax,bx
地址:20008H~20009H,内容:01 D8,长度:2Byte,对应汇编指令:add ax,bx
> 如果该指令中有数据,那么该指令的长度为 3 Byte;如果没有,那么指令的长度位 2 Byte
下面一组图,以上图为初始状态,展示了 8086CPU 读取、执行一条指令的过程。注意每幅图中发生的变化(下面对 8086 CPU 的描述,是在逻辑结构、宏观过程的层面上进行的,目的是使读者对 CPU 工作原理有一个清晰、直观的认识,为汇编语言学习打下基础。其中隐蔽了 CPU 的物理结构以及具体的工作细节)。
!(https://i.loli.net/2020/06/26/RnupHroIYwJ6gOL.png)
!(https://i.loli.net/2020/06/26/qneTcoL89SsFWdw.png)
!(https://i.loli.net/2020/06/26/2jI4ymANlv59bpg.png)
!(https://i.loli.net/2020/06/26/BIQVhCRpwy7foir.png)
!(https://i.loli.net/2020/06/26/GbhS6rIdj4z2kTp.png)
!(https://i.loli.net/2020/06/26/um9o7DdLnH1lzfF.png)
!(https://i.loli.net/2020/06/26/5uPDpGhoCbBWQFM.png)
!(https://i.loli.net/2020/06/26/azdjJhl4iBMg9T7.png)
!(https://i.loli.net/2020/06/26/Kg6xekDi9yBOpJY.png)
!(https://i.loli.net/2020/06/26/tDdAcu9J1Xj3EQ2.png)
!(https://i.loli.net/2020/06/26/L9etXR3QCIwyz2k.png)
!(https://i.loli.net/2020/06/26/VvPrkULTDgGOy1Z.png)
!(https://i.loli.net/2020/06/26/ZCjMBUvGYK2mw3t.png)
!(https://i.loli.net/2020/06/26/kGyA8zRowZJqSaj.png)
!(https://i.loli.net/2020/06/26/gCWBUEwLODevbRh.png)
!(https://i.loli.net/2020/06/26/8HZ59iDTgWANtUM.png)
通过上面的过程展示,8086 CPU 的工作过程可以简要描述如下。
1. 从 CS:IP 指向内存单元读取指令,读取的指令进入指令的缓冲器;
2. IP=IP+所读取的指令长度,从而指向下一条指令;
3. 执行指令。转到步骤 1,重复这个过程。
演示:
!(https://user-images.githubusercontent.com/25861639/160364979-a1f1d52a-ae14-459b-8a5b-3b707b61e1d7.jpg)
当涉及堆栈操作的时,则自动引用堆栈段寄存器 SS,再加上 SP 所给出的16位偏移,得到堆栈操作所需的物理地址。当偏移涉及到BP寄存器时,省略引用的段寄存器也是堆栈段寄存器SS。
再存取一个普通存储器操作数时,则自动选择数据段寄存器 DS 或附加段寄存器 ES,再加上16位偏移,得到存储器操作数的物理地址。
!(https://user-images.githubusercontent.com/25861639/160365001-ec6394fb-b867-47a0-8543-6f9653f33f56.jpg)
# hello.exe 编写
1. 汇编代码编写
```asm
;程序名:hello.asm
;功能:显示一个字符串--调用 DOS 功能函数
;===========================================
assume cs:code,ds:data;,ss:stack
;assume 假设,cs(代码段)地址标号为code,ds(数据段)地址标号为data,ss(栈段)地址标号为stack。整行代码可以理解为时为了把段寄存器起一个别名,方便后续定义各个段。
;编译器会默认分配一个栈段供程序使用,一般不需要额外定义栈段
;stack segment ;开辟 16 个字节的栈段,并填充为0
;db 16 dup(0)
;stack ends
;定义数据段
data segment
string db 'hello welcome to asm!','$' ;在数据段偏移地址0处,定义字符串,在8086汇编中以 "$" 表示结束(也可把 $ 替换为 24h)。
data ends
code segment
start:
;mov ax,stack ;编译器会自动分配段值,编译完成后 stack 变成数值
;mov ss,ax ;段寄存器传值只允许使用数据寄存器
;mov sp,16
mov bx,data
mov ds,bx
mov si,0
mov ax,word ptr ds: ;word 取一个字,也就是 2 个字节 'he'
mov dx,offset string
mov ah,9 ;21号中断9号功能:显示字符串
int 21h ;21号中断
;mov ax,4c00h ;21号中断4ch号功能:终止程序
mov al,0 ;这里可以理解为返回值 return 0
mov ah,4ch
int 21h
code ends
end start ;告诉编译器代码的起始位置在 start
```
2. 编译
!(https://user-images.githubusercontent.com/25861639/161715947-d5e7d62a-6801-49eb-bb97-da727488a4eb.jpg)
3. 链接
!(https://user-images.githubusercontent.com/25861639/161715988-013e74b7-d3dc-4b1b-87e8-ae5e5159eccb.jpg)
4. 执行
!(https://user-images.githubusercontent.com/25861639/161716045-7d08b4b8-e33b-445c-abe8-4a62835db402.jpg)
> 编译链接可不写后缀名
!(https://user-images.githubusercontent.com/25861639/161716073-e58de0b8-eef0-46fd-bb4f-687a3175c74d.jpg)
## hello.asm 探究
1. assume 把段寄存器和段名关联
!(https://user-images.githubusercontent.com/25861639/161751899-13fc0022-092c-48de-b5a9-fda33ab8d15a.jpg)
2. 程序运行时会给相应段随机分配一个值
在之前的调试发现 debug 模式下数据段的值一直是 "076A",这里做个验证ds段是否是随机分配的
!(https://user-images.githubusercontent.com/25861639/161753266-c0b490fa-8a6c-43ab-8b60-313067f01428.jpg)
段值确实是随机分配的。同时发现 debug 为了方便调试会把一些寄存器初始化,所以写程序时最好把要用的寄存器初始化避免出现调试的时候结果是正确的,运行程序时结果是错误的。
!(https://user-images.githubusercontent.com/25861639/161753277-a37dfc7c-6fad-460f-a1fd-3fecfce91e45.jpg)
3. 栈段可由程序原分配或系统自动分配
删除栈段进行验证
!(https://user-images.githubusercontent.com/25861639/161755249-a24becf9-6c47-4750-a661-0f1ce55ca675.jpg)
编译执行成功
!(https://user-images.githubusercontent.com/25861639/161755482-952226ba-a8d7-4e56-91c3-ae39ba8b63c2.jpg)
!(https://user-images.githubusercontent.com/25861639/161755791-ed9ac423-da13-4af8-a9bb-0c111dca8433.jpg)
4. 代码段
```start:.....end start```告诉编译器代码从哪里执行
!(https://user-images.githubusercontent.com/25861639/161757885-2059e734-2650-490a-bd99-4aeae025311a.jpg)
既然代码的真正执行位置在start处,那我的数据和 assume 是否放在代码段中 start 上面。下方实验成功
!(https://user-images.githubusercontent.com/25861639/161759147-5a0ed5e6-f946-46a9-8d58-251cb2e7703d.jpg)
5. 逻辑地址和物理地址
还原代码重新编译调试,查看代码段地址时从 076C:0000 开始

单步执行两次,定位到当前数据段,发现相同的代码在内存中也有,而且是从 076A:0020 处开始的。

为什么会有同样的命令会出现在 076C 和 076A 两个段中?
> 答:076A0+0020(逻辑地址) = 076C0(物理地址)= 076C:0000(逻辑地址)
>
> 总结:不同的逻辑地址(段地址:偏移地址)可指向同一个物理地址。
谢谢分享 这个不错谢谢,看一下 感谢楼主 感谢大佬的分享 刚好需要 谢谢大佬 不知道来晚了没有 感谢楼主 感谢楼主 前来向大佬学习