学习笔记
《x86汇编语言:从实模式到保护模式》
https://www.jianshu.com/p/d481cb547e9f
基于RTC更新周期结束中断实现的动态时钟
- 计算机启动后,
RTC芯片
的中断号默认是0x70
; -
RTC
发出一个中断后,通过设置8259芯片
内部的IMR寄存器
,使得8259芯片
允许RTC
的中断; - 动态时钟,每秒更新一次;
- 间隔
:
,随秒数更新时,变换颜色属性; - 符号
@
,随处理器从 停机状态 被外部中断 唤醒 时,变换颜色属性;
测试运行
配书源程序参考
- 用户程序代码
c09_1.asm
- 加载程序代码
c08_mbr.asm
源码使用方法参考
用户程序代码 c09_1.asm
:完整源码(增加注释)
;======================================================================
;用户程序开始
;======================================================================
;代码清单9-1
;文件名:code_9-1.asm
;文件说明:用户程序
;代码功能:显示动态时钟
;创建日期:10:07 2018/5/26
;======================================================================
;头部段
;======================================================================
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
;-------------------------------------------------------------------------------
;0x70号中断程序
;-------------------------------------------------------------------------------
new_int_0x70: ;新的0x70中断
;在屏幕上显示 时分秒
push ax
push bx
push cx
push dx
push es
;读RTC寄存器A,根据UIP位的状态来决定是等待更新周期结束还是继续往下执行
.w0:
mov al,0x0a ;阻断NMI RTC寄存器A 第7位UIP位
or al,0x80
out 0x70,al
in al,0x71
test al,0x80
jnz .w0
;更新周期结束中断
xor al,al ;al = 0
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 ;RTC寄存器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 byte [es:bx+4],':'
not byte [es:bx+5]
pop ax
call bcd_to_ascii
mov [es:bx+6],ah
mov [es:bx+8],al ;显示两位分钟数字
mov byte [es:bx+10],':'
not byte [es:bx+11]
pop ax
call bcd_to_ascii
mov [es:bx+12],ah
mov [es:bx+14],al ;显示两位秒钟数字
mov al,0x20 ;中断结束命令EOI(End Of Interrupt)
out 0xa0,al ;向8259芯片从片(Slave)发送EOI
out 0x20,al ;向8259芯片主片(Master)发送EOI
pop es
pop dx
pop cx
pop bx
pop ax
iret
;-------------------------------------------------------------------------------
;子程序: bcd_to_ascii
;参数: AL = BCD码
;返回: AH 十位数的ASCII码
; AL 个位数的ASCII码
;-------------------------------------------------------------------------------
bcd_to_ascii: ;新0x70中断中调用的子程序
;将BCD码转换成ASCII
mov ah,al
and al,0x0f
add al,0x30 ;个位数的ASCII码
shr ah,4
and ah,0x0f
add ah,0x30 ;十位数的ASCII码
ret
;-------------------------------------------------------------------------------
;用户程序入口
;-------------------------------------------------------------------------------
start: ;用户程序入口
;设置寄存器
mov ax,[stack_segment]
mov ss,ax
mov sp,ss_pointer
mov ax,[data_segment]
mov ds,ax
;显示信息,调用子程序 put_string
mov bx,init_msg ;显示初始信息
call put_string
mov bx,inst_msg ;显示安装信息
call put_string
;计算0x70号中断在中断向量表(IVT)中的入口地址
mov al,0x70
mov bl,4
mul bl
mov bx,ax
;将0x70号中断的入口地址改写为 cs:new_int_0x70
cli
push es
mov ax,0x0000
mov es,ax
mov word [es:bx],new_int_0x70 ;偏移地址
mov word [es:bx+2],cs ;段地址
pop es
;0x70 [索引端口],用来指定CMOS RAM内的单元
;0x71 [数据端口],用来读写CMOS RAM相应单元里的内容
;现在要访问的就是位于CMOS RAM中的RTC(REAL TIME CLOCK)
mov al,0x0b ;RTC寄存器B
or al,0x80 ;端口0x70的最高位(bit 7)是控制NMI的开关,
out 0x70,al ; 0表示允许NMI中断到达处理器、1表示阻断所有NMI信号
mov al,0x12 ;设置“更新周期结束中断”
out 0x71,al
mov al,0x0c ;RTC寄存器C
out 0x70,al
in al,0x71 ;读一下RTC寄存器C,使之可以产生新的中断信号
in al,0xa1 ;通过端口0xa1访问8259芯片从片上的IMR寄存器
and al,0xfe ;0xfe = 1111 1110B 清除第0位(此位通过从片引脚IRO连接着RTC)
out 0xa1,al ;回写寄存器
sti
;显示信息,调用子程序 put_string
mov bx,done_msg
call put_string
mov bx,tips_msg
call put_string
;显示标志 @ 符号
mov cx,0xb800
mov ds,cx
mov byte [12*160+32*2],'@'
;创建循环 停机状态响应外部中断恢复执行
.idle:
hlt ;使CPU进入低功耗状态,直到用外部中断唤醒
not byte [12*160+32*2+1] ;反转显示属性
jmp .idle
;-------------------------------------------------------------------------------
;子程序: put_string
;功能: 显示字符串,字符串以0结尾
;-------------------------------------------------------------------------------
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
resb 256
ss_pointer:
;======================================================================
;尾部段
;======================================================================
SECTION program_trail
program_end:
;======================================================================
;用户程序结束
;======================================================================
名词概念
NMI (non maskable interrupt):非屏蔽中断;
中断代理,中断控制器,8259芯片,Intel处理器允许256个中断,中断号范围是0~255,8259负责提供其中的15个,该中断控制器芯片有自己的端口号,可以像访问其他外部设备一样用in和out指令来改变它的状态,包括引脚的中断号,正因如此,又叫做可编程中断控制器(programming interrupt controller PIC);
IMR(interrupt mask register):中断屏蔽寄存器,8位寄存器,位于8259芯片内部,位0对应着中断输入引脚IR0,置为零时表示允许中断;
8259芯片,主片端口号是0x20和0x21,从片端口号是0xa0和0xa1;
CMOS RAM的访问,需要通过两个端口来进行, 0x70或者0x74是索引端口,用来指定CMOS RAM内的单元;0x71或者0x75是数据端口,用来读写相应单元里的内容;
8259芯片
、CMOS RAM
与处理器CPU
的关系
8259 芯片,是一个中断代理,
任务是对设备发出的中断进行仲裁,以决定让设备中的哪一个优先向处理器提出服务请求;
CMOS RAM,内含RTC(Real Time Clock)实时时钟电路,
是保存计算机基本启动信息(如日期、时间、启动设置等)的芯片;
代码说明
读写 CMOS RAM
- 端口0x70的最高位(bit 7)是控制NMI的开关
mov al,0x0b ;RTC寄存器B
or al,0x80 ;端口0x70的最高位(bit 7)是控制NMI的开关,
out 0x70,al ; 0表示允许NMI中断到达处理器、1表示阻断所有NMI信号
- 设置“更新周期结束中断
mov al,0x12 ;设置“更新周期结束中断” 0001 0010
out 0x71,al
根据 0001 0010 每一位设置寄存器B
设置“更新周期结束中断”
允许更新周期照常发生
禁止周期性中断
禁止闹钟功能
允许更新周期结束中断
使用24小时制
日期和时间使用BCD编码
- 读一下RTC寄存器C,使之可以产生新的中断信号
mov al,0x0c ;RTC寄存器C
out 0x70,al
in al,0x71 ;读一下RTC寄存器C,使之可以产生新的中断信号
第一行 al后面放上要访问的单元号;
第二行 将单元号通过数据端口0x70送入CMOS RAM;
第三行 从端口0x71读出访问的单元号的数据;
必须使用al来送入数据或者拿出数据;
必须使用0x70以及0x71两个端口来读写CMOS RAM;
==============================================
xor al,al ;al = 0
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
忽略最后的 push指令
都是同样格式的通过 al 利用端口0x70以及0x71进行CMOS RAM读操作
访问与设置 8259芯片
- 将8259芯片与RTC连接起来
in al,0xa1 ;通过端口0xa1访问8259芯片从片上的IMR寄存器
and al,0xfe ;0xfe = 1111 1110B 清除第0位(此位通过从片引脚IRO连接着RTC)
out 0xa1,al ;回写IMR寄存器
IMR(interrupt mask register):
中断屏蔽寄存器,8位寄存器,位于8259芯片内部,位0对应着中断输入引脚IR0,置为零时表示允许中断;
- 中断结束命令EOI(End Of Interrupt)
mov al,0x20 ;中断结束命令EOI(End Of Interrupt)
out 0xa0,al ;向8259芯片从片(Slave)发送EOI
out 0x20,al ;向8259芯片主片(Master)发送EOI
第一行,是将 中断结束命令 0x20 送入al,这个中断结束命令值恰好就是0x20;
第二行,通过端口0xa0发送EOI到从片;
第三行,通过端口0x20发送EOI到主片;
第一行的EOI(0x20)与第三行的端口0x20,不是一个东西,只是恰好数值相等, 不可混淆。
Bochs 调试: 在中断向量表IVT中,查看新的0x70号中断程序的入口地址
- 1、准备好虚拟硬盘,即完成加载程序写到扇区号0、用户程序写到扇区号100;
以下为bochs的调试命令,在提示符后面直接输入,每一行输入后要按下回车键:
-----------------
s
b 0x7c00
c
s
-----------------
最后一条命令s执行后,加载程序全部执行后,处理器跳转到用户程序开始执行
- 2、查看内存,查找通过加载程序回写的用户程序各个SECTION 段的段基址
-----------------
xp/6 0x1000:0x000c
-----------------
如图所示,可以查看在用户程序的代码段段基址是0x1002
- 3、在查找好代码段段基址的前提下,可以通过
.lst
文件查看每条指令的汇编地址,确定指令具体所在内存地址
.lst 文件由编译过程自动生成,与.asm文件一一对应:
============================== code_9-1.lst =============================================
164 ;将0x70号中断的入口地址改写为 cs:new_int_0x70
165 000000AC FA cli
166
167 000000AD 06 push es
168 000000AE B80000 mov ax,0x0000
169 000000B1 8EC0 mov es,ax
170 000000B3 26C707[0000] mov word [es:bx],new_int_0x70 ;偏移地址
171 000000B8 268C4F02 mov word [es:bx+2],cs ;段地址
172 000000BC 07 pop es
173
========================================================================================
可以观察到,第165行的 cli 指令的汇编地址是 0x00AC,这条指令在内存地址就是 0x1002:0x00AC
我们知道,当用户程序执行完第171行的mov指令后,就完成了新的中断程序入口地址的改写
现在,跳转到第172行,即相当于执行完第171行,内存地址0x1002:0x00BC
-----------------
b 0x1002:0x00bc
c
-----------------
- 4、改写好了入口地址,去内存看一看是不是真的改写了
中断向量表 位于 内存物理地址 0x00000 开始到 0x003ff 结束,段基址0x0000
0x70号中断程序原先的入口地址就是放在偏移量 0x70 * 4 = 0x1c0 处
即 段地址:偏移地址= 0x0000:0x1c0 物理地址 0x1c0 处
-----------------
xp 0x1c0
-----------------
xp 命令显示一个双字
双字
高字|低字
高字 = 段地址 = 0x1002 符合第2步里读出来的数据;
低字 = 偏移地址 = 0x0000 符合新的标号new_int_0x70相对于段CODE的偏汇编地址0x0000
新的入口地址 段地址:偏移地址 = 0x1002:0x0000
即发生0x70号中断时,根据中断向量表的新入口,跳转到 CODE段标号new_int_0x70开始执行。
各种魔数
CPU充电会读取位于硬盘主引导扇区(扇区号0)处的加载程序,将其放到内存0x0000:0x7c00处,并且跳转到该处,开始执行指令;
加载程序执行过程中,会将位于硬盘扇区号100处开始的用户程序,加载到内存物理地址0x10000开始的空闲部分,之后跳转到用户程序标号start处,从此用户程序开始执行;
中断向量表,中断向量表 位于 内存物理地址 0x00000 开始到 0x003ff 结束;N号中断的中断入口地址是4N,0x70号中断程序的入口地址就是0x1c0(注意这是十六进制的0x70乘以四);
加载程序,在跳转到用户程序之前,曾经将用户程序中各个SECTION 标记的段回写成可以访问真实内存的段基址,比如用户程序的CODE段(代码段)的段基址就是0x1002;
.lst文件可以提供每一条汇编指令相对段(vstart=0)的汇编地址,标号new_int_0x70位于段CODE内,汇编地址是0x0000,因此真实物理地址是0x1002:0x0000;