Linux 程序调试指南

GDB 是 GNU 发布的、运行在 UNIX/Linux 下的命令行调试器,也是 Linux 下 C/C++ 程序员必不可少的工具。它的命令很多,但日常调试只需掌握其中约十个。本文先从一个完整的调试过程讲起,再延伸介绍 pstackstracenmobjdumpreadelf 等常用的程序调试与分析工具。

命令速览

工具 用途
gdb 命令行调试器
pstack 打印进程的栈跟踪
strace 跟踪系统调用
nm 列出目标文件的符号
objdump 反汇编、查看二进制信息
readelf 查看 ELF 文件详细信息
size 查看各段的内存占用
file 查看文件类型
strings 提取文件中的文本信息
fuser 查看文件 / 套接字的使用者
xxd 十六进制显示文件内容

GDB 基础

下面通过一个完整的调试过程,说明最常用的几个命令:

$ gdb programmer      # 启动 gdb
> break main          # 设置断点
> run                 # 运行程序
> next                # 单步调试
> print var1          # 打印变量 var1 的值
> list                # 显示当前调试处的源代码
> info b              # 查看断点设置情况

完成第一个程序的调试后,你会需要更多命令。GDB 的常用命令及各种调试方法详见 gdb 调试利器

两个提升效率的技巧:

  • 常用命令都有单字符缩写,使用缩写更方便;
  • 直接敲回车会重复执行上一条命令,单步调试时非常有用。

pstack 跟踪栈空间

pstack 是一个脚本工具,用于显示每个进程的栈跟踪。它必须由进程属主或 root 运行,其核心实现就是调用 gdb 的 thread apply all bt 命令。

$ pstack <pid>

示例:

$ pstack 4551
Thread 7 (Thread 1084229984 (LWP 4552)):
#0  0x000000302afc63dc in epoll_wait () from /lib64/tls/libc.so.6
#1  0x00000000006f0730 in ub::EPollEx::poll ()
#2  0x00000000006f172a in ub::NetReactor::callback ()
#3  0x00000000006fbbbb in ub::UBTask::CALLBACK ()
#4  0x000000302b80610a in start_thread () from /lib64/tls/libpthread.so.0
#5  0x000000302afc6003 in clone () from /lib64/tls/libc.so.6
#6  0x0000000000000000 in ?? ()

strace 分析系统调用

strace 用来跟踪进程执行时的系统调用和所接收的信号。在 Linux 中,进程不能直接访问硬件设备,访问磁盘、收发网络数据等操作必须从用户态切换到内核态、通过系统调用来完成。strace 能跟踪到一个进程产生的系统调用,包括参数、返回值和执行耗时。

$ strace -o output.txt -T -tt -e trace=all -p 28979

上面的命令跟踪 PID 为 28979 的进程的所有系统调用(-e trace=all),统计每次调用的耗时和开始时间(以时分秒格式显示),并把结果写入 output.txt

实时查看某个进程正在做什么:

$ strace -p <pid>

关于 strace 的详细介绍,详见 strace 跟踪进程中的系统调用

nm 查看符号清单

nm 用来列出目标文件的符号清单。

$ nm myprogram
08049f28 d _DYNAMIC
08049ff4 d _GLOBAL_OFFSET_TABLE_
080484dc R _IO_stdin_used
w _Jv_RegisterClasses
08049f18 d __CTOR_END__
08049f14 d __CTOR_LIST__
08049f20 D __DTOR_END__
08049f1c d __DTOR_LIST__
080485e0 r __FRAME_END__
08049f24 d __JCR_END__
08049f24 d __JCR_LIST__
0804a014 A __bss_start
0804a00c D __data_start
08048490 t __do_global_ctors_aux
08048360 t __do_global_dtors_aux
0804a010 D __dso_handle
w __gmon_start__
08048482 T __i686.get_pc_thunk.bx
08049f14 d __init_array_end
08049f14 d __init_array_start
08048480 T __libc_csu_fini
08048410 T __libc_csu_init
0804a014 A _edata
0804a01c A _end
080484bc T _fini
080484d8 R _fp_hw
080482b4 T _init
08048330 T _start
0804a014 b completed.6086
0804a00c W data_start
0804a018 b dtor_idx.6088
080483c0 t frame_dummy
080483e4 T main

这些包含可执行代码的段称为正文段(text);数据段(data)包含不可执行的信息或数据;还有一种 BSS 段,包含以符号数据开头的块。nm 列出的每个符号,其值默认用十六进制表示,符号前会有一个表示符号类型的编码字符。常见的编码包括:

  • A 表示绝对(absolute),意味着该值不能被更改为其他的连接;
  • B 表示 BSS 段中的符号;
  • C 表示引用未初始化数据的一般符号。

目标文件中的不同部分可以划分为段,段里可能包含可执行代码、符号名称、初始数据值等。关于这些数据类型的详细信息,可以阅读 UNIX 中 nm 的 man 手册,其中按输出字符编码对每种类型做了描述。即使是简单的 Hello World 程序,目标文件中也有大量细节。nm 可以列举符号及其类型和值,但要更深入地研究目标文件中命名段的内容,需要更强大的工具——其中两种就是 objdumpreadelf

参数说明及更多示例详见 nm 目标文件格式分析

objdump 反汇编

objdump 以可读的格式显示二进制文件的信息,让你了解二进制文件可能带有的附加信息。

$ objdump -d myprogram
a.out: file format elf32-i386

Disassembly of section .init:

080482b4 :
80482b4: 53 push %ebx
80482b5: 83 ec 08 sub $0x8,%esp
80482b8: e8 00 00 00 00 call 80482bd
80482bd: 5b pop %ebx
80482be: 81 c3 37 1d 00 00 add $0x1d37,%ebx
80482c4: 8b 83 fc ff ff ff mov -0x4(%ebx),%eax
80482ca: 85 c0 test %eax,%eax
80482cc: 74 05 je 80482d3
80482ce: e8 3d 00 00 00 call 8048310
80482d3: e8 e8 00 00 00 call 80483c0
80482d8: e8 b3 01 00 00 call 8048490
80482dd: 83 c4 08 add $0x8,%esp
80482e0: 5b pop %ebx
80482e1: c3 ret

Disassembly of section .plt:
...

每个可执行代码段会在特定事件发生时执行,比如库初始化和程序主入口。对底层编程细节感兴趣的开发者,可以用它研究编译器和汇编器的输出,了解本地处理器的运行方式;结合处理器厂商的技术文档,就能深入理解内部的运行机制。

参数说明及更多示例详见 objdump 二进制文件分析

readelf 查看 ELF 信息

readelf 的功能和 objdump 类似,但显示的信息更具体,并且不依赖 BFD 库(BFD 是 GNU 项目,目标是用统一的接口处理不同的目标文件)。

$ readelf -all a.out
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048330
Start of program headers: 52 (bytes into file)
Start of section headers: 4412 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 9
Size of section headers: 40 (bytes)
Number of section headers: 30
Section header string table index: 27

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4
[ 3] .note.gnu.build-i NOTE 08048188 000188 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 080481ac 0001ac 000020 04 A 5 0 4
[ 5] .dynsym DYNSYM 080481cc 0001cc 000050 10 A 6 1 4
[ 6] .dynstr STRTAB 0804821c 00021c 00004c 00 A 0 0 1
[ 7] .gnu.version VERSYM 08048268 000268 00000a 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 08048274 000274 000020 00 A 6 1 4
[ 9] .rel.dyn REL 08048294 000294 000008 08 A 5 0 4
[10] .rel.plt REL 0804829c 00029c 000018 08 A 5 12 4
[11] .init PROGBITS 080482b4 0002b4 00002e 00 AX 0 0 4
[12] .plt PROGBITS 080482f0 0002f0 000040 04 AX 0 0 16
[13] .text PROGBITS 08048330 000330 00018c 00 AX 0 0 16
[14] .fini PROGBITS 080484bc 0004bc 00001a 00 AX 0 0 4
[15] .rodata PROGBITS 080484d8 0004d8 000011 00 A 0 0 4
[16] .eh_frame_hdr PROGBITS 080484ec 0004ec 000034 00 A 0 0 4
[17] .eh_frame PROGBITS 08048520 000520 0000c4 00 A 0 0 4
[18] .ctors PROGBITS 08049f14 000f14 000008 00 WA 0 0 4
[19] .dtors PROGBITS 08049f1c 000f1c 000008 00 WA 0 0 4
[20] .jcr PROGBITS 08049f24 000f24 000004 00 WA 0 0 4
[21] .dynamic DYNAMIC 08049f28 000f28 0000c8 08 WA 6 0 4
[22] .got PROGBITS 08049ff0 000ff0 000004 04 WA 0 0 4
[23] .got.plt PROGBITS 08049ff4 000ff4 000018 04 WA 0 0 4
[24] .data PROGBITS 0804a00c 00100c 000008 00 WA 0 0 4
[25] .bss NOBITS 0804a014 001014 000008 00 WA 0 0 4
[26] .comment PROGBITS 00000000 001014 00002a 01 MS 0 0 1
[27] .shstrtab STRTAB 00000000 00103e 0000fc 00 0 0 1
[28] .symtab SYMTAB 00000000 0015ec 000410 10 29 45 4
[29] .strtab STRTAB 00000000 0019fc 0001f9 00 0 0 1
...

ELF Header 展示了文件中所有段的详细摘要,以及 Header 的具体数目,研究较大的目标文件时这些信息非常有用。编译器还可以把调试信息放入目标文件,用下面的命令可以分析这些信息:

$ readelf --debug-dump a.out | more

GDB 等调试工具能读取这些调试信息,从而在调试时显示更具描述性的标记,而不是反汇编出来的原始地址。

参数说明及更多示例详见 readelf ELF 文件格式分析

size 查看内存占用

size 用来查看程序运行时各个段的实际内存占用:

$ size a.out
text data bss dec hex filename
1146 256 8 1410 582 a.out

file 查看文件类型

file 用于查看文件类型。比如在 64 位机器上发现了一个 32 位的库、链接不上,就能用它来排查:

$ file a.out
a.out: ELF 64-bit LSB executable, AMD x86-64, version 1 (SYSV), for GNU/Linux 2.6.9, dynamically linked (uses shared libs), for GNU/Linux 2.6.9, not stripped

也可以查看 core 文件是由哪个程序生成的:

$ file core.22355

strings 提取文本信息

一个文件中既包含二进制数据也包含文本数据,如果只想查看其中的文本信息,用 strings 很方便——它会过滤掉非字符数据,只输出文本:

$ strings <file>

fuser 查看文件使用者

fuser 显示所有正在使用指定文件、文件系统或套接字的进程:

$ fuser -m -u redis-server
redis-server: 11552rce(weber) 22912rce(weber) 25501rce(weber)

-m-u 选项用于查找所有正在使用 redis-server 的进程的 PID 及其属主。fuser 常被用来诊断 “resource busy” 问题;如果需要 kill 掉所有正在使用某个文件、文件系统或套接字的进程,可以使用 -k 选项:

$ fuser -k /path/to/your/filename

xxd 十六进制显示

以十六进制方式显示文件内容:

$ xxd a.out
0000000: 7f45 4c46 0101 0100 0000 0000 0000 0000 .ELF............
0000010: 0200 0300 0100 0000 3083 0408 3400 0000 ........0...4...
0000020: 3c11 0000 0000 0000 3400 2000 0900 2800 ...

以字符方式显示(od 命令):

$ od -c a.c
0000000 # i n c l u d e \n \n v o i d m a i n ( ) \n
0000040 { \n \t i n t i = 5 ; \n \t p
0000060 r i n t f ( " h e l l o , % d "
0000100 , i ) ; \n } \n
0000107

类似命令还有 hexdump(十六进制输出)。


   转载规则


《Linux 程序调试指南》 吴杭沉 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
What is the Shell? What is the Shell?
What Is “The Shell”?Simply put, the shell is a program that takes commands from the keyboard and gives them to the opera
2020-09-13
下一篇 
Linux 一切皆文件 Linux 一切皆文件
Unix 有一条著名的哲学——「一切皆文件」(everything is a file)。但严格来说,socket 和 pipe 并不完全符合这一点:它们虽然能被当作文件描述符来读写,却没有被纳入统一的目录树命名空间。本文从这一点出发,探讨
2020-08-20
  目录