本文介绍了在缺少符号表的Release版本中定位Core问题的实用方法。首先回顾了ELF文件格式、常用调试工具命令,然后详细讲解了进程内存映像与符号表的作用,最后提供了通过GDB附加符号、分析栈回溯以及函数地址符号化等一系列实战技巧。

在软件开发中,Debug版本虽然便于调试,但体积臃肿、运行速度慢。通常,Release版本会剥离符号表和调试信息以优化性能。然而,当Release版本发生崩溃(Core)且无法在Debug环境复现时,问题定位将变得困难。本文将帮助你掌握在缺少符号信息的环境下进行调试的核心技能。
所需基础知识涵盖以下几个方面:
- 常用命令
- obj文件与可执行文件段结构
- 进程内存映像
- 符号表与调试信息
- GDB命令
一、常用命令工具
以下介绍的命令均属于GNU Binutils套件,是进行Linux系统运维和底层调试的利器。
| 命令 |
用途 |
| ar |
创建、修改和提取归档文件(静态库) |
| nm |
列出目标文件中的符号 |
| objcopy |
复制和转换目标文件 |
| objdump |
显示目标文件信息 |
| strip |
丢弃符号 |
| addr2line |
将地址转换为文件名和行号 |
| readelf |
显示ELF格式文件的内容 |
1. ar
ar命令主要用于将目标文件打包成静态链接库。
常用参数:
-c:创建归档文件。
-r:将文件插入归档文件中。
-t:显示归档文件中包含的文件。
-v:显示详细信息。
使用示例:
- 创建静态库
$ ar -crv libmakefile_test.a cli.o lib.o resource.o
r - cli.o
r - lib.o
r - resource.o
- 使用静态库编译程序
$ gcc -o app_test_static ./app/main.c -I ./include -L ./ -lmakefile_test -static
$ gcc -o app_test_dynamic ./app/main.c -I ./include -L ./ -lmakefile_test
-I指定头文件搜索路径,-L指定库文件搜索路径,-l指定要链接的库名。
2. nm
nm命令用于列出目标文件中的符号。
常用参数:
-C:将C++符号名解码为用户级名字。
-l:显示符号所在文件名和行号(需调试信息)。
-n:按地址顺序排序。
-u:仅显示未定义符号。
--defined-only:仅显示已定义的符号。
示例:
// main.c
#include<stdio.h>
#include"../include/cli.h"
#include"../include/lib.h"
#include"../include/resource.h"
int main()
{
installcmd();
openlib();
initresource();
return 0;
}
$ nm -n main.o
U _GLOBAL_OFFSET_TABLE_
U initresource
U installcmd
U openlib
0000000000000000 T main

第二列符号含义:
| 符号 |
含义 |
| A |
绝对地址符号,链接时不改变 |
| B |
未初始化数据段(.bss) |
| D |
初始化数据段(.data) |
| R |
只读数据段(.rodata) |
| T |
代码段(.text)函数 |
| U |
未定义符号(需在其他文件中解析) |
3. objcopy
objcopy用于复制和转换目标文件,常用于分离调试信息。
常用参数:
--only-keep-debug:剥离出独立的调试信息文件。
--add-gnu-debuglink:向可执行文件添加.gnu_debuglink节,指向独立的调试文件。
示例:分离与附加调试信息
# 从algorithm分离出调试信息文件algorithm.dbg
$ objcopy --only-keep-debug algorithm algorithm.dbg
$ ls
algorithm algorithm.dbg
# 剥离原文件的符号表
$ strip -s algorithm
# 将algorithm.dbg作为独立调试文件链接到algorithm
$ objcopy --add-gnu-debuglink=$(pwd)/algorithm.dbg algorithm
# 确认.gnu_debuglink节已添加
$ readelf -S algorithm | grep gnu_debuglink
[28] .gnu_debuglink PROGBITS 0000000000000000 0000303c

4. objdump
objdump是强大的反汇编和文件信息查看工具。
常用参数:
-h:显示节头信息。
-d:反汇编特定节。
-S:混合显示源代码和汇编代码(需-g编译)。
-l:显示文件名和行号。
-s:显示指定节的完整内容。
-t:显示符号表。
示例:查看节头及归档文件信息
$ objdump -h algorithm
...
13 .text 00000151 00000000000010c0 00000000000010c0 000010c0 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
...
$ objdump -a libparallelmenu.a
In archive libparallelmenu.a:
parallelmenu.o: file format elf64-x86-64
rw-r--r-- 0/0 7216 Jan 1 08:00 1970 parallelmenu.o
parallelmenu_api.o: file format elf64-x86-64
rw-r--r-- 0/0 25872 Jan 1 08:00 1970 parallelmenu_api.o

5. strip
strip用于从目标文件中丢弃符号表和调试信息,减小文件体积。
常用参数:
示例:
$ strip -g algorithm # 仅去除调试符号,保留函数名
$ file algorithm
algorithm: ELF 64-bit LSB pie executable, x86-64, ... not stripped
$ strip -s algorithm # 去除所有符号表
$ file algorithm
algorithm: ELF 64-bit LSB pie executable, x86-64, ... stripped
-g选项移除调试节(如.debug_*),但保留.symtab和.strtab,因此nm仍能看到函数名,但GDB无法显示行号。-s选项会进一步移除.symtab和.strtab,仅保留动态链接必须的.dynsym和.dynstr。
6. addr2line
addr2line将运行时地址映射回源代码的文件名和行号,是分析崩溃日志的关键工具。
常用参数:
-a:显示十六进制地址。
-e:指定可执行文件。
-f:显示函数名。
-C:解码C++符号。
示例: 对于崩溃地址0x40077B:
addr2line -aCfse mleak 0x40077B
注意: 对于动态库中的地址,需要先减去该库在进程地址空间中的加载基址,得到在库文件内的偏移地址,再对库文件使用addr2line。
例如,从dmesg得到段错误信息:
[2022.242578] a.out[6050]: segfault at 0 ip 0000558c30b8f161 sp 00007ffd40f0e6f0 error 6 in a.out[558c30b8f000+1000]
崩溃地址为0x558c30b8f161,模块基址为0x558c30b8f000,则偏移地址为 0x161。使用此偏移进行查询。
7. readelf
readelf专门用于解析ELF格式文件,功能全面。
常用功能:
- 查看符号表:
readelf -Ws
- 查看节头信息:
readelf -S
- 查看程序头/段信息:
readelf -l
- 查看动态段信息:
readelf -d
- 查看DWARF调试信息:
readelf -w 或 --debug-dump
二、obj文件与可执行文件段结构
Linux下的目标文件(.o)和可执行文件均采用ELF格式,其核心结构如下:
- ELF头部:包含文件类型、机器架构、入口地址、程序头表和节头表偏移等元信息。
- 程序头表:描述段信息,用于指导系统加载器和动态链接器如何将文件映射到进程内存。
- 节头表:描述节信息,包含代码、数据、符号表、重定位表等,主要用于链接和调试。
- 节区:实际的数据区域,如
.text(代码)、.data(初始化数据)、.bss(未初始化数据)、.rodata(只读数据)等。
- 符号表:记录函数、变量等符号的名称、类型、大小和地址。
- 重定位表:记录链接时需要修正的地址信息。
三、进程内存映像
理解进程内存映像对于调试Core文件至关重要。代码段与数据段之间可能存在对齐空隙,而栈、堆、共享库的地址会受地址空间布局随机化影响,每次运行时都可能变化。
调试Core时,使用add-symbol-file加载带调试信息的库需要指定正确的.text段地址。这个地址应是动态库在Core产生时刻的加载基址加上该库.text节在文件内的偏移。我们的目标是让GDB将调试信息映射到Core中库代码实际所在的内存位置。
四、符号表与调试信息
符号表主要包含两个部分:
- .dynsym:动态符号表,保存了动态链接所需的符号(如引用的外部库函数),运行时必需。
- .symtab:静态符号表,保存了所有的本地和全局符号,用于调试和静态链接,发布时可被剥离。
调试信息则存储在.debug_*系列节中(现代编译器也可能使用.zdebug_*压缩格式),例如:
.debug_info:类型、变量、函数定义等高级调试信息。
.debug_line:源代码行号与机器码地址的映射。
.debug_str:调试信息中使用的字符串池。
编译选项-g生成调试信息,-rdynamic则将所有符号(包括静态符号)添加到动态符号表(.dynsym),这对backtrace_symbols()等运行时函数解析调用栈很有用。
五、GLibc版本查看
排查问题时可能需要确定C库版本,方法如下:
ldd --version
strings /lib/x86_64-linux-gnu/libc.so.6 | grep GLIBC
getconf GNU_LIBC_VERSION
# 直接运行libc.so.6(它是一个特殊可执行文件)
/lib/x86_64-linux-gnu/libc.so.6
六、GDB相关命令
1. add-symbol-file
此命令用于向已加载的Core文件或运行中的进程附加独立的调试符号文件。
(gdb) add-symbol-file FILE [ADDR] [-s SECT-NAME SECT-ADDR]…
ADDR是文件.text节的加载地址。对于Core文件中的库,这通常是info proc mappings显示的库起始地址加上该库.text节的文件偏移。
- 如果数据段(
.data)等与文本段不连续,需要用-s参数分别指定。
file命令用于初始加载可执行文件及其符号,而add-symbol-file用于后续增量添加其他模块(如动态库)的符号。
2. info proc mappings
显示进程的内存映射区域,即/proc/<pid>/maps的内容。可以查看各模块(主程序、共享库)在内存中的具体加载地址范围。
3. info sharedlibrary
显示当前已加载的共享库列表及其符号读取状态。其中“From”地址通常是该库代码段(.text)的加载起始地址。
4. info proc mappings 与 info sharedlibrary 的差异
info proc mappings:显示整个库文件被映射到内存的所有区域(可能包括代码、数据、只读数据等多个区间)。
info sharedlibrary:显示的“From”地址通常对应库的代码段(.text)的起始加载地址。
在计算add-symbol-file所需地址时,通常使用info sharedlibrary显示的地址,或者根据info proc mappings中库的起始地址加上.text节在文件内的偏移来计算。
5. x 与 disassemble 的异同
x:用于检查原始内存内容,可以按不同格式(指令、字节、字符串等)显示。
disassemble:专门用于反汇编指令,功能更专注,可显示源代码交织(/s或/m修饰符)。
七、coredump配置与使用
Core Dump是进程崩溃时的内存快照,是事后调试的关键。
查看coredump开启情况
ulimit -c
若输出为0,则表示core dump功能被关闭。
打开 coredump 功能
临时生效(仅当前Shell):
ulimit -c unlimited # 大小不受限
# 或
ulimit -c 1024 # 限制大小为1024个磁盘块
永久生效(修改系统配置):
编辑/etc/security/limits.conf文件,修改或添加如下行:
* soft core unlimited
设置coredump路径与命名
默认生成在当前目录,名为core。可通过修改/proc/sys/kernel/core_pattern来定制:
# 启用pid后缀
echo 1 > /proc/sys/kernel/core_uses_pid
# 设置生成路径和文件名格式(需root权限)
echo "/tmp/core-%e-%p-%t" > /proc/sys/kernel/core_pattern
%e可执行文件名,%p进程ID,%t崩溃时间戳。
离线查看core文件
将Core文件、对应的可执行文件及其调试符号(或未strip的版本)拷贝到同一调试环境。使用GDB加载Core文件进行分析。这正是前述add-symbol-file和objcopy --only-keep-debug技术的典型应用场景。
八、符号表剥离与附加实战
这是一个完整的离线调试工作流:
- 开发环境剥离符号:使用
objcopy --only-keep-debug生成独立的调试文件(.dbg)。
- 部署发布:将strip后的二进制文件部署到生产环境。
- 问题发生:生产环境产生Core文件。
- 离线分析:将Core文件和独立的
.dbg文件拿到开发环境,使用GDB的add-symbol-file命令附加符号进行调试。
关键步骤:确定.text段加载地址
- 从Core中用
info sharedlibrary或info proc mappings找到目标库的加载地址(例如0x00007ffff7fb2000)。
- 用
readelf -S libxxx.so或objdump -h libxxx.so查看该库的.text节在文件内的偏移(例如Offset为0x12c0)。
- 计算
.text的实际加载地址:库加载基址 + .text节文件偏移。有时这个地址就是info sharedlibrary显示的“From”地址,无需再加偏移,这与系统网络的地址随机化实现有关。
九、栈回溯原理
栈回溯是调试的核心,它揭示了函数调用链。其原理主要分两种:
-
基于DWARF CFI:
通过.eh_frame或.debug_frame节中的调用帧信息来重构栈。这些信息描述了如何从当前栈帧计算上一帧的基址(CFA)和返回地址。这是现代Linux系统默认且高效的方式,glibc必须保留.eh_frame以保证异常处理正常工作。
-
基于帧指针:
编译器通过-fno-omit-frame-pointer选项保留帧指针寄存器(如x86_64的RBP)。每个函数都会保存和更新帧指针,从而形成一个链式结构,便于回溯。这种方式有轻微性能开销。
当遇到栈回溯不完整时,在确认编译保留了.eh_frame或帧指针后,应首先怀疑栈数据被破坏或程序计数器(PC)值非法。
十、函数地址符号化
将运行时地址解析为函数名、文件名和行号,是分析日志或性能采样的基础。
核心步骤:
- 定位所属模块:通过地址判断它属于主程序还是哪个共享库(参考
/proc/[pid]/maps)。
- 计算文件内偏移:
地址 - 模块加载基址。
- 查找符号:
- 若有完整的
.symtab或.debug_info,可直接用addr2line -e <文件> <偏移>得到函数名和行号。
- 若符号表被剥离,但
.dynsym(动态符号表)还在,可通过readelf -Ws <文件>或objdump -T <文件>查找该偏移落在哪个动态符号的地址范围内,从而得到函数名(无法得到行号)。
- 若
.dynsym也被移除,则符号化将极其困难。
十一、函数劫持机制简介
函数劫持是实现内存泄漏检测等高级调试工具的基础,其方法按介入时机可分为:
- 编译链接期:
--wrap符号包装,适用于可重编的代码。
- 动态链接期:
- LD_PRELOAD:最常用。通过预加载一个定义了同名函数(如
malloc)的共享库,覆盖标准库的实现。在自定义函数内部可用dlsym(RTLD_NEXT, ...)调用原函数。
- LD_AUDIT:提供更细粒度的动态链接过程审计接口。
- 运行时:
- PLT/GOT Hook:修改过程链接表中的跳转地址。
- Inline Hook:直接修改函数入口处的指令。
- 调试器介入:通过
ptrace拦截进程执行流。
- 内核追踪:利用eBPF的uprobes在用户态函数入口处挂载探针,进行观测。
参考文章
- Linux环境Release版本的符号表剥离及调试方法
- Why does the same shared library show up multiple times when using
info proc mappings in gdb?
- Thread: addr2line doesn’t work - returns ??:0
- Linux下离线调试之coredump文件介绍
- strip,eu-strip 及其符号表,gdb调试strip过的程序