linux驱动开发——内核调试技术

目录

一、前言

二、内核调试方法

2.1 内核调试概述

2.2 学会分析内核源程序

2.3调试方法介绍

三、内核打印函数

3.1内核镜像解压前的串口输出函数

3.2 内核镜像解压后的串口输出函数

3.3 内核打印函数

四、获取内核信息

4.1系统请求键

4.2 通过/proc 接口

4.3 通过/sys 接口

4.3.1.属性

4.3.2子系统操作函数

五、处理出错信息

5.1 oops 信息

5.1.1.oops 消息包含系统错误的详细信息

5.1.2.使用 ksymoops 转换 oops 信息

5.1.3,内核 kallsyms 选项支持符号信息

5.2 panic

5.3 通过ioctl 方法

六、内核源码调试

七、习题


一、前言


        编写驱动程序难免会遇到一些问题,要快速地解决这些问题,就需要熟练掌握内核的各种调试方法。本章介绍了各种 Linux内核调试方法,内核的调试需要从内核源码本身、调试工具等方面做好准备。通过本章的学习,可以了解不同调试方法的特点和使用方法,再根据需要选择不同的内核调试方式。
 

二、内核调试方法



        对于庞大的 Linux 内核软件工程,单靠阅读代码查找问题已经非常围难,需要借助调试技术解决 BUG。通过合适的调试手段,可以有效地查找和判断 BUG 的位置和原因。

2.1 内核调试概述


        当内核运行出现错误的时候,首先要明确定义和可靠地重现这个错误现象。如果个BUG 不能重现,修正时只能凭想象和读代码。内核、用户空间和硬件之间的交互非复杂,在特定配置、特定机器、特殊负载条件下,运行某些程序可能会产生一个BUG,但在其他条件下就不一定产生。这在嵌入式 Linux 系统上很常见,例如:在 X86 平台运行正常的驱动程序,在 ARM 平台上就可能会出现 BUG。在跟踪 BUG 的时候,掌提的信息越多越好。
        内核的 BUG 是多种多样的,可能由于不同原因出现,并且表现形式也多种多样。BUG的范围从完全不正确的代码(如没有在适当的地址存储正确的值)到同步的错误(如不适当地对一个共享变量加锁)。BUG 的表现形式也各种各样,从系统崩溃的错误操作到系统性能差等。
        通常 BUG 是一系列事件,内核代码的错误使用户程序出现错误。例如,一个不带引用计数的共享结构可能引起条件竞争。没有合适的统计,一个进程可以释放这个结构,但是另外一个进程仍然想要用它。第二个进程可能会使用一个无效的指针访问一个不存在的结构。这就会导致空指针访问、读垃圾数据,如果这个数据还没有被覆盖,也可能基本正常。对空指针访问会产生 oops; 垃圾数据会导致数据错误(接下来可能是错误的行为或者 oops); 内核报告 oops 或者错误的行为。内核开发者必须处理这个错误,知道这个数据是在释放以后访问的,这存在一个条件竞争。修正的方法是为这个结构添加引用计数,并且可能需要加锁保护。
        调试内核很难,实际上内核不同于其他软件工程。内核有操作系统独特的问题,例
如时间管理和条件竞争,这可以使多个线程同时在内核中执行。

        因此,调试 BUG 需要有效的调试手段。几乎没有一种调试工具或者方法能够解决全部问题。即使在一些集成测试环境中,也要划分不同测试调试功能,例如跟踪调试、内存泄漏测试、性能测试等。掌握的调试方法越多,调试 BUG 就越方便。Linux 有很多放源码的工具,每一个工具的调试功能都是专一的,所以这些工具的实现一般也比较简单。


2.2 学会分析内核源程序


        由于内核的复杂性,无论使用什么调试手段,都需要熟悉内核源码。只有熟悉了内核各部分的代码实现,才能够找到准确的跟踪点; 只有熟悉操作系统的内核机制,才能准确地判断系统运行状态。
        对于初学者来说,阅读内核源码将是非常枯燥的工作。最好先掌握一种搜索工具,学会从源码树中搜索关键词。当能够对内核源码进行情景分析的时候,你就能感到其中的乐趣了。
调试是无法逃避的任务。进行调试有很多种方法,比如将消息打印到屏幕上、使用调试器,或只是考虑程序执行的情况并仔细地分析问题所在。
        在修正问题之前,必须先找出问题的源头。举例来说,对于段错误,需要了解段错误发生在代码的哪一行。一旦发现了代码中出错的行,就要确定该方法中变量的值、方法被调用的方式以及错误如何发生的详细情况。使用调试器将使找出所有这些信息变得很简单。如果没有调试器可用,还可以使用其他的工具。(请注意: 有些 Linux 软件产品中可能并不提供调试器)。

2.3调试方法介绍

内核调试方法很多,主要有以下四类。
。打印函数。
。获取内核信息
。处理出错信息
。内核源码调试
        在调试内核之前,通常需要配置内核的调试选项。下图给出了“Kernel hacking”菜单下的各种调试选项。不同的调试方法需要配置对应的选项。


        每一种调试选项都有不同的调试功能,并且不是所有的调试选项在所有的平台上都能内核调试技术被支持。这里介绍一些“Kernel hacking”的调试选项,具体配置使用可以根据情况选择。
(1) printk and dmesg options
        该子菜单中的若干选项用来决定 printk 打印和dmesg 输出的一些特性,如是否在打印信息前加上时间信息、默认的打印级别以及延迟打印的时间。

(2) Compile-time checks and compiler options
        该子菜单中的若干选项用来决定编译时的检查和设置一些编译选项,如内核是否可调试(是否加“-g”选项);是否使能“ deprecated”逻辑(禁止该选项将不会得到如warning:'foo' is deprecated (declared at kernel/power/somefile.c:1234)”等信息);是否使能must check”逻辑(禁止该选项将不会进行必须检查,如有的函数的返回值必须要求检查,如果没有检查编译器将会产生警告);设置栈的帧数上限值等。
(3) Magic SysRq key
        CONFIG_MAGIC_SYSRQ(Magic SysRg key 选项所对应的内核源码宏定义,后面选项类似,不再进行说明) 使能系统请求键,可以用于系统调试。
(4)Kernel debugging
        CONFIGDEBUG_KERNEL 选择调试内核选项以后,才可以显示有关的内核调试子项。大部分内核调试选项都依赖于它。
(5) Memory Debugging
        该子菜单中的若干选项用来选择内核内存调试的一些选项。
(6) Debug shared IRO handlers
        CONFIG_DEBUG_SHIRQ 共享中断的相关调试使能。
(7) Debug Lockups and Hangs
        该子菜单中的若干选项用来选择内核死锁和挂起的一些调试功能,如死锁检测、挂起检测、挂起的超时设置等。

(8) Panic on Oops
        CONFIG_PANIC_ON_OOPS 在 Oops 信息输出后是否 Panic,内核输出 Oops 信息不意味着内核就一定不能继续往下运行选择该选项意味着一旦 Oops 后内核就在一个预定的时间后重启和一直死循环。
(9) panic timeout
        CONFIG_PANIC_TIMEOUT 配置 Panic 的超时值,为0 表示死循环

(10) Collect scheduler debugging info

        CONFIG_SCHED_DEBUG 调度器调试信息收集,保存在/proc/sched_debug 文件中
(11) Collect scheduler statistics
        CONFIG_SCHEDSTATS 调度器统计信息收集,保存在/proc/schedstat 文件中

(12) Collect kernel timers statistics

        CONFIG_TIMER_STATS 定时器统计信息收集,保存在/procimer_stats 文件中
(13) Debug preemptible kernel
        CONFIG_DEBUG_PREEMPT 使能内核抢占调试功能。如果在非抢占安全的状况下使用,将打印警告信息,还可以探测抢占技术下溢。

(14) Lock Debugging (spinlocks, mutexes, etc...)
        自旋锁、互斥锁的一些调试选项。
(15) kobject debugging
        CONFIG_DEBUG_KOBJECT 使能一些额外的 kobject 调试信息发送到 syslog。
(16) Debug filesystem writers count 
        CONFIG_DEBUGWRITECOUNT 使能后能捕对 vfsmount 结构中的针对写者进行计数的成员的错误使用。
(17) Debug linked list manipulation
        CONFIG_DEBUG_LIST 使能对链表使用的额外检查。
(18) Debug SG table operations
        CONFIG_DEBUG_SG使能对集一散表的检查能帮助驱动找到未能正确初始化集一
散表的问题。
(19) Debug notifier call chains
        CONFIG_DEBUG_NOTIFIERS 使能对通知调用链的完整性检查,帮助内核开发者确定模块正确地从通知调用链上注销。
(20) Debug credential management
        CONFIG DEBUG CREDENTIALS 使能一些对证书管理的调试检查
(21) RCU Debugging

RCU 的一些调试选项。
(22) Force extended block device numbers and spread them
        CONFIG_DEBUG_BLOCK_EXT_DEVT 用于强制大多数块设备号是从扩展空间分配并延伸它们,以便发现那些假定设备号是按预先决定的连续设备号进行分配的内核或用户代码路径。使能该选项可能导致内核启动失败。
(23) Notifier error injection
        CONFIG_NOTIFIER_ERROR_INJECTION 提供人为向特定通知链回调的功能,如错误。
(24) Fault-injection framework
        CONFIG_FAULT_INJECTION 提供失败注入框架
(25) Tracers
        跟踪器的一些选项。
(26) Runtime Testing
        运行时测试选项。
(27) Enable debugging of DMA-API usage
        CONFIG_DMA_API_DEBUG 用于设备驱动对 DMA 的API函数的使用调试。
(28) Test module loading with "hello world' module

        CONFIG_TEST_MODULE 编译一个“test module”模块,用于模块加载测试

(29) Test user/kernel boundary protections
        CONFIG_TEST_USER_COPY 编译一个“test_user _copy”模块,用于测试内核空间和用户空间的数据复制 (copy_to/from_user) 是否能正常工作。
(30) Sample kernel code
        CONFIG_SAMPLES 用于编译一些内核的实例代码,如 kobiect 和 kfifo 的实例代码
(31) KGDB: kernel debugger
        CONFIG_KGDB 内核远程调试的选项。
(32) Export kernel pagetable layout to userspace via debugfs
        CONFIG_ARM_PTDUMP 通过 debugfs 向用户空间导出内核空间的页表布局
(33) Filter access to /dev/mem
        CONFIG_STRICT_DEVMEM 禁止该选项则允许用户空间访问整个内存,包括用户空间和内核空间的所有内存。
(34) Enable stack unwinding support (EXPERIMENTAL)
        CONFIG_ARM_UNWIND 使用编译器在内核自动生成的信息来提供栈展开的支持
(35) Verbose user fault messages
        CONFIG_DEBUG_USER 当一个应用程序因为异常崩溃时,内核打印一个是什么原因造成崩溃的简短信息。
(36) Kernel low-level debugging functions
        CONFIG_DEBUG_LL用于在内核中包含 printascii、printch 和 printhex 函数的定义这对于调试在控制台初始化之前代码会很有帮助。但是这会指定一个串口,给移植性带来了一些问题。
(37) Kernel low-level debugging port (Use S3C UART 2 for low-level debug)

        选择串口 2 作为内核低级别调试输出端口。

(38) Early printk

        CONFIG EARLY PRINTK 使能内核的早期打印输出
(39) On-chip ETM and ETB
        CONFIG OC ETM 使能片上入的跟踪宏单元跟踪缓存驱动。
( 40) Write the current PID to the CONTEXTIDR register
        CONFIG PID IN CONTEXTIDR 使能该选项后,内会把当前进程的 PID 写入CONTEXTIDR 寄存器的 PROCID 域。

(41)Set loadable kernel module data as NX and text as RO

        CONFIG DEBUG SET_MODULE_RONX 用于对可加模块的代码段和只读数据段的意外修改。


三、内核打印函数


        嵌入式系统一般都可以通过串口与用户交互。大多数 Bootloader 可以向串口打印信息,并且接收命令。内核同样可以向串口打印信息。但是在内核启动过程中,不同阶段的打印函数不同。分析这些打印函数的实现,可以更好地调试内核。

3.1内核镜像解压前的串口输出函数


如果在配置内核时选择了以下的选项:

System Type --->

        (2) S3C UART to use for low-level messages
Kernel hacking --->

        [*] Kernel low-level debugging functions (read help!)

                Kernel low-level debugging port (Use S3C UART 2 for low-level debug)


那么在内核自解压时就会通过串口 2打印如下信息:

.Uncompressing Linux...done, booting the kernel


        这句话的打印是因为在 decompresss_kernel()函数中调用了 putstr()函数,直接向串口打印内核解压的信息。
        putstr()函数实现了向串口输出字符串的功能。因为不同的处理器可以有不同的串口控制器,所以putstr()函数的实现依赖于硬件平台。下面分析一下 Exynos4412平台中putstr()函数的使用及实现。
 

/*
 * misc.c
 * 
 * This is a collection of several routines from gzip-1.0.3 
 * adapted for Linux.
 *
 * malloc by Hannu Savolainen 1993 and Matthias Urlichs 1994
 *
 * Modified for ARM Linux by Russell King
 *
 * Nicolas Pitre <[email protected]>  1999/04/14 :
 *  For this code to run directly from Flash, all constant variables must
 *  be marked with 'const' and all other variables initialized at run-time 
 *  only.  This way all non constant variables will end up in the bss segment,
 *  which should point to addresses in RAM and cleared to 0 on start.
 *  This allows for a much quicker boot time.
 */

unsigned int __machine_arch_type;

#include <linux/compiler.h>	/* for inline */
#include <linux/types.h>
#include <linux/linkage.h>

static void putstr(const char *ptr);
extern void error(char *x);

#include CONFIG_UNCOMPRESS_INCLUDE

#ifdef CONFIG_DEBUG_ICEDCC

#if defined(CONFIG_CPU_V6) || defined(CONFIG_CPU_V6K) || defined(CONFIG_CPU_V7)

static void icedcc_putc(int ch)
{
	int status, i = 0x4000000;

	do {
		if (--i < 0)
			return;

		asm volatile ("mrc p14, 0, %0, c0, c1, 0" : "=r" (status));
	} while (status & (1 << 29));

	asm("mcr p14, 0, %0, c0, c5, 0" : : "r" (ch));
}


#elif defined(CONFIG_CPU_XSCALE)

static void icedcc_putc(int ch)
{
	int status, i = 0x4000000;

	do {
		if (--i < 0)
			return;

		asm volatile ("mrc p14, 0, %0, c14, c0, 0" : "=r" (status));
	} while (status & (1 << 28));

	asm("mcr p14, 0, %0, c8, c0, 0" : : "r" (ch));
}

#else

static void icedcc_putc(int ch)
{
	int status, i = 0x4000000;

	do {
		if (--i < 0)
			return;

		asm volatile ("mrc p14, 0, %0, c0, c0, 0" : "=r" (status));
	} while (status & 2);

	asm("mcr p14, 0, %0, c1, c0, 0" : : "r" (ch));
}

#endif

#define putc(ch)	icedcc_putc(ch)
#endif

static void putstr(const char *ptr)
{
	char c;

	while ((c = *ptr++) != '\0') {
		if (c == '\n')
			putc('\r');
		putc(c);
	}

	flush();
}

/*
 * gzip declarations
 */
extern char input_data[];
extern char input_data_end[];

unsigned char *output_data;

unsigned long free_mem_ptr;
unsigned long free_mem_end_ptr;

#ifndef arch_error
#define arch_error(x)
#endif

void error(char *x)
{
	arch_error(x);

	putstr("\n\n");
	putstr(x);
	putstr("\n\n -- System halted");

	while(1);	/* Halt */
}

asmlinkage void __div0(void)
{
	error("Attempting division by 0!");
}

unsigned long __stack_chk_guard;

void __stack_chk_guard_setup(void)
{
	__stack_chk_guard = 0x000a0dff;
}

void __stack_chk_fail(void)
{
	error("stack-protector: Kernel stack is corrupted\n");
}

extern int do_decompress(u8 *input, int len, u8 *output, void (*error)(char *x));


void
decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,
		unsigned long free_mem_ptr_end_p,
		int arch_id)
{
	int ret;

	__stack_chk_guard_setup();

	output_data		= (unsigned char *)output_start;
	free_mem_ptr		= free_mem_ptr_p;
	free_mem_end_ptr	= free_mem_ptr_end_p;
	__machine_arch_type	= arch_id;

	arch_decomp_setup();

	putstr("Uncompressing Linux...");
	ret = do_decompress(input_data, input_data_end - input_data,
			    output_data, error);
	if (ret)
		error("decompressor returned an error");
	else
		putstr(" done, booting the kernel.\n");
}


        从上面的代码分析可知,在arch/arm/boot/compressed/misc.c文件中调用了putstr函数,该函数循环打印字符,直到字符串结束,如果是换行符,再补充打印一个回车符,从而实现回车换行的效果。具体的打印由 putc 函数来实现,该函数被定义在         arh/arm/platsamsung/include/plat/uncompress.h 文件中。putc 首先判断了底层调试宏开关是否打开,如果不是则直接返回,如果是则进一步检查是否使能了 FIFO。如果 FIFO 使能则一直等待,直到FIFO 可用,如果没有使能,则一直等待发送缓冲可用,最后将要发送的字符写入发送寄存器中。很明显,数据是否能够通过串口正常发送,需要依赖于在 U-Boot 中是否将串口正确初始化,这也是内核启动代码对 U-Boot 的一个求。不过 U-Boot 的代码通常都会初始化一个串口来打印信息,所以这个条件通常也是满足的。这里的 putstr 只在内核解压时使用,内核解压后调用不了该函数,而内核解压部分的代码几乎不会出错,所以驱动开发者很少使用该函数。


3.2 内核镜像解压后的串口输出函数


        在内核解压完成后,跳转到 vmlinux 镜像入口,这时还没有初始化控制台设备,但是执行系统初始化的过程中也可能出现严重的错误,导致系统崩溃。怎样才能报告这种错误信息呢?可以通过 printascii 子程序来向串口打印。
        printascii,printhex8 等子程序包含在 arch/arm/kernel/debug.S 文件中。如果要编译链
接这些子程序,需要内核使能“Kernel low-level debugging functions”选项。

        printascii 子程序实现向串口打印字符串的功能,printhex 也用了 printascii 子程序来显示数字。在 printascii 子程序中,调用了宏(macro): addruart、waituart、senduart、busyuart,这些宏都是在arch/arm/include/debug/exynos.S 和arch/arm/include/debug samsung.S /中定义的。printascii 函数的代码如下。

/* 
*  linux/arch/arm/kernel/debug.S
 *
 *  Copyright (C) 1994-1999 Russell King
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 *  32-bit debugging code
 */
#include <linux/linkage.h>
#include <asm/assembler.h>

		.text

/*
 * Some debugging routines (useful if you've got MM problems and
 * printk isn't working).  For DEBUGGING ONLY!!!  Do not leave
 * references to these in a production kernel!
 */

#if !defined(CONFIG_DEBUG_SEMIHOSTING)
#include CONFIG_DEBUG_LL_INCLUDE
#endif

#ifdef CONFIG_MMU
		.macro	addruart_current, rx, tmp1, tmp2
		addruart	\tmp1, \tmp2, \rx
		mrc		p15, 0, \rx, c1, c0
		tst		\rx, #1
		moveq		\rx, \tmp1
		movne		\rx, \tmp2
		.endm

#else /* !CONFIG_MMU */
		.macro	addruart_current, rx, tmp1, tmp2
		addruart	\rx, \tmp1
		.endm

#endif /* CONFIG_MMU */

/*
 * Useful debugging routines
 */
ENTRY(printhex8)
		mov	r1, #8
		b	printhex
ENDPROC(printhex8)

ENTRY(printhex4)
		mov	r1, #4
		b	printhex
ENDPROC(printhex4)

ENTRY(printhex2)
		mov	r1, #2
printhex:	adr	r2, hexbuf
		add	r3, r2, r1
		mov	r1, #0
		strb	r1, [r3]
1:		and	r1, r0, #15
		mov	r0, r0, lsr #4
		cmp	r1, #10
		addlt	r1, r1, #'0'
		addge	r1, r1, #'a' - 10
		strb	r1, [r3, #-1]!
		teq	r3, r2
		bne	1b
		mov	r0, r2
		b	printascii
ENDPROC(printhex2)

hexbuf:		.space 16

		.ltorg

#ifndef CONFIG_DEBUG_SEMIHOSTING

ENTRY(printascii)
		addruart_current r3, r1, r2
		b	2f
1:		waituart r2, r3
		senduart r1, r3
		busyuart r2, r3
		teq	r1, #'\n'
		moveq	r1, #'\r'
		beq	1b
2:		teq	r0, #0
		ldrneb	r1, [r0], #1
		teqne	r1, #0
		bne	1b
		mov	pc, lr
ENDPROC(printascii)

ENTRY(printch)
		addruart_current r3, r1, r2
		mov	r1, r0
		mov	r0, #0
		b	1b
ENDPROC(printch)

#ifdef CONFIG_MMU
ENTRY(debug_ll_addr)
		addruart r2, r3, ip
		str	r2, [r0]
		str	r3, [r1]
		mov	pc, lr
ENDPROC(debug_ll_addr)
#endif

#else

ENTRY(printascii)
		mov	r1, r0
		mov	r0, #0x04		@ SYS_WRITE0
	ARM(	svc	#0x123456	)
	THUMB(	svc	#0xab		)
		mov	pc, lr
ENDPROC(printascii)

ENTRY(printch)
		adr	r1, hexbuf
		strb	r0, [r1]
		mov	r0, #0x03		@ SYS_WRITEC
	ARM(	svc	#0x123456	)
	THUMB(	svc	#0xab		)
		mov	pc, lr
ENDPROC(printch)

ENTRY(debug_ll_addr)
		mov	r2, #0
		str	r2, [r0]
		str	r2, [r1]
		mov	pc, lr
ENDPROC(debug_ll_addr)

#endif


        首先调用了 addruart_current 获得调试串口的物理地址和虚拟地址,调用返回后 r3 保存的是物理地址、r1 是虚拟地址、r2 是一个临时寄存器。然后跳转到局部标号 2 去执行代码,r0 是指向要打印的字符串的指针,判断不为空指针后,取出一个字符,并判断是否是字符串的结尾,如果不是则跳转到局部标号 1 执行代码。从局部标号 1 开始,首先等待串口可用,然后发送字符,接下来等待发送完成,最后判断要发送的字符是否是换行字符,如果是则补一个回车字符。在函数中调用的宏都比较简单,这里就不再详细分析了。
        printascii 函数的使用也非常简单,首先声明该函数,然后传入要打印的字符串指针即可,实例代码如下。
 

extern void printascii(char *);
asmlinkage void __init start_kernel(void)
{
    char * command_line;
    extern const struct kernel_param __start__param[], __stop__param[];
    printascii("enter start_kernel\n");
......


3.3 内核打印函数


        Linux 内核标准的系统打印函数是 printk。printk 函数具有极好的健壮性,不受内核运行条件的限制,在系统运行期间都可以使用。printk 日志级别如下所示。

日志级别一共有8个级别,printk的日志级别定义如下(在include/linux/kernel.h中):

​
#define KERN_EMERG 0/*紧急事件消息,系统崩溃之前提示,表示系统不可用*/

#define KERN_ALERT 1/*报告消息,表示必须立即采取措施*/

#define KERN_CRIT 2/*临界条件,通常涉及严重的硬件或软件操作失败*/

#define KERN_ERR 3/*错误条件,驱动程序常用KERN_ERR来报告硬件的错误*/

#define KERN_WARNING 4/*警告条件,对可能出现问题的情况进行警告*/

#define KERN_NOTICE 5/*正常但又重要的条件,用于提醒*/

#define KERN_INFO 6/*提示信息,如驱动程序启动时,打印硬件信息*/

#define KERN_DEBUG 7/*调试级别的消息*/

​

        这些级别有助于内核控制信息的紧急程度,判断是否向串口输出等。正如 printk 函数的日志级别,printk 函数的实现也比较复杂。printk 函数不是直接向控制台设备或串口
打印信息,而是把打印信息先写到缓冲区里面。下面分析一下 printk 函数的代码实现。
 

/* kernel/printk/printk.c */
/* 不指定级别的 printk 函数用这个默认级别·.....*/
#define DEFAULT_MESSAGE_LOGLEVEL  CONFIG_DEFAULT_MESSAGE_IOGLEVEL /* KERN_WARNING级别*/
......
#define MINIMUM_CONSOLE_LOGLEVEL 1 /* 控制台可以使用的最小级别数 */
#define DEFAULT_CONSOLE_LOGLEVEL 7 /* 任何比 KERN_DEBUG 更严重级别的信息都显示 */
int console printk[4] = [ /* 定义控制台的默认打印级别 */
    DEFAULT_CONSOLE_LOGLEVEL,    /* console loglevel */
    DEFAULT_MESSAGE_LOGLEVEL,    /* default message_loglevel */
    MINIMUM_CONSOLE_LOGLEVEL,    /* minimum console loglevel */
    DEFAULT_CONSOLE_LOGLEVEL,    /* default_console_loglevel */
};
......
/* 这是 printk 函数的实现,它可以在任何上下文中调用。
*对控制台操作之前,先尝试获得 console_lock 锁,
*如果成功,那么将会把输出记录下来并调用控制台驱动程序;
*如果失败,把输出信息写到日志缓冲区中,并立即返回。
*console sem 信号量的拥有者在 console_unlock 函数中
*将会发现有一个新的输出,然后会在释放这个锁之前将输出信息
*通过控制台打印
asmlinkage int printk(const char *fmt, ...)
{
    va_list args;
    int r;
#ifdef CONFIG_KGDB_KDB
    if (unlikely(kdb_trap_printk)) {
        va_start(args,fmt);
        r = vkdb_printf(fmt, args);
        va_end(args);
        return r;
    }
#endif

    va_start(args,fmt);    /*使用变参*/
    r = vprintk_emit(0, -1, NULL, 0, fmt, args); /* vprintk emit 函数完成打印任务*/
    va_end(args);
    return r;
}
EXPORT_SYMBOL(printk);

asmlinkage int vprintk_emit(int facility, int level, 
            const char *dict,size_t dictlen,
            const char *fmt,va_list args)
{
    static int recursion_bug;
    static char textbuf[LOG_LINE_MAX];
    char *text = textbuf;
    size_t text_len;
    enum log_flags lflags = 0;
    unsigned long flags;
    int this_cpu;
    int printed_len = 0;

    boot_delay_msec(level);
    /*取决于CONFIG_BOOT_PRINTK_DELAY宏是否被定义,用于控制内核启动阶段的打印延时*/
    
    printk_delay();/*打印延时控制 */
    local_irq_save(flags);/*关闭本地CPU的中断并保存中断使能标志*/
    this_cpu = smp_processor_id();/*获取当前CPU的ID号*/

    /*如果发生了递归调用*/
    if (unlikely(logbuf_cpu == this_cpu)) {
        /*如果在这个CPU上调用printk时内核崩溃,那么将尝试获得崩溃信息,
         *但要确保不会发生死锁。否则立即返回,以避免递归,并将 recursion_bug
         *标志置位,以便在以后某个适当的时刻可以打印该信息
        */
        if (!oops_in_progress && !lockdep_recursing(current)) {
            recursion_bug = 1;
            goto out_restore_irqs;
        }
        /* 强制初始化自旋锁和信号量,但要留足够的时间给慢速的控制台
         *以便打印出完整的oops 信息
        */
        zap_locks();
    }
    lockdep_off();                  /*递归深度加一*/
    raw_spin_lock(&logbuf_lock);    /*日志缓冲区上锁*/
    logbuf_cpu = this_cpu;          /*保存日志CPU的ID号*/


    if (recursion_bug) {/* 如果出现了递归的 bug,打印该信息 */
        static const char recursion_msg[] = 
            "BUG: recent printk recursion!";

        recursion_bug = 0;
        printed_len += strlen(recursion_msg);
        /*将信息记录到日志缓冲区 */
        log_store(0,2,LOG_PREFIX | LOG_NEWLINE,0,NULL,0,recursion_msg,                             
        printed_len);
        }

    /*将信息格式化输出到text 指向的缓冲区中*/
    text_len = vscnprintf(text,sizeof(textbuf),fmt, args);

    /*如果有换行符则置位LOG_NEWLINE*/
    if (text_len && text[text_len-1] == '\n') {
        text_len--;
        lflags |= LOG_NEWLINE;
    }

    /* 如果打印来自内核,那么裁剪一些前缀并提取打印级别和控制信息 */
    if (facility == 0) (
        int kern_level = printk_get_level(text);

        if (kern_level) {
            const char *end_of_header = printk_skip_level(text);
            switch (kern_level) {
            case'0' ...'7':
                if (level == -1)
                    level = kern_level -'0';
           
            case 'd': /* KERN_DEFAULT */
                lflags |= LOG_PREFIX;
            case'c':/* KERN CONT */
                break;
        }
        text_len -= end_of_header - text;
        text = (char *)end_of_header;
        }
   } 
    /* 如果未指定打印级别,则使用默认的打印级别*/
    if (level == -1)
        level = default_message_loglevel;

    /* 如果输出信息带有键值对组成的字典,则设置相应的标志 */
    if (dict)
         lflags |= LOG_PREFIXILOG_NEWLINE;

    if (!(lflags & LOG_NEWLINE)) {

    /*一个早期的新行丢失或者另一个任务要继续打印,刷新冲突的缓存*/
    if (cont.len &&(lflags & LOG_PREFIXI || cont.owner != current))
        cont_flush(LOG_NEWLINE);

    /*如果可能则缓存该行,否则立即保存下来*/
    if (!cont_add(facility, level,text,text_len))
        log_store(facility; level,lflags | LOG CONT,0, dict,dictlen,text,text_len);
    }else{
        bool stored= false;
    /*如果一个早期的新行正被丢失并且来自于同一任务,那么它将会和现在的缓存内容
    *合并并刷新输出。但如果存在一个和中断的竞态,那么将会单存该行并刷新输出。
    *如果先前的print来自于不同的任务并且丢掉了新行,那么刷新并追加新行
    */
    if (cont.len) {
        if (cont.owner == current && !(lflags & LOG_PREFIX))
            stored = cont_add(facility, level, text, text_len);
            cont_flush(LOG NEWLINE);
    }
    if (!stored)
        log_store(facility, level, lflags,0, dict,dictlen,text, text len);
    }
    printed_len += text_len;
    /*尝试获得并立即释放控制台信号量。这将会引起缓存的打印输出并唤醒
     */dev/kmsg和syslog()的用户
     *console_trylock_forprintk()函数将会释放logbuf lock锁,而不管其是否
     *获得了控制台信号量
     */
    if (console_trylock_for_printk(this_cpu))
        console_unlock();
    lockdep on();     /*递归深度减一*/
out_restore_irqs:
    local_irq_restore(flags);/*恢复本地CPU的中断使能标志*/

    return printed_len;
}
EXPORT_SYMBOL(vprintk_emit);


        由以上的代码可知,在控制台初始化之前,printk 的输出只能先保存在日志缓存中所以在控制台初始化之前系统崩溃的话,将不会在控制台上看到 printk 的打印输出。
        printk 的使用方法同 printf,但可以添加打印级别,示例代码如下。

printk("%s\n", "default level")
printk(KERN_DEBUG "%s\n","debug-level messages");
printk(KERN_INFO "%s\n","informational");
printk(KERN_NOTICE "%s\n","normal but significant condition");
printk(KERN_WARNING "s\n", "warning conditions");
printk(KERN_ERR "%s\n", "error conditions");
printk(KERN_CRIT "s\n", "critical conditions");
printk(KERN_ALERT "s\n", "action must be taken immediately");
printk(KERN EMERG "sn","system is unusable");



        如果 printk 中没有加调试级别,则使用默认的调试级别。注意,调试级别和格式化字符串之间没有逗号。当前控制台的各打印级别可以通过下面的命令来查看。

cat /proc/sys/kernel/printk

4        4        1        7


        上面的信息表示控制台当前的打印级别为 4 (KERN_WARNING),凡是打印级别小于等于(数值上大于等于) 该打印级别的信息都不会在控制台上显示; printk 的默认打印级别是 4,即 printk 中如果不指定打印级别,则使用4 的打印级别;控制台能够设置的最高打印级别为1(KERN_ALERT),默认的控制台级别为7。使用下面的命令可以修改控制台打印级别。

# echo"7 4 1 7" > /proc/sys/kernel/printk


        如果要查看完整的控制台打印信息,可以使用下面的命令。# dmesg
        如果要实时查看控制台打印信息,可以使用下面的命令。# cat /proc/kmsg
        printk 只能在控制台初始化完成以后看到输出,这对调试来说极为不方便。为了能在早期看到 printk 的打印输出,可以首先使能“Early printk”选项,然后在 bootargs 中添加earlyprintk 参数
 

四、获取内核信息

        Linux 内核提供了一些与用户空间通信的机制,大部分驱动程序与用户空间的接口都可以作为获取内核信息的手段。而且,内核也有专门的调试机制。

4.1系统请求键

        系统请求键可以使 Linux 内核回溯跟踪进程,当然这要在 Linux 的键盘仍然可用的前提下,并且 Linux 内核已经支持 MAGIC_SYSRQ 功能模块。

        大多数系统平台(特别是 X86)都已经实现了系统请求键功能,它是在 drivers/charsysrq.c 中实现的。在配置内核的时候需要选择“Magic SysRq key”菜单选项,使能配选项CONFIG_MAGIC_SYSRQ。
        使用这项功能,必须是在文本模式的控制台上,并且启动 CONFIG_MAGIC_SYSRO.

        SysRq(系统请求)键是复合键[Alt+SysRq],大多数键盘的 SysRq 和 PrtSc 键是复用的。
        按住 SysRq 复合键,再输入第三个命令键,可以执行相应的系统调试命令。例如,输入t键,可以得到当前运行的进程和所有进程的堆栈跟踪。回溯跟踪将被写到/var/log/messages 文件中。如果内核都配置好了,系统应该已经转换了内核的符号地址。
        但是,在串口控制台上不能使用 SsRq 复合键,可以先发送一个“BREAK”,在 5s之内输入系统请求命令键。
        另外,有些硬件平台也不能使用 SysRg 复合键。不过,各种目标板都可以通过/proc接口进入系统请求状态。

S echo t > /proc/sysrq-trigger


表 13.2 列出了系统请求键的命令解释。更多信息可以查阅内核文档 Documentationsysrq.txt。

键命令 说明
SysRq-b 重启机器
SysRq-e 给init之外的所有进程发送SIGTERM信号
SysRq-i 给init之外的所有进程发送SIGKILL信号
SysRq-k 安全访问键:杀掉这个控制台上的所有进程
SysRq-l 给包括init在内的所有进程发送SIGKILL信号
SysRq-m 在控制台上显示内存信息
SysRq-o 关闭机器
SysRq-p 在控制台上显示寄存器
SysRq-r 关闭键盘的原始模式
SysRq-s 同步所有挂接的磁盘
SysRq-t 在控制台上显示所有的任务信息
SysRq-u 卸载所有已经挂载的磁盘


        神奇的系统请求键是辅助调试或者拯救系统的重要方法,它为控制台上的每个用户提供了强大的功能。在系统宕机或者运行状态不正常的时候,通过系统请求键可以查询当前进程执行的状态,从而判断出错的进程和函数。

4.2 通过/proc 接口

        proc 文件系统是一种伪文件系统。实际上,它并不占用存储空间,而是系统运行时在内存中建立的内核状态映射,可以瞬时地提供系统的状态信息。
        在用户空间,可以作为文件系统挂接到/proc 目录下,提供给用户访问;可以通过 Shell命令挂接;可以在/etc/fstab 中做出相应的设置。

s mout -t proc proc /proc



        通过 proc 文件系统可以查看运行中的内核、查询和控制运行中的进程和系统资源等状态。这对于监控性能、查找系统信息、了解系统是如何配置的以及更改该配置很有用。

        在用户空间,可以直接访问/proc 目录下的条目、读取信息或者写入命令。但是不能使用编辑器打开修改/proc 条目,因为在编辑过程中,同步保存的数据将是不完整的命令

        在命令行下使用 echo 命令,从命令行将输出重定向至/proc 下指定条目中。例如,关闭系统请求键功能的命令:

S echo 0 > /proc/sys/kernel/sysrq


        在命令行下查看/proc 目录下的条目信息,应该使用命令行下的 cat 命令。例如:

$ cat /proc/cpuinfo


        另外,/proc 接口的条目可以作为普通的文件打开访问。这些文件也有访问的权限大部分条目是只读的,少数用于系统控制的条目具有写操作属性。在应用程序中,可以通过 open()、read()、write0等函数操作。
        /proc 中的每个条目都有一组分配给它的非常特殊的文件访问权限,并且每个文件属于特定的用户标识。这一点实现得非常仔细,从而提供给管理员和用户正确的功能。这些特定的访问权限如下
(1) 只读权限:任何用户都不能对该文件进行写操作,用于获取系统信息。
(2) root 写权限:如果/proc 中的某个文件是可写的,则通常只能由 root 用户来写

(3) root 读权限:有些文件对一般系统用户是不可见的,只对 root 用户是可见的。
(4)其他权限:可能有不同于以上常见的三种访问权限的组合。

        就具体/proc 条目的功能而言,每一个条目的读写操作在内核中都有特定的实现。当查看/proc 目录下的文件时,会发现有些文件是可读的,可以从中读出内核的特定信息:
有些文件是可写的,可以写入特定的配置和控制命令。
        Linux 的一些系统工具就是通过/proc 接口读取信息的。例如,top 命令就是读取/proc接口下相关条目的信息,实时地显示当前运行中的进程和系统负载。要获得/proc 文件的所有信息,一个最佳来源就是 Linux 内核源码本身,它包含了一些非常优秀的文档。

4.3 通过/sys 接口

        Sysfs 文件系统是 Linux 2.6 内核新增加的文件系统。它也是一种伪文件系统,是在内存中实现的文件系统。它可以把内核空间的数据、属性、链接等输出到用户空间。

        在 Linux 2.6 内核中,sysfs 和 kobject 是紧密结合的,成为动程序型的组成部分。

        当加载或者卸载 kobject 的时候,需要注册或者注销操作。当注册 kobject 时,注册勇数除了把 kobiect 插入到 kset 链表中,还在 syss 中创建对应的目录。反过来,当注销kobject 时,注销函数也会删除 sysfs 中相应的目录。

        通常,sysfs 文件系统要挂接到/sys 目录下,给用户提供访问空间。可以通过 Shell命令挂接,也可以在/etc/fstab 中做出相应的设置。
S mount -t sysfs sysfs /sys

sysfs 文件系统的目录组织结构反映了内核数据结构的关系。/sys 的目录结构下应包含以下子目录。

block/ bus/ class/ devices/ firmware/ net/

        devices/目录下的目录树代表设备树,直接映射了内核内部的设备(按照 device结构的层次关系)。
        bus/目录包含内核各种总线类型的目录。每一种总线目录包含两个子目录: devices/和 drives/。
        devices/目录包含了系统探测到的每一个设备的符号链接,指向 sysfs 文件系统的root/目录下的设备。
        drivers/目录包含了在特定总线结构上为每一个加载的设备驱动创建的子目录

        class/目录包含设备接口类型的目录,在类型子目录下还有设备接口的子目录


为了方便使用 sysfs,下面介绍一些 sysfs 的编程接口。


4.3.1.属性

        属性能够以文件系统的正常文件形式输出到用户空间。sysfs 文件系统间接调用属性定义的函数操作,提供读写内核属性的方法。
        属性应该是 ASCII 文本文件,每个文件只能有一个值。可能这样效率不高,可以通过相同类型的数组来表示。
        不赞成使用混合类型、多行数据格式和奇异的数据格式。这样做可能使代码得不到认可。

简单的属性定义示例如下:

struct attribute{
    char *name;
    umode_t mode;
};
int sysfs_create_file(struct kobject * kobj, struct attribute * attr);
void sysfs_remove_file(struct kobject *kobj, struct attribute * attr);


        定义空洞的属性是没有用的,所以好针对转定的目标类型添加自己的结构属性或者封装好的函数。

例如,设备驱动程序可以定义下面的结构 device attribute。
 

struct device_attribute{
    struct attribute    attr;
    ssize_t (*show) struct device *dev, struct device_attribute *attr,char *buf);
    ssize_t (*store)(struct device *dev, struct device_attribute *attr,const char *buf, size_t count);
extern int device_create_file(struct device *device,const struct device_attribute *entry);
extern void device_remove_file(struct device *dev,const struct device_attribute *attr);


使用下面的宏可以简化 device attribute 结构对象的定义和初始化。

#define __ATTR( _name, _mode, _show, _store) {
    .attr = {.name =  __stringify(_name), .mode =  _mode ),
    .show = show,
    .store =  store,
    }
    #define DEVICE_ATTR(_name,_mode,_show, _store)\
        struct device_attribute dev_attr_##_name = __ATTR(_name, _mode,_show,_store)


举例说明如何使用上面的宏来定义属性。

        static DEVICE_ATTR(foo, S_IWUSR | S_IRUGO, show_foo, store_foo);


等价于:
 

static struct device_attribute dev_attr_foo = {
    .attr = {
        .name = "foo",
        .mode = S_IWUSR | S_IRUGO,
    },
    .show = show_foo,
    .store = store_foo,
};

4.3.2子系统操作函数

        当子系统定义了一个属性类型时,必须实现一些 sysfs 操作函数。当应用程序调用read/write 函数时,通过这些子系统函数显示或保存属性值。

struct sysfs_ops {
    ssize_t (*show) (struct kobject *, struct attribute *, char *);
    ssize_t (*store) (struct kobject *, struct attribute *, const char *, size_t);
};

        当读或写这个 sys 文件时,sys 调用对应的函数。然后,把通用的 kobjeet结构和结构属性指针转换成适当的指针类型,并且调用相关的函数。举例说明:

 

#define to_dev_attr(_attr) container_of(_attr, struct device_attribute, attr)

static ssize_t dev_attr_show(struct kobject *kobj, struct attribute *attr,char *buf)
{
    struct device_attribute *dev_attr = to_dev_attr(attr);
    struct device *dev = kobj_to_dev(kobj);
    ssize_t ret = -EIO;

    if (dev_attr->show)
        ret = dev_attr->show(dev, dev attr, buf);
    if (ret >= (ssize_t)PAGE_SIZE) {
        print_symbol("dev_attr_show: %s returned bad count\n",
            (unsigned long)dev_attr->show);
    }
    return ret;
}

要读写属性,还要声明和实现 show() 和 store()函数。这两个函数的声明如下:

ssize_t (*show)(struct device *dev, struct device_attribute *attr,char *buf);
ssize_t (*store)(struct device *dev, struct device_attribute *attr, const char *buf,size_t count);


读写函数的操作主要是数据缓冲区的读写操作,下面是一个最简单的设备属性实现的例子。
 

static ssize_t show_name(struct device *dev, struct device_attribute *attr, chaR *buf)
{
    return snprintf(buf, PAGE_SIZE, "%s\n", dev->name);
}
static ssize_t store_name(struct device * dev, const char * buf)
{
    sscanf(buf,"%20s",dev->name);
    return strnlen(buf,PAGE_SIZE);
}
static DEVICE_ATTR(name, S_IRUGO, show_name, store_name);

五、处理出错信息

        当系统出现错误时,内核有两个基本的错误处理机制: oops 和 panic。

5.1 oops 信息

        尽管有了各种调试方法,系统或驱动程序的一些 BUG 仍可能直接导致系统出错,打印出 oops 信息。通常 oops 发生以后,系统处于不稳定状态,可能崩溃,也可能继续运行


5.1.1.oops 消息包含系统错误的详细信息

        通常 oops 信息中包含当前进程 (Task) 的回溯(Call Trace) 和 CPU 寄存器的内容。分析在发生崩溃时发送到系统控制台的 oops 消息,这是 Linux 调试系统崩溃的传统方法。oops 信息是机器指令级的,是很难懂的。ksymoops 工具可以将机器指令转换为代码并将堆栈值映射到内核符号。在很多情况下,这些信息就足够确定错误的可能原因。

        分析 oops 信息是一项很艰苦的工作,先来看看下面这些信息吧。

Oops: machine check, sig:7NIP: CO00F290 XER: 20000000 LR: CO00FOFO SP: CO13E940 REGS: C013f890 TRAP: 0200MSR: 00009030 EE: 1 PR: O FP:O ME: 1 IR/DR: 11TASK = c013e020[0] 'swapper' Last syscall: 120last math 00000000 last altivec 00000000GPR00: 00000000 C013E940 C013E020 000001F5 C500F200 C3A89000 00000002 C023BEA8GPR08: 00000007 00000570 0000017 0000015C 84002022 1002B4DC 00000000 00000000GPR16: 00000000 00000000 00000000 00000000 00001032 0013EA90 00000000 C00047CCGPR24: C0150000 000003C0 C07368C0 C013E9C8 000005EE C3A89000 C0160000 C0160000Call backtrace:C00334C8 C0160000 C000EE4C COOACE60 C00A9584 CO0AD258 COOAD008C00A879C C00057A4 C0005860 C00047CC 00000020 C00C1404 C00C146CC00A8C08 COOCE3C8 C00C59A4 C0ODA4A4 C0OD9068 C00DA608 C00D9340CO0E9224 C00E7A54 COOEFDE4 C00E032C COOD62CC COOD6504 C00C6060C00C6214 C00C6384 C001B820 C00058C8 C00047CCKernel panic: Aiee, killing interrupt handler!Warning (Oops read): Code line not seen, dumping what data is available


        其中打印出了处理器寄存器的值,还有进程和栈回溯信息。对照 System.map 完全可以进行分析。


5.1.2.使用 ksymoops 转换 oops 信息

        ksymoops 工具可以翻译 oops 信息,从而分析发生错误的指令,并显示一个跟踪部分表明代码如何被调用。它是根据内核镜像的 System.map 来转换的,因此,必须提供正在运行的内核镜像的 System.map 文件。
        关于如何使用 ksymoops,内核源码 Documentation/oops-tracingtxt 中或 ksymoops 手册页上有完整的说明可以参考。
        将 oops 消息复制保存在一个文件中,通过 ksymoops 工具转换它。
S ksymoops -m System.map < oops.txt
        这样 oops 信息就转换成符号信息打印到控制台上了。如果想把结果保存下来,可以把结果重定向到文件中。


5.1.3,内核 kallsyms 选项支持符号信息

        Linux 2.6 内核引入了 kallsyms 特性,可以通过定义 CONFIG_KALLSYMS 配置选启动。该选项可以载入内核镜像对应内存地址的符号的名称,内核可以直接跟踪回溯函数名称,而不再打印难懂的机器码了。这样,就不再需 System.map 和 ksymoops 工了。因为符号表要编译到内核镜像中,所以内核镜像会变大,并且符号表永久驻留在内存中,对于开发来说,这也是值得的。


5.2 panic


当系统发生严重错误的时候,将调用 panic 函数。
那么 panic函数执行了哪些操作呢? 不妨分析一下 panic函数的实现。
 

/*
 *  linux/kernel/panic.c
 *
 *  Copyright (C) 1991, 1992  Linus Torvalds
 */

/*
 * This function is used through-out the kernel (including mm and fs)
 * to indicate a major problem.
 */
#include <linux/debug_locks.h>
#include <linux/interrupt.h>
#include <linux/kmsg_dump.h>
#include <linux/kallsyms.h>
#include <linux/notifier.h>
#include <linux/module.h>
#include <linux/random.h>
#include <linux/ftrace.h>
#include <linux/reboot.h>
#include <linux/delay.h>
#include <linux/kexec.h>
#include <linux/sched.h>
#include <linux/sysrq.h>
#include <linux/init.h>
#include <linux/nmi.h>

#define PANIC_TIMER_STEP 100
#define PANIC_BLINK_SPD 18

int panic_on_oops = CONFIG_PANIC_ON_OOPS_VALUE;
static unsigned long tainted_mask;
static int pause_on_oops;
static int pause_on_oops_flag;
static DEFINE_SPINLOCK(pause_on_oops_lock);

int panic_timeout = CONFIG_PANIC_TIMEOUT;
EXPORT_SYMBOL_GPL(panic_timeout);

ATOMIC_NOTIFIER_HEAD(panic_notifier_list);

EXPORT_SYMBOL(panic_notifier_list);

static long no_blink(int state)
{
	return 0;
}

/* Returns how long it waited in ms */
long (*panic_blink)(int state);
EXPORT_SYMBOL(panic_blink);

/*
 * Stop ourself in panic -- architecture code may override this
 */
void __weak panic_smp_self_stop(void)
{
	while (1)
		cpu_relax();
}

/**
 *	panic - halt the system
 *	@fmt: The text string to print
 *
 *	Display a message, then perform cleanups.
 *
 *	This function never returns.
 */
/** panic - 停止系统运行
*参数 fmt: 要打印的字符串
*显示信息,然后清理现场,不再返回
*/
void panic(const char *fmt, ...)
{
	static DEFINE_SPINLOCK(panic_lock);
	static char buf[1024];
	va_list args;
	long i, i_next = 0;
	int state = 0;

	/*
	 * Disable local interrupts. This will prevent panic_smp_self_stop
	 * from deadlocking the first cpu that invokes the panic, since
	 * there is nothing to prevent an interrupt handler (that runs
	 * after the panic_lock is acquired) from invoking panic again.
	 */
	local_irq_disable();

	/*
	 * It's possible to come here directly from a panic-assertion and
	 * not have preempt disabled. Some functions called from here want
	 * preempt to be disabled. No point enabling it later though...
	 *
	 * Only one CPU is allowed to execute the panic code from here. For
	 * multiple parallel invocations of panic, all other CPUs either
	 * stop themself or will wait until they are stopped by the 1st CPU
	 * with smp_send_stop().
	 */
	if (!spin_trylock(&panic_lock))
		panic_smp_self_stop();

	console_verbose();
	bust_spinlocks(1);
	va_start(args, fmt);
	vsnprintf(buf, sizeof(buf), fmt, args);
	va_end(args);
	printk(KERN_EMERG "Kernel panic - not syncing: %s\n",buf);
#ifdef CONFIG_DEBUG_BUGVERBOSE
	/*
	 * Avoid nested stack-dumping if a panic occurs during oops processing
	 */
	if (!test_taint(TAINT_DIE) && oops_in_progress <= 1)
		dump_stack();
#endif

	/*
	 * If we have crashed and we have a crash kernel loaded let it handle
	 * everything else.
	 * Do we want to call this before we try to display a message?
	 */
	crash_kexec(NULL);

	/*
	 * Note smp_send_stop is the usual smp shutdown function, which
	 * unfortunately means it may not be hardened to work in a panic
	 * situation.
	 */
	smp_send_stop();

	/*
	 * Run any panic handlers, including those that might need to
	 * add information to the kmsg dump output.
	 */
	atomic_notifier_call_chain(&panic_notifier_list, 0, buf);

	kmsg_dump(KMSG_DUMP_PANIC);

	bust_spinlocks(0);

	if (!panic_blink)
		panic_blink = no_blink;

	if (panic_timeout > 0) {
		/*
		 * Delay timeout seconds before rebooting the machine.
		 * We can't use the "normal" timers since we just panicked.
		 */
		printk(KERN_EMERG "Rebooting in %d seconds..", panic_timeout);

		for (i = 0; i < panic_timeout * 1000; i += PANIC_TIMER_STEP) {
			touch_nmi_watchdog();
			if (i >= i_next) {
				i += panic_blink(state ^= 1);
				i_next = i + 3600 / PANIC_BLINK_SPD;
			}
			mdelay(PANIC_TIMER_STEP);
		}
	}
	if (panic_timeout != 0) {
		/*
		 * This will not be a clean reboot, with everything
		 * shutting down.  But if there is a chance of
		 * rebooting the system it will be rebooted.
		 */
		emergency_restart();
	}
#ifdef __sparc__
	{
		extern int stop_a_enabled;
		/* Make sure the user can actually press Stop-A (L1-A) */
		stop_a_enabled = 1;
		printk(KERN_EMERG "Press Stop-A (L1-A) to return to the boot prom\n");
	}
#endif
#if defined(CONFIG_S390)
	{
		unsigned long caller;

		caller = (unsigned long)__builtin_return_address(0);
		disabled_wait(caller);
	}
#endif
	local_irq_enable();
	for (i = 0; ; i += PANIC_TIMER_STEP) {
		touch_softlockup_watchdog();
		if (i >= i_next) {
			i += panic_blink(state ^= 1);
			i_next = i + 3600 / PANIC_BLINK_SPD;
		}
		mdelay(PANIC_TIMER_STEP);
	}
}

EXPORT_SYMBOL(panic);


struct tnt {
	u8	bit;
	char	true;
	char	false;
};

static const struct tnt tnts[] = {
	{ TAINT_PROPRIETARY_MODULE,	'P', 'G' },
	{ TAINT_FORCED_MODULE,		'F', ' ' },
	{ TAINT_UNSAFE_SMP,		'S', ' ' },
	{ TAINT_FORCED_RMMOD,		'R', ' ' },
	{ TAINT_MACHINE_CHECK,		'M', ' ' },
	{ TAINT_BAD_PAGE,		'B', ' ' },
	{ TAINT_USER,			'U', ' ' },
	{ TAINT_DIE,			'D', ' ' },
	{ TAINT_OVERRIDDEN_ACPI_TABLE,	'A', ' ' },
	{ TAINT_WARN,			'W', ' ' },
	{ TAINT_CRAP,			'C', ' ' },
	{ TAINT_FIRMWARE_WORKAROUND,	'I', ' ' },
	{ TAINT_OOT_MODULE,		'O', ' ' },
};

/**
 *	print_tainted - return a string to represent the kernel taint state.
 *
 *  'P' - Proprietary module has been loaded.
 *  'F' - Module has been forcibly loaded.
 *  'S' - SMP with CPUs not designed for SMP.
 *  'R' - User forced a module unload.
 *  'M' - System experienced a machine check exception.
 *  'B' - System has hit bad_page.
 *  'U' - Userspace-defined naughtiness.
 *  'D' - Kernel has oopsed before
 *  'A' - ACPI table overridden.
 *  'W' - Taint on warning.
 *  'C' - modules from drivers/staging are loaded.
 *  'I' - Working around severe firmware bug.
 *  'O' - Out-of-tree module has been loaded.
 *
 *	The string is overwritten by the next call to print_tainted().
 */
const char *print_tainted(void)
{
	static char buf[ARRAY_SIZE(tnts) + sizeof("Tainted: ")];

	if (tainted_mask) {
		char *s;
		int i;

		s = buf + sprintf(buf, "Tainted: ");
		for (i = 0; i < ARRAY_SIZE(tnts); i++) {
			const struct tnt *t = &tnts[i];
			*s++ = test_bit(t->bit, &tainted_mask) ?
					t->true : t->false;
		}
		*s = 0;
	} else
		snprintf(buf, sizeof(buf), "Not tainted");

	return buf;
}

int test_taint(unsigned flag)
{
	return test_bit(flag, &tainted_mask);
}
EXPORT_SYMBOL(test_taint);

unsigned long get_taint(void)
{
	return tainted_mask;
}

/**
 * add_taint: add a taint flag if not already set.
 * @flag: one of the TAINT_* constants.
 * @lockdep_ok: whether lock debugging is still OK.
 *
 * If something bad has gone wrong, you'll want @lockdebug_ok = false, but for
 * some notewortht-but-not-corrupting cases, it can be set to true.
 */
void add_taint(unsigned flag, enum lockdep_ok lockdep_ok)
{
	if (lockdep_ok == LOCKDEP_NOW_UNRELIABLE && __debug_locks_off())
		printk(KERN_WARNING
		       "Disabling lock debugging due to kernel taint\n");

	set_bit(flag, &tainted_mask);
}
EXPORT_SYMBOL(add_taint);

static void spin_msec(int msecs)
{
	int i;

	for (i = 0; i < msecs; i++) {
		touch_nmi_watchdog();
		mdelay(1);
	}
}

/*
 * It just happens that oops_enter() and oops_exit() are identically
 * implemented...
 */
static void do_oops_enter_exit(void)
{
	unsigned long flags;
	static int spin_counter;

	if (!pause_on_oops)
		return;

	spin_lock_irqsave(&pause_on_oops_lock, flags);
	if (pause_on_oops_flag == 0) {
		/* This CPU may now print the oops message */
		pause_on_oops_flag = 1;
	} else {
		/* We need to stall this CPU */
		if (!spin_counter) {
			/* This CPU gets to do the counting */
			spin_counter = pause_on_oops;
			do {
				spin_unlock(&pause_on_oops_lock);
				spin_msec(MSEC_PER_SEC);
				spin_lock(&pause_on_oops_lock);
			} while (--spin_counter);
			pause_on_oops_flag = 0;
		} else {
			/* This CPU waits for a different one */
			while (spin_counter) {
				spin_unlock(&pause_on_oops_lock);
				spin_msec(1);
				spin_lock(&pause_on_oops_lock);
			}
		}
	}
	spin_unlock_irqrestore(&pause_on_oops_lock, flags);
}

/*
 * Return true if the calling CPU is allowed to print oops-related info.
 * This is a bit racy..
 */
int oops_may_print(void)
{
	return pause_on_oops_flag == 0;
}

/*
 * Called when the architecture enters its oops handler, before it prints
 * anything.  If this is the first CPU to oops, and it's oopsing the first
 * time then let it proceed.
 *
 * This is all enabled by the pause_on_oops kernel boot option.  We do all
 * this to ensure that oopses don't scroll off the screen.  It has the
 * side-effect of preventing later-oopsing CPUs from mucking up the display,
 * too.
 *
 * It turns out that the CPU which is allowed to print ends up pausing for
 * the right duration, whereas all the other CPUs pause for twice as long:
 * once in oops_enter(), once in oops_exit().
 */
void oops_enter(void)
{
	tracing_off();
	/* can't trust the integrity of the kernel anymore: */
	debug_locks_off();
	do_oops_enter_exit();
}

/*
 * 64-bit random ID for oopses:
 */
static u64 oops_id;

static int init_oops_id(void)
{
	if (!oops_id)
		get_random_bytes(&oops_id, sizeof(oops_id));
	else
		oops_id++;

	return 0;
}
late_initcall(init_oops_id);

void print_oops_end_marker(void)
{
	init_oops_id();
	printk(KERN_WARNING "---[ end trace %016llx ]---\n",
		(unsigned long long)oops_id);
}

/*
 * Called when the architecture exits its oops handler, after printing
 * everything.
 */
void oops_exit(void)
{
	do_oops_enter_exit();
	print_oops_end_marker();
	kmsg_dump(KMSG_DUMP_OOPS);
}

#ifdef WANT_WARN_ON_SLOWPATH
struct slowpath_args {
	const char *fmt;
	va_list args;
};

static void warn_slowpath_common(const char *file, int line, void *caller,
				 unsigned taint, struct slowpath_args *args)
{
	disable_trace_on_warning();

	pr_warn("------------[ cut here ]------------\n");
	pr_warn("WARNING: CPU: %d PID: %d at %s:%d %pS()\n",
		raw_smp_processor_id(), current->pid, file, line, caller);

	if (args)
		vprintk(args->fmt, args->args);

	print_modules();
	dump_stack();
	print_oops_end_marker();
	/* Just a warning, don't kill lockdep. */
	add_taint(taint, LOCKDEP_STILL_OK);
}

void warn_slowpath_fmt(const char *file, int line, const char *fmt, ...)
{
	struct slowpath_args args;

	args.fmt = fmt;
	va_start(args.args, fmt);
	warn_slowpath_common(file, line, __builtin_return_address(0),
			     TAINT_WARN, &args);
	va_end(args.args);
}
EXPORT_SYMBOL(warn_slowpath_fmt);

void warn_slowpath_fmt_taint(const char *file, int line,
			     unsigned taint, const char *fmt, ...)
{
	struct slowpath_args args;

	args.fmt = fmt;
	va_start(args.args, fmt);
	warn_slowpath_common(file, line, __builtin_return_address(0),
			     taint, &args);
	va_end(args.args);
}
EXPORT_SYMBOL(warn_slowpath_fmt_taint);

void warn_slowpath_null(const char *file, int line)
{
	warn_slowpath_common(file, line, __builtin_return_address(0),
			     TAINT_WARN, NULL);
}
EXPORT_SYMBOL(warn_slowpath_null);
#endif

#ifdef CONFIG_CC_STACKPROTECTOR

/*
 * Called when gcc's -fstack-protector feature is used, and
 * gcc detects corruption of the on-stack canary value
 */
void __stack_chk_fail(void)
{
	panic("stack-protector: Kernel stack is corrupted in: %p\n",
		__builtin_return_address(0));
}
EXPORT_SYMBOL(__stack_chk_fail);

#endif

core_param(panic, panic_timeout, int, 0644);
core_param(pause_on_oops, pause_on_oops, int, 0644);

static int __init oops_setup(char *s)
{
	if (!s)
		return -EINVAL;
	if (!strcmp(s, "panic"))
		panic_on_oops = 1;
	return 0;
}
early_param("oops", oops_setup);

        panic()函数首先尽可能把出错信息打印出来,再拉响警报,然后清理现场。这时候大概系统已经崩溃,需等待一段时间让系统重启。在开发调试过程中,可以让 panic 打印更多信息或调试 panic 函数,从而分析系统出错原因。


5.3 通过ioctl 方法


        ioctl 是对一个文件描述符响应的系统调用,它可以实现特殊命令操作。ioctl 可以替代/proc 文件系统,实现一些调试的命令。
        使用 ioctl 获取信息比/proc 麻烦一些,因为通过应用程序的 ioctl 函数调用并且显示结果必须编写、编译一个应用程序,并且与正在测试的模块保持一致。反过来,驱动程序代码比实现/proc 文件相对简单一点。

        大多数时候 ioct 是获取信息的最好方法,因为它比读/proc 运行得快。假如数据必须在打印到屏幕上之前处理,以二进制格式获取数据将比读一个文本文件效率更高。另外,ioctl 不需要把数据分割成小于一页的碎片。
        ioctl 还有一个优点,就是信息获取命令可以保留在驱动程序中,即使已经完成调试工作。不像/proc 文件,在目录下所有人都可以看到。
        在内核空间,ioctl 驱动程序函数原型如下。

long (*unlocked ioctl) (struct file *filp, unsigned int cmd, unsigned long arg);

        filp 指针指向一个打开的文件所对应的 file 结构,cmd 参数是从用户空间未加修改传递过来的,可选的参数 arg 以无符号长整数传递,可以使用整数或指针。如果调用这个函数的时候不传递第 3 个参数,驱动程序接收的 arg 是未定义的。因为对于额外的参数的类型检查已经关闭,编译器不会警告一个非法的参数传递给 ioctl,并且任何相关的 BUG都将很难查找。
        大多数 ioctl 实现包含了一个大的 switch 语句,可以根据 cmd 参数选择适当的操作。不同的命令有不同的数值,可以通过宏定义简化编程。定制的驱动可以在头文件中声明这些符号。用户程序也必须包含这些头文件,以便使用这些符号。
        用户空间可以使用 ioctl 系统调用。
        int ioctl(int d,int request, ...);

        原型函数的省略号标志表明这个函数可以传递数量可变的参数。在实际系统中,系统调用不能用数量可变的参数。系统调用必须使用定义好的原型,因为用户可以通过硬件操作来访问。因此,这些省略号不代表变参,而是一个可选参数,传统上定义为 char *argp。原型的省略号可以防止编译过程的类型检查。第了个参数的本质依赖于特定的控制命令(第2个参数)。有些命令没有参数,有些取整型参数,有些取数据指针。使用指针可以把任意数据传递给 ioct 函数,设备就可以与用户空间交互任意大小的数据块了。

        ioctl 函数的不规范性使内核开发者并不喜欢它。每一个 ioctl 命令是一个分离的非正式的系统调用,并且没有办法按照易于理解的方式整理,也很难使这些不规范的 ioctl参数在所有的系统上都能工作。例如,用户空间进程运行 32 位模式的 64位系统,这导致强烈需要实现其他方式的多种控制操作。可行的方式包括在数据流中嵌入命令或者使用虚拟文件系统,以及 sysfs 或者驱动程序相关的文件系统。但是,事实上,ioctl 仍然是对设备操作最简单和最直接的选择。


六、内核源码调试

        因为 Linux 内核程序是 GNU GCC 编译的,所以对应地使用GNU GDB 调试器。Linux应用程序需要 gdbserver 辅助交叉调试。那么内核源码调试时,谁来充当 gdbserver 的角色呢?
        KGDB 是 Linux 内核调试的一种机制。它使用远程主机上的 GDB 调试目标板上的Linux 内核。准确地说,KGDB 是内核的功能扩展,它在内核中使用插 (Stub)的机制内核在启动时等待远程调试器的连接,相当于实现了 gdbserver 的功能。然后,远程主机的调试器 GDB 负责读取内核符号表和源码,并且建立连接。接下来,就可以在内核源码中设置断点、检查数据并进行其他操作。
        KGDB 的调试模型如图所示。


        在图中,KGDB 调试需要一台开发主机和一台目标板,开发主机和目标板之间通过一条串口线(null 调制解器电)连接。内源码在开发机器上编译且通过 GDB调试,内核镜像下载到目标机上运行,两者通过串口进行通信,Linux 2.6 内核还增加以太网接口通信的方式。

下面详细说明通过串口来调试 3.14.25 内核的步骤。

(1)配编译 Linux 内核镜像。

内核的配置选项如下。
 

Kernel hacking --->

        Compile-time checks and compiler options --->

                [*] Compile the kernel with debug info

        [*] KGDB: kernel debugger --->

                <*> KGDB: use kgdb over the serial console


(2) 在目标板上启动内核。
启动开发板,在 U-Boot 中重新设置 bootargs 环境变量,添加如下启动参数
kgdboc=ttySAC2,115200 kgdbwait
        kgdboc 表示用串口进行连接 (kgdboe 表示通过以太网口进行连接)。ttySAC2表示使用串口 2,这里需要注意的是,串口号必须要和控制台串口保持一致,否则连接不成功。115200 表示使用的波特率。kgdbwait 表示内核的串口驱动加载成功后,将会等待主机的gdb 连接。通过 U-Boot 加载并启动内核,运行正常将会出现下面的信息,然后内核等待连接。


0.550000] Serial: 8250/16550 driver,4 ports,IRQ sharing disabled

0.550000] 13800000.serial: ttysAcO at MMIO 0x13800000 (irg = 84, base baud0) is a s3C6400/10
0.555000] 13810000.serial: ttysAcl at MMIO 0x13810000 (irg = 85, base baud0) is a s3C6400/10
0.555000] 13820000.serial: ttysAC2 at MMIO 0x13820000 (irg = 86,base baud =
0) is a s3C6400/10

1.200000] console [ttysAC2] enabled

1.205000] 13830000.serial: ttysAC3 at MMIO 0x13830000 (irq = 87, base baud0) is a s3C6400/10

1.215000] kgdb: Registered I/0 driver kgdboc.

1.220000] kgdb: Waiting for connection from remote gdb..


成功看到上面的打印信息后,需要关闭串口终端软件,否则将会和 GDB 产生冲突另外,在 Linux 主机中通过下面的命令来修改串口设备文件的权限。$ sudo chmod 666 /dev/ttyuSBO
ttyUSBO表示连接开发板的主机上的串口。


(3)启动 gdb,建立连接。
        创建一个gdb 启动脚本文件,名字为,gdbinit,保存在内核源文件目录中。脚本.gdbinit内容如下。
 

#.gdbinit

set remotebaud 115200

symbol-file vmlinux

target remote /dev/ttyUSB0

set output-radix 16


        到内核源码树顶层目录下,启动交叉工具链的 gdb 工具。.gdbinit 脚本将在 gdb启动过程中自动执行。如果一正常,目标板连接成功进入调试模式。常见的情况是连接不成功,可能是因为串口设置或者连接不正确。使用的命令及输出如下。

$ arm-linux-gdb

GNU gdb 6.8

Copyright (C) 2008 Free Software Foundation, Inc,

License GPLv3+: GNU GPL version 3 or later <http;//gnu.org/11censes/gpl.html>

This is free software: you are free to change and redistribute it.

There is NO WARRANTY, to the extent permitted by law. Type "show copying"

and "show warranty" for details.

This GDB was configured as --host=i686-build pc-linux-gnu --target=armcortex a8-linux-gnueabi".0xc0078b68 in kgdb_breakpoint () at /home/kevin/workspace/fs4412/kernel/1inux3.14.25/arch/arm/include/asm/outercache.h:103103outer cache.sync();

(gdb)

(4) 使用 gdb 的调试命令设置断点,跟踪调试。
        找到内核源码适当的函数位置,设置断点,继续执行。这样就可以进行内核源码的调试了。


七、习题


1.要使用 printascii 函数,需要在内核配置时使能哪个选项( )。
{B] Kernel low-level debugging functions
[A] KGDB: kernel debugger
D] printk and dmesg options
[C] Panic on Oops


2.通过哪个文件可以查看并修改当前控制台的各个打印级别( )。
[B] /proc/kmsg
[A] /proc/devices
[C] /proc/sys/kernel/printk
[D] /var/log/dmesg


3.系统请求键是哪两个键的复合(
[B] Ctrl+SysRq
[A]Alt+SysRq
{D] Shift+Ctrl+SysRq
[C] Shift+SysRq

4.通过 sys 接口读取属性,需要实现哪个接口函数 ( )。
(D] print
[C] store
[B] show
[A]read

5.哪个工具可以用来转换 oops 信息 ( )。
[D] panic
[B] kallsyms

[A] ksymoops

[C] oops

6.使用 KGDB,通过串口调试内核,需要在 bootargs 中添加哪个参数 ( )


[C] ttyUSBO

[C] init

[B] ttySAC2
[A] kgdboc
 

答案: B        C        A        B        A        A

猜你喜欢

转载自blog.csdn.net/qq_52479948/article/details/134888782