内核调试:一次多线程调试与KASAN检测实例
内核调试:一次多线程调试与KASAN检测实例
- 1. 环境说明
- 2. 问题描述
- 3. 问题排查与定位
- 3.1 线程并发问题(减少线程数)
- 3.2 轻量地跟踪对象的分配与释放
- 3.3 检查空指针与潜在修改者
- 3.4 KASAN检查
- 4. 总结
博主最近遇到一个非常顽固的多线程BUG,复现起来具有很大的随机性,本文介绍博主一步步定位问题并解决BUG的思路和方案,希望对大家有启发(注:本思路同样适用于用户态调试)
1. 环境说明
- OS内核:本文内核跑在QEMU环境上,详细配置见我之前的博客:用VSCode + QEMU跑起来能够可视化Debug的NOVA文件系统
- 测试程序:
Filebench
,一个文件系统测试工具。参考这篇博客了解其编译并放入QEMU的方法:编译静态文件系统测试工具【Filebench】并在QEMU中运行
2. 问题描述
在虚拟机上用50个线程运行如下Filebench脚本:
...
define fileset name=bigfileset,path=$dir,size=$filesize,entries=$nfiles,dirwidth=$meandirwidth,prealloc=80define process name=filereader,instances=1
{thread name=filereaderthread,memsize=10m,instances=$nthreads{flowop createfile name=createfile1,filesetname=bigfileset,fd=1flowop writewholefile name=wrtfile1,srcfd=1,fd=1,iosize=$iosizeflowop closefile name=closefile1,fd=1flowop openfile name=openfile1,filesetname=bigfileset,fd=1flowop appendfilerand name=appendfilerand1,iosize=$meanappendsize,fd=1flowop closefile name=closefile2,fd=1flowop openfile name=openfile2,filesetname=bigfileset,fd=1flowop readwholefile name=readfile1,fd=1,iosize=$iosizeflowop closefile name=closefile3,fd=1flowop deletefile name=deletefile1,filesetname=bigfilesetflowop statfile name=statfile1,filesetname=bigfileset}
}
简单来说,他就类似每个线程执行一堆文件操作序列,对应的:
createfile
: 创建文件writewholefile
: 填充文件closefile
:关闭文件openfile
:打开文件appendfilerand
:随机追加写文件readwholefile
:读整个文件deletefile
:删除文件statfile
:输出文件基本属性
在运行过程中,发现被测试的文件系统删除了本不应该存在的File(具体而言,被测试的文件系统为每个目录维护一个hash表用于快速目录项索引,但是在文件删除过程中,发现在hash表里找不到对应文件),至此,事情变得扑朔迷离起来。可能的原因有很多,包括:
- 文件删除(
unlink
)部分实现存在BUG - 文件创建(
create
)部分实现存在BUG - 多线程BUG
- 内存踩踏BUG
最后,还可能是VFS
本身的BUG,这是我最不敢想像的,如果是VFS
本身的BUG,那需要做的工作就太多了……
3. 问题排查与定位
3.1 线程并发问题(减少线程数)
我在单线程下运行Filebench,发现了不少单线程下存在的内存越界问题以及强转问题。其中:
-
强转问题通常带来数值的overflow进而导致不正确的内存访问,例如,计算32位
blk
对应的相对偏移地址,即:unsigned int blk = 0xffff0000; unsigned long addr;addr = blk << 12;
上述结果
b
的值为0xf0000000
,而非0xffff0000000
,正确的写法应该是:addr = (unsigned long)blk << 12;
-
其次,无符号数大小比较问题。切忌不能直接两数相减然后与0比较,如下:
unsigned int a = 0; unsigned int b = 1;assert(a - b > 0);
上述减法出来的结果是
UINT32_MAX
,即:0xffffffff
。
这些问题解决后,单线程下的大多数BUG都消失了。接着提升线程数至2线程,同样没有问题发生;当线程数回调到50时,同样的BUG再次出现,还伴随着其他种种问题,例如:空指针访问、写入已经被删除的文件、读取已经被删除的文件等。这证明问题仍在高并发场景下仍然存在,我们继续检查前面提到的点:
- 文件删除(
unlink
)部分实现存在BUG - 文件创建(
create
)部分实现存在BUG 多线程BUG- 内存踩踏BUG
除此之外,由于观察到了空指针访问,我们还希望检查相应的指针修改情况,看看是哪行代码有可能修改该指针,即:
- 空指针访问BUG
3.2 轻量地跟踪对象的分配与释放
为什么文件系统会访问已经被删除的File呢? 验证问题的思路是跟踪整个Filebench运行过程中文件的创建和删除情况,看看这个文件究竟有没有被创建。
在高并发场景下,通用调试手段printk()
(即内核的打印)函数已经不能够及时输出,表现为:printk: X messages dropped。
针对难以通过print调试的问题,可以考虑自行构建跟踪文件的代码,然后在出错的地方自行输出跟踪的信息。例如,在博主调试过程中,我在栈上分配了固定大小的数组,每次创建和删除文件时便向其中追加写入当前文件的inode号,在unlink
调用且在父目录中找不到该文件时强行停止内核(BUG_ON(1);
,详见3.3节)并输出该文件号的所有创建的和删除记录。这里要注意追踪器的轻量高效性,不能使其过度影响程序的并发,否则可能BUG无法发作。
为此博主构造了一个per cpu
文件号跟踪器,相关代码如下:
#define CPU 32
#define MAX_FILE 4000u32 remove_lists_pos[CPU];
u32 remove_lists[CPU][MAX_FILE];
spinlock_t remove_locks[CPU];u32 create_lists_pos[CPU];
u32 create_lists[CPU][MAX_FILE];
spinlock_t create_locks[CPU];// 创建文件部分伪代码
int create(dir) {...// per cpu create跟踪器int cpuid, i, is_find = 0;int start_cpuid = smp_processor_id();for (i = 0; i < 32; i++) {cpuid = (start_cpuid + i) % CPU ;spin_lock(&create_locks[cpuid]);if (create_lists_pos[cpuid] < MAX_FILE ) {create_lists[cpuid][create_lists_pos[cpuid]++] = inode;spin_unlock(&create_locks[cpuid]);break;}spin_unlock(&create_locks[cpuid]);}...
}// 删除文件部分伪代码
int unlink(dir, inode) {...// per cpu unlink跟踪器int cpuid, i, is_find = 0;int start_cpuid = smp_processor_id();for (i = 0; i < 32; i++) {cpuid = (start_cpuid + i) % CPU ;spin_lock(&remove_locks[cpuid]);if (remove_lists_pos[cpuid] < MAX_FILE ) {remove_lists[cpuid][remove_lists_pos[cpuid]++] = inode;spin_unlock(&remove_locks[cpuid]);break;}spin_unlock(&remove_locks[cpuid]);}...// 目录里面没有找到inodeif (inode not found in dir) {int i, j;// 输出跟踪记录for (i = 0; i < 32; i++) {for (j = 0; j < remove_lists_pos[i]; j++) {if (create_lists[i][j] == inode) {hk_info("%s: create_lists[%d][%d] %lu\n", __func__, i, j, create_lists[i][j]);}if (remove_lists[i][j] == inode) {hk_info("%s: remove_lists[%d][%d] %lu\n", __func__, i, j, remove_lists[i][j]);}}}// 停止内核BUG_ON(1);}
}
折腾一番后,博主发现几个更有意思的问题:有些文件确实根本就没有create
就被unlink
了,这是根本不能发生的事情(除非VFS
有BUG)。再者反复核对文件的删除和创建逻辑,也没有发现问题,看来事出另有因,我们继续往下检查:
文件删除(unlink
)部分实现存在BUG文件创建(create
)部分实现存在BUG多线程BUG- 内存踩踏BUG
- 空指针访问BUG
3.3 检查空指针与潜在修改者
在内核中,开发者通常使用BUG_ON(condition)
来当作断言assert(condition)
。例如:
void *pointer = get_pointer();
BUG_ON(pointer == NULL);
这样就能使系统在上述pointer
为空时停下来,验证确实是这个变量为空。
此外,博主还经常使用BUG_ON()
来验证某个函数一定不会被调用,某个可能修改pointer
的的变量是否真的被调用等(即用于确定潜在修改者),这点比较鸡肋,不过对于视觉疲劳懒得翻printk记录的人来说,这是省事的好方法。举个例子:
void *pointer = NULL;void threadB(){// 确认线程B永远不会调用,一旦调用系统就会报错BUG_ON(1);// 如果线程B会被调用,删除BUG_ON(1),浏览代码// 确认线程B会对pointer做出什么事pointer = 0x1;
}
总而言之,博主在代码中各式各样的指针处都写上了BUG_ON(1)
,但遗憾的是,我并没有发现存在修改者会使该指针变空。此时,只剩下一条路可走:内存溢出或内存踩踏使得乱象丛生。
文件删除(unlink
)部分实现存在BUG文件创建(create
)部分实现存在BUG多线程BUG- 内存踩踏BUG
空指针访问BUG
3.4 KASAN检查
KASAN是一个强大的内存泄漏、越界访问问题的检测工具,参考资料很多:
- KASAN实现原理
- KASAN配置
- KASAN实践
进一步的,我们cd
到内核源码目录下,直接用脚本开启下述config
即可:
[deadpool@localhost linux-5.1]$ ls
arch COPYING Documentation include Kbuild lib Makefile modules.order README security tools vmlinux
block CREDITS drivers init Kconfig LICENSES mm Module.symvers samples sound usr vmlinux-gdb.py
certs crypto fs ipc kernel MAINTAINERS modules.builtin net scripts System.map virt vmlinux.o
[deadpool@localhost linux-5.1]$ ./scripts/config -e CONFIG_SLUB_DEBUG
[deadpool@localhost linux-5.1]$ ./scripts/config -e CONFIG_SLUB_DEBUG_ON
[deadpool@localhost linux-5.1]$ ./scripts/config -e CONFIG_KASAN
[deadpool@localhost linux-5.1]$ ./scripts/config -e CONFIG_KASAN_INLINE
然后make -j32
编译即可。接着,果然,检查出如下类似错误:
==================================================================
BUG: KASAN: use-after-free in function+0xxx/0xxx [xx_module]
Write of size 8 at addr `addr1` by task filebench/2760
...
The buggy address belongs to the object at `addr`
...
==================================================================
上面报错信息中,关注:
- 报错类型:
use-after-free
,访问了free后的内存 function
:具体哪个函数访问的addr1
: 具体访问addr1
处变量产生的错误addr
:addr1
属于addr
处的对象(malloc出来的对象)
接着,检查function
函数,至此,终于找到了这个藏得非常深的BUG,由于不方便开放源码,这里简单来说就是在并发地插入hash表时,没有正确地上锁,导致出现各种各样的问题。
4. 总结
至此,终于结束了这为期两天的DEBUG之旅,现在回想起来,要是从一开始就使用KASAN,也许一下就能够解决遇到的问题,以后一定要多多使用KASAN来帮忙检查内存相关的错误。
题外话,听说rust好像可以在编译阶段就避免很多这样类似的问题,看来真的有必要向Linux Kernel中引入rust。
OK,就这样,起飞🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫
相关文章:

内核调试:一次多线程调试与KASAN检测实例
内核调试:一次多线程调试与KASAN检测实例1. 环境说明2. 问题描述3. 问题排查与定位3.1 线程并发问题(减少线程数)3.2 轻量地跟踪对象的分配与释放3.3 检查空指针与潜在修改者3.4 KASAN检查4. 总结博主最近遇到一个非常顽固的多线程BUG&#x…...

Java - 数据结构,队列
一、什么是队列 普通队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(FirstIn First Out) 入队列:进行插入操作的一端称为队尾(Tail/Rear) 出队列…...

ccc-pytorch-感知机算法(3)
文章目录单一输出感知机多输出感知机MLP反向传播单一输出感知机 内容解释: w001w^1_{00}w001:输入标号1连接标号0(第一层)x00x_0^0x00:第0层的标号为0的值O11O_1^1O11:第一层的标号为0的输出值t:真实…...

LeetCode 225.用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。实现 MyStack 类:void push(int x) 将元素 x 压入栈顶。int pop() 移除并返回栈顶元素。int top() …...

【面试】spring控制反转IOC
目录一.说明二.ioc的概念和作用三.优点四.实现机制五.IOC和DI的区别六.设计原则一.说明 1.ioc的概念2.ioc的作用3.ioc的优点4.ioc的实现机制 二.ioc的概念和作用 1.全称Inversion of Control2.控制:创建对象的控制权3.反转:以前对象是程序员主动去new…...

Spring 事务管理详解及使用
✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏…...

LeetCode 232.用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):实现 MyQueue 类:void push(int x) 将元素 x 推到队列的末尾int pop() 从队列的开头移除并返回元素int peek() 返回队列开头的元…...

go面向对象思想封装继承多态
go貌似都没有听说过继承,当然这个继承不像c中通过class类的方式去继承,还是通过struct的方式,所以go严格来说不是面向对象编程的语言,c和java才是,不过还是可以基于自身的一些的特性实现面向对象的功能,面向…...

【网络原理9】HTTP响应篇
在前两篇文章当中,已经分别介绍了HTTP是什么,以及常见的请求头当中的属性。【网络原理7】认识HTTP_革凡成圣211的博客-CSDN博客HTTP抓包,Fiddler的使用https://blog.csdn.net/weixin_56738054/article/details/129148515?spm1001.2014.3001.…...

SpringCloud之Seata(二)
4.Seata如何应用于项目? 安装seata及修改配置 4.1 官网下载Seata安装包 4.2 修改seata/config.txt 4.2.1 修改存储方式 store.db.dbTypemysql store.db.driverClassNamecom.mysql.jdbc.Driver store.db.urljdbc:mysql://你的IP:3306/seata?useUnicodetrue sto…...

【Redis-入门阶段】基本数据结构
Redis支持多种数据结构,包括字符串、列表、哈希、集合和有序集合。这些数据结构在Redis中被称为键值对,其中键是一个字符串,值可以是一个字符串、列表、哈希、集合或有序集合。接下来,我们将详细介绍这些数据结构的使用方法。字符…...

BACnet协议详解————MS/TP物理层,数据链路层和网络层
文章目录写在前面1 物理层2 数据链路层MSTP的流程如下noteMS/TP帧格式3 网络层写在前面 这周加更一篇,来弥补一下之前落下的进度。简单的说两句,之前讲应用层的时候,只是跟官方的手册来同步一下,但是从个人理解来说,自…...

Tomcat
Tomcat 1 简介 1.1 什么是Web服务器 Web服务器是一个应用程序(软件),对HTTP协议的操作进行封装,使得程序员不必直接对协议进行操作,让Web开发更加便捷。主要功能是"提供网上信息浏览服务"。 Web服务器是安…...

创客匠人直播:构建公域到私域的用户增长模型
进入知识付费直播带货时代,很多拥有知识技能经验的老师和培训机构吃到了流量红利。通过知识付费直播,老师们可以轻松实现引流、变现,还可以突破时间、地域的限制,为全国各地的学员带来优质的教学服务,因此越来越受到教…...

机试指南
文章目录零、绪论和IDE安装int取值范围常犯的编程小错误一、枚举和模拟 (暴力求解)(一) 枚举1.Reverse函数 求 反序数2.程序出错的原因1.编译错误 (compile):基本语法错误2.链接错误 (link):函数名写错了3.运行错误 (run):结果与预期不符&…...

Android CTA认证设定首选网络类型
需求 硬件只支持4G,过CTA认证时打网络电话,会出现3G网络的选择,会导致过不了,需要禁用3G网络选择功能。 Android 8.1.0 分析 可adb命令查看当前的网络类型 getprop | grep “network” 打印如下: [gsm.network.type]: [LTE,LTE] [ro.telephony.default_network]: [9] …...

Android 动态切换应用图标方案
经常听到大家讨论类似的需求,怀疑大厂是不是用了此方案,据我个人了解,多数头部 app 其实都是发版来更新节假日的 icon。当然本方案也是一种可选的方案,以前我也调研过,存在问题和作者所述差不多,此外原文链…...
SMART PLC斜坡函数功能块(梯形图代码)
斜坡函数Ramp的具体应用可以参看下面的文章链接: PID优化系列之给定值斜坡函数(PLC代码+Simulink仿真测试)_RXXW_Dor的博客-CSDN博客很多变频器里的工艺PID,都有"PID给定值变化时间"这个参数,这里的给定值变化时间我们可以利用斜坡函数实现,当然也可以利用PT1…...

不那么认真的linux复习
这是个不那么认真的linux总结,可能有一些错误 1、linuxkernel(内核)shell(外壳)fs(文件系统)pro/uti/tol(应用程序) 2、ls(列出文件) -a…...

Redis系列文章总纲
跟着老万学Redis 前言 从事开发工作这么久,很多核心技术其实都还只是局限在满足日常开发工作中的基础使用,并没有完整的总结研究。今年的目标之一是完成几个技术栈的系列博客,系统的总结一下知识体系,目前计划是从Redis开始。 Re…...

更新丨三大模块升级,助力高效交付商业项目!
功能更新!本文将介绍最新升级的步进漫游、行业方案、VR漫游三个模块,让您更快更好的了解系统能力,为您带来更加便捷、高效的使用体验。步进漫游 离线导出步进式漫游系统,是基于全景图自动生成三维建模的解决方案,实现大…...

C++回顾(二)——const和引用
2.1 C中的const 2.1.1 C与C中const的比较 (1)C语言中的const C语言中 const修饰的变量是一个 常变量,本质还是变量,有自己的地址空间。 (2)C中的const 1、C中 const 变量声明的是一个真正的常量ÿ…...

MXNet中使用双向循环神经网络BiRNN对文本进行情感分类<改进版>
在上一节的情感分类当中,有些评论是负面的,但预测的结果是正面的,比如,"this movie was shit"这部电影是狗屎,很明显就是对这部电影极不友好的评价,属于负类评价,给出的却是positive。…...

DNS 域名解析
介绍域名 网域名称(英语:Domain Name,简称:Domain),简称域名、网域。 域名是互联网上某一台计算机或计算机组的名称。 域名可以说是一个 IP 地址的代称,目的是为了便于记忆。例如,…...

Spring MVC 源码- ViewResolver 组件
ViewResolver 组件ViewResolver 组件,视图解析器,根据视图名和国际化,获得最终的视图 View 对象回顾先来回顾一下在 DispatcherServlet 中处理请求的过程中哪里使用到 ViewResolver 组件,可以回到《一个请求响应的旅行过程》中的 …...

【Hello Linux】初识冯诺伊曼体系
作者:小萌新 专栏:Linux 作者简介:大二学生 希望能和大家一起进步! 本篇博客简介:简单介绍冯诺伊曼体系 冯诺伊曼体系 冯诺伊曼体系结构的合理性 我们在Linux的第一篇博客中讲解了第一台计算机的发明是为了解决导弹的…...

mysql索引,主从多个核心主题去探索问题。
网上收集不错的优化方案 事务 mvcc 详讲 详讲 索引 索引概念 MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据 库系统还维护者满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据, 这样就可以在这些数 据…...

前端一面必会面试题(边面边更)
哪些情况会导致内存泄漏 以下四种情况会造成内存的泄漏: 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。被遗忘的计时器或回调函数: 设置了 setInterval…...

【Hello Linux】初识操作系统
作者:小萌新 专栏:Linux 作者简介:大二学生 希望能和大家一起进步! 本篇博客简介:简单介绍下操作系统的概念 操作系统 操作系统是什么? 操作系统是管理软硬件资源的软件 为什么要设计操作系统 为什么要设…...

完美的vue3动态渲染菜单路由全程
前言: 首先,我们需要知道,动态路由菜单并非一开始就写好的,而是用户登录之后获取的路由菜单再进行渲染,从而可以起到资源节约何最大程度的保护系统的安全性。 需要配合后端,如果后端的值不匹配࿰…...