上一篇谈到了怎样把计算机用我们自己的程序启动起来,然而我已经说过,那只是最最初 始的一步,只表明了我们的确可以让计算机从一开始就按照我们的命令去执行一个任务。 但它并不能算是一个操作系统,当计算机用引导程序引导起来之后,我们需要让它把真正 的操作系统内核载入内存中,然后跳转到真正的操作系统内核中运行。 本篇将完成一个真正意义上的操作系统引导,而不是第一篇里所描述的计算机的引导。这 里我们将从计算机启动时的16位的实模式转到现在通用的32位的保护模式下。 现在的操作系统除了最低层的部份之外,均由高级语言完成,在本篇中我们也将用高级语 言来编写我们的内核。本篇中实现的一个内核是用C语言编写的。用汇编写的引导程序,把 C语言写的内核载入并执行,这就是本篇将要完成的任务。 阅读本文最好有那么一丁点的汇编基础~~~ 还是那句老话,我水平极其有限,这只是一个尝试,对于不计其数的错误,望大家海涵, 千万不要来砍我啊~~~ 非常感谢各位老师,大牛能指点一二! 现在,我们继续我们的实验。 首先,我想先介绍一下实模式与保护模式,对于本篇,它们是非常重要的概念!一定要领 会!就算现在领会有误也要尽可能的领会!大家都知道现在用的Intel的CPU在原先8086的 时代是16位的,后来技术发展了,Intel的CPU也改成了32位的了。但是为了同以前16位的 程序兼容,Inetl在它的32位的CPU中仍然保留了16位的模式。这样在32位的CPU中,就有两 种工作模式,一种是16位的模式,称之为实模式;另一种是32位的模式,称之为保护模式 。在计算机刚刚启动的时候,它是工作在实模式情况下,为了让它工作在保护模式的情况 下,我们需要向CPU中一个cr0的寄存器置位,这样,当cr0的第0位被置位后,CPU就转到32 位模式下工作了。因此,从16位转到32位是非常简单的,只需要对CPU的一个寄存器进行置 位操作就行了。但是,还是需要做一个准备工作,这里我们先来谈谈16位与32位模式下内 存是怎样访问的。 学过汇编的都知道,在8086中,内存是分段的,一个内存地址由 段基址:偏移量 构成。这 是因为在16位的CPU中,寄存器的大小是16位的,只能访问2^16=64K的地址,这实在是太小 了,因此需要通过分段的机制来扩大程序的访问内存的范围。显然,在16位的CPU中,一个 段的大小最大只能是64K 但是在32位的CPU中,寄存器的大小是32位的,可以访问2^32=4GB有空间,这又太大了!一 个程序只需要其中的能小一部分空间就足够了。因此,在32位的CPU中,也将内存分段,为 每个程序分配必要的空间,以免浪费。(注,在32位的CPU中,内存管理有分段与分页两种 模式,分页主要用于实现虚拟内存,本文只讨论分段模式)这样就需要记住每个段是属于 哪个程序的?是用来干啥的?是数据段还是代码段?可以访问还是不可以访问?是只读的 还是可写的?。。。这些需要记录的信息是在太多了,CPU就把它们组织起来放在内存中一 个专门的地方,这块内存就称之为段描述符,它描述了这个段的属性。每个段都有一个描 述属性的段描述符,把它们全部组织起来放在内存的一个专门的地方,就形成了段描述符 表。32位CPU有全局描述符表(GDT)与局部描述符表(LDT)之分。全局描述符表就指此表 可被所有的程序访问。在32位的CPU中,段寄存器并不是表示段的基址,而是表示一个指向 描述符表的索引,用来指出此程序用到的段的描述符在描述符表的位置。然后,访问此描 述符表取出描述符,由此才得到有关段的属性信息。32位CPU的段寄存器器还是16位的,不 过,它用13位来作段描述符表的索引,因此,我们可以定义的最大段数为2^13=8192个。由 此可见,在32位的保护模式下,内存是通过段描述符表来访问的,因此,当我们转到32位 模式之间,我们需要创建好段与段描述符表。 好了,基础知识我们已经有了,闲话少说,我们来编写我们的引导程序,完整的源代码如 下: 注:在上一篇已经注解过的地方,这次就不注解了,如果有不理解之处,请找上一篇看看 [BITS 16] [ORG 0x7C00] jmp main ; --------------------------------------------------------------------------- ; 数据定义 bootdrive db 0 ; -------------------------------------------------------------- ; GDT 定义,此处定义段及段描述符 gdt: gdt_null: ; 这是CPU要求保留的,CPU要求第一个段必须是空段,不知它 ; 想用来干啥 dd 0 dd 0 ; 每个段的描述符是64位(8字节),空描述符的这64位全是0 gdt_code_addr equ $ - gdt ; 求得代码段在GDT表中的位置 gdt_code: dw 0xffff ; 段大小为4GB dw 0 ; 基地址(24位) db 0 db 10011010b ; 属性描述位,指明此是代码段,可读可执行 db 11001111b ; 具体的每一位是代表什么可查Intel CPU编程手册 db 0 ; 没有的可以去网上下,也可以找我要 gdt_data_addr equ $ - gdt ; 求得数据段在GDT表中的位置 gdt_data: dw 0xffff dw 0 db 0 db 10010010b ; 指明此是数据段,可读可写 db 11001111b db 0 gdt_end: gdt_addr: dw gdt_end - gdt - 1 ; GDT 表的大小 dd gdt ; GDT 表的位置 ; --------------------------------------------------------------------------- main: ; 引导程序从此处开始执行 mov [bootdrive] , dl ; 得到启动的驱动器号 xor ax , ax ; 设置 DS mov ds , ax ; 清屏 ;mov ax , 3 ; 设置清屏功能号 ;int 0x10 ; 调用 BIOS 10 号中断清屏 .ResetFloppy ; 重置磁盘,不是必须的,主要是为了安全起见 mov ax , 0 ; 设置重置磁盘的功能号 mov dl , [bootdrive] ; 选择启动磁盘 int 0x13 ; 调用 BIOS 13 号中断重置磁盘 jc .ResetFloppy ; 如果出错则重试 .ReadFloppy ; 读内核到内存中 0000:9000 (es:bx)处, ; 为什么要读到9000处,这不是一定的, ; 你可以读到另外一个合适的地址 ; 什么地址合适?怎样知道一个地址合不合适?待会再说 xor ax , ax ; 设置 es 寄存器 mov es , ax mov bx , 0x9000 mov ah , 2 ; 设置读磁盘功能号 mov dl , [bootdrive] ; 设置欲读驱动器号 mov ch , 0 ; 磁头号 mov cl , 2 ; 起始扇区号,从第二个扇区开始读, ; 第一个扇区是引导扇区,第二个才是内核所在 mov al , 17 ; 需读入扇区的数量,此处读了17个扇区, ; 是怕内核较大,读少了读不完 int 13h ; 调用 BIOS 13 号中断开始读扇区, ; 此中断会将数据读到 es:bx 处 jc .ReadFloppy ; 如果出错则重试(ah中是错误号,为0则没错) mov dl , [bootdrive] ; 停止驱动器 mov edx , 0x3f2 mov al , 0x0c out dx , al cli ; 关中断 lgdt [gdt_addr] ; 载入 GDT 的描述符 mov eax , cr0 ; 下面三句设置 cr0 的第 0 位(PE位)为1, ; 表示进入保护模式 or eax , 1 mov cr0 , eax jmp gdt_code_addr:code_32 ; 跳入32位的代码段中 [BITS 32] code_32: mov ax , gdt_data_addr ; 以下三句设置 DS,ES,SS,FS,GS ; 为数据段描述表的位置 mov ds , ax mov es , ax mov ss , ax mov fs , ax mov gs , ax mov esp , 0xffff ; 设置堆栈的头指针 jmp gdt_code_addr:0x9000 ; 跳入内核, ; gdt_code_addr是定义的代码段的描述符所在的索引 ; 由于我们先前是把内核读到了 0x9000的位置, ; 因此我们现在就转到内核所在去执行, ; 引导程序胜利完成历史使命! ;--------------------------------------------------------------------------- times 510-($-$$) db 0 db 0x55 db 0xAA 下面,我们用我们最最最最最最熟悉的C语言来编写我们的系统内核。源代码如下: char* msg = "Welcome to HIT Operation System!Version 0.0001 by xyb" ; void main() { unsigned char* videomem = ( unsigned char* )0xb8000 ; /* 获得显存地址 */ while( *msg != '\0' ){ *videomem++ = *msg++ ; /* 设置显示字符的ASCII码 */ *videomem++ = 0x1b ; /* 设置文字属性(背景色,前景色,是否闪烁等)*/ } for(;;); } 这个代码太简单了,就不详加解释了,这里只需要说明一下0xb8000是啥地方。让我们首先 来看一下内存是怎样被CPU分配的。 0x0000:0x0000 -> 0x0000:0x03FF = 中断相量表住的地方 0x0000:0x0400 -> 0x0000:0x04FF = BIOS 住的地方 (BIOS 数据区 BDA) 0x0000:0x0500 -> 0x0000:0x7BFF = 你可以使用的自由内存区 0x0000:0x7C00 -> 0x0000:0x7DFF = 系统引导程序的家, 现在你知道引导程序中要加上[ORG 0x7c00]了吧 0x0000:0x7E00 -> 0x9000:0xFFFF = 你可以使用的自由内存区 0xA000:0x0000 -> 0xB000:0xFFFF = VGA 显示器的显示内存所在!!!!!!!!(*) 0xC000:0x0000 -> 0xF000:0xFFFF = BIOS 住的地方 (BIOS 的只读内存区) 。。。。。。。。。 上面列出的就是CPU保留的内存部分的分配情况,注意*行,再看看C语言程序,显然,我们 直接把数据写到显存中了!这样,数据就可能被显示出来。 现在我们的程序已经编制完成,接下来,需要完成的工作是生成我们的启动盘启动我们可 爱电脑,就快熬出头了~~~,累啊!!! 先编译我们的引导程序,然后把它写到引导文件img的头512字节中,这一点在上一篇里已 经详细描述过了,这里就不在描述了,若有不清楚的地方,请找上一篇看看 下面,我们要生成我们的内核,这比较麻烦,因为在windows下却少必要的编译工具与链接 工具,目前的vc,bc都只能生成win32标准的程序,而非二进制文件,弄了好久也没解决这 个问题,有没有哪位大侠知道啊!!!麻烦告诉一声,谢谢!^_^ 没办法,MS的东东太烂了,我们只好在Linux下编译它。 在Linux下编写好此C语言程序(也可以在Windows下编写好,然后拷到Linux下用),取名为 xyb.c 然后,建如如下命令: gcc -c xyb.c 其中 -c 表示只编译不链接 接着再敲 ld -o xyb -Ttext 0x9000 -e main xyb.o -o 表示输出文件名,-Ttext 0x9000 表示程序基址定为 0x9000 -e main 表示从main()开 始执行 还要敲 objcopy -R .note -R .comment -S -O binary xyb xyb.bin -R .note -R .comment 表示移掉 .note 与 .comment 段 -S 表示移出所有的标志及重定位信息 -O binary xyb xyb.bin 表示由xyb生成二进制文件xyb.bin 然后,把xyb.bin拷回到Windows中,并用WinHex写到img文件中方才引导程序的后面,OK! 大功告成! 下面是运行的结果:
 这里,本文的所有任务与预定目标已经完成。现在的引导程序已经是一个比较完整的引导 程序了,不过内核还不能算是一个内核,如果有时间,我希望能将实验继续下去~~~,还望 大家多多支持,多多指点!^_^ 本文所介绍的程序全部源代码及最后生成的映象文件可在下 面地址下载 ftp://202.118.239.46/Incoming/Other/BTC/temp/pyos1.rar 仍然留个Mail,欢迎大家指教! [email protected] 原文:http://purec.binghua.com/Article/ShowArticle.asp?ArticleID=4 
|