세그멘테이션 오류 로그 확인 2026, Bash exec 최적화와 CI 장애 재현 기준

세그멘테이션 오류 로그 확인이 안 되면 개발자는 보통 C 코드나 런타임부터 의심한다.
하지만 충돌한 프로그램이 조용히 사라진 것처럼 보이는 원인은 코드가 아니라 셸 실행 경로일 때가 있다.
특히 파일 감시 도구, Bash -c, CI one-liner, 자동 재빌드 명령을 섞으면 SIGSEGV 메시지를 찍어줄 부모 셸이 남지 않는 경로가 생긴다.
이 글은 GeekNews에서 발견한 사례를 출발점으로 삼되, GNU Bash와 Linux wait 상태, signal 문서를 기준으로 실무 재현 기준을 정리한다.
결론은 단순하다.
세그멘테이션 오류 로그 확인은 화면 메시지에 맡기지 말고 종료 상태, signal 번호, stdout/stderr, 빌드 조건을 파일로 남기는 래퍼를 기준으로 해야 한다.
- Segmentation fault 문구는 충돌한 프로그램이 직접 출력하는 메시지가 아니라 부모 셸이 자식 종료 상태를 해석해 보여주는 경우가 많다.
- Bash가 단순한 마지막 명령을 execve로 교체하면 메시지를 찍을 부모 Bash가 사라질 수 있다.
- CI와 파일 감시 자동화에서는 화면 문구보다 exit code, signal, stderr, core dump 정책을 아티팩트로 남겨야 한다.
- 세그멘테이션 오류 로그 확인 기준은 개발자 개인 터미널이 아니라 재현 가능한 래퍼 스크립트와 테스트 정책으로 고정해야 한다.
이 글이 필요한 사람
- entr, watchexec, nodemon류 파일 감시 도구로 C, Rust, Go 네이티브 코드를 반복 실행하는 개발자.
- GitHub Actions, GitLab CI, Jenkins에서 충돌 로그가 간헐적으로 사라져 재현성을 잃은 플랫폼팀.
- Segmentation fault가 보이지 않는다는 이유로 테스트 성공과 충돌 실패를 구분하지 못한 QA 담당자.
- SIGSEGV, core dumped, exit 139 같은 상태를 운영 로그와 알림으로 연결해야 하는 SRE.
- 네이티브 확장, 데이터베이스 플러그인, 이미지 처리 모듈처럼 메모리 오류가 비용으로 이어지는 팀의 기술 리드.
세그멘테이션 오류 로그 확인의 첫 오해
Segmentation fault 문구는 충돌한 프로세스가 마지막 힘으로 예쁘게 남기는 로그가 아니다.
Linux signal 문서는 SIGSEGV를 잘못된 메모리 참조에서 발생하는 signal로 설명하고, 기본 동작은 프로세스 종료와 core dump 계열이다.
waitpid 문서는 부모 프로세스가 자식 종료 상태를 회수하고 WIFSIGNALED와 WTERMSIG 같은 매크로로 signal 종료 여부를 확인하는 방식을 제공한다.
즉 화면에 보이는 Segmentation fault 메시지는 자식이 죽은 뒤 부모가 상태를 해석해 출력하는 관찰 결과에 가깝다.
| 상황 | 개발자가 보는 증상 | 실제 확인할 것 | 실무 판단 |
|---|---|---|---|
| 로컬 터미널에서 직접 실행 | Segmentation fault 또는 core dumped 문구가 보임 | exit code 139, ulimit, core pattern | 메시지와 종료 상태를 같이 기록 |
| Bash -c 마지막 명령 | 충돌했는데 문구가 안 보일 수 있음 | 부모 Bash가 남아 wait 했는지 확인 | 후속 명령 또는 래퍼로 고정 |
| 파일 감시 자동 실행 | 빌드 출력 뒤 실행 로그가 비어 있음 | 감시 도구가 어떤 셸로 실행하는지 확인 | watch one-liner 대신 스크립트 사용 |
| CI job | job failed만 보이고 signal 원인이 약함 | 아티팩트와 exit-status 파일 확인 | signal-aware wrapper를 표준화 |
| 컨테이너 테스트 | core dump 파일이 없고 exit 139만 남음 | ulimit, kernel core pattern, container securityContext | 디버그 job과 운영 job을 분리 |
Bash exec 최적화가 로그를 흐리게 만드는 경로
GNU Bash의 실행 환경 문서는 명령 실행과 환경, 자식 프로세스, 종료 상태를 셸이 관리한다는 전제를 둔다.
문제는 Bash가 항상 부모로 남아 자식을 기다리는 방식만 쓰지는 않는다는 점이다.
Bash 구현에는 더 실행할 일이 없는 단순 명령 경로에서 새 프로세스를 남기지 않고 execve로 자신을 교체하는 최적화가 있다.
이 최적화 자체는 정상 동작이며 대부분의 명령에서는 눈에 띄지 않는다.
그러나 충돌한 마지막 프로그램의 signal 종료 메시지를 부모 Bash가 찍어주길 기대한 경우에는 관찰 결과가 달라진다.
- gcc -o hello hello.c && ./hello 같은 one-liner에서 마지막 명령이 충돌하면 Bash가 계속 남아 있지 않을 수 있다.
- bash -c로 감싸도 남은 작업이 사실상 마지막 실행 하나라면 같은 관찰 문제가 생길 수 있다.
- 별도 스크립트는 shebang으로 시작된 Bash가 자식 프로세스를 실행하고 기다리므로 메시지가 다시 보일 수 있다.
- 충돌 명령 뒤에 후속 작업을 남기거나 서브셸로 감싸면 부모 셸이 종료 상태를 회수할 기회가 생긴다.
- CI에서는 이 차이를 사람의 터미널 감각이 아니라 명시적 래퍼와 아티팩트로 처리해야 한다.
파일 감시 도구에서는 one-liner를 줄인다
entr 프로젝트는 파일 변경에 맞춰 명령을 다시 실행하는 간단한 도구다.
이런 도구는 빠른 피드백에는 좋지만, 빌드와 실행, 종료 상태 해석, 로그 저장을 한 줄에 몰아넣기 쉽다.
세그멘테이션 오류 로그 확인이 필요한 네이티브 코드에서는 one-liner가 편의보다 리스크가 된다.
# entr에서 충돌 메시지 확인이 약할 때 비교하는 실행 예시
ls hello.c | entr -s './segfault-runner.sh'
# Bash가 마지막 명령으로 자신을 교체하는 경로를 피하려면 후속 작업을 둔다.
ls hello.c | entr -s "bash -c 'gcc -g -O0 -o hello hello.c && ./hello; rc=$?; echo exit=$rc; exit $rc'"
# 충돌 프로그램을 서브셸로 감싸 부모 Bash가 회수 메시지를 남기게 하는 방법도 있다.
ls hello.c | entr -s 'gcc -g -O0 -o hello hello.c && (./hello)'
이 예시는 Bash가 어떤 경로로 실행되는지 확인하기 위한 재현용이다.
팀 표준으로는 파일 감시 명령 안에 복잡한 조건문을 넣기보다, 아래처럼 signal-aware wrapper를 별도 파일로 두는 편이 낫다.
signal-aware 래퍼 스크립트
세그멘테이션 오류 로그 확인을 사람 눈에 보이는 문구에 맡기지 않으려면 종료 상태를 직접 파일로 남긴다.
#!/usr/bin/env bash
# segfault-runner.sh
# 목적: 파일 감시, CI, 로컬 재현에서 SIGSEGV 종료를 숨기지 않고 기록한다.
# 실제 컴파일 명령과 테스트 바이너리 경로는 프로젝트에 맞게 바꾼다.
set -o pipefail
build_cmd=(gcc -g -O0 -Wall -Wextra -o hello hello.c)
run_cmd=(./hello)
log_dir="${SEGFAULT_LOG_DIR:-./artifacts/crash}"
mkdir -p "$log_dir"
printf '[segfault-runner] build start
'
"${build_cmd[@]}"
printf '[segfault-runner] run start
'
"${run_cmd[@]}" >"$log_dir/stdout.log" 2>"$log_dir/stderr.log"
rc=$?
if (( rc >= 128 )); then
signal=$((rc - 128))
printf '[segfault-runner] terminated by signal %s, exit=%s
' "$signal" "$rc" >&2
printf 'signal=%s
exit=%s
' "$signal" "$rc" >"$log_dir/exit-status.env"
else
printf '[segfault-runner] exit=%s
' "$rc"
printf 'signal=0
exit=%s
' "$rc" >"$log_dir/exit-status.env"
fi
exit "$rc"
Bash 관례에서 signal로 종료된 명령은 128에 signal 번호를 더한 exit status로 보이는 경우가 많다.
SIGSEGV는 일반적으로 11번이므로 exit 139가 대표적인 단서가 된다.
다만 이 숫자만으로 원인을 단정하지 말고 stderr, 컴파일 플래그, sanitizer 결과, core dump 정책을 함께 남겨야 한다.
wait 상태로 확인하는 재현 스크립트
셸 메시지 출력 경로가 헷갈릴 때는 wait 상태를 직접 확인하는 작은 재현 스크립트가 도움이 된다.
#!/usr/bin/env python3
# wait-signal-check.py
# 목적: 셸 메시지 유무와 별개로 자식 프로세스가 어떤 signal로 종료됐는지 확인한다.
# Linux/Unix 계열 CI에서 재현용으로 쓰고, 운영 경로에서는 샌드박스에서만 실행한다.
import os
import signal
import subprocess
import sys
cmd = sys.argv[1:] or ["./hello"]
proc = subprocess.Popen(cmd)
pid, status = os.waitpid(proc.pid, 0)
if os.WIFSIGNALED(status):
sig = os.WTERMSIG(status)
print(f"pid={pid} signaled={sig} name={signal.Signals(sig).name}")
sys.exit(128 + sig)
if os.WIFEXITED(status):
code = os.WEXITSTATUS(status)
print(f"pid={pid} exited={code}")
sys.exit(code)
print(f"pid={pid} status={status}")
sys.exit(1)
이 스크립트는 waitpid 계열 상태 해석을 Python으로 노출해 signal 종료와 정상 exit를 분리한다.
운영 서버에서 임의 바이너리를 실행하는 용도로 쓰면 안 되며, 샌드박스나 CI 재현 job에서만 제한적으로 사용한다.
CI에서는 로그보다 아티팩트 계약이 먼저다
CI 화면에 Segmentation fault 한 줄이 보였는지 여부로 실패를 판정하면 재현성이 약해진다.
플랫폼팀은 job 실패 메시지보다 표준 아티팩트 계약을 먼저 정해야 한다.
| 아티팩트 | 남기는 이유 | 없을 때 생기는 문제 | 담당자 |
|---|---|---|---|
| stdout.log | 실행 경로와 일반 출력 확인 | 빌드와 실행 출력이 섞여 원인 분리 실패 | 개발팀 |
| stderr.log | 런타임 경고와 crash 메시지 확인 | signal 종료가 테스트 실패처럼 보임 | 개발팀 |
| exit-status.env | exit code와 signal 번호 고정 | 화면 문구에 따라 판정이 흔들림 | CI 플랫폼팀 |
| compiler-version.txt | 컴파일러와 libc 차이 확인 | 로컬과 CI 결과가 달라져도 원인 추적 불가 | 빌드 담당자 |
| core policy note | core dump 허용 여부 기록 | core가 없는 이유를 매번 다시 조사 | SRE 또는 보안팀 |
name: segfault-reproduction
on:
pull_request:
workflow_dispatch:
jobs:
reproduce:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Build with debug symbols
run: gcc -g -O0 -Wall -Wextra -o hello hello.c
- name: Run through signal-aware wrapper
run: ./scripts/segfault-runner.sh
- name: Upload crash artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: segfault-artifacts
path: artifacts/crash/
GitHub Actions 예시는 재현 흐름을 보여주는 최소 스켈레톤이다.
실제 조직에서는 checkout 권한, artifact 보존 기간, 비밀값 노출, self-hosted runner 격리 기준을 함께 봐야 한다.
실무 시나리오 1: 로컬에서는 보이고 CI에서는 안 보이는 충돌
개발자 노트북에서는 Segmentation fault가 보이는데 CI에서는 exit 1처럼만 보이는 상황을 생각해보자.
이 조건이면 CI 래퍼부터 고친다.
로컬과 CI의 컴파일 플래그가 다르고, CI 로그에 stdout과 stderr가 섞여 있으며, 종료 상태 파일이 없는 경우다.
이 경우는 코드 수정보다 재현 환경 정리가 먼저다.
빌드 플래그, 실행 명령, sanitizer 사용 여부, artifact 경로를 맞춘 뒤 같은 signal이 반복되는지 본다.
실무 시나리오 2: 파일 감시 루프에서만 사라지는 메시지
파일을 저장할 때마다 entr가 gcc와 실행 파일을 돌리는데 충돌 메시지가 사라지는 상황도 흔하다.
이 조건이면 one-liner를 줄이고 별도 스크립트로 옮긴다.
감시 도구가 셸 문자열을 받아 실행하고, 마지막 명령이 바로 충돌 프로그램이며, 후속 상태 기록이 없는 경우다.
이 경우는 파일 감시 도구의 버그로 단정하지 않는다.
부모 셸이 남아 있는 실행 경로인지 확인하고, wrapper가 exit code와 signal 파일을 남기는지 먼저 본다.
실무 시나리오 3: 네이티브 확장 배포 전 품질 게이트
Python, Node.js, Postgres 확장처럼 네이티브 모듈을 배포하는 팀은 작은 crash도 운영 비용으로 이어진다.
이 조건이면 세그멘테이션 오류 로그 확인을 release gate로 올린다.
테스트가 컨테이너에서 돌고, sanitizer job이 있으며, core dump 정책을 보안팀이 승인할 수 있는 경우다.
이 경우는 보류한다.
충돌이 한 번이라도 재현됐지만 exit status만 남고 stderr, 빌드 플래그, runner 정보가 사라진 경우다.
이때는 원인 분석보다 먼저 재현 실패를 실패로 보는 정책을 세운다.
비용·보안·운영 리스크
세그멘테이션 오류 로그 확인은 작은 디버깅 팁처럼 보이지만, 실제로는 장애 대응 비용과 보안 정책에 닿아 있다.
core dump를 무조건 켜면 분석은 쉬워지지만, 메모리 안의 개인정보나 토큰 조각이 파일로 남을 수 있다.
반대로 core dump를 전부 막으면 원인 분석은 stderr와 재현 스크립트에 더 의존하게 된다.
| 리스크 | 실패 징후 | 사전 통제 | 비용 영향 |
|---|---|---|---|
| 관찰 실패 | 충돌 메시지가 사라져 성공처럼 보임 | signal-aware wrapper와 exit-status 파일 | 디버깅 시간 증가 방지 |
| core dump 노출 | 민감 메모리 조각이 artifact로 저장됨 | core dump 허용 job 분리와 보존 기간 제한 | 보안 사고 비용 감소 |
| CI 재현 실패 | 로컬에서만 crash가 보임 | 컴파일러·runner·환경 변수 기록 | 장애 회고 반복 비용 감소 |
| 로그 과다 | 전체 stdout/stderr를 무기한 보관 | 실패 job 중심 보존과 마스킹 | 저장 비용과 노출면 축소 |
| 책임 불명확 | 개발팀과 플랫폼팀이 서로 도구 탓으로 넘김 | 아티팩트 계약과 owner 지정 | 장애 처리 리드타임 감소 |
운영 정책 스켈레톤
아래 정책은 세그멘테이션 오류 로그 확인을 팀 규칙으로 올릴 때 쓸 수 있는 검토용 예시다.
# segfault-triage-policy.yaml
# 목적: 세그멘테이션 오류 로그 확인 실패를 개인 디버깅 문제가 아니라 재현성 게이트로 관리한다.
owner:
dev: runtime-owner
ci: platform-ci
security: appsec
required_artifacts:
- stdout.log
- stderr.log
- exit-status.env
- compiler-version.txt
- binary-build-flags.txt
fail_closed_when:
- signal_exit_status_missing
- core_dump_policy_unknown
- sanitizer_result_missing_for_native_code
- crash_reproduces_only_on_developer_laptop
allow_merge_when:
- crash_not_reproduced_after_wrapper
- test_logs_include_exit_status
- owner_confirms_signal_or_normal_exit
- follow_up_issue_created_for_non_deterministic_failure
핵심은 crash 자체를 없애겠다는 선언이 아니라, crash를 관찰 가능한 실패로 만드는 것이다.
관찰 가능한 실패가 되어야 개발자, 플랫폼팀, 보안팀이 같은 증거를 보고 판단할 수 있다.
도입 전 체크리스트
- 파일 감시 명령과 CI 명령에서 빌드, 실행, 상태 기록을 한 줄에 몰아넣지 않는다.
- 세그멘테이션 오류 로그 확인이 필요한 테스트는 stdout, stderr, exit-status를 별도 파일로 남긴다.
- exit 139를 SIGSEGV 단서로 보되, signal 번호와 OS별 차이를 함께 확인한다.
- core dump를 켤 때는 보안팀과 보존 기간, 접근 권한, 민감정보 마스킹 기준을 먼저 정한다.
- sanitizer job과 일반 unit test job을 분리해 비용과 실행 시간을 관리한다.
- 재현 실패도 실패로 본다는 정책을 PR gate 또는 release gate에 명시한다.
- 파일 감시 도구는 개발 편의용이고, 최종 판정은 CI 래퍼와 artifact 기준으로 한다.
- 장애 회고에는 코드 원인뿐 아니라 관찰 실패와 로그 누락 원인도 기록한다.
함께 보면 좋은 글
세그멘테이션 오류 로그 확인은 CI 비용, 테스트 자동화, 서비스 모니터링, 로그 보존 정책과 함께 봐야 운영 기준이 선다.
자주 묻는 질문
세그멘테이션 오류 로그 확인이 안 되면 프로그램이 충돌하지 않은 건가요?
아니며, 화면에 Segmentation fault 문구가 없어도 exit code와 wait 상태로는 signal 종료가 남아 있을 수 있다.
exit 139는 항상 SIGSEGV라고 봐도 되나요?
많은 Unix 셸 관례에서는 128에 signal 번호를 더하므로 139는 SIGSEGV 단서가 되지만, 실제 판정은 wait 상태와 실행 환경을 같이 확인해야 한다.
Bash -c로 감싸면 왜 메시지가 계속 안 보일 수 있나요?
Bash가 더 실행할 일이 없는 단순 마지막 명령 경로에서 자신을 execve로 교체하면 자식 종료 상태를 해석해 메시지를 남길 부모 Bash가 사라질 수 있다.
CI에서는 core dump를 항상 켜야 하나요?
항상 켜는 방식은 위험하며, 민감정보가 메모리 덤프에 남을 수 있으므로 별도 디버그 job, 접근 권한, 보존 기간, 마스킹 기준을 정한 뒤 제한적으로 켜야 한다.
entr 같은 파일 감시 도구를 쓰지 말아야 하나요?
그렇지는 않지만, crash 재현과 판정이 필요한 작업은 one-liner보다 별도 래퍼 스크립트를 쓰고 종료 상태를 파일로 남기는 편이 안전하다.
로그가 사라지는 문제를 코드 리뷰에서 어떻게 막나요?
네이티브 코드나 확장 모듈 PR에는 signal-aware wrapper, stderr 저장, sanitizer job, artifact 업로드 여부를 체크리스트로 넣는 방식이 실무적이다.
출처와 확인일
- GNU Bash Manual — Command Execution Environment (확인일: 2026-07-13)
- GNU Bash Source — execute_cmd.c execve optimization reference (확인일: 2026-07-13)
- Linux man-pages — signal(7) (확인일: 2026-07-13)
- Linux man-pages — waitpid(2) (확인일: 2026-07-13)
- The Open Group — wait() (확인일: 2026-07-13)
- entr project — entr file watcher (확인일: 2026-07-13)
이 글은 GNU Bash, Linux man-pages, POSIX wait 문서와 entr 프로젝트 문서를 기준으로 정리한 일반적인 IT 운영 가이드다.
OS, 셸, CI runner, 컨테이너 보안 정책, core dump 설정은 조직마다 다르므로 실제 적용 전 공식 문서와 내부 보안 기준을 다시 확인해야 한다.






댓글
댓글 쓰기