一步步写操作系统之第二步:在内核添加屏幕输出函数
这一步,主要是一个过渡,为将来的工作做准备。在这里的主要工作是整理代码与添加字符串与int型数值的输出函数。
在yc09中,编译后的二进制文件,函数似乎是按照在代码中的先后顺序排列的,然后再在末尾放置所有的变量。
在整个内核中,是以kernel.c文件为核心。在后面添加任何模块,都是在kernel.c文件的开头包含头文件,在kernel.c文件的末尾包含代码文件。如此,就可以保证内核的入口函数(main函数)一直是在最前面。
所有的头文件里,主要包含宏、数据结构定义、全局变量以及函数的声明等。
而对应的代码文件,则是具体的函数体。
下面是此次新添加或有过修改的代码,未作改动的文件将不再贴出。
code:kernel.h(新)
程序代码:
//文件:kernel.h //功能:kernel头文件,放置需要用到的宏、数据结构定义、全局变量以及函数的声明等 //作者:miao //时间:2010-5-14 /* 宏定义区 */ #define ProtecAddr 0x7f00 //进入保护模式后的程序基址 //GDT 选择子 #define SelectorCode32 8*1 //指向32位段处代码段,可执行可读 #define SelectorVideo 8*2 //指向显存首地址 #define SelectorData32 8*3 //指向32位段处,这样,在程序中的变量就可以读写了 /* 数据结构与全局变量定义区 */ //GDT界限,注意,这个与boot.c中的GDT不同,从boot.c跳转过来后会立即载入这个新的GDT DESCRIPTOR label_gdt[] = { // 段基址 段界限 属性 Descriptor(0, 0, 0), Descriptor(ProtecAddr, 0xfffff, DA_CR | DA_32), //32位代码段,可执行可读 Descriptor(0xb8000, 0xffff, DA_DRW), //显存地址段,可读可写 Descriptor(ProtecAddr, 0xfffff, DA_DRW | DA_32), //令32位代码段的变量可以读写 }; #pragma pack(1) struct GDT_PTR { t_16 size; void *addr; }GdtPtr = {sizeof(label_gdt), (char*)&label_gdt + ProtecAddr}; //段界限,基地址 #pragma pack() int kernel_main();
code:kernel.c(改)
程序代码:
//文件:kernel.c //功能:内核程序,目前功能为测试print.c里的几个输出函数 //运行:run.exe自动会编译boot.c与生成img并调用Bochs运行此程序。 //作者:miao //时间:2010-5-14 #define YCBIT 32 //告诉编译器,以32位格式编译程序 #define YCORG 0x0 //此值会对在编译时对变量函数等产生地址基址偏移量,简单起便,设置为0 #include "global.h" #include "kernel.h" #include "print.h" //内核入口点 asm void main() { lgdt cs:GdtPtr //加载新的GDTR mov eax, SelectorVideo mov gs, ax //视频段选择子(目的) mov eax, SelectorData32 //令32位代码段的变量(printPlace)可以读写 mov ds, ax jmp kernel_main } int kernel_main() { int i; //测试字符串输出函数 disp_str("this is default color string.\n"); for(i=0;i<=0x7f;i++) { disp_color_str("c ",i); } disp_str("\n"); for(i=0x80;i<=0xff;i++) { disp_color_str("c ",i); } disp_str("\n"); for(i=0;i<0x2f;i++) disp_int(i); disp_str("\n"); for(i=0xffff;i>0xffe0;i--) disp_int(i); while(1); return 0; } #include "print.c"
code:print.h(新)
程序代码:
//文件:print.h //功能:print头文件,放置需要用到的宏、数据结构定义、全局变量以及函数的声明等 //作者:miao //时间:2010-5-14 /* 数据结构与全局变量定义区 */ int disp_pos;//记录已经输出到屏幕哪里的坐标("光标") /* 函数的声明区 */ void disp_str(char * info);//使用蓝底红字输出一个字符串 asm void disp_color_str(char * info, int color);//使用指定颜色输出一个字符串 void disp_int(int input);//以十六进制格式输出一个数 //文件:print.h //功能:print头文件,放置需要用到的宏、数据结构定义、全局变量以及函数的声明等 //作者:miao //时间:2010-5-14 /* 数据结构与全局变量定义区 */ int disp_pos;//记录已经输出到屏幕哪里的坐标("光标") /* 函数的声明区 */ void disp_str(char * info);//使用蓝底红字输出一个字符串 asm void disp_color_str(char * info, int color);//使用指定颜色输出一个字符串 void disp_int(int input);//以十六进制格式输出一个数
code:print.c(新)
程序代码:
//文件:print.c //功能:向屏幕输出字符的相关函数 //运行:run.exe自动会编译boot.c与生成img并调用Bochs运行此程序。 //作者:miao //时间:2010-5-14 /* 屏幕颜色与字符的显示(两字节大小) 高字节 低字节 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 闪烁 R G B 高亮 R G B ASCII 背景颜色 字符颜色 字符 */ void disp_str(char * info) { disp_color_str(info,0x14);//默认蓝底红字 } asm void disp_color_str(char * info, int color) { push ebp mov ebp, esp mov esi, [ebp + 8] //info mov edi, disp_pos //取上次输入完后的“光标”位置 mov ah, [ebp + 0xc] //color dpc_1: lodsb //从esi指向的源地址中逐一读取一个字符送入al中 test al, al //al是否为空 jz dpc_2 //为空(无字符串要输出),结束函数 cmp al, 0Ah //是回车吗? jnz dpc_3 //不是换行符,跳到dpc_3直接输出字符 push eax //保存ah里的字符颜色信息 mov eax, edi //求出出下一行起始位置坐标 mov bl, 160 div bl and eax, 0FFh inc eax mov bl, 160 mul bl mov edi, eax pop eax jmp dpc_1 dpc_3: mov gs:[edi], ax //将ax的信息放到显存显示出来 add edi, 2 jmp dpc_1 dpc_2: mov disp_pos, edi //保存“光标”位置 pop ebp ret } char *itoa(char *str, int num)//数字前面的 0 不被显示出来, 比如 0000B800 被显示成 0xB800 { char *p = str; char ch; int i; t_bool flag = FALSE;//标记是否将零输出,从高位起,若当前位不为0,标记为true,其后遇到0,都会输出 *p++ = '0'; *p++ = 'x'; if(num == 0) //为零,以0x0形式输出 *p++ = '0'; else { for(i=28;i>=0;i-=4)//从十六进制高位起,逐个转换为字符形式 { ch = (num >> i) & 0xF; if(flag || (ch > 0)) { flag = TRUE; ch += '0'; if(ch > '9') //为a,b,c,d,e,f ch += 7; *p++ = ch; } } } *p = 0; return str; } //将数字转换为十六进制字符串显示出来 void disp_int(int num) { char output[16]; itoa(output, num); disp_str(output); }