Chinaunix首页 | 论坛 | 博客
  • 博客访问: 5760430
  • 博文数量: 675
  • 博客积分: 20301
  • 博客等级: 上将
  • 技术积分: 7671
  • 用 户 组: 普通用户
  • 注册时间: 2005-12-31 16:15
文章分类

全部博文(675)

文章存档

2012年(1)

2011年(20)

2010年(14)

2009年(63)

2008年(118)

2007年(141)

2006年(318)

分类: LINUX

2009-04-19 00:15:38

好久没有看kernel的启动部分代码了,今天在看tim robinson's gdt trick的时候有些吃力了,本来是想翻译来着,但是比较简单,就直接贴过来了,里面掺杂着一些我添加的注释。


//其实下面的关于地址的讨论,有些概念性的错误。
//这里应该注意几个概念:逻辑地址,线性地址,虚拟地址和物理地址。
//逻辑地址:也就是汇编代码中的段选择子+段内偏移形式的表示的地址,例如这里的0x08:highhalf
//线性地址,或者是虚拟地址,是经过分段机制后得到的地址,即通过GDT的代码段基地址+段内偏移量。
//物理地址,是线性地址经过分页机制得到的地址,即通过cr3,页目录表,页表查询到的页地址+页内偏移地址
//关于这些地址的概念和使用,可以参考:

---------------------------------------------------------------------------------
Introduction

In this tutorial we'll write a very simple higher-half kernel using TimRobinson's GDT trick. This will allow us to load our kernel at some high address (e.g 0xC0000000) without enabling paging. Later, when we have done all our "critical" jobs, we'll enable paging and use a "normal" GDT.

The sources are very well commented and may be compiled with NASM/YASM and GCC.
The assembler part

This is our first file, start.asm. Here we define the Multiboot header and some useful functions, such as gdt_flush. Once GRUB has loaded the kernel in memory, we use the lgdt instruction to load the "fake" GDT, that will allow us to jump into our kernel. The CPU will add the base 0x40000000 to every function, so it will jump to address 0xC0100000 + 0x40000000 = 0x100000.

Note the .setup section: the CPU always needs the physical address of the GDT, but we are linking all the kernel in the higher half. How do we manage this? Simple, just link the GDT within a special section with identical virtual and physical addresses.

; 现在处于保护模式
; grub以保护模式启动内核,这样避免了每个内核进行自己切换
; grub引导后,GDT中基地址为0,并不开启分页机制
; grub调用操作系统后,A20必须打开,PG位必须清空,PE位必须设置
;
[BITS 32]      ; 32 bit code
[global start] ; make 'start' function global
[extern kmain] ; our C kernel main

; Multiboot constants
MULTIBOOT_PAGE_ALIGNequ 1<<0
MULTIBOOT_MEMORY_INFOequ 1<<1
MULTIBOOT_HEADER_MAGICequ 0x1BADB002
MULTIBOOT_HEADER_FLAGSequ MULTIBOOT_PAGE_ALIGN | MULTIBOOT_MEMORY_INFO
MULTIBOOT_CHECKSUMequ -(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS)

; Multiboot header (needed to boot from GRUB)
ALIGN 4
multiboot_header:
    dd MULTIBOOT_HEADER_MAGIC
    dd MULTIBOOT_HEADER_FLAGS
    dd MULTIBOOT_CHECKSUM

; the kernel entry point
start:
    ; here's the trick: we load a GDT with a base address
    ; of 0x40000000 for the code (0x08) and data (0x10) segments
    ; 查看选择子结构:后面还有3个位: TI(1)+RPL(2)。因此index为1的选择子为0b1000=0x8;同理index为2的选择子为0b10000=0x10
    lgdt [trickgdt]
    mov ax, 0x10
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax

    ; jump to the higher half kernel
    jmp 0x08:higherhalf

higherhalf:
    ; from now the CPU will translate automatically every address
    ; by adding the base 0x40000000

    mov esp, sys_stack ; set up a new stack for our kernel

    call kmain ; jump to our C kernel ;)

    ; just a simple protection...
    jmp $

[global gdt_flush] ; make 'gdt_flush' accessible from C code
[extern gp]        ; tells the assembler to look at C code for 'gp'

; this function does the same thing of the 'start' one, this time with
; the real GDT
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

[section .setup] ; tells the assembler to include this data in the '.setup' section

trickgdt:
    dw gdt_end - gdt - 1 ; size of the GDT
    dd gdt ; linear address of GDT

gdt:
    dd 0, 0; null gate
    ;type 9A:描述符的属性A为执行/读, 2为读/写; 9A和92的9: 0b1001,末尾的1表示描述符类型,1表示代码段和数据段
    db 0xFF, 0xFF, 0, 0, 0, 10011010b, 11001111b, 0x40; code selector 0x08: base 0x40000000, limit 0xFFFFFFFF, type 0x9A, granularity 0xCF
    db 0xFF, 0xFF, 0, 0, 0, 10010010b, 11001111b, 0x40; data selector 0x10: base 0x40000000, limit 0xFFFFFFFF, type 0x92, granularity 0xCF

gdt_end:

[section .bss]

resb 0x1000
sys_stack:
    ; our kernel stack


//虚拟地址0xC0100000 => 物理地址0x00100000
//jmp 0x08:highhalf
//0x08选择子表示的基地址为0x40000000
//物理地址 = 基地址 + 偏移地址
//这里基地址为GDT中指明的,偏移地址设定为虚拟地址,因此物理地址=GDT基地址+虚拟地址
//物理地址 = 0x40000000 + 0xC0100000 = 0x00100000(1M)

The C part

At this point the CPU will be executing our kmain function. For this tutorial it will be very small and easy: just call some functions and print a message.

// We declare a pointer to the VGA array and its attributes
//注意: 物理地址0xB8000为显存,这里lgdt并且设置页表之后,虚拟地址0xB8000指向物理地址0xB8000。当然这个地方也可以设置为虚拟地址0xC00B8000,得到的物理地址也是0xB8000
//主要在于设置的两个页表:虚拟地址0x000000000(1M)和0xC0000000(1M),分别指向第0号页表和第768号页表,这两个页表中的表项都是0-4M之间的物理内存,最终两个虚拟地址都指向了物理地址0x00000000(1M)
unsigned short *video = (unsigned short *)0xB8000; // We could also use the virtual address 0xC00B8000
unsigned char attrib = 0xF; // White text on black background

// Extern functions
void gdt_install();
void init_paging();

// Clears the screen
void cls()
{
    int i = 0;

    for (i = 0; i < 80 * 25; i++)
        video[i] = (attrib << 8) | 0;
}

// Prints the welcome message ;)
void helloworld()
{
    char msg[] = "Hello, World!";
    int i = 0;

    for (i = 0; msg[i] != '\0'; i++)
        video[i] = (attrib << 8) | msg[i];
}

// Our kernel's first function: kmain
void kmain()
{
    // FIRST enable paging and THEN load the real GDT!
    init_paging();
    gdt_install();

    // We clear the screen and print our welcome message
    cls();
    helloworld();

    // Hang up the computer
    for (;;);
}

Note that you must enable paging before loading the new GDT as the CPU, once loaded the GDT with 0x0 as base address, will be still using virtual addresses and if paging isn't enabled... triple fault!

// Declare the page directory and a page table, both 4kb-aligned
unsigned long kernelpagedir[1024] __attribute__ ((aligned (4096)));
unsigned long lowpagetable[1024] __attribute__ ((aligned (4096)));

// This function fills the page directory and the page table,
// then enables paging by putting the address of the page directory
// into the CR3 register and setting the 31st bit into the CR0 one
void init_paging()
{
    // Pointers to the page directory and the page table
    void *kernelpagedirPtr = 0;
    void *lowpagetablePtr = 0;
    int k = 0;

    kernelpagedirPtr = (char *)kernelpagedir + 0x40000000;// Translate the page directory from
                                // virtual address to physical address
    lowpagetablePtr = (char *)lowpagetable + 0x40000000;// Same for the page table

    // Counts from 0 to 1023 to...
    for (k = 0; k < 1024; k++)
    {
        lowpagetable[k] = (k * 4096) | 0x3;// ...map the first 4MB of memory into the page table...
        kernelpagedir[k] = 0;// ...and clear the page directory entries
    }

    // Fills the addresses 0...4MB and 3072MB...3076MB of the page directory
    // with the same page table
    kernelpagedir[0] = (unsigned long)lowpagetablePtr | 0x3;
    kernelpagedir[768] = (unsigned long)lowpagetablePtr | 0x3;

    // Copies the address of the page directory into the CR3 register and, finally, enables paging!

    asm volatile ("mov %0, %%eax\n"
            "mov %%eax, %%cr3\n"
            "mov %%cr0, %%eax\n"
            "orl $0x80000000, %%eax\n"
            "mov %%eax, %%cr0\n" :: "m" (kernelpagedirPtr));
}

//虚拟地址0xC0100000 => 物理地址0x00100000
//页转化: 10 | 10 | 12
//前10位为页目录表中索引,中间10位为页表中索引,后12位为页内偏移
//0xC0100000 => 0x300 | 0x100 | 0x000
//0x300=768,页目录表的第768项,这里指向lowpagetablePtr
//0x100=256,页表的第256项,物理地址为256*4K=1M=0x100<<12=0x100000
//0x000=0,页内偏移为0
//最终物理地址为0x100000

//unsigned short *video = (unsigned short *)0xB8000; // We could also use the virtual address 0xC00B8000
//虚拟地址0xB8000 => 物理地址0xB8000
//0x000B8000 => 0x000 | 0x0B8 |0x000
//0x000: 页目录表的第0项,指向lowpagetablePtr
//0x0B8=184: 页表的第184项目,物理地址为184*4K=0x0B8<<12 = 0x0B8000
//0x000: 页内偏移为0
//最终物理地址为0x0B8000


And here is the gdt_install function. I think it doesn't need any explanation as it has been taken from Bran's Kernel Development Tutorial, so read that if you need more information.

// Defines the structures of a GDT entry and of a GDT pointer

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

struct gdt_ptr
{
    unsigned short limit;
    unsigned int base;
} __attribute__((packed));

// We'll need at least 3 entries in our GDT...

struct gdt_entry gdt[3];
struct gdt_ptr gp;

// Extern assembler function
void gdt_flush();

// Very simple: fills a GDT entry using the parameters
void gdt_set_gate(int num, unsigned long base, unsigned long limit, unsigned char access, unsigned char gran)
{
    gdt[num].base_low = (base & 0xFFFF);
    gdt[num].base_middle = (base >> 16) & 0xFF;
    gdt[num].base_high = (base >> 24) & 0xFF;

    gdt[num].limit_low = (limit & 0xFFFF);
    gdt[num].granularity = ((limit >> 16) & 0x0F);

    gdt[num].granularity |= (gran & 0xF0);
    gdt[num].access = access;
}

// Sets our 3 gates and installs the real GDT through the assembler function
void gdt_install()
{
    gp.limit = (sizeof(struct gdt_entry) * 6) - 1;
    gp.base = (unsigned int)&gdt;

    gdt_set_gate(0, 0, 0, 0, 0);
    gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF);
    gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF);

    gdt_flush();
}

The linker script

Our last file is the linker script. Let's give a look at it:

OUTPUT_FORMAT("elf32-i386")
ENTRY(start)

SECTIONS
{
    . = 0x100000;

    .setup :
    {
        *(.setup)
    }

    . += 0xC0000000;

    .text : AT(ADDR(.text) - 0xC0000000)
    {
        *(.text)
    }

    .data ALIGN (4096) : AT(ADDR(.data) - 0xC0000000)
    {
        *(.data)
        *(.rodata*)
    }

    .bss ALIGN (4096) : AT(ADDR(.bss) - 0xC0000000)
    {
        *(COMMON*)
        *(.bss*)
    }
}

As you can see, we link the .setup section to the lower half (0x100000) and the whole kernel to the higher half (0xC0100000). A simple and elegant solution!
Conclusion

I hope this tutorial will help you writing your own higher half kernel. For any question please post on the forum!

阅读(2007) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~