浅析linux kernel段错误及调试手段

oops,kernel panic, Segmentationfault。相信跟linux kernel打交道的人都会遇到过这些内核提示。根据严重性不一样可以分为:警告,惊慌,还有段错误——也就是我们平常遇到的内核崩溃的情况。对于oops和kernel panic而言,事态过于严重的时候都可能导致Segmentation fault的产生,然后内核会打印出一大堆信息,如下。或许有些内核过了1分钟后会重启,不同的内核配置表现不一样,重启的内核是开了看门狗,在一定时间内不去喂狗,就会强制性地被重启。

rtl8192c_sreset_xmit_status_check REG_TXDMA_STATUS:0x00000001
Unable to handle kernel NULL pointer dereference at virtual address 00000028
pgd = 80004000
[00000028] *pgd=00000000
Internal error: Oops: 17 [#1] PREEMPT
last sysfs file: /sys/kernel/uevent_seqnum
Modules linked in: 8192cu
CPU: 0    Not tainted  (2.6.35.3 #380)
PC is at rtw_txframes_sta_ac_pending+0x44/0x4c [8192cu]
LR is at rtl8192cu_hal_xmit+0x3c/0x15c [8192cu]
pc : [<7f0250e0>]    lr : [<7f044718>]    psr: 40000013
sp : 9c433f48  ip : 8067f638  fp : 9dae60c8
r10: 9c432000  r9 : 00000001  r8 : 9c432000
r7 : 9dae6004  r6 : 9daa64d8  r5 : 9daa6524  r4 : 9daa5000
r3 : 00000018  r2 : 00000001  r1 : 00000001  r0 : 9daa5000
Flags: nZcv  IRQs on  FIQs on  Mode SVC_32  ISA ARM  Segment kernel
Control: 10c5387d  Table: 8c354019  DAC: 00000017
Process RTW_CMD_THREAD (pid: 2370, stack limit = 0x9c4322e8)
Stack: (0x9c433f48 to 0x9c434000)
3f40:                   9daa6524 9daa5000 9dace470 9dae6004 9c432000 00000001
3f60: 9c432000 7f02eea0 9daa6524 7f014dd0 7f014cd0 9c63abc0 9daa5000 9daa6428
3f80: 00003778 0000377c 00000003 9c432000 0000145c 7f000ba4 9daa6428 9c4d1200
3fa0: 9daa6434 9c432000 00000000 9c351de0 9c433fd4 7f000a50 9daa5000 00000000
3fc0: 00000000 00000000 00000000 800c51f0 00000000 00000001 9c433fd8 9c433fd8
3fe0: 00000000 9c351de0 800c5178 8008a9c0 00000013 8008a9c0 e19520b3 e2822001

这是我曾经调试RTL8192cu wifi模块的旧驱动的时候遇到过的一个问题,在wifi长时间工作后就会导致内核出现崩溃,当然,新的rtl8192cu wifi驱动已经修复了这个问题。或许你也会觉得rtl8192cu是一个老掉牙的wifi模块,但是对于我们来说,没有老掉牙的问题。

说到段错误,通常引起段错误的原因有两个:

1、  空指针或野指针导致访问了非法内存地址。

2、  访问临界资源,资源竞态导致死锁

第一种好理解,例如在内核给一个指向NULL的指针赋值。必然导致段错误出现,因为触碰了内核的非法地址。而什么称之为非法地址?是linux kernel会对某些地址进行保护,不允许我们随便访问,这些地址就称之为非法地址。

第二种也好理解,假设有:A线程,B线程,a锁,b锁。A线程获取了a锁,B线程获取了b锁;这时候A线程又想获取b锁的同时,B线程又想获取a锁。就导致了死锁的情况。CPU不能被释放。对于这种CPU长期被占有的情况。内核有一个重要的参数:CONFIG_RCU_CPU_STALL_TIMEOUT。但内核占据一定时间后,内核会把相关的栈信息打印出来(打印这种情况内核一般都是默认打开的),而这个时间就由CONFIG_RCU_CPU_STALL_TIMEOUT来决定。如果有时候看门狗重启太快,导致信息不能被打印出来,这时候就可以适当地修改CONFIG_RCU_CPU_STALL_TIMEOUT来提早在内核重启之前打印错误信息。

对于用户空间跟内核空间而言,一般用户空间导致段错误的原因出现在死锁的情况,也就是资源竞态加锁不恰当,又或者是调度不恰当导致,如在获取锁的情况下调用引起调度的函数等;而一般内核空间导致段错误的情况,这两种都常见,因为内核里面的调度很多,锁也很多。如在中断函数里面使用能引起调度的函数,又或者对空指针的操作,而这个空指针是指向一个非法地址。(因为空指针在没有初始化的情况下,其值是不确定的,通常是内核的一个很大的隐患,不知道什么时候会导致内核错误,所以以上说到wifi在长时间工作后,会导致内核崩溃)。而在用户空间访问的虚拟地址跟内核是有区别的,如果在用户空间访问空指针,可能只会单单引起程序的异常退出,不会导致内核崩溃那么严重。

 

说回上面一段栈信息,有些内核高手可以凭借“pc”,“lr”和“反汇编文件”来推断出错的位置。但这个方法比较不好弄,再加上一般我们的程序很庞大,反汇编文件的内容也自然很多,单单看汇编文件已经够我们折腾的了。所以,在这里,我们说一种比较简单的方法。

这种简单的方法单凭上面的信息是不够的,还需要把函数的调用关系打印出来。在内核配置项里面把CONFIG_FRAME_POINTER打开,那么内核崩溃时候的函数调用关系(也就是我们平常听到的栈回溯信息)就会被打印出来。如下

[<7f0250e0>] (rtw_txframes_sta_ac_pending+0x44/0x4c [8192cu]) from [<7f044718>] (rtl8192cu_hal_xmit+0x3c/0x15c [8192cu])
[<7f044718>] (rtl8192cu_hal_xmit+0x3c/0x15c [8192cu]) from [<7f02eea0>] (rtw_hal_xmit+0x1c/0x20 [8192cu])
[<7f02eea0>] (rtw_hal_xmit+0x1c/0x20 [8192cu]) from [<7f014dd0>] (tx_beacon_hdl+0x100/0x168 [8192cu])
[<7f014dd0>] (tx_beacon_hdl+0x100/0x168 [8192cu]) from [<7f000ba4>] (rtw_cmd_thread+0x154/0x204 [8192cu])
[<7f000ba4>] (rtw_cmd_thread+0x154/0x204 [8192cu]) from [<800c51f0>] (kthread+0x78/0x80)
[<800c51f0>] (kthread+0x78/0x80) from [<8008a9c0>] (kernel_thread_exit+0x0/0x8)
Code: e3120006 1283302c 1a000000 e2833018 (e5930010) 
---[ end trace f67a6be3f98b73c7 ]---
Kernel panic - not syncing: Fatal exception in interrupt
[<8008e4ac>] (unwind_backtrace+0x0/0xf0) from [<804f8aec>] (panic+0x6c/0xe0)
[<804f8aec>] (panic+0x6c/0xe0) from [<8008d388>] (die+0x2b4/0x304)
[<8008d388>] (die+0x2b4/0x304) from [<8008f284>] (__do_kernel_fault+0x64/0x84)
[<8008f284>] (__do_kernel_fault+0x64/0x84) from [<8008f464>] (do_page_fault+0x1c0/0x1d4)
[<8008f464>] (do_page_fault+0x1c0/0x1d4) from [<800892b8>] (do_DataAbort+0x34/0x94)
[<800892b8>] (do_DataAbort+0x34/0x94) from [<80089a2c>] (__dabt_svc+0x4c/0x60)
Exception stack(0x9c433f00 to 0x9c433f48)
3f00: 9daa5000 00000001 00000001 00000018 9daa5000 9daa6524 9daa64d8 9dae6004
3f20: 9c432000 00000001 9c432000 9dae60c8 8067f638 9c433f48 7f044718 7f0250e0
3f40: 40000013 ffffffff
[<80089a2c>] (__dabt_svc+0x4c/0x60) from [<7f0250e0>] (rtw_txframes_sta_ac_pending+0x44/0x4c [8192cu])
[<7f0250e0>] (rtw_txframes_sta_ac_pending+0x44/0x4c [8192cu]) from [<7f044718>] (rtl8192cu_hal_xmit+0x3c/0x15c [8192cu])
[<7f044718>] (rtl8192cu_hal_xmit+0x3c/0x15c [8192cu]) from [<7f02eea0>] (rtw_hal_xmit+0x1c/0x20 [8192cu])
[<7f02eea0>] (rtw_hal_xmit+0x1c/0x20 [8192cu]) from [<7f014dd0>] (tx_beacon_hdl+0x100/0x168 [8192cu])
[<7f014dd0>] (tx_beacon_hdl+0x100/0x168 [8192cu]) from [<7f000ba4>] (rtw_cmd_thread+0x154/0x204 [8192cu])
[<7f000ba4>] (rtw_cmd_thread+0x154/0x204 [8192cu]) from [<800c51f0>] (kthread+0x78/0x80)
[<800c51f0>] (kthread+0x78/0x80) from [<8008a9c0>] (kernel_thread_exit+0x0/0x8)
RTW_CMD_THREAD: page allocation failure. order:2, mode:0x4020
[<8008e4ac>] (unwind_backtrace+0x0/0xf0) from [<800e9634>] (__alloc_pages_nodemask+0x500/0x568)
[<800e9634>] (__alloc_pages_nodemask+0x500/0x568) from [<800e96ac>] (__get_free_pages+0x10/0x24)
[<800e96ac>] (__get_free_pages+0x10/0x24) from [<803ff7e4>] (__alloc_skb+0x50/0xe0)
[<803ff7e4>] (__alloc_skb+0x50/0xe0) from [<804003e4>] (__netdev_alloc_skb+0x1c/0x44)
[<804003e4>] (__netdev_alloc_skb+0x1c/0x44) from [<7f0454d4>] (usb_read_port+0xbc/0x214 [8192cu])
[<7f0454d4>] (usb_read_port+0xbc/0x214 [8192cu]) from [<7f007194>] (_rtw_read_port+0x38/0x3c [8192cu])
[<7f007194>] (_rtw_read_port+0x38/0x3c [8192cu]) from [<7f0457c8>] (usb_read_port_complete+0x19c/0x308 [8192cu])
[<7f0457c8>] (usb_read_port_complete+0x19c/0x308 [8192cu]) from [<803354f4>] (usb_hcd_giveback_urb+0xa0/0xec)
[<803354f4>] (usb_hcd_giveback_urb+0xa0/0xec) from [<80347998>] (ehci_urb_done+0xc4/0xe0)
[<80347998>] (ehci_urb_done+0xc4/0xe0) from [<80347a80>] (qh_completions+0xcc/0x4c8)
[<80347a80>] (qh_completions+0xcc/0x4c8) from [<803491cc>] (ehci_work+0xc8/0x944)
[<803491cc>] (ehci_work+0xc8/0x944) from [<8034c800>] (ehci_irq+0x358/0x3bc)
[<8034c800>] (ehci_irq+0x358/0x3bc) from [<80334dc0>] (usb_hcd_irq+0x38/0x90)
[<80334dc0>] (usb_hcd_irq+0x38/0x90) from [<800dc878>] (handle_IRQ_event+0x24/0xe4)
[<800dc878>] (handle_IRQ_event+0x24/0xe4) from [<800de940>] (handle_level_irq+0xd4/0x180)
[<800de940>] (handle_level_irq+0xd4/0x180) from [<8008906c>] (asm_do_IRQ+0x6c/0x8c)
[<8008906c>] (asm_do_IRQ+0x6c/0x8c) from [<80089a8c>] (__irq_svc+0x4c/0xcc)

对于以上一段信息,调用关系很明确,但即使打印了这么多的关系出来,有时候还是看不出具体出现错误的函数,因为栈信息有时候也会把一些函数跳过去。如a函数调用b函数,再调用c函数。栈信息把b函数跳过去了。直接打印a函数调用c函数。这点略过跟编译器有点相像,在用keil编程的时候,我们会发现,有些时候当我们把编译优化等级提高,会导致有些写得不好的,或者很简单的函数或语句会被编译器忽略掉。这就会导致当我们在运行程序的时候,找不到相应的执行代码。当然,这种情况,编译器是直接舍弃了对于的代码,而内核只是跳过了打印调用关系,其实内容还是存在的。

以上两段打印信息有几个参数我们要重点看待:

PC is at rtw_txframes_sta_ac_pending+0x44/0x4c [8192cu]
LR is at rtl8192cu_hal_xmit+0x3c/0x15c [8192cu]
pc : [<7f0250e0>]    lr : [<7f044718>]    psr: 40000013

看pc指针停留的地方和出错的地方相对pc指针的偏移量。然后我们要对比system.map  还有cat/proc/kallsyms 里面的函数地址,看错误是出现在内核函数还是驱动模块当中。这种情况,模块函数出问题的居多。

接下来,就要反汇编了(内核出错就反汇编vmlinux,模块出错就反汇编ko文件),使用命令:

arm-linux-objdump –D xx.ko > dump.txt

这条命令就是用反汇编工具反汇编模块ko文件,然后重定向输出到 dump.txt文件里面去。然后通过pc制作和出错的偏移量,就可以定位出具体出错的位置。

 

而如果是用户空间使用调度函数导致的内核死锁的情况,也可以把用户空间的栈信息通过串口打印出来,具体是内核的CONFIG_DEBUG_USER 选项,打开就好。

然后修改uboot启动时的环境变量,user_debug=0xff,因为内核在打印回溯信息的时候会判断user_debug的值

搜索到__do_user_fault  fault.c

#ifdef CONFIG_DEBUG_USER
if(```)
{
	usigned long ret;
	unsigned long val;
	int i = 0;
	```
	
	printk("begin to printk stack: \n");
	while(i < 1024)
	{
		if(copy_from_user(&val ,(const void __user *)(reg->ARM_sp + i*4) , 4))
			break;
		
		printk("%08x " , val);
		i++;
		if(i % 8 == 0)
			printk("\n");
	}
	printk("end of ptintk stack: \n");
}

在网上也有介绍一下再用户空间编程把栈信息打印出来的方法,调用以下两个函数:

size = backtrace (array, 10);
strings = backtrace_symbols (array, size);

不过这种情况略为不保险,因为内核崩溃的情况下,是没有那么多时间去打印用户空间的信息的。如果是单单调试用户程序,使用这种方法就没问题。

附上在网上找的这两个函数的使用例子

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <linux/unistd.h>
#include <pthread.h>
#include <execinfo.h>
#include <sys/syscall.h>  
 
#define gettid() syscall(__NR_gettid) 
 
void do_backtrace(int s)
{
        void* buff[32] = {0};
        int n = 0;
 
        int nstack = backtrace(buff,32);
        char** callstack = backtrace_symbols(buff,nstack);
 
        for(n = 0; n < nstack; ++n)
        {
                printf("%d %d : %s\n", (int)gettid(), n, callstack[n]);
        }
 
        free(callstack);
        printf("do_backtrace done\n");
}
 
void* test_thread_proc(void* arg)
{
        while(1) {
                usleep(10000);
        }
return 0;
}
 
int main()
{
        pthread_t tid;
        signal(SIGUSR1, do_backtrace);
        pthread_create(&tid, NULL, test_thread_proc, NULL);
        while(1) {
                usleep(10000);
        }
 
return 0;
}

在调试调度或者死锁引起的内核崩溃的时候,有两个内核选项也有助于调试,多任务(SMP)与抢占(PREEMPT)

CONFIG_SMP=y
CONFIG_SMP_ON_UP=y
CONFIG_PREEMPT 
CONFIG_PREEMPT_NONE 

在一个多核的系统中,多任务与抢占是必不可少的。并且在一个SMP的系统中,run queue的数目是逻辑cpu的数目,所以,由此而知,如果我们把SMP关闭,那么CPU就相当于运行在单核的状态下。其中CONFIG_SMP是在编译的时候生效的,CONFIG_SMP_ON_UP是在内核启动的过程中生效的,如果把这个选项关闭,内核启动的过程中就不会去在线检测CPU的核的个数。而CONFIG_PREEMPT是支持内核抢占,CONFIG_PREEMPT_NONE是不支持内核抢占。但即使在单核的情况下也能发生。也有主动让出CPU的情况,这是因为内核有些地方为了能让程序执行更有效率;以减少内核延时;有更好的实时性。从而会主动的schedule,放弃对CPU的占有,内核也有相关的配置可以选。有空研究研究也挺有意思。










  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值