[转载]NT 内核的进程调度分析笔记

[转载]NT 内核的进程调度分析笔记

信息来源:whitecell
文章作者:sinister
--------
NT 内核的进程调度分析笔记

Author:  sinister
Email:  [email]sinister@whitecell.org[/email]
Homepage:[url]http://www.whitecell.org[/url]
Date:   2005-11-16


2005-2-15

  众所周知 nt kernel 是多任务抢占试方式运行的,在非 SMP 系统上,每个进程分配特
定的CPU 时间片来达到执行目的,这样看上去就象多个任务在同时运行。而 nt kernel
又是以线程调度为核心的,这样线程切换了也就意味着当前进程的切换。系统不断重复
这个过程,来使每个进程得以运行。在介绍流程前,需要先了解几个系统内部结构:KPC
R、ETHREAD、EPROCESS、可以说,涉及到进程调度的函数基本都是对这几个重要结构的
设置与填充。关于各结构的细节,对系统内核有所了解的人自然都很熟悉,我就不再多
废话了。这里是结合进程调度来说。KPCR 这个结构存放的是当前 CPU 所正在处理的各
种信息,其中包括了当前正在运行的线程 ETHREAD 结构。而 ETHREAD 结构又与 EPROC
ESS 结构是相互关联的。这也就是很多核心函数通过 KPCR 启始地址 0xFFDFF000 + 偏
移就能够得到当前正在运行的线程与进程的原因,如 KeGetCurrentThread()、IoGetCu
rrentProcess() 等。这也表明在非 SMP 系统上,某时间段内当前 CPU 处理的进程只
可能有一个。当进行进程调度时系统将根据当前所存活的进程来选择让哪个线程 ETHR
EAD 结构来替换 KPCR 中的 ETHREAD,使其变为正在运行的状态。那么又是如何触发进
程调度请求能让所有进程均得以执行的呢?开始已经提到过,每个进程分配特定的 CPU
时间片来达到执行目的,而系统的 CPU 时钟中断确定了每个进程分配的时间片。也就
是当系统 CPU 时钟中断触发时,产生的进程调度请求。在详细分析调度流程与各函数
分支前,我们先来看一下大致的流程。首先当 CPU 时钟中断触发时,系统将调用 KiDi
spatchInterrupt(),比较当前进程分配的时间片,如用完后会调用 KiQuantumEnd()
根据各线程优先级等信息用特定的调度算法选择新的线程(ETHREAD),然后将其返回
值,一个 ETHREAD 结构作为参数,来调用 SwapContext() 设置 ETHREAD,EPROCESS 中
的各项参数,并替换 KPCR 中的相应结构完成线程切换,调度到另一个进程(EPROCESS)
的线程(ETHREAD)继续执行。(当线程等待某一事件(Event)或信号量(Semaphore)
时会自动放弃当前时间片) 。通过上面的大致分析,我们可以看出与进程调度密切相关
的几个函数,KiDispatchInterrupt() 与 SwapContext(),下面我们将就这两个关键的
调度函数进行详细的分析。

  首先来看下 KiDispatchInterrupt() 函数,当调用此函数时,首先得到当前 KPCR 自身
结构与DCP链表头,并比较当前是否有 DPC 正在处理,如果有 DPC 正在处理,则设置
DPC 异常炼。如果没有,则直接跳转到比较 KPCR 中 QuantumEnd 值,QuantumEnd 表示
当前 KPCR 中正在处理的线程时间片总数。此值是是根据当前运行线程 ETHREAD->Quant
um 中的值来填充的,就是说 KPCR 中 QuantumEnd 值不为0并不代表当前线程不允许切
换,此时还需要调用 KiQuantumEnd() 来近一步判断是否允许切换。所以当前值不为0时
则跳转到 KiQuantumEnd() 函数处做进一步判断。KiQuantumEnd() 函数会取当前线程
(ETHREAD)结构中的 Quantum 值来进行判断是否为0,根据线程优先等参数调用 KiFind
ReadyThread() 函数来选择一个新的线程填充 KPCR中的 NextThread ,并将 NextThread
作为函数返回值,跳转到相应地址继续进行线程切换。如果返回值返为空,则表示无法进
行切换,函数返回。但如果 KPCR 中的 QuantumEnd 本身已为0,则表示当前线程 ETHREAD
分配时间片已经用完,可以进行线程换,所以继续比较 KPCR 中是否有下一个线程(偏移
NextThread),如果KPCR 中下一线程(偏移NextThread)偏移为空,则表示没有就绪线程
可切换,直接跳转到返回地址,完成函数调用。如果不为空的话,则可继续进行线程切换。
到此所需的基本参数都已经准备就绪,下面要做的就是把 KPCR 中下一个线程(偏移Next
Thread),替换成 KPCR 中当前线程(偏移CurrentThread),并将 KPCR 中下一线程(
偏移NextThread)清0,调用 KiReadyThread() 就绪刚刚设置好的下一线程(偏移NextTh
read),也就是现在为当前线程 (偏移CurrentThread)。最后调用 SwapContext() 函
数完成最终的切换。



  下面来看一下 SwapContext() 函数的实现,上面提到过 SwapContext() 是在 KiDispatch
Interrupt() 函数中调用的,用来完成最终的线程切换。函数首先设置要切换的新线程状
态(NextThread->Status) 为运行状态。接下来判断当前是否有 DCP 列程正在运行(
KPCR 中的 DpcRoutineActive 是否为 0,不为0则表示当前有 DPC 处理,微软规定在进
行线程调度时不允许 DPC 列程运行,否则将系统崩溃,其实这不是必须的,仅仅是微软
的规定而已) 如果有则跳转到调用 KeBugCheck() 函数处,系统崩溃。如果没有则继续
取要切换的新线程(NextThread->DebugActive)调试标志状态,并赋给 KPCR 中 Debug
Active 。保存 ESP 到要被切换的旧线程 (CurrentThread->KernelStack)内核堆栈中,
并将要切换的新线程(NextThread->InitalStack,NextThread->StackLimit)中的堆栈
启始地址与大小赋给 KPCR 中的相应位置。继续取要切换的新线程(NextThread->NpxSta
te)中的 NPX 状态与CR0 的 NPX 状态进行比较,如不相等跳转到重新设置 CR0 处进行
处理。刷新 CR0 后回跳转回来继续下面的运行。(CR0 中的 NPX 位状态是 CPU 通过某
几个指令触发一个异常后进入一个特殊的状态来处理浮点指令,这时是不允许线程切换的,
所以不相同的情况下需要重新刷新 CR0 状态。 )接下来进行模式判断,判断要切换的新
线程(NextThread)是否运行在 V86 模式下,如果是继续调整内核堆栈空间。如果不是则
跳过调整。从 KPCR 中得到KTSS 地址,并将 NPX 标志位保存到 KTSS 中的 ESP0 处(这
样不论是否为 V86 模式下运行的线程都可以共享)。此时各项标志,结构与参数都已就绪,
下面的工作就是要进行具体的切换过程了。首先取要切换的新线程(NextThread->Kernel
Stack)的内核堆栈赋与当前内核堆栈指针ESP,并设置 KPCR 中的用户堆栈(TEB)为要切
换的新线程(NextThread->TEB)的用户堆栈(TEB)。然后将用户堆栈(TEB)放入 KPCR
中 GDT 的相应结构中。比较要被切换的旧线程(CurrentThread->EPROCESS)中的进程,
是否与要切换的新线程(NextThread->EPROCESS)中的进程相等?也就是判断要切换的进
程是否为当前进程,如果是则不刷新当前页目录表(CR3)以及其他相关值,而直接跳转到
添加当前进程切换计数与判断当前线程是否存在 Pending 的 APC 调用处,然后退出,完
成切换。如果要切换的线程不是当前进程,则从要切换的新线程(NextThread->EPROCESS)
中取出当前进程,并从当前进程(EPROCESS->DirectoryTableBase)中得到页目录表来更新
KPCR 中 KTSS 中的 TSSCR3 的值与 CR3 寄存器中的值,(这也就是为什么 CR3 总指向当
前进程页目录地址)继续将当前进程(EPROCESS->IopmOffset)中 IOPM 值赋与 KPCR 中
KTSS 中的 IOPM。再比较当前进程(EPROCESS->LdtDescriptor)中的 LDT 是否为空,如果
不为空则从 KPCR中取得 KGDT 的位置,并从 KGDT 中索引到 LDT,把当前进程(EPROCESS
->LdtDescriptor)中的 LDT 赋与 KPCR 中的LDT。再得到 KPCR 中 KIDT 的位置,把当前
进程(EPROCESS->Int21Descriptor)中的 INT 21中断赋与 KPCR 中 KIDT中的相应位置,
使当前进程可以调用 INT 21 。最后调用 LLDT 使当前所有设置生效。(按理说 NT 内核
中 32 位应用程序是不使用 LDT 的但为什么在线程切换中会有设置LDT的部分呢?这是为
了向下兼容 16 位的应用程序,当调度到一个 16 位的应用程时则会特意为它分配 LDT 并
且使 IDT 中的 INT 21 有效,玩过 DOS 的人都知道 INT 21 是 DOS 下的系统调用,可
以试着运行一个 16 位的 DOS 程序,然后观察下 IDT 表就会发现,原来没有用到的 INT
21 会被设置成一个 16 位的 TrapGate)否则如果为空则设置成不使用 LDT,把要切换的
新线程(NextThread->ContextSwitches)中的切换次数与 KPCR 中的切换总和各加一,
恢复异常链,并比较要切换的新线程(NextThread->KernelApcPending)中的 APC 调用
是否没有完成 ,如果当前 APC 状态没有完成的话则判断当前是否可以处理 APC 调用,
如果不能则设置返回标志为 Pending 完成线程切换的所有工作并返回。否则设置当前
IRQL 为 APC LEVEL 并调用 HalRequestSoftwareInterrupt() 函数来处理 APC Pending
状态,处理完成后清除 Pending 状态,完成线程切换的所有工作并返回。如果当前 APC
状态完成的话,则恢复各寄存器和标志寄存器的值并返回,完成线程切换的所有工作。



当调用 KiDispatchInterrupt() 函数时,

:u KiDispatchInterrupt l 1000
ntoskrnl!KiDispatchInterrupt
0008:80467DD0  MOV     EBX,[FFDFF01C]
0008:80467DD6  LEA     EAX,[EBX+00000800]

得到当前 KPCR 自身结构与DCP链表头,EAX=DPC,EBX=KPCR

0008:80467DDC  CLI
0008:80467DDD  CMP     EAX,[EAX]
0008:80467DDF  JZ     80467DFE

关中断,并比较当前 DPC 是否为空,如果为空,直接跳转到比较 KPCR 中
QuantumEnd 值和比较是否有下一个线程(ETHREAD)结构

0008:80467DE1  PUSH    EBP
0008:80467DE2  PUSH    DWORD PTR [EBX]
0008:80467DE4  MOV     DWORD PTR [EBX],FFFFFFFF
0008:80467DEA  MOV     EDX,ESP
0008:80467DEC  MOV     ESP,[EBX+0000081C]  注释: 得到 DPC 堆栈
0008:80467DF2  PUSH    EDX
0008:80467DF3  MOV     EBP,EAX
0008:80467DF5  CALL    804633E7  注释: KiRetireDpcList()函数
0008:80467DFA  POP     ESP
0008:80467DFB  POP     DWORD PTR [EBX]
0008:80467DFD  POP     EBP

设置 DPC 异常链


0008:80467DFE  STI
0008:80467DFF  CMP     DWORD PTR [EBX+00000870],00  注释: QuantumEnd 线程时间片
0008:80467E06  JNZ    80467E5A

如果 KPCR 中 QuantumEnd 值不为 0 则表示可能当前线程(ETHREAD)时间片没有用完,
跳转到判断当前线程(ETHREAD) 是否可以进行切换。

0008:80467E08  CMP     DWORD PTR [EBX+00000128],00  注释: NextThread (ETHREAD结构)
0008:80467E0F  JZ     80467E59

是否有下一个线程,如果 KPCR 中下一线程 NextThread(ETHREAD)偏移为空,则直接跳
转到返回地址,完成函数调用。

0008:80467E11  MOV     EAX,[EBX+00000128]  

此时 EAX = NextThread (ETHREAD结构)

0008:80467E17  SUB     ESP,0C
0008:80467E1A  MOV     [ESP+08],ESI
0008:80467E1E  MOV     [ESP+04],EDI
0008:80467E22  MOV     [ESP],EBP
0008:80467E25  MOV     ESI,EAX  
0008:80467E27  MOV     EDI,[EBX+00000124]  注释: CurrentThread (ETHREAD结构)
0008:80467E2D  MOV     DWORD PTR [EBX+00000128],00000000
0008:80467E37  MOV     [EBX+00000124],ESI
0008:80467E3D  MOV     ECX,EDI
0008:80467E3F  CALL    8042F944  注释: KiReadyThread函数

把 KPCR 中下一个线程 NextThread (ETHREAD) 结构,替换成 KPCR 中当前 Current
Thread(ETHREAD)线程结构,并将 KPCR 中下一线程 NextThread (ETHREAD) 偏移清0,
调用 KiReadyThread() 就绪刚刚设置好的下一线程 NextThread(ETHREAD) 结构,现在
为当前线程 CurrentThread (ETHREAD) 结构。

0008:80467E44  MOV     CL,01

设置 APC IRQL 标志,SwapContext() 调用会先设置当前IRQL 为 APC_LEVEL。

0008:80467E46  CALL    80467E70 注释: SwapContext函数
0008:80467E4B  MOV     EBP,[ESP]
0008:80467E4E  MOV     EDI,[ESP+04]
0008:80467E52  MOV     ESI,[ESP+08]
0008:80467E56  ADD     ESP,0C

调用 SwapContext() 完成线程切换。(__fastcall 调用规范,平衡堆栈)

0008:80467E59  RET  注释: KiDispatchInterrupt 函数调用完毕,返回。

0008:80467E5A  MOV     DWORD PTR [EBX+00000870],00000000  注释: QuantumEnd
0008:80467E64  CALL    804306D2                   注释: KiQuantumEnd
0008:80467E69  OR      EAX,EAX                   注释: EAX = NextThread(ETHREAD)
0008:80467E6B  JNZ    80467E17  
0008:80467E6D  RET 注释: KiDispatchInterrupt 函数调用完毕,返回。

设置 KPCR 中 QuantumEnd 值为 0,并调用 KiQuantumEnd 相应处理,KiQuantumEnd 函数
会取当前线程(ETHREAD)结构中的 Quantum 值来进行判断是否为0,如果可以进行线程切
换,则根据线程优先等参数调用 KiFindReadyThread() 函数来选择一个新的线程填充 KPC
R 中的 NextThread (ETHREAD结构),并将 NextThread (ETHREAD结构) 赋给 EAX,并
跳转到线程切换地址进行切换。如果 EAX 返回为空,则表示无法进行切换,函数返回。

_______________________________________________________________________________________

  以上基本把线程调度各分之走完,可以大概看出一个进程交替运行的流程,下面
的 SwapContext 函数完成实际切换。

SwapContext()  

EBX = KPCR
ESI = NextThread
EDI = CurrentThread

0008:80467E6E  MOV     EDI,EDI
0008:80467E70  OR      CL,CL
0008:80467E72  MOV     BYTE PTR ES:[ESI+2D],02 (ETHREAD->State)

设置要切换的新线程NextThread(ETHREAD)为运行状态。

0008:80467E77  PUSHFD
0008:80467E78  MOV     ECX,[EBX]
0008:80467E7A  CMP     DWORD PTR [EBX+0000080C],00
0008:80467E81  PUSH    ECX
0008:80467E82  JNZ    80467F7D

线程切换时不允许有 DPC 列程产生,所以先比较 KPCR 中的
DpcRoutineActive 是否为 0,不为0则表示当前有 DPC 处理,
跳转到 KeBugCheck() 函数处显示蓝屏。

0008:80467E88  MOV     EBP,CR0    注释:EBP = CR0
0008:80467E8B  MOV     EDX,EBP    注释:EDX = CR0
0008:80467E8D  MOV     CL,[ESI+2C]
0008:80467E90  MOV     [EBX+50],CL

取要切换的新线程NextThread(ETHREAD)调试标志状态,并赋给 KPCR 中
DebugActive 相应标志。

0008:80467E93  CLI
0008:80467E94  MOV     [EDI+28],ESP

将堆栈指针赋给要被切换的旧线程 CurrentThread(ETHREAD)中的 KernelStack。

0008:80467E97  MOV     EAX,[ESI+18]
0008:80467E9A  MOV     ECX,[ESI+1C]
0008:80467E9D  SUB     EAX,00000210
0008:80467EA2  MOV     [EBX+08],ECX
0008:80467EA5  MOV     [EBX+04],EAX

将要切换的新线程NextThread(ETHREAD)中的初始化堆栈(InitalStack)与
堆栈大小(StackLimit)赋给 KPCR 中的相应位置,以便处理。

0008:80467EA8  XOR     ECX,ECX
0008:80467EAA  MOV     CL,[ESI+31]
0008:80467EAD  AND     EDX,-0F
0008:80467EB0  OR      ECX,EDX
0008:80467EB2  OR      ECX,[EAX+0000020C]
0008:80467EB8  CMP     EBP,ECX
0008:80467EBA  JNZ    80467F75

取要切换的新线程NextThread(ETHREAD)中的 NPX 位与 CR0 的 NPX 位
进行比较,如不相等跳转到重新设置 CR0 处进行处理。


0008:80467EC0  TEST    DWORD PTR [EAX-1C],00020000
0008:80467EC7  JNZ    80467ECC

判断当前是否为 V86模式,如果不是直接跳到取得 KTSS 处。

0008:80467EC9  SUB     EAX,10

如果是 V86 模式则继续调整内核堆栈空间。

0008:80467ECC  MOV     ECX,[EBX+40]  注释:ECX = KPCR->KTSS
0008:80467ECF  MOV     [ECX+04],EAX
0008:80467ED2  MOV     ESP,[ESI+28]  
0008:80467ED5  MOV     EAX,[ESI+20]
0008:80467ED8  MOV     [EBX+18],EAX  注释:EAX = TEB

从 KPCR 中得到 KTSS 地址,并将 NPX 位保存到 KTSS 中的 ESP0 处,取
要切换的新线程NextThread(ETHREAD)的内核堆栈(KernelStack )赋与
ESP,并设置 KPCR 中的用户堆栈(TEB) 为要切换的新线程NextThread
(ETHREAD)的用户堆栈(TEB)。

0008:80467EDB  STI
0008:80467EDC  MOV     ECX,[EBX+3C]  注释:ECX = KPCR->GDT
0008:80467EDF  MOV     [ECX+3A],AX
0008:80467EE3  SHR     EAX,10
0008:80467EE6  MOV     [ECX+3C],AL
0008:80467EE9  SHR     EAX,08
0008:80467EEC  MOV     [ECX+3F],AL
0008:80467EEF  MOV     EAX,[EDI+44]
0008:80467EF2  CMP     EAX,[ESI+44]
0008:80467EF5  JZ     80467F20

将用户堆栈(TEB)放入 KPCR 中 GDT 的相应结构中。比较要被切换的
旧线程 CurrentThread(ETHREAD)中的 EPROCESS,与要切换的新线
程NextThread(ETHREAD)中的 EPROCESS 是否相等?也就是判断要切换
的是否为当前进程,如果是则不设置当前页目录表(CR3)以及其他相关
值,而直接跳转到添加当前进程切换计数与判断当前线程是否存在 Pending
的 APC 调用处,然后退出,完成切换。

0008:80467EF7  MOV     EDI,[ESI+44]  注释:EDI = NextThread->EPROCESS
0008:80467EFA  XOR     EAX,EAX
0008:80467EFC  MOV     GS,AX
0008:80467EFF  MOV     EAX,[EDI+18]  注释:EAX = EPROCESS->DirectoryTableBase
0008:80467F02  MOV     EBP,[EBX+40]  注释:EBP = KPCR->KTSS
0008:80467F05  MOV     ECX,[EDI+30]  注释:EDI = EPROCESS->IopmOffset
0008:80467F08  MOV     [EBP+1C],EAX
0008:80467F0B  MOV     CR3,EAX     注释:CR3 = EPROCESS->DirectoryTableBase
0008:80467F0E  MOV     [EBP+66],CX
0008:80467F12  XOR     EAX,EAX
0008:80467F14  CMP     [EDI+20],AX  
0008:80467F18  JNZ    80467F47
0008:80467F1A  LLDT    AX
0008:80467F1D  LEA     ECX,[ECX+00]

如果要切换的线程不是当前进程,则从要切换的新线程NextThread(ETHREAD)
中取当前进程(EPROCESS),并从当前进程(EPROCESS)中得到页目录表来更
新KPCR->KTSS中的TSSCR3的值与 CR3 寄存器中的值,继续将当前进程(EPROCESS)中
IOPM 值赋与 KPCR-KTSS 中的 IOPM。再比较当前进程(EPROCESS)中的 LDT 是否为
空,如果不为空则跳转到设置 LDT 处执行。否则设置 LDT 为空。


0008:80467F20  INC     DWORD PTR [ESI+4C]
0008:80467F23  INC     DWORD PTR [EBX+000005C0]
0008:80467F29  POP     ECX
0008:80467F2A  MOV     [EBX],ECX
0008:80467F2C  CMP     BYTE PTR [ESI+49],00
0008:80467F30  JNZ    80467F36
0008:80467F32  POPFD
0008:80467F33  XOR     EAX,EAX
0008:80467F35  RET  注释: SwapContext 函数调用完毕,返回。


把要切换的新线程NextThread(ETHREAD)中的 ContextSwitches 与 KPCR 中的
KeContextSwitches 各加一,这两个值表示进行线程切换的次数。恢复异常链,
并比较要切换的新线程NextThread(ETHREAD)中的 KernelApcPending,如果当前
APC 状态是 Pending 的话则跳转到处理 APC Pending 地址继续。不为 Pending 的
话,则恢复各寄存器和标志寄存器的值并返回,完成线程切换的所有工作。


0008:80467F36  POPFD
0008:80467F37  JNZ    80467F3C
0008:80467F39  MOV     AL,01
0008:80467F3B  RET

判断当前是否可以处理 APC 调用,如果不能则设置返回标志为 Pending 完成线程切换
的所有工作并返回。否则跳转到处理软中断地址继续。

0008:80467F3C  MOV     CL,01  注释: IRQL = APC LEVEL
0008:80467F3E  CALL    [HAL!HalRequestSoftwareInterrupt]
0008:80467F44  XOR     EAX,EAX
0008:80467F46  RET

设置当前 IRQL 为 APC LEVEL 并调用 HalRequestSoftwareInterrupt() 函数来处理
APC Pending 状态,处理完成后清除 Pending 状态,完成线程切换的所有工作并返回。

0008:80467F47  MOV     EBP,[EBX+3C]   注释: EBP = KPCR->KGDT
0008:80467F4A  MOV     EAX,[EDI+20]   注释: EAX = EPROCESS->LdtDescriptor
0008:80467F4D  MOV     [EBP+48],EAX
0008:80467F50  MOV     EAX,[EDI+24]
0008:80467F53  MOV     [EBP+4C],EAX
0008:80467F56  MOV     EAX,00000048
0008:80467F5B  MOV     EBP,[EBX+38]   注释: EBP = KPCR->KIDT
0008:80467F5E  MOV     ECX,[EDI+28]   注释: ECX = EPROCESS->Int21Descriptor
0008:80467F61  MOV     [EBP+00000108],ECX
0008:80467F67  MOV     ECX,[EDI+2C]
0008:80467F6A  MOV     [EBP+0000010C],ECX
0008:80467F70  LLDT    AX
0008:80467F73  JMP    80467F20

从 KPCR 中取得 KGDT 的位置,并从 KGDT 中索引到 LDT,把当前进程(EPROCESS)中的
LDT 赋与 KPCR 中的 LDT。再得到 KPCR 中 KIDT 的位置,把当前进程(EPROCESS)中的
INT 21中断赋与 KPCR 中 KIDT 中的相应位置,使当前进程可以调用 INT 21 。最后调用
LLDT 使当前所有设置生效后跳转回添加线程切换次数与判断当前 APC Pending 处继续运
行。


0008:80467F75  MOV     CR0,ECX
0008:80467F78  JMP    80467EC0

重新设置 CR0 的 NPX 位,并向上跳转到判断是否为 V86 模式处继续。

0008:80467F7D  PUSH    000000B8
0008:80467F82  CALL    ntoskrnl!KeBugCheck
0008:80467F87  RET

调用蓝屏函数,系统崩溃重新启动。





  笔记是今年春节利用放假时间写的,当时分析到一半才发现原来 WIN2K 源代码中已
经包含了此部分,无奈已经把汇编进行了简单的注释,索性就这样写下去。错误之处再
所难免,还望得到您的指正。


参考资源: Windows 2000 源代码
感谢 FlashSky,SoBeIt 与我探讨。




WSS(Whitecell Security Systems),一个非营利性民间技术组织,致力于各种系统安全技术的研究。坚持传统的hacker精神,追求技术的精纯。
WSS 主页:[url]http://www.whitecell.org/[/url]
WSS 论坛:[url]http://www.whitecell.org/forums/[/url]
页: [1]
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值