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


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


在Linux下用汇编写程序

示例:

;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”) 去掉符号表等内容,可起到对生成的可执行代码减肥之用。

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

汇编和C互相调用

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

 $ 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-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以上的内存空间

    ;***************************************************************
    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.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
//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就会被调用处理了。

;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 条评论
登录 后参与评论

相关文章

来自专栏一英里广度一英寸深度的学习

Zookeeper 分布式环境中的注册表

ZooKeeper 典型的应用场景,限于篇幅就不详细展开,百度或https://www.jianshu.com/p/1e052bddba80

1383
来自专栏遊俠扎彪

如何在命令行中处理CSV文件

CSV,全称Comma-Separated Values。CSV文件是每一行都是以逗号分隔的纯文本文件。

3360
来自专栏小特工作室

1分钟生成Net对象的注释

      我们在开发过程中,肯定会有几个项目作为基础项目,存放一些比较常用的类和方法,供其他项目使用.一般来说,方法实现以后,就不想再去管它了,以致于新加入的...

2086
来自专栏xdecode

Java源码安全审查

最近业务需要出一份Java Web应用源码安全审查报告, 对比了市面上数种工具及其分析结果, 基于结果总结了一份规则库. 本文目录结构如下: 

7742
来自专栏码农阿宇

国内开源社区巨作AspectCore-Framework入门

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技...

2811
来自专栏YG小书屋

ElasticSearch 5.6源码解析HTTP/TCP请求

5183
来自专栏岑玉海

hbase源码系列(十)HLog与日志恢复

HLog概述 hbase在写入数据之前会先写入MemStore,成功了再写入HLog,当MemStore的数据丢失的时候,还可以用HLog的数据来进行恢复,下面...

4048
来自专栏圣杰的专栏

Asp.net mvc 知多少(二)

本系列主要翻译自《ASP.NET MVC Interview Questions and Answers 》- By Shailendra Chauhan,想...

2228
来自专栏日常分享

JavaWeb 基于Session的用户登陆注销实现

  通过Session来存储用户的部分登陆信息来验证用户是否在线,这应该时最容易实现的一种Web端方案,本文以SSM(Spring、SpringMVC、myBa...

6441
来自专栏开发之途

Android 模拟登陆网站实现移动客户端

5838

扫码关注云+社区

领取腾讯云代金券