打印文件、函数行数

以下宏是编译器可以识别的,在编译时,编译器会将该宏代表的值写到字符串中。

__FILE__               包含当前程序文件名的字符串 

__LINE__              表示当前行号 

__FUNCTION__    所在的函数。__FUNCTION__也可以用__func__

__DATE__             包含当前日期的字符串。格式:"MAY 7 2019"

__TIME__              包含当前时间的字符串。格式:"19:15:20"

__STDC__              如果编译器遵循ANSI C标准,它就是个非零值 

内核中:

printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);

应用中:

printf("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__)

注意:__FUNCTION__也可以用__func__。VC6.0不支持__FUNCTION__

驱动中调试宏

法一

#define DEBUG

#ifdef DEBUG

#define dbg() printk("[%s] : %d\n",  __FUNCTION__, __LINE__);

#else

#define dbg() do {} while (0)

#endif

法二

#ifdef DEBUG

#define dbg(format, arg...) printk(KERN_ERR __FILE__ ": " format "\n" , ## arg)

#else

#define dbg(format, arg...) do {} while (0)

#endif

法三

#undef DEBUG

#ifdef DEBUG

#define dbg(fmt, args...) printk(KERN_DEBUG "[%s]  " fmt ,  __func__, ## args)

#else

#define dbg(fmt, args...) do {} while (0)

#endif

errno、strerror、perror

调用linux 系统api 的时候会出现一些错误,想知道失败的原因

一、errno

1.errno是"errno.h"中定义的以E开头的宏,代表数字。

        (如果是在内核,可能是 /include/uapi/asm-generic/errno.h、/usr/include/asm/errno.h 、include/asm-generic/errno-base.h)

2.程序刚刚启动的时候,errno 被设置为 0;程序在运行过程中,任何一个函数发生错误都有可能修改 errno 的值。

3.标准库中的函数只会将 errno 设置为一个用以表明具体错误类型的非零值,不会再将 errno 设置回零值。

4.再次设置 errno 的值会覆盖以前 errno 的值

二、strerror

作用:返回errno对应的字符串

头文件:error.h  errno.h

函数原型:char *strerror(int errnum);

返回值:errno对应的字符串

例程:

FILE *fp=NULL;

fp = fopen("test.txt", "r");

if(fp == NULL){

printf("%s\n",strerror(errno));

}

运行结果:(如果没有此文件)

No such file or directory

三、perror

作用:先打印s对应的字符串,再打印:(冒号),再打印errno对应的字符串

头文件:error.h

函数原型:void perror(const char *s);

例程:

FILE *fp=NULL;

fp = fopen("test.txt", "r");

if(fp == NULL){

perror("Open test.txt");

}

运行结果:(如果没有此文件)

Open test.txt:No such file or directory

用strace来跟踪

​strace命令用法​​​    ​​strace交叉编译方法​

打开相应模块的宏定义

代码中一般是用pr_debug打印调试信息,此调试信息由是否定义DEBUG宏来决定。

pr_debug​打印的调试信息要用​dmesg​命令查看。

有两种方法定义DEBUG宏。

一:make menuconfig,找到对应项,配置。一般名为:CONFIG_XX_DEBUG

例1:

        linux-3.4.2/arch/arm/plat-samsung/pm.c

         有S3C_PMDBG("..."),其定义位置在linux-3.4.2/arch/arm/plat-samsung/include/plat/pm.h

         #ifdef CONFIG_SAMSUNG_PM_DEBUG

         #define S3C_PMDBG(fmt...) s3c_pm_dbg(fmt)

     在内核make menuconfig;搜索CONFIG_SAMSUNG_PM_DEBUG将其打开即可

例2:

        linux-4.9.37/drivers/mmc/core/core.c:

        #ifdef CONFIG_MMC_DEBUG

            pr_info("%s: %s: trying to init card at %u Hz\n",

                mmc_hostname(host), __func__, host->f_init);

        #endif

        在内核make menuconfig;搜索CONFIG_MMC_DEBUG将其打开即可。

二:有的找不到对应的配置,解决方法:

    在相应目录添加如下:

ccflags-y += -DDEBUG
subdir-ccflags-y += -DDEBUG

        (有的内核是添加:EXTRA-FLAGS += -DDEBUG)

    例:对ubi子系统进行调试

          /drivers/mtd/ubi/Makefile

          添加:

          ccflags-y += -DDEBUG

          subdir-ccflags-y += -DDEBUG

打印堆栈信息

内核或驱动可以调用dump_stack()函数打印函数调用过程(会打印从最顶层到函数执行处的调用流程)

addr2line命令

内核调试方法:《独辟蹊径品内核 Linux内核源代码导读》 =>第13章


选项



含义



-b bfdname         

--target=bfdname



    指定目标文件的格式为bfdname                                                                                  

    注:bfdname是BFD库中描述的标准格式名。其它信息详google                                                          



-C                 

--demangle=[style]



    指定显示的函数为用户可读的函数名,也就是说去编译器自动生成的那些标识换成我们代码里面的对应的函数名。           

    这点在C语言里面主要常见的就是把_下划线下标去掉,以及在C++里面把对应的函数里面的一些编译器自动添加的连接符去掉。                                                          



-e filename        

--exe=filename



    指定要换的地址对应的程序名。默认是a.out                                                       



-f                 

--functions



    除了显示文件名和行号外,还把地址所在的函数名显示出来。                                                       



-s                

--basename



     只显示每个文件的文件名,不显示文件前面的路径,如果有的话。                                   



-i                

--inlines



     如果一个函数是内联函数的话,那么凡是包含了这个内联函数的所有的代码都会显示出来。



@file             



     从文件file里面读取所有的选项和参数。                               


    有时候,出错后产生不了崩溃信息。用ulimit -c查看,如果显示是0,则说明core dump没有打开,可以使用ulimit -c unlimited打开(也可以用ulimit -c 1024来限制core文件的大小,此时不会崩溃,但会有core dump信息写到文件中)。

    core dump不打开也是有好处的,这样如果出错了,不会崩溃。如果打开了,则出错的时候会引起崩溃,而且能打印出错信息。

内核调试

调试方法见:​《独辟蹊径品内核 Linux内核源代码导读.pdf》

以下为此书介绍的方法:

宏是否定义

定位同名宏

定位同名结构体

定位同名函数

定位全局变量定义位置

定位函数调用过程

printk打印

dmesg

dmesg读取的是/proc/kmsg的内容,可以直接cat /proc/kmsg

dmesg默认会打印所有级别的信息。

dmesg命令

-c                            显示信息后,清除ring buffer中的内容。 

-s<缓冲区大小>       预设置为8196,刚好等于ring buffer的大小。 

-n                           设置记录信息的层级。

printk打印级别

include/linux/kernel.h有四个宏控制printk的记录级别

#define console_loglevel (console_printk[0])

#define default_message_loglevel (console_printk[1]}

#define minimum_console_loglevel (console_printk[2])

#define default_console_loglevel (console_printk[3])

console_loglevel:                     高于此级别的会被打印出来。即:数字小于console_loglevel

default_message_loglevel:    printk的默认值(​不加打印级别时的值​)。

                                                         一般此值为4,若printk不加KERN_*,则默认为printk("<4>")

minimum_console_loglevel:   是预设值,平时不起作用,通过其他工具设置console_loglevel时,

                                                        console_loglevel值不能小于minimum_console_loglevel

default_console_loglevel:      设置console_loglevel时的默认值。平时不起作用,它表示设置console_loglevel时的默认值。

include/linux/kern_level.h

#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 */

打印级别修改方法

1.用户空间对打印级别的修改

cat /proc/sys/kernel/printk可得到上述四个值,默认为7 4 1 7

可直接修改:echo "8 4 1 7" > /proc/sys/kernel/printk  此时,所有信息都被打印出来。

注意:echo 8 > /proc/sys/kernel/printk  等价于 echo "8 4 1 7" > /proc/sys/kernel/printk。即:只设置console_loglevel

2.内核启动以前设置打印级别

环境变量bootargs添加 "loglevel=n",等价于启动后echo n > /proc/sys/kernel/printk

调试示例

示例1:崩溃后信息

在栈回溯的上边,有PC和LR相关信息。

             例如:

                    PC is at wm8976_write+0x2c/0x138

                    LR is at snd_soc_wm8976_resume+0x34/0x60

           表明:出错的函数是wm8976_write,它是snd_soc_wm8976_resume调用的。

示例2:驱动、应用调试方法

        1. 使产生的目标文件(.o和.ko)带有debug信息:

            驱动程序调试方法:在自己写的驱动的Makefile里增加一句

                                       EXTRA_CFLAGS +=-g  //或者EXTRA_CFLAGS += "-g"

             应用程序调试方法:用gcc编译时加-g选项。 

                 (编译应用的编译选项等第三期电子书项目的Makefile有例子)

        2. make之后反编译目标文件,然后将其拷贝出来

            虚拟机方法:objdump -D myuvc.ko > myuvc.dis

            开发板方法:arm-linux-objdump -D myuvc.ko > myuvc.dis

        3. insmod myuvc.ko;调试信息将出现在之前设置的文件中

           [  951.778546] EIP: 0060:[] EFLAGS: 00010046 CPU: 0

           [  951.779680] EIP is at myuvc_video_complete+0x7f/0x210 [myuvc]

        4. 在myuvc.dis中查找“myuvc_video_complete”得到:

           00000670 :

                      670: 55                   push   �p

                      671: 89 e5               mov    %esp,�p

                     ...

             确定出出错的地址为:0x670+0x7f = 0x6ef

        5.  重启虚拟机,用addr2line命令显示错误位置:

            addr2line -f -e myuvc.ko 0x6ef;   //开发板可能是arm-linux-addr2line命令

            得到:

            myuvc_video_complete

           /work/projects/video_monitoring/myuvc/myuvc.c:574

           至此可以确定是第574行出错。

        另外:

        a. 也可以通过调试信息打印出的网页收藏“linux中的ERROR用法_飞鹰0苍狼_新浪博客”中的错误,通

                  过检查出 错地方前边的函数的返回值来查看错误。

        b. 通过printk调试时,会时程序变卡。如果有了printk,然后出现了卡顿情况,删掉printk

        附:自己的Makefile中的编译选项:

               EXTRA_CFLAGS           gcc选项

               EXTRA_AFLAGS           编译汇编源代码的选项

               EXTRA_LDFLAGS         LD选项

               EXTRA_ARFLAGS         AR选项

示例3:内核调试方法

           1. 在内核根目录下make menuconfig

                   Kernel hacking => Compile the kernel with debug info  //选中此项

                    经过此设置之后,编译内核时产生的vmlinux和uImage就会带有debug信息

          法1:用addr2line命令

                 错误信息:

                     Unable to handle kernel NULL pointer dereference at virtual address 00000150

                     pgd = c3a88000

                     [00000150] *pgd=00000000

                     Internal error: Oops: 5 [#1] ARM

                     Modules linked in:

                     CPU: 0    Not tainted  (3.4.2 #3)

                     PC is at get_dump_page+0x6c/0xa8

                     LR is at __get_user_pages+0x328/0x410

                     pc : [<<b>c006f728>]    lr : []    psr: 20000013

                     ...

                     Flags: nzCv  IRQs on  FIQs on  Mode SVC_32  ISA ARM  Segment kernel

                     Control: c000717f  Table: 33a88000  DAC: 00000017

                     Process sshd (pid: 950, stack limit = 0xc3a5c270)

                     Stack: (0xc3a5dce0 to 0xc3a5e000)

                 查找方法:

                 arm-linux-gcc -e vmlinux c006f728


           法2:gdb调试

                错误信息:

                   例如内核调试驱动之类的时,产生如下错误

                     [ 1023.520000] PC is at atmel_tasklet_func+0x104/0x690

                     [ 1023.520000] LR is at atmel_tasklet_func+0x10/0x690

              1. gdb vmlinux;

              2. 在gdb下键入命令 : l *at atmel_tasklet_func+0x10

                    得到:


                   即可确定内核出错的地方

             3. 上一步确定了是dma导致错误,再次make menuconfig;去掉dma的支持:

其他

coredump

(coredump是默认关闭的,需要进行相应的配置操作)

代码中出现以下逻辑错误会触发coredump。

  SIGSEGV:信号11:使用无效的内存(段错误)

  SIGABRT:信号6:调用abort系统函数产生,malloc调用崩溃产生coredump

  SIGFPE: 信号8:算数异常,如:除以0

gdb

gdb解析命令三要素

  1)编译链+gdb

  2)包含符号表的执行文件

  3)core文件

调试举例:数组越界导致设备崩溃

gdbserver

优点:

与gdb工具的调试命令相似,具有很好的兼容性

可以在不破坏环境的前提下进行调试。手动coredump会破坏现场的环境。

能很好的定位死锁问题。

可以进行远程调试:远程调试由宿主机gdb和目标机的调试程序组成,两者通过TCP通信。使用GDB标准串行协议协

         同工作,实现对目标机上的系统内核和应用的监控和调试功能。

常用的是调试应用程序,采用gdb+gdbserver的方式进行。采用gdb方法调试,因为嵌入式系统资源有限,一般不能

         在目标系统上直接调试,所以采用gdb+gdbserver的方式进行。

调试举例:死锁问题

dmalloc

适用情况:

1)排查A线程内存拷贝越界,B线程使用这块内存中的异常数据,导致B线程躺枪崩溃的问题

2)排查内存泄漏的问题

局限性:是开源工具,运行时占用大量的内存,经济型设备无法使用

dmalloc原理:

    在每段申请的动态内存前后各加一段只读权限的内存空间,如果出现内存越界立即触发coredump动作,不会出现其他线程再去使用该内存中的异常数据

钩子函数

使用情况:

内存泄漏,句柄泄漏

原理:

在编译过程中使用自己封装的函数替代系统函数,如:malloc,free,socket,open,close针对性的打印出需要关注的变量,如:函数调用地址、句柄编号、申请内存的起始地址