ASM:《X86汇编语言-从实模式到保护模式》第9章:实模式下中断机制和实时时钟...
中断是处理器一个非常重要的工作机制。第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)。
处理器执行中断过程如下:
- 保护断点现场。首先要将FLAGS寄存器压栈,然后清除IF和TF位(TF位是陷阱标志),注意这个时候IF被清除,处理器无法响应其他中断,但是可以用sti指令形成中断嵌套。然后,再将当前的代码段寄存器CS和IP压栈。
- 执行中断处理过程。处理器根据中断号乘以4得到在中断向量表中的偏移地址,然后将中断向量表相应位置的偏移地址和段地址分别赋予IP和CS,执行中断过程。
- 返回到断点继续执行。所有中断处理器指令的最后一条指令必须是中断返回指令iret。这将导致处理器依次从栈中弹出和恢复IP,CS和FLAGS的内容。
- 当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中时间信息表如下:
CMOS的访问,需要通过两个端口来进行,0x70或者0x74是索引端口用来指定CMOS RAM内的单元,0x71和0x75是数据端口,用来读写相应内存单元的内容。比如可以像下面一样读取星期:
端口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:
注:寄存器的RS主要记住1101,1110和1111这三个时间。
寄存器B:
寄存器C和D是标志寄存器,这些标志反映了RTC的工作状态,寄存器C是只读的,寄存器可读可写,他们都是8位的寄存器。
寄存器C:
寄存器D:
一旦响应了中断,8259中断控制器就无法知道该中断什么时候才能处理结束,需要显式发送中断信号给主片和从片发送中断结束命令(End Of Interrupt,EOI),中断命令代码是0x20,一般主片和从片都要发EOI,但是如果外部中断仅仅是主片处理的,可以只发给主片(发给端口0x20);当中断是从片处理的,就要都发(主片0x20和从片0xa0)。
要注意的是,如果计算机进入了停机状态(hlt),则需要中断来唤醒,该指令可以降低处理器的功耗。
★PART3:内部中断和软中断
1. 内部中断
和硬件中断不一样,内部中断发生在处理器内部。是由执行的指令引起的。比如执行idiv指令时除数为0。
内部中断不受标志寄存器的IF位的影响,也不需要中断识别总线周期。他们的中断类型是固定的,可以立即转入相应的处理过程。
2. 软中断
软中断是通过int指令引起的中断处理,这类中断也不需要识别总线周期,中断号在指令中给出。int指令的格式如下:
int3是断点中断指令,用于程序调试。into是溢出中断指令,如果标志寄存器OF位是1,那么则产生4号中断,否则啥都不做。
int是软中断指令,后面跟一个8位数的操作码,用于指定中断号(把中断号乘以4在IVT找到相应的偏移地址:段地址)。
可以位所有的中断自定义中断处理过程,包括软中断,硬件中断和软中断(中断号大部分都没有被硬件和处理器内部占用)。
3. BIOS中断
简单的来说,BIOS中断也是一种软中断,只是这些中断功能是计算机加点之后由BIOS建立的,这些中断功能在加载和执行主引导扇区代码之前就可以使用了。BIOS中断主要是为了方便地使用最基本的硬件访问功能。比如可以直接int 0x16来调用键盘服务,调用此中断时,AH指定具体的功能编号,AL会得到中断返回的ASCII码。
具体的中断怎么用可以查表,练习给出了一个读取键盘信息并且在屏幕上打印的小例程。
BIOS可能会给一些简单的外围设备提供初始化代码和功能调用代码,并填写中断向量表,但是也有一些BIOS中断是由外围设备接口自己建立的。每个外部设备接口都有自己的只读存储器,这些ROM中提供了它自己的功能调用例程。以及本设备的初始化代码。按照规范,前两个单元的内容是0x55和0xAA,第三个单元是本ROM中以512字节为单元的代码长度;从第四个单元开始,就是实际的ROM代码。
在实模式下的物理内存0xA0000-0xFFFFF中,有一部分就是留给外围设备的,如果这个设备存在,那么它自带的ROM就会映射到分配给他的地址范围内。
在计算机启动的期间,BIOS程序会以2KB为单位搜索内存地址0XC0000-0xE0000之间的区域,当它发现某个区域的头两个字节是0x55和0xAA时,那就意味着这个区域有ROM代码的存在,而且是有效的,接着,它对这个区域做累加和检查。看结果是否和第三个单元相符合。如果相符,就从第四个单元开始进入。这个时候,处理器执行的是硬件自带的程序指令,这些指令初始化外部设备的相关寄存器和工作状态。最后,填写相关的中断向量表,使他们指向自带的中断处理程序。
★PART4:本章习题
1. 屏蔽中断信号
这一题要求我们对8259编程,屏蔽除了RTC外的其他所有中断,并且观察时钟的变化速度。说实话书上是没有说清楚的,可以参考一下我刚才给出的链接,一定要注意主片和从片的状态接口和参数接口的问题,不要写错了,否则将会引发严重错误。主引导程序还是用的第八章那个。代码其实挺好理解的,学过保护模式就觉得实模式的程序真是好简单。
1 software_start equ 100 ;用户程序加载的磁盘的地址 2 3 ;=================================================== 4 SECTION loader_header align=16 vstart=0x7c00 5 ;=================================================== 6 mov ax,0 ;设置栈区指针 7 mov ss,ax 8 mov sp,ax 9 10 mov ax,[cs:phy_base] ;设置寄存器的位置,准备从对应磁盘位置读取相应的数据 11 mov dx,[cs:phy_base+2] 12 ;进行32位的除法(高位在bx上,低位在ax上) 13 mov bx,0x10 ;右移1位 14 div bx 15 ;设置两个变址偏移和基址指针bx,把两个段寄存器指向相应位置 16 xor di,di ;这里情况比较特殊,因为磁盘号太小了所以di直接置空就可以了 17 mov si,software_start 18 xor bx,bx 19 mov ds,ax 20 mov es,ax 21 call Read_HardDisk 22 23 mov ax,[0x00] ;获取整个程序的大小 24 mov dx,[0x02] 25 mov bx,0x200 ;0x200=512 26 div bx 27 cmp dx,0 ;如果计算结果为0,则ZF=1 28 jnz @Allocate_Start ;如果余数是0,那么就说明除尽 29 dec ax ;否则需要减去一个扇区,因为已经预读了一个了 30 31 @Allocate_Start: 32 cmp ax,0x00 33 jz Realloc_Header ;如果小于1个扇区,则直接开始分配 34 35 call Read_Other_Harddisk;读取其他扇区 36 37 Realloc_Header: ;重新分配段地址,段首 38 mov ax,[0x06] 39 mov dx,[0x08] 40 call Set_Segment 41 mov [0x06],ax ;回填 42 43 mov cx,[0x0a] ;需要计算的段地址的个数 44 mov bx,0x0c ;程序代码段偏移地址 45 46 Realloc_Other_Segment: 47 mov ax,[bx] 48 mov dx,[bx+0x02] 49 call Set_Segment 50 mov [bx],ax 51 add bx,4 ;bx记得自增! 52 loop Realloc_Other_Segment 53 54 jmp far [0x04] ;段间转移,直接去程序段执行程序 55 56 ;==================================================== 57 Read_HardDisk: 58 ;现在ax里面是内存加载的段地址,ds和es都指向了磁盘区域,si和di是磁盘号 59 push ax 60 push bx 61 push cx 62 push dx 63 64 mov dx,0x1f2 ;磁盘接口0x1f2:读取的磁盘数为1个 65 mov al,1 66 out dx,al 67 68 inc dx ;磁盘接口0x1f3:磁盘号的0-7位:si的0-7位 69 mov ax,si 70 out dx,al 71 72 inc dx ;磁盘接口0x1f4:磁盘号的8-15位:si的8-15位 73 mov al,ah 74 out dx,al 75 76 inc dx ;磁盘接口0x1f5:磁盘号的16-23位:di的0-7位 77 mov ax,di 78 out dx,al 79 80 inc dx 81 mov ax,0xe0 ;磁盘接口0x1f6:磁盘号的24-31位:LBA模式主硬盘 82 or al,ah 83 out dx,al 84 85 inc dx ;磁盘接口0x1f7:读命令0x20,写命令0x30 86 mov ax,0x20 87 out dx,al 88 89 _Wait: 90 in al,dx ;磁盘接口0x1f7:这个端口既是命令端口,又是状态端口,第7位1表示在准备中,准备好后第七位置零,同时第3位变1 91 and al,0x88 ;保留第7位和第3位 92 cmp al,0x08 93 jnz _Wait 94 95 mov cx,256 96 mov dx,0x1f0 ;磁盘接口0x1f0:数据端口,准备读取256个字(注意是字,不是字节)也就是一个扇区的大小(512字节) 97 98 _Read_data: 99 in ax,dx 100 mov [bx],ax 101 add bx,2 102 loop _Read_data 103 104 pop dx 105 pop cx 106 pop bx 107 pop ax 108 109 ret 110 ;==================================================== 111 Set_Segment: ;重定位过程,dx:ax是32位的地址,现在需要把它弄成16位的 112 push dx 113 114 add ax,[cs:phy_base] ;一定要注意偏移地址是cs 115 adc dx,[cs:phy_base+2] ;是dx不是bx,不要搞错了, 116 ;因为本来start的汇编地址都是相对于应用程序开头的了 117 ;现在我们要做的就是把他们我们的加载地址,然后把它们搞成段地址 118 ;这就是为什么所有的用户程序段都要16字节对齐的原因,不然右移会出问题 119 shr ax,4 120 ror dx,4 121 and dx,0xf000 ;清掉低12位 122 or ax,dx 123 124 pop dx ;ax的内容就是16位的段地址 125 126 ret 127 ;==================================================== 128 Read_Other_Harddisk: 129 push ds 130 131 mov cx,ax ;统计还要读入的扇区数 132 @run: 133 xor di,di ;设置新的磁盘位置 134 inc si 135 136 mov ax,ds ;把段地址往前移动两个位置指向新的段 137 add ax,0x20 138 mov ds,ax 139 140 xor bx,bx ;还要注意设置新的偏移地址 141 call Read_HardDisk 142 loop @run 143 144 pop ds 145 ret 146 ;==================================================== 147 phy_base dd 0x10000 ;用户程序加载内存地址 148 149 times 510-($-$$) db 0 ;填充0,末尾填充0xaa55 150 dw 0xaa55
;=============================================================================== SECTION header vstart=0 ;定义用户程序头部段 program_length dd program_end ;程序总长度[0x00] ;用户程序入口点 code_entry dw start ;偏移地址[0x04] dd section.code.start ;段地址[0x06] realloc_tbl_len dw (header_end-realloc_begin)/4 ;段重定位表项个数[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=16 vstart=0 ;定义代码段(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,2 or al,0x80 out 0x70,al in al,0x71 ;读RTC当前时间(分) push ax mov al,4 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*160 + 36*2 ;从屏幕上的12行36列开始显示 mov [es:bx],ah mov [es:bx+2],al ;显示两位小时数字 mov al,':' mov [es:bx+4],al ;显示分隔符':' not byte [es:bx+5] ;反转显示属性 and byte [es:bx+5],0x07 pop ax call bcd_to_ascii mov [es:bx+6],ah mov [es:bx+8],al ;显示两位分钟数字 mov al,':' mov [es:bx+10],al ;显示分隔符':' not byte [es:bx+11] ;反转显示属性 and byte [es:bx+11],0x07 pop ax call bcd_to_ascii mov [es:bx+12],ah mov [es:bx+14],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 ;逻辑右移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,4 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+2],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*160 + 33*2],'@' ;屏幕第12行,35列 .idle: mov al,0xef ;关闭主片的IR2 out 0x20,al ;写回此寄存器 hlt ;使CPU进入低功耗状态,直到用中断唤醒 not byte [12*160 + 33*2+1] ;反转显示属性 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,80 div bl mul bl mov bx,ax jmp .set_cursor .put_0a: cmp cl,0x0a ;换行符? jnz .put_other ;不是,那就正常显示字符 add bx,80 jmp .roll_screen .put_other: ;正常显示字符 mov ax,0xb800 mov es,ax shl bx,1 mov [es:bx],cl ;以下将光标位置推进一个字符 shr bx,1 add bx,1 .roll_screen: cmp bx,2000 ;光标超出屏幕?滚屏 jl .set_cursor mov ax,0xb800 mov ds,ax mov es,ax cld mov si,0xa0 mov di,0x00 mov cx,1920 rep movsw mov bx,3840 ;清除屏幕最底一行 mov cx,80 .cls: mov word[es:bx],0x0720 add bx,2 loop .cls mov bx,1920 .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=16 vstart=0 init_msg db 'Starting...',0x0d,0x0a,0 inst_msg db 'Installing a new interrupt 70H...',0 done_msg db 'Done.',0x0d,0x0a,0 tips_msg db 'Clock is now working.',0 ;=============================================================================== SECTION stack align=16 vstart=0 ;这里栈默认大小256字节 resb 256 ss_pointer: ;当前sp是0,当push时,回绕到最顶上 ;=============================================================================== SECTION program_trail program_end:
2. 用新的周期性中断
1 ;===================================================== 2 ;时钟Demo 3 SECTION Program_Header align=16 vstart=0 4 Program_Length: dd Program_end ;程序总长度 [0x00] 5 Code_Entry: dw start ;入口点偏移量 [0x04] 6 dd section.code.start ;入口点段地址 [0x06] 7 ;等下从加载器跳过来,然后从入口点拿一个双字的内容,0x06是段地址,0x04是偏移地址 8 9 ralloc_section_nums: dw (segment_end-segment_start)/4 10 ;段重定位个数 [0x0a] 11 segment_start: 12 code_segment: dd section.code.start ;代码段开始 [0x0c] 其实这里不要也没关系,但是要改加载器 13 data_segment: dd section.data.start ;数据段开始 [0x10] 14 stack_segment: dd section.stack.start ;栈段的开始 [0x14] 15 16 segment_end: 17 ;===================================================== 18 SECTION code align=16 vstart=0 19 start: 20 cli ;不允许中断 21 mov ax,[stack_segment] ;设置栈段 22 mov ss,ax 23 mov sp,ss_poniter 24 25 mov ax,[data_segment] ;设置数据段 26 mov ds,ax 27 28 mov bx,inform1 29 call put_string 30 mov bx,inform2 31 call put_string 32 33 mov al,0x70 ;计算70号中断在cs的偏移位置 34 mov bl,4 35 mul bl 36 mov bx,ax ;放到bx上去保存 37 38 push es 39 mov ax,0x0000 40 mov es,ax 41 mov word [es:bx],print_clock ;设定偏移地址 42 mov [es:bx+2],cs ;设定段地址 43 pop es 44 45 call Set_RTC 46 sti ;开放中断 47 48 mov ax,0xb800 49 mov es,ax 50 mov byte[es:12*160 + 33*2],'!' 51 mov byte[es:12*160 + 46*2],'!' 52 53 @loop: 54 hlt ;使CPU进入低功耗状态,直到用中断唤醒 55 not byte[es:12*160 + 33*2+1] ;反转字符属性,使得!变来变去 56 not byte[es:12*160 + 46*2+1] 57 jmp @loop 58 ;===================================================== 59 Set_RTC: 60 mov ax,0x0b 61 or al,0x80 ;阻断NMI 62 out 0x70,al ;访问寄存器b 63 mov al,0x42 ;周期性更新,BCD模式,24小时模式 64 out 0x71,al 65 66 mov ax,0x0a ;设置分频点路时间 67 or al,0x80 68 out 0x70,al 69 in al,0x71 ;先读一下看al的内容 70 and al,0xf0 71 or al,0x0e ;一秒4次中断 72 out 0x71,al ;重新写入0x71端口 73 74 mov ax,0x0c 75 out 0x70,al 76 in al,0x71 ;读一下寄存器C使得标记消失 77 78 mov ax,0xfe ;只开放从片的IR0端口 79 out 0xa1,al ;从新写回从片 80 mov ax,0xfb 81 out 0x21,al 82 ret 83 ;===================================================== 84 print_clock: 85 push ax 86 push bx 87 push cx 88 push dx 89 push es 90 91 @wait: 92 mov ax,0x0a 93 or al,0x80 ;阻断NMI 94 out 0x70,al ;读寄存器a 95 in al,0x71 96 test al,0x80 ;看是否处于更新周期或即将进入更新,否则就等到可以进入为止 97 jnz @wait 98 99 xor al,al 100 or al,0x80 101 out 0x70,al 102 in al,0x71 ;阻断NMI,读取秒 103 push ax 104 105 mov ax,2 106 or al,0x80 107 out 0x70,al 108 in al,0x71 ;阻断NMI,读取分 109 push ax 110 111 mov ax,4 112 or al,0x80 113 out 0x70,al 114 in al,0x71 ;阻断NMI,读取时 115 push ax 116 117 mov al,0x0c 118 out 0x70,al 119 in al,0x71 ;读取一下寄存器C,不然下次就不产生中断了 120 121 mov ax,0xb800 122 mov es,ax 123 124 mov bx,12*160 + 36*2 ;从屏幕上的12行36列开始显示 125 mov cx,2 ;时,分 126 @1: 127 pop ax 128 call bcd_to_ascii 129 mov [es:bx],ah ;显示高位 130 add bx,2 131 mov [es:bx],al ;显示低位 132 add bx,2 133 134 mov al,':' ;显示: 135 mov [es:bx],al 136 inc bx 137 not byte[es:bx] ;反转属性 138 and byte[es:bx],0x07 139 inc bx 140 loop @1 141 pop ax ;显示秒 142 call bcd_to_ascii 143 mov [es:bx],ah ;显示高位 144 add bx,2 145 mov [es:bx],al ;显示低位 146 147 mov ax,0x20 148 out 0xa0,al ;发送EOI命令 149 out 0x20,al 150 151 pop es 152 pop dx 153 pop cx 154 pop bx 155 pop ax 156 157 iret 158 ;===================================================== 159 bcd_to_ascii: 160 mov ah,al 161 and al,0x0f 162 add al,0x30 163 164 shr ah,4 165 and ah,0x0f 166 add ah,0x30 167 ret 168 ;===================================================== 169 put_string: 170 mov cl,[bx] 171 or cl,cl 172 jz .exit 173 call put_char 174 inc bx 175 jmp put_string 176 .exit: 177 ret 178 179 put_char: 180 push ax 181 push bx 182 push cx 183 push dx 184 push ds 185 push es 186 187 mov dx,0x3d4 ;设置光标的索引端口 188 mov al,0x0e 189 out dx,al 190 mov dx,0x3d5 191 in al,dx 192 mov ah,al ;0x0e是高8位 193 mov dx,0x3d4 194 mov al,0x0f 195 out dx,al ;0x0f是低8位 196 mov dx,0x3d5 197 in al,dx 198 mov bx,ax 199 set_0d: 200 cmp cl,0x0d 201 jnz set_0a ;不是回车符,看是不是换行符 202 mov bl,80 203 div dl ;扔掉余数 204 mul dl ;直接得到本行的偏移地址 205 mov bx,ax 206 jmp set_cursor ;回车就直接设置光标了 207 set_0a: 208 cmp cl,0x0a 209 jnz put_other 210 add bx,80 ;如果是换行,直接把光标放到下一行就可以了 211 jmp roll_screen 212 put_other: 213 mov ax,0xb800 214 mov es,ax 215 shl bx,1 ;把bx的位置乘以2(ASCII+属性) 216 mov [es:bx],cl 217 218 shr bx,1 ;千万记得要把bx弄回去然后+1,因为光标要移动的! 219 add bx,1 220 jmp roll_screen 221 roll_screen: 222 cmp bx,2000 ;2000是屏幕的字符总数,如果超过2000,那就滚屏 223 jl set_cursor ;小于直接设置光标,大于等于滚屏 224 mov ax,0xb800 225 mov es,ax 226 mov ds,ax 227 cld 228 mov di,0x00 229 mov si,0xa0 230 mov cx,1920 231 rep movsw 232 233 mov cx,80 234 mov bx,3840 235 @2: 236 mov word [es:bx],0x0720 237 add bx,2 238 loop @2 239 mov bx,1920 ;记得清空最后一行后要把bx放到最后一行的前面哦! 240 set_cursor: 241 mov dx,0x3d4 ;设置光标的新高位 242 mov al,0x0e 243 out dx,al 244 mov al,bh 245 mov dx,0x3d5 246 out dx,al 247 248 mov dx,0x3d4 ;设置光标的新低位 249 mov al,0x0f 250 out dx,al 251 mov al,bl 252 mov dx,0x3d5 253 out dx,al 254 255 pop es 256 pop ds 257 pop dx 258 pop cx 259 pop bx 260 pop ax 261 ret 262 ;===================================================== 263 SECTION data align=16 vstart=0 264 inform1: db ' This is a clock demo--by Philip',0x0d,0x0a,0 265 inform2: db ' The clock is running',0 266 ;===================================================== 267 SECTION stack align=16 vstart=0 268 resb 256 ;声明256个字节的区域 269 ss_poniter: 270 ;===================================================== 271 SECTION Program_Trial 272 Program_end:
3. 从键盘中读取字符,显示到屏幕上
这本来是书上的例程,作为练手的我自己打了一遍,其实也挺简单的,据说我们隔壁的电子系的实验有一个就是做这个……
1 ;===================================================== 2 ;中断BIOS:从键盘中获得字符 3 SECTION Program_Header align=16 vstart=0 4 Program_Length: dd Program_end ;程序总长度 [0x00] 5 Code_Entry: dw start ;入口点偏移量 [0x04] 6 dd section.code.start ;入口点段地址 [0x06] 7 ;等下从加载器跳过来,然后从入口点拿一个双字的内容,0x06是段地址,0x04是偏移地址 8 9 ralloc_section_nums: dw (segment_end-segment_start)/4 10 ;段重定位个数 [0x0a] 11 segment_start: 12 code_segment: dd section.code.start ;代码段开始 [0x0c] 其实这里不要也没关系,但是要改加载器 13 data_segment: dd section.data.start ;数据段开始 [0x10] 14 stack_segment: dd section.stack.start ;栈段的开始 [0x14] 15 16 segment_end: 17 ;===================================================== 18 SECTION code align=16 vstart=0 19 start: 20 cli ;不允许中断 21 mov ax,[stack_segment] ;设置栈位置 22 mov ss,ax 23 mov ax,ss_pointer 24 mov sp,ax 25 mov ax,[data_segment] ;设置数据区位置 26 mov ds,ax 27 sti ;开放中断 28 29 mov cx,msg_end-message 30 mov bx,message 31 _putc: 32 mov ah,0x0e 33 mov al,[bx] 34 int 0x10 35 inc bx 36 loop _putc 37 38 _jmp: 39 mov ah,0x00 40 int 0x16 ;此时字符在al处 41 42 mov ah,0x0e 43 int 0x10 44 jmp near _jmp 45 46 ;===================================================== 47 SECTION data align=16 vstart=0 48 message: db 'Hello, guy!',0x0d,0x0a 49 db 'This simple procedure used to demonstrate ' 50 db 'the BIOS interrupt.',0x0d,0x0a 51 db 'Please press the keys on the keyboard ->' 52 msg_end: 53 ;===================================================== 54 SECTION stack align=16 vstart=0 55 resb 256 56 ss_pointer: 57 ;===================================================== 58 SECTION Program_Trial 59 Program_end: