第5章 保护模式进阶,向内核迈进

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

一、获取内存物理信息

必须先知道物理内存有多大、哪些可用、哪些是保留 / 空洞,才能做后续分页与内核放置

使用BIOS 0x15中断获取内存容量的探测方法(3种):

  • AH=0x88:最多识别 64MB,简单但不准。
  • AX=0xE801:识别低 15MB 与 16MB~4GB,适合 32 位系统。
  • EAX=0xE820推荐 ):遍历完整内存布局,返回 "起始地址 + 长度 + 类型",能识别可用 / 保留 / ACPI / 内存空洞(15MB~16MB。

代码实现

在loader.s中增添如下代码(内存探测):

复制代码
;----------------------------E820 内存探测代码----------------------------

.e820_mem_get_loop:
    mov eax,0x0000E820                                        ;BIOS规定,eax存放int 0x15 的功能号,0xE820 就是 "获取系统内存布局" 功能号
    mov ecx,0x14                                              ;告诉BIOS,缓冲区大小是20字节,写入ecx是固定搭配
    mov edx,0x534d4150                                        ;0x534D4150 是 ASCII 字符串 SMAP 的十六进制值,BIOS 会检查这个签名是否正确
    
    int 0x15                                                  ;触发0x15中断,调用BIOS的E820功能,获取一个内存块的信息,写入 es:di 指向的缓冲区
    jc  .e820_failed_so_try_e801                              ;如果 CF=1(调用失败),就跳转到 E801 探测方案
    add di,cx						      ;把di的数值增加20,让di指向下一个缓冲区位置,为下一次调用准备
    inc word [ards_nr]                                        ;ards_nr 变量的值加 1,即记录"获取到的内存块数量"加1
    cmp ebx,0                                                 ;判断 BIOS 是否还有下一个内存块。BIOS 会在最后一个内存块返回时,把 ebx 设为 0
    jne .e820_mem_get_loop                                    ;如果ebx != 0(不相等,说明内存块还没读完),跳回循环开头继续读取
    
    mov cx,[ards_nr]                                          ;ards_nr 变量的值,即内存块的数量赋值给cx

    mov ebx,ards_buf                                          ;把 ards_buf 缓冲区的地址赋值给 ebx,让 ebx 指向第一个内存块
    xor edx,edx                                               ;edx 用来保存所有内存块中最大的地址,把它初始化设为0

;---------------------------------------"求最大内存地址"代码------------------
.find_max_mem_area:
    
    mov eax,[ebx]		           ;ebx 指向第一个内存块,[ebx]取的是ARDS结构的基地址低32位,让eax也指向第一个内存块	
    add eax,[ebx+8]                        ;[ebx+8]得到当前内存块长度的低32位,add后就得到了当前内存块的结束地址               
    add ebx,20    	                   ;让 ebx 指向下一个内存块					
    cmp edx,eax                            ;edx 保存的是当前已探测内存块中的最大结束地址,eax 是当前内存块的结束地址
    jge .next_ards                         ;如果 edx ≥ eax,说明当前块的结束地址不是最大结束地址,说明还有内存快没读完,此时去出路下一个内存块
    mov edx,eax                            ;如果 eax > edx,说明当前块的结束地址是新的最大值,把 eax 的值更新到 edx,让 edx 保存新的最大地址

.next_ards:
    loop .find_max_mem_area                ;loop 是汇编循环指令
                                           ;先把 cx 寄存器的值减 1;如果减完后 cx != 0,就跳转到 .find_max_mem_area 标签;
                                           ;如果 cx == 0,则不跳转,继续执行下一条指令
                                           ;前面 mov cx, [ards_nr] 已经把内存块数量存进了 cx
    jmp .mem_get_ok                        ;前面的循环结束后,所有内存块都被探测完了
    



;---------------------------"E820探测失败,使用E801探测"代码---------------------------

.e820_failed_so_try_e801:                                      
    mov ax,0xe801                          ;同之前的一样,吧功能号赋值给ax
    int 0x15                               ;
    jc .e801_failed_so_try_88
   
                                           ;计算出来低15MB的内存    
    mov cx,0x400
    mul cx                                                      
    shl edx,16                                                  
    and eax,0x0000FFFF                                          
    or edx,eax                                                 
    add edx,0x100000                                           
    mov esi,edx
    
                                           ;计算16MB以上的内存,以字节为单位
    xor eax,eax
    mov ax,bx
    mov ecx,0x10000                                              
    mul ecx                                                      
    mov edx,esi
    add edx,eax
    jmp .mem_get_ok
 

;---------------------------"E820、E801探测失败,使用0x88探测"代码---------------------------
.e801_failed_so_try_88:
     mov ah,0x88                           ;同样赋值功能号
     int 0x15
     jc .error_hlt
     and eax,0x0000FFFF
     mov cx,0x400                                                 ;1024
     mul cx
     shl edx,16
     or edx,eax 
     add edx,0x100000
;-----------------------------"所有内存探测方案失败,停止运行"代码-------------------------
.error_hlt:
     jmp $

;------------------------------内存探测成功代码-------------------------------------------
.mem_get_ok:
     mov [total_mem_bytes],edx              ;内存探测成功,把最终得到的物理内存大小保存进edx,为后面的分页初始化准备

进入bochs进行仿真运行,输入以下命令:

复制代码
;LOADER_BASE_ADDR 是 0x600
;前面的 GDT、预留空间、gdt_ptr 等数据占了 0x200 字节
;所以 total_mem_bytes 这个变量的物理地址就是:0x600+0x200=0x800
;xp 0x800:查看物理地址 0x800 处的内容

xp 0x800

结果如图所示:

0x20000000 是十六进制数,换算成十进制:0x20000000=2×167=536,870,912 字节=512 MB而我们在 Bochs 配置里设置的虚拟机内存就是 512MB。

二、分页

为什么要分页

在操作系统进入保护模式后,实行的是通过「段选择子:段内偏移」的格式来得到地址,在这样的格式下,CPU 会根据段选择子去 GDT 中找到对应的段描述符,然后取出对应的段的基地址,再加上段内偏移,最终得到了线性地址。即:分段机制完成了从 逻辑地址 → 线性地址 的转换。

但是,分段机制存在着一些缺陷:

  • 段的空间尺寸大,容易造成内存空间的浪费,必须占用连续的物理内存空间,物理内存碎片化后难以高效利用;
  • 无法实现多进程独立地址空间隔离,没有成熟的内存访问控制体系。

因此,在操作系统保护模式的基础性上,引入分页机制

分页核心概念:

页(Page):虚拟内存的最小管理单位 ,这里规定大小为 4KB。系统将整个 4GB 线性地址空间,按固定 4KB 大小切分为一个个虚拟页。

**页框(Page Frame):物理内存的最小分配单位,**大小同样为 4KB。物理内存条被切分为连续、等大的物理页框,用于存放虚拟页的数据。

分页核心本质:建立虚拟页与物理页框的映射关系。

二级分页架构

32 位线性地址总寻址范围:0~4GB,共 32 位二进制,x86 采用两级页表分层管理,将 32 位线性地址划分为三段:

高 10 位:页目录索引(PDI) +中间 10 位:页表索引(PTI) + 低 12 位:页内偏移 (Offset)

页目录(Page Directory): 10 位寻址范围:2的10次方,即1024个项,每个页目录项占 4 字节,用来存储某一张页表的物理基地址 + 内存属性标志位,整个操作系统只有一个页目录,管控整个4GB大小的虚拟空间。

页表(Page Table): 10 位寻址范围:2的10次方,即1024个项,每个页表录项占 4 字节,用来存储某一张页表的物理基地址 + 内存属性标志位,整个操作系统只有一个页目录,管控整个4GB大小的虚拟空间

通过唯一的页目录找到一个页表--->通过这个页表找到一个页表项--->通过页表项得到最终的物理地址

在引入例如分页后,操作系统内的完整寻址流程就如下所示:逻辑地址(对应着虚拟内存) → 分段机制 → 线性地址 → 分页机制 → 物理地址(对应实际存在的内存条)

地址转换详细流程:

(1)CPU 取出 32 位线性地址,拆分:高 10 位 PDI、中间 10 位 PTI、低 12 位页内偏移;

(2) 从寄存器 CR3 中读取页目录物理起始地址

(3) 页目录基地址 + PDI×4,计算出目标页目录项内存地址;

(4) 读取页目录项,校验 P 位是否有效,取出其中页表物理基地址

(5) 页表基地址 + PTI×4,计算出目标页表项内存地址;

(6) 读取页表项,取出物理页框基地址

(7) 物理页框基地址 + 低 12 位页内偏移,拼接得到最终物理内存地址

(8) CPU 访问该物理地址,完成内存读写。

代码实现

在boot.inc中增添如下代码:

复制代码
;------------------ 开启页表所需要的宏定义---------------------------

PAGE_DIR_TABLE_POS  equ 0x100000                            ;定义页目录表的起始物理地址为 0x100000(1MB)



;------------------ 对页表的相关属性进行定义---------------------------------
PG_P 	 equ  1b                                            ;PG目录项第0位P位,当p位=1 表示存在于当前物理内存
PG_RW_R  equ 00b                                            ;第1位Read/Write 位,当该位=0表示该页只读
PG_RW_W  equ 10b                                            ;                         =1表示可写可读
PG_US_S  equ 000b                                           ;第2位user位,当该位=0表示超级用户
PG_US_U  equ 100b                                           ;                  =1普通用户


;-------------------加载内核所需要的宏定义 -------------------------------

KERNEL_BIN_SECTOR    equ  0x9                  ;定义内核二进制文件在硬盘上的起始扇区号为 0x9
                                               ;后面 rd_disk_m_32 函数会从这个扇区开始读取内核
KERNEL_BIN_BASE_ADDR equ  0x70000              ;定义内核二进制文件加载到内存的物理地址为 0x70000
                                               ;Loader会把内核文件读到这,后面 kernel_init 会解析这个地址上的 ELF 文件头

KERNEL_ENTER_ADDR    equ  0xc0001500           ;定义内核的入口虚拟地址为 0xc0001500
                                               ;在开启分页后,loader程序会跳转到这个地址(虚拟地址),然后开始进入内核

PT_NULL              equ  0x0                  ;定义ELF程序段的 PT_NULL 类型为 0
                                               ;在 kernel_init 中,用该变量来判断当前段是否为空段,如果是,则跳过该段进行后续操作

在loader.s中增添如下代码(开启分页+在32位模式下读取硬盘):

复制代码
;------------------------------- 加载内核到缓冲区 -------------------------------------------------

    mov eax, KERNEL_BIN_SECTOR                ;把 KERNEL_BIN_SECTOR(0x9)赋值给 eax,从第9个扇区开始读内核
    mov ebx, KERNEL_BIN_BASE_ADDR             ;从扇区中读到的内核数据放到以0x70000为起始地址的内存地址上
    
    mov ecx,200                               ;读取扇区的数量为200个
    call rd_disk_m_32

;--------------------------------开始启动分页 ---------------------------------------------------
    
    call setup_page                           ;对业目录表和页表进行初始化(这个函数的具体实现在后面)
    				              ;gdtr的格式:0-15位:GDT界限 16-47位:起始地址
    sgdt [gdt_ptr]                            ;将gdt寄存器中的值,写入到gdt_ptr 变量中,保存当前 GDT 的信息
    mov ebx,[gdt_ptr+2]                       ;[gdt_ptr+2]即GDT的起始地址,写入ebx
                                                               
    or dword [ebx+0x18+4],0xc0000000          ;把显存段的基地址从0xB8000 修改为 0xC00B8000
                                              ;使其映射到虚拟内存中,这样设置之后,即使开启分页也能访问显存
    add dword [gdt_ptr+2],0xc0000000          ;gdt_ptr+2是GDT的起始地址,同样将它也进行映射,分页开启后能找到GDT         
    
    add esp,0xc0000000                        ;栈指针同样进行映射
    
    mov eax,PAGE_DIR_TABLE_POS              
    mov cr3,eax                               ;把页目录表的地址写入 cr3 寄存器,告诉 CPU 页目录表在哪里,这是开启分页的前提
    
    mov eax,cr0
    or eax,0x80000000                         ;把 cr0 的最高位(PG 位)设为1,开启分页 
    mov cr0,eax
    
    lgdt [gdt_ptr]                            ;加载映射后的新的GDT地址
    
    mov eax,SELECTOR_VIDEO
    mov gs,eax                                ;重新加载显存段
    mov byte [gs:160],'V'                     ;验证分页是否开启成功,如果屏幕上显示了 V,说明显存映射正常,分页工作正常
    
    jmp SELECTOR_CODE:enter_kernel            ;跳转到内核区
    
;------------------------------ 跳转到内核区    

enter_kernel:
    call kernel_init	                      ;初始化内核(具体实现在后面)解析内核ELF文件,把内核的各个段加载到正确的虚拟地址上,完成重定位,为执行内核做准备				          
    mov  esp,0xc009f000                       ;内核设置新的栈指针
    jmp  KERNEL_ENTER_ADDR                    ;跳转到内核的入口

;------------------------------- 创建页表 ------------------------------------------------    
setup_page:
    mov ecx,0x1000                                             
    mov esi,0                                                   
 .clear_page_dir_mem:                         ;对页目录表进行初始化,全部清空                
    mov byte [PAGE_DIR_TABLE_POS+esi],0
    inc esi
    loop .clear_page_dir_mem
    
 .create_pde:                                 ;创建一个页目录项
    mov eax,PAGE_DIR_TABLE_POS				  
    add eax,0x1000                            ;页表物理地址 = PAGE_DIR_TABLE_POS + 0x1000                  
    or  eax, PG_P | PG_RW_W | PG_US_U                          
     
    mov [PAGE_DIR_TABLE_POS+0x0],eax                            
    mov [PAGE_DIR_TABLE_POS+0xc00],eax                        
    sub eax,0x1000      
    
    mov [PAGE_DIR_TABLE_POS+4092],eax                           

   
    mov eax,PAGE_DIR_TABLE_POS                ;
    add eax,0x1000
    mov ecx,256
    mov esi,0
    mov ebx,PG_P | PG_RW_W | PG_US_U 
    
 .create_kernel_pte:                          ;填充低地址页表:物理地址 0x00000000 ~ 0x000FFFFF(0~1MB)的物理内存,映射到虚拟地址
    mov [eax+esi*4],ebx                       
    inc esi
    add ebx,0x1000
    loop .create_kernel_pte 
    
                                              ;为内核的高地址空间页目录项设置初始的页表地址
    mov eax,PAGE_DIR_TABLE_POS                ;为内核高地址空间(0xC0400000 ~ 0xFFFFFFFF),初始化页目录项,让它们指向连续的页表
    add eax,0x2000       					   
    or  eax,PG_P | PG_RW_W | PG_US_U
    mov ebx,PAGE_DIR_TABLE_POS
    mov ecx,254						  
    mov esi,769
        
 .create_kernel_pde:                          
    mov [ebx+esi*4],eax
    inc esi
    add eax,0x1000
    loop .create_kernel_pde 
    
    ret            
    
;----------------------- 初始化内核 把缓冲区的内核代码放到0x1500区域 ------------------------------------------
;这个地方主要对elf文件头部分用的很多
;可以参照着书上给的格式 来比较对比
kernel_init:
    xor eax,eax                               ;寄存器全部清零
    xor ebx,ebx
    xor ecx,ecx
    xor edx,edx
    
                                              ;解析 ELF 文件头,获取程序段表信息
    mov ebx,[KERNEL_BIN_BASE_ADDR+28]         
    add ebx,KERNEL_BIN_BASE_ADDR              ;ebx当前位置为程序段表
    mov dx,[KERNEL_BIN_BASE_ADDR+42]	      ;获取程序段表每个条目描述符字节大小
    mov cx,[KERNEL_BIN_BASE_ADDR+44]          ;段的总数量
    

                                              ;遍历每个程序段     
 .get_each_segment:
    cmp dword [ebx+0],PT_NULL
    je .PTNULL                                                 
    
    mov eax,[ebx+8]
    cmp eax,0xc0001500
    jb .PTNULL
    
        
    push dword [ebx+16]                                        
                                                               
    mov eax,[ebx+4]                                            
    add eax,KERNEL_BIN_BASE_ADDR
    push eax                                                          
    push dword [ebx+8]                                        
     
    call mem_cpy
    add esp,12                                                 
    
                                            ;若当前段是空的,则跳过当前段,对下一个段进行处理
 .PTNULL:
    add  ebx,edx                                               
    loop .get_each_segment                                       
    ret
                                            ;对内存的数据进行复制备份
mem_cpy:
    cld                                     ;清除方向标志位,确保复制数据时是从低位到高位进行复制的
          
    push ebp                                ;建立栈帧、保存寄存器                           
    mov  ebp,esp 
    push ecx                                                
    
                                                              
                                                              
    mov edi,[ebp+8]                         ;[ebp+8]对应目标地址,存入edi                     
    mov esi,[ebp+12]                        ;[ebp+12]对应源地址, 存入esi                
    mov ecx,[ebp+16]                        ;[ebp+16]对应"一次复制多大的数据" ,存入ecx                 
    
    
    rep movsb                               ;开始进行复制,一个一个字节复制
       
    pop ecx                                 ;恢复栈帧、寄存器
    pop ebp
    ret

    
;------------------------ rd_disk_m_32  ----------------------
rd_disk_m_32:
;1 写入待操作磁盘数
;2 写入LBA 低24位寄存器 确认扇区
;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1
;4 command 写指令
;5 读取status状态寄存器 判断是否完成工作
;6 完成工作 取出数据
 
 ;;;;;;;;;;;;;;;;;;;;;
 ;1 写入待操作磁盘数
 ;;;;;;;;;;;;;;;;;;;;;
    mov esi,eax   ; !!! 备份eax
    mov di,cx     ; !!! 备份cx
    
    mov dx,0x1F2  ; 0x1F2为Sector Count 端口号 送到dx寄存器中
    mov al,cl     ; !!! 忘了只能由ax al传递数据
    out dx,al     ; !!! 这里修改了 原out dx,cl
    
    mov eax,esi   ; !!!袄无! 原来备份是这个用 前面需要ax来传递数据 麻了
    
;;;;;;;;;;;;;;;;;;;;;
;2 写入LBA 24位寄存器 确认扇区
;;;;;;;;;;;;;;;;;;;;;
    mov cl,0x8    ; shr 右移8位 把24位给送到 LBA low mid high 寄存器中

    mov dx,0x1F3  ; LBA low
    out dx,al 
    
    mov dx,0x1F4  ; LBA mid
    shr eax,cl    ; eax为32位 ax为16位 eax的低位字节 右移8位即8~15
    out dx,al
    
    mov dx,0x1F5
    shr eax,cl
    out dx,al
    
;;;;;;;;;;;;;;;;;;;;;
;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1
;;;;;;;;;;;;;;;;;;;;;

    		 
    		  ; 24 25 26 27位 尽管我们知道ax只有2 但还是需要按规矩办事 
    		  ; 把除了最后四位的其他位置设置成0
    shr eax,cl
    
    and al,0x0f 
    or al,0xe0   ;!!! 把第四-七位设置成0111 转换为LBA模式
    mov dx,0x1F6 ; 参照硬盘控制器端口表 Device 
    out dx,al

;;;;;;;;;;;;;;;;;;;;;
;4 向Command写操作 Status和Command一个寄存器
;;;;;;;;;;;;;;;;;;;;;

    mov dx,0x1F7 ; Status寄存器端口号
    mov ax,0x20  ; 0x20是读命令
    out dx,al
    
;;;;;;;;;;;;;;;;;;;;;
;5 向Status查看是否准备好惹 
;;;;;;;;;;;;;;;;;;;;;
    
		   ;设置不断读取重复 如果不为1则一直循环
  .not_ready:     
    nop           ; !!! 空跳转指令 在循环中达到延时目的
    in al,dx      ; 把寄存器中的信息返还出来
    and al,0x88   ; !!! 0100 0100 0x88
    cmp al,0x08
    jne .not_ready ; !!! jump not equal == 0
    
    
;;;;;;;;;;;;;;;;;;;;;
;6 读取数据
;;;;;;;;;;;;;;;;;;;;;

    mov ax,di      ;把 di 储存的cx 取出来
    mov dx,256
    mul dx        ;与di 与 ax 做乘法 计算一共需要读多少次 方便作循环 低16位放ax 高16位放dx
    mov cx,ax      ;loop 与 cx相匹配 cx-- 当cx == 0即跳出循环
    mov dx,0x1F0
 .go_read_loop:
    in ax,dx      ;两字节dx 一次读两字
    mov [ebx],ax
    add ebx,2
    loop .go_read_loop
    ret ;与call 配对返回原来的位置 跳转到call下一条指令

三、最终代码

boot.inc:

复制代码
;---------------------------------------进入loader所需的宏定义

LOADER_START_SECTOR equ 2       ;把 LOADER_START_SECTOR 这个符号直接替换成数值 2
                                ;表明Loader 程序在磁盘上的起始扇区号是2扇区
                                ;扇区从0开始计数,第0扇区是MBR,第1扇区是1,所以2就是第3个扇区
                                
LOADER_BASE_ADDR equ 0x600      ;Loader 程序被加载到内存的起始地址是 0x600
;MBR 引导程序在调用 BIOS 中断读取磁盘时,会把 0x600 作为目标内存地址,把 Loader 读到这里,然后跳过去执行





;------------------------------------对gdt描述符属性进行定义(高32位)

;下划线没有语法作用,只是为了让人看清哪一位是哪一位!
DESC_G_4K equ 1_00000000000000000000000b ;第23位,G位表示4K或者1字节位,将此位设置为1,则表明此时粒度的大小则为4kB 
DESC_D_32 equ 1_0000000000000000000000b  ;第22位,D/B位,将此位设置为1,表明此时使用32位保护模式
DESC_L    equ 0_000000000000000000000b   ;第21位,L位,将此位设置为0,表明此时所写的代码都是32位的代码
DESC_AVL  equ 0_00000000000000000000b    ;第20位,软件可用位,暂时用不到它,设置为0
                                         
DESC_LIMIT_CODE2  equ  1111_0000000000000000b   ;第16-19位,段界限的高四位 全部初始化为1 因为最大段界限*粒度必须等于
                                                ;0xffffffff,即4GB的大小 ,段界限的后16位是在低32位中的,到时由我们自行写入
                                                         
DESC_LIMIT_DATA2  equ  DESC_LIMIT_CODE2         ;相同的值  数据段与代码段段界限相同
DESC_LIMIT_VIDEO2 equ	0000_0000000000000000b	  ;这里将显存段的段界限高4位填0000,显存不需要4GB那么大,只需一小块
   
DESC_P            equ 	1_000000000000000b	  ;第15位,P位,设置为1,用于判断段是否存在于内存
                                                ;1 = 段存在,必须是 1,否则 CPU 抛异常  
                                                
DESC_DPL_0        equ  00_0000000000000b        ;第13-14位,权限位
DESC_DPL_1        equ  01_0000000000000b	  ;0为操作系统内核,权力最高 3为用户程序,权力最低
DESC_DPL_2        equ  10_0000000000000b
DESC_DPL_3        equ  11_0000000000000b

DESC_S_sys        equ  0_000000000000b           ;第12位为0 则表示系统段,为1则表示数据段或代码段
DESC_S_CODE       equ  1_000000000000b           ;判断是否为系统段还是数据段
DESC_S_DATA       equ  DESC_S_CODE               ;代码段、数据段都填 1

                                                 ;第9-11位表示该段类型 1000 可执行 不允许可读 已访问位0
                                                 ;当第12位为1时:若11位为1,则为代码段;若11位为0,则为数据段
DESC_TYPE_CODE    equ  1000_00000000b            ;1000,表示:代码段、可执行、不可读、未访问
DESC_TYPE_DATA    equ  0010_00000000b            ;0010,表示:数据段、可读写 



;拼出 GDT 代码段描述符的 "高32位",将其特征属性放在一起,高32位全是属性位,用宏定义拼写较省力
;低32位根据需要,要写什么就写什么,格式为:段基址+段界限
;DESC_G_4K-->4KB粒度
;DESC_D_32-->32位保护模式
;DESC_L-->32位代码段
;DESC_LIMIT_CODE2-->段界限的最高4位1111,段最大,能访问 4GB
;DESC_P-->段在内存中
;DESC_DPL_0-->最高权限(内核)
;DESC_S_CODE-->代码/数据段
;DESC_TYPE_CODE-->若是代码段则是1000,即可执行
;0x00-->段基址是0x00,段界限
DESC_CODE_HIGH4   equ  (0x00<<24) + DESC_G_4K + DESC_D_32 + \
DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + \
DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0X00



;拼出 GDT 数据段描述符的 "高32位",将其特征属性放在一起
DESC_DATA_HIGH4   equ  (0x00<<24) + DESC_G_4K + DESC_D_32 + \
DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + \
DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0X00



;拼出 GDT 显存段描述符的 "高32位",将其特征属性放在一起
;显存基址是 0xB8000,高字节是 0x0B
DESC_VIDEO_HIGH4   equ (0x00<<24) + DESC_G_4K + DESC_D_32 + \
DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + \
DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0X0B  

;-------------------- 选择子属性 
RPL0    equ 00b        ;4个访问段时的特权级,与前面段本身的特权级是两个概念,注意不要混淆
RPL1    equ 01b
RPL2    equ 10b
RPL3    equ 11b

TI_GDT  equ 000b       ;TI = 1 表示用 LDT(局部表),0 表示用 GDT(全局表)
TI_LDT  equ 100b       



;------------------ 开启页表所需要的宏定义---------------------------

PAGE_DIR_TABLE_POS  equ 0x100000                            ;定义页目录表的起始物理地址为 0x100000(1MB)



;------------------ 对页表的相关属性进行定义---------------------------------
PG_P 	 equ  1b                                            ;PG目录项第0位P位,当p位=1 表示存在于当前物理内存
PG_RW_R  equ 00b                                            ;第1位Read/Write 位,当该位=0表示该页只读
PG_RW_W  equ 10b                                            ;                         =1表示可写可读
PG_US_S  equ 000b                                           ;第2位user位,当该位=0表示超级用户
PG_US_U  equ 100b                                           ;                  =1普通用户


;-------------------加载内核所需要的宏定义 -------------------------------

KERNEL_BIN_SECTOR    equ  0x9                  ;定义内核二进制文件在硬盘上的起始扇区号为 0x9
                                               ;后面 rd_disk_m_32 函数会从这个扇区开始读取内核
KERNEL_BIN_BASE_ADDR equ  0x70000              ;定义内核二进制文件加载到内存的物理地址为 0x70000
                                               ;Loader会把内核文件读到这,后面 kernel_init 会解析这个地址上的 ELF 文件头

KERNEL_ENTER_ADDR    equ  0xc0001500           ;定义内核的入口虚拟地址为 0xc0001500
                                               ;在开启分页后,loader程序会跳转到这个地址(虚拟地址),然后开始进入内核

PT_NULL              equ  0x0                  ;定义ELF程序段的 PT_NULL 类型为 0
                                               ;在 kernel_init 中,用该变量来判断当前段是否为空段,如果是,则跳过该段进行后续操作

loader.s:

复制代码
%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR             ;告诉汇编器:这段代码的运行地址是 0x600
LOADER_STACK_TOP equ LOADER_BASE_ADDR 		   ;栈顶指针 = 0x600,实模式和保护模式都需要栈,这里同样设置为0x600
jmp loader_start                     		   ;下面存放数据段 构建gdt 跳跃到下面的代码区 


;--------------------------------------构建全局描述附表
;db define byte,dw define word,dd define dword
;一个段描述符是8个字节				        
    GDT_BASE        : dd 0x00000000          		;Intel规定:GDT第0个段描述符不可用,必须是空描述符,必须全 0	  
   		       dd 0x00000000 
    
    CODE_DESC       : dd 0x0000FFFF         		;代码段描述符,权限:内核级、可执行、可读
    		       dd DESC_CODE_HIGH4               ;FFFF(4GB)是与其他的几部分相连接 形成0XFFFFF段界限,0x00000000是基地址                                                  ;下面这个dd就是我们在boot.inc中组合出的,段描述符的高32位
    
    DATA_STACK_DESC : dd 0x0000FFFF                     ;数据段描述符,权限:内核级、可读写
  		       dd DESC_DATA_HIGH4               ;下面这个dd就是我们在boot.inc中组合出的,段描述符的高32位
    		       
    VIDEO_DESC      : dd 0x80000007         		;0xB8000 到0xBFFFF为文字模式显示内存 B只能在boot.inc中出现定义了 此处不够空间了 8000刚好够
                      dd DESC_VIDEO_HIGH4     	        ;0x0007 (bFFFF-b8000)/4k = 0x7
                 
    GDT_SIZE              equ $ - GDT_BASE              ;GDT 总大小:当前位置减去GDT_BASE的地址 
    GDT_LIMIT       	   equ GDT_SIZE - 1   	        ;GDT 界限(必须是大小 - 1)


    
    times 59 dq 0                             	        ;为GDT预留59个 define double四字型 8字描述符
    times 5 db 0                                        ;4个段描述符32字节+59个8字节,共472字节+填充5字节=512字节(0x200)
                                                        ;让 total_mem_bytes 变量刚好落在 0x600+0x200=0x800 地址上
    total_mem_bytes  dd 0                               ;该变量存储 E820/E801 探测到的总内存字节数
    	               			           
    							   
 
    
    gdt_ptr           dw GDT_LIMIT			;定义GDT,格式:低16位,存GDT界限,高32位,存GDT的起始地址
    		       dd GDT_BASE
    		       
    ards_buf times 244 db 0                             ;定义244字节的缓冲区,初始值全为0,BIOS E820 中断会把内存布局信息(ARDS 结构)写到这里
    ards_nr dw 0					;该变量存储"E820中断探测到的内存块数量(ARDS结构的数量)
    							   
    
                                                            ;段选择子格式如下:
                                                            ;位15~3:索引 + 位2:TI + 位 1~0:RPL
    SELECTOR_CODE        equ (0X0001<<3) + TI_GDT + RPL0    ;01代码段描述符索引(左移3位是因为段选择子的格式规定,索引必须放在第 3~15 位;TI_GDT:是常量0,表示选择的是GDT全局描述符表;RPL0:是常量0,表示请求特权级为0内核级)
    SELECTOR_DATA	  equ (0X0002<<3) + TI_GDT + RPL0   ;数据段
    SELECTOR_VIDEO       equ (0X0003<<3) + TI_GDT + RPL0    ;显存段



;----------------------------loader启动代码---------------------------
    
loader_start:
    
    mov sp,LOADER_BASE_ADDR                                 ;初始化栈指针,设置栈顶地址(0X600)  
    xor ebx,ebx                                             ;初始化:把 ebx 寄存器清零
    mov ax,0                                       
    mov es,ax                                               ;初始化es段寄存器为 0
                                                            ;后面mov di, ards_buf,而di是偏移地址,最终的物理地址是 es:di;把es设为 0,就能保证 es:di 指向的就是物理地址 di


    mov di,ards_buf                                          ;把ards_buf变量的地址,赋值给 di 寄存器,此时di指向了缓冲区


;----------------------------E820 内存探测代码----------------------------

.e820_mem_get_loop:
    mov eax,0x0000E820                                        ;BIOS规定,eax存放int 0x15 的功能号,0xE820 就是 "获取系统内存布局" 功能号
    mov ecx,0x14                                              ;告诉BIOS,缓冲区大小是20字节,写入ecx是固定搭配
    mov edx,0x534d4150                                        ;0x534D4150 是 ASCII 字符串 SMAP 的十六进制值,BIOS 会检查这个签名是否正确
    
    int 0x15                                                  ;触发0x15中断,调用BIOS的E820功能,获取一个内存块的信息,写入 es:di 指向的缓冲区
    jc  .e820_failed_so_try_e801                              ;如果 CF=1(调用失败),就跳转到 E801 探测方案
    add di,cx						      ;把di的数值增加20,让di指向下一个缓冲区位置,为下一次调用准备
    inc word [ards_nr]                                        ;ards_nr 变量的值加 1,即记录"获取到的内存块数量"加1
    cmp ebx,0                                                 ;判断 BIOS 是否还有下一个内存块。BIOS 会在最后一个内存块返回时,把 ebx 设为 0
    jne .e820_mem_get_loop                                    ;如果ebx != 0(不相等,说明内存块还没读完),跳回循环开头继续读取
    
    mov cx,[ards_nr]                                          ;ards_nr 变量的值,即内存块的数量赋值给cx

    mov ebx,ards_buf                                          ;把 ards_buf 缓冲区的地址赋值给 ebx,让 ebx 指向第一个内存块
    xor edx,edx                                               ;edx 用来保存所有内存块中最大的地址,把它初始化设为0

;---------------------------------------"求最大内存地址"代码------------------
.find_max_mem_area:
    
    mov eax,[ebx]		           ;ebx 指向第一个内存块,[ebx]取的是ARDS结构的基地址低32位,让eax也指向第一个内存块	
    add eax,[ebx+8]                        ;[ebx+8]得到当前内存块长度的低32位,add后就得到了当前内存块的结束地址               
    add ebx,20    	                   ;让 ebx 指向下一个内存块					
    cmp edx,eax                            ;edx 保存的是当前已探测内存块中的最大结束地址,eax 是当前内存块的结束地址
    jge .next_ards                         ;如果 edx ≥ eax,说明当前块的结束地址不是最大结束地址,说明还有内存快没读完,此时去出路下一个内存块
    mov edx,eax                            ;如果 eax > edx,说明当前块的结束地址是新的最大值,把 eax 的值更新到 edx,让 edx 保存新的最大地址

.next_ards:
    loop .find_max_mem_area                ;loop 是汇编循环指令
                                           ;先把 cx 寄存器的值减 1;如果减完后 cx != 0,就跳转到 .find_max_mem_area 标签;
                                           ;如果 cx == 0,则不跳转,继续执行下一条指令
                                           ;前面 mov cx, [ards_nr] 已经把内存块数量存进了 cx
    jmp .mem_get_ok                        ;前面的循环结束后,所有内存块都被探测完了
    



;---------------------------"E820探测失败,使用E801探测"代码---------------------------

.e820_failed_so_try_e801:                                      
    mov ax,0xe801                          ;同之前的一样,吧功能号赋值给ax
    int 0x15                               ;
    jc .e801_failed_so_try_88
   
                                           ;计算出来低15MB的内存    
    mov cx,0x400
    mul cx                                                      
    shl edx,16                                                  
    and eax,0x0000FFFF                                          
    or edx,eax                                                 
    add edx,0x100000                                           
    mov esi,edx
    
                                           ;计算16MB以上的内存,以字节为单位
    xor eax,eax
    mov ax,bx
    mov ecx,0x10000                                              
    mul ecx                                                      
    mov edx,esi
    add edx,eax
    jmp .mem_get_ok
 

;---------------------------"E820、E801探测失败,使用0x88探测"代码---------------------------
.e801_failed_so_try_88:
     mov ah,0x88                           ;同样赋值功能号
     int 0x15
     jc .error_hlt
     and eax,0x0000FFFF
     mov cx,0x400                                                 ;1024
     mul cx
     shl edx,16
     or edx,eax 
     add edx,0x100000
;-----------------------------"所有内存探测方案失败,停止运行"代码-------------------------
.error_hlt:
     jmp $

;------------------------------内存探测成功代码-------------------------------------------
.mem_get_ok:
     mov [total_mem_bytes],edx              ;内存探测成功,把最终得到的物理内存大小保存进edx,为后面的分页初始化准备



; --------------------------------- 设置进入保护模式 -----------------------------
; 1 打开A20 gate
; 2 加载gdt
; 3 将cr0 的 pe位置1
    
    in al,0x92                 ;端口号0x92 中 第1位变成1即可
    or al,0000_0010b
    out 0x92,al
   
    
    lgdt [gdt_ptr]
    
    
    mov eax,cr0                ;cr0寄存器第0位设置位1
    or  eax,0x00000001              
    mov cr0,eax
      
;-------------------------------- 已经打开保护模式 ---------------------------------------
    jmp dword 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 eax, KERNEL_BIN_SECTOR                ;把 KERNEL_BIN_SECTOR(0x9)赋值给 eax,从第9个扇区开始读内核
    mov ebx, KERNEL_BIN_BASE_ADDR             ;从扇区中读到的内核数据放到以0x70000为起始地址的内存地址上
    
    mov ecx,200                               ;读取扇区的数量为200个
    call rd_disk_m_32

;--------------------------------开始启动分页 ---------------------------------------------------
    
    call setup_page                           ;对业目录表和页表进行初始化(这个函数的具体实现在后面)
    				              ;gdtr的格式:0-15位:GDT界限 16-47位:起始地址
    sgdt [gdt_ptr]                            ;将gdt寄存器中的值,写入到gdt_ptr 变量中,保存当前 GDT 的信息
    mov ebx,[gdt_ptr+2]                       ;[gdt_ptr+2]即GDT的起始地址,写入ebx
                                                               
    or dword [ebx+0x18+4],0xc0000000          ;把显存段的基地址从0xB8000 修改为 0xC00B8000
                                              ;使其映射到虚拟内存中,这样设置之后,即使开启分页也能访问显存
    add dword [gdt_ptr+2],0xc0000000          ;gdt_ptr+2是GDT的起始地址,同样将它也进行映射,分页开启后能找到GDT         
    
    add esp,0xc0000000                        ;栈指针同样进行映射
    
    mov eax,PAGE_DIR_TABLE_POS              
    mov cr3,eax                               ;把页目录表的地址写入 cr3 寄存器,告诉 CPU 页目录表在哪里,这是开启分页的前提
    
    mov eax,cr0
    or eax,0x80000000                         ;把 cr0 的最高位(PG 位)设为1,开启分页 
    mov cr0,eax
    
    lgdt [gdt_ptr]                            ;加载映射后的新的GDT地址
    
    mov eax,SELECTOR_VIDEO
    mov gs,eax                                ;重新加载显存段
    mov byte [gs:160],'V'                     ;验证分页是否开启成功,如果屏幕上显示了 V,说明显存映射正常,分页工作正常
    
    jmp SELECTOR_CODE:enter_kernel            ;跳转到内核区
    
;------------------------------ 跳转到内核区    

enter_kernel:
    call kernel_init	                      ;初始化内核(具体实现在后面)解析内核ELF文件,把内核的各个段加载到正确的虚拟地址上,完成重定位,为执行内核做准备				          
    mov  esp,0xc009f000                       ;内核设置新的栈指针
    jmp  KERNEL_ENTER_ADDR                    ;跳转到内核的入口

;------------------------------- 创建页表 ------------------------------------------------    
setup_page:
    mov ecx,0x1000                                             
    mov esi,0                                                   
 .clear_page_dir_mem:                         ;对页目录表进行初始化,全部清空                
    mov byte [PAGE_DIR_TABLE_POS+esi],0
    inc esi
    loop .clear_page_dir_mem
    
 .create_pde:                                 ;创建一个页目录项
    mov eax,PAGE_DIR_TABLE_POS				  
    add eax,0x1000                            ;页表物理地址 = PAGE_DIR_TABLE_POS + 0x1000                  
    or  eax, PG_P | PG_RW_W | PG_US_U                          
     
    mov [PAGE_DIR_TABLE_POS+0x0],eax                            
    mov [PAGE_DIR_TABLE_POS+0xc00],eax                        
    sub eax,0x1000      
    
    mov [PAGE_DIR_TABLE_POS+4092],eax                           

   
    mov eax,PAGE_DIR_TABLE_POS                ;
    add eax,0x1000
    mov ecx,256
    mov esi,0
    mov ebx,PG_P | PG_RW_W | PG_US_U 
    
 .create_kernel_pte:                          ;填充低地址页表:物理地址 0x00000000 ~ 0x000FFFFF(0~1MB)的物理内存,映射到虚拟地址
    mov [eax+esi*4],ebx                       
    inc esi
    add ebx,0x1000
    loop .create_kernel_pte 
    
                                              ;为内核的高地址空间页目录项设置初始的页表地址
    mov eax,PAGE_DIR_TABLE_POS                ;为内核高地址空间(0xC0400000 ~ 0xFFFFFFFF),初始化页目录项,让它们指向连续的页表
    add eax,0x2000       					   
    or  eax,PG_P | PG_RW_W | PG_US_U
    mov ebx,PAGE_DIR_TABLE_POS
    mov ecx,254						  
    mov esi,769
        
 .create_kernel_pde:                          
    mov [ebx+esi*4],eax
    inc esi
    add eax,0x1000
    loop .create_kernel_pde 
    
    ret            
    
;----------------------- 初始化内核 把缓冲区的内核代码放到0x1500区域 ------------------------------------------
;这个地方主要对elf文件头部分用的很多
;可以参照着书上给的格式 来比较对比
kernel_init:
    xor eax,eax                               ;寄存器全部清零
    xor ebx,ebx
    xor ecx,ecx
    xor edx,edx
    
                                              ;解析 ELF 文件头,获取程序段表信息
    mov ebx,[KERNEL_BIN_BASE_ADDR+28]         
    add ebx,KERNEL_BIN_BASE_ADDR              ;ebx当前位置为程序段表
    mov dx,[KERNEL_BIN_BASE_ADDR+42]	      ;获取程序段表每个条目描述符字节大小
    mov cx,[KERNEL_BIN_BASE_ADDR+44]          ;段的总数量
    

                                              ;遍历每个程序段     
 .get_each_segment:
    cmp dword [ebx+0],PT_NULL
    je .PTNULL                                                 
    
    mov eax,[ebx+8]
    cmp eax,0xc0001500
    jb .PTNULL
    
        
    push dword [ebx+16]                                        
                                                               
    mov eax,[ebx+4]                                            
    add eax,KERNEL_BIN_BASE_ADDR
    push eax                                                          
    push dword [ebx+8]                                        
     
    call mem_cpy
    add esp,12                                                 
    
                                            ;若当前段是空的,则跳过当前段,对下一个段进行处理
 .PTNULL:
    add  ebx,edx                                               
    loop .get_each_segment                                       
    ret
                                            ;对内存的数据进行复制备份
mem_cpy:
    cld                                     ;清除方向标志位,确保复制数据时是从低位到高位进行复制的
          
    push ebp                                ;建立栈帧、保存寄存器                           
    mov  ebp,esp 
    push ecx                                                
    
                                                              
                                                              
    mov edi,[ebp+8]                         ;[ebp+8]对应目标地址,存入edi                     
    mov esi,[ebp+12]                        ;[ebp+12]对应源地址, 存入esi                
    mov ecx,[ebp+16]                        ;[ebp+16]对应"一次复制多大的数据" ,存入ecx                 
    
    
    rep movsb                               ;开始进行复制,一个一个字节复制
       
    pop ecx                                 ;恢复栈帧、寄存器
    pop ebp
    ret
    
;------------------------ rd_disk_m_32  ----------------------
rd_disk_m_32:
;1 写入待操作磁盘数
;2 写入LBA 低24位寄存器 确认扇区
;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1
;4 command 写指令
;5 读取status状态寄存器 判断是否完成工作
;6 完成工作 取出数据
 
 ;;;;;;;;;;;;;;;;;;;;;
 ;1 写入待操作磁盘数
 ;;;;;;;;;;;;;;;;;;;;;
    mov esi,eax   ; !!! 备份eax
    mov di,cx     ; !!! 备份cx
    
    mov dx,0x1F2  ; 0x1F2为Sector Count 端口号 送到dx寄存器中
    mov al,cl     ; !!! 忘了只能由ax al传递数据
    out dx,al     ; !!! 这里修改了 原out dx,cl
    
    mov eax,esi   ; !!!袄无! 原来备份是这个用 前面需要ax来传递数据 麻了
    
;;;;;;;;;;;;;;;;;;;;;
;2 写入LBA 24位寄存器 确认扇区
;;;;;;;;;;;;;;;;;;;;;
    mov cl,0x8    ; shr 右移8位 把24位给送到 LBA low mid high 寄存器中

    mov dx,0x1F3  ; LBA low
    out dx,al 
    
    mov dx,0x1F4  ; LBA mid
    shr eax,cl    ; eax为32位 ax为16位 eax的低位字节 右移8位即8~15
    out dx,al
    
    mov dx,0x1F5
    shr eax,cl
    out dx,al
    
;;;;;;;;;;;;;;;;;;;;;
;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1
;;;;;;;;;;;;;;;;;;;;;

    		 
    		  ; 24 25 26 27位 尽管我们知道ax只有2 但还是需要按规矩办事 
    		  ; 把除了最后四位的其他位置设置成0
    shr eax,cl
    
    and al,0x0f 
    or al,0xe0   ;!!! 把第四-七位设置成0111 转换为LBA模式
    mov dx,0x1F6 ; 参照硬盘控制器端口表 Device 
    out dx,al

;;;;;;;;;;;;;;;;;;;;;
;4 向Command写操作 Status和Command一个寄存器
;;;;;;;;;;;;;;;;;;;;;

    mov dx,0x1F7 ; Status寄存器端口号
    mov ax,0x20  ; 0x20是读命令
    out dx,al
    
;;;;;;;;;;;;;;;;;;;;;
;5 向Status查看是否准备好惹 
;;;;;;;;;;;;;;;;;;;;;
    
		   ;设置不断读取重复 如果不为1则一直循环
  .not_ready:     
    nop           ; !!! 空跳转指令 在循环中达到延时目的
    in al,dx      ; 把寄存器中的信息返还出来
    and al,0x88   ; !!! 0100 0100 0x88
    cmp al,0x08
    jne .not_ready ; !!! jump not equal == 0
    
    
;;;;;;;;;;;;;;;;;;;;;
;6 读取数据
;;;;;;;;;;;;;;;;;;;;;

    mov ax,di      ;把 di 储存的cx 取出来
    mov dx,256
    mul dx        ;与di 与 ax 做乘法 计算一共需要读多少次 方便作循环 低16位放ax 高16位放dx
    mov cx,ax      ;loop 与 cx相匹配 cx-- 当cx == 0即跳出循环
    mov dx,0x1F0
 .go_read_loop:
    in ax,dx      ;两字节dx 一次读两字
    mov [ebx],ax
    add ebx,2
    loop .go_read_loop
    ret ;与call 配对返回原来的位置 跳转到call下一条指令

四、仿真运行验证

同样,进入bochs仿真

复制代码
写入loader.s:
nasm -I /home/xpy/bochs/include/ -o loader.bin loader.s
dd if=/home/xpy/bochs/boot/loader.bin of=/home/xpy/bochs/hd60M.img bs=512 count=3 seek=2 conv=notrunc


bin/bochs -f bochsrc.disk 

输入c,结果如下图所示,字母V被打印了,说明分页成功开启:

输入 ctrl+c,中断仿真,再输入info gdt 查看全局描述符表,其正确显示了:

输入 info tab,查看当前的页表、段描述符表、内存映射关系:

映射关系正确显示

在bochs下新建文件夹kernel,在kernel文件夹内新建内核文件main.c:

复制代码
int main()
{
    while(1);
    return 0;
}

然后再kernel文件夹下对内核文件进行编译与写入:

复制代码
​
;把 C 语言源码 编译成 32 位目标文件,还没链接
gcc -m32 -c -o /home/xpy/bochs/kernel/main.o /home/xpy/bochs/kernel/main.c


;链接 生成内核镜像 kernel.bin
;-m elf_i386:生成 32 位 ELF 格式(内核必须是这个格式)
;-Ttext 0xc0001500:指定代码段起始地址 = 0xc0001500
;这是内核的虚拟地址,和你汇编代码里的 KERNEL_ENTER_ADDR 完全一致
;-e main:指定入口符号是 main
ld -m elf_i386 /home/xpy/bochs/kernel/main.o -Ttext 0xc0001500 -e main -o /home/xpy/bochs/kernel/kernel.bin

;把内核写入虚拟机硬盘
dd if=/home/xpy/bochs/kernel/kernel.bin of=/home/xpy/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc

​

在kernel文件夹下打开终端输入命令:xxd kernel.bin 查看内核二进制文件的信息:

在bochs仿真软件内输入命令:xp /100 0x70000 ,查看内存0x70000处是否存储的是内核二进制文件信息:

发现全为0,说明没有正确写入,先排查虚拟硬盘问题:在bochs文件夹下输入以下命令:

复制代码
检查 hd60M.img 第 9 扇区是否有内核
# 从 hd60M.img 读取第9扇区开始的200个扇区,查看开头是否是ELF魔数 0x7f454c46
dd if=hd60M.img bs=512 skip=9 count=1 | xxd

开头显示 7f 45 4c 46,说明写入成功,再次进行仿真,进入boch软件,输入以下命令:

复制代码
b 0x7000     ;运行到该位置
c            ;集训运行,此时才开始加载loader程序
xp 0x70000

结果如下所示:

开头显示 7f 45 4c 46,说明成功写入(一些与原作者细微的差距是因为编译器的不同导致的,只要开头出现了 7f 45 4c 46,就说明内核成功写入了

然后在kernel文件夹下进入终端,输入以下命令:

复制代码
;查看 ELF 文件详细信息
;-e = 显示全部信息(全部头)

readelf -e kernel.bin

结果如下所示:

与上面在bochs中查看的相一致,成功。

相关推荐
小叮当⇔2 小时前
M4A 转 MP3 桌面转换器(PyQt5 + FFmpeg)
开发语言·qt·ffmpeg
aq55356002 小时前
Laravel9.x新特性全解析
java·开发语言·数据库
代码中介商2 小时前
Linux 信号处理与进程控制深度解析
linux·运维·信号处理
珹洺2 小时前
C++AI多模型聊天系统(三)AI多模型(豆包/Kimi/千问)接入与实现
开发语言·c++·人工智能
姚青&2 小时前
Linux 文件处理命令
linux·运维·服务器
tryqaaa_2 小时前
学习日志(二)【linux全部命令,http请求头{有例题},Php语法学习】
linux·学习·http·php·web
万法若空3 小时前
ANSI转义码详解
linux·c++
Bat U3 小时前
JavaEE|多线程(五)
java·开发语言·jvm
计算机安禾3 小时前
【Linux从入门到精通】第21篇:Shell脚本开篇——什么是Shell?写第一个Hello World
linux·运维·服务器