스크랩

C 바깥에서 errno 읽기: Hare로 perror 출력을 가로채 오류 번호를 되찾는 기묘한 꼼수

C 바깥에서 errno 읽기: Hare로 perror 출력을 가로채 오류 번호를 되찾는 기묘한 꼼수

한눈에 보기

C가 아닌 언어에서 libc 함수를 호출하고 나면 늘 같은 문제가 생깁니다. errno 값을 어떻게 읽을 것인가? 이 글은 Hare 언어로 진행된 실험을 소개합니다. 이 실험은 C 래퍼 함수도, libc 내부 구현도 쓰지 않습니다. 대신 perror가 출력하는 오류 메시지를 가로챈 뒤 숫자 오류 코드로 되돌립니다.

핵심 아이디어는 세 가지입니다.

  • perror는 errno를 직접 읽는 거의 유일한 표준 함수입니다. 그래서 그 출력(stderr)을 임시 파일로 돌려 메시지를 캡처합니다.
  • 초기화 때 strerror로 오류 메시지 테이블을 만들어 둡니다. 캡처한 문자열과 일치하는 항목의 인덱스가 곧 errno 값입니다.
  • strerror_r의 GNU/POSIX 버전 차이나 구현마다 다를 수 있는 SEEK_SET 값 같은 이식성 함정도 하나씩 우회합니다.

원작자도 "괴상하고 엉망인" 방법이라고 부릅니다. 그래도 이게 실제로 된다는 점이 흥미롭고, libc와 FFI의 경계를 이해하는 데도 좋은 사례입니다.


왜 C 밖에서 errno를 읽기 어려울까?

errno는 평범한 전역 변수처럼 보이지만 실제로는 매크로입니다. 확장 결과는 구현마다 다르고(예: 스레드 로컬 변수의 주소를 반환하는 함수 호출), 이식성이나 안정성도 보장되지 않습니다.

그래서 다른 언어에서 errno를 얻으려면 보통 둘 중 하나를 택합니다.

  1. 구현 세부사항에 의존하기: glibc의 __errno_location, BSD의 __error 등
  2. C 래퍼 작성하기: errno를 반환하는 작은 C 함수를 만들어 링크

이 실험은 두 방법을 모두 쓰지 않는 것이 목표입니다. libc에는 errno를 반환하는 함수가 없습니다. 하지만 오류를 설명하는 문자열을 주는 함수는 있습니다.

| 함수 | 표준 | errno 직접 읽음? | |---|---|---| | strerror | C | ❌ (오류 번호를 인자로 받음) | | perror | C | ✅ | | strerror_r | POSIX | ❌ | | strerror_l | POSIX | ❌ |

이 중 errno를 스스로 읽는 함수는 perror뿐입니다. 문제는 결과를 stderr에 출력한다는 점입니다. 그렇다면 stderr에 실제로 찍히지 않게 하면서 그 출력을 캡처하면 됩니다.


1단계: stderr를 임시 파일로 돌려 메시지 캡처하기

초기화와 정리

  • 프로그램 초기화 단계에서 dup으로 원래 stderr를 보관합니다.
  • mkstemp로 임시 파일을 만든 뒤, dup2로 STDERR_FILENO(2번)를 이 임시 파일에 연결합니다.
  • 종료할 때는 임시 파일 디스크립터를 닫고 파일을 삭제합니다.
  • stderr를 쓰는 다른 libc/시스템 호출 래퍼는 2번 대신 보관해 둔 원래 stderr 디스크립터를 사용하도록 바꿉니다.

💡 주의할 점: 이 전환은 오류가 발생하기 전부터 유지해야 합니다. perror 호출 직전에 dup2로 전환하면 dup2 자체가 errno를 덮어쓸 수 있기 때문입니다.

문자열을 반환하는 첫 번째 get_errno()

  1. perror(null)을 호출해 임시 파일에 오류 메시지를 기록합니다.
  2. 뒤에 널 바이트를 씁니다.
  3. lseek로 파일 시작 위치로 이동한 뒤 256바이트 정적 버퍼로 읽어 들입니다.
  4. strnlen으로 길이를 구해 문자열을 반환하고, 파일 위치를 다시 처음으로 되돌립니다.
  5. 널 바이트 쓰기나 읽기가 실패하면 "Unknown error"를 반환합니다.

여기까지만 해도 "오류 메시지 문자열"은 얻을 수 있습니다.


2단계: 오류 문자열을 숫자로 역변환하기

문자열만으로는 부족합니다. EBADF인지 ENOENT인지 숫자로 비교할 수 있어야 하니까요.

오류 메시지 테이블 만들기

  • 초기화 함수에서 256×256바이트 테이블을 만듭니다.
  • 오류 번호 0부터 255까지 strerror가 반환한 문자열을 테이블에 복사합니다.

왜 strerror_r이 아니라 strerror일까?

스레드 안전성만 보면 strerror_r이 더 나아 보입니다. 하지만 이 함수에는 서로 호환되지 않는 두 버전이 있습니다.

  • GNU 버전(비표준): 오류 메시지 포인터를 반환하며, 자체 불변 정적 저장 공간을 쓸 수 있습니다.
  • POSIX 버전: 항상 사용자 버퍼에 기록하고 int를 반환합니다.

심볼 이름도 libc마다 다릅니다.

  • glibc: strerror_r 심볼은 GNU 버전이고, POSIX 버전은 __xpg_strerror_r입니다.
  • musl, BSD libc: strerror_r 심볼로 POSIX 버전만 제공합니다.

결국 POSIX 버전을 확실히 호출하려면 glibc 내부 심볼명에 의존해야 합니다. "libc 독립성"이라는 목표와 맞지 않으므로 단순한 strerror를 택한 것입니다.

숫자를 반환하는 get_errno()

  • 캡처한 메시지 끝의 개행 문자를 널 문자로 바꿉니다.
  • 테이블 전체와 strcmp로 비교해 일치하는 항목의 인덱스를 정수 기반 errno 타입으로 반환합니다.
  • 버퍼는 정적 버퍼 대신 지역 버퍼를 씁니다.
  • 쓰기·읽기에 실패하거나 일치하는 항목이 없으면 -1을 반환합니다.

write 래퍼 예시

  • 대상이 STDERR_FILENO이면 보관해 둔 원래 stderr 디스크립터로 바꿔서 씁니다.
  • 실제 write가 -1을 반환하면 get_errno()를 호출해 오류 코드를 돌려줍니다.
  • 성공하면 기록한 바이트 수를 반환합니다.

3단계: 범용 POSIX 타깃과 SEEK_SET 우회

실험의 진짜 출발점

원래 목적은 따로 있었습니다. Hare가 공식 지원하지 않는 POSIX 시스템에서도 범용 "posix" 타깃으로 컴파일하고 시스템 libc에 링크해 실행할 수 있는지 확인하는 것이었습니다. 오류 번호는 시스템마다 다르기 때문에 이 타깃에서는 원래 오류 문자열만 반환하려 했습니다. errno 숫자 복원은 그 과정에서 나온 파생 실험입니다.

SEEK_SET도 0이라는 보장이 없다

앞의 코드는 SEEK_SET = 0으로 정의합니다. 하지만 POSIX는 이 값을 보장하지 않고 구현 정의로 남겨 둡니다. 대부분 0이긴 해도, 이 역시 C 밖에서 구현 세부사항에 기대는 지점입니다.

우회 방법은 lseek의 반환값을 확인하는 것입니다.

  • 오프셋 0과 후보 whence 값을 넣어 lseek를 호출합니다.
  • 반환된 오프셋이 0이면 파일 시작으로 이동한 것으로 보고, 그 값을 SEEK_SET으로 간주합니다.
  • 예제는 후보를 0부터 2까지 순회하며, 찾은 값을 파일 위치를 되돌릴 때도 사용합니다.
  • 0~2 범위는 "아마 이 중 하나일 것"이라는 예상일 뿐이므로 더 큰 값도 검사할 수 있습니다.

실제로 동작할까? EBADF로 검증하기

최종 예제는 일부러 write(-1, ...)를 호출해 잘못된 파일 디스크립터 오류를 일으킵니다.

  • 호출이 성공하면 중단합니다(있을 수 없는 상황이므로).
  • 오류가 나면 복원한 정수 값이 sys::EBADF와 같은지 단언(assert)합니다.

그리고 이 단언은 통과합니다. 즉 C 래퍼 없이, libc 내부 심볼 없이도 errno 값을 되찾을 수 있다는 것이 증명됩니다.


에디터의 한마디

이 방법을 실무에 그대로 쓰라고 권하기는 어렵습니다. 실제로 적용하기엔 걸리는 점이 많습니다.

  • 오류마다 파일 I/O가 발생해 성능 오버헤드가 있습니다.
  • 서로 다른 errno가 같은 메시지를 갖거나 로케일에 따라 메시지가 바뀌면 역변환이 모호해질 수 있습니다.
  • strerror는 스레드 안전성이 보장되지 않고, stderr 리디렉션은 멀티스레드 환경에서 위험합니다.

그래도 이 실험의 가치는 방법이 존재한다는 것을 보여준 데 있습니다. Rust, Zig, Go, Hare 등 C 이외의 언어로 시스템 프로그래밍을 하거나 FFI를 설계해 본 분이라면, 왜 많은 언어가 결국 __errno_location 같은 구현 세부사항에 기대는지 체감할 수 있을 겁니다. "표준만으로 어디까지 갈 수 있는가"를 끝까지 밀어붙인 흥미로운 해킹 기록입니다.


원문: https://news.hada.io/topic?id=35910