Application DebuggingLesson 3 of 4
Crashes: core dumps, coredumpctl and gdb
Capture a crashed process with systemd-coredump, read its backtrace with gdb, and tell a crash from a clean error.
coredumpctlcoredumpctl listcoredumpctl infocoredumpctl debugcoredumpctl dumpgdbgdb -batch -ex bt/proc/sys/kernel/core_pattern/etc/systemd/coredump.confulimitdnf debuginfo-installkill -SEGV
After this lesson you can
- Explain what a core dump is and how RHEL decides where it goes (core_pattern and systemd-coredump).
- List and inspect recent crashes with coredumpctl and open one in gdb.
- Read a backtrace to name the function, source line and bad value behind a segmentation fault.
The idea
A program that dies from a signal such as SIGSEGV (it touched memory it does not own), SIGABRT (it called abort(), often after a failed assertion) or SIGBUS leaves the kernel the option to save a core dump: a copy of the process’s memory and registers at the moment of death. A core dump is not a log message; it is the frozen process. A debugger can then show which function was running, what called it, and the values of variables.
A crash and an error are different things. A program that prints an error and exits with status 1 has chosen to stop; the reason is in its output or its log. A program killed by a signal did not choose, the shell reports status 128 plus the signal number (139 for SIGSEGV), and systemctl status shows status=11/SEGV. Only the second kind produces a core dump.
How RHEL decides what happens to a dump
Four things must all permit it:
| Gate | Where | Default on RHEL |
|---|---|---|
| Size limit of the process | ulimit -c, /proc/PID/limits, LimitCORE= in units | Shells usually have a soft limit of 0; for services read the live value with systemctl show UNIT -p LimitCORE |
| Handler | /proc/sys/kernel/core_pattern | |/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h |
| Storage and caps | /etc/systemd/coredump.conf | Storage=external, ProcessSizeMax= and ExternalSizeMax= caps; compression on |
| Space | /var/lib/systemd/coredump/ | Cleaned by systemd-tmpfiles after about three days |
The leading | in core_pattern pipes the dump to a program instead of writing a core file in the working directory. systemd-coredump saves it as a compressed file, logs a journal entry with a short backtrace, and coredumpctl lets you query it.
coredumpctl list # recent crashes: time, PID, UID, signal, present?, size, program
coredumpctl info PROGRAM|PID # details, including a stack trace if debug info is installed
coredumpctl debug PROGRAM|PID # open the dump in gdb
coredumpctl dump PROGRAM -o core.file # extract the core file itself
Reading a backtrace
The key tool is gdb:
| gdb command | Meaning |
|---|---|
bt | Backtrace: the chain of function calls at the crash, innermost (frame 0) first |
frame N | Select a frame |
list | Show source code around the line |
info locals, info args | Variables in the selected frame |
print EXPR, print *ptr | Evaluate an expression |
info registers rip | The instruction that faulted |
A backtrace shows symbols and source lines only when debug information is present. Compiling with -g provides it for your own code. For distribution packages, debug information is separate: dnf debuginfo-install PKG (repository *-debuginfo-rpms enabled), or gdb can fetch it on demand from Red Hat’s debuginfod server when DEBUGINFOD_URLS is set. Without it you still get function names for exported symbols, which is often enough to recognise a known bug in a package.
Try it
These outputs are real, from gdb 13 on a core file of a small C program with -g. (The machine in the test writes a plain core file; on RHEL the same file comes out of coredumpctl dump, and coredumpctl debug opens it for you.)
static int load(struct conf *c, const char *key) {
if (strcmp(key, "name") == 0) { c->name = "labapp"; return 0; }
return -1; /* never sets c->name for other keys */
}
static void show(struct conf *c) { printf("%s:%d\n", c->name, c->port); }
The crash
./crash; echo "rc=$?"
Segmentation fault (core dumped)
rc=139
Status 139 is 128 + 11: killed by SIGSEGV. Now the dump:
gdb -batch -ex bt -ex 'print *c' -ex 'info registers rip' ./crash core
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x000056031214720f in show (c=0x7ffc2143de60) at crash.c:8
#0 0x000056031214720f in show (c=0x7ffc2143de60) at crash.c:8
#1 0x000056031214727b in main () at crash.c:12
$1 = {name = 0x0, port = 0}
rip 0x56031214720f 0x56031214720f <show+60>
Read it from the top. Program terminated with signal SIGSEGV confirms the kind of failure. Frame 0 is show at crash.c:8, called from main at crash.c:12. The structure *c contains name = 0x0: a NULL pointer, and show used it in printf. The cause is upstream: load(&c, "port") returned -1 without setting name, and nobody checked the result. A fix is in the program, not in the system, but the diagnosis (and the evidence to report to the vendor or developer) came entirely from the dump.
On RHEL with coredumpctl
coredumpctl list --no-pager | tail -2
coredumpctl info crash --no-pager | sed -n 1,18p
TIME PID UID GID SIG COREFILE EXE SIZE
Wed 2026-11-04 12:21:09 +03 4410 0 0 SIGSEGV present /tmp/crash 34.2K
PID: 4410 (crash)
UID: 0 (root)
Signal: 11 (SEGV)
Timestamp: Wed 2026-11-04 12:21:09 +03 (12s ago)
Command Line: ./crash
Executable: /tmp/crash
Storage: /var/lib/systemd/coredump/core.crash.0.....zst (present)
Message: Process 4410 (crash) of user 0 dumped core.
Stack trace of thread 4410:
#0 0x000055f1... show (/tmp/crash + 0x120f)
#1 0x000055f1... main (/tmp/crash + 0x127b)
(Representative output.) The column COREFILE says present while the file is still on disk and missing once it has been cleaned up. If you need a dump later, copy it: coredumpctl dump crash -o /root/crash.core.
A crash is also a service event
Because the kernel hands the dump to systemd, the crash appears in the journal even if the application kept no log:
journalctl -u myapp.service -b --no-pager | tail
systemctl status myapp.service
myapp.service: Main process exited, code=dumped, status=11/SEGV
myapp.service: Failed with result 'core-dump'.
code=dumped and Result: core-dump mark a crash. With Restart=on-failure the service will start again; repeated crashes show as a loop of these lines, so count them (coredumpctl list shows how many).
When there is no dump
| Cause | Check | Fix |
|---|---|---|
| Core limit is 0 | grep core /proc/PID/limits | LimitCORE=infinity in a unit drop-in; ulimit -c unlimited in a shell |
| Handler is not systemd-coredump | cat /proc/sys/kernel/core_pattern | Install the package providing it and restart; do not point it at a world-writable path |
| Dump larger than the caps | /etc/systemd/coredump.conf | Raise ProcessSizeMax= and ExternalSizeMax= |
| Disk full | df -h /var | Free space; coredumpctl also has a KeepFree= setting |
| Privilege-dropping or setuid program | sysctl fs.suid_dumpable | Value 2 allows dumps, but protects them from ordinary users |
The process was killed by SIGKILL | status=9/KILL | SIGKILL produces no dump; look for the OOM killer or a human |
| Container or PID namespace | coredumpctl works on the host for host-visible processes |
Going further
Sensitive data. A core dump contains everything the process had in memory: keys, passwords, session data. Treat dumps like secrets. They are stored root-only, and Storage=none in coredump.conf turns storage off for machines where that is unacceptable. Beyond the exam: gdb can attach to a live process (gdb -p PID) to take a backtrace without stopping the service permanently (gcore PID writes a core of a running process), valgrind finds invalid memory use before it becomes a crash, and abrt may be installed to report crashes to Red Hat automatically on systems that use it.
Practice
0 of 3 done
Do these on your own system. They get harder. Check each result, then tick it off.
- Warm-up
Crash a process and find the record
Start
sleep 300 &, kill it with signal 11 (kill -SEGV), then usecoredumpctlto show that the crash was recorded: the program, the signal, the time and the size of the dump. Print the value ofkernel.core_patternand explain what it means.Check your result
coredumpctl listshows a line forsleepwith signalSIGSEGV(11).cat /proc/sys/kernel/core_patternbegins with|/usr/lib/systemd/systemd-coredump.coredumpctl info sleepprintsSignal: 11 (SEGV)and aStorage:line.
Hint
The leading
|incore_patternmeans "pipe the dump to this program". On RHEL that program issystemd-coredump, which stores dumps in/var/lib/systemd/coredumpand logs the event in the journal.Solution
sleep 300 & PID=$! kill -SEGV $PID sleep 1 coredumpctl list --no-pager | tail -3 coredumpctl info sleep --no-pager | sed -n 1,20p cat /proc/sys/kernel/core_pattern - Practice
Why did it segfault?
Setup: save, compile with debug information, and run:
cat > /tmp/crash.c <<'EOT' #include <stdio.h> #include <string.h> struct conf { char *name; int port; }; static int load(struct conf *c, const char *key) { if (strcmp(key, "name") == 0) { c->name = "labapp"; return 0; } return -1; } static void show(struct conf *c) { printf("%s:%d\n", c->name, c->port); } int main(void) { struct conf c = {0}; load(&c, "port"); show(&c); return 0; } EOT gcc -g -O0 -o /tmp/crash /tmp/crash.c && /tmp/crashOpen the resulting core dump in
gdb(throughcoredumpctl debug, or directly with the core file) and report: the function and line where it crashed, the full call chain, and the value that caused it. State the root cause in one sentence.Check your result
- The backtrace shows
#0 ... in show (c=...) at crash.c:8and#1 ... in main () at crash.c:12. print *cgives{name = 0x0, port = 0}.- Your sentence names the NULL
namepointer:load()did not set it for the key"port"andshow()printed it without checking.
Hint
coredumpctl debug crashstarts gdb on the newest matching dump. Inside gdb:bt(backtrace),frame 0,info locals,print *c,list. Compiling with-ggives source lines; without it you see only addresses and function names.Solution
/tmp/crash; echo "rc=$?" # 139 = 128 + 11 (SIGSEGV) coredumpctl list --no-pager | tail -2 coredumpctl debug crash -A "-batch -ex bt -ex 'print *c' -ex 'frame 1' -ex 'info locals'" - The backtrace shows
- Challenge
The service crashed, but there is no dump
A daemon crashes with
SIGSEGV(systemctl statussaysstatus=11/SEGV) butcoredumpctl listshows nothing for it. Write the checks you would make, in order, and the configuration lines that fix each cause: (1) core size limit zero; (2)systemd-coredumpnot the handler or not installed; (3) storage limit or size cap incoredump.conf; (4) disk full in/var/lib/systemd/coredump; (5) the process dropped privileges or issetuid, which the kernel dumps differently (fs.suid_dumpable). Then verify the fix by crashing a test service started withsystemd-run.Check your result
- You check
grep core /proc/PID/limitsorsystemctl show UNIT -p LimitCORE, and raise withLimitCORE=infinityin a drop-in. - You check
cat /proc/sys/kernel/core_patternand thatrpm -q systemd-udev/ thesystemd-coredumpunit exists. - You check
/etc/systemd/coredump.confforStorage=,ProcessSizeMax=andExternalSizeMax=. - A test unit crashed by
kill -SEGVappears incoredumpctl list.
Hint
Four independent gates: the limit (
RLIMIT_CORE), the handler (core_pattern), the size caps (ProcessSizeMax,ExternalSizeMax), and space (df -h /var). A dump is silently skipped when any gate stops it.Solution
systemd-run --unit=crashme --property=LimitCORE=infinity sleep 300 sleep 1; sudo kill -SEGV $(systemctl show crashme -p MainPID --value) sleep 1; coredumpctl list --no-pager | tail -2 cat /proc/sys/kernel/core_pattern grep -vE '^\s*(#|$)' /etc/systemd/coredump.conf df -h /var/lib/systemd/coredump systemctl show crashme -p LimitCORE - You check