赞
踩
BIOS中断0x15的子功能0xE820能够获取系统的内存布局,每次BIOS只返回一种类型的内存信息,直到将所有内存类型返回完毕。内存信息的内容是用地址范围描述符来描述的,用于存储这种描述符的结构称之为地址范围描述符(ARDS)



1.填写好“调用前输入”中列出的寄存器
2.执行中断调用int 0x15
3. 在CF位为0表示未出错,“返回后输出”中对应的寄存器便会有对应结果


;loader.s %include "boot.inc" section loader vstart=LOADER_BASE_ADDR LOADER_STACK_TOP equ 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的大小 GDT_LIMIT equ GDT_SIZE - 1 ;GDT的界限 times 60 dq 0 ; 此处预留60个描述符的空位 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用于保存内存容量,以字节为单位,此地址是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+ard_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 ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转 ; 这将导致之前做的预测失效,从而起到了刷新的作用。 jmp dword SELECTOR_CODE:p_mode_start .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 mov byte [gs:160], 'P' jmp $


在命令窗口Ctrl+C中断bochs运行,再输入xp 0xb00回车。查询到total_mem_bytes处为0x2000000,转换后正好是32MB





(1)用虚拟地址的高 10 位乘以 4,作为页目录表内的偏移地址,加上页目录表的物理地址,所得的和,便是页目录项的物理地址。读取该页目录项,从中获取到页表的物理地址。
(2)用虚拟地址的中间 10 位乘以 4,作为页表内的偏移地址,加上在第 1 步中得到的页表物理地址,所得的和,便是页表项的物理地址。读取该页表项,从中获取到分配的物理页地址。
(3)虚拟地址的高 10 位和中间 10 位分别是 PDE 和 PTE 的索引值,所以它们需要乘以 4。但低 12 位
就不是索引值啦,其表示的范围是 0~0xfff,作为页内偏移最合适,所以虚拟地址的低 12 位加上第 2 步
中得到的物理页地址,所得的和便是最终转换的物理地址。

目录项和页表项中的都是物理页地址,标准页大小是 4KB,故地址都是 4K 的倍数,也就是地址的低 12位是 0,所以只需要记录物理地址高 20 位就可以啦。这样省出来的 12 位(第 0~11 位)可以用来添加其他属性,下面对这些属性从低到高逐位介绍。
启用分页机制,我们要按顺序做好三件事。
(1)准备好页目录表及页表。
(2)将页表地址写入控制寄存器 cr3。 (3)寄存器 cr0 的 PG 位置 1
页目录表的位置,我们就放在物理地址0x100000处,为了让页表和页目录表紧凑一些(这不是必须
的),咱们让页表紧挨着页目录表。页目录本身占 4KB,所以第一个页表的物理地址是 0x101000。
0~1G: 0x00000000~0x3FFFFFFFF
1~2G: 0x40000000~0x7FFFFFFFF
2~3G: 0x80000000~0xBFFFFFFFF
3~4G: 0xC0000000~0xFFFFFFFFF
;loader.s %include "boot.inc" section loader vstart=LOADER_BASE_ADDR LOADER_STACK_TOP equ 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的大小 GDT_LIMIT equ GDT_SIZE - 1 ;GDT的界限 times 60 dq 0 ; 此处预留60个描述符的空位 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用于保存内存容量,以字节为单位,此地址是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+ard_nr2,共256字节 ards_buf times 244 db 0 ards_nr dw 0 ;用于记录ARDS结构体数量 ;0xc00 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 ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转 ; 这将导致之前做的预测失效,从而起到了刷新的作用。 jmp dword SELECTOR_CODE:p_mode_start .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 ; 创建页目录及页表并初始化页内存位图 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 jmp $ ;------------- 创建页目录及页表 --------------- setup_page: ;先把页目录占用的空间逐字节清 0 mov ecx,4096 mov esi,0 .clear_page_dir: mov byte [0x100000 + esi], 0 inc esi loop .clear_page_dir ;开始创建页目录项(PDE) .create_pde: mov eax,0x101000 ; 此时 eax 为第一个页表的位置及属性 mov ebx,eax or eax, PG_US_U | PG_RW_W | PG_P mov [0x100000 + 0], eax ;在页目录表中的第 1 个目录项写入第一个页表的位置(0x101000)及属性(7) mov [0x100000 + 0xc00], eax ; 0xc00 表示第 768 个页表占用的目录项,0xc00 以上的目录项用于内核空间 sub eax,0x1000 mov [0x100000 + 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: mov [0x101000+esi*4],edx add edx,4096 inc esi loop .create_pte ;创建内核其他页表的PDE mov eax,0x102000 ; 此时 eax 为第二个页表的位置 or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性 US、 RW 和 P 位都为 1 mov ecx,254 mov esi,769 .create_kernel_pde: mov [0x100000+esi*4],eax inc esi add eax,0x1000 loop .create_kernel_pde ret

总结:
gcc 源文件 -m32 -c -o 输出文件 ld -m elf_i386 源文件 -Ttext 起始虚拟地址 -e main -o 输出文件






下面是程序头表中的条目的数据结构elf32_phdr,这是用来描述各个段的信息用的:



链接后,程序运行的代码、数据等资源都是在段中
int main(void)
{
while(1);
return 0;
}
#编译
gcc -m32 -c -o main.o main.c
#链接
ld -m elf_i386 main.o -Ttext 0xc0001500 -e main -o kernel.bin
#将内核文件kernel.bin,把内核文件写入硬盘第9个扇区
dd if=kernel.bin of=/ your_path/hd60M.img bs=512 count=200 seek=9 conv=notrunc
loader.s需要修改两个地方
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。