# 首先

本文基于macOS27.0 Beta版(26A5406e),似乎是用了写Apple内部的东西,那么

本文仅供学习和交流使用,出现一切损失本人概不负责

# 一切的开始

原本只是刷b站,看到这个视频:

【苹果的功耗数字是假的?——我发现了powermetrics的一个漏洞-哔哩哔哩】 https://b23.tv/h2HWwYn

我就想着试试逆一下powermetrics

然后我把powermetrics提取出来发现这东西感觉像个空壳。。。

不过很明显功耗是假的(正如视频里说的)

大概就是:

P = ΔE / Δt

其中 ΔE 是相邻两个采样窗口之间,内核能量计数器的增量Δt 是窗口时长

那么,能量计数从哪来?

# 开始踩坑

截屏2026-09-02 21.35.56

根据我们的逆向结果,powermetrics用了IOReport这个库,我们用macho view看一眼load commands:

截屏2026-09-02 21.37.15

去这个路径找了一下,嗯?怎么没有

上网一查发现啊噗噜把系统库全扔dyld cache了,而且而以前负责把镜像从缓存里抠出来的 dyld_shared_cache_util也没了

好在缓存本体、dyld_info还在

而且dyld开源的,在这里

既然有格式、有数据、没有工具——那就自己写一个提取器

好吧我承认我是让ai写的

# 提取器的关键认知

这代缓存的布局比老版本复杂,几个坑值得一提:

  • 子缓存文件各自带完整的 dyld_v1 头和映射表,映射表里的 fileOffset 是相对该文件自己的偏移(不是相对整个缓存);
  • 主缓存头部里镜像表用的是新字段 imagesOffset(偏移 0x1C0),老字段 imagesOffsetOld 恒为 0;
  • macOS 新版缓存把 __DATA/__DATA_CONST/__LINKEDIT 拆进后缀为 .dylddata/.dyldreadonly 的独立文件,一个镜像的段散落在多个文件里,必须以"地址 → 映射表 → (文件, 偏移)"的全局方式解析,而不是按文件顺序猜;
  • arm64e 镜像代码里有大量 auth 相关指令(pacibsp/braa/retab),段内节偏移字段还带着高位垃圾——重建文件时节的 offset 必须按 vmaddr 差值重算。

重建出的文件:

__TEXT       0x100000000
__DATA_CONST 0x14E044B60
__AUTH_CONST 0x156337670  //含全部键常量 CFString

为了让 Hopper 能直接看到函数名,从 dyld_info 导出表合成了一份 LC_SYMTAB 注入文件,好消息:验证结果很干净!

# 然后我们开逆

打开hopper第一个发现就很有意思:IOReport的用户态对象模型没有任何私有CFRuntime类型,全部是 CFDictionary

比如IOReportChannelGetGroup:

截屏2026-09-02 21.43.29

你会发现怎么这函数就这么点东西

0x156337A80 落在__AUTH_CONST.__cfstring区——这是一个 CFString 常量对象。把那一整片__cfstring解码出来,IOReport 的"字典键词典"就全摊在眼前了

这里我还是让ai写了个表

ai真的太好用了你知道吗

常量 含义
IOReportChannels / IOReportDrivers / IOReportChannelGroups 样本信封的三层键
ChannelName / IOReportGroupName / IOReportSubGroupName / DriverName / DriverID 通道身份
LegendChannel 通道记录数组(ChannelID 就在里面)
RawElements 真正的数值载体
SimpleReport / StateReport / HistogramReport 三种格式
StateNames / Residency / DutyCycle / InTransitions / StateID / StateName State 记录字段
BucketIndex / LowBound / HighBound / BucketHits / BucketMin / BucketMax / BucketSum Histogram 记录字段
Format / Unit / UnitLabel / UnitScale / BaseUnitLabel 元数据

顺着一串 getter 的汇编读下去,大概还原出来的伪代码:

channel = CFDictionary {
    "ChannelName"   : "DIE_0_EACC0_CPU",
    "IOReportGroupName" : "Energy Model",
    "Format"        : 1,
    "LegendChannel" : [ 通道记录... ]   // ChannelID = 数组元素
    ...
}

然后是RawElements

截屏2026-09-02 21.47.46

还原下:

sample["RawElements"]          // 是一块 CFData
→ CFDataGetBytePtr             // 得到记录区
→ record[0x10] == 1            // 格式码: SimpleInteger
→ return *(int64_t*)(record+0x20)  ; // 计数值!

也就是说,样本的机器可读数值是一段定长二进制记录(每 64 字节一条,塞在 CFData 里),CFDictionary只是外壳

之前我看过powermtrics,powermetrics 能量回调里IOReportSimpleGetIntegerValue拿到的增量,就是记录+0x20的那个值;实测标定 1 count约等于1 mJ

# 最坑的部分!!!

别信伪代码里的函数名。

你自己看一下就会发现Hopper 伪代码里所有跨镜像调用的名字都是错的

IOReport 的代码调用 CoreFoundation/IOKit 函数时,反汇编显示为 bl 0x10543d080 这类地址

这是新版 dyld 缓存的"统一跳板":一段adrp/add/ldr/braa的认证跳转序列,目标函数指针存在一片共享 GOT 里

因为这事我又去问ai了

ai真的太好用了你知道吗

ai干的活是:

  1. 从全量反汇编提取 85 个外部 bl 目标;
  2. 逐个解析跳板指令,算出 GOT 槽地址;
  3. 槽里是 auth 链式指针(形如 0x8014...:低 32 位 offset + 认证位);
  4. 按"offset + 共享区基址"解读,再用 CF/IOKit 导出表反查函数名。

然后就翻车了,85个名字和 dyld_info 导入表零重合——全是CFURLCopyScheme、CFRunLoopAddSource这种明显不可能被IOReport调用的函数

不过代码还算能读,问题不大

# 调用链

graph LR
    K[kernel 能量计数器] -->|raw| R[IOReportCreateSamples 快照]
    R -->|两次快照| D[IOReportCreateSamplesDelta]
    D -->|逐通道记录| I[IOReportIterate 回调]
    I --> V[SimpleGetIntegerValue 取 +0x20]
    V --> P[P = ΔE / Δt  →  mW]

依旧ai

ai真的太好用了你知道吗

# 附成果

嗯还是ai

# element布局

// CFData 内容 = n 个 64 字节元素,元素 0 从 buffer+0x00 开始
struct raw_sample_buffer {
    // 每个元素 0x40 字节;负载区在 元素+0x20 .. 0x3F
};

// 格式标记(format) —— 1=simple, 2=state, 3=histogram, 4=array
//   以字节形式存在 buffer+0x10(元素 0 的头部区)
// 元素个数 = CFDataGetLength() >> 6
// 取值失败哨兵:0x8000000000000000("无值/不可用")
// double 路径失败哨兵:0x7ff8000000000000(quiet NaN)

# 数据/代码位置

// libIOReport
#define kIOReportRawElementsKey  0x156337920   // CFString 常量地址
// powermetrics 全局(数据段)
//   g_timebase_ratio       : tick→ns 比例(≈41.67)
//   g_elapsed_ticks        : 当前窗口 tick 数
//   g_gpu/ane/dram/dcs_energy_delta
//   g_gpu_energy_channel_id
//   g_gpu_energy_scale_div1e6
//   能量通道 id 名单:ANE  表基址 0x10001d018(嵌套结构,计数 dword_10001e0d8)
//                    DRAM 表基址 0x10001d060(flat,  计数 dword_10001e0d8)
//                    DCS  表基址 0x10001d080(flat,  计数 dword_10001e0d8)
//                    CPU cluster 表基址 0x10001d0d0 + n*0x100
//                        +0x6A : 该 cluster 是否有能量通道(==1)
//                        +0x80 : 能量通道 channel_id
//                        +0x90 : ΔE(窗口能量差,raw count)
//                        +0xA8 : active residency (double)

# libIOReport基础访问器

# 0x100003818(sample_raw_record_lookup)

// 取样本字典里的原始元素缓冲区并校验
// 返回元素缓冲区指针;失败返回 NULL,并把 *out_count 置 0
static uint8_t *sample_raw_record_lookup(CFDictionaryRef sample,
                                         uint16_t *out_count,
                                         uint8_t  want_format)
{
    CFDataRef data = CFDictionaryGetValue(sample, kIOReportRawElementsKey);
    if (!data) return NULL;
    if (CFGetTypeID(data) != CFDataGetTypeID()) return NULL;

    if (CFDataGetLength(data) < 0x40) return NULL;        // 至少 1 个元素

    uint8_t *bytes = CFDataGetBytePtr(data);
    if (bytes[0x10] != want_format) return NULL;          // 格式标记不符

    *out_count = (uint16_t)(CFDataGetLength(data) >> 6);  // 元素个数 = len/64
    return bytes;
}

# 0x100004aa0

// 从 channel legend 里取 format;失败返回 0xe00002bc
static kern_return_t channel_get_format(CFTypeRef channel, uint8_t *out_format)
{
    uint64_t f = legend_array_element(channel, 1);   // legend 槽位 1
    if (f == 0x8000000000000000) return 0xe00002bc;
    *out_format = f;
    return 0;
}

# 0x100005294(_IOReportSimpleGetIntegerValue)

// 读 simple 样本的唯一计数值(能量通道走的就是这个)
// out_unit 非空时顺带返回通道单位;失败/格式不对返回 0x8000000000000000
uint64_t IOReportSimpleGetIntegerValue(CFDictionaryRef sample, uint64_t *out_unit)
{
    uint16_t count;
    uint8_t *elem = sample_raw_record_lookup(sample, &count, 1);
    if (!elem || count != 1)  return 0x8000000000000000;
    if (elem[0x10] != 1)      return 0x8000000000000000;   // 非 simple

    uint64_t v = *(uint64_t *)(elem + 0x20);
    if (out_unit) *out_unit = IOReportChannelGetUnit(sample);
    return v;
}

# 0x100005a3c

// 把状态样本第 idx 个元素负载区(元素+0x20 起 32 字节)拷到 out
// residency    = out[0]        ← 元素+0x20
// in_transitions = out[1]      ← 元素+0x28
// (元素+0x30/+0x38 另有两个 qword,含状态 id 相关字段)
static kern_return_t state_element_load(CFDictionaryRef sample, uint32_t idx,
                                        uint64_t out[4])
{
    uint16_t count;
    uint8_t *elems = sample_raw_record_lookup(sample, &count, 2);
    if (!elems)                    return 0xe00002bc;
    if (!channel_is_format(sample, 2)) return 0xe00002bc;
    if (idx >= count)              return 0xe00002bc;

    memcpy(out, elems + idx*0x40 + 0x20, 32);
    return 0;
}

uint64_t IOReportStateGetResidency  (CFDictionaryRef s, uint64_t i) { uint64_t v[4]; return state_element_load(s,i,v) ? 0x8000000000000000 : v[0]; } // @0x100005d24
uint64_t IOReportStateGetInTransitions(CFDictionaryRef s, uint64_t i) { uint64_t v[4]; return state_element_load(s,i,v) ? 0x8000000000000000 : v[1]; } // @0x100005ce8

# 核心:0x100005f64(sample_element_delta_calc)

// 返回:0 = 成功;1 = 元素数不一致/某字段无效;0xe00002c2 = 格式不匹配;
//       0xe00002e6 = 未知格式
static kern_return_t
sample_element_delta_calc(CFDictionaryRef sample_a,   // arg0(r21):被减数侧…… 见注
                          CFDictionaryRef sample_b,   // arg1(r20):差值 = b − a
                          uint8_t *dst,               // arg2(r24):目标元素缓冲区
                          kern_return_t *err_out)     // arg3(r19):错误输出
{
    uint8_t fmt_a = IOReportChannelGetFormat(sample_a);
    uint8_t fmt_b = IOReportChannelGetFormat(sample_b);
    if (fmt_a == 0 || fmt_b == 0)                 goto fail_with(0xe00002c2);
    if (fmt_a != fmt_b) { if (err_out) *err_out = format_error(fmt_a); return 0xe00002c2; }

    uint16_t count_b;
    uint8_t *raw_b = sample_raw_record_lookup(sample_b, &count_b, fmt_a);
    uint16_t count_a;
    uint8_t *raw_a = sample_raw_record_lookup(sample_a, &count_a, fmt_a);
    if (!raw_b || !raw_a || count_a != count_b)   return 1;

    switch (fmt_a) {

    /* ---------- format 1:simple ---------- */
    case 1: {
        if (count_b != 1) return 1;
        uint64_t v_b = IOReportSimpleGetIntegerValue(sample_b, NULL);
        uint64_t v_a = IOReportSimpleGetIntegerValue(sample_a, NULL);
        if (v_b == kNoValue || v_a == kNoValue) return 0;   // 不写
        *(uint64_t *)(dst + 0x20) = v_b - v_a;              // ΔE 主体
        return 0;
    }

    /* ---------- format 2:state ---------- */
    case 2: {
        for (uint32_t i = 0; i < count_b; i++) {
            uint64_t eb[4], ea[4];
            // 任一侧读不到 → 整通道失败(不产生半截差)
            if (state_element_load(sample_b, i, eb)) return 1;
            if (eb[0] == kNoValue)                   return 1;
            if (state_element_load(sample_a, i, ea)) return 1;
            if (ea[0] == kNoValue)                   return 1;
            // eb[0]=in_transitions? … 精确槽位见 0.1;[INFERENCE] 差值写目标 0x28/0x30 一带
            *(uint64_t *)(dst + i*0x40 + 0x28) = eb[0] - ea[0];   // 状态字段差 1
            if (eb[1] == kNoValue)                   return 1;
            if (ea[1] == kNoValue)                   return 1;
            *(uint64_t *)(dst + i*0x40 + 0x30) = eb[1] - ea[1];   // 状态字段差 2
        }
        return 0;
    }

    /* ---------- format 3:histogram ---------- */
    case 3: {
        // 每 bucket 一个 0x40 元素:{hits, …(sum 类字段)…}
        for (uint32_t i = 0; i < count_b; i++) {
            uint64_t hits_b, sum_b, hits_a, sum_a;
            if (bucket_load(sample_b, i, &hits_b, &sum_b)) return 1;
            if (hits_b == kNoValue)                   return 1;
            if (bucket_load(sample_a, i, &hits_a, &sum_a)) return 1;
            if (hits_a == kNoValue)                   return 1;

            uint64_t dh = hits_b - hits_a;
            *(uint64_t *)(dst + i*0x40 + 0x20) = dh;         // Δhits

            if (dh == 0) {
                *(uint64_t *)(dst + i*0x40 + 0x38) = kNoValue; // 0 命中 → sum 置无效
            } else {
                if (sum_b == kNoValue) return 1;
                uint64_t s = sum_b;                            // Δsum = sum_b − sum_a
                if (hits_a != 0) {                             // 仅当旧窗有命中才减
                    if (sum_a == kNoValue) return 1;
                    s -= sum_a;
                }
                *(uint64_t *)(dst + i*0x40 + 0x38) = s;
            }
        }
        return 0;
    }

    /* ---------- format 4:array ---------- */
    case 4: {
        // 每元素 4 个 lane,逐 lane 求差;哨兵规则:能减则减,
        // 新值无效取旧值,旧值无效取新值
        for (uint32_t i = 0; i < count_b; i++) {
            for (uint32_t lane = 0; lane < 4; lane++) {
                uint32_t p = i*4 + lane;
                uint64_t v_a = IOReportArrayGetValueAtIndex(sample_a, p);
                uint64_t v_b = IOReportArrayGetValueAtIndex(sample_b, p);
                uint64_t d  = v_b - v_a;
                if (v_b == kNoValue) d = v_a;
                if (v_a == kNoValue) d = v_b;
                *(uint64_t *)(dst + i*0x40 + 0x20 + lane*8) = d;
            }
        }
        return 0;
    }

    default:
        return 0xe00002e6;      // unknown report format
    }
}

注:内部一律Δ = sample_b − sample_a(第二个传入参数是被减的对象),调用方(wrapper/引擎)保证传入顺序使Δ= 新−旧,powermetrics侧语义即current − previous

# 差值调用链

# 0x10000644c(_IOReportCreateSamplesDelta)

// 公开 API:对两批样本逐通道求差,结果放进新的差值样本集合void IOReportCreateSamplesDelta(CFDictionaryRef samples_a,   // 旧                                CFDictionaryRef samples_b,   // 新                                CFDictionaryRef *out_delta){    // 构造 block 迭代器(块体 = sub_1000064b8,描述符 @0x14e044ce0)    block_ctx ctx = { .func = sub_1000064b8, .out = out_delta };    samples_pairwise_delta_engine(samples_a, samples_b, &ctx);}

# 0x100002310(samples_pairwise_delta_engine)

// 配对差值引擎:对(a,b)两批样本里的每个通道做一次差值,生成差值样本集合
static kern_return_t samples_pairwise_delta_engine(CFDictionaryRef a,
                                                   CFDictionaryRef b,
                                                   block_ctx *ctx)
{
    iterator_state st = {};                  // 0x50 字节状态
    // ctx->func 经 sub_100002368 包装成 engine 的"每通道处理器"
    kern_return_t err = samples_iteration_engine(b, a, &st, NULL);
    return (err != 0) ? 0 : st.count_mismatch;   // [核实头部/返回逻辑]
}

# 0x100002368

// 引擎回调:一对 (sample_a, sample_b) 通道样本 → sample_element_delta_calc
static void delta_worker(CFDictionaryRef a, CFDictionaryRef b, block_ctx *ctx)
{
    if (ctx->skip_flag & 1) return;                 // ctx+0x38 低 1 位
    if (!a || !b) return;
    // 类型核对(CF 类型 / 集合类型)
    if (CFGetTypeID(a) != kDictTypeID) return;
    if (CFGetTypeID(b) != kDictTypeID) return;

    // 元素数核对:count_a == count_b,不等则整对跳过并置错
    uint16_t ca = element_count_of(a);
    uint16_t cb = element_count_of(b);
    if (ctx->count_check && ca != cb) { ctx->err = 0x111; return; }

    // 目标差值元素缓冲:按 count 建 CFData(空元素)
    // 逐元素: sample_element_delta_calc(b, a, dst, &err)   ← 参数顺序见 2 节注
    ...
    if (err) ctx->err |= err;
}

# 0x10000119c(samples_iteration_engine)

// 迭代引擎:遍历一批样本的通道;对每个通道在另一批样本里找配对通道,
// 找到后调用迭代器给定的 4 个 format 处理器槽(simple/state/histogram/array)。
static kern_return_t
samples_iteration_engine(CFDictionaryRef samples_cur,  // 待遍历
                         CFDictionaryRef samples_other,// 配对来源(可为 NULL)
                         iterator_state *it,           // 处理器/上下文
                         kern_return_t *out_err)
{
    if (!samples_cur && !it->processor3) return err_bad(0x111);

    // 空集合/无通道处理器的快速路径
    if (!it->proc_simple && !it->proc_state && !it->proc_histogram && !it->proc_array)
        return 0;

    // —— 遍历通道(基于 channel legend / samplesDict 展开,含组/子组字典)——
    for (channel in collect_channels(samples_cur)) {
        // 配对:在 samples_other 里找 (driver_id, channel_id) 相同的通道
        //   —— 引擎中部核实片段:反复 GetChannelID/GetDriverID 比对,
        //      带"前向搜索、跳过不匹配"内层 while 循环
        Channel *mate = find_matching_channel(samples_other, channel); // [部分还原]

        // 有 mate:调 mate 槽;无 mate:调"仅当前样本"槽(如 raw 迭代)
        switch (channel.format) {
        case 1:  err = it->proc_simple ? call(it->proc_simple, channel, mate, it) : 0; break;
        case 2:  err = it->proc_state  ? call(it->proc_state,  channel, mate, it) : 0; break;
        case 3:  err = it->proc_hist   ? call(it->proc_hist,   channel, mate, it) : 0; break;
        case 4:  err = it->proc_array  ? call(it->proc_array,  channel, mate, it) : 0; break;
        default: err = 0x10;                       // 处理器返回 0x10 = 跳过
        }
        if (err == 0x10) { /* 该通道跳过,计数器回退(引擎尾部核实) */ }
        if (err & 1) { /* 置整体错误 */ }
    }
    return it->err;
}

IOReportIterate(0x100001160)=该引擎+用户回调的薄封装,遍历样本、逐通道回调(每次回调传通道对象),错误码原样上抛

# 占空比与单位换算

# 0x100005d60(_IOReportStateGetDutyCycle)

// duty_cycle(i) = residency(i) / Σ_k residency(k)
// 任何一步失败或总和为 0 → NaN(0x7ff8000000000000)
double IOReportStateGetDutyCycle(CFDictionaryRef sample, uint64_t state_index)
{
    if (!sample) return kNan;
    uint8_t fmt;
    if (channel_get_format(sample, &fmt)) return kNan;
    if (fmt != 2) return kNan;                    // 只对 state 通道

    uint16_t n = state_count(sample);             // 元素(状态)数
    if (n == 0) return kNan;

    double total = 0.0;
    for (uint32_t i = 0; i < n; i++) {
        uint64_t v[4];
        if (state_element_load(sample, i, v)) return kNan;
        total += (double)v[0];                    // residency
    }
    if (total == 0.0) return kNan;

    uint64_t v[4];
    if (state_element_load(sample, state_index, v)) return kNan;
    return (double)v[0] / total;                  // residency(i) / Σ
}

# 0x100006b28(_IOReportScaleValue)

// 按单位 id 做比例换算;返回 double;失败 → NaN
double IOReportScaleValue(uint64_t value, uint64_t unit_from, uint64_t unit_to)
{
    if (((unit_from ^ unit_to) >> 56) != 0) return kNan;   // 单位族(最高字节)不同,拒绝

    double a, b;
    if (unit_lookup(unit_from & 0xFFFFFFFFFFULL, &a, &b)) return kNan; // sub_100006cac
    double c, d;
    if (unit_lookup(unit_to   & 0xFFFFFFFFFFULL, &c, &d)) return kNan;

    return (double)value * a / b / c * d;         // 按调用序原样(缩放方向由比例表定义)
}

# powermetrics功耗组装

# 0x10000c438(ioreport_delta_samples)

// 参数(反编译核实):
//   slot_prev   : 上一批样本的持有槽(滚动)
//   slot_cur    : 当前样本槽
//   slot_saved  : 用于保存的槽
//   slot_delta  : 差值样本输出槽
//   channels    : 该域通道集合
//   options     : 采样选项
static void ioreport_delta_samples(void *slot_prev, void *slot_cur,
                                   void *slot_saved, void *slot_delta,
                                   CFArrayRef channels, uint64_t options)
{
    // 滚动:上一批先释放,再把"当前"挪进 prev(若开启连续模式)
    if (config.is_continuous) {
        CFRelease(*slot_prev);
        *slot_prev = *slot_cur;
    }

    // 新采样
    *slot_cur = IOReportCreateSamples(channels, options, NULL);   // libIOReport
    if (!*slot_cur) { perror_exit("IOReportCreateSamples"); }

    // 有上一批就求差
    if (*slot_prev && *slot_cur)
        *slot_delta = IOReportCreateSamplesDelta(*slot_prev, *slot_cur); // libIOReport
}

# 0x100009d54(sample_processor_power_stats)

static void sample_processor_power_stats(void)
{
    if (!config.cpu_power_enabled) return;

    // 窗口起点时间(ticks)
    *g_elapsed_ticks = mach_absolute_time();        // 首次;后续由主循环维护

    // 5 组 (prev/cur/delta 槽位,channels) —— 数据段地址:
    //   [0x10001e170/178/180 → qword_10001e188]   组1(域 A)
    //   [0x10001e190/198/1a0 → qword_10001e1a8]   组2(域 B)
    //   [0x10001e1b0/1b8/1c0 → qword_10001e1c8]   组3(CPU cluster 能量)
    //   [0x10001e1d0/1d8/1e0 → qword_10001e1e8]   组4(条件:dword_10001e140!=0)
    //   [0x10001e1f0/1f8/200 → qword_10001e208]   组5(条件:dword_10001e158!=0)
    // 各组 channels/options 取 *qword_10001e0e8, *qword_10001e0f0 等
    ioreport_delta_samples(&0x10001e170, &0x10001e178, &0x10001e180, &qword_10001e188,
                           *qword_10001e0e8, *qword_10001e0f0);
    ... // 其余组同构

    // 每窗口清空 cluster 累计槽
    for (n = 0; n < cluster_count; n++) {
        bzero(&cluster[n].accum, ...);              // 0x10001d180 一带
        cluster[n].freq_accum = 0;
    }

    // 对组3 差值样本(*qword_10001e1c8)执行 IOReportIterate,
    // 遍历块挂数据段 0x100018f50(CPU cluster 能量块) → 见 5.3
    IOReportIterate(*qword_10001e1c8, block_at_0x100018f50);
    // 组2/组4(ANE/GPU 等)差值样本同理,块在 0x100018f70 / 0x100018f90 …(0x100018fb0 局部块 sub_10000b4f4)
    IOReportIterate(*qword_10001e1a8, block_at_0x100018f70);

    // 收尾:每个 cluster 算 active residency(double)存 +0xA8
    for (n = 0; n < cluster_count; n++)
        cluster[n].active_residency =
            freq_weighted_sum(cluster[n].freq_hist, ...);   // sub_10000c870
}

# 0x10000b228(ioreport_energy_channel_block)

// 作为 IOReportIterate 的块体,对差值样本里的每个通道调用一次
// (channel 即差值样本中的一个通道对象;SimpleGetIntegerValue 读到的是 ΔE)
static int ioreport_energy_channel_block(void *self, CFDictionaryRef channel)
{
    uint64_t id = IOReportChannelGetChannelID(channel);

    /* --- GPU:单个能量通道 --- */
    if (id == *g_gpu_energy_channel_id && gpu_enabled()) {
        *g_gpu_energy_delta = IOReportSimpleGetIntegerValue(channel, NULL);
        return 0;
    }

    /* --- ANE:嵌套通道组表 (0x10001d018) --- */
    if (ane_enabled()) {
        for (each group g in ane_table /* 0x10001d018,count dword_10001e0d8 */)
            for (each member_id m in g->ids)
                if (id == m) { *g_ane_energy_delta +=
                                   IOReportSimpleGetIntegerValue(channel, NULL);
                               return 0; }
    }

    /* --- DRAM:flat 表 (0x10001d060,count dword_10001e0d8) --- */
    if (dram_enabled())
        for (i = 0; i < dram_count; i++)
            if (id == dram_ids[i]) { *g_dram_energy_delta +=
                                           IOReportSimpleGetIntegerValue(channel, NULL);
                                     return 0; }

    /* --- DCS:flat 表 (0x10001d080) --- */
    if (dcs_enabled())
        for (i = 0; i < dcs_count; i++)
            if (id == dcs_ids[i]) { *g_dcs_energy_delta +=
                                           IOReportSimpleGetIntegerValue(channel, NULL);
                                     return 0; }

    /* --- CPU cluster:0x10001d0d0 + n*0x100 --- */
    for (n = 0; n < cluster_count; n++) {
        if (cluster[n].has_energy_channel /* +0x6A==1 */ &&
            cluster[n].energy_channel_id  /* +0x80 */ == id) {
            cluster[n].energy_delta /* +0x90 */ =
                IOReportSimpleGetIntegerValue(channel, NULL);
            break;
        }
    }
    return 0;
}

# 0x100009244(display_processor_power_stats)

static void display_gpu_power_stats(void)
{
    if (!config.gpu_enabled) return;
    bool plist = (output_mode == 1);

    // GPU HW active frequency(由各频点 tick 加权,sub_10000c870)存 double_10001c1b8
    double freq = weighted_freq();
    // active residency = (总 − idle)/总 × 100 …(residency 来自状态样本差值)

    if (!plist) {
        fputs("\n**** GPU usage ****\n\n", out);
        fprintf(out, "GPU HW active frequency: %0.0f MHz\n", freq);
        fprintf(out, "GPU HW active residency: %6.2f%% (", active_pct);
        for (each dvfm 档 i) fprintf(out, "%0.0f MHz: %s%%%s", f[i], pct(i), sep(i));
        fprintf(out, "GPU idle residency: %6.2f%%\n", idle_pct);

        // 与 5.4 同形:double_10001c208 = g_gpu_energy_delta(double 化),
        //           double_10001c2a0 = 窗口时长(ticks,double 化)
        fprintf(out, "GPU Power: %0.0f mW\n",
                *double_10001c208 / *g_timebase_ratio * *double_10001c2a0 / 1e9);
    } else {
        fprintf(out, "<key>gpu_energy</key><integer>%llu</integer>\n",
                (uint64_t)*double_10001c208);
        // …(plist 分支输出 dvfm 数组/频率/驻留)
    }
}

# 0x100003d54(main_sample_loop)

static void main_sample_loop(void)
{
    // 189 个基本块 / 3712 字节;按采样间隔循环:
    for (;;) {
        wait_until_next_interval();               // nanosleep 等(导入表核实)

        // 1) 更新窗口时间基准
        //    g_elapsed_ticks = 本次窗口 tick 数(连续采样时 mach_continuous_time 差)

        // 2) 各采样器:
        if (cpu_power_sampler)   sample_processor_power_stats();  // 5.2
        if (gpu_sampler)         gpu_sample_flow();               // GPU 域,同 5.2 结构
        //    … 其他采样器(battery/thermal/misc)→ 与功耗主题无关,略

        // 3) 显示:
        if (cpu_power_sampler)   display_processor_power_stats(); // 5.4
        if (gpu_sampler)         display_gpu_power_stats();       // 5.5

        fflush(out);
    }
}
c

输出开关/文件:模式dword_10001c0f8(0=文本,1=plist),流*qword_10001c0c8,域开关:byte_10001d012(GPU)、byte_10001d010(ANE)、byte_10001e160(CPU power)等,时间基准结构 @0x10001c088:+0x0(模式)、+0x8timebase ratio(≈g_timebase_ratio)

# 地址表

#

二进制 地址 符号 还原程度
libIOReport 0x100003818 sample_raw_record_lookup 核实
libIOReport 0x100005294 IOReportSimpleGetIntegerValue 核实
libIOReport 0x100005a3c state 元素加载 核实
libIOReport 0x100005ce8 / 0x100005d24 StateGetInTransitions / Residency 核实
libIOReport 0x100005d60 IOReportStateGetDutyCycle 核实
libIOReport 0x100005f64 sample_element_delta_calc 核实
libIOReport 0x100006b28 IOReportScaleValue 核实
libIOReport 0x10000644c IOReportCreateSamplesDelta 核实
libIOReport 0x100002310 samples_pairwise_delta_engine 核实
libIOReport 0x100002368 delta worker 核实
libIOReport 0x10000119c samples_iteration_engine 部分还原
powermetrics 0x100003d54 main_sample_loop 部分还原
powermetrics 0x100009d54 sample_processor_power_stats 核实
powermetrics 0x10000c438 ioreport_delta_samples 核实
powermetrics 0x10000b228 ioreport_energy_channel_block 核实
powermetrics 0x100009244 display_processor_power_stats 核实
powermetrics 0x100000c5c display_gpu_power_stats 核实