• Windows7 x64 了解堆


    一、前言

      堆对于开发者一般来说是熟悉又陌生的,熟悉是因为我们常常使用new/delete或者malloc/free使用堆,陌生是因为我们基本没有去了解堆的结构。堆在什么地方?怎么申请?怎么释放?系统又是怎么管理堆的呢?

      带着疑问,这两天看了<软件漏洞分析技术>与<漏洞战争>中关于堆的说明,终于对于堆有一点点的了解了。这里记录一下在学习和调试中的一点笔记。

    二、关于堆的基本知识

      1).首先了解空闲双向链表和快速单向链表的概念

      1.空闲双向链表(空表)

      空闲堆块的块首中包含一对重要的指针,这对指针用于将空闲堆块组织成双向链表。按照堆块的大小不同,空表总共被分为128条。

      堆区一开始的堆表区中有一个128项的指针数组,被称作空表索引(Freelist array)。该数组的每一项包括两个指针,用于标识一条空表。

      如图所示,空表索引的第二项(free[1])标识了堆中所有大小为8字节的空闲堆块。之后每个索引项指示的空闲堆块递增8字节。例如free[2]为16字节的空闲堆块,free[3]为24字节的空闲堆块,free[127]为1016字节的空闲堆块。
            空闲堆块的大小 = 索引项(ID) x 8(字节)
      把空闲堆块按照大小的不同链入不同的空表,可以方便堆管理系统高效检索指定大小的空闲堆块。需要注意的是,空表索引的第一项(free[0])所标识的空表相对比较特殊。这条双向链表链入了所有大于等于1024字节的堆块(小于512KB),升序排列。

      

      2.快速单项链表(快表)

      快表是Windows用来加速堆块分配而采用的一种堆表。这里之所以叫做"快表"是因为这类单项链表中从来不会发生堆块合并(其中的空闲块块首被设置为占用态,用来防止堆块合并)

      快表也有128条,组织结构与空表类似,只是其中的堆块按照单项链表组织。

      快表总是被初始化为空,而且每条快表最多只有4个结点,故很快就会被填满。

      

      2)堆块的结构

      堆块分为块首和块身,实际上,我们使用函数申请得到的地址指针都会越过8字节(32位系统)的块首,直接指向数据区(块身)。堆块的大小包括块首在内的,如果申请32字节,实际会分配40字节,8字节的块首+32字节的块身。同时堆块的单位是8字节,不足8字节按8字节分配。堆块分为占用态和空闲态。

      其中空闲态结构为:

      占用态结构为

      空闲态将块首后8个字节用于存放空表指针了。

      在64位系统,块首大小为16字节,按16字节对齐。

      

      3)堆块的分配和释放

      1.堆块分配

      堆块分配可以分为三类:快表分配、普通空表分配和零号空表(free[0]分配)。

      从快表中分配堆块比较简单,包括寻找到大小匹配的空闲堆块、将其状态修改为占用态、把它从堆表中"卸下"、最后返回一个指向堆块快身的指针给程序使用。
      普通空表分配时首先寻找最优的空闲块分配,若失败,则寻找次优的空闲块分配,即最小的能满足要求的空闲块。
      零号空表中按照大小升序链着大小不同的空闲块,故在分配时先从free[0]反向查找最后一个块(即最大块),看能否满足要求,如果满足要求,再正向搜索最小能满足要求的空闲堆块进行分配。
      当空表中无法找到匹配的"最优"堆块时,一个稍大些的块会被用于分配,这种次优分配发生时,会先从大块中按请求的大小精确地"割"出一块进行分配,然后给剩下的部分重新标注块首,链入空表。
      由于快表只有在精确匹配才会分配,所以不存在上述现象。

      

      2.堆块的释放
      释放堆块的操作包括将堆块状态改为空闲,链入相应的堆表。所有的释放块都链入堆表的末尾,分配的时候也先从堆表末尾拿。
      另外需要强调,快表最多只有4项。

      3.堆块的分配和释放

      在具体进行堆块分配和释放时,根据操作内存大小不同,Windows采取的策略也会有所不同。可以把内存按照大小分为三类:

          小块:Size < 1KB
          大块:1KB < Size < 512KB
          巨块:Size >= 512KB

                                分配  释放
     小块  

       首先进行快表分配
       若快表分配失败,进行普通空表分配
       若普通空表分配失败,使用堆缓存分配
       若堆缓存分配失败,尝试零号空表分配(freelist[0])
       若零号空表分配失败,进行内存紧缩后在尝试分配
       若仍无法分配,返回NULL

     

    优先链入快表(只能链入4个空闲块)
    如果快表满,则将其链入相应的空表

     大块  

       首先使用堆缓存进行分配
       若堆缓存分配失败,使用free[0]中的大块进行分配

     

    优先将其放入堆缓存
    若堆缓存满,将链入freelist[0]

     巨块  

       一般来说巨块申请非常罕见,要用到虚分配方法(实际上并不是从堆区分配的)
       这种类型的堆块在堆溢出利用中几乎不会遇到

     直接释放,没有堆表操作

      

      在分配的过程中需要注意的几点是:

      (1)快表中的空闲块被设置为占用态,故不会发生堆块合并操作,且只能精确匹配时才会分配。

      (2)快表是单链表,操作比双链表简单,插入删除都少用很多指令

      (3)快表只有4项,很容易被填满,因此空表也是被频繁使用的

     三、调试堆在PEB中的数据结构

      1)完成C代码,在x64下编译为Release版本,运行

    #include "stdafx.h"
    #include <Windows.h>
    #include <iostream>
    using namespace std;
    
    extern "C" PVOID64 _cdecl GetPebx64();
    int _tmain(int argc, _TCHAR* argv[])
    {
        
        PVOID64 Peb = 0;
        Peb = GetPebx64();
        printf("Peb is 0x%p
    ",Peb);
    
        HANDLE hHeap;
        char *heap;
        char str[] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";  //0x20
    
        hHeap = HeapCreate(HEAP_GENERATE_EXCEPTIONS,0x1000,0xffff);
        getchar();   //用于暂停,便于调试器附加
    
        heap = (char*)HeapAlloc(hHeap,0,0x20);
        printf("Heap addr:0x%08p
    ",heap);
    
        strcpy(heap,str);
        printf("str is %s
    ",heap);
    
        cin>>Peb;
        HeapFree(hHeap,0,heap);  //释放
        HeapDestroy(hHeap);
    
        cin>>Peb;
        return 0;
    }

      其中GetPebx64()函数为使用.asm文件的汇编,通过gs:[0x60]获得

    .CODE
      GetPebx64 PROC 
        mov rax,gs:[60h]
      ret
      GetPebx64 ENDP
    END

      运行结果为

      我们使用Windbg附加,查看PEB结构

    0:001> dt _PEB 0x000007FFFFFDB000
    ntdll!_PEB
       +0x000 InheritedAddressSpace : 0 ''
       +0x001 ReadImageFileExecOptions : 0 ''
       +0x002 BeingDebugged    : 0x1 ''
       +0x003 BitField         : 0x8 ''
       +0x003 ImageUsesLargePages : 0y0
       +0x003 IsProtectedProcess : 0y0
       +0x003 IsLegacyProcess  : 0y0
       +0x003 IsImageDynamicallyRelocated : 0y1
       +0x003 SkipPatchingUser32Forwarders : 0y0
       +0x003 SpareBits        : 0y000
       +0x008 Mutant           : 0xffffffff`ffffffff Void
       +0x010 ImageBaseAddress : 0x00000001`3f050000 Void
       +0x018 Ldr              : 0x00000000`77522640 _PEB_LDR_DATA
       +0x020 ProcessParameters : 0x00000000`00242170 _RTL_USER_PROCESS_PARAMETERS
       +0x028 SubSystemData    : (null) 
       +0x030 ProcessHeap      : 0x00000000`00240000 Void        //进程默认堆的地址
       +0x038 FastPebLock      : 0x00000000`7752a960 _RTL_CRITICAL_SECTION
       +0x040 AtlThunkSListPtr : (null) 
       +0x048 IFEOKey          : (null) 
       +0x050 CrossProcessFlags : 0
       +0x050 ProcessInJob     : 0y0
       +0x050 ProcessInitializing : 0y0
       +0x050 ProcessUsingVEH  : 0y0
       +0x050 ProcessUsingVCH  : 0y0
       +0x050 ProcessUsingFTH  : 0y0
       +0x050 ReservedBits0    : 0y000000000000000000000000000 (0)
       +0x058 KernelCallbackTable : (null) 
       +0x058 UserSharedInfoPtr : (null) 
       +0x060 SystemReserved   : [1] 0
       +0x064 AtlThunkSListPtr32 : 0
       +0x068 ApiSetMap        : 0x000007fe`ff710000 Void
       +0x070 TlsExpansionCounter : 0
       +0x078 TlsBitmap        : 0x00000000`77522590 Void
       +0x080 TlsBitmapBits    : [2] 0x11
       +0x088 ReadOnlySharedMemoryBase : 0x00000000`7efe0000 Void
       +0x090 HotpatchInformation : (null) 
       +0x098 ReadOnlyStaticServerData : 0x00000000`7efe0a90  -> (null) 
       +0x0a0 AnsiCodePageData : 0x000007ff`fffa0000 Void
       +0x0a8 OemCodePageData  : 0x000007ff`fffa0000 Void
       +0x0b0 UnicodeCaseTableData : 0x000007ff`fffd0028 Void
       +0x0b8 NumberOfProcessors : 4
       +0x0bc NtGlobalFlag     : 0
       +0x0c0 CriticalSectionTimeout : _LARGE_INTEGER 0xffffe86d`079b8000
       +0x0c8 HeapSegmentReserve : 0x100000             //堆的默认保留大小
       +0x0d0 HeapSegmentCommit : 0x2000                //堆的默认提交大小
       +0x0d8 HeapDeCommitTotalFreeThreshold : 0x10000  //解除提交的总空闲块阈值
       +0x0e0 HeapDeCommitFreeBlockThreshold : 0x1000   //解除提交的单块阈值
       +0x0e8 NumberOfHeaps    : 5                      //进程堆的数量
       +0x0ec MaximumNumberOfHeaps : 0x10               //ProcessHeaps数组目前的大小
       +0x0f0 ProcessHeaps     : 0x00000000`7752a6c0  -> 0x00000000`00240000 Void  //一个数组,记录了每一个堆的地址
       +0x0f8 GdiSharedHandleTable : (null) 
       +0x100 ProcessStarterHelper : (null) 
       +0x108 GdiDCAttributeList : 0
       +0x110 LoaderLock       : 0x00000000`77527490 _RTL_CRITICAL_SECTION
       +0x118 OSMajorVersion   : 6
       +0x11c OSMinorVersion   : 1
       +0x120 OSBuildNumber    : 0x1db1
       +0x122 OSCSDVersion     : 0x100
       +0x124 OSPlatformId     : 2
       +0x128 ImageSubsystem   : 3
       +0x12c ImageSubsystemMajorVersion : 5
       +0x130 ImageSubsystemMinorVersion : 2
       +0x138 ActiveProcessAffinityMask : 0xf
       +0x140 GdiHandleBuffer  : [60] 0
       +0x230 PostProcessInitRoutine : (null) 
       +0x238 TlsExpansionBitmap : 0x00000000`77522580 Void
       +0x240 TlsExpansionBitmapBits : [32] 1
       +0x2c0 SessionId        : 1
       +0x2c8 AppCompatFlags   : _ULARGE_INTEGER 0x0
       +0x2d0 AppCompatFlagsUser : _ULARGE_INTEGER 0x0
       +0x2d8 pShimData        : (null) 
       +0x2e0 AppCompatInfo    : (null) 
       +0x2e8 CSDVersion       : _UNICODE_STRING "Service Pack 1"
       +0x2f8 ActivationContextData : 0x00000000`00140000 _ACTIVATION_CONTEXT_DATA
       +0x300 ProcessAssemblyStorageMap : (null) 
       +0x308 SystemDefaultActivationContextData : 0x00000000`00130000 _ACTIVATION_CONTEXT_DATA
       +0x310 SystemAssemblyStorageMap : (null) 
       +0x318 MinimumStackCommit : 0
       +0x320 FlsCallback      : 0x00000000`0027fe90 _FLS_CALLBACK_INFO
       +0x328 FlsListHead      : _LIST_ENTRY [ 0x00000000`0027fa70 - 0x00000000`0027fa70 ]
       +0x338 FlsBitmap        : 0x00000000`77522570 Void
       +0x340 FlsBitmapBits    : [4] 3
       +0x350 FlsHighIndex     : 1
       +0x358 WerRegistrationData : (null) 
       +0x360 WerShipAssertPtr : (null) 
       +0x368 pContextData     : 0x00000000`00150000 Void
       +0x370 pImageHeaderHash : (null) 
       +0x378 TracingFlags     : 0
       +0x378 HeapTracingEnabled : 0y0
       +0x378 CritSecTracingEnabled : 0y0
       +0x378 SpareTracingBits : 0y000000000000000000000000000000 (0)

      我们可以使用dd 0x00000000`7752a6c0查看进程堆的地址

      也可以使用!heap -h 命令查看进程堆的地址和分配的大小

      而我们可以看到运行结果中分配的地址4A0A90,正好在段4A0000中。

      2)堆的相关结构

      我们首先了解下面几个结构_HEAP_ENTRY,_HEAP_SEGMENT,_HEAP。

      1._HEAP_ENTRY就是块首,下面是一个64位系统堆块的结构,我们在申请得到的地址减去0x10,就可以得到HEAP_ENTRY的首地址。

                      

       2._HEAP_SEGMENT是段结构,我们可以这么认为,堆申请内存的大小是以段为单位的,当新建一个堆的时候,系统会默认为这个堆分配一个段叫0号段,通过刚开始的new和malloc分配的空间都是在这个段上分配的,当这个段用完的时候,如果当初创建堆的时候指明了HEAP_GROWABLE这个标志,那么系统会为这个堆在再分配一个段,这个时候新分配的段就称为1号段了,以下以此类推。每个段的开始初便是HEAP_SEGMENT结构的首地址,由于这个结构也是申请的一块内存,所以它前面也会有个HEAP_ENTRY结构: 

                                                    

      我们使用Windbg查看HEAP_SEGMENT结构如下:

    ntdll!_HEAP_SEGMENT
       +0x000 Entry            : _HEAP_ENTRY
       +0x010 SegmentSignature : Uint4B
       +0x014 SegmentFlags     : Uint4B
       +0x018 SegmentListEntry : _LIST_ENTRY
       +0x028 Heap             : Ptr64 _HEAP    //段所属的堆
       +0x030 BaseAddress      : Ptr64 Void      //段的基地址
       +0x038 NumberOfPages    : Uint4B      //段的内存页数
       +0x040 FirstEntry       : Ptr64 _HEAP_ENTRY  //第一个堆块(HEAP_ENTRY指针,堆块一般位于HEAP_SEGMENT后面)
       +0x048 LastValidEntry   : Ptr64 _HEAP_ENTRY  //堆块的边界值
       +0x050 NumberOfUnCommittedPages : Uint4B    //尚未提交的内存页数
       +0x054 NumberOfUnCommittedRanges : Uint4B    //UnCommittedRanges数组元素数
       +0x058 SegmentAllocatorBackTraceIndex : Uint2B
       +0x05a Reserved         : Uint2B
       +0x060 UCRSegmentList   : _LIST_ENTRY

      

      3._HEAP结构

      HEAP结构则是记录了这个堆的信息,这个结构可以找到HEAP_SEGMENT链表入口,空闲内存链表的入口,内存分配粒度等等信息。HEAP的首地址便是堆句柄的值,但是堆句柄的值又是0号段的首地址也是堆句柄,何解?其实很简单,0号段的HEAP_SEGMENT就在HEAP结构里面,HEAP结构类定义如这样:

    0:001> dt ntdll!_HEAP 4a0000
       +0x000 Entry            : _HEAP_ENTRY
       +0x010 SegmentSignature : 0xffeeffee
       +0x014 SegmentFlags     : 0
       +0x018 SegmentListEntry : _LIST_ENTRY [ 0x00000000`004a0128 - 0x00000000`004a0128 ]
       +0x028 Heap             : 0x00000000`004a0000 _HEAP
       +0x030 BaseAddress      : 0x00000000`004a0000 Void
       +0x038 NumberOfPages    : 0x10
       +0x040 FirstEntry       : 0x00000000`004a0a80 _HEAP_ENTRY
       +0x048 LastValidEntry   : 0x00000000`004b0000 _HEAP_ENTRY
       +0x050 NumberOfUnCommittedPages : 0xe
       +0x054 NumberOfUnCommittedRanges : 1
       +0x058 SegmentAllocatorBackTraceIndex : 0
       +0x05a Reserved         : 0
       +0x060 UCRSegmentList   : _LIST_ENTRY [ 0x00000000`004a1fe0 - 0x00000000`004a1fe0 ]
       +0x070 Flags            : 0x1004         //堆标志
       +0x074 ForceFlags       : 4              //强制标志
       +0x078 CompatibilityFlags : 0            
       +0x07c EncodeFlagMask   : 0x100000
       +0x080 Encoding         : _HEAP_ENTRY
       +0x090 PointerKey       : 0x5c50b3ba`3fc7668b 
       +0x098 Interceptor      : 0
       +0x09c VirtualMemoryThreshold : 0xff00      //最大堆块大小
       +0x0a0 Signature        : 0xeeffeeff        //HEAP结构的签名
       +0x0a8 SegmentReserve   : 0x100000          //段的保留空间大小
       +0x0b0 SegmentCommit    : 0x2000            //每次提交内存的大小
       +0x0b8 DeCommitFreeBlockThreshold : 0x100   //解除提交的单块阈值
       +0x0c0 DeCommitTotalFreeThreshold : 0x1000  //解除提交的总空闲块阈值
       +0x0c8 TotalFreeSize    : 0x151             //空闲块的总大小
       +0x0d0 MaximumAllocationSize : 0x000007ff`fffdefff   //可分配的最大值
       +0x0d8 ProcessHeapsListIndex : 5        //本堆在进程堆列表中的索引
       +0x0da HeaderValidateLength : 0x208     //头结构的验证长度
       +0x0e0 HeaderValidateCopy : (null) 
       +0x0e8 NextAvailableTagIndex : 0    //下一个可用的堆块标记索引
       +0x0ea MaximumTagIndex  : 0         //最大的堆块标记索引
       +0x0f0 TagEntries       : (null)    //指向用于标记堆块的标记结构
       +0x0f8 UCRList          : _LIST_ENTRY [ 0x00000000`004a1fd0 - 0x00000000`004a1fd0 ]   //UnCommitedRange Segments
       +0x108 AlignRound       : 0x1f   
       +0x110 AlignMask        : 0xffffffff`fffffff0    //用于地址对齐的掩码
       +0x118 VirtualAllocdBlocks : _LIST_ENTRY [ 0x00000000`004a0118 - 0x00000000`004a0118 ]
       +0x128 SegmentList      : _LIST_ENTRY [ 0x00000000`004a0018 - 0x00000000`004a0018 ]  //段链表HEAP_SEGMENT
       +0x138 AllocatorBackTraceIndex : 0
       +0x13c NonDedicatedListLength : 0    //用于记录回溯信息
       +0x140 BlocksIndex      : 0x00000000`004a0230 Void
       +0x148 UCRIndex         : (null) 
       +0x150 PseudoTagEntries : (null) 
       +0x158 FreeLists        : _LIST_ENTRY [ 0x00000000`004a0ac0 - 0x00000000`004a0ac0 ]  //空闲块链表数组
       +0x168 LockVariable     : 0x00000000`004a0208 _HEAP_LOCK  //用于串行化控制的同步对象
       +0x170 CommitRoutine    : 0x5c50b3ba`3fc7668b     long  +5c50b3ba3fc7668b
       +0x178 FrontEndHeap     : (null)     //用于快速释放堆块的"前端堆"
       +0x180 FrontHeapLockCount : 0      //"前端堆"的锁定计数
       +0x182 FrontEndHeapType : 0 ''      //"前端堆"的类型
       +0x188 Counters         : _HEAP_COUNTERS
       +0x1f8 TuningParameters : _HEAP_TUNING_PARAMETERS

      对比一下上面HEAP_SEGMENT的结构,可以发现HEAP中就包含一个HEAP_SEGMENT结构。

                  

     四、定位申请的内存

      我们知道我们申请的内存地址为4A0A90,根据前面学习的,4A0A90-0x10 = 4A0A80就是_HEAP_ENTRY的地址,而该地址在段4a0000中

      1)我们使用!heap -a 4a0000查该堆的内容

    0:001> !heap -a 4a0000
    Index   Address  Name      Debugging options enabled
      5:   004a0000 
        Segment at 00000000004a0000 to 00000000004b0000 (00002000 bytes committed)
        Flags:                00001004
        ForceFlags:           00000004
        Granularity:          16 bytes
        Segment Reserve:      00100000
        Segment Commit:       00002000
        DeCommit Block Thres: 00000100
        DeCommit Total Thres: 00001000
        Total Free Size:      00000151
        Max. Allocation Size: 000007fffffdefff
        Lock Variable at:     00000000004a0208
        Next TagIndex:        0000
        Maximum TagIndex:     0000
        Tag Entries:          00000000
        PsuedoTag Entries:    00000000
        Virtual Alloc List:   004a0118
        Uncommitted ranges:   004a00f8
                004a2000: 0000e000  (57344 bytes)
        FreeList[ 00 ] at 00000000004a0158: 00000000004a0ac0 . 00000000004a0ac0  
            00000000004a0ab0: 00030 . 01510 [100] - free
    
        Segment00 at 004a0000:
            Flags:           00000000
            Base:            004a0000
            First Entry:     004a0a80
            Last Entry:      004b0000
            Total Pages:     00000010
            Total UnCommit:  0000000e
            Largest UnCommit:00000000
            UnCommitted Ranges: (1)
    
        Heap entries for Segment00 in Heap 00000000004a0000
                     address: psize . size  flags   state (requested size)
            00000000004a0000: 00000 . 00a80 [101] - busy (a7f)
            00000000004a0a80: 00a80 . 00030 [101] - busy (20)
            00000000004a0ab0: 00030 . 01510 [100]
            00000000004a1fc0: 01510 . 00040 [111] - busy (3d)
            00000000004a2000:      0000e000      - uncommitted bytes.

      可以看到内存粒度问16字节

     Granularity:          16 bytes

      继续往下看可以发现堆中正好有地址为4a0a80的一项

    00000000004a0a80: 00a80 . 00030 [101] - busy (20)

      第一项为地址,第二项a80为上一项的堆块大小,0x30为该堆块的大小,[101]为是这个内存的标志位,最右边的1表示内存块被占用,然后busy(20)表示这块内存被占用,申请的内存为0x20,加上块首的大小为0x10,一共是0x30

      

      2)我们知道了_HEAP_ENTRY的地址为4a0a80,我们查看该结构体

      

      发现这里的Size和我们的0x30完全不符!

      我们可以看上面的_HEAP结构,Win7下面的_HEAP结构比XP多了两项

       +0x07c EncodeFlagMask   : 0x100000
       +0x080 Encoding         : _HEAP_ENTRY

       相对于XP,Vista之后增加了对堆块的头结构(HEAP_ENTRY)的编码。编码的目的是引入随机性,增加堆的安全性,防止黑客轻易就可以预测堆的数据结构内容而实施攻击。在_HEAP结构中新增了如下两个字段:

      其中的EncodeFlagMask用来指示是否启用编码功能,Encoding字段是用来编码的,编码的方法就是用这个Encoding结构与每个堆块的头结构做亦或(XOR)

      读取_HEAP偏移为0x80的Encoding子结构:注意Size字段是从偏移8开始的两个字节,不是从偏移0开始

      

      我们使用dd查看我们的HEAP_ENTRY信息

      

       做异或解码:

    0:001> ?2b552b39^29542b3a
    Evaluate expression: 33619971 = 00000000`02010003

      低地址的word是Size字段,所以Size字段是0x3,因为是以0x10为内存粒度的,所以字节大小为

    0:001> ?3*0x10
    Evaluate expression: 48 = 00000000`00000030

      也就是0x30,与我们显示出的正好一致

      

      3)内存中的数据

      

      

    五、总结

      1.在PEB中保存这进程的堆地址和数量。

      2.HEAP结构记录HEAP_SEGMENT的方式采用了链表,这样不再受数组大小的约束,同时将HEAP_SEGMENT字段包含进HEAP,这样各堆段的起始便统一为HEAP_SEGMENT,不再有xp下0号段与其他段那种区别,可以统一进行管理了。

      3.每个HEAP_SEGMENT都有多个堆块,每个堆块包含块首和块身,块身为我们申请得到的地址。

      下一篇会研究堆溢出。

      代码链接:http://pan.baidu.com/s/1bpBm8W3

      参考:

      windbg调试HEAP

      堆和堆的调试

      <软件漏洞分析技术>

      <漏洞战争>

      

  • 相关阅读:
    小程序开发学习记录(一)
    解决移动端touch事件与click冲突的问题
    解决node.js使用fs读取文件出错
    防抖和节流
    promise
    ES6新的特性有哪些?
    css盒模型
    CSS3有哪些新特性?
    ES6新增的数据类型Map和Set。
    js求两个数组的交集|并集|差集|去重
  • 原文地址:https://www.cnblogs.com/aliflycoris/p/5914663.html
Copyright © 2020-2023  润新知