一、问题现象
在reboot压力测试中出现一台死机
二、问题分析
2.1 kernel calltrace
[ 71.688270][ T120] Unexpected kernel BRK exception at EL1
[ 71.688280][ T120] Internal error: BRK handler: 00000000f2000001 [#1] PREEMPT SMP
[ 71.688370][ T120] Dumping ftrace buffer:
[ 71.688382][ T120] (ftrace buffer empty)
[ 71.689307][ T120] CPU: 7 PID: 120 Comm: kworker/7:2 Tainted: G C OE 6.1.118-android14-11-ga3b9c44908dd-ab13320413 #1
[ 71.689315][ T120] Hardware name: Qualcomm Technologies, Inc. Spring QRD (DT)
[ 71.689319][ T120] Workqueue: events fg_monitor_workfunc [fg_bq28z610]
[ 71.689343][ T1958] smblib_get_prop_batt_status: chg_dis:0, chg_dis_clint:FORCE_RECHARGE_VOTER, fake_status:-22, usb_online:1, dc_online: 0, batt_status: 4
[ 71.689361][ T120] pstate: 00400005 (nzcv daif +PAN -UAO -TCO -DIT -SSBS BTYPE=--)
[ 71.689377][ T1958] pm7250b_charger: smblib_reduce_vbus_to_5V: reduce vbus start, real_type = 14
[ 71.689368][ T120] pc : fg_mac_read_block+0x2b4/0x2cc [fg_bq28z610]
[ 71.689399][ T120] lr : fg_mac_read_block+0x144/0x2cc [fg_bq28z610]
[ 71.689423][ T120] sp : ffffffc00a96bc50
[ 71.689426][ T120] x29: ffffffc00a96bc90 x28: ffffff805576d208 x27: ffffff81f6ff4fa8
[ 71.689436][ T120] x26: ffffff801c1e2910 x25: ffffffc00a96bc60 x24: 0000000000000024
[ 71.689444][ T120] x23: 000000000000003e x22: 00000000000000ff x21: ffffff805576d040
[ 71.689452][ T120] x20: 0000000000000020 x19: ffffffc00a96bd08 x18: ffffffc00a87b030
[ 71.689460][ T120] x17: 00000000a488ebfc x16: 00000000a488ebfc x15: ffffffc008fdc190
[ 71.689468][ T120] x14: 0000000000000001 x13: 0a7a483030303030 x12: 343d716572662032
[ 71.689477][ T120] x11: ffffffc00a96bc60 x10: 00000000000000d4 x9 : 00000000000000fd
[ 71.689485][ T120] x8 : 0000000000000000 x7 : a8b0080500000010 x6 : b0f41d3108013202
[ 71.689492][ T120] x5 : ffffff804de21cf6 x4 : ffffffc00a96b7a6 x3 : 0000000000061a80
[ 71.689500][ T120] x2 : 0000000000000000 x1 : ffffff8002718000 x0 : 0000000000000000
[ 71.689508][ T120] Call trace:
[ 71.689511][ T120] fg_mac_read_block+0x2b4/0x2cc [fg_bq28z610]
[ 71.689536][ T120] fg_select_poweroff_voltage_config+0x70/0x310 [fg_bq28z610]
[ 71.689560][ T120] fg_monitor_workfunc+0x3c/0x134 [fg_bq28z610]
[ 71.689583][ T120] process_one_work+0x1e4/0x43c
[ 71.689596][ T120] worker_thread+0x25c/0x430
[ 71.689604][ T120] kthread+0x104/0x1d4
[ 71.689610][ T120] ret_from_fork+0x10/0x20
[ 71.689622][ T120] Code: 911bcc21 95b34008 52800036 17ffff91 (d4200020)
[ 71.689630][ T120] ---[ end trace 0000000000000000 ]---
可以获知以下信息:
- 函数死在
fg_mac_read_block
函数 - 死在CPU7上
- 死机类型为Unexpected kernel BRK exception at EL1
备注:这种死机类型按照经验一般为数组越界
2.2 T32恢复现场
对照源码如下:
static u8 fg_checksum(u8 *data, u8 len)
{
u8 i;
u16 sum = 0;
for (i = 0; i < len; i++) {
sum += data[i];
}
sum &= 0xFF;
return 0xFF - sum;
}
static int fg_mac_read_block(struct bq_fg_chip *bq, u16 cmd, u8 *buf, u8 len)
{
int ret;
u8 cksum_calc, cksum;
u8 t_buf[40];
u8 t_len;
int i;
t_buf[0] = (u8)cmd;
t_buf[1] = (u8)(cmd >> 8);
ret = fg_write_block(bq, bq->regs[BQ_FG_REG_ALT_MAC], t_buf, 2);
if (ret < 0)
return ret;
msleep(4);
ret = fg_read_block(bq, bq->regs[BQ_FG_REG_ALT_MAC], t_buf, 36);
if (ret < 0)
return ret;
cksum = t_buf[34];
t_len = t_buf[35];
if(t_len <= 2){
fg_err("%s len is invaild, force vaild\n", bq->log_tag);
t_len = 2;
}
cksum_calc = fg_checksum(t_buf, t_len - 2);
if (cksum_calc != cksum) {
fg_err("%s failed to checksum\n", bq->log_tag);
return 1;
}
for (i = 0; i < len; i++)
buf[i] = t_buf[i + 2];
return 0;
}
T32显示代码死于fg_checksum的for循环处。
这个brk指令的地址是 0xFFFFFFC002317624 ,查看是否有一条BL语句跳转到此地址,可以发现如下的地方进行了跳转
static u8 fg_checksum(u8 *data, u8 len)
{
u8 i;
u16 sum = 0;
92401D29 and x9,x9,#0xFF ; x9,x9,#255
2A1F03E8 mov w8,wzr
D100A52A sub x10,x9,#0x29 ; x10,len,#41
910043EB add x11,sp,#0x10 ; x11,sp,#16
B100A15F cmn x10,#0x28 ; x10,#40
54000623 b.cc 0xFFFFFFC002317624 ;;;这里跳转到了brk函数
for (i = 0; i < len; i++) {
3840156C ldrb w12,[x11],#0x1 ; w12,[x11],#1
}
关键指令解析
and x9, x9, #0xFF
- 这条指令将
x9
寄存器的高 56 位清零,只保留最低 8 位(即x9 = x9 & 0xFF
)。 - 作用:确保
x9
的值在0x00–0xFF
(0–255)范围内,因为len
是u8
类型(8 位无符号整数)。
- 这条指令将
sub x10, x9, #0x29
- 计算
x10 = x9 - 41
(0x29 = 41
)。 - 由于
x9 ∈ [0, 255]
,因此x10
的取值范围为[-41, 214]
。
- 计算
cmn x10, #0x28
- 等价于
cmp x10, #-0x28
(即比较x10
和-40
)。 - 若
x10 < -40
(即x9 - 41 < -40
→x9 < 1
),则触发b.cc
(无符号小于跳转)。
- 等价于
b.cc 0x...
b.cc
(Carry Clear)在CPSR.C = 0
时跳转,对应无符号比较中的x10 < -40
。- 结合
cmn
,实际检测的是x9 < 1
(即len == 0
)。
这段代码的意思就是用来检测x9的值是否小于41,如果x9小于41,则继续往下执行,如果大于了41,则跳转
0xFFFFFFC002317624,抛出brk异常。
这里可能难以理解,为何是41??难道是编译器随机设置的值?
41是固定的,是特意设置的值,我们可以看到 fg_checksum
的第一个参数是data,是在 fg_mac_read_block
中调用fg_checksum时传入的,实参为t_buf,而这个值是一个 u8 t_buf[40]
的数组,有40个成员,而在 fg_checksum
中的for循环中 data[i]其实调用的就是t_buf[i]
,另一个参数是t_len-2,也就是t_buf[35]-2 ,这个值也就对应的是x9寄存器,所以这段汇编的代码中检测x9是否小于41,原因就再此。如果超过40就越界了!lem=40是没有问题的
所以临界情况就是40,当len=40时,t_len-2=40,所以t_len=t_buf[35]=42,
一旦出现t_buf[35] > 42的情况,就会导致数组越界!!!
然后我们看一下此时的len是多少呢?
可以看到X9此时是0xFD, X10 = 0xFD-0x29=0xD4,确实和寄存器中x10的值一致,所以可以证明此时x9=0xFD=253,这个值是大于40的,此时t_buf[35]=0xFD+0x2=0xFF!!!,造成了数组越界!!
三、问题根因
对于t_len没有做范围限制,导致数组越界,造成死机
四、解决方案
if (t_len > 42)
{
t_len=42;
}
五、问题的引申思考
在这个问题中,我们可以看到t_buf[35]是0,这个和我们从寄存器来推断的值完全是两种结果。
5.1 不能完全相信trace32解析处变量结果
这个问题在一开始的时候就是以t_buf[35]=0来正向分析的,结果发现完全推断不下去,和实际不符合
当t_buf[35]=0时,现在的代码是完全不可能造成数组越界的
t_len = t_buf[35];
if(t_len <= 2){
fg_err("%s len is invaild, force vaild\n", bq->log_tag);
t_len = 2;
}
如果小于2,那么t_len是直接赋值为2的,所以len=t_len-2=0,是不可能造成数组越界的!
所以不能完全相信T32解析处的变量值,某些情况下是需要根据寄存器值来推导出实际的值!!
5.2 编译器优化
在我们认为t_buf[35]=0时,我们去看汇编,我们希望能够找到t_buf[35]=2的赋值,然而我们找了很久都没有发现这句赋值语句!知道发现了如下的代码
3940CBF6 ldrb w22,[sp,#0x32] ; w22,[sp,#50]
t_len = t_buf[35];
71000909 subs w9,w8,#0x2 ; w9,w8,#2
540002C8 b.hi 0xFFFFFFC00231754C
if(t_len <= 2){
B0000048 adrp x8,0xFFFFFFC002320000 ; x8,last_soc
B9412D08 ldr w8,[x8,#0x12C] ; w8,[x8,#300]
36F80748 tbz w8,#0x1F,0xFFFFFFC0023175E8 ; w8,#31,0xFFFFFFC0023175E8
52801FE8 mov w8,#0xFF ; w8,#255
-
subs
指令的双重作用:- 它不仅执行减法
w9 = w8 - 2
- 同时根据结果设置条件标志位(NZCV寄存器):
N
(Negative):结果是否为负Z
(Zero):结果是否为零C
(Carry):是否发生借位(无符号下溢)V
(oVerflow):有符号溢出
- 它不仅执行减法
-
b.hi
的条件:HI
= Higher (无符号大于)- 触发条件:
C == 1
且Z == 0
- 翻译:
无借位(C=1)
且结果不为零(Z=0)
-
条件标志位的含义:
w8 (t_len) 运算结果 C (借位) Z (零) b.hi
条件t_len > 2
正数 1
(无借位)0
(非零)✅ 跳转 t_len = 2
0
1
(无借位)1
(为零)❌ 不跳转 t_len < 2
负数 (下溢) 0
(有借位)0
(非零)❌ 不跳转
为什么等价于 t_len > 2
- 无借位 (
C=1
):表示w8 ≥ 2
(无符号减法没有下溢) - 结果非零 (
Z=0
):表示w8 ≠ 2
- 组合条件:
w8 ≥ 2
且w8 ≠ 2
→w8 > 2
所以这里的意思就是当t_len>2时就跳转0xFFFFFFC00231754C(也就是fg_checksum函数)执行,如果t_len<=2时,就正常往下执行
adrp x8,0xFFFFFFC002320000 ; 加载日志配置基址
ldr w8,[x8,#0x12C] ; 读取日志级别 (offset 300)
tbz w8,#0x1F,0x75E8 ; 如果bit31=0,跳过日志打印
mov w8,#0xFF
前三句应该就是跳转执行printk去打印日志,最后一句将0xff赋值给w8寄存器,很难理解!
我们可以得知这种情况下t_len<=2时,被重新赋值t_len=2,这种情况下 fg_checksum
的返回值其实是固定的
static u8 fg_checksum(u8 *data, u8 len)
{
u8 i;
u16 sum = 0;
//len=0,不执行循环
for (i = 0; i < len; i++) {
sum += data[i];
}
//sum=0
sum &= 0xFF;
//return 0xff
return 0xFF - sum;
}
所以这个是编译器进行了优化,直接在编译时就把结果直接赋值给x8寄存器,不再运行时计算!