介绍
内核开发不是一件简单的工作,它是对你编程技术的一次考验。所谓的内核开发,也就是你要开发一个直接管理硬件的软件。内核是一个操作系统的核心, 它管理着硬件所能提供的资源。
内核所要管理的最重要的资源之一就是中央处理器(CPU)。它为特定的操作分配时间,在另一个事件需要发生时中断一项任务或进程。这就是“多任务”。多任务的内核是非常具有合作性的,在其中,每个程序自身都具有一种叫做“让步”的功能。在必要时,它能将自己的处理时间“慷慨”地让给下一个任务或进程。在抢占式多任务内核中,系统时钟(system timer)被用来中断当前进程,并切换到新的进程——这是一种强制性切换,它很大程度地保证了重要的进程能有大量的时间去执行。现在有很多种分配CPU时间的调度算法。最简单的一种被叫做“时间片轮转”(Round Robin)。它让所有的进程都按照一个列表来运行。更加复杂的算法需要优先级(priorities),这样高优先权的任务就能比低优先权的任务有更多的CPU时间去运行。比这还复杂的算法就是实时算法。它能保证特定的进程能拥有至少确定数目的时钟滴答(timer ticks)来运行。但最根本地,最重要的资源还是时间。
下一个重要的资源似乎是显而易见的,那就是内存。曾经有段时间,内存资源比CPU时间更宝贵,因为内存是有限的,而CPU时间是无限的。你可以将内核设计成能内存高效型,但需要大量CPU时间。或者CPU高效型,但要使用大量的内存空间来做高速缓存。最好的方法还是将两钟算法综合:争取最好的内存使用率,同时节约CPU时间。
最后一个内核需要管理的资源就是硬件资源。这包括中断请求(IRQ),它是硬件用来告诉CPU去执行特定任务或者操纵特定数据的特殊信号。另一个重要的硬件资源是直接存储器存取(DMA)通道。一个批处理文件注释DMA通道允许设备锁定内存总线并将它的数据直接传输进系统内存,同时无需CPU参与。这是一个提高系统性能的好方法:一个支持DMA的设备能在不打扰CPU的情况下传输数据,然后用IRQ中断CPU,告诉它数据传输已经完成。声卡和网卡就同时使用IRQ和DMA通道。第三种硬件资源是地址,就像内存,但是它是以接口的形式出现在I/O总线上。通过I/O接口,设备能被配置、读取或给予数据。设备能使用很多I/0接口。举例来说,8到16就是典型的接口范围。
总览
我写这篇指南的目的是要向你展示怎样去建立内核开发最基本的组件。包括:
1)建立内核开发环境
2)最基本的:设置引导器GRUB
3)连接各个源文件并调用main()
4)在屏幕上显示信息
5)建立全局描述表(GDT)
6)建立中断描述表(IDT)
7)建立控制中断和IRQ的中断服务例程(ISR)
8)重新映射可编程中断控制器(PIC)到新的IDT入口
9)安装并管理IRQ
10)管理可编程记时器/系统时钟(PIT)
11)管理键盘IRQ和键盘数据
12)…剩下的就看你自己的了!
2)最基本的:设置引导器GRUB
3)连接各个源文件并调用main()
4)在屏幕上显示信息
5)建立全局描述表(GDT)
6)建立中断描述表(IDT)
7)建立控制中断和IRQ的中断服务例程(ISR)
8)重新映射可编程中断控制器(PIC)到新的IDT入口
9)安装并管理IRQ
10)管理可编程记时器/系统时钟(PIT)
11)管理键盘IRQ和键盘数据
12)…剩下的就看你自己的了!
开始
内核开发是一项漫长的写代码和调试的工作。这似乎在开始会是很令人居丧的。但是你不
必需要大量的写内核的工具。本指南将使用GRUB来将你的内核载入内存。GRUB需要读入一个运行在保护模式下的二进制镜像:这个“镜像”就是我们马上就开始建立的内核。
在读这篇指南之前,你至少需要具备C语言的知识。我强烈建议你能掌握X86汇编的知识,因为这将对你在后来熟练地使用寄存器有很大帮助。你至少需要这些工具:一个可以产生32位代码的编译器,一个32位连接器和一个能产生32位输出的汇编器。
至于硬件方面,你必须有一太拥有386或者更好的处理器(包括386、486、5x86、6x86、Pentium、 Athlon、 Celeron、 Duron等)。你最好能有第二台用来实验的电脑,它应该就在你开发用的电脑旁边。如果你没有,你可以使用虚拟机软件或者就在开发机上测试(虽然这样开发会很耗费时间,且需要你重新启动很多次)。
用于测试的硬件配置要求
- 一台IBM兼容机
- 一块386或更好的处理器
- 大于4MB的内存
-
- 一块386或更好的处理器
- 大于4MB的内存
-
一块VGA兼容的显卡和一台显示器
- 一块键盘
- 一个软驱
(是的,你甚至不需要一块硬盘)
- 一块键盘
- 一个软驱
(是的,你甚至不需要一块硬盘)
用于开发的硬件配置要求
- 一台IBM兼容机
- 一块Pentium II or K6 300MHz或者更好的处理器
- 至少32MB内存
- 一块VGA兼容的显卡和一台显示器
- 一块键盘
- 一个软驱
- 一块有足够空闲空间的硬盘(用来存储开发工具、开发文档和源代码)
- Microsoft Windows, 或者类Unix(Linux, FreeBSD) 操作系统
- 一个因特网浏览器,用来查资料
(强烈建议使用一个鼠标)
- 一块Pentium II or K6 300MHz或者更好的处理器
- 至少32MB内存
- 一块VGA兼容的显卡和一台显示器
- 一块键盘
- 一个软驱
- 一块有足够空闲空间的硬盘(用来存储开发工具、开发文档和源代码)
- Microsoft Windows, 或者类Unix(Linux, FreeBSD) 操作系统
- 一个因特网浏览器,用来查资料
(强烈建议使用一个鼠标)
工具
编译器
- The Gnu C Compiler (GCC) [Unix]
- DJGPP (GCC for DOS/Windows) [Windows]
- DJGPP (GCC for DOS/Windows) [Windows]
汇编器
- Netwide Assembler (NASM) [Unix/Windows]
虚拟机软件
- VMWare Workstation 4.0.5 [Linux/Windows NT/2000/XP]
- Microsoft VirtualPC [Windows NT/2000/XP]
- Bochs [Unix/Windows]
- Microsoft VirtualPC [Windows NT/2000/XP]
- Bochs [Unix/Windows]
基本内核
在这部分,我们将涉及一点汇编的知识,学习创建使用最基本的连接脚本。最后,我们将学习如何使用批处理文件自动进行汇编、编译和连接这个最最基本的运行在保护模式下的内核。请注意,我将假设你已经安装了NASM和DJGPP在你的操作系统上,并且你已经掌握了最基本的X86汇编语言。
内核入口
内核的入口就是当引导器调用内核时最先被执行的那段代码。这部分代码一般总是用汇编语言来写的。这是为 完成很多特定功能,比如建立新堆栈和加载新的GDT、IDT 或段寄存器。这些都是不能用C语言来完成的。在很多入门级的内核,以及一些更大,更专业的内核中,这些汇编代码被写入一个单独的文件,而其它部分则被写入若干C语言文件中。
如果你对汇编程序有一点了解,这个文件中的代码就将变得非常直观。正如代码所做的,整个文件就是装载一个新的8KB堆栈,然后跳转到一个无限循环。堆栈占用很少的内存,但它能被用来存储并传递参数到用C语言的函数。它也可被用来存储在你函数中使用的局部变量。其它的全局变量被存储在Data和BSS区域中。在下面代码中“mboot”和“stublet”之间的部分组成了一段特殊的代码,它能让GRUB知道这个二进制文件是一个内核。如果你
不能理解这个多引导的头部的话,就跳过吧。
; 这是内核入口。我们能够在这里调用main函数。 ; 我们也能在这里设置堆栈等其它有趣的东东,比如说, ; 建立GDT。请注意:中断在这里被禁用了的,后面将设置。 [BITS 32] global start start: mov esp, _sys_stack ; 这里指示堆栈到我们新的堆栈位置 jmp stublet ; 这里必须是4byte的对齐方式, 因此我们用“ALIGN 4”来处理。 ALIGN 4 mboot: ; 这里定义的多引导宏使后面的某些内容更加的可读。 MULTIBOOT_PAGE_ALIGN equ 1<<0 MULTIBOOT_MEMORY_INFO equ 1<<1 MULTIBOOT_AOUT_KLUDGE equ 1<<16 MULTIBOOT_HEADER_MAGIC equ 0x1BADB002 MULTIBOOT_HEADER_FLAGS equ MULTIBOOT_PAGE_ALIGN | MULTIBOOT_MEMORY_INFO | MULTIBOOT_AOUT_KLUDGE MULTIBOOT_CHECKSUM equ -(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS) EXTERN code, bss, end ; 这是GRUB多引导的头部。一个引导标识。 dd MULTIBOOT_HEADER_MAGIC dd MULTIBOOT_HEADER_FLAGS dd MULTIBOOT_CHECKSUM ; 关于kludge - 必须是物理地址。 注释:连接脚本将填充这些数据! dd mboot dd code dd bss dd end dd start ; 这是一个无穷循环。 注释:后面,我们将在“jmp $”前插入“extern _main”,然后紧接着插入“call _main”。 stublet: jmp $ ; 后面我将在这里添加GDT的代码! ; 仅仅看完了几页指南,我们就将在这里添加中断服务例程(ISR)! ; 这里是BSS区的定义。 我们将用它来存储堆栈。 ; 记住:一个堆栈的体积实际上是不断减少的, ; 所以我们在申明标识“_sys_stack”之前申明数据的大小。 SECTION .bss resb 8192 ; 预留8KBytes的内存空间 _sys_stack: |
内核入口文件: “start.asm” |
连接脚本
连接器是将所有编译器和汇编器的输出文件连接成一个二进制映象的工具。二进制映象有很多种不同格式,而Flat、AOUT、COFF、PE和ELF是最常见的几种。你是否还记得,我们在选择的连接器是LD,它有很多功能。现在存在很多不同版本的LD,它们都能将产生你需要的二进制映象格式。但无论你选用哪种格式,在输出文件中都会出现三个区域,Text(或者Code),Data和BSS区。Text(或者Code)区是只读的代码区。Data区是可读可写的数据区,举例来说,你在程序定义了一个变量并给它赋值5,那么这个“5”就被存储在Data区。而BSS区则是可读可写且没有初始化的数据区。它存储着未赋任何值的数组。注意,BSS区是一个虚拟的区域它不存在于二进制映象中,但当二进制映象被加载后,它就存在于内存中了。
下面将登场的就是LD连接脚本了。在脚本中,我们需要突出三个关键字:OUTPUT_FORMAT、ENTRY和SECTIONS。OUTPUT_FORMAT告诉LD,要创建什么类型的二进制映象。简便起见,我们就创建plain binary映象。ENTRY告诉连接器哪个目标文件将第一个被连接。我们希望start.asm编译后的文件start.o第一个被连接,因为这是我们
内核的入口。下一行是“phys”。它不是一个关键字,但是是一个将在连接器脚本中被用到的变量。在这种情况下,我们把它用来指向一个地址,它占用1MB空间。我们的映像将被加载到那里并在那里运行。第三个关键字是SECTIONS。如果你仔细研究这个连接器脚本,你将会发现它定义了3个最要的区:.text(代码区),“.data”(数据区),“.bss”(BSS区)。同时还定义了4个变量:code,data,bss和end。别被这个几个bianliang 给弄迷糊了,你看见的是在我们的启动文件start.asm中的实际变量。ALIGN(4096)确保了每个区都以4096byte为界限。这样,每个区都在一个单独的内存页中运行。
OUTPUT_FORMAT("binary") ENTRY(start) phys = 0x00100000; SECTIONS { .text phys : AT(phys) { code = .; *(.text) . = ALIGN(4096); } .data : AT(phys + (data - code)) { data = .; *(.data) . = ALIGN(4096); } .bss : AT(phys + (bss - code)) { bss = .; *(.bss) . = ALIGN(4096); } end = .; } |
连接器脚本: 'link.ld' |
汇编,然后连接!
现在,我们将使用上面提到的这个连接器脚本来汇编start.asm。这将创建一个能被GRUB加载的内核映像。如果你在Unix环境下,最简单的方法就是创建一个Makefile全自动地来汇编,编译和连接。但包括我在内的大多数人都偏向于使用windows。在win下,我们可以使用批处理文件(.bat)来做这些。所谓批处理文件,就是仅仅是一个DOS命令的集合。但你只需要使用一个命令(就是批处理文件的文件名)就可以运行一整套的命令。甚至更简单,你只需双击批处理文件,就可以在win下编译你的内核了。
下面就是我们将要使用的批处理文件。echo是个在屏幕上显示文本的DOS命令。nasm是我们使用的汇编器。这里,我们需要产生一个aout格式的目标文件,因为 LD需要知道其格式以便在连接过程中解释符号。参数-o使start.asm被汇编成start.o。rem是注释命令,表示它后面的内容是注释内容,但在执行时,计算机将忽略此段。ld是连接器。参数-T指定连接脚本的名称。参数-o指定输出文件的文件名。其它的参数可以被理解为我们需要的文件,以便连接起来建立kernel.bin。最后,pause命令将在屏幕上显示“Press any key ”以等待用户按键继续。这样,我们就能看到汇编器和连接器的输出信息,以便我们查错了。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论