热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

KiFastCallEntry()机制分析

1.概述从windowsxp和windows2003开始使用了快速切入内核的方式提供系统服务例程的调用。KiFastCallEntry()的实现是直接使用汇编语言,


1. 概述

从 windows xp 和 windows 2003 开始使用了快速切入内核的方式提供系统服务例程的调用。

KiFastCallEntry() 的实现是直接使用汇编语言,C 语言不能直接表达某些操作。我从 windows 2003 里反汇编出来,写成 C 伪码形式,点击这里察看:KiFastCallEntry()

在下面的篇章里,我将分析从 Win32 子系统 API WriteFile() 的调用为例,介绍如何切入到 nt 模块的 NtWriteFile() 系统服务例程。


2. Win32 子系统 API 调用

当我向一个文件或 device 使用 WriteFile() API 来写入一些数据时,像下面的调用:

//
// 往设备里写数据
//
if (WriteFile(hFile, _T("Hello, world!"), 20, &nCount, NULL) == FALSE)
{_tprintf(_T("fail: WriteFile for device, ErrorCode: %d\n"), GetLastError());CloseHandle(hFile);return -2;
}

象这样的 API 会切入到 kernel 执行系统服务例程(Service Routine),在切入 kernel 前会经过一些 stub 函数转发。

如下图所示:


在用户代码里,对于 WriteFile() 函数的调用,编译器在用户代码里会生成对子系统 DLL 的 ntdll 模块的 ZwWriteFile() 函数的调用,如下代码所示:

Status = ZwWriteFile(hFile, // file handle0, // event handleNULL, // APC routineNULL, // APC context&IoStatus, // IO_STATUS_BLOCK 块lpBuffer, // buffernNumberOfBytesToWrite, // write bytesNULL, // Byte Offset, PLARGE_INTEGER 指针NULL); // key: ULONG 指针

这个真实的 ntdll!ZwWriteFile() 是 9 个参数,ntdll!NtWriteFile() 会定向到 ntdll!ZwWriteFile() 里,因此:它们是完全一样的,只是名字不同而已!


3. ntdll!ZwWriteFile() 函数

ZwWriteFile() 是一个 stub 函数,作用也只是转发一下,因此它很简单:

ntdll!ZwWriteFile:
7c957b9d b81c010000      mov     eax,11Ch                                                      ; 系统服务例程号
7c957ba2 ba0003fe7f      mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)            ; 取得 KiFastCallEntry() stub 函数
7c957ba7 ff12            call    dword ptr [edx]                                                ; 调用这个 stub 函数
7c957ba9 c22400          ret     24h

ZwWriteFile() 在 eax 寄存器里传递一个系统服务例程号,NtWriteFile() 服务例程这号码是 11Ch。从 UserSharedData 结构里得到另一个 stub 函数。

这个 stub 函数是 KiFastCallEntry() 的 stub 函数。


4. ntdll!KiFastSystemCall() 函数

根据上面的代码,ZwWriteFile() 函数从一个叫 UserSharedData 结构区域里得到一个函数地址值,它就是 KiFastSystemCall() 函数。

这个函数是最后一个运行在用户层的 stub 函数,它将会转入 kernel 层:

ntdll!KiFastSystemCall:
7c958458 8bd4            mov     edx,esp                                                ; 传送 caller 的 stack frame pointer
7c95845a 0f34            sysenter                                                      ; 快速切入到 kernel
7c95845c c3              ret                                                            ; 注意:实际上这是一个独立的 ntdll!KiFastSystemCallRet() 例程

值得注意的是:这三行代码实际上包含了两个 ntdll 例程,最后一条 ret 指令,它是一个独立的 ntdll!KiFastSystemCallRet() 例程,我们可以在后面介绍_KUSER_SHERED_DATA 结构时看到。

KiFastSystemCall() 使用 sysenter 指令快速切入到内核的 nt!KiFastCallEntry() 代码里。

注意:给 edx 寄存器传送当前的 esp 值,这一点很重要,看看下图的 stack 布局:


在内核层的 KiFastCallEntry() 代码里,将 edx + 8 来获得传递给 ZwWriteFile() 的参数地址,从而读取完整的参数。


5. _KUSER_SHARED_DATA 结构

在 User 层和 Kernel 层分别定义了一个 _KUSER_SHARED_DATA 结构区域,用于 User 层和 Kernel 层共享某些数据,在 sysenter 快速切入机制里就使用了这个区域。

它们使用固定的地址值映射,_KUSER_SHARED_DATA 结构区域在 User 和 Kernel 层地址分别为:


  • User 层地址为:0x7ffe0000
  • Kernnel 层地址为:0xffdf0000

值得注意的是: User 层和 Kernel 层的 _KUSER_SHARED_DATA 区域都映射到同一个物理地址,下面是在 windbg 里得到 windows 2003 里的信息:

kd> !pte 7ffe0000
                    VA 7ffe0000
PDE at C0601FF8            PTE at C03FFF00
contains 00000000108AD067  contains 0000000000041025
pfn 108ad     ---DA--UWEV   pfn 41        ----A--UREV


kd> !pte ffdf0000
                    VA ffdf0000
PDE at C0603FF0            PTE at C07FEF80
contains 0000000000513063  contains 0000000000041163
pfn 513       ---DA--KWEV   pfn 41        -G-DA--KWEV

可以看到:它们都映射到物理页面 0x41000 上。因此:User 层和 Kernel 层的 _KUSER_SHARED_DATA 区域内容是完全一样的。基于这种设计可以方便地在 User 层和 Kernel 层共享某些数据。

另一点:在 User 层里 _KUSER_SHARED_DATA 区域是只读的,只有在 Kernel 层才是可写的。因此:Kernel 在初始化阶段某个时刻对 _KUSER_SHHARED_DATA 区进行设置。User 层只能读取其中的值,如前面的 stub 函数所示:

ntdll!ZwWriteFile:
7c957b9d b81c010000      mov     eax,11Ch                                                      ; 系统服务例程号
7c957ba2 ba0003fe7f      mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)            ; 取得 KiFastCallEntry() stub 函数
7c957ba7 ff12            call    dword ptr [edx]                                                ; 调用这个 stub 函数
7c957ba9 c22400          ret     24h

下面,来看看 _KUSER_SHARED_DATA 区域是些什么内容(User 层和 Kernel 层是一样的),在 windbg 用 dt 命令来查看:

kd> dt _KUSER_SHARED_DATA 0x7ffe0000 
ntdll!_KUSER_SHARED_DATA
   +0x000 TickCountLowDeprecated : 0
   +0x004 TickCountMultiplier : 0xfa00000
   +0x008 InterruptTime    : _KSYSTEM_TIME
   +0x014 SystemTime       : _KSYSTEM_TIME
   +0x020 TimeZoneBias     : _KSYSTEM_TIME
   +0x02c ImageNumberLow   : 0x14c
   +0x02e ImageNumberHigh  : 0x14c
   +0x030 NtSystemRoot     : [260] 0x43
   +0x238 MaxStackTraceDepth : 0
   +0x23c CryptoExponent   : 0
   +0x240 TimeZoneId       : 0
   +0x244 LargePageMinimum : 0x200000
   +0x248 Reserved2        : [7] 0
   +0x264 NtProductType    : 3 ( NtProductServer )
   +0x268 ProductTypeIsValid : 0x1 ''
   +0x26c NtMajorVersion   : 5
   +0x270 NtMinorVersion   : 2
   +0x274 ProcessorFeatures : [64]  ""
   +0x2b4 Reserved1        : 0x7ffeffff
   +0x2b8 Reserved3        : 0x80000000
   +0x2bc TimeSlip         : 0
   +0x2c0 AlternativeArchitecture : 0 ( StandardDesign )
   +0x2c8 SystemExpirationDate : _LARGE_INTEGER 0x0
   +0x2d0 SuiteMask        : 0x112
   +0x2d4 KdDebuggerEnabled : 0x3 ''
   +0x2d5 NXSupportPolicy  : 0x2 ''
   +0x2d8 ActiveConsoleId  : 0
   +0x2dc DismountCount    : 0
   +0x2e0 ComPlusPackage   : 0xffffffff
   +0x2e4 LastSystemRITEventTickCount : 0x239f29d
   +0x2e8 NumberOfPhysicalPages : 0x17f1b
   +0x2ec SafeBootMode     : 0 ''
   +0x2f0 TraceLogging     : 0
   +0x2f8 TestRetInstruction : 0xc3
   &#43;0x300 SystemCall       : 0x7c958458                                 <---------  System Call stub 函数
   &#43;0x304 SystemCallReturn : 0x7c95845c                                 <---------  System Call return 函数
   &#43;0x308 SystemCallPad    : [3] 0
   &#43;0x320 TickCount        : _KSYSTEM_TIME
   &#43;0x320 TickCountQuad    : 0x2481d8
   &#43;0x330 COOKIE           : 0xa4a0f27b
   &#43;0x334 Wow64SharedInformation : [16] 0

其中 &#43;0x300 位置上就是 KiFastSystemCall() stub 函数地址&#xff0c;而 &#43;0x304 位置上就是返回函数地址&#xff1a;

ntdll!KiFastSystemCall:
7c958458 8bd4            mov     edx,esp                                                ; 传送 caller 的 stack frame pointer
7c95845a 0f34            sysenter                                                      ; 快速切入到 kernel
7c95845c c3              ret                                                            ; 注意&#xff1a;实际上这是一个独立的 ntdll!KiFastSystemCallRet() 例程

地址 0x7c958458 是 ntdll!KiFastSystemCall() 函数地址&#xff0c;地址 0x7c95845c 是 ntdll!KiFastSystemCallRet() 函数地址。


6. 切入 KiFastCallEntry()

在用户层的 stub 函数会使用 sysenter 指令切入到内核层的 KiFastCallEntry() 函数&#xff0c;再由 KiFastCallEntry() 函数分发到相应的系统服务例程执行。

如下图所示&#xff1a;


KiFastCallEntry() 函数是使用汇编语言来实现的&#xff0c;我将写成 C 伪码形式&#xff0c;点击这里察看&#xff1a;KiFastCallEntry()&#xff0c;这里不再重复贴出。


6.1 读取 TSS 信息

在 x86 体系的 sysenter/sysexit 指令快速切入机制里 IA32_SYSENTER_ESP 寄存器&#xff08;MSR 地址为 175h&#xff09;提供了 ESP 值。但是&#xff0c;在 windows 里并没有使用这个值&#xff0c;而是使用KPCR 结构TSS 块里的 ESP 值。

//// 得到当前 TSS 块&#xff0c;并读取 0 级的 esp 值// 注意&#xff1a;这个 Esp0 指向一个 KTRAP_FRAME 结构的 V86Es 成员&#xff01;// Esp0 值减去 0x7c 就等于 KTRAP_FRAME 结构地址&#xff0c;trap 用于 context 信息// esp 被赋予 KTRAP_FRAME 结构地址&#xff1a;esp &#61; KtrapFrame&#xff0c;它以 push 的方式保存 context 信息//PKTSS Tss &#61; GetCurrentTss(); PKTRAP_FRAME KtrapFrame &#61; (PKTRAP_FRAME)(Tss->Esp0 - 0x7c);

在 KPCR&#xff08;Processor Cotnrol Region&#xff09;区域的 &#43;0x40 位置是 TSS 指针&#xff08;指向一个 KTSS 结构&#xff09;&#xff0c;KPCR 结构的地址在0xffdff000&#xff1a;

kd> dt _kpcr 0xffdff000 
ntdll!_KPCR
   &#43;0x000 NtTib            : _NT_TIB
   &#43;0x000 Used_ExceptionList : 0xf6ac85b8 _EXCEPTION_REGISTRATION_RECORD
   &#43;0x004 Used_StackBase   : (null) 
   &#43;0x008 PerfGlobalGroupMask : (null) 
   &#43;0x00c TssCopy          : 0x80042000 Void
   &#43;0x010 ContextSwitches  : 0x10d344b
   &#43;0x014 SetMemberCopy    : 1
   &#43;0x018 Used_Self        : 0x7ffdf000 Void
   &#43;0x01c SelfPcr          : 0xffdff000 _KPCR
   &#43;0x020 Prcb             : 0xffdff120 _KPRCB
   &#43;0x024 Irql             : 0 &#39;&#39;
   &#43;0x028 IRR              : 0
   &#43;0x02c IrrActive        : 0
   &#43;0x030 IDR              : 0xffffffff
   &#43;0x034 KdVersionBlock   : 0x8088e3b8 Void
   &#43;0x038 IDT              : 0x8003f400 _KIDTENTRY
   &#43;0x03c GDT              : 0x8003f000 _KGDTENTRY
   &#43;0x040 TSS              : 0x80042000 _KTSS                                   <------ TSS 结构地址
   &#43;0x044 MajorVersion     : 1
   &#43;0x046 MinorVersion     : 1
   &#43;0x048 SetMember        : 1
   &#43;0x04c StallScaleFactor : 0x95a
   &#43;0x050 SpareUnused      : 0 &#39;&#39;
   &#43;0x051 Number           : 0 &#39;&#39;
   &#43;0x052 Spare0           : 0 &#39;&#39;
   &#43;0x053 SecondLevelCacheAssociativity : 0 &#39;&#39;
   &#43;0x054 VdmAlert         : 0
   &#43;0x058 KernelReserved   : [14] 0
   &#43;0x090 SecondLevelCacheSize : 0
   &#43;0x094 HalReserved      : [16] 0
   &#43;0x0d4 InterruptMode    : 0
   &#43;0x0d8 Spare1           : 0 &#39;&#39;
   &#43;0x0dc KernelReserved2  : [17] 0
   &#43;0x120 PrcbData         : _KPRCB

我们看到这个 KTSS 结构地址在 0x80042000 里&#xff0c;这个 KTSS 结构如下&#xff1a;

kd> dt _ktss 0x80042000 
ntdll!_KTSS
   &#43;0x000 Backlink         : 0xc45
   &#43;0x002 Reserved0        : 0x4d8a
   &#43;0x004 Esp0             : 0xf649bde0                                <------- 0 级的 Esp 值&#xff0c;这指向一个 KTRAP_FRAME 结构 V86Es 成员
   &#43;0x008 Ss0              : 0x10
   &#43;0x00a Reserved1        : 0xb70f
   &#43;0x00c NotUsed1         : [4] 0x5031ff00
   &#43;0x01c CR3              : 0x8b55ff8b
   &#43;0x020 Eip              : 0xc75ffec
   &#43;0x024 EFlags           : 0xe80875ff
   &#43;0x028 Eax              : 0xfffffbdd
   &#43;0x02c Ecx              : 0x1b75c084
   &#43;0x030 Edx              : 0x8b184d8b
   &#43;0x034 Ebx              : 0x7d8b57d1
   &#43;0x038 Esp              : 0x2e9c110
   &#43;0x03c Ebp              : 0xf3ffc883
   &#43;0x040 Esi              : 0x83ca8bab
   &#43;0x044 Edi              : 0xaaf303e1
   &#43;0x048 Es               : 0xeb5f
   &#43;0x04a Reserved2        : 0x6819
   &#43;0x04c Cs               : 0x24fc
   &#43;0x04e Reserved3        : 0x44
   &#43;0x050 Ss               : 0x75ff
   &#43;0x052 Reserved4        : 0xff18
   &#43;0x054 Ds               : 0x1475
   &#43;0x056 Reserved5        : 0x75ff
   &#43;0x058 Fs               : 0xff10
   &#43;0x05a Reserved6        : 0xc75
   &#43;0x05c Gs               : 0x75ff
   &#43;0x05e Reserved7        : 0xe808
   &#43;0x060 LDT              : 0
   &#43;0x062 Reserved8        : 0xffff
   &#43;0x064 Flags            : 0
   &#43;0x066 IoMapBase        : 0x20ac
   &#43;0x068 IoMaps           : [1] _KiIoAccessMap
   &#43;0x208c IntDirectionMap  : [32]  "???"

KTSS 结构内的 Esp0 指向 KTRAP_FRAME 结构的 V86Es 成员&#xff0c;如下图所示&#xff1a;


这个 Esp0 值被赋值给 esp 寄存器&#xff0c;那么 KiFastCallEntry() 将会使用这个值来压入 context 信息&#xff0c;如下代码所示&#xff1a;

8088387d 8b0d40f0dfff    mov     ecx,dword ptr ds:[0FFDFF040h]                  ; 读取 KTSS 结构
80883883 8b6104          mov     esp,dword ptr [ecx&#43;4]                          ; 读取 Esp0&#xff0c;Esp0 指向 KTRAP_FRAME 的 V86Es 成员
80883886 6a23            push    23h                                            ; 压入 HardwareSegSs 值&#xff0c;也就是 SS 值

esp 指向 KTRAP_FRAME 结构 V86Es&#xff0c;当 push 23h 时则等于将 HardwareSegSs 赋值为 23h 值。我们将在后面了解到 KTRAP_FRAME 结构


6.2 KTRAP_FRAME 结构

在 KiFastCallEntry() 中将 context 信息保存在一个被称为 KTRAP_FRAME 的结构里&#xff0c;在前面我们看到 KTRAP_FRAME 结构的地址被赋予 esp 寄存器&#xff0c;因此&#xff1a;KTRAP_FRAME 结构就是 KiFastCallEntry() 函数的 stack 区域。KTRAP_FRAME 结构如下面所示&#xff1a;

kd> dt _ktrap_frame 0xf649bde0-0x7c                    <--- KTRAP_FRAME 结构基址等于 Esp0 值减 0x7c
ntdll!_KTRAP_FRAME
   &#43;0x000 DbgEbp           : 0x12fa74
   &#43;0x004 DbgEip           : 0x7c95845c
   &#43;0x008 DbgArgMark       : 0xbadb0d00
   &#43;0x00c DbgArgPointer    : 0x12fa30
   &#43;0x010 TempSegCs        : 0
   &#43;0x014 TempEsp          : 0
   &#43;0x018 Dr0              : 0
   &#43;0x01c Dr1              : 0
   &#43;0x020 Dr2              : 0
   &#43;0x024 Dr3              : 0
   &#43;0x028 Dr6              : 0
   &#43;0x02c Dr7              : 0
   &#43;0x030 SegGs            : 0
   &#43;0x034 SegEs            : 0x23
   &#43;0x038 SegDs            : 0x23
   &#43;0x03c Edx              : 0xc
   &#43;0x040 Ecx              : 2
   &#43;0x044 Eax              : 0x12f6a0
   &#43;0x048 PreviousPreviousMode : 1
   &#43;0x04c ExceptionList    : 0xffffffff _EXCEPTION_REGISTRATION_RECORD
   &#43;0x050 SegFs            : 0x3b
   &#43;0x054 Edi              : 1
   &#43;0x058 Esi              : 0
   &#43;0x05c Ebx              : 0
   &#43;0x060 Ebp              : 0x12fa74
   &#43;0x064 ErrCode          : 0
   &#43;0x068 Eip              : 0x7c95845c
   &#43;0x06c SegCs            : 0x1b
   &#43;0x070 EFlags           : 0x213
   &#43;0x074 HardwareEsp      : 0x12fa28
   &#43;0x078 HardwareSegSs    : 0x23
   &#43;0x07c V86Es            : 0
   &#43;0x080 V86Ds            : 0
   &#43;0x084 V86Fs            : 0
   &#43;0x088 V86Gs            : 0

注意&#xff1a;KTRAP_FRAME 结构的基址是 Esp0 值减 0x7c 而来&#xff0c;因为&#xff1a;Esp0 指向 V86Es 成员。在执行 push 23h 指令后&#xff0c;HardwareSegSs 就等于 23h。上面显示的 KTRAP_FRAME 结构的内容是 KiFastCallEntry() 在已经保存好 context 信息后的内容&#xff0c;将要转入执行真正的系统服务例程&#xff08;nt!NtWriteFile)。

KiFastCallEntry() 在 KTRAP_FRAME 里保存下面的内容&#xff1a;

//// 注意&#xff1a;下面保存 context 的操作是以 push 方式压入 TrapFrame 为 esp 的栈中//KtrapFrame->HardwareSegSs &#61; 0x23; // 保存原 R3 的 SS 值KtrapFrame->HardwareEsp &#61; edx; // edx 是原 R3 的 ESP 值 KtrapFrame->EFlags &#61; eflags; // 保存原 eflags 值KtrapFrame->EFlags.IF &#61; 1; // context 中的 eflags.IF 置位eflags &#61; 0; // 当前的 eflags 清为 0//// 当前 edx 保存着 sysenter 进入前的 esp 值// 加上 8 后&#xff1a;edx 指向 native API 调用中的第 1 个参数//PVOID ArgumentPointer &#61; edx &#43; 8;KtrapFrame->SegCs &#61; 0x1b; // 保存原 R3 的 CS 值KtrapFrame->Eip &#61; UserSharedData->SystemCallReturn; // 保存返回函数KtrapFrame->ErrCode &#61; 0; // 错误码为 0KtrapFrame->Ebp &#61; ebp;KtrapFrame->Ebx &#61; ebx;KtrapFrame->Esi &#61; esi;KtrapFrame->Edi &#61; edi;KtrapFrame->SegFs &#61; 0x3b; // 原 R3 的 FS 值PKPCR Kpcr &#61; (PKPCR)0xffdff000; // 也就是 fs.baseKtrapFrame->ExceptionList &#61; Kpcr->NtTib.ExceptionList; // 保存原 SEH 链底Kpcr->->NtTib.ExceptionList &#61; -1; // 设置为空 SEH 链PKTHREAD Thread &#61; Kpcr->PrcbData.CurrentThread; // 得到当前线程结构PVOID InitialStack &#61; Thread->InitialStack; // 得到初始的 stack 地址KtrapFrame->PreviousPreviousMode &#61; 1; // 1 值是原 MODE_MASK 值KtrapFrame &#61; (PKTRAP_FRAME)((ULONG)KtrapFrame - 0x48); // 计算出 Ktrap_frame 基地址//// 计算初始 stack 的 ktrap_frame 基址:// 这个 0x29c 值等于&#xff1a;NPX_FRAME_LENGTH &#43; KTRAP_FRAME_LENGTH// NPX_FRAME_LEGNTH &#61; 0x210// KTRAP_FRAME_LENGTH &#61; 0x8c//InitialStack &#61; (PVOID)((ULONG)InitialStack - 0x29c);Thread->PreviousMode &#61; 1;//// 假如这两个 stack 基址值不同//if (KtrapFrame !&#61; InitialStack){goto 3869;}//// 此时 InitialStack 指向 KtrapFrame 基址&#xff0c;也就是&#xff1a;InitialStack &#61;&#61; KtrapFrame//InitialStack->Dr7 &#61; 0; // 清 Dr7 值Thread->TrapFrame &#61; InitialStack;//// 检测是否需要保存 debug context 信息&#xff08;debug 寄存器&#xff09;//if (Thread->Header.DebugActive !&#61; 0xff){goto 372c;}ebx &#61; KtrapFrame->Ebp; // 读取原 ebp 值edi &#61; KtrapFrame->Eip; // 读取 UserSharedData->SystemCallReturnKtrapFrame->DbgArgPointer &#61; ArgumentPointer; // native API 调用的第 1 个参数KtrapFrame->DbgArgMark &#61; 0xbadb0d00;//// 实际上&#xff1a;当前 KtrapFrame 值等价于当前 esp // 因此&#xff0c;下面两行代码是构建一个标准的 call 返回流程// 1. push UserSharedData->SystemCallReturn&#xff08;返回地址&#xff09;// 2. push ebp&#xff08;原 stack frame base &#xff09;//// 当前&#xff1a;// 1. ebp &#61;&#61; esp// 2. ebp 指向 KtrapFrame->DbgEbp 值&#xff08;stack top)//KtrapFrame->DbgEbp &#61; ebx; KtrapFrame->DbgEip &#61; edi;


7. 系统服务例程号与 ServiceTable

KiFastCallEntry() 保存好相关的 context 信息后&#xff0c;接下来要重的一步是分析系统服务例程号以便读取系统服务例程地址。如下图所示&#xff1a;


32 位的系统服务例程号&#xff0c;实际只使用低 12 位&#xff0c;bit12 位是 index 值&#xff0c;用来在 Service table 里查找自己的系统服务例程表。

补充&#xff1a;这个 ServiceTable 的内容貌似就是 SDT&#xff08;Service Descirptor Table&#xff09;&#xff01;


7.1 ServiceTable

windows 的 Service Table 来自于 KTHREAD 结构内的 ServiceTable 成员&#xff0c;ServiceTable 的寻址是&#xff1a;

PKPCR Pcr &#61; (PKPCR)0xffdff000; // 内核中的 Processor Cotnrol Region 地址为 0FFDFF000h
PKTHREAD Thread &#61; Pcr->PrcbData.CurrentThread; // 得到 kernel 中的 KTHREAD 结构
PVOID ServiceTable &#61; Thread->ServiceTable; // 得到 KTHREAD 结构中的 ServiceTable 地址

实际上 KTHREAD 的地址在 0x880c7330 上&#xff0c;在 windbg 上观察如下&#xff1a;

kd> dt _kthread 0x880c7330 
ntdll!_KTHREAD
   &#43;0x000 Header           : _DISPATCHER_HEADER
   &#43;0x010 MutantListHead   : _LIST_ENTRY [ 0x880c7340 - 0x880c7340 ]
   &#43;0x018 InitialStack     : 0xf649c000 Void
   &#43;0x01c StackLimit       : 0xf6495000 Void
   &#43;0x020 KernelStack      : 0xf649b914 Void
   &#43;0x024 ThreadLock       : 0
   &#43;0x028 ApcState         : _KAPC_STATE
   &#43;0x028 ApcStateFill     : [23]  "Xs???"
   &#43;0x03f ApcQueueable     : 0x1 &#39;&#39;
   &#43;0x040 NextProcessor    : 0 &#39;&#39;
   &#43;0x041 DeferredProcessor : 0 &#39;&#39;
   &#43;0x042 AdjustReason     : 0 &#39;&#39;
   &#43;0x043 AdjustIncrement  : 2 &#39;&#39;
   &#43;0x044 ApcQueueLock     : 0
   &#43;0x048 ContextSwitches  : 0x14cda
   &#43;0x04c State            : 0x2 &#39;&#39;
   &#43;0x04d NpxState         : 0xa &#39;&#39;
   &#43;0x04e WaitIrql         : 0 &#39;&#39;
   &#43;0x04f WaitMode         : 1 &#39;&#39;
   &#43;0x050 WaitStatus       : 0n0
   &#43;0x054 WaitBlockList    : 0x880c73d8 _KWAIT_BLOCK
   &#43;0x054 GateObject       : 0x880c73d8 _KGATE
   &#43;0x058 Alertable        : 0x1 &#39;&#39;
   &#43;0x059 WaitNext         : 0 &#39;&#39;
   &#43;0x05a WaitReason       : 0x6 &#39;&#39;
   &#43;0x05b Priority         : 12 &#39;&#39;
   &#43;0x05c EnableStackSwap  : 0x1 &#39;&#39;
   &#43;0x05d SwapBusy         : 0 &#39;&#39;
   &#43;0x05e Alerted          : [2]  ""
   &#43;0x060 WaitListEntry    : _LIST_ENTRY [ 0xffdffb70 - 0xffdffb70 ]
   &#43;0x060 SwapListEntry    : _SINGLE_LIST_ENTRY
   &#43;0x068 Queue            : (null) 
   &#43;0x06c WaitTime         : 0x5378
   &#43;0x070 KernelApcDisable : 0n0
   &#43;0x072 SpecialApcDisable : 0n0
   &#43;0x070 CombinedApcDisable : 0
   &#43;0x074 Teb              : 0x7ffdf000 Void
   &#43;0x078 Timer            : _KTIMER
   &#43;0x078 TimerFill        : [40]  "???"
   &#43;0x0a0 AutoAlignment    : 0y0
   &#43;0x0a0 DisableBoost     : 0y0
   &#43;0x0a0 GuiThread        : 0y0
   &#43;0x0a0 VdmSafe          : 0y0
   &#43;0x0a0 ReservedFlags    : 0y0000000000000000000000000000 (0)
   &#43;0x0a0 ThreadFlags      : 0n0
   &#43;0x0a8 WaitBlock        : [4] _KWAIT_BLOCK
   &#43;0x0a8 WaitBlockFill0   : [23]  "???"
   &#43;0x0bf SystemAffinityActive : 0 &#39;&#39;
   &#43;0x0a8 WaitBlockFill1   : [47]  "???"
   &#43;0x0d7 PreviousMode     : 1 &#39;&#39;
   &#43;0x0a8 WaitBlockFill2   : [71]  "???"
   &#43;0x0ef ResourceIndex    : 0x1 &#39;&#39;
   &#43;0x0a8 WaitBlockFill3   : [95]  "???"
   &#43;0x107 LargeStack       : 0x1 &#39;&#39;
   &#43;0x108 QueueListEntry   : _LIST_ENTRY [ 0x0 - 0x0 ]
   &#43;0x110 TrapFrame        : 0xf649bd64 _KTRAP_FRAME
   &#43;0x114 CallbackStack    : (null) 
   &#43;0x118 ServiceTable     : 0x8089f440 Void                                   <--- 这是 ServiceTable 地址
   &#43;0x11c ApcStateIndex    : 0 &#39;&#39;
   &#43;0x11d IdealProcessor   : 0 &#39;&#39;
   &#43;0x11e Preempted        : 0 &#39;&#39;
   &#43;0x11f ProcessReadyQueue : 0 &#39;&#39;
   &#43;0x120 KernelStackResident : 0x1 &#39;&#39;
   &#43;0x121 BasePriority     : 8 &#39;&#39;
   &#43;0x122 PriorityDecrement : 2 &#39;&#39;
   &#43;0x123 Saturation       : 0 &#39;&#39;
   &#43;0x124 UserAffinity     : 1
   &#43;0x128 Process          : 0x87d2e958 _KPROCESS
   &#43;0x12c Affinity         : 1
   &#43;0x130 ApcStatePointer  : [2] 0x880c7358 _KAPC_STATE
   &#43;0x138 SavedApcState    : _KAPC_STATE
   &#43;0x138 SavedApcStateFill : [23]  "ht???"
   &#43;0x14f FreezeCount      : 0 &#39;&#39;
   &#43;0x150 SuspendCount     : 0 &#39;&#39;
   &#43;0x151 UserIdealProcessor : 0 &#39;&#39;
   &#43;0x152 CalloutActive    : 0 &#39;&#39;
   &#43;0x153 Iopl             : 0 &#39;&#39;
   &#43;0x154 Win32Thread      : 0xe10b2350 Void
   &#43;0x158 StackBase        : 0xf649c000 Void
   &#43;0x15c SuspendApc       : _KAPC
   &#43;0x15c SuspendApcFill0  : [1]  "??? 0$"
   &#43;0x15d Quantum          : 32 &#39; &#39;
   &#43;0x15c SuspendApcFill1  : [3]  "???"
   &#43;0x15f QuantumReset     : 0x24 &#39;$&#39;
   &#43;0x15c SuspendApcFill2  : [4]  "???"
   &#43;0x160 KernelTime       : 0x21e
   &#43;0x15c SuspendApcFill3  : [36]  "???"
   &#43;0x180 TlsArray         : (null) 
   &#43;0x15c SuspendApcFill4  : [40]  "???"
   &#43;0x184 LegoData         : (null) 
   &#43;0x15c SuspendApcFill5  : [47]  "???"
   &#43;0x18b PowerState       : 0 &#39;&#39;
   &#43;0x18c UserTime         : 0x133
   &#43;0x190 SuspendSemaphore : _KSEMAPHORE
   &#43;0x190 SuspendSemaphorefill : [20]  "???"
   &#43;0x1a4 SListFaultCount  : 0
   &#43;0x1a8 ThreadListEntry  : _LIST_ENTRY [ 0x88204888 - 0x87d2e9a8 ]
   &#43;0x1b0 SListFaultAddress : (null) 


7.2 ServiceTable entry

由 index 值在 ServiceTable 里定位 Service Table entry 结构&#xff0c;它是 16 字节大&#xff0c;它看起来包括&#xff1a;


  • ServiceRoutineTable&#xff1a;提供真正的系统服务例程的地址
  • unknow 未知的元素
  • MaxServiceNumber&#xff1a;最大的系统服务例程号
  • ArgumentSizeTable&#xff1a;提供每个例程所需要的参数大小&#xff0c;这个值将要用来从 caller 里复制多少个参数。

当 index &#61; 1 时&#xff0c;是指向 GUI 类的系统服务例程表&#xff0c;它们在 GUI 类系统内核驱动模块 win32k.sys 模块。例如&#xff1a;win32k!NtGdiBitBlt() 例程&#xff0c;index &#61; 0 时&#xff0c;使用普通的系统例程表。

在 windows service 2003 系统上&#xff0c;两个系统服务例程表的 MaxServiceNumber 值分别是&#xff1a;


  • index &#61; 0&#xff1a;MaxServiceNumber 为 0x128
  • index &#61; 1&#xff1a;MaxServiceNumber 为 0x299

我们可以在 windbg 里查看这些 entry 值是多少&#xff0c;如下所示&#xff1a;

kd> dd 0x8089f440
8089f440  80830f84 00000000 00000128 80831428                   ; index &#61; 0
8089f450  bf9a7000 00000000 00000299 bf9a7d08                   ; index &#61; 1

下面的情况下是属于超出例程号&#xff1a;

if (ServiceNumber >&#61; ServceTableEntry->MaxServiceNumber){goto nt!KiBBTUnexpectedRange (80883662)}



当提供的服务例程号大小等于 MaxServiceNumber 时就属于超限&#xff01;因此&#xff1a;普通的系统服务例程号最大为 0x127 号&#xff0c;而 GUI 类例程号最大为 0x298 号


7.3 读取目标例程地址和参数 size

根据 ServiceNumber 号在 ServiceRoutineTable 里找到最终的系统服务例程地址值&#xff0c;例如 WriteFile() 的服务例程号是 0x11c&#xff0c;那么将在地址[ServiceRoutineTable &#43; 0x11c * 4] 的位置上就 nt 模块的 WriteFile() 地址。

而 ArgumentSize 值被读取后&#xff0c;用它来复制参数在 KiFastCallEntry() 的栈上&#xff1a;

80883949 8a0c18          mov     cl,byte ptr [eax&#43;ebx]                                         ; 读取 ArgumentSize 值
8088394c 8b3f            mov     edi,dword ptr [edi]                                           ; 读取 ServiceRoutineTable
8088394e 8b1c87          mov     ebx,dword ptr [edi&#43;eax*4]                                      ; 读取服务例程地址
80883951 2be1            sub     esp,ecx                                                       ; 在当前栈上开辟空间容纳参数
80883953 c1e902          shr     ecx,2
80883956 8bfc            mov     edi,esp
80883958 3b35e8588980    cmp     esi,dword ptr [nt!MmUserProbeAddress (808958e8)]               ; 是否属于用户空间
8088395e 0f83f0010000    jae     nt!KiSystemCallExit3&#43;0x90 (80883b54)

nt!KiFastCallEntry&#43;0xf4:
80883964 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]                            ; 复制参数到当前栈上
80883966 ffd3            call    ebx                                                           ; 调用最终的服务例程

在复制前&#xff0c;KiFastCallEntry() 还会判断 caller 的地址是否属于用户空间&#xff0c;当大于等于 MmUserProbeAddress 值时&#xff0c;属于内核空间&#xff0c;那么会进行另外的处理。

这个 MmUserProbeAddress 值为 0x7FFF0000&#xff0c;它是用户空间最大可用的地址值。在复制完参数后&#xff0c;紧接着就调用最终的系统服务例程&#xff01;


8. KiFastCallEntry() 的返回处理

KiFastCallEntry() 的返回处理很复杂&#xff0c;需要检测多种情况&#xff0c;主要是检查出调用者属于 0 级&#xff0c;还是 3 级情况下。

之所以需要过多的检测&#xff0c;是因为进入 KiFastCallEntry() 的途径可能有多种&#xff0c;下面我们看看返回前的一些处理。


8.1 检查 APC 和提交 APC

//// 假如调用者是 ring 3 并且需要检查 APC//if ((KiEnableApcCheck & 0x01) && (KtrapFrame->SegCs.RPL &#61;&#61; 3)){KIRQL Irql &#61; KeGetCurrentIrql();//// 如果当前 Irql 不是 PASSIVE_LEVEL 级别&#xff1a;抛出 BugCheck 错误&#xff01;//if (Irql !&#61; PASSIVE_LEVEL){Kpcr->Irql &#61; PASSIVE_LEVEL;KeBugCheck2(0x4A,NtWriteFile,Irql,0,0,InitialStack);}Thread &#61; Kpcr->CurrentThread;//// 检查 process 是否 attached ?// 或者 APCs 是否被 disable ?// 如果是的话&#xff1a;抛出 BugCheck 错误&#xff01;//if ((Thread->ApcStateIndex & 0xff) || (Thread->CombinedApcDisable !&#61; 0)){KeBugCheck2(1, NtWriteFile, Thread->ApcStateIndex, Thread->CombinedApcDisable,0,InitialStack);}}// // 恢复 stack frame//esp &#61; ebp;PKTRAP_FRAME OldTrapFrame &#61; KtrapFrame->Edx; // 找到原 esp 值&#xff08;进入 sysenter 之前&#xff09;Thread->TrapFrame &#61; OldTrapFrame; // 保存在 Thread 的 TrapFrame 域里cli();if ((KtrapFrame->EFlags.VM &#61;&#61; 1) || (KtrapFrame->SegCs.RPL &#61;&#61; 3)){Thread->Alerted &#61; 0;while (Thread->ApcStateFill.AsUserApcPending !&#61; 0){KtrapFrame->Eax &#61; eax; // 保存返回值KtrapFrame->SegFs &#61; 0x3b;KtrapFrame->SegDs &#61; 0x23;KtrapFrame->SegEs &#61; 0x23;KtrapFrame->SegGs &#61; 0;//// 下面代码将 IRQL 提升到 APC_LEVEL 级别// 然后提交 APC 排队处理&#xff08;需要开中断&#xff09;// 完成后恢复原 IRQL 级别并关闭中断许可//OldIrql &#61; KfRaiseIrql(APC_LEVEL);sti();KiDeliverApc(1, 0, KtrapFrame); // 提交 APC 处理KfLowerIrql(OldIrql);cli();Thread->Alerted &#61; 0;}}



当调用者是 user 层的话&#xff0c;如果需要检查 APC&#xff0c;则检查&#xff1a;


  1. 当前的 IRQL 必须在 PASSIVE_LEVEL 级别&#xff0c;否则会引发 BugCheck&#xff08;死亡蓝屏&#xff09;。
  2. 检查 process 是否 attached&#xff0c;或者 kernel APCs 是否被 disable 掉&#xff0c;这两种情况都会引发 BugCheck&#xff01;

并且当有 APC 在 pending 状态时&#xff0c;需要提交完所有的 APC 进行处理。


8.2 检查是否开启调试功能

if (KtrapFrame->Dr7 & 0xffff23ff){//// 如果开启了 DR7 调试功能//if ((KtrapFrame->EFlags.VM &#61;&#61; 1) || (KtrapFrame->SegCs.RPL &#61;&#61; 3)){dr7 &#61; 0; // 先关闭调试功能dr0 &#61; KtrapFrame->Dr0;dr1 &#61; KtrapFrame->Dr1;dr2 &#61; KtrapFrame->Dr2;dr3 &#61; KtrapFrame->Dr3;dr6 &#61; KtrapFrame->Dr6;dr7 &#61; KtrapFrame->Dr7;}}

如果是的话&#xff1a;恢复原来的 debug 寄存器值。


8.3 恢复部分 context 信息

if (KtrapFrame->EFlags.VM &#61;&#61; 1){//// 如果开启了 V8086 模式// edx &#61; KtrapFrame->Edx;ecx &#61; KtrapFrame->Ecx;eax &#61; KtrapFrame->Eax;}else if (KtrapFrame->SegCs & 0xFFF9 &#61;&#61; 0){//// 如果 CS selector 为 0 值&#xff08;0级下的 NULL selector&#xff09;//KtrapFrame->SegCs &#61; KtrapFrame->TempSegCs; //// ErrCode 指向构造的 TempStack 结构//PVOID TempStack &#61; KtrapFrame->TempEsp - 0x0c;KtrapFrame->ErrCode &#61; TempStack;//// 下面构造一个 stack 结构&#xff08;TempStack&#xff09;以便使用 iretd 指令返回://// Eflags// SegCs// esp ------> Eip//TempStack->Eflags &#61; KtrapFrame->EFlags;TempStack->SegCs &#61; KtrapFrame->SegCs;TempStack->Eip &#61; KtrapFrame->Eip;edi &#61; KtrapFrame->Edi;esi &#61; KtrapFrame->Esi;ebx &#61; KtrapFrame->Ebx;ebp &#61; KtrapFrame->Ebp;//// 基于构造的 TempStack 来中断返回//esp &#61; &TempStack;iretd;}else if (KtrapFrame->SegCs.RPL &#61;&#61; 3){//// 恢复 ring 3 的寄存器值//eax &#61; KtrapFrame->Eax;edx &#61; KtrapFrame->Edx;ecx &#61; KtrapFrame->Ecx;gs &#61; KtrapFrame->SegGs;es &#61; KtrapFrame->SegEs;ds &#61; KtrapFrame->SegDs;fs &#61; KtrapFrame->SegFs;}else if (KtrapFrame->SegCs !&#61; 8){fs &#61; KtrapFrame->SegFs;}

上面的代码中&#xff0c;其中一项&#xff1a;KtrapFrame->SegCs & 0xFFF9 &#61;&#61; 0&#xff0c;意图是检查调用者的 CS 是否为 0&#xff0c;因为 windows 只使用 0 和 3 级的权限运行级别。

这段代码比较奇怪&#xff0c;注意&#xff0c;我不能完全理解它的用意&#xff01;它在 ErrCode 的地方&#xff0c;构造一个 32 位宽的 far pointer&#xff0c;用来加载到 16 位的 SP 寄存器和 SS 寄存器。我想是为了返回到 16 位代码


8.4 完成一个典型的中断调用 stack 结构

在返回前&#xff0c;经过一系列的 pop 操作之后&#xff0c;形成一个典型的中断调用栈结构&#xff1a;

        //
        // 现在&#xff1a;esp 指向 KtrapFrame->Edi 域
        // 下一步工作是&#xff1a;pop 出相关的值
        //
        esp &#61; &KtrapFrame->Edi;
        
        //
        // 下面恢复原部分寄存器 context 信息
        // 也就是执行&#xff1a;
        //        pop edi        ----> 此时 esp 指向 edi 保存的地址
        //        pop esi
        //        pop ebx
        //        pop ebp
        //
        edi &#61; KtrapFrame->Edi;
        esi &#61; KtrapFrame->Esi;
        ebx &#61; KtrapFrame->Ebx;
        ebp &#61; KtrapFrame->Ebp;


        //
        // 此时 stack frame 内的值为:
        //
        //     esp ---->  KtrapFrame->ErrCode
        //                KtrapFrame->Eip
        //                KtrapFrame->SegCs
        //                KtrapFrame->EFlags
        //                ktrapFrame->HardwareEsp
        //                KtrapFrame->HardwareSegSs
        //
        // 这是一个标准的调用中断 handler 入栈的情形&#xff0c;esp 指向 ErrorCode 
        //

它形成的 stack 结构如下图所示&#xff1a;


同样这个 stack 结构是基于 KtrapFrame 来构造的&#xff0c;因为此时 esp 指向 KtrapFrame 结构的 ErrCode 成员。形成这样一个典型的中断调用 stack 目的是&#xff1a;出于另一个调用途径可以使用 iret 指令来执行中断返回&#xff01;


8.5 根据调用者的权限级别来决定如何返回

最后&#xff0c;KiFastCallEntry() 根据调用者是 Ring0 还是 Ring3 来使用何种方式返回。


  1. 当属于 ring0 时&#xff1a;


            //
            // 下面进行判断两种情形&#xff1a;
            // 1. 当调用者属于 Ring 0 时&#xff0c;直接使用 jmp 指令返回到目标返回地址
            // 2. 当调用者属于 Ring 3 时&#xff0c;使用 sysexit 指令返回
            //

            esp &#61; esp &#43; 4;                                  // 跳过 ErrCode
           
            if (KtrapFrame->SegCs.RPL &#61;&#61; 0)
            {
                    //
                    // 属于 ring0 的调用&#xff0c;下面操作是&#xff1a;手动销栈&#xff0c;读出 Eip 值到 edx 寄存器
                    // 等价于下面的操作&#xff1a;
                    // pop edx
                    // pop ecx
                    // popfd
                    // jmp edx
                    //


                    edx &#61; KtrapFrame->Eip;                                  // 得到返回地址
                    ecx &#61; KtrapFrame->SegCs;                                // SegCs 值
                    eflags &#61; KtrapFrame->Eflags;                            // pop 出 eflags 寄存器


                    //
                    // 跳转到 edx 地址上&#xff0c;也就是跳到&#xff1a;ntdll!KiFastSystemCallRet() 例程
                    // 这个 ntdll!KiFastSystemCallRet() 例程只有一条 ret 指令
                    // 通过这种方式返回到 API 的调用者&#xff08;而非执行 sysexit 指令&#xff09;
                    //
                    jmp edx                                                 
            }

    返回到 ring0 时&#xff0c;分别主动 pop 出 Eip&#xff0c;SegCs 以及 Eflags 值到 edx&#xff0c;ecx 以及 eflags 寄存器&#xff0c;然后使用 jmp edx 指令直接跳转到返回地址上。当然在 pop 操作之前需要先跳过 ErrCode 码。

    显然这种情形发生在&#xff1a;在 ring0 里通过 KiFastCallEntry() 来分发系统服务例程时使用。

  2. 当属于 User 层时&#xff1a;

            else
            {
                    //
                    // 当调用者是用户代码时&#xff0c;返回的地址属于 3 级的用户层代码
                    //

                    if (KtrapFrame->EFlags.TF &#61;&#61; 1)
                    {
                            //
                            // 如果开启单步调试&#xff0c;则使用 iretd 指令返回
                            //
                            iretd;                                
                    }
                    else
                    {
                            //
                            // 下面的处理&#xff0c;目的是&#xff1a;
                            // 1. 销掉部分 stack
                            // 2. 找到返回地址到 EDX 寄存器
                            // 3. 找到返回的 esp 值到 ECX 寄存器
                            // 4. 使用 sysexit 指令返回 
                            //
                            edx &#61; KtrapFrame->Eip;                                        // pop 出 EIP 值
                            KtrapFrame->EFlags.IF &#61; 0;                                      // 清掉 stack 的中 IF 标志位
                            eflags &#61; KtrapFrame->EFlags;                                    // pop 出 eflags 值
                            ecx &#61; ktrapFrame->HardwareEsp;                                  // pop 出 Esp 值到


                            sti();                                                        // 返回前打开中断
                            sysexit;                                                      // 执行 sysexit 指令返回
                    } 
            }

    在返回 User 层里&#xff0c;当调用者的 Eflags.TF &#61; 1 时&#xff08;开启了单步调试&#xff09;时&#xff0c;直接使用 iretd 指令返回&#xff01;

    最后返回正常途径下的 User 层时这是一个典型地通过 sysexit 指令返回的情形&#xff1a;将 pop 出 Eip 值到 edx 寄存器&#xff0c;pop 出 EFlags 值到 eflags 寄存器&#xff0c;还有 pop 出 Esp 值到 ecx 寄存器&#xff0c;构造一个 sysexit 指令的执行环境&#xff0c;通过 sysexit 指令返回&#xff01;

至此&#xff1a;我对 KiFastCallEntry() 的大致流程分析完毕。

后记&#xff1a;事实上可能会多种途径通过 KiFastCallEntry() 来分发系统服务例程&#xff0c;以及返回调用者&#xff0c;这里的分析只是基于一种常用的使用途径。这里的分析并不代表全部&#xff01;敬请留意。

转载地址:http://www.mouseos.com/windows/kernel/KiFastCallEntry.html


推荐阅读
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文讨论了在Windows 8上安装gvim中插件时出现的错误加载问题。作者将EasyMotion插件放在了正确的位置,但加载时却出现了错误。作者提供了下载链接和之前放置插件的位置,并列出了出现的错误信息。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 本文介绍了九度OnlineJudge中的1002题目“Grading”的解决方法。该题目要求设计一个公平的评分过程,将每个考题分配给3个独立的专家,如果他们的评分不一致,则需要请一位裁判做出最终决定。文章详细描述了评分规则,并给出了解决该问题的程序。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 解决Cydia数据库错误:could not open file /var/lib/dpkg/status 的方法
    本文介绍了解决iOS系统中Cydia数据库错误的方法。通过使用苹果电脑上的Impactor工具和NewTerm软件,以及ifunbox工具和终端命令,可以解决该问题。具体步骤包括下载所需工具、连接手机到电脑、安装NewTerm、下载ifunbox并注册Dropbox账号、下载并解压lib.zip文件、将lib文件夹拖入Books文件夹中,并将lib文件夹拷贝到/var/目录下。以上方法适用于已经越狱且出现Cydia数据库错误的iPhone手机。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • 如何搭建Java开发环境并开发WinCE项目
    本文介绍了如何搭建Java开发环境并开发WinCE项目,包括搭建开发环境的步骤和获取SDK的几种方式。同时还解答了一些关于WinCE开发的常见问题。通过阅读本文,您将了解如何使用Java进行嵌入式开发,并能够顺利开发WinCE应用程序。 ... [详细]
  • MATLAB函数重名问题解决方法及数据导入导出操作详解
    本文介绍了解决MATLAB函数重名的方法,并详细讲解了数据导入和导出的操作。包括使用菜单导入数据、在工作区直接新建变量、粘贴数据到.m文件或.txt文件并用load命令调用、使用save命令导出数据等方法。同时还介绍了使用dlmread函数调用数据的方法。通过本文的内容,读者可以更好地处理MATLAB中的函数重名问题,并掌握数据导入导出的各种操作。 ... [详细]
  • SpringMVC接收请求参数的方式总结
    本文总结了在SpringMVC开发中处理控制器参数的各种方式,包括处理使用@RequestParam注解的参数、MultipartFile类型参数和Simple类型参数的RequestParamMethodArgumentResolver,处理@RequestBody注解的参数的RequestResponseBodyMethodProcessor,以及PathVariableMapMethodArgumentResol等子类。 ... [详细]
  • Week04面向对象设计与继承学习总结及作业要求
    本文总结了Week04面向对象设计与继承的重要知识点,包括对象、类、封装性、静态属性、静态方法、重载、继承和多态等。同时,还介绍了私有构造函数在类外部无法被调用、static不能访问非静态属性以及该类实例可以共享类里的static属性等内容。此外,还提到了作业要求,包括讲述一个在网上商城购物或在班级博客进行学习的故事,并使用Markdown的加粗标记和语句块标记标注关键名词和动词。最后,还提到了参考资料中关于UML类图如何绘制的范例。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
author-avatar
蒋易之
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有