程序
1 源程序
1.1 构成
寄存器与段的关联假设
- assume:含义为“假设”。
- 它假设某一段寄存器和程序中的某一个用 segment … ends 定义的段相关联。
- 通过assume说明这种关联,在需要的情况下 ,编译程序可以将段寄存器和某一个具体的段相联系。
标号
定义一个段
程序结束标记
程序返回
程序运行
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的执行过程
实际过程
操作过程
- 操作系统是由多个功能模块组成的庞大 、复杂的软件系统。任何通用的操作系统 ,都要提供一个称为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。
总结
3 程序编写
3.1 两个基本的问题
- 计算机是进行数据处理、运算的机器,那么有两个基本的问题就包含在其中:(1)处理的数据在什么地方?(2)要处理的数据有多长?这两个问题,在机器指令中必须给以明确或隐含的说明,否则计算机就无法工作。
- 为了描述上的简洁,在以后的课程中,我们将使用两个描述性的符号 reg来表示一个寄存器,用sreg表示一个段寄存器。
3.2 数据在哪里
机器指令处理的数据所在位置
汇编语言中数据位置的表达
汇编语言中用三个概念来表达数据的位置。
1、立即数(idata)
2、寄存器
-
指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名。例如:
- 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)
3、段地址(SA)和偏移地址(EA)
存放段地址的寄存器也可以显性的给出。
显性的给出存放段地址的寄存器
寻址方式
当数据存放在内存中的时候,我们可以用多种方式来给定这个内存单元的偏移地址,这种定位内存单元的方法一般被称为寻址方式。
3.3 指令处理的数据有多长
8086CPU的指令,可以处理两种尺寸的数据,byte和word。所以在机器指令中要指明,指令进行的是字操作还是字节操作
对于这个问题,汇编语言中用以下方法处理。
在没有寄存器参与的内存单元访问指令中,用word ptr或byte ptr显性地指明所要访问的内存单元的长度是很必要的。
有些指令默认了访问的是字单元还是字节单元,
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的作用:
在代码段中使用栈
完成下面的程序,利用栈,将程序中定义的数据逆序存放
程序的思路大致如下:
- 程序运行时,定义的数据存放在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。
比如对于:
将数据、代码、栈放入不同的段
- 在前面的内容中,我们在程序中用到了数据和栈,我们将数据、栈和代码都放到了一个段里面。我们在编程的时候要注意何处是数据,何处是栈,何处是代码。
- 这样做显然有两个问题:
- 但如果数据、栈和代码需要的空间超过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 指令
ret 和 retf
ret
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指令,则:
示例
call 指令
CPU执行call指令,进行两步操作:
主要应用格式
依据位移进行转移的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来实现子程序的机制。
子程序的框架
参数和结果传递的问题
- 子程序一般都要根据提供的参数处理一定的事务,处理后,将结果(返回值)提供给调用者。
- 其实,我们讨论参数和返回值传递的问题,实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值。
- 我们设计一个子程序,可以根据提供的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
- 用寄存器来存储参数和结果是最常使用的方法。对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰恰相反:
- 调用者将参数送入参数寄存器,从结果寄存器中取到返回值;
- 子程序从参数寄存器中取到参数,将返回值送入结果寄存器。