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

비주얼 스튜디오로 C# 프로젝트를 하나 만든 후, 마법사가 생성해 준 기본 폼 애플리케이션을 debug와 release로 제각기 모두 빌드하여 실행해 본다.
그 후 이 프로젝트 디렉터리의 크기를 측정해 보라. 본인은 200KB가 채 나오지 않았다.

그런데 C/C++ 프로젝트를 만들고(MFC는 쓰지도 않고), 그냥 간단한 창만 하나 띄우는 Win32 프로그램을 debug와 release로 모두 빌드해서 실행한 후 디렉터리 크기를 재 보라.
프로젝트가 차지하는 용량은 무려 20MB가 넘는다. (비주얼 스튜디오 2008 기준)

그렇다. C/C++은 프로젝트를 만들고 빌드를 좀 하면, 잡다하게 생기는 중간(intermediate) 파일이 엄청 많다. 게다가 용량도 상당히 많이 잡아먹는다.

<날개셋> 한글 입력기 프로젝트도 32비트 디버그/릴리스, 그리고 64비트까지 빌드하다 보니 디렉터리 전체 크기가 무려 800MB에 달해 있다. 하지만 그 중 실제로 빌드에 쓰이는 소스나 데이터 파일의 합은 20~30MB대는 절대 안 넘을 것이다. -_-;;

OBJ 파일이 생기는 것이야 C/C++ 자체가 링크를 염두에 두고 만들어진 언어이니 어쩔 수 없고 그건 어차피 그렇게 크지도 않다. ..... 라고 말하려고 했는데, 오브젝트 파일도 각종 디버깅 내지 고급 최적화와 관련된 메타정보가 첨가되다 보면 단순히 소스 코드의 기계어 번역이라고 볼 수 없을 정도로 덩치가 은근히 커지긴 한다.

OBJ 말고도 디스크 용량을 상당히 차지하는 주범은 잘 알다시피 pre-compiled header이다(*.PCH) 겨우 몇몇 개의 헤더 정도나 인클루드하면 되는 정올 답안 수준의 프로그램이 아니라 특정 운영체제/플랫폼이나 거대한 라이브러리의 프로그래밍 요소를 다 인클루드하는 프로그램이라면, 그렇게 고정불변이고 덩치가 많은 요소들은 미리 컴파일을 좀 시켜 놔야지 프로그램의 빌드 시간을 줄일 수 있다.

본인이 비주얼 C++을 처음으로 쓴 게 4.2 시절부터이다. 그때엔 MFC 심볼들을 다 빌드해 놓은 pch 파일도 이미 3~5MB 정도 했던 것 같다. 하지만 지금은 그것보다 덩치가 훨씬 더 커져 있고 한 빌드 configuration당 10MB는 훌쩍 넘어간다. 프로젝트 하나 만들 때마다 50~100MB씩은 잡아야 한다. 오로지 C/C++ 언어 프로젝트만이 이런 삽질이 필요하다.

윈도우 SDK나 MFC처럼 매 프로젝트마다 일일이 빌드가 필요하지 않은 것들은 공용 PCH라는 개념으로 공유만 좀 하게 해 놔도 이런 파일의 크기를 상당 부분 줄일 수 있을 텐데 너무 낭비라는 생각이 든다. 하지만 요즘은 하드디스크 용량이 워낙 많다 보니, 빌드 시간을 네트워큭 분산 기술을 줄이려는 연구는 해도 PCH 파일 크기를 줄이려는 연구는 거의 행해지지 않는 것 같다.

이외에도 인텔리센스 데이터베이스인 NCB 파일도 은근히 크고, 매 빌드 때마다 생기는 심볼 디버그 데이터베이스인 PDB 파일도 무시 못 한다.
왜 이런 일이 생기는 것일까? 대략 다음과 같은 관점에서 살펴볼 수 있다.

첫째, C/C++은 굉장히 작은 언어이고, 프로그래밍에 필요한 요소들을 전적으로 소스와 동일한 텍스트 포맷인 헤더 파일의 파싱(느림!!)에 의존하여 읽어들이는 형태이기 때문이다. 라이브러리 링크는 별도의 계층으로 따로 존재하지, 즉시 읽어들일 수 있는 바이너리 유닛/패키지라든가(파스칼, 자바, C# 등) 언어가 자체 내장하고 있는 요소가 없다. 원천적으로 이식성을 택했지, 속도를 배려하지는 않은 느린 프로세스를 사용하는 셈이다.

둘째, C/C++은 전처리기라든가 링크 과정으로 인해 빌드가 더욱 느리고, 언어의 해석이 더디기 때문이다. 모든 토큰은 그냥 토큰이 아니라 전처리기를 재귀적으로 거치면서 다 까 봐야 실체가 드러난다. ^^;;; 헤더 파일의 글자 하나를 고치면 이 여파가 수백, 수천 개의 소스 파일에 동시에 파급되고 프로그램의 의미가 전혀 다르게 바뀔 수 있다. 이것은 장점도 있지만 똑똑한 개발 환경이나 빠른 빌드 환경을 만드는 데는 불리한 구조이다.
그러니, 소스 코드를 조금이라도 빨리 분석하기 위해서는 소스코드 자체뿐만 아니라 온갖 메타정보들을 ‘별도의 파일’로 보관할 수밖에 없다. NCB처럼 말이다.

셋째, C/C++은 그 어느 에뮬레이터나 가상 기계 계층이 없이, CPU 차원에서 기계가 직통으로 알아듣는 프로그램을 만들 수 있다. 잘 최적화된 프로그램은 사람이 원래 짠 소스 코드와는 전혀 다른 형태가 될 수도 있다. 그러니 이런 코드의 디버깅은 어려울 수밖에 없다. 변수의 내용을 확인하거나 한 줄씩 실행하는 것까지는 못 하더라도 프로그램이 뻗었을 때 스택 덤프라도 보려면 빌드된 프로그램과 소스 코드 사이의 관계를 설명하는 최소한의 정보라도 있어야 한다. 소스 코드와 형태가 전혀 딴판인 코드를 생성하는 컴파일러일수록 그런 정보는 더욱 세부적이고 양이 많아질 수밖에 없다.

한 마디로 C/C++이 정말 강력하고 포괄적이고 대인배 같은 언어이다 보니 주변에 붙는 군더더기도 많은 모양이다. ^^ 코드 생성이 여러 단계를 거치면서 매우 번거롭고 어려운 대신, 한번 만들어 진 코드는 그 어느 언어보다도 강력하다.

Posted by 사무엘

2010/02/22 21:40 2010/02/22 21:40
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/194

C# 코딩 (2)

1.
자바는 잘 알다시피 파일 하나와 클래스 하나가 완전히 일대일 대응하며, 파일 이름이 클래스 이름과 일치까지 해야 한다(여러 클래스를 선언하더라도 한 클래스의 내부에서 선언해야 함). 자바는 여러 소스 코드를 한데 모아서, 다시 말해 링크해서 exe를 만든다는 개념이 없고 그냥 소스 코드가 그대로 바이트 코드로 컴파일된 클래스 파일이 되고 거기에 있는 static void Main 함수를 실행하는 구조이다. 그런 만큼 아예 그런 형태를 언어 차원에서 강제함으로써 관리 면에서 얻는 편의도 분명 있을 것이다.

그런데 C#은 그렇지는 않다. C++과 마찬가지로, 파일과 클래스가 일대일 대응하지 않아도 된다. 한 파일에 여러 클래스가 담길 수도 있고, partial 예약어를 쓰면 반대로 한 클래스가 여러 파일에 나눠 담길 수도 있다!
(MSDN에 설명돼 있는 것처럼, 여러 사람이 한 클래스에 대해 공동 작업을 할 때나 클래스의 일부분이 다른 출처로부터 자동으로 생성된다거나 할 때 무척 유용하겠다.)

게다가 C#은 비록 닷넷 전용이긴 해도 여러 소스로부터 한 exe를 만들 수 있는 언어이다. 그렇다 보니 자바에서는 걱정할 필요가 없던 문제가 하나 생기는 게 있는데, 바로 여러 클래스들이 자기만의 static void Main 함수를 정의할 수 있다는 점이다. 이 경우 컴파일러는 에러를 발생시키며, Main 함수를 하나만 남기든가, 어느 클래스의 Main 함수를 실행시킬 지 별도의 옵션으로 지정해 달라는 메시지를 남긴다.

Main 함수가 없거나 둘 이상 존재한다는 에러는 링크 에러가 아니고 런타임 에러는 더욱 아니며, 컴파일 에러라는 게 흥미롭다. C#도 자바와 마찬가지로 링크라는 개념은 없다.

2.
C#에는 C/C++과 마찬가지로 const라는 예약어도 있고 이에 덧붙여 readonly라는 예약어도 있다. const는 컴파일 시점에서 값이 완전히 고정되어 있는 primitive 타입의 상수나 문자열만 지정할 수 있다. 그리고 C++과는 달리 const와 static을 동시에 지정할 수 없다. 어떤 멤버가 const이기만 하면 static은 당연히 자동으로 지정되기 때문이다.

그 반면 readonly는 const보다 더 동적인 값을 선언할 때나 생성자 함수에 한 번 정한 후에 그 뒤부터 고칠 수 없도록 지정할 수 있어 편리하다. primitive 타입이 아니라 new로 할당하는 개체/배열이라든가, 상수이긴 한데 값이 실행 시점에서 가변적으로 정해질 수 있는 값--함수 실행 결과처럼--이라면 readonly로 지정하면 된다.

C++은 이 둘의 경계가 모호했다. 굳이 구분하자면 일반 const 멤버는 생성자 함수에서 동적인 값으로 초기화가 가능했던 반면, static const는 컴파일 때 정적인 값으로만 초기화가 가능했다.
C#은 배열은 자바와 마찬가지로 무조건 new로 할당하기 때문에, 값이 전혀 동적이지 않는 상수 테이블 같은 것도 배열이라면 const가 아닌 readonly로 할당해야 한다.

한편, 자바는 이 점에서 C#과는 다른 재미있는 차이를 보인다. 내 기억이 맞다면 const가 없고 아마 final이 const 역할도 하며, 오버라이드 가능하지 않은 고정 함수임을 나타낼 때도 쓰이는 예약어이다. 즉, C++과 C#은 virtual이라고 별도로 지정한 함수만 가상 함수인 반면, 자바는 별도로 지정하지 않은 모든 함수가 기본적으로 가상 함수인 것이다.

그나저나, this 포인터를 const로 바꿈으로써 이 멤버 함수가 실행 결과가 read-only임을 나타내는 const는, 자바와 C#에서 어떻게 표현하는지 모르겠다.

3.
뒷북일 수도 있지만 한 마디.
유니코드가 제정된 후에 발명된 언어인 자바와 C#은, 해당 언어가 규정하는 문자 집합이 유니코드이다. 그래서 char 형의 기본 크기가 놀랍게도 2바이트이며 문자열 리터럴도 기본이 유니코드 UTF16 형태이다. 게다가 변수를 비롯한 각종 명칭 이름을 한글로 지을 수 있다. ^^ 사실, 비주얼 스튜디오에서는 C# 소스 코드 자체가 기본적으로는 BOM까지 붙어 있는 UTF8 인코딩으로 저장된다.

하지만 C/C++은 그렇지 않다. 1970년대부터 존재한 저수준 시스템 언어인 만큼, 21세기에도 C/C++ 코드는 ANSI 인코딩이다. char 형은 규정상 무조건 1바이트여야 하고 절대로 바뀔 수 없으며, ""는 기본적으로 char 문자열이고 wide 문자열은 L""이라고 접두어를 붙여서 별도로 지정해 줘야 한다.

옛날의 일부 C언어 컴파일러는 키보드로 바로 칠 수 없는 일부 기호를 다른 기호의 나열로 대체하는 시퀀스까지 존재했었다. 그렇게도 열악한 환경에서부터 쓰였던 언어와 유니코드 시대에 등장한 언어는 그 배경이 극과 극일 수밖에 없다.

문자열 타입은 그렇다 치더라도 C/C++이 현대 언어들처럼 유니코드 명칭을 받아들이는 일도 있을 수 없다. 그러려면 그렇잖아도 링크라는 계층이 존재하는 언어인데 오브젝트 파일의 포맷이 바뀌어야 하고, 심지어 운영체제의 API 구조조차 바뀌어야 할지도 모른다. 윈도우 API 중에 GetProcAddress 함수의 둘째 인자가 괜히 PCSTR인 게 아니다(PCTSTR이 아닌 일반 string). 보수적이고 이식성을 생명처럼 여기는 이쪽 바닥에서는 상상조차 할 수 없는 일인 것이다.

이처럼, 똑같이 클래스가 있고 상속이 존재하는 소위 객체 지향 언어라 하더라도 C++, C#, 자바 같은 언어들의 설계 목적과 주 용도, 빌드 단계, 문법 구조, 고안된 당시 배경을 살펴보는 것은 각 언어들의 철학을 이해하는 데 큰 도움이 될 것이다.

Posted by 사무엘

2010/02/15 18:42 2010/02/15 18:42
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/185

C# 코딩

예전에 짜 놓은 C++ 코드를 C#으로 포팅할 일이 있어서 모처럼 비주얼 스튜디오로 C# 프로젝트를 만들어 봤다. 그렇게 거창한 건 아니고, 그냥 콘솔에서 간단하게 입출력만 주고받는 150~200줄 남짓의 정올 답안 정도 되는 규모의 프로그램이다.

public void 이런 식으로 멤버 함수를 선언하는 것이나, 클래스 안에다가 함수 몸체를 다 써 넣는 것은 자바를 닮았다.
전처리기 같은 게 없고 C++보다 소스 코드 파싱이 훨씬 더 용이한 것 역시 자바와 일치한다. 사실 이건 요즘 객체 지향 언어들의 보편적인 추세이기도 하니까.

이 언어 역시 자바처럼 생산성 하나는 정말 좋겠다는 생각이 들었다. 파싱이 쉽기 때문에 IDE가 굉장히 똑똑해질 수 있다. 소스 코드 자동 리팩터링이라든가 인텔리센스 자동 완성, 코딩 컨벤션 교정 같은 것을 월등히 더 지능적으로 구현할 수 있다는 뜻.
실제로 C++ 쓰다가 C#을 쓰면 정말 그 편리함에 감탄하곤 한다.
환상적인 빌드 속도도 큰 매력이며, precompiled header 나부랭이도 없다. delete 때문에 골머리를 썩을 필요도 없다.
네이티브 바이너리를 못 만들어서 낭패이긴 하지만 말이다.

자바와 C#에는 C++에서 ::와 ->에 해당하는 연산자가 없으며 이들의 기능을 .이 모두 맡고 있다. 일단 포인터라는 게 존재하지 않으니 ->가 필요 없고, . 가 개체의 멤버 참조뿐만 아니라 scope와 namespace 식별까지 모두 맡고 있는 것이다. 그렇게 해도 문법상으로 모호해지지는 않는다.

C에는 다차원 배열이 존재하지 않으며 오직 배열의 배열 내지 다차원 포인터만이 그 개념을 대신하고 있는 반면, C#은 자유롭게 다차원 배열을 선언할 수 있다. 무슨 얘기냐 하면 실행 시간에 임의의 값으로 x*y 크기의 동적 배열을 만들 수 있다는 뜻. (C/C++은 언어 차원에서 이 간단한 일도 바로 가능하지 않다.)

C#은 콤마 연산자는 존재하지 않는 듯하다. 그리고 int 같은 기본 타입을 reference로 전달할 수 있는데, 이때는 반드시 ref를 추가해야 한다. 함수를 선언할 때 ref를 붙이는 건 마치 파스칼에서 var을 붙이는 것과 비슷한데, 이뿐만이 아니라 그런 함수를 호출할 때도 func(ref a) 이런 식으로 ref를 붙여야 하는 게 무척 신기하게 느껴졌다.

앞으로 간단한 벡터 그래픽 데모 프로그램을 짤 일이 있을 것 같은데 이것도 간단하게 C#으로 짜면 딱일 것 같다. 복잡한 최적화 루틴 같은 거 필요 없고, 그냥 결과물이 마음에 들 때까지 빌드를 엄청 자주 해야 할 텐데 C++보다 생산성이 월등히 더 뛰어날 수 있기 때문이다.
하지만 그러려면 Win32 API와는 완전히 다른 그래픽 체계를 또 공부해야 되네. -_-;;

Posted by 사무엘

2010/02/12 22:20 2010/02/12 22:20
Response
No Trackback , 9 Comments
RSS :
http://moogi.new21.org/tc/rss/response/183

내 프로그램의 중복 실행 여부를 판단하려면? (물론 윈도우 프로그래밍 기준)

실행 직후에 자신만 식별할 수 있는 이름으로 커널 오브젝트를 만들어서, 이놈의 생성 여부로 판단하는 게 제일 무난하고 안전하다. 커널 오브젝트라 함은 메모리 맵드 파일, 뮤텍스, 이벤트 등 이름의 scope가 전역적인 어느 것이라도 될 수 있겠다.

다른 방법으로 중복 실행을 판단하는 방법은 크게 윈도우 아니면 파일로 식별하는 것으로 나뉘는데, 커널 오브젝트만치 완전하지는 못하다. 그 이유를 지금부터 설명하겠다.

※ 응용 프로그램이 생성한 윈도우로 판단하는 법

FindWindow 함수로 나만이 지정하는 윈도우 클래스 이름이나 윈도우 캡션 이름을 검색하여 그게 존재하면 그 윈도우로 포커스를 옮겨 버리고 나는 실행을 종료한다. 대개, 이미 존재하는 인스턴스로 포커스를 옮겨 주는 작업이 필요할 것이므로 윈도우로 검색하는 방법은 어지간해서는 상당히 간편하고 직관적이고 좋은 방법이긴 하다. 다만,

만약 MFC 같은 프레임워크로 프로그램을 개발하고 있었다면, 메인 윈도우의 클래스 이름을 나만의 명칭으로 변경하기 위해 PreCreateWindow 같은 함수를 번거롭게 오버라이드해야 한다.

또한 클래스 이름이 아니라 캡션 이름으로 검색하는 것은 어지간해서는 피해야 한다. 캡션 이름 검색은 모든 top-level 윈도우들에 WM_GETTEXT 메시지를 보내는 방법으로 행해지기 때문에 오버헤드가 클 뿐만 아니라, 이미 실행된 내 프로그램 윈도우가 작업 중이어서 응답을 안 하고 있다면 프로그램 실행이 의도대로 되지 않을 우려가 크다.

윈도우로 검색하는 방법은 근본적으로 큰 약점이 있다. 일반적으로 프로그램이 실행된 직후 로딩, 각종 초기화를 끝내어 메인 윈도우를 생성하기까지는 적지 않은 시간이 소요된다는 것이다. 커널 오브젝트를 생성하는 것처럼 즉시 생성되는 게 아니다. 그렇기 때문에 첫 인스턴스가 아직 메인 윈도우를 만들기 전에 사용자가 실수나 고의로 또 엔터를 눌러서 둘째 인스턴스까지 실행한 경우 여전히 프로그램이 두 개가 실행되어 버릴 수가 있다. 프로그램이 어떤 경우에도 절대로 두 인스턴스 이상이 실행돼서는 안 되는 중요한 프로그램인 경우 윈도우 검색의 결과에만 의존해서는 안 된다.

※ 파일 차원에서 판단하는 법

윈도우 3.1 시절에는 WinMain 함수의 둘째 인자인 hPrevInstance를 살펴보는 것만으로도 내 프로그램의 중복 인스턴스를 판단할 수 있었다.
32비트 이후의 운영체제에서는 인스턴스 핸들의 의미가 한 주소공간 안의 포인터로 완전히 바뀌어 버렸기 때문에, 주소공간 자체가 독립적인 프로세스를 식별할 수는 없게 되었다. 오로지 그 주소공간 안에 로드되어 있는 여러 DLL 같은 모듈들만 식별할 수 있다.

그 반면, 지금도 EXE 내지 DLL 내부에 공유 가능한 섹션을 따로 생성하여 여기에 중복 인스턴스와 관련된 정보를 간단하게 집어넣을 수도 있다. 즉,
#pragma data_seg()
#pragma comment(linker, "/Section:SHARED,RWS")
이런 지시문 안에다가 전역변수를 선언하면 그 변수는 운영체제의 가상 메모리 상으로 나의 모든 인스턴스들이 공유하게 된다는 뜻이다. 자세한 것은 MSDN 참고. 번거롭게 메모리 맵드 파일 API를 호출할 필요 없이 간단한 데이터 공유에는 이 방법이 굉장히 편리하다.

이렇게 파일 차원에서 식별하는 방법은 윈도우 차원에서 식별하는 방법이 잠재적으로 갖고 있는 부작용들이 전혀 없어서 좋으나, 말 그대로 파일에 전적으로 종속적이라는 큰 한계가 있다.
같은 EXE를 이름만 바꿔 복사해서 실행한 것은 중복 인스턴스로 전혀 판단하지 못한다는 것이다. 이 점이 매우 중요하며, 이는 대부분의 경우 원치 않는 결과일 것이다. 결국 실행 파일 그 자체가 아니라 그 실행 파일이 만들어 놓은 결과를 추적해서 중복 실행을 판단하는 접근 방식이 필요하게 된다.

Posted by 사무엘

2010/02/08 22:38 2010/02/08 22:38
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/176

자기가 어느 분야에서 완전 덕후이고 전문가이면..

가령 영상 처리, 필기 인식, 음성/동영상 압축, 디바이스 드라이버, 폰트 엔진, 게임 3D 엔진, 자연어 처리, 컴파일러, 파일 압축, 데이터베이스 엔진 ....
뭐 하이튼 그런 쪽으로 회사나 연구소 하나 먹여살릴 정도의 기술이 있으면..

그 기술 분야 자체가 수요가 없어지고 사장되지 않는 한, 딱히 외공이 없어도 먹고 사는 데 지장이 없다.
컴퓨터 조립할 줄 몰라도 되고-_-, 모바일 쪽 개발 하나도 몰라도 된다.
아직까지도 윈도우 XP + 비주얼 C++ 6으로 개발한다 하더라도 기술 이사로 대접 받을 수 있다.

외공이 필요하면 외공을 갖춘 다른 개발자를 고용해서 일 시키면 된다.
사실 컴퓨터 관련 이공계 대학원은 '내공'을 쌓으라고 있는 것이다. 굳이 컴퓨터 자체만 골수로 파고들지 않아도 되며, 사실은 다른 분야와 학제간의 연구가 분야가 더욱 넓기도 하다.
단순 비트 아카데미, 게임 스쿨 같은 사설 교육기관과 근본적으로 다른 점이 바로 이것이다.

물론 전자라고 해서 외공이 전혀 필요 없다거나, 후자라고 해서 내공을 아예 등한시한다는 것은 아니지만, 추세가 그렇다는 뜻이다.

외공은 내공과는 반대이다. 한 분야에 대한 세부적인 깊이는 그리 없더라도 정말 다양한 분야를 섭렵해야 한다. 깊이 대신 넓이이다.
늘 새로운 기술과 플랫폼을 익혀야 하고 최신 IT 동향을 익히고, 처음 보는 환경에서도 기술 문서를 척 보면 바로 이해하고 잘 적응해야 한다. 그 바닥의 숲을 척 꿰뚫고 있어야 한다.

여기에다 사업 수완과 사회성, 경제 관념까지 갖춰지면, 처음엔 개발자로 시작했다가도 금세 개발자 딱지 떼고 관리자 내지 심지어 경영자의 길로 갈 수 있다. 굳이 내 손으로 개발 안 해도 된다. 앞으로 무엇을 개발해야 할지, 이 일을 누구에게 시키면 되는지 그 일만 잘 해도 내 역할 다 한 것이기 때문이다.

내공, 외공 어느 것도 시원찮으면 정말로
그냥 노가다 코딩만 하는 3D 업종 개발자의 굴레를 벗어나지 못한다.
나는 내공형인가, 외공형인가?

Posted by 사무엘

2010/01/15 14:43 2010/01/15 14:43
Response
No Trackback , a comment
RSS :
http://moogi.new21.org/tc/rss/response/144

윈도우 환경에서야, 실행 가능한 기계어 코드가 들어있는 다른 모듈을 동적으로 로드하는 것이 일도 아니다. 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

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

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/12   »
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:
3043871
Today:
1063
Yesterday:
2435