本文先会介绍打印式调试技术, 也就是用类似printf的方式打印信息, 然后查看信息, 在调试驱动的过程中, 我们经常会用到这种方式.
然后会介绍查询式调试方式, 这种方式一般是在用户空间来查询内核信息.
最后会介绍调系统故障的几种方式: gdb如何使用, oops错误如何调试等.
最后一章还有个驱动设计指导规范, 总结了一些注意事项.
在实际调试过程中, 最主要的问题是某些printk打印出来的信息, 我们看不到它(比如log level问题), 本节最主要的目的是让我们知道如何去查看这些信息.
接下来会按照printk的数据流向来介绍它. 先来看一张图:
数据流向: printk ---> 记录字符串到log_buf ---> 获取console口的信号量 ---> 调用console口底层write函数将内容输出到实际的console口 ---> 唤醒klogd进程.
klogd进程会通知 syslogd进程,syslogd进程会把信息记录到/var/log/messages.
也可以通过dmesg命令从log_buf中获取信息.
本文不分析printk的具体实现, 而是从使用的角度来讲解printk. 如果想了解printk的实现细节, 可以参考网上的这篇文章或者这篇文章.
头文件: include/linux/printk.h
实现文件: kernel/printk/printk.c
在内核代码中, 我们经常会看见如下的打印语句:
printk(KERN_DEBUG “debug message\n”);
这就是printk的语法格式, 它与C语言中的printf很类似, 唯一的不同之处在于有个KERN_DEBUG, 这个标号就是loglevel.
loglevel: 打印级别, 它的作用在后面会介绍, 这里我们主要讲讲loglevel的原理.
loglevel是在内核头文件include/linux/kern_levels.h中定义的, printk.h会自动include此头文件. kern_levels.h中定义了[ 0 - 7]这8中打印级别:
#define KERN_SOH "\001" /* ASCII Start Of Header */
#define KERN_EMERG KERN_SOH "0" /* system is unusable */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
#define KERN_INFO KERN_SOH "6" /* informational */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
#define KERN_DEFAULT KERN_SOH "d" /* the default kernel loglevel */
每一个打印级别都是用宏定义的一个字符串, 字符串的头是‘\001‘这个特殊的字符, 后面跟上一个整数表示优先级, 数值越小, 优先级越高.
在编译过程中, 上述宏会被展开, 而且编译器会自动把这些字符串合并在一起, 类似如下这样:
printk("\0017debug message\n");
这也是为什么形如printk(KERN_DEBUG “debug message\n”)这条语句, KERN_DEBUG后面直接用空格与“debug message\n”分割的原因.
当然, 你直接这样写printk也是可以的:
printk("debug message\n");
这种情况下, 该条printk的默认打印级别就是default_message_loglevel(如果你在printk.c中搜索一下这个宏, 就能明白原理了, 这里不细说).
我们来看下这个宏是怎么定义的:
/* printk‘s without a loglevel use this.. */
#define MESSAGE_LOGLEVEL_DEFAULT CONFIG_MESSAGE_LOGLEVEL_DEFAULT
int console_printk[4] = {
CONSOLE_LOGLEVEL_DEFAULT, /* console_loglevel */
MESSAGE_LOGLEVEL_DEFAULT, /* default_message_loglevel */
CONSOLE_LOGLEVEL_MIN, /* minimum_console_loglevel */
CONSOLE_LOGLEVEL_DEFAULT, /* default_console_loglevel */
};
#define default_message_loglevel (console_printk[1])
从上面这段代码中, 你知道了什么? 没错, 默认的打印级别是在menuconfig中定义的: CONFIG_MESSAGE_LOGLEVEL_DEFAULT
有的时候, 我们希望在每条打印信息前面加上一个时间信息, 方便我们判断两条消息间隔了多久, 这个时候, 我们可以在menuconfig中打开CONFIG_PRINTK_TIME开关. 这样, 你就会得到类似下面的信息:
[ 0.000198] Console: colour dummy device 80x30
[ 0.000230] Calibrating delay loop... 996.14 BogoMIPS (lpj=4980736)
如果你想了解这个机制的细节, 在printk.c中搜索CONFIG_PRINTK_TIME, 你就知道原理了.
顺便说一下, 除了可以通过menuconfig开启CONFIG_PRINTK_TIME, 你还可以用echo修改/sys/module/printk/parameters/time. 它是在printk.c中定义的一个module_param
有些时候, 我们可能需要在一个循环里面调用printk. 如果某种情况下这个循环无法跳出, 在那不停的运行, 这个时候就可能产生成千上万次printk.
这种大量的printk非常糟糕, 因为printk打印的消息, 最终可能会送往UART, 而UART的速度较慢, 大量的printk可能导致系统卡死.
内核提供了一个API来解决这种问题: int printk_ratelimit(void), 它的用法一般如下:
if (printk_ratelimit())
printk(KERN_NOTICE “The is an error\n”);
printk_ratelimit可以跟踪发送到console的消息数量, 如果输出速度超过一个阀值, printk_ratelimit将返回零.
我们可以通过/proc/sys/kernel/printk_ratelimit(在重新打开消息之前应该等待的秒数) 以及 /proc/sys/kernel/printk_ratelimit_burst(在进行速度限制之前可以接收的消息数) 来定制printk_ratelimit的行为.
NOTE: 上述/proc中的两个参数是在kernel/sysctl.c中创建的.
当你调用printk打印一条消息时, 该消息会先拷贝到一个局部变量textbuf[LOG_LINE_MAX]中(printk.c -> vprintk_emit中定义的), LOG_LINE_MAX的大小是 (1024 - 32)字节.
#define PREFIX_MAX 32
#define LOG_LINE_MAX (1024 - PREFIX_MAX)
然后, 会把textbuf中的数据拷贝到log_buf中(printk.c中定义一个环形缓冲区).
#define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)
static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN);
static char *log_buf = __log_buf;
static u32 log_buf_len = __LOG_BUF_LEN;
从上面的代码可知, log_buf的大小可以在menuconfig中通过CONFIG_LOG_BUF_SHIFT来配置, 默认情况下是64KB. 当log_buf中的数据大于64KB时, 会覆盖掉前面的数据.
OK, 本小节唯一的目的就是让你知道如何配置log_buf的大小.
注意, 不管printk的loglevel是什么, 都会被写入到log_buf.
当消息被存储到log_buf, 接下来就会显示到console上.
console是个什么东西?
要了解console, 首先需要知道一个概念: 终端.
终端又名tty, 本小节不打算讨论终端的细节, 会有一篇专门的文章来讲述tty子系统.
终端的类型多种多样, 取决于载体是什么: 例如用串口做为收发数据的载体, 就是串口终端; 用控制台做为收发数据的载体, 就是控制台终端….
额, 控制台? 它又是什么? 注意不要把控制台跟console混淆了, 虽然console翻译过来也叫控制台, 但是你就记着它俩是两个概念吧. 控制台一般可以理解为Linux的显示子系统+输入子系统. 控制台终端就是指在显示子系统上(LCD, HDMI)显示数据, 并从输入子系统(键盘, 鼠标)获取数据的终端.
OK, 终端大致解释完了, 那么console呢? 终端有多种类型, 任何一种类型的终端都可以被注册为console. console的注册的API是register_console(printk.c中定义), 所有注册的console都会挂载到console_drivers这个全局链表下.
那么printk数据会显示在哪些console上呢? 看看如下代码:
for_each_console {
...
if (!(con->flags & CON_ENABLED))
continue;
if (!con->write)
continue;
con->write(con, text, len);
...
}
意思很清楚了, 针对console_drivers下的每个console, 检查CON_ENABLED是否置位, 并检查是否实现了write函数. 如果条件都满足, 则会调用con->write. 假设此con是个串口终端, 则write函数就会往串口写入一串数据. 此时, 你就能在串口上看到printk打印的消息了.
write函数一般都会实现, 关键问题是CON_ENABLED位是否置位. 你可能会想, 在注册console的时候把这位打开不就可以了吗? 但实际上, console在注册的时候, CON_ENABLED位默认是DISABLE状态的. 那么谁会enable这些console呢? 请看下一小节.
还记得uboot传递的bootargs参数是怎么写的吗, 一般如下:
bootargs = "console=ttyS0,115200 console=tty1 ignore_loglevel earlyprintk";
内核在启动阶段, 会解析bootargs, 针对”console=”这个字符串, 解析它的函数是在printk.c中定义的console_setup.
__setup("console=", console_setup);
console_setup函数会解析出console的name, index, options; 并用一个结构体struct console_cmdline来表示这个console.
struct console_cmdline
{
char name[16]; /* Name of the driver */
int index; /* Minor dev. to use */
char *options; /* Options for the driver */
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE
char *brl_options; /* Options for braille driver */
#endif
};
bootargs中每个”console=”字符串都会导致console_setup被调用1次, 每次调用都会生成一个console_cmdline结构体.
按照内核的惯例, 肯定有一个全局的什么东西来存储这些console_cmdline’s. 没错, printk.c中定义了一个全局结构体数组:
#define MAX_CMDLINECONSOLES 8
static struct console_cmdline console_cmdline[MAX_CMDLINECONSOLES];
这个结构体数组最多存储8个元素, 也就意味着, bootargs中最多只能有8个”console=”.
当bootargs被解析完毕之后, console_cmdline中就存储了所有这些通过”console=”指定的consoles.
bootargs的解析时间很早, 差不多是在内核刚刚启动的时候, 之后才会执行那些注册console的驱动代码.
当这些代码调用API register_console注册一个console时, register_console函数会检查这个console->name & console->index在console_cmdline中是否存在, 如果存在, 则证明这个新注册的console被选中用于显示printk的打印消息了, 接而会执行newcon->flags |= CON_ENABLED
另外一个问题: 如果bootargs里面没有指定”console=”, 那么printk的信息会显示到哪里呢?
答案也很简单, 会把第一个注册的console的flags设置为CON_ENABLED.
OK, 现在你应该明白了printk打印的信息会显示在哪些console上了吧.
printk打印的所有信息都会显示出来吗? 显然不是, 不然要log_level干嘛, 接下来我们说说哪些数据会显示在console上.
在把log_buf中的数据显示到console之前, 需要检查loglevel. 代码如下:
static void call_console_drivers(int level, const char *text, size_t len)
{
struct console *con;
……
if (level >= console_loglevel && !ignore_loglevel)
return;
if (!console_drivers)
return;
for_each_console(con) {
……
if (!(con->flags & CON_ENABLED))
continue;
if (!con->write)
continue;
……
con->write(con, text, len);
}
}
注意红色字体的那条if语句, 如果条件不满足, 则直接return, 不会write到console上.
这个if里面涉及到两个判断, 一是比较loglevel, 二是判断ignore_loglevel这个bool变量. 我们先看看后者.
ignore_loglevel
从名字也能猜到意思了: 如果ignore_loglevel = true, 那么不管loglevel是什么, 都会无条件的write到console上.
ignore_loglevel默认情况下是false, 有两种方式可以修改它:
一种是通过/sys/module/printk/parameters/ignore_loglevel
另外一种是通过bootargs, 如果bootargs里面包含”ignore_loglevel”这个字符串, 则ignore_loglevel为true.
bootargs = "console=ttyS0,115200 console=tty1 ignore_loglevel earlyprintk";
”ignore_loglevel”是被printk.c中的ignore_loglevel_setup函数解析的:
static int __init ignore_loglevel_setup(char *str)
{
ignore_loglevel = true;
pr_info("debug: ignoring loglevel setting.\n");
return 0;
}
early_param("ignore_loglevel", ignore_loglevel_setup);
module_param(ignore_loglevel, bool, S_IRUGO | S_IWUSR);
MODULE_PARM_DESC(ignore_loglevel, "ignore loglevel setting, to"
"print all kernel messages to the console.");
比较loglevel
如果ignore_loglevel = false, 则要比较loglevel, 当level < console_loglevel时, 才会被write到console上.
level就是你在使用printk的时候, 指定的打印级别[ 0 - 7].
那么console_loglevel是多少呢?
先看看默认情况下, printk.c和printk.h中的定义:
#define CONSOLE_LOGLEVEL_DEFAULT 7 /* anything MORE serious than KERN_DEBUG */
int console_printk[4] = {
CONSOLE_LOGLEVEL_DEFAULT, /* console_loglevel */
MESSAGE_LOGLEVEL_DEFAULT, /* default_message_loglevel */
CONSOLE_LOGLEVEL_MIN, /* minimum_console_loglevel */
CONSOLE_LOGLEVEL_DEFAULT, /* default_console_loglevel */
};
#define console_loglevel (console_printk[0])
上述代码表明console_loglevel默认情况下是7, 也就意味着打印级别[0 - 6]都可以被显示在console上.
上面说的是默认情况, 那么console_loglevel如何修改呢? 可以通过/proc, 也可以通过bootargs, 还可以通过用户空间的dmesg命令修改.
先说如何通过/proc修改:
# cat /proc/sys/kernel/printk
7 4 1 7
第一个数字就是对应console_loglevel, 可以通过echo 5 > /proc/sys/kernel/printk来修改console_loglevel.
NOTE: 上述/proc中的printk参数是在kernel/sysctl.c中创建的.
在来说说如何通过bootargs修改:
bootargs = "console=ttyS0,115200 console=tty1 ignore_loglevel quiet/debug loglevel=xx";
如果bootargs中定义了这quiet或者debug; loglevel这几个early_param的话, 也可以修改console_loglevel.
这几个early_param的解析函数在init/main.c中:
static int __init debug_kernel(char *str)
{
console_loglevel = CONSOLE_LOGLEVEL_DEBUG; //CONSOLE_LOGLEVEL_DEBUG = 10
return 0;
}
static int __init quiet_kernel(char *str)
{
console_loglevel = CONSOLE_LOGLEVEL_QUIET; //CONSOLE_LOGLEVEL_QUIET = 4
return 0;
}
early_param("debug", debug_kernel);
early_param("quiet", quiet_kernel);
static int __init loglevel(char *str)
{
int newlevel;
/*
* Only update loglevel value when a correct setting was passed,
* to prevent blind crashes (when loglevel being set to 0) that
* are quite hard to debug
*/
if (get_option(&str, &newlevel)) {
console_loglevel = newlevel;
return 0;
}
return -EINVAL;
}
early_param("loglevel", loglevel);
代码已经非常清楚了, 就不多说了.
在说说怎么通过dmesg命令修改:
dmesg -n xx
就能修改console_loglevel的值.
回头看看本章开头的那个图, 用户空间中:
syslogd进程可以读取log_buf中的数据, 并把这些数据存储在/var/log/messages中. 我们知道不管什么打印级别的消息, 都会被存储在log_buf中, 那syslogd在读取这些数据的时候, 会不会做loglevel的过滤呢? 应该也会, 没有仔细研究. syslogd在调试驱动中用的不多, 就 不细说了.
除了syslogd, 还可以用dmesg从log_buf中读取数据, 并将信息显示在stdout上.
在使用dmesg的时候, 同样会关注一个问题, 它是否会对log_buf中的数据做loglevel的过滤?
实验了一下, 如果直接使用dmesg命令, 它会把log_buf中所有的信息都打印出来, 然后如果用dmesg -l warn, 则只会打印warn级别以上的消息.
每台机器上的dmesg实现都不一样, 用dmesg -h看看你所用的dmesg支持哪些功能.
这个也有DEMO, 是的, 弄个DEMO吧.
DEMO的主要目的是让你知道用prink打印出来的消息, 在哪里可以查看到. 因为我们在调试内核驱动的时候, 最想看到的就是驱动里面printk打印出来的那些消息, 以便我们调试.
设计这样一个简单的DEMO, 在装载和卸载函数里面各用一句printk(DEBUG “XXX\N”);
在load module之前, 思考一个问题? 当load module之后, 你能在console上看到打印信息吗? 能用dmesg看到打印信息吗? 如果不能, 怎样看到这些打印信息.
https://gitlab.com/study-kernel/debugging_techniques/printk
如果你觉得每次printk的时候, 都要敲一个loglevel很麻烦的话, 内核代码还提供了一些宏, 帮你省事.
头文件: include/linux/printk.h
/*
* These can be used to print at the various log levels.
* All of these will print unconditionally, although note that pr_debug()
* and other debug macros are compiled out unless either DEBUG is defined
* or CONFIG_DYNAMIC_DEBUG is set.
*/
#define pr_emerg(fmt, ...) \
printk(KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__)
#define pr_alert(fmt, ...) \
printk(KERN_ALERT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_crit(fmt, ...) \
printk(KERN_CRIT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_err(fmt, ...) \
printk(KERN_ERR pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warning(fmt, ...) \
printk(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warn pr_warning
#define pr_notice(fmt, ...) \
printk(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)
#define pr_info(fmt, ...) \
printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
#define pr_cont(fmt, ...) \
printk(KERN_CONT fmt, ##__VA_ARGS__)
/* pr_devel() should produce zero code unless DEBUG is defined */
#ifdef DEBUG
#define pr_devel(fmt, ...) \
printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#else
#define pr_devel(fmt, ...) \
no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#endif
#include <linux/dynamic_debug.h>
/* If you are writing a driver, please use dev_dbg instead */
#if defined(CONFIG_DYNAMIC_DEBUG)
/* dynamic_pr_debug() uses pr_fmt() internally so we don‘t need it here */
#define pr_debug(fmt, ...) \
dynamic_pr_debug(fmt, ##__VA_ARGS__)
#elif defined(DEBUG)
#define pr_debug(fmt, ...) \
printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#else
#define pr_debug(fmt, ...) \
no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#endif
注意: 如果需要用pr_devel或者pr_debug打印出消息, 则需要在你的C文件里面define DEBUG.
#define DEBUG
在驱动代码中, 我们应该用dev_xxx来打印消息, 它们的好处是会自动帮你显示出device->name和driver->name, 方便你调试.
头文件: include/linux/device.h
实现文件: drivers/base/core.c
虽然dev_xxx不是直接调用的printk, 不过你可以简单的理解为它们就是对printk的封装.
define_dev_printk_level(dev_emerg, KERN_EMERG);
define_dev_printk_level(dev_alert, KERN_ALERT);
define_dev_printk_level(dev_crit, KERN_CRIT);
define_dev_printk_level(dev_err, KERN_ERR);
define_dev_printk_level(dev_warn, KERN_WARNING);
define_dev_printk_level(dev_notice, KERN_NOTICE); define_dev_printk_level(_dev_info, KERN_INFO);
#define dev_info(dev, fmt, arg...) _dev_info(dev, fmt, ##arg)
还有下面这些宏, 不过在使用这些宏的时候, 你应该在你的C文件里面define DEBUG.
或者, 你也在menuconfig中打开CONFIG_DYNAMIC_DEBUG这个开关, 这样不单单某一个C文件, 所有使用下面宏的地方都可以打印出消息.
#if defined(CONFIG_DYNAMIC_DEBUG)
#define dev_dbg(dev, format, ...) \
do { \
dynamic_dev_dbg(dev, format, ##__VA_ARGS__); \
} while (0)
#elif defined(DEBUG)
#define dev_dbg(dev, format, arg...) \
dev_printk(KERN_DEBUG, dev, format, ##arg)
#else
#define dev_dbg(dev, format, arg...) \
({ \
if (0) \
dev_printk(KERN_DEBUG, dev, format, ##arg); \
0; \
})
#endif
本小节就没必要做DEMO了, 本小节介绍的打印消息的宏, 都只是数据产生的一种方式, 类似于printk, 它们都把消息放到log_buf这个环形缓冲区中.
至于log_buf中的信息能否在console或者dmesg中看到, 遵循前一节中所描述的规则.
如果你想试验, 可以在前一节DEMO的基础上测试.
首先说明一下, early_printk不是printk的简单封装, 它是为了解决某一类问题而存在.
为了解决什么问题? 如果你能猜到, 那证明你对前一章了解的很透彻.
在《printk》一章中我们知道, 一个板子, 假设你把它的串口接到的PC上, 在内核启动过程中, 希望在串口看到内核打印的消息.
这种情况下, 你需要在内核代码里面把串口注册为console, 并在bootargs里面用类似”console=ttyS0”这样的标志.
当内核代码成功将串口注册为console之后, 你就能在串口上看到打印信息了.
但是, 在内核的整个启动过程中, console注册是在比较靠后的位置.
如果内核在console注册之前就挂掉了, 这个时候你就无法看到任何打印信息了.
为了解决这个问题, 内核提供了early_printk机制.
要使用该机制, 你需要做以下几件事情:
? 在menuconfig中打开CONFIG_EARLY_PRINTK
? 在bootargs中传递一个参数: bootargs="…. earlyprintk";
? 用汇编语言实现函数: addruart, waituart, senduart, busyuart这几个函数, 利用UART打印信息
整个机制的是这样的:
在内核参数解析阶段, arch/arm/kernel/early_printk.c里面实现了一段代码, 用于解析earlyprintk
extern void printch(int);
static void early_write(const char *s, unsigned n)
{
while (n-- > 0) {
if (*s == ‘\n‘)
printch(‘\r‘);
printch(*s);
s++;
}
}
static void early_console_write(struct console *con, const char *s, unsigned n)
{
early_write(s, n);
}
static struct console early_console_dev = {
.name = "earlycon",
.write = early_console_write,
.flags = CON_PRINTBUFFER | CON_BOOT,
.index = -1,
};
static int __init setup_early_printk(char *buf)
{
early_console = &early_console_dev;
register_console(&early_console_dev);
return 0;
}
early_param("earlyprintk", setup_early_printk);
整个实现一目了然, 不在赘述. 上述代码最终会调用printch打印消息, 那么printch是在哪里实现的呢?
printch的实现
它是在arch/arm/kernel/debug.S中定义的.
该汇编文件最终会调用addruart, waituart, senduart, busyuart.
所以你需要有个汇编文件来实现上述4个函数, 一般来说, 半导体原厂都会编写这几个函数.
那么怎么查看原厂是否提供了这几个函数呢? debug.S中是怎样调用到这几个函数的呢(还需要在menuconfig中打开CONFIG_DEBUG_LL)? 我们将在下一节讲解 (放在下一节讲解更合适).
当参数解析完毕之后, 我们就可以使用early_printk来打印消息了.
early_printk是在kernel/printk/printk.c中实现的, 代码如下:
#ifdef CONFIG_EARLY_PRINTK
struct console *early_console;
void early_vprintk(const char *fmt, va_list ap)
{
if (early_console) {
char buf[512];
int n = vscnprintf(buf, sizeof(buf), fmt, ap);
early_console->write(early_console, buf, n);
}
}
asmlinkage __visible void early_printk(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
early_vprintk(fmt, ap);
va_end(ap);
}
#endif
代码实现也非常清晰了.
注意: 从上述实现中可知, early_printk与printk不一样, 它没有打印级别这一说, 凡事调用early_printk打印的消息, 都会无条件显示在early_printk.c中注册的earlycon这个console上.
为什么都多出来个early_print?
原因是类似的, 内核启动过程中, 如果在bootargs参数解析之前, 内核就挂掉了, 这个时候你用early_printk也看不到任何消息. 此时你就需要使用early_print.
early_print的有用之处在于, 我们经常会遇到一种情况:
uncompressing linux...ok, booting the kernel
然后, 就没有然后了, 内核卡住了, 什么消息都没有, 我们不知道出了什么事情, 此时就需要用early_print进行调试.
如果要使用early_print, 你需要做以下几件事情:
? 在menuconfig中, 打开CONFIG_DEBUG_LL
? 用汇编语言实现函数: addruart, waituart, senduart, busyuart这几个函数, 利用UART打印信息
early_print的代码是在arch/arm/kernel/setup.c中实现的:
void __init early_print(const char *str, ...)
{
extern void printascii(const char *);
char buf[256];
va_list ap;
va_start(ap, str);
vsnprintf(buf, sizeof(buf), str, ap);
va_end(ap);
#ifdef CONFIG_DEBUG_LL
printascii(buf);
#endif
printk("%s", buf);
}
从上述代码中我们可以知道:
如果开启了CONFIG_DEBUG_LL, 则early_prink会通过printascii来打印消息.
同时, 它还会调用printk, 也就是说即使你不开启CONFIG_DEBUG_LL, early_print打印的消息最终也会通过printk显示出来.
那么printascii是在哪里实现的呢?
上一节我们说到, printch是在arch/arm/kernel/debug.S中实现的, printascii也是在该文件中实现的, 代码如下:
#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 */
......
#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
ret lr
ENDPROC(printascii)
ENTRY(printch)
addruart_current r3, r1, r2
mov r1, r0
mov r0, #0
b 1b
ENDPROC(printch)
......
#else
......
#endif
代码逻辑也很清晰, 唯一的疑问是, 蓝色标注的4个函数是在哪里实现的呢?
一般半导体原厂会实现这4个函数.
那么如何查看原厂是否实现呢? head.S又是怎么调用到这4个函数的呢?
注意上述代码最顶上的一条语句:
#if !defined(CONFIG_DEBUG_SEMIHOSTING)
#include CONFIG_DEBUG_LL_INCLUDE
#endif
CONFIG_DEBUG_LL_INCLUDE其实是代指一个汇编代码的路径, 原厂实现的4个函数就放在该汇编代码中. #include代表引用此汇编代码.
那么CONFIG_DEBUG_LL_INCLUDE所指代的路径到底在哪里呢?
CONFIG_DEBUG_LL_INCLUDE是在arch/arm/Kconfig.debug中被赋值的, arch/arm/Kconfig会自动引用 Kconfig.debug:
config DEBUG_LL_INCLUDE
string
default "debug/sa1100.S" if DEBUG_SA1100
……..
default "debug/omap2plus.S" if DEBUG_OMAP2PLUS_UART
……..
debug/omap2plus.S就是原厂汇编代码所在的路径, 它的实际目录是arch/arm/include/debug/omap2plus.S
查看omap2plus.S, 4个函数都在里面了.
至此, 你应该就明白early_print到底是如何工作的了.
Kernel提供了打印函数调用栈的API, 为了更好的理解栈的回溯过程, 你可能需要一点栈帧的知识, 下面先介绍下栈帧的概念, 在介绍打印调用栈的方法.
理解栈帧的概念对我们查看crash log中的stack dump信息很有帮助.
//以下两个网页简述
// 理解APCS-- ARM过程调用标准 : 比喻法描述ARM寄存器的压栈出栈过程
// ARM FP寄存器及frame pointer介绍 : 栈帧的结构示意图
个人对栈帧原理的理解(add @2019.02.28):
我们通过当前EBP的值, 可以手动推导出整个调用栈, 推导过程是:
Linux 内核打印堆栈的实现原理(add @2019.02.28) : 内核中dump_stack()的实现,并在用户态模拟dump_stack()
内核提供了打印调用栈的接口, 只需要#inlcude <linux/printk.h>, 就能使用dump_stack()函数打印调用栈了.
dump_stack能打印出内核符号表和内核模块符号表的函数名与地址的对应关系, 例如我们在内核模块里面加入一个dump_stack(), 能得到如下信息:
[21526.804155] Hardware name: Generic AM33XX (Flattened Device Tree)
[21526.804185] [<c0015e8d>] (unwind_backtrace) from [<c0012579>] (show_stack+0x11/0x14)
[21526.804197] [<c0012579>] (show_stack) from [<c03ddf7d>] (dump_stack+0x5d/0x6c)
[21526.804215] [<c03ddf7d>] (dump_stack) from [<bf83a043>] (alloc_page_owner+0x42/0x5c [page_owner_test])
[21526.804232] [<bf83a043>] (alloc_page_owner [page_owner_test]) from [<bf83c03d>] (hello_init+0x3c/0x67 [page_owner_test])
[21526.804248] [<bf83c03d>] (hello_init [page_owner_test]) from [<c0009713>] (do_one_initcall+0x9b/0x198)
[21526.804261] [<c0009713>] (do_one_initcall) from [<c00fb215>] (do_init_module+0x4d/0x310)
[21526.804279] [<c00fb215>] (do_init_module) from [<c00a116b>] (load_module+0x16eb/0x1b80)
[21526.804289] [<c00a116b>] (load_module) from [<c00a17c7>] (SyS_finit_module+0x77/0x9c)
[21526.804303] [<c00a17c7>] (SyS_finit_module) from [<c000ed21>] (ret_fast_syscall+0x1/0x52)
WARN_ON(include/asm-generic/bug.h)封装了dump_stack(), 也可用于打印调用栈.
BUG_ON(include/asm-generic/bug.h)会调用panic(), panic函数会调用dump_stack(), 从而打印出调用栈.
不过panic也会让内核崩溃卡死, 因此慎用BUG_ON.
第二章我们介绍了打印式调试, 打印式调试很常用, 不过也有它的缺点, 很多打印式调试都是借助串口来打印信息, 但是串口是串行传输的, 速度很慢, 一条printk可能会耗时10ms左右.
过多的打印信息会影响到系统的性能. 所以在release代码的时候, 一般会尽可能减少打印信息. 对Linux内核来讲, 内核启动完毕进入到文件系统之后, 正常情况下不应该打印任何信息.
鉴于上述缺点, 我们介绍另外一种调试方式: 查询式调试.
所谓查询式调试就是说当我需要某些调试信息的时候, 内核代码才会产生这些信息.
一般的查询式调试有以下几种:
时间关系, 这几种就先不细说了, 后面再来完善.
proc属于虚拟文件系统之一, 我们经常会通过cat /proc/xxx来查询内核的一些信息, 例如 cat /proc/meminfo.
内核里面很多子系统都用到了proc, 特别是一些进程调度, 内存管理类的核心系统. 如果想通过/proc查看信息, 需要做如下两个动作:
sudo mount -t proc none /proc
然后, cat /proc/xxx即可.
如果我们自己在编写驱动的时候, 也想用proc, 怎么办呢?
其实很简答, 既然proc是文件系统, 哪怕是虚拟的, 与文件系统打交道也就2个主要动作: 创建文件或者创建文件夹.
proc提供给其它模块的头文件是 : include/linux/proc_fs.h
proc的实现文件是 : fs/proc/generic.c
如果想要使用proc, 只需要在自己的驱动里面#incude上述头文件, 然后准备好file_operations结构体, 然后调用proc提供的API创建文件即可.
以/proc/meminfo为例, 看看代码该如何写:
static const struct file_operations meminfo_proc_fops = {
.open = meminfo_proc_open,
.read = seq_read,
.llseek = seq_lseek,
.release = single_release,
};
static int __init proc_meminfo_init(void)
{
proc_create("meminfo", 0, NULL, &meminfo_proc_fops);
return 0;
}
fs_initcall(proc_meminfo_init);
除了创建文件, 你还可以创建文件夹, 或者在某个文件夹下创建文件. 更多API请查看源码.
debugfs属于虚拟文件系统之一. 可以利用debugfs向用户空间提供一些调试信息.
内核里面的很多子系统都已经用到了debugfs, 如果我们想查看这些信息, 需要做两个动作:
sudo mount -t debugfs debugfs /sys/kernel/debug/
然后, 你就可以在/sys/kernel/debug/目录下看到很多文件或文件夹, cat这些文件, 就能得到你需要的调试信息了.
如果我们自己在编写驱动的时候, 也想用debugfs来向用户空间展示一些调试信息, 该怎么办呢?
其实很简答, 既然debugfs是文件系统, 哪怕是虚拟的, 与文件系统打交道也就2个主要动作: 创建文件或者创建文件夹.
debugfs提供给其它模块的头文件是 : include/linux/debugfs.h
debugfs的实现文件是 : fs/debugfs/inode.c
如果想要使用debugfs, 只需要在自己的驱动里面#incude上述头文件, 然后准备好file_operations结构体, 然后调用debugfs提供的API创建文件即可.
以GPIO子系统为例, GPIO子系统在debugfs下创建了一个文件, 名称是”gpio”.
static const struct file_operations gpiolib_operations = {
.owner = THIS_MODULE,
.open = gpiolib_open,
.read = seq_read,
.llseek = seq_lseek,
.release = seq_release,
};
static int __init gpiolib_debugfs_init(void)
{
/* /sys/kernel/debug/gpio */
(void) debugfs_create_file("gpio", S_IFREG | S_IRUGO,
NULL, NULL, &gpiolib_operations);
return 0;
}
subsys_initcall(gpiolib_debugfs_init);
上述代码演示了如何创建一个文件, 当你mount了debugfs, 然后cat /sys/kernel/debug/gpio, cat操作就会调用到open&read函数, read函数就会返回调试信息给到用户空间了.
至于file_operations该如何实现, 就是你自己的事情了.
除了创建文件, 你还可以创建文件夹, 或者在某个文件夹下创建文件. 更多API请查看源码.
ecall是海思做的一套系统, 它可以实现在用户空间查看任意寄存器的值、或者调用内核里面的任何一个函数.
系统分内核部分和应用程序, 内核部分hisi-easy-shell.c最终会提供一个设备节点, 应用程序则(暂无代码)通过ioctl与此设备节点交互.
官网 : https://www.kernel.org/doc/html/latest/admin-guide/sysrq.html
IBM上对命令的介绍更详细些 : https://www.ibm.com/developerworks/cn/linux/l-cn-sysrq/
Command |
Function |
b |
Will immediately reboot the system without syncing or unmounting your disks. |
c |
Will perform a system crash by a NULL pointer dereference. A crashdump will be taken if configured. |
d |
Shows all locks that are held. |
e |
Send a SIGTERM to all processes, except for init. |
f |
Will call the oom killer to kill a memory hog process, but do not panic if nothing can be killed. |
g |
Used by kgdb (kernel debugger) |
h |
Will display help (actually any other key than those listed here will display help. but h is easy to remember :-) |
i |
Send a SIGKILL to all processes, except for init. |
j |
Forcibly “Just thaw it” - filesystems frozen by the FIFREEZE ioctl. |
k |
Secure Access Key (SAK) Kills all programs on the current virtual console. NOTE: See important comments below in SAK section. |
l |
Shows a stack backtrace for all active CPUs. |
m |
Will dump current memory info to your console. |
n |
Used to make RT tasks nice-able |
o |
Will shut your system off (if configured and supported). |
p |
Will dump the current registers and flags to your console. |
q |
Will dump per CPU lists of all armed hrtimers (but NOT regular timer_list timers) and detailed information about all clockevent devices. |
r |
Turns off keyboard raw mode and sets it to XLATE. |
s |
Will attempt to sync all mounted filesystems. |
t |
Will dump a list of current tasks and their information to your console. |
u |
Will attempt to remount all mounted filesystems read-only. |
v |
Forcefully restores framebuffer console |
v |
Causes ETM buffer dump [ARM-specific] |
w |
Dumps tasks that are in uninterruptable (blocked) state. |
x |
Used by xmon interface on ppc/powerpc platforms. Show global PMU Registers on sparc64. Dump all TLB entries on MIPS. |
y |
Show global CPU Registers [SPARC-64 specific] |
z |
Dump the ftrace buffer |
0-9 |
Sets the console log level, controlling which kernel messages will be printed to your console. (0, for example would make it so that only emergency messages like PANICs or OOPSes would make it to your console.) |
反汇编的主要目的是通过异常地址找到对应的代码, 本章介绍一些相关的概念和工具.
ELF代表Executable and Linkable Format. 它是一种文件格式, 可用于描述目标文件(即编译生成的obj文件)、可执行文件(即链接生成的可执行程序)和库文件.
当系统出现故障时, 展示给我们的经常是一些出错时的地址信息, 我们需要的是把这些地址信息转换对应C/C++代码, 然后才能分析代码找到出错的原因.
要想更好的理解这些地址的信息的含义, 我们得有一点ELF的背景知识, 因此我们首先简单介绍一下ELF文件格式.
ELF在Linux下成为标准格式已经很长时间,代替了早年的a.out格式。ELF一个特别的优点在于,同一文件格式可以用于内核支持的几乎所有体系结构上。这不仅简化了用户空间工具程序的创建,也简化了内核自身的程序设计。例如,装载程序的设计。
但是文件格式相同并不意味着不同系统上的程序之间存在二进制兼容性,例如,FreeBSD和Linux都使用ELF作为二进制格式。尽管二者在文件中组织数据的方式相同,但在系统调用机制以及系统调用的语义方面,仍然有差别。这也是在没有中间仿真层的情况下,FreeBSD程序不能在Linux下运行的原因(反过来,同样如此) 。
另外, 二进制程序不能在不同体系结构间交换 (例如, 为Alpha CPU编译的Linux二进制程序不能在Sparc Linux上执行),因为底层的体系结构是完全不同的。但由于ELF的存在,对所有体系结构而言,程序本身的相关信息以及程序的各个部分在二进制文件中编码的方式都是相同的。
Linux不仅将ELF用于用户空间应用程序和库,还用于构建模块。内核本身也是ELF格式。ELF是一种开放格式,其规范可以自由获得。
在后文的介绍中, 我们会边讲解原理, 边演示实例. 演示实例需要编译C代码, 因此我们在这里列出一个简短的C代码, 以备后文使用.
#include<stdio.h>
int add (int a, int b) {
printf("Numbers are added together\n");
return a+b;
}
int main() {
int a,b;
a = 3;
b = 4;
int ret = add(a,b);
printf("Result: %d\n", ret);
return 0;
}
然后我们用gcc编译这段代码, 生成一个目标文件和一个可执行文件:
gcc -c main.c -> main.o (目标文件)
gcc -o main main.o -> main (可执行文件)
可以通过readelf -h或者file xxx来确定一个文件是否是ELF格式的文件.
参考《深入Linux内核架构 附录E》来完成本章内容
节/段, 也叫section.
上一节我们介绍了ELF中存在的各个sections, 符号表也属于section之一(.symtab).
符号表是每个ELF文件的一个重要部分, 因为它存储着程序使用的全局变量和函数. 这些全局变量和函数包括程序自身定义的和程序使用的外部库中定义的.
对于程序自身定义的全局变量和函数, 由于它们运行时的地址是在链接期间确定的, 因此符合表里面存储着这些变量和函数与其地址的一一对应关系.
对于外部库中定义的全局变量和函数, 如果是静态链接, 那么符合表里面也能存储其与地址的一一对应关系; 如果是动态链接(例如.so库, 是一种可重定向的ELF格式的文件), 由于其在运行期间(使用 ld-linux.so)动态确定函数的运行地址, 因此符号表里面只存储了所引用的变量/函数名, 无法确定与地址的一一对应关系.
下面看个实例, 加深理解(利用前文编译的main.o和main).
nm 工具可生成程序定义和使用的所有符号列表, 我们先来看看main.o:
? 左侧一列给出了符号的值, 即符号定义在目标文件中的位置(注意这个值并不是运行时的地址哦, 因为此时还没进行链接动作, 只有链接后才能确认运行地址).
? 例子包括两个不同的符号类型,程序自身实现的add/main函数存放在text段(由缩写 T 标明), 而未定义的引用printfs/puts由 U 标明. 未定义的引用没有符号值.
我们在用nm工具看看main. 在可执行文件中还会出现更多符号. 但由于大多数都是编译器自动生成的, 供运行时系统内部使用, 以下例子只给出了同时出现在目标文件中的符号:
? 左侧列出的是函数运行期间的地址.
? printf/puts 仍然是未定义的, 这说明链接期间采用的是动态链接, 但同时增加了一些信息, 表明能够提供函数的GNU标准库的名称和最低版本.
readelf -s / objdump -t 可以提供类似的信息, 你可以使用readelf -s main.o 和 readelf -s main来查看, 这里不赘述.
ELF是如何实现符号表机制的?以下3个section用于容纳相关的数据。
? .symtab 确定符号的名称与其值之间的关联。但符号的名称不是直接以字符串形式出现的,而是表示为某个字符串数组的索引
? .strtab 保存了字符串数组
? .hash 保存了一个散列表,以帮助快速查找符号
更多细节这里就不多说了, 有兴趣可以上网深究.
这里想表达的重点是:
由于符合表这个section的存在, 使得我们可以通过地址定位到函数! 当程序崩溃时, 我们一般可以得到崩溃时PC指针所指向的地址, 通过这个地址, 我们可以使用addr2line/nm/odjdump得到对应的函数名. 具体方法后文介绍.
但是依旧存在一个缺点, 我们只能得到函数名, 但无法知道这个函数是在哪个c文件中的哪一行. 如果系统中有多个c文件, 情况就会很糟糕.
例如内核发生崩溃, 然后你定位到崩溃的函数名, 然后要在茫茫代码中去找到对应的c文件, 也是非常耗时的.
解决的办法是有的, gcc编译时加上-g选项即可. 详情见4.1.4节.
参考《深入Linux内核架构 附录E》, 暂时不打算细写, 有时间在补充
《4.1.2 符号表》一节的最后解释了为什么要用-g选项, 建议先回头看看《符号表》一节.
当我们使用gcc -g命令编译代码时, 会在ELF文件里面增加5个sections, 它们分别是: .debug_aranges, .debug_info, .debug_abbrev, .debug_line, .debug_str.
你可以分别用gcc -o main main.c 和 gcc -g -o main-g main.c编译4.1.1节的实例代码, 然后用readelf -S main 和 readelf -S main-g对比两者的sections.
多出来的5个sections中, .debug_line这个section里面存储就是函数”运行期间的地址”与”函数所在的文件路径和行数”之间的对应关系.
你可以用readelf -wl main-g查看.debug_line这个section的详细内容. (readelf --debug-dump可以查看5个section的全部内容).
有了.debug_line这个section, 我们就可以通过程序出错的地址轻松定位到出错时所在的源文件路径以及行数. 在配合”符号表”这个section定位出的函数名, 我们基本上能锁定出错的代码了.
strip命令可以删除上述5个sections, 例如:
strip main-g
经过这个处理后, main-g就和main一模一样了!这个命令的一个用途是我们在调试期间可以用-g编译代码, 发布镜像时用strip删除镜像中的.debug_xxx sections.
-g 是一个编译选项, -rdynamic 却是一个连接选项, 它将指示连接器把所有符号(而不仅仅只是程序已使用到的外部符号)都添加到动态符号表(即.dynsym表)里, 以便那些类似 dlopen() 或 backtrace() (这一系列函数使用.dynsym表内符号)这样的函数使用.
-rdynamic的详细信息请参考 : http://www.cnblogs.com/LiuYanYGZ/p/5550544.html
时间关系这里不深入看了, 注意strip无法删除.dynsym这个section里面的内容!
错误定位方法:
addr2line -fe main 40057f : main是可执行文件, 后面跟的是出错时PC寄存器的值.
? 可执行程序可以是用户空间的某个程序, 也可以是内核的vmlinux
? 如果编译时加了-g选项, 则上述命令会显示出错的函数名以及函数所在的文件位置和行数. 否则只能显示函数名.
? 注意必须使用与编译器对应的addr2line, 例如如果gcc是$CROSS_COMPILE-gcc, 则需要使用$CROSS_COMPILE-addr2line
? addr2line --help可以查看它的更多选项
objdump命令是Linux下的反汇编目标文件或者可执行文件的命令, 它还有其他作用, 下面以ELF格式可执行文件test为例详细介绍:
? objdump -f test
显示test的文件头信息
? objdump -d test
反汇编test中的需要执行指令的那些section
? objdump -D test
与-d类似,但反汇编test中的所有section
? objdump -h test
显示test的Section Header信息
? objdump -x test
显示test的全部Header信息
? objdump -s test
除了显示test的全部Header信息,还显示他们对应的十六进制文件代码
如何对任意一个二进制文件进行反汇编?
我们可以这样做:
objdump -D -b binary -m i386 a.bin
-D表示对全部文件进行反汇编, -b表示二进制, -m表示指令集架构, a.bin就是我们要反汇编的二进制文件.
objdump -m可以查看更多支持的指令集架构, 如i386:x86-64,i8086等.
同时我们也可以指定big-endian或little-endian(-EB或-EL), 我们可以指定从某一个位置开始反汇编等. 所以objdump命令是非常强大的!
同样, 注意使用与编译器对应的objdump.
ldd是用来分析程序运行时需要依赖的动态库的工具; nm是用来查看指定程序中的符号表相关内容的工具. 可以通过--help查看更多信息.
借助《4.1.1实例代码》, 看看两者的输出:
ldd main
在上面的例子中, ldd的结果可以分为三列来看:
? 第一列:程序需要依赖什么库
? 第二列:系统提供的与程序需要的库所对应的库
? 第三列:库加载的开始地址
通过上面的信息,我们可以得到以下几个信息
? 通过对比第一列和第二列, 我们可以分析程序需要依赖的库和系统实际提供的, 是否相匹配
? 通过观察第三列, 我们可以知道在当前的库中的符号在对应的进程的地址空间中的开始位置
nm
内容的格式如下:
? 第一列:当前符号的地址
? 第二列:当前符号的类型(关于类型的说明,感兴趣的朋友可以man nm详阅)
? 第三列:当前符号的名称
nm对我们程序有啥具体的帮助呢, 我觉得主要有以下几个方面
? 判断指定程序中有没有定义指定的符号 (比较常用的方式:nm -C proc | grep symbol)
? 解决程序编译时undefined reference的错误, 以及mutiple definition的错误
? 查看某个符号的地址, 以及在进程空间的大概位置(bss, data, text区, 具体可以通过第二列的类型来判断)
参考《4.2 addr2line》, 使用addr2line -fe program pcaddress即可获取pc地址对应的代码位置.
objdump + addr2line
在一次系统死机的过程中, 我们得到如下的信息, 死机的模块是tejxapci.ko:
具体位置是: shtej_spanconfig+0x98/0x123
之后先使用
#objdump -S tejxapci.ko
得到shtej_spanconfig的地址0x248a, 加上0x98就是, 0x2522.
得到0x2522这个地址之后, 运行
#addr2line -fe tejxapci.ko 0x2522
就可以定位到对应的那一行代码了.
如果遇到objdump无法使用的情况, 可以使用如下command来check ELF格式的bin档中的symbol list
readelf -s output/debug/vmlinux | grep xxx
nm + objdump
错误信息显示 PC is at snd_soc_dai_set_sysclk+0x10/0x84
0x10: 表示出错的偏移位置; 0x84表示snd_soc_dai_set_sysclk函数的大小
先找到snd_soc_dai_set_sysclk函数的位置
然后objdump出此函数
接下来就去查看vim ~/temp/soc文件, 找到0xc04116bc+0x10的位置即可
Crash log示例
//寄存器堆栈信息
[ 2][ 18.572091] Pid: 678, comm: minismarthub
[ 2][ 18.577125] CPU: 2 Tainted: P O (3.8.13 #1)
[ 2][ 18.582943] PC is at 0x150ac58
[ 2][ 18.586367] LR is at 0xa96a3460
[ 2][ 18.589892] pc : [<0150ac58>] lr : [<a96a3460>] psr: 60000010
[ 2][ 18.589892] sp : 9aa7eb48 ip : 0150ac50 fp : 9aa7eb64
[ 2][ 18.602218] r10: 00000000 r9 : 00004ae0 r8 : 00000001
[ 2][ 18.607854] r7 : a055d478 r6 : a055d6a4 r5 : a96f85d4 r4 : 00000b64
[ 2][ 18.614815] r3 : 0000272b r2 : ffffffff r1 : 00000001 r0 : 00000b64
//dump map 信息
[ 2][ 18.747704] -----------------------------------------------------------
[ 2][ 18.754763] * dump maps on pid (678 - minismarthub)
[ 2][ 18.760043] -----------------------------------------------------------
......
[ 2][ 21.700779] a963f000-a977c000 r-xp 00000000 b3:0f 495 /mtd_rwarea/libMiniSmartHubApp.so // libMiniSmartHubApp.so代码所分配的地址,注意起始地址a963f000
......
addr2line -Cfe exeAPP 0xa96a3460 // exeAPP 是可执行程序
??
??:0
这里出现了一个问题, 0xa96a3460对应的函数是某个so里面的函数, 所以直接用addr2line无法打印出函数名. 原因是so里面的地址在可执行文件装载的时候, 是可以被 reallocate的, 这个地址是动态的, 无法在编译期间确定, 因此直接使用运行期地址没用.
如何解决? 使用addr2line或者gdb都可以, 下文分别描述.
addr2line
思路很简单, 将运行期地址转换为so内部的偏移量, 然后就可以用addr2line了.
例如, 由”dump map信息”可知, libMiniSmartHubApp.so被装载到了a963f000-a977c000之间, 0xa96a3460这个地址位于区间内, 因此说明它是libMiniSmartHubApp.so里面的某个函数. 0xa96a3460 - a963f000 = 0x64460 就是该函数在so内部的偏移量.
addr2line -Cfe libMiniSmartHubApp.so 0x64460
CMNMiniHubView::StartViewTimer()
/home/sundh/GolfP/AP_MM/AP_MiniSmartHub/Src/MNMiniHubView.cpp:1689
就能得到函数名.
gdb
如果想使用gdb, 只需要敲下面的命令, 让gdb装载so.
(gdb) set solib-search-path /path/to/the/so
然后就可以使用gdb命令正常调试了, 例如
(gdb) bt
oops 显示发生错误时处理器的状态,包括 CPU 寄存器的内容、页描述符表的位置,以及其一些难理解的信息。这些消息由失效处理函数(arch/*/kernel/traps.c)中的printk 语句产生。较为重要的信息就是指令指针(EIP),即出错指令的地址。
这种问题的定位一般会使能内核中的DEBUG_INFO = Y (也就是打开-g选项), 然后重新编译运行内核, 然后根据出错地址, 用上述反汇编方式定位出错的代码位置.
[Note add @ 1018-10-19] 现在出了一个更新的调试器LLDB, 大有替代gdb的趋势.
从宏观上说, gdb类的调试工具的运行模式可分为本地模式和远程模式:本地模式是指gdb和被调试程序在同一个机器上运行; 远程模式是指gdb和被调试程序在不同的机器上运行, 两者通过UART或Ethernet通信.
从调试手段的角度来说,可分为静态方法和动态方法:
静态方法下, gdb可以
? 进行一些反汇编的事情, 例如《5.1.3 gdb反汇编》
? 分析coredump文件, 例如《5.1.4 gdb分析coredump》
动态方法下, gdb可以
? 跟踪调试一个全新的程序, 可以单步运行、设置断点、打印变量值等等, 就像以前用MDK调试裸机程序一样, 参见《5.1.2 gdb本地调试》
? attach到一个已运行的程序上并调试它 (后续补充)
不管是哪种方法, 都离不开gdb命令, 因此《5.1.1 gdb命令》一节首先介绍gdb的一些命令.
liuxin@ubuntu:~/gcc-g$ gdb : 进入gdb调试
(gdb) help : 列出所有命令的大类
(gdb) help stack : 查看某个大类下的具体命令, 例如查看stack这个大类.
注意gdb支持tab补全!
常用的gdb命令如下:
? l(list) ,显示源代码,并且可以看到对应的行号;
? b <行号>
b <函数名称>
b *<函数名称>
b *<代码地址>
Breakpoint的简写, 设置断点. 两可以使用“行号”“函数名称”“执行地址”等方式指定断点位置.
其中在函数名称前面加“*”符号将断点设置在“由编译器生成的prolog代码处”, 实际的效果就是在进入函数前先停住.
任不明白的话, 可以用4.1.1节的小程序, 对比 b add 和b *add的区别.
? d [编号] : Delete breakpoint的简写, 删除指定编号的某个断点, 或删除所有断点. 断点编号从1开始递增.
? p(print)x, x是变量名,表示打印变量x的值
? r(run), 表示继续执行到断点的位置
? n(next),表示执行下一步
? c(continue),表示继续执行
? q(quit),表示退出gdb
这里给出一个简单的例子, 以便熟悉gdb调试的基本方法.
在任意linux机器上都可以尝试, 首先下载编译程序:
git clone https://gitlab.com/study-kernel/debugging_techniques/gdb.git
cd gdb && make
然后运行gdb调试:
start运行到main入口:
next单步执行:
bt打印调用栈:
通过异常地址定位代码位置:
假设内核打印了如下错误. 例如:
Call Trace: [<800a73b8>] do_vfs_ioctl+0x88/0x5c8
如果内核在编译时使能了CONFIG_DEBUG_INFO, 我们就可以用以下命令尝试定位
# gdb vmlinux
(gdb) list *(0x800a73b8)
(gdb) list *(do_vfs_ioctl+0x88)
上述这两种方式都行.
其它情况后续补充
http://blog.csdn.net/tenfyguo/article/details/8159176/
我们经常听到大家说到程序core掉了, 需要定位解决, 这里说的大部分是指对于应用程序,由于各种异常或者bug导致在运行过程中异常退出或者中止, 并且在满足一定条件下(这里为什么说需要满足一定的条件呢?下面会分析)会产生一个叫做core的文件.
通常情况下, core文件会包含了程序运行时的内存, 寄存器状态, 堆栈指针, 内存管理信息还有各种函数调用堆栈信息等, 我们可以理解为是程序工作当前状态存储生成第一个文件. 许多的程序出错的时候都会产生一个core文件, 通过工具分析这个文件, 我们可以定位到程序异常退出的时候对应的堆栈调用等信息, 找出问题所在并进行及时解决.
core文件默认的存储位置与对应的可执行程序在同一目录下, 文件名是core, 大家可以通过下面的命令看到core文件的存在位置:
cat /proc/sys/kernel/core_pattern
缺省值是core
注意: 这里是指在进程当前工作目录的下创建. 通常与程序在相同的路径下. 但如果程序中调用了chdir函数, 则有可能改变了当前工作目录. 这时core文件创建在chdir指定的路径下. 有好多程序崩溃了, 我们却找不到core文件放在什么位置, 和chdir函数就有关系. 当然程序崩溃了不一定都产生 core文件.
通过下面的命令可以更改coredump文件的存储位置, 若你希望把core文件生成到/data/coredump/core目录下:
echo "/data/coredump/core" > /proc/sys/kernel/core_pattern
缺省情况下, 内核在coredump时所产生的core文件放在与该程序相同的目录中, 并且文件名固定为core. 很显然, 如果有多个程序产生core文件, 或者同一个程序多次崩溃, 就会重复覆盖同一个core文件, 因此我们有必要对不同程序生成的core文件进行分别命名.
我们通过修改kernel的参数, 可以指定内核所生成的coredump文件的动态文件名. 例如, 使用下面的命令使kernel生成名字为core.filename.pid格式的core dump文件:
echo "/data/coredump/core.%e.%p" >/proc/sys/kernel/core_pattern
这样配置后, 产生的core文件中将带有崩溃的程序名、以及它的进程ID. 上面的%e和%p会被替换成程序文件名以及进程ID.
需要说明的是, 在内核中还有一个与coredump相关的设置, 就是/proc/sys/kernel/core_uses_pid. 如果这个文件的内容被配置成1, 那么即使core_pattern中没有设置%p, 最后生成的core dump文件名仍会加上进程ID.
可以在core_pattern模板中使用变量还很多, 见下面的列表:
%% |
单个%字符 |
%p |
所dump进程的进程ID |
%u |
所dump进程的实际用户ID |
%g |
所dump进程的实际组ID |
%s |
导致本次core dump的信号 |
%t |
core dump的时间 (由1970年1月1日计起的秒数) |
%h |
主机名 |
%e |
程序文件名 |
产生coredump的条件, 首先需要确认当前会话的ulimit -c, 若为0, 则不会产生对应的coredump, 需要进行修改和设置.
? ulimit -c unlimited (可以产生coredump且不受大小限制)
? ulimit -c 4 (ulimit –c [size] 可以设定大小, 这里的size的单位是blocks, 一般1block=512bytes. 笔者实测, 最小要设置为4及以上才可产生coredump文件)
1: 内存访问越界
? 由于使用错误的下标, 导致数组访问越界.
? 搜索字符串时, 依靠字符串结束符来判断字符串是否结束, 但是字符串没有正常的使用结束符.
? 使用strcpy, strcat, sprintf, strcmp, strcasecmp等字符串操作函数, 将目标字符串读/写爆. 应该使用strncpy, strlcpy, strncat, strlcat, snprintf, strncmp, strncasecmp等函数防止读写越界.
2: 多线程程序使用了线程不安全的函数
3: 多线程读写的数据未加锁保护
对于会被多个线程同时访问的全局数据, 应该注意加锁保护, 否则很容易造成coredump
4: 非法指针
? 使用空指针
? 随意使用指针转换.
一个指向一段内存的指针, 除非确定这段内存原先就分配为某种结构或类型, 或者这种结构或类型的数组, 否则不要将它转换为这种结构或类型的指针, 而应该将这段内存拷贝到一个这种结构或类型中, 再访问这个结构或类型. 这是因为如果这段内存的开始地址不是按照这种结构或类型对齐的, 那么访问它时就很容易因为bus error而core dump
? 堆栈溢出
不要使用大的局部变量(因为局部变量都分配在栈上), 这样容易造成堆栈溢出,破坏系统的栈和堆结构, 导致出现莫名其妙的错误
The core file will not be generated if
(a) the process was set-user-ID and the current user is not the owner of the program file, or
(b) the process was set-group-ID and the current user is not the group owner of the file,
(c) the user does not have permission to write in the current working directory,
(d) the file already exists and the user does not have permission to write to it, or
(e) the file is too big (recall the RLIMIT_CORE limit in Section 7.11). The permissions of the core file (assuming that the file doesn‘t already exist) are usually user-read and user-write, although Mac OS X sets only user-read.
在类unix系统下, coredump文件本身主要的格式也是ELF格式, 因此, 我们可以通过readelf命令进行判断.
也可以简单的通过file命令快速判断:
将其保存在BBB板子上, 进行如下步骤:
? git clone https://gitlab.com/study-kernel/debugging_techniques/gdb.git
? cd gdb && make
? ulimit -c 4
? ./main
将得到如下结果:
在当前目录下ls, 会看到core dump文件 : core
core dump相当于程序运行的某个时刻的快照. 通过core dump, gdb可以复现出出错时程序运行的情况.
假如没有core dump文件, gdb加载一个可执行程序时, 相当于处于此程序的”main()”处, 你得一步步运行至出错的地方. 当然, 这时的错误和core dump的错误可能就不一样了, 因为很多bug都是随机的.
所以, 最好是有core dump文件, 把崩溃时的尸体留下, 以便后面剖解分析!
如果得到了core dump文件(借助《core dump》中的示例, 假设dump出来的文件名为core, 与之对应的可执行程序为main), 则调试步骤如下:
gdb main core
? gdb一启动, 我们就能看到出错时的函数以及所在的文件及行数.
? bt命令能显示到出错位置的调用栈
当然这只是一个很简单的示例, 复杂的可能不好一眼看出来, 但可以用此思路慢慢调试.
gdb远程调试一般用于IDE上面. 例如Android官方的NDK, NDK提供了eclipse的一个插件, 用于编辑、编译代码; 另外NDK还提供了gdb和gdbserver, gdbserver在手机上运行, gdb在PC上配合eclipse运行, 这样可以提供给用户图形化的调试界面.
我们在平时的调试过程中, 如果场景需要, 也可以用远程模式, 下文展示远程调试必要的一些设置.
使用远程调试时, gcc、gdb、gdbserver这三者最好是配套的.
我们以BBB运行debian为例, 首先在PC上下载gcc编译器: https://releases.linaro.org/components/toolchain/binaries/5.3-2016.02/arm-linux-gnueabihf/, 根据PC的型号, 选择相应的压缩包(例如gcc-linaro-5.3-2016.02-x86_64_arm-linux-gnueabihf.tar.xz). 解压压缩包后, 进入到bin目录:
可以看到, gcc-linaro提供了我们需要的gcc、gdb和gdbserver, 它们是”配套”的.
然后在PC上下载并交叉编译程序:
? git clone https://gitlab.com/study-kernel/debugging_techniques/gdb.git
? export CROSS_COMPILE=/path/to/gcc-linaro-5.3-2016.02-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-
? cd gdb && make
? 将编译出来的main和压缩包里面的gdbserver弄到BBB的板子上
然后在BBB板子上运行./gdbserver --help, 看一下就大致知道怎么使用gdbserver了, 这里以网络通信方式为例:
192.168.2.110是PC的IP, 端口号1234随意取.
然后在PC上运行arm-linux-gnueabihf-gdb和target remote命令:
192.168.2.105是板子的IP, 端口号必须与gdbserver指定的端口号保持一致.
至此, PC和板子就已经通过网络建立起了远程调试了, 此时板子上的gdbserver会打印如下一句:
最后, 就可以在PC上运行list、continue等命令调试远端程序了.
[写在前面, 后面有新发现可删掉此段]
在自己的实验过程中, 发现kdb和kgdb在调试内核方面优势不是很明显, 主要原因是要进入调试状态必须停止内核运行, 而一旦内核被停止后, 想continue继续运行, 貌似无法成功.
目前唯一能想到有用的场景是当内核运行崩溃时, 例如出现oops/fault, 在使能kdb/kgdb的情况下, 内核会自动进入kdb调试状态, 此时可以查看下内存、寄存器、调用栈等.
也许还有更多有用场景自己没发现, 后面遇到实际问题了在来考虑是否能用kdb/kgdb解决
[END]
gdb是用于调试应用程序的, 对应的kdb和kgdb是用于调试内核的.
kdb类似于“本地模式的gdb”, 只需target这一台机器即可. 通过串口或者键盘即可使用它提供的调试命令, 这些命令包括查改内存、寄存器, 打印调用栈, 设置断点等等.
kgdb则类似于”远程模式的gdb”, 需要target和Host两台机器. kgdb运行在target的内核中, host(例如PC)运行gdb命令. target和host通过串口或者网络通信.
与kdb相比, kgdb的优势在于可以提供源码级别的调试. 另外kgdb中也可运行部分kdb的命令.
目前kdb和kgdb都已经merge到了内核主线, merge历史参见https://kgdb.wiki.kernel.org/index.php/Main_Page#Quick_History.
不过主线内核只支持通过串口连接gdb和kgdb, 因为作者觉得通过网络方式存在一些问题, 参考: What_is_the_status_of_kgdboe_and_the_mainline. 如果想自己尝试网络通信方式, 可参考《RefLink》中第三方提供的方法.
虽然kdb和kgdb是两种调试方法, 但两者可以相互切换, 而且使能KDB必须使能KGDB, 因此建议在内核中同时使能kdb和kgdb, 如下:
CONFIG_KGDB=y |
加入KGDB支持 |
CONFIG_KGDB_SERIAL_CONSOLE=y |
使KGDB通过串口与主机通信(打开这个选项,默认会打开CONFIG_CONSOLE_POLL和CONFIG_MAGIC_SYSRQ) |
CONFIG_KGDB_KDB=y |
加入KDB支持 |
CONFIG_DEBUG_RODATA=n |
关闭这个,能在只读区域设置断点 |
CONFIG_FRAME_POINTER=y |
使KDB能够打印更多的栈信息 |
|
|
CONFIG_KALLSYMS |
加入符号信息 |
CONFIG_KDB_KEYBOARD=y |
如果是通过目标版的键盘与KDB通信,需要把这个打开,且键盘不能是USB接口 |
CONFIG_DEBUG_KERNEL=y |
包含驱动调试信息 |
CONFIG_DEBUG_INFO=y |
使内核包含基本调试信息 |
另外, BBB的板子默认已经开启了上述选项了.
kdb可以通过bootargs开启, 也可以在内核启动完毕之后通过sysfs开启.
可在bootargs里面添加如下内容:
kgdboc=[kms][[,]kbd][[,]serial_device][,baud]
? kgdboc的意思是"kgdb over console"
? kms = Kernel Mode Setting, 主要目的是设置显示系统为临时console. 一般不用它, 暂不关注
? kbd = Keyboard
? serial_device和baud代表要使用的串口和波特率
举个例子, 我们在BBB板子上是这样配置的: kgdboc=ttyS0,115200
另外还有一个参数要说明一下: kgdbwait
它的意思是在内核完成一些必要的初始化后, 就进入kdb调试状态, 可以用于调试内核无法正常启动的情境. 典型的用法如: bootargs=XXX kgdboc=ttyS0,115200 kgdbwait
当我们在BBB的板子上使用kgdboc和kgdbwait时, 可以看到如下输出:
此时就可以用kdb调试了, 输入help可以查询kdb提供的各种命令.
内核启动后, 可通过如下方式开启/禁止kdb:
Enable kgdboc on ttyS0 : echo ttyS0 > /sys/module/kgdboc/parameters/kgdboc
Disable kgdboc : echo "" > /sys/module/kgdboc/parameters/kgdboc
NOTE: 此时无需指定波特率, 因为在内核启动过程中已经配置好了.
kdb/kgdb开启后要让内核停止运行才能进入调试状态, 有多种方式可以停止内核: 使用kgdbwait as a boot argument、使用sysre-g、或者运行内核直到出现exception, 例如oops or fault.
通过sysrq-g的方式如下:
echo g > /proc/sysrq-trigger
BBB板子上的运行效果如下:
开启kgdb要求先在target上进入kdb调试状态, 然后输入命令”kgdb”切换到kgdb调试模式:
Note: 输入”$3#33”可以从kgdb模式切换到kdb模式.
下一步就需要在host上运行gdb并通过串口与target建立连接, 如下:
arm-linux-gnueabihf-gdb ./vmlinux
(gdb) target remote /dev/ttyUSB0
建立连接后, 你就可以像调试普通应用程序一样来调试内核了.
官方wiki: https://kgdb.wiki.kernel.org/index.php/Main_Page
官方文档: https://www.kernel.org/pub/linux/kernel/people/jwessel/kdb/
? Running kdb commands from gdb
? The kgdbcon feature allows you to see printk() messages inside gdb while gdb is connected to the kernel
kgdboe: http://sysprogs.com/VisualKernel//kgdboe/
samsung上使用kgdb的示例 : http://www.cnblogs.com/Ph-one/p/6432717.html
LLDB是llvm推出的新一代调试器, 用来取代gdb, 其官网 : http://lldb.llvm.org/.
LLDB和GDB提供的功能对比参见 : http://lldb.llvm.org/lldb-gdb.html.
关于如何使用LLDB, 可参考官方提供的教程 : http://lldb.llvm.org/tutorial.html
探针技术是指在不改动原始代码的基础上, 刺探代码的运行状态, 就像用万用表检查电路一样.
Kprobes技术用于内核层.
kprobe是一个动态地收集调试和性能信息的工具,它从Dprobe项目派生而来,是一种非破坏性工具,用户用它几乎可以跟踪任何函数或被执行的指令以及一些异步事件(如timer)。它的基本工作流程是:用户指定一个探测点,并把一个用户定义的处理函数关联到该探测点,当内核执行到该探测点时,相应的关联函数被执行,然后继续执行正常的代码路径。
kprobe实现了三种类型的探测点: kprobes, jprobes和kretprobes (也叫返回探测点)。
? kprobes是可以被插入到内核的任何指令位置的探测点
? jprobes则只能被插入到一个内核函数的入口
? kretprobes则是在指定的内核函数返回时才被执行
一般,使用kprobe的程序实现作一个内核模块,模块的初始化函数来负责安装探测点,退出函数卸载那些被安装的探测点。kprobe提供了接口函数(APIs)来安装或卸载探测点。
kprobes的本质是利用CPU的断点指令: 386和x86_64有专门的断点指令int3, 当执行该指令时会触发一个断点中断从而执行相应的处理函数; ARM体系架构没有专门的断点指令, 所以用未定义指令来模拟断点指令.
当安装一个kprobes探测点时,kprobe首先备份被探测的指令,然后使用断点指令(即在i386和x86_64的int3指令)来取代被探测指令的头一个或几个字节。当CPU执行到探测点时,将因运行断点指令而执行trap操作,那将导致保存CPU的寄存器,调用相应的trap处理函数,而trap处理函数将调用相应的notifier_call_chain(内核中一种异步工作机制)中注册的所有notifier函数,kprobe正是通过向trap对应的notifier_call_chain注册关联到探测点的处理函数来实现探测处理的。
当kprobe注册的notifier被执行时,它首先执行关联到探测点的pre_handler函数,并把相应的kprobe struct和保存的寄存器作为该函数的参数,接着,kprobe设置CPU为单步模式, 并单步执行被探测指令的备份, 单步模式会使得CPU运行完一条指令后再次触发断点中断, 在中断处理函数中, kprobe执行post_handler.
等所有这些运行完毕后, 恢复CPU运行模式, 然后紧跟在被探测指令后的指令流将被正常执行。
在ARM体系架构中, 用未定义指令替代int3, 当执行到该未定义指令时, 会触发相应的中断, 对应的中断处理函数是do_undefinstr, do_undefinstr进一步调用call_undef_hook, 继而调用kprobes_arm_break_hook->fn, 继而执行kprobe_handler.
jprobe能无缝地访问被探测函数的参数。jprobe处理函数应当和被探测函数有同样的原型,而且该处理函数在函数末尾必须调用kprobe提供的函数jprobe_return()。
jprobe依赖kprobe, 相当于利用kprobe设置一个断点, 断点位置是被探测函数的入口,当执行到该断点时,kprobe备份CPU寄存器和栈的一些部分,然后修改指令寄存器指向jprobe处理函数,当执行该jprobe处理函数时,寄存器和栈内容与执行真正的被探测函数一模一样,因此它不需要任何特别的处理就能访问函数参数, 在该处理函数执行到最后时,它调用jprobe_return(),那将导致寄存器和栈恢复到执行探测点时的状态,因而被探测函数能被正常运行。需要注意,被探测函数的参数可能通过栈传递,也可能通过寄存器传递,但是jprobe对于两种情况都能工作,因为它既备份了栈,又备份了寄存器,当然,前提是jprobe处理函数原型必须与被探测函数完全一样。
kretprobe也使用了kprobes来实现,当用户调用register_kretprobe()时,kprobe在被探测函数的入口建立了一个探测点,当执行到探测点时,kprobe保存了被探测函数的返回地址并取代返回地址为一个trampoline的地址,kprobe在初始化时定义了该trampoline并且为该trampoline注册了一个kprobe,当被探测函数执行它的返回指令时,控制传递到该trampoline,因此kprobe已经注册的对应于trampoline的处理函数将被执行,而该处理函数会调用用户关联到该kretprobe上的处理函数,处理完毕后,设置指令寄存器指向已经备份的函数返回地址,因而原来的函数返回被正常执行。
被探测函数的返回地址保存在类型为kretprobe_instance的变量中,结构kretprobe的maxactive字段指定了被探测函数可以被同时探测的实例数,函数register_kretprobe()将预分配指定数量的kretprobe_instance。如果被探测函数是非递归的并且调用时已经保持了自旋锁(spinlock),那么maxactive为1就足够了; 如果被探测函数是非递归的且运行时是抢占失效的,那么maxactive为NR_CPUS就可以了;如果maxactive被设置为小于等于0, 它被设置到缺省值(如果抢占使能, 即配置了 CONFIG_PREEMPT,缺省值为10和2*NR_CPUS中的最大值,否则缺省值为NR_CPUS)。
如果maxactive被设置的太小了,一些探测点的执行可能被丢失,但是不影响系统的正常运行,在结构kretprobe中nmissed字段将记录被丢失的探测点执行数,它在返回探测点被注册时设置为0,每次当执行探测函数而没有kretprobe_instance可用时,它就加1。
头文件 : include/linux/kprobes.h
struct kprobe |
Comment |
struct hlist_node hlist |
所有注册的kprobe都会添加到kprobe_table哈希表中, hlist成员用来链接到某个槽位中 |
struct list_head list |
如果在同一个位置注册了多个kprobe, 这些kprobe会形成一个队列,队首是一个特殊的kprobe实例, list成员用来用来链接到这个队列中. 当探测点被触发时, 队首的kprobe实例中注册的handler会逐个遍历队列中注册的handler |
unsigned long nmissed |
记录当前的probe没有被处理的次数 |
kprobe_opcode_t *addr |
这个成员有两个作用: 一个是用户在注册前指定探测点的基地址(加上偏移得到真实的地址), 基地址可以通过符号名在System.map中查询. 另一个是在注册后保存探测点的实际地址.
在注册前, 这个可以不指定, 由kprobes来初始化. 如果没有指定, 则必须指定探测的位置的符号信息(symbol_name), 例如函数名 |
const char *symbol_name |
探测点的符号名称. 可从System.map中获取到符号名. 名称和地址不能同时指定, 否则注册时会返回EINVAL错误. |
unsigned int offset |
探测点相对于addr地址的偏移. addr/symbol_name只能精确到函数名, 配合offset, 就能在函数内部的任意位置设置断点了 |
kprobe_pre_handler_t pre_handler |
这个接口在断点异常触发之后, 开始单步执行原始的指令之前被调用 |
kprobe_post_handler_t post_handler |
在单步执行原始的指令后会被调用 |
kprobe_fault_handler_t fault_handler |
指定错误处理函数, 当在执行pre_handler、post_handler以及被探测函数期间发生错误时, 它会被调用 |
kprobe_break_handler_t break_handler |
在调用probe的处理函数(比如pre_handler接口)时触发了断点异常会调用该接口. 断点异常是通过中断门来处理的, 在调用相应的处理函数前会自动关闭中断. 关中断的情况下虽然不会接收可屏蔽的中断, 但是CPU引发的异常或者NMI还是会接收到, 所以有可能会发生断点异常处理嵌套, jprobes的实现就用到了这点. |
kprobe_opcode_t opcode |
原始指令, 在被替换为断点指令(X86下是int 3指令)前保存 |
struct arch_specific_insn ainsn |
保存了探测点原始指令的拷贝. 这里拷贝的指令要比opcode中存储的指令多, 拷贝的大小为MAX_INSN_SIZE * sizeof(kprobe_opcode_t) |
u32 flags |
探测点的标志, 可取的值为KPROBE_FLAG_GONE和?KPROBE_FLAG_DISABLED. 如果设置了KPROBE_FLAG_GONE标志, 表示断点指令被移除; 如果设置了KPROBE_FLAG_DISABLED, 则表示只注册probe, 但是并不启用它, 也就是说在断点异常触发时并不会调用该probe的接口 |
API
API kprobe |
Comment |
int register_kprobe(struct kprobe *p) |
该函数的参数是struct kprobe类型的指针 在调用该注册函数前, 用户必须先设置好struct kprobe的相应字段, kprobe的3个处理函数不一定要全部定义, 任何处理函数都可设为NULL
该函数成功时返回0, 否则返回负的错误码 |
void unregister_kprobe(struct kprobe *p) |
注销断点 |
int register_kprobes(struct kprobe **kps, int num) |
一次注册多个kprobe |
void unregister_kprobes(struct kprobe **kps, int num) |
一次注销多个kprobe |
头文件 : include/linux/kprobes.h
struct jprobe |
Comment |
struct kprobe kp |
前文介绍的kprobe |
void *entry |
在注册jprobe前, 需要定义一个struct jprobe类型的变量并设置它的kp.addr和entry字段. kp.addr指定探测点的位置, 它必须是被探测函数的第一条指令的地址. entry指定探测点的处理函数, 该处理函数的参数表和返回类型应当与被探测函数完全相同, 而且它必须正好在返回前调用jprobe_return(). 如果被探测函数被声明为asmlinkage、fastcall或影响参数传递的任何其他形式, 那么相应的处理函数也必须声明为相应的形式. |
API
API jprobe |
Comment |
int register_jprobe(struct jprobe *p) |
该注册函数在jp->kp.addr注册一个jprobes类型的探测点, 当内核运行到该探测点时, jp->entry指定的函数会被执行. 如果成功, 该函数返回0, 否则返回负的错误码. |
void unregister_jprobe(struct jprobe *p) |
注销jprobe |
int register_jprobes(struct jprobe **jps, int num) |
注册多个jprobe |
void unregister_jprobes(struct jprobe **jps, int num) |
注销多个jprobe |
头文件 : include/linux/kprobes.h
struct kretprobe |
Comment |
struct kprobe kp |
前文介绍的kprobe |
kretprobe_handler_t handler |
用户在注册kretprobe前必须定义一个struct kretprobe的变量并设置它的kp.addr、handler以及maxactive字段. kp.addr指定探测点的位置 当被探测函数返回前, 会执行handler函数 |
kretprobe_handler_t entry_handler |
当执行到被探测函数的入口时, 会执行entry_handler函数 |
int maxactive |
maxactive指定可以同时运行的最大处理函数实例数, 它应当被恰当设置, 否则可能丢失探测点的某些运行. 详见《4.10.2 Kretprobes》 |
int nmissed |
记录被丢失的探测点执行数 |
size_t data_size |
|
struct hlist_head free_instances |
|
raw_spinlock_t lock |
|
struct kretprobe_instance |
Comment |
struct hlist_node hlist |
|
struct kretprobe *rp |
指向相应的kretprobe |
kprobe_opcode_t *ret_addr |
表示返回地址 |
struct task_struct *task |
|
char data[0] |
|
API
API Kretprobe |
Comment |
int register_kretprobe(struct kretprobe *rp) |
如果成功, 该函数返回0, 否则返回负的错误码. |
kprobe允许在同一地址注册多个kprobes, 但是不能同时在该地址上有多个jprobes.
通常, 用户可以在内核的任何位置注册探测点, 特别是可以对中断处理函数注册探测点, 但是也有一些例外. 如果用户尝试在实现kprobe的代码(包括kernel/kprobes.c和arch/*/kernel/kprobes.c以及do_page_fault和notifier_call_chain)中注册探测点, register_*probe将返回-EINVAL.
如果为一个内联(inline)函数注册探测点, kprobe无法保证对该函数的所有实例都注册探测点,因为gcc可能隐式地内联一个函数. 因此, 要记住, 用户可能看不到预期的探测点的执行.
一个探测点处理函数能够修改被探测函数的上下文, 如修改内核数据结构, 寄存器等. 因此, kprobe可以用来安装bug解决代码或注入一些错误或测试代码.
如果一个探测处理函数调用了另一个探测点, 该探测点的处理函数不将运行, 但是它的nmissed数将加1. 多个探测点处理函数或同一处理函数的多个实例能够在不同的CPU上同时运行.
除了注册和卸载, kprobe不会使用mutexe或分配内存.
探测点处理函数在运行时是失效抢占的, 依赖于特定的架构, 探测点处理函数运行时也可能是中断失效的. 因此, 对于任何探测点处理函数, 不要使用导致睡眠或进程调度的任何内核函数(如尝试获得semaphore)
kretprobe是通过取代返回地址为预定义的trampoline的地址来实现的, 因此栈回溯和gcc内嵌函数__builtin_return_address()调用将返回trampoline的地址而不是真正的被探测函数的返回地址.
如果一个函数的调用次数与它的返回次数不相同, 那么在该函数上注册的kretprobe探测点可能产生无法预料的结果(do_exit()就是一个典型的例子, 但do_execve() 和 do_fork()没有问题)
当进入或退出一个函数时, 如果CPU正运行在一个非当前任务所有的栈上, 那么该函数的kretprobe探测可能产生无法预料的结果, 因此kprobe并不支持在x86_64上对__switch_to()的返回探, 如果用户对它注册探测点, 注册函数将返回-EINVAL.
https://gitlab.com/study-kernel/debugging_techniques/kprobes
README里面有示例的使用说明.
Ptrace技术用于用户空间.
ptrace()是一个系统调用, 它的核心是允许一个进程(跟踪进程)修改另外一个进程(被跟踪进程)的内存和寄存器, 从而改变被跟踪进程的行为.
注意: ptrace()是高度依赖于底层硬件的.使用ptrace的程序通常不容易在个钟体系结构间移植.
Android bionic中, ptrace的原型如下 (Linux下glibc也有类似的实现, ptrace在普通linux系统上也可使用): http://androidxref.com/7.1.1_r6/xref/bionic/libc/bionic/ptrace.cpp
#include <sys/ptrace.h>
extern "C" long __ptrace(int req, pid_t pid, void* addr, void* data);
我们可以看到ptrace有4个参数:
? req决定ptrace做什么
? pid是被跟踪进程的ID
? data存储从进程空间偏移量为addr的地方开始将被读取/写入的数据
这里面最重要的是req参数, 下面我们看看req可以取哪些值以及对应的意义: https://linux.die.net/man/2/ptrace
[Note] 本打算在此用表格详细说明每个req参数的意义, 但由于没有实际动手试过ptrace, 因此这里也不做过多说明, 细节可以参数上述链接. 后续有需要可补充说明.
函数hook的具体场景是 : 假设有进程A, A调用了libc.so这个库提供的func1函数. 假设我们想修改func1的实现, 该怎么办? 其中一种方式就是直接修改libc中func1的代码, 然后重新编译libc.so, 这样做的坏处是会改动”官方代码”. 另外一种方式是我们自己实现一个函数func2, 把这个函数编译成libx.so, 然后利用ptrace技术, 在运行期间把进程A中的func1替换成libx.so的func2.
要实现上述场景, 需要两个步骤:
第一步, 需要把libx.so注入到进程A的地址空间. 参考 : http://www.cnblogs.com/jiayy/p/4283766.html
第二步, 需要获取func1在进程A中的内存地址, 然后利用ptrace修改此地址, 把func1替换为func2, 参考 : http://www.cnblogs.com/jiayy/p/4283990.html
[Note]由于没有实际动手试过ptrace, 因此这里也不做过多说明, 细节可以参数上述链接. 后续有需要可补充说明.
断点, 分为软件断点和硬件断点.
软件断点的原理一般是修改RAM, 先备份被监测地址, 然后将该地址的内容替换为某个特征值. 当代码运行到此处时会触发异常, 在异常处理里面运行之前备份的内容.
硬件断点依赖具体的硬件CPU, 原理一般是往CPU的某寄存器写入被监测地址, CPU在运行过程中会与该寄存器进行比较, 当地址相等时会触发异常. 在异常处理中我们可以做想做的事情, 例如dump stack来看看是谁再访问这个地址, 该特性可用于调试踩内存问题.
这里详细介绍了它们的区别和优缺点.
http://www.360doc.com/content/17/0823/11/17136639_681458868.shtml
Another link : http://processors.wiki.ti.com/index.php/Data_Breakpoint/Watchpoint#Difference_between_Software_and_Hardware_Breakpoints
首先, 硬件上需要支持硬件断点功能, 一般x86, x64, ARM64, ARM(好像是A8, A9, A15以后)都支持.
其次, 内核在2.6.37后添加了硬件断点子系统:
? 对上, 提供了register_user_hw_breakpoint和register_wide_hw_breakpoint, 分别供用户空间和内核空间设置硬件断点.
? 对下, arch/xxx/kernel/hw_breakpoint.c实现了访问各个体系架构上的硬件寄存器的方法.
最后, 从使用的角度:
? samples/hw_breakpoint/data_breakpoint.c展示了如何在内核中设置硬件断点.
? arch/xxx/kernel/ptrace.c则向用户空间提供接口, 使得我们可以在用户空间设置硬件断点. (后文demo展示了如何在用户空间通ptrace设置硬件断点).
当断点设置成功后, 一旦条件匹配, 硬件就会产生异常, 内核就会产生中断, 然后在中断处理函数里面回调注册的断点处理函数.
在内核层, 我们就可以在断点处理函数里面做自己想做的事情.
那用户空间如何得知异常产生了? 断点处理函数会给进程发一个SIGTRAP信号(例如ptrace_hbptriggered), 用户进程捕获这个信号即可.
各个体系架构的实现略有不同, 但原理都是一样的, 详见硬件断点子系统.
请阅读README文档以了解如何编译和使用该Demo.
https://gitlab.com/study-android/memory_related/ptrace_hw_watchpoint
如果你想要在linux下调调kernel, 抓抓程序的性能, 那么首先想到的可能是 OProfile 和 Linux Perf. 但是显然, 开源有一个非常显著地你无法回避的特点, 就是你会有太多的选择: perf, oprofile, systemtap, dtrace4linux, lttng, kgtp, ktap, sysdig, ftrace, eBPF. 是不是已经眼花了? 那么你不能错过这篇文章:
http://www.brendangregg.com/blog/2015-07-08/choosing-a-linux-tracer.html
作者非常细心的列出了大量的工具原理及使用教程
http://www.brendangregg.com/linuxperf.html , 上图非常全面的显示了Linux的各个模块可以使用的分析工具, 点击网址获取详细信息. (Brendan Gregg是Netflix的高级性能架构师,他在那里做大规模计算机性能设计、分析和调优。此外,他还是《Systems Performance》等技术书的作者,曾获得过2013年USENIX LISA大奖! 他的github地址 : https://github.com/brendangregg).
strace主要用于进程执行时的系统调用和所接收的信号. 更多使用说明请参考: http://www.cnblogs.com/ggjucheng/archive/2012/01/08/2316692.html
ftrace主要用于跟踪内核的运行时行为, 包括如下功能:
? Function tracer 和 Function graph tracer: 跟踪函数调用。
? Schedule switch tracer: 跟踪进程调度情况。
? Wakeup tracer:跟踪进程的调度延迟,即高优先级进程从进入 ready 状态到获得 CPU 的延迟时间。该 tracer 只针对实时进程。
? Irqsoff tracer:当中断被禁止时,系统无法相应外部事件,比如键盘和鼠标,时钟也无法产生 tick 中断。这意味着系统响应延迟,irqsoff 这个 tracer 能够跟踪并记录内核中哪些函数禁止了中断,对于其中中断禁止时间最长的,irqsoff 将在 log 文件的第一行标示出来,从而使开发人员可以迅速定位造成响应延迟的罪魁祸首。
? Preemptoff tracer:和前一个 tracer 类似,preemptoff tracer 跟踪并记录禁止内核抢占的函数,并清晰地显示出禁止抢占时间最长的内核函数。
? Preemptirqsoff tracer: 同上,跟踪和记录禁止中断或者禁止抢占的内核函数,以及禁止时间最长的函数。
? Branch tracer: 跟踪内核程序中的 likely/unlikely 分支预测命中率情况。 Branch tracer 能够记录这些分支语句有多少次预测成功。从而为优化程序提供线索。
? Hardware branch tracer:利用处理器的分支跟踪能力,实现硬件级别的指令跳转记录。在 x86 上,主要利用了 BTS 这个特性。
? Initcall tracer:记录系统在 boot 阶段所调用的 init call 。
? Mmiotrace tracer:记录 memory map IO 的相关信息。
? Power tracer:记录系统电源管理相关的信息。
? Sysprof tracer:缺省情况下,sysprof tracer 每隔 1 msec 对内核进行一次采样,记录函数调用和堆栈信息。
? Kernel memory tracer: 内存 tracer 主要用来跟踪 slab allocator 的分配情况。包括 kfree,kmem_cache_alloc 等 API 的调用情况,用户程序可以根据 tracer 收集到的信息分析内部碎片情况,找出内存分配最频繁的代码片断,等等。
? Workqueue statistical tracer:这是一个 statistic tracer,统计系统中所有的 workqueue 的工作情况,比如有多少个 work 被插入 workqueue,多少个已经被执行等。开发人员可以以此来决定具体的 workqueue 实现,比如是使用 single threaded workqueue 还是 per cpu workqueue.
? Event tracer: 跟踪系统事件,比如 timer,系统调用,中断等。
这里还没有列出所有的 tracer,ftrace 是目前非常活跃的开发领域,新的 tracer 将不断被加入内核。
ftrace简介介绍了ftrace的主要功能、使用方法、基本原理.
使用 ftrace 调试 Linux 内核则主要从menuconfig的角度讲述如何配置内核以使能ftrace.
? menuconfig中, CONFIG_MESSAGE_LOGLEVEL_DEFAULT用于配置不显示指明printk的loglevel时, 默认loglevel是多少
? menuconfig中, CONFIG_PRINTK_TIME用于配置是否使能显示时间, 同样可以通过/sys/module/printk/parameters/time来修改.
? printk_ratelimit API可以用于控制打印速度, /proc/sys/kernel/printk_ratelimit 和 /proc/sys/kernel/printk_ratelimit_burst可以控制printk_ratelimit的行为.
? menuconfig中, CONFIG_LOG_BUF_SHIFT用于配置log_buf的大小.
不论loglevel是什么, 数据都会被存储到log_buf
? bootargs里面, 可以通过”console=”来指定printk打印的信息被送到哪些console上
如果没有指定”console=”, 则会默认打印的第一个注册的console上
? 可以通修改/sys/module/printk/parameters/ignore_loglevel, 来忽略loglevel, 从而不管用什么loglevel, 都可以将信息显示到console上.
bootargs里面, 也可以通过” ignore_loglevel”来达到类似的效果.
? 可以通过/proc/sys/kernel/printk来修改console_loglevel的值, 从而控制哪些loglevel的信息可以显示在console上
同样可以通过bootargs中的”quiet/debug; loglevel=”来修改console_loglevel的值.
还可以通过dmesg -n xx来修改console_loglevel的值.
? 可以在用户空间通过dmesg命令读取log_buf中的数据, 默认情况下, 它会读出log_buf中的所有数据, 不做loglevel过滤.
? 在menuconfig中打开CONFIG_EARLY_PRINTK 和 CONFIG_DEBUG_LL
? 在bootargs中传递一个参数: bootargs="…. earlyprintk";
? 用汇编语言实现函数: addruart, waituart, senduart, busyuart这几个函数, 利用UART打印信息
? 在menuconfig中, 打开CONFIG_DEBUG_LL
? 用汇编语言实现函数: addruart, waituart, senduart, busyuart这几个函数, 利用UART打印信息
原文:https://www.cnblogs.com/jliuxin/p/14129363.html