【我所認知的BIOS】-->自己動手啟動計算機
LightSeed
2009-11-5
我当时在做《汇编语言》王爽(第一版)著的那本书的最后一题“课程设计二”的时候有关于启动OS的操作,想想这个题目对理解我们BIOS到OS的控制权的交付有比较形象的理解,故在这里也和大家探讨一下。
1、软盘的引导扇区格式及原理
1.1引导扇区的格式
(笔者:以下这段是参考
http://hi.baidu.com/rabbit5455/blog/item/7f482fdf2dccc310632798ec.html的文章。)
FAT12是DOS时代就开始使用的文件系统,目前软盘上也一直在用。
引导扇区是软盘的第0个扇区,在这个扇区中有一个很重要的数据结构叫BPB(BIOS Parameter Block),下面是引导扇区格式,其中BPB_开头的属于BPB,以BS_开头的只是BOOT Sector的一部分,不属于BPB。见图1
图1 软盘的启动扇区格式
紧接着引导扇区的是两个完全相同的FAT表,每个占用BPB_FATSz16(0x9)个扇区;第二个FAT之后就是根目录区的第一个扇区,根目录区后面是数据区。根目录区的位置:引导扇区占1个Sec,两个FAT表共18Sec,所以根目录区开始扇区号为19。根目录区中由若干个目录入口(Directory Entry)组成,最多有BPB_RootEntCnt个,所以根目录区大小不固定。每个Directory Entry占32字节。
1.2 提取其中的原理
其实看了那么多,我们不妨投机取巧一下,发现了一个现象了没有?原来这个扇区的第一条指令是jmp。那么我们就可以在这里做文章了。因为这里用了jmp指令,那么CPU取指令的时候必定是到这里来了的。所以我们只需要在这添上合理的(小于510 byte)的程序来完成引导等其他的任务即可。故我们只是那软盘的引导扇区来做我们自己的动作而已。
2、我们想要实现的功能
在明知道有C盘的情况下通过软盘来启动。如果可以顺便实现自己的功能。(比如说显示时钟呀,输入密码呀等等什么的。。。自己加了。)实现的功能程序在启动扇区中的逻辑图,在第一个扇区中的代码会把从第二扇区开始的代码copy到相应的内存中去。见图2
图2 启动扇区程序结构图
3、构思一下我们的程序架构
可以分成这么几个小块:
①安装启动代码到软盘的启动扇区模块
②重定位启动程序实体模块(因为自己写的程序,大多会超过512byte)
③检测键盘输入输出模块
④针对输入输出可以分支处理(进入各个功能的子程序)
⑤调用本来OS的启动程序。(调用原本的0:7C00H处的程序)
4、代码实例
外甥打灯笼,我们来看看实际的代码并加注。
;********************************************************************
;Note::这段代码是参考一位前辈的不过不记得是哪里了---------
; 如果您发现了是哪里的code还希望您提示我一下--------
; 在全部code基础之上,我做了简化,目的是为了结构明显易懂----
;Function: 大致實現了 重啟電腦,啟動電腦-----------------------------
; 查看系統時鐘,修改系統時鐘四個功能---------------------
;PS: 值得學習的地方是怎麼通過自製啟動扇區來引導現有的OS,并在引導
; OS之前可加入自己的function。
;
; By LightSeed-2009-10-27-----------------------
;********************************************************************
ASSUME CS:CODE
CODE SEGMENT
;**********************************************************************************
;安装程序模块:
;写入以下的程序到第一个和后面的扇区
;不需要确定程序的确切固定长度,因为int 13h只能按照512字节的倍数传递,所以即使比512多
;一个字节,也要传递512个。
;**********************************************************************************
ERROR00H DB 'successful completion!',0
begin:
MOV AX,CS
MOV ES,AX ;要写的内存基址
MOV DS,AX
LEA BX,REAL ;要写的内存偏移
MOV AH,3 ;功能号,写
MOV AL,((PROGRAMEEND-REAL)/512+1) ;写扇区的数目,浪费一个扇区以获得编写的方便。
MOV CH,0 ;磁道号
MOV CL,1 ;扇区号
MOV DL,0 ;驱动器号
MOV DH,0 ;磁头号(面)
INT 13H
;////////////////磁盘写容错处理////////////////////////
;容错的代码请加在这里
;/////////////////////////成功写入,显示提示符并返回DOS////////////////////
WriteFinish:
;////////////////清屏////////////////////
CALL CLEARSCREAN
;清屏后,显示您想要显示的信息
MOV SI,OFFSET ERROR00H
MOV DH,10 ;显示子函数入口参数---显示的横行位置
MOV DL,20 ;显示子函数入口参数---显示的纵行位置
PUSH CX
MOV CL,00001111B
CALL SHOW_STR
POP CX
;//////////////键盘等待输入按任意键返回DOS////////////////////
MOV AH,0
INT 16H
ok_WriteErrorManage:
;////////////////清屏////////////////////
CALL CLEARSCREAN
MOV AX,4C00H ;返回DOS
INT 21H
;**********************************************************************************
;程序实体:(在启动扇区中实际存在的程序)
;第一个扇区的数据
;第一个扇区只作为引导第二个扇区的引导程序作用是把第二个扇区读到0:800处,
;并置cs:ip到0:800,原因是在引导现有的操作系统的时候,需要用到0:7c00内存区
;其中int 9h 需要占用0040:17作为状态字
;必须把0:7c00内存空闲出来。
;由于硬盘引导记录只能被存放到0:7c00处才能被正确引导,因此,需要在引导前把本程序
;移出内存。
;程序转移
;设置系统用堆栈内存区,由于没有操作系统支持,因此必须自己设定堆栈范围
;栈顶设置在9FBFF:0处,9FC00H(BIOS)保留数据区前一个位置.
;负责把自己要做的功能程序copy到内存中去的模块
;**********************************************************************************
Real:
CLI ;避免堆栈设置时发生中断
MOV AX,3000H
MOV SS,AX
MOV SP,1024
STI
;/////////////内存基址设置,一次设置,以后不用更改,因为程序小于64K,
;/////////////也为后面提供统一的地址偏移提供了方便
MOV AX,0
MOV DS,AX
MOV ES,AX
;/////////////读取第二和以后的扇区到内存的0:800处
MOV BX,800H ;要读的内存偏移
MOV AH,2 ;功能号,读
MOV AL,((PROGRAMEEND-REALCODE)/512+1) ;读扇区的数目,浪费512个字节以获得编写的方便。
MOV CH,0 ;磁道号
MOV CL,2 ;扇区号
MOV DL,0 ;驱动器号
MOV DH,0 ;磁头号(面)
INT 13H
;/////////////置CS,IP
;/////////////以下程序还有一种方法用jmp
MOV AX,0
MOV BX,800H
PUSH AX
PUSH BX
retf ;retf 相当于
;pop ip
;pop cs
;cs ip 的值不能通过普通的mov指令修改
;/////////////凑足一个扇区,主要满足int 19 对'55AA'标志的要求
;/////////////当前指令减去标号指令地址来计算应当的填充数据.
DB 510-($- REAL) DUP( 'A');处于以后的空间用字符A来填充
DW 0AA55H
;**********************************************************************************
;前面是引导第二扇区的程序,以下程序才是课程中的实体代码
;**********************************************************************************
realCode:
JMP START
FIRST DB "ENTER 1 YOU WILL RESET COMPUTER.",0
SECEND DB " ENTER 2 YOU WILL START SYSTEM .",0
THIRD DB " ENTER 3 YOU WILL WATCH CLOCK .",0
FOURTH DB " ENTER 4 YOU WILL SET CLOCK . ",0
ENTERCLOCK DB "PLEASE ENTER THE TIME:",0
ENTERFORMAT DB "AND LIKE 080808080808 IT MEANS 2008/08/08 08:08:08"
RESETBUFFER DW 0,0
TOP DW 0 ;栈顶
DATABUFFER DB 16 DUP (0) ;如果这里不用0来填充,后面就会出大问题!
YES DB 'It is a leap year!',0
NO DB 'It is not a leap year!',0
DATA_ERROR DB 'Enter month Error! Please enter again.',0
TIME_ERROR DB 'Enter time Error! Please enter again.',0
FORMATOKFLAG DW 0
LEAPYEARFLAG DW 0
DECIMALISTDATABUFFER DB 16 DUP (0) ;大一倍,给自己留点余量
start:
;*****************以上初始化**************
;////////////////清屏////////////////////
CALL CLEARSCREAN
;///////////////显示主菜单//////////////
CALL DISPLAYMENU
MOV AH,0
INT 16H ;循环检测是否you健按下
;////////////////下两句是调试时用的////////////////////
CMP AL,31H
JE RESETPC
CMP AL,'2'
JE STARTSYSTEM
CMP AL,33H
JE WATCHCLOCK
CMP AL,34H
JE SETCLOCK
JMP SHORT START
ResetPC:
CALL RESET_PC ;可以实现重启的功能,jmp到f000:fff0处
JMP SHORT START
StartSystem:
CALL START_SYSTEM ;通过调用0:7C00来启动OS
JMP SHORT START
WatchClock:
CALL WATCH_CLOCK ;实时查看时钟,功能的话就随便您加了
JMP SHORT START
SetClock:
CALL SET_CLOCK ;设置时钟
JMP SHORT START
;**************************************************
;入口:ClearScrean
;功能:dos清屏
;
;入参:无
;回参:无
;**************************************************
CLEARSCREAN PROC NEAR
PUSH AX
PUSH BX
PUSH CX
PUSH ES
MOV AX,0B800H ;这的清屏方式用了直接访问B800H处的内存。
MOV ES,AX
MOV CX,2000
MOV BX,0
loopClearScrean:
MOV ES:[BX],BYTE PTR ' '
ADD BX,2
LOOP LOOPCLEARSCREAN
POP ES
POP CX
POP BX
POP AX
RET
CLEARSCREAN ENDP
;end
;**************************************************
;入口:DisplayMenu
;功能:显示主菜单
;
;入参:无
;回参:无
;**************************************************
DISPLAYMENU PROC NEAR
PUSH BX
PUSH CX
PUSH DX
PUSH SI
PUSH ES
PUSH DI
MOV CX,4 ;循环次数为4次
MOV SI,(FIRST-REALCODE+800H) ;要显示的内存地址偏移量,因为显示子函数的入口参数要求ds:si指向要显示的字符窜
MOV BX,10 ;要显示的横行位置基数
MOV DH,10 ;显示子函数入口参数---显示的横行位置
loopDisplayMenu:
PUSH CX ;由于要用到CX故,先保存现场
MOV DL,20 ;显示子函数入口参数---显示的纵行位置
;mov cx,di ;显示子函数入口参数---字符的颜色属性
PUSH CX
MOV CL,00001111B ;显示子函数入口参数---字符的颜色属性
CALL SHOW_STR
POP CX
INC BX ;显示的行往下移一行
MOV DH,BL ;把bl加上的1给dh
ADD SI,34 ;表行加1
POP CX
LOOP LOOPDISPLAYMENU
POP DI
POP ES
POP SI
POP DX
POP CX
POP BX
RET
DISPLAYMENU ENDP
;end
;***********************************************
;入口:show_str
;功能:字符串显示子程序
;入参:dh=行号(取值范围0~24);dl=列号(取值范围0~79);
; cl=颜色属性,dS:SI指向字符窜首地址。
;回参:无
;***********************************************
SHOW_STR PROC NEAR
PUSH AX
PUSH BX
PUSH CX
PUSH SI
PUSH DI
MOV AX,0
MOV BX,0
MOV DI,0
MOV AL,0A0H ;行增量16d
DEC DH ;行号在显存中下标从0开始,所以减1
MUL DH ;行起始地址,类似于段地址
MOV BX,AX ;把显存的行起始值,存入BX中
MOV AL,2 ;字符显示是两个字节的,故增量为2
MUL DL ;计算列地址,类似于偏移量
SUB AX,2 ;列号在显存中下标从0开始,又因为偶字节存放字符,所以减2
ADD BX,AX ;此时bx中存放的是行与列号的偏移地址,就是要显示的位置
MOV AX,0B800H
MOV ES,AX ;es中存放的是显存的第0页(共0--7页)的起始的段地址
;--------------以上为计算显存的偏移量,方法又是用的段+偏移的方法来实现
MOV DI,0
MOV AL,CL
MOV CH,0
mov_color_and_str:
MOV CL,DS:[SI] ;变址寻址
CMP CL,0 ;检查是否到字符串结束,因为标志为‘0’,是则结束
JE OK
MOV ES:[BX+DI],CL ;偶地址存放字符
MOV ES:[BX+DI+1],AL ;奇地址存放字符的颜色属性
INC SI
ADD DI,2
JMP SHORT MOV_COLOR_AND_STR
ok:
POP DI
POP SI
POP CX
POP BX
POP AX
RET ;显示字符串的子程序[定义结束]
SHOW_STR ENDP
;end
;**************************************************
;入口:reset_pc
;功能:使CS:IP指向 FFFF:0000 电脑重启
;
;入参:无
;回参:无
;*************************************************
RESET_PC PROC NEAR
PUSH AX
PUSH BX
PUSH DS
;mov ax,0ffffh
;mov bx,0
;push ax
;push bx
;retf ;retf 相当于
;pop ip
;pop cs
;cs ip 的值不能通过普通的mov指令修改
;注掉的部分是用retf指令来实现far jump两者都可以实现,笔者有调试过
LEA BX,RESETBUFFER
MOV AX,0H
MOV WORD PTR DS:[BX],AX
MOV AX,0FFFFH
MOV WORD PTR DS:[BX+2],AX
JMP DWORD PTR DS:[BX]
POP DS
POP BX
POP AX
RET
RESET_PC ENDP
;end
;**************************************************
;入口:watch_clock
;功能:从CMOS读出时间并显示
;
;入参:无
;回参:无
;*************************************************
WATCH_CLOCK PROC NEAR
;加入您的子程式,实时地看时钟信息
RET
WATCH_CLOCK ENDP
;end
;
;**************************************************
;入口:set_clock
;功能:设置时间,并写入CMOS中
;
;入参:无
;回参:无
;*************************************************
SET_CLOCK PROC NEAR
;加入您的子程序,去设置时钟
RET
SET_CLOCK ENDP
;end
START_SYSTEM PROC NEAR
;读取C盘的第一扇区到内存的0:7c00处
MOV BX,7C00H ;读到内存中的地址。
MOV DL,80H
MOV AH,2 ;读磁盘操作
MOV DH,0 ;0盘面
MOV CH,0 ;0磁道
MOV CL,1 ;1扇区
MOV AL,1 ;只读一个扇区
INT 13H
;置CS,IP
MOV AX,0
MOV BX,7C00H
PUSH AX
PUSH BX
retf ;retf 相当于
;pop ip
;pop cs
;cs ip 的值不能通过普通的mov指令修改
RET
START_SYSTEM ENDP
;end
;***********************************************
;入口:DIVDW 除法子程序
;功能:进行不会溢出的DWORD/WORD除法。
;入参:dx=被除数高16位;ax=被除数低16位;
; cx=16位除数,适用被除数DWORD,除数WORD。
;回参:dx=商的高16位;ax=商的低16位;cx=余数
;***********************************************
DIVDW PROC NEAR ;子程序定义开始
PUSH BX
PUSH AX ;低位AX进栈
MOV AX,DX ;高16位给AX,方便把高16位除以除数
MOV DX,0 ;把DX清零方便把高16位的除法的余数存入
DIV CX
MOV BX,AX ;把高位除出的结果存在BX中
POP AX ;低位出栈
DIV CX ;自动把DX和AX当作DWORD的被除数来做除法
MOV CX,DX ;把余数存到CX中
MOV DX,BX ;
POP BX
RET ;子程序定义结束
DIVDW ENDP
;end
ProgrameEnd:
CODE ENDS
END BEGIN