0CCh Blog

PIO读IDE

经过各种代码的东拼西凑、改来改去,总算是把PIO读取硬盘信息的代码“写”好了,上图是读取硬盘的前512字节的效果图。目前看来还是很挫,原因有两点:

1.只支持LBA48的读取方法,不支持CHS,LBA28,虽然这两个方法的读取范围很有限,但是感觉至少要把LBA28给支持了才行。
2.很郁闷的一点,这个读取代码读取成功了,但是IO后返回的状态值是错误的。不知道哪里出了问题,会不会是虚拟硬盘太小而不能用LBA48的问题呢?没有头绪。


补充1.通过IDENTIFY DEVICE命令发现,可能由于设置的虚拟硬盘比较小的原因,虚拟硬盘不支持48bit的地址。IDENTIFY DEVICE会通过PIO方式返回一个256字(512字节)的数据。其中第83个字的第10位表示是否支持48bit的地址。如下图(来自ATA官方手册AT Attachment with Packet Interface - 6)。

补充2.由于不支持LBA48,我还是实现了LBA28。不过这个只能访问128G的硬盘了。至于CHS目前还是不考虑实现。
补充3.PIO写的方式大概也是差不多的。准备慢慢实现,还有DMA读写硬盘也需要了解下。不过好消息是现在基本能看懂ATA的手册了。
补充4.MiniKernel的代码依然写得很挫,暂时不准备共享出来,因为共享出来也没啥用,想学写系统的也看不懂那种烂代码。
补充5.感觉读写硬盘是一个挺有意思地方,完全可以单独拿出来写一个系列的blog。只是有没有时间和懒不懒的问题。
补充6.补充5的最后一句是P话,时间肯定是有的,就是懒而已。。。

一个月一篇文章。。。多一点都没有。。。我果然是个需要被监督的人。。。

总结2011,展望2012

2011还是过去了,2012来了。觉得有点必要写点东西,总结一下过去,展望一下未来。如果要用几个词来总结我的2011,那应该是天真,失望,浮躁。可以说我对我的2011是比较不满意的。

天真,我自认为自己还算是个性情中人,所以很多事情对我来说就是讲得就是个胃口。我是很愿意把结识的人当作朋友处。但是,在工作中,有些“朋友”确实是建立在利益基础上的。如果把所有说过“我们是朋友”的人当作朋友,很有可能吃亏是自己。工作也是一样,代入太多感情色彩也是很天真的一种做法。为了讲胃口,有时候退一步,多干一些活。做的好当然没事,但是做的不尽如人意有时候给你带来的真的会是麻烦。

失望,在去年对一些人失望了,对工作的事情也失望,对自己也挺失望的。有些事情自己也不想拿出来说,也不想以后看到在想起,能快点忘记就忘记。但是对自己失望需要深刻的自我剖析检讨了。技术上提高真的不大,基本上都看不出自己做成了什么厉害的事情,决定的事情大部分没有坚持下来,空余的时间大部分花在娱乐上。

浮躁,一整年,都是浮躁的。买的书一本一本的增多,耐心看完的,甚至说看了一大半的都没几本。想学习的东西很多很多,但是没有一个耐心去学习的。给自己开的代码项目很多,也没见过几个写成的。做事情的思路大概是这样的:哎哟,这个东西挺好玩,去实现一个呗;恩,找点资料吧;我靠,资料不是很多嘛;晕,环境怎么这么难搭建;耐心点,慢慢来;好,环境搭建好了,可以开始了;哎,细节问题好多啊,一个人写这个真的大丈夫么;妈的,确实很难写,比想的难好多啊;不行了,弄不下去了;哎哟,弄这个意义大么;意义不是很多大吧,哎,还是弄点别的吧……

所以我觉得,我的2011基本上就是失败的。但是,我的字典里面没有后悔,因为后悔不能改变任何事情。而且不也不会激励自己明年一定要怎么样。因为貌似这种自我暗示已经被我免疫了。当然如果从过得怎么样的角度来看,我的2011还是相当精彩和快乐的。只不过说,这个人有点贱,快乐的事情总是记不住。

虽然说我现在已经不喜欢那些所谓的立志大湿,但是还是应该给自己一个2012的展望。怎么说呢?继续浮躁吧,想做的事情很多很多。

1.写完自己的mini kernel。

2.看完几本书,包括:nt 文件系统的后半部分,编译原理,算法导论。说实话,我还是感觉自己能看完其中两本就算不错了。

3.完善自己的BaseLib,加上自己实现各种算法。

4.实现一个简单的脚本语言。

5.最后多看两眼wrk吧。

看吧,我真的很浮躁很浮躁,如果明年的这个时候的总结(当然,前提是别2012-12-21就结束了),其中有三条圆满完成,我就觉得很奇迹了。

最后,无论怎么说,2011已经过去,过去的事情无论好坏都过去了。期待2012自己的改变吧。祝福我的家人,朋友和我自己,新年快乐,健康平安,家庭和睦温馨。

我本来不想写kernel,直到我的膝盖中了一箭

最近中箭体很火,我也凑个热闹。话说自从delete那篇文章过后,又有一个多月没写了。其实不是不想写,是不知道些什么才好。简单的东西不想写,难的东西写不出来。

正像标题写的,恩,我开始写kernel玩了。其实写一个简单,功能单一的kernel并不难。麻烦的只是搭建环境等等。kernel的编写资料也很多,但是可惜的是,绝大部分都是应用在linux环境。我是那种看到linux就头晕的人。所以还是坚持用windows和vc来开发kernel。令人惊喜的是grub能够帮助我们map kernel到内存中,所以boot loader这一步可以想放下。等kernel写了个大概再来写boot loader也不迟。

环境和工具:
环境正如我上面提到了windows xp 和 vs 2008。其他工具包括winimage,virtual pc,bochsdbg(+ IDA)。当然还有grub4dos。

要高效的起步,先要了解mulitBoot的一些知识。还有就是写一套能够在text mode下打印信息的函数,例如printf。这样在不用调试的情况下,就能了解一些信息。说实在的bochsdbg的调试功能真心不好用,但是加上IDA可能是一个比较好的做法。具备以上条件后,就可以开始kernel之旅了。

可以看出内存的基本状况已经可以从boot_info中获取了。接下来要做的事情也很明了。就是需要一个物理内存管理器,实现最基本的物理内存管理器也不算难,不过那就是下篇文章的事情了。现在的kernel大小为7168字节,慢慢玩,看我能坚持多久。

为什么必须用delete[]释放数组

最近不知哪来的好奇心,对C++产生了一些兴趣,要知道我通常情况下都是cpp文件中写c代码,c代码中嵌汇编。不过,在做了一些大点的项目之后,确确实实发现了,这种编码方式麻烦的一面。有时候甚至自己都难得维护以前写的东西。所以选择性看了google的c++编程规范,而且对scoped_ptr和auto_ptr的区别参数兴趣。然后我得出的结论是这两种实际上是其实差不多,只不过scoped_ptr拷贝构造函数和赋值构造函数都是私有的。这样就避免粗心大意的程序员调用他。其他的区别还真没看出来。

OK,这些都不是记录这篇tip的重点。重点在于boost,或者说是google的scoped_ptr代码里面实现了scoped_array。而要用在数组上使用智能指针,就必须用数组的智能指针类。而auto_ptr刚好没有数组部分,所以对于数组,就不能用auto_ptr了。(为什么不用vector?这也不是重点)

作为一个蹩脚的C++程序员,我这时候开始犯晕了。我们知道只能指针都是帮助程序员去释放资源,让程序员把精力放到更重要的地方。那么在我看来那么所需要做的就是析构的时候 delete 或者 delete[] 就行了。要知道,delete[] 就是调用的delete,他们只是单纯的释放内存。那么数组和非数组又有什么区别?

光想肯定不行,写两个例子。

首先是new 一个char数组,分别用delete和delete[]释放。结果表明,没有任何问题,而且不会产生内存泄露。坑爹么?NO,还没完,其实咱们最怀疑的一直都是数组对象,因为他们都有构造和析构函数。而char这样的系统内建类型,想象得出不会出什么问题。new一个对象数组,分别用delete和delete[] 释放。果然问题暴露了,delete的时候出了问题。

知其然,不足以满足好奇心。下面才是拿手的,精彩的要放在后面嘛。

先看测试代码

#include

using namespace std;

class A {

public:
A() {cout << "start 1" << endl;}
~A() {cout << "End 1" << endl;}
};

int main()
{
A *a = new A[10]();

delete[] a;
return 0;
}

编译后看到代码如下

0040103D push 0Eh
0040103F call operator new[] (403ED0h)

注意到这里传入的大小时0Eh,也就是说申请分配14个字节大小的内存。但是我们知道C++标准中空类的大小应该是1字节。那么多出的dword我们就很容易想到他的用处,应该是记录数组大小的。事实也确实如此。

mov dword ptr [eax],0Ah

这里明确交代,给分配内存的第一个dword传入10。

00401069 push offset A::~A (4011D0h)
0040106E push offset A::A (401120h)
00401073 push 0Ah
00401075 push 1
00401077 mov ecx,dword ptr [ebp-0F8h]
0040107D add ecx,4
00401080 push ecx
00401081 call `eh vector constructor iterator' (40A010h)

这里是调用构造函数,注意是vector版本的构造函数。参数分别是数组的this指针,sizeof(A),数组数量,构造和析构函数。这样,在这个函数内部循环10次调用构造函数,构造完毕。

004010BD mov eax,dword ptr [ebp-14h]
004010C0 mov dword ptr [ebp-0E0h],eax
004010C6 mov ecx,dword ptr [ebp-0E0h]
004010CC mov dword ptr [ebp-0ECh],ecx
004010D2 cmp dword ptr [ebp-0ECh],0
004010D9 je main+0F0h (4010F0h)
004010DB push 3
004010DD mov ecx,dword ptr [ebp-0ECh]
004010E3 call A::`vector deleting destructor' (401230h)

这里是析构部分传入数组的this指针,传入flag(3),调用析构函数。

继续看call之后的代码

00401253 mov eax,dword ptr [ebp+8]
00401256 and eax,2
00401259 je A::`vector deleting destructor'+61h (401291h)
0040125B push offset A::~A (4011D0h)
00401260 mov eax,dword ptr [this]
00401263 mov ecx,dword ptr [eax-4]
00401266 push ecx
00401267 push 1
00401269 mov edx,dword ptr [this]
0040126C push edx
0040126D call `eh vector destructor iterator' (40A920h)
00401272 mov eax,dword ptr [ebp+8]

如果flag中位1是set,那么调用eh_vector_destructor_iterator调用每个析构函数。参数分别是this,sizeof(A),数组个数(这里很明显是从eax-4中拿出来的)以及析构函数地址。

OK,明白了delete[]的做法,我们看看delete为什么失败。

004010BD mov eax,dword ptr [ebp-14h]
004010C0 mov dword ptr [ebp-0E0h],eax
004010C6 mov ecx,dword ptr [ebp-0E0h]
004010CC mov dword ptr [ebp-0ECh],ecx
004010D2 cmp dword ptr [ebp-0ECh],0
004010D9 je main+0F0h (4010F0h)
004010DB push 1
004010DD mov ecx,dword ptr [ebp-0ECh]
004010E3 call A::`scalar deleting destructor' (4012D0h)

这里调用的析构函数都不一样是一个scalar版本的函数。

继续看这个函数的关键部分

004012F3 mov ecx,dword ptr [this]
004012F6 call A::~A (4011D0h)
004012FB mov eax,dword ptr [ebp+8]
004012FE and eax,1
00401301 je A::`scalar deleting destructor'+3Fh (40130Fh)
00401303 mov eax,dword ptr [this]
00401306 push eax
00401307 call operator delete (40A890h)

这里很清楚的看到,只进行一次析构,然后就释放内存。所以我们看到的现象是只调用一次析构函数。那么为什么会崩溃呢?因为delete错了地址。看上面的对比的值,eax-4才是new返回的地址,所以delete的不应该是eax,而是eax-4。

真相大白?NO,还有一个问题,delete[] 和delete 内建类型真的成功了么?
看看我贴出的代码吧,这里不解释了。

;delete[] version
0040101E push 0Ah
00401020 call operator new[] (4014C0h)
00401025 add esp,4
00401028 mov dword ptr [ebp-0E0h],eax
0040102E mov eax,dword ptr [ebp-0E0h]
00401034 mov dword ptr [a],eax
00401037 mov eax,dword ptr [a]
0040103A mov dword ptr [ebp-0D4h],eax
00401040 mov ecx,dword ptr [ebp-0D4h]
00401046 push ecx
00401047 call operator delete[] (401600h) `

;delete version
0040101E push 0Ah
00401020 call operator new[] (4014C0h)
00401025 add esp,4
00401028 mov dword ptr [ebp-0E0h],eax
0040102E mov eax,dword ptr [ebp-0E0h]
00401034 mov dword ptr [a],eax
00401037 mov eax,dword ptr [a]
0040103A mov dword ptr [ebp-0D4h],eax
00401040 mov ecx,dword ptr [ebp-0D4h]
00401046 push ecx
00401047 call operator delete (401600h)

就像我刚刚所说的delete[]会调用delete。所以不会出任何问题。

如果汇编看的头疼的话,这里我写了两个函数的逆向代码(说了精彩的应该放在后面的):

void A::'scalar deleting destructor'(unsigned int flags)
{
this->~A();
if (flags & 1) {
A::operator delete(this);
}

void A::'vector deleting destructor'(unsigned int flags)
{
if (flags & 2) {
count = *(int *)((int)this - 4);
'eh vector destructor iterator'(this, sizeof(A), count, A::~A);
if (flags&1) {
A::operator delete((void *)((int)this - 4));
}
}
else {
this->~A();
if (flags & 1) {
A::operator delete(this);
}
}
};

分析到这,终于明白。数组一定要用delete[]释放才安全。所以千万不要用auto_ptr作为数组的智能指针,不然会死的很惨。这里还要提一点,auto_ptr也不要用到容器里面去了,也是不允许的。非要这么做就用shared_ptr吧, C++0x已经在stl中加入的这部分。从vs2008 sp1开始支持。低版本的vs的话就去boost里面找吧。

Volume snapshot

上个月说了,准备放一份基于卷磁盘快照代码。拖到现在也没有更新了,就把它放出来,留着也没啥用。本来就是为了做一个演示demo,没考虑效率和稳定性,只是提供一种思路,其实思路也很简单,有人已经把他完善的很好了。感觉博客更新确实慢,但是也没什么想写的,其实也写不出来什么。悲剧啊!

和我其他代码一样,这么代码同样没有注释。需要的讲究读读吧。那啥,最近昨天把google的代码规范看了看,确实有很多值得学习的地方。不过呢,有些地方可能个性使然,不太认同。不过风格的问题,其实也没什么。

最近还写了hive文件读解析的代码,还没完善,然后又转去看系统缓存那部分的东西了。有时间完善完善。还要完善了是自己写的一个ini文件解析的类。哎,都是一些自己造轮子的活。不过还挺有意思。

下载:SectorMon

NTSTATUS Lookup

磁盘快照写好了后,闲着无聊写了个nslookup,用来看驱动返回值解释的。写这个程序还先写了个nsstatus.h的解析工具。生成了一个超大的switch case。没啥技术含量。至于那个磁盘快照的代码,过段时间如果合适也可以共享出来。

1.0.0.2 更新:

1.增加程序初始化时,直接读取剪切板中的数据功能。
2.增加对输入的判断,支持“0x”前缀。

下载:nslookup

一份简陋的NTFS Parse代码

研究了近两周的NTFS文件格式,初步了解了一些NTFS基本属性的解析方法。
为了自己研究方便,写了点简单的解析代码。发出来以方便需要的人。
话说这份代码的解析类只是属于demo阶段,而且没有注释。不过代码写的还算清晰。

参考资料方面,我是转了一份NTFS 3G的文档,这份文档把NTFS的基本特点写的比较清晰了。值得一看

http://0cch.net/ntfsdoc/

代码:NtfsParse

ReactOS-Freeldr磁盘及文件管理2

ArcOpen的大体流程我们看过了。大致分为这几步

  1. 函数会尝试找到文件所在分区的设备句柄,如果还没有对应的句柄。那么使用DEVICE.FuncTable中的Open函数打开设备,并为这个设备分配句柄。

  2. 打开设备后条用XxxMount识别分区格式,识别成功返回另外的FuncTable,存储到设备的FileData.FileFuncTable域。

  3. 为文件分配一个句柄,在对应的FileData.DeviceId为上面创建设备句柄,FileData.FuncTable为设备的FileData.FileFuncTable。

  4. 最后调用文件的FileData.FuncTable.Open函数打开文件。

挂载分区时做了什么

之前我们忽略了XxxMount函数。现在来读读比较简单的FatMount (freeldr\freeldr\fs\fat.c)。

  1. const DEVVTBL***** FatMount**(ULONG DeviceId)**

  2. {

  3. .......****.

  4. // 生成一个FAT_VOLUME_INFO结构

  5. Volume = MmHeapAlloc**(sizeof(FAT_VOLUME_INFO));**

  6. if (****!Volume)

  7. return NULL;

  8. RtlZeroMemory(Volume, sizeof(FAT_VOLUME_INFO));

  9. // 读第一个扇区

  10. Position**.HighPart = 0;**

  11. Position**.LowPart = 0;**

  12. ret = ArcSeek**(*DeviceId, &Position, SeekAbsolute);*

  13. if (ret !****= ESUCCESS)

  14. {

  15. MmHeapFree**(Volume)****;**

  16. return NULL;

  17. }

  18. ret = ArcRead**(DeviceId, Buffer, sizeof(Buffer)**, &Count)****;

  19. if (ret !****= ESUCCESS |****| Count !****= sizeof(Buffer)****)

  20. {

  21. MmHeapFree**(Volume)****;**

  22. return NULL;

  23. }

  24. // 判断是否有fat分区标志

  25. if (!RtlEqualMemory(BootSector->FileSystemType, “FAT12 “, 8) &****&

  26. !RtlEqualMemory(BootSector-****>FileSystemType, “FAT16 “, 8) &****&

  27. !RtlEqualMemory(BootSector32-****>FileSystemType, “FAT32 “, 8) &****&

  28. !RtlEqualMemory(BootSectorX->FileSystemType, “FATX”, 4))

  29. {

  30. MmHeapFree**(Volume)****;**

  31. return NULL;

  32. }

  33. // 获得分区大小等信息

  34. ret = ArcGetFileInformation**(*DeviceId, &FileInformation);*

  35. if (ret !****= ESUCCESS)

  36. {

  37. MmHeapFree**(Volume)****;**

  38. return NULL;

  39. }

  40. SectorCount**.HighPart = FileInformation.EndingAddress.HighPart;**

  41. SectorCount**.LowPart = FileInformation.EndingAddress.LowPart;**

  42. SectorCount**.QuadPart /****= SECTOR_SIZE;**

  43. Volume**-****>DeviceId = DeviceId;**

  44. // 打开分区

  45. if (!FatOpenVolume(Volume, BootSector, SectorCount.QuadPart))

  46. {

  47. MmHeapFree**(Volume)****;**

  48. return NULL;

  49. }

  50. // 存储FAT_VOLUME_INFO结构

  51. FatVolumes**[DeviceId]** = Volume**;**

  52. // 返回fat文件读写的FuncTable

  53. return &FatFuncTable;

  54. }

  55. **
    **

函数中的DeviceId是设备的句柄。

生成FAT_VOLUME_INFO结构。这个结构里面存储了FAT分区的基本信息。包括扇区大小,每个簇的扇区数等等。

  1. typedef struct _FAT_VOLUME_INFO

  2. {

  3. ULONG BytesPerSector**;** /* Number of bytes per sector */

  4. ULONG SectorsPerCluster**;** /* Number of sectors per cluster */

  5. ULONG FatSectorStart**;** /* Starting sector of 1st FAT table */

  6. ULONG ActiveFatSectorStart**;** /* Starting sector of active FAT table */

  7. ULONG NumberOfFats**;** /* Number of FAT tables */

  8. ULONG SectorsPerFat**;** /* Sectors per FAT table */

  9. ULONG RootDirSectorStart**;** /* Starting sector of the root directory (non-fat32) */

  10. ULONG RootDirSectors**;** /* Number of sectors of the root directory (non-fat32) */

  11. ULONG RootDirStartCluster**;** /* Starting cluster number of the root directory (fat32 only) */

  12. ULONG DataSectorStart**;** /* Starting sector of the data area */

  13. ULONG FatType**;** /* FAT12, FAT16, FAT32, FATX16 or FATX32 */

  14. ULONG DeviceId**;**

  15. } FAT_VOLUME_INFO**;**

  16. **
    **

读取第一个山区,判断是否有fat标志。如果没有直接返回,挂载失败。之后使用ArcGetFileInformation获得分区大小。ArcGetFileInformation里面调用了FileData.FuncTable.GetFileInformation。因为当前DeviceId是设备句柄,所以他实际调用的是DiskGetFileInformation(freeldr\freeldr\arch\i386\hardware.c)。这个函数很简单,通过FileInformation返回分区开始和结束的地址,这里就不列出了。

这里的代码用FileInformation**.**EndingAddress / SECTOR_SIZE计算出了该分区的扇区数SectorCount。这里应该BUG。因为EndingAddress是分区结束地址,真的扇区数应该是 (分区开始地址 - EndingAddress ) / SECTOR_SIZE。好在SectorCount只是判断fat分区的一个依据,而且一般C盘计算出的SectorCount误差不会很大,影响不大。

最后执行FatOpenVolume真正执行分区的挂载、初始化。初始化结束后将生成的Volume放到fat.c维护的全局数组FatVolumes里,之后对fat分区进行操作(读写)时,通过设备的DeviceId就可以找到对应的FAT_VOLUME_INFO结构。

最后函数返回FatFuncTable函数数组

  1. const DEVVTBL FatFuncTable =

  2. {

  3. FatClose,

  4. FatGetFileInformation,

  5. FatOpen,

  6. FatRead,

  7. FatSeek,

  8. L”fastfat”,

  9. }****;

用户可以通过这些函数就读写改fat分区啦。

那么FatOpenVolume都干了什么呢。

这个函数简单来说就是根据分区内容填写了Volume结构,已经算是一个分区的具体实现细节了,和整体架构无关,不多说了。这个函数在freeldr\freeldr\fs\fat.c中。

打开文件时做了什么

上一篇文章中还有一个地方没说,就是打开设备并创建完文件的句柄后,ArcOpen调用了文件对应的FileData.FuncTable.Open。对于fat分区而言这个函数是FatOpen(freeldr\freeldr\fs\fat.c). 这个函数也是和分区结构有关的了,有一点比较重要就是函数最后调用了FsSetDeviceSpecific把一个和文件相关的内部结构与文件句柄相关联。以后使用FatRead对文件句柄进行读操作时直接就可以获得这个结构啦。

  1. LONG FatOpen**(CHAR*** Path, OPENMODE OpenMode, ULONG***** FileId**)**

  2. {

  3. .....****.

  4. // 根据文件的FileId获得文件所在的设备句柄FileData.DeviceId, 从而获得FatMount时生成的Volume结构。

  5. DeviceId = FsGetDeviceId**(FileId);*

  6. FatVolume = FatVolumes**[DeviceId]****;**

  7. // 从DeviceId设备中读取并查询fat表,判断path表示的文件是否存在

  8. RtlZeroMemory(&TempFileInfo, sizeof(TempFileInfo)****);

  9. ret = FatLookupFile**(*FatVolume, Path, DeviceId, &TempFileInfo);*

  10. if (ret !****= ESUCCESS)

  11. return ENOENT**;**

  12. // 判断是否是目录

  13. IsDirectory = (TempFileInfo.Attributes & ATTR_DIRECTORY) !****= 0**;**

  14. if (IsDirectory &****& OpenMode !****= OpenDirectory)

  15. return EISDIR**;**

  16. else if (**!*IsDirectory &&* OpenMode !=** OpenReadOnly**)

  17. return ENOTDIR**;**

  18. // 生成FAT_FILE_INFO结构,里面存放了文件的信息(开始的扇区等)

  19. FileHandle = MmHeapAlloc**(sizeof(FAT_FILE_INFO));**

  20. if (****!FileHandle)

  21. return ENOMEM**;**

  22. RtlCopyMemory**(*FileHandle, &TempFileInfo, sizeof(FAT_FILE_INFO)****);*

  23. FileHandle**-****>Volume = FatVolume;**

  24. // 把这个结构和文件对应的FileData.Specific关联。之后进行FatRead等操作时可以直接获得这个结构了

  25. FsSetDeviceSpecific**(FileId, FileHandle);*

  26. return ESUCCESS**;**

  27. }

ReactOS-Freeldr磁盘及文件管理

Freeldr提供了对fat12、fat32、fatx、ntfs等文件系统的只读功能。这部分代码主要集中在boot\freeldr\freeldr\fs\fs.c文件中。
首先计算机加电后会把mbr读取到物理内存的0x7c00位置,mbr搜索活动分区并加载活动分区根目录下的Freeldr.sys文件。加载后跳入Freeldr入口start。Freeldr进行32为初始化后跳入主初始化函数BootMain(boot\freeldr\freeldr\Freeldr.c)中。

VOID BootMain(LPSTR CmdLine)
{
......
MachInit(CmdLine);
FsInit();
......
RunLoader();
}

BootMain会对硬件(MachInit)和文件系统(FsInit)进行检测和初始化。所有准备工作进行完毕后就会调用RunLoader进行系统的加载工作。
Fs初始化和DEVICE、FILEDATA结构
下面看一下文件系统的初始化 FsInit(boot\freeldr\freeldr\fs\fs.c)

VOID FsInit(VOID)
{
ULONG i;
RtlZeroMemory(FileData, sizeof(FileData));
for (i = 0; i < MAX_FDS; i++)
FileData[i].DeviceId = (ULONG)-1;
InitializeListHead(&DeviceListHead);
}

FsInit初始化FileData数组。和一个和磁盘分区相关的链表DeviceListHead。
首先fs.c维护了一个MAX_FDS(60)大小的数组 static FILEDATA FileData[MAX_FDS];

typedef struct tagDEVVTBL
{
ARC_CLOSE Close;
ARC_GET_FILE_INFORMATION GetFileInformation;
ARC_OPEN Open;
ARC_READ Read;
ARC_SEEK Seek;
LPCWSTR ServiceName;
} DEVVTBL;
typedef struct tagFILEDATA
{
ULONG DeviceId; // 文件所在磁盘的磁盘文件句柄, 同样也是FileData的索引
ULONG ReferenceCount; // 引用计数
const DEVVTBL* FuncTable; // 对文件进行读写的指针
const DEVVTBL* FileFuncTable; // 对文件进行读写的函数数组
VOID* Specific; // 文件系统自定义指针
} FILEDATA;

每一个成功打开的文件会返回一个文件句柄,这个句柄实际上就是FileData数组的索引。所以每个打开的文件都有一个对应的FileData。这个结构就类似windows中的FILE_OBJECT
FileData中DeviceId是文件所在磁盘的句柄。这个句柄同样也是FileData数组的索引,通过这个句柄可以找到”磁盘文件”,对”磁盘文件”的读写就是直接对相应的磁盘或磁盘分区的读写。类似Windows中直接对磁盘分区进行CreateFile返回的句柄。”磁盘文件”的DeviceId没有意义。
ReferenceCount是该文件的引用计数。
FuncTable这是一个函数数组指针,里面存放了对文件进行读写、SEEK等操作的函数指针。
FileFuncTable只对”磁盘文件”有意义。当Freeldr确定了磁盘文件对应的分区的分区格式后,会把与分区格式相关的函数指针数组放到这个字段里面。如Fat12分区”磁盘文件”的FileFuncTable字段存放的就是FatFuncTable指针。
Specific存放于文件有关的结构。磁盘文件就是DISKCONTEXT指针,fat12下的文件就是FAT_FILE_INFO指针 等等。
之后是DeviceListHead,这是DEVICE结构的链表头

typedef struct tagDEVICE
{
LIST_ENTRY ListEntry; // 链表节点
const DEVVTBL* FuncTable; // 操作该分区的函数表
CHAR* Prefix; // 分区对应的ArcName
ULONG DeviceId; // FILEDATA中该分区对应的句柄
ULONG ReferenceCount; // 引用计数
} DEVICE;

用户电脑中的每一个硬盘和硬盘中的每一分区都对应了一个DEVICE结构。
FuncTable里面存放了对该分区进行读写等操作的指针,对于硬盘而言这个数组就是DiskVtbl。
Prefix是该分区或硬盘的ArcName。(如multi(0)disk(0)rdisk(0)partition(0))。Freeldr中的文件路径都是Arc形式的路径。而且0号分区代表整个硬盘,真正的分区从1号开始。如multi(0)disk(0)rdisk(0)partition(0)便代表第0块硬盘本身。multi(0)disk(0)rdisk(0)partition(1)代表第0块硬盘的第0个分区。
通过DeviceId字段可以找到该DEVICE的文件句柄。这个字段和FILEDATA相互配合,使系统可以遍历DEVICE结构快速找到某个分区的文件句柄。
DEVICE(磁盘及分区)的检测
上面说到Freeldr操作的路径都是存储在DEVICE结构中的ArcPath。那么这些DEVICE是怎么来的呢?
首先我们看一下DEVICE的注册函数,FsRegisterDevice(boot\freeldr\freeldr\fs\fs.c)

VOID FsRegisterDevice(CHAR* Prefix, const DEVVTBL* FuncTable)
{
DEVICE* pNewEntry;
ULONG dwLength;
dwLength = strlen(Prefix) + 1;
pNewEntry = MmHeapAlloc(sizeof(DEVICE) + dwLength);
if (!pNewEntry)
return;
pNewEntry->FuncTable = FuncTable;
pNewEntry->ReferenceCount = 0;
pNewEntry->Prefix = (CHAR*)(pNewEntry + 1);
memcpy(pNewEntry->Prefix, Prefix, dwLength);
InsertHeadList(&DeviceListHead, &pNewEntry->ListEntry);
}

这么函数非常简单。Prefix就是Arc路径,FuncTable是操作这个分区(磁盘)对应的函数数组。FsRegisterDevice生成了一个DEVICE结构,把ArcName和FuncTable复制进去。之后连入了DeviceListHead链表。
那么又是谁调用的FsRegisterDevice呢?是DetectBiosDisks(boot\freeldr\freeldr\arch\i386\hardware.c)函数。虽然这一部分已经不属于FS的范畴,还是在这里简单讲一下便于理解。这里我略去了不必要的代码。
DetectBiosDisks的调用顺序是 RunLoader -> MachHwDetect (PcHwDetect)-> DetectISABios -> DetectBiosDisks

static VOID
DetectBiosDisks(PCONFIGURATION_COMPONENT_DATA BusKey)
{
BOOLEAN BootDriveReported = FALSE;
ULONG i;
ULONG DiskCount = GetDiskCount(BusKey);
CHAR BootPath[512];
......
for (i = 0; i < DiskCount; i++)
{
ULONG Size;
CHAR Identifier[20];
......
if (BootDrive == 0x80 + i)
BootDriveReported = TRUE;
/* Get disk values */
GetHarddiskIdentifier(Identifier, 0x80 + i);
}
}

首先使用GetDiskCount从Freeldr注册表的System键中读取硬盘总数。System键的初始化在DetectSystem(freeldr\freeldr\arch\i386\hardware.c)中,一会儿再看。
于是进入一个for循环,为每个硬盘调用GetHarddiskIdentifier函数。在BIOS中硬盘号是从0x80开始的,所以GetHarddiskIdentifier的硬盘号加了0x80。
GetHarddiskIdentifier的作用是为制定硬盘生成一个唯一的ID,并通过Identifier参数返回。但这个函数名起得并不好,因为生成ID其实只是这个函数的功能之一。另外的一大功能是检测硬盘,并且为硬盘本身和硬盘分区调用FsRegisterDevice函数进行注册。通过这个注册后硬盘才能真正被文件系统识别。
freeldr\freeldr\arch\i386\hardware.c中

static VOID
GetHarddiskIdentifier(PCHAR Identifier,
ULONG DriveNumber)
{
PMASTER_BOOT_RECORD Mbr;
ULONG *Buffer;
ULONG i;
ULONG Checksum;
ULONG Signature;
CHAR ArcName[256];
PARTITION_TABLE_ENTRY PartitionTableEntry;
/* Read the MBR */
if (!MachDiskReadLogicalSectors(DriveNumber, 0ULL, 1, (PVOID)DISKREADBUFFER))
{
DPRINTM(DPRINT_HWDETECT, "Reading MBR failed\n");
return;
}
Buffer = (ULONG*)DISKREADBUFFER;
Mbr = (PMASTER_BOOT_RECORD)DISKREADBUFFER;
Signature = Mbr->Signature;
DPRINTM(DPRINT_HWDETECT, "Signature: %x\n", Signature);
/* Calculate the MBR checksum */
Checksum = 0;
for (i = 0; i < 128; i++)
{
Checksum += Buffer[i];
}
Checksum = ~Checksum + 1;
DPRINTM(DPRINT_HWDETECT, "Checksum: %x\n", Checksum);
/* Fill out the ARC disk block */
reactos_arc_disk_info[reactos_disk_count].Signature = Signature;
reactos_arc_disk_info[reactos_disk_count].CheckSum = Checksum;
sprintf(ArcName, "multi(0)disk(0)rdisk(%lu)", reactos_disk_count);
strcpy(reactos_arc_strings[reactos_disk_count], ArcName);
reactos_arc_disk_info[reactos_disk_count].ArcName =
reactos_arc_strings[reactos_disk_count];
reactos_disk_count++;
sprintf(ArcName, "multi(0)disk(0)rdisk(%lu)partition(0)", DriveNumber - 0x80);
FsRegisterDevice(ArcName, &DiskVtbl);
/* Add partitions */
i = 1;
DiskReportError(FALSE);
while (DiskGetPartitionEntry(DriveNumber, i, &PartitionTableEntry))
{
if (PartitionTableEntry.SystemIndicator != PARTITION_ENTRY_UNUSED)
{
sprintf(ArcName, "multi(0)disk(0)rdisk(%lu)partition(%lu)", DriveNumber - 0x80, i);
FsRegisterDevice(ArcName, &DiskVtbl);
}
i++;
}
DiskReportError(TRUE);
/* Convert checksum and signature to identifier string */
Identifier[0] = Hex[(Checksum >> 28) & 0x0F];
Identifier[1] = Hex[(Checksum >> 24) & 0x0F];
Identifier[2] = Hex[(Checksum >> 20) & 0x0F];
Identifier[3] = Hex[(Checksum >> 16) & 0x0F];
Identifier[4] = Hex[(Checksum >> 12) & 0x0F];
Identifier[5] = Hex[(Checksum >> 8 ) & 0x0F];
Identifier[6] = Hex[(Checksum >> 4) & 0x0F];
Identifier[7] = Hex[Checksum & 0x0F];
Identifier[8] = '-';
Identifier[9] = Hex[(Signature >> 28) & 0x0F];
Identifier[10] = Hex[(Signature >> 24) & 0x0F];
Identifier[11] = Hex[(Signature >> 20) & 0x0F];
Identifier[12] = Hex[(Signature >> 16) & 0x0F];
Identifier[13] = Hex[(Signature >> 12) & 0x0F];
Identifier[14] = Hex[(Signature >> 8 ) & 0x0F];
Identifier[15] = Hex[(Signature >> 4) & 0x0F];
Identifier[16] = Hex[Signature & 0x0F];
Identifier[17] = '-';
Identifier[18] = 'A';
Identifier[19] = 0;
}

函数首先使用MachDiskReadLogicalSectors读取指定硬盘的MBR。对于PC机而言MachDiskReadLogicalSectors使用int 13h中断实现对硬盘的读操作。里面包括了16、32位代码的互转,和本节内容无关,以后再做说明。
MBR结构为。详细信息可以参考(http://en.wikipedia.org/wiki/Master_boot_record)

typedef struct _MASTER_BOOT_RECORD
{
UCHAR MasterBootRecordCodeAndData[0x1b8]; /* 0x000 */
ULONG Signature; /* 0x1B8 */
USHORT Reserved; /* 0x1BC */
PARTITION_TABLE_ENTRY PartitionTable[4]; /* 0x1BE */
USHORT MasterBootRecordMagic; /* 0x1FE */
} MASTER_BOOT_RECORD, *PMASTER_BOOT_RECORD;

GetHarddiskIdentifier在获取了Signature、计算了Checksum后 。
sprintf(ArcName, “multi(0)disk(0)rdisk(%lu)partition(0)”, DriveNumber - 0x80);
FsRegisterDevice(ArcName, &DiskVtbl);
生成对应硬盘的ArcName,使用FsRegisterDevice注册这块硬盘,这个函数我们已经看过。注意这里Partition为0,所以0号分区实际表示硬盘本身。
之后

i = 1;
while (DiskGetPartitionEntry(DriveNumber, i, &PartitionTableEntry))
{
if (PartitionTableEntry.SystemIndicator != PARTITION_ENTRY_UNUSED)
{
sprintf(ArcName, "multi(0)disk(0)rdisk(%lu)partition(%lu)", DriveNumber - 0x80, i);
FsRegisterDevice(ArcName, &DiskVtbl);
}
i++;
}

DiskGetParititionEntry将会解析DriveNumber对应磁盘的分区表,填充第i个分区的信息到PartitionTableEntry结构。如果分区存在则使用FsRegisterDevice注册分区。
使用刚才计算的CheckSum和Signature组合一个ID返回给调用者。其实这个ID没有被使用过。。。
最后看一下调用FsRegisterDevice时的第二个参数DiskVtbl

static const DEVVTBL DiskVtbl = {
DiskClose,
DiskGetFileInformation,
DiskOpen,
DiskRead,
DiskSeek,
};

这里面包含了对磁盘扇区读写的全部函数。我们之后再介绍。
至此硬盘的及硬盘分区的注册完成。
执行完DetectBiosDisks后,DeviceListHead里面就存放了当前计算机所有的磁盘和分区对应的DEVICE结构。
文件系统的识别和文件的打开
这时Fs模块已经知道的硬盘数量,分区信息。下面来看看一个文件的打开流程。
首先,Freeldr使用的是Arc路径,IDE硬盘以multi(0)disk(0)rdisk(n)开头,文件也是以Arc路径表示的。打开文件的函数在Freeldr\Freeldr\fs\fs.c中。这函数比较长,我们分段阅读。
Freeldr\Freeldr\fs\fs.c

LONG ArcOpen(CHAR* Path, OPENMODE OpenMode, ULONG* FileId)
{
......
*FileId = MAX_FDS;
/* Search last ')', which delimits device and path */
FileName = strrchr(Path, ')');
if (!FileName)
return EINVAL;
FileName++;
/* Count number of "()", which needs to be replaced by "(0)" */
dwCount = 0;
for (p = Path; p != FileName; p++)
if (*p == '(' && *(p + 1) == ')')
dwCount++;
/* Duplicate device name, and replace "()" by "(0)" (if required) */
dwLength = FileName - Path + dwCount;
if (dwCount != 0)
{
DeviceName = MmHeapAlloc(FileName - Path + dwCount);
if (!DeviceName)
return ENOMEM;
for (p = Path, q = DeviceName; p != FileName; p++)
{
*q++ = *p;
if (*p == '(' && *(p + 1) == ')')
*q++ = '0';
}
}
else
DeviceName = Path;
......

这个函数有三个参数Path是文件名的Arc路径,如multi(0)disk(0)rdisk(0)partition(1)Freeldr.sys就表示C盘中的Freeldr.sys文件。
OpenMode是打开模式(OpenReadOnly、OpenReadWrite等)。
如果打开成功,文件句柄将通过FileId参数返回。
首先这一部分代码分理出Arc磁盘路径中的”()”替换成”(0)”并存入DeviceName中,如multi()disk()rdisk()partition(1)Freeldr.sys处理后,DeviceName将指向multi(0)disk(0)rdisk(0)partition(1)。注意这个DeviceName是不以NULL结尾的。。。这是个很蛋疼的设计。
FileName会指向Arc路径中的文件名部分,上面的例子将是Freeldr.sys。
文件打开分为两步,第一步是开个文件所在的设备、创建设备的句柄。第二部才是打开文件本身。
这里是第一步打开设备的代码。

......
pEntry = DeviceListHead.Flink;
while (pEntry != &DeviceListHead)
{
pDevice = CONTAINING_RECORD(pEntry, DEVICE, ListEntry);
if (strncmp(pDevice->Prefix, DeviceName, dwLength) == 0)
{
/* OK, device found. It is already opened? */
if (pDevice->ReferenceCount == 0)
{
/* Search some room for the device */
for (DeviceId = 0; DeviceId < MAX_FDS; DeviceId++)
if (!FileData[DeviceId].FuncTable)
break;
if (DeviceId == MAX_FDS)
return EMFILE;
/* Try to open the device */
FileData[DeviceId].FuncTable = pDevice->FuncTable;
ret = pDevice->FuncTable->Open(pDevice->Prefix, DeviceOpenMode, &DeviceId);
if (ret != ESUCCESS)
{
FileData[DeviceId].FuncTable = NULL;
return ret;
}
else if (!*FileName)
{
/* Done, caller wanted to open the raw device */
*FileId = DeviceId;
pDevice->ReferenceCount++;
return ESUCCESS;
}
/* Try to detect the file system */
FileData[DeviceId].FileFuncTable = FatMount(DeviceId);
if (!FileData[DeviceId].FileFuncTable)
FileData[DeviceId].FileFuncTable = NtfsMount(DeviceId);
if (!FileData[DeviceId].FileFuncTable)
FileData[DeviceId].FileFuncTable = Ext2Mount(DeviceId);
if (!FileData[DeviceId].FileFuncTable)
{
/* Error, unable to detect file system */
pDevice->FuncTable->Close(DeviceId);
FileData[DeviceId].FuncTable = NULL;
return ENODEV;
}
pDevice->DeviceId = DeviceId;
}
else
{
DeviceId = pDevice->DeviceId;
}
pDevice->ReferenceCount++;
break;
}
pEntry = pEntry->Flink;
}
if (pEntry == &DeviceListHead)
return ENODEV;

一个循环,遍历DEVICE链表,找到DEVICE->Prefix (磁盘、分区的Arc路径,上一节说过)和刚刚分解出来的DeviceName相等的节点。如果没有则函数直接失败。
找到DEVICE节点后判断DEVICE->ReferenceCount是否为0。这个代表该DEVICE被打开的次数,如果ReferenceCount不为0,说明DEVICE已经被打开。那个直接从Device->DeviceId中获得设备的文件句柄。可以看出无论打开一个设备多少次,只会有ReferenceCount的变化,而句柄都是相同的。所以如果设备打开两次,SEEK时会相互影响。读写之前最好重新调用SEEK函数。
当DEVICE->ReferenceCount为0时是Freeldr需要调用进行打开和文件系统的识别。我们仔细读读。

/* Search some room for the device */
for (DeviceId = 0; DeviceId < MAX_FDS; DeviceId++)
if (!FileData[DeviceId].FuncTable)
break;
if (DeviceId == MAX_FDS)
return EMFILE;

首先在FileData数组中找到空闲项,数组的索引即将成为设备句柄。

/* Try to open the device */
FileData[DeviceId].FuncTable = pDevice->FuncTable;
ret = pDevice->FuncTable->Open(pDevice->Prefix, DeviceOpenMode, &DeviceId);
if (ret != ESUCCESS)
{
FileData[DeviceId].FuncTable = NULL;
return ret;
}
else if (!*FileName)
{
/* Done, caller wanted to open the raw device */
*FileId = DeviceId;
pDevice->ReferenceCount++;
return ESUCCESS;
}

之后把DEVICE中存储的设别操作函数数组FuncTable赋值给对应FileData中的FuncTable。之后对该句柄的读写操作将直接传递给FileData.FuncTable中的函数。
调用FuncTable->Open打开设备。上面我们看过这个函数实际是freeldr\freeldr\arch\i386\hardware.c中的DiskOpen。
打开成功后,如果FileName(需要打开的文件名)为空,说明这次请求只打开设备,于是直接返回设备的句柄。
如果不为空,则下面开始识别分区格式,打开文件的操作。
在继续读ArcOpen函数前我们先看看DiskOpen在打开设备时都做了什么。

ArcOpen -> DiskOpen (freeldr\freeldr\arch\i386\hardware.c)
static LONG DiskOpen(CHAR* Path, OPENMODE OpenMode, ULONG* FileId)
{
......
if (!DissectArcPath(Path, FileName, &DriveNumber, &DrivePartition))
return EINVAL;
if (DrivePartition == 0xff)
{
/* This is a CD-ROM device */
SectorSize = 2048;
}
else
{
SectorSize = 512;
}
if (DrivePartition != 0xff && DrivePartition != 0)
{
if (!DiskGetPartitionEntry(DriveNumber, DrivePartition, &PartitionTableEntry))
return EINVAL;
SectorOffset = PartitionTableEntry.SectorCountBeforePartition;
SectorCount = PartitionTableEntry.PartitionSectorCount;
}
Context = MmHeapAlloc(sizeof(DISKCONTEXT));
if (!Context)
return ENOMEM;
Context->DriveNumber = DriveNumber;
Context->SectorSize = SectorSize;
Context->SectorOffset = SectorOffset;
Context->SectorCount = SectorCount;
Context->SectorNumber = 0;
FsSetDeviceSpecific(*FileId, Context);
return ESUCCESS;
}

这个函数非常简单,使用DissectArcPath根据设备的Arc路径分解出文件名FileName、BIOS驱动器号DriveNumber、和分区号DrivePartition(第0个分区的编号是1,0代表整个硬盘)
之后确定扇区大小,分区开始的扇区号、分区扇区数等信息,存入DISKCONTEXT结构。使用FsSetDeviceSpecific和FildId相关联。
还记得FILEDATA的结构么?FsSetDeviceSpecific就是填充里面的Specific指针 :)

VOID FsSetDeviceSpecific(ULONG FileId, VOID* Specific)
{
if (FileId >= MAX_FDS || !FileData[FileId].FuncTable)
return;
FileData[FileId].Specific = Specific;
}

实际上DiskOpen的作用就是获得该设备(分区)的基本信息——BIOS驱动器号、扇区大小、开始扇区号、扇区数量和当前读写指针(SectorNumber)。生成DISKCONTENT结构使用FsSetDeviceSpecific和FileID绑定。
现在我们回到ArcOpen函数,希望你还记得 :)

/* Try to detect the file system */
FileData[DeviceId].FileFuncTable = FatMount(DeviceId);
if (!FileData[DeviceId].FileFuncTable)
FileData[DeviceId].FileFuncTable = NtfsMount(DeviceId);
if (!FileData[DeviceId].FileFuncTable)
FileData[DeviceId].FileFuncTable = Ext2Mount(DeviceId);
if (!FileData[DeviceId].FileFuncTable)
{
/* Error, unable to detect file system */
pDevice->FuncTable->Close(DeviceId);
FileData[DeviceId].FuncTable = NULL;
return ENODEV;
}
pDevice->DeviceId = DeviceId;

现在FileData[DeviceId]已经代表刚刚打开的设备了,开始挂载分区。啥叫挂载分区,就是让文件系统提供个接口,能让我们操作分区里面的文件。而这个接口就是个DEVVTBL指针,和直接操作硬盘的接口一样,只不过这次这个可以操作文件了。如果分区识别成功,XxxMount函数将会返回另外一个DEVVTBL指针数组,这个指针赋值给设备对象的FileFuncTable成员。使用这个指针数组就可以在文件级别操作了。比如打开freeldr.sys文件就可以调用FileData[DeviceId].FileFuncTable->open函数。FileFuncTable和FuncTable是不同的哦! :)
注意FileFuncTable其实是不直接使用的,这个指针的作用是为之后打开的文件对应的FileData.FileTable赋值。于是操作文件和操作磁盘都是用对应的FileData.FileTable,实现形式上的统一。而且这种架构还可以轻易的实现将一个文件虚拟成为一个分区,只要为文件对象调用XxxMount并且给FileFuncTable域赋值就可以了,非常易于扩展。Freeldr并没有实现这种功能,文件的FileData.FileFuncTable没有使用~
下面我们就来看看第二步,打开文件
首先为文件找一个空闲的FileData

for (i = 0; i < MAX_FDS; i++)
if (!FileData[i].FuncTable)
break;
if (i == MAX_FDS)
return EMFILE;

跳过文件名开始的 “" 字符

if (*FileName == '\\')
FileName++;

我们前面说的,为FileData.FuncTable赋值。FileData.DeviceId是文件所在分区的句柄。FuncTable内部函数将通过这个句柄调用读写分区内容,为用户提供文件的读写接口。

FileData[i].FuncTable = FileData[DeviceId].FileFuncTable;
FileData[i].DeviceId = DeviceId;
*FileId = i;
ret = FileData[i].FuncTable->Open(FileName, OpenMode, FileId);
if (ret != ESUCCESS)
{
FileData[i].FuncTable = NULL;
*FileId = MAX_FDS;
}

至此打开文件的操作结束。这里略去了XxxMount和文件的Open函数。以后再说。 :)

关于文件ShareAccess

我是真心太懒了,虽然平时也在研究一些东西,但是总是理解了就算了,没有把他们记录下来的想法。虽然不记录下来也不至于会忘记,但是人的记忆总是有限的我也不敢保证记忆完全不错。好不容易说服自己写点东西,就从今天看的那点东西写起吧。

(吐槽:我尽量把以后的文章写得详细以至于啰嗦,免得以后自己又看不懂了。)

什么是ShareAccess。我们做一个简单的实验,进入系统目录(一般就是C:\Windows)。在C:\Windows\system32\config中,复制一个SYSTEM文件,然后把文件粘贴到另一个地方。如果我们的系统正常,那么我们看到肯定是一个错误框。(图1)“无法复制 system: 文件正在被另一个人或程序使用。关闭任何可能使用这个文件的程序,重新试一次。”无论懂不懂编程,这样一个令人蛋疼的错误框应该会看过无数次吧。这里我就不解释Windows为什么要这么做,假设所有读者都是了解其中的原因了。这篇文章想介绍的是,Windows怎么做到“访问拒绝”的。简单的来说就是当一个进程打开该文件的时候ShareAccess中没有ShareRead属性,所以其他的进程无法访问他。

(图1)

在我们平时打开文件中(CreateFile)总是需要我们传入一个dwShareMode的参数。它有三个值分别是FILE_SHARE_DELETE,FILE_SHARE_READ,FILE_SHARE_WRITE。如果一个打开一个文件的时候,没有传入了FILE_SHARE_READ,那么如果有另一段代码对文件用FILE_READ_DATA权限打开的时候一定返回的是一个失败。其他两个SHARE也是一样。那么是不是设置了FILE_SHARE_READ,其他代码用FILE_READ_DATA权限打开该文件都会成功呢?答案是不一定,主要要看在这段代码CreateFile的dwShareMode。如果也设置的FILE_SHARE_READ,那么打开文件就会成功,否则返回一个SHARE错误。

(吐槽:上面说了一堆,还是没进入正题,貌似有点太详细了。接下来才是重头戏。)

来看看NTFS文件系统是怎么来Check权限的。
每个文件打开的时候系统会为文件分配一个FILE_OBJECT(文件对象)。在这里我们主要关注的是以下几个域。

nt!_FILE_OBJECT
...
+0x00c FsContext : Ptr32 Void
...
+0x026 ReadAccess : UChar
+0x027 WriteAccess : UChar
+0x028 DeleteAccess : UChar
+0x029 SharedRead : UChar
+0x02a SharedWrite : UChar
+0x02b SharedDelete : UChar
...

熟悉NTFS文件系统的同学都知道FsContext实际上是对应着一个SCB。SCB的数据结构是未公开的,所以只有逆向或者通过其他途径获得。而这篇文章只需要关注的是SCB的SHARE_ACCESS。SHARE_ACCESS在SCB的0x60的偏移处,这个和NT的SCB有些不同。SHARE_ACCESS的数据结构是这样

typedef struct _SHARE_ACCESS {
ULONG OpenCount;
ULONG Readers;
ULONG Writers;
ULONG Deleters;
ULONG SharedRead;
ULONG SharedWrite;
ULONG SharedDelete;
} SHARE_ACCESS, *PSHARE_ACCESS;

这个就是这篇文章的关键。

当一个文件被打开的时候,系统会初始化这个数据结构。根据CreateFile的权限设置来填充这个结构。
比如DesiredAccess中设置了FILE_READ_DATA,那么Readers,OpenCount就会增加1,如果在此同时设置了ShareMode为FILE_SHARE_READ,那么SharedRead也会加1。同时FILE_OBJECT的ReadAccess和SharedRead会被设置为TRUE。那么在文件被关闭的时候,如果FILE_OBJECT的ReadAccess和SharedRead为TRUE,那么SHARE_ACCESS的Readers,OpenCount,SharedRead就会减1。

在进程准备去打开一个已经打开的文件时,文件系统会做一系列的检查,包括文件权限(比如如果是只读文件,你却想要写权限,这样就会失败),安全描述符,以及共享权限(ShareAccess)。假设前面两个都符合要求,那么就到了共享权限的检查了。

还是以刚才那个SYSTEM文件为例,他打开的权限是FILE_READ_DATA,FILE_WRITE_DATA,DELETE。那么SHARE_ACCESS的OpenCount,Readers,Writers,Deleters都为1,而完全没有Share的意图,所以其他的域都是0。

当有另外一段代码去试图用FILE_READ_DATA权限打开这个文件的时候,那么文件系统就会去检查第一个打开这个文件的操作共享权限。这时的OpenCount是1,SharedRead是0,他会发现SharedRead小于OpenCount,那么他认为这个文件并没有SHARE_READ,所以参数检查返回失败,你会得到一个共享错误。这就是为什么我们复制粘贴SYSTEM文件的时候会失败。

原因分析到这里就结束了。但是我就这样满足了么?显然我没那么容易满足滴~

我想做的就是复制出这个SYSTEM文件,实际上网上已经有很多做法,什么底层磁盘解析读取数据,句柄复制大法。而我这次是修改底层SCB的ShareAccess来达到复制的目的。如果读懂了上面的原理,看下面这段代码就很轻松了。

kfile File;
ns = File.Create(FILENAME, FILE_OPEN, FILE_READ_ATTRIBUTES, 0);
FileObj = File.GetObject();
ShareAccess = (SHARE_ACCESS *)((ULONG)FileObj->FsContext + Offset);
ShareAccess->SharedRead = ShareAccess->Readers;
File.Release();

OK,编写好测试代码,生成一个驱动。运行即可。接下来就是见证奇迹的时刻了。还是用同样的方法复制看看,完全没有问题了。(图2)

(图2)

(吐槽:好久没写这么长的文章,写的我都崩溃了。说到写文章,我发现现在我如果拿起笔去写字,经常会发生提笔忘字的情况!!!天啊!!!)