admin 管理员组

文章数量: 887019

          

          windows程序员进阶系列:《软件调试》之Windows操作系统概要

 

 

     操作系统是计算机系统中的基本软件。它负责管理系统中的软硬件资源。通常都包括文件管理、内存管理、进程管理、打印管理、网络管理等基本功能。除此之外,支持调试也是操作系统设计的一项根本任务。

 

     从被调试对象的角度来看,可以把操作系统的调试支持分为以下三个方面:

 

     一:对应用程序的调试。即如何简单高效的调试运行在系统中的应用程序。

     二:对设备驱动程序的调试。

     三:对操作系统自身调试的支持。

 

 

    本文我们会介绍Windows操作系统的架构及核心组件,加深大家对Windows系统的理解。

 

     Windows是个典型的多任务操作系统,它允许有多个进程在系统中同时运行。

     进程是资源分配和保护的基本单位。每个进程都有自己的虚拟地址空间。这保证了每个进程相互隔离互不干扰。

 

 

     除了地址空间之外每个进程还包括:

       

     一:一个全局唯一的进程ID 

     二一个可执行映像(可执行文件)。

     三:一个或多个线程。

 

     四:一个位于内核空间中的名为_EPROCESS数据结构。用以记录该进程的关键信息,包括进程创建时间、映像文件名称等。

 

     五:一个位于内核空间的句柄表。用以记录和索引该进程所创建的或打开的内核对象。句柄只是句柄表的索引,操作系统根据句柄表来得到指向内核对象的指针。

 

   六:一个用于描述内存页目录表起始位置的基地址。简称页目录基地址。当cpu切换任务时会将该地址加载到CR3寄存器中。

 

   七:一个位于用户空间中的环境块(Process Environment block)PEB

 

   八:一个访问权限令牌。用以表示该进程的用户 、安全组以及优先级别。

 

 

为了更好的理解以上列出的每个项目。我们可以结合Windbg使用dt命令来观察_EPROCESS的每个字段。

 

windbg中可以输入命令:dt _EPROCESS。该命令会显示_EPROCESS结构的各个字段。如果你通过其他方法,如!process 0 0 可得到了某个进程的_ERPCESS结构的地址,你可以为上面的命令加上_EPROCESS结构地址。如输入dt _EPROCESS  EPROCESS结构地址。该命令会在显示_EPROCESS结构的同时,显示该地址处各个字段的取值。

 

 

以下为_EPROCESS结构:

 

typedef  struct _EPROCESS {
    KPROCESS Pcb;
    NTSTATUS ExitStatus;
    KEVENT LockEvent;
    ULONG LockCount;
    LARGE_INTEGER CreateTime;
    LARGE_INTEGER ExitTime;
    PKTHREAD LockOwner;
    HANDLE UniqueProcessId;
    LIST_ENTRY ActiveProcessLinks;
    SIZE_T QuotaPeakPoolUsage[2];
    SIZE_T QuotaPoolUsage[2];
    SIZE_T PagefileUsage;
    SIZE_T CommitCharge;
    SIZE_T PeakPagefileUsage;
    SIZE_T PeakVirtualSize;
    SIZE_T VirtualSize;
    MMSUPPORT Vm;
    LIST_ENTRY SessionProcessLinks;
    PVOID DebugPort;
    PVOID ExceptionPort;
    PHANDLE_TABLE ObjectTable;
    PACCESS_TOKEN Token;        

    FAST_MUTEX WorkingSetLock;
    PFN_NUMBER WorkingSetPage;
    BOOLEAN ProcessOutswapEnabled;
    BOOLEAN ProcessOutswapped;
    UCHAR AddressSpaceInitialized;
    BOOLEAN AddressSpaceDeleted;
    FAST_MUTEX AddressCreationLock;
    KSPIN_LOCK HyperSpaceLock;
    struct _ETHREAD *ForkInProgress;
    USHORT VmOperation;
    UCHAR ForkWasSuccessful;
    UCHAR MmAgressiveWsTrimMask;
    PKEVENT VmOperationEvent;
    PVOID PaeTop;
    ULONG LastFaultCount;
    ULONG ModifiedPageCount;
    PVOID VadRoot;
    PVOID VadHint;
    PVOID CloneRoot;
    PFN_NUMBER NumberOfPrivatePages;
    PFN_NUMBER NumberOfLockedPages;
    USHORT NextPageColor;
    BOOLEAN ExitProcessCalled;
    BOOLEAN CreateProcessReported;
    HANDLE SectionHandle;
    PPEB Peb;
    PVOID SectionBaseAddress;
    PEPROCESS_QUOTA_BLOCK QuotaBlock;
    NTSTATUS LastThreadExitStatus;
    PPAGEFAULT_HISTORY WorkingSetWatch;
    HANDLE Win32WindowStation;
    HANDLE InheritedFromUniqueProcessId;
    ACCESS_MASK GrantedAccess;
    ULONG DefaultHardErrorProcessing;
    PVOID LdtInformation;
    PVOID VadFreeHint;
    PVOID VdmObjects;
    PVOID DeviceMap;
    ULONG SessionId;
    LIST_ENTRY PhysicalVadList;
    union {
        HARDWARE_PTE PageDirectoryPte;
        ULONGLONG Filler;
    };
    ULONG PaePageDirectoryPage;
    UCHAR ImageFileName[ 16 ];
    ULONG VmTrimFaultValue;
    BOOLEAN SetTimerResolution;
    UCHAR PriorityClass;
    union {
        struct {
            UCHAR SubSystemMinorVersion;
            UCHAR SubSystemMajorVersion;
        };
        USHORT SubSystemVersion;
    };
    PVOID Win32Process;
    struct _EJOB *Job;
    ULONG JobStatus;
    LIST_ENTRY JobLinks;
    PVOID LockedPagesList;
    PVOID SecurityPort ;              
    PWOW64_PROCESS Wow64Process;
    LARGE_INTEGER ReadOperationCount;
    LARGE_INTEGER WriteOperationCount;
    LARGE_INTEGER OtherOperationCount;
    LARGE_INTEGER ReadTransferCount;
    LARGE_INTEGER WriteTransferCount;
    LARGE_INTEGER OtherTransferCount;
    SIZE_T CommitChargeLimit;
    SIZE_T CommitChargePeak;
    LIST_ENTRY ThreadListHead;
    PRTL_BITMAP VadPhysicalPagesBitMap;
    ULONG_PTR VadPhysicalPages;
    KSPIN_LOCK AweLock;
} EPROCESS;


 

      从上面可以看到_EPROCESS结构几乎包括了进程的所有关键信息和重要资产。如预调试密切相关的DebugPortExceptionPort字段。我们将在后面逐步介绍其中的重要字段。

 

 

访问令牌

   _EPROCESStoken字段记录着这个进程的TOCKEN结构的地址,进程的很多余安全有关的信息都保存在这个结构中。本文不再介绍。

 

 

PEB

   PEB是进程环境块的缩写。它包含进程大多数的用户态信息。但是它由内核创建之后,映射到用户空间中的。

 

     同上面介绍的一样 ,同样可以使用dt _PEB来显示_PEB结构的各个字段。也可以指定_PEB的地址,显示各个字段的取值。除了使用此种方法外 还可以使用扩展命令:!peb xxxx来观察进程环境块。xxxxPEB结构地址。

PEB结构如下:

 

typedef struct _PEB { // Size: 0x1D8
/*000*/ UCHAR InheritedAddressSpace;
/*001*/ UCHAR ReadImageFileExecOptions;
/*002*/ UCHAR BeingDebugged;
/*003*/ UCHAR SpareBool; // Allocation size
/*004*/ HANDLE Mutant;
/*008*/ HINSTANCE ImageBaseAddress; // Instance
/*00C*/ VOID *DllList;
/*010*/ PPROCESS_PARAMETERS *ProcessParameters;
/*014*/ ULONG SubSystemData;
/*018*/ HANDLE DefaultHeap;
/*01C*/ KSPIN_LOCK FastPebLock;
/*020*/ ULONG FastPebLockRoutine;
/*024*/ ULONG FastPebUnlockRoutine;
/*028*/ ULONG EnvironmentUpdateCount;
/*02C*/ ULONG KernelCallbackTable;
/*030*/ LARGE_INTEGER SystemReserved;
/*038*/ ULONG FreeList;
/*03C*/ ULONG TlsExpansionCounter;
/*040*/ ULONG TlsBitmap;
/*044*/ LARGE_INTEGER TlsBitmapBits;
/*04C*/ ULONG ReadOnlySharedMemoryBase;
/*050*/ ULONG ReadOnlySharedMemoryHeap;
/*054*/ ULONG ReadOnlyStaticServerData;
/*058*/ ULONG AnsiCodePageData;
/*05C*/ ULONG OemCodePageData;
/*060*/ ULONG UnicodeCaseTableData;
/*064*/ ULONG NumberOfProcessors;
/*068*/ LARGE_INTEGER NtGlobalFlag; // Address of a local copy
/*070*/ LARGE_INTEGER CriticalSectionTimeout;
/*078*/ ULONG HeapSegmentReserve;
/*07C*/ ULONG HeapSegmentCommit;
/*080*/ ULONG HeapDeCommitTotalFreeThreshold;
/*084*/ ULONG HeapDeCommitFreeBlockThreshold;
/*088*/ ULONG NumberOfHeaps;
/*08C*/ ULONG MaximumNumberOfHeaps;
/*090*/ ULONG ProcessHeaps;
/*094*/ ULONG GdiSharedHandleTable;
/*098*/ ULONG ProcessStarterHelper;
/*09C*/ ULONG GdiDCAttributeList;
/*0A0*/ KSPIN_LOCK LoaderLock;
/*0A4*/ ULONG OSMajorVersion;
/*0A8*/ ULONG OSMinorVersion;
/*0AC*/ USHORT OSBuildNumber;
/*0AE*/ USHORT OSCSDVersion;
/*0B0*/ ULONG OSPlatformId;
/*0B4*/ ULONG ImageSubsystem;
/*0B8*/ ULONG ImageSubsystemMajorVersion;
/*0BC*/ ULONG ImageSubsystemMinorVersion;
/*0C0*/ ULONG ImageProcessAffinityMask;
/*0C4*/ ULONG GdiHandleBuffer[0x22];
/*14C*/ ULONG PostProcessInitRoutine;
/*150*/ ULONG TlsExpansionBitmap;
/*154*/ UCHAR TlsExpansionBitmapBits[0x80];
/*1D4*/ ULONG SessionId;
} PEB, *PPEB;


 

SessionId

 

 

   进程的SessionId是指该进程所在Windows会话的ID号。当有多个用户登陆时,Windows会为每个用户建立一个会话。每个会话有自己的Workstation和桌面。当只有一个用户登陆时用户程序和系统服务都运行在Session0 。当用户切换到另一个帐号时,系统会建立Session1

 

 

进程ID

   进程ID是用以标识进程的整数。很多用户态API使用它作为参数。在内核态主要使用_EPROCESS结构地址来标识一个进程。

 

 

父进程ID

 

即创建当前进程的进程ID

 

 

页目录地址

 

   DirBase代表该进程的页目录基地址。即任务切换时CR3寄存器的内容。它是将虚拟地址翻译为物理地址的必须参数。页目录是按4KB对齐的,因此在DirBase只保存了高20位的地址。页目录基地址在前一篇文章中曾介绍过,此处略过。

 

 

句柄表

   ObjectTable是该进程的句柄表格。Windows使用这个表格将句柄翻译为内核对象的指针。在WinDbg可以使用!handle命令查看句柄和对象信息。使用!object可以进一步查看内核对象的信息。注意它们都是在内核模式中使用。

 

 

 

句柄数量

   HandleCount即该进程使用的句柄个数。也就是ObjectTable所包含的项数。

   在任务管理器的可以非常直观的观察到进程的各种信息。

如图:

 

     

      在内核对话中,可以使用!handle 0 0 xxxx显示xxxx进程的所有句柄概况。其中xxxx为进程_EPROCESS结构地址。还可以使用!object命令进一步查看内核对象的信息。

 

 

内核模式和用户模式

 

 

   Windows定义了两种访问模式:用户模式和内核模式。应用程序代码运行在用户模式下,操作系统代码运行在内核模式下。

 

 

   内核模式对应处理器的最高权限级别。在内核模式下执行的代码可以访问所有资源并可以执行所有特权指令。用户模式具有较低的优先级,用户模式只能访问用户空间,且不能执行特权指令。

   如果用户代码不慎访问了系统空间的数据或执行了特权指令将会导致保护性异常的发生。但是用户代码可以通过调用系统服务来间接的访问系统空间中的数据或间接执行系统空间中的代码。当调用系统服务时,调用线程会从用户模式切换到内核模式,调用结束后再返回用户代码。这就是所谓的模式切换,也被称为上下文切换。

 

   在每个线程的_KTHREAD结构中定义了UserTimeKernelTme两个字段,分别记录这个线程在用户模式和内核模式运行的时间。

 

 

使用INT 2E切换到内核模式

 

 

   2e号向量号专门被用来做系统调用。在windbg中可以输入:!idt 2e来查看该向量号在IDT对应的异常处理函数。如:

 

     可以看到2e号向量对应的异常处理函数为:ntKiSystemService。该函数是内核中用以分发系统调用的。

下面我们以调用ReadFile为例来展示使用int2e指令进行系统调用的步骤:

下图为调用ReadFile API的过程。

 

 

     因为ReadFile是从Kernel32导出的,所以我们看到调用首先转到了Kernel32ReadFile函数。在ReadFile中又调用了ntdll!NtReadFile函数ntdll.dll是内核空间和用户控件的桥梁,用户空间的代码通过这个dll来调用内核空间的系统服务。它会被加载到所有用户进程的进程空间中,且位于同一位置。

   下图为ntdllNtReadFile函数反汇编代码:

 

    

    通过反汇编代码可以看到ntdll!NtReadFile非常的短,首先将请求的读文件的系统调用的系统服务号0xa1放入eax寄存器,然后便通过INT 2e指令引发系统调用异常。INT 2e会导致陷阱异常 。异常发生时,发生异常代码处得CSEIP寄存器会被压入堆栈,用于在处理完异常后的返回。然后系统会在IDT表中查询2e号向量对应的异常处理函数,得到KiSystemService函数地址。

 

 

     在调用内核态的KiSystemService函数值前,cpu会做一些模式切换工作。包括权限检查和准备内核态使用的栈空间。Nt!KisystemService,得到传入的系统调用号,并将传入的参数从用户态复制到内核态,调用系统读取文件的系统调用。执行完毕后,nt!KiSystemService会将操作结果复制到用户态。弹出CSEIP将执行权交给NtReadFile用以执行INT 2e后面的指令。

 

快速系统调用

 

   前面介绍了Windows使用INT 2e来实现系统调用。但是它是使用中断机制实现的,伴随着中断产生的还有权限检查和查询IDT表等操作,这会导致额外的开销。因此在最新版本的Windows中,微软采用了被称为快速系统调用的机制。这主要是得益于Intel从奔腾2开始在处理器新加的三个特殊的MSR寄存器以及sysentersysexit指令。

三个MSR寄存器包括:SYSENTER_EIP_MSR寄存器,用来指定新的程序指针。SYSENTER_ESP_MSR寄存器,用来指向新的代码段。SYSENTER_CS_MSR寄存器,用来指向新的栈指针。

 

      当执行SYSENTER时,cpu会直接从MSR寄存器获得nt!KiSystemService函数地址,并调用。由于SYSENTER无法使用堆栈传递信息,也就不能向栈中压入CSEIP以及参数。因此必须通过其他方法,如寄存器来获得传入的系统调用号、返回地址等。

 

   新版本的Windows在启动过程中会检测cpu是否支持快速系统调用指令。如果cpu不支持sysentersysexit指令,那么仍然使用INT 2e方式。如果支持快速系统调用,则Windows会使用这些最新的方式,并做以下准备:

 

    一:在GDT全局描述符表中建立四个段描述符,分别用来描述供sysenter指令进入内核模式时使用的代码段和栈段。以及sysexit指令从内核模式返回用户模式时使用到代码段和栈段。它们都是存储的段选择子且这四个段描述符位置一定。

 

 

   二:设置专门用于系统调用的MSR寄存器。SYSENTER_EIP_MSR寄存器存储程序指针。也就是SYSENTER指令要跳转的目标例程地址。Windows会将其设置为KiFastCallEntry地址,此函数是Windows中专门用以处理快速系统调用的。SYSENTER_CS_MSR寄存器用以存储代码段选择子,也就是KiFastCallEntry所在的代码段。SYSENTER_ESP_MSR寄存器用以存储栈指针。栈段由SYSTEM_CS_MSR寄存器值加8得到。

 

      三:将一小段名为SystemCallStub函数代码复制到SharedUserData内存区。该区域会被映射到每个win32进程的地址空间中,当应用程序进行系统调用时,ntdll中的stub函数便会调用这段SystemCallStub函数。在SystemCallStub中会执行sysenter指令。

 

  Windows把快速系统调用的目标指向的是内核代码的KiFastCallEntry函数。

 

   在快速调用情况下,使用WinDbg查看NtReadFile的汇编代码为:

 

 

   可以看到在NtReadFile中,调用了SystemCallStub,从下图可以看到,它只有三条指令:

     将esp的值赋给edx后,就调用sysenter指令。systenter指令跳转到KiFastCallEntry函数。

 

如上图可以看到,在KiFastCallEntry又调用了KiSystemService函数。因此我们可以知道快速系统调用和使用INT 2e进行系统调用的大部分步骤都是一样的。

   前面介绍使用INT 2e进行系统调用时我们知道,INT2e指令会自动将终端发生时的CSEIP寄存器压入栈。在中断程序调用IRETD时会使用栈中保存的CSEIP返回到用户模式的相应位置。

但是在快速系统调用时,sysenter指令不会向栈中压入要返回的位置,

所以sysexit指令必须通过其他方式知道要返回的地址。在KiFastCallEntry函数的第三行有一句:move ecx,0x7ffe0304。其实0x7ffe0304就是SharedUserData区域中的SystemCallStub函数ret指令要返回的地址。在第13行,将ecx压入栈在需要返回的时候再从栈中恢复,这样sysexit就可以知道用户模式的目标地址。

   从下图中可以看到,在执行sysexit指令之前ecx被从栈中恢复出来。这句指令在KiSystemCallExit函数中:

下图为快速系统调用的完整过程:

 

逆向过程调用

   前面我们介绍了进入内核模式的两种方法:INT 2e指令和快速系统调用。

 

   这两种方法都是使用用户态的代码调用位于内核模式的系统服务。其实内核态的代码也可以调用用户态的代码。这种调用被称为逆向调用。

 

      调用过程:

 

   内核代码使用KiCallUserMode发起调用,接下来的执行过程与KiServiceExit类似。但是这次是执行的KiUserCallbackDiapatcher。然后此函数会调用内核希望调用的函数。

 

 

   当用户态的工作完成时,会执行INT 2B指令。该指令会触发一个0x2B异常。这个异常的处理函数是内核态的KiCallbackReturn函数。然后便进入内核态。

 

 

内核空间

 

 

内核空间主要包含以下部件:

 

     硬件抽象层(Hardware abstraction layer)HAL:主要作用为隔离硬件的差异性,是内核和顶层模块可以通过统一的方式来访访问硬件。

 

   操作系统内核:负责线程调度、中断处理、异常分发、多处理器同步等,是操作系统最核心的部分。

 

   执行体:包括操作系统的基本服务,如内存管理 、进程管理。如果把操作系统比喻成最高权力机构,那么执行体便是它的一个个职能部门,负责各种事务。

 

   内核态驱动程序:包括文件系统和图形显示驱动程序。是对内核功能的补充。

 

   Windows子系统驱动程序:包括USERGDI两部分。

 

   内核支持模块:包括用于内核调试的KDCOM.dll

 

 

System)系统进程和IDLE进程

 

 

   普通的进程都是调用CreateProcess或具有类似功能的API来创建的。但System进程和IDLE进程却不是,它们没有对应的映像文件。普通的进程都有内核态和用户态两部分,而这两个进程却只有内核态部分。

   IDLE进程是IDLE线程的载体,进程ID0,父进程ID也为0。其内部线程的个数与处理器个数相同。当cpu没有其他任务要执行时,它就会执行IDLE线程。

   System是系统内核和大多数系统线程的宿主和载体。它的进程ID8

 

 

用户空间

 

   用户空间是应用程序代码和各种用户态模块运行的场所。用户空间中运行着以下进程:

   会话管理器(SMSS.exe):它是系统中第一个根据映像文件创建的线程。

   Windows子系统服务器进程:负责维护Windows子系统的日常事务。为Windows的各个进程提供服务。

   登陆进程(WinLogon.exe):负责用户登录和安全有关的事务。

   本地安全和认证进程(LSASS.exe):负责用户身份验证。

   服务管理进程(Services.exe):负责启动和管理系统服务程序。

   Shell程序:默认为Explorer.exe负责显示开始菜单、任务栏和桌面等。

 

ntdll.dll

 

ntdll.dll是沟通用户空间和内核空间的桥梁。用户空间代码通过这个DLL来调用内核空间的服务。 同时它也是内核在用户空间的代理,它会被影射到所有进程的地址空间中。当内核需要用户空间代码配合时会调用这个ntdll。后面会介绍ntdll的调试支持。

 

 

       Windows 系统非常的庞大,本文仅仅涉及一些最基本的概念。后面的文章中会介绍其他的系统机制和数据结构,尤其是对软件调试关系密切的部分。软件调试技术是一种很难掌握的技术,但是一旦掌握将受益终生,且变化极小。所以在学习的路上遇到些困难也再所难免,大家不要灰心,一定要坚持学习下去!!共勉!

 

                       以上参考自《软件调试》,如有纰漏,请指正!谢谢!

                                       2013.3.13于浙江杭州。

 

       上班八天了,每天都很忙碌。接触到一个新的领域,太多东西需要学。突然想起来一位好友以前跟我说过的一句话:真正上班了,做哪方面就不是你能左右的了,公司让你做什么你就得做什么。现在想想还真是这样。不过只要是跟C++有关的,我就愿意去做。跟过去的三年一样,这八天中的每一天很忙碌,也很充实。只是唯一不同的是我再也不能有大块儿的时间去看我想看的书了。每天看看书、写写博客、写写代码的惬意日子不会再有,迎接我的将是新的挑战。白天谋生存,晚上求发展。只能在不忙的晚上或是周末,抽出时间看看书,为自己充充电。

 

本文标签: 进阶 概要 程序员 之五 操作系统