千家信息网

汇编基础程序编写的方法是什么

发表于:2024-11-22 作者:千家信息网编辑
千家信息网最后更新 2024年11月22日,这篇文章主要介绍"汇编基础程序编写的方法是什么",在日常操作中,相信很多人在汇编基础程序编写的方法是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答"汇编基础程序编写的
千家信息网最后更新 2024年11月22日汇编基础程序编写的方法是什么

这篇文章主要介绍"汇编基础程序编写的方法是什么",在日常操作中,相信很多人在汇编基础程序编写的方法是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答"汇编基础程序编写的方法是什么"的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

源程序

1.1 构成

寄存器与段的关联假设

assume:含义为"假设"。

它假设某一段寄存器和程序中的某一个用 segment … ends 定义的段相关联。

通过assume说明这种关联,在需要的情况下 ,编译程序可以将段寄存器和某一个具体的段相联系。

标号

一个标号指代了一个地址。

codesg:放在segment的前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址。

定义一个段

segment和ends的功能是定义一个段,segment说明一个段开始,ends 说明一个段结束。

segment和ends是一对成对使用的伪指令

一个段必须有一个名称来标识,使用格式为:

段名 segment

段名 ends

一个汇编程序是由多个段组成的,这些段被用来存放代码、数据或当作栈空间来使用。

一个有意义的汇编程序中至少要有一个段,这个段用来存放代码。

程序结束标记

End 是一个汇编程序的结束标记,编译器在编译汇编程序的过程中,如果碰到了伪指令 end,就结束对源程序的编译。

如果程序写完了,要在结尾处加上伪指令end 。否则,编译器在编译程序时,无法知道程序在何处结束。

注意:不要搞混了end和ends。

程序返回

一个程序结束后,将CPU的控制权交还给使它得以运行的程序,我们称这个过程为:程序返回。

如何返回

应该在程序的末尾添加返回的程序段。

mov ax,4c00H

int 21H

程序运行

DOS是一个单任务操作系统。

一个程序P2在可执行文件中,则必须有一个正在运行的程序P1,将P2从可执行文件中加载入内存后,将CPU的控制权交给P2,P2才能得以运行。P2开始运行后,P1暂停运行。

而当P2运行完毕后,应该将CPU的控制权交还给使它得以运行的程序P1,此后,P1继续运行。

1.2 源程序中的"程序"

汇编源程序:

伪指令 (编译器处理)

汇编指令(编译为机器码)

程序:源程序中最终由计算机执行、处理的指令或数据。

注意

我们可以将源程序文件中的所有内容称为源程序,将源程序中最终由计算机执行处理的指令或数据 ,成为程序。

程序最先以汇编指令的形式存在源程序中,经编译、连接后转变为机器码,存储在可执行文件中,

1.3 段结束、程序结束、程序返回

1.4 语法错误和逻辑错误

语法错误

程序在编译时被编译器发现的错误

逻辑错误

程序在编译时不能表现出来的、在运行时发生的错误

2 程序执行的过程

2.1 一个汇编语言程序从写出到最终执行的简要过程:

2.2 连接

作用

当源程序很大时,可以将它分为多个源程序文件来编译,每个源程序编译成为目标文件后,再用连接程序将它们连接到一起,生成一个可执行文件;

程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起,生成一个可执行文件;

一个源程序编译后,得到了存有机器码的目标文件,目标文件中的有些内容还不能直接用来生成可执行文件,连接程序将这此内容处理为最终的可执行信息。

所以,在只有一个源程序文件,而又不需要调用某个库中的子程序的情况下,也必须用连接程序对目标文件进行处理,生成可执行文件。

注意,对于连接的过程,可执行文件是我们要得到的最终结果。

使用汇编语言编译程序对源程序文件中的源程序进行编译,产生目标文件;再用连接程序对目标文件进行连接,生成可在操作系统中直接运行的可执行文件。

2.3 可执行文件

可执行文件中包含两部分内容:

  • 程序(从原程序中的汇编指令翻译过来的机器码)和数据(源程序中定义的数据)

  • 相关的描述信息(比如:程序有多大、要占多少内存空间等)

执行可执行文件中的程序

  • 在操作系统中,执行可执行文件中的程序。

  • 操作系统依照可执行文件中的描述信息,将可执行文件中的机器码和数据加载入内存,并进行相关的初始化(比如:设置CS:IP指向第一条要执行的指令),然后由CPU执行程序。

可执行文件中的程序装入内存并运行的原理

  • 在DOS中,可执行文件中的程序P1若要运行,必须有一个正在运行的程序P2 ,将 P1 从可执行文件中加载入内存,将CPU的控制权交给它,P1才能得以运行;

  • 当P1运行完毕后,应该将CPU的控制权交还给使它得以运行的程序P2

exe的执行过程

实际过程

(1)我们在提示符"C:\masm"后面输入可执行文件的名字"1",按Enter键。

(2)1.exe中的程序运行;

(3)运行结束,返回,再次显示提示符"C:\masm"。

操作过程

操作系统是由多个功能模块组成的庞大 、复杂的软件系统。任何通用的操作系统 ,都要提供一个称为shell(外壳)的程序 ,用户(操作人员)使用这个程序来操作计算机系统工作。

DOS中有一个程序command.com ,这个程序在 DOS 中称为命令解释器,也就是DOS系统的shell。

(1)我们在DOS中直接执行 1.exe 时,是正在运行的command将1.exe中的程序加载入内存。

(2)command设置CPU的CS:IP指向程序的第一条指令(即程序的入口),从而使程序得以运行。

(3)程序运行结束后,返回到command中,CPU继续运行command。

2.4 程序执行过程的跟踪

Debug 可以将程序加载入内存,设置CS:IP指向程序的入口,但Debug并不放弃对CPU 的控制,这样,我们就可以使用Debug 的相关命令来单步执行程序 ,查看每条指令指令的执行结果。

我们在 DOS中用 "Debug 1.exe" 运行Debug对1.exe进行跟踪时,程序加载的顺序是:command加载Debug,Debug加载1.exe。

返回的顺序是:从1.exe中的程序返回到Debug,从Debug返回到command。

EXE文件中的程序的加载过程

总结

程序加载后,ds中存放着程序所在内存区的段地址,这个内存区的偏移地址为 0 ,则程序所在的内存区的地址为:ds:0;

这个内存区的前256 个字节中存放的是PSP,dos用来和程序进行通信。

从 256字节处向后的空间存放的是程序。

所以,我们从ds中可以得到PSP的段地址SA,PSP的偏移地址为 0,则物理地址为SA×16+0。

因为PSP占256(100H)字节,所以程序的物理地址是:

SA×16+0+256= SA×16+16×16=(SA+16)×16+0

可用段地址和偏移地址表示为:SA+10:0。

3 程序编写

3.1 两个基本的问题

计算机是进行数据处理、运算的机器,那么有两个基本的问题就包含在其中:

(1)处理的数据在什么地方?

(2)要处理的数据有多长?这两个问题,在机器指令中必须给以明确或隐含的说明,否则计算机就无法工作。

为了描述上的简洁,在以后的课程中,我们将使用两个描述性的符号 reg来表示一个寄存器,用sreg表示一个段寄存器。

reg的集合包括:ax、bx、cx、dx、ah、al、bh、bl、ch、cl、dh、dl、sp、bp、si、di;

sreg的集合包括:ds、ss、cs、es。

3.2 数据在哪里

机器指令处理的数据所在位置

  • 绝大部分机器指令都是进行数据处理的指令,处理大致可分为三类:读取、写入、运算

  • 在机器指令这一层来讲,并不关心数据的值是多少,而关心指令执行前一刻,它将要处理的数据所在的位置。

  • 指令在执行前,所要处理的数据可以在三个地方:CPU内部、内存、端口

  • 指令举例

汇编语言中数据位置的表达

汇编语言中用三个概念来表达数据的位置。

立即数(idata)

对于直接包含在机器指令中的数据(执行前在cpu 的指令缓冲器中),在汇编语言中称为:立即数(idata ) ,在汇编指令中直接给出。例如:

mov ax,1

add bx,2000h

or bx,00010000b

mov al,'a'

寄存器

指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名。例如:

mov ax,bx

mov ds,ax

push bx

mov ds:[0],bx

push ds

mov ss,ax

mov sp,ax

mov ax,bx

对应机器码:89D8

执行结果:(ax) = (bx)

段地址(SA)和偏移地址(EA)

指令要处理的数据在内存中,在汇编指令中可用[X]的格式给出EA,SA在某个段寄存器中。

存放段地址的寄存器可以是默认的。

mov ax,[0]

mov ax,[bx]

mov ax,[bx+8]

mov ax,[bx+si]

mov ax,[bx+si+8]

段地址默认在ds中

存放段地址的寄存器也可以显性的给出。

mov ax,[bp]

mov ax,[bp+8]

mov ax,[bp+si]

mov ax,[bp+si+8]

段地址默认在ss中

显性的给出存放段地址的寄存器

寻址方式

当数据存放在内存中的时候,我们可以用多种方式来给定这个内存单元的偏移地址,这种定位内存单元的方法一般被称为寻址方式。

3.3 指令处理的数据有多长

8086CPU的指令,可以处理两种尺寸的数据,byte和word。所以在机器指令中要指明,指令进行的是字操作还是字节操作

对于这个问题,汇编语言中用以下方法处理。

(1)通过寄存器名指明要处理的数据的尺寸。

(2)在没有寄存器名存在的情况下,用操作符X ptr指明内存单元的长度,X在汇编指令中可以为word或byte。

(3)其他方法

下面的指令中,寄存器指明了指令进行的是字节操作:

mov al,1

mov al,bl

mov al,ds:[0]

mov ds:[0],al

inc al

add al,100

下面的指令中,寄存器指明了指令进行的是字操作:

mov ax,1

mov bx,ds:[0]

mov ds,ax

mov ds:[0],ax

inc ax add ax,1000

在没有寄存器参与的内存单元访问指令中,用word ptr或byte ptr显性地指明所要访问的内存单元的长度是很必要的。

否则,CPU无法得知所要访问的单元是字单元,还是字节单元

下面的指令中,用word ptr指明了指令访问的内存单元是一个字单元:

mov word ptr ds:[0],1

inc word ptr [bx]

inc word ptr ds:[0]

add word ptr [bx],2

下面的指令中,用byte ptr指明了指令访问的内存单元是一个字节单元:

mov byte ptr ds:[0],1

inc byte ptr [bx]

inc byte ptr ds:[0]

add byte ptr [bx],2

有些指令默认了访问的是字单元还是字节单元,

比如:push [1000H]就不用指明访问的是字单元还是字节单元

因为push指令只进行字操作

3.4 数据处理

在代码段中使用数据

考虑这样一个问题,编程计算以下8个数据的和,结果存在ax 寄存器中:

0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H。

在前面的课程中,我们都是累加某些内存单元中的数据,并不关心数据本身。

可现在我们要累加的就是已经给定了数值的数据。

程序第一行中的 "dw"的含义是定义字型数据。dw即define word。

在这里,我们使用dw定义了8个字型数据(数据之间以逗号分隔),它们所占的内存空间的大小为16个字节。

程序中的指令就要对这8个数据进行累加,可这8个数据在哪里呢?

由于它们在代码段中,程序在运行的时候CS中存放代码段的段地址,所以我们可以从CS中得到它们的段地址

这8个数据的偏移地址是多少呢?

  • 因为用dw定义的数据处于代码段的最开始,所以偏移地址为0,这8 个数据就在代码段的偏移0、2、4、6、8、A、C、E处。

  • 程序运行时,它们的地址就是CS:0、CS:2、CS:4、CS:6、CS:8、CS:A、CS:C、CS:E。

程序中,我们用bx存放加2递增的偏移地址,用循环来进行累加。

在循环开始前,设置(bx)=0,cs:bx指向第一个数据所在的字单元。

每次循环中(bx)=(bx)+2,cs:bx指向下一个数据所在的字单元。

如何让这个程序在编译后可以存系统中直接运行呢?我们可以在源程序中指明界序的入口所在

探讨end的作用:

end 除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。

有了这种方法,我们就可以这样来安排程序的框架:

在代码段中使用栈

完成下面的程序,利用栈,将程序中定义的数据逆序存放

assume cs:codesg

codesgsegment

dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h

?

code ends end

程序的思路大致如下:

程序运行时,定义的数据存放在cs:0~cs:15单元中,共8个字单元。依次将这8个字单元中的数据入栈,然后再依次出栈到这 8 个字单元中,从而实现数据的逆序存放。

问题是,我们首先要有一段可当作栈的内存空间。如前所述,这段空间应该由系统来分配。我们可以在程序中通过定义数据来取得一段空间,然后将这段空间当作栈空间来用

mov ax,cs

mov ss,ax

mov sp,32

我们要讲 cs:16 ~ cs:31 的内存空间当作栈来用,初始状态下栈为空,所以 ss:sp要指向栈底,则设置ss:sp指向cs:32。

比如对于:

dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H

我们可以说,定义了8个字型数据,也可以说,开辟了8个字的内存空间,这段空间中每个字单元中的数据依次是:

0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H。

因为它们最终的效果是一样的

将数据、代码、栈放入不同的段

在前面的内容中,我们在程序中用到了数据和栈,我们将数据、栈和代码都放到了一个段里面。我们在编程的时候要注意何处是数据,何处是栈,何处是代码。这样做显然有两个问题:

(1)把它们放到一个段中使程序显得混乱;

(2)前面程序中处理的数据很少,用到的栈空间也小,加上没有多长的代码,放到一个段里面没有问题。

但如果数据、栈和代码需要的空间超过64KB,就不能放在一个段中(一个段的容量不能大于64 KB,是我们在学习中所用的8086模式的限制,并不是所有的处理器都这样)。

所以,我们应该考虑用多个段来存放数据、代码和栈。

我们用和定义代码段一样的方法来定义多个段,然后在这些段里面定义需要的数据,或通过定义数据来取得栈空间。

程序中"data"段中的数据"0abch"的地址就是:data:6。

我们要将它送入bx中,就要用如下的代码:

mov ax,data

mov ds,ax

mov bx,ds:[6]

我们不能用下面的指令:

mov ds,data

mov ax,ds:[6]

其中指令"mov ds,data" 是错误的,因为8086CPU不允许将一个数值直接送入段寄存器中。

程序中对段名的引用,如指令"mov ds,data"中的"data",将被编译器处理为一个表示段地址的数值。

"代码段"、"数据段"、"栈段"完全是我们的安排

我们在源程序中用伪指令

"assume cs:code,ds:data,ss:stack"将cs、ds和ss分别和code、data、stack段相连。

这样做了之后,CPU是否就会将 cs指向 code,ds 指向 data,ss 指向stack,从而按照我们的意图来处理这些段呢?

当然也不是,要知道 assume 是伪指令,是由编译器执行的,也是仅在源程序中存在的信息,CPU并不知道它们。

若要CPU按照我们的安排行事,就要用机器指令控制它,源程序中的汇编指令是CPU要执行的内容

CPU如何知道去执行它们?

我们在源程序的最后用"end start"说明了程序的入口,这个入口将被写入可执行文件的描述信息,可执行文件中的程序被加载入内存后,CPU的CS:IP被设置指向这个入口,从而开始执行程序中的第一条指令。

标号"start"在"code"段中,这样CPU就将code段中的内容当作指令来执行了。

我们在code段中,使用指令:

mov ax,stack

mov ss,ax

mov sp,16 设置ss指向stack,设置ss:sp指向stack:16, CPU 执行这些指令后,将把stack段当做栈空间来用。 CPU若要访问data段中的数据,则可用 ds 指向 data 段,用其他的寄存器(如:bx)来存放 data段中数据的偏移地址

总之,CPU到底如何处理我们定义的段中的内容,是当作指令执行,当作数据访问,还是当作栈空间,完全是靠程序中具体的汇编指令,和汇编指令对CS:IP、SS:SP、DS等寄存器的设置来决定的。

3.5 模块化实现:call 和 ret 指令

功能:call和ret 指令都是转移指令,它们都修改IP,或同时修改CS和IP。

ret

ret指令用栈中的数据,修改IP的内容,从而实现近转移;

CPU执行ret指令时,进行下面两步操作:

(1)(IP)=((ss)*16+(sp))

(2)(sp)=(sp)+2

retf

retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移;

CPU执行retf指令时,进行下面两步操作:

(1)(IP)=((ss)*16+(sp))

(2)(sp)=(sp)+2

(3)(CS)=((ss)*16+(sp))

(4)(sp)=(sp)+2

可以看出,如果我们用汇编语法来解释ret和retf指令,则:

CPU执行ret指令时,相当于进行:

pop IP

CPU执行retf指令时,相当于进行:

pop IP

pop CS

示例

ret指令

程序中ret指令执行后,(IP)=0,CS:IP指向代码段的第一条指令。

retf指令

程序中retf指令执行后,CS:IP指向代码段的第一条指令。

call 指令

CPU执行call指令,进行两步操作:

(1)将当前的 IP 或 CS和IP 压入栈中

(2)转移

主要应用格式

call 指令不能实现短转移,除此之外,call指令实现转移的方法和 jmp 指令的原理相同

依据位移进行转移的call指令

call 标号(将当前的 IP 压栈后,转到标号处执行指令)

CPU执行此种格式的call指令时,进行如下的操作:

(1) (sp) = (sp) - 2 ((ss)*16+(sp)) = (IP)

(2) (IP) = (IP) + 16位位移

call 标号

16位位移="标号"处的地址-call指令后的第一个字节的地址;

16位位移的范围为 -32768~32767,用补码表示;

16位位移由编译程序在编译时算出。

从上面的描述中,可以看出,如果我们用汇编语法来解释此种格式的 call指令,则: CPU 执行指令"call 标号"时,相当于进行: push IP jmp near ptr 标号

转移的目的地址在指令中的call指令

前面讲解的call指令,其对应的机器指令中并没有转移的目的地址 ,而是相对于当前IP的转移位移。

指令"call far ptr 标号"实现的是段间转移。

CPU执行"call far ptr 标号"这种格式的call指令时的操作:

(1) (sp) = (sp) - 2 ((ss) ×16+(sp)) = (CS) (sp) = (sp) - 2 ((ss) ×16+(sp)) = (IP)

(2) (CS) = 标号所在的段地址 (IP) = 标号所在的偏移地址

从上面的描述中可以看出,如果我们用汇编语法来解释此种格式的 call 指令,则: CPU 执行指令 "call far ptr 标号" 时,相当于进行: push CS push IP jmp far ptr 标号

转移地址在寄存器中的call指令

指令格式:call 16位寄存器

功能:

(sp) = (sp) - 2

((ss)*16+(sp)) = (IP)

(IP) = (16位寄存器)

汇编语法解释此种格式的 call 指令,CPU执行call 16位reg时,相当于进行: push IP jmp 16位寄存器

转移地址在内存中的call指令

转移地址在内存中的call指令有两种格式:

(1) call word ptr 内存单元地址

汇编语法解释: push IP jmp word ptr 内存单元地址 比如下面的指令: mov sp,10h mov ax,0123h mov ds:[0],ax call word ptr ds:[0] 执行后,(IP)=0123H,(sp)=0EH

(2) call dword ptr 内存单元地址

汇编语法解释: push CS push IP jmp dword ptr 内存单元地址 比如,下面的指令: mov sp,10h mov ax,0123h mov ds:[0],ax mov word ptr ds:[2],0 call dword ptr ds:[0] 执行后,(CS)=0,(IP)=0123H,(sp)=0CH

call 和 ret 的配合使用

我们看一下程序的主要执行过程:

(1)前三条指令执行后,栈的情况如下:

(2)call 指令读入后,(IP) =000EH,CPU指令缓冲器中的代码为 B8 05 00; CPU执行B8 05 00,首先,栈中的情况变为:

然后,(IP)=(IP)+0005=0013H。

(3)CPU从cs:0013H处(即标号s处)开始执行。

(4)ret指令读入后:(IP)=0016H,CPU指令缓冲器中的代码为 C3;CPU执行C3,相当于进行pop IP,执行后,栈中的情况为:

(IP)=000EH;

(5)CPU回到 cs:000EH处(即call指令后面的指令处)继续执行。

我们发现,可以写一个具有一定功能的程序段,我们称其为子程序,在需要的时候,用call指令转去执行

call指令转去执行子程序之前,call指令后面的指令的地址将存储在栈中,所以可以在子程序的后面使用 ret 指令,用栈中的数据设置IP的值,从而转到 call 指令后面的代码处继续执行。

这样,我们可以利用call和ret来实现子程序的机制。

子程序的框架

标号: 指令 ret 具有子程序的源程序的框架:

参数和结果传递的问题

子程序一般都要根据提供的参数处理一定的事务,处理后,将结果(返回值)提供给调用者。

其实,我们讨论参数和返回值传递的问题,实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值。

我们设计一个子程序,可以根据提供的N,来计算N的3次方。

这里有两个问题:

(1)我们将参数N存储在什么地方?

(2)计算得到的数值,我们存储在什么地方?

很显然,我们可以用寄存器来存储,可以将参数放到 bx 中 ;

因为子程序中要计算 N×N×N ,可以使用多个 mul 指令,为了方便,可将结果放到 dx 和 ax中。

子程序

说明:计算N的3次方

参数: (bx)=N

结果: (dx:ax)=N∧3

cube:mov ax,bx

mul bx ;用ax与bx相乘

mul bx

ret

用寄存器来存储参数和结果是最常使用的方法。对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰恰相反:

调用者将参数送入参数寄存器,从结果寄存器中取到返回值;

子程序从参数寄存器中取到参数,将返回值送入结果寄存器。

到此,关于"汇编基础程序编写的方法是什么"的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注网站,小编会继续努力为大家带来更多实用的文章!

0