二进制安全-汇编语言-03-寄存器(内存访问)
三、寄存器(内存访问)
KEEP DOING
3.1 内存中字的存储
- 小端存储:低地址存放低位字节,高地址存放高位字节
- 大端存储:低地址存放高位字节,高地址存放低位字节
8086CPU中,采用小端存储,高8位存放高位字节,低8位存放低位字节
在内存中存放数据20000H(4E20H)在0,1两个内存单元:
0号单元是低地址,存放低位字节20H;
1号单元是高地址,存放高位字节4EH;
小端存储是从左到右,地址也是从左到右递增
字单元
存放一个字型数据(16位)的内存单元,有两个地址连续的内存单元组成。
高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。
我们将起始地址为N的字单元简称为N地址字单元。
比如一个字单元由2、3两个内存单元组成,则这个字单元的起始地址为2,我们可以说这是2地址字单元。
问题3.1
对于图3.1:
(1)0地址单元中存放的字节型数据是多少?
20H
(2)0地址字单元中存放的字型数据是多少?
4E20H
(3)2地址单元中存放的字节型数据是多少?
12H
(4)2地址字单元中存放的字型数据是多少?
0012H
(5)1地址字单元中存放的字型数据是多少?
124EH
3.2 DS和[address]
CPU要读写一个内存单元时,需要给出这个内存单元的地址
在8086PC中,内存地址由段地址
和偏移地址
组成
因此,8086CPU设计一个DS寄存器,来存放要访问数据的段地址
注:Data Segment register 数据段寄存器
mov bx,1000H
mov ds,bx
mov al, [0]
上面的3条指令将10000H(1000:0)中的数据读到al中
mov指令,可完成两种传送:
- 将数据直接送入寄存器
- 将一个寄存器中的内容送入另一个寄存器
mov 寄存器名,内存单元地址
重点:段地址+偏移地址
“[…]”表示一个内存单元,“[…]”中的0表示内存单元的偏移地址
有了偏移地址,那么我们就需要有段地址,才能够定位一个内存单元
指令执行时,8086CPU自动获取DS寄存器中的数据为内存单元的段地址
1、如何用mov指令从10000H中读取数据?
10000H用段地址和偏移地址表示为1000:0,我们先将段地址1000H放入ds,然后用moval,[0]完成传送。mov指令中的[...]
说明操作对象是一个内存单元,[ 0 ]
中的0说明这个内存单元的偏移地址是0,它的段地址默认放在ds中,指令执行时,8086CPU会自动从ds中取出。
mov bx,1000H
mov ds,bx
若要用mov al,[0] 完成数据从1000:0单元到al的传送,这条指令执行时,ds中的内容应为段地址1000H,所以在这条指令之前应该将1000H送入ds。
2、如何把一个数据送入寄存器呢?
我们以前用类似“movax,1”这样的指令来完成,从理论上讲,我们可以用相似的方式:mov ds,1000H,来将1000H送入ds。可是,现实并
非如此,8086CPU不支持将数据直接送入段寄存器的操作,ds是一个段寄存器,所以movds,1000H这条指令是非法的。
那么如何将1000H送入ds呢?
只好用一个寄存器来进行中转,即先将1000H送入一个一般的寄存器,如bx,再将bx中的内容送入ds。
总结:
要给段寄存器赋值,只能通过一个寄存器作为中转赋值,不能直接赋值数据。
段寄存器ds中有段地址了,再给一个偏移地址[xxx]就能够访问到对应的内存单元了。
问题3.2
写几条指令,将al中的数据送入内存单元10000H中,思考后看分析。
mov bx,1000
mov ds,bx
mov [0],al
思考:
先确定好段地址,把段地址赋值给一个寄存器,然后在赋值给ds寄存器,
最后利用[xxx]确定偏移地址,就可以对这个地址进行操作。
3.3 字的传送
前面我们用mov指令在寄存器和内存之间进行字节型数据的传送。
因为8086CPU是16位结构,有16根数据线,所以,可以一次性传送16位的数据,也就是说可以一次性传送一个字。
只要在mov指令中给出16位的寄存器就可以进行16位数据的传送了。
比如:
mov ax,1000H
mov ds,bx
mov ax,[0] ;1000:0处的字型数据送入ax
mov [0],cx ;cx中的16位数据送到1000:0处
问题3.3
内存中的情况如图3.2所示,写出下面的指令执行后寄存器ax,bx,cx中的值。
mov ax,1000H
mov ds,ax
mov ax,[0]
mov bx,[2]
mov cx,[1]
add bx,[1]
add cx,[2]
指令 | 执行后相关寄存器中的内容 | 说明 |
---|---|---|
mov ax,1000H | ax=1000H | |
mov ds,ax | ds=1000H | 前两条指令的目的就是为了将ds设置为1000H |
mov ax,[0] | ax=1123H | 将1000:0处存放的字型数据送入ax, |
1000:1单元存放字型数据的高8位:11H | ||
1000:0单元存放字型数据的低8位:23H | ||
所以1000:0处存放的字型数据为1123H | ||
指令执行时,字型数据的高8位送入ah,字型数据的低8位送入al | ||
mov bx,[2] | bx=6622H | 以下同上原理 |
mov cx,[1] | cx=2211H | |
add bx,[1] | bx=2211+6622=8833H | |
add cx,[2] | cx=6622+2211=8833H |
问题3.4
内存中的情况如图3.3所示,写出下面的指令执行后内存中的值。
mov ax,1000H
mov ds,ax
mov ax,11316
mov [0],ax
mov bx,[0]
sub bx,[2]
mov [2],bx
说明
指令 | 执行后相关寄存器中的内容 | 说明 |
---|---|---|
mov ax,1000H | ax=1000H | |
mov ds,ax | ds=1000H | 将ds设置为1000H |
mov ax,11316 | ax=2C34H | 十进制11316,十六进制2C34H |
mov [0],ax | 10000H内存单元=34,10001H内存单元=2C | |
mov bx,[0] | bx=2C34H | |
sub bx,[2] | bx=bx-1122H=1B12H | sub减法指令 |
mov [2],bx | 10002H内存单元=12,10003H内存单元=1B |
3.4 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 寄存器,段寄存器 比如:mov ax,ds可以在dosbox上验证
-r cs
1000
-r ip
0
a 1000:0
1000:0000 mov bx,1314
1000:0003 mov ds,bx
1000:0006 mov ax,ds验证可以实现
继续尝试:mov 内存单元,寄存器
mov 内存单元,寄存器 比如:mov [0],csmov ax,1000H
mov ds,ax
mov [0],cs和上面类似:在加一个查看某个内存单元的数据的命令
d 1000:0
最后尝试:mov 段寄存器,内存单元
mov ax,1000H
mov ds,ax
mov ds,[0]最后验证是可以的
add和sub指令同mov一样,都有两个操作对象。它们也可以有以下几种形式。
add 寄存器,数据 比如:add ax,8
add 寄存器,寄存器 比如:add ax,bx
add 寄存器,内存单元 比如:add ax,[o]
add 内存单元,寄存器 比如:add [o],ax
sub 寄存器,数据 比如:sub ax,9
sub 寄存器,寄存器 比如:sub ax,bx
sub 寄存器,内存单元 比如:sub ax,[0]
sub 内存单元,寄存器 比如:sub[o],ax
那么add,sub指令可以对ds进行操作吗?
不行,不合法
add ds,ax
段寄存器不能参与算术运算
add ax,ds
段寄存器不能作为源操作数
3.5 数据段
内存的数据其实都是二进制数据,也不存在某个数据段
但是为了方便编写程序,那我们就可以定义某个内存单位区间作为数据段
数据段是逻辑上的概念,真实不存在
比如,将123B0H~123B9H的内存单元定义为数据段。现在要累加这个数据段中的前3个单元中的数据,代码如下。
mov ax,123BH
mov ds,ax ;将123BH送入ds中,作为数据段的段地址
mov al,0 ;用al存放累加结果
add al,[0] ;将数据段第一个单元(偏移地址为0)中的数值加到a1中
add al,[1] ;将数据段第二个单元(偏移地址为1)中的数值加到a1中
add al, [2] ;将数据段第三个单元(偏移地址为2)中的数值加到a1中
问题3.5
写几条指令,累加数据段中的前3个字型数据,思考后看分析。
mov ax,123BH
mov ds,ax
mov ax,0
add ax,[0]
add ax,[2]
add ax,[4]
字型数据,每一个字型数据占用两个单元
小结3.1~3.5
1、字在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。
2、用mov指令访问内存单元,可以在mov指令中只给出单元的偏移地址,此时,段地址默认在DS 寄存器中。
3、[address]表示一个偏移地址为address的内存单元。
4、在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。
5、mov、add、sub是具有两个操作对象的指令。jmp是具有一个操作对象的指令。
6、可以根据自己的推测,在Debug中实验指令的新格式。
检测点3.1
(1)在Debug中,用 “ d 0:0 1f ”查看内存,结果如下。
0000:0000 70 80 F0 30 EF 60 30 E2-00 80 80 12 66 20 22 60
0000:0010 62 26 E6 D6 CC 2E 3C 3B-AB BA 00 00 26 06 66 88
下面的程序执行前,AX=0,BX=0,写出每条汇编指令执行完后相关寄存器中的值。
- 段地址设置
mov ax,1
→AX=0001H
mov ds,ax
→DS=0001H
,此时数据段基址为00010H
(DS*16
)。1
- 内存寻址规则
- 指令中的偏移地址对应物理地址
00010H
,即内存0000:0010
处的数据62 26
(低字节在前),故AX=2662H
。
- 指令中的偏移地址对应物理地址
- 逐条指令执行结果:
mov ax,
→AX=2662H
(从0000:0010
读取字数据)。mov bx,
→BX=E626H
(从0000:0011
读取字数据)。mov ax,bx
→AX=E626H
mov ax,
→AX=2662H
mov bx,
→BX=D6E6H
(从0000:0012
读取)。add ax,bx
→AX=FD48H
(2662H+D6E6H
,进位截断)。add ax,
→AX=2C14H
(FD48H+2ECCH
,结果取低16位)。mov ax,0
→AX=0000H
mov al,
→AX=00E6H
(仅修改AL,AH保持0)。mov bx,0
→BX=0000H
mov bl,[000C]
→BX=0026H
(从0000:001C
读取字节)。add al,bl
→AX=000CH
(E6H+26H=0CH
,AL为8位)
指令 | 执行后相关寄存器中的内容 | 说明 |
---|---|---|
mov ax,1 | ax=1 | |
mov ds,ax | ds=1 | 将ds设置为1 |
mov ax,[0000] | ax=2662 | 0000:0010处的字型数据赋值给ax |
mov bx,[0001] | bx=E626 | 0000:0011处的字型数据赋值给bx |
mov ax,bx | ax=E626 | 将bx中的数据赋值到ax |
mov ax,[0000] | ax=2662 | 0000:0010处的字型数据赋值给ax |
mov bx,[0002] | bx=D6E6 | 0000:0012处的字型数据赋值给bx |
add ax,bx | ax=FD48 | 2662+D6E6=FD48,进位截断 |
add ax,[0004] | ax=2C14 | FD48+2ECC=2C14,进位截断,取低位 |
mov ax,0 | ax=0000 | ax直接赋值为0 |
mov al,[0002] | ax=00E6 | ax的低位al=E6,高位ah=0 |
mov bx,0 | bx=0 | bx直接赋值为0 |
mov bl,[000C] | bx=0026 | bx的低位bl=E6,高位bh=0 |
add al,bl | ax=000C | E6+26=10C,进位截断 |
(2)内存中的情况如图3.6所示。
1、各寄存器的初始值:CS=2000H,IP=0,DS=1000H,AX=0,BX=0;
2、写出CPU执行的指令序列(用汇编指令写出)。
3、写出CPU执行每条指令后,CS、IP和相关寄存器中的数值。
再次体会:数据和程序有区别吗?如何确定内存中的信息哪些是数据,哪些是程序?
CS:IP | 指令 | 各寄存器的值 | 说明 |
---|---|---|---|
CS=2000H,IP=0,指向2000:0000 | mov ax,6622H | AX=6622H,BX=0,DS=1000H | |
CS=2000H,IP=3,指向2000:0003 | jmp 0ff0:0100 | AX=6622H,BX=0,DS=1000H | 0ff0x10+0100=10000H |
CS=1000H,IP=0,指向1000:0000 | mov ax,2000H | AX=2000H,BX=0,DS=1000H | |
CS=1000H,IP=3,指向1000:0003 | mov ds,ax | AX=2000H,BX=0,DS=2000H | |
CS=1000H,IP=5,指向1000:0005 | mov ax,[0008] | AX=C389H,BX=0,DS=2000H | |
CS=1000H,IP=8,指向1000:0008 | mov ax,[0002] | AX=EA66H,BX=0,DS=2000H | |
CS=1000H,IP=B,指向1000:000B | 无指令 |
理解:
内存中的数据其实都是二进制数据0和1,本质上没有区别
但是人类很聪明,将计算机设计成可以分时段,即指令周期,来把数据分为指令和数据
当指令周期中的取指令周期时,把数据当做指令来操作
当指令周期中的执行周期时,把数据当做操作数来操作
这里作者水平有限,让deepseek来帮助一下吧!
再次体会数据和程序的区别:在物理层面,内存中的所有信息都只是二进制数据(0和1),本质上并无区别。
区分它们的关键在于计算机执行指令的时序。计算机体系结构的精妙之处在于,它将指令的执行划分为不同的时钟周期阶段(最典型的是取指周期和执行周期)。
- 在取指周期(Fetch Cycle): CPU 将特定内存地址的内容解释为指令(操作码),并将其取入指令寄存器。
- 在执行周期(Execute Cycle): CPU 根据取到的指令,可能再次访问内存,此时从相应地址读取的内容则被解释为该指令的操作数(数据)。
因此,内存中的同一串二进制位是“数据”还是“程序”(指令),完全取决于它在指令执行周期中被访问的时机和上下文。是 CPU 当前的“解读意图”(由硬件时序逻辑控制)赋予了它们不同的角色。
3.6 栈
栈不多说,可以类比弹夹压子弹
栈顶:栈中最上面的(有指针,看怎么定义了)
入栈:将一个新元素方到栈顶
出栈:从栈顶取出一个元素
栈最重要的一句话:后进先出,Last In First Out
3.7 CPU提供的栈机制
现今的CPU中都有栈的设计,8086CPU也不例外。8086CPU提供相关的指令来以栈的方式访问内存空间。
这意味着,在基于8086CPU编程的时候,可以将一段内存当作栈来使用。
8086CPU提供入栈和出栈指令,最基本的两个是PUSH(入栈)和POP(出栈)。
比如,push ax表示将寄存器ax中的数据送入栈中,pop ax表示从栈顶取出数据送入ax。
8086CPU的入栈和出栈操作都是以字为单位进行的。
这里值得注意的就是:
字型数据要用到两个内存单元来存放,高地址单元放高8位,低地址单元放低8位。
以mov ax,0123H push ax
为例,01是高位字节,因此放在位于1000FH的内存单元
23是低位字节,因此放在位于1000EH的内存单元
看懂上述栈操作后,我们这里会产生两个疑惑?
(1)CPU怎么知道10000H~1000FH这段空间被当做是栈来使用呢?
(2)push、pop在执行的时候,必须要知道哪个单元是栈顶,CPU又怎么知道栈顶是哪个呢?
因此,与CS:IP的设计思维一致,CS:IP始终指向下一条要执行的指令的位置
那么,任意时刻,SS:SP指向栈顶元素,同样的是栈段寄存器SS和栈寄存器SP(自编的)
push、pop指令执行时,CPU先去从SS和SP中得到栈顶的地址
官方说法:
SS (Stack Segment Register)是 堆栈段寄存器 ,用于存放堆栈的段地址;
SP (Stack Pointer)是 堆栈指针寄存器 ,用于存放栈顶的偏移地址
下面以书中的例子来看一下push、pop在内存中的体现:
push ax 的执行,由以下两步完成。
(1)SP=SP-2,SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;
(2)将 ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶。
思考:
这里其实和CS:IP很像,可以类比来学习。
CS:IP中CS是段地址,就是基础地址,IP呢?是偏移地址,每次执行一条指令,IP都要自增。从而实现“自动执行”
SS:SP中SS是堆栈段地址,也是基础地址,SP呢?是指针寄存器,始终要指向栈顶。
因此,每次push/pop,SP都需要进行调整,从而达到“自动执行”的效果。
从图中我们可以看出,8086CPU中,入栈时,栈顶从高地址向低地址方向增长。
入栈SP-X,出栈SP+X。不用刻意记住,在脑海里构思一个栈就好。
问题3.6
如果将10000H~1000FH这段空间当作栈,初始状态栈是空的,此时,SS=1000H,SP=?思考后看分析。
其实,思考这个问题,我们要回归到SS:SP的核心:始终指向栈顶
如果10000H~1000FH都是栈空间,且栈为空,那么我们就要找到栈空间中最高地址
但是栈为空,说明在这个最高地址的上面就是栈顶:1000FH + 1H = 10010H
这里是pop指令执行的操作
这里会有一个思考:为什么这个pop出去了,2266还在内存单元中呢?
其实,这也是计算机的一个设计思维:
出栈的目的只是为了拿到栈中的一个数据,我只要拷贝到这个数据就可以了
如果我要删除这个数据,我还得给这个内存单元赋值,明显不划算
一般计算机采用的就是,写入时把新数据给它覆盖就行了
不禁思考到,很多时候,删除电脑中的数据文件,真的删除了吗?
还只是改变了指向这个文件的指针,毕竟我不指向他了就等于访问不到他了
如果我需要这片空间写数据,我直接覆盖就好了
就凭这个思维,想必会有很多漏洞或者信息暴露出来,
但是作者水平有限,只能思考到这,后续学习希望能碰上这类手法
3.8 栈顶超界的问题
越界的问题就不多说了,多次入栈,多次出栈,很容易造成栈顶超界的问题。
一旦越界,就可能导致覆盖掉了别的程序的代码或数据,影响很严重。
但是,8086CPU并没有设置相应的限制,或者定义栈空间等
这也能理解,当时计算机还在发展初期,在x86汇编中我们会学到保护模式,
那里应该会有限制
因此,我们可以发现8086CPU的两条核心原则:
- 当前的栈顶在何处
- 当前要执行的指令是哪一条
3.9 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中获得。
问题3.7
编程,将10000H~1000FH这段空间当作栈,初始状态栈是空的,将AX、BX、DS中的数据入栈。
mov ax,1000H
mov ss,ax ;设置栈的段地址,SS=1000H,不能直接向段寄存器SS中送入数据,所以用ax中转,和ds一样
mov sp,0010H ;设置栈顶的偏移地址,因栈为空,所以sp=0010H。这里设置栈顶地址。
push ax
push bx
push ds
问题3.8
编程:
(1)将10000H~1000FH这段空间当作栈,初始状态栈是空的;
(2)设置AX=001AH,BX=001BH;
(3)将AX、BX中的数据入栈;
(4)然后将AX、BX清零;
(5)从栈中恢复AX、BX原来的内容。
mov ax,1000H
mov ss,ax
mov sp,0010H ;设置栈顶地址
mov ax,001AH
mov bx,001BH
push ax
push bx ;把ax,bx压入栈中
mov ax,0
mov bx,0 ;把ax,bx清零
pop bx
pop ax ;这里注意先pop bx,再pop ax,后进先出原则
问题3.9
编程:
(1)将10000H~1000FH这段空间当作栈,初始状态栈是空的;
(2)设置AX=001AH,BX=001BH;
(3)利用栈,交换AX和BX中的数据。
mov ax,1000H
mov ss,ax
mov sp,0010H ;设置栈顶地址
mov ax,001AH
mov bx,001BH
push ax
push bx ;把ax,bx压入栈中
pop ax
pop bx ;先pop ax,即将原先bx的内容赋给ax;再pop bx,即将原先ax的内容赋给bx
问题3.10
如果要在10000H处写入字型数据2266H,可以用以下的代码完成:
mov ax,1000H
mov ds,ax
mov ax,2266H
mov [0],ax补全下面的代码,使它能够完成同样的功能:在10000H处写入字型数据2266H。
要求:不能使用“mov 内存单元,寄存器”这类指令。
这里有三行空行
____________
____________
____________mov ax,2266H
push axmov ax,1000H
mov ss,ax
mov sp,2
提供:SS、SP指示栈顶:改变SP后写内存的入栈指令;读内存后改变SP的出栈指令
这就是 8086CPU 提供的栈操作机制。
栈的综述
(1)8086CPU提供了栈操作机制,方案如下.
在SS、SP中存放栈顶的段地址和偏移地址:
提供入栈和出栈指令,它们根据SS:SP指示的地址,按照的方式访问内存单元。
(2)push指令的执行步骤:①SP-SP-2;②向 SS:SP 指向的字单元中送入数据。
(3)pop指令的执行步骤:①从SS:SP指向的字单元中读取数据;②SP=SP+2.
(4)任意时刻,SS:SP指向栈顶元素。
(5)8086CPU只记录栈顶,栈空间的大小我们要自己管理。
(6)用栈来暂存以后需要恢复的寄存器的内容时,寄存器出栈的顺序要和入栈的顺序相反。
(7)push、pop实质上是一种内存传送指令,注意它们的灵活应用。
栈是一种非常重要的机制,一定要深入理解,灵活掌握。
3.10 栈段
栈段与前面的段概念类似,指定一段内存空间作为栈空间(栈段)
将SS:SP指向我们定义的栈段
问题3.11
如果将10000H~1FFFFH这段空间当作栈段,初始状态栈是空的,此时,SS=1000H,SP=?
这和之前的问题3.6类似,但有所不同,栈中是存放字型单元的
起初,我以为栈中最低的位置为1000:FFFF,这里我犯了一个错误就是,把栈中数据当做一个内存单元来看了
但其实不是的,如果在10000H~1FFFFH的栈段中,栈底指向的位置就是1000:FFFE
也就是SS=1000H,SP=FFFEH
如果栈为空栈,那么栈底就为栈中最低位置的下一个字型单元,即SP+2
那么SS=1000H,SP=0000H
总结:栈中存放的都是字型单元
问题3.12
一个栈段最大可以设为多少?为什么?
在8086CPU中,一个栈段的最大大小是64KB
最主要的原因就是:栈指针寄存器SP的位数的限制
SS:SP,SP是用来寻址的,SP是一个16位寄存器,其取值范围为0x0000到0xFFFF
因此,不管SS指向哪一个栈段,栈的可用空间始终由SP的16位偏移量限制
数据、代码、栈段
数据段:段地址DS指定
代码段:CS:IP指定
栈段:SS:SP指定
参考一下下面的代码理解一下:
比如我们将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
检测点3.2
(1)补全下面的程序,使其可以将10000H1000FH中的8个字,逆序复制到20000H2000FH中。逆序复制的含义如图3.17所示(图中内存里的数据均为假设)。
mov ax,1000H
mov ds,ax ;设置段寄存器地址mov ax,2000H
mov ss,ax
mov sp,0010H ;设置栈顶地址push [0]
push [2]
push [4]
push [6]
push [8]
push [A]
push [C]
push [E]
(2)补全下面的程序,使其可以将10000H1000FH中的8个字,逆序复制到20000H2000FH中。
mov ax,2000H
mov ds,axmov ax,1000H
mov ss,ax
mov sp,0000Hpop [E]
pop [C]
pop [A]
pop [8]
pop [6]
pop [4]
pop [2]
pop [0]
这里可以自行画一个堆栈图出来辅助理解,主要是对push、pop两个指令的加深理解,以及栈顶的设置
实验二
1、D命令
我们来回顾一下D命令:“ d 段地址:偏移地址 ” 可以查看指定内存单元的内容
我们尝试来拆解一下D命令,
首先,D命令后面是直接给了一个段地址,
比如,Debug在执行“ d 1000:0 ”这样的命令时,会先把段地址1000H送入段寄存器中
那么我们就会有疑问了,
(1)D命令为什么会自动执行?
D命令后面是一段程序
(2)谁来执行这段程序呢?
CPU
(3)CPU是从哪里拿到内存单元的段地址?
段地址寄存器
因此,我们得出结论,Debug在处理D命令时,肯定要把段地址送入段寄存器中
段寄存器有四个:CS、DS、SS、ES
CS:IP控制指令执行
SS:SP始终指向栈顶
DS段地址寄存器合适!
ES Extra Segment 附加段寄存器
DS 主要指向程序的数据段,而 ES 在一些特殊操作中提供了额外的段地址信息,
帮助程序更有效地执行某些任务,比如字符串处理、内存操作等。
D命令支持一种格式:“ d 段寄存器:偏移地址 ”
SA:偏移地址
(1)-r ds:1000-r ds:0 ;查看从1000:0开始的内存区间中的内容(2)-r ds:1000-d ds:10 18 ;查看1000:0~1000:18中的内容(3)-d cs:0 ;查看当前代码段中的指令代码(4)-d ss:0 ;查看当前栈段中的内容
2、在E、A、U命令中使用段寄存器
在E、A、U这些可以带有内存单元地址的命令中,也可以同D命令一样,用段寄存器表示内存单元的段地址,以下是几个例子。
-r ds
:1000
-e ds:0 11 22 33 44 55 66 ;在从1000:0开始的内存区间中写入数据-u cs:0 ;以汇编指令的形式,显示当前代码段中的代码,0代码的偏移地址-r ds
:1000
-a ds:0 ;以汇编指令的形式,向从1000:0开始的内存单元中写入指令
3、下一条指令执行了吗?
我们来看一段有意思的程序
在Debug中,用A命令写一段程序:
mov ax,2000
mov sS,ax
mov sp,10 ;安排2000:0000~2000:000F为栈空间,初始化栈顶mov ax,3123
push ax
mov ax,3366
push ax ;在栈中压入两个数据
如下图
我们不禁要问mov sp,10
哪去了呢?
其实,这里我们发现,当我们设置了SS的值后,紧接着SP的值也被改变了
一般情况下,T命令执行完一条指令,就会停下来给你显示各个寄存器的状态,还有下一步要执行的指令
但是T命令在执行mov ss,ax
并没有这样,为什么呢?
其实,不只有mov ss,ax
,还有 mov ss,bx
mov ss,[0]
pop ss
等指令都会发生上面这种情况
它们有什么共性吗?都是修改栈段寄存器SS
为什么会这样呢?计算机是出于什么考虑这样设计呢?
计算机设计了一个中断机制,具体怎么操作这里就不多说,后面会学到
但是我们可以思考一下,计算机中断(介入)的原因:
这里我们通过mov ss,ax
指令修改了SS的值,那么如果SP不及时修改,
就会导致栈段指向原来的地方或是一个随机的地方,
那么就会导致某些需要用到栈段的程序崩溃、系统锁定、数据损坏等后果,
所以修改了SS,那么我们也要让计算机停下来等SP修改后再运行,保持它两是个“原子性”操作
不知道这么说能不能理解,不懂可以到后面学到中断就理解了
现在我们只要知道这一点就可以了:
Debug的T命令在执行修改寄存器SS的指令时,下一条指令也紧接着被执行。
实验任务
(1)使用Debug,将下面的程序段写入内存,逐条执行,根据指令执行后的实际运行情况填空。
mov ax,ffff
mov ds,ax ;设置段地址为FFFFHmov ax,2200
mov ss,ax
mov sp,0100 ;设置栈段地址为2200:0100mov ax,[0] ;ax=C0EA
add ax,[2] ;ax=0012
mov bx,[4] ;bx=0012
add bx,[6] ;bx=0012push ax ;sp=__00FEH____ ; 修改的内存单元的地址是_22100H____内容为___0012H__
push bx ;sp=__2F31H____ ; 修改的内存单元的地址是__220FEH___内容为___2F31H__
pop ax ;sp=__00FCH____ ; ax=__0012H____
pop bx ;sp=__0100H____ ; bx=__0012H____push [4] ;sp=__00FEH____ ; 修改的内存单元的地址是__220FEH___内容为__0___
push [6] ;sp=__00FCH____ ; 修改的内存单元的地址是__220FCH___内容为__0___
(2)仔细观察图3.19中的实验过程,然后分析:为什么2000:0~2000:f中的内容会发生改变?
其实,关于这段内存单元为什么会被改变?我们可以通过后续的学习来了解
这里我就简单引导一下,毕竟我也刚开始学,很多地方还得看后面,如有疑惑,请多包涵
我们先来看这里吧,为什么正好CS:IP的数据存放在这呢?计算机把它们存起来是干什么呢?
我们先从CS:IP的作用分析,CS:IP指向的是下一条要执行的指令的位置
我们把他存起来其实是为了方便我们进行其他操作后(中断),再恢复到原来的步骤
一般,我们把这个行为称为:保护现场(保护上下文环境)
这里还会保存标志寄存器中的内容,后面会学到标志寄存器的具体内容,这里就不多赘述