《汇编语言》- 读书笔记 - 第3章-寄存器(内存访问)
- 3.1 内存中字的存储
- 问题 3.1
- 3.2 DS 和 [address]
- 问题 3.2
- 3.3 字的传送
- 问题 3.3
- 问题 3.4
- 3.4 mov、add、sub 指令
- 3.5 数据段
- 问题 3.5
- 3.1~3.5 小结
- 检测点 3.1
- 3.6 栈
- 3.7 CPU 提供的栈机制
- 问题 3.6
- 3.8 栈顶超界的问题
- 3.9 push、pop 指令
- 问题 3.7
- 问题 3.8
- 问题 3.9
- 问题 3.10
- 栈的综述
- 3.10 栈段
- 问题3.11
- 问题 3.12
- 段的综述
- 检测点 3.2
3.1 内存中字的存储
1字
= 2字节
20000
= 4E20H
在内存中表示如图。从地址 0 开始
读一个字节
,得到20H
读一个字
,得到4E20H
(读取时先读到的是低字节,后读到高字节)
我们将起始地址为 N 的字单元简称为 N 地址字单元。
问题 3.1
对于图 3.1:
- 0 地址
单元
中存放的字节
型数据是多少? 20H - 0 地址
字单元
中存放的字
型数据是多少? 4E20H - 2 地址
单元
中存放的字节
型数据是多少? 12H - 2 地址
字单元
中存放的字
型数据是多少? 0012H - 1 地址
字单元
中存放的字
型数据是多少? 124EH
3.2 DS 和 [address]
CPU必须知道一个内存的单元地址,才能读写它。8086PC中,内存地址
=段地址
+偏移地址
组成。
8086CPU 中有一个 ds
寄存器,通常用来存放要访问数据的段地址。
以下是从10000H
单元读取1字节到al
(如果是ax
就是读一个字了,mov能见风使舵。)
mov bx,1000H
mov ds,bx
mov al,[0]
从这段汇编可以看到:
DS
很害羞不让直接写,需要bx
递给她。mov
能把内存
中的数据送进寄存器
。[]
表示一个内存地址。其中的0
表示内存单元偏移地址。- 省掉
段地址
,默认会用ds
中的值。
问题 3.2
写几条指令,将 al 中的数据送入内存单元 10000H 中,思考后看分析。
为了便于查看,最开始就先把ax的内容改为9527
。
3.3 字的传送
正式明确:只要在 mov
指令中给出 16 位的寄存器就可以进行 16 位数据的传送。
问题 3.3
内存中的情况如图 3.2 所示,写出下面的指令执行后寄存器 ax,bx,cx 中的值。
- 动手练习:
assume cs:codesg
codesg segment
start: ; ax bx cx
mov ax, 1000H ; 1000H
mov ds, ax ;
mov ax, ds:[0] ; 1123H
mov bx, ds:[2] ; 6622H
mov cx, ds:[1] ; 2211H
add bx, ds:[1] ; 8833H
add cx, ds:[2] ; 8833H
codesg ends
end start
这里注意:按书中是直接在Debug
中打的指令,支持[0]。如果我们写成MASM汇编代码编译[0] 会被当值。加上ds
才能正常识别为地址。
问题 3.4
内存中的情况如图 3.3 所示,写出下面的指令执行后内存中的值,思考后看分析
- 动手练习:
assume cs:codesg
codesg segment
start: ; ax bx cx | 00 01 02 03
mov ax, 1000H ; 1000H | 23 11 22 11
mov ds, ax ; |
mov ax, 11316 ; 2C34H |
mov ds:[0], ax ; | 34 2C
mov bx, ds:[0] ; 2C34H |
sub bx, ds:[2] ; 1B12H |
mov ds:[2], bx ; | 12 1B
codesg ends
end start
注意:如果是汇编代码编译运行,记得带上段地址。(Debug中可以直接写偏移)
3.4 mov、add、sub 指令
mov、add、sub 指令都带有两个操作对象,可知 mov 指令可有以下形式:
指令 | 目标 | 来源 | 示例 |
---|---|---|---|
mov | 寄存器 | 数据 | mov ax, 8 |
mov | 寄存器 | 寄存器 | mov ax, bx |
mov | 寄存器 | 内存单元 | mov ax, [0] |
mov | 内存单元 | 寄存器 | mov [0], ax |
mov | 段寄存器 | 寄存器 | mov ds, ax |
推导出: | |||
mov | 寄存器 | 段寄存器 | mov ax, ds |
mov | 内存单元 | 段寄存器 | mov [0], cs |
mov | 段寄存器 | 内存单元 | mov ds, [0] |
add、sub 无法使用段寄存器。(源、目标都不行)
指令 | 目标 | 来源 | 示例 |
---|---|---|---|
add | 寄存器 | 数据 | add ax, 8 |
add | 寄存器 | 寄存器 | add ax, bx |
add | 寄存器 | 内存单元 | add ax, [0] |
add | 内存单元 | 寄存器 | add [0], ax |
sub | 寄存器 | 数据 | sub ax, 9 |
sub | 寄存器 | 寄存器 | sub ax, bx |
sub | 寄存器 | 内存单元 | sub ax, [0] |
sub | 内存单元 | 寄存器 | sub [0], ax |
3.5 数据段
在8086PC机中,可以将这样一组内存单元定义为一个段,用来存储数据:
- 一段连续的地址。
- 起始地址为 16 的倍数。
- 长度小于等于64KB。
访问数据段中的数据需要用ds存放数据段的段地址,再根据需要使用相关指令访问具体单元。
问题 3.5
写几条指令,累加数据段中的前 3 个字型数据,思考后看分析。
mov ax, 123BH
mov ds, ax ;将 123BH 送入 ds 中,作为数据段的段地址
mov ax, 0 ;用 ax 存放累加结果
add ax, [0] ;将数据段第一个字(偏移地址为 0)加到 ax 中
add ax, [2] ;将数据段第二个字(偏移地址为 2)加到 ax 中
add ax, [4] ;将数据段第三个字(偏移地址为 4)加到 ax 中
注意,一个字型数据占两个单元,所以偏移地址是 0、2、4
- 动手练习
3.1~3.5 小结
- 字在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
- 用
mov
指令访问内存单元,可以在mov
指令中只给出单元的偏移地址,此时,段地址默认在DS
寄存器中。 [address]
表示一个偏移地址为address
的内存单元。- 在内存和寄存器之间传送字型数据时,高地址单元和高 8 位寄存器、低地址单元和低 8 位寄存器相对应。
mov
、add
、sub
是具有两个操作对象的指令。jmp
是具有一个操作对象的指令。- 可以根据自己的推测,在
Debug
中实验指令的新格式。
检测点 3.1
《汇编语言》- 读书笔记 - 检测点# 检测点 3.1
3.6 栈
栈是一种数据结构,它的特点是后进选出
。
我们可以把它理解成一个严谨的酸菜坛子,先
放进去的酸菜铺在下面
,后
放进去的铺在上面
。
当我们取出酸菜时,最先拿出来的就是坛子口处的酸菜。(也就是最后放进去的那片)
3.7 CPU 提供的栈机制
8086CPU 提供了PUSH
(入栈)和 POP
(出栈)两个最基本的指令用来以栈
的方式访问内存空间。
push ax
将寄存器 ax
中的数据送入栈
中
pop ax
从栈顶
取出数据送入 ax
。
操作以字
为单位进行。
下面举例说明,我们可以将 10000H~1000FH 这段内存当作栈来使用。
图 3.9 描述了下面一段指令的执行过程。
任意时刻,SS:SP
指向栈顶
元素。
push 指令和 pop 指令执行时,CPU 从 SS 和 SP 中得到栈顶的地址。
-
push ax
的执行步骤:SP=SP-2
,SS:SP 指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;- 将 ax 中的内容送入 SS:SP 指向的内存单元处,SS:SP 此时指向新栈顶。
图 3.10 描述了 8086CPU 对 push 指令的执行过程
-
pop ax
的执行步骤:(执行过程和 push ax 刚好相反)- 将 SS:SP 指向的内存单元处的数据送入 ax 中;
SP=SP+2
,SS:SP 指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。
图 3.12 描述了 8086CPU 对 pop 指令的执行过程
问题 3.6
如果将 10000H~1000FH 这段空间当作栈,初始状态栈是空的,此时,SS=1000H,SP=? 思考后看分析。
分析:空栈时SS:SP的指向。
- 我们以
10000H~1000FH
为栈。当栈中只有一个元素时,如果再次POP
按照规则SP=SP+2
,SS:SP的指向10010H
。 - 反过来推,如果当前是空栈,
PUSH
后SP=SP-2
栈顶元素在1000E
,那么要满足这个结果,空栈时SS:SP的指向10010H
。
3.8 栈顶超界的问题
栈只是我们人为对内存中一个区域的使用规划。CPU只知道SS:SP
指向栈顶,以及PUSH
、POP
。
栈的边界也在我们的脑海里。需要我们自己来控制使用。
当栈满
时继续PUSH
就会超界。
当栈空
时继续POP
也会超界。
超界后就可能会意外的读写到栈外的数据,但是那段内存现在是谁在用,存的什么内容,对于我们当前来说是未知
的(无意义的、随机的)
所以如果我们不小心修改了它,就可能造成其他程序发生错乱。当然我们使用这坨无意义的数据,我们当前的程序通常也会出问题。
3.9 push、pop 指令
指令 | 目标 | 说明 |
---|---|---|
push | 寄存器 | 将一个寄存器 中的数据入栈 |
push | 段寄存器 | 将一个段寄存器 中的数据入栈 |
push | 内存单元 | 将一个内存字单元 处的字入栈(注意: 栈操作都是以字为单位) |
pop | 段寄存器 | 出栈,用一个段寄存器 接收出栈的数据 |
pop | 寄存器 | 出栈,用一个寄存器 接收出栈的数据 |
pop | 内存单元 | 出栈,用一个内存字单元 接收出栈的数据 |
如果只给内存单元的偏移地址
,段地址
默认从 ds
中取。
问题 3.7
编程,将 10000H~1000FH
这段空间当作栈,初始状态栈是空的,将 AX、BX、DS
中的数据入栈。
Debug 的 T 命令在执行修改寄存器 SS 的指令时,下一条指令也紧接着被执行。后面学到了,再回来补传送门。
问题 3.8
编程:
- 将
10000H~1000FH
这段空间当作栈,初始状态栈是空的; - 设置
AX=001AH
,BX=001BH
; - 将 AX、BX 中的数据入栈;
- 然后将 AX、BX 清零;
- 从栈中恢复 AX、BX 原来的内容。
mov ax, 1000
mov ss, ax
mov sp, 0010 ; 初始化栈顶,栈的情况如图3.15(a)
mov ax, 001A
mov bx, 001B
push ax
push bx ; ax、bx入栈,栈的情况如图3.15(b) 所示
sub ax, ax ; 将 ax 清零。也可以用mov ax, 0
; sub ax,ax 的机器码占2字节。
; mov ax,0 的机器码占3字节。
sub bx, bx
pop bx ; 从栈中恢复 ax、bx 原来的数据,记得先进后出
pop ax
pop
时与 push
顺序相反。数据保持原序。
问题 3.9
- 将
10000H~1000FH
这段空间当作栈,初始状态栈是空的; - 设置
AX=001AH
,BX=001BH
; - 利用栈,交换
AX
和BX
中的数据;
只要把 问题3.8 中 两个pop
顺序调换一下就行了。
pop
时与 push
顺序相同。数据的顺序反转。
问题 3.10
如果要在 10000H
处写入字型数据 2266H
,可以用以下的代码完成:
mov ax,1000H
mov ds,ax
mov ax,2266H
mov [0],ax
补全代码
分析,最后两句是将 ax 数据入栈,那前面缺的就是设置栈顶了。
mov ax, 1000
mov ss, ax
mov sp, 2
mov ax, 2266
push ax
栈的综述
- 8086CPU 提供了栈操作机制,方案如下。
在 SS、SP 中存放栈顶的段地址和偏移地址;
提供入栈和出栈指令,它们根据 SS:SP 指示的地址,按照栈的方式访问内存单元。 push
指令的执行步骤: ①SP=SP-2
; ②向SS:SP指向的字单元中送入数据。pop
指令的执行步骤: ①从SS:SP指向的字单元中读取数据; ②SP=SP+2
。- 任意时刻,SS:SP 指向
栈顶
元素。 - 8086CPU 只记录
栈顶
,栈空间的大小
我们要自己管理
。 - 用
栈
来暂存以后需要恢复的寄存器的内容时,寄存器出栈的顺序要和入栈的顺序相反。 - push、pop 实质上是一种内存传送指令,注意它们的灵活应用。
栈是一种非常重要的机制,一定要深入理解,灵活掌握。
3.10 栈段
编程时可以将一组内存单元定义为一个段。
如将一段地址当栈空间来使用:
- 起始地址为16倍数;
- 长度N(N<=64KB)
- 一组连续的地址。
问题3.11
如果将 10000H~1FFFFH
这段空间当作栈段,初始状态栈是空的,此时,SS=1000H
,SP=?
分析:
- 栈中元素是字。(push、pop一次都是两个字节)
- 当只有一个元素时,SP 指向
FFFE
(最后一个元素占FFFE~FFFF
) - 此时再
POP
一次,使栈为空,SP 指向FFFE+2
=0000
所以 SP=0
问题 3.12
一个栈段最大可以设为多少? 为什么?
答:一个栈段最大可以设为64KB
。因为偏移地址取值范围0000~FFFF
。
当栈满时SP=0
,继续PUSH,SP=SP-2 = FFFE
。
段的综述
我们可以将一段内存定义为一个段,用一个
段地址
指示段,用偏移地址
访问段内的单元。这完全是我们自己的安排。
我们可以用一个段存放数据,将它定义为“数据段”;
我们可以用一个段存放代码,将它定义为“代码段”;
我们可以用一个段当作栈,将它定义为“栈段”。
我们可以这样安排,但若要让 CPU 按照我们的安排来访问这些段,就要:
对于数据段
,将它的段地址
放在DS
中,用 mov、add、sub 等访问内存单元的指令时,CPU 就将我们定义的数据段中的内容当作数据来访问;
对于代码段
,将它的段地址放在CS
中,将段中第一条指令的偏移地址
放在IP
中,这样 CPU 就将执行我们定义的代码段中的指令;
对于栈段,将它的段地址放在SS
中,将栈顶单元的偏移地址
放在SP
中,这样 CPU 在需要进行栈操作的时候,比如执行 push、pop 指令等,就将我们定义的栈段当作栈空间来用。
可见,不管我们如何安排,
CPU 将内存中的某段内容当作代码,是因 CS:IP 指向了那里;
CPU 将某段内存当作栈,是因为 SS:SP指向了那里。
我们一定要清楚,什么是我们的安排,以及如何让 CPU 按我们的安排行事。
要非常清楚 CPU 的工作机理,才能在控制 CPU 按照我们的安排运行的时候做到游刀有余。
比如我们将10000H~1001FH
安排为代码段,并在里面存储如下代码:mov ax, 1000H mov ss, ax mov sp, 0020H ; 初始化栈顶 mov ax, cs mov ds, ax ; 设置数据段段地址 mov ax, [0] add ax, [2] mov bx, [4] add bx, [6] push ax push bx pop ax pop bx
设置
CS=1000H
,IP=0
,这段代码将得到执行。可以看到,在这段代码中,我们又将10000H~1001FH
安排为栈段
和数据段
。10000H-1001FH
这段内存,既是代码段,又是栈段和数据段。
一段内存,可以既是代码的存储空间,又是数据的存储空间,还可以是栈空间,也可以什么也不是。关键在于 CPU 中寄存器的设置,即 CS、I,SS、SP,DS 的指向。
检测点 3.2
《汇编语言》- 读书笔记 - 检测点# 检测点 3.2