MS-DOS编程深入解析与实践
1. 相关资源与参考资料
在MS - DOS编程领域,有许多有价值的参考资料。例如Ray, Duncan所著的《IBM RUM BIOS》(1998年,Microsoft Press出版)和《Advanced MS - DOS Programming》(第二版,1988年,同样由Microsoft Press出版)。还有Frank van, Gilluwe的《The Undocumented PC: A Programmer’s Guide to I/O, CPUs, and Fixed Memory Areas》(1996年,Addison - Wesley出版),以及Thom, Hogan的《Programmer’s PC Sourcebook: Reference Tables for IBM PCs and Compatibles, Ps12 Systems, Eisa - Based Systems, Ms - DOS Operating System Thmugh Version》(1991年,Microsoft Press出版)等。此外,网站www.asmirvine.com提供了许多额外的信息来源,包括Ralf Brown当前的MS - DOS和BIOS中断列表。
2. 实地址模式下的编程练习
在实地址模式下,有一系列编程练习可以帮助我们更好地掌握MS - DOS编程。以下是这些练习的详细介绍:
1.
ASCII表显示
:使用INT 10h,显示IBM扩展ASCII字符集中的所有256个字符。每行显示32列,每个字符后跟随一个空格。
2.
滚动文本窗口
:
- 定义一个大约为视频显示大小四分之三的文本窗口。
- 程序按顺序执行以下操作:
- 在窗口的顶行绘制一串随机字符(可调用Irvine 16库中的Random_range函数)。
- 将窗口向下滚动一行。
- 暂停程序约200毫秒(可调用Irvine 16库中的Delay函数)。
- 绘制另一行随机文本。
- 继续滚动和绘制,直到显示50行。
3.
滚动彩色列
:以滚动文本窗口练习为基础,进行如下更改:
- 随机字符串仅在第0、3、6、9…78列有字符,其他列应为空白,以实现向下滚动时的列效果。
- 每列应使用不同的颜色。
4.
不同方向的滚动列
:同样以滚动文本窗口练习为起点,在循环开始前,随机选择每列向上或向下滚动,并且在程序运行期间保持该方向不变。提示:将每列定义为单独滚动的窗口。
5.
使用INT 10h绘制矩形
:利用INT 10h的像素绘制功能,创建一个名为DrawRectangle的过程,该过程接受指定左上角和右下角位置以及颜色的输入参数。编写一个简短的测试程序,绘制几个不同大小和颜色的矩形。
6.
使用INT 10h绘制函数图像
:使用INT 10h的像素绘制功能,绘制由方程Y = 2(X²)确定的直线。
7.
模式13下的单垂直线
:修改第15.5.2节中的内存映射图形程序,使其绘制一条单垂直线。
8.
模式13下的多条垂直线
:修改第15.5.2节中的内存映射图形程序,使其绘制一系列10条垂直线,每条线使用不同的颜色。
9.
绘制框程序
:20世纪80年代和90年代初的MS - DOS应用程序通常在文本模式下使用线条绘制字符来显示框和框架。编写一个过程,在屏幕上的任意位置绘制单行框架。使用以下扩展ASCII代码:C0h、BFh、B3h、C4h、D9h和DAh。该过程的唯一输入参数应为指向FRAME结构的指针:
FRAME STRUCT
Left BYTE 7 ; 左边
Top BYTE 7 ; 顶行
Right BYTE 7 ; 右边
Bottom BYTE 7 ; 底行
FrameColor BYTE 7 ; 框颜色
FRAME ENDS
编写一个程序来测试该过程,将指向各种FRAME对象的指针传递给它。
3. 专家级MS - DOS编程概述
专家级MS - DOS编程涵盖了多个重要方面,包括定义段、运行时程序结构、中断处理以及硬件控制等。这部分内容对于计划从事英特尔处理器硬件级工作的工程师,或者希望成为系统级程序员的人来说非常有价值。以下是具体内容的详细介绍:
1.
定义段
-
简化段指令
:使用.MODEL指令时,汇编器会自动为近数据段定义DGROUP。.DATA和.DATA?指令都会创建近数据段,在实地址模式下最大可达64Kb,它们被放置在DGROUP组中,该组也限制为64Kb。在小和中等内存模型中使用.FARDATA和.FARDATA?时,汇编器会分别创建名为FAR..DATA和FAR_BSS的远数据段。
-
识别变量的段
:一些BIOS和DOS函数要求在传递参数数据时使用特定的段寄存器。可以使用SEG运算符将段的地址分配给段寄存器。例如:
mov ax, SEG farvar
mov ds, ax
- **代码段**:代码段由.CODE指令定义。在小内存模型程序中,.CODE指令会使汇编器生成名为_TEXT的段。在中、大、巨型模型程序中,每个源代码模块会被分配不同的段名,格式为模块名后跟_TEXT。也可以在同一模块中声明多个代码段,只需在.CODE指令后添加可选的段名。需要注意的是,如果调用16位链接库过程,代码必须位于名为_TEXT的段内。
- **多代码段程序示例**:以下是一个包含两个代码段的MultCode.asm程序:
TITLE Multiple Code Segments (MultCode.asm)
; 此小模型程序包含多个代码段
.model small, stdcall
.stack 100h
WriteString PROTO
.data
msgl db "FirstMessage", 0dh, 0ah, 0
msg2 db "Second Message", 0dh, 0ah, "$"
.code
main PROC
mov ax, @data
mov ds, ax
mov dx, OFFSET msgl
call WriteString ; 近调用
call Display ; 远调用
exit
main ENDP
.code OtherCode
Display PROC FAR
mov ah, 9
mov dx, offset msg2
int 21h
ret
Display ENDP
END main
在这个例子中,_TEXT段包含main过程,OtherCode段包含Display过程。Display过程必须有FAR修饰符,以告知汇编器生成保存当前段和偏移量到栈中的调用指令。
2.
显式段定义
:在某些情况下,可能需要创建显式段定义。例如,定义多个带有额外内存缓冲区的数据段,或者将程序链接到使用自有专有段名的对象库,又或者编写要从不使用Microsoft段名的高级语言编译器调用的过程。
-
SEGMENT和ENDS指令
:用于定义段的开始和结束。程序可以包含任意数量的段,每个段都有唯一的名称,段也可以组合在一起。语法如下:
name SEGMENT [align] [combine] ['class']
statement - list
name ENDS
其中,name标识段;align可以是BYTE、WORD、DWORD、PARA或PAGE;combine可以是PRIVATE、PUBLIC、STACK、COMMON、MEMORY或AT address;class是用于识别特定类型段(如CODE或STACK)的单引号括起来的标识符。
-
对齐类型(Align Type)
:当两个或多个段要组合时,它们的对齐类型告诉链接器如何对齐起始地址。默认是PARA,表示段必须从偶数16字节边界开始。可用的对齐类型有:
- BYTE:段从前面段的下一个字节开始。
- WORD:段从下一个16位边界开始。
- DWORD:段从下一个32位边界开始。
- PARA:段从下一个16字节边界开始。
- PAGE:段从下一个256字节边界开始。
如果程序可能在8086或80286处理器上运行,对于数据段,WORD对齐类型(或更大)是最好的,因为这些处理器有16位数据总线。而IA - 32处理器每次取32位数据,应使用DWORD对齐类型。
-
组合类型(Combine Type)
:
- PRIVATE:默认类型,此类段不会与其他段组合。
- PUBLIC和MEMORY:使段与所有同名的公共或内存段组合,实际上成为一个段,所有标签的偏移量会调整为相对于同一段的起始位置。
- STACK:所有其他栈段会与之组合。MS - DOS在加载程序时,会自动将SS初始化为第一个具有STACK组合类型的段的起始位置,并将SP设置为段的长度减1。在EXE程序中,至少应该有一个具有STACK组合类型的段,否则链接器会显示警告消息。
- COMMON:使段与任何其他同名的COMMON段从相同地址开始,实际上这些段会相互覆盖,所有偏移量从相同起始地址计算,变量可能会重叠。
- AT address:允许在绝对地址创建段,常用于硬件或操作系统预定义位置的数据。不能初始化变量或数据,但可以创建引用特定偏移量的变量名。例如:
bios SEGMENT AT 40h
ORG 17h
keyboard_flag BYTE? ; MS - DOS键盘标志
bios ENDS
.code
mov ax, bios ; 指向BIOS段
mov ds, ax
and ds:keyboard_flag, 7Fh ; 清除高位
- **类类型(Class Type)**:段的类类型提供了另一种组合段的方式,特别是对于不同名称的段。类类型是区分大小写的单引号括起来的字符串。具有相同类类型的段会一起加载,尽管它们在原始程序中的顺序可能不同。标准类型CODE被链接器识别,用于包含指令的段。如果计划使用调试器,必须包含此类型标签。
- **ASSUME指令**:该指令告诉汇编器在汇编时如何计算代码和数据标签的偏移量。通常放在代码段的SEGMENT指令之后。语法要求段寄存器名,后跟冒号,再跟段名:
ASSUME segreg:segname
ASSUME实际上不会改变段寄存器的值,这必须在运行时使用将段值分配给段寄存器的指令来完成。代码中可以包含多个ASSUME指令,遇到新的指令时,汇编器会从该点开始修改地址计算方式。例如:
ASSUME ds:datal ; 告诉汇编器使用DS作为datal段的默认寄存器
ASSUME cs:myCode, ss:myStack ; 关联CS与myCode,SS与myStack
- **多数据段示例**:以下是一个包含两个数据段的MultData.asm程序:
TITLE Multiple Data Segments (MultData.asm)
; 此程序展示如何显式声明多个数据段
cseg SEGMENT 'CODE'
ASSUME cs:cseg, ds:datal, es:data2, ss:mystack
main PROC
mov ax, datal ; 指向datal段
mov ds, ax
mov ax, SEG val2 ; 指向data2段
mov es, ax
mov ax, vall ; 假设为datal段
mov bx, val2 ; 假设为data2段
mov ax, 4C00h ; 退出程序
int 21h
main ENDP
cseg ENDS
datal SEGMENT 'DATA'
vall WORD 1000h
datal ENDS
data2 SEGMENT 'DATA'
val2 WORD 1002h
data2 ENDS
mystack SEGMENT para STACK 'STACK'
BYTE 100h dup('S')
mystack ENDS
END main
在这个程序中,使用了两种设置段寄存器值的方法。第一种使用段名(datal),第二种使用SEG运算符获取val2的段地址。汇编器生成的列表文件显示两个变量vall和val2具有相同的值(起始偏移量)但不同的段属性:
| 名称 | 类型 | 值 | 属性 |
| ---- | ---- | ---- | ---- |
| vall | Word | 0000 | datal |
| val2 | Word | 0000 | data2 |
下面是一个简单的mermaid流程图,展示了显式段定义的基本流程:
graph TD;
A[开始] --> B[定义段开始 SEGMENT];
B --> C{选择对齐类型};
C --> |BYTE| D1[按字节对齐];
C --> |WORD| D2[按16位边界对齐];
C --> |DWORD| D3[按32位边界对齐];
C --> |PARA| D4[按16字节边界对齐];
C --> |PAGE| D5[按256字节边界对齐];
D1 --> E{选择组合类型};
D2 --> E;
D3 --> E;
D4 --> E;
D5 --> E;
E --> |PRIVATE| F1[不与其他段组合];
E --> |PUBLIC| F2[与同名公共段组合];
E --> |STACK| F3[与其他栈段组合];
E --> |COMMON| F4[与同名COMMON段重叠];
E --> |AT address| F5[在绝对地址创建段];
F1 --> G[定义段内容];
F2 --> G;
F3 --> G;
F4 --> G;
F5 --> G;
G --> H[定义段结束 ENDS];
H --> I[结束];
通过以上内容,我们对MS - DOS编程中的段定义有了更深入的理解,包括简化段指令和显式段定义的各种细节。这些知识对于编写高效、灵活的MS - DOS程序至关重要。在后续内容中,我们将继续探讨运行时程序结构、中断处理以及硬件控制等方面的内容。
MS-DOS编程深入解析与实践
4. 运行时程序结构
运行时程序结构主要涉及程序段前缀(PSP)、COM程序和EXE程序,了解这些内容有助于我们深入理解MS - DOS程序在运行时的行为。
1.
程序段前缀(PSP)
:PSP是MS - DOS为每个程序分配的一块内存区域,位于程序加载地址的起始处。它包含了程序运行所需的一些重要信息,如环境字符串的地址等。通过对PSP的分析,我们可以找到MS - DOS环境字符串,这对于获取系统环境信息非常有用。
2.
COM程序
:COM程序是一种简单的MS - DOS程序格式,它的代码、数据和栈都位于同一个64KB的段中。COM程序没有复杂的头部信息,加载速度较快,适合一些简单的应用场景。其执行过程相对简单,MS - DOS会将程序加载到内存中,并将控制权直接交给程序的入口点。
3.
EXE程序
:EXE程序是一种更为复杂的MS - DOS程序格式,它支持多个代码段和数据段。EXE程序包含一个头部,其中包含了程序的加载信息、重定位信息等。在加载EXE程序时,MS - DOS会根据头部信息将程序的各个段加载到正确的内存位置,并进行必要的重定位操作。EXE程序的灵活性更高,可以处理更复杂的任务。
下面是一个简单的表格,对比COM程序和EXE程序的特点:
| 程序类型 | 内存结构 | 头部信息 | 加载过程 | 适用场景 |
| ---- | ---- | ---- | ---- | ---- |
| COM程序 | 代码、数据和栈在同一64KB段 | 无 | 直接加载到内存并执行 | 简单应用 |
| EXE程序 | 支持多个代码段和数据段 | 包含加载、重定位等信息 | 根据头部信息加载并重定位 | 复杂任务 |
5. 中断处理
中断处理是MS - DOS编程中的一个重要部分,它允许程序在特定事件发生时进行响应。以下是中断处理的详细内容:
1.
硬件中断
:硬件中断是由外部硬件设备触发的中断信号。例如,键盘输入、鼠标移动等都会产生硬件中断。在Intel处理器中,使用8259可编程中断控制器(PIC)来管理硬件中断。8259 PIC有多个中断请求(IRQ)级别,每个级别对应一个特定的硬件设备或事件。不同的IRQ级别有不同的优先级,当多个中断同时发生时,PIC会根据优先级决定先处理哪个中断。
2.
中断控制指令
:MS - DOS提供了一些中断控制指令,用于控制中断的开启和关闭。例如,CLI指令用于关闭中断,STI指令用于开启中断。这些指令可以在程序中根据需要灵活使用,以确保程序在关键代码段不会被中断干扰。
3.
编写自定义中断处理程序
:我们可以编写自己的中断处理程序(也称为中断服务例程,ISR)来替换现有的中断处理程序。编写自定义中断处理程序的步骤如下:
- 保存现场:在中断处理程序开始时,需要保存当前的寄存器状态,以便在处理完中断后恢复现场。
- 处理中断:根据具体的中断事件,执行相应的处理代码。
- 恢复现场:在处理完中断后,恢复之前保存的寄存器状态。
- 发送中断结束信号:向8259 PIC发送中断结束信号,以通知它中断已经处理完毕。
- 返回:使用IRET指令返回被中断的程序。
4.
终止并驻留程序(TSR)
:TSR程序是一种特殊的程序,它在执行完主要任务后,并不会完全退出内存,而是驻留在内存中,等待特定的事件触发。例如,我们可以编写一个TSR程序来拦截Ctrl - Alt - Del键组合。编写TSR程序的关键步骤如下:
- 保存原中断向量:在安装自定义中断处理程序之前,需要保存原有的中断向量,以便在程序卸载时恢复。
- 安装自定义中断处理程序:将自定义中断处理程序的地址写入中断向量表中,替换原有的中断处理程序。
- 释放占用的内存:在程序执行完主要任务后,释放不需要的内存,只保留必要的部分驻留在内存中。
- 退出程序:使用INT 27H指令退出程序,使程序驻留在内存中。
5.
应用:No_Reset程序
:No_Reset程序是一个具体的应用示例,它通过编写自定义中断处理程序来拦截系统的复位信号,防止系统意外复位。其实现步骤如下:
- 编写自定义中断处理程序:在中断处理程序中,忽略复位信号,不执行复位操作。
- 安装自定义中断处理程序:将自定义中断处理程序的地址写入相应的中断向量表中。
- 运行程序:程序驻留在内存中,监控复位信号,当检测到复位信号时,执行自定义的处理逻辑。
以下是一个简单的mermaid流程图,展示了自定义中断处理程序的执行流程:
graph TD;
A[中断发生] --> B[保存现场];
B --> C[处理中断];
C --> D[恢复现场];
D --> E[发送中断结束信号];
E --> F[返回被中断的程序];
6. 硬件控制使用I/O端口
在MS - DOS编程中,我们可以通过I/O端口来控制硬件设备。以下是相关内容的介绍:
1.
输入 - 输出端口
:I/O端口是计算机与外部硬件设备进行通信的接口。每个硬件设备都有自己的I/O端口地址,通过向这些端口写入数据或从端口读取数据,我们可以控制硬件设备的行为。例如,通过向键盘控制器的I/O端口写入数据,可以模拟键盘输入;从显卡的I/O端口读取数据,可以获取显卡的状态信息。
2.
PC声音程序
:作为一个具体的应用示例,我们可以编写一个PC声音程序来控制计算机的扬声器发声。实现步骤如下:
- 初始化扬声器:通过向定时器和扬声器的I/O端口写入特定的数据,初始化扬声器的工作模式。
- 生成声音信号:根据需要的音调,计算出相应的定时器计数值,并将其写入定时器的I/O端口。
- 控制发声时间:通过延时函数控制扬声器发声的时间长度。
- 关闭扬声器:在发声结束后,向扬声器的I/O端口写入数据,关闭扬声器。
以下是一个简单的PC声音程序示例:
; 初始化定时器
mov al, 0B6h ; 选择定时器2,方式3
out 43h, al
mov ax, 1193 ; 计算计数值
out 42h, al ; 写入低字节
mov al, ah
out 42h, al ; 写入高字节
; 打开扬声器
in al, 61h
or al, 03h
out 61h, al
; 延时发声
mov cx, 1000 ; 延时时间
delay_loop:
loop delay_loop
; 关闭扬声器
in al, 61h
and al, 0FCh
out 61h, al
7. 总结
通过对MS - DOS编程的深入学习,我们了解了许多重要的知识和技术。从实地址模式下的编程练习,到专家级的段定义、运行时程序结构、中断处理和硬件控制,每一个部分都有其独特的特点和应用场景。这些知识不仅可以帮助我们理解MS - DOS系统的工作原理,还为我们从事英特尔处理器硬件级工作或成为系统级程序员提供了宝贵的经验。在实际编程中,我们可以根据具体的需求,灵活运用这些技术,开发出高效、稳定的MS - DOS程序。同时,这些知识也为我们进一步学习其他操作系统和编程语言打下了坚实的基础。
超级会员免费看
87

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



