程式執行緒篇——執行緒切換(上)

寂靜的羽夏發表於2021-12-04

寫在前面

  此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統核心的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統核心——簡述 ,方便學習本教程。

  看此教程之前,問幾個問題,基礎知識儲備好了嗎?保護模式篇學會了嗎?系統呼叫篇學會了嗎?練習做完了嗎?沒有的話就不要繼續了。


? 華麗的分割線 ?


練習及參考

本次答案均為參考,可以與我的答案不一致,但必須成功通過。程式碼在摺疊區,而思考原因解釋將會在文章內部講解。

1️⃣ 斷鏈程式結構體,實現隱藏,並思考為什麼斷鏈程式為什麼還能夠執行。

? 點選檢視應用程式碼 ?
#include "stdafx.h"
#include <windows.h>
#include <winioctl.h>
#include <stdlib.h>

//操作碼:0x0-0x7FF 被保留,0x800-0xFFF 可用
#define Hide CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define _Show CTL_CODE(FILE_DEVICE_UNKNOWN,0x900,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define SYMBOL_LINK_NAME L"\\\\.\\HideProcess"

HANDLE g_Device;

int main(int argc, char* argv[])
{
   //獲取驅動連結物件控制程式碼
   g_Device=CreateFileW(SYMBOL_LINK_NAME,GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
   if (g_Device==INVALID_HANDLE_VALUE)
   {
      puts("訪問驅動符號連結失敗!");
      goto endproc;
   }

   DWORD pid;
   DWORD outBuffer;
   DWORD m[2];
   DWORD re;

   puts("請輸入需要隱藏程式的 pid :");
   scanf("%d",&pid);

   if (pid)
   {
      if (DeviceIoControl(g_Device,Hide,&pid,sizeof(DWORD), outBuffer,sizeof(DWORD),&re,NULL))
      {
         puts("隱藏命令傳送成功,請檢視程式是否隱藏……");
      }  
      puts("檢查工作管理員,按任意鍵恢復隱藏!");  
      m[0]=GetCurrentProcessId();
      m[1]=outBuffer;
      if (DeviceIoControl(g_Device,_Show,m,sizeof(DWORD)*2, outBuffer,sizeof(DWORD),&re,NULL))
      {
         puts("恢復命令傳送成功,請檢視程式是否顯示……");
      }
   }
   else
   {
      puts("pid 非法!");
   }

endproc:

    CloseHandle(g_Device);
    system("pause");
    return 0;
}

? 點選檢視驅動程式碼 ?
#include <ntifs.h>
#include <ntddk.h>

UNICODE_STRING Devicename;
UNICODE_STRING SymbolLink;

#define DEVICE_NAME L"\\Device\\HideProcess"
#define SYMBOL_LINK L"\\??\\HideProcess"
#define Hide CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define Show CTL_CODE(FILE_DEVICE_UNKNOWN,0x900,METHOD_BUFFERED,FILE_ANY_ACCESS)

NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
    IoDeleteSymbolicLink(&SymbolLink);
    IoDeleteDevice(DriverObject->DeviceObject); //假設 DriverEntry 函式中的 dobj 變數宣告為全域性的,為什麼不使用這個刪除裝置物件
    DbgPrint("解除安裝成功!!!");
}

NTSTATUS DispatchFunction(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
    
    NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
    PIO_STACK_LOCATION pIrqlStack;
    ULONG uIoControlCode;
    PVOID pIoBuffer;
    ULONG uInLength;
    ULONG uOutLength;
    ULONG uRead[2];
    ULONG uWrite;

    pIrqlStack = IoGetCurrentIrpStackLocation(pIrp);
    uIoControlCode = pIrqlStack->Parameters.DeviceIoControl.IoControlCode;

    pIoBuffer = pIrp->AssociatedIrp.SystemBuffer;
    uInLength = pIrqlStack->Parameters.DeviceIoControl.InputBufferLength;
    uOutLength = pIrqlStack->Parameters.DeviceIoControl.OutputBufferLength;

    LIST_ENTRY* lethis;
    LIST_ENTRY* fle;
    LIST_ENTRY* ble;
    PEPROCESS eProcess;

    switch (uIoControlCode)
    {
        case Hide:
            RtlMoveMemory(uRead, pIoBuffer, 4);
            PsLookupProcessByProcessId(uRead[0], &eProcess);

            RtlMoveMemory(pIoBuffer, &eProcess, 4);
            pIrp->IoStatus.Information = 4;

            lethis = (LIST_ENTRY*)((ULONG)eProcess + 0x88);
            fle = lethis->Flink;
            ble = lethis->Blink;

            fle->Blink = ble;
            ble->Flink = fle;

            DbgPrint("斷鏈成功!!!");
            break;
        case Show:

            RtlMoveMemory(uRead, pIoBuffer, 8);
            PsLookupProcessByProcessId(uRead[0], &eProcess);     //第一個成員:pid

            lethis = (LIST_ENTRY*)((ULONG)eProcess + 0x88);  //自身的pid
            ble= (LIST_ENTRY*)((ULONG)uRead[1] + 0x88);

            fle = lethis->Flink;
           
            lethis->Flink = ble;
            ble->Blink = lethis;

            ble->Flink = fle;
            fle->Blink = ble;

            DbgPrint("恢復成功!!!");
            pIrp->IoStatus.Information = 0;
            break;

    }
    pIrp->IoStatus.Status = STATUS_SUCCESS;    
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

NTSTATUS DefaultDispatchFunction(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
   pIrp->IoStatus.Information = 0;
   pIrp->IoStatus.Status = STATUS_SUCCESS;
   IoCompleteRequest(pIrp, IO_NO_INCREMENT);
   return STATUS_SUCCESS;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{

    DriverObject->DriverUnload = UnloadDriver;

    UNICODE_STRING Devicename;
    RtlInitUnicodeString(&Devicename, DEVICE_NAME);

    DEVICE_OBJECT dobj;
    IoCreateDevice(DriverObject, 0, &Devicename, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &dobj);
    RtlInitUnicodeString(&SymbolLink, SYMBOL_LINK);
    IoCreateSymbolicLink(&SymbolLink, &Devicename);

    DriverObject->Flags |= DO_BUFFERED_IO;

    DriverObject->MajorFunction[IRP_MJ_CREATE] = DefaultDispatchFunction;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = DefaultDispatchFunction;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchFunction;

    DbgPrint("載入成功!!!");

    return STATUS_SUCCESS;
}
? 點選檢視答案 ?


  先給個效果圖來看看:

程式執行緒篇——執行緒切換(上)

  註釋中有一個問題:假設DriverEntry函式中的dobj變數宣告為全域性的,為什麼不使用這個刪除裝置物件。你可以試試,答案是不可以,會導致藍屏,原因這個東西是使用的分頁記憶體,而這個解除安裝驅動的執行緒是不能使用分頁記憶體的。


2️⃣ 使用DebugPort清零實現反除錯。

? 點選檢視應用程式碼 ?
#include "stdafx.h"
#include <windows.h>
#include <winioctl.h>
#include <stdlib.h>

//操作碼:0x0-0x7FF 被保留,0x800-0xFFF 可用
#define Hide CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define SYMBOL_LINK_NAME L"\\\\.\\HideProcess"

HANDLE g_Device;

int main(int argc, char* argv[])
{
   //獲取驅動連結物件控制程式碼
   g_Device=CreateFileW(SYMBOL_LINK_NAME,GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
   if (g_Device==INVALID_HANDLE_VALUE)
   {
      puts("訪問驅動符號連結失敗!");
      goto endproc;
   }

   DWORD pid;
   DWORD outBuffer;
   DWORD re;
   
   puts("請輸入需要反除錯保護程式的 pid :");
   scanf("%d",&pid);
   
   if (pid)
   {
      if (DeviceIoControl(g_Device,Hide,&pid,sizeof(DWORD), &outBuffer,sizeof(DWORD),&re,NULL))
      {
         puts("反除錯保護命令傳送成功……");
      }  
   }
   else
   {
      puts("pid 非法!");
   }
   
endproc:
   
   CloseHandle(g_Device);
   system("pause");
   return 0;
}
}
? 點選檢視驅動程式碼 ?
#include <ntifs.h>
#include <ntddk.h>

UNICODE_STRING Devicename;
UNICODE_STRING SymbolLink;

ULONG* pDebugPort=NULL;
HANDLE hThread;

#define DEVICE_NAME L"\\Device\\HideProcess"
#define SYMBOL_LINK L"\\??\\HideProcess"
#define Hide CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)

VOID HideThread(_In_ PVOID StartContext)
{
    while (1)
    {
      *pDebugPort = 0;
    }
}

NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
    IoDeleteDevice(DriverObject);
    IoDeleteSymbolicLink(&SymbolLink);
    DbgPrint("解除安裝成功!!!");
}

NTSTATUS DispatchFunction(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
    NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
    PIO_STACK_LOCATION pIrqlStack;
    ULONG uIoControlCode;
    PVOID pIoBuffer;
    ULONG uInLength;
    ULONG uOutLength;
    ULONG uRead;
    ULONG uWrite;

    pIrqlStack = IoGetCurrentIrpStackLocation(pIrp);
    uIoControlCode = pIrqlStack->Parameters.DeviceIoControl.IoControlCode;

    pIoBuffer = pIrp->AssociatedIrp.SystemBuffer;
    uInLength = pIrqlStack->Parameters.DeviceIoControl.InputBufferLength;
    uOutLength = pIrqlStack->Parameters.DeviceIoControl.OutputBufferLength;

    PEPROCESS eProcess;

    switch (uIoControlCode)
    {
        case Hide:
            RtlMoveMemory(&uRead, pIoBuffer, 4);
            PsLookupProcessByProcessId(uRead, &eProcess);

            if (pDebugPort)
                break;

            pDebugPort = (ULONG*)((ULONG)eProcess + 0xbc);

            PsCreateSystemThread(&hThread, GENERIC_ALL, NULL, NULL, NULL, HideThread, NULL);

            DbgPrint("斷鏈成功!!!");
            break;
    }
    pIrp->IoStatus.Information = 0;
    pIrp->IoStatus.Status = STATUS_SUCCESS;    
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

NTSTATUS DefaultDispatchFunction(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
   pIrp->IoStatus.Information = 0;
   pIrp->IoStatus.Status = STATUS_SUCCESS;
   IoCompleteRequest(pIrp, IO_NO_INCREMENT);
   return STATUS_SUCCESS;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{

    DriverObject->DriverUnload = UnloadDriver;

    UNICODE_STRING Devicename;
    RtlInitUnicodeString(&Devicename, DEVICE_NAME);

    DEVICE_OBJECT dobj;
    IoCreateDevice(DriverObject, 0, &Devicename, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &dobj);
    RtlInitUnicodeString(&SymbolLink, SYMBOL_LINK);
    IoCreateSymbolicLink(&SymbolLink, &Devicename);

    DriverObject->Flags |= DO_BUFFERED_IO;

    DriverObject->MajorFunction[IRP_MJ_CREATE] = DefaultDispatchFunction;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = DefaultDispatchFunction;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchFunction;

    DbgPrint("載入成功!!!");

    return STATUS_SUCCESS;
}

3️⃣ 如何判斷一個程式是否為GUI執行緒。

? 點選檢視答案 ?


  我預先在虛擬機器開啟一個DebugView,然後在WinDbug開始輸入指令,遍歷程式:

kd> !process 0 0
****NT ACTIVE PROCESS DUMP****
Failed to get VadRoot
PROCESS 89c258c0  SessionId: 0  Cid: 0758    Peb: 7ffd3000  ParentCid: 05f4
    DirBase: 13ac01a0  ObjectTable: e1d63088  HandleCount:  57.
    Image: Dbgview.exe

  然後再看看這個程式資訊:

kd> !process 89c258c0
Failed to get VadRoot
PROCESS 89c258c0  SessionId: 0  Cid: 0758    Peb: 7ffd3000  ParentCid: 05f4
    DirBase: 13ac01a0  ObjectTable: e1d63088  HandleCount:  57.
    Image: Dbgview.exe
    VadRoot 00000000 Vads 0 Clone 0 Private 192. Modified 7. Locked 0.
    DeviceMap e1638c88
    Token                             e176a7d0
    ElapsedTime                       00:00:05.476
    UserTime                          00:00:00.010
    KernelTime                        00:00:00.010
    QuotaPoolUsage[PagedPool]         0
    QuotaPoolUsage[NonPagedPool]      0
    Working Set Sizes (now,min,max)  (974, 50, 345) (3896KB, 200KB, 1380KB)
    PeakWorkingSetSize                974
    VirtualSize                       30 Mb
    PeakVirtualSize                   32 Mb
    PageFaultCount                    1030
    MemoryPriority                    FOREGROUND
    BasePriority                      8
    CommitCharge                      392

THREAD 898b0020 Cid 0758.0760 Teb: 7ffdf000 Win32Thread: e1e6d2c8 WAIT: (UserRequest) UserMode Non-Alertable
89b0b6a8 SynchronizationEvent
89b43930 SynchronizationEvent
Not impersonating
DeviceMap e1638c88
Owning Process 00000000 Image:
Attached Process 89c258c0 Image: Dbgview.exe
Wait Start TickCount 6228 Ticks: 1 (0:00:00:00.010)
Context Switch Count 225 IdealProcessor: 0 LargeStack
UserTime 00:00:00.000
KernelTime 00:00:00.010
Win32 Start Address 0x00413487
Stack Init b8138000 Current b813795c Base b8138000 Limit b8132000 Call 00000000
Priority 9 BasePriority 8 PriorityDecrement 0 IoPriority 0 PagePriority 0
ChildEBP RetAddr
b8137974 80501cd6 nt!KiSwapContext+0x2e (FPO: [Uses EBP] [0,0,4])
b8137980 804faae2 nt!KiSwapThread+0x46 (FPO: [0,0,0])
b81379b8 805b7418 nt!KeWaitForMultipleObjects+0x284 (FPO: [Non-Fpo])
b8137d48 8053e638 nt!NtWaitForMultipleObjects+0x2a2 (FPO: [Non-Fpo])

b8137d48 7c92e4f4 (T) nt!KiFastCallEntry+0xf8 (FPO: [0,0] TrapFrame @ b8137d64)

0012fc84 00000000 (T) ntdll!KiFastSystemCallRet (FPO: [0,0,0])

THREAD 89cca928 Cid 0758.0764 Teb: 7ffde000 Win32Thread: 00000000 WAIT: (DelayExecution) UserMode Alertable
89ccaa18 NotificationTimer
Not impersonating
DeviceMap e1638c88
Owning Process 00000000 Image:
Attached Process 89c258c0 Image: Dbgview.exe
Wait Start TickCount 6228 Ticks: 1 (0:00:00:00.010)
Context Switch Count 13 IdealProcessor: 0
UserTime 00:00:00.000
KernelTime 00:00:00.000
Win32 Start Address 0x0043261b
Stack Init b873e000 Current b873dcbc Base b873e000 Limit b873b000 Call 00000000
Priority 8 BasePriority 8 PriorityDecrement 0 IoPriority 0 PagePriority 0
ChildEBP RetAddr
b873dcd4 80501cd6 nt!KiSwapContext+0x2e (FPO: [Uses EBP] [0,0,4])
b873dce0 804fa79f nt!KiSwapThread+0x46 (FPO: [0,0,0])
b873dd0c 8060db19 nt!KeDelayExecutionThread+0x1c9 (FPO: [Non-Fpo])
b873dd54 8053e638 nt!NtDelayExecution+0x87 (FPO: [Non-Fpo])

b873dd54 7c92e4f4 (T) nt!KiFastCallEntry+0xf8 (FPO: [0,0] TrapFrame @ b873dd64)

00d4ff68 00000000 (T) ntdll!KiFastSystemCallRet (FPO: [0,0,0])

  可以裡面有兩個程式,其中有一個是GUI執行緒,一個為普通執行緒,那麼是如何知道的呢?我們檢視它們的ServiceTable

kd> dt _KTHREAD 898b0020
ntdll!_KTHREAD

+0x0e0 ServiceTable : 0x80553f60 Void

kd> dt _KTHREAD 89cca928
ntdll!_KTHREAD

+0x0e0 ServiceTable : 0x80553fa0 Void

kd> dd KeServiceDescriptorTable
80553fa0 80502b8c 00000000 0000011c 80503000
80553fb0 00000000 00000000 00000000 00000000
80553fc0 00000000 00000000 00000000 00000000
80553fd0 00000000 00000000 00000000 00000000

kd> dd KeServiceDescriptorTableShadow
80553f60 80502b8c 00000000 0000011c 80503000
80553f70 bf999b80 00000000 0000029b bf99a890
80553f80 00000000 00000000 00000000 00000000
80553f90 00000000 00000000 00000000 00000000

  KeServiceDescriptorTableShadowKeServiceDescriptorTable沒有的有關GUI執行緒相關的服務表,是不是很容易判斷出來哪個是GUI執行緒了嗎?是KeServiceDescriptorTableShadowGUI執行緒,是KeServiceDescriptorTable則為普通執行緒。


4️⃣ 斷鏈執行緒結構體,實現隱藏,並思考為什麼斷鏈執行緒為什麼還能夠執行。

? 點選檢視應用程式碼 ?
#include "stdafx.h"
#include <windows.h>
#include <winioctl.h>
#include <stdlib.h>

//操作碼:0x0-0x7FF 被保留,0x800-0xFFF 可用
#define Hide CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define SYMBOL_LINK_NAME L"\\\\.\\HideThread"

HANDLE g_Device;

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
   int i=0;
   while (1)
   {
      printf("\n%d:執行緒老子還活著!!!\n",i++);
      Sleep(1500);
   }
   return 0;
}

int main(int argc, char* argv[])
{
   //獲取驅動連結物件控制程式碼
   g_Device=CreateFileW(SYMBOL_LINK_NAME,GENERIC_READ GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0   ;
   if (g_Device!=INVALID_HANDLE_VALUE)
   {
      DWORD tid;
      DWORD re;
      DWORD outBuffer;

      HANDLE hthread = CreateThread(NULL,NULL   (LPTHREAD_START_ROUTINE)ThreadProc,NULL,NULL,&tid);
      CloseHandle(hthread);

      if (hthread==INVALID_HANDLE_VALUE)
      {
         puts("執行緒建立失敗!!!");
         goto endproc;
      }  
      system("pause");

      if (DeviceIoControl(g_Device,Hide,&tid,sizeof(DWORD), outBuffer,sizeof(DWORD),&re,NULL))
      {
         puts("隱藏命令正在傳送,請檢視執行緒數是否減少……");
      }  
      system("pause");
   }
   else
   {
      puts("訪問驅動符號連結失敗!");
   }

endproc:

   CloseHandle(g_Device);
   system("pause");
   return 0;
}
? 點選檢視驅動程式碼 ?
#include <ntifs.h>
#include <ntddk.h>

UNICODE_STRING Devicename;
UNICODE_STRING SymbolLink;

#define DEVICE_NAME L"\\Device\\HideThread"
#define SYMBOL_LINK L"\\??\\HideThread"
#define Hide CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)

NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
    IoDeleteSymbolicLink(&SymbolLink);
    IoDeleteDevice(DriverObject->DeviceObject);
    DbgPrint("解除安裝成功!!!");
}

NTSTATUS DispatchFunction(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
    
    NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
    PIO_STACK_LOCATION pIrqlStack;
    ULONG uIoControlCode;
    PVOID pIoBuffer;
    ULONG uInLength;
    ULONG uOutLength;
    ULONG uRead;
    ULONG uWrite;

    pIrqlStack = IoGetCurrentIrpStackLocation(pIrp);
    uIoControlCode = pIrqlStack->Parameters.DeviceIoControl.IoControlCode;

    pIoBuffer = pIrp->AssociatedIrp.SystemBuffer;
    uInLength = pIrqlStack->Parameters.DeviceIoControl.InputBufferLength;
    uOutLength = pIrqlStack->Parameters.DeviceIoControl.OutputBufferLength;

    LIST_ENTRY* lethis;
    LIST_ENTRY* fle;
    LIST_ENTRY* ble;
    PETHREAD eThread;
    
    switch (uIoControlCode)
    {
        case Hide:
            RtlMoveMemory(&uRead, pIoBuffer,4);         
            PsLookupThreadByThreadId(uRead, &eThread);

            lethis = (LIST_ENTRY*)((ULONG)eThread + 0x1B0);
            fle = lethis->Flink;
            ble = lethis->Blink;

            fle->Blink = ble;
            ble->Flink = fle;

            lethis = (LIST_ENTRY*)((ULONG)eThread + 0x22C);
            fle = lethis->Flink;
            ble = lethis->Blink;

            fle->Blink = ble;
            ble->Flink = fle;

            DbgPrint("斷鏈成功!!!");
            break;
    }

    pIrp->IoStatus.Information = 0;
    pIrp->IoStatus.Status = STATUS_SUCCESS;    
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

NTSTATUS DefaultDispatchFunction(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
   pIrp->IoStatus.Information = 0;
   pIrp->IoStatus.Status = STATUS_SUCCESS;
   IoCompleteRequest(pIrp, IO_NO_INCREMENT);
   return STATUS_SUCCESS;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{

    DriverObject->DriverUnload = UnloadDriver;

    UNICODE_STRING Devicename;
    RtlInitUnicodeString(&Devicename, DEVICE_NAME);

    DEVICE_OBJECT dobj;
    IoCreateDevice(DriverObject, 0, &Devicename, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &dobj);
    RtlInitUnicodeString(&SymbolLink, SYMBOL_LINK);
    IoCreateSymbolicLink(&SymbolLink, &Devicename);

    DriverObject->Flags |= DO_BUFFERED_IO;

    DriverObject->MajorFunction[IRP_MJ_CREATE] = DefaultDispatchFunction;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = DefaultDispatchFunction;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchFunction;

    DbgPrint("載入成功!!!");

    return STATUS_SUCCESS;
}
? 點選檢視答案 ?


  先給個效果圖來看看:

程式執行緒篇——執行緒切換(上)

  剩下的就沒難點和疑點了,自己看摺疊程式碼分析。


等待連結串列與排程連結串列

  程式結構體EPROCESS連結串列,裡面有兩處圈著當前程式所有的執行緒。對程式斷鏈,程式可以正常執行,原因是CPU執行與排程是基於執行緒的,程式斷鏈只是影響一些遍歷系統程式的API,並不會影響程式執行。對執行緒斷鏈也是一樣的,斷鏈後在Windbg或者OD中無法看到被斷掉的執行緒,但並不影響其執行,原因是CPU排程執行緒的時候壓根不用這個連結串列。
  執行緒有3種狀態:就緒(ready)、等待(wait)、執行(running)。
  正在執行中的執行緒儲存在KPCR中,就緒和等待的執行緒全在另外的33個連結串列中。一個等待連結串列,32個就緒連結串列。這些連結串列都使用了_KTHREAD + 0x060這個位置。這個一個位置,兩個名字,如下所示。也就是說,執行緒在某一時刻,只能屬於其中一個圈。

   +0x060 WaitListEntry    : _LIST_ENTRY
   +0x060 SwapListEntry    : _SINGLE_LIST_ENTRY

等待連結串列

  執行緒呼叫了Sleep或者WaitForSingleObject等函式時,就掛到一個連結串列之中,它是等待連結串列。學習等待連結串列之前,我們需要知道一個全域性變數:

kd> dd KiWaitListHead
80553d88  89b59540 89bb0300 00000010 00000000
80553d98  b6de6b92 a787ea13 01000013 ffdff980
80553da8  ffdff980 80500df0 00000000 00006029
80553db8  00000000 00000000 80553dc0 80553dc0
80553dc8  00000000 00000000 80553dd0 80553dd0
80553dd8  00000000 00000000 00000000 89da7da8
80553de8  00000000 00000000 00040001 00000000
80553df8  89da7e18 89da7e18 00000001 00000000

  這個全域性變數儲存的是雙向連結串列,我們測試一下:

kd> dt _ETHREAD 89b59540-60
ntdll!_ETHREAD
   +0x000 Tcb              : _KTHREAD
   +0x1c0 CreateTime       : _LARGE_INTEGER 0x0ebf1cd6`795268e0
   +0x1c0 NestedFaultCount : 0y00
   +0x1c0 ApcNeeded        : 0y0
   +0x1c8 ExitTime         : _LARGE_INTEGER 0x89b596a8`89b596a8
   +0x1c8 LpcReplyChain    : _LIST_ENTRY [ 0x89b596a8 - 0x89b596a8 ]
   +0x1c8 KeyedWaitChain   : _LIST_ENTRY [ 0x89b596a8 - 0x89b596a8 ]
   +0x1d0 ExitStatus       : 0n0
   +0x1d0 OfsChain         : (null) 
   +0x1d4 PostBlockList    : _LIST_ENTRY [ 0xe1828d30 - 0xe198e730 ]
   +0x1dc TerminationPort  : 0xe18e94d8 _TERMINATION_PORT
   +0x1dc ReaperLink       : 0xe18e94d8 _ETHREAD
   +0x1dc KeyedWaitValue   : 0xe18e94d8 Void
   +0x1e0 ActiveTimerListLock : 0
   +0x1e4 ActiveTimerListHead : _LIST_ENTRY [ 0x89b596c4 - 0x89b596c4 ]
   +0x1ec Cid              : _CLIENT_ID
   +0x1f4 LpcReplySemaphore : _KSEMAPHORE
   +0x1f4 KeyedWaitSemaphore : _KSEMAPHORE
   +0x208 LpcReplyMessage  : (null) 
   +0x208 LpcWaitingOnPort : (null) 
   +0x20c ImpersonationInfo : (null) 
   +0x210 IrpList          : _LIST_ENTRY [ 0x89b596f0 - 0x89b596f0 ]
   +0x218 TopLevelIrp      : 0
   +0x21c DeviceToVerify   : (null) 
   +0x220 ThreadsProcess   : 0x89c2cd40 _EPROCESS
   +0x224 StartAddress     : 0x7c8106e9 Void
   +0x228 Win32StartAddress : 0x7c930230 Void
   +0x228 LpcReceivedMessageId : 0x7c930230
   +0x22c ThreadListEntry  : _LIST_ENTRY [ 0x89c2ced0 - 0x89aeafd4 ]
   +0x234 RundownProtect   : _EX_RUNDOWN_REF
   +0x238 ThreadLock       : _EX_PUSH_LOCK
   +0x23c LpcReplyMessageId : 0
   +0x240 ReadClusterSize  : 7
   +0x244 GrantedAccess    : 0x1f03ff
   +0x248 CrossThreadFlags : 0
   +0x248 Terminated       : 0y0
   +0x248 DeadThread       : 0y0
   +0x248 HideFromDebugger : 0y0
   +0x248 ActiveImpersonationInfo : 0y0
   +0x248 SystemThread     : 0y0
   +0x248 HardErrorsAreDisabled : 0y0
   +0x248 BreakOnTermination : 0y0
   +0x248 SkipCreationMsg  : 0y0
   +0x248 SkipTerminationMsg : 0y0
   +0x24c SameThreadPassiveFlags : 0
   +0x24c ActiveExWorker   : 0y0
   +0x24c ExWorkerCanWaitUser : 0y0
   +0x24c MemoryMaker      : 0y0
   +0x250 SameThreadApcFlags : 0
   +0x250 LpcReceivedMsgIdValid : 0y0
   +0x250 LpcExitThreadCalled : 0y0
   +0x250 AddressSpaceOwner : 0y0
   +0x254 ForwardClusterOnly : 0 ''
   +0x255 DisablePageFaultClustering : 0 ''

  為什麼拿到的值還要減去0x60,是因為它串在這個結構體的偏移位置,需要減去偏移才是真正執行緒結構體的頭部,我們還可以通過它其中的成員找到它所屬的程式:

kd> dt _EPROCESS 0x89c2cd40 
ntdll!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x06c ProcessLock      : _EX_PUSH_LOCK
   +0x070 CreateTime       : _LARGE_INTEGER 0x01d7e39a`7e928c10
   +0x078 ExitTime         : _LARGE_INTEGER 0x0
   +0x080 RundownProtect   : _EX_RUNDOWN_REF
   +0x084 UniqueProcessId  : 0x00000374 Void
   +0x088 ActiveProcessLinks : _LIST_ENTRY [ 0x89b12e28 - 0x89c3e7a0 ]
   +0x090 QuotaUsage       : [3] 0x1a30
   +0x09c QuotaPeak        : [3] 0x1b20
   +0x0a8 CommitCharge     : 0x316
   +0x0ac PeakVirtualSize  : 0x3fc6000
   +0x0b0 VirtualSize      : 0x3f86000
   +0x0b4 SessionProcessLinks : _LIST_ENTRY [ 0x89b12e54 - 0x89c3e7cc ]
   +0x0bc DebugPort        : (null) 
   +0x0c0 ExceptionPort    : 0xe12aa720 Void
   +0x0c4 ObjectTable      : 0xe172c078 _HANDLE_TABLE
   +0x0c8 Token            : _EX_FAST_REF
   +0x0cc WorkingSetLock   : _FAST_MUTEX
   +0x0ec WorkingSetPage   : 0x14b84
   +0x0f0 AddressCreationLock : _FAST_MUTEX
   +0x110 HyperSpaceLock   : 0
   +0x114 ForkInProgress   : (null) 
   +0x118 HardwareTrigger  : 0
   +0x11c VadRoot          : 0x89caf290 Void
   +0x120 VadHint          : 0x89caf290 Void
   +0x124 CloneRoot        : (null) 
   +0x128 NumberOfPrivatePages : 0x14c
   +0x12c NumberOfLockedPages : 0
   +0x130 Win32Process     : 0xe179b368 Void
   +0x134 Job              : (null) 
   +0x138 SectionObject    : 0xe177e0b0 Void
   +0x13c SectionBaseAddress : 0x01000000 Void
   +0x140 QuotaBlock       : 0x8055b200 _EPROCESS_QUOTA_BLOCK
   +0x144 WorkingSetWatch  : (null) 
   +0x148 Win32WindowStation : 0x00000038 Void
   +0x14c InheritedFromUniqueProcessId : 0x00000290 Void
   +0x150 LdtInformation   : (null) 
   +0x154 VadFreeHint      : (null) 
   +0x158 VdmObjects       : (null) 
   +0x15c DeviceMap        : 0xe1005450 Void
   +0x160 PhysicalVadList  : _LIST_ENTRY [ 0x89c2cea0 - 0x89c2cea0 ]
   +0x168 PageDirectoryPte : _HARDWARE_PTE_X86
   +0x168 Filler           : 0
   +0x170 Session          : 0xbadce000 Void
   +0x174 ImageFileName    : [16]  "svchost.exe"
   +0x184 JobLinks         : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x18c LockedPagesList  : (null) 
   +0x190 ThreadListHead   : _LIST_ENTRY [ 0x89c2cc44 - 0x89b5970c ]
   +0x198 SecurityPort     : (null) 
   +0x19c PaeTop           : 0xbaf190e0 Void
   +0x1a0 ActiveThreads    : 0x14
   +0x1a4 GrantedAccess    : 0x1f0fff
   +0x1a8 DefaultHardErrorProcessing : 0
   +0x1ac LastThreadExitStatus : 0n0
   +0x1b0 Peb              : 0x7ffda000 _PEB
   +0x1b4 PrefetchTrace    : _EX_FAST_REF
   +0x1b8 ReadOperationCount : _LARGE_INTEGER 0x3e
   +0x1c0 WriteOperationCount : _LARGE_INTEGER 0x9
   +0x1c8 OtherOperationCount : _LARGE_INTEGER 0x195
   +0x1d0 ReadTransferCount : _LARGE_INTEGER 0x3864a
   +0x1d8 WriteTransferCount : _LARGE_INTEGER 0x1c4
   +0x1e0 OtherTransferCount : _LARGE_INTEGER 0x7506
   +0x1e8 CommitChargeLimit : 0
   +0x1ec CommitChargePeak : 0x16d4
   +0x1f0 AweInfo          : (null) 
   +0x1f4 SeAuditProcessCreationInfo : _SE_AUDIT_PROCESS_CREATION_INFO
   +0x1f8 Vm               : _MMSUPPORT
   +0x238 LastFaultCount   : 0
   +0x23c ModifiedPageCount : 1
   +0x240 NumberOfVads     : 0x80
   +0x244 JobStatus        : 0
   +0x248 Flags            : 0xd2800
   +0x248 CreateReported   : 0y0
   +0x248 NoDebugInherit   : 0y0
   +0x248 ProcessExiting   : 0y0
   +0x248 ProcessDelete    : 0y0
   +0x248 Wow64SplitPages  : 0y0
   +0x248 VmDeleted        : 0y0
   +0x248 OutswapEnabled   : 0y0
   +0x248 Outswapped       : 0y0
   +0x248 ForkFailed       : 0y0
   +0x248 HasPhysicalVad   : 0y0
   +0x248 AddressSpaceInitialized : 0y10
   +0x248 SetTimerResolution : 0y0
   +0x248 BreakOnTermination : 0y1
   +0x248 SessionCreationUnderway : 0y0
   +0x248 WriteWatch       : 0y0
   +0x248 ProcessInSession : 0y1
   +0x248 OverrideAddressSpace : 0y0
   +0x248 HasAddressSpace  : 0y1
   +0x248 LaunchPrefetched : 0y1
   +0x248 InjectInpageErrors : 0y0
   +0x248 VmTopDown        : 0y0
   +0x248 Unused3          : 0y0
   +0x248 Unused4          : 0y0
   +0x248 VdmAllowed       : 0y0
   +0x248 Unused           : 0y00000 (0)
   +0x248 Unused1          : 0y0
   +0x248 Unused2          : 0y0
   +0x24c ExitStatus       : 0n259
   +0x250 NextPageColor    : 0xbd35
   +0x252 SubSystemMinorVersion : 0 ''
   +0x253 SubSystemMajorVersion : 0x4 ''
   +0x252 SubSystemVersion : 0x400
   +0x254 PriorityClass    : 0x2 ''
   +0x255 WorkingSetAcquiredUnsafe : 0 ''
   +0x258 Cookie           : 0x7f42570f

  我們很輕鬆地找到了,這個程式屬於svchost.exe這個程式。

排程連結串列

  排程連結串列有32個圈,就是優先順序是0-31,0為最低優先順序,31為最高,預設優先順序一般是8。改變優先順序就是從一個圈裡面卸下來掛到另外一個圈上,這32個圈是正在排程中的執行緒,包括正在執行的和準備執行的。比如:只有一個CPU但有10個執行緒在執行,那麼某一時刻,正在執行的執行緒在KPCR中,其他9個在這32個圈中。
  既然有32個連結串列,就要有32個連結串列頭,我們來看一下:

kd> dd KiDispatcherReadyListHead L50
80554820  80554820 80554820 80554828 80554828
80554830  80554830 80554830 80554838 80554838
80554840  80554840 80554840 80554848 80554848
80554850  80554850 80554850 80554858 80554858
80554860  80554860 80554860 80554868 80554868
80554870  80554870 80554870 80554878 80554878
80554880  80554880 80554880 80554888 80554888
80554890  80554890 80554890 80554898 80554898
805548a0  805548a0 805548a0 805548a8 805548a8
805548b0  805548b0 805548b0 805548b8 805548b8
805548c0  805548c0 805548c0 805548c8 805548c8
805548d0  805548d0 805548d0 805548d8 805548d8
805548e0  805548e0 805548e0 805548e8 805548e8
805548f0  805548f0 805548f0 805548f8 805548f8
80554900  80554900 80554900 80554908 80554908
80554910  80554910 80554910 80554918 80554918
80554920  00000000 00000000 00000000 00000000
80554930  00000000 00000000 00000000 00000000
80554940  00000000 00000000 00000000 00000000
80554950  00000000 e1006000 00000000 00000000

  其中每一個成員都是一個雙向連結串列。如果你心細地發現。現在每個成員的地址都和當前地址一樣,前結點和後結點一樣,這就是說明當前沒有在排程連結串列的執行緒。這是因為我們中斷作業系統除錯輸入命令的時候,它會把作業系統的所有執行緒掛起,所以都在等待連結串列中。如果你真把執行緒結構體從上面幾個摘掉的話,這執行緒真的就跑不起來了。由此可知,執行緒是永遠沒法隱藏的。
  不同的Windows版本略有不同,XP只有一共33個圈,也就是說上面這個陣列只有一個,多核也只有一個。Win7也是一樣的只有一個圈,如果是64位的,那就有64個圈。而伺服器版本KiWaitListHead整個系統只有一個,但KiDispatcherReadyListHead這個陣列有幾個CPU就有幾組。

模擬執行緒切換

  在正式學習Windows執行緒切換,我們需要讀懂一份程式碼,點選 此藍奏雲連結 下載,密碼為hwso,可以直接用VC6.0直接開啟。

關鍵結構體

  我們來看一下模擬執行緒的結構體:

typedef struct 
{
   char* name;      //執行緒名
   int Flags;      //執行緒狀態
   int SleepMillsecondDot;      //休眠時間   
   void* initialStack;      //執行緒堆疊起始位置
   void* StackLimit;      //執行緒堆疊界限
   void* KernelStack;      //執行緒堆疊當前位置,也就是ESP 
   void* lpParameter;      //執行緒函式的引數
   void(*func)(void* lpParameter);      //執行緒函式
}GMThread_t;

  跟執行緒切換相關的最關鍵的引數是如下幾個專案:

   void* initialStack;      //執行緒堆疊起始位置
   void* StackLimit;      //執行緒堆疊界限
   void* KernelStack;      //執行緒堆疊當前位置,也就是ESP

模擬排程連結串列

  執行緒切換有排程連結串列,我們是如何處理的呢,我們可以看到如下程式碼:

//執行緒的列表
GMThread_t GMThreadList[MAXGMTHREAD] = {NULL, 0};

  所謂建立執行緒,就是建立一個結構體,並且掛到這個陣列中此時的執行緒狀態為建立。我們看到主函式用行程式碼實現建立執行緒:

   RegisterGMThread("Thread1", Thread1, NULL);

  跟進去看一下,具體函式內容如下:

//將一個函式註冊為單獨執行緒執行
int RegisterGMThread(char* name, void(*func)(void*lpParameter), void* lpParameter)
{
   int i;
   for (i = 1; GMThreadList[i].name; i++) 
   {
      if (0 == _stricmp(GMThreadList[i].name, name)) 
      {
         break;
      }
   }
   initGMThread(&GMThreadList[i], name, func, lpParameter);
   return (i & 0x55AA0000);
}

  可以看到,它是查詢有沒有執行緒,如果有就用initGMThread初始化這個結構體,跟進去看看:

//初始化執行緒的資訊
void initGMThread(GMThread_t* GMThreadp, char* name, void (*func)(void* lpParameter), void* lpParameter)
{
   unsigned char* StackPages;
   unsigned int* StackDWordParam;
   GMThreadp->Flags = GMTHREAD_CREATE;
   GMThreadp->name = name;
   GMThreadp->func = func;
   GMThreadp->lpParameter = lpParameter;
   StackPages = (unsigned char*)VirtualAlloc(NULL, GMTHREADSTACKSIZE, MEM_COMMIT, PAGE_READWRITE);
   ZeroMemory(StackPages, GMTHREADSTACKSIZE);
   GMThreadp->initialStack = StackPages + GMTHREADSTACKSIZE;
   StackDWordParam = (unsigned int* GMThreadp->initialStack;
   //入棧
   PushStack(&StackDWordParam, (unsigned int)GMThreadp   ;        //startup 函式所需要的引數
   PushStack(&StackDWordParam, (unsigned int)0);        /   你好奇這裡為什麼放0,簡單來說是為了平衡堆疊,其次是因為調   startup是要引數的,pop startup->eip 後 esp也就是這裡,   函式後會把 mov ebp,esp ,然後 ebp+8 就是函式預設的引數   置。
   PushStack(&StackDWordParam, (unsigned int GMThreadStartup);
   PushStack(&StackDWordParam, (unsigned int)5);    //push  ebp
   PushStack(&StackDWordParam, (unsigned int)7);    //push  edi
   PushStack(&StackDWordParam, (unsigned int)6);    //push  esi
   PushStack(&StackDWordParam, (unsigned int)3);    //push  ebx
   PushStack(&StackDWordParam, (unsigned int)2);    //push  ecx
   PushStack(&StackDWordParam, (unsigned int)1);    //push  edx
   PushStack(&StackDWordParam, (unsigned int)0);    //push  eax
   //當前執行緒的棧頂
   GMThreadp->KernelStack = StackDWordParam;
   GMThreadp->Flags = GMTHREAD_READY;
   return;
}

  我們可以看到,它首先初始化所謂的執行緒結構體,掛到陣列中,分配一個記憶體作為堆疊,然後進行一系列的堆疊操作。

初始化執行緒堆疊

  initGMThread這個函式裡面有一系列的PushStack,其實這個就是我們所謂的初始化執行緒堆疊操作,示意圖如下:

程式執行緒篇——執行緒切換(上)

模擬執行緒切換

  這個就是我們模擬執行緒切換的核心,我們看一下程式碼:

//切換執行緒
__declspec(naked) void SwitchContext(GMThread_t* SrcGMThreadp, GMThread_t* DstGMThreadp)
{
   __asm
      {
         push ebp
         mov ebp, esp
         push edi
         push esi
         push ebx
         push ecx
         push edx
         push eax

         mov esi, SrcGMThreadp
         mov edi, DstGMThreadp
         mov [esi+GMThread_t.KernelStack], esp
         //經典執行緒切換,另外一個執行緒復活
         mov esp, [edi+GMThread_t.KernelStack]        
         pop eax  //esp在上面已經切換到新的執行緒棧中,這個棧       pop eax,拿到的就是儲存的esp(初始化的esp/執行時esp)
         pop edx
         pop ecx
         pop ebx
         pop esi
         pop edi
         pop ebp
         ret   //把棧頂的值彈到eip中,在這裡彈出的就是startup的地址到eip中
      }
}

  從上面的程式碼看出,上面的程式碼把我們定義堆疊的值挨個壓入,然後把新執行緒的堆疊的值依次替換,然後把新堆疊的值彈回給暫存器繼續執行,這就是所謂了執行緒切換。那麼我們看看是誰呼叫了這個函式:

//這個函式會讓出cpu,從佇列裡重新選擇一個執行緒執行
void Scheduling(void)
{
   int i;
   int TickCount;
   GMThread_t* SrcGMThreadp;
   GMThread_t* DstGMThreadp;
   TickCount = GetTickCount();
   SrcGMThreadp = &GMThreadList[CurrentThreadIndex];
   DstGMThreadp = &GMThreadList[0]; 
   for (i = 1; GMThreadList[i].name; i++) {
      if (GMThreadList[i].Flags & GMTHREAD_SLEEP) {
         if (TickCount > GMThreadList[i].SleepMillsecondDot) {
            GMThreadList[i].Flags = GMTHREAD_READY;
         }
      }
      if (GMThreadList[i].Flags & GMTHREAD_READY) {
         DstGMThreadp = &GMThreadList[i];
         break;
      }
   }  
   CurrentThreadIndex = DstGMThreadp - GMThreadList;
   SwitchContext(SrcGMThreadp, DstGMThreadp);
   return;
}

  我們再看看是誰呼叫這個函式:

void GMSleep(int MilliSeconds)
{
   GMThread_t* GMThreadp;
   GMThreadp = &GMThreadList[CurrentThreadIndex];
   if (GMThreadp->Flags != 0) {
      GMThreadp->Flags = GMTHREAD_SLEEP;
      GMThreadp->SleepMillsecondDot = GetTickCount() + MilliSeconds;
   }

   Scheduling();
   return;
}

  而這個函式又是執行緒主動呼叫的:

void Thread1(void*) {
    while(1){
        printf("Thread1\n");
        GMSleep(500);
    }
}

  綜上可知:執行緒不是被動切換的,而是主動讓出CPU。執行緒切換並沒有使用TSS來儲存暫存器,而是使用堆疊。執行緒切換的過程就是堆疊切換的過程。
  我們可以看一下效果,由於執行緒是自己模擬的,所以在工作管理員看到只是一個執行緒,也就是作業系統幫我們建立的主執行緒:

程式執行緒篇——執行緒切換(上)

執行緒切換

  之前我們介紹了模擬Windows執行緒切換,在這個專案裡面我們介紹了一個重要的函式:SwitchContext,只要呼叫這個函式,就會導致執行緒切換。Windows也有類似的函式:KiSwapContext,只要呼叫這個函式,就會觸發執行緒切換。這個函式請自行分析,有關執行緒切換的部分將會在下一篇進行揭曉。

下一篇

  程式執行緒篇——執行緒切換(下)

相關文章