core文件在文章顶部
一、通过 core 文件中的内存片段可以看到:
$ strings core.45460|grep-C5"list_destory"list_delete()complete prepare into list_find()tofindnodeindex=%d..........---...---findwanted node:id=%d,name=%s,math=%d,chinese=%d not found indexid=%d, errno ret val=%d. list_find()complete prepare into list_destory()............list_destroy()complete :*3$" `g%P stu0 stu1 stu2 stu3 stu4 kKiN kKiN prepare into list_destory()............,chinese=27....---...--- @3CP L>?P V<- N搜索list_
xxxxxxxxxxxxxxxxx$ strings core.45460|grep-C5"list_"GLIBC_2.34 GLIBC_2.2.5 _ITM_deregisterTMCloneTable __gmon_start__ _ITM_registerTMCloneTable list_insert()'s ptr parameter is/are NULL malloc in list_insert() err, return NULL %d %s %d %d list_delete()'s ptr parameter is/are NULL list_find()'s ptr parameter is/are NULL stu%d err:inmain(), list_insert()returnerrno:%d prepare into list_delete()............ list_delete()complete prepare into list_find()tofindnodeindex=%d..........---...---findwanted node:id=%d,name=%s,math=%d,chinese=%d not found indexid=%d, errno ret val=%d. list_find()complete prepare into list_destory()............list_destroy()complete :*3$" `g%P stu0 stu1 stu2 stu3 stu4 kKiN kKiN prepare into list_destory()............,chinese=27....---...--- @3CP L>?P V<- N xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx$二、为什么能找到
你之所以能在没有 -g 的情况下得到深度分析,是因为:
Log 缓冲区:程序崩溃前最后打印的一段话往往还留在内存缓冲区里。
只读数据段:你代码里写的所有 printf 模版都存在 Core 文件里。
三、怎么推断出“崩溃发生在 list_destroy”?
这是通过分析内存中残留的日志顺序来推断的。内存中保留了程序输出缓冲区的历史记录。
证据来源:和 (这里是空白)
原文片段:(这里也是空白)
首先看到了:list_delete() complete —— 说明删除操作走完了。
然后看到了:list_find() complete —— 说明查找操作走完了。
最后看到了:prepare into list_destory() —— (注意这里的拼写是 destory)。
解读:这是日志流的最后一部分。既然程序在打印“准备进入销毁函数”后就没有新的正常日志,紧接着就出现了 double free 错误,那么崩溃一定发生在这个 list_destroy 函数的执行过程中。
它的**“生前遗言”(Log)、“作案工具”(GCC版本)、“受害者特征”(数据结构)以及“死亡原因”**(Double Free 报错)都留在了 core 文件里。
怎么知道是 “Double Free”?
这是最直接的证据。当 C 标准库(GLIBC)检测到内存错误时,它会生成一段报错文本并在崩溃前尝试写入内存(通常是 stderr 缓冲区)。这段文本被完整记录在了 core 文件中。
证据来源:`` (这里是空白)
原文片段:free(): double free detected in tcache 2
解读:这句话不是你的代码写的,而是 GLIBC 内存管理器在“临死前”喊出来的。哪怕没有源码,这句话也足以说明死因是“重复释放内存”。
没有 -g 导致我看不到函数的入口地址映射,但是你在代码中写了非常详细的 日志(printf)。这些日志中的字符串(如 “list_insert”)作为常量数据存储在二进制文件的 .rodata(只读数据段)中,并随着程序加载进入内存。
证据来源:`` (这里是空白)
原文片段: (这里是空白)
list_insert()'s ptr parameter is/are NULL
list_delete()'s ptr parameter is/are NULL
解读:我并不是通过调试符号看到了函数名,而是你自己在打印语句里写了函数名。这些字符串证明了这些函数的存在,以及你做了空指针检查。
- 怎么推断出“崩溃发生在 list_destroy”?
这是通过分析内存中残留的日志顺序来推断的。Core 文件通常包含程序输出缓冲区的内容。
证据来源:和 (这里是空白)
原文片段:(这里是空白)
首先看到了:list_delete() complete —— 说明删除操作走完了。
然后看到了:list_find() complete —— 说明查找操作走完了。
最后看到了:prepare into list_destory() —— (注意:你这里拼写成了 destory,这也是一个特征)。
解读:这是日志流的最后一部分。既然程序在打印“准备进入销毁函数”后就没有新的正常日志,紧接着就是乱码和错误信息,那么崩溃一定发生在这个 list_destroy 函数的执行过程中。
- 怎么知道数据结构有 id, name, math?
同样来源于你的 printf 格式化字符串。
证据来源:``
原文片段:find wanted node: id=%d, name=%s, math=%d, chinese=%d
解读:这行格式化字符串揭示了你定义的结构体(Node/Student)至少包含这四个成员变量。
没有加-g,怎么在gdb或者从core文件分析
即便没有 -g 选项,你依然可以在 GDB 中看到这些残留的字符串。
方法 1:在 GDB 中直接搜索字符串
如果你想在 Core 文件中查找特定的日志信息,可以使用 GDB 的 find 命令。
Bash
语法:find [/格式] 开始地址, 结束地址, “你要找的字符串”
示例:查找日志中的关键单词
(gdb) find 0x400000, 0x800000, “prepare into list_destory”
如果找到了,GDB 会返回该字符串所在的内存地址。
方法 2:查看特定地址附近的内存
如果你已经通过 bt(即使没有符号表,也能看到返回地址)定位到了崩溃附近的指令地址,可以用 x/s 命令将内存解析为字符串。
Bash
查看某个地址开始的字符串
(gdb) x/s 0x402010
x 代表 examine(检查内存)。
s 代表 string(字符串格式)。
方法 3:查看所有的字符串常量(最推荐)
其实,甚至不需要进入 GDB,在 Linux 命令行下使用 strings 命令是最快的方法。它可以提取出二进制文件或 Core 文件中所有可读的字符串。
# 查看 core 文件中所有 4 个字符及以上的连续字符串strings core.45460|grep"list_"你会看到诸如 list_insert() complete、list_delete() complete 这样的信息。正是这些信息暴露了程序的执行路径。
四、为什么strings core.45|grep “list_” 有2行“prepare into list_destory() … …”
- 核心原因:输出缓冲区(Stdout Buffer)的残留
在 Linux C 语言中,printf 或 puts 默认是行缓冲(Line Buffered)。
当你执行 puts(“prepare into list_destory() … …”); 时,这行字符串被放进了内存中的 标准输出缓冲区。
程序崩溃(Crash)时,缓冲区内的内容还没有被操作系统完全清理,因此在 core 文件的内存快照里,既存在于你定义的 .rodata(只读数据段)中,也存在于 stdout 的缓冲区内存中。
strings 命令会扫描整个 core 文件的二进制数据,所以它会把这块字符串在内存中出现的所有位置都抓出来。
五、gdb看不出什么内容:
(gdb)info proc mappings Mapped address spaces: Start Addr End Addr Size Offset objfile 0x55b747ec6000 0x55b747ec7000 0x1000 0x0 /home/y/lihuiqin/agf_v70_makefile_DS/agp_find_func_print_node_ptr_/headless_list 0x55b747ec7000 0x55b747ec8000 0x1000 0x1000 /home/y/lihuiqin/agf_v70_makefile_DS/agp_find_func_print_node_ptr_/headless_list 0x55b747ec8000 0x55b747ec9000 0x1000 0x2000 /home/y/lihuiqin/agf_v70_makefile_DS/agp_find_func_print_node_ptr_/headless_list 0x55b747ec9000 0x55b747eca000 0x1000 0x2000 /home/y/lihuiqin/agf_v70_makefile_DS/agp_find_func_print_node_ptr_/headless_list 0x55b747eca000 0x55b747ecb000 0x1000 0x3000 /home/y/lihuiqin/agf_v70_makefile_DS/agp_find_func_print_node_ptr_/headless_list 0x7f1450210000 0x7f1450238000 0x28000 0x0 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7f1450238000 0x7f14503cd000 0x195000 0x28000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7f14503cd000 0x7f1450425000 0x58000 0x1bd000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7f1450425000 0x7f1450426000 0x1000 0x215000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7f1450426000 0x7f145042a000 0x4000 0x215000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7f145042a000 0x7f145042c000 0x2000 0x219000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7f1450444000 0x7f1450446000 0x2000 0x0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7f1450446000 0x7f1450470000 0x2a000 0x2000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7f1450470000 0x7f145047b000 0xb000 0x2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7f145047c000 0x7f145047e000 0x2000 0x37000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7f145047e000 0x7f1450480000 0x2000 0x39000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(gdb)find0x55b747ec7000, 0x55b747ec8000,"prepare into list_destory"Pattern not found.(gdb)x/20s 0x55b747ec7000 0x55b747ec7000:""0x55b747ec7001:""0x55b747ec7002:""0x55b747ec7003:""0x55b747ec7004:""0x55b747ec7005:""0x55b747ec7006:""0x55b747ec7007:""0x55b747ec7008:""0x55b747ec7009:""0x55b747ec700a:""0x55b747ec700b:""0x55b747ec700c:""0x55b747ec700d:""0x55b747ec700e:""0x55b747ec700f:""0x55b747ec7010:""0x55b747ec7011:""0x55b747ec7012:""0x55b747ec7013:""链表数据: 程序处理的是结构化数据(stu0, stu1, stu2 等),包含 id、name、math、chinese 等字段 。
库文件: 使用了 libc.so.6 (GLIBC 2.34/2.35) 和 GCC 14.2.0 。
+1
执行环境: 运行在 Debian 6.12.43+deb13-amd64 内核环境下
根本原因推测
导致 double free 在 list_destroy() 中出现的常见场景包括:
节点指针重复: 链表结构可能因为逻辑错误(如 list_insert 或 list_delete 逻辑不当)导致两个不同的节点指针指向了同一个内存地址。当 list_destroy 遍历并释放每个节点时,该地址被释放了两次。
手动删除后未置 NULL: 在调用 list_delete() 删除某个节点后,如果程序逻辑中还保留了该节点的引用,并在最后的 list_destroy() 中再次尝试释放它。
循环引用: 如果链表意外形成了环状结构,销毁函数在遍历时可能会重复访问并释放同一个节点。
使用工具调试: 建议使用 Valgrind 运行程序,它可以精准定位哪一行代码进行了第二次无效的释放。
valgrind --tool=memcheck --leak-check=full ./headless_list