本文主要是介绍《操作系统真象还原》第五章——加载内核,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
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
这篇关于《操作系统真象还原》第五章——加载内核的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!