Where did my segfault go?
摘要
作者在 entr 监控小 C 程序时发现 hello.c 成功编译但 segfault 却完全没有输出。尝试加 bash -c 前缀仍静默,改写成 run.sh 脚本文件让 entr 指向它后终于打印出 Segmentation fault。原因在于 bash 如果能直接 execve 替换自身而不 fork 新进程就不会触发父进程的 SIGSEGV 信号处理;脚本的 shebang 启动新 bash 后,gcc 先 fork 再运行./hello,wait 后由父 bash 打印信号。作者还测试了命令用小括号括起来的 subshell 版本(fork 子 shell,父 bash 能捕获)以及在崩溃命令后加 sleep 的方案,这些都有效让信号被正确打印出来。
荐读理由
这篇文章揭示了shell exec优化导致的segfault捕获失效,结合正文实例与解释,提供了排查C程序崩溃的工程技巧,能直接迁移到手上的AI工程项目
原文
Where did my segfault go?
The other day I was iterating on a small C program with entr:
ls hello.c | entr -s "gcc -o hello hello.c && ./hello"
hello happened to segfault, but entr showed me… nothing. No Segmentation fault, no non-zero exit visible, just complete silence after the compile output. Hmm, very weird. Let’s prefix with bash -c:
ls hello.c | entr -s "bash -c 'gcc -o hello hello.c && ./hello'"
Still nothing. What about moving the command into a script:
#!/bin/bash
gcc -o hello hello.c && ./hello
And point entr to it:
ls hello.c | entr -s ./run.sh
./run.sh: line 2: 104465 Segmentation fault (core dumped) ./hello
There’s my segmentation fault!
The reason(s)
Thanks to superuser (remember that site we used to use pre LLMs?) for pointing me in the right direction. The question is not exactly the same, but it still helped me. At first, I thought the problem was with
entrbecause I could reproduce both with bash and fish.
Who actually prints “Segmentation fault”? Obviously not the crashing program. It can’t since it’s dead. The message is printed by the shell when it reaps a child that died from SIGSEGV. No parent shell, no message, simple enough.
But wait, why does even the explicit bash -c 'gcc && ./hello' version stay silent?
Turns out bash likes to exec the command if it can. When you run bash -c "some_command" and some_command is effectively the only thing to do, bash doesn’t bother forking a new process for it. It just execves into it, replacing itself. It’s a nice optimization, invisible 99% of the time.
In the script version, entr runs ./run.sh as a child, and the script’s shebang starts a fresh bash to interpret the file. That bash runs gcc, forks for ./hello, waits, sees it died from SIGSEGV, and dutifully prints Segmentation fault before exiting.
The follow-up
But then it occurred to me, maybe you don’t need a wrapper script after all, what happens if I have the crashing command running in a subshell?
$ ls hello.c | entr -s "gcc -o hello hello.c && (./hello)"
hello.c: line 1: 106595 Segmentation fault (core dumped) ( ./hello )
Yep, I don’t think I have ever been that satisfied to see a program segfaulting. Here the parens force ./hello into a forked subshell, so bash can’t exec its way out of being the parent. When the subshell dies from SIGSEGV, bash reaps it and prints the message like normal.
I could have stopped there but I wanted a way to test my second hypothesis. What if you just make sure the shell has something to do after the crashing command?
$ ls hello.c | entr -s "bash -c 'gcc -o hello hello.c && ./hello; true'"
bash: line 1: 109516 Segmentation fault (core dumped) ./hello
Victory!
Now I should probably get back to writing some C…
Written on July 11, 2026
这条对你有帮助吗?