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)。

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

    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中时间信息表如下:

 

  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:

 

 

 

 

 

 

 

 

 

 

posted @ 2016-03-25 01:32  PhiliAI  阅读(1709)  评论(0编辑  收藏  举报