这篇分析有点长了,慢慢看~~。
请随手携带一本《32位微机原理》宝典,以备查询。
这是kernel目录下的Makefile,很简单
CFLAGS = -I../include/ -c -Wall -fno-stack-protector -o all: kernel.o page.o idt.o printk.o keyboard.o irq.o i8259.o console.o clean: rm -f *.o *~ kernel.o: gcc ${CFLAGS} kernel.o kernel.c page.o: gcc ${CFLAGS} page.o page.c idt.o: gcc ${CFLAGS} idt.o idt.c printk.o: gcc ${CFLAGS} printk.o printk.c keyboard.o: gcc ${CFLAGS} keyboard.o keyboard.c irq.o: gcc ${CFLAGS} irq.o irq.c i8259.o: nasm -felf i8259.s console.o: gcc ${CFLAGS} console.o console.c
我们先来看目录下的kernel.c文件,主函数就在这里,也就是sagalinux的“内核"核心。。。。我们从主函数发散出去逐个研究。
#include "kernel/idt.h" #include "kernel/page.h" #include "kernel/kernel.h" #include "kernel/keyboard.h" #include "kernel/console.h" int main(void) { // int i; char* message = "\nWelcome!\n"; char* kernel_release = "Kernel on an i386\n\n"; char* fake_login = "[LinuxSaga]# "; page_init();//初始化页机制,开启分页,映射了0~16M的物理页 trap_init();//初始化陷阱门 keyboard_init();//初始化键盘 con_clear();//清除终端的所有字符 printk("%s",message); printk("%s",kernel_release); printk("%s",fake_login); // i = 3 / 0; while(1); return 0; }
简单吧,下面是每一个初始化函数的详细
/kernel/page.c 用于初始化页表,页表中描述一个4K的物理页的一个描述项叫做PTE,PTE大小为4B, 如果PTE在一个4K的物理页中连续存放,形成一个表,我们用PDE来描述这个PTE表,一个PDE大小为4B。 就是这一回事。 #include "kernel/page.h"//一些宏定义 #include "asm/system.h" //初始化页表 void page_init() { int pages = NR_PGT;//PTE表的描述项PDE的个数 // NR_PGT 为4,创建4个PTE表的描述项(PDE),每个PDE占4B,描述了一个PTE表,每个PTE描述了一个4K的页,这样总共可以访问4*1024*4K即16M大小的内存 unsigned int page_offset = PAGE_OFFSET;//#define PAGE_OFFSET (unsigned int)0x2000 (8K) //PDE项的开始物理地址,PGD_BASE=0x1000(4K),也就是说PDE位于物理内存的第二个页框内(每个page为4K) unsigned int* pgd = PGD_BASE;//#define PGD_BASE (unsigned int*)0x1000 PGD_BASE 是一个指针,指向unsigned int类型,指针的值是0x1000 //赋值之后pgd指向了0x1000这个内存地址,注意pgd这个指针变量占用4B, unsigned int phy_add = 0x0000;// 从物理地址的最低端开始建立PTE项的映射,映射内存地址0~16M // PTE表从物理内存8K处开始,每个PTE表有1024项,占满一个物理页。共有4个PTE表,且连续存放 unsigned int* pgt_entry = (unsigned int*)0x2000; //PTE表的开始地址(8K) // 填充页目录表PDE项 // 这里依次创建4(pages=4)个页目录表PDE,一个PDE就是一个PTE表的描述项 //(每个PTE表有1024项,1024*4B=4K,占一个物理页) while (pages--) { *pgd++ = page_offset |PTE_USR|PTE_RW|PTE_PRE; page_offset += 0x1000;//继续描述下一个PTE表 } //PDE表,有四项PDE:0x1000~0x2000 //PTE表:共四个表,位于0x2000~0x3000, 0x3000~0x4000, 0x4000~0x5000, 0x5000~0x6000 pgd = PGD_BASE;//恢复pgd为PDE表基地址0x1000,4K // 在页表中填写页到物理地址的映射关系,映射了16M大小的物理内存 即0~16M的物理内存 //共映射了0x1000(4K)个PTE,每个PTE指向一个4K的物理页,共4K*4K=16M内存 //这里因为PTE表是连续的,所以可以直接给所有的PTE赋值,(*pgt_entry++) while (phy_add < 0x1000000) {//0x1000000=16M *pgt_entry++ = phy_add |PTE_USR|PTE_RW|PTE_PRE;//下一个PTE项,每个PTE项描述了一个物理页 phy_add += 0x1000;//下一个物理页,共有4K个PTE,每个PTE一次赋值,共循环了4K次 } //嵌入式汇编语法 //__asm__(assembler template // : output operands /* optional *// // : input operands /* optional */ // : list of clobbered registers /* optional */ //); // 启用页机制, cr3指向页目录根PDE表的开始 __asm__ __volatile__ ("movl %0, %%cr3;"//pgd-->cr3,PDT基地址 "movl %%cr0, %%eax;" "orl $0x80000000, %%eax;" "movl %%eax, %%cr0;" //eax-->cr0,cr0最高位置1,用于开启分页 : :"r"(pgd)//%0代表pgd :"memory","%eax"); //Invalidate TLB before and after setting up page tables //使TLB无效,在多核处理器上,避免其他cpu引用到无效的页表 invalidate_tlb(); }
#include "kernel/idt.h" #include "kernel/kernel.h" #include "asm/system.h" //x86的描述符类型:存储段描述符;系统段描述符;门描述符 extern void __irq1(void);//声明了外部的中断函数 //idt包含三种类型的门描述符:任务门描述符,中断门描述符,陷阱门描述符。共同点都是CPU暂停当下过程,转而去执行其他过程。 //中断具体过程如下: // 0. cpu得到中断向量 // 1. cpu由idtr寄存器中的基址和界限得到idt在物理内存中的信息 // 2. 基址+8*中断向量=中断描述符表中的中断门或陷阱门 // 3. cpu将某一门描述符中的选择子送cs,根据ti位确定从gdt或ldt中选择一个段描述符,即确定了一个段 // 4. 根据门描述符中的偏移确定段中中断服务程序的具体入口地址。 //intel x86支持256种中断,编号为中断向量0~255,中断向量0~32为intel设定的中断,又称为异常,由CPU内部同步产生,比如中断向量0为除法错误。 //中断向量y和外部中断号x的关系 默认为y=x+32 void trap_init() { int i; idtr_t idtr; //idtr 用于指示中断描述符表idt的基址和界限 6B ////设置陷阱门 set_trap_gate(0, (unsigned int)÷_error); set_trap_gate(1, (unsigned int)&debug); set_trap_gate(2, (unsigned int)&nmi); set_trap_gate(3, (unsigned int)&int3); set_trap_gate(4, (unsigned int)&overflow); set_trap_gate(5, (unsigned int)&bounds); set_trap_gate(6, (unsigned int)&invalid_op); set_trap_gate(7, (unsigned int)&device_not_available); set_trap_gate(8, (unsigned int)&double_fault); set_trap_gate(9, (unsigned int)&coprocessor_segment_overrun); set_trap_gate(10,(unsigned int) &invalid_TSS); set_trap_gate(11, (unsigned int)&segment_not_present); set_trap_gate(12, (unsigned int)&stack_segment); set_trap_gate(13, (unsigned int)&general_protection); set_trap_gate(14, (unsigned int)&page_fault); set_trap_gate(15, (unsigned int)&coprocessor_error); set_trap_gate(16, (unsigned int)&alignment_check); //(unsigned int)把指针强制转换成unsigned int类型 for (i = 17;i<32;i++)//保留中断向量 set_trap_gate(i, (unsigned int)&reserved); //设置外部中断 set_trap_gate(33, (unsigned int)&__irq1);//irq1 为键盘中断,中断向量为1+32=33(中断号和中断向量的关系由主板物理连接决定) //设置中断描述符表寄存器 idtr.limit = 34*8; //0~33共34个中断 idtr.lowerbase = 0x0000;//从内存地址0x0开始 idtr.higherbase = 0x0000; cli();//关中断cf <--0 __asm__ __volatile__ ("lidt (%0)"//装载IDT的信息至idtr寄存器 ::"p" (&idtr)); sti();//开中断cf <--1 } void set_trap_gate(int vector, unsigned int handler_offset)//在IDT中填入8B的门描述符 { //#define IDT_BASE 0x0000 trapgd_t* trapgd = (trapgd_t*) IDT_BASE + vector; //IDT_BASE+8*vector,在IDT中相应位置存放8B的陷阱描述符 trapgd->loffset = handler_offset & 0x0000FFFF; //存储函数地址低16位 trapgd->segment_s = CODESEGMENT; //这里选择子是0x08,指示的是内核代码段 trapgd->reserved = 0x00; trapgd->options = 0x0F | PRESENT | KERNEL_LEVEL;//存在的,特权级为0的段 trapgd->hoffset = ((handler_offset & 0xFFFF0000) >> 16);//存储函数地址高16位 } void set_int_gate(int vector, unsigned int handler_offset)//中断门 { intgd_t* intgd = (intgd_t*) IDT_BASE + vector; intgd->loffset = handler_offset & 0x0000FFFF; intgd->segment_s = CODESEGMENT; intgd->reserved = 0x0; intgd->options = 0x0E | PRESENT | KERNEL_LEVEL;//和set_trap_gate()的不同之处仅在于此,这里是0x0E intgd->hoffset = ((handler_offset & 0xFFFF0000) >> 16); } void set_task_gate(int vector, ushort_t tss) //任务门 { taskgd_t* taskgd = (taskgd_t*) IDT_BASE + vector; taskgd->reserved = 0x0; taskgd->tss = tss;//指向tss段的选择子 taskgd->reservedd = 0x0; taskgd->options = 0x05 | PRESENT | KERNEL_LEVEL; taskgd->reserveddd = 0x0; } // Nooooo... just sleep :) void sleep(char* message) { printk("%s",message); while(1); } void divide_error(void) { sleep("divide error"); } void debug(void) { sleep("debug"); } void nmi(void) { sleep("nmi"); } void int3(void) { sleep("int3"); } void overflow(void) { sleep("overflow"); } void bounds(void) { sleep("bounds"); } void invalid_op(void) { sleep("invalid op"); } void device_not_available(void) { sleep("device not available"); } void double_fault(void) { sleep("double fault"); } void coprocessor_segment_overrun(void) { sleep("coprocessor segment overrun"); } void invalid_TSS(void) { sleep("invalid TSS"); } void segment_not_present(void) { sleep("segment not present"); } void stack_segment(void) { sleep("stack segment"); } void general_protection(void) { sleep("general protection"); } void page_fault(void) { sleep("page fault"); } void coprocessor_error(void) { sleep("coprocessor error"); } void alignment_check(void) { sleep("alignment check"); } void reserved(void) { sleep("reserved"); }
/kernel/i8259.s
;进入保护模式就是32位了 [BITS 32] ;这里的中断是外部8259上的中断,两片8259可以提供15个外部中断 extern irq_handler ;在./include/kernel/irq.h中,是一个函数指针数组:void (*irq_handler[16])(); 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 ;中断函数 do_irq: pop eax ;外部中断号出栈 call [irq_handler + 4*eax] ;irq_handler+4*eax,跳到真正的中断处理函数地址,每个地址4B mov al, 0x20 ;向8259发送EOI,通知中断结束。 mov dx, 0x20 out dx, al popa iret __irq0: ;硬件中断的处理程序,这里的函数地址将会存储在IDT中的门描述符中 pusha mov eax, $0 push eax jmp do_irq __irq1: ;sagalinux只实现了这个键盘中断, pusha mov eax, $1 push eax jmp do_irq __irq2: pusha mov eax, $2 push eax jmp do_irq __irq3: pusha mov eax, $3 push eax jmp do_irq __irq4: pusha mov eax, $4 push eax jmp do_irq __irq5: pusha mov eax, $5 push eax jmp do_irq __irq6: pusha mov eax, $6 push eax jmp do_irq __irq7: pusha mov eax, $7 push eax jmp do_irq __irq8: pusha mov eax, $8 push eax jmp do_irq __irq9: pusha mov eax, $9 push eax jmp do_irq __irq10: pusha mov eax, $10 push eax jmp do_irq __irq11: pusha mov eax, $11 push eax jmp do_irq __irq12: pusha mov eax, $12 push eax jmp do_irq __irq13: pusha mov eax, $13 push eax jmp do_irq __irq14: pusha mov eax, $14 push eax jmp do_irq __irq15: pusha mov eax, $15 push eax jmp do_irq
#include "kernel/irq.h" #include "kernel/kernel.h" #include "asm/io.h" #include "sys/types.h" static byte shift = 0;//here the variable shift is global; typedef unsigned char byte; #define LEFTSHIFT 0x2A//make code of leftshift #define RIGHTSHIFT 0x36//make code of rightshift //you should know : make code and break code ,which mean press and relax, break code = make code | 0x80 static byte keymap[] = { /* SHIFT OFF */ \ ' ',' ', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\b', '\t', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\n', ' ', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '~', ' ', '\"', 'z', 'x', 'c', 'v', 'b', 'n', 'm', '<', '>', '/', ' ', ' ', ' ', ' ', ' ', //the last character this line is keymap[0x3B] /* SHIFT ON */ ' ', ' ', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', ' ', ' ', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', ' ', ' ', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '\"', '<', ' ', '"', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', ' ', ' ', ' ', ' ', ' '}; //keyboard interrupt process function,when key press or relax ,interrupt occur void keyboard_handler()//这就是真正的键盘中断处理函数 { //过程:按下键盘->芯片收到中断信号->将中断号发给cpu->cpu加32得到中断向量->cpu去向量表中找到函数指针__irq1->__irq1跳到do_irq->do_irq通过中断号索引函数指针数组irq_handler[],最终执行中断函数keyboard_handler()。 byte scancode = inb(0x60); //read code from buffer ,only one byte when the interrupt occur,每次中断只处理缓冲区中的一个字节 byte ascii = 0; switch (scancode) {//键盘按下和松开都会产生扫描码 case LEFTSHIFT: case RIGHTSHIFT: shift = 0x3B; // Nothing special.只是按下shift键的话不做任何处理,注意shift是一个全局变量 break; case LEFTSHIFT | 0x80: //break code,relax the key case RIGHTSHIFT | 0x80: //松开键盘,shift标志清0 shift = 0; break; default://the other character if (scancode < 0x3B) { ascii = keymap[scancode + shift];//由扫描码索引得到字符 printk("%c",ascii); } } } void keyboard_init() { // #define KEYBOARD 0x01 request_irq(KEYBOARD, keyboard_handler);//在中断处理函数数组中写入keyboard_handler函数指针 #define KEYBOARD 0x01 }
request_irq()的实现在./kernel/irq.c中 #include "kernel/irq.h" #include "asm/io.h" void enable_irq(int irq) { if (irq > 8) //从芯片irq>8 outb(inb(SLAVE)&(~(1<<irq)),SLAVE);//set bit 0 means open,使用inb(SLAVE)读取从芯片的配置,并将irq号位清0,使能此中断。 else outb(inb(MASTER)&(~(1<<irq)),MASTER); } void disable_irq(int irq) { if (irq > 8) outb((inb(SLAVE)|irq),SLAVE);//set bit 1 means close,这里应该是1<<irq吧 else outb((inb(MASTER)|irq),MASTER); } //设置某中断号的中断处理程序 void request_irq(int irq, void (*handler)()) { irq_handler[irq] = handler;// void (*irq_handler[16])(); irq_handler[irq]为函数指针 is a address of function enable_irq(irq); //使能中断 }
再回到主函数中看下一个函数con_clear(),它在./kernel/concole.c中
concole.c主要是设置界面终端字符的显示和清空的
代码也很简单,采用了直接写视频缓冲区0xB8000的方式,看一下就好了
#include "kernel/console.h" #include "sys/types.h" #include "asm/system.h" #include "asm/io.h" #define SCREEN 0xB8000 #define MAXLINES 25 #define MAXCOLUMNS 80 #define NEXTLINE 160 static unsigned int cur_pos = 0; static unsigned int cur_x,cur_y; static char* scr_origin = (char*)SCREEN; static ushort_t video_cont_reg = 0x3d4; // Video Controller Chip 6845 static ushort_t video_cont_val = 0x3d5; void set_cursor() { cli(); outb(14, video_cont_reg); outb((cur_pos>>9) & 0xFF, video_cont_val); outb(15, video_cont_reg); outb(cur_pos>>1 & 0xFF, video_cont_val); sti(); } void con_write(char* message, char attr) { unsigned int pos = cur_x * 2 + cur_y * NEXTLINE;//屏幕上的一个字符需要2B来表示,一行有80个字符需要160B。 char c; while ((c = *message++) != '\0') { switch (c) { case '\n': pos += (NEXTLINE - pos % NEXTLINE); cur_y++; cur_x = 0; break; case '\b': pos -= 2; cur_x--; *(scr_origin + pos) = ' '; break; default: *(scr_origin + pos) = c; *(scr_origin + pos + 1) = attr; cur_x++; pos += 2; } } cur_pos = pos; set_cursor(); } void con_clear() { int i; int scr_buffer = MAXLINES * MAXCOLUMNS * 2; for (i = 0; i < scr_buffer; i+=2) *(scr_origin + i) = ' '; cur_x = 0; cur_y = 0; } void scroll_up() { } void scroll_down() { } void goto_xy(unsigned int x, unsigned int y) { if (x > MAXCOLUMNS || y > MAXLINES) return; cur_x = x; cur_y = y; }
从这个函数里我们可以学到可变参数函数的原理
#include "stdarg.h" #include "kernel/kernel.h" #include "kernel/console.h" static char buf[1024]; //定义了一个静态全局变量buf缓冲区 void printk(const char* fmt,...)//格式化输出,函数参数是逆序入栈的(从括号末尾处的参数开始进栈),内存最低处存的是字符串指针。 { va_list args; va_start(args,fmt); vsnprintf(buf,sizeof(buf), fmt, args);//将 va_end(args); con_write(buf,0xf);//最后输出的字符串存在buf中,在终端上显示buf。 }
你需要理解函数运行时栈的变化。
比如printf("hello,world! %d %d", 1, 2);
栈由高向低生长,从高到低依次存储的是2,1,以及字符串"hello,world! %d %d"的地址。
#ifndef __STRARG_H__ #define __STRARG_H__ typedef char* va_list;//指针类型 // Implicit type conversion occurs in argument passing. In order to use the arguments // in an argument list where the parameters are pushed to stack, the sizes of the arguments has to be known. // In printf you can use %d for char short int and int. Thats because all are converted to int. // !!!!!!!! Implicit type conversion occurs if the prototype does not specify a type... the prototype of // printf(const char* fmt,...) does not specify a type for arguments so all are promoted... #define __va_size(type) \ (((sizeof(type) + sizeof(long) - 1) / sizeof (long)) * sizeof (long))//这里是得到类型的大小,考虑到了内存4字节对齐。如果type是char型的话会返回4。 #define va_start(ap, last) \ ((ap) = ((char*)&last) + __va_size(last))//得到堆栈中下个元素的指针赋给ap #define va_arg(ap, type) \ (*(type*)((ap)+= __va_size(type), (ap) - __va_size(type)))//先让ap指向堆栈中的下一个参数,然后返回上一个参数的指针 #define va_end(va_list) ((void)0) #endif
#include "stdarg.h" #include "sys/types.h" void vsnprintf (char *str, size_t size, const char *fmt, va_list ap) { char *temp; char c; size_t len = size; int i,j,sign = 0; int num; char numbers[] = {'0','1', '2', '3', '4', '5', '6', '7', '8', '9'}; char strnumber[] = " "; // :) max 11 digit with the sign.用来存储数的字符串,带符号 while (*fmt && len) { if(*fmt != '%')//未遇见格式标识则照常规输出 { *str++ = *fmt++; len--; continue; } fmt++; //接下来是控制字符 switch (*fmt++){ case 'c': *str++ = (unsigned char)va_arg(ap,int); len--; break; case 'i': case 'd': num = va_arg(ap,int);//先将指针指向下一个参数,然后返回堆栈中下一个参数的值, if (num < 0) { num *= -1; sign = 1; } for(i = 0; i < 11; i++){ if(num < 10) { strnumber[i] = numbers[num]; break; } strnumber[i] = numbers[num % 10]; num = num / 10; } if(sign--)//数字在临时缓冲区中逆序存放,比如-1234,存的是"4321-" strnumber[++i] = '-'; for(j = i; j >= 0 && len>0; j--,len--) *str++ = strnumber[j]; break; case 's': temp = va_arg(ap,char*); while((c = *temp++) != '\0') *str++ = c; break; default: break; } } *str='\0'; } void snprintf (char *str, size_t size, const char *format,...) { va_list args; va_start(args,format); vsnprintf(str, size, format, args); va_end(args); } void sprintf (char *str, const char *format,...) { va_list args; va_start(args,format); vsnprintf(str, 0xFFFFFFFFUL, format, args); va_end(args); }
我下次准备再分析一个GeekOS来看看。