汇编语言
汇编语言
第一章 基础知识
汇编语言的组成:
==1.汇编指令(核心)==:机器码的助记符,有对应的机器码。
2.伪指令:没有对应的机器码,由编译器执行,计算机并不执行。
3.其他符号:如+、-、*、/等,由编译器 识别,没有对应的机器码。。
储存单元:
==n位的cpu,一个字就有(n/8)个字节==(这应该是第二章的内容,但是我觉得首先要把单位分清楚就放在这里了)
==Byte(储存单元/字节)简写 B==
1KB=1024B 1MB=1024KB 1GB=1024MB 1TB=1024GB
1024 = 10^10
计算机最小单位是bit(比特)如果一个计算机有128个储存单元,编号从0~127
cpu对储存器都读写
CPU 要想进行数据的读写,必须和外部器件(标准的说法是芯片)进行下面 3类信息的交互。
1.存储单元的地址(地址信息);
2.器件的选择,读或写的命令(控制信息);
3.读或写的数据(数据信息)。
CPU 是通过**==总线==**将地址、数据和控制信息传到存储器芯片中的
**==总线==是计算机中专门连接CPU 和其他芯片的导线。从物理上来讲,就是一根根导线的集合。根据传送信息的不同,总线从逻辑上又分为3 类,==地址总线==、==控制总线==和==数据总线==**。
(1)CPU 通过地址线将地址信息3 发出。
(2)CPU 通过控制线发出内存读命令,选中存储器芯片,并通知它,将要从中读取数据。
(3)存储器将3号单元中的数据8通过数据线送入CPU。
地址总线
一个CPU有N根地址线,则可以说这个 CPU 的地址总线的==宽度==为 N。这样的 CPU最多可以寻找2的N 次方个内存单元。最小数是0,最大数是(2^n)-1
图1.4展示了一个具有 1根地址线的CPU向内存发出地址信息11时 10根地址线上传送的二进制信息。
数据总线
CPU 与内存或其他器件之间的数据传送是通过数据总线来进行的。数据总线的==宽度==决定了 CPU 和外界的数据==传送速度==。8 根数据总线一次可传送一个 ==字节==(字节是8进制数据)。**16 根数据总线一次可传送两个字节。
控制总线
CPU 对外部器件的控制是通过控制总线来进行的。在这里控制总线是个总称,控制总线是一些不同控制线的集合。有多少根控制总线,就意味着 CPU 提供了对外部器件的多少种控制。所以,**==控制总线的宽度决定了 CPU 对外部器件的控制能力。==**
前面所讲的内存读或写命令是由几根控制线综合发出的,其中有一根称为“读信号输出”的控制线负责由 CPU 向外传送读信号,CPU 向该控制线上输出低电平表示将要读取数据:有一根称为“写信号输出”的控制线则负责传送写信号。
内存地址空间(概述)
什么是内存地址空间呢?举例来讲,一个 CPU 的地址总线宽度为 10,那么可以寻址1024 个内存单元,**==这 1024 个可寻到的内存单元就构成这个CPU的内存地址空间==**。
主板
在每一台 PC 机中,都有一个主板,主板上有核心器件和一些主要器件,这些器件通过总线(地址总线、数据总线、控制总线)相连。这些器件有 CPU、存储器、外围芯片组、扩展插槽等。扩展插槽上一般插有 RAM内存条和各类接口卡。
接口卡
计算机系统中,所有可用程序控制其工作的设备,必须受到 CPU 的控制。CPU 对外部设备都不能直接控制,如显示器、音箱、打印机等。直接控制这些设备进行工作的是插在扩展插槽上的接口卡。扩展插槽==通过总线==和 ==CPU== 相连,所以==接卡也通过总线同 CPU==相连。CPU 可以直接控制这些接口卡,从而实现 CPU 对外设的间接控制。*==简单地讲,就是CPU通过总线向接口卡发送命令,接口卡根据CPU的命令控制外设进行工作。==*
各类存储器芯片
分为ROM(只读储存器)与RAM(随机储存器)
随机存储器
用于存放供CPU使用的绝大部分程序和数据,主随机存储器一般由两个位置上的RAM 组成,装在主板上 RAM 和插在扩展插槽上的RAM。
装有BIOS(BasicInput/Output System,基本输入/输出系统)的ROM
BIOS 是由主板和各类接口卡(如显卡、网卡等)厂商提供的软件系统,可以通过它利用该硬件设备进行最基本的输入输出。在主板和某些接口卡上插有存储相应BIOS的ROM。
接口卡上的 RAM
某些接口卡需要对大批量输入、输出数据进行暂时存储,在其上装有 RAM。最典型的是显示卡上的 RAM,一般称为显存。显示卡随时将显存中的数据向显示器上输出。换句话说,我们将需要显示的内容写入显存,就会出现在显示器上。
内存地址空间
在图 1.8中,所有的物理存储器被看作一个由若干存储单元组成的逻辑存储器,每个物理存储器在这个逻辑存储器中占有一个地址段,==即一段地址空间==。==CPU 在这段地址空间中读写数据,实际上就是在相对应的物理存储器中读写数据。==
第二章 寄存器 //你猜他为什么会有个“寄”()
在cpu中寄存器进行信息储存
不同的 CPU,寄存器的个数、结构是不相同的。8086CPU 有14 个寄存器,每个寄存器有一个名称。这些寄存器是: AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW。
通用寄存器
8086CPU 的所有寄存器都是 16 位的,可以存放两个字节。AX、BX、CX、DX这4个寄存器通常用来存放一般性的数据,被称为通用寄存器。
8086CPU的上一代 CPU 中的寄存器都是 8位的,为了保证兼容,使原来基于上代CPU编写的程序稍加修改就可以运行在 8086之上,8086CPU的AX、BX、CX、DX这4个寄存器都可分为两个可独立使用的8位寄存器来用:
AX 可分为AH和AL;
BX可分为BH和BL;
CX可分为CH和CL;
DX可分为 DH和DL
AX的低8位(0位7位)构成了AL寄存器,存器高8位(8位15位)构成了AH寄存器。AH 和AL 寄存器是可以独立使用的 8 位存器。
==几条汇编指令==
mov与add
汇编指令 | 控制CPU完成的操作 | 用高级语言的语法描述 |
---|---|---|
mov ax,18 | 将18送入寄存器AX | AX=18 |
mov ah,78 | 将78送入寄存器AH | AH=78 |
add ax,8 | 将寄存器AX中的数值加上8 | AX=AX+8 |
mov ax,bx | 将寄存器BX中的数据送入寄存器AX | AX-BX |
add ax,bx | 将AX和BX中的数值相加,结果存在AX中 | AX-AX+BX |
在进行数据传送或运算时,要注意指令的==两个操作对象的位数应当是一致的==
物理地址
CPU 访问内存单元时,要给出内存单元的地址。所有的内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间中都有唯一的地址,我们将这个唯一的地址称为物理地址。
CPU 通过地址总线送入存储器的,必须是一个内存单元的物理地址。在 CPU 向地址总线上发出物理地址之前,必须要在内部先形成这个物理地址。不同的 CPU 可以有不同的形成物理地址的方式。我们现在讨论 8086CPU 是如何在内部形成内存单元的物理地址的。
16位结构cpu(典型8086)
概括地讲,16 位结构(16 位机、字长为 16 位等常见说法,与 16 位结构的含相同)描述了一个CPU具有下面几方面的结构特性。
1.运算器一次最多可以处理 16位的数据
2.寄存器的最大宽度为16位;
3.寄存器和运算器之间的通路为 16位
8086 是 16 位结构的 CPU,这也就是说,在 8086 内部,能够一次性处理、传输、暂时存储的信息的最大长度是 16 位的。内存单元的地址在送上地址总线之前,必须在 CPU中处理、传输、暂时存放,对于 16位 CPU,能一次性处理、传输、暂时存储 16位的地址。
8086cpu给出物理地址的方法
8086CPU有20位地址总线,可以传送20位地址,达到1MB寻址能力。8086CPU又是16位结构,在内部一次性处理、传输、暂时存储的地址为 16 位。
8086CPU采用一种在内部用两个 16位地址合成的方法来形成一个20位的物理地址8086CPU相关部件的逻辑结构如图2.6所示。
当8086CPU要读写内存时:
1.CPU中的相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址:
2.段地址和偏移地址通过内部总线送入一个称为地址加法器的部件;
3.地址加法器将两个16位地址合成为一个20位的物理地址;
4**.地址加法器通过内部总线将 20位物理地址送入输入输出控制电路**;
5.输入输出控制电路将 20位物理地址送上地址总线:
6.20位物理地址被地址总线传送到存储器。
地址加法器采用物理地址=段地址X16+偏移地址的方法用段地址和偏移地址合成物理地址。例如,8086CPU 要访问地址为 123C8H 的内存单元,此时,地址加法器的工作过程如图 2.7 所示。
==一个数据的十六进制形式左移1位,相当于乘以16:一个数据的十进形式左移1位,相当于乘以10;一个X进制的数据左移1位,相当于乘以X。==
“段地址X16+偏移地址=物理地址”的本质含义
“段地址x16+偏移地址=物理地址”的本质含义是:CPU 在访问内存时,用一个**==基础地址==(段地址X16)和一个相对于基础地址的==偏移地址==相加,给出内存单元的物理地址。
更一般地说,8086CPU的这种寻址功能是==“基础地址+偏移地址=物理地址”==(这就是本质,请牢记它**)寻址模式的一种具体实现方案。8086CPU 中,段地址x16 可看作是基础地址。
段的概念
其实,内存并没有分段,段的划分来自于 CPU,由于 8086CPU 用“基础地址(段地址X16)+偏移地址=物理地址”的方式给出内存单元的物理地址,使得我们可以用分段的方式来管理内存。
以后,在编程时可以根据需要,将若干地址连续的内存单元看作一个段,用段地址X16 定位段的起始地址(基础地址),用偏移地址定位段中的内存单元。有两点需要注意:==段地址X16 必然是 16 的倍数,所以一个段的起始地址也一定是 16 的倍数==: ==偏移地址为16位,16位地址的寻址能力为 64KB,所以一个段的长度最大为 64KB==
段寄存器
什么部件提供段地址。段地址在 8086CPU 的段寄存器中存放。8086CPU 有 4 个段存器:CS、DS、SS、ES。当8086CPU 要访问内存时由这4个段寄存器提供内存单元的段地址。
CS和IP
CS和IP是8086CPU中两个最关键的存器,它们指示了CPU 当前要读取指令的地址。CS 为代码段寄存器,IP为指令指针寄存器。
在8086PC 机中,任意时刻,设 CS 中的内容为M,IP 中的内容为N,8086CPU 将从内存Mx16+N单元开始,读取一条指令并执行。
也可以这样表述:8086机中,任意时刻,CPU将CS:IP 指向的内容当作指令执行图2.10展示了8086CPU 读取、执行指令的工作原理(图中只包括了和所要说明的问题
密切相关的部件,图中数字都为十六进制)。(没写完,未完待续)
==8086CPU 的工作过程可以简要描述如下==
==1.从 CS:IP 指向的内存单元读取指令,读取的指令进入指令缓冲器==
==2.IP=IP+所读取指令的长度,从而指向下一条指令;==
==3.执行指令。转到步骤(1),重复这个过程。==
在 8086CPU加电启动或复位后(即 CPU 刚开始工作时)CS 和IP被设置为CS=FFFFH,IP=0000H,即在 8086PC 机刚启动时,CPU 从内存 FFFF0H 单元中读取指执行,FFFF0H单元中的指令是8086PC机开机后执行的第一条指令。
==修改CS、IP的指令==
mov 指令==不能==用于设置 CS、IP 的值
==jmp 指令==可以修改CS、IP 的指令
==jmp语法:“jmp 段地址:偏地址”== jmp 2AE3:3,执行后:CS=2AE3H,IP=0003H,CPU将从2AE33H 处读取指令
==jmp ax,在含义上好似:mov IP,ax==
代码段
前面讲过,对于 8086PC 机,在编程时,可以根据需要,将一组内存单元定义为一个段。我们可以将长度为 N(N≤64KB)的一组代码,存在一组地址连续、起始地址为 16 的倍数的内存单元中,我们可以认为,这段内存是用来存放代码的,从而定义了一个代码段
==查看cpu和内存,用机器指令和汇编指令编程(dosbox)==
==R命令:查看、修改CPU中寄存器的内容==
==D命令:查看内存中的内容==
==E命令:修改内存中的内容(可以写入数据、指令,在内存中,它们实际上没有区别)==
==U命令:将内存中的内容解释为机器指令和对应的汇编指令==
==T命令:执行CSIP指向的内存单元处的指令==
==A命令:以汇编指令的形式向内存中写入指令==
第三章 寄存器(内存访问)
内存中字的存储
字单元:即存放一个字型数据**(16 位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元中存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节**。
DS 和[address]
DS 寄存器,通常用来存放要访问数据的段地址。
1 | mov bx,1000H |
上面的3条指令将10000H(1000:)中的数据读到al中
8086CPU ==不支持==将数据直接送入段寄存器的操作,ds 是一个段寄存器,所以mov ds,1000H这条指是非法的。只好用一个寄存器来进行中转
字的传送
8086CPU是16 位结构,有 16 根数据线,所以,可以一次性传送 16 位的数据,也就是说可以一次性传送一个字。只要在 mov 指令中给出 16 位的存器就可以进行 16 位数据的传送了。
1 | mov ax,1000H |
==mov、add、sub 指令==
前面我们用到了mov、add、sub 指令,它们都带有两个操作对象
1 | mov ax,8 ;mov 寄存器,数据 |
1 | add ax,8 ;add 寄存器,数据 |
数据段
对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。我们可以将一组长度为 N(N<64KB)、地址连续、起始地址为 16的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。
==栈==
**==栈==是一种具有特殊的访问方式的存储空间。它的特殊性就在于,==最后进入==这个空间的数据,==最先出去==**。
==CPU 提供的栈机制==
==PUSH==(入栈) push ax 表示将寄存器 ax 中的数据送入栈中,
==POP==(出栈) pop ax 表示从顶取出数据送入 ax。
1 | mov ax,0123H |
==段寄存器 SS==:栈顶的段地址存放在SS中,相关规则与ds相同 mov ss,1000h非法
==寄存器 SP==:栈顶的偏移地址存放在SP中。
==任意时刻,SS:SP 指向栈顶元素。==
入栈时,栈顶从==高地址向低地址=方向增长。
栈顶超界的问题
push超界
pop超界
这也就是说,8086CPU 只知道栈顶在何处(由SS:SP 指示),而不知道我们安排的栈空间有多大。8086CPU 它只考虑当前的情况:当前的栈顶在何处、当前要执行的指令是哪一条。
我们在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界,执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。
push、pop 指令
push 寄存器 将一个寄存器中的数据入栈
pop 寄存器;出栈 用一个寄存器接收出栈的数据
push 段寄存器 将一个段寄存器中的数据入栈
pop 段寄存器 出栈,用一个段寄存器接收出栈的数据
push 内存单元 将一个内存字单元处的字入栈(注意:栈操作都是以字为单位)
pop 内存单元 出栈,用一个内存字单元接收出栈的数据
栈段
对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。我们可以将长度为 N(N64KB)的一组地址连续、起始地址为 16的倍数的内存单元,当作栈空间来用,==从而定义了一个栈段==。比如,我们将10010H~1001FH这段长度为 16 字节的内存空间当作栈来用,以栈的方式进行访问。这段空间就可以称为一个栈段,段地址为 1001H,大小为 16 字节。
第四章:第一个程序
一个源程序从写出到执行的过程-
第一步:编写汇编源程序
使用文本编辑器(如 Edit、记事本等),用汇编语言编写汇编源程序。
这一步工作的结果是产生了一个存储源程序的文本文件。
第二步:对源程序进行编译连接。
使用汇编语言编译程序对源程序文件中的源程序进行编译,产生目标文件;再用连接程序对目标文件进行连接,生成可在操作系统中直接运行的可执行文件。
可执行文件包含两部分内容
1.程序(从源程序中的汇编指令翻译过来的机器码)和数据(源程序中定义的
2.相关的描述信息(比如,程序有多大、要占用多少内存空间等)
结果:产生了一个可在操作系统中运行的可执行文件。
第三步:执行可执行文件中的程序
在操作系统中,执行可执行文件中的程序
操作系统依照可执行文件中的描述信息,将可执行文件中的机器码和数据加载入内存,并进行相关的初始化(比如设置 CS:IP 指向第一条要执行的指),然后由 CPU 执行程序。
源程序
1 | assume cs:codesg |
在汇编语言源程序中,包含两种指令,一种是==汇编指令==,一种是**==伪指令==**
**==汇编指令==是有对应的机器码的指令,可以被编译为机器指令,最终为 ==CPU 所执行==**。
==伪指令==没有对应的机器指令,最终不被 CPU 所执行,伪指令是由==编译器==来执行的指令,编译器根据伪指令来进行相关的编译工作。
==1.伪指令==
1.segment和ends是一对成对使用的伪指令,这是在写可被编译器编译的汇编程序时必须要用到的一对伪指令。segment 和ends的功能是==定义一个段==。segment 说明一个段开始,ends 说明一个段结束。一个段必须有一个名称来标识,使用格式为:
1 | 段名 segment |
2.end 是一个汇编程序的==结束标记==,编译器在编译汇编程序的过程中,如果碰到了伪指令 end,就结束对源程序的编译。所以,在我们写程序的时候,如果程序写完了,要在结尾处加上伪指令end。否则,编译器在编译程序时,无法知道程序在何处结束。
==注意,不要搞混了end和ends==,ends 是和segment 成对使用的,标记一个段的结束ends的含义可理解为“end segment”。我们这里讲的end 的作用是标记整个程序的结束。
3.assume
这条伪指令的含义为“假设”。它假设某一段寄存器和程序中的某一个用segment…ends 定义的段相关联。通过 assume 说明这种关联,在需要的情况下,编译程序可以将段寄存器和某一个具体的段相联系。assume 并不是一条非要深入理解不可的伪指令,以后我们编程时,记着用 assume 将有特定用途的段和相关的段寄存器关联起来即可。
比如,在程序4.1中,我们用 codesg segment … codesg ends 定义了一个名为 codseg的段,在这个段中存放代码,所以这个段是一个代码段。在程序的开头,用 ==assume cs:codesg==将用作代码段的段 ==codesg 和CPU中的段存器cs 联系起来==。
==2.源代码中的程序==
这里所说的程序就是指源程序中最终由计算机执行、处理的指令或数据。
==注意==,以后可以将源程序文件中的==所有内容称为源程序==,将源程序中==最终由计算机执行、处理的指令或数据,称为程序==。程序最先以汇编指令的形式存在源程序中,经编译、连接后转变为机器码,存储在可执行文件中。
==3.标号==
汇编源程序中,除了汇编指令和伪指令外,还有一些标号,比如“codesg’个标号指代了一个地址。比如 codesg 在 segment 的前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址。
==4.程序的结构==
我们现在讨论一下汇编程序的结构。在前3章中,我们都是通过直接在 Debug 中写入汇编指令来写汇编程序,对于十分简短的程序这样做的确方便。可对于大一些的程序,就不能如此了。我们需要写出能让编译器进行编译的源程序,这样的源程序应该具备起码的结构。
源程序是由一些段构成的。我们可以在这些段中存放代码、数据、或将某个段当作栈空间。我们现在来一步步地完成一个小程序,从这个过程中体会一下汇编程序中的基本要素和汇编程序的简单框架。
任务:编程运算23。源程序应该怎样来写呢?
(1)我们要定义一个段,名称为 abc。
1 | abc segment |
(2) 在这个段中写入汇编指令,来实现我们的任务
1 | abc segment |
(3)然后,要指出程序在何处结束
1 | abc segment |
(4) abc 被当作代码段来用,所以,应该将 abc 和 cs 联系起来。(当然,对于这个程序,也不是非这样做不可。)==//4.2==
1 | assume cs:abc |
==5.程序返回==
我们的程序最先以汇编指令的形式存在源程序中,经编译、连接后转变为机器码,存储在可执行文件中,那么,它怎样得到运行呢?
下面,我们在 DOS(一个单任务操作系统)的基础上,简单地讨论一下这个问题
一个程序 P2在可执行文件中,则必须有一个正在运行的程序 P1,将 P2从可执行文件中加载入内存后,将CPU 的控制权交给 P2,P2才能得以运行。P2开始运行后,P1暂停运行。
而当P2 运行完毕后,应该将 CPU 的控制权交还给使它得以运行的程序 P1,此后P1继续运行。
现在,我们知道,一个程序结束后,将 CPU的控制权交还给使它得以运行的程序,我们称这个过程为:程序返回。那么,如何返回呢?应该在程序的末尾添加返回的程序段。
我们回过头来,看一下程序 4.1 中的两条指令:
1 | mov ax,4c00H |
这两条指令所实现的功能就是程序返回
在目前阶段,我们不必去理解 int 21H 指的含义,和为什么要在这条指令的前面加上指令mov ax,4c00H。我们只要知道,在程序的末尾使用这两条指令就可以实现程序返回。
==6.语法错误和逻辑错误==
可见,程序 4.2在运行时会引发一些问题,因为程序没有返回。当然,这个错误在编译的时候是不能表现出来的,也就是说,程序 4.2 对于编译器来说是正确的程序。
般说来,程序在编译时被编译器发现的错误是语法错误,比如将程序 4.2 写成如下这样就会发生语法错误:
1 | aume cs:abc |
显然,程序中有编译器不能识别的aume,而且编译器在编译的过程中也无法知道abc段到何处结束。
在源程序编译后,在运行时发生的错误是逻辑错误。语法错误容易发现,也容易解决。而逻辑错误通常不容易被发现。不过,程序 4.2 中的错误却显而易见,我们将它改正过来:
1 | assume cs:abc |
编辑源程序
==编译==
1.dosbox设置虚拟c盘
2.输入C:\masm
3.如果已经将d:/asm/MASM设置为虚拟c盘,要运行其中的.ASM在虚拟化c盘的目录下,我们需要将那个文件夹视为c盘,输入c:1.asm
4.obj(目标文件)文件的生成
不输入默认在1.asm的所在文件夹内生成1.obj
5.列表文件的生成
不输入输出地址即为不生成
6.交叉引用文件的生成
不输入输出地址即为不生成
7.编译结束
连接
1.2.步骤与上同理
3.输入link.exe
4.如果是obj文件输入文件名即可,非obj文件需要带上文件后缀
5.因为已经确定了目标文件名为 1.bj,则程序默认要输出的可执行文件名为1.EXE,所以可以不必再另行指定文件名。直接按 Enter 键,编译程序将在当前的目录下,生成1.EXE文件。
6.忽略映像文件生成,直接【enter】
7.库文件连接,忽略直接【enter】
警告为:没有栈段(后文再叙)
连接的作用:
(1)当源程序很大时,可以将它分为多个源程序文件来编译,每个源程序编译成为目标文件后,再用连接程序将它们连接到一起,生成一个可执行文件;
(2)==程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起,生成一个可执行文件;==
(3)一个源程序编译后,得到了存有机器码的目标文件,目标文件中的有些内容还不能直接用来生成可执行文件,连接程序将这些内容处理为最终的可执行信息。所以,在==只有一个源程序文件==,而==又不需要调用某个库中的子程序==的情况下,也==必须用==连接程序对目标文件进行处理,生成可执行文件。
注意,对于连接的过程,可执行文件是我们要得到的最终结果。
以简化的方式进行编译和连接
简化编译 C:\>masm c:\1;
简化连接 C:\>link 1;
==自动忽略中间文件的生成==
1.exe的执行
程序执行过程的追踪
第五章[BX]和 loop 指令
1.[bx]和内存单元的描述
要完整地描述一个内存单元,需要两种信息:==1内存单元的地址==;==2内存单元的长度==
mov ax,[bx]
==将一个内存单元的内容送入 ax==,这个内存单元的长度为 2 字节(字单元),存放一个字,==偏移地址在 bx中==,==段地址在 ds 中==。
2.我们定的描述性的符号:“()”
(ax)表示ax中的内容、(al)表示al中的内容
(20000H)表示内存 20000H 单元的内容(()中的内存单元的地址为物理地址);
((ds)*16+(bx))表示:ds 中的内容为ADR1,bx中的内容为 ADR2,内存ADR1*16+ADR2单元的内容
也可以理解为:ds 中的 ADR1 作为段地址,bx 中的 ADR2作为偏移地址,内存ADR1:ADR2单元的内容。
注意,“( )”中的元素可以有 3 种类型:==1.寄存器名==;==2.段寄存器名==:==3.内存单元的物理地址==(一个 20位数据)。比如:
**(ax)、(ds)、(al)、 (cx)、(20000H)、((ds)*16+(bx))等是正确**的用法;
(2000:0)、((ds):1000H)等是不正确的用法。
我们看一下(X)的应用,比如,
(1)ax中的内容为0010H,可以这样来描述:(ax)=0010H;
(2) 2000:1000 处的内容为 0010H,可以这样来描述:(21000H)=0010H;
(3)对于mov ax,[2]的功能,可以这样来描述:(ax)=((ds)16+2);
(4)对于 mov [2],ax的功能,可以这样来描述:((ds)*16+2)=(ax);
(5)对于add ax,2的功能,可以这样来描述:(ax)=(ax)+2;
(6)对于add ax,bx的功能,可以这样来描述:(ax)=(ax)+(bx);
(7)对于 push ax的功能,可以这样来描述:(sp)=(sp)-2 ((ss)*16+(sp))=(ax)
(8)对于 pop ax的功能,可以这样来描述:(ax)=((ss)*16+(sp)) (sp)=(sp)+2
“(X)”所表示的数据有两种类型:1.字节;2.字。
(al)、(bI)、(cl)等得到的数据为字节型;
(ds)、(ax)、(bx)等得到的数据为字型;
(al)=(20000H),则(20000H)得到的数据为字节型;
(ax)=(20000H),则(20000H)得到的数据为字型。
3.约定符号idata 表示常量
5.1[BX]
1 | inc bx |
含义:bx中的内容加1。
5.2 loop指令
格式
(1)在 ==cx 中存放循环次数==
(2)==loop 指令中的标号所标识地址要在前面==
(3)==要循环执行的程序段,要写在标号和 loop 指令的中间==
1 | mov cx,循环次数 |
例子
1 |
|
(1) 标号**==s==**
在汇编语言中,标号代表一个地址,程序中有一个标号 ==s==。它实际上标识了一个地址,这个地址处有一条指令:==add ax,ax==。
(2) loop s
CPU执行 loops的时候,要进行两步操作:
==1.(cx)=(cx)-1;==
==2.判断 cx中的值,不为 0则转至标号s所的地址处执行(这里的指是 add ax,ax)如果为零则执行下一条指令(下一条指令是mov ax,4c00h)。==
(3) 以下3条指令
1 | mov cx,11 |
执行 loop s 时,首先要将(cx)减 1,然后若(cx)不为0,则向前转至 s 处执行add ax,ax。所以,可以利用cx来控制add ax,ax 的执行次数。
5.3 在Debug 中跟踪用 loop 指令实现的循环程序
1 | assume cs:code |
循环程序段从CS:0012开始,我们不想再一步步地跟踪CS:0012前面的指令,从 CS:0012 处开始跟踪。**==可以这样来使用g 命令,“g 0012”==**
当然,也可以用g命令来达到目的,可以用“g 0016”直接执行到 CS:0016 处
我们希望将循环一次执行完。可以使用 p 命令来达到目的。再次遇到loop 指令时,使用p命令来执行,Debug 就会自动重复执行循环中的指令,直到(cx)=0为止。
5.4 Debug 和汇编编译器 masm 对指令的不同处理
“mov al,[0]”,含义:(al)=0,将常量0送入al中(与mov al,0含义相同)
“mov al,ds:[0]”,含义:(al)=((ds)*16+0),将内存单元中的数据送入al中;
“mov al,[bx]”,含义:(al)=((ds)*16+(bx)),将内存单元中的数据送入al中
“mov al,ds:[bx]”,含义:与“mov al,[bx]”相同。
从上面的比较中可以看出:
(1)在汇编源程序中,如果用指令访问一个内存单元,则在指令中必须用“[…]”来表示内存单元,如果在“[]”里用一个常量 idata 直接给出内存单元的偏移地址,就要在“[]”的前面显式地给出段地址所在的段寄存器。比如
mov al,ds:[0]
如果没有在“[]”的前面显式地给出段地址所在的段寄存器,比如
mov al,[0]
那么,编译器masm将把指令中的“[idata]”解释为“idata”
(2) 如果在“[]”里用寄存器,比如 bx,间接给出内存单元的偏移地址,则段地址默认在 ds 中。当然,也可以显式地给出段地址所在的段寄存器。
5.5 loop 和[bx]的联合应用
计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中
1 | assume cs:code |
8位字节型数累加结果存入16位字型dx中
1.将8位字节型数据存入中间暂存16位字型ax中,
1 | mov al,[bx] |
2.将16位ax中的数据转入到dx中
1 | add dx,ax |
3.设计循环,可把bx(bx已经初始化为0)当作为中间变量,在循环的末尾加上一句
1 | inc bx |
5.6段前缀
用于显式地指明内存单元的段地址的“ds:” “cs:” “ss:” “es:”在汇编语言中称为段前缀
例如mov ax,ds:[bx] 中的ds:
5.7 一段安全的空间
(1)我们需要直接向一段内存中写入内容:
(2)这段内存空间不应存放系统或其他程序的数据或代码,否则写入操作很可能引发错误;
(3) DOS 方式下,一般情况,0:20~0:2ff 空间中没有系统或其他程序的数据或代码;(可以先用debug检查一下)
(4)以后,我们需要直接向一段内存中写入内容时,就使用 0:200~0:2ff这空间
5.8段前缀的使用
将内存ffff:0ffff:b单元中的数据复制到0:2000:20b 单元中
0:2000:20b→0020:00020:b
1 | assume cs:code |
第6章包含多个段的程序
6.1在代码段中使用数据
我们若要 CPU从何处开始执行程序,只要在源程序中用“end 标号”指明就可以了。
有了这种方法,就可以这样来安排程序的框架:
1 | assume cs:code |
考虑这样一个问题,编程计算以下8个数据的和,结果存在ax寄存器中:
0123h、0456h、0789h、0abch、0defh、0fedh、0cbah、0987h
1 | assume cs:code |
程序中的“dw”的含义是定义字型数据。**dw 即“defineword’’**在这里,使用 dw 定义了 8 个字型数据(数据之间以号分隔),它们所占的内存空间的大小为16个字节。
6.2在代码段中使用栈
1 | assume cs:codesg |
监测点6.1
cs是代码段寄存器,所以改写程序中的数据就是改写cs:ip的数据所以要mov cs:[bx],ax
6.3将数据、代码、栈放入不同的段
1 | assume cs:code,ds:data,ss:stack |
(1)定义多个段的方法
1 | assume 寄存器名:[name] |
(2)对段地址的引用
要把data:6送入bx中
要用下面代码
1 | mov ax,data |
我们不能用下面的指令:
1 | mov ds,data |
(3)“代码段”数据段”“栈段”完全是我们的安排
CPU 到底如何处理我们定义的段中的内容,是当作指令执行,当作数据访问,还是当作栈空间,完全是靠程序中具体的汇编指令,和汇编指令对 CS:IP、SS:SP、DS 等寄存器的设置来决定的。