Windows XP 开机预读取过程

        开机预读这个功能是Windows Xp的之后加入的目的是为了加快开机速度改善用户体验,微软不光对技术,对产品方面也是非常用心的。为什么预读就能改加快开机速度呢?其实一个开机过程中所做的操作就是将需要的数据读入内存中并初始化环境,没有太大量的CPU运算属于IO密集性的过程。其中硬盘(机械盘)操作是最浪费时间的(有人说过访问硬盘的时间相对于访问内存的时间可以认为是无限大,单看磁盘的寻道,数据的读取都是非常可观的),这也是为什么换一个SSD,开机时间立刻减半。

        假想一下,我们同步的读取数据并用来初始化环境,数据要读半天这不是耽误事吗?而且数据都分布在硬盘各个地方,读的操作也是上万次。如果我们事先把这些数据用异步IO把它们都读出来,等到我们真正去读的时候发现它们已经在缓存里了,这样能节省多少等待的时间。

        Windows的预读过程的实现在WRK里是没开源的,只有零星几个数据结构声明和函数的调用。我们不得不用IDAWindbg结合着来逆向分析ntoskrnl.exe

        在Windows启动的中期即初始化完成BOOT驱动后会创建一个线程进行并发的预读,我们用户直观感觉是XP滚动条刚开始滚,这个过程发生在IoInitSystem函数中,在WRK里面可以看到显示的函数调用(函数实现就没公开了):


        CcPfBeginBootPhase会创建新线程去执行CcPfBootWorker,它来负责整个预读取的任务。注:此时文件系统都已初始化完毕。推荐大家看一下潘爱民老师的《Windows内核原理与实现》2.6章节,看完会对Windows的启动过程有一个整体的概念和把握。

        下面一起来看一下它是怎么预读的,汇编就不列出来了太装B了,文字会好点:

        停!有想法的同学有疑问了,你怎么知道要预读哪些数据呢?操作系统在上一次开机的整个过程中,如果发现缺页中断,就把它偏移与对应的文件 记录下来,记录到C:\Windows\NTOSBOOT-XXXX.pf文件里。

网上有下载的简单PF文件查看器: WinPrefetchView.


我们继续,

---------------------------------------------

Pf文件格式:

文件头:

0x0

DWORD

Version

Pf版本信息

Winxp :0x11Win7,VISTA:0x17

0x4

DWORD

Magic

魔数标识

恒值0x41434353 ('SCCA')

0x8

DWORD

Version2

版本信息

Winxp0xFWin70x11

0xC

DWORD

pfLength

PF文件名长度

0x10

PWCHAR

pfName

PF文件名

最长30个字符(含0

0x4C

DWORD

Hash

文件名的HASH

和真实文件名是对应的

0x50

DWORD

IsBoot

是否是系统启动pf

NTOSBOOT.PF1

0x54

DWORD

offsetSum

Sum表的相对文件偏移

记录着预读取项的综合文件描述

0x58

DWORD

SumEntryCount

Sum中的文件个数

PF中预读取文件个数

0x5C

DWORD

offsetPages

Pages的相对文件偏移

记录着预读取文件页信息

0x60

DWORD

PagesEntryCount

整个PF文件中读取页的数目

预读取不是整个文件而是某些页面

0x64

DWORD

offsetFileNames

文件名表的文件偏移

记录着预读取文件名字

0x68

DWORD

LenFileNames

文件名表的长度

0x6C

DWORD

offsetDirs

元数据表(卷,目录)的文件移

记录着预读取文件可能经过的路径信息

0x70

DWORD

DirsEntryCount

表中大类的个数

说白了就是卷的个数

0x74

DWORD

LengthDirs

元数据表的长度

0x78

FILETIME

ftLastExecuteTime

最近一次执行时间

0x80

DWORD[4]

Observed3

没有用到

0x90

DWORD

ExecutionCount

执行次数

0x94

DWORD

Count

执行循环内次数(1-7)

应该是最近没重新生成期间的次数

汇总表项结构说明(以PF里面预读文件为单位):

0x0

DWORD

PagesIndex

PF页表中的索引

此文件要预读哪些页?这是第一个页的索引

0x4

DWORD

PreCountPages

页表中此文件的页数

Pages表中,相关此文件的页的数目

0x8

DWORD

offsetFileName

标识文件名

在文件名表中的偏移

0xC

DWORD

FileNameLen

文件名长度

WCHAR为单位

0xD

DWORD

FileFlag

文件标识

只使用低两位:00-数据,预读;01-数据,不预读;10-镜像(IMAGE),预读;11-镜像,不预读。

文件页表项结构说明:

0x0

DWORD

nIndex

索引号

0开始,-1代表当前文件结束,但也占一索引数

0x4

DWORD

offset

预读偏移

标记预读文件中的哪些页面需要读取

0x8

DWORD

PageFlag

页属性

只使用低3位,4执行,2读写,1不能访问即不预读

文件名表项(没有索引,长度不定):

WCHAR[1]…..

元数据头结构说明(以下偏移相对元数据头):

0x0

DWORD

offsetVolume

卷名的偏移

UNICODE C字符串

0x4

DWORD

LenVolumeName

卷名的长度

单位是WCHAR

0x8

FTIME

FileTime

文件时间

应该是$MFT时间? 

0x10

DWORD

VolumeSerial

卷序列号

未使用

0x14

DWORD

offsetHardDisk

卷预读块偏移

$MFT预读取,私有接口由NTFS实现。入口:ZwFsControlFile

0x18

DWORD

LenofHardDisk

卷预读块的长度

FSCTL_PERFETCH_FILE

0x1C

DWORD

offsetDir

目录集合的偏移

一堆的目录名字

0x20

DWORD

DirCount

目录个数

0x24

DWORD

Reserved

未使用

HardDisk块格式:

Struct HD {

DWORD Version;

DWORD dwCount;

CHAR [8][1] data;

};

前两个是版本和数量,这个应该是ZwFSControlFile对 FSCTL_PREFETCH_FILE的格式要求。Data是什么我也没搞清,只知道在底层会用位移把它解析成相对$MFT的偏移。

Dir块格式:

Struct DirName {

DWORD dwLength;

WCHAR FileName[];

};

以上文件格式也是通过逆向工程得到的。

------------------------------------------------------------------------

        首先它会打开NTOSBOOT-XXXX.pf文件,把文件内容读取出来。根据标识,和各个表的大小偏移做安全校验。这一点WINDOWS对文件格式化校验的严格。

1. 针对$MFT进行预读取,取出元数据表把里面的数据(卷预读块)分段调用ZwFsControlFile,参数是FSCTL_PERFETCH_FILE。由NTFS来把这些数据解析成相对$MFT的一个个偏移,做预读取最终调用到MmPrefetchPages(这个函数最后还会调用到NtfsFSDRead)。

2. 针对文件系统目录项预读,把元数据表里,的预读取目录遍历一下用NtQueryDirectoryFile+FileNameInformation,读一下。

3. 解析汇总表,进行预读。把Pages表里的一个个偏移和上面一样组装成MmPrefetchPages可以只用的参数:

typedef struct _READ_LIST {

    PFILE_OBJECT FileObject;

    ULONG NumberOfEntries;

    LOGICAL IsImage;

    FILE_SEGMENT_ELEMENT List[ANYSIZE_ARRAY];

} READ_LIST, *PREAD_LIST;(来自WRK

4. 由于MmPrefetchPages参数 READ_LIST.IsImage的限制,预读是分了两到三个阶段。先读镜像文件内存,再读数据文件内存和镜像文件里的数据内存。

        这个是具体Windows的实现步骤,可能觉得会很苦涩,不知道它搞毛~。我们来一一对应着了解一下:

$MFT的预读,只读文件数据就可以了和$MFT有什么关系呢?MFTNTFS文件系统中最重要的一个内容,它记录着每个文件除数据以外的信息(小文件的数据也在MFT里)。

        再假想一下,要读取一个文件C:\Windows\System32\ntdll.dll的内容怎么读?打开C盘吧,得到根目录吧,遍历出Windows目录,从Windows目录项中遍历出System32目录,继续,最后找到ntdll.dll这个文件的MFT项,然后算出偏移进行读取。

        那么上面前两步就是把MFT和目录项里面的内容都先读出来,第一步调用的是公开的接口不公开的参数。翻了一下FAT32的代码它没有处理FSCTL_PREFETCH_FILE请求,可知这一步对FAT32是没用的。第二步是文档化的函数,可以把目录项里面的内容让NTFS先加到自己的缓存里。第三步就是正经的把数据读出来了,系统把pf文件中记录的一个个偏移和对应的文件对象放在一起调用MmPrefetchPages,由于很多偏移是连续的,这个函数会把这些偏移组装合并一下,一个异步IO读取就把它们都读出来了(具体就是发了个异步IRP包并设置一事件给了文件系统,文件系统继续下发)。

        最后从代码上面可以看到微软做的很用心的一点是:在预读的时候它会把着内存的使用量,分阶段读出来的,这个阶段是有一定的优先级的,肯定重要的先预读。由于预读和其它的开机操作是并行的,所以不能出内存峰值不够用的情况。一个循环里不断的与MmAvailablePages比较,所以选择合适的量一批一批的把数据读出来。操作系统还会定期根据统计信息整理pf文件里面的内容把不需要的淘汰掉。

        这样预读取线程就完成自己的任务了,这里讲的是操作系统启动预读,另外还有注册表预读和应用程序预读他们都是分开的,不过原理是一样的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值