몇 주 전, 저는 Windows 3.1의 수학 에뮬레이터이자 FPU 지원 라이브러리인 WIN87EM.DLL의 수학 예외 처리 핸들러를 살펴보는 의문스러운 즐거움을 누렸습니다. 사실 WIN87EM.DLL은 Windows 3.0에 처음 배포된 것으로 보이며, Windows 3.1 버전과 거의 동일합니다. Windows 3.11에 포함된 버전은 Windows 3.1의 버전과 바이트 단위로 완전히 일치합니다.
WIN87EM.DLL의 주요 기능은 이름에서 알 수 있듯이 x87 부동 소수점 에뮬레이터입니다. 이는 많은 Microsoft 언어와 함께 배포되었던 수학 에뮬레이션 패키지의 파생물인 것으로 보입니다. 하지만 x87 하드웨어가 있는 시스템에서도 WIN87EM.DLL은 여전히 해야 할 작업이 있습니다.
구체적으로, WIN87EM.DLL은 수학 오류(예외)를 가로챕니다. 시스템 유형에 따라 WIN87EM.DLL은 NMI vector 2 (PC 및 PC/XT) 또는 IRQ 13 (PC/AT 및 호환 기기) 중 하나를 후킹합니다.
WIN87EM.DLL의 수학 인터럽트 핸들러는 매우, 매우 이상합니다. 이 코드는 작성되고, 재작성되고, 다시 재작성되었으며, 해킹되고, 조정되고, 수정된 흔적을 모두 가지고 있으며, 심지어 저자가 왜 그것이 마침내 작동하는지 실제로 알지 못했음에도 불구하고 마치 굴복시키듯 만들어진 느낌입니다.
물론 x87 수학 오류 처리는 FPU와 CPU 세대에 걸쳐 수많은 세부 사항이 변경되는 매우 까다로운 주제이며, Intel은 이 분야에서 상당한 버그를 가지고 있었습니다. 하지만 WIN87EM.DLL의 코드는 변화가 거의 의미 없거나 전혀 없는 상태에서 '어떻게든' 작동할 때까지 절박함 속에서 이루어진 수정의 결과물처럼 보입니다.
WIN87EM.DLL이 하는 일
WIN87EM.DLL의 IRQ 13 핸들러는 다음 단계를 수행합니다:
- 인터럽트 비활성화 (CLI)
- AX 레지스터를 70번 PUSH하고 POP합니다. 어쩌면 속도를 늦출 수도 있습니다.
- I/O 포트 F0h에 0을 써서 PC/AT FPU 오류 래치(error latch)를 지웁니다.
- 선택된 인터럽트를 마스킹합니다.
- 마스터 인터럽트 컨트롤러에 EOI를 작성합니다 (하지만 슬레이브는 아님).
- FNSTSW 명령어를 실행하여 FPU 상태 워드(status word)를 저장합니다 (오직 그것만).
- AX 레지스터를 16번 PUSH하고 POP합니다. 속도가 죽입니다.
- I/O 포트 F0h에 다시 0을 씁니다. 혹시 첫 번째에 작동하지 않았을 경우 대비해서요.
- FNCLEX 명령어를 실행하여 대기 중인 FPU 예외(exception)를 지웁니다.
- I/O 포트 F0h에 또다시 0을 씁니다. 세 번째가 마법이니까요.
- AX 레지스터를 16번 PUSH하고 POP합니다. 지난번이 너무 재미있었으니까요.
- FNCLEX 명령어를 다시 실행하여, 정말 확실하게 하려고 합니다.
- AX 레지스터를 16번 PUSH하고 POP합니다. 해야 할 일이니까요.
- I/O 포트 F0h에 또다시 0을 씁니다. 세 번으로는 충분하지 않았을 수도 있으니까요.
- 마침내 정신 나간 것처럼 보이지 않는 코드로 점프합니다.
그렇게 그리 복잡해 보이지 않는 코드는 FPU 환경을 저장하기 위해 FNSTENV를 실행하고, 이전에 저장된 상태 워드(FNCLEX에 의해 나중에 변경됨)로 교체하며, 저장된 FPU 오류 포인터의 코드를 검사하여 명령어 접두사를 건너뛰어야 하는지 확인합니다 (여기서 충돌이 발생할 수 있습니다). STI를 사용하여 인터럽트를 활성화하고, 마지막으로 PC와 AT 모두에서 실행되는 공통 코드에 점프하여 실제 수학적 오류를 처리합니다.
전반적으로 볼 때, WIN87EM.DLL의 수학 오류 인터럽트 핸들러는 거의 의미가 없습니다. 예를 들어, I/O 포트 F0h에 네 번 쓰는 것이 유용한 일을 하거나, FNCLEX를 두 번 실행하는 것이 한 번보다 낫다고 생각하기는 매우 어렵습니다. 하지만 그 추가 시간이 무언가를 할 수도 있다는 것은 완전히 가능합니다. 마찬가지로 AX를 PUSH하고 POP하는 속도 저하 루프 역시 필요하지 않을 가능성이 높지만, 특정 시스템에서는 겉보기에 유용한 일을 했을 수 있습니다.
이 코드는 정말 무언가를 작동시키기 위해 필사적으로 노력한 것처럼 보이며, 이것이 고장 난 하드웨어(broken hardware)를 지원하려던 시도의 결과일 가능성이 있습니다. 제가 살펴본 다른 유사한 에러 핸들러(error handler)들은 이 코드만큼 복잡해 보이지 않으며 훨씬 더 '정석적'입니다.
이 코드가 왜 이렇게까지 과하게 되었는지는 아마 영원히 알 수 없을 것이지만, 우리는 추측해 볼 수는 있습니다.