一般的debug的手段无非是依赖gdb、coredump,那是因为gdb类似的动态调试工具非常完善,可以帮助我们极快地完成debug。除此之外,debug还有一种方式,就是纯静态分析,在条件有限的情况下,gdb类的动态工具无法施展拳脚,只能依靠人脑模拟cpu运行以静态的方式来debug了。
一、起因
有用户在OpenAnolis Kernel SIG群里发了个有点模糊的截图和一句话:
经过简单询问发现客户机器是12代intel i5-12400,在安装官网上下载的AnolisOS 8.8系统镜像过程中crash,无法进入系统,没有coredump,只有这个屏幕的打印的信息,无法看到更多的log。
好了,这就是所有的信息,现在开始debug阶段。
二、debug
收集信息
首先根据图中的信息判断,此时应该是内核发生了crash,并且crash的地方发生在resctrl_mon_resource_init()函数中。
确定内核范围
确定内核版本范围:跟baseOS的同学确认官网上下载的AnolisOS 8.8镜像里面包含了4.19和5.10.134-13两个版本的内核,经客户确认,发生问题的是5.10内核。因此发生问题的内核版本是5.10.134-13。
确定函数范围
可以看到5.10.134-13的内核代码中resctrl_mon_resource_init()是一个23行的小函数,做了一些初始化的工作。
确定代码位置
从log中可以看到当cpu执行到resctrl_mon_resource_init+0xc0的地方时出现了panic,因此需要找到:
resctrl_mon_resource_init+0xc0
到底在代码中的哪个地方才能确定出问题的代码。
- 这里需要将对应的vmlinux解析成汇编,通过objdump -S -I -z vmlinux > vmlinux.txt解析成汇编的形式,解析出来最左边是地址,中间是二进制,右边是二进制对应的汇编代码。
- 通过解析出来的汇编可以看到resctrl_mon_resource_init函数的地址是ffffffff8148eea0,0xc0是ffffffff8148ef60,对应截图中的RIP 部分的二进制,刚好都能对应上,因此可以确认就是这里出现了问题。
- 到底汇编对应代码的哪里呢?这里分享个小经验:在汇编代码里面看到类似这样的结构,一个向下的大跳+一个向上的小跳,这种结构一般对应代码中的循环结构。汇编中的向上跳转比较特殊,一般只有在goto、循环这样的结构才能汇编出向上跳的情况。可以看到crash时RIP指向了一个循环结构结束的地方。
- 在resctrl_mon_resource_init()——
dom_data_init()中发现了这个for循环,大概捋一下汇编代码前后的逻辑基本就能很快确定crash的时候RIP执行到了dom_data_init()第23行的位置。
代码逻辑分析
dom_data_init()的先alloc了一块空间存放rmid_entry数组,并且将数组的首地址存在了全局变量rmid_ptrs中,接着for循环挨个对rmid_entry数据初始化加到rmid_free_lru链表中,由于第0个rmid_entry节点比较特殊,所以在for循环过后又把这个节点从rmid_free_lru中单独拿出来了。
- dom_data_init() 23行中:
resctrl_arch_rmid_idx_encode(0,0)返回0,然后在rmid_ptrs中取第0个元素并返回。
- 通过对汇编分析,可以知道在crash RIP处,RBX就是rmid_ptrs。
(图中分析是一种方法,还有一种方法就是看R12寄存器,R12寄存器为0说明代码完全没有进到for循环中,最后还是可以得到RBX就是rmid_ptrs这个结论,感兴趣的读者可以自行分析一下)而报错中可以看到crash时 RBX值为0x10!
- 所以原因就很明显了,crash是因为dom_data_init()中kcalloc()返回了一个非空,但是却不合法的值。在后续的代码中又对这个值取地址操作,产生了panic!
原因分析
所以为啥kcalloc()会返回0x10呢?我们可以看到nr_idx是从下方函数来的:
resctrl_arch_system_num_rmid_idx()
而resctrl_arch_system_num_rmid_idx()的返回值为:
x86_cache_max_rmid + 1
而x86_cache_max_rmid在初始化阶段如果检测cpu不支持CQM_LLC这个feature的话就会被置为-1 !所以在客户机器中,可能存在的情况是:客户机器的cpu不支持CQM_LLC,在这种情况下nr_id就会等于0,所以kcalloc()返回了不合法的值,然而dom_data_init()对kcalloc()返回值检查的时候只检查了是否非空,导致后续对该内存访问的过程中出现了panic!
打完收工!
至此,crash原因已经分析明确,后面找客户确认客户机器的cpu确实不支持 CQM_LLC。
三、结果
在知道crash原因后,迅速出了一个验证内核给用户,用户验证问题解决。(此处感谢涤安提供快速修复补丁和出验证内核)。证明之前所有分析正确,此次debug全程只通过静态的代码分析完成了问题定位和原因分析。
四、写在后面
当然,这个问题完整的解决涉及到resctrl的整体init逻辑:为什么在用户cpu不支持该feature的情况下还会走到这个init函数?这是个历史遗留问题:之前合入的某个patch修改了resctrl init流程的判断逻辑,导致了在特定的cpu型号下会出现这个bug,具体的细节就不在本文讨论了。至少我们已经明确了这个问题和知道这个问题的解决方案,后面的修复过程都好说。
来源 | 阿里云开发者公众号
作者 | 库恩