# 首先
本文基于macOS27.0 Beta版(26A5406e),似乎是用了写Apple内部的东西,那么
本文仅供学习和交流使用,出现一切损失本人概不负责
# 一切的开始
原本只是刷b站,看到这个视频:
【苹果的功耗数字是假的?——我发现了powermetrics的一个漏洞-哔哩哔哩】 https://b23.tv/h2HWwYn
我就想着试试逆一下powermetrics
然后我把powermetrics提取出来发现这东西感觉像个空壳。。。
不过很明显功耗是假的(正如视频里说的)
大概就是:
P = ΔE / Δt
其中 ΔE 是相邻两个采样窗口之间,内核能量计数器的增量Δt 是窗口时长
那么,能量计数从哪来?
# 开始踩坑

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

去这个路径找了一下,嗯?怎么没有
上网一查发现啊噗噜把系统库全扔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:

你会发现怎么这函数就这么点东西
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

还原下:
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干的活是:
- 从全量反汇编提取 85 个外部
bl目标; - 逐个解析跳板指令,算出 GOT 槽地址;
- 槽里是 auth 链式指针(形如
0x8014...:低 32 位 offset + 认证位); - 按"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 | 核实 |