前置知识
显卡与显存
为了显示文字,通常需要两种硬件,一是显示器,二是显卡。
- 显卡的职责是为显示器提供内容,并控制显示器的显示模式和状态。
- 显示器的职责是将那些内容以视觉可见的方式呈现在屏幕上。
除此以外,还需要知道的是
- 显卡未必一定是独立的插卡。为了节省使用者的成本,有的显卡会直接做在主板上,这样的显卡也有个名字,叫集成显卡。
- 显卡控制显示器的最小单位是像素。
那么,什么是显存呢?
显存就是存放要在显示器上显示的内容的器件 ,因为它位于显卡上,故称显示存储器(Video RAM,VRAM),简称显存,要显示的内容都预先写入显存。
显卡的工作是周期性地从显存中提取这些比特,并把它们按顺序显示在屏幕上。如果是比特"0",则像素保持原来的状态不变,因为屏幕本来就是黑的;如果是比特"1",则点亮对应的像素。
因此,**为了给出要显示的字符,处理器需要访问显存,把字符的ASCII码写进去。**但是,显存是位于显卡上的,访问显存需要和显卡这个外围设备打交道。同时,多一道手续自然是不好的,这当中最重要的考量是速度和效率。
为此,计算机系统的设计者们,决定把显存映射到处理器可以直接访问的地址空间里,也就是内存空间里。
如图所示,8086可以访问1MB内存。其中,0x00000~9FFFF属于常规内存,由内存条提供;0xF0000~0xFFFFF由主板上的一个芯片提供,即ROM-BIOS。而0xB8000~0xBFFFF这段物理地址空间,是留给显卡的,由显卡来提供,用来显示文本。
屏幕是如何显示内容的
- 显卡在任何时候都认为你发送的是ASCII码。
- 屏幕上的每个字符对应着显存中连续2字节,前一个是字符的ASCII代码,后面是字符的显示属性,包括字符颜色(前景色)和底色(背景色)
如图所示,字符的显示属性(1字节)分为两部分
- 低4位定义的是前景色,高4位定义的是背景色。
- 色彩主要由R、G、B这3位决定
- K是闪烁位,为0时不闪烁,为1时闪烁
- I是亮度位,为0时正常亮度,为1时呈高亮。
操作显存显示内容
编写mbr.S代码
bash
;主引导程序MBR,由BIOS通过jmp 0:0x7c00跳转
;------------------------------------------
;vstart=0x7c00表示本程序在编译时,起始地址编译为0x7c00
SECTION MBR vstart=0x7c00
;使用通用寄存器中的值(0)初始化其余寄存器
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
;初始化栈指针
mov sp,0x7c00
;存入显存的段基址
mov ax,0xb800
mov gs,ax
;------------------------------------------
;------------------------------------------
;利用0x06号功能进行清屏
;INT 0x10 功能号:0x06 功能描述:上卷窗口清屏
;输入:
;AH 功能号:0x06
;AL=上卷的行数(如果为0,则表示全部)
;BH=上卷的行属性
;(CL,CH)=窗口左上角(X,Y)位置
;(DL,DH)=窗口右下角(X,Y)位置
;返回值
;无返回值
mov ax,0x0600
mov bx,0x0700
mov cx,0 ;左上角(0,0)
mov dx,0x184f ;右下角(80,25)
;0x18=24,0x4f=79
int 0x10 ;调用BIOS中断函数
;------------------------------------------
;将要显示的字符串写入到显存中
mov byte [gs:0x00],'1';在第一个字节的位置写入要显示的字符"1"
;在第二个字节的位置写入显示字符(也就是字符1)的属性,其中A表示绿色背景,4表示前景色为红色
mov byte [gs:0x01],0xA4
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4
jmp $
times 510-($-$$) db 0
db 0x55,0xaa
编译
bash
nasm -o mbr.bin mbr.S
将程序写入磁盘
bash
dd if=./osCode/mbr.bin of=./bochs/hd60M.img bs=512 count=1 conv=notrunc
运行
bash
./bin/bochs -f boot.disk
从硬盘中加载程序到内存
boot.inc
bash
;-----loader and kernel-----
LOADER_BASE_ADDR equ 0x900;loader在内存中位置
LOADER_START_SECTOR equ 0x2;loader在磁盘中的逻辑扇区地址,即LBA地址
mbr.S
bash
;主引导程序MBR,由BIOS通过jmp 0:0x7c00跳转
;------------------------------------------
%include "boot.inc"
;vstart=0x7c00表示本程序在编译时,起始地址编译为0x7c00
SECTION MBR vstart=0x7c00
;使用通用寄存器中的值(0)初始化其余寄存器
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
;初始化栈指针
mov sp,0x7c00
;存入显存的段基址
mov ax,0xb800
mov gs,ax
;------------------------------------------
;------------------------------------------
;利用0x06号功能进行清屏
;INT 0x10 功能号:0x06 功能描述:上卷窗口清屏
;输入:
;AH 功能号:0x06
;AL=上卷的行数(如果为0,则表示全部)
;BH=上卷的行属性
;(CL,CH)=窗口左上角(X,Y)位置
;(DL,DH)=窗口右下角(X,Y)位置
;返回值
;无返回值
mov ax,0x0600
mov bx,0x0700
mov cx,0 ;左上角(0,0)
mov dx,0x184f ;右下角(80,25)
;0x18=24,0x4f=79
int 0x10 ;调用BIOS中断函数
;------------------------------------------
;------------------------------------------
;将要显示的字符串写入到显存中
mov byte [gs:0x00],'1';在第一个字节的位置写入要显示的字符"1"
;在第二个字节的位置写入显示字符(也就是字符1)的属性,其中A表示绿色背景,4表示前景色为红色
mov byte [gs:0x01],0xA4
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4
mov eax,LOADER_START_SECTOR;起始扇区地址
mov bx,LOADER_BASE_ADDR;要写入的内存地址
mov cx,1;待读入的扇区数目
call rd_disk_m16;调用磁盘读取程序
jmp LOADER_BASE_ADDR;
;------------------------------------------
;函数功能:读取硬盘的n个扇区
;参数:
;eax:LBA扇区号
;bx:要写入的内存地址
;cx:待读入的扇区数目
rd_disk_m16:
;out命令,通过端口向外围设备发送数据
;其中目的操作数可以是8为立即数或者寄存器DX,源操作数必须是寄存器AL或者AX
;因此需要将之前ax中保存的值进行备份
mov esi,eax
mov di,cx
;正式读写硬盘
;第一步:设置要读取的扇区数目
mov dx,0x1f2
mov ax,cx
out dx,ax
mov eax,esi;恢复eax的值
;第二步:将LBA的地址存入0x1f3~0x1f6
;LBA的0~7位写入端口0x1f3
mov dx,0x1f3
out dx,al
;LBA的8~15位写入端口0x1f4
mov cl,8
shr eax,cl;ax左移8位
mov dx,0x1f4
out dx,al
;LBA的16~23位写入端口0x1f5
shr eax,cl
mov dx,0x1f5
out dx,al
;LBA的24~27位写入端口0x1f6
shr eax,cl;左移8位
and al,0x0f;与0000 1111相与,取后四位
or al,0xe0;设置7~4位为1110,表示lba模式
mov dx,0x1f6
out dx,al
;第三步,向0x1f7端口写入读命令,0x20
mov dx,0x1f7
mov al,0x20
out dx,al
;第四步,检测硬盘状态
.not_ready:
nop;空操作,什么也不做,相当于sleep,只是为了增加延迟
in al,dx;由于读取硬盘状态信息的端口仍旧是0x1f7,因此不需要再为dx指定端口号
;取出状态位的第3位和第7位
;其中第3位若为1表示硬盘控制器已经准备好数据传输,第7位为1表示硬盘忙
and al,0x88
;cmp为减法指令,此处是为了判断第7位是否为1,如果为1,则减法的结果应该为0
cmp al,0x08
;JNZ是条件跳转指令,它表示"Jump if Not Zero",
;也就是如果零标志位(ZF)不为0,则进行跳转。否则不进行跳转
jnz .not_ready;若未准备好,则继续等
;第五步,从0x1f0端口读取数据
mov ax,di;di是之前备份的要读取的扇区数
mov dx,256
;mul指令表示乘法操作,当只有一个操作数时,被乘数隐含在al或者ax中
mul dx;一个扇区512字节,每次读取两个字节,需要读取di*256次
mov cx,ax;cx在此时表示要循环读取的次数
mov dx,0x1f0
.go_on_read:
in ax,dx
mov [bx],ax;通过循环将输入写入bx寄存器所指向的内存,每次读入2个字节的数据
add bx,2
loop .go_on_read
ret
times 510-($-$$) db 0
db 0x55,0xaa
编译
其中**-I选项表示指定代码中包含的头文件(boot.inc)路径**
bash
nasm -I include/ -o mbr.bin mbr.S
将MBR程序写入磁盘的MBR扇区内
bash
dd if=./osCode/mbr.bin of=./bochs/hd60M.img bs=512 count=1 conv=notrunc
实现内核加载器
loader.S
bash
%include "boot.inc"
SECTION vstart=LOADER_BASE_ADDR
;------------------------------------------
;将要显示的字符串写入到显存中
mov byte [gs:0x00],'2';在第一个字节的位置写入要显示的字符"1"
;在第二个字节的位置写入显示字符(也就是字符1)的属性,其中A表示绿色背景,4表示前景色为红色
mov byte [gs:0x01],0xA4
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'L'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'O'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'A'
mov byte [gs:0x09],0xA4
mov byte [gs:0x0a],'D'
mov byte [gs:0x0b],0xA4
mov byte [gs:0x0c],'E'
mov byte [gs:0x0d],0xA4
mov byte [gs:0x0e],'R'
mov byte [gs:0x0f],0xA4
jmp $
编译
bash
nasm -I ./include/ -o loader.bin loader.S
将程序写入磁盘的2号扇区内
由于0号扇区内写入的是MBR程序,因此loader程序需要写入2号扇区内
bash
dd if=./osCode/loader.bin of=./bochs/hd60M.img bs=512 count=1 seek=2 conv=notrunc