512바이트 안에 C 컴파일러를? 불가능을 현실로!
512바이트 부트 섹터(boot sector)에 맞는 C 컴파일러 SectorC는 x86-16 어셈블리로 작성되었으며, 최소한의 C 언어 기능을 지원한다.
토크나이징(tokenizing) 및 파싱(parsing) 과정을 간소화하고, atoi()를 해시 함수로 활용하는 등 독창적인 아이디어를 통해 크기를 최소화했다.
Barely C라는 독특한 언어 설계를 통해 공백 기반 토큰 분리(space-delimited tokens) 방식을 채택하여 토크나이저를 단순화했다.
바이트 스레드 코드(byte-threaded code)를 시도했으나, 오버헤드(overhead)로 인해 포기하고, 기존 방식을 최적화하여 컴파일러 크기를 줄였다.
512바이트 제약 조건과 Barely C
SectorC는 512바이트라는 극단적인 제약 조건 하에서 C 컴파일러를 구현하기 위해 토크나이저(tokenizer)의 단순화에 집중했다. 특히, Forth와 유사한 아이디어를 차용하여 공백으로 구분된 토큰(space-delimited tokens)을 사용하는 Barely C라는 언어를 설계했다. 이 접근 방식은 토크나이징 과정을 대폭 단순화하여 컴파일러 크기를 줄이는 데 기여했다. 하지만, 이러한 설계는 코드 가독성을 희생하는 결과를 낳았다.
atoi()를 활용한 토큰 처리
SectorC는 문자열을 정수로 변환하는 atoi() 함수를 해시 함수(hash function)로 활용하여 토큰을 처리하는 독창적인 방법을 사용했다. atoi()의 반환값을 토큰 타입으로 사용하여 심볼 테이블(symbol table)을 제거하고, 변수를 64K 세그먼트(segment)에 직접 접근하도록 구현했다. 이러한 방식은 컴파일러의 크기를 줄이는 데 효과적이었지만, 해시 충돌(hash collision)의 위험을 내포하고 있다.
바이트 스레드 코드(Byte-Threaded Code) 시도와 실패
저자는 Forth의 아이디어를 차용하여 바이트 스레드 코드(byte-threaded code)를 구현하려 시도했으나, 512바이트의 제약 내에서 오버헤드(overhead)가 커서 실패했다. 바이트 스레드 코드는 각 명령어를 2바이트로 정렬하여 단일 바이트로 주소를 지정하는 방식이다. 하지만, 정렬, 추가적인 ret 명령어, 함수 호출 등의 오버헤드로 인해 직접적인 코드 방식보다 크기가 증가했다.
최적화 기법을 통한 코드 크기 최소화
SectorC는 바이트 스레드 코드 대신, 기존의 직접적인 코드 방식(straight-forward version)을 최적화하여 컴파일러 크기를 줄였다. fall-through, tail-call, call-fusion과 같은 기법을 활용하여 불필요한 코드를 제거하고, stosw, lodsw를 광범위하게 사용하여 코드 밀도를 높였다. 또한, 8비트 내에서 점프 오프셋(jump offsets)을 유지하여 코드 크기를 더욱 줄였다.
asm 확장과 에러 처리 부재
SectorC는 I/O를 위해 asm 확장(asm extension)을 지원하여, x86-16 머신 코드를 직접 삽입할 수 있도록 했다. 이를 통해 저수준의 하드웨어 제어가 가능해졌다. 하지만, SectorC는 에러 처리(error handling)를 거의 구현하지 않았다. 이는 컴파일러의 크기를 최소화하기 위한 결정으로, 개발자는 코드의 정확성을 스스로 보장해야 한다.