Windows API에서 LoadLibrary는 말 그대로 실행 파일(exe/dll)을 현재 프로세스의 주소 공간에다 불러들여서 거기 있는 코드를 실행하거나 리소스를 추출하게 해 주는 함수이다.
그리고 얘의 심화 버전은 LoadLibraryEx이다. Ex 버전은 옵션을 추가로 받아서 절대 경로 없이 파일명만 주어졌을 때 디렉터리를 탐색하는 순서를 지정할 수 있고, 파일이 이미 load되어 있을 때 레퍼런스 카운트 변경 여부 같은 것도 수동 지정할 수 있다.
하지만 그런 옵션들은 현업에서 잘 쓰이지 않는다. 저 함수에서 실질적으로 자주 쓰이는 옵션은.. DLL에서 리소스를 추출할 준비만 하고, 코드를 실행할 준비--기준 주소 재배치, DllMain 함수 실행--는 생략해서 로딩 속도를 좀 더 향상시키는 LOAD_LIBRARY_AS_DATAFILE이다. 특히 x86, x64, ARM 같은 아키텍처를 불문하고 동일 DLL에 있는 리소스 데이터를 추출하려면 이 '간소화' 플래그를 반드시 지정해야 한다(다국어 UI 리소스 같은..).
그런데 문제는.. 이 DATAFILE 간소화 로딩이란 게, 과거에는 "리소스 추출에만 특화"이라는 자기 본연의 기능에도 모종의 이유로 인해 뭔가 2% 부족한 구석이 있었다는 것이다.
Windows 9x 시절에는 이 제약이 제일 심했다. 간소화 로딩된 DLL 핸들에 대해서는 (1) 리소스를 제일 저수준에서 탐색하는 EnumResourceLanguages/Names/Times 및 Enum/Find/LoadResource 계열 함수만 사용할 수 있었다. 이들보다 상위 계층에서 동작하는 Load*계열 함수들은(string, menu, bitmap, image 따위) 지원되지 않았다. 그러니 간소화 로딩의 활용성이 부족했으며, 여전히 기존 full(?) 방식 로딩을 해야 하는 경우도 있었다.
허나, 한편으로는 저 제약이 그렇게까지 본질적이고 치명적인 문제가 아니었다.
Windows 프로그램에서 리소스 전용 DLL을 사용하는 주 목적은 다국어 UI 제공.. 아니면 대화상자· 메뉴 같은 표준 리소스가 아니라 자기 자신만 사용하는 custom 데이터의 저장이기 때문이다.
그리고 표준 리소스들도 특정 언어에 속하는 놈을 지정하려면 "DLL 핸들 + 리소스 ID"만으로는 어차피 충분치 않다. FindResourceEx와 LoadResource의 결과값인 메모리 포인터를 줘야 하며, 함수도 LoadMenuIndirect, DialogBoxIndirect처럼 뒤에 indirect라는 단어가 붙은 '저수준 버전'을 써야 한다.
그렇기 때문에 리소스 추출용 간소화 방식으로 load한 DLL은 저수준 함수로만 다룰 수 있더라도 그럭저럭 사용할 만했다. 그런데 여기에는 다른 이상한, 자잘한 문제도 있었다.
DialogBoxIndirect 함수는 대화상자 리소스를 "모듈(인스턴스) 핸들 + 리소스 ID"가 아니라 대화상자 템플릿 포인터 하나로만 곧장 지정함에도 불구하고, 모듈 핸들을 여전히 인자로 받는다. 내부적으로 CreateWindowEx 함수를 호출할 때 모듈 핸들이 필요하기 때문이다(대화상자 자신, 그리고 내부의 child 컨트롤들 생성).
그런데 (2) 이때 리소스 추출 간소화 방식으로 load한 DLL의 핸들을 주면.. 구형 운영체제에서는 여러 문제들이 발생했다.
일단, 자기 자신이 내부적으로 사용하는 커스텀 컨트롤--표준 컨트롤이 아니고, CS_GLOBALCLASS 등록된 커스텀 컨트롤도 아닌 놈--이 만들어지지 않는다. 이건 CreateWindowEx 함수의 특성상 자연스러운 귀결이지만, 그 이상으로..
내 기억이 맞다면 대화상자의 배경색이 일반적인 회색이 아니라 흰색으로 바뀌고 좀 만지다 보면 프로그램이 뻗었다. Windows 9x뿐만 아니라 나름 NT 계열인 2000에서도 말이다.
그 이유는 딱히 알 수 없었다. 그저 경험적으로 이런 DLL 핸들을 집어넣어서는 안 된다고 날개셋 한글 입력기 소스의 주석에도 엄청 옛날에 적혀 있었다.
물론 이 역시 본질적이고 치명적인 문제는 아니다.
윈도우의 생성과 관련해서 전달하는 인스턴스/모듈 핸들은 그 윈도우의 클래스를 등록한 주체를 식별하는 용도이다. 애초부터 리소스가 전혀 아니라 코드와 관계가 있다. 그러니 여기는 애초에 리소스 추출 간소화 방식으로 load된 DLL이 들어갈 자리가 아니다. 그런 DLL을 집어넣은 것은 사실상 프로그래머의 실수에 지나지 않는다.
하지만 이쯤 되니 의문이 생긴다. 프로그래머가 아무리 실수할 수 있기로서니, 그걸 넘겨주면 단순히 custom 컨트롤이 생성되지 않는 것 이상으로 왜 다른 이상한 부작용까지 발생한 것일까? 차라리 깔끔하게 에러와 실패 처리를 하는 것도 아니고 말이다.
DLL을 일반적인 방식으로 load하는 것과 datafile(리소스 특화 간소화) 방식으로 load하는 것은 내부적으로 무슨 차이가 있는 걸까?
오늘날의 32비트 및 64비트 Windows 환경에서는 DLL을 로딩한 결과 핸들(HMODULE / HINSTANCE)은 그 파일 내용을 가리키는 데이터 포인터와 거의 동급이라고 여겨진다. 파일을 memory-mapped file 형태로 통째로, 혹은 약간의 보정만 거쳐서 읽어들인 첫 지점이다. 쉽게 말해 그 핸들이 가리키는 메모리에는 EXE 파일 시그니처인 MZ부터 쭉 나온다는 것이다.
그리고 실행 파일은 메모리 주소가 언제나 64KB의 배수 단위로만 배당된다는 것도 이 바닥에서 프로그래밍 좀 한 사람들은 아실 것이다. 그 말인즉슨, 일반적으로 HMODULE 내지 HINSTANCE의 값은 64KB의 배수이며, 하위 word가 언제나 0이 된다는 뜻이다.
하지만 특수한 상황에서는 이런 조건을 만족하지 않는 핸들도 있을 수 있다.
(1) 먼저, 과거의 Windows 9x 환경에서는 16비트 프로그램에서 호출한 LoadLibrary의 리턴값이 대표적인 예이다. 얘들은 핸들의 크기 자체가 16비트밖에 안 되니 리턴값과 내부 의미 역시 32비트 프로그램과는 완전히 다른 형태여야 한다.
물론 이미 32비트 형태로 빌드된 프로그램이야 이런 거 신경 쓸 필요가 전혀 없으며, 16비트와 32비트 프로그램을 모두 한데 관리하는 운영체제의 관점에서나 구분이 필요하다.
(2) 그리고 LoadLibraryEx + datafile 방식으로 불러들인 dll 핸들도 형태가 약간 달라진다. 운영체제의 버전에 따라 차이가 있지만 일단 해당 DLL의 preferred base는 완전히 무시되며, 굳이 64KB라는 큼직한 단위로 주소가 배당되지 않는다.
결정적으로는 최하위 비트에 1이 추가돼서(= 홀수!!) 얘는 datafile 방식으로 생성되었다는 것을 나타낸다. 메모리 주소로서의 DLL 핸들은 하위 16비트에 어차피 유의미한 정보가 담겨 있지 않으니.. 그 잉여 공간에다 이런 정보를 보관한다는 뜻이다.
요컨대 HMODULE / HINSTANCE는 16비트 프로그램 또는 datafile 방식에 한해서는 64KB의 배수 단위인 깔끔한 포인터가 아니게 된다. 그런데 과거에는 운영체제 내부에서 이런 변칙적인 핸들을 취급하는 방식이 서로 충돌했던가 보다.
kernel32는 이 DLL이 datafile 방식으로 load되었다는 것을 식별하기 위해서 핸들값에다가 1을 추가했다. 하지만 user32의 대화상자 표시 함수는 datafile 방식에 대해서는 전혀 관심이 없으며, 이 핸들값이 하위 16비트가 비영인 것을 보고는 이건 16비트 모듈이라고 인식해 버렸다. 그리고 16비트 프로그램과의 하위 호환을 위한 보정 처리를 수행했다.
그 보정 처리 중에는 대화상자 내부의 각 에디트 컨트롤들에 대해 고유한 데이터 세그먼트를 생성하는 것도 있었다.
아시다시피 에디트 컨트롤, 특히 multiline으로 동작하는 놈은 혼자서 수백, 수만 바이트에 달하는 텍스트를 저장할 수 있다. 모든 컨트롤들이 한 64KB 데이터 세그먼트를 공유할 게 아니라 각각이 고유한 세그먼트를 갖는 게 낫다. 이것을 대화상자 표시 함수가 내부적으로 해 줬다.
(그럼 이건 특별히 메모리가 많이 필요한 에디트 컨트롤에 대해서 고유한 스타일을 줘서 그 컨트롤이 알아서 처리하면 되지, 이런 걸 왜, 어떻게 상위 윈도우인 대화상자에서 처리하는지는 잘 모르겠다. 그리고 그런 식이면 에디트 컨트롤뿐만 아니라 리스트나 콤보박스도 수천 개의 아이템을 추가하느라 메모리가 많이 필요할 때가 있을 텐데 걔네들은 어떻게 처리되는지도.. 개인적으로는 잘 모르겠다. ㄲㄲ)
어쨌든.. 대화상자를 생성할 때 datafile DLL의 핸들이 지정되면 저런 복잡한 이유로 인해 16비트 보정이 수행되는데.. 실제로 대화상자를 돌리는 이 프로그램은 16비트 프로그램이 아니다. 그래서 보정 처리가 제대로 되지 않고 프로그램이 죽는 등 갖가지 오동작과 이상 현상이 발생한다고 한다. 그래서 그랬던 것이군~!! (☞ 더 자세한 설명)
대화상자에도 스타일이 있다. 하지만 이건 윈도우 스타일의 형태로 지정해 주는 게 아니고 DialogBox 계열 함수에다가 인자로 전하는 것도 아니며, 그냥 대화상자 리소스 템플릿에 박혀 들어가는 값일 뿐이다. 그러니 다른 스타일 플래그들에 비해 인지도가 매우 낮으며 프로그램 코드에서 볼 일이 없다시피하다.
이 대화상자가 다른 대화상자의 child로 들어갈 수 있음을 나타내는 DS_CONTROL, 용도가 좀 모호하긴 하지만 [?] 모양의 도움말 버튼을 우측 상단에다 표시하는 DS_CONTEXTHELP 같은 건.. 오늘날까지도 유효하다. 하지만 16비트 시절의 잔재이고 오늘날은 아무 의미 없는 플래그도 있다.
대표적으로 DS_3DLOOK은.. Windows 95/NT4부터는 대화상자들이 처음부터 기본적으로 버튼과 동일한 은색/회색이고 각종 테두리도 양각 음각 입체(?) 효과가 적용되어 나오므로 존재의 의미가 없어졌다.
그리고 DS_LOCALEDIT라는 놈이 있는데.. 얘는 자기 내부의 모든 에디트 컨트롤들이 고유한 데이터 세그먼트가 아니라 기본 제공되는 단일 64K 세그먼트를 공유하게 해서 메모리를 아끼는 플래그이다. 에디트 컨트롤에 많아야 수십~수백 자밖에 들어가지 않는다는 게 보장되면 사용해 볼 만한 옵션이었다. 32비트 이후부터는 아무런 의미가 없어졌지만..
그리고 이렇게 DS_LOCALEDIT 옵션이 적용된 대화상자는 아까처럼 Windows 9x에서 datafile DLL 핸들을 지정해 주더라도 16비트 보정 처리가 행해지지 않기 때문에 오동작· 오류도 발생하지 않았다고 한다.
물론 이 문제는 Windows NT 계열을 넘어 16비트 프로그램 자체가 존재하지 않는 64비트 운영체제의 관점에서는 더욱 무의미한 지나간 옛날 추억이 되었을 뿐이다.
16비트에서 32비트로 넘어갈 때는 16비트 환경에서도 far이니 huge니 하면서 어떻게든 16비트 코드에서 64KB를 초과하는 메모리 영역을 다루려고 애썼으며, 반대로 32비트 주소 공간에서 16비트 코드를 수용하고 실행하려고 온갖 발악을 했었다. 하지만 32비트와 64비트는 서로 완벽하게 격리된 채 공존할 뿐, 상대방 영역을 전혀 건드리지 않는다는 차이가 있다.
이상이다.
여담이지만 날개셋 한글 입력기의 소스를 뒤져 보니.. 어떤 DLL을 datafile 방식으로 읽어들인 상태에서는 그 DLL에 대해서 VerQueryValue 같은 버전 정보 확인 API도 제대로 동작하지 않았다는 주석이 적혀 있다. 그래서 버전 리소스를 수동으로 직접 파싱하는 방식으로 기능을 구현했다.
Windows Vista 이상 또는 심지어 9x 계열에서도 괜찮았으며 2000/XP에서만 문제가 발생했다고 하는데.. 이 역시 LoadLibraryEx 함수의 부작용이 아니었나 추측해 본다. 과거에 일반 로딩과 datafile 특화 로딩은 내부 동작이 여러 모로 차이가 컸던 모양이다.
Posted by 사무엘