处理器:Intel Celeron® Dual-CoreCPU
操作系统:Windows 7 专业版 x86
阅读书籍:《30天自制操作系统》—川合秀实[2015.04.05-04.06]
工具:../toolset/
编写完鼠标移动程序后,内存分布如下。
Figure1. 内存使用情况
x86保护模式下的内存地址空间为0x00000000 ~ 0xFFFFFFFF,其中包含ROM等存储器的地址空间,需要检测出RAM容量。在检测可用内存容量大小时,需要将CPU内的高速缓存(cache)功能关闭。才无cache功能的情况下,检测出的RAM空间才是准确的。
首先检查CPU是否支持cache功能(EFLAG),如果支持cache,则关闭cache功能(CR0)。
/*检查内存地址空间中的RAM *start,end分别是需要检查的内存的起始和结束地址 */ unsigned int memtest(unsigned int start, unsigned int end) { char flg486 = 0; unsigned int eflg, cr0, i; //80386 CPU不支持cache,检查EFLAG的第18位 //如果不支持cache,EFLAG的bit[17]始终为0 eflg = io_load_eflags(); eflg |= EFLAGS_AC_BIT; /* AC-bit = 1 */ io_store_eflags(eflg); eflg = io_load_eflags(); if ((eflg & EFLAGS_AC_BIT) != 0) {//支持cache flg486 = 1; } eflg &= ~EFLAGS_AC_BIT; io_store_eflags(eflg); if (flg486 != 0) { cr0 = load_cr0(); cr0 |= CR0_CACHE_DISABLE; //禁止cache功能:将cr0的bit[30:29]置1 store_cr0(cr0); } i = memtest_sub(start, end); //调用反转法检查RAM的函数 if (flg486 != 0) { cr0 = load_cr0(); cr0 &= ~CR0_CACHE_DISABLE; //允许cache store_cr0(cr0); } return i; }
load_cr0()和store_cr0(cr0)函数是在naskfunc.nas中用汇编指令来操作CR0寄存器的汇编函数:
1. GLOBAL _load_cr0, _store_cr0
2. …
3. _load_cr0: ; int load_cr0(void);
4. MOV EAX,CR0
5. RET
6.
7. _store_cr0: ; void store_cr0(int cr0);
8. MOV EAX,[ESP+4]
9. MOV CR0,EAX
10. RET
用“书”中“反转内存中的值”的方式检测内存是否为RAM(由于C编译器ccl.exe会优化掉反转代码,所以检测可用内存容量大小的程序直接在naskfunc.nas中采用汇编语言来写)。
1. _memtest_sub: ;unsigned int memtest_sub(unsigned intstart, unsigned int end)
2. ;start为检查内存段的起始地址,end为结束地址
3. PUSH EDI ;备份进入此函数前的寄存器的值
4. PUSH ESI
5. PUSH EBX
6. MOV ESI,0xaa55aa55
7. MOV EDI,0x55aa55aa
8. MOV EAX,[ESP+12+4] ;i = start;12为EDI, ESI,EBX 3个寄存器使用的栈空间,
9. ;4为CALL指令引起的(CS:)IP压栈空间
10. mts_loop:
11. MOV EBX,EAX
12. ADD EBX,0xffc ;以4KB为单位进行检查,检查4KB内存段的最后四个字节;
13. MOV EDX,[EBX] ;将[EBX]中的内容备份
14. MOV [EBX],ESI ;往[EBX]中写入0xaa55aa55
15. XOR DWORD[EBX],0xffffffff ;反转[EBX]内存中的每一位
16. CMP EDI,[EBX] ;检查[EBX]内存中的值是否反转成功,
17. JNE mts_fin ;不成功则跳转到mts_fin处
18.
19. XOR DWORD[EBX],0xffffffff ;再反转[EBX]4字节内存中的内存
20. CMP ESI,[EBX] ;检查是否反转成功,
21. JNE mts_fin ;反转不成功则跳转到mts_fin处
22. MOV [EBX],EDX ;将[EBX]的值恢复
23. ADD EAX,0x1000 ;检查下一个4KB内存块,
24. CMP EAX,[ESP+12+8] ;如果低于end则继续检查,否则返回EAX
25. JBE mts_loop
26. POP EBX
27. POP ESI
28. POP EDI
29. RET
30. mts_fin:
31. MOV [EBX],EDX ;恢复[EBX]的值
32. POP EBX
33. POP ESI
34. POP EDI
35. RET对于机器级及以上的程序来说,内存的管理都是基于逻辑的内存地址空间。首先需要一种数据结构来描述内存地址空间。
#define MEMMAN_FREES 4090 //空闲的不连续的内存块数目 struct FREEINFO { //内存地址和大小 unsigned int addr, size; }; struct MEMMAN { //空闲内存空间个数,空闲内存空间的最大个数, //没能紧跟空闲内存释放的内存大小和个数 int frees, maxfrees, lostsize, losts; struct FREEINFO free[MEMMAN_FREES]; };
查看保存在数据结构内的空闲内存块,找到符合要求的内存块则返回此内存块的首地址。将分配出去的内存块从空闲内存块中除去。
/* 分配一块内存来使用 * man是描述当前内存空闲与否的数据结构 * size是当前要分配内存的大小 * 如果分配成功则返回所分配内存的首地址,失败则返回0 */ unsigned int memman_alloc(struct MEMMAN *man, unsigned int size) { unsigned int i, a; for (i = 0; i < man->frees; i++) { //查询到有一段空闲的内存空间大于等于申请分配的内存的大小 if (man->free[i].size >= size) { a = man->free[i].addr; //在数据结构中除去分配除去的内存地址空间 man->free[i].addr += size; man->free[i].size -= size; //如果free[i]这块内存刚好被分完,则在数据结构中将其溢出 if (man->free[i].size == 0) { man->frees--; for (; i < man->frees; i++) { man->free[i] = man->free[i + 1]; } } return a; } } return 0; //无空闲内存 }
在标记内存可用的数据结构中,按照内存地址从小到大的顺序标记每一块可用的内存空间。释放不用的内存时也按照内存地址大小顺序标记在数据结构中。
/* 释放内存:将不用的内存标记在数据结构中,表明这些内存处于空闲状态 * man为描述内存空闲状态的数据结构 * addr,size分别为释放内存的起始地址和大小 * 释放[addr,addr + size]这段内存成功时返回0,否则返回-1 */ int memman_free(struct MEMMAN *man, unsigned int addr, unsigned int size) { int i, j; //数据结构描述空闲内存空间以内存地址从小到大的方式 for (i = 0; i < man->frees; i++) { if (man->free[i].addr > addr) { break; } } //free[i - 1].addr < addr < free[i].addr if (i > 0) { //与数据结构中上一段空闲内存相邻 if (man->free[i - 1].addr + man->free[i - 1].size == addr) { //合成一块 man->free[i - 1].size += size; if (i < man->frees) { //且与数据结构中后一段空闲内存相邻 if (addr + size == man->free[i].addr) { //合成一块 man->free[i - 1].size += man->free[i].size; //在数据结构中清除free[i],它已经合并到free[i-1]描述的空闲内存中了 man->frees--; for (; i < man->frees; i++) { man->free[i] = man->free[i + 1]; } } } return 0; } } //释放内存不与数据结构记录的上一段空闲内存相邻 if (i < man->frees) { //与后一段空闲内存相邻 if (addr + size == man->free[i].addr) { //合并 man->free[i].addr = addr; man->free[i].size += size; return 0; } } //释放内存不与数据结构记录的空闲内存相邻 if (man->frees < MEMMAN_FREES) { for (j = man->frees; j > i; j--) { man->free[j] = man->free[j - 1]; } man->frees++; if (man->maxfrees < man->frees) { man->maxfrees = man->frees;//空闲内存最大数目更新 } //将释放的内存放到合适的位置 man->free[i].addr = addr; man->free[i].size = size; return 0; } //若以上情形皆不符合,则释放内存失败 //记录内存失败的次数和大小 man->losts++; man->lostsize += size; return -1; }
其它函数是为记录内存空闲的数据结构而服务的。
//给记录空闲内存的数据结构初始值 void memman_init(struct MEMMAN *man) { man->frees = 0; man->maxfrees = 0; man->lostsize = 0; man->losts = 0; return; }
此函数没有给空闲内存地址和大小的元素赋予初始值,在要使用内存分配函数时才会初始化这两个值。
//计算当下空闲内存的总和并将其返回 unsigned int memman_total(struct MEMMAN *man) { unsigned int i, t = 0; for (i = 0; i < man->frees; i++) { t += man->free[i].size; } return t; }
将_memtest_sub函数写进naskfunc.nas文件中,将其余函数写进memman.c文件中,按照“书”中那样组织主函数HariMain()的内容。依据各文件关系在各个文件中作声明,并修改Makefile。通过编译、链接后,在“!cons_nt.bat”中使用“makerun”命令运行程序,得到如下界面:
Figure2. 程序运行结果
#define MEMMAN_FREES 4090 #define MEMMAN_ADDR 0x003c0000 struct FREEINFO { unsigned int addr, size; }; struct MEMMAN { int frees, maxfrees, lostsize, losts; struct FREEINFO free[MEMMAN_FREES]; }; struct MEMMAN *memman = (struct MEMMAN *) 0x003c0000;
x86保护模式下,memman= 0x003c0000;memman->frees表示0x003c0000 + 0~0x003c0003这4字节内存;memman->maxfrees表示0x003c0000+ 4 ~ 0x003c0007这4字节内存;…;结构体被编译器编译后,每个元素表示相对结构体起始地址的偏移值。引用结构体元素时,按照“结构体起始地址+ 元素偏移值”访问到对应的内存。
Figure3. 编译器和结构体
内存检测函数检测0x00400000~ 0xbfffffff这段(0x00400000之前的都被使用过了,是RAM)内存空间(32M)都为RAM。
初始化structMEMMAN结构体的函数没有初始化结构体内的数组,在释放首地址为0x00001000,大小为0x0009e000这段内存空间时,这种情况属于释放内存不与数据结构记录的空闲空间相邻,故而.free[0].addr= 0x00001000,.free[0].size= 0x0009e000(647KB);再释放首地址为0x00400000,大小为32M– 0x00400000(28M)这段内存空间时,这段空闲内存将会被记录在.free.[1]中。所以在数据结构structMEMMAN中的空闲内存约29M左右。
/*以4KB为单位的内存分配 *man为记录内存空闲的数据结构 *size所需分配内存大小 *分配成功返回分配内存的首地址,分配失败返回0 */ unsigned int memman_alloc_4k(struct MEMMAN *man, unsigned int size) { unsigned int a; size = (size + 0xfff) & 0xfffff000; //以4KB(0x1000)向上舍入 a = memman_alloc(man, size; return a; } /*以4KB为单位进行释放内存,以空闲的状态记录在数据结构中 *man为记录内存空闲的数据结构, *addr为即将释放内存的首地址, *size为即将释放内存的大小 *内存释放成功返回0,否则返回-1 */ int memman_free_4k(struct MEMMAN *man, unsigned int addr, unsigned int size) { int i; size = (size + 0xfff) & 0xfffff000; //以4KB(0x1000)向上舍入 i = memman_free(man, addr, size); return 0; }
将这两个函数声明后,替换主函数HariMain()中的内存释放函数。就这样一步一步,内存分配和内存释放函数变得越来越靠谱。
/*bootpack.c*/ #include "bootpack.h" #include <stdio.h> void HariMain(void) { int data; int mx, my; char str[30]; char mcursor[256]; char mousebuf[128]; //接收鼠标数据缓冲区 unsigned int memtotal; struct MOUSE_DEC mdec; struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO; struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR; init_gdtidt(); init_pic(); //CPU处理中断----------- io_sti(); fifo8_init(&mousefifo, 32, mousebuf); //鼠标数据的缓冲区FIFO io_out8(PIC0_IMR, 0xfb);//主PIC IRQ2中断开启 io_out8(PIC1_IMR, 0xef); //从PIC IRQ12中断开启 init_keyboard(); memtotal = memtest(0x00400000, 0xbfffffff); memman_init(memman); memman_free_4k(memman, 0x00001000, 0x00000300); //1kb memman_free_4k(memman, 0x00400000, 0x00000400);//2kb init_palette(); enable_mouse(&mdec); init_screen(binfo->vram, binfo->scrnx, binfo->scrny); init_mouse_cursor8(mcursor, COL8_000000); sprintf(str, "memory %dMB free : %dKB", memtotal / (1024 * 1024), memman_total(memman) / 1024); putfonts8_asc(binfo->vram, binfo->scrnx, 0, 32, COL8_FFFFFF, str); //不断查询缓冲区,如果有数据,将缓冲区内的数据显示完为止 for (;;) { io_cli(); //查看缓冲区时,屏蔽中断 if (fifo8_status(&mousefifo) == 0) { io_stihlt(); //如果缓冲区内无数据则开启中断并让CPU休眠,直到中断唤醒 } else { data = fifo8_get(&mousefifo); io_sti(); //从缓冲区读完数据立即开启中断 if (mouse_decode(&mdec, data) != 0) { //获得解读到的鼠标坐标 sprintf(str, "[lcr %4d %4d]", mdec.x, mdec.y); if ((mdec.btn & 0x01) != 0) { str[1] = 'L'; } if ((mdec.btn & 0x02) != 0) { str[3] = 'R'; } if ((mdec.btn & 0x04) != 0) { str[2] = 'C'; } boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 + 15 * 8 - 1, 31); putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, str); boxfill8(binfo->vram, binfo->scrnx, COL8_000000, mx, my, mx + 15, my + 15);//隐藏上次鼠标移动留下的痕迹 mx += mdec.x; my += mdec.y; if (mx < 0) { mx = 0; } if (my < 0) { my = 0; } if (mx > binfo->scrnx - 16) { mx = binfo->scrnx - 16; } if (my > binfo->scrny - 16) { my = binfo->scrny - 16; } sprintf(str, "(%3d, %3d)", mx, my); putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16); } } } }
虽然实际要求的是释放1kb + 2kb = 3kb的内存,但实际会释放4kb + 4kb = 8kb的内存:
Figure4. 以4kb为单位管理内存
[x86OS] Note Over.
[2015.04.17]