
Ruby, Python, JavaScript에서 잠시 벗어나 컴퓨터 역사 속 대담한 유물을 살펴보자. 상업용 소프트웨어 수만 개가 어셈블리어(assembly language)만으로 만들어졌다.
Atari 2600용 게임은 모두 어셈블리어로 작성됐다. 초기 BASIC 프로그램을 제외하면 Apple II 게임과 애플리케이션도 거의 전부 그렇다. VisiCalc도 마찬가지다. Atari 800, Commodore 64, Sinclair Spectrum용 게임과 애플리케이션도 대부분 어셈블리어로 만들어졌다. NES 카트리지는 전부, 1970년대부터 1990년대 초까지의 아케이드 게임도 거의 모두 어셈블리어로 작성됐다. 여기에는 《Pac-Man》처럼 검은 화면에 작은 스프라이트가 나오는 게임뿐 아니라 《Smash TV》, 《Total Carnage》, 《NBA Jam》 같은 16비트 게임도 포함된다. SNES와 SEGA Genesis 게임도 거의 전부 어셈블리어로 만들어졌다. 이보다 앞선 메인프레임 시대는 여기서 다루지 않는다.
8비트 시대에도 어셈블리어로 작성되지 않은 소프트웨어가 있었다. Apple II용 《SunDog: Frozen Legacy》의 상당 부분은 인터프리터 방식의 Pascal로 작성됐다. Atari 8비트 컴퓨터용 통합 애플리케이션 제품군 HomePak은 Action!이라는 언어로 만들어졌다. 1984년 아케이드 게임 《Marble Madness》는 당시 드물게 C로 작성된 게임 중 하나였으며, 덕분에 Amiga와 이후 Genesis로 쉽게 이식할 수 있었다. 다른 아케이드 게임 몇 개는 BLISS를 사용했다.
1994년에는 65816 어셈블리어로 10만 줄이 넘는 SNES 게임을 작업했다. 디버거도 없었다. 극단적으로 들리지만 당시에는 쓸 만한 대안이 없었다. 가진 도구를 써야 했다. 기타 연주자들이 하는 일이 불가능해 보일 때가 있지만, 그렇게 연주하려고 시간을 들이는 사람은 많다. 어셈블리어도 충분히 연습하고 어렵다는 생각에 시간을 낭비하지 않으면 꽤 단순하다.
더 극단적인 사례도 있다. Commodore 64 코드를 손으로 어셈블리한 사람도 있었고, Apple II 게임을 기계어 모니터에서 통째로 작성한 사람도 있었다. 이 이야기를 알려준 지인은 디스어셈블한 코드를 보면 함수가 256바이트 페이지에 맞춰 배치된 것을 확인할 수 있다고 했다. 그러면 프로그램의 나머지 부분을 옮기지 않고도 해당 함수를 수정할 수 있다.
이런 오래되고 제한적인 시스템용 게임을 만든다는 것이 어땠을지 생각해 보는 일은 흥미롭다. 여기에 현대 하드웨어와 Perl, Erlang, Python 같은 최신 도구를 더한다면 어떨까? Commodore 64 게임을 Haskell이나 Ruby로 만들 생각은 없었겠지만, 그런 언어를 더 강력한 하드웨어에서 쓸 수 있었다면 모든 것이 달라졌을 것이다. 지금이라면 다음과 같이 해볼 수 있다.
직접 어셈블러를 만든다. 당시에는 어려운 일처럼 보였다. 6502 코드로 파싱과 심벌 테이블 관리를 구현하는 일이 큰 부담이었고, 빠르게 동작하도록 만드는 데도 시간이 더 들었기 때문이다. 하지만 이제 Erlang이나 Perl로 크로스 어셈블러를 만드는 일은 많아야 주말 프로젝트다. 코드 몇백 줄이면 된다.
직접 에뮬레이터도 만든다. 여기서 말하는 것은 현대 PC에서 옛 코드를 실행하기 위한 고성능 에뮬레이터가 아니다. 실제 기기에서 충돌을 일으키지 않고 코드를 한 단계씩 실행해 작동을 확인하는 간단한 인터프리터다. 이 역시 빠르게 만들 수 있다. 함수형 언어는 이런 작업에 알맞다. 코드 실행뿐 아니라 어떤 레지스터가 바뀌는지 질의하고, 함수가 읽거나 수정하는 메모리 주소 목록을 얻고, 코드 구간의 사이클 수를 셀 수도 있다. 지금 보면 아주 쉬운 일이지만, 독학으로 게임을 만들던 당시에는 생각도 못 했다. 당시 개발 도구 제작자들이 이런 기능을 제공하는 것도 본 적이 없다.
까다로운 경우를 위한 최적화기도 직접 만든다. 어셈블리의 핵심은 제어권이지만, 손으로 하기에는 지나치게 번거로운 최적화도 있다. 예를 들어 점프 명령이 실행될 때마다 캐리 플래그가 설정된다는 사실을 파악하면, 3바이트짜리 점프를 2바이트짜리 조건 분기로 바꿀 수 있다.
전용 언어도 만들 수 있다. 저수준 최적화를 중시했지만, 당시 최적화의 상당 부분은 패턴을 알아보거나 명령어 수를 줄이도록 코드를 무작정 재배치하는 일이었다. 지금도 대부분의 컴파일러가 내놓는 결과를 보면 움찔한다. 복잡한 언어에서 완벽한 기계어를 생성하는 일은 어렵기 때문이다. 하지만 6502, 6089, Z80처럼 단순한 프로세서라면 과거에 쓰던 기법을 자동화하는 데 그치지 않고, 직접 손대기 두려울 정도의 최적화까지 할 수 있을 것 같다. 자기 수정 코드는 좋은 사례다. 일부 루프에서는 비교 명령의 상수를 직접 바꾸는 방식보다 나은 방법이 없다. 이 작업을 손으로 하려면 몹시 번거롭다.
이런 접근이 가능한 이유 중 상당 부분은 8비트 시스템이 단순하기 때문이다. COFF나 Preferred Executable 형식의 세부 사항을 살펴본 적이 있는가? 여러 페이지에 걸친 복잡한 규격을 Atari 실행 파일의 6바이트 헤더와 비교해 보라. 6502 명령어 집합은 한 장에 요약할 수 있지만, Intel의 x86 명령어 설명서는 두 권에 이른다. 또 다른 큰 이유는 현대 프로그래밍 언어가 과거라면 온전한 프로젝트였을 문제를 훨씬 편하게 다루도록 해준다는 점이다.
원문 · 2007년 11월 20일
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요