6 内存管理
6.1 内存管理
大家在日常开发的时候经常用到malloc和free函数。但是,在内核中,我们并没有malloc和free这两个函数。这该怎么办呢?
答案是自己规划。我们先建立kernel/memory/memory.c,然后把cstart中的内存处理相关程序移动到memory.c。
为了保持内存使用的对齐,我们将所有可用的内存分组,最少为8字节一组,然后16字节……以此类推,最大可达2GB一组。内存中,我们用链表的形式,链表节点存放在空闲内存的前4字节的位置上。
知道原理以后,代码其实很好写。
另外,笔者观赏了一下GCC生成的代码,感觉非常狗屎,memory_free函数笔者用汇编可以21行写出的代码,GCC用了38行,而且还是手工对C代码优化过后的。这38行中包含大量的内存操作。而笔者的代码完全没有进行过内存操作。设想这是一段核心代码(还真的是核心代码),这点小小的时间差距会累积成多大!malloc函数笔者用了16行,而GCC用了39行。这就体现了为什么大型工程中的核心代码都是汇编写的。在用汇编改写之后,最后的二进制文件减小了1.5KB。在NorlitOS最初的开发阶段中,我们会尽少使用汇编,但是之后的优化工程中,汇编是必不可少的。
u_addr* memoryBlocks[30]={EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,//0-7 Bytes
EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,//8-17 KBs
EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,//18-27 MBs
EMPTY,EMPTY};// 28-29 GBs
代码6.1.1数据结构(chapter6/a/kernel/memory/memory.c)
我们不管理4Bytes以下的内存,因为管理这些内存会给系统额外的开销。对于这些内存,我们简单地丢弃它们。不过,使用malloc和free功能永远也不会产生这些多余项。EMPTY被定义成了0xFFFFFFFF,但其实0xFFFFFFD和0xFFFFFFFE也是可以的。超过4KB的内存实际上应该让分页机制管理了,但是在分页机制实现之前,我们先把实现容许的最大范围变成2^31次,也就是2GB。
笔者修改了loader.asm,将cpuid.inc与之合并,并且加入了检测分页机制支持的功能。
CPUInit:
pushfd ;储存EFLAGS
mov eax,[esp] ;保存老的EFLAGS
bts dword[esp],21 ;更改EFLAGS.ID
popfd ;设置EFLAGS
pushfd ;保存新EFLAGS
pop ebx ;EBX=EFLAGS
cmp eax,ebx ;检查EFLAGS.ID有没有被成功更改
je .nocpuid
mov eax,0x80000000 ; CPU扩展信息
cpuid
cmp eax,0x80000001
xor eax,eax
jb .nonx ;不支持CPU扩展信息
mov eax,0x80000001
cpuid
xor eax,eax
bt edx,20 ;Check NX
adc al,0
shl al,1
.nonx:
push eax
mov eax,0x7
xor ecx,ecx
cpuid
pop eax
bt ebx,7 ;Check SMEP
adc al,0
shl al,1
push eax ;Reserved NX SMEP PSE-36 PAT PGE PAE PSE
mov eax,0x1
cpuid
pop eax
bt edx,17 ;Check PSE-36
adc al,0
shl al,1
bt edx,16 ;Check PAT
adc al,0
shl ax,1
bt edx,13 ;Check PAT
adc ax,0
shl ax,1
bt edx,6 ;Check PAE
adc ax,0
shl ax,1
bt edx,3 ; CheckPSE
adc ax,0
mov [BootParamPhyAddr+ PGInfo],eax
bt eax,1
jc PagingOnPAE
jmp PagingOn
.nocpuid:
mov esi, STR_FAIL
call DispStrPM
cli
hlt
代码6.1.2检测机能(chapter6/a/boot/loader.asm)
当然了,由于我们修改了0x500处的结构,别忘记同时修改loader.inc和struct。
FASTCALLstaticvoidmemory_block_free(u_addraddress, u_addr sizep, u8 bit){
u_addr* ll=(u_addr*)address;
u_addr** head=&memoryBlocks[bit-2];
u_addr*entry=*head;
u_addr**prev=head;
for(;entry!=EMPTY;prev=(u_addr**)entry,entry=*prev){
if(entry>ll){
if(!(address&sizep)&&(address+sizep==(u_addr)entry)){
*prev=(u_addr*)(*entry);
memory_block_free(address, sizep*2, bit+1);
return;
}
break;
}elseif((address&sizep)&&((u_addr)entry+sizep==address)){
*prev=(u_addr*)(*entry);
memory_block_free((u_addr)entry, sizep*2, bit+1);
return;
}
}
*prev=ll;
*ll=(u_addr)entry;
}
FASTCALLvoid memory_free_nocheck(u_addr address, u_addr size){
u8 bit;
for(bit=2;bit<32;bit++){
u_addr sizep=1<<bit;
if(address&sizep){
if(size<sizep)break;
memory_block_free(address,sizep,bit);
address+=sizep;
size-=sizep;
}
}
for(;bit>1;bit--){
u_addr sizep=1<<bit;
if(size&sizep){
memory_block_free(address,sizep,bit);
address+=sizep;
size-=sizep;
}
}
}
/******************************
FASTCALL void memory_free(u_addr, u_addr) ; Free memorywith security check
******************************
memory_free:
cmp edx, 3
jbe .3
test eax, 3
jz .1
mov ecx, eax
and ecx, 0xFFFFFFFC
add ecx, 4
sub ecx, eax
sub edx, ecx
add eax, ecx
cmp edx, 3
jbe .3
.1:
test edx, 3
jz .2
and edx, 0xFFFFFFFC
.2:
jmp memory_free_nocheck
.3:
ret
******************************/
FASTCALLvoid memory_free(u_addr address, u_addr size){
if(size<0b100)return;
if(address&0b11){
u_addr diff=(address&0b111111111111111111111111111111100)+0b100-address;
size-=diff;
address+=diff;
if(size<0b100)return;
}
if(size&0b11)size&=0b11111111111111111111111111111100;
memory_free_nocheck(address,size);
}
/******************************
FASTCALL void* memory_alloc(u8) ; allocate 2^bit-lengthmemory block
******************************
memory_alloc:
lea edx, [eax*4+memoryBlocks-8]
mov ecx, [edx]
inc ecx
or ecx, ecx
jz .1
dec ecx
mov eax, [ecx]
mov [edx],eax
mov eax, ecx
ret
.1:
push ebx
inc eax
mov ebx, eax
call memory_alloc
mov ecx, ebx
xor edx, edx
inc edx
shl edx, cl
mov ebx, eax
add eax, edx
call memory_free_nocheck
mov eax, ebx
pop ebx
ret
******************************/
FASTCALLvoid* memory_alloc(u8 bit){
u_addr** head=&memoryBlocks[bit-2];
u_addr* getit=(u_addr*)(*head);
if(getit==EMPTY){
getit=memory_alloc(bit+1);
memory_free_nocheck(((u_addr)getit)+(1<<bit),1<<bit);
}else{
*head=*((u_addr**)getit);
}
return(void*)getit;
}
/******************************
FASTCALL void* malloc(u_addr); allocate a block ofmemory
******************************
malloc:
add eax, 3
and eax, 0xFFFFFFFC
add eax, 4
bsr edx, eax
push ebx
mov ebx, eax
btr eax, edx
xor eax, eax
jz .1
inc edx
.1:
mov eax, edx
push eax
call memory_alloc
pop ecx
xor edx, edx
inc edx
shl edx, cl
mov [eax], edx
cmp edx, ebx
jz .2
push eax
add eax, ebx
sub edx, ebx
call memory_free_nocheck
pop eax
.2:
pop ebx
ret
******************************/
FASTCALLvoid* malloc(u_addr size){
size=((size-1+sizeof(u_addr))/4+1)*4;
u8 bit;
for(bit=2;bit<32;bit++){
if((1<<bit)>=size)break;
}
void* getit=memory_alloc(bit);
if((1<<bit)!=size)memory_free_nocheck(((u_addr)getit)+size,(1<<bit)-size);
*((u_addr*)getit)=1<<bit;
return getit+sizeof(u_addr);
}
/******************************
FASTCALL void free(void*) ; free an mallocedblock
******************************
free:
sub eax, 4
mov edx, [eax]
jmp memory_free_nocheck
******************************/
FASTCALLvoid free(void* addr){
u_addr* ad=(u_addr*)(addr-sizeof(u_addr));
memory_free_nocheck((u_addr)ad,*ad);
}
FASTCALLvoid init_memory(){
BootParam* bp=(BootParam*)0x500;
{
u32 pgFlag=bp->pgFlag;
if(pgFlag&0b1)puts("PSE ");
if(pgFlag&0b10)puts("PAE ");
if(pgFlag&0b100)puts("PGE ");
if(pgFlag&0b1000)puts("PAT ");
if(pgFlag&0b10000)puts("PSE-36 ");
if(pgFlag&0b100000)puts("SMEP ");
if(pgFlag&0b1000000)puts("NX ");
puts("\r\n");
}
u32 bplen=bp->len;
puts("Total Items: ");dispInt(bp->len);
puts("\r\nBase: Limit: Type:\r\n");
ARDSItem* ai=bp->items;
u64 max=0;
for(;bplen>0;bplen--,ai++){
dispInt(ai->base);putc(' ');
dispInt(ai->limit);putc(' ');
switch(ai->type){
case5:puts("Unusable\r\n");continue;
case6:puts("Disabled\r\n");continue;
case1:puts("Avaliable\r\n");break;
case3:puts("ACPI\r\n");break;
case4:puts("NVS\r\n");break;
default:puts("Reserved\r\n");continue;
}
max=ai->base+ai->limit;
}
puts("The Size of Your Memory: ");dispInt(max/0x100000);puts("MB\r\n");
if(max<0x800000){
puts("Your memory isless than 8MB");
}
if(max>0xFFFFFFFF&&bp->pgFlag&0b10)puts("Your memory isover 4GB. Please Turn on PAE if you want to use them.");
memory_free_nocheck(0x100000+KERNEL_SIZE,0x100000-KERNEL_SIZE);
}
代码6.1.3好戏在后头(chapter6/a/kernel/memory/memory.c)
总体来看,在分配的时候还得释放,导致我们分配的速度会下降。释放时需要计算块的合并,复杂度也比较高。总体来看我们的算法不咋的,不过在实现分页之前,我们只要一个能用的算法就行。
6.2 题外话调试断点
在编写程序的时候,无论是操作系统还是应用软件,都可能会存在意想不到的bug。这些bug就需要我们来debug,也就是调试。但是在我们系统开发中,只能使用bochs进行调试。由于操作系统本身不带有调试信息,所以我们也不知道断点应该设置在哪里。所以,我们需要手工实现调试断点。
调试断点一般用int 3,因为这个指令可以生成1字节的二进制代码,所以能够充当软断点的作用。我们也使用int 3。要中断时,我们在汇编中插入int 3的代码或者在c中插入__asm__(“int $3”);的代码。
break_point:
mov [0x0],esp
mov esp,0x500
pushad
mov eax,[0x0]
push dword[eax+8]
push dword[eax]
push dword[0x0]
call breakpoint_disp
add esp,16
popad
mov esp,[0x0]
iretd
代码6.2.1中断处理程序改写(chapter6/a/kernel/interrupt.asm)
然后我们再breakpoint_disp中显示各参数。我们用pushad的本意是保存寄存器不被破坏,但是我们也可以加以利用,在breakpoint_disp中加以显示。CS的值是恒定的我们就忽略了。
ASMLINKAGEvoid breakpoint_disp(u_addr esp12, u_addr eip, u_addr eflags,
u_addr edi, u_addr esi, u_addr ebp, u_addr temp,
u_addr ebx, u_addr edx, u_addr ecx, u_addr eax){
puts("\r\n#BP Breakpoint\r\nEAX: ");
dispInt(eax);puts(" ECX: ");dispInt(ecx);puts("\r\nEDX: ");
dispInt(edx);puts(" EBX: ");dispInt(ebx);puts("\r\nESP: ");
dispInt(esp12+12);puts(" EBP: ");dispInt(ebp);puts("\r\nESI: ");
dispInt(esi);puts(" EDI: ");dispInt(edi);puts("\r\nEIP: ");
dispInt(eip);puts("\r\nEFLAGS: ");dispInt(eflags);
}
代码6.2.2调试信息(chapter6/a/kernel/int.c)
我们顺便在typedef.h里面定义BREAKPOINT为__asm__(“int $3”),然后随便加一句BREAKPOINT;试试。
图6.2.1成功!
6.3 高端内存
Linux把操作系统的内存区域放置在0xC000000以上,也就是3GB以上的逻辑地址。你应该还记得我们最初loader里面映射内存的时候把全部的内存都映射到了2MB或者4MB的区域(视PAE情况而定)。也就是说,我们几乎无需改动什么代码就可以把内核加载到高端的内存。唯一需要改动的是我们必须先分页再复制内核。接下来就可以简单地把入口点设置为0xC0100000,(别忘了改loader.inc),然后把内核中所有的常量都改为对应的0xC0开头的地址。这部分很简单,我就不贴代码了。
将来我们将把进程的地址空间放在前3GB,所以在目前的开发中,我们必须避免对低3GB的访问。我们删除掉前3GB的页表。
BootParam* bp=(BootParam*)0xC0000500;
if(bp->pgFlag&0b10){
for(a=0xC0003000;a<0xC0003018;a++)*a=0x0;
}else{
for(a=0xC0002000;a<0xC0002C00;a++)*a=0x0;
}
__asm__ __volatile__("mov%cr3,%eax");
__asm__ __volatile__("mov%eax,%cr3");
__asm__ __volatile__("":::"memory");
代码6.3.1去除低端页表(chapter6/b/kernel/memory/memory.c)
这里我们用了gcc的内联汇编来刷新TLB缓存,使我们的更改即时生效,要注意的是GCC的汇编和MASM、NASM不同,运算是结果在右的,也就是说mov%cr3,%eax(GCC)=mov eax, cr3(MASM&NASM)。
Make一下,运行正常的话说明我们的程序已经完全运行在高端地址了。
不过这么临时的页表修改不是大计,我们接下来肯定还要完善页表管理机制,就像之前的内存管理一样。由于我们分配页表时还要考虑特权极的问题,所以,在此之前,我们切换到Ring3试试。
6.4 TSS (Task-State Segment)
TSS,根据英文来看的话是用来保存进程的状态的。然而,我们现在并没有实现进程。那我们为什么要讲述TSS呢?原因是栈。从Ring3返回Ring0时(如中断),势必要涉及到栈的切换。而一旦进入中断处理程序,一些参数已经被压栈,我们再要改变已经来不及了。所以,我们势必要在进入内核前切换栈。Intel给我们准备了一个强有力的工具-TSS。TSS中能够设置栈的位置,并且在优先级变换的同时栈也会被切换。这样就保证了内核和进程互不干扰。
本来Intel让我们直接Call或者Jmp到TSS就可以开始进程。不过这个方法可控性不如由系统来开始。所以,我们不利用TSS的其它功能,只利用栈的切换。
图6.4.1 TSS的结构
上图是TSS的图示。我们先来实现一下TSS的结构。
;=====================================================
; InitTSS(u32 esp0, u16 ss0);初始化TSS
;-----------------------------------------------------
; Entry:
; - arg0 -> ss0
; - arg1 -> esp0
; Exit:
; -填充一个TSS
%macro InitTSS 2
dw0,0;backlink
dd%2 ; esp0
dw%1,0 ; ss0
dd0 ; esp1
dw0,0;ss1
dd0 ; esp2
dw0,0;ss2
dd0 ; cr3
dd0 ; eip
dd0 ; flags
dd0 ; eax
dd0 ; ecx
dd0 ; edx
dd0 ; ebx
dd0 ; esp
dd0 ; ebp
dd0 ; esi
dd0 ; edi
dw0,0;es
dw0,0;cs
dw0,0;ss
dw0,0;ds
dw0,0;fs
dw0,0;gs
dw0,0;ldt
dw0 ; trap
dw104; iobase
%endmacro
代码6.4.1填充TSS(chapter6/c/boot/include/protect.inc)
然后在GDT表中加入一行
GDT_FLAT_TSS: SegmentDescriptor0,TSS_END-TSS_START,DA_386TSS;TSS段
按照我们一贯的偷懒风格,最好所有的内容都在编译时设置好。但是TSS在data段,而我们又没法再编译时知道data段的地址,所以我们还是在运行时设置Base。设置base的代码非常简单:
mov eax,TSS_START
mov [GDT_FLAT_TSS+2],ax
shr eax,16
mov [GDT_FLAT_TSS+4],al
mov [GDT_FLAT_TSS+7],ah
lgdt [GdtPtr]
lidt [IdtPtr]
mov ax,SEL_FLAT_TSS
ltr ax
代码6.4.2设置GDT中的TSS项(chapter6/c/kernel/interrupt.asm)
So Easy, Right?我们打开Bochs调试,用info tss命令看一下
<bochs:2>info tss
tr:s=0x28, base=0x00000000c0101090, valid=1
ss:esp(0): 0x0010:0xc019fc00
ss:esp(1): 0x0000:0x00000000
ss:esp(2): 0x0000:0x00000000
cr3: 0x00000000
eip: 0x00000000
eflags: 0x00000000
cs: 0x0000 ds: 0x0000 ss: 0x0000
es: 0x0000 fs: 0x0000 gs: 0x0000
eax: 0x00000000 ebx: 0x00000000 ecx:0x00000000 edx: 0x00000000
esi: 0x00000000 edi: 0x00000000 ebp:0x00000000 esp: 0x00000000
ldt: 0x0000
i/o map: 0x0068
Perfect!我们已经设置完毕了TSS。
接下来就可以开始转移特权极了,我们利用iretd函数:
mov ax,32|3
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
push dword32|3
push esp
push dword0x1202
push dword24|3
push ring3
iretd
ring3:
int 3
jmp $
代码6.4.3最后的转移(chapter6/c/kernel/entry.asm)
这里要注意的是,如果使用软中断,目标门的DPL必须要小于等于当前CPL。所以我们修改一下我们的IDT:
GateDescriptor BREAK_POINT, SEL_FLAT_C,0, DA_386IGate + SDA_DPL3 ;3
这样就好了。至此,我们已经完成了从Ring0到Ring3的切换。Make并运行一下。不好!bochs自动重启了!对了,笔者突然想到,我们初始化的页表是内核级别的页表。把lib.inc里的00000011b都换成00000111b了以后,成功运行了!我们用sreg看一下,的确在ring3运行!
<bochs:2>sreg
es:0x0023, dh=0x00cff300, dl=0x0000ffff, valid=1
Datasegment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
cs:0x001b, dh=0x00cffb00, dl=0x0000ffff, valid=1
Codesegment, base=0x00000000, limit=0xffffffff, Execute/Read, Accessed,
32-bit
ss:0x0023, dh=0x00cff300, dl=0x0000ffff, valid=1
Datasegment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
ds:0x0023, dh=0x00cff300, dl=0x0000ffff, valid=7
Datasegment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
fs:0x0023, dh=0x00cff300, dl=0x0000ffff, valid=1
Datasegment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
gs:0x0023, dh=0x00cff300, dl=0x0000ffff, valid=1
Datasegment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0028, dh=0xc0008b10, dl=0x10b00068, valid=1
gdtr:base=0x00000000c0100f00, limit=0x2f
idtr:base=0x00000000c0100f30, limit=0x17f
这是在是太妙了!从今以后,我们就可以灵活地在不同特权极间转移了!我们的GDT终于也发挥了全部的作用。
6.5 Oh! Oh~ Start Page♪
我们之前的键盘中断还开着,先关了,不然有点烦。
还有,在测试的过程中,我发现malloc中大部分的时间都浪费在回收多余的内存上了
if((1<<bit)!=size)memory_free_nocheck(((u_addr)getit)+size,(1<<bit)-size)
我们稍作修改以后malloc的时间顿时减少了80%。可喜可贺。看来果真80%的时间会消耗在20%的代码上(八坂真寻:这里貌似只有1%的代码啊)。
这部分代码就自己去源代码看吧,自己写东西的时候就会发现很多时候,一些细节会影响成败,但能发现的话就再好不过了。
创建4GB内存对应的页表最多需要4MB。而我们一共也就初始化了4MB的页表。我们修改一下loader.inc和lib.inc,使其提供8MB的页表支持,然后修改memory.c,把这些空间加入到分配中。
然后我们创建paging.c,添加一个函数
#define ALLOC_PAGE() memory_alloc(12)
#define PAGE_OFFSET 0xC0000000
#define MAX_MAPPING 0x40000000
#define va2pa(x) ((u_addr)(x)-PAGE_OFFSET)
#define pa2va(x) ((u_addr)(x)+PAGE_OFFSET)
u_addr* flat_ptes=NULL;
u_addr* kernel_pde=NULL;
FASTCALLvoid init_paging(u_addr memsize){
u_addr index;
u_addr pages=memsize/0x1000;
flat_ptes=memory_alloc(22);// Allocate at most 4MB page table entrys
u_addr allocpages=(pages+1024-1)&~(1024-1);
{
memory_free_nocheck(((u_addr)flat_ptes)+allocpages*sizeof(u_addr),(1<<22)-allocpages*sizeof(u_addr));
// Release extra memory
u_addr* pteptr=flat_ptes;
for(index=0;index<pages;index++,pteptr++){
*pteptr=index*0x1000+0b111;
}
for(;index<allocpages;index++,pteptr++){
*pteptr=index*0x1000; // Page not present
}
}
u_addr* kernel_pde=ALLOC_PAGE();
{
u_addr* pdeptr=kernel_pde;
u_addr maxpdeentry=allocpages/1024;
maxpdeentry=maxpdeentry>(MAX_MAPPING/0x1000/1024)?(MAX_MAPPING/0x1000/1024):maxpdeentry;
for(index=0;index<PAGE_OFFSET/0x1000/1024;index++,pdeptr++){
*pdeptr=0;// Page not present
}
for(index=0;index<maxpdeentry;index++,pdeptr++){
*pdeptr=va2pa(flat_ptes)+index*0x1000+0b111;
}
for(;index<1024-(PAGE_OFFSET/0x1000/1024);index++,pdeptr++){
*pdeptr=0;// Page not present
}
}
__asm__ __volatile__("movl%0,%%cr3"::"a"(va2pa(pdes)));
}
代码6.5.1平坦分页(chapter6/d/kernel/memory/paging.c)
这里面比较重要的一点是va2pa(virtualaddress to physical address),虚拟地址转换为物理地址。对于内核来说,虚拟地址等同线性地址,而线性地址又相当于物理地址+0xC0000000。不过这些数值都用宏替换来解决。本质上讲,这段代码将flat_ptes所指向的对象初始化为一个pte数组,这部分在平坦分页或者说一次性处理4MB的分页时非常有用。然后初始化了内核用的pde,只初始化了高1GB的内存。最后一句话把pde的地址转化为物理地址加载到了cr3。至此,1GB一下的内存完全被初始化到了内核空间。对了,这里初始化的分页全部是用户级别的,为了使我们之前的切换到ring3的代码正常工作。未来在实现保护机制的时候,我们还会要修改这些特权级的事。我们先实现一下进程看看。
前6章源代码下载地址:百度网盘
本文详细介绍了在自制操作系统Norlit OS中如何实现内存管理,包括分组内存、空闲链表、分配与释放内存的函数实现,以及利用TSS进行特权级切换。通过手动优化代码,提高内存管理效率,并探讨了调试断点和高端内存的处理方法。
2553

被折叠的 条评论
为什么被折叠?



