프로덕션에서 프로세스가 Segmentation fault로 죽었는데 로그에는 아무것도 안 남았다. 재현도 안 되고, 붙어 있던 디버거도 없다. 이럴 때 유일한 단서가 코어 덤프(core dump)다. 그런데 Ubuntu 24.04를 기본 설정 그대로 쓰면 직접 빌드한 바이너리는 코어 파일이 아예 생기지 않는다. 이 글에서는 왜 코어가 안 남는지 확인하고, 코어를 확실히 받도록 설정한 뒤 gdb로 크래시 지점까지 짚어내는 과정을 정리한다.
재현용 크래시 프로그램
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct node {
int id;
char name[16];
struct node *next;
};
static void fill(struct node *n, const char *s)
{
n->id = 42;
strcpy(n->name, s);
}
static void walk(struct node *head)
{
struct node *p = head;
while (p) {
printf("id=%d name=%s\n", p->id, p->name);
p = p->next;
}
}
int main(int argc, char **argv)
{
struct node *head = malloc(sizeof(*head));
fill(head, argc > 1 ? argv[1] : "ok");
head->next = (struct node *)0x10; /* 유효하지 않은 주소 */
walk(head);
return 0;
}walk()가 두 번째 노드로 넘어가면서 0x10을 역참조해 SIGSEGV로 죽는다. 디버그 정보를 남겨 빌드하고 실행한다.
gcc -g -O0 -fno-omit-frame-pointer -o crash crash.c
./crash test
ls -l core*Segmentation fault (core dumped)
ls: cannot access 'core*': No such file or directory
셸은 (core dumped)라고 보고하는데 정작 코어 파일은 없다. 원인은 두 곳에 있다.
Ubuntu 기본값에서 코어가 사라지는 두 가지 이유
cat /proc/sys/kernel/core_pattern
ulimit -c
tail -2 /var/log/apport.log|/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -F%F -- %E
0
INFO: apport (pid 3080): executable: /home/noble/coredemo/crash (command line "./crash test")
ERROR: apport (pid 3080): executable does not belong to a package, ignoring
core_pattern이 |로 시작하면 커널은 코어를 파일이 아니라 그 프로그램의 표준 입력으로 흘려보내는데, Ubuntu가 지정한 apport는 데비안 패키지에 속하지 않는 실행 파일을 그냥 버린다. 여기에 ulimit -c까지 0이라, 파일로 받도록 바꾸기만 해서는 크기 0으로 잘린다.
| 설정 | Ubuntu 기본값 | 코어를 받으려면 |
|---|---|---|
kernel.core_pattern | apport 파이프 | 파일 경로 패턴 또는 systemd-coredump |
ulimit -c (RLIMIT_CORE) | 0 | unlimited |
fs.suid_dumpable | 2 | 그대로 (root만 읽기 가능) |
core_pattern을 파일로 바꿔 직접 받기
sudo mkdir -p /var/lib/coredumps
sudo chmod 1777 /var/lib/coredumps
sudo sysctl -w kernel.core_pattern='/var/lib/coredumps/core.%e.%p.%t'
# ulimit 0 상태 그대로 실행
(ulimit -c 0; ./crash test); ls /var/lib/coredumps/
# 제한을 풀고 실행
(ulimit -c unlimited; ./crash test); ls -l /var/lib/coredumps/Segmentation fault
(비어 있음)
Segmentation fault (core dumped)
-rw------- 1 noble noble 446464 Aug 8 20:42 core.crash.3311.1786189346
ulimit -c 0일 때는 셸 메시지에서 (core dumped)가 빠진다 — 커널이 덤프 자체를 건너뛰었다는 뜻이다. 설정을 영구화하려면 /etc/sysctl.d/와 /etc/security/limits.conf에 함께 넣어야 한다.
| 패턴 | 의미 |
|---|---|
%e | 실행 파일 이름 (경로 제외) |
%p | PID (네임스페이스 기준) |
%P | 글로벌 PID (컨테이너 안에서 유용) |
%t | 덤프 시각 (epoch 초) |
%s | 덤프를 유발한 시그널 번호 |
%h | 호스트명 |
gdb로 크래시 지점 찾기
gdb -q -batch \
-ex 'bt' \
-ex 'frame 0' -ex 'info locals' -ex 'print p' \
-ex 'print $_siginfo._sifields._sigfault.si_addr' \
./crash /var/lib/coredumps/core.crash.3311.1786189346Core was generated by `./crash test'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x00005c2a8ab441e7 in walk (head=0x5c2aae8452a0) at crash.c:21
21 printf("id=%d name=%s\n", p->id, p->name);
#1 0x00005c2a8ab44275 in main (argc=2, argv=0x7ffef6b79e88) at crash.c:31
#0 0x00005c2a8ab441e7 in walk (head=0x5c2aae8452a0) at crash.c:21
p = 0x10
$1 = (struct node *) 0x10
$2 = (void *) 0x10
si_addr가 실제로 접근을 시도한 주소다. 여기서는 p의 값과 si_addr가 모두 0x10으로 일치하므로, 포인터가 오염된 게 아니라 애초에 0x10이 대입된 것이 원인이라고 좁힐 수 있다.
심볼이 없으면 무엇이 보이는가
gcc -O2 -o crash-nodbg crash.c && strip crash-nodbg
(ulimit -c unlimited; ./crash-nodbg test)
gdb -q -batch -ex 'bt' ./crash-nodbg /var/lib/coredumps/core.crash-nodbg.*#0 0x00005ed8209210f8 in ?? ()
#1 0x000074b647a2a1ca in __libc_start_call_main (main=main@entry=0x5ed8209210a0, ...) at ../sysdeps/nptl/libc_start_call_main.h:58
#2 0x000074b647a2a28b in __libc_start_main_impl (main=0x5ed8209210a0, argc=2, ...) at ../csu/libc-start.c:360
#3 0x00005ed820921165 in ?? ()
애플리케이션 프레임이 전부 ??로 나온다. 배포 바이너리는 작게 유지하면서 분석 가능성도 남기려면 디버그 정보를 별도 파일로 떼어내고 gnu_debuglink로 연결한다.
gcc -g -O2 -o crash-sep crash.c
objcopy --only-keep-debug crash-sep crash-sep.debug
objcopy --strip-debug --add-gnu-debuglink=crash-sep.debug crash-sep
ls -l crash-sep crash-sep.debug
(ulimit -c unlimited; ./crash-sep test)
gdb -q -batch -ex 'bt' ./crash-sep /var/lib/coredumps/core.crash-sep.*-rwxrwxr-x 1 noble noble 16016 Aug 8 20:43 crash-sep
-rwxrwxr-x 1 noble noble 8000 Aug 8 20:43 crash-sep.debug
#0 0x00005c6f618710f8 in walk (head=<optimized out>) at crash.c:21
21 printf("id=%d name=%s\n", p->id, p->name);
#1 main (argc=<optimized out>, argv=0x7fffa584b368) at crash.c:31
함수명과 소스 라인은 되살아났지만 -O2 때문에 인자가 <optimized out>으로 나오고 walk()가 인라인되어 프레임도 하나 줄었다. 변수 값까지 봐야 한다면 -Og 정도로 낮추는 편이 낫다.
systemd-coredump와 coredumpctl
코어 파일을 직접 관리하는 대신 systemd에 맡기면 압축 저장·메타데이터 기록·자동 정리를 한 번에 해결할 수 있다. 설치하면 core_pattern이 자동으로 교체된다.
sudo apt install -y systemd-coredump
cat /proc/sys/kernel/core_pattern
(ulimit -c unlimited; ./crash test)
coredumpctl list|/usr/lib/systemd/systemd-coredump %P %u %g %s %t 9223372036854775808 %h %d
Segmentation fault (core dumped)
TIME PID UID GID SIG COREFILE EXE SIZE
Sat 2026-08-08 20:44:56 KST 5131 1000 1000 SIGSEGV present /home/noble/coredemo/crash 17.2K
파일로 받았을 때 446KB였던 코어가 17.2KB로 줄었다 — systemd가 zstd로 압축해 /var/lib/systemd/coredump/에 넣기 때문이다. coredumpctl info는 코어를 풀지 않고도 스택 트레이스와 실행 환경을 보여준다.
coredumpctl info crash PID: 5131 (crash)
UID: 1000 (noble)
Signal: 11 (SEGV)
Timestamp: Sat 2026-08-08 20:44:56 KST (6s ago)
Command Line: ./crash test
Executable: /home/noble/coredemo/crash
Control Group: /user.slice/user-1000.slice/session-21.scope
Storage: /var/lib/systemd/coredump/core.crash.1000.7742....5131.1786189496000000.zst (present)
Size on Disk: 17.2K
Message: Process 5131 (crash) of user 1000 dumped core.
Stack trace of thread 5131:
#0 0x00006272109a21e7 n/a (/home/noble/coredemo/crash + 0x11e7)
#1 0x00006272109a2275 n/a (/home/noble/coredemo/crash + 0x1275)
coredumpctl debug는 압축 해제와 gdb 실행을 한 번에 처리한다. 배치로 돌리면 CI에서도 그대로 쓸 수 있다.
coredumpctl debug crash --debugger-arguments="-q -batch -ex bt -ex 'print p'"
coredumpctl dump crash --output=/tmp/crash.core # 원본 코어를 파일로 추출#0 0x00006272109a21e7 in walk (head=0x62723e8162a0) at crash.c:21
21 printf("id=%d name=%s\n", p->id, p->name);
#1 0x00006272109a2275 in main (argc=2, argv=0x7ffc1a917038) at crash.c:31
$1 = (struct node *) 0x10
죽지 않은 프로세스 덤프하기 (gcore)
행(hang)이 걸렸거나 메모리가 계속 늘어나는 프로세스는 죽이지 않고 스냅샷만 떠서 분석한다.
sleep 300 &
gcore -o /tmp/live $!ptrace: Inappropriate ioctl for device.
gcore: failed to create /tmp/live.4198
Yama LSM의 ptrace_scope가 1이면 자기 자손이 아닌 프로세스에는 attach할 수 없다. sudo로 올리거나 값을 임시로 0으로 내리면 된다.
cat /proc/sys/kernel/yama/ptrace_scope
sudo gcore -o /tmp/live 4198
gdb -q -batch -ex 'bt' /usr/bin/sleep /tmp/live.41981
Saved corefile /tmp/live.4198
[Inferior 1 (process 4198) detached]
#0 0x000078096d0ecb7a in __GI___clock_nanosleep (clock_id=0, flags=0, req=0x7fff4cfd0fb0, rem=0x7fff4cfd0fa0) at ../sysdeps/unix/sysv/linux/clock_nanosleep.c:78
#1 0x000078096d0f9b27 in __GI___nanosleep (req=<optimized out>, rem=<optimized out>) at ../sysdeps/unix/sysv/linux/nanosleep.c:25
#2 0x000064c57c70ca7f in ?? ()
gcore는 대상 프로세스를 잠깐 멈춘 뒤 재개시키므로, 메모리가 큰 프로세스에서는 덤프를 쓰는 동안 수 초간 정지할 수 있다.
setuid 프로그램의 코어는 어떻게 되는가
setuid 바이너리의 코어에는 권한 상승된 상태의 메모리가 그대로 들어 있어 별도 규칙이 적용된다. “setuid는 코어를 안 남긴다”고 알려져 있지만, systemd가 fs.suid_dumpable을 2로 올려두는 요즘 Ubuntu에서는 사실이 아니다.
sudo cp crash crash-suid && sudo chown root:root crash-suid && sudo chmod 4755 crash-suid
sysctl fs.suid_dumpable
(ulimit -c unlimited; ./crash-suid test)
sudo ls -l /var/lib/systemd/coredump/
sudo getfacl -p /var/lib/systemd/coredump/core.crash*.zst | grep -E '^# file|^user'fs.suid_dumpable = 2
Segmentation fault (core dumped)
-rw-r-----+ 1 root root 17697 Aug 8 20:44 core.crash.1000.7742...5131....zst
-rw-r----- 1 root root 17783 Aug 8 20:45 core.crash-suid.1000.7742...5384....zst
# file: core.crash.1000....zst
user::rw-
user:noble:r--
# file: core.crash-suid.1000....zst
user::rw-
일반 프로그램의 코어에는 소유자에게 읽기 권한을 주는 ACL(user:noble:r--)이 붙지만, setuid 프로그램의 코어에는 그 ACL이 없다. 그래서 코어를 만든 당사자도 읽지 못한다.
coredumpctl info crash-suid | grep Storage Storage: /var/lib/systemd/coredump/core.crash-suid.1000....zst (inaccessible)
fs.suid_dumpable을 0으로 되돌리면 그때는 코어가 아예 생기지 않고 셸 메시지에서도 (core dumped)가 사라진다.
주의사항
- 컨테이너 안에서
core_pattern을 바꿔도 소용없다. 이 값은 호스트 커널 전역이라 컨테이너의 코어도 호스트 설정을 따른다. 파이프 핸들러는 호스트 네임스페이스에서 실행되므로 경로 패턴에는%P(글로벌 PID)를 넣어야 대상을 특정할 수 있다. - systemd 서비스로 뜬 프로세스는 셸의
ulimit과 무관하다. 유닛 파일에LimitCORE=infinity를 넣어야 한다. systemd-coredump는/etc/systemd/coredump.conf의ProcessSizeMax·ExternalSizeMax(64비트 기본 각각 32G)를 넘으면 코어를 저장하지 않고coredumpctl list의COREFILE칸에none만 남긴다. 디스크 여유가 없을 때도KeepFree에 걸려 조용히 버려지므로, 코어가 안 보이면journalctl -u systemd-coredump를 먼저 확인한다.- 코어에는 메모리에 있던 비밀번호·토큰·개인정보가 그대로 들어간다. 저장 디렉터리 권한을 조이고
MaxUse/MaxRetentionSec로 보존 기간을 제한한다. - 덤프 대상 메모리 영역은
/proc/PID/coredump_filter로 조절한다. 공유 메모리나 파일 매핑까지 포함시키면 코어가 수십 GB로 불어날 수 있다. - 코어를 다른 장비에서 분석하려면 실행 파일뿐 아니라 크래시 당시의 공유 라이브러리도 같은 버전이어야 한다. 버전이 어긋나면 백트레이스가 조용히 엉뚱하게 나온다.
마무리
정리하면 코어를 확보하는 데 필요한 건 세 가지다. core_pattern을 파일이나 systemd-coredump로 지정하고, RLIMIT_CORE를 풀고, 디버그 심볼을 남겨두는 것. 이 셋 중 하나만 빠져도 정작 사고가 났을 때 ??만 잔뜩 찍힌 백트레이스를 보게 된다. 운영 장비라면 사고가 나기 전에 일부러 한 번 죽여보고 코어가 실제로 떨어지는지 확인해두는 편이 좋다.