博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
win10系统调用架构分析
阅读量:5952 次
发布时间:2019-06-19

本文共 26422 字,大约阅读时间需要 88 分钟。

1.  操作系统模型

大多数操作系统中,都会把应用程序和内核代码分离执行在不同的模式下。

内核模式訪问系统数据和硬件,应用程序执行在没有特权的模式下(用户模式),仅仅能使用有限的API,且不能直接訪问硬件。

当用户模式调用系统服务时。CPU执行一个特殊的指令以切换到内核模式(Ring0),当系统服务调用完毕时。操作系统切换回用户模式(Ring3)

Windows与大多数UNIX系统类似,驱动程序代码共享内核模式的内存空间,意味着不论什么系统组件或驱动程序都可能訪问其它系统组件的数据。可是,Windows实现了一套内核保护机制,比方PatchGuard和内核模式代码签名。

内核模式的组件尽管共享系统资源,但也不会互相訪问,而是通过传參数的方式来訪问或改动数据结构。

大多数系统代码用C写的是为了保证可移植性,C语言不是面向对象的语言结构。比方动态类型绑定,多态函数,类型继承等。可是。基于C的实现借鉴了面向对象的概念。但并不依赖面向对象。

2.  系统架构

下图是简化版的Windows系统架构实现:

首先注意那条横线将用户模式和内核模式分开两部分了。横线之上是用户模式的进程,以下是内核模式的系统服务。

服务进程和用户程序之下的子系统DLL”。在Windows下。用户程序不直接调用本地Windows服务。而是通过子系统DLL来调用。子系统DLL的角色是将文档化的函数翻译成调用的非文档化的系统服务(未公开的)。

内核模式的几个组件包含:

    • Windows运行实体。包含基础系统服务,比方内存管理器,进程和线程管理器。安全管理,I/O管理,网络。进程间通信。
    • Windows内核,包含底层系统函数。比方线程调度。中断,异常分发,多核同步。

      也提供了一些routine和实现高层结构的基础对象。

    • 设备驱动,包含硬件设备驱动(翻译用户I/O到硬件I/O)。软件驱动(比如文件和网络驱动)。
    • 硬件抽象层,独立于内核的一层代码,将设备驱动与平台的差异性分离开。

    • 窗体和图形系统,实现了GUI函数,处理用户接口和画图。

下表中是Windoows系统核心组件的文件名称:

文件名称

组件

Ntoskrnl.exe

运行体和内核

Ntkrnlpa.exe(32位才有)

支持PAE

Hal.dll

硬件抽象层

Win32k.sys

子系统的内核模式部分

Ntdll.dll

内部函数

KERNEL32.DLL,KERNELBASE.dll,USER32.dll, GDI32.dll

核心子系统的组件

在一个安装完毕的Windows操作系统中可见并有效的内核实现文件是:

C:\Windows\System32\ntoskrnl.exe

C:\Windows\System32\ntkrnlpa.exe

请注意有两个内核文件,当中第二个比第一个的名字少了os多了个pa,省去的os没有不论什么意义,可是多出来的pa所代表的意思是PAE(物理地址扩展)。这是X86CPU的一个硬件特性,Windows启动之后依据当前系统设置是否开启了PAE特性会自己主动选择把当中一个作为内核载入到内存中。

为什么加了这么多限定词,由于ntoskrnl.exe这个文件名称并不一定是这个文件的真实名称,能够从文件属性中看到:

ntoskrnl.exe原始文件名称为可能为ntoskrnl.exe或者ntkrnlmp.exe

ntkrnlpa.exe原始文件名称为可能为ntkrnlpa.exe或者ntkrpamp.exe

能够发现当中的不同之处就是mp,mp就是Multi-processor(多处理器。也能够理解为多核,由于IA-32架构对多核处理器的编程和多处理器的编程是相似的机制)。为什么会出现这中情况呢?由于这全然是由计算机硬件的不同配置导致的。当安装Windows操作系统的时候,Windows安装程序会自己主动检測机器的CPU特性,依据CPU的核心数来确定使用哪一套内核。假设是单核心就仅仅复制ntkrnlpa.exe和ntoskrnl.exe到系统文件夹下。假设是多核心就复制ntkrnlpamp.exe和ntoskrnlmp.exe到系统文件夹下。所以假设你有一台单核心CPU的机器,有一天你换了双核的CPU却没有又一次安装操作系统。那么你就不会在看到熟悉的Windows启动画面了。类似这两个文件的另一个文件C:\Windows\System32\hal.dll。这是Windows的硬件抽象层程序文件,这个就不做详细介绍了。

注意:因为在跟踪分析系统内核调用的时候须要导入对应的符号文件以及对函数偏移位置等进行分析。因此须要知道自己系统上内核文件的原始文件名称。

3.  系统服务调用机制

对于应用程序进程来说,操作系统内核的作用体如今一组可供调用的函数。称为系统调用(也成"系统服务")。

从程序执行的角度来看,进程是主动、活性的,是发出调用请求的一方;而内核是被动的,仅仅是应进程要求而提供服务。

从整个系统执行角度看,内核也有活性的一面,详细体如今进程调度。

系统调用所提供的服务(函数)是执行在内核中的。也就是说。在"系统空间"中。而应用软件则都在用户空间中,二者之间有着空间的间隔(CPU执行模式不同)。

综上所述,应用软件若想进行系统调用,则应用层和内核层之间,必须存在"系统调用接口"。即一组接口函数,这组接口执行于用户空间。

对于windows来说,其系统调用接口并不公开,公开是的一组对系统调用接口的封装函数,称为windowsAPI。

用户空间与系统空间所在的内存区间不一样,相同。对于这两种区间,CPU的执行状态也不一样。

在用户空间中,CPU处于"用户态"。在系统空间中。CPU处于"系统态"。

CPU从系统态进入用户态是easy的,由于能够运行一些系统态特有的特权指令。从而进入用户态。

而相反,用户态进入系统态则不easy。由于用户态是无法运行特权指令的。

所以。一般有三种手段,使CPU进入系统态(即转入系统空间运行)

①    中断:来自于外部设备的中断请求。

当有中断请求到来时。CPU自己主动进入系统态,并从某个预定地址開始运行指令。中断仅仅发生在两条指令之间。不影响正在运行的指令。

②    异常:不管是在用户空间或系统空间。运行指令失败时都会引起异常。CPU会因此进入系统态(假设原先不在系统空间)。从而在系统空间中对异常做出处理。异常发生在运行一条指令的过程中,所以当前运行的指令已经半途而废了。

③    自陷:以上两种都CPU被动进入系统态。

而自陷是CPU通过自陷指令主动进入系统态。多数CPU都有自陷指令,系统调用函数一般都是靠自陷指令实现的。一条自陷指令的作用相当于一次子程序调用。子程序存在于系统空间

4.  Windows使用系统调用的方法

4.1. 通过自陷实现系统调用

Windows API假设设涉及到系统调用就要由RING3进入RING0,这就牵扯到了X86保护模式下有特权级变化的控制转移。

在早期的CPU中(Pentium II之前),没有高速系统调用这个机制。所以能用来进行特权级变化的控制转移的机制仅仅有通过自陷实现(非常多书或网络上也常常称为中断方式),保护模式下的中断的实现方式是通过IDT表来实现。IDT表中存放的是一种特殊的X86段描写叙述符——门描写叙述符。门描写叙述符的格式例如以下:

能够看到当中有一个Selector字段和一个Offset字段,而且是不连续的,这里仅仅介绍这两个字段的含义,其它字段的含义这里不再赘述,有兴趣的话能够自己去看下保护模式相关资料。

说究竟这个门描写叙述符的作用就是描写叙述一个程序段,对我们来说重要的就是Selector和Offset字段了。由于Selector能够帮我们找到它所描写叙述的程序的【段】,Offset就是程序在【段】内的【偏移】,有了【段】和【偏移】就能够确定程序的线性地址。

在Win10 X64操作系统中IDT表的结构又有些不一样。详细的结构能够用WinDbg获得,详细指令及结果例如以下:

 

kd> dt_KIDTENTRY64

ACPI!_KIDTENTRY64

   +0x000 OffsetLow        : Uint2B

   +0x002 Selector         : Uint2B

   +0x004 IstIndex         : Pos 0, 3 Bits

   +0x004 Reserved0        : Pos 3, 5 Bits

   +0x004 Type             : Pos 8, 5 Bits

   +0x004 Dpl              : Pos 13, 2 Bits

   +0x004 Present          : Pos 15, 1 Bit

   +0x006 OffsetMiddle     : Uint2B

   +0x008 OffsetHigh       : Uint4B

   +0x00c Reserved1        : Uint4B

   +0x000 Alignment        : Uint8B

kd> r idtr

idtr=fffff801b88ca070

kd> dt_KIDTENTRY64 fffff801b88ca070

ACPI!_KIDTENTRY64

   +0x000 OffsetLow        : 0x7500

   +0x002 Selector         : 0x10

   +0x004 IstIndex         : 0y000

   +0x004 Reserved0        : 0y00000 (0)

   +0x004 Type             : 0y01110 (0xe)

   +0x004 Dpl              : 0y00

   +0x004 Present          : 0y1

   +0x006OffsetMiddle     : 0xb6d5

   +0x008 OffsetHigh       : 0xfffff801

   +0x00c Reserved1        : 0

   +0x000 Alignment        : 0xb6d58e00`00107500

 

在使用这样的机制的windows系统中,系统调用2E号中断。进入了系统内核。

一般在中断调用前都会初始化一个系统服务号;也叫做分发 ID,该 ID 须要在运行 int 2Eh 前,载入到EAX 寄存器,以便在切换到内核模式的时候调用对应的内核函数来完毕对应的功能。

粗略地讲,INT 指令在内部涉及例如以下几个操作:

1)   清空陷阱标志(TF),和中断同意标志(IF);

2)   依序把(E)FLAGS,CS,(E)IP 寄存器中的值压入栈上;

3)   转移到 IDT 中的中断门描写叙述符记载的对应 ISR(中断服务例程)的起始地址;

4)   运行 ISR。直至遇到 IRET 返回。

最关键的第3步涉及“段间”转移,通过中断门描写叙述符,可以引用一个 Ring0 权限代码段。该代码段相应的 64 位段描写叙述符(存储在 GDT 中)中的 DPL 位,即特权级位等于0(0=Ring0。3=Ring3,即便由 Intel 规定的段描写叙述符的 DPL 位有4种取值。但 Windows 仅使用了当中的最高特权级 Ring0 与最低特权级 Ring3。整体而言。用户模式应用程序位于  Ring3 代码或数据段。内核与设备驱动程序则位于 Ring0 代码或数据段 ),再结合段描写叙述符中的“基址”与中断门描写叙述符中的“偏移”。就能计算出 ISR在 Ring0 代码段中的起始地址。下表是64位段描写叙述符的格式,取自 Intel 文档。自行加入了翻译:

 

我们知道了系统调用了2E号中断,从而进入了系统内核。知道了中断号以下我们要做的就是找到这个中断的服务程序,也就是RING3进入到RING0之后的第一条指令在哪里。

以下就进入内核调试模式。因为IDT是由IDTR指定的,这里用WINDBG进行手工分析:

1)  X86模式下:

 

0: kd> r idtr

idtr=8003f400

 

这个IDT有多大呢?

0: kd> r idtl

idtl=000007ff

 

事实上大小就是这个数加一。地址找到了。大小找到了。关键是这个是啥结构,IDT长啥样呢?

 

0: kd> dt _KIDTENTRY

ntdll!_KIDTENTRY

   +0x000Offset      : Uint2B

   +0x002Selector     : Uint2B

   +0x004Access      : Uint2B

   +0x006ExtendedOffset  : Uint2B

 

就是这个结构的数组。

以下看看第一个成员。

 

0: kd> dt _KIDTENTRY 8003f400

ntdll!_KIDTENTRY

   +0x000Offset      : 0x3360

   +0x002Selector     : 8

   +0x004Access      : 0x8e00

   +0x006ExtendedOffset  : 0x8054

 

这个结构的详细的含义。请看前面对中断门描写叙述符的解释或查看Intel的手冊及者相关的资料。经过计算得出地址是:0x80543360

验证的方式之中的一个:

1

2

3

4

5

6

7

8

9

10

0: kd> u 0x80543360

nt!KiTrap00:

80543360 6a00      push    0

80543362 66c74424020000 mov    word ptr [esp+2],0

80543369 55       push    ebp

8054336a 53       push    ebx

8054336b 56       push    esi

8054336c 57       push    edi

8054336d 0fa0      push    fs

8054336f bb30000000   mov    ebx,30h

看到了吧。显示的是正确的。

还有一个办法是:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

0: kd> !idt -a

 

Dumping IDT: 8003f400

 

8cde863500000000:   80543360 nt!KiTrap00

8cde863500000001:   805434dc nt!KiTrap01

8cde863500000002:   Task Selector = 0x0058

8cde863500000003:   805438f0 nt!KiTrap03

8cde863500000004:   80543a70 nt!KiTrap04

8cde863500000005:   80543bd0 nt!KiTrap05

8cde863500000006:   80543d44 nt!KiTrap06

8cde863500000007:   805443bc nt!KiTrap07

8cde863500000008:   Task Selector = 0x0050

8cde863500000009:   805447c0 nt!KiTrap09

8cde86350000000a:   805448e0 nt!KiTrap0A

8cde86350000000b:   80544a20 nt!KiTrap0B

8cde86350000000c:   80544c80 nt!KiTrap0C

8cde86350000000d:   80544f6c nt!KiTrap0D

8cde86350000000e:   8054568c nt!KiTrap0E

8cde86350000000f:   8054590c nt!KiTrap0F

8cde863500000010:   80545a2c nt!KiTrap10

8cde863500000011:   80545b68 nt!KiTrap11

 

2)  X64模式下:

首先查看IDTRIDTL

kd> r idtr

idtr=fffff801b88ca070

kd> r idtl

idtl=0fff

64位系统中使用的结构是_KIDTENTRY64

kd> dt _KIDTENTRY64

ACPI!_KIDTENTRY64

  +0x000 OffsetLow        : Uint2B

  +0x002 Selector         : Uint2B

  +0x004 IstIndex         : Pos 0, 3Bits

  +0x004 Reserved0        : Pos 3, 5Bits

  +0x004 Type             : Pos 8, 5Bits

  +0x004 Dpl              : Pos 13,2 Bits

  +0x004 Present          : Pos 15,1 Bit

  +0x006 OffsetMiddle     : Uint2B

  +0x008 OffsetHigh       : Uint4B

  +0x00c Reserved1        : Uint4B

  +0x000 Alignment        : Uint8B

kd> dt _KIDTENTRY64 fffff801b88ca070

ACPI!_KIDTENTRY64

  +0x000 OffsetLow        : 0x7500

  +0x002 Selector         : 0x10

  +0x004 IstIndex         : 0y000

  +0x004 Reserved0        : 0y00000(0)

  +0x004 Type             : 0y01110(0xe)

  +0x004 Dpl              : 0y00

   +0x004 Present          : 0y1

  +0x006 OffsetMiddle     : 0xb6d5

  +0x008 OffsetHigh       :0xfffff801

  +0x00c Reserved1        : 0

  +0x000 Alignment        :0xb6d58e00`00107500

查看IDT服务表

kd> !idt

 

Dumping IDT: fffff801b88ca070

 

00: fffff801b6d57500nt!KiDivideErrorFault

01: fffff801b6d57600nt!KiDebugTrapOrFault

02: fffff801b6d577c0nt!KiNmiInterrupt Stack =0xFFFFF801B88E5000

03: fffff801b6d57b40nt!KiBreakpointTrap

04: fffff801b6d57c40nt!KiOverflowTrap

05: fffff801b6d57d40nt!KiBoundFault

06: fffff801b6d57fc0nt!KiInvalidOpcodeFault

07: fffff801b6d58200nt!KiNpxNotAvailableFault

08: fffff801b6d582c0nt!KiDoubleFaultAbort Stack =0xFFFFF801B88E3000

09: fffff801b6d58380nt!KiNpxSegmentOverrunAbort

0a: fffff801b6d58440nt!KiInvalidTssFault

0b: fffff801b6d58500nt!KiSegmentNotPresentFault

0c: fffff801b6d58640nt!KiStackFault

0d: fffff801b6d58780nt!KiGeneralProtectionFault

0e: fffff801b6d58880nt!KiPageFault

10: fffff801b6d58c40nt!KiFloatingErrorFault

11: fffff801b6d58dc0nt!KiAlignmentFault

12: fffff801b6d58ec0nt!KiMcheckAbort  Stack =0xFFFFF801B88E7000

13: fffff801b6d59540nt!KiXmmException

1f: fffff801b6d52890nt!KiApcInterrupt

20: fffff801b6d56c10nt!KiSwInterrupt

29: fffff801b6d59700nt!KiRaiseSecurityCheckFailure

2c: fffff801b6d59800nt!KiRaiseAssertion

2d: fffff801b6d59900nt!KiDebugServiceTrap

2f: fffff801b6d52b60nt!KiDpcInterrupt

30: fffff801b6d52d90nt!KiHvInterrupt

31: fffff801b6d530f0nt!KiVmbusInterrupt0

32: fffff801b6d53440nt!KiVmbusInterrupt1

33: fffff801b6d53790nt!KiVmbusInterrupt2

34: fffff801b6d53ae0nt!KiVmbusInterrupt3

35: fffff801b6d51718hal!HalpInterruptCmciService (KINTERRUPT fffff801b7425cb0)

50: fffff801b6d517f0USBPORT!USBPORT_InterruptService (KINTERRUPT ffffd001fee5c640)

60: fffff801b6d51870VBoxGuest+0x1290 (KINTERRUPT ffffd001fee5cb40) 

70: fffff801b6d518f0storport!RaidpAdapterInterruptRoutine (KINTERRUPT ffffd001fee5cc80)                      HDAudBus!HdaController::Isr(KINTERRUPT ffffd001fee5c780)  

80: fffff801b6d51970i8042prt!I8042MouseInterruptService (KINTERRUPT ffffd001fee5c8c0)

90: fffff801b6d519f0i8042prt!I8042KeyboardInterruptService (KINTERRUPT ffffd001fee5ca00)

a0: fffff801b6d51a70serial!SerialCIsrSw (KINTERRUPT ffffd001fee5c500) 

b0: fffff801b6d51af0ACPI!ACPIInterruptServiceRoutine (KINTERRUPT ffffd001fee5cdc0) 

b1: fffff801b6d51af8dxgkrnl!DpiFdoLineInterruptRoutine (KINTERRUPT ffffd001fee5c3c0) 

ce: fffff801b6d51be0hal!HalpIommuInterruptRoutine (KINTERRUPT fffff801b74266b0)

d1: fffff801b6d51bf8hal!HalpTimerClockInterrupt (KINTERRUPT fffff801b74264b0) 

d2: fffff801b6d51c00hal!HalpTimerClockIpiRoutine (KINTERRUPT fffff801b74263b0) 

d7: fffff801b6d51c28hal!HalpInterruptRebootService (KINTERRUPT fffff801b74261b0) 

d8: fffff801b6d51c30hal!HalpInterruptStubService (KINTERRUPT fffff801b7425fb0) 

df: fffff801b6d51c68hal!HalpInterruptSpuriousService (KINTERRUPT fffff801b7425eb0) 

e1: fffff801b6d53e30nt!KiIpiInterrupt

e2: fffff801b6d51c80hal!HalpInterruptLocalErrorService (KINTERRUPT fffff801b74260b0)

e3: fffff801b6d51c88hal!HalpInterruptDeferredRecoveryService (KINTERRUPT fffff801b7425db0) 

fd: fffff801b6d51d58hal!HalpTimerProfileInterrupt (KINTERRUPT fffff801b74265b0)

 fe: fffff801b6d51d60hal!HalpPerfInterrupt (KINTERRUPT fffff801b74262b0)

能够看到在X64环境下INT 2E中断服务表已经没有能够导出的服务了,在使用!idt –a指令查看。能够考到2E中断服务表的内容为:

2e: fffff801b6d516e0nt!KiIsrThunk+0x170

通过下一个章节的分析能够看到在Win10 X64系统中实际是使用高速系统调用机制。

 

4.2. 使用高速系统调用机制

从Pentium II系列開始的CPU引入了高速系统调用这一特性,添加了两条指令SYSENTER和SYSEXIT(AMD CPU中的指令为SYSCALL和SYSRET,在Intel 64 CPU中也用SYSCALL和SYSRET),Windows 10 X64系统使用的是SYSCALL和SYSRET指令。

这一机制的实现就是专门用于解决操作系统的系统调用的性能问题的。这样的机制实现的控制转移比中断系统要快非常多,由于转移的目标地址是存放在MSR寄存器内。而中断实现的系统调用目标地址存放在内存中的IDT中,所以能提高运行速度。

windows 10 x64 版本号会使用 processor 提供的 syscall/sysret 指令来构造一个高速的调用系统服务例程机制。实际上他就是在上一节自陷模式中调用int 2Eh/IRET指令的地方使用了syscall/sysret指令,同一时候在调用用指令前对对应的寄存器进行现场保护和初始化。

当中在EAX寄存器中存放了系统服务号。Syscall指令有一个统一的内核服务程序入口,他的内核服务程序入口存放在MSR_LSTAR 寄存器中。我们要想看 syscall 指令进入哪里,能够查看 MSR_LSTAR 寄存器的值,在 windbg 的内核调试模式下,使用 rdmsr 命令观察 MSR_LSTAR 寄存器值,比如:

 

kd> rdmsr c0000082

msr[c0000082] = fffff801`b6d59d00

上面的结果显示 fffff801`b6d59d00 就是 MSR_LSTAR 里的值,它是 syscall 指令的进入点。

当用户模式的程序运行Syscall指令后CPU将切换到内核模式,并開始运行内核服务程序入口的指令,同一时候依据系统服务号调用内核对应的服务函数,完毕后返回到用户模式。实际上入口函数为nt!KiSystemCall64:

 

kd> uffffff801`b6d59d00

Flow analysis wasincomplete, some code may be missing

nt!KiSystemCall64:

fffff801`b6d59d000f01f8                  swapgs

fffff801`b6d59d03654889242510000000      mov     qword ptr gs:[10h],rsp

fffff801`b6d59d0c65488b2425a8010000      mov     rsp,qword ptr gs:[1A8h]

fffff801`b6d59d15 6a2b                    push    2Bh

fffff801`b6d59d1765ff342510000000        push    qword ptr gs:[10h]

fffff801`b6d59d1f4153                    push    r11

fffff801`b6d59d216a33                    push    33h

fffff801`b6d59d2351                      push    rcx

fffff801`b6d59d24498bca                  mov     rcx,r10

fffff801`b6d59d274883ec08                sub     rsp,8

fffff801`b6d59d2b55                      push    rbp

fffff801`b6d59d2c4881ec58010000          sub     rsp,158h

fffff801`b6d59d33 488dac2480000000        lea    rbp,[rsp+80h]

fffff801`b6d59d3b48899dc0000000          mov     qword ptr [rbp+0C0h],rbx

fffff801`b6d59d424889bdc8000000          mov     qword ptr [rbp+0C8h],rdi

fffff801`b6d59d494889b5d0000000          mov     qword ptr [rbp+0D0h],rsi

fffff801`b6d59d50c645ab02                mov     byte ptr [rbp-55h],2

fffff801`b6d59d5465488b1c2588010000      mov     rbx,qword ptr gs:[188h]

fffff801`b6d59d5d0f0d8b90000000          prefetchw[rbx+90h]

fffff801`b6d59d640fae5dac                stmxcsr dword ptr [rbp-54h]

fffff801`b6d59d68650fae142580010000      ldmxcsr dword ptrgs:[180h]

fffff801`b6d59d71807b0300                cmp     byte ptr [rbx+3],0

fffff801`b6d59d7566c785800000000000      mov     word ptr [rbp+80h],0

fffff801`b6d59d7e0f849a000000            je      nt!KiSystemServiceUser+0xce(fffff801`b6d59e1e)  Branch

前面提到,运行 SYSCALL 前,须要载入一个系统服务号到 EAX 寄存器,系统服务号的作用就是提供给 KiSystemCall64 ()在 KeServiceDescriptorTable 或 KeServiceDescriptorTableShadow中索引对应系统服务的入口地址并调度运行。

在WindowsNT系列操作系统中,有两种类型的系统服务,一种实如今内核文件里,是经常使用的系统服务;还有一种实如今win32k.sys中。是一些与图形显示及用户界面相关的系统服务。这些系统服务在系统运行期间常驻于系统内存区中,而且他们的入口地址保存在两个系统服务地址表KiServiceTable和Win32pServiceTable中。

而每一个系统服务的入口參数所用的总字节数则分别保存在另外两个系统服务參数表(ArgumentTable)中。

系统服务地址表和系统參数表是一一相应的,每一个系统服务表(一下简称SST)都指向一个地址表和一个參数表。在Windows 2000/xp/7系统中,仅仅有两个SST。

一个SST指向了KiServiceTable,而还有一个SST则指向了Win32pServiceTable.

全部的SST都保存在系统服务描写叙述表(SDT)中。系统中一共同拥有两个SDT,一个是ServiceDescriptorTable,还有一个是ServiceDescriptorTableShadow。ServiceDescriptor中仅仅有指向KiServiceTable的SST。而ServiceDescriptorTableShadow则包括了全部的两个SST。SSDT是能够訪问的。而SSDTShadow是不公开的。

windows内核文件导出了一个公开的变量KeServiceDecriptorTable,它指向了SSDT。在内核程序中能够直接使用这个变量,通过数据结构之间的关系,找到KiServiceTable,然后从KiServiceTable中查找不论什么一个系统服务的入口地址。

以下是关于这些数据结构的示意图:

比如:在用户模式下用户应用程序调用CreateFile这个系统api。在Win10 X64系统中通过指令SYSCALL切换至内核模式,同一时候在EAX寄存器中设置系统服务号为0x55h,在内核模式下通过KiSystemCall64查找SSDT服务表。并调用对应的内核函数,在Windbg下能够看到例如以下内容:

 

kd> rdmsrc0000082

msr[c0000082] =fffff802`7f7d4d00

kd> ufKiSystemCall64

Flow analysis wasincomplete, some code may be missing

nt!KiSystemCall64:

fffff802`7f7d4d000f01f8                  swapgs

fffff802`7f7d4d03654889242510000000      mov     qword ptr gs:[10h],rsp

fffff802`7f7d4d0c65488b2425a8010000      mov     rsp,qword ptr gs:[1A8h]

fffff802`7f7d4d156a2b                    push    2Bh

fffff802`7f7d4d17 65ff342510000000        push   qword ptr gs:[10h]

fffff802`7f7d4d1f4153                    push    r11

fffff802`7f7d4d216a33                    push    33h

fffff802`7f7d4d2351                      push    rcx

fffff802`7f7d4d24498bca                  mov     rcx,r10

fffff802`7f7d4d274883ec08                sub     rsp,8

fffff802`7f7d4d2b55                      push    rbp

fffff802`7f7d4d2c4881ec58010000          sub     rsp,158h

fffff802`7f7d4d33488dac2480000000        lea     rbp,[rsp+80h]

fffff802`7f7d4d3b48899dc0000000          mov     qword ptr [rbp+0C0h],rbx

fffff802`7f7d4d424889bdc8000000          mov     qword ptr [rbp+0C8h],rdi

fffff802`7f7d4d494889b5d0000000          mov     qword ptr [rbp+0D0h],rsi

fffff802`7f7d4d50c645ab02                mov    byte ptr [rbp-55h],2

fffff802`7f7d4d5465488b1c2588010000      mov     rbx,qword ptr gs:[188h]

fffff802`7f7d4d5d0f0d8b90000000          prefetchw[rbx+90h]

fffff802`7f7d4d640fae5dac                stmxcsr dword ptr[rbp-54h]

fffff802`7f7d4d68 650fae142580010000      ldmxcsr dword ptr gs:[180h]

fffff802`7f7d4d71807b0300                cmp     byte ptr [rbx+3],0

fffff802`7f7d4d7566c785800000000000      mov     word ptr [rbp+80h],0

fffff802`7f7d4d7e0f849a000000            je      nt!KiSystemServiceUser+0xce(fffff802`7f7d4e1e)

 ---------------------------------------省略若干代码------------------------------------

nt!KiSystemServiceRepeat:

fffff802`7f7d4e444c8d1535292300          lea     r10,[nt!KeServiceDescriptorTable(fffff802`7fa07780)]

fffff802`7f7d4e4b4c8d1dee282300          lea     r11,[nt!KeServiceDescriptorTableShadow(fffff802`7fa07740)]

fffff802`7f7d4e52f7437840000000          test    dword ptr [rbx+78h],40h

fffff802`7f7d4e594d0f45d3                cmovne  r10,r11

fffff802`7f7d4e5d423b441710              cmp     eax,dword ptr [rdi+r10+10h]

fffff802`7f7d4e620f83ef020000            jae     nt!KiSystemServiceExit+0x1ac(fffff802`7f7d5157)

 

如上红色代码。

在KiSystemServiceRepeat里面找到了KeServiceDescriptorTable ,之后我们看一下KeServiceDescriptorTable:

 

kd> dq KeServiceDescriptorTable

fffff802`7fa07780  fffff802`7f94a150 00000000`00000000

fffff802`7fa07790  00000000`000001bc fffff802`7f94af34

fffff802`7fa077a0  00000000`00000000 00000000`00000000

fffff802`7fa077b0  00000000`00000000 00000000`00000000

fffff802`7fa077c0  00000000`00000000 00000000`00f54d67

fffff802`7fa077d0  00007ffb`a8178b70 ffffe001`65e87d30

fffff802`7fa077e0  ffffe001`65e92dc0 00000000`00000000

fffff802`7fa077f0  00000000`00000000 00000000`00000001

 

kd> ddfffff802`7f94a150

fffff802`7f94a150  fdbeb004 fe0f4600 01930742 0365ad00

fffff802`7f94a160  01530300 fe832200 01258905 01477b06

fffff802`7f94a170  0126ce05 01a6d001 01ac7600 01307e80

fffff802`7f94a180  01992b00 0128df00 01550300 0111da00

fffff802`7f94a190  01a65601 01497a01 01368700 013c1302

fffff802`7f94a1a0  015b9700 01b7bf80 013f3801 013fbb02

fffff802`7f94a1b0  011ad302 0130e201 01a84801 01a02545

fffff802`7f94a1c0  01694e00 01359b43 0118cf00 035d5b00

 

KeServiceDescriptorTable基地址:fffff802`7fa07780,ServiceTable的基地址为**KeServiceDescriptorTable = fffff802`7f94a150

公式:

ServiceTableBase = **KeServiceDescriptorTable

ServiceTableBase[Index] = ServiceTableBase + Index * 4

ServiceAddress = ServiceTableBase[Index] >> 4 + ServiceTableBase

通过公式,我们能够推算CreateFile这个服务调用的服务地址为(CreateFile的服务号为0x55h):

1)  ServiceTableBase[Index]  = 0xfffff802`7f94a150 + 0x55 * 4

= 0x FFFFF8027F94A2A4

2)  ServiceTableBase[Index] >> 4= [0xFFFFF8027F94A2A4] >>4 = 0x01367507 >> 4 = 0x0136750

kd> ddFFFFF8027F94A2A4

fffff802`7f94a2a4  01367507 01b3c141 035d77c2 0135fd80

fffff802`7f94a2b4  01b7944c 03a756c0 01a60001 01a52600

fffff802`7f94a2c4  01a00a00 fd94b700 01f70b41 010c0202

fffff802`7f94a2d4  fe17e440 fe336b03 fe0f19c7 ff3873c7

fffff802`7f94a2e4  038ee8cc 038ef34d 018c3640 03ad5b40

fffff802`7f94a2f4  03ad5d40 0211e2c2 0283e24c 03808c40

fffff802`7f94a304  03809d00 01258280 01ab9b00 00f056c0

fffff802`7f94a314  0364a140 01ce0180 019480c5 019a7800

3)     ServiceAddress = ServiceTableBase[Index] >> 4 + ServiceTableBase

= 0x0136750 + 0xfffff802`7f94a150

= 0xFFFFF8027FA808A0

 

kd> uFFFFF8027FA808A0

nt!NtCreateFile:

fffff802`7fa808a04881ec88000000          sub     rsp,88h

fffff802`7fa808a733c0                    xor     eax,eax

fffff802`7fa808a94889442478              mov     qword ptr [rsp+78h],rax

fffff802`7fa808aec744247020000000        mov     dword ptr [rsp+70h],20h

fffff802`7fa808b689442468                mov     dword ptr [rsp+68h],eax

fffff802`7fa808ba4889442460              mov     qword ptr [rsp+60h],rax

fffff802`7fa808bf89442458                mov     dword ptr [rsp+58h],eax

fffff802`7fa808c38b8424e0000000          mov     eax,dword ptr [rsp+0E0h]

 

kd> xnt!NtCreateFile

fffff802`7fa808a0nt!NtCreateFile (<no parameter info>)

 

能够看到通过SSDT服务表的查找,内核正确的找到了服务函数。

 

5.  Win10 X64系统调用跟踪分析

5.1. 研究測试系统环境搭建

本文主要研究Windows 10 X64系统的系统调用框架。主要使用Visual Studio 2015及WinDbg工具(通过VS2015集成环境使用),详细的环境搭建參考本人的另外一篇博文《》,链接地址:中新建一个项目syscalltest(Visual C++-->Windows-->空白应用通用Windows),准备一段VC測试代码。代码中通过使用Windows API来实现系统调用。我们通过VS2015的调试功能来跟踪系统调用实现的流程及框架。

代码的内容例如以下:

main.cpp内容:

 

#include "stdafx.h"

#include <windows.h>

#include "iostream"

 

using namespace std;

 

int APIENTRY _tWinMain(HINSTANCE hInstance,

    HINSTANCEhPrevInstance,

    LPTSTR    lpCmdLine,

    int       nCmdShow)

{

   

    HANDLE hFile= CreateFile("Hello.txt", GENERIC_READ, 0, NULL, OPEN_ALWAYS, 0,NULL);

    if ( hFile ==INVALID_HANDLE_VALUE )

    {

 

       OutputDebugString("无法打开文件!\n" );

       return -1;

    }

    charbuffer[1024];

    DWORDdwActualRead = 0;

    BOOL bRet =ReadFile(hFile, buffer, 1024, &dwActualRead, NULL);

    if ( !bRet )

    {

 

       OutputDebugString("无法读取文件!\n" );

       return -1;

    }

    CloseHandle(hFile);

    buffer[dwActualRead]= 0;

    MessageBox(NULL,TEXT(buffer), TEXT("Hello"), 0);

    return 0;

}

 

通过跟踪代码中使用的CreateFile系统API来发现Windows 10 X64系统是怎么调用内核实现的过程。

注意:

1)       在工具菜单—》选项窗体中设置调试属性窗中不要勾选--启用“仅我的代码”。假设此项勾选则在单步跟踪时不可以步入到windowsAPI内部。

2)       勾选“源码不可用时显示反汇编”

3)       设置好符号文件位置

 

 

5.2. 分析跟踪过程

5.1.1. 用户模式的调用过程

1)  在VS2015 IDE环境中将刚才准备的syscalltest项目设置为X64。Debug方式。然后编译生成项目。

2)  開始调试项目,在语句HANDLE hFile = CreateFile("Hello.txt", GENERIC_READ, 0, NULL,OPEN_ALWAYS, 0, NULL);处设置断点。程序执行到此断点停下。打开调试菜单,在窗体项目上打开反汇编窗体,然后按F11键,单步步入API函数的汇编代码,调试界面例如以下(L注意:因为WIN10的系统保护机制。每次程序的载入地址可能都不一样)。

继续按F11键单步执行,到第一个Call qword ptr[_imp_CreateFileA(地址xxxxxh)],从截图中标注点能够看出这里还是实在应用程序空间,按F11键单步步入。

3)       从以下的截图能够看出这时程序已经步入到应用层的KERNEL32.DLL系统函数空间。

通过DLL Export Viewer工具打开KERNEL32.DLL文件,能够看到在KERNEL32.DLL文件里CreateFileA方法的相对偏移地址为0x0002d8a0 ,在VS2015的模块窗体中能够看到本次KERNEL32.DLL的基地址为0x00007FFF836A0000,则CreateFileA在内存中的地址为:0x00007FFF836A0000 + 0x0002d8a0 = 0x00007FFF836CD8A0, 这和VS汇编窗体中的汇编指令行地址是一致的。我们继续F11单步跟踪步入。

4)       这个时候程序又跳到应用层的KERNELBASE.DLL系统函数空间。

通过DLL Export Viewer工具打开KERNELBASE.DLL文件,能够看到在KERNELBASE.DLL文件里CreateFileA方法的相对偏移地址为0x00060d90    。在VS2015的模块窗体中能够看到本次KERNELBASE.DLL的基地址为0x00007FFF80D90000,则CreateFileA在内存中的地址为:0x00007FFF80D90000 + 0x00060d90 = 0x00007FFF80DF0D90, 这和VS汇编窗体中的汇编指令行地址是一致的。

我们继续F10单步执行,直到汇编指令Call  CreateFileW(0x00007FFF80DAA0F0h)。在按键F11单步跟踪步入。这个时候程序又跳到KERNELBASE.DLL动态库的CreateFileW函数空间。通过DLL Export Viewer工具打开KERNELBASE.DLL文件,能够看到在KERNELBASE.DLL文件里CreateFileW方法的相对偏移地址为0x0001a0f0   ,在VS2015的模块窗体中能够看到本次KERNELBASE.DLL的基地址为0x00007FFF80D90000。则CreateFileA在内存中的地址为:0x00007FFF80D90000 + 0x0001a0f0 = 0x00007FFF80DAA0F0, 这和VS汇编窗体中的汇编指令行地址是一致的。

继续按F10键单步执行,直到汇编语句call  CreateFileInternal。按F11键单步跟踪步入调用函数,CreateFileInternal是KERNELBASE.DLL动态库的内部函数,他是在为进行内核系统调用初始化对应的数据。我们能够一直F10单步执行到汇编指令会执行到call qword ptr[__imp_NtCreateFile (07FF922EFC1A8h)] (注意:每次跟踪可能模块载入的地址会都不一样)

再按F11键单步跟踪步入函数。这次指令来到了NTDLL.DLL动态库的地址空间。而且看到了syscall指令和int 2Eh指令。也就是说我们来到了用户模式和内核模式交界的地方,在NTDLL的NtCreateFile函数内初始化EAX为0x55H系统服务号。同一时候推断执行的系统环境是要使用INT 2Eh中断调用方式,依据跟踪在我的系统上使用的是SYSCALL系统调用的方式。

通过DLL Export Viewer工具打开NTDLL.DLL文件。能够看到在NTDLL.DLL文件里NtCreateFile方法的相对偏移地址为0x000a5b60  。在VS2015的模块窗体中能够看到本次KERNELBASE.DLL的基地址为0x00007FFF844F0000,则NtCreateFile在内存中的地址为:0x00007FFF844F0000 + 0x000a5b60 = 0x00007FFF84595B60, 这和VS汇编窗体中的汇编指令行地址是一致的。

这个时候能够将窗体视图切换到调用堆栈视图,这时能够看到当前的函数调用堆栈,效果例如以下图:

到此我们已经跟踪了所实用户模式下API的调用过程,SYSCALL指令后系统将切换到内核模式,后面我们将分析在内核模式下系统调用过程是怎么样工作的。

5.1.2. 内核模式的调用过程

内核模式下我们建议採用双机调试方式或虚拟机的方式进行调试。详细的环境搭建參考本人的另外一篇博文《》。链接地址:链接地址:

执行生成的执行文件,这时在測试计算机上弹出了一个消息窗体。

在主计算机的VS2015的调试菜单中—》选择附加到进程--》在传输(P)下拉框中选择Windows Kernel Model Debugger。在限定符(Q)下拉框中选择刚才配置的測试目标主机名称—》在可用进程中选择Kernel—》最后点击附加button。

 

在VS2015打开调试窗体后。点击工具栏上调试菜单中的所有中断button(菜单),在命令行中执行下面命令:

kd> .symfix

kd> !sym noisy

noisy mode - symbolprompts off

kd>.reload /f

这时符号载入成功。接下来我们须要找到syscalltest.exe进程的一些信息,以确保我们仅仅在这个进程里断下,而不是在每一个进程都断下。

而我们须要寻找的信息是一个指向EPROCESS结构的指针。

该EPROCESS结构是用来表示一个进程的主内核数据结构。你能够看到包括“DT _EPROCESS”(在EPROCESS结构dump类型)的信息。为了找到一个给定的过程的EPROCESS结构。我们能够调用!Process扩展命令。该扩展命令打印目标系统中当前活动进程的信息。我们过滤筛选出syscalltest.exe的进程,而且仅仅显示最低限度的信息:

 

kd>!process 0 0 syscalltest.exe

PROCESS ffffe000eb1a1300

    SessionId: 1  Cid: 11e0   Peb: 00266000  ParentCid: 0b60

    DirBase: 2e466000  ObjectTable: ffffc0008a560dc0  HandleCount: <Data Not Accessible>

Image:syscalltest.exe

该EPROCESS的指针为蓝色突出“PROCESS”字段。我们接下来就会用到这个值。

我们要设置在内核中设置NtCreateFile断点。

这是系统的调用,全部用户模式调用CreateFile的api终于内核都会调用该函数。

通过在此处设置断点,我们能够看到系统用户模式切换到内核模式后执行的过程。所以我们将使用上述EPROCESS值。要求从我们选择的进程上下文中断NtCreateFile函数。我们能够使用命令例如以下:

 kd>bp /p ffffe000eb1a1300 nt!NtCreateFile

kd> g

这就仅仅在我们的进程中设置(通过/ P使用我们的EPROCESS值)nt!NtCreateFile(NT为内核模块的名称)断点。

在測试计算机上点击OKbutton。消息对话窗体消失,測试程序继续执行。当断点命中时主计算机的VS调试窗体会切换到断点处,而且系统也进入断点中断状态。

通过程序调用堆栈窗体能够看到系统的调用情况。可是因为系统採用多线程方式。因此看到的调用堆栈信息不完整。通过lmi命令能够可到内核载入的模块信息,我们能够看到调用的nt。NtCreateFile函数地址fffff800`b08838a0是落在内核模块ntkrnlmp.exe的地址空间fffff800`b048b000 fffff800`b0c57000内的。

上文我们已经讲到内核的函数调用时通过SSDT系统服务描写叙述表来分发调用内核文件的,内核导出的SSDT是具有下面格式符号KeServiceDescriptorTable的结构:

typedef struct _KSERVICE_DESCRIPTOR_TABLE {

    PULONG ServiceTableBase;         // Pointer to function/offset table (the table itself is exported as KiServiceTable)

    PULONG ServiceCounterTableBase; 

    ULONG  NumberOfServices;         // The number of entries in ServiceTableBase

    PUCHAR ParamTableBase; 

} KSERVICE_DESCRIPTOR_TABLE,*PKSERVICE_DESCRIPTOR_TABLE;

 在Windows的32位版本号,ServiceTableBase是一个指向函数指针数组的指针。在64位中略微有点复杂。ServiceTableBase指向数组偏移值为32位处。所有都是相对于KiServiceTable在存储器中的表的位置。这使得可视化使用经常使用的内存显示命令(如dds)是不可能的。相反。我们将不得不使用一些WinDbgs更高级的命令在列表中迭代,数据操纵到一个更合适的形式。

让我们先来看看在内存中是怎样偏移,我们能够用dd(显示DWORD)命令列出数组偏移值。

使用/c 1选项指示调试器每行显示一个DWORD:

kd> dqKeServiceDescriptorTable

fffff800`b080a780  fffff800`b074d150 00000000`00000000

fffff800`b080a790  00000000`000001bc fffff800`b074df34

fffff800`b080a7a0  00000000`00000000 00000000`00000000

fffff800`b080a7b0  00000000`00000000 00000000`00000000

fffff800`b080a7c0  00000000`00000000 00000000`00f54d53

fffff800`b080a7d0  00007ff8`e0758b70 ffffe000`e8485dc0

fffff800`b080a7e0  ffffe000`e8487dc0 00000000`00000000

fffff800`b080a7f0  00000000`00000000 00000000`00000001

kd> dd /c 1KiServiceTable

DBGHELP:SharedUserData - virtual symbol module

fffff800`b074d150  fdbeb004

fffff800`b074d154  fe0f4600

fffff800`b074d158  01930742

fffff800`b074d15c  0365ad00

fffff800`b074d160  01530300

fffff800`b074d164  fe832200

fffff800`b074d168  01258905

fffff800`b074d16c  01477b06

fffff800`b074d170  0126ce05

fffff800`b074d174  01a6d001

fffff800`b074d178  01ac7600

……

这些值通过左移4位并和其它数据编码。终于至少显示四位。为了形成我们须要的每一个值的绝对存储器地址,须要右移4位移,并加上KiServiceTable的地址。

我们希望在表的每一个入口点都这样做。并输出与绝对地址相关联的符号。要做到这一点,我们能够使用.foreach命令迭代,使用.printf显示符号。以下是一个命令的实现,会对每一个部分进行解释和说明: 

.foreach /ps 1 /pS 1 ( offset {dd /c 1 nt!KiServiceTable L poi(nt!KeServiceDescriptorTable+10)}){ .printf "%y\n", ( offset >>> 4) + nt!KiServiceTable }

 .foreach——该步骤指定每一个令牌(在我们的样例中。我们使用dd命令来提供令牌)。

这些參数/ PS 1和/ PS 1使得foreach每秒跳过一个令牌。

我们这样做是由于dd命令输出<地址><值>。我们仅仅对当前值感兴趣。

这些选项每次跳过令牌地址。

偏移——声明一个名为offset变量。该变量保存当前foreach迭代的令牌(当前的偏移值)

dd ——执行dd命令显示DWORD的偏移列表,这些将被.foreach进行迭代。

/C 1确保每行仅仅输出一个DWORD。Nt!KiServiceTable是我们将要显示的地址(这是偏移数组)。”L poi(nt!KeServiceDescriptorTable+10)”描写叙述了要显示多少个值。在这样的情况下,我们从指向我们的结构体NumberOfServices的KeServiceDescriptorTable開始处取出16个字节(10H),POI(),然后间接引用实际地址存储的值,比如表中有效入口的值。

.printf ——printf命令让我们运行格式化的打印。这里我们使用格式化字符串%y向给定的内存地址打印符号。当我们传递一个參数“(offset>>>4)+。NT KiServiceTable”。这是当前偏移值右移4位,并加入到KiServiceTable的地址。

我们使用>>>operator而不是>>operator,来保持的符号位,由于一些值是代表负偏移。

假设标志设置正确,上述命令的输出结果应该是这样:

kd> .foreach /ps1 /pS 1 ( offset {dd /c 1 nt!KiServiceTable Lpoi(nt!KeServiceDescriptorTable+10)}){ .printf "%y\n", ( offset>>> 4) + nt!KiServiceTable }

nt!NtAccessCheck(fffff800`b050bc50)

nt!NtWorkerFactoryWorkerReady(fffff800`b055c5b0)

nt!NtAcceptConnectPort(fffff800`b08e01c4)

nt!NtMapUserPhysicalPagesScatter(fffff800`b0ab2c20)

nt!NtWaitForSingleObject(fffff800`b08a0180)

nt!NtCallbackReturn(fffff800`b05d0370)

nt!NtReadFile(fffff800`b08729e0)

nt!NtDeviceIoControlFile(fffff800`b0894900)

nt!NtWriteFile(fffff800`b0873e30)

nt!NtRemoveIoCompletion(fffff800`b08f3e50)

nt!NtReleaseSemaphore(fffff800`b08f98b0)

nt!NtReplyWaitReceivePort(fffff800`b087d938)

nt!NtReplyPort(fffff800`b08e6400)

nt!NtSetInformationThread(fffff800`b0875f40)

nt!NtSetEvent(fffff800`b08a2180)

nt!NtClose(fffff800`b085eef0)

……

nt!NtCreateFile(fffff800`b08838a0)

nt!NtQueryEvent(fffff800`b0900d64)

nt!NtWriteRequestData(fffff800`b0aaa8cc)

nt!NtOpenDirectoryObject(fffff800`b0883128)

nt!NtAccessCheckByTypeAndAuditAlarm(fffff800`b0904a94)

……

结果应该显示可供用户态代码的主要内核系统调用一个合适的SSDT功能列表。

6.  总结

Win10 X64系统在用户层使用的系统调用。将通过KERNEL32.DLL—》KERNELBASE.dll—》NTDLL.DLL的流程。然后在NTDLL动态库中通过SYSCALL或INT 2EH指令切入内核层的KiSystemCall64函数,再通过SSDT服务表派发到内核ntkrnlmp.exe模块的对应函数。终于在调用底层的驱动或模块。

 

转载于:https://www.cnblogs.com/gavanwanggw/p/7275596.html

你可能感兴趣的文章
钱到用时方恨少(随记)
查看>>
mybatis主键返回的实现
查看>>
org.openqa.selenium.StaleElementReferenceException
查看>>
Android Intent传递对象为什么要序列化?
查看>>
数论之 莫比乌斯函数
查看>>
linux下查找某个文件位置的方法
查看>>
python之MySQL学习——数据操作
查看>>
Harmonic Number (II)
查看>>
长连接、短连接、长轮询和WebSocket
查看>>
day30 模拟ssh远程执行命令
查看>>
做错的题目——给Array附加属性
查看>>
Url.Action取消字符转义
查看>>
JQuery选择器大全
查看>>
Gamma阶段第三次scrum meeting
查看>>
python3之装饰器修复技术@wraps
查看>>
[考试]20150606
查看>>
Javascript_备忘录5
查看>>
Can’t create handler inside thread that has not called Looper.prepare()
查看>>
敏捷开发方法综述
查看>>
Hadoop数据操作系统YARN全解析
查看>>