linux kernel内核调试方法(二)
创始人
2024-02-15 22:49:09
0

本文是对网络资料进行总结归纳,抄录其他博客资料,如有侵权,请告知,进行删除

一:通过打印调试printk

printk是打印内核消息的函数,printk通过附加不同日志级别(loglevel)或者说消息优先级,让printk对消息进行分类,这是与printf最大的区别,在编译时,日志级别宏会被展开为一个字符串,然后与消息本文拼接在一起,因此printk中优先级和格式字符串之间没有逗号。
如果编译内核时选择了CONFIG_PRINTK=y,则会增加这个功能,否则所有printk都会被替换成空语句。

rsyslog从/proc/kmsg中持续读取,并写入/var/log/messages文件(通过/etc/rsyslog.conf配置)。
dmesg从/dev/kmsg中读取。
cat /dev/kmsg可以获得和dmesg几乎同样的效果,区别是命令最后会阻塞住等待新日志并持续打印。

这里是prink命令的两个例子,一条调试信息和一个临界信息:
printk(KERN_DEBUG “Here I am: %s:%i\n”, FILE, LINE);
printk(KERN_CRIT “I’m trashed; giving up on %p\n”, ptr);

在头文件中定义了8中可用的日志级别字符串,下面以严重程序的降序方式排列:

KERN_EMEGR:用于紧急事件消息,一般是系统崩溃之前提示的消息
KERN_ALERT:用于需要立即采取动作的情况
KERN_CRIT:临界状态,通常设计严重的硬件或软件操作失败
KERN_ERR:用于报告错误状态。设备驱动程序会经常使用该宏来报告来自硬件的问题
KERN_WARNING:对可能出现问题的情况进行警告,但这类情况通常不会对系统造成严重问题
KERN_NOTICE:有必要进行提示的正常情形。许多与安全相关的状态用这个级别进行汇报
KERN_INFO:提示性信息。很多驱动程序在启动的时候以这个级别来打印出它们找到的硬件信息
KERN_DEBUG:用于调试信息
上面每个字符串(以宏的形式展开)表示一个尖括号中的整数。范围分别为0~7。数值越小,优先级越高。

打印级别
默认打印机别
MESSAGE_LOGLEVEL_DEFAULT、CONSOLE_LOGLEVEL_DEFAULT宏

未指定优先级的printk语句采用的默认级别是MESSAGE_LOGLEVEL_DEFAULT;该宏在kernel/printk/printk.c中被指定为另一个宏CONFIG_MESSAGE_LOGLEVEL_DEFAULT,该宏通过config配置

在Linux 2.6.10内核中,MESSAGE_LOGLEV_ELDEFAULT就是KERN_WARNGIN(从config.gz中看到其值为4,说明为KERN_WARNING)

根据日志级别,内核会把消息打印到控制台上或者保存到dmesg中。这个控制台可以是一个字符串终端、一个打印机。当优先级小于console_loglevel这个整数变量时,消息才会打印到控制台,而且每次输出一行。

动态修改打印级别

我们可以通过对文本文件/proc/sys/kernel/printk的访问来读取和修改控制台的日志级别。文件中分别有4个数值字段,从左到右分别为:当前的日志级别、未明确指定日志级别时的默认消息级别、最小允许的日志级别、引导时的默认日志级别。向该文件中写入单个整数值,将会把当前日志级别修改为这个值。

~/$ cat /proc/sys/kernel/printk
4 4 1 7

消息如何被记录
printk函数将消息写到一个长度为__LOG_BUF_LEN字节的循环缓冲区中(ring buff),我们可在配置内核时为__LOG_BUF_LEN指定4KB-1MB之间的值。Linux消息处理方法的另一特点是,可以在任何地方调用printk,甚至在中断处理函数里,而且对数据的大小没有限制,唯一缺点是可能会丢失某些数据。

rsyslogd日志记录器由两个守护进程(rklogd,rsyslogd)和一个配置文件(/etc/rsyslog.conf)组成。rklogd不使用配置文件,它负责截获内核消息,它既可以独立使用也可以作为rsyslogd的客户端运行。rsyslogd默认使用/etc/syslog.conf作为配置文件,负责截获应用程序消息,还可以截获rklogd向其转发的内核消息,然后根据不同服务产生的消息分别记录到不同的文件中。

/var/log/messages或/var/log/syslog — 包括整体系统信息,其中也包含系统启动期间的日志。此外,mail,cron,daemon,kern和auth等内容也记录在var/log/messages日志中。
/var/log/dmesg — 包含内核缓冲信息(kernel ring buffer)。在系统启动时,会在屏幕上显示许多与硬件有关的信息。可以用dmesg查看它们。
/var/log/auth.log — 包含系统授权信息,包括用户登录和使用的权限机制等。
/var/log/boot.log — 包含系统启动时的日志。
/var/log/daemon.log — 包含各种系统后台守护进程日志信息。
/var/log/dpkg.log — 包括安装或dpkg命令清除软件包的日志。
/var/log/kern.log — 包含内核产生的日志,有助于在定制内核时解决问题。
/var/log/lastlog — 记录所有用户的最近信息。这不是一个ASCII文件,因此需要用lastlog命令查看内容。
/var/log/maillog 与 /var/log/mail.log — 包含来着系统运行电子邮件服务器的日志信息。例如,sendmail日志信息就全部送到这个文件中。
/var/log/user.log — 记录所有等级用户信息的日志。
/var/log/Xorg.x.log — 来自X的日志信息。
/var/log/alternatives.log — 更新替代信息都记录在这个文件中。
/var/log/btmp — 记录所有失败登录信息。使用last命令可以查看btmp文件。例如,last -f /var/log/btmp | more 。
/var/log/cups — 涉及所有打印信息的日志。
/var/log/anaconda.log — 在安装Linux时,所有安装信息都储存在这个文件中。
/var/log/yum.log — 包含使用yum安装的软件包信息。
/var/log/cron — 每当cron进程开始一个工作时,就会将相关信息记录在这个文件中。
/var/log/secure — 包含验证和授权方面信息。例如,sshd会将所有信息记录(其中包括失败登录)在这里。
/var/log/wtmp或/var/log/utmp — 包含登录信息。使用wtmp可以找出谁正在登陆进入系统,谁使用命令显示这个文件或信息等。
/var/log/faillog —包含用户登录失败信息。此外,错误登录命令也会记录在本文件中。

开启及关闭消息
在程序开发的初期阶段,printk对于调试和测试新代码是相当有帮助的。不过在正式发布驱动程序时,就得删除这些打印,或者至少禁用它们。不幸的是,你可能会发生这样的情况,即在删除了那些已被认为不再需要的提示消息后,又需要实现一个新的功能(或者发现一个bug),这时,又希望恢复那些log。解决办法:

使用条件语句,因此可在运行期间打开或者关闭,这是一个好功能;但是每次都要进行额外的处理,甚至在禁用消息后仍然会影响性能
定义一个宏,在需要的时候这个宏展开为一个printk调用。麻烦的是需要重新编译,好处是不影响性能速度限制
有时读者会一不小心利用printk产生了上千条消息,从而让日志信息充满控制台,更可能使系统日志文件溢出。如果使用某个慢速控制台(如串口),过快的消息输出会导致系统变慢产生其他时序的问题。因此我们应该非常小心的管理我们的打印信息。通常,正式代码不应该在正常操作下打印任何信息,而打印出的信息应该作为在异常时的提示信息。另一方面,在我们设备异常停止工作时,也许希望产生一条日志信息,但是我们要小心,不能在重试过程中不断地打印失败的提示消息,这样的巨量输出会阻塞CPU运行。

在许多情况下,最好的办法是设置一个标志,表示我已经就此声明过了,并在该标志被设置时不再打印任何消息。但在某些情况下,仍然希望偶尔发出一条该设备停止工作的提示消息。

printk_ratelimit函数(kernel建议用printk_ratelimited代替)通过跟踪发送到控制台的消息数量工作,如果输出的速度超过一个阈值,printk_ratelimit函数将返回零。从而避免发送重复消息。printk_ratelimit函数返回非零值表示我们可以继续打印,否则就应该跳过。

修改/proc/sys/kernel/printk_ratelimit(在重新打开消息之前应该等待的秒数);/proc/sys/kernel/printk_ratelimit_burst(在进行速度限制之前可以接受的消息数)

if (printk_ratelimit()) {
printk(KERN_NOTICE “the printer is still on fire\n”);
}

打印设备号
有时侯,当从一个驱动程序中打印信息时,你想打印与硬件结合的设备号以引起注意。打印主次设备号并不是非常难,但是,为了一致性,内核提供了一对工具宏(在中定义)来达成这个目的:
int print_dev_t(char *buffer, dev_t dev);
char *format_dev_t(char *buffer, dev_t dev);
两个宏都把设备号编码到给出的buffer中;唯一的区别是print_dev_t返回的是被打印的字符数目,而format_dev_t返回buffer;因此,它可以直接作为printk调用的参数,虽然必须记住printk在遇到换行符之前不会输出。缓冲区必须足够大以能保存一个设备号;64位的设备号在将来的内核中是明显可能的,缓冲区至少需要20字节长。

通过查询调试
由于rsyslogd一直保持对输出文件的同步刷新,即使我们通过console_loglevel控制打印到控制台的信息,但是大量使用printk仍然会显著降低系统性能。多数情况下,获取相关信息的最好方法是在需要的时候采取查询系统信息,而不是持续不断地产生数据。

二、使用/proc文件系统

/proc文件系统是一种特殊的、由软件创建的文件系统,内核使用它向外界导出信息。/proc下面的每个文件都绑定了一个内核函数,用户读取其中的文件时,该函数动态地生成文件的“内容”。在Linux系统中对/proc的使用很频繁,很多系统工具都市通过/proc来获取它们需要的信息,例如ps、top和uptime。/proc文件大多是只读文件,不过也可以写入数据。最初的用途是用于提供系统中进程的信息,所以不鼓励在/proc目录下添加过多文件,建议放到/sys目录。
proc文件系统是一种无存储的文件系统,当读其中的文件时,其内容由文件关联的读函数动态生成,当写文件时,文件所关联的写函数被调用。每个proc文件都关联特定的读写函数,因而它提供了另外的一种和内核通信的机制:内核部件可以通过该文件系统向用户空间提供接口来提供查询信息、修改软件行为,因而它是一种比较重要的特殊文件系统。
由于proc文件系统以文件的形式向用户空间提供了访问接口,这些接口可以用于在运行时获取相关部件的信息或者修改部件的行为,因而它是非常方便的一个接口。内核中大量使用了该文件系统。proc文件系统就是一个文件系统,它可以挂载在目录树的任意位置,不过通常挂载在/proc下。

在老版本内核中, 是通过实现read_proc_t 回调函数,再通过create_proc_read_entry注册接口来创建节点的读取

proc数据结构a、proc文件及目录在内核中用proc_dir_entry来表示。它在proc文件系统内部包含了proc文件的所有信息。其数据结构如下所示
struct proc_dir_entry {  unsigned int low_ino;  umode_t mode;  nlink_t nlink;  kuid_t uid;  kgid_t gid;  loff_t size;  const struct inode_operations *proc_iops;//inode操作  const struct file_operations *proc_fops;//文件操作  struct proc_dir_entry *next, *parent, *subdir;  void *data;  read_proc_t *read_proc;  write_proc_t *write_proc;  atomic_t count;       int pde_users;    struct completion *pde_unload_completion;  struct list_head pde_openers;     spinlock_t pde_unload_lock;   u8 namelen;  char name[];  };  
 b、内核还提供了一个数据结构proc_inode用于将特定于proc的数据与文件所对应的inode关联起来,其定义如下。借助该数据结构,内核可以方便的在inode和与该inode相关的proc数据之间进行转换。    
struct proc_inode {  struct pid *pid;  int fd;  union proc_op op;  struct proc_dir_entry *pde;  struct ctl_table_header *sysctl;  struct ctl_table *sysctl_entry;  void *ns;  const struct proc_ns_operations *ns_ops;  struct inode vfs_inode;  
};  

操作proc文件系统的API,相关的函数在内核代码的头文件声明,主要用到以下三个函数。

1、这个函数用于在proc文件系统中创建一个proc文件。struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode,  struct proc_dir_entry *parent);  2、这个函数用于在proc文件系统中创建一个目录项,大多数时候,当我们期望实现自己的proc文件时,都要先创建一个自己的目录,然后在该目录里创建自己的文件,当然我们也可以直接在已经存在的proc文件系统目录里创建自己的文件struct proc_dir_entry *proc_mkdir(const char *name,struct proc_dir_entry *parent);  3、这个函数用于从proc文件系统的指定目录删除指定的proc文件。实际上也可以用来删除目录的。void remove_proc_entry(const char *name, struct proc_dir_entry *parent);
struct proc_dir_entry *create_proc_read_entry(const char *name, mode_t mode, struct proc_dir_entry *base, read_proc_t *read_proc, void * data);
void remove_dir_entry(const char *name, struct proc_dir_entry *parent);

1、创建目录:

struct proc_dir_entry *proc_mkdir(const char *name,   struct proc_dir_entry *parent);  

2、创建proc文件:

struct proc_dir_entry *create_proc_entry( const char *name,  mode_t mode,  struct proc_dir_entry *parent );  

create_proc_entry函数用于创建一个一般的proc文件,其中name是文件名,比如“hello”,mode是文件模式,parent是要创建的proc文件的父目录(若parent = NULL则创建在/proc目录下)。create_proc_entry 的返回值是一个 proc_dir_entry 指针(或者为 NULL,说明在 create 时发生了错误)。然后就可以使用这个返回的指针来配置这个虚拟文件的其他参数,例如在对该文件执行读操作时应该调用的函数。

struct proc_dir_entry {  ......  const struct file_operations *proc_fops;    <==文件操作结构体  struct proc_dir_entry *next, *parent, *subdir;  void *data;  read_proc_t *read_proc;                    <==读回调  write_proc_t *write_proc;                  <==写回调  ......  
}; 

3 .删除proc文件/目录:

void remove_dir_entry(const char *name, struct proc_dir_entry *parent);  

要从 /proc 中删除一个文件,可以使用 remove_proc_entry 函数。要使用这个函数,我们需要提供文件名字符串,以及这个文件在 /proc 文件系统中的位置(parent)。

4、proc文件读回调函数

static int (*proc_read)(char *page, char **start,  off_t off, int count,  int *eof, void *data);

5、proc文件写回调函数

static int proc_write_foobar(struct file *file,  const char *buffer, unsigned long count,  void *data);

proc文件实际上是一个叫做proc_dir_entry的struct(定义在proc_fs.h),该struct中有int read_proc和int write_proc两个元素,要实现proc的文件的读写就要给这两个元素赋值。但这里不是简单地将一个整数赋值过去就行了,需要实现两个回调函数。在用户或应用程序访问该proc文件时,就会调用这个函数,实现这个函数时只需将想要让用户看到的内容放入page即可。在用户或应用程序试图写入该proc文件时,就会调用这个函数,实现这个函数时需要接收用户写入的数据(buff参数)。

写回调函数
我们可以使用 write_proc 函数向 /proc 中写入一项。这个函数的原型如下:

int mod_write( struct file *filp, const char __user *buff,unsigned long len, void *data );

filp 参数实际上是一个打开文件结构(我们可以忽略这个参数)。buff 参数是传递给您的字符串数据。缓冲区地址实际上是一个用户空间的缓冲区,因此我们不能直接读取它。len 参数定义了在 buff 中有多少数据要被写入。data 参数是一个指向私有数据的指针。在这个模块中,我们声明了一个这种类型的函数来处理到达的数据。

Linux 提供了一组 API 来在用户空间和内核空间之间移动数据。对于 write_proc 的情况来说,我们使用了 copy_from_user 函数来维护用户空间的数据。

读回调函数
我们可以使用 read_proc 函数从一个 /proc 项中读取数据(从内核空间到用户空间)。这个函数的原型如下:

int mod_read( char *page, char **start, off_t off, int count, int *eof, void *data );

page 参数是这些数据写入到的位置,其中 count 定义了可以写入的最大字符数。在返回多页数据(通常一页是 4KB)时,我们需要使用 start和 off 参数。当所有数据全部写入之后,就需要设置 eof(文件结束参数)。与 write 类似,data 表示的也是私有数据。此处提供的 page 缓冲区在内核空间中。因此,我们可以直接写入,而不用调用 copy_to_user。

实例代码:

#include   
#include   
#include   
#include   
#include   
#include   #define MODULE_VERS "1.0"  
#define MODULE_NAME "procfs_example"  #define FOOBAR_LEN 8  struct fb_data_t {  char name[FOOBAR_LEN + 1];  char value[FOOBAR_LEN + 1];  
};  static struct proc_dir_entry *example_dir, *foo_file;    struct fb_data_t foo_data;  static int proc_read_foobar(char *page, char **start,  off_t off, int count,    int *eof, void *data)  
{  int len;  struct fb_data_t *fb_data = (struct fb_data_t *)data;  /* DON'T DO THAT - buffer overruns are bad */  len = sprintf(page, "%s = '%s'\n",    fb_data->name, fb_data->value);  return len;  
}  static int proc_write_foobar(struct file *file,  const char *buffer,  unsigned long count,   void *data)  
{  int len;  struct fb_data_t *fb_data = (struct fb_data_t *)data;  if(count > FOOBAR_LEN)  len = FOOBAR_LEN;  else  len = count;  if(copy_from_user(fb_data->name, buffer, len))  return -EFAULT;  fb_data->value[len] = '\0';  return len;  
}  static int __init init_procfs_example(void)  
{  int rv = 0;  /* create directory */  example_dir = proc_mkdir(MODULE_NAME, NULL);  if(example_dir == NULL) {  rv = -ENOMEM;  goto out;  }  /* create foo and bar files using same callback  * functions   */  foo_file = create_proc_entry("foo", 0644, example_dir);  if(foo_file == NULL) {  rv = -ENOMEM;  goto no_foo;  }  strcpy(foo_data.name, "foo");  strcpy(foo_data.value, "foo");  foo_file->data = &foo_data;  foo_file->read_proc = proc_read_foobar;  foo_file->write_proc = proc_write_foobar;  /* everything OK */  printk(KERN_INFO "%s %s initialised\n",  MODULE_NAME, MODULE_VERS);  return 0;  no_foo:  remove_proc_entry("jiffies", example_dir);  out:  return rv;  
}  static void __exit cleanup_procfs_example(void)  
{  remove_proc_entry("foo", example_dir);    remove_proc_entry(MODULE_NAME, NULL);  printk(KERN_INFO "%s %s removed\n",  MODULE_NAME, MODULE_VERS);  
}  module_init(init_procfs_example);  
module_exit(cleanup_procfs_example);  MODULE_AUTHOR("Erik Mouw");  
MODULE_DESCRIPTION("procfs examples");  
MODULE_LICENSE("GPL");  

4.19内核版本,在proc目录下出创建目录

struct proc_dir_entry dbg_root_dir;
struct proc_dir_entry dbg_file_a;dbg_root_dir = proc_mkdir("debug", NULL);
dbg_file_a = proc_create("a-dbg", 0644, dbg_root_dir, &adbg_proc_ops);//(文件名,权限,父目录,ops)proc_remove(dbg_file_a );//释放
proc_remove(dbg_root_dir);

ops函数

struct file_operations adbg_proc_ops=
{.open       = adbg_proc_open,     .read       = seq_read,.write      = adbg_proc_write,     .llseek     = seq_lseek,.release    = single_release,
};

//可以通过echo 写proc的文件达到传入内核驱动命令的效果

static ssize_t adbg_proc_write(struct file *file, const char __user *buffer, size_t count, loff_t *f_pos)  
{  char str[4] = {0};if (count >= sizeof(str)) {return -EFAULT;}if (copy_from_user(str, buffer, count)) { return -EFAULT;  }aa = simple_strtol(str, NULL, 0);if (aa ) {pr_info("debug ON!\n");} else {pr_info("debug OFF!\n");}return count;  
} 
static int adbg_proc_open(struct inode *inode, struct file *file)
{return single_open(file, dbg_proc_show, NULL);
}
static int dbg_proc_show(struct seq_file *m, void *v)
{
seq_printf(m, "********dbg*****\n");
seq_printf(m, "%-8s\t %-5d\t\n", name, index);
return 0;
}

之前的linux版本,使用,
create_proc_entry,
remove_proc_entry来创建和删除proc目录下的文件,
但在4.19版本中已找不到create_proc_entry函数了,具体哪个版本去掉的未了解。

三、使用gdb
gdb在探究系统内部行为非常有用。在这个层次上进行调试需要具备以下基本素养:

熟练使用调试器,掌握gdb命令
了解目标平台的汇编代码
具备对源代码和优化后的汇编代码进行匹配的能力
启动调试器时必须把内核看作一个应用程序。除了指定未压缩的内核映像文件名以外,还应在命令中提供core文件。对于正在运行的内核,所谓的core文件就是这个内核在内存中的核心映像,即/proc/kcore。典型的gdb命令如下gdb /usr/src/linux/vmlinux /proc/kcore。第一个参数是未经压缩的内核ELF可执行文件,而不是zImage或bzImage以及其他特殊的内核映像。第二个参数是core文件。与其他/proc中的文件类似,/proc/kcore也是在被读取时产生的。由于它要表示对应与所有物理内存的整个内核地址空间,所以是一个非常巨大的文件。

对内核调试时,gdb很多功能都不可用,例如不能修改内核数据,不能设置断点或者观察点,也不能单步踪内核。其原因是内核不信任交互式的调试器,担心调试器有不良修改导致系统异常。只能简单的查看信息。而且必须打开CONFIG_DEBUG_INFO选项编译的内核才能看到变量。在调试信息可用的情况下,我们可了解到许多内核内部的工作情况。但是困难在于处理模块,因为模块不是传递给gdb的vmlinux映像的一部分,因此调试器不知道模块的存在。可以通过一条gdb命令告诉调试器有关模块的信息,这条命令就是add-symbol-file,该命令指定目标文件的名称,代码段基地址、数据段基地址以及其他参数。

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...