精华内容
下载资源
问答
  • 把它们放到一起是因为这三个数据结构及其外延和windows中进程的表示关系密切,我们在做进程隐藏和进程枚举的时候一定会涉及到这3类数据结构,所以这里有必要及时做一下总结,做个学习笔记,理清思路。 1. 相关...

    在学习笔记(1)中,我们学习了IRP的数据结构的相关知识,接下来我们继续来学习内核中很重要的另一批数据结构: EPROCESS/KPROCESS/PEB。把它们放到一起是因为这三个数据结构及其外延和windows中进程的表示关系密切,我们在做进程隐藏和进程枚举的时候一定会涉及到这3类数据结构,所以这里有必要及时做一下总结,做个学习笔记,理清思路。

     

    1. 相关阅读材料

    《windows 内核原理与实现》 --- 潘爱民

    《深入解析windows操作系统(第4版,中文版)》 --- 潘爱民

    《Windows核心编程(第四版)》中文版

     

     

     

    首先我们来看一张windows的内核分布图,我们之后要讲的执行体和内核层都在上面。

    每个windows进程都是由一个执行体进程块(EPROCESS)块来表示的。"EPROCESS块中除了包含许多与进程有关的属性之外,还包含和指向许多其他的相关数据结构"(这也是为什么能利用EPROCESS进行进程枚举的原因)。例如,每个进程都有一个或多个线程,这些线程由执行体线程块(ETHREAD)来表示。EPROCESS块和相关的数据结构位于系统空间中,不过,进程环境块(PEB)是个例外。它位于进程地址空间中(因为它包含一些需要由用户模式代码来修改的信息)。

    除了EPROCESS块以外,windows子系统进程(Csrss.exe)为每个windows进程维护了一个类似的结构。而且,windows子系统的内核部分(Win32k.sys)有一个针对每个进程的数据结构(KPROCESS),当一个线程第一次调用windows的USER或GDI函数(它们在内核模式中实现的)时,此数据结构就会被创建。在学习的一开始,我们对这些数据结构在windows系统的层级位置和大致的作用有一个了解很关键。接下来,我们来详细了解这些数据结构。

     

     

     

     

    一 . EPROCESS

    EPEOCESS(执行体进程块,E是Execute的意思,注意和KPROCESS区分开来)位于内核层之上(KPROCESS就内核层,我们之后会学习到KPROCESS),它侧重于提供各种管理策略,同时为上层应用程序提供基本的功能接口。所以,在执行体层的进程和线程数据结构中,有些成员直接对应于上层应用程序中所看到的功能实体。

    我们使用winDbg来查看windows XP下的EPROCESS数据结构(winDbg双机调试的方法在《寒江独钓》第一章以及网上有很多成熟的使用说明)。

    我们接下来的例子都以windows XP下的notepad.exe作为实验材料。首先,启动winDbg后,找到notepad.exe这个进程(你在虚拟机里要先启动notepad.exe哦)

    !process 0 0    //查看当前进程 
    ....
    PROCESS 823e5490  SessionId: 0  Cid: 0af0    Peb: 7ffd5000  ParentCid: 02b8
        DirBase: 0f200340  ObjectTable: e283dc30  HandleCount: 106.
        Image: alg.exe
    
    PROCESS 824a7020  SessionId: 0  Cid: 00a0    Peb: 7ffda000  ParentCid: 0668
        DirBase: 0f2001a0  ObjectTable: e292f898  HandleCount:  44.
        Image: notepad.exe
    ...

    可以看到 PROCESS 824a7020 ,824a7020 就是notepad.exe的_EPROCESS结构地址(这个地址在notepad的生命周期中就不会变了)

    dt _eprocess 824a7020    //查看notepad.exe的_EPROCESS 
    kd> dt _eprocess 824a7020 
    ntdll!_EPROCESS
       +0x000 Pcb              : _KPROCESS
       +0x06c ProcessLock      : _EX_PUSH_LOCK
       +0x070 CreateTime       : _LARGE_INTEGER 0x1ceee34`5d8ce8a2
       +0x078 ExitTime         : _LARGE_INTEGER 0x0
       +0x080 RundownProtect   : _EX_RUNDOWN_REF
       +0x084 UniqueProcessId  : 0x000000a0 
       +0x088 ActiveProcessLinks : _LIST_ENTRY [ 0x8055b158 - 0x823e5518 ]
       +0x090 QuotaUsage       : [3] 0xa50
       +0x09c QuotaPeak        : [3] 0xc48
       +0x0a8 CommitCharge     : 0x184
       +0x0ac PeakVirtualSize  : 0x244d000
       +0x0b0 VirtualSize      : 0x1f87000
       +0x0b4 SessionProcessLinks : _LIST_ENTRY [ 0xf8b6a014 - 0x823e5544 ]
       +0x0bc DebugPort        : (null) 
       +0x0c0 ExceptionPort    : 0xe1982848 
       +0x0c4 ObjectTable      : 0xe292f898 _HANDLE_TABLE
       +0x0c8 Token            : _EX_FAST_REF
       +0x0cc WorkingSetLock   : _FAST_MUTEX
       +0x0ec WorkingSetPage   : 0xc6af
       +0x0f0 AddressCreationLock : _FAST_MUTEX
       +0x110 HyperSpaceLock   : 0
       +0x114 ForkInProgress   : (null) 
       +0x118 HardwareTrigger  : 0
       +0x11c VadRoot          : 0x82401480 
       +0x120 VadHint          : 0x8256a530 
       +0x124 CloneRoot        : (null) 
       +0x128 NumberOfPrivatePages : 0xc0
       +0x12c NumberOfLockedPages : 0
       +0x130 Win32Process     : 0xe109b210 
       +0x134 Job              : (null) 
       +0x138 SectionObject    : 0xe19d2c18 
       +0x13c SectionBaseAddress : 0x01000000 
       +0x140 QuotaBlock       : 0x82cf5db8 _EPROCESS_QUOTA_BLOCK
       +0x144 WorkingSetWatch  : (null) 
       +0x148 Win32WindowStation : 0x0000003c 
       +0x14c InheritedFromUniqueProcessId : 0x00000668 
       +0x150 LdtInformation   : (null) 
       +0x154 VadFreeHint      : (null) 
       +0x158 VdmObjects       : (null) 
       +0x15c DeviceMap        : 0xe21f5258 
       +0x160 PhysicalVadList  : _LIST_ENTRY [ 0x824a7180 - 0x824a7180 ]
       +0x168 PageDirectoryPte : _HARDWARE_PTE_X86
       +0x168 Filler           : 0
       +0x170 Session          : 0xf8b6a000 
       +0x174 ImageFileName    : [16]  "notepad.exe"
       +0x184 JobLinks         : _LIST_ENTRY [ 0x0 - 0x0 ]
       +0x18c LockedPagesList  : (null) 
       +0x190 ThreadListHead   : _LIST_ENTRY [ 0x823c224c - 0x823c224c ]
       +0x198 SecurityPort     : (null) 
       +0x19c PaeTop           : 0xf8cba1a0 
       +0x1a0 ActiveThreads    : 1
       +0x1a4 GrantedAccess    : 0x1f0fff
       +0x1a8 DefaultHardErrorProcessing : 1
       +0x1ac LastThreadExitStatus : 0
       +0x1b0 Peb              : 0x7ffda000 _PEB
       +0x1b4 PrefetchTrace    : _EX_FAST_REF
       +0x1b8 ReadOperationCount : _LARGE_INTEGER 0x1
       +0x1c0 WriteOperationCount : _LARGE_INTEGER 0x0
       +0x1c8 OtherOperationCount : _LARGE_INTEGER 0x248
       +0x1d0 ReadTransferCount : _LARGE_INTEGER 0x57ce
       +0x1d8 WriteTransferCount : _LARGE_INTEGER 0x0
       +0x1e0 OtherTransferCount : _LARGE_INTEGER 0x18846
       +0x1e8 CommitChargeLimit : 0
       +0x1ec CommitChargePeak : 0x184
       +0x1f0 AweInfo          : (null) 
       +0x1f4 SeAuditProcessCreationInfo : _SE_AUDIT_PROCESS_CREATION_INFO
       +0x1f8 Vm               : _MMSUPPORT
       +0x238 LastFaultCount   : 0
       +0x23c ModifiedPageCount : 7
       +0x240 NumberOfVads     : 0x42
       +0x244 JobStatus        : 0
       +0x248 Flags            : 0xd0800
       +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 : 0y0
       +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       : 259
       +0x250 NextPageColor    : 0x8d96
       +0x252 SubSystemMinorVersion : 0 ''
       +0x253 SubSystemMajorVersion : 0x4 ''
       +0x252 SubSystemVersion : 0x400
       +0x254 PriorityClass    : 0x2 ''
       +0x255 WorkingSetAcquiredUnsafe : 0 ''
       +0x258 Cookie           : 0x5c77be6c

    并结合windows的"开源"学习版本的源代码: WRK。   在\base\ntos\inc\ps.h中,可以找到相关的数据结构的定义说明。

    typedef struct _EPROCESS 
    {
        KPROCESS Pcb; 
    
        EX_PUSH_LOCK ProcessLock;
    
        LARGE_INTEGER CreateTime;
        LARGE_INTEGER ExitTime; 
    
        EX_RUNDOWN_REF RundownProtect;
    
        HANDLE UniqueProcessId; 
    
        LIST_ENTRY ActiveProcessLinks; 
    
        SIZE_T QuotaUsage[PsQuotaTypes];
        SIZE_T QuotaPeak[PsQuotaTypes];
        SIZE_T CommitCharge; 
    
        SIZE_T PeakVirtualSize;
        SIZE_T VirtualSize;
    
        LIST_ENTRY SessionProcessLinks;
    
        PVOID DebugPort;
        PVOID ExceptionPort;
        PHANDLE_TABLE ObjectTable; 
    
        EX_FAST_REF Token;
    
        PFN_NUMBER WorkingSetPage;
        KGUARDED_MUTEX AddressCreationLock;
        KSPIN_LOCK HyperSpaceLock;
    
        struct _ETHREAD *ForkInProgress;
        ULONG_PTR HardwareTrigger;
    
        PMM_AVL_TABLE PhysicalVadRoot;
        PVOID CloneRoot;
        PFN_NUMBER NumberOfPrivatePages;
        PFN_NUMBER NumberOfLockedPages;
        PVOID Win32Process;
        struct _EJOB *Job;
        PVOID SectionObject;
    
        PVOID SectionBaseAddress;
    
        PEPROCESS_QUOTA_BLOCK QuotaBlock;
    
        PPAGEFAULT_HISTORY WorkingSetWatch;
        HANDLE Win32WindowStation;
        HANDLE InheritedFromUniqueProcessId;
    
        PVOID LdtInformation;
        PVOID VadFreeHint;
        PVOID VdmObjects;
        PVOID DeviceMap;
    
        PVOID Spare0[3];
        union 
        {
            HARDWARE_PTE PageDirectoryPte;
            ULONGLONG Filler;
        };
        PVOID Session;
        UCHAR ImageFileName[ 16 ];
    
        LIST_ENTRY JobLinks;
        PVOID LockedPagesList;
    
        LIST_ENTRY ThreadListHead; 
    
        PVOID SecurityPort; 
        PVOID PaeTop; 
    
        ULONG ActiveThreads;
    
        ACCESS_MASK GrantedAccess;
    
        ULONG DefaultHardErrorProcessing;
    
        NTSTATUS LastThreadExitStatus; 
    
        PPEB Peb; 
    
        EX_FAST_REF PrefetchTrace;
    
        LARGE_INTEGER ReadOperationCount;
        LARGE_INTEGER WriteOperationCount;
        LARGE_INTEGER OtherOperationCount;
        LARGE_INTEGER ReadTransferCount;
        LARGE_INTEGER WriteTransferCount;
        LARGE_INTEGER OtherTransferCount;
    
        SIZE_T CommitChargeLimit;
        SIZE_T CommitChargePeak;
    
        PVOID AweInfo; 
    
        SE_AUDIT_PROCESS_CREATION_INFO SeAuditProcessCreationInfo;
    
        MMSUPPORT Vm;
     
        LIST_ENTRY MmProcessLinks; 
    
        ULONG ModifiedPageCount; 
    
        ULONG JobStatus; 
     
        union 
        { 
            ULONG Flags; 
    
            struct {
                ULONG CreateReported            : 1;
                ULONG NoDebugInherit            : 1;
                ULONG ProcessExiting            : 1;
                ULONG ProcessDelete             : 1;
                ULONG Wow64SplitPages           : 1;
                ULONG VmDeleted                 : 1;
                ULONG OutswapEnabled            : 1;
                ULONG Outswapped                : 1;
                ULONG ForkFailed                : 1;
                ULONG Wow64VaSpace4Gb           : 1;
                ULONG AddressSpaceInitialized   : 2;
                ULONG SetTimerResolution        : 1;
                ULONG BreakOnTermination        : 1;
                ULONG SessionCreationUnderway   : 1;
                ULONG WriteWatch                : 1;
                ULONG ProcessInSession          : 1;
                ULONG OverrideAddressSpace      : 1;
                ULONG HasAddressSpace           : 1;
                ULONG LaunchPrefetched          : 1;
                ULONG InjectInpageErrors        : 1;
                ULONG VmTopDown                 : 1;
                ULONG ImageNotifyDone           : 1;
                ULONG PdeUpdateNeeded           : 1;    // NT32 only
                ULONG VdmAllowed                : 1;
                ULONG SmapAllowed               : 1;
                ULONG CreateFailed              : 1;
                ULONG DefaultIoPriority         : 3;
                ULONG Spare1                    : 1;
                ULONG Spare2                    : 1;
            };
        };
    
        NTSTATUS ExitStatus;
    
        USHORT NextPageColor;
        union 
        {
            struct 
        {
                UCHAR SubSystemMinorVersion;
                UCHAR SubSystemMajorVersion;
            };
            USHORT SubSystemVersion;
        };
        UCHAR PriorityClass;
    
        MM_AVL_TABLE VadRoot;
    
        ULONG Cookie;
    
    } EPROCESS, *PEPROCESS; 

    不好意思一下子复制了一大堆的代码出来,也说明EPOCESS在windows内核中的地位,所以很复杂,我们现在来一条一条的学习这个EPROCESS的数据结构成员,和其他内核中数据结构的联系以及相关的应用场景和外延。

     

     

    1. KPROCESS Pcb

    Pcb域即KPROCESS结构体,它们是同一种东西,只是两种叫法而已,我们现在只要知道几点,KRPOCESS的详细细节我们放到后面讲:

    1) KPROCESS位于比EPROCESS更底层的内核层中
    
    2) KPROCESS被内核用来进行线程调度使用

    这里还要注意的是,因为Pcb域是EPROCESS结构的第一个成员,所以在系统内部,一个进程的KPROCESS对象的地址和EPROCESS对象的地址是相同的。这种情况和"TIB就是TEB结构的第一个成员,而EXCEPTION_REGISTRATION_RECORD又是TIB的第一个成员,又因为FS:[0x18] 总是指向当前线程的 TEB 。 所以导致用 FS:[0x18] 就直接可以寻址到SEH的链表了"。windows中的这种结构体的嵌套思想,应该予以领会。

     

    2. EX_PUSH_LOCK ProcessLock

    ProcessLock域是一个推锁(push lock)对象,用于保护EPROCESS中的数据成员(回想IRP中也同样有一种类似的锁机制)。用来对可能产生的并行事件强制串行化。

     

    typedef struct _EX_PUSH_LOCK
    {
         union
         {
              ULONG Locked: 1;
              ULONG Waiting: 1;
              ULONG Waking: 1;
              ULONG MultipleShared: 1;
              ULONG Shared: 28;
              ULONG Value;
              PVOID Ptr;
         };
    } EX_PUSH_LOCK, *PEX_PUSH_LOCK;

     

     

     

     

    3. LARGE_INTEGER CreateTime  /   LARGE_INTEGER ExitTime

    这两个域分别代表了进程的创建时间和退出时间,windows在内核中会记录大量和进程周边的相关信息,例如创建事件,运行时间,负载,使用内存数等,一方面对于上层应用来说,我们可以很方便地调用一些API去获取到实时的系统运行状态,另一方面操作系统本身也可以依据这些信息对进程进行合理的调度。

    typedef union _LARGE_INTEGER 
    {
      struct 
      {
        DWORD LowPart;
        LONG  HighPart;
      };
      struct 
      {
        DWORD LowPart;
        LONG  HighPart;
      } u;
      LONGLONG QuadPart;
    } LARGE_INTEGER, *PLARGE_INTEGER;
    +0x070 CreateTime       : _LARGE_INTEGER 0x1ceee34`5d8ce8a2

     

     

     

    4. EX_RUNDOWN_REF RundownProtect

    RundownProtect域是进程的停止保护锁,当一个进程到最后被销毁时,它要等到所有其他进程和线程已经释放了此锁,才可以继续进行,否则就会产生孤儿线程。加锁机制也是windows中进程间或线程间同步的一个很经典的机制,进程只要设置阻塞等待在一个锁对象上,等待拥有那个锁的其他进程或线程释放锁对象,操作系统会发出signal信号,重新激活之前阻塞等待在那个锁上的进程,这样就完成了进程间,线程间的同步。

    struct EX_RUNDOWN_REF typedef struct _EX_RUNDOWN_REF
    {
         union
         {
              ULONG Count;
              PVOID Ptr;
         };
    } EX_RUNDOWN_REF, *PEX_RUNDOWN_REF;

     

     

    5. HANDLE UniqueProcessId

    UniqueProcessId域是进程的唯一编号,在进程创建时就设定好了,我们在"任务管理器"中看到的PID就是从这个域中获取的值。

    +0x084 UniqueProcessId  : 0x000000a0

     

     

    6. LIST_ENTRY ActiveProcessLinks

    ActiveProcessLinks域是一个双链表节点(注意是双链表中的一个节点),在windows系统中,所有的活动进程都连接在一起,构成了一个链表。

    表头是全局变量PsActiveProcessHead。内部变量PsActiveProcessHead是一个LIST_ENTRY结构,它是一个双链表的表头,在windows的进程双链表中指定了系统进
    程列表的第一个成员(回想数据结构中双链表需要一个和链表项相同的表头结构来当作入口点)。
    这里注意一下:
    PID=4的System的ActiveProcessLinks其实也就是PsActiveProcessHead。即系
    统进程System的LIST_ENTRY结构充当这个进程双链表的表头。

    当一个进程被创建时,其ActiveProcessLinks域将被作为"节点"加入到内核中的进程双链表中,当进程被删除时,则从链表中移除。如果我们需要枚举所有的进程,直接操纵此链表即可。思路其实很清晰的,利用PsGetCurrentProcess()获得当前进程的EPROCESS结构,在根据指定偏移找到Flink和Blink,后面的事情就是数据结构中的遍历双链表了。

    typedef struct _LIST_ENTRY 
    {
      struct _LIST_ENTRY  *Flink;
      struct _LIST_ENTRY  *Blink;
    } LIST_ENTRY, *PLIST_ENTRY;
    +0x088 ActiveProcessLinks : _LIST_ENTRY [ 0x8055b158 - 0x823e5518 ]

    我们暂且不表,将进程枚举的知识放到后面的学习笔记中,我们集中精力学习EROCESS的数据结构。

     

     

    7. 进程内存配额使用

    ULONG QuotaUsage[3]
    ULONG QuotaPeak[3]
    
    QuotaUsage和QuotaPeak域是指一个进程的内存使用量和尖峰使用量(作用我们之前说过,即为了实时地报告出当前进程运行的性能数据)。这两个域是数组,其中的元素:
    [0]: 非换页内存池
    [1]: 换页内存池
    [2]: 交换文件中的内存使用情况。
    QuotaUsage[3]和QuotaPeak[3]这两个数组是在PspChargeQuota函数中计算的。在WRK中 \base\ntos\ps\psquota.c 有具体的函数实现。
    NTSTATUS FORCEINLINE PspChargeQuota (
        IN PEPROCESS_QUOTA_BLOCK QuotaBlock,
        IN PEPROCESS Process,
        IN PS_QUOTA_TYPE QuotaType,
        IN SIZE_T Amount)
    {
    ..
    }
    +0x090 QuotaUsage       : [3] 0xa50
    +0x09c QuotaPeak        : [3] 0xc48

     

     

     

    8. 进程虚拟内存使用

    ULONG CommitCharge
    ULONG PeakVirtualSize
    ULONG VirtualSize
    CommitCharge域中存储了一个进程的虚拟内存已提交的"页面数量"。PeakVirtualSize域是指虚拟内存大小的尖峰值。VirtualSize域是指一个进程的虚拟内存大小。
    +0x0a8 CommitCharge     : 0x184
    +0x0ac PeakVirtualSize  : 0x244d000
    +0x0b0 VirtualSize      : 0x1f87000

    (我们在任务管理器中看到的这些性能参数就是从这些域中计算总和得到的)

     

     

    9. LIST_ENTRY SessionProcessLinks

    SessionProcessLinks域是一个双链表节点,当进程加入到一个系统会话中时,这个进程的SessionProcessLinks域将作为一个节点(LIST_ENTRY在内核中很常见)加入到该会话的进程链表中。

    +0x0b4 SessionProcessLinks : _LIST_ENTRY [ 0xf8b6a014 - 0x823e5544 ]

     

    10. 端口

    PVOID DebugPort;
    PVOID ExceptionPort; 
    DebugPort和ExceptionPort域是两个句柄(指针),分别指向当前进程对应的调试端口和异常端口。当该进程的线程发生用户模式的异常时(软件中断)时,内核的异常处理例程在处理异常过程中,将向
    该进程的"异常端口"或"调试端口"发送消息,从而使这些端口的接收方(调试器或windows子系统)能够处理该异常。这涉及到windows的异常分发过程。请参考《windows内核原理与实现》
    5.2.7节: 异常分发。这里总结一下windows异常发生时的处理顺序。
    发生异常时系统的处理顺序:  
    
    1.系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送,并且目标程序正在被调试,则系统 
    挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息.注意,这也是用来探测调试器的存在的技术之一 
    
    2.如果你的程序没有被调试或者调试器未能处理异常,系统就会继续查找你是否安装了线程相关的异常处理例程,如果 
    你安装了线程相关的异常处理例程,系统就把异常发送给你的程序seh处理例程,交由其处理. 
    
    3.每个线程相关的异常处理例程可以处理或者不处理这个异常,如果他不处理并且安装了多个线程相关的异常处理例程, 
    可交由链起来的其他例程处理. 
    
    4.如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知debugger. 
    
    5.如果程序未处于被调试状态或者debugger没有能够处理,并且你调用SetUnhandledExceptionFilter安装了最后异 
    常处理例程的话,系统转向对它的调用. 
    
    6.如果你没有安装最后异常处理例程或者他没有处理这个异常,系统会调用默认的系统处理程序,通常显示一个对话框, 
    你可以选择关闭或者最后将其附加到调试器上的调试按钮.如果没有调试器能被附加于其上或者调试器也处理不了,系统 
    就调用ExitProcess终结程序. 
    
    7.不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程异常处理例程最后清理的机会.
    
    
    +0x0bc DebugPort        : (null) 
    +0x0c0 ExceptionPort    : 0xe1982848 

     



    11. PHANDLE_TABLE ObjectTable
    ObjectTable域是当前进程的句柄表。句柄是一个抽象概念,代表了进程已打开的一个对象。句柄表包含了所有已经被该进程打开的那些"对象"的引用(这里对象的概念很大了,所有的内核对象都算对象)。
    windows进程的句柄表是一个层次结构。
    windows执行体实现了一套对象机制来管理各种资源或实体。每种对象都有一个"类型对象",类型对象定义了该类对象的一些特性和方法。对象管理器中的对象是执行体对象(在内核空间中),
    它们位于系统空间中,在进程空间不能通过地址来引用它们。windows使用句柄(handle)来管理进程中的对象引用(类似我们进行文件操作时返回的文件对象句柄)。

    当一个进程利用名称来创建或打开一个对象时,将获得一个句柄(这个句柄本质上一个指针,保存在当前进程的EPROCESS的句柄表中)。
    该句柄指向所创建或打开的对象(同时,被创建或打开的内核对象的引用计数+1)。以后,该进程无须再次使用名称来引用该对象,使用此句柄访问即可。windows这样做可以显著提高引用对象的效率。

    在windows中,句柄是进程范围内的对象引用,换句话说,句柄仅在一个进程范围内有效。一个进程中的句柄传递给另一个进程后(例如父进程创建了子进程时会自动复制父进程拥有的句柄表),
    但是句柄值已经不一样了,不同进程空间中可能引用的是同一个对象,但是它们的句柄值不同(联想进程的空间独立性)。
    实际上,windows支持的句柄是一个"索引",指向该句柄所在进程的句柄表(handle table)中的一个表项。

    base\ntos\inc\ex.h
    typedef struct _HANDLE_TABLE
    {
        //指向句柄表的结构
        ULONG TableCode; 
        //句柄表的内存资源记录在此进程中
        PEPROCESS QuotaProcess; 
        //创建进程的ID,用于回调函数
        PVOID UniqueProcessId;
        //句柄表锁,仅在句柄表扩展时使用
        EX_PUSH_LOCK HandleLock;
        //所有的句柄表形成一个链表(这个成员域用来指向下一个句柄表节点的),链表头为全局变量HandleTableListHead
        LIST_ENTRY HandleTableList;
        //若在访问句柄时发生竞争,则在此推锁上阻塞等待
        EX_PUSH_LOCK HandleContentionEvent;
        //调试信息,仅当调试句柄时才有意义
        PHANDLE_TRACE_DEBUG_INFO DebugInfo;
        //审计信息所占用的页面数量
        LONG ExtraInfoPages; 
        //空闲链表表头的句柄索引
        LONG FirstFreeHandle;
        //最近被释放的句柄索引,用于FIFO类型空闲链表
        PHANDLE_TABLE_ENTRY LastFreeHandleEntry;
        //下一次句柄表扩展的起始句柄索引
        ULONG NextHandleNeedingPool;
        //正在使用的句柄表项的数量
        LONG HandleCount;
        union
        {
            //标志域
            ULONG Flags;
            //是否使用FIFO风格的重用,即先释放还是先重用
            BOOLEAN StrictFIFO : 1;
        }
    } HANDLE_TABLE, *PHANDLE_TABLE;

    HANDLE_TABLE结构中的第一个域成员TableCode域是一个指针,指向句柄表的最高层表项页面(前面说过句柄表是一个多层的结构),它的低2bit的值代表了当前句柄表的层数。

    1) 如果TableCode的最低2位为0(..00)。说明句柄表只有一层(从0开始计数),这种情况下该进程最多只能容纳512个句柄(宏定义LOWLEVEL_THRESHOLD)。
    
    2) 如果TableCode的最低2位为1(..01)。说明句柄表有两层,这种情况下该进程可容纳的句柄数是512*1024(宏定义MIDLEVEL_THRSHOLD)。
    
    3)  如果TableCode的最低2位为2(..10)。说明句柄表有三层,这种情况下该进程可容纳的句柄数是512*1024*1024(宏定义HIGHLEVEL_THRSHOLD)。

    但windows执行体限定每个进程的句柄数不得超过2^24=16777216(宏定义MAX_HANDLES)。

    看到这里,我们要理清一下思路:

    1. 在EPROCESS中有一个 PHANDLE_TABLE ObjectTable 成员域指向的是一个HENDLE_TABLE数据结构,这个结构并不是真正保存句柄表真实数据的地址,我们可以把这个

    结构理解为一个句柄表的meta元数据结构。在HANDLE_TABLE数据结构的第一个域成员TableCode才是指向"这个句柄表"对应的真实句柄数组数据。

    2. TableCode指向的这个句柄数据的数据结构为: HANDLE_TABLE_ENTRY。即图中的每个小格子为一个HANDLE_TABLE_ENTRY结构

    typedef struct _HANDLE_TABLE_ENTRY 
    { 
        union 
        { 
            PVOID Object; 
            ULONG ObAttributes; 
            PHANDLE_TABLE_ENTRY_INFO InfoTable; 
            ULONG_PTR Value;
        }; 
        union 
        { 
            union 
        { 
                ACCESS_MASK GrantedAccess; 
                struct 
            { 
                    USHORT GrantedAccessIndex;
                    USHORT CreatorBackTraceIndex;
                };
            }; 
            LONG NextFreeTableEntry;
        }; 
    } HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;

    3. 每个HANDLE_TABLE_ENTRY结构体,即每个句柄的大小的为4byte,而windows执行体在分配句柄表内存时按页面(32位系统下4KB大小)来申请内存。因此,才有了我们

    之前看到了每层的句柄数1024。

    4. 那我们在图中看到的不同的句柄表(注意是表,一个表中有很多句柄项)之间的的链接是怎么回事呢?这是通过TABLE_HANDLE中的成员域HandleTableList来进行链接的,这个一个LIST_ENTRY结构,也即和进程双链表一样,这是一个句柄表双链表。

    总结一下:EPROCESS中的PHANDLE_TABLE ObjectTable 成员域指向的是一个HENDLE_TABLE数据结构,这个域的第一个成员的低2位很重要,它决定了这个进程中总
    共有多少个这样的PHANDLE_TABLE ObjectTable结构,例如(..10),那就有3个PHANDLE_TABLE ObjectTable结构,它们依靠PHANDLE_TABLE ObjectTable中
    的HandleTableList来形成一个双链表,依次形成层次关系,而PHANDLE_TABLE ObjectTable中的TableCode指向的才是指向真实的句柄数组,每个句柄以
    HANDLE_TABLE_ENTRY节诶狗来标识,大小为4byte。所有的HANDLE_TABLE_ENTRY形成一个句柄数组组成一层这样的"句柄数据组",总共有3排
    (因为TableCode的低2位为..10)。
    
    这样,windows中一个进程的句柄表就被表现出来了。
    +0x0c4 ObjectTable      : 0xe292f898 _HANDLE_TABLE
    +0x000 TableCode        : 0xe10fd000
    +0x004 QuotaProcess     : 0x824a7020 _EPROCESS
    +0x008 UniqueProcessId  : 0x000000a0 
    +0x00c HandleTableLock  : [4] _EX_PUSH_LOCK
    +0x01c HandleTableList  : _LIST_ENTRY [ 0xe1090f64 - 0xe283dc4c ]
    +0x024 HandleContentionEvent : _EX_PUSH_LOCK
    +0x028 DebugInfo        : (null) 
    +0x02c ExtraInfoPages   : 0
    +0x030 FirstFree        : 0xac
    +0x034 LastFree         : 0
    +0x038 NextHandleNeedingPool : 0x800
    +0x03c HandleCount      : 48
    +0x040 Flags            : 0
    +0x040 StrictFIFO       : 0y0

    更多的关于句柄表的数据结构和句柄表随着进程创建和运行过程中的扩展和收缩请参考《windows 内核原理与实现》3.4.1: windows进程的句柄表。我们这里就不继续展开了。

     

     

     

    12. EX_FAST_REF Token

    Token域是一个快速引用,指向该进程的访问令牌,用于该进程的安全访问检查。这是进程访问令牌相关的知识,之前做西电线上赛的时候就遇到一题是要提升进程访问令牌权限的要求。

    BOOL WINAPI OpenProcessToken(
      _In_   HANDLE ProcessHandle,
      _In_   DWORD DesiredAccess,
      _Out_  PHANDLE TokenHandle
    );
    +0x0c8 Token            : _EX_FAST_REF
    +0x000 Object           : 0xe10ea4bb 
    +0x000 RefCnt           : 0y011
    +0x000 Value            : 0xe10ea4bb

    http://msdn.microsoft.com/library/aa379295(VS.85).aspx

    相关的内容请参考《Windows核心编程(第四版)》中文版》4.5章,有关UAC的部分。这里提供一个提升进程Access Token权限的链接,小聂博客看到的,当时用的就是这个思路

    http://xiaonieblog.com/?post=81

     

     

    13. PFN_NUMBER WorkingSetPage

    WorkingSetPage域是指包含进程工作集的页面

    工作集是指一个进程当前正在使用的物理页面的集合。在windows中,有3种工作工作集。
    
    1) 进程工作集
    
    2) 系统工作集
    系统工作集是指System进程(由全局变量PsInitialSystemProcess表示)的工作集,其中包括系统模块的映像区(比如ntoskrnl.exe和设备驱动程序)、换页内存池和系统缓存(cache)
    
    3) 会话工作集
    会话工作集是指一个会话所属的代码和数据区,包括windows子系统用到的与会话有关的数据结构、会话换页内存池、会话的映射视图,以及会话空间中的设备驱动程序。

     每个进程都有一个专门的页面用来存放它的"工作集链表",这是在创建进程地址空间时已经映射好的一个页面,其地址位于全局变量MmWorkingSetList中,所以,在任何一个进程中,通过变量MmWokingSetList就可以访问到它的"工作集链表"。windows通过MmWokingSetList来跟踪一个进程所使用的物理页面。 这个MmWokingSetList的类型为"MMWSL":

    typedef struct _MMWSL 
    {
        WSLE_NUMBER FirstFree;
        WSLE_NUMBER FirstDynamic;
        WSLE_NUMBER LastEntry;
        WSLE_NUMBER NextSlot;               // The next slot to trim
        PMMWSLE Wsle;
        WSLE_NUMBER LastInitializedWsle;
        WSLE_NUMBER NonDirectCount;
        PMMWSLE_HASH HashTable;
        ULONG HashTableSize;
        ULONG NumberOfCommittedPageTables;
        PVOID HashTableStart;
        PVOID HighestPermittedHashAddress;
        ULONG NumberOfImageWaiters;
        ULONG VadBitMapHint; 
        USHORT UsedPageTableEntries[MM_USER_PAGE_TABLE_PAGES];
        ULONG CommittedPageTables[MM_USER_PAGE_TABLE_PAGES/(sizeof(ULONG)*8)]; 
    } MMWSL, *PMMWSL;

    更多的内容请学习《windows 内核原理与实现》 4.6章: 工作集管理的相关知识。因为windows内核的知识很多,要在一次的学习中把所有相关的知识点都学到,那就有点像无线递归的感觉了,一层又一层。我们把工作集的内容放到以后研究,但是有一点很重要,我们要记住的一点是: 工作集定了这个进程所拥有的所有内存页,那进程的内存分配,使用,调度算法一定和工作集有关,如果以后我们遇到有关内存调度算法的题目,我们应该要能反应到一些相关的基础知识点,然后到这些经典的书上去翻阅相关知识,这样慢慢积累下来感觉会比较好。

     

     

    14. KGUARDED_MUTEX AddressCreationLock

    AddressCreationLock域是一个守护互斥体锁(guard mutex),用于保护对地址空间的操作。当内核代码需要对虚拟地址空间进行操作时(回想IRP中讲到3种缓冲区时说过,UserBuffer方式,内核代码直接访问用户空间的内存数据),它必须在AddressCreationLock上执行"获取锁"操作,完成以后再"释放锁"操作。

    base\ntos\mm\mi.h中的宏可以简化代码:

    #define LOCK_ADDRESS_SPACE(PROCESS) KeAcquireGuardedMutex (&((PROCESS)->AddressCreationLock));
    #define UNLOCK_ADDRESS_SPACE(PROCESS) KeReleaseGuardedMutex (&((PROCESS)->AddressCreationLock));

     

    15. KSPIN_LOCK HyperSpaceLock

    HyperSpaceLock是一个自旋锁,用于保护进程的超空间。

    typedef struct
    {
        volatile unsigned int lock;
    }
    spinlock_t;

     

    16. struct _ETHREAD *ForkInProgress

    ForkInProgress指向正在复制地址空间的那个线程,仅当在地址空间复制过程中,此域才会被赋值,在其他情况下为NULL。

    base\ntos\mm\forksup.c:

    NTSTATUS
    MiCloneProcessAddressSpace (
        IN PEPROCESS ProcessToClone,
        IN PEPROCESS ProcessToInitialize
        )

     

    17. ULONG_PTR HardwareTrigger

    HardwareTrigger用于记录硬件错误性能分析次数

     

    18.进程的复制

    PMM_AVL_TABLE PhysicalVadRoot:  PhysicalVadRoot域指向进程的物理VAD的根。它并不总是存在,只有当确实需要映射物理内存时才会被创建。

    PVOID CloneRoot:   CloneRoot指向一个平衡树的根,当进程地址空间复制时,此树被创建,创建出来后,一直到进程退出的时候才被销毁。CloneRoot域完全是为了支持fork语义而引入。

    PFN_NUMBER NumberOfPrivatePages: 指进程私有页面的数量

    PFN_NUMBER NumberOfLockedPages: 指进程被锁住的页面的数量

    +0x11c VadRoot          : 0x82401480 
    +0x120 VadHint          : 0x824392c8 
    +0x124 CloneRoot        : (null) 
    +0x128 NumberOfPrivatePages : 0xd9
    +0x12c NumberOfLockedPages : 0

     

    19. PVOID Win32Process / struct _EJOB *Job

    Win32Process域是一个指针,指向由windows子系统管理的进程区域,如果此值不为NULL,说明这是一个windows子系统进程(GUI进程)。对于job域,只有当一个进程属于一个job(作业)的时候,它才会指向一个_EJOB对象。

    +0x130 Win32Process     : 0xe109b210
    +0x134 Job              : (null) 

     

    20. 进程对应的内存区

    PVOID SectionObject:  SectionObject域也是一个指针,代表进程的内存去对象(进程的可执行映像文件的内存区对象)

    PVOID SectionBaseAddress:  SectionBaseAddress域为该内存区对象的基地址

    +0x138 SectionObject    : 0xe19d2c18 
    +0x13c SectionBaseAddress : 0x01000000 

     

     

    21. PEPROCESS_QUOTA_BLOCK QuotaBlock

    QuotaBlock域指向进程的配额块,进程的配额块类型为: EPROCESS_QUOTA_BLOCK

    typedef struct _EPROCESS_QUOTA_BLOCK           
    {
        struct _EPROCESS_QUOTA_ENTRY QuotaEntry[3];
        struct _LIST_ENTRY QuotaList; 
        ULONG32 ReferenceCount;
        ULONG32 ProcessCount;
    }EPROCESS_QUOTA_BLOCK, *PEPROCESS_QUOTA_BLOCK;

    注意到结构中有我们熟悉的LIST_ENTRY双链表结构,心里基本猜到了。windows系统中的"配额块"相互串起来构成了一个双链表,每个配额块都可以被多个进程共享,所以有一个引用计数指用来说明当前有多少个进程正在使用这一配额块。配额块中主要定义了非换页内存池、换页内存池、交换文件中的内存配额限制。

    这里要注意的是,所有配额块构成的双链表的表头为PspQuotaBlockList。系统的默认配额块为PspDefaultQuotaBlock。
    +0x140 QuotaBlock       : 0x82cf5db8 _EPROCESS_QUOTA_BLOCK
    +0x000 QuotaEntry       : [3] _EPROCESS_QUOTA_ENTRY
    +0x030 QuotaList        : _LIST_ENTRY [ 0x82c4dea0 - 0x82ca7700 ]
    +0x038 ReferenceCount   : 0x751
    +0x03c ProcessCount     : 7

     

     

    22. PPAGEFAULT_HISTORY WorkingSetWatch

    WorkingSetWatch域用于监视一个进程的页面错误,一旦启用了页面错误监视功能(由全局变量PsWatchEnabled开关来控制),则每次发生页面错误都会将该页面错误记录到WorkingSetWatch域的WatchInfo成员数组中,知道数组满为止。

    相关的处理函数为: PsWatchWorkingSet()。

    typedef struct _PAGEFAULT_HISTORY
    {
        ULONG CurrentIndex;
        ULONG MapIndex;
        KSPIN_LOCK SpinLock;
        PVOID Reserved;
        PROCESS_WS_WATCH_INFORMATION WatchInfo[1];
    } PAGEFAULT_HISTORY, *PPAGEFAULT_HISTORY;

     

     

    23. HANDLE Win32WindowStation

    Win32WindowStation域是一个进程所属的窗口站的句柄。由于句柄的值是由每个进程的句柄表来决定的,所以,两个进程即使同属于一个窗口站,它们的Win32WindowStation也可能不同,但指向的窗口站对象是相同的。窗口站是由windows子系统来管理和控制的。

     

    24. HANDLE InheritedFromUniqueProcessId

    InheritedFromUniqueProcessId域说明了一个进程是从哪里继承来的,即父进程的标识符。

     

    25. PVOID LdtInformation

    LdtInformation用来维护一个进程的LDT(局部描述符表)信息。

    http://blog.csdn.net/billpig/article/details/5833980

    http://hi.baidu.com/zeyu203/item/74192edfb16abd392b35c7c0

    这两篇文章对GDT和LDT解释的很好了,这里总结一下我对GDT和LDT的理解。

     

    1) 全局描述符表GDT(Global Descriptor Table)

    除了使用虚拟地址来实现有效和灵活的内存管理以外,在计算机发展史上,另一种重要的内存管理方式是将物理内存划分成若干个段(segment),处理器在访问一个内存单元时,通过"段基址+偏移"的方式算出实际的物理地址。每个段都可以有自己的访问属性,包括读写属性、特权级别等。例如,在Intel x86处理器中,有专门的段寄存器,允许每条指令在访问内存时指定在哪个段上进行。段的概念在Intel 8086/8088实模式中已经使用了,但当时段的用途是扩展地址范围,将系统的内存从64KB扩展到1MB(CS:IP那种模式)。

    在Intel x86中,逻辑地址的段部分(即原来的CS:IP中的CS部分)称为"段选择符(segment selector)",也有称为段选择子的。
    这个"段选择符"指定了段的索引以及要访问的特权级别。段寄存器CS,SS,DS,ES,FS,GS专门用于指定一个地址的段选择符。虽然只有这六个寄存器,但是软件可以灵活地使用它们来完成各种功能。其中与有三个段寄存器有特殊的用途:
    1) CS: 代码段寄存器,指向一个包含指令的段,即代码段
    2) SS: 栈段寄存器,指向一个包含当前调用栈的段,即栈段
    3) DS: 数据段寄存器,指向一个包含全局和静态数据的段,即数据段

     

    在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT),GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此积存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限(也就是说,我们通过GDTR中的值可以找到这张全局描述表的真实数据结构,GDTR的作用也就仅仅这样了,找到GDT就没了)。

    那借来来就有一个问题了,我们通过GDTR中保存的基地址找到了一个所谓的全局描述表GDT,并通过"段选择子"索引到了GDT中的某一项(具体怎么索引我们后面会举例子),我们这里暂时称为某一项,因为我们目前还不知道GDT中的一项项是什么数据,那GDT中都保存了什么数据呢?这就要涉及到局部描述符表LDT了。

     

    2) 局部描述符表LDT(Local Descriptor Table)
    局部描述符表可以有若干张,每个任务可以有一张。我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。

    我们知道,一个应用程序中有代码段/数据段/TSS段/LDT段,而每一个段都对一个一个LDT,而这些LDT可以通过GDT寻址到。我们回想一下,GDTR是用来寻址GDT的,因为GDT只有一个,所以可以用GDTR+段选择子的方法来寻址LDT,但是LDT不一样了,LDT有很多个,所以不存在基址这一说,所以LDTR中保存的是当前的LDT,这个指是可以动态改变的。

    LDT即局部描述符(或者叫段描述符),他记录了这个段的一些属性: 段的起始地址、有效范围、和一些属性。

    每一个LDT中都这样的数据结构,它很好地描述了这个段对应的信息。我们重点解释其中几个字段:

    G: 当G位为0时,此长度单位为byte。当G为1时,此长度单位为4096byte。所以,段长度可达2^20 * 4096 = 4GB,即整个32位线性地址空间。
    
    DPL(描述符特权级 Descriptor Privilege Level)是允许访问此段的最低特权级(结合下面学习的"段选择子"中有一个字段(RPL)是标识这个段选择子也即这个内存访问请求的
    特权级),这样是不是就把对应关系建立起来了,比如DPL为0的段只有当RPL=0时才能访问,而DPL为3的段,可由任何RPL的代码访问。这样就解释了为什么ring3的内存空间
    ring0的内核代码可以任意访问,而ring0的内存空间ring3不能访问了。
    
    TYPE(类型域): 指定了段的类型,包括代码段、数据段、TSS段、LDT段。

     

    3) 段选择子(Selector)

    我们之前还有一个疑问没解决,"我们通过GDTR中保存的基地址找到了一个所谓的全局描述表GDT,并通过"段选择子"索引到了GDT中的某一项"。那段选择字是怎么来进行索引的呢?

    段选择子是一个16位的寄存器(同实模式下的段寄存器相同)

    段选择子包括三部分:描述符索引(index)、TI、请求特权级(RPL)。他的index(描述符索引)部分表示所需要的段的描述符(LDT)在描述符表的位置(编号),由这个位置再根据在GDTR中存储的描述符表基址(GDTR的32位基地址用来寻址GDT的基地址)就可以找到相应的描述符(LDT)。然后用描述符表中的段基址加上逻辑地址(假如给出这样的逻辑地址 SEL:OFFSET)的OFFSET就可以转换成线性地址,段选择子中的TI值只有一位0或1,0代表选择子是在GDT选择,1代表选择子是在LDT选择。请求特权级(RPL)则代表选择子的特权级,共有4个特权级(0级、1级、2级、3级)(但是只有2中状态被实际使用,0表示最高特权级,3表示最低特权级,CPU只能访问同一特权级或级别较低特权级的段)。

    例如给出逻辑地址:21h:12345678h转换为线性地址
    a. 选择子SEL=21h=0000000000100 0 01b 他代表的意思是:选择子的index=4即100b选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;左后的01b代表特权级RPL=1
    b. OFFSET=12345678h若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h

     

    说了这么多,来总一下LDT和GDT:

    1. 首先,要把一些概念和名词弄清楚,有很多书和网上的文章中给了很多名词,我们要能理解它们。段选择符和段选择子是一个概念,它们就相当于8080下实模式中的CS。
    
    2. 我们给出的逻辑地址由段选择子和偏移量组成: SEL + OFFSET.
    
    3. GDTR中保存着GDT的基地址,通过GDTR我们可以寻址到GDT(你可以理解为寻址一个数组的基地址)
    
    4. GDT这个"数组"中保存着很多LDT,它们每个都代表着一个段,我们需要通过SEL(段选择子)来索引具体的LDT
    
    5. LDTR中保存的是当前的LDT地址,也即一个段选择子。
    
    6. 每个LDT中保存了这个段的一些关键的信息,包括段基址,段的特权,段的大小等等。

    更多细节请参考《windows 内核原理与实现》4.1.2 段式内存管理

    说了这么多,回到我们的主线上来,LdtInformation域保存的就是一个进程的LDT。

     

     

     

    26. PVOID VadFreeHint

    VadFreeHint域指向一个提示VAD(虚拟地址描述符)节点,用于加速在VAD树中执行查找操作。

     

    27. PVOID VdmObjects

    VdmObjects域指向当前进程的VDM数据区,其类型为VMD_PROCESS_OBJECTS,进程可通过NtVdmControl系统服务来初始化VDM。

     

    28. PVOID DeviceMap

    DeviceMap域指向进程使用的设备表,通常情况下同一个会话中的进程共享同样的设备表。有关设备表的用法请参考《windows 内核原理与实现》 7章。请原谅我又采取这么马虎的方式忽略过去了,因为我不想让这篇文章的主线变得过于冗长,我会在之后的学习笔记中补充上之前说过的类似的参考之类的话,分专题进行学习笔记。

     

    29. PVOID Spare0[3]

    Spare0域是一个备用域。用法未知,WRK中也没有使用。

     

    30. 页表项

    union
    {
            HARDWARE_PTE PageDirectoryPte;
            ULONGLONG Filler;
    };
    PageDirectoryPte域是顶级页目录页面的页表项。这涉及到windows中的页式内存管理的知识,我们拓展出去。
    让进程使用虚拟地址,而虚拟地址和物理地址之间通过一个映射表来完成转译。页式内存和我们之前学习的段式内存管理方式都是基于这样一种思路的具体实现。

    在页式内存管理中,虚拟地址空间是按页(page)来管理的(回想段式内存管理中,虚拟地址空间是靠"段+偏移"来管理的),对应于物理内存也按页来管理(一般情况下虚拟内存和物理内存使用相同的页面大小
    4KB)。物理内存中的页面有时称为"页帧(page frmae)",其大小与虚拟空间中的页面相同。因此,映射的基本度量单位为"页",在虚拟地址空间中连续的页面对应于在物理内存中的页面可以不连续。
    通过维护这个虚拟地址空间与物理内存页面之前的映射关系,物理页面可以被"动态"地分配给特定的虚拟页面,从而当只有真正有需要的时候才把物理页面分配给虚拟页面(其他时候,进程看到的都是一个
    伪的平坦4GB内存空间)。


    注意,在一个系统中,物理地址空间只有一个,但虚拟地址空间可以有多个。每个虚拟地址空间都必须有一个映射关系。
    有了页面划分的机制以后,每个虚拟地址(逻辑地址)32位信息中,其中一部分位信息指定了一个"页索引",其余的位信息则指定了业内的偏移量。也就是说,虚拟地址分成了两部分: 页索引+页内偏移。

    既然有页索引,就自然会有一个页表。在windows中,我们称之为"页面映射表"。Intel x86采用了"分级页表"的方式来管理这一映射关系。32位的虚拟地址中的"页索引部分"又被分成"页目录索引(10位)"
    和"页表索引(10位)"这两部分。

    基于这样的虚拟地址构成,每个虚拟地址(进程地址空间)对应有一个页目录(最顶层),其中包含2^10=1024个目录项(PDE Page Directory Entry)。而页目录中的每一个目录项又指向一张包含1024项的页
    表,而每个页表中才保存的是页面。
    所以,Intel x86处理器在解析一个虚拟地址时,首先根据最高10位在"页目录"中定位到一个"页目录项(PDE)",这个"页目录项"指向一个"页表",然后根据接下来的10位,在页表中定位到一个"页表项(
    PTE Page Table Entry)"。这个"页表项"就是一个"页面",此页表项指定了目标页面的物理地址。最后在此物理地址的基础上加上页面偏移,即得到最终的物理地址。
    (这段话可能有点绕,一定要结合图片细心的理解,它其实就是一个多层的关系)
    《windows 内核原理与实现》 4.1.1 节中有关于页式内存管理的详细细节。

    回到我们的主线上来:
    PageDirectoryPte域是顶级页目录页面的页表项
    
    

    PageDirectoryPte域是页目录中的第一项对应的那个页表。

     

     

    31. PVOID Session

    Session指向进程所在的系统会话,实际上它是一个指向MM_SESSION_SPACE的指针。\base\ntos\mm\mi.h 中相关的结构体定义。

    每个进程在初始化创建地址空间时会加入到当前的系统会话中。

     

    +0x170 Session          : 0xf8b6a000 

     

     

    32. UCHAR ImageFileName[ 16 ]

    ImageFileName域包含了进程的映像文件名,仅包含最后一个路径分隔符之后的字符串,不超过16字节。

    +0x174 ImageFileName    : [16]  "notepad.exe"

     

    33. LIST_ENTRY JobLinks

    JobLinks域是一个双链表节点,通过此节点,一个job中的所有进程构成了一个链表。在windows中,所有的job构成了一个双链表,其链表头为全局变量PspJobList。每个job中的进程又构成了一个双链表。

    (可以发现,在windows内核中,进程,job,线程都是可以通过双链表遍历的方法来进行枚举的,但是遇到"断链法"就不行了,我们之后会专题研究windows内核中进程枚举的知识)

    +0x184 JobLinks         : _LIST_ENTRY [ 0x0 - 0x0 ]
          +0x000 Flink            : (null) 
          +0x004 Blink            : (null) 

     

    34. PVOID LockedPagesList

    LockedPagesList域是一个指向LOCK_HEADER结构的指针,该结构包含了一个链表头,windows通过此链表来记录哪些页面已被锁住(这里所谓的锁住和Mdll中的映射机制有关,本质上就是把用户空间下的内存地址锁定到内核空间中以便访问)。

    base\ntos\mm\iosup.c中有一组函数用于管理此链表: MiAddMdlTracker、MiFreeMdlTracker、MiUpdateMdlTracker/

     

     

    35. LIST_ENTRY ThreadListHead

    ThreadListHead域是一个双链表的"头结点",该链表中包含了一个进程中的所有"线程"。即EPROCESS中的ThreadListHead域的链表中包含了各个子线程的ETHREAD结构中的ThreadListHead节点。

    这里要注意看清除哦,EPROCESS中的ThreadListHead域包含的不是EHREEAD的基址,而是ETHREAD中的ThreadListHead节点的指针,即它们是通过这个双链表来串起
    来的,这个细节在我们枚举进程或枚举进程的时候会经常遇到,就是要处理一个偏移量的问题。

     

     

    36. PVOID SecurityPort

    SecurityPort域是一个安全端口,指向该进程域lsass.exe进程之间的跨进程通信端口。

     

    37. PVOID PaeTop

    PaeTop域用于支持PAE内存访问机制。

    +0x19c PaeTop           : 0xf8cb21a0 

     

    38. ULONG ActiveThreads

    ActiveThreads域记录了当前进程有多少活动线程。当该值减为0时,所有的线程将退出,于是进程也退出。

    +0x1a0 ActiveThreads    : 1

    (可以看到,当前记事本进程就一个线程: 主线程)

     

    39. ACCESS_MASK GrantedAccess

    GrantedAccess域包含了进程的访问权限,访问权限是一个"位组合"。 public\sdk\inc\ntpsapi.h 中的宏 PROCESS_XXX

    ...
    #define PROCESS_TERMINATE         (0x0001)  // winnt
    #define PROCESS_CREATE_THREAD     (0x0002)  // winnt
    #define PROCESS_SET_SESSIONID     (0x0004)  // winnt
    #define PROCESS_VM_OPERATION      (0x0008)  // winnt
    #define PROCESS_VM_READ           (0x0010)  // winnt
    #define PROCESS_VM_WRITE          (0x0020)  // winnt
    // begin_ntddk begin_wdm begin_ntifs
    #define PROCESS_DUP_HANDLE        (0x0040)  // winnt
    ...
    +0x1a4 GrantedAccess    : 0x1f0fff

     

    40. ULONG DefaultHardErrorProcessing

    DefaultHardErrorProcessing域指定了默认的硬件错误处理,默认为1

    +0x1a8 DefaultHardErrorProcessing : 1

     

    41. NTSTATUS LastThreadExitStatus

    LastThreadExitStatus域记录了刚才最后一个线程的退出状态。当主线程的入口点函数(WinMain, wWinMain, main, wmain)返回时,会返回到C/C++"运行库启动代码",后者将正确清理进程使用的全部C运行时资源。在《windows核心编程》这本书中的第4章: 进程。有对进程和线程的创建以及C/C++运行库的启动代码的权威解释。

    +0x1ac LastThreadExitStatus : 0

    42. PPEB Peb

    Peb域是一个进程的"进程环境块(PEB Process Environment Block)",这是一个位于"进程地址空间(即用户模式空间)"的内存块(为什么要放在用户模式空间呢?因为这个结构需要在被用户模式空间的代码在运行中修改),其中包含了有关进程地址空间中的堆和系统模块等信息。我们之后会详细分析PEB。

     

    43. EX_FAST_REF PrefetchTrace

    PrefetchTrace域是一个快速引用,指向与该进程关联的一个"预取痕迹结构",以支持该进程的预取。

    +0x1b4 PrefetchTrace    : _EX_FAST_REF
          +0x000 Object           : 0x8252944e 
          +0x000 RefCnt           : 0y110
          +0x000 Value            : 0x8252944e

     

    44. 进程中和IRP相关的内容

    LARGE_INTEGER ReadOperationCount;
    LARGE_INTEGER WriteOperationCount;
    LARGE_INTEGER OtherOperationCount;
    LARGE_INTEGER ReadTransferCount;
    LARGE_INTEGER WriteTransferCount;
    LARGE_INTEGER OtherTransferCount;
    ReadOperationCount,WriteOperationCount记录了当前进程NtReadFile和NtWriteFile系统服务被调用的次数,OtherOperationCount记录了除读写操作以外的其他IO服务的次数(文件信息设置.)
    ReadTransferCountWriteTransferCount记录了IO读写操作"完成"的次数,OtherTransferCount记录了除读写操作以外操作完成的次数。



    44. PVOID AweInfo
    AweInfo域是一个指向AWEINFO结构的指针,其目的是支持AWE(Adress Windowing Extension 地址窗口扩展)


    45. SE_AUDIT_PROCESS_CREATION_INFO SeAuditProcessCreationInfo
    SeAuditProcessCreationInfo域包含了创建进程时指定的进程映像全路径名,我们之前学过的ImageFileName域实际上就是从这里"提取"出来的。
    +0x1f4 SeAuditProcessCreationInfo : _SE_AUDIT_PROCESS_CREATION_INFO
          +0x000 ImageFileName    : 0x82851db0 _OBJECT_NAME_INFORMATION
    
    

    46. MMSUPPORT Vm

    Vm域是windows为每个进程管理虚拟内存的重要数据结构成员,其类型为MMSUPPORT, \base\ntos\inc\ps.h 中有相关定义。

    typedef struct _MMSUPPORT 
    {
        LIST_ENTRY WorkingSetExpansionLinks;
        LARGE_INTEGER LastTrimTime;
    
        MMSUPPORT_FLAGS Flags;
        ULONG PageFaultCount;
        WSLE_NUMBER PeakWorkingSetSize;
        WSLE_NUMBER GrowthSinceLastEstimate;
    
        WSLE_NUMBER MinimumWorkingSetSize;
        WSLE_NUMBER MaximumWorkingSetSize;
        struct _MMWSL *VmWorkingSetList;
        WSLE_NUMBER Claim;
    
        WSLE_NUMBER NextEstimationSlot;
        WSLE_NUMBER NextAgingSlot;
        WSLE_NUMBER EstimatedAvailable;
        WSLE_NUMBER WorkingSetSize;
    
        EX_PUSH_LOCK WorkingSetMutex;
    
    } MMSUPPORT, *PMMSUPPORT;
    +0x1f8 Vm               : _MMSUPPORT
          +0x000 LastTrimTime     : _LARGE_INTEGER 0x1ceeef4`925cc6fc
          +0x008 Flags            : _MMSUPPORT_FLAGS
          +0x00c PageFaultCount   : 0x3c6
          +0x010 PeakWorkingSetSize : 0x39b
          +0x014 WorkingSetSize   : 0x399
          +0x018 MinimumWorkingSetSize : 0x32
          +0x01c MaximumWorkingSetSize : 0x159
          +0x020 VmWorkingSetList : 0xc0883000 _MMWSL
          +0x024 WorkingSetExpansionLinks : _LIST_ENTRY [ 0x805595f0 - 0x828ec62c ]
          +0x02c Claim            : 0
          +0x030 NextEstimationSlot : 0
          +0x034 NextAgingSlot    : 0
          +0x038 EstimatedAvailable : 0
          +0x03c GrowthSinceLastEstimate : 0x3c6

    (从它的数据结构中我们可以看到很多熟悉的字段,因为一定涉及到内存调度,所以有工作集字段,此外,包括峰值等..)

     

     

    47. LIST_ENTRY MmProcessLinks

    MmProcessLinks域代表一个双链表节点,所有拥有自己地址空间的进程都将加入到一个双链表中,链表头是全局变量MmProcessList。当进程地址空间被初始创建时,MmProcessLinks节点会被加入到此全局链表中。当进程地址空间被销毁时,该节点脱离此链表。此全局链表的存在使得windows系统共可以方便地执行一些全局的内存管理任务,同时也可以被我们用来进行进程枚举。

     

    48. ULONG ModifiedPageCount

    ModifiedPageCount域记录了该进程中已修改的页面的数量,即"脏页面数量",这和缓存的读写有关。

    +0x23c ModifiedPageCount : 7

     

     49. ULONG JobStatus

    JobStatus域记录了进程所属job的状态。

     

    50. ULONG Flags

    Flags域包含了进程的标志位,这些标志位反映了进程的当前状态和配置。  \base\ntos\inc\ps.h 中的宏定义 PS_PROCESS_FLAGS_XXX

    ...
      #define PS_PROCESS_FLAGS_PROCESS_DELETE         0x00000008UL // Delete process has been issued
        #define PS_PROCESS_FLAGS_WOW64_SPLIT_PAGES      0x00000010UL // Wow64 split pages
        #define PS_PROCESS_FLAGS_VM_DELETED             0x00000020UL // VM is deleted
        #define PS_PROCESS_FLAGS_OUTSWAP_ENABLED        0x00000040UL // Outswap enabled
        #define PS_PROCESS_FLAGS_OUTSWAPPED             0x00000080UL // Outswapped
        #define PS_PROCESS_FLAGS_FORK_FAILED            0x00000100UL // Fork status
        #define PS_PROCESS_FLAGS_WOW64_4GB_VA_SPACE     0x00000200UL // Wow64 process with 4gb virtual address space
        #define PS_PROCESS_FLAGS_ADDRESS_SPACE1         0x00000400UL // Addr space state1
        #define PS_PROCESS_FLAGS_ADDRESS_SPACE2         0x00000800UL // Addr space state2
        #define PS_PROCESS_FLAGS_SET_TIMER_RESOLUTION   0x00001000UL // SetTimerResolution has been called
    ...
    +0x248 Flags            : 0xd0800

     

    51. NTSTATUS ExitStatus

    ExitStatus域包含了进程的退出状态,从进程的退出状态通常可以获知进程非正常退出的大致原因。反映退出状态的一些宏定义位于 public\sdk\inc\ntstatus.h

    ...
    #define STATUS_SEVERITY_SUCCESS          0x0
    #define STATUS_SEVERITY_INFORMATIONAL    0x1
    ...

     

    52. USHORT NextPageColor

    NextPageColor域用于物理页面分配算法。

     

    53. USHORT SubSystemVersion

    SubSystemVersion域中的SubSystemMinorVersion和SubSystemMajorVersion分别记录了一个"进程的子系统"的主板本号和此版本号,它们的值来源于进程映像文件PE的对应版本信息(PE的头部包含了此信息)。在之前的PE结构探究中有关于这方面的内容。

    http://www.cnblogs.com/LittleHann/archive/2013/06/14/3136111.html

    +0x252 SubSystemMinorVersion : 0 ''
    +0x253 SubSystemMajorVersion : 0x4 ''

     

    54. UCHAR PriorityClass

    PriorityClass域是一个单字节值,它说明了一个进程的优先级程度。这和进程、线程优先级的知识有关。

    1) windows支持6个进程优先级类(priority class): idle, below normal, normal, above normal, high, real-time。

    normal是最常用的优先级类
    real-time:此进程中的线程必须立即响应事件,执行实时任务。次进程中的线程还会抢占操作系统的组件的CPU时间。
    high:此进程中的线程必须立即响应事件,执行实时任务。任务管理器运行在这一级,因此用户可以通过它结束失控的进程
    above normal:
    normal:此进程中的线程无需特殊的调度,大多数进程都是这一级别的
    below normal:
    idle:此进程中的线程在系统空闲时运行。屏保,后台实时程序通常使用该优先级

     

    2) 选择了进程优先级后,我们应该转而关心进程中线程的相对优先级
    idle, lowest, below normal, normal, above normal, highest, time-critical

    应用程序的开发人员无需处理优先级,而是由系统将进程的优先级类和线程的相对优先级整合起来映射到一个优先级值(组成一个二维表)。

    注意:
    1. 表中线程优先级没有0,因为0优先级保留给页面清零线程了,系统不允许其他任何线程的优先级为0.
    2. ring3应用程序无法获得一下优先级:171819202127282930。如果你编写的是内核模式的驱动程序,那可以获得这些优先级
    3. real-time优先级类的线程,其优先级不能低于16.非real-time优先级线程的优先级不能高于1

    在 public\sdk\inc\ntpsapi.h 中可以找到PROCESS_PRIORITY_CLASS_XX的宏定义

    #define PROCESS_PRIORITY_CLASS_UNKNOWN      0
    #define PROCESS_PRIORITY_CLASS_IDLE         1
    #define PROCESS_PRIORITY_CLASS_NORMAL       2
    #define PROCESS_PRIORITY_CLASS_HIGH         3
    #define PROCESS_PRIORITY_CLASS_REALTIME     4
    #define PROCESS_PRIORITY_CLASS_BELOW_NORMAL 5
    #define PROCESS_PRIORITY_CLASS_ABOVE_NORMAL 6

    PriorityClass域说明了一个进程的优先级程度

     

     

    55. MM_AVL_TABLE VadRoot

    VadRoot域指向一个平衡二叉树的根,用于管理该进程的虚拟地址空间。

     

    56. ULONG Cookie

    Cookie域存放的是一个代表该进程的随机值,当第一次通过NtQueryInformationProcess函数获取此Cookie值的时候,系统会生成一个随机值,以后就用此值代表此进程。

    +0x258 Cookie           : 0x936269b3

    我们在编译时使用的GS防御技术中用到的Cookie值指的就是这个,关于这个Cookie,我了解的不是很多,这里给出在程序中dunp下来的cookie创建流程吧。

    大概思路是计算这么一个表达式的值 
    KPRCB->KeSystemCalls^KPRCB->InterruptTime^KeQuerySystemTime()返回值的高双字^KeQuerySystemTime()返回值的低双字 
    再用cmpxchg指令把这个随机值赋给EPROCESS.COOKIE 
      
    805c395b 8d45c4           lea     eax,[ebp-0x3c] 
    805c395e 50               push    eax 
    805c395f e8c643f3ff       call    nt!KeQuerySystemTime (804f7d2a) 
    805c3964 3ea120f0dfff     mov     eax,ds:[ffdff020] ;eax=kpcr->Prcb 
    805c396a 8b8818050000     mov     ecx,[eax+0x518] ;ecx=KPRCB->KeSystemCalls 
    805c3970 3388b8040000     xor     ecx,[eax+0x4b8] ;ecx=KPRCB->KeSystemCalls ^ KPRCB->InterruptTime 
    805c3976 334dc8           xor     ecx,[ebp-0x38] ;ecx= 
    805c3979 334dc4           xor     ecx,[ebp-0x3c] 
    805c397c 898d34ffffff     mov     [ebp-0xcc],ecx ;ecx=KPRCB->KeSystemCalls ^ KPRCB->InterruptTime ^KeQuerySystemTime()
    返回值的高双字^KeQuerySystemTime()返回值的 低双字 805c3982 89bd2cffffff mov [ebp-0xd4],edi ;edi=&eprocess->Cookie 805c3988 b800000000 mov eax,0x0 ;eax=0 805c398d 8b8d2cffffff mov ecx,[ebp-0xd4] ;ecx=&eprocess->Cookie 805c3993 8b9534ffffff mov edx,[ebp-0xcc] ;edx=KPRCB->KeSystemCalls ^ KPRCB->InterruptTime ^KeQuerySystemTime()
    返回值的高双字^KeQuerySystemTime()返回值的 低双字 805c3999 0fb111 cmpxchg [ecx],edx
    +0x258 Cookie           : 0x936269b3

     

     

    至此,我们已经把EPROCESS的全部结构都分析完毕了,本来是想把KPROCESS/PEB放到一起的,可以发现这样篇幅有些过长了,所以决定把KPROCESS和PEB放到下一篇学习笔记一起解决。

    EPROCESS KPROCESS PEB  《寒江独钓》内核学习笔记(3)

     

    转载于:https://www.cnblogs.com/LittleHann/p/3453148.html

    展开全文
  • 1.3.5 设置Windows内核符号表 12 1.3.6 实战调试first 13 第2章 内核编程环境及其特殊性 16 2.1 内核编程的环境 16 2.1.1 隔离的应用程序 16 2.1.2 共享的内核空间 17 2.1.3 无处不在的内核模块 18 2.2 数据...
  • 1.3.5 设置Windows内核符号表 12 1.3.6 实战调试first 13 第2章 内核编程环境及其特殊性 16 2.1 内核编程的环境 16 2.1.1 隔离的应用程序 16 2.1.2 共享的内核空间 17 2.1.3 无处不在的内核模块 18 2.2 数据...
  • 本书是目前第一本关于rootkit的详尽指南,包括rootkit的概念、它们是怎样工作的...本站提供的Rootkits--Windows内核的安全防护 中文 PDF版 [21M] 资源来源互联网,版权归该下载资源的合法拥有者所有。 收起信息返回顶部
  • 在Linux系统中, 内核进程和用户进程所占的虚拟内存比例是1:3,而Windows系统为2:2(通过设置Large-Address-Aware Executables标志也可为1:3)。这并不意味着内核使用那么多物理内存,仅表示它可支配这部分地址空间,...

    一:Linux虚拟地址空间布局

    在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中。这个沙盘就是虚拟地址空间(Virtual Address Space),在32位模式下它是一个4GB的内存地址块。

    Linux系统中, 内核进程和用户进程所占的虚拟内存比例是1:3,而Windows系统为2:2(通过设置Large-Address-Aware Executables标志也可为1:3)。这并不意味着内核使用那么多物理内存,仅表示它可支配这部分地址空间,根据需要将其映射到物理内存。

    虚拟地址通过页表(Page Table)映射到物理内存,页表由操作系统维护并被处理器引用。内核空间在页表中拥有较高特权级,因此用户态程序试图访问这些页时会导致一个页错误(page fault)。

    Linux中,内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。内核代码和数据总是可寻址,随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化。

    Linux进程在虚拟内存中的标准内存段布局如下图所示:
    在这里插入图片描述
    其中,用户地址空间中的蓝色条带对应于映射到物理内存的不同内存段,灰白区域表示未映射的部分。这些段只是简单的内存地址范围,与Intel处理器的段没有关系。

    上图中Random stack offsetRandom mmap offset等随机值意在防止恶意程序。

    Linux通过对栈、内存映射段、堆的起始地址加上随机偏移量来打乱布局,以免恶意程序通过计算访问栈、库函数等地址。

    execve(2)负责为进程代码段和数据段建立映射,真正将代码段和数据段的内容读入内存是由系统的缺页异常处理程序按需完成的。另外,execve(2)还会将BSS段清零。

    用户进程部分分段存储内容如下表所示(按地址递减顺序):
    在这里插入图片描述

    在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS段的加载,并在内存中为这些段分配空间。栈也由操作系统分配和管理;堆由程序员自己管理,即显式地申请和释放空间。

    BSS段、数据段和代码段是可执行程序编译时的分段,运行时还需要栈和堆。
    以下详细介绍各个分段的含义。

    1 内核空间

    内核总是驻留在内存中,是操作系统的一部分。内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。

    2 栈(stack)

    栈又称堆栈,由编译器自动分配释放,行为类似数据结构中的栈(先进后出)。堆栈主要有三个用途:

    为函数内部声明的非静态局部变量(C语言中称“自动变量”)提供存储空间。
    记录函数调用过程相关的维护性信息,称为栈帧(Stack Frame)或过程活动记录(Procedure Activation Record)。它包括函数返回地址,不适合装入寄存器的函数参数及一些寄存器值的保存。除递归调用外,堆栈并非必需。因为编译时可获知局部变量,参数和返回地址所需空间,并将其分配于BSS段。
    临时存储区,用于暂存长算术表达式部分计算结果或alloca()函数分配的栈内内存。
    持续地重用栈空间有助于使活跃的栈内存保持在CPU缓存中,从而加速访问。进程中的每个线程都有属于自己的栈。向栈中不断压入数据时,若超出其容量就会耗尽栈对应的内存区域,从而触发一个页错误。此时若栈的大小低于堆栈最大值RLIMIT_STACK(通常是8M),则栈会动态增长,程序继续运行。映射的栈区扩展到所需大小后,不再收缩。

    Linux中ulimit -s命令可查看和设置堆栈最大值,当程序使用的堆栈超过该值时, 发生栈溢出(Stack Overflow),程序收到一个段错误(Segmentation Fault)。注意,调高堆栈容量可能会增加内存开销和启动时间。

    堆栈既可向下增长(向内存低地址)也可向上增长, 这依赖于具体的实现。本文所述堆栈向下增长。

    堆栈的大小在运行时由内核动态调整。

    3 内存映射段(mmap)

    此处,内核将硬盘文件的内容直接映射到内存, 任何应用程序都可通过Linuxmmap()系统调用或WindowsCreateFileMapping()/MapViewOfFile()请求这种映射。内存映射是一种方便高效的文件I/O方式, 因而被用于装载动态共享库。用户也可创建匿名内存映射,该映射没有对应的文件, 可用于存放程序数据。在Linux中,若通过malloc()请求一大块内存,C运行库将创建一个匿名内存映射,而不使用堆内存。”大块” 意味着比阈值 MMAP_THRESHOLD还大,缺省为128KB,可通过mallopt()调整。

    该区域用于映射可执行文件用到的动态链接库。在Linux 2.4版本中,若可执行文件依赖共享库,则系统会为这些动态库在从0x40000000开始的地址分配相应空间,并在程序装载时将其载入到该空间。在Linux 2.6内核中,共享库的起始地址被往上移动至更靠近栈区的位置。

    从进程地址空间的布局可以看到,在有共享库的情况下,留给堆的可用空间还有两处:一处是从.bss段到0x40000000,约不到1GB的空间;另一处是从共享库到栈之间的空间,约不到2GB。这两块空间大小取决于栈、共享库的大小和数量。这样来看,是否应用程序可申请的最大堆空间只有2GB?事实上,这与Linux内核版本有关。在上面给出的进程地址空间经典布局图中,共享库的装载地址为0x40000000,这实际上是Linux kernel 2.6版本之前的情况了,在2.6版本里,共享库的装载地址已经被挪到靠近栈的位置,即位于0xBFxxxxxx附近,因此,此时的堆范围就不会被共享库分割成2个“碎片”,故kernel 2.632Linux系统中,malloc申请的最大内存理论值在2.9GB左右。

    4 堆(heap)

    堆用于存放进程运行时动态分配的内存段,可动态扩张或缩减。堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。当进程调用malloc©/new(C++)等函数分配内存时,新分配的内存动态添加到堆上(扩张);当调用free©/delete(C++)等函数释放内存时,被释放的内存从堆中剔除(缩减) 。

    分配的堆内存是经过字节对齐的空间,以适合原子操作。堆管理器通过链表管理每个申请的内存,由于堆申请和释放是无序的,最终会产生内存碎片。堆内存一般由应用程序分配释放,回收的内存可供重新使用。若程序员不释放,程序结束时操作系统可能会自动回收。

    堆的末端由break指针标识,当堆管理器需要更多内存时,可通过系统调用brk()和sbrk()来移动break指针以扩张堆,一般由系统自动调用。

    使用堆时经常出现两种问题:1) 释放或改写仍在使用的内存(“内存破坏”);2)未释放不再使用的内存(“内存泄漏”)。当释放次数少于申请次数时,可能已造成内存泄漏。泄漏的内存往往比忘记释放的数据结构更大,因为所分配的内存通常会圆整为下个大于申请数量的2的幂次(如申请212B,会圆整为256B)。

    注意,堆不同于数据结构中的”堆”,其行为类似链表。

    【扩展阅读】栈和堆的区别

    ①管理方式:栈由编译器自动管理;堆由程序员控制,使用方便,但易产生内存泄露。

    ②生长方向:栈向低地址扩展(即”向下生长”),是连续的内存区域;堆向高地址扩展(即”向上生长”),是不连续的内存区域。这是由于系统用链表来存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历。

    ③空间大小:栈顶地址和栈的最大容量由系统预先规定(通常默认2M或10M);堆的大小则受限于计算机系统中有效的虚拟内存,32位Linux系统中堆内存可达2.9G空间。

    ④存储内容:栈在函数调用时,首先压入主调函数中下条指令(函数调用语句的下条可执行语句)的地址,然后是函数实参,然后是被调函数的局部变量。本次调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的指令地址,程序由该点继续运行下条可执行语句。堆通常在头部用一个字节存放其大小,堆用于存储生存期与函数调用无关的数据,具体内容由程序员安排。

    ⑤分配方式:栈可静态分配或动态分配。静态分配由编译器完成,如局部变量的分配。动态分配由alloca函数在栈上申请空间,用完后自动释放。堆只能动态分配且手工释放。

    ⑥分配效率:栈由计算机底层提供支持:分配专门的寄存器存放栈地址,压栈出栈由专门的指令执行,因此效率较高。堆由函数库提供,机制复杂,效率比栈低得多。Windows系统中VirtualAlloc可直接在进程地址空间中分配一块内存,快速且灵活。

    ⑦分配后系统响应:只要栈剩余空间大于所申请空间,系统将为程序提供内存,否则报告异常提示栈溢出。

    操作系统为堆维护一个记录空闲内存地址的链表。当系统收到程序的内存分配申请时,会遍历该链表寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点空间分配给程序。若无足够大小的空间(可能由于内存碎片太多),有可能调用系统功能去增加程序数据段的内存空间,以便有机会分到足够大小的内存,然后进行返回。,大多数系统会在该内存空间首地址处记录本次分配的内存大小,供后续的释放函数(如free/delete)正确释放本内存空间。

    此外,由于找到的堆结点大小不一定正好等于申请的大小,系统会自动将多余的部分重新放入空闲链表中。

    ⑧碎片问题:栈不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面的后进的栈内容已弹出。而频繁申请释放操作会造成堆内存空间的不连续,从而造成大量碎片,使程序效率降低。

    可见,堆容易造成内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和内核态切换,内存申请的代价更为昂贵。所以栈在程序中应用最广泛,函数调用也利用栈来完成,调用过程中的参数、返回地址、栈基指针和局部变量等都采用栈的方式存放。所以,建议尽量使用栈,仅在分配大量或大块内存空间时使用堆。

    使用栈和堆时应避免越界发生,否则可能程序崩溃或破坏程序堆、栈结构,产生意想不到的后果。

    5 BSS段

    BSS(Block Started by Symbol)段中通常存放程序中以下符号:

    未初始化的全局变量和静态局部变量
    初始值为0的全局变量和静态局部变量(依赖于编译器实现)
    未定义且初值不为0的符号(该初值即common block的大小)
    C语言中,未显式初始化的静态分配变量被初始化为0(算术类型)或空指针(指针类型)。由于程序加载时,BSS会被操作系统清零,所以未赋初值或初值为0的全局变量都在BSS中。BSS段仅为未初始化的静态分配变量预留位置,在目标文件中并不占据空间,这样可减少目标文件体积。但程序运行时需为变量分配内存空间,故目标文件必须记录所有未初始化的静态分配变量大小总和(通过start_bss和end_bss地址写入机器代码)。当加载器(loader)加载程序时,将为BSS段分配的内存初始化为0。在嵌入式软件中,进入main()函数之前BSS段被C运行时系统映射到初始化为全零的内存(效率较高)。

    注意,尽管均放置于BSS段,但初值为0的全局变量是强符号,而未初始化的全局变量是弱符号。若其他地方已定义同名的强符号(初值可能非0),则弱符号与之链接时不会引起重定义错误,但运行时的初值可能并非期望值(会被强符号覆盖)。因此,定义全局变量时,若只有本文件使用,则尽量使用static关键字修饰;否则需要为全局变量定义赋初值(哪怕0值),保证该变量为强符号,以便链接时发现变量名冲突,而不是被未知值覆盖。

    某些编译器将未初始化的全局变量保存在common段,链接时再将其放入BSS段。在编译阶段可通过-fno-common选项来禁止将未初始化的全局变量放入common段。

    此外,由于目标文件不含BSS段,故程序烧入存储器(Flash)后BSS段地址空间内容未知。U-Boot启动过程中,将U-Boot的Stage2代码(通常位于lib_xxxx/board.c文件)搬迁(拷贝)到SDRAM空间后必须人为添加清零BSS段的代码,而不可依赖于Stage2代码中变量定义时赋0值。

    【扩展阅读】BSS历史

    BSS(Block Started by Symbol,以符号开始的块)一词最初是UA-SAP汇编器(United Aircraft Symbolic Assembly Program)中的伪指令,用于为符号预留一块内存空间。该汇编器由美国联合航空公司于20世纪50年代中期为IBM 704大型机所开发。

    后来该词被作为关键字引入到了IBM 709和7090/94机型上的标准汇编器FAP(Fortran Assembly Program),用于定义符号并且为该符号预留指定字数的未初始化空间块。

    在采用段式内存管理的架构中(如Intel 80x86系统),BSS段通常指用来存放程序中未初始化全局变量的一块内存区域,该段变量只有名称和大小却没有值。程序开始时由系统初始化清零。

    BSS段不包含数据,仅维护开始和结束地址,以便内存能在运行时被有效地清零。BSS所需的运行时空间由目标文件记录,但BSS并不占用目标文件内的实际空间,即BSS节段应用程序的二进制映象文件中并不存在。

    6 数据段(Data)

    数据段通常用于存放程序中已初始化且初值不为0的全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),可读可写。

    数据段保存在目标文件中(在嵌入式系统里一般固化在镜像文件中),其内容由程序初始化。例如,对于全局变量int gVar = 10,必须在目标文件数据段中保存10这个数据,然后在程序加载时复制到相应的内存。

    数据段与BSS段的区别如下:

    1. BSS段不占用物理文件尺寸,但占用内存空间;数据段占用物理文件,也占用内存空间。

      对于大型数组如int ar0[10000] = {1, 2, 3, …}和int ar1[10000],ar1放在BSS段,只记录共有10000*4个字节需要初始化为0,而不是像ar0那样记录每个数据1、2、3…,此时BSS为目标文件所节省的磁盘空间相当可观。

    2. 当程序读取数据段的数据时,系统会出发缺页故障,从而分配相应的物理内存;当程序读取BSS段的数据时,内核会将其转到一个全零页面,不会发生缺页故障,也不会为其分配相应的物理内存。

      运行时数据段和BSS段的整个区段通常称为数据区。某些资料中“数据段”指代数据段 + BSS段 + 堆。

    7 代码段(text)

    代码段也称正文段或文本段,通常用于存放程序执行代码(即CPU执行的机器指令)。一般C语言执行语句都编译成机器代码保存在代码段。通常代码段是可共享的,因此频繁执行的程序只需要在内存中拥有一份拷贝即可。代码段通常属于只读,以防止其他程序意外地修改其指令(对该段的写操作将导致段错误)。某些架构也允许代码段为可写,即允许修改程序。

    代码段指令根据程序设计流程依次执行,对于顺序指令,只会执行一次(每个进程);若有反复,则需使用跳转指令;若进行递归,则需要借助栈来实现。

    代码段指令中包括操作码和操作对象(或对象地址引用)。若操作对象是立即数(具体数值),将直接包含在代码中;若是局部数据,将在栈区分配空间,然后引用该数据地址;若位于BSS段和数据段,同样引用该数据地址。

    代码段最容易受优化措施影响。

    8 保留区

    位于虚拟地址空间的最低部分,未赋予物理地址。任何对它的引用都是非法的,用于捕捉使用空指针和小整型值指针引用内存的异常情况。

    它并不是一个单一的内存区域,而是对地址空间中受到操作系统保护而禁止用户进程访问的地址区域的总称。大多数操作系统中,极小的地址通常都是不允许访问的,如NULL。C语言将无效指针赋值为0也是出于这种考虑,因为0地址上正常情况下不会存放有效的可访问数据。

    在32位X86架构的Linux系统中,用户进程可执行程序一般从虚拟地址空间0x08048000开始加载。该加载地址由ELF文件头决定,可通过自定义链接器脚本覆盖链接器默认配置,进而修改加载地址。0x08048000以下的地址空间通常由C动态链接库、动态加载器ld.so和内核VDSO(内核提供的虚拟共享库)等占用。通过使用mmap系统调用,可访问0x08048000以下的地址空间。

    通过cat /proc/self/maps命令查看加载表如下:

    在这里插入图片描述
    【扩展阅读】分段的好处

    进程运行过程中,代码指令根据流程依次执行,只需访问一次(当然跳转和递归可能使代码执行多次);而数据(数据段和BSS段)通常需要访问多次,因此单独开辟空间以方便访问和节约空间。具体解释如下:

    当程序被装载后,数据和指令分别映射到两个虚存区域。数据区对于进程而言可读写,而指令区对于进程只读。两区的权限可分别设置为可读写和只读。以防止程序指令被有意或无意地改写。

    现代CPU具有极为强大的缓存(Cache)体系,程序必须尽量提高缓存命中率。指令区和数据区的分离有利于提高程序的局部性。现代CPU一般数据缓存和指令缓存分离,故程序的指令和数据分开存放有利于提高CPU缓存命中率。

    当系统中运行多个该程序的副本时,其指令相同,故内存中只须保存一份该程序的指令部分。若系统中运行数百进程,通过共享指令将节省大量空间(尤其对于有动态链接的系统)。其他只读数据如程序里的图标、图片、文本等资源也可共享。而每个副本进程的数据区域不同,它们是进程私有的。

    此外,临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。全局数据和静态数据可能在整个程序执行过程中都需要访问,因此单独存储管理。堆区由用户自由分配,以便管理。

    二:Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈

    Linux 中有几种栈?各种栈的内存位置?
    介绍完栈的工作原理和用途作用后,我们回归到Linux 内核上来。内核将栈分成四种:

    • 进程栈
    • 线程栈
    • 内核栈
    • 中断栈

    一、进程栈

    进程栈是属于用户态栈,和进程 虚拟地址空间(Virtual Address Space)密切相关。那我们先了解下什么是虚拟地址空间:在 32 位机器下,虚拟地址空间大小为 4G。这些虚拟地址通过页表 (Page Table) 映射到物理内存,页表由操作系统维护,并被处理器的内存管理单元 (MMU) 硬件引用。每个进程都拥有一套属于它自己的页表,因此对于每个进程而言都好像独享了整个虚拟地址空间。

    Linux内核将这4G字节的空间分为两部分,将最高的 1G字节(0xC0000000-0xFFFFFFFF)供内核使用,称为内核空间。而将较低的3G字节(0x00000000-0xBFFFFFFF)供各个进程使用,称为 用户空间。每个进程可以通过系统调用陷入内核态,因此内核空间是由所有进程共享的。虽然说内核和用户态进程占用了这么大地址空间,但是并不意味它们使用了这么多物理内存,仅表示它可以支配这么大的地址空间。它们是根据需要,将物理内存映射到虚拟地址空间中使用。
    在这里插入图片描述
    Linux对进程地址空间有个标准布局,地址空间中由各个不同的内存段组成 (Memory Segment),主要的内存段如下:

    • 程序段 (Text Segment):可执行文件代码的内存映射
    • 数据段 (Data Segment):可执行文件的已初始化全局变量的内存映射
    • BSS段 (BSS Segment):未初始化的全局变量或者静态变量(用零页初始化)
    • 堆区 (Heap) : 存储动态内存分配,匿名的内存映射
    • 栈区 (Stack) : 进程用户空间栈,由编译器自动分配释放,存放函数的参数值、局部变量的值等
    • 映射段(Memory Mapping Segment):任何内存映射文件

    在这里插入图片描述
    而上面进程虚拟地址空间中的栈区,正指的是我们所说的进程栈。进程栈的初始化大小是由编译器和链接器计算出来的,但是栈的实时大小并不是固定的,Linux 内核会根据入栈情况对栈区进行动态增长(其实也就是添加新的页表)。但是并不是说栈区可以无限增长,它也有最大限制 RLIMIT_STACK(一般为 8M),我们可以通过ulimit来查看或更改 RLIMIT_STACK 的值。

    【扩展阅读】:如何确认进程栈的大小
    我们要知道栈的大小,那必须得知道栈的起始地址和结束地址。栈起始地址 获取很简单,只需要嵌入汇编指令获取栈指针 esp 地址即可。栈结束地址 的获取有点麻烦,我们需要先利用递归函数把栈搞溢出了,然后再GDB 中把栈溢出的时候把栈指针 esp 打印出来即可。代码如下:

    /* file name: stacksize.c */
    
    void *orig_stack_pointer;
    
    void blow_stack() {
        blow_stack();
    }
    
    int main() {
        __asm__("movl %esp, orig_stack_pointer");
    
        blow_stack();
        return 0;
    }
    
    $ g++ -g stacksize.c -o ./stacksize
    $ gdb ./stacksize
    (gdb) r
    Starting program: /home/home/misc-code/setrlimit
    
    Program received signal SIGSEGV, Segmentation fault.
    blow_stack () at setrlimit.c:4
    4       blow_stack();
    (gdb) print (void *)$esp
    $1 = (void *) 0xffffffffff7ff000
    (gdb) print (void *)orig_stack_pointer
    $2 = (void *) 0xffffc800
    (gdb) print 0xffffc800-0xff7ff000
    $3 = 8378368    // Current Process Stack Size is 8M
    

    上面对进程的地址空间有个比较全局的介绍,那我们看下Linux内核中是怎么体现上面内存布局的。
    内核使用内存描述符来表示进程的地址空间,该描述符表示着进程所有地址空间的信息。
    内存描述符由 mm_struct结构体表示,下面给出内存描述符结构中各个域的描述,请大家结合前面的 进程内存段布局 图一起看:

    struct mm_struct {
        struct vm_area_struct *mmap;           /* 内存区域链表 */
        struct rb_root mm_rb;                  /* VMA 形成的红黑树 */
        ...
        struct list_head mmlist;               /* 所有 mm_struct 形成的链表 */
        ...
        unsigned long total_vm;                /* 全部页面数目 */
        unsigned long locked_vm;               /* 上锁的页面数据 */
        unsigned long pinned_vm;               /* Refcount permanently increased */
        unsigned long shared_vm;               /* 共享页面数目 Shared pages (files) */
        unsigned long exec_vm;                 /* 可执行页面数目 VM_EXEC & ~VM_WRITE */
        unsigned long stack_vm;                /* 栈区页面数目 VM_GROWSUP/DOWN */
        unsigned long def_flags;
        unsigned long start_code, end_code, start_data, end_data;    /* 代码段、数据段 起始地址和结束地址 */
        unsigned long start_brk, brk, start_stack;                   /* 栈区 的起始地址,堆区 起始地址和结束地址 */
        unsigned long arg_start, arg_end, env_start, env_end;        /* 命令行参数 和 环境变量的 起始地址和结束地址 */
        ...
        /* Architecture-specific MM context */
        mm_context_t context;                  /* 体系结构特殊数据 */
    
        /* Must use atomic bitops to access the bits */
        unsigned long flags;                   /* 状态标志位 */
        ...
        /* Coredumping and NUMA and HugePage 相关结构体 */
    };
    

    在这里插入图片描述
    【扩展阅读】:进程栈的动态增长实现

    进程在运行的过程中,通过不断向栈区压入数据,当超出栈区容量时,就会耗尽栈所对应的内存区域,这将触发一个 缺页异常 (page fault)
    通过异常陷入内核态后,异常会被内核的 expand_stack()函数处理,进而调用 acct_stack_growth()来检查是否还有合适的地方用于栈的增长。

    如果栈的大小低于RLIMIT_STACK(通常为8MB),那么一般情况下栈会被加长,程序继续执行,感觉不到发生了什么事情,这是一种将栈扩展到所需大小的常规机制。

    然而,如果达到了最大栈空间的大小,就会发生 栈溢出(stack overflow),进程将会收到内核发出的 段错误(segmentation fault) 信号。

    动态栈增长是唯一一种访问未映射内存区域而被允许的情形,其他任何对未映射内存区域的访问都会触发页错误,从而导致段错误。一些被映射的区域是只读的,因此企图写这些区域也会导致段错误。

    二、线程栈

    Linux 内核的角度来说,其实它并没有线程的概念。
    Linux把所有线程都当做进程来实现,它将线程和进程不加区分的统一到了 task_struct中。

    线程仅仅被视为一个与其他进程共享某些资源的进程,而是否共享地址空间几乎是进程和 Linux中所谓线程的唯一区别。
    线程创建的时候,加上了 CLONE_VM标记,这样 线程的内存描述符 将直接指向 父进程的内存描述符。

     if (clone_flags & CLONE_VM) {
        /*
         * current 是父进程而 tsk 在 fork() 执行期间是共享子进程
         */
        atomic_inc(&current->mm->mm_users);
        tsk->mm = current->mm;
      }
    

    虽然线程的地址空间和进程一样,但是对待其地址空间的stack还是有些区别的。
    对于Linux 进程或者说主线程,其 stack 是在fork的时候生成的,实际上就是复制了父亲的 stack 空间地址,然后写时拷贝 (cow) 以及动态增长。
    然而对于主线程生成的子线程而言,其stack 将不再是这样的了,而是事先固定下来的,使用 mmap 系统调用,它不带有VM_STACK_FLAGS 标记。
    这个可以从 glibcnptl/allocatestack.c中的 allocate_stack()函数中看到:

    mem = mmap (NULL, size, prot,
                MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
    

    由于线程的 mm->start_stack 栈地址和所属进程相同,所以线程栈的起始地址并没有存放在 task_struct中,应该是使用 pthread_attr_t中的stackaddr来初始化 task_struct->thread->spsp 指向struct pt_regs 对象,该结构体用于保存用户进程或者线程的寄存器现场)。
    这些都不重要,重要的是,线程栈不能动态增长,一旦用尽就没了,这是和生成进程的 fork不同的地方。
    由于线程栈是从进程的地址空间中 map 出来的一块内存区域,原则上是线程私有的。但是同一个进程的所有线程生成的时候浅拷贝生成者的 task_struct的很多字段,其中包括所有的vma,如果愿意,其它线程也还是可以访问到的,于是一定要注意。

    三、进程内核栈

    在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先进程用户空间中的栈,而是一个单独内核空间的栈,这个称作进程内核栈。进程内核栈在进程创建的时候,通过 slab 分配器从 thread_info_cache缓存池中分配出来,其大小为 THREAD_SIZE,一般来说是一个页大小 4K

    union thread_union {                                   
            struct thread_info thread_info;                
            unsigned long stack[THREAD_SIZE/sizeof(long)];
    };   
    

    thread_union 进程内核栈 和 task_struct 进程描述符有着紧密的联系。由于内核经常要访问 task_struct,高效获取当前进程的描述符是一件非常重要的事情。因此内核将进程内核栈的头部一段空间,用于存放 thread_info 结构体,而此结构体中则记录了对应进程的描述符,两者关系如下图(对应内核函数为 dup_task_struct()):
    在这里插入图片描述
    有了上述关联结构后,内核可以先获取到栈顶指针 esp,然后通过 esp 来获取 thread_info。这里有一个小技巧,直接将 esp 的地址与上 ~(THREAD_SIZE - 1) 后即可直接获得 thread_info 的地址。由于 thread_union 结构体是从thread_info_cache 的 Slab 缓存池中申请出来的,而 thread_info_cache 在 kmem_cache_create 创建的时候,保证了地址是 THREAD_SIZE 对齐的。因此只需要对栈指针进行 THREAD_SIZE 对齐,即可获得 thread_union 的地址,也就获得了 thread_union 的地址。成功获取到 thread_info 后,直接取出它的 task 成员就成功得到了task_struct。其实上面这段描述,也就是 current 宏的实现方法:

    register unsigned long current_stack_pointer asm ("sp");
    
    static inline struct thread_info *current_thread_info(void)  
    {                                                            
            return (struct thread_info *)                        
                    (current_stack_pointer & ~(THREAD_SIZE - 1));
    }                                                            
    
    #define get_current() (current_thread_info()->task)
    
    #define current get_current()             
    

    四、中断栈

    进程陷入内核态的时候,需要内核栈来支持内核函数调用。中断也是如此,当系统收到中断事件后,进行中断处理的时候,也需要中断栈来支持函数调用。由于系统中断的时候,系统当然是处于内核态的,所以中断栈是可以和内核栈共享的。但是具体是否共享,这和具体处理架构密切相关。

    X86 上中断栈就是独立于内核栈的;独立的中断栈所在内存空间的分配发生在 arch/x86/kernel/irq_32.c 的irq_ctx_init() 函数中(如果是多处理器系统,那么每个处理器都会有一个独立的中断栈),函数使用 __alloc_pages在低端内存区分配 2个物理页面,也就是8KB大小的空间。有趣的是,这个函数还会为 softirq 分配一个同样大小的独立堆栈。如此说来,softirq 将不会在 hardirq 的中断栈上执行,而是在自己的上下文中执行。

    在这里插入图片描述
    ARM 上中断栈和内核栈则是共享的;中断栈和内核栈共享有一个负面因素,如果中断发生嵌套,可能会造成栈溢出,从而可能会破坏到内核栈的一些重要数据,所以栈空间有时候难免会捉襟见肘。

    Linux 为什么需要区分这些栈?
    为什么需要区分这些栈,其实都是设计上的问题。这里就我看到过的一些观点进行汇总,供大家讨论:

    1.为什么需要单独的进程内核栈?

    所有进程运行的时候,都可能通过系统调用陷入内核态继续执行。假设第一个进程 A陷入内核态执行的时候,需要等待读取网卡的数据,主动调用schedule()让出CPU;此时调度器唤醒了另一个进程B,碰巧进程 B 也需要系统调用进入内核态。

    那问题就来了,如果内核栈只有一个,那进程B进入内核态的时候产生的压栈操作,必然会破坏掉进程 A 已有的内核栈数据;一但进程 A的内核栈数据被破坏,很可能导致进程 A的内核态无法正确返回到对应的用户态了;

    2.为什么需要单独的线程栈?

    Linux调度程序中并没有区分线程和进程,当调度程序需要唤醒”进程”的时候,必然需要恢复进程的上下文环境,也就是进程栈;但是线程和父进程完全共享一份地址空间,如果栈也用同一个那就会遇到以下问题。假如进程的栈指针初始值为0x7ffc80000000;父进程 A 先执行,调用了一些函数后栈指针esp0x7ffc8000FF00,此时父进程主动休眠了;接着调度器唤醒子线程 A1
    此时A1的栈指针 esp如果为初始值0x7ffc80000000,则线程 A1 一但出现函数调用,必然会破坏父进程 A 已入栈的数据。
    如果此时线程A1 的栈指针和父进程最后更新的值一致,esp0x7ffc8000FF00,那线程 A1 进行一些函数调用后,栈指针 esp 增加到 0x7ffc8000FFFF,然后线程 A1 休眠;调度器再次换成父进程 A 执行,那这个时候父进程的栈指针是应该为 0x7ffc8000FF00 还是 0x7ffc8000FFFF 呢?无论栈指针被设置到哪个值,都会有问题不是吗?

    3.进程和线程是否共享一个内核栈?

    No,线程和进程创建的时候都调用 dup_task_struct来创建 task 相关结构体,而内核栈也是在此函数中 alloc_thread_info_node 出来的。因此虽然线程和进程共享一个地址空间 mm_struct,但是并不共享一个内核栈。

    4.为什么需要单独中断栈?

    这个问题其实不对,ARM 架构就没有独立的中断栈。

    三:自己的总结

    上面的图都很好,但我觉得这张图更形象,32位进程栈大小是8M,理论上堆区最大大小约为2.9G,所以还是蛮大的。

    从上面两篇文章,我知道的线程栈是使用mmap系统调用分配的空间,但是mmap分配的系统空间是什么呢?也就是上图中的mmap区域或者说共享的内存映射区域是什么呢?它的方向是向上生长还是向下生长的?

    下面两幅图给出了答案:

    图一:
    在这里插入图片描述
    图二:
    在这里插入图片描述.
    所以,mmap其实和堆一样,实际上可以说他们都是动态内存分配,但是严格来说mmap区域并不属于堆区,反而和堆区会争用虚拟地址空间。

    这里要提到一个很重要的概念,内存的延迟分配,只有在真正访问一个地址的时候才建立这个地址的物理映射,这是Linux内存管理的基本思想。Linux内核在用户申请内存的时候,只是给它分配了一个线性区(也就是虚拟内存),并没有分配实际物理内存;只有当用户使用这块内存的时候,内核才会分配具体的物理页面给用户,这时候才占用宝贵的物理内存。内核释放物理页面是通过释放先行区,找到其对应的物理页面,将其全部释放的过程。

    这篇文章关于mmap生长方向说的也挺详细的: 进程地址空间的布局(整理)

    最后还有一个mmap机制的源代码分析博客,我水平暂时不够,只能看懂意思,待日后阅读内核源码再来回顾一遍:Linux用户空间线程管理介绍之二:创建线程堆栈。

    展开全文
  • 进程相关的数据结构(页表内核栈、task) 物理内存 内核代码数据 用户栈 共享库的内存映射 运行时堆 未初始化数据段.bss 已初始化数据段.data 代码段.text 2. 进程堆栈大小 32位Windows进程栈默认1M vs的...

    extern “C”的作用详解

    • 作用

      • 为能正确实现C++代码调用其他C语言代码。
    • 加extern "C"后,

      • 指示编译器这部分代码按C(而不是C++)方式编译
    • C++支持函数重载

      • 编译器编译函数的过程中会将函数的参数类型也加到编译后的代码
    • C不支持函

      • 编译C语言代码的函数时只包括函数名。
    • C++出现以前,很多代码都是C语言写的,

      • 很底层的库也是C写的
      • 为更好支持原来的C代码和已经写好的C语言库,
      • 需在C++中尽可能的支持C
      • extern "C"就是一个策略。
    • 用在下面情况

    • C++代码调用C语言代码

    • 在C++的头文件中用

    • 多人协同时

      • 有人擅长C,
      • 有人C++,
      • 这样的情况下也会用

    例子:

    • moduleA、moduleB两模块
    • B调A中代码
    • A是用C实现
    • B用C++实现
    //moduleA头文件
    #ifndef __MODULE_A_H //宏是为了防止头文件的重复引用
    #define __MODULE_A_H
    int fun(int, int);
    #endif
    
    //moduleA实现文件moduleA.C 
    #include"moduleA"
    int fun(int a, int b)
    {
    return a+b;
    }
    
    //moduleB头文件
    #idndef __MODULE_B_H //很明显这一部分也是为了防止重复引用
    #define __MODULE_B_H
    #ifdef __cplusplus //而这一部分就是告诉编译器,如果定义了__cplusplus(即如果是cpp文件, 
    extern "C"{ //因为cpp文件默认定义了该宏),则采用C语言方式进行编译
    #include"moduleA.h"
    #endif//其他代码
     
    #ifdef __cplusplus
    }
    #endif
    #endif
    
    //moduleB.cpp //B模块的实现也没有改变,只是头文件的设计变化了
    #include"moduleB.h"
    int main()
    {
      cout<<fun(2,3)<<endl;
    }
    
    • void fun(int, int),
      • 编译后_fun_int_int(不同编译器可能不同,但都采用类似机制,用函数名和参数类型来命名编译后的函数名)
      • C没有重载机制
      • 一般是利用函数名来指明编译后的函数名的,
        • 上面的函数可能会是_fun

    • 为什么标准头文件都有类似的结构?

    #ifndef __INCvxWorksh /*防止该头文件被重复引用*/
    #define __INCvxWorksh
    #ifdef __cplusplus             //告诉编译器,这部分代码按C语言的格式进行编译,而不是C++的
    extern "C"{
    #endif
     
    /*…*/
     
    #ifdef __cplusplus
    }
     
    #endif
    #endif /*end of __INCvxWorksh*/
    
    • extern "C"双含义

      • 首先,被它修饰的目标是"extern"的;
      • 其次,被它修饰的目标代码是"C"的。
    • 被extern "C"限定的函数或变量是extern类型

    • extern是C/C++中表明函数和全局变量的作用范围的关键字,

    • 该关键字告诉编译器,其申明的函数和变量可以在本模块或其他模块中使用

    • extern int a; 仅是一个变量的声明,其并不是在定义变量a,也并未为a分配空间

    • 变量a在所有模块中作为一种全局变量只能被定义一次,否则会出错

    • 在模块的头文件中对本模块提供给其他模块引用的函数和全局变量以关键字extern声明

    • 如果模块B要引用模块A中定义的全局变量和函数时只需包含模块A的头文件

    • 模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但不报错;

    • 链接阶段从模块A编译生成的目标代码中找到该函数。

    • extern对应的关键字是static,static表明变量或者函数只能在本模块中用,被static修饰的变量或者函数不可能被extern C修饰。

    • 被extern "C"修饰的变量和函数是按照C语言方式进行编译和链接的:很重要

    • 由于C++支持函数重载,而C语言不支持,因此函数被C++编译后在符号库中的名字是与C语言不同的;

    • C++编译后的函数需要加上参数的类型才能唯一标定重载后的函数,而加上extern "C"后,是为了向编译器指明这段代码按照C语言的方式进行编译

    • 未加extern "C"声明时的链接方式:

    • 未加extern "C"声明时的链接方式:
    //模块A头文件 moduleA.h
    #idndef _MODULE_A_H
    #define _MODULE_A_H
     
    int foo(int x, int y);
    #endif
    
    在模块B中调用该函数
    //模块B实现文件 moduleB.cpp
    #include"moduleA.h"
    foo(2,3);
    

    • 链接阶段,链接器会从模块A生成的目标文件moduleA.obj中找_foo_int_int符号,不可能找到的,
    • foo()函数被编译成了_foo的符号,因此会出现链接错误。

    extern "C"总结

    • 1,可以是如下的单一语句:
    • extern “C” double sqrt(double);

    • 2,相当于复合语句中的声明都加了extern “C”
    extern "C"
    {
          double sqrt(double);
          int min(int, int);
    }
    

    • 3,可包含头文件,相当于头文件中的声明都加了extern “C”
    extern "C"
    {
        #include <cmath>
    }
    ``` 
    
    
    
    - 不可将extern "C" 添加在函数内部
    
    - 如果函数有多个声明,可以都加extern "C", 也可以只出现在第一次声明中,后面的声明会接受第一个链接指示符的规则。
    除extern "C", 还有extern "FORTRAN" 等。
    
    

    canci

    “free store” VS “heap”

    • C++内存布局
    • C++中,内存区分5
      • 堆、栈、自由存储区、全局/静态存储区、常量存储区”

    • 自由存储区与堆区别
    • “malloc在堆上分配的内存块,free释放内存,
    • new申请的内存则是在自由存储区,用delete来释放

    • 自由存储区与堆是两块不同的内存区域吗?
      • 可能相同吗?
    • 很多博客划分自由存储区与堆的分界线就是
      • new/delete与malloc/free。
    • C++标准没有要求
      • 但很多编译器的new/delete都以malloc/free为基础来实现
    • 借以malloc实现的new,所申请的内存是在堆上还是在自由存储区上?
    • 堆是操作系统维护的一块特殊内存,
      • 提供动态分配功能
      • 运行程序调malloc()时就从中分配,稍后调用free把内存交还
    • 自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区
    • 基本C++编译器默认用堆来实现自由存储
      • 即缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现
      • 这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确
      • 但也可通过重载操作符,改用其他内存来实现自由存储
      • 如全局变量做的对象池,这时自由存储区就区别于堆

    • 堆是操作系统维护的一块内存
    • 自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。
    • 堆与自由存储区并不等价。

    问题的来源

    • 这个问题的起源在哪里。
    • 最先用C时,并没有这样的争议,
    • 很明确地知道malloc/free是在堆上进行内存操作。
    • 直到Bjarne Stroustrup的书籍中数次看到free store (自由存储区),说实话,我一直把自由存储区等价于堆。
    • Herb Sutter的《exceptional C++》中,指出free store(自由存储区) 与 heap有区别。
    • 自由存储区与堆是否等价的问题讨论,大概就是从这里开始的:
    The free store is one of the two dynamic memory areas, allocated/freed by new/delete. 
    Object lifetime can be less than the time the storage is allocated;
    free store objects can have memory allocated without being immediately initialized, and can be destroyed without the memory being immediately deallocated. During the period when the storage is allocated but outside the object's lifetime, the storage may be accessed and manipulated through a void* but none of the proto-object's nonstatic members or member functions may be accessed, have their addresses taken, or be otherwise manipulated.
    
    
    The heap is the other dynamic memory area, allocated/freed by malloc/free and their variants. 
    while the default global new and delete might be implemented in terms of malloc and free by a particular compiler, the heap is not the same as free store and memory allocated in one area cannot be safely deallocated in the other. Memory allocated from the heap can be used for objects of class type by placement-new construction and explicit destruction. If so used, the notes about free store object lifetime apply similarly here
    
    • 作者指出,
    • 把堆与自由存储区要分开来,是因为在C++标准草案中关于这两种区域是否有联系的问题一直很谨慎地没有给予详细说明,而且特定情况下new和delete是按照malloc和free来实现,
    • 或者说是放过来malloc和free是按照new和delete来实现的也没有定论。
    • 这两种内存区域的运作方式不同、访问方式不同,所以应该被当成不一样的东西来使用。

    结论

    • 自由存储是C++中通过new与delete动态分配和释放对象的抽象概念,而堆(heap)是C语言和操作系统的术语,是操作系统维护的一块动态分配内存。

    • new所申请的内存区域在C++中称为自由存储区。

      • 藉由堆实现的自由存储,可以说new所申请的内存区域在堆上。
    • 堆与自由存储区还是有区别的,它们并非等价。

    • 你来自C,没接触过C++;

    • 或你一开始就熟悉C++的自由储存概念,

      • 从没听说过C的malloc,
      • 就不会陷入“自由存储区与堆好像一样,好像又不同”这样的迷惑之中。
      • 这就像Bjarne Stroustrup所说的:
        usually because they come from a different language background.

    canci

    修改线程默认栈空间大小

    • ulimit -s 查看linux的默认栈空间大小,
      • 默认 为10240 即10M
    • ulimit -s 设置大小值临时改变栈空间大小:
      • ulimit -s 102400, 即修改为100M
    • /etc/rc.local 内加入 ulimit -s 102400
      • 开机就设置栈空间大小
    • /etc/security/limits.conf 中也可以改变栈空间大小:
      #
    • soft stack 102400
      重新登录,执行ulimit -s 即可看到改为102400 即100M

    Why does Linux have a default stack size soft limit of 8 MB?

    • protect the OS
    • Programs that have a legitimate reason to need more stack are rare.
    • sometimes said mistakes lead to code that gets stuck in an infinite loop.
    • if that infinite loop happens to contain a recursive function call,
      • the stack would quickly eat all the available memory.
    • soft limit on the stack size prevents this:
      • the program will crash but the rest of the OS will be unaffected.
    • this is only a soft limit,
      • you can actually modify it from within your program (see setrlimit(2): get/set resource limits) if you really need to.

    进程空间分配和堆栈大小

    1. Linux中进程空间

    • 进程的空间分配:
    • 与进程相关的数据结构(页表、内核栈、task)
    • 物理内存
    • 内核代码和数据
    • 用户栈
    • 共享库的内存映射
    • 运行时堆
    • 未初始化数据段.bss
    • 已初始化数据段.data
    • 代码段.text

    2. 进程堆栈大小

    • 32位Windows,进程栈默认1M
      • vs的编译属性可修改程序运行时进程栈大小
    • Linux下进程栈默认10M
      • ulimit -s查看并修改默认栈大小
      • 默认一个线程要预留1M左右栈
      • 进程中有N个线程时,Windows下大概有N*M栈大小
    • 堆大小理论上大概=进程虚拟空间大小-内核虚拟内存大小。
    • windows
      • 进程高位2G留给内核,低2G给用户
        • 进程堆大小小于2G。
    • Linux下,进程高1G给内核,
      • 低3G给用户,
      • 所以进程堆大小小于3G。

    3. 进程的最大线程数:

    • 32位windows,一个进程空间4G,内核占2G,留给用户只有2G,
    • 一个线程默认栈1M,
    • 所以一个进程最大开2048个线程。
    • 当然内存不会完全拿来做线程的栈,所以最大线程数实际值要小于2048,大概2000个。
    • 32位Linux下,用户留3G,
      • 一个线程默认8M,
      • 最多380个左右线程。
      • (ps:ulimit -a 查看电脑的最大进程数,大概7000多个)

    canci

    展开全文
  • Linux进程空间分配

    2020-12-09 15:23:03
    从上图可以看出,进程的空间分配:与进程相关的数据结构(页表内核栈、task) ---> 物理内存 ---> 内核代码数据 ---> 用户栈 ---> 共享库的内存映射区 ---> 运行时堆 --->未初始化数据段.bss ...

    1. Linux中进程空间的分配情况如下:

    从上图可以看出,进程的空间分配:与进程相关的数据结构(页表、内核栈、task) ---> 物理内存 ---> 内核代码和数据 ---> 用户栈 ---> 共享库的内存映射区 ---> 运行时堆 --->未初始化数据段.bss ---> 已初始化数据段.data ---> 代码段.text

    2. 进程的堆栈大小:

    • 32位Windows,一个进程栈的默认大小是1M,在vs的编译属性可以修改程序运行时进程的栈大小。
    • Linux下进程栈的默认大小是10M,可以通过 ulimit -s查看并修改默认栈大小。
    • 默认一个线程要预留1M左右的栈大小,所以进程中有N个线程时,Windows下大概有N*M的栈大小。
    • 堆的大小理论上大概等于进程虚拟空间大小-内核虚拟内存大小。windows下,进程的高位2G留给内核,低位2G留给用户,所以进程堆的大小小于2G。Linux下,进程的高位1G留给内核,低位3G留给用户,所以进程堆大小小于3G。

    3. 进程的最大线程数:

    • 32位windows下,一个进程空间4G,内核占2G,留给用户只有2G,一个线程默认栈是1M,所以一个进程最大开2048个线程。当然内存不会完全拿来做线程的栈,所以最大线程数实际值要小于2048,大概2000个。
    • 32位Linux下,一个进程空间4G,内核占1G,用户留3G,一个线程默认8M,所以最多380个左右线程。(ps:ulimit -a 查看电脑的最大进程数,大概7000多个)
    展开全文
  • Linux内核源码+电子书

    热门讨论 2011-02-21 15:13:10
    4.1 进程和程序(Process and Program) 4.2 Linux中的进程概述 4.3 task_struct结构描述 4.4 task_struct结构在内存中的存放 4.4.1 进程内核栈 4.4.2 当前进程(current宏) 4.5 进程组织的方式 4.5.1哈希表...
  • 4.1 进程和程序(Process and Program) 4.2 Linux中的进程概述 4.3 task_struct结构描述 4.4 task_struct结构在内存中的存放 4.4.1 进程内核栈 4.4.2 当前进程(current宏) 4.5 进程组织的方式 4.5.1哈希表...
  • 另一种读写进程内存空间的方法

    千次阅读 2012-07-14 12:57:09
    内存空间不能跨进程访问的原因主要在于不同进程都有自己的页目录和页表进程切换的很大一块也就是切换掉页目录。   Windows自己的ReadProcessMemory最终也是通过KeStackAttachProcess附加到目标进程空间执行...
  • 为了实现系统中每个进程都有一个私有的虚拟地址空间,系统为每一个进程都创建一个页目录( Directory)一组页表。每个进程页表是独立的,而内核空间的页表是所有进程共享的。 在x86平台上,CR3寄存器标识了页目录...
  • 在Linux系统中, 内核进程和用户进程所占的虚拟内存比例是1:3,而Windows系统为2:2(通过设置Large-Address-Aware Executables标志也可为1:3)。这并不意味着内核使用那么多物理内存,仅表示它可支配这部分地址空间,...
  • 内存管理1)内核模式与用户模式地址 图示 地址空间中用户模式部分和内核模式部分每个用户模式进程都有自己的地址上下文,它把用户模式的虚拟地址映射成一组唯一的物理页帧。这意味着,当Windows NT调度器把控制从一个...
  • 内存管理 1)内核模式与用户模式地址 图示 地址空间中用户模式部分和内核模式部分 每个用户模式进程都有自己的地址上下文,它把用户模式的虚拟地址映射成一组唯一的物理页帧。这意味着,当Windows NT调度器把控制从...
  • 本书是著名的操作系统内核专家Mark RussinovichDavid Solomon撰写的Windows操作系统原理的最新版著作,全面深入地阐述了Windows操作系统的整体结构以及内部工作细节。本书针对Windows Server 2003、Windows XP...
  • 本书是著名的操作系统内核专家Mark RussinovichDavid Solomon撰写的Windows操作系统原理的最新版著作,全面深入地阐述了Windows操作系统的整体结构以及内部工作细节。本书针对Windows Server 2003、Windows XP...
  • 主板芯片组内存映射

    千次阅读 2020-06-18 14:02:06
    内存管理是操作系统的核心。 这对于编程系统管理都至关重要。...这些虚拟地址通过页表映射到物理内存,这些页表由操作系统内核维护并由处理器查询。 每个进程都有自己的一组页表,但是有一个陷阱。 启
  • windows系统下,内核进程和用户进程所占的虚拟内存比例为2:2(也可以修改为1:3); 虚拟地址通过页表映射到物理地址,页表由操作系统维护并被处理器引用。(Linux系统通过对栈、堆、内存映射段的起始地址加上随机偏移...
  • 在Linux系统中, 内核进程和用户进程所占的虚拟内存比例是1:3,而Windows系统为2:2(通过设置Large-Address-Aware Executables标志也可为1:3)。这并不意味着内核使用那么多物理内存,仅表示它可支配这部分地址空间,...
  • 内存管理API

    千次阅读 2010-12-03 21:24:00
    一、进程的内存空间1、实现 为实现系统中每个进程都有一个私有的虚拟地址空间,系统为每个进程都创建了一个页目录一组页表。每个进程页表是独立的,而内核空间中的页表是共享的。 x86平台上,CR3寄存器标识了...
  • 虚拟地址布局

    2021-02-21 19:06:59
    内存管理是操作系统的核心任务;它对程序员系统管理员来说也是至关重要的。在接下来的几篇文章中,我将从实践出发着眼于内存管理,并深入到它的内部结构。虽然这些概念很通用,但...这些虚拟地址是通过内核页表page
  • 每一个进程都有自己的4G虚拟地址空间,一个页表项负责4K的地址空间物理内存的映射,一个页有1024项,所以一个页负责4M...在windows中4G虚拟地址空间分为低2G的用户空间高2G的内核空间,在Linux系统中分为低3G的...
  • 虚拟内存管理: 在多任务操作系统中,每个...在Linux系统中, 内核进程和用户进程所占的虚拟内存比例是1:3,而Windows系统为2:2(通过设置Large-Address-Aware Executables标志也可为1:3)。 虚拟地址通过页表(Page ...
  • 内核空间用户空间2.Linux进程中的标准内存布局2.1栈2.2内存映射段2.3堆2.4数据段代码段参考 内存管理是操作系统的核心,它对于编程系统管理都是至关重要的。在接下来的文章中,我将着眼于实际方面来介绍内存,...

空空如也

空空如也

1 2
收藏数 39
精华内容 15
关键字:

windows内核页表和进程页表