Intel Pin 本体是一个动态二进制插桩框架,但是其内部集成的 Intel XED 库接口非常适合做指令统计,更重要的是……已经有现成的工具包含在官方示例里面了。

本文简单记录一下 Intel Pin 的概念和用法,仅作为备忘录。

NOTES:

  • 静态分析的话,可以使用 LLVM-MCA
  • 动态分析但是面向性能剖析的话,perf 就够了。
  • 我本来想着这种观测用途首先就得找 eBPF/bcc 工具集,结果往里一看,没有一个工具是关于指令的。然后我又找了 valgrind --tools=lackey,卡🐔没法用。看来我对工具的认知还不够。
  • Intel Pin 是闭源的,不要管什么 JIT 原理实现,能用就行;另外这里只做统计不做分析。

基本概念

以下是对 Pin User Guide 的拙劣解释,不保证理解无误。推荐读者亲自看用户指南,或者找有安全背景的人(我不是!)写的文章。


$ pin -t <pintool.so> -- <target_binary>

Intel Pin 可以视为一个 JIT 编译器,它会接收两个组件:一是需要被插桩的二进制文件(目标程序),二是 pintool。Intel Pin 根据目标程序在同一虚拟地址空间内将原始指令编译成新的指令,而 pintool 则注册插桩例程和提供分析例程,前者决定插入点,后者用于插入点执行用户定义函数。也就是说,pintool 是我们用户要实现的工具,用它来指导 Intel Pin 的代码生成过程。

pintool pintool 示例:计算指令总数(图源

Intel Pin 为了提高执行效率,将插桩的粒度分为多种对象,从大到小:

  • IMG/image:一个二进制文件。
  • SEC/section:一个 ELF section。
  • RTN/routine:一个函数。
  • TRACE/trace:连续的指令序列,单入口多出口。(动态构建的执行路径,用于优化性能。)
  • BBL/basic block:连续的指令序列,单入口单出口。(内部不存在跳转。)
  • INS/instruction:单条指令。

对于指令级的插桩,其插入点(IPOINT)除了不仅有前后之分(BEFORE/AFTER,后者只用于直落场景),还有控制流之分(TAKEN_BRANCH,只用于非直落场景),直落(fall through)指的是控制流不会拐弯,也就是要排除掉 jmp/call/ret 这类指令,像 jcc 这种指令就是 AFTER 和 TAKEN_BRANCH 都可以插入,但是运行时互斥;对于更粗粒度的插桩,还有「不管什么位置总之插入就行」的选项(ANYWHERE),以便 JIT 优化。

IPOINT 文档还是抽象,我构造了一个半成品测试程序,感兴趣可以试试。

一些节选特性,详情需要看官方指南:

  • 代码缓存:trace 可理解为多个 BBL 动态创建,提供代码缓存优化。也就是说,如果其中有同一个 BBL 是在 trace 里面反复跳转,那么能节省 JIT 开销。
  • 共享上下文:由于是共享的地址空间,pintool 可以与原有的目标二进制程序进行共享内存通信;不仅如此,连文件描述符等系统提供的资源也是共享的。
  • 线程安全:pintool 的分析例程如果是运行在多线程环境中,那么访问全局变量就需要避免数据竞争,此时必须要使用 Intel PIN 提供的接口(锁原语等)。
  • 探针模式:除了 JIT 模式以外,Intel Pin 还支持 probe 模式。官方说辞是这个跑得快但是接口会更少。我的理解是后者只是给特定函数(RTN)入口换了个 trampoline 跳板。
  • 更多的插入语义:常规的插入接口是 insert-call,但是接口文档还提供了 insert-if-call 和 insert-then-call 等更加复杂的语义。为什么不在 insert-call 层面上自己做 if-else 判断呢?文档的意思是有更好的内联优化。

所以基本概念大概这样?再复杂点我也不会。

示例使用

# 位于 ./source/tools/SimpleExamples,我们使用 catmix 来分析 ls 程序
# 别忘了编译
../../../pin -t obj-intel64/catmix.so -- /bin/ls && cat catmix.out

Intel Pin 内置的 catmix 示例已经基本符合我的工具需求,先来一个 demo。

#
# static counts
#
#num category   count-unpredicated count-predicated
#
  2 ADOX_ADCX                 0           3
  3 AES                       0       12636
  4 AMX_TILE                  0         196
  6 AVX                     818           0
  7 AVX2                   2844       11862
  8 AVX2GATHER                0           2
  9 AVX512                  866           0
 13 AVX512_VBMI               0        7227
 14 AVX512_VP2INTERSECT       0         531
 16 BINARY                59325           0
 17 BITBYTE                1176         520
 19 BMI1                    537        1766
 20 BMI2                    146           1
 21 BROADCAST                57           0
 22 CALL                  18901           0
 23 CET                      20        5433
 25 CLFLUSHOPT                0        6573
 28 CMOV                      0        2383
 30 COND_BR               55168          56
 32 CONVERT                 558           0
 33 DATAXFER             166068           0
 34 DECIMAL                   0       20845
 38 FLAGOP                    3           0
 44 HRESET                    0       19963
 46 INTERRUPT                 0         529
 48 IOSTRINGOP                0           5
 51 KMASK                   791           0
 54 LOGICAL               49049           0
 55 LOGICAL_FP               32           0
 56 LZCNT                    27           0
 57 MISC                  26814           0
 62 NOP                    3327           0

#
# dynamic counts
#
#num category   count-unpredicated count-predicated
#
  3 AES                       0       24011
  6 AVX                    2879           0
  7 AVX2                   8549       29049
 13 AVX512_VBMI               0        9552
 14 AVX512_VP2INTERSECT       0          31
 16 BINARY               128776           0
 17 BITBYTE                1361          89
 19 BMI1                   2409        5883
 20 BMI2                    316           0
 21 BROADCAST               455           0
 22 CALL                   9557           0
 23 CET                       0       17601
 25 CLFLUSHOPT                0        6503
 28 CMOV                      0        2255
 30 COND_BR              119821           8
 32 CONVERT                  74           0
 33 DATAXFER             225624           0
 34 DECIMAL                   0       13874
 44 HRESET                    0       17370
 48 IOSTRINGOP                0           1
 54 LOGICAL               99891           0
 57 MISC                  25563           0
 62 NOP                     524           0
# eof

实现这些指令统计需要依赖 Intel XED 库,这也是 catmix.cpp 的实现方式。为了精简,这里的结果去掉了第三列(使用 INS_InsertPredicatedCall 的报告)。

catmix.cpp 实现
/*
 * Copyright (C) 2004-2021 Intel Corporation.
 * SPDX-License-Identifier: MIT
 */

/*! @file
 *  This file contains a static and dynamic instruction category mix profiler
 */

#include "pin.H"
#include <list>
#include <iostream>
#include <fstream>
#include <stdlib.h>
using std::cerr;
using std::endl;
using std::list;
using std::ofstream;
using std::string;

/* ===================================================================== */
/* Commandline Switches */
/* ===================================================================== */

KNOB< string > KnobOutputFile(KNOB_MODE_WRITEONCE, "pintool", "o", "catmix.out", "specify profile file name");
KNOB< BOOL > KnobProfilePredicated(KNOB_MODE_WRITEONCE, "pintool", "p", "0",
                                   "enable accurate profiling for predicated instructions");
KNOB< BOOL > KnobProfileStaticOnly(KNOB_MODE_WRITEONCE, "pintool", "s", "0",
                                   "terminate after collection of static profile for main image");
KNOB< BOOL > KnobNoSharedLibs(KNOB_MODE_WRITEONCE, "pintool", "no_shared_libs", "0", "do not instrument shared libraries");

/* ===================================================================== */
/* Print Help Message                                                    */
/* ===================================================================== */

INT32 Usage()
{
    cerr << "This pin tool computes static and  dynamic instruction family (=category) mix profile\n"
            "\n";

    cerr << KNOB_BASE::StringKnobSummary() << endl;
    return -1;
}

/* ===================================================================== */
/* Global Variables */
/* ===================================================================== */

const UINT32 MAX_INDEX = 64;

typedef UINT64 COUNTER;

/* zero initialized */

typedef struct
{
    COUNTER unpredicated[MAX_INDEX];
    COUNTER predicated[MAX_INDEX];
    COUNTER predicated_true[MAX_INDEX];
} STATS;

STATS GlobalStatsStatic;
STATS GlobalStatsDynamic;

class BBLSTATS
{
  public:
    BBLSTATS(UINT16* stats) : _stats(stats), _counter(0) {};

    const UINT16* _stats;
    COUNTER _counter;
};

list< const BBLSTATS* > statsList;

/* ===================================================================== */

VOID ComputeGlobalStats()
{
    // We have the count for each bbl and its stats, compute the summary
    for (list< const BBLSTATS* >::iterator bi = statsList.begin(); bi != statsList.end(); bi++)
    {
        for (const UINT16* stats = (*bi)->_stats; *stats; stats++)
        {
            GlobalStatsDynamic.unpredicated[*stats] += (*bi)->_counter;
        }
    }
}

/* ===================================================================== */

UINT16 INS_GetStatsIndex(INS ins)
{
    if (INS_IsPredicated(ins))
        return MAX_INDEX + INS_Category(ins);
    else
        return INS_Category(ins);
}

/* ===================================================================== */

VOID docount(COUNTER* counter) { (*counter)++; }

/* ===================================================================== */

VOID Trace(TRACE trace, VOID* v)
{
    if (KnobNoSharedLibs.Value() && IMG_Type(SEC_Img(RTN_Sec(TRACE_Rtn(trace)))) == IMG_TYPE_SHAREDLIB) return;

    const BOOL accurate_handling_of_predicates = KnobProfilePredicated.Value();

    for (BBL bbl = TRACE_BblHead(trace); BBL_Valid(bbl); bbl = BBL_Next(bbl))
    {
        // Summarize the stats for the bbl in a 0 terminated list
        // This is done at instrumentation time
        UINT16* stats = new UINT16[BBL_NumIns(bbl) + 1];

        INT32 index = 0;
        for (INS ins = BBL_InsHead(bbl); INS_Valid(ins); ins = INS_Next(ins))
        {
            // Count the number of times a predicated instruction is actually executed
            // this is expensive and hence disabled by default
            if (INS_IsPredicated(ins) && accurate_handling_of_predicates)
            {
                INS_InsertPredicatedCall(ins, IPOINT_BEFORE, AFUNPTR(docount), IARG_PTR,
                                         &(GlobalStatsDynamic.predicated_true[INS_Category(ins)]), IARG_END);
            }

            stats[index++] = INS_GetStatsIndex(ins);
        }
        stats[index] = 0;

        // Insert instrumentation to count the number of times the bbl is executed
        BBLSTATS* bblstats = new BBLSTATS(stats);
        INS_InsertCall(BBL_InsHead(bbl), IPOINT_BEFORE, AFUNPTR(docount), IARG_PTR, &(bblstats->_counter), IARG_END);

        // Remember the counter and stats so we can compute a summary at the end
        statsList.push_back(bblstats);
    }
}

/* ===================================================================== */
VOID DumpStats(ofstream& out, STATS& stats, const string& title)
{
    out << "#\n"
           "# "
        << title
        << "\n"
           "#\n"
           "#num category   count-unpredicated count-predicated count-predicated-true\n"
           "#\n";

    for (UINT32 i = 0; i < MAX_INDEX; i++)
    {
        if (stats.unpredicated[i] == 0 && stats.predicated[i] == 0) continue;

        out << decstr(i, 3) << " " << ljstr(CATEGORY_StringShort(i), 15) << decstr(stats.unpredicated[i], 12)
            << decstr(stats.predicated[i], 12) << decstr(stats.predicated_true[i], 12) << endl;
    }
}

/* ===================================================================== */
static std::ofstream* out = 0;

VOID Fini(int, VOID* v)
{
    DumpStats(*out, GlobalStatsStatic, "static counts");

    *out << endl;

    ComputeGlobalStats();

    DumpStats(*out, GlobalStatsDynamic, "dynamic counts");

    *out << "# eof" << endl;

    out->close();
}

/* ===================================================================== */

VOID Image(IMG img, VOID* v)
{
    for (SEC sec = IMG_SecHead(img); SEC_Valid(sec); sec = SEC_Next(sec))
    {
        for (RTN rtn = SEC_RtnHead(sec); RTN_Valid(rtn); rtn = RTN_Next(rtn))
        {
            RTN_Open(rtn);

            for (INS ins = RTN_InsHead(rtn); INS_Valid(ins); ins = INS_Next(ins))
            {
                if (INS_IsPredicated(ins))
                    GlobalStatsStatic.predicated[INS_Category(ins)]++;
                else
                    GlobalStatsStatic.unpredicated[INS_Category(ins)]++;
            }

            RTN_Close(rtn);
        }
    }

    if (KnobProfileStaticOnly.Value())
    {
        Fini(0, 0);
        exit(0);
    }
}

/* ===================================================================== */
/* Main                                                                  */
/* ===================================================================== */

int main(int argc, CHAR* argv[])
{
    PIN_InitSymbols();

    if (PIN_Init(argc, argv))
    {
        return Usage();
    }

    out = new std::ofstream(KnobOutputFile.Value().c_str());

    TRACE_AddInstrumentFunction(Trace, 0);

    PIN_AddFiniFunction(Fini, 0);

    IMG_AddInstrumentFunction(Image, 0);

    // Never returns

    PIN_StartProgram();

    return 0;
}

/* ===================================================================== */
/* eof */
/* ===================================================================== */

源码在上方,接下来做点简要解析。

Intel XED

// 输入
UINT16 INS_GetStatsIndex(INS ins)
{
    // 关键是 INS_Category
    if (INS_IsPredicated(ins))
        return MAX_INDEX + INS_Category(ins);
    else
        return INS_Category(ins);
}

// 输出
VOID DumpStats(ofstream& out, STATS& stats, const string& title)
{
    out << "#\n"
           "# "
        << title
        << "\n"
           "#\n"
           "#num category   count-unpredicated count-predicated count-predicated-true\n"
           "#\n";

    for (UINT32 i = 0; i < MAX_INDEX; i++)
    {
        if (stats.unpredicated[i] == 0 && stats.predicated[i] == 0) continue;

        // 关键是 CATEGORY_StringShort
        out << decstr(i, 3) << " " << ljstr(CATEGORY_StringShort(i), 15)
            << decstr(stats.unpredicated[i], 12)
            << decstr(stats.predicated[i], 12)
            << decstr(stats.predicated_true[i], 12)
            << endl;
    }
}

XED 就是 X86 Encoder/Decoder,不仅能编码(生成指令),还能解码(解析指令)。工具里面用的它的解码能力:说白了就是调接口 INS_Category()

category 是比指令更高层的抽象,这就是我之前特别想要的需求,我想看到的是程序的某些例程在运行一段时间后不同类别的指令数占比,而不是每一种指令的占比。比如 ja 和 jc,类似的指令总不应该分成两个项单独列出来吧,所以用 COND_BR 作为一个高层类别是合理的。其实这种事情实质就是查表,只是工作量有点大

更多接口

# 稍微对工具做了定制
# $dynamic-counts
#
#     opcode                         count
#
3000 *total                         576158
3001 *mem-atomic                        76
3002 *stack-read                     52148
3003 *stack-write                    46084
3004 *iprel-read                     10675
3005 *iprel-write                     1029
3007 *mem-read-1                     28931
3008 *mem-read-2                      3091
3010 *mem-read-4                     22536
3014 *mem-read-8                     81995
3022 *mem-read-16                     2548
3038 *mem-read-32                     4412
3527 *mem-write-1                     1796
3528 *mem-write-2                      400
3530 *mem-write-4                     6017
3534 *mem-write-8                    50163
3542 *mem-write-16                    2409
3558 *mem-write-32                     410

除此以外,官方示例中还有一个 insmix.cpp 实现,也是用来了解访存模式的好工具。总之就是拿一堆 INS_IsMemoryRead() 类似接口做的。

静态分析

VOID Image(IMG img, VOID* v)
{
    // 从 IMG 下探到 SEC -> RTN -> INS
    for (SEC sec = IMG_SecHead(img); SEC_Valid(sec); sec = SEC_Next(sec))
    {
        for (RTN rtn = SEC_RtnHead(sec); RTN_Valid(rtn); rtn = RTN_Next(rtn))
        {
            RTN_Open(rtn);

            for (INS ins = RTN_InsHead(rtn); INS_Valid(ins); ins = INS_Next(ins))
            {
                // 静态地获取 INS 信息
                if (INS_IsPredicated(ins))
                    GlobalStatsStatic.predicated[INS_Category(ins)]++;
                else
                    GlobalStatsStatic.unpredicated[INS_Category(ins)]++;
            }

            RTN_Close(rtn);
        }
    }

    if (KnobProfileStaticOnly.Value())
    {
        Fini(0, 0);
        exit(0);
    }
}


int main() {
    // ...
    IMG_AddInstrumentFunction(Image, 0);
    // ...
}

前面的示例结果(# static counts)和源码其实也展示了 Intel Pin 的静态分析(统计)能力。通过 IMAGE 下探到 SEC、RTN,就可以静态地获取到所有指令的元数据。知道这种方式的写法,自己也可以做一个静态 pintool 了。

动态分析

前面已经有了 XED 给的接口,在静态分析那里也看到了也是调用一下的事情,那么动态分析的一种可行的办法就是 INS_AddInstrumentFunction 直接运行时把每条指令算出来。

VOID Trace(TRACE trace, VOID* v)
{
    for (BBL bbl = /*...*/)
    {
        // 只在 BBL 的入口插桩,降低分析例程的执行开销
        INS_InsertCall(BBL_InsHead(bbl), /* ... */);
    }
}

int main(int argc, CHAR* argv[])
{
    // ...
    TRACE_AddInstrumentFunction(Trace, 0);
    PIN_AddFiniFunction(Fini, 0);
    // ...

    // Never returns
    PIN_StartProgram();

    return 0;
}

不过示例的做法更加精明一些:作者只对 BBL 入口进行插桩,从而把分析例程的执行开销从指令量级降低到 BBL 量级。这也是官网文档提到的:插桩例程只会执行一次,但是分析例程会执行 N 次,因此要想办法降低分析例程的开销。

const UINT32 MAX_INDEX = 64;

typedef UINT64 COUNTER;

/* zero initialized */

typedef struct
{
    COUNTER unpredicated[MAX_INDEX];
    COUNTER predicated[MAX_INDEX];
    COUNTER predicated_true[MAX_INDEX];
} STATS;

STATS GlobalStatsStatic;
STATS GlobalStatsDynamic;

class BBLSTATS
{
  public:
    BBLSTATS(UINT16* stats) : _stats(stats), _counter(0) {};

    const UINT16* _stats;
    COUNTER _counter;
};

list< const BBLSTATS* > statsList;

VOID Trace(TRACE trace, VOID* v)
{
    // ...
    for (BBL bbl = TRACE_BblHead(trace); BBL_Valid(bbl); bbl = BBL_Next(bbl))
    {
        // Summarize the stats for the bbl in a 0 terminated list
        // This is done at instrumentation time
        // 注意是 instrumentation time 而不是 analysis time
        UINT16* stats = new UINT16[BBL_NumIns(bbl) + 1];

        INT32 index = 0;
        // 将这个 BBL 的批量(指令)静态结果记录到 stats 中
        for (INS ins = BBL_InsHead(bbl); INS_Valid(ins); ins = INS_Next(ins))
        {
            // ...

            stats[index++] = INS_GetStatsIndex(ins);
        }
        stats[index] = 0;

        // Insert instrumentation to count the number of times the bbl is executed
        BBLSTATS* bblstats = new BBLSTATS(stats);
        INS_InsertCall(BBL_InsHead(bbl), IPOINT_BEFORE, AFUNPTR(docount), IARG_PTR, &(bblstats->_counter), IARG_END);

        // Remember the counter and stats so we can compute a summary at the end
        statsList.push_back(bblstats);
    }
}

// 用于 Fini 的辅助函数
VOID ComputeGlobalStats()
{
    // We have the count for each bbl and its stats, compute the summary
    for (list< const BBLSTATS* >::iterator bi = statsList.begin(); bi != statsList.end(); bi++)
    {
        for (const UINT16* stats = (*bi)->_stats; *stats; stats++)
        {
            GlobalStatsDynamic.unpredicated[*stats] += (*bi)->_counter;
        }
    }
}

VOID Fini(int, VOID* v)
{
    // 静态统计
    DumpStats(*out, GlobalStatsStatic, "static counts");

    *out << endl;

    ComputeGlobalStats();

    // 动态统计
    DumpStats(*out, GlobalStatsDynamic, "dynamic counts");

    *out << "# eof" << endl;

    out->close();
}

具体来说就是,先是静态统计了每个 BBL 的指令信息 bblstats,其中使用两个字段:

  • 一个数组,长度为 BBL_NumIns(bbl) + 1,记录每条指令应归属的 (un)predicated[] 下标。
  • 一个计数器,就是 BBL 触发的次数。

控制流全部执行完了再走 Fini 插桩例程,聚合计算 category 类别的次数,注意这里是无分支地计算了 predicated[] 和 unpredicated[],因为它们布局是连续的。

我个人有另外一个实现设想,这里可利用 BBL 的性质:只要控制流能进入 BBL 的入口,它的性质保证了内部所有的指令均会被执行。可以尝试将 bblstats 的描述改为包含一个计数器和一个长度固定的 stat[MAX_CATEGORY] 数组,静态遍历块内指令时更新 stat[INS_Category(ins)]++。也就是说不仅可以让分析例程降低到 BBL 粒度,还能在聚合计算时改进为 unpredicated[i] += bblstats.stat[i] * bblstats.counter。其实复杂度和原有实现是相同的,但是访存会更有局部性,也易于 SoA 改造。

有问题吗?

其实心里是有点质疑这个工具的质量,我看一眼示例就认为有两个问题:

  1. 为什么会有 AVX512 类别?我的主机根本就不支持该指令啊,哪里长出来的?
  2. 为什么没有 UNCOND_BR 类别?这是 jmp 指令的抽象,怎么连这都抓不到?
# jojo.s

.section .text
.global _start

_start:
    mov $1234567, %rcx
jump_loop:
    dec %rcx
    jnz jump_loop

    mov $60, %rax        # syscall number for exit
    xor %rdi, %rdi       # exit status 0
    syscall
# 记住这个名字,后面还会用
# gcc jojo.s -nostdlib -static -o jojo
# pin -t catmix.so -- ./jojo
#
# dynamic counts
#
#num category   count-unpredicated count-predicated
#
 16 BINARY              1234567           0
 30 COND_BR             1234567           0
 33 DATAXFER                  2           0
 54 LOGICAL                   1           0
# eof

问题一我先是认为可能是 C库/ld.so 引入的乱七八糟的指令,至少写了一个足够简单的汇编,加载后确实没有胡来的指令;但这能证明一个常规的用了 C 库的二进制文件的统计是正确的吗?继续问题二的讨论,你会心中有数。

题外话,看到 catmix.cpp 的 -no_shared_libs 选项吗?使用它会直接 core dumped。

# 仍然是 pin -t obj-intel64/catmix.so -- /bin/ls && cat catmix.out

#
# dynamic counts
#
#num category   count-unpredicated count-predicated
#
  6 AVX                    2903           0
  7 AVX2                   8637           0
 16 BINARY               140626           0
 17 BITBYTE                1361           0
 19 BMI1                   2433           0
 20 BMI2                    318           0
 21 BROADCAST               455           0
 22 CALL                   9643           0
 28 CMOV                      0        2126
 30 COND_BR              126124           0
 32 CONVERT                  74           0
 33 DATAXFER             235297           0
 54 LOGICAL              104085           0
 57 MISC                  27162           0
 62 NOP                     544           0
 67 POP                   24322           0
 71 PUSH                  29364           0
 77 RET                    9638           0
 78 ROTATE                   31           0
 81 SEMAPHORE                91           0
 83 SETCC                  7083           0
 87 SHIFT                 17703           0
 89 SSE                    6498           0
 90 STRINGOP                  0        2175
 92 SYSCALL                 156           0
 94 SYSTEM                    8           0
 98 UNCOND_BR             14076           0 # 问题二解决了😋
108 WIDENOP               17658           0
112 XSAVE                     1           0
# eof

问题二的根因出现在源码上。

// catmix.cpp 关键一行
const UINT32 MAX_INDEX = 64;

这个 catmix.cpp 太老旧了,硬编码导致跟不上时代。其实简单地把 MAX_INDEX 数值改为 XED_CATEGORY_LAST 就能覆盖所有的类别。

其实问题二也在影响问题一。如果你拿原 catmix 去观测 jojo 二进制,你会发现还多了一次 CMOV 的统计,其实这是静态分析越界到动态分析了。

所以还有没有问题三?这我就不清楚了。

结尾

不知道为啥,这个工具存在很多年了,我在网上没找到太多信息,可能是人不在安全圈子的原因吧。

总之这只是一次 Intel Pin 的折腾笔记,我目前使用 pintool 统计了一些程序的运行时信息,至于如何分析,这又是另一个难题了。

闭源软件我是真的没什么好总结的,官网文档写得不错,就这样吧。