图形还是文本
在显示HelloWorld前我们需要先确定显示的模式(方式),目前就分为图形显示模式和文本显示模式吧。本质上来讲这两种方式是相同的,都是以像素点(点阵)来显示的,但是从实现方式来讲是不同的,图形模式可显示的内容更丰富(只要你能做出来),但是实现更复杂;文本模式可实现内容不多,但是实现简单,更容易上手。按我的想法来说肯定选简单的,毕竟图形模式涉及的知识太多了,从显卡驱动到图像处理,其工作量都可以作为另一个项目来做了,而且文本模式对我们完全够用。所以我们删繁就简,选文本模式吧。
80*25文本模式简介
在这个模式下,一个屏幕最多可以显示80(宽)*25(高)个字符,每个字符占用两个字节,第一个字节存储该字符的属性(颜色、背景色、是否闪烁等),第二个字节存储该字符的ASCII码,所有字符数据存储在以物理地址0xb8000起始地址的内存中。看到这里你有没有明白应该怎么操作屏幕了,整个屏幕不就是一个起始地址为0xb8000,元素长度为2个字节,元素个数为80*25的一位数组嘛(如果暂时不理解也可以理解为一个类似A[25][80]的二维数组,但是要明白所有的高维数组都可以化为一位数组处理哦),这段内存我们以后称之为显存。那让屏幕显示一个字符不就是向显存中写入一个元素嘛。到这里显示相关的知识就够用了,我们可以开始输出HelloWorld了。
向天再借500根头发
以下内容虽然不难,但是可以算是开启了秃顶(变强)之路,不过不用怕,正如标题所讲,最多也就掉500根。
写代码要三思而后行,不能上来就啪啪啪一顿乱敲,结果写着写着写不下去了,于是乎Ctrl+A、delete、码生重来。咱们写这个输出代码时也要想好,不能直接把"Hello World“这个字符串写进显存就完事了。仔细想一想,这个显示字符的功能不仅仅是现在用吧,内核其他部分也要向屏幕输出字符,用户程序也要向屏幕输出字符,我们总不能在输出字符时就把这段代码再写一遍吧,这么一想,不如我们写一个函数,这个函数接收并显示一个字符串,当用的时候就调用这个函数,问题不就解决了嘛。但是,如果我们想输出一个字符怎么办,这个函数就不能用了鸭,那再写一个输出字符的函数???大可不必,字符串是由字符组成的鸭,我们编写一个输出字符的函数,然后在输出字符串时反复调用这个字符函数不就可以了吗,由这个思路出发,我们可以设计出如下的代码:
//graphic.c
#include "types.h"
#define WIDTH 80
#define HEIGHT 25
static u16 * vm=(u16 *)0xb8000;
static u8 cursor_x=0;
static u8 cursor_y=0;
void putchar(u8 c){
u8 color=(0<<4)|(15&0x0f);
switch(c){
case '\n':
if((++cursor_y)==HEIGHT){
cursor_y=HEIGHT-1;
cursor_x=0;
}else{
cursor_x=0;
}
break;
default:
*(vm+cursor_y*WIDTH+cursor_x)=(color<<8)|c;
if((++cursor_x)==WIDTH){
cursor_x=0;
if((++cursor_y)==HEIGHT){
cursor_y=HEIGHT-1;
}
}
}
}
int puts(char * str){
for(int i=0;*(str+i)!=0;i++){
putchar(*(str+i));
}
return 0;
}
我们把这个源文件存储为graphic.c,其中的putchar函数为最底层的输出函数,它是今后所有向屏幕输出信息函数实现的基础(比如puts),而这个源文件是内核其他模块输出信息的基础,所以我们把graphic.c称之为显示模块(或者文本模式下的图形驱动),至此我们已经对计算机硬件的显示部分(可以说是显卡相关部分吧)作了一个抽象。对这里的抽象的概念一定要有自己的理解,因为今后的工作就是在重复这个过程:学习硬件接口-->将硬件抽象为几个函数接口(比如将显卡抽象为putchar和puts)-->将这几个函数接口归整为一个新的模块并加入内核。通过不断重复上述过程将硬件一一抽象为模块并加入内核,这样我们的内核就会支持越来越多的硬件,也就拥有了越来越多的功能,这样内核就得以不断的成长直至成熟。这个就是我个人编写内核的思路,虽然今后涉及的内容有很多,但是只要运用这个思路,就可以对其逐一击破,直至掌握一个完整的内核结构。
开始显示HelloWorld
哔哔了这么多,咱们终于可以进入正题了,这里想必也不必多说了,思路已经十分明显了,在boot.S的死循环之前调用puts输出“Hello World"就可以呗。这样写当然可以,但是我们的内核不止显示字符串这一个功能鸭,今后还有很多功能,还需要在内核开始调用很多函数,总不能都在boot.S这个汇编程序中调用吧(如果不明白AT&T汇编语言和C语言之间的互相调用,可以直接搜索相关内容,或者查看我写的附录),毕竟写汇编程序真的很麻烦,还是尽量把工作都放在C代码中吧,所以首先由boot.S调用一个内核的C主函数bootmain,然后由bootmain调用puts函数进行输出,今后的其他函数也由bootmain函数进行调用,这样就尽可能的避免了编写汇编代码。修改后的代码如下:
#boot.S
#Multiboot头,可以通过grub kernel指令加载并通过boot启动
.align 4
.text
multiboot_header:
#define magic 0x1badb002
#define mboot_mem_info 1<<1
#define flags mboot_mem_info
.long magic
.long flags
.long (-magic-flags)
#内核汇编入口
.global _start
.align 4
_start:
movl $stack_top,%esp#设置函数调用所需的栈
call bootmain
stop:
jmp stop
.data
stack_bottom:
.skip 16384
stack_top:
//bootmain.c
#include "types.h"
#include "graphic.h"
void bootmain(void){
puts("Hello World!");
}
既然添加了新的源文件,那么Makefile也是需要修改的(后面会学习Makefile一劳永逸的写法),新的Makefile如下:
MAKE=make
GCC=gcc
LD=ld
CFLAGS=-m32 -ggdb -gstabs+ -fno-stack-protector -fno-builtin -fno-strict-aliasing -O0 -Wall -fno-pic -nostdinc -I include
LDFLAGS=-m elf_i386 -nostdlib
QEMU_OPTION= -m 128M
OBJS=\
boot.o\
graphic.o\
bootmain.o
all:
$(MAKE) kernel
kernel:$(OBJS) kernel.ld
$(LD) $(LDFLAGS) -T kernel.ld $(OBJS) -o kernel
boot.o:boot.S
$(GCC) $(CFLAGS) -c boot.S -o boot.o
graphic.o:graphic.c
$(GCC) $(CFLAGS) -c graphic.c -o graphic.o
bootmain.o:bootmain.c
$(GCC) $(CFLAGS) -c bootmain.c -o bootmain.o
run:
sudo qemu-system-i386 $(QEMU_OPTION) --kernel kernel
debug:
sudo qemu-system-i386 $(QEMU_OPTION) -S -s --kernel kernel &
gdb -x gdbinit
clean:
rm *.o
还有一个重要的头文件types.h,这是个内核中基本数据类型的定义,不直接用C语言的原因有两个,主要是为了方便内核以后适配其他平台,其次是为了少敲点键盘(懒是人类进步的第一动力[\滑稽]),最后要说的是该内核所有的头文件都放在include目录下哦,这一点在编译参数"-I include"中可以看出。
//types.h
#ifndef TYPES_H
#define TYPES_H
#define NULL (void*)0
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;
typedef u32 pdt;
typedef u32 ptt;
#endif
现在我们终于可以编译了,如果不出意外就是以下结果。
其实直接复制粘贴代码后的结果肯定不是这个样子,是有几个问题需要你自己解决的,需要你自己创建几个目录、文件,具体做什么就需要你对照错误信息(这对于我们排除错误是很重要的,不要怕)逐一解决。
编译成功后运行的结果如下
至此我们的HelloWorld工程终于完成了,让我们先在成功的喜悦中沉浸一会儿,啊~~~爽~~~
纳尼???看着多余的字还有点不爽,那就自己亲手清除它们吧,自己动手,丰衣足食哦!
最后啰嗦一下
对于这个HelloWorld工程,上下两篇罗里吧嗦得说了一堆,主要就是为了能让刚刚入门的萌新能走的不那么坎坷,同时呢也是想结合具体实例叙述编写内核的思路,相关代码我自己也重新敲了一遍、运行了一遍,如果还有不够详细的地方请留言,我会酌情进行对应的修改。
需要说明的是以后的文章就不会这么详细了,千万不要说我懒哦(看破不说破)。
最后最后再啰嗦一句,如果在阅读过程中对环境安装、编译链接、代码调试等有任何疑问还没有解决的话请阅读对应的附录,相关的文献资料以后会上传的。
终于写完了,我又想起了我的快乐风男~~~哈塞给!!!