| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 4445 人关注过本帖
标题:操作系统实验二:从实模式跳转到保护模式+简单说明什么是引导程序及其作用
取消只看楼主 加入收藏
miaowangjian
Rank: 2
等 级:论坛游民
帖 子:34
专家分:30
注 册:2010-1-29
结帖率:100%
收藏
已结贴  问题点数:0 回复次数:1 
操作系统实验二:从实模式跳转到保护模式+简单说明什么是引导程序及其作用
菜鸟给菜鸟解释引导程序:

    关于引导程序的问题,本来应该是在至少有了自己的内核了才需要说明的。但是未来的几个实验都会是在引导扇区编写的。对于完全从零开始的初学者来说,不知道引导扇区与引导程序,可能会一直心里没底,也没法很好的理解实验代码,最重要的是没法用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
}

搜索更多相关主题的帖子: 从实 实验 模式 引导程序 操作系统 
2010-02-01 06:52
miaowangjian
Rank: 2
等 级:论坛游民
帖 子:34
专家分:30
注 册:2010-1-29
收藏
得分:0 
我目前只是实验到LDT(局部任务),共四个实验,都发在CSDN上我的博客里。http://blog.
现在有两个实验已经移植到了这里(添加了一些我的理解和之后实验新的认识,修正了一些注释错误或解释不恰当的内容)。

之前在CSDN上,因为没有调试经验,加上对YC90编译器的使用不太熟悉,我基本是一周发一个实验。
如果可能的话,等我将四个实验都整理在这里发好后,会加快实验进度。

ps:(此次实验遗漏说明的地方)
run.c中定义了一个imgBuffer数组(缓冲区),大小为1474560字节(1.4MB)。
(若保存为以img为后缀名的文件,就可以把它当做是一个软盘镜像,虚拟机就可以把它当做软盘,在启动时读取它的引导扇区内容(前512字节))。

其后,将pm16.c的代码编译并放到缓冲区的前256字节内(也就是说,pm16.c生成的程序必须小于等于256字节)
再后,将pm32.c的代码编译并放到缓冲区的256~509字节内(也就是说,pm32.c生成的程序必须小于等于254字节)
pm16.c的256字节 加上 pm32.c的254字节  在加上2个字节(值为:0x55、0xaa)的引导扇区结束标记,如此就形成了一个完整的引导扇区。

电脑启动后:
    引导扇区中pm16.c编译后的代码加载到0x7c00起的内存地址处,范围为0x7c00~0x7cff(256字节)。
    引导扇区中pm32.c编译后的代码加载到0x7d00起的内存地址处(0x7c00+256字节),范围为0x7d00~0x7dff(256字节,含结束标志)。
引导扇区加载完毕,就开始执行0x7c00的指令(也就是pm16.c的main函数),需要执行pm32.c的main函数,只需要跳转到内存地址0x7d00处就行了。

注意:main函数不一定是一个程序的开头最先执行的部分,如果程序里全局变量付了初始值,而初始值又无法再编译器的预编译阶段计算出来,
那么会生成几条指令放在main函数前面执行,以便在程序执行一开始就给变量付好初始值。
2010-02-01 16:58
快速回复:操作系统实验二:从实模式跳转到保护模式+简单说明什么是引导程序及其作 ...
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.024146 second(s), 8 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved