汇编语言程序设计-2-访问寄存器和内存

2. 访问寄存器和内存

本篇笔记对应课程第二章(下图倾斜),章节划分和教材对应关系如下。


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 寄存器及数据存储

图2-1 8086CPU的寄存器示意图

  在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:

图2-2 通用寄存器拆分——AX

  注意“字长(word size)”=“CPU的寄存器位宽”。因为8086是16位CPU,所以8086的字长为16bit。一个字(word)的高位字节(byte)存在这个寄存器的高8位寄存器、低位字节(byte)存在这个寄存器的低8位寄存器。但实际上,现代的CPU普遍是64位,此时“字长”就是64。

2.2 mov和add指令

图2-3 mov和add指令——默认十进制

  汇编指令不区分大小写。上图给出了movadd指令所完成的操作。现在假设 ax、bx 的初始值均为 0000H,那么下面两个程序分别展示了汇编指令程序的执行结果:

注意最后一步,左侧程序发生了溢出,最高位直接溢出。右侧程序add al,93H会直接舍弃低8位的进位,AX结果为 0058H;只有add ax,93H才会将低8位的进位保存到高8位中,AX结果为 0158H

【检测点2.1】

  1. 写出每条汇编指令执行后相关寄存器中的值。初始值为0000H。
汇编指令寄存器的值
mov ax,62627AX=F4A3H
mov ah,31H AX=31A3H
mov al,23H AX=3123H
add ax,ax AX=6246H
mov bx,826CHAX=6246H, BX=826CH
mov cx,ax AX=6246H, BX=826CH, CX=6246H
mov ax,bx AX=826CH, BX=826CH, CX=6246H
add ax,bx AX=04D8H, BX=826CH, CX=6246H
mov al,bh AX=0482H, BX=826CH, CX=6246H
mov ah,bl AX=6C82H, BX=826CH, CX=6246H
add ah,ah AX=D882H, BX=826CH, CX=6246H
add al,6 AX=D888H, BX=826CH, CX=6246H
add al,al AX=D810H, BX=826CH, CX=6246H
mov ax,cx AX=6246H, BX=826CH, CX=6246H
  1. 最多使用4条addmov指令,编程计算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 物理地址的生成方式、地址加法器的工作过程

注意,“段地址”并不是固定的,段地址和物理地址可以任意指定,只要最后的物理地址正确即可。

2.4 内存的分段表示法

  虽然8086使用分段的方式管理内存,但内存是一个完整空间并没有分段,段的划分来自于CPU!“段地址×16”被称为“段的起始地址”,显然“起始地址”一定是16的倍数;而“偏移地址”为16位,所以一个段的长度最大为 216=64K。同一段内存,多种分段方案,于是同一个物理地址也可以由不同的段地址和偏移地址组成,如下图所示:

图2-5 多种分段方案

段地址:用专门的寄存器存放段地址。下面是4个段寄存器:

  • CS - 代码段寄存器
  • DS - 数据段寄存器
  • SS - 栈段寄存器
  • ES - 附加段寄存器

偏移地址:可以用多种方法提供,也就是8086丰富的取址方式,也是汇编语言的重点。

最后补充一点,在8086PC机中,若数据存在内存的 2000H 段中的 1F60H 单元中,则存储单元地址表示为 2000:1F60。也就是 段地址:偏移地址的形式。

【检测点2.2】

  1. 给定段地址为0001H,仅通过变化偏移地址寻址,CPU 的寻址范围为 00010H 1000FH
  2. 有一数据存放在内存 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当前要读取的指令所在的内存地址。
  • 左下角0000CS:IP内存地址中存放的机器码。
  • 下中间ADD [ES+SI],ALCS:IP内存地址中存放的指令的含义。
  • 右下角DS:0000=CD:刚才改变的寄存器值。后面还会介绍。

用D命令查看内存中的内容

  • D:列出从预设地址(CS:IP)开始,128个字节的内容。后续再调用 D会接着上一次的地址。右侧是ASCII码表示对应的字符。
  • D 段地址:偏移地址:列出指定地址开始,128个字节的内容。
  • D 段地址:偏移地址 结尾偏移地址:列出内存中指定地址范围内的内容,最多显示 216 个数据。

用E命令改变内存中的内容

  • E 段地址:偏移地址 数据1 数据2 ...:直接连续修改数据。
  • E 段地址:偏移地址:逐个询问式修改,空格表示继续修改;回车表示结束修改。
汇编指令对应的机器码
mov ax,0123HB8 23 01
mov bx,0003HBB 03 00
mov ax,bx89 D8
add ax,bx01 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 自动增加当前指令长度,然后继续读取下一指令并执行。如下图:

图2-6 8086CPU读取和执行指令的相关部件

【8086CPU 当前状态】
CS:IP2000H: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指向的内存单元中的内容默认看作指令!

【实证演示】

  1. 写入汇编指令。
  2. 查看汇编指令。
  3. 执行代码。

2.7 【代码段】jmp指令

  在实际的程序运行过程中,经常会出现程序的跳转,比如程序进入中断时需要跳转到“中断向量入口地址”。上一小节又讲到 CS:IP就是当前指令地址,于是要实现程序跳转本质上就需要修改 CS:IP,有如下方法:

  1. debug的R命令:上一小节用到的方法,但debug只是一个调试工具,而非汇编程序本身!
  2. mov指令:mov CS,2000mov IP,0000,但是8086CPU不支持使用mov指令修改CS和IP。
  3. 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-7 内存中字的存储-小端模式

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]

下面针对 movaddsub这三个指令,给出其支持的指令格式。其他没有提到的指令格式,可以在debug中尝试。

表2-1 mov、add、sub 的指令格式
类型mov指令形式例示add指令形式例示sub指令形式例示
寄存器mov 寄存器,数据mov ax, 8add 寄存器,数据add ax, 8sub 寄存器,数据sub ax, 8
mov 寄存器,寄存器mov ax, bxadd 寄存器,寄存器add ax, bxsub 寄存器,寄存器sub ax, bx
mov 寄存器,内存单元mov ax, [0]add 寄存器,内存单元add ax, [0]sub 寄存器,内存单元sub ax, [0]
mov 寄存器,段寄存器mov ax, dsadd 寄存器,段寄存器不支持sub 寄存器,段寄存器不支持
内存单元mov 内存单元,数据不支持add 内存单元,数据不支持sub 内存单元,数据不支持
mov 内存单元,寄存器mov [0], axadd 内存单元,寄存器add [0], axsub 内存单元,寄存器sub [0], ax
mov 内存单元,内存单元不支持add 内存单元,内存单元不支持sub 内存单元,内存单元不支持
mov 内存单元,段寄存器mov [0], dsadd 内存单元,段寄存器不支持sub 内存单元,段寄存器不支持
段寄存器mov 段寄存器,数据不支持add 段寄存器,数据不支持sub 段寄存器,数据不支持
mov 段寄存器,寄存器mov ds, axadd 段寄存器,寄存器不支持sub 段寄存器,寄存器不支持
mov 段寄存器,内存单元mov ds, [0]add 段寄存器,内存单元不支持sub 段寄存器,内存单元不支持
mov 段寄存器,段寄存器不支持add 段寄存器,段寄存器不支持sub 段寄存器,段寄存器不支持

【数据段-小结】

  1. 字在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放再高地址单元中。
  2. mov指令要访问内存单元,可以在 mov指令中只给出单元的偏移地址,此时,段地址默认在DS寄存器中。
  3. [address]表示一个偏移地址为address的内存单元。
  4. 在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。
  5. movaddsub是具有两个操作对象的指令,访问内存中的数据段。jmp是具有一个操作对象的指令,对应内存中的代码段。
  6. 可以根据自己的推测,在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(以字为单位)。

【实例演示】使用pushpop交换ax和bx中的内容。

  • 初始栈顶始终不存储任何有效元素。
  • 第一个元素存储在初始栈顶指针SP的上一个字。

  若不断的调用 push或者pop指令,就会导致 SP 指针超出栈的范围,也就是“栈顶超界问题”。遗憾的是,8086CPU只知道栈顶在何处(SS:SP),却不知道程序安排的栈空间有多大,8086CPU不保证对栈的操作不会超界。为了防止超界,汇编语言程序员需要格外小心。

2.12 关于“段”的总结

  由于内存地址为 段地址:偏移地址,所以每一种“段”的起始地址都是16的倍数。目前我们学习了三种段:

  1. 数据段:将段地址放在 DS 中。用mov、add、sub等访问内存单元的指令时,CPU将我们定义的数据段中的内容当作数据段来访问。
  2. 代码段:将段地址放在 CS 中,将段中第一条指令的偏移地址放在 IP 中。CPU将执行我们定义的代码段中的指令。
  3. 栈段:将段地址放在 SS 中,将栈顶单元的偏移地址放在 SP 中。CPU在需要进行栈操作(push、pop)时,就将我们定义的栈段当作栈空间来用。

实际在编写汇编语言程序时,为了逻辑清晰,可以对不同的段进行分区,可以将三个段全部分开或者放在一起。但注意“段”的划分只取决于汇编语言程序员,与CPU设计无关。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虎慕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值