sagalinux学习之/kernel目录

来源:互联网 发布:2016男女就业比例数据 编辑:程序博客网 时间:2024/06/11 04:29

这篇分析有点长了,慢慢看~~。

请随手携带一本《32位微机原理》宝典,以备查询。

这是kernel目录下的Makefile,很简单

CFLAGS = -I../include/ -c -Wall -fno-stack-protector -oall:kernel.o page.o idt.o printk.o keyboard.o irq.o i8259.o console.oclean:rm -f *.o *~kernel.o:gcc ${CFLAGS} kernel.o kernel.cpage.o:gcc ${CFLAGS} page.o page.cidt.o:gcc ${CFLAGS} idt.o idt.cprintk.o:gcc ${CFLAGS} printk.o printk.ckeyboard.o:gcc ${CFLAGS} keyboard.o keyboard.cirq.o:gcc ${CFLAGS} irq.o irq.ci8259.o:nasm -felf i8259.sconsole.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();}

回到主函数中下一个trap_init()函数,位于./kernel/idt.c中

#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+32void 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");}

idtr_t、trapgd_t、intgd_t、taskgd_t类型就不细说了,就拿本《32位微机原理》,和书上的格式对照一下,就知道了。


注意上面有一个extern void __irq1(void);
是对外部中断函数的声明,函数的实现在/kernel/i8259.s中
下面来看一下这个文件

/kernel/i8259.s

;进入保护模式就是32位了[BITS 32];这里的中断是外部8259上的中断,两片8259可以提供15个外部中断extern  irq_handler;在./include/kernel/irq.h中,是一个函数指针数组:void (*irq_handler[16])();global__irq0global__irq1global__irq2global__irq3global__irq4global__irq5global__irq6global__irq7global__irq8global__irq9global__irq10global__irq11global__irq12global__irq13global__irq14global__irq15;中断函数do_irq:popeax;外部中断号出栈call [irq_handler + 4*eax];irq_handler+4*eax,跳到真正的中断处理函数地址,每个地址4Bmoval, 0x20;向8259发送EOI,通知中断结束。movdx, 0x20outdx, alpopairet__irq0:;硬件中断的处理程序,这里的函数地址将会存储在IDT中的门描述符中pushamov eax, $0push eaxjmp do_irq__irq1:;sagalinux只实现了这个键盘中断,pushamov eax, $1push eaxjmp do_irq__irq2:pushamov eax, $2push eaxjmp do_irq__irq3:pushamov eax, $3push eaxjmp do_irq__irq4:pushamov eax, $4push eaxjmp do_irq__irq5:pushamov eax, $5push eaxjmp do_irq__irq6:pushamov eax, $6push eaxjmp do_irq__irq7:pushamov eax, $7push eaxjmp do_irq__irq8:pushamov eax, $8push eaxjmp do_irq__irq9:pushamov eax, $9push eaxjmp do_irq__irq10:pushamov eax, $10push eaxjmp do_irq__irq11:pushamov eax, $11push eaxjmp do_irq__irq12:pushamov eax, $12push eaxjmp do_irq__irq13:pushamov eax, $13push eaxjmp do_irq__irq14:pushamov eax, $14push eaxjmp do_irq__irq15:pushamov eax, $15push eaxjmp do_irq

再细看究竟键盘中断是怎么实现的,就是按一下键,屏幕上产生一个字符。
我们回到主函数中看下一个函数keyboard_init(),它位于./kernel/keyboard.c中

#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 | 0x80static 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 occurvoid 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);//使能中断}

看到这里,基本上这个系统最核心的东西就完了,还有一些处理终端界面,输入输出的函数,下面我们再分析。
内存的初始化和中断的过程是sagalinux所描述最多的,这也是计算机中基本而又最重要的知识。




分析到这里,./kernel目录下的文件就只剩下console.c和printk.c了

再回到主函数中看下一个函数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 160static 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 6845static 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;}

再看主函数中的最后一个函数printk();

从这个函数里我们可以学到可变参数函数的原理微笑

#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。}

这里的va_list类型在./include/stdarg.h中

你需要理解函数运行时栈的变化。

比如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

再看printk()函数下的vsnprintf()函数,它位于./lib/vsnprintf.c中

#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);  }

至此,sagaLinux中的所有运行代码分析完毕,我花了一天时间写的这篇东西。。。希望能够对大家有所帮助。
包括汇编,寻址方式,中断原理,内存初始化,描述符表,嵌入汇编,对指针的理解,函数调用堆栈,C语法。又向高手迈出了巨大的一步~~~~。
感谢Faik Yalcin Uygur,是他写的整个代码,而且现在又出了一个改进的版本,提供基本的内存管理和文件系统,有兴趣的同志可以去看看。


我下次准备再分析一个GeekOS来看看。

原创粉丝点击