ASM:《X86汇编语言-从实模式到保护模式》第9章:实模式下中断机制和实时时钟

时间:2024-01-16 22:34:20

  中断是处理器一个非常重要的工作机制。第9章是讲中断在实模式下如何工作,第17章是讲中断在保护模式下如何工作。

★PART1:外部硬件中断

  外部硬件中断是通过两个信号线引入处理器内部的,这两条线分别叫NMI和INTR。处理器正在运行的时候会收到各种各样的中断,有些中断必须被处理,这就叫非屏蔽中断;有一些中断的处理优先级没有那么高,并且可以屏蔽,这就叫可屏蔽中断

1. 非屏蔽中断(Non Maskable Interrupt,NMI)

  一旦处理器接受到NMI,说明处理器遇到了严重事件,这个时候必须无条件地处理这个事件。中断信号的来源称为中断源。NMI的中断源通过一个与非门连接到处理器。处理器的NMI引脚是高电平有效的,而中断信号是低电平有效的。当不存在中断的时候,与非门的所有输入都是高,因此处理器的NMI引脚是低电平,这意味着没有中断发生。当有任何一个非屏蔽中断的产生,则与非门的输出为高。Intel处理器规定,NMI中断信号由0跳到1后,至少要维持4个以上的时钟周期才算是有效的,才能被识别。在实模式下,NMI被赋予了统一的中断号2,不会再细分。一旦发生2号中断,处理器和操作系统停止工作,给出严重错误的信息。

2. 可屏蔽中断

  可屏蔽中断是通过INTR引脚进入处理器内部的,像NMI一样,不可能为所有的中断源都提供一个引脚,处理器每次只能处理一个中断。所以处理器使用中断代理来处理这件事。中断代理可以仲裁中断的优先级和向处理器发送中断。其中最常用的就是8259芯片(可编程中断控制器(Programming Interrupt Controller,PIC))。在现在,绝大多数的单处理器都使用这个芯片作为中断代理。Intel处理器允许256个中断(中断号是0-255),8259负责其中的15个。中断号不固定,可以通过编程来改变它的中断号。可以通过in和out指令来访问8259。

  事实上8259是级联(Cascade)的两块芯片(每块芯片有8个中断输入引脚),主片(Master,端口号0x20和0x21,0x20是状态端口(可以用于发送EOI),0x21是参数端口)的代理输出INT直接传送到处理器的INTR引脚上,从片(Slave,端口号0xa0和0xa1,0xa0是状态端口(可以用于发送EOI),0x21是参数端口)的INT输出送到第一块的引脚2上。

  8259芯片的内部,有中断屏蔽寄存器(Interrupt Mask Register,IMR),这是个8位寄存器,对应着芯片的8个中断输入引脚,每个对应着0和1的状态,当为“0”则表示从该引脚传送来的中断信号能够被继续处理,为“1”则该中断会被IMR阻断,具体8259的详细的工作方式可以看这里:http://www.cnblogs.com/Philip-Tell-Truth/articles/5169767.html

  处理器内部的FLAGS有IF位,决定了是否响应从INTR引脚来的中断信号,当IF为0则不接受,为1则处理器可以接受和响应中断。置零IF位可以用cli(Clear Interrupt flag)指令,置位用sti(Set Interrupt flag)。

3. 中断向量表(Interrupt Vector Table,IVT)

  中断向量表只在实模式下才有意义,处理器可以识别256个中断,每个中断向量占两个字(偏移地址:段地址),从物理地址0x00000到0x003ff结束(1KB)。

处理器执行中断过程如下:

    1. 保护断点现场。首先要将FLAGS寄存器压栈,然后清除IF和TF位(TF位是陷阱标志),注意这个时候IF被清除,处理器无法响应其他中断,但是可以用sti指令形成中断嵌套。然后,再将当前的代码段寄存器CS和IP压栈。
    2. 执行中断处理过程。处理器根据中断号乘以4得到在中断向量表中的偏移地址,然后将中断向量表相应位置的偏移地址和段地址分别赋予IP和CS,执行中断过程。
    3. 返回到断点继续执行。所有中断处理器指令的最后一条指令必须是中断返回指令iret。这将导致处理器依次从栈中弹出和恢复IP,CS和FLAGS的内容。
    4. 当NMI发生时,处理器不会从外部获得中断号,它自动生成中断代码2,其他处理过程和可屏蔽中断相同。

  中断向量表的建立和初始化是由BIOS在计算机启动的时候负责完成的,BIOS为每个中断号填写相同入口地址,并且这个地址对应的内存只有一条指令就是iret。操作系统和用户需要根据需要自己修改IVT中的偏移地址和段地址,再编写相应的代码来实现相应中断执行相应过程。

★PART2:实时时钟和CMOS RAM

  在南桥ICH内部,集成了实时时钟电路(Real Time Clock,RTC)和两块CMOS组成的静态存储器(CMOS RAM),实时时钟电路负责即使,而日期和时间的数值则存储在这块存储器中,8259的主片的IR0接的是系统定时器/计数器芯片;从片的IR0接的就是RTC。

  实时时钟是由主板内的电池供电的,为整个计算机提供了基准时间,为所有需要时间的软件和硬件提供服务。RTC芯片也可以提供闹钟和周期性中断功能。

  日期和时间是保存在CMOS RAM中的,通常由128字节,而日期和时间信息只占了小部分容量,其他空间则用于保存整机的配置信息。比如各种硬件的类型和工作参数,开机密码和辅助存储设备的启动顺序等。

  RTC芯片是由一个振荡频率为32.768KHZ的石英晶体振荡器驱动的,经分频后,用于对CMOS RAM进行每秒一次的时间刷新。常规的日期和时间的信息占据了CMOS RAM的开始的10个字节。报警的时,分,秒用于产生到时间报警中断,如果他们的内容是0xc0-0xFF则表示不使用报警功能。CMOS RAM中时间信息表如下:

ASM:《X86汇编语言-从实模式到保护模式》第9章:实模式下中断机制和实时时钟

  CMOS的访问,需要通过两个端口来进行,0x70或者0x74是索引端口用来指定CMOS RAM内的单元,0x71和0x75是数据端口,用来读写相应内存单元的内容。比如可以像下面一样读取星期:

  ASM:《X86汇编语言-从实模式到保护模式》第9章:实模式下中断机制和实时时钟

  端口0x70的最高位是控制NMI中断的开关,当它为0时,允许中断到达处理器;当它为1则阻断所有的NMI信号,其他的7个比特用于指定CMOS RAM单元的索引号,CMOS RAM中保存的日期和时间,通常是以二进制编码的十进制数(Binary Coded Decimal,BCD),这是默认状态,如果需要,也可以设置成按正常的二进制数表示,比如十进制数25,BCD编码就是00100101(BCD编码的高低4位都不能大于1001,否则无效)。

  单元0x0a~0x0d不是普通的储存单元,而是4个索引寄存器(8位寄存器)的索引号,也是通过0x70和0x71访问的,这4个寄存器用于设定实时时钟电路的参数和工作状态。

寄存器A:

    ASM:《X86汇编语言-从实模式到保护模式》第9章:实模式下中断机制和实时时钟

  注:寄存器的RS主要记住1101,1110和1111这三个时间。

寄存器B:

ASM:《X86汇编语言-从实模式到保护模式》第9章:实模式下中断机制和实时时钟

  寄存器C和D是标志寄存器,这些标志反映了RTC的工作状态,寄存器C是只读的,寄存器可读可写,他们都是8位的寄存器。

寄存器C:

  ASM:《X86汇编语言-从实模式到保护模式》第9章:实模式下中断机制和实时时钟

寄存器D:

  ASM:《X86汇编语言-从实模式到保护模式》第9章:实模式下中断机制和实时时钟

  一旦响应了中断,8259中断控制器就无法知道该中断什么时候才能处理结束,需要显式发送中断信号给主片和从片发送中断结束命令(End Of Interrupt,EOI),中断命令代码是0x20,一般主片和从片都要发EOI,但是如果外部中断仅仅是主片处理的,可以只发给主片(发给端口0x20);当中断是从片处理的,就要都发(主片0x20和从片0xa0)。

  ASM:《X86汇编语言-从实模式到保护模式》第9章:实模式下中断机制和实时时钟

  要注意的是,如果计算机进入了停机状态(hlt),则需要中断来唤醒,该指令可以降低处理器的功耗。

★PART3:内部中断和软中断

1. 内部中断

  和硬件中断不一样,内部中断发生在处理器内部。是由执行的指令引起的。比如执行idiv指令时除数为0。

  内部中断不受标志寄存器的IF位的影响,也不需要中断识别总线周期。他们的中断类型是固定的,可以立即转入相应的处理过程。

2. 软中断

  软中断是通过int指令引起的中断处理,这类中断也不需要识别总线周期,中断号在指令中给出。int指令的格式如下:

ASM:《X86汇编语言-从实模式到保护模式》第9章:实模式下中断机制和实时时钟

  int3是断点中断指令,用于程序调试。into是溢出中断指令,如果标志寄存器OF位是1,那么则产生4号中断,否则啥都不做。

  int是软中断指令,后面跟一个8位数的操作码,用于指定中断号(把中断号乘以4在IVT找到相应的偏移地址:段地址)。

  可以位所有的中断自定义中断处理过程,包括软中断,硬件中断和软中断(中断号大部分都没有被硬件和处理器内部占用)。

3. BIOS中断

  简单的来说,BIOS中断也是一种软中断,只是这些中断功能是计算机加点之后由BIOS建立的,这些中断功能在加载和执行主引导扇区代码之前就可以使用了。BIOS中断主要是为了方便地使用最基本的硬件访问功能。比如可以直接int 0x16来调用键盘服务,调用此中断时,AH指定具体的功能编号,AL会得到中断返回的ASCII码。

ASM:《X86汇编语言-从实模式到保护模式》第9章:实模式下中断机制和实时时钟

  具体的中断怎么用可以查表,练习给出了一个读取键盘信息并且在屏幕上打印的小例程。

  BIOS可能会给一些简单的外围设备提供初始化代码和功能调用代码,并填写中断向量表,但是也有一些BIOS中断是由外围设备接口自己建立的。每个外部设备接口都有自己的只读存储器,这些ROM中提供了它自己的功能调用例程。以及本设备的初始化代码。按照规范,前两个单元的内容是0x55和0xAA,第三个单元是本ROM中以512字节为单元的代码长度;从第四个单元开始,就是实际的ROM代码。

  在实模式下的物理内存0xA0000-0xFFFFF中,有一部分就是留给外围设备的,如果这个设备存在,那么它自带的ROM就会映射到分配给他的地址范围内。

  在计算机启动的期间,BIOS程序会以2KB为单位搜索内存地址0XC0000-0xE0000之间的区域,当它发现某个区域的头两个字节是0x55和0xAA时,那就意味着这个区域有ROM代码的存在,而且是有效的,接着,它对这个区域做累加和检查。看结果是否和第三个单元相符合。如果相符,就从第四个单元开始进入。这个时候,处理器执行的是硬件自带的程序指令,这些指令初始化外部设备的相关寄存器和工作状态。最后,填写相关的中断向量表,使他们指向自带的中断处理程序。

★PART4:本章习题

1. 屏蔽中断信号

这一题要求我们对8259编程,屏蔽除了RTC外的其他所有中断,并且观察时钟的变化速度。说实话书上是没有说清楚的,可以参考一下我刚才给出的链接,一定要注意主片和从片的状态接口和参数接口的问题,不要写错了,否则将会引发严重错误。主引导程序还是用的第八章那个。代码其实挺好理解的,学过保护模式就觉得实模式的程序真是好简单。

 software_start equ          ;用户程序加载的磁盘的地址

 ;===================================================
SECTION loader_header align= vstart=0x7c00
;===================================================
mov ax, ;设置栈区指针
mov ss,ax
mov sp,ax mov ax,[cs:phy_base] ;设置寄存器的位置,准备从对应磁盘位置读取相应的数据
mov dx,[cs:phy_base+]
;进行32位的除法(高位在bx上,低位在ax上)
mov bx,0x10 ;右移1位
div bx
;设置两个变址偏移和基址指针bx,把两个段寄存器指向相应位置
xor di,di ;这里情况比较特殊,因为磁盘号太小了所以di直接置空就可以了
mov si,software_start
xor bx,bx
mov ds,ax
mov es,ax
call Read_HardDisk mov ax,[0x00] ;获取整个程序的大小
mov dx,[0x02]
mov bx,0x200 ;0x200=512
div bx
cmp dx, ;如果计算结果为0,则ZF=1
jnz @Allocate_Start ;如果余数是0,那么就说明除尽
dec ax ;否则需要减去一个扇区,因为已经预读了一个了 @Allocate_Start:
cmp ax,0x00
jz Realloc_Header ;如果小于1个扇区,则直接开始分配 call Read_Other_Harddisk;读取其他扇区 Realloc_Header: ;重新分配段地址,段首
mov ax,[0x06]
mov dx,[0x08]
call Set_Segment
mov [0x06],ax ;回填 mov cx,[0x0a] ;需要计算的段地址的个数
mov bx,0x0c ;程序代码段偏移地址 Realloc_Other_Segment:
mov ax,[bx]
mov dx,[bx+0x02]
call Set_Segment
mov [bx],ax
add bx, ;bx记得自增!
loop Realloc_Other_Segment jmp far [0x04] ;段间转移,直接去程序段执行程序 ;====================================================
Read_HardDisk:
;现在ax里面是内存加载的段地址,ds和es都指向了磁盘区域,si和di是磁盘号
push ax
push bx
push cx
push dx mov dx,0x1f2 ;磁盘接口0x1f2:读取的磁盘数为1个
mov al,
out dx,al inc dx ;磁盘接口0x1f3:磁盘号的0-7位:si的0-7位
mov ax,si
out dx,al inc dx ;磁盘接口0x1f4:磁盘号的8-15位:si的8-15位
mov al,ah
out dx,al inc dx ;磁盘接口0x1f5:磁盘号的16-23位:di的0-7位
mov ax,di
out dx,al inc dx
mov ax,0xe0 ;磁盘接口0x1f6:磁盘号的24-31位:LBA模式主硬盘
or al,ah
out dx,al inc dx ;磁盘接口0x1f7:读命令0x20,写命令0x30
mov ax,0x20
out dx,al _Wait:
in al,dx ;磁盘接口0x1f7:这个端口既是命令端口,又是状态端口,第7位1表示在准备中,准备好后第七位置零,同时第3位变1
and al,0x88 ;保留第7位和第3位
cmp al,0x08
jnz _Wait mov cx,
mov dx,0x1f0 ;磁盘接口0x1f0:数据端口,准备读取256个字(注意是字,不是字节)也就是一个扇区的大小(512字节) _Read_data:
in ax,dx
mov [bx],ax
add bx,
loop _Read_data pop dx
pop cx
pop bx
pop ax ret
;====================================================
Set_Segment: ;重定位过程,dx:ax是32位的地址,现在需要把它弄成16位的
push dx add ax,[cs:phy_base] ;一定要注意偏移地址是cs
adc dx,[cs:phy_base+] ;是dx不是bx,不要搞错了,
;因为本来start的汇编地址都是相对于应用程序开头的了
;现在我们要做的就是把他们我们的加载地址,然后把它们搞成段地址
;这就是为什么所有的用户程序段都要16字节对齐的原因,不然右移会出问题
shr ax,
ror dx,
and dx,0xf000 ;清掉低12位
or ax,dx pop dx ;ax的内容就是16位的段地址 ret
;====================================================
Read_Other_Harddisk:
push ds mov cx,ax ;统计还要读入的扇区数
@run:
xor di,di ;设置新的磁盘位置
inc si mov ax,ds ;把段地址往前移动两个位置指向新的段
add ax,0x20
mov ds,ax xor bx,bx ;还要注意设置新的偏移地址
call Read_HardDisk
loop @run pop ds
ret
;====================================================
phy_base dd 0x10000 ;用户程序加载内存地址 times -($-$$) db ;填充0,末尾填充0xaa55
dw 0xaa55
;===============================================================================
SECTION header vstart= ;定义用户程序头部段
program_length dd program_end ;程序总长度[0x00] ;用户程序入口点
code_entry dw start ;偏移地址[0x04]
dd section.code.start ;段地址[0x06] realloc_tbl_len dw (header_end-realloc_begin)/
;段重定位表项个数[0x0a] realloc_begin:
;段重定位表
code_segment dd section.code.start ;[0x0c]
data_segment dd section.data.start ;[0x14]
stack_segment dd section.stack.start ;[0x1c] header_end: ;===============================================================================
SECTION code align= vstart= ;定义代码段(16字节对齐)
new_int_0x70:
push ax
push bx
push cx ;必须把cs也压栈了,不然等一下是回不去的!
push dx
push es mov al,0xef ;开放主片的IR5
out 0x20,al ;写回此寄存器
.w0:
mov al,0x0a ;阻断NMI。当然,通常是不必要的
or al,0x80
out 0x70,al
in al,0x71 ;读寄存器A
test al,0x80 ;测试第7位UIP
jnz .w0 ;以上代码对于更新周期结束中断来说
;是不必要的
xor al,al
or al,0x80
out 0x70,al
in al,0x71 ;读RTC当前时间(秒)
push ax mov al,
or al,0x80
out 0x70,al
in al,0x71 ;读RTC当前时间(分)
push ax mov al,
or al,0x80
out 0x70,al
in al,0x71 ;读RTC当前时间(时)
push ax mov al,0x0c ;寄存器C的索引。且开放NMI
out 0x70,al
in al,0x71 ;读一下RTC的寄存器C,否则只发生一次中断
;此处不考虑闹钟和周期性中断的情况
mov ax,0xb800
mov es,ax pop ax
call bcd_to_ascii
mov bx,* + * ;从屏幕上的12行36列开始显示 mov [es:bx],ah
mov [es:bx+],al ;显示两位小时数字 mov al,':'
mov [es:bx+],al ;显示分隔符':'
not byte [es:bx+] ;反转显示属性
and byte [es:bx+],0x07 pop ax
call bcd_to_ascii
mov [es:bx+],ah
mov [es:bx+],al ;显示两位分钟数字 mov al,':'
mov [es:bx+],al ;显示分隔符':'
not byte [es:bx+] ;反转显示属性
and byte [es:bx+],0x07 pop ax
call bcd_to_ascii
mov [es:bx+],ah
mov [es:bx+],al ;显示两位小时数字 mov al,0x20 ;中断结束命令EOI
out 0xa0,al ;向从片发送
out 0x20,al ;向主片发送 pop es
pop dx
pop cx
pop bx
pop ax iret ;-------------------------------------------------------------------------------
bcd_to_ascii: ;BCD码转ASCII
;输入:AL=bcd码
;输出:AX=ascii
mov ah,al ;分拆成两个数字
and al,0x0f ;仅保留低4位
add al,0x30 ;转换成ASCII shr ah, ;逻辑右移4位
and ah,0x0f
add ah,0x30 ret ;-------------------------------------------------------------------------------
start:
mov ax,[stack_segment]
mov ss,ax
mov sp,ss_pointer
mov ax,[data_segment]
mov ds,ax mov bx,init_msg ;显示初始信息
call put_string mov bx,inst_msg ;显示安装信息
call put_string mov al,0x70
mov bl,
mul bl ;计算0x70号中断在IVT中的偏移
mov bx,ax cli ;防止改动期间发生新的0x70号中断 push es
mov ax,0x0000
mov es,ax
mov word [es:bx],new_int_0x70 ;偏移地址。 mov word [es:bx+],cs ;段地址
pop es mov al,0x0b ;RTC寄存器B
or al,0x80 ;阻断NMI
out 0x70,al
mov al,0x12 ;设置寄存器B,禁止周期性中断,开放更
out 0x71,al ;新结束后中断,BCD码,24小时制 mov al,0x0c
out 0x70,al
in al,0x71 ;读RTC寄存器C,复位未决的中断状态 mov al,0xfe
out 0xa1,al ;只留从片的IR0
mov ax,0xfb
out 0x21,al sti ;重新开放中断 mov bx,done_msg ;显示安装完成信息
call put_string mov bx,tips_msg ;显示提示信息
call put_string mov cx,0xb800
mov ds,cx
mov byte [* + *],'@' ;屏幕第12行,35列 .idle:
mov al,0xef ;关闭主片的IR2
out 0x20,al ;写回此寄存器
hlt ;使CPU进入低功耗状态,直到用中断唤醒
not byte [* + *+] ;反转显示属性 jmp .idle ;-------------------------------------------------------------------------------
put_string: ;显示串(0结尾)。
;输入:DS:BX=串地址
mov cl,[bx]
or cl,cl ;cl=0 ?
jz .exit ;是的,返回主程序
call put_char
inc bx ;下一个字符
jmp put_string .exit:
ret ;-------------------------------------------------------------------------------
put_char: ;显示一个字符
;输入:cl=字符ascii
push ax
push bx
push cx
push dx
push ds
push es ;以下取当前光标位置
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
in al,dx ;高8位
mov ah,al mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
in al,dx ;低8位
mov bx,ax ;BX=代表光标位置的16位数 cmp cl,0x0d ;回车符?
jnz .put_0a ;不是。看看是不是换行等字符
mov ax,bx ;
mov bl,
div bl
mul bl
mov bx,ax
jmp .set_cursor .put_0a:
cmp cl,0x0a ;换行符?
jnz .put_other ;不是,那就正常显示字符
add bx,
jmp .roll_screen .put_other: ;正常显示字符
mov ax,0xb800
mov es,ax
shl bx,
mov [es:bx],cl ;以下将光标位置推进一个字符
shr bx,
add bx, .roll_screen:
cmp bx, ;光标超出屏幕?滚屏
jl .set_cursor mov ax,0xb800
mov ds,ax
mov es,ax
cld
mov si,0xa0
mov di,0x00
mov cx,
rep movsw
mov bx, ;清除屏幕最底一行
mov cx,
.cls:
mov word[es:bx],0x0720
add bx,
loop .cls mov bx, .set_cursor:
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
mov al,bh
out dx,al
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
mov al,bl
out dx,al pop es
pop ds
pop dx
pop cx
pop bx
pop ax ret ;===============================================================================
SECTION data align= vstart= init_msg db 'Starting...',0x0d,0x0a, inst_msg db 'Installing a new interrupt 70H...', done_msg db 'Done.',0x0d,0x0a, tips_msg db 'Clock is now working.', ;===============================================================================
SECTION stack align= vstart= ;这里栈默认大小256字节 resb
ss_pointer: ;当前sp是0,当push时,回绕到最顶上 ;===============================================================================
SECTION program_trail
program_end:

2. 用新的周期性中断

 ;=====================================================
;时钟Demo
SECTION Program_Header align= vstart=
Program_Length: dd Program_end ;程序总长度 [0x00]
Code_Entry: dw start ;入口点偏移量 [0x04]
dd section.code.start ;入口点段地址 [0x06]
;等下从加载器跳过来,然后从入口点拿一个双字的内容,0x06是段地址,0x04是偏移地址 ralloc_section_nums: dw (segment_end-segment_start)/
;段重定位个数 [0x0a]
segment_start:
code_segment: dd section.code.start ;代码段开始 [0x0c] 其实这里不要也没关系,但是要改加载器
data_segment: dd section.data.start ;数据段开始 [0x10]
stack_segment: dd section.stack.start ;栈段的开始 [0x14] segment_end:
;=====================================================
SECTION code align= vstart=
start:
cli ;不允许中断
mov ax,[stack_segment] ;设置栈段
mov ss,ax
mov sp,ss_poniter mov ax,[data_segment] ;设置数据段
mov ds,ax mov bx,inform1
call put_string
mov bx,inform2
call put_string mov al,0x70 ;计算70号中断在cs的偏移位置
mov bl,
mul bl
mov bx,ax ;放到bx上去保存 push es
mov ax,0x0000
mov es,ax
mov word [es:bx],print_clock ;设定偏移地址
mov [es:bx+],cs ;设定段地址
pop es call Set_RTC
sti ;开放中断 mov ax,0xb800
mov es,ax
mov byte[es:* + *],'!'
mov byte[es:* + *],'!' @loop:
hlt ;使CPU进入低功耗状态,直到用中断唤醒
not byte[es:* + *+] ;反转字符属性,使得!变来变去
not byte[es:* + *+]
jmp @loop
;=====================================================
Set_RTC:
mov ax,0x0b
or al,0x80 ;阻断NMI
out 0x70,al ;访问寄存器b
mov al,0x42 ;周期性更新,BCD模式,24小时模式
out 0x71,al mov ax,0x0a ;设置分频点路时间
or al,0x80
out 0x70,al
in al,0x71 ;先读一下看al的内容
and al,0xf0
or al,0x0e ;一秒4次中断
out 0x71,al ;重新写入0x71端口 mov ax,0x0c
out 0x70,al
in al,0x71 ;读一下寄存器C使得标记消失 mov ax,0xfe ;只开放从片的IR0端口
out 0xa1,al ;从新写回从片
mov ax,0xfb
out 0x21,al
ret
;=====================================================
print_clock:
push ax
push bx
push cx
push dx
push es @wait:
mov ax,0x0a
or al,0x80 ;阻断NMI
out 0x70,al ;读寄存器a
in al,0x71
test al,0x80 ;看是否处于更新周期或即将进入更新,否则就等到可以进入为止
jnz @wait xor al,al
or al,0x80
out 0x70,al
in al,0x71 ;阻断NMI,读取秒
push ax mov ax,
or al,0x80
out 0x70,al
in al,0x71 ;阻断NMI,读取分
push ax mov ax,
or al,0x80
out 0x70,al
in al,0x71 ;阻断NMI,读取时
push ax mov al,0x0c
out 0x70,al
in al,0x71 ;读取一下寄存器C,不然下次就不产生中断了 mov ax,0xb800
mov es,ax mov bx,* + * ;从屏幕上的12行36列开始显示
mov cx, ;时,分
@1:
pop ax
call bcd_to_ascii
mov [es:bx],ah ;显示高位
add bx,
mov [es:bx],al ;显示低位
add bx, mov al,':' ;显示:
mov [es:bx],al
inc bx
not byte[es:bx] ;反转属性
and byte[es:bx],0x07
inc bx
loop @1
pop ax ;显示秒
call bcd_to_ascii
mov [es:bx],ah ;显示高位
add bx,
mov [es:bx],al ;显示低位 mov ax,0x20
out 0xa0,al ;发送EOI命令
out 0x20,al pop es
pop dx
pop cx
pop bx
pop ax iret
;=====================================================
bcd_to_ascii:
mov ah,al
and al,0x0f
add al,0x30 shr ah,
and ah,0x0f
add ah,0x30
ret
;=====================================================
put_string:
mov cl,[bx]
or cl,cl
jz .exit
call put_char
inc bx
jmp put_string
.exit:
ret put_char:
push ax
push bx
push cx
push dx
push ds
push es mov dx,0x3d4 ;设置光标的索引端口
mov al,0x0e
out dx,al
mov dx,0x3d5
in al,dx
mov ah,al ;0x0e是高8位
mov dx,0x3d4
mov al,0x0f
out dx,al ;0x0f是低8位
mov dx,0x3d5
in al,dx
mov bx,ax
set_0d:
cmp cl,0x0d
jnz set_0a ;不是回车符,看是不是换行符
mov bl,
div dl ;扔掉余数
mul dl ;直接得到本行的偏移地址
mov bx,ax
jmp set_cursor ;回车就直接设置光标了
set_0a:
cmp cl,0x0a
jnz put_other
add bx, ;如果是换行,直接把光标放到下一行就可以了
jmp roll_screen
put_other:
mov ax,0xb800
mov es,ax
shl bx, ;把bx的位置乘以2(ASCII+属性)
mov [es:bx],cl shr bx, ;千万记得要把bx弄回去然后+1,因为光标要移动的!
add bx,
jmp roll_screen
roll_screen:
cmp bx, ;2000是屏幕的字符总数,如果超过2000,那就滚屏
jl set_cursor ;小于直接设置光标,大于等于滚屏
mov ax,0xb800
mov es,ax
mov ds,ax
cld
mov di,0x00
mov si,0xa0
mov cx,
rep movsw mov cx,
mov bx,
@2:
mov word [es:bx],0x0720
add bx,
loop @2
mov bx, ;记得清空最后一行后要把bx放到最后一行的前面哦!
set_cursor:
mov dx,0x3d4 ;设置光标的新高位
mov al,0x0e
out dx,al
mov al,bh
mov dx,0x3d5
out dx,al mov dx,0x3d4 ;设置光标的新低位
mov al,0x0f
out dx,al
mov al,bl
mov dx,0x3d5
out dx,al pop es
pop ds
pop dx
pop cx
pop bx
pop ax
ret
;=====================================================
SECTION data align= vstart=
inform1: db ' This is a clock demo--by Philip',0x0d,0x0a,
inform2: db ' The clock is running',
;=====================================================
SECTION stack align= vstart=
resb ;声明256个字节的区域
ss_poniter:
;=====================================================
SECTION Program_Trial
Program_end:

3. 从键盘中读取字符,显示到屏幕上

这本来是书上的例程,作为练手的我自己打了一遍,其实也挺简单的,据说我们隔壁的电子系的实验有一个就是做这个……

 ;=====================================================
;中断BIOS:从键盘中获得字符
SECTION Program_Header align= vstart=
Program_Length: dd Program_end ;程序总长度 [0x00]
Code_Entry: dw start ;入口点偏移量 [0x04]
dd section.code.start ;入口点段地址 [0x06]
;等下从加载器跳过来,然后从入口点拿一个双字的内容,0x06是段地址,0x04是偏移地址 ralloc_section_nums: dw (segment_end-segment_start)/
;段重定位个数 [0x0a]
segment_start:
code_segment: dd section.code.start ;代码段开始 [0x0c] 其实这里不要也没关系,但是要改加载器
data_segment: dd section.data.start ;数据段开始 [0x10]
stack_segment: dd section.stack.start ;栈段的开始 [0x14] segment_end:
;=====================================================
SECTION code align= vstart=
start:
cli ;不允许中断
mov ax,[stack_segment] ;设置栈位置
mov ss,ax
mov ax,ss_pointer
mov sp,ax
mov ax,[data_segment] ;设置数据区位置
mov ds,ax
sti ;开放中断 mov cx,msg_end-message
mov bx,message
_putc:
mov ah,0x0e
mov al,[bx]
int 0x10
inc bx
loop _putc _jmp:
mov ah,0x00
int 0x16 ;此时字符在al处 mov ah,0x0e
int 0x10
jmp near _jmp ;=====================================================
SECTION data align= vstart=
message: db 'Hello, guy!',0x0d,0x0a
db 'This simple procedure used to demonstrate '
db 'the BIOS interrupt.',0x0d,0x0a
db 'Please press the keys on the keyboard ->'
msg_end:
;=====================================================
SECTION stack align= vstart=
resb
ss_pointer:
;=====================================================
SECTION Program_Trial
Program_end: