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

2018-06-08 11:35:30 浏览数 (1)


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


在Linux下用汇编写程序

示例:

代码语言:javascript复制
;hello.asm
[section .data] ; 数据在此

strHello    db  "Hello, world!", 0Ah
STRLEN      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”) 去掉符号表等内容,可起到对生成的可执行代码减肥之用。

代码语言:javascript复制
$ nasm -f elf hello.asm -o hello.o
$ ld -s hello.o -o hello
$ ./hello
Hello, world!
$

汇编和C互相调用

代码语言:javascript复制
;foo.asm
extern choose   ; int choose(int a, int b);

[section .data] ; 数据在此

num1st      dd  3
num2nd      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
代码语言:javascript复制
// bar.c
void myprint(char* msg, int len);

int choose(int a, int b)
{
    if(a >= b){
        myprint("the 1st onen", 13);
    }
    else{
        myprint("the 2nd onen", 13);
    }

    return 0;
}

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

代码语言:javascript复制
 $ 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中的值把内核中相应段放到正确的位置。

代码语言:javascript复制
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-4G
LABEL_DESC_FLAT_RW: Descriptor 0,      0fffffh, DA_DRW|DA_32|DA_LIMIT_4K;0-4G
LABEL_DESC_VIDEO:   Descriptor 0B8000h, 0ffffh, DA_DRW|DA_DPL3 ; 显存首地址

GdtLen      equ $ - LABEL_GDT
GdtPtr      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  0
TopOfStack  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以上的内存空间

代码语言:javascript复制
    ;***************************************************************
    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中,之后我们需要将它们都挪到内核中,以便于控制。


向内核交出控制权

代码语言:javascript复制
KernelEntryPointPhyAddr equ 030400h ; 
;...
    ;***************************************************************
    jmp SelectorFlatC:KernelEntryPointPhyAddr   ; 正式进入内核 *
    ;***************************************************************

切换堆栈和GDT

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

代码语言:javascript复制
;kernel.asm
SELECTOR_KERNEL_CS  equ 8
; 导入函数
extern  cstart
; 导入全局变量
extern  gdt_ptr

[SECTION .bss]
StackSpace      resb    2 * 1024
StackTop:       ; 栈顶

[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:csinit
csinit:     ; “这个跳转指令强制使用刚刚初始化的结构”——<<OS:D&I 2nd>> P90.

    push    0
    popfd   ; Pop top of stack into EFLAGS

    hlt
代码语言:javascript复制
//start.c
PUBLIC  u8          gdt_ptr[6]; // 0~15:Limit  16~47:Base
PUBLIC  DESCRIPTOR      gdt[GDT_SIZE];

PUBLIC void cstart()
{
    disp_str("nnnnnnnnnnnnnnn"
         "-----"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就会被调用处理了。

代码语言:javascript复制
;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
代码语言:javascript复制
;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 人点赞