고전 텍스트 어드벤처 개발사 인포컴의 게임을 메모리 수준 디버거로 뜯어보던 개발자 앤드루 플로킨(zarf)이, 1983년작 '인피델(Infidel)'에서 지금껏 본 것 중 가장 심각한 버그를 발견했다. 플레이에는 거의 영향을 주지 않아 흔히 '사소하다'고 치부될 만한 종류지만, 실체는 의도치 않게 메모리를 덮어쓰는 와일드 포인터 버그다. C 프로그래머 출신인 그는 메모리 손상을 가장 큰 죄악으로 여긴다고 말한다. 게다가 이것은 게임 코드 자체의 실수가 아니라 ZIL 컴파일러의 버그였다. 분석 대상은 매킨토시용 업데이트판(릴리스 22, 시리얼 840522)이며, 원본(830916)도 주소만 다를 뿐 동일한 문제를 안고 있다.
끝없는 사막이라는 눈속임
인피델의 무대는 이집트 사막이다. 캠프를 기준으로 동쪽 3x3 격자만 정상적인 지도이고, 그 바깥으로 벗어나면 플레이어는 '길을 잃은' 상태가 된다. 이때 게임은 ENDLESS-DESERT라는 단 하나의 방으로 모든 미지의 사막 칸을 표현한다. 인챈터에서 처음 쓰인 기법으로, 플레이어가 움직여도 캠프나 지도 구역으로 돌아오지 않는 한 계속 같은 방으로 되돌아오게 만든다. 대신 위도·경도 좌표를 추적해 둔다.
이 눈속임을 그럴듯하게 유지하려면 물건을 옮겨 두는 처리가 필요하다. 사막에 물건을 떨어뜨리고 이동하면, 그 아이템은 무대 뒤로 치워지고 좌표와 함께 DESERT-TABLE이라는 배열에 기록된다. 나중에 같은 좌표로 돌아오면 다시 꺼내어 보여 주는 식이다. 다만 떨어뜨린 물건은 3분의 1 확률로 모래에 묻혀 영영 사라지는데, 이 무작위성이 나중에 버그를 더 찾기 어렵게 만든 요소이기도 하다.
상수가 아닌 전역 변수
문제는 물건을 치우는 DESERT-TO-TABLE 루틴에 있다. 이 루틴은 기록할 배열 주소 TBL을 선택적 인자로 받고, 기본값으로 DESERT-TABLE(주소 11129)을 쓰도록 되어 있다. 플로킨은 디버거의 상태 탭에 이 배열 내용을 표시하려다 배열이 끝내 비어 있다는 사실을 발견했다. 물건은 분명 사라졌다 나타나는데, 어디로도 저장되지 않고 있었던 것이다.
디스어셈블해 보니 TBL을 11129로 초기화하는 코드가 없었다. Z-머신 함수는 지역 변수를 상수로 초기화하는 슬롯을 두는데, ZIL은 DESERT-TABLE을 상수로 간주해 그 '값'이 아니라 전역 변수 번호인 30(16진수 1e)을 TBL에 넣어 버렸다. 전역 변수는 16번부터 매겨지며 DESERT-TABLE은 15번째라 번호가 30이다. 값은 게임 내내 11129로 고정되지만 변수인 이상 컴파일 시점의 상수가 아니다. 즉 기본값이 전역 변수라는 경우를 아무도 상정하지 않은 탓에, 컴파일러는 오류를 내지도, F처럼 별도 STORE 명령을 생성하지도 않고 그냥 30을 썼다.
결과적으로 루틴은 11129가 아니라 주소 30번지부터 메모리를 덮어쓴다. 되돌리는 TABLE-TO-DESERT 루틴도 똑같이 30번지에서 읽어 오기 때문에 겉보기 동작은 멀쩡하다. 그러나 그사이 메모리는 짓밟히고 있었다.
운 좋게 숨어 있던 재앙
피해가 눈에 띄지 않은 것은 순전히 운이었다. 메모리 앞부분 64바이트는 헤더인데, Z-머신 버전 3에서 실제로 의미를 갖는 영역은 0~29번지뿐이고 30~63번지는 쓰이지 않는다. 만약 DESERT-TABLE이 세 번째 전역(번호 18)이었다면 게임의 시리얼 번호가 손상돼 훨씬 일찍 들통났을 것이다. 64번지부터는 자주 쓰는 단어 96개를 묶어 둔 축약(abbreviations) 데이터가 시작된다. 플로킨은 사막에 아홉 개의 물건을 떨어뜨리면 마지막 하나가 이 축약 테이블 앞부분을 덮어쓸 것이라 예측했고, 모래에 묻히는 확률을 뚫고 재현에 성공했다. 그러자 환각 묘사 속 'the'가 'You'로 바뀌어 출력됐고, 다른 시도에서는 인터프리터가 무한 루프에 빠져 크래시가 나기도 했다.
이 버그는 그대로 출시됐고 인포컴의 플레이테스터를 포함해 아무도 알아채지 못했다. 1984년 개정판도 손대지 않았다. 사막은 피라미드를 찾기 전 시간을 때우는 공간일 뿐 아무것도 없는 곳이라, 애초에 여덟 개가 넘는 물건을 거기 버릴 이유가 없었기 때문이다. 세심하기로 유명한 인포컴이지만 이 구역만큼은 집중적으로 테스트되지 않았다는 방증이다. 플로킨은 모래 구덩이를 다루는 PROP-TO-TBL·TBL-TO-PROP 루틴에도 PROP-TBL(주소 11329, 전역 22번)을 대상으로 한 같은 버그가 있으나, ENDLESS-DESERT에서는 땅을 팔 수 없어 이 루틴 자체가 쓰이지 않는 사장된 흔적으로 보인다고 덧붙였다.
실무자가 가져갈 교훈
수정은 간단하다. TBL의 인자 기본값을 없애고 루틴 첫머리에서 명시적으로 DESERT-TABLE을 대입하거나, 호출부에서 두 번째 인자로 넘기면 된다. 다만 이 사례가 남기는 함의는 작지 않다. 컴파일러가 '상수'와 '값이 변하지 않는 변수'를 혼동했고, 그 결과가 조용한 메모리 손상으로 이어졌다는 점이다. 겉으로 대칭이 맞아떨어지는 읽기·쓰기 경로 때문에 오동작이 수십 년간 가려졌고, 거의 가 보지 않는 기능 구석은 테스트 커버리지에서 통째로 빠져 있었다. 정적 분석이 닿지 않는 가정, 사용 빈도가 낮은 경로의 잠복 결함, 그리고 적절한 관측 도구 하나가 어떻게 오래된 버그를 드러내는지를 동시에 보여 주는 사례다.