浅析Windows NT/2000环境切换(http://webcrazy.yeah.net)

浅析Windows NT/2000环境切换
                  WebCrazy( tsu00@263.net)
                       注:本文最初见于 www.nsfocus.com

    本文假设您已经了解Windows NT/2000系统体系,对Windows NT/2000内部KPEB/KTEB等数据结构与内核工作方式已有一定的概念,对80x86保护模式,Intel/AT&T格式汇编语言有过学习,能熟练使用SoftICE for Windows NT,且曾经接触过Microsoft Visual Studio及其附带工具,翻阅过Linux内核代码,如果您对这些方面不甚了解,请自行参阅相关书籍。

    环境切换(Context Switch)牵涉到很多方面的内容,本文仅对与其有关的几个数据进行详细的讨论,并给出取得这些数据的部分程序段,还列出Windows 2000的少量环境切换代码。另外文中讨论的系统内部数据均未来自Microsoft官方文档,在Windows NT/2000的下个版本甚至目前各版本间均会有差别,所以我尽量详细的将文中所涉及的软硬件列于下面,所有因硬件体系、软件版本不同等因素引起的差异,请自行根据您的情况予以调整。

⊙ x86平台单处理机Windows 2000 Server Build 2195
⊙ Numega SoftICE 4.05 for Windows NT/2000 Build 334
⊙ Linux 2.0.30内核
⊙ Datarescue IDA 4.0.4.362
⊙ Microsoft Visual Studio 6.0 SP3
⊙ Windows 2000 DDK

    80x86产生的环境切换有以下几种可能:
    1.当前任务执行一个FAR CALL或JMP指令,而选择器指向一个TSS描述符或一个任务门。
    2.当前任务执行IRET指令返回先前任务,IRET只在EFLAGS寄存器中的NT位置1时产生切换。
    3.发生一个中断或异常情况,并且IDT项是个任务门。

    Linux内核中有如下代码:

    /*
    /usr/src/linux/include/asm-i386/system.h
    仅列出单处理器实现代码
    */

    #define switch_to(prev,next) do { /
    __asm__("movl %2,"SYMBOL_NAME_STR(current_set)"/n/t" /
    "ljmp %0/n/t" /
    "cmpl %1,"SYMBOL_NAME_STR(last_task_used_math)"/n/t" /
    "jne 1f/n/t" /
    "clts/n" /
    "1:" /
    : /* no outputs */ /
    :"m" (*(((char *)&next->tss.tr)-4)), /
    "r" (prev), "r" (next)); /
    /* Now maybe reload the debug registers */ /
        .
        .
        .
    } /
    } while (0)

    这段代码使用了上面讨论的第一种情况。

    众所周知,Linux是个开放源代码的操作系统,而Microsoft则没如此“大方”,但我们仍能对其进行些逆向工程,可喜的是网上目前已经有很多人对此有过研究,现摘录Mark Russinovich部分成果( http://www.sysinternals.com/tips.htm):

    //
    // NT's main
    // NTOSKRNL main
    //
    int main( boot parameters )
    {
        //
        // Fire up NT!
        //
        KiSystemStartup();
        return 0;
    }


    从中可看出ntoskrnl.exe(PE格式,可方便的使用反汇编工具进行分析)是NT OSLOADER真正调用内核的开始,其对文件对象(File)、作业对象(Job)、进程对象(Process)、线程对象(Thread)、纤程对象(Fiber)、文件映射对象(FileMapping)、事件对象(Event)、互斥对象(Mutex)、信号对象(Semaphore)等许多内核对象进行管理,其也负责线程调度,内存管理,进程间通信等所有操作系统功能,让它们协调工作,我们要讨论的线程切换代码也在此模块中。

    用IDA等对ntoskrnl.exe进行反汇编所得的结果,其分析的工作量恐怕大家都是可想而知的。在我们讨论Windows 2000环境切换详细代码前,还是先让我们看看以下几个重要的与环境切换有关的系统数据:

    1 进程Context
    进程Context是指80x86在保护模式下内存分页机制中当前进程的页目录所在的物理地址,其存放在系统CR3寄存器中,在Windows 2000中所处的位置为KPEB偏移后18h处,看看SoftICE的输出结果吧(限于篇幅,我对输出结果进行了删减,但仍对重要数据进行注解,应注意的是与您当前运行的程序等系统环境密切相关,随机性很强,下同):

    :cpu //显示当前cpu的寄存器值

    Processor 00 Registers
    ----------------------
    CS:EIP=0008:80069582 SS:ESP=0010:8046FD98
    EAX=8046BDF0 EBX=FFDFF000 ECX=FFDFF878 EDX=0000BA5A
    ESI=8046BDF0 EDI=8046BB60 EBP=FFDFF800 EFL=00000213
    DS=0023 ES=0023 FS=0030 GS=0000

    CR0=8000003B PE MP TS ET NE PG
    CR2=76EE18EC
    CR3=00030000
           |
           |_当前进程的CR3
    CR4=000002D1 VME PSE MCE PGE
        .
        .
        .

    :proc idle

    Process KPEB    PID Threads Pri User Time  Krnl Time  Status
    *Idle   8046BB60 0    1      0  00000000   0000BA5A   Running
      |        |
      |        |_Idle进程的KPEB
      |_系统中当前进程(SoftICE中用不同颜色突出,且前面有个*)

    :dd 8046bb60+18 l 4 //dd Idle's KPEB+18h
    0010:8046BB78 00030000 00000000 00000000 00000000 ................
                     |
                     |_Idle进程Context
    :addr
    CR3      LDT Base:Limit KPEB Addr PID Name
    00030000                FE4E1C60  0008 System
    02D59000                FF8E6540  0090 smss
    01D41000                FF8E17E0  00AC csrss
    00686000                FE51BAE0  00C0 winlogon
    0095D000                FF8A7AE0  00DC services
    0276E000                FF8A5D60  00E8 lsass
    00394000                FF881020  0180 svchost
    02CAE000                FF884020  01A4 SPOOLSV
    00882000                FF85B560  01D0 msdtc
    02993000                FF83F020  0238 svchost
    00D2F000                FF83D760  024C llssrv
    0063A000                FF837860  0274 regsvc
    02EFA000                FF6ECD60  0318 dfssvc
    00A5E000                FF823A20  0328 inetinfo
    03612000                FF6AF860  0384 explorer
    003A2000                FF68E460  03B4 internat
    003A7000                FF68CD60  0130 OSA
    008C1000                FF6769A0  03E8 svchost
    01BAA000                FF65A020  01C0 cmd
    00822000                FF86C960  038C conime
    03362000                FF6B3540  0388 notepad
    *00030000               8046BB60  0000 Idle
        |
        |__当前进程Context,是不是与上一命令输出结果一致。

    可以用同样的方法进行再次进行验证。

    2.Context Switches Times 线程已被操作系统调度次数
    当每次操作系统调用线程时,都会将这个值加一的。Visual Studio所附的工具Spy++,在Thread窗口中Thread Properties中Context Switches指出系统中该线程已调度的次数(Spy++只有在Windows NT/2000中运行时才会显示出这个值,9x中则没有)。Switch Times在系统中所处的位置在KTEB的偏移4ch处。

    :thread idle
    TID  Krnl TEB StackBtm StkTop   StackPtr User TEB Process(Id)
    0000 8046BDF0 8046D040 80470040 8046FD90 00000000 Idle(00)
    :dd 8046bdf0+4c l 4
    0010:8046BE3C 0000E778 00000000 00000002 00000000 x...............
                    |
                    |_指出当前Idle线程已经被系统调用0E778h(十进制59256)次了。用e命令改改再看看Spy++的输出结果!

    3.线程所属进程的KPEB与进程名 //分别位于KTEB+44h与KPEB+1fch处
    具体见我在《 再谈Windows NT/2000内部数据结构》( Nsfocus Magazine 11)一文所述。

    应该指出的是,以上讨论只是针对Windows 2000 Server Build 2195的,如果您的系统不是的话或想知道如何得到这些值的具体位置,请参阅我以下的叙述的方法:

    通过找突破口,正像上面我所描述的Context Switches Times在Spy++中显示的一样,然后可以用逆向工程法,我也是用这个方法来取得这个具体位置的。

    举个例子吧!我曾经对SoftICE for NT中的addr命令输出结果(包含进程Context)来自何处感到困惹,也曾经在国外的一些著名的新闻组中提问过,不过至今仍没人应答(可能是我的英文水平太差,人家看不懂什么意思吧!)

    addr命令输出结果见上。

    后来无奈之下我还是想到CR3(存放进程页目录物理地址的寄存器)应与特定的进程有关,其应该存放在KPEB结构中(实际上的确是这样的)。而如果真是这样的话,不是只要枚举(Enum)出系统中所有的KPEB,则能得到所有的CR3值(当然前提是找出其相对KPEB的偏移值),相应的使用我在《 再谈Windows NT/2000内部数据结构》( Nsfocus Magazine 11)的方法就可以取出所有进程的进程名了吗?(PID也是一样的)。

    我在通过分析PSAPI.DLL中枚举系统进程的函数后(EnumProcesses等),发现系统启动后的第一个进程system的KPEB是存放在ntoskrnl.exe导出的PsInitialSystemProcess指出的地址处的,而系统中各个KPEB由一链表联结着,至于链表的定义在Windows 2000 DDK中的ntdef.h中如下定义的:

    typedef struct _LIST_ENTRY {
        struct _LIST_ENTRY *Flink;
        struct _LIST_ENTRY *Blink;
    } LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

    有了KPEB,跟踪相应的代码(这段反汇编代码我将在下面列出),就能找出偏移地址18h处的CR3值。

    以上操作,在Windows 9x中可由Kernel32.dll中序号为1的Undocumented函数(NONAME,Softice Export列表中显示为ORD_0001)实现,但因为Windows 9x与NT/2000的内核的不同,Softice for 9x与NT的addr命令输出的格式也完全不同。至于在Windows NT/2000中不知道是否有现成的函数可以得到结果,至少现在我也没找到,这可是题外话。

    因为上面的叙述还是比较抽象,我还是将SoftICE的输出结果列于此,更利于理解:

    :dd PsInitialSystemProcess l 4
    0008:8046A844 FE4E1C60 E1000968 00000000 00000000 `.N.h...........
                    |
                    |_System进程的KPEB

    :dd @PsInitialSystemProcess+18 l 4
    0008:FE4E1C78 00030000 00000000 00000000 00000000 ................
                    |
                    |_System进程的Context

    :dd @PsInitialSystemProcess+9c l 4 //9ch是PID相对KPEB的位置
    0008:FE4E1CFC 00000008 FF8E65E0 8046A180 00000000 .....e....F.....
                     |
                     |_System进程PID

    :dd @PsInitialSystemProcess+1fc l 10 //1fch是Process Name相对KPEB的位置
    0008:FE4E1E5C 74737953 00006D65 00000000 00000000 System..........
                                                        |
                                 System进程Process Name_|
    :? @(@PsInitialSystemProcess+a0)-a0
    //计算System指向的下一个进程的KPEB,0a0h是链状结构相对KPEB的偏移
    FF8E6540 4287522112 (-7445184) "巈@"
      |
      |_System进程KPEB指向的下一个KPEB,从Process Name可知为smss.exe(见下)

    :dd @(@PsInitialSystemProcess+a0)-a0+18 l 4
    0008:FF8E6558 02D59000 02D5A000 00000000 00000000 ................
                    |
                    |_smss.exe的进程Context

    :dd @(@PsInitialSystemProcess+a0)-a0+9c l 4
    0008:FF8E65DC 00000090 FF8E1880 FE4E1D00 00000518 ..........N.....
                     |
                     |_smss.exe的PID

    :dd @(@PsInitialSystemProcess+a0)-a0+1fc l 10
    0008:FF8E673C 73736D73 6578652E 00000000 00000000 smss.exe........
                                                        |
                               smss.exe进程Process Name_|
    :dd @(@(@PsInitialSystemProcess+a0))-a0+18 l 4

            .
            . (可以用Softice用同样的方法一直跟踪到链表结束)
            .


    实现代码段如下:

    /*
    由于以下代码段均要求获取系统数据结构,即要求在Ring 0状态下运行,所
    以必须位于NT/2000设备驱动程序中。因设备驱动程序的架构,决定的代码的长
    度较长,此处仅列出关键代码段,您可以找本WDM的书,将此程序段置入您的代码中,
    或是直接联系我( tsu00@263.net)。
    */

        .
        .
        .
    PLIST_ENTRY KPEBListHead, KPEBListPtr;          //PLIST_ENTRY定义见上
    ULONG KPEBListOffset=0xa0;                      //定义链表相对KPEB的偏移值
    ULONG ProcessNameOffset=0x1fc;                  //定义ProcessName相对KPEB的偏移值
    ULONG ProcessContextOffset=0x18;                //定义Process Context相对KPEB的偏移值
    ULONG PIDOffset=0x9c;                           //定义PID相对KPEB的偏移值

    if(((USHORT)NtBuildNumber)!=2195){
        DbgPrint("Only test on Windows 2000 Server Build 2195!/n");
        return;
    }

    DbgPrint("/n CR3/t/tKPEB Addr/tPID/t Name");
    DbgPrint("/n ---/t/t-------- /t---/t ----/n");

    KPEBListHead=KPEBListPtr=(PLIST_ENTRY)(((char *)PsInitialSystemProcess)+KPEBListOffset);
    while (KPEBListPtr->Flink!=KPEBListHead) {
        void *kpeb;
        char ProcessName[16];
        ULONG ProcessContext;
        ULONG PID;

        //取KPEB
        kpeb=(void *)(((char *)KPEBListPtr)-KPEBListOffset);

        //取ProcessName
        memset(ProcessName, 0, sizeof(ProcessName));
        memcpy(ProcessName, ((char *)kpeb)+ProcessNameOffset, 16);

        //取Process Context
        ProcessContext=*(ULONG *)(((char *)kpeb)+ProcessContextOffset);

        //取PID
        PID=*(ULONG *)(((char *)kpeb)+PIDOffset);

        //向Debugger输出结果
        DbgPrint(" %08X/t%08X/t%04X/t %s/n",ProcessContext, kpeb,PID,ProcessName);

        //指向下一链表
        KPEBListPtr=KPEBListPtr->Flink;
    }

            .
            .
            .

    使用Checked方式编译运行后调试器输出结果如下(俨然就是一个最底层的EnumProcesses实现方法):

    CR3      KPEB Addr PID  Name
    ---      --------  ---  ----
    00030000 FE4E1C60  0008 System
    02D59000 FF8E7920  0090 smss.exe
    003C1000 FE520520  00AC csrss.exe
    026C6000 FE51A020  00A8 winlogon.exe
    03209000 FF8A8D60  00DC services.exe
            .
            . 略
            .

    我之所以花如此大的篇幅去讲述进程Context的获得,似乎与环境切换的主题不相一致,主要是由于Windows NT/2000的封闭性,我觉得真正要明白环境切换,Linux平台就可以比较容易理解,在NT中重要是知道如何取得些与此有关的重要数据结构,然后再与x86平台体系结构联系在一起,就能更好的帮助自己理解。使用上面所述的类似方法,还可以找出很多KPEB/KTEB重要信息,如进程优先级(KPEB+62h)、进程在内核态与用户态所使用的时间(KPEB+38h与KPEB+3ch)、线程ID(KTEB+1e4h)等等,可与linux的task_struct等结构比较比较。

    好了谈了这么多,我还是简单说说Windows 2000中的环境切换代码吧。

    那么Windows NT/2000什么情况下发生环境切换呢?曾见过一DDK FAQ中是这样描述的:

    Q:What are the causes of a context switch in Windows NT?
    A:There are only two ways that a thread context is switched.
        1.The thread yields it's quantum by blocking on something(event,semaphore,etc.).
        2.The time period is up.This is caused by a timer interrupt.

    KiDispatchInterrupt是NT/2000中定时进行环境切换例程,以下列出其部分代码:

    ;Linux中实现类似功能的代码位于/usr/src/linux/kernel/sched.c
    00403A58 KiDispatchInterrupt proc near
    00403A58
    00403A58 var_C = dword ptr -0Ch
    00403A58 var_8 = dword ptr -8
    00403A58 var_4 = dword ptr -4
    00403A58
    00403A58 mov ebx, ds:0FFDFF01Ch
    00403A5E lea eax, [ebx+800h]
    00403A64 cli
    00403A65 cmp eax, [eax]
    00403A67 jz short loc_403A86
    00403A69 push ebp
    00403A6A push dword ptr [ebx]
    00403A6C mov dword ptr [ebx], 0FFFFFFFFh
    00403A72 mov edx, esp
    00403A74 mov esp, [ebx+81Ch]
    00403A7A push edx
    00403A7B mov ebp, eax
    00403A7D call sub_460BA4
    00403A82 pop esp
    00403A83 pop dword ptr [ebx]
    00403A85 pop ebp
   
            .
            .(限于篇幅,此处略去部分,感兴趣的自己步步跟踪)
            .

    ;另CR3切换代码:

    ;Linux中实现此功能的代码位于/usr/src/linux/include/asm-i386/pgtable.h
    ;由宏定义SET_PAGE_DIR实现,请参考之。

    ;此时EDI存储KPEB(自己用SoftICE跟跟),执行后EAX则为进程Context
    ;这句结合mov cr3,eax是不是可以跟踪到CR3在KPEB的具体位置的呢,我就是从这儿跟踪到的。
    00403B87 mov eax, [edi+18h]
    00403B8A mov ebp, [ebx+40h]
    00403B8D mov ecx, [edi+30h]
    00403B90 mov [ebp+1Ch], eax
    00403B93 mov cr3, eax ;EAX->CR3
    00403B96 mov [ebp+66h], cx
    00403B9A xor eax, eax
    00403B9C cmp [edi+20h], ax
    ;转去错误处理,必要时还会调用KeBugCheck,出现可怕的蓝屏死机.
    00403BA0 jnz short loc_403BCE
    ;LDTR置空选择器
    00403BA2 lldt ax
    00403BA5 lea ecx, [ecx]
    00403BA7
    00403BA7 loc_403BA7: ; CODE XREF: .text:00403B7Dj
    00403BA7 ; .text:00403BFAj
    ;将Context Switches Times加一
    00403BA7 inc dword ptr [esi+4Ch]
    00403BAA inc dword ptr [ebx+5C0h]
    00403BB0 pop ecx
    00403BB1 mov [ebx], ecx
    00403BB3 cmp byte ptr [esi+49h], 0
    00403BB7 jnz short loc_403BBD
    00403BB9 popf
    00403BBA xor eax, eax
    00403BBC retn
    00403BBD loc_403BBD: ; CODE XREF: .text:00403BB7j
    00403BBD popf
    00403BBE jnz short loc_403BC3
    00403BC0 mov al, 1
    00403BC2 retn
    00403BC3 loc_403BC3: ; CODE XREF: .text:00403BBEj
    00403BC3 mov cl, 1
    00403BC5 call ds:HalRequestSoftwareInterrupt
    00403BCB xor eax, eax
    00403BCD retn

            .
            .(略)
            .


    部分代码我尚未进行注解,主要是一些代码与代码运行环境有关,如处理NT执行体的错误检查(包括有效性、安全性等),而且我这儿也未列出代码,如果你有兴趣的话用SoftICE步步跟踪可以发现很多NT内部机制。

    Windows 2000是抢占式多线程操作系统,文中并未涉及到线程调度的具体方法。真正线程调度切换,还要考虑很多因素,如线程状态(是否可调度等)、线程优先级(Priority)、线程的亲缘性(Affinity)等,这些具体的重要信息,也都由ntoskrnl.exe模块处理,我也不可能都详细的在此列出。本文只讨论如何获得这信息,不过若想知道得更多,仍可以根据文中的讨论对其进行进一步的分析。

    我曾经接触过单片机,也曾经对其似乎从头开始设计一个OS(可能只是几条指令,单片机高手千万别见笑)感到不解,但当我接触过NT Kernel后才觉得自己是多么的可笑。不过在接触NT内核时,可大量参照Linux代码,毕竟她们原理应该是一样的,虽然Linux不是一个微内核OS,而NT/2000是。个人认为linux的task_struct与NT的KPEB,linux中的Bottom half机制与NT的DPC(延时过程调用)等有其相似的地方(虽然在机制上实现方法上仍有很大的不同)。由于Microsoft未提供任何官方文档且NT内核的复杂性(曾有人批评NT的微内核比Linux还要大呢),本文所讨论的,我也不能保证其绝对的正确性,如果您发现任何错误之处或是有什么建议,请予以告之,谢谢!

    参考资料:
    1.Jeffrey Richter
        <<Programming Applications for Microsoft Windows,Fourth Edition>>
    2.Linux相关文档
    3.Mark Russinovich相关文档
    4.Intel Corp<<Intel Architecture Software Developer's Manual,Volume 3>>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值