深入探索MS-DOS编程与浮点处理
1. MS-DOS编程基础
在MS-DOS编程中,有许多重要的概念和指令需要掌握。首先,在处理段定义时,当程序员需要与使用特定段名的现有代码库适配时,就可能需要显式地定义段。
SEGMENT
和
ENDS
指令分别用于定义段的开始和结束。段的对齐类型告诉链接器在将该段与其他段组合时跳过多少字节,组合类型则说明如何合并同名段,而类类型提供了另一种组合段的方式。通过给多个段相同的名称并指定
PUBLIC
组合类型,就可以将它们合并。
ASSUME
指令能让汇编器在汇编时计算标签和变量的偏移量,而段超越前缀则指示处理器在执行当前指令时使用与默认段不同的段寄存器。
MS-DOS命令处理器会解释在命令提示符下输入的每个命令。扩展名为
COM
和
EXE
的程序被称为临时程序,它们被加载到内存中执行,执行完毕后所占用的内存会被释放。MS-DOS会在临时程序开始处创建一个名为程序段前缀的256字节特殊块。
临时程序有
COM
和
EXE
两种类型。
COM
程序是机器语言程序的未修改二进制映像,而
EXE
程序存储在磁盘上时,前面有一个
EXE
头,后面跟着包含程序本身的加载模块。
EXE
程序的头区域用于MS-DOS正确计算段和其他组件的地址。
2. 中断处理与常驻内存程序
中断处理程序(中断服务例程)简化了输入/输出以及基本系统任务。程序员还可以用自己的代码替换默认的中断处理程序,以提供更完整或定制化的服务。中断向量表位于RAM的前1024字节(地址从0:0到0:03FF),表中的每个条目都是一个32位的段偏移地址,指向一个中断服务例程。
硬件中断由8259可编程中断控制器(PIC)生成,它会向CPU发出信号,暂停当前程序的执行并执行中断服务例程。硬件中断能让CPU在重要数据丢失之前注意到后台的重要事件。不同设备可以触发中断,每个设备根据其中断请求级别(IRQ)有不同的优先级。
中断标志控制着CPU对外部(硬件)中断的响应方式。如果中断标志被设置,中断被启用;如果标志被清除,中断被禁用。
STI
(设置中断)指令启用中断,
CU
(清除中断)指令禁用中断。
终止并常驻内存(TSR)程序会将自身的一部分留在内存中。TSR程序最常见的用途是安装中断处理程序,这些处理程序会一直留在内存中,直到计算机重新启动或通过特殊的卸载程序将其移除。例如,
No_reset
程序就是一个TSR程序,它可以防止系统通过通常的
Ctrl-Alt-Delete
组合键重新启动。
3. 硬件控制:I/O端口的使用
IA - 32系统提供了两种硬件输入输出方式:内存映射和基于端口的方式。
3.1 内存映射I/O
当使用内存映射I/O时,程序可以将数据写入特定的内存地址,数据会被传输到输出设备。同样,通过从预定义的内存地址复制数据,可以从输入设备读取数据。文本视频显示器就是一个内存映射设备的例子,当在视频段中放置字符时,它们会立即出现在显示器上。
3.2 基于端口的I/O
基于端口的I/O需要使用
IN
和
OUT
指令来读写数据到特定编号的位置,这些位置称为端口。端口是CPU与其他设备(如键盘、扬声器、调制解调器和声卡)之间的连接或网关。
每个输入输出端口都有一个介于0到FFFFh之间的特定编号。例如,在控制扬声器时,可以通过快速切换扬声器锥体的进出状态来使用端口。通过设置端口参数(波特率、奇偶校验等)并通过端口发送数据,可以通过串行端口直接与异步适配器进行通信。
键盘端口是一个典型的输入输出端口。当按下一个键时,键盘控制器芯片会将一个8位扫描码发送到端口60h。按键操作会触发一个硬件中断,促使CPU调用ROM BIOS中的INT 9。INT 9从端口输入扫描码,查找该键的ASCII码,并将这两个值存储在键盘输入缓冲区中。实际上,完全可以绕过操作系统,直接从端口60h读取字符。
除了传输数据的端口,大多数硬件设备还有用于监控设备状态和控制设备行为的端口。
IN
指令用于从端口输入一个字节、字或双字,而
OUT
指令用于将一个值输出到端口。它们的语法如下:
IN accumulator,port
OUT port, accumulator
端口可以是0到FFh范围内的常量,也可以是DX中0到FFFFh之间的值。累加器在8位传输时必须是AL,16位传输时是AX,32位传输时是EAX。示例如下:
in al,3Ch ; input byte from port 3Ch
out 3Ch,al ; output byte to port 3Ch
mov dx, portNumber ; DX can contain a port number
in ax,dx ; input word from port named in DX
out dx,ax ; output word to the same port
in eax,dx ; input doubleword from port
out dx,eax ; output doubleword to same port
3.3 PC声音程序
我们可以编写一个使用
IN
和
OUT
指令通过PC内置扬声器生成声音的程序。扬声器控制端口(编号61h)通过操作Intel 8255可编程外围接口芯片来打开和关闭扬声器。要打开扬声器,需要输入端口61h的当前值,设置最低2位,然后将该字节通过端口输出。要关闭扬声器,清除位0和1并再次输出状态。
不过,如果笔记本电脑的扬声器直接连接到声卡而不是扬声器端口(61h),这个声音程序将无法产生声音。
Intel 8253定时器芯片控制所生成声音的频率(音调)。要使用它,我们需要向端口42h发送一个介于0到255之间的值。以下是一个示例程序
Speaker Demo Program
,它展示了如何通过播放一系列升调音符来生成声音:
TITLE Speaker Demo Program (Spkr.asm)
; This program plays a series of ascending notes on an IBM-PC or compatible computer.
INCLUDE Irvinel6.inc
speaker EQU 61h ; address of speaker port
timer EQU 42h ; address of timer port
delayl EQU 500
delay2 EQU 00000h ; delay between notes
.code
main PROC
in al,speaker ; get speaker status
push ax ; save status
or al,000000llb ; set lowest 2 bits
out speaker,al ; turn speaker on
mov al,60 ; starting pitch
L2:
out timer,al ; timer port: pulses speaker
; Create a delay loop between pitches.
mov cx,delayl
L3:
push cx ; outer loop
mov cx,delay2
L3a: ; inner loop
loop L3a
pop cx
loop L3
sub al,1 ; raise pitch
jnz L2 ; play another note
pop ax ; get original status
and al,llllllOOb ; clear lowest 2 bits
out speaker,al ; turn speaker off
exit
main ENDP
END main
该程序首先通过设置扬声器状态字节的最低2位来打开扬声器:
or al,000000llb ; set lowest 2 bits
out speaker,al ; turn speaker on
然后通过向定时器芯片发送60来设置音调:
mov al,60 ; starting pitch
L2:
out timer,al ; timer port: pulses speaker
在改变音调之前,程序会通过一个延迟循环暂停。由于不同计算机的处理器速度不同,延迟的时间也会有所不同,可能需要调整
delayl
和
delay2
的值:
mov cx,delayl
L3:
push cx ; outer loop
mov cx,delay2
L3a: ; inner loop
loop L3a
pop cx
loop L3
延迟之后,程序会从周期(1/频率)中减去1,从而提高音调。当循环重复时,新的频率会输出到定时器。这个过程会一直持续到
AL
中的频率计数器等于0。最后,程序从扬声器端口弹出原始状态字节,并通过清除最低2位来关闭扬声器:
pop ax ; get original status
and al,llllllOOb ; clear lowest 2 bits
out speaker,al ; turn speaker off
4. 浮点处理:二进制表示
4.1 浮点十进制数的组成
浮点十进制数包含三个组成部分:符号、有效数字和指数。例如,在数字
-1.23154 × 10⁵
中,符号为负,有效数字为1.23154,指数为5。
4.2 IEEE二进制浮点表示
Intel处理器使用IEEE组织制定的标准754 - 1985中规定的三种浮点二进制存储格式,具体如下表所示:
| 格式 | 位数 | 符号位 | 指数位 | 有效数字小数部分位数 | 近似规范化范围 | 别名 |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| 单精度 | 32位 | 1位 | 8位 | 23位 | 2⁻¹²⁶ 到 2¹²⁷ | 短实数 |
| 双精度 | 64位 | 1位 | 11位 | 52位 | 2⁻¹⁰²² 到 2¹⁰²³ | 长实数 |
| 双扩展精度 | 80位 | 1位 | 16位 | 63位 | 2⁻¹⁶³⁸² 到 2¹⁶³⁸³ | 扩展实数 |
由于这三种格式非常相似,我们将重点关注单精度格式。32位的排列方式是最高有效位(MSB)在左边,标记为
Fraction
的段表示有效数字的小数部分。各个字节在内存中以小端序存储(最低有效字节在起始地址)。
单精度格式如下:
| 1位 | 8位 | 23位 |
| ---- | ---- | ---- |
| 符号 | 指数 | 小数部分 |
符号位为1时,数字为负;为0时,数字为正,零被视为正数。
有效数字在浮点数字表达式
m * bᵉ
中,
m
被称为有效数字或尾数,
b
是基数,
e
是指数。浮点数字的有效数字由小数点左右的十进制数字组成。例如,十进制值123.154可以表示为:
123.154 = (1 × 10²) + (2 × 10¹) + (3 × 10⁰) + (1 × 10⁻¹) + (5 × 10⁻²) + (4 × 10⁻³)
所有小数点左边的数字具有正指数,右边的数字具有负指数。
二进制浮点数字也使用加权位置表示法。例如,浮点二进制值11.1011可以表示为:
11.1011 = (1 × 2¹) + (1 × 2⁰) + (1 × 2⁻¹) + (0 × 2⁻²) + (1 × 2⁻³) + (1 × 2⁻⁴)
另一种表示二进制小数点右边值的方法是将它们列为分母为2的幂的分数之和。在这个例子中,和为11/16(即0.6875):
.1011 = 1/2 + 0/4 + 1/8 + 1/16 = 11/16
生成十进制分数相对直观。十进制分子(11)表示二进制位模式1011。如果
e
是二进制小数点右边有效位的数量,十进制分母就是2ᵉ。以下是一些将二进制浮点表示法转换为十进制分数的示例:
| 二进制浮点 | 十进制分数 |
| ---- | ---- |
| 11.11 | 3 3/4 |
| 101.0011 | 5 3/16 |
| 1101.100101 | 13 37/64 |
| 0.00101 | 5/32 |
| 1.011 | 1 3/8 |
| 0.00000000000000000000001 | 1/8388608 |
同时,还有一些二进制和十进制分数的对应关系示例:
| 二进制 | 十进制分数 | 十进制值 |
| ---- | ---- | ---- |
| .1 | 1/2 | .5 |
| .01 | 1/4 | .25 |
| .001 | 1/8 | .125 |
| .0001 | 1/16 | .0625 |
| .00001 | 1/32 | .03125 |
需要注意的是,任何具有有限位数的浮点格式都无法表示整个实数连续体。例如,一个简化的浮点格式如果只有5位有效数字,就无法表示二进制值1.1111和10.0000之间的数值。扩展到IEEE双精度格式,其53位有效数字也无法表示需要54位或更多位的二进制值。
4.3 指数
单精度指数以带有127偏置的8位无符号整数形式存储。数字的实际指数必须加上127。例如,对于二进制值
1.101 × 2⁵
,实际指数(5)加上127后,得到的偏置指数(132)会存储在数字的表示中。以下是一些指数在有符号十进制、偏置十进制和无符号二进制中的表示示例:
| 指数 (E) | 偏置 (E + 127) | 二进制 |
| ---- | ---- | ---- |
| +5 | 132 | 10000100 |
| 0 | 127 | 01111111 |
| -10 | 117 | 01110101 |
| +127 | 254 | 11111110 |
| -126 | 1 | 00000001 |
| -1 | 126 | 01111110 |
偏置指数始终为正,介于1到254之间。实际指数范围是从 -126 到 +127,选择这个范围是为了确保最小可能指数的倒数不会导致溢出。
4.4 规范化二进制浮点数字
大多数浮点二进制数字以规范化形式存储,以最大化有效数字的精度。对于任何浮点二进制数字,可以通过移动二进制小数点,直到二进制小数点左边出现一个“1”来进行规范化。指数表示二进制小数点向左(正指数)或向右(负指数)移动的位数。以下是一些示例:
| 非规范化 | 规范化 |
| ---- | ---- |
| 1110.1 | 1.1101 × 2³ |
| .000101 | 1.01 × 2⁻⁴ |
| 1010001. | 1.010001 × 2⁶ |
要将规范化操作反转,可以说将二进制浮点数字非规范化。将二进制小数点移动直到指数为零。如果指数为正
n
,将二进制小数点向右移动
n
位;如果指数为负
n
,将二进制小数点向左移动
n
位,必要时填充前导零。
4.5 创建IEEE表示
一旦符号位、指数和有效数字字段被规范化并编码,就可以轻松生成完整的二进制IEEE短实数。以单精度格式为例,我们可以先放置符号位,接着是指数位,最后是有效数字的小数部分。例如,二进制
1.101 × 2⁰
的表示如下:
- 符号位: 0
- 指数: 01111111
- 小数部分: 10100000000000000000000
偏置指数(01111111)是十进制127的二进制表示。所有规范化的有效数字在二进制小数点左边都有一个“1”,因此无需显式编码该位。以下是一些单精度位编码的示例:
| 二进制值 | 偏置指数 | 符号, 指数, 小数部分 |
| ---- | ---- | ---- |
| -1.11 | 127 | 1 01111111 11000000000000000000000 |
| +1101.101 | 130 | 0 10000010 10110100000000000000000 |
| -.00101 | 124 | 1 01111100 01000000000000000000000 |
| +100111.0 | 132 | 0 10000100 00111000000000000000000 |
| +.0000001101011 | 120 | 0 01111000 10101100000000000000000 |
IEEE规范包括几种实数和非数字编码:
- 正零和负零
- 非规范化有限数字
- 规范化有限数字
- 正无穷和负无穷
- 非数值(NaN,即非数字)
- 不确定数字
不确定数字由Intel浮点单元作为对某些无效浮点操作的响应使用。
规范化有限数字是所有可以在零和无穷之间的规范化实数中编码的非零有限值。虽然似乎所有非零有限浮点数字都应该被规范化,但当它们的值接近零时,这是不可能的。例如,当FPU计算出结果为
1.0101111 × 2⁻¹²⁹
时,其指数太小,无法存储在单精度数字中,会产生下溢异常条件,数字会通过每次向左移动1位二进制小数点逐渐非规范化,直到指数达到有效范围:
1.01011110000000000001111 × 2⁻¹²⁹
0.10101111000000000000111 × 2⁻¹²⁸
0.01010111100000000000011 × 2⁻¹²⁷
0.00101011110000000000001 × 2⁻¹²⁶
在这个例子中,由于二进制小数点的移动,有效数字会有一些精度损失。
正无穷(+∞)表示最大的正实数,负无穷(-∞)表示最大的负实数。可以将无穷与其他值进行比较:-∞ 小于 +∞,-∞ 小于任何有限数字,+∞ 大于任何有限数字。任何一种无穷都可能表示浮点溢出条件,即计算结果无法规范化,因为其指数太大,无法用可用的指数位表示。
NaN是不代表任何有效实数的位模式。IA - 32架构包括两种类型的NaN:安静NaN可以在大多数算术运算中传播而不会导致异常,信号NaN可用于生成浮点无效操作异常。编译器可能会用信号NaN值填充未初始化的数组,这样任何对该数组进行计算的尝试都会产生异常。安静NaN可用于在调试会话期间保存诊断信息,程序可以在NaN中自由编码任何所需的信息。浮点单元不会尝试对NaN执行操作,Intel IA - 32手册详细说明了当两种类型的NaN组合用作操作数时确定指令结果的一组规则。
以下是一些常见浮点操作中值的特定编码:
| 编码 | 描述 |
| ---- | ---- |
| 0 00000000 00000000000000000000000 | 正零 |
| 1 00000000 00000000000000000000000 | 负零 |
| 0 11111111 00000000000000000000000 | 正无穷 |
| 1 11111111 00000000000000000000000 | 负无穷 |
| 0 11111111 1xxxxxxxxx…xxxxxxxx | 安静NaN |
| 0 11111111 0xxxxxxxxx…xxxxxxxx | 信号NaN |
综上所述,MS-DOS编程和浮点处理是计算机编程中非常重要的领域,涉及到许多底层的概念和技术。理解这些知识对于开发高效、稳定的程序至关重要。无论是硬件控制还是浮点运算,都需要程序员深入掌握相关的原理和指令,才能编写出高质量的代码。
深入探索MS-DOS编程与浮点处理
5. 浮点处理单元(FPU)
5.1 FPU寄存器栈
浮点处理单元(FPU)拥有一个寄存器栈,通常由8个80位的寄存器(ST(0) - ST(7))组成。这个栈用于存储浮点数值,在进行浮点运算时,数据会在这些寄存器之间流动。例如,当执行一个加法运算时,操作数会从栈中取出,运算结果再压入栈中。
FPU的寄存器栈操作遵循后进先出(LIFO)的原则。当进行数据压入操作时,栈指针会向下移动;而进行弹出操作时,栈指针会向上移动。这种栈结构使得FPU能够高效地处理一系列的浮点运算,尤其是在进行复杂的数学计算时。
5.2 舍入
在浮点运算中,舍入是一个重要的概念。由于浮点格式的位数有限,很多时候运算结果无法精确表示,这就需要进行舍入操作。FPU支持多种舍入模式,常见的有:
-
就近舍入
:将结果舍入到最接近的可表示值。如果结果正好位于两个可表示值的中间,则舍入到偶数。
-
向零舍入
:直接截断小数部分,只保留整数部分。
-
向上舍入
:将结果舍入到比它大的最接近的可表示值。
-
向下舍入
:将结果舍入到比它小的最接近的可表示值。
程序员可以根据具体的应用需求选择合适的舍入模式,以确保运算结果的准确性。
5.3 浮点异常
在浮点运算过程中,可能会出现各种异常情况。FPU能够检测并处理这些异常,常见的浮点异常包括:
-
溢出异常
:当运算结果的指数超出了浮点格式所能表示的范围时,会发生溢出异常。例如,在单精度格式中,如果指数超过了+127,就会产生溢出。
-
下溢异常
:当运算结果的指数小于浮点格式所能表示的最小指数时,会发生下溢异常。如前面提到的
1.0101111 × 2⁻¹²⁹
这种情况。
-
除数为零异常
:当进行除法运算时,如果除数为零,会触发该异常。
-
无效操作异常
:例如对负数取平方根等无效的浮点操作会引发此异常。
-
不精确异常
:当运算结果无法精确表示,需要进行舍入时,会产生不精确异常。
FPU会根据异常的类型设置相应的状态标志,程序员可以通过检查这些标志来处理异常情况。同时,FPU还支持异常屏蔽功能,允许程序员选择忽略某些异常。
5.4 浮点指令集
FPU提供了丰富的指令集,用于执行各种浮点运算。这些指令可以分为以下几类:
-
算术指令
:如加法(FADD)、减法(FSUB)、乘法(FMUL)、除法(FDIV)等,用于执行基本的数学运算。
-
比较指令
:如FCOM(比较)、FCOMP(比较并弹出栈顶元素)等,用于比较两个浮点数值的大小。
-
数据传输指令
:如FLD(加载浮点值到栈顶)、FST(存储栈顶浮点值)等,用于在寄存器栈和内存之间传输数据。
以下是一些浮点指令的使用示例:
FLD st(0) ; 将栈顶元素复制一份压入栈顶
FADD st(1) ; 将栈顶元素和次栈顶元素相加,结果存于栈顶
FSTP st(0) ; 弹出栈顶元素并存储到指定位置
5.5 浮点值的读写
在程序中,我们需要对浮点值进行读写操作。读取浮点值通常是从内存中加载到FPU寄存器栈,而写入则是将寄存器栈中的值存储到内存。
例如,要从内存中读取一个单精度浮点值到FPU寄存器栈,可以使用以下代码:
mov eax, [floatValue] ; 将内存中的浮点值加载到EAX
fld dword ptr [eax] ; 将EAX指向的单精度浮点值加载到栈顶
要将栈顶的浮点值存储到内存中,可以使用:
fstp dword ptr [floatResult] ; 将栈顶的单精度浮点值弹出并存储到指定内存位置
6. Intel指令编码
6.1 IA - 32指令格式
IA - 32处理器的指令具有特定的格式,通常由操作码、操作数和前缀组成。操作码指定了指令要执行的操作,操作数则是指令操作的对象,前缀可以修改指令的行为。
指令格式可以用以下mermaid流程图表示:
graph TD;
A[前缀] --> B[操作码];
B --> C[操作数];
前缀可以是段超越前缀、操作数大小前缀等。段超越前缀用于指定使用不同的段寄存器,操作数大小前缀用于指定操作数的大小。
6.2 单字节指令
单字节指令是指指令长度为一个字节的指令,这类指令通常是一些简单的操作,如NOP(空操作)、HLT(停机)等。单字节指令的操作码直接包含在这一个字节中,没有额外的操作数或前缀。
例如,NOP指令的机器码是90h,当处理器执行到这个指令时,会简单地跳过一个时钟周期,不进行任何实际的操作。
6.3 立即数到寄存器的移动
将立即数移动到寄存器的指令是常见的操作。例如,MOV指令可以将一个立即数赋值给寄存器。
MOV EAX, 1234h ; 将立即数1234h移动到EAX寄存器
这种指令的格式相对简单,操作码指定了MOV操作,后面跟着目标寄存器和立即数。
6.4 寄存器模式指令
寄存器模式指令是指操作数都在寄存器中的指令。例如,ADD指令可以对两个寄存器中的值进行加法运算。
ADD EAX, EBX ; 将EBX寄存器的值加到EAX寄存器中
这类指令的操作码指定了具体的操作(如加法),操作数则是寄存器。
6.5 IA - 32处理器操作数大小前缀
在IA - 32处理器中,操作数的大小可以是8位、16位或32位。操作数大小前缀可以用于指定指令操作的操作数大小。
例如,默认情况下,MOV指令操作的是32位操作数,但如果使用操作数大小前缀,可以将其改为16位操作。
66h MOV AX, BX ; 66h是操作数大小前缀,指定操作数为16位
6.6 内存模式指令
内存模式指令是指操作数涉及到内存的指令。例如,MOV指令可以将内存中的值移动到寄存器,或者将寄存器中的值移动到内存。
MOV EAX, [memoryAddress] ; 将内存地址memoryAddress处的值移动到EAX寄存器
MOV [memoryAddress], EBX ; 将EBX寄存器的值移动到内存地址memoryAddress处
内存模式指令需要指定内存地址的计算方式,常见的有直接寻址、间接寻址、基址加偏移寻址等。
7. 总结与展望
在计算机编程的广阔领域中,MS-DOS编程和浮点处理是两个基础且重要的方面。MS-DOS编程涉及到段定义、中断处理、硬件控制等底层概念,这些知识对于理解计算机系统的运行机制和开发高效的程序至关重要。通过掌握
SEGMENT
、
ENDS
、
ASSUME
等指令,程序员可以更好地管理内存和组织代码;利用中断处理程序,能够实现高效的输入输出和系统任务调度;而对I/O端口的操作,则为硬件控制提供了直接的手段。
浮点处理则是处理实数运算的关键技术。IEEE二进制浮点表示法为浮点数的存储和运算提供了标准,通过理解符号位、指数和有效数字的编码方式,程序员可以准确地处理各种浮点数值。FPU的寄存器栈、指令集和异常处理机制,使得浮点运算能够高效、稳定地进行。同时,Intel指令编码的知识有助于程序员理解处理器如何执行指令,优化代码的性能。
未来,随着计算机技术的不断发展,这些基础知识仍然具有重要的价值。在嵌入式系统开发、操作系统内核编程等领域,对底层知识的掌握将变得更加关键。同时,随着人工智能、大数据等新兴领域的兴起,浮点运算的性能和精度要求也在不断提高,这将促使我们进一步深入研究浮点处理技术,开发更加高效的算法和程序。
希望通过本文的介绍,读者能够对MS-DOS编程和浮点处理有更深入的理解,为今后的编程工作打下坚实的基础。在实际应用中,不断实践和探索,将这些知识运用到具体的项目中,创造出更优秀的软件作品。
超级会员免费看
6

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



