操作系统Ucore:Lab2内存管理(六)

进入Lab2,Lab2的主要内容是内存的管理

0.新的bug

按照文档上的说明,我们要把lab1中写的代码复制到Lab2中。我一开始并不想这样做,因为我想保留我在Lab1中写的注释,所以我就merge了lab2到lab1中,经过了艰难的处理冲突之后,make qemu 。但悲剧的是qemu的控制台没有输出,我仔细的再次比对代码,还是没有找出为什么。无奈之下,只好复制lab1的代码到lab2…

但这还没完
在这里插入图片描述

源代码中由好几个像注释部分这样初始化的位置,但是我前文已经验证了这样是无法正常初始化的啊…用gdb看这些值都是0,而且程序也无法正常运行。把这种代码搬到自己电脑上用gcc编译都是不过的,不知道ucore里使用了什么样的魔法可以编译通过。

所以只能手动修改了,把初始化放到使用这个变量之前的函数体中,但是这个几个引用的函数还都是static的,那就只好在对应的文件里写一个init函数,仔细一看,这个default_pmm_manager还是const的…还得用一个其他变量去初始化…

ps:关于这个初始化问题,我是这样想的,我发现错误是在编译阶段报的,所以应该是编译器认为初始化的变量没有办法算出来(因为具体的地址需要在链接阶段才能算出来),所以拒绝初始化。在我的机器上实验(gcc version= 9.3.0 )这种情况是会报error的,但是不知道ucore的哪个选项让它可以无视错误继续编译,我也没有深入研究了,要是有谁知道可以在评论里留言。

总之,经过了一番和gcc的斗智斗勇之后,程序总算是能跑起来了…

1.探测内存

在bootasm中,加入了探测内存的功能,可以得到机器中可用的内存分布。

具体的位置在probe_memory标号处,具体是怎么探测的,其实并不重要,只需要知道

  • 这个功能是由BIOS来完成的,我们只需要使用int指令加上参数调用就行。

  • 最终返回的结果在内存位置0x8000处,我们这时候还是在实模式下的,所以这是物理内存的位置。

  • 我们使用一个结构体来描述内存分布,即e820map

在这里插入图片描述
注意:我们只是定义了这个结构体,并没有实例化,也不需要实例化,只需要让其指向0x8000就行了(这个过程在page_init里执行)。定义这个结构体的目的在于方便我们访问内存分布的信息。

我们只需要知道实际的输出是什么就行了
在这里插入图片描述
得到的结果如图,第一列是内存大小,第二列是范围,第三列是类型。(根据文档的描述,这里报告的也不是全部,比如现存的额映射地址就不会报告)
文档在这里
在这里插入图片描述
这里的类型由1和2两种,简而言之,只可以使用type = 1的内存

一共可以用两块内存,从0x0开始的640KB和从0x100000开始的126MB.第一部分,装了bootblock,我们主要管理的是第二部分大约128MB的内存。

2.Entry.S

bootmain还是熟悉的配方,读取磁盘,载入ELF,跳到内核段代码。

这里有一点,就是编译的时候链接脚本变了,程序的入口地址不再是0x100000了,而是0xc010_0000。
在这里插入图片描述
但这并不影响我们加载
在这里插入图片描述
bootmain里加载的时候都只取了低30位,所以实际加载和跳转的地址和Lab1是一样的。

转到e_entry之后,在kern_init之前要先执行entry.s

在这里插入图片描述
这一套操作是熟悉的加载gdt表,唯一不同的就是gdt使用了REALLOC宏修饰,这样做的原因是,我们在链接的时候给的起始地址是0xc010_0000,所以__gdtdesc这个标号的值是0xc010_0000加上偏移量,这对于我们来说是个虚拟地址,实际上的__gdtdesc是在0x0010_0000开头的位置上,所以要减去0xc000_0000

我们再看gdt的内容
在这里插入图片描述
基址是 (-0xc000_0000),界限是4G。为什么会这样呢,原因就是我们现在需要把0xc010_0000 的地址都映射到0x0010_0000位置

而寻址的方式是CS+IP,所以我们需要把CS设置位-0xc000_0000就可以完成映射。

这里一定要清楚:

执行完ljmp指令之后,EIP的值就是0xc010_0000开头的了

但是实际上我们访问的物理地址还是0x0010_0000位置(因为CS基址映射)
在这里插入图片描述
设置新的内核栈,这次和Lab1不一样了,新的栈空间为

data段的开头 ---- data段开头+8KB。

这里有一个小问题,就是同时链接这么多文件的时候,这么确保栈在data段的开头。
在这里插入图片描述
我们可以看到,链接时entry.o是在开头的。我们做一个实验

step1.在1.c文件中写main函数,在2.c文件中写func函数。

step2.分别编译成1.obj和2.obj

step3. ld 1.obj 2.obj -o 12 和 ld 2.obj 1.obj -o 21

step4.查看反汇编(省略了具体汇编代码)

12.hex
在这里插入图片描述
21.hex
在这里插入图片描述
我们发现,函数的位置和链接的顺序是一样的。所以entry.s总是在data段的开头(这样的实验不太严谨,毕竟我也没有看ld的文档…但大概就是这样吧)

最后,call kern_init

3.pmm_init

pmm_init前都没有什么变化.

这里还有一个问题,就是如果使用gdb在经过entry.s中的ljmp之后,每一步返汇编的值就不对了,而且使用b 函数名称这种方式也打不上断点了。原因是gdb好像并不会看CS的值,只会看EIP,但是实际的地址和EIP并不一样,所以无法正确显示信息。

解决的办法为:
1.对于单步反汇编
define rawsi
si
x/i ($eip - 0xc0000000)
end
定义这个宏,手动反汇编实际内存位置
2.对于函数断点
这个暂时还没有找到好办法,去反汇编出来的kern的asm里找地址吧
经过半个小时的研究,终于找到一种奇妙的方法
define pb
set $pfunc = (uint32_t)$arg0 - 0xc0000000
b *($pfunc)
end
使用pb func_name 就可以了

别想了,这些虽然能正确的打上断点,但是并不能单步执行和查看变量值。

所以我们只能手动debug了,首先我们要善其器,来一个DEBUG起手式

#define SPLIT cprintf("------------------------\n")

#define DEBUG_LEVEL 2
#define __output(...) \
    cprintf(__VA_ARGS__); 

#if DEBUG_LEVEL == 1
#define __format(__fmt__) "<%s>: " __fmt__ "\n"
#define DEBUG(__fmt__, ...) \
         __output(__format(__fmt__), __FUNCTION__, ##__VA_ARGS__);
#elif DEBUG_LEVEL == 2
#define __format(__fmt__) "(%d)-<%s>: " __fmt__ "\n"
#define DEBUG(__fmt__, ...) \
         __output(__format(__fmt__), __LINE__, __FUNCTION__, ##__VA_ARGS__);
#else 
#define __format(__fmt__) "%s(%d)-<%s>: " __fmt__ "\n"
#define DEBUG(__fmt__, ...) \
         __output(__format(__fmt__), __FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__);
#endif

定义完这些之后,就可以方便的看出输出的位置了

我们使用一个叫做 pmm_manager的结构体来代表现在正在使用的管理方式
在这里插入图片描述
此结构体里有数据和可供调用的方法,可以理解为一个类,我们在管理内存过程中会调用这些方法。

4.page_init

此函数的开头是探测内存,这里就不在解释了。

第二部分,我们主要来看输出的值是什么
在这里插入图片描述
在这里插入图片描述
首先,最大的内存max_pa 为 7fe0000,这实际上包含了e820map的前四段内容。至于为什么要把不可用的也算进来,我也还不清楚,看到后面再补充

end 指示数据段的末尾

npage 表示把所有的内存分为 32736 个页

pages 是第一个Page结构体的起始位置,其算法为将end取整到4kb后的第一个页

freemem 为pages 后面的内存减去page数组大小的值

这样描述很不清楚,所以我画了一张图来方便理解
在这里插入图片描述
如图,具体的内存地址可能会不太一样。

所有的页都会对应pages数组中的一项,pages数组中的每一项都是一个page结构体。

再page_init的最后会调用init_memmap函数,我们直接看传进去的参数就行

在这里插入图片描述
第一个参数是需要管理内存的起始位置,第二个是一共有多少个页

在这里插入图片描述
这里有一个很方便的函数page2ppn()可以显示当前的地址属于哪一个页。

在这里插入图片描述
我们来看输出,显示现在实在第444号页面上,我们可以管理32292个页面,加起来就 前面显示的一共 32736 个页。

for循环中,将base后面所有的page的property设置为0,因为这是后,双向链表里因该只有两项(head节点 和 一个显示有32292个空页面的节点)。

后面的代码就是把第二个节点加入。
在这里插入图片描述
直观的来看,就是上图所示。第二个节点属于第444页面(可分配的起始位置,记住这个位置)

初始化完成之后,就是一个检测算法正确性的check函数了。现在,这个check函数是无法通过的。

5.alloc 和 free

这两个函数是内存管理的关键。
在这里插入图片描述
alloc的逻辑非常简单,先检测剩余的位置够不够,然后顺序的遍历链表,直到找到第一个符合的位置(property > n) 然后,删除原来的节点,在原来的位置上插入新的节点,注意到这里调用的list_add是直接在头节点插入的,所以这里要改。(要保持整个链表是顺序的)
在这里插入图片描述
在这里插入图片描述
删除的逻辑如上图,也是比较清晰的。同样,在最后更新的时候,原来的代码也是没有按顺序直接list_add的,这里是改过的。
在这里插入图片描述
写一个按顺序的插入函数替换掉list_add就可以通过检测了。

想要全部看完代码的逻辑还是挺复杂的。可以用我写好的打印函数。

#include <pmm.h>
void print_freeList(const list_entry_t* freeList){
    const list_entry_t* t = freeList;
    _print_freeList_elm_head(t);
    list_entry_t* next = (t->next);
    while(next != t ){
        _print_freeList_elm(next);
        next = (next->next);
    }
}
void inline _print_freeList_elm_head(list_entry_t *elm){
    cprintf("head:     prev = %08x , next = %08x \n", elm, (*elm).prev, (*elm).next);
}
void inline _print_freeList_elm(list_entry_t *elm){
    struct Page *thisPage = le2page(elm, page_link);
    cprintf("%08x(%d):free = %d , flag = %08x \n", thisPage, page2ppn(thisPage),\
             thisPage->property , thisPage->flags );
}

6.总结

没有gdb太难了…

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值