当前位置: 首页 > news >正文

《操作系统真象还原》调试总结篇

文章目录

    • 前言
    • 第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的物理内存上进行。)

内存相关就说这么多吧,文字没有经过润色,病句不少,格式也比较混乱,大家将就着看吧。本来我计划整个操作系统写完后再写总结的,结果为了调试代码提前写了内存部分的总结,那就发出来给大家看看。

相关文章:

  • B站Michale_ee——ESP32_IDF SDK——FreeRTOS_8 消息缓冲区
  • javascript交换值最好三种
  • 计算机网络——客户端/服务端,URI与URL的区别,以及TCP/IP核心机制全解析
  • (36)VTK C++开发示例 ---纹理贴图四边形
  • 【大模型实战篇】对Qwen3提到的thinking和no thinking混合思考模式的讨论
  • Manus AI多语言手写识别技术解析
  • PostgreSQL 的 VACUUM 与 VACUUM FULL 详解
  • 【git】获取特定分支和所有分支
  • 【Linux深入浅出】之全连接队列及抓包介绍
  • 阿里云服务器防御是怎么做出来的?服务器攻击方式有几种?
  • Java文件上传
  • 【算法基础】选择排序算法 - JAVA
  • ARM 指令集(ubuntu环境学习)第六章:ARM 编程技巧与优化策略
  • 供应链算法整理(一)--- 销量预估
  • 如何掌握 Lustre/Scade 同步数据流语言
  • 基于建造者模式的信号量与理解建造者模式
  • 每日算法-250502
  • Python爬虫实战:获取好大夫在线各专业全国医院排行榜数据并分析,为患者就医做参考
  • 传统银行服务和 区块链支付无缝融合的一种解决方案
  • 【AI面试准备】数据治理与GDPR脱敏机制构建
  • 申活观察|人潮涌动成常态,豫园为何常来常新?
  • 澳大利亚联邦选举投票正式开始
  • 著名医学翻译家王贤才逝世,享年91岁
  • 五一期间全国高速日均流量6200万辆,同比增长8.1%
  • 居委业委居民群策群力,7位一级演员来到上海一小区唱戏
  • 应急管理部派出工作组赴山西太原小区爆炸现场指导救援处置