64、深入探索MS - DOS编程:段定义、程序结构与中断处理

MS-DOS编程核心详解

深入探索MS - DOS编程:段定义、程序结构与中断处理

1. 段定义

在汇编语言编程中,段定义是一个重要的概念。段定义主要涉及到 SEGMENT ENDS 指令,它们用于划分程序中的不同区域,如代码段、数据段和堆栈段等。

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

2. 运行时程序结构

要成为一名高效的汇编语言程序员,需要深入了解MS - DOS。这部分主要介绍 command.com 、程序段前缀以及 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 --> K{是否找到匹配文件};
    K -- 是 --> L[执行文件];
    K -- 否 --> M[结束搜索];
2.2 程序段前缀(PSP)

MS - DOS在程序加载到内存时,会在程序开头创建一个256字节的特殊块,称为程序段前缀(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 指令跳过数据到第一个实际指令:

TITLE Hello Program in COM format      (HelloCom.asm)
.model tiny
.code
org 100h       
; must be before entry point
main proc
jmp start
; skip over the data
hello_message BYTE 'Hello, world!', 0dh, 0ah, '$'
start:
mov ah, 9
mov dx, OFFSET hello_message
int 21h
mov ax, 4C00h
int 21h
main ENDP
END main

Microsoft链接器需要 /T 参数来创建 COM 文件而非 EXE 文件。 COM 程序存储在磁盘上时通常比其 EXE 对应程序小,但在内存中会占用整个64K内存段,无论是否需要这么多空间。而且 COM 程序并非为多任务环境设计。

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下可用的内存。因此,程序加载时,MS - DOS会自动分配可用的内存。

可以在链接程序时使用 /CP 选项设置最大分配。例如,对于名为 prog1.obj 的程序:

link16 /cp:1024 prog1;

使用Microsoft汇编器附带的 exehdr 程序可以在编译 EXE 程序后修改 EXE 头的值。例如,将名为 prog1.exe 的程序的最大分配设置为400h段落(16,384字节)的命令是:

exehdr prog1 /max 400

exehdr 还可以显示程序的重要统计信息。以下是将 prog1.exe 程序的最大分配设置为1024个段落并链接后的示例输出:
| 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. 中断处理

在计算机系统中,中断处理是一个至关重要的机制,它允许程序在特定事件发生时暂停当前操作,转而执行相应的处理程序。在MS - DOS环境下,我们可以对BIOS和MS - DOS的中断处理程序进行定制,以满足特定的需求。

3.1 中断处理的意义和用途

中断处理程序可以根据不同的原因进行编写。例如,我们可能希望程序在按下热键时被激活,即使此时用户正在运行其他应用程序。像Borland的SideKick程序,就能在按下特定热键组合时弹出记事本或计算器。

我们还可以替换MS - DOS的默认中断处理程序,以提供更完善的服务。比如,当CPU尝试除以零的时候,会触发除以零中断,但程序没有标准的恢复方法,我们可以自定义处理程序来解决这个问题。另外,MS - DOS的默认关键错误处理程序会使程序中止并返回MS - DOS,而我们自己编写的处理程序可以从错误中恢复,让用户继续运行当前应用程序。

用户编写的中断服务程序在处理硬件中断方面比MS - DOS更有效。以PC的异步通信处理程序(INT 14h)为例,它不进行输入/输出缓冲,这意味着如果在另一个字符到达之前没有从端口复制输入字符,该字符就会丢失。而一个驻留内存的程序可以等待传入字符产生硬件中断,从端口输入字符并将其存储在循环缓冲区中,从而使应用程序无需花费大量时间反复检查串口。

不过需要注意的是,本章介绍的中断处理程序仅在计算机启动到MS - DOS模式时有效。在较新的Windows版本中,操作系统会对系统硬件进行屏蔽,以保证系统的稳定性和安全性,不允许应用程序直接修改硬件设备的内部设置。

3.2 中断向量表

MS - DOS的灵活性关键在于位于RAM前1024字节(地址从0:0到0:03FF)的中断向量表。表17 - 2展示了向量表条目的简短示例。表中的每个条目(称为中断向量)是一个32位的段 - 偏移地址,指向现有的某个服务程序。

不同计算机上的向量值会因BIOS和MS - DOS版本的不同而有所差异。每个中断向量对应一个中断号,中断向量的偏移量可以通过将其中断号乘以4得到。例如,INT 9h的向量偏移量是9 * 4,即十六进制的0024。

中断号 偏移 中断向量
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 执行中断处理程序

中断处理程序可以通过两种方式执行:
1. 软件中断 :包含INT指令的应用程序可以调用相应的处理程序。
2. 硬件中断 :当硬件设备(如异步端口、键盘、定时器等)向可编程中断控制器芯片发送信号时,会产生硬件中断。

在进行对段寄存器和堆栈的敏感操作时,有时需要禁用硬件中断。可以使用 CLI (清除中断标志)指令禁用中断,使用 STI (设置中断标志)指令启用中断。

3.4 IRQ级别

PC上的许多不同设备都可以触发中断,每个设备根据其中断请求级别(IRQ)具有不同的优先级。优先级从0级最高到15级最低,较低级别的中断不能中断正在进行的较高级别中断。例如,如果通信端口1(COM1)试图中断键盘中断处理程序,它必须等待键盘中断处理程序完成。同时,多个同时发生的中断请求会根据其优先级进行处理,这些调度工作由8259可编程中断控制器(PIC)完成。

以下是IRQ分配的详细信息:
| IRQ | 中断号 | 描述 |
| ---- | ---- | ---- |
| 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. 将标志寄存器压入堆栈。
2. 清除中断标志,防止其他硬件中断。
3. 将当前的CS和IP压入堆栈。
4. 找到INT 9的中断向量表条目,并将该地址放入CS和IP。

接下来,BIOS的INT 9例程会执行,它会按以下顺序进行操作:
1. 重新启用硬件中断,以免影响系统定时器。
2. 从键盘端口输入扫描码,尝试将其转换为ASCII字符,或者将ASCII码赋值为零。然后将扫描码和ASCII码存储在BIOS数据区的一个32字节的循环缓冲区中。

这个过程可以用以下mermaid流程图表示:

graph TD;
    A[按下按键] --> B[8259 PIC发送INTR信号并传递中断号];
    B --> C{外部中断是否禁用};
    C -- 否 --> D[将标志寄存器压入堆栈];
    D --> E[清除中断标志];
    E --> F[将当前CS和IP压入堆栈];
    F --> G[查找INT 9中断向量表条目并放入CS和IP];
    G --> H[执行BIOS的INT 9例程];
    H --> I[重新启用硬件中断];
    I --> J[输入扫描码并处理];
    J --> K[存储扫描码和ASCII码到缓冲区];
    C -- 是 --> L[不做处理];

综上所述,在MS - DOS编程中,段定义、程序结构和中断处理是非常重要的内容。通过合理地进行段定义和组合,我们可以更好地组织程序的结构;了解COM和EXE程序的运行时结构,有助于我们优化程序的内存使用;而定制中断处理程序,则可以让我们的程序更加灵活和强大。希望这些知识能帮助你在MS - DOS编程的道路上取得更好的成果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值