《一个操作系统的实现》笔记(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){//... }
- 《一个操作系统的实现》笔记(5)--内核雏形
- 一个操作系统的实现---内核雏形
- 一个操作系统的实现---内核雏形
- 一个操作系统的实现(7):内核雏形
- 《一个操作系统的实现》学习笔记5
- 一个操作系统的实现笔记
- 操作系统内核Hack:(四)内核雏形
- hurlex 操作系统内核实现 笔记
- 《Orange'S:一个操作系统的实现》学习笔记(一)
- 《Orange'S:一个操作系统的实现》学习笔记(二)
- 《Orange'S:一个操作系统的实现》学习笔记(四)
- 《Orange'S:一个操作系统的实现》学习笔记(四)
- 《Orange'S:一个操作系统的实现》学习笔记(1)
- 《一个操作系统的实现》笔记(2)--保护模式
- 《一个操作系统的实现》笔记(4)-- Boot&Loader
- 《一个操作系统的实现》笔记(6)--进程
- 学习笔记:一个操作系统的实现--前言
- 《orange'S一个操作系统的实现》 笔记
- VS2017链接报错:fatal error LNK1318: 非意外的 PDB 错误
- Linux电源管理(五)thermal
- Java打印数组中不重复的元素和个数
- 10月7日 c语言 函数调用 输入两个整数,要求输出其中值较大者,要求用函数找到最大数
- C语言字符串创建方法
- 《一个操作系统的实现》笔记(5)--内核雏形
- CF343D Water Tree(线段树+dfs序+思路)
- Python3+phantomjs+selenium配置
- 对于超前,滞后,超前滞后使用范围
- 自学linux笔记
- Spring 4.x 配置类学习笔记
- SpringBoot创建一个最基本的项目
- 《一个操作系统的实现》笔记(6)--进程
- 关于求最大公约数经典算法---辗转相除法的思考