【C++面试】程序崩溃如何定位:从Segmentation fault到GDB、Core Dump与ASan

一、C++程序为什么会突然崩溃

C++ 相比一些高级语言,需要程序员自己处理更多:

复制代码
内存
指针
对象生命周期
线程同步

所以运行过程中可能出现:

复制代码
Segmentation fault

Aborted

程序异常退出

Access Violation

例如:

复制代码
int *p = nullptr;

*p = 10;

这里:

复制代码
p == nullptr

却对它进行:

复制代码
*p

访问,就可能直接导致程序崩溃。

再例如:

复制代码
int *p = new int(10);

delete p;

std::cout << *p << std::endl;

这里的问题是:

复制代码
内存已经释放
↓
但是继续访问

也就是:

复制代码
Use After Free

再比如:

复制代码
int arr[5] = {0};

arr[100] = 10;

这是:

复制代码
数组越界

这类问题更加麻烦,因为它不一定在:

复制代码
arr[100] = 10;

这一行立刻崩溃。

有可能:

复制代码
这里把别的内存破坏了
        ↓
程序继续运行
        ↓
过一段时间
        ↓
在另一个完全不同的地方崩溃

所以看到:

复制代码
程序崩在A函数

并不一定说明:

复制代码
真正的问题就在A函数

真正原因可能更早就已经发生了。

C++ 中常见崩溃原因可以简单归纳为:

复制代码
空指针访问

野指针

数组 / buffer越界

Use After Free

Double Free

错误的类型转换

对象生命周期错误

栈溢出

多线程数据竞争

库接口使用错误

因此程序崩溃以后,第一件事不应该是:

复制代码
看到哪个指针就加if(ptr)

而应该先确定:

复制代码
程序具体死在哪里?
调用路径是什么?
当时变量是什么状态?

二、第一步:先复现问题并找到崩溃位置

程序崩溃以后,排查流程首先应该是:

复制代码
能不能稳定复现?

例如:

复制代码
每次启动都会崩

这种最好排查。

而:

复制代码
运行几个小时才偶尔崩一次

就要更多依赖:

复制代码
日志
Core Dump

所以先记录:

复制代码
什么输入会触发?

执行到哪一步会触发?

必现还是偶现?

单线程还是多线程?

Debug版本和Release版本是否都出现?

如果问题能够稳定复现,可以先通过日志快速缩小范围。

例如:

复制代码
void process() {
    printf("1. enter process\n");

    loadData();

    printf("2. loadData finished\n");

    parseData();

    printf("3. parseData finished\n");
}

运行以后只有:

复制代码
1. enter process
2. loadData finished

随后崩溃。

那么至少可以判断:

复制代码
崩溃发生在parseData()
或者parseData()内部调用的函数

问题范围马上就缩小了。

实际项目中的日志最好带上:

复制代码
时间
线程ID
函数名
关键变量
错误码

例如:

复制代码
printf("[parseData] buffer=%p, len=%d\n", buffer, len);

如果日志显示:

复制代码
buffer = 0x0
len = 1024

就已经非常可疑了。

但如果能够直接 Debug,通常下一步就应该:

复制代码
使用调试器定位准确崩溃位置

Windows 下可以使用:

复制代码
Visual Studio Debugger

Linux 下则经常使用:

复制代码
GDB

三、Linux下使用GDB定位崩溃

假设有这样一段代码:

复制代码
#include <iostream>

void test(int *p) {
    *p = 100;
}

int main() {
    int *ptr = nullptr;

    test(ptr);

    return 0;
}

编译时加:

复制代码
g++ -g main.cpp -o app

其中:

复制代码
-g

表示:

复制代码
加入调试信息

然后:

复制代码
gdb ./app

进入 GDB。

运行:

复制代码
run

程序可能直接停在:

复制代码
Program received signal SIGSEGV

也就是:

复制代码
Segmentation Fault
段错误

这时候首先看:

复制代码
bt

也就是:

复制代码
backtrace

调用栈可能显示:

复制代码
#0  test(int*) at main.cpp:4
#1  main() at main.cpp:10

这句话实际上已经告诉我们:

复制代码
main()
  ↓
调用test()
  ↓
test第4行崩溃

所以:

复制代码
bt

通常是程序崩溃后最重要的命令之一。

可以继续:

复制代码
frame 0

切换到:

复制代码
第0层栈帧

再:

复制代码
p p

查看变量。

可能得到:

复制代码
$1 = (int *) 0x0

马上就发现:

复制代码
p是空指针

完整思路:

复制代码
程序崩溃
↓
GDB停在崩溃位置
↓
bt
↓
看调用栈
↓
frame
↓
切换到相关函数
↓
p 变量
↓
检查参数和局部变量

常用几个 GDB 命令可以记住:

复制代码
run / r
↓
运行程序

bt
↓
查看调用栈

frame n
↓
切换栈帧

p variable
↓
查看变量

list
↓
查看源码

info locals
↓
查看当前局部变量

info threads
↓
查看线程

如果程序还没有崩,但想观察某个位置:

复制代码
b test

设置断点。

然后:

复制代码
run

再:

复制代码
next
step

逐步执行。

所以如果面试问:

程序发生 Segmentation fault 怎么查?

一个非常标准的回答就是:

如果可以稳定复现,我通常先使用 GDB 运行程序,崩溃后通过 bt 查看调用栈,确定崩溃函数和调用路径,再用 frame 切换栈帧,通过 print、info locals 查看当时的指针、参数和局部变量状态。

四、偶现崩溃怎么用Core Dump和ASan

有些程序不能直接一直挂在 GDB 下面。

例如:

复制代码
服务器运行了8个小时
突然崩溃

或者:

复制代码
客户现场偶尔崩一次

这种问题就非常适合:

复制代码
Core Dump

Core Dump 可以理解成:

复制代码
程序崩溃瞬间的现场快照

程序崩溃时,把当时的一部分:

复制代码
内存状态
寄存器状态
线程状态
调用栈

保存下来。

之后再分析。

Linux 中可以先检查:

复制代码
ulimit -c

如果:

复制代码
0

说明当前不允许生成 Core 文件。

可以临时:

复制代码
ulimit -c unlimited

程序崩溃以后,如果获得:

复制代码
core

文件,就可以:

复制代码
gdb ./app core

然后最先:

复制代码
bt

如果是多线程程序,可以:

复制代码
info threads

然后:

复制代码
thread apply all bt

把所有线程的调用栈都打印出来。

例如发现:

复制代码
Thread 1
↓
崩在memcpy()

Thread 2
↓
正在释放某个对象

那么就要怀疑:

复制代码
线程1正在使用资源
线程2已经把资源释放了

也就是:

复制代码
生命周期
+
并发竞争

问题。


但有些内存错误,单纯 GDB 不一定容易定位。

例如:

复制代码
int *p = new int[10];

p[100] = 20;

越界可能先破坏别的内存:

复制代码
真正越界的位置
↓
没有立即崩

几十行代码以后
↓
程序才崩

这时候非常适合使用:

复制代码
AddressSanitizer
ASan

编译:

复制代码
g++ -g -fsanitize=address main.cpp -o app

然后正常运行:

复制代码
./app

如果发生数组越界,ASan 往往会直接告诉你:

复制代码
heap-buffer-overflow

并给出:

复制代码
出错行号
调用栈
非法访问地址

ASan 特别适合检查:

复制代码
Heap Buffer Overflow

Stack Buffer Overflow

Use After Free

Double Free

部分内存泄漏

例如:

复制代码
int *p = new int(10);

delete p;

*p = 20;

可能得到:

复制代码
heap-use-after-free

这比单纯看到:

复制代码
Segmentation fault

有效得多。

所以实际排查崩溃时:

复制代码
GDB
↓
回答"程序崩在哪里"

ASan
↓
回答"是不是更早发生了内存破坏"

两者经常结合使用。

五、程序崩溃时一套完整的排查思路

实际工作中,可以按照固定顺序排查。

首先:

复制代码
1. 收集现场

不要一出问题就马上改代码。

先记录:

复制代码
错误信息
日志
输入数据
线程情况
崩溃时间
Core Dump

然后:

复制代码
2. 判断能否复现

如果:

复制代码
稳定复现
↓
直接Debugger / GDB

如果:

复制代码
偶现
↓
日志 + Core Dump

接下来:

复制代码
3. 看调用栈

例如:

复制代码
bt

找到:

复制代码
崩溃函数
+
上层调用路径

然后:

复制代码
4. 检查关键变量

重点看:

复制代码
指针是否为空

地址是否合法

数组下标是否越界

长度参数是否异常

对象是否已经析构

返回值是否正确

线程之间是否同时操作资源

如果看到:

复制代码
memcpy(dst, src, len);

崩溃,不要马上认为:

复制代码
memcpy有问题

而应该重点检查:

复制代码
dst是否有效?

src是否有效?

len是否正确?

buffer是否已经释放?

因为库函数通常只是:

复制代码
最后触发问题的地方

而不是:

复制代码
真正制造错误的地方

如果怀疑内存破坏:

复制代码
5. 开ASan

例如:

复制代码
-fsanitize=address

检查:

复制代码
越界
UAF
Double Free

如果是多线程程序,还要继续检查:

复制代码
6. 生命周期和线程问题

例如:

复制代码
线程A正在使用对象
       ↓
线程B把对象delete
       ↓
线程A继续访问
       ↓
崩溃

这种问题可能表现为:

复制代码
偶现崩溃
地址每次都不同
Release更容易出现

可以配合:

复制代码
GDB线程调用栈
ThreadSanitizer
日志中的线程ID

排查。

所以完整流程可以记成:

复制代码
程序崩溃
   ↓
先保存现场
   ↓
能否复现?
   ↓
┌──────────────┐
│              │
能            不能
│              │
GDB          Core Dump
│              │
└──────┬───────┘
       ↓
     bt调用栈
       ↓
确定崩溃位置
       ↓
检查指针 / 下标 / 生命周期
       ↓
怀疑内存错误
       ↓
ASan
       ↓
怀疑多线程
       ↓
线程栈 / TSan
       ↓
找到真正根因

这里最重要的一个思想就是:

复制代码
崩溃位置
≠
问题产生位置

例如:

复制代码
第100行数组越界
      ↓
破坏堆内存
      ↓
程序继续运行
      ↓
第500行malloc时崩溃

真正的问题其实在:

复制代码
第100行

而不是:

复制代码
malloc

所以不能只盯着:

复制代码
最后崩溃的那一行

还要结合:

复制代码
调用栈
内存检测工具
日志
对象生命周期

一起判断。

如果面试官问:

程序发生崩溃时,通常如何定位问题?

可以直接回答:

我一般先保留日志、错误信息和输入条件,确认问题是否能够稳定复现。如果可以复现,会使用 GDB 或 IDE 调试器,在崩溃以后通过调用栈确定崩溃位置,再检查相关参数、指针和对象状态;如果是线上或偶现崩溃,会保存 Core Dump,再通过 GDB 分析。如果怀疑数组越界、Use After Free、Double Free 等内存问题,会进一步使用 AddressSanitizer。多线程程序还会检查各线程调用栈和对象生命周期,避免只根据最后崩溃的位置判断问题。

如果继续问:

Segmentation fault最常见的原因有哪些?

可以回答:

复制代码
空指针

野指针

数组越界

Use After Free

栈溢出

对象生命周期错误

多线程并发访问导致内存被破坏

如果继续问:

为什么程序崩在memcpy,不一定是memcpy的问题?

可以回答:

memcpy 只是执行内存访问的地方,真正的问题可能是上层传入了非法的源地址、目标地址或错误的长度,也可能是这块内存更早已经被释放或越界破坏。因此需要沿调用栈向上检查参数来源,而不能只看最后崩溃的库函数。

最后把面试中需要记住的几个关键词串起来:

复制代码
崩溃
↓
复现
↓
日志
↓
GDB
↓
bt调用栈
↓
Core Dump
↓
ASan
↓
指针 / 越界 / 生命周期
↓
找到Root Cause

程序崩溃排查真正考察的不是会不会背几个 GDB 命令,而是能不能做到:

复制代码
先保存证据
↓
定位现场
↓
缩小范围
↓
找到真正根因

而不是看到哪里崩了,就只修改哪里。

0voice · GitHub

相关推荐
汉克老师1 小时前
GESP2026年9月认证C++七级( 第三部分编程题(1、必经之路))精讲
c++·gesp·小学生·学c++编程
“AI国潮设计-小江”1 小时前
《Python+SDXL实战:用ControlNet精准控制“英歌舞戚风蛋糕”质感,附批量生成脚本》
开发语言·人工智能·python·prompt·aigc
1024奇点1 小时前
【仓颉语言入门 · 第26课】
开发语言·ide·开源
jayhgq1 小时前
新一代Python包与项目管理工具——UV
开发语言·python·uv
xixiaoyunya1 小时前
CPU 100% 本身不是问题,它是一个症状
开发语言·php
ebiobiz1 小时前
Zig 嵌入式底层库选择与 build.zig 组织指南
c语言·c++·嵌入式硬件·mcu
weixin_440401691 小时前
质朴的可视化绘图+pyecharts
开发语言·python·信息可视化·pyecharts
ss2731 小时前
AI全栈实战 | 3.3-01 Python OOP:__init__ 真的是构造函数吗,元类怎么让 Django Model 变魔法
开发语言·python·django
漂流瓶jz2 小时前
UVA-11491 奖品的价值 题解答案代码 算法竞赛入门经典第二版
c++·算法·图论·题解·aoapc·算法竞赛入门经典·uva