-
-
[原创]浅谈自己对Reactos内核设计的感悟
-
发表于:
2016-9-7 17:21
7060
-
个人认为,学习内核不像学习其他技术,如果把技术和古代的武功相比,那么内核则更像是内功心法,它不重招式(也就是内核是怎么实现的),只重其意(内核到底要做些什么)。
自己接触内核是今年年初,当时是自己想做windows驱动开发方向,驱动的开发其实就是用内核给设计的接口,用这些接口完成你想做的事情就可以了。但是重点就是这些接口也太多了,有的时候的有些接口的资料很少。后来看到了《windows内核设计思想》这本书接触到了Reactos这个开源的系统,自己就开始了内核的学习上,个人的意见就是不要看最新的源码,要看最旧的源码,原因是新的源码太庞大,不适合初学者;新的源码考虑的东西也越多,很容易让初学者胡子眉毛一把抓,分不清到底那些是内核的筋骨,那些谁内核的皮肉。废话不多说,正文开始了。
我看的源码是0017的版本,我能找的最低的版本,我下载了源码和它们编译好的二进制文件;我用SourceInsigh来处理源码之间的调用的问题,对于具体的源码用Notepad++来看,Notepad++有一个好处就是能把if,while,for等结构折叠起来,这样能看到总体的思想。然后下载个bochs的freedos文件(在linux下用mount挂载),把二进制文件拷到freedos的镜像盘里(刚开始的版本只能现在dos系统下加载和启动的),开始动态调试,不过bochs的只有汇编语言,所以用ida加载内核文件ntoskrnl.exe就能看到他对一些CALL函数命名和一些变量名,就不会在汇编代码中迷失了。
loader阶段想动态调试的话,在bochs写个命令b 0x2faf:0x100就可断在loader第一条指令了(com程序在我这个freedos系统中第一条指令就是这个地址),loader主要工作就是从实模式转到保护模式,加载内核文件ntsokrnl.exe到物理内存0x200000处,加载boot.bat中的其他文件,计算物理内存有多少,然后把参数填写到PLOADER_PARAMETER_BLOCK这个结构中,填,然后跳转到ntoskrnel(0x201000)内核中,在这个版本的内核执行的过程中并不是从main函数执行而是先从head.s文件执行的主要做的就是开启虚拟内存管理,开启分页机制,分页机制把虚拟内存地址0xc0000000映射到物理地址0x200000处,对于内存的分页机制,多动态调试一下,然后在看点资料就能明白,主要是调试一下,然后在调到ke目录下的main.c文件中的main函数。内核开始工作了。
1.物理内存的管理的设计:
内核管理物理内存是用MM_STATS来记录有多少物理页的(一页4k),每页对应一个PHYSICAL_PAGE结构,在ntoskrnl文件后面连续的放置,比如我的虚拟机设置的物理内存为32M那么就有32*1024/4=8192个这样的PHYSICAL_PAGE结构连续放置。结构中还有一个双链表,初始化会按照设计要求把这些物理页插入到不同的链表中管理,已使用的物理页放到一个链表中,未使用的物理页放到一个链表中等等。虚拟内存管理其实就很好理解了,只要把想要分配的虚拟地址的页表中放一个“从未使用链表”中脱下来的一个PHYSICAL_PAGE,然后计算这个PHYSICAL_PAGE结构在数组中的偏移再乘上4k(每页大小)。这是最简单的物理内存和虚拟内存管理方案。
2.对象的设计:
在Windows中很多的设计都是以对象为基础的.而且个人也很佩服它们巧妙的设计,可以把C语言使用的很像C++;对象在这个版本中有三部分组成,1.对象头(OBJECT_HEADER),2对象类型(OBJECT_TYPE),3.对象(任意的结构体,比如线程的结构体,进程的结构体)。
在对象头的结构体里有两个重要的数据,一个是指向对象类型的指针,另一是记录对象被引用的次数。对象类型里面其实也是有两个重要的数据,一个是这个对象的大小,另一个就是一些函数指针(和C++中对象的属性对应)。比如我要生成一个进程对象。首先我要生成一个进程的对象类型,然后填充一些数据,比如PsProcessType->NonpagedPoolCharge = sizeof(EPROCESS),PsProcessType->Delete = PiDeleteProcess;,PsProcessType->Create = NULL;,PsProcessType->Close = NULL;对象类型有很多,我为什么只写这几个是原因的,首先NonpagedPoolCharge和对象头的大小加起来才是一个完整对象的大小,Delete函数指针其实就是C++中的析构函数(在DereferenceObject中被调用),Create函数指向对象的构造函数(在CreateObject中被调用),而Close函数则是在调用CloseHandle函数是被调用。我们在调用ReferenceObject函数会让对象头中的引用计数加一,调用DereferenceObject会让引用计数减一当引用计数为零是调用Delete函数,不同的对象需要处理的内容不一样这样内核只要设置不同的函数指针来处理就可以。CloseHandle函数中也会调用DereferenceObject这个函数。和对象可以在内核中暴露但不可以在应用层暴露这样就有了句柄的概念,在进程的结构体中就有句柄表的结构,其实最简单的句柄其实就是我在这个进程是第几个被生成的。生成的数字就可以被当作句柄。通过在句柄表中循环到这个数字也就找到这个对象了。
3.进程和线程的设计:
进程和线程其实都可以理解成一个结构体,只不过每个结构体中记录的内容和要做的事情不同而已。进程主要是保证自己的进程的虚拟内存(我们用的堆是在这里记录的)和一些资源的记录,像是上面说的句柄。在reactos中线程是任务调度的最小单位,任务切换好几次可能也还是在这个进程中。线程记录的内容也比较小,我们程序用的栈是在这里记录的,那我们常用CreateThead为例,其中我们有一个函数指针,就会放到线程里。因为这样线程调度的是时候会执行这个函数。还有一下同步的记录。下面我想主要说一下线程是如何切换的和线程的创建。当我们双击一个可执行文件是其实就是在创建进程和线程的,先创建进程在创建进程。会为进程分配出一个栈空间来此时的栈空间还有处理一下,要和任务切换函数在切换进程的页目录表后,函数在执行栈操作的平衡,后面主要是pop操作。栈是从高向低变化的所以线程记录的栈是申请的最高地址。所以要减去几个地址,在任务切换函数执行到ret是此时的栈的内容要是我们在创建线程时添加的函数指针。任务切换程序主要都是在时钟中断处理程序中被调用,还有一部分是在WaitForSingleObject中被调用。如图所示
创建的线程和进程都会分别插入到特定的链表里,每个线程都有特权级,每个特权级都一个链表,在特权级链表的线程会得到调度。内核中还有一个记录所有线程的链表,它面的有可以调度的还有不会被执行的。是通过线程里的CurrentThread->Tcb.State这个标志的。
4.系统同步和异步的设计:
Reactos内核中既有同步的机制也有异步的机制,异步的机制很好说那就是APC,同步的机制有EVENT,MUTEX,SEMAPHORE,SPIN其中spin自旋锁其实就是一个循环其它线程设置了这个标志,循环结束线程继续执行,而其他几个则是通过线程的调度来实现的,如果是要等待的线程则不把它加入到调度的链表了,他就不会在执行,而另一个线程完成后则设置让其加入到调度链表里,重新得到调度。其中如果是不想让它一直等下去则是通过设置一个定时器来实现的,时间一到就会把它加入到调度的链表中。定时器包含两个结构一个是PKTIMER另一个是DPC结构,PKTIMER结构会被插入到全局时间序列中。每当时钟中断程序调用KiUpdateSystemTime时会将全局时间序列中的PKTIMER结构中的时间来比较,大于等于的PKTIMER结构,会把其中的DPC结构插入到全局DPC序列中,系统会创建一个叫IDLE线程来遍历全局DPC结构中的函数,
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!