« Previous : 1 : ... 25 : 26 : 27 : 28 : 29 : 30 : 31 : Next »
윈도우 환경에서야, 실행 가능한 기계어 코드가 들어있는 다른 모듈을 동적으로 로드하는 것이 일도 아니다. LoadLibrary, GetProcAddress라는 마법 같은 API가 있기 때문이다. 플러그 인 같은 것도 프로토콜만 하나 잘 짜 놓으면 얼마든지 만들 수 있다.

하지만 도스 시절에 그런 일종의 DLL이라는 것을 직접 구현을 어떻게 했을까?
본인은 도스 환경에서 하드웨어를 직접 제어한다거나 시스템 프로그래밍 경험이 전혀 없다. PC 통신 시절에 올라오던 랄프 브라운의 "인터럽트 릴리즈" 이런 것들도 내게는 완전 외계인 문서였다.
32비트 윈도우에서 곧바로 C/C++을 공부한 경우이다 보니, 지금 다시 생각해 보면 그게 무척 신기하다. 그래서 점점 다시 저수준 시스템 쪽으로 회귀 중이다.

도스용 아래아한글은 2.5에서 덧실행이라는 기능이 추가되었다. 그래서 과거 1.2 시절에 잠깐 있었던 테트리스 게임도 덧실행으로 부활하고, 아래아한글 안에서 숫제 한네트라는 통신 에뮬레이터까지 구동할 수 있었다. (개인적으로 그래픽 에디터를 그렇게 덧실행으로 내장했으면 무척 좋았을 것 같다.)

3.0에서는 덧실행의 활용의 폭이 더욱 커져서 계산기, 지뢰 찾기, CD 플레이어도 제공했으며 심지어 화면 보호기까지 덧실행 프로그램으로 독립했다. 2.5 확장팩에서부터 제공되던 영한 사전도 GUI는 덧실행 애플릿 형태였다. 이런 덧실행 프로그램들은 내부적으로 32비트 네이티브 코드였으며, 응당 32비트 에디션(HWP386)에서만 지원되었다.

도스용 이야기 역시 이름은 덧실행이 아니지만, 이런 덧실행에 해당하는 여러 기능이 있었다. 그림 파일 뷰어도 있고, AI가 상당히 똑똑했던 걸로 기억하는 오목 게임도 있었다. 16컬러에서밖에 실행되지 않았던 아래아한글과는 달리 이야기는 256색/트루컬러 그래픽까지 지원했지만, 내부 코드는 여전히 볼랜드 C++로 빌드된 16비트 프로그램이었으며, 덧실행 역시 16비트 코드라는 게 아래아한글과는 달랐다.

물론 덧실행에 앞서 이들 프로그램은 그래픽/프린터 드라이버가 이미 일종의 DLL이며 아래아한글의 경우 폰트 드라이버라는 계층이 있었지만, 이들보다 화면에 보이는 GUI가 있는 덧실행 프로그램이 사용자에게 더욱 존재감 있게 다가온 것 역시 부인할 수 없는 사실이다.

이런 덧실행들은 어떻게 만들었을까? 아래아한글 덧실행의 경우, SDK가 있었다.
아래아한글 32비트 에디션과 덧실행 프로그램들은 왓콤 C/C++로 빌드되었는데, SDK가 지정한 각종 함수/구조체 프로토콜에 맞게 덧실행 프로그램을 작성한 후, 역시 SDK가 제공하는 특수한 라이브러리를 써서 링크하고 바이너리에다 고유한 post processing을 마치면 간단히 덧실행 프로그램이 만들어졌다. 얘네들은 헤더만 다를 뿐, 내부적으로는 인텔 32비트 어셈블리 기계어 코드가 들어있는 exe 그대로였다.

아래아한글은 자기 밑에 실행되는 덧실행 프로그램에다가 지금 돌아가는 비디오 환경 같은 걸 알려 주고, 하드웨어 독립적인 각종 그래픽 루틴, 그리고 요즘 GUI 운영체제들이 다 그렇듯이 막강한 한글 입출력 엔진을 이용하여 글자를 찍고 GUI를 출력하는 API를 덧실행에다가 제공해 주었다.

그때 화면 보호기 중에는 아주 고전적인 "우주 여행"도 있었고 점 찍기, 선 그리기, 생명 게임, 퍼즐, 벌떼 같은 것들이 기본 제공되었다. 우주 여행은 단순히 전진만 하는 게 아니라 방향을 꺾는 것도 있고 무척 박진감 넘치고 원근감이 무척 멋지게 잘 표현돼 있었다.

그리고 벌떼도 나름대로 2차원 공간에서의 boid 시뮬레이션인데 움직임이 굉장히 사실적이어서 저런 걸 어떻게 짰을지가 무척 궁금했다.

이것 말고 선이나 점을 그리는 것은 while 루프 안에서 랜덤하게 화면에 낙서를 하는 녀석이었다. 특히 점 찍기는 그냥 화면 가득히 검은 1픽셀 점을 채워넣는 것으로, 프로그램 파일의 크기가 400바이트도 채 되지 않았다.

그래서 문득 궁금해졌다. 크기도 꽤 작고 프로그램을 어떻게 짰을지 감도 오고 하니,
코드 부분을 긁어 와서 디스어셈블을 해 봤다.

본인은 어셈블러 쪽 지식이 거의 없다. 뭐 복잡한 레지스터 이름만 나와도 머리가 지끈거린다.
본인보다 더 고수이신 분이 있으면 아래의 해설에 오류 교정이나 보충할 만한 설명 있으면 얼마든지 환영한다.

; 레지스터에 주어져 있는 어떤 값들을 스택에다 push함.
; 화면 보호기들은 다들 이런 명령으로 시작하더라.
0012FDF8 53               push        ebx
0012FDF9 51               push        ecx
0012FDFA 52               push        edx

; EAX 레지스터의 값을 0으로 초기화. x xor x는 언제나 0이므로, mov 0을 하는 것보다 코드 길이가 짧아서 좋다.
0012FDFB 31 C0            xor         eax,eax

; 뭔가 초기화 함수를 호출한다.
; 아래아한글 덧실행 SDK가 제공해 주는 라이브러리 함수에다 static link를 한 것이다.
0012FDFD FF 15 88 02 00 00 call        dword ptr ds:[288h]
0012FE03 FF 15 50 02 00 00 call        dword ptr ds:[250h]

; 고급 언어로 치면 while문의 시작인 듯. while(CanContinue()); 로 딱 번역 가능한 문장이다.
; 함수 리턴값을 테스트하여 조건이 만족하는 경우 뺑뺑이 바깥으로 빠져나간다. (12FE54)
; 화면 보호기의 종료 조건을 테스트하는 것이므로, 아마 키보드나 마우스 입력을 감지하는 함수일 것이다.
0012FE09 FF 15 14 02 00 00 call        dword ptr ds:[214h]
0012FE0F 85 C0            test        eax,eax
0012FE11 75 41            jne         0012FE54

; 뭔가 포인터 참조를 하는 듯하다. x = ptr->member 정도? 먼저 ptr의 위치를 임시로 EDX에다 저장 후,
0012FE13 8B 15 24 03 00 00 mov         edx,dword ptr ds:[324h]

; 함수 호출을 염두에 두고, 아마 EAX의 값인 0을 얹어 놓는 것 같다. 이렇게 EAX를 자주 써먹는 이유는 아까 xor EAX,EAX와 마찬가지로 명령어 길이가 짧기 때문.
0012FE19 50               push        eax

; EBX에다가 화면 세로 해상도를 저장하는 것 같다.
0012FE1A 8B 5A 14         mov         ebx,dword ptr [edx+14h]
; 아마도 이 0x24C 오프셋이 난수를 되돌리는 함수인 듯하다.
0012FE1D FF 15 4C 02 00 00 call        dword ptr ds:[24Ch]

; 정확한 의미 잘 모름.;;
0012FE23 89 C2            mov         edx,eax
0012FE25 43               inc         ebx

; 32비트 숫자를 31만치 오른쪽으로 비트 shift 한다는 것은 결국 그 숫자의 부호만 남기고 싹 없앤다는 뜻인데.. 특별한 의미는 모르겠다. 나눗셈 연산 전에 부호와 관련된 무슨 플래그 설정인 듯.
0012FE26 C1 FA 1F         sar         edx,1Fh

; 이제 난수가 화면 세로 해상도의 범위 안에 있도록, 난수 값을 화면 해상도로 나눈 나머지를 구한다.
; idiv 명령은 한번에 몫과 나머지를 모두 구하는데, 나머지의 값을 EDX에다 저장해 준다. 그렇기 때문에 EDX를 push하는 것을 알 수 있다.
; 아까 inc ebx 명령은, 화면 해상도에 0이 들어오더라도 나눗셈 에러가 나지 않게 안전 조치를 취한 것 같다.
0012FE29 F7 FB            idiv        eax,ebx
0012FE2B 52               push        edx

; 세로에 이어 가로 위치 argument를 얹을 준비를 한다. 다시 EDX에다가 324h 포인터 위치를 저장하고,
0012FE2C 8B 15 24 03 00 00 mov         edx,dword ptr ds:[324h]

; 아까 0x14가 화면의 세로 해상도이고 이거는 10h이니 가로 해상도? ㅋㅋ
0012FE32 8B 5A 10         mov         ebx,dword ptr [edx+10h]
0012FE35 FF 15 4C 02 00 00 call        dword ptr ds:[24Ch]

; 아까와 거의 동일하다. 이번엔 inc 명령은 들어가 있지 않다.
0012FE3B 89 C2            mov         edx,eax
0012FE3D C1 FA 1F         sar         edx,1Fh
0012FE40 F7 FB            idiv        eax,ebx
0012FE42 52               push        edx

; 이제는 324h 오프셋 자체를 push한다. 이 값은 역시 덧실행 프로그램이 호스트(아래아한글) 프로그램으로부터 받은 핸들 같다.
; 이게 제일 나중에 push된다는 말은, C++ 코드 상으로 제일 첫째 argumet라는 뜻이다. 즉,
; SetPixel(hDC, x, y, 0); 에서 hDC뻘 된다는 뜻이다. 0은 아까 EAX의 값으로 대체했던 점의 색깔 정도? (이 화면 보호기는 화면을 온통 검은 점으로 채운다)
0012FE43 FF 35 24 03 00 00 push        dword ptr ds:[324h]

; 점 찍는 함수 드디어 호출!
0012FE49 FF 15 E0 01 00 00 call        dword ptr ds:[1E0h]

; 함수 호출 후 argument를 얹었던 스택 위치를 재정리하는 작업. 16바이트를 더하는 것을 보면, 위의 함수가 총 4개의 인자를 받았다는 것을 알 수 있다.
0012FE4F 83 C4 10         add         esp,10h

; 다시 while의 시작으로 빠꾸
0012FE52 EB B5            jmp         0012FE09

; 루프 끗.
0012FE54 5A               pop         edx
0012FE55 59               pop         ecx
0012FE56 5B               pop         ebx
0012FE57 C3               ret

따라서 위의 코드에서 loop 부분을 슈도코드로 재구성하면 대략 이런 형태가 되겠다.

HANDLE hHWPHost;
while( CanContinue() )
        PutPixel( hHWPHost, rand() % hHWPHost->nScreenX, rand() % (hHWPHost->nScreenY+1), 0 );

저 간단하기 그지없는 코드를 C언어로 짜도, 컴파일을 하면 저 정도 수준의 기계어 코드로 번역된다는 것이 놀랍다.
역시 전산학의 총아는 컴파일러이다. 컴파일러 제작자가 존경스럽다.

프로그램 논리를 다 알고 있는 초간단 코드도 겨우 인텔 CPU 인스트럭션 세트 매뉴얼을 뒤지고,
간단히 내가 생각하는 코드를 VC++로 최적화 없이 컴파일하여 살펴보면서 낑낑대면서 디스어셈블을 해 봤는데,
이보다 훨씬 더 복잡한 기계어 프로그램은, 암호 같은 숫자와 명령 코드 속에서 본디 의미와 논리를 찾는다는 게 거의 불가능에 가깝다.

커다란 wav 파일 하나 던져 주고서, 이걸 재생하지 않고서 무슨 소리인지 알아 맞히는 거나 다름없다.
wav는 그래도 손실 압축이라도 되지, 기계어 exe는 같은 크기이더라도 정보량이 훨씬 더 많으며 손실 압축을 할 수가 없다.

점 찍기와는 달리, 선 그리기만 해도 코드 길이가 저것보다 더 길고, 특히 난수를 다섯 개나 요청한다.
x1, y1, x2, y2, 그리고 색깔 이렇게 다섯 개인 것을 어셈블리 코드 상으로 확인을 할 수 있었다.

비록 도스에서 돌아가는 일개 덧실행 프로그램이지만
돌아가는 기계 환경이 동일하고 똑같은 32비트 기계어이기 때문에 비주얼 C++의 디버깅 기능으로 이렇게 간단히 디스어셈블리 결과를 들여다볼 수 있다는 게 신기했다 (실행은 당연히 못 하지만).

단지 운영체제에 따라 EXE 파일의 포맷이 다르며, 입출력이라든가 운영체제가 제공하는 기능을 요청할 때, 도스 프로그램은 인터럽트에 의존하는 반면, 윈도우는 커널 영역에 자리잡은 함수 호출 기법을 사용하는 식의 차이가 존재하는 것이다.

Posted by 사무엘

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

 http://www.dilascia.com/ruint.htm

본인이 이 사람 이름을 본 것은 비주얼 C++ 6.0을 쓰던 시절부터이다.
MSDN을 보면 각종 함수 레퍼런스, 툴 설명서뿐만이 아니라 고맙게도 일부 책이나 간행물 내용까지 수록돼 있었는데, 어느 프로그래밍 잡지의 C++ Q&A란을 애독하기 시작했다. 그리고 그 코너를 집필하는 사람이 바로 저 전설의 프로그래머 Paul DiLascia였다.

특히 비주얼 C++ 6.0 MSDN에는 bmp 파일 뷰어를 밑바닥부터 만드는 과정을 설명해 놓은 게 있었는데
친절한 설명도 설명이거니와 이 아저씨는 글빨 입담이 정말 구수하다는 것을, 생소한 영어를 읽으면서도 느끼지 않을 수가 없었다.

윈도우+MFC 프로그래밍의 달인인 건 의심의 여지가 없고, 나중에 알고 보니 이 사람은 원래 수학 전공에다 컴퓨터 예술 쪽에도 심취해 있는 다재다능 엄친아였다. 이름이 좀 유럽풍인 것 같아 보이나, 실제로는 뉴욕에서 태어나서 자란 골수 미국인이라고 한다. 조상이 이민자?

링크를 건 곳은 저 사람의 2003년 시절 인터뷰이다.
고수 프로그래머로서의 조언도 여럿 담겨 있는데, 그 내용이 무척 공감이 간다.

- 최신 기술 동향은 놓치지 않되, 남들이 좋다고 하는 데에 소신 없이 절대 우루루 휩쓸려 따라가지 말라. 가령 클라이언트처럼 C/C++가 독보적인 분야가 있고, .NET 같은 곳이 더 유리한 분야가 따로 있을 뿐이다. 자신의 문제 해결에 가장 적합한 툴이나 기술을 잘 고르는 요령이 무엇보다도 중요하다. 그런 것들은 도구일 뿐이며 절대적인 우열이 존재하는 게 아니다.
- Win32 API가 존재하는 한.. 윈도우즈 운영체제가 밑바닥부터 새로 뒤바뀌지 않는 한, 너무나 클래식(?)한 C/C++이나 MFC 같은 것은.. 결코 그렇게 호락호락 없어지지 않는다. 더 업데이트가 안 되고 있다는 말은 그만큼 API가 성숙하고 안정화됐다는 뜻으로 오히려 다행스러운 현상인 것이다.
- 늘 목표를 명확히 하고 내가 무슨 문제를 해결해야 하고 그 목표 달성을 위해 무슨 도구를 쓰는 게 가장 최적일까를 고민하라. 디자인 과정을 소홀히 하지 말라.

민장(minjang.egloos.com) 님 블로그에서도 비슷한 요지의 말을 봤던 것 같다.

그리고.....

  "워드, 엑셀 같은 유명 소프트웨어에 들어있는 GUI 베껴서 따라 만드느라 시간 낭비 절대 하지 말라!" (그 시간에 실제 기능 구현에 필요한 자료구조/알고리즘 연구나 더 해라)

란 주문도 들어있다. ^^;;
아마 C++ Q&A 운영하면서 "나도 저기에 들어있는 그 기능, 그 UI 만들고 싶다. 어떡하면 좋은가?" 류의 뱁새가 황새 따라가려는 급의 문의를 엄청 많이 받았지 싶다.

* * * * *
  Too many programmers spend all their energy implementing some cutesy UI feature like docking windows or pink scrollbars because they saw it somewhere else. Microsoft has 5000 programmers to create animated paper-clips. You don't. Don't fall into the code envy trap!

  Don't get side-tracked implementing the latest GUI feature you saw in Word or Excel.
(그런 공룡 대기업들이나 부리는 '가진 자의 여유'를 당신이 따라할 여건은 안 된다는 걸 알아야 한다)
* * * * *

저건 우리나라의 유명한 비주얼 C++ 서적의 저자인 이 상엽 씨도 똑같은 말을 했다.

* * * * *
  그래도 예술적 가치가 있는 프로그램 제작에 열을 올린다면 좋은 이야기다. 그것도 아닌 것을 예술인냥 착각하고 움직이지는 절대 말라는 것이다. 예술적 가치가 없는 부분이 어떤것인가를 물어 볼것이다. 거 있지 않은가? MS 사에서 도움말 강아지 이리저리 왔다 갔다 한다고 자신의 프로그램에 강아지 만들어 넣는거...Visual C++의 워크 스페이스 창이 도킹 되었다가 떨어졌다 하는데 나두 이거 만들구 싶다 라는거...
예를 간단하게 들어서 MP3 에 있는 압축기술이나 음성인식 또는 지문인식 등의 기능이 예술이라고 볼수 있고 그냥 강아지 이리저리뛰어 다니는 것은 처음 만들어 내지 않는다면 것은 잡다구리 테크닉이다.
* * * * *

그래서 <날개셋> 한글 입력기의 편집기 프로그램은... 9년이 넘게 개발되고 버전이 5.5가 넘어선 지금까지도 완전 윈도우 95의 기본 컨트롤과 UI 요소만 사용하여 만들어져 있다. ^^;;; 편집기의 경우 과거 3.41 버전에서 MFC를 떼어내는 과정에서, 이제 도구모음줄이 도킹을 할 수 없게 바뀌었다. 그게 원래 MFC가 구현해 주던 일이었기 때문이다.

사실, 편집기를 실행해 보면 도구모음줄 아이콘들이 좀 중앙에 안 있고 메뉴, 즉 위쪽에 너무 바싹 붙었다는 인상을 받는데 이것도 딱히 바꿀 방법이 없다.
아이콘 사이에 임의의 크기로 여백을 내는 것도 MFC가 윈도우 프로시저를 다 서브클래싱해서 굉장히 지저분한 작업을 한 끝에 구현한 것이다. 이런 점에서 MFC는 단순히 윈도우 API wrapper 역할만 하는 것은 아님을 알 수 있다. 하지만 그런 거 따라하는 일에 너무 심취하지 말라는 얘기이다.

아쉽게도 이 사람은 작년(2008) 9월, 40대 후반의 나이로 세상을 떠났다. 사인은 밝혀져 있지 않다. 비주얼 C++ 2008의 내장 MSDN에는 2006년자로 작성된 그의 글을 볼 수 있는데, 이제 더는 그런 글을 접할 수 없으니 안타깝다.

Posted by 사무엘

2010/01/11 10:40 2010/01/11 10:40
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/104

printf/scanf가 받는 % 문자는 이식성 면에서 매우 큰 문제를 일으킬 수 있다. 기계 종류와 운영체제/컴파일러(정확하게는 CRT 라이브러리)의 종류에 따라 미묘한 차이가 존재하기 때문이다.

이런 잡음이 제일 없던 꿈 같은 시절은 단연 32비트 시절이다. 포인터와 정수가 전부 4바이트가 됨으로써 %d와 %ld 같은 골치아픈 구분도 없어졌고, 포인터도 far/huge 같은 구분이 없어져서 모든 것이 32비트 단위로 끝이 났기 때문이다. %d, %x, %u 하나만으로 컴퓨터에서 통용되는 거의 모든 정수를 바로 읽고 쓸 수 있던 시절. -_-

* * * * * *
Note 1
  참고로 정수가 아닌 실수는?
16비트 시절에는 터보 C/C++에 무려 10바이트 크기의 실수인 long double이 있었고, 파스칼에는 아예 6바이트짜리 Real이라는 기괴한 실수가 존재했다. CPU의 machine word가 16비트 크기이고, GPU는커녕 부동소숫점 전용 프로세서(FPU)마저 흔치 않아서 이런 연산도 소프트웨어적으로 직접 구현하는 게 당연시되던 시절이었으니까 그런 게 존재 가능했다.
요즘 세상엔 무조건 32비트 float 아니면 64비트 double이지, 저런 건 상상도 못 할 개념일 것이다. 픽셀 크기조차도 옛날에는 트루컬러 24비트이다가 요즘은 컴퓨터가 더 처리하기 편한 형태인 32비트이다.
* * * * * *

하지만 이렇게 32비트 천하통일 시대에 끝이 보이기 시작한 것은, 64비트 컴퓨터가 속속 등장하고 문자열도 일반적인 8비트 크기가 아닌 16비트 단위의 소위 wide string이 공존하게 되고부터이다.

그럼, 이번에도 역시 숫자부터 예를 들어 보겠다.

32비트 윈도우 + 비주얼 C++의 CRT는
32비트 정수를 주고받을 때는 당연히 그대로 %d나 %u를 주면 되고 별도의 크기 지정자가 필요 없다. 하지만 64비트 정수에 대해서는 I64라는 접두사를 넣어서 %I64d처럼 해야 한다.

이 규칙은 64비트에서도 완전히 동일하게 적용되기 때문에 이식이 쉽다.
특히 호환성을 극도로 중요시하는 윈도우는 64비트 기계에서도 int 형을 32비트 4바이트로 책정한 관계로, 64비트에서도 %d가 아닌 %I64d를 해 줘야 32비트 영역을 넘어서는 정수를 읽거나 쓸 수 있다. 64비트 기계이더라도 숫자는 일단 변함없이 32비트가 주류라는 인상을 넣은 것이다.

* * * * * *
Note 2
  윈도우즈 문화권은 왜 이리도 호환성에 목숨을 걸까?
간단하다. 그쪽은 오픈소스 진영과는 근본적으로 분위기가 다르기 때문이다.
모든 것이 투명하게 소스 공개이고, 사용자들이 다 컴퓨터를 능수능란하게 다루는 능동적인 해커들인 세상에서는 뭔가 소프트웨어를 업데이트해도 줘도 못 먹는 사람이 없이 물갈이도 금방 된다. 소프트웨어 계층에 breaking change가 잦더라도 재컴파일 한 번으로 '끗'이며, 그렇게 문제가 되지 않는다.

하지만 여기는 사정이 다르다. 마우스로 느릿느릿 아이콘 클릭밖에 못 하고 악성 코드에 속수무책으로 당하는 컴맹도 많다. 또한 돈 내기 싫어서 구닥다리 OS를 계속 고집하는 사람도 많다. 오로지 MS라는 회사가 모든 내부 사정을 관장하고 고객을 다 떠먹여 줘야 한다. 그러니 무조건 한번 만들어 놓은 것에 대한 유지 관리가 편리한 시스템을 만들어야 하며, 새 제품을 단절 없이 많이 팔려면 하위 호환성이라는 보수적인 가치를 최우선 순위로 두고 목숨 걸 수밖에 없는 것이다.
* * * * * *

단, 딱 하나 문제가 될 수 있는 것은 소위 INT_PTR 타입으로, 32비트 기계에서는 32비트이지만, 64비트에서 실제로 64비트 크기로 확장되는 정수이다. 이게 진짜로 포인터의 크기와 같으며 machine word와 크기가 일치함이 보장되는 정수이다.

이런 정수를 다루는 프로그램의 이식성을 위해서 %Id가(64만 빼고) 별도로 추가되었지만, 이건 반대로 구형 CRT에서는 지원되지 않는 걸로 알고 있다.
그래도 다행인 건, binary format이 아니라 사람이 읽을 수 있는 문자열 형태로 숫자를 읽고 쓰는데 32비트 크기를 넘어서는 범위를 다루는 일은 굉장히 드물다는 것. 차라리 실수를 다루면 다뤘지 정수가 그러는 일은 드문 편이다.

참고로, 가변 인자 함수가 호출될 때, 모든 정수형은 기본적으로 int 형으로 promote가 일어난다. char이든 short이든 다 32비트 내지 64비트 크기로 폭이 증폭된다는 것이다. float는 double로 바뀐다. 그렇기 때문에 float나 double이나 동일하게 %f나 %g로 출력 가능하다. 단지, 값이 아니라 포인터가 전달되는 scanf를 호출할 때는, float에 대해서는 %f를, double에 대해서는 %lf라고 반드시 타입 구분을 엄격히 해 줘야 할 것이다.

64비트 정수를 전달할 때는 32비트 기계에서는 스택에 push가 두 번에 걸쳐 일어나지만, 본디 64비트이던 기계에서는 역시 한 번만에 인자 전달이 끝난다. 그렇기 때문에 %d %d %d 해 놓고 실제로 32, 64, 32비트의 순으로 변수를 전달했다면 32비트 기계에서는 마지막 숫자가 꼬이겠지만(push는 128비트, 하지만 pop은 96비트) 64비트는 둘째 정수가 범위만 32비트 내부에 있다면 세 숫자가 모두 제대로 출력이 된다(push와 pop 모두 64*3비트 동일).
물론 이 경우, 둘째 %d를 %I64d로 해 줘야 32와 64비트 기계에서 모두 잘 동작하는 portable 코드를 만들 수 있다.

윈도우 외의 다른 운영체제는 사정이 어떤가 모르겠다. 64비트 정수를 출력할 때 32비트 기계에서는 %lld, 심지어 64비트에서는 %ld 이렇게 차이가 존재한다고도 하는데.. =_=;;
gcc 자체가 I64와는 다른 관행을 사용하는데 기계마저 64비트로 가고 나면 이거 이식성 면에서 재앙이 발생하지는 않으려나 우려된다.

다음으로 문자열이 있다.
알파벳 이외의 문자는 다룰 일이 없는 서버나 게임 엔진 같은 아주 특수한 프로그램을 개발하는 게 아니라면 이제 유니코드 문자는 대세가 되어 있다. 물론 UTF8도 쓰이고 유닉스 계열 운영체제에서는 심지어 UTF32도 쓰이지만, 그래도 유니코드 문자열을 컴퓨터 메모리 상으로 저장하는 데 비용 대 효율이 가장 뛰어난 방법은 UTF16이다. 특히 윈도우는 NT 시절부터 이렇게 16비트 단위의 wide string을 내부적으로 다뤄 와서 wchar_t = 곧 unsigned short나 다름없을 정도이다.

printf는 ansi 버전과 wide 버전이 존재하며, format으로 지정해 주는 문자열과 %s로 전달하는 문자열의 타입은 대체로 일치한다. ansi 버퍼에다가 wide string을 출력한다거나 그 반대로 해야 하는 경우는 드물다. 하지만 그런 일이 아주 없는 것은 아니다.

이럴 때 윈도우에서는 %hs, %ls라는 지정자를 주어 h는 버퍼 크기와 상관없이 무조건 ansi, l은 버퍼 크기와 상관없이 무조건 wide라고 지정을 할 수 있게 했다. gcc 쪽은 잘 모르겠다.

그래서 함수 오버로딩이 지원되는 C++ 스트림이 짱이라는 말이 있을 정도이니까. 아무 곳에서나 그저 무조건 OK.

Posted by 사무엘

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

C++의 멤버 포인터

C++에는 pointer-to-member이라는 아주 기괴한 개념이 있다.

마치 일반 변수를 포인터로 가리키고 전역 함수를 가리키는 포인터가 있는 것처럼,
이 포인터는 포인터이긴 한데, 변수 포인터라면 특정 클래스(=구조체)에 대한 오프셋에 매여 있다고 볼 수 있으며, 함수 포인터라면 특정 클래스의 멤버 함수로 소속된 함수만 가리킨다는 특수성을 지닌다.

그래서 한 클래스 안에 프로토타입이 일치하는(리턴값, 인자의 개수와 type들) 여러 멤버 함수들이 있으면 그것들 중 하나를 가리켜서 특정한 한 함수를 if나 switch..case 없이 계속 가리키면서 호출하게 할 수 있다. 이걸 이용해서 얼추 다형성까지 구현할 수 있다는 것이다.

또한 한 클래스 안에 동일한 타입을 지닌 여러 멤버에 대해서 일괄적으로 초기화를 해 줘야 할 때, 멤버 함수를 가리키는 포인터를 써서 이 일을 아주 generic하고 수월하게 할 수 있다.
MFC에서 message map도 매크로를 파헤쳐 보면 응당 pointer-to-member를 써서 구현되어 있다.

C++에서 추가된 클래스 멤버 함수는 굉장히 묘한 존재이다.
사실, C에서도 구조체에다가 함수 포인터를 갖다 둠으로써 obj.func() ptr->func() 같은 문법 자체가 불가능한 것은 아니다. 그런데 이 경우 ()를 생략함으로써 해당 함수의 주소를 간단하게 얻을 수 있은 반면, C++ 멤버 함수는 그렇게 할 수 없다. 그 멤버 함수는 그 클래스의 '인스턴스'에 소속되어 있으면서 그 클래스의 크기에 영향을 주는 존재가 아니기 때문이다. scope이라는 개념이 존재하고 this 값을 기본으로 받는 것만 빼면 위상은 오히려 전역 함수에 더 가깝다.

아예 개체와 함께 함수 호출 연산자 ()를 줘서 확실하게 호출을 하든가, 아니면 &Class::Function과 같은 형태로 pointer-to-member 값을 얻어 와서 자기 타입에 맞는 pointer-to-member에다가 그 주소를 대입만 수 있다. 일반 함수 포인터와는 달리 이렇게 얻어진 값은 곧바로 함수 호출을 할 수 없으며, 반드시 그 클래스에 해당하는 개체가 있어야 한다. 개체 뒤에다 .* 이라든가 ->* 연산자를 붙이면 되는데, 이때 .*나 ->*은 *까지 붙어서 완전히 한 연산자 토큰이기 때문에 둘 사이에 공백이 들어갈 수 없다.

또 하나 주의해야 하는 것은 C++의 연산자 우선 순위이다. (obj->*pfnFunc)(함수인자)처럼 함수 인자 앞에는 반드시 괄호가 들어가야 한다. 또한 우리 클래스 안이라고 해도 ->*나 .* 앞에서는 this를 생략할 수 없으며 반드시 붙여야 한다. 여러 모로 문법부터가 기괴하다는 것이다.
이 pointer-to-member의 타입은 공식적으로 void (Class::*)(함수인자) 이라 불린다. 이때, ::와 *은 서로 독립된 토큰이다.

그런데 pointer-to-member하고 가상 함수가 서로 만나면 이거 정말 골치아픈 문제가 발생하고 만다. 둘 다 다형성을 위해 존재하는 기능이며, 둘 다 구현하기 위해서 컴파일러가 프로그래머가 모르게 암시적으로 몰래 하는 일이 상당히 많은 기능들이다. 하지만, 결론부터 말하자면 이 두 기능은 서로 연동해서 사용할 수 없다. 이게 무슨 말인지를 지금부터 설명하겠다.

A라는 클래스가 있고 f라는 가상 함수를 정의했다. 그 후 A로부터 상속 받은 B라는 클래스는 f라는 가상 함수를 또 오버라이드했다.

그 후 A의 포인터 pf에 어떤 값이 들어왔다. 얘가 가리키는 값이 실제로 A인지, 아니면 A의 파생인 B인지는 알 수 없다.
이때,

if( &pf->f == &A::f ) puts("pf 개체는 A 타입");
else if( &pf->f == &B::f ) puts("pf 개체는 B 타입");

와 같은 형태로, 이 개체에 소속된 가상 함수의 주소를 판단함으로써 그 개체의 원래 타입을 판별할 수 있을까? 마치 C 언어 구조체의 멤버로 들어있는 함수 포인터를 비교하듯이, 가상 함수와 pointer-to-member 주소를 이런 식으로 연계할 수 있을까?

그럴 수가 없다는 뜻이다.
일단 클래스 멤버 함수의 pointer-to-member 주소를 되돌리는 방법 자체가 pf 같은 동적 바인딩을 허락하지 않는다.

그리고 더욱 나쁜 사실은, &A::f와 &B::f의 값 자체가 실제로 확인해 보면 동일하다는 것이다!
어떤 A 타입의 포인터가 있는데, pointer-to-member를 이용하여 어떨 때는 강제로 A::f를 호출하고, 어떨 때는 일부러 B::f를 호출하여 기반 클래스와 파생 클래스를 구분하는 것 자체가 불가능하다. 한 클래스 안에서 프로토타입이 같은 여러 수평적 함수들을 구분할 수는 있으나, 부모와 파생 클래스 사이의 수직적 관계가 존재하는 가상 함수는 전혀 구분할 수 없다.

사실은 이것이 원래 의도했던 C++ pointer-to-member의 동작 스펙이기도 하다. 가상 함수와 일반 함수를 구분하지 않고 그 위 계층에서 동작하게끔 말이다.

C++에서 가상 함수가 어떻게 구현되는지 대충 아는 분들도 있겠지만,
가상 함수가 생기면 사실 해당 클래스에 가상 함수들의 포인터가 당장 쭉 추가되는 게 아니라, 해당 클래스를 나타내는 가상 함수 포인터 배열이 하나 생기고, 이를 가리키는 '포인터'가 하나 추가된다. 즉,

ptr->nonVirtual(...) ==> globalFunc(this, ...) 가 일반 함수라면, 가상 함수는

ptr->virtualFunc(...) ==> ptr->vtbl->pfn[n](this, ...) 처럼 전개된다는 뜻.

그런데 pointer-to-member 함수는 매 타입에 대해서 vtbl의 값을 저장하고 있는 게 아니다.

virtualFunc_stub(this, ...)
{
        return this->vtbl->pfn[n](this, ...)
}

가상 함수의 address란 바로, ptr에 대해 가상 함수 테이블을 뒤져서 실제 가상 함수를 호출해 주는 "껍데기 함수"의 주소로 정적으로 고정되는 것이다! &A::f이든 &B::f이든 가리키는 주소는 바로 저기가 된다는 것이다. 이 일을 컴파일러가 몰래 슬쩍 해 준다.

그렇기 때문에 pointer-to-member 함수는 함수의 프로토타입만 일치한다면 일반이든 virtual이든 전혀 구분 없이 함수를 가리킬 수 있으며,
(pp->*pfn)(); 와 같은 문장은

004010C8 8B CE            mov         ecx,esi
004010CA FF D3            call        ebx

가리키는 대상이 virtual이든 아니든 관계없이 위와 같은 두 명령으로 간단히 번역된다. 실제 가상 함수 호출은 저 껍데기의 내부에서 이루어지니까.
pp->SayA(); 처럼 대놓고 가상 함수를 호출할 때는

004010CC 8B 16            mov         edx,dword ptr [esi]
004010CE 8B 02            mov         eax,dword ptr [edx]
004010D0 8B CE            mov         ecx,esi
004010D2 FF D0            call        eax 

테이블을 참조하는 오버헤드가 해당 코드에서 바로 이뤄지는 것과 좋은 대조를 이루는 셈이다.

따라서 이 함수가 어느 가상 함수를 가리키는지는, 문서화되지 않은 컴파일러 꽁수라든가 어셈블러 같은 지저분한 방법을 동원하지 않고서는 알 수 없다는 것이 결론이 되겠다. pointer-to-member의 동작은 가상 함수의 영향을 받지 않는다.

그런데, 가상 함수는 그나마 이런 식으로 극복해서 일관성을 얻었다지만, 가상 상속, 다중 상속 같은 극악한 C++만의 기능과 마주치면(요즘 언어들은 채택도 안 하고 있는), pointer-to-member 구현의 복잡도는 그야말로 GG 치는 경지에 다다른다.

결국 자기가 가리키는 함수뿐만 아니라 기반 클래스처럼 그 클래스와 관련된 다른 정보도 들어가야 하기 때문에, machine word(통상 4 또는 8바이트) 단위 크기보다 크기가 더 커지게 된다! this 포인터도 막 왔다갔다 해야 하므로.
그런 클래스에서 멤버 포인터가 구체적으로 어떻게 구현되며 잉여 데이터가 어떻게 활용되는지는 본인도 잘 모르겠다.. -_-;; () [] * 가 막 뒤섞인 복잡한 타입을 바로 읽고 쓰는 것만큼이나 도저히 이해를...;;;

더구나, forward 선언만 해 놓은 클래스인 경우, 이 클래스의 pointer-to-member는 4바이트 크기로만 잡아도 될지, 아니면 더 커야 할지를 알 수 없기 때문에 일단 최악의 경우를 가정하여 엄청 큰 사이즈가 잡힌다. 같은 포인터가 클래스 몸체의 선언 전과 후에 계산되는 크기가 달라진다는 뜻이다. (차라리 몸체가 없는 클래스의 멤버 포인터는 크기를 계산할 수 없다고 컴파일 에러를 내뱉는 게 더 낫겠다 -_-)

pointer-to-member는 꼭 필요는 하지만 이렇게 지저분한 존재가 되어 있다.
C++ 다음 표준에서는 이것도 뭔가 문법이 바뀔 거라는 식으로 들은 것 같기도 한데 잘 모르겠다. 가령, 이제 true, false도 꽤 오래 전에 정식 예약어로 추가가 됐는데, 0말고 NULL 포인터만을 가리키는 nil 같은 키워드도 type-safety를 위해서라면 좀 있어야 않나 싶다. 사실, C++이 오버로딩이 가능해지면서 타입 구분 강화 쪽으로 개선이 많이 되어 왔으며, explicit도 type-safety 보장을 위해서 추가된 키워드이다.

차라리 함수 포인터는 껍데기 함수 만들지 말고 그 함수 실체만 바로 가리키게 하면 영 안 되려나?? 지금처럼 해 놓은 이유가 있는지 잘 모르겠다.

Posted by 사무엘

2010/01/11 10:13 2010/01/11 10:13
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/88

- 클래스 이름과 파일 이름이 반드시 일치함이 보장되니, 소스 navigation이 은근히 편하다. 그리고 각 클래스 내부에 static void main 함수만 구현해 주면, 그 클래스만 용도를 테스트하는 프로그램을 간단히 짜고 그 유닛 단위로 실행이 가능하니 무척 편하다.
- 클래스에 각종 명칭의 선언과 정의가 따로 구분되어 있지 않으며, forward 선언이라든가, 온갖 dependency 따지기, 재컴파일 같은 지저분한 튜닝이 자바에는 필요하지 않다. 링크 에러라는 개념도 자바에는 존재하지 않는다. ㅋㅋ

- 모든 오브젝트들에 무조건 RTTI 정보가 들어있어서 type을 알 수 있다.
- 프로그램이 뻗으면 자동으로 함수 호출 스택 목록과 줄번호가 다 뜬다.

물론, 자바의 장점들 중 구조적인 것 말고 프로그램 실행과 관련된 편의는, 대부분 C/C++에 비해 성능을 상당히 희생하고서 얻어진 것임은 의심의 여지 없는 사실이다.

그래서 남이 짠 코드를 분석하고 들여다보고 유지보수 해야 할 때, 자바가 적응만 잘 돼 있으면 생산성이 상당히 높겠다는 점을 인정한다.
C/C++은 정말 변태스러운 튜닝과 자유도를 추구하는 대신, 그 코드가 여러 사람의 손을 거치면서 무질서도의 증가로 이어진다면 maintenance 측면에서 재앙이 될 수도 있다. 복잡한 암호 같은 C++ 코드에서 메모리 누수 하나 찾아 보시겠는가?

C/C++은 각 기계의 특성을 일일이 수용하고 존중해 준다는 점에서 이식성이 높다. 조건부 컴파일, 공용체, 포인터, 온갖 복잡한 컴파일러/링커 옵션 등등등...
하지만 자바나 C#급 언어는 그런 기계스러운 건 숨기고 포인터를 감싸고 특히 C/C++의 야생마스러운 면모를 적당히 제어하면서, 그 언어를 돌리는 플랫폼 자체를 이식성 있게 여럿 만듦으로써 이식성을 추구한다고 볼 수 있다. (자바 가상 머신, 닷넷 프레임워크 등) 즉, 언어의 근본 설계 철학과 용도가 다르다.

C++은 virtual로 지정된 놈만 가상 함수인 반면,
자바는 final이 지정되지 않은 다른 모든 놈이 기본으로 다 가상 함수이다.
가상 함수의 구현 비용이 만만찮은데, 이런 발상의 전환이 어떻게 이뤄졌는지 대단하다.

C/C++은 static이 무척 의미가 다양한 키워드인데 이는 자바도 어느 정도 이어받고 있다.
그런데 자바에만 있는 키워드로 final이 있는데, 얘가 일종의 const 역할도 하고 비가상함수임도 나타내니 문법이 무척 기발하다.

끝으로, 자바에서 아쉬운 것을 꼽자면,

- 조건부 컴파일이 안 되고, 특정 코드를 #if 0 ... #end if 이렇게 간편하게 막아 버리는 방법이 없다. -_-
- C 스타일 %d, %s로 간편하게 스트링을 포맷하는 방법이 없나? (디버그 로그 찍을 때 필요)

- 나는 자바로 범용적인 swap 함수를 어떻게 만드는지 아직도 전혀 모른다.
int a=3, b=5; swap(a,b); 이렇게 할 수가 없나? (자바에는 템플릿도, 매크로 함수도 없으며, int는 무조건 call by value로 전달됨)

Posted by 사무엘

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

비주얼 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

C++, 템플릿 이야기

오늘날 소프트웨어 업계에서 네이티브 기계어 기반 프로그램을 개발, 빌드하는 데 가장 널리 쓰이는 언어는 단연 C++이다.
C++은 잘 알다시피 C언어로부터 파생되어 C의 호환성을 유지하면서 거기에 OOP 요소를 가미한 매우 복잡한 언어이다. 클래스와 this 포인터, 생성자/소멸자. 상속, 정보 은닉, 다형성 따위는 언뜻 C처럼 보이는 코드의 함축성과 표현력을 월등히 끌어올려 주었다.

C++이 처음 등장한 것은 1983년이지만, 당대의 주류 컴퓨터 성능에 비해서 컴파일러 구현의 난해함, C++의 개념을 구현하기 위한 비용에 대한 논란 등등 때문에 실제로 C++이 업계에서 널리 쓰이기 시작한 것은 최소한 90년도가 지나서이다.

C++도 일종의 순수주의자의 관점에서 보자면 지저분한 특성, 욕 얻어먹을 면모가 굉장히 많다. 마치 윈도우라는 운영체제처럼.
하지만 상업적으로 성공하는 제품은 역시 무조건 성능이 우수한 것보다는, 현실과 이상을 적당히 잘 절충하고 대중화를 잘 한 녀석이라는 사실은 프로그래밍 언어라는 바닥에서도 적용되는 게 틀림없다.

오늘날 C++ 말고 동급 용도의 다른 어떤 언어에도 존재하지 않으며 앞으로도 차용되지 않을 기능을 뽑자면 아마 전처리기와 다중 상속이 아닌가 싶다. 강력한 만큼 괴악하고 폐단(?)이 심하기도 한 기능이어서이다. 요즘은 조건부 컴파일의 범위를 넘어서는 전처리기/매크로 기능은 거의 빠지는 추세이고 다중 상속도 인터페이스라는 다른 개념으로 대체되고 있다. 자바, C#, D 같은 언어의 스펙을 보면 그 alternative를 알 수 있다.

C++은 첫 등장한 후에도 꾸준히 변화해 왔다.
90년대 이후에는 템플릿이라는 어마어마한 기능이 추가되었으며
기능상으로 없던 게 추가된 건 아니지만, type-safety 강화 및 모호성 발생 예방을 위해서 특별히 취해진 조치도 상당하다. 가령, static_cast 같은 장황한 형변환 연산자가 추가되었으며 bool, wchar_t 같은 타입이 built-in으로 추가되었다. explicit도 이런 차원에서 추가된 키워드이다.

namespace는 각종 명칭들의 scope을 C와 같은 2차원적인 평면이 아닌 3차원적인 입체 계층으로 관리하게 해 준 엄청난 기능인 한편으로 C++ 컴파일러 구현의 난해성을 더욱 올린 기능이 아닌가 싶다. 컴파일러 개발자 내지 컴파일러를 돌리는 컴퓨터가 고생하는 만큼 프로그래머는 더 편해지고 프로그램을 유지 관리하기가 더 수월해지는 셈이다.

얼마 전엔 꽤 흔치 않은 개념을 코딩해야 할 일이 있었다.
템플릿 클래스 안에 static 멤버가 있었고, 이 멤버는 그 클래스가 자체 정의하는 구조체를 사용하고 있었다. 그래서 이 static 멤버를 클래스 바깥에서 초기화를 해 줘야 했는데, 그 멤버의 type을 클래스 바깥에서 표현이 제대로 되지 않고 자꾸 컴파일 에러가 나는 것이었다.

template<typename T>
class A {
        struct B {
        };
        static B data;
};

template<typename T>
A<T>::B A<T>::data;

(1) C++ 초창기.. 그러니까 한 터보 C++ 1.0 시절에는 static 데이터 멤버를 이렇게 바깥에서 정의 안 해 줘도 괜찮았다. 하지만 스펙이 바뀌어서 정의를 안 하면 링크 에러가 나게 나중에 바뀌었다.

(2) 또한 typename이라는 키워드도 C++에 템플릿이 추가되고 나서 한 박자 뒤에 표준화되어 90년대 중반에 도입된 것이다. 예전에는 class T만 가능했다가 문맥상 혼동을 없애기 위해 나중에 추가되었다.

어쨌든.. A라는 클래스가 템플릿이 아니거나,
혹은 data의 타입이 저런 자체 구조체가 아니라 전역 scope로 존재하는 다른 구조체 내지 그냥 built-in 타입이었다면.. 저렇게 선언하는 건 정말 일도 아니다.
그리고 솔직히 템플릿 클래스에다가 저런 식의 멤버를 만드는 일은 굉장히 희박한 것도 사실이다.

템플릿 클래스 내부의 자체 구조체로 선언된 static 멤버를 정의하는 방법은 아무리 생각해도 저것밖에 없는데 컴파일러가 도무지 말을 안 들어서 한 30분을 삽질했다.
그런데 문제 해결 방법은 기괴했다. typename을 앞에 추가하면 되더라..;;

template<typename T>
typename A<T>::B A<T>::data;

템플릿은 컴파일러가 실제로 생성해 내는 그 코드 자체가 아니라, 나중에 코드를 생성하는 틀, 말 그대로 템플릿에 불과하기 때문에 C++ 컴파일러에 템플릿이 첫 도입되었던 초창기엔 디버깅도 굉장히 어렵고, 실제로 템플릿을 A<int>처럼 인스턴스화해서 사용해 보기 전엔 컴파일 에러 자체도 잡아내기가 쉽지 않았다. 더구나 템플릿 클래스의 각종 구현부도 소스 파일이 아니라 헤더 파일에 다 선언되어 있어야 하기 때문에 한계가 많으며, 최적화 성능도 시원찮아서 code bloat의 주범이라고 욕도 먹었다. int형 코드 따로, short형 코드 따로 등등등.

하지만 지금은 상황이 많아 나아져서, 적당히 비슷한 타입으로 여러 템플릿을 사용하더라도 어지간한 건 void *형 포인터로 C 문법으로 general하게 코드를 생성할 정도로 컴파일러도 많이 똑똑해졌다. compile-time뿐만 아니라 link-time에 코드를 생성하고 한 obj 파일뿐만 아니라 여러 obj 파일 사이의 전역적인 최적화를 수행하는 기술이 도입된 것도 템플릿의 처리에 무척 유리하다. 압축으로 치면 RAR의 솔리드 압축 기능뻘 된다. 비주얼 C++은 6.0은 이런 기능이 없고, 200x닷넷급에서 최초로 이런 게 도입되었다.

C는 일단 그 가벼움과 C 특유의 이식성 때문에 영원히 절대 없어지지 않을 언어이다. (그 정도 고급 언어 중에 공용체, 비트 필드가 존재하는 언어가 또 무엇이 있을까? =_=;; C에 비트 회전 연산자가 없는 게 이상하다.)
그에 반해 C++은 동급이면서 더 깔끔하고 생산성 뛰어나고 GC(누수 메모리 자동 회수)까지 지원하는 다른 차세대 OOP 언어로 먼 미래에 대체될 수 있을 수도 있겠다는 생각이 들기도 한다. 특히 클라이언트에서 네이티브의 비중이 낮아질수록 지위가 역전되는 시기는 더욱 일러질지도 모르겠다.

윈도우 운영체제가 전통적으로 C언어식 API를 제공해 왔다면, 닷넷은 C# 기반이기도 하다.
하지만 20여 년에 가까운 컴퓨터 역사상, 무수히 쌓인 C/C++ 코드의 쪽수와 짬밥이 쉽사리 역전될 것 같지는 않기도 하고.. 미래를 예측하기란 참 어렵다. ^^;;

Posted by 사무엘

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

« Previous : 1 : ... 25 : 26 : 27 : 28 : 29 : 30 : 31 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

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

Site Stats

Total hits:
2620163
Today:
3162
Yesterday:
1544