第二天 开始执行我们的程序
我们先看一段代码。
; 文件名:boot_A.asm
;##########################################################################
org 07c00h ; Boot 状态, Bios 将把 Boot Sector 加载到 0:7C00 处并开始执行
[bits 16] ; 定义以下代码生成为16位代码。
;==================================================================
; 常量定义
;==================================================================
BaseOfStack equ 07c00h ; 引导程序开始地址,堆栈的顶部。
;--------------------------------------------------------------------------
LABEL_START:
mov ax, cs ; 取代码所在段,其他段均与代码段一致。因为在16位下,只能寻址64K范围。
mov ds, ax ; 数据段
mov es, ax ; 附加段
mov ss, ax ; 堆栈段
mov sp, BaseOfStack ; 堆栈基地址。一般情况,此时的堆栈只是临时的,到了加载模块时还要重新设置,
; 由其是进入32位代码模块后,才是最重要的。
mov [0523h], dl ; 保存启动盘号到内存0523H处,(A盘为0,硬盘C为80)
; 开机后,启动盘号由BIOS保存至寄存器dl中。
jmp $ ; 以死循环作为结束
;############################################################
; 主程序代码结束
;############################################################
;****************************************************************************
times 510-($-$$) db 0 ; 510减去本行开始到程序开始的长度,重复生成字节0。
; 填充剩下的空间,使生成的二进制代码恰好为512字节,因为引导代码只能限定在512个字节即一个扇区内。
; 如果以上代码超过510个字节,编译程序会发出错误警告。
; 需要预留2个字节,以便生成引导的标志。
dw 0xaa55 ; 结束标志,生成引导标志。
;****************************************************************************
上面的代码很简单,但有几个需要说明的地方:
org xxx 指令,是表示以下代码从以xxx为偏移地址开始编译,编译出来的代码将在xxx位置运行,这个指今在一个程序中只能使用一次。
[bits xx] 是定义以下代码生成xx位的指令,一般只有16和32两个选项。
equ 为常量定义指令,将指令后面的常量用前面的标号代替,程序中出现该标号的地方都会替换为后面的常量。
$ 是指本行代码开始处的偏移地址,不能作为常量来用。jmp $ 就是跳转到本行代码开始处,即无限循环。
$$ 是指本段代码开始处的偏移地址,也不能作为常量来用。没有定义段的情况下为本程序的开始处。($-$$)是本程序开始处(或段开始处)到本行代码开始处的长度(单位字节),这两个偏移量相减得出的是一个常量。
times x db 0 是重复定义指令,重复次数为x,x必须是一个常量。
dw 0xaa55 是一个数据定义行,只不过它没有标号,因为程序中不需要引用它。
这段代码仅完成保存启动盘的盘号后就进行无限的循环中。不管怎么样,这就是一个引导程序的雏形,我们可以在这基础上不断的完善,变成最终可用的引导程序。
首先用VPC生成一个软盘IMG映像文件,然后在DOS的命令提示符下键入:“nasm boot_A.asm –o boot_A.bin ”(不含引号),回车,编译出boot_A.bin文件,再用FloppyWriter程序将编译出来的boot_A.bin文件写入软盘映像文件的引导扇区内(就是软盘的第一个扇区)。
这时,你就可以运行VPC并加载软盘映像文件,采用软盘启动,我们的引导系统就会运行了。但是,因为我们没有任何的提示信息,所以你什么也看不到。是不是很没劲?那我们来加点料吧!
如果让屏幕上显示文本信息呢?这里有两种方法,一是用BIOS中断服务,另一个就是直接写显存的映射区。
第一种方法代码量小,但不是很灵活。第二种方法很灵活,但需要很长的代码来实现。所以,在引导程序中我们只用第一种方法,等进入内核时,我们就可以用第二种方法编程,来随心所欲的实现我们的目的。
先介绍一个我们将要用到的中断,即 10H 号中断。(注意!这里的10H是十六进制的,以H结尾或0x开头的数值都表示为十六进制)BIOS的10H号中断功能为显示服务,它有很多子功能,我们目前只用它的显示字符串功能。
参数:
AL 子功能号=13
AH 显示输出方式,取值0-3,其他值无效,
0:字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变
1:字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变
2:字符串中含显示字符和显示属性。显示后,光标位置不变
3:字符串中含显示字符和显示属性。显示后,光标位置改变
BL 属性(当AL=00H或01H时有效)
BH 显示屏的页码
CX 字符串的长度
DH、DL 坐标(行、列)
ES:BP 显示字符串的地址
返回值:无
具体的功能为在指定页码的屏幕上从的指定位置开始显示出指定长度的字符串。显示属性指定方式为统一指定或字符串中包含。
目前,一般的显卡支持文本显示8屏,就是说可以有8个屏幕的文本内容同时保存在显存中,需要时直接设置显示的页码,指定页码的屏幕就会显示出来。如Unix、Linux的控制台转换就是用的这种方式。
言归正传,当你把参数设定好后用int 10h命令就可以调用了。相关知识可以请参考汇编语言的学习资料。
增加了显示字符功能后,程序代码如下:(黑体字为新增代码!)
; 文件名:boot.asm
;##########################################################################
org 07c00h ; Boot 状态, Bios 将把 Boot Sector 加载到 0:7C00 处并开始执行
[bits 16] ; 定义以下代码生成为16位代码。
;==================================================================
; 常量定义
;==================================================================
BaseOfStack equ 07c00h ; 引导程序开始地址,堆栈的顶部。
;--------------------------------------------------------------------------
LABEL_START:
mov ax, cs ; 取代码所在段,其他段均与代码段一致。因为在16位下,只能寻址64K范围。
mov ds, ax ; 数据段
mov es, ax ; 附加段
mov ss, ax ; 堆栈段
mov sp, BaseOfStack ; 堆栈基地址。一般情况,此时的堆栈只是临时的,到了加载模块时还要重新设置,
; 由其是进入32位代码模块后,才是最重要的。
mov [0523h], dl ; 保存启动盘号到内存0523H处,(A盘为0,硬盘C为80)
; 开机后,启动盘号由BIOS保存至寄存器dl中。
;####################################################
; 清屏
;----------------------------------------------------
mov ax, 0600h ; 子功能号 AH = 6, 清屏操作 AL = 0h
mov bx, 0700h ; 空白区域的显示属性:黑底白字(好象只有BH有效,对BL值忽略)
mov cx, 0 ; 左上角: (x:0, y:0)
mov dx, 0184fh ; 右下角: (x:80, y:50)
int 10h ; 调用BIOS中断int 10h
;====================================================
;####################################################
; 显示字符串
;----------------------------------------------------
mov dh, 0 ; "Booting ",设定字符串的序号,
call DispStr ; 调用 显示字符串 子程序
;====================================================
jmp $ ; 以死循环作为结束
;############################################################
; 主程序代码结束
;############################################################
;----------------------------------------------------------------------------
;字符串定义
;----------------------------------------------------------------------------
; 为简化代码, 下面每个字符串的长度均为 MessageLength
MessageLength equ 9
BootMessage: db "Booting " ; 9字节, 不够则用空格补齐. 序号 0
;############################################################
; 用到的几个子程序
;############################################################
; 函数名: DispStr
; 参数: dh 为需显示字符串的序号
; 作用: 显示一个字符串, 函数开始时 dh 中应该是字符串序号(0-based)
;----------------------------------------------------------------------------
DispStr:
mov ax, MessageLength ; 要显示的信息长度。
mul dh ; 乘以字符串序号
add ax, BootMessage ; 加上信息基地址,得到字符串起始地址
mov bp, ax ; ┓
mov ax, ds ; ┣ ES:BP = 串地址
mov es, ax ; ┛
mov cx, MessageLength ; CX = 串长度
mov ax, 01301h ; AH = 13, AL = 01h
mov bx, 0007h ; 页号为0(BH = 0) 黑底白字(BL = 07h)
mov dl, 0
int 10h ; int 10h
ret
;==================================================================
;****************************************************************************
times 510-($-$$) db 0
; 填充剩下的空间,使生成的二进制代码恰好为512字节,因为引导代码只能限定在512个字节即一个扇区内。
; 如果以上代码超过510个字节,编译程序会发出错误警告。
; 需要预留2个字节,以便生成引导的标志。
dw 0xaa55 ; 结束标志,生成引导标志。
;****************************************************************************
上面代码中的清屏部分希望你自己能够学习理解,有不清楚的地方,查看有关资料书籍。
同时编译后写入软盘,运行。
如果你忘记如何编译,那我再说一遍!
编译:nasm boot_A.asm –o boot_A.bin
用FloppyWriter程序将编译出来的boot_A.bin文件写入软盘映像文件;
运行VirtualPC程序,在菜单中载入软盘映像文件。
屏幕上左上角是不是显示出了“Booting ”的字符串!
我们的第一步已经迈出去了,下面继续我们的远征之路!
为了能让Windows识别软盘,我们加上FAT12的数据头,这些数据是相对固定的。
增加后的代码如下:(黑体字为新增代码)
; 文件名:boot.asm
;##########################################################################
org 07c00h ; Boot 状态, Bios 将把 Boot Sector 加载到 0:7C00 处并开始执行
[bits 16] ; 定义以下代码生成为16位代码。
;==================================================================
; 常量定义
;==================================================================
BaseOfStack equ 07c00h ; 引导程序开始地址,堆栈的顶部。
;==================================================================
jmp short LABEL_START ; 跳转至程序开始处,一定要加short,强制为短跳转。
nop ; 这个 nop 不可少,为确保Fat12格式头数据对齐。
;==================================================================
;==================================================================
; 以下是FAT12格式的数据头。 *** 下面的标号后面没有冒号也是合法的!
BS_OEMName DB 'ForrestY' ; OEM String, 必须 8 个字节
BPB_BytsPerSec DW 512 ; 每扇区字节数
BPB_SecPerClus DB 1 ; 每簇多少扇区
BPB_RsvdSecCnt DW 1 ; Boot 记录占用多少扇区
BPB_NumFATs DB 2 ; 共有多少 FAT 表
BPB_RootEntCnt DW 224 ; 根目录文件数最大值
BPB_TotSec16 DW 2880 ; 逻辑扇区总数
BPB_Media DB 0xF0 ; 媒体描述符
BPB_FATSz16 DW 9 ; 每FAT扇区数
BPB_SecPerTrk DW 18 ; 每磁道扇区数
BPB_NumHeads DW 2 ; 磁头数(盘面数)
BPB_HiddSec DD 0 ; 隐藏扇区数
BPB_TotSec32 DD 0 ; 如果 wTotalSectorCount 是 0 由这个值记录扇区数
BS_DrvNum DB 0 ; 中断 13 的驱动器号,即启动驱动器号
BS_Reserved1 DB 0 ; 未使用
BS_BootSig DB 29h ; 扩展引导标记 (29h)
BS_VolID DD 0 ; 卷序列号
BS_VolLab DB 'Xos System '; 卷标, 必须 11 个字节
BS_FileSysType DB 'FAT12 ' ; 文件系统类型, 必须 8个字节
;==================================================================
; #############################
; # 主程序代码开始 #
; #############################
;--------------------------------------------------------------------------
LABEL_START:
mov ax, cs ; 取代码所在段,其他段均与代码段一致。因为在16位下,只能寻址64K范围。
mov ds, ax ; 数据段
mov es, ax ; 附加段
mov ss, ax ; 堆栈段
mov sp, BaseOfStack ; 堆栈基地址。一般情况,此时的堆栈只是临时的,到了加载模块时还要重新设置,
; 由其是进入32位代码模块后,才是最重要的。
mov [0523h], dl ; 保存启动盘号到内存0523H处,(A盘为0,硬盘C为80)
; 开机后,启动盘号由BIOS保存至寄存器dl中。
;####################################################
; 清屏
;----------------------------------------------------
mov ax, 0600h ; 子功能号 AH = 6, 清屏操作 AL = 0h
mov bx, 0700h ; 空白区域的显示属性:黑底白字(好象只有BH有效,对BL值忽略)
mov cx, 0 ; 左上角: (x:0, y:0)
mov dx, 0184fh ; 右下角: (x:80, y:50)
int 10h ; 调用BIOS中断int 10h
;====================================================
;####################################################
; 显示字符串
;----------------------------------------------------
mov dh, 0 ; "Booting ",设定字符串的序号,
call DispStr ; 调用 显示字符串 子程序
;====================================================
jmp $ ; 以死循环作为结束
;############################################################
; 主程序代码结束
;############################################################
;----------------------------------------------------------------------------
;字符串定义
;----------------------------------------------------------------------------
; 为简化代码, 下面每个字符串的长度均为 MessageLength
MessageLength equ 9
BootMessage: db "Booting " ; 9字节, 不够则用空格补齐. 序号 0
;############################################################
; 用到的几个子程序
;############################################################
; 函数名: DispStr
; 参数: dh 为需显示字符串的序号
; 作用: 显示一个字符串, 函数开始时 dh 中应该是字符串序号(0-based)
;----------------------------------------------------------------------------
DispStr:
mov ax, MessageLength ; 要显示的信息长度。
mul dh ; 乘以字符串序号
add ax, BootMessage ; 加上信息基地址,得到字符串起始地址
mov bp, ax ; ┓
mov ax, ds ; ┣ ES:BP = 串地址
mov es, ax ; ┛
mov cx, MessageLength ; CX = 串长度
mov ax, 01301h ; AH = 13, AL = 01h
mov bx, 0007h ; 页号为0(BH = 0) 黑底白字(BL = 07h)
mov dl, 0
int 10h ; int 10h
ret
;==================================================================
;****************************************************************************
times 510-($-$$) db 0
; 填充剩下的空间,使生成的二进制代码恰好为512字节,因为引导代码只能限定在512个字节即一个扇区内。
; 如果以上代码超过510个字节,编译程序会发出错误警告。
; 需要预留2个字节,以便生成引导的标志。
dw 0xaa55 ; 结束标志,生成引导标志。
;****************************************************************************
将上面的代码编译后写入软盘映像,再用VDM1.EXE程序加载(如果你的电脑有软驱,请先将软驱设备停用),这样可以虚拟一个软驱,我们以后就可以直接在Windows系统下对软盘映像文件进行操作,就像对真正的软盘操作一样方便。一旦引导程序完成后,就不再需要FloppyWriter工具了,编译出来的其他程序直接就可以复印到软盘上。
今天就到这里吧!有时间看看与此相关的资料,了解汇编与中断!明天学习引导程序中最关键的软盘数据的读取。