这是x86 bootloader的第四篇,实模式的最后一篇,后面就要开启A20线-分页内存等进入32bit保护模式。前阵子看到protues带有8086 CPU,琢磨着等这边bootloader结束了,尝试一下在protues上仿真。
8086 CPU在进入实模式前,中断表跟8051长的有点神似,从0x0000开始每4B为一个中断向量,4B空间肯定不够处理中断事件,于是,这4B空间被安排为中断处理函数的在内存中的地址,其中低2B为函数偏移,高2B为函数段地址;而传统8051从0x0000开始每2B为一个JMP跳转,差不多长的如下:
ORG 0000H
LJMP MAIN
ORG 003BH
LJMP PCA_ISR
当8051中断发生时,如PCA中断,mcu会去003BH处取指令,然后跳转到PCA_ISR处执行;8086为了显示他的高贵,没这么处理:当遇到中断,从中断代理芯片处取出中断号,然后把中断号*4,得到中断入口点的地址,从中取出偏移和段地址装入IP:CS,于是CPU到中断处理函数中执行。这显然是一个自动装载的过程,不像8051一样用LJMP。
关于中断代理芯片,我到有很多想说。2个月前看<深入Linux设备驱动程序内核机制>(不可否认书在良莠不齐的国内图书市场上是本像样的书),对5.2节 PIC与软件中断号一节中的部分内容不太理解:“2)将外设的中断引脚编号映射到处理器可见的软件中断号irq”及"软件中断号irq,它是发生设备中断时处理器从PIC中读到的中断号码,在操作系统建立的中断处理框架内,会使用这个irq号来标识一个外设的中断并调用对应的中断处理例程"。如果PIC指的是8259,那么,应该可以这么理解以上两句话:1)首先,8259的中断号是是可配置的,可以指定主片的中断好从0x08开始,他的每个引脚IR0-IR7对应的中断号分别为0x08-0x0E。因此,“2)将外设的中断引脚编号映射到处理器可见的软件中断号irq”中的软件中断号应该就是从8259输出的经过自定义配置的中断号;2)而"...使用这个irq号来标识一个外设的中断并调用对应的中断处理例程" 这句中的irq好,应指前面的自定义配置的中断号,用这个8259的输出到中断向量表中取ISR地址。
下面来调试一些中断处理过程。调试的代码还是用原作者提供的bootloader,相对容易查看。
先来观察一下中断向量表长成什么样:
上电复位后,执行第一条指令前,中断向量表还是空的:
BIOS初始化结束,准备执行Bootloader时,中断向量表已经被简单的安装完毕,简单到什么程度呢?多数中断向量表项中的段地址:偏移的值为0xf000ff53,都说这个指向一个ISR过程,查看这个地址的内容是一个中断返回:
图中显示0x00000 为0xf000ff53 0xf000为高2B的段地址,0xff53为低2B的偏移。反汇编0xfff53的结果为iref
程序初始化RTC中断处理程序后,中断号0x70H(地址0x1c0处):
此时已经指向了RTC的中断处理。
最后,来调试一下中断处理过程,为了调试RTC中断处理函数,必须打开RTC周期更新中断功能,要不然只能进入RTC中断1次,开始时我就遇到这种情况,各种想不通。
调试ISR,只要在中断入口下断点即可。不过调试ISR不是重点,重点是查看发生ISR时的堆栈变化:
27 new_int_0x70:
28 00000000 50 push ax
29 00000001 53 push bx
30 00000002 51 push cx
31 00000003 52 push dx
32 00000004 06 push es
这是作者在中断入口压入堆栈的寄存器,并且在触发中断时会压入flag/cs/ip,但作者没有明确的说这些寄存器的入栈顺序因此只能调试观察了:
中断发生时ss:sp指向0x10300,堆栈是向下生长,而且中断发生时一共入栈了8个字,因此从0x10300-0x1030f是寄存器的内容。地址越高的最先入栈。
可见,入栈次序是0x0246:flag-0x1002:cs-0x00ed:ip[查看lst文件0x00ed处正好是hlt指令]
169 000000E2 B900B8 mov cx,0xb800
170 000000E5 8ED9 mov ds,cx
171 000000E7 C606C20740 mov byte [12*160 + 33*2],'@' ;屏幕第12行,35列
172
173 .idle:
174 000000EC F4 hlt ;使CPU进入低功耗状态,直到用中断唤醒
175 000000ED F616C307 not byte [12*160 + 33*2+1] ;反转显示属性
176 000000F1 E9F8FF jmp .idle
以此,可以得出结论,中断发生时8086会依次往堆栈中压入FLAG-CS-IP!
OVER
补上作者的代码:
;代码清单9-1
;文件名:c09_1.asm
;文件说明:用户程序
;创建日期:2011-4-16 22:03
;===============================================================================
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
push dx
push es
.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] ;反转显示属性
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] ;反转显示属性
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,复位未决的中断状态
in al,0xa1 ;读8259从片的IMR寄存器
and al,0xfe ;清除bit 0(此位连接RTC)
out 0xa1,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:
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
resb 256
ss_pointer:
;===============================================================================
SECTION program_trail
program_end:
section ProgHead align=16 vstart=0
MagicNum db 'M',0x2e,'A',0x2e,'G',0x2e,'I',0x2e,'C',0x2e
times 6 db 0x00
ProgSize dd ProgEnd
CodeEntry dw start
CodeSeg dd section.CodeSeg.start
ProgSegNums dw (ProgSegEntryTabEnd-ProgSegEntryTab)/4
ProgSegEntryTab:
CodeSegEntry dd section.CodeSeg.start
DataSegEntry dd section.DataSeg.start
StackSegEntry dd section.StackSeg.start
ProgSegEntryTabEnd:
Arg1Off equ 0x04 ;第一个参数相对bp偏移
Arg2Off equ 0x06 ;第二个参数相对bp偏移
Arg3Off equ 0x08 ;第三个参数相对bp偏移
Arg4Off equ 0x0A ;第四个参数相对bp偏移
;中断号
;rtc
RTCInt equ 0x70
section CodeSeg align=16 vstart=0x0000
Intx70RTC:
push ax
push bx
push cx
push dx
push es
;下面这段也是照抄的,I am a lazy man
.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,24*160 + 60*2 ;从屏幕上的12行36列开始显示
mov [es:bx],ah
mov [es:bx+2],al ;显示两位小时数字
mov al,':'
mov [es:bx+4],al ;显示分隔符':'
not byte [es:bx+5] ;反转显示属性
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] ;反转显示属性
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
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
MountIntVec:
push bp
mov bp,sp
;保存es
mov ax,es
push ax
xor ax,ax
;es指向0x0000
mov es,ax
;获得中断号
mov bx,[bp+Arg1Off]
;中断号左移2位(*4),获得在中断向量表中的地址
shl bx,0x02
;Int70RTC
;低2B存放中断处理函数的偏移
mov ax,[bp+Arg2Off];Intx70RTC
mov word [es:bx],ax
mov ax,cs ;目前cs段地址是0x0000
;高2B存放中断处理函数的段地址
mov [es:bx+0x02],ax
;恢复es
pop ax
mov es,ax
mov sp,bp
pop bp
ret
InitRTC:
;以下这段完全抄书的,设置寄存器太单调
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,复位未决的中断状态
in al,0xa1 ;读8259从片的IMR寄存器
and al,0xfe ;清除bit 0(此位连接RTC)
out 0xa1,al ;写回此寄存器
ret
;执行到这,要设置数据段堆栈段等信息,
;加载器在加载时,设置ds/es指向LoadPhyBase
;既然这样,加载器跳转过来后ds:[n]能正确访问
;自己定义的用户头
start:
;现在section.CodeSeg.start,section.DataSeg.start中的内容是段基地址
;开始时,我先加载了ds,再用[ds:StackSegEntry],出错了
;因为ds改变后不能在用
;mov ax,[StackSegEntry]
;mov ss,ax
;语句加载ss了 因为ds指向其他段,不再是LoadPhyBase,因此应该在ds改变前先加载ss段
;设置堆栈段
mov ax,[StackSegEntry]
mov ss,ax
mov sp,stack_end
;设置数据段,附加段
mov ax,[DataSegEntry]
mov ds,ax
mov es,ax
mov bx,Logo
call put_string
mov bx,MountMsg
call put_string
cli
;中断服务历程的偏移
push Intx70RTC
;安装rtc向量
push word RTCInt
call MountIntVec
add sp,0x04
call InitRTC
sti
mov bx,MountEndMsg
call put_string
hlt
jmp $
times 0x100 db 0xAA
section DataSeg align=16 vstart=0x0000
Logo db ' congratulations! ',0x0d,0x0a
db ' YZ Loader complete loading program ',0x0d,0x0a
db ' Author: Hanyj ',0x0d,0x0a
db ' 2014-12-16 ',0x0d,0x0a
db 0
MountMsg db ' Mount interrupt... ',0x0d,0x0a
db 0
MountEndMsg db ' Mount interrupt vector complete! ',0x0d,0x0a
db 0
section StackSeg align=16 vstart=0x0000
resb 256
;汇编中的标号都表示偏移位置,偏移堆栈基址256,堆栈向下生长,即堆栈保留了256字节。
;好吧,我承认stack_end这段我完全抄书的
stack_end:
section tail align=16
ProgEnd: