《操作系统真象还原》第五章——加载内核

2024-05-07 06:44

本文主要是介绍《操作系统真象还原》第五章——加载内核,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

elf文件

elf文件介绍

一个程序文件需要有程序头来说明程序的入口地址及其相关信息,如下所示

程序是由段(如代码段、数据段)和节组成的,因此在程序头中要有一个段头表(程序头表)和节头表来描述程序中各种段及节的信息,故

  • 程序头表:也称段头表,其内元素用于描述程序中的各个段
  • 节头表:其内元素用于描述程序中的各个节

由于程序头(段头)和节头的数量不固定,因此程序头表和节头表的大小也就不固定,因此需要一个数据结构来说明程序头表和节头表的大小和位置信息,这个数据结构就是elf

elf文件格式

ELF格式的作用体现在两方面,一是链接阶段:另一方面是运行阶段,故它们在文件中组织布局咱们也从这两方面展示,如图所示。 

elf header结构

elf header的数据类型

elf header结构

  • e_ident[16]:16字节,用来表示elf字符等信息,开头的4个字节是固定不变的,是elf文件的魔数,它们分别是0x7f,以及字符串ELF的asc码:0x45,0x4c,0x46

  • e_type:2字节,指定elf目标文件的类型

  • e_machine:2字节,描述elf目标文件要在那种硬件平台运行

  • e_version:4字节,版本信息
  • e_entry:占用4字节,用来指明操作系统运行该程序时,将控制权转交到的虚拟地址
  • e_phoff(program header table offset):4字节,程序头表在文件内的字节偏移量
  • e_shoff(section header table offset):4字节,节头表在文件内的偏移量
  • e_flags:4字节,指明与处理器相关的标志
  • e_ehsize:2字节,指明elf header字节大小
  • e_phentsize;2字节,指明程序头表中每个条目(entry)的字节大小,也就是每个用来描述段信息的数据结构的字节大小
  • e_phnum:2字节,程序头表中条目的数量
  • e_shentsize:2字节,节头表中每个条目的字节大小
  • e_shnum:2字节,节头表中条目的数量
  • e_shstrndx:2字节,指明string name table在节头表中的索引index

程序头表中条目的数据结构

  • p_type:4字节,程序中段的类型

  • p_offset:4字节,本段在文件内的起始偏移地址
  • p_vaddr:4字节,本段在内存中的起始虚拟地址
  • p_paddr:4字节,暂且保留,未设定
  • p_filez:4字节,本段在文件中的大小
  • p_memsz:4字节,本段在内存中的大小
  • p_flags:4字节,指明与本段相关的标志

  • p_align:4字节,用来指明本段在文件和内存中的对齐方式。如果值为0或1,则表示不对齐。否则p_align应该是2的幂次数。

将内核载入内存

将loader中栈指针地址的宏转移到boot.inc中

    LOADER_STACK_TOP equ LOADER_BASE_ADDR           ;初始化栈指针地址

同时在boot.inc中定义内核相关的宏

    KERNEL_BIN_BASE_ADDR equ 0x70000                ;内核文件加载到内存中的位置KERNEL_START_SECTOR equ 0x9                     ;内核文件在磁盘中的起始盘区KERNEL_ENTRY_POINT equ 0xc0001500               ;定义内核可执行代码的入口地址;-----  程序段的类型定义  ---------PT_NULL equ 0

在分页机制开启之前将内核从磁盘加载到内存中

;------------- 加载内核 ----------------
;将内核从磁盘的9号扇区加载的内存的KERNEL_BIN_BASE_ADDR地址处mov eax,KERNEL_START_SECTORmov ebx,KERNEL_BIN_BASE_ADDRmov ecx,200 call rd_disk_m_32
rd_disk_m_32:	   ; eax=LBA扇区号; ebx=将数据写入的内存地址; ecx=读入的扇区数mov esi,eax	                                        ;备份eaxmov di,cx		                                    ;备份cx;读写硬盘:;第1步:选择特定通道的寄存器,设置要读取的扇区数mov dx,0x1f2mov al,clout dx,al                                           ;读取的扇区数mov eax,esi	                                        ;恢复ax;第2步:在特定通道寄存器中放入要读取扇区的地址,将LBA地址存入0x1f3 ~ 0x1f6;LBA地址7~0位写入端口0x1f3mov dx,0x1f3                       out dx,al                          ;LBA地址15~8位写入端口0x1f4mov cl,8shr eax,clmov dx,0x1f4out dx,al;LBA地址23~16位写入端口0x1f5shr eax,clmov dx,0x1f5out dx,alshr eax,cland al,0x0f	                                        ;lba第24~27位or al,0xe0	                                        ; 设置7~4位为1110,表示lba模式mov dx,0x1f6out dx,al;第3步:向0x1f7端口写入读命令,0x20 mov dx,0x1f7mov al,0x20                        out dx,al;第4步:检测硬盘状态
.not_ready:;同一端口,写时表示写入命令字,读时表示读入硬盘状态nopin al,dxand al,0x88	                                        ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙cmp al,0x08jnz .not_ready	                                    ;若未准备好,继续等。;第5步:从0x1f0端口读数据mov ax, di                                          ;di当中存储的是要读取的扇区数mov dx, 256                                         ;每个扇区512字节,一次读取两个字节,所以一个扇区就要读取256次,与扇区数相乘,就等得到总读取次数mul dx                                              ;8位乘法与16位乘法知识查看书p133,注意:16位乘法会改变dx的值!!!!mov cx, ax	                                        ; 得到了要读取的总次数,然后将这个数字放入cx中mov dx, 0x1f0
.go_on_read:in ax,dxmov [ebx],ax                                        ;与rd_disk_m_16相比,就是把这两句的bx改成了ebxadd 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_readret

遍历内存中内核的elf文件头,取出其内的每一个段头,再将其拷贝到内存中对应的位置

;------------- 进入内核函数 ----------------
enter_kernel:call kernel_initmov esp,0xc009f000jmp KERNEL_ENTRY_POINTkernel_init:xor eax,eaxxor ebx,ebx                             ;记录程序头(段)地址xor ecx,ecx                             ;记录程序头表(段头表)中程序头数量xor edx,edx                             ;记录程序头(段)大小mov dx,[KERNEL_BIN_BASE_ADDR+42]        ;42字节处是e_phentsize,即程序头(段)大小mov ebx,[KERNEL_BIN_BASE_ADDR+28]       ;28字节处是e_phoff,即程序头表的偏移add ebx,KERNEL_BIN_BASE_ADDR            ;程序头表的偏移加上内核在内存中的起始地址,就是程序头表的虚拟起始地址mov cx,[KERNEL_BIN_BASE_ADDR+44]        ;44字节处是e_phnum,即程序头表(段头表)中程序头数量;遍历段头表
.each_segment:
;检查段头表中的段头是会否是空段(PT_NULL),如果不是就将该段拷贝到对应区域,否则就继续遍历下一个段头cmp byte [ebx+0],PT_NULL                ;比较p_type是否等于PT_NULL,若相等说明程序头未使用je .PTNULL                              ;若相等则跳转到.PTNULL;为函数memcpy(dst,src,size)压入参数push dword [ebx+16]                     ;实参size,程序头表偏移16字节的地方p_sizesz,本段在文件内的大小mov eax,[ebx+4]                         ;程序头表偏移4字节的地方p_offset,本段在文件内的起始偏移add eax,KERNEL_BIN_BASE_ADDRpush eax                                ;实参srcpush dword [ebx+8]                      ;实参dst,p_vaddr,本段在内存中的起始虚拟地址call mem_cpyadd esp,12                              ;回收mem_cpy的栈帧空间.PTNULL:add ebx,edx                             ;指向下一个段头loop .each_segment                      ;继续遍历段头表ret

其中逐字节拷贝函数为

;逐字节拷贝函数,将esi指向的内存区域的size个字节拷贝到edi指向的区域
mem_cpy:cld                                    ;指明拷贝时esi与edi的增长方向是向上的push ebp                               ;保存ebpmov ebp,esp                            ;将esp指向ebppush ecx                               ;由于rep指令会用到ecx的循环计数,;而外层函数也用到了ecx的值,;因此此处需要将外层函数的ecx的值进行备份mov edi,[ebp+8]                        ;参数dstmov esi,[ebp+12]                       ;参数srcmov ecx,[ebp+16]                       ;参数sizerep movsb                              ;rep(repeat)指令,重复执行movsb指令;movsb指令,s表示string,b表示byte,;即将esi指向的内存拷贝一个字节给edi指向的内存;因此本条指令表示逐字节拷贝,拷贝的字节个数为ecx的值pop ecx                                ;取出备份的值pop ebp                                ;返回上层函数ret

完整代码

boot.inc

;-----loader and kernel-----LOADER_BASE_ADDR equ 0x900                      ;loader在内存中位置LOADER_START_SECTOR equ 0x2                     ;loader在磁盘中的逻辑扇区地址,即LBA地址LOADER_STACK_TOP equ LOADER_BASE_ADDR           ;初始化栈指针地址PAGE_DIR_TABLE_POS equ 0x100000                 ;页目录表基址KERNEL_BIN_BASE_ADDR equ 0x70000                ;内核文件加载到内存中的位置KERNEL_START_SECTOR equ 0x9                     ;内核文件在磁盘中的起始盘区KERNEL_ENTRY_POINT equ 0xc0001500               ;定义内核可执行代码的入口地址;-----  gdt描述符属性  ---------DESC_G_4K   equ   1_00000000000000000000000b    ;设置段界限的单位为4KBDESC_D_32   equ    1_0000000000000000000000b    ;设置代码段/数据段的有效地址(段内偏移地址)及操作数大小为32位DESC_L    equ       0_000000000000000000000b    ;64位代码段标记位,现在是32位操作系统,因此标记为0即可。DESC_AVL  equ        0_00000000000000000000b;定义段界限位;段界限的第2部分,即描述符的高32位中的第16~19位,最终的代码段段界限为0xFFFFFDESC_LIMIT_CODE2 equ 1111_0000000000000000b     ;定义代码段要用的段描述符高32位中16~19段界限为全1DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2           ;定义数据段要用的段描述符高32位中16~19段界限为全1DESC_LIMIT_VIDEO2  equ 0000_000000000000000b    ;定义我们要操作显存时对应的段描述符的高32位中16~19段界限为全0DESC_P  equ 1_000000000000000b                  ;定义了段描述符中的P标志位,表示该段描述符指向的段是否在内存中;定义描述符的特权级别位DESC_DPL_0 equ 00_0000000000000bDESC_DPL_1 equ 01_0000000000000bDESC_DPL_2 equ 10_0000000000000bDESC_DPL_3 equ 11_0000000000000b
;定义类型位DESC_S_CODE equ 1_000000000000b                 ;代码段和数据段都是非系统段,故类型字段s设置为1DESC_S_DATA equ DESC_S_CODE                     ;代码段和数据段都是非系统段,故类型字段s设置为1DESC_S_sys equ 0_000000000000b                  ;系统段的类型字段设置为0
;定义子类型位DESC_TYPE_CODE equ 1000_00000000b               ;x=1,c=0,r=0,a=0 代码段是可执行的,非一致性,不可读,已访问位a清0DESC_TYPE_DATA equ 0010_00000000b               ;x=0,e=0,w=1,a=0,数据段不可执行,向上扩展,可写,已访问位a清0;拼接代码段的描述符
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
;拼接数据段的描述符
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;拼接显存段的描述符位                    
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 00bRPL1 equ 01bRPL2 equ 10bRPL3 equ 11bTI_GDT equ 000bTI_LDT equ 100b;-----  模块化的页目录表字段  ---------PG_P equ 1bPG_RW_R equ 00bPG_RW_W equ 10bPG_US_S equ 000bPG_US_U equ 100b;-----  程序段的类型定义  ---------PT_NULL equ 0

loader.S

%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR;------------- 构建gdt及其内部的描述符 -------------GDT_BASE:   dd 0x00000000dd 0x00000000;代码段描述符的低4字节部分,其中高两个字节表示段基址的0~15位,在这里定义为0x0000;低两个字节表示段界限的0~15位,由于使用的是平坦模型,因此是0xFFFFCODE_DESC:  dd 0x0000FFFFdd DESC_CODE_HIGH4   ;段描述符的高4字节部分DATA_STACK_DESC: dd 0x0000FFFFdd DESC_DATA_HIGH4;定义显存段的描述符;文本模式下的适配器地址为0xb8000~0xbffff,为了方便显存操作,显存段不使用平坦模型;因此段基址为0xb8000,段大小为0xbffff-0xb8000=0x7fff,;段粒度位4k,因此段界限的值为0x7fff/4k=7VIDEO_DESC: dd 0x80000007dd DESC_VIDEO_HIGH4GDT_SIZE equ $-GDT_BASEGDT_LIMIT equ GDT_SIZE-1times 60 dq 0            ;此处预留60个描述符的空位;------------- 构建选择子 -------------SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0total_mem_bytes dd 0    ;total_mem_bytes用于保存最终获取到的内存容量,为4个字节;由于loader程序的加载地址为0x900,而loader.bin的文件头大小为0x200;(4个gdt段描述符(8B)加上60个dp(8B)填充字,故64*8=512B),;故total_mem_bytes在内存中的地址为0x900+0x200=0xc00;该地址将来在内核中会被用到;------------- 定义gdtr(指向GDT的寄存器) -------------gdt_ptr dw GDT_LIMITdd GDT_BASEards_buf times 244 db 0 ;开辟一块缓冲区,用于记录返回的ARDS结构体,;该定义语句事实上是定义了一个数组ards_buf[244];244是因为total_mem_bytes(4)+gdt_ptr(6)+244+ards_nr(2)=256,即0x100;这样loader_start的在文件内的偏移地址就是0x100+0x200=0x300ards_nr dw 0            ;用于记录ards结构体数量;------------------------------------------
;INT 0x15 功能号:0xe820 功能描述:获取内存容量,检测内存
;------------------------------------------
;输入:;EAX:功能号,0xE820,但调用返回时eax会被填入一串ASCII值;EBX:ARDS后续值;ES:di:ARDS缓冲区,BIOS将获取到的内存信息存到此寄存器指向的内存,每次都以ARDS格式返回;ECX:ARDS结构的字节大小,20;EDX:固定为签名标记,0x534d4150;返回值;CF:若cf为0,表示未出错,cf为1,表示调用出错;EAX:字符串SMAP的ASCII码值,0x534d4150;ES:di:ARDS缓冲区,BIOS将获取到的内存信息存到此寄存器指向的内存,每次都以ARDS格式返回;ECX:ARDS结构的字节大小,20;EBX:ARDS后续值,即下一个ARDS的位置。;每次BIOS中断返回后,BIOS会更新此值,BIOS会通过此值找到下一个待返回的ARDS结构。;在cf位为0的情况下,若返回后的EBX值为0,表示这是最后一个ARDS结构loader_start:xor ebx,ebx             ;第一次调用时,要将ebx清空置为0,此处使用的是异或运算置0mov edx,0x534d4150mov di,ards_buf         ;di存储缓冲区地址,即指向缓冲区首地址.e820_mem_get_loop:mov eax,0x0000e820mov ecx,20              ;一个ards结构体的大小int 0x15                ;调用0x15中断函数,返回的ards结构体被返回给di指向的缓冲区中add di,cx               ;使di增加20字节指向缓冲区中下一个的ARDS结构位置inc word [ards_nr]      ;inc(increment增加)指令表示将内存中的操作数增加一,此处用于记录返回的ARDS数量cmp ebx,0               ;比较ebx中的值是否为0jnz .e820_mem_get_loop  ;若ebx不为0,则继续进行循环获取ARDS,;若为0说明已经获取到最后一个ards,则退出循环mov cx,[ards_nr]        ;cx存储遍历到的ards结构体个数mov ebx,ards_buf        ;ebx指向缓冲区地址xor edx,edx             ;EDX用于保存BaseAddrLow+LengthLow最大值,此处初始化为0.find_max_mem_area:mov eax,[ebx]           ;eax用于遍历缓冲区中的每一个ards的BaseAddrLowadd eax,[ebx+8]         ;ebx+8获取的是LengthLow,故该代码计算的是BaseAddrLow+LengthLowadd ebx,20              ;遍历下一个ardscmp edx,eax             ;分支语句,如果edx大于等于eax,则跳转到.next_ards,也就是进入循环jge .next_ardsmov edx,eax             ;否则就是更新edx
.next_ards:loop .find_max_mem_areamov [total_mem_bytes],edx   ;将最终结果保存到total_mem_bytes;------------- 准备进入保护模式 -------------
;1.打开A20
;2.加载gdt
;3.置cr0的PE位为1;------------- 打开A20 -------------in al,0x92or al,0000_0010Bout 0x92,al;------------- 加载gdt -------------lgdt [gdt_ptr];------------- 置cr0的PE位为1 -------------mov eax,cr0or eax,0x00000001mov cr0,eaxjmp dword SELECTOR_CODE:p_mode_start    ;刷新流水线.error_hlt:hlt                                     ;出错则挂起[bits 32]
p_mode_start:mov ax,SELECTOR_DATA                    ;初始化段寄存器,将数据段的选择子分别放入各段寄存器mov ds,axmov es,ax                      mov ss,axmov esp,LOADER_STACK_TOP                ;初始化栈指针,将栈指针地址放入bsp寄存器mov ax,SELECTOR_VIDEO                   ;初始化显存段寄存器,显存段的选择子放入gs寄存器mov gs,ax;------------- 加载内核 ----------------
;将内核从磁盘的9号扇区加载的内存的KERNEL_BIN_BASE_ADDR地址处mov eax,KERNEL_START_SECTORmov ebx,KERNEL_BIN_BASE_ADDRmov ecx,200 call rd_disk_m_32;------------- 开启分页机制 ----------------call setup_page                         ;创建页目录表和页表,并初始化页内存位图mov ebx,[gdt_ptr+2]                     ;gdt_ptr+2表示GDT_BASE,也就是GDT的起始地址or dword [ebx+0x18+4],0xc0000000        ;ebx中保存着GDT_BASE,0x18=24,故ebx+0x18表示取出显存段的起始地址;+4表示取出段描述符的高32位,之后和0xc0000000进行或操作add dword [gdt_ptr+2],0xc0000000        ;同理将GDT_BASE的起始地址也增加3Gadd esp,0xc0000000                      ;同理将esp栈指针的起始地址也增加3Gmov eax,PAGE_DIR_TABLE_POSmov cr3,eaxmov eax,cr0                             ;打开cr0的PG位or eax,0x80000000mov cr0,eaxlgdt [gdt_ptr]                          ;开启分页后,用gdt的新地址重新加载;------------- 进入内核函数 ----------------
enter_kernel:call kernel_initmov esp,0xc009f000jmp KERNEL_ENTRY_POINTkernel_init:xor eax,eaxxor ebx,ebx                             ;记录程序头(段)地址xor ecx,ecx                             ;记录程序头表(段头表)中程序头数量xor edx,edx                             ;记录程序头(段)大小mov dx,[KERNEL_BIN_BASE_ADDR+42]        ;42字节处是e_phentsize,即程序头(段)大小mov ebx,[KERNEL_BIN_BASE_ADDR+28]       ;28字节处是e_phoff,即程序头表的偏移add ebx,KERNEL_BIN_BASE_ADDR            ;程序头表的偏移加上内核在内存中的起始地址,就是程序头表的虚拟起始地址mov cx,[KERNEL_BIN_BASE_ADDR+44]        ;44字节处是e_phnum,即程序头表(段头表)中程序头数量;遍历段头表
.each_segment:
;检查段头表中的段头是会否是空段(PT_NULL),如果不是就将该段拷贝到对应区域,否则就继续遍历下一个段头cmp byte [ebx+0],PT_NULL                ;比较p_type是否等于PT_NULL,若相等说明程序头未使用je .PTNULL                              ;若相等则跳转到.PTNULL;为函数memcpy(dst,src,size)压入参数push dword [ebx+16]                     ;实参size,程序头表偏移16字节的地方p_sizesz,本段在文件内的大小mov eax,[ebx+4]                         ;程序头表偏移4字节的地方p_offset,本段在文件内的起始偏移add eax,KERNEL_BIN_BASE_ADDRpush eax                                ;实参srcpush dword [ebx+8]                      ;实参dst,p_vaddr,本段在内存中的起始虚拟地址call mem_cpyadd esp,12                              ;回收mem_cpy的栈帧空间.PTNULL:add ebx,edx                             ;指向下一个段头loop .each_segment                      ;继续遍历段头表ret;逐字节拷贝函数,将esi指向的内存区域的size个字节拷贝到edi指向的区域
mem_cpy:cld                                    ;指明拷贝时esi与edi的增长方向是向上的push ebp                               ;保存ebpmov ebp,esp                            ;将esp指向ebppush ecx                               ;由于rep指令会用到ecx的循环计数,;而外层函数也用到了ecx的值,;因此此处需要将外层函数的ecx的值进行备份mov edi,[ebp+8]                        ;参数dstmov esi,[ebp+12]                       ;参数srcmov ecx,[ebp+16]                       ;参数sizerep movsb                              ;rep(repeat)指令,重复执行movsb指令;movsb指令,s表示string,b表示byte,;即将esi指向的内存拷贝一个字节给edi指向的内存;因此本条指令表示逐字节拷贝,拷贝的字节个数为ecx的值pop ecx                                ;取出备份的值pop ebp                                ;返回上层函数ret;------------- 创建页目录表和页表 -------------
;初始化页目录表和页表;逐字节清空页目录表
setup_page:mov ecx,4096                                    ;页目录表的大小为4KB,ecx是loop指令的循环计数器;由于初始化页表是逐字节置0的,因此ecx的值为4096mov esi,0                                       ;页目录表的偏移量
.clear_page_dir:mov byte [PAGE_DIR_TABLE_POS+esi],0             ;逐字节清空页目录表;其中PAGE_DIR_TABLE_POS为页目录表初始地址的宏inc esi                                         ;递增偏移量,清空下一个字节loop .clear_page_dir;初始化创建页目录表
.create_pde:mov eax,PAGE_DIR_TABLE_POS                      ;eax保存页目录表的起始地址add eax,0x1000                                  ;0x1000为1k,故该代码的计算结果是将eax指向第一张页表的起始地址mov ebx,eax                                     ;ebx保存第一张页表的起始地址,后续会用到or eax,PG_US_U|PG_RW_W|PG_P                     ;eax已经有了第一张页表的起始地址;此处再加上属性,即可表示为页目录表的一个表项,;该表项代表的是第一张页表的物理地址及其相关属性mov [PAGE_DIR_TABLE_POS+0x0],eax                ;页目录表的第一个表项指向第一张页表mov [PAGE_DIR_TABLE_POS+0xc00],eax              ;0xc0000000即为3GB,由于我们划分的虚拟地址空间3GB以上为os地址空间;因此该语句是将3GB的虚拟空间映射到内核空间 ;而0xc00/4=768,也就是说页目录表的768号表项映射为物理内核空间sub eax,0x1000          mov [PAGE_DIR_TABLE_POS+4092],eax               ;最后一个页表项指向自己,为将来动态操作页表做准备;创建第一张页表的页表项,由于os的物理内存不会超过1M,故页表项个数的最大值为1M/4k=256mov ecx,256                                     ;循环计数器mov esi,0                                       ;偏移量mov edx,PG_US_S|PG_RW_W|PG_P                    ;此时的edx表示拥有属性PG_US_S|PG_RW_W|PG_P;且物理地址为0的物理页的页表项
.create_pte:mov [ebx+esi*4],edx                             ;此前ebx已经保存了第一张页表的起始地址add edx,4096                                    ;edx指向下一个物理页(一个物理页4KB)inc esi                                         ;esi指向页表的下一个偏移loop .create_pte; -------------------初始化页目录表769号-1022号项,769号项指向第二个页表的地址(此页表紧挨着上面的第一个页表),770号指向第三个,以此类推mov eax,PAGE_DIR_TABLE_POSadd eax,0x2000                                  ;此时的eax表示第二张页表的起始地址or eax,PG_US_U|PG_RW_W|PG_P                     ;为eax表项添加属性mov ebx,PAGE_DIR_TABLE_POSmov ecx,254                                     ;要设置254个页表项mov esi,769                                     ;从第769个页表项开始设置
.create_kernel_pde:mov [ebx+esi*4],eax                             ; 设置页目录表项inc esi                                         ; 增加要设置的页目录表项的偏移add eax,0x1000                                  ; eax指向下一个页表的位置,由于之前设定了属性,所以eax是一个完整的指向下一个页表的页目录表项loop .create_kernel_pde                         ; 循环设定254个页目录表项ret;-------------------------------------------------------------------------------
rd_disk_m_32:	   ; eax=LBA扇区号; ebx=将数据写入的内存地址; ecx=读入的扇区数mov esi,eax	                                        ;备份eaxmov di,cx		                                    ;备份cx;读写硬盘:;第1步:选择特定通道的寄存器,设置要读取的扇区数mov dx,0x1f2mov al,clout dx,al                                           ;读取的扇区数mov eax,esi	                                        ;恢复ax;第2步:在特定通道寄存器中放入要读取扇区的地址,将LBA地址存入0x1f3 ~ 0x1f6;LBA地址7~0位写入端口0x1f3mov dx,0x1f3                       out dx,al                          ;LBA地址15~8位写入端口0x1f4mov cl,8shr eax,clmov dx,0x1f4out dx,al;LBA地址23~16位写入端口0x1f5shr eax,clmov dx,0x1f5out dx,alshr eax,cland al,0x0f	                                        ;lba第24~27位or al,0xe0	                                        ; 设置7~4位为1110,表示lba模式mov dx,0x1f6out dx,al;第3步:向0x1f7端口写入读命令,0x20 mov dx,0x1f7mov al,0x20                        out dx,al;第4步:检测硬盘状态
.not_ready:;同一端口,写时表示写入命令字,读时表示读入硬盘状态nopin al,dxand al,0x88	                                        ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙cmp al,0x08jnz .not_ready	                                    ;若未准备好,继续等。;第5步:从0x1f0端口读数据mov ax, di                                          ;di当中存储的是要读取的扇区数mov dx, 256                                         ;每个扇区512字节,一次读取两个字节,所以一个扇区就要读取256次,与扇区数相乘,就等得到总读取次数mul dx                                              ;8位乘法与16位乘法知识查看书p133,注意:16位乘法会改变dx的值!!!!mov cx, ax	                                        ; 得到了要读取的总次数,然后将这个数字放入cx中mov dx, 0x1f0
.go_on_read:in ax,dxmov [ebx],ax                                        ;与rd_disk_m_16相比,就是把这两句的bx改成了ebxadd 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_readret

结果

dd if=./build/kernel.bin of=~/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc

这篇关于《操作系统真象还原》第五章——加载内核的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/966609

相关文章

Spring Boot 配置文件之类型、加载顺序与最佳实践记录

《SpringBoot配置文件之类型、加载顺序与最佳实践记录》SpringBoot的配置文件是灵活且强大的工具,通过合理的配置管理,可以让应用开发和部署更加高效,无论是简单的属性配置,还是复杂... 目录Spring Boot 配置文件详解一、Spring Boot 配置文件类型1.1 applicatio

新特性抢先看! Ubuntu 25.04 Beta 发布:Linux 6.14 内核

《新特性抢先看!Ubuntu25.04Beta发布:Linux6.14内核》Canonical公司近日发布了Ubuntu25.04Beta版,这一版本被赋予了一个活泼的代号——“Plu... Canonical 昨日(3 月 27 日)放出了 Beta 版 Ubuntu 25.04 系统镜像,代号“Pluc

SpringBoot项目启动报错"找不到或无法加载主类"的解决方法

《SpringBoot项目启动报错找不到或无法加载主类的解决方法》在使用IntelliJIDEA开发基于SpringBoot框架的Java程序时,可能会出现找不到或无法加载主类com.example.... 目录一、问题描述二、排查过程三、解决方案一、问题描述在使用 IntelliJ IDEA 开发基于

Android WebView无法加载H5页面的常见问题和解决方法

《AndroidWebView无法加载H5页面的常见问题和解决方法》AndroidWebView是一种视图组件,使得Android应用能够显示网页内容,它基于Chromium,具备现代浏览器的许多功... 目录1. WebView 简介2. 常见问题3. 网络权限设置4. 启用 JavaScript5. D

SpringBoot项目启动错误:找不到或无法加载主类的几种解决方法

《SpringBoot项目启动错误:找不到或无法加载主类的几种解决方法》本文主要介绍了SpringBoot项目启动错误:找不到或无法加载主类的几种解决方法,具有一定的参考价值,感兴趣的可以了解一下... 目录方法1:更改IDE配置方法2:在Eclipse中清理项目方法3:使用Maven命令行在开发Sprin

spring-boot-starter-thymeleaf加载外部html文件方式

《spring-boot-starter-thymeleaf加载外部html文件方式》本文介绍了在SpringMVC中使用Thymeleaf模板引擎加载外部HTML文件的方法,以及在SpringBoo... 目录1.Thymeleaf介绍2.springboot使用thymeleaf2.1.引入spring

关于Spring @Bean 相同加载顺序不同结果不同的问题记录

《关于Spring@Bean相同加载顺序不同结果不同的问题记录》本文主要探讨了在Spring5.1.3.RELEASE版本下,当有两个全注解类定义相同类型的Bean时,由于加载顺序不同,最终生成的... 目录问题说明测试输出1测试输出2@Bean注解的BeanDefiChina编程nition加入时机总结问题说明

高效管理你的Linux系统: Debian操作系统常用命令指南

《高效管理你的Linux系统:Debian操作系统常用命令指南》在Debian操作系统中,了解和掌握常用命令对于提高工作效率和系统管理至关重要,本文将详细介绍Debian的常用命令,帮助读者更好地使... Debian是一个流行的linux发行版,它以其稳定性、强大的软件包管理和丰富的社区资源而闻名。在使用

龙蜥操作系统Anolis OS-23.x安装配置图解教程(保姆级)

《龙蜥操作系统AnolisOS-23.x安装配置图解教程(保姆级)》:本文主要介绍了安装和配置AnolisOS23.2系统,包括分区、软件选择、设置root密码、网络配置、主机名设置和禁用SELinux的步骤,详细内容请阅读本文,希望能对你有所帮助... ‌AnolisOS‌是由阿里云推出的开源操作系统,旨

五大特性引领创新! 深度操作系统 deepin 25 Preview预览版发布

《五大特性引领创新!深度操作系统deepin25Preview预览版发布》今日,深度操作系统正式推出deepin25Preview版本,该版本集成了五大核心特性:磐石系统、全新DDE、Tr... 深度操作系统今日发布了 deepin 25 Preview,新版本囊括五大特性:磐石系统、全新 DDE、Tree