拼一个自己的操作系统 SnailOS 0.03的实现
拼一个自己的操作系统SnailOS0.03源代码-Linux文档类资源-CSDN下载
操作系统SnailOS学习拼一个自己的操作系统-Linux文档类资源-CSDN下载
SnailOS0.00-SnailOS0.00-其它文档类资源-CSDN下载
硬盘驱动程序
说起硬盘驱动程序,大家肯定会认为很难。笔者最开始看操作系统相关书籍的时候,也是首先有这种感觉的。完全没有头绪,尤其那些奇奇怪怪的同步和互斥原语插入到硬盘驱动中更是乱了方寸。相信大家初学操作系统的时候也会是这样。因此,这里首先就是要通过解释,为什么会是那样,然后直接来真的,给大家列出代码,就完成了。需要注意的是我们这里的硬盘类型是ide的。也就是说目前它只是适用于与ide硬盘,scsi的如何发送命令,目前还没有看到相关书籍,所以请大家自行研究。
那么我们就先说说向硬盘发送命令的原理好了。当我们的应用程序向硬盘发送读命令时,硬盘自己的控制器如果准备好了,它就会把一定量的数据准备到自己设备的缓冲区中,然后我们再通过输入(in)指令读入到内存中,也就是我们在内存中甚至的所谓的缓冲区中,当然这种读取的动作可能需要多次的操作。这种情况在我们如果是单任务的操作系统中,当然是理所当然的了。也就是说硬盘是一个对于处理器来说很慢的设备,如果是单任务的系统,无疑处理器等也就等了,而且处理器不等待也没有其他的事情去做,所以你会感觉没有任何问题。不一会的功夫,硬盘数据就读到了内存中了。
可是现在不同了,我们的系统是一个多任务的操作系统,不但多个进程会使用硬盘(向硬盘发送读命令),而且这些是同时发生的。这肯定涉及到了同步和互斥的问题。所以读硬盘时又是会用到互斥锁,有时又用到信号量就一点都不奇怪了。
所谓互斥的问题就是当一个线程使用硬盘读操作时,我们应该通过互斥锁独占该硬盘设备的使用,直到本次读写完毕,这个硬盘设备才能让给其他线程使用。这与前面对于信息区写入字符产生的竞争条件是多么的相似呀。也就是你用时,我必须等待,我用时你也得等。硬盘读操作程序,作为临界区一个时刻内,只能又一个应用程序进入。
那么同步又是什么呢?这里就要设计到在硬盘工作在中断打开的情况下,合适发出中断了。是这样的,当我们向某块硬盘发送读命令时,如果不存在互斥问题,硬盘就接到了我们的命令,这时硬盘是否就把数据传给我们了呢?还真不是,由于硬盘是慢速的设备,所以如果处理器在这个时候忙等待的话,就会浪费很多处理能力。因此,在这里,要通过信号量的down操作使向硬盘发送读命令的应用程序阻塞一会儿,直到硬盘数据准备好了为止。再通过信号量up操作唤醒阻塞在此处的用户进程。那么谁负责唤醒进程呢?对了,你可能已经猜到了,是硬盘中断处理程序,也就是当硬盘准备好数据后,会产生一次中断,正好通过此次中断我们有机会唤醒发送读取硬盘数据的应用程序。从而使应用程序继续执行。直到执行完毕。所以大家到此就明白了吧。不是说,硬盘驱动程序有多难,而是要在理解互斥和同步的基础上,才能编写硬盘驱动程序。这个是比较难于理解的地方。值得注意的是信号量在用在这个同步时,初始值是0。也就是说,最开始在应用程序调用读硬盘函数时,一定在发送了读命令后,会有一次信号量的down操作,这时由于初始值是0,所以程序会阻塞到这里,这也正是,我么们想要的,也就是等待一下硬盘数据的到来,一旦硬盘数据到来,中断就唤醒我们的应用程序。从而继续完成下面的工作。
硬盘的写入操作其实跟读出操作,也基本相同,只是阻塞应用程序的时机,不太一样,我们只要掌握发生阻塞的时机就好了。但唤醒的时机都是一样的,也就是当硬盘中断来临时,立即唤醒应用程序。
接下来我们再来讲讲个人计算机中ide硬盘配置,按照这种体系结构,pc机可以有两个ide通道,每个通道可以挂载主从两块硬盘,也就是最多4块硬盘。这两个通道分别占用可编程中断控制器的两个端口,分别是14和15(0xe和0xf),具体到中断向量上,因为是从0x20开始,所以是0x2e和0x2f两项。
这回我们在讲硬盘相关的输入输出指令的使用方法,我们就拿第一个通道的主硬盘的读取来举例说明。下表就是第一个通道上与硬盘对应的端口号,端口号对应者硬盘控制器的各个寄存器,因为硬盘是外设,所以寻址寄存器是通过in、out指令访问端口来完成的。
寄存器 |
端口 |
作用 |
data寄存器 |
0x1F0 |
已经读取或写入的数据,大小为两个字节(16位数据) 每次读取1个word,反复循环,直到读完所有数据 |
features寄存器 |
0x1F1 |
读取时的错误信息 写入时的额外参数 |
sector count寄存器 |
0x1F2 |
指定读取或写入的扇区数 |
LBA low寄存器 |
0x1F3 |
lba地址的低8位 |
LBA mid寄存器 |
0x1F4 |
lba地址的中8位 |
LBA high寄存器 |
0x1F5 |
lba地址的高8位 |
device寄存器 |
0x1F6 |
lba地址的前4位(占用device寄存器的低4位) 主盘值为0(占用device寄存器的第5位) 第6位值为1 LBA模式为1,CHS模式为0(占用device寄存器的第7位) 第8位值为1 |
command寄存器 |
0x1F7 |
读取,写入的命令,返回磁盘状态 1 读取扇区:0x20 写入扇区:0x30 磁盘识别:0xEC |
更具体的来说我们是通过lba28模式来访问硬盘数据的,lba(Logical Block Addressing)是逻辑块寻址的意思,对应的是CHS(Cylinder、Heads、Sector)柱面磁头扇区寻址方式。由于lba方式读取硬盘数据更接近于人们的思维模式,所以既然硬件直接就支持,所以很少有人再用CHS模式。所谓寻址其实是每次读取若干个扇区,所以要一个记录读取扇区数量的寄存器,它就是0x1f2,那么从那个扇区开始读取呢?所以还要有一个记录从哪个扇区开始读取的寄存器,那就是0x1f3—0x1f6,当然这4个寄存器的最后一个寄存器只是用到了低4位。高4位另有他用。这样三八二十四再加上这个四就是“lba28”中28的含义了。而接下来的0x1f7就是指令发送指令的寄存器,用out指令向该寄存器写入0x20,就是想向硬盘发送读取若干扇区的命令了。可以这样一来硬盘数据读到哪里了呢?答案是还在硬盘中,只不过是到了硬盘自己的缓冲区中,这时我们要通过处理器中的寄存器作为中转站再读取到内存中,才算完成的整个读取动作。
不过大家先不要着急,再这个硬盘往自己缓冲区中读取数据的过程中,是否读取完成,我们还要通过检测0x1f7来的得到,也就是通过in指令将0x1f7的数据读取到al中,然后同时比较al中的第3位和第7位(从零开始计算)分别是否为1和0,如果第3位和第7位是同时是上述数据,说明硬盘已经准备好数据且硬盘现在不忙。这看起来挺难,其实就是让al和0x88相与,然后比较al中是否是0x08就好了。
达到上一步的要求后我们就可以从数据端口,即0x1f0读取硬盘缓冲区的数据到内存缓冲区中了。也就是通过in指令读取到ax中,然后再写入到内存的某个地址处。
当然,因为这次是每次读取两个字节,所以内存地址每次需要增加2。而我们整个需要读取的数据长度,也就是读取的次数,需要整个读取的字节数除以2来计算。可是我们总共读取的了多少个字节呢?这里面有一个隐藏的计算就是要用每扇区字节数乘以前面提到的扇区数,就是我们要读取的总字节数。而告诉大家每扇区字节数是512字节了。
通过是上面的一些列骚操作,我们完成了一次硬盘读取操作,也就是说再lba28逻辑块选址模式下,你要告诉硬盘控制器,我要读取多少个扇区,我要从哪个扇区开始读取,我要读取(发送读取命令),询问硬盘控制器准备好数据了吗?您老先生现在还忙吗?在您不忙而且准备好数据的情况下,我开始两个字节两个字节的把数据读取到内存中。
最后在翻回头说说0x1f6的高4位分别代表什么。第4位(从0开始计算)为0代表主盘,为1代表从盘,第5为必须为1,第6为1代表lba28模式,为0代表CHS模式,所以必须为1,第7为必须为1。
我们用汇编指令来描述上面的过程大概就是下面这段代码的样子
; 用输入输入指令读取ide硬盘的汇编过程,这个只是简单的例子。
; void load_sector(int start_sector, int sector_cnt, int start_memory)
; mov dword [esp + 0 * 4], START_SECTOR
; mov dword [esp + 1 * 4], SECTOR_CNT
; mov dword [esp + 2 * 4], START_MEMORY
; call load_sector
load_sector:
; 用户函数调用保护寄存器
enter
pusha
; 汇编获得参数的方法
mov esi, [ebp + 2 * 4]
mov edi, [ebp + 3 * 4]
mov ebx, [ebp + 4 * 4]
; 为了简化操作,每次仅读取1个扇区。
.3:
mov al, 1
mov dx, 0x1f2 ; 这个就是读取扇区个数的寄存器
out dx, al
mov eax, esi ; 读取的起始扇区
mov dx, 0x1f3 ; 起始扇区寄存器低8位,下面依次类推。
out dx, al ; 写入低8位
shr eax, 8
mov dx, 0x1f4
out dx, al ; 写入低中8位
shr eax, 8
mov dx, 0x1f5
out dx, al ; 写入高中8位
shr eax, 8
and al, 0x0f
or al, 1110_0000b
mov dx, 0x1f6
; 写入高4位,同时置lba28模式,第4位(从0开始数)为0,表示主盘
out dx, al
mov al, 0x20 ; 读取命令
mov dx, 0x1f7 ; 命令寄存器
out dx, al ; 向硬盘控制器发送读指令
mov dx, 0x1f7
.1:
in al, dx
and al, 1000_1000b
cmp al, 0000_1000b ; 判别硬盘状态,第3位为1表示准备好,第7为0表示不忙
jne .1
mov ecx, 256 ; 一个扇区512字节,每次读取2字节,则读取次数为512/2=256
mov dx, 0x1f0
.2:
in ax, dx
mov [ebx], ax ; 读取到内存
inc ebx
inc ebx
loop .2
inc esi ; 读取的起始扇区数增1
dec edi ; 读取的总扇区数减少1
cmp edi, 0 ; 当读取次数为0时,读取完成
jne .3
popa
leave
ret
有了对这些基础知识的理解以后,我们马上把代码贴给大家,它一点都不复杂,而且通过笔者的一再简化,简直可以用简陋来形容,不过却极大地提高了可理解性。
【intr.c 节选】
(上面省略)
void idt_init(void) {
(中间省略)
create_gate(0x80, (unsigned int)_syscall, 1 * 8, 0x8e00 + 0x6000);
/*
在中断描述表中,添加第一个ide通道的中断处理函数。包括汇编形式和c形式的。
*/
i_table[0x2e] = (unsigned int)hd_handler;
create_gate(0x2e, (unsigned int)intr_hd, 1 * 8, 0x8e00);
(中间省略)
/*
硬盘中断处理程序的c部分,够简单吧。
*/
void hd_handler(void) {
if(expecting_intr) {
expecting_intr = false;
sema_up(&hd_done);
/*
要是硬盘继续接收中断,必须读一次命令寄存器(当然也可能由别的方法)。
*/
in((portbase + 7));
}
// printf_("&&HD&&");
}
(下面省略)
【system.asm 节选】
(上面省略)
; 这是把数据端口中数据批量读取到内存缓冲区的函数,
; 这里是用在读取硬盘数据缓冲区的数据。
global _insw
align 8
_insw: ; void insw(unsigned short port, void* buf, unsigned int word_cnt);
push ebp
mov ebp, esp
push ecx
push edx
push ebx
cld
mov dx, [ebp + 2 * 4]
mov ebx, [ebp + 3 * 4]
mov ecx, [ebp + 4 * 4]
.1:
in ax, dx
mov [ebx], ax
inc ebx
inc ebx
loop .1
pop ebx
pop edx
pop ecx
leave
ret
; 对应的,这是把数据端口中数据批量写入到内存缓冲区的函数,
; 这里是用在把数据写入硬盘数据缓冲区。
global _outsw
align 8
_outsw: ; void outsw(unsigned short port, void* buf, unsigned int word_cnt);
push ebp
mov ebp, esp
push ecx
push edx
push ebx
cld
mov dx, [ebp + 2 * 4]
mov ebx, [ebp + 3 * 4]
mov ecx, [ebp + 4 * 4]
.1:
mov ax, [ebx]
out dx, ax
inc ebx
inc ebx
loop .1
pop ebx
pop edx
pop ecx
leave
ret
; 硬盘中断处理的程序的汇编部分
global _intr_hd:
align 8
_intr_hd:
push 0x2e ; 向量号0x20 + 14
jmp _interrupt_entry
【hd.h】
// hd.h 创建者:至强 创建时间:2022年8月
#ifndef __IDE_H
#define __IDE_H
#include "global.h"
#include "thread.h"
#include "sync.h"
/*
分区结构,但我们的ide硬盘出动似乎根本没有用到,乃至于我们
的文件系统中都没有用,主要是我们只进行了fat32文件系统的读取。
*/
struct partition {
char name[32];
unsigned int start_lba, sec_cnt;
struct disk* disk_;
struct super_block* sb;
};
/*
ide硬盘结构,似乎仅有设备名称和设备号有用处。
*/
struct disk {
char name[32];
unsigned char dev_no;
struct partition prim_parts[4];
struct partition logic_parts[8];
};
extern void insw(unsigned short port, void* buf, unsigned int word_cnt);
extern void outsw(unsigned short port, void* buf, unsigned int word_cnt);
void hd_init(void);
void select_hd(struct disk* hd);
void select_lba(struct disk* hd, unsigned int lba, unsigned char sec_cnt);
void hd_cmd(struct disk* hd, unsigned char cmd);
void read_sectors(struct disk* hd, void* buf, unsigned char sec_cnt);
void write_sectors(struct disk* hd, void* buf, unsigned char sec_cnt);
bool fake_busy_wait(struct disk* hd);
void hd_read(struct disk* hd, unsigned int lba, void* buf,
unsigned int sec_cnt);
void hd_write(struct disk* hd, unsigned int lba, void* buf,
unsigned int sec_cnt);
void hd_read_sub(struct disk* hd, unsigned int lba, void* buf,
unsigned int sec_cnt);
void hd_write_sub(struct disk* hd, unsigned int lba, void* buf,
unsigned int sec_cnt);
#endif
【hd.c】
// hd.c 创建者:至强 创建时间:2022年8月
#include "hd.h"
#include "string.h"
#include "debug.h"
#include "intr.h"
/*
每个ide硬盘受控于硬盘控制器,控制器中含有一组非常有用的寄存器。
对于硬盘的读写和控制就是操作这组寄存器来完成的,而处理器操作
这些外设是通过读写端口完成。由于端口是连续的,所以都有一个基
端口。
*/
unsigned short portbase;
/*
硬盘锁,锁的目的当然是为了实现互斥,但这里只定义了一个锁似乎
会有问题,这个我不确定,还是等处了问题再改进吧,现在暂时这样,
原因是我们只有一块硬盘。
*/
struct lock hd_lock;
/*
应用程序在调用硬盘驱动程序发送命令时,会把该变量置为1,表示中断
一定会在将来的某个时刻发生。
*/
bool expecting_intr;
/*
这个信号号用户同步调用硬盘驱动程序的应用程序和硬盘中断处理程序,
具体地说,当用户程序利用驱动发送读写命令后,会在适当时刻阻塞自己,
从而等待硬盘完成数据读写操作,直到硬盘中断唤醒应用程序。
*/
struct semaphore hd_done;
/*
第一个ide通道上将有主从两块硬盘。
*/
struct disk hds[2];
/*
unsigned short portbase;
struct lock hd_lock;
bool expecting_intr;
struct semaphore hd_done;
struct partition {
char name[32];
unsigned int start_lba, sec_cnt;
struct disk* disk_;
struct super_block* sb;
};
struct disk {
char name[32];
unsigned char dev_no;
struct partition prim_parts[4];
struct partition logic_parts[8];
};
*/
/*
初始化硬盘结构,并初始化用到的基端口、锁、信号量。
*/
void hd_init(void) {
portbase = 0x1f0;
expecting_intr = false;
lock_init(&hd_lock);
sema_init(&hd_done, 0);
strcpy_(hds[0].name, "hd0");
strcpy_(hds[1].name, "hd1");
hds[0].dev_no = 0;
hds[1].dev_no = 1;
}
/*
通过端口0x1f6的第4位(从0开始计算)选择是主盘还是从盘。
*/
void select_hd(struct disk* hd) {
/*
0xa0是固定的,而0x40是选择lba28模式。
*/
unsigned char dev = 0xa0 + 0x40;
/*
当主盘的设备号是0时,说明选择的是主盘,而为1时选择的是
从盘。同时也要改变基端口的值。
*/
portbase = 0x1f0;
if(hd->dev_no == 1) {
portbase = 0x170;
dev += 0x10;
}
out((portbase + 6), dev);
}
/*
这个名称似乎不太准确,它的目标是把要操作的扇区数和lba模式的起始扇区,
写入到相关端口(寄存器)中。
*/
void select_lba(struct disk* hd, unsigned int lba, unsigned char sec_cnt) {
unsigned char dev = 0xa0 + 0x40;
portbase = 0x1f0;
if(hd->dev_no == 1) {
portbase = 0x170;
dev += 0x10;
}
out((portbase + 2), sec_cnt);
out((portbase + 3), lba);
out((portbase + 4), lba >> 8);
out((portbase + 5), lba >> 16);
/*
由于主从设备号包含在对应的起始lba扇区寄存器中,所以这里又把设备号重新
写了一遍。
*/
out((portbase + 6), dev + ((lba >> 24) & 0x0f));
}
/*
向命令寄存器发送命令,只要将命令数值写入该端口行了。
*/
void hd_cmd(struct disk* hd, unsigned char cmd) {
portbase = 0x1f0;
if(hd->dev_no == 1) {
portbase = 0x170;
}
expecting_intr = true;
out((portbase + 7), cmd);
}
/*
把批量数据从硬盘的数据端口(数据缓冲区)读取到内存的缓冲区中。
*/
void read_sectors(struct disk* hd, void* buf, unsigned char sec_cnt) {
unsigned int size;
portbase = 0x1f0;
if(hd->dev_no == 1) {
portbase = 0x170;
}
/*
通过扇区数计算需要读取的字节数,但单次最多256 * 512(等价于
256 << 9)个字节,这是由硬盘的扇区数寄存器决定的,这个扇区数
寄存器仅仅是个8位的,这样当其中数值为0时,lba28模式默认最多
读取256扇区。
*/
if(!sec_cnt) {
size = (256 << 9);
} else {
size = (sec_cnt << 9);
}
insw((portbase + 0), buf, (size >> 1));
}
/*
与上面对应这是写入的操作。
*/
void write_sectors(struct disk* hd, void* buf, unsigned char sec_cnt) {
unsigned int size;
portbase = 0x1f0;
if(hd->dev_no == 1) {
portbase = 0x170;
}
if(!sec_cnt) {
size = (256 << 9);
} else {
size = (sec_cnt << 9);
}
outsw((portbase + 0), buf, (size >> 1));
}
/*
通过循环读取硬盘的状态寄存器,判断硬盘数据是否准备好以及
硬盘是否处于忙的状态。函数名称说是忙等待,实际是通过睡眠
让出处理器给别的进程。
*/
bool fake_busy_wait(struct disk* hd) {
portbase = 0x1f0;
if(hd->dev_no == 1) {
portbase = 0x170;
}
unsigned int i = 30000;
for(; i >= 0; i -= 10) {
if(!(in((portbase + 7)) & 0x80)) {
return (in((portbase + 7)) & 0x08);
} else {
mtime_sleep1(10);
}
}
return false;
}
/*
读取硬盘扇区数据的函数。这两个函数笔者在使用过程中,会读取失败,
由于笔者对虚拟机和硬盘的了解有限,目前尚未确定原因。因此,笔者
改用了它们下面的两个函数,这样就没有问题了。
*/
void hd_read(struct disk* hd, unsigned int lba, void* buf,
unsigned int sec_cnt)
{
lock_acquire(&hd_lock);
select_hd(hd);
unsigned int per_sec, done_sec = 0;
while(done_sec < sec_cnt) {
if((done_sec + 256) <= sec_cnt) {
per_sec = 256;
} else {
per_sec = sec_cnt - done_sec;
}
select_lba(hd, lba + done_sec, per_sec);
hd_cmd(hd, 0x20);
sema_down(&hd_done);
if(!fake_busy_wait(hd)) {
panic("read hard disk failed!...");
}
read_sectors(hd, (void*)((unsigned int)buf + (done_sec << 9)), per_sec);
done_sec += per_sec;
}
lock_release(&hd_lock);
}
/*
向硬盘扇区写入数据的函数。
*/
void hd_write(struct disk* hd, unsigned int lba, void* buf,
unsigned int sec_cnt)
{
lock_acquire(&hd_lock);
select_hd(hd);
unsigned int per_sec, done_sec = 0;
while(done_sec < sec_cnt) {
if((done_sec + 256) <= sec_cnt) {
per_sec = 256;
} else {
per_sec = sec_cnt - done_sec;
}
select_lba(hd, lba + done_sec, per_sec);
hd_cmd(hd, 0x30);
if(!fake_busy_wait(hd)) {
panic("read hard disk failed!...");
}
write_sectors(hd, (void*)((unsigned int)buf + (done_sec << 9)), per_sec);
sema_down(&hd_done);
done_sec += per_sec;
}
lock_release(&hd_lock);
}
/*
读取硬盘扇区数据的函数。
*/
void hd_read_sub(struct disk* hd, unsigned int lba, void* buf,
unsigned int sec_cnt)
{
/*
互斥的进入该临界区。
*/
lock_acquire(&hd_lock);
select_hd(hd);
/*
每次只读取一个扇区,这样效率低了,但在虚拟机上未出现任何问题。
*/
unsigned int per_sec = 1, done_sec = 0;
while(done_sec < sec_cnt) {
select_lba(hd, lba++, per_sec);
hd_cmd(hd, 0x20);
/*
当发送完读命令后,我们就可以阻塞自己等待硬盘中断处理程序唤醒。
*/
sema_down(&hd_done);
if(!fake_busy_wait(hd)) {
panic("read hard disk failed!...");
}
/*
上面确定了硬盘状态后,把数据读取到内存缓冲区中。
*/
read_sectors(hd, (void*)((unsigned int)buf + (done_sec << 9)), per_sec);
done_sec += per_sec;
}
lock_release(&hd_lock);
}
/*
向硬盘扇区写入数据的函数。
*/
void hd_write_sub(struct disk* hd, unsigned int lba, void* buf,
unsigned int sec_cnt)
{
/*
互斥的进入该临界区。
*/
lock_acquire(&hd_lock);
select_hd(hd);
unsigned int per_sec = 1, done_sec = 0;
while(done_sec < sec_cnt) {
select_lba(hd, lba + done_sec, per_sec);
hd_cmd(hd, 0x30);
/*
当发送完写命令后,马上确定硬盘状态。
*/
if(!fake_busy_wait(hd)) {
panic("read hard disk failed!...");
}
write_sectors(hd, (void*)((unsigned int)buf + (done_sec << 9)), per_sec);
/*
写入一组数据,立即阻塞自己,等待硬盘中断处理程序唤醒。
*/
sema_down(&hd_done);
done_sec += per_sec;
}
lock_release(&hd_lock);
}
由于这一章是要结合文件系统才能在硬盘中读出数据的,所以屏幕上不会有任何变化了。当然我们就别贴图了。
文件系统和简单的文件读操作
说是文件系统,其实只是用上一章我们的硬盘驱动程序,把一张或几张图片从fat32文件系统上读到内存中,然后显示在桌面或窗口中。大家别忘了,蜗牛操作系统的内核文件是在fat32文件系统上的,所以我们也能把图片复制到虚拟硬盘上,当然也可以复制文本文件等等。
不过,在从fat32文件系统的硬盘上读取图片文件之前,我们要先构造一些能够显示图片的函数。可是图片文件的格式有好多种啊,我们选择哪一种呢?对了听说bmp格式的图像数据是非压缩的,我们就选它吧。否则的话,我们选择那些经过数据压缩的图片格式可就糗大了,因为还要先来研究一下解压算法,这个对笔者来说可以不小的挑战呐。
现在,我们要解决的第一个问题就是简单地了解一下bmp图像文件的数据结构。bmp文件大体由4部分组成,第一部分是文件头,第二部分是信息头,第三部分是调色板、第四部分是实际图片像素数据区。其实在我们的函数中,第一部分只是用到了最后一个数据项,它是像素数据区相对于文件开始的偏移量,这样我们只要知道文件的内存地址,然后加上这个偏移量就得到了数据区的地址。通过文件开始地址和文件头长度相加我们就得到了第二部分也就是bmp信息头的基地址,这部分我们用到的信息头的4个数据项目,分别是每像素占用的比特位数,图像的像素宽度、像素高度以及图像字节大小。bmp格式图像虽然说是非压缩的存储像素,但却在存储像素时,做了很多手脚,给实际编程带来了很多麻烦。
第一个问题就是,图像的像素宽度并不是实际存储的数据宽度,如果该像素宽度不是4的倍数,能够生成bmp文件的软件会自动将像素用0补齐为4的倍数。这样一来,我们就不得不把添加的多余像素,在显示之前去掉,否则图像将是倾斜的,无法正常显示。我们采取的方案是如果为4的倍数,则直接拷贝。而如果不是4的倍数,就计算出每行实际的字节宽度(公式参见代码),而有效的像素宽度是在信息头中给出的,所以只复制有效的像素,而将多余的剔除就可以了。
第二个问题是图像居然是从下到上倒着放置的,所以这时,还要将图像数据从最后,再复制一遍才能形成我们直接显示的像素数据。
具体的实现方法,还请大家开代码体会吧?
【x.h 节选】
(上面省略)
// 为了缩短画三角形函数的长度,我们定义了一个描述点的数据
// 数据结构。
struct point {
int x;
int y;
};
/*
bmp图像文件的文件头结构。
*/
struct bitmap_file_header {
/*
bmp图像文件的特殊标记。
*/
unsigned short bm;
/*
整个bmp文件的大小。
*/
int file_size;
/*
下面两个保留。
*/
unsigned short reserved1;
unsigned short reserved2;
/*
bmp图像文件开始位置到像素数据的字节偏移量。
*/
int bitmap_offset;
}__attribute__((packed));
/*
bmp图像文件的信息头结构。
*/
struct bmp_info_header {
/*
该结构的大小。
*/
int i_size;
/*
水平像素数。
*/
long width;
/*
垂直像素数。
*/
long height;
short planes;
/*
每像素所占用的比特位数。
*/
short bitcount;
int compression;
/*
图像像素数据的大小。
*/
int image_size;
long xpels;
long ypels;
int colour_used;
int colour_improtant;
}__attribute__((packed));
/*
我们杜撰的用于保存必要bmp信息的结构。
*/
struct bmp_buf_info {
int width, height, *bmp_buf;
};
(下面省略)
【x.c 节选】
(上面省略)
// 为配合实心三角形的算法,专门弄了一个从窗口缓冲区获取颜色的函数。
unsigned int get_colour(int* buf, unsigned int win_x_size, int x, int y) {
unsigned int colour = buf[x + y * win_x_size];
return colour;
}
/*
提取bmp文件中的像素信息,并存储到我们准备的一块内存中。
需要注意的是原始文件中多添加了字节,所以通过拷贝去掉多
余的字节。
*/
void save_bmp(char* bmp, struct bmp_buf_info* bbi) {
/*
得到bmp信息头结构,从而访问必要的bmp文件信息。
*/
struct bmp_info_header * p = (struct bmp_info_header *)
((int)bmp + sizeof(struct bitmap_file_header));
int width, height;
width = p->width;
height = p->height;
int bitcount = p->bitcount;
/*
bmp图像文件每行的像素数必须为4的整数倍,如果不是整数倍的,生成
bmp文件的软件将其会自动补齐,这里是计算每行的差额。
*/
int offset_bytes = 4 - (width * bitcount / 8) % 4;
/*
添加了多余像素,计算每行实际的字节宽度。
*/
int row_length = (width * bitcount + 31) / 32 * 4;
/*
有效像素每行的字节宽度。
*/
int per_count = row_length - offset_bytes;
unsigned int image_size = p->image_size;
int i, j;
/*
bmp图像文件的像素数据区的开始。
*/
char*q = (char*)((int)bmp + ((struct bitmap_file_header*)bmp)->bitmap_offset);
char*t = q;
char* new_q = (char*)get_kernel_pages(up(image_size, 4096) / 4096);;
char* picture_start = new_q;
/*
当每行像素数为4的整数倍时,我们直接赋值数据区就可以了。
*/
if(width % 4 == 0) {
memcpy_(new_q, q, image_size);
} else {
/*
否则赋值的数据区要剔除掉多余的无效像素。
*/
for(i = 0; i < height; i++) {
memcpy_(new_q, q, per_count);
new_q += per_count;
q += row_length;
}
}
new_q = picture_start;
int* buf_res = (int*)get_kernel_pages(up_pgs(width * height * sizeof(int)));
unsigned char* bmp_buf = new_q;
unsigned int r, g, b;
unsigned int colour;
/*
bmp图像文件的玩的花活还真不少,居然是从左到右,从下到上存放的。
因此我们再复制一遍,从而使文件正过来存放。在我们这里默认的认为
每像素所占的比特位数为24位也就是3个字节。当然这也是现在大多数
bmp文件的默认配置。能够直接读取数据区还有一个真实的原因是大部分
bmp文件是非压缩的,所以像素数据未经任何处理可以直接显示(画矩形)。
这真的是一个福音。
*/
for(j = height - 1; j >= 0; j--) {
for(i = 0; i < width; i++) {
b = *bmp_buf++;
g = *bmp_buf++;
r = *bmp_buf++;
colour = (r << 16) | (g << 8) | b;
buf_res[j * width + i] = colour;
}
}
mfree_page(&kernel_vir, (unsigned int)picture_start);
bbi->width = width;
bbi->height = height;
bbi->bmp_buf = buf_res;
}
/*
把bmp文件复制到任何窗口中从而能够正常显示图像。也就是说指定
了正确的窗口宽度就能够正常显示了。
*/
void put_bmp_buf(int* buf, int xsize, int x, int y, struct bmp_buf_info* bbi) {
int i, j;
for(j = 0; j < bbi->height; j++) {
for(i = 0; i < bbi->width; i++) {
buf[(y + j) * xsize + x + i] = bbi->bmp_buf[j * bbi->width + i];
}
}
}
由于我们现在还没有解析fat32的能力,所以为展示bmp操作函数,我们把bmp文件以二进制文件的形式包含在内核中,方法其实很简单,只要定义全局变量,且用incbin命令就能够通过nasm汇编器,轻松的在内核中加入该文件,不过图像文件的还是比较大的,所以内核会增加的很快。
【system.asm 节选】
(上面省略)
; 硬盘中断处理的程序的汇编部分
global _intr_hd:
align 8
_intr_hd:
push 0x2e ; 向量号0x20 + 14
jmp _interrupt_entry
; 在汇编文件中包含二进制文件,也就是说文件将编译到内核中。
align 8
global _zzz;
_zzz:
incbin "z.bmp"
【kernel.c 节选】
(上面省略)
extern unsigned int zzz;
void kernel_main(unsigned int magic, unsigned int addr) {
(中间省略)
// out(0x21, 0xfb);
// out(0xa1, 0xfe);
struct bmp_buf_info bbi;
save_bmp((char*)&zzz, &bbi);
unsigned int* v = (unsigned int*)(0xe0000000 + 1024 * 128 * 4);
put_bmp_buf(v, 1024, 20, 20, &bbi);
/*
将表盘在这里描画。从而成为桌面图层的一部分。
*/
(下面省略)
其实,我们对kernel.c的其他地方还做了一些小小的调整,不过由于对内核的本质没有影响,只是表面文章,这里还是请大家根据自己的实际修改吧。哦,对了。一定要把待显示的图片放在咱们系统的工作目录中呦。否则汇编器无法找到二进制文件。下面就是效果图(为了勾起大家学习的欲望,我可是找了个当红美女明星的靓照啊!)。