v0.0.5的项目地址:
github.com地址:
https://github.com/zenglong/zenglOX (只包含git提交上来的源代码)
Dropbox地址:
点此进入Dropbox网盘 (该版本位于zenglOX_v0.0.5的文件夹,该文件夹里的zip压缩包为源代码)
sourceforge地址:
https://sourceforge.net/projects/zenglox/files (该版本位于zenglOX_v0.0.5的文件夹,该文件夹里的zip压缩包为源代码)
另外再附加一个英特尔英文手册的共享链接地址:
https://www.dropbox.com/sh/brrv4dnher09s2d/AABa6QRpN-uc6tDpvEd7KtmMa (里面的intel_manual.pdf有分页等的相关介绍)
有关zenglOX的编译及gdb调试的方法,请参考zenglOX v0.0.1里的文章。
在之前
zenglOX v0.0.3 初始化GDT和IDT的文章里,介绍过IA-32平台的内存管理机制分为两部分:段和分页,如下图所示:
图1
上图里的Segmentation段管理部分已经在前面GDT章节里讲解过,之前的版本没有开启Paging分页机制,所以之前的zenglOX里,线性地址就等于实际的物理地址,由于编程时,程序里的变量,函数等使用的都是线性地址,在编译后,程序里的这些地址值很多都是固定的,尤其是全局变量的地址,但是在后面的多任务环境下,当有很多程序一起运行时,每个程序都有自己单独的物理内存空间,每个程序每次启动时的物理内存地址也会不完全一样,这样就会导致程序里的那些地址值变得不确定,所以如果不开启分页映射机制,则多任务环境下的程序开发就会变得很困难。
Paging(分页)最主要的作用就是在程序的线性地址(或者叫做虚拟内存地址)与实际的物理内存地址之间建立一个映射关系,这样不同程序里相同的线性地址就会被系统自动映射为不同的物理地址,这种映射是系统内部完成的,对用户是透明的,从而可以解决多任务下程序开发的难题,例如,在Linux系统里,每个用户程序运行时的起始虚拟内存地址都是0x8048000,结束地址是0xbfffffff,这个起始虚拟内存地址0x8048000会被系统映射为程序实际的物理内存地址,这个物理内存地址可以是一个随机的不确定的值,不过这些不确定的物理地址无需用户关心。
由于存在这么一层映射机制,所以一个4G的线性地址空间可以和128M甚至更低的物理内存建立映射关系。
另外,分页机制还可以在映射的过程中,判断用户是否具有对目标物理内存的读写权限,例如,用户态的程序,不能修改系统内核里的内存数据,每个用户程序都只能查看和修改自己内存空间里的数据等,从而在硬件层面对内存进行有效的保护。
分页还有一个重要的作用是:当实际的物理内存空间不足时,还可以将一些暂时不用的物理内存空间里的数据交换到磁盘,当需要使用这些数据时,再从磁盘里将数据交换回物理内存,Linux系统里的Swap磁盘交换分区就可以起到交换内存数据的作用。
以上是分页机制的作用,在英特尔英文手册的第1938页(即第三卷的第4章),就详细的介绍了IA-32平台的分页机制,下面就对zenglOX里用到的分页方式进行介绍。
在介绍分页机制之前,需要先说明的是:只有在保护模式下,才可以开启分页,但是在zenglOX里并没有包含开启保护模式的代码,要开启保护模式,需要将CR0控制寄存器的位0(即PE位)设置为1(有关CR0,CR1,CR2,CR3,CR4寄存器各二进制位的含义可以参考
http://en.wikipedia.org/wiki/Control_register 该链接里的文章),但是在zenglOX源代码里并没有设置PE位的相关代码,其实,在GRUB加载我们的内核之前,已经设置了一个自己的GDT(全局描述符表),并帮我们设置为保护模式了,所以当我们的内核被加载执行时,已经是保护模式了,我们只需重新加载一次GDTR寄存器,将GDT表设置为我们自己的描述符表,再开启分页即可。
从英特尔手册里可以看到,分页其实有三种模式:
-
32位分页模式
-
PAE分页模式
-
IA-32e分页模式(该分页模式仅用于支持64位的处理器)
由于zenglOX只用了第一个32位分页模式,所以下面只对该分页模式进行介绍,另外两种分页模式请参考英特尔英文手册或者查询Google。
要开启32位分页模式,需要将CR0控制寄存器的位31(即PG位)设置为1,同时确保CR4控制寄存器的位5(即PAE位)处于清零状态,则处理器就会进入32位分页模式,如果CR4的PAE位被设置为1,则处理器就会进入PAE分页模式或IA-32e分页模式(详情参考手册),所以为了进入32位分页模式,在zlox_paging.c文件的zlox_switch_page_directory函数里就有如下代码:
ZLOX_VOID zlox_switch_page_directory(ZLOX_PAGE_DIRECTORY *dir)
{
ZLOX_UINT32 cr0_val;
current_directory = dir;
asm volatile("mov %0, %%cr3":: "r"(dir->tablesPhysical));
asm volatile("mov %%cr0, %0": "=r"(cr0_val));
cr0_val |= 0x80000000; // Enable paging!
asm volatile("mov %0, %%cr0":: "r"(cr0_val));
}
|
上面zlox_switch_page_directory函数的最后三条代码,先通过asm volatile("mov %%cr0, %0": "=r"(cr0_val));嵌入式汇编代码将CR0控制寄存器的当前值写入cr0_val局部变量,再通过cr0_val |= 0x80000000;或运算将cr0_val的最高位设置为1,最后由asm volatile("mov %0, %%cr0":: "r"(cr0_val));将修改后的cr0_val的值写入CR0寄存器,这样CR0的PG位就会被设置为1,同时在默认情况下,CR4的PAE位是处于清零状态,所以处理器就会进入32位分页模式。
32位分页模式里又有两种分页方式,一种是以4M为一页大小的分页方式,如下图所示(该图位于英特尔英文手册的第1946页):
图2
另一种是以4K为一页大小的分页方式,如下图所示(该图位于手册的第1945页):
图3
zenglOX系统使用的是图3所示的每页4K大小的分页方式,该分页方式里,有两个数组:Page Directory页目录数组,和Page Table页表数组。一个32位的Linear Address(线性地址)要转成对应的物理地址需要经过三个步骤:
-
先由线性地址的位22到位31一共10位的值作为Page Directory页目录数组的索引,从该索引对应的数组成员里得到对应的Page Table页表数组的物理内存基地址
-
再由线性地址的位12到位21一共10位的值作为上一步得到的Page Table页表数组的索引,从该索引对应的数组成员里得到对应的4K页的物理内存基地址
-
最后由线性地址的位0到位11一共12位的值作为Offset偏移值,加上前一步得到的4K页基地址,就可以得到对应的物理内存地址了。
Page Directory页目录数组的物理内存基地址是存储在CR3寄存器里的,所以在之前的zlox_switch_page_directory函数里,会先用asm volatile("mov %0, %%cr3":: "r"(dir->tablesPhysical));将dir->tablesPhysical即页目录的物理内存基地址赋值给CR3寄存器。
Page Directory页目录数组和Page Table页表数组的结构体类型都定义在zlox_paging.h头文件里:
/* zlox_paging.h Defines the interface for and structures relating to paging.*/
#ifndef _ZLOX_PAGING_H_
#define _ZLOX_PAGING_H_
#include "zlox_common.h"
#include "zlox_isr.h"
struct _ZLOX_PAGE
{
ZLOX_UINT32 present : 1; // Page present in memory
ZLOX_UINT32 rw : 1; // Read-only if clear, readwrite if set
ZLOX_UINT32 user : 1; // Supervisor level only if clear
ZLOX_UINT32 accessed : 1; // Has the page been accessed since last refresh?
ZLOX_UINT32 dirty : 1; // Has the page been written to since last refresh?
ZLOX_UINT32 unused : 7; // Amalgamation of unused and reserved bits
ZLOX_UINT32 frame : 20; // Frame address (shifted right 12 bits)
}__attribute__((packed));
typedef struct _ZLOX_PAGE ZLOX_PAGE;
struct _ZLOX_PAGE_TABLE
{
ZLOX_PAGE pages[1024];
}__attribute__((packed));
typedef struct _ZLOX_PAGE_TABLE ZLOX_PAGE_TABLE;
typedef struct _ZLOX_PAGE_DIRECTORY
{
/**
Array of pointers to pagetables.
**/
ZLOX_PAGE_TABLE *tables[1024];
/**
Array of pointers to the pagetables above, but gives their *physical*
location, for loading into the CR3 register.
**/
ZLOX_UINT32 tablesPhysical[1024];
/**
The physical address of tablesPhysical. This comes into play
when we get our kernel heap allocated and the directory
may be in a different location in virtual memory.
**/
ZLOX_UINT32 physicalAddr;
}ZLOX_PAGE_DIRECTORY;
ZLOX_VOID zlox_init_paging();
#endif //_ZLOX_PAGING_H_
|
上面代码里的ZLOX_PAGE_DIRECTORY结构体就是页目录数组的定义,在该结构体里定义了两个含有1024个元素的数组,其中tablesPhysical是CR3寄存器需要指向的Page Directory页目录数组的物理内存基地址,既然有了tablesPhysical数组,为何还要一个tables[1024]数组?tables数组里存储的是编程用的线性地址,可以在程序里用它对ZLOX_PAGE_TABLE页表结构进行一些操作,而tablesPhysical数组里存储的是实际的物理内存地址,是给处理器用的。
ZLOX_PAGE_TABLE就是页表结构体,里面存储了含有1024个元素的pages数组,该数组里的每一项都包含了4K页的物理内存基地址信息,可以看出来,页目录和页表数组都是1024个元素的大小,这是因为前面说过,线性地址是用10位的二进制值来索引页目录和页表的,10位二进制的范围为0到1023,所以页目录和页表数组都是1024的大小。
页目录数组里的每一项即上面tablesPhysical数组里的每一项的二进制含义如下:
图4
图4所示的表格位于英特尔英文手册的第1949页,从上图可以看出来,页目录的每一项里只有位12到位31用于表示Page Table页表的物理地址的高20位(因为是4K对齐,所以低12位默认为0),其余二进制位用于表示该页表的一些属性,例如,位0(P位)用于表示该页表是否存在,位1(R/W位)用于表示该页表是否可读写,当该位为1时,表示该页表可以进行写操作,否则就只能进行读操作,位2(U/S位)为0时,表示该页表只有系统内核可以访问,用户态程序不能访问,等等,这些属性可以对页表结构进行有效的保护。
页表里的每一项即上面ZLOX_PAGE_TABLE结构体里定义的pages数组里的每一项的二进制含义如下:
图5
上图也位于手册的1949页,根据上面二进制位的含义,前面zlox_paging.h头文件里的ZLOX_PAGE结构体就有对应的位定义:
struct _ZLOX_PAGE
{
ZLOX_UINT32 present : 1; // Page present in memory
ZLOX_UINT32 rw : 1; // Read-only if clear, readwrite if set
ZLOX_UINT32 user : 1; // Supervisor level only if clear
ZLOX_UINT32 accessed : 1; // Has the page been accessed since last refresh?
ZLOX_UINT32 dirty : 1; // Has the page been written to since last refresh?
ZLOX_UINT32 unused : 7; // Amalgamation of unused and reserved bits
ZLOX_UINT32 frame : 20; // Frame address (shifted right 12 bits)
}__attribute__((packed));
|
该结构体使用特殊的冒号来定义成员,表示这些成员对应为32位数据里的某一位或某几位,例如,present成员即位0(P位)用于表示对应的4K页面是否存在,rw成员即位1(R/W位)用于表示该4K页面是否可读写,user成员即位2用于表示该4K页面是否允许用户态程序访问,accessed和dirty成员的位置不对,它们应该处于位5和位6,不过当前版本里这两个成员对内核的正常运行没什么影响,以后的版本再去修改,上面图4里,位3(PWT),位4(PCD),位7(PAT),位8(G)以及位9到位11都不要去随便修改,以免产生page-fault(页面错误)的异常。最后一个frame成员即位12到位31一共20位,用于表示所引用的4K页面的物理内存基地址的高20位,因为是4K对齐的,所以基地址的低12位默认是0 。
在了解页目录和页表的结构后,剩下的事情就是在程序里给这些结构分配内存和进行一些初始化操作,这些操作都是在zlox_paging.c文件里完成的:
..................................
..................................
ZLOX_UINT32 *frames;
..................................
..................................
ZLOX_VOID zlox_init_paging()
{
ZLOX_UINT32 i;
// The size of physical memory. For the moment we
// assume it is 16MB big.
ZLOX_UINT32 mem_end_page = 0x1000000;
nframes = mem_end_page / 0x1000;
frames = (ZLOX_UINT32 *)zlox_kmalloc(nframes/8);
zlox_memset((ZLOX_UINT8 *)frames, 0, nframes/8);
// Let's make a page directory.
kernel_directory = (ZLOX_PAGE_DIRECTORY *)zlox_kmalloc_a(sizeof(ZLOX_PAGE_DIRECTORY));
// we must clean the memory of kernel_directory!
zlox_memset((ZLOX_UINT8 *)kernel_directory, 0, sizeof(ZLOX_PAGE_DIRECTORY));
current_directory = kernel_directory;
// We need to identity map (phys addr = virt addr) from
// 0x0 to the end of used memory, so we can access this
// transparently, as if paging wasn't enabled.
// NOTE that we use a while loop here deliberately.
// inside the loop body we actually change placement_address
// by calling kmalloc(). A while loop causes this to be
// computed on-the-fly rather than once at the start.
i = 0;
while (i < placement_address)
{
// Kernel code is readable but not writeable from userspace.
zlox_alloc_frame( zlox_get_page(i, 1, kernel_directory), 0, 0);
i += 0x1000;
}
// Before we enable paging, we must register our page fault handler.
zlox_register_interrupt_callback(14,zlox_page_fault);
// Now, enable paging!
zlox_switch_page_directory(kernel_directory);
}
|
上面的zlox_init_paging函数里,先将mem_end_page设置为0x1000000,这样可访问的物理内存范围就是16M,尽管你的系统实际内存可能会大于这个值,不过暂时先只使用这么大的物理内存,一个页表有1024项,每项对应一个4K的页面,这样1024乘以4K就是4M,所以每个页表可以表示4M的物理内存,那么系统里就只需设置4个页表(4 x 4M = 16M)即可,其余的暂时留空。
为了能有效的分配和管理4K页面,这里增加了一个4K页面位图,如下图所示:
图6
frames是zlox_paging.c在开头定义的 ZLOX_UINT32 * 类型的指针,或者说frames指向的是由32位无符号整数构成的数组,每个32位整数里的每一个二进制位都对应一个frame(即4K页面),二进制位的索引刚好就是对应4K页面的物理基址的高20位值,当某个二进制位被设置为1时,表示该位对应的4K页面在页表里是存在的,如果二进制位为0,则对应的4K页面在页表里还没有被设置。在zlox_paging.c文件的zlox_alloc_frame函数里,就是先查询frames位图,当找到空闲的frame(对应的二进制位为0)时,再设置对应的页表项:
...........................................
...........................................
// Static function to find the first free frame.
static ZLOX_UINT32 zlox_first_frame()
{
ZLOX_UINT32 i, j;
for (i = 0; i < ZLOX_INDEX_FROM_BIT(nframes); i++)
{
if (frames[i] != 0xFFFFFFFF) // nothing free, exit early.
{
// at least one bit is free here.
for (j = 0; j < 32; j++)
{
ZLOX_UINT32 toTest = 0x1 << j;
if ( !(frames[i]&toTest) )
{
return i*4*8+j;
}
}
}
}
return 0xFFFFFFFF;
}
...........................................
...........................................
// Function to allocate a frame.
ZLOX_VOID zlox_alloc_frame(ZLOX_PAGE *page, ZLOX_SINT32 is_kernel, ZLOX_SINT32 is_writeable)
{
if (page->frame != 0)
{
return;
}
else
{
ZLOX_UINT32 tmp_page = zlox_first_frame();
if (tmp_page == ((ZLOX_UINT32)(-1)))
{
// PANIC! no free frames!!
}
zlox_set_frame(tmp_page*0x1000);
page->present = 1;
page->rw = (is_writeable)?1:0;
page->user = (is_kernel)?0:1;
page->frame = tmp_page;
}
}
|
上面的zlox_alloc_frame函数,先通过zlox_first_frame函数从frames位图里循环查找出空闲的frame,然后返回该frame对应的二进制的索引值,并存储到tmp_page局部变量里,这样tmp_page的值就是空闲frame的物理基址的高20位部分,然后先通过zlox_set_frame函数将位图里该frame对应的二进制位设置为1,再将对应的page页表项进行设置,将page里的present设置为1,表示该页面存在,然后根据参数设置page页表项的rw读写属性与user用户是否可访问属性,最后将tmp_page赋值给page的frame成员,从而完成单个页表项的设置。
有了frames位图,就可以快速的检测出某个4K页面是否有效,以及可以快速的找出空闲的页面。
在为页目录和页表分配内存空间时,都用到了一些以zlox_kmalloc开头的函数,这些函数都定义在zlox_kheap.c文件里:
/* zlox_kheap.c Kernel heap functions, also provides
a placement malloc() for use before the heap is
initialised. */
#include "zlox_kheap.h"
// end is defined in the linker script.
extern ZLOX_UINT32 _end;
ZLOX_UINT32 placement_address = (ZLOX_UINT32)&_end;
ZLOX_UINT32 zlox_kmalloc_int(ZLOX_UINT32 sz, ZLOX_SINT32 align, ZLOX_UINT32 *phys)
{
ZLOX_UINT32 tmp;
// This will eventually call malloc() on the kernel heap.
// For now, though, we just assign memory at placement_address
// and increment it by sz. Even when we've coded our kernel
// heap, this will be useful for use before the heap is initialised.
if (align == 1 && (placement_address & 0x00000FFF) )
{
// Align the placement address;
placement_address &= 0xFFFFF000;
placement_address += 0x1000;
}
if (phys)
{
*phys = placement_address;
}
tmp = placement_address;
placement_address += sz;
return tmp;
}
ZLOX_UINT32 zlox_kmalloc_a(ZLOX_UINT32 sz)
{
return zlox_kmalloc_int(sz, 1, 0);
}
ZLOX_UINT32 zlox_kmalloc_p(ZLOX_UINT32 sz, ZLOX_UINT32 *phys)
{
return zlox_kmalloc_int(sz, 0, phys);
}
ZLOX_UINT32 zlox_kmalloc_ap(ZLOX_UINT32 sz, ZLOX_UINT32 *phys)
{
return zlox_kmalloc_int(sz, 1, phys);
}
ZLOX_UINT32 zlox_kmalloc(ZLOX_UINT32 sz)
{
return zlox_kmalloc_int(sz, 0, 0);
}
|
上面的zlox_kmalloc_int函数,其实就是从内核结束位置开始,不断向高地址方向分配堆,一般的编程概念里,堆应该是可以动态分配的,也就是说,可以为某个变量或数组分配一段内存,也可以释放掉这段内存,还可以动态扩容这段内存,不过目前zlox_kmalloc_int函数只是做一个简单的扩充分配操作,没有释放操作,因为目前阶段,为页目录和页表分配的内存空间都是常驻于内存里的,所以暂时还用不到释放操作,要到下一个版本才会新增完善的堆管理机制。
上面的zlox_kmalloc有好几个版本,主要是一些快捷函数,例如,zlox_kmalloc_a用于分配堆时,让分配的内存基址能够按照4K对齐,zlox_kmalloc_p函数则是通过提供第二个参数phys,来将物理地址写入phys引用的变量里,它们最终都还是调用zlox_kmalloc_int来完成分配工作,zlox_kmalloc_int函数返回的是可用于编程的线性地址,当给该函数提供第三个参数时,就可以获取到堆的实际物理地址,目前版本里,线性地址和物理地址没什么区别,但是到了以后,多任务及用户态程序时,线性地址就可能会与物理地址不同了。
在zlox_paging.c的zlox_init_paging函数的最后,在开启分页模式之前,会调用zlox_register_interrupt_callback函数来注册PAGE-FAULT EXCEPTIONS(页面错误异常)的处理函数:
ZLOX_VOID zlox_init_paging()
{
................................
................................
// Before we enable paging, we must register our page fault handler.
zlox_register_interrupt_callback(14,zlox_page_fault);
// Now, enable paging!
zlox_switch_page_directory(kernel_directory);
................................
................................
}
................................
................................
ZLOX_VOID zlox_page_fault(ZLOX_ISR_REGISTERS regs)
{
// A page fault has occurred.
// The faulting address is stored in the CR2 register.
ZLOX_UINT32 faulting_address;
ZLOX_SINT32 present;
ZLOX_SINT32 rw;
ZLOX_SINT32 us;
ZLOX_SINT32 reserved;
ZLOX_SINT32 id;
asm volatile("mov %%cr2, %0" : "=r" (faulting_address));
// The error code gives us details of what happened.
present = !(regs.err_code & 0x1); // Page not present
rw = regs.err_code & 0x2; // Write operation?
us = regs.err_code & 0x4; // Processor was in user-mode?
reserved = regs.err_code & 0x8; // Overwritten CPU-reserved bits of page entry?
id = regs.err_code & 0x10; // Caused by an instruction fetch?
// Output an error message.
zlox_monitor_write("Page fault! ( ");
if (present)
zlox_monitor_write("present ");
if (rw)
zlox_monitor_write("read-only ");
if (us)
zlox_monitor_write("user-mode ");
if (reserved)
zlox_monitor_write("reserved ");
if(id)
zlox_monitor_write("instruction fetch ");
zlox_monitor_write(") at 0x");
zlox_monitor_write_hex(faulting_address);
zlox_monitor_write("\n");
ZLOX_PANIC("Page fault");
}
|
当发生页面错误的异常时,就会调用zlox_page_fault函数将相关的异常信息显示出来,在英特尔英文手册的第1967页,有页面错误异常的详细介绍。
一般有两种情况会发生页面错误异常:
-
一种是当线性地址无法映射为物理地址时,例如,我们只设置了16M的物理内存的页目录和页表项,如果程序里要访问的线性地址超过16M时,由于页目录或页表里没有设置相关的项目,所以就无法正常映射
-
另一种情况就是,当无权限访问某个线性地址时,也会发生页面错误的异常。
当发生页面错误的异常时,处理器会将出错的线性地址设置到CR2寄存器里,同时在中断处理程序的错误代码里设置相关的二进制位来表示出错的原因,如下图所示:
图7
上图也位于手册的第1967页,当错误代码里的位0(P)被清零时,表示错误是由于线性地址对应的页面不存在导致的,否则就是其他的页保护措施引起的,当位1(W/R)被设置时,表示错误是由于程序对只读的内存区域进行了写操作造成的。当位2(U/S)被设置时,表示错误是由于用户态程序访问了无权访问的地址而引起的。当位3(RSVD)被设置时,表示错误是由于程序修改覆盖了页表项里的CPU保留位导致的。当位4(ID)被设置时,表示错误是由instruction fetch取指令引起的。
在zlox_kernel.c文件里,就对页错误异常进行了如下的测试:
//zenglOX kernel main entry
ZLOX_SINT32 zlox_kernel_main(ZLOX_VOID * mboot_ptr)
{
// init gdt and idt
zlox_init_descriptor_tables();
// Initialise the screen (by clearing it)
zlox_monitor_clear();
zlox_init_paging();
// Write out a sample string
zlox_monitor_write("hello world!\nwelcome to zenglOX v0.0.5!\n");
ZLOX_UINT32 *ptr = (ZLOX_UINT32*)0xA0000000;
ZLOX_UINT32 do_page_fault = *ptr;
*ptr = do_page_fault;
/*asm volatile("int $0x3");
asm volatile("int $0x4");
asm volatile("sti");
zlox_init_timer(50);*/
return (ZLOX_SINT32)mboot_ptr;
}
|
上面在调用zlox_init_paging函数开启分页后,先将0xA0000000的线性地址设置到ptr指针,再通过do_page_fault = *ptr;尝试读取ptr地址里的值到do_page_fault变量,但是由于ptr的线性地址0xA0000000已经超过了zlox_init_paging函数里设置的16M的大小,所以就会出现如下图所示的异常:
图8
如果是使用bochs加gdb远程调试,则在发生Page fault异常时,bochs会向gdb发送signal 0信号,gdb收到该信号时会中断下来进入调试,只需在gdb里输入c命令继续执行即可。
OK,就到这里,休息,休息一下 o(∩_∩)o~~