首页
社区
课程
招聘
[转帖]ntldr分析
发表于: 2007-11-10 23:52 16355

[转帖]ntldr分析

2007-11-10 23:52
16355
前言:

(我希望你在读这篇文章时不要跳过前言部分,因为有很多需要注意的地方在这里声明。)

分析ntldr可以说历经了三起三落,曾经一度的放弃过,因为开始部分与电脑硬件有很大的关系,由于手头资料的匮乏,所以不得不几次暂停分析工作,最近终于下定决心,将ntldr的代码全部分析出。

该部分的代码分析历时一年多的时间,我不是专家,所以,不当之处再所难免,敬请原谅。

其实,在早期分析ntldr代码时,已经写出了部分的代码分析,只是在后来再看时,觉得十分不妥,于是改变了代码分析的风格,为了更加清晰明了,我将以实际地址进行解说。

我就偏移地址等等的问题在网上查过一些资料,也问过一些朋友,不过都没有答案,所以我在代码分析中注名了是否是动态调试器调试出的地址或指令,例如: bochs:0x00ah等等,由于时间有限,我没有详细的解释在代码中是怎么计算的,(呵呵,其实是不会算,主要是文凭太低所至),希望你能发现其中的规律。

还有重要的一点需要提及,我没有winnt系统,所以bochs调试器调试的是win2k中的ntldr,我不知道winnt中的ntldr和win2k中的ntldr有多大的区别,所以错误的地方肯定是再所难免,而这篇文章也成了两种系统中的ntldr的混合体了。

在正式的介绍代码分析以前,我先简单的介绍一下我的思路,本篇会分三部分列出,而分析的过程也是按照ntldr的执行过程来进行分析,第一部分:介绍ntldr的初始化;第二部分:介绍ntldr的主体部分;第三部分:介绍系统的加载。

在该篇其后同样还有两篇文章《ntldr内存初始化、分配、操作及相关函数分析》、《ntldr磁盘操作及相关函数分析》,我把其中过于冗长的代码罗列其中,你可以对照参看。而关键的结构定义分析我也放在《ntldr关键结构定义分析》当中。

关于参考资料:
我将相关的代码及各个部分的相关参考资料一并压缩为一个包上传,所以不必担心你没有代码或参考。

声明:
如果我将这篇文章公开发表,没有别的意思,我只是希望能有更多的朋友相互交流,由于我的电脑知识完全是自学来的,而且没有启蒙,所以知识面相当的窄,很希望能得到高手的指点,特别是文件系统方面的专家(因为以后要往这个方面发展了)。

第一部分:
再次的分析整理我也不会象以前一样在无关痛痒的地方纠缠,也不会大段的粘贴复制代码,各位可以自行查找相关的资料。

Ntldr的引导部分是由汇编代码完成的,所以,最好有一些汇编的功底,这样阅读起来会更为方便。

其实在Ntldr内部是由startup.com和osloader.exe两部分组成的,只不过在编译的时候将这两部分整合到一起(我想这就是为什么 ntldr没有后缀名的原因吧),ntldr被硬性的加载到固定的位置,他会被obr(系统引导记录)加载到2000:0000的位置,至于为什么要加载到这个位置,我还不太清楚。

如过你手头有代码(代码可以随该篇一起下载),在su.asm中,你会看到,在开始的部分(当然并不包括第一条指令)并没有直接执行ntldr的初始化部分,如果你的文件分区格式fat12或者是fat16的话,那么系统引导记录(OBR)在向startup.com转交控制权时会跳过 startup.com中的第一条指令(jmp RealStart),首先读取ntldr其他部分的代码到内存,然后再执行RealStart部分,如果文件分区格式是fat32又或者是其他的文件分区格式,则没有这一步。至于其他的文件系统格式我不在这里涉及。

jmp RealStart   ;我们来看看真正的ntldr代码的初始化部分
RealStart部分的代码相当的简短,这里是最基础的初始化工作,包括设置各个段寄存器,设置堆栈等等,当然了,他还有一个重要的职责:向SuMain传入参数。
    代码段的起始位置当然是2000:0000,其他的段寄存器是在这里动态的计算得来的
    mov   bx,offset _TEXT:DGROUP ;bochs:mov bx,0x2ac0
    shr   bx,4               ; must be para aligned

    mov   ax,cs             ; get base of code
    add   ax,bx             ; add paragraph offset to data

    mov     ss,ax               ; ints disabled for next instruct
    mov     sp,offset DGROUP:SuStack   ; bochs:mov sp,0x1528

    xor   dh,dh
    push   dx              
    push   ds               
    push   si                  
    push   es              
    push   di         
;
; Make DS point to the paragraph address of DGROUP
;
    mov   ds,ax

    movzx   edx,ax
    shl     edx,4
    add     edx,offset DGROUP:_edata   ;bochs:add edx,0x1dd0
    mov     dword ptr _FileStart,edx   ;bochs:mov [0xcbe],edx
    xor     bp,bp
    movzx   ebp,bp
    movzx   esp,sp
    mov     [saveDS],ds     ;bochs:mov word ptr[0x15bc],ds
call   _SuMain       ;调用SuMain,进行近一步的初始化工作
终于看到C代码了。

SuMain模块在main.c中可以找到,同时,他也是ntldr第一步初始化的主体部分。

VOID SuMain(IN FPVOID BtRootDir,IN FPDISKBPB BtBiosBlock,IN SHORT BtBootDriver)

传入的参数的类型可以参考types.h中对类型的重新定义。

在su.asm中看到了参数的传递:
;Build C stack frame for _SuMain
;
xor dh,dh
push dx   ;pass bootdisk(dl)to main,即BtBootDriver参数,实为驱动器号
push ds   ;segment of bios parameter block
push si   ;offset of bios parameter block ,ds:si即BtBiosBlock参数
push es   ;segment of root directory
push di   ;offset of root directory ,es:di即BtRootDir参数

在来看看SuMain 函数的返回值,该函数没有返回值,直接将控制权交给osLoader。

好了,我们来看看具体的实现:
在SuMain 中首先要做的就是保存fs context info ,以免被覆盖掉
  FsContext.BootDrive = (ULONG)BtBootDrive;
  FsContext.PointerToBPB = MAKE_FLAT_ADDRESS(BtBiosBlock); //在bochs反汇编的// 代码中(也就是win2k下// 的ntldr),我没有找到// 和这条代码相关的指令,// 也就是说在win2k中的// ntldr可能没有这一步
FSCONTEXT_RECORD在types.h中定义,MAKE_FLAT_ADDRESS宏函数在constant.h中定义。

接下来初始化Video SubSystem,用以显示错误和异常:
InitializeVideoSubSystem()   //该函数请参考display.c,在这里我只讲解一下关键的部分
     在参考display.c时,你可能对下面的代码不甚明了:
     BiosArea=(UCHAR_far*)(0x410L);   //这句指令相当于BiosArea=(40:10)
     *BiosArea&=~0x30;
*BiosArea|=0x20;   //在(40:10)处存储的是设备编码表,所以这两句的意思就是将设备
// 编码表的5~4位置为10,他的意思是将初始显示方式置为80列文
// 本方式(认为是彩色图形适配器),如果你还不明白,找一些关于设
// 备编码表的资料,一看便知

让我们回到SuMain中,看看接下来做什么
TurnMotorOff() //该函数参考su.asm
     该函数好象是关掉软盘马达,为从软盘引导做准备
     DriveControlRegister     equ     3f2h   ;软盘控制寄存器
     out dx,al       ;对端口的读写
      对于out命令,更详细的资料我就没有了,如果哪位有的话,能不能给我一份,不胜感激(zl21_spawn@163.com

接着,SuMain调用PatchDiskBaseTable()函数,该函数比较简单,不提。

接下来,SuMain要做的是判断机器的总线类型,调用了两个函数 BtIsEisaSystem()、
BtIsMacSystem()分别判断机器的总线类型是Eisa还是Mac,如果这两种都不是,则默认该机器的总线
类型为ISA.
     在BtIsEisaSystem()函数中是通过比较f000:ffd9处(扩展Bios数据区)是否设置了”ASIE”标志,该标志是在系统加电启动时由bios检测设置的。(参考eisaa.asm)
     在BtIsMcaSystem()函数中是通过调用int 15 0c00并测试es[bx+5]是否为2来判断是否为Mca总线类型。(参考abiosa.asm)
     现在,EISA和MCA系统总线结构已经是过时的了。

接下来是SuMain.c中最重要的部分,获取内存容量,我屏弃了对MCA和EISA系统获取内存的介绍。
     SuMain.c通过调用ConstructMemeoryDescriptors()函数来获取内存容量。
     在该函数中首先初始化“内存描述符链表”,详细的分析可以参照《ntldr内存初始化、分配、操作及相关函数分析》一文中对内存操作模块的理解,我自认为已经是写的很详细了,对于其他两种系统总线类型内存的获取我就不再介绍了,如果你感兴趣可以自行参看相关的代码。

在接下来的Sumain函数中,是一个while{}循环体,我看到注释中对这一段的描述是“搜索内存描述符的低内存”。要做的是确保包含 osLoader的映射,如果你分析过PE头的话,对接下来的代码会很快理解,其实就是在内存描述符链表中寻找能容纳osLoader的内存块,具体的代码可以参照《ntldr内存初始化、分配操作及相关函数分析》。

在循环体之后Sumain函数调用EnableA20()函数,很简单,打开A20地址线,为切换到保护模式做准备。

调用Relocatex86Structure()函数,重新定位x86的结构,包括GDT、IDT、页目录和第一级页表。
在Relocatex86Structure()函数中,我们要注意的是,gdt和idt已经静态的嵌入到SU模块的数据段中。

调用EnableProtectPaging()函数,切换到保护模式并允许分页(参见su.asm)。

调用RelocateLoaderSections()函数重新定位osLoader的位置,并返回其入口点,注意,在调用该函数时,传入的两个参数并没有赋值,这两个参数为全局变量,通过计算后写入。
LoaderEntryPoint = RelocateLoaderSections(&OsLoaderStart, &OsLoaderEnd);
具体的计算我已经在《ntldr内存初始化、分配、操作及相关函数分析》中写的很清楚了。

调用TransferToLoader(LoaderEntryPoint)将控制权交给osLoader.
     该函数我只在这里简单的提两句(写给不太熟悉汇编的朋友):
     mov ebx,dword ptr [ebp+2]   ;指向传入该函数的参数,即osLoader的入口点
     做一些其他的工作,例如装入数据段选择器,堆栈段等等
     push KeCodeSeletor
     push ebx
     retf      ;这样便实现了控制权的转交

OK,第一部分就写到这里,如果你能把上面的代码理解透彻那么恭喜你,不过我没有把重点放在这一部分,因为其中大部分是对硬件的操作,有些地方你可以在下面的参考资料中找到相关的介绍,我把这些资料随同代码部分一起发出。
参考资料:
     代码位置 boot/startup/i386
     《PC技术内幕》该书虽然有些过时,不过其中的部分章节还是蛮有意思的
《IA-32 Intel Architecture Software Developer’s Manual Volume1~Volume3》该书我建议看看第三卷,前两卷可以作为参考手册查询使用
《linux nt获取扩展内存》其中介绍了几种获取内存的方法
《保护模式编程》我最欣赏其中对各个系统寄存器的介绍和保护模式与实模式互切换的代码例程
《对ntldr的反汇编分析》一年多以前写的,不才之作,并不完整。
bochs2.2.5.exe:bochs虚拟机、调试器的安装包。

感想:
看看自己写过的《对ntldr的反汇编分析》,不禁感叹起来,对于现在这个被pdb和nms充斥的世界,我怎么都想象不到当初我是怎么熬过来的,在我还没有得到nt的代码以前,曾经反汇编过ntldr,不过只是初始化部分,后来由于bochs动态调试器的问题搁浅了。本来再没有想过使用bochs的,只是为了在代码的解释中以实际地址进行演算,现在又不得不动用它了。该动态调式器只有两个字来形容“要命”,虽然该调试器可以在win中使用,但我想作者的原意并没有打算应用于win系统中。写完这篇文章后我再也不用bochs了,因为只是安装系统就花了我6个多小时,由于bochs完全的模拟cpu硬件指令,所以速度吗可以想象的到,其实这是bochs最大的缺点同时也是它最大的优点,所谓鱼与熊掌不可兼而得之,bochs还有一个缺点就是不能加载pdb 或者nms,这不免是一个极大的遗憾,如果有可能动手写一个插件最好了,不知道对于这样妄自菲薄的评价 bochs的作者会不会有意见啊,呵呵,其实特殊的工具有特殊的用途,到现在我接触过的虚拟机和软件调试器,除了bochs,我还没有看到有哪一个虚拟机能真正的模拟物理计算机的执行,还没有看到有哪一个软件调试器能在系统加电后就能进行调试的。

第二部分
其实第一部分只是最初步的初始化工作,在这一部分中才进入关键的初始化工作。
此时虽然是属于osLoader,不过他只是进一步的初始化工作。
如果第一部分称之为SU (Start-Up)的话,那么我将这一部分称为SL(Setup-Loader)

这部分的入口点函数是NtProcessStartup()函数,也就是说第一部分的控制权转交到这里。

我不知道是我的错误还是win2k中的ntldr和winnt中的ntldr有所区别,在我使用bochs进行调试的时候,由 TransferToLoader转交控制权到这里,在我看到的bochs反汇编代码中好象并不是直接调用NtProcessStartup()函数,不过我在源代码中并没有找到其他的更为合适的入口点,不过纵览整个代码,似乎也只有这里最为合适,如果你有不同的意见或者更清楚,请联系我(zl21_spawn@163.com

传入的参数是很重要的, BOOT_CONTEXT结构的定义:
     typedef struct _BOOT_CONTEXT
     {
           PFSCONTEXT_RECORD FSContextPointer;
             PEXTERNAL_SERVICES_TABLE ExternalServicesTable;
             PSU_MEMORY_DESCRIPTOR MemoryDescriptorList;
           ULONG MachineType;
             ULONG OsLoaderStart;
             ULONG OsLoaderEnd;
             ULONG ResourceDirectory;
             ULONG ResourceOffset;
             ULONG OsLoaderBase;
             ULONG OsLoaderExports;
}BOOT_CONTEXT, *PBOOT_CONTEXT;
所有重要的信息都在这个结构中了。
这里有一个成员需要注意,ExternalServicesTable(函数指针),在TransferToLoader()函数中也许没有注意到,由于这个成员比较重要,所以我在这里简单的解释一下,在 TransferToLoader()函数中看到了压栈传入参数的动作:
    shl     eax,4
    xor     ecx,ecx
    mov     cx,offset _BootRecord
    add     eax,ecx
    push   eax
ExternalServicesTable的分析参见《ntldr关键结构分析》
在TransferToLoader() 函数中传入的_ExportEntryTable被重新定义为ExternalServicesTable,这样便形成了一一对应的关系。
好了,让我看看该函数具体的实现:
首先调用DoGlobalInitialization()函数。
在注释中解释为“该函数负责调用所有的子系统初始化例程”,不过在我看来,该函数主要是初始化内存子系统。
     InitializeMemorySubsystem()
前面不是已经调用int 15 e820获取内存了吗?注意,不要混淆视听,这里是初始化内存子系统,与前面是两个概念,这个过程比较复杂,我慢慢的解释给大家,当然,肯定有不准确的地方,还请各位指正。
先看看对该函数的注释:“初始堆被映射、分配,指向的页目录和页表被初始化。”
在一个while{}循环体中创建内存描述符用来描述我们所知道的所有内存,然后设置页表,最后分配描述符描述我们的内存布局。
详细的分析参照《ntldr内存初始化、分配、操作及相关函数分析》。
           在初始化部分的最后还需要调用两个函数:
                 MempTurnOnPaging()   //设置页表,并允许分页
                 MempCopyGdt()   //拷贝GDT、IDT
                 这两个函数比较简单。
内存子系统的第一步初始化部分在这里就算完结了,后边还有第二步。

让我们回到DoGlobalInitialization()函数中去看看还有什么要做的。
中间的代码我就不再多废口舌了,只不过是将BOOT_CONTEXT结构中的信息拷贝出来,以备后用。
最后,该函数还需要调用InitializeMemoryDescriptor()函数实现内存子系统初始化的第二步。
InitializeMemoryDescriptors():
     该函数读取固件地址空间映射并保留该范围,并将固件空间正式宣布为“地址空间保留”。
     还有一点需要提到,调用宏函数 GET_MEMORY_DESCRIPTOR(),该函数的定义如下:
     #define GET_MEMORY_DESCRIPTOR (*ExternalServicesTable->GetMemoryDescriptor)
  GetMemoryDescriptor 宏函数的定义参见esp.asm。
     其实该函数与我在第一部分中介绍的ConstructMemoryDescriptors ()函数在行为上没有多大的差别,只不过是用GetMemoryDescriptor 宏函数来替代 Int15E820 函数,那么现在我开始想该函数的作用了,如果我们的电脑在初步的初始化完成后,在打开A20地址线以后,会不会再次获取的内存和在 ConstructMemoryDescriptors ()函数中获取的内存有所区别呢??所以说单从代码的角度理解该函数不困难,但是从全局来看,对于该函数的理解我到是有些困惑了,如果你更清楚,请联系我(zl21_spawn@163.com

至此,内存子系统的初始化过程完毕,与此同时,DoGlobalInitialization()函数执行完毕,让我们回到我们的原点,回到SL的入口点函数中去。

接下来,NtProcessStartup()函数调用BlFillInSystemParameters()函数
     该函数填充 Global System Parameter Block 中的段,包括固件的中断向量,中断向量的专有信息。
     填充的中断向量包括:
     for (cnt=0; cntFSContextPointer->BootDrive == 0) //从A:盘启动
     else if (BlIsElToritoCDBoot(BootContextRecord->FSContextPointer->BootDrive))
                                                                              //从CD启动
     else { BlGetActivePartition(BootPartitionName);}       //从硬盘启动
BlIsElToritoCDBoot()函数我只在这里简单的介绍一些,该函数首先调用FwAllocateHeap (SCRATCH_BUFFER_SIZE)从"firmware" temporary heap中分配内存赋给LocalBuffer,然后判断DriveNum是否大于0x81并返回真假值。
我把重点放在BlGetActivePartition()函数中,下面我详细的解释一下该函数:
             i=1;
             do {
               sprintf(NameBuffer, "multi(0)disk(0)rdisk(0)partition(%u)",i);

                 // 所有关于ArcXXX的函数参考nt4src/private/ntos/inc/arc.h
// ArcOpen函数只不过是一个函数指针,实际调用的函数是AEOpen()函数,这两个函// 数的对应关系可以参照《ntldr关键结构分析》中的部分章节。
// AEOpen()函数的分析参考《ntldr磁盘操作及相关函数分析》,我已经将该函数// 及其相关的函数放在此篇中,如果你不明白,可以联系我(zl21_spawn@163.com

Status = ArcOpen(NameBuffer,ArcOpenReadOnly,&FileId);  
               if (Status != ESUCCESS) {
                   // we've run out of partitions, return the default.
                   i=1;
                   break;
               } else {

                   // 读取开始的 512 字节
// 该函数我没有在《ntldr磁盘操作及相关函数分析》中列出,原因很简单,
// AERead()函数实际调用BiosDiskRead()函数,该函数已经在AEOpen()的相关的// 函数中介绍过了。你可以自己参考相关的代码,代码位置:arcemul.c
                   Status = ArcRead(FileId, SectorBuffer, 512, &Count);
                   ArcClose(FileId);
                   if (Status==ESUCCESS) {
                     // 只需要比较最开始的 36 字节
                     //   jmp 跳转指令占用 3 字节
                     //   Oem 段占用 8 字节
                     //   BPB(Bios参数块)占用 25 字节
                     if (memcmp(SectorBuffer, (PVOID)0x7c00, 36)==0) {
                       // 找到匹配的,跳出循环体
                       break;
                     }
                   }
               }
               ++i;
} while ( TRUE );
// 概括的讲,上面的循环体比较好理解,其实就是获取活动分区并读取其中的引导记录, 判断// 是否是引导扇区。

回到NtProcessStartup(),接下来调用BlMemoryInitialize()函数,初始化内存描述符链表、OS loader堆和OS loader参数块,该函数比较复杂,详细的分析参看《ntldr内存初始化、分配、操作及相关函数分析》。

接下来就比较简单了,调用 BlIoInitialize()初始化 OS loader I/O,这个函数着实简单,其中调用的各个文件系统初始化也只是简单的返回 ESUCCESS,根本就算不上什么初始化。

最后由NtProcessStartup()调用 BlStartup()函数,再一次完成控制权的转交。

感想:
OK,这一部分的代码分析到这里就算是结束了,只能看你理解多少了,因为没有相关的参考资料可以参照,不是我吝啬,确实没有找到什么,只能自己理解。对于这一部分的代码,我想只能用一个词来形容“精彩”,包括内存的初始化、磁盘的操作,起初分析时感觉很混乱,但是当我一步步的走出来时,真的有一种拨开云雾见天日的感觉!现在你我都一样,对系统的初始化部分有了更深一层的理解,而不只是停留在《windows internals》中对ntldr的简单介绍了,还有最后一部分,加油!

第三部分:

在上一部分,我说过最后将控制权转交到 BlStartup()函数中,也许你要问了,为什么还没有开始加载系统内核?别忘了,我们还有一个中间环节还没做,读取 BOOT.INI 以及加载 NTDETECT.COM 程序。

这一部分的代码比上两部分要复杂的多,稍有不甚就有可能绕在里边,所以请你做好心理准备,而且从这里开始就要根据具体的文件分区格式区别对待了。

我把其中相关的函数放在《ntldr其他相关函数分析》一文中,可以自己查找部分函数的分析过程。

我们来看看 BlStartup()函数的具体实现:

VOID
BlStartup(
  IN PCHAR PartitionName
  )
{
  PUCHAR Argv[10];
  ARC_STATUS Status;
  ULONG BootFileId;
  PCHAR BootFile;
  ULONG Read;
  PCHAR p;
  ULONG i;
  ULONG DriveId;
  ULONG FileSize;
  ULONG Count;
  LARGE_INTEGER SeekPosition;
  PCHAR LoadOptions = NULL;
  BOOLEAN UseTimeOut=TRUE;
  BOOLEAN AlreadyInitialized = FALSE;
  extern BOOLEAN FwDescriptorsValid;

  //
  // Open the boot partition so we can load boot drivers off it.
//
// 打开我们的系统分区
  Status = ArcOpen(PartitionName, ArcOpenReadOnly, &DriveId);
  if (Status != ESUCCESS) {
    BlPrint("Couldn't open drive %s\n",PartitionName);
    BlPrint(BlFindMessage(BL_DRIVE_ERROR),PartitionName);
    goto BootFailed;
  }

  //
  // 初始化 dbcs 字符并显示支持?? (我对字符操作不是太在行)
  //
  TextGrInitialize(DriveId);

do {

    // 打开 boot.ini文件,对 BlOpen()函数的分析参考《ntldr磁盘操作及相关函数分析》一文
    Status = BlOpen( DriveId,
                "\\boot.ini",
                ArcOpenReadOnly,
                &BootFileId );
    // MyBuffer 为全局变量       // UCHAR MyBuffer[SECTOR_SIZE+32];
    BootFile = MyBuffer;
    // 将 MyBuffer 缓冲区清0
    RtlZeroMemory(MyBuffer, SECTOR_SIZE+32);

    if (Status != ESUCCESS) {
        BootFile[0]='\0';
    } else {   // eles (Status==ESUCCESS)
        //
        // Determine the length of the boot.ini file by reading to the end of
        // file.
        //
        // 确定 boot.ini 文件的长度
        FileSize = 0;
        do {
          Status = BlRead(BootFileId, BootFile, SECTOR_SIZE, &Count);
          if (Status != ESUCCESS) {
            BlClose(BootFileId);
            BlPrint(BlFindMessage(BL_READ_ERROR),Status);
            BootFile[0] = '\0';
            FileSize = 0;
            Count = 0;
            goto BootFailed;
          }

          FileSize += Count;
        } while (Count != 0);

        // 如果 boot.ini 文件超过了一个扇区的大小(512 bytes),那么我们需要重新分配
// BootFile
        if (FileSize >= SECTOR_SIZE) {

          //
          // We need to allocate a bigger buffer, since the boot.ini file
          // is bigger than one sector.
          //

          BootFile=FwAllocateHeap(FileSize);
        }

        if (BootFile == NULL) {
          BlPrint(BlFindMessage(BL_READ_ERROR),ENOMEM);
          BootFile = MyBuffer;
          BootFile[0] = '\0';
          goto BootFailed;
        } else {     // esle (BootFile!=NULL)

          SeekPosition.QuadPart = 0;
          Status = BlSeek(BootFileId,
                    &SeekPosition,
                    SeekAbsolute);
          if (Status != ESUCCESS) {
            BlPrint(BlFindMessage(BL_READ_ERROR),Status);
            BootFile = MyBuffer;
            BootFile[0] = '\0';
            goto BootFailed;
          } else {
            Status = BlRead( BootFileId,
                        BootFile,
                        FileSize,
                        &Read );

            SeekPosition.QuadPart = 0;
            Status = BlSeek(BootFileId,
                        &SeekPosition,
                        SeekAbsolute);
            if (Status != ESUCCESS) {
                BlPrint(BlFindMessage(BL_READ_ERROR),Status);
                BootFile = MyBuffer;
                BootFile[0] = '\0';
                goto BootFailed;
            } else {
                BootFile[Read]='\0';
            }
          }
        }     // ends esle (BootFile!=NULL)

        //
        // Find Ctrl-Z, if it exists
        //

        p=BootFile;
        while ((*p!='\0') && (*p!=26)) {
          ++p;
        }
        if (*p != '\0') {
          *p='\0';
        }
        BlClose(BootFileId);
    }       // ends else (Status==ESUCCESS)

    if (!AlreadyInitialized) {
        // 在 IBM PS/2带有另外一个BIOS,内置于标准的BIOS中,通常称为ABIOS,只有OS/2系统才// 使用他。该函数的责任就是加载 ABIOS.SYS,但是对于这个文件我不是太熟悉,也不知道他// 的具体用途,所以就没有再查询资料来分析他
        //  
        AbiosInitDataStructures();
    }

    MdShutoffFloppy(); // 关闭软盘马达

    TextClearDisplay(); // 清屏.并设置光标到坐标(0,0)处
     
// 在boot.ini文件中选择要启动的操作系统内核,对于该函数我没有详细的分析,(代码参考
//《ntldr其他相关函数分析》),不过需要注意一点,在该函数中 LoadOptions参数指示是否加载// 了Debug选项,即是否可以调试内核。
    p=BlSelectKernel(DriveId,BootFile, &LoadOptions, UseTimeOut);

    if (!AlreadyInitialized) {

        BlPrint(BlFindMessage(BL_NTDETECT_MSG));
        // 打开 NTDETECT.COM 文件并加载他,本来是不打算写这个模块的分析的,可是这一步实在// 是不可缺少,所以将这一部分的代码单独放在《NTDETECT.COM程序分析》当中
        if (!BlDetectHardware(DriveId, LoadOptions)) {
          BlPrint(BlFindMessage(BL_NTDETECT_FAILURE));
          return;
        }

        TextClearDisplay();

        //
        // Initialize SCSI boot driver, if necessary.
        // 如果我们使用了 SCSI设备,那么需要初始化他们
        if(!_strnicmp(p,"scsi(",5)) {
          AEInitializeIo(DriveId);
        }
        ArcClose(DriveId);
        //
        // Indicate that fw memory descriptors cannot be changed from
        // now on.
        // 将 FwDescriptorsValid 置为 FALSE,这意味着我们不能在使用 FwAllocateHeap()函// 数分配内存空间
        FwDescriptorsValid = FALSE;
    } else {
        TextClearDisplay();
    }

    //
    // Set AlreadyInitialized Flag to TRUE to indicate that ntdetect
    // and abios init routines have been run.
    //

    AlreadyInitialized = TRUE;

    //
    // Only time out the boot menu the first time through the boot.
    // For all subsequent reboots, the menu will stay up.
    //
    UseTimeOut=FALSE;

    i=0;
    while (*p !='\\') {
        KernelBootDevice[i] = *p;
        i++;
        p++;
    }
    KernelBootDevice[i] = '\0';

    // 填充 BlOsLoader()函数需要用到的参数
    strcpy(OsLoadFilename,"osloadfilename=");
    strcat(OsLoadFilename,p);

    //
    // We are fooling the OS Loader here. It only uses the osloader= variable
    // to determine where to load HAL.DLL from. Since x86 systems have no
    // "system partition" we want to load HAL.DLL from \nt\system\HAL.DLL.
    // So we pass that it as the osloader path.
    //

    strcpy(OsLoaderFilename,"osloader=");
    strcat(OsLoaderFilename,p);
    strcat(OsLoaderFilename,"\\System32\\NTLDR");

    strcpy(SystemPartition,"systempartition=");
    strcat(SystemPartition,KernelBootDevice);

    strcpy(OsLoadPartition,"osloadpartition=");
    strcat(OsLoadPartition,KernelBootDevice);

    strcpy(OsLoadOptions,"osloadoptions=");
    if (LoadOptions) {
        strcat(OsLoadOptions,LoadOptions);
    }

    strcpy(ConsoleInputName,"consolein=multi(0)key(0)keyboard(0)");

    strcpy(ConsoleOutputName,"consoleout=multi(0)video(0)monitor(0)");

    strcpy(X86SystemPartition,"x86systempartition=");
    strcat(X86SystemPartition,PartitionName);

    Argv[0]="load";

    Argv[1]=OsLoaderFilename;
    Argv[2]=SystemPartition;
    Argv[3]=OsLoadFilename;
    Argv[4]=OsLoadPartition;
    Argv[5]=OsLoadOptions;
    Argv[6]=ConsoleInputName;
    Argv[7]=ConsoleOutputName;
    Argv[8]=X86SystemPartition;

    // 在这里实现控制权的转交,准备加载内核
    Status = BlOsLoader(9,Argv,NULL);

  BootFailed:

    if (Status != ESUCCESS) {
        //
        // Boot failed, wait for reboot
        //
        while (TRUE) {
          GET_KEY();
        }
    } else {
        //
        // Need to reopen the drive
        //
        Status = ArcOpen(BootPartitionName, ArcOpenReadOnly, &DriveId);
        if (Status != ESUCCESS) {
          BlPrint(BlFindMessage(BL_DRIVE_ERROR),BootPartitionName);
          goto BootFailed;
        }

    }
  } while (TRUE);

}

// 现在开始加载系统内核
ARC_STATUS
BlOsLoader (IN ULONG Argc,IN PCHAR Argv[],IN PCHAR Envp[])
/*
Routine Description:

  This is the main routine that controls the loading of the NT operating
  system on an ARC compliant system. It opens the system partition,
  the boot partition, the console input device, and the console output
  device. The NT operating system and all its DLLs are loaded and bound
  together. Control is then transfered to the loaded system.

Arguments:

  Argc - Supplies the number of arguments that were provided on the
    command that invoked this program.

  Argv - Supplies a pointer to a vector of pointers to null terminated
    argument strings.

  Envp - Supplies a pointer to a vector of pointers to null terminated
    environment variables.

Return Value:

  EBADF is returned if the specified OS image cannot be loaded.

--*/

{

  CHAR BootDirectoryPath[256];
  ULONG CacheLineSize;
  PCHAR ConsoleOutDevice;
  PCHAR ConsoleInDevice;
  ULONG Count;
  PCONFIGURATION_COMPONENT_DATA DataCache;
  CHAR DeviceName[256];
  CHAR DevicePrefix[256];
  PCHAR DirectoryEnd;
  CHAR DllName[256];
  CHAR DriverDirectoryPath[256];
  PCHAR FileName;
  ULONG FileSize;
  PLDR_DATA_TABLE_ENTRY HalDataTableEntry;
  CHAR HalDirectoryPath[256];
  CHAR KernelDirectoryPath[256];
  PVOID HalBase;
  PVOID SystemBase;
  ULONG Index;
  ULONG Limit;
  ULONG LinesPerBlock;
  PCHAR LoadDevice;
  ULONG LoadDeviceId;
  PCHAR LoadFileName;
  PCHAR LoadOptions;
  ULONG i;
  CHAR OutputBuffer[256];
  ARC_STATUS Status;
  PLDR_DATA_TABLE_ENTRY SystemDataTableEntry;
  PCHAR SystemDevice;
  ULONG SystemDeviceId;
  PTRANSFER_ROUTINE SystemEntry;
  PIMAGE_NT_HEADERS NtHeaders;
  PWSTR BootFileSystem;
  PCHAR LastKnownGood;
  BOOLEAN BreakInKey;
  CHAR BadFileName[128];
  PBOOTFS_INFO FsInfo;

  //
  // Get the name of the console output device and open the device for
  // write access.
  // 获取输出设备控制台的名称(一般指屏幕),并以写访问权限打开该设备
  ConsoleOutDevice = BlGetArgumentValue(Argc, Argv, "consoleout");
  if (ConsoleOutDevice == NULL) {
    return ENODEV;
  }
  Status = ArcOpen(ConsoleOutDevice, ArcOpenWriteOnly, &BlConsoleOutDeviceId);
  if (Status != ESUCCESS) {
    return Status;
  }

  //
  // Get the name of the console input device and open the device for
  // read access.
  // 获取输入设备控制台的名称(一般指键盘和鼠标),并以读访问权限打开该设备
  ConsoleInDevice = BlGetArgumentValue(Argc, Argv, "consolein");
  if (ConsoleInDevice == NULL) {
    return ENODEV;
  }
  Status = ArcOpen(ConsoleInDevice, ArcOpenReadOnly, &BlConsoleInDeviceId);
  if (Status != ESUCCESS) {
    return Status;
  }

  // 向输出设备(显示屏)写入 OsLoader 的版本号
  strcpy(&OutputBuffer[0], "OS Loader V4.00\r\n");
  ArcWrite(BlConsoleOutDeviceId,
        &OutputBuffer[0],
        strlen(&OutputBuffer[0]),
        &Count);

  //
  // Initialize the memory descriptor list, the OS loader heap, and the
  // OS loader parameter block.
  //
  Status = BlMemoryInitialize();
  if (Status != ESUCCESS) {
    BlFatalError(LOAD_HW_MEM_CLASS,
                DIAG_BL_MEMORY_INIT,
                LOAD_HW_MEM_ACT);
    goto LoadFailed;
  }

  //
  // Compute the data cache fill size. This value is used to align
  // I/O buffers in case the host system does not support coherent
  // caches.
  //
  // If a combined secondary cache is present, then use the fill size
  // for that cache. Otherwise, if a secondary data cache is present,
  // then use the fill size for that cache. Otherwise, if a primary
  // data cache is present, then use the fill size for that cache.
  // Otherwise, use the default fill size.
  //
  // KeFindConfigurationEntry()函数到还是简单,他的代码位于 \ke\conifg.c 中,但是也是我最// 想不通的一件事情,此时运行在 OsLoader中,现在 既没有加载 ntoskrnl.exe 的image,也没有// 加载 hal.dll 的 image,怎么可能去调用 \ke\conifg.c 中的函数呢??只有一种可能,\ke 下// 的代码是和 OsLoader 编译在一起的,这种可能太让我吃惊了
  //  
  // 在由 NTDETECT.COM 创建的设备配置树中寻找分类为 CacheClass,类型为 SecondaryCache 的  
// DataCache
  DataCache = KeFindConfigurationEntry(BlLoaderBlock->ConfigurationRoot,
                          CacheClass,
                          SecondaryCache,
                          NULL);

  if (DataCache == NULL) {
    DataCache = KeFindConfigurationEntry(BlLoaderBlock->ConfigurationRoot,
                              CacheClass,
                              SecondaryDcache, // 类型不是一样的
                              NULL);

    if (DataCache == NULL) {
        DataCache = KeFindConfigurationEntry(BlLoaderBlock->ConfigurationRoot,
                                CacheClass,
                                PrimaryDcache,   // 类型不是一样的
                                NULL);
    }
  }

  if (DataCache != NULL) {
    LinesPerBlock = DataCache->ComponentEntry.Key >> 24;
    CacheLineSize = 1 ComponentEntry.Key >> 16) & 0xff);
    BlDcacheFillSize = LinesPerBlock * CacheLineSize;
  }

  //
  // Initialize the OS loader I/O system.
  //

  Status = BlIoInitialize();
  if (Status != ESUCCESS) {
    BlFatalError(LOAD_HW_DISK_CLASS,
                DIAG_BL_IO_INIT,
                LOAD_HW_DISK_ACT);
    goto LoadFailed;
  }

  //
  // Initialize the resource section
  // 对于该函数我不是太明白其用意,其中读取了前两个扇区
  Status = BlInitResources(Argv[0]);
  if (Status != ESUCCESS) {
    BlFatalError(LOAD_HW_DISK_CLASS,
                DIAG_BL_IO_INIT,
                LOAD_HW_DISK_ACT);
  }

  //
  // Initialize the NT configuration tree.
  //

  BlLoaderBlock->ConfigurationRoot = NULL;

// BlConfigurationInitialize()函数根据 NTDETECT.COM 中所创建的设备配置树创建  
// NT configuration tree
  Status = BlConfigurationInitialize(NULL, NULL);
  if (Status != ESUCCESS) {
    BlFatalError(LOAD_HW_FW_CFG_CLASS,
              DIAG_BL_CONFIG_INIT,
              LOAD_HW_FW_CFG_ACT);
    goto LoadFailed;
  }

  //
  // 有的时候为了适应特殊的要求,我们可能配置 boot.ini 文件,并加载所需要的 boot options,// 下面这部分代码就是从 boot.ini文件中析取出 boot options
  //
  LoadOptions = BlGetArgumentValue(Argc, Argv, "osloadoptions");
  if (LoadOptions != NULL) {
    FileSize = strlen(LoadOptions)+1;
    FileName = (PCHAR)BlAllocateHeap(FileSize);
    strcpy(FileName, LoadOptions);
    BlLoaderBlock->LoadOptions = FileName;

    //
    // check for magic switch that says we should output
    // the filenames of the files instead of just dots.
    //
    if ((strstr(FileName,"SOS")!=NULL) ||
        (strstr(FileName,"sos")!=NULL)) {
        BlOutputDots=FALSE;
    }

    FileName=strstr(BlLoaderBlock->LoadOptions,"HAL=");
    if (FileName) {
        for (i=0; iLoadOptions,"KERNEL=");
    if (FileName) {
        for (i=0; iLoadOptions = NULL;
  }

  //
  // 获取 load device 的名称并以读访问方式打开该设备
  //
  LoadDevice = BlGetArgumentValue(Argc, Argv, "osloadpartition");
  if (LoadDevice == NULL) {
        Status = ENODEV;
    BlFatalError(LOAD_HW_FW_CFG_CLASS,
                DIAG_BL_FW_GET_BOOT_DEVICE,
                LOAD_HW_FW_CFG_ACT);
    goto LoadFailed;
  }

  Status = ArcOpen(LoadDevice, ArcOpenReadOnly, &LoadDeviceId);
  if (Status != ESUCCESS) {
    BlFatalError(LOAD_HW_DISK_CLASS,
                DIAG_BL_OPEN_BOOT_DEVICE,
                LOAD_HW_DISK_ACT);
    goto LoadFailed;
  }

  //
  // 获取系统设备的名称(即系统分区)并以读访问方式打开该设备  
  //
  SystemDevice = BlGetArgumentValue(Argc, Argv, "systempartition");
  if (SystemDevice == NULL) {
    Status = ENODEV;
    BlFatalError(LOAD_HW_FW_CFG_CLASS,
                DIAG_BL_FW_GET_SYSTEM_DEVICE,
                LOAD_HW_FW_CFG_ACT);
    goto LoadFailed;
  }

  Status = ArcOpen(SystemDevice, ArcOpenReadOnly, &SystemDeviceId);
  if (Status != ESUCCESS) {
    BlFatalError(LOAD_HW_FW_CFG_CLASS,
                DIAG_BL_FW_OPEN_SYSTEM_DEVICE,
                LOAD_HW_FW_CFG_ACT);
    goto LoadFailed;
  }

  //
  // 初始化 debugging system.
  //
  BlLogInitialize(SystemDeviceId);

  //
  // Display the Configuration prompt for breakin key at this
  // point. No key presses are checked for at this point, but
  // this gives the user a little more reaction time.
  //
  BlStartConfigPrompt();

  //
  // 获取系统根目录的名称
  //
  LoadFileName = BlGetArgumentValue(Argc, Argv, "osloadfilename");
  if (LoadFileName == NULL) {
    Status = ENOENT;
    BlFatalError(LOAD_HW_FW_CFG_CLASS,
                DIAG_BL_FW_GET_BOOT_DEVICE,
                LOAD_HW_FW_CFG_ACT);
    goto LoadFailed;
  }

  //
  // 系统根目录为  ( "\winnt" )
  //

  //
  // 产生 SYSTEM32 的目录名
  //
  strcpy(BootDirectoryPath, LoadFileName);
  strcat(BootDirectoryPath, "\\System32\\");

  //
  // ntoskrnl.exe 内核的全路径名为:
  //     "\winnt\system32\ntoskrnl.exe"
  //
  strcpy(KernelDirectoryPath, BootDirectoryPath);
  strcat(KernelDirectoryPath, KernelFileName);

  //
  // 将系统映象加载入内存
  //
  BlOutputLoadMessage(LoadDevice, KernelDirectoryPath);
  Status = BlLoadImage(LoadDeviceId,
                LoaderSystemCode,
                KernelDirectoryPath,
                TARGET_IMAGE,
                &SystemBase);

  if (Status != ESUCCESS) {
    BlFatalError(LOAD_SW_MIS_FILE_CLASS,
                          DIAG_BL_LOAD_SYSTEM_IMAGE,
                          LOAD_SW_FILE_REINST_ACT);
    goto LoadFailed;
  }

  //
  // 获取系统映象所在分区的文件系统格式
  //
  FsInfo = BlGetFsInfo(LoadDeviceId);
  if (FsInfo != NULL) {
    BootFileSystem = FsInfo->DriverName;
  } else {
    BlFatalError(LOAD_SW_MIS_FILE_CLASS,
                          DIAG_BL_LOAD_SYSTEM_IMAGE,
                          LOAD_SW_FILE_REINST_ACT);
    goto LoadFailed;
  }

  //
  // 获取 HAL DLL 所在的路径
  //
  FileName = BlGetArgumentValue(Argc, Argv, "osloader");

  if (FileName == NULL) {
    Status = ENOENT;
    BlFatalError(LOAD_HW_FW_CFG_CLASS,
                DIAG_BL_FIND_HAL_IMAGE,
                LOAD_HW_FW_CFG_ACT);
    goto LoadFailed;
  }

  DirectoryEnd = strrchr(FileName, '\\');
  FileName = strchr(FileName, '\\');
  HalDirectoryPath[0] = 0;
  if (DirectoryEnd != NULL) {
    Limit = (ULONG)DirectoryEnd - (ULONG)FileName + 1;
    for (Index = 0; Index OptionalHeader.AddressOfEntryPoint);

  //
  // 构造设备驱动程序所在的目录
  //
  strcpy(&DriverDirectoryPath[0], &BootDirectoryPath[0]);
  strcat(&DriverDirectoryPath[0], "\\Drivers\\");

  //
// 为 NLS (民族语言支持能力)数据分配结构. 该数据将在加载系统注册表
// (BlLoadAndScanSystemHive)时被填充.
  //
  BlLoaderBlock->NlsData = BlAllocateHeap(sizeof(NLS_DATA_BLOCK));
  if (BlLoaderBlock->NlsData == NULL) {
    Status = ENOMEM;
    BlFatalError(LOAD_HW_MEM_CLASS,
                DIAG_BL_LOAD_SYSTEM_HIVE,
                LOAD_HW_MEM_ACT);
    goto LoadFailed;
  }
  //
  // Load the registry SYSTEM hive.
  //
  // DIAG_BL_LOAD_SYSTEM_REGISTRY_HIVE
  // "Cannot load system hardware configuration file.\r\n"
  // 在该函数中不仅加载了系统注册表,同时还加载了 NLS,更为重要的是启动设备的驱动程序也在该// 函数中被加载
  Status = BlLoadAndScanSystemHive(LoadDeviceId,
                        LoadDevice,
                        LoadFileName,
                        BootFileSystem,
                        BadFileName);

  if (Status != ESUCCESS) {
    if (BlRebootSystem) {
        Status = ESUCCESS;
    } else {
        BlBadFileMessage(BadFileName);
    }
    goto LoadFailed;
  }

  //
  // Generate the ARC boot device name and NT path name.
  //

  Status = BlGenerateDeviceNames(LoadDevice, &DeviceName[0], &DevicePrefix[0]);
  if (Status != ESUCCESS) {
        BlFatalError(LOAD_HW_FW_CFG_CLASS,
                DIAG_BL_ARC_BOOT_DEV_NAME,
                LOAD_HW_FW_CFG_ACT);
    goto LoadFailed;
  }

  FileSize = strlen(&DeviceName[0]) + 1;
  FileName = (PCHAR)BlAllocateHeap(FileSize);
  strcpy(FileName, &DeviceName[0]);
  BlLoaderBlock->ArcBootDeviceName = FileName;

  FileSize = strlen(LoadFileName) + 2;
  FileName = (PCHAR)BlAllocateHeap( FileSize);
  strcpy(FileName, LoadFileName);
  strcat(FileName, "\\");
  BlLoaderBlock->NtBootPathName = FileName;

  //
  // Generate the ARC HAL device name and NT path name.
  //

#ifdef i386

  //
  // On X86, the systempartition variable lies, and instead points to the location
  // of the hal. We pass in another variable, 'X86SystemPartition', that tell us
  // the real system partition.
  //
  strcpy(&DeviceName[0], BlGetArgumentValue(Argc, Argv, "x86systempartition"));

#else

  Status = BlGenerateDeviceNames(SystemDevice, &DeviceName[0], &DevicePrefix[0]);
  if (Status != ESUCCESS) {
    BlFatalError(LOAD_HW_FW_CFG_CLASS,
                DIAG_BL_ARC_BOOT_DEV_NAME,
                LOAD_HW_FW_CFG_ACT);
    goto LoadFailed;
  }

#endif //i386

  FileSize = strlen(&DeviceName[0]) + 1;
  FileName = (PCHAR)BlAllocateHeap(FileSize);
  strcpy(FileName, &DeviceName[0]);
  BlLoaderBlock->ArcHalDeviceName = FileName;

#ifdef i386

  //
  // On X86, this structure is unfortunately named. What we really need here is the
  // osloader path. What we actually have is a path to the HAL. Since this path is
  // always at the root of the partition, hardcode it here.
  //

  FileName = (PCHAR)BlAllocateHeap(2);
  FileName[0] = '\\';
  FileName[1] = '\0';

#else

  FileSize = strlen(&HalDirectoryPath[0]) + 1;
  FileName = (PCHAR)BlAllocateHeap(FileSize);
  strcpy(FileName, &HalDirectoryPath[0]);

#endif //i386

  BlLoaderBlock->NtHalPathName = FileName;

  //
  // Get the NTFT drive signatures to allow the kernel to create the
  // correct ARC name  NT name mappings.
  //
  BlGetArcDiskInformation();

  //
  // Execute the architecture specific setup code.
  //
  Status = BlSetupForNt(BlLoaderBlock);
  if (Status != ESUCCESS) {
    BlFatalError(LOAD_SW_INT_ERR_CLASS,
                DIAG_BL_SETUP_FOR_NT,
                LOAD_SW_INT_ERR_ACT);

    goto LoadFailed;
  }

  //
  // Turn off the debugging system.
  //
  BlLogTerminate();

  //
  // 实际调用了 KiSystemStartup(LoaderBlock)函数,向 ntoskrnl.exe 转交控制权
  // 到这里,ntldr 完成了他的使命
  (SystemEntry)(BlLoaderBlock);

  Status = EBADF;
  BlFatalError(LOAD_SW_BAD_FILE_CLASS,
              DIAG_BL_KERNEL_INIT_XFER,
              LOAD_SW_FILE_REINST_ACT);

  //
  // The load failed.
  //

LoadFailed:
  return Status;

}

感想:
在这一分中并不如上两部分介绍的那么详细,而且对于 NTDETECT.COM 的分析也只是简单的一带而过(因为其中大部分是对硬件的检测,而且没有太详细的参考资料),将这一部分分析下来感觉并没有像前两章中那么兴奋,只是感觉在这一段中按部就班的走下来,这一部分中绝大多数就是在析取boot.ini文件的选项,加载系统映象(对PE头或DLL头的操作),要说看点,我认为只有两出:一个是创建配置树(NTDETECT.COM中),第二就是加载注册表的部分了。

写在最后:
我知道其实分析ntldr对实际应用来说没有什么太大的用处,除了更好的理解系统的运作外,我没有想出太好的理由,不过我比较喜欢走边缘路线,佛曰:我不入地狱谁入地狱!

[课程]Linux pwn 探索篇!

收藏
免费 0
支持
分享
最新回复 (5)
雪    币: 239
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
壳壳好久不见P.S 原文在哪里?
2007-11-11 21:59
0
雪    币: 267
活跃值: (16)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
3
http://blog.csdn.net/icelord/
2007-11-15 13:30
0
雪    币: 109
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
http://blog.zndev.com/blog.php?uid=356
2007-11-15 17:39
0
雪    币: 331
活跃值: (56)
能力值: ( LV13,RANK:410 )
在线值:
发帖
回帖
粉丝
5
收藏.
补丁补丁
2007-11-16 14:08
0
雪    币: 8674
活跃值: (3848)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
请问:文件提到的附件在什么地方可以下载?
2007-12-8 14:39
0
游客
登录 | 注册 方可回帖
返回
//