将内核加载到内存

news2024/12/26 21:02:25

文章目录

  • 前言
  • 前置知识
  • 代码
  • 实验操作

前言

本博客记录《操作系统真象还原》第五章第3个实验的操作~

实验环境:ubuntu18.04+VMware , Bochs下载安装

实验内容:将内核载入内存,初始化内核代码

实验原理

  1. 编写内核程序。
  2. 将内核程序用dd命令复制到磁盘。
  3. Loader.S 读取磁盘把内核加载到内存(内存缓冲区)中。接着初始化内核,即需要在分页后,将加载进来的 elf 内核文件安置到相应的虚拟内存地址,然后跳过去执行,从此 loader 的工作结束。

在这里插入图片描述

前置知识

elf文件格式(待填坑。。。)

代码

kernel/main.c

编写的内核程序。

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

boot/loader.S

loader.S文件需要修改的两处

  • 加载内核:需要把内核文件加载到内存缓冲区(单纯拷贝至内存)。加载到内存后,Loader还要通过分析其 elf 结构将其展开到新的位置。具体来说,内核在内存中有两份拷贝,一份是 elf 格式的原文件 kernel.bin,另一份是 loader 解析 elf 格式的 kernel.bin 后在内存中生成的内核映像,这个映像才是真正运行的内核

  • 初始化内核:需要在分页后,将加载进来的 elf 内核文件安置到相应的虚拟内存地址,然后跳过去
    执行,从此 loader 的工作结束。

Loader.S第一部分

这部分的作用是把内核文件从硬盘上加载到内存中。

; -------------------------   加载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
  • ecx 为 200,这是读入的扇区数,这里同用 dd 命令往硬盘上写入内核文件时的参数 count 保持一致。
  • call rd_disk_m_32,用于从硬盘上读取文件。
  • setup_page,开始创建页表。

Loader.S第二部分

这部分的作用是初始化内核。初始化内核就是根据 elf 规范将内核文件中的段(segment)展开到(复制到)内存中的相应位置。本实验选取了 0x1500 作为内核映像的入口地址。

;-----------------   将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		   ; 逐字节拷贝

   ;恢复环境
   pop ecx		
   pop ebp
   ret
  • kernel_init 的原理是分析程序中的每个段(segment),如果段类型不是 PT_NULL(空程序类型),就将该段拷贝到编译的地址中。
  • each_segment函数的作用是遍历每一个段的信息。
  • mem_cpy函数的作用是复制到段自身的虚拟地址处。

Loader.S第三部分

这部分作用是在开启分页后,用gdt新的地址重新加载。

  ;在开启分页后,用gdt新的地址重新加载
   lgdt [gdt_ptr]             ; 重新加载

   mov byte [gs:160], 'V'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:162], 'i'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:164], 'r'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:166], 't'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:168], 'u'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:170], 'a'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:172], 'l'     ;视频段段基址已经被更新,用字符v表示virtual addr
   
;;;;;;;;;;;;;;;;;;;;;;;;;;;;  此时不刷新流水线也没问题  ;;;;;;;;;;;;;;;;;;;;;;;;
;由于一直处在32位下,原则上不需要强制刷新,经过实际测试没有以下这两句也没问题.
;但以防万一,还是加上啦,免得将来出来莫句奇妙的问题.
   jmp SELECTOR_CODE:enter_kernel	  ;强制刷新流水线,更新gdt
enter_kernel:    
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   mov byte [gs:320], 'k'     ;视频段段基址已经被更新
   mov byte [gs:322], 'e'     ;视频段段基址已经被更新
   mov byte [gs:324], 'r'     ;视频段段基址已经被更新
   mov byte [gs:326], 'n'     ;视频段段基址已经被更新
   mov byte [gs:328], 'e'     ;视频段段基址已经被更新
   mov byte [gs:330], 'l'     ;视频段段基址已经被更新

   mov byte [gs:480], 'w'     ;视频段段基址已经被更新
   mov byte [gs:482], 'h'     ;视频段段基址已经被更新
   mov byte [gs:484], 'i'     ;视频段段基址已经被更新
   mov byte [gs:486], 'l'     ;视频段段基址已经被更新
   mov byte [gs:488], 'e'     ;视频段段基址已经被更新
   mov byte [gs:490], '('     ;视频段段基址已经被更新
   mov byte [gs:492], '1'     ;视频段段基址已经被更新
   mov byte [gs:494], ')'     ;视频段段基址已经被更新
   mov byte [gs:496], ';'     ;视频段段基址已经被更新
   call kernel_init
   mov esp, 0xc009f000
   jmp KERNEL_ENTRY_POINT                 ; 用地址0x1500访问测试,结果ok

完整代码

   %include "boot.inc"
   section loader vstart=LOADER_BASE_ADDR
;构建gdt及其内部的描述符
   GDT_BASE:   dd    0x00000000 
	       dd    0x00000000

   CODE_DESC:  dd    0x0000FFFF 
	       dd    DESC_CODE_HIGH4

   DATA_STACK_DESC:  dd    0x0000FFFF
		     dd    DESC_DATA_HIGH4

   VIDEO_DESC: dd    0x80000007	       ; limit=(0xbffff-0xb8000)/4k=0x7
	       dd    DESC_VIDEO_HIGH4  ; 此时dpl为0

   GDT_SIZE   equ   $ - GDT_BASE
   GDT_LIMIT   equ   GDT_SIZE -	1 
   times 60 dq 0					 ; 此处预留60个描述符的空位(slot)
   SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0         ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
   SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0	 ; 同上
   SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0	 ; 同上 

   ; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记。
   ; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900,
   ; 故total_mem_bytes内存中的地址是0xb00.将来在内核中咱们会引用此地址
   total_mem_bytes dd 0					 
   ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

   ;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
   gdt_ptr  dw  GDT_LIMIT 
	    dd  GDT_BASE

   ;人工对齐:total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节
   ards_buf times 244 db 0
   ards_nr dw 0		      ;用于记录ards结构体数量

   loader_start:
   
;-------  int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局  -------

   xor ebx, ebx		      ;第一次调用时,ebx值要为0
   mov edx, 0x534d4150	      ;edx只赋值一次,循环体中不会改变
   mov di, ards_buf	      ;ards结构缓冲区
.e820_mem_get_loop:	      ;循环获取每个ARDS内存范围描述结构
   mov eax, 0x0000e820	      ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
   mov ecx, 20		      ;ARDS地址范围描述符结构大小是20字节
   int 0x15
   jc .e820_failed_so_try_e801   ;若cf位为1则有错误发生,尝试0xe801子功能
   add di, cx		      ;使di增加20字节指向缓冲区中新的ARDS结构位置
   inc word [ards_nr]	      ;记录ARDS数量
   cmp ebx, 0		      ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个
   jnz .e820_mem_get_loop

;在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量。
   mov cx, [ards_nr]	      ;遍历每一个ARDS结构体,循环次数是ARDS的数量
   mov ebx, ards_buf 
   xor edx, edx		      ;edx为最大的内存容量,在此先清0
.find_max_mem_area:	      ;无须判断type是否为1,最大的内存块一定是可被使用
   mov eax, [ebx]	      ;base_add_low
   add eax, [ebx+8]	      ;length_low
   add ebx, 20		      ;指向缓冲区中下一个ARDS结构
   cmp edx, eax		      ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
   jge .next_ards
   mov edx, eax		      ;edx为总内存大小
.next_ards:
   loop .find_max_mem_area
   jmp .mem_get_ok

;------  int 15h ax = E801h 获取内存大小,最大支持4G  ------
; 返回后, ax cx 值一样,以KB为单位,bx dx值一样,以64KB为单位
; 在ax和cx寄存器中为低16M,在bx和dx寄存器中为16MB到4G。
.e820_failed_so_try_e801:
   mov ax,0xe801
   int 0x15
   jc .e801_failed_so_try88   ;若当前e801方法失败,就尝试0x88方法

;1 先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
   mov cx,0x400	     ;cx和ax值一样,cx用做乘数
   mul cx 
   shl edx,16
   and eax,0x0000FFFF
   or edx,eax
   add edx, 0x100000 ;ax只是15MB,故要加1MB
   mov esi,edx	     ;先把低15MB的内存容量存入esi寄存器备份

;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
   xor eax,eax
   mov ax,bx		
   mov ecx, 0x10000	;0x10000十进制为64KB
   mul ecx		;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax.
   add esi,eax		;由于此方法只能测出4G以内的内存,故32位eax足够了,edx肯定为0,只加eax便可
   mov edx,esi		;edx为总内存大小
   jmp .mem_get_ok

;-----------------  int 15h ah = 0x88 获取内存大小,只能获取64M之内  ----------
.e801_failed_so_try88: 
   ;int 15后,ax存入的是以kb为单位的内存容量
   mov  ah, 0x88
   int  0x15
   jc .error_hlt
   and eax,0x0000FFFF
      
   ;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
   mov cx, 0x400     ;0x400等于1024,将ax中的内存容量换为以byte为单位
   mul cx
   shl edx, 16	     ;把dx移到高16位
   or edx, eax	     ;把积的低16位组合到edx,为32位的积
   add edx,0x100000  ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB

.mem_get_ok:
   mov [total_mem_bytes], edx	 ;将内存换为byte单位后存入total_mem_bytes处。


;-----------------   准备进入保护模式   -------------------
;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 dword SELECTOR_CODE:p_mode_start	     ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
					     ; 这将导致之前做的预测失效,从而起到了刷新的作用。
.error_hlt:		      ;出错则挂起
   hlt

[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:160], 'V'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:162], 'i'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:164], 'r'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:166], 't'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:168], 'u'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:170], 'a'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:172], 'l'     ;视频段段基址已经被更新,用字符v表示virtual addr
   
;;;;;;;;;;;;;;;;;;;;;;;;;;;;  此时不刷新流水线也没问题  ;;;;;;;;;;;;;;;;;;;;;;;;
;由于一直处在32位下,原则上不需要强制刷新,经过实际测试没有以下这两句也没问题.
;但以防万一,还是加上啦,免得将来出来莫句奇妙的问题.
   jmp SELECTOR_CODE:enter_kernel	  ;强制刷新流水线,更新gdt
enter_kernel:    
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   mov byte [gs:320], 'k'     ;视频段段基址已经被更新
   mov byte [gs:322], 'e'     ;视频段段基址已经被更新
   mov byte [gs:324], 'r'     ;视频段段基址已经被更新
   mov byte [gs:326], 'n'     ;视频段段基址已经被更新
   mov byte [gs:328], 'e'     ;视频段段基址已经被更新
   mov byte [gs:330], 'l'     ;视频段段基址已经被更新

   mov byte [gs:480], 'w'     ;视频段段基址已经被更新
   mov byte [gs:482], 'h'     ;视频段段基址已经被更新
   mov byte [gs:484], 'i'     ;视频段段基址已经被更新
   mov byte [gs:486], 'l'     ;视频段段基址已经被更新
   mov byte [gs:488], 'e'     ;视频段段基址已经被更新
   mov byte [gs:490], '('     ;视频段段基址已经被更新
   mov byte [gs:492], '1'     ;视频段段基址已经被更新
   mov byte [gs:494], ')'     ;视频段段基址已经被更新
   mov byte [gs:496], ';'     ;视频段段基址已经被更新
   call kernel_init
   mov esp, 0xc009f000
   jmp KERNEL_ENTRY_POINT                 ; 用地址0x1500访问测试,结果ok

;-----------------   将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		   ; 逐字节拷贝

   ;恢复环境
   pop ecx		
   pop ebp
   ret


;-------------   创建页目录及页表   ---------------
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)及属性(3)
   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  ; 页目录项的属性RW和P位为1,US为0
   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


;-------------------------------------------------------------------------------
			   ;功能:读取硬盘n个扇区
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	   ;恢复ax

;第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

;;;;;;; 至此,硬盘控制器便从指定的lba地址(eax)处,读出连续的cx个扇区,下面检查硬盘状态,不忙就能把这cx个扇区的数据读出来

;第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	   ;以下从硬盘端口读数据用insw指令更快捷,不过尽可能多的演示命令使用,
			   ;在此先用这种方法,在后面内容会用到insw和outsw等

      mov dx, 256	   ;di为要读取的扇区数,一个扇区有512字节,每次读入一个字,共需di*512/2次,所以di*256
      mul dx
      mov cx, ax	   
      mov dx, 0x1f0
  .go_on_read:
      in ax,dx		
      mov [ebx], ax
      add ebx, 2
			  ; 由于在实模式下偏移地址为16位,所以用bx只会访问到0~FFFFh的偏移。
			  ; loader的栈指针为0x900,bx为指向的数据输出缓冲区,且为16位,
			  ; 超过0xffff后,bx部分会从0开始,所以当要读取的扇区数过大,待写入的地址超过bx的范围时,
			  ; 从硬盘上读出的数据会把0x0000~0xffff的覆盖,
			  ; 造成栈被破坏,所以ret返回时,返回地址被破坏了,已经不是之前正确的地址,
			  ; 故程序出会错,不知道会跑到哪里去。
			  ; 所以改为ebx代替bx指向缓冲区,这样生成的机器码前面会有0x66和0x67来反转。
			  ; 0X66用于反转默认的操作数大小! 0X67用于反转默认的寻址方式.
			  ; cpu处于16位模式时,会理所当然的认为操作数和寻址都是16位,处于32位模式时,
			  ; 也会认为要执行的指令是32位.
			  ; 当我们在其中任意模式下用了另外模式的寻址方式或操作数大小(姑且认为16位模式用16位字节操作数,
			  ; 32位模式下用32字节的操作数)时,编译器会在指令前帮我们加上0x66或0x67,
			  ; 临时改变当前cpu模式到另外的模式下.
			  ; 假设当前运行在16位模式,遇到0X66时,操作数大小变为32位.
			  ; 假设当前运行在32位模式,遇到0X66时,操作数大小变为16位.
			  ; 假设当前运行在16位模式,遇到0X67时,寻址方式变为32位寻址
			  ; 假设当前运行在32位模式,遇到0X67时,寻址方式变为16位寻址.

      loop .go_on_read

更多详细部分请看原书。

实验操作

1.创建内核程序并更新之前的文件

(base) user@ubuntu:/home/cooiboi/bochs$ sudo mkdir kernel
[sudo] password for user: 
(base) user@ubuntu:/home/cooiboi/bochs$ cd kernel/
(base) user@ubuntu:/home/cooiboi/bochs/kernel$ sudo vim main.c
(base) user@ubuntu:/home/cooiboi/bochs/include$ sudo vim boot.inc
(base) user@ubuntu:/home/cooiboi/bochs/boot$ sudo vim loader.S

2.编译loader.S

sudo nasm -I include/ -o boot/loader.bin boot/loader.S
(base) user@ubuntu:/home/cooiboi/bochs$ sudo nasm -I include/ -o boot/loader.bin boot/loader.S

3.将mbr和load写入磁盘中

sudo dd if=/home/cooiboi/bochs/boot/mbr.bin of=/home/cooiboi/bochs/boot/hd60M.img bs=512 count=1 conv=notrunc
sudo dd if=/home/cooiboi/bochs/boot/loader.bin of=/home/cooiboi/bochs/boot/hd60M.img bs=512 count=3 seek=2 conv=notrunc

sudo dd if=/home/cooiboi/bochs/boot/loader.bin of=/home/cooiboi/bochs/boot/hd60M.img bs=512 count=3 seek=2 conv=notrunc解释:

  • seek 为 9,目的是跨过前 9 个扇区(第 0~8 个扇区),我们在第 9 个扇区写入。这是由于MBR写在第0号扇区,第 1 扇区是空着的不写,loader 写在硬盘的第 2 扇区,loader 占用 3 个扇区,所以第 2~4 扇区不能再用啦。所以从5号扇区起都是可用的。
  • count 为 200,目的是一次往参数 of 指定的文件中写入 200 个扇区。【将来的内核大小不会超过 100KB】
(base) user@ubuntu:/home/cooiboi/bochs/boot$ sudo dd if=/home/cooiboi/bochs/boot/mbr.bin of=/home/cooiboi/bochs/boot/hd60M.img bs=512 count=1 conv=notrunc
1+0 records in
1+0 records out
512 bytes copied, 0.000190907 s, 2.7 MB/s
(base) user@ubuntu:/home/cooiboi/bochs/boot$ sudo dd if=/home/cooiboi/bochs/boot/loader.bin of=/home/cooiboi/bochs/boot/hd60M.img bs=512 count=3 seek=2 conv=notrunc
3+0 records in
3+0 records out
1536 bytes (1.5 kB, 1.5 KiB) copied, 0.000253831 s, 6.1 MB/s

【补充】创建hd60M.img的命令
(base) user@ubuntu:/home/cooiboi/bochs/boot$ sudo /home/cooiboi/bochs/bin/bximage
创建的hd60M.img就会再boot目录下。


4.编译,链接,写入硬盘

sudo gcc -c -o kernel/main.o kernel/main.c
sudo ld kernel/main.o -Ttext 0xc0001500 -e main -o kernel/kernel.bin
sudo dd if=/home/cooiboi/bochs/kernel/kernel.bin of=/home/cooiboi/bochs/boot/hd60M.img bs=512 count=200 seek=9 conv=notrunc
  • 链接命令 ld 中用-Ttext 指定了代码段的起始虚拟地址
(base) user@ubuntu:/home/cooiboi/bochs$ sudo gcc -c -o kernel/main.o kernel/main.c
(base) user@ubuntu:/home/cooiboi/bochs$ sudo ld kernel/main.o -Ttext 0xc0001500 -e main -o kernel/kernel.bin
(base) user@ubuntu:/home/cooiboi/bochs$ sudo dd if=/home/cooiboi/bochs/kernel/kernel.bin of=/home/cooiboi/bochs/boot/hd60M.img bs=512 count=200 seek=9 conv=notrunc
12+1 records in
12+1 records out
6232 bytes (6.2 kB, 6.1 KiB) copied, 0.000251117 s, 24.8 MB/s

:以上命令省略了目录切换,只给出关键性代码及结果。

5.启动Bochs

sudo bin/bochs -f boot/bochsrc.disk
(base) user@ubuntu:/home/cooiboi/bochs$ sudo bin/bochs -f boot/bochsrc.disk

死循环啦~

在这里插入图片描述

内核文件 kernel.bin 是 elf 格式的二进制可执行文件,我们可用使用xxd kernel.bin命令查看elf文件头格式

在这里插入图片描述
参考资料

  • 《操作系统真象还原》
  • 《操作系统真象还原》第五章 ---- 轻取物理内存容量 启用分页畅游虚拟空间 力斧直斩内核先劈一角 闲庭信步摸谈特权级

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/148371.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Odoo 16 企业版手册 - 库存管理之存储类别

存储类别 Odoo中的存储类别功能将允许您将许多存储位置分组到一个类别下。您可以在Odoo 库存管理模块中创建许多此类类别&#xff0c;这将有助于执行更智能的放置操作。在配置存储类别之前&#xff0c;您必须配置库存中可用的存储位置。然后&#xff0c;您可以将它们分组到一个…

LeetCode刷题模版:31 - 40

目录 简介31. 下一个排列32. 最长有效括号33. 搜索旋转排序数组34. 在排序数组中查找元素的第一个和最后一个位置35. 搜索插入位置36. 有效的数独37. 解数独38. 外观数列39. 组合总和40. 组合总和 II结语简介 Hello! 非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指…

电影解说开头怎么写吸引人?

电影解说开头怎么写吸引人&#xff1f;很多电影解说创作者文采不够好&#xff0c;开头不知道怎么写&#xff1f;毕竟想留住用户继续观看视频&#xff0c;开头是至关重要的&#xff0c;今天笔者就分享电影解说文案万能公式模板&#xff0c;让大家创作更简单&#xff01;一个好的…

feature engnineering 特征工程

特征工程数值型变量standardizationlog_transformation(使其符合正态分布)polynomial features分类型变量orinigalencoderonehot encoder分类创造下的数值以下代码根据Abhishek Thakur在kaggle上的机器学习30天 &#xff08;b站&#xff09; (kaggle)可惜的是&#xff0c;我没有…

Oracle 19c VLDB and Partitioning Guide 第5章:管理和维护基于时间的信息 读书笔记

本文为Oracle 19c VLDB and Partitioning Guide第5章Managing and Maintaining Time-Based Information的读书笔记。 Oracle 数据库提供了基于时间管理和维护数据的策略。 本章讨论 Oracle 数据库中的组件&#xff0c;这些组件可以构建基于时间管理和维护数据的策略。 尽管大…

计算机网络复习之网络层

文章目录数据报与虚电路服务的对比IP 协议IP数据报格式IP地址NAT&#xff08;网络地址转换&#xff09;子网划分和子网掩码在支持子网划分的因特网中&#xff0c;路由器如何转发IP数据报无分类编制CIDR构成超网RIP协议OSPF协议ARP协议ICMP协议Ping和Traceroute参考路由选择是网…

Eclipse安装教程

Eclipse安装教程 目录一&#xff0e; 概述二&#xff0e; 下载eclipse三&#xff0e; 安装eclipse四&#xff0e; 使用eclipse。一&#xff0e; 概述 eclipse是针对java编程的集成开发环境&#xff0c;其设计思想是“一切皆插件”。就其本身而言&#xff0c;eclipse只是一个框架…

Hive表的创建,删除,修改

TBLPROPERTIES的主要作用是按键-值对的格式为表增加额外的文档说明。Hive会自动增加两个表属性:一个是last_modified_by&#xff0c;其保存着最后修改这个表的用户的用户名﹔另一个是 last_modified_time&#xff0c;其保存着最后一次修改的新纪元时间秒。用户还可以拷贝一张已…

数据的合并和分组聚合

一&#xff1a;字符串离散化的案列 对于这一组电影数据&#xff0c;如果我们希望统计电影分类(genre)的情况&#xff0c;应该如何处理数据&#xff1f; 思路&#xff1a;重新构造一个全为0的数组&#xff0c;列名为分类&#xff0c;如果某一条数据中分类出现过&#xff0c;就让…

Java之class类

Class类 1.类图 2.Class类对象 系统创建 该class对象是通过类加载器ClassLoader的loadClass()方法生成对应类对应的class对象 通过debug可以追到该方法 3.对于某个类的class类对象 只加载一次 因为类值加载一次 类加载的时机 //1.创建对象实例的时候&#xff08;new&#xf…

7-10 列车调度

火车站的列车调度铁轨的结构如下图所示。 两端分别是一条入口&#xff08;Entrance&#xff09;轨道和一条出口&#xff08;Exit&#xff09;轨道&#xff0c;它们之间有N条平行的轨道。每趟列车从入口可以选择任意一条轨道进入&#xff0c;最后从出口离开。在图中有9趟列车&am…

联合证券|道指狂拉700点!八大科技巨头市值暴涨1.3万亿!

本周最终一个交易日&#xff0c;美股三大指数收盘团体大涨。道指涨2.13%&#xff0c;标普500指数涨2.28%&#xff0c;纳斯达克指数涨2.56%。 大型科技股遍及走强&#xff0c;苹果、亚马逊涨超3%。特斯拉经历惊险一夜&#xff0c;股价盘初跌近8%后反弹&#xff0c;收盘涨幅超2%。…

上半年要写的博客,提前占坑1

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注…

第二章 创建数据集

2.1 数据集的概念 数据集通常是由数据构成的一个矩形数组&#xff0c;行表示观测&#xff0c;列表示变量。表2-1提供了一个假想的病例数据集。 不同的行业对于数据集的行和列叫法不同。统计学家称它们为观&#xff08;observation&#xff09;和变量&#xff08;variable&…

可变形卷积DCN/DConv

1、定义 可变形卷积是指卷积核在每一个元素上额外增加了一个方向参数&#xff0c;这样卷积核就能在训练过程中扩展到很大的范围&#xff0c;卷积核可以变成任意方向。 图(a)是普通卷积 图(b)、©、(d)是可变形卷积&#xff0c;©(d)是(b)的特例 2、为什么要使用可变…

Nginx配置负载均衡到网关

Nginx配置负载均衡到网关 1.需求图示 前置准备工作 https://blog.csdn.net/qq_44981526/article/details/128599898 2.配置实现 1.在C:\Windows\System32\drivers\etc目录下修改hosts文件 #配置llpliving.com nginx虚拟机 192.168.56.100 www.llpliving.com2.配置nginx负载…

【C++】优先级队列priority_queue仿函数

这里先简单介绍一下优先级队列priority_queue&#xff1a;优先队列是一种容器适配器,默认的情况下&#xff0c;如果没有为特定的priority_queue类实例化指容器类&#xff0c;则使用vector (deque 也是可以的),需要支持随机访问迭代器&#xff0c;以便始终在内部保持堆结构 文章…

Springboot配置静态资源

目录 1. springboot默认的静态资源存放路径 2.Springboot添加静态资源映射addResourceHandlers addResourceLocations 3.坑 如果方法一和二同时配置 那么就会遵循方法二 方法一的静态文件将找不到 1. springboot默认的静态资源存放路径 静态资源的存放路径为classpath,也就是…

【卷积码系列4】卷积码的状态转移函数、距离谱和译码性能界分析及matlab仿真

一、卷积码的状态图和转移函数 以一个例子入手,对于如下编码器所示的码率1/3卷积码 根据输入和寄存器状态,可以得到其状态图如下所示 图中虚线表示输入比特为1时的转移,而实线表示输入比特为0时的转移 同样,图中虚线表示输入比特为1时的转移,而实线表示输入比特为0…

Diffusion Models从入门到放弃:必读的10篇经典论文

前言&#xff1a;diffusion models是现在人工智能领域最火的方向之一&#xff0c;并引爆了AIGC方向&#xff0c;一大批创业公司随之诞生。笔者2021年6月开始研究diffusion&#xff0c;见证了扩散模型从无人问津到炙手可热的过程&#xff0c;这些篇经典论文我的专栏里都详细介绍…