과거 Windows 9x 시절에는 내부의 16비트 코드가 gdi/user 계층에서 사용하는 64KB짜리 구닥다리 힙으로 인해 일명 '리소스' 제약이란 게 있었다. 그래서 램이 수백, 수천 MB으로 아무리 많더라도, 프로그램을 많이 띄워서 UI와 관련된 오브젝트들을 이것저것 생성하다 보면 리소스가 바닥 나고 운영체제가 패닉에 빠지곤 했다.

지금으로서는 정말 말도 안 되는 황당한 제약이다. 9x에서는 메모장이 60KB를 조금만 넘는 파일도 열 수 없었던 것처럼 말이다. 숫자 세는 단위 자체가 16비트로 제한돼 있으니, 실제 메모리가 아무리 썩어 넘쳐도 셀 수 없는 영역은 몽땅 그림의 떡이었던 것이다.

사용자 삽입 이미지

꼴랑 64KB짜리 중에서 메모리가 몇만 바이트 남았다고 출력하는 건 좀 민망했는지, 남은 리소스의 양은 퍼센트 비율로 출력되었으며, Windows 기본 프로그램들의 About 대화상자에서 값을 간단히 확인할 수 있었다.
그런데 이 퍼센티지를 얻어 오는 API는 무엇일까? Windows 3.x에서 도입된 GetFreeSystemResources라는 함수가 그 주인공이었다. 얘는 0~2 사이의 정수 인자도 받아서 시스템 전체, GDI, user 종류도 얻을 수 있었다.

Windows 3.1 SDK에서 windows.h를 열어 보면 저 함수는 #if WINVER >= 0x030a 안에 고이 감싸진 채 선언되어 있었다. 즉, 초창기부터 처음부터 존재하지는 않았다는 뜻이다. Windows 1, 2 시절에는 샘플 프로그램들의 About 대화상자를 보면 그냥 남은 주메모리의 양(수백 KB)과 주 하드디스크의 남은 용량만 출력했지, 저런 비율을 따로 알려 주지는 않았었다. NT 계열이 아니라 도스 위에서 돌아가던 16비트 시절에도 말이다.

저 함수의 공식적인 수명은 Windows 3.x에서 그대로 끝났다. 32비트 Windows API에는 정식으로 이식되지 않았으며, 여전히 16비트 user.exe를 통해서만 제공되었다. 그렇기 때문에 32비트 프로그램이 시스템 정보 같은 기능을 구현할 일이 있어서 남은 리소스 퍼센티지를 얻으려면... 원래는... 마치 32/64비트 훅 DLL을 따로 만들듯이 16비트 DLL을 만들어서 그 DLL이 16비트 API를 호출하여 값을 얻고.. 32비트 프로그램은 그 DLL과 flat 썽킹을 해서 의사소통을 해야 했다. 썽킹에 대해서는 지난번에 한번 다룬 적이 있다.

이런 번거로운 일이 필요한 이유는 32비트 프로그램이 user.exe로 직통으로 API 호출을 할 수는 없기 때문이었다. 일단은 말이다.
옛날에 한컴사전이 노클릭 단어 인식 기능을 구현하기 위해 그래픽 API 훅킹을 했었는데, 훅킹용으로 32비트 DLL과 16비트 DLL이 모두 있었던 것이 기억에 남아 있다. 32비트 gdi32.dll뿐만 아니라 16비트 gdi.exe로 직통으로 들어가는 그래픽 API 호출까지 잡아 내서 거기 문자열을 얻기 위해서 만든 거지 싶다. 그러니 32비트 DLL엔 훅 프로시저가 들어있고 16비트 DLL엔 썽킹 루틴이 들어있었을 것이다.

그런데, 없는 길을 부분적으로나마 만들어 낸 용자가 그 시절에 이미 있었다.
Windows 9x의 kernel32.dll이 제공하는 비공개, 봉인, 문서화되지 않은 API를 이용해서 32비트 프로그램이 user.exe를 직통으로 호출해서 리소스를 얻어 온 것이다. Windows 95 Programming Secret의 저자인 Matt Pietrek가 그 용자이다.

마소 내부에서만 사용할 목적으로 만들어진 듯한 비공개 API 중에는 16비트 바이너리를 로딩할 수 있는 일명 LoadLibrary16 / GetProcAddress16 / FreeLibrary16 세트가 있다. 얘는 kernel32.dll의 export table에 이름이 노출돼 있지도 않아서 ordinal 번호로만 접근이 가능한데.. 이 번호를 근성의 리버스 엔지니어링으로 일단 알아 냈다. 참고로 얘들은 Generic 썽킹용으로 쓰이는 LoadLibraryEx32W처럼 뒤에 32W가 붙은 함수하고는 다른 물건이므로 혼동하지 말 것.

그런데 알아 냈다고 전부가 아니다. Windows 9x의 GetProcAddress에는 특별한 보정 코드가 들어 있어서 kernel32만은 예외적으로 ordinal을 이용한 함수 주소 요청을 고의로 막았다! 고로 이름이 없이 ordinal만 존재하고 운영체제 내부에서만 사용되는 비공개 API를 제3자 프로그램이 멋대로 사용하는 걸 자연스럽게 차단했다.

이런 조치를 취한 심정을 이해 못 하는 바는 아니다. 같은 함수라도 운영체제의 버전이 바뀜에 따라 ordinal이 수시로 바뀔 수 있으니 일반적인 함수라면 어차피 번호가 아닌 이름만으로 import하는 게 맞다.
또한 프로그램들이 비공개 API를 무단으로 사용하다가 Windows의 버전이 바뀌면 그 프로그램들이 호환성이 깨져서 동작하지 않게 되는데, 이 경우 사용자는 프로그램의 제작사가 아니라 마소를 비난하는 편이었다. 신제품을 팔아 먹으려고 일부러 프로그램의 동작을 막았네 뭐네 하는 음모론의 희생양이 되는 것이다. 마소에서도 이런 힐난에 이골이 났는지 더 방어적인 조치를 취하게 됐다.

그래도 이런 비공개 API들을 끝끝내 끄집어내서 사용하려면
(1) 로드 타임 차원: kernel32.dll의 비공개 API ordinal을 직결로 연결하는 import library를 직접 만들거나,
(2) 런 타임 차원: PE 파일 포맷을 분석해서 GetProcAddress 함수를 손으로 직접 구현하면 된다. 메모리에 로드된 kernel32.dll 내부의 export table을 수동으로 뒤지면 된다는 뜻이다.

L(로드), F(해제), G(함수 탐색) 함수의 ordinal은 1부터 시작하는 번호 기준으로 35~37이라고 한다. Windows 95부터 ME까지 변함이 없다. 어차피 더 바뀌어야 할 이유가 없는 번호이기도 하고.

이렇게 얻어 낸 HMODULE (WINAPI* pfnLoadLibrary16)(PCSTR)을 호출해서 "user.exe"를 로드한다.
그리고 GetProcAddress에다가 "GetFreeSystemResources"를 하면 드디어 우리가 원하는 함수 포인터를 얻을 수 있는데, 얘는 바로 호출 가능하지가 않다. kernel32에 존재하는 또 다른 비공개 API인 QT_Thunk를 거쳐서 함수를 호출해야 하는데, 이 함수는 또 기계어 차원에서 호출 방식이 반드시 일치해야 하기 때문에 대략 다음과 같은 인라인 어셈블리를 넣어야 한다.

_asm {
    push 0~2  ; 시스템, GDI, user. 얻고 싶은 리소스 타입
    mov edx, [pfnGetFreeSystemResources] ; 32비트 주소
    call QT_Thunk ; kernel32에 대해 "QT_Thunk"를 GetProcAddress 한 결과
    mov [ret_val], ax ; 함수의 실행 결과를 받을 16비트 WORD 변수
}

이렇게 하면 32비트 프로그램이 일단 16비트 API를 호출해서 리소스 값을 얻어 올 수 있다. (참고로 Windows NT 계열은 QT_Thunk 함수가 존재하지 않는다.)
그런데 내가 실험해 본 바로는.. 저거 사용하는 게 굉장히 까다롭다.
저 어셈블리 코드에 도달할 때까지 각종 DLL를 로드하고 여러 단계에 걸쳐서 여러 함수들의 포인터를 얻는 등 절차가 복잡한데, 클래스를 만들어서 중간 단계의 결과들을 저장해 놓거나 절차를 여러 단계의 함수로 분리하면.. asm 부분이 갑자기 동작하지 않게 된다.

비공개 API가 내부에서 썽킹을 수행하는 동안 프로그램의 스택이라든가 내부 상태를 이상하게 건드리는 것 같다. 컴파일러의 최적화 옵션의 영향을 받기도 하고.. 그렇지 않고서야 위의 저 간단한 어셈블리 코드가 딱히 뻑이 날 리가 없는데 말이다.

16비트 DLL을 따로 만들지 않고 편법을 동원해서 16비트 API를 호출하고 구체적으로는 리소스 퍼센티지를 얻는 방법을 알아 봤는데, 참 어렵긴 하다는 걸 느꼈다. 사실, 과거에 thunk 컴파일러가 하는 일 중 하나도 내부적으로 UT_Thunk를 호출하는 중간 계층 코드를 생성하는 것이었다. 더 들여다보니 말로만 듣던 ThunkConnect32 같은 함수도 쓰는 듯했다.

비공개라고 해서 무슨 ntdll 같은 하위 계층도 아니고 참 신기한 노릇이다. 어차피 Windows 9x는 kernel32가 최하위 계층이지 ntdll 같은 추가적인 하위 계층은 없으니 말이다.
Windows Programming Secret 책을 당시의 마소 Windows 95 팀의 엔지니어들이 직접 봤다면..
블리자드에서 스타크래프트를 직접 개발한 프로그래머들이 스탑 럴커처럼 자신조차 상상하지 못한 컨트롤과 테크닉을 구사하는 프로게이머를 보는 것과 비슷한 느낌을 받았지 싶다.

리소스를 되돌리는 함수 정도야 간단한 정수 하나만을 인자로 받고 역시 정수 하나를 되돌리는 아주 단순한 형태이다. 그러니 이런 테크닉을 구현하는 것에도 큰 무리가 없다. 구조체나 문자열의 포인터가 동원되기라도 했다면 메커니즘이 훨씬 더 복잡해지며, 그냥 정석적인 썽크 컴파일러를 쓰는 것밖에 답이 없지 싶다.
그러고 보니 문득 든 생각인데, 과거에 GWBASIC이 처음 구동되었을 때 Ok 프롬프트 앞에 "6만 몇천 바이트 남았습니다(6xxxx bytes free)"라고 메시지가 떴던 게 저런 리소스와 성격이 좀 비슷한 것 같이 느껴진다.

저런 식으로 프로그램이 시작된 직후, 혹은 프로그램의 도움말이나 About 대화상자 한 구석에다가 간단하게 남은 메모리/자원의 양을 표시하는 건 오랫동안 소프트웨어 업계에 남아 있던 관행이었다. 심지어 도스 시절부터 말이다.
그랬는데 요즘은 메모리가 너무 많아지고 숫자 단위가 커져서 그런지 Windows의 작업 관리자는 남은 메모리의 양을 KB 단위 대신 비율로 표시하기 시작했다. 옛날에는 64KB짜리 리소스는 스케일이 너무 작고 민망해서 퍼센트로 표시한 게 아닐까 의심될 지경이었는데 이제는 반대로 너무 커져서 세부적인 숫자가 무의미한 지경이 됐으니 다시 퍼센트로 복귀한 걸로 생각된다.

Posted by 사무엘

2016/03/06 08:35 2016/03/06 08:35
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1200


블로그 이미지

그런즉 이제 애호박, 단호박, 늙은호박 이 셋은 항상 있으나, 그 중에 제일은 늙은호박이니라.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/03   »
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            

Site Stats

Total hits:
2635469
Today:
2267
Yesterday:
1754