Segmentation fault (core dumped) 是 Linux 运维、C/C++ 程序调试、EDA/CAD 工具部署中非常常见的一类错误。它不是一个具体原因,而是一个结果:进程访问了它不应该访问的虚拟内存地址,内核向进程发送 SIGSEGV 信号,进程退出;如果系统允许生成 core 文件,就会留下 core dump。
在芯片研发环境里,这类问题可能来自程序自身 bug,也可能来自运行环境问题,例如:
- 空指针、野指针、数组越界;
- 栈溢出;
mmap()/munmap()使用错误;- 多线程并发释放后访问;
- System V shared memory 参数过小;
LD_LIBRARY_PATH、RPATH、RUNPATH导致动态库加载错误;- EDA 工具版本、插件、Tcl/Python native extension、
libstdc++/ glibc ABI 不匹配。
本文基于 Rocky Linux 8.10 设计一组可复现实验,用来理解不同类型的 Segmentation fault 是如何产生的,以及如何通过 gdb、strace、dmesg、ipcs、readelf、LD_DEBUG 进行定位。
1. 实验环境准备
建议在测试机或 VM 上操作,不要直接在生产 EDA 服务器上实验。尤其是后文涉及 kernel.shmmni、kernel.shmmax 这类 System V IPC 参数,它们是系统级全局参数,必须保存并恢复。
安装基础工具:
1
2
sudo dnf groupinstall -y "Development Tools"
sudo dnf install -y gcc gdb strace procps-ng util-linux binutils
创建实验目录:
1
2
mkdir -p ~/segv-lab
cd ~/segv-lab
打开 core dump:
1
ulimit -c unlimited
查看当前 core dump 配置:
1
2
ulimit -c
cat /proc/sys/kernel/core_pattern
如果希望 core 文件直接生成到 /tmp,可以临时设置:
1
echo '/tmp/core.%e.%p.%t' | sudo tee /proc/sys/kernel/core_pattern
实验后可以按需恢复原来的 core_pattern。
2. Segmentation fault 的基本原理
Linux 进程看到的是虚拟地址空间。程序每次访问内存时,CPU 的 MMU 会根据页表检查这个地址是否有效,以及访问权限是否匹配。
典型链路如下:
1
2
3
4
5
6
7
8
9
程序访问非法地址
↓
CPU/MMU 触发异常
↓
内核判断该访问不合法
↓
向进程发送 SIGSEGV
↓
进程退出,显示 Segmentation fault
常见的非法访问包括:
- 访问
NULL; - 访问随机地址;
- 写只读内存;
- 访问已经
free()或munmap()的区域; - 数组越界进入未映射页;
- 栈耗尽;
- 动态库 ABI 不匹配导致函数返回错误指针;
- shared memory / mmap 失败后程序仍继续使用错误地址。
3. 通用定位命令
程序崩溃后,先看内核日志:
1
dmesg -T | tail -30
典型输出类似:
1
your_program[12345]: segfault at 0 ip 0000000000401234 sp 00007fff... error 6 in your_program
常见判断:
| 现象 | 初步方向 |
|---|---|
segfault at 0 |
空指针访问 |
segfault at ffffffffffffffff |
可能访问了 (void *)-1,常见于 mmap() / shmat() 失败后未检查 |
in libxxx.so |
崩溃发生在某个动态库 |
| 偶发崩溃 | 多线程 race、use-after-free、内存破坏 |
| 一启动就崩 | 动态库、ABI、环境变量、插件版本不匹配 |
| 多个无关程序都崩 | 系统、硬件、内存、文件系统、基础库问题 |
用 gdb 分析 core:
1
gdb -q ./your_program /tmp/core.your_program.<pid>.<timestamp>
进入 gdb 后:
bt
bt full
info registers
info threads
thread apply all bt
frame 0
也可以直接用 gdb 运行:
1
2
3
gdb -q ./segv_lab
(gdb) run null
(gdb) bt
用 strace 看崩溃前系统调用:
1
2
strace -f -o trace.log ./segv_lab null
tail -100 trace.log
4. 实验程序:segv_lab.c
下面的程序集中模拟多类崩溃场景。
保存为 segv_lab.c:
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#define KEY_SHMMNI_1 0x51414101
#define KEY_SHMMNI_2 0x51414102
#define KEY_SHMMAX 0x51414103
#define KEY_SMALLSEG 0x51414104
static void die(const char *msg) {
perror(msg);
exit(1);
}
static long page_size(void) {
long ps = sysconf(_SC_PAGESIZE);
if (ps <= 0) die("sysconf(_SC_PAGESIZE)");
return ps;
}
static void cleanup_key(key_t key) {
int shmid = shmget(key, 1, 0600);
if (shmid >= 0) {
shmctl(shmid, IPC_RMID, NULL);
}
}
/* 1. 空指针访问 */
static void exp_null(void) {
volatile int *p = NULL;
*p = 1;
}
/* 2. 访问明显非法地址 */
static void exp_bad_addr(void) {
volatile int *p = (int *)0x12345678;
*p = 1;
}
/* 3. 写字符串常量:只读内存写入 */
static void exp_readonly(void) {
char *s = "hello";
s[0] = 'H';
puts(s);
}
/* 4. 错误函数指针 */
static void exp_bad_funcptr(void) {
void (*fp)(void) = (void (*)(void))0x1;
fp();
}
/* 5. use-after-free 的确定性版本:munmap 后继续访问 */
static void exp_after_munmap(void) {
long ps = page_size();
char *p = mmap(NULL, ps, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (p == MAP_FAILED) die("mmap");
p[0] = 'A';
if (munmap(p, ps) != 0) die("munmap");
/* 已经释放映射,继续访问,必然非法 */
p[0] = 'B';
}
/* 6. guard page 触发越界写 */
static void exp_guard_overflow(void) {
long ps = page_size();
char *p = mmap(NULL, ps * 2, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (p == MAP_FAILED) die("mmap");
if (mprotect(p + ps, ps, PROT_NONE) != 0) die("mprotect");
/*
* 第一个 page 可写。
* 第二个 page 被 mprotect 成 PROT_NONE。
* 写 ps+1 字节会跨入 guard page。
*/
memset(p, 'A', ps + 1);
}
/* 7. malloc use-after-free:普通编译未必崩,用 ASan 更明显 */
static void exp_malloc_uaf(void) {
char *p = malloc(16);
if (!p) die("malloc");
strcpy(p, "hello");
free(p);
/* 未定义行为:普通运行可能不崩,ASan 一定能报 */
p[0] = 'H';
printf("%c\n", p[0]);
}
/* 8. malloc heap overflow:普通编译未必崩,用 ASan 更明显 */
static void exp_malloc_overflow(void) {
char *p = malloc(16);
if (!p) die("malloc");
for (int i = 0; i < 1024; i++) {
p[i] = 'A';
}
free(p);
}
/* 9. 栈溢出 */
__attribute__((noinline))
static void recurse_forever(int depth) {
volatile char buf[1024 * 1024];
memset((void *)buf, depth & 0xff, sizeof(buf));
if (depth % 100 == 0) {
printf("depth=%d\n", depth);
fflush(stdout);
}
recurse_forever(depth + 1);
/* 防止尾递归优化 */
if (buf[0] == 0x42) {
printf("never\n");
}
}
static void exp_stack_overflow(void) {
recurse_forever(1);
}
/* 10. 多线程场景:一个线程释放映射,另一个线程继续访问 */
static char *g_map = NULL;
static long g_map_size = 0;
static pthread_barrier_t g_barrier;
static void *thread_unmapper(void *arg) {
(void)arg;
pthread_barrier_wait(&g_barrier);
usleep(20000);
munmap(g_map, g_map_size);
return NULL;
}
static void *thread_writer(void *arg) {
(void)arg;
pthread_barrier_wait(&g_barrier);
usleep(50000);
/* 另一个线程已经 munmap,这里访问会崩 */
g_map[0] = 'X';
return NULL;
}
static void exp_thread_unmap(void) {
pthread_t t1, t2;
g_map_size = page_size();
g_map = mmap(NULL, g_map_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (g_map == MAP_FAILED) die("mmap");
g_map[0] = 'A';
pthread_barrier_init(&g_barrier, NULL, 3);
pthread_create(&t1, NULL, thread_unmapper, NULL);
pthread_create(&t2, NULL, thread_writer, NULL);
pthread_barrier_wait(&g_barrier);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
}
/*
* 11. 文件 mmap 后文件被 truncate。
* 注意:这个场景在 Linux 上更常见的是 SIGBUS,即 Bus error,不一定是 SIGSEGV。
*/
static void exp_mmap_truncate(void) {
const char *path = "/tmp/segv_lab_mmap_file.bin";
long ps = page_size();
int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0600);
if (fd < 0) die("open");
if (ftruncate(fd, ps) != 0) die("ftruncate initial");
char *p = mmap(NULL, ps, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) die("mmap");
p[0] = 'A';
if (ftruncate(fd, 0) != 0) die("ftruncate zero");
/*
* 访问已经被 truncate 掉的文件映射区域。
* 常见结果:Bus error (core dumped)
*/
p[0] = 'B';
close(fd);
}
/*
* 12. System V shared memory:segment 太小,程序按更大空间访问。
* 这不是 sysctl 限制太小,而是应用自己申请太小。
*/
static void exp_sysv_small_segment_overrun(void) {
cleanup_key(KEY_SMALLSEG);
int shmid = shmget(KEY_SMALLSEG, 4096, IPC_CREAT | IPC_EXCL | 0600);
if (shmid == -1) die("shmget small segment");
char *p = shmat(shmid, NULL, 0);
if (p == (void *)-1) die("shmat small segment");
printf("small segment shmid=%d, attached at %p\n", shmid, p);
fflush(stdout);
/*
* 只申请 4KB,但故意访问很远的偏移。
* 大概率访问到未映射区域,触发 SIGSEGV。
*/
p[64 * 1024 * 1024] = 'X';
}
/*
* 13. System V shared memory:SHMMAX 太小。
* shmget 失败后,程序不正确处理错误,继续 shmat / 写入,触发 SIGSEGV。
*/
static void exp_sysv_shmmax_bad(void) {
cleanup_key(KEY_SHMMAX);
size_t request_size = 4 * 1024 * 1024;
int shmid = shmget(KEY_SHMMAX, request_size, IPC_CREAT | IPC_EXCL | 0600);
if (shmid == -1) {
perror("shmget expected failure when kernel.shmmax is too small");
} else {
printf("shmget unexpectedly succeeded, shmid=%d\n", shmid);
}
/*
* 故意模拟坏代码:即使 shmget 失败,也继续 shmat。
* 如果 shmid == -1,shmat 会失败并返回 (void *)-1。
*/
char *p = shmat(shmid, NULL, 0);
if (p == (void *)-1) {
perror("shmat expected failure");
} else {
printf("shmat unexpectedly succeeded at %p\n", p);
}
/*
* 故意模拟坏代码:不检查 p == (void *)-1,继续写。
*/
p[0] = 'X';
}
/*
* 14. System V shared memory:SHMMNI 太小。
* 第一个 segment 占满剩余名额,第二个 shmget 失败。
* 程序不正确处理错误,继续 shmat / 写入,触发 SIGSEGV。
*/
static void exp_sysv_shmmni_bad(void) {
cleanup_key(KEY_SHMMNI_1);
cleanup_key(KEY_SHMMNI_2);
int shmid1 = shmget(KEY_SHMMNI_1, 4096, IPC_CREAT | IPC_EXCL | 0600);
if (shmid1 == -1) {
perror("first shmget");
} else {
printf("first shmget succeeded, shmid=%d\n", shmid1);
}
int shmid2 = shmget(KEY_SHMMNI_2, 4096, IPC_CREAT | IPC_EXCL | 0600);
if (shmid2 == -1) {
perror("second shmget expected failure when kernel.shmmni is too small");
} else {
printf("second shmget unexpectedly succeeded, shmid=%d\n", shmid2);
}
/*
* 故意模拟坏代码:即使第二次 shmget 失败,也继续 shmat。
*/
char *p = shmat(shmid2, NULL, 0);
if (p == (void *)-1) {
perror("second shmat expected failure");
} else {
printf("second shmat unexpectedly succeeded at %p\n", p);
}
/*
* 故意模拟坏代码:不检查 p == (void *)-1,继续写。
*/
p[0] = 'Y';
}
static void usage(const char *prog) {
fprintf(stderr,
"Usage: %s <case>\n"
"\n"
"Cases:\n"
" null\n"
" bad_addr\n"
" readonly\n"
" bad_funcptr\n"
" after_munmap\n"
" guard_overflow\n"
" malloc_uaf\n"
" malloc_overflow\n"
" stack_overflow\n"
" thread_unmap\n"
" mmap_truncate\n"
" sysv_small_segment_overrun\n"
" sysv_shmmax_bad\n"
" sysv_shmmni_bad\n",
prog);
}
int main(int argc, char **argv) {
if (argc != 2) {
usage(argv[0]);
return 2;
}
if (strcmp(argv[1], "null") == 0) exp_null();
else if (strcmp(argv[1], "bad_addr") == 0) exp_bad_addr();
else if (strcmp(argv[1], "readonly") == 0) exp_readonly();
else if (strcmp(argv[1], "bad_funcptr") == 0) exp_bad_funcptr();
else if (strcmp(argv[1], "after_munmap") == 0) exp_after_munmap();
else if (strcmp(argv[1], "guard_overflow") == 0) exp_guard_overflow();
else if (strcmp(argv[1], "malloc_uaf") == 0) exp_malloc_uaf();
else if (strcmp(argv[1], "malloc_overflow") == 0) exp_malloc_overflow();
else if (strcmp(argv[1], "stack_overflow") == 0) exp_stack_overflow();
else if (strcmp(argv[1], "thread_unmap") == 0) exp_thread_unmap();
else if (strcmp(argv[1], "mmap_truncate") == 0) exp_mmap_truncate();
else if (strcmp(argv[1], "sysv_small_segment_overrun") == 0) exp_sysv_small_segment_overrun();
else if (strcmp(argv[1], "sysv_shmmax_bad") == 0) exp_sysv_shmmax_bad();
else if (strcmp(argv[1], "sysv_shmmni_bad") == 0) exp_sysv_shmmni_bad();
else {
usage(argv[0]);
return 2;
}
return 0;
}
编译普通版本:
1
gcc -g -O0 -Wall -Wextra -fno-omit-frame-pointer -pthread segv_lab.c -o segv_lab
编译 AddressSanitizer 版本:
1
gcc -g -O0 -Wall -Wextra -fno-omit-frame-pointer -fsanitize=address -pthread segv_lab.c -o segv_lab_asan
5. 一个容易犯的错误:把 heredoc 包装行写进 C 源码
如果 segv_lab.c 第一行变成:
1
cat > segv_lab.c <<'EOF'
最后一行还有:
1
EOF
编译会出现类似:
1
2
3
segv_lab.c:1:5: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘>’ token
cat > segv_lab.c <<'EOF'
^
这不是 gcc 参数问题,也不是系统头文件问题,而是文件内容错误。
cat > segv_lab.c <<'EOF' 和最后的 EOF 是 shell heredoc 的包装行,只应该在终端里执行,不应该进入 segv_lab.c 文件正文。
修复方式:
1
2
cp segv_lab.c segv_lab.c.bad
sed '1d;$d' segv_lab.c.bad > segv_lab.c
检查文件头尾:
1
2
head -5 segv_lab.c
tail -5 segv_lab.c
正确开头应该是:
1
2
3
4
5
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
正确结尾应该是:
1
2
return 0;
}
6. 基础内存访问错误实验
6.1 空指针访问
1
./segv_lab null
预期:
1
Segmentation fault (core dumped)
查看:
1
dmesg -T | tail -20
常见特征:
1
segfault at 0
这类问题通常来自 NULL 指针解引用。
6.2 访问非法地址
1
./segv_lab bad_addr
程序访问:
1
(int *)0x12345678
预期:
1
Segmentation fault (core dumped)
地址看起来像地址,但不属于当前进程的有效虚拟地址空间。
6.3 写只读内存
1
./segv_lab readonly
程序试图修改字符串常量:
1
2
char *s = "hello";
s[0] = 'H';
预期:
1
Segmentation fault (core dumped)
这类问题常见于老 C 代码、脚本语言 native extension、EDA 插件代码。
6.4 错误函数指针
1
./segv_lab bad_funcptr
程序调用地址 0x1:
1
2
void (*fp)(void) = (void (*)(void))0x1;
fp();
预期:
1
Segmentation fault (core dumped)
常见工程原因:
- 函数指针未初始化;
- vtable 被破坏;
- callback 指针错误;
- 动态库 ABI 不匹配;
- 插件接口版本不一致。
7. 释放后访问、越界访问、栈溢出
7.1 munmap() 后继续访问
1
./segv_lab after_munmap
逻辑:
1
2
3
4
mmap()
写入
munmap()
继续访问旧地址
预期:
1
Segmentation fault (core dumped)
这是一个确定性的 use-after-release 实验。
7.2 guard page 越界写
1
./segv_lab guard_overflow
逻辑:
1
2
3
第 1 页:可读写
第 2 页:mprotect(PROT_NONE)
写入超过第 1 页边界
预期:
1
Segmentation fault (core dumped)
这类实验可以稳定复现数组越界进入非法页的场景。
7.3 普通 malloc use-after-free
普通运行:
1
./segv_lab malloc_uaf
这个不一定每次崩,因为 free() 之后的 heap 内存未必立刻不可访问。
用 ASan 版本更清楚:
1
./segv_lab_asan malloc_uaf
预期:
1
ERROR: AddressSanitizer: heap-use-after-free
7.4 普通 malloc heap overflow
普通运行:
1
./segv_lab malloc_overflow
可能崩,也可能不崩。
用 ASan 版本:
1
./segv_lab_asan malloc_overflow
预期:
1
ERROR: AddressSanitizer: heap-buffer-overflow
很多 heap overflow 不会在越界写那一刻崩,而是在后续 free()、malloc()、函数返回、线程切换时崩。ASan 能更早暴露根因。
7.5 栈溢出
1
./segv_lab stack_overflow
预期:
1
Segmentation fault (core dumped)
查看当前栈大小:
1
ulimit -s
让问题更快出现:
1
2
ulimit -s 1024
./segv_lab stack_overflow
常见原因:
- 无限递归;
- 递归层次过深;
- 函数局部变量过大;
- 线程栈太小。
8. 多线程释放后访问
1
./segv_lab thread_unmap
实验逻辑:
1
2
线程 A:munmap 一块映射
线程 B:稍后继续访问这块映射
预期:
1
Segmentation fault (core dumped)
实际系统里,这类问题经常表现为:
1
2
3
偶发崩溃
同一个输入不一定每次复现
崩溃点看起来和真正 bug 点不一致
如果是工程代码,建议配合 ThreadSanitizer:
1
gcc -g -O1 -fsanitize=thread -fno-omit-frame-pointer -pthread your_code.c -o your_code_tsan
9. 文件 mmap 后被 truncate:常见是 Bus error
1
./segv_lab mmap_truncate
这个实验常见结果是:
1
Bus error (core dumped)
而不是:
1
Segmentation fault (core dumped)
这是因为文件被 mmap() 后,如果文件被截断,进程继续访问原映射区域,Linux 上常触发 SIGBUS。
这类问题在以下场景中需要注意:
- 数据库;
- EDA cache;
- 大文件索引;
- NFS 文件映射;
- 多进程共享文件映射。
10. System V Shared Memory 相关实验
先看当前 IPC 状态:
1
2
3
ipcs -u
ipcs -m
ipcs -m -l
查看关键 sysctl:
1
2
3
sysctl kernel.shmmni
sysctl kernel.shmmax
sysctl kernel.shmall
含义:
| 参数 | 含义 |
|---|---|
kernel.shmmni |
系统最大 shared memory segment 数量 |
kernel.shmmax |
单个 shared memory segment 最大大小 |
kernel.shmall |
系统全部 shared memory 页数上限 |
10.1 segment 本身申请太小,程序按更大区域访问
这个实验不修改 sysctl:
1
./segv_lab sysv_small_segment_overrun
程序逻辑:
1
2
3
shmget 只申请 4096 bytes
shmat 成功
程序错误地访问 64MB 偏移
预期:
1
Segmentation fault (core dumped)
清理:
1
ipcrm -M 0x51414104 2>/dev/null || true
工程对应场景:
- 共享内存结构体版本不一致;
- 一个进程认为 segment 很大,另一个进程实际只创建了很小;
- 程序没有保存或校验 segment size;
- 跨版本工具或 daemon 协议不一致。
10.2 kernel.shmmax 太小导致 shmget() 失败
保存原始值:
1
2
orig_shmmax=$(sysctl -n kernel.shmmax)
echo "$orig_shmmax"
临时把单个 segment 上限调小到 1MB:
1
sudo sysctl -w kernel.shmmax=$((1024*1024))
运行实验:
1
./segv_lab sysv_shmmax_bad
程序会申请 4MB:
1
shmget(KEY, 4 * 1024 * 1024, ...)
因为 kernel.shmmax=1MB,shmget() 应该失败。常见输出:
1
2
3
shmget expected failure when kernel.shmmax is too small: Invalid argument
shmat expected failure: Invalid argument
Segmentation fault (core dumped)
恢复:
1
sudo sysctl -w kernel.shmmax="$orig_shmmax"
清理:
1
ipcrm -M 0x51414103 2>/dev/null || true
关键结论:
1
2
shmget 失败本身不会导致 segfault
坏代码没有检查返回值,继续访问错误地址,才导致 segfault
10.3 kernel.shmmni 太小导致 segment 数量耗尽
查看当前已经分配的 segment 数量:
1
ipcs -u | grep segment
保存原始值:
1
2
orig_shmmni=$(sysctl -n kernel.shmmni)
echo "$orig_shmmni"
根据当前使用量设置一个很小但可控的上限:
1
2
3
4
used=$(ipcs -u | awk '/segments allocated/{print $3}')
echo "currently allocated segments: $used"
sudo sysctl -w kernel.shmmni=$((used + 1))
运行实验:
1
./segv_lab sysv_shmmni_bad
预期输出类似:
1
2
3
4
first shmget succeeded, shmid=xxx
second shmget expected failure when kernel.shmmni is too small: No space left on device
second shmat expected failure: Invalid argument
Segmentation fault (core dumped)
这里的 No space left on device 不一定表示磁盘满,也可能是 System V IPC 资源耗尽。
恢复:
1
sudo sysctl -w kernel.shmmni="$orig_shmmni"
清理:
1
2
ipcrm -M 0x51414101 2>/dev/null || true
ipcrm -M 0x51414102 2>/dev/null || true
10.4 用 strace 证明 shared memory 失败链路
以 shmmax 实验为例:
1
2
3
4
5
sudo sysctl -w kernel.shmmax=$((1024*1024))
strace -f -o shmmax.trace ./segv_lab sysv_shmmax_bad
sudo sysctl -w kernel.shmmax="$orig_shmmax"
查看关键系统调用:
1
grep -E 'shmget|shmat|SIGSEGV' shmmax.trace
可能看到:
1
2
3
shmget(0x51414103, 4194304, IPC_CREAT|IPC_EXCL|0600) = -1 EINVAL (Invalid argument)
shmat(-1, NULL, 0) = -1 EINVAL (Invalid argument)
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0xffffffffffffffff} ---
这个链路很清楚:
1
2
3
4
5
6
7
shmget 失败
↓
shmat 失败
↓
程序错误访问 (void *)-1
↓
触发 SIGSEGV
11. 动态库加载错误实验:LD_LIBRARY_PATH、RPATH 与 RUNPATH
动态库加载错误是 EDA/CAD 环境里非常典型的一类 Segmentation fault 来源。
常见原因包括:
LD_LIBRARY_PATH顺序错误;- 工具加载了错误版本
.so; - Tcl/Python/Perl native extension 与解释器 ABI 不匹配;
libstdc++.so版本不一致;- EDA 工具主程序、插件、wrapper 混用不同版本库;
- 程序内置
RPATH或RUNPATH后,用户误以为LD_LIBRARY_PATH可以覆盖。
11.1 构造 good / bad 两个库
1
2
cd ~/segv-lab
mkdir -p good bad
正常库:
1
2
3
4
5
cat > good/foo.c <<'EOF'
const char *get_msg(void) {
return "hello from good libfoo";
}
EOF
错误库:
1
2
3
4
5
cat > bad/foo.c <<'EOF'
const char *get_msg(void) {
return 0;
}
EOF
主程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cat > dyn_main.c <<'EOF'
#include <stdio.h>
extern const char *get_msg(void);
int main(void) {
const char *p = get_msg();
/*
* 如果加载到 bad/libfoo.so,p 是 NULL。
* 这里继续访问 p[0],会触发 SIGSEGV。
*/
printf("first char = %c\n", p[0]);
return 0;
}
EOF
编译两个库:
1
2
gcc -fPIC -shared -o good/libfoo.so good/foo.c
gcc -fPIC -shared -o bad/libfoo.so bad/foo.c
11.2 没有 RPATH 的版本:LD_LIBRARY_PATH 可以控制加载哪个库
1
2
3
gcc -g -O0 -Wall -Wextra -fno-omit-frame-pointer \
-L"$PWD/good" \
-o dyn_main_no_rpath dyn_main.c -lfoo
确认没有 RPATH / RUNPATH:
1
readelf -d ./dyn_main_no_rpath | egrep 'RPATH|RUNPATH' || echo "no rpath/runpath"
加载 good:
1
LD_LIBRARY_PATH="$PWD/good" ./dyn_main_no_rpath
预期:
1
first char = h
加载 bad:
1
LD_LIBRARY_PATH="$PWD/bad" ./dyn_main_no_rpath
预期:
1
Segmentation fault (core dumped)
用 LD_DEBUG 验证:
1
LD_DEBUG=libs LD_LIBRARY_PATH="$PWD/bad" ./dyn_main_no_rpath
应该看到:
1
2
3
4
find library=libfoo.so [0]; searching
search path=/home/wanlin/segv-lab/bad/... (LD_LIBRARY_PATH)
trying file=/home/wanlin/segv-lab/bad/libfoo.so
calling init: /home/wanlin/segv-lab/bad/libfoo.so
11.3 RPATH 版本:LD_LIBRARY_PATH 未必能覆盖
如果这样编译:
1
2
3
4
gcc -g -O0 -Wall -Wextra -fno-omit-frame-pointer \
-L"$PWD/good" \
-Wl,-rpath,"$PWD/good" \
-o dyn_main dyn_main.c -lfoo
查看:
1
readelf -d ./dyn_main | egrep 'RPATH|RUNPATH'
如果看到的是:
1
RPATH
那么即使运行:
1
LD_DEBUG=libs LD_LIBRARY_PATH="$PWD/bad" ./dyn_main
也可能仍然优先加载 good/libfoo.so。
典型 LD_DEBUG 日志会显示:
1
2
3
find library=libfoo.so [0]; searching
search path=/home/wanlin/segv-lab/good/... (RPATH from file ./dyn_main)
trying file=/home/wanlin/segv-lab/good/libfoo.so
最后程序输出:
1
first char = h
这说明加载的是 good/libfoo.so,不是 bad/libfoo.so。
这个实验非常适合解释 EDA/CAD 环境里的一个常见误区:
1
2
用户以为设置了 LD_LIBRARY_PATH 就能覆盖动态库
但程序二进制里写死了 RPATH,实际仍然加载 RPATH 指向的库
11.4 RUNPATH 版本:LD_LIBRARY_PATH 通常可以覆盖
重新编译为 RUNPATH:
1
2
3
4
5
gcc -g -O0 -Wall -Wextra -fno-omit-frame-pointer \
-L"$PWD/good" \
-Wl,--enable-new-dtags \
-Wl,-rpath,"$PWD/good" \
-o dyn_main_runpath dyn_main.c -lfoo
查看:
1
readelf -d ./dyn_main_runpath | egrep 'RPATH|RUNPATH'
期望看到:
1
RUNPATH
运行:
1
LD_DEBUG=libs LD_LIBRARY_PATH="$PWD/bad" ./dyn_main_runpath
如果加载到 bad/libfoo.so,程序会触发:
1
Segmentation fault (core dumped)
12. 实验结果对照表
| 实验 | 命令 | 预期信号 | 重点观察 |
|---|---|---|---|
| 空指针 | ./segv_lab null |
SIGSEGV | segfault at 0 |
| 非法地址 | ./segv_lab bad_addr |
SIGSEGV | 访问未映射地址 |
| 写只读内存 | ./segv_lab readonly |
SIGSEGV | 写字符串常量 |
| 错误函数指针 | ./segv_lab bad_funcptr |
SIGSEGV | 错误跳转地址 |
munmap 后访问 |
./segv_lab after_munmap |
SIGSEGV | 释放映射后访问 |
| guard page 越界 | ./segv_lab guard_overflow |
SIGSEGV | 越界进入 PROT_NONE page |
| malloc UAF | ./segv_lab_asan malloc_uaf |
ASan 报错 | 普通运行未必崩 |
| malloc overflow | ./segv_lab_asan malloc_overflow |
ASan 报错 | 普通运行未必崩 |
| 栈溢出 | ./segv_lab stack_overflow |
SIGSEGV | 栈耗尽 |
| 多线程 unmap | ./segv_lab thread_unmap |
SIGSEGV | 并发释放后访问 |
| mmap truncate | ./segv_lab mmap_truncate |
通常 SIGBUS | 文件映射失效 |
| SysV segment 太小 | ./segv_lab sysv_small_segment_overrun |
SIGSEGV | 访问超出 segment |
shmmax 太小 |
./segv_lab sysv_shmmax_bad |
SIGSEGV | shmget 失败后坏代码继续访问 |
shmmni 太小 |
./segv_lab sysv_shmmni_bad |
SIGSEGV | segment 数量耗尽后坏代码继续访问 |
| 错误动态库 | LD_LIBRARY_PATH="$PWD/bad" ./dyn_main_no_rpath |
SIGSEGV | 加载错误 .so |
13. 清理实验环境
清理残留 shared memory:
1
2
3
4
ipcrm -M 0x51414101 2>/dev/null || true
ipcrm -M 0x51414102 2>/dev/null || true
ipcrm -M 0x51414103 2>/dev/null || true
ipcrm -M 0x51414104 2>/dev/null || true
清理 mmap 文件:
1
rm -f /tmp/segv_lab_mmap_file.bin
确认 IPC 状态:
1
2
ipcs -m
ipcs -u | grep segment
确认 sysctl 已恢复:
1
2
3
sysctl kernel.shmmni
sysctl kernel.shmmax
sysctl kernel.shmall
14. 面向 EDA/CAD 环境的排查思路
在 EDA/CAD 服务器上遇到 Segmentation fault (core dumped),建议按这个顺序看:
14.1 先看是否是环境问题
1
2
3
4
5
6
7
8
which tool_binary
ldd tool_binary
readelf -d tool_binary | egrep 'RPATH|RUNPATH'
echo "$PATH"
echo "$LD_LIBRARY_PATH"
echo "$PYTHONPATH"
echo "$TCL_LIBRARY"
echo "$PERL5LIB"
重点关注:
- 是否加载了错误版本的
.so; - 是否有
not found; - 是否加载到了别的 EDA 版本目录;
LD_LIBRARY_PATH是否被 wrapper 覆盖;- 二进制是否内置 RPATH;
- 插件目录和主工具版本是否匹配。
14.2 再看系统调用失败
1
2
strace -f -o trace.log tool_command ...
grep -E 'shmget|shmat|mmap|munmap|open|access|ENOENT|EINVAL|ENOMEM|ENOSPC|SIGSEGV|SIGBUS' trace.log
如果看到:
1
2
3
shmget(...) = -1 ENOSPC
shmat(-1, ...) = -1 EINVAL
--- SIGSEGV ---
就要重点查 System V IPC 限制和程序错误处理路径。
14.3 看 shared memory 状态
1
2
3
4
ipcs -u
ipcs -m
ipcs -m -l
sysctl kernel.shmmni kernel.shmmax kernel.shmall
常见现象:
| 现象 | 可能原因 |
|---|---|
| segment 数量很多 | 工具异常退出后残留 shared memory |
nattch=0 |
segment 无进程附着,可能是残留 |
shmget ENOSPC |
SHMMNI 或 SHMALL 不够 |
shmget EINVAL |
申请大小超过 SHMMAX 或参数非法 |
shmat EINVAL |
shmid 非法,常见于上一步失败后继续使用 |
14.4 最后看 core
1
gdb tool_binary core
进入 gdb:
bt
bt full
info threads
thread apply all bt
info sharedlibrary
如果崩在某个 .so 中,应回到动态库加载路径继续查。
15. 核心结论
Segmentation fault (core dumped) 是结果,不是根因。
在 System V shared memory 场景中,真实链路往往是:
1
2
3
4
5
6
7
8
9
10
11
kernel.shmmax 太小 / kernel.shmmni 太小 / kernel.shmall 不够
↓
shmget() 失败
↓
shmat() 失败
↓
程序没有检查返回值
↓
继续访问 NULL、(void *)-1 或未映射区域
↓
Segmentation fault
在动态库场景中,真实链路往往是:
1
2
3
4
5
6
7
8
9
LD_LIBRARY_PATH / RPATH / RUNPATH 导致加载错误版本 .so
↓
函数 ABI 或语义不匹配
↓
返回错误指针、错误对象、错误函数地址
↓
程序继续访问
↓
Segmentation fault
所以排查时不要只看最后一行报错。更有效的顺序是:
1
2
3
4
5
6
dmesg -T | tail -50
strace -f -o trace.log ./program
grep -E 'shmget|shmat|mmap|munmap|SIGSEGV|SIGBUS' trace.log
readelf -d ./program | egrep 'RPATH|RUNPATH'
LD_DEBUG=libs ./program
gdb ./program core
把 SIGSEGV 前面的系统调用、动态库加载路径、IPC 状态和 core backtrace 串起来,才能定位真正的根因。