赞
踩
本节主要是设置二级分页,配置线性内存和物理内存的映射关系,然后按照elf文件规则,将C语言写的内核加载到指定虚拟内存中。目前的内核是一个main函数,里面是一个while(1)循环。例外,获取内存大小用汇编的0x15中断会比较方便,所以在进入内核前先获取到内存大小,保持到内存中,以后会使用到。以下是设置的分页映射关系,左边是线性内存地址,右边是物理内存地址:
1、0x15子功能号
EAX=0xE820 : 遍历全部内存,dmesg 命令就与 0xE820 相关。
AX=0xE801 : 别检测低 15M和16M~4GB 的内存。
AH=0x88 : 最多检查64M。
2、0xe820介绍
(1)地址范围描述符(ARDS):存储内存信息内容。每次 int OxlS 之后, BIOS就返回这样一个结构的数据,20字节。
(2)Type字段说明内存的用途
(3)CF位:若 CF 位为0表示调用未出错, CF为1,表示调用出错。
3、0xe801介绍
(1) AX和CX 存储15M及以下空间,AX和CX值一样,单位1KB。所以15M及以下容量=AX1024
BX和DX 存储16M到4G空间,BX和DX值一样,单位64KB,所以15M以上容量=BX641024
4、0x88
内存大小:AX1024 字节+1MB
1、分页机制作用:
(1)将线性地址转换成物理地址
(2)用大小相等的页代替大小不等的段。操作系统先分段,再分页。
2、分页原理—— 一级页表
(1)在32位地址总线,默认高20位表内存块数量,即1M。低12位作为内存块尺寸,即4KB.
(2)线性地址的一页对应物理地址的一页。4GB物理内存也被分成4GB/4KB=1M(1048576)个页。
(3)32位线性地址中,高20位索引物理内存第几个页,低12位索引页块内具体物理地址(即偏移量)。虚拟地址的高20 位可用来定位一个物理页,低 12 位可用来在该物理页内寻址。
(4)页表物理地址加载到CR3寄存器。页表和页表项寻址时,都是物理地址。
(5)总结一级页表寻址:高20位*4B+CR3内页表物理地址+低12位地址。
表示高20位为页表项索引,每个页表项大小4B,CR3存储的地址相当基地址,这样就得到页表项的地址,该地址保存着对应物理页地址。然后+低12位,得到具体到1字节的物理地址。
举例:mov eax, [0x1234],平坦模式下段基址为0。寻址流程如下图:
1、目的:一级页表是一次性建好,二级页表可以随进程创建动态生成页表项。
2、二级页表原理
(1)32位地址中,二级页表分为一级页号(高10位),二级页号(中10位),页内偏移(低12位)。
(2)一级页号也称页目录表,页目录表占4KB,里面页目录项占4B,共1024个页目录项。页目录项保存页表物理地址。
(3)二级页号也称页表,每个页表占4KB,里面页表项占4B,共1024个页表项。页表项保存页块物理地址。
(4)页块4KB,页块物理地址+页内偏移量=保存物理地址的页块内地址(映射的物理地址)。
3、地址转换
(1) 从CR3中读取页目录起始地址+高10位4,计算出页目录表内页目录项,保存要找的页表物理地址。
(2) 页表物理地址+中10位4,计算出页表项地址,保存着物理页地址。
(3) 物理页地址+低12位,计算出物理地址。
mov eax,[0x1234567]为例,最后得到物理地址0xfa567,需要注意的是中位10位计算方式10 0011 0100很容易看成0x8d0(1000 1101 00)。
4、cr3寄存器
5、页目录项和页表项结构
一般只设置US,RW,P位,其他位为0。
(1)P:该页是否存在内存中,存在1,不存在0
(2)RW:意为读写位。若为 1 表示可读可写,若为 0 表示可读不可写 。
(3)US:普通用户/超级用户位。若为 1 时,表示处于 User 级,任意级别( 0 、 l 、 2、3 )特权的程序都可以访问该页。若为 0,表示处于 Supervisor 级,特权级别为 3的程序不允许访问该页,该页只允许特权级别为0、1、2的程序访问。
6、TLB中存储虚拟地地址高20位到物理地址高20位的映射关系。
7、TLB的维护由操作系统完成
(1)重新加载cr3寄存器,会重新加载整个TLB。
(2)使用invlpg指令对指定的缓存条目进行刷新。
1、启用分页机制,按顺序做好三件事 :
(1)准备好页目录表及页表。
(2)将页表地址写入控制寄存器cr3。还是物理地址状态。
(3)寄存器cr0的PG位置1。开启后,使用虚拟地址。
2、规划页表:
(1)LINUX系统中,0~3GB 是用户进程, 3GB~4GB 是操作系统。
(2)实现操作系统被用户进程共享,则是所有用户进程的3GB~4GB都指向操作系统物理内存空间
3、二级页表物理内存分布
4、如果页目录项的物理地址放在0x100000,那么页表的内存地址分布如下:
1、code tree
2、bochs
############################################################### # Configuration file for Bochs ############################################################### # how much memory the emulated machine will have megs: 32 magic_break: enabled=1 display_library: x, options="gui_debug" # filename of ROM images romimage: file=/usr/local/share/bochs/BIOS-bochs-latest vgaromimage: file=/usr/local/share/bochs/VGABIOS-lgpl-latest # what disk images will be used #floppya: 1_44=./hd30M.img, status=inserted ata0-master: type=disk, path="./hd30M.img", mode=flat # choose the boot disk. boot: disk # where do we send log messages? # log: bochsout.txt # disable the mouse mouse: enabled=0 # enable key mapping, using US layout as default. #keyboard_mapping: enabled=1, map=/usr/share/bochs/keymaps/x11-pc-us.map
3、makefile
.PHONY:build image clean mbr_src=mbr.asm boot_src=boot.asm mbr=./out/boot/mbr.bin boot=./out/boot/boot.bin kernel=./out/kernel/kernel.bin img=./hd30M.img all:clean build image bochs build: if [ ! -d "./out/boot" ]; then mkdir ./out/boot;fi;if [ ! -d "./out/kernel" ]; then mkdir ./out/kernel;fi nasm -I ./boot/include/ -o ./out/boot/mbr.bin ./boot/mbr.asm nasm -I ./boot/include/ -o ./out/boot/boot.bin ./boot/boot.asm gcc -m32 -c -o out/kernel/main.o kernel/main.c ld -m elf_i386 out/kernel/main.o -Ttext 0xc0001500 -e main -o out/kernel/kernel.bin ## boot.img,30MB大小,前512字节是MBR image: @-rm -rf $(img) bximage -hd=30 -func=create -imgmode=flat -sectsize=512 -q $(img) dd if=$(mbr) of=$(img) bs=512 count=1 conv=notrunc dd if=$(boot) of=$(img) bs=512 count=4 seek=2 conv=notrunc dd if=$(kernel) of=$(img) bs=512 count=200 seek=9 conv=notrunc bochs: bochs -qf bochsrc clean: rm -rf *.img ./out/kernel/* ./out/boot/*
4、boot.inc
LOADER_BASE_ADDR equ 0x500 ;boot.asm的起始内存地址
PT_NULL equ 0 ;一个变量
5、mbr.asm
;引入外部文件,这个文件定义常量 %include "boot.inc" ;代码以此地址作为偏移地址的基地址 [ORG 0x7c00] ;指定代码段和16bit模式 [SECTION .text] [BITS 16] _start: mov ax,cs ;寄存器清0 mov ds,ax mov es,ax mov ss,ax mov fs,ax mov sp,0x7c00 ;设置栈寄存器sp,栈地址从上往下存储,恰好0x500~0x7bff是可用区域 mov ax,0xb800 ;设置gs段寄存器地址,gs段寄存器不能直接赋值,只能通过先传给通用寄存器,再传gs寄存器。 mov gs,ax ;call print ;跳转打印 mov eax,2 ;读取起始扇区地址 mov bx,LOADER_BASE_ADDR ;写入的地址 mov cx,4 ;待读入扇区数 call rd_disk ;将boot加载到内存 call print ;打印"MBR" jmp LOADER_BASE_ADDR print: ;int 0x10 相当函数,参数从ax,bx,cx,dx这些寄存器取。清掉BIOS的输出。 mov ax, 0x0600 ;ah=0x06,表示功能号,功能为上卷清屏.al=0x0,表示上卷行数,为0则表示全部 mov bx, 0x0700 ;上卷属性,黑底 mov cx, 0 ;左上角坐标(0, 0) mov dx, 0x184f ;右下角坐标(24, 79), 0x18=24,0x4f=79 int 0x10 ;0x10中断 ;写入1MB内存中,文本显示区域0xb800-0xbffff,显卡会自动在该内存映射到显示器。显示字符需要两个字节,一个保存字符,一个保存字符属性 mov byte [gs:0x00],'M' mov byte [gs:0x01],0x0F ;表示黑色背景,4表示前景色为白色 mov byte [gs:0x02],'B' mov byte [gs:0x03],0x0F mov byte [gs:0x04],'R' mov byte [gs:0x05],0x0F ret rd_disk: mov esi,eax ;备份esi mov di,cx ;备份di ;设置读取扇区数 mov dx,0x1f2 mov al,cl out dx,al mov eax,esi ;0x1f3 8bit iba地址低八位 0-7 inc dx out dx,al ;0x1f4 8bit iba地址中八位 8-15 inc dx mov cl,8 ;shr移多个位可以用cl存储移位数 shr eax,cl out dx,al ;0x1f5 8bit iba地址高八位 16-23 inc dx shr eax,cl out dx,al ; 0x1f6 8bit ; 0-3 位iba地址的24-27 ; 4 0表示主盘 1表示从盘 ; 5、7位固定为1 ; 6 0表示CHS模式,1表示LAB模式 inc dx mov al, ch or al, 0xe0 out dx, al ; 0x1f7 8bit 命令或状态端口 inc dx mov al, 0x20 out dx, al ;循环检查硬盘状态 .read_check: in al,dx and al,0x88 ; 取硬盘状态的第3、7位 cmp al,0x08 ; 硬盘数据准备好了且不忙了 jnz .read_check ; 读数据 mov ax, di mov cx,256 mul cx ;一个扇区512字节,每次读2字节,故一个扇区需要读取256次。既是ax*cx。mul的隐式表达式为:dx*ax=ax*cx,溢出会存到dx,故这里dx会赋值0x00,所以mov dx,0x1f0需要写在mul后面,否则会被修改。 mov cx,ax mov dx,0x1f0 ;循环读取硬盘数据到内存 .read_data: in ax, dx mov [bx], ax add bx, 2 loop .read_data ret times 510-($-$$) db 0 ;一个扇区512字节,减去0xaa55为510字节。($$-$)为上面代码所占用字节数,510-($-$$)为当前行到魔术符字节数。time 字节数 dd 0 : 填充当前字节开始,到字节数为0 dw 0xaa55 ;BIOS程序识别MBR魔术符
5、boot.asm
;引入外部文件,这个文件定义常量 %include "boot.inc" [ORG 0x500] [SECTION .data] PAGE_DIR_TABLE_POS equ 0x100000 ;二级页目录表,页表放在内存中1M起始位置连续存放,尽可能简单 KERNEL_BIN_BASE_ADDR equ 0x70000 ;disk to memory address KERNEL_START_SECTOR equ 0x9 ;load kernel in the disk start sector KERNEL_ENTRY_POINT equ 0xc0001500 ;inter virtual kernel address ;---------------- 页表相关属性 -------------- PG_P equ 1b ; PG_RW_R equ 00b PG_RW_W equ 10b PG_US_S equ 000b PG_US_U equ 100b [SECTION .gdt] SEG_BASE equ 0 SEG_LIMIT equ 0xfffff CODE_SELECTOR equ (1 << 3) DATA_SELECTOR equ (2 << 3) ;第一个段描述符,不可访问,设为0 gdt_base: dd 0, 0 ;0x00_1_1_0_0_0xf_1_00_1_1000_0x00_0x0000_0xffff gdt_code: dw SEG_LIMIT & 0xffff ;段界限低16位 dw SEG_BASE & 0xffff ;段基址低16位 db SEG_BASE >> 16 & 0xff ;段基址中8位 db 0b1_00_1_1000 ;P_DPL_S_TYPE db 0b1_1_0_0_0000 | (SEG_LIMIT >> 16 & 0xf) ;G_DB_L_AVL_LIMIT:LIMIT段极限高4位 db SEG_BASE >> 24 & 0xf ;段基址高8位 ;0x00_1_1_0_0_0xf_1_00_1_0010_0x00_0x0000_0xffff gdt_data: dw SEG_LIMIT & 0xffff ;段界限低16位 dw SEG_BASE & 0xffff ;段基址低16位 db SEG_BASE >> 16 & 0xff ;段基址中8位 db 0b1_00_1_0010 ;P_DPL_S_TYPE db 0b1_1_00_0000 | (SEG_LIMIT >> 16 & 0xf) ;G_DB_AVL_LIMIT:LIMIT段极限高4位 db SEG_BASE >> 24 & 0xf ;段基址高8位 gdt_display: dd 0x80000007 ;文本显示内存地址为0xb8000~0xbffff,所以文本显示段基址为0xb8000,段界限为(0xbffff-0xb8000)/4k=0x7 db 0x0b db 0b1_00_1_0010 ;P_DPL_S_TYPE db 0b1_1_00_0000 | (SEG_LIMIT >> 24 & 0xf) ;G_DB_AVL_LIMIT db SEG_BASE >> 24 & 0xf ;段基址高8位 gdt_limit equ $ - gdt_base - 1 times 30 dq 0 ;此处预留30个描述符的位置 SELECTOR_CODE equ (0x0001<<3) + 000b + 00b ;相当于(gdt_code - gdt_base)/8 + TI_GDT + RPL0 SELECTOR_DATA equ (0x0002<<3) + 000b + 00b ;同上 SELECTOR_DISPLAY equ (0x0003<<3) + 000b + 00b ;同上 total_mem_bytes dd 0 ;save memory size,this physical address is 0x700.(4+60)*8+0x500=0x700 ;计算出全局描述符界限和内存起始地址 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结构体数量 [SECTION .text] [BITS 16] global boot_start boot_start: call get_memory_size ;开启A20总线 in al,0x92 or al,0000_0010B out 0x92,al lgdt [gdt_ptr] ;cr0寄存器PE位置1,开启保护模式 mov eax, cr0 or eax, 0x00000001 mov cr0, eax ;跳转到保护模式地址,刷新掉16位实模式下流水线和缓存。 jmp SELECTOR_CODE:p_mode_start get_memory_size: ;------- 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处。 ret .error_hlt: ;出错则挂起 hlt [BITS 32] p_mode_start: mov ax, SELECTOR_DATA ;初始化寄存器 mov ds, ax mov es, ax mov ss, ax mov esp,LOADER_BASE_ADDR mov ax, SELECTOR_DISPLAY 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], ';' ;视频段段基址已经被更新 xchg bx,bx call kernel_init mov esp, 0xc009f000 jmp KERNEL_ENTRY_POINT ; 用地址0x1500访问测试,结果ok ;------------- 创建页目录及页表 --------------- setup_page: ;先把页目录占用的空间逐字节清0 mov ecx, 4096 mov esi, 0 .clear_page_dir: mov byte [PAGE_DIR_TABLE_POS + esi], 0 inc esi loop .clear_page_dir ;开始创建页目录项(PDE) .create_pde: ; 创建Page Directory Entry mov eax, PAGE_DIR_TABLE_POS add eax, 0x1000 ; 此时eax为第一个页表的位置及属性 mov ebx, eax ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。 ; 下面将页目录项0和0xc00都存为第一个页表的地址, ; 一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表, ; 这是为将地址映射为内核地址做准备 or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问. mov [PAGE_DIR_TABLE_POS + 0x0], eax ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(7) mov [PAGE_DIR_TABLE_POS + 0xc00], eax ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间, ; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程. sub eax, 0x1000 mov [PAGE_DIR_TABLE_POS + 4092], eax ; 使最后一个目录项指向页目录表自己的地址 ;下面创建页表项(PTE) mov ecx, 256 ; 1M低端内存 / 每页大小4k = 256 mov esi, 0 mov edx, PG_US_U | PG_RW_W | PG_P ; 属性为7,US=1,RW=1,P=1 .create_pte: ; 创建Page Table Entry mov [ebx+esi*4],edx ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址 add edx,4096 ; 4096为4K,这里表示下一个页表项地址。 inc esi loop .create_pte ;创建内核其它页表的PDE mov eax, PAGE_DIR_TABLE_POS add eax, 0x2000 ; 此时eax为第二个页表的位置 or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性US,RW和P位都为1 mov ebx, PAGE_DIR_TABLE_POS mov ecx, 254 ; 范围为第769~1022的所有目录项数量 mov esi, 769 .create_kernel_pde: mov [ebx+esi*4], eax inc esi add eax, 0x1000 loop .create_kernel_pde ret ;------------------------------------------------------------------------------- ;功能:读取硬盘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 ret ;----------------- 将kernel.bin中的segment拷贝到编译的地址 ----------- kernel_init: xor eax, eax xor ebx, ebx ;ebx记录程序头表地址 xor ecx, ecx ;cx记录程序头表中的program header数量 xor edx, edx ;dx 记录program header尺寸,即e_phentsize mov dx, [KERNEL_BIN_BASE_ADDR + 42] ; 偏移文件42字节处的属性是e_phentsize,表示program header大小 mov ebx, [KERNEL_BIN_BASE_ADDR + 28] ; 偏移文件开始部分28字节的地方是e_phoff,表示第1 个program header在文件中的偏移量 ; 其实该值是0x34,不过还是谨慎一点,这里来读取实际值 add ebx, KERNEL_BIN_BASE_ADDR mov cx, [KERNEL_BIN_BASE_ADDR + 44] ; 偏移文件开始部分44字节的地方是e_phnum,表示有几个program header .each_segment: cmp byte [ebx + 0], PT_NULL ; 若p_type等于 PT_NULL,说明此program header未使用。 je .PTNULL ;为函数memcpy压入参数,参数是从右往左依然压入.函数原型类似于 memcpy(dst,src,size) push dword [ebx + 16] ; program header中偏移16字节的地方是p_filesz,压入函数memcpy的第三个参数:size mov eax, [ebx + 4] ; 距程序头偏移量为4字节的位置是p_offset add eax, KERNEL_BIN_BASE_ADDR ; 加上kernel.bin被加载到的物理地址,eax为该段的物理地址 push eax ; 压入函数memcpy的第二个参数:源地址 push dword [ebx + 8] ; 压入函数memcpy的第一个参数:目的地址,偏移程序头8字节的位置是p_vaddr,这就是目的地址 call mem_cpy ; 调用mem_cpy完成段复制 add esp,12 ; 清理栈中压入的三个参数 .PTNULL: add ebx, edx ; edx为program header大小,即e_phentsize,在此ebx指向下一个program header xchg bx,bx 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
7、main.c
int main(void) {
while(1);
return 0;
}
8、运行结果
1、现象
jmp 0xc0001500会自动重启,jmp 0x00001500可以顺利进入main函数执行死循环。
2、解决思路
(1)加了代码段选择子 jmp SELECTOR_CODE:KERNEL_ENTRY_POINT,报以下警告
(2)查看设置的段描述符,果然代码段描述符段界限设置错误,正确的该是4G,像第02个选择子数据段那样。
(3)查看代码段描述符,果然G位设置错误了,所以之前博客的代码也是错误的,已经改正。代码段G位被我错误设为0
这是我的笔记,若有错误,望请多指教。
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。