Linux addr2line介绍

打开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 信息的库。

修改内核打印日志级别:

相关推荐
寂柒3 小时前
信号量——基于环形队列的生产消费模型
linux·ubuntu
一袋米扛几楼984 小时前
【密码学】CrypTool2 工具是什么?
服务器·网络·密码学
vin_zheng5 小时前
破解企业安全软件网络拦截实战记录
运维
林姜泽樾7 小时前
Linux入门第十二章,创建用户、用户组、主组附加组等相关知识详解
linux·运维·服务器·centos
xiaokangzhe7 小时前
Linux系统安全
linux·运维·系统安全
feng一样的男子7 小时前
NFS 扩展属性 (xattr) 提示操作不支持解决方案
linux·go
南棱笑笑生8 小时前
20260310在瑞芯微原厂RK3576的Android14查看系统休眠时间
服务器·网络·数据库·rockchip
xiaokangzhe8 小时前
Nginx核心功能
运维·nginx
松果1778 小时前
以本地时钟为源的时间服务器
运维·chrony·时间服务器
XDHCOM8 小时前
ORA-32152报错咋整啊,数据库操作遇到null number问题远程帮忙修复
服务器·数据库·oracle