打开linux调试选项
嵌入式 linux 经常要编译 linux 内核,默认情况下编译出的内核镜像是不带调试信息的,这样,当内核 crash 打印 PC 指针和堆栈信息时,我们需要反汇编来确认出错位置,不直观。
如果内核开启了调试选项,我们只需要一个 addr2line 命令,就可以将 PC 指针定位到 C 程序的哪个文件的哪一行,非常快捷高效。
下面我们就来介绍下,如何开启内核调试选项。
-g
gcc 编译应用程序时,使用 -g 选项编译出带有调试信息的可执行程序。编译内核也是同样的道理。所以,我们先在顶层 Makefile 中搜索 -g 选项,下面是 linux-4.1.15 例子
ifdef CONFIG_DEBUG_INFO
ifdef CONFIG_DEBUG_INFO_SPLIT
KBUILD_CFLAGS += $(call cc-option, -gsplit-dwarf, -g)
else
KBUILD_CFLAGS += -g
endif
KBUILD_AFLAGS += -Wa,-gdwarf-2
endif
ifdef CONFIG_DEBUG_INFO_DWARF4
KBUILD_CFLAGS += $(call cc-option, -gdwarf-4,)
endif
要想使能 -g 选项,就要使能 CONFIG_DEBUG_INFO 编译选项。
make menuconfig,搜索 CONFIG_DEBUG_INFO
在rk3568上此选项已经打开了:
vim .config
3244 # CONFIG_DEBUG_INFO is not set
修改为
3244 CONFIG_DEBUG_INFO=y
然后使用addr2line工具解析PC指针地址:
addr2line -f -e vmlinux 0x809c70a8
do_mount_root
/home/liyongjun/project/board/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/init/do_mounts.c:373
addr2line
addr2line translates addresses into file names and line numbers. Given an address in an executable or an offset in a section of a relocatable object, it uses the debugging information to figure out which file name and line number are associated with it.
描述:addr2line将地址转换为文件名和行号。给定可执行文件中的地址或可重定位对象部分中的偏移量,它会使用调试信息来确定与之相关的文件名和行数。
通过addr2line的描述可知,在使用addr2line将地址转换为函数、文件名或行号时,其有两种使用方法:
- 对于可执行文件,addr2line后直接跟十六进制的地址值;
- 对于可重定位对象文件,addr2line后直接跟十六进制的地址偏移量
从而实现正确输出需要的信息,否则会导致地址无法解析。通过file命令区分可执行文件与可重定位对象。executable表示可执行程序。
relocatable表示可重定位对象文件。
用法
addr2line用于得到程序指令地址所对应的函数,以及函数所在的源文件名和行号。如果没有在命令行中给出地址,就从标准输入中读取它们。
基本用法:addr2line [选项] [地址]
调试用户态普通程序
使用方法:addr2line -e 进程名 IP指令地址 -f
用户态程序有时可能因为各种原因导致崩溃,发生段错误,比如空指针等。如果没有靠谱的工具,我们就只能靠猜哪里的代码可能存在问题,这里通过Linux自带的addr2line工具调试程序,能够快速直接帮我们准确定位到文件、异常函数名以及行号。
segfault.c源文件:
#include <stdio.h>
int main()
{
int *p = NULL;
*p = 0;
return 0;
}
使用gcc进行编译,如下:
[root@localhost 68]# gcc segfault.c -o segfault -g
[root@localhost 68]# ls
segfault segfault.c
[root@localhost 68]# ./segfault
Segmentation fault (core dumped)
dmesg查看报错信息,如下:
[root@localhost ~]# dmesg
[134563.793925] segfault[53791]: segfault at 0 ip 0000000000400546 sp 00007fff7956af70 error 6 in segfault[400000+1000]
[134563.793946] Code: 01 5d c3 90 c3 66 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 40 00 f3 0f 1e fa eb 8a 55 48 89 e5 48 c7 45 f8 00 00 00 00 48 8b 45 f8 <c7> 00 00 00 00 00 b8 00 00 00 00 5d c3 66 2e 0f 1f 84 00 00 00 00
使用addr2line定位:
[root@localhost 68]# addr2line -e segfault 0000000000400546 -f
main
/tmp/68/segfault.c:5
该例子说明addr2line能够直观程序发生段错误的函数以及文件和行号。
注:如果编译程序时没有加上-g参数(即程序不含调试信息),就只能显示出函数名,显示不出具体所在文件的位置,如下:
[root@localhost 68]# addr2line -e segfault 0x0000000000400546 -f
main
??:?
调试动态库程序
在动态库中发生段错误,也可以使用addr2line进行定位。
使用方法:addr2line -e 动态库名 IP指令地址-基地址 -f
#include "foo.h"
int main(void)
{
foo();
return 0;
}
foo.h:
#ifndef __FOO_LIB_H__
#define __FOO_LIB_H__
int foo(void);
#endif
foo.c:
#include "foo.h"
int foo()
{
int *p = 0;
*p = 0;
return 0;
}
先编译动态库, 再编译主程序, 让它链接动态库, 最后运行之:
gcc -O3 -g -o libfoo.so -shared -fPIC foo.c
gcc -O3 -g -o test test.c -L. -lfoo
注:
-fPIC
是 GCC 编译器的一个选项,用于生成位置无关的代码(Position Independent Code, PIC)。这对于创建共享库(shared libraries)是非常有用的,因为共享库可以被多个进程共享内存映射,而不是每个进程都复制一份代码。
当你在编译共享库时,你需要确保所有的对象文件都是用 -fPIC
选项编译的,这样才能正确地链接到共享库中,并且在运行时能够正确地被多个进程映射。
查看dmesg日志,如下:
[2487863.992827] test[332334]: segfault at 0 ip 00007f36d42c3650 sp 00007fff074eefa8 error 6 in libfoo.so[7f36d42c3000+1000]
根据日志可知,段错误发生的位置是在test进程调用的libfoo.so
库里,我们先使用ldd找到动态库的位置,如下:
jli@ubuntu:/work/jli/test$ ldd test
linux-vdso.so.1 => (0x00007ffc7189e000)
libfoo.so => ./libfoo.so (0x00007f46e901a000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f46e8c50000)
/lib64/ld-linux-x86-64.so.2 (0x00007f46e921c000)
dmesg中的ip后面的地址为发生错误的指令地址:00007f36d42c3650
libfoo.so后面中括号中的地址为库的基地址:7f36d42c3000
错误指定的偏移为:00007f36d42c3650 - 7f36d42c3000 = 650
调试内核模块
使用方法:addr2line -e xxx.ko 地址偏移量 -f
当内核模块程序异常时,可能会导致机器直接死机重启,这种情况下定位bug可能就比较麻烦,dmesg日志在机器重新启动后,新的日志会覆盖掉原来的报错日志,从而对定位内核异常问题造成麻烦。常见
提示:Linux内核错误
1、panic 当内核遇到严重错误的时候,内核panic,立马崩溃。死机。
2、Oops Oops是内核遇到错误时发出的提示“声音”,Oops有时候会触发panic,有时候不会,而是直接杀死当前进程,系统可以继续运行。
比如说内核态下的段错误,当内核设置了panic_on_oops=1的时候,Oops会触发panic。【panic_on_oops的值在内核编译的时候配置,可以在/proc/sys/kernel/panic_on_oops查看值,同时可以使用sysctl修改】
当panic_on_oops=0的时候,如果错误发生在中断上下文,Oops也会触发panic。如果错误只是发生在进程上下文,这个时候只需要kill当前进程。【中断上下文包括以下情况:硬中断、软中断、NMI】。Oops的时候内核还可以运行,只是可能不稳定,这个时候,内核会调用printk打印输出内核栈的信息和寄存器的信息。
本人所用主机即属于一旦发生Oops,就会触发panic,因此总是无法查看Oops时的dmesg日志,经查阅资料,发现是内核参数panic_on_oops的原因导致的,因为该参数被设置为1,所以Oops会触发panic,从而导致机器总是死机重启,无法查看Oops时的dmesg日志。下面提供两种方法修改Oops内核参数,使其不会在Oops的时候触发panic导致死机重启。
方法一:修改 /proc下内核参数文件内容,临时生效,重启后失效。
echo 0 > /proc/sys/kernel/panic_on_oops
方法二:修改/etc/sysctl.conf
文件的内核参数来永久更改。
[root@localhost ~]# vi /etc/sysctl.conf
[root@localhost ~]# cat /etc/sysctl.conf
# sysctl settings are defined through files in
# /usr/lib/sysctl.d/, /run/sysctl.d/, and /etc/sysctl.d/.
#
# Vendors settings live in /usr/lib/sysctl.d/.
# To override a whole file, create a new file with the same in
# /etc/sysctl.d/ and put new settings there. To override
# only specific settings, add a file with a lexically later
# name in /etc/sysctl.d/ and put new settings there.
#
# For more information, see sysctl.conf(5) and sysctl.d(5).
kernel.panic_on_oops=0
[root@localhost ~]# cat /proc/sys/kernel/panic_on_oops
1
[root@localhost ~]# sysctl -p
kernel.panic_on_oops = 0
[root@localhost ~]#
[root@localhost ~]# cat /proc/sys/kernel/panic_on_oops
0
例如下面的oops日志如下:
[root@localhost ~]# dmesg
[ 1039.918606] my_oops_init
[ 1039.918616] BUG: unable to handle kernel NULL pointer dereference at 0000000000000000
[ 1039.926442] PGD 0 P4D 0
[ 1039.928979] Oops: 0002 [#1] SMP NOPTI
[ 1039.932637] CPU: 34 PID: 3843 Comm: insmod Kdump: loaded Tainted: G OE --------- - - 4.18.0-394.el8.x86_64 #1
[ 1039.943756] Hardware name: New H3C Technologies Co., Ltd. H3C UniServer R4950 G5/RS45M2C9SB, BIOS 5.37 09/30/2021
[ 1039.954000] RIP: 0010:do_oops+0x5/0x11 [oops]
[ 1039.958364] Code: Unable to access opcode bytes at RIP 0xffffffffc02e6fdb.
[ 1039.965231] RSP: 0018:ffffb9d40a8c7cb0 EFLAGS: 00010246
[ 1039.970449] RAX: 000000000000000c RBX: 0000000000000000 RCX: 0000000000000000
[ 1039.977573] RDX: 0000000000000000 RSI: ffff98942ee96758 RDI: ffff98942ee96758
[ 1039.984697] RBP: ffffffffc02e7011 R08: 0000000000000000 R09: c0000000ffff7fff
[ 1039.991822] R10: 0000000000000001 R11: ffffb9d40a8c7ad8 R12: ffffffffc02e9000
[ 1039.998944] R13: ffffffffc02e9018 R14: ffffffffc02e91d0 R15: 0000000000000000
[ 1040.006069] FS: 00007f1b8d93b740(0000) GS:ffff98942ee80000(0000) knlGS:0000000000000000
[ 1040.014145] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 1040.019884] CR2: ffffffffc02e6fdb CR3: 0000000145c02000 CR4: 0000000000350ee0
[ 1040.027008] Call Trace:
[ 1040.029454] my_oops_init+0x16/0x19 [oops]
[ 1040.033550] do_one_initcall+0x46/0x1d0
[ 1040.037390] ? do_init_module+0x22/0x220
[ 1040.041318] ? kmem_cache_alloc_trace+0x142/0x280
[ 1040.046023] do_init_module+0x5a/0x220
[ 1040.049777] load_module+0x14ba/0x17f0
[ 1040.053530] ? __do_sys_finit_module+0xb1/0x110
[ 1040.058059] __do_sys_finit_module+0xb1/0x110
[ 1040.062411] do_syscall_64+0x5b/0x1a0
[ 1040.066077] entry_SYSCALL_64_after_hwframe+0x65/0xca
[ 1040.071130] RIP: 0033:0x7f1b8c8509bd
[ 1040.074701] Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 8b 0d 9b 54 38 00 f7 d8 64 89 01 48
[ 1040.093446] RSP: 002b:00007ffc4df0a968 EFLAGS: 00000246 ORIG_RAX: 0000000000000139
[ 1040.101004] RAX: ffffffffffffffda RBX: 00005653fb1997d0 RCX: 00007f1b8c8509bd
[ 1040.108126] RDX: 0000000000000000 RSI: 00005653f980c8b6 RDI: 0000000000000003
[ 1040.115251] RBP: 00005653f980c8b6 R08: 0000000000000000 R09: 00007f1b8cbd9760
[ 1040.122375] R10: 0000000000000003 R11: 0000000000000246 R12: 0000000000000000
[ 1040.129498] R13: 00005653fb1997b0 R14: 0000000000000000 R15: 0000000000000000
[ 1040.136623] Modules linked in: oops(OE+) binfmt_misc xt_CHECKSUM ipt_MASQUERADE xt_conntrack ipt_REJECT nf_reject_ipv4 nft_compat nft_counter nft_chain_nat nf_nat nf_conntrack nf_defrag_ipv6 nf_defrag_ipv4 nf_tables nfnetlink rpcsec_gss_krb5 auth_rpcgss nfsv4 dns_resolver nfs lockd grace fscache bridge stp llc intel_rapl_msr intel_rapl_common amd64_edac_mod edac_mce_amd amd_energy kvm_amd kvm irqbypass ipmi_ssif pcspkr crct10dif_pclmul crc32_pclmul ghash_clmulni_intel rapl joydev ccp sp5100_tco i2c_piix4 k10temp ptdma acpi_ipmi ipmi_si sunrpc vfat fat xfs libcrc32c sd_mod t10_pi sg crc32c_intel ast drm_vram_helper drm_kms_helper syscopyarea sysfillrect sysimgblt fb_sys_fops drm_ttm_helper ttm ahci drm libahci nfp(OE) igb libata dca i2c_algo_bit dm_mirror dm_region_hash dm_log dm_mod ipmi_devintf ipmi_msghandler
[ 1040.208357] CR2: 0000000000000000
[ 1040.211668] ---[ end trace b69c1e8998070273 ]---
[ 1040.230185] RIP: 0010:do_oops+0x5/0x11 [oops]
[ 1040.234540] Code: Unable to access opcode bytes at RIP 0xffffffffc02e6fdb.
[ 1040.241409] RSP: 0018:ffffb9d40a8c7cb0 EFLAGS: 00010246
[ 1040.246626] RAX: 000000000000000c RBX: 0000000000000000 RCX: 0000000000000000
[ 1040.253750] RDX: 0000000000000000 RSI: ffff98942ee96758 RDI: ffff98942ee96758
[ 1040.260876] RBP: ffffffffc02e7011 R08: 0000000000000000 R09: c0000000ffff7fff
[ 1040.267998] R10: 0000000000000001 R11: ffffb9d40a8c7ad8 R12: ffffffffc02e9000
[ 1040.275124] R13: ffffffffc02e9018 R14: ffffffffc02e91d0 R15: 0000000000000000
[ 1040.282247] FS: 00007f1b8d93b740(0000) GS:ffff98942ee80000(0000) knlGS:0000000000000000
[ 1040.290323] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 1040.296061] CR2: ffffffffc02e6fdb CR3: 0000000145c02000 CR4: 0000000000350ee0
Oops: 0002 -- 错误码
Oops: [#1] -- Oops发生的次数
CPU: 34 -- 表示Oops是发生在CPU34上
关键信息如下,这里提示在操作函数do_oops的时候出现异常,地址偏移量0x5:
[ 1039.954000] RIP: 0010:do_oops+0x5/0x11 [oops]
为什么这条信息关键,因为其含有指令指针RIP;指令指针IP/EIP/RIP的基本功能是指向要执行的下一条地址。在8080 8位微处理器上的寄存器名称是PC(program counter,程序计数器),从8086起,被称为IP(instruction pointer,指令指针)。主要区别在与PC指向正在执行的指令,而IP指向下一条指令。在64位模式下,指令指针是RIP寄存器。这个寄存器保存着下一条要执行的指令的64位地址偏移量。64位模式支持一种新的寻址模式,被称为RIP相对寻址。使用这个模式,有效地址的计算方式变为RIP(指向下一条指令)加上位移量。
由此可以看出内核执行到do_oops+0x5/0x11
这个地址的时候出现异常,我们只需要找到这个地址对应的代码即可。
打印格式:do_oops+0x5/0x11 [oops]
即:symbol+offset/size [module]
symbol
: 符号
offset
:地址偏移量
size
:函数的长度
module
: 所属内核模块
do_oops
指示了是在do_oops
函数中出现的异常, 0x5
表示出错的地址偏移量, 0x11
表示do_oops
函数的大小。使用file查看内核模块文件类型:
[root@localhost oops]# file oops.ko
oops.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=4b5b58c4aaf63d65d338be17399bfd3c480504c3, with debug_info, not stripped
上述结果显示该内核模块文件为可重定位对象文件,因此使用addr2line直接跟十六进制的地址偏移量定位文件名、行号和行数,如下:
[root@localhost oops]# addr2line -e oops.ko 0x5 -f -p
do_oops at /tmp/oops/oops.c:7
可以看到异常代码在oops.c
文件第7行,根据源代码,可知该行代码访问非法内存地址。
总结
Addr2line 工具(它是标准的 GNU Binutils 中的一部分)是一个可以将指令的地址和可执行映像转换成文件名、函数名和源代码行数的工具。这在应用程序和内核程序执行过程中出现崩溃时,可用于快速定位出出错的位置,进而找出代码的bug。一般适用于 debug 版本或带有 symbol 信息的库。
修改内核打印日志级别: