SIGSEGV (11) 段错误详解:C++ 开发者的崩溃调试指南

一、信号基础认知

信号核心信息

  • 信号编号:11
  • 信号名称:SIGSEGV (Segmentation Violation)
  • POSIX 标准:是(POSIX.1-2001 定义)
  • 可捕获:是
  • 默认行为:终止进程并生成 coredump

核心定位

SIGSEGV 的本质作用是内存访问违规告警。当进程尝试访问不属于它的内存区域,或者访问被保护的内存区域时,操作系统会发送 SIGSEGV 信号。

默认行为

Linux 内核的默认处理逻辑:

  • 终止进程:立即终止当前进程
  • 生成 coredump:如果系统配置允许,会生成 core 文件用于后续调试
  • 不可忽略:虽然可以捕获,但默认行为是终止

与 C++ 的关联性

SIGSEGV 在 C++ 开发中是最常见的崩溃信号,高发场景包括:

  1. STL 容器使用std::vectorstd::string 等容器的越界访问
  2. 指针操作:空指针解引用、野指针访问、悬垂指针
  3. 多线程内存共享:数据竞争、未同步的内存访问
  4. 智能指针误用std::shared_ptrstd::unique_ptr 的循环引用或提前释放
  5. 内存管理:使用已释放的内存、重复释放、内存泄漏导致的堆损坏

二、信号触发场景

核心触发原因

1. 编程失误类

场景 1.1:空指针解引用
1
2
3
4
5
6
7
8
9
10
11
12
13
// 错误代码
#include <iostream>

void processData(int* data) {
*data = 100; // 如果 data 为 nullptr,触发 SIGSEGV
std::cout << "Data: " << *data << std::endl;
}

int main() {
int* ptr = nullptr;
processData(ptr); // 崩溃点
return 0;
}

Coredump 信息示例

1
2
3
4
5
6
Program received signal SIGSEGV, Segmentation fault.
0x0000000000401123 in processData(int*) (data=0x0) at main.cpp:5
5 *data = 100;
(gdb) bt
#0 0x0000000000401123 in processData(int*) (data=0x0) at main.cpp:5
#1 0x0000000000401145 in main () at main.cpp:10
场景 1.2:数组越界访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 错误代码
#include <iostream>
#include <vector>

int main() {
std::vector<int> vec = {1, 2, 3};
int value = vec[10]; // 越界访问,可能触发 SIGSEGV
std::cout << value << std::endl;

// 更危险的场景:使用指针算术
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
p += 100; // 越界指针
*p = 42; // 触发 SIGSEGV
return 0;
}
场景 1.3:使用已释放的内存
1
2
3
4
5
6
7
8
9
10
11
12
13
// 错误代码
#include <iostream>

int main() {
int* ptr = new int(42);
delete ptr;

// 使用已释放的内存
*ptr = 100; // 触发 SIGSEGV(悬垂指针)
std::cout << *ptr << std::endl;

return 0;
}

Coredump 信息示例

1
2
3
4
5
6
7
8
9
10
11
12
Program received signal SIGSEGV, Segmentation fault.
0x0000000000401156 in main () at main.cpp:8
8 *ptr = 100;
(gdb) info registers
rax 0x0 0
rbx 0x5555555592a0 93824992247840
rcx 0x0 0
rdx 0x64 100
rsi 0x7fffffffdd40 140737488346944
rdi 0x1 1
rbp 0x7fffffffdd20 0x7fffffffdd20
rsp 0x7fffffffdd18 0x7fffffffdd18
场景 1.4:STL 容器迭代器失效
1
2
3
4
5
6
7
8
9
10
11
12
13
// 错误代码
#include <iostream>
#include <vector>

int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin();

vec.push_back(6); // 可能导致重新分配,迭代器失效

*it = 10; // 使用失效的迭代器,可能触发 SIGSEGV
return 0;
}

2. 系统限制类

场景 2.1:栈溢出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 错误代码:递归过深导致栈溢出
#include <iostream>

void recursiveFunction(int depth) {
int largeArray[1000]; // 栈上分配大数组
if (depth > 0) {
recursiveFunction(depth - 1); // 无限递归
}
}

int main() {
recursiveFunction(10000); // 栈溢出,触发 SIGSEGV
return 0;
}

Coredump 信息示例

1
2
3
4
5
6
7
8
Program received signal SIGSEGV, Segmentation fault.
0x0000000000401120 in recursiveFunction(int) (depth=...) at main.cpp:5
5 int largeArray[1000];
(gdb) bt
#0 0x0000000000401120 in recursiveFunction(int) (depth=...) at main.cpp:5
#1 0x0000000000401135 in recursiveFunction(int) (depth=...) at main.cpp:7
#2 0x0000000000401135 in recursiveFunction(int) (depth=...) at main.cpp:7
... (重复多次)

3. 外部触发类

场景 3.1:多线程数据竞争
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 错误代码:未同步的内存访问
#include <iostream>
#include <thread>
#include <vector>

int shared_data = 0;

void increment() {
for (int i = 0; i < 100000; ++i) {
shared_data++; // 数据竞争,可能导致内存损坏
}
}

int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment);
}

for (auto& t : threads) {
t.join();
}

// 在某些情况下,数据竞争可能导致内存损坏,触发 SIGSEGV
std::cout << shared_data << std::endl;
return 0;
}

4. 运行时异常类

场景 4.1:智能指针循环引用导致的内存问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 错误代码:循环引用导致内存管理问题
#include <memory>
#include <iostream>

struct Node {
std::shared_ptr<Node> next;
int value;

~Node() {
std::cout << "Node destroyed" << std::endl;
}
};

int main() {
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

node1->next = node2;
node2->next = node1; // 循环引用

// 虽然这里不会直接触发 SIGSEGV,但可能导致后续内存问题
// 如果后续代码错误地访问了已释放的内存,会触发 SIGSEGV
return 0;
}

易混淆场景辨析

SIGSEGV vs SIGBUS

SIGSEGV:地址无效(访问了不属于进程的内存空间)

1
2
int* ptr = nullptr;
*ptr = 42; // SIGSEGV:地址 0x0 无效

SIGBUS:地址有效但访问权限不足(对齐错误、访问只读内存等)

1
2
3
4
// 在某些架构上(如 SPARC)
char buffer[100];
int* aligned_ptr = (int*)(buffer + 1); // 未对齐的地址
*aligned_ptr = 42; // SIGBUS:地址有效但对齐错误

三、崩溃调试与定位

基础定位:core 文件 + gdb 调试

步骤 1:开启 core 文件

1
2
3
4
5
6
7
8
9
# 检查当前限制
ulimit -c

# 启用 coredump(unlimited)
ulimit -c unlimited

# 验证
ulimit -c
# 输出:unlimited

步骤 2:gdb 加载 core 文件

1
2
3
4
5
6
7
8
9
10
# 编译时添加调试信息
g++ -g -O0 -o program main.cpp

# 运行程序(会崩溃并生成 core 文件)
./program

# 使用 gdb 加载 core 文件
gdb ./program core
# 或
gdb ./program core.12345

步骤 3:关键调试命令

1
2
3
4
5
6
7
8
9
10
11
(gdb) bt                    # 查看调用栈(最重要)
(gdb) bt full # 查看完整调用栈,包括局部变量
(gdb) info registers # 查看寄存器信息
(gdb) list # 查看崩溃位置的源代码
(gdb) print variable # 打印变量值
(gdb) print *ptr # 打印指针指向的内容
(gdb) x/10x $rsp # 查看栈内存(16进制)
(gdb) info locals # 查看局部变量
(gdb) frame 1 # 切换到上一层栈帧
(gdb) up # 向上移动栈帧
(gdb) down # 向下移动栈帧

实际调试示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ gdb ./sigsegv_example core
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
...
Core was generated by `./sigsegv_example'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000000000401123 in processData(int*) (data=0x0) at main.cpp:5
5 *data = 100;

(gdb) bt
#0 0x0000000000401123 in processData(int*) (data=0x0) at main.cpp:5
#1 0x0000000000401145 in main () at main.cpp:10

(gdb) print data
$1 = (int *) 0x0

(gdb) info registers
rax 0x0 0
rbx 0x5555555592a0 93824992247840
rcx 0x0 0
rdx 0x64 100
rsi 0x7fffffffdd40 140737488346944
rdi 0x0 0 # rdi 是第一个参数,值为 0(空指针)

进阶工具

工具 1:Address Sanitizer (ASan)

1
2
3
4
5
# 编译时启用 ASan
g++ -g -fsanitize=address -fno-omit-frame-pointer -o program main.cpp

# 运行程序(ASan 会在崩溃前提供详细信息)
./program

ASan 输出示例

1
2
3
4
5
6
7
8
9
10
==12345==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000
==12345==The signal is caused by a WRITE memory access.
==12345==Hint: address points to the null pointer
#0 0x401123 in processData(int*) main.cpp:5
#1 0x401145 in main main.cpp:10
#2 0x7f8b8c5d0b97 in __libc_start_main
#3 0x4010aa in _start

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV main.cpp:5 in processData(int*)

工具 2:Valgrind

1
2
# 使用 Valgrind 检测内存错误
valgrind --tool=memcheck --leak-check=full ./program

Valgrind 输出示例

1
2
3
4
5
6
==12345== Invalid write of size 4
==12345== at 0x401123: processData(int*) (main.cpp:5)
==12345== by 0x401145: main (main.cpp:10)
==12345== Address 0x0 is not stack'd, malloc'd or (recently) free'd
==12345==
==12345== Process terminating with default action of signal 11 (SIGSEGV)

工具 3:strace 跟踪系统调用

1
2
# 跟踪系统调用(虽然 SIGSEGV 不是系统调用问题,但可以查看程序执行流程)
strace -e trace=all ./program 2>&1 | tail -20

定位关键点

SIGSEGV 崩溃的核心排查方向:

  1. 检查指针是否为 nullptr:查看寄存器或变量值
  2. 检查数组/容器边界:确认索引是否越界
  3. 检查内存生命周期:确认内存是否已被释放
  4. 检查多线程同步:确认是否存在数据竞争
  5. 检查栈溢出:查看调用栈深度和局部变量大小

四、崩溃修复方案

分场景修复代码

场景 1:空指针解引用

快速修复:加判空
1
2
3
4
5
6
7
8
9
10
11
12
13
// 修复前
void processData(int* data) {
*data = 100; // 崩溃点
}

// 快速修复
void processData(int* data) {
if (data == nullptr) {
std::cerr << "Error: null pointer" << std::endl;
return;
}
*data = 100;
}
优雅修复:使用引用或智能指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 优雅修复方案 1:使用引用(如果指针不能为空)
void processData(int& data) { // 引用不能为空
data = 100;
}

// 优雅修复方案 2:使用智能指针
#include <memory>

void processData(std::shared_ptr<int> data) {
if (!data) {
throw std::invalid_argument("data cannot be null");
}
*data = 100;
}

// 优雅修复方案 3:使用 std::optional (C++17)
#include <optional>

void processData(std::optional<int>& data) {
if (!data.has_value()) {
throw std::invalid_argument("data has no value");
}
data.value() = 100;
}

场景 2:数组越界访问

快速修复:边界检查
1
2
3
4
5
6
7
8
9
10
11
// 修复前
std::vector<int> vec = {1, 2, 3};
int value = vec[10]; // 越界

// 快速修复
std::vector<int> vec = {1, 2, 3};
if (10 < vec.size()) {
int value = vec[10];
} else {
std::cerr << "Index out of bounds" << std::endl;
}
优雅修复:使用 at() 或范围检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 优雅修复方案 1:使用 at()(会抛出异常)
#include <stdexcept>

std::vector<int> vec = {1, 2, 3};
try {
int value = vec.at(10); // 越界会抛出 std::out_of_range
} catch (const std::out_of_range& e) {
std::cerr << "Index out of bounds: " << e.what() << std::endl;
}

// 优雅修复方案 2:使用范围检查函数
template<typename Container>
auto safe_at(Container& c, size_t index) -> decltype(c[0]) {
if (index >= c.size()) {
throw std::out_of_range("Index out of bounds");
}
return c[index];
}

// 使用
int value = safe_at(vec, 10);

场景 3:使用已释放的内存

快速修复:置空指针
1
2
3
4
5
6
7
8
9
10
11
12
// 修复前
int* ptr = new int(42);
delete ptr;
*ptr = 100; // 使用已释放的内存

// 快速修复
int* ptr = new int(42);
delete ptr;
ptr = nullptr; // 置空,后续访问会立即发现问题
if (ptr != nullptr) {
*ptr = 100;
}
优雅修复:使用智能指针
1
2
3
4
5
6
7
8
9
10
11
12
// 优雅修复:使用 std::unique_ptr
#include <memory>

{
auto ptr = std::make_unique<int>(42);
// 自动管理内存,离开作用域自动释放
*ptr = 100;
} // ptr 在这里自动释放,无法再访问

// 如果需要共享所有权
auto shared_ptr = std::make_shared<int>(42);
// 多个地方可以安全地共享这个指针

场景 4:STL 容器迭代器失效

快速修复:重新获取迭代器
1
2
3
4
5
6
7
8
9
10
11
// 修复前
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin();
vec.push_back(4); // 可能导致重新分配
*it = 10; // 使用失效的迭代器

// 快速修复
std::vector<int> vec = {1, 2, 3};
vec.push_back(4);
auto it = vec.begin(); // 重新获取迭代器
*it = 10;
优雅修复:使用索引或范围 for
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 优雅修复方案 1:使用索引
std::vector<int> vec = {1, 2, 3};
vec.push_back(4);
if (!vec.empty()) {
vec[0] = 10; // 使用索引,不会失效
}

// 优雅修复方案 2:使用范围 for(C++11)
std::vector<int> vec = {1, 2, 3};
vec.push_back(4);
for (auto& value : vec) { // 范围 for 自动处理迭代器
value = 10;
break; // 只修改第一个
}

修复验证

单元测试覆盖异常场景

1
2
3
4
5
6
7
8
9
10
11
#include <gtest/gtest.h>

TEST(PointerTest, NullPointerHandling) {
int* ptr = nullptr;
EXPECT_THROW(processData(ptr), std::invalid_argument);
}

TEST(VectorTest, OutOfBoundsAccess) {
std::vector<int> vec = {1, 2, 3};
EXPECT_THROW(vec.at(10), std::out_of_range);
}

压测复现崩溃条件

1
2
3
4
5
6
7
8
9
10
11
// 压力测试:重复触发边界条件
void stressTest() {
for (int i = 0; i < 1000000; ++i) {
std::vector<int> vec(1000);
try {
int value = vec.at(1001); // 应该被捕获
} catch (const std::out_of_range&) {
// 正常处理
}
}
}

避坑提醒

  1. 不要忽略 SIGSEGV:捕获 SIGSEGV 后必须正确处理,不能简单地忽略,否则可能导致数据损坏
  2. 避免在信号处理函数中分配内存:信号处理函数应该是异步信号安全的
  3. 不要依赖未定义行为:即使程序没有崩溃,未定义行为也是危险的

五、长期预防策略(从编码到部署全链路)

编码规范

C++ 开发中规避 SIGSEGV 的编码习惯:

  1. 指针使用前必判空

    1
    2
    3
    if (ptr != nullptr) {
    *ptr = value;
    }
  2. 禁用野指针:初始化所有指针

    1
    int* ptr = nullptr;  // 总是初始化
  3. 优先使用引用而非指针:引用不能为空

    1
    void func(int& value);  // 而不是 int* value
  4. 使用容器而非裸数组std::vector 提供边界检查

    1
    std::vector<int> arr;  // 而不是 int arr[100]
  5. 使用智能指针管理内存:自动管理生命周期

    1
    std::unique_ptr<int> ptr = std::make_unique<int>(42);

编译阶段

开启防御性编译选项:

1
2
3
4
5
6
7
# 完整的防御性编译选项
g++ -Wall -Wextra -Werror \
-fsanitize=address \
-fsanitize=undefined \
-fno-omit-frame-pointer \
-g -O0 \
-o program main.cpp
  • -Wall -Wextra:启用所有警告
  • -Werror:将警告视为错误
  • -fsanitize=address:启用 Address Sanitizer
  • -fsanitize=undefined:检测未定义行为
  • -fno-omit-frame-pointer:保留帧指针,便于调试
  • -g:保留调试信息
  • -O0:禁用优化,便于调试

测试策略

针对性测试用例设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 边界值测试
TEST(BoundaryTest, EmptyVector) {
std::vector<int> vec;
EXPECT_THROW(vec.at(0), std::out_of_range);
}

// 异常注入测试
TEST(ExceptionTest, NullPointerInjection) {
int* ptr = nullptr;
EXPECT_DEATH(*ptr = 42, ".*");
}

// 压力测试
TEST(StressTest, LargeAllocation) {
for (int i = 0; i < 1000; ++i) {
std::vector<int> vec(1000000);
// 测试大量分配是否导致问题
}
}

线上监控

提前感知崩溃风险:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <signal.h>
#include <execinfo.h>
#include <iostream>

void segfaultHandler(int sig) {
void* array[10];
size_t size = backtrace(array, 10);

// 打印调用栈
std::cerr << "SIGSEGV caught! Stack trace:" << std::endl;
backtrace_symbols_fd(array, size, STDERR_FILENO);

// 记录日志
// logToFile("SIGSEGV occurred", array, size);

// 退出
exit(1);
}

int main() {
signal(SIGSEGV, segfaultHandler);
// 程序代码
return 0;
}

工具赋能

推荐 C++ 专用静态检查工具:

Clang-Tidy

1
2
3
4
5
6
7
8
9
10
11
12
13
# 安装
sudo apt-get install clang-tidy

# 使用
clang-tidy main.cpp -- -std=c++17

# 配置 .clang-tidy 文件
Checks: >
-*,cppcoreguidelines-*,
-*,readability-*,
-*,performance-*,
bugprone-*,
modernize-*

Cppcheck

1
2
3
4
5
# 安装
sudo apt-get install cppcheck

# 使用
cppcheck --enable=all --std=c++17 main.cpp

六、拓展延伸(加深理解)

相关信号对比

SIGSEGV vs SIGBUS

特性 SIGSEGV SIGBUS
触发原因 地址无效 地址有效但访问权限不足
常见场景 空指针、越界 对齐错误、只读内存写入
可捕获
默认行为 终止+core 终止+core

SIGSEGV vs SIGABRT

  • SIGSEGV:由操作系统内核触发,表示内存访问违规
  • SIGABRT:由程序自身触发(如 abort()),表示程序主动中止

进阶技巧:用户态自定义信号处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <signal.h>
#include <execinfo.h>
#include <cxxabi.h>
#include <iostream>
#include <string>
#include <sstream>

void printStackTrace() {
void* array[50];
int size = backtrace(array, 50);
char** messages = backtrace_symbols(array, size);

std::cerr << "Stack trace:" << std::endl;
for (int i = 0; i < size; ++i) {
std::cerr << " [" << i << "] " << messages[i] << std::endl;
}
free(messages);
}

void segfaultHandler(int sig, siginfo_t* info, void* context) {
std::cerr << "=== SIGSEGV Detected ===" << std::endl;
std::cerr << "Signal: " << sig << std::endl;
std::cerr << "Fault address: " << info->si_addr << std::endl;
std::cerr << "Fault code: " << info->si_code << std::endl;

printStackTrace();

// 可以在这里保存状态、发送告警等
// 然后退出
exit(1);
}

int main() {
struct sigaction sa;
sa.sa_sigaction = segfaultHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO;

sigaction(SIGSEGV, &sa, nullptr);

// 程序代码
int* ptr = nullptr;
*ptr = 42; // 会触发自定义处理函数

return 0;
}

实际案例分享

案例 1:STL 容器迭代器失效导致的崩溃

问题描述:在高并发场景下,程序偶尔崩溃,coredump 显示 SIGSEGV。

排查过程

  1. GDB 分析显示崩溃在 std::vector::operator[]
  2. 检查调用栈,发现崩溃前有 push_back 操作
  3. 使用 ASan 复现问题,发现迭代器失效

根本原因

1
2
3
4
5
6
7
// 问题代码
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (condition) {
vec.push_back(newValue); // 导致重新分配,迭代器失效
}
process(*it); // 使用失效的迭代器
}

解决方案

1
2
3
4
5
6
7
// 修复代码
for (size_t i = 0; i < vec.size(); ++i) {
if (condition) {
vec.push_back(newValue);
}
process(vec[i]); // 使用索引,不会失效
}

案例 2:多线程数据竞争导致的内存损坏

问题描述:多线程程序在压力测试时随机崩溃。

排查过程

  1. 使用 ThreadSanitizer 检测到数据竞争
  2. 发现多个线程同时访问共享数据结构
  3. 内存损坏导致后续访问触发 SIGSEGV

根本原因

1
2
3
4
5
6
// 问题代码
std::vector<int> shared_vec;

void threadFunc() {
shared_vec.push_back(42); // 多线程同时访问,未加锁
}

解决方案

1
2
3
4
5
6
7
8
// 修复代码
std::vector<int> shared_vec;
std::mutex vec_mutex;

void threadFunc() {
std::lock_guard<std::mutex> lock(vec_mutex);
shared_vec.push_back(42); // 加锁保护
}

总结

SIGSEGV 是 C++ 开发中最常见的崩溃信号,主要原因是内存访问违规。通过:

  1. 预防:使用智能指针、容器、引用等现代 C++ 特性
  2. 检测:启用 Address Sanitizer、Valgrind 等工具
  3. 调试:掌握 GDB 调试技巧,分析 coredump
  4. 修复:快速修复 + 优雅修复相结合
  5. 监控:线上捕获信号并记录详细信息

可以有效减少 SIGSEGV 崩溃,提高程序稳定性。