« Previous : 1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : ... 25 : Next »

* 3년 전에 썼던 글을 내용을 보충하여 리메이크 한 것이다.

Windows 운영체제에서 생성하는 윈도우들은 그 본질이 크게 overlapped, popup, 그리고 child 이렇게 셋으로 나뉜다. 이해를 돕기 위해 아래의 Windows 1.0 사진을 한번 살펴보도록 하자. 그때는 이 세 종류의 구분이 지금보다 훨씬 더 명확했기 때문이다.

사용자 삽입 이미지

1. overlapped

1985년에 발표된 Windows 1.0 첫 버전은 기술적인 한계 때문..은 아니고, 애플 사와의 이상한 특허 분쟁에 얽히는 바람에 응용 프로그램 창들이 서로 겹치지를 못하고 타일 형태의 배치만 가능한 정말 괴상한 형태로 개발되었던 걸로 유명하다.
그러다 Windows 2.0에서는 타일 제약 봉인이 풀렸기 때문에 이 윈도우들은 겹쳐지는 게 가능해졌으며 Z-order라는 개념도 생겼다. 그게 워낙 뜻깊은 일이었던지라 명칭에까지 OVERLAPPED가 붙은 것이다.

그리고 저렇게, 타일 형태의 배치가 가능한 응용 프로그램의 최상단 껍데기 윈도우가 바로 오늘날의 개념으로 치면 overlapped 윈도우이다. 캡션이라고 불리는 제목 표시줄이 달려 있고 크기가 언제든지 유동적으로 바뀔 수 있으며, CreateWindow(Ex) 함수에다 위치와 크기를 지정할 때 CW_USEDEFAULT(대충 적당히 알아서)를 줄 수 있는 유일한 타입의 윈도우이다.

사실, WS_OVERLAPPED의 값은 그냥 0이다. popup이나 child 같은 속성이 따로 지정되지 않은 윈도우는 기본적으로 overlapped 속성이 지정된다. 여기에다가 최소화/최대화(WS_M??MIZEBOX)/닫기(시스템 메뉴 WS_SYSMENU) 버튼, 크기 조절 가능한 굵은 껍데기(WS_THICKBORDER) 비트들이 합쳐진 것이 바로 WS_OVERLAPPEDWINDOW 스타일이다.

2. popup

그럼 popup은 무엇이냐 하면 저 위의 About 대화상자처럼, overlapped window의 위에 겹쳐져서 배치될 수 있는 윈도우이다.
그런데 당장 Windows 2.0부터 오버랩은 말 그대로 overlapped window에서도 다 가능해졌으니, 둘의 실질적인 차이가 없어졌다고 볼 수도 있다. 하지만 둘은 여전히 완전히 동일하지는 않다.

popup 윈도우는 기본적으로 캡션이 없는 형태이며, WS_CAPTION 같은 별도의 옵션을 줘야만 캡션이 달린다. 그러나 overlapped 윈도우는 옵션을 주지 않아도 캡션이 무조건 달려 나온다. Windows 2~3 시절까지만 해도 응용 프로그램에서 캡션이 없고 제목이 없는 대화상자는 지금보다 훨씬 더 흔하게 볼 수 있었다.

지금은 대화상자들도 다 캡션이 달려 있으며 일반적인 응용 프로그램처럼 아이콘에다 최소· 최대화 버튼과 두꺼운 프레임까지 별도로 스타일로 주고 나면.. popup 형태의 대화상자 프로그램과, overlapped 형태의 일반 프로그램 창과 외형상의 구분은 사실상 다 사라지는 건 사실이다.

그럼에도 불구하고 popup과 overlapped의 구분이 원래 저런 데서 시작되었다는 것을 알면 되겠다. 다른 창의 내부에 종속되지 않고 독자적으로 화면에 떠 있으면서 캡션 같은 외형이 없거나 취사선택 가능한 모든 custom 윈도우라면, 묻지도 따지지도 말고 그냥 WS_POPUP을 주면 된다.

대화상자 리소스 편집기에서도 이 대화상자의 초기 스타일을 지정해 줄 수 있다. 프로퍼티 페이지처럼 다른 대화상자의 내부에 들어가는 대화상자이면 WS_CHILD를 주면 되고, 나머지 경우에는 WS_OVERLAPPED는 신경 쓸 필요 없고 그냥 WS_POPUP을 지정하면 된다.
여담이지만, 인터넷을 하면서 수시로 튀어나오는 웹브라우저 팝업창은 명칭과는 달리 사실은 overlapped 윈도우라고 생각하면 된다. 팝업창에도 웹브라우저 창 고유의 캡션과 프레임은 그대로 남아 있기 때문에 overlapped 윈도우의 정의에 훨씬 더 부합하는 걸 알 수 있다.

3. child

끝으로, WS_CHILD는 동작 방식이 위의 둘과는 굉장히 다르니 이해하기 쉽다.
자기의 위상이 독자적이지 않고 외형상 부모 윈도우의 내부에 종속된 모든 윈도우들은 child 윈도우이다. 대화상자의 내부 컨트롤들이 대표적인 예임.

얘는 컨트롤 ID라는 정보도 갖는다. HWND는 운영체제가 창들을 식별하기 위해 부여하는 가변적인 번호인 반면, ID는 창을 생성하는(= 운영체제에다 생성을 요청하는) 주체 측에서 고정붙박이로 부여하는 번호라는 차이가 있다. GetDlgItem은 이름처럼 굳이 대화상자의 자식 컨트롤뿐만 아니라 부모-자식 관계를 갖는 아무 윈도우에서나 ID값으로부터 자식 창을 얻을 때 사용 가능하다.

popup이나 overlapped 윈도우에는 저런 ID라는 개념이 존재하지 않으며, 그 대신 메뉴를 표시하는 기능이 있다.
뭐, child 윈도우도 비록 메뉴는 태생적으로 없을지언정 마치 overlapped 윈도우처럼 캡션과 프레임, 그리고 시스템 메뉴를 갖는 건 불가능하지 않다. 그 대표적인 예는 MDI 프레임 윈도우이긴 한데.. 그래도 그걸 빼면 캡션과 프레임을 갖춘 child 윈도우는 매우 드물다. 캡션과 프레임 자체가 최상위 윈도우의 상징과도 같으니 말이다.

이렇게 보면 overlapped와 popup이 한 묶음이고, 성격이 다른 child가 혼자 좀 따로 노는 것처럼 보인다. 하지만 동일한 클래스의 윈도우가 상황에 따라서 popup과 child 속성을 취사선택해서 동작하는 경우도 의외로 있다. 콤보 박스에서 내부적으로 쓰이는 ComboLBox라는 리스트 박스가 대표적인 예이다.

콤보 박스의 타입이 Simple이어서(대표적인 예는 글꼴 선택 대화상자) 리스트가 언제나 표시되어 보일 때는 얘는 콤보 박스에 딸려 있는 child 윈도우이다.
그러나 콤보 박스를 클릭하거나 F4를 눌렀을 때만 리스트가 표시되는 drop list 상태일 때는 그 리스트는 대화상자의 위에 별도로 표시되는 popup 윈도우 형태로 생성된다. 이해가 되시겠는가?

차일드 윈도우의 표시 위치는 자기 부모 윈도우의 클라이언트 위치를 기준으로 상대적으로 산정된다. 그런데 자기가 현재 부모 윈도우의 클라이언트 위치 기준으로 어디에 있는지를 한 번에 얻는 게 은근히 힘들다. 대화상자 크기에 따라 차일드 컨트롤들을 적절하게 재배치하는 코드를 작성해 보았다면 이 말이 무슨 뜻인지 잘 알 것이다.

이 경우 GetWindowRect를 한 후에 부모 윈도우를 기준으로 ScreenToClient를 하여 화면 좌표를 한번 거쳐야 하거나, 아니면 번거로운 구조체 초기화를 해야 하는 GetWindowPlacement 함수를 호출해야 한다. 후자 함수의 경우, 최대화된 윈도우라도 원래 있던 위치와 크기까지.. 그 윈도우의 위치와 관련된 모든 정보를 되돌려 주기 때문에 유용하다. 응용 프로그램이 종료 후 나중에 재실행될 때 원래 위치를 100% 그대로 실행되기를 원할 때 이 구조체 값을 백업해 두면 된다.

4. 윈도우 간의 부모/자식 관계

child 윈도우야 그 정의상 태생적으로 부모 자식 관계가 명백하게 존재할 수밖에 없다. 하지만 popup 윈도우도 비록 child처럼 표시되는 위치와 영역이 부모 윈도우 내부로 한정되는 급까지는 아니더라도, 부모 자식 관계 비스무리한 개념이 물론 존재한다.

popup 윈도우는 Z-order상으로 자기 부모 윈도우를 가리고 언제나 더 앞에 출력되며, 부모 윈도우가 소멸될 때 자기도 같이 없어진다. 요렇게 child가 아닌 popup 윈도우의 부모 역할을 하는 윈도우를 개념상으로 owner 윈도우라고 따로 부르기도 한다.

그럼 popup 말고 overlapped 윈도우는? 지금까지 살펴보았듯이 쟤는 애초에 주 용도가 응용 프로그램의 최상단 프레임 껍데기이다. 그러니 태생적으로 부모 윈도우 같은 걸 지정하지 않고 생성되며 부모 자식 관계를 따지는 건 딱히 의미가 없다고 봐야 할 것이다.

그런데, 여기서 유의해야 할 점이 있다. EnumChildWindow나 GetWindow(GW_CHILD) 함수에서 찾아 주는 건 순수하게 child 윈도우들뿐이다. Spy++를 실행하면 계층 구조로 표시된 윈도우 트리를 볼 수 있는데, 이것도 child 윈도우들의 관계만 볼 수 있다.
쉽게 말해 어떤 대화상자 내부의 대화상자(프로퍼티 페이지)라든가 각종 컨트롤들은 계층 구조로 표시되지만, 대화상자에서 얘를 owner로 삼아서 또 다른 modal 대화상자를 꺼내 놓은것을 계층 구조로 보여주지는 않는다는 뜻이다.

자신을 부모(정확히는 owner)로 갖는 서열상 하위의 popup 윈도우들을 한번에 찾아 주는 API는 의외로 존재하지 않는다. 난 이게 당연히 있을 줄 알았는데 없는 걸 발견하고는 개인적으로 굉장히 놀랐다.
일단 top-level 윈도우들을 다 enumerate 한 뒤, 얘들의 owner가 일치하는 놈을 일일이 뒤져 봐야 한다. 그래서 Spy++가 표시해 준 윈도우 리스트가 생각보다 직관적이지 않고 top-level 윈도우가 많은 것이었구나.

이상이다. Windows 프로그래밍을 15년 가까이나 판 본인도 몇 년 전까지만 해도 child는 그렇다 치더라도 popup과 overlapped는 도대체 왜 존재하는 구분인지를 잘 몰랐다. 그리고 parent 윈도우와 owner 윈도우의 관계도 정확하게 모르고 있었고 owned 윈도우는 child 윈도우 조회하듯이 곧장 조회가 가능하지 않다는 것도 미처 생각을 못 하고 있었다. 그러다가 요 근래에야 어렴풋이 이해하게 된 것들을 이렇게 정리해 보았다.

Posted by 사무엘

2017/05/10 08:35 2017/05/10 08:35
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1358

1. 레거시 부동소수점 MBF

컴퓨터에서 쓰이는 2진법 기반의 부동소수점이라는 개념이야 컴공· 전산에서 기본 중의 기본에 속하는 내용이며 본인 역시 이에 대해서 거의 7년 전에 글을 한번 쓴 적이 있다.
본인은 GWBASIC으로 프로그래밍에 처음 발을 내디딘 세대이다. 그런데 베이직이 PC와는 따로 노는 고유한 부동소수점 체계를 갖춘 언어였다는 사실을 30대 나이가 될 때까지 전혀 모르고 있었다.

쉽게 말해 같은 컴퓨터에서 실행한 다음 프로그램의 실행 결과가 GWBASIC과 QuickBasic이 서로 동일하지 않다는 것이다. 참고로 MKS$, MKI$는 해당 숫자들의 binary representation을 문자열 형태로 되돌리는 저수준 함수이다. C++라면 reinterpret_cast<char *>(&num) 한 방이면 끝났을 일이다.

10 INPUT A!
20 IF ABS(A!)<.01 THEN END
30 C$ = MKS$(A!)
40 FOR I = 1 TO 4: PRINT ASC(MID$(C$, I, 1)): NEXT
50 GOTO 10

사용자 삽입 이미지

하긴, 옛날에 베이직은 DEFINT A-Z 같은 걸 하지 않으면 변수의 기본 자료형이 정수가 아니라 실수였다. 언어를 설계할 때 성능보다 인간적인 면모를 더 추구해서 그렇다. (5/3을 구하면 매정하게 1이 아니라 알아서 1.6666..이 나오게..) 그러니 구조적으로 실수를 지원하는 건 필수였다.

때는 무려 1975년, 빌 게이츠가 폴 앨런과 함께 알테어 베이직을 개발하던 시절에 동료들과 함께 뚝딱 해서 2진법 기반의 부동소수점 표기 방식을 만든 게 Microsoft Binary Format, 일명 MBF라는 스펙이 됐다. 32비트와 64비트 두 형태로 말이다.
이 부동소수점은 알테어뿐만 아니라 BASICA, GWBASIC 등 온갖 플랫폼에서 돌아가는 베이직 인터프리터에 두루 쓰이기 시작했다. 지금도 인터넷에 굴러다니는 GWBASIC은 IEEE754가 아닌 MBF 고유 방식으로 부동소수점을 처리한다.

그랬는데 훗날 1984년경에 IEEE754라고 공신력이 더 높은 표준이 등장하면서 판도가 급격히 그쪽으로 기울었다. 게다가 PC에서는 인텔 80x87이라고 오늘날로 치면 하드웨어 가속에 해당하는 수치 연산 보조 프로세서(코프로세서)도 응당 IEEE754를 기반으로 만들어졌다.

마소는 일찍부터 자체적인 부동소수점 포맷을 먼저 제정해서 이를 퍼뜨려 왔지만 이런 시국에서는 자기도 대세를 거스를 수 없게 되었다. GWBASIC의 후신인 QuickBasic도 80년대 중반에 나왔던 1, 2까지는 MBF를 사용했지만 3.0부터는 IEEE 방식으로 갈아탔다. 그 대신 기존 MBF 방식은 별도의 옵션을 줬을 때에만 지원하게 동작이 바뀌었다. (/MBF) MBF 형태로 저장된 부동소수점을 읽어들이는 레거시 프로그램들과의 호환성도 중요하니까 말이다.

그럼 IEEE와 MBF는 어떤 차이가 있었는가? 몇 가지가 있다.
똑같은 32비트 또는 64비트 공간에다 지수와 유효숫자와 부호 비트를 어느 순서대로 어떻게 분배할지 문제는 한 마디로 그냥 정하기 나름이고 대동소이하다. 마치 철도 궤간을 정하는 문제와 비슷하다.

수 전체의 부호 1비트는 IEEE나 MBF든 공통일 수밖에 없고, 32비트의 경우는 지수 8비트, 유효숫자(mantissa) 23비트라는 비율 역시 동일했다. 다만,

(1) IEEE는 2의 보수 기반인 정수의 관행을 존중해서 부호 비트가 수 전체의 최상위 비트에 있는 반면, MBF는 지수와 유효숫자 사이에 존재했다. 다시 말해 mantissa의 최상위 비트에 있는 셈이다. 이렇게 배치를 함으로써 MBF는 IEEE와는 달리 지수와 유효숫자가 딱 8비트와 24비트로 byte padding이 맞춰지게 했다.

(2) 64비트 실수의 경우 이 비율도 달라진다. IEEE는 지수의 공간도 딱 3비트 더 늘어서 11비트이지만, MBF는 여전히 8비트이다. 그래서 32비트 single 실수를 쓰다가 64비트 double 실수를 쓰면 정밀도는 왕창 심지어 IEEE보다도 더 올라가지만 수의 표현 가능 자리수가 늘어나지는 않는다. 그 대신 바이트 경계는 여전히 1:7 비율로 지켜진다.

(3) 다음으로, MBF는 IEEE처럼 denormal number나 NaN, 무한대/무한소 같은 개념도 없다. denormal이야 숫자 표현과 관련된 내부 디테일이니 그렇다 치더라도 베이직 언어로 수학 함수를 사용하면서 NaN이나 무한대/무한소 같은 걸 접한 경험은 없다. 그런 숫자가 생성될 상황이라면 그냥 "Illegal function call" 에러가 나고 말지.
어쩐지 이런 것들은 본인이 훗날 C/C++로 갈아타면서 처음으로 접했다. 이게 엄밀히 말하면 언어 차이가 아니라 이런 부동소수점 표현 방식 때문에 생긴 차이점이다.

세계적으로 문자들은 언어와 문화권마다 제각각이지만 아라비아 숫자만은 세계 공통이다. 컴퓨터 세계도 사정이 얼추 비슷했는데 그나마 유니코드라는 규격 덕분에 동일한 문자는 세계 어디서나 동일한 방식으로 통용 가능해졌다. 그에 반해 숫자가 부동소수점 한정으로 표현 방식이 파편화돼 있었다는 건 개인적으로 무척 흥미롭게 와 닿는다.

C, 파스칼 같은 언어 이름은 함수 호출 규약 명칭에 등장하는데 베이직은 MBF라는 레거시를 보유하고 있구나. IEEE754의 등장 이전에는 MBF 말고 다른 부동소수점 표현 방식은 존재하지 않았나 하는 의문이 남으며, 파스칼에만 있던 6바이트 실수가 규격이 어떠했는지도 다시 보게 된다. 스펙을 검색해 보니 파스칼도 지수부는 8비트이고 나머지가 부호부(1비트)와 가수부(39비트)이다.

2. MOTOR의 정체는?

이 블로그에서 GWBASIC에 대한 추억들 중에서 지금까지 이걸 거론한 적은 없었던 것 같다.
GWBASIC의 대화식 환경에는 코딩 중에 자주 사용하는 키워드들을 곧바로 입력하는 일종의 키매크로가 있었다. F1부터 F10까지 기능키에 배당된 매크로는 LIST, RUN, LOAD...의 순으로 화면 밑줄에 표시되었으며 KEY라는 키워드(?)를 이용해 사용자가 재정의도 할 수 있었다. 후대의 QuickBasic 계열에서는 없어지기도 할 법도 한 키워드인데 KEY에 그 기능만 있는 건 아니기 때문에 없어지지 않고 남아 있긴 하다.

그리고 매크로가 거기에만 있는 게 아니라 Alt+알파벳에도 있었다. A부터 Z 중 J, Q, Y, Z를 제외한 나머지 22개 알파벳에는 AUTO, BSAVE, COLOR ... WIDTH, XOR까지.. 키워드가 즉시 입력되었다. 이 키워드들은 딱히 재정의 가능하지 않았다. RUN과 SCREEN은 Alt에도 있고 F 기능키에도 있었다. (후자는 엔터까지 자동으로 입력된다는 차이가 있음)

그런데 본인이 주목한 것은 M 자리에 배당되어 있던 MOTOR라는 단어였다. 이거 도대체 뭘까? 경험상 숫자 인자를 하나 받는 것 같던데 도대체 하는 일이 뭘까? 두툼한 GWBASIC 매뉴얼/키워드 레퍼런스를 뒤져봐도 의외로 딱히 제대로 설명돼 있지 않았다. 그러니 궁금증은 더욱 커질 수밖에 없었다.

이 역시 전세계에 존재하거나 존재했던 모든 것들에 대한 정보가 손끝 하나로 검색되는 세상이 온 뒤에야 그게 그런 용도였다는 것을 뒤늦게 알 수 있었다.
MOTOR는 카세트 테이프 장치의 헤더를 올리거나 내리는 명령문이었다. 0부터 255 사이의 숫자를 인자로 받긴 하는데 실질적인 의미는 그냥 zero냐 non-zero냐, 쉽게 말해 그냥 bool이었다. 카세트 테이프가 퇴출된 16비트 이상의 IBM-PC급용 베이직에서는 이 명령은 구현되지 않고 아무 동작도 안 하는 레거시 잉여가 되었다.

옛날에 카세트 테이프에다 소스 코드 저장을 SAVE"FILE" 한 뒤 '녹음' 버튼을 눌러서 쭈루룩 하고, 불러오려면 저장되었던 위치로 정확하게 되감기를 하고 LOAD"FILE"한 뒤, '재생' 버튼을 눌러서 했다던데.. MOTOR는 그런 호랑이 담배 피우던 시절에나 유의미한 기능을 했다는 뜻 되겠다.

그런데 왜 이런 잉여가 한때에는 그 시절에는 자주 쓰이기라도 했는지 Alt+M 매크로에 떡 등재돼 있었다. 현실에서는 모터 따위보다는 MOD 연산자 또는 부분문자열을 구하는 MID$ 함수가 훨씬 더 자주 쓰일 텐데 말이다. 그러고 보니 실제로 Alt+M에 MOTOR 대신 쿨하게 MID$가 배당돼 있던 GWBASIC 구현체가 있기도 했던 것 같다. 베이직은 바리에이션 구현체가 워낙 많으니.. 아니면 그건 그냥 내 기억력의 한계로 인한 착각이었는지는 모르겠다.

※ 기타

(1)
이 외에도 GWBASIC은 그러고 보니 소스 코드의 저장도 고유 방식으로 했고 심지어 후대의 QuickBasic에도 비슷한 관행이 있었다(디폴트 옵션). 베이직 언어들은 그 옛날에도 일종의 가상 기계나 독자적인 개발 환경까지 다 짬뽕으로 추구했던 것 같다.
비주얼 베이직의 중후반대(4정도?) 넘어가서 COM 기반의 BSTR 방식으로 갈아타기 전에는 베이직은 문자열도 자기만의 독자적인 이중 포인터 참조 방식으로 구현돼 있었다. 일단 null-terminate 방식이 아니기 때문에 C와는 다름. 이것도 아마 MBF만큼이나 역사가 왕창 오래 된 독자적인 관행이 아닐까 싶다. (문자열에 대해서도 옛날에 한번 글을 쓴 적이 있다.)

(2)
난 C/C++ 파스칼 같은 타 언어로는 도스에서 텍스트 모드에서 색깔을 바꾸고 표준 VGA 그래픽, 특히 mode 13h를 바꾸는 코드를 작성해 본 적이 없다. 베이직에서는 COLOR 내지 SCREEN으로 곧장 됐을 일이 타 언어에서는 표준 라이브러리에서 지원해 주지 않았기 때문이다.
GWBASIC에서 Q(uick)Basic 계열로 바뀌면서 정말 좋은 것 중 하나가 본격적인 VGA 그래픽이 지원된다는 것이었는데, 16진수를 10진수로 바꿔 버릴 생각을 어째 했나 모르겠다. 실제로는 0x13인데 그걸 그냥 13만 써도 되게..;; 그것까지 초보자를 배려한 것이었나 궁금해진다. 그 초보자가 숙련자로 등급이 바뀌는 순간부터 문화 충격을 경험할지도 모르는데..

(3)
베이직이라는 언어 자체는 다트머스 대학의 컴공 교수가 고안한 것이지만, 저런 구현체는 빌 게이츠 같은 괴짜가 아니면 생각해 낼 사람이 별로 없을 물건이다.마소에서는 처음에는 다양한 8/16비트 컴퓨터를 대상으로 베이직 인터프리터를 개발했지만, 사실 IBM PC용으로는 베이직 컴파일러도 DOS 1980년대부터 만들어 오고 있었다.
그래서 Quick이라는 브랜드를 붙여서 QuickBasic 1.0을 1985년에 내놓았다. 이때 퀵베이직은 지원하는 문법은 GWBASIC과 별 차이가 없지만 대화식 환경이 아닌 명령줄에서 컴파일 + EXE 생성만 가능한 베이직일 뿐이었다.

그러다가 1년 주기로 버전 2와 3을 내놓으면서 기존의 구닥다리 행번호 위주가 아닌 구조화 문법이 차근차근 도입되었다. 베이직이라는 언어가 이때(1980년대 중반) 1차로 마개조된 셈이다. 그리고 4.0에 와서야 비로소 함수의 재귀호출이 가능해지고, 즉석 문법 체크와 실행이 지원되는 IDE가 추가되었다. 사실, IDE 자체는 2에서부터 도입됐고 그때 이미 퀵라이브러리도 도입됐다고 하지만, 그때는 지금과 같은 IDE가 아니었다.

그 뒤 1988년 가을에 출시된 QB 4.5가 장수만세 안정판이 되었다. 퀵베이직은 1990년에 어쩐 일인지 버전 5와 6을 건너뛰고 QuickBasic Extended 내지 MS Basic PDS (전문 개발 시스템)이라는 이름으로 7과 7.1 버전까지 개발된 뒤, Visual이라는 브랜드로 바뀌었으며 이때부터 플랫폼도 Window로 바뀌었다. 5, 6을 건너뛴 이유는 퀵베보다 먼저 개발되어 온 그 전신 컴파일러의 버전 번호를 맞췄기 때문이다. (참고로 Visual C++도 IDE의 버전보다 컴파일러의 버전이 더 높음. 전신인 MS C 의 버전을 계승하기 때문이다.)

이 와중에 1991년에 출시된 MS-DOS 5.0에서는 QuickBasic에서 컴파일 기능을 떼어낸 QBasic이라는 물건을 만들고, 이 엔진으로 MS-DOS 4.0까지 내장하고 있던 GWBASIC과, EDLIN 텍스트 에디터를 동시에 대체했다. 무척 흥미로운 점이다. MS-DOS가 전체 화면 형태로 제공하던 유틸리티는 4.0에서 도입됐던 DOS Shell 이후로 이게 둘째가 아닌가 싶다.

Posted by 사무엘

2017/04/28 08:38 2017/04/28 08:38
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1354

임시 파일 다루기

수 년 전에 회사에서 만들어 놨던 코드가 업무상 다시 필요해져서 새 컴퓨터에서 돌려 봤다. 빌드 과정에서는 별 문제가 없었고 실행도 잘 되는 듯했으나.. 데이터 내용을 파일로 잠시 직렬화 덤프한 뒤에 서버로 전송하는 부분이 동작하지 않고 있었다.
문제를 추적해 보니 개발 당시에는 전혀 볼 일이 없었던 엉뚱한 파일명이 내부에 생성된 것이 원인이었다.

그리고 최종적으로 밝혀진 근본 원인은 이러했다. tmpnam_s 함수가 Visual C++ 2015부터는 동작 방식이 싹 바뀌었기 때문이다.
원래 tmpnam은 \ 로 시작하는 파일명만 달랑 되돌렸다. 그러나 2015부터는 운영체제의 공인 임시 디렉터리까지 포함한 전체 경로를 되돌리게 됐다.
예전에는 tmpnam_s의 결과에다가 또 임시 파일 저장용 디렉터리를 붙이는 후처리를 해야 했으나 지금은 그럴 필요 없다. 문자열의 형태가 달라져 버렸으니 기존 코드는 당연히 오동작을 하게 된 것이다.

알고 보니 tmpnam은 Visual C++ 2015 문서의 breaking changes에도 응당 명시돼 있는 아이템이다. 난 보통은 이런 거 꼼꼼히 다 읽어보는 편인데 이 함수는 어쩌다 보니 놓쳤다.
breaking changes는 단순히 어떤 함수· 변수를 제거하거나 형태를 바꾸는 것들이 대부분이기 때문에 기존 코드에 대한 여파는 명백한 컴파일 경고· 에러나 링크 에러 형태로 드러나는 게 대부분이다. 하지만 외형의 변경 없이 내부 동작만 잠수함 패치되어서 동작이 달라지는 식의 변화는 드물다. 프로그램을 실제로 돌려 보기 전까지는 부작용을 알 수 없기 때문이다.

이 코드가 나중에 어디서 또 어떻게 쓰일지 알 수 없는 관계로, 결국은 tmpnam을 감싸는 함수를 만들어야 했다. 얘의 몸체는 #if _MSC_VER >= 1900 이냐 아니냐로 구분해서 어느 VC++에서나 동일한 결과가 나오게 조치를 취했다.
귀찮은 일을 겪긴 했지만 임시 파일이라는 건 십중팔구 전용 임시 디렉터리에다 잠시 만들었다가 지우는 게 바람직하다. 임시 파일과 임시 디렉터리는 마치 바늘과 실처럼, 정수 나눗셈에서 몫과 나머지만큼이나 서로 따라다니는 명칭인 셈이다. 그러니 VC++ 2015에서의 변화는 궁극적으로는 긍정적인 변화이다.

프로그램을 개발하다 보면 임시 파일을 만들어야 할 때가 있다. 하긴, 옛날에 컴퓨터에 메모리가 아주 부족하던 시절에는 페이지 스왑 파일도 임시 파일의 범주에 들었는데 이건 아무래도 응용 프로그램 개발자가 직접 건드리는 파일은 아니다. 디렉터리 이름으로 TEMP라는 명칭을 본인이 최초로 본 게 아래아한글 2.0의 임시 파일 디렉터리였다.
디렉터리 트리 구조, 글꼴 캐시 파일 같은 건 없어도 실행에 지장은 없지만 그래도 반영구적으로 보관하고 참조하라고 만들어진 임시 파일이라는 점에서 성격과 용도가 약간 다르다.

이 정도로 저수준 시스템스러운 것이 아니더라도 특정 API나 기능에 접근하기 위해서, 입력 데이터를 반드시 파일 형태로 줘야 할 때 임시 파일을 만들게 된다. <날개셋> 한글 입력기의 경우 내부적으로 <날개셋> 변환기를 잠시 호출해서 구버전 입력 설정 파일을 변환할 때, 키보드 드라이버 관련 레지스트리 값을 변경하기 위해 레지스트리 편집기를 호출할 때 이런 테크닉을 쓴다.

tmpnam 같은 C 표준 함수 말고 운영체제 API에도 임시 파일과 디렉터리 이름을 얻어 오는 함수가 존재한다.
먼저 디렉터리는... 무슨 C:\asfa\zfdaaf 이렇게 무슨 악성 코드마냥 임의로 생성해서 쓰는 건 아니고, '내 문서', 'Program Files'처럼 임시 파일들의 생성과 보관을 위한 known 위치가 각 사용자 계정별로 따로 있다. GetTempPath 함수를 호출하면 이 위치를 얻어 올 수 있다. 하긴, 사용자 계정이라는 개념이 없던 시절엔 위치가 무슨 시스템 디렉터리처럼 쿨하게 Windows\temp이긴 했었다.

임시 디렉터리는 모든 프로그램들이 한데 공유하는 일종의 공공장소이다. 그래서 임시 파일을 많이 생성하는 프로그램이라면 그 디렉터리 밑에다가 자기 회사나 제품명으로 디렉터리를 또 만들어서 거기에다 파일을 저장하기도 한다. 그 정도로 복잡한 일을 하는 프로그램이 얼마나 될지는 모르겠지만 말이다. 참고로 <날개셋> 한글 입력기는 일부 기능에서 끽해야 파일 하나만 달랑 만들었다가 곧장 지우며, 임시 파일의 생존 주기가 함수 하나의 실행 주기를 벗어나지 않는다.

그럼 디렉터리 다음으로 파일 이름을 구체적으로 어떻게 지을지가 문제로 남는다. 무작위하게 이름을 붙이되, 그게 이미 있는 파일과 겹치지 않는다는 게 보장되어야 한다. 굳이 다른 프로그램이 아니어도 나 자신도 여러 인스턴스 형태로 동시에 실행될 수 있기 때문이다.
그렇기 때문에 임시 파일의 이름은 "자기 고유 명칭 + 숫자"의 형태로 붙곤 한다. 그래서 이 이름의 파일이 이미 존재하면 중복이 없을 때까지 숫자를 1식 증가시켜서 다시 시도한다.

GetTempFileName 함수가 정확하게 이런 일을 한다. 본인은 이 함수의 존재를 알기 전에 저 알고리즘을 수동으로 구현해서 임시 파일 이름을 생성했는데, 나중에 전용 함수에 대해 알게 되자 적지 않게 놀랐다.
이 함수는 '자기 고유 명칭'에 해당하는 접두사를 딱 세 글자 길이까지 받는다. 그 뒤 번호를 인자로 받는데, 유니크한 임시 파일 이름을 생성하는 게 목적이라면 번호는 그냥 0으로 주면 된다. 그러면 생성된 번호를 리턴값으로 돌려주며, 그 이름의 텅 빈 0바이트 파일을 실제로 생성도 해서 '찜'해 준다. 파일 이름을 얻고 파일을 여는 그 짧은 순간에도 혹시나 다른 프로세스나 스레드가 이 이름을 새치기로 찜하지 못하게 하기 위해서이다. 철두철미한 놈..;;

혹시 한 프로그램이 생성해 놓은 임시 파일을 다른 프로그램이 참조해야 한다면 참조하는 프로그램에다가 저 무작위하게 생성된 번호만 전해 주면 된다. 그럼 거기서는 GetTempFileName에다 동일한 접두사와 동일한 디렉터리를 넘기되, 번호는 0이 아니라 외부로부터 받은 그 값을 주면 그 임시 파일의 전체 경로와 이름을 얻을 수 있다.

지금도 어느 컴퓨터에서든 Users\계정명\AppData\Local|Temp 디렉터리에 가 보면 수백· 수천 개의 정체를 알 수 없는 임시 파일들을 볼 수 있다. 특히 "3글자 + 4자리 16진수.tmp"인 파일들은 100% GetTempFileName 함수에 의해 작명된 파일이다. 심지어 Visual C++도 실행해서 프로젝트를 열어 놓은 중에는 edgXXXX.tmp라는 수십 MB에 달하는 임시 파일을 여기에다 만들어서 사용하더라. 저건 Edison Design Group의 이니셜이니 인텔리센스 컴파일러가 사용하는 듯. IDE를 종료하면 물론 지워지고 없어진다.

GetTempFileName는 임시 파일 이름을 생성하는 것과 이미 생성된 명칭을 얻는 것이 모두 가능하며 나름 편리하게 잘 만들어져 있긴 하다. 다만, 파일의 확장자 지정이 안 되고 언제나 tmp로 고정되는 건 약간 불편하다.
(1) 임시 파일을 이름을 무작위 생성해서 파일도 새로 생성하기 또는 (2) 이미 있는 파일을 이름부터 id로부터 얻어 와서 열기 이건 일종의 정형화된 패턴이 있어서 본인은 클래스를 만들어서 사용하고 있다.

이 클래스의 소멸자는 임시 파일을 삭제도 해 준다. 임시 파일의 처리가 별도의 스레드에서 행해진다면 클래스 개체를 스택이 아닌 heap에다 new로 선언해서 개체의 delete 처리를 스레드 함수에게 시키면 된다. 뭐, 별도의 프로세스라면 내가 delete를 해서는 안 될 것이고.
삭제를 제대로 안 해 주면 이것도 일종의 메모리 leak 같은 부작용을 야기할 것이다. 시간이 흐를수록 임시 파일 디렉터리는 수천 개의 쓰레기들이 쌓여서 난장판이 될 테니 말이다. 요즘이야 하드디스크가 용량이 워낙 방대하니 디스크 용량 고갈보다는 파일 관리 성능· 효율 저하 문제가 더 크게 와 닿을 것으로 보인다.

이상. 이렇듯, 디스크의 파일은 메모리와는 달리 기록 효과가 영구적이며, 모든 프로세스에서 32/64비트도 가리지 않고 동일하게 공유 가능하기 때문에 프로세스 간의 데이터 공유와 통신 수단으로도 쓰일 수 있다.
단, 프로세스 사이의 통신 수단으로는 WM_COPYDATA라는 아주 유용한 물건도 있다. 그렇기 때문에 두 프로그램이 모두 윈도우를 생성해 있고 그 창의 주소를 알고 있다면 굳이 임시 파일을 만들었다가 지울 필요 없이 메시지만 주고받아도 된다.

<날개셋> 편집기와 입력 패드는 자기 프로그램이 중복 실행되었을 때 자기가 받아서 갖고 있던 명령줄을 기존 인스턴스에다가 넘겨 주기만 하고 자신은 실행을 종료하는 기능이 있다. 파일을 여는 등의 작업 요청은 기존 인스턴스가 받아서 대신 수행하게 된다. 예전에는 커스텀 메시지 + 임시 파일을 이용해서 명령줄을 전달했으나, 근래에는 훨씬 더 간편한 WM_COPYDATA 기반으로 구현 형태를 변경했다. 왜 진작부터 이 메시지를 안 썼나 모르겠다.

단, 명령줄을 자신의 타 인스턴스로 전달할 때 주의해야 할 점이 있다. 사용자가 명령줄로 전달하는 건 대체로 파일과 경로이다. 이게 절대경로인 경우는 흔치 않으니, 나의 current directory도 같이 전해서 저 경로가 무엇에 대한 상대경로인지를 알 수 있게 해야 한다. 안 그러면 내 쪽에서는 찾을 수 있는 파일을 명령줄을 받는 기존 인스턴스에서는 못 찾게 될 수도 있다. current directory는 프로세스 단위로 고유하게 갖고 있는 상태 정보이다.

Posted by 사무엘

2017/03/30 08:39 2017/03/30 08:39
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1344

C 언어는 가히 프로그래밍 언어계의 라틴어라 해도 과언이 아닌 대중적인 언어가 돼 있다. 얘는 알골(Algol), 그 다음으로 B라는 언어의 뒤를 이어 단순하게 C라고 명명되었으며 1972년에 만들어졌다. 이걸 보면 컴퓨터계에서 3.0 버전이 흥행 대박 친다는 법칙은 언어 분야에서도 유효한 것 같다.

C 언어의 고안자는 '데니스 리치'이다. 이 사람은 지난 2011년 가을에 마침 스티브 잡스와 거의 1주일 간격으로 나란히 작고했다(잡스가 먼저). 그래서 컴퓨터쟁이들 사이에서는 둘 중 덜 유명한 사람이 사실은 컴퓨터계에 훨씬 더 큰 공헌을 했다는 요지의 글을 올리곤 했다.

C는 기본적으로 컴파일 형태로 빌드되는 언어이며, 1990년대를 전후해서 16비트 도스 시절엔 볼랜드라는 회사에서 개발한 터보 C 컴파일러가 아주 대중적으로 쓰였다.
그러나 터보 C보다 이전에 IBM PC용으로 최초로 등장한 C 컴파일러는 Lattice C라고 다른 회사 제품이었다. 1982년인가 그렇다. 이게 그 먼 옛날에 타 플랫폼용으로 개발된 프로그램들을 도스용으로 포팅하는 데 중요한 선구자적 역할을 했다. 얘가 당대의 다른 후속 경쟁사 컴파일러들에 비해 코드 생성 성능도 좋았다고 한다.

사실은 Microsoft C도 Lattice C를 기반으로 개발되었다. 그러다가 1985년에 개발된 MS C 3.0부터 마소가 완전히 독자적인 컴파일러 개발 라인을 구축했다고 영문 위키백과를 보면 나온다. 브라우저에다 비유하자면 IE의 소스에서 모자이크의 소스를 완전히 떼어낸 것과 비슷한 격이겠다.

Windows의 경우 1.0은 처음에 파스칼로 개발되었으며, 이거 영향으로 실행 바이너리들을 들여다보면 export 심벌들의 명칭은 대소문자 구분이 없고 문자열도 앞에 길이가 기록된 형태로 저장되었다고 한다. 대소문자 구분이 없는 건 확실하게 본 기억이 있는 반면, 후자는 잘 모르겠다.

물론 초창기에도 파스칼이 아닌 C언어 기반의 Windows SDK가 있긴 했다. Windows 1.0 SDK의 경우 바로 저 초창기의 MS C 3.0까지는 아니고 4.0과 연계해서 동작했던 걸로 기억한다. 운영체제(?)의 개발과 컴파일러의 개발이 나름 병행되었던 셈이다. 그래도 뭐, 파스칼의 흔적이 어떤 형태로든 과거에 존재했기 때문에 PASCAL이라는 calling convention 명칭도 오늘날까지 legacy로 버젓이 전해지는 아닌가 싶다.

그러다 Lattice C는 1980년대 후반에 개발사가 타사에 인수되었으며 물건 역시 MS, Borland 같은 후발주자 대기업(?) 제품에 밀려서 역사 속으로 사라졌다. C를 제외하면 볼랜드는 파스칼을 민 반면, 마소는 빌 게이츠의 입김과 추억이 담긴 Basic을 밀었다. 베이직이 Quick-을 거쳤다가 나중에 폼 디자인 기능이 탑재된 Visual Basic이 되었다면, C 계열은 Quick-을 거쳤다가 C++ 언어에 MFC까지 탑재하여 Visual C++이라는 공룡으로 거듭났다. 물론, 그래도 VC에 지금과 같은 IDE의 프로토타입이라도 갖춰진 물건은 또 한참 뒤인 4.0 (1995)부터이다.

도스 시절에는 Turbo/Borland라는 브랜드로 볼랜드 컴파일러가 심지어 마소의 컴파일러조차도 따돌리며 리즈 시절을 구가했다. 1990년대 중반이 되면서 32비트 도스라는 틈새시장을 겨냥해서 Watcom, DJGPP 같은 제품이 꼽사리로 꼈을 뿐이며, 정작 마소와 볼랜드는 32비트 도스 플랫폼 지원은 상대적으로 미흡했다.

허나, Windows 95/NT가 널리 퍼지면서 주력 C/C++ 컴파일러는 Visual C++로 판도가 급격히 기울었다. Lotus 1-2-3이 하루아침에 급격히 밀리고 Excel이 천하를 평정했으며, 넷스케이프가 90년대 말에 정말 급격히 몰락한 뒤 IE 세상이 된 것처럼 말이다. 컴파일러는 브라우저처럼 무슨 끼워팔기 독점 같은 게 있지도 않았는데 어쩌다 상황이 바뀌었는지 모르겠다. (옛날엔 플랫폼 SDK와 함께 제공되던 공짜 컴파일러는 상용 Visual C++와 동급의 고성능 컴파일러가 아니었음)

자, 그럼 다음으로 C에 이어 C++도 언어와 컴파일러 역사를 회고해 보겠다. C++은 1970년대 말에 C with Classes라는 가칭으로 개발되었다가 1983년에 지금의 이름으로 첫 발표되었다. C++의 고안자는 덴마크 사람이다. 그리고 초기의 몇 년 동안(1980년대 중반) C++은 인지도가 안습했던 관계로 독자적인 컴파일러가 존재하지 않았다.

오늘날 C++의 위상과 지위를 생각하면 저런 시절이 존재했다는 게 믿어지지 않는다만, 그때는 C++ 코드를 C 코드로 변환해 주는 Cfront라는 전처리기 형태로 C++의 구현체가 명맥을 이었다. 말은 전처리기라고 했지만 소스 코드를 완전히 분석하고 변환하는 것이기 때문에 기술 수준은 엄연히 전처리기를 넘어 컴파일러의 front end급은 된다.

그러다가 C++ 직통 컴파일러가 등장한 것은 1980년대 말~1990년대 초이다. 메이저한 개발사인 볼랜드와 마소에서 C++ 컴파일러를 내놓은 것은 역시나 빨라도 1990년과 그 이후부터이지만, 1980년대 말에.. 그래픽 카드로 치면 VGA의 등장과 비슷한 시기에 C++ 직통 컴파일러를 내놓은 제조사도 있었다.
IBM PC/도스용으로는 Zortech C++가 그런 선구자 축에 든다. 딱 우리나라가 올림픽 하던 시절과 얼추 비슷하게 첫 작품이 나왔다.

Zortech C++은 훗날 1993년경에 Symantec C++ 이라고 브랜드 이름이 바뀌어서 6~7.x 버전까지 개발되었다. 도스와 OS/2, Windows (16/32비트)를 모두 지원하는데 역시나 볼랜드, 마소, 왓컴 같은 다른 브랜드에 밀려서 인지도는 그리 높지 못했던 듯하다.
본인은 먼 옛날에 어둠의 경로를 통해서 이 컴파일러 자체는 접한 적이 있다. Hello, world!만 출력하는 프로그램을 빌드해 봤는데 exe의 크기가 꽤 작게 나왔던 걸로 기억한다.

그리고 Zortech / Symantec C++ 컴파일러의 개발자는 Walter Bright이라고.. 프로그래밍 언어 연구와 컴파일러 개발에만 뼈를 묻은 유명한 아저씨이다. 원래 전공은 전산· 컴공도 아닌 기계공학인데 프로그래머로 전업 후, 컴공에서 최고로 어려운 분야 축에 드는 컴파일러를 곧장 파기 시작했다는 게 대단하다.
이 사람이 D 언어의 고안자이기도 하다는 걸 본인은 최근에 알게 됐다. D에 대해서는 개발자 개인이 아니라 Digital Mars라는 개발사의 이름만 알고 있었기 때문이다.

C++ 컴파일러를 개발하는 현업에 수십 년 종사했으니 그는 C++의 언어 구조와 빌드 과정에 존재하는 구조적인 비효율과 단점에 대해서 누구보다도 잘 알고 있을 것이다. 그러니 자신의 경험과 노하우를 집약해서 네이티브 코드 컴파일 언어이면서 C/C++의 단점을 보완한 새로운 언어를 직접 만드는 지경에 이르렀다. 하지만 D의 지지자· 사용자들이 어떻게든 똘똘 뭉쳐서 언어의 인지도를 끌어올리는 데 목숨을 걸어도 시원찮을 판에, 런타임 라이브러리가 Phobos와 Tango로 분열되고 커뮤니티가 폭파되는 큰 악재를 겪기도 한 모양이다.

거기에다 C++ 자체도 2010년대부터는 부스터를 단 듯이 언어와 라이브러리가 모두 하루가 다르게 미친 듯이 발전하는 중이다. 이게 과연 내가 알던 그 C++가 맞나 싶은 생각이 들 지경이며, 오죽했으면 같은 C++로도 이런 새로운 패러다임을 잔뜩 도입해서 코딩을 하는 걸 Modern C++이라는 비공식 명칭으로 따로 일컬을 정도이다. 이대로 가면 인클루드의 단점을 개선하는 import/패키지 기능까지 가까운 미래에 C++에 도입될 추세다. 그러니 "호환용 레거시가 너무 지저분하다"처럼 태생적으로 어쩔 수 없는 것 빼고는 단점들이 의외로 많이 해소되었다.

그걸로도 모자라서 다른 대기업이나 오픈소스 진영에서도 Rust처럼 네이티브 기반이면서 독특한 패러다임을 담고 있는 언어를 내놓고 있으니 D 역시 자신만의 메리트와 경쟁력을 확보하기 위해서는 갈 길이 아직 먼 것 같다.
C에서 파생형 언어 명칭을 만든 게 C++, C#뿐만 아니라 D라니 참 재미있다. C++뿐만 아니라 C#도 고안자가 덴마크 사람이라니 저 나라도 의외로 전산 강국인 듯하다.

(여담이지만 Walter Bright 아저씨는 컴파일러 개발자 겸 PL 연구자로 이름을 날리기 전인 1970년대부터 이미 Empire이라는 턴 기반 전략 시뮬 게임을 만들기도 했다. 워낙 너무 옛날이니 오늘날과 같은 컴퓨터에서 컬러 그래픽이 나오는 형태의 게임은 아니었겠지만, 아주 어린 시절부터 정말 비범한 분이었다는 건 확실해 보인다. 게다가 저 작품은 전략 시뮬 장르에서 맵의 전체 시야를 노출해 주지 않는 fog of war라는 개념을 첫 도입한 선구자이기도 하다고 한다.)

Walter Bright 말고, 또 볼랜드나 마소 계열도 아니면서 C++ 골수 덕후인 컴파일러 제조사가 하나 더 있다. 바로 Comeau. C++98이던가 03 시절에 그 악명 높은 템플릿 export 키워드를 유일하게 손수 다 구현한 이력도 있는 대단한 용자이다. 얘들 역시 1989년 초에 곧장 C++ 컴파일러를 내놓았으며, 그때부터 도스와 OS/2 등 다양한 플랫폼을 지원했는데, 거기 내부엔 또 어떤 출신과 배경을 가진 컴파일러/PL 괴수가 기업을 이끌고 있나 궁금해진다.

Comeau 컴파일러는 오늘날은 프런트 엔드로는 Edison Design Group의 제품을 사용하여 동작한다. 그럼 저 업체와는 어떤 관계인지 궁금하다. 그리고 프런트가 그런 관계이면 쟤들은 최적화와 타겟 코드 생성 같은 백 엔드 쪽에 차별화 요소가 있어야 할 텐데.. 백 엔드로는 아예 CPU 제조사라는 결정적인 텃새가 있는 인텔 컴파일러도 강세 아니던가? 그런 제품과 경쟁이 되려나 모르겠다.

이상. 이 글은 볼랜드나 마소 같은 유명 대기업 계열이 아니고 그렇다고 gcc 같은 오픈소스 진영도 아니면서 C/C++ 컴파일러를 상업용으로 제일 먼저 PC에다 구현했던 선구자들이 누군지를 문득 생각하면서 끄적여 보았다.

Posted by 사무엘

2017/03/24 19:25 2017/03/24 19:25
, , , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1342

1. WNDCLASS와 HCURSOR

GUI 환경에서 키보드로 글자 입력을 받기 위해 캐럿(caret, 혹은 cursor)이라는 깜빡이는 세로줄이 나타난다면, 마우스의 입력을 받기 위해서는 마우스 포인터라는 게 떠 있다. 키보드 문자 입력과 마우스는 상호 배타적인 관계이다 보니, 문자 입력이 시작되면 마우스 포인터는 화면을 가리지 말라고 쏙 사라지곤 한다. 그 반면, 키보드 단축키와 마우스는 전혀 배타적이지 않고 상호 보완적이므로 이 경우는 마우스 포인터가 사라질 필요가 없다. 간단히 말해 스타를 하는 경우를 생각하면 된다.

Windows 운영체제 내부에서 생성되는 모든 창(window)들은 마우스 포인터가 자기 영역을 지날 때 어떤 모양의 포인터를 표시할지를 자유롭게 지정할 수 있다. 가장 static하고 간단한 방법으로는 윈도우 클래스를 등록할 때 WNDCLASS의 hCursor 멤버에다가 지정해 주면 된다.

HCURSOR라는 타입은 마우스 포인터의 모양을 나타내는 자료구조의 포인터이다. 마우스 포인터는 아이콘(HICON)과 거의 동급으로 취급되며, 아이콘에다가 중심 위치(hot spot) 정보만이 추가되었을 뿐이다. 화살표 그림의 경우 화살표가 가리키는 뾰족한 지점이 바로 hot spot의 위치가 되는 것이다.

그리고 그 아이콘이라는 것은 개념적으로 AND 연산용 비트맵(마스크)과 XOR 연산용 비트맵(그리기)이 추가된 정사각형 비트맵(HBITMAP) 쌍이다.
마우스 포인터 자체를 프로그램 코드를 통해 동적으로 생성하고자 한다면 이런 관계에 대해서도 이해할 필요가 있다. 이런 구조 덕분에 배경색을 반전시키는 마우스 포인터도 만들 수 있다. 또한, Windows에서 아이콘과 마우스 포인터가 매우 유사하게 취급된다는 것은 GetIconInfo 함수나 ICONINFO 구조체의 스펙을 보면 금방 수긍할 수 있다.

색깔 중에 system color가 있고 DC 오브젝트들(브러시· 펜 따위) 중에도 stock object가 있으며, 클립보드 포맷 중에 표준 포맷(CF_TEXT ...)이 있는 것처럼.. 마우스 포인터 중에도 용도가 고정되었고 운영체제 차원에서 모양을 공통으로 관리하는 것이 몇 종류 있다. 이런 공용 포인터의 예로는 일반 화살표, 모래시계, 입력란용 I-beam 등 우리에게 친숙한 것이 있으며, 이들은 제어판을 통해 그 모양을 바꿀 수 있다. 응용 프로그램에서는 LoadCursor(NULL, IDC_*)를 호출해서 이들의 HCURSOR 값을 얻을 수 있으며 이를 응당 클래스 등록 시에 사용하면 된다.

그래픽 에디터라든가 게임 급으로 정말 아주 튀는 GUI를 제공하는 프로그램을 만드는 게 아니라면, 공용 포인터 말고 다른 독자적인 포인터를 쓸 일은 잘 없을 것이다. 하지만 튀지 않는 일반 업무용 프로그램에서도 custom 포인터가 필요한 경우가 가끔은 있다.

  • 워드 프로세서의 경우, IDC_IBEAM의 변형이 필요할 때가 있다. 이탤릭체 글자에서는 포인터의 모양도 살짝 기울어지며, 세로쓰기 모드에서는 포인터의 모양 역시 90도 돌아간다.
  • drag & drop 상태를 표시하기 위해, 화살표 밑에 사각형 테두리와 [+] 마크가 붙은 포인터가 필요할 때가 있다. 이것도 의외로 공용 포인터에는 존재하지 않으며, ole32.dll 내부에 있는 비공식 리소스를 몰래 뽑아 와서 쓰는 경우가 많다.
  • 먼 옛날, IDC_HAND가 존재하지 않던 Windows 95/NT4에서는 winhlp32.exe의 내부에 있는 손가락 링크 모양 비공식 리소스를 몰래 뽑아 와서 하이퍼링크를 구현할 때 쓰기도 했다.

LoadCursor는 원래 모듈(EXE/DLL)의 리소스로부터 마우스 포인터 그림을 추출하는 함수이다.
CreateCursor 함수는 HBITMAP을 받는 게 아니라 쌩짜 AND/XOR 비트맵 배열만을 입력받아서 포인터를 생성해 주는데, 그 말인즉슨 얘는 애초에 모노크롬 포인터밖에 못 만든다는 뜻이다. 컬러를 지원하지 않는다.

그러고 보니 마우스 포인터는 마치 GIF처럼 애니메이션 가능한 버전도 생겨서 단순 아이콘과 차별화가 이뤄지긴 했다. ico 파일에는 크기와 화질이 다른 여러 아이콘들이 있을 수 있다면, ani에는 동일 아이콘의 여러 프레임이 들어갈 수 있게 된 것이다. 교집합인 정보가 있지만 서로 완전히 호환되지는 않는 미묘한 관계가 됐다.

2. WM_SETCURSOR와 SetCursor 함수

윈도우 클래스를 등록할 때 hCursor 멤버에다가 NULL을 지정하면 그 윈도우는 마우스 포인터가 기본적인 화살표로 지정된다거나, 아니면 말 그대로 아무것도 없는 올투명 이미지가 지정되어서 포인터가 사라진다거나 하지 않는다.
어찌 되는가 하면, 이 윈도우 영역으로 들어오기 직전에 유지되었던 마우스 포인터가 변경 없이 그대로 유지된다..! 마치 C언어에서 초기화되지 않은 변수처럼 undefined 상태가 되는 것이다.

이런 동작을 원하는 프로그래머나 기대하는 사용자는 전무할 것이다. 그러므로 클래스 차원에서 지정된 기본 포인터가 없는 윈도우는 자신의 윈도우 프로시저 내부에서 매번 실시간으로 마우스 포인터를 지정해 줘야 한다. 어떻게? WM_SETCURSOR라는 메시지가 왔을 때 SetCursor라는 함수를 호출해서 하면 된다.
아니 사실은 클래스 포인터가 이미 지정돼 있는 창이라도 필요하다면 이렇게 마우스 포인터를 실행 중에 얼마든지 변경할 수 있다. 동일한 웹브라우저 창이라도 포인터가 링크 위를 가리키고 있을 때는 조건부로 손가락 모양으로 바뀌어야 할 테니까 말이다.

윈도우 안에서 마우스 포인터가 움직이면 WM_MOUSEMOVE만 오는 게 아니라 그 전에 WM_SETCURSOR부터 날아온다. 그에 반해 SetCursor는 굳이 WM_SETCURSOR 메시지 타이밍이 아니어도 아무 때나 언제든지 호출 가능하다. 이 함수 자체는 지금 포인터가 나 자신이(스레드 단위) 생성한 윈도우에만 있으면 위치 불문하고 포인터 모양을 즉시 바꿔 준다. WM_PAINT 타이밍 때에만 사용 가능한 BeginPaint/EndPaint처럼 특정 메시지에 매여 있는 게 아니라는 뜻이다.

그럼 왜 굳이 WM_SETCURSOR라는 메시지가 따로 있는 것일까? 그 이유는 저렇게 일상적으로 마우스 포인터가 움직였을 때 빼고는 얘는 WM_MOUSEMOVE와는 설계 철학과 생성 조건이 매우 다르기 때문이다.

  • 윈도우가 disable됐을 때는 그 윈도우로 마우스가 움직이더라도 통상적인 WM_MOUSEMOVE가 오지 않는다. 그러나 이때에도 WM_SETCURSOR는 전달하는 상황 정보(hit-test code)만 달라진 채 언제나 온다.
  • hit-test code가 같이 온다는 점에서 유추할 수 있듯, WM_SETCURSOR는 클라이언트와 논클라이언트를 가리지 않고 온다. 그에 반해 WM_MOUSEMOVE는 클라이언트 영역 전용이고 WM_NCMOUSEMOVE가 따로 있다.
  • 마우스가 capture된 뒤부터는 마우스가 움직이면 반대로 WM_MOUSEMOVE만 오지 WM_SETCURSOR는 오지 않는다. 마우스의 포커스가 포인터 위치와 무관하게 이 윈도우에 집중되었기 때문에 포인터의 모양도 잠시 고정된다.
  • 그리고 결정적으로.. WM_MOUSEMOVE는 지금 화면을 대면하고 있는 최하위 child 윈도우에 직통으로 전달되는 반면, WM_SETCURSOR는 최상위 parent 윈도우에 먼저 전달되어서 얘들이 처리를 포기/거부했을 때에만 child로 내려간다.

마지막 항목이 중요하다. 이런 메커니즘의 차이로 인해 두 메시지는 서로 호환성이 전혀 없으며 별도의 메시지로 분리되어야만 한다. 이 메시지가 그냥 이 시점에서 표시할 HCURSOR 값만 곱게 얻는 게 목적이라면 WM_SETCURSOR 메시지는 SET이 아니라 GET이라는 동사가 붙어서 WM_GETCURSOR, WM_QUERYCURSOR처럼 명명됐을 수도 있다. 대화상자의 WM_GETDLGCODE 메시지처럼 그냥 return (LRESULT)LoadCursor(...)의 형태.
그런데 그게 아니기 때문에 자기가 직접 마우스 포인터를 재지정할 의향이 있다면 WM_SETCURSOR가 올 때마다 SetCursor를 수동으로 매번 호출도 해야 하고, 그러면서 리턴값도 0이 아닌 값으로 되돌려야 한다. 특히 DefWindowProc를 호출해서는 안 된다.

DefWindowProc가 WM_SETCURSOR 때 하는 일 중에는 논클라이언트 영역에서 포인터를 화살표 내지 창의 크기 조절 손잡이 모양으로 바꾸는 것이 포함돼 있다.
하지만 클라이언트 영역에서 DefWindowProc은 "난 마우스 포인터 모양을 자체적으로 처리할 의향이 없으니, (1) 내 부모 윈도우에서 이의 없으면 (2) 최종 처리를 내 자식 윈도우에 맡기겠소"라는 의미가 된다. Def..없이 return 0은 (2)만을 담당한다.

참고로, SetCursor(NULL)을 하면 클래스 WNDCLASS::hCursor = NULL과는 달리 비로소 마우스 포인터가 화면에서 사라진다. 이것은 HideCursor / ShowCursor 함수와 비슷한 효과를 낸다. 이들 함수는 포인터의 레퍼런스 카운터를 1 증가나 감소시켜서 카운터가 양수이면 포인터를 계속 표시시키고, 그렇지 않으면 계속 감추고 있는다. 캐럿을 표시하거나 감추는 ShowCaret / HideCaret과 비슷한 원리로 동작한다.
그에 반해 SetCursor(NULL)은 효과가 일시적이므로 해당 윈도우가 WM_SETCURSOR에서 계속해서 SetCursor(NULL)을 해 줘야만 포인터가 없는 상태가 유지된다.

사소한 사항이다만, WM_MOUSEMOVE는 메시지 큐에 post 형태로 전해지는 반면, WM_SETCURSOR는 리턴값을 꼼꼼히 확인해야 하기 때문에 언제나 sent된다는 차이도 있다. 마우스 메시지 훅킹 같은 걸 한다면 요런 차이가 민감하게 와 닿을 것이다.

3. 대기 상태 표현하기

프로그램이 파일을 읽고 쓰고 복잡한 계산을 시작해서 대략 0.n초 정도 짤막하게 사용자의 응답(더 정확히는 운영체제 메시지)에 반응을 하지 않게 됐다면, 이에 대해 가장 간단하게 피드백을 주는 방법은 SetCursor(LoadCursor(NULL, IDC_WAIT))를 해서 마우스 포인터를 그 악명 높은 모래시계 모양으로 바꾸는 것이다.

물론 처리가 끝났다면 포인터 모양을 원상복구 해야 한다. 이것은 SetCursor의 리턴값을 보관하고 있다가 도로 전달하는 것으로 쉽게 구현 가능하며, 이렇게 시작과 끝을 생성자와 소멸자에다 넣어서 간단한 C++ 클래스를 구현할 수도 있다. MFC에 있는 CWaitCursor가 그 예이다.
모래시계로 변해 있던 동안 마우스 포인터가 조금이라도 다른 곳으로 이동했거나, 위치가 안 바뀌었더라도 그 사이에 포인터 아래의 윈도우가 바뀌었다면.. 프로그램이 의식을 회복(?)했을 때 WM_MOUSEMOVE와 그에 상응하는 WM_SETCURSOR도 오기 때문에 포인터 모양이 자동으로 갱신되긴 한다. 그러나 그런 외부적인 변화가 전혀 없었더라도 포인터 모양이 원상복귀 되어야 하니까 말이다.

마우스 포인터의 움직임은 일종의 하드웨어 인터럽트 형태로 발생하며, 응용 프로그램이 WM_SETCURSOR 메시지에 응답하지 않고 있더라도 포인터가 움직인 것에 대한 반응은 해야 한다. 그렇기 때문에 프로그램이 처리를 열심히 하고 있는 동안에는 좀 전에 지정된 모래시계 모양이 유지된다. 물론, 포인터가 정상적으로 응답 중인 다른 프로그램 창 위에 놓여 있으면 거기 모양으로 바뀌며, 한 프로그램이 수 초 이상 너무 오랫동안 응답을 안 하고 있으면 그건 그것대로 문제가 된다. 내 프로그램 창이 고스트 윈도우로 바뀌는 일은 없어야 한다.

시간이 굉장히 오래 걸리는 작업을 한다면 프로그램의 디자인 형태가 바뀐다. 작업은 백그라운드 스레드에다 담당시키고 프로그램은 현재 진행 상황을 출력하면서 UI 메시지 반응도 평소처럼 한다. progress 컨트롤이 장착된 대화상자가 이 역할을 하며, 사실 Windows Vista부터는 task dialog로 이걸 간단하게 띄울 수도 있게 됐다.
동영상 인코더처럼 input 데이터를 직접 생성하고 작성하는 기능은 없고, 이미 있는 데이터를 변환하는 일이 전부인 프로그램이라면 별도의 대화상자 없이 자기 main frame window 자체가 통째로 진행 상황을 표시하는 용도로 쓰이기도 한다. <날개셋> 변환기도 이런 형태의 프로그램이다.

이를 좀 더 일반화해서 생각하면 이렇다. 어떤 윈도우가 하는 역할이 자신과 별개이고 독립적인 타 작업의 진행 상황을 관찰하면서 표시하는 게 전부라면, 보통은 그 윈도우 내부의 마우스 포인터를 굳이 별도로 모래시계 모양으로 바꾸지 않는다. 설치 프로그램들이 그 예이다. 다만, Windows Installer 엔진의 경우 본격적으로 설치/제거를 수행하는 마법사가 뜨기 전에 준비 작업을 하느라 자그마한 대화상자가 떴을 때는 마우스 포인터를 거기로 가져가면 모래시계로 바뀐다.

사용자 삽입 이미지

요런 게 대화상자 윈도우에서 WM_SETCURSOR를 처리함으로써 구현 가능하다. 이 메시지는 부모-자식 top-to-bottom 형태로 내려가기 때문에, 부모에서 메시지를 가로채 버리면 자식 윈도우의 의도와 상관없이 마우스 포인터를 모래시계 모양으로 바꿀 수 있다. 밑에 지금 무슨 윈도우가 있는지 핸들도 wParam으로 친절하게 전달된다. 여기서 SetCursor 호출만 하고 리턴값으로 nonzero를 지정하지 않으면, 대화상자 배경들만 포인터가 바뀌고 버튼 같은 각종 컨트롤들은 바뀌지 않게 된다. (위의 스크린샷처럼)

이와 대조적으로, 키보드 메시지는 포커스를 잡고 있는 최하위 윈도우에 직통으로 전달되니(bottm-to-top), 그 위에서 공통 단축키 같은 걸 처리하려면 message loop 차원에서의 pre-processing이 필요한 것이다.

<날개셋> 변환기의 경우 변환하는 파일이 적으면 스레드 없이 그냥 비응답 상태로 빠진 채로 변환을 수행한다. 그러나 수십 개, 수MB 이상 분량 파일을 요청하면 대화상자의 모든 컨트롤들을 disable시키고 progress 컨트롤을 출력하고, 대화상자 내부의 마우스 포인터를 모래시계로 바꾼 뒤 변환을 수행한다. 이때는 어차피 대화상자의 다른 기능들을 전혀 사용할 수 없고 ESC나 [X]를 눌러 중간 취소만 가능하기 때문이다.

그리고 하나 더 생각할 만한 상황은.. 딴 작업이 아니라 대화상자 자기 내부에다 출력할 데이터들을 준비하고 초기화하는 작업이 시간이 좀 오래 걸릴 때이다. <날개셋> 한글 입력기 제어판의 대화상자에도 그런 경우가 몇 가지 있다.
이때는 문제의 콤보나 리스트박스가 빈 채로 먼저 대화상자를 출력한 뒤, 스레드를 만들고 마우스 포인터를 IDC_WAIT가 아니라 IDC_APPSTARTING 모양으로 바꿨다. 대화상자가 출력은 됐지만 아직 초기화가 덜 돼서 백그라운드에서 작업 중임을 이렇게 나타낸다.

요렇게 백그라운드의 스레드 작업이 끝난 뒤에는 마우스 포인터를 어떻게 원상복구 할지가 문제가 된다.
아까처럼 스레드 없던 시절에는 작업하던 사이에 포인터 위치가 바뀌었으면 WM_SETCURSOR와 WM_MOUSEMOVE가 자동으로 생겼다. 그러나 지금은 그렇지 않다. 작업이 수행되던 중에 포인터 이동에 대한 처리는 이미 다 이뤄졌기 때문이다.

마우스 포인터의 이동 없이 아래의 창에다가 WM_SETCURSOR를 인위적으로 생성해서 포인터 모양을 원래 것으로 갱신할 수 있어야 하는데.. 이것만 어떻게 하는지 잘 모르겠다.
일단 본인이 사용하는 방법은 GetCursorPos로 현재 포인터 위치를 얻은 뒤, 그거 그대로 SetCursorPos를 하는 것이다. 위치가 바뀐 게 없음에도 불구하고 이렇게 하면 WM_SETCURSOR와 WM_MOUSEMOVE가 생성되기는 하는 것 같더라.
이 정도면 Windows 프로그래밍에서 마우스 포인터 제어와 관련해서 어지간한 문제는 다 다룬 것 같다.

Posted by 사무엘

2017/02/06 08:35 2017/02/06 08:35
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1324

<날개셋> 한글 입력기는 잘 알다시피 16년 전에 개발된 1.0과 지금의 8.6이 요구하는 운영체제 사양(그리고 사실상 하드웨어 사양도)에 차이가 전혀 없는 좀 사기급의 프로그램이다. 32비트 에디션은 Windows 95/NT4 이상에서도 돌아간다. Win95쯤은 안드로이드 스마트폰 내부에서 가상 머신으로도 돌리는 지경이 됐는데도 말이다. 뭐, 내 프로그램은 게임처럼 딱히 최신 사양빨을 타는 분야의 프로그램이 아니며, 한글이 무슨 한자처럼 처리하는 데 메모리가 엄청 많이 든다거나 아랍· 태국 문자처럼 내부 메커니즘이 복잡한 것도 아니기 때문이다.

Windows는 API 함수들이 유니코드를 표방하는 2바이트 문자열을 취급하는 버전(W 함수)과 비유니코드 일명 'ANSI 인코딩'을 표방하는 1바이트 문자열을 취급하는 버전(A 함수)으로 나뉘어 있다. 맥이나 리눅스 같은 타 운영체제에서는 찾을 수 없는 독특한 형태이다. 물론 문자 집합이라는 건 굳이 인코딩 단위에 얽매여 있지는 않으니, 1바이트라는 단위는 그대로 놔 두고 UTF-8만 사용해도 유니코드 지원은 가능했다. 하지만 Windows는 호환성 때문인지 문자 집합과 함께 인코딩까지 완전히 바꿔 버리는 방식을 채택했다. 그래서 wchar_t도 4가 아닌 2바이트이며, UTF-16을 유난히 좋아한다.

Windows NT는 W가 기본이고 A도 호환성 차원에서 지원하지만 Windows 9x는 메모리 부족 문제로 인해 A만 지원하고 W는 아예 제공하지 않았다. 그러니 일반적으로는 Windows 9x를 지원하려다 보면 유니코드를 지원할 수 없어서 깨진 문자 크리 때문에 프로그램의 국제화에 애로사항이 꽃폈으며, 반대로 W 함수만 사용하면 가정에 NT 계열보다 더 많이 보급돼 있던 9x 계열 운영체제를 지원할 수 없었다.

이 딜레마를 해소하는 방법은 일단 프로그램은 W 함수 기반으로 개발한 뒤, 9x에서는 특별히 W 함수 진입로에서 함수 argument를 변환하고 나서 A 함수를 호출하는 일종의 훅/thunk DLL을 구동하는 것이었다. <날개셋> 한글 입력기는 이 테크닉을 사용한다.
훅 DLL의 소스 코드는 동작 방식의 특성상, import table상의 함수 이름 문자열과 거기에 대응하는 훅킹 함수 포인터를 명시한 테이블을 갖고 있다. 또한 기존 Windows API 함수와 프로토타입이 동일하지만, 하는 일에는 살짝 차이가 있는 함수도 즐겨 사용한다.
이런 걸 구현할 때는 C/C++ 언어에 존재하는 다음과 같은 기능들이 유용하게 쓰였다.

1.
함수 훅킹 테이블을 만들 때 #define과 더불어 #(문자열화)와 ##(토큰 연결)라는 전처리기 연산자를 즐겨 썼다.
_FUNC(SetWindowTextW) 하나로 { "SetWindowTextW", (FARPROC)My_SetWindowTextW } 요걸 표현할 수 있으니 전처리기 연산자를 써서 매크로를 정의하는 게 완전 딱이지 않은가?
C언어는 전처리기의 단항 연산자는 # 1개로, 이항 연산자는 # 2개로 표현해서 나름 직관성을 추구했다. 그리고 안 그래도 전처리기 연산자는 C/C++의 고유한 연산자와는 섞여서는 안 되는데 굳이 # 말고 다른 기호를 끌어다 쓰지 않아서 형태 구분이 잘 되게 했다.

그런데 여기서 문제가 하나 있다.
문자열화 연산자는 매크로 전개를 한 놈을 문자열로 바꾸는지, 아니면 언제나 주어진 인자를 문자 그대로 문자열로 바꾸는지를 본인은 엄밀하게 생각을 하지 않고 지냈다. #define ToString(a) #a라고 정의해 주면, ToString(SetWindowText)은 "SetWindowText"로 바뀌는지, 혹은 "SetWindowTextW"나 "SetWindowTextA"로 바뀌는지 궁금했다.

이에 대한 정답을 먼저 말하자면, # 연산자는 그 자체로는 매크로 전개를 전혀 하지 않는다. 그렇기 때문에 저 문제의 정답은 "SetWindowText"이다.
만약 W/A가 붙은 놈을 얻고 싶으면 매크로를 한 단계 더 거쳐 줘야 한다. #define ToString_Expanded(a) ToString(a)를 선언한 뒤, ToString_Expanded(SetWindowText)라고 명령을 내리면 그제서야 "SetWindowTextW"(또는 A)가 얻어진다.

물론 딱히 매크로가 없는 인자를 넘기면 ToString_Expanded는 그냥 ToString과 동일한 결과가 나온다. 이런 차이가 있다는 걸 근래에 알게 됐다.

C/C++ 코드에는 검증과 디버깅을 위해 assert 부류의 매크로를 볼 수 있는데, C 언어 표준 매크로 상수와 연산자들은 상당수가 얘를 구현하기 위해 만들어진 게 아닐까 싶을 정도이다.
상식적으로 생각해 봐도, 실행 파일 내부에 "result > 0이라는 수식의 assertion이 실패했습니다. 아무개.cpp n째 줄입니다." 정도의 검증 명령이 삽입되려면 딱 봐도 __FILE__, __LINE__이 들어가야 했을 것이고 검증 대상 수식은 # 연산자에 의해 문자열로 바뀌었을 거라는 걸 알 수 있다.

파일명과 줄번호는 바이너리 형태의 디버그 심벌에도 포함되긴 하지만, result > 0처럼 대놓고 코드를 구성하는 문자열은 # 연산자 없이는 답이 없다. 이런 사기급의 전처리 기능은 C/C++ 외의 다른 언어에서는 유례를 거의 찾을 수 없지 싶다.

2.
또한 decltype이라는 연산자가 있는 줄을 난생 처음 알았다. 연산자이긴 하지만 되돌리는 게 어떤 값이 아니라 타입 그 자체이다. typeid처럼 RTTI와 관계 있는 기능도 아니며, 컴파일 타임 때 결정되는 고정 타입이다. 그래서

auto x=3.4f;
decltype(3.4f) x = 3.4f;
float x=3.4f;

는 의미가 모두 동일하다. auto와도 어떤 관계인지 바로 알 수 있을 것이다.
sizeof는 값 또는 타입을 모두 받아들여서 값(크기. 고정된 정수)을 되돌리는 반면, decltype은 값을 받아서 타입을 되돌린다는 차이가 있다. 또한 sizeof와 decltype 모두 그 값을 실제로 실행(evaluate)하지는 않는다.

auto는 타입과 동시에 변수값 초기화를 할 때 번거로운 타이핑을 줄여 준다. decltype은 값을 동반하지 않고 타입 자체만을 명시할 때 매우 유용하다. 템플릿 인자를 명시하거나 형변환을 할 때, 길고 복잡한 namespace나 함수 포인터의 프로토타입을 쓰는 수고를 덜어 준다. typedef를 하자니 번거로운 이름을 떠올려야 하는데.. 그럴 필요도 없어진다. 가령,

CAPIPtr<int (*)(int flags, WPARAM wParam)> pfnAbout(hNgsLib, "ngsAbout");

라고 쓸 것을

CAPIPtr<decltype(&::ngsAbout)> pfnAbout(hNgsLib, "ngsAbout");

로 간편하게 대체 가능하다. 함수의 이름만으로 그 함수의 포인터의 프로토타입을 간단히 명시할 수 있으니 얼마나 편리한가? API 훅킹 라이브러리를 만들 때도 이런 문법이 매우 유용할 수밖에 없다. 훅킹 대상인 Wndows API들이야 헤더 파일에 프로토타입이 다 선언돼 있으므로 그걸 decltype의 피연산자로 주면 되기 때문이다..

또한, 과거에는 클래스에서 함수 포인터 형변환 연산자 함수를 선언할 때는 C++ 문법의 한계 때문에 반드시 그 함수 프로토타입을 typedef부터 해야 했다. 하지만 decltype은 여기서도 그런 번거로움을 응당 없애 준다. 아래 코드를 보면 차이를 알 수 있다.

class CMyTable {
    static int _Func();
public:
    //과거
    typedef int (*PFN)();
    operator PFN() { return _Func; }

    //현재
    operator decltype(&CMyTable::_Func)() { return _Func; }
};

decltype 연산자는 Visual C++ 2010부터 지원됐다. 함수 포인터에다가 람다를 바로 대입하는 건 2010은 아니고 2012부터 지원되기 시작했다. 물론 캡처가 없는 람다에 한해서. 람다는 함수 포인터보다 더 추상적인 놈이기 때문에 calling convention은 컴파일러가 알아서 다 해결해 준다.

C++은 잘 알다시피 A *B와 A B(), (A)+B 같은 문장이 A와 B의 정체가 무엇인지에 따라(타입? 값?) 파싱 방식이 완전히 달라진다. 템플릿이 추가된 뒤부터는 <와 >조차도 이항 연산자 vs 타입 명시용의 여닫는 괄호처럼 해석이 달라질 수 있게 되었고, 21세기에 와서는 템플릿 인자를 이중으로 닫을 때 굳이 > > 안 하고 >>로 써도 되게 문법이 바뀌었다. 저게 제대로 돌아가려면 값과 타입의 구분이 더욱 절실히 필요하다.

이런 특성 때문에 템플릿의 컴파일 편의를 위해 typename이라는 힌트 키워드가 도입되었으며, auto와 decltype도 동일한 용도는 아니지만 비슷한 맥락에서 type과 관련된 기술을 돕기 위해 등장한 게 아닌가 싶다.

3.
유니코드 API 훅킹 DLL을 만든다면, SetWindowTextW라면 WCHAR 문자열 형태로 전달된 인자를 char 문자열로 바꾼 뒤 A 함수에다 전달하고, GetWindowTextW라면 먼저 내부적으로 char 버퍼를 준비해서 A 함수를 호출한 뒤, 그걸 WCHAR로 변환해서 사용자에게 되돌리는 형태로 전달한다.

물론 용례가 무궁무진한 메시지를 주고받는 함수라든가 GetOpenFileName처럼 입· 출력 겸용 복잡한 구조체를 운용하는 함수, SystemParametersInfo처럼 PVOID 하나에 온갖 종류의 데이터를 주고받는 함수라면 훅킹 함수를 만들기가 아주 까다로워진다. 하지만 그 함수가 제공하는 모든 기능에다 일일이 변환 기능을 넣을 필요는 없다. 다양한 플래그와 기능들 중에서 내 프로그램이 실제로 사용하는 것에 대해서만 변환을 하면 된다.

그런데 훅킹 함수 중에는 의외로 아무 변환 없이 인자를 그대로 A 함수로 넘기기만 하고 리턴값도 아무 보정 없이 그대로 되돌리는 것도 있다. 훅킹 함수 단계에서 딱히 할 게 없다고 말이다.

그 대표적인 예로는 리소스를 리소스 ID가 아니라 메모리 포인터 차원에서 저수준으로 읽어들이는 DialogBoxIndirect와 LoadMenuIndirect가 있다.
얘들이 인자로 받아들이는 DLGTEMPLATE와 MENUTEMPLATE 구조체는 내부에 PCTSTR 같은 게 없으며, 애초에 A/W 구분이 없다. 왜냐하면 저 구조체는 메모리가 아니라 디스크에 저장되는 리소스 데이터 포맷을 기술하기 때문이다. Windows 9x용이든 NT계열용이든 실행 파일이야 서로 완전히 동일한 포맷이며 리소스들은 모두 유니코드 형태로 저장된다. 그러니 인자가 동일한데 저 두 함수도 원론적으로는 굳이 W/A 구분을 할 필요가 없다.

그럼에도 불구하고 이런 함수에도 굳이 A/W 구분이 존재하는 이유는 얘들이 내부적으로 대화상자와 메뉴 윈도우를 생성할 때 사용하는 CreateWindowEx 함수가 A/W 구분이 존재하며, 9x에서는 W 버전이 존재하지 않기 때문이다. 비록 리소스 데이터 상으로는 원래의 언어 텍스트가 들어있지만, 운영체제가 관리하는 윈도우의 텍스트 버퍼는 ANSI 기반이니 그걸 운영체제의 표준 기능만으로 제대로 표시할 방법도 없다.

그렇다면.. Windows 9x에서는 DialogBoxIndirectW나 LoadMenuIndirectW가 호출 됐을 때,
SetLastError(ERROR_CALL_NOT_IMPLEMENTED); return FALSE / NULL; 을 하지 말고..
return DialogBoxIndirectA( ... ) / LoadMenuIndirectA( ... ); 를 해도 되지 않았나 하는 의문이 남는다. 직통으로 A로 포워딩하는 거 말이다.
그럼 9x에서는 현 ANSI 인코딩으로 표현되지 않는 문자들은 비록 깨져서 출력되겠지만 최소한 메뉴나 대화상자가 뜨고 동작은 하지 않겠는가?

하지만 그건 별 의미가 없다고 생각돼서 조치를 취하지 않은 것 같다. GetOpenFileNameW, CreateFileW, CreateWindowExW, GetMessageW, SendMessageW 등등.. Windows 프로그램의 근간을 이루는 함수들이 유니코드 버전은 몽땅 동작하지 않는데 저런 것만 살려 놔서 뭘 하겠나? Windows 9x에서는 최소한의 유니코드 문자를 찍는 GDI 함수만이 제 기능을 하며, MessageBoxW는 인자들을 char 형태로 변환해서 예외적으로 지원해 주고 있다. 최소한의 에러 메시지를 찍고 종료하는 기능만은 유니코드 API 직통으로 동작하게 말이다. =_=;;

Posted by 사무엘

2017/01/02 08:25 2017/01/02 08:25
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1312

오늘날 Windows에서 실행되는 모든 프로그램들.. exe, dll 따위는 잘 알다시피 portable executable이라는 형식으로 만들어져 있다. 하지만 이 파일 포맷도.. 처음 만들어지던 당시에 여전히 컴퓨터에서 현역이던 도스와 최소한의 호환성을 유지할 필요가 있었기 때문에, 맨 앞에 MZ로 시작하는 16비트 도스 헤더를 여전히 갖추고 있다.

호환성이란 게 딴 게 아니고, 도스에서 Windows용 프로그램이 실행됐을 때 컴퓨터가 다운되는 게 아니라 "이 프로그램은 도스용이 아닙니다" 같은 짤막한 에러 메시지라도 뜨게 하는 것 말이다.

옛날에 Win32s가 제대로 설치되지 않은 상태에서 32비트 프로그램을 Windows 3.1에서 실행했더니.. "상위 버전에서 실행해 주십시오 / Win32s를 다시 설치해 주십시오" 이런 말이 메시지 박스 형태로 뜨는 게 아니라 황당하게 This program cannot be run in DOS mode라고.. 지금 시스템이 아예 Windows가 아닌 듯한 자비심 없는 메시지가 도스창에 떴다. 20여 년 전에 그 인상이 무척 강렬했었다. 요즘은 32비트 OS에서 64비트 exe의 실행을 시도해도 에러 메시지가 그 정도로 막나가는 형태는 아니다.

Windows용 프로그램들은 빌드할 때 그렇게 도스에서 잘못 실행됐을 때를 대비해 짤막하게 대신 실행해 줄 도스용 일명 "stub" 프로그램을 링크 옵션으로 지정할 수 있다. 이름하여 /STUB. 이걸 지정하지 않으면 아까 같은 저런 짤막한 에러 메시지 한 줄만 찍는 기본 stub 프로그램이 들어간다.
16비트 시절에 Visual C++ 1.5x를 보면 그 예제 stub 프로그램 자체가 winstub.exe라고 있었다. 하지만 그 이후부터는 디폴트 stub 프로그램은 그냥 링커 내부에 내장되어 버렸는지 그런 게 따로 있지는 않다.

프로그램을 특수하게 빌드하면 그런 stub을 아예 전혀 집어넣지 않는 것도 가능하다. 맨 앞에 MZ, 그리고 0x3C 오프셋에 PE 헤더가 있는 지점만 들어있으면 되고 나머지 칸은 몽땅 0으로 채움. 심지어 PE 헤더가 0x3C 오프셋보다도 전에, 도스 EXE 헤더가 있어야 할 지점에서 바로 시작하는 것도 가능하다.

미래에 마소에서 빌드하는 EXE/DLL들은 번거로운 This program cannot be ... 메시지를 떼어내고 이렇게 만들어져 나올지도 모른다. 물론 이런 프로그램은 Windows 환경에서 실행하는 건 문제 없지만 만에 하나 어느 레트로 변태 덕후가 그걸 굳이 도스에서 실행해 보면 컴퓨터가 어찌 되는지 책임 못 지는 상태가 될 것이다.

반대로 기본 stub 대신에 꽤 규모 있는 16비트 프로그램을 집어넣어서 동일 EXE가 도스에서도 그럭저럭 기능을 하고 Windows에서도 GUI를 띄우며 제대로 실행되는 프로그램을 만든 경우가 있다. Windows 9x 시절엔 레지스트리 편집기가 그러했다. 이건 Windows에서 보기 드문 하이브리드 universal binary 형태의 프로그램인 것 같다.
16비트 프로그램이 자기 자신 EXE를 열어서 PE 헤더를 파싱해서 리소스 같은 걸 읽어들이는 코드가 같이 빌드되었다면.. 도스 파트가 나중에 합쳐진 Windows 파트와 더불어 한 리소스를 공유하는 형태로 실행될 테니 이 역시 무척 흥미로울 것이다.

이 시점에서 문득 궁금해졌다.
링커가 얹어 주는 기본 stub 프로그램은 명령어가 겨우 몇 바이트밖에 되지 않는다. 얘들은 무슨 의미를 갖고 있는지, 혹시 옛날 16비트 NE 시대와 지금의 PE 시대에 stub 프로그램에 차이가 있는지..?
그래서 오랜만에 도스 API와 8086 어셈블리 명령어 레퍼런스까지 찾아서 stub 프로그램을 분석해 봤다.

stub 프로그램의 코드는 이게 전부이다.

(1) 0E        PUSH CS
(2) 1F        POP DS
(3) BA 0E 00  MOV DX,000E
(4) B4 09     MOV AH,09
(5) CD 21     INT 21
(6) B8 01 4C  MOV AX,4C01
(7) CD 21     INT 21
"문자열"


(1), (2) 맨 앞의 PUSH와 POP은 데이터 세그먼트를 코드 세그먼트의 값과 맞추는(DS=CS) 일종의 초기화이다. 스택에다가 CS 값을 넣은 뒤 그걸 DS로 도로 가져오는 거니까.
지금 이 프로그램은 화면에다 찍을 에러 메시지도 기계어 코드와 정확하게 같은 영역에 있으므로 저건 수긍이 가는 조치이다.

(3) 그 다음으로 DX 레지스터에다가 16진수로 0xE, 즉 14를 기록한다. 저 stub 프로그램은 길이가 정확하게 14바이트이다. 이 값은 프로그램의 시작 지점을 기준(0)으로 해서 그로부터 14바이트 뒤에 있는 문자열을 가리킨다.

(4) AX 레지스터의 high byte에다가 9를 기록한다.

(5) 이렇게 기록된 AX와 DX 레지스터 값을 토대로 0x21 인터럽트를 날려서 도스 API를 호출한다. 도스 API 중 9는 DX가 가리키는 주소에 있는 문자열을 화면, 정확히는 표준 출력에다가 찍는 기능을 수행한다.
그런데 굉장히 기괴한 점이 있는데.. 얘가 받아들이는 문자열은 null-terminated가 아니라 $-terminated여야 한다!

믿어지지 않으면 아무 Windows용 EXE/DLL이나 헥사 에디터로 열어서 앞부분의 에러 메시지 텍스트가 무슨 문자로 끝나는지를 확인해 보시기 바란다.
왜 그렇게 설계되었는지 모르겠다. 파일이나 디렉터리 이름을 받는 도스 API들은 당연히 null-terminated 문자열인데 말이다.

(6) 그 다음, AX 레지스터에다가 0x4C (high)와 0x1 (low)을 기록하고..

(7) 또 도스 API를 호출한다. 0x4C는 프로그램을 종료하는 기능을 하며, 종료와 동시에 low byte에 있는 1이라는 값을 에러코드로 되돌린다. 정상 종료는 0인데 1은 뭔가 오류와 함께 종료되었음을 나타낸다.
사실, 도스 API 레퍼런스를 보면 AH 값으로 0도 프로그램을 종료시키는 역할을 하는 듯하다(도스 1.0때부터 최초). 하지만 모종의 이유로 인해 그건 오늘날은 사용이 별로 권장되지 않으며 0x4C가 원칙이라 한다(도스 2.0에서부터 추가됨).

이렇게 분석 끝. 정말 간결 단순명료하다.
참고로 도스 EXE에서 헤더를 제끼고 기계어 코드가 시작되는 부분은 0x8~0x9 오프셋에 있는 unsigned short값에다가 16을 곱한 오프셋부터이다. 가령, 거기에 04 00 이렇게 적혀 있으면 0x40 오프셋부터 디스어셈블링을 해 나가면 된다. EXE는 헤더에 고정 길이 구조체뿐만 아니라 가변 길이인 '재배치 섹션'이 나오고 그 뒤부터 코드가 시작되기 때문이다.

그럼 과거 16비트 Windows에서 쓰이던 stub은 어떻게 돼 있었을까?
거의 차이가 없긴 한데, 문자열이 들어있는 위치와 얘의 주소를 전하는 방법이 달랐다.

(1) E8 53 00  CALL 0056
"문자열"
20 20 20 20 .. padding 후
(2) 5A        POP DX
(3) 0E        PUSH CS
(4) 1F        POP DS
(5) B4 09     MOV AH,09
(6) CD 21     INT 21
(7) B8 01 4C  MOV AX,4C01
(8) CD 21     INT 21


(1) 맨 먼저 JMP도 아니고 웬 CALL 인스트럭션이 나온다. 기계어로 표기할 때는 인자값이 0x53이어서 3바이트짜리 자기 자신 인스트럭션 이후에 0x53바이트 뒤로 가라는 뜻이 되는데, 영단어로 바꿔서 표기할 때는 자기 자신 원래 위치 기준으로 0x56바이트 뒤가 된다. 이 위치는 그냥 바로 다음 (2) 명령이 있는 곳과 같다.

(2) 함수 호출을 했는데 RET를 하는 게 아니라 스택을 pop하여 DX 레지스터에다 가져온다. 그렇다. 아까 그 call에 대한 복귀 주소에 문자열이 담겨 있으니, 아까 같은 하드코딩이 아닌 요런 방식으로 문자열 주소를 얹었다.

(3) (4) 이제부터는 아까처럼 DS = CS 해 주고,

(5)~(8) 아까와 동일. 문자열을 찍은 뒤 프로그램을 종료한다.

이런 초간단 초미니 프로그램은 exe가 아니라 com 형태로도 만들지 말라는 법이 없어 보인다. com은 그 어떤 헤더나 시그니처도 없이 첫 바이트부터 바로 기계어 코드와 데이터를 써 주면 되는.. 정말 원시적이기 그지없는 바이너리 덤프일 뿐이기 때문이다. 빌드 날짜, 버전, 요구하는 아키텍처나 운영체제 등등 그 어떤 부가정보도 존재하지 않는다.

요즘 프로그래밍 언어들이 기본 제공하는 런타임들의 오버헤드가 너무 크다 보니, 이에 대항하여 세상에서 제일 작은 "Hello world" 프로그램 이런 것에 집착하는 덕후들이 있다. Windows 프로그램의 경우 프로그램을 특수하게 빌드하여 CRT 라이브러리는 당연히 떼어내고, 코드와 데이터도 한 섹션에다 우려넣고, 거기에다 후처리까지 해서 단 몇백 바이트만으로 MessageBoxA(NULL, NULL, "Hello, world!", 0) 하나만 호출하는 프로그램을 만든 예가 있다.

그러나 이런 것들도 com 앞에서는 몽땅 버로우 타야 한다. 얘는 아예 파일 포맷 자체가 없으니까. 이 이상 더 줄일 수가 없다. com 형태로 만든 Hello world 프로그램은 겨우 20몇 바이트가 전부이다.
무슨 명령어를 내렸는지 기억은 안 나지만 컴퓨터를 재시작시키는 com 파일이 있었는데, 얘는 크기가 겨우 2바이트에 불과했다.

(1) BA 0C 01  MOV DX,010C
(2) B4 09     MOV AH,09
(3) CD 21     INT 21
(4) B8 01 4C  MOV AX,4C01
(5) CD 21     INT 21
그 뒤에 "Hello, world!$" 같은 문자열. 따옴표는 제외하고.


com은 exe처럼 코드/데이터 세그먼트 DS=CS 따윈 전혀 신경 쓸 필요 없이, 바로 본론부터 들어가면 된다. 그 대신 com은 16비트 단일 세그먼트 안에서 코드와 데이터 크기 한계가 모두 64K라는 치명적인 한계를 갖는다. 메모리 모델로 치면 그 이름도 유명한 tiny 모델 되겠다. 애초에 exe가 16비트 CPU에서 저 한계를 극복하고, 또 멀티태스킹에 대비하여 재배치도 가능하게 하려고 만들어진 포맷이기도 하다.

아, 아주 중요한 사항이 있다. com에서는 첫 256바이트, 즉 0x100 미만의 메모리 주소는 시스템용으로 예약되어 있어서 사용할 수 없다. 내 코드와 데이터는 0x100부터 시작한다. 그렇기 때문에 저 프로그램의 코드 크기는 12바이트이고, 문자열은 0xC 오프셋부터 시작하긴 하는데 거기에다가 0x100을 더해서 DX에다가는 0x10C를 써 줘야 한다.

Windows PE에다 비유하자면 0x100이 고정된 base address값인 셈이다. 그리고 DX의 값은 그냥 VA이지 RVA가 아니다.
과거에 굴러다니던 exe/com 상호 변환 유틸리티들이 하던 주된 작업 중 하나도 이런 오프셋 재계산이었다. 그리고 com에서 exe라면 모를까 더 넓은 곳에서 좁은 곳으로 맞추는 exe -> com은 아무 exe에 대해서나 가능한 게 물론 아니었다. (단일 세그먼트 안에서만 놀아야..) 과거 도스에 exe2bin이라는 외부 명령어가 있었는데 걔가 사실상 exe2com의 역할을 했다.

아무튼, 저 바이너리 코드와 문자열을 헥사 에디터를 이용해서 입력한 뒤, 파일을 hello.com이라고 명명하여 저장한다. 이걸 도스박스 같은 가상화 프로그램에서 도스 부팅하여 실행하면 신기하게도 Hello, world!가 출력될 것이다.
고급 언어를 사용하지 않고 컴파일러 나부랭이도 전혀 동원하지 않고 가장 원초적인 방법으로 나름 네이티브 실행 파일을 만든 것이다. 사용 가능한 코드와 데이터 용량이 심각하게 작다는 것과, 요즘 64비트 Windows에서는 직통으로 실행조차 할 수 없다는 게 문제이긴 하지만. (네이티브 코드라는 의미가 없다~!)

이런 식으로 컴퓨터에 간단히 명령을 내리고 램 상주 프로그램이나 바이러스 같은 것도 만들기 위해 옛날에는 debug.com이라는 도스 유틸리티가 요긴하게 쓰였다. 간단한 어셈블러/디스어셈블러 겸 헥사 에디터로서 가성비가 뛰어났기 때문이다. edlin 에디터의 바이너리 버전인 것 같다.

오늘날 어셈블리어라는 건 극소수 드라이버/컴파일러 개발자 내지 악성 코드· 보안· 역공학 같은 걸 연구하는 사람들이나 들여다보는 어려운 물건으로 전락한 지 오래다. 하지만 이것도 알면 디버깅이나 코드 분석에 굉장한 도움이 될 듯하다.
디스어셈블리 자체는 주어진 규칙대로 바이트 시퀀스를 몇 바이트씩 떼어서 명령어로 분해해 주는 비교적 간단한 작업일 뿐이다. 파서(parser)가 아니라 스캐너(scanner) 수준의 작업만 하면 된다.

하지만 디스어셈블리가 골치 아프고 귀찮은 이유는 코드의 첫 실행 지점을 정확하게 잡아서 분해를 시작해야 하며, 그래도 어느 게 코드이고 어느 게 데이터인지가 프로그램 실행 문맥에 의해 시시각각 달라지고 무진장 헷갈리기 때문이다. 데이터는 백 날 디스어셈블링 해 봤자 아무 의미가 없고, 오히려 코드의 분석에 방해만 된다. 이런 역공학을 어렵게 하기 위해서 디스어셈블러를 엿먹이는 테크닉도 보안 분야에는 발달해 있다.
하긴, 코드와 데이터가 그렇게 경계 구분 없이 자유자재로 변할 수 있는 게 "폰 노이만 모델 기반의 튜링 기계"가 누리는 극한의 자유이긴 하다.

Posted by 사무엘

2016/12/17 08:34 2016/12/17 08:34
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1306

오옷, 지금까지 내 블로그에서 데이터베이스에 대한 얘기가 거의 없었던 것 같다.
오늘날 정보화· 컴퓨터 세상의 근간을 담당하는 핵심 소프트웨어 기술을 꼽자면 (1) 운영체제(!!), (2) 컴파일러(컴퓨터에서 돌아가는 모든 프로그램들을 생성..), (3) 손실/무손실 압축 알고리즘, 그리고 (4) DB엔진이지 싶다. 딱히 무순으로 나열한 것임.

요즘은 전국민의 신분 근황, 학생들의 모든 학적 정보, 카드 거래 내역, 병원 진료 내역 등등등~ 모든 기록과 행적이 전산화됐다.
그리고 저기서 전산화라는 건 곧 DB화를 의미한다. DB 엔진 없이는 이 복잡한 세상이 돌아갈 수 없는 지경이 된 지 오래다. 또한 key-value 개념부터 시작해 삼라만상의 정보들을 다 표와 표를 융합해서 구축한다는 '관계형'이라는 모델, 그리고 정규화 계층 같은 DB 이론도 깊이 들어가면 생각보다 굉장히 심오하고 복잡하다.

똑같이 총이라 해도 권총부터 시작해 소총, 중기관총, 대포까지 다양한 크기가 있듯이 DB 엔진이라는 것도 스케일이 생각보다 매우 다양하다.
네트워크를 통해 들어오는 수백~수만~수백만 건의 동시 접속 트랜잭션을 소화하면서 방대한 양의 데이터를 극도의 안정성(그 대신 성능 오버헤드도..)을 보장하면서 처리하는 대형 DB 엔진이 있다.
이런 건 일반 사용자가 개인용 PC에서 돌릴 일은 없는 물건이다. 오라클 내지 MS SQL Server 같은 프로그램의 제일 고급 에디션이 이 범주에 해당할 것이며 이런 건 가격도 왕창 비싸다.

MySQL은 저 정도로 방대한 스케일은 아니지만 원격· 다중 접속을 지원하고 로컬 내지 중소규모 웹 서버에서 굴리는 용도로 가성비가 아주 좋다. 게시판이나 블로그 엔진들이 컨텐츠를 얘를 기반으로 구축하곤 한다.

MS Office에 포함돼 있는 Access 정도로 가면 다중 접속은 이제 없고, 서버가 아닌 클라이언트 지향 DB가 된다. 개인용 컴퓨터에서 엑셀로 처리하기엔 좀 방대한 양의 데이터를 엑셀보다 더 프로그래밍 지향적으로 전문적으로 처리하는 도구로 격이 더 낮아진다. 예전에 Visual C++ 책을 봐도 DB 관련 API는 꼭 한 챕터가 할당돼 있었으며, ODBC는 큰 DB, DAO는 좀 작은 DB라고 봤었다.

개인적으로는 성경을 DB로 구축하니 좋았다. 성경은 신구약 전체가 31000구절쯤 되고 역본을 10여 개 갖고 있으면 구절 수가 몇십만 개에 달한다. 그리고 내가 원하는 구절만 쿼리를 날려서 찾는 건 아무래도 스프레드 시트보다는 응당 DB가 제격이다.

또한, 먼 옛날에 컴퓨터 학원에서 dBase III+를 배우던 추억이 떠오른다. 얘도 그 당시로서는 Access에 준하는 체급의 개인용 DBMS라 볼 수 있겠다. SQL이 아닌 독자적인 문법 기반이었고, 명령 프롬프트 모드도 있고 메뉴를 띄워서 DB 파일을 관리하는 assist 모드도 있어서 UI가 독특했다. 또한 dBase가 생성하던 DBF 파일은 도스 시절에 아래아한글도 전화번호부에서 사용하고 DB Viewer를 제공할 정도로 옛날에 꽤 대중적인 파일 포맷이었다.

여느 워드 프로세서나 스프레드 시트와는 달리, DB 프로그램에서는 각 데이터에 속하는 속성들을 자료형과 크기까지 꽤 까다롭게 미리 지정해 놓고 데이터를 넣어야 한다. 프로그램 코딩을 할 때 말고 '자료형'이라는 개념을 따지고 생각해야 하는 분야는 아마 DB밖에 없지 싶다.

사실은 프로그래밍 언어 중에도 자료형이 엄격하지 않고 귀걸이 코걸이 식으로 변할 수 있는 언어가 있다. 그리고 DB 자료형은 엔진에 따라 다르긴 하지만 프로그래밍 언어의 그것과는 달리 딱히 기계 친화적으로 지정하지 않아도 되는 경우가 있다. 숫자형의 표현 범위를 2진법이 아닌 10진법 기준 자릿수로 지정하는 것처럼 말이다.
전화번호는 절대로 숫자형으로 지정하지 말고 문자열형으로 지정해서 넣어야 한다고 학원 선생님에게서 들은 기억이 남아 있다.

"명령줄 기반 + UI + 반쯤 절차형 프로그래밍 환경"이라는 점에서는 이런 DB 프로그램은 매쓰매티카 같은 수학 패키지와도 구조가 비슷한 구석이 있는 것 같다. 아무나 함부로 접근하기는 어렵다는 공통점도 있고 말이다.

그에 비해 엑셀은 어떤가? 대용량 데이터를 취급하는 성능은 DBMS보다 뒤쳐지고, 수식 계산은 수학 패키지에, 비주얼과 레이아웃 기능은 워드 프로세서에 밀린다. 엑셀은 심벌 연산이나 임의 자릿수 계산 기능이 없으며(수학 패키지), 성능을 위해 위지윅(워드 프로세서)도 포기했다.

그럼에도 불구하고 엑셀은 이들 이념을 어중간하게 절충해서 얻은 접근성과 성능, 가성비 덕분에 일반 사용자에게 최고의 업무 처리 앱이 되었다고 볼 수 있다. 일종의 포지셔닝을 잘해서 승리자가 됐다. 한 값이 바뀌었을 때 관련된 셀의 값들이 연달아 쫙 바뀌는 동적인 문서를 손쉽게 만들 수 있는 게 최고의 강점인 듯하다. 또한 피벗테이블/차트는 SQL 같은 거 하나도 몰라도 SELECT 쿼리에서 특히 GROUP BY를 적절하게 구현해 줬다고 볼 수 있다.

DBMS는 굳이 사람만 쓰는 건 아니고 다른 컴퓨터 프로그램이 로컬에서 내부적으로 사용하기도 한다. 에.. 그러니까, 사람이 관리하는 데이터 말고 프로그램이 자기 혼자만 취급하는 데이터를 관리할 목적으로 말이다. 이런 데에 미들웨어 컴포넌트처럼 쓰이는 DB 엔진은 덩치가 더욱 작고 백업· 응급 복구 같은 안전 기능이 없는 대신, 크기· 성능 오버헤드가 더욱 작고 빠르다.

예전에 파일 포맷에 대해서 글을 쓴 적이 있었다. 내 프로그램이 테이블 형태이고 수정이 빈번한 몇백만 개의 대용량 데이터를 다루는데, 파일 포맷을 새로 만들기는 심히 귀찮고 그렇다고 단순 선형적인 바이너리/텍스트 컨테이너 포맷을 쓰기에는 성능이 우려된다면, 범용성으로 인한 약간의 오버헤드를 감수하고라도 저런 내장형 소형 DB를 얹는 게 좋은 선택이 될 수 있다.

괜히 파일 내부에서 골치 아픈 청크가 어떻고 헤더가 어떻고 데이터를 바이너리 비트 수준에서 신경 쓸 필요 없이, 그냥 테이블 스키마.. 이건 프로그래밍 언어로 치면 C/C++ 쓰던 게 아주 고수준 언어로 바뀐 것과도 같다. DB 구조 자체가 일종의 파일 시스템에 대응하니까.

특히 데이터 전체를 무식하게 메모리에 다 올려서 작업하는 형태가 아니라면 DB의 가성비가 더욱 올라간다. 요즘 시대에 다 차려져 있는 밥상인 검증된 오픈소스 솔루션을 놔두고 개발자가 B+ 트리 같은 거 일일이 구현하면서 삽입 삭제 수정 케이스를 일일이 테스트 할 이유가 없기 때문이다.

이런 컴퓨터지향적인 DB는 DB가 하는 본연의 작업에다가 비교/정렬/데이터 변형 알고리즘 같은 일부 핵심 작업만 내가 custom으로 작성한 함수로 대체할 수 있어서 대단히 강력하고 편리하다. 당연히 C/C++로 작성하여 네이티브 코드로 빌드한 함수로 말이다. 파이썬이나 Lua처럼 C/C++ glue에 뛰어난 고급 언어가 있듯, glue에 최적화된 DBMS도 응당 있다.

Visual Studio의 경우 인텔리센스 엔진이 ncb 자체구현 DB를 쓰던 것이 2010부터는 자사의 SQL Server "Compact Edition" DB 기반으로 바뀐 것으로 유명하다. 그런 건 DB를 사용하기 꽤 적절한 용례로 보인다. C++ 문법이란 건 앞으로 또 뭐가 생기고 어떻게 변할지 모르는데 그런 것에 대응하는 것도 파일보다는 DB 지향이 더 유리하겠다.

MS 것 말고도 이 바닥의 유명한 오픈소스 소형 DBMS로는 SQLite가 있다. 리처드 힙이라는 아저씨가 만들었는데, 그냥 오픈소스로도 모자라 골치아픈 LGPL, MIT 라이선스 그딴 것조차 거부하고 소스를 걍 public domain으로 뿌렸다..;;; 그러면서 "님이 받은 만큼 님도 남에게 베풀어 주세요"를 저작권 notice랍시고 적은 게 전부이고.. 천재에다 신자이고 굉장한 대인배이신 듯하다.

The author disclaims copyright to this source code. In place of a legal notice, here is a blessing:
- May you do good and not evil.
- May you find forgiveness for yourself and forgive others.
- May you share freely, never taking more than you give.


모질라 재단의 이메일 클라이언트 유틸인 ThunderBird는 워낙 대용량 편지함을 관리하다 보니 내부 파일이 SQLite DB인 듯하며, 안드로이드 OS에서도 얘를 적극 활용 중이라고 한다. 그러고 보니 소형 DB들은 MS것과 오픈소스 모두 제품명에 compact, lite라는 '꼬마'를 나타내는 단어는 꼭 들어가 있다.

본인도 회사에서 SQLite를 좀 다룰 일이 있었다.
SQLite는 코드가 다양한 플랫폼에서 다양한 문자 인코딩(UTF-8, UTF-16 빅/리틀/디폴트)에 대비하여 API가 굉장히 세심하게 설계된 게 인상적이었다. 하긴, 인코딩에 따라 한글 같은 건 글자 수가 달라져 버리니 정보량에 매우 민감한 DB에서 그걸 민감하게 다루지 않을 수가 없다. 간단하게 단일 문자열로 통합· 추상화가 가능하지 않다는 얘기다.

콜백 함수는 자신이 받고 싶은 문자열의 형태를 지정해 줄 수 있으며, 콜백 함수 자체의 인자는 char도, wchar_t도 아닌 const void*로 돼 있다.
그리고 DB 내부에서 사용하는 문자열뿐만 아니라 열고 싶은 DB 파일을 지정하는 것도 16비트 문자열형 버전이 따로 있는데, 이건 Windows처럼 16비트 문자열을 네이티브로 쓰는 OS에서 CreateFileW 같은 W API를 쓰면서 제 성능을 낼 수 있게 한 배려로 보인다.

다음은 DB와 관련된 여러 문자열 처리 관련 잡설들이다.

1. 정렬

프로그래밍 언어들이 제공하는 문자열 비교는 정말 단순무식하게 숫자 비교의 연장선으로서 각 문자들의 코드값 비교 그 이상도 이하도 아니다. 허나 실생활에서는 오름차순/내림차순부터 시작해 대소문자 구분, 언어 정보를 고려한 비교 같은 복잡다양한 옵션이 필요하다.

대중적이고 자주 쓰이는 옵션은 SQL에서도 언어 차원에서 (1) 옵션을 제공한다. 하지만 좀 더 복잡한 정렬을 위해서는 값을 그대로 비교하는 게 아니라 (2) 사용자가 변조한 값을 비교한다거나 (3) 아예 비교 함수 자체를 customize할 수 있어야 한다.
물론 (3)만 있어도 (1)과 (2)는 다 처리가 가능하니 C 언어의 qsort 함수는 비교 함수만 인자로 받는다. 그러나 파이썬의 정렬 함수는 (1)~(3)까지 다양한 방식으로 운용 가능하다. SQL은 collation이라는 개념으로 정렬 알고리즘 자체를 customize할 수 있다.

2. 토큰화

구분자를 사이에 두고 여러 문자열들이 뭉쳐 있는 문자열을 토큰화해서 문자열(단어)들의 리스트로 뽑아내는 건 탈출문자 인코드/디코드만큼이나 이 바닥에서 굉장히 흔하게 행해지는 작업인 것 같다. 파이썬의 경우 split이라는 메소드가 있다.

그런데 토큰화라는 게 두 부류가 있다. 하나는 구분자가 whitespace 부류이기 때문에 "A    B"나 "A B"나 똑같이 A와 B로 분간되는 것이다. A와 B 자체는 빈 문자열이 될 수 없다.
다른 하나는 구분자가 콤마나 세미콜론 같은 부류이며, 한 구분자가 정확하게 한 아이템만을 분간한다. A,,,B라고 쓰면 A와 B 사이에 빈 문자열이 두 개 더 걸려 나온다..

C가 제공하는 오리지널 strtok는 컨텍스트를 받는 인자가 없어서 (1) 토큰 안에서 또 토큰 구분을 할 수 없으며 멀티스레드 환경에서 사용하기에도 위험하다. 그뿐만이 아니라 얘는 (2) whitespace형 토큰화만 지원하기 때문에 콤마형 토큰화에는 사용할 수 없다는 것도 단점이다. 그래도 뭔가 문자열을 또 복사하고 생성하는 게 없고 성능 하나는 나쁘지 않기 때문에 컨텍스트 인자만 추가해 주면 여전히 유용한 구석은 있다.

DB를 텍스트 형태로 덤프 백업하면 그냥 csv 형태로만 뱉는 게 아니라, 그대로 SQL을 실행만 하면 DB의 재구성이 가능하게 INSERT INTO xxx VALUES가 붙은 형태로 백업되는 것도 많다. DB 스키마는 그냥 CREATE TABLE ... 형태가 될 것이고.
코드와 데이터의 경계가 모호하다. DB 백업도 뭔가 JSON 같은 포맷과 연계 가능하지 않을까 하는 생각이 잠시 들었다.

3. 검색어의 전처리

SQL로 문자열을 검색하고 싶으면 그 이름도 유명한 LIKE 연산자를 쓰면 된다. 어지간한 프로그래밍 언어라면 함수 형태로 구현되었을 기능이 SQL에서는 연산자이다.
얘는 정규 표현식과 같지는 않은데 반쯤은 정규 표현식을 닮은 문법을 지원하여, A LIKE B는 A가 B라는 패턴을 만족하는지 여부를 되돌린다. 0개 이상의 임의의 문자열을 뜻하는 와일드카드가 *가 아니라 %이다. XXX로 시작하는 문자열, 끝나는 문자열, 중간에 XXX가 포함된 문자열 같은 게 다 이걸로 커버 가능하다.

그런데 탈출문자/와일드카드가 존재하는 모든 문자열 체계가 그렇듯이 그 탈출문자 자체는 어찌 표현하느냐가 또 문제가 된다. 이를 위해 SQL에서는 A LIKE B 다음에 ESCAPE C라고, '필요한 경우' 탈출문자를 사용자가 지정해 줄 수 있다. 그래서 \%, \_ 이런 식으로 와일드카드 자체를 표현할 수 있다. 탈출문자 자체는 역시 그 탈출문자를 두 번 찍으면 표현 가능.
탈출문자로는 C/C++처럼 역슬래시를 써도 되지만, 다른 걸 지정해 줘도 된다. SQL은 의외로 이런 데에 유도리가 있다. LIKE는 뒤의 ESCAPE와 합쳐져서 삼항 연산자 역할도 한다고 생각하면 되겠다.

다음으로, SQL에서 문자열 상수(리터럴)는 작은따옴표 또는 큰따옴표로 모두 표현 가능하다. 문자열 내부에 작은따옴표가 있으면 큰따옴표로 둘러싸면 되고, 그 반대의 경우를 사용해도 된다. 그런데 고약하게 문자열 내부에 두 종류의 따옴표가 모두 존재한다면 그 따옴표 자체는 따옴표를 두 번 찍어서 표현하면 된다. 이건 LIKE 연산자가 아니라 SQL 파서 자체에서 인식하는 탈출문자이므로 LIKE 연산자가 인식하는 탈출문자와는 성격이 다르다. C/C++로 비유하자면 위상이 \ 탈출문자와 printf % 탈출문자와의 관계와도 같다.

쿼리 내부에서 따옴표 탈출문자의 처리는 매우 철저하게 해야 한다. 안 그러면 이건 SQL injection이라는 보안 취약점이 되기 때문이다. SELECT ... WHERE id='A' 이런 식으로 쿼리를 작성했는데 A 내부에 또 작은따옴표가 존재해서 문자열 상수를 종결해 버리고, 사용자가 입력한 문자열이 쿼리의 실행에 영향을 줄 수 있다면.. WHERE 절을 언제나 true로 만들 수 있고 DB 내용을 몽땅 유출할 수 있기 때문이다. 이런 사건이 대외적으로는 '해킹' 내지 '개인정보 유출'이라고 보도된다.

Posted by 사무엘

2016/12/11 08:38 2016/12/11 08:38
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1304

Visual Studio 201x, MSDN 이야기

1. 도움말 시스템

Visual C++ (지금의 Visual Studio)이 개발된 이래로 IDE가 제공하는 도움말 및 API 레퍼런스 시스템은 다음과 같이 변모해 왔다.

  • 1세대 1.x~2.x: 그냥 평범한 WinHelp 기반 hlp
  • 2세대 4.x, 5: 리치 텍스트(RTF) 기반의 자체적인 도움말 시스템이 IDE 내부에 통합되어 제공. 같은 컴퓨터 사양에서 RTF 기반 엔진은 이후에 등장한 IE+HTML 기반 엔진보다 텍스트 표시와 스크롤 속도가 훨씬 더 빨랐다.
  • 3세대 6: RTF 대신 HTML 기반의 외부 도움말로 갈아탐. MSDN이라는 명칭 정립.
  • 4세대 200x (.NET ~ 2008): HTML 기반이지만 CHM 말고 다른 컨테이너를 사용하는 Document Explorer. 도움말을 IDE 내부에 구동할 수도 있고 외부에 구동할 수도 있음. 융통성이 생겼다.
  • 5세대 201x: Help Viewer 도입. 버전도 1.0부터 리셋 재시작.

하긴, 비주얼 C++의 프로젝트 파일 포맷도 이와 거의 비슷한 단계를 거치며 바뀌어 왔다. vcp(1세대), mdp(2세대), 3세대(dsw/dsp), 4세대(sln/vcproj), 5세대(sln/vcxproj)의 순. 단, 비주얼 C++ 5는 2세대 도움말 기반이지만 프로젝트 파일은 예외적으로 3세대 6.0과 동일한 dsw/dsp기반이다.

본인은 지금의 일명 5세대 도움말 시스템을 별로 좋아하지 않았다.
일단 5세대 시대를 처음으로 시작한 Visual Studio 2010은 후대 버전은 안 그런데 얘만 유독 무겁고 시동 속도가 무척 느렸다.
그리고 같이 내장된 Help Viewer 1은 '색인' 탭으로 가면 심한 랙이 걸려서 몹시 불편했다. 재래식 4세대 도움말에 비해 기능 차이는 별로 없는데 느리고 무거워지기만 해서 학을 뗐다.

그나마 2012부터는 IDE가 가벼워지고 도움말의 랙도 없어진 듯하다. 그 대신 2010에는 없던 다른 사이드 이펙트가 생겼다.
첫 구동되어서 Help Viewer 스플래시 화면이 뜰 때 마우스 포인터가 움직이지 않을 정도로 컴퓨터가 잠시 stun(멈칫)된다. 구닥다리 내 컴에서만 그런 줄 알았는데 회사의 초고성능 최신식 컴퓨터에서도 동일한 현상이 발생한다.

먼 옛날의 불안정한 유리몸이던 Windows 9x도 아니고 엄연히 7~10급의 최신 OS에서 하드웨어를 도대체 어떻게 건드렸길래 마우스 포인터조차 움직이지 않는 상태가 되나?

잘 알다시피 요즘 Visual Studio IDE는 평범한 Win32 API로 GUI를 만드는 게 아니라 닷넷 + Windows Presentation Foundation 기반으로 특수하게 하드웨어 가속도 받으면서 아주 뽀대나는 방식으로 그래픽을 출력한다.
글자를 찍는 계층도 뭐가 바뀌었는지, 텍스트 에디터에는 트루타입 글꼴만 지정되지 FixedSys 같은 비트맵 글꼴을 사용할 수 없게 바뀌었다. '굴림'은 트루타입이니 사용은 가능하지만 embedded 비트맵이 대신 찍히는 크기에서도 ClearType이 적용되어 색깔이 살짝 바뀌어 찍히며, 같은 글자끼리도 폭이 좀 들쭉날쭉하게 찍힌다.

이렇듯, 재래식 GDI API로 글자를 찍었다면 절대로 나타나지 않을 사이드 이펙트들이 좀 보인다.
그런 특수한 그래픽/GUI를 사용하기 위해서 마치 게임 실행 전처럼 하드웨어 초기화가 일어나고, 그때 마우스 포인터가 살짝 멈추는가 하는 별별 생각이 든다.

2. GDI API 설명은 어디에?

요즘(2010년대) Visual Studio의 MSDN 레퍼런스엔 왜 GDI API들이 누락돼 있는지 궁금하다. BitBlt, SetPixel 같은 것들. desktop app development에 해당하는 몇백 MB짜리 도움말을 분명히 설치했는데도 로컬 도움말에 포함되지 않아서 저것들 설명은 느린 인터넷 외부 링크로 대체된다.

VS 2010에서는 GDI 관련 API들이 색인으로는 접근 가능하지만 목차에서는 존재하지 않아서 접근불가였다. 그리고 MFC 레퍼런스도 단순한 API wrapper의 경우(가령 CDC::MoveTo) See also 란에 자신의 원래 API 함수에 대한 링크(가령 MoveToEx)가 있는데, 요건 내부 링크가 아니라 인터넷 MSDN 사이트의 외부 링크로 바뀌어 있었다.

즉, 그때부터 GDI API의 설명은 제외될 준비를 하고 있었던 듯하다. 그 뒤로 2012인가 2013 이후부터는 그것들이 색인에서도 제외되고 완전히 없어졌다. 2015도 마찬가지인 걸 보니 GDI의 누락은 단순 지엽적인 실수가 아니라 의도적인 계획인 것으로 보인다.

kernel32, user32, advapi32 등 나머지 API들은 다 남아 있는데 왜 GDI만 없앴는지, 얘는 정말로 완전히 deprecate 시킬 작정인지 알 길이 없다. Windows NT 3.1 초창기 때부터 20년이 넘게 운영체제의 중추를 구성해 온 놈인데 그걸 호락호락 없애는 게 가능할까? 게다가 BeginPaint, GetDC처럼 GDI를 다루지만 실제로는 USER 계층에 속해 있는 기초 필수 API조차 언급이 누락된 것은 좀 문제라고 여겨진다.

이런 것 때문에 본인은 Visual Studio는 옛날 Document Explorer 기반이던 200x도 여전히 한 카피 설치해 놓고 지낸다.
옛날에는 또 Visual C++ 2005의 MSDN만 TSF API 레퍼런스도 없고 뭔가 나사가 빠진 듯이 컨텐츠가 왕창 부실해서 내가 놀랐던 기억이 있다. 2003이나 2008은 안 그랬고 걔만 좀 이상했었다.

3. 프로젝트에 소속되지 않은 소스 코드도 심층 분석

Visual C++. 2013인지 2015인지 언제부턴가 프로젝트에 등재되지 않은 임의의 C/C++ 소스 코드를 열었을 때도 이 파일을 임시로 파싱해서 인텔리센스가 동작하기 시작했다. 이거 짱 유용한 기능이다.
전통적으로 프로젝트 소속이 아닌 파일은 문맥을 전혀 알 수 없으며 빌드 대상도 아니기 때문에 IDE에서의 대접이 박했다. 정말 기계적인(문맥 독립적이고 명백한) 신택스 컬러링과 자동 들여쓰기 외에는 자동 완성이나 인텔리센스 따위는 전혀 제공되지 않았다. 전혀 기대를 안 하고 있었는데 이제는 걔들도 miscellaneous file이라는 범주에 넣어서 친절하게 분석해 준다.

4. Spy++

Visual C++에는 프로그램 개발에 유용하게 쓰일 만한 아기자기한 유틸리티들이 같이 포함돼 있다.
'GUID 생성기'라든가 '에러 코드 조회'는 아주 작고 간단하면서도 절대로 빠질 일이 없는 고정 멤버이다.
옛날에는 'OLE/COM 객체 뷰어'라든가 'ActiveX 컨트롤 테스트 컨테이너'처럼 대화상자가 아닌 가변 크기 창을 가진 유틸리티도 있었는데 OLE 내지 ActiveX 쪽 기술이 인기와 약발이 다해서 그런지 6.0인가 닷넷 이후부터는 빠졌다.

그 반면, 기능이 제법 참신하면서 1990년대부터 지금까지 거의 20년 동안 변함없이 Visual C++과 함께 제공되어 온 장수 유틸리티는 단연 Spy++이다.
얘는 제공하는 기능이 크게 변한 건 없었다. 다만 아이콘이 초록색 옷차림의 첩보요원(4.x..!), 분홍색 옷차림(6.0~200x), 검정색 옷차림(2010~현재)으로 몇 차례 바뀌었으며, 운영체제의 최신 메시지가 추가되고 도움말이 hlp에서 chm으로 바뀌는 등 외형만이 최소한의 유지보수를 받아 왔다.

아, 훅킹을 사용한다는 특성상 2000년대 중반엔 64비트 에디션이 따로 추가되기도 했다. 하지만 GUI 껍데기는 x86용 하나만 놔두고 64비트 프로그램에 대해서는 내부적으로 64비트 서버 프로그램을 실행해서 얘와 통신을 하는 식으로 프로그램을 개발하면 더 깔끔했을 텐데 하는 아쉬움이 남는다. 그러면 사용자는 겉보기로 한 프로그램에서 32비트와 64비트 구분 없이 창을 마음대로 들여다보고 훅킹질을 할 수 있을 테니 말이다.

실제로 <날개셋> 입력 패드도 그런 식으로 동작하며, 당장 Visual C++ IDE도 내부적으로 64비트 IPC 서버를 따로 운용하기 때문에 IDE 자체는 32비트이지만 64비트 프로그램도 아무 제약 없이 디버깅이 가능하다. 하지만 안 그래도 훅킹을 하느라 시스템 성능을 잡아먹는 프로그램인데.. 성능 문제 때문에 깔끔하게 64비트 에디션을 따로 빌드한 것일 수도 있으니 Spy++ 개발자의 취향은 존중해 주도록 하겠다.

Spy++는 워낙 역사가 긴 프로그램이기 때문에 초창기 버전은 창/프로세스들의 계층 구조를 전용 트리 컨트롤이 아니라 리스트박스를 정교하게 서브클래싱해서 표현했다. 쉽게 말해 과거 Windows 3.1의 파일 관리자가 디렉터리 계층 구조를 표현한 방식과 비슷하다. 사실은 리스트박스에서 owner draw + user data로 계층 구조를 표현하고 [+/-] 버튼을 눌렀을 때 하부 아이템을 표시하거나 숨기는 건 1990년대 초반에 프로그래밍 잡지에서 즐겨 다뤄진 Windows 프로그래밍 테크닉이기도 했다.

그러다가 VC++ 2005인가 2008 사이쯤에서 Spy++은 운영체제의 트리 컨트롤을 사용하는 걸로 리팩터링이 됐다. 사용자의 입장에서는 기능상의 변화가 없지만 내부적으로는 창을 운용하는 방식이 완전히 바뀐 것이기 때문에 이건 내부적으로 굉장히 큰 공사였으리라 여겨진다.

그런데 VC++ 2010과 함께 제공된 Spy++는 일부 단축키들이 동작하지 않는 버그가 있었다. 전부 먹통인 것도 아니고 창 찾기 Alt+F3, 목록 새로 고침 F5, 속성 표시 Alt+Enter 같은 게 동작하지 않아서 프로그램을 다루기가 불편했다. 이 버그는 잠깐 있었다가 다시 2012 이후에 제공되는 Spy++부터는 고쳐졌다.

Posted by 사무엘

2016/12/03 08:31 2016/12/03 08:31
, ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1301

1.
잘 알다시피 C언어는 원래 '이식성 있는 어셈블리'를 표방할 정도로 고수준 언어의 탈을 쓴 뭐랄까.. 안에서 돌아가는 모든 내부 과정이 있는 그대로 투명하게 노출되고, 프로그램이 메모리 내부에서 다루는 모든 물건들은 비트와 바이트 단위로 접근 가능한 식으로.. 모든 것을 프로그래머 재량에 맡기는 가볍고 이상야릇한 언어라는 성격이 강했다.

그래서 static/global을 제외하면 변수값의 초기화도 몽땅 수동으로 해야 하고, 배열 첨자 체크가 없고 심지어 문자열 타입도 없고.. 뭐 그랬다.
그 대신 공용체와 비트필드 같은 변태스러운 물건은.. C 말고 도대체 다른 어떤 언어에서 찾을 수 있겠는가? 단적인 예로, 부동소수점을 부호, 지수, 가수부별로 쪼개서 내부 구조가 어떻게 돌아가는지 보여주는 프로그램을 C 말고 다른 언어로 만드는 건 직관적이지 못하고 꽤 귀찮은 일이 될 것이다.

내가 직접 코드를 작성하지 않았는데 C가 언어 차원에서 자동으로 해 주는 일이라고는 환경변수 세팅이라든가 main 함수에 전달되는 argument의 파싱 같은 정말 최소한의 초기화밖에 없다시피했다.

하지만 C++은 언어 차원에서 몰래 하는 일이 더 있다. 우리가 빌드하는 프로그램에 코드가 추가되기 때문에 그 존재감과 오버헤드에 대해서 최소한의 인지는 하고 있어야 하는 것들이 종종 있다.
생성자와 소멸자, 임시 R-value 오브젝트, 암시적인 형변환 같은 건 그야말로 기본 중의 기본이고.. 가상 함수가 호출되는 원리도 아주 흔한 예다. C로 표현하자면 pData->vptr->pfnFuncXXX(pData, ...) 과 같은 급의 다단계 포인터 참조 오버헤드가 발생한다. 이런 건 C++ 한다는 사람이 아무리 초짜라도 절대로 몰라서는 안 된다.

가상 상속 정도면 가상 함수보다는 훨씬 볼 일이 없는 물건이다. 컴파일 타임 때 미리 계산된 오프셋으로 기반 클래스를 참조하는 게 아니라 기반 클래스의 위치 자체를 포인터를 통해 런타임 때 얻어 온다고 생각하면 된다.

더 어려운 걸로 내려가자면 pointer-to-member가 구현된 원리가 있는데, 이것도 forward 선언된 클래스 + 다중 상속이라는 변수를 만나면 내부 구현이 더럽게 복잡해지며 컴파일러간의 바이너리 호환성도 깨진다. C++에서 한번 홍역을 치른 뒤에 다른 언어에서는 별로 도입할 생각을 안 하고 있다.

global scope에 속한 객체들이 생성자와 소멸자가 호출되는 타이밍, 순서와 원리도 알아두면 좋다. 이식성을 위해서는 global 객체를 만들지 말고, 번거롭지만 차라리 포인터만 만들어 놓고 new와 delete를 프로그램이 수동으로 하는 게 권장되고 있다.

Exception이라는 것도 아주 요상한 물건이고...
끝으로, 언어 차원에서 지원되기 시작한 RTTI(런타임 시점에서의 타입 정보 인식) 기능도 있다. 하지만 이건 제대로 쓰이지는 않는 것 같다. dynamic_cast, typeid 같은 연산자 말이다. 가상 함수가 존재하는 모든 클래스들에는 자동으로 언어 차원에서의 타입 식별 정보가 추가된다.

얘는 구현 오버헤드가 만만찮으며, 언어의 기능에 의존하지 않고 자체적으로 RTTI를 구현한 레거시 코드도 많기 때문에 결국 컴파일러 옵션이 지정되었을 때만 지원되는 기능이 되었다. Visual C++의 경우 /GR 옵션이다.
개발 역사가 오래 됐고 다중 플랫폼을 지원하는 어지간한 프로젝트들은 이 기능을 사용하지 않는다. 마치 문자열 클래스만큼이나 파편화와 중복 구현이 난립해 있다.
사실, RTTI가 제대로 지원되려면 가상 함수가 존재하는 모든 오브젝트들이 공통으로 상속하는 베이스 클래스라는 개념도 있어서 그 베이스 클래스에서 타입 식별과 관련된 멤버들을 제공해야 하지 않나 싶다.

C++은 언어 차원에서의 개입을 최소화한다는 철학을 가진 언어에서 출발했는데 점점 기능이 비대해지고 언어 차원에서의 개입이 늘고 있다.

2.
포인터는 CPU가 메모리 위치를 식별할 때 사용하는 숫자로, 일반적으로는 machine word와 다를 바 없는 아주 가볍고(= 함수 인자로 값을 그대로 넘겨줄 수 있는) 단순한 자료형이다.
여느 자료형의 포인터는 정수와 reinterpret_cast로 형변환이 가능하다. 함수의 포인터는 + - 산술 연산이 되지 않지만 그래도 역시 정수와 교환이 된다.

하지만 포인터가 machine word 하나와 딱 대응하지 않을 때도 있다.
과거 16비트 시절에는 64KB보다 더 큰 영역의 메모리에 접근하기 위해 세그먼트 번호를 추가로 묶은 far pointer라는 게 있었으며 far은 예약어였다. 뭐 그래 봤자 이 포인터는 32비트 long 정수 하나에 대응했으니, Windows 프로그래밍에서는 L이라는 접두어로 원거리 포인터를 표현했다. LPSTR, LPVOID, LPCWSTR 등.

32/64비트로 오면서 그런 구분이 없어졌기 때문에 접두어 L은 불필요한 잉여가 되었다. 본인 역시 PSTR, PVOID, PCWSTR이라고만 쓴다.
단, PVOID는 winnt.h에 typedef로 정의돼 있는데 const void *는 왜 PCVOID라고 정의돼 있지 않고 여전히 LPCVOID만 있는지는 본인이 알 길이 없다. 믿어지지 않으면 한번 검색해 보시기 바란다. 정말 없다.

그리고 다음으로 machine word 하나와 딱 대응하지 않는 대표적인 기괴한 포인터는 아까도 잠깐 언급됐던 C++의 멤버 포인터이다. 다중 상속은 포인터간의 형변환이 일어났을 때 단순 언어적인 semantic뿐만 아니라 주소값 자체가 바뀔 수도 있는 상황을 만들었으며, pointer-to-member는 이를 보정하는 정보를 담느라 크기가 언제나 machine word 하나에 딱 들어가는 게 보장되지 않게 만들었다.

그래서 멤버 포인터는 신기하게도 reinterpret_cast나 C-style 캐스트로도 결코 숫자로 형변환이 되지 않는다. 숫자 하나가 아니라 구조체 같은 완전 생뚱맞은 자료형으로 취급된다. 크기와 내부 구현이 어떻게 가변적으로 달라질지 모르기 때문에 이것만은 C의 철학과는 정반대로 내부 구현과 접근을 프로그래머로부터 싹 감추고 숨겨 버렸다. 이거 굉장한 이질감이 느껴지지 않으신가?

자주 발생하는 일은 아니지만 구조체에서 어떤 멤버가 구조체의 시작 지점으로부터 정확하게 몇 바이트째 오프셋에 있는지 알고 싶을 때가 있다. 당연한 말이지만 이건 컴파일 타임 때 값이 결정되는 상수이다.
이럴 때 흔히 사용하는 방법은 &((STRUCTURE *)0)->member이다. 이렇게 해도 동작은 잘 하지만 그래도 더 깔끔한 방법이 있었으면 좋겠다는 생각이 든다.

개인적으로는 &STRUCTURE::member가 제일 직관적이고 깔끔한 형태라고 생각한다. 이건 pointer-to-member에 대입 가능한 멤버 주소를 얻을 때 사용하는 문법이다.
member가 static 데이터 멤버라면 저 값은 그놈 자신의 주소가 될 것이고, non-static이라면 메모리 주소가 아니라 자신의 오프셋이 된다. 비록 pointer-to-member(데이터 멤버)가 단순 오프셋의 superset으로서 그 이상의 추상적인 자료형이긴 하지만, 결국은 내부적으로도 오프셋을 갖고 있는 꼴이기 때문에 int형으로 reinterpret_cast도 됐으면 하는 생각이 든다. &((STRUCTURE *)0)->member을 안 써도 되게 말이다.

요즘 C++이 캡처가 없는 람다에 한해서 람다를 함수 포인터로 캐스트하는 것도 지원하듯이, 저것도 같은 맥락에서 정수형과 호환됐으면 하는 아쉬움이 남는다.

Posted by 사무엘

2016/11/22 08:33 2016/11/22 08:33
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1297

« Previous : 1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : ... 25 : Next »

블로그 이미지

철도를 명절 때에나 떠오르는 4대 교통수단 중 하나로만 아는 것은, 예수님을 사대성인· 성인군자 중 하나로만 아는 것과 같다.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2019/11   »
          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

Site Stats

Total hits:
1280848
Today:
35
Yesterday:
501