博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
自制操作系统-kernel加载
阅读量:4149 次
发布时间:2019-05-25

本文共 11902 字,大约阅读时间需要 39 分钟。

kernel加载

1. 为什么需要ELF文件格式

  • 在操作系统的层面,任何一个程序在运行时就类似于将cs和ip指向了当前程序的入口地址, 但是此入口地址不一定在程序的起始处,因为起始处可能存在函数及其他变量声明; 那么现在的问题就是操作系统怎么去确定程序的起始地址在哪里。

    • 固定在程序开始部分设置元数据信息指定程序起始地址
    • 总结:ELF定义了程序元信息的格式
  • 程序中重要组成部分

    • 程序段、程序节、程序头表(program header table)、程序节头表(section header table), ELF header
    • 程序段在汇编中很常见,就类似我们常见的代码段和数据段, 程序节是组成程序段的元素。 由于程序中的程序段和程序节的数量不是固定的所以需要利用程序段表、程序节表来记录这两个信息。
    • 目前程序段和程序节都是由相关table描述其个数及大小信息, 但由于本省table也是不固定的所以需要ELF header描述table的大小及地址信息
  • C文件到32位ELF文件生成

    • 编译: gcc -m32 -c -o kernel.o kernel.c
    • 链接: ld -m elf_i386 kernel.o -Ttext 0xc0001500 -e main -o kernel.elf
      • -Ttext ADDRESS Set address of .text section
      • -m EMULATION Set emulation
      • -e ADDRESS, --entry ADDRESS Set start address
  • ELF文件格式图

2.内核加载

2.1 C语言编写简单kernel

int main(void) {
while(1); return 0;}

2.2 将内核写入磁盘

注意1: 需要指定为32位是因为我当前使用的ubantu是64位的

注意2: 目前0-1扇区给了boot程序, 2-11 十个扇区给了loader, 也就是kernel只能12扇区开始了

  • 编译: gcc -m32 -c -o kernel.o kernel.c

  • 连接: ld -m elf_i386 kernel.o -Ttext 0xc0001500 -e main -o kernel.bin

  • 写入磁盘: dd if=kernel.bin of=boot.img bs=512 count=200 seek=12 conv=notrunc

  • 为了后续方便将kernel的写盘操作作为脚本

gcc -m32 -c -o kernel.o kernel.c && ld -m elf_i386 kernel.o -Ttext 0xc0001500 -e main -o kernel.bin && dd if=kernel.bin of=boot.img  bs=512 count=200 seek=12  conv=notrunc

2.3 Loader加载内核

  • 需要修改部分

    • 将kernel加载到内存缓冲区(将kernel从硬盘加载到内存,)
    • 在分页后, 将加载进来的elf文件安置到相应的虚拟地址处,然后移交控制权
  • 内核加载函数

rd_disk_m_32:	   ;-------------------------------------------------------------------------------; eax=LBA扇区号; ebx=将数据写入的内存地址; ecx=读入的扇区数      mov esi,eax	   ; 备份eax      mov di,cx		   ; 备份扇区数到di;读写硬盘:;第1步:设置要读取的扇区数      mov dx,0x1f2      mov al,cl      out dx,al                  mov eax,esi;第2步:将LBA地址存入0x1f3 ~ 0x1f6      ;LBA地址7~0位写入端口0x1f3      mov dx,0x1f3                             out dx,al                                ;LBA地址15~8位写入端口0x1f4      mov cl,8      shr eax,cl      mov dx,0x1f4      out dx,al      ;LBA地址23~16位写入端口0x1f5      shr eax,cl      mov dx,0x1f5      out dx,al      shr eax,cl      and al,0x0f	   ;lba第24~27位      or al,0xe0	   ; 设置7~4位为1110,表示lba模式      mov dx,0x1f6      out dx,al;第3步:向0x1f7端口写入读命令,0x20       mov dx,0x1f7      mov al,0x20                              out dx,al;第4步:检测硬盘状态  .not_ready:		     	  ;测试0x1f7端口(status寄存器)的的BSY位      ;同一端口,写时表示写入命令字,读时表示读入硬盘状态      nop      in al,dx      and al,0x88	   ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙      cmp al,0x08      jnz .not_ready	   ;若未准备好,继续等。;第5步:从0x1f0端口读数据      mov ax, di	   ;di为要读取的扇区数,一个扇区有512字节,每次读入一个字,共需di*512/2次,所以di*256      mov dx, 256	         mul dx      mov cx, ax	         mov dx, 0x1f0  .go_on_read:      in ax,dx		      mov [ebx], ax      add ebx, 2      loop .go_on_read      ret

2.4 kernel程序映像构建

  • 如何将程序映射到0xc0001500

    • 0xc000 1500对应的物理地址就是0x1500也就是说我们只需要把kernel的入口函数(mian)加载到此处就行, 其余各个段加载到指定的虚拟地址处即可
  • 构建程序映像

;-----------------   将kernel.bin中的segment拷贝到编译的地址   -----------kernel_init:   xor eax, eax   xor ebx, ebx		;ebx记录程序头表地址   xor ecx, ecx		;cx记录程序头表中的program header数量   xor edx, edx		;dx 记录program header尺寸,即e_phentsize   mov dx, [KERNEL_BIN_BASE_ADDR + 42]	  ; 偏移文件42字节处的属性是e_phentsize,表示program header大小   mov ebx, [KERNEL_BIN_BASE_ADDR + 28]   ; 偏移文件开始部分28字节的地方是e_phoff,表示第1 个program header在文件中的偏移量					  ; 其实该值是0x34,不过还是谨慎一点,这里来读取实际值   add ebx, KERNEL_BIN_BASE_ADDR   mov cx, [KERNEL_BIN_BASE_ADDR + 44]    ; 偏移文件开始部分44字节的地方是e_phnum,表示有几个program header.each_segment:   cmp byte [ebx + 0], PT_NULL		  ; 若p_type等于 PT_NULL,说明此program header未使用。   je .PTNULL   ;为函数memcpy压入参数,参数是从右往左依然压入.函数原型类似于 memcpy(dst,src,size)   push dword [ebx + 16]		  ; program header中偏移16字节的地方是p_filesz,压入函数memcpy的第三个参数:size   mov eax, [ebx + 4]			  ; 距程序头偏移量为4字节的位置是p_offset   add eax, KERNEL_BIN_BASE_ADDR	  ; 加上kernel.bin被加载到的物理地址,eax为该段的物理地址   push eax				  ; 压入函数memcpy的第二个参数:源地址   push dword [ebx + 8]			  ; 压入函数memcpy的第一个参数:目的地址,偏移程序头8字节的位置是p_vaddr,这就是目的地址   call mem_cpy				  ; 调用mem_cpy完成段复制   add esp,12				  ; 清理栈中压入的三个参数.PTNULL:   add ebx, edx				  ; edx为program header大小,即e_phentsize,在此ebx指向下一个program header    loop .each_segment   ret;----------  逐字节拷贝 mem_cpy(dst,src,size) ------------;输入:栈中三个参数(dst,src,size);输出:无;---------------------------------------------------------mem_cpy:		         cld   push ebp   mov ebp, esp   push ecx		   ; rep指令用到了ecx,但ecx对于外层段的循环还有用,故先入栈备份   mov edi, [ebp + 8]	   ; dst   mov esi, [ebp + 12]	   ; src   mov ecx, [ebp + 16]	   ; size   rep movsb		   ; 逐字节拷贝,rep重复执行movsb ecx次   ;恢复环境   pop ecx		   pop ebp   ret

2.5 移交控制权

  • 在开启了分页模式之后构建映像并移交控制权
jmp SELECTOR_CODE:enter_kernelenter_kernel:       call kernel_init   mov esp, 0xc009f000   ; 跳转程序   jmp KERNEL_ENTRY_POINT   hlt

3.完整代码

3.1 loader.asm

%include "common_info.conf"section loader vstart=LOADER_BASE_ADDRLOADER_STACK_TOP equ LOADER_BASE_ADDRjmp loader_start					   ;构建gdt及其内部的描述符   GDT_BASE:   	dd    0x00000000 	       		dd    0x00000000   CODE_DESC:  	dd    0x0000FFFF 	       		dd    DESC_CODE_HIGH4   DATA_STACK_DESC:  	dd    0x0000FFFF		     			dd    DESC_DATA_HIGH4;因为单位是4K, 所以limit=(0xbffff-0xb8000)/4k=0x7   VIDEO_DESC: 	dd    0x80000007	       	       		dd    DESC_VIDEO_HIGH4     GDT_SIZE   equ   $ - GDT_BASE   GDT_LIMIT   equ   GDT_SIZE -	1    times 10 dq 0   ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0   SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0            SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0	 ; 同上   SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0	 ; 同上    ;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址   gdt_ptr  dw  GDT_LIMIT 	    	dd  GDT_BASEloader_start:    mov	 sp, LOADER_BASE_ADDR    mov byte [gs:0xa0],'L'    mov byte [gs:0xa1],0xA4       mov byte [gs:0xa2],'O'    mov byte [gs:0xa3],0xA4    mov byte [gs:0xa4],'A'    mov byte [gs:0xa5],0xA4    mov byte [gs:0xa6],'D'    mov byte [gs:0xa7],0xA4    mov byte [gs:0xa8],'E'    mov byte [gs:0xa9],0xA4    mov byte [gs:0xaa],'R'    mov byte [gs:0xab],0xA4;----------------------------------------   准备进入保护模式   -----------------------------;1 打开A20;2 加载gdt;3 将cr0的pe位置1   ;-----------------  打开A20  ----------------   in al,0x92   or al,0000_0010B   out 0x92,al   ;-----------------  加载GDT  ----------------   lgdt [gdt_ptr]   ;-----------------  cr0第0位置1  ----------------   mov eax, cr0   or eax, 0x00000001   mov cr0, eax   jmp  SELECTOR_CODE:p_mode_start	   [bits 32]p_mode_start:   mov ax, SELECTOR_DATA   mov ds, ax   mov es, ax   mov ss, ax   mov esp,LOADER_STACK_TOP   mov ax, SELECTOR_VIDEO   mov gs, ax; 加载kernel   mov eax, KERNEL_START_SECTOR        ; kernel.bin所在的扇区号   mov ebx, KERNEL_BIN_BASE_ADDR       ; 从磁盘读出后,写入到ebx指定的地址   mov ecx, 200			       ; 读入的扇区数   call rd_disk_m_32; 创建页目录及页表并初始化页内存位图   call setup_page   ;要将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载   sgdt [gdt_ptr]	      ; 存储到原来gdt所有的位置   ;将gdt描述符中视频段描述符中的段基址+0xc0000000   mov ebx, [gdt_ptr + 2]     or dword [ebx + 0x18 + 4], 0xc0000000      ;视频段是第3个段描述符,每个描述符是8字节,故0x18。					      ;段描述符的高4字节的最高位是段基址的31~24位   ;将gdt的基址加上0xc0000000使其成为内核所在的高地址   add dword [gdt_ptr + 2], 0xc0000000 ; 将栈指针同样映射到内核地址   add esp, 0xc0000000          ; 把页目录地址赋给cr3   mov eax, PAGE_DIR_TABLE_POS   mov cr3, eax   ; 打开cr0的pg位(第31位)   mov eax, cr0   or eax, 0x80000000   mov cr0, eax   ;在开启分页后,用gdt新的地址重新加载   lgdt [gdt_ptr]             ; 重新加载   mov byte [gs:320], 'O'   mov byte [gs:322], 'K'   jmp SELECTOR_CODE:enter_kernelenter_kernel:       call kernel_init   mov esp, 0xc009f000   ; 跳转程序   jmp KERNEL_ENTRY_POINT   hlt;-------------   创建页目录及页表   ---------------setup_page:;先把页目录占用的空间逐字节清0   mov ecx, 4096   mov esi, 0.clear_page_dir:   mov byte [PAGE_DIR_TABLE_POS + esi], 0   inc esi   loop .clear_page_dir;开始创建页目录项(PDE).create_pde:				     ; 创建Page Directory Entry   mov eax, PAGE_DIR_TABLE_POS   add eax, 0x1000 			     ; 此时eax为第一个页表的位置及属性   mov ebx, eax				     ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。;   下面将页目录项0和0xc00都存为第一个页表的地址,;   一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,;   这是为将地址映射为内核地址做准备   or eax, PG_US_U | PG_RW_W | PG_P	     ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.   mov [PAGE_DIR_TABLE_POS + 0x0], eax       ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(7)   mov [PAGE_DIR_TABLE_POS + 0xc00], eax     ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,					     ; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.   sub eax, 0x1000   mov [PAGE_DIR_TABLE_POS + 4092], eax	     ; 使最后一个目录项指向页目录表自己的地址;下面创建页表项(PTE)   mov ecx, 256				     ; 1M低端内存 / 每页大小4k = 256   mov esi, 0   mov edx, PG_US_U | PG_RW_W | PG_P	     ; 属性为7,US=1,RW=1,P=1.create_pte:				     ; 创建Page Table Entry   mov [ebx+esi*4],edx			     ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址    add edx,4096   inc esi   loop .create_pte;创建内核其它页表的PDE   mov eax, PAGE_DIR_TABLE_POS   add eax, 0x2000 		     ; 此时eax为第二个页表的位置   or eax, PG_US_U | PG_RW_W | PG_P  ; 页目录项的属性US,RW和P位都为1   mov ebx, PAGE_DIR_TABLE_POS   mov ecx, 254			     ; 范围为第769~1022的所有目录项数量   mov esi, 769.create_kernel_pde:   mov [ebx+esi*4], eax   inc esi   add eax, 0x1000   loop .create_kernel_pde   ret;-----------------   将kernel.bin中的segment拷贝到编译的地址   -----------kernel_init:   xor eax, eax   xor ebx, ebx		;ebx记录程序头表地址   xor ecx, ecx		;cx记录程序头表中的program header数量   xor edx, edx		;dx 记录program header尺寸,即e_phentsize   mov dx, [KERNEL_BIN_BASE_ADDR + 42]	  ; 偏移文件42字节处的属性是e_phentsize,表示program header大小   mov ebx, [KERNEL_BIN_BASE_ADDR + 28]   ; 偏移文件开始部分28字节的地方是e_phoff,表示第1 个program header在文件中的偏移量					  ; 其实该值是0x34,不过还是谨慎一点,这里来读取实际值   add ebx, KERNEL_BIN_BASE_ADDR   mov cx, [KERNEL_BIN_BASE_ADDR + 44]    ; 偏移文件开始部分44字节的地方是e_phnum,表示有几个program header.each_segment:   cmp byte [ebx + 0], PT_NULL		  ; 若p_type等于 PT_NULL,说明此program header未使用。   je .PTNULL   ;为函数memcpy压入参数,参数是从右往左依然压入.函数原型类似于 memcpy(dst,src,size)   push dword [ebx + 16]		  ; program header中偏移16字节的地方是p_filesz,压入函数memcpy的第三个参数:size   mov eax, [ebx + 4]			  ; 距程序头偏移量为4字节的位置是p_offset   add eax, KERNEL_BIN_BASE_ADDR	  ; 加上kernel.bin被加载到的物理地址,eax为该段的物理地址   push eax				  ; 压入函数memcpy的第二个参数:源地址   push dword [ebx + 8]			  ; 压入函数memcpy的第一个参数:目的地址,偏移程序头8字节的位置是p_vaddr,这就是目的地址   call mem_cpy				  ; 调用mem_cpy完成段复制   add esp,12				  ; 清理栈中压入的三个参数.PTNULL:   add ebx, edx				  ; edx为program header大小,即e_phentsize,在此ebx指向下一个program header    loop .each_segment   ret;----------  逐字节拷贝 mem_cpy(dst,src,size) ------------;输入:栈中三个参数(dst,src,size);输出:无;---------------------------------------------------------mem_cpy:		         cld   push ebp   mov ebp, esp   push ecx		   ; rep指令用到了ecx,但ecx对于外层段的循环还有用,故先入栈备份   mov edi, [ebp + 8]	   ; dst   mov esi, [ebp + 12]	   ; src   mov ecx, [ebp + 16]	   ; size   rep movsb		   ; 逐字节拷贝,rep重复执行movsb ecx次   ;恢复环境   pop ecx		   pop ebp   retrd_disk_m_32:	   ;-------------------------------------------------------------------------------; eax=LBA扇区号; ebx=将数据写入的内存地址; ecx=读入的扇区数      mov esi,eax	   ; 备份eax      mov di,cx		   ; 备份扇区数到di;读写硬盘:;第1步:设置要读取的扇区数      mov dx,0x1f2      mov al,cl      out dx,al                  mov eax,esi;第2步:将LBA地址存入0x1f3 ~ 0x1f6      ;LBA地址7~0位写入端口0x1f3      mov dx,0x1f3                             out dx,al                                ;LBA地址15~8位写入端口0x1f4      mov cl,8      shr eax,cl      mov dx,0x1f4      out dx,al      ;LBA地址23~16位写入端口0x1f5      shr eax,cl      mov dx,0x1f5      out dx,al      shr eax,cl      and al,0x0f	   ;lba第24~27位      or al,0xe0	   ; 设置7~4位为1110,表示lba模式      mov dx,0x1f6      out dx,al;第3步:向0x1f7端口写入读命令,0x20       mov dx,0x1f7      mov al,0x20                              out dx,al;第4步:检测硬盘状态  .not_ready:		     	  ;测试0x1f7端口(status寄存器)的的BSY位      ;同一端口,写时表示写入命令字,读时表示读入硬盘状态      nop      in al,dx      and al,0x88	   ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙      cmp al,0x08      jnz .not_ready	   ;若未准备好,继续等。;第5步:从0x1f0端口读数据      mov ax, di	   ;di为要读取的扇区数,一个扇区有512字节,每次读入一个字,共需di*512/2次,所以di*256      mov dx, 256	         mul dx      mov cx, ax	         mov dx, 0x1f0  .go_on_read:      in ax,dx		      mov [ebx], ax      add ebx, 2      loop .go_on_read      ret

3.2 common_info.conf

  • 在配置文件中追加以下内容
;-------------	 loader和kernel   ----------KERNEL_BIN_BASE_ADDR equ 0x70000KERNEL_START_SECTOR equ 0x9KERNEL_ENTRY_POINT equ 0xc0001500PT_NULL equ 0

转载地址:http://dkpti.baihongyu.com/

你可能感兴趣的文章
【JavaScript 教程】面向对象编程——实例对象与 new 命令
查看>>
我在网易做了6年前端,想给求职者4条建议
查看>>
SQL1015N The database is in an inconsistent state. SQLSTATE=55025
查看>>
RQP-DEF-0177
查看>>
MySQL字段类型的选择与MySQL的查询效率
查看>>
Java的Properties配置文件用法【续】
查看>>
JAVA操作properties文件的代码实例
查看>>
IPS开发手记【一】
查看>>
Java通用字符处理类
查看>>
文件上传时生成“日期+随机数”式文件名前缀的Java代码
查看>>
Java代码检查工具Checkstyle常见输出结果
查看>>
北京十大情人分手圣地
查看>>
Android自动关机代码
查看>>
Android中启动其他Activity并返回结果
查看>>
2009年33所高校被暂停或被限制招生
查看>>
GlassFish 部署及应用入门
查看>>
X-code7 beta error: warning: Is a directory
查看>>
Error: An App ID with identifier "*****" is not avaliable. Please enter a different string.
查看>>
3.5 YOLO9000: Better,Faster,Stronger(YOLO9000:更好,更快,更强)
查看>>
iOS菜鸟学习--如何避免两个按钮同时响应
查看>>