(一)基础知识
一、机器语言
机器语言是机器指令的集合。
机器指令展开来讲就是一台机器可以正确执行的命令。(只有0和1)
比如堆栈指令:0101 0000(PUSH AX)
二、汇编语言的产生以及组成
汇编语言的主体是汇编指令。
汇编指令与机器指令的差别在于指令的表示方法上。汇编指令是机器指令便于记忆的书写格式。
汇编指令是机器指令的助记符。
例如:
机器指令:1000100111011000
操作:寄存器BX内容送到AX中
汇编指令:MOV AX,BX
寄存器:简单的讲就是CPU中可以存储数据的器件,一个CPU中有多个寄存器。而上面所说的AX是其中一个寄存器的代号;BX是另一个寄存器的代号。
汇编语言由以下三类组成:
汇编指令(机器码的助记符)
伪指令(由编译器执行)
其他符号(由编译器识别)
汇编语言的核心是汇编指令,它决定了汇编语言的特性。
三、存储器
CPU是计算机的核心部件,它控制整个计算机的运作并进行运算,想要一个CPU工作,就必须向他提供指令和数据。
指令和数据在存储器中存放,也就是平时所说的内存。
磁盘不同于内存,磁盘上的数据或程序如果不读到内存中,就无法被CPU使用。
四、指令和数据
指令和数据是应用上的概念。
在内存或磁盘上,指令和数据没有任何区别,都是二进制信息。
二进制信息:
1000100111011000 ->89D8H (数据)
1000100111011000 -> MOV AX BX(程序)
五、存储单元
存储器被划分为若干个存储单元,每个存储单元从0开始顺序编号。
对于大容量的存储器一般还用以下单位来计量容量(以下用B来代表Byte),微机中常用的计量单位:
1KB = 1024B
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
CPU对存储器的读写
CPU想要进行数据的读写,必须和外部器件(标准的说法是芯片)进行三类信息的交互:
存储单元的地址(地址信息)
器件的选择,读或写命令(控制信息)
读或写的数据(数据信息)
CPU是通过什么将地址、数据和控制信息传到存储芯片中的?
电子计算机能处理、传输的信息都是电信号,电信号当然要用导线传送。
在计算机中有专门连接CPU和其他芯片的导线,通常称为总线。
物理上:可以称为一根根导线的集合。
逻辑上可以划分为:
地址总线
数据总线
控制总线
总线在逻辑上划分的图示:读写流程:(地址总线(存储单元)-->控制总线(读/写)-->数据总线(读数据/写数据))
地址总线(CPU的寻址能力,32位代表有32根地址总线)
CPU是通过地址总线来指定存储单元的。地址总线上能传送多少个不同的信息,CPU就可以对多少个存储单元进行寻址。
补充:如果要有64位的性能,需要有:64位的CPU、64位的系统、64位的软件。
一个CPU有N根地址总线,则可以说这个CPU的地址总线宽度为N。
这样的CPU最多可以寻找2的N次方个内存单元。
数据总线
CPU与内存或其他器件之间的数据传送是通过数据总线来进行的。
数据总线的宽度决定了CPU与外界的数据传送速度。
控制总线
CPU对外部器件的控制是通过控制总线来进行的。在这里控制总线是个总称,控制总线是一些不同控制线的集合。有多少根控制总线就意味着CPU提供了对外部器件有多少种控制。所以控制总线的宽度决定了CPU对外部器件的控制能力。
前面所讲的内存读或写命令是由几根控制总线综合发出的,其中有一根名为读信号输出的控制线负责由CPU向外传送读信号,CPU向该控制线上输出低电平表示将要读取数据;有一根名为写信号输出控制线负责由CPU向外传送写信号。
六、小结
汇编指令是机器指令的助记符,同机器指令一一对应。
每一种CPU都有自己的汇编指令集。
CPU可以直接使用的信息在存储器中存放。
在存储器中指令和数据没有任何区别,都是二进制信息。
存储单元从0开始顺序编号。
一个存储单元可以存储8个bit(用作单位写成”b“),即8位二进制数。
每一个CPU芯片都有许多管脚,这些管脚和总线相连。也可以说,这些管脚引出总线。一个CPU可以引出三种总线的宽度标志了这个CPU的不同方面的性能:
地址总线的宽度决定了CPU的寻找能力。
数据总线的宽度决定了CPU与其他器件进行数据传送时的一次数据传送量。
控制总线宽度决定了CPU对系统中其他器件的控制能力。
什么是内存地址空间?
一个CPU的地址线宽度为10,那么可以寻找1024个内存单元,这1024个可寻到的内存单元就构成了这个CPU的内存地址空间。
主板:每一台PC机中都有一个主板,主板上有核心器件和一些主要器件。这些器件通过总线(地址总线、数据总线、控制总线)相连。
接口卡:计算机系统中,所有可用程序控制其工作的设备,必须受到CPU的控制。CPU对外部设备不能直接控制,如显示器、音箱、打印机等。直接控制这些设备进行工作的是插在扩展插槽上的接口卡。
各类存储器芯片:
从读写属性上看分为两类:随机存储器(RAM)和只读存储器(ROM)
从功能和连接上分类:
随机存储器RAM
装有BIOS的ROM
接口卡上的RAM
装有BIOS的ROM:
BIOS:Basic Input/Output System,基本输入输出系统。BIOS有什么用?就是当我们开机时,电脑上会出现一串一串的数字、字母等,这些都是BIOS作用的结果,一上电就运行BIOS的程序。BIOS是由主板和各类接口卡(如:显卡、网卡等)厂商提供的软件系统,可以通过它利用该硬件设备进行最基本的输入输出。在主板和某些接口卡上插有存储相应的BIOS的ROM。
PC集中各类存储器的逻辑连接
内存空间地址
上述的存储器在物理上是独立的器件。但是他们有以下两个共同点:
都和CPU通过总线相连、CPU对它们进行读或写的时候都通过控制线发出内存读写命令。
将各类存储器看作一个逻辑存储器:
(1)所有的物理存储器被看作一个由若干存储单元组成的逻辑存储器。
(2)每个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间。
(3)CPU在这段地址空间中读写数据,实际上就是在相对应得物理存储器中读写数据。
最终运行程序的是CPU,我们用汇编编程的时候,必须要从CPU角度考虑问题。
(二)寄存器(CPU工作原理)
CPU:一个典型的CPU由运算器、控制器、寄存器等器件组成,这些器件靠内部总线相连。
内部总线:实现CPU内部各个器件之间的联系。
外部总线:实现CPU和主板上其他器件的联系。
通用寄存器
8086CPU有14个寄存器:AX,BX,CX,DX,SI,DI,SP,BP,IP,CS,SS,DS,ES,PSW。
8086CPU所有的寄存器都是16位的,可以存放2个字节。(一个字节=8位)
AX,BX,CX,DX通常用来存放一般性数据被称为通用寄存器。8086上一代CPU中的寄存器都是8位的。为了保证兼容性,这四个寄存器都可以分为两个独立的8位的寄存器使用。
AX可以分为AH和AL。(BX,CX,DX一样的道理)
字(word)在寄存器中的存储
一个字可以存在一个16位的寄存器中,这个字的高位字节和低位字节自然就存在这个寄存器的高8位寄存器和低8位寄存器中。
几条汇编指令
MOV AX,4E20H //将4E20H放入AX中
ADD AX,1406H //将1406H与AX相加
MOV AX,BX //将BX的值放入AX中
当所相加的值超过16位的时候会溢出到哪呢?当超过所限制的位数,超过的位数会自动地被丢失,比如我们一个八位的寄存器,所算出来的值是0158H,这个时候它会报错58H,而丢失01。这里的丢失,是指进位制不能在8位寄存器中报错,但是CPU并不是真的丢弃这个进位值。
物理地址
CPU访问内存单元时要给出内存单元的地址。所有的内存单元构成的存储空间是一个一维的线性空间,这个唯一的地址称为物理地址。
16位结构的CPU,到底什么是16位?
运算器一次最多可以处理16位的数据。
寄存器的最大宽度为16位。
寄存器和运算器之间的通路是16位的。
8086CPU给出物理地址的方法
8086有20位地址总线,可传送20位地址,寻址能力为1M(2的20次方)。
8086内部为16位结构,它只能传送16位的地址,表现出的寻址能力却只有64K。那它如何使用内部的16位数据转换成20位的地址?8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。
CPU中的相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址。
段地址和偏移地址通过内部总线送入一个称为地址加法器的部件。
地址加法器将两个16位地址合并成一个20位的地址。
地址加法器工作原理:地址加法器合成物理地址的方法:
物理地址=段地址*16+偏移地址
段地址*16相对于向左偏移一位。也可以说是数据左移4位。
段的概念
内存并没有分段,段的划分来自于CPU,由于8086CPU用“段地址 * 16+偏移地址=物理地址”的方式给出内存单元的物理地址,使得我们可以用分段的方式来管理内存。段是我们在使用时由于需要才加以描述,在编程时可以根据需要,将若干地址连续的内存单元看作一个段,用段地址 * 16定位段的起始地址(基础地址),用偏移地址定位段中的内存单元。
注意:
段地址 * 16必然是16的倍数,所以一个段的起始地址也一定是16的倍数。
偏移地址为16位,16位地址的寻址能力为64KB,所以一个段的长度最大为64KB。
8.小结与注意
CPU可以用不同的段地址和偏移地址形成同一个物理地址。
给定段地址1000H,用偏移地址寻址,CPU的寻址范围为:10000H~1FFFFH。
在8086PC中,存储单元地址用两个元素来描述,即段地址和偏移地址。
数据在21F60H内存单元中,对于8086PC机的两种描述:
数据存在内存2000:1F60单元中。
数据存在内存的2000段中的1F60H单元中。
可根据需要,将地址连续、起始地址为16的倍数的一组内存单元定义为一个段。
段寄存器
段寄存器就是提供段地址的:8086CPU有4个段寄存器:CS、DS、SS、ES
当8086CPU要访问内存时,由这4个段寄存器提供内存单元的段地址。
CS和IP
CS和IP是8086CPU中最关键的寄存器,它们指示了CPU当前要读取指令的地址。
CS为代码段寄存器
IP为指令指针寄存器。(偏移地址)
8086PC工作过程:
(1)从CS:IP指向内存单元读取指令,读取的指令进入指令缓冲器中。
(2)IP=IP+所读取指令的长度,从而指向下一条指令。
(3)执行指令,转回步骤(1)
在8086CPU加电启动或复位后(即CPU刚开始工作时)CS和IP被设置为CS=FFFFH,IP=0000H。即在8086PC机刚启动时,CPU从内存FFFF0H单元中读取指令执行。
FFFF0H单元中的指令是8086PC机开机后执行的第一条指令。
在任何时候,CPU将CS、IP中的内容当作指令的段地址和偏移地址,用它们合成指令的物理地址,到内存中读取指令码,执行。
如果说,内存中的一段信息曾被CPU执行过的话,那么,它所在的内存单元必然被CS:IP指向过。
如何改变CS、IP的值呢?
mov指令不能用于设置CS、IP的值,8086CPU没有提供这样的功能。8086CPU为CS、IP提供了另外的指令来改变它们的值,也就是转移指令。
同时修改CS、IP的内容:
JMP 段地址:偏移地址
JMP 2AE3:3 JMP 3:0B16
功能:使用指令中给出的段地址修改CS,偏移地址修改IP。
仅修改IP的内容:
JMP 某一合法寄存器
JMP AX ;类似于mov IP,ax JMP BX
功能:用寄存器中的值修改IP。
举例:内存中存放的机器码和对应汇编指令情况:初始:CS=2000H,IP=0000H。当执行如下代码时,内存中的码会怎么变化?
MOV AX,6622 JMP 1000:3 MOV AX,0000 MOV BX,AX JMP BX MOV AX,0123H ....
最后会发现这是一个死循环,一直在内存10000H~10009H中反复横跳。
代码段
对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。
可以将长度为N(N<=64KB)的一组代码,存在一组地址连续、起始地址为16的倍数的内存单元中,这段内存是用来存放代码的,从而定义了一个代码段。
例如以上的代码,这段长度为10字节的字节的指令,存在从123B0H~123B9H的一组内存单元中,我们就可以认为,123B0H~123B9H这段内存单元是用来存放代码的,是一个代码段,它的段地址为123BH,长度为10字节。
小结以及注意
总结一下8086CPU的工作过程:
(1)从CS:IP指向内存单元读取指令,读取的指令进入指令缓冲器。
(2)IP指向下一条指令。(IP=IP+长度)
(3)执行指令。
实验
(1)学会使用debug
使用DOSBox 0.74-3+Debug.exe进行调试:
常用指令:
R命令 查看、改变CPU寄存器的内容 D命令 查看内存中的内容 E命令 改写内存中的内容 U命令 将内存中的机器指令翻译成汇编指令 T命令 执行一条机器指令 A命令 以汇编指令的格式在内存中写入一条机器指令
当我在终端输入:
之后调用d查看内存:要注意修改起始地址。
我们发现在内存中的数字比较杂乱,我们可以将其转为汇编语言:
首先修改CS和IP的值,使其指向我们想要的所在语句:
我指的值是正确的,所以无需修改,若要修改则输入:
r cs r ip
之后我们使用t命令执行所在的语句:
其实t语句的作用就相当于是单步调试,我们可以仔细的观察到内存的变化。
最后可以使用quit,退出调试。
(2)将下面3条指令写入2000:0开始的内存单元中,利用这3条指令计算2的8次方。
修改cs,ip指向的值:
最后当t跳转到ax=0100时,才是我们需要的效果。
(3)查看内存中的内容
PC机主板上的ROM中写有一个生产日期,在内存FFF00H~FFFFFH的某几个单元中,请找到这个生产日期并试图改变它。
最后那一行就是所对应的日期。上面的数字所代表的就是ASCII码的值,比如第六个数30代表的是十进制的47,对应表中的数字1,我们可以使用命令e来改变生产日期:
最后我们再调用命令d可以发现它并没有改变:
也就是我们主板里面的bios是无法更改的。
(4)向内存从B8100H开始的单元中填写数据,如:
-e B810:0 01 01 02 02 03 03 04 04
最后会惊喜的发现我们的调试器会出现这个:
我们可以随意改变任意数字,然后会在显示屏上显示不一样的字符,其实B810是我们显存的地址。
三、寄存器(内存访问)
内存中字的存储(两个字节=一个字)
在0地址处开始存放20000(4E20H),在内存中0号单元是低地址单元,1号单元是高地址单元。也就是说,0号地址存放的是20H,1号地址存放的是4EH。
字型数据:两个字节,比如0地址存放的字型数据为4E20H
字节型数据:一个字节,比如0地址存放的字节型数据为20H
那么,图中1地址字单元中存放的字型数据是多少?答案是124EH。
结论:
任何两个地址连续的内存单元,N号单元和N+1号单元,可以将它们看成两个内存单元,也可以看程一个地址为N的字单元中的高位字节单元和低位字节单元。
DS和[address]
CPU要读取一个昵称单元的时候,必须先给出这个内存单元的地址。
在8086PC中,内存地址由段地址和偏移地址组成。
8086CPU中有一个DS寄存器,通常用来存放要访问的数据的段地址。
例如:我们要读取10000H单元的内容可以用如下程序段进行:
mov bx,1000H ;会自动向左偏移一位 mov ds,bx mov al,[0]
上面三条指令将10000H(1000:0)中的数据读到al中。
mov al,[0]这条指令有三种传送功能:
(1)将数据直接送入寄存器
(2)将一个寄存器中的内存送入另一个寄存器中
(3)mov指令还可以将一个内存单元中的内容送入一个寄存器。
mov指令格式:
mov 寄存器名,内存单元地址
“[...]”表示一个内存单元,“[0]”中的0表示内存单元的偏移地址。在执行命令时,8086CPU自动取DS中的数据为内存单元的段地址。
那如何用mov指令从10000H中读取数据?
10000H表示为1000:0(段地址:偏移地址)
将段地址1000H放入ds
用mov al,[0]完成传送(mov指令中的[]说明操作对象是一个内存单元,[]中的0说明这个内存单元的偏移地址是0,它的段地址默认放在ds中)
那如何把1000H送入ds?
也就是在最开始的时候使用mov将1000H送入,要注意的是8086CPU不支持将数据直接送入段寄存器的操作,ds是一个段寄存器,只能是通用寄存器。所以输送方式应该为:
数据->通用寄存器->段寄存器
所以反过来,如何将数据从寄存器中送入内存单元?
mov bx,1000H mov ds,bx mov [0],al
字的传送
因为8086CPU是16位结构,有16根数据线,所以可以一次性传送16位的数据,也就是一次性传送一个字。
例题:内存中的情况如下图,写出下面指令执行后寄存器ax,bx,cx中的值:
先使用e命令,将我们所需要的值存入内存中:
用命令a将我们的指令输入:
之后对指针进行修改:
之后单步调试,发现程序运行正常:
mov,add,sub指令
我们已经学会了mov指令的几种形式:
mov 寄存器,数据
mov 寄存器,寄存器
mov 寄存器,内存单元
mov 内存单元,寄存器
mov 段寄存器,寄存器
我们也可以根据已知指令推测:
mov 段寄存器,寄存器 --> mov 寄存器,段寄存器
mov 内存单元,寄存器-->mov 内存单元,段寄存器/mov 段寄存器,内存单元
add和sub指令同mov一样,都有两个操作对象:
不可以执行add ds,ax
数据段
对于8086PC机,我们可以根据需要将一组内存单元定义为一个段,可以是代码段也可以是数据段。
我们可以将一组长度为N(N<=64K)、地址连续、起始地址为16倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。其实也就是我们根据需要来分配内存。
比如我们使用123B0H~123B9H这段空间来存放数据:
段地址:123BH
长度:10字节
如何访问数据段中的数据呢?
将一段内存当作数据段,是我们在编程时的一种安排,我们可以在具体操作的时候,用ds存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。
小结
(1)字再内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放再低地址单元中,高位字节存放在高地址单元中。
(2)用mov指令要访问内存单元,可以在mov指令中只给出单元的偏移地址,此时,段地址默认在DS寄存器中。
(3)[address]表示一个偏移地址为address的内存单元。
(4)在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。
(5)mov、add、sub是具有两个操作对象的指令。jmp是具有一个操作对象的指令。
栈
从栈的角度理解栈:
栈是一种具有特殊的访问方式的存储空间。它的特殊就在于,最后进入这个空间的数据,最先出去。
栈有两个基本操作:入栈和出栈(后进先出)
入栈:将一个新的元素放到栈顶
出栈:从栈顶取出一个元素。
8086CPU提供相关的指令来以栈的方式访问内存空间。我们在基于8086CPU贬称的时候,可以将一段内存当作栈来使用。
8086CPU提供入栈和出栈指令(最基本):
PUSH(入栈)
POP(出栈)
push ax ;将寄存器ax中的数据送入栈中 pop ax ;从栈顶取出数据送入ax
8086CPU的入栈和出栈操作都是以字为单位进行的。
CPU如何知道一段内存空间被当作栈使用?
8086CPU中,有两个寄存器:段寄存器SS 存放栈顶的段地址;寄存器SP存放栈顶的偏移地址。任意时刻,SS:SP指向栈顶元素。
执行push和pop的时候,如何知道哪个单元是栈顶单元?
当我们执行push ax操作时会发生以下操作:
(1)SP=SP-2
(2)将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新的栈顶。
当我们执行pop ax操作时会发生以下操作:
(1)将SS:SP指向的内存单元处的数据送入ax中
(2)SP=SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。
如果我们将10000H~1000FH这段空间当作栈,初始状态栈是空的,此时,SS=1000H,SP=?
栈空,SS:SP指向栈空间最高地址单元的下一个单元。
例题:我们将10000H~1000FH这段空间当作栈段,SS=1000H,栈空间大小为16字节,栈最底部的字单元地址为1000:000E。
(1)任意时刻,SS:SP指向栈顶,当栈中只有一个元素的时候:SS=1000H,SP=000EH。
(2)栈为空,就相当于栈中唯一的元素出栈,出栈后,SP=SP+2,SP原来为000EH,加2后SP=10H。
任意时刻,SS:SP指向栈顶元素,当栈为空的时候,栈中没有元素,也就不存在栈顶元素。
所以SS:SP只能指向栈的最底部单元下面的单元,该单元的偏移地址为栈最底部的字单元的偏移地址+2。
栈最底部字单元的地址为1000:000E,所以栈空时,SP=0010H。
注意:
出栈后,SS:SP指向新的栈顶1000EH,pop操作前的栈顶元素,1000CH处的数据依然存在,但是它已不在栈中,当我们使用push等入栈指令后,会在里面写入新的数据,将它覆盖,而不是将它删除。
SS和SP只记录了栈顶的地址,依靠SS和SP可以保证在入栈和出栈时找到栈顶。
栈顶越界问题
当栈满的时候再使用push指令入栈,栈空的时候再使用pop指令出栈,都将发生栈顶超界问题。
栈顶超界是危险的。因为可能会涉及覆盖掉我们内存中一些重要的东西。
那如何解决这个问题呢?
比如说在CPU记录栈顶上限和下限的寄存器,我们可以通过填写这些寄存器来指定栈空间的范围,然后,CPU在执行push指令的时候靠检测栈顶上限寄存器,在执行pop指令的时候靠检测栈顶下限寄存器保证不会超界。但8086并没有这个功能。
当8086CPU工作时,只考虑当前的情况:
当前栈顶在何处
当前要执行的指令是哪一条
push、pop指令
push和pop指令是可以在寄存器和内存之间传送数据的。
栈空间当然也是内存空间的一部分,它只是一段可以以一种特殊的方式进行访问的内存空间。
(1)格式1
push 寄存器:将一个寄存器中的数据入栈。
pop 寄存器:出栈,用一个寄存器接收出栈的数据。
(2)格式2
push 段寄存器:将一个段寄存器中的数据入栈。
pop 段寄存器:出栈,用一个段寄存器接收出栈的数据。
如:
push ds pop es
(3)格式3
push 内存单元:将一个内存单元处的字入栈(栈操作都是以字为单位)。
pop 内存单元:出栈,用一个内存字单元接收出栈的数据。
指令执行时,CPU要知道内存单元的地址,可以在push、pop指令中给出内存单元的偏移地址,段地址在指令执行时,CPU从ds中取得。
例子:将10000H~1000FH这段空间当作栈,初始状态是空的,将AX、BX、DS中的数据入栈。
mov ax,1000H mov ss,ax ;设置栈的段地址 mov sp,0010H ;设置栈顶偏移地址,由于栈为空,所以SP=0010H push ax push bx push ds
将寄存器ax和bx的数值清零有如下方法:
sub ax,ax sub bx,bx ;机器码为2个字节 mov ax,0 mov bx,0 ;机器码为3个字节
结论:
push、pop实质上就是一种内存传送指令,可以在寄存器和内存之间传送数据,与mov指令不同的是,push和pop指令访问的内存单元的地址不是在指令中给出的,而是由SS:SP指出的。
但是push和pop指令同mov指令不同,CPU执行mov指令只需一步操作,就是传送,而执行push、pop指令却要两步操作。
执行push时:先改变sp,后向ss:sp处传送
执行pop时:先读取ss:sp处的数据,后改变sp
需要注意的是:push、pop等栈操作指令,修改的只是sp,也就是说,栈顶的变化范围最大为:0~FFFFH。
提示:
SS、SP指示栈顶,改变SP后写内存的入栈指令,读内存后改变SP的出栈指令。(8086栈机制)
用栈来暂存以后需要恢复的寄存器的内容时,寄存器出栈的顺序要和入栈的顺序相反。
栈段
比如我们将10010H~1001FH这段长度为16字节的内存空间当作栈来用,以栈的方式进行访问。这段空间就可以成为栈段,段地址为1000H,大小为16字节。
将一段内存当作栈段,仅仅是我们在编程时的一种安排,CPU并不会由于这种安排,就在执行push、pop等栈操作指令时就自动地将我们定义的栈段当作栈空间来访问。
问题:如果我们将10000H~1FFFFH这段空间当作栈段,初始状态是空的,此时,SS = 1000H,SP=?
我们将10000H~1FFFFH这段空间当作栈段,SS=1000H,栈空间大小为64KB,栈最底部的字单元地址为1000:FFFE。任意时刻,SS:SP指向栈顶,当栈中只有一个元素的时候,SS=1000H,SP=FFFEH。
栈为空,就相当于栈中唯一的元素出栈,出栈后,SP=SP+2,SP原来为FFFEH,加2后SP=0,所以,当栈为空的时候,SS=1000H,SP=0。(溢出为0)
总结:
首先从栈操作指令所完成的功能的角度上来看,push、pop等指令在执行的时候只修改SP。SS不可以改变
栈顶的变化范围是0~FFFFH,从栈空的时候的SP=0,一直压栈,直到栈满时SP=0;如果再次压栈,栈顶将环绕,覆盖了原来栈中的内容。
我们将一段内存定义为一个段,用一个段地址来指示段,用偏移地址访问段内的单元,这完全取决于我们的安排,如果想要存放数据,那就是数据段,如果想要存放代码,那就是代码段,如果想要设定为栈,那就是栈段,它是根据我们的需求来安排的。
数据段:将它的段地址放在ds中,用mov、add、sub等访问内存单元的指令时,CPU就将我们定义的数据段中的内容当作数据段来访问。
代码段:将它的段地址放在CS中,将段中的第一条指令的偏移地址放在ip中,这样CPU就将执行我们定义的代码段中的指令。
栈段:将它的段地址放在SS中,将栈顶单元的偏移地址放在SP中,这样CPU在需要进行栈操作的时候,比如执行push、pop指令等,就将我们定义的栈段当作栈空间来使用。
总而言之,CPU将内存中的某段内存当作代码,是因为CS:IP指向了那里,CPU将某段内存当作栈,是因为SS:IP指向了那里。
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 ax
以上的代码实现的是将ax,bx进行修改值后,入栈,出栈后交换数值的功能,一段内存可以既是代码的存储空间又可以是数据的存储空间,还可以是栈空间,也可以什么都不是,其实关键就在于CPU中寄存器的设置,即CS、IP、SS、SP、DS的指向。
四、第一个程序
汇编语言的程序可以使用编译器将它们编译成可执行文件(.exe),在操作系统中运行。
编写
对源程序进行编译连接
使用汇编语言编译程序(MASM.EXE)对源程序文件中的源程序进行编译,产生目标文件。
再用连接程序(LINK.EXE)对目标文件进行连接,生成可在操作系统中直接运行的可执行文件。
可执行文件(两部分内容)
程序(从源程序中的汇编指令翻译过来的机器码)和数据(源程序中定义的数据)
相关的描述信息(比如:程序有多大、要占多少内存空间等)
执行可执行文件中的程序
操作系统依照可执行文件中的描述信息,将可执行文件中的机器码和数据加载入内存,并进行相关的初始化(比如:设置CS:IP指向第一条要执行的指令),然后由CPU执行程序。
伪指令
没有对应的机器码的指令,最终不被CPU所执行。伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作。
定义一个段
segment和ends是一对成对使用的伪指令,这是在写可被编译器编译的汇编程序时,必须要用到的一对伪指令。
segment和ends的功能是定义一个段,segment说明一个段开始,ends说明一个段结束。
一个段必须有一个名称来标识,使用格式为:
段名 segment
段名 ends
一个汇编程序是由多个段组成的,这些段被用来存放代码、数据或当作栈空间来使用。
一个有意义的汇编程序中至少有一个段,这个段用来存放代码。
end
End 是一个汇编程序的结束标记,编译器在编译汇编程序的过程中,如果碰到了伪指令end,就结束了对源程序的编译。
assume(寄存器与段的关联假设)
它假设某一段寄存器和程序中的某一个用segment...ends定义的段相关联。
通过assume说明这种关联,在需要的情况下,编译程序可以将段寄存器和某一个具体的段相联系。例如:
assume cs:codesg ;代码段 codesg segment start: mov ax,0123H mov bx,0456H add ax,bx add ax,ax mov ax,4c00h int 21h codesg ends end
汇编源程序
汇编源程序:
伪指令 (编译器处理)
汇编指令 (编译为机器码)
程序:源程序中最终由计算机执行、处理的指令或数据。可以将源程序文件中的所有内容称为源程序,将源程序中最终由计算机执行处理的指令或数据,成为程序。程序最先以汇编指令的形式存在源程序中,经编译、连接后转变为机器码,存储在可执行文件中。
源程序
(1)标号
一个标号指代了一个地址。
codesg:放在segment的前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址。
(2)Dos中的程序运行
DOS是一个单任务操作系统。
一个程序P2在可执行文件中,则必须有一个正在运行的程序P1,将P2从可执行文件中加载入内存后,将CPU的控制权交给P2,P2才能得以运行。P1暂停运行。而当P2运行完毕后,应该将CPU的控制权交还给使它得以运行的程序P1,此后,P1继续运行。
程序返回
一个程序结束后,将CPU控制权交还给使它得以运行的程序,这个过程称为程序返回。
应该在程序的末尾添加返回的程序段:
mov ax,4c00H int 21H
以上两条指令所实现的功能就是程序返回。
(3)汇编程序运行
下载软件MASM,之后写入程序:
.486 .model flat, stdcall option casemap :none includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib includelib \masm32\lib\gdi32.lib includelib \masm32\lib\msvcrt.lib includelib \masm32\lib\masm32.lib include \masm32\include\kernel32.inc include \masm32\include\user32.inc include \masm32\include\gdi32.inc include \masm32\include\windows.inc include \masm32\include\msvcrt.inc include \masm32\include\masm32.inc include \masm32\macros\macros.asm .data .code start: print chr$("Hello Wordld!") print chr$(" ",13,10) mov eax, sval(input("Enter any press to continue...")) end start
创建文件: File -> New, 然后Ctrl+S保存(此处的文件的路径应与刚刚安装的盘符相同, 文件名格式为:xxx.asm)
编译
点击Project -> Assemble ASM file进行编译,编译之后会在同级目录下生成后缀名为obj的文件
链接
点击Project -> Assemble && file进行链接,链接之后会在同级目录下生成后缀名为exe的文件
运行
点击Project -> Run Program, 出现如下图结果
.486
告诉汇编器应该生成486处理的伪代码,也可以使用.386。
.model flat, stdcall
使用平坦内存模式并使用stdcall调用习惯(stdcall指函数的参数从右往左压入,即最后的参数先压入,且函数在结束时清栈),这几乎是所有Windows API函数和dll的标准
option casemap :none
控制字符的映射为大写。为使“Windows.inc”文件正常工作,这里应用为“none”
includelib include
为了使用Windows API的函数, 需要导入dll文件。这里由静态库(.lib)完成,它们使系统能在内存的动态基地址处动态的载入dll。Includelib \masm32\lib\kernel32.lib
我们不只需要包含静态库,还需要包含.inc
文件,这是由l2inc工具根据库文件自动生成的。include \masm32\include\kernel32.inc
特殊的包含文件Windows.inc
,其中包含了Windows API的所有常量和结构的定义。
.data
定义变量的地方
.code
代码区域
start ... end start
表示程序的开始/结束标签。这里不一定要用start, 可以使用任何单词和“end”语句后相同的标签。
转载链接:汇编环境搭建 -- MASM32 - 简书
由于我的系统不兼容,所以无法使用MASM.EXE/LINK.EXE等插件T T。
(4)关于编译和链接
编译:就是将指令转换为机器码。
链接:当源程序很大时,可以将它分为多个源程序文件夹编译,每个源程序编译成为目标文件后,再用链接程序将它们连接到一起,生成一个可执行文件。程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起,生成一个可执行文件。一个源程序编译后,得到了存有机器码的目标文件,目标文件中的有些内容还不能直接用来生成可执行文件,连接程序将这此内容处理为最终的可执行文件。
操作系统的外壳
操作系统是由多个功能模块组成的庞大、复杂的软件系统。任何通过的操作系统,都要提供一个称为shell(外壳)的程序,用户(操作人员)使用这个程序来操作计算机系统工作。
DOS中有一个程序command.com,这个程序再DOS中称为命令解释器,也就是DOS系统的shell。
当在DOS中直接执行.exe时,是正在运行的command将.exe中的程序加载入内存。
command设置CPU的CS:IP指向程序的第一条指令(即程序的入口),从而使程序得以运行。
程序运行结束后,返回到command中,CPU继续运行command。
汇编程序从写出到执行的过程:
编程(edit)-->1.asm-->编译(masm)-->1.obj-->连接(link)-->1.exe-->加载-->内存中的程序-->运行(CPU)
EXE文件中的程序的加载过程
也就是说,我们程序的指令,是在SA+10H开始的。
这个内城区的前256个字节中存放的是PSP,dos用来和程序进行通信。从256字节处向后的空间存放的是程序。
所以,我们从ds中得到的PSP的段地址SA,PSP的段地址SA,PSP的偏移地址为0,则物理地址为SA*16+0。因为PSP占256(100H)字节,所以程序的物理地址是SA16+0+256=SA16+16 * 16=(SA+16) * 16+0。可用段地址和偏移地址表示为:SA+10:0。
程序执行注意
要使用P命令执行int 21。(否则跑飞,下一句为NOP)如果显示“Program terminated normally”,返回到debug中。表示程序正常结束。
返回顺序:从.exe中的程序返回到debug,从debug返回到command。