Reddit

Annoyances when dealing with unsigned long int vs unsigned long long int when targeting Linux vs my own 32bit OS

unsigned long int vs unsigned long long int — Linux와 직접 만든 32bit OS 사이에서 겪은 골칫거리

취미로 32bit OS를 개발하는 저자가 `typedef unsigned long u64`가 자기 OS에서는 32bit, Linux에서는 64bit로 해석되면서 타이밍 함수가 통째로 깨졌던 경험을 공유합니다. sizeof 검증 과정, 오버로드 가능한 memset, 그리고 JavaScript식 이벤트 큐로 구현한 스케줄러 설계까지 함께 소개하며, C의 데이터 모델(LP64/LLP64) 차이가 왜 중요한지 보여줍니다.

본문

자신만의 32bit 운영체제를 취미로 개발 중인 저자가, C의 정수 타입 크기가 플랫폼마다 달라진다는 사실에 발목을 잡힌 경험을 공유합니다. 스케줄러 데모가 계속 이상하게 동작한 원인을 추적한 끝에, u64로 typedef 해 둔 타입이 자신의 OS에서는 사실 32bit였다는 것을 확인했습니다.

거짓말을 하는 u64

저자는 types.h에 typedef unsigned long u64;라고 써 두고 당연히 64bit라고 믿었습니다. 그런데 Linux와 자기 OS 양쪽에서 컴파일해 보니 Linux에서는 64bit, 자기 시스템에서는 32bit였고, 이 때문에 타이밍 함수가 전부 틀어져 있었습니다. 문제의 코드는 rdtscp 명령으로 상위·하위 32bit를 읽어 (u64)hi<<32 | lo로 합치는 get_cpu_cycles()인데, u64가 실제로는 32bit라면 이 계산은 성립하지 않습니다. 해결책은 typedef unsigned long long int u64;로 바꾸는 것이었고, 저자는 64bit 숫자 하나를 쓰려고 저 긴 선언을 쳐야 한다는 게 어이없다며, 언어에 추가되는 편의 기능들이 정작 편하지 않은 경우가 많다고 푸념합니다.

sizeof로 검증하기

자신이 미친 게 아니라는 걸 확인하기 위해 저자는 검증 코드를 작성했습니다. 먼저 정수를 ASCII로 바꾸는 itoa 함수를 새로 만들고(자릿수를 거꾸로 출력한 뒤 문자열을 뒤집는 방식), memset을 __attribute__((overloadable))를 이용해 u32·char·u64 버전으로 오버로드했습니다. VGA 텍스트 모드용 print 함수에는 개행 문자 처리도 추가했습니다. kernel_main에서 char, unsigned int, unsigned long int, unsigned long long int, u32, u64의 sizeof를 차례로 출력한 결과 자기 OS에서는 1, 4, 4, 8, 4, 8이 나왔고, 수정 전이라면 마지막 u64 자리에 4가 찍혔을 것이라고 합니다. 같은 코드를 Linux에서 돌리면 1, 4, 8, 8, 4, 8이 나옵니다. 즉 Linux의 unsigned long int는 64bit지만 자기 32bit OS에서는 32bit였던 것입니다.

환경마다 '주력 데이터 타입'이 있다

저자는 좋은 환경에는 핵심이 되는 원시 타입이 있다고 봅니다. Bash에는 문자열, JavaScript에는 딕셔너리, Erlang에는 디스트럭처링 튜플, Python에는 다차원 배열, Uiua에는 스택, 그리고 C에는 포인터가 그것이라는 식입니다. 특정 타입을 겨냥하지 않는 언어나 환경은 잘못된 방향으로 간다는 게 저자의 지론이고, 자기 OS에서는 u32가 그 역할을 하기 때문에 JavaScript처럼 쓰고 싶어 typedef unsigned int var;까지 만들어 두었습니다.

스케줄러는 JavaScript식 이벤트 큐로

userland도 프로세스 격리도 컨텍스트 스위칭도 없는 싱글 스레드 OS에서 '여러 일이 동시에 일어나는 것처럼' 보이게 하려고, 저자는 JavaScript의 이벤트 큐 모델을 택했습니다. 원래는 이벤트 큐와 setTimeout·setInterval이 만들 스케줄링 테이블을 분리할 계획이었지만, 일회성 이벤트와 반복 이벤트를 어떻게 구분할지 고민하다가 이벤트·타임아웃·인터벌을 하나의 packed struct로 합쳤습니다. 구조체는 active 플래그, target_cycle, repeat_interval, 콜백, 데이터 포인터로 이루어져 있고, 홀이 있는 링 버퍼(holey ring-buffer)에 이벤트를 쌓은 뒤 현재 사이클이 target_cycle을 지났는지 검사해 콜백을 호출합니다. 콜백 호출 전 active를 0으로 만들되, repeat_interval이 0이 아니면 target_cycle을 갱신하고 active를 하나 감소시켜 '최대 횟수가 정해진 인터벌' 기능까지 얻었습니다. 무한 반복은 U32_MAX를 넣어 약 40억 번 실행되게 하거나, U32_MAX이면 감소를 건너뛰는 방식으로 처리합니다. 콜백에는 state 인자와 반환값도 있는데, 전역 상태를 쓰기 싫은 함수형 프로그래머들을 위한 것이라며 농담을 곁들입니다.

앞으로의 과제

개선된 sleep 함수, 이미지·비디오 타입과 포맷 변환, 스케일링 기능을 추가했지만 bilinear·trilinear 업스케일링은 아직 제대로 동작하지 않는다고 합니다. snake 게임 정도는 만들 수 있는 수준까지 왔지만, VGA 그래픽스 모드(framebuffer)에서 글자를 찍을 bitmap font가 없어 QEMU로는 텍스트 출력을 확인하고 실제 하드웨어로 부팅해서는 그래픽을 보는 식으로 따로 돌려야 하는 불편이 있습니다. 저자는 오래됐지만 하위 호환이 되는 하드웨어 그래픽스 가속을 찾고 있고, 예전에 OpenGL로 만든 탑다운 Doom류 게임을 이식하고 이미지 회전도 추가하고 싶다고 합니다.

Reddit 반응

  • @u/jason-reddit-public — "64bit Linux와 64bit Windows는 다릅니다. 제 코드를 작성할 때는 int64_t 같은 타입을 씁니다."
  • @u/zeekar — "C가 그렇죠. 보장되는 건 char <= short <= int <= long <= long long뿐입니다. 64bit 플랫폼에서는 int가 64bit일 거라 기대하고, 아니어도 long은 64bit겠지 하는데, Linux에서는 그게 맞습니다. 이걸 LP64(Long = Pointer = 64bit)라고 부릅니다. Windows는 LLP64(Long Long = Pointer = 64bit)를 써서 64bit로 컴파일해도 long은 32bit이고, 64bit를 얻으려면 long long까지 가야 합니다."
  • @u/thisisjustascreename — "C 언어 하는 꼴을 보면 '변수 크기를 알고 싶다고? 행운을 빌어!' 하는 겁니다."
  • @u/FartusMagutic — "확실히 'long long int'가 아니라 그냥 'long'과 'long long'입니다."
  • @u/synept — "이 페이지의 서식은 모바일에서 읽기 꽤 힘듭니다."

원문: submatrix.net / 번역·요약: Trawling