一、问题现象

在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 ]---

可以获知以下信息:

  1. 函数死在 fg_mac_read_block函数
  2. 死在CPU7上
  3. 死机类型为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
}

关键指令解析

  1. and x9, x9, #0xFF
    • 这条指令将 x9 寄存器的高 56 位清零,只保留最低 8 位(即 x9 = x9 & 0xFF)。
    • 作用:确保 x9 的值在 0x00–0xFF(0–255)范围内,因为 lenu8 类型(8 位无符号整数)。
  2. sub x10, x9, #0x29
    • 计算 x10 = x9 - 410x29 = 41)。
    • 由于 x9 ∈ [0, 255],因此 x10 的取值范围为 [-41, 214]
  3. cmn x10, #0x28
    • 等价于 cmp x10, #-0x28(即比较 x10-40)。
    • x10 < -40(即 x9 - 41 < -40x9 < 1),则触发 b.cc(无符号小于跳转)。
  4. 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

  1. subs 指令的双重作用

    • 它不仅执行减法 w9 = w8 - 2
    • 同时根据结果设置条件标志位(NZCV寄存器):
      • N (Negative):结果是否为负
      • Z (Zero):结果是否为零
      • C (Carry):是否发生借位(无符号下溢)
      • V (oVerflow):有符号溢出
  2. b.hi 的条件

    • HI = Higher (无符号大于)
    • 触发条件:C == 1Z == 0
    • 翻译:无借位(C=1)结果不为零(Z=0)
  3. 条件标志位的含义

    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 ≥ 2w8 ≠ 2w8 > 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寄存器,不再运行时计算!