前言
在编写linux驱动程序时,最让人头疼的,莫不是内核发生了oops错误,并打印了一大堆错误信息如下:
Unable to handle kernel NULL pointer dereference at virtual address 00000000
pgd = 23b16ac7
[00000000] *pgd=00000000
Internal error: Oops: 80000005 [#1] PREEMPT SMP ARM
Modules linked in: ssd1306fb_spi(O+) g_multi snd_soc_fsl_asrc snd_soc_core snd_pcm_dmaengine snd_pcm snd_timer [last unloaded: ssd1306fb_spi]
CPU: 0 PID: 476 Comm: insmod Tainted: G O 4.19.35-imx6 #1stable
Hardware name: Freescale i.MX6 UltraLite (Device Tree)
PC is at (null)
LR is at bit_putcs+0x270/0x40c
pc : [<00000000>] lr : [<80497a6c>] psr: 600f0013
sp : 86edba18 ip : 00000001 fp : 86008800
r10: 00000000 r9 : 000000ff r8 : 00000001
r7 : 00000010 r6 : 876e9120 r5 : 87692010 r4 : ffffffff
r3 : 00000000 r2 : 00000000 r1 : 86edba7c r0 : 8741a000
Flags: nZCv IRQs on FIQs on Mode SVC_32 ISA ARM Segment none
Control: 10c53c7d Table: 8686406a DAC: 00000051
Process insmod (pid: 476, stack limit = 0xa0438ff4)
Stack: (0x86edba18 to 0x86edc000)
...
[<80497a6c>] (bit_putcs) from [<80491a60>] (fbcon_putcs+0xf4/0x10c)
[<801ae464>] (do_init_module) from [<801ad38c>] (load_module+0x2174/0x257c)
[<801ad38c>] (load_module) from [<801ada18>] (sys_finit_module+0xc4/0x110)
[<801ada18>] (sys_finit_module) from [<80101000>] (ret_fast_syscall+0x0/0x54)
Exception stack(0x86edbfa8 to 0x86edbff0)
bfa0: 336e7f00 00000000 00000003 004cc7e0 00000000 7ebc7c38
bfc0: 336e7f00 00000000 00000000 0000017b 0238b160 00000000 7ebc7db8 00000000
bfe0: 7ebc7be8 7ebc7bd8 004c4e41 76ca0d92
Code: bad PC value
---[ end trace 701ce5fc9b7e7151 ]---
常见的方法是利用objdump工具进行反汇编,然后使用gbd的list指令加上出错函数和偏移量,就可以定位出错的具体位置,前提是有源代码的情况下。
上面的oops打印信息展示了内核出错时程序的调用关系和出错的原因。可以看到内核发生错误是在fbcon_putcs+0xf4/0x10c处,并且是因为访问了NULL指针所导致的错误,其中0xf4表示的是位于该函数第0x18字节处,0x10c表示该函数共0x10c个字节。尽管如此,我们还是不知道具体是到底是哪里使用了NULL指针。有没有什么工具可以利用这些信息定位到具体是某个文件的某一行呢?说到这里,不得不吐槽一下,嵌入式真是一步一个脚印(坑)!使用单片机时,有硬件错误HardFault;本想着进入linux世界,起码会好点,结果还是泪。。。得益于之前调试HardFault的cmBackTrace工具,该工具本质上是利用addr2line和输出的函数调用栈信息,还原发生错误时的现场信息,定位问题代码位置。抱着尝试的态度,有了这篇文章,作为记录。
addr2line工具
addr2line工具是一个可以将指令的地址和可执行映像转换为文件名、函数名和源代码行数的工具,更多的介绍,可以进行百度/谷歌。下面列举之后会用到的参数以及对应含义:
参数 | 作用 |
---|---|
-e | 指定需要转换地址的可执行文件名 |
-f | 在显示文件名、行号输出信息的同时显示函数名信息 |
-p | 输出信息更加人性化 |
-a | 在函数名、文件和行号信息之前,显示地址,以十六进制形式 |
内核配置CONFIG_DEBUG_INFO
使用addr2line工具之前,请确保内核配置正确:CONFIG_DEBUG_INFO=y
,该配置使能以调试方式编译内核,这样编译生成的vmlinux文件才会带有调试信息。make menuconfig位置位于:Kernel hacking > Compile-time checks and compiler options>[] Compile the kernel with debug info处。
使用addr2line进行定位
可以看到addr2line工具,为我们定位到文件drivers/video/fbdev/core/bitblit.c的第192行。该文件的第192行位于函数bit_putcs之中。
static void bit_putcs(struct vc_data *vc, struct fb_info *info,
const unsigned short *s, int count, int yy, int xx,
int fg, int bg)
{
/* 省略部分代码 */
184 if (!mod)
185 bit_putcs_aligned(vc, info, s, attribute, cnt, pitch,
186 width, cellsize, &image, buf, dst);
187 else
188 bit_putcs_unaligned(vc, info, s, attribute, cnt,
189 pitch, width, cellsize, &image,
190 buf, dst);
191
192 image.dx += cnt * vc->vc_font.width;
/* 省略部分代码 */
}
在bit_puts函数的第192行之前调用了函数bit_putcs_unaligned,bit_putcs_unaligned函数的结尾调用了函数指针info->fbops->fb_imageblit。
static inline void bit_putcs_unaligned(struct vc_data *vc,
struct fb_info *info, const u16 *s,
u32 attr, u32 cnt, u32 d_pitch,
u32 s_pitch, u32 cellsize,
struct fb_image *image, u8 *buf,
u8 *dst)
{
/* 省略部分代码 */
info->fbops->fb_imageblit(info, image);
}
而我编写的驱动模块,fb_ops结构体确实没有对成员fb_imageblit进行赋值,最终成功定位到使用NULL指针的具体位置。实际测试,将一个空函数赋值给fb_fbops函数的成员fb_imageblit,加载模块时,内核也不提示oops错误,证明找对地方了。
static struct fb_ops fb_fbops = {
.owner = THIS_MODULE,
.fb_read = fb_sys_read,
};