到目前为止,说了一部分有关显存的内容,这对于一般的输出来说已经足够了,下面咱们可以尝试写显存啦。我们将之前MBR改造一下,保留滚屏的操作,只修改有关输出的部分。即把通过bios的输出改为通过显存,你会发现,其实反而更容易,请见代码
1 ;主引导程序
2 ;
3 ;LOADER_BASE_ADDR equ 0xA000
4 ;LOADER_START_SECTOR equ 0x2
5 ;------------------------------------------------------------
6 SECTION MBR vstart=0x7c00
7 mov ax,cs
8 mov ds,ax
9 mov es,ax
10 mov ss,ax
11 mov fs,ax
12 mov sp,0x7c00
13 mov ax,0xb800
14 mov gs,ax
15
16 ; 清屏
17 ;利用0x06号功能,上卷全部行,则可清屏。
18 ; -----------------------------------------------------------
19 ;INT 0x10 功能号:0x06 功能描述:上卷窗口
20 ;------------------------------------------------------
21 ;输入:
22 ;AH 功能号= 0x06
23 ;AL = 上卷的行数(如果为0,表示全部)
24 ;BH = 上卷行属性
25 ;(CL,CH) = 窗口左上角的(X,Y)位置
26 ;(DL,DH) = 窗口右下角的(X,Y)位置
27 ;无返回值:
28 mov ax, 0600h
29 mov bx, 0700h
30 mov cx, 0 ; 左上角: (0, 0)
31 mov dx, 184fh ; 右下角: (80,25),
32 ; VGA文本模式中,一行只能容纳80个字符,共25行。
33 ; 下标从0开始,所以0x18=24,0x4f=79
34 int 10h ; int 10h
35
36 ; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
37 mov byte [gs:0x00],'1'
38 mov byte [gs:0x01],0xA4 ; A表示绿色背景闪烁,4表示前景色为红色
39
40 mov byte [gs:0x02],' '
41 mov byte [gs:0x03],0xA4
42
43 mov byte [gs:0x04],'M'
44 mov byte [gs:0x05],0xA4
45
46 mov byte [gs:0x06],'B'
47 mov byte [gs:0x07],0xA4
48
49 mov byte [gs:0x08],'R'
50 mov byte [gs:0x09],0xA4
51
52 jmp $ ; 通过死循环使程序悬停在此
53
54 times 510-($-$$) db 0
55 db 0x55,0xaa
前36行除第13~14行以外,和上一版本的MBR一样,忘记的话也不用翻回去看了,直接看注释简单了解下就好,剧透一下,以后连滚屏我们都要直接通过显卡来搞定啦。
前面说过了,显存文本模式中,其内存地址是0xb8000,忘记的话可以得往前翻翻“表3-15”显存地址分布。时刻要清楚,我们目前是在实模式下编程,实模式下内存分段访问策略是“段基址*16+段内偏移地址”。注意,要考虑到最终地址的段基址要乘以16,所以咱们选择的段基址必须是除以16以后的值。目标地址是0xb8000,按照以上策略,有多种“段基址+段内偏移地址”的组合可以拼凑出此地址。最直观的段基址为0xb800,即0xb8000除以16,也就是右移4位,偏移地址为0。
所以第13行和第14行往gs寄存器中存入段基址。这里和大家说明一下,显存段基址放在哪个寄存器中都是没关系的,对于访问的是数据来说,如果不用ds做段基址寄存器,就要在寻址中“显式地”指明要用哪个段寄存器的值做为段基址。这个“显式地”的段寄存器叫做段跨越前缀,有的书中叫段超越前缀,个人觉得意义不明确。何为超越?由于有“跨段访问”的说法,所以咱们这里统一为段跨越前缀。“段跨越”相对好理解,如cpu的访存策略是“段地址+段内偏移地址”。堆栈段的寄存器是SS,代码段寄存器CS,这两者不存在默不默认之说,因为它们都不能改变。不过对于数据段来说却有些不同,默认的寄存器是DS,但其是可以改变的。一般访问数据时只要给出偏移地址就可以了,这是因为已经存在了默认的段寄存器DS,所以访存中给出的偏移地址便是相对于DS的偏移量,也就是说访问的地址属于以DS为起始的段(是指一般意义上的分段机制,不考虑实模式或保护模式)。但若不想用这个段了,或者访问的地址不属于这个段,想“跨越”这个默认段而用新的段基址,“跨过”DS的限制,这就是“跨越”的理解。而“前缀”的是意思是,在编译后的机器码中,指定的这个新的段寄存器会出现在IA32指令格式中的“前缀”字段,可以参见表“3-1 IA32指令格式”。基于以上两点,为代替默认段基址寄存器而改用的新的段基址寄存器,称为段跨越前缀。
我们在第37~50行执行的mov操作都是往显存中写字符。拿37行和38行举例,第37行的“mov byte [gs:0x00],'1'”,是往以gs为数据段基址,以0为偏移地址的内存中写入字符1的ASCII码。按之前我们讲过的,写入1时,要写入1的ASCII码0x31。这是最直接的做法。但编译器诞生的意义就是为了给大家带来方便,尽管我们可以把37行的代码改为 mov byte [gs:0x00], 0x31,但这样毕竟还要自己查ASCII码表。编译器对于出现在代码中的字符,它会自动将其改为相应的ASCII码,免去了人工查表的过程。即使把表整个背下来了,本质上也是在脑子中经过了一次查表。所以,对于字符的输出,直接写出相应字符就行了,稍微有点人性化的编译器都会自动完成字符到编码的转换。
这里还有一个关键字byte,用于指定操作数所占的空间。同类的关键字还有word、dword等。这些关键字是指明了操作数的数据宽度(字节数),同c语言中的变量类型一个道理,都是指明数据所需要的存储空间。“mov byte [gs:0x00],'1' ”表示的意思是:把字符1的ASCII码写入以gs:0x00为起始,大小为1字节的内存中。word、dword分别表示2字节和4字节,意义同理。如果源操作数或目的操作数已经明确了数据宽度,在指令中就不必“显示地”指明操作数所占的空间大小了。如mov ax, 0x10,目的操作数ax是16位的,所以不用“显示地”在ax前或0x10前加个关键字word。在我们的代码“mov byte [gs:0x00],'1' ”中,由于这里的’1’对应的ASCII码是0x31,这是个立即数,对于立即数是无法判断它的存储空间的。它是占1字节还是2字节?将来会不会超过255?它现在是0x31还是0x0031?这谁知道呢。可cpu需要知道这个0x31要用多少字节来存储,因为它不确定这个数将来会不会超过255。要是用一字节来存储0x31,万一哪天往此处存个大于255的数,这一字节是万万不能胜任的。
我们之前说过了,一个字符是用2个字节来表示。低字节是字符的ASCII码,这里的偏移地址是还是0x00,gs在程序开头被赋值为0xb800,故最终地址是0xb8000,即显存的第0个字节。这表示字符1会在屏幕的左上角。
字符的高字节是属性,所以我们在第38行用“mov byte [gs:0x01],0xA4”为字符添加颜色。这里的偏移地址已经变成了0x01,是该字符’1’的高位,写入的属性值是0xA4,这表示K位为1,结合表3-16可知,其为红色跳动字符,绿色背景。
第39行~50行是分别在显存中创建字符‘M’,‘B’,‘R’及其属性,拼接字符串“MBR”,原理同上。
第52行还是个死循环,程序会卡在这里不动。其余代码同之前一样,是为了凑足512字节并写入魔数0xaa55。
代码不多,分析到此为止,事不宜迟,马上编译。
nasm -o mbr.bin mbr.S回车
下面将生成的mbr.bin写入我们的虚拟硬盘,还是用dd命令:
dd if=./mbr.bin
of=/your_path/bochs/hd60M.img
bs=512 count=1 conv=notrunc 回车
好了,按照前面介绍的方法启动bochs,执行c命令,将会在屏幕的左上角出现绿色背景、红色跳动的字符。效果如图
以上内容摘自《操作系统真象还原》,请大家支持正版。