一次硬件故障导致的kernel-panic排查

一次硬件故障导致的kernel-panic排查

Posted by Albert on April 26, 2021

一次硬件故障导致的kernel-panic排查

前言

​ 最近遇到一个由于硬件故障导致的kernel panic, 把过程记录下来,方便后续整理思路,逐渐完成自己的知识库。之前记录这些太少了,导致很多之前的经验后面又需要重头分析一遍,很花费时间。

分析过程

​ 第一步收集内核的coredump以及带符号表的vmlinux二进制文件,关于如何打开内核coredump以及获取对应的vmlinux文件,这里不再多说,请自行Google

​ 先用看下内核挂掉的时候调用栈在哪个地方,看下能否发现一些蛛丝蚂迹:

image-20210511101311031

结合使用使用dmesg看下内核挂在什么地方,看下能不能看到些什么异常,看是否能够匹配上。

image-20210511102153284

​ 从dmesg的信息基本可以确认,这个kernel panic是由于在 __rmqueue+0x35a/0x4d0这条指令的地方访问了一个非法地址导致的kernel panic , 我们想要知道这条指令是什么,于是用crash工具里面的dis命令看下这条指令是什么。

dis -l __rmqueue -x,输出如下:我这里仅仅是截图 __rmqueue+0x35a附近的代码出来

image-20210511103108800

打开对应的源代码,看下是在干什么事情:

image-20210511103546625

image-20210511103738502

这里很奇怪,不是说是在__rmqueue 这个函数中吗?为什么跑到__rmqueue_smallest里面来了,请仔细看:__rmqueue 中调用了

__rmqueue_smallest,而__rmqueue_smallest是一个inline的内联函数,所以本质上也是属于__rmqueue 的。这里就不再多说了。

static struct page *__rmqueue(struct zone *zone, unsigned int order,
						int migratetype)
{
	struct page *page;

retry_reserve:
	page = __rmqueue_smallest(zone, order, migratetype);

	if (unlikely(!page) && migratetype != MIGRATE_RESERVE) {
		page = __rmqueue_fallback(zone, order, migratetype);

		/*
		 * Use MIGRATE_RESERVE rather than fail an allocation. goto
		 * is used because __rmqueue_smallest is an inline function
		 * and we want just one call site
		 */
		if (!page) {
			migratetype = MIGRATE_RESERVE;
			goto retry_reserve;
		}
	}

	trace_mm_page_alloc_zone_locked(page, order, migratetype);
	return page;
}

从源代码可以看出,看起来是从伙伴系统上面取页描述符下来的时候出了问题,第一反应是,这个地址怎么会出现错误呢,这可是一个基础函数啊,这怎么可能出错。我们继续看下这个出错的内存地址(ffffe200d1eef028),看下有内容没有,是否可以通过这些内存的内容排查下:

image-20210511111632011

rd命令看下这段内存,发现这段内存是一个非法的内存地址,于是我们只能在看代码了。

static inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
						int migratetype)
{
	unsigned int current_order;
	struct free_area * area;
	struct page *page;

	/* Find a page of the appropriate size in the preferred list */
	for (current_order = order; current_order < MAX_ORDER; ++current_order) {
		area = &(zone->free_area[current_order]);
		if (list_empty(&area->free_list[migratetype]))
			continue;

		page = list_entry(area->free_list[migratetype].next,
							struct page, lru);
		list_del(&page->lru);
		rmv_page_order(page);
		area->nr_free--;
		expand(zone, page, order, current_order, area, migratetype);
		return page;
	}

	return NULL;
}

从代码可以看到,是在操作&page->lru这个字段出现的问题,我们首先排查下这个struct page, 而这个struct page是从伙伴系统拿下来的,我们来看看struct free_area里面的内容是什么。看下能不能找到什么异常,我们可以通过汇编代码看下,怎么去取得struct free_area的内容,首先看下__rmqueue函数的参数传递:

image-20210511142225390

在之前的文章中我们讲到,一般来说x64进行函数参数传递的时候,一般会把第一个参数传递给rdi这个寄存器,函数一进来又把rdi的值传递给r13. 也就是说后续我们可以在r13里面取struct zone的地址。接下来看下出问题附近的代码,看下是不是这样,因为那附近也有用zone值的代码:

/usr/src/debug/kernel-default-3.0.101/linux-3.0/mm/page_alloc.c: 835
0xffffffff81104542 <__rmqueue+0x342>:   lea    (%rcx,%rdx,2),%rdx
0xffffffff81104546 <__rmqueue+0x346>:   shl    $0x3,%rdx
0xffffffff8110454a <__rmqueue+0x34a>:   lea    (%r11,%rdx,1),%rax
0xffffffff8110454e <__rmqueue+0x34e>:   mov    0x88(%r13,%rax,1),%rsi ;这种move指令一般是访问数组元素
0xffffffff81104556 <__rmqueue+0x356>:   lea    -0x28(%rsi),%r14
/usr/src/debug/kernel-default-3.0.101/linux-3.0/include/linux/list.h: 106
0xffffffff8110455a <__rmqueue+0x35a>:   mov    0x28(%r14),%rcx
0xffffffff8110455e <__rmqueue+0x35e>:   mov    0x30(%r14),%rax

重点关注汇编代码:0x88(%r13,%rax,1),%rsi,其对应的c代码如下:

image-20210511142906011

通过这里可以确认,函数代码执行到出问题的地方,r13寄存器里面存放的还是struct zone, 0x88free_areastruct zone中的偏移,我们可以通过struct命令来进一步确认一下:

image-20210511143316995

没错吧,r13现在存放的是struct zone的地址。我们查看下zone->free_area的内容:先看下r13的内容

image-20210511143549749

ffff88407ff98e80就是struct zone的内容。我们使用如下命令zone.free_area ffff88407ff98e80查看下free_area的内容:


如下的内容比较奇怪:

image-20210511144248853

我解释一下下面的内容,由于nr_free等于3, 代表这个free_list中有三个块, 当free_list链表为空的时候:

image-20210511145134111

也就是nextprev都是指向的free_list, 我们可以反推如果为空,那么nextprev的地址都在struct zone中, 如果prevnext指针相等,那么还有一种情况,就是链表只有一个元素,但是nextprev地址都不在struct zone

image-20210511145955432

那么可以推测出:

image-20210511170054671

前三项都只有一个元素,后两项为空。于是我们看下第一项的内容

        next = 0xffffe200d1eef028
        prev = 0xffffea00d1eef028

0xffffe200d1eef028这个地址理论上应该是0xffffea00d1eef028, 而两个地址只有”ea“和“e2”这里不同, a=0x1010 , 2=0x0010, 一般来说踩内存问题不会只更改一个bit位,而且这个位在中间,很大可能是内存硬件bit位翻转导致。于是讲问题交给硬件去排查,果然发现了一些异常信息。这个锅甩给硬件了。

值得注意的是,这种问题也不一定百分百是硬件问题,万一有个执行路径,就是随机踩掉这一个bit也是有可能的,但是这种bit位变化,可以先让硬件排查,如果是其他问题,那么就得继续分析了。踩内存一向比较棘手,后面遇到了再具体分析。