前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《一个操作系统的实现》笔记(5)--内核雏形

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

作者头像
felix
发布2018-06-08 11:35:30
1.2K1
发布2018-06-08 11:35:30
举报
文章被收录于专栏:Felix的技术分享

我们希望自己的操作系统内核至少应该在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 one\n", 13);
    }
    else{
        myprint("the 2nd one\n", 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("\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就会被调用处理了。

代码语言: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)
{
//...   
}

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017年10月07日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 在Linux下用汇编写程序
    • 汇编和C互相调用
    • ELF文件格式
    • 把内核加载到内存
    • 重新放置内核
    • 向内核交出控制权
      • 切换堆栈和GDT
        • 添加中断处理
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档