《一个操作系统的实现》笔记(5)--内核雏形

来源:互联网 发布:eventbus 跳转传数据 编辑:程序博客网 时间:2024/06/16 00:41

我们希望自己的操作系统内核至少应该在Linux下用GCC编译链接。
Loader要做的事有两件:加载内核入内存、跳入保护模式。


在Linux下用汇编写程序

示例:

;hello.asm[section .data] ; 数据在此strHello    db  "Hello, world!", 0AhSTRLEN      equ $ - strHello[section .text] ; 代码在此global _start   ; 我们必须导出 _start 这个入口,以便让链接器识别_start:    mov edx, STRLEN    mov ecx, strHello    mov ebx, 1    mov eax, 4      ; sys_write    int 0x80        ; 系统调用    mov ebx, 0    mov eax, 1      ; sys_exit    int 0x80        ; 系统调用

编译链接方法:
(ld 的‘-s’选项意为“strip all”) 去掉符号表等内容,可起到对生成的可执行代码减肥之用。

$ nasm -f elf hello.asm -o hello.o$ ld -s hello.o -o hello$ ./helloHello, world!$

汇编和C互相调用

;foo.asmextern choose   ; int choose(int a, int b);[section .data] ; 数据在此num1st      dd  3num2nd      dd  4[section .text] ; 代码在此global _start   ; 我们必须导出 _start 这个入口,以便让链接器识别。global myprint  ; 导出这个函数为了让 bar.c 使用_start:    push    dword [num2nd]  ; `.    push    dword [num1st]  ;  |    call    choose      ;  | choose(num1st, num2nd);    add esp, 8      ; /    mov ebx, 0    mov eax, 1      ; sys_exit    int 0x80        ; 系统调用; void myprint(char* msg, int len)myprint:    mov edx, [esp + 8]  ; len    mov ecx, [esp + 4]  ; msg    mov ebx, 1    mov eax, 4      ; sys_write    int 0x80        ; 系统调用    ret
// bar.cvoid myprint(char* msg, int len);int choose(int a, int b){    if(a >= b){        myprint("the 1st one\n", 13);    }    else{        myprint("the 2nd one\n", 13);    }    return 0;}

编译链接方法
(ld 的‘-s’选项意为“strip all”)

 $ nasm -f elf foo.asm -o foo.o $ gcc -c bar.c -o bar.o $ ld -s hello.o bar.o -o foobar $ ./foobar the 2nd one $
  • 1、由于在bar.c中用到函数myprint(),所以要用关键字global将其导出。
  • 2、由于用到本文件外定义的choose(), 所以要用关键字extern声明。
  • 3、不管是myprint()还是choose(),都遵循C调用约定,后面的参数先入栈,并由调用者清理堆栈。

ELF文件格式

详见《程序员的自我修养》
这里只分析了ELF_HEADER和Program header部分。没有难度更大的动态链接部分。


把内核加载到内存

加载内核到内存这一步和引导扇区的工作非常相似,只是处理内核时我们需要根据Program header table中的值把内核中相应段放到正确的位置。

BaseOfLoader        equ  09000h ; LOADER.BIN 被加载到的位置 ----  段地址OffsetOfLoader      equ   0100h ; LOADER.BIN 被加载到的位置 ---- 偏移地址BaseOfLoaderPhyAddr equ BaseOfLoader*10h ; LOADER.BIN 被加载到的位置 ---- 物理地址;...; GDT;                            段基址     段界限, 属性LABEL_GDT:          Descriptor 0,            0, 0              ; 空描述符LABEL_DESC_FLAT_C:  Descriptor 0,      0fffffh, DA_CR|DA_32|DA_LIMIT_4K ;0-4GLABEL_DESC_FLAT_RW: Descriptor 0,      0fffffh, DA_DRW|DA_32|DA_LIMIT_4K;0-4GLABEL_DESC_VIDEO:   Descriptor 0B8000h, 0ffffh, DA_DRW|DA_DPL3 ; 显存首地址GdtLen      equ $ - LABEL_GDTGdtPtr      dw  GdtLen - 1              ; 段界限            dd  BaseOfLoaderPhyAddr + LABEL_GDT     ; 基地址; GDT 选择子SelectorFlatC       equ LABEL_DESC_FLAT_C   - LABEL_GDT;...LABEL_FILE_LOADED:    ;...    ; 下面准备跳入保护模式    ; 加载 GDTR    lgdt    [GdtPtr]    ;...    ; 真正进入保护模式    jmp dword SelectorFlatC:(BaseOfLoaderPhyAddr+LABEL_PM_START)    jmp $; 从此以后的代码在保护模式下执行 ----------------------------------------------------; 32 位代码段. 由实模式跳入 ---------------------------------------------------------[SECTION .s32]ALIGN   32[BITS   32]LABEL_PM_START:    mov ax, SelectorVideo    mov gs, ax    mov ax, SelectorFlatRW    mov ds, ax    mov es, ax    mov fs, ax    mov ss, ax    mov esp, TopOfStack    push    szMemChkTitle    call    DispStr    add esp, 4    call    DispMemInfo    call    SetupPaging    ;...;...[SECTION .data1]LABEL_DATA:; 实模式下使用这些符号; 字符串_szMemChkTitle: db "BaseAddrL BaseAddrH LengthLow LengthHigh   Type", 0Ah, 0_szRAMSize: db "RAM size:", 0;...; 堆栈就在数据段的末尾StackSpace: times   1024    db  0TopOfStack  equ BaseOfLoaderPhyAddr + $    ; 栈顶; SECTION .data1 之结束

重新放置内核

我们要做的工作是根据内核的Program header table的信息进行类似下面这个C语言语句的内存复制:
memcpy(pPHdr->p_vaddr,BaseOfKernelFilePhyAddr+pPHdr->p_offset,pPHdr->p_filesz)

现在的内存分布式这样的:0x90000开始的63KB留给了Loader.bin,0x80000开始的64KB留给了Kernel.bin,0x30000开始的320KB留给了整理后的内核,而页目录和页表被放置在了1MB以上的内存空间

    ;***************************************************************    jmp SelectorFlatC:KernelEntryPointPhyAddr   ; 正式进入内核 *    ;***************************************************************    ; 内存看上去是这样的:    ;              ┃                                    ┃    ;              ┃                 .                  ┃    ;              ┃                 .                  ┃    ;              ┃                 .                  ┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃■■■■■■■■■■■■■■■■■■┃    ;              ┃■■■■■■Page  Tables■■■■■■┃    ;              ┃■■■■■(大小由LOADER决定)■■■■┃    ;    00101000h ┃■■■■■■■■■■■■■■■■■■┃ PageTblBase    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃■■■■■■■■■■■■■■■■■■┃    ;    00100000h ┃■■■■Page Directory Table■■■■┃ PageDirBase  <- 1M    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃□□□□□□□□□□□□□□□□□□┃    ;       F0000h ┃□□□□□□□System ROM□□□□□□┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃□□□□□□□□□□□□□□□□□□┃    ;       E0000h ┃□□□□Expansion of system ROM □□┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃□□□□□□□□□□□□□□□□□□┃    ;       C0000h ┃□□□Reserved for ROM expansion□□┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃□□□□□□□□□□□□□□□□□□┃ B8000h ← gs    ;       A0000h ┃□□□Display adapter reserved□□□┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃□□□□□□□□□□□□□□□□□□┃    ;       9FC00h ┃□□extended BIOS data area (EBDA)□┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃■■■■■■■■■■■■■■■■■■┃    ;       90000h ┃■■■■■■■LOADER.BIN■■■■■■┃ somewhere in LOADER ← esp    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃■■■■■■■■■■■■■■■■■■┃    ;       80000h ┃■■■■■■■KERNEL.BIN■■■■■■┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃■■■■■■■■■■■■■■■■■■┃    ;       30000h ┃■■■■■■■■KERNEL■■■■■■■┃ 30400h ← KERNEL 入口 (KernelEntryPointPhyAddr)    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃                                    ┃    ;        7E00h ┃              F  R  E  E            ┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃■■■■■■■■■■■■■■■■■■┃    ;        7C00h ┃■■■■■■BOOT  SECTOR■■■■■■┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃                                    ┃    ;         500h ┃              F  R  E  E            ┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃□□□□□□□□□□□□□□□□□□┃    ;         400h ┃□□□□ROM BIOS parameter area □□┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇┃    ;           0h ┃◇◇◇◇◇◇Int  Vectors◇◇◇◇◇◇┃    ;              ┗━━━━━━━━━━━━━━━━━━┛ ← cs, ds, es, fs, ss    ;    ;    ;       ┏━━━┓       ┏━━━┓    ;       ┃■■■┃ 我们使用  ┃□□□┃ 不能使用的内存    ;       ┗━━━┛       ┗━━━┛    ;       ┏━━━┓       ┏━━━┓    ;       ┃      ┃ 未使用空间  ┃◇◇◇┃ 可以覆盖的内存    ;       ┗━━━┛       ┗━━━┛    ;    ; 注:KERNEL 的位置实际上是很灵活的,可以通过同时改变 LOAD.INC 中的    ;     KernelEntryPointPhyAddr 和 MAKEFILE 中参数 -Ttext 的值来改变。    ;     比如把 KernelEntryPointPhyAddr 和 -Ttext 的值都改为 0x400400,    ;     则 KERNEL 就会被加载到内存 0x400000(4M) 处,入口在 0x400400。    ;

此时,cs、ds、es、fs、ss表示的段统统指向内存地址0h,gs表示的段则指向显存,这是我们在进入保护模式之后设置的。
同时,esp、GDT等内容也在loader中,之后我们需要将它们都挪到内核中,以便于控制。


向内核交出控制权

KernelEntryPointPhyAddr equ 030400h ; ;...    ;***************************************************************    jmp SelectorFlatC:KernelEntryPointPhyAddr   ; 正式进入内核 *    ;***************************************************************

切换堆栈和GDT

gdt_ptr本质还是一块内存,我们可以用c语言来重新这个内存,然后再用汇编的lgdt指令重新加载它,这样就方便地达到了切换的目的了。
start.c中,我们成功的把gdt_ptr的值修改了,让它的基地址字段等于在start.c中定义的gdt数组变量。
memcpy把在loader.asm中定义的GDT表复制给gdt数组了。

;kernel.asmSELECTOR_KERNEL_CS  equ 8; 导入函数extern  cstart; 导入全局变量extern  gdt_ptr[SECTION .bss]StackSpace      resb    2 * 1024StackTop:       ; 栈顶[section .text] ; 代码在此global _start   ; 导出 _start_start:    ; 把 esp 从 LOADER 挪到 KERNEL    mov esp, StackTop   ; 堆栈在 bss 段中    sgdt    [gdt_ptr]   ; cstart() 中将会用到 gdt_ptr    call    cstart      ; 在此函数中改变了gdt_ptr,让它指向新的GDT    lgdt    [gdt_ptr]   ; 使用新的GDT    ;lidt   [idt_ptr]    jmp SELECTOR_KERNEL_CS:csinitcsinit:     ; “这个跳转指令强制使用刚刚初始化的结构”——<<OS:D&I 2nd>> P90.    push    0    popfd   ; Pop top of stack into EFLAGS    hlt
//start.cPUBLIC  u8          gdt_ptr[6]; // 0~15:Limit  16~47:BasePUBLIC  DESCRIPTOR      gdt[GDT_SIZE];PUBLIC void cstart(){    disp_str("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"         "-----\"cstart\" begins-----\n");    /* 将 LOADER 中的 GDT 复制到新的 GDT 中 */    memcpy(&gdt,                  /* New GDT */           (void*)(*((u32*)(&gdt_ptr[2]))),   /* Base  of Old GDT */           *((u16*)(&gdt_ptr[0])) + 1     /* Limit of Old GDT */        );    /* gdt_ptr[6] 共 6 个字节:0~15:Limit  16~47:Base。用作 sgdt/lgdt 的参数。*/    u16* p_gdt_limit = (u16*)(&gdt_ptr[0]);    u32* p_gdt_base  = (u32*)(&gdt_ptr[2]);    *p_gdt_limit = GDT_SIZE * sizeof(DESCRIPTOR) - 1;    *p_gdt_base  = (u32)&gdt;    disp_str("-----\"cstart\" ends-----\n");}

添加中断处理

从进程本身的角度看,它只不过是一段执行中的代码,它与操作系统的代码没有本质区别。
从操作系统角度看,进程必须是可控的,这就涉及到进程和操作系统之间执行的转换。因为CPU只有一个,同一时刻要么是客户进程在运行,要么是操作系统系统在运行。
如果实现进程,需要一种控制权转换机制,这种机制便是中断。

要添加中断处理,主要的工作有两项:设置8259A和建立IDT。

以一个divide_error为例,它在kernel.asm中是一个导出符号,而exception_handler是在protect.c中定义的一个处理程序,因为在init_prot初始化了,所以当发生divide_error中断时exception_handler就会被调用处理了。

;kernel.asm; 中断和异常 -- 异常divide_error:    push    0xFFFFFFFF  ; no err code    push    0       ; vector_no = 0    jmp exception;...exception:    call    exception_handler    add esp, 4*2    ; 让栈顶指向 EIP,堆栈中从顶向下依次是:EIP、CS、EFLAGS    hlt
;protect.c/* 门描述符 */typedef struct s_gate{    u16 offset_low; /* Offset Low */    u16 selector;   /* Selector */    u8  dcount;     /* 该字段只在调用门描述符中有效。如果在利用                   调用门调用子程序时引起特权级的转换和堆栈                   的改变,需要将外层堆栈中的参数复制到内层                   堆栈。该双字计数字段就是用于说明这种情况                   发生时,要复制的双字参数的数量。*/    u8  attr;       /* P(1) DPL(2) DT(1) TYPE(4) */    u16 offset_high;    /* Offset High */}GATE;PUBLIC void init_prot(){    init_8259A();    // 全部初始化成中断门(没有陷阱门)    init_idt_desc(INT_VECTOR_DIVIDE,    DA_386IGate,              divide_error,     PRIVILEGE_KRNL);    //...}/*  初始化 386 中断门*/PRIVATE void init_idt_desc(unsigned char vector, u8 desc_type,              int_handler handler, unsigned char privilege){    GATE *  p_gate  = &idt[vector];    u32 base    = (u32)handler;    p_gate->offset_low  = base & 0xFFFF;    p_gate->selector    = SELECTOR_KERNEL_CS;    p_gate->dcount      = 0;    p_gate->attr        = desc_type | (privilege << 5);    p_gate->offset_high = (base >> 16) & 0xFFFF;}PUBLIC void exception_handler(int vec_no,int err_code,int eip,int cs,int eflags){//...   }

阅读全文
0 0
原创粉丝点击