操作系统实验二:从实模式跳转到保护模式+简单说明什么是引导程序及其作用
菜鸟给菜鸟解释引导程序:关于引导程序的问题,本来应该是在至少有了自己的内核了才需要说明的。但是未来的几个实验都会是在引导扇区编写的。对于完全从零开始的初学者来说,不知道引导扇区与引导程序,可能会一直心里没底,也没法很好的理解实验代码,最重要的是没法用bochs调试。
那么什么是引导程序,它的作用是什么呢?这要从系统的启动说起。
我们知道,内存和外存(硬盘、光盘、软盘、优盘等)不是一回事。电脑一断电,内存信息就没了,而外存随你拔下插回去随便玩都无所谓。显然外存更可靠,可是内存和电脑关系更铁。只要电脑一启动,就可以开始执行内存中的程序或是随便获取读写内存中任何信息(这句话是有水分的)。
可问题来了,电脑刚启动,内存中空无一物,即没有程序可执行,也没有什么数据好读写的。
就好比我们要在一个山顶上开party,可是山顶上空无一物,主人、客人、食物(整个操作系统程序及其他数据)都在山下,却没有一个上山的途径。此时就需要引导程序登场了。引导程序就好比一条细绳(真的很细),只要你一按下电脑开关,就会将山下绳子绑住的东西拉到上山(BIOS将启动盘的前引导扇区(一般为前512字节)复制到内存地址0x7c00~0x7dff上,并从0x7c00开始执行程序代码)。
那么怎么用一根细绳将山下的大堆人和东西运到上山呢?有些人可能见过类似的智力题也知道答案,就是:细绳子将粗一点的绳子拉上去,粗一点的绳子将更粗的绳子拉上去。直到绳子能够胜任搬运操作系统的搬运操作。为什么我们使用操作系统能能够方便随意地运行程序打开保存文件数据呢,就是因为已经运到山顶的操作系统根据需要为客人(应用程序)和食物(文件数据)打造好了各种专用的豪华电梯。
对于简单的系统,只需在引导扇区512字节大小内编写一个读取外存中操作系统到内存合适的位置,并将运行权交给操作系统就行了。
可是在实际应用中,我们往往不希望如此直接,而是希望开机时能选择进入Ghost备份还原系统,或是装了双系统,需要在开始时选择进入哪个系统。加上需要先识别硬盘所采用的格式及分区,如此一来,在引导扇区512字节编写的简陋引导程序就不够用了。如此,就需要先让引导扇区编写的程序加载一个更专业的引导程序来负责引导(如Linux下的Lilo及后来的grub,windows下的NTLDR、bootmgr等),实际的情况可能比我说的更复杂一些。
引导程序说完,下面是这次实验的正餐了。
电脑一开始是运行在实模式下的。但为什么我们要从实模式跳转到保护模式呢?原因可以列出很多,但简单的说就是实模式的操作系统已经过时了,对于实模式下内存寻址1MB的限制与现在的电脑内存动辄1G到2G来说实在是太简陋了(虽然据说可以通过一些技巧可能实现4G内存的访问,可惜我不懂也用不着)。
关于保护模式,我们第一个要认识的是GDT(全局描述符)。它是一个指具有特定结构的数据块,可以定义为一个一维数组,每个数组元素包含三类信息:段基址、段界限、段属性。通过创建GDT,将系统内存分为一个个具有特定特性(可读、可写、可执行等)的区域,如此就可以有了代码段、数据段、堆栈段等等。
第二个问题,如何跳转到保护模式,请参考以下几个步骤:
1.准备好GDT
2.使用lgdt指令加载gdtr //gdtr也是一个具有特定结构的数据块,里面包含了GDT的地址及大小
3.打开地址线A20
4.设置cr0的PE位为1
5.使用cli指令关闭中断
6.使用GDT选择子跳转到32位代码段,此时进入保护模式
下面是实验代码,分pm16.c、pm32.c以及公共的头文件pm.h,当然还有不可缺少的宿主程序run.c。
具体功能为:
pm16.c:清屏然后打印一个字符串"This is real model! ",然后跳转到保护模式(pm32.c中main函数)
pm32.c:打印一个字符串:"This is protect model!"
code:run.c
程序代码:
//文件:run.c //功能:编译操作系统的实验代码并创建img,生成bochs配置文件,运行bochs。 //说明:实验代码由16位部分引导程序与32位部分引导程序组成。 // 16位部分引导程序放在引导扇区的前半部分,0~255字节 // 32位部分引导程序放在引导扇区的后半部分,256~509字节 // 510、511字节放引导程序结束标记:0x55、0xaa //运行:请使用yc00编译器编译运行,点击回车再次编译运行 //作者:miao //时间:2010-2-1 #define FDISK_SIZE 1474560 //镜像大小:1.4MB //虚拟机设置 char *pmSrc = "megs: 32 \n" "romimage: file=BIOS-bochs-latest, address=0xf0000 \n" "vgaromimage: VGABIOS-elpin-2.40 \n" "floppya: 1_44=pm.img, status=inserted \n" "boot: a \n" "log: pm.log \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; //保存部分引导程序的临时缓冲区 //编译此部分引导程序,结果放到tempBuffer中 int length = YC_CompileCpp(&tempBuffer, fileName, 0, 0); if(length <= 0 || length > limitSize) { printf("文件: %s 中存在语法错误或文件过大(%S字节):%d字节\n", fileName,limitSize,length); return 1; } printf("文件: %s 编译成功,大小为:%d字节。\n", fileName, length); //将1此部分引导程序放到镜像引导扇区缓冲区指定起始位置 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: //编译16位部分引导程序并放在引导扇区的前半部分,0~255字节 if(CompileFile("pm16.c", imgBuffer, 0, 256)) goto _restart; //编译32位部分引导程序并放在引导扇区的后半部分,256~509字节 if(CompileFile("pm32.c", imgBuffer, 256, 256-2)) goto _restart; //0000H-01FFH 为FAT引导扇区[第0扇区] 以55 AA标志结束 长度为200H(512)字节 imgBuffer[510] = 0x55; imgBuffer[511] = 0xaa;//标记软盘引导结尾 //创建操作系统镜像pm.img if(YC_writefile("pm.img", imgBuffer, FDISK_SIZE) != FDISK_SIZE) { printf("写: %s 文件过程中出现错误。\r\n", fileName); goto _restart; } printf("\n%s 创建成功。\n", fileName); //生成操作系统虚拟机配置文件pm.src YC_writefile("pm.src", pmSrc, strlen(pmSrc)); //运行虚拟机 YC_WinExec(strcat(strcpy(fileName, filePath), "bochs.exe"), "-q -f pm.src"); _restart: printf("\n点击回车重新编译运行!\n\n"); while(getchar() != '\n'); goto _start; return 0; }
code:pm.h
程序代码:
//文件:pm.h //功能:pm16.c与pm32.c的公共头文件 //运行:run.exe自动会编译pm16.c与pm32.c然后生成img并调用Bochs运行此程序 //提示:请先用yc90编译run.c文件,生成run.exe程序 // 之后修改pm16.c与pm32.c中代码,可直接运行run.exe查看效果,点击回车再次编译运行 //作者:miao //时间:2010-2-1 //定义GDT属性 #define DA_32 0x4000 //32位段 #define DA_DRW 0x92 //存在的可读写数据段属性值 #define DA_CR 0x9A //存在的可执行可读代码段属性值 //GDT 选择子,根据pm16.c中的GDT界限设置偏移量值 #define SelectorSode32 8*1 //指向32位段处 #define SelectorVideo 8*2 //指向显存首地址 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 } \
code:pm16.c
程序代码:
//文件:pm16.c //功能:设置GDT,在GDT中初始化32位代码段,然后切换到保护模式跳转到32位代码段 //说明:16位部分引导程序放在镜像引导扇区的前半部分,0~255字节中,程序大小不能超过这个限制 //运行:run.exe自动会编译pm16.c与pm32.c然后生成img并调用Bochs运行此程序 //提示:请先用yc90编译run.c文件,生成run.exe程序 // 之后修改pm16.c与pm32.c中代码,可直接运行run.exe查看效果,点击回车再次编译运行 //作者:miao //时间:2010-2-1 #define YCBIT 16 //告诉编译器,以16位格式编译程序 #define YCORG 0x7c00 //告诉编译器,在下面的程序会被加载在内存0x7c00处,编译器就会加上这个偏移量生成变量和函数地址 #include "pm.h" //GDT(全局描述符表),告诉系统保护模式下段的结构(分多少个段,各段的基址和界限)和属性(可执行、可读、可写、多少位等) //因为GDT的段基址、段界限、属性并不是顺序排列,而是段基址分两块穿插在其中(见pm.h),所以需要用借用宏Descriptor将其放入正确的地方 DESCRIPTOR label_gdt[] = { // 段基址 段界限 属性 Descriptor(0, 0, 0), Descriptor(0x7d00, 0xfffff, DA_CR | DA_32),//32位代码段,可执行可读,即pm32.c中main函数的内存地址 Descriptor(0xb8000, 0xffff, DA_DRW) //显存地址段,可读可写 }; #pragma pack(1) struct GDT_PTR //通过这个结构体,在加载GDT时(lgdt命令)告诉系统GDT的地址和大小 { unsigned short size;//GDT的大小 void *addr; //GDT的地址 }GdtPtr = { sizeof(label_gdt), (char*)label_gdt}; #pragma pack() #define MsgLngth 24 //串长度 char Msg[MsgLngth] = "This is real model! "; asm void main() { mov ax, cs mov ds, ax mov es, ax //清屏 mov ah, 06h //屏幕初始化或上卷 mov aL, 00h //AH = 6, AL = 0h mov bx, 1110h //蓝色底色 mov cx, 0 //左上角: (0, 0) mov dl, 4fh //第x列 mov dh, 1fh //第x行 int 10h //显示中断 mov ax, &Msg mov bp, ax //es:bp=串地址 mov ah, 13h //AH:13显示字符串 mov al, 1h //AH = 13, AL = 01h mov bx, 0014h //页号为0(BH = 0) 蓝底红字(BL = 14h) mov cx, MsgLngth //CX = 串长度 mov dl, 00h //起始列 mov dh, 00h //起始行 int 10h //显示中断 lgdt GdtPtr //加载 GDTR cli //关中断 //打开地址线A20 in al, 92h or al, 00000010b out 92h, al //准备切换到保护模式 mov eax, cr0 or eax, 1 mov cr0, eax //真正进入保护模式,根据GDR中注册的信息,跳转到段基址0x7d00,偏移量为0的地方(pm32.c中main函数内存地址) jmp dword SelectorSode32:0x0 }
code:pm32.c
程序代码:
//文件:pm32.c //功能:保护模式下32位代码段,功能为显示一个字符串 //说明:32位部分引导程序放在镜像引导扇区的后半部分,256~509字节中,程序大小不能超过这个限制 //运行:run.exe自动会编译pm16.c与pm32.c然后生成img并调用Bochs运行此程序 //提示:请先用yc90编译run.c文件,生成run.exe程序 // 之后修改pm16.c与pm32.c中代码,可直接运行run.exe查看效果,点击回车再次编译运行 //作者:miao //时间:2010-2-1 #define YCBIT 32 //告诉编译器,以32位格式编译程序 #define YCORG 0x0 //此值会对在编译时对变量函数等产生地址基址偏移量,简单起便,设置为0 #include "pm.h" char Msg1[] = "This is protect model!"; //32 位代码段. 由实模式跳入(可执行可读) asm void main() { mov ax, SelectorVideo mov gs, ax //视频段选择子(目的) //下面显示一个字符串(显示已经到达保护模式信息) mov ah, 14h //蓝底红字 mov esi, &Msg1 //源数据偏移 mov edi, ((80 * 1 + 0) * 2) //目的数据偏移。屏幕第1行, 第0列。 //循环逐个将字符串输出 _DispStr: mov al, cs:[esi]//因为可读,才能用cs指向当前段的Msg1字符串 inc esi cmp al, '\0' //判断是否字符串结束 jz _stop mov gs:[edi], ax add edi, 2 jmp _DispStr _stop: //显示完毕 jmp _stop }