최근에 드라이버 프로그램 하나를 릴리즈할 일이 있었다. 계획에도 없던 것이라 성급한 일정, 준비되지 않은 계획, 거침없는 코딩, 끝없는 밤샘 작업을 통해서 결국 드라이버는 릴리즈 되었다. 결과는? 당연히 엄청난 양의 BSOD와 문제점들로 나타났다. 이러한 디버깅 경험이 늘어나면서 몇 가지 사소한 생각들이 디버깅 시간을 지나치게 지연시킨다는 사실을 발견했다. 물론 좋은 책들에서 이미 모두 다 언급해버린 것들이지만 아직도 그 부분을 미처 읽지 못했던 분들을 위해서 반성문 같은 글을 적어보려 한다.
#0: 코드에 대한 지나친 신뢰는 금물이다.
우리를 방해하는 가장 큰 산은 지나친 신뢰다. 특히나 코드에 대한 지나친 신뢰는 버그 소탕과는 영영 멀어지게 하는 지름길이다. 개발자들이 흔히 하는 말이 있다. 원래 잘 동작하는 코든데요. 하지만 원래라는 말은 따져 본들 의미도 없을뿐더러 그 말이 사실이라면 그 코드는 지금부터 제대로 동작하지 않는 코드라고 생각하는 것이 정신 건강에 좋다.
CPU 점유율과 관련된 이슈 사항이 보고 되었다. 일부 PC에서 CPU 점유율이 과도하게 높다는 것이었다. 일부 PC를 가질 수 없는 우리 입장에서는 당연히 왜 그런 일들이 발생했는지를 생각하고, 코드를 검토해보는 일이 최선이다. 이 과정에서 나는 이전 드라이버에서부터 잘 사용해왔던 IRQL Hack 관련 코드는 검토 대상에서 제외했다. 물론 의도적으로 제외한 것은 아니었지만 이건 아니겠지 하는 심정으로 코드를 검토한 것이었다. 지난 드라이버에서도 잘 사용했던 코드였고, 그 드라이버를 통해서는 그런 보고를 받은 적이 한 번도 없었기 때문이었다. 하지만 결과적으로 버그의 원인은 IRQL Hack 코드에 있었다. 지난 번 드라이버와의 차이점은 그 코드를 호출하는 시점에 있었다.
**#1: 성급한 단정은 절대 피하자.**단정은 금물이다. 생각은 물과 같아서 한번 길을 터주면 그 곳으로만 흐르려는 습성이 있다. 따라서 절대로 마음속으로라도 단정은 하지 않는 것이 좋다. 항상 모든 가능성을 열어두라는 의미다.
특정 PC에서 특정 보안 제품을 설치하자 드라이버가 로딩이 되지 않는 문제가 발생했다. 정상적인 두뇌를 가진 인간이라면 디버깅 버전의 드라이버를 넣어보거나 해서 무엇이 문제인지를 찾는 작업에서 시작을 할 것이다. 하지만 이전에 해당 보안 제품을 테스트 하던 과정 중에 특정 바이너리 시그니처만 포함시키면 드라이버 로딩을 펜딩 시키는 현상을 경험했던 나는 해당 보안 제품이 우리 쪽 드라이버 로딩을 차단했다고 생각을 해버렸다. 이렇게 단정이 지어지고 나니 계속 그 보안 제품에 대한 코드만 검토하게 되었다. 해당 보안제품의 훅 코드에서부터 드라이버 코드까지 말이다.
몇 시간을 뒤졌지만 이상한 점을 찾지 못했다. 당연했다. 문제는 우리 쪽 코드에 있었기 때문이다. 원인은 커널에서 사용하는 시스템 콜백 루틴에 있었다. 그 루틴은 보통 8개까지 등록을 할 수 있도록 제한되어 있다. 문제가 발생한 PC에는 과도하게 많은 보안 제품이 설치되어 있어서 그 특정 보안 제품이 설치되면서 8개를 모두 사용했던 것이다. 그것을 지우면 7개가 되면서 우리가 등록할 수 있는 상황이었던 것이었다. 정상적인 디버깅 루트를 따랐더라면 30초면 해결할 수 있는 문제를 성급한 단정으로 인해서 5시간이 넘도록 고생했던 것이다. 물론 그 디버깅 시간이 죄다 의미 없지는 않았지만 말이다.
#2: 경계를 넘는 순간 믿을 수 있는 정보는 하나도 없다.
윈도우 개발자라면 누구나 MSDN은 틀리지 않았다는 사실을 믿고 싶어한다. 하지만 그 방대한 MSDN도 틀린 곳이 없을 수는 없으며, MSDN의 예제 코드 중에도 문제를 일으킬만한 소지를 담고 있는 것도 있다. 특히나 이런 것들은 알려지지 않은 테크닉을 사용한 코드와 결부될 때 더 두드러지게 나타난다.
특정 상황에서 KeSetEvent의 호출로 BSOD가 발생하는 현상이 나타났다. DDK 문서에 따르면 KeSetEvent는 IRQL에 상관없이 사용할 수 있다고 언급되어 있다. 또한 그렇게 사용하기 위한 모든 조건을 해당 코드는 충족 시키고 있었다. 하지만 특정 부분에서 호출하는 KeSetEvent는 100% BSOD를 발생시켰다. 결국 원인은 해당 함수의 호출 지점에 있었다. KeSetEvent를 호출하면 내부적으로 커널 디스패처 락이라는 것을 획득하려는 시도를 한다. 그런데 우리가 호출한 KiSwapProcess라는 후킹 함수에서는 이 락이 이미 획득된 상태였던 것이다. 이 상태에서 KeSetEvent를 호출하면 그 락을 다시 획득하려는 시도를 하게 된다. 그러면서 시스템은 안드로메다로 가버린 것이다. 커널 디스패처 락은 외부로 노출된 객체가 아니기 때문에 일반적인 상황에서는 해당 락을 획득한 상태에서 작업할 일이 없다. 하지만 시스템 내부 함수를 후킹하면서 그런 가정이 깨져버린 것이다.