《操作系统真象还原》调试总结篇
文章目录
- 前言
- 第11章调试
- 我们操作系统目前的内存管理现状
前言
上一章结尾调试还没有完成,本章开始前需要先完成上一章代码的调试。
总的来说,我们的操作系统目前有三大块内容:线程-进程内容、内存管理内容、中断内容。当然这三部分肯定不可能是独立的,线程切换需要时钟中断实现,线程创建需要向内存管理申请内存,等等。我只是暂时分块方便梳理。
第11章调试
分析上章结尾的截图,发现数字一直是0,可能是用户进程没有成功创建,进程通过线程创建,我们先确认线程有无问题。
注释掉创建进程的两行代码,系统运行正常,那么我们的多线程应该没有问题。
调试了一下,发现有这么一个运行结果
这行哨兵是:ASSERT(pthread != NULL);
分析一下,数字不变,说明用户进程没有正常运行。两个线程切换正常,但是在某种情况下,pthread==NULL。process_activate只在schedule中调用过,传进来的参数应该是下一个线程/进程next,说明有一次传参失败。在ab线程切换的时候少打印了一个空格,也需要注意
next的来源是就绪队列的队头,正好之前写过相应的测试函数,我们调用一下,看看什么是怎么个情况
这是一次调试过程,不过无果,贴出来吧
<bochs:2> info tab
cr3: 0x00000021a000
0xbffff000-0xbfffffff -> 0x000008100000-0x000008100fff
0xc0000000-0xc00fffff -> 0x000000000000-0x0000000fffff
0xc0100000-0xc0133fff -> 0x000000200000-0x000000233fff
0xffeff000-0xffefffff -> 0x000000234000-0x000000234fff
0xfff00000-0xffffefff -> 0x000000101000-0x0000001fffff
0xfffff000-0xffffffff -> 0x00000021a000-0x00000021afff
<bochs:3> info gdt
Global Descriptor Table (base=0xc0000903, limit=55):
GDT[0x0000]=??? descriptor hi=0x00000000, lo=0x00000000
GDT[0x0008]=Code segment, base=0x00000000, limit=0xffffffff, Execute-Only, Non-Conforming, Accessed, 32-bit
GDT[0x0010]=Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
GDT[0x0018]=Data segment, base=0xc00b8000, limit=0x00007fff, Read/Write, Accessed
GDT[0x0020]=32-Bit TSS (Busy) at 0xc0006340, length 0x0006b
GDT[0x0028]=Code segment, base=0x00000000, limit=0xffffffff, Execute-Only, Non-Conforming, Accessed, 32-bit
GDT[0x0030]=Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
You can list individual entries with 'info gdt [NUM]' or groups with 'info gdt [NUM] [NUM]'
<bochs:4> sreg
es:0x0033, dh=0x00cff300, dl=0x0000ffff, valid=1Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
cs:0x0008, dh=0x00cf9900, dl=0x0000ffff, valid=1Code segment, base=0x00000000, limit=0xffffffff, Execute-Only, Non-Conforming, Accessed, 32-bit
ss:0x0010, dh=0x00cf9300, dl=0x0000ffff, valid=31Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
ds:0x0033, dh=0x00cff300, dl=0x0000ffff, valid=31Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
fs:0x0033, dh=0x00cff300, dl=0x0000ffff, valid=1Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
gs:0x0018, dh=0xc0c0930b, dl=0x80000007, valid=1Data segment, base=0xc00b8000, limit=0x00007fff, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0020, dh=0xc0808b00, dl=0x6340006b, valid=1
gdtr:base=0xc0000903, limit=0x37
idtr:base=0xc0006180, limit=0x17f
<bochs:5> r
eax: 0xc0101d10 -1072685808
ebx: 0xc0101cfc -1072685828
ecx: 0x00000000 0
edx: 0x00000000 0
esp: 0xc0101cb4 -1072685900
ebp: 0xc0101ccc -1072685876
esi: 0x00000000 0
edi: 0xc0001804 -1073735676
eip: 0xc0002374
eflags 0x00000046: id vip vif ac vm rf nt IOPL=0 of df if tf sf ZF af PF cf
通过上面的信息,分析出了一些疑点:
内存映射相关:是否有内存缺页,cpu是否运行到了没有建立好映射的虚拟地址?用户进程无法正确运行
TSS相关:ESP0是否正确记录?
特权级相关:特权级是否正确切换?我们的cs还在内核态?
调试一上午没有弄出来😡,暂时搁置吧,先刷一下今天的力扣。
2025年5月2日09点18分更新:今天继续调试。给自己设了一个底线,如果今天没有解决这个问题,就先打个补丁暂时略过。
页表信息:
<bochs:17> info tab
cr3: 0x000000218000
0xc0000000-0xc00fffff -> 0x000000000000-0x0000000fffff
0xc0100000-0xc0133fff -> 0x000000200000-0x000000233fff
0xfff00000-0xffffefff -> 0x000000101000-0x0000001fffff
0xfffff000-0xffffffff -> 0x000000218000-0x000000218fff
观察第5行。我们初始的页表结构物理地址基址是0x00100000,这是内核页表结构。我们先创建了一个用户进程,打上断点,发现此时cr3指向0x000000218000,页目录表位置是0x000000218000-0x000000218fff,说明我们给用户进程创建了新的页表结构。说明创建新的内存页的功能正常。
说一下最后发现的问题吧,在loader的库文件boot.inc(是的,第四章的一个小小的参数的不完备)里,有一行宏定义:
PG_US_U equ 100b ;最开始设置的000b,这是修改后的
PG_US_S equ 100b
当时设置的参数错误,花费了我两天时间去调试,其中心酸一言难尽啊。
18点58分,调试成功,运行结果截图:
多的话也不说了,总之就是很累,还好最后问题解决了!!!!!
我们操作系统目前的内存管理现状
简略回顾寻址:
cr3存页目录物理基址,前10位(0xffc00000)用来寻址页目录表
pde页目录项,前20位(0xfffff000)用来寻址页表
pde页表项,前20位用来寻址具体的页
虚拟地址取低12位和pde寻得的页结合,得到最后的物理地址。
先说物理地址:我们的操作系统内核都在物理内存0-4mb(第0个页表覆盖范围,4mb=0x400000),准确的说,在物理内存0-1mb(第0个页表覆盖范围,1mb=0x100000)。
二级页表的结构:一张页目录表在最低地址,后面跟着1k个页表,每个页表1k项。对于我们的系统,那张页目录表的起始位置(基址)是物理地址0x00100000。
在加载内核之前,我们在loader中开启分页模式之前,我们创建了两个页目录项(pde),这两项是页目录表第0项和第768项,它们都指向同一个地址,这个地址就是页目录基址+4000(0x1000),代表的是第0个页表的起始地址。
为什么是0和768?总的而言,我们的页表的排序标准是按虚拟地址从低到高的,虚拟地址0-4gb对应0-1kb页表,也就是0-1k页目录项。
所以,在第0项pde中,记录的物理地址是第0个页表的物理地址,最大可以对应实际的0-4mb物理内存。记录的最大可以对应的虚拟地址是0x00000000-0x003fffff,也就是0-4mb。
同样,768项,记录的物理地址是第0个页表的物理地址,最大可以对应实际的0-4mb物理内存。只是记录的最大可以对应的虚拟地址是0xc0000000-0xc03fffff,也就是3gb–3gb+4mb。
然后,我们额外处理了一个特殊的页目录项:1023项,我们使用被称为递归映射的方法,让页目录表的1023项指向页目录表本身的物理基址。
但是,我们创建的页目录项对应了页表,页表却不一定创建了1k个页表项(pte,用来完整映射4mb内存)。观察我们的loader.s,发现我们随后创建了256个项(覆盖1mb的内内存),现在是:0和768的pde->(不完整的)第0页表->0-1mb物理内存。
然后loader.s创建了第769项-1022项(共256-1-1=254)个页目录项,希望它们对应254个页表,再进一步对应虚拟的3gb-4gb地址(然而实际上没有创建这些页表的页目录,因而后面的对应不成立)。
接着我们开启了内存分页,因而后面所有的内存(地址)默认情况下都是在说虚拟内存。但是需要注意,到现在为止,我们只实现了非常非常短的物理-虚拟映射,事实上,在我们开启分页后,只有0-0x000fffff和0xc0000000-0xc00fffff能完整的走完虚拟-物理完整寻址过程。剩下的虚拟地址无法找到对应的物理地址,出现缺页错误(pf)。
接下来到了我们memory.c文件,第一次编写是在开启内核多线程之前,核心问题是解决为新开辟的线程申请内存的问题。
在这之前我们引入了位图这个结构来管理内存,位图唯一的作用就是指示某段资源是否被使用**(被使用是1,没被使用是0)**。目前我们用它来管理内存(可以管理物理内存或虚拟内存)。我们定义了两个结构体:pool(用来管理物理内存?),virtual_addr(用来管理虚拟内存),它们内部都包含bitmap。
vaddr_get:从用户/内核pool申请虚拟内存页,返回虚拟地址
pde_ptr:得到某个虚拟地址的页目录项指针
pte_ptr:得到某个虚拟地址的页表项指针
palloc:从某个物理内存池申请一页物理内存页,返回物理地址
page_table_add:建立一个物理地址和一个虚拟地址的映射关系,即最终实现虚拟地址-页目录项-页表项-物理地址的关系构建。
malloc_page:完整的分配页过程,包括申请虚拟页,申请物理页,将二者建立映射。最终返回起始虚拟地址
get_kernel_pages:对malloc_page的调用,代表从内核内存池申请内存。这个函数会在创建线程时调用。
以上的函数,目前实现给内核线程分配内存。这部分涉及到的页表还都是内核页表,也就是我们在loader里实现的1mb页表。
然后我们进入到用户进程的研究领域。对于用户进程,有两个主要的问题,一个是特权级切换与任务调度切换的问题,一个是用户进程内存管理的问题。我们这部分研究内存问题。
用户进程的内存管理相比于内核线程,主要是多了一个页表结构问题。相关代码在memory、process、tss三个源文件里面。
memory.c是辅助,下面是用户进程新增的函数。
get_user_page:用户内存池中申请地址。
get_a_page:同样是关联物理地址和虚拟地址,对象是目前运行的进线程。内部区分了内核线程和用户进程,调用了page_table_add。
addr_v2p:获取虚拟地址对应的物理地址。
process.c是核心,内部包含内联汇编,最终完成创建页表的功能。下面是内部函数。
start_process:第一个重要函数。作用就是构建一个用户进程。方法是先完整的初始化一个中断栈,然后通过内联汇编,让cpu执行一次iretw中断返回,这样从0特权级来到3特权级,把这个中断栈中的数据压入到了cpu里面。
其实这个函数严格来说不属于内存管理部分,属于是任务切换部分,我这里顺带提一下。
简单复习内联汇编:基本格式是
指令:输出操作数列表(储存汇编执行后得到的结果):输入操作数列表(储存给汇编的条件):破坏的内容。另外涉及到约束和修饰符,不再一一介绍。
page_dir_activate:第二个重要函数。这个是正儿八经的内存管理相关函数,作用是激活页表。同样用了内联汇编,更新cr3寄存器的值。
process_activate:完成激活页表和更新esp。激活页表调用上面的page_dir_activate,更新esp调用update_tss_esp
所谓的激活页表,严格来说是更新cr3寄存器的值,确保了有页目录表基址。但是整个页表结构还没有建立好。
create_page_dir:这个函数用来实际创建用户进程页表结构中的页目录结构,包括三个过程。首先第一步,先从内核内存池?申请一页的内存,这页内存存用户进程的页目录表。然后第二步,把目前页表的页目录表的后256项复制到新申请的内存位置,也就是将新的页表的后1gb虚拟内存映射到内核空间。最后第三步,获取新的页表的页目录基址并返回,并且安排好新的页表的递归映射。
目前,用户进程页表结构中,cr3和页目录表安排的比较合理了,但是页表这个层级是否还不完善?还只有1个页目录项的1个页表的256个页,对应1mb的内存空间。
(如果我没有猜错的话,我们整个操作系统,包括内核和用户进程,应该只会在0-1mb的物理内存上进行。)
内存相关就说这么多吧,文字没有经过润色,病句不少,格式也比较混乱,大家将就着看吧。本来我计划整个操作系统写完后再写总结的,结果为了调试代码提前写了内存部分的总结,那就发出来给大家看看。
户进程页表结构中,cr3和页目录表安排的比较合理了,但是页表这个层级是否还不完善?还只有1个页目录项的1个页表的256个页,对应1mb的内存空间。
(如果我没有猜错的话,我们整个操作系统,包括内核和用户进程,应该只会在0-1mb的物理内存上进行。)
内存相关就说这么多吧,文字没有经过润色,病句不少,格式也比较混乱,大家将就着看吧。本来我计划整个操作系统写完后再写总结的,结果为了调试代码提前写了内存部分的总结,那就发出来给大家看看。