비주얼 C++ 2010 외

※ auto keyword

지금까지 signed와 더불어서 C/C++의 주요 '유명무실' 사문 예약어이던 auto가 비주얼 C++ 2010에서는 완전히 새롭게 거듭날 예정이다.
auto는 그 변수에 초기화해주는 값이 나타내는 타입을 그대로 부여해 준다. 즉,

int a=5; (...) auto b=a; 라면 b는 int형이 되고
vector<int> arr; auto itor=arr.begin(); 이라면 itor의 타입은 vector<int>::iterator 가 된다.

그래서 말 그대로 auto는, 뒤에 나오는 대입값을 보고 자료형을 '자동'으로 알아서 판단하라는 의미가 된다.
이런 게 언제 C++ 표준으로 상정됐는지는 모르겠는데, 기능과 문법을 만들어 놓은 방식이 정말 기발하고 참신하기 그지없다.
이 키워드 하나가 수많은 번거로운 자료형 하드코딩 타이핑 내지 typedef들을 예방해 주리라 기대된다.

당연한 말이지만 auto 자체는 sizeof 같은 연산자의 피연산자가 될 수 없다. 또한 auto가 무슨 비주얼 베이직의 variant 타입처럼 아무 자료형이나 수시로 집어넣을 수 있는 타입을 의미하지는 않는다.
auto 형으로 선언되는 개체나 변수는, 마치 참조자를 선언할 때처럼 반드시 선언과 동시에 값 대입 내지 초기화가 되어야 할 것이다.
C++은 함수의 인자 개수와 인자의 자료형으로만 오버로딩을 구현하지, 함수의 리턴값으로 오버로딩을 구현하지는 않기 때문에 이런 기능을 모호성 없이 구현할 수 있는 것이다.

이 외에 다른 이상한 C++ 문법도 도입되고 C++ 라이브러리 쪽에도 무슨 변화가 있는데 그건 잘 모르겠고,
프로젝트 파일 포맷이 또 바뀔 예정이라고 한다.

※ 비주얼 C++ 프로젝트 파일

여기서 잠깐 역사 수업. 비주얼 C++ 프로젝트 파일의 역대 계보는 다음과 같다.

1기 *.vcp는 16비트 1.x 초창기 시절에 쓰였다.
2기 *.mdp는 32비트 통합 IDE인 Developer Studio 초창기 시절인 4.x에서 쓰였다. 세월이 세월이니만큼, 1기와 2기는 지금은 거의 찾을 수 없는 전설의 파일 포맷이 돼 있다.
3기 *.dsw와 *.dsp는 드디어 여러 프로젝트를 한데 묶어 관리하는 워크스페이스라는 개념이 도입된, 5와 6 두 버전에서 쓰였다.
그 후 닷넷급이라 할 수 있는 비주얼 C++ 200x에 가서는 4기인 *.sln, *.vcproj 방식이 등장했다. 비주얼 베이직/C++/C# 모든 언어들의 프로젝트 사이에 일관성이 생겼으며 워크스페이스라는 명칭이 솔루션으로 바뀌었다. 그리고 프로젝트 파일은 내부 구조가 XML 형태로 바뀌었다.

이제 프로젝트 파일의 내부 구조가 또 바뀔 일은 없을 거라고 생각했는데 2010에서는 vcxproj라고 또 바뀔 거라고 하니 이게 웬일?
설마 MS 오피스 2007처럼 또 zip 압축을 하는 건 아닌가 모르겠다.
하지만 개발툴의 프로젝트 파일은 소스 버전 관리의 편의성 같은 이유도 있고 해서 다들 plain text 방식으로 저장하지, 바이너리 방식을 쓰지는 않을 텐데 말이다.

더구나 개발툴의 버전이 바뀔 때마다 각종 컴파일러 옵션들은 시시때때로 추가되거나 삭제, deprecate되기도 하기 때문에, 이런 프로젝트 파일들은 유연성, 하위 호환성, 확장성을 최대한 유지하는 게 짱이다. 즉 plain text가 정답이라는 뜻이다.

경험상 비주얼 C++ 2005하고 2008은 프로젝트 파일 포맷이 거의 차이가 없다.
sln과 vcproj 파일 앞줄에 있는 버전 번호만 1 줄여 주면, 2008로 만든 프로젝트도 2005에서 아무 문제 없이 바로 읽어들일 수 있다.

※ 비주얼 C++ 6.0이 나온 지 좀 있으면 벌써 12주년

얘기가 여기까지 왔는데 또, 애증이 교차하는 비주얼 C++ 6.0 얘기를 하지 않을 수 없다.
무려 11년 전엔 1998년에 출시되었고 아직까지도 애용되고 있는 소프트웨어로는 스타크와 더불어 양대 산맥이 아닌가 생각된다.
너무 구닥다리가 되어 외형이 후져 보이고(비록 당대로서는 MS 오피스 97 스타일의 새끈-_-한 UI였지만),
64비트 지원 안 되고 최신 하드웨어 기술 및 C++ 표준이 반영 안 돼 있는 것 자체만 빼면 정말 잘 만든 물건이긴 하다.

가끔은 6.0 특유의 그 클래스 마법사가 그리워질 때도 있다.
MFC는 이제 <날개셋> 타자연습 소스 열어볼 때 말고는 별로 접할 일도 없어졌구나.

그 비주얼 C++이 곧 있으면 연도도 2010이고 버전 번호로도 10.0이 나올 예정이다. 감회가 새롭다.
참고로 비주얼 C++ 2005는 2005년 하반기에 나왔지만 2005라는 타이틀이 붙은 반면,
2008은 비슷한 2007년 하반기에 출시됐지만 1이 더해진 2008이라는 타이틀이 붙었다.

※ 여자 프로그래머

7월 1일.. 경의선 전철 개통, 용인-서울 고속도로 개통, 거기에다 비정규직법 상정 2주년-_- 등 여러 굵직한 일이 많았는데, 나도 이제 병특 끝난 지 1주년이 됐다.
그리고 직장 생활 경력이 3년을 넘어갔다. 최소한 말단 신입사원 딱지는 떼는 짬밥이 됐고, 주임/대리급을 바라보는 신분이다. 하지만 옛날 회사나 지금 회사나 여전히 제일 나이가 어리고 내 밑으로 신입을 대한 적이 없으니, 예나 지금이나 늘 막내이다.

그리고 내가 지금까지 다닌 회사에서 여자 개발자를 본 적은 전혀 없다.
IT 회사라고 해도 여직원 하면 오로지 디자인, 기획 아니면 회계/경리이다. 개발자가 여자인 경우는.. 글쎄다.
하지만 외근 나간 연구소에서는 그래도 비주얼 C++ 띄워 놓고 코드와 씨름하고 있는 여자분들을 주변에서 여럿 볼 수 있어서 인상적이었다.

차라리 학교에 있을 때는 그런 모습을 심심찮게 볼 수 있었다. 내가 나온 학교는 공대 중에서 성비가 상당히 균형 잡힌-_-;; 편에 드는 곳이었으며, 전산과 역시 예외가 아니었기 때문이다.
하지만 나는 학교 시절엔 과 친구들하고 거의 어울리지 않고 동아리 같은 것도 안 하고 지냈기 때문에 그런 학우들로부터는 이렇다할 '임팩트'를 받지 못했다.

여직원하고 "아.. 그 땐 이런 함수를 쓰면 돼요"
"이 클래스는 여기서 상속 받았고 이 가상 함수는 이런 용도로 오버라이드하면 됩니다. 대략 이렇게 구현을 하면 어떨까요?"
"아 그렇게 생각해 보니 이렇게 짜면 버퍼 오버런이 우려되고, 멀티스레드 환경에서는 잠재적으로 또 문제가 있겠군요"
이런 이야기를 나눈다면.. 졸라 감회가 새로울 것 같다. 태어나서 이성하고 그런 얘기는 한 번도 한 적이 없으니까. =_=

하긴, 부부 교사도 있고,
커플이 나란히 코레일에 입사하여 기관차 운전 연수를 받던 부부 기관사도 봤는데, (촬영 당시 곧 결혼 예정이라고 쓴 걸 봤음)
부부 개발자라고 없을 이유는 없으리라 생각한다. -_-

Posted by 사무엘

2010/01/11 10:10 2010/01/11 10:10
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/86

레거시 코드

꽤 희귀한 기회 덕분에,
한때 시대를 풍미했던 상업용 소프트웨어의 소스를 구경하게 됐습니다.

윈도우 3.1에서 돌아가는 녀석이었거든요.
저야 도스박스에다 윈도우 3.1 + 비주얼 C++ 1.5도 갖추고 있어서 이걸로 돌려도 되지만
32비트로 포팅을 해 보고 싶어서 최신 개발툴로 프로젝트를 만들고 빌드를 해 봤습니다.
당연히 바로 빌드는 안 되고 수백여 군데에서 에러가 나는데...
16비트 윈도우 코드의 특징을 한눈에 알 수 있었습니다.

1. precompiled header가 없었는지 매 소스 파일마다 <windows.h> 인클루드.

2. 말로만 듣던 far, huge 포인터의 압박. 메모리 모델별로 malloc이라든가 포인터를 다루는 함수도 따로 존재. _fmalloc 같은.

3. POINT 구조체. 옛날엔 POINT 구조체의 x,y 멤버가 16비트 크기였기 때문에 32비트 long 하나로 POINT를 나타내는 게 관행이었으며, 심지어 lParam *을 POINT *로 바로 캐스트하는 MAKEPOINT 같은 매크로까지 존재했습니다. 하지만 지금은 좌표계가 32비트로 커지고 이것 때문에 MoveTo, SetWindowOrg, SetViewportExt 같은 함수들은 모두 Ex가 붙은 형태로 프로토타입이 바뀌게 됐죠.

4. 옛날에는 핸들(HANDLE, HINSTANCE, HGDIOBJ, HWND 등등)은 전부 void *의 typedef였고, 아무 형변환 없이 마음대로 섞어 썼습니다. 이 폐해를 막으려고 지금은 STRICT(엄격한 타입 구분)라는 개념이 추가됐지만 어마어마한 분량의 레거시 코드를 컴파일시키려면 그 옵션을 꺼야 하죠.

5. WM_CTLCOLOR 메시지. 옛날에는 이 메시지 하나만 왔지만 지금은 WM_CTLCOLORBTN, DLG, EDIT, LISTBOX 등으로 세분화됐습니다. 다만, 16비트 관행을 물려받은 MFC는 여전히 OnCtlColor 라는 한 함수로 이 메시지들을 모두 처리합니다.

6. 멀건 윈도우 3.1 대화상자에다가 은빛 3차원 효과를 입혀 주는 ctl3d.dll과 교신을 하는 흔적. 그러고 보니 그때는 이런 효과가 지금의 공용 컨트롤 6.0 매니페스트 같은 그런 매개체였던 것 같습니다. MFC에도 CWinApp 클래스에 Enable3dControls 같은 멤버 함수가 있을 정도였는데, 이제는 완전히 deprecated로 처리되죠.

7. DLL도 아니고 EXE에 웬 export 속성이 지정된 함수? 그것도 __declspec(dllexport) 같은 지금 통용되는 문법으로 작성된 것도 아닙니다.
16비트 윈도우 EXE는 헥사 에디터로 들여다보면, 윈도우/대화상자 프로시저처럼 운영체제로부터 호출을 받는 콜백 함수들의 이름을 따로 name table에다 등재를 해 놓는 경우가 많더군요. 꼭 그렇게 할 필요는 없는데 왜 그러는지 궁금.. 디버깅 정보와 관련이 있는지, 아니면 속도를 높이려고 그러는지 이유는 모르겠습니다.

8. C++ 문법이 바뀌어서 옛날에는 허용되었으나 지금은 허용되지 않는 몇몇 문법들

맥이나 리눅스 같은 다른 플랫폼들은 도스 같은 극악의 호환성 발목은 없었겠지만, 16비트를 겪은 시절조차 전혀 없었는지가 문득 궁금해지네요.

참고로 저는 C/C++은 32비트 도스 익스텐더 환경에서(왓콤, DJGPP) 처음 시작했고 16비트는 거의 접한 적이 없는 세대입니다. ^^

Posted by 사무엘

2010/01/11 09:58 2010/01/11 09:58
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/79

thread local storage

컴퓨터 프로그램에서 뭔가 데이터를 저장할 기억 장소를 확보하는 방법은 크게 다음과 같은 세 가지이다.

먼저, 함수 안에서 선언하는 지역 변수이다. 지역 변수는 스택에다 할당되며, 그 함수와 사실상 일심 동체이기 때문에 거듭 실행되면 계속 스택을 점유하여 기억 장소도 계속 할당된다. 따라서 스레드 충돌 걱정을 할 필요도 없으며, scope을 벗어나면 소멸도 100% 자동으로 되기 때문에 memory leak가 생길 여지도 전혀 없다. 끼칠 수 있는 영향의 범위 자체가 가장 좁다.

그리고 전역/static 변수가 있다. 이들은 scope이 말 그대로 굉장히 정적이며, 프로그램 실행 기간 내내 살아 있고 프로세스 내부의 모든 스레드들이 공유한다. 잘 알다시피 static은 메모리에 저장되는 형태는 전역 변수와 완전히 같고, 접근성은 지역 변수와 같은 중간 존재이다.
이런 변수가 C++ 개체라면, 생성자나 소멸자 함수가 호출되어야 하는데 이는 main 함수의 실행을 전후하여 CRT 라이브러리가 알아서 해 준다.

마지막으로 heap에다 할당되는 동적 메모리가 있다. 이것은 가장 동적이고 자유로운 야생마 같은 형태의 메모리로, 프로그램 언어 차원에서 보장해 주는 scope이란 개념이 없다. 전적으로 프로그램이 양도 얼마든지 할당하고 아무 스레드에서나 해제 가능하다. 그 대신 C/C++에서 온갖 포인터 관련 버그, memory leak의 온상이 되는 존재이기도 하다.

그런데 멀티스레드 환경이 보편화하면서 첫째와 둘째의 또다른 형태의 중간에 속하는 scope이 필요해진 게 있으니, 바로 한 스레드 안에서만 global인 공간이다.
사실 표준 C 라이브러리조차도 멀티스레드 환경을 고려하지 않고 디자인되어 지금 문제되고 있는 요인들이 적지 않다.

함수의 실행 결과를 나타내는 에러 코드를 예전에는 그저 전역 변수 하나에만 집어넣으면 됐지만 멀티스레드 환경에서는 최소한 스레드마다 독립된 공간에 저장해야 한다.
strtok 함수는 예전에는 토큰 해석 위치를 그저 static 변수에다가 집어넣으면 됐지만, 이랬다가는 여러 스레드가 동시에 이 함수를 호출했을 때는 재앙을 각오하야 한다.
qsort 함수는 콜백 함수만 달랑 받지 콜백 함수에 따로 사용자가 지정한 데이터 따위를 넘겨 주지는 않는다. 결국 콜백 함수가 외부로부터 정렬 옵션 같은 걸 얻고자 한다면 전역 변수로부터 참고를 해야 하는데, 서로 다른 정렬 옵션으로 여러 스레드가 동시에 qsort를 호출하면 어떤 상황이 벌어지게 될까?

이런 기초적인 문제를 해결하기 위해 멀티스레드 운영체제는 thread local storage를 관리하는 기능을 제공한다. 윈도우 API에서는 TlsAlloc()으로 TLS 슬롯 번호를 하나 받아서 그 값을 전역 변수로 저장한다. (해제는 나중에 TlsFree()로) 그래서 TlsGetValue()와 TlsSetValue()에다가 아까 받은 TLS 슬롯을 넘겨줌으로써 machine word와 같은 크기의 정수/포인터 값을 읽고 쓰면 된다.

한 프로세스 안에서 동일한 슬롯 번호이지만, 이 함수를 호출하는 스레드가 무엇이냐에 따라 서로 제각기 독립된 값을 읽고 쓸 수 있음이 보장된다는 것이다. 스레드별로 공유해야 하는 값이 그냥 정수 하나라면 그 값을 바로 집어넣으면 되지만, 더 큰 영역의 메모리라면 따로 heap에다 할당한 메모리의 포인터를 집어넣으면 된다.

한 프로세스가 가질 수 있는 TLS 슬롯은 윈도우 95/NT4 시절에는 64개였다. 그러나 윈도우 2000/XP/비스타급에서는 수백, 1천 개가 넘어가도 괜찮게 바뀌었다. 나뿐만이 아니라 한 프로세스에 같이 로드되어 있는 다른 수십 개의 DLL들이 다 TLS 슬롯을 요청하기도 하기 때문에 TLS 슬롯 요청은 최소화하는 습관을 들여야 한다. TLS는 EXE보다도, 내가 생성하지 않은 스레드에 느닷없이 붙어서 돌아가야 하기도 하는 DLL을 만들 때 더욱 필요한 존재이다.

CRT 라이브러리의 DLL 에디션도 strtok 같은 함수를 구현하기 위해 TLS를 사용한다. 직전의 토큰 위치를 TLS 슬롯이 가리키고 있는 별도의 메모리에다 저장해 놓고 그때 그때 참고한다는 것이다.
또한 qsort처럼 콜백 함수가 사용할 데이터를 별도로 얻는 방법이 없는 함수를 사용하면서도 스레드 안전은 보장되게 코드를 짜야 할 때, 그 데이터를 TLS에다가 저장해 놓은 뒤 내가 짠 콜백 함수는 그 TLS를 사용하도록 만들 수도 있다.

TlsGet/SetValue()는 일종의 없는 scope을 새로 구현하는 함수이다. 그렇기 때문에 함수 실행 결과값을 기존 scope에 속하는 전역/지역 변수에다가 저장해 놓는다는 게 완전 무의미하다. 그러므로 콜백 함수가 TLS를 사용한다면 이 함수도 매번 무지막지한 빈도로 호출해야 한다. 따라서 이 함수는 다른 어떤 함수들보다도 성능에 초점을 맞춰 구현되어 있으며, 인자값의 검증을 거의 하지 않는다고 MSDN에 명시되어 있기도 하다.

물론 근본적으로 비주얼 스튜디오 2005에서 도입된 strtok_s는 TLS를 꺼낼 필요가 없이 토큰 위치를 아예 별도의 인자로 넘겨준 포인터에다 저장하도록 프로토타입이 바뀌어, 문자열을 중첩적으로 토큰화할 수도 있게 됐다.
그리고 qsort_s는 콜백 함수에다 넘겨줄 데이터를 별도로 같이 지정도 할 수 있다. 이렇게 하는 게 전역 변수급(TLS 포함) 메모리의 사용을 줄이고, 가능한 한 함수 인자와 지역 변수만 사용함으로써 모듈 사이의 독립성도 높이는 좋은 방법일 것이다.

DLL의 함수를 매번 LoadLibrary, GetProcAddress, FreeLibrary로 퍼 와서 쓰는 게 불편하기 때문에 import library가 존재하듯이, TLS도 비주얼 C++의 컴파일러 확장을 이용하여 좀더 쉽게 사용하는 방법이 있긴 하다. __declspec(thread)로 선언된 변수는 컴파일되는 바이너리의 내부에 .data, .text 처럼 섹션이 .tls라고 하나 더 생기고 거기에 보관된다. 하지만 이건 운영체제 관점에서 오버헤드가 굉장히 크고 옛날 운영체제에서는 동작하지 않기도 해서 잘 쓰이지 않는다.
#pragma data_seg로 섹션을 하나 더 만들어서 share 속성을 주어, 메모리 맵을 쓰지 않고 모든 프로세스에서 공유되는 메모리 영역을 만드는 것과 비슷한 차원인데, 물론 사용되는 기술 계층은 서로 차이가 있다.

우리가 다른 프로세스에서 멀티스레드로 동작하는 DLL을 만들고 있는데 우리 프로그램이 스레드마다 꽤 복잡한 C++ 오브젝트라든가(별도의 메모리 내지 리소스의 할당/해제가 필요한) 대용량 오브젝트를 운영해야 한다면 어떻게 해야 할까? TLS 슬롯에는 그 포인터밖에 집어넣을 수 없으며 메모리는 별도로 할당해야 한다. 그러나 디버거 쪽 훅을 설치하지 않는 이상 스레드의 생성, 소멸을 일일이 다 감시할 수는 없다. 그 대신 스레드마다 새로 생성된 TLS 슬롯의 초기값은 언제나 0임이 보장되기 때문에 오브젝트의 생성은 TlsGetValue의 값을 매번 검사하여 NULL일 때 하면 된다.

소멸은 약간 주의해야 한다. 프로세스의 실행 중에 스레드가 하나 실행이 끝났다면 DllMain에 DLL_THREAD_DETACH 통지가 오기 때문에 그때 우리 스레드 TLS 슬롯이 가리키는 포인터를 해제해 주면 되나, 문제는 모든 스레드의 소멸에 대해 일일이 이런 통지가 오지는 않는다는 것이다. primary thread의 실행이 종료됨으로써 DLL_PROCESS_DETACH 한 방으로 끝인 경우도 빈번하기 때문이다.

한 스레드가 여타 스레드의 모든 TLS 슬롯 값들을 enumerate하는 방법은 윈도우 API에 존재하지 않는다. 그렇기 때문에 각 스레드가 갖고 있는 포인터들은 별도의 전역 변수 링크드 리스트 같은 데에 별도로 관리하고 있다가 프로세스가 단번에 종료되면 이들을 한꺼번에 해제도 해 줘야 한다. 어차피 프로세스가 종료되는 마당에 memory leak 정도야 문제될 게 없지만, inter-process한 커널 오브젝트처럼 "cleanup"이 반드시 제때에 이루어져야 하는 자원도 있을 수 있기 때문이다.

물론 가장 이상적인 경우는 DLL을 만드는 우리가 매 스레드별로 Init 내지 Uninit 함수를 만들어서 우리 DLL을 사용하는 응용 프로그램이 스레드 실행의 시작과 종료를 알려 주는 것이다. 이렇게만 하면 모든 논란이 일축된다.

Posted by 사무엘

2010/01/11 09:53 2010/01/11 09:53
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/75

스레드 동기화


컴퓨터가 충분히 발전하여 오늘날의 운영체제 시스템 개념을 실현하게 된 것은 중앙 처리 장치가 최소한 32비트로 커지고부터이다. 보호 모드, 가상 메모리, 스레드 같은 것들. 4GB 정도는 돼야 주소 공간이 아쉬운 대로 넓다고 말할 수 있기 때문이다. 아무리 메모리 부족하고 열악한 임베디드 기기라 해도, 일단 최소한 디스플레이가 있는 general-purposed 컴퓨터라면, 16비트 시절의 원거리/근거리 포인터 같은 기법을 재도입할 정도로 열악하지는 않다.

물리 메모리와 가상 메모리의 대응을 운영체제가 CPU 차원의 지원으로 즉각 수행할 수 있게 되고 시분할 동시작업도 하드웨어 차원에서 할 수 있게 된 것은 소프트웨어 개발자에게 매우 큰 편의와 잠재성을 제공하게 되었다. 이를 입증하는 윈도우 3.1과 95의 큰 차이는 마우스 포인터 모양만 봐도 알 수 있다. 옛날에는 프로그램을 로딩하고 있을 때는 마우스 포인터가 완전 모래시계로 바뀌고 시스템 전체가 응답 불가 상태였지만, 지금은 화살표 오른쪽에 작은 모래시계가 붙고 시스템은 여전히 사용 가능하다(물론 좀 느려지기는 하지만). 또한, 모래시계 포인터는 해당 프로그램 안에서만 모래시계이지 다른 응용 프로그램에서는 여전히 화살표이다. 이른바 선점형 멀티태스킹 덕분이다.

단일 스레드 환경에서 사용자의 입력도 받으면서 작업도 동시에 수행하려면 타이머로 정말 찔끔찔끔 작업을 하거나, 작업 루틴 내부에서 수시로 입력을 체크(윈도우 API로는 PeekMessage 같은)해 주어야 했다. 효율은 효율대로 떨어지고 코드는 더욱 복잡해질 수밖에 없었다. 멀티스레드에서는 그런 번거로운 짓을 할 필요가 없다. 하지만 단일 스레드에서 전혀 신경쓸 필요가 없던 또다른 걱정거리가 생겼는데, 그것은 바로 스레드 동기화이다.

실행되고 있는 여러 스레드에 CPU 시간이 어떻게 분배되는지는 정말 전혀 예측할 수 없다. 심지어 a++; 같은 간단한 문장도 기계어로는 두세 개로 번역되는데 이 명령을 수행하던 도중에 스레드의 context가 바뀔 수가 있다. 여러 스레드가 동일한 데이터에 동시에 접근하여 값을 읽고 쓰다 보면, 다른 스레드에 의해 계산 전 값이 덮어써지거나, 아직 처리가 덜 끝난 중간 상태가 다른 스레드에 의해 뒤엉켜 버릴 수가 있다.

쉽게 말해서 a=0이고 a의 값을 1 증가시키는 스레드를 100개를 만들어서 아무 통제도 없이 이들을 동시에 실행시켰다고 가정해 보자. 그렇다면 스레드 실행이 모두 끝난 후 a의 값이 100이 되어 있을까? 천만의 말씀이다. 한 스레드가 a의 값이 0임을 인지만 한 찰나에 다른 스레드로 context가 넘어가고, 그 스레드 역시 a의 값을 0으로 인식하게 된다. 그러면 두 스레드는 모두 1이라는 값을 기록하게 되고.. 그렇다면 a는 도저히 100이 될 수가 없어짐이 명확하다. a++ 같은 지극히 단순한 연산이 이러한데 하물며 링크드 리스트나 트리 같은 복잡한 자료 구조를 변형하는 루틴에 여러 스레드가 동시에 접근한다면 어떤 재앙이 벌어질까?

멀티스레드 환경에서는 필연적으로 한 데이터를 여러 스레드가 공유하게 된다. 문서를 입력 받는 GUI 스레드와, 내부적으로 문서의 맞춤법 검사를 하는 스레드. 작업을 진행하는 스레드와 그로부터 작업 상황을 출력하는 스레드 등. 컴퓨터는 사용자가 프로그램을 짜 준 작업 중 어느 구간이 반드시 원자성이 보장되어야 하는지, 어느 구간이 반드시 여러 스레드들이 순차적으로 실행되어야 하는지를 알지 못한다. 이는 사용자가 지정해 주어야 하며, 운영체제는 스레드 동기화를 위한 여러 기법들을 제공하고 있다.

아무 통제 없는 무법천지인 멀티스레드 환경만으로는 프로그래머가 뭔가 의미 있는 작업을 하는 게 불가능하기 때문이다. 흔히 멀티스레드 환경을 여러 사람이 사용하는 한 화장실에다 비유하는데 무척 재미있고 적절한 비유 같다. 사람은 바글바글한데 화장실 문을 잠글 수 없다면 누가 감히 용변을 볼 수 있을까?

먼저, 여러 스레드들이 공유하여 시시때때로 값을 바꿀 수 있는 변수라면 C/C++의 경우 volatile로 선언하는 것이 필수이다. 고급 언어로 한 메모리 영역으로 표현되는 변수라 할지라도, 기계상으로는 메모리와 레지스터라는 차이가 존재할 수 있기 때문이다. 아울러 멀티스레드를 고려하다 보면 프로그램 모듈간의 독립성도 결국 고려하지 않을 수 없게 된다. 당장 쓰기 쉽다고 습관적으로 남발하던 전역/static 변수들은 멀티스레드 환경에서는 재앙으로 돌아올 가능성이 높다.

윈도우 운영체제는 정수 변수값을 1 증가하거나 감소시키는 것을 원자성이 보장되게 수행해 주는 InterlockedIncrement/Decrement라는 함수를 제공한다. 앞서 말했듯이 심지어 a++; 같은 간단한 연산조차도 컴퓨터에서는 CPU 한 사이클로 해결되지 않으며, 연산 수행 도중에 스레드 context가 바뀌고 결과가 꼬일 수 있다. 여러 스레드가 동시 접근할 수 있는 COM 오브젝트를 만든다면 reference count를 관리하는 함수를 이 함수를 이용해서 만들면 될 것이다. 고작 숫자를 1 더하고 빼는 것밖에 없지만 이것이 운영체제가 제공하는 가장 간단한 형태의 스레드 동기화 기능이라고 볼 수 있다.

이것보다 좀더 복잡하고 임의적인 루틴에 동기화 제동을 걸고 싶다면 임계 구간(critical section)이 좋은 선택이 될 수 있다. 크리티컬 섹션 오브젝트를 전역 변수로 선언하여 초기화해 준 후, 한 번에 한 스레드만이 차례대로 접근해야 하는 구간의 앞에 EnterCriticalSection을 해 주고, 끝에 LeaveCriticalSection을 해 주면 된다. 방법도 쉽다. 아까의 Interlocked* 함수는 크리티컬 섹션이 가미된 a++ 연산이라고 보면 된다.

이 정도면 다 된 것 같은데 동기화와 관련해서 또 필요한 게 있을까? 크리티컬 섹션은 간단하고 쓰기 쉽고 성능도 매우 좋은 반면, 타 스레드가 임계 구간을 빠져나올 기미가 안 보이더라도 한없이 기다리는 수밖에 없다. 또한 동일한 코드에 대한 스레드 접근만 제어할 수 있지, 다른 스레드(심지어 다른 프로세스)가 임의의 다른 작업을 끝낼 때까지 나의 스레드 실행을 멈추는 식으로 제어를 할 수는 없다.

이쯤 되면 그림이 나온다. 스레드 동기화 기능들을 수행하는 중요한 역할 중 하나는 busy waiting을 없애는 것이다. XT, 286 시절에는 컴퓨터 실행을 좀 느리게 하기 위해서 for i=1 to 5000: next 같은 문장을 썼지만 지금은 그건 큰일 날 짓이다. 자발적으로 일정 시간 CPU 시간을 내어 주고 프로그램 실행을 멈추는 Sleep 같은 운영체제 함수를 써야 한다. 어떤 스레드가 작업을 끝낼 때까지 쉬는 것도, while(bProcessing); 이런 식으로 수도 없이 뺑뺑이를 도는 polling은 절대 피해야 한다.

그렇기 때문에 일정 조건이 만족될 때까지 스레드를 CPU의 사용 없이 자동으로 기다리게 하는 함수가 있으며 이와 관련된 스레드 동기화 오브젝트들이 커널 차원에서 제공된다. “어떤 스레드가 임계 구간을 빠져나갈 때까지”란, 그 조건의 subset일 뿐인 것이다.

커널 오브젝트는 단순 크리티컬 섹션보다는 느리다. 이를 사용하기 위해서는 user 모드와 kernel 모드 사이의 전환 overhead를 감수해야 한다. 하지만 이들은 쓰임이 훨씬 더 범용적이며, 단순한 변수가 아니라 핸들과 이름으로 식별하기 때문에 inter-process하며 시스템 전체에서 통용이 가능하다. (이런 커널 오브젝트의 존재 여부로 프로그램의 중복 실행을 감지할 수도 있을 정도이다.) 크리티컬 섹션은 스레드의 waiting이 아주 implicit하게 일어나지만, 커널 오브젝트는 이를 WaitForSingleObject 같은 함수로 explicit하게 지정 가능하며 기다리는 한도를 제어할 수 있다. 또한 다른 커널 오브젝트들과 덩달아 함께 기다리게 할 수도 있다.

뮤텍스는 크리티컬 섹션의 커널 오브젝트 버전에 가깝고, 세마포어는 임계 구간에 들어갈 수 있는 스레드의 최대 개수를 지정할 수 있어서 뮤텍스보다 더욱 범용적이다.
한편 이벤트는 굳이 임계 구간이 아니더라도 사용자가 임의의 신호를 날려서, 그 신호를 기다리며 잠들어 있던 스레드를 깨울 수 있게 한 원초적이고 general-purpose한 동기화 수단이다.

결국 스레드 동기화 수단이 여럿 등장했는데, 구현 형태는 달라도 결국 여기에 적용될 수 있는 동사는 딱 둘.. ‘잠그기, 풀기’로 요약된다. 그래서 MFC는 이런 것들을 CSyncObject라는 클래스로 요약하여, Lock과 Unlock이라는 가상 함수로 공통 기능을 추상화해 놓고 있다.

참고로 이런 것뿐만 아니라 프로세스, 파일, 스레드 핸들 그 자체 같은 다른 커널 오브젝트들도 동기화 오브젝트에 해당하는 것이 여럿 있다. 그래서 해당 프로세스의 실행이 끝날 때까지, 혹은 스레드의 실행이 끝나고 파일 트랜잭션이 끝날 때까지 프로그램을 기다리게 하는 게 가능하다.

Posted by 사무엘

2010/01/11 09:52 2010/01/11 09:52
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/74

윈도우 API로 다른 프로그램을 "실행", 즉 기술적으로 말하면 프로세스를 생성할 때 쓰는 함수는 크게 다음과 같다.

WinExec, LoadModule, CreateProcess, ShellExecute

어느 것을 사용하더라도 다음과 같은 정보를 받는 란은 꼭 존재한다:
실행 파일 이름, 명령인자, 그리고 메인 윈도우를 표시할 디폴트 방식(SW_SHOW 등).

즉, 윈도우 운영체제는 GUI 기반이라는 특성상 프로그램 메인 윈도우를 기본적으로 최대화하여 실행하라, 창을 숨겨 놓고 실행하라는 식의 지시를 내릴 수 있다. 이런 정보들은 WinMain 함수의 매개변수로 고스란히 그대로 전달된다. 물론 응용 프로그램이 그걸 무시하면 어쩔 수 없지만.

앞의 W..., L... 두 함수는 매개변수가 매우 단순한 편해서 쓰기는 편하나, 16비트 API 시절의 잔재로 치부되어 유니코드 버전조차 존재하지 않으며, 사용이 비추(discourage)되고 있다.

CreateProcess가 32비트 이상급 윈도우 API에서 표준으로 통용되는 가장 원초적인 프로그램 실행 함수이다. 그 대신 받아들이는 매개변수가 무진장 많으며 쓰기가 좀 어렵다. 여기에 대해서는 뒤에서 다시 좀 다루기로 하겠다.

끝으로 ShellExecute는 그 이름이 암시하듯, 커널 계층이 아닌 쉘이라는 꽤 상위 계층에서 구현된 함수로 단순히 파일을 실행하는 게 아니라 인터넷 URL을 기본 웹 브라우저에서 열거나 텍스트 파일을 메모장에서 여는 일도 다 담당한다. 동작 자체도 "open", "print" 등 아예 명령 문자열로 지정할 수 있다. 즉, 쓰임이 훨씬 더 포괄적이다. 이 함수도 EXE를 실행할 때는, 내부적으로 어차피 CreateProcess를 응당 호출한다.

C...는 리턴값이 프로그램의 실행 성공 여부를 나타내는 BOOL 형태인 반면, 나머지 세 함수들은 값이 약간 특이하다. 32보다 큰 정수를 되돌리면 성공을 뜻하고 그렇지 않으면 실패라는 뜻이다.
왜 이렇게 되었냐 하면 이 legacy 함수들은 원래 리턴값이 "인스턴스/모듈 핸들"이었으며 32 이하의 핸들값은 에러를 나타내는 값으로 의미가 예정되었었기 때문이다. 일종의 과거 잔재이다.

오늘날 윈도우 프로그래밍에서 HINSTANCE라고 부르는 핸들은, 과거에는 프로세스 ID와 비슷한 개념이었다. 이 핸들은 자신이 실행한 파일을 식별하는 정보도 있었던지라 동일 EXE를 중복 실행한 것을 WinMain 함수와 함께 넘어온 hPrevInstance로 분간할 수 있었다. 또한 EXE를 실행하여 생긴 인스턴스 핸들과, 그 EXE 안에서 DLL를 읽어들임으로써 이를 식별하는 모듈 핸들(HMODULE)도 별개의 존재였다.

하지만 32비트로 넘어오면서 운영체제의 메모리/파일 관리 모델이 완전히 바뀌었고 오늘날은 HINSTANCE와 HMODULE의 구분이 완전히 없어졌다. 단순히 프로세스 메모리 공간에 맵핑되어 있는 파일 이미지를 가리키는 포인터일 뿐이다. 메모리 영역 overlap에 따른 재배치만 일어나지 않는다면, 해당 DLL/EXE의 preferred base가 그대로 핸들값이 되는 것이다. 인스턴스 핸들이 이렇게 특정 프로세스 안의 주소 공간을 가리키는 포인터가 되는 바람에(national), 이제 이 핸들로 여러 프로세스들을 식별(international)하는 것은 불가능해졌다.

CreateProcess는 사용자가 보내준 파일 + 명령행 사이에다 null 문자를 잠시 삽입하여 토큰화를 했다가, 함수 실행이 끝난 후 문자열을 원래대로 되돌려 준다. C 언어의 strtok 함수를 떠올리면 된다. 이런 이유로 인해 명령행을 넘겨주는 포인터 영역이 read-only const여서는 안 되며 쓰기가 가능해야 한다. (물론 윈도우 NT 계열 운영체제에서 W 버전이 아닌 A 버전을 호출하면 어차피 쓰기 가능한 메모리 버퍼로 인코딩 변환이 일어나기 때문에 read-only 메모리를 넘겨줘도 문제될 건 없다.)

프로그램을 실행할 때는 이 프로그램에다 기본으로 넘겨 줄 각종 환경 변수, 콘솔 프로그램인 경우 표준 입출력 스트림의 핸들, 디버그 실행 여부 등 갖가지 고급 정보를 넘겨줄 수 있으며,
프로그램이 실행되었을 경우 생성된 프로세스의 핸들과 ID 등도 돌려받게 된다. 가령, 이 프로그램이 실행이 완전히 끝날 때까지 내가 기다려야 할 때 이 핸들에 대해 WaitForSingleObject를 호출하고 있으면 된다는 뜻이다. 단, 이 핸들을 Close하는 것도 우리 책임이다.

불필요하게 높은 계층에 자리잡고 있는 ShellExecute 대신, 커널 계층에 있는 CreateProcess를 좀더 간편하게 활용하기 위해 본인은 이 함수를 클래스로 감싸서 쓰고 있다. OpenFileName, TaskDialogIndirect (윈도우 비스타) 같은 복잡한 대화상자 UI 함수만큼이나 CreateProcess도

- 각종 디폴트 argument나 구조체들 챙기기
- 소멸자에서는 결과물로 받은 핸들들 닫아 주기
- 커맨드 라인을 알아서 자체 버퍼에다 생성하고, 필요한 경우 매개변수 전체를 따옴표로 싸 주기
- 이 프로그램의 실행이 끝날 때까지 기다리는 멤버 함수도 추가.

같은 자질구레한 일을 클래스로 감싸 놓으니, 프로그램 실행하기가 한결 편리하다.

Posted by 사무엘

2010/01/11 00:42 2010/01/11 00:42
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/58

class CAppFrame: public CWnd {
public:
        void PostNcDestroy() { delete this; }
};

class CMyApp: public CWinApp {
public:
        BOOL InitInstance();
};

CMyApp g_App;

BOOL CMyApp::InitInstance()
{
        m_pMainWnd=new CAppFrame;
        m_pMainWnd->CreateEx(0, AfxRegisterWndClass(0), _T("Hello World"), WS_OVERLAPPEDWINDOW,
                100, 100, 500, 500, NULL, NULL);
        m_pMainWnd->ShowWindow(SW_SHOW);
        m_pMainWnd->UpdateWindow();
        return TRUE;
}

위의 코드는 MFC를 써서 만들 수 있는 가장 간단한 GUI 프로그램이다. 빈 창만 달랑 뜨는 게 허전하면, message map 넣고 OnPaint에다가 Hello, world! 출력하는 코드만 추가해 주면 된다.
MFC로 창을 띄우기 위해서는 본질적인 것 딱 두 가지만 기억하면 된다.

첫째, CWnd에서 상속 받은 클래스를 만들기

둘째, CWinApp에서 상속 하나 받아서 전역 개체를 하나 반드시 만들고, InitInstance를 오버라이드하여 내 윈도우 클래스를 생성하는 코드를 짜 주기

물론 요즘은 닷넷 프레임워크 같은 더 객체 지향적이고 깔끔한 API도 나와는 있지만 저 정도만 해도 그냥 C언어 + Win32 API만 썼을 때와는 비교할 수도 없이 간단하게 내가 원하는 일을 곧장 시작할 수 있다. 윈도우 클래스 등록, 윈도우 프로시저 등 온갖 지저분한 내부 처리를 상당 부분 MFC가 알아서 해 주기 때문이다.

MFC의 핵심부이며 가장 자주 다루게 되는 부분은 역시 윈도우를 나타내는 CWnd 관련 개체이다. 응용 프로그램의 메인 윈도우부터 시작해서 대화상자와 대화상자 안의 자그마한 컨트롤까지 매우 다양한 용도로 쓰이는 HWND를 한 뿌리 클래스와 상속 클래스만으로 원활히 제어하기 위해 MFC는 굉장히 심도 있게 설계되었으며 내부적으로 자질구레한 일들을 매우 많이 해 주고 있다. 단순히 ShowWindow(hWnd, SW_SHOW)를 wnd.ShowWindow(SW_SHOW)로 바꿔 주는 것을 훨씬 넘어서는 수준이다.

더구나 메시지 맵을 통해 컨트롤 서브클래싱(기존 윈도우 컨트롤의 동작을 부분적으로 조작하는 것)까지 매끄럽게 연결시킨 것까지 보면, 솔직히 굉장히 잘 만든 프레임워크임을 인정하지 않을 수 없다.

이런 프레임워크를 만들 때 근본적으로 문제가 되는 것은 CWnd라는 C++ 오브젝트와 운영체제의 HWND 사이의 씽크를 맞춰 주는 작업이다. 둘은 원래 개념적으로 생성 주기나 scope이 서로 완전히 다른 존재이다. 하지만 MFC는 한 HWND를 가리키는 CWnd 오브젝트가 중첩되지 않도록 배려하고 있으며, 나의 C++ 코드에 의해 생성되지 않은 외부 HWND에 대해서도 임시 맵까지 만들어 가면서 CWnd를 동기화해 주고 있다. 멀티스레드 환경까지 감안하면 이는 더욱 복잡한 작업이 된다.

new 연산자로 CWnd가 생성될 때 CreateWindow를 같이 해 주고, HWND가 완전히 소멸되어 WM_NCDESTROY가 왔을 때 CWnd까지 delete로 지워 주는 것은 대부분의 경우엔 바람직한 디자인 패턴이나, 이것이 언제나 유용한 것은 아니다. 가령 modal 대화상자는 자체적으로 message loop까지 갖추고 있기 때문에 heap이 아닌 스택에다가 간단히 지역변수로 선언하는 경우가 더 유용하기 때문이다. 더구나 대화상자는 사용자가 대화상자를 닫았더라도, 이 C++ 클래스 갖고 있는 데이터 변수들을 후에 활용하는 경우가 많기 때문에 HWND 개체가 사라진 뒤라도 C++ 개체는 남아 있어 줘야 한다.

이래저래 CWnd와 HWND 사이의 관계와 생성/소멸 주기는 여러 모로 이해하기 까다롭다. CWnd 클래스 중에서 new로 생성되고 HWND의 소멸과 동시에 소멸되어야 하는 오브젝트라면, PostNcDestroy 함수를 오버라이드하여 delete this를 넣어 주어야 메모리 누수가 발생하지 않는다. 응용 프로그램의 주 프레임 윈도우라든가 modeless 대화상자가 이런 예에 속한다.

저 소스 코드에서는 CWnd를 날것으로 사용했지만, CWnd에서 파생되어 아예 전문적으로 응용 프로그램의 주 프레임 윈도우를 담당하고 있는 MFC 클래스인 CFrameWnd는 PostNcDestroy가 이미 저렇게 구현되어 있다. 그뿐만 아니라 이 클래스는 LoadFrame 함수에서 이미 윈도우 클래스의 등록까지 다 알아서 해 주기 때문에 저 소스 코드에서처럼 AfxRegisterWndClass를 번거롭게 호출할 필요도 없다. 사용자가 준 ID와 일치하는 응용 프로그램 타이틀, 아이콘, 심지어 단축키 액셀러레이터 테이블 따위를 모두 엮어서 윈도우의 기반을 자동으로 구성해 주기 때문이다.

MFC를 써서 아주 간단한 프로그램을 짜고 싶은데 비주얼 C++의 MFC 마법사가 기본으로 생성해 주는 코드는 군더더기가 너무 많다. 그래서 이럴 때 본인은 MFC 없이 일반 Win32 응용 프로그램으로 프로젝트를 시작한 후, 저 코드 템플릿을 갖다 붙이고 프로젝트 세팅을 “MFC 사용”으로 수동으로 바꾸는 방법을 쓴다.

필요한 건 MFC가 잘 캡슐화해 준 message loop, 몇몇 GDI 오브젝트 같은 것밖에 없는데 이것만 쓰기에는 MFC는 덩치가 너무 커지고 static link를 하기에도 오버헤드가 너무 큰 것도 불만이다. 차라리 known DLL만 쓰기 때문에 매우 가벼운 바이너리를 만들 수 있는 비주얼 C++ 6.0이 그리울 때도 있다. 어차피 이후 버전에서 추가된 기능은 거의 안 쓰기 때문에.

<날개셋> 타자연습은 소스를 보면 알겠지만 딱 MFC를 써서 개발되었다. 하지만 한글 입력기는 3.0부터 MFC를 쓰지 않으며, 믿거나 말거나 MFC를 어설프게 흉내 내어 본인이 내부적으로 자체 개발한 프레임워크 라이브러리를 static link하여 개발해 오고 있다. 하지만 자체 라이브러리는 오버헤드 줄이고 최대한 가볍게 만드느라, Win32 API라든가 각종 핸들을 캡슐화한 수준이 MFC 수준으로 범용적이고 체계적이고 편하지는 못하기 때문에, 여전히 MFC가 그리울 때가 있다. ^^;;

Posted by 사무엘

2010/01/11 00:27 2010/01/11 00:27
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/50

비주얼 C++의 역사

그동안 비주얼 C++에 대한 글을 적지 않게 썼었는데 이렇게 전버전의 특징과 역사를 정리해 본 적은 없는 것 같다.
역시 2003과 2005에 대한 설명이 가장 길다.

※ 1.0

MS C 7.0의 차기 버전으로서, 비주얼 베이직처럼 비주얼이라는 브랜드를 걸고 첫 제품이 출시되었다. 비주얼 C++의 역사는 초기에 Application framework라 불리던 MFC 라이브러리와도 역사를 함께 한다.

※ 1.52

윈도우 3.1 타겟을 지원하는 마지막 버전인지라 굉장히 중요한 위치를 차지하고 있다. AppStudio라는 통합 환경이 있기는 하나 도움말 열람, 리소스 편집 등은 여전히 다른 프로그램에서 따로 해야 했다.
프론트 엔드 GUI는 모두 16비트 프로그램인 반면, 커맨드 라인에서 동작하는 컴파일러, 링커 같은 주요 툴들은 도스 익스텐더 stub을 내장하고 있는 32비트 PE 포맷인 점이 무척 인상적이다.

이 시절에 컴파일러는 도스/윈도우 모두 그것도 메모리 모델별로 다 지원해야 했기 때문에 타겟 지정하는 옵션이 무척 까다로웠던 것 같다. 그리고 kernel32, gdi32, user32처럼 API가 분야별로 나뉘어 있는 게 아니라 윈도우 API import 라이브러리가 한 파일에 다 들어있었다.

군대 갔다 온 분, 특히 육군 쪽으로 갔다 온 분은 ‘상호 존중과 배려’라는 표어에 매우 익숙할 것이다. 실제로 이걸로 검색해 보면 군대 관련 사이트, 블로그 포스트들이 제일 먼저 많이 뜬다. -_-;;
그런데 윈도우 3.1만큼 이 문구가 잘 들어맞는 환경도 별로 없을 것 같다. 프로세스들이 혼자 시스템 자원 CPU 시간을 절대 독점하지 말고 다른 프로세스에 대한 자발적인 ‘상호 존중과 배려’로 멀티태스킹을 서로 만들어 가야 하기 때문이다. ^^;;

※ 2.0

32비트 타겟으로 나온 첫 버전으로 알려져 있으나, 그 시기가 윈도우 95가 나오기도 전이고 NT는 점유율이 미미하던 때였다. 시대를 너무 앞서 가는 바람에 주목을 별로 못 받은 전설 같은 존재이다.
(그나저나 그렇다면 94년 이전에 윈도우 NT 3.1의 각종 바이너리들은 무엇으로 빌드했는지 궁금하다. MS 자체적으로만 쓰는 내부 컴파일러? ㄱ-)

32비트 환경에서는 메모리 모델 구분이 없는 대신, CRT에 링크 방식(static/DLL)과 멀티스레드 지원 여부라는 개념이 새롭게 생겼다.

※ 4.0

DirectX에 4라는 버전이 없는가 하면 비주얼 C++에는 3.x대 버전이 없다. 내부적으로 꾸준히 버전업이 돼 온 MFC의 버전 번호에 맞춰 곧바로 4.0으로 건너뛰게 된다. 참고로 윈도우 95의 워드패드가 웬 듣보잡 MFC 3.0 기반인 점이 흥미롭다. 98 이후부터는 전부 MFC42 기반으로 바뀌었음.

4.0에 와서야 비로소 오늘날의 비주얼 C++과 상당한 골격이 갖춰졌다. 즉 코딩, 리소스 편집, 클래스 마법사, 도움말 열람을 한 프로그램에서 할 수 있는 진정한 통합 환경 Developer Studio가 이 버전부터 생겼다. msdev.exe라는 프로그램 이름은 훗날 6.0까지 이어졌다.
이때는 아직 16비트에서 32비트로 한창 넘어가는 과도기였기 때문에 Win32s 타겟이 지원되기도 했다.

참고로 MS에서 개발한 프로그램 중에서 MFC를 쓴 놈은 고작 워드패드, 그림판, 그리고 비주얼 C++ 6 이하 버전의 IDE와 관련 몇몇 유틸리티(Spy++, Dependency Walker) 정도? -_-;;

※ 4.2

4.1을 거쳐 4.0이 마이너 업그레이드된 버전으로, 제법 인지도를 얻었다. ActiveX 컨트롤, STL 같은 기능이 추가되고 MFC DLL의 이름도 이때부터 MFC42로 이름이 바뀌어 큰 뼈대의 변경 없이 훗날의 6.0까지 이어졌다. MFC42는 윈도우 98 이래로 운영체제가 이제 known DLL로 인정하는 마지막 버전 숫자이기도 하니 더욱 의미가 있다.

4.2에서부터 Win32s의 지원이 중단되었다. 오죽했으면 설치할 때 “이 버전부터 Win32s를 더 지원하지 않으니, 그 환경 개발하고 싶으면 이거 설치하지 말고 옛날 버전 쓰셈!”이란 경고문까지 나온다.

※ 5.0

GUI 껍데기는 거의 다 6.0처럼 바뀐 반면, 내부 구조와 기능은 여전히 4.2와 비슷하다고 보면 되겠다. 도구모음줄과 메뉴가 MS 오피스 97 스타일로 바뀌고(하지만 내부 코드 베이스는 서로 완전히 다름) 대화상자도 리모델링되었으나, 도움말은 여전히 RTF 기반이고 인텔리센스도 아직 없었다. 이듬해에 나온 6.0에 밀려 완전히 존재감을 상실했다. 프로젝트 파일 포맷이 이때 처음으로 DSP, DSW로 바뀌었는데, 이 포맷은 결국 5와 6 두 버전에서만 쓰이고 이후 버전에서는 또 바뀌게 되었다.

이때부터 bool이 built-in type으로 지원되기 시작했다. 그리고 이때부터 빌드하는 EXE 파일에 PE 재배치 정보가 기본적으로 생략되게 되었다.

※ 6.0

나온 지 10년이 지난 지금도 어렵지 않게 찾을 수 있고, 윈도우 XP만큼이나 최장수 개발툴로 이용되고 있는 결정판이다. 이 버전 이후로 거의 4년 가까이 버전업이 없었고 다음 버전인 닷넷이 변화폭이 너무 크기도 했기 때문이다.

6에서는 인텔리센스, edit and continue, delay-loaded DLL 같은 획기적인 기능이 처음으로 추가되고 도움말이 별도의 MSDN 라이브러리로 독립함과 동시에 HTML 기반으로 모두 바뀌었다.
이 버전은 윈도우 9x에서 설치 가능한 마지막 버전임과 동시에 MSVCRT, MFC42 같은 known DLL만 사용하는 바이너리를 생성할 수 있는 마지막 버전이기도 하다. 주요 라이브러리를 DLL 링크하고도 DLL 배포 걱정을 할 필요가 전혀 없다는 장점이 있다.

4.2 때는 ClassView에 있는 각종 클래스, 변수 정보가 소스 파일을 매번 저장할 때에만 업데이트됐다. 이것이 내용이 바뀔 때마다 실시간으로 바뀌기 시작한 것도 5.0은 아니고 6.0부터인 걸로 기억한다.

※ 7.1 (2003)

비주얼 C++의 다음 버전은 처음엔, 버전 번호나 연도가 아닌 닷넷이라는 브랜드로 출시되었다. MFC를 써서 개발된 VC 특유의 전통적인 msdev.exe는 퇴출되고 devenv라는 MS 오피스 내지 비주얼 베이직 스타일의 IDE가 등장했는데, 이는 완전히 새로운 것이라기보다는 예전에 비주얼 스튜디오에 있던 Visual InterDev와 비슷한 형태였다. 단지 거기에다 외형만 MS 오피스 XP 스타일 UI를 그대로 물려받았다.

덕분에 비주얼 C++과는 영 인연이 없고 비주얼 베이직만의 전유물인 것 같던 Property grid가 도입되어 비주얼 C++ 특유의 등록정보 대화상자를 대체했다. 클래스 마법사도 이 형태로 대체됐다. 도움말은 5.0 시절처럼 IDE 내부에 띄울 수도 있고 6.0처럼 외부에 띄울 수도 있는 융통성 있는 형태로 바뀌었다.

이제 주류로 내세운 게 C#, 공용 언어 런타임 같은 닷넷 환경이긴 했지만 네이티브 C/C++ 쪽도 크게 향상되었다. 6이 표준대로 제대로 지원하지 않던 문법 내지 컴파일러의 버그 많이 수정되기도 하고 네이티브 wchar_t 형식이 지원되기 시작했으며, 6에서 첫 도입됐던 인텔리센스도 굉장히 강력해져 특히 #define 심볼에 대해서도 동작하기 시작했다. 링크 시점에서 코드를 생성하는 전역 최적화라든가 crash mini-dump 생성 기능도 이때 첫 도입된 기능이다. 닷넷 정도만 써 보면 이제 6.0은 충분히 열악함을 느끼기에 충분할 것이다.

공용 라이브러리는 이제 MSVCR71, MFC71로 바뀌었는데, 이 DLL은 최신 운영체제라고 해서 더는 기본 내장을 해 주지 않기 때문에 응용 프로그램이 알아서 자기 디렉터리나 윈도우 시스템 디렉터리에다 구비해야 한다. 이 점에 관한 한 6.0이라든가 side-by-side assembly를 사용하는 8.0 이후만도 더 못하고 무책임한 면이 없지는 않다. 또한 64비트 개발은 불가능하지는 않으나 불편하고 디버깅도 되지 않고 IDE 차원의 적극적인 보조는 못 받는 어정쩡한 위상을 차지하고 있다. 이 불완전한 면모들은 후속 버전인 8에서 보완되었다.

사실 7.1은 최초로 나왔던 ‘닷넷’(7.0)의 버그 수정판이다. 엄청난 기능이 도입되었던 만큼 첫 버전은 불안정하고 버그도 많았기 때문이다. 7.1 버전은 아래아한글, VMware, 스타크래프트 같은 다수의 상용 프로그램의 최신 버전 개발에 아직까지 쓰이고 있다.

※ 8.0 (2005)

이 버전부터 닷넷이라는 이름을 떼고 출시는 되었으나 8은 7.1하고 아무 단절도 없이 닷넷과 네이티브를 모두 지원하는 동일한 개발 도구의 연장선이다. 이 버전은 닷넷 초창기 버전인 7의 과도기적 불완전하고 2% 부족하던 면모를 속 시원히 보완한 게 무척 많았다. 요즘 어도비 사에서 나오는 프로그램들이 이 버전으로 개발되고 있다.

첫째, 별도의 플랫폼 SDK를 설치할 필요 없이 IDE 차원에서 64비트 빌드와 디버깅이 정식 지원된다.
둘째, 공용 DLL인 MSVCR80, MFC80의 배포 방식이 side-by-side assembly 방식으로 완전히 바뀌었다. 일각에서의 불만을 감수하고라도 이건 꽤 강한 정책이었다. 이와 덩달아, 리소스 편집기로 수동으로 해야 하던 메니페스트 생성/편집 기능도 상당 부분 자동화됐다.

셋째, for 문의 변수 scope처럼 그동안 표준을 어기고 있던 문법을 교정하고, CRT의 보안을 크게 강화했다. strcpy_s와 gets_s(대상 버퍼 크기), qsort_s(콜백 함수에 데이터 전달 가능), strtok_s(토큰 중복 호출 가능) 같은 함수가 새롭게 등장하고 strchr 같은 경우, C++에서는 const 포인터를 되돌리는 버전과 비const 포인터를 되돌리는 함수가 오버로딩으로 분리해 나갔다. 무척 신선한 변화라 느껴진다. STL 디버깅도 훨씬 더 편해졌다.
넷째, UI 상으로도 이제 MS 오피스 스타일에서 독립한 새로운 외형 비주얼을 갖기 시작했으며, 도구모음줄 아이콘도 256색으로 더 화려해졌다. 전통적으로 ClassView에 클래스와 멤버들이 한 트리에 쫙 나열되던 것이 멤버는 별도의 리스트에 뜨도록 바뀌기도 했다.
이 버전부터는 생성하는 프로젝트의 디폴트 기반 인코딩이 드디어 유니코드로 바뀌었다.

※ 9.0 (2008)

8이 질적으로 많이 바뀌었다면 9는 윈도우 비스타의 등장처럼 8 출시 이후에 생긴 변화를 IDE나 MFC 라이브러리 같은 데에 적극 반영하여 양적인 향상을 꾀했다. 또한 MS 오피스 스타일의 GUI를 손쉽게 만들 수 있는 feature pack을 드디어 도입하기도 했다.

이렇게 시대의 조류 때문에 생긴 변화를 제외하면 9는 네이티브 C/C++의 관점에서 봤을 때, 7에서 8로 넘어갈 때 같던 큰 변화는 느껴지지 않으며 따라서 8은 조만간 별다른 존재감 없이 9로 흡수될 걸로 예상된다. 완전히 새로운 기능으로는 보안 강화를 위해 링커에 추가된 시작 주소 랜덤화 기능 정도? 이 옵션은 MS 오피스 2007과 윈도우 비스타의 모든 바이너리에 기본 적용돼 있으며, 과거 Win32s의 잔재로나 기억되던 EXE 파일의 재배치 정보를 다시 끌어들인 장본인이기도 하다.

9는 8과는 달리 윈도우 2000도 설치되지 않으며, 윈도우 9x 계열은 아예 타겟 플랫폼에서도 완전히 제외되었다. 링커 옵션에서 바이너리의 최소 요구 운영체제 버전이 4에서 5로 껑충 뛰었고, 이보다 낮은 값은 지정 자체가 안 된다.
또한 single threaded CRT 라이브러리 옵션도 없어졌다. 이것만 빼면 그다지 아쉬울 것 없다.

Posted by 사무엘

2010/01/10 23:50 2010/01/10 23:50
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/41

훅킹 프로그래밍

윈도우 훅킹은 운영체제의 내부 메커니즘(주로 메시지)을 가로채어 시스템 전체의 동작 방식을 바꾸는 매우 강력한 기법입니다.

이런 훅은 우리 프로세스 안의 특정 스레드 안에다가만 설치할 수도 있고 시스템의 모든 스레드에다가 설치할 수도 있는데, 32비트 운영체제로 오면서 후자 같은 global 훅(혹은 시스템 훅)을 설치하기 위해서는 훅 프로시저는 반드시 DLL에 따로 존재해야 하는 약간의 번거로움이 생겼습니다.

global 훅은 동작 방식의 특성상 모든 프로세스들에 나의 코드를 주입하는 가장 간편한 방법입니다. 굳이 윈도우 메시지 훅킹이 아니더라도 다른 종류의 훅킹을 위해서라도 윈도우 훅이 사용됩니다. 한컴사전의 노클릭 단어 인식이라든가 과거의 한스타, 그리고 Dependency Walker의 EXE 프로파일 기능처럼 API 훅킹이 동원되는 프로그램도 살펴보시면 별도의 DLL 파일이 존재하는 것을 알 수 있는데, 이는 API 호출을 변조하는 코드 자체를 삽입하기 위해서 윈도우 훅을 사용한 것입니다.

Global 훅은 완전히 특이한 프로그래밍 패러다임을 제공합니다. 다른 프로세스에서 완전 제각기 따로 실행되는 함수를 만들 수 있습니다.
가령, A라는 프로세스에서 B라는 DLL에 들어있는 훅 프로시저로 global 훅을 설치했습니다. 그러면 A와 B 사이의 통신 방법이 문제가 됩니다. 통상 A는 B의 동작 방식을 결정하는 입력 데이터를 주고, B는 훅킹을 통해 얻은 결과를 A에다 전달해야 할 것입니다.

이를 위해 보통 메시지를 쓰면 무난하죠. B에서 A로 통신할 때야 우리끼리만 쓰는 WM_USER+n이라든가, 심지어 WM_COPYDATA를 바로 보내도 무난하지만, 다른 프로세스 안에 존재하기 때문에 메시지가 겹칠 우려가 있는 B를 “향해서” 메시지를 보낼 때는 RegisterWindowMessage로 값이 안 겹치는 게 보장되는 별도의 메시지를 등록해서 쓰는 게 안전합니다.

또한 프로세스 A가 자신의 주소 공간에 로드되어 있는 DLL B의 변수값을 바꾼다고 해서 다른 프로세스들의 주소 공간에 로딩되어 있는 DLL B의 인스턴스의 변수값이 바뀌지는 않는다는 것도 조심해야 합니다. global 훅 프로시저는 메시지를 받는 그 프로세스의 주소 공간을 기준으로 호출된다는 것!
이걸 헷갈려서는 안 됩니다. 변수 초기화 같은 걸 잘못하면 훅을 설치한 프로세스 A 안의 B만 제대로 동작하게 되고, 다른 프로세스에 침투한 B는 그렇지 못하게 됩니다.

이것 때문에 과거에 굉장히 주의가 필요했던 점이 뭐냐 하면 훅 핸들 값을 공유하는 것이었습니다.
훅 프로시저는 자신에게 걸린 메시지를 처리한 뒤, CallNextHookEx 함수로 그 메시지를 다음 훅에다가 전달도 해 줘야 했습니다. 운영체제에 갈고리질을 하는 놈이 나만 있는 것 아니기 때문에... 그런데 윈도우 9x는 유독 이때 자신이 받은 훅 핸들값도 알아서 전달해 줘야 했습니다.

프로세스 A가 자신의 주소 공간에 있는 B DLL에다가 훅 핸들을 넘겨준다고 해도, 다른 주소 공간에 복제된 다른 B DLL의 인스턴스는 그 값을 알 수 없었습니다.
그래서 이 값은 숫제 메모리 맵드 파일 같은 공유 메모리를 만들어서 넘겨주거나, #pragma data_seg 같은 전처리기로 별도의 공유 섹션을 만들어서 그 전역변수에다 핸들을 공유해야 했지요.

그런데 윈도우 2000 이상, 아니 NT 계열은 그럴 필요가 없습니다. CallNextHookEx 함수를 호출하는 것 자체만으로 이 문맥에서의 훅 핸들은 알아서 감지가 됩니다. 당연히 그렇게 되는 게 이치에 맞죠. 그럼, 윈도우 95보다 NT가 3.1 시절부터 먼저였는데 왜 애시당초 아무 쓸모없던 HHOOK 인자를 받는 게 있었을까? 그건 아마 16비트 시절의 훅킹 API의 프로토타입을 그대로 베끼다 보니 그렇게 된 게 아니었나 싶군요. 32비트 윈도우로 와서 WinMain의 hPrevInstance 인자가 완전 무의미해졌지만 여전히 그대로 남아있는 것처럼.

그럼에도 불구하고 훅킹 API에 관한 한 윈도우 9x는 NT를 100% 닮지 못하고, 그렇다고 해서 아예 3.1 시절처럼 단일 주소 공간도 아니면서(핸들 값 공유도 어려운 환경에서) 꽤 불편한 프로그래밍 관행을 개발자에게 강요하게 되었던 것 같습니다.

윈도우 9x 시절에만 해도 global 훅 프로그래밍은 굉장히 조심스럽게 해야 했습니다. 훅 프로시저에서 뭔가 뻑이 났다간 그건 90% 이상 운영체제 다운으로 연결됐습니다. 디버깅은 더욱 힘들었음. 보호 모드 운영체제란 말이 무색할 정도였어요. 그만큼 운영체제가 안전보다는 열악한 PC 환경에서 효율 내지 도스와의 호환성 위주였으며, 불안정하고 응용 프로그램의 위험한 동작에 대해 취약했습니다.

하지만 XP 정도 되니, 특히 비스타는 이 정도로 안정성이 강화될 줄은 몰랐습니다. 훅 프로시저에 굉장히 어이없는 실수가 들어있었는데 그냥 그 훅만 싹 없어지고 프로그램은 잘 돌아가더군요. 깜짝 놀랐음. 윈도우 9x였으면 당장 blue dead screen이었을 겁니다.
윈도우 2000 때만 해도 IME/TSF 모듈이 해당 응용 프로그램을 뻗게 만들 수 있었는데, XP 이후부터는 안 그렇더군요. 자체적으로 예외 핸들링을 합니다.

global 훅 프로그래밍을 할 때 괴로운 점.
훅을 거둬들이고 모든 프로그램을 종료한 뒤에도 훅 프로시저가 들어있는 DLL의 lock이 즉시 풀리지 않는 경우가 많다는 것입니다. 훅을 해제한 뒤에도 이 DLL이 여전히 몇몇 프로세스의 주소 공간에서 사라지지 않고 상주해 있기 때문입니다. 그런데 3~5분 정도 기다리고 나면 없어져 있어요. 그러니 DLL을 이것저것 고치면서 자주 리빌드를 하기가 힘듭니다. -_-;;

비슷한 예로 글꼴도 있답니다. 프로젝트 파일로부터 최종 TTF 파일을 만들어서 여타 프로그램에서 테스트를 했습니다. 그런데 한번 그러고 나면, 그 프로그램을 종료한 후에도 내가 새로 설치한 TTF 파일이 여전히 in use 상태여서 지워지거나 교체가 안 되는 겁니다. 그러니 글꼴을 빈번히 수정하고 테스트하기가 힘들죠.

이럴 때 VMware가 해결책이 될 수 있습니다. 가상 머신을 만들어서 훅 프로그램을 실행하거나 글꼴을 설치하기 직전 순간의 스냅샷을 만든 후, 테스트를 하고 나서 스냅샷 시점으로 revert 하기. -_-;; 그게 저절로 파일 lock이 풀리길 기다리거나 재부팅을 하는 것보다 더 빠르더군요. ㄱㅅ!

뭐, global 훅 얘기로 길어졌습니다만, 내 응용 프로그램 안에서의 작은 규모의 훅도 충분히 쓰일 일이 있으며 특히 MFC에서는 내부적으로 이런 훅을 사용합니다. 일반적인 방법으로 잡기 힘든 메시지들도 MFC의 단일 프레임워크 하에서 일관성 있게 처리시키기 위한 목적이기도 하고요,
또 모든 대화상자들을 부모 윈도우 기준으로 중앙에다 재배치시키기 위해서도 훅을 사용해 대화상자가 생성되는 시점을 가로챕니다.

그냥 DialogBox 같은 함수만 호출해 보면 잘 알다시피 대화상자가 중앙에 뜨지 않습니다. MFC는 modal 대화상자만 중앙으로 옮겨 주기 때문에, 그냥 Create 함수로 modeless 대화상자를 만들어 보면 당장 그 위치 차이를 감지할 수 있습니다.

Posted by 사무엘

2010/01/10 23:29 2010/01/10 23:29
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/37

※ QuickBasic

아래의 PB와 더불어 제 인생에서 거의 최초로 접한 “EXE 생성기”가 아니었나 싶습니다. 중학교 시절 저의 주된 장난감이었습니다. 특히 가장 대중적인 베이직 컴파일러였던지라 한글 입출력/그래픽/사운드/마우스/메모리 등 갖가지 분야의 공개/상용 라이브러리로 기능을 확장해서 프로그램을 만들던 기억이 납니다.

MS는 오로지 고객 편의 위주로 기존 관행을 확 깨는 새로운 제품 만드는 데는 정말 비상한 재주가 있다는 걸 인정해야겠습니다. 그래서 윈도우 9x 같은 타협안으로 소비자 시장을 우려먹기도 했고, 베이직 개발 환경만 해도 저런 대화형 구조에다가 라이브러리까지 특수한 EXE로 링크해서 ‘퀵라이브러리’(QLB)라는 개념을 발명하여, IDE에서 빌드도 없이 바로 프로그램을 실행시킨 것도 정말 대단하다고밖에 말할 수 없군요.

QB는 최종 버전이 4.5였고 그로부터 몇 년 뒤에 소위 MS Basic이라 불린 Basic PDS (전문 개발 시스템)가 7.0/7.1 (VS .NET 같군)라는 QB 후속 버전이 나오긴 했습니다. 하지만 차이를 전혀 모르겠어요. 오히려 기능 확장의 생명인 라이브러리가 호환이 안 되고 불편해서 널리 사용되지는 않았습니다. PDS 이후 MS는 비주얼 베이직으로 넘어갑니다. 마치 MS C 7 이후로 비주얼 C++이 나온 것처럼.

※ PowerBasic

볼랜드에서 Turbo Basic을 잠시 만들던 전 직원이 볼랜드를 나와서 따로 창조한 브랜드이죠. 베이직만으로 QB보다 월등히 빠르고 거의 C/C++ 수준의 효율적인 네이티브 코드를 만들어 준 덕분에 QB와는 별개 영역에서 사용자를 구축하고 있습니다. 저는 3.1을 써 봤습니다.

단점으로는 QB에 비해 역시 라이브러리 캐부족, 일부 QB와 호환되지 않는 문법, QB보다는 다루기 불편한 IDE. 그리고 PB도 아예 Win32 콘솔 프로그램은 만들어도 32비트 도스 익스텐터와의 연계는 없었던 걸로 압니다.
또한 QBasic도 지원하는 스크린 mode 13H (VGA 320x200 256색) 모드를 지원 안 하는 것도 너무너무 아쉬운 점.

세상에 베이직처럼 Dialect가 많은 언어도 별로 없는 것 같습니다. 요즘 베이직은 그냥 스크립트 언어처럼 쓰거나 아니면 .NET 구도에 맞춰서 문법 다 뜯어고쳐서 쓰는데, PB는 베이직으로 하드코어 네이티브 코드를 지향했다고 볼 수 있습니다. 참고로 베이직을 거의 어셈블리 수준으로 짬뽕시킨 언어 내지 툴로 ASIC이라는 녀석도 있었습니다.

※ Visual Basic

GUI 바로 만들고 거기에다 코드를 즉시 갖다붙이는 형태가 무척 재미있어서 잠시 갖고 놀아 봤습니다. 하지만 런타임 DLL이 필요한 EXE밖에 못 만든다는 걸 알고서 이내 좌절함. 그나마 5.0 이전 버전은 EXE 자체도 자바 내지 닷넷 같은 슈도코드 기반이지 네이티브 코드는 지원도 안 하고 있었고요.

아울러, 닷넷부터는 IDE가 언어 불문하고 한데 통합되었기 때문에 VB 특유의 그 개성 넘치는 IDE를 볼 수 없게 된 것이 좀 아쉽습니다.

※ Borland Pascal

언어 특성상 C/C++과는 비교가 안 될 정도로 빌드 속도가 월등히 빠르고 라이브러리 오버헤드도 훨씬 작아서(Hello, world! 프로그램의 exe 크기) 굉장히 좋은 인상을 받았습니다. 16비트 도스용 네이티브 컴파일러로는 그야말로 최고봉이 아닌가 싶습니다. 파스칼 언어도 제가 베이직에서 C/C++로 전환할 때의 과도기 컨셉 역할을 잘 해 주었습니다. 이걸로 뭔가 걸출한 프로젝트를 진행해 봤으면 하는 아쉬움도 있습니다.

파스칼은 교육용으로 무척 훌륭한 언어이고 간단한 문법 덕분에 빌드하기는 간편하나, C/C++보다 함축성이 굉장히 떨어져서 코딩하기는 번거롭습니다. 가령 정수 나누기 정수도 결과가 무조건 실수가 되고 일일이 형변환을 해야 하는 건 기계 입장에서는 직관적이지 못하죠. 또한 단축연산이란 것도 없고.

※ Turbo C/C++

고등학교 때 정보 올림피아드 준비하던 시절에 잠시 다뤘을 뿐, 제품 자체의 왕년의 영향력에 비해서 얘를 써 본 경험은 거의 없습니다. 본격적인 응용 프로그램 개발을 위해 필요한 프로젝트 세팅, 메모리 모델 설정 같은 것도 전혀 해 본 적 없습니다. 저는 16비트 C/C++과는 도스/윈도우 공히 인연이 없는 셈입니다.

※ Delphi

놀랍게도 제가 얘를 다뤄 본 건 95년에 나온 초창기, 그것도 16비트 버전인 1.0뿐입니다. 오브젝트 파스칼 언어로 비주얼 베이직 같은 프로그래밍을 할 수 있구나 하는 인상만 받고 더 진척된 건 없습니다.

델파이는 런타임 DLL이 필요한 EXE를 만드는 옵션 자체가 없고 기본적으로 EXE 크기가 2~300KB대 이상은 먹고 들어가는 걸로 알고 있습니다.

※ C++ Bulder

비베, 델파이 수준의 RAD 환경을 C++로도 만들어 버렸으니 대단하다는 인상을 받았음. 물론 다른 건 다 델파이를 따라해도 빌드 속도를 따라할 수는 없지요.
버전 3을 써 봤습니다. 골치아프게 Win32 GDI API 쓸 필요 없이 캔바스 객체에 접근하여 프로퍼티를 바꾸는 것만으로 선과 면 색을 바꾸는 것도 흥미로웠습니다.

하지만 RAD 용도로는 어차피 델파이가 한 역할 담당하고 있었고, C++ 빌더는 상대적으로 존재감이 덜했던 것 같습니다.
MFC도 지원은 하지만, 빌드 속도 내지 빌드된 코드의 성능이 MFC의 본고장인 VC로 빌드한 것보다 무척 뒤쳐졌던 걸로 기억합니다. 빌드 속도는 흠, pre-compiled header 설정을 안 해서 그런 걸까?

델파이와는 달리 C++ 빌더의 EXE는 기본적으로 VCL.DLL이던가 컴포넌트 DLL이 필요한 형태입니다. 스테틱 링크가 가능한지는 기억이 안 납니다.

※ Watcom C/C++

MS와 볼랜드 컴파일러의 공통점은 32비트 도스 익스텐더 기반 코드 생성을 지원하지 않았다는 것입니다. 자기네 컴파일러들이나 겨우 몇 MB 정도 추가 메모리 확보를 위해, 286용 16비트 도스 익스텐더 정도를 사용한 게 고작입니다.

그러던 차에, 상용 게임들 덕분에 유명하던 DOS4GW 기반 코드 생성을 지원하던 왓콤 컴파일러는 정말 신선한 충격이었습니다. sizeof(int)도 말로만 듣던 4바이트로 나왔죠.
최적화 성능도 매우 뛰어났던 걸로 기억합니다. 익스텐더는 다르지만 도스용 아래아한글 386 에디션도 왓콤 기반이었던 것으로 유명합니다.

하지만 라이브러리가 부족하고 IDE도 없는 이 컴파일러로 본인이 당장 할 수 있는 일이 별로 없었던 게 아쉽습니다.

※ DJGPP

자유 소프트웨어 진영의 위력을 보여준 불세출의 32비트 도스 컴파일러입니다. 소스까지 공짜, DOS4GW보다 stub 크기가 훨씬 작고 더구나 DPMI가 기제공되는 환경에서는(윈도우 도스박스 같은) 필요하지도 않은 CWSDPMI.
거기에다 rhide 같은 IDE와 알레그로 같은 무지막지한 게임 라이브러리.

빌드 속도가 좀 느리고 exe 크기가 살짝 좀 큰 게 압박인데, 그 정도는 좀 대형 프로그램 작성하다 보면 아무것도 아닙니다. 도스가 살아 있다면 계속해서 쓰고 싶습니다. 물론 윈도우용 GCC도 있다고는 들었지만.

※ Visual C++

여러 개발툴들을 거쳐 이제 최종적으로 정착한 도구는, 그 이름도 유명한 비주얼 C++입니다. 더 설명이 필요없으리라 믿습니다. 닷넷이고 뭐고 많이 나와도 역시 한글 IME를 직접 개발하다 보니 제게는 네이티브 말고 다른 계층은 필요하지가 않네요.

Posted by 사무엘

2010/01/10 23:26 2010/01/10 23:26
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/36

윈도우 운영체제가 NT 초창기 시절 이래로 지금까지 사용해 오고 있는 실행 파일 포맷은 잘 알다시피 portable executable 형식입니다. 헤더도 이니셜인 PE로 시작합니다. 물론 네이티브 EXE이기 때문에 코드 부분은 기계마다 다르겠지만, 헤더 구조체라든가 리소스 같은 공통된 부분은 최대한 일치시켜서 이식성을 고려해서 설계했다는 뜻이지요.

늘 인텔 CPU에서만 돌아가는 EXE만 보다가 MIPS 같은 RISC CPU에서 돌아가는 PE 실행 파일을 헥사 에디터로 들여다보니 진짜로 기계어 코드가 한눈에 보기에도 일정 바이트 간격으로 아주 균일하게 나열돼 있더군요. 그걸 보고 놀랐던 기억이 납니다.

64비트 PE도 일부 구조체만 64비트로 확장되었을 뿐 기본적인 골격은 초창기 32비트 PE와 같습니다. 더구나 윈도우 운영체제가 인식하는 리소스(스트링 테이블, 대화상자, 메뉴 등)의 포맷은 매우 다행스럽게도 32비트 PE와 완전히 일치합니다.

EXE와 DLL은 자신만의 프로세스 공간을 만들어서 단독 실행이 가능하냐의 차이가 존재하는데, 기술적으로는 헤더의 비트 몇 군데만 다르지 똑 같은 PE 바이너리입니다. 이런 바이너리를 ‘모듈’이라고 부릅니다.

c, cpp 같은 소스 코드를 컴파일하면 기계어 코드인 obj 파일이 생깁니다. 이런 obj 파일과 lib를 링크하면 그런 모듈 파일이 결과물로 생성됩니다. lib는 또다른 obj의 묶음일 뿐 obj와 완전히 다른 파일이 아닙니다. 또한 모듈 역시 그런 obj, lib에 들어있는 코드를 PE 규격에 맞게 재배치하고 묶은 파일일 뿐이지 원시 파일과 그렇게 큰 차이가 없습니다.

윈도우 운영체제에서 개발 환경을 만든 사람들의 생각은, 링커가 특별히 할 일이 없게 하는 것이었던 것 같습니다. 물론 요즘은 전역 최적화처럼 링크 타임에도 코드를 생성하는 기술도 도입되어 사정이 그렇게 간단하지만은 않게 됐지요.

PE는 text(실행되는 기계어 코드), rdata(스트링 리터럴처럼 읽기전용 상수나 초기화 값), rsrc(윈도우 리소스 데이터), DLL 심볼 import/export 테이블, reloc(재배치 정보) 등 여러 섹션으로 나뉩니다. 특히 재배치 정보는 Win32s 시절에는 exe에도 필요했지만 지금은 dll에만 넣어 주면 됩니다.

PE의 헤더에는 자신의 기본 어드레스, 자신이 돌아가는 데 필요한 최소한의 운영체제 버전 같은 여러 정보가 들어가고 심지어 자신을 빌드한 링커의 버전을 기입하는 공간도 있습니다. 가령 비주얼 C++로 빌드하면 6.0, 7.1 (닷넷 2003), 8.0 (2005) 같은 번호를 쉽게 식별할 수 있지요.

원래 MS 자체에서 만든 프로그램 바이너리들의 링커 버전은 비주얼 C++의 버전과 거의 일치하지 않았습니다.
가령 윈도우 95는 까마득한 2.5, 그리고 98/ME는 3.1, 윈도우 2000은 5.12, 오피스 XP는 6.2였습니다. 비주얼 C++과는 별도로 자신들만 쓰는 컴파일러/링커가 있었던 것 같습니다.

하지만 이것이 언제부턴가, 한 02~03년부터 버전이 일치하기 시작했습니다. MS에서도 내부적으로 비주얼 스튜디오를 쓰기라도 했는지?
윈도우 XP는 7.0으로 당대의 최신 비주얼 C++이던 닷넷 2002와 일치합니다.
그리고 XP sp2 (sp1은 모르겠음)와 오피스 2003은 비주얼 C++ 닷넷 2003의 버전과 같은 7.1입니다.

그 후 윈도우 비스타와 오피스 2007의 모든 바이너리들은 비주얼 C++ 2005의 버전인 8.0으로 물갈이되어 있습니다. 하지만 CRT 라이브러리는 살짝 다릅니다. 오피스는 msvcr80을 쓰지만 운영체제는 자신만의 msvcrt를 고수하고 있습니다. 하지만 이제는 msvcrt에도 비주얼 C++ 2005에서 새로 추가된 strcpy_s 같은 보안 강화 함수들이 추가되어 있습니다.

msvcrt는 이제 운영체제가 혼자 마음대로 바꿔 쓰는 CRT DLL로 격리시키고 응용 프로그램들은 이제 msvcr??을 알아서 배포해서 쓰든가, 싫으면 스테틱 링크하라는 구도가 된 셈입니다.

Posted by 사무엘

2010/01/10 23:17 2010/01/10 23:17
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/31

« Previous : 1 : ... 8 : 9 : 10 : 11 : 12 : 13 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/04   »
  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:
2677530
Today:
2098
Yesterday:
2124