[汇编语言]第一个程序


一、一个源程序从写出到执行的过程

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

  1. 编写汇编源程序
    使用文本编辑器,用汇编语言编写汇编源程序(产生了一个存储源程序的文本文件)
  2. 对源程序进行编译连接
    使用汇编语言编译程序对源程序文件中的源程序进行编译,产生目标文件;再用连接程序对目标文件进行连接,生成可在操作系统中直接运行的可执行文件。

可执行文件包含两部分内容:
(a)程序(从源程序中汇编指令翻译过来的机器码)和数据(源程序中定义的数据)
(b)相关的描述信息(比如程序多大,占多少内存空间等)

这一步工作的结果是产生了一个可在操作系统中运行的可执行文件

  1. 执行可执行文件中的程序
    在操作系统中,执行可执行文件中的程序

操作系统依照可执行文件中的描述信息,将可执行文件中的机器码和数据加载入内存,并进行相关的初始化,然后由CPU执行程序。


二、源程序

2.1 伪指令

下面就是一段简单的汇编语言源程序

assume cs:codesg

codesg segment
	mov ax, 0123H
	mov bx, 0456H
	add ax, bx
	add ax, ax
	mov ax, 4c00H
	int 21H

codesg ends

end

下面对上述程序进行说明:

  1. segment
    在汇编语言源程序中,包含两种指令,一种是汇编指令,一种是伪指令。汇编指令是有对应的机器码的指令,可以被编译为机器指令,最终为CPU所执行。而伪指令没有对应的机器指令,最终不被CPU所执行。伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作。

上述程序中出现了3种伪指令:

XXX segment
...
...
XXX ends

segment和ends是一对成对使用的伪指令,这是在写可被编译器编译的汇编程序时,必须要用到的一对伪指令。segment和ends的功能是定义一个段,segment说明一个段开始,ends说明一个段结束。一个段必须有一个名称来表示,使用格式为:

段名 segment
...
...
段名 ends

比如上述程序中的:

codesg segment	;定义一个段,段的名称为codesg,这个段从此处开始
...
...
codesg ends		;名称为codesg的段到此结束
  1. end

end是一个汇编程序的结束标记,编译器在编译汇编程序的时候,如果碰到了伪指令end,就结束对源程序的编译,所以我们写程序的时候如果程序写完了,一定要在结尾加上伪指令end,否则编译器在编译程序的时候,无法知道程序在何处结束。

注意:end和ends不同,ends是要和segment成对使用的,我们可以理解ends的含义为end segment,是结束一个段,我们此处说明的end是整个程序的结束

  1. assume

假设某一个段寄存器和程序中的某一个用segment...ends定义的段相关联。通过assume说明这种关联,在需要的情况下,编译程序可以将段寄存器和某一个具体的段相联系。assume并不是一条非要深入理解不可的伪指令。

比如在上述程序中,我们用codesg segment ... codesg ends定义了一个名为codesg的段,在这个段中存放代码,所以这个段是一个代码段。在程序的开头,用asume cs:codesg将用作代码段的段codesg和CPU中的段寄存器cs联系起来。

2.2 源程序中的程序

用汇编语言写的源程序,包括伪指令和汇编指令,我们编程的最终目的是为了让计算机完成一定的任务。源程序中的汇编指令组成了最终由计算机执行的程序,而源程序中的伪指令是由编译器来处理的,他们并不实现我们编程的最终目的。这里所说的程序就是指源程序中最终由计算机执行、处理的指令或数据。

注意,以后可以将源程序文件中的所有内容称为源程序,将源程序中最终由计算机执行、处理的指令或数据称为程序。程序最先以汇编指令的形式存在源程序中,经编译、连接后转变为机器码,存储在可执行文件中。如图所示
在这里插入图片描述

2.3 标号

汇编源程序中,除了汇编指令和伪指令外,还有一些标号,比如“codesg”。一个标号指代了一个地址。比如codesg在segment前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址。

2.4 程序的结构

之前我们都是直接在debug中写入汇编指令来写汇编程序,对于十分简短的程序这样子做的确方便,但是对一些大一点的程序,就不能这样子了,我们需要写出能让编译器进行编译的源程序,这样子源程序应该具备起码的结构。

源程序是由一些段构成的。我们可以在这些段中存放代码、数据、或将某个段当作栈空间。我们现在来一步步地完成一个小程序,体会到汇编程序中基本要素和汇编程序简单框架

任务:编程运算2^3,源程序该如何写?
(a)定义一个段,名称为abc

abc segment
...
abc ends

(b)在这个段中写入汇编指令,来实现我们的任务

abc segment

mov ax, 2
add ax, ax
add ax, ax

abc ends

(c)然后,要指出程序在何处结束

abc segment

mov ax, 2
add ax, ax
add ax, ax

abc ends

end

(d)abc被当做代码段来使用,所以应该将abc和cs联系起来

assume cs:abc

abc segment

mov ax, 2
add ax, ax
add ax, ax

abc ends

end

2.5 程序返回

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

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

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

现在我们知道,一个程序结束后,将CPU的控制权交还给使它得以运行的程序,我们称这个过程为:程序返回,那么如何返回?应该在程序的末尾添加返回的程序段。

再来看之前的程序中的两条指令:

mov ax, 4c00H
int 21H

这两条指令所实现的功能就是程序返回,现在只需要知道程序的末尾使用这两条指令就可以实现程序返回。

目的相关指令指令性质指令执行者
通知编译器一个段结束段名 ends伪指令编译时,由编译器执行
通知编译器程序结束end伪指令编译时,由编译器执行
程序返回mov ax,4c00H int 21H汇编指令执行时,由CPU执行

2.6 语法错误和逻辑错误

可见上述我们写的程序还是会引发一些问题,因为程序没有返回,当然这个错误在编译的时候是不能表现出来的,也就是说,程序对于编译器来说是正确的程序

一般来说,程序在编译时被编译器发现的错误是语法错误,比如将程序4.2写成如下这样子就会发生语法错误

aume cs:abc

abc segment
mov ax, 2
add ax, ax
add ax, ax

end

显然程序中有编译器不能识别的aume,而且编译器在编译的过程中也无法知道abc在何处结束。而逻辑错误通常不容易被发现。


三、编辑源程序

可以用任意文本编辑器来编辑源程序,只需要最终将其存储位纯文本文件即可。

(1) 输入edit打开文本编辑器

在这里插入图片描述

(2) 在里面编辑程序
在这里插入图片描述

(3) 将其保存,命名为1.asm后退出edit


四、编译

在上面,我们完成了对源程序的编辑后,得到了一个源程序文件c:\1.asm,可以对他进行编译,生成包含机器代码的目标文件

在编译一个源程序之前,我们首先要找到一个相应的编译器,我们这里采用微软的masm汇编编译器。之后我们按照下面的过程来进行源程序的编译

(a)进入masm
在这里插入图片描述
运行masm后,首先会弹出一个版本信息,然后根据输入将要被编译的源程序的名称,[.asm]表示默认文件的扩展名是.asm,要编译的源程序名称为p1.asm,那么我们这里只需要输入p1即可。但是如果源程序文件不是以asm为扩展名就要输入它的全名,比如源程序的文件名为p1.txt,我们就要输入它的全名。

在输入源程序文件名的时候一定要指明他所在的路径,如果文件就在当前路径下,只需要输入文件名就可以,如果文件在其他目录中,则需要输入路径,在这里我们编译的是C盘根目录下的1.asm,所以此处输入c:\1.asm

(b)输入要编译的源程序文件名称后,按Enter键
在这里插入图片描述

在输入源程序文件名后,程序继续提示我们输入编译出的目标文件的名称,目标文件是我们对一个源程序进行编译要得到的最终结果。此时屏幕上显示1.OBJ,因为我们已经输入了源程序文件名为1.asm,则编译程序默认输出的目标文件名是1.obj,所以可以不必再另行指定文件名,直接按Enter键,编译程序将在当前的目录下生成1.obj

在这里也可以指定生成的目标文件所在的目录,比如想让在编译程序在"C:\windows"下生成目标文件1.obj,则可以输入"c:\windows\1"

(c)确定了目标文件的名称后,屏幕如图所示
在这里插入图片描述
编译程序提示输入列表文件的名称,这个文件是编译器将源程序编译为目标文件的过程中产生的中间结果,可以让编译器不生成这个文件,直接按Enter即可

(d)忽略了列表文件的生成后,屏幕显示如图所示
在这里插入图片描述
编译程序提示输入交叉引用文件的名称,是编译器将源程序编译为目标文件的过程中产生的中间结果,可以让编译器不生成这个文件,直接按Enter即可

(5)忽略了交叉文件的生成后,屏幕显示如图所示

在这里插入图片描述
如图所示,我们对源程序的编译结束,编译器输出的最后两行告诉我们这个源程序没有警告错误和必须要改正的错误


五、连接

在对源程序进行编译得到目标文件后,我们需要对目标文件进行连接,从而得到可执行文件,连接上一节的过程,我们已经对c:\1.asm进行编译得到c:\masm\1.obj,现在将c:\masm\1.obj连接为c:\masm\1.exe

我们使用微软的Overlay Linker连接器

(a)输入link
在这里插入图片描述
首先会显示版本信息,然后提示输入要被连接的目标文件名称,[.OBJ]表示默认文件的扩展名是.obj,要编译的源程序名称为p1.obj,那么我们这里只需要输入p1即可。但是如果源程序文件不是以obj为扩展名就要输入它的全名,比如源程序的文件名为p1.bin,我们就要输入它的全名。

在输入目标文件名的时候,要注意指明所在的路径,这里要连接的文件是当前目录下的1.obj,所以我们输入1

(b)输入要连接的目标文件名后,按Enter键

在这里插入图片描述
输入目标文件名后,程序继续提示我们输入要生成的可执行文件的名称,可执行文件是我们对一个程序进行连接后要得到的最终结果,直接按Enter键,即可在当前目录下生成1.exe文件

这里也可以指定生成的目录,方法如上所示

(c)确定了可执行文件的名称后,屏幕显示如图
在这里插入图片描述
连接程序提示输入映像文件的名称,这个文件是连接程序将目标文件连接为可执行文件过程中产生的中间结果,可以让连接程序不生成这个文件,直接按Enter即可

(d)忽略了映像文件生成后,屏幕显示如图
在这里插入图片描述
连接程序提示输入库文件的名称,库文件里面包含了一些可以调用的子程序,如果程序中调用了某一个库文件中的子程序,就需要在连接的时候,将这个库文件和目标文件连接到一起,生成可执行文件,但是这个程序中没有调用任何子程序,所以可以忽略文件名的输入,直接按Enter即可

(e)忽略库文件的连接后,屏幕显示如图
在这里插入图片描述
连接结束后,最后一行告诉我们这个程序中有一个警告错误,没有栈段,在这里我们不理会这个错误。

连接的作用:

  1. 当源程序很大的时候,可以将他分为多个源程序文件来进行编译,每个源程序编译称为目标文件后,再用连接程序将他们连接到一起,生成一个可执行文件;
  2. 程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起,生成一个可执行文件
  3. 一个源程序编译后,得到了存有机器码的目标文件,目标文件中有些内容还不能直接用来生成可执行文件,连接程序将这些内容处理为最终可执行信息。所以在只有一个源程序文件,而又不需要调用某个库中的子程序的情况下,也必须用连接程序对目标文件进行处理,生成可执行文件

六、以简化的方式进行编译连接

在这里我们使用一种较为间接的方式进行编译连接,如图所示:

在这里插入图片描述
masm c:\1,在masm后面加上被编译的源程序文件的路径、文件名,在命令行的结尾再加上分号,按Enter键,编译器就会对c:\1.asm进行编译,在当前路径下生成1.obj,并在编译的过程中忽略中间文件的产生

在这里插入图片描述
link 1;,在link后面加上被连接的目标文件的路径、文件名,在命令行结尾加上分号,按Enter键,连接程序就会对当前路径下的1.obj进行处理,在当前路径下生成可执行文件1.exe,并在过程中忽略中间文件的产生


七、1.exe的执行

现在,我们的第一个汇编程序加工成了一个可在操作系统下执行的程序我们,现在我们执行下:
在这里插入图片描述
可以发现没有任何结果,那么程序到底运行了吗?——当然运行了

只是从屏幕上不可能看到任何运行结果,我们因为程序没有向显示器输出任何信息。


八、谁将可执行文件的程序装载进入内存并使他运行

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

exe中的程序加载入内存,这个正在运行的程序是什么?它将程序加载入内存后,如何使得程序得以运行?程序运行结束后返回到那里?

这里就需要提到操作系统的外壳

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

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

DOS启动后,先完成其他重要的初始化工作,然后运行command.com,command.com运行后,执行完其他的相关任务后,在屏幕上显示出提示符,后等待用户输入。

用户可以输入要执行的命令(如dr,cd等),这些命令由command执行,command执行完这些命令后,再次显示由当前盘符和路径组成的提示符,等待用户输入

如果用户要执行一个程序,则输入该程序可执行文件的名称,command首先根据文件名找到可执行文件,然后将其加载入内存,设置CS:IP指向程序的入口。然后command停止运行,CPU运行程序,程序运行结束后,返回到command中,command再次显示由当前盘符和路径组成的提示符,等待用户输入
  1. 在DOS中直接指向1.exe时,是正在运行的command,将1.exe中的程序加载入内存
  2. command设置CPU的CS:IP指向程序的第一条指令,从而使程序得以运行
  3. 程序运行结束后,返回到command,CPU继续运行command

在这里插入图片描述


九、程序执行过程的跟踪

可以使用Debug来跟踪一个程序的运行过程,这通常是必须要做的工作,方法如下:

在这里插入图片描述
我们可以用r命令来观察寄存器的情况
在这里插入图片描述
可以看到程序从可执行文件加载入内存后,cx中存放的是程序的长度,1.exe中程序的机器码有15个字节

现在1.exe被装入了内存,接下来查看它的内容,但是我们查看哪里的内容?程序被装在内存的什么地方?

下图展示在DOS系统中.EXE文件中的程序加载过程
在这里插入图片描述
那么我们的程序被装入内存的什么地方?如何得知?

  1. 程序加载后,ds中存放着程序所在内存区的段地址,这个内存区的偏移地址为0,则程序所在的内存区的地址为ds:0
  2. 这个内存区的前256个字节中存放的是PSP,DOS用来程序进行通信。从256字节处向后的空间存放的是程序。

所以从ds中可以得到PSP的段地址SA,PSP的偏移地址为0,则物理地址为SA10H + 0,因为PSP占256字节(100H)所以程序的物理地址为:SA10H + 0 + 256 = SA + 10H:0
图中显示,DS = 075AH,则PSP的地址为075A:0,程序的地址为075A + 10H:0

注意:源程序中的指令是mov ax,0123H,在Debug中记为mov ax,0123是因为debug默认所有数据都是16进制表示。
在这里插入图片描述
我们用U命令看一下其他指令,可以看到从075A:0000 ~ 076A:000E都是程序的机器码

现在开始进行追踪,用T命令单步执行程序中的每一条指令,并观察每条指令的执行结果,到了int21,我们要用P命令执行

在这里插入图片描述
出现这行字表示程序正常结束,返回到Debug中

使用Q命令退出debug,将返回到command中,因为Debug是由command加载运行的

command加载debug,debug加载1.exe,返回的顺序是:从1.exe程序返回到debug,从debug返回到command

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值