2038 문제
유닉스 계열 시스템은 시각을 1970년 1월 1일부터 흐른 초의 개수로 셉니다. 그 개수를 부호 있는 32비트 정수 한 칸에 담아 온 시스템이 많습니다. 그 칸은 2038년 1월에 다 찹니다.
상세
시각을 담는 타입의 이름은 time_t 입니다. POSIX(Portable Operating System Interface, 이식 가능 운영체제 인터페이스)는 이 타입을 「초 단위 시간에 쓴다」고만 적습니다. 리눅스 time_t(3type) 도 POSIX 에 따라 정수 타입이라고 적습니다.
기준점은 에포크입니다. POSIX 는 에포크를 1970년 1월 1일 0시 0분 0초 협정 세계시로 정의합니다. 그 위에 「에포크 이후의 초」라는 값이 서 있습니다. POSIX 의 표현은 「에포크 이후로 흐른 초의 개수에 근사하는 값」입니다. 근사라는 말이 붙어 있습니다.
근사인 이유는 이 값이 실제 천문 시각을 그대로 따라가지 않기 때문입니다. POSIX 는 국제지구자전좌표국이 정하는 실제 협정 세계시와 시스템의 현재 초 값 사이의 관계를 미지정으로 둡니다. 그 값을 실제 시각에 맞추는 방식은 구현이 정합니다. 대신 한 가지를 못 박습니다. 에포크 이후의 초로 표현할 때 하루하루는 정확히 86400초로 계산됩니다. 윤초가 이 값에 자리를 얻지 못하는 자리가 여기입니다.
리눅스 time(2) 는 이 값을 그대로 돌려줍니다. 반환값은 에포크 1970-01-01 00:00:00 +0000 이후의 초 수입니다.
남은 것은 폭입니다. 정수형이라는 것만으로는 몇 비트인지 정해지지 않습니다. 역사적으로 많은 구현이 부호 있는 32비트 정수를 썼습니다. 커널의 32비트 시간 헤더에도 그 타입이 남아 있습니다. typedef s32 old_time32_t; 한 줄입니다. s32 는 부호 있는 32비트입니다.
부호가 절반을 가져갑니다. POSIX <stdint.h> 는 정확한 폭을 가진 부호 있는 정수 타입의 최댓값을 2^(N-1) - 1 로 적습니다. 부호 없는 쪽은 2^N - 1 입니다. 같은 32비트인데 위쪽으로 갈 수 있는 거리가 한 비트만큼 갈립니다. POSIX 의 time() 근거 절이 그 결과를 한 문장으로 적습니다. 많은 역사적 구현이 쓴 부호 있는 32비트 정수는 2038년에 실패합니다.
불가능한 것
부호 있는 32비트 time_t 로는 2038-01-19 03:14:07 이후의 시각을 담을 수 없습니다. 이것이 그 선입니다.
선 위에 적힌 값들입니다.
| 무엇 | 값 | 어디에 적혀 있나 |
|---|---|---|
| 에포크 | 1970년 1월 1일 00:00:00 협정 세계시 | POSIX 3.125 Epoch |
| 부호 있는 N비트 정수의 최댓값 | 2^(N-1) - 1 |
POSIX <stdint.h> 의 {INTN_MAX} |
{INT_MAX} 의 최소 허용치 |
2 147 483 647 | POSIX <limits.h> |
64비트 time_t 가 넘어서 다루는 시각 |
2038-01-19 03:14:07 | glibc 매뉴얼 D.3.1 |
32비트 time_t 실행 파일이 넘어지는 시각 |
2038-01-19 03:14:08 협정 세계시 이후 | 리눅스 time(2) · clock_gettime(2) |
MySQL TIMESTAMP 의 상한 |
2038-01-19 03:14:07 협정 세계시 | MySQL 8.4 레퍼런스 |
넘어가면 무슨 일이 나는지는 문서마다 끊는 지점이 다릅니다. POSIX 는 time() 이 EOVERFLOW 로 실패한다고 적습니다. 에포크 이후의 초 수가 time_t 타입의 객체에 들어가지 않는다는 뜻입니다. 리눅스 time(2) 와 clock_gettime(2) 도 같은 자리에서 EOVERFLOW 를 답니다. 32비트 time_t 로 빌드된 실행 파일을 64비트 커널에서 돌릴 때 시각이 2038-01-19 03:14:08 협정 세계시 이후이면 이렇게 될 수 있습니다. 다만 같은 man page 가 곧바로 덧붙입니다. 시스템 시각이 time_t 범위 밖인 다른 상황에서의 동작은 정의되지 않습니다. 데비안 위키는 기존 32비트 부호 있는 정수가 넘어갈 때 시각이 1900년으로 되돌아갈 가능성을 적습니다. 이 문장을 가진 1급 출처는 확보되지 않았습니다.
우회
우회는 그 칸을 넓히는 것입니다. time_t 를 64비트로 바꾸면 그 자리는 2038년에 안 걸립니다. 대신 넓힌 것이 타입이라 타입을 쓰는 모든 함수의 원형이 같이 바뀝니다. glibc 매뉴얼은 _TIME_BITS 가 time_t 의 비트 크기와 함께 time_t 에서 파생된 모든 타입, 그리고 관련된 모든 함수의 원형을 정한다고 적습니다.
바뀌는 것은 소스가 아니라 ABI 입니다. 데비안 13 릴리스 노트는 32비트 아키텍처인 armel 과 armhf 에서 많은 라이브러리의 ABI 가 라이브러리 soname 을 바꾸지 않은 채로 바뀌었다고 적습니다. 그래서 이 아키텍처에서는 서드파티 소프트웨어와 패키지를 다시 빌드해야 하고, 조용한 데이터 손실 가능성을 확인해야 한다고 적습니다. 넓히는 값은 여기서 나옵니다.
부호를 떼는 우회도 있습니다. POSIX 는 부호 없는 32비트 정수라면 2106년까지 실패하지 않는다고 적습니다. 같은 문장이 선호되는 해법은 부호를 떼는 쪽이 아니라 time_t 를 더 넓히는 쪽이라고 이어 적습니다.
원인
정의입니다. 물리량이 정한 값도 아니고 증명으로 막힌 것도 아닙니다. 명세가 폭을 열어 두었고 구현이 32비트로 채웠고 그것이 ABI 로 굳었습니다.
명세 쪽 문장은 짧습니다. POSIX Issue 7 의 <sys/types.h> 는 time_t shall be an integer type. 까지만 적었습니다. 정수형이라는 것만 정하고 폭은 구현에 맡겼습니다.
Issue 8 에서 그 문장이 바뀌었습니다. 지금은 time_t 가 최소 64비트의 폭을 가진 정수 타입이어야 한다고 적습니다. 오스틴 그룹 결함 1462 가 적용된 결과입니다. 그 결함 문서의 제목이 그대로 「최소 64비트 time_t 를 요구한다」입니다. 근거로 적힌 말은 다음 개정판이 이번 판에서 최소 10년 뒤일 것이고 그러면 2038년에 불편하게 가까워진다는 것이었습니다.
바뀐 명세가 이미 굳은 자리를 풀어 주지는 않습니다. 같은 결함 문서가 적습니다. time_t 가 64비트로 요구되더라도 구현은 time_t 가 32비트인 레거시 프로그래밍 환경을 계속 지원할 수 있습니다. 적합성을 얻으려면 time_t 가 64비트인 환경이 적어도 하나만 있으면 됩니다. 32비트 환경은 명세를 어기지 않은 채로 남습니다.
굳은 자리가 ABI 입니다. 32비트 아키텍처의 기존 바이너리는 32비트 time_t 를 전제로 컴파일되어 있습니다. 라이브러리 쪽에서 타입 폭을 바꾸면 그 전제가 깨집니다. 데비안 13 릴리스 노트의 전환 항목이 그 깨짐을 그대로 적습니다. soname 은 그대로인데 ABI 가 바뀌었으므로 다시 빌드해야 합니다. 명세를 고치는 것과 이미 배포된 바이너리를 고치는 것은 다른 일입니다.
경계
64비트 time_t 로 옮기면 이 선이 사라진 것인가. 아닙니다. 32비트 time_t 의 선은 그 자리에 그대로 있습니다. 일이 그 선에 안 닿는 자리로 옮겨간 것입니다.
_TIME_BITS
옮기는 일은 자동이 아닙니다. 리눅스 feature_test_macros(7) 는 _TIME_BITS 를 64로 정의하면 time_t 의 폭이 64비트로 바뀌어 2038년 너머의 타임스탬프를 다룰 수 있게 된다고 적습니다. 이 매크로는 glibc 2.34부터 쓸 수 있습니다. 정의하는 쪽이 켜는 것이므로 안 켜면 그대로입니다.
두 폭이 한 시스템에 같이 있기도 합니다. glibc 매뉴얼 D.3.1 은 구성이 __TIMESIZE 값에 따라 두 부류로 갈린다고 적습니다. __TIMESIZE == 32 인 이중 시간 구성은 32비트와 64비트 시간 지원을 둘 다 가집니다. 그 구성에서 32비트 시간 지원이 제공하는 타입이 time_t 이고, 이쪽은 2038년 너머의 날짜를 다루지 못합니다. 64비트 시간 지원이 제공하는 타입은 __time64_t 이고 이쪽은 다룹니다. 같은 시스템 안에서 못 넘는 쪽과 넘는 쪽이 나란히 있습니다. 선이 지워지지 않았다는 것이 여기서 보입니다.
_TIME_BITS 를 32로 정의하는 선택지도 남아 있습니다. glibc 매뉴얼은 그러면 time_t 가 32비트 정수로 정의되며 이는 권장되지 않는다고 적습니다. 32비트 time_t 는 2038년에 동작을 멈춥니다.
디스크에 적힌 32비트 타임스탬프
코드를 넓히면 디스크에 이미 적힌 값도 넓어지는가. 아닙니다. 온디스크 포맷은 별개의 자리입니다.
커널의 ext4 문서가 그 자리를 보여 줍니다. inode 구조체의 아래쪽 128바이트에 네 개의 타임스탬프가 기록됩니다. inode 변경 시각, 접근 시각, 데이터 수정 시각, 삭제 시각입니다. 네 필드는 유닉스 에포크 이후의 초를 나타내는 32비트 부호 있는 정수이고, 그래서 2038년 1월에 넘칩니다.
같은 문서가 넓힌 자리도 적습니다. inode 크기가 128바이트보다 크고 여분 필드가 충분히 크면 ctime, atime, mtime 세 필드가 64비트로 넓혀집니다. 그 여분 32비트 중 아래 두 비트가 32비트 초 필드를 34비트로 늘리고 위 30비트가 나노초 정밀도를 담습니다. 그러면 타임스탬프는 2446년 5월까지는 넘치지 않아야 합니다. 다만 dtime 은 넓혀지지 않았습니다. 넓히기가 필드 단위로 갈리는 자리입니다.
운영
32비트 time_t 를 쓰는 자리에서 이 선이 실제로 닿습니다. 리눅스 time(2) 는 2038년 이후에 돌아갈 것을 의도한 애플리케이션이 time_t 가 32비트보다 넓은 ABI 를 써야 한다고 적습니다.
켜는 손잡이
| 손잡이 | 값과 기본값 | 문서가 적은 것 |
|---|---|---|
_TIME_BITS |
정의하지 않으면 폭이 아키텍처마다 다릅니다 | 현재 대부분의 아키텍처에서 64비트가 기본입니다. i686 과 ARM 같은 일부 전통적 아키텍처에서는 32비트가 기본이지만 이는 바뀔 예정이므로 애플리케이션이 기대면 안 됩니다 |
_FILE_OFFSET_BITS |
정의하지 않으면 현재 32로 떨어집니다 | 이 기본값은 앞으로 바뀔 예정입니다. 애플리케이션이 현재 기본값에 기대면 안 됩니다 |
두 손잡이는 따로 놀지 않습니다. glibc 매뉴얼은 _TIME_BITS=64 를 _FILE_OFFSET_BITS=64 가 같이 정의되었을 때만 정의할 수 있다고 적습니다. 다른 조합은 컴파일 시점 오류가 납니다. 같은 문서가 _TIME_BITS=64 가 time_t 의 Y2038 안전성에 요구된다고 적고, 큰 파일을 다루지 않는 애플리케이션에서도 _FILE_OFFSET_BITS 가 64로 따라가야 하는 이유로 이 조건을 듭니다. feature_test_macros(7) 도 두 매크로가 밀접하게 관련되어 있으며 구현에 따라 _FILE_OFFSET_BITS 설정을 요구할 수 있다고 적습니다.
32비트 아키텍처의 시스템콜
전통적으로 32비트 time_t 를 쓰던 자리에서 _TIME_BITS=64 를 켜면, 실제로 어떤 시스템콜을 부를지는 돌아가는 커널 버전에 달립니다. glibc 매뉴얼은 리눅스 커널 5.1 위에서는 64비트 시간을 지원하는 시스템콜을 쓰고, 그렇지 않으면 레거시 32비트 시스템콜을 쓰는 대체 코드가 쓰인다고 적습니다.
flowchart TD
A["32비트 아키텍처 · _TIME_BITS=64"] --> B{"커널 5.1 위인가"}
B -->|예| C["64비트 시간 시스템콜"]
B -->|아니오| D["레거시 32비트 시스템콜 대체 코드"]
그 시스템콜들은 커널 쪽에 따로 번호를 받아 놓았습니다. include/uapi/asm-generic/unistd.h 에서 __BITS_PER_LONG == 32 인 경우에만 열리는 자리에 403번 clock_gettime64 부터 clock_settime64, clock_adjtime64, clock_getres_time64, clock_nanosleep_time64 가 이어집니다. 그 위 자리에서 glibc 는 선언을 다른 것으로 펼쳤음을 알리려고 __USE_TIME64_REDIRECTS 를 정의합니다. 예를 들어 clock_gettime 심볼이 __clock_gettime64 로 펼쳐집니다.
커널 안쪽의 낡은 인터페이스도 같은 이유로 밀려나는 중입니다. 커널 타임키핑 문서는 struct timeval 이나 struct timespec 을 반환하는 모든 인터페이스가 교체되었다고 적습니다. 이유는 그 구조체의 tv_sec 멤버가 32비트 아키텍처에서 2038년에 넘치기 때문입니다. 커널의 include/linux/time32.h 주석도 같은 말을 답니다. 여기 있는 인터페이스는 32비트 아키텍처에서 2038년에 넘치는 옛 time_t 정의에 기반한 것들이고 새 코드는 time64_t 와 timespec64 에 기반한 대체물을 써야 합니다.
배포판
데비안 13 트릭시 릴리스 노트는 i386 을 제외한 모든 아키텍처가 이제 64비트 time_t ABI 를 쓰며 2038년 너머의 날짜를 지원한다고 적습니다. i386 은 이 전환에 참여하지 않았습니다. 주된 역할이 레거시 소프트웨어 지원이기 때문입니다. 32비트 아키텍처인 armel 과 armhf 에서는 서드파티 소프트웨어와 패키지를 다시 빌드해야 하고 조용한 데이터 손실 가능성을 확인해야 합니다. 데비안 위키는 실제 전환이 2024년 2월 27일에 시작되었다고 적습니다.
라즈베리파이 OS armhf 의 릴리스 노트는 2025년 10월 1일자 항목에서 데비안 트릭시 기반이라고 적습니다. armhf 가 64비트 time_t 로 넘어갔다는 문장 자체는 라즈베리파이 쪽 문서가 아니라 데비안 릴리스 노트에 있습니다. 두 문서를 이어야 나오는 사실입니다.
데이터베이스
애플리케이션 밖에도 32비트 타임스탬프가 있습니다. MySQL 8.4 레퍼런스는 TIMESTAMP 타입의 범위를 1970-01-01 00:00:01 협정 세계시부터 2038-01-19 03:14:07 협정 세계시까지로 적습니다. 소수부를 포함한 표기에서도 상한은 2038-01-19 03:14:07.499999 입니다. 같은 문서의 DATETIME 은 1000-01-01 00:00:00 부터 9999-12-31 23:59:59 까지입니다. 컬럼 타입을 무엇으로 잡았는지가 이 선에 닿는지를 가릅니다.
관련 항목
이 선이 딛고 선 시간 값
time_t · 에포크 · 협정 세계시 · 윤초 · 타임스탬프
이 선을 정의하는 표준과 매뉴얼
POSIX · 오스틴 그룹 · glibc · time(2) · time_t(3type) · clock_gettime(2) · stdint.h · limits.h · sys/types.h
이 선을 넓히는 손잡이와 켜는 도구
_TIME_BITS · _FILE_OFFSET_BITS · feature_test_macros(7) · __TIMESIZE · __USE_TIME64_REDIRECTS · __BITS_PER_LONG · gcc · dpkg
이 선이 새겨지는 파일시스템 구조
ext4 · inode · 온디스크 포맷
이 선이 걸리는 데이터 저장소
이 선을 넘기려 커널이 연 시스템콜
시스템콜 · unistd.h · clock_gettime64 · clock_settime64 · clock_adjtime64 · clock_getres_time64 · clock_nanosleep_time64
이 선 탓에 커널이 밀어낸 옛 인터페이스
struct timeval · struct timespec · old_time32_t · time32.h
이 옛 인터페이스를 대신하는 새 타입
time64_t · timespec64 · __time64_t
이 선을 넘어간 배포판과 아키텍처
데비안 · ABI · soname · 라즈베리파이 OS · armhf · armel · i386
이 선을 관리하는 주체
협정 세계시를 실제로 정하는 기관
국제지구자전좌표국
이 선을 넘을 때 나는 오류
EOVERFLOW
이 선과 폭이 같은 다른 시간 한계
2106 문제
이 한계가 속하는 상위 분류
다른 이름: Y2038 · year 2038 problem · 2038