一次硬件故障导致的kernel-panic排查
前言
最近遇到一个由于硬件故障导致的kernel panic
, 把过程记录下来,方便后续整理思路,逐渐完成自己的知识库。之前记录这些太少了,导致很多之前的经验后面又需要重头分析一遍,很花费时间。
分析过程
第一步收集内核的coredump
以及带符号表的vmlinux
二进制文件,关于如何打开内核coredump
以及获取对应的vmlinux
文件,这里不再多说,请自行Google
。
先用看下内核挂掉的时候调用栈
在哪个地方,看下能否发现一些蛛丝蚂迹:
结合使用使用dmesg
看下内核挂在什么地方,看下能不能看到些什么异常,看是否能够匹配上。
从dmesg
的信息基本可以确认,这个kernel panic
是由于在 __rmqueue+0x35a/0x4d0
这条指令的地方访问了一个非法地址导致的kernel panic
, 我们想要知道这条指令是什么,于是用crash
工具里面的dis
命令看下这条指令是什么。
dis -l __rmqueue -x
,输出如下:我这里仅仅是截图 __rmqueue+0x35a
附近的代码出来
打开对应的源代码,看下是在干什么事情:
这里很奇怪,不是说是在__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
),看下有内容没有,是否可以通过这些内存的内容排查下:
用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
函数的参数传递:
在之前的文章中我们讲到,一般来说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代码如下:
通过这里可以确认,函数代码执行到出问题的地方,r13
寄存器里面存放的还是struct zone
, 0x88
是free_area
在struct zone
中的偏移,我们可以通过struct
命令来进一步确认一下:
没错吧,r13
现在存放的是struct zone
的地址。我们查看下zone->free_area
的内容:先看下r13
的内容
ffff88407ff98e80
就是struct zone
的内容。我们使用如下命令zone.free_area
ffff88407ff98e80
查看下free_area
的内容:
如下的内容比较奇怪:
我解释一下下面的内容,由于nr_free
等于3, 代表这个free_list
中有三个块, 当free_list
链表为空的时候:
也就是next
和prev
都是指向的free_list
, 我们可以反推如果为空,那么next
和prev
的地址都在struct zone
中, 如果prev
和next
指针相等,那么还有一种情况,就是链表只有一个元素,但是next
和prev
地址都不在struct zone
中
那么可以推测出:
前三项都只有一个元素,后两项为空。于是我们看下第一项的内容
next = 0xffffe200d1eef028
prev = 0xffffea00d1eef028
0xffffe200d1eef028
这个地址理论上应该是0xffffea00d1eef028
, 而两个地址只有”ea
“和“e2
”这里不同, a=0x1010 , 2=0x0010
, 一般来说踩内存问题不会只更改一个bit
位,而且这个位在中间,很大可能是内存硬件bit
位翻转导致。于是讲问题交给硬件去排查,果然发现了一些异常信息。这个锅甩给硬件了。
值得注意的是,这种问题也不一定百分百是硬件问题,万一有个执行路径,就是随机踩掉这一个bit也是有可能的,但是这种bit位变化,可以先让硬件排查,如果是其他问题,那么就得继续分析了。踩内存一向比较棘手,后面遇到了再具体分析。