硬中断
- 硬中断的工作原理
- 软中断的工作原理
- 中断向量表
硬中断一般是外部硬件中断-就是从处理器外部来的中断信号。
当外部设备发生错误或者有数据要传送时,或者处理器交给它的任务处理完了,它都会向处理器发送信号,高速处理器。
如下图,外部硬件中断是通过两个信号线引入处理器内部的。这两根线的名字恩别叫做NMI和INTR!
当一个中断发生时,处理器将会通过中断引脚NMI和INTR得到通知。除此之外,处理器还需要知道发生了什么,以便采取适当的处理措施。
每种类型的中断都被统一编号,这称为中断类型号、中断向量或者中断号。
非屏蔽中断(不可屏蔽中断)
NMI接收的是不可屏蔽中断。
不可屏蔽中断的中断号统一为2(因为所有不可屏蔽中断都几乎是致命的无法解决,所以处理器干脆不处理的,统一将他们的中断号令为2,处理器接收到2号中断时直接放弃继续正常工作,也不会试图纠正已经发生的错误。)。
可屏蔽中断
和NMI不同,INTR一般接收的是可屏蔽中断。大多数的中断也是可屏蔽中断。
INTR接收可屏蔽中断。
Intel处理器允许有256个中断。中断号是0~255。如下图是一个中断控制器,它管理传送来的中断信号,决定是否将中断传送给CPU。
以上两个8259芯片负责15个中断号的管理。注意这里为什么是15个而不是256个呢?其实是每次它管理的中断号不一样的,这一次可能管理的是10-25号,下次可能管理的是其他的号。
那么在哪里屏蔽中断或者不屏蔽中断呢?实际上有两个位置,一个是8259内部,一个是处理器内部。
8259内部:在8259内部有一个中断屏蔽寄存器,这是个8位寄存器,对应着该芯片的8个输入引脚,对应的位是0还是1决定了该引脚输入的中断是否能通过8259芯片。0表示允许 1表示阻断
处理器内部:除了要看8259内部,最终还要看处理器。处理器内部的标志寄存器FLAGS有一个标志IF。这是中断标志。当IF为0时所有从INTR来的中断都被忽略。当IF为1时处理器可以接收和相应中断。
实模式下中断向量表
所谓中断处理,就是处理器要执行一段与该中断有关系的程序(指令)。处理器可以识别256个中断,那么理论上就需要256个中断程序。
这些程序的实际位置并不重要,重要的是,在实模式中,处理器要求将它们的入口点(还记得程序的入口点么?不记得话看上一篇文章)集中存放到内存中的从物理地址0x00000处(0x0000:0x0000)到0x003ff处(0x0000:0x03ff)。共1KB的内存空间。这就是所谓的中断向量表。
如下图是实模式下的中断向量表:
中断信号来自哪个引脚,8259芯片是最清楚的,所以他会把相应的中断号告诉处理器,处理器拿着这个中断号,要顺序做以下几件事:
保护断点的现场
首先将FLAGS寄存器压栈,然后清楚它的IF位(防止在中断的时候被打断,如果想要有嵌套中断,可以在编写中断处理程序时适时使用sti指令开放中断)
然后依次将CS于IP寄存器压栈
执行中断处理程序
由于处理器已经拿到了中断号,它用中断号乘以4(见上图)就得到了该中断的入口点的偏移地址和段地址
接着将得到的段地址与偏移地址传送给CS和IP
返回到断点接着执行
所有中断的程序的最后是一条iret指令。这将导致处理器一次从栈中弹出IP CS FLAGS的原始内容,于是转到主程序继续执行
由于中断处理程序返回时已经将FLAGS内容恢复,所以IF标志位也恢复。也就是说可以接收新的中断
软中断
和硬件中断不同,软中断是处理器内部产生的。是由执行指令引起的。
软中断是由int指令引起的。这类中断不需要中断识别总线周期,中断号在指令中给出。
实时时钟、 CMOS RAM 和 BCD 编码
为什么计算机能够准确地显示日期和时间?原因很简单,如图 9-2 所示, 在外围设备控制器芯片 ICH 内部,集成了实时时钟电路(Real Time Clock, RTC)和两小块由互补金属氧化物(CMOS)材料组成的静态存储器(CMOS RAM)。实时时钟电路负责计时,而日期和时间的数值则存储在这块存储器中!
RTC 芯片由一个振荡频率为 32.768kHz 的石英晶体振荡器(晶振)驱动,经分频后,用于对CMOS RAM 进行每秒一次的时间刷新!
CMOS RAM 的访问,需要通过两个端口来进行。 0x70 或者 0x74 是索引端口,用来指定 CMOS RAM 内的单元; 0x71 或者 0x75 是数据端口,用来读写相应单元里的内容。举个例子,以下代码用于读取今天是星期几:
mov al,0x06
out 0x70,al
in al,0x71
不得不说的是,从很早的时候开始,端口 0x70 的最高位(bit 7)是控制 NMI 中断的开关。当它为 0 时,允许 NMI 中断到达处理器,为 1 时,则阻断所有的 NMI 信号,其他 7 个比特,即 0~6 位,则实际上用于指定 CMOS RAM 单元的索引号,这种规定直到现在也没有改变。 如图 9-4 所示,尽管端口 0x70 的位 7 不是中断信号,但它能控制与非门的输出,决定真正的NMI 中断信号是否能到达处理器!
代码清单
代码如下:
1 ;代码清单9-1
2 ;文件名:c09_1.asm
3 ;文件说明:用户程序
4 ;创建日期:2011-4-16 22:03
5
6 ;===============================================================================
7 SECTION header vstart=0 ;定义用户程序头部段
8 program_length dd program_end ;程序总长度[0x00]
9
10 ;用户程序入口点
11 code_entry dw start ;偏移地址[0x04]
12 dd section.code.start ;段地址[0x06]
13
14 realloc_tbl_len dw (header_end-realloc_begin)/4
15 ;段重定位表项个数[0x0a]
16
17 realloc_begin:
18 ;段重定位表
19 code_segment dd section.code.start ;[0x0c]
20 data_segment dd section.data.start ;[0x14]
21 stack_segment dd section.stack.start ;[0x1c]
22
23 header_end:
24
25 ;===============================================================================
26 SECTION code align=16 vstart=0 ;定义代码段(16字节对齐)
27 new_int_0x70:
28 push ax
29 push bx
30 push cx
31 push dx
32 push es
33
34 .w0:
35 mov al,0x0a ;阻断NMI。当然,通常是不必要的
36 or al,0x80
37 out 0x70,al
38 in al,0x71 ;读寄存器A
39 test al,0x80 ;测试第7位UIP
40 jnz .w0 ;以上代码对于更新周期结束中断来说
41 ;是不必要的
42 xor al,al
43 or al,0x80
44 out 0x70,al
45 in al,0x71 ;读RTC当前时间(秒)
46 push ax
47
48 mov al,2
49 or al,0x80
50 out 0x70,al
51 in al,0x71 ;读RTC当前时间(分)
52 push ax
53
54 mov al,4
55 or al,0x80
56 out 0x70,al
57 in al,0x71 ;读RTC当前时间(时)
58 push ax
59
60 mov al,0x0c ;寄存器C的索引。且开放NMI
61 out 0x70,al
62 in al,0x71 ;读一下RTC的寄存器C,否则只发生一次中断
63 ;此处不考虑闹钟和周期性中断的情况
64 mov ax,0xb800
65 mov es,ax
66
67 pop ax
68 call bcd_to_ascii
69 mov bx,12*160 + 36*2 ;从屏幕上的12行36列开始显示
70
71 mov [es:bx],ah
72 mov [es:bx+2],al ;显示两位小时数字
73
74 mov al,':'
75 mov [es:bx+4],al ;显示分隔符':'
76 not byte [es:bx+5] ;反转显示属性
77
78 pop ax
79 call bcd_to_ascii
80 mov [es:bx+6],ah
81 mov [es:bx+8],al ;显示两位分钟数字
82
83 mov al,':'
84 mov [es:bx+10],al ;显示分隔符':'
85 not byte [es:bx+11] ;反转显示属性
86
87 pop ax
88 call bcd_to_ascii
89 mov [es:bx+12],ah
90 mov [es:bx+14],al ;显示两位小时数字
91
92 mov al,0x20 ;中断结束命令EOI
93 out 0xa0,al ;向从片发送
94 out 0x20,al ;向主片发送
95
96 pop es
97 pop dx
98 pop cx
99 pop bx
100 pop ax
101
102 iret
103
104 ;-------------------------------------------------------------------------------
105 bcd_to_ascii: ;BCD码转ASCII
106 ;输入:AL=bcd码
107 ;输出:AX=ascii
108 mov ah,al ;分拆成两个数字
109 and al,0x0f ;仅保留低4位
110 add al,0x30 ;转换成ASCII
111
112 shr ah,4 ;逻辑右移4位
113 and ah,0x0f
114 add ah,0x30
115
116 ret
117
118 ;-------------------------------------------------------------------------------
119 start:
120 mov ax,[stack_segment]
121 mov ss,ax
122 mov sp,ss_pointer
123 mov ax,[data_segment]
124 mov ds,ax
125
126 mov bx,init_msg ;显示初始信息
127 call put_string
128
129 mov bx,inst_msg ;显示安装信息
130 call put_string
131
132 mov al,0x70
133 mov bl,4
134 mul bl ;计算0x70号中断在IVT中的偏移
135 mov bx,ax
136
137 cli ;防止改动期间发生新的0x70号中断
138
139 push es
140 mov ax,0x0000
141 mov es,ax
142 mov word [es:bx],new_int_0x70 ;偏移地址。
143
144 mov word [es:bx+2],cs ;段地址
145 pop es
146
147 mov al,0x0b ;RTC寄存器B
148 or al,0x80 ;阻断NMI
149 out 0x70,al
150 mov al,0x12 ;设置寄存器B,禁止周期性中断,开放更
151 out 0x71,al ;新结束后中断,BCD码,24小时制
152
153 mov al,0x0c
154 out 0x70,al
155 in al,0x71 ;读RTC寄存器C,复位未决的中断状态
156
157 in al,0xa1 ;读8259从片的IMR寄存器
158 and al,0xfe ;清除bit 0(此位连接RTC)
159 out 0xa1,al ;写回此寄存器
160
161 sti ;重新开放中断
162
163 mov bx,done_msg ;显示安装完成信息
164 call put_string
165
166 mov bx,tips_msg ;显示提示信息
167 call put_string
168
169 mov cx,0xb800
170 mov ds,cx
171 mov byte [12*160 + 33*2],'@' ;屏幕第12行,35列
172
173 .idle:
174 hlt ;使CPU进入低功耗状态,直到用中断唤醒
175 not byte [12*160 + 33*2+1] ;反转显示属性
176 jmp .idle
177
178 ;-------------------------------------------------------------------------------
179 put_string: ;显示串(0结尾)。
180 ;输入:DS:BX=串地址
181 mov cl,[bx]
182 or cl,cl ;cl=0 ?
183 jz .exit ;是的,返回主程序
184 call put_char
185 inc bx ;下一个字符
186 jmp put_string
187
188 .exit:
189 ret
190
191 ;-------------------------------------------------------------------------------
192 put_char: ;显示一个字符
193 ;输入:cl=字符ascii
194 push ax
195 push bx
196 push cx
197 push dx
198 push ds
199 push es
200
201 ;以下取当前光标位置
202 mov dx,0x3d4
203 mov al,0x0e
204 out dx,al
205 mov dx,0x3d5
206 in al,dx ;高8位
207 mov ah,al
208
209 mov dx,0x3d4
210 mov al,0x0f
211 out dx,al
212 mov dx,0x3d5
213 in al,dx ;低8位
214 mov bx,ax ;BX=代表光标位置的16位数
215
216 cmp cl,0x0d ;回车符?
217 jnz .put_0a ;不是。看看是不是换行等字符
218 mov ax,bx ;
219 mov bl,80
220 div bl
221 mul bl
222 mov bx,ax
223 jmp .set_cursor
224
225 .put_0a:
226 cmp cl,0x0a ;换行符?
227 jnz .put_other ;不是,那就正常显示字符
228 add bx,80
229 jmp .roll_screen
230
231 .put_other: ;正常显示字符
232 mov ax,0xb800
233 mov es,ax
234 shl bx,1
235 mov [es:bx],cl
236
237 ;以下将光标位置推进一个字符
238 shr bx,1
239 add bx,1
240
241 .roll_screen:
242 cmp bx,2000 ;光标超出屏幕?滚屏
243 jl .set_cursor
244
245 mov ax,0xb800
246 mov ds,ax
247 mov es,ax
248 cld
249 mov si,0xa0
250 mov di,0x00
251 mov cx,1920
252 rep movsw
253 mov bx,3840 ;清除屏幕最底一行
254 mov cx,80
255 .cls:
256 mov word[es:bx],0x0720
257 add bx,2
258 loop .cls
259
260 mov bx,1920
261
262 .set_cursor:
263 mov dx,0x3d4
264 mov al,0x0e
265 out dx,al
266 mov dx,0x3d5
267 mov al,bh
268 out dx,al
269 mov dx,0x3d4
270 mov al,0x0f
271 out dx,al
272 mov dx,0x3d5
273 mov al,bl
274 out dx,al
275
276 pop es
277 pop ds
278 pop dx
279 pop cx
280 pop bx
281 pop ax
282
283 ret
284
285 ;===============================================================================
286 SECTION data align=16 vstart=0
287
288 init_msg db 'Starting...',0x0d,0x0a,0
289
290 inst_msg db 'Installing a new interrupt 70H...',0
291
292 done_msg db 'Done.',0x0d,0x0a,0
293
294 tips_msg db 'Clock is now working.',0
295
296 ;===============================================================================
297 SECTION stack align=16 vstart=0
298
299 resb 256
300 ss_pointer:
301
302 ;===============================================================================
303 SECTION program_trail
304 program_end:
用户程序的入口点在代码清单 9-1 的第 119 行,从这一行开始,到第 124 行,用于初始化各个段寄存器的内容。下面开始在中断向量表中安装实时时钟中断的入口点。既然本章的主题是中断, 那么就很有必要强调一件事。当处理器执行任何一条改变堆栈段寄存器 SS 的指令时,它会在下一条指令执行完期间禁止中断!
想象一下,如果刚刚修改了段寄存器 SS,在还没来得及修改 SP 的情况下,就发生了中断,会出现什么后果,而且要知道,中断是需要依靠堆栈来工作的。 因此,处理器在设计的时候就规定,当遇到修改段寄存器 SS 的指令时,在这条指令和下一条指令执行完毕期间,禁止中断,以此来保护堆栈。换句话说,你应该在修改段寄存器 SS 的指令之后,紧跟着一条修改堆栈指针 SP 的指令!
就代码清单 9-1 来说,在第 121、 122 行执行期间,处理器禁止中断!
RTC 芯片的中断信号,通向中断控制器 8259 从片的第 1 个中断引脚 IR0。在计算机启动期间,BIOS 会初始化中断控制器,将主片的中断号设为从 0x08 开始,将从片的中断号设为从 0x70 开始。 所以,计算机启动后, RTC 芯片的中断号默认是 0x70。
在安装中断向量之前,应该先显示些什么。第 126~130 行,显示两行提示信息,表明正在安装中断向量。这两个字符串位于第 286 行的数据段中。对于过程 put_string 没有什么好说的,它的代码和上一章相同,工作过程更没有区别!
为了修改某中断在中断向量表中的登记项,需要先找到它。第 132~135 行,将中断号 0x70 乘以 4,就是它在中断向量表内的偏移!
第 137 行,修改中断向量表时,需要先用 cli 指令清中断。
第 139~141 行,将段寄存器 ES 压栈暂时保存,并使它指向中断向量表(所在的段)!
接着,第 142~145 行,访问中断向量表内 0x70 号中断的表项,分别写入新中断处理过程的偏移地址和段地址。新的中断处理过程是从标号 new_int_0x70 处开始的,而且位于当前代码段内。所以,该中断处理过程的偏移地址就是标号 new_int_0x70 的汇编地址(注意,段 code 的定义中带有vstart=0 子句), 段地址就是当前段寄存器 CS 的内容。表项修改完毕,从堆栈中恢复段寄存器 ES的原始内容。
接下来,我们要设置 RTC 的工作状态,使它能够产生中断信号给 8259 中断控制器。
为了设置该中断,代码清单 9-1 第 147 行,将 RTC 寄存器 B 的索引 0x0b 传送到寄存器 AL。在访问 RTC 期间,最好是阻断 NMI,因此,第 148、 149 行,先用 or 指令将 AL 的最高位置 1,再写端口 0x70。
第 150、 151 行,用于通过数据端口 0x71 写寄存器 B。写的内容是 0x12,其二进制形式为00010010,对照表 9-3,其意义不难理解:允许更新周期照常发生,禁止周期性中断,禁止闹钟功能,允许更新周期结束中断,使用 24 小时制,日期和时间采用 BCD 编码。
每次当中断实际发生时,可以在程序(中断处理过程)中读寄存器 C 的内容来检查中断的原因。比如,每当更新周期结束中断发生时, RTC 就将它的第 4 位置 1。该寄存器还有一个特点,就是每次读取它后,所有内容自动清零。而且,如果不读取它的话(换句话说,相应的位没有清零),同样的中断将不再产生。 为此,第 153~155 行,读一下寄存器 C 的内容,使之开始产生中断信号。注意,在向索引端口 0x70 写入的同时,也打开了 NMI。毕竟,这是最后一次在主程序中访问 RTC。
RTC 芯片设置完毕后,再来打通它到 8259 的最后一道屏障。正常情况下, 8259 是不会允许 RTC中断的,所以,需要修改它内部的中断屏蔽寄存器 IMR。 IMR 是一个 8 位寄存器,位 0 对应着中断输入引脚 IR0,位 7 对应着引脚 IR7,相应的位是 0 时,允许中断,为 1 时,关掉中断。 8259 芯片是我见过的芯片中,访问起来最麻烦,也是我最讨厌的一个。好在有关它的资料非常好找,这里就简单地进行讲解。代码清单 9-1 第 157~159 行,通过端口 0xa1 读取 8259 从片的 IMR寄存器,用 and 指令清除第 0 位,其他各位保持原状,然后再写回去。于是, RTC 的中断可以被8259 处理了。 第 161 行, sti 指令将标志寄存器的 IF 位置 1,开放设备中断。从这个时候开始,中断随时都会发生,也随时会被处理。
在为中断过程做了初始化工作之后,主程序还是要继续执行的。代码清单 9-1 第 163~167行,用于显示中断处理程序已安装成功的消息。
接着,第 169~171 行,使段寄存器 DS 指向显示缓冲区,并在屏幕上的第 12 行 33 列显示一个字符“@”,该位置差不多是整个屏幕的中心。表达式 12160 + 332 是在指令编译阶段计算的,是该字符在显存中的位置。每个字符在显存中占 2 字节的位置,每行 80 个字符。
第 174 行, hlt 指令使处理器停止执行指令,并处于停机状态,这将降低处理器的功耗。
第 174~176 行用于形成一个循环,先是停机,接着某个外部中断使处理器恢复执行。 一旦处理器的执行点来到 hlt 指令之后,则立即使它继续处于停机状态。
第 42~52 行,分别访问 CMOS RAM 的 0、 2、 4 号单元,从中读取当前的秒、 分、时数据,按顺序压栈等待后续操作。 第 60~62 行,读一下 RTC 的寄存器 C,使得所有中断标志复位。这等于是告诉 RTC,中断已经得到处理,可以继续下一次中断。否则的话, RTC 看到中断未被处理,将不再产生中断信号。 RTC产生中断的原因有多种,可以在程序中通过读寄存器 C 来判断具体的原因。不过这里不需要,因为除了更新周期结束中断外,其他中断都被关闭了。
现在,终于可以在屏幕上显示时间信息了。
第 64、 65 行,临时将段寄存器 ES 指向显示缓冲区。 第 67、 68 行,首先从堆栈中弹出小时数,调用过程 bcd_to_ascii 来将用 BCD 码表示的“小时”转换成 ASCII。该过程是在第 105 行定义的,调用该过程时,寄存器 AL 中的高 4 位和低 4 位分别是“小时”的十位数字和个位数字。 第 108 行,将寄存器 AL 中的内容复制一份给 AH,以方便下一步操作。 第 109、 110 行,将寄存器 AL 中的高 4 位清零,只留下“小时”的个位数字。接着,将它加上0x30,就得到该数字对应的 ASCII 码。 十位上的数字在寄存器 AH 的高 4 位。第 112 行,用右移 4 位的方法,将“拉”到低 4 位,高 4 位在移动的过程中自动清零。 接着,第 113、 114 行,用同样的办法来得到十位数字的 ASCII 码。此时,寄存器 AH 中是十位数字的 ASCII 码, AL 中是个位数字的 ASCII 码,它们将作为结果返回给调用者。 最后,第 116 行用于返回调用者。
接着回到第 69 行,为了连续在屏幕上显示内容,最好是采用基址寻址来访问显存。这一行用于指定显示的内容位于显存的什么位置。实际上,这里指定的是第 12 行 36 列。同以前一样,每个字符在显存中占两个字节,每行 80 个字符,所以这里使用了表达式 12160 + 362,该表达式的值是在编译阶段计算的。 第 71、 72 行,分别将“小时”的两个数位写到显存中, 段地址在 ES 中,偏移地址分别是由寄存器 BX 和 BX+2 提供的。这里没有写入显示属性,这是因为我们希望采用默认的显示属性(屏幕是黑的,默认的显示属性是 0x07,即黑底白字)。 第 74、 75 行,用于在下一个屏幕位置显示冒号“:”,这是在显示时间时都会采用的分隔符。
最后,第 96~102 行,从堆栈中恢复被中断程序的现场,并用中断返回指令 iret 回到中断之前的地方继续执行。 iret 的意思是 Interrupt Return。
9-2 代码清单如下:
1 ;代码清单9-2
2 ;文件名:c09_2.asm
3 ;文件说明:用于演示BIOS中断的用户程序
4 ;创建日期:2012-3-28 20:35
5
6 ;===============================================================================
7 SECTION header vstart=0 ;定义用户程序头部段
8 program_length dd program_end ;程序总长度[0x00]
9
10 ;用户程序入口点
11 code_entry dw start ;偏移地址[0x04]
12 dd section.code.start ;段地址[0x06]
13
14 realloc_tbl_len dw (header_end-realloc_begin)/4
15 ;段重定位表项个数[0x0a]
16
17 realloc_begin:
18 ;段重定位表
19 code_segment dd section.code.start ;[0x0c]
20 data_segment dd section.data.start ;[0x14]
21 stack_segment dd section.stack.start ;[0x1c]
22
23 header_end:
24
25 ;===============================================================================
26 SECTION code align=16 vstart=0 ;定义代码段(16字节对齐)
27 start:
28 mov ax,[stack_segment]
29 mov ss,ax
30 mov sp,ss_pointer
31 mov ax,[data_segment]
32 mov ds,ax
33
34 mov cx,msg_end-message
35 mov bx,message
36
37 .putc:
38 mov ah,0x0e
39 mov al,[bx]
40 int 0x10
41 inc bx
42 loop .putc
43
44 .reps:
45 mov ah,0x00
46 int 0x16
47
48 mov ah,0x0e
49 mov bl,0x07
50 int 0x10
51
52 jmp .reps
53
54 ;===============================================================================
55 SECTION data align=16 vstart=0
56
57 message db 'Hello, friend!',0x0d,0x0a
58 db 'This simple procedure used to demonstrate '
59 db 'the BIOS interrupt.',0x0d,0x0a
60 db 'Please press the keys on the keyboard ->'
61 msg_end:
62
63 ;===============================================================================
64 SECTION stack align=16 vstart=0
65
66 resb 256
67 ss_pointer:
68
69 ;===============================================================================
70 SECTION program_trail
71 program_end:
从键盘读字符并显示
代码清单 9-2 在框架上和前面的用户程序是一致的,差别在于代码段的功能上。 代码清单 9-2 第 28~32 行用于初始化各个段寄存器,这和以前的做法是相同的。 第 34~42 行用于在屏幕上显示字符串,采用的是循环的方法。循环用的是 loop 指令,为此,第 34 行用于计算字符串的长度,并传送到寄存器 CX 中,以控制循环的次数。第 35 行用于取得字符串的首地址。 向屏幕上写字符使用的是 BIOS 中断,具体地说就是中断 0x10 的 0x0e 号功能,该功能用于在屏幕上的光标位置处写一个字符,并推进光标位置。第 38~40 行分别按规范的要求准备各个参数,执行软中断。 第 41、 42 行将递增寄存器 BX 中的偏移地址,以指向下一个字符在数据段中的位置。然后, loop指令将寄存器 CX 的内容减 1,并在其不为零的情况下返回到循环体开始处,继续显示下一个字符。 剩下的工作内容既复杂,又简单。复杂是指,从键盘读取你按下的那个键,并把它显示在屏幕上,很复杂,需要访问硬件,写一大堆指令。简单是指,因为有了 BIOS 功能调用,这只需几条语句就能完成。 第 45、 46 行使用软中断 0x16 从键盘读字符,需要在寄存器 AH 中指定 0x00 号功能。该中断返回后,寄存器 AL 中为字符的 ASCII 码。 第 48~50 行又一次使用了 int 0x10 的 0x0e 号功能,把从键盘取得的字符显示在屏幕上。 第 52 行,执行一个无条件转移指令,重新从键盘读取新的字符并予以显示。