1. 스마트 포인터의 필요성

C/C++에서 포인터로 참조하는 동적 메모리가 안전하게 관리되기 위해서는.. 가장 간단하게는 포인터의 생명 주기와 그 포인터가 가리키는 메모리 실체의 생명 주기가 동일하게 유지돼야 할 것이다. 어느 한쪽이 소멸되면 다른 쪽도 소멸돼야 한다. C++에서는 이 정도 절차는 포인터를 클래스로 감싸고 그 클래스의 생성자와 소멸자 함수를 구현함으로써 자동화할 수 있다.

하지만 이것만으로 문제가 다 해결되는 건 아니다. 어떤 메모리에 대한 포인터의 ownership이 더 깔끔하게 관리되고 통제돼야 한다. 멀쩡한 주소값이 딴 걸로 바뀌어서 원래 가리키던 메모리로 접근 불가능해지거나(leak..), 이미 해제된 메모리를 계속 가리키고 있다가 사고가 나는 일도 없어야 한다.

그런 일을 예방하려면 여러 포인터가 동일 메모리를 참조하는 것을 완전히 금지하고 막든가, 아니면 reference count 같은 걸 따로 둬서 그런 상황에 대비를 해야 한다. 실행시켰을 때 뻑이 날 만한 짓은 아예 컴파일이 되지 않고 거부되게 해야 한다.
이런 메모리 관리를 자동으로 해 주는 클래스가 표준 C++ 라이브러리에도 물론 구현돼 있으며, 크게 두 가지 관점에서 존재한다.

  • 배열 지향: POD 또는 비교적 단순한 오브젝트들의 동적 배열로, 원소들의 순회, 추가· 삭제와 전체 버퍼 재할당 같은 동작에 최적화돼 있다. 원소 전체 개수와 메모리 할당량 정보가 별도로 들어 있으며, 문자열 클래스도 어찌 보면 배열의 특수한 형태라고 간주할 수 있다. [] 연산자가 오버로딩 돼 있다.
  • 오브젝트 지향: 단일 오브젝트 중심으로 메모리 할당 크기보다는 소유자(ownership) 관리에 더 최적화돼 있다. 그래서 구현 방식에 따라서는 원소 개수 대신 레퍼런스 카운트 정보가 있곤 한다. 담고 있는 타입 형태로 곧장 활용 가능하게 하기 위해, ->와 * 같은 연산자가 반드시 오버로딩 돼 있다.

C/C++은 배열과 포인터의 구분이 애매하니 helper class는 각 분야에 특화된 형태로 따로 구현되었다는 것을 알 수 있다.
배열 버전이야 std::vector라는 유명한 클래스가 있고, 오브젝트를 담당하는 물건을 우리는 smart pointer라는 이름으로 오랫동안 불러 왔다.

Windows 진영에서도 ATL 내지 WTL 라이브러리에는 일반 포인터뿐만 아니라 COM 인터페이스를 감싸서 소멸자에서 Release를 해 주고, 대입 연산자 및 복사 생성자에서 AddRef 따위 처리를 해 주는 간단한 클래스가 물론 있었다.
소멸자는 예외 처리가 섞여 있을 때 더욱 빛을 발한다. 함수의 실행이 종료되는 경로가 여럿 존재하게 됐을 때 goto문을 안 쓰고도 메모리 단속이 꼼꼼하게 되는 것을 언어와 컴파일러 차원에서 보장해 주기 때문이다. 그리고 이 정도 물건은 C++ 좀 다루는 프로그래머라면 아무라도 생각해 내고 구현할 수 있다.

2. 초창기에 도입됐던 auto_ptr과 그 한계

C++은 이런 스마트 포인터도 표준화하려 했으며, 그 결과로 auto_ptr이라는 클래스가 C++98 때부터 도입됐다. 선언된 헤더는 #include <memory>이다.
그러나 auto_ptr는 오늘날의 최신 C++의 관점에서 봤을 때는 썩 좋지 못한 설계 형태로 인해 deprecate됐다. 이미 이걸 사용해서 작성돼 버린 레거시 코드를 실행하는 것 외의 용도로는 사용이 더 권장되거나 지원되지 않게 되었다.

그 대신, C++11부터는 용도를 세분화한 unique_ptr, shared_ptr, weak_ptr이라는 대체제가 등장했다. 이거 마치 C-style cast와 C++ *_cast 4종류 형변환의 관계처럼 보이지 않는가? =_=;;

auto_ptr은 한 메모리를 오직 한 포인터만이 참조하도록 하고 포인터가 사라질 때 소멸자도 호출해 주는 최소한의 기본 조치는 잘 해 줬었다. auto_ptr<T> ptr(new T(arg1, ...)) 같은 꼴로 선언해서 사용하면 됐다. 하지만...

(1) 단일 포인터와 배열의 구분이 없었다.
물론 스마트 포인터는 전문적인 배열 컨테이너 클래스와는 용도가 다르니, 원소의 삽입· 삭제나 원소 개수 관리, 메모리 재할당 처리까지 할 필요는 없다.

하지만 클래스의 소멸자에서 호출해 주는 clean-up을 별도의 템플릿 인자로 추상화하지는 않았고 그냥 delete ptr로 고정해 놓았기 때문에.. 당장 delete와 delete[]조차도 구분할 수 없어서 번거로웠다. 다시 말해 auto_ptr<T> ptr(new T[100]) 이런 식으로 써먹을 수는 없다.

(2) 포인터의 ownership을 관리하는 것까지는 좋으나.. 그게 복사 생성자 내지 대입 연산자에서 우항 피연산자를 변조하는 꽤 기괴한 형태로 구현돼 있었다.
무슨 말이냐 하면.. auto_ptr<T> a(ptr), b에서 b=a 또는 b(a)라고 써 주면.. b는 a가 가리키는 값으로 바뀜과 동시에 a가 가리키는 값은 NULL로 바뀌었다. 즉, 포인터와 메모리의 일대일 관계를 유지시키기 위해, 소유권은 언제나 복사되는 게 아니라 이동되게 한 것이다.

그렇게 구현한 심정은 이해가 되지만, 대입 연산에서 A=B라고 하면 A만 변경되어야지, B가 바뀌는 건 좀 납득이 어렵다.
복사 생성자라는 것도 형태가 T::T(const T&)이지, T::T(T&)는 아니다. 차라리 임시 객체만 받는 R-value 이동 전용 생성자라면 T::T(T&&)이어서 우항의 변조가 허용되지만, 복사 생성자는 그런 용도가 아니다.

(3) 위와 같은 특성이랄지 문제로 인해.. auto_ptr은 call-by-value 형태로 함수의 인자나 리턴값으로 그대로 전달했다간 큰일 났다.
메모리의 소유권이 호출된 함수의 인자로 완전히 옮겨져 버리고, 그 함수가 끝날 때 그 메모리는 auto_ptr의 소멸자에 의해 해제돼 버리기 때문이다. 이 문제를 컴파일러 차원에서 잡아낼 수 없다. (뭐, 이미 free된 메모리를 이중으로 해제시키는 사고는 나지 않는다. 깔끔한 null pointer 접근 에러가 날 뿐.)

auto_ptr을 함수 인자로 전달하려면 그냥 call-by-reference로 하든가, 아니면 그 원래의 T* raw 포인터 형태로 전해야 했다.
아니, 함수 인자뿐만 아니라 값을 그대로 함수의 리턴값으로 전할 때, 혹은 vector 및 list 같은 컨테이너에다 집어넣을 때 등.. 임시 객체가 발생할 만한 모든 상황에서 동일한 문제가 발생하게 된다.

이게 제일 치명적이고 심각한 문제이다. 여러 함수를 드나들고 컨테이너에다 집어넣는 것도 raw pointer와 다를 바 없이 가볍게 되라고 smart pointer를 만들었는데 그러지 못한다면.. 이걸 만든 의미가 없다. 그러면 한 함수 안에서 달랑 소멸자 호출만 자동화해 주는 것 말고는 쓸모가 없다.
또한, 매번 call-by-reference로 전하는 건 엄밀히 말해 포인터의 포인터.. 즉, 포인터를 정수가 아니라 구조체 같은 덩치 큰 물건으로 취급하는 거나 마찬가지이고..

이런 이유로 인해 auto_ptr은 좋은 취지로 도입됐음에도 불구하고, 현재는 이런 게 있었다는 것만 알고 최신 C++에서는 잊어버려야 할 물건이 됐다.
(1) C 라이브러리 함수라든가(gets...) (2) C++ 키워드뿐만 아니라(export) (3) C++ 라이브러리 클래스 중에서도 흑역사가 생긴 셈이다.

auto_ptr이 무슨 보안상의 결함이 있다거나 성능 오버헤드가 크다거나 한 건 아니다. 21세기 이전에는 C++에 R-value 참조자 같은 문법이 없었으니 복사 생성자에다가 move 기능을 집어넣을 수밖에 없었다. 나중에 C++에 언어 차원에서 smart pointer의 불편을 해소해 주는 기능이 추가된 뒤에도 이미 만들어진 클래스의 문법이나 동작을 변경할 수는 없으니 새 클래스를 따로 만들게 된 것일 뿐이다.

3. unique_ptr

auto_ptr의 가장 직접적인 대체제는 unique_ptr이다.
얘는 최신 C++에서 새로 추가된 문법을 활용하여 단일 개체와 배열 개체를 구분할 수 있다. unique_ptr<T>와 unique_ptr<T []>로 말이다. 신기하다..;;
그리고 템플릿 가변 인자 문법을 이용하여 new를 생략하고 std::make_unique<T>(arg1, arg2..) 이렇게 객체를 생성할 수도 있다. 얘는 C++14에서야 도입된 더 새로운 물건이다.

unique_ptr은.. 말 많고 탈 많던 복사 생성자와 대입 연산자가 막혀 있다. 함수에 날것 형태로 전달하거나 컨테이너에 집어넣는 등의 시도를 하면.. 그냥 컴파일 에러가 나게 된다. 그래서 안전하다.
이전의 auto_ptr이 하던 것처럼 소유권을 옮기는 것은 R-value 이동 생성자라든가 std::move 같은 다른 방법으로 하면 된다.

어떤 클래스에 대해서 복사 생성자와 대입 연산자가 구현돼 있지 않으면 컴파일러가 디폴트, trivial 구현을 자동 생성하는 편이다. 각 멤버들에 대한 memcpy 신공 내지 대입 연산자 호출처럼 해야 할 일이 비교적 직관적으로 뻔히 유추 가능하기 때문이다. 하지만 클래스에 따라서는 그런 오지랖이나 유도리가 바람직하지 않으며 이를 금지해야 할 때가 있다. 인스턴스가 단 하나만 존재해야 하는 singleton 클래스, 또는 저렇게 반드시 1핸들, 1리소스 원칙을 유지해야 하는 클래스를 구현할 때 말이다.

그걸 금지하는 가장 전형적이고 전통적인 테크닉은 해당 함수를 private으로 선언해 버리는 것이 있다. (정의는 당연히 하지 말고)
하지만 이것도 friend 함수에서는 안 통하는 한계가 있기 때문에 최신 C++에서는 액세스 등급과 별개로 상속 받았거나 디폴트 구현된 멤버 함수의 사용을 그냥 무조건적으로 금지해 버리는.. = delete라는 문법이 추가되었다. 순수 가상 함수를 나타내는 = 0처럼 말이다! unique_ptr은 이 문법을 사용하고 있다.

그럼 unique_ptr은 컨테이너에 집어넣는 게 전혀 불가능한가 하면.. 그렇지 않다.

vector<unique_ptr<T> > lc;
lc.push_back( unique_ptr<T>(new T) );

처럼 push_back이나 insert에다가 T에 속하는 변수를 줄 게 아니라 저렇게 애초부터 R-value 임시 객체를 주면 된다.
그러면 임시 객체의 ownership이 컨테이너 안으로 자연스럽게 옮겨지고, 컨테이너 안의 unique_ptr만이 유일하게 T를 가리키고 있게 된다.

얘는 auto_ptr보다 상황이 훨씬 더 나아졌고 이제 좀 쓸 만한 smart pointer가 된 것 같다.
사실, 작명 센스조차도.. auto는 도대체 뭘 자동으로 처리해 준다는 건지 좀 막연한 구석이 있었다. 그게 unique/shared로 바뀐 것은 마치 '인공지능'이라는 막연한 용어가 AI 암흑기를 거친 후에 분야별로 더 구체적인 기계학습/패턴인식 같은 말로 바뀐 것과 비슷하게 들리기도 한다. ㅎㅎ

4. shared_ptr와 weak_ptr

그럼 다음으로, shared_ptr을 살펴보자.
얘는 마치 COM의 IUnknown 인터페이스처럼 reference counting을 통해 다수의 포인터가 한 메모리를 참조하는 것에 대한 대비가 돼 있다. 그래서 unique_ptr과 달리, 대입이나 복사를 자유롭게 할 수 있다.

(1) 날포인터는 그냥 대책 없이 허용하기 때문에 ownership 문제가 발생하고.. 아까 (2) auto_ptr은 무조건 ownership을 옮겨 버리고, (3) unique_ptr은 깔끔하게 금지하는데 (4) 얘는 참조 횟수를 관리하면서 허용한다는 차이가 있다. 소멸자는 가리키는 놈의 reference count를 1 감소시켜서 그게 0이 됐을 때만 실제로 메모리를 해제한다.

그래서 shared_ptr은 크기 오버헤드가 좀 있다.
unique_ptr은 일반 포인터 하나와 동일한 크기이고 기술적으로 machine word 하나와 다를 바 없는 반면, shared_ptr은 reference count 데이터를 가리키는 포인터를 추가로 갖고 있다. 일반 포인터 두 개 크기를 차지한다.

이는 static_cast보다 dynamic_cast가 오버헤드가 더 큰 것과 비슷한 모습 같다. 그리고 멤버 포인터가 다중 상속 하에서의 this 오프셋 보정 때문에 추가 정보를 갖고 있다면, 얘는 ownership 관리 때문에 추가 정보를 갖고 있다는 점이 비교된다.

끝으로, weak_ptr이라고, shared_ptr로부터 얻어 올 수 있는 포인터도 있다. 얘는 이름에서 유추할 수 있듯이 reference count를 건드리지 않으며 소멸자에서도 아무 처리를 하지 않는 포인터.. 즉 일반 포인터와 차이가 사실상 없는 물건이다. 순환 참조 문제를 예방하려면 A에서 B를 참조한 뒤에 B에서 또 A를 참조할 때는 레퍼런스 카운트를 건드리지 않아야 하기 때문이다.

그런데도 일반 포인터 대신 굳이 이런 자매품도 따로 만든 이유는 언어 차원에서의 무결성 보장처럼 for the sake of completeness 때문으로 보인다. 무결성 보장이란 게 무슨 말인지 예를 들자면, weak_ptr은 가리키는 주소가 반드시 shared_ptr로부터 유래되었고, unique_ptr과는 절대 섞이지 않는다는 것 말이다.

물론 COM 인터페이스도 아니고 일반 포인터에서 굳이 weak_ptr이 필요할 정도로 극단적인 상황은 현실에서는 거의 없을 것이다. 상상조차 잘 안 된다. 포인터 A가 다른 클래스 B를 가리키는데, 그 클래스 B 내부에 포인터 A가 소속된 다른 객체를 가리키는 포인터가 들어 있다던가.. 뭐 그런 상황 정도이다.
다만, 순환 참조는 단순히 A→B→A뿐만 아니라 A→B→C→A 같은 더 복잡한 형태로도 발생하고, 일단 발생한 것을 감지하기란 몹시 난감하다. 그러니 weak_ptr이라는 개념 자체는 반드시 필요하다.

이상이다. 그냥 생성자와 소멸자를 적절히 구현해 주고 ->와 *만 오버로딩 해 주면 끝일 것 같은 smart pointer도 깊게 들어가면 내막이 생각보다 더 복잡하다는 것을 알 수 있다.
Rust 언어는 garbage collector 기반이 아니면서 더 독특한 방식으로 메모리 소유권을 관리한다던데 그 내막이 어떠했던지가 다시 궁금해진다.

5. 여담

(1) = delete는 다시 봐도 참신하기 그지없다. delete라는 키워드가 연산자 말고 이런 용도로도 활용되는 날이 오더니!
배열 첨자 연산자이던 []와 구조체 참조 연산자이던 ->가 람다 선언에서 의미가 완전히 확장된 것만큼이나 참신하다.
하긴, 옛날에 템플릿이 처음 등장했을 때.. 그저 비교 연산자일 뿐이었던 <와 >가 완전히 새로운 여닫는 형태로 사용되기 시작한 것도 정말 충격적인 변화였을 것이다.

(2) 글쎄, 멤버 함수의 접근을 금지하는 방법이 저렇게 도입됐는데, 어떤 클래스에 대해서 Java의 final이나 C#의 sealed처럼 상속이 더 되지 않게 하는 옵션은 C++에 도입되지 않으려나 모르겠다. C++은 타 언어에 없는 protected, private 상속이 존재하지만 상속 자체를 금지하는 옵션은 없어서 말이다.

특히 내부 구조가 아주 간단하고 가상 함수가 존재하지 않는 것, 특히 소멸자가 가상 함수 형태로 별도로 선언되지 않은 클래스는 상속을 해도 어차피 polymorphism을 제대로 살릴 수 없다. 그냥 단순 기능 확장에만 의미를 둬야 할 것이다.
Java는 모든 함수가 기본적으로 가상 함수일 정도로 유연한데도 이와 별개로 상속을 금지하는 옵션이 있는데.. 그보다 더 경직된 언어인 C++은 의외로 그런 기능이 없다.

(3) C/C++의 사고방식에 익숙한 프로그래머라면 포인터란 곧 메모리 주소이고, 본질적으로 machine word와 동일한 크기의 부호 없는 정수 하나일 뿐이라는 편견 아닌 편견을 갖고 있다.
하지만 객체지향이라든가 함수형 등 프로그래밍 언어 이론을 조금이라도 제대로 구현하려면 숫자 하나만으로 모든 것을 표현하기엔 부족한 포인터가 얼마든지 등장하게 된다.

앞서 다뤘던 shared_ptr이라든가 다중 상속을 지원하는 멤버 함수 포인터..
그리고 자기를 감싸는 문맥 정보가 담긴 클래스 객체 포인터라든가 람다 함수 포인터 말이다.
C++은 전자를 기본 지원하지 않기 때문에 모든 클래스들이 Java 용어로 치면 개념적으로 static class인 거나 마찬가지이다.
그리고 후자를 기본 지원하지 않기 때문에 람다는 캡처가 없는 놈만 기존 함수 포인터에다 담을 수 있다.

그런 것들이 내부적으로 어떻게 구현되고 구현하는 시공간 비용이 어찌 되는지를 프로그래머라면 한 번쯤 생각할 필요가 있어 보인다.

(4) C++에서 class T; struct V; 처럼 이름만 전방 선언된 incomplete type에 대해서는 제일 단순한 직통 포인터, 그리고 무리수가 좀 들어간 멤버 포인터 정도만 선언할 수 있다. T나 V의 실체를 모르니 이런 타입의 개체를 생성하거나, 포인터를 실제로 참조해서 뭔가를 할 수는 없다.
그런데 이런 불완전한 타입을 가리키는 포인터를 상대로 delete는 가능할까? 난 이런 상황에 대해 지금까지 한 번도 생각해 본 적이 없었다.

sizeof(T)의 값을 모르더라도 포인터가 가리키는 heap 메모리 블록을 free하는 것은 얼마든지 가능하다. 애초에 malloc/void가 취급하는 것도 아무런 타입 정보가 없는 void*이니 말이다.
그러니 operator delete(ptr)은 할 수 있지만, 해당 타입에 대한 소멸자 함수는 호출되지 못한다.

컴파일러는 이런 코드에 대해서 경고를 띄우는 편이다. Visual C++의 경우 C4510이며, delete뿐만 아니라 delete[]에 대해서도 동일한 정책이 적용된다.

Posted by 사무엘

2019/10/09 08:35 2019/10/09 08:35
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1671

그리고.. 이튿날 아침에는 계속해서 봉화로 이동하기 시작했다. 새벽에는 계절이 바뀌기라도 했나 싶을 정도로 공기가 차가워져 있었다.

사용자 삽입 이미지

주변엔 계속해서 이런 풍경이 펼쳐졌다.
그러고 보니 옛날 도로는 가드레일 말고 저렇게 노란 경계석이 쓰였던 것 같은데 요즘은 어느 샌가 보기 힘든 풍경이 돼 있다.

사용자 삽입 이미지

중간에 마을 어귀를 흐르는 요런 맑은 개울을 발견해서 차를 세우고 물놀이를 했다.
이럴 때 햇볕이 쨍쨍하고 날씨가 더 더웠으면 물놀이의 효과가 더 커졌겠지만, 폭염과 가뭄 때문에 개울이 말라 버리는 것보다야 이렇게라도 물에 들어가는 게 훨씬 더 낫다. 비 덕분에 여기 모든 개천· 개울들은 물이 콸콸 세차게 흐르고 있어서 참 보기 좋았다.
그렇잖아도 어제 땀을 많이 흘려서 몸이 온통 끈적거리는 상태였는데 싹 개운해졌다.

사용자 삽입 이미지사용자 삽입 이미지

그리고.. 일월산 자생화 공원이라는 곳을 발견해서 들렀다가 갔다. 주변에 나밖에 없는 오지에서 자연을 즐긴다는 건 참 멋진 일이다. 지구가 금성 화성과 달리 초록별이라는 사실에 경이로움과 고마움을 느끼게 된다.

사용자 삽입 이미지

영양에서 봉화로 가려면 역시 산을 하나 넘어야 하더라. 길은 어느 샌가 꼬불꼬불한 산길로 바뀌었다.
터널 안에 들어갈 때는 일반적으로는 헤드라이트를 켜는 게 맞는데, 현실에서는 '끄시오'라고 안내된 표지판도 많이 보인다. 화장실에서 휴지는 변기에 "넣으시오/넣지 마시오"만큼이나 사람을 헷갈리게 만드는 사항인 것 같다.

저 영양 터널을 지난 다음에는 행정구역이 봉화로 바뀌고, 봉화 터널과 어느 공군 부대가 뒤이어 등장했다. 그리고 오르막이 끝나고 내리막이 시작됐다.

6. 봉화군 탐험기

본인은 이번 여행의 마지막 일정으로, 봉화에서 영동선 양원 역을 답사하고 봉화 시가지로 돌아갔다.

사용자 삽입 이미지
사용자 삽입 이미지사용자 삽입 이미지

상· 하행 합쳐도 차가 몇 분에 한 대 다닐까말까인 이 꼬불꼬불 산길을 떠나고 싶지 않았다. 그래서 수시로 정차하고 사진 찍고 시동 끄고 사색에 잠기곤 했다.

사용자 삽입 이미지

산을 하나 넘어서 내려오니 또 강이 펼쳐졌다. 이 강은 온통 흙탕물이었다.

사용자 삽입 이미지사용자 삽입 이미지

그리고 영동선 철길 위를 지나갔다. 참 공교롭게도 본인이 아래를 내려다보고 있는 타이밍에 맞춰서 여객도 아니고 화물 열차가 하나 지나갔다. 얼마나 무거운 짐을 끄는지, 전기 기관차 중련에다가 보조 중형 디젤 기관차까지 편성한 상태였다.

사용자 삽입 이미지

드디어 양원 역이 자리잡고 있다는 어느 마을 어귀에 도착했다. 마을 앞에도 맑은 물이 졸졸 흐르고 있었다.
양원 역은 행정구역상 봉화이지만 동쪽 맨 끝이기 때문에 거의 울진 근처이며, 가는 길에 울진의 서쪽 끝을 경유하기도 했다. 봉화 시가지와는 수십 km 이상 떨어져 있었다.

사용자 삽입 이미지

그런데 마을에 도착했다고 끝이 아니었다. 양원 역으로 가려면 거기서도 산을 하나 타넘어야 했다~!
엔진 회전수 4000rpm을 고속도로에서 고속으로 밟을 때도 찍고, 여기서 2단 엔진 브레이크로도 찍었다. 1단 말고 2단으로 말이다.
그렇게 산을 넘은 뒤에도 저 길로 들어가야 했는데.. 차가 지나갈 수는 있지만 거기서 주민에게 민폐를 끼치지 않고 딱히 차를 세울 만한 공간이 없었다. 그래서 차는 이 부근에서 세우고 여기서부터 몇백 m 남짓은 걸어가야 했다.

사용자 삽입 이미지사용자 삽입 이미지

지도를 보니 이 강은 낙동강 상류라고 한다. 물이 콸콸 세차게 흐르고 있었다. 저 교각은 무슨 옛날에 있었던 다리의 흔적 같다.
양원 역이 이 정도로 답 없는 오지에 있는지는 미처 몰랐다. 저기를 건너간 다음에야..

사용자 삽입 이미지

드디어 얘를 실물로 보게 되었다.
예전에 태백선· 함백선 부근에서 조동 역과 함백 역을 보던 느낌이었다.

사용자 삽입 이미지

양원 역은 전국에서 유일하게.. 지역 주민들이 교통이 너무 불편해서 현기증이 나니 제발 열차 좀 정차시켜 달라고 아우성 치고 정부에 청원 넣고, 아무 국고 지원 없이 스스로 돈을 모아서 역 건물을 지어서 승인 받은 역이다. 진정한 의미의 민자역사인 셈이다. 단지, 자본주의 논리가 아니라 지역 주민 복지를 위한 민자역사인 것이고.. 그게 지금으로부터 무려 30년 전의 일이다.
전날 옥천에서 정 지용 시인 관련 안내문에서도 1988이라는 숫자를 봤는데, 양원 역의 개업 시기도 1988년 4월이라니 우연 치고는 절묘하다. (지용회: 1988년 3월)

얼마나 한이 서렸으면 양원 역에 첫 열차가 정차하던 날 사람도 감격하고 산과 강도 감격했댄다.
옛날엔 열차가 여기 마을을 통과할 때 짐보따리부터 미리 던져 놓은 뒤 더 먼 승부 역에서 여기까지 걸어서 돌아와서 그 짐을 챙겼다니..

지금으로서는 상상하기 어렵겠지만, 옛날에는 열차에 화장실이 비산식이었고(오물이 선로로 그대로...; ), 주행 중일 때 차량의 출입문이나 창문을 수동으로 조작할 수 있었다.
그럼 근성 있는 사람이라면 짐만 미리 던져 놓는 게 아니라, 자기 자신까지 훌쩍 뛰어내릴 법도 해 보이는데.. 그러기에는 아무리 젊고 민첩한 사람이라도 좀 위험했을 것 같다.

사용자 삽입 이미지사용자 삽입 이미지

영락없이 컨테이너 가건물 같은 규모이지만 컨테이너가 아니다. 시멘트를 얹어서 정식으로 지은 건축물이다.

사용자 삽입 이미지사용자 삽입 이미지

이 궁서체 안내판까지도 주민들이 직접 만든 거라고 한다.
현재는 여기가 유명세를 타면서 정규 여객열차뿐만 아니라 V-train(백두대간 협곡)과 O-train(중부내륙 순환)이라는 관광 열차도 정차하는 곳이 되었다. 그러니 역 건물과 안내판 말고 저 승강장은 코레일에서 만들었다는 티가 난다.
순환선이기 때문에 O이고, 그리고 계곡 모양을 형상화해서 V라니.. 코레일 수뇌부에서 머리 좀 쓴 것 같다. 이름을 참 기발하게 지었다.

사진을 찍지는 못했지만 마침 저기에 열차가 정차하는 것을 봤다.
이번 여행 동안 경부선과 영동선을 지나는 열차를 종종 목격했는데, 모두 전기 기관차 기반이었다. 디젤 기관차는 전혀 보지 못했다.

사용자 삽입 이미지사용자 삽입 이미지
사용자 삽입 이미지

서쪽의 봉화 시가지로 가는 길에 또 절경을 발견하여 풍경 사진을 찍었다.

사용자 삽입 이미지

그리고 오후가 돼서야 봉화 시가지에 도착했다. 이 강은 낙동강의 지류인 내성천이다.
여기서 식사를 하고 보급을 받는 것으로 2일간의 여행에 종지부를 찍었다.
올해도 어김없이 정말 즐거운 시간을 보냈다. 앞으로는 3년 주기로 남해안, 서해안, 동해안을 한 번씩 가는 것으로 어렴풋이 계획을 잡았다.

Posted by 사무엘

2019/09/13 08:33 2019/09/13 08:33
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1661

Windows용 GUI 프로그램은 가시적인 GUI 구성요소인 윈도우/창이라는 걸 생성해서 화면에 띄울 수 있다.
윈도우는 메뉴, 제목 표시줄, 테두리 같은 '껍데기' non-client 영역과, 그 내부에 프로그램이 재량껏 내용을 표시하는 '알맹이' client 영역으로 나뉜다. 윈도우에다 스타일을 무엇을 주느냐에 따라 non-client 영역의 구성요소와 차지 면적이 달라진다. 똑같이 테두리를 주더라도 크기 조절이 가능한 창은 그렇지 않은 창보다 테두리가 더 두껍게 그려지곤 한다.

GetWindowRect 함수는 어떤 창이 화면에서 문자 그대로 차지하는 스크린 좌표(non-client와 client 모두 포함)를 되돌리며, GetClientRect는 창 내부의 클라이언트 영역의 크기를 되돌린다.
후자의 경우 RECT를 되돌리긴 하지만 left와 top 멤버의 값은 언제나 0으로 설정된다. 그러니 얘는 진짜 RECT라기보다는 SIZE의 성격에 더 가깝다. 하지만 창의 client 영역을 원점(0,0) 기준으로 DC에다 그림을 그릴 일이 많으며 GDI 함수 중에도 RECT를 받는 놈이 있다. 그렇기 때문에 저런 형태의 RECT를 받는 건 여러 모로 유용하다.

어떤 창의 클라이언트 영역이 자기 창의 non-client 영역을 기준으로 얼마나 떨어져 있는지, 그리고 원점 또는 전체 스크린 좌표 기준으로 어디에서 시작하는지를 얻기란 쉽지 않다.
또한, 어떤 child 윈도우가 부모 창의 클라이언트 영역 좌표를 기준으로 어디쯤에 있는지를 얻는 것도 바로 가능하지 않다(예: 대화상자의 어떤 컨트롤이 대화상자를 기준으로 어느 위치에 있는가?). 기준이 되는 창들을 모두 스크린 좌표로 얻어 온 뒤에 수동으로 좌표 오프셋 보정을 해야 답을 구할 수 있다.

뭐 이런 식이다.
크기와 스타일이 동일한 창에 대해서 전체 영역 대비 클라이언트 영역이 위치와 크기가 어떻게 결정될 것인지는 얼추 고정적으로 예상 가능하다.
그런데 이렇게 직관적이고 명백해 보이는 영역에도 뭔가 골치 아프고 지저분한 가상화, 보정, 샌드박스라는 게 존재한다는 게 믿어지시는가?

이 문제는 지금으로부터 10수 년 전, Windows Vista가 등장하면서 시작됐다.
과거의 XP가 단조로운 고전 테마를 탈피하여 non-client 영역과 각종 표준 컨트롤에 임의의 테마 비트맵을 도입했는데, Vista/7은 더 나아가서 그래픽 가속 기능의 도움으로 프로그램 창 주변에 짙은 그림자를 그리고, 테두리에는 뭔가 청명한 느낌이 나는 반투명(?) 유리 재질을 구현했다.

그래서 법적으로(?) 똑같은 크기의 창이라도 창이 화면에다 실제로 1픽셀이라도 그림을 그리는 영역이 더 커졌으며, 테두리의 두께도 더 두꺼워졌다.
특히, 크기 조절이 되지 않는 창이라도 "크기 조절이 되는 창과 동급으로" 테두리가 두꺼워졌다.
이로 인해 window rect라는 개념을 해석하는 방식에도 변화가 불가피해졌다.

그럼 골치 아픈 설명 대신 예를 들도록 하겠다.
대화상자를 생성하여 (1, 1) 위치에다 300, 200픽셀 크기로 배치(SetWindowPos 호출)하는 프로그램을 돌려 보았다.
이걸 Visual C++ 2010 이하, 또는 2012 이상이라도 Windows XP 호환 툴체인으로 빌드하면 실행 결과가 다음과 같이 나온다.

사용자 삽입 이미지

그러나 동일한 코드를 Visual C++ 2012 이상의 정규 툴체인으로 빌드하면 실행 결과가 다음과 같다. 둘의 차이가 뭘까?

사용자 삽입 이미지

옛날 방식으로 빌드하면.. window rect를 계산할 때 Windows Vista/7의 Aero가 추가적으로 그려 주는 껍데기 공간이 영역에 포함되지 않는다. 그래서 window rect가 음수가 아니라 분명 (1, 1)임에도 불구하고 창의 좌측 상단이 좀 짤린다.
이 창이 실제로 차지하는 영역을 얻으려면 DwmGetWindowAttribute(m_hWnd, DWMWA_EXTENDED_FRAME_BOUNDS, &rc, sizeof(rc))를 호출해 줘야 한다. 보다시피 실제 영역은 (-4, -4)에서부터 시작하면서 5픽셀이나 더 크다는 걸 알 수 있다.

그러나 최신 방식으로 빌드하면 창의 모든 영역이 window rect에 포함된다. 이 때문에 창의 크기가 더 작아지며, 클라이언트 영역의 크기도 10픽셀씩이나 차이가 나게 된다. 그 대신 얘는 GetWindowRect와 DwmGetWindowAttribute의 결과가 서로 일치한다.
두 프로그램의 실행 결과를 한 화면에 포개 놓으면 다음과 같다.

사용자 삽입 이미지

이런 차이를 만들어 내는 것은 바로.. 실행 파일의 PE 헤더에 기록되어 있는 운영체제/서브시스템의 버전이다. 링커에서 /SUBSYSTEM:WINDOWS,6.0 옵션으로 지정 가능하다.
Visual C++ 2010의 컴파일러는 5.1 (XP)이 최저값이기 때문에 필요한 경우 6.0 (Vista)으로 올려서 지정할 수 있다. 그러나 2012 이상 후대의 컴파일러는 선택의 여지 없이 6.0이 최소값이며 더 낮은 값으로 지정이 되지 않는다. 툴체인 자체를 더 낮은 것으로 골라야 한다.

의외로 매니페스트 XML이 아니라는 게 신기하다. 공용 컨트롤 6.0 사용 여부, 고해상도 DPI 인식 여부, 요구 보안 등급 같은 건 매니페스트 속성이니까 말이다.
당연한 얘기이지만 공용 컨트롤 6.0과 최소 운영체제 6.0은 서로 다른 개념이라는 걸 유의하도록 하자. 공용 컨트롤의 버전은 차라리 과거 IE의 버전(6)과 비슷한 구도이다. 일개 웹브라우저가 운영체제의 셸까지 마개조하던 시절의 잔재이다(DLL hell도 존재했었고..).

그래도 현재의 Visual C++ 2019도 최소 운영체제 버전이 6.0에서 값이 더 오르지는 않고 있다. Vista를 진짜 하한선 마지노 선으로 잡고 있는 듯하다.
그럼 다음으로 Aero를 끄고 Basic 테마에서 두 프로그램을 실행한 결과는 다음과 같다.

사용자 삽입 이미지사용자 삽입 이미지

desktop window manager가 동작하지 않으니 DwmGetWindowAttribute 함수는 실행이 실패했다.
두 경우 모두 프로그램의 외형은 짤리는 것 없이 동일하고, 테두리의 두께만이 차이가 날 뿐이다. 최신 방식으로 빌드된 프로그램은 더 두껍고, 그렇지 않은 레거시 프로그램은 얇다.

단, Aero의 동작 여부와 무관하게 최신 방식과 레거시 방식이 클라이언트 영역 크기는 동일하다는 걸 알 수 있다. 전자는 테두리가 두꺼우니 클라이언트 영역이 (284, 162)로 더 작고, 후자는 (294, 172)로 더 크다.

사용자 삽입 이미지사용자 삽입 이미지

고전 테마는 Windows 8~10에서는 더 찾아볼 수도 없는 모드가 됐는데.. 얘는 빌드 방식에 따른 차이가 전혀 존재하지 않는다. DwmGetWindowAttribute도 당연히 동작하지 않고 말이다.
단지, 얘는 크기 조절 가능한 창에 대해서는 테두리의 두께가 1픽셀.. 눈꼽만치 더 늘어난다. 그래서 창의 크기가 같을 때 클라이언트 영역의 크기가 (294, 175)에서 (292, 173)으로 2픽셀씩 더 줄어드는 걸 알 수 있다.

사실은, 대화상자를 크기 조절 가능하게 만들면 6.0이 아닌 옛날 방식으로 빌드된 프로그램이라도 앞의 Aero 내지 Basic 테마에서 창이 최신 6.0 버전이 지정된 프로그램과 동일하게 동작한다.
리소스 편집기에서 대화상자의 Border 속성을 Dialog Frame 말고 Resizing으로 지정했을 때 말이다.

그럼 마지막으로 Windows의 종결자 버전인 10에서는 창이 어떻게 뜨는지 살펴보자.
Windows 10의 프로그램 창은 외관상으로 Vista/7 같은 두툼한 테두리가 없고 슬림하다. 그럼에도 불구하고 10에서도 6.0이 지정된 프로그램은 클라이언트 영역의 크기가 레거시 방식 프로그램보다 더 작게 설정된다. 다시 말하지만 GetWindowRect의 결과는 둘 다 동일한데... 이렇게 동작해야 할 이유가 없어 보이는데도 말이다.

사용자 삽입 이미지사용자 삽입 이미지

더욱 괴이한 것은 DwmGetWindowAttribute의 실행 결과이다.
나름 DWM은 동작하고 있는지 이 함수도 값을 되돌리기는 하는데... 보다시피 리턴된 사각형이 GetWindowRect의 리턴값보다도 더 작다~! Vista/7의 Aero와는 결과가 딴판이다.
최신 6.0 방식으로 빌드된 프로그램은 (1, 1) 위치에서 (300, 200) 픽셀 크기를 찍더라도 실제로 생기는 창의 크기는 (8, 1)에서 (286, 193) 픽셀 크기 남짓으로 예상보다 꽤 작게 된다.

지금까지 나열한 실험 결과를 표로 한데 요약하면 다음과 같다. 96dpi (100%) 확대 배율에서 굴림 아니면 맑은 고딕의 완전 기본 metric 기준이다.
GetWindowRect의 값이 다 똑같이 [1,1, 301,201]인 윈도우이더라도 실제 외형과 클라이언트 영역 크기는 운영체제와 빌드 방식에 따라 저렇게 차이가 날 수 있다는 것이다.

  레거시 (2010/XP) 신형 (2012+/Vista+)
DWM Attribute client 크기 DWM Attribute client 크기
Vista/7 Aero [-4,-4, 306,206] [294, 172] [1,1, 301,201] [284, 162]
Vista/7 Basic N/A [294, 172] N/A [284, 162]
Classic N/A [294, 175] N/A [294, 175]*
Windows 10 [3,1, 299,199] [294, 171] [8,1, 294,194] [284, 161]

* resizable하지 않은 대화상자의 경우, 고전 테마에서는 레거시 및 신형 방식 모두 대화상자 외형 차이가 없다. 단지, resizable한 대화상자는 프레임 두께가 1픽셀 더 두꺼워져서 클라이언트 영역이 2픽셀씩 줄어든다.
그리고 고전 이외의 다른 테마에서 resizable한 대화상자는 레거시 방식도 신형 방식과 동일한 결과가 나온다.

그러니, 똑같은 소스 코드이더라도 최신 Visual C++ 컴파일러로 다시 빌드를 하면 예전보다 창 크기가 더 작게 나와서 UI 구성 요소의 오른쪽/아래쪽이 짤리는 부작용이 발생할 수 있다. 그런 상황이 발생하면 놀라지 말고 SetWindowPos 같은 함수에 숫자를 무식하게 하드코딩 하지 않았는지를 침착하게 점검해 보기 바란다.

옛날에는 SetWindowPos, GetClientRect 같은 함수의 msdn 설명을 보면 Windows Vista에서 보정으로 인한 동작 차이가 발생할 수 있다는 말이 '비고'란에 분명 있었다. DWM 쪽의 API를 참고하라는 말도 봤었는데 2010년대 이후부터는 삭제된 듯하다. 지금은 그런 말을 찾을 수 없다.

하지만 그런 말이 있건 없건, 창의 크기를 지정할 때는.. non-client 요소가 일체 없고 픽셀값을 반드시 곧이곧대로 지정해야만 하는 불가피한 상황이 아니라면 어지간해서는 SetWindowPos에다가 하드코딩된 값을 무식하게 전하는 일이 없어야 한다.
이 글에서 소개한 non-client 영역 크기 보정도 있고, 화면 확대 배율(DPI)도 있고.. 하드코딩 픽셀값을 쓸 수 없게 만드는 요인은 여럿 존재한다. 잘 만들어진 프로그램이라면 창 크기를 계산할 때 그런 변수들을 잘 감안해야 한다.

가령, 대화상자 리소스 템플릿에서 사용되는 단위인 DLU를 픽셀수로 보정하려면 MapDialogRect을 사용하면 된다. 이것만으로도 화면 확대 배율은 커버 가능하다.
그리고 특정 클라이언트 영역의 픽셀수와 정확하게 대응하는 window rect 크기를 구하려면 AdjustWindowRect(Ex)를 사용하면 된다.

Posted by 사무엘

2019/05/27 08:34 2019/05/27 08:34
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1624

Windows 운영체제에서 쓰이는 EXE나 DLL 같은 실행 파일들은 잘 알다시피 Portable executable이라는 포맷을 따라 만들어져 있다. 그래도 맨 앞에는 MZ로 시작하는 MS-DOS EXE 헤더가 호환성 차원에서 여전히 있으며, 거기서 가리키는 오프셋을 따라가 보면 그제서야 PE 헤더가 시작된다.

그런데 아무 EXE/DLL이나 찍어서 바이너리 에디터로 들여다 보시라. 도스 EXE stub과 실제 Windows용 실행 파일의 사이 공간에는 뭔가 4바이트 간격으로 규칙성이 느껴지는 정체불명의 데이터가 수십~수백 바이트가량 있으며, 끝에는 약속이나 한 듯 "Rich"라는 문자열이 있다. 이건 도스/Windows 어느 플랫폼에서도 쓰이지 않는 잉여 데이터이다.
이놈의 정체는 도대체 무엇일까...??

사용자 삽입 이미지

더욱 괴이한 것은.. 이건 Visual Basic, Visual C++ 같은 마소의 개발툴(링커)로 생성한 바이너리에만 존재한다는 것이다.
게다가 마소 개발툴조차도 처음부터 이랬던 게 아니다. Visual C++의 경우, 5.0의 마지막 서비스 팩(아마 3)부터 적용된 거라고 한다. 그러니 지금으로부터 20여 년 전, 대략 1997~98년 사이부터이고.. 그냥 편의상 6.0부터라고 생각해도 무방하다.

본인의 고딩 시절, Visual C++ 4.2로 빌드된 날개셋 한글 입력기 1.0 내지 PentaCombat 오목 게임의 실행 파일을 들여다보면.. 아니나다를까 일명 "Rich 헤더"가 존재하지 않는다. MZ와 PE 사이에 아무 공백이 없다.

사용자 삽입 이미지

그러나 VC++ 6으로 빌드된 날개셋 1.2부터는 응당 Rich 헤더가 추가되어 있다.

과거 Windows 9x의 경우, 내부의 프로그램들을 빌드할 때 Visual C++이 아닌 다른 자체 컴파일러를 사용했다. kernel32, gdi32, user32 같은 DLL이라든가 메모장 같은 간단한 프로그램 말이다.
여기서 유래된 바이너리들은 Rich 헤더가 존재하지 않으며, 이 관행은 Visual C++ 6이 출시된 뒤에도 Windows 98/ME까지 변함없이 이어졌다.

하지만 내장 프로그램 중에서 워드패드와 그림판, EUDC 편집기처럼 Program Files\Accessories에 들어있던 프로그램은 MFC도 사용하고 나름 Visual C++ 냄새가 났었다.
얘들은 Windows 95 시절엔 대외적으로 공개된 적이 없는 MFC 짝퉁을 사용하다가, Windows 98부터 깔끔하게 Visual C++ 5 sp3으로 빌드되기 시작했다. 그래서 얘들은 예외적으로 Rich 헤더가 포함되기 시작했다.

이런 9x와 달리 Windows NT는 운영체제 차원에서 처음부터 Visual C++ 팀과 잘 연계하는 편이었다.
9x 계열은 98에 와서야 msvcrt와 mfc42같은 Visual C++ 출신의 배포용 DLL들이 최초로 정식 포함돼 들어간 반면, NT 계열은 처음부터 메모장도 진작부터 msvcrt를 사용해 왔다. NT4는 직접 확인해 보지 않아서 모르겠지만, 2000은 모든 EXE/DLL의 내부에 Rich 헤더가 존재한다.

한편, Visual C++ 말고 델파이 같은 타 개발툴로 빌드된 실행 파일에는 Rich 헤더 같은 건 당연히 존재하지 않는다.

사용자 삽입 이미지

그럼 본론으로 들어가도록 하겠다.
마소에서는 이 헤더? 데이터? chunk?를 왜 집어넣기 시작했으며, 이 정보의 의미는 도대체 무엇일까? 엄밀히 말하면 헤더라고도 볼 수 없는 단순 데이터일 뿐인데 말이다.
놀랍게도 마소에서는 이에 대해서 지금까지 공식적인 답변을 한 번도 제공하지 않았으며 undocumented, 묵묵부답으로 일관한 듯하다. 빌드 시에 이 헤더를 제외시키는 옵션도 없다.

그래서 일각에서는 흉흉한 음모론까지 나돌기 시작했다. 제일 유명한 게 뭐냐 하면, 이건 이 바이너리를 빌드한 컴퓨터 환경을 식별하는 정보라는 것이다.
그래서 이 exe/dll이 악성 코드로 밝혀져서 경찰에 수사를 의뢰하게 되면.. 이 정보로부터 개발자의 컴퓨터를 추적할 수 있고, 따라서 악성 코드를 만든 사람도 아무 단서가 없는 것보다는 더 용이하게 색출할 수 있다고 한다..;;

이거 마치 컬러 복사기 얘기처럼 들린다. 컬러 복사기의 결과물에는 아주 정교한 워터마크가 사람 눈에 안 띄게 몰래 새겨진댄다. 그래서 어설픈 컬러 복사 위조지폐가 발견되면 그 워터마크를 토대로 복사기의 일련번호를 추적할 수 있으며, 이를 통해 범인도 색출할 수 있다고 한다. 허나 본인은 그런 게 실제로 존재한다고 믿지는 않는다.

뭐, 복사기는 그렇다 치고.. 실행 파일의 경우, 상식적으로 생각해 봐도 저건 개발 컴퓨터의 색출 목적으로 사용하기에는 보안이 너무 허술하다.
진짜 나쁜 마음 품은 악성 코드 개발자라면.. 그 Rich 헤더 부분을 후처리로 몽땅 0으로 칠해서 일부러 지워 버리기만 해도 자기 정체를 숨길 수 있으며, 그래도 악성 코드의 동작에는 하등 문제될 것 없다.

이 경우 PE 헤더의 다른 필드에 존재하는 checksum 정보가 어긋나서 파일이 변조되었다는 것이 감지되겠지만, 이것도 일부러 기재하지 않았다고 0을 집어넣어 버리면 그만이다. 더구나 보안을 위해 소프트웨어에서 이미 있던 이스터 에그도 다 없앴고 요즘은 바이너리 차원에서 철저히 예측 가능한 reproducible build까지 추구하는 마당에, 이런 식의 비밀 식별 정보는 마소의 개발 방침 이념과도 어울리지 않는다.

그러니 프로그램을 빌드할 때마다 내 컴퓨터의 맥 어드레스가 유출된다는 식으로 불안해할 필요는 없다. 하지만 Rich 헤더의 정체는 여전히 베일에 싸여 있었다.
그래서 전세계의 많은 컴덕과 해커들이 의문을 품기 시작했으며, 어떤 용자는 MS에서 개발한 링커 프로그램을 아예 근성으로 리버스 엔지니어링까지 했다. 그래서 이 데이터에 대한 여러 사실들을 밝혀냈다.

가장 먼저.. Rich 헤더는 4바이트 덩어리 단위로

A B B B X1 Y1 X2 Y2 ... "Rich" B

대체로 요런 형태로 돼 있다.
끝의 Rich 다음에 나오는 마지막 double word인 B가 일종의 난수이며, 암호화 key이다. 그리고 Rich 앞에 있는 숫자들은 바로 그 B와 xor을 해 주면 실제값을 얻을 수 있다.

그러면 맨 첫째 값 A는 언제나 0x44 0x61 0x6E 0x53.. 문자 형태로 늘어놓으면 "DanS"라는 시그니처가 된다.
다음으로 시그니처 뒤에 이어지는 몇 개의 B는 16바이트 단위 padding을 맞추기 위한 0값인 것 같다. 자기 자신을 xor 하면 결과는 언제나 0이 되니 말이다.

그 뒤 이어지는 같은 값들은 숫자 2개가 X, Y 형태로 한 pair를 이룬다. X는 이 바이너리를 생성하는 데 쓰인 툴(C 컴파일러, C++ 컴파일러, 리소스 컴파일러, 어셈블러 등..)과 버전(빌드 번호)을 나타내며, Y는 그 도구를 이용하여 생성된 아이템.. 이를테면 obj 파일의 개수를 나타낸다고 한다.

그럼 이런 정보들은 링커가 어디에서 얻어서 집어넣는가 하면.. 당연히 자기가 input으로 받아들이는 obj 파일로부터이다. 그렇잖아도 1997~98년을 전후해서 obj 파일 포맷이 바뀌었다고 한다.
obj야 소스 코드를 번역한 기계어 코드의 뭉치일 뿐이니 20년 전이나 지금이나 컴퓨터 아키텍처가 변함없는 한 바뀔 일이 없으며, 내부 구조가 바뀌어 봤자 새 버전의 컴파일러/링커가 인식하는 새로운 chunk 정도나 추가되는 게 전부일 것 같은데.. 꼭 그렇지는 않은 모양이다.

하긴, 같은 양의 코드를 빌드해도 요즘 컴파일러는 예전에 비해 obj의 파일 크기가 점점 더 커지고 있어 보이긴 하다. 디버깅 또는 최적화와 관련된 온갖 힌트와 메타정보들이 첨가되어서 그런 것 같다.
그래도 obj는 소스 코드를 빌드할 때마다 새로 생성되는 일회용 임시 파일일 뿐이니, 포맷에 하위 호환성 걱정 따위는 할 필요가 없을 것이다. 날려 봤자 빌드만 다시 하면 될 일이고..

아무튼 흥미로운 사실을 알게 됐다.
개인 정보 유출은 아니고, 그렇다고 디버깅과 관련된 힌트도 아니고 저런 정보가 마소의 개발툴에서 도대체 왜 들어갔는지는 여전히 오리무중이다만.. 그래도 내역을 까맣게 모르던 시절보다는 상황이 나아졌다.
본인이 이 글을 쓰기 위해 검색하고 참고한 외국 사이트는 다음과 같으니,  더 자세한 관심이 있는 분은 참고하시기 바란다.

The devil’s in the Rich header
Microsoft's Rich Signature (undocumented)
The Undocumented Microsoft "Rich" Header
Article: Things They Didn't Tell You About MS LINK and the PE Header

저 외국 사이트들의 설명에 따르면, Rich와 DanS는 모두 1990년대에 마소의 Visual C++ 팀에서 근무하고 이 데이터의 구현에 직접 관여했던 프로그래머의 이름에서 유래되었을 가능성이 높다고 한다. 그 이름은 각각 Richard Shupak와 Dan Spalding이다. ㄷㄷㄷ;;
MZ, PK만큼이나 프로그래머의 이름이 파일 포맷에 각인된 사례라 하겠다.

참고로 전자 리처드의 경우, Windows SDK에서 psapi.h의 작성자로 이름이 나와 있기도 하다.
PSAPI는 Windows NT 계열용으로 실행 중인 프로세스 정보를 조회하는 EnumProcesses, GetModuleFileNameEx 등의 함수를 정의하는 라이브러리인데, 작성 날짜가 무려 1994년이라고 적혀 있다.

Posted by 사무엘

2019/05/21 08:31 2019/05/21 08:31
,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1621

1.
먼 옛날에 기술이란 게 지금보다 비싸고 희귀하던 시절에는 컴파일러 자체가 유료화 대상이었다. Windows 플랫폼 SDK에 같이 들어있는 무료 기본 컴파일러는 상업용으로 따로 판매되는 Visual C++의 컴파일러와 같은 빡센 최적화 기능이 없었다. 사용자가 작성한 코드대로 돌아가는 실행 파일은 만들어 주지만 최적의 성능을 발휘하는 형태로 만들지는 않았다는 것이다.

그러다가 Visual C++ 2005 Express를 기점으로 고성능 컴파일러도 무료화되고 기본 제공되기 시작했다.
요즘 컴파일러의 최적화 테크닉은 학교에서 배우는 것처럼 (1) 쓰이지 않는 변수나 코드 제거, (2) 값이 뻔한 수식은 한 번만 미리 계산해 놓고 loop 밖으로 옮기기, (3) 변수는 적절하게 레지스터 등재, (4) 함수는 적절하게 인라이닝, (5) 최대한 병렬화, (6) CPU 명령 하나로 처리 가능한 단위로 묶기, (7) switch 분기를 매번 번거로운 if문이 아니라 테이블 참고 방식으로 변경... 같은 미시적인 것만이 그림의 전부가 아니다.

그야말로 번역 단위(소스 코드) 간의 경계를 넘나들고 컴파일러와 링커의 역할까지 재정립하면서까지 저런 최적화 테크닉을 적용할 부위를 판단할 필요가 있다. 그래서 컴파일러에는 전역 최적화라는 옵션이 도입되고, 링커에는 link time code generation이라는 옵션이 추가되어 서로 연계한다. 뭐, 대단한 기능이긴 하지만 이 정도로 최적화를 쥐어짜서 성능이 더 나아진 것 대비 프로그램의 빌드 타임이 너무 길어지는 건 별로 마음에 안 든다.

그리고 세상에 C++ 컴파일러가 MS 것만 있는 것도 아니고, 경쟁사들의 제품도 갈수록 성능이 좋아지고 가격이 저렴해지고 있으니 optimizing compiler 정도는 충분히 대중적인 영역으로 내려가게 됐다.

2.
한편, Visual Studio Express 에디션은 기업에서 사용이나 상업용 소프트웨어 개발까지 아무 제약 없는 무료인 대신, 리소스 편집기와 MFC 라이브러리가 없다. 지금도 그런지는 모르겠지만 처음엔 심지어 64비트 빌드 기능도 없었다.
그 뒤 VC++ 2013부터는 MFC와 리소스 편집기가 다 있는 Community 에디션이란 게 나왔다. 얘는 기능 제약이 없는 대신 개인 개발자나 스타트업 중소기업 수준까지만(인원 얼마 이하, 연 매출 얼마 이하..) 무료이다.

서로 조건이 다른 무료이기 때문에 VC++ 2013, 2015, 2017까지는 한 버전에 대해서 express와 community 에디션이 모두 나왔다. 하지만 express는 장기적으로는 community로 흡수되고 없어질 것으로 보인다.

3.
이렇게 프로그램을 기본적으로 만들고 빌드하는 데 필요한 도구들은 슬슬 무료화 단계에 들어섰고, 그 다음으로는 그냥 빌드만 하는 게 아니라 제품의 품질을 높이는 데 도움을 주는 도구, 대규모 공동 작업과 테스트를 위해 필요한 전문 도구들이 아직 유료이다. 어떤 것은 Visual Studio의 엔터프라이즈 같은 제일 비싼 에디션에서만 제공된다.

Visual C++ 2008에서는 GUI 툴킷이 feature pack을 통해 무료로 풀렸더니 2012부터는 정적 분석 도구가 무료로 풀렸다. 이걸 처음 써 본 소감은 꽤 강렬했다.

사실, 본인도 그저 닥치는 대로 코딩과 디버깅만 하는 것 말고 소프트웨어공학적인 개발 프로세스라든가 기술 문서 잘 쓰는 요령, 변수와 함수의 이름 잘 짓는 요령, 전문적인 테스트 절차와 프로파일링 같은 것을 잘 알지 못한다. 더 발전하려면 그저 무료로 풀려 있는 도구들만 쓰는 게 아니라 그 너머에 있는 도구들도 뭔지 알고 필요성을 공감할 정도는 돼야 할 텐데..

그렇잖아도 날개셋 한글 입력기도 어떤 형태로든 버그가 없었던 적은 거의 없었다. 그리고 개인 프로젝트뿐만 아니라 회사 업무도 늘 깔끔하게 마무리 짓지는 못해서 버그가 있는 채로 제품을 내고 후회한 적이 적지 않았다.
만들어진 제품의 품질을 검증하는 절차 자체는 소프트웨어뿐만 아니라 건축· 건설을 포함해 공학의 어느 분야에서나 다 필요하고 존재할 것이다. 그 가운데에 무형의 지적 산물인 소프트웨어만이 갖는 특수성 때문에 이 바닥에서만 통용되는 방법론도 있겠지만 말이다.

아무튼, 프로그래밍 툴의 제작사들이 흙 파서 장사하는 건 아닐 테니.. 이런 어마어마한 컴파일러쯤은 딴 데서 개발 비용을 회수할 통로가 다 있으니까 무료로 풀어 놓는 게 가능할 것이다.

4.
(1) 이렇듯, Visual Studio를 포함해 개발툴들은 갈수록 기능이 좋아지고 기술들이 상향평준화되고 있다. 일례로, 2017인가 2019부터는 코드 편집기에 일반 컴파일러의 경고/에러뿐만 아니라 정적 분석 결과까지 초록색 밑줄로 띄워 주는 걸 보고 무척 감탄했다. (NULL 포인터 역참조가 일어날 수 있습니다 따위..)

(2) 2017/2019부터는 도움말과 API 레퍼런스는 몽땅 인터넷으로 처리하고 로컬 오프라인용 Help Viewer를 더 관리하지 않으려는지? 설치 화면에서는 기본 선택돼 있지도 않으며, 컨텐츠 역시 2015 내용 이후로 달라진 게 없어 보인다.

(3) 그리고 Spy++의 64비트 버전은 왜 도구 메뉴에서 숨겨 버린 걸까?
한 프로그램만으로 32비트와 64비트를 통합해서 잘 동작하는 것도 아니고, "이 창의 메시지를 들여다보려면 64비트 Spy++를 실행해 주십시오" 안내를 해 주는 것도 아닌데 왜 이런 조치를 취했는지 이해할 수 없다. 사실은 32와 64비트를 가리지 않고 아주 seamless하게 동작하는 Visual Studio의 디버거가 정말 예술적인 경지의 대단한 도구이긴 하다.

(4) 시대 유행과 별 관계 없는 부분은 계속해서 안 바뀌기도 하는 것 같다.
도구상자를 customize할 때 콤보 박스의 길이 조절을 200x 시절처럼 마우스 드래그로 할 수 없고.. 2010 이래로 그냥 픽셀수 입력으로 때운 걸로 굳히려는가 보다.

무려 7년 전 글에서 지적했던 2. 메뉴 편집기의 우클릭 버그, 3. 툴바 편집기의 화면 잔상 역시, Visual Studio 2019 현재까지도 전혀 고쳐진 게 없다.
그리고 예전엔 안 그랬던 것 같은데 2019는 Visual Studio 텍스트 에디터의 폰트를 딴 걸로 바꾸고 껐다가 다시 켜서 곧장 반영되는 걸 확인까지 했는데.. 켠 채로 절전 상태로 몇 번 갔다가 돌아오면 폰트가 컴터에 따라 아주 가끔은 돋움이나 Courier New 같은 기본 글꼴로 돌아오는 것 같다.

Posted by 사무엘

2019/05/10 08:36 2019/05/10 08:36
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1617

Windows의 Region

Windows API에는 region(영역)이라는 일종의 자료구조 라이브러리가 있다. 얘는 2차원 래스터(픽셀/비트맵) 그래픽 공간에서 각 픽셀별로 "영역에 포함되냐 안 되냐"라는 일종의 '집합'을 표현한다.
그리고 운영체제는 이 자료구조를 이용하여 각종 그래픽이 그려지는 영역을 정한다. 즉, region은 클리핑(clipping) 영역을 표현하는 데 쓰인다는 것이다.

도스 시절의 여느 그래픽 라이브러리에도 간단한 사각형 영역에만 그림이 그려지게 하는 초보적인 수준의 클리핑 기능은 있었다. 하지만 Windows의 region은 여러 사각형이 겹친 것, 임의의 다각형, 원 등 아무 모양이나 표현하고, 그 영역 안에만 그림이 그려지게 만들 수 있다.

그도 그럴 것이 Windows 같은 GUI 운영체제라면 창들의 Z-order 같은 걸 구현하는 과정에서 밥 먹고 맨날 하는 짓이 정교한 클리핑일 수밖에 없게 된다. 뒤쪽에 있는 창 내용은 앞쪽에 있는 창의 영역을 침범하지 않고 그려져야 하기 때문이다. 그러니 그런 기능을 사용자들에게도 쓰라고 제공해 주는 게 결코 이상한 일이 아니다.

region은 가장 먼저, (1) 직사각형, 타원, 모서리가 둥근 직사각형, 다각형(CreatePolygonRgn, CreatePolyPolygonRgn)처럼.. 속이 닫힌 도형을 그리는 다양한 API를 통해 생성할 수 있다. 당연히 그 도형의 모양이 영역의 모양이 된다.

다음으로, GDI가 제공하는 기능인 (2) path로부터 region을 생성할 수 있다(PathToRegion). path란 마치 윤곽선 글꼴 글립처럼 직선(MoveTo, LineTo)과 곡선(PolyBezirTo)을 임의로 조합하여 어떤 궤적이나 경계선을 기술하는 자료구조이다. region과 달리 벡터 기반이며, 별도의 자료구조로 존재하는 게 아니라 DC의 내부 상태에 종속인 형태로 보관된다는 차이가 있다.

path를 사용하면 경계선이 베지어 곡선인 region을 만들 수 있으며, TextOut 같은 글자 출력 함수를 path에다 넣으면 임의의 글자의 윤곽선도 따서 고스란히 region으로 만들 수 있다. 커다란 두 글자를 포개 놓은 뒤, 겹치는 영역만 다른 색깔로 칠하는 게 region으로는 가능하다. 그 비결은 바로...

사용자 삽입 이미지

(3) CombineRgn이라는 함수를 통해 region 간에 일종의 집합 연산을 할 수 있기 때문이다. 두 region의 교집합, 합집합, 차집합을 구함으로써 더 복잡한 형태의 region을 만들 수 있다.

위의 그림을 보아라. 운영체제나 하드웨어 차원에서 제공되는 layer이나 alpha channel 합성 같은 걸 쓴 게 아니다. 옅은 회색(B), 짙은 회색(S), 검정(겹침)이 차지하는 영역을 2차원적으로 완전히 따로 떼어내서 서로 완전히 다른 색깔과 무늬로 칠할 수 있다. 뚝 떨어진 영역도 당연히 같이 감안해서 말이다. 이런 게 평범한 글자 찍기 API로는 가능하지 않을 것이다.
다만, region은 anti-aliasing을 지원하지 않는 boolean 흑백 자료구조이다 보니, 글자 경계가 거친 것은 아쉬운 점이며 요즘 그래픽 기술의 트렌드와 맞지 않다.

그리고 끝으로.. region은 내부 자료구조를 어느 정도 노출해 주고 있기까지 하다. 그래서 그걸 직통으로 저장하고 불러오는 식으로 생성할 수도 있다. 데이터를 얻는 함수는 GetRegionData이고, (4) 그걸로부터 region을 다시 생성하는 함수는 ExtCreateRegion이다.

어떤 방식으로 region을 생성했건, 얘는 내부적으로 크게 세 부류로 나뉜다. region을 생성하거나 받아들이는 함수들이 그 region의 유형을 리턴값을 통해 알려주기도 한다.

  • 아무 영역도 없는 공집합인 NULLREGION
  • 직교좌표 직사각형 하나로만 구성된 SIMPLEREGION
  • 그 외의 다른 모든 모양을 표현하는 COMPLEXREGION.. 얘는 내부적으로 2개 이상의 사각형, 아니 scan line들로 구성된다.

자연에서 관찰되는 힘이라는 것들이 중력이나 원자력과 관계 있는 게 아니면 나머지는 출처가 몽땅 전자기력이듯이(폭발력, 마찰력, 탄성, 자석, 정전기, 표면장력, 생물 근육 등등등..),
그리고 사람이 생성(...)되는 방식이 흙을 빚어서 직통, 여자의 씨 같은 극소수 예외를 제외하면 나머지 수십~수백 억의 인간들은 몽땅 남자의 씨 기반이듯이.. 그런 것처럼 region도 일상생활에서 보는 단순하지 않은 물건들은 몽땅 complex라고 생각하면 되겠다.

region이 내부적으로 구현된 방식의 특성상(벡터/오브젝트 기반이 아닌 비트맵/픽셀 스캔라인 기반) 무한을 구현할 수 있지는 않으니, 집합 연산에서도 not 연산인 여집합이 지원되지는 않는 걸 볼 수 있다. 차라리 이미 있는 집합끼리 차집합이 대신 지원되고 말이다.

region을 식별하는 핸들 내지 포인터 자료형은 HRGN이다. 그런데 Create...처럼 HRGN을 리턴값으로 주는 함수 말고, Get...Rgn, CombineRgn 이런 이름이면서 HRGN을 인자로 받는 함수들은... 이미 있는 HRGN에다가 값만 바꿔서 넣어 준다. 그런 함수를 쓰려면 null region 하나라도 미리 미리 생성해서 전해 줘야 한다.

그런데 Windows API에는 많고 많은 region 생성 함수들 중에 null region만을 달랑 생성하는 함수는 의외로 없다. 좌표가 모두 0인 직사각형.. CreateRectRgn(0,0,0,0) 이게 그냥 텅 빈 region을 생성하는 역할을 한다. 좀 교묘한 점이다.

그럼 이 region는 어떤 용도로 쓰이며, 할 수 있는 일이 무엇일까? 본인이 생각하기에 다음과 같다.

1. 단독

그냥 저거 자체만으로 뭔가 2차원 공간 상의 기하/집합 알고리즘 구현체로.. 다른 GDI API와의 연계 없이, 심지어 명령 프롬프트용 프로그램에서도 쓰일 수 있다. 펜, 브러시, 글꼴, 비트맵 같은 타 오브젝트들이 DC와의 연계 없이는 거의 쓸모없는 것과 굉장히 대조적이다.
하지만 region이 그렇게 단독으로 쓰이는 경우는 그리 많지 않아 보인다.

2. 창의 invalid 영역 표현

어떤 창의 앞을 가리던 다른 창이 없어지고 내 창의 내용이 다시 그려지게 됐을 때.. 그 이름도 유명한 WM_PAINT 메시지가 날아온다.
BeginPaint와 함께 제공되는 DC는 창 전체가 아니라 정확하게 다시 그려져야 하는 영역에만 그림이 그려지도록 클리핑 처리가 돼 있는데, 이 영역이 말 그대로 region으로 표현되며 GetUpdateRgn 함수를 통해 얻어 올 수 있다.

WM_PAINT 때 이 영역에 대해서 PtInRegion이나 RectInRegion을 적절히 호출하면서 그림을 그리면, 무식하게 화면 전체를 그리는 것보다 프로그램 성능과 반응성을 향상시킬 수 있다.
물론 DC 차원에서 클리핑 처리가 되는 것만으로도 화면 전체를 그리는 것보다는 속도가 향상되지만, 애초에 그리기 요청을 안 하고 CPU 계산을 안 하는 게 더 낫기 때문이다.

3. 내부 클리핑

운영체제뿐만 아니라 사용자 역시 임의의 region을 생성해서 클리핑 용도로 쓸 수 있다. 비트맵이 사각형 모양이 아니라 원 모양으로만 뿌려진다거나, 특정 글자 모양으로만 뿌려지게 할 수 있다는 것이다.
그렇게 하려면 HRGN을 DC에다가 지정하면 된다. 이것은 SelectObject로 해도 되고 SelectClipRgn으로 해도 된다. 완전히 동일하다.
단지, 클리핑을 해제하는 것은 SelectClipRgn로만 가능하다. HRGN 값으로 NULL을 전해야 하기 때문이다. null region은 그림이 전혀 그려지지 않게 하는 효과를 낼 테니까..

HRGN은 기술적으로는 HPEN, HBRUSH, HBITMAP, HFONT와 마찬가지로 여러 GDI 오브젝트 중 하나로 취급된다. 상술한 바와 같이 DC에 SelectObject 될 수 있으며, 소멸 함수가 DeleteObject인 것까지도 동일하다.
하지만 얘는 다른 오브젝트들과 달리 select나 get 될 때 내부 메모리가 복사될 뿐, 핸들값 자체를 주고 받지는 않는다. 즉, HRGN은

HRGN oldRgn = (HRGN)SelectObject(dc, newRgn);
(.....)
SelectObject(dc, oldRgn);

이런 식으로 운용되지 않는다는 것이다. 옛날 핸들값을 보관하고 되돌리는 식의 절차가 필요하지 않다.
DC와 region은 서로 따로 논다. 이렇게 설정한 뒤에 원래 있던 HRGN 핸들은 곧장 삭제해 버려도 된다.

본인은 MFC의 CGdiObject처럼 GDI 객체 핸들만 한데 뭉뚱그린 템플릿 클래스를 만들어서 쓰고 있다. (소멸자에는 DeleteObject가 있고..)
그런데 다른 오브젝트들은 template<T> Handle(T v=NULL) 이런 식으로 NULL이 default 인자인 생성자를 만들어서 초기화할 수 있는 반면,
HRGN에 대해서는 인자가 없는 경우에 대해 specialize된 생성자를 따로 만들어서 이때는 null region을 생성해 놓게 했다. 그래야 이놈을 region을 얻어 오는 다른 함수에다가 인자로 줄 수 있기 때문이다.

4. 칠하고 그리는 공간

그리고 region 자체가 도형을 나타내니 그 모양대로 클리핑이 아니라 내부를 칠하는 용도로 응당 활용할 수 있다. 내부를 칠하는 FillRgn과, 내부의 경계선을 그려 주는 FrameRgn이라는 함수가 제공된다.
흥미로운 것은 경계선을 그릴 때도 pen 대신 brush가 쓰인다는 것이다. region은 path와 달리 벡터 드로잉이 아니기 때문이다. 경계선은 그냥 픽셀 차원에서 색깔이 변하는 곳을 얼추 감지해서 표시해 주는 것일 뿐이다.

5. 창 자체의 외형

끝으로, region은 윈도우의 모양을 지정하는 용도로도 쓰인다. 통상적인 사각형 모양이 아니라 리모콘 같은 다른 물건처럼 생긴 프로그램 창, 현란한 스플래시 윈도우, 내부에 구멍까지 있는 윈도우.. 전부 SetWindowRgn 함수의 산물이다.
한번 SetWindowRgn에다 전해 준 HRGN은 이제 운영체제가 관리하기 때문에 사용자가 DeleteObject 하지 말아야 한다고 문서에 거듭 명시되어 있다.

SetWindowRgn를 지정하는 순간부터 그 창은 운영체제가 non-client 영역과 테두리 경계에 기본 제공하는 각종 테마와 반투명 프레임, 둥그런 테두리, 그림자 효과들로부터 완전히 열외된다. 그 대신 고전 테마의 완전 무미건조한 테두리만이 그려진다. 일반적으로는 당연히 아래처럼 그려질 프로그램 창이 위처럼 그려지게 된다는 뜻이다.

사용자 삽입 이미지

그런 창은 어차피 non-client 영역이 전혀 없이 외형을 사용자가 완전히 customize하는 형태로 쓰일 테니 말이다. 제목 표시줄과 테두리의 외형은 보급품 그대로이면서 중앙에만 region을 지정해서 총알 구멍 같은 게 숭숭 뚫린 프로그램 창 같은 건 만들 수 없다는 뜻이다.

보통은.. 공용 컨트롤 6.0 매니페스트가 없는 프로그램의 경우, 버튼 같은 컨트롤들이 구닥다리 고전 스타일로 그려지고 non-client 영역은 운영체제가 자동으로 쌈빡하게 그려 준다. (그림에서 아래 오른쪽) 그런데 SetWindowRgn을 지정하면 반대로 컨트롤들은 정상적으로 그려지는데 non-client 영역이 고전 스타일로 돌아간다는 게 흥미롭다.

단, 지난 Windows 2000부터는 SetWindowRgn가 아닌 다른 방법으로도 사각형 모양이 아닌 윈도우를 표현할 수 있게 되었다. 바로 layered window이다. WS_EX_LAYERED 스타일을 준 윈도우에다가 SetLayeredWindowAttributes 함수를 호출하면 (1) 이 창에 대해서 투명도를 지정할 수 있고, (2) 특정 RGB 값을 color key로 지정해서 그 색깔은 투명으로 처리할 수 있다. 배경색을 칠하는 것만으로 그 부위가 투명해지니, 번거로운 region보다 훨씬 더 편리해지기까지 했다.

과거 MS Office 97~2000 시절의 흑역사 중에는 'Office 길잡이'라는 "취지만 좋았다" 급의 물건이 있었다. 애니메이션 캐릭터가 화면에 나타나서 돌아다니는데.. 첫 도입되었던 97은 캐릭터가 사각형 창 안에 갇힌 형태였던 반면, 2000부터는 배경 없이 창이 시시각각 애니메이션 캐릭터 모양으로 변했다.

사용자 삽입 이미지사용자 삽입 이미지

이게 Windows 2000에서는 하드웨어빨을 탄 layered window로 비교적 간편하게 구현됐던 반면.. 9x에서는 일일이 window region을 바꿔 가면서 동작했다. 그 밑의 창은 매번 WM_PAINT가 발생하면서 성능 페널티가 장난이 아니었을 것이다.

본인은 먼 옛날에 이런 프로그램도 구경한 적이 있었다. 실행하면 만화영화 그림체로 그려진 병아리 한 마리가 마치 저 Office 길잡이처럼 튀어나왔는데, 그냥 가만히 있는 게 아니라 각종 프로그램 창들 위를 돌아다녔다. 나름 중력도 구현했던지라, 마우스로 집어다가 공중에다 옮겨 놓으면 아래의 근처에 있는 창으로 떨어지기까지 했다. 내 기억이 맞다면 꽤 재미있는 프로그램이었는데.. 지금 인터넷으로 다시 검색하고 구할 길이 없다.

이 프로그램에 대해서 본인이 현재 기억하고 있는 건 아마 일본에서 만들어진 걸로 추정된다는 것, 그리고 무려 Windows 3.x용 16비트 프로그램이었다는 것이다. 그 열악한 Windows 3.x에서도 타이머를 걸어서 최소화 아이콘에다가 애니메이션을 구현하고, 창의 경계가 시시각각 곡선 윤곽으로 바뀌는 저런 액세서리 프로그램이 만들어지기도 했다.

이렇듯, SetWindowRgn을 이용해서 이런 재미있는 활용을 할 수 있는데.. 날개셋 한글 입력기에서 사각형이 아닌 모양의 창이 나타나는 곳은 마우스 휠을 눌렀을 때 나타나는 동그란 자동 스크롤 앵커가 유일하다.

사용자 삽입 이미지

에디트 컨트롤은 자동 스크롤 모드가 없고, MS 오피스 제품들은 동그란 테두리 없이 그냥 배경에다가 회색 화살표가 나타나는 듯하지만.. 마소에서 만든 웹 브라우저(IE, Edge)에서는 앵커 윈도우가 나타난다. 날개셋의 앵커 윈도우도 먼 옛날에 얘를 참고해서 만들어진 것이다. 맨 처음에는 region만 쓰다가 이내 layered window도 사용하도록 형태가 바뀌었다.

여담: 좌표계 관련 문제

아, region의 경계면과 관련해서 주의해야 할 점이 있다.
같은 좌표를 줬을 때, 직사각형은 pen으로 그려지는 테두리와 region의 영역이 픽셀 단위로 정확하게 일치한다. 다시 말해 같은 RECT rc에 대해서 CreateRectRgn + SetClipRgn을 한 뒤에Rectangle을 호출한 결과는 클리핑을 안 했을 때와도 동일하다.

하지만 타원(Ellipse vs CreateEllipticRgn)이나 폴리곤(Polygon vs CreatePolygonRgn) 같은 다른 도형에 대해서는 이것이 성립하지 않는다. region은 오른쪽과 아래 끝의 1픽셀이 미묘하게 잘린다.

//직사각형
CRgn rg; CRect rc(10, 10, 80, 80);
rg.CreateRectRgnIndirect(rc);
dc.SelectClipRgn(&rg); dc.Rectangle(rc);
rc.OffsetRect(40, 40); dc.Rectangle(rc);

//원
CRgn rg; CRect rc(110, 10, 180, 80);
rg.CreateEllipticRgnIndirect(rc);
dc.SelectClipRgn(&rg); dc.Ellipse(rc);
rc.OffsetRect(40, 40); dc.Ellipse(rc);

//폴리곤으로 표현한 직사각형
CRgn rg; POINT pt[4] = {
{10, 100}, {80, 100}, {80, 170}, {10, 170} };
rg.CreatePolygonRgn(pt, 4, ALTERNATE);
dc.SelectClipRgn(&rg); dc.Polygon(pt, 4);

이 코드를 실행한 결과는 다음과 같이 차이가 난다.

사용자 삽입 이미지

폴리곤으로 직사각형 좌표를 지정해 줘도, 아예 직사각형 전용 생성 함수를 줬을 때와 달리, region은 영역이 살짝 덜 생긴다. 그래서 그 region 안에서 동일 좌표로 도형을 직접 그려 보면 테두리의 오른쪽과 아래쪽이 잘린다.

이를 감안해서 원형 region을 생성할 때는 그리기 함수일 때보다 1픽셀 정도 더 크게 원을 그리면 잘리는 현상은 막을 수 있다. 하지만 그래도 그리기 함수와 region 함수는 경계 계산 결과가 서로 미묘하게 달라서 직사각형일 때처럼 깔끔하게 일치하는 모양이 나오지 않는다.
그러니 region 자체의 경계를 그려 주는 FrameRgn 함수를 대신 쓸 수밖에 없다. 허나, 얘가 그려 주는 테두리는 전문적인 원 그리기 함수에 비해 표면이 거칠며 별로 예쁘지 않다.

본인은 처음에 이런 특성을 몰라서 한동안 삽질을 했었다. 이럴 때도 region 대신 layered window는 순수하게 그리기 결과에 따라서 투명색을 자동으로 처리해 주니 더욱 유용하다.

Posted by 사무엘

2019/04/12 08:34 2019/04/12 08:34
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1607

1. 재부팅 이벤트

본인은 지금까지 Windows가 시스템 종료/재시작/로그아웃 될 때.. 메시지에 즉각 응답하면서 정상적으로 돌아가는 프로그램이라면 종료 처리도 당연히 정상적으로 되는 줄로 알고 있었다.
사용자가 [X] 버튼이나 Alt+F4를 눌러서 종료할 때와 완전히 동일하게 말이다. WM_CLOSE에 이어 WM_DESTROY, WM_NCDESTROY가 날아오고, WM_QUIT이 도달하고, message loop이 종료되고, MFC 프로그램으로 치면 ExitInstance와 CWinApp 소멸자가 호출되고 말이다.

왜냐하면 시스템이 종료될 때 발생하는 현상이 언뜻 보기에 정상적인 종료 과정과 동일했기 때문이다. 저장되지 않은 문서가 있는 프로그램에서는 문서를 저장할지 확인 질문이 뜨고, 운영체제는 그 프로그램 때문에 시스템 종료를 못 하고 있다고 통보를 한다. 좋게 WM_CLOSE 메시지를 보내는 걸로 반응을 안 하는 프로그램에 대해서만 운영체제가 TerminateProcess 같은 강제 종료 메커니즘을 동원할 것이다.

그런데 알고 보니 그게 아니었다.
정상적인 종료라면 ExitInstance 부분이 실행되어야 할 것이고 프로그램 설정들을 레지스트리에다 저장하는 부분도(본인이 구현한..) 실행돼야 할 텐데, 내 프로그램이 실행돼 있는 채로 시스템 종료를 하고 나니까 이전 설정이 저장되어 있지 않았다.

꼭 필요하다면 WM_ENDSESSION 메시지에서 사실상 WM_DESTROY 내지 ExitInstance와 다를 바 없는 cleanup 작업을 해야 할 듯했다. 뭐, 날개셋 한글 입력기에서 이 부분이 반영되어 수정돼야 할 건 딱히 없었지만 아무튼 이건 내 직관과는 다른 부분이었다.

시스템이 종료될 때 발생하는 일은 딱히 디버거를 붙여서 테스트 가능하지 않다. =_=;; 로그 파일만 기록하게 해야 하고, 매번 운영체제를 재시작해야 하니 가상 머신을 동원한다 해도 몹시 불편하다. 꽤 오래 전 일이 됐다만, 날개셋 외부 모듈이 특정 운영체제와 특정 상황에서 시스템 종료 중에 운영체제 시스템 프로세스 안에서 죽는 문제가 발생해서 디버깅을 한 적이 있었다. 몹시 골치 아팠었던 걸로 기억한다.

2. 파일의 동등성 비교

파일 이름을 나타내는 어떤 문자열이 주어졌을 때,

  • 상대 경로(현재의 current directory를 기준) vs 절대 경로
  • 과거 Windows 9x 시절이라면 ~1 같은 게 붙은 8.3 짧은 이름 vs 원래의 긴 이름
  • 대소문자 (Windows는 대소문자 구분이 없으므로. 그런데 이것도 A~Z 26자만 기계적으로 되나? 언어별로 다른 문자의 대소문자는?)
  • 그리고 옵션으로는 정규 DLL search path와 환경 변수

이런 것들을 몽땅 다~ 감안해서 다음과 비스무리한 일을 하는 가벼운 API가 좀 있으면 좋겠다. 보다시피 동일한 파일을 표현하는 방법이 굉장히 다양하기 때문이다.

  • 두 개의 파일 명칭이 서로 동일한 파일인지 여부를 판단. 당연히 파일을 직접 open/load하지 않고 말이다.
  • 그 파일의 대소문자 원형을 유지한 절대 경로. 일명 '정규화된' 이름을 되돌린다. 이게 동일한 파일은 물리적으로, 절대적으로 동일한 파일임이 물론 보장된다.
    참고로 Windows API 중에서 얼추 비슷한 일을 한다고 여겨지는 GetFullPathName이나 GetModuleFileName 함수는 절대 경로 말고 파일의 대소문자 원형 복원이 제대로 되지 않는다.
  • 혹은 그 파일의 시작 지점이 디스크에서 물리적으로 어디에 있는지를 나타내는 64비트 정수 식별자를 구한다. 포인터가 아니지만 파일의 동등성 여부 판단은 가능한 값이다. 정수를 넘어 UUID급이어도 무방함.

본인은 비슷한 목적을 수행하기 위해, 그냥 두 파일을 CreateFileMapping으로 열어 보고 리턴된 주소가 동일하면 동일한 파일로 간주하게 한 적이 있었다. 핸들 말고 MapViewOfFile 말이다. 본질적으로 동일한 파일이라면 운영체제가 알아서 같은 주소에다 매핑을 하고 레퍼런스 카운트만 증가시킬 테니까..
Windows 9x에서는 주소가 시스템 전체 차원에서 동일하겠지만 NT 계열에서는 한 프로세스 내부에서만 동일할 것이다.

하지만 내가 필요한 건 파일의 내용이 아니라 그냥 두 파일의 동등성뿐인데 이렇게 매핑을 하는 건 overkill 삽질 같다는 인상을 지울 수 없었다.
비슷한 예로, LoadLibrary도 같은 파일에 대해서는 같은 리턴값이 돌아온다. HMODULE도 오늘날은 핸들이 아니라 메모리 map된 주소이니까.. 다만, 오버헤드를 줄인답시고 LoadLibraryEx + LOAD_LIBRARY_AS_DATAFILE 이렇게 열어서는 안 된다. 그러면 로딩 방식이 크게 달라지더라.

3. JNI에서 문자열 처리하기

Java 언어는 JNI라고 해서 자기네 바이트코드 가상 머신이 아닌 C/C++ 네이티브 코드를 호출하는 통로를 제공한다.
프로그램 자체를 C/C++로 짜던 시절에는 극한의 성능을 짜내야 하는 부분에 어셈블리어를 집어넣는 게 관행이었는데.. 이제는 일반적인 코딩은 garbage collector까지 있는 상위 계층에서 수행하고, 극한의 성능을 짜내야 하는 부분에서만 C/C++ 코드를 호출한다는 게 흥미롭다.

JNI는 그냥 언어 스펙에 가까운 광범위한 물건이다. Windows 환경에서는 그냥 Visual C++로 빌드한 DLL이 export하는 함수를 그대로 연결할 수도 있다. 물론 그 DLL을 빌드하기 위해서는 Java SDK에서 제공하는 jni 인터페이스 헤더와 static 라이브러리를 사용해야 한다.
한편, 안드로이드 앱 개발에서 쓰이는 NDK는 JNI 스펙을 기반으로 자체적인 C++ 컴파일러까지 갖춘 네이티브 코드 빌드 도구이다.

Java의 문자열은 JNI에서는 jstring이라고 내부 구조를 알 수 없는 자료형의 포인터 형태로 전달된다. C++에서는 UTF-8과 UTF-16 중 편한 형태로 바꿔서 참조 가능하다.
UTF-8로 열람하려면 JNIEnv::GetStringUTFChars를 호출하면 된다. 길이를 알아 오려면 GetStringUTFLength부터 호출한다. 전해받은 문자열 포인터는 ReleaseStringUTFChars로 해제한다.

그 반면, UTF-16 형태로 열람하려면 위의 함수 명칭에서 UTF를 빼면 된다. GetStringChars, GetStringLength, ReleaseStringChars의 순이다. Java가 내부적으로 문자를 2바이트 단위로 처리하기 때문에 이들이 주로 취급하는 자료형은 jchar*이다. 그러니 얘는 char16_t 자료형과 호환된다고 간주해도 좋다. 참고로 wchar_t는 NDK 컴파일러의 경우 4바이트로 처리되더라.

UTF-16이나 UTF-8이나 다 UTF이긴 마찬가지인데, Java는 변별 요소인 8을 생략하고 함수 이름을 왜 저렇게 지었나 개인적으로 의구심이 든다. 물론 GetStringChars는 Java가 내부적으로 문자열을 원래부터 2바이트 단위로 처리하다 보니 우연히 UTF-16과 대응하게 됐을 뿐, 대놓고 UTF-16을 표방했던 건 아닐 것이다. 뭐, 이제 와서 그 체계를 바꾸는 건 불가능하고 "자바 문자열 = 2바이트 단위"는 완전히 고정되고 정착했지만 말이다.

또한 GetStringChars는 GetStringUTFChars와 달리 굉장히 치명적으로 불편한 단점이 하나 있다. 바로.. 변환된 문자열이 NULL-terminated라는 보장이 없다는 것이다!
그래서 본인은 이 포인터를 사용할 때 메모리를 n+1글자만치 또 할당해서 null문자를 추가해 주는 매우 번거로운 두벌일을 하고, 아예 클래스를 이렇게 따로 만들어야 했다. 좀 개선의 여지가 없으려나 모르겠다.

class CJstrToString16 {
    JNIEnv *_ev;
    jstring _jstr;
    const jchar *_ret;
    char16_t *_arr;
public:
    CJstrToString16(JNIEnv *ev, jstring js): _ev(ev), _jstr(js) {
        jsize n = ev->GetStringLength(js);
        _ret = ev->GetStringChars(js, NULL);
        _arr = new char16_t[n+1];
        memcpy(_arr, _ret, n*sizeof(char16_t));
        _arr[n] = 0; //고작 요거 하나 때문에..
    }
    ~CJstrToString16() {
        ev->ReleaseStringChars(_jstr, _ret);
        delete[] _arr;
    }
    operator const char16_t*() const { return _arr; }
};

4. Visual C++의 STL

C++은 타 프로그래밍 언어들과 달리, 심지어 전신인 C와도 달리, 언어가 개발되고 나서 자신의 특성을 잘 살린 라이브러리가 언어 차원에서 붙박이로 곧장 제정되지 않았던 모양이다. 그래서 각 컴파일러들이 중구난방으로 파편화된 형태로 라이브러리를 제공해 오다가.. 표준화라는 게 1990년대 말이 돼서야 논의되기 시작했다.

템플릿이 추가되어 C++에서도 제네릭, 메타프로그래밍이라는 게 가능해진 뒤부터 말이다. 처음에는 자료구조 컨테이너 위주로 STL이라는 이름이 붙었다가 나중에는 그냥 C++ library가 된 걸로 본인은 알고 있다.

Windows용으로 가장 대중적인 C++ 컴파일러야 두 말할 나위 없이 MS Visual C++이다. 얘는 거의 20여 년 전 6.0 시절부터 P.J. Plauger라는 사람이 구현한 C++ 라이브러리를 제공해 왔다. C 라이브러리와 달리 C++ 라이브러리는 소스가 비교도 안 될 정도로 복잡하고 난해하다는 것(암호 같은 템플릿 인자들..=_=), 그리고 저렇게 마소 직원이 아닌 개인 이름이 붙어 있다는 게 인상적이었다. 2000년대 초까지만 해도 휴렛-패커드라는 회사명도 주석에 기재돼 있었다.

P.J. Plauger는 현재는 Dinkumware라고 C++ 라이브러리만 전문적으로 관리하고 라이선스 하는 회사를 설립해 있다. 나이도 생각보다 지긋한 듯..
그런데 이런 세계적인 제품에 들어가는 라이브러리가.. 성능이 의외로 시원찮은가 보다. Visual C++이 제공하는 컨테이너 클래스가 유난히도 느리다고 까이는 걸 여러 사이트에서 봐 왔다.

최근에는 본인 직장의 상사마저도 같은 말씀을 하시기에 "헐~!" 했다. 업무상 필요해서 string, set, map 등을 써서 수십, 수백 MB에 달하는 문자열을 분석하는 프로그램을 돌렸는데, 자료 용량이 커질수록 속도가 급격히 느려져서 자료구조를 직접 새로 짜야 할 판이라고 한다.

난 개인적으로는 C++ 라이브러리를 거의 사용하지 않고, 더구나 그걸로 그 정도까지 대용량 작업도 해 보지 않아서 잘 모르겠다. 그 날고 기는 전문가가 만든 코드에 설마 그런 결함이 있으려나? 아니면 컴파일러의 최적화 문제인지?

글쎄.. 이런 게 있을 수는 있다. MFC의 CString은 그냥 포인터와 크기가 동일하며 값으로 전할 때의 reference counting도 처리한다. 그러나 std::string은 자주 쓰이는 짧은 문자열을 번거로운 heap 메모리 할당 없이 빠르게 취급하기 위한 배열까지 내부에 포함하고 있다. 이런 특성을 모르고 std::string도 함수에다 매번 value로 전달하면 성능에 악영향을 줄 수밖에 없다.

그런 식으로 임시 객체가 쓸데없이 생겼다가 사라지는 구조적인 비효율이 C++ 라이브러리에 좀 있는 걸로 들었다. R-value 참조자 &&가 도입된 것도 vector의 내부 처리에서 그런 삽질을 예방하는 근거를 언어 차원에서 마련하기 위해서라지 않는가? 그리고 Visual C++이 그런 비효율을 보정하는 성능이 좀 시원찮다거나 한 것 같다. 전부 다 그냥 추측일 뿐이다.

그러고 보니 cout<<"Hello world"가 printf("Hello world")보다 코드 오버헤드가 작아지는 날이 과연 올지 모르겠다. 이것도 그냥 떡밥인 건지..?? =_=;;

Posted by 사무엘

2019/04/09 08:32 2019/04/09 08:32
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1606

본인은 박사 과정에 진학해서 수업을 다 듣고 종합 시험도 통과한 뒤, 지난 2년이 넘는 기간 동안 휴학을 했다. 그리고 그 기간 동안 날개셋 한글 입력기는 8.6에서 9.7까지 올라갔다.
입력기 연구의 연장선에다가 글꼴 연구도 새로 접목하는 것을 목표로 진학했었는데, 입력기 연구가 당초 계획보다 다소 오래 걸렸다.

이제 입력기는 파일 포맷과 엔진 구조를 다 뜯어고칠 정도로 너무 비현실적인 추상화(재설계나 리팩터링), 아니면 너무 이상적인 수준의 기능을 제외하고 어지간히 규칙 기반 한글 입력과 관련된 것들은 다 통달했으며 잘 실현됐다. 9.7도 특별히 심각한 문제 없이 아주 잘 만들어졌다.

다만, 날개셋은 TSF 기반의 한글 IME일 뿐만 아니라 반대로 타 IME들을 구동해 주는 텍스트 에디터이기도 하니.. 요즘은 편집기에서 타 IME를 구동하는 동작과 관련된 이슈들을 좀 살펴보고 있다.

1. 내 프로그램에서 9.7 이후로 개선된 사항

(1) 외부 모듈의 옛한글 조합을 여느 블록(selection)과 달리 취급

날개셋 편집기에서 입력 항목을 '빈 입력 스키마'로 고르면 앞서 언급한 바와 같이 자체 입력기가 아니라 다른 외부 IME들을 사용할 수 있다.
그런데 한글 IME로 현대 한글을 조합할 때는 깜빡이는 네모 cursor가 나타나는 반면, 옛한글을 조합할 때는 조합이 그냥 블록 형태로 잡힌다. 그래서 자체 입력기로 옛한글을 입력할 때와는 달리 이질적이고 아마추어스러운 느낌이 난다.

이건 일차적으로는 운영체제에서 옛한글처럼 내부적으로 2개 이상의 코드값으로 표시되는 한글에 대한 배려를 안 해서 그렇다. 조합 문자열이 한글로만 이뤄져 있을 때 응용 프로그램이 강제로 보정을 해서 깜빡이는 네모 cursor를 구현하라면 할 수도 있다.

내 프로그램에서는 그렇게까지는 안 하는 대신, 비록 블록처럼 보이더라도 진짜 블록이 잡힌 것처럼 복사/잘라내기 버튼이 켜지지도 않게 프로그램의 동작을 깨알같이 개선했다. 그건 블록이 아니라 조합을 표시하는 용도일 뿐이기 때문이다..

사용자 삽입 이미지

(2) 붙여넣기를 할 때 외부 모듈의 조합이 덧나지 않게

또한, 자체 입력기가 아닌 외부 IME로 한글을 조합하고 있던 중에 도구모음줄의 '붙여넣기' 버튼을 마우스로 누르면..
자체 입력기를 사용할 때와 마찬가지로 조합이 종료된 뒤에 클립보드 내용이 삽입되는게 정상이다.

하지만 지금까지는 그렇지 않았다. 조합이 중단되고 그 문자열이 사라지고서 클립보드 내용이 삽입되었다. 이 버그를 발견하여 고쳤다.

사용자 삽입 이미지

이 두 가지 사항은 언제쯤 다음 버전에 반영되어 나올지 모르겠다.

2. 내 프로그램과 무관한 운영체제의 버그

(1) IME 도구모음줄이 두 종류 모두 표시됨

Windows 10 1803 버전 기준으로..
IME의 구형 재래식 도구모음줄과 Windows 8 스타일의 간소화 도구모음줄이 다같이 동시에 뜨는 경우가 있다. 정확한 재연 조건은 잘 모르겠지만 컴퓨터를 절전 상태로 껐다가 다시 켰을 때 가끔, 그러나 확실하게 이런 현상이 발생한다.

사용자 삽입 이미지

"고급 키보드 설정"에서 "사용 가능한 경우 바탕 화면 입력 도구 모음 사용" 옵션을 건드려 주면 다시 둘 중 하나만 나타나게 개선되긴 한다. 하지만 이건 운영체제의 버그이니 나중에 업데이트를 통해 해결되어야 할 것이다. Windows 8은 물론이고 10도 초창기에는 이런 현상이 발생한 적이 없었다.

(2) 일본어 IME의 조합 관리 버그

날개셋 편집기 또는 IE/Edge 브라우저의 텍스트 입력 폼에서 Microsoft 일본어 IME를 구동하고, 히라가나 모드에서 일본어를 몇 자 입력한다.
space를 눌러서 그 일본어 문자를 변환은 하지 말고, 좌우 화살표 키를 눌러서 조합 영역을 빠져나간다. 그러면 조합을 나타내는 밑줄이 일시적으로 사라진다.

그 뒤에 caret이 기존 조합 영역으로 돌아오면 기존 조합이 다시 생겨야 되는데 그리 되지 않는다.
그 상태에서 다른 곳에서 Shift+화살표를 눌러서 블록을 만들어 보면 아까 조합하던 일본어 문자가 덧나서 잘못 삽입된다.

사용자 삽입 이미지

이 버그는 Windows 10의 16xx대 이전 버전에서는 발생하지 않다가 후대 버전에서 나타났다. 1803의 후대 버전에서는 어찌 되었나 모르겠다. 날개셋 편집기뿐만 아니라 MS에서 만든 TSF A급 웹브라우저에서 모두 동일하게 발생하니 내 프로그램만의 문제도 아니다.

단, 워드패드에서는 동일 운영체제와 동일 IME에서 저런 오동작이 발생하지 않는다. 서식을 지원하기도 하니 에디팅 엔진 차원에서 무슨 차이가 있어서 그런 것 같다.

(3) 옛한글 IME의 조합 영역 처리 버그

이건 Windows 8 이래로 계속 동일한 것 같은데..
마소에서 제공하는 옛한글 입력기는 초성이나 중성에 옛한글이 들어간 상태에서 종성의 첫 타를 입력하면.. caret 위치가 좀 이상하게 찍힌다. 내부적으로 표현되는 글자 수가 3자가 되었으니 0~3까지 모두 조합 영역으로 설정해야 하는데 종성이 입력되기 전처럼 0~2까지만 설정한다.
그래서 날개셋 편집기에서는 화면이 일시적으로 이렇게 표시된다. 종성 둘째 타 이후부터는 다시 괜찮아진다.

사용자 삽입 이미지

시각적으로 좀 이상한 것 말고 다른 오동작은 없다. 하지만 날개셋, 한컴 입력기 등 옛한글 입력을 지원하는 다른 모든  IME에는 이런 현상이 없고 MS IME만 저러니.. 이건 저 프로그램만이 단독으로 해결해야 할 문제로 보인다.

3. 단순 차이점 -- 옛한글 filler 글쇠

두벌식 옛한글 글자판에는 중성이 빠진 미완성 한글 내지 종성 단독 낱자를 입력하기 위해서 일명 filler라는 글쇠가 있다. 위치는 관례적으로 Shift+J로, 날개셋, 아래아한글, MS 옛한글 입력기가 모두 동일하다.

날개셋과 아래아한글에서는 이 filler라는 게 언제나 '중성 filler'를 의미한다. 이것만 있어도 초성이나 종성이 없는 글자는 입력 가능하기 때문이다.
하지만 MS의 경우, filler도 뭔가 두벌식스럽게 글자를 완전 처음 입력할 때는 빈 자리에다가 '초성 filler'를 흉내 내어 주는 것 같다. 굳이 그럴 필요가 없지만 말이다.

그래서 초기 상태에서 종성을 단독으로 입력하려면 filler를 한 번이 아닌 두 번 눌러야 한다. 본인은 처음엔 이런 차이를 몰라서 마소 옛한글 입력기로는 종성 단독 입력이 불가능한 줄 알았다.
초성 filler도 지원해 주는 게 사람에 따라서는 더 직관적으로 보일 수도 있다. 하지만 한글을 연속으로 입력하기 시작하면 filler는 어차피 사실상 중성으로만 동작해야 하기 때문에 굳이 저럴 예외를 둘 필요가 있나 싶다.

중요한 건 이런 동작조차도 표준으로 딱 정해진 게 없어서 프로그램마다 차이가 있을 수 있다는 점이다. 날개셋에서는 글쇠배열의 수식을 바꿔 주면 지금 동작(중성 고정)뿐만 아니라 MS IME의 동작도 물론 구현할 수 있다.

4. 원인을 알 수 없는 문제

다음은 본인의 개발 환경에서 아주 드물게 발생하는 것을 확인하긴 했지만 재연 조건을 전혀 몰라서 좀 난감한 지경에 있는 버그 아이템들이다. 이것들이 문제의 원인이 전적으로 내 프로그램의 귀책사유로 판명되어 해결된다면.. 위의 1번의 개선 사항까지 포함해서 다음 버전인 9.71이 지금이라도 당장 나오게 된다.

(1) 여전히 발생하는 랙

이번 9.7에서는 안 그래도 편집기의 에디팅 엔진과 관련된 몇몇 버그들이 잡히고 내부 동작 방식이 최적화 됐다. 그런데 편집기를 한번 띄워 놓고 며칠 이상 오래--특히 중간에 컴의 절전 모드와 복귀를 수차례 반복할 정도로-- 쓰다 보면, 어느 샌가 글자가 화면에 나타나는 속도가 내 타자 속도를 못 따라갈 정도로 랙이 걸리는 경우가 여전히 발생한다.

게다가 이게 참 악랄한 게.. 랙의 발생하던 당시에 발생 조건이 다음과 같이 가변적이었다는 것이다.

  • 오로지 날개셋 외부 모듈로 한글을 입력할 때만 느려짐 (MS IME, 자체 입력기 등등은 괜찮음)
  • 외부 모듈로 한글을 입력할 때만 느려짐 (날개셋, MS IME에서 랙. 자체 입력기는 괜찮음)
  • 아무 방식으로나 글자를 입력할 때 몽땅 느려짐

마지막으로 이 문제가 발생했을 때엔.. 처음엔 외부 모듈에서만 발생하는 것 같더니 이내 상황이 최악으로 바뀌었다.
특정 문서의 맨 마지막 줄에서 글자를 입력할 때만 극심한 랙이 걸리고, 그렇지 않을 때는 괜찮았다(타 문서 or 다른 줄). 심지어 그 문서에서 편집하고 있던 텍스트를 몽땅 지우고 새로 입력을 시작해도 랙이 사라지지 않았다.

이 랙이 발생하는 동안 내 프로그램의 내부에서는 무슨 일이 벌어지고 있고 도대체 어느 계층에서 뺑뺑이를 도는 건지 도무지 알 길이 없다. 그냥 평범하게 프로그램을 띄워서는 절대로 발생하지 않는다. 그나마 유력한 단서가 될 만한 현상은.. 이때 날개셋 편집기가 다음과 같이 memory leak이 발생해 있었다는 것이다.

(2) MS IME를 사용할 때 발생하는 괴이한 memory leak

날개셋 편집기와 작업 관리자를 같이 실행한다. 다음으로, 편집기에서 TSF 지원 옵션을 켠 상태에서 '빈 입력 스키마'를 고른다.
Microsoft 기본 한글 IME로, "세벌식 390/최종"(두벌식 말고)으로 "ㅇ.ㅇ.ㅇ.ㅇ." 처럼.. 한글 + 비한글 문자를 수십 회 쭈룩쭈룩 교대로 입력해 보라.

그러면 초기에 2~3MB대 안팎이던 프로세스 메모리 사용량이 계속해서 증가하는 게 관찰된다. 명백하게 memory leak이다.
COM 오브젝트 간의 reference count 같은 게 꼬인 것 같은데.. 이건 도대체 누구 잘못이라고 봐야 할까?

당연히, 디버그 빌드에서 단순 memory leak detector로는 문제가 전혀 감지되지 않는다. 내 프로그램은 10수 년에 달하는 짬밥을 자랑하며 얼마나 오랫동안 안정화가 돼 왔는데.. 소스 코드 상으로 무식한 결함이 있지는 않다.

더구나 날개셋, 한컴 입력기 등 "타 IME에서는 이런 현상이 없다." MS IME도 세벌식을 쓰고 있을 때만 저렇고 두벌식일 때는 문제 없다.
그리고 Windows Vista, 7, 10에서 이 현상을 확인했다. 구닥다리 XP에서는 MS IME+세벌식에서도 문제가 없다.

그럼 내 과실 0, 마소 과실 100을 입증하려면 날개셋 편집기 말고 다른 프로그램에서도 MS IME + 세벌식으로 저렇게 쳤을 때 동일한 memory leak이 발생한다는 걸 입증해야 하는데 그건 또 그렇지 않아 보인다~! 워드패드, MS Word, IE, Edge 등 TSF를 지원하는 프로그램들은 또 희한하게도 저런 현상이 발생하지 않는다.

다음으로 지푸라기를 잡는 심정으로 날개셋 구버전까지도 구해서 써 봤다.
날개셋 8.0까지는 이 문제가 없고, 8.4부터 leak이 발생하더라. 8.2는 불명.. 그러니 이 버그는 대략 2016년부터 있어 왔다는 것이다.
이때 도대체 어떤 변화가 있었는지.. 본인은 프로그램 소스를 자주 백업하는 편이지만, 컴퓨터가 바뀌는 과정에서 지금으로부터 3년이 넘게 너무 오래된 소스는 갖고 있지 않아서 이 방법으로도 문제의 원인을 파악할 수 없었다. ㅠㅠ

난 Windows용 IME라는 물건을 개발하느라 지난 10여 년 동안 온갖 희한한 버그 신고들을 받고 기상천외한 지저분한 환경에서 디버깅을 했다. 그러면서 마치 교통사고 과실 비율 따지는 것 같은 현상들을 많이 경험했었다.
둘 다 스펙대로 100% 무결하게 구현된 건 아니었고(혹은 스펙 자체가 모호해서..) 둘 다 조금만 조심하면 됐는데 둘 다 무데뽀로 동작해서 운 나쁘게 문제가 발생하는 것.. 말이다. Windows용 IME라는 바닥은 무척 "구리다."

아무튼 현재로서는 저 memory leak의 원인과 해결 방법이 오리무중이다. 해결만 된다면 9.7 다음으로 9.71이 당장 나와야 할 것이다.

(3) 제어판 닫았을 때 프로그램 뻗나?

정말 민망하고 황당한 버그인데.. 올해 들어 두세 번인가 겪었다.
날개셋 편집기에서 제어판을 꺼내서 설정을 바꾼 뒤, 확인을 눌러서 닫았더니 편집기 프로그램이 그냥 대짜로 뻗어 버렸다.
한 번 발생했을 때는 그냥 재수없는 우연인가 싶었는데, 몇 주 전에 마지막으로 동일 문제를 겪었을 때는 '미저장 확인'을 누른 것만으로도 뻗었다.

물론 그 뒤로는 편집기에서 날개셋 외부 모듈을 또 얹고, 제어판에서 온갖 설정을 바꾸고 빠른설정을 띄우면서 지지고 볶아 봐도.. 동일 문제가 다시는 재발하지 않고 있다. 위의 랙도 몹시 드물게 발생하지만 이 crash는 그것보다 더 드물게 발생했다. 그래서 난감하다.

Posted by 사무엘

2019/03/31 08:30 2019/03/31 08:30
, , ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1603

1. 32비트 컴파일러: 16비트 메모리 접근의 한계를 극복하기

예전에도 언급한 적이 있지만.. 1993년 말에 발매되었던 Doom 게임은 그야말로 충격적인 3차원 그래픽 덕분에 게임 업계에 큰 충격을 선사했다. 업계 종사자들은 기술 수준 자체뿐만 아니라 "얘는 어셈블리어를 거의 사용하지 않고 순수 C만으로 개발되었습니다"라는 존 카맥의 말에 더 큰 충격을 받게 됐다.

훗날(1997년) Doom의 소스 코드가 공개되면서 이 말은 사실임이 밝혀졌다.
Doom은 무슨 16비트 Windows 같은 쑤제 어셈블리어 튜닝 위주로 개발된 게 아니라, Windows NT처럼 굉장히 이식성 있게 개발되었다. 그러니 Doom 엔진 기반의 수많은 게임과 mod들이 온갖 플랫폼으로 이식되어 만들어질 수 있었다.

단지, 오리지널 도스용의 경우, 컴파일러를 그 당시에 흔하던 볼랜드나 MS 같은 16비트용을 쓴 게 아니라 Watcom이라는 다소 생소한 32비트 고성능 제품을 썼을 뿐이다.
그리고 어셈블리어를 안 쓰더라도 고정소수점이라든가, IEEE754의 특성을 이용해서 3차원 그래픽용 실수 연산(삼각함수, 제곱근, 벡터 정규화...)을 왕창 빠르게 수행하는 각종 tweak들은 응당 최대한 구사해서 성능을 끌어올렸다.

그러니 Doom은 아직 상대적으로 생소하던 32비트 컴파일러라든가 DOS/4G 도스 익스텐더 같은 물건의 인지도를 끌어올려 줬다. 이렇게 Doom을 통해 Watcom 컴파일러까지 알렸던 id 소프트웨어에서는 훗날 퀘이크를 만들어서 이번에는 오픈소스 진영의 걸출한 도스용 32비트 컴파일러이던 djgpp를 알리게 되었다.

운영체제 자체를 OS/2나 Windows NT처럼 통째로 32비트로 쓰기에는 아직 기계값이 너무 비싸고 특히 메모리가 부족했다. 그러니 도스에서 돌아가는 일부 대형/고사양 프로그램이 자체적으로 도스의 한계를 극복하고 보호 모드로 진입하는 솔루션을 내장했던 것이다.

생각해 보니 국내에서도 아래아한글 2.1이 전문용은 Watcom C/C++을 이용한 32비트 전용으로 만들어졌다. 얘는 발매 시기가 심지어 Doom보다도 3개월 남짓 더 앞섰다(1993년 9월 vs 12월). 그러니, 터보 C, 볼랜드 C++ 하던 그 시절에도 32비트 컴파일러에 대해서 알 사람은 이미 다 알기는 했던 모양이다.

다만, 아직도 286 똥컴이 많이 굴러다니고 서민용 운영체제들은 아직도 16비트 도스와 Windows가 주류인데, 내 프로그램을 386 전용으로 개발하는 것에 대한 득과 실을 신중하게 따져야 했다. 오죽했으면 아래아한글도 후속 버전인 2.5와 3.0에서는 일반용/전문용 구분이 없어지고 그냥 hwp86.exe와 hwp386.exe 두 에디션을 모두 내장하는 것으로 형태가 바뀌었다. 추가 글꼴과 사전 컨텐츠는 '확장팩'으로 분리되고 말이다.

아래아한글은 Phar Lap 도스 익스텐더를 사용했다. 아래아한글이 그 시절의 도스용 게임처럼 DOS/4G(W) 로고를 띄우면서 실행되었다면 무척 볼 만했을 것이다.
86과 386 에디션은 성능 말고는 덧실행 프로그램이 지원되는지의 여부가 가장 큰 차이점이었다. 덧실행은 16/32비트용이 따로 나오지 않고 32비트 전용이었기 때문이다.

화면 보호기들, 그리고 확장팩에서 제공되었던 프라임 영한사전도 다 덧실행 프로그램이었다.
먼 옛날 1.2 시절에는 별도의 액세서리로 테트리스 게임이 있었는데 나중에 그게 덧실행으로 컴백한 걸 보니 개인적으로 감회가 새로웠었다.

이렇게 1990년 중반에 도스용 프로그램들의 32비트화 추세와 달리, 마소는 진작부터 PC에서 도스를 Windows로 대체하려는 큰 그림을 갖고 있어서 그런지.. 도스용으로 32비트 컴파일러를 결코 내놓지 않았다. 정작 자기들은 그 기술을 내부적으로 보유하고 사용했으면서 말이다.
Visual C++ 1.5x는 16비트 도스/Windows 바이너리들을 빌드할 수 있었는데, 명령 프롬프트에서 돌아가는 컴파일러와 링커 같은 툴들은 그냥 32비트 프로그램이 아니라 32비트 PE 기반의 콘솔 프로그램이었다.

Windows NT 같은 데서는 직통으로 실행 가능하고, 도스에서 실행되면 stub으로 embed된 도스 익스텐더가 컴을 보호 모드로 진입시키고 CreateFile/GlobalAlloc 같은 Win32 API를 제공해서 프로그램을 실행했다.
스레드를 만들지는 못했겠지만 컴파일러· 링커가 사용하는 Win32 API야 뭐 파일이나 메모리 I/O 정도밖에 없었을 것이고, 이건 도스 익스텐더가 감당 가능했다. 결국 한 바이너리만으로 도스와 Windows에서 모두 사용 가능.

이건 뭐 콘솔 프로그램계의 Win32s나 마찬가지인 엄청난 기술인데.. 마소의 Visual C++에서 이런 이중 바이너리를 만드는 걸 end-user에게 지원한 적은 내가 알기로 없다.
마치 C# 네이티브 코드 컴파일러만큼이나 대외적으로 공개되지 않고 마소 내부에 봉인된 기술인 것 같다.

2. 슈퍼 VGA 라이브러리: 표준 VGA의 한계를 극복하기

IBM 호환 PC라고 불리는 물건에서 IBM이 주도하는 PC의 단일/표준 규격이라는 건 286 AT 이후로 없어졌다. 그러니 286 이후로 최초의 386 PC는 IBM이 아닌 컴팩에서 출시되기까지 했다.
그리고 그래픽 카드도 절대불변 단일 표준은 1987년의 구닥다리 VGA가 마지막이다. 표준 VGA는 800*600 해상도조차 지원하지 않았으며, 그나마 색깔이 아쉬운 대로 다양해진 256색은 겨우 320*200에서밖에 지원되지 않아서 업무라기보다는 그냥 게임 전용 모드로만 쓰였다.

그 뒤로 VGA보다 더 높은 해상도와 더 많은 색상을 지원하는 규격은 그야말로 온갖 싸제 SVGA 제조사들이 난립하면서 파편화 천국이 됐다. VESA 같은 규격이 괜히 필요해진 게 아니다.

이게 불과 1990년대 초반의 일이니, 앞에서 언급한 보호 모드가 어떻고 DPMI가 제정되던 때와 시기적으로 비슷하다. 하긴, 1990년에 나온 그 옛날 프로그램인 Deluxe Paint조차도 처음 실행될 때 맨 아래에 1024*768 256색 SVGA 모드가 있긴 했다. 물론 당대에 그걸 선뜻 고를 수 있을 정도의 금수저 컴퓨터를 소유한 사용자는 매우 소수였을 것이다.

마소의 베이직 컴파일러야 SCREEN 명령으로 SVGA 지원은 전무했다. API 구조가 완전히 다른 3rd-party 라이브러리를 구해서 써야 했다.
볼랜드의 경우는 상황이 약간 낫다. 비록 자체적으로는 VGA까지밖에 지원하지 않았지만, 일종의 그래픽 드라이버인 bgi 파일이 내부 스펙이 공개돼 있고 확장 가능했기 때문에 이걸 기반으로 SVGA 라이브러리를 만든 곳이 있긴 했다.

검색을 해 보니 Jordan Hargraphix 소프트웨어가 이 업계의 독자적인 큰손이었던 모양이다. 이미 1991년 무렵부터 유명했다.
바이오스를 거치지 않고 일명 VGA mode X라고 불리는 320*240, 400*300 같은 변형 모드까지 다 지원했다.
그때는 소프트웨어가 잘못된 명령을 내려서 컴퓨터만 뻗게 하는 게 아니라 모니터를 손상시키는 것도 가능했던 시절이다. (주사율 변조..) 옛날에 CGA도 160*100 같은 tweak mode가 있었다고 하는데 그것만큼이나 신기한 일이 아닐 수 없다.

다만, BGI라는 그래픽 API는 무려 1980년대 후반에 개발된 것이며, 아무리 bgi 드라이버를 새 하드웨어에 맞게 확장한다 해도 256색 이상의 색을 지원하는 것은 구조적으로 불가능했다고 한다. 트루컬러 SVGA를 지원하려면 완전히 새로운 독자 라이브러리를 써야 했다.
BGI는 색상을 관리하는 게 RGB값 기반이 아니라 팔레트 인덱스 기반으로 고정돼 있었던 모양인데, 16비트 시절에 이는 충분히 수긍이 간다. 쟤가 무슨 Windows GDI 급으로 하드웨어 통합과 추상화를 표방한 물건은 아니었으니 말이다.

도스용 아래아한글은 16비트 바이너리의 경우 Turbo/Borland 컴파일러로 개발되었다. 하지만 아주 초창기인 1.x 시절부터 그래픽 라이브러리를 독자 구현했는지, 볼랜드의 보급 BGI 라이브러리를 사용한 흔적이 전혀 없는 것이 매우 흥미롭다.
이건 비슷한 시기에 도스용 한메 타자 교사도 마찬가지다. 얘도 MS C로 개발되었지, 의외로 볼랜드 출신이 아니다.

Posted by 사무엘

2019/03/23 08:31 2019/03/23 08:31
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1600

* 2014년에 썼던 글을 보완하여 다시 올린다.

옛날에 도스 시절에는 일명 '외부 명령'이라 하여 별도의 프로그램 형태로 존재하는 명령들이 있었다. format.com, diskcopy.exe 같은 것들.
이것들은 자기가 소속된 도스 버전을 가려서 동작했다. 가령, MS 도스 5.0이 설치된 컴퓨터에다 도스 6.x에 존재하는 새로운 유틸리티를 복사해 와서 실행하면, 실행에 필요한 파일들이 다 있다 하더라도 '도스 버전이 다릅니다'라는 에러 메시지와 함께 프로그램이 그냥 실행되지 않았다. 이것은 운영체제의 버전을 가려 가며 실행하는 프로그램을 본인이 난생 처음으로 접한 사례였다.

Windows에도 자신의 버전을 알려 주는 API가 응당 존재한다. 하지만 이건 지금 구동 중인 운영체제가 무엇인지를 알려 주는 편의 기능을 구현할 때나 사용할 만한 기능이다. 일반적인 프로그램이라면 About 대화상자 같은 데서 말이다.
만약 프로그램이 운영체제의 버전을 가려 가며 실행해야 한다면, 단순히 운영체제의 버전을 갖고 판단하는 건 썩 좋은 방법이 아니다. 내가 실제로 사용하고자 하는 기능을 요청해 보고(CoCreateInstance, LoadLibrary/GetProcAddress 등), 그 요청의 성공 여부에 따라 실행 여부를 결정하는 게 바람직하다.

뭐, 지금은 아무 의미가 없는 예가 돼 버렸다만,
가령 내 프로그램이 유니코드 API를 사용하기 때문에 Windows 9x에서는 실행을 거부해야 한다고 치자.
그렇다면 CreateWindowExW건 RegisterClassW건 유니코드 API를 실제로 호출해 본 뒤, 그게 실패하고 GetLastError()==ERROR_CALL_NOT_IMPLEMENTED가 돌아올 때 실행을 거부하면 된다. 운영체제의 외형보다는 그 운영체제의 실제 실행 결과를 보고 판단하는 게 낫다는 게 바로 이런 의미이다.

그런 것도 다 필요 없고 운영체제의 버전 숫자를 정말로 정확하게 알아 와야 한다면,
그 경우를 위해 태초에 GetVersion()이라는 간단한 함수가 있었다. 얘는 버전과 관련된 여러가지 정보들을 비트 자릿수별로 묶은 32비트 정수를 되돌렸다.

그 정보의 의미를 C언어의 비트필드 구조체로 나타내 보면 대충 다음과 같다. 주석으로 표시된 숫자는 윈도 7 기준으로 반환되는 값들이다.
(최신 Windows 10 기준의 반환값을 소개하지 않은 이유는 후술하도록 하겠다)

union WINVERSION {
    DWORD dwValue;
    struct {
        UINT nMajorVer: 8; //6
        UINT nMinorVer: 8; //1
        UINT nBuildNumber: 15; //7601
        UINT bWin9xOrWin32s: 1; //0
    };
};

WINVERSION os;
os.dwValue = ::GetVersion();

이 함수는 아무 매개변수도 필요하지 않으며, 리턴값도 DWORD 달랑 하나이니 미치도록 가볍고 사용하기 편하다. Windows 9x와 NT 계열이 공존하던 옛날에, 지금 운영체제가 (1) NT 계열인지를 알고 싶다면 GetVersion()&0x80000000 (최상위 비트)만 하면 OK였다.
그 뒤, NT 3.x인지 4.0인지, 9x 계열의 경우 95인지 98인지 ME인지 같은 건 (2) major와 minor 번호를 보고 판별하면 됐다. (3) 빌드 번호는... 딱히 막 중요한 정보는 아닌 듯하다.

그러나 이 함수는 문제점과 한계도 보였다. 한눈에 봐도 각 비트로부터 의미 있는 정보를 추출하는 게 매우 지저분하고 번거로웠다. HIWORD, LOBYTE 삽질이 싫다면, 저런 비트필드 구조체는 프로그래머가 재량껏 알아서 만들어야 했으며, 응용 프로그램이 이 정보를 잘못 취급하는 경우도 많았다.

비교할 필요가 없는 필드까지 다 비교를 해 버리는 바람에, Windows 95 이상에서 모두 동작할 수 있는 프로그램이 Windows 95에서“만” 동작하게 고정돼 버리기도 했다. 혹은 Windows NT 4.0이 NT 3.51보다 낮은 버전으로 취급되는 촌극도 벌어졌다. (리틀 엔디언 기준으로 저 구조체를 보면, minor 버전이 major 버전보다 더 높은 자릿수에 놓여 있음)

더구나 운영체제의 정체성을 나타내는 정보는 단순히 버전 번호와 빌드 번호 이상으로 더욱 복잡해져 왔다. NT 계열의 경우 당장 서비스 팩이 있고, 이게 무슨 에디션인지도(홈? 서버? 워크스테이션? 등) 알 필요가 있는데 단순히 숫자 하나만 달랑 되돌리는 함수로는 그런 걸 알려 줄 수가 없었다.

이런 문제를 해결하기 위해 Windows 95 내지 NT 3.5에서는 OSVERSIONINFO라는 구조체를 인자로 받는 GetVersionEx라는 함수가 추가되었다. major, minor 버전 번호와 빌드 번호, 운영체제 계열이 모두 독립된 구조체 멤버로 독립하였으며, (4) 서비스 팩 내역도 완전한 문자열 형태로 되돌려 주니 버전 정보를 다루기가 편해졌다.

이 구조체는 맨 앞에 자신의 크기를 써 주게 돼 있으며, 덕분에 추후 확장이 가능한 형태이다.
Windows 2000부터는 OSVERSIONINFOEX 구조체가 추가됐다. 확장된 구조체는 서비스 팩의 번호조차도 major와 minor 꼴로 받을 수 있으며, (5) 같은 NT 계열 중에서도 클라이언트 라인과 서버 라인을 구분할 수 있다(wProductType==VER_NT_WORKSTATION / VER_NT_SERVER). Windows XP와 Server 2003은 버전 번호가 5.1과 5.2로 서로 달랐지만, 후대 버전부터는 버전 번호는 동일하고 이걸로 구분을 해야 한다. (Vista / Server 2008, 10 / Server 2016 같은..)

그리고 클라이언트 라인은 XP 이래로 오늘날의 10까지 (6) home과 pro 에디션 구분이 거의 관행이 돼 있는데.. 이건 wSuiteMask 멤버의 비트 플래그 VER_SUITE_PERSONAL (0x200)의 존재 여부로 판별 가능하다. 저 플래그가 존재하는 게 home 에디션이다.
VER_SUITE_* 다른 플래그들 중에는 Windows XP의 embedded 에디션, enterprise 에디션 같은 걸 나타내는 것들도 있으니 참고하면 된다.

요컨대 9x/NT 이후로도 클라이언트/서버, home/pro 같은 복잡한 구분이 계속 이어지는 것을 알 수 있다. 그래도 GetVersionEx 한 방이면 모든 정보를 얻을 수 있다.

이걸로 모든 이야기가 끝이 났으면 좋겠지만.. 아이고, 끝이 아니다. GetVersionEx 함수는 2010년대 이후로 마소의 정책상 사용이 더 권장되지 않는 deprecate 판정을 받고, 시간이 정지해 버렸다.
이 함수는 아무런 단서가 없는 환경에서는 Windows 8, 즉 버전 6.2보다 더 높은 값을 되돌리지 않는 샌드박스가 되었다. 실제로는 이 컴퓨터에 Windows 8.1이나 10이 돌아가고 있더라도 말이다. 이와 관련된 더 자세한 정보를 원한다면 다음 URL을 참고하시기 바란다.

이제 이 함수는 응용 프로그램에게 그 응용 프로그램보다 나중에 출시된 운영체제에 대한 정보는 주지 않기로 작정한 듯하다. GetVersionEx가 샌드박스 없이 실제 자기 버전을 되돌리는 조건은 다음과 같다.

  • 응용 프로그램의 manifest XML에(compatibility-application-supportedOS) 그 운영체제의 GUID가 등록되어 있다.
  • 혹은 응용 프로그램의 PE 헤더에 OS의 최소 요구 버전이 최신 운영체제의 버전으로 맞춰져 있다. Windows 8.1의 경우 6.3, Windows 10이라면 10.0이 되겠다.

운영체제와 함께 제공되는 메모장 같은 기본 프로그램들은 후자의 조치를 취한 상태이다. 이렇게 빌드된 프로그램에서는 GetVersionEx가 해당 버전을 정확하게 되돌린다. 하지만 이런 프로그램은 이전 버전 운영체제에서는 아예 전혀 동작하지 않으므로, 3rd-party 응용 프로그램이라면 이런 방법을 쓰기 곤란하다. 그러니 매니페스트 등록을 해야 한다.

물론 마소에서 2015년의 Windows 10부터는 기존 버전 번호 자체를 10.0으로 동결시켜 버리고 더 바꾸지 않기로 작정했다. 그러니 버전 번호 변경으로 인해 GUID를 또 등록하는 식의 혼란은 앞으로 더 없을 것이다.

운영체제의 버전의 절대값을 되돌리는 GetVersionEx 대신 마소에서 사용을 권장하는 함수는... 지금 운영체제의 버전이 응용 프로그램이 제시하는 버전보다 상대적으로 높은지 안 높은지 여부만을 되돌리는 VerifyVersionInfo 함수이다. 그리고 이걸 기반으로 IsWindows10OrGreater 같은 helper 함수들도 만들어져 있다. (VersionHelpers.h)

하지만 이 함수들도 내부적으로 GetVersionEx의 결과값을 기반으로 비교를 하는 것이기 때문에 앞서 언급한 샌드박스의 제약을 받는 건 마찬가지이다.

샌드박스 없이 운영체제의 정확한 버전을 얻어 오는 함수는 크게 두 군데에 있다.
먼저, 의외로 네트워크 API이다. 그렇다고 소켓 API 같은 건 아니고, Windows에서 독자적으로 제공하는 함수 중에 내 로컬 컴퓨터를 포함하여 원격 컴퓨터에 설치된 운영체제의 버전을 얻어 오는 함수가 있다. 대략 다음과 같이 코드를 작성하면 된다.

#include <LM.h>
#pragma comment(lib, "netapi32")

WKSTA_INFO_100 *p;
::NetWkstaGetInfo(NULL, 100, (LPBYTE *)&p);
printf("%d, %d\n", p->wki100_ver_major, p->wki100_ver_minor); //10, 0
::NetApiBufferFree(p);

저기 100은 수효를 나타내는 게 아니며 각각의 숫자들이 별개의 의미를 지님에도 불구하고, 상수 명칭이 존재하지 않아서 그냥 생으로 100을 넘겨 줘야 한다.
운영체제 버전 하나 좀 얻자고 웬 생뚱맞은 분야의 API를 써야 하는 것도 삽질스럽지만.. 저 함수를 통해서는 그냥 major와 minor 버전 번호만 얻을 수 있다. 서비스 팩이나 빌드 번호 같은 세부 정보는 얻을 수 없다.

저거 말고 다른 대안으로는.. ntdll.dll에 있는 native API인 RtlGetVersion을 써도 된다.
OSVERSIONINFO(EX)의 포인터를 받아들이고 정수값을 리턴하므로 prototype이 기존 GetVersionEx와 거의 동일하다.
단, native API 버전은 성공한 경우의 리턴값이 0이다. 리턴 타입이 BOOL이 아닌 셈이다.

얘는 Windows 8.1 내지 10 같은 요즘 운영체제에서는 잘 동작하는데, 과거의 Windows 2000에서는 GetVersionEx와 달리 서비스 팩 정보를 되돌리지 않았던 것으로 기억한다. 구형 OS에서는 오히려 기존 함수를 쓰는 게 더 낫다. 거 참..;;
Windows가 지난 20년 동안 운영체제의 버전과 제품 종류를 얻는 그 단순한 절차만 해도 얼마나 복잡하고 지저분해져 왔는지를 확인할 수 있다. 관련 여담을 몇 가지 더 남기는 것으로 글을 맺고자 한다.

  • OSVERSIONINFOEX는 C++ 상속 문법 같은 걸 이용해서 선언된 게 아닌 관계로, OSVERSIONINFO와는 언어 차원에서 아무런 연결 고리가 없다. GetVersionEx에다가 전달할 때는 OSVERSIONINFO*로 reinterpret_cast를 해 줘야 된다.
  • 과거 Windows XP에는 media center 에디션 내지 태블릿 PC 에디션 같은 바리에이션이 있었는데.. 이거 여부를 얻는 건 GetVersionEx가 아니라 GetSystemMetric라는 다소 생뚱맞은 함수에 있었다. SM_MEDIACENTER, SM_TABLETPC처럼 말이다 .
  • 끝으로, Windows 10부터는 (7) 릴리스 연-월을 나타내는 4자리 숫자가 사실상 버전 번호가 됐으니 이걸 표시해 줘야 할 것이다. 그런데 이건.. 본인이 아는 방법은 그냥 무식한 레지스트리 조회가 유일하며, 공식적인 API가 따로 있지 않다.;;;

Posted by 사무엘

2019/03/14 08:36 2019/03/14 08:36
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1596

« Previous : 1 : 2 : 3 : 4 : 5 : ... 18 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2019/10   »
    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:
1267033
Today:
166
Yesterday:
552