现代dump技术及保护措施[Ms-Rem](上)

本文深入探讨了现代软件保护中的反Dump技术及其绕过方法,包括利用内核级函数拦截和自定义处理程序来防止软件被非法复制。此外,还讨论了如何通过各种手段绕过这些保护措施。


鄙人拙译

现代dump技术及保护措施

本文目的

我们都喜欢使用免费的软件,这也就意味着需要有人对它们进行破解。而破解的时候就要对付各种各样的壳和protectors。壳的工作原理和脱壳的基本方法在《Packer终结篇》(NEOx, Volodya)一文中有不错的讲解。在此文中详细的讲解了PE文件格式以及protectors对它的利用方法。无疑,脱壳过程中一个重要的部分就是取得dump。关于dump的取得以及反dump的方法在那些文章中也有介绍,而所有讲到的这些PE Tools中的anti-dumping的方法都有绕过的办法。但是,遗憾的是,所有这些只能用于简单的保护,而更为复杂的protectors(eXtreme Protector, Armadillo)都使用了完全不同的反dump的方法,PE Tools已经是相形见绌了。在本文中,我想来研究一下现代的反dump方法以及绕过的办法。这对于那些想学习如何脱掉比ASPack更复杂的壳的人们来说无疑是有用的。

ring 0下的anti-dump

所有的进程dumpers都是由OpenProcess/ReadProcessMemory/VirtualQueryEx等函数来实现的。为了取得进程加载的模块列表通常要使用ToolHelp API,ToolHelp API函数使用ReadProcessMemory来读取进程的内存。在NativeAPI级别上调用的是函数ZwOpenProcess和ZwReadVirtualMemory。一个很显然的反dump的方法就是创建驱动程序,利用驱动在内核模式下拦截这些函数并禁止它们对所保护的进程进行访问。

最简单的办法就是只拦截ZwOpenProcess函数,因为要读取进程的内存首先得把进程打开。HOOK的handler一般是这样的:

NTSTATUS NewNtOpenProcess (
    OUT PHANDLE ProcessHandle,
    IN ACCESS_MASK DesiredAccess,
    IN POBJECT_ATTRIBUTES ObjectAttributes,
    IN PCLIENT_ID ClientId OPTIONAL)
{
    HANDLE ProcessId;

    if ((ULONG)ClientId > *MmUserProbeAddress) return STATUS_INVALID_PARAMETER;
    __try
    {
        ProcessId = ClientId->UniqueProcess;
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        DPRINT("Exception");
        return STATUS_INVALID_PARAMETER;
    }

    if (IsAdded(wLastItem, ProcessId))
    {
        DPRINT("Access Denied!");
        return STATUS_ACCESS_DENIED;
    } else
   
    return TrueNtOpenProcess(ProcessHandle, DesiredAccess,
                             ObjectAttributes, ClientId);
}

这段代码首先用保险的办法取出要打开的进程的ProcessID,然后判断是该传递控制权还是返回STATUS_ACCESS_DENIED。因为系统中受保护的进程可能不止一个,所以必须建立进程的列表并向其中添加新的进程并删除已经搞定的进程。为了建立这个列表我们来讲一下用来描述受保护进程的结构体:

typedef struct _ProcessList
{
    PVOID NextItem;
    HANDLE Pid;
} TProcessList, *PProcessList;

下面是管理结构体链表的代码:

BOOLEAN IsAdded(PProcessList List, HANDLE Pid)
{
    PProcessList Item = List;
    while (Item)
    {
        if (Pid == Item->Pid) return TRUE;
        Item = Item->NextItem;
    }
    return FALSE;
}

void DelItem(PProcessList *List, HANDLE Pid)
{
    PProcessList Item = *List;
    PProcessList Prev = NULL;
    while (Item)
    {
        if (Pid == Item->Pid)
        {
                   if (Prev) Prev->NextItem =
                     Item->NextItem; else *List = Item->NextItem;
                   ExFreePool(Item);
                   return;
        }
        Prev = Item;
        Item = Item->NextItem;
    }
    return;
}


void FreePointers(PProcessList List)
{
    PProcessList Item = List;
    PVOID Mem;
    while (Item)
    {
        Mem = Item;        
        Item = Item->NextItem;
        ExFreePool(Mem);
    }
    return;
}


void AddItem(PProcessList *List, HANDLE Pid)
{
    PProcessList wNewItem;
    wNewItem = ExAllocatePool(NonPagedPool, sizeof(TProcessList));
    wNewItem->NextItem = *List;
    *List = wNewItem;
    wNewItem->Pid = Pid;
    return;
}

当发现新进程时,为了向链表中添加此进程我们将使用IOCTL向驱动程序发出请求,而完成后则用同样的办法将其删除。实现这种anti-dump的驱动程序的完整代码位于本文的附件程序中。

更为可靠的保护就是拦截ZwReadVirtualMemory/ZwWriteVirtualMemory和ZwCreateThread,但是这样就必须通过进程的句柄来取得它的ProcessID。为此可以使用ZwQueryInformationProcess函数,但是对于那些有PROCESS_QUERY_INFORMATION访问标志的句柄来说这招就不灵了,
所以最好是使用ObReferenceObjectByHandle来取得EPROCESS结构体指针,然后直接从EPROCESS结构体中取出ProcessID。代码如下:

ULONG GetPid(HANDLE PHanlde)
{
    NTSTATUS st = 15;
    PEPROCESS process = 0;
    ULONG pId;
   
    st = ObReferenceObjectByHandle(PHanlde, 0, NULL, UserMode, &process, NULL);

    if (st == STATUS_SUCCESS)
    {
        pId = *(PULONG)(((ULONG)process) + pIdOffset);

        ObDereferenceObject(process);

        return pId;
    }
    return 0;
}

pIdOffset - ProcessId在EPROCESS结构体中的偏移量,这个constant在不同版本系统的内核中是不一样的,所以在驱动启动的时候要检查内核的版本并相应的为变量赋值。在写handler时要考虑到,如果进程试图用ZwOpenProcess来打开自己时也应该能工作(比较PsGetCurrentProcessId),而在处理接收句柄的函数时要考虑到用于表示当前进程的伪句柄(-1)的存在性。

说起理论来都挺容易,但实际实现起来却是一路的磕磕绊绊。比如,在Windows XP中有style服务。为了实现它需要允许子系统服务进程(csrss.exe)访问受保护进程的内存。由此就产生了问题——如何确定这个进程的Id?这个问题初看上去好像简单,其实不然。用进程名来确定是不行的,因为系统中可能有不止一个的csrss.exe进程(例如,cracker可以给自己的dumper也取这个名字),所以还需要更为可靠的唯一确定进程的方法。为此我决定使用这样的办法,子系统sever拥有某些命名对象,通过这些命名对象可以确定这个进程。例如,我们取LPC端口/Windows/ApiPort,在所有版本的Windows NT上它都是由csrss创建的。为了确定它需要使用ZwQuerySystemInformation枚举所有已打开的句柄,将它们拷贝到内核句柄表中,调用ZwQueryObject并比较所取得的名称与所寻找的是否一致。若一致,则拥有此句柄的进程的Id就是csrss的。下面是实现代码:

PVOID GetInfoTable(ULONG ATableType)
{
  ULONG mSize = 0x4000;
  PVOID mPtr = NULL;
  NTSTATUS St;

  do
  {
    mPtr = ExAllocatePool(PagedPool, mSize);

    if (mPtr != NULL)
    {
      St = ZwQuerySystemInformation(ATableType, mPtr, mSize, NULL);

    } else return NULL;

    if (St == STATUS_INFO_LENGTH_MISMATCH)
    {
      ExFreePool(mPtr);
      mSize = mSize * 2;
    }

  } while (St == STATUS_INFO_LENGTH_MISMATCH);

  if (St == STATUS_SUCCESS)
  {
    DPRINT("GetInfoTable Success!");
    DPRINT("Info table in memory size - %d", mSize);
    return mPtr;

  } else ExFreePool(mPtr);

  DPRINT("Error on GetInfoTable %X", St);

  return NULL;
}

ULONG GetCsrPid()
{
int r;
HANDLE Process, hObject;
NTSTATUS St;
ULONG CsrId = 0;
OBJECT_ATTRIBUTES obj;
CLIENT_ID cid;
POBJECT_NAME_INFORMATION ObjName;
UNICODE_STRING ApiPortName;
  
  PSYSTEM_HANDLE_INFORMATION_EX Handles;

  RtlInitUnicodeString(&ApiPortName, L"//Windows//ApiPort");

  DPRINT("Get handles info");

  Handles = GetInfoTable(SystemHandleInformation);

  if (Handles == NULL) return 0;

  ObjName = ExAllocatePool(PagedPool, 0x2000);

  DPRINT("Number of handles %d", Handles->NumberOfHandles);

  for (r = 0; r != Handles->NumberOfHandles; r++)
  {
    if (Handles->Information[r].ObjectTypeNumber == 21) //Port object
    {
  InitializeObjectAttributes(&obj, NULL, OBJ_KERNEL_HANDLE, NULL, NULL);
      
      cid.UniqueProcess = (HANDLE)Handles->Information[r].ProcessId;
      cid.UniqueThread  = 0;

      if (ZwOpenProcess(&Process, PROCESS_DUP_HANDLE, &obj, &cid)
             == STATUS_SUCCESS)
      {
        if (ZwDuplicateObject(Process,
            (HANDLE)Handles->Information[r].Handle,
            NtCurrentProcess(), &hObject,
            0, 0, DUPLICATE_SAME_ACCESS) == STATUS_SUCCESS)
        {
          if (ZwQueryObject(hObject, ObjectNameInformation,
                     ObjName, 0x2000, NULL) == STATUS_SUCCESS)
          {
            if (ObjName->Name.Buffer != NULL)
            if (wcsncmp(ApiPortName.Buffer, ObjName->Name.Buffer, 20) == 0)
            {
              DPRINT("Csrss %d", Handles->Information[r].ProcessId);
              DPRINT("csr port - %ws", ObjName->Name.Buffer);
              CsrId = Handles->Information[r].ProcessId;
              ZwClose(Process);
              ZwClose(hObject);
              CsrId = Handles->Information[r].ProcessId;
              ExFreePool(Handles);
              ExFreePool(ObjName);
              return CsrId;
            }
          } else DPRINT("Error in Query Object");
          ZwClose(hObject);
        } else DPRINT("Error on duplicating object");
        ZwClose(Process);
      } else DPRINT("Could not open process");  
    }
  }

  ExFreePool(Handles);
  ExFreePool(ObjName);

  return 0;
}

Themida (Extreme Protector) 使用的就是这种方法,在早期版本中为了处理好GUI的问题,使用的就是上面介绍的办法,但在后来的版本中却放弃了它(使得保护效果变差),新版的Themida并未禁止对受保护进程全部地址空间的访问,而只是禁止了对EXE文件所驻留的内存范围的访问,而所有加载到内存里的DLL未收到任何的保护。

在本文所附的程序中可以找到编写好的驱动程序,这个驱动程序可以保护进程不会被用这种方法dump出来。如果有人想把这个驱动添加到自己的protector里并开始大卖自己的Xprot的话,我会感到悲哀的——这种方法已经过时了,这里讲到它只是为了让文章完整。

当然,这种方法看上去还是相当好的,因为大多数cracker都不在内核模式下进行创作。摘除这种保护的办法之一就是找到原始处理程序的地址,patch掉SDT摘除HOOK。问题在于,这种方法并不通用,因为HOOK不只是能通过SDT建立,还可以通过拦截int 2Eh (win2000)或是修改sysenter (win XP)的处理程序,再有就是可以对原始处理程序的代码进行splicing。摘除protector的HOOK并不是最好的办法,因为protector可以检查HOOK是否还存在,而且还会采取措施来阻止HOOK被摘除(StarForce就是这样),所以我给出另一种方法——不在驱动程序中使用ZwReadVirtualMermory来进行dump。

在Windows NT内核中有两个未公开的函数KeAttachProcess/KeDetachProcess,驱动程序可以调用这两个函数来改变当前地址空间。函数原型如下:

extern
void KeAttachProcess(PEPROCESS Process);

extern
void KeDetachProcess(void);

通过这两个函数可以attach到受保护的进程并读取它的内存,但遗憾的是最新版的Themida对这俩个函数进行了patch,禁止用这种方法来dump自己的进程。但是它的保护并不全面,因为能attach到别的进程的函数还有KeStackAttachProcess/KeUnstackDetachProcess,而这两个函数却常被protectors遗忘(也容易产生问题,因为系统驱动程序会用到这两个函数),所以我们就用它们来读取内存。以下是函数原型和用到的结构体:

typedef struct _KAPC_STATE
{
  LIST_ENTRY ApcListHead[2];
  PVOID Process;
  BOOLEAN KernelApcInProgress;
  BOOLEAN KernelApcPending;
  BOOLEAN UserApcPending;
} KAPC_STATE, *PKAPC_STATE;

extern
NTKERNELAPI void KeStackAttachProcess(PVOID Process, PKAPC_STATE ApcState);

extern
NTKERNELAPI void KeUnstackDetachProcess(PKAPC_STATE ApcState);

即使下一版的Themida实现了对这两个函数的拦截,attach到所需的进程也不难,手动修改cr3寄存器就行了。

为了读取进程的内存,我们需要使用IOCTL向我们的驱动程序传递进程的句柄、要读取的内存地址、当前进程缓冲区的地址以及要读取的字节数,在驱动获得dump进程的EPROCESS指针之后就会读取请求的数据。也就是说,实际上我们自行手工实现了ZwReadVirtualMemory。代码如下:

void CopyProcessMem(HANDLE hProcess, PVOID SrcAddr, PVOID DstAddr, ULONG *Size)
{
    PEPROCESS process = NULL;
    NTSTATUS  st;
    PUCHAR pMem = NULL;
    ULONG Addr, Bytes;
    PUCHAR cPtr, dPtr;
    KAPC_STATE ApcState;

    st = ObReferenceObjectByHandle(hProcess, 0, NULL, UserMode, &process, NULL);

    if (NT_SUCCESS(st))
    {   
        Bytes = *Size;
        pMem = ExAllocatePool(NonPagedPool, Bytes);
        dPtr = pMem;

        cPtr = (PUCHAR)SrcAddr;

        KeStackAttachProcess(process, &ApcState);
   
        __try
        {
            while (Bytes)
            {
                *dPtr = *cPtr;
                cPtr++;
                dPtr++;
                Bytes --;
            }
        }

        __except(EXCEPTION_EXECUTE_HANDLER)
        {
        }

        KeUnstackDetachProcess(&ApcState);

        Bytes = *Size - Bytes;

        __try
        {
            memcpy(DstAddr, pMem, Bytes);
            *Size = Bytes;
        }
        __except(EXCEPTION_EXECUTE_HANDLER)
        {
        }

        ExFreePool(pMem);

        ObDereferenceObject(process);
    }
    return;
}

对于驱动方式的dumping来说,要建立针对它的保护措施就要难多了,因为实际上来讲很难确定传递的信息究竟是来自于dumper还是来自于系统。但也不是不可能做到,您不要以为这招可以对付所有使用驱动dump的protectors(尽管目前还是有用的)。

遗憾的是,自行替换ZwWriteVirtualMemory在很多情况下是不够的,因为有些protectors还拦截了ZwOpenProcess。要绕过它,可以不去打开进程,而是通过在子系统的server进程里搜索来获得它的句柄(参见《使用Code Injection拦截Windows NT API函数》)。在我的advApiHook库里,对应这项功能的函数叫OpenProcessEx。有些保护程序会修改进程的security marker,以此来禁止打开内存读取。对这种保护,这个方法也可以对付。如果这种获取进程句柄的方法流行起来的话,保护程序的作者们就会最先在子系统server中把句柄关闭。要在驱动程序中获取进程句柄,只需使用PsLookupProcessByProcessId来取得指向其EPROCESS的指针,此后可以用ObOpenObjectByPointer把它加入到我们进程的句柄表中。实现代码如下:


HANDLE MyOpenProcess(HANDLE ProcessId)
{
    PEPROCESS Process;
    NTSTATUS St;
    HANDLE hProcess = NULL;

    PsLookupProcessByProcessId(ProcessId, &Process);

    ObOpenObjectByPointer(Process, 0, NULL, 0, NULL, UserMode, &hProcess);

    ObDereferenceObject(Process);

    return hProcess;
}

保护程序的作者们当然可以拦截PsLookupProcessByProcessId,但这对于获取dump只能起到5分钟的拖延作用,因为句柄的获取还可以通过手工挖掘内核结构体来完成,或者干脆就不用句柄也不用API。

ring 0下的保护手段就丰富多彩了(想象无限,创意无限)。例如,一个非常不错的anti-dump的方法就是破坏受保护进程的页表。为此需要干预调度器的工作并拦截某些SwapContext的未导出函数,这些函数都是在线程切换时要调用的。在拦截处理程序中,在切换入受保护进程时需要恢复页表,而在切换出时就把页表破坏掉。更为简单的是可以对抗dumper驱动。绕过这种保护也不困难,既可以在内核中进行(拦截中断处理程序),也可以在用户模式下进行(Code Injection)。我想,只要肯动脑,大家都能想出进行dump的自己原创的方法。

<think> 我们正在讨论的是 `adb shell pm dump-profiles --dump-classes-and-methods <package-name>` 命令。 根据引用[1]和[2],我们知道在adb shell中可以使用pm(Package Manager)命令来执行与包相关的操作。 然而,引用中并没有直接提到`dump-profiles`这个选项。但是,我们可以根据已有知识和类似命令进行推断。 在Android系统中,`pm dump-profiles`命令用于转储指定应用的ART(Android Runtime)性能分析(profile)信息。 这个profile信息是ART在运行过程中收集的,用于指导AOT(Ahead-Of-Time)编译优化,以提高应用性能。 具体来说,`--dump-classes-and-methods`选项会输出该应用中被记录的热点类和方法,这些类和方法是在应用运行期间被频繁调用的。 使用方法: ``` adb shell pm dump-profiles --dump-classes-and-methods <package-name> ``` 注意:该命令需要设备具有root权限,或者是在debuggable的应用上才能使用(通常非系统应用无法使用,除非是debug版本)。 输出结果解读: 命令执行后,会输出一个文本,其中包含以下信息: 1. 应用的包名 2. 记录的热点方法(hot methods)和热点类(hot classes)的列表 3. 每个方法的信息包括:方法名、调用次数(或热度值)等 但是,具体的输出格式可能因Android版本而异。在较新的Android版本中,profile文件是二进制的,但该命令会将其转换为可读文本输出。 示例输出(模拟): ``` Profile for com.example.app: Classes: Lcom/example/app/MainActivity; Lcom/example/app/util/NetworkHelper; Methods: Lcom/example/app/MainActivity;->onCreate(Landroid/os/Bundle;)V: hotness=15 Lcom/example/app/util/NetworkHelper;->downloadData()Ljava/lang/String;: hotness=8 ``` 其中,`hotness`表示该方法的“热度”,通常代表被调用的次数或执行频率(具体计算方式由ART内部决定)。 但是,需要注意的是,在非root设备上,如果没有开启调试或者应用不是debuggable,可能无法成功执行该命令。 替代方案:如果我们没有root权限,可以尝试使用以下方法获取profile信息: 1. 使用`adb shell run-as <package-name>`命令(要求应用为debuggable) 2. 进入应用的数据目录:`cd /data/data/<package-name>` 3. 然后使用`profman`工具(如果可用)来解析profile文件,但注意`profman`通常不在用户设备上,而是在开发机上(在Android SDK的构建工具中) 因此,在实际操作中,我们通常通过以下步骤: 1. 获取profile文件(需要root或debuggable): ``` adb shell run-as <package-name> cat /data/misc/profiles/cur/0/<package-name>/primary.prof > primary.prof ``` 2. 然后在开发机上使用`profman`工具解析: ``` profman --profile-file=primary.prof --dump-classes-and-methods --dump-hotness ``` 但是,`pm dump-profiles`命令实际上是上述步骤的一个封装,它直接输出解析后的文本。 总结: 命令:`adb shell pm dump-profiles --dump-classes-and-methods <package-name>` 作用:输出指定应用的热点类和方法列表,用于分析应用性能。 使用条件:需要root或debuggable应用。 输出:列出热点类和方法,以及它们的热度值。 注意:由于该命令是系统级命令,不同Android版本输出可能不同,且可能在未来版本中发生变化。 §§相关问题§§ 1. 如何在没有root的情况下获取应用的profile信息? 2. 如何利用profile信息进行应用编译优化? 3. ART中的profile数据是如何收集的? </think> ### `adb shell pm dump-profiles --dump-classes-and-methods` 命令详解 #### 一、命令作用 该命令用于**直接获取应用的热点方法/类记录**,无需手动提取 `.prof` 文件。它通过 Android 的 Package Manager 服务访问 ART 运行时收集的 profile 数据: 1. 显示已记录的热点方法(hot methods)及其调用频率 2. 列出参与优化的类(optimized classes) 3. 展示方法内联记录(inline caches) 4. 输出 JIT/AOT 编译决策信息[^1] #### 二、使用条件 | 条件 | 要求 | 验证方法 | |------|------|----------| | Android版本 | ≥ 7.0 (Nougat) | `adb shell getprop ro.build.version.release` | | 应用状态 | 已运行过至少一次 | `adb shell ps \| grep <包名>` | | 权限 | **root 或 debuggable 应用** | `adb shell dumpsys package <包名> \| grep flags=`<br>`... flags=[ DEBUGGABLE ]` | | Profile状态 | 已生成记录 | `adb shell ls /data/misc/profiles/cur/0/<包名>/` | #### 三、命令语法 ```bash adb shell pm dump-profiles [选项] <包名> ``` 常用选项组合: ```bash # 完整输出热点方法和类 adb shell pm dump-profiles --dump-classes-and-methods <包名> # 仅显示统计摘要(推荐首次使用) adb shell pm dump-profiles --dump-only <包名> # 详细输出+十六进制原始数据 adb shell pm dump-profiles --dump-and-print <包名> ``` #### 四、输出结果解析(示例) ```plaintext Profile of com.example.app: Aggregated hotness: kHot: 15 methods kWarm: 8 methods kCold: 42 methods Classes (optimized): Lcom/example/MainActivity; [startup] Lcom/network/HttpUtil; [post_startup] Hot methods (top 3): Lcom/example/MainActivity;->onCreate(Landroid/os/Bundle;)V (hotness=15) Inline caches: 2 Compilation: AOT Lcom/network/HttpUtil;->fetch(Ljava/lang/String;)V (hotness=8) Inline caches: 1 Compilation: JIT Lcom/utils/ImageLoader;->load(Landroid/widget/ImageView;)V (hotness=5) ``` 关键字段说明: 1. **热力分级**(Aggregated hotness) - `kHot`:高频方法(>10次/启动) - `kWarm`:中频方法(2-10次) - `kCold`:低频方法(≤1次) 2. **类标记** - `[startup]`:影响启动性能的关键类 - `[post_startup]`:启动后高频使用的类 3. **方法热度值** - `hotness=15`:方法被执行的总次数 - `Inline caches`:内联优化次数 - `Compilation`:编译方式(AOT/JIT) #### 五、典型应用场景 1. **启动优化** ```bash # 查找启动相关热点 adb shell pm dump-profiles --dump-classes-and-methods <包名> | grep "startup" ``` 2. **验证基线配置** ```bash # 检查关键方法是否被记录 adb shell pm dump-profiles <包名> | grep "onCreate" ``` 3. **性能回归排查** ```bash # 对比版本间热点变化 adb shell pm dump-profiles com.example.app > v1.txt adb shell pm dump-profiles com.example.app > v2.txt diff v1.txt v2.txt ``` #### 六、常见问题处理 1. **无输出/权限拒绝** ```bash # 检查 debuggable 标志 adb shell dumpsys package <包名> | grep flags= # 启用调试模式(需重装) android:debuggable="true" # 在AndroidManifest.xml中 ``` 2. **数据未更新** ```bash # 强制重置 profile adb shell cmd package compile --reset <包名> # 重新触发记录 adb shell am start -n <包名>/MainActivity sleep 30 # 等待操作 ``` 3. **输出截断处理** ```bash # 保存完整输出到文件 adb shell pm dump-profiles --dump-classes-and-methods <包名> > profile_dump.txt ``` #### 七、与其他工具对比 | 工具 | 优势 | 局限 | |------|------|------| | `pm dump-profiles` | 直接输出可读结果<br>无需文件提取 | 需 root/debuggable | | `profman` | 支持深度分析<br>可转二进制格式 | 需手动提取 .prof 文件 | | Android Studio Profiler | 可视化分析<br>火焰图展示 | 需连接开发机<br>实时采样影响性能 | ```mermaid graph TD A[启动应用] --> B{是否 debuggable?} B -->|是| C[执行 pm dump-profiles] B -->|否| D[获取 root 权限] D --> C C --> E[解析输出] E --> F[定位热点方法] F --> G[优化高频代码] G --> H[验证性能提升] ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值