2. 访问寄存器和内存
文章目录
- 2. 访问寄存器和内存
- 2.0 导学
- 2.1 寄存器及数据存储
- 2.2 mov和add指令
- 2.3 确定物理地址的方法
- 2.4 内存的分段表示法
- 2.5 Debug的使用
- 2.6 【代码段】CS、IP与代码段
- 2.7 【代码段】jmp指令
- 2.8 【数据段】内存中字的存储
- 2.9 【数据段】用DS和[address]实现字的传送
- 2.10 【数据段】DS与数据段
- 2.11 【栈段】栈及栈操作的实现
- 2.12 关于“段”的总结
- 参考视频:烟台大学贺利坚老师的网课《汇编语言程序设计系列专题》,或者是B站《汇编语言程序设计 贺利坚主讲》,大家一起看比较热闹。
- 中文教材:《汇编语言-第3版-王爽》(课程使用)、《汇编语言-第4版-王爽》(最新版)。
- 老师的博客:《迂者-贺利坚的专栏-汇编语言》
- 检测点答案参考:《汇编语言》- 读书笔记 - 各章检测点归档
本篇笔记对应课程第二章(下图倾斜),章节划分和教材对应关系如下。
2.0 导学
本章针对寄存器和内存的使用,开展如下学习内容:
【2.1 寄存器及数据存储】认识两者之间的关系。
【2.2 mov和add指令】通过这两个指令看到在运算中如何使用“寄存器”。
【2.3 确定物理地址的方法】CPU运行过程中,和内存的关系非常密切,所以介绍如何确定“物理地址”。
【2.4 内存的分段表示法】上面会分段使用内存,所以介绍“分段表示法”。
【2.5 Debug的使用】后续会非常频繁的使用Debug工具观察计算机内部状态。下面使用Debug工具观察内存的不同分段。
- 代码段:【2.6 CS、IP与代码段】【2.7 jmp指令】
- 数据段:【2.8 内存中字的存储】 【2.9 用DS和[address]实现字的传送】【2.10 DS与数据段】
- 栈段: 【2.11 栈及栈操作的实现】
【2.12 关于“段”的总结】
2.1 寄存器及数据存储
在CPU中,“运算器”进行信息处理、“寄存器”进行信息存储、“控制器”协调各种器件进行工作、“内部总线”实现CPU内各个器件之间的联系。8086CPU所有的寄存器都是16位的,可以存放两个字节,8086CPU有14个寄存器(如上图标红):
- 通用寄存器:AX(AH/AL)、BX(BH/BL)、CX(CH/CL)、DX(DH/D L)
- 变址寄存器:SI、DI
- 指针寄存器:SP、BP
- 指令指针寄存器:IP
- 段寄存器:CS、SS、DS、ES
- 标志寄存器:PSW
而8086上一代CPU中的寄存器都是8位的,为了保证8086与上一代CPU程序兼容,“通用寄存器”均可以分为两个独立的8位寄存器使用。比如下图中,AX可以分为AH、AL:
注意“字长(word size)”=“CPU的寄存器位宽”。因为8086是16位CPU,所以8086的字长为16bit。一个字(word)的高位字节(byte)存在这个寄存器的高8位寄存器、低位字节(byte)存在这个寄存器的低8位寄存器。但实际上,现代的CPU普遍是64位,此时“字长”就是64。
2.2 mov和add指令
汇编指令不区分大小写。上图给出了mov
和add
指令所完成的操作。现在假设 ax、bx 的初始值均为 0000H,那么下面两个程序分别展示了汇编指令程序的执行结果:
注意最后一步,左侧程序发生了溢出,最高位直接溢出。右侧程序add al,93H
会直接舍弃低8位的进位,AX结果为 0058H;只有add ax,93H
才会将低8位的进位保存到高8位中,AX结果为 0158H。
【检测点2.1】
- 写出每条汇编指令执行后相关寄存器中的值。初始值为0000H。
汇编指令 寄存器的值 mov ax,62627
AX= F4A3H
mov ah,31H
AX= 31A3H
mov al,23H
AX= 3123H
add ax,ax
AX= 6246H
mov bx,826CH
AX=6246H, BX= 826CH
mov cx,ax
AX=6246H, BX=826CH, CX= 6246H
mov ax,bx
AX= 826CH
, BX=826CH, CX=6246Hadd ax,bx
AX= 04D8H
, BX=826CH, CX=6246Hmov al,bh
AX= 0482H
, BX=826CH, CX=6246Hmov ah,bl
AX= 6C82H
, BX=826CH, CX=6246Hadd ah,ah
AX= D882H
, BX=826CH, CX=6246Hadd al,6
AX= D888H
, BX=826CH, CX=6246Hadd al,al
AX= D810H
, BX=826CH, CX=6246Hmov ax,cx
AX= 6246H
, BX=826CH, CX=6246H
- 最多使用4条
add
或mov
指令,编程计算2的4次方。mov ax,0002H # 2 add ax,ax # 2+2=4 add ax,ax # 4+4=8 add ax,ax # 8+8=16
2.3 确定物理地址的方法
CPU访问内存单元时要给出内存单元的地址。所有的内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间中都有唯一的地址,这个唯一的地址称为物理地址。但实际上,8086有20位地址总线,可传送20位地址,寻址能力为1M。但8086是16位结构的CPU,所以8086CPU使用两个16位地址(段地址、偏移地址)合成一个20位的物理地址:
物理地址 = 段地址 × 16 + 偏移地址 \bold{物理地址=段地址×16+偏移地址} 物理地址=段地址×16+偏移地址
- 物理地址:20位。
- 段地址:16位,相当于一个基础地址。“段地址×16”也就是“左移4位”。
- 偏移地址:16位,也就是一个相对于基础地址的偏移地址。
注意,“段地址”并不是固定的,段地址和物理地址可以任意指定,只要最后的物理地址正确即可。
2.4 内存的分段表示法
虽然8086使用分段的方式管理内存,但内存是一个完整空间并没有分段,段的划分来自于CPU!“段地址×16”被称为“段的起始地址”,显然“起始地址”一定是16的倍数;而“偏移地址”为16位,所以一个段的长度最大为 216=64K。同一段内存,多种分段方案,于是同一个物理地址也可以由不同的段地址和偏移地址组成,如下图所示:
段地址:用专门的寄存器存放段地址。下面是4个段寄存器:
- CS - 代码段寄存器
- DS - 数据段寄存器
- SS - 栈段寄存器
- ES - 附加段寄存器
偏移地址:可以用多种方法提供,也就是8086丰富的取址方式,也是汇编语言的重点。
最后补充一点,在8086PC机中,若数据存在内存的 2000H 段中的 1F60H 单元中,则存储单元地址表示为 2000:1F60。也就是 段地址:偏移地址
的形式。
【检测点2.2】
- 给定段地址为0001H,仅通过变化偏移地址寻址,CPU 的寻址范围为
00010H
到1000FH
。- 有一数据存放在内存 20000H 单元中,现给定段地址为SA,若想用偏移地址寻到此单元。则 SA 应满足的条件是:最小为
1001H
,最大为2000H
。
提示,反过来思考一下,当段地址给定为多少,CPU无论怎么变化偏移地址都无法寻到20000H单元?
2.5 Debug的使用
Debug是DOS系统中的著名的调试程序,也可以运行在windows系统实模式下。使用Debug程序,可以查看CPU各种寄存器中的内容、内存的情况,并且在机器指令级跟踪程序的运行!Debug就是传奇!Debug一共有20多个命令,但下面这6个命令是和汇编学习密切相关的:
- R命令:查看、改变CPU寄存器的内容
- D命令:查看内存中的内容
- E命令:改变内存中的内容
- U命令:将内存中的机器指令翻译成汇编指令
- A命令:以汇编指令的格式在内存中写入机器指令
- T命令:执行机器指令
- Q命令:退出debug
- …
下面依次来进行演示:
【启动Debug】在DOS提示符下输入命令:
debug
(也就是启动了masm文件夹中的Debug.exe)
【用R命令查看、改变CPU寄存器的内容】
R
:查看寄存器内容R 寄存器名
:改变指定寄存器内容。可以没有中间的空格。
- 左下角
0000:0000
:其实就是CS:IP
,表示CPU当前要读取的指令所在的内存地址。- 左下角
0000
:CS:IP
内存地址中存放的机器码。- 下中间
ADD [ES+SI],AL
:CS:IP
内存地址中存放的指令的含义。- 右下角
DS:0000=CD
:刚才改变的寄存器值。后面还会介绍。
【用D命令查看内存中的内容】
D
:列出从预设地址(CS:IP
)开始,128个字节的内容。后续再调用D
会接着上一次的地址。右侧是ASCII码表示对应的字符。D 段地址:偏移地址
:列出指定地址开始,128个字节的内容。D 段地址:偏移地址 结尾偏移地址
:列出内存中指定地址范围内的内容,最多显示 216 个数据。
【用E命令改变内存中的内容】
E 段地址:偏移地址 数据1 数据2 ...
:直接连续修改数据。E 段地址:偏移地址
:逐个询问式修改,空格
表示继续修改;回车
表示结束修改。
汇编指令 | 对应的机器码 |
---|---|
mov ax,0123H | B8 23 01 |
mov bx,0003H | BB 03 00 |
mov ax,bx | 89 D8 |
add ax,bx | 01 D8 |
【用U命令将内存中的机器指令翻译成汇编指令】
e 段地址:偏移地址 数据
:写入指令对应的机器码。d 段地址:偏移地址
:查看写入的机器码。u 段地址:偏移地址
:将相应内存中的内容看作是“指令”,并进行翻译。
【用A命令以汇编指令的格式在内存中写入机器指令】
a 段地址:偏移地址
:从指定地址处写入汇编指令。编写汇编程序时常用。d 段地址:偏移地址
:查看从指定地址开始的二进制机器码。u 段地址:偏移地址
:翻译从指定地址开始的二进制机器码,会根据汇编指令的不同自动切割字节。
【用T命令执行机器指令】
t
:从CS:IP
处,逐条执行机器指令。
【用Q命令退出Debug】
q
:退出Debug
2.6 【代码段】CS、IP与代码段
CS为代码段寄存器,IP 为指令指针寄存器,它们是8086CPU中两个最关键的寄存器。从名称上我们可以看出它们和指令的关系。CS:IP
指示了CPU当前要读取指令的地址。8086CPU读取指令时,会通过“地址加法器”将要读取的指令地址传输到数据总线,内存会一次性发送完整的一条指令给CPU执行(指令的操作码包含指令长度信息),此时 IP 自动增加当前指令长度,然后继续读取下一指令并执行。如下图:
【8086CPU 当前状态】
CS:IP
为2000H:0000H
,内存 20000H~20009H 单元存放着可执行的如下机器码:
- 地址 20000H~20002H,长度 3Byte,内容 B8 23 01,对应汇编指令
mov ax,0123H
- 地址 20003H~20005H,长度 3Byte,内容 BB 03 00,对应汇编指令
mov bx,0003H
- 地址 20006H~20007H,长度 2Byte,内容 89 D8,对应汇编指令
mov ax,bx
- 地址 20008H~20009H,长度 2Byte,内容 01 D8,对应汇编指令
add ax,bx
笔者注:
CS:IP
就相当于“计算机组成原理”中的PC
(计数器)。
注意到,机器码存储在内存中,既可以是数据、也可以是内存。但是CPU会将 CS:IP
指向的内存单元中的内容默认看作指令!
【实证演示】
- 写入汇编指令。
- 查看汇编指令。
- 执行代码。
2.7 【代码段】jmp指令
在实际的程序运行过程中,经常会出现程序的跳转,比如程序进入中断时需要跳转到“中断向量入口地址”。上一小节又讲到 CS:IP
就是当前指令地址,于是要实现程序跳转本质上就需要修改 CS:IP
,有如下方法:
- debug的
R命令
:上一小节用到的方法,但debug只是一个调试工具,而非汇编程序本身!mov
指令:mov CS,2000
、mov IP,0000
,但是8086CPU不支持使用mov
指令修改CS和IP。jmp
指令:转移指令,专门用于程序跳转!
jmp 段地址:偏移地址
:将CS
修改为“段地址”,IP
修改为“偏移地址”。jmp 寄存器名
:只是将IP
修改为寄存器中的值,比如jmp ax
相当于mov IP,ax
(实际并不能这样使用mov
)。
下面通过一个演示,来验证 jmp
指令的作用:
【实证演示】
假设在下图所示的内存中存放相应的指令,并且程序从 2000H 执行,那么执行序列最后会进入死循环。
此时,我们可以规定某个“段地址”就对应一个“代码段”,这个“代码段”的最大长度为 216=64K 个字节。但注意CPU本身并不会有这样的区分,只是这样规定可以使得代码的存放位置更加清晰。
【检测点2.3】
下面的3条指令执行后,CPU几次修改IP?都是在什么时候?最后IP中的值是多少?mov ax, bx sub ax, ax jmp ax
答:IP 总共改了4次:读取每条指令后IP修改一次;执行
jmp ax
再修改一次。最后 IP 中的值是 0 (因为sub ax,ax
后 ax=0000H)。
2.8 【数据段】内存中字的存储
8086CPU中,一个“字”为16位,于是需要两个连续的字节单元存放,被称为“字单元”。高8位放高字节,低8位放低字节,也就是计组中的“小端模式”。于是,一个“单元”存放一个“字节型数据”、一个“字单元”存放一个“字型数据”。下图中,前两种都是小端模式,但是将高地址写在上面方便阅读(中间图):
2.9 【数据段】用DS和[address]实现字的传送
8086CPU中的内存单元地址都是 段地址:偏移地址
的格式,CPU寄存器 CS:IP
表示当前的指令地址,于是 DS:[...]
则表示想读取的数据(字单元)的地址。另外,8086CPU不支持将数据直接送入段寄存器(硬件设计的问题),所以段地址写入顺序固定为:数据 -> 一般的寄存器 -> 段寄存器。如下:
DS
寄存器:存放要访问的数据的段地址。[...]
:直接给出16位的偏移地址(16进制),而不是特定的寄存器。【示例1】将10000H(1000:0)中的数据读到al中
mov bx,1000H mov ds,bx mov al,[0]
【示例2】将al中的数据写到10000H(1000:0)中
mov bx,1000H mov ds,bx mov [0],al
【实证演示1】按照下图调整数据和内存,注意不同偏移地址的 读出数据。
【实证演示2】注意观察写入数据时,也是小端模式。
2.10 【数据段】DS与数据段
和“代码段”类似,根据数据的内存单元地址为 DS:[偏移地址]
,我们也可以利用 数据段寄存器DS 将一组内存单元定义为一个“数据段”,而具体的数据单元则由 [偏移地址]
给出。同样,“数据段”也是编程时的人为安排,与8086CPU或者内存的物理结构无关。下面是一个利用“数据段”进行编程的示例:
【程序1】累加数据段中的前3个单元中的数据:使用寄存器 ax 的低8位 al。
mov ax, 123BH mov ds, ax mov al, 0 add al, [0] add al, [1] add al, [2]
【程序2】累加数据段中的前3个字型数据:使用完整的16位寄存器 ax。
mov ax, 123BH mov ds, ax mov ax, 0 add ax, [0] add ax, [2] add ax, [4]
下面针对 mov
、add
、sub
这三个指令,给出其支持的指令格式。其他没有提到的指令格式,可以在debug中尝试。
类型 | mov指令形式 | 例示 | add指令形式 | 例示 | sub指令形式 | 例示 |
---|---|---|---|---|---|---|
寄存器 | mov 寄存器,数据 | mov ax, 8 | add 寄存器,数据 | add ax, 8 | sub 寄存器,数据 | sub ax, 8 |
mov 寄存器,寄存器 | mov ax, bx | add 寄存器,寄存器 | add ax, bx | sub 寄存器,寄存器 | sub ax, bx | |
mov 寄存器,内存单元 | mov ax, [0] | add 寄存器,内存单元 | add ax, [0] | sub 寄存器,内存单元 | sub ax, [0] | |
mov 寄存器,段寄存器 | mov ax, ds | add 寄存器,段寄存器 | 不支持 | sub 寄存器,段寄存器 | 不支持 | |
内存单元 | mov 内存单元,数据 | 不支持 | add 内存单元,数据 | 不支持 | sub 内存单元,数据 | 不支持 |
mov 内存单元,寄存器 | mov [0], ax | add 内存单元,寄存器 | add [0], ax | sub 内存单元,寄存器 | sub [0], ax | |
mov 内存单元,内存单元 | 不支持 | add 内存单元,内存单元 | 不支持 | sub 内存单元,内存单元 | 不支持 | |
mov 内存单元,段寄存器 | mov [0], ds | add 内存单元,段寄存器 | 不支持 | sub 内存单元,段寄存器 | 不支持 | |
段寄存器 | mov 段寄存器,数据 | 不支持 | add 段寄存器,数据 | 不支持 | sub 段寄存器,数据 | 不支持 |
mov 段寄存器,寄存器 | mov ds, ax | add 段寄存器,寄存器 | 不支持 | sub 段寄存器,寄存器 | 不支持 | |
mov 段寄存器,内存单元 | mov ds, [0] | add 段寄存器,内存单元 | 不支持 | sub 段寄存器,内存单元 | 不支持 | |
mov 段寄存器,段寄存器 | 不支持 | add 段寄存器,段寄存器 | 不支持 | sub 段寄存器,段寄存器 | 不支持 |
【数据段-小结】
- 字在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放再高地址单元中。
- 用
mov
指令要访问内存单元,可以在mov
指令中只给出单元的偏移地址,此时,段地址默认在DS寄存器中。[address]
表示一个偏移地址为address的内存单元。- 在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。
mov
、add
、sub
是具有两个操作对象的指令,访问内存中的数据段。jmp
是具有一个操作对象的指令,对应内存中的代码段。- 可以根据自己的推测,在Debug中实验指令的新格式。
2.11 【栈段】栈及栈操作的实现
“栈”是一种只能在一端进行插入或删除操作的数据结构,符合LIFO(Last In First Out,后进先出)的操作规则。现今的CPU中都有栈的设计,也就是8086CPU提供相关的栈指令,支持用栈的方式访问内存空间,并且寄存器 SS:SP
始终指向栈顶元素的地址。下面是 SS:SP
以及 栈操作指令:
- 栈段寄存器SS:存放栈顶的段地址,同时也默认为最小的栈顶地址。
- 栈顶指针寄存器SP:存放栈顶的偏移地址,初始值为栈的大小(可以理解为栈底)。
push ax
:将ax中的数据送入栈中,SP
自动减2(以字为单位)。pop ax
:从栈顶取出数据送入ax,SP
自动加2(以字为单位)。
【实例演示】使用
push
和pop
交换ax和bx中的内容。
- 初始栈顶始终不存储任何有效元素。
- 第一个元素存储在初始栈顶指针SP的上一个字。
若不断的调用 push
或者pop
指令,就会导致 SP 指针超出栈的范围,也就是“栈顶超界问题”。遗憾的是,8086CPU只知道栈顶在何处(SS:SP
),却不知道程序安排的栈空间有多大,8086CPU不保证对栈的操作不会超界。为了防止超界,汇编语言程序员需要格外小心。
2.12 关于“段”的总结
由于内存地址为 段地址:偏移地址
,所以每一种“段”的起始地址都是16的倍数。目前我们学习了三种段:
- 数据段:将段地址放在 DS 中。用mov、add、sub等访问内存单元的指令时,CPU将我们定义的数据段中的内容当作数据段来访问。
- 代码段:将段地址放在 CS 中,将段中第一条指令的偏移地址放在 IP 中。CPU将执行我们定义的代码段中的指令。
- 栈段:将段地址放在 SS 中,将栈顶单元的偏移地址放在 SP 中。CPU在需要进行栈操作(push、pop)时,就将我们定义的栈段当作栈空间来用。
实际在编写汇编语言程序时,为了逻辑清晰,可以对不同的段进行分区,可以将三个段全部分开或者放在一起。但注意“段”的划分只取决于汇编语言程序员,与CPU设计无关。