一步步写操作系统之第一步:加载内核到内存中
在完成了《自己动手写操作系统》第三章的几个实验(除了分页机制实验外)后。下面就要迈开编写自己的操作系统这万里长征的第一步了。在迈出第一步前,有几件事要简要说明一下。
首先,为将要实现的操作系统命名为:maios。请不要问我这个名称的含义是什么……
其次,制作此操作系统的目的是什么,或者所此操作系统的用户是谁。简单的说,maios是为自己开发,给自己使用而设计的一个个人专用操作系统。至于要实现的具体功能什么的,在此就不进行详细说明了。
最后,关于进度安排与完成期限,嗯,进度安排与完成期限是什么意思来着……
虽说是编写自己的操作系统,但是开头这几步里,基本还是按照《自己动手写操作系统》后面几章的步调慢慢来。
在这一步里的任务就是想办法将软盘(外存)里的操作系统内核加载到内存里面,并且能够将执行权交给内核。
我们知道,操作系统的启动步骤一般为:处于引导扇区的boot(引导程序)将loader(装载程序)加载到内存里,由loader完成一些必要的初始准备工作,然后才将操作系统的内核加载到内存的适当位置,最后将执行权交给内核……
但因为是给自己写的操作系统,一开始也没有什么需要初始化的,在此,我就偷懒一下,省略掉loader(装载程序)。直接在boot(引导程序)完成加载内核以及进入保护模式的工作,然后跳转到内核入口点。在以后如果boot无法满足需要后,再添加loader什么的;)
以下是相关代码:
code:run.c
程序代码:
//文件:run.c //功能:运行此程序,会自动编译“boot.c”文件与"kernel.c"文件,生成"maios.img"镜像文件, // 然后启动虚拟机运行镜像文件里的操作系统。在编写与修改代码期间,不必关闭此程序。 // 在改完代码并保存好后,在此程序(控制台)点击回车键后,会再次编译运行修改后的代码。 // 如此,可以方便快捷的在windows下修改与测试所写的操作系统 ;) //运行:请使用yc09编译器编译此程序。 //作者:miao //时间:2010-5-13 #define FDISK_SIZE 1474560 //镜像大小:1.4MB //虚拟机设置 char *maiosSrc = "megs: 32 \n" "romimage: file=BIOS-bochs-latest, address=0xf0000 \n" "vgaromimage: VGABIOS-elpin-2.40 \n" "floppya: 1_44=maios.img, status=inserted \n" "boot: a \n" "log: maios.out \n" "mouse: enabled=0 \n" "keyboard_mapping: enabled=1, map=x11-pc-us.map \n"; //编译指定代码文件并放入镜像指定位置 //filename:要编译的文件名 imgBuffer:保存到的镜像缓冲区 //startIndex:指定起始位置 limitSize:编译后程序限定大小 int CompileFile(char *fileName, byte *imgBuffer, int startIndex, int limitSize) { char *tempBuffer; //保存编译后程序的临时缓冲区 //编译fileName里的代码,结果放到tempBuffer中 int length = YC_CompileCpp(&tempBuffer, fileName, 0, 0); if(length <= 0 || length > limitSize) { printf("文件: %s 中存在一些错误或文件过大(超过%d字节):%d字节\n", fileName,limitSize,length); return 1; } printf("文件: %s 编译成功,大小为:%d字节。\n", fileName, length); //将编译好的程序放到镜像引导扇区缓冲区指定起始位置 memcpy(imgBuffer + startIndex, tempBuffer, length); free(tempBuffer); return 0; } int main(int argc, char **argv) { char * filePath = argv[0]; //当前文件夹路径 char fileName[MAX_PATH]; //用于缓存各个文件名 //将可执行文件的完整路径去掉文件名,保留文件夹路径 for( int i = strlen(filePath);filePath[i] != '\\';i--) filePath[i] = '\0'; byte *imgBuffer = new byte[FDISK_SIZE];//镜像缓冲区 _start: //编译引导程序并放在引导扇区 if(CompileFile("boot.c", imgBuffer, 0, 509)) goto _restart; //0000H-01FFH 为FAT引导扇区[第0扇区] 以55 AA标志结束 长度为200H(512)字节 imgBuffer[510] = 0x55; imgBuffer[511] = 0xaa;//标记软盘引导结尾 //编译内核 if(CompileFile("kernel.c", imgBuffer, 512, 65536*8)) goto _restart; //创建操作系统镜像maios.img if(YC_writefile("maios.img", imgBuffer, FDISK_SIZE) != FDISK_SIZE) { printf("写: %s 文件过程中出现错误。\r\n", fileName); goto _restart; } printf("\n%s 创建成功。\n", fileName); //生成操作系统虚拟机配置文件maios.src YC_writefile("maios.src", maiosSrc, strlen(maiosSrc)); //运行虚拟机 YC_WinExec(strcat(strcpy(fileName, filePath), "bochs.exe"), "-q -f maios.src"); _restart: printf("\n点击回车重新编译运行!\n\n\n"); while(getchar() != '\n'); goto _start; return 0; }
code:global.h
程序代码:
//文件:global.h //功能:操作系统的公共头文件 //作者:miao //时间:2010-5-13 //定义GDT(全局描述符)属性 #define DA_32 0x4000 //32位段 #define DA_DRW 0x92 //存在的可读写数据段属性值 #define DA_DRWA 0x93 //存在的已访问可读写数据段类型值 #define DA_CR 0x9A //存在的可执行可读代码段属性值 #define DA_C 0x98 //存在的可执行可读代码段属性值 //定义LDT(局部描述符)属性 #define DA_LDT 0x82 //局部描述符表类型值 //定义门属性 #define DA_386CGate 0x8c //386调用门类型 //描述符特权等级(0~3:从高到低) #define DA_DPL0 0x00 //描述符特权等级为3 //选择子 #define SA_TIL 0x4 //将TI位置1,表示是LDT选择子 typedef unsigned int t_32; //4字节 typedef unsigned short t_16; //2字节 typedef unsigned char t_8; //1字节 typedef int t_bool;//4字节 typedef unsigned int t_port;//4字节 //存储段描述符/系统段描述符 struct DESCRIPTOR //共 8 个字节 { t_16 limit_low; //Limit 2字节 t_16 base_low; //Base 2字节 t_8 base_mid; //Base 1字节 t_8 attr1; //P(1) DPL(2) DT(1) TYPE(4) 1字节 t_8 limit_high_attr2; //G(1) D(1) 0(1) AVL(1) LimitHigh(4) 1字节 t_8 base_high; //Base 1字节 }; #define Descriptor(bas,len,attr) { \ (len) & 0xffff, \ (bas) & 0xffff, \ ((bas)>>16)&0xff, \ (attr) & 0xff, \ (((attr)>>8) &0xf0) + (((len)>>16) & 0x0f), \ ((bas) >> 24) & 0xff } \ #define Gate(slector,offset,dCount,attr) { \ (offset) & 0xffff, \ slector, \ (dCount)&0x1f , \ attr, \ ((offset)>>16) &0xff, \ ((offset) >> 24) & 0xff } \
code:boot.c
程序代码:
//文件:boot.c //功能:操作系统的引导程序,将内核程序(引导扇区后的几个扇区内容)加载到内存0x7f00处(省略loader程序) // 加载完内核后,进入保护模式并跳转到内核程序的入口点(内存0x7f00处) //运行:run.exe自动会编译boot.c与生成img并调用Bochs运行此程序。 //作者:miao //时间:2010-5-13 #define YCBIT 16 //告诉编译器,以16位格式编译程序 #define YCORG 0x7c00 //告诉编译器,在7c00处加载程序 #include "global.h" //GDT界限,只负责跳转到保护模式,到时会加载新的GDT DESCRIPTOR label_gdt[] = { // 段基址 段界限 属性 Descriptor(0, 0, 0), Descriptor(0x7f00, 0xfffff, DA_CR | DA_32),//32位代码段,可执行可读 }; //GDT 选择子,根据GDT界限设置偏移量值 #define SelectorCode32 8*1 //指向32位段处 #pragma pack(1) struct GDT_PTR { unsigned short size; void *addr; }; #pragma pack() GDT_PTR GdtPtr = {sizeof(label_gdt), (char*)label_gdt}; //段界限,基地址 asm void main() { mov ax, cs mov ds, ax mov ax, 0x0 mov es, ax //清屏 mov ah, 06h //屏幕初始化或上卷 mov aL, 00h //AH = 6, AL = 0h mov bx, 1110h //蓝色底色 mov cx, 0 //左上角: (0, 0) mov dl, 4fh //第0列 mov dh, 1fh //第0行 int 10h //显示中断 //将软盘引导扇区后的几个扇区信息(内核程序)移动到内存中 mov al, 0x04 //移动4个扇区 mov ah, 0x02 //表示读软盘扇区到内存 mov bx, 0x7f00 //移动到的位置:es:bx mov cl, 0x2 //开始扇区(位0-5),磁道(柱面)号的高2位(位6-7) mov ch, 0x0 //磁道(柱面)号的低8位 mov dl, 0x0 //驱动号(若为硬盘,位7置1) mov dh, 0x0 //磁头号 int 0x13 mov ax, cs mov es, ax lgdt GdtPtr //加载 GDTR cli //关中断 //打开地址线A20 in al, 92h or al, 00000010b out 92h, al //准备切换到保护模式,置cr0的PE位为1 mov eax, cr0 or eax, 1 mov cr0, eax //真正进入保护模式(内核入口点) jmp dword SelectorCode32:0x0 }
code:kernel.c
程序代码:
//文件:kernel.c //功能:内核程序,目前功能仅为显示一个字符串。用以证明成功加载内核到内存并运行内核代码。 //运行:run.exe自动会编译boot.c与生成img并调用Bochs运行此程序。 //作者:miao //时间:2010-5-13 #define YCBIT 32 //告诉编译器,以32位格式编译程序 #define YCORG 0x0 //此值会对在编译时对变量函数等产生地址基址偏移量,简单起便,设置为0 #include "global.h" #define ProtecAddr 0x7f00 //进入保护模式后的程序基址 #define retf db 0xcb //因为yc09编译器不识别指令retf,使用宏直接定义指令retf asm void DispStr();//显示一个字符串,需要先设置好esi指向字符串地址,edi指向字符串的起始位置 //GDT 选择子 #define SelectorCode32 8*1 //指向32位段处代码段,可执行可读 #define SelectorVideo 8*2 //指向显存首地址 #define SelectorData32 8*3 //指向32位段处,这样,在程序中的变量就可以读写了 #define SelectorLDT 8*4 //指向LDT,通过这个跳转到局部任务 //门选择子 #define SelectorCallGateTest 8*5 //指向门任务 //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() char Msg1[] = "I am in kernel now!"; //内核入口点 asm void main() { lgdt cs:GdtPtr //加载新的GDTR mov eax, SelectorVideo mov gs, ax //视频段选择子(目的) mov eax, SelectorData32 //令32位代码段的变量(printPlace)可以读写 mov ds, ax //下面显示一个字符串(显示已经到达保护模式信息) mov esi, &Msg1 //源数据偏移 mov edi, ((80 * 0 + 0) * 2) //目的数据偏移。屏幕第0行, 第0列。 call DispStr died: jmp died } //显示一个字符串,需要先设置好esi指向字符串地址,edi指向字符串的起始位置 asm void DispStr() { mov ah, 14h //蓝底红字(ah = 14h) //循环逐个将字符串输出 _DispStr: mov al, ds:[esi]//因为可读,才能用cs指向当前段的Msg1字符串 inc esi cmp al, '\0' //判断是否字符串结束 jz _stop mov gs:[edi], ax add edi, 2 jmp _DispStr _stop: //显示完毕 ret }