惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
P
Privacy International News Feed
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
Cisco Talos Blog
Cisco Talos Blog
S
Schneier on Security
Project Zero
Project Zero
T
Threatpost
Spread Privacy
Spread Privacy
阮一峰的网络日志
阮一峰的网络日志
C
Cybersecurity and Infrastructure Security Agency CISA
AWS News Blog
AWS News Blog
H
Heimdal Security Blog
V
Visual Studio Blog
Google DeepMind News
Google DeepMind News
P
Privacy & Cybersecurity Law Blog
J
Java Code Geeks
罗磊的独立博客
博客园 - Franky
博客园 - 叶小钗
S
Security Affairs
月光博客
月光博客
Application and Cybersecurity Blog
Application and Cybersecurity Blog
The Last Watchdog
The Last Watchdog
WordPress大学
WordPress大学
人人都是产品经理
人人都是产品经理
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
A
Arctic Wolf
Cloudbric
Cloudbric
www.infosecurity-magazine.com
www.infosecurity-magazine.com
V2EX - 技术
V2EX - 技术
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LINUX DO - 最新话题
Y
Y Combinator Blog
宝玉的分享
宝玉的分享
酷 壳 – CoolShell
酷 壳 – CoolShell
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
N
News | PayPal Newsroom
Hugging Face - Blog
Hugging Face - Blog
美团技术团队
W
WeLiveSecurity
云风的 BLOG
云风的 BLOG
The Register - Security
The Register - Security
I
InfoQ
F
Fortinet All Blogs
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
Recent Announcements
Recent Announcements
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
L
Lohrmann on Cybersecurity

Peng's Blog

Vibe paper不负责指北 SPFC参考文献阅读《CONGA》 读《Uno:数据中心间和数据中心内拥塞控制与可靠连接的一站式解决方案》 理解《From ATOP to ZCube》中的ZCube网络拓扑 集合通信原语学习 Zotero在Wayland下侧边栏无法拖动的解决办法 再读CAVER 我读的第一篇论文--NSDI_26_CAVER 在archlinux下使用万象拼音实现更智慧的中文输入法 Traefik实现单Docker容器托管多个静态网站 Pengs.top浴火重生!备案、服务器迁移与全站Docker化 严谨与温度并存:北航宋友老师程设课的学习感悟 Android Studio全流程换源指南 解决linux系统下pipewire作为音频服务器持续播放声音中断问题 在ThinkBook 16+ 2025上安装archlinux驯服记录【基本信息与触摸板修复】 「行云流水」在挂载移动硬盘时自动启动Syncthing开始同步的丝滑体验 自建图床和PicGo闹不愉快?干脆自己写个脚本,深度整合到KDE右键菜单! btrfs各压缩等级速率测试脚本,选择最适合你的压缩级别 别当冤大头,Adobe全家桶破解源头方案:修图剪辑不求人,盗版网站也得从你这儿进货! 轻松配置aria2下载器,通过systemd设置开机自启并与浏览器集成 Git Commit 规范指南
Lenovo Legion Y9000P 在 Linux 下只有 30W:从现象、WMI 到驱动补丁
Pengbo · 2026-04-29 · via Peng's Blog

本文题图为GPT-Image-2生成

这篇文章描述的逆向与代码编写过程全部由Claude Code完成,你可以认为本文的“我”也是GPT-5.5。文中的电脑是我室友的在2025年购买的拯救者Y9000P。官方驱动对温控支持不佳,所以我们消耗了大量token自己修复了。为了减少重复劳动,我将思路记录于此,当然本文也是AI写就

Fastfetch

这次问题发生在一台 Lenovo Legion Y9000P IAX10 上,系统是 Arch Linux。

现象很直接:插着电,跑 CPU 压力测试,风扇会转,但不怎么激动;频率掉到 2GHz 出头;CPU package power 长时间只有 30W 左右。

CPU package power 指整个 CPU 封装的功耗,不是单个核心的功耗。对 HX 级别平台来说,满载 30W 显然不正常。最容易怀疑的是风扇、散热、Linux 电源模式或者 RAPL 限制,但最后问题落在了联想 WMI 驱动和固件接口的细节上。

本文不只记录最后的命令,还会说明每个名字、method、feature ID 是从哪里来的。否则只写“method 17 是 GetFeatureValue”没有意义,读者看完也没法迁移到自己的机器上。

先说结论

最终定位到两层问题:

  1. max-power 不是这台机器上真正允许写 CPU PPT 的状态。Lenovo GameZone provider 的 custom profile 对应 firmware thermal mode 0xff,而 max-power 对应 0xe0
  2. lenovo-wmi-other 驱动在 current_value get/set 路径里使用了带 mode/type 的 attribute ID,但本机固件的 WMI method 17/18 要求的是 plain device+feature ID。

修补 lenovo-wmi-other 后,通过标准 sysfs 写入:

1
2
ppt_pl1_spl=145
ppt_pl2_sppt=210

再用直接 WMI probe 读回:

1
2
0x01010000 -> 210
0x01020000 -> 145

最后压力测试中,CPU package power 从原来的 30W 提升到约 170W 持续区间。

先把调查对象讲清楚

实际排查主要用了四类证据:

  1. sysfs 暴露的当前状态,例如 RAPL、platform profile、firmware attributes;
  2. 当前内核模块信息,例如 modinfo 里的 WMI GUID alias;
  3. Linux 驱动源码和旧 legion_laptop DKMS 源码里的常量;
  4. 一个只读 WMI probe 模块,直接调用 wmi_evaluate_method() 验证固件实际返回值。

第一步:确认不是 RAPL sysfs 把 CPU 锁在 30W

RAPL 是 Intel 的 Running Average Power Limit 机制,负责 CPU 功耗限制和能耗统计。Linux 会把一部分 RAPL 状态暴露到:

1
/sys/class/powercap/intel-rapl:0/

查看方式很简单:

1
2
3
4
5
for f in /sys/class/powercap/intel-rapl:0/enabled \
/sys/class/powercap/intel-rapl:0/name \
/sys/class/powercap/intel-rapl:0/constraint_*; do
[ -e "$f" ] && printf '%s=' "$f" && cat "$f"
done

记录到的关键输出是:

1
2
3
4
5
/sys/class/powercap/intel-rapl:0/name=package-0
/sys/class/powercap/intel-rapl:0/constraint_0_name=long_term
/sys/class/powercap/intel-rapl:0/constraint_0_power_limit_uw=165000000
/sys/class/powercap/intel-rapl:0/constraint_1_name=short_term
/sys/class/powercap/intel-rapl:0/constraint_1_power_limit_uw=210000000

单位是 microwatt,所以这里是 165W / 210W。RAPL sysfs 里没有 30W。

这一步只能排除“显式 RAPL sysfs 限制为 30W”,不能排除所有固件功耗策略。笔记本厂商还可能通过 EC、WMI、BIOS thermal policy、平台 profile 等路径限制 CPU。

顺便解释一下 PL1/PL2:PL 是 power limit。PL1 通常是长期持续功耗,PL2 通常是短时睿频功耗,这是Intel的表述。不同厂商会换名字,比如 SPL、SPPT、FPPT,但本质仍然是围绕 CPU package 的功耗窗口。

第二步:找出当前机器加载了哪些 Lenovo WMI 驱动

先看模块:

1
lsmod | grep -E '^(lenovo_wmi|legion_laptop|platform_profile|intel_rapl|processor_thermal|intel_pstate)'

关键输出:

1
2
3
4
5
6
7
lenovo_wmi_gamezone
lenovo_wmi_other
lenovo_wmi_helpers
lenovo_wmi_capdata01
firmware_attributes_class
legion_laptop
platform_profile

接着看模块信息:

1
modinfo lenovo_wmi_gamezone lenovo_wmi_other lenovo_wmi_capdata01 lenovo_wmi_helpers

这里第一次把两个重要名字和 GUID 对上:

1
2
3
4
5
6
7
8
9
10
filename: .../lenovo-wmi-gamezone.ko.zst
description: Lenovo GameZone WMI Driver
alias: wmi:887B54E3-DDDC-4B2C-8B88-68A26A8835D0

filename: .../lenovo-wmi-other.ko.zst
description: Lenovo Other Mode WMI Driver
alias: wmi:DC2A8805-3A8C-41BA-A6F7-092E0089CD3B

description: Lenovo Capability Data 01 WMI Driver
alias: wmi:7A8F5407-CB67-4D6E-B547-39B3BE018154

这几个名字是什么意思?

GameZone 是联想在游戏本上使用的一组 WMI 接口。Linux 里的 lenovo-wmi-gamezone 驱动使用 GUID 887B54E3-DDDC-4B2C-8B88-68A26A8835D0 绑定到这组接口,主要处理 thermal mode、platform profile、部分风扇/性能模式相关功能。

Lenovo Other 是另一组 WMI 接口。Linux 里的 lenovo-wmi-other 驱动使用 GUID DC2A8805-3A8C-41BA-A6F7-092E0089CD3B 绑定到它。CPU/GPU power limit 这类 firmware attributes 就是通过这个驱动暴露出来的。

capdata01 是 Capability Data 01。可以把它理解成“固件能力描述表”的 WMI 侧数据来源。lenovo-wmi-other 需要通过 capdata 查询 ppt_pl1_spl 这类 attribute 的范围、默认值和步进。

也就是说,GameZone 管“当前是什么热/性能模式”,Lenovo Other 管“有哪些可调参数和怎么读写”,capdata01 给这些参数提供范围信息。

第三步:看 platform profile,区分汇总入口和 provider 入口

Linux 有一个通用 platform profile 接口:

1
2
cat /sys/firmware/acpi/platform_profile
cat /sys/firmware/acpi/platform_profile_choices

这台机器输出:

1
2
custom
low-power balanced performance max-power custom

但这个路径是汇总入口。实际提供 profile 的可能不止一个驱动,所以还要看具体 provider。记录里用的是:

1
2
3
4
5
provider=/sys/devices/platform/PNP0C14:02/wmi_bus/wmi_bus-PNP0C14:02/887B54E3-DDDC-4B2C-8B88-68A26A8835D0-4/platform-profile/platform-profile-0
for f in "$provider"/name "$provider"/profile "$provider"/choices; do
printf '%s=' "$f"
cat "$f"
done

输出:

1
2
3
.../platform-profile-0/name=lenovo-wmi-gamezone
.../platform-profile-0/profile=custom
.../platform-profile-0/choices=low-power balanced performance max-power custom

这条长路径可以拆开读:

1
/sys/devices/platform/PNP0C14:02

PNP0C14 是 ACPI WMI 设备 ID。WMI 全称 Windows Management Instrumentation,很多笔记本厂商会把风扇、性能模式、电源限制等 OEM 功能放在 WMI 接口后面。Linux 支持这些功能时,通常是写一个 WMI driver 绑定到对应 GUID。

1
wmi_bus/wmi_bus-PNP0C14:02

Linux WMI 子系统为这个 ACPI WMI 设备创建的 bus 节点。

1
887B54E3-DDDC-4B2C-8B88-68A26A8835D0-4

Lenovo GameZone WMI GUID 对应的设备节点,后面的 -4 是内核枚举出的实例后缀。

1
platform-profile/platform-profile-0/profile

lenovo-wmi-gamezone 注册出的 platform profile provider。写这个文件,就是直接切 GameZone provider 的 profile。

实际脚本里不建议固定 PNP0C14:02platform-profile-0,因为这些编号可能变化。更稳妥的写法是按 GUID 和 name 找:

1
2
3
4
5
6
7
for p in /sys/devices/platform/*/wmi_bus/wmi_bus-*/887B54E3-DDDC-4B2C-8B88-68A26A8835D0-4/platform-profile/platform-profile-*/profile; do
[ -e "$p" ] || continue
name=${p%/profile}/name
if [ -r "$name" ] && [ "$(cat "$name")" = lenovo-wmi-gamezone ]; then
printf '%s\n' "$p"
fi
done

第四步:max-power 为什么不是 custom

最开始容易被 max-power 这个名字误导。按常识,它应该就是最高性能。实测不是。

max-power 下,日志显示:

1
before profile_provider=max-power aggregate=max-power pl1=0 pl2=0 pl3=0 pkg_w=30 fan1=2000 fan2=2000 temp1=55000 temp3=55000

直接写 provider 为 custom 后:

1
printf custom > /sys/devices/platform/PNP0C14:02/wmi_bus/wmi_bus-PNP0C14:02/887B54E3-DDDC-4B2C-8B88-68A26A8835D0-4/platform-profile/platform-profile-0/profile

功耗马上变化:

1
after_custom profile_provider=custom aggregate=custom pl1=0 pl2=0 pl3=0 pkg_w=91 fan1=2000 fan2=2000 temp1=75000 temp3=55000

还没写 PL,单是进入 custom,CPU package power 就从 30W 到了 91W。这说明 GameZone 的固件模式已经改变。

max-powercustom 的区别来自哪里?有两个来源。

第一个来源是旧 legion_laptop DKMS 源码。它不是最终方案依赖,但里面有较完整的联想 WMI 常量。通过下面的命令查:

1
2
grep -n 'LEGION_WMI_POWERMODE\|WMI_METHOD_ID_GETSMARTFANMODE\|WMI_METHOD_ID_SETSMARTFANMODE\|OtherMethodFeature_CPU\|ppt' \
/usr/src/lenovolegionlinux-1.0.0/legion-laptop.c | head -220

输出里有:

1
2
3
4
5
6
7
8
#define WMI_METHOD_ID_GETSMARTFANMODE 45
#define WMI_METHOD_ID_SETSMARTFANMODE 44
OtherMethodFeature_CPU_SHORT_TERM_POWER_LIMIT = 0x01010000
OtherMethodFeature_CPU_LONG_TERM_POWER_LIMIT = 0x01020000
LEGION_WMI_POWERMODE_QUIET = 1
LEGION_WMI_POWERMODE_BALANCED = 2
LEGION_WMI_POWERMODE_PERFORMANCE = 3
LEGION_WMI_POWERMODE_CUSTOM = 255

第二个来源是当前内核的 lenovo-wmi-gamezone 行为。通过驱动源码/反汇编可以确认 profile 到 firmware thermal mode 的映射:

1
2
3
4
5
low-power    -> 0x01
balanced -> 0x02
performance -> 0x03
max-power -> 0xe0
custom -> 0xff

firmware thermal mode 是联想固件内部使用的热/性能模式编号,不等于 Linux 展示给用户的字符串。max-power 对应 0xe0custom 对应 0xff,两者不是同一个状态。

这个映射还用 WMI probe 验证过。处于 max-power 时,GameZone method 45/55 返回:

1
2
y9000p_wmi_probe 887B54E3-DDDC-4B2C-8B88-68A26A8835D0 method=45 ... integer=224 hex=0xe0
y9000p_wmi_probe 887B54E3-DDDC-4B2C-8B88-68A26A8835D0 method=55 ... integer=224 hex=0xe0

进入 custom 后返回:

1
2
y9000p_wmi_probe 887B54E3-DDDC-4B2C-8B88-68A26A8835D0 method=45 ... integer=255 hex=0xff
y9000p_wmi_probe 887B54E3-DDDC-4B2C-8B88-68A26A8835D0 method=55 ... integer=255 hex=0xff

这样,max-power 不是 custom 就不再是猜测,而是驱动映射和固件返回共同证明的事实。

第五步:firmware attributes 是怎么冒出来的

当前系统里能看到:

1
2
3
for f in /sys/devices/virtual/firmware-attributes/lenovo-wmi-other-0/attributes/ppt_pl{1_spl,2_sppt,3_fppt}/{display_name,type,min_value,max_value,default_value,scalar_increment,current_value}; do
[ -e "$f" ] && printf '%s=' "$f" && timeout 1s cat "$f" 2>&1 | tr '\n' ' ' && printf '\n'
done

关键输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
ppt_pl1_spl/display_name=Set the CPU sustained power limit
ppt_pl1_spl/min_value=50
ppt_pl1_spl/max_value=145
ppt_pl1_spl/default_value=90

ppt_pl2_sppt/display_name=Set the CPU slow package power tracking limit
ppt_pl2_sppt/min_value=60
ppt_pl2_sppt/max_value=210
ppt_pl2_sppt/default_value=125

ppt_pl3_fppt/min_value=无效的参数
ppt_pl3_fppt/max_value=无效的参数
ppt_pl3_fppt/default_value=无效的参数

这些文件不是手写出来的,也不是 udev 规则生成的。它们由 lenovo-wmi-other 驱动注册到 Linux 的 firmware_attributes_class

驱动代码里有对应的 attribute 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static struct tunable_attr_01 ppt_pl1_spl = {
.device_id = LWMI_DEVICE_ID_CPU,
.feature_id = LWMI_FEATURE_ID_CPU_SPL,
.type_id = LWMI_TYPE_ID_NONE,
};

static struct tunable_attr_01 ppt_pl2_sppt = {
.device_id = LWMI_DEVICE_ID_CPU,
.feature_id = LWMI_FEATURE_ID_CPU_SPPT,
.type_id = LWMI_TYPE_ID_NONE,
};

LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl1_spl, "ppt_pl1_spl",
"Set the CPU sustained power limit");
LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl2_sppt, "ppt_pl2_sppt",
"Set the CPU slow package power tracking limit");
LWMI_ATTR_GROUP_TUNABLE_CAP01(ppt_pl3_fppt, "ppt_pl3_fppt",
"Set the CPU fast package power tracking limit");

ppt_pl1_splppt_pl2_sppt 的命名也来自驱动,不是本次排查临时造的名字。

这里的 CPU PPT 可以理解成 CPU package power tracking。ppt_pl1_spl 近似长期功耗限制,SPL 是 sustained power limit;ppt_pl2_sppt 近似短时/慢跟踪功耗限制,SPPT 是 slow package power tracking;ppt_pl3_fppt 是更快的峰值跟踪,但这台机器上读范围返回 EINVAL,因此没有写它。

第六步:为什么 custom 下才能写 PL

进入 custom 后写 PL:

1
2
3
write .../ppt_pl1_spl/current_value <- 90: ok
write .../ppt_pl2_sppt/current_value <- 125: ok
write .../ppt_pl3_fppt/current_value <- 210: failed: 无效的参数

而在 max-power 下写 PL 会返回 EBUSY。这不是 shell 的问题,而是驱动里的检查。

lenovo-wmi-otherattr_current_value_store() 在真正写 WMI 之前,会通过 notifier 问 lenovo-wmi-gamezone 当前 thermal mode:

1
2
3
4
5
6
ret = lwmi_om_notifier_call(&mode);
if (ret)
return ret;

if (mode != LWMI_GZ_THERMAL_MODE_CUSTOM)
return -EBUSY;

也就是说,PL 写入被驱动限制在 LWMI_GZ_THERMAL_MODE_CUSTOM 下。前面已经验证 custom=0xffmax-power=0xe0,所以 max-power 下返回 EBUSY 是符合代码逻辑的。

这一步把现象、驱动代码、WMI 返回值连起来了。

第七步:method 17/18 和 feature ID 从哪里来

前面提到:

1
2
3
4
17  GetFeatureValue
18 SetFeatureValue
0x01010000 CPU short-term / SPPT / PL2-like limit
0x01020000 CPU long-term / SPL / PL1-like limit

这些数字有两个来源。

第一个来源是 Linux 当前驱动源码。wmi-other.c 里定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define LENOVO_OTHER_MODE_GUID "DC2A8805-3A8C-41BA-A6F7-092E0089CD3B"

#define LWMI_DEVICE_ID_CPU 0x01

#define LWMI_FEATURE_ID_CPU_SPPT 0x01
#define LWMI_FEATURE_ID_CPU_SPL 0x02
#define LWMI_FEATURE_ID_CPU_FPPT 0x03

#define LWMI_FEATURE_VALUE_GET 17
#define LWMI_FEATURE_VALUE_SET 18

#define LWMI_ATTR_DEV_ID_MASK GENMASK(31, 24)
#define LWMI_ATTR_FEAT_ID_MASK GENMASK(23, 16)
#define LWMI_ATTR_MODE_ID_MASK GENMASK(15, 8)
#define LWMI_ATTR_TYPE_ID_MASK GENMASK(7, 0)

这里能得出:

1
2
method 17 = LWMI_FEATURE_VALUE_GET
method 18 = LWMI_FEATURE_VALUE_SET

第二个来源是旧 legion_laptop DKMS 源码。前面的 grep 输出里能看到:

1
2
3
OtherMethodFeature_CPU_SHORT_TERM_POWER_LIMIT = 0x01010000
OtherMethodFeature_CPU_LONG_TERM_POWER_LIMIT = 0x01020000
OtherMethodFeature_CPU_PEAK_POWER_LIMIT = 0x01030000

Linux 驱动里的 bit mask 也能算出相同结果:

1
2
SPPT: device_id=0x01, feature_id=0x01 -> 0x01010000
SPL: device_id=0x01, feature_id=0x02 -> 0x01020000

所以 feature ID 不是读 ACPI 表直接读出来的,而是由驱动常量和旧驱动常量交叉确认,然后用 WMI probe 验证。

第八步:WMI probe 做了什么

为了避免只相信 sysfs,又写了一个只读 kernel module:

1
src/y9000p-wmi-probe.c

核心不是复杂逆向,就是调用内核 WMI API:

1
st = wmi_evaluate_method(guid, 0, method, &in, &out);

对于 Lenovo Other,probe 用 32-bit 参数调用 method 17:

1
2
3
4
5
6
7
8
static void eval_u32(const char *guid, u32 method, u32 arg)
{
struct acpi_buffer in = { sizeof(arg), &arg };
struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL };

st = wmi_evaluate_method(guid, 0, method, &in, &out);
...
}

probe 里列出要读的 feature:

1
2
3
4
5
6
7
8
9
static const u32 features[] = {
0x01010000, 0x01020000, 0x01030000, 0x01040000,
0x01050000, 0x01060000, 0x01070000, 0x02010000,
0x02020000, 0x02030000, 0x02040000, 0x04030001,
0x04030002, 0x05040000, 0x05050000,
};

for (i = 0; i < ARRAY_SIZE(features); i++)
eval_u32(OTHER_GUID, 17, features[i]);

这里的 OTHER_GUID 就是 Lenovo Other:

1
#define OTHER_GUID "DC2A8805-3A8C-41BA-A6F7-092E0089CD3B"

构建方式和普通 out-of-tree kernel module 一样,Makefile 内容大致是:

1
2
3
4
5
6
obj-m += y9000p-wmi-probe.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
$(MAKE) -C $(KDIR) M=$(PWD) modules

加载后从内核日志读取结果。归档日志 logs/y9000p-wmi-probe-20260429-131059.log 里有:

1
2
3
4
y9000p_wmi_probe begin read-only probe
y9000p_wmi_probe DC2A8805-3A8C-41BA-A6F7-092E0089CD3B method=17 arg32=0x01010000 status=0x0 integer=125 hex=0x7d
y9000p_wmi_probe DC2A8805-3A8C-41BA-A6F7-092E0089CD3B method=17 arg32=0x01020000 status=0x0 integer=90 hex=0x5a
y9000p_wmi_probe end read-only probe

这说明固件 method 17 直接读到:

1
2
0x01010000 -> 125
0x01020000 -> 90

也就是说,当 sysfs 的 current_value 显示 0 时,不能直接认为固件里就是 0。至少在这台机器上,直接 WMI method 17 读到的真实值是 125/90。

第九步:为什么说驱动 current_value ID 用错了

lenovo-wmi-other 有两类操作容易混在一起:

1
2
capdata/range 查询:查 min/max/default/step
current value 查询:查当前值或写当前值

capdata/range 查询需要完整 attribute ID:

1
device + feature + mode + type

驱动里是这样构造的:

1
2
3
4
5
6
7
attribute_id =
FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, tunable_attr->device_id) |
FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, tunable_attr->feature_id) |
FIELD_PREP(LWMI_ATTR_MODE_ID_MASK, mode) |
FIELD_PREP(LWMI_ATTR_TYPE_ID_MASK, tunable_attr->type_id);

ret = lwmi_cd01_get_data(priv->cd01_list, attribute_id, &capdata);

这段用于 capdata 查询是合理的,因为它要查某个模式下的范围信息。

问题在于旧代码接着把同一个 attribute_id 传给 WMI method 18:

1
2
3
4
5
args.arg0 = attribute_id;
args.arg1 = value;

ret = lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_SET,
(unsigned char *)&args, sizeof(args), NULL);

custom=0xff 下,这样会把参数变成:

1
2
ppt_pl2_sppt: 0x0101ff00
ppt_pl1_spl: 0x0102ff00

但前面的直接 WMI probe 证明,本机 firmware method 17/18 要的是:

1
2
ppt_pl2_sppt: 0x01010000
ppt_pl1_spl: 0x01020000

差别就是中间的 ff。对 capdata 来说,ff 表示 custom mode;对 current value get/set 来说,它反而让固件读写不到真实值。

attr_current_value_show() 也有同样问题。旧代码把带 mode/type 的 attribute_id 传给 method 17,因此 current_value 读回不可信。

第十步:补丁为什么只改两处

修法不能把所有 ID 构造都改掉,否则 min/max/default 可能坏掉。正确边界是:

1
2
capdata/range 查询继续用 device + feature + mode + type
method 17/18 current_value get/set 改用 device + feature

补丁新增一个 helper:

1
2
3
4
5
static u32 lwmi_om_feature_value_id(struct tunable_attr_01 *tunable_attr)
{
return FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, tunable_attr->device_id) |
FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, tunable_attr->feature_id);
}

然后只改 method 17/18 的参数:

1
2
3
4
5
6
7
8
@@ attr_current_value_store
- args.arg0 = attribute_id;
+ args.arg0 = lwmi_om_feature_value_id(tunable_attr);
args.arg1 = value;

@@ attr_current_value_show
- args.arg0 = attribute_id;
+ args.arg0 = lwmi_om_feature_value_id(tunable_attr);

补丁小,但验证必须完整。加载修补后的 lenovo-wmi-other 后,通过标准 sysfs 写:

1
2
3
set .../platform-profile-0/profile = custom
set .../ppt_pl1_spl/current_value = 145
set .../ppt_pl2_sppt/current_value = 210

再用 WMI probe 读回:

1
2
y9000p_wmi_probe ... method=17 arg32=0x01010000 status=0x0 integer=210 hex=0xd2
y9000p_wmi_probe ... method=17 arg32=0x01020000 status=0x0 integer=145 hex=0x91

这一步证明:修补后不是绕过 sysfs 私下写 firmware,而是标准 firmware attributes 路径已经能正确写入。

第十一步:压力测试验证功耗确实打开

最后用 stress-ng 压 CPU,同时通过 RAPL energy 采样算 package power:

1
stress-ng --cpu 32 --cpu-method matrixprod --timeout 60s

记录到的采样:

1
2
3
4
5
t=002.0s package_power_w=0162.2
t=014.0s package_power_w=0169.8
t=021.0s package_power_w=0172.6
t=033.0s package_power_w=0173.4
t=058.1s package_power_w=0173.7

汇总:

1
2
3
max_w=173.7
avg_w=168.7
last_20s_avg_w=171.8

这说明原来的 30W 墙已经解除。PL2 写 210W 不代表 CPU 会长期保持 210W;实际长期功耗还受 PL1、温度、EC 固件、适配器、BIOS thermal policy 等因素共同影响。约 170W 的持续 package power 已经足以证明问题不在原来的 30W 状态。

EC 固件是 Embedded Controller firmware,笔记本里的键盘热键、充电、风扇曲线、温度响应等常由 EC 参与控制。最开始看到“风扇不积极”,并不等于风扇驱动坏了。CPU 被压在 30W 时,温度上不去,EC 没有理由把风扇拉高。

固化方案

运行时脚本放在:

1
/usr/local/sbin/legion-y9000p-performance.sh

它做几件事:

1
2
3
4
5
1. 动态找到 Lenovo GameZone provider
2. 写 profile=custom
3. 写 ppt_pl1_spl=145
4. 写 ppt_pl2_sppt=210
5. 设置 CPU governor/EPP/frequency/intel_pstate

CPU 策略没有固定成 performance,而是:

1
2
3
4
5
6
7
governor=powersave
EPP=balance_power
scaling_min_freq=cpuinfo_min_freq
scaling_max_freq=cpuinfo_max_freq
max_perf_pct=100
min_perf_pct=0(实际被 kernel clamp 到 15)
hwp_dynamic_boost=1

EPP 是 Energy Performance Preference,表示能耗/性能偏好。balance_powerpower 更愿意在负载上来时提功耗;power 空闲差不多低,但满载响应明显慢。

对比测试结果:

1
2
3
balance_performance: idle≈12W,load 很快到 125W,吞吐最高
balance_power: idle≈11W,load 第二个采样到 125W,吞吐接近
power: idle≈11W,但升功耗慢,10 秒测试吞吐明显低

因此固化为 balance_power。这样低负载功耗可以下探,满载时也不会被 30W 按住。

systemd 服务是:

1
/etc/systemd/system/legion-y9000p-init.service

内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=Initialize Lenovo Legion Y9000P IAX10 platform support
Before=legiond.service
After=systemd-modules-load.service

[Service]
Type=oneshot
ExecStart=-/usr/bin/modprobe legion_laptop force=1 enable_platformprofile=0
ExecStart=/usr/local/sbin/legion-y9000p-performance.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

第一条 ExecStart 前面的 - 表示即使 legion_laptop DKMS 加载失败,service 也继续执行下一条。

DKMS 是 Dynamic Kernel Module Support,用于在内核升级后自动重建第三方内核模块。它适合维护额外模块,但不应该成为这次解锁 30W 墙的硬依赖。真正关键的是内核自带的:

1
2
lenovo-wmi-gamezone
lenovo-wmi-other

本地修补模块安装到内核 updates 目录,例如:

1
/usr/lib/modules/6.19.14-arch1-1/updates/y9000p/lenovo-wmi-other.ko

为了不靠 DKMS 维护这个补丁,使用 pacman hook:

1
/etc/pacman.d/hooks/99-y9000p-lenovo-wmi-other-patch.hook

配套脚本:

1
/usr/local/sbin/y9000p-build-patched-lenovo-wmi-other.sh

内核升级时,它下载匹配版本的 Linux source tarball,应用补丁,只构建 lenovo-wmi-other.ko,安装到 updates 目录,再执行 depmod。这是上游修复合并前的过渡方案。

如果要迁移到别的机器

这类问题不能直接抄数值。不同机型、BIOS、内核版本可能使用同样 GUID,但 feature 行为不完全相同。比较稳的流程是:

1
2
3
4
5
6
7
8
1. 读 RAPL sysfs,确认 30W 不是显式 RAPL 限制
2. 用 lsmod/modinfo 找当前 Lenovo WMI 模块和 GUID
3. 区分 aggregate platform_profile 和具体 provider profile
4. 查看 firmware-attributes 下是否有 ppt_pl1_spl/ppt_pl2_sppt
5. 从驱动源码确认 method ID 和 feature ID
6. 用只读 WMI probe 验证 method 17 返回值
7. 如果要改驱动,只改最小路径
8. 用 sysfs 写入、WMI 读回、stress-ng、RAPL energy 采样做闭环

几个命令很有用:

1
2
3
4
5
modinfo lenovo_wmi_gamezone lenovo_wmi_other lenovo_wmi_capdata01
cat /sys/firmware/acpi/platform_profile
cat /sys/firmware/acpi/platform_profile_choices
find /sys/devices/virtual/firmware-attributes -path '*lenovo-wmi-other*' -maxdepth 5 -type f
modprobe -D lenovo-wmi-other

如果能看到 sysfs 写入成功,但功耗没变,或者 current_value 读回和直接 WMI 读回不一致,就要警惕驱动参数构造问题。

回头看

最终链路可以整理成:

1
2
3
4
5
6
7
8
9
10
11
Linux profile=max-power

Lenovo GameZone firmware thermal mode=0xe0

不是 custom(0xff)

Lenovo Other PPT 写入被拒绝,或 current_value 路径没有真正更新 firmware

CPU package power 被压在约 30W

温度上不去,EC 固件不拉高风扇

修复链路:

1
2
3
4
5
6
7
8
9
10
11
写 GameZone provider=custom

firmware thermal mode=0xff

修补 lenovo-wmi-other current_value get/set ID

ppt_pl1_spl=145,ppt_pl2_sppt=210 经 sysfs 写入

WMI method 17 读回 145/210

CPU package power 压测稳定到约 170W

这次排查最有价值的不是某个固定命令,而是证据链:sysfs 只能说明 Linux 驱动暴露了什么;驱动源码说明 Linux 打算怎么调用固件;WMI probe 说明固件实际怎么回应;压力测试说明这些设置是否真的改变了机器行为。

只看其中一个,很容易误判。

这篇文章描述的逆向与代码编写过程全部由Claude Code完成,你可以认为本文的“我”也是GPT-5.5。文中的电脑是我室友的在2025年购买的拯救者Y9000P。官方驱动对温控支持不佳,所以我们消耗了大量token自己修复了。为了减少重复劳动,我将思路记录于此,当然本文也是AI写就