深入探索MS - DOS编程:段定义、运行时结构与中断处理
1. 段定义
在汇编语言编程中,段定义是一个重要的概念。它允许我们将程序划分为不同的逻辑部分,如代码段、数据段和堆栈段。
1.1 获取段地址
可以使用
SEG
运算符来获取变量的段地址。例如:
mov ax, SEG val2 ; point ES to data2 segment
mov es, ax
汇编器生成的列表文件可能显示两个变量
val1
和
val2
具有相同的值(起始偏移),但段属性不同:
| Name | Type | Value | Attr |
| ---- | ---- | ---- | ---- |
| val1 | Word | 0000 | data1 |
| val2 | Word | 0000 | data2 |
1.2 段覆盖
段覆盖是一个单字节前缀,它使当前指令在计算有效地址时使用与
ASSUME
指令指定不同的段寄存器。例如:
mov al, cs:var1 ; segment pointed to by CS
mov al, es:var2 ; segment pointed to by ES
在实地址模式下,可以将变量放在代码段,但在保护模式下不行。若要获取当前
DS
或
ES
未假定的段中变量的偏移,可以使用以下指令:
mov bx, OFFSET AltSeg:var2
对于变量的多次引用,可以通过插入
ASSUME
指令临时更改默认段引用,示例如下:
ASSUME ds:AltSeg ; use AltSeg for a while
mov ax, AltSeg
mov ds, ax
mov al, var1
...
ASSUME ds:data ; use the default data segment
mov ax, data
mov ds, ax
1.3 段组合
大型程序应划分为独立模块,不同模块的源代码也可组合到同一分段。只需在每个模块中使用相同的段名,并指定
PUBLIC
组合类型。
段对齐类型有
BYTE
、
WORD
和
PARA
。使用
BYTE
对齐类型时,每个段紧跟前一个段;使用
WORD
对齐类型时,段会在下一个偶数字边界之后跟随另一个段;默认对齐类型为
PARA
,即每个段在下一个段落边界之后跟随。
以下是一个包含一个代码段(
CSEG
)、一个数据段(
DSEG
)和一个堆栈段(
SSEG
)的两模块程序示例:
主模块(Seg2.asm) :
TITLE Segment Example (main module, Seg2.asm)
EXTRN var2:WORD, subroutine_1:PROC
cseg SEGMENT BYTE PUBLIC 'CODE'
ASSUME cs:cseg,ds:dseg, ss:sseg
main PROC
mov ax, dseg ; initialize DS
mov ds, ax
mov ax, var1 ; local variable
mov bx, var2 ; external variable
call subroutine_1 ; external procedure
mov ax, 4C00h ; exit to OS
int 21h
main ENDP
cseg ENDS
dseg SEGMENT WORD PUBLIC 'DATA'
; local data segment
var1 WORD 1000h
dseg ends
sseg SEGMENT STACK 'STACK'
; stack segment
BYTE 100h dup('S')
sseg ENDS
END main
子模块(Seg2a.ASM) :
TITLE Segment Example (submodule, Seg2a.ASM)
PUBLIC subroutine_1, var2
cseg SEGMENT BYTE PUBLIC 'CODE'
ASSUME cs:cseg, ds:dseg
subroutine_1 PROC
; called from MAIN
mov ah, 9
mov dx, OFFSET msg
int 21h
ret
subroutine_1 ENDP
cseg ENDS
dseg SEGMENT WORD PUBLIC 'DATA'
var2 WORD 2000h
; accessed by MAIN
msg BYTE 'Now in Subroutine_1'
BYTE 0Dh, 0Ah, '$'
dseg ENDS
END
链接器可以创建一个
MAP
文件,列出程序中的所有段。此程序的
MAP
文件显示如下:
| Start | Stop | Length | Name | Class |
| ---- | ---- | ---- | ---- | ---- |
| 00000H | 0001BH | 0001CH | CSEG | CODE |
| 0001CH | 00035H | 0001AH | DSEG | DATA |
| 00040H | 0013FH | 00100H | SSEG | STACK |
程序入口点在
0000:0000
。
下面是段定义相关操作的流程图:
graph TD;
A[开始] --> B[定义段];
B --> C{选择对齐类型};
C -->|BYTE| D[段紧跟前一个段];
C -->|WORD| E[段在下一个偶数字边界后跟随];
C -->|PARA| F[段在下一个段落边界后跟随];
D --> G[选择组合类型];
E --> G;
F --> G;
G -->|PUBLIC| H[可组合不同模块同段名代码];
G -->|其他| I[按规则处理];
H --> J[完成段定义];
I --> J;
J --> K[结束];
2. 运行时程序结构
有效的汇编语言程序员需要深入了解MS - DOS。下面介绍命令处理器、程序段前缀以及
COM
和
EXE
程序的结构。
2.1 命令处理流程
当在MS - DOS提示符下输入命令时,会按以下顺序处理:
1. MS - DOS检查命令是否为内部命令,如
DIR
、
REN
或
DEL
。若是,则由驻留内存的MS - DOS例程立即执行。
2. MS - DOS查找扩展名为
COM
的匹配文件。若文件在当前目录中,则执行该文件。
3. MS - DOS查找扩展名为
EXE
的匹配文件。若文件在当前目录中,则执行该文件。
4. MS - DOS查找扩展名为
BAT
的匹配文件。若文件在当前目录中,则执行该文件。
BAT
文件是包含MS - DOS命令的文本文件。
5. 若在当前目录中未找到匹配的
COM
、
EXE
或
BAT
文件,MS - DOS会搜索当前路径中的第一个目录。若未找到匹配项,则继续搜索路径中的下一个目录,直到找到匹配文件或路径搜索结束。
以下是命令处理流程的mermaid流程图:
graph TD;
A[输入命令] --> B{是否为内部命令};
B -->|是| C[立即执行];
B -->|否| D{是否有COM文件};
D -->|是| E[执行COM文件];
D -->|否| F{是否有EXE文件};
F -->|是| G[执行EXE文件];
F -->|否| H{是否有BAT文件};
H -->|是| I[执行BAT文件];
H -->|否| J{路径中是否还有目录};
J -->|是| D;
J -->|否| K[未找到可执行文件];
C --> L[结束];
E --> L;
G --> L;
I --> L;
K --> L;
2.2 程序段前缀(PSP)
MS - DOS在程序加载到内存时,会在程序开头创建一个特殊的256字节块,称为程序段前缀(PSP)。PSP的结构如下:
| Offset | Comments |
| ---- | ---- |
| 00–15 | MS - DOS指针和向量地址 |
| 16–2B | 由MS - DOS保留 |
| 2C–2D | 当前环境字符串的段地址 |
| 2E–5B | 由MS - DOS保留 |
| 5C–7F | 文件控制块1和2,主要用于MS - DOS 2.0之前的程序 |
| 80–FF | 默认磁盘传输区域和当前MS - DOS命令尾部的副本 |
2.3 COM程序
COM
程序是机器语言程序的未修改二进制映像。它由MS - DOS加载到可用的最低段地址,在偏移0处创建PSP。代码、数据和堆栈存储在同一物理(和逻辑)段中,程序最大可达64K,减去PSP的大小和堆栈末尾的两个保留字节。
所有段寄存器都设置为PSP的基地址,代码区域从偏移
100h
开始,数据区域紧跟代码,堆栈区域在段末尾,因为MS - DOS将
SP
初始化为
FFFEh
。
以下是一个简单的
COM
格式程序示例:
TITLE Hello Program in COM format (HelloCom.asm)
.model tiny
.code
org 100h ; must be before main
main PROC
mov ah, 9
mov dx, OFFSET hello_message
int 21h
mov ax, 4C00h
int 21h
main ENDP
hello_message BYTE 'Hello, world!', 0dh, 0ah, '$'
END main
变量通常位于主程序之后,因为没有单独的数据段。也可以在开头放置
JMP
指令跳过数据到第一个实际指令。
Microsoft链接器需要
/T
参数来创建
COM
文件而非
EXE
文件。
COM
程序存储在磁盘上时通常比
EXE
程序小,但在内存中会占用整个64K内存段,且不适合在多任务环境中运行。
2.4 EXE程序
EXE
程序存储在磁盘上时,包含一个
EXE
头和一个包含程序本身的加载模块。程序头不实际加载到内存,而是包含MS - DOS用于加载和执行程序的信息。
当MS - DOS加载
EXE
程序时,会在第一个可用地址创建PSP,程序放在其上方。MS - DOS解码程序头时,会将
DS
和
ES
设置为程序的加载地址(即PSP),
CS
和
IP
设置为程序代码的入口点,
SS
设置为堆栈段的开头,
SP
设置为堆栈大小。
EXE
程序可能包含多达65,535个段。若程序有多个数据段,程序员通常需要手动将
DS
或
ES
设置为每个新段。
EXE
程序使用的内存量由其程序头指定,特别是代码区域之后运行时处理变量和堆栈所需的最小和最大段落数(每个段落16字节)。默认情况下,链接器将最大值设置为65,535个段落,MS - DOS会自动分配可用的内存。
可以在链接程序时使用
/CP
选项设置最大分配,例如:
link16 /cp:1024 prog1;
也可以使用
exehdr
程序在
EXE
程序编译后修改
EXE
头值,例如将
prog1.exe
的最大分配设置为400h段落(16,384字节):
exehdr prog1 /max 400
exehdr
还可以显示程序的重要统计信息,示例输出如下:
| PROG1 | (Hex) | (Dec) |
| ---- | ---- | ---- |
| EXE size (bytes) | 876 | 2166 |
| Minimum load size (bytes) | 786 | 1926 |
| Overlay number | 0 | 0 |
| Initial CS:IP | 0000:0010 | 16 |
| Initial SS:SP | 0068:0100 | 256 |
| Minimum allocation (para) | 11 | 17 |
| Maximum allocation (para) | 400 | 1024 |
| Header size (para) | 20 | 32 |
| Relocation table offset | 1E | 30 |
| Relocation entries | 1 | 1 |
EXE
头包含以下信息:
- 重定位表,包含程序加载时要计算的地址。
-
EXE
程序的文件大小,以512字节为单位。
- 最小分配:程序代码区域之后要保留的最小内存段落数,部分存储可用于保存动态数据的运行时堆。
- 最大分配:程序上方所需的最大段落数。
- 要赋予
IP
和
SP
寄存器的起始值。
- 堆栈和代码段相对于加载模块开头的位移(以16字节段落为单位)。
- 文件中所有字的校验和,用于在将程序加载到内存时捕获数据错误。
3. 中断处理
在这部分,我们将探讨如何通过安装中断处理程序(中断服务例程)来自定义BIOS和MS - DOS。BIOS和MS - DOS包含简化输入/输出以及基本系统任务的中断处理程序,如INT 10h用于视频操作、INT 16h用于键盘操作、INT 21h用于MS - DOS服务等。同时,操作系统的一组中断处理程序会响应硬件中断,MS - DOS允许我们用自己的程序替换这些服务例程。
3.1 中断处理程序的用途
编写中断处理程序可能有多种原因:
-
热键激活
:希望程序在按下热键时激活,即使用户正在运行其他应用程序。例如,Borland的SideKick程序能够在按下特殊组合热键时弹出记事本或计算器。
-
提供更完善的服务
:可以替换MS - DOS的默认中断处理程序,以提供更完整的服务。比如,当CPU尝试除以零时,会触发除零中断,但程序没有标准的恢复方法,我们可以自定义处理程序。
-
错误处理
:可以用自己的程序替换MS - DOS的关键错误处理程序或Ctrl - Break处理程序。MS - DOS的默认关键错误处理程序会使程序中止并返回MS - DOS,而自定义处理程序可以从错误中恢复,让用户继续运行当前应用程序。
-
硬件中断处理
:用户编写的中断服务程序可以比MS - DOS更有效地处理硬件中断。例如,PC的异步通信处理程序(INT 14h)不执行输入/输出缓冲,输入字符如果在另一个字符到达之前未从端口复制就会丢失,而内存驻留程序可以等待传入字符生成硬件中断,从端口输入字符并存储在循环缓冲区中,使应用程序无需花费大量时间反复检查串行端口。
3.2 中断向量表
MS - DOS灵活性的关键在于位于RAM前1024字节(位置0:0到0:03FF)的中断向量表。表中的每个条目(称为中断向量)是一个32位的段 - 偏移地址,指向现有的服务例程之一。
不同计算机上的向量值会因BIOS和MS - DOS的不同版本而有所不同。每个中断向量对应一个中断号,中断向量的偏移可以通过将其中断号乘以4来找到。例如,INT 9h的向量偏移为9 * 4,即十六进制的0024。
以下是中断向量表条目的示例:
| Interrupt Number | Offset | Interrupt Vectors |
| ---- | ---- | ---- |
| 00 - 03 | 0000 | 02C1:5186 0070:0C67 0DAD:2C1B 0070:0C67 |
| 04 - 07 | 0010 | 0070:0C67 F000:FF54 F000:837B F000:837B |
| 08 - 0B | 0020 | 0D70:022C 0DAD:2BAD 0070:0325 0070:039F |
| 0C - 0F | 0030 | 0070:0419 0070:0493 0070:050D 0070:0C67 |
| 10 - 13 | 0040 | C000:0CD7 F000:F84D F000:F841 0070:237D |
3.3 执行中断处理程序
中断处理程序可以通过以下两种方式执行:
-
软件中断
:包含INT指令的应用程序可以调用该例程。
-
硬件中断
:当硬件设备(如异步端口、键盘、定时器等)向可编程中断控制器芯片发送信号时发生。
3.4 硬件中断
硬件中断由Intel 8259可编程中断控制器(PIC)生成,它会向CPU发出信号,暂停当前程序的执行并执行中断服务例程。
在执行对段寄存器和堆栈的敏感操作时,程序有时必须禁用硬件中断。CLI(清除中断标志)指令用于禁用中断,STI(设置中断标志)指令用于启用中断。
IRQ级别 :PC上的许多不同设备都可以触发中断,每个设备根据其中断请求级别(IRQ)具有优先级,级别0优先级最高,级别15优先级最低。低级别中断不能中断正在进行的高级别中断。例如,如果通信端口1(COM1)试图中断键盘中断处理程序,它必须等待键盘处理程序完成。同时,两个或多个同时发生的中断请求将根据其优先级级别进行处理,中断的调度由8259 PIC处理。
以下是IRQ分配表:
| IRQ | Interrupt Number | Description |
| ---- | ---- | ---- |
| 0 | 8 | 系统定时器(每秒18.2次) |
| 1 | 9 | 键盘 |
| 2 | 0Ah | 可编程中断控制器 |
| 3 | 0Bh | COM2(串行端口2) |
| 4 | 0Ch | COM1(串行端口1) |
| 5 | 0Dh | LPT2(并行端口2) |
| 6 | 0Eh | 软盘控制器 |
| 7 | 0Fh | LPT1(并行端口1) |
| 8 | 70h | CMOS实时时钟 |
| 9 | 71h | (重定向到INT 0Ah) |
| 10 | 72h | (可用)声卡 |
| 11 | 73h | (可用)SCSI卡 |
| 12 | 74h | PS/2鼠标 |
| 13 | 75h | 数学协处理器 |
| 14 | 76h | 硬盘控制器 |
| 15 | 77h | (可用) |
以键盘为例,当按下一个键时,8259 PIC会向CPU发送INTR信号,并传递中断号。如果当前未禁用外部中断,CPU会按以下顺序执行操作:
1. 将Flags寄存器压入堆栈。
2. 清除中断标志,防止任何其他硬件中断。
3. 将当前的CS和IP压入堆栈。
4. 找到INT 9的中断向量表条目,并将该地址放入CS和IP。
然后,INT 9的BIOS例程会按以下顺序执行:
1. 重新启用硬件中断,以免影响系统定时器。
2. 从键盘端口输入扫描码,尝试将其转换为ASCII字符,或分配一个等于零的ASCII码,然后将扫描码和ASCII码存储在BIOS数据区的32字节循环缓冲区中。
下面是硬件中断处理流程的mermaid流程图:
graph TD;
A[硬件设备产生中断] --> B[8259 PIC发送INTR信号];
B --> C{外部中断是否禁用};
C -->|否| D[CPU将Flags寄存器压入堆栈];
C -->|是| E[等待中断启用];
D --> F[清除中断标志];
F --> G[将当前CS和IP压入堆栈];
G --> H[查找中断向量表条目并更新CS和IP];
H --> I[执行中断服务例程];
I --> J[重新启用硬件中断];
J --> K[中断服务例程完成];
E --> B;
K --> L[恢复程序执行];
4. 总结
本文深入探讨了MS - DOS编程中的几个关键方面:
-
段定义
:通过
SEG
运算符获取段地址,利用段覆盖和段组合功能,将程序划分为不同的逻辑段,如代码段、数据段和堆栈段,有助于大型程序的模块化开发和管理。
-
运行时程序结构
:了解了MS - DOS的命令处理流程,以及
COM
和
EXE
程序的特点和加载方式。
COM
程序结构简单,但内存使用不够灵活;
EXE
程序可以包含多个段,内存分配更具弹性。
-
中断处理
:掌握了中断处理程序的用途和执行方式,以及硬件中断的工作原理和IRQ级别。通过自定义中断处理程序,可以实现热键激活、错误处理和更高效的硬件中断处理。
掌握这些知识对于深入理解MS - DOS编程,开发高效、稳定的汇编语言程序具有重要意义。在实际应用中,可以根据具体需求灵活运用这些技术,提高程序的性能和功能。
深入解析MS-DOS编程核心机制
超级会员免费看
6

被折叠的 条评论
为什么被折叠?



