汇编语言-03寄存器(内存访问)
寄存器
上一章笔记是从CPU执行指令的角度8086CPU的逻辑结构、形成物理地址的方法、相关的寄存器以及一些指令。这章节从访问内存的角度学习几个寄存器。
内存中字的存储
CPU中,用16 位寄存器来存储一个字。高8位存放高位字节,低8位存放低位字节。在内存中存储时,由于内存单元是字节单元(一个单元存放一个字节),则一个字要用两个地址连续的内存单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
用0、1两个内存单元存放数据20000(4E20H) 。0 、1两个内存单元用来存储一个字,内存中字的存储单元可以看作一个起始地址为0的字单元(存放一个字的内存单元,由0、1两个字节单元组成)。对千这个字单元来说,0号单元是低地址单元,1号单元是高地址单元,则字型数据4E20H的低位字节存放在0号单元中,高位字节存放在1号单元中。同理,将2、3号单元看作一个字单元,它的起始地址为2。在这个字单元中存放数据18(0012H),则在2 号单元中存放低位字节12H,在3号单元中存放高位字节00H。
字单元的概念:字单元,即存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。
DS和[address]
CPU要读写一个内存单元的时候,必须先给出这个内存单元的地址。
在8086PC 中,
内存地址由段地址和偏移地址组成。8086CPU中有一个DS寄存器,通常用来存放要访问数据的段地址。比如我们要读取10000H 单元的内容,可以用如下的程序段进行。
mov指令作用: 1.将数据直接送入寄存器;2.将一个寄存器中的内容送入另一个寄存器。
3.mov指令也可以将一个内存单元中的内容送入一个寄存器中。在指令中必须指明从哪一个内存单元送到哪一个寄存器。寄存器用寄存器名来指明,内存单元则需用内存单元的地址来指明。显然,此时mov指令的格式应该是:mov 寄存器名,内存单元地址。“[…]”表示一个内存单元, “[…]”中的0表示内存单元的偏移地址。只有偏移地址是不能定位一个内存单元的,在指令执行时,8086CPU自动取ds中的数据为内存单元的段地址。
8086CPU不支待将数据直接送入段寄存器的操作,ds是一个段寄存器,所以mov ds,1000H这条指令是非法的。那么如何将1000H送入ds呢?只好用一个寄存器来进行中转,即先将1000H 送入一个一般的寄存器,如bx, 再将bx中的内容送入ds。
字的传送
mov指令在寄存器和内存之间进行字节型数据的传送。因为8086CPU 是16位结构,有16根数据线,所以,可以一次性传送16 位的数据,也就是说可以一次性传送一个字(两个内存单元)。只要在mov指令中给出16位的寄存器就可以进行16 位数据的传送了。
mov、add、sub指令
此外,还有
mov 寄存器,段寄存器
例如:mov ax,ds
mov 内存单元,段寄存器 (因为段寄存器是16位,所以要占用两个字节/16位/四个十六进制,内存单元起始位为低位)
例如:
用a写好汇编指令,用t执行,用d查看。
mov [0000],cs 执行后, CS 中的数据(14E3H)被写入1000:0 处, 1000:1 单元存放14H, 1000:0 单元存放E3H 。
mov 段寄存器,内存单元
数据段
对千8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。我们可以将一组长度为N(N~64KB) 、地址连续、起始地址为16 的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。比如用123B0H~123B9H 这段内存空间来存放数据,我们就可以认为, 123B0H~123B9H 这段内存是一个数据段,它的段地址为123BH, 长度为10 个字节。
用ds存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。比如,将123B0H ~ 123B9H的内存单元定义为数据段。现在要累加这个数据段中的前3个单元中的数据,代码如下。
小结
(1) 字在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
(2) 用mov指令访问内存单元,可以在mov指令中只给出单元的偏移地址,此时,段地址默认在DS寄存器中。.
(3) [address]表示一个偏移地址为address的内存单元。
(4) 在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。
(5) mov、add 、sub 是具有两个操作对象的指令。jmp是具有一个操作对象的指令。
栈
栈是一种具有特殊的访问方式的存储空间。它的特殊性就在于,最后进入这个空间的数据,最先出去。
从程序化的角度来讲,应该有一个标记,这个标记一直指示着盒子最上边的位置。
栈有两个基本的操作:入栈和出栈。入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。栈的这种操作规则被称为: LIFO(Last In First Out, 后进先出)。
CPU提供的栈机制
现今的CPU中都有栈的设计,8086CPU也不例外。8086CPU提供相关的指令来以栈的方式访问内存空间。这意味着,在基千8086CPU 编程的时候,可以将一段内存当作栈来使用。
8086CPU提供入栈和出栈指令,最基本的两个是PUSH(入栈)和POP(出栈)。比如,push ax表示将寄存器ax中的数据送入栈中,pop ax表示从栈顶取出数据送入ax。8086CPU的入栈和出栈操作都是以字为单位进行的。
注意: 字型数据用两个单元存放, 高地址单元存放高8 位,低地址单元存放低8 位。
8086CPU中,有两个寄存器,段寄存器SS和寄存器SP, 栈顶的段地址存放在SS中,偏移地址存放在SP中。任意时刻,SS:SP指向栈顶元素。push指令和pop指令执行时,CPU从SS和SP中得到栈顶的地址。
push命令
push ax 的执行,由以下两步完成。
(1) SP = SP - 2, SS:SP 指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;
(2) 将ax中的内容送入SS:SP 指向的内存单元处, SS:SP 此时指向新栈顶。
10000H - 1000FH栈空间大小为16 字节,栈最底部的字单元地址为1000:000E。任意时刻,SS:SP 指向栈顶,当栈中只有一个元素的时候,SS=1000H,SP=000EH。栈为空,就相当于栈中唯一的元素出栈,出栈后,SP=SP+2,SP原来为000EH, 加2后SP=10H。所以,当栈为空的时候, SS=1000H,SP=10H 。
栈空,SS:SP指向栈空间最高地址单元的下一个单元。执行push ax后, SS : SP指向栈中的第一个元素。10000H - 1000FH 这段空间当作栈,初始状态栈是空的,此时, SS=1000H,SP=0010H。
pop命令
pop ax 的执行过程和push ax 刚好相反,由以下两步完成。
(I) 将SS:SP 指向的内存单元处的数据送入ax 中;
(2) SP=SP+2, SS : SP 指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。
出栈后,SS:SP指向新的栈顶1000EH,pop操作前的栈顶元素,1000CH处的2266H依然存在,但是,它已不在栈中。当再次执行push 等入栈指令后,SS:SP 移至1000CH,并在里面写入新的数据,它将被覆盖。
栈顶超界的问题
push后栈顶超出
将10010H~1001FH 当作栈空间,该栈空间容量为16 字节(8 字),初始状态为空, SS=1000H 、SP=0020H,SS:SP指向10020H;在执行8次push ax后,向栈中压入8个字,栈满,SS:SP 指向10010H;再次执行push ax;sp=sp - 2,SS:SP 指向1000EH,栈顶超出了栈空间,ax中的数据送入1000EH单元处,将栈空间外的数据覆盖。
pop后栈顶超出
在执行8 次pop ax 后,从栈中弹出8 个字,栈空, SS:SP 指向10020H;再次执行pop ax;sp=sp+2, SS:SP指向10022H,栈顶超出了栈空间。此后,如果再执行push 指令,10020H、10021H 中的数据将被覆盖。
栈顶超界是危险的,因为我们既然将一段空间安排为栈,那么在栈空间之外的空间里很可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们自己程序中的,也可能是别的程序中的(毕竟一个计算机系统中并不是只有我们自己的程序在运行)。但是由于我们在入栈出栈时的不小心,而将这些数据、代码意外地改写,将会引发一连串的错误。
8086CPU不保证我们对栈的操作不会超界。这也就是说,8086CPU只知道栈顶在何处(由SS:SP 指示),而不知道我们安排的栈空间有多大。这点就好像CPU只知道当前要执行的指令在何处(由CS:IP 指示),而不知道要执行的指令有多少。从这两点上我们可以看出8086CPU的工作机理,它只考虑当前的情况: 当前的栈顶在何处、当前要执行的指令是哪一条。我们在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。
push、pop指令
push和pop指令是可以在寄存器和内存(栈空间当然也是内存空间的一部分,它只是一段可以以一种特殊的方式进行访问的内存空间)之间传送数据的。
push 和pop 指令的格式可以是如下形式:
push 寄存器; 将一个寄存器中的数据入栈
pop 寄存器; 出栈,用一个寄存器接收出栈的数据
push 段寄存器;将一个段寄存器中的数据入栈
pop 段寄存器;出栈,用一个段寄存器接收出栈的数据
push 内存单元;将一个内存字单元处的字入栈(注意:栈操作都是以字为单位)
pop 内存单元;出栈,用一个内存字单元接收出栈的数据
mov ax,1000H mov ds,ax #内存单元的段地址要放在ds中 push [0] #将1000:0处的字压入栈中 pop [2] #出栈,出栈的数据送入1000:2处
指令执行时,CPU要知道内存单元的地址,可以在push、pop指令中只给出内存单元的偏移地址,段地址在指令执行时,CPU从ds中取得。
初始化栈地址
将10000H ~ 1000FH这段空间当作栈,初始状态栈是空的,将AX、BX、DS中的数据入栈。
mov ax ,1000H
mov ss,ax ;设置栈的段地址,SS=1000H,不能直接向段寄存器SS中送入数据,所以用ax中转。
mov sp,0010H ;设置栈顶的偏移地址,因栈为空,所以sp=0010H 。3 条指令设置栈顶地址。
push ax
push bx
push ds
利用栈交换AX和BX数据
push、pop 实质上就是一种内存传送指令,可以在寄存器和内存之间传送数据,与mov指令不同的是, push和pop指令访问的内存单元的地址不是在指令中给出的,而是由SS:SP 指出的。同时,push和pop指令还要改变SP中的内容。
push和pop指令同mov指令不同,CPU执行mov指令只需一步操作,就是传送,而执行push、pop指令却需要两步操作。执行push时,CPU的两步操作是:先改变SP,后向SS:SP处传送。执行pop时,CPU的两步操作是:先读取SS:SP处的数据,后改变SP。
注意: push,pop 等栈操作指令,修改的只是SP 。也就是说,栈顶的变化范围最大为: 0 - FFFFH。
栈总结
(1)8086CPU提供了栈操作机制,在SS 、SP 中存放栈顶的段地址和偏移地址;提供入栈和出栈指令,它们根据SS:SP指示的地址,按照栈的方式访问内存单元。
(2)push指令的执行步骤:1. SP=SP - 2。2. 向SS:SP指向的字单元中送入数据。
(3)pop指令的执行步骤:1. 从SS:SP指向的字单元中读取数据。 2.SP=SP+2 。
(4)任意时刻,SS:SP指向栈顶元素。
(5)CPU只记录栈顶,栈空间的大小我们要自己管理。
(6)用栈来暂存以后需要恢复的寄存器的内容时,寄存器出栈的顺序要和入栈的顺序相反。
(7)push、pop实质上是一种内存传送指令,注意它们的灵活应用。
栈段
在编程时,可以根据需要将一组内存单元定义为一个段。我们可以将长度为N(N~64KB)的一组地址连续、起始地址为16的倍数的内存单元,当作栈空间来用,从而定义了一个栈段。比如,我们将10010H-1001FH这段长度为16字节的内存空间当作栈来用,以栈的方式进行访问。这段空间就可以称为一
个栈段,段地址为1001H, 大小为16 字节。
将一段内存当作栈段,仅仅是我们在编程时的一种安排,CPU并不会由于这种安排就在执行push、pop等栈操作指令时自动地将我们定义的栈段当作栈空间来访问。如何使得如push 、pop等栈操作指令访问我们定义的栈段呢?就是将SS:SP指向我们定义的栈段的栈顶。
栈段最大容量
push、pop等指令在执行的时候只修改SP,所以栈顶的变化范围是0 - FFFFH,从栈空时候的SP=0,一直压栈,直到栈满时SP=0;如果再次压栈,栈顶将环绕,覆盖了原来栈中的内容。所以一个栈段的容量最大为64KB 。
栈段总结
我们可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元。这完全是我们自己的安排。
我们可以用一个段存放数据,将它定义为“数据段”;
我们可以用一个段存放代码,将它定义为“代码段”;
我们可以用一个段当作栈,将它定义为“栈段” 。
我们可以这样安排,但若要让CPU按照我们的安排来访问这些段,就要:
对于数据段,将它的段地址放在DS中,用mov、add、sub等访问内存单元的指令时,CPU就将我们定义的数据段中的内容当作数据来访问;
对于代码段,将它的段地址放在CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码段中的指令;
对于栈段, 将它的段地址放在SS中,将栈顶单元的偏移地址放在SP中,这样CPU 在需要进行栈操作的时候,比如执行push、pop 指令等,就将我们定义的栈段当作栈空间来用。
可见,不管我们如何安排,CPU将内存中的某段内容当作数据,是因DS:[偏移量]指向了那里;CPU将内存中的某段内容当作代码,是因CS:IP指向了那里;CPU将某段内存当作栈,是因为SS:SP指向了那里。我们一定要清楚,什么是我们的安排,以及如何让CPU按我们的安排行事。要非常清楚CPU的工作机理,才能在控制CPU按照我们的安排运行的时候做到游刃有余。
实验
段地址是放在段寄存器中的,在D命令后面直接给出段地址,是Debug提供的一种直观的操作方式。D命令是由Debug 执行的,Debug在执行“d 1000:0"这样的命令时,也会先将段地址1000H 送入段寄存器中。
访问内存的指令如“ mov ax,[]" 等一般都默认段地址在ds 中,所以Debug 在执行如“ d 段地址:偏移地址”这种D命令时,将段地址送入ds中比较方便。
D命令也提供了一种符合CPU机理的格式:“d 段寄存器:偏移地址"
E、A 、U这些可以带有内存单元地址的命令中,也可以同D命令一样,用段寄存器表示内存单元的段地址。
在Debug中,用A 命令写一段程序:
用t执行
![微信公众号二维码](https://images.cnblogs.com/cnblogs_com/aeolian/1679458/o_wechat_gzh_qrcode.jpg)