电池改dcdc直供后

电量还是会一直下滑,到低电量的时候,驱动层会降频等。

原因

电量它是计算电池端耗电多少,高通傻逼pm芯片,不看电压。。。。

怎么办怎么办?

欺骗系统,直接改驱动层,不想重新编译系统,直接HOOK驱动劫持!!!!

开干!!!

现在是0:52了,群主也没睡,我也不能睡。

技术说明

是的,Linux内核模块可以修改其他驱动的寄存器值,原理如下:

为什么可以这样做

Linux内核是单一地址空间
        ↓
所有内核代码(包括模块)共享同一个内核虚拟地址空间
        ↓
任何内核模块都可以访问内核中的任何数据结构和函数
        ↓
没有进程间隔离,没有权限边界

这和用户空间完全不同:

用户空间进程A          内核空间
    ↓                    ↓
虚拟地址空间A        所有驱动共享
无法访问进程B    VS   模块A可以直接
的内存               访问驱动B的数据

我们做了什么

Hook①:替换函数指针

/* power_supply_desc 是个普通的内核结构体 */
struct power_supply_desc {
    const char *name;
    int (*get_property)(...);  /* ← 函数指针 */
    ...
};

/* 我们直接修改这个指针 */
orig_desc = bat_psy->desc;           /* 保存原始指针 */
fake_desc.get_property = fake_fn;    /* 替换为我们的函数 */
bat_psy->desc = &fake_desc;          /* 写入结构体 */

这就像在C语言里修改一个全局变量,完全合法。

Hook②:通过regmap写寄存器

/* regmap是内核提供的寄存器访问抽象层 */
regmap_write(chg_regmap, 0x1061, fake_soc);
regmap_write()
    ↓
regmap核心层(内核)
    ↓
SPMI总线驱动
    ↓
实际写入PMIC硬件寄存器

regmap是公开的内核API,任何模块都可以调用,前提是拿到regmap句柄。

完整链路

我们的模块
    ↓
bus_find_device_by_name()      ← 在内核设备树里找到charger设备
    ↓
dev_get_regmap()               ← 获取该设备的regmap句柄
    ↓
regmap_write(map, 0x1061, 95)  ← 通过regmap写PMIC寄存器
    ↓
SPMI总线                       ← 串行电源管理接口
    ↓
PM8150B PMIC芯片               ← 物理写入寄存器

为什么0x4009写不进去但0x1061可以

0x4009 — FG芯片SOC寄存器
    ↓
由FG硬件自主维护
CPU写入后硬件立即覆盖
属于"硬件只读"

0x1061 — Charger驱动SOC缓存
    ↓
由charger驱动软件维护
CPU写入后保持,直到驱动下次更新
属于"软件可写"

安全性说明

这种做法在生产系统中属于hack,存在以下风险:

风险说明
内核稳定性修改其他驱动数据可能导致竞争条件
内核升级结构体布局变化导致模块失效或崩溃
模块签名生产内核要求模块签名,out-of-tree模块会taint内核
并发访问未加锁的结构体修改可能在SMP下出问题

但在这个场景下(DCDC供电的嵌入式设备,固定内核版本),完全可以接受。

通过逐步缩小范围的方法找到的,整个过程是:

第一步:找到设备节点

# 从已知的电源管理路径出发
ls /sys/class/power_supply/
# → pm8150b-charger  qcom-battery  tcpm-...

# 找到FG设备在哪个SPMI总线上
ls /sys/bus/spmi/devices/0-02/
# → c440000.spmi:pmic@2:fuel-gauge@4000  ← FG设备
# → c440000.spmi:pmic@2:charger@1000     ← Charger设备

第二步:找到regmap调试接口

# debugfs暴露了所有regmap设备的寄存器
ls /sys/kernel/debug/regmap/
# → 0-02  ← pmic@2,包含FG和Charger

# 可以直接读取所有寄存器的值
cat /sys/kernel/debug/regmap/0-02/registers

第三步:用diff定位SOC寄存器

这是最关键的方法,等电量变化1%,对比前后寄存器差异

# 保存77%时的快照
cat /sys/kernel/debug/regmap/0-02/registers > /tmp/reg_77.txt

# 等电量降到76%,保存快照
cat /sys/kernel/debug/regmap/0-02/registers > /tmp/reg_76.txt

# 对比差异
diff /tmp/reg_77.txt /tmp/reg_76.txt

diff结果中跟随电量变化的寄存器就是SOC寄存器:

< 4009: c5    ← 77%时
> 4009: c3    ← 76%时  ← 差了2,对应1%
< 400a: c5
> 400a: c3    ← 同步变化,备份寄存器

第四步:用grep找charger SOC寄存器

# 已知电量78%=0x4e,搜索所有值为0x4e的寄存器
cat /sys/kernel/debug/regmap/0-02/registers | \
  awk -F': ' '$2=="4e"'

# 结果
1061: 4e   ← 在charger区域(0x1000~0x1fff)

第五步:确认0x1061是charger读取的

通过地址范围推断:

PMIC寄存器地址空间分配:
0x4000~0x4fff  → FG(fuel-gauge@4000)
0x1000~0x1fff  → Charger(charger@1000)

0x1061 在charger地址空间内
    ↓
必然是charger驱动读取的SOC缓存

第六步:验证0x1061可写

# 通过内核模块写入,读回验证
regmap_write(chg_regmap, 0x1061, 95);
regmap_read(chg_regmap, 0x1061, &cur);
# 读回=95 → 可写 ✅

整个方法论总结

已知现象(电量下降)
    ↓
找到相关设备节点(/sys/class/power_supply)
    ↓
找到debugfs寄存器接口(/sys/kernel/debug/regmap)
    ↓
diff快照法定位变化寄存器(等1%变化)
    ↓
grep法搜索特定值(搜索0x4e=78%)
    ↓
地址范围推断归属驱动(0x1000=charger)
    ↓
内核模块验证可写性
    ↓
确认方案可行

本质上是黑盒逆向,不依赖任何高通内部文档,完全通过Linux内核暴露的调试接口推断出来的。

FG SOC Hook — 高通PM8150B电量伪造技术文档

背景

小米K20 Pro(Snapdragon 865)拆除电池后使用DCDC模块固定4.2V供电,
系统燃油计(Fuel Gauge)通过库仑计累积放电电流,导致电量持续下降,
最终触发系统关机保护。本方案通过内核模块实现双重hook,将电量固定为95%。


一、原理

1.1 问题根源

DCDC 4.2V 固定供电
        ↓
PM8150B FG 燃油计
  ├── 电压法:4.2V → 约90%(开机时读一次)
  └── 库仑计:持续累积 current_now(负值)
              ↓
         电量 = 初始值 - 累积放电量
              ↓
         电量持续下降,最终触发关机

1.2 尝试过的方案(均失败)

方案失败原因
写 capacity sysfs只读节点
写 pm8150b-charger/status被驱动覆盖,电量仍下降
FG驱动 unbind/bindFG从PMIC SRAM恢复旧值
写 0x4009 SOC寄存器硬件只读,FG芯片自主更新
写 0x41a0 CC_SOC寄存器FG芯片立即覆盖
kprobe hook内核禁用了kprobe(EOPNOTSUPP)

1.3 成功方案:双重Hook

系统中有两套独立的电量读取路径:

FG芯片硬件
    ↓
    ├─→ qcom-fg驱动 → power_supply子系统
    │       ↓
    │   Hook ①:替换get_property函数指针
    │       ↓
    │   用户空间/脚本看到95%        ✅
    │
    └─→ qcom-smbx-charger驱动
            直接读0x1061寄存器
            ↓
        Hook ②:定时器每300秒写入fake值
            ↓
        低电量保护看到95%            ✅

Hook ① — power_supply描述符替换

Linux power_supply子系统通过 power_supply_desc.get_property 函数指针
向上层提供数据。内核模块替换该指针为自定义函数,在返回时强制将
POWER_SUPPLY_PROP_CAPACITY 替换为 fake_soc。

Hook ② — charger寄存器定时写入

charger驱动(qcom-smbx-charger)直接读取PMIC寄存器 0x1061 获取SOC,
用于低电量关机保护决策,完全绕过power_supply子系统,因此需要独立处理。
通过内核定时器每300秒向 0x1061 写入fake值,覆盖真实电量。

1.4 为什么选择95%而不是100%

充电IC检测到100%会触发停充逻辑,关闭USB供电路径(本设备曾出现
充满后USB断电问题)。95%让系统认为仍在充电中,USB供电路径保持开启。

1.5 关键寄存器

寄存器地址说明
FG SOC0x4009FG芯片自主维护,硬件只读
FG SOC备份0x400a同上
Charger SOC0x1061charger驱动读取,可写
CC_SOC0x41a0~0x41a3库仑计累积值,硬件只读

二、编译环境搭建

2.1 环境要求

项目要求
硬件小米K20 Pro(SM8150/Snapdragon 865)
系统Ubuntu 26.04 (aarch64)
内核7.0.0-sm8150-g03c45beed5d5
编译器clang(内核由clang编译,必须用clang)
内核头文件linux-headers-7.0.0-sm8150-g03c45beed5d5

2.2 安装依赖

apt update
apt install -y clang make

# 确认内核头文件存在
ls /usr/src/linux-headers-$(uname -r)/

# 确认clang版本
clang --version
uname -r

2.3 目录结构

/opt/app/fg_hook/
├── fg_hook.c          # 内核模块源码
├── Makefile           # 编译脚本
├── fg_hook.ko         # 编译产物
└── install.sh         # 一键安装脚本

三、源代码

3.1 内核模块 fg_hook.c

见同目录 fg_hook.c 文件。

核心逻辑说明:

// Hook ①:替换power_supply描述符的get_property
fake_desc.get_property = fake_get_property;
bat_psy->desc = &fake_desc;

// Hook ②:定时器每300秒写charger SOC寄存器
regmap_write(chg_regmap, 0x1061, fake_soc);
mod_timer(&soc_timer, jiffies + HZ * 300);

3.2 Makefile

obj-m += fg_hook.o
KDIR  := /usr/src/linux-headers-$(shell uname -r)
PWD   := $(shell pwd)

all:
    make CC=clang -C $(KDIR) M=$(PWD) modules

clean:
    make CC=clang -C $(KDIR) M=$(PWD) clean

四、编译

mkdir -p /opt/app/fg_hook
cd /opt/app/fg_hook

# 将 fg_hook.c 和 Makefile 放入该目录后编译
make

# 编译成功输出
# CC [M]  fg_hook.o
# MODPOST Module.symvers
# LD [M]  fg_hook.ko

五、部署

5.1 一键安装

chmod +x install.sh
./install.sh

5.2 手动加载测试

# 加载(默认95%)
insmod /opt/app/fg_hook/fg_hook.ko fake_soc=95

# 验证power_supply hook
cat /sys/class/power_supply/qcom-battery/capacity
# 应输出:95

# 验证charger寄存器
cat /sys/kernel/debug/regmap/0-02/registers | grep "^1061:"
# 应输出:1061: 5f

# 查看内核日志
dmesg | grep fg_hook

5.3 systemd 开机自启

cat > /etc/systemd/system/fg-hook.service << 'EOF'
[Unit]
Description=FG SOC Hook v2 - Fix battery percentage for DCDC power supply
After=multi-user.target
DefaultDependencies=no

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/insmod /opt/app/fg_hook/fg_hook.ko fake_soc=95
ExecStop=/sbin/rmmod fg_hook

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable fg-hook
systemctl start fg-hook
systemctl status fg-hook

5.4 运行时修改电量值

# 不重载模块,直接修改参数,下次定时器触发时生效
echo 90 > /sys/module/fg_hook/parameters/fake_soc

六、验证

# 验证电量固定
cat /sys/class/power_supply/qcom-battery/capacity

# 验证charger寄存器(0x5f = 95)
cat /sys/kernel/debug/regmap/0-02/registers | grep "^1061:"

# 查看hook日志
dmesg | grep fg_hook

# 查看定时器运行(300秒后出现)
dmesg | grep "定时写0x1061"

# 查看模块参数
cat /sys/module/fg_hook/parameters/fake_soc

预期内核日志:

[  24s] fg_hook: [1] power_supply hook完成
[  24s] fg_hook: [2] charger regmap获取成功
[  24s] fg_hook: [3] 0x1061写前=0x4e(78%)
[  24s] fg_hook: [3] 0x1061写后=0x5f(95%)
[  24s] fg_hook: [4] 定时器已启动(每300秒)
[  24s] fg_hook: 全部完成 fake_soc=95%
[ 324s] fg_hook: 定时写0x1061,读回=0x5f(95%)

七、注意事项

内核升级后需要重新编译

内核模块与内核版本强绑定,升级内核后必须重新编译:

cd /opt/app/fg_hook
make clean && make
systemctl restart fg-hook

不影响实际供电

本模块只修改上报给软件层的电量数值,不修改FG硬件寄存器,
DCDC实际供电电压和电流不受影响。

卸载后恢复真实电量

systemctl stop fg-hook
# 电量恢复真实值,会重新开始下降

定时器间隔说明

定时器300秒写一次0x1061,是为了应对charger驱动定期刷新该寄存器。
如发现电量被短暂恢复为真实值,可缩短间隔:

# 修改源码后重新编译
sed -i 's/HZ \* 300/HZ * 60/' /opt/app/fg_hook/fg_hook.c
make clean && make
systemctl restart fg-hook

八、文件清单

/opt/app/fg_hook/
├── fg_hook.c          内核模块源码
├── Makefile           编译脚本
├── fg_hook.ko         编译产物(需重新编译)
└── install.sh         一键安装脚本

/etc/systemd/system/
└── fg-hook.service    systemd服务配置

RTC时间

有两个问题:

  1. 网络还没就绪 DNS 解析失败,服务启动太早了
  2. hwclock 路径不对,Ubuntu 26.04 路径变了

修复脚本

先安装 apt install ntpsec-ntpdate -y

sudo tee /usr/local/bin/force-timesync.sh > /dev/null <<'EOF'
#!/bin/bash
# 等待网络真正就绪
for i in $(seq 1 30); do
    ping -c 1 -W 2 223.5.5.5 > /dev/null 2>&1 && break
    sleep 2
done

# 强制同步时间
ntpdate ntp.aliyun.com || ntpdate 203.107.6.88 || ntpdate 223.5.5.5

# 修正 hwclock 路径
$(which hwclock) --systohc 2>/dev/null || true

# 重启 chrony
systemctl restart chrony
EOF

sudo chmod +x /usr/local/bin/force-timesync.sh

修复 service,等待网络真正就绪

sudo tee /etc/systemd/system/force-timesync.service > /dev/null <<'EOF'
[Unit]
Description=Force NTP time sync on boot
After=network-online.target nss-lookup.target
Wants=network-online.target nss-lookup.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/force-timesync.sh
RemainAfterExit=yes
TimeoutStartSec=120

[Install]
WantedBy=multi-user.target
EOF

重新加载

sudo systemctl daemon-reload
sudo systemctl restart force-timesync.service
sudo systemctl enable force-timesync.service
journalctl -u force-timesync.service -f

-f 实时查看日志,确认同步成功。