1P by GN⁺ | ★ favorite | 댓글 1개
  • entr로 C 프로그램을 반복 컴파일·실행하자 hello가 비정상 종료됐지만, 컴파일 출력 뒤에 Segmentation fault 메시지가 나타나지 않음
  • 이 메시지는 충돌한 프로그램이 아니라, 자식 프로세스를 회수하며 SIGSEGV 종료를 확인한 부모 셸이 출력함
  • bash -c를 명시해도 남은 명령이 사실상 하나뿐이면 Bash가 새 프로세스를 만들지 않고 execve로 자신을 교체할 수 있어 메시지를 출력할 부모 셸이 사라짐
  • 별도 스크립트에서는 shebang으로 시작된 Bash가 ./hello를 자식으로 실행하고 기다리므로, SIGSEGV를 확인해 Segmentation fault (core dumped) 를 출력함
  • ./hello서브셸 (./hello)에서 실행하거나 뒤에 ; true를 추가해 후속 작업을 남기면 exec 최적화를 막고 오류 메시지를 확인할 수 있음

메시지가 사라진 실행 경로

  • 다음 entr 명령으로 hello.c가 바뀔 때마다 컴파일과 실행을 반복함

    ls hello.c | entr -s "gcc -o hello hello.c && ./hello"
    
  • hello가 세그멘테이션 오류로 종료됐지만 Segmentation fault 메시지나 눈에 보이는 0이 아닌 종료 상태 없이 아무 출력도 남지 않음

  • bash -c로 명령을 감싸도 결과는 같았음

    ls hello.c | entr -s "bash -c 'gcc -o hello hello.c && ./hello'"
    
  • 반면 같은 명령을 run.sh로 옮겨 실행하면 오류가 표시됨

    #!/bin/bash
    gcc -o hello hello.c && ./hello
    
    ls hello.c | entr -s ./run.sh
    ./run.sh: line 2: 104465 Segmentation fault (core dumped) ./hello
    
  • Segmentation fault를 출력하는 주체

    • 충돌한 프로그램은 이미 종료된 상태이며, 부모 셸이 자식 프로세스를 회수하면서 SIGSEGV로 죽었다는 사실을 확인한 뒤 메시지를 출력함
    • 따라서 메시지를 출력할 부모 셸이 남아 있지 않으면 아무것도 나타나지 않음
    • Bash는 bash -c "some_command"에서 실행할 일이 사실상 해당 명령뿐이면 새 프로세스를 포크하지 않고 execve로 자신을 교체할 수 있음
    • 이 최적화는 대부분의 실행에서는 외부로 드러나지 않음
    • 스크립트 방식에서는 entr./run.sh를 자식으로 실행하고, shebang이 새 Bash를 시작함
    • 새 Bash는 gcc 실행 후 ./hello를 포크하고 기다리므로, 자식의 SIGSEGV 종료를 확인해 오류 메시지를 남길 수 있음

부모 셸을 남기는 두 가지 방법

  • 충돌하는 명령을 서브셸에서 실행하면 메시지가 다시 나타남
    ls hello.c | entr -s "gcc -o hello hello.c && (./hello)"
    hello.c: line 1: 106595 Segmentation fault (core dumped) ( ./hello )
    
    • 괄호는 ./hello가 포크된 서브셸에서 실행되도록 강제함
    • Bash가 부모 역할을 유지하므로 서브셸의 SIGSEGV 종료를 회수해 메시지를 출력할 수 있음
  • 충돌 명령 뒤에 실행할 작업을 남겨도 exec 최적화를 막을 수 있음
    ls hello.c | entr -s "bash -c 'gcc -o hello hello.c && ./hello; true'"
    bash: line 1: 109516 Segmentation fault (core dumped) ./hello
    
    • ; true 때문에 Bash는 ./hello가 끝난 뒤에도 할 일이 남아 있어 자신을 해당 프로그램으로 교체하지 않음
    • 별도 래퍼 스크립트 없이도 세그멘테이션 오류 메시지를 확인할 수 있음

댓글과 토론

Lobste.rs 의견들
  • 최신 Linux 배포판은 기본적으로 코어 덤프를 비활성화해 둬서 곤란할 때가 많음
    ulimit -c unlimited로 다시 활성화할 수 있지만, 코어 파일이 실행 파일과 같은 디렉터리에 생기지도 않는 듯하며 systemd가 어딘가로 옮기는 것 같음
    OpenBSD처럼 코어 덤프를 기본으로 활성화하고 실행 파일과 같은 디렉터리에 두면 좋겠는데, 지금은 충돌이 발생하면 대개 실행 파일을 gdb 안에서 다시 실행해야 함

    • systemd에서는 coredumpctl 로 최근 코어 덤프를 나열하고 가져올 수 있음
    • 실행 파일이 있는 위치에 코어 덤프를 둔다는 건 꽤 이상한 선택으로 보임
      /usr/bin 같은 곳에서 코어 덤프를 찾을 생각은 하지 않을뿐더러, 그런 위치는 종종 용량이 작거나 읽기 전용인 파일 시스템에 있음
      기본 위치로는 작업 디렉터리가 훨씬 낫지 않을까
    • 코어 파일이 기본적으로 비활성화된 이유는 보안 전문가들이 사용자가 Linux를 안전하게 운영하지 못하며, 방치된 코어 파일이 악용될 수 있다고 보기 때문인 듯함
      심지어 사용자가 코어 파일을 활성화해도 뭘 하는지 모른다고 간주해서, root조차 찾기 어려운 곳에 숨겨 두는 것처럼 느껴짐
  • C나 D 등으로 프로그램을 작성하면서 종료 원인을 알고 싶다면 wait 시스템 호출의 반환값을 확인하면 되며, 프로그램이 시그널로 종료됐는지 알 수 있음
    운영체제가 코어 덤프를 다른 곳으로 전달하면 필요할 때 바로 디버깅하거나 배포판에 오류 보고서를 보낼 수 있다는 장점도 있음
    Linux 커널은 코어 덤프를 생성할 때 /proc/sys/kernel/core_pattern 이라는 특수 파일을 읽어 저장 위치를 결정함
    파일로 쓰는 대신 프로그램에 파이프로 전달할 수도 있어서 GUI 대화상자 표시, 디버거 즉시 실행, 개발자에게 보고서 이메일 전송 같은 처리가 가능함
    systemd는 커널의 core_pattern을 자신이 관리할 값으로 설정하지만, systemd를 사용하지 않아도 동일한 커널 기능을 쓸 수 있음
    Windows XP 개발 당시 코어 덤프를 Microsoft로 전송해 존재조차 몰랐을 여러 버그를 수정했다는 이야기도 있으며, Windows에 개발 도구를 설치하면 자체 프로그램을 개발할 때 주문형 디버거가 꽤 유용함
    이 기능은 2003년 Linux에 도입됐고 자세한 내용은 man 5 core에서 확인할 수 있음

  • 코어 덤프는 디버깅에는 훌륭하지만 무관해 보이는 파일 옆에 놓이면 일반 사용자에게 혼란을 주고, 진단 보고 체계로 쓰기에는 끔찍할 수 있음
    제출된 Windows 충돌 보고서에 얼마나 많은 데이터가 들어가는지 처음 보고 경악했음
    개인정보를 보호하는 충돌 보고 체계를 전제로 한다면, 제출된 충돌을 진단할 수 있도록 스택과 레지스터 정보만으로 원인을 추론하는 법을 익혀야 함

  • Bash가 스크립트 안의 명령을 때때로 직접 exec 한다는 사실은 전혀 몰랐고 놀라운 기능임
    Bash 프로세스 자체의 부하가 크지도 않고 필요한 곳에서는 습관적으로 exec를 사용하기 때문에 다소 잘못 설계된 기능처럼 느껴짐
    다만 전체 실행 횟수를 합치면 예상보다 큰 차이를 만들 수도 있겠다는 생각이 듦