bkerndev内核开发完整教程(中文版) 内核开发(原文来自网络)
简介 内核是操作系统的核心。开发内核意味你必须能够编写与硬件打交道的软件使之与硬件交互并管理硬件。内核是用来管理硬件提供的资源的软件系统。 系统中最中要的资源就是CPU时间。也就是说要为指定的操作分配CPU时间,而且可能还要中断当前正在执行的进程或者任务当其他任务设定的事件发生时。这意味着多任务。多任务系统中,程序自身调用一个生成函数产生另外一个进程,当它想把执行时间交给下一个可运行的进程或任务时。抢占式的多任务调度中,系统时钟用来中断当前正在执行的进程切换到新的进程,这是一种强制的切换,它能保证进程可以被给予一个时间片去执行。有多种进程调度算法用来找出下一个要运行的进程。最简单的调度算法称为循环队列。更复杂的算法引入了优先级的概念,某些高优先级的进程比低优先级的进程允许请求更多的CPU运行时间。更复杂的调度算法是实时调度算法。这种调度程序被设计用来保证某个进程至少能够运行一定大小的时间段。 另外一个重要的系统资源就是内存。有些时候内存可能比CPU时间更为珍贵,因为内存是有限的,但是CPU时间是无限的。你可以编写内核似的你的系统是内存高效的这就需要更多的CPU时间,或者你也可以使得你的内核是CPU高小的,这就需要占用大量的内存去存储缓存和高速缓存以便记忆公用的一些数据避免再次查找这些数据。最好的办法是兼顾两者。
最后一个系统资源就是硬件资源。这包括中断请求(IRQ),他们是一些特殊的信号,这些信号被硬件用来告诉CPU执行一个特定的程序去处理这写设备已经准备好的数据。另外一个硬件资源就是DMA通道。DMA通道允许设备锁定内存总线并直接读写系统内存不用CPU的参与。这可以大大提高系统的性能。可以使用DMA的设备能够在不打扰CPU的请卡下直接传输数据,然后使用IRQ中断CPU,告诉CPU数据传输已完毕。声卡和以太网卡是能够使用IRQ和DMA的两个例子。第三种硬件资源是地址,类似于内存,但是这里是指的I/O断口地址。一个设备可以被配置用他的I/O端口读取或者给出数据。一个设备可以使用多个I/O端口。
概览
这个教程用来教读者如何创建一个基本的内核,包括如下内容: 1)设置你的开发环境 2)基础:设置GRUB两个BOOT阶段 3)连接入其他文件并调用main() 4)打印到屏幕 5)设置自定义的Global Descriptor Table(GDT) 6)设置自定义的Interrupt Descriptor Table(IDT) 7)设置中断服务程序(ISR)以便处理你的中断请求 8)重新映射PIC到新的IDT入口(修改中断向量表) 9)安装和响应IRQ 10)管理可编程计数器/系统时钟(PIT) 11)管理键盘IRQ和键盘数据 12)剩下的内容自己去添加吧
正式开始
内核开发是一个漫长的编写代码的过程,要调试各种系统组件。这是一个另人生畏的任务,然而你并不需要很多的工具去书写你自己的内核。这个内核开发教程主要是通过使用GRUB来载入你自己的内核到内存中。GRUB需要被引导到一个保护模式下的二进制映像,这个映像就是我们要开发的内核。 对于这个教程来说,你至少需要懂得C语言编程的一般知识。X86汇编语言的一般知识。这就是说你需要的工具只是:一个可以产生32位代码的C编译器,一个32位的连接器,和一个可以产生X86目标代码的汇编器。 在硬件方面你至少必须有一太具有386以上的 处理器 材计算机(386,486,5X86,6X86 Pentium,Athlon,Celeron,Duron,等等)如果你有第二太计算机用于测试那就更好。如果你没有二台计算机,你也可以使用虚拟机器。
用于调试的硬件要求 -100%IBM兼容的PC -具有386或更高级的处理器 -4M的内存 -VGA兼容的显示卡和显示器 -键盘 -软盘驱动器 (你并不需要在硬盘上来测试内核)
推荐的开发硬件配置
-100%IBM兼容的PC -PentiumII或K6 300MHZ -32M内存 -VGA兼容的显卡和监视器 -键盘 -软驱 -足够大小的硬盘 -MS-WINDOWS或者自己喜欢的UNIX(LINUX,FREEBSD) (强烈推荐配置鼠标)
要使用的工具
编译器 -Gnu C编译器 (GCC) -DJGPP(GCC的WINDOWS转换版)
汇编器 -NASM
虚拟机 -Bochs -VMWare Workstation -MS VirtualPC
基本内核 在这部分教程中,我们将深入研究汇编器,我们将学习如何创建一个连接脚本以及为什么要用连接脚本的原因,最后我们将学习如何使用批处理文件自动实现汇编,编译和这个基本的保护模式的内核的连接。请注意,本教程假定你已经将NASM和DJGPP安装到了WINDOWS中。我们也假定你对X86汇编语言有基本的了解。
内核入口 内核的入口点是一段代码,这段代码将在bootloader调用你的内核的时候首先被执行。这段代码几乎总是使用汇编语言编写的,因为某些操作,比如,设置一个新的堆栈或者载入一个新的GDT,IDT或者段寄存器等,不能使用C代码简单的实现。在基本的内核中包括那些更大的内核以及更专业的内核中,将把这写汇编代码放入到一个文件当中,而把剩下的部分当到几个C源程序中。 如果你了解一些汇编器的知识,则阅读这些代码将会很容易。这些代码所做的所有工作就是装入一个新的8KB的堆栈,然后跳入一个无限的循环。堆栈是一块小的内存区域,但是它被用来存储或着传递C函数的参数。它也被用来保存局部变量,这些局部变量是在你的函数中声明和使用的。任何全局变量被保存在数据段和BSS段中。在"mboot"和"stublet"之间的代码块制作了一个特殊的签名,GRUB用这个签名来验证输出的二进制文件就是将要载入的代码,实际上它是一个内核映象。不要太花费力气去理解那个多重启动的头。
-------------------------------------------------------------------------------------------------------
start.asm(内核入口文件)
; 这是内核的入口点。 我们可以在这里调用main()也可以 ; 用来设置堆栈和其他重要内容, 比如: ; 设置段寄存器和GDT. 请注意中断在这里是被关闭的,后面我们再讨论中断的问题。 [BITS 32] global start ;定义一个全局标签 start: mov esp, _sys_stack ; 将堆栈指向我们新设置的堆栈区 jmp stublet ; 跳转到stublet标签
; 这部分必须是4BYTE对齐的,因此我们使用ALIGN 4命令 ALIGN 4 mboot: ;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
; This is the GRUB Multiboot header. A boot signature dd MULTIBOOT_HEADER_MAGIC dd MULTIBOOT_HEADER_FLAGS dd MULTIBOOT_CHECKSUM ; AOUT kludge - must be physical addresses. Make a note of these: ; The linker script fills in the data for these ones! dd mboot dd code dd bss dd end dd start
; This is an endless loop here. Make a note of this: Later on, we ; will insert an 'extern _main', followed by 'call _main', right ; before the 'jmp $'. stublet: jmp $
; Shortly we will add code for loading the GDT right here!
; In just a few pages in this tutorial, we will add our Interrupt ; Service Routines (ISRs) right here!
; Here is the definition of our BSS section. Right now, we'll use ; it just to store the stack. Remember that a stack actually grows ; downwards, so we declare the size of the data before declaring ; the identifier '_sys_stack' SECTION .bss resb 8192 ; 保留8KB的内存空间 _sys_stack:
The kernel's entry file: 'start.asm'
----------------------------------------------------------------------------------------------------- 连接器脚本
连接器是一个工具,这个工具提取所有编译器和汇编器的输出文件并将他们连接成一个二进制文件。二进制文件可以是多种格式的:FLAT,AOUT,COFF,PE以及ELF是比较常用的几种格式。我们选择LD作为连接器。这个连接器是个多用途的连接器而且具有大量的有用特性。无论你选择输出哪种格式的文件,输出的文件都将由三部分组成:TEXT,DATA,BSS段。BSS是由未初始化的数据组成的,比如,它保存任意你还没有设定值的数组。BSS是一个虚拟的段。它在二进制映象中是不存在的,但是当你的二进制映象被载入的时候它存在于内存中。 下面我们就要讲LD连接器脚本。有三个关键词与连接器脚本相关:OUTPUT_FORMAT将告诉连接器创建何种格式的二进制映象。为了简单起见,我们将始终使用plain binary映象。ENTRY将告诉连接器哪一个目标文件将作为列表中的第一个文件被连接。我们想把start.o作为第一个文件连接,因为它是我们内核的入口点。下一行是"phys",它不是关键字,它是一个连接器脚本使用的变量。这里,我们把它作为一个指向内存地址的指针:指向1MB处的指针,这就是我们的二进制印象载入和执行的地方。第三个关键字是SECTION。如果研究这个连接器脚本,你将发现如果定义了三个重要的段:TEXT,DATA,BSS,脚本中将会有三个变量,定义为TEXT,DATA,BSS,还有一个END变量。不要搞混了,你看到的三个变量实际上就是我们的start.asm中的三个变量。ALIGN(4096),保证每一部分都以4096字节为边界,也就是说是以4K为边界的,这正好是一页的大小。
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。在WINDOWS下我们可以创建一个批处理文件。一个批处理文件就是一个DOS命令的简单集合,但是这个批处理文件你可以只使用一个命令去执行:就是批处理文件的名字本身。你甚至可以简单地双击批处理文件在WINDOWS下编译你的内核。
下面就是一个我们将要使用的批处理文件。"echo" 是一个DOS命令,它在屏幕上显示"echo"后面的文本。"nasm"是我们使用的汇编器:我们将编译为AOUT格式,因为LD需要一个已知的格式以便在连接的过程中解吸符号。这将把start.asm 汇编为start.o。rem 命令是注释命令,后面跟注释文本。ld 是我们的连接器,-T参数告诉LD告诉连接器后面要跟一个连接器脚本。-o参数后面跟着输出文件。其他的参数理解为我们需要连接在一起的文件以便产生kernel.bin。最后,pause命令将显示“press a key to continue”并且等到我们按下一个键。 ----------------------------------------------------------------------------------------- echo Now assembling, compiling, and linking your kernel: nasm -f aout -o start.o start.asm rem Remember this spot here: We will add 'gcc' commands here to compile C sources
rem This links all your files. Remember that as you add *.o files, you need to rem add them after start.o. If you don't add them at all, they won't be in your kernel! ld -T link.ld -o kernel.bin start.o echo Done! pause Our builder batch file: 'build.bat'
------------------------------------------------------------------------------------------
创建Main()并连接C源程序 在标准的C编程中,函数main()是你的程序的标准入口点。为了保持你的编程习惯和经验,熟悉内核开发过程,本教程中将保持main()函数作为你的C代码的程序入口的风格。上一节我们说过,我们力图使用最少的汇编代码。在后面的部分中,我们必须返回到start.asm处,添加调用C函数的中断服务程序。
在这一部分中,我们将创建一个main.c和一个头文件,这个头文件包含了一些共用的函数原形,这个头文件的名字叫system.h。main.c将包含函数main(),它是C程序的入口点。作为内核开发,我门不需要从 main()函数正常返回。许多操作系统用main来初始化内核和子系统,载入SHELL应用程序,最后main()将进入一个空闲的循环中。当多任务系统中没有其他任务需要执行时,系统使用空闲循环。这里有一个main.c的例子: ----------------------------------------------------------------------------------------------- #include < system.h >
/* You will need to code these up yourself! */ unsigned char *memcpy(unsigned char *dest, const unsigned char *src, int count) { /* Add code here to copy 'count' bytes of data from 'src' to * 'dest', finally return 'dest' */ }
unsigned char *memset(unsigned char *dest, unsigned char val, int count) { /* Add code here to set 'count' bytes in 'dest' to 'val'. * Again, return 'dest' */ }
unsigned short *memsetw(unsigned short *dest, unsigned short val, int count) { /* Same as above, but this time, we're working with a 16-bit * 'val' and dest pointer. Your code can be an exact copy of * the above, provided that your local variables if any, are * unsigned short */ }
int strlen(const char *str) { /* This loops through character array 'str', returning how * many characters it needs to check before it finds a 0. * In simple words, it returns the length in bytes of a string */ }
/* We will use this later on for reading from the I/O ports to get data * from devices such as the keyboard. We are using what is called * 'inline assembly' in these routines to actually do the work */ unsigned char inportb (unsigned short _port) { unsigned char rv; __asm__ __volatile__ ("inb %1, %0" : "=a" (rv) : "dN" (_port)); return rv; }
/* We will use this to write to I/O ports to send bytes to devices. This * will be used in the next tutorial for changing the textmode cursor * position. Again, we use some inline assembly for the stuff that simply * cannot be done in C */ void outportb (unsigned short _port, unsigned char _data) { __asm__ __volatile__ ("outb %1, %0" : : "dN" (_port), "a" (_data)); }
/* This is a very simple main() function. All it does is sit in an * infinite loop. This will be like our 'idle' loop */ void main() { /* You would add commands after here */
/* ...and leave this loop in. There is an endless loop in * 'start.asm' also, if you accidentally delete this next line */ for (;;); } 'main.c': Our kernel's small, yet important beginnings
------------------------------------------------------------------------------------------------ 在编译main.c之前,我们需要在start.asm中加如两行代码。我们需要让NASM知道main()在外部文件中,我们需要从汇编文件中调用main().打开start.asm,查找标签为stublet:的行,在这行的后面加入如下行:
extern _main call _main
等等,为什么main前面有下划线呢“_main",在C中我们是声明为main的?GCC编译器在编译的时候将在所有函数和变量名前面加入一个"_",因此,要从汇编程序中引用一个函数或者变量名,我们必须在函数或者变量名前面加一个"_",如果这个函数或者变量在C源程序中的话。
这下似乎可以进行编译了,但是我们还忽略了system.h。简单地创建一个文件名字叫system.h,把所有函数原形如:memcpy, memset, memsetw, strlen, inportb, and outportb 都加入到这个文件中去。使用宏定义来避免文件包含是明智的,使用宏定义还可以避免在头文件中使用#ifndef, #define, 和#endif这类技巧声明某些东西。本教程的每一个C源程序中都将包含这个头文件---system.h。这里定义了每一个你将在你的内核中使用的函数。你可以按照你的需要随意地扩展这个库。system.h的代码如下: ---------------------------------------------------------------------------------------- #ifndef __SYSTEM_H #define __SYSTEM_H
/* MAIN.C */ extern unsigned char *memcpy(unsigned char *dest, const unsigned char *src, int count); extern unsigned char *memset(unsigned char *dest, unsigned char val, int count); extern unsigned short *memsetw(unsigned short *dest, unsigned short val, int count); extern int strlen(const char *str); extern unsigned char inportb (unsigned short _port); extern void outportb (unsigned short _port, unsigned char _data);
#endif
Our global include file: 'system.h' ----------------------------------------------------------------------------------------- 下面我们需要知道如何编译。打开你的"build.bat",然后添加如下行以编译你的"main.c"。请注意,我们假定"system.h"在你的内核源代码目录的"include"目录中。这个命令执行编译"gcc"。在众多的参数中,"-wall"会对你的代码提出警告信息。"-nostdinc"与"-fno -builtin"结合意味着你不能使用标准C库函数。"-I./inlude"告诉编译器你的头文件在当前目录中的"include"目录中。"-c"告诉GCC仅执行编译而不进行连接。"-o main.o"是编译器将要输出的文件。与最后一个参数"main.c"结合在一起意思是把"main.c"编译为"main.o"。
------------------------------------------------------------------------------------------- gcc -Wall -O -fstrength-reduce -fomit-frame-pointer -finline-functions -nostdinc -fno-builtin -I./include -c -o main.o main.c
Add this line to 'build.bat'
------------------------------------------------------------------------------------------ 不要忘记遵循我们在"build.bat"中的指令。你需要把"main.o“添加到创建内核需要连接的目标文件列表中。 最后如果你仍然坚持创建你我们附加的函数比如memcpy,则main.c的代码如下:
------------------------------------------------------------------------------------------ /* bkerndev - Bran's Kernel Development Tutorial * By: Brandon F. (friesenb@gmail.com) * Desc: Main.c: C code entry. * * Notes: No warranty expressed or implied. Use at own risk. */ #include <system.h>
void *memcpy(void *dest, const void *src, size_t count) { const char *sp = (const char *)src; char *dp = (char *)dest; for(; count != 0; count--) *dp++ = *sp++; return dest; }
void *memset(void *dest, char val, size_t count) { char *temp = (char *)dest; for( ; count != 0; count--) *temp++ = val; return dest; }
unsigned short *memsetw(unsigned short *dest, unsigned short val, size_t count) { unsigned short *temp = (unsigned short *)dest; for( ; count != 0; count--) *temp++ = val; return dest; }
size_t strlen(const char *str) { size_t retval; for(retval = 0; *str != '\0'; str++) retval++; return retval; }
unsigned char inportb (unsigned short _port) { unsigned char rv; __asm__ __volatile__ ("inb %1, %0" : "=a" (rv) : "dN" (_port)); return rv; }
void outportb (unsigned short _port, unsigned char _data) { __asm__ __volatile__ ("outb %1, %0" : : "dN" (_port), "a" (_data)); }
void main() { int i;
gdt_install(); idt_install(); isrs_install(); irq_install(); init_video(); timer_install(); keyboard_install();
__asm__ __volatile__ ("sti");
puts("Hello World!\n");
// i = 10 / 0; // putch(i);
for (;;); }
main.c的详细代码(实现了某些库函数)
-----------------------------------------------------------------------------------------------
打印到屏幕 现在我门尝试在屏幕上打印一些东西。为了在屏幕上打印,我们需要一种方法按照我们的需要控制屏幕滚动。而且允许不同的颜色会感觉更好。幸运的是,VGA显卡使得这个工作变的非常简单。它给我们提供了一块内存,我们可以写如代表字符和字符显示属性的字节对以在屏幕上显示信息。VGA的控制器将自动绘制屏幕象素的刷新。屏幕滚动是用我们的内核软件来实现的。这是我们第一个要写的驱动程序。
前面我提到过,text内存是地址空间中的一小块内存。这个缓冲区位于物理内存的0xb8000(图形区是在0X3C800)。这个缓冲区具有"short"类型的数据类型,也就是说缓冲中的每一个单元都占16位。每一个16位的单元可以被看做高8位和低8位组成。低8位告诉显示控制器在屏幕上输出哪一个字符,其实它就是字符的ASCII码。高8位定义了绘制字符的前景色和背景色。 ---------|-----------|---------| 15 12| 11 8 |7 0| ---------|-----------|---------| Backcolor| Forecolor |Character| ---------|-----------|---------|
高8位称为属性字节,低8位称为字符字节。属性字节又被分为两个4字的部分,一个代表前景色,一个代表背景色。因为只有四字节定义颜色,因此只有16种颜色可以显示。下面是一个缺省的16色的调色板: ------------------------------------------------------- Value Color Value Color ------------------------------------------------------- 0 BLACK 8 DARK GREY 1 BLUE 9 LIGHT BLUE 2 GREEN 10 LIGHT GREEN 3 CYAN 11 LIGHT CYAN 4 RED 12 LIGHT RED 5 MAGENTA 13 LIGHT MAGENTA 6 BROWN 14 LIGHT BROWN 7 LIGHT GREY 15 WHITE --------------------------------------------------------
最后,为了访问内存中的一个特定索引,有一个公式我们必须使用。文本模式内存是一个线性内存区域,但是视频控制器将它看做一个80x25的16位数值的矩阵。没一个文本行在内存中都是一个连续的区域,一行跟着一行。我们因此试着将屏幕分成一些水平的行。我们使用如下公式: index = (y_value * width_of_screen) + x_value;
这个公式表明,要访问文本内存的索引比如说(3,4),我们使用公式4*80+3=323。这说明为了在屏幕的三行四列的位置上绘制文本,我们需要这样做:
unsigned short *where = (unsigned short *)0xB8000 + 323; *where = character | (attribute << 8); 下面就是"scrn.c",它是我们所有处理屏幕输出的函数所在的位置。我们在这个文件中包含"system.h"文件,以便我们可以使用outportb,memcpy,memset,memset,memsetw,和strlen.滚动的方式是这样的,我门读取从第一行开始的所有文本内存区域并把它拷贝到原来第0行开始的位置上。这就使得整个屏幕向上滚动了一行。为了完成这个滚动操作,我们擦除最后一行文本,这可以通过在我们的属性字节中写入空格来是现。putch()函数是这个文件中最复杂的函数。它也是最庞大的一个函数,因为它需要处理换行("/n"),回车("/r"),和空格("/b")。以后,如果你希望的话,你可以处理警告字符("/a",ASCII码的7),当遇到这个字符的时候会发出一声短响。我已经包含了一个用来设置屏幕颜色的函数(settextcolor). ------------------------------------------------------------------------------------------------- #include < system.h >
/* These define our textpointer, our background and foreground * colors (attributes), and x and y cursor coordinates */ unsigned short *textmemptr; int attrib = 0x0F; int csr_x = 0, csr_y = 0;
/* Scrolls the screen */ void scroll(void) { unsigned blank, temp;
/* A blank is defined as a space... we need to give it * backcolor too */ blank = 0x20 | (attrib << 8);
/* Row 25 is the end, this means we need to scroll up */ if(csr_y >= 25) { /* Move the current text chunk that makes up the screen * back in the buffer by a line */ temp = csr_y - 25 + 1; memcpy (textmemptr, textmemptr + temp * 80, (25 - temp) * 80 * 2);
/* Finally, we set the chunk of memory that occupies * the last line of text to our 'blank' character */ memsetw (textmemptr + (25 - temp) * 80, blank, 80); csr_y = 25 - 1; } }
/* Updates the hardware cursor: the little blinking line * on the screen under the last character pressed! */ void move_csr(void) { unsigned temp;
/* The equation for finding the index in a linear * chunk of memory can be represented by: * Index = [(y * width) + x] */ temp = csr_y * 80 + csr_x;
/* This sends a command to indicies 14 and 15 in the * CRT Control Register of the VGA controller. These * are the high and low bytes of the index that show * where the hardware cursor is to be 'blinking'. To * learn more, you should look up some VGA specific * programming documents. A great start to graphics: * http://www.brackeen.com/home/vga */ outportb(0x3D4, 14); outportb(0x3D5, temp >> 8); outportb(0x3D4, 15); outportb(0x3D5, temp); }
/* Clears the screen */ void cls() { unsigned blank; int i;
/* Again, we need the 'short' that will be used to * represent a space with color */ blank = 0x20 | (attrib << 8);
/* Sets the entire screen to spaces in our current * color */ for(i = 0; i < 25; i++) memsetw (textmemptr + i * 80, blank, 80);
/* Update out virtual cursor, and then move the * hardware cursor */ csr_x = 0; csr_y = 0; move_csr(); }
/* Puts a single character on the screen */ void putch(unsigned char c) { unsigned short *where; unsigned att = attrib << 8;
/* Handle a backspace, by moving the cursor back one space */ if(c == 0x08) { if(csr_x != 0) csr_x--; } /* Handles a tab by incrementing the cursor's x, but only * to a point that will make it divisible by 8 */ else if(c == 0x09) { csr_x = (csr_x + 8) & ~(8 - 1); } /* Handles a 'Carriage Return', which simply brings the * cursor back to the margin */ else if(c == '\r') { csr_x = 0; } /* We handle our newlines the way DOS and the BIOS do: we * treat it as if a 'CR' was also there, so we bring the * cursor to the margin and we increment the 'y' value */ else if(c == '\n') { csr_x = 0; csr_y++; } /* Any character greater than and including a space, is a * printable character. The equation for finding the index * in a linear chunk of memory can be represented by: * Index = [(y * width) + x] */ else if(c >= ' ') { where = textmemptr + (csr_y * 80 + csr_x); *where = c | att; /* Character AND attributes: color */ csr_x++; }
/* If the cursor has reached the edge of the screen's width, we * insert a new line in there */ if(csr_x >= 80) { csr_x = 0; csr_y++; }
/* Scroll the screen if needed, and finally move the cursor */ scroll(); move_csr(); }
/* Uses the above routine to output a string... */ void puts(unsigned char *text) { int i;
for (i = 0; i < strlen(text); i++) { putch(text); } }
/* Sets the forecolor and backcolor that we will use */ void settextcolor(unsigned char forecolor, unsigned char backcolor) { /* Top 4 bytes are the background, bottom 4 bytes * are the foreground color */ attrib = (backcolor << 4) | (forecolor & 0x0F) }
/* Sets our text-mode VGA pointer, then clears the screen for us */ void init_video(void) { textmemptr = (unsigned short *)0xB8000; cls(); } Printing to the screen: 'scrn.c' --------------------------------------------------------------------------------------- 接下来,我们需要把这个文件编译进我们的内核。要把它编译进内核,我们需要编辑"build.bat",加入一个新的GCC编译命令。简单地拷贝"build.bat"文件中对应欲"main.c"的命令,然后把它粘贴到"build.bat"文件的最后。在我们新拷贝的行中,我们把"main"变成"scrn"。并且,不要忘记了将"scrn.o"添加到LD需要连接的目标文件的列表中。在我们可以在main中使用这个函数的功能之前,你必须把putch,puts,cls,init_video,和settextcolor的函数原形添加到"system.h"文件中,#include < system.h > --------------------------------------------------------------------------------------- /* These define our textpointer, our background and foreground * colors (attributes), and x and y cursor coordinates */ unsigned short *textmemptr; int attrib = 0x0F; int csr_x = 0, csr_y = 0;
/* Scrolls the screen */ void scroll(void) { unsigned blank, temp;
/* A blank is defined as a space... we need to give it * backcolor too */ blank = 0x20 | (attrib << 8);
/* Row 25 is the end, this means we need to scroll up */ if(csr_y >= 25) { /* Move the current text chunk that makes up the screen * back in the buffer by a line */ temp = csr_y - 25 + 1; memcpy (textmemptr, textmemptr + temp * 80, (25 - temp) * 80 * 2);
/* Finally, we set the chunk of memory that occupies * the last line of text to our 'blank' character */ memsetw (textmemptr + (25 - temp) * 80, blank, 80); csr_y = 25 - 1; } }
/* Updates the hardware cursor: the little blinking line * on the screen under the last character pressed! */ void move_csr(void) { unsigned temp;
/* The equation for finding the index in a linear * chunk of memory can be represented by: * Index = [(y * width) + x] */ temp = csr_y * 80 + csr_x;
/* This sends a command to indicies 14 and 15 in the * CRT Control Register of the VGA controller. These * are the high and low bytes of the index that show * where the hardware cursor is to be 'blinking'. To * learn more, you should look up some VGA specific * programming documents. A great start to graphics: * http://www.brackeen.com/home/vga */ outportb(0x3D4, 14); outportb(0x3D5, temp >> 8); outportb(0x3D4, 15); outportb(0x3D5, temp); }
/* Clears the screen */ void cls() { unsigned blank; int i;
/* Again, we need the 'short' that will be used to * represent a space with color */ blank = 0x20 | (attrib << 8);
/* Sets the entire screen to spaces in our current * color */ for(i = 0; i < 25; i++) memsetw (textmemptr + i * 80, blank, 80);
/* Update out virtual cursor, and then move the * hardware cursor */ csr_x = 0; csr_y = 0; move_csr(); }
/* Puts a single character on the screen */ void putch(unsigned char c) { unsigned short *where; unsigned att = attrib << 8;
/* Handle a backspace, by moving the cursor back one space */ if(c == 0x08) { if(csr_x != 0) csr_x--; } /* Handles a tab by incrementing the cursor's x, but only * to a point that will make it divisible by 8 */ else if(c == 0x09) { csr_x = (csr_x + 8) & ~(8 - 1); } /* Handles a 'Carriage Return', which simply brings the * cursor back to the margin */ else if(c == '\r') { csr_x = 0; } /* We handle our newlines the way DOS and the BIOS do: we * treat it as if a 'CR' was also there, so we bring the * cursor to the margin and we increment the 'y' value */ else if(c == '\n') { csr_x = 0; csr_y++; } /* Any character greater than and including a space, is a * printable character. The equation for finding the index * in a linear chunk of memory can be represented by: * Index = [(y * width) + x] */ else if(c >= ' ') { where = textmemptr + (csr_y * 80 + csr_x); *where = c | att; /* Character AND attributes: color */ csr_x++; }
/* If the cursor has reached the edge of the screen's width, we * insert a new line in there */ if(csr_x >= 80) { csr_x = 0; csr_y++; }
/* Scroll the screen if needed, and finally move the cursor */ scroll(); move_csr(); }
/* Uses the above routine to output a string... */ void puts(unsigned char *text) { int i;
for (i = 0; i < strlen(text); i++) { putch(text); } }
/* Sets the forecolor and backcolor that we will use */ void settextcolor(unsigned char forecolor, unsigned char backcolor) { /* Top 4 bytes are the background, bottom 4 bytes * are the foreground color */ attrib = (backcolor << 4) | (forecolor & 0x0F) }
/* Sets our text-mode VGA pointer, then clears the screen for us */ void init_video(void) { textmemptr = (unsigned short *)0xB8000; cls(); } Printing to the screen: 'scrn.c'
----------------------------------------------------------------------------- 接下来,我么需要把刚才的这个文件编译进我们的内核。为了把它编译进内核,我们需要编辑"bulid.bat"文件,在其中加如新的GCC编译命令。我们简单的拷贝在"build.bat"中对应于"main.c"的命令行,然后把它粘贴到文件的最后。在我们新粘贴的行中,我们把"main"替换为"scrn"。不要忘记了在LD需要连接的目标文件列表中添加"scrn.o"。在我们可以在main中使用刚才添加的内容之前,我们还需要把 putch, puts, cls, init_video, 和settextcolor的函数原形添加到"system.h"中,注意不要忘记了"extern"关键字,在最后还要加上一个分号: ------------------------------------------------------------------------------ extern void cls(); extern void putch(unsigned char c); extern void puts(unsigned char *str); extern void settextcolor(unsigned char forecolor, unsigned char backcolor); extern void init_video(); Add these to 'system.h' so we can call these from 'main.c' ------------------------------------------------------------------------------- 现在我们可以安全地在main函数中使用我们新定义的屏幕打印函数了。打开"main.c",在里面添加一行调用 init_video()的语句,最后添加一行调用puts的语句,给这个函数传递一个字符串参数,puts("hello world!"),最后,保存修改后的文件,然后双击"build.bat"编译我们的内核,编译的时候调试语法错误。把编译得到的"kernel.bin"拷贝到你的GRUB软盘上,如果一切都顺利的话,我们现在已经拥有了一个能够在黑色屏幕上打印一行“hello,world"白色文字的内核了。
GDT
在386的各种不同的保护方式中,最重要的就是Global Descriptor Table,即GDT。GDT定义了内存特定区域的访问权限。我们可以使用一个GDT中的入口产生一个段操作异常,这个段操作给内核机会去结束一个正在做它不应当做的某些事情的一个进程。大多数现代操作系统使用内存的保护模式,称为分页机制来实现上面说说的保护机制:它具有更多的功能和灵活性。GDT还可以定义内存中的一个部分是可执行的程序还是数据。GDT还可以用来定义Task State Segments(TSS). TSS被用于基于硬件的多任务操作,这里我们不讨论它。注意,TSS并不是实现多任务的唯一途径。
注意GRUB已经安装了一个GDT给你,大使如果我们覆盖了GRUB装入的内存区域,我们将破坏GDT,这被称为"triple fault",简单地说,它将RESET机器。为了避免这个问题的发生,我们需要在内存中建立自己的一个GDT,这个区域我们知道而且可以访问。这样我们就必须建立我们自己的GDT,告诉处理器GDT在哪里,最后以我们新的入口值装入CS,DS,ES,FS和GS。CS寄存器是代码段的寄存器,代码段告诉处理器在GDT中偏移量是多少,在这个位置处的值将设置当前代码的访问权限。DS寄存器是数据段的寄存器,它定义了当前数据的访问权限。ES,FS和GS可以做为DS的替代,对我们来说并不重要。
GDT本身是一个64位入口值组成的列表。这些入口定义了允许访问的区域在内存的起始地址,这个区域的某些限制,以及与入口相关的访问权限。一个通用的规则是,GDT的第一个入口编号为入口0,被称为NULL描述符。任何一个段寄存器不应当设置为0,否则这将引起保护错误,这是处理器的一个特性。一般保护错误和其他类型的异常将被详细解释如下: --------------------------------------------------------------------- Exception # Description Error Code? --------------------------------------------------------------------- 0 Division By Zero Exception No 1 Debug Exception No 2 Non Maskable Interrupt Exception No 3 Breakpoint Exception No 4 Into Detected Overflow Exception No 5 Out of Bounds Exception No 6 Invalid Opcode Exception No 7 No Coprocessor Exception No 8 Double Fault Exception Yes 9 Coprocessor Segment Overrun Exception No 10 Bad TSS Exception Yes 11 Segment Not Present Exception Yes 12 Stack Fault Exception Yes 13 General Protection Fault Exception Yes 14 Page Fault Exception Yes 15 Unknown Interrupt Exception No 16 Coprocessor Fault Exception No 17 Alignment Check Exception (486+) No 18 Machine Check Exception (Pentium/586+) No 19 to 31 Reserved Exceptions No -------------------------------------------------------------------- 每一个GDT入口定义了处理器正在运行的当前段是运行在系统使用的Ring 0还是运行在应用程序的Ring 3级。还有一些别的ring的级别类型,但是都不是太重要。目前主流的操作系统只使用ring 0和ring 3。作为一个基本的规则,当应用程序试图访问系统区或者Ring 0级的数据的时候将产生一个异常。这种保护机制避免了应用程序导致内核崩溃。RING 级别告诉处理器是否允许当前段去执行某些特定的特权指令。某些指令是特权指令,这意味着他们只能够在更高的Ring级别下运行。比如,"sti","cli"就是两个特权指令,分别开中断和关中断。如果一个应用程序被允许使用"cli"或者"sti",则此应用程序就可以停止你的内核的运行。后面的部分中将详细讨论中断的问题。 每一个GDT入口的内部结构定义如下:
7 6 5 4 3 0 -|-------|-------|------| P| DPL |DT | Type | -|-------|-------|------| P - 段是否出现了? (1 = 出现了) DPL - RING级别 (0 to 3) DT - 描述符类型 Type - 哪一种类型?
7 6 5 4 3 0 -|--|---|---|-------------| G|D | 0 | A |SegLen 19:16 | -|--|---|---|-------------|
G - 粒度 (0 = 1byte, 1 = 4kbyte) D - 操作数位数 (0 = 16bit, 1 = 32-bit) 0 - 总是 0 A - 系统使用(总是 0)
在我们的教程中,我们将创建一个只有三个入口的GDT。为什么只有三个呢。在GDT的开始部分我门只需要一个哑元描述符来作为处理器内存保护模式的NULL段。我们需要一个代码段入口,我们还需要一个数据段寄存器的入口。为了告诉处理器我们的新的GDT在什么位置,我们使用汇编操作码“LGDT”。LGDT需要一个指向一个特殊的48位结构的指针作为操作数。由于GDT的限制,这个特殊的48位的结构是由16位的单元(因为需要保护,因此如果我们请求操作一个偏移量不在GDT中的段时,处理器可以立即产生一个一般保护错误)和一个表示GDT本身所在地址的32位单元组成。
我们可以使用一个简单的三个入口的数组来定义我们的GDT。我们要用到的特殊的GDT指针是唯一需要声明的东西。我们把这个指针叫做"gp"。创建一个新的文件,"gdt.c",象前面的章节中所说的那样在"build.bat"中加入一个行,然后使用GCC编译"gdt.c"。再次提醒你在LD需要连接的目标文件列表中加入"gdt.o"以便创建你自己的内核映象。分析如下的代码,这些代码组成了"gdt.c"的第一部分:
---------------------------------------------------------------------------------
#include < system.h >
/* Defines a GDT entry. We say packed, because it prevents the * compiler from doing things that it thinks is best: Prevent * compiler "optimization" by packing */ struct gdt_entry { unsigned short limit_low; unsigned short base_low; unsigned char base_middle; unsigned char access; unsigned char granularity; unsigned char base_high; } __attribute__((packed));
/* Special pointer which includes the limit: The max bytes * taken up by the GDT, minus 1. Again, this NEEDS to be packed */ struct gdt_ptr { unsigned short limit; unsigned int base; } __attribute__((packed));
/* Our GDT, with 3 entries, and finally our special GDT pointer */ struct gdt_entry gdt[3]; struct gdt_ptr gp;
/* This will be a function in start.asm. We use this to properly * reload the new segment registers */ extern void gdt_flush(); Managing your GDT with 'gdt.c' --------------------------------------------------------------------------------------- 你将注意到我们加入了一个并不存在的函数的声明:gdt_flush()。gdt_flush()使用我们前面说过的指针告诉处理器我们新的GDT在哪里。我们需要重新为段寄存器装入新的值,然后执行一个far跳转,重新载入我们新的代码段。分析这段代码,然后把它加到"start.asm"中位于"stublet"无限循环之后的空白区域中: -------------------------------------------------------------------------------- ; This will set up our new segment registers. We need to do ; something special in order to set CS. We do what is called a ; far jump. A jump that includes a segment as well as an offset. ; This is declared in C as 'extern void gdt_flush();' global _gdt_flush ; Allows the C code to link to this extern _gp ; Says that '_gp' is in another file _gdt_flush: lgdt [_gp] ; Load the GDT with our '_gp' which is a special pointer mov ax, 0x10 ; 0x10 is the offset in the GDT to our data segment mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax jmp 0x08:flush2 ; 0x08 is the offset to our code segment: Far jump! flush2: ret ; Returns back to the C code! Add these lines to 'start.asm' -------------------------------------------------------------------------------------- 对于在内存中实际为GDT保留的内存空间来说,这还不够。我们需要将一些值写入每一个GDT入口之中,设置"gp"--GDT指针,然后我门需要调用gdt_flush()执行更新。这里有一个特殊的函数跟在后面,称为"gdt_set_entry()",这个函数简单地使用它的参数完成给定GDT的每一个入口的设置。你必须在"system.h"中为这两个函数添加函数原形(至少我们需要函数"gdt_install"),这样我们就可以在"main.c"中使用这些函数了。分析如下代码,这些代码完成了"gdt.c"剩余的部分: -------------------------------------------------------------------------------------- /* Setup a descriptor in the Global Descriptor Table */ void gdt_set_gate(int num, unsigned long base, unsigned long limit, unsigned char access, unsigned char gran) { /* Setup the descriptor base address */ gdt[num].base_low = (base & 0xFFFF); gdt[num].base_middle = (base >> 16) & 0xFF; gdt[num].base_high = (base >> 24) & 0xFF;
/* Setup the descriptor limits */ gdt[num].limit_low = (limit & 0xFFFF); gdt[num].granularity = ((limit >> 16) & 0x0F);
/* Finally, set up the granularity and access flags */ gdt[num].granularity |= (gran & 0xF0); gdt[num].access = access; }
/* Should be called by main. This will setup the special GDT * pointer, set up the first 3 entries in our GDT, and then * finally call gdt_flush() in our assembler file in order * to tell the processor where the new GDT is and update the * new segment registers */ void gdt_install() { /* Setup the GDT pointer and limit */ gp.limit = (sizeof(struct gdt_entry) * 3) - 1; gp.base = &gdt;
/* Our NULL descriptor */ gdt_set_gate(0, 0, 0, 0, 0);
/* The second entry is our Code Segment. The base address * is 0, the limit is 4GBytes, it uses 4KByte granularity, * uses 32-bit opcodes, and is a Code Segment descriptor. * Please check the table above in the tutorial in order * to see exactly what each value means */ gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF);
/* The third entry is our Data Segment. It's EXACTLY the * same as our code segment, but the descriptor type in * this entry's access byte says it's a Data Segment */ gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF);
/* Flush out the old GDT and install the new changes! */ gdt_flush(); }
Add this to 'gdt.c'. It does some of the dirty work relating to the GDT! Don't forget the prototypes in 'system.h'! --------------------------------------------------------------------------------------------- 现在我们的GDT载入程序已经就绪了,我们把它编译和连接进我们的内核,为了实际上完成一些工作我们需要调用gdt_install()。打开"main.c"并且在你的main()函数的第一行的位置上加入"gdt_install();"。GDT是需要首先被初始化的东西之一,因为它是非常重要的。现在你可以编译,连接然后把你的内核放到软盘上进行测试了。你将在屏幕上看到一些变化,这是一个内部变化。我们来看中断描述符表(IDT)!
IDT 中断描述符表,或者叫IDT,被用来告诉处理器应当调用哪一个中断服务程序(ISR)去处理异常或者INT 软中断。当一个设备已经完成了一饿请求并需要一个服务的时候IDT也被称为中断请求。异常和ISR将在本教程的下一部分进行详细的解释。 每一个IDT入口类似于GDT入口。他们都有一个基地址,都有一个访问标志,而且都是64位长的。二者最主要的不同在于这64位中的域的含义,也就是每一位的含义。在IDT中,在描述符中指定的基地址实际上是当中断被调用的时候处理器应当调用的中断服务程序的地址。IDT入口是没有限制的,然而你需要为它指定一个段。这个段必须与给定的ISR是同一个段。这就允许运行在不同ring级别的处理器通过中断将控制权交给内核。(比如说当一个应用程序运行的时候,这个时候处理器是运行在ring 3的,当一个中断在这个时候发生后,根据我们前面所说的,这个中断具有一个IDT入口,在入口中指定了ISR所在的段,并根据IDT入口的其他地址信息执行ISR,这就可以将控制权利交给内核)(这段话翻译的可能有问题,把原文拷贝下来:This allows the processor to give control to the kernel through an interrupt that has occured when the processor is in a different ring (like when an application is running).
IDT入口的访问标志与GDT的入口访问标志是一样的。在入口中有一个域指出描述符实际上出现(present)还是没出现。有一个域是设置描述符特权级的(DPL),这个域指出哪一个ring级是更高的级从而被允许使用给定的中断。IDT和GDT主要的不同之处就在于剩余的访问标志定义。最低5位的访问权限字节总是设置成01110,用十进制来表示就是14。关于IDT入口的访问权限字节,我们给出一个图形表示如下: --|---------|---------------| 7 | 6 5|4 0| --|---------|---------------| P | DPL |总是 01110 (14)| --|---------|---------------|
P - 段是否 present? (1 = Yes) DPL - 哪一个ring级(0 to 3)
在你的内核目录中创建一个名叫"idt.c"的文件。编辑你的"build.bat"文件,在其中加入另外一行以使得GCC可以编译"idt.c"。最后在LD需要连接在一起的目标文件列表总加入"idt.o"。"idt.c"将声明一个结构,这个结构定义了每一个IDT入口,载入IDT需要一个特殊IDT指针结构(类似于载如GDT,但是工作量少的多),然后声明一个256个IDT入口的数组:这将成为我们的IDT
------------------------------------------------------------------------------ #include < system.h >
/* Defines an IDT entry */ struct idt_entry { unsigned short base_lo; unsigned short sel; /* Our kernel segment goes here! */ unsigned char always0; /* This will ALWAYS be set to 0! */ unsigned char flags; /* Set using the above table! */ unsigned short base_hi; } __attribute__((packed));
struct idt_ptr { unsigned short limit; unsigned int base; } __attribute__((packed));
/* Declare an IDT of 256 entries. Although we will only use the * first 32 entries in this tutorial, the rest exists as a bit * of a trap. If any undefined IDT entry is hit, it normally * will cause an "Unhandled Interrupt" exception. Any descriptor * for which the 'presence' bit is cleared (0) will generate an * "Unhandled Interrupt" exception */ struct idt_entry idt[256]; struct idt_ptr idtp;
/* This exists in 'start.asm', and is used to load our IDT */ extern void idt_load(); This is the beginning half of 'idt.c'. Defines the vital data structures! --------------------------------------------------------------------------------------------- 类似于"gdt.c",你将注意到文件里声明了一个实际上是在别的文件中的函数。象"gdt_flush"一样"idt_load"是用汇编语言写的。"idt_load"其实就是一个使用我们随后要在"idt_install"中创建的特殊的IDT指针对"lidt"汇编操作进行调用的函数。打开"start.asm"然后在"_gdt_flush"中的"ret"指令后面加入如下行: ---------------------------------------------------------------------------------------------
; Loads the IDT defined in '_idtp' into the processor. ; This is declared in C as 'extern void idt_load();' global _idt_load extern _idtp _idt_load: lidt [_idtp] ret Add this to 'start.asm' ---------------------------------------------------------------------------------------------- 设置每一个IDT入口比设置GDT入口要简单的多。我们创建一个"idt_set_gate"函数,这个函数接受IDT入口号,IRS的基地址,我们的内核代码段地址和访问标志作为参数。我们创建一个"idt_install"函数,这个函数设置我们特殊的IDT指针并且将我们的IDT设置为一个已知的缺省状态。最后,我们将通过调用"idt_load"载入IDT。注意,你可以在IDT载入后的任何时刻加入你的ISR到你的IDT。后面我们再详细说IRS的问题。 ----------------------------------------------------------------------------------------------- /* Use this function to set an entry in the IDT. Alot simpler * than twiddling with the GDT */ void idt_set_gate(unsigned char num, unsigned long base, unsigned short sel, unsigned char flags) { /* We'll leave you to try and code this function: take the * argument 'base' and split it up into a high and low 16-bits, * storing them in idt[num].base_hi and base_lo. The rest of the * fields that you must set in idt[num] are fairly self- * explanatory when it comes to setup */ }
/* Installs the IDT */ void idt_install() { /* Sets the special IDT pointer up, just like in 'gdt.c' */ idtp.limit = (sizeof (struct idt_entry) * 256) - 1; idtp.base = &idt;
/* Clear out the entire IDT, initializing it to zeros */ memset(&idt, 0, sizeof(struct idt_entry) * 256);
/* Add any new ISRs to the IDT here using idt_set_gate */
/* Points the processor's internal register to the new IDT */ idt_load(); } The rest of 'idt.c'. Try to figure out 'idt_set_gate'. It's easy! -------------------------------------------------------------------------------------------- 最后,确保已经将"idt_set_gate"和"idt_install"的函数原形加入到了"system.h"中。记住我们需要从其他文件中调用这三个函数,比如"main.c"。在"main()"中"gdt_install"调用之后调用"idt_install"。你应该能够毫无问题地编译你的内核了。花些时间在你的新的内核上做些实验。如果你试图做一些非法的操作比如除0操作,你将发现你的机器将RESET。我们可以通过在我们新的IDT中安装新的ISR捕捉这些异常,并处理它们。 如果你正在坚持书写"idt_set_gate",这些代码可以做为一个参考方案: ------------------------------------------------------------------------------------------- /* bkerndev - Bran's Kernel Development Tutorial * By: Brandon F. (friesenb@gmail.com) * Desc: Interrupt Descriptor Table management * * Notes: No warranty expressed or implied. Use at own risk. */ #include <system.h>
/* Defines an IDT entry */ struct idt_entry { unsigned short base_lo; unsigned short sel; unsigned char always0; unsigned char flags; unsigned short base_hi; } __attribute__((packed));
struct idt_ptr { unsigned short limit; unsigned int base; } __attribute__((packed));
/* Declare an IDT of 256 entries. Although we will only use the * first 32 entries in this tutorial, the rest exists as a bit * of a trap. If any undefined IDT entry is hit, it normally * will cause an "Unhandled Interrupt" exception. Any descriptor * for which the 'presence' bit is cleared (0) will generate an * "Unhandled Interrupt" exception */ struct idt_entry idt[256]; struct idt_ptr idtp;
/* This exists in 'start.asm', and is used to load our IDT */ extern void idt_load();
/* Use this function to set an entry in the IDT. Alot simpler * than twiddling with the GDT ;) */ void idt_set_gate(unsigned char num, unsigned long base, unsigned short sel, unsigned char flags) { /* The interrupt routine's base address */ idt[num].base_lo = (base & 0xFFFF); idt[num].base_hi = (base >> 16) & 0xFFFF;
/* The segment or 'selector' that this IDT entry will use * is set here, along with any access flags */ idt[num].sel = sel; idt[num].always0 = 0; idt[num].flags = flags; }
/* Installs the IDT */ void idt_install() { /* Sets the special IDT pointer up, just like in 'gdt.c' */ idtp.limit = (sizeof (struct idt_entry) * 256) - 1; idtp.base = &idt;
/* Clear out the entire IDT, initializing it to zeros */ memset(&idt, 0, sizeof(struct idt_entry) * 256);
/* Add any new ISRs to the IDT here using idt_set_gate */
/* Points the processor's internal register to the new IDT */ idt_load(); } --------------------------------------------------------------------------------------------------- ISR(中断服务程序) 中断服务程序,或者叫ISR,被用来保存当前处理器的状态和设置在内核的C级别的中断处理程序调用之前内核保护模式需要的合适的段寄存器的值。这可以使用15到20行汇编语言程序全部解决,包括调用我们的C语言的中断处理程序。我们只需要在当前的IDT入口中把中断指向相应的ISR以处理特定的异常。
异常是处理器在不能正常执行程序的时候遇到的特殊情况。这些情况可是是如除0操作这样的情况。操作的结果是未知的或者非实数。所以处理器将抛出一个延长以便内核可以停止处理器的执行避免发生某些问题。如果处理器发现一个程序试图访问它不应该访问的一段内存,它将产生有个一般性的保护错误。当你设置分页机制的时候,处理器引起一个PAGE FAULT,页面错误,但是这是可以恢复的:你可以将一个内存页映射到一个错误的地址,但是那是另外一个教程要说的问题。
IDT中的前32个入口对应于处理器可能产生的异常,因此必须被处理。一些异常将把其他值推入堆栈:错误代码被指定给这些异常: --------------------------------------------------------------------------- Exception # Description Error Code? --------------------------------------------------------------------------- 0 Division By Zero Exception No 1 Debug Exception No 2 Non Maskable Interrupt Exception No 3 Breakpoint Exception No 4 Into Detected Overflow Exception No 5 Out of Bounds Exception No 6 Invalid Opcode Exception No 7 No Coprocessor Exception No 8 Double Fault Exception Yes 9 Coprocessor Segment Overrun Exception No 10 Bad TSS Exception Yes 11 Segment Not Present Exception Yes 12 Stack Fault Exception Yes 13 General Protection Fault Exception Yes 14 Page Fault Exception Yes 15 Unknown Interrupt Exception No 16 Coprocessor Fault Exception No 17 Alignment Check Exception (486+) No 18 Machine Check Exception (Pentium/586+) No 19 to 31 Reserved Exceptions No -------------------------------------------------------------------------- 象前面说的那样,有些异常将错误代码推入堆栈中。为了减少复杂性,我们为那些没有推入错误代码到堆栈中的ISR推入堆栈一个哑元错误代码0。(也就是说我们为没有错误代码的一样推入一个0错误代码)这将保持一个统一的堆栈桢。为了追踪到底产生的是哪一个异常,我们也将中断号推入到堆栈中。我们使用汇编操作"cli"关闭中断,阻止中断请求,这些中断请求可能在我们的内核中引起冲突。为了在内核中节省空间使得能够输出一个更小的二进制文件,我们通过跳转到一个共用的"isr_common_stub"从而获得每一个ISR。"isr_common_stub"将把处理器状态包春到堆栈中,把当前堆栈的地址退到堆栈中(给你一个C中断处理程序的堆栈),调用我们的"fault_handler"C函数,最后恢复堆栈。把这些代码加到"start.asm"中: --------------------------------------------------------------------------------------- ; In just a few pages in this tutorial, we will add our Interrupt ; Service Routines (ISRs) right here! global _isr0 global _isr1 global _isr2 ... ; Fill in the rest here! global _isr30 global _isr31
; 0: Divide By Zero Exception _isr0: cli push byte 0 ; A normal ISR stub that pops a dummy error code to keep a ; uniform stack frame push byte 0 jmp isr_common_stub
; 1: Debug Exception _isr1: cli push byte 0 push byte 1 jmp isr_common_stub ... ; Fill in from 2 to 7 here!
; 8: Double Fault Exception (With Error Code!) _isr8: cli push byte 8 ; Note that we DON'T push a value on the stack in this one! ; It pushes one already! Use this type of stub for exceptions ; that pop error codes! jmp isr_common_stub
... ; You should fill in from _isr9 to _isr31 here. Remember to ; use the correct stubs to handle error codes and push dummies!
; We call a C function in here. We need to let the assembler know ; that '_fault_handler' exists in another file extern _fault_handler
; This is our common ISR stub. It saves the processor state, sets ; up for kernel mode segments, calls the C-level fault handler, ; and finally restores the stack frame. isr_common_stub: pusha push ds push es push fs push gs mov ax, 0x10 ; Load the Kernel Data Segment descriptor! mov ds, ax mov es, ax mov fs, ax mov gs, ax mov eax, esp ; Push us the stack push eax mov eax, _fault_handler call eax ; A special call, preserves the 'eip' register pop eax pop gs pop fs pop es pop ds popa add esp, 8 ; Cleans up the pushed error code and pushed ISR number iret ; pops 5 things at once: CS, EIP, EFLAGS, SS, and ESP! Add this to 'start.asm' in the spot we indicated in "The Basic Kernel" ------------------------------------------------------------------------------------------------ 创建你自己的一个新的文件,名叫"isrs.c"。再次把适当的命令行到"build.bat"中以便于使GCC编译该新创建的文件。把文件"isrs."加到LD的文件列表中,以便使LD能够把新的文件连接进内核。"isrs.c"中先声明了标准的#include行,声明了"start.asm"中每一个ISRs的原型,把IDT的入口指向正确的ISR,最后,创建一个用C编写的中断处理程序去处理我们定义的所有异常。 ------------------------------------------------------------------------------------------------ #include < system.h >
/* These are function prototypes for all of the exception * handlers: The first 32 entries in the IDT are reserved * by Intel, and are designed to service exceptions! */ extern void isr0(); extern void isr1(); extern void isr2();
... /* Fill in the rest of the ISR prototypes here */
extern void isr29(); extern void isr30(); extern void isr31();
/* This is a very repetitive function... it's not hard, it's * just annoying. As you can see, we set the first 32 entries * in the IDT to the first 32 ISRs. We can't use a for loop * for this, because there is no way to get the function names * that correspond to that given entry. We set the access * flags to 0x8E. This means that the entry is present, is * running in ring 0 (kernel level), and has the lower 5 bits * set to the required '14', which is represented by 'E' in * hex. */ void isrs_install() { idt_set_gate(0, (unsigned)isr0, 0x08, 0x8E); idt_set_gate(1, (unsigned)isr1, 0x08, 0x8E); idt_set_gate(2, (unsigned)isr2, 0x08, 0x8E); idt_set_gate(3, (unsigned)isr3, 0x08, 0x8E);
... /* Fill in the rest of these ISRs here */
idt_set_gate(30, (unsigned)isr30, 0x08, 0x8E); idt_set_gate(31, (unsigned)isr31, 0x08, 0x8E); }
/* This is a simple string array. It contains the message that * corresponds to each and every exception. We get the correct * message by accessing like: * exception_message[interrupt_number] */ unsigned char *exception_messages[] = { "Division By Zero", "Debug", "Non Maskable Interrupt", ... /* Fill in the rest here from our Exceptions table */ "Reserved", "Reserved" };
/* All of our Exception handling Interrupt Service Routines will * point to this function. This will tell us what exception has * happened! Right now, we simply halt the system by hitting an * endless loop. All ISRs disable interrupts while they are being * serviced as a 'locking' mechanism to prevent an IRQ from * happening and messing up kernel data structures */ void fault_handler(struct regs *r) { /* Is this a fault whose number is from 0 to 31? */ if (r->int_no < 32) { /* Display the description for the Exception that occurred. * In this tutorial, we will simply halt the system using an * infinite loop */ puts(exception_messages[r->int_no]); puts(" Exception. System Halted!\n" ; for (;;); } } The contents of 'isrs.c' ----------------------------------------------------------------------------------------------- 等等,在"fault_handler"的参数中有一个新的结构:结构"reg"。在这里,"reg"是一种为C代码显示堆栈桢是什么样子的一种途径。记住,在"start.asm"中我们把一个堆栈指针指针推入了堆栈:这就是为什么我们为什么能够找到任何错误代码和从中断处理程序自身中找到中断号的原因。这个设计允许我们使用响动的C的中断处理程序处理同的ISR,而且可以知道哪一种中断或者异常发生了。 ----------------------------------------------------------------------------------------------- /* This defines what the stack looks like after an ISR was running */ struct regs { unsigned int gs, fs, es, ds; /* pushed the segs last */ unsigned int edi, esi, ebp, esp, ebx, edx, ecx, eax; /* pushed by 'pusha' */ unsigned int int_no, err_code; /* our 'push byte #' and ecodes do this */ unsigned int eip, cs, eflags, useresp, ss; /* pushed by the processor automatically */ }; Defines a stack frame pointer argument. Add this to 'system.h' ------------------------------------------------------------------------------------------------- 打开"system.h",在其中加入"struct"的定义还有"isrs_install"的函数原型声明,以便于我们能够从"main.c"中调用他们。最后,当我们装入我们新的IDT之后,从"main.c"中调用"isrs_install",。现在就测试一下我们内核中的的异常处理程序是个好注意。 可选项:在"main"中,加入一些测试代码,这些代码将一个数字除以0。当处理器碰到这个代码的时候,处理器将产生一个“除零”异常,你将看到在屏幕上看到它。当你测试这些代码而且异常处理机智正常运行的情况下,你可以删除那些用于产生异常的代码("putch(myvar/0)"这一行),或者其他你所写的测试程序。 最终完整的"start.s"如下: ------------------------------------------------------------------------------------------------- ; bkerndev - Bran's Kernel Development Tutorial ; By: Brandon F. (friesenb@gmail.com) ; Desc: Kernel entry point, stack, and Interrupt Service Routines. ; ; Notes: No warranty expressed or implied. Use at own risk. ; ; This is the kernel's entry point. We could either call main here, ; or we can use this to setup the stack or other nice stuff, like ; perhaps setting up the GDT and segments. Please note that interrupts ; are disabled at this point: More on interrupts later! [BITS 32] global start start: mov esp, _sys_stack ; This points the stack to our new stack area jmp stublet
; This part MUST be 4byte aligned, so we solve that issue using 'ALIGN 4' ALIGN 4 mboot: ; Multiboot macros to make a few lines later more readable 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
; This is the GRUB Multiboot header. A boot signature dd MULTIBOOT_HEADER_MAGIC dd MULTIBOOT_HEADER_FLAGS dd MULTIBOOT_CHECKSUM ; AOUT kludge - must be physical addresses. Make a note of these: ; The linker script fills in the data for these ones! dd mboot dd code dd bss dd end dd start
; This is an endless loop here. Make a note of this: Later on, we ; will insert an 'extern _main', followed by 'call _main', right ; before the 'jmp $'. stublet: extern _main call _main jmp $
; This will set up our new segment registers. We need to do ; something special in order to set CS. We do what is called a ; far jump. A jump that includes a segment as well as an offset. ; This is declared in C as 'extern void gdt_flush();' global _gdt_flush extern _gp _gdt_flush: lgdt [_gp] mov ax, 0x10 mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax jmp 0x08:flush2 flush2: ret
; Loads the IDT defined in '_idtp' into the processor. ; This is declared in C as 'extern void idt_load();' global _idt_load extern _idtp _idt_load: lidt [_idtp] ret
; In just a few pages in this tutorial, we will add our Interrupt ; Service Routines (ISRs) right here! global _isr0 global _isr1 global _isr2 global _isr3 global _isr4 global _isr5 global _isr6 global _isr7 global _isr8 global _isr9 global _isr10 global _isr11 global _isr12 global _isr13 global _isr14 global _isr15 global _isr16 global _isr17 global _isr18 global _isr19 global _isr20 global _isr21 global _isr22 global _isr23 global _isr24 global _isr25 global _isr26 global _isr27 global _isr28 global _isr29 global _isr30 global _isr31
; 0: Divide By Zero Exception _isr0: cli push byte 0 push byte 0 jmp isr_common_stub
; 1: Debug Exception _isr1: cli push byte 0 push byte 1 jmp isr_common_stub
; 2: Non Maskable Interrupt Exception _isr2: cli push byte 0 push byte 2 jmp isr_common_stub
; 3: Int 3 Exception _isr3: cli push byte 0 push byte 3 jmp isr_common_stub
; 4: INTO Exception _isr4: cli push byte 0 push byte 4 jmp isr_common_stub
; 5: Out of Bounds Exception _isr5: cli push byte 0 push byte 5 jmp isr_common_stub
; 6: Invalid Opcode Exception _isr6: cli push byte 0 push byte 6 jmp isr_common_stub
; 7: Coprocessor Not Available Exception _isr7: cli push byte 0 push byte 7 jmp isr_common_stub
; 8: Double Fault Exception (With Error Code!) _isr8: cli push byte 8 jmp isr_common_stub
; 9: Coprocessor Segment Overrun Exception _isr9: cli push byte 0 push byte 9 jmp isr_common_stub
; 10: Bad TSS Exception (With Error Code!) _isr10: cli push byte 10 jmp isr_common_stub
; 11: Segment Not Present Exception (With Error Code!) _isr11: cli push byte 11 jmp isr_common_stub
; 12: Stack Fault Exception (With Error Code!) _isr12: cli push byte 12 jmp isr_common_stub
; 13: General Protection Fault Exception (With Error Code!) _isr13: cli push byte 13 jmp isr_common_stub
; 14: Page Fault Exception (With Error Code!) _isr14: cli push byte 14 jmp isr_common_stub
; 15: Reserved Exception _isr15: cli push byte 0 push byte 15 jmp isr_common_stub
; 16: Floating Point Exception _isr16: cli push byte 0 push byte 16 jmp isr_common_stub
; 17: Alignment Check Exception _isr17: cli push byte 0 push byte 17 jmp isr_common_stub
; 18: Machine Check Exception _isr18: cli push byte 0 push byte 18 jmp isr_common_stub
; 19: Reserved _isr19: cli push byte 0 push byte 19 jmp isr_common_stub
; 20: Reserved _isr20: cli push byte 0 push byte 20 jmp isr_common_stub
; 21: Reserved _isr21: cli push byte 0 push byte 21 jmp isr_common_stub
; 22: Reserved _isr22: cli push byte 0 push byte 22 jmp isr_common_stub
; 23: Reserved _isr23: cli push byte 0 push byte 23 jmp isr_common_stub
; 24: Reserved _isr24: cli push byte 0 push byte 24 jmp isr_common_stub
; 25: Reserved _isr25: cli push byte 0 push byte 25 jmp isr_common_stub
; 26: Reserved _isr26: cli push byte 0 push byte 26 jmp isr_common_stub
; 27: Reserved _isr27: cli push byte 0 push byte 27 jmp isr_common_stub
; 28: Reserved _isr28: cli push byte 0 push byte 28 jmp isr_common_stub
; 29: Reserved _isr29: cli push byte 0 push byte 29 jmp isr_common_stub
; 30: Reserved _isr30: cli push byte 0 push byte 30 jmp isr_common_stub
; 31: Reserved _isr31: cli push byte 0 push byte 31 jmp isr_common_stub
; We call a C function in here. We need to let the assembler know ; that '_fault_handler' exists in another file extern _fault_handler
; This is our common ISR stub. It saves the processor state, sets ; up for kernel mode segments, calls the C-level fault handler, ; and finally restores the stack frame. isr_common_stub: pusha push ds push es push fs push gs mov ax, 0x10 mov ds, ax mov es, ax mov fs, ax mov gs, ax mov eax, esp push eax mov eax, _fault_handler call eax pop eax pop gs pop fs pop es pop ds popa add esp, 8 iret
global _irq0 global _irq1 global _irq2 global _irq3 global _irq4 global _irq5 global _irq6 global _irq7 global _irq8 global _irq9 global _irq10 global _irq11 global _irq12 global _irq13 global _irq14 global _irq15
; 32: IRQ0 _irq0: cli push byte 0 push byte 32 jmp irq_common_stub
; 33: IRQ1 _irq1: cli push byte 0 push byte 33 jmp irq_common_stub
; 34: IRQ2 _irq2: cli push byte 0 push byte 34 jmp irq_common_stub
; 35: IRQ3 _irq3: cli push byte 0 push byte 35 jmp irq_common_stub
; 36: IRQ4 _irq4: cli push byte 0 push byte 36 jmp irq_common_stub
; 37: IRQ5 _irq5: cli push byte 0 push byte 37 jmp irq_common_stub
; 38: IRQ6 _irq6: cli push byte 0 push byte 38 jmp irq_common_stub
; 39: IRQ7 _irq7: cli push byte 0 push byte 39 jmp irq_common_stub
; 40: IRQ8 _irq8: cli push byte 0 push byte 40 jmp irq_common_stub
; 41: IRQ9 _irq9: cli push byte 0 push byte 41 jmp irq_common_stub
; 42: IRQ10 _irq10: cli push byte 0 push byte 42 jmp irq_common_stub
; 43: IRQ11 _irq11: cli push byte 0 push byte 43 jmp irq_common_stub
; 44: IRQ12 _irq12: cli push byte 0 push byte 44 jmp irq_common_stub
; 45: IRQ13 _irq13: cli push byte 0 push byte 45 jmp irq_common_stub
; 46: IRQ14 _irq14: cli push byte 0 push byte 46 jmp irq_common_stub
; 47: IRQ15 _irq15: cli push byte 0 push byte 47 jmp irq_common_stub
extern _irq_handler
irq_common_stub: pusha push ds push es push fs push gs
mov ax, 0x10 mov ds, ax mov es, ax mov fs, ax mov gs, ax mov eax, esp
push eax mov eax, _irq_handler call eax pop eax
pop gs pop fs pop es pop ds popa add esp, 8 iret
; Here is the definition of our BSS section. Right now, we'll use ; it just to store the stack. Remember that a stack actually grows ; downwards, so we declare the size of the data before declaring ; the identifier '_sys_stack' SECTION .bss resb 8192 ; This reserves 8KBytes of memory here _sys_stack:
------------------------------------------------------------------------------------------------- 最终完整的"isrs.c"如下: ------------------------------------------------------------------------------------------------- /* bkerndev - Bran's Kernel Development Tutorial * By: Brandon F. (friesenb@gmail.com) * Desc: Interrupt Service Routines installer and exceptions * * Notes: No warranty expressed or implied. Use at own risk. */ #include <system.h>
/* These are function prototypes for all of the exception * handlers: The first 32 entries in the IDT are reserved * by Intel, and are designed to service exceptions! */ extern void isr0(); extern void isr1(); extern void isr2(); extern void isr3(); extern void isr4(); extern void isr5(); extern void isr6(); extern void isr7(); extern void isr8(); extern void isr9(); extern void isr10(); extern void isr11(); extern void isr12(); extern void isr13(); extern void isr14(); extern void isr15(); extern void isr16(); extern void isr17(); extern void isr18(); extern void isr19(); extern void isr20(); extern void isr21(); extern void isr22(); extern void isr23(); extern void isr24(); extern void isr25(); extern void isr26(); extern void isr27(); extern void isr28(); extern void isr29(); extern void isr30(); extern void isr31();
/* This is a very repetitive function... it's not hard, it's * just annoying. As you can see, we set the first 32 entries * in the IDT to the first 32 ISRs. We can't use a for loop * for this, because there is no way to get the function names * that correspond to that given entry. We set the access * flags to 0x8E. This means that the entry is present, is * running in ring 0 (kernel level), and has the lower 5 bits * set to the required '14', which is represented by 'E' in * hex. */ void isrs_install() { idt_set_gate(0, (unsigned)isr0, 0x08, 0x8E); idt_set_gate(1, (unsigned)isr1, 0x08, 0x8E); idt_set_gate(2, (unsigned)isr2, 0x08, 0x8E); idt_set_gate(3, (unsigned)isr3, 0x08, 0x8E); idt_set_gate(4, (unsigned)isr4, 0x08, 0x8E); idt_set_gate(5, (unsigned)isr5, 0x08, 0x8E); idt_set_gate(6, (unsigned)isr6, 0x08, 0x8E); idt_set_gate(7, (unsigned)isr7, 0x08, 0x8E);
idt_set_gate(8, (unsigned)isr8, 0x08, 0x8E); idt_set_gate(9, (unsigned)isr9, 0x08, 0x8E); idt_set_gate(10, (unsigned)isr10, 0x08, 0x8E); idt_set_gate(11, (unsigned)isr11, 0x08, 0x8E); idt_set_gate(12, (unsigned)isr12, 0x08, 0x8E); idt_set_gate(13, (unsigned)isr13, 0x08, 0x8E); idt_set_gate(14, (unsigned)isr14, 0x08, 0x8E); idt_set_gate(15, (unsigned)isr15, 0x08, 0x8E);
idt_set_gate(16, (unsigned)isr16, 0x08, 0x8E); idt_set_gate(17, (unsigned)isr17, 0x08, 0x8E); idt_set_gate(18, (unsigned)isr18, 0x08, 0x8E); idt_set_gate(19, (unsigned)isr19, 0x08, 0x8E); idt_set_gate(20, (unsigned)isr20, 0x08, 0x8E); idt_set_gate(21, (unsigned)isr21, 0x08, 0x8E); idt_set_gate(22, (unsigned)isr22, 0x08, 0x8E); idt_set_gate(23, (unsigned)isr23, 0x08, 0x8E);
idt_set_gate(24, (unsigned)isr24, 0x08, 0x8E); idt_set_gate(25, (unsigned)isr25, 0x08, 0x8E); idt_set_gate(26, (unsigned)isr26, 0x08, 0x8E); idt_set_gate(27, (unsigned)isr27, 0x08, 0x8E); idt_set_gate(28, (unsigned)isr28, 0x08, 0x8E); idt_set_gate(29, (unsigned)isr29, 0x08, 0x8E); idt_set_gate(30, (unsigned)isr30, 0x08, 0x8E); idt_set_gate(31, (unsigned)isr31, 0x08, 0x8E); }
/* This is a simple string array. It contains the message that * corresponds to each and every exception. We get the correct * message by accessing like: * exception_message[interrupt_number] */ unsigned char *exception_messages[] = { "Division By Zero", "Debug", "Non Maskable Interrupt", "Breakpoint", "Into Detected Overflow", "Out of Bounds", "Invalid Opcode", "No Coprocessor",
"Double Fault", "Coprocessor Segment Overrun", "Bad TSS", "Segment Not Present", "Stack Fault", "General Protection Fault", "Page Fault", "Unknown Interrupt",
"Coprocessor Fault", "Alignment Check", "Machine Check", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved",
"Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved", "Reserved" };
/* All of our Exception handling Interrupt Service Routines will * point to this function. This will tell us what exception has * happened! Right now, we simply halt the system by hitting an * endless loop. All ISRs disable interrupts while they are being * serviced as a 'locking' mechanism to prevent an IRQ from * happening and messing up kernel data structures */ void fault_handler(struct regs *r) { if (r->int_no < 32) { puts(exception_messages[r->int_no]); puts(" Exception. System Halted!\n" ; for (;;); } } ------------------------------------------------------------------------------------------------------ IRQ和PIC 中断请求或者叫IRQ是由硬件产生的中断。比如说,有些设备在已经准备好数据用于读取的时候产生一个中断,或者是当完成了一个诸如将缓冲写入磁盘这样的命令的时候产生一个中断。也可以这么说,无论何时当设备想要得到CPU的注意的时候它就发生一个中断请求。中断请求可以由网卡,声卡,鼠标,键盘,串口等每一种设备产生。 任何IBM PC/AT兼容机(286或以其上的机器)都具有两个心片用于管理IRQ。这两个心片就是常说的可编程中断控制器或者叫PIC。这个PIC也可以以它的名称来叫它“8259”。两个8259中一个是主IRQ控制器另外一个是从IRQ控制器。从控制器连接到主控制器的IRQ2上,住控制器直接连接到处理器上,直接产生信号。每个PIC可以处理8个中断请求,主控制器处理IRQ0-IRQ7,从控制器处理IRQ8-IRQ15。注意从控制器是连接到主控制器的IRQ2上的,因此每当IRQ8-IRQ15产生的时候,IRQ2也同时产生。 当一个设备发出IRQ请求信号的时候,中断就产生了,CPU暂停它现在正在做的任何事情并调用相应的中断处理程序。然后CPU就执行任何需要的动作(比如从键盘读取输入等),然后它必须告诉发出IRQ请求信号的PIC CPU已经完成了所请求的程序。CPU是通过在PIC的命令寄存器中写入0X20H来告诉PIC中断处理已经完成。主PIC的命令寄存器在I/O口0X20H,从PIC的命令寄存器在I/O口0XA0H。 在我们写我们自己的IRQ管理代码之前,我们需要知道IRQ0-IRQ7一般是在映射到IDT的入口8到入口15的。IRQ8-IRQ15是被映射到IDT入口0X70到0X78的。如果你还记得教程的前面的一些章节,IDT入口0到31是为异常保留的。幸运的是,中断控制器是可编程的,你可以改变IRQ的所映射的IDT入口。在这个教程中,我们将把IRQ0到IRQ15映射到IDT入口的32到47。在开始之前,我们必须在"start.asm"中加入一些ISR以便于处理我们的中断。 --------------------------------------------------------------------------------------------------- global _irq0 ... ; You complete the rest! global _irq15
; 32: IRQ0 _irq0: cli push byte 0 ; Note that these don't push an error code on the stack: ; We need to push a dummy error code push byte 32 jmp irq_common_stub
... ; You need to fill in the rest!
; 47: IRQ15 _irq15: cli push byte 0 push byte 47 jmp irq_common_stub
extern _irq_handler
; This is a stub that we have created for IRQ based ISRs. This calls ; '_irq_handler' in our C code. We need to create this in an 'irq.c' irq_common_stub: pusha push ds push es push fs push gs mov ax, 0x10 mov ds, ax mov es, ax mov fs, ax mov gs, ax mov eax, esp push eax mov eax, _irq_handler call eax pop eax pop gs pop fs pop es pop ds popa add esp, 8 iret Add this chunk of code to 'start.asm' ----------------------------------------------------------------------------------------------------- 象教程的每一个部分那样,我们需要创建一个新的文件名叫"irq.c"。编辑"build.bat"在其中加入适当的行使得GCC编译源代码并且要记得在LD的列表文件中加入新的目标文件,以便把新的功能加入到我们的内核中去。 ----------------------------------------------------------------------------------------------------- include < system.h >
/* These are own ISRs that point to our special IRQ handler * instead of the regular 'fault_handler' function */ extern void irq0(); ... /* Add the rest of the entries here to complete the declarations */ extern void irq15();
/* This array is actually an array of function pointers. We use * this to handle custom IRQ handlers for a given IRQ */ void *irq_routines[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
/* This installs a custom IRQ handler for the given IRQ */ void irq_install_handler(int irq, void (*handler)(struct regs *r)) { irq_routines[irq] = handler; }
/* This clears the handler for a given IRQ */ void irq_uninstall_handler(int irq) { irq_routines[irq] = 0; }
/* Normally, IRQs 0 to 7 are mapped to entries 8 to 15. This * is a problem in protected mode, because IDT entry 8 is a * Double Fault! Without remapping, every time IRQ0 fires, * you get a Double Fault Exception, which is NOT actually * what's happening. We send commands to the Programmable * Interrupt Controller (PICs - also called the 8259's) in * order to make IRQ0 to 15 be remapped to IDT entries 32 to * 47 */ void irq_remap(void) { outportb(0x20, 0x11); outportb(0xA0, 0x11); outportb(0x21, 0x20); outportb(0xA1, 0x28); outportb(0x21, 0x04); outportb(0xA1, 0x02); outportb(0x21, 0x01); outportb(0xA1, 0x01); outportb(0x21, 0x0); outportb(0xA1, 0x0); }
/* We first remap the interrupt controllers, and then we install * the appropriate ISRs to the correct entries in the IDT. This * is just like installing the exception handlers */ void irq_install() { irq_remap();
idt_set_gate(32, (unsigned)irq0, 0x08, 0x8E); ... /* You need to add the rest! */ idt_set_gate(47, (unsigned)irq15, 0x08, 0x8E); }
/* Each of the IRQ ISRs point to this function, rather than * the 'fault_handler' in 'isrs.c'. The IRQ Controllers need * to be told when you are done servicing them, so you need * to send them an "End of Interrupt" command (0x20). There * are two 8259 chips: The first exists at 0x20, the second * exists at 0xA0. If the second controller (an IRQ from 8 to * 15) gets an interrupt, you need to acknowledge the * interrupt at BOTH controllers, otherwise, you only send * an EOI command to the first controller. If you don't send * an EOI, you won't raise any more IRQs */ void irq_handler(struct regs *r) { /* This is a blank function pointer */ void (*handler)(struct regs *r);
/* Find out if we have a custom handler to run for this * IRQ, and then finally, run it */ handler = irq_routines[r->int_no - 32]; if (handler) { handler(r); }
/* If the IDT entry that was invoked was greater than 40 * (meaning IRQ8 - 15), then we need to send an EOI to * the slave controller */ if (r->int_no >= 40) { outportb(0xA0, 0x20); }
/* In either case, we need to send an EOI to the master * interrupt controller too */ outportb(0x20, 0x20); } The contents of 'irq.c' ---------------------------------------------------------------------------------------------------- 为了安装IRQ的处理程序ISR,我们需要在"main.c"中的"main"函数中调用"irq_install"。在我们加入这个调用之前,我们需要为"irq_install","irq_install_handler"和"irq_uninstall_handler"在"system.h"中加入函数原型。"irq_install_handler"用于允许我们为我们的是被在给定的IRQ下安装特定的用户IRQ子处理程序。在后面的章节中,我们将使用"irq_install_handler"为系统时钟(PIT-IRQ0)和键盘(IRQ1)安装我们自己的IRQ处理程序。当我们安装问我们的异常处理ISR的时候,在"main.c"的"main"函数中加入"irq_install"。在允许IRQ安全进行的代码行后面加入行: __asm__ __volatile__ ("sti" ; 恭喜你,你已经一步步的学会了如何去创建一个可以处理IRQ和异常的简单的内核了。安装了IDT,用GRUB载入了我们自己的GDT替换了原来的那个GDT。如果你已经理解了我们直到目前说讲的东西,你已经翻越了与操作系统开发相关的最大的障碍之一。大多数热中于开发操作系统的人没有能成功的安装ISR和IDT。接下来,我们将要学习关于一个简单的设备去使用一个IRQ:可编程定时器(PIT)。
PIT,系统时钟 可编程定时器(PIT,8253或者8254)也被称为系统时钟,它是对于在固定的时间间隔内精确地产生中断是非常有用的心片。这个心片有三个通道:Chcannel 0被绑定在IRQ0上,它在固定的时间间隔内周期性地中断CPU;channel 1是系统只指定的,channel 2连接在系统扬声器上。就象你看到的那样,一个简单的心片提供了几个非常重要的系统服务。
你需要关心的只有channel 0和channel 2.你可以使用channel 2让计算机发出蜂鸣。在这部分教材中,我们只关心channel 0--映射到IRQ0。这个计时器的通道允许你在后续的过程中精确的调度一个新的进程,也可以将当前任务等待一个特定的时间间隔(后面我们将简单地描述这个问题)。在缺省情况下,计时器的通道被设置成每秒产生18.222次IRQ0请求。IBM PC/AT BIOS缺省的情况下就是如此设置的。这本教程的读者告诉我说18.222HZ的系统滴答是为了以0.055秒为周期进行滴答数的计数。因为是使用16bit的系统滴答计数器,则计数器将每隔一小时就溢出一次并归0。
为了设置计数器的通道0发出IRQ0的周期,我们必须使用端口输出函数写入I/O端口。有三个数据寄存器分别对应于计时器的三个通道:0X40,0X41,0X42,计时器的命令寄存器在OX43。数据率实际上是一个保存"除数"的寄存器.计时器将把它的输入时钟1.19MH(1193180HZ)除以你在数据寄存器中给出的数字来计算出每秒在这个通道上产生多少次中断请求信号。在写如data/divisor寄存器之前,你必须首先通过使用命令寄存器选择我们想要更新的通道。下面的图是对于命令寄存器的各个位的意义的解释: ----|------|-------|-----| 7 6| 5 4| 3 1| 0 | ----|------|-------|-----| CNTR| RW | Mode | BCD| ----|------|-------|-----| CNTR - Counter # (0-2) RW - Read Write mode(1 = LSB, 2 = MSB, 3 = LSB then MSB) Mode - See right table BCD - (0 = 16-bit counter,1 = 4x BCD decade counters)
---------------------------------------------------- Mode Description ---------------------------------------------------- 0 Interrupt on terminal count 1 Hardware Retriggerable one shot 2 Rate Generator 3 Square Wave Mode 4 Software Strobe 5 Hardware Strobe ---------------------------------------------------- Bit definitions for 8253 and 8254 chip's Command Register located at 0x43
为了设置通道0的数据寄存器,我们首先需要选择计数器0并且设置命令寄存器中的一些模式。我们想要写入数据寄存器中的除数是一个16位的值,因此我们需要将MSB(Most Significant Byte)和LSB(Least Significant Byte)转换到数据寄存器中去。这是一个16位的值,我们不能把数据以BCD的形式发送数据,因此BCD域应当设置为0。最后,我们想产生一个方波:MODE 3。我们在应该写入命令寄存器的最终的字节是0X36。上面的两段和图表可以被总结为如下函数。如果你想用的话就用吧,为了保持简单性我们将不在本教程中使用这个函数。为了精确而简单的计时,我推荐在实际的内核中设置为100HZ。
void timer_phase(int hz) { int divisor = 1193180 / hz; /* Calculate our divisor */ outportb(0x43, 0x36); /* Set our command byte 0x36 */ outportb(0x40, divisor & 0xFF); /* Set low byte of divisor */ outportb(0x40, divisor >> 8); /* Set high byte of divisor */ } 创建一个名叫"timer.c”的文件,把它加到你的"build.bat"文件中。当你分析下面的代码的时候,你将看到我们将跟踪计时器所产生的系统滴答数,这可以在你的内核变的更加复杂的情况下做为“系统正常运行时间”计数器。这里计时器中断只是简单的使用18.222HZ的频率去计算出什么时候应当显示一个简单的信息“One second has passed”,这个信息每秒种显示一次。如果你决定在你的代码中使用"timer_phase"函数,你应当把"timer_handler"中的"timer_ticks%18==0"这一行改为"timer_ticks%100==0"。你可以在内核中的任何函数中设置timer phase,然而,我推荐你在"timer_install"中设置,以保持代码的清晰。 ----------------------------------------------------------------------------------------------------- #include < system.h >
/* This will keep track of how many ticks that the system * has been running for */ int timer_ticks = 0;
/* Handles the timer. In this case, it's very simple: We * increment the 'timer_ticks' variable every time the * timer fires. By default, the timer fires 18.222 times * per second. Why 18.222Hz? Some engineer at IBM must've * been smoking something funky */ void timer_handler(struct regs *r) { /* Increment our 'tick count' */ timer_ticks++;
/* Every 18 clocks (approximately 1 second), we will * display a message on the screen */ if (timer_ticks % 18 == 0) { puts("One second has passed\n"); } }
/* Sets up the system clock by installing the timer handler * into IRQ0 */ void timer_install() { /* Installs 'timer_handler' to IRQ0 */ irq_install_handler(0, timer_handler); } Example of using the system timer: 'timer.c' ------------------------------------------------------------------------------------------------- 记得在"main.c"中的"main"函数中加入对"timer_install"的调用。有问题吗???记得在"system.h"中加入"timer_install"的原型。接下来的代码示范了你可以用系统时钟做什么。如果你仔细看的话,这个简单的函数在指定的系统滴答数时间内在一个循环中等待。这几乎与标准的C库函数"delay"完全相同。 --------------------------------------------------------------------------------------------------- /* This will continuously loop until the given time has * been reached */ void timer_wait(int ticks) { unsigned long eticks;
eticks = timer_ticks + ticks; while(timer_ticks < eticks); }
If you wish, add this to 'timer.c' and a prototype to 'system.h' ------------------------------------------------------------------------------------------------- 下面我们将讨论如何使用键盘,这牵扯到安装用户自己的IRQ处理程序还有每个中断上的I/O断口。
键盘
键盘是一种用户输入的最常用的方法,因此创建一些处理和管理键盘的驱动程序是非常重要的。当你开始做这部分工作的时候,了解一下键盘的基本知识也不坏。现在我们将讲一下这些基础知识:如果在按下键盘的时候获得一个键,如果把扫描码转换为ASCII码的可理解形式。
扫描码是一个简单的键的数字。键盘为每一个键盘上的键分配了一个数字,这就是扫描码。扫描码是从上到下,从左到右编码的,为了保持与老的键盘的向后兼容这里也有一些例外。 你必须使用一个表(一个值的数组)并使用扫描码作为索引去查这个表。这个表被称为键盘映射表,将被用来把扫描码转化为ASCII值。在我们看具体的代码之前你还需要注意一点,如果位7被设为1的话(可用"scancode & 0X80"来检验)则表示一个键刚被释放。创建你自己的"kb.h"并且做好一些常规步骤,比如为GCC加入一行,为LD加入一个目标文件等等 -------------------------------------------------------------------------------------------------------- /* KBDUS means US Keyboard Layout. This is a scancode table * used to layout a standard US keyboard. I have left some * comments in to give you an idea of what key is what, even * though I set it's array index to 0. You can change that to * whatever you want using a macro, if you wish! */ unsigned char kbdus[128] = { 0, 27, '1', '2', '3', '4', '5', '6', '7', '8', /* 9 */ '9', '0', '-', '=', '\b', /* Backspace */ '\t', /* Tab */ 'q', 'w', 'e', 'r', /* 19 */ 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\n', /* Enter key */ 0, /* 29 - Control */ 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', /* 39 */ '\'', '`', 0, /* Left shift */ '\\', 'z', 'x', 'c', 'v', 'b', 'n', /* 49 */ 'm', ',', '.', '/', 0, /* Right shift */ '*', 0, /* Alt */ ' ', /* Space bar */ 0, /* Caps lock */ 0, /* 59 - F1 key ... > */ 0, 0, 0, 0, 0, 0, 0, 0, 0, /* < ... F10 */ 0, /* 69 - Num lock*/ 0, /* Scroll Lock */ 0, /* Home key */ 0, /* Up Arrow */ 0, /* Page Up */ '-', 0, /* Left Arrow */ 0, 0, /* Right Arrow */ '+', 0, /* 79 - End key*/ 0, /* Down Arrow */ 0, /* Page Down */ 0, /* Insert Key */ 0, /* Delete Key */ 0, 0, 0, 0, /* F11 Key */ 0, /* F12 Key */ 0, /* All other keys are undefined */ }; Sample keymap. Add this array to your 'kb.c' -------------------------------------------------------------------------------------------------------- 把扫描码转换为ASCII值可以非常简单的按照如下方式实现:
mychar = kbdus[scancode];
注意,我们留下了功能键和shift/control/alt的注释,我们把他们作为数组中的第0个元素。你需要设计一些随机的值比如那些你通常不使用的ASCII值以便于捕捉这些键。我将这部分工作留给你去做,但是你应当应当维护一个全局标量用来作为键盘状态变量。这个键盘状态变量将有一个位用来设置ALT,一个位用来设置CONTROL,一个位用来设置SHIFT。为CAPSLOCK和NUMLOCK以及SCROLLLOCK也分配一个位将是个好主义。这个教程将结实如何设置键盘灯,但是我们将把这部分工作留给你去写。
键盘通过主板上的一个特殊微控制器连接到计算机系统上。这个键盘控制器心片有2个通道:一个是为键盘使用的,一个是为鼠标使用的。还要注意,也是通过着个键盘控制器心片,你可以启用处理器上的A20地址线,它可以允许你访问超过1MB的内存空间(GRUB做了这部分工作,你不需要为此担心)。因为键盘控制器是一个系统可访问的设备,因此它拥有一个I/O地址,通过这个地址我们可以用来访问饿控制键盘控制器。键盘控制器有两个主要的寄存器:一个数据寄存器地址在0X60,一个控制寄存器地址在0X64。键盘想要发送到计算机的任何信息都是保存在数据寄存器只能感的。键盘在具有可被我们读取的数据时便产生一个中断--IRQ1。
----------------------------------------------------------------------------------------------------- /* Handles the keyboard interrupt */ void keyboard_handler(struct regs *r) { unsigned char scancode;
/* Read from the keyboard's data buffer */ scancode = inportb(0x60);
/* If the top bit of the byte we read from the keyboard is * set, that means that a key has just been released */ if (scancode & 0x80) { /* You can use this one to see if the user released the * shift, alt, or control keys... */ } else { /* Here, a key was just pressed. Please note that if you * hold a key down, you will get repeated key press * interrupts. */
/* Just to show you how this works, we simply translate * the keyboard scancode into an ASCII value, and then * display it to the screen. You can get creative and * use some flags to see if a shift is pressed and use a * different layout, or you can add another 128 entries * to the above layout to correspond to 'shift' being * held. If shift is held using the larger lookup table, * you would add 128 to the scancode when you look for it */ putch(kbdus[scancode]); } } This might look intimidating, but it's 80% comments ;) Add to 'kb.c' ----------------------------------------------------------------------------------------------------- 就象你能看到的那样,键盘将产生IRQ1中断告诉我们它有数据要传。键盘的数据寄存器在地址0X60。当IRQ发生的时候,我们调用与中断相应的处理程序,这个程序从端口0X60读取数据。我们读到的数据就是键盘的扫描码。对于这个例子来说,我们检查一个键是否被按下或者是被释放。如果是被按下,我们把扫描码转换成ASCII码,然后使用一行打印这个字符。写一个"keyboard_install"函数,这个函数调用"irq_install_handler"为"keyboard_handler"安装用户自定义的键盘处理程序。确保在"main"中调用了"keyboard_install"。
为了设置键盘上的灯,你必须为键盘控制器发送一个命令。为键盘发送命令有一个特定的步骤。你必须先等待键盘控制器告诉你它什么时候"not busy"。为了实现这一点,你需要循环地从键盘控制器的控制寄存器中读取数据(当你从它读数据的时候它被称为状态寄存器),当键盘不忙的时候你将跳出这个循环。
if ((inportb(0x64) & 2) == 0) break;
循环之后,你可以写入一个命令字节到数据寄存器中。除了特殊的情况外你不能写如控制寄存器中。为了设置键盘上的 灯,你首先需要发送命令字节0XED,然后你发送一个字节,这个字节表示了哪个灯亮哪个不亮。这个字节有如下格式: bit 0 表示Scroll lock 灯,bit 1表示Num lock灯,bit 2表示Caps lock灯。
既然你已经具备了关于键盘的基础知识,你可能希望扩展这个代码。这个部分只是为了讲键盘的基本知识而不是给出详细的所有键盘控制器的功能。注意你使用键盘控制器起用和操作PS/2鼠标口。键盘控器上的福建通道管理PS/2鼠标。到目前为止,我们已经有了一个内核,这个内核可以在屏幕上绘制,处理异常,处理IRQ,处理计时器,处理键盘消息。我门下面将看看接下来该为内核开发做什么。
剩下的工作
接下来你要为你的内核做的什么完全取决于你。下一件你应当思考的问题是内存管理。内存管理器将允许你提取内存中的一块空间,以便于你可以动态的分配和释放内存。使用内存管理程序,你可以使用更复杂的数据结构比如连表和二叉树,这样就可以高效地使用内存及操纵数据。这也是防止应用程序写入内核页的一种途径,这是一个保护特性。
写一个VGA驱动也是可能的。使用VGA驱动,你可以在你的内核中设置不同的图形模式,允许更高的分辨率和图形显示选项诸如按纽和图象。如果你想更进一步的话,你可以最终进入VESA模式,使用更高的分辨率更多的色彩。
最好你能够写一个设备界面,这个界面将允许你装入或者卸载内核的模块。加入文件系统和磁盘驱动这样你就可以访问磁盘文件及打开应用程序。
很可能你能够为内核加入多任务支持,设计进程调度算法给某些任务指定更高的优先级和更长的运行时间。多任务系统与你的内存管理程序密切相关,内存管理程序可以给每一个任务分配独立的内存空间。
示例内核的源代码树:
start.asm gdt.c idt.c irq.c isrs.c kb.c main.c scrn.c timer.c include/system.h link.ld build.bat dev_kernel_grub.img
我希望这个教程将给你对于创建内核时不同底层细节的更为彻底的理解。
------------------------------------全文完---------------------------------------------------------- 原文网址: http://bbs.kaoyan.com/thread-1537337-1-1.html
|