« Previous : 1 : ... 21 : 22 : 23 : 24 : 25 : 26 : 27 : 28 : 29 : Next »

C 언어는 다른 언어가 언어 차원에서 기본으로 제공해 주는 상식적인 기능이 없고, 대신 별도의 함수 호출에 의존하는 형태인 게 몇 가지 있다. 거듭제곱 연산이 대표적인 예이고, 문자열 타입도 언어가 자체 제공하지 않는다. 사실은 동적(힙) 메모리를 할당하는 기능 자체가 아예 없다.

그 이유는 간단하다. 저런 기능들은 컴퓨터 CPU 명령 차원에서 직관적으로 구현 가능하지 않기 때문이다. 그래서 연산자가 그렇게도 많다는 C 언어는 거듭제곱 연산자가 없으며 pow라는 함수를 호출해야 한다. (그나마 파스칼은 그런 함수조차도 없기 때문에, exp와 log 함수 조합으로 임의의 수의 거듭제곱을 얻어내야 한다.)

메모리 할당도 마찬가지이다. 메모리 관리는 CPU뿐만이 아니라 해당 운영체제/플랫폼이 담당하는 비중도 크기 때문에, 작은 언어인 C가 언어 차원에서 자체 제공하지는 않는 것이다. malloc, free, realloc 같은 함수를 써야 한다. 그러면 윈도우 운영체제의 C 라이브러리는 내부적으로 또 HeapCreate, HeapAlloc 같은 더 저수준의 윈도우 API를 이용해서 그런 메모리 관리 기능을 구현해 준다.

그런데 C++에서는 드디어 동적 메모리 할당과 해제 기능이 언어 차원에서 연산자로 추가되었다. 바로 new와 delete 연산자이다. 그때까지 영단어로 이루어진 연산자는 sizeof가 고작이던 것이 새로 추가되었으며, 그 후로 *_cast라든가 typeid 등 여러 영단어 연산자가 C++에 추가되었다. 메모리 할당이라면 몰라도 개체의 생성과 소멸에 따른 생성자와 소멸자 함수 호출은 언어 차원에서 책임져 줘야 하는 영역이기 때문에 별도의 연산자가 생긴 것이다.

연산자가 추가된 덕분에 일단 type casting이나 sizeof 계산을 할 필요가 없게 된 것은 좋다.

pData = new DATA[nCount];
pData = (DATA *)malloc(sizeof(DATA)*nCount);

물론 번거로운 문법 정도야 C 시절에도 매크로로 대체 가능했겠지만 말이다.

#define NEW_C(T, N)  (T *)malloc(sizeof(T)*(N))

그러나 new 연산자는 malloc 함수처럼 범용적인 void* 포인터를 되돌리는 건 지원하지 않으며, 해당 타입의 배수가 아닌 크기의 메모리도 할당할 수 없다. 그렇기 때문에 가변 길이 구조체 같은 메모리를 할당하는 건 오히려 더 불편할 수 있다.
또한 할당 아니면 해제만 지원되지 C 함수처럼 realloc 기능도 없다. C++의 메모리 연산자는 오로지 개체의 생성과 소멸에만 초점을 둔 것이다. 그렇기 때문에 이것이 기존 C의 메모리 관리 함수를 완전히 대체하지는 못할 것으로 보인다.

new 연산자로 데이터 타입을 지정한 뒤에는 new DATA[100] 처럼 배열 첨자가 올 수 있고, 아니면 new Object(x, y)처럼 해당 개체의 생성자 함수에다 넘겨 줄 인자가 올 수도 있다. 두 문법 중 오로지 하나만 허용된다.
그러므로 생성될 때 생성자 함수 인자 전달이 필요한 개체는 배열로 만들 수 없다. 그러나 인자가 필요한 생성자 함수가 존재한다 할지라도, 전부 default argument가 있어서 대체가 가능하다면 배열을 만들 수 있다.

1. new operator vs operator new

이 new 연산자(new operator)는 내부적으로 operator new라는 함수를 호출하는 형태로 구현되어 있으며, 이 특수한 함수는 나름 오버로딩이 가능하다! (delete도 마찬가지) 비록 개체를 생성하여 생성자 함수를 호출한다는 기본 기능은 C++의 특성상 불변이지만, 이 연산자가 하는 일 중 메모리를 할당하고 해제하는 계층은 customize가 된다는 뜻이다.

void *operator new(size_t size);
void operator delete(void *ptr);

operator new 함수는 첫째 인자는 무조건 포인터 크기와 같은 부호 없는 정수형이어야 한다. 부호 있는 정수형도 허용되지 않는다. 그리고 리턴값은 void *이어야 한다.
한편 delete 함수는 첫째 인자는 무조건 void *이어야 하고, 함수의 리턴값은 void여야 한다. 일단 기본적인 생김새는 malloc, free와 완전히 일치한다는 뜻.

당연한 말이지만 이 함수만 단독 호출이 가능하다.
malloc(100)을 쓸 곳에 그냥 operator new(100) 이라고만 써도 된다. 그러면 어차피 new char[100]과 비슷한 효과가 나게 된다. C++ 언어는 이 함수들의 기본 구현을 라이브러리 차원에서 제공하고 있다. 만약 기본 C/C++ 라이브러리를 사용하지 않으면서 new/delete 연산자도 쓰고 싶다면 내가 직접 이들 함수를 구현해 줘야 한다.

거기에다 나만의 인자를 추가한 operator new/delete를 만들 수 있다. 예를 들어, C/C++ 라이브러리가 사용하는 프로세스 기본 힙이 아닌 다른 곳에다가 메모리를 할당하고 싶다면 이렇게 코드를 써 주면 된다.

void *operator new(size_t size, HANDLE hHeap)
{ return HeapAlloc(hHeap, 0, size); }

HANDLE hMyHeap = HeapCreate( ... );
Object *pt = new(hMyHeap) Object( ... );

new 바로 옆에다가 전달해 주는 인자는 operator new의 둘째 이후의 인자로 전달된다. delete도 비슷한 방식으로 오버로딩 가능하다. 놀랍지 않은지?

모든 개체과 기본 자료형에서 통용되는 global scope의 operator new/delete가 있는 반면, 특정 클래스에서만 통용되는 new/delete 함수를 만들 수도 있다. 함수 프로토타입은 동일하다. 이 new/delete 함수는 굳이 static을 지정해 주지 않더라도 언제나 static으로만 선언되기 때문에, 클래스 내부에 있더라도 가상 함수 지정이나 this 포인터는 지원되지 않는다. 또한 생성자· 소멸자· 대입 연산자 등과는 달리, 파생 클래스로 상속도 된다.

2. new operator vs new[] operator

그런데, 더욱 충공깽한 사실은 new와 new[] (delete도 delete[])가 구분되어 있다는 것. 이런 구분이 언제 필요하냐 하면 소멸자 함수가 존재하는 개체의 배열을 선언할 때이다. (물론 기본 자료형이 아니라 개체를 배열로 만드는 경우는 드물지만 말이다.)
우리가 요청하는 메모리의 크기와 실제로 운영체제로부터 할당되는 메모리의 크기는 여러 가지 요인으로 인해 일치하지 않는 경우가 있으며 후자가 전자보다 대체로 더 크게 잡힌다.

배열을 delete로 해제할 때는 여기에 있던 배열 각 원소들에 대해서도 소멸자 함수를 일일이 호출해 줘야 하는데, 원래 여기에 개체가 정확하게 몇 개 있었는지를 메모리 블록만 봐서는 알 수 없게 되는 것이다.
그래서 1980년대에 C++이 처음 등장했을 때는 delete 연산자에다가 배열의 개수까지 지정을 해 줘야 했다.

int *arr = new int[nCount];
Object *ptr = new Object[nCount];
(....)
delete arr; //기본 자료형은 그냥 이렇게 지워도 무방
delete[nCount] ptr; //이놈은 흠좀무

C++은 그렇잖아도 garbage collector도 없어서 불편해 죽겠는데 배열의 원소 개수까지 프로그래머가 관리해야 한다니, 이게 말이나 되는 소리인가?

프로그래머의 원성이 빗발친 덕분에 시스템이 바뀌었다. 배열의 원소 개수는 C++이 메모리를 할당하면서 내부적으로 알아서 관리하도록 바뀌고 원소 개수를 생략 가능해졌다. 그러나 그래도 이게 배열이라는 힌트는 알아서 줘야 한다. 배열일 때와 그렇지 않을 때 C++이 메모리를 관리하고 인식하는 방식은 여전히 서로 약간 다르기 때문이다.

delete arr; delete[] ptr; 를 해도 괜찮다는 소리이지 delete arr; delete ptr; 처럼 구분이 완전히 사라진 건 아니다.

그래서 operator new/delete를 오버로드했다면 operator new[]/delete[]의 오버로드도 지원된다. 둘은 인자의 의미나 하는 일의 차이는 전혀 없다. 단지 new[]의 경우, 연산자의 리턴값 포인터에다가 곧바로 개체가 저장되지는 않는다는 차이가 존재할 뿐이다. 배열 원소 개수가 앞부분에 먼저 저장되고 그 뒤의 공간부터가 쓰인다.

자바와 C#에서 볼 수 있듯, 요즘 대세는 개체는 무조건 new로 선언하는 것이다. 그게 언어 문법까지 더 명료하게 만들어 주는 효과까지 있다. 그러나 C++은 기본 자료형이든 개체든 스택과 힙에 모두 선언 가능하고, 심지어 함수 전달도 둘 다 call by name이나 reference 방식이 모두 가능하다.

일반적으로 컴파일러들은 C++의 operator new/delete도 내부적으로는 C의 malloc/free로 구현한다. 기능이 완전히 동일한데 둘의 동작 방식이 달라야 할 이유가 전혀 없기 때문이다. 그러나 원칙대로라면 malloc으로 할당한 포인터를 delete로 해제한다거나, new로 할당한 메모리를 free로 해제하는 것은 허용되지 않는 비추 행동이다. 그렇게 섞어 쓰지는 않는 게 좋겠다.

Posted by 사무엘

2010/07/28 08:27 2010/07/28 08:27
, ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/332

C++에는 namespace라는 엄청난 키워드가 존재한다.
namespace는 소스 코드에 존재하는 수많은 명칭(심볼)들로 하여금 이들이 통용되는 구획을 강제로 구분해 준다. (명칭의 decoration도 달라지기 때문에, 링크 때도 동명의 심볼들이 서로 구분 가능함)
방대한 프로그램을 짜고 특히 남이 만든 여러 라이브러리들을 한데 뭉뚱그려 관리하다 보면 함수나 전역 변수 이름, 심지어 매크로 같은 게 겹쳐서 링크 시 충돌이 있을 수 있다. 이때 namespace는 그런 문제에 대한 근본적인 해결책이 되어 준다.

C++은 C에 비해 scope이라는 개념이 더욱 발달했다.
여기서 말하는 scope이란, 단순히 전역 변수냐 지역 변수냐 하는 생명 주기 차원이 아니라, 어떤 심볼이 언어의 문맥 차원에서 인식되고 접근이 허용되는 범위를 일컫는다.
가령, C++ 클래스 내부에 있는 static 변수는 생명 주기로 말하자면야 C의 전역 변수와 다를 바가 없다. 그러나 단순 전역 변수와는 확연하게 다른 scope을 지니고 있다. 그래서 :: 같은 연산자도 생겼다.

예전에는, 특히 C 시절에는 global이라는 기본 namespace 하나밖에 없는 것과 마찬가지였지만 C++에서는 나만의 namespace를 정의할 수 있고, 심지어 이중 삼중으로 namespace 안에 또 namespace를 만들 수도 있다. 심볼들의 입체적인 관리와 구별이 가능해진 것이다.
사실 namespace는 90년대에 나중에 추가된 키워드로, 도스 시절의 볼랜드 C++ 같은 컴파일러에서는 지원도 되지 않는다. (MFC 역시 namespace는커녕 템플릿조차 없던 시절부터 만들어져 온 클래스 라이브러리인지라, namespace를 사용한 흔적이 없음)

그런데, namespace가 하는 일은 클래스가 하는 일과 좀 중복이 있어 보인다.
클래스도 그 자체가 이미 자신만의 새로운 scope을 만들어 낸 것이기 때문이다.
클래스 내부에 public으로 선언된 static 변수 내지 함수하고,
namespace 내부에 존재하는 전역 변수 내지 함수는 언뜻 보기에 위상이 완전히 똑같다.

밖에서는 클래스::이름, 또는 namespace::이름 이렇게 ::을 써서 호칭하는 것마저 동일하다.
클래스도 안에 클래스 내지 구조체가 중첩해서 존재할 수 있으며, 심지어 클래스 내부에서만 통용되는 enum이나 typedef를 선언하는 것도 가능하다.
그럼 도대체 namespace만의 특징은 무엇이 있을까? 아래의 코드를 생각해 보자.

namespace NS {
   class A {};
   void f( A *&, int ) {}
}

//void f(NS::A *&, int) {} //이게 뭘까?

class CS {
public:
   class A {};
   static void g( A *&, int ) {}
};

이렇게만 보면 NS라는 namespace에 소속된 클래스 A와 전역 함수 f,
그리고 CS라는 클래스에 소속된 클래스 A와 전역 함수 g는 서로 그게 그거 같고 정말 차이를 발견할 수 없어 보인다.
다만, class나 struct와는 달리 namespace는 뭔가 인스턴스화하는 자료형을 만드는 것이 아니기 때문에, 닫는 중괄호 뒤에 세미콜론을 붙일 필요가 없다. 뭐, 그 정도 차이는 존재한다.

이들 각 심볼을 외부에서 접근하는 방법도 완전히 동일하다. 아래 코드를 보라.

NS::A *pfm = NULL;
NS::f(pfm, 0); //하지만 바로 f(pfm, 0)만 해도 된다. 이유는 나중에 설명

CS::A *qfm = NULL;
CS::g(qfm, 0);

그런데, namespace는 클래스에 없는 부가 기능이 좀 있다.

첫째, 바로 ADL(Argument dependent name lookup)이라는 기법이다.
C++ 컴파일러는 함수의 argument의 타입으로부터 함수의 소속 scope를 자동 추론하는 기능이 있다.
namespace NS에 속해 있는 f를 호출할 때 굳이 NS::를 할 필요가 없다.
왜냐하면 f가 받는 함수 인자 중에 이미 NS에 소속된 자료형이 존재하기 때문에, 컴파일러는 이 f를 먼저 global scope에서 살펴봐서 없으면 NS namespace 안에서도 찾아보게 된다.

함수의 인자를 이용하여 함수를 추정한다는 점에서는 함수 오버로딩의 확장판이라고 볼 수도 있겠다.
사실, 위의 소스에서 주석을 쳐 놓은 global scope의 f 함수까지 정의한다면 컴파일러는 어느 f 함수를 선택해야 할지 모호하다면서 에러를 낸다.
이런 기능은 클래스에는 존재하지 않는다. g 함수를 호출할 때는 매번 CS::g를 해 줘야 한다.

둘째, using 키워드이다.
반복되는 타이핑을 좀 줄이고 싶어하는 건 프로그래머들의 공통된 희망 사항이다.
타입 선언을 좀더 간편하게 하기 위해서 C/C++에는 typedef라는 키워드가 있고, 베이직이나 파스칼에는 구조체 참조를 좀더 간편하게 하려고 With 같은 키워드가 있다.

그와 마찬가지로 C++에는 여타 namespace에 있는 명칭을 매번 :: 연산자 없이도 바로 참조 가능하도록 using namespace 선언을 제공한다. using namespace std; 처럼 말이다.
using namespace NS를 한번 해 주면, 그 뒤부터는 NS::A *pfm 마저도 A *pfm로 축약 가능해진다.
using의 용법으로는 또 다른 것도 있는데, 설명서를 읽어 봐도 잘 모르겠다. 정말 무진장 복잡하고 저런 걸 언제 어디서 써먹으면 될지 영 감이 안 잡힌다. =_=;;
다만, namespace가 아니라 클래스에 의해 만들어진 scope에 대해서는 그런 것 역시 지원되지 않는다.

셋째, namespace p = FS; 처럼, namespace에다 별명(alias)을 붙여 쓰는 것도 가능하다. 길고 복잡한 다단계 namespace를 손쉽게 축약하는 방법이다. 저런 문법도 있다니, 가히 충격과 공포.

끝으로, 이름 없는 namespace는 마치 C 시절의 static 전역변수/함수처럼, 해당 번역 단위(소스 코드; translation unit) 바깥으로 함수나 변수 심볼이 노출되지 않게 하는 역할을 한다는 것도 알아 두면 좋다.
이 정도 되면 namespace는 C++ 언어에서는 단순히 클래스 이상으로 자신만의 역할이 있다고도 볼 수 있겠다.

가장 먼저 언급한 ADL에 대해서는 비판은 있다. namespace에다가 일종의 예외 규정을 만드는 것이나 마찬가지이기 때문에 C++ 문법을 더욱 복잡하게 하고 컴파일러 만들기도 난해한 언어로-_- 만드는 데 일조했기 때문이다. 그러나 프로그래밍의 편의를 위해서 ADL은 어쩔 수 없이 꼭 필요하기도 하다. 이게 없으면 다른 namespace에 소속되어 있는 클래스의 오트젝트에 대해서는 연산자 오버로딩조차도 제대로 못 하게 되는 경우가 생길 수도 있기 때문이다.

참고로 자바나 C#처럼 C++보다 나중에 등장한 본격 객체 지향 언어들은 C++처럼 global scope이라는 게 존재하지 않는다. 전역 함수나 전역 변수라는 게 애초부터 존재하지 않으며 모든 심볼들은 무조건 클래스에다 소속되어 있어야 한다. 또한 이런 언어들은 C++ 같은 텍스트 include라든가 링크라는 개념이 없으며, 클래스가 곧 패키지요 namespace의 형태로 구조가 잘 짜여 있다. 그래서 C++처럼 namespace를 별도로 갖고 있지는 않다.

Posted by 사무엘

2010/07/07 08:44 2010/07/07 08:44
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/313

※ CreateFont

받는 인자가 무려 14개나 되어, Win32 API 전체를 통틀어 손꼽힐 정도로 받아들이는 인자가 많다. MSDN 레퍼런스 없이는 함부로 꺼내 쓰지도 못한다.
그렇다고 해서 딱히 포인터를 전달한다거나, 결과값을 받는다거나 하는 것도 아니고 인자들은 다 단순한 int 아니면 문자열들이다. 이 값들을 LOGFONT라는 구조체에다 담아서 한꺼번에 전달하는 CreateFontIndirect라는 함수가 별도로 있기도 하다.

하지만 실제로 쓰이는 인자는 글꼴 이름과 크기(가중치 100), 그리고 아주 가끔 빌드/이탤릭 여부(가중치 20) 정도가 진짜 전부이다.
전문적인 워드/그래픽 프로그램이 아니고서야 장평값이 다르다거나 기울여진 상태로 글자를 찍을 일이 있기나 할까? 운영체제가 기본으로 지정한 퀄리티(안티앨리어싱 여부) 말고 다른 걸 지정할 일도 거의 없기는 마찬가지. 좀더 간단한 형태의 함수가 있었으면 좋겠다. MFC의 CFont::CreatePointFont()처럼 말이다.

※ CreateProcess

인자 개수 10개. 프로그램을 실행하는, 좀더 정확히 말하면 프로세스를 생성해 주는 가장 저수준 함수이다.
실생활에서는 파일 이름과 매개변수(가중치 100)만 있으면 거의 다 통용되고, 거기에다 초기 시작 디렉터리와 윈도우 초기 배치 방식을 나타내는 SW_* 값 정도만 있으면 될 것이다(가중치 20).

하지만 이 함수 역시 사용하기는 무진장 까다롭다. 파일 경로를 나타내는 버퍼는 read only여서는 안 되고 write 가능한 영역에 있어야 한다. 운영체제가 이 문자열을 그대로 strtok 스타일로 tokenize를 했다가 다시 원래 형태로 되돌려 주기 때문이다. null 문자를 잠시 삽입하므로 이는 버퍼를 수정하는 행위임.
디버거 붙일지 여부, 각종 보안 설정, 환경 변수 값, 심지어 멀티모니터 환경에서 프로그램을 초기에 띄울 모니터 위치 등등.. 별별 정보를 다 지정 가능하기 때문이다.

게다가 리턴값으로는 새로 생긴 프로세스 및 스레드의 식별자와 핸들 같은 것도 돌아오는데, 그 핸들은 이 함수를 호출한 프로세스가 알아서 Close 해 줘야 된다. 여간 손이 많이 가는 게 아니다.
MS는 프로그램을 실행할 때 16비트 시절의 잔재인 WinExec 함수를 사용하지 말고 이 함수를 사용할 것을 권하나, WinExec의 간편함(달랑 명령줄과 윈도우 배치 방식만 넘겨주면 됨!)의 유혹을 뿌리치기는 쉽지 않을 것이다. 사실 WinExec도 이제는 내부적으로 CreateProcess를 호출하는 방식으로 실행된다.

※ CreateFile

인자 개수 7개. C 표준 함수에 있는 fopen처럼 파일 이름과 용도(많지도 않다. 읽기 아니면 신규 생성)만 있었으면 좋겠지만, 역시 운영체제 API이다 보니 보안 관련, 파일 공유 여부 같은 잡다한 정보들이 엄청 많이 들어간다. 파일을 그저 문서의 열기/저장 기능 구현 용도로나 쓴다면 대부분의 세부 옵션들은 필요하지 없으며, 그냥 디폴트만 넘겨 주면 된다.
사실 이 함수는 디스크 상의 파일뿐만 아니라 파일의 형태로 표현 가능한 각종 운영체제 오브젝트들을 생성할 수도 있는 상당히 다재다능한 녀석이다. 피타고라스는 세상만물이 수라고 말했다면, 유닉스에서는 모든 게 파일이라고 한다. 윈도우 운영체제도 그런 철학이 아주 없지는 않다.

※ GetOpenFileName

우리에게 친근한 파일 열기 대화상자를 꺼내 주는 함수. 이제 얘는 인자로 일일이 입력 정보를 받는 방식을 애초부터 포기하고, 대신 크고 아름다운 구조체만을 받는 형태이다. C++로는 응당 클래스를 만들어 쓴다. MFC의 CFileDialog처럼.

하지만 실제로 자주 쓰이는 정보는 열기/저장 여부, 시작 디렉터리, 파일 포맷 필터 말고 있나? (가중치 100)
거기에다가 아주 가끔씩 파일 복수 선택 여부라든가 '읽기 전용으로 열기' 같은 자잘한 옵션만.. (가중치 30)
이렇게 굳이 구조체 만들 필요 없이 간단하게 실행이 끝나는 함수도 좀 있었으면 좋겠다는 생각을 꽤 자주 하곤 했다.

※ TaskDialogIndirect

너무나 클래식한 API인 MessageBox 함수를 대체하는 새로운 UI 대화상자 함수로, 윈도우 비스타에서 처음으로 추가된 걸로 잘 알려져 있다.
다른 건 '메시지 박스' 그대로 쓰면 되는데 "다음부터 이 메시지 표시하지 않음" 체크 같은 딱.. 2% 아쉬운 면모만 보충하고 싶을 때... 이 함수가 가히 구세주이다. 참고로 이 함수를 쓰려면 공용 컨트롤 6.0 매니페스트가 필수라는 걸 잊지 말자.

물론 얘는 TaskDialog와 TaskDialogIndirect로 아예 두 버전으로 나뉘어 있고, 쓸데없이 괜히 함수 프로토타입만 복잡한 게 아니라 진짜로 사용자가 실제로 쓸 수 있는 기능이 엄청 많은 게 맞다. 최근에 만들어진 만큼 API를 비교적 잘 설계했다는 생각이 든다. 하지만 정확하게 기억은 안 나는데 TaskDialog에는 정작 내가 꼭 써야 하는 customize 기능이 빠져 있어서 어쩔 수 없이 훨씬 더 복잡한 TaskDialogIndirect 함수만 써야 했던 것 같다. 그건 아쉬운 점.

대화상자의 제목, 큰 제목, 본문, 각 버튼의 모양, 링크, 라디오 상자 등등등...
이건 구조체가 아니라 아예 대화상자 생성 스크립트(XML 기반)를 받게 해도 이상할 게 없어 보인다.
최신 MFC에는 이 task dialog를 감싸는 클래스가 "당연히" 추가되어 있으며, 각종 상업용 GUI 패키지들에는 XP 이하 운영체제에다가도 task dialog를 자체 구현한 솔루션이 있기도 하다.

Posted by 사무엘

2010/06/29 08:53 2010/06/29 08:53
,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/306

C++의 typename 키워드

C++ typename 키워드의 용법은 크게 두 가지이다. 그러나 주된 목적은 동일하다. 다음에 나오는 명칭이 변수도 될 수 있고 변수의 타입 이름이 될 수도 있는 문맥일 때, 이것이 명백하게 후자임을 알려 주는 것이다.

먼저, typename은 잘 알다시피 템플릿 인자를 선언할 때 class 대신 쓸 수 있다.

template<class T> void Swap(T& a, T&b );
template<typename T> void Swap(T& a, T&b );

위의 두 줄은 의미상 완전히 동일하다.
템플릿이 C++ 언어에 처음으로 추가되었던 당시에는 typename이라는 키워드가 없었고 템플릿 인자를 선언할 때에도 class를 썼던 것이다.
그러나 이것은 의미상으로 문제가 있었다. 아래의 예를 보자.

template <class T, int N>
class MyClass {
public:
    T data[N];
};

MyClass<int, 20> obj;

템플릿 인자로는 잘 알다시피 자료형 이름 내지 정수 숫자가 올 수 있다.
그런데 int N에 해당하는 템플릿의 인자로는 마치 일반 함수의 인자처럼 int 값에 해당하는 20이 쓰였다. 그런데, class T에 해당하는 첫째 인자는 그럼 T라는 클래스에 속하는 개체가 쓰인단 말인가?

전혀 그렇지 않다. 여기서는 진짜로 특정 type에 속하는 20 같은 값이 아니라, type 자체가 인자로 와야 하기 때문이다.
그래서 의미상 완전히 다르다는 걸 표현하기 위해 typename이라는 키워드를 class 대신 사용할 수 있게 되었다. 매우 바람직한 조치이다. class라는 키워드는 이제 진짜로 새로운 클래스를 선언할 때만 쓰도록 하자.

그리고 다음 용법이 개념상으로 진짜 중요하다. scope resolution과 관계가 있다.
A가 클래스 이름일 때 A::B라는 표현을 썼다면, C++의 특성상 B는 A의 멤버 변수일 수도 있고, A 클래스 내부에 선언된 다른 타입(클래스, 구조체 따위)의 이름일 수도 있다. 그 클래스 내부에 무엇이 선언돼 있냐에 따라서 해석이 달라진다. 처리가 어렵다.

그런데 설상가상으로 A 자체가 실제로 무슨 타입이 들어올지 모르는 템플릿 클래스의 인자라면? B에 대한 해석은 그야말로 귀에 걸면 귀걸이, 코에 걸면 코걸이가 될 수밖에 없어진다. A의 실체가 무엇이건 B의 정체는 컴파일 시점 때 다 결정되어야 하는데 말이다.

그럴 때 typename A::B를 써 주면, B는 A가 무엇이건 상관없이 변수가 아니라 말 그대로 type 이름으로 처리되어야 함을 컴파일러에게 알려 준다. 이 키워드는 절대적인 모호성 해결보다는, C++의 문법 해석의 복잡성을 좀 줄이고 컴파일러 개발을 더 수월하게 만들려는 목적이 더 크다고 보면 정확하다.
자, 이번에도 이해를 돕기 위해 예제 코드를 보자.

template<typename T>
class MyClass {
public:
    struct MYSTRUCT {
    };
    static MYSTRUCT dat;

    typename T::COMP pp;
};

struct SAMPLE {
    struct COMP {
    };
};

MyClass<SAMPLE> obj;

바로 이런 식으로 MyClass가, 자신의 템플릿 인자 T가 내부적으로 또 갖고 있는 COMP라는 구조체를 이용하기 위해서는 pp를 저렇게 typename을 줘서 선언해야 한다. COMP를 자료형 이름임을 명확하게 해 줄 필요가 있다.

비슷한 이유로 인해,

template<typename T>
typename MyClass<T>::MYSTRUCT MyClass<T>::dat;

템플릿 클래스 내부에 있든 구조체 형태로 된 static 멤버를 밖에서 또 정의해 줄 때, typename을 넣어 줘야 한다.
똑같이 MyClass<T>::로 시작하는 명칭이지만 typename이 선행된 MYSTRUCT는 자료형이고, dat는 멤버 변수로 인식되는 근거가 여기에 있는 것이다.
늘 생각하는 것이지만 C++의 세계는 참으로 심오하다. -_-;;

Posted by 사무엘

2010/06/28 08:56 2010/06/28 08:56
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/305

프로그램의 권한

1.
병특 회사에서 근무하던 시절의 일이다.
그때 본인은 본업을 넘어-_-, ActiveX 컨트롤을 만들어 관리하던 적이 있었다.
(사용자들이 아무리 ActiveX 욕하고 우리나라가 무슨 MS 공화국이네 뭐네 하면서 까도, 일선 개발자들은 위에서 까라면 깔 수밖에 없다. 더구나 본인은 국방부 시계가 돌아가던 중! ^^;;; )
ActiveX는 내부에 플래시 UI를 하나 만들어서 플래시는 웹 상에서 사용자와 소통을 하고, ActiveX는 플래시와 소통을 하면서 플래시만으로 하기 힘든 네이티브 코드를 수행했다. 내가 왕년에 저런 일까지 했다니..

그런데 문제가 있었다. 정확하게는 기억이 안 나지만, ActiveX든 플래시든.. 뭔가 로컬에 있는 파일을 참조하는 건 아무 문제가 없었는데 웹에 있는 놈을 가져오는 건 아무 이유 없이 도무지 되지 않고 작동을 거부하는 것이었다.
먼 옛날 일이 됐으니 망정이지, 이것 때문에 당시 회사에 환멸을 느낄 정도로 좌절하고 삽질했었다. -_-;;

문제의 원인은 보안이었다. 그 당시 갓 출시된 플래시 7이던가 8이던가.. 하필 그때부터 보안 정책이 딱 바뀌어 플래시의 액션스크립트는 아무 웹에서나 정보를 덥석 가져오지 못하게 되었다.
그 반면, 내 로컬 컴퓨터의 ....\flash player\#security\flashplayertrust 이런 디렉터리에다가 configuration file을 만들어서 접근을 허용하는 웹 주소를 먼저 적어 줘야 하고, 플래시는 허용된 웹으로만 접근할 수 있다.
자세한 정보: http://kb2.adobe.com/cps/116/1165eb90.html

어쨌든 이것 때문에.. 가장 권한이 많고 강력한 ActiveX가 DllRegisterServer를 통해 등록될 때 저 flashplayertrust에다가 우리 플래시에 대한 정보를 덩달아 등록해 주고, 등록 해제될 때 그 정보를 삭제하도록 함으로써 문제는 일단락되었던 걸로 기억한다. ActiveX는 네이티브 코드인 관계로 파일, 레지스트리, 웹 등에 다 접근이 되고, 심지어 Win32 API를 직접 호출해서 뭐든 다 할 수 있으니.. 사기 유닛이다.

물론 오늘날은 다른 웹 표준과 RIA 기술도 풍부한데 저런 무식한 방법을 쓰는 건 곤란하다.
참고로 플래시에 전설의 flv 동영상이 추가된 게 그 무렵부터일 것이다. 유튜브가 그때 막 태동했으니 말이다. 플래시가 벡터 드로잉 애니메이션뿐만 아니라 일반 비디오 플레이어 분야도 섭렵하기 시작했으며, 덕분에 이제 인터넷으로 동영상 볼 때 ActiveX 설치를 요구하는 사이트는 개념 없다는 소리를 듣기 시작하게 됐다.

2.
안드로이드 어플 만들면서도 비슷한 경험을 했다. 아놔 다른 프로그램에서는 잘 되는 환경설정 변경이 왜 도대체 안 되고, 기껏 되더라도 왜 내가 바꾼 환경설정이 다른 곳에 도무지 적용이 안 되는지.. 함수 호출 결과는 성공인데.. 그 뒤 결과는 그냥 씹히고 있던 것이다.
하루를 삽질하고 났더니 원인은 역시 매니페스트 파일에다가 android.permission.WRITE_SETTINGS , android.permission.CHANGE_CONFIGURATION 요 따위 퍼미션 요청을 안 해 놨기 때문이었다.

3.
그동안 유닉스 계열 OS에 비해 권한이나 보안 같은 관념이 너무 약하던 윈도우도, 비스타부터는 그쪽으로 좀더 엄격해졌다.
잘 알다시피 사용자 계정 컨트롤(UAC)라는 게 추가됐으며, 프로그램을 관리자 권한으로 실행할 때와 그렇지 않을 때에 허용되는 권한의 차이가 매우 커졌다. 가령, 관리자 권한이 아니면 '내 문서' 말고 다른 디렉터리에다가는 파일을 제대로 만들지도 못한다.

그리고 이 프로그램이 요구하는 권한을 명시하는 게 가능해졌다.
아무 권한에서나 실행 가능한지, 무조건 관리자 모드에서 실행돼야 하는지 하는 걸 말이다.
지정은 EXE 내부의 매니페스트 XML에다가 하면 된다. 그 개념은 이미 윈도우 XP에서 시스템 DLL의 로딩 방식을 제어하기 위해 도입된 바 있으므로 새삼스러울 게 없다.
비주얼 C++ 2008부터는 링커 옵션에 이걸 바로 지정해 주는 게 추가됐다. 그 이전 버전에서는 사용자가 직접 xml 파일을 손으로 써서 링크해 주면 된다.

스크린 키보드처럼 장애인 Accessbility용 프로그램은 의외로 높은 보안 수준이 필요하다.
내가 받은 입력에 대한 결과를 시스템 모든 프로그램에다가 끼쳐야(키보드 입력 흉내) 하기 때문에
이런 프로그램은 별도의 인증을 거쳐야 운영체제가 그 정도의 권한을 허락하게 되어 있다.
그런 인증을 거치지 않은 "<날개셋> 입력 패드"는, 사용자가 직접 관리자 권한으로 실행해 주지 않으면,
자기보다 권한 등급이 높은 프로그램에다가는 글자 입력을 전달해 줄 수 없다.

글을 맺는 소감:
삽질해야 하는 게 싫다. -_-;;
지금 유닉스 명령어 익히느라 땀 뻘뻘 흘리는 걸 보면, 옛날에 지금보다 영어도 훨씬 더 못 하던 시절에 도스 명령은 어째 알아서 외웠는지 궁금하다.
지금 이놈의 안드로이드 때문에 삽질하는 걸 보면, 옛날에 윈도우 API는 어째 공부했는지 내가 생각해도 나 자신을 이해할 수 없다.
그때는 삽질을 삽질이라고 여기지 않고 전적으로 재미로 했기 때문에 프로그래밍에 재미를 붙일 수 있었던 것 같다.

Posted by 사무엘

2010/06/22 08:55 2010/06/22 08:55
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/300

C++의 템플릿은 두말할 나위도 없이 매우 강력하고 유용한 개념이다.
C++에다가 제네릭/메타 프로그래밍--프로그램을 만드는 프로그램.. 즉 더욱 추상화된 기법--의 가능성을 무한히 열어 줬기 때문이다.

swap, min, max 같은 것부터 시작해서
옛날에는 문법적으로 매우 불완전하기 짝이 없는 매크로를 쓰고 위험한 typecasting을 해야 했던 것을, 템플릿 덕분에 언어의 정식 문법으로 아주 깔끔하고 type-safe하게 구현할 수 있게 된 게 많다.

그리고 static 배열의 크기(원소 개수)를 되돌리는 매크로인 ARRAYSIZE도 생각해 보자.
C 시절에는 sizeof(x)/sizeof(x[0]) 와 같은 식으로 구현했다. 물론 이 값들은 모두 컴파일 시간 때 이미 결정이 되기 때문에 실제로 나눗셈 연산이 일어나지는 않는다.

하지만 템플릿을 이용하면...
template<typename T, size_t N> char (*__GetArraySize(T (&n)[N]))[N];
#define ARRAYSIZE(x)   sizeof(*__GetArraySize(x))

자 이게 무슨 의미인지 이해가 가시는가?
무슨 타입인지는 모르겠지만 어쨌든 N개짜리 배열의 참조자를 받아서 N개짜리 char형 배열의 포인터를 되돌리는 함수를 템플릿으로 선언한다.
그 후, N의 함수의 리턴값을 역참조한 배열의 크기를 sizeof로 구하면... 당연히 char형 배열의 크기는 본디 배열의 원소 개수와 일치할 수밖에 없게 된다. 함수의 몸체를 정의할 필요조차 없다! 마치 &( ((DATA*)NULL)->member ) 가 에러를 일으키지 않고 멤버 오프셋을 되돌려 주는 것과 같은 이치이다.

템플릿이 아예 배열의 참조자를 받게 함으로써 좋은 점은, 이 매크로에다가 static 배열이 아니라 단순 포인터를 집어넣으면 컴파일 에러가 발생하게 된다는 것이다. 단순 sizeof(A)/sizeof(A[0])보다 타입 safety도 보장되고 훨씬 더 좋다.
(), *, [] 등이 뒤섞인 복잡 난해한 C/C++ type string 읽는 법에 대해서는 이전에 별도로 글로 다룬 바 있다.

template<typename T, size_t N> size_t ARRAYSIZE(T (&n)[N]) { return N; }

물론 위와 같이 코드를 쓰면 더 간단하고 알아보기도 쉽게 동일한 효과를 이룰 수 있지만, 최적화를 하지 않은 디버그 빌드는 불필요한 함수 호출이 계속 일어나는 문제가 있다. 배열의 크기 정도는 컴파일 타임 때 모든 계산이 딱 일어나게 하는 방법을 쓰는 게 더 좋을 것이다.

이렇게 템플릿은 매우 편리한 개념이긴 하나, 한계도 분명 있다.
템플릿은 동일한 패턴을 지닌 여러 다른 코드들을 찍어내는 ‘틀’과 같다. 하지만 이 틀 자체는 한 번만 정의해 놓고 링크 타임 때 여러 오브젝트 파일 사이를 자유자재로 넘나들게 할 수는 없다. 최적화처럼 기술적으로 여러 난관이 있기 때문이다. 템플릿 인자로 들어온 타입이 int일 때, double일 때, 심지어 개당 100바이트가 넘는 구조체일 때 이들에 대한 각종 비교나 대입 연산과 최적화 방식과 코드 생성 방식은 완전히 천차만별이 될 수밖에 없다.

이런 이유로 인해 템플릿의 정의 효과는 오로지 한 소스 코드, 한 translation unit 안에서만 유효하며, 템플릿 클래스나 함수는 모든 소스 코드에 헤더 파일의 형태로 매번 include되어야 한다. 몸체까지(body; definition; implementation) 죄다 말이다. 일반적인 클래스의 함수처럼 선언 따로, 정의 따로일 수가 없다. 사실은 템플릿 코드에 대한 에러 체킹 자체도 템플릿이 인자가 들어와서 어떤 형태로든 실현(realize)이 됐을 때에야 할 수 있다.

그러니 템플릿으로 구축된 각종 함수와 클래스는 소스 코드가 노출될 수밖에 없으며, 그 소스 코드를 고치면 템플릿을 include하는 모든 소스 파일들이 재컴파일되어야 하는 등 프로그래밍 상의 한계가 결코 만만한 수준이 아니다. 하지만 이 한계는 C++ 언어의 컴파일/링크 모델이라든가 기존 컴파일러들의 오브젝트 파일 포맷 내지 컴파일러/링커의 동작 방식이 근본적으로 바뀌지 않는 한 극복되기 쉽지 않을 것이다.

이런 구조적인 불편을 해소하고자 C++ 표준 위원회가 제안한 것은 export 키워드이다.
흔히 import/export하면 윈도우 프로그래밍 세상에서는 DLL 심볼을 내놓거나 가져오는 개념을 떠올리는데, 이 문맥에서는 그런 건 아니다. 한 translation unit에 존재하는 템플릿 함수 구현체를 다른 translation unit이 그 경계를 초월하여 링크 타임 때 가져다 쓸 수 있게 하는 흠좀무한 개념이다. 즉, 템플릿 몸체에 대한 export를 뜻한다.

헤더 파일에다가는

export template<typename T> void Swap(T& a, T& b);

이라고 해 놓고 모처의 cpp 파일 한 군데에다가는

export template<typename T> void Swap(T& a, T& b)
{
    T c(a); a=b, b=c;
}

이런 식으로 써 놓음으로써,
Swap 함수는 export라고 마크가 되어 있으니 자기 translation unit에서만 쓰지 말고, 다른 단위에서도 필요하면 링크 때 가져다 쓸 수 있게 한다는 게 당초 의도였던 모양이다. (인터넷 검색을 해 보니)
템플릿으로 들어간 클래스 멤버 함수에 대해서도 마찬가지이다. export가 없으면 저 함수 body는 마치 static 변수/함수처럼 그 소스 파일 내부에서만 유효하고 다른 소스 파일에서는 링크 에러가 나게 될 것이다.

그러나 이것을 본 컴파일러 개발사들은 '이뭐병, 이딴 걸 무슨 얼어죽을 표준안이라고 내놓냐' 하는 반응이었고..
비주얼 C++, gcc 등 유수의 컴파일러들은 이 키워드의 구현을 포기/거부하고 말았다.
비주얼 C++의 경우 도움말에 Nonstandard Behavior로 자기는 이 키워드를 지원하지 않는다고 당당히 명시까지 되어 있다. 2010은 모르겠고 2008까지도 마찬가지임.
하지만 마이너급 컴파일러 중엔 export를 구현 안 한 놈이 없는 건 아니라고 한다. 흠좀무.

컴파일해 놓은 코드를 짜깁기만 하는 게 링크인데, 저걸 구현하려면 링크에다가 다시 컴파일을 하고 재링크(?) 과정을 집어넣어야 한다. C/C++의 정상적인 빌드 루트와는 정면으로 모순되는 과정을 요구하는 것이다. 그래서 컴파일러 개발사들이 떡실신한 것이다.

어쨌든 이런 이유로 인해서 export는 C++의 흑역사 키워드로 전락해 있다.
옛날에 MS는 링크 과정을 최대한 간단하게 만들려고 COFF 방식 obj 파일과 PE 방식 exe 파일을 채택했다고 하던데, 하지만 요즘은 워낙 translation unit을 넘나드는 링크 타임의 코드 생성과 전역 최적화 같은 기술이 대세가 돼 있다 보니, export 키워드의 의도가 옛날만치 그저 병맛나게 들리지만은 않을 것 같은 생각이 들기도 한다.

마치 유명무실하던 auto가 리모델링되고 서울 지하철 5호선 마곡 역이 13년만에 부활했듯이 export 키워드도 의도 자체는 좋은데.. 언젠가 부활할 날이 올 수 있지 않을까? 하지만 C++의 차세대 표준인 C++0x에서는 export를 아예 빼 버리고 백지화하자는 제안까지 나온 상태이니, 과연 지못미이다.

Posted by 사무엘

2010/06/12 09:27 2010/06/12 09:27
, ,
Response
A trackback , 11 Comments
RSS :
http://moogi.new21.org/tc/rss/response/293

C/C++의 type string은 간단한 건 간단하지만 복잡한 건 한없이 복잡하다. C/C++ 프로그래밍 경력 10년이 넘는 본인조차 아직 그런 쪽에는 능숙하지 않으며, 좀 복잡한 type 선언을 해야 하면 옛날에 짜 놓은 코드를 복사해서 가져온다. -_-

복잡한 게 뭔지를 물으신다면, 이런 것을 말한다. 특정 함수의 포인터, 배열의 포인터를 되돌리는 함수의 포인터, 포인터의 참조자, C++ 멤버 포인터 등등... 생각만 해도 머리가 뱅뱅 돌지 않는지?

C/C++에서 뭔가 명칭을 선언하는 건 아래와 같이 일면 단순하다. 간단한 것, 상식적인 것부터 살펴보자.

type p;

이렇게 써 주면 p라는 명칭은 type이라는 타입으로 선언된다. p는 변수가 될 수도 있고 함수도 될 수도 있고 포인터나 배열 변수가 될 수도 있다. C++은 함수 내부의 아무 위치에서나 변수를 선언할 수 있으나, 함수 안에서 또 함수를 선언할 수는 없다. nested 함수라는 개념이 존재하지 않는 것이다.

type a, b, c;

처럼 콤마를 써서 여러 명칭을 동일 type으로 동시에 선언할 수도 있다.
type에는 int, float 같은 built-in type이 들어갈 수 있고, 사용자가 예전에 정의한 구조체· 공용체나 클래스가 들어갈 수도 있다.

C에서는 구조체· 공용체의 명칭 앞에 struct나 union 키워드를 생략할 수 없으며 생략하려면 typedef를 별도로 만들어야 하는 부조리가 있었으나, C++에서는 그런 한계가 없어졌다. type이 템플릿인 경우, 템플릿을 실제로 만들어 내는 argument도 < >에다 둘러싸서 넣어 줘야 하며, 타입 명칭이 다른 scope에 존재할 경우 :: 연산자도 써 줘야 한다. std::vector<int>처럼.

type 명칭에는 이 변수의 성격을 규정하는 modifier 키워드도 선택사항으로 들어갈 수 있다. 이런 예로는 const, volatile, register 같은 키워드가 있다.

type에 대한 설명은 여기까지로 하고, 그럼 p(명칭)에 대해 알아보자.
명칭은 한 번에 여러 개를 동시에 선언할 수 있고, 또 원한다면 p=1처럼 =을 써서 선언과 동시에 초기화도 가능하다. C++의 경우, 아예 ()을 써서 생성자 함수 호출을 바로 시키는 것도 가능하며 built-in type에 대해서도 생성자 함수 호출하듯 값을 초기화할 수 있다. 즉,

int *a=NULL, b=7; /* C style */
int *a(NULL), b(7); //C++ style

C에서는 위의 문장만 허용되는 반면 C++은 아래의 문장도 허용된다는 뜻이다.

자, 그럼 이제 진짜 복잡한 부분으로 들어가 보겠다.
C/C++의 문법이 판타지 같은 이유는, 분명 명칭의 type과 관련된 modifier들이 type 부분에 확실하게 구분되어 있는 게 아니라 name 부분으로 개별 적용되는 것도 있기 때문이다. 그렇기 때문에 C/C++은

int *a, b;

라고 선언하면 *라는 modifier는 a에만 적용되어 a만 int형에 대한 포인터가 되고 b는 일반 int가 되는 것이다. 그런데 D라는 언어는 그렇지 않아서 위와 같이 선언하면 a와 b의 타입이 모두 int*가 된다.

이런 식으로 개별적으로 적용되는 modifier로는 다음과 같은 것이 있다. 이런 것들이 막 섞이면 사람 머리 터지게 만든다. ^^;;

*p : p가 포인터임을 뜻한다. 변수의 왼쪽에 붙으며, 오른쪽에서 왼쪽으로 해석한다. *가 여러 개 붙으면 2중, 3중 포인터가 될 수 있다. (pointer to)
&p : C++에서 추가된 문법이며, p가 참조자임을 뜻한다. 쓰임이 포인터보다 훨씬 제한적이기 때문에 다중으로 붙을 수 없다. 용법은 *와 동일. (reference to)

int *&p;

라고 하면 우에서 좌로 & → * 순으로 해석되어 p는 포인터의 참조자가 된다(a reference to a pointer to integer). 반대로 참조자를 가리키는 포인터라든가 참조자를 또 가리키는 참조자라는 개념은 C++에 없기 때문에, &*나 && 같은 문법은 틀렸다. 포인터의 문법을 간소화하려고 만든 게 참조자인데 이는 상식적으로 당연한 얘기. 하지만 이중 포인터의 참조자인 **&은 있을 수 있다. 이 정도면 *와 &의 관계는 충분히 설명됐을 것이다.
다음,

p() : 어떤 명칭 바로 오른쪽에 ()가 붙었다면 이는 그 명칭이 함수임을 뜻한다. 쉽다.

p[n] : 그 명칭이 배열임을 뜻한다. 첨자가 들어있어야 하는 게 원칙이지만, 함수 argument라든가 일부 1차원적인 문맥에서는 첨자가 생략되어서 포인터와 별 차이 없는 용법이 되기도 한다. 영어로는 array of에 해당. []가 오른쪽 끝에 계속 붙으면 다차원 배열을 만들 수 있다.

그렇다면 명칭의 왼쪽에 포인터가, 오른쪽에 ()나 []가 다 붙어 있으면 어떻게 해석해야 할까?
일단 오른쪽 것부터 해석한다. 그 후 오른쪽 끝에 도달하면 왼쪽으로 간다. 그래서

int *a[10];

은 []이 먼저 해석되어 array of / pointer to / int가 되고, 따라서 ‘int *가 10개 있는 배열’이 된다.
이 순서를 바꾸기 위해서 또 괄호가 사용된다. 함수를 뜻하는 ()와는 쓰이는 문맥이 다르며, 의미도 다르다. 이걸 아는 게 중요하다.

int (*a)[10];

은 *이 먼저 해석된 후 오른쪽의 배열로 넘어가서 pointer to array[10] of int가 되고, 따라서 배열의 포인터가 된다. 사실, C/C++의 type string은 일종의 영어 어순을 따르고 있는 셈이다. 이걸 알면 쉽다. 꼭 기억하자.

int func(int x);
int (*funcptr)(int x) = func;

명칭 다음에 곧바로 ()가 나오면 함수 선언이 되나, 이름이 괄호로 둘러싸여서 *가 먼저 해석되므로 funcptr은 pointer to function, 즉 함수의 포인터가 되고, 자신과 prototype이 완전히 같은 func라는 함수를 가리킬 수 있게 되는 것이다.

닫는 괄호를 만나면 아직 해석되지 않았던 왼쪽으로 이동하고, 그러다가 여는 괄호를 만나면 다시 닫는 괄호 바깥의 오른쪽으로 가면서 완전히 바깥에 도달할 때까지 이 과정을 반복하면 된다.
따라서 명칭 뒤에 붙는 (), *, [] 같은 게 아무리 복잡하더라도, 명칭의 좌우에 가장 가까이 붙어 있는 놈이 뭔지만 보면, 얘가 포인터인지 함수인지 배열인지 정도는 바로 알 수 있다.

double ( *varr( double (*)[3] ) )[3];

위는 배열의 포인터와 함수의 포인터가 모두 동원된 예이다. 슬슬 머리가 아파질 것이다. varr의 좌우로 *와 ()가 있는데, 이때 오른쪽으로 먼저 간다. 그래서 varr은 함수가 되고 왼쪽의 *는 함수의 리턴값과 관계가 있게 된다. 그렇다. 이놈은 double 형 배열의 포인터를 인자로 받는 함수인데, 이 함수의 리턴값 역시 double 형 배열의 포인터라는 뜻이다.

double (* (*pfnFunc)( double (*)[3] ) )[3] = varr;

그리고 저 varr을 가리키는 함수의 포인터는.. varr만 (*pfnFunc)라고 또 감싸 주면 만들 수 있다. ^^;; 포인터를 되돌리는 함수의 포인터인 것이다.

int *(*(*fp1)(int))[10];

굉장히 변태-_-스러운 예제인데, 별표를 맨 왼쪽에 있는 것부터 [1], [2], [3]으로 번호를 매기자면,
fp1은 int 형을 인자로 받고, 원소 개수가 10인 int 포인터[1]의 배열에 대한 포인터[2]를 되돌리는 함수의 포인터[3]이다.

pointer to *
function (int)
returning pointer to *
array [10] of int*

이제 진짜 궁극의 변태 같은 예를 들면,

char *(*(**foo[2][8])())[10];

array [2][8] of
pointer to **
function ()
returning pointer to *
array [10] of char*

다시 말해 char*가 10개 들어있는 배열의 포인터를 되돌리는 함수의 2중 포인터를 담고 있는 2차원 배열이라는 소리이다. ^^;;

그럼 마지막으로, 또 하나의 기괴한 C++ 문법을 소개하면서 글을 맺겠다. 그것은 바로 멤버 포인터라는 특이한 포인터이다.

class CMyObject {
public:
 int x,y,z;
 void foo() {}
 void bar() {}
};

CMyObject obj;
int CMyObject::*pVal = &CMyObject::x;
void (CMyObject::*pFunc)() = &CMyObject::foo;

obj.*pVal = 10;
(obj.*pFunc)();

위의 코드에서 볼 수 있듯 pVal은 int형인 x, y, z중 한 멤버 변수를 가리킬 수 있고, pFunc는 자신과 prototype이 같은 foo()와 bar() 중 하나를 가리킬 수 있다.
일반적인 C++ 클래스의 non-static 멤버들은 멤버 포인터로 하여금 자신을 가리키게 할 때 "&클래스::멤버"와 같은 식으로 주소를 얻을 수 있다. 이때 어느 토큰 하나도 생략할 수 없다. 심지어 자기 클래스 멤버 함수 내부에서라도 자기 클래스 이름을 반드시 명시해야 한다.

멤버 포인터를 나타내는 ::*은 ::와 *가 합쳐진 것이다. 그러나 멤버 포인터를 실제로 사용하는 연산자인 .* 또는 ->* 는 완전히 한 토큰으로, 사이를 띄울 수 없다. 또한 멤버 포인터 함수를 선언하고 호출할 때는 반드시 괄호가 필요하다. 이걸 하지 않으면 오른쪽의 함수 호출 ()가 먼저 해석되어서 개체와 멤버 포인터가 먼저 연결되지 못하기 때문이라 한다.
마치 파스칼 언어에서 우선순위 처리의 특이점 때문에 (a=1) and (b>5)처럼 각 항을 괄호로 싸 줘야 하는 것과 비슷한 맥락이라 하겠다.

그나저나 C++은 :: . -> 이렇게 세 연산자가 모두 따로 존재하는 언어라는 게 특이하다. 자바나 C#은 . 하나가 이들 기능을 모두 수행한다.

Posted by 사무엘

2010/05/29 15:20 2010/05/29 15:20
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/279

파일 대화상자

1. 파일 대화상자라는 개념

윈도우 운영체제.. 사실 이뿐만이 아니라 플랫폼을 불문하고 현대적인 GUI 프레임워크들은
불러오기/저장하기(파일 선택) 기능을 공통 기능으로 제공한다.

어떤 형태로든 사용자가 작업하는 문서(데이터)를 파일로 읽고 쓰는 소프트웨어치고 이 기능을 안 쓰는 녀석은 없으므로, 이건 가히 필수 공통 기능이라 하기에 손색이 없다. 도스 시절에는 불러오기/저장하기 UI도 프로그래머가 제각각으로 직접 구현해야 했으니 얼마나 번거로웠는지 모른다. 이거 하나만 운영체제가 고수준 API를 제공해 줌으로써 응용 프로그램이 직접 FindFirstFile, FindNextFile 같은 파일 탐색 함수를 호출해야 할 일은 상당수 없어졌다.

물론 이것 말고도 색깔을 찍는 대화상자, 인쇄 대화상자 같은 것도 공통 대화상자에 속하며 운영체제가 제공해 주는 게 있다. 그리고 필요한 경우, 대화상자의 일부 요소나 동작 방식을 프로그래머가 자기 입맛에 맞게 customize하는 테크닉도 응당 제공된다.

2. 윈도우 운영체제의 파일 대화상자의 역사

윈도우 3.x의 파일 대화상자는 파일 목록과 디렉터리 목록이 리스트 박스의 형태로 좌우로 나란히 나오고, 그 아래에는 저장하거나 열려는 파일의 format 그리고 드라이브 목록이 콤보 박스의 형태로 나란히 나와 있었다. 드라이브-디렉터리-파일이라는 클래식한 형태에 충실한 디자인이라 하겠다. 가정에서는 네트웍이 아직 많이 생소하던 시절이었기 때문에, 네트웍 드라이브에 접근하기 위해서는 별도의 버튼을 눌러야 했다.

사용자 삽입 이미지

그러던 것이 윈도우 95/NT4에 와서는 크게 바뀌었다. 사실 윈도우 95부터는 쉘의 디자인의 근간이 싹 바뀌어 오늘날까지 이어져 오고 있다. 드라이브를 제공하는 장치간의 구분이 완화되었고, 가장 최상위 계층인 바탕 화면 아래로 내 컴퓨터, 휴지통 등등이 따르는 컨셉이 이때 도입된 것이다. 파일 리스트는 구닥다리 리스트 박스 대신, 소위 공용 컨트롤이라고 불리는 리스트뷰 컨트롤 기반으로 바뀌고 디렉터리와 드라이브는 폴더라는 개념으로 바뀌었다. 그리고 탐색기가 제공하는 쉘 기능(파일 복사, 붙여넣기, 삭제, 개명, 각종 우클릭 메뉴)은 파일 대화상자 내부에서도 그대로 쓸 수 있게 된 것 역시 큰 변화이다.

사용자 삽입 이미지

윈도우 98부터는 파일 대화상자의 크기 조절이 가능해졌다. 상당히 바람직한 변화이다.

윈도우 2000/ME부터는 파일 리스트 왼쪽에 바탕 화면, 내 문서 등 주요 폴더로 곧바로 이동하는 아이콘이 추가되었다(일명 favorite bar). 그리고 아마 이 무렵부터, 파일이나 디렉터리 이름의 처음 몇 자를 입력하면 자동 완성 후보가 뜨는 아주 편리한 기능이 생겼다.

이 구조가 윈도우 XP까지 이어지다가 비스타/7부터는 또 파일 대화상자가 싹 바뀌었다. 변화의 양상에 대해 한 마디로 요약하자면, 탐색기와의 경계가 더욱 모호해지고 좀더 "웹처럼"(webby) 바뀌었다. 웹 페이지 탐색하는 기분으로 내 컴퓨터의 폴더를 탐색하게 되었는데, 이는 IE4부터 MS에서 부르짖은 캐치프레이즈이기도 하다. 물론 탐색기와 IE의 완전 통합은 보안상의 이유로 인해 IE7부터는 좀 지양되었지만 말이다.

얼마나 바뀌었냐 하면, 파일 대화상자에도 웹브라우저처럼 "뒤로, 앞으로" 버튼이 생겼고, 검색란이 생겼다. 자주 쓰는 폴더 목록은 마치 인터넷 즐겨찾기처럼 뜨기 시작한 것이다. 그런 디자인이야 디자이너의 취향 나름이라 하지만, 기본 크기가 좀더 큼직해지고 마우스로 두세 단계의 폴더를 바로 건너뛰어 상위 단계로 갈 수 있어서 디렉터리 변경이 좀더 편해진 게 매우 좋다.

3. 과거 호환성

이렇게 운영체제가 버전업되면서 파일 대화상자의 디자인은 몇 차례 변화를 겪었다.
응용 프로그램이 아무 조작을 안 가하고 기계적으로 MSDN에 명시된 input과 output만 FM대로 잘 활용한다면, 파일 대화상자의 디테일은 응용 프로그램의 본질적인 동작에 전혀 영향을 끼치지 않는다. (당연한 말이지만)

그렇기 때문에 윈도우 비스타 이전에 개발된 프로그램이라 할지라도, 파일 대화상자 API만 곱게 쓴다면 비스타에서 실행했을 때 최신 디자인의 파일 대화상자가 뜬다. 이게 무슨 공용 컨트롤 매니페스트도 아니고, 최신 대화상자를 쓰기 위해서 응용 프로그램이 뭔가 조치를 취해야 할 필요는 없다.

그럼에도 불구하고 운영체제들은 옛날 버전 파일 대화상자들의 디자인도 갖고는 있다.
응용 프로그램이 파일 대화상자에다가 자신만의 컨트롤을 추가하고, 동작 방식을 제어하고 특정 컨트롤을 서브클래싱까지 하는 경우, 그 파일 대화상자의 디자인이 최신 운영체제에서 바뀌어 버린다면 동작 방식에 심각한 문제가 생길 수 있기 때문이다.

이미지 프로그램의 경우 열기 대화상자에다가는 그림 preview를 추가하기도 하고, 저장 대화상자에다가는 JPG로 저장할 경우 화질을 지정하는 추가 옵션을 대화상자에다 덧붙이곤 했다.
압축 유틸리티나 파일 변환 프로그램은 아예 파일 열기 대화상자에다가 자기네 동작 옵션을 한 보따리 가득 집어넣어 그걸로 프로그램 메인 화면을 만들기도 했다는 걸 알 필요가 있다.

최신 디자인을 적용할 수 없는 경우, 운영체제는 여전히 윈도우 2000이나 심지어 98(favorite bar까지 없어진) 스타일로 파일 대화상자를 출력해 준다.
특히 MFC를 써서 프로그램을 개발하는 경우 이 점을 매우 신경써야 한다. MFC는 프로그래머가 원하는 타이밍에 손쉽게 이벤트를 날려 주기 위해서.. 쉽게 말해 개발자의 편의를 위해서 윈도우에다가 온갖 보이지 않는 훅킹과 서브클래싱을 남발하는데, 이런 이유로 인해서 구형 버전의 비주얼 C++로 CFileDialog 클래스를 쓰면, 아무리 내가 customize를 하는 게 없어도 최신 운영체제에서 파일 대화상자가 최신 디자인으로 나오지 않는다!

까놓고 말해 본인이 개발한 <날개셋> 편집기의 열기 대화상자와, <날개셋> 타자연습의 연습글 추가 대화상자를 윈도우 비스타/7에서 살펴보기 바란다. 차이가 느껴질 것이다.
이런 이유로 인해, 최신 운영체제의 GUI 혜택을 입으려면 새로운 기능을 쓰는 게 없어도 개발툴도 최신으로 써야 하는 경우가 있다. 싫으면 MFC 클래스 쓰지 말고, 불편하더라도 GetOpenFileName처럼 윈도우 API를 직접 호출하든가 말이다. ^^

4. 파일 대화상자의 중복 개발

그런데 MS 오피스 제품들은 전통적으로 운영체제의 표준 API를 쓰지 않고, 무려 자기네가 따로 자체 개발한 파일 대화상자를 사용해 왔다. 가령, 운영체제의 표준 파일 대화상자는 윈도우 2000/ME대에 가서야 favorite bar가 추가된 반면, MS 오피스의 대화상자는 꽤 일찍부터.. 최소한 오피스 97 시절부터 그런 걸 갖추고 있었다.

이런 식으로 MS 내부에서 운영체제 GUI가 오피스 GUI를 뒤쫓아가는 양상은 일상적인 모습 같다. 하다못해 윈도우 3.1 시절에도 오피스가 자체 구현했던 도구모음줄(toolbar), 상황선(status bar), 은빛 3D 효과 대화상자, 프로퍼티 시트와 위저드 같은 게 95로 넘어가면서 운영체제 표준 GUI로 나중에야 편입되었다. 그러던 관행이 세월이 흘러 윈도우 7에서는 리본 인터페이스가 워드패드와 그림판에까지 적용되어 있다.

오피스 제품 중에서도 무려 1980년대부터 개발되어 온 워드와 엑셀은, 겉모습은 별 차이 안 나지만 사실 일반 대화상자들마저도 윈도우 운영체제의 표준 대화상자가 아니며 마치 아래아한글처럼 자체 구현 대화상자이다! 걔네들이 처음부터 아주 크로스 플랫폼으로 개발되어서 그런 건 아니고, 애초에 개발 컨셉이, 빈약한 운영체제의 기본 컴포넌트에 의존하지 않고 자기네 컴포넌트로 UI를 자체 구상하겠다는 방향이었던 것 같다. 그 부자 기업이 하고 싶은 걸 뭘 못 하겠는가?

90년대 이후부터 개발된 파워포인트나 액세스는 그런 수준까지는 아니지만 열기/저장 대화상자라든가 About 대화상자만은 여전히 MS 오피스 공용 라이브러리의 것을 사용해 왔다.
개발툴인 비주얼 스튜디오도 닷넷급부터는 MS 오피스의 GUI 엔진을 이어받고 있는 중이다. 순수 MFC만으로 MS 오피스 짝퉁 GUI을 구현했던 비주얼 C++ 5와 6의 계보는 흑역사가 된 지 오래.

그런데, 그러던 정책이 이제는 바뀌었다.
윈도우 비스타와 동일한 timeline에 개발된 제품인 비주얼 스튜디오 2008과 MS 오피스 2007부터는
놀랍게도 운영체제의 표준 파일 대화상자를 사용하며, 앞으로도 계속 그런 구도가 유지될 것이다!
비주얼 스튜디오 2005와 MS 오피스 2003은 그렇지 않다는 소리.

다만, 이건 비스타 이상에서 실행됐을 때에 한해서이다. 윈도우 XP에서 돌아가는 비주얼 스튜디오 2008이나 MS 오피스 2007은 여전히 자체 대화상자를 사용한다. -_-;; 이것도 무지 신기한 점임.

비스타부터는 이제 운영체제의 파일 대화상자도 기능이 매우 향상되었고 customize하는 수단도 충분히 깔끔해졌기 때문에 굳이 운영체제 따로, 오피스 따로 노선을 갈 필요를 느끼지 않게 되어 그렇게 바뀐 것 같다.
비스타에서는 비주얼 스튜디오 2005의 옛날식 파일 대화상자가 오히려 더 촌스럽게 느껴진다. ^^;;

Posted by 사무엘

2010/05/13 07:34 2010/05/13 07:34
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/267

사용자 삽입 이미지
위의 그림은 동일한 32비트 윈도우용 프로그램을 세 개 연달아 실행하여
1. 자신의 인스턴스 핸들
2. 어떤 지역 변수의 주소
3. 어떤 전역 변수의 주소
4. 그리고 동일한 공유 메모리(memory-mapped file)를 가리키는 주소
를 차례로 찍은 것이다.

그림을 보면 알겠지만 동일한 실험을
a. 윈도우 3.1+Win32s
b. 윈도우 9x
c. NT급 윈도우

에서 모두 해 봤다. (요즘 버전의 비주얼 C++로 그 구닥다리 Win32s에서도 동작하는 프로그램을 만들려면, 컴파일/링크 옵션을 상당히 특이하게 바꿔야 한다. ㄲㄲ)

Win32s의 한계를 절실히 느낄 수 있을 것이다.
CPU의 가상 메모리 기능을 적극 활용하여 각 프로세스마다 자신만의 주소 공간이 절대 보장되는 윈도우 NT에서는,
같은 프로그램은 아무리 동시에 여럿 실행하더라도 자기 주소가 0x400000으로 고정 불변임을 알 수 있다. 심지어 윈도우 9x조차도 그건 보장된다.

그러나 Win32s는 프로그램을 실행할 때마다 프로그램의 인스턴스 핸들이 제각각이며, 지역 변수와 전역 변수의 주소조차도 완전히 달라진다. 시스템의 모든 프로그램들이 단일 주소 공간을 공유한다는 게 바로 저런 의미인 것이다.

Win32s는 모든 메모리 주소가 0x80000000 위의 상위로 잡혀 있는 것도 매우 신기하다.
9x나 NT급 윈도우에서는 그런 주소는 사실상 커널에서나 볼 수 있기 때문이다.
16비트 운영체제에다 아주 특수한 임시방편으로 32비트를 구현한 Win32s의 동작 방식을 짐작케 한다.

또 하나 재미있는 차이를 발견할 수 있는 것은 인스턴스 핸들과 포인터와의 관계이다.
9x/NT에서는 인스턴스 내지 모듈 핸들이 곧 포인터이기 때문에, 0x400000 같은 값에 해당하는 메모리 주소를 들여다보면 EXE 파일이 통째로 로드된 흔적을 고스란히 찾을 수 있다. 즉 MZ 같은 EXE 헤더가 바로 나타난다는 뜻이다. 그리고 전역 변수의 주소는 역시 근처의 0x40????대로 잡힌 것을 볼 수 있다.

그러나 Win32s의 인스턴스 핸들은 포인터와 아무 관계가 없는 임의의 16비트 정수일 뿐이다. 이는 원래부터 포인터가 서로 아무 관계가 없던 16비트 윈도우의 인스턴스 핸들과 개념을 일치시키기 위한 조치로 보인다. 제아무리 32비트 프로그램이라 하더라도 16비트 운영체제 내부에서는 16비트 규모로 식별이 가능해야 하기 때문이다.

끝으로, 공유 메모리의 주소도 흥미로운 결과가 나와 있다. 오로지 윈도우 9x만이 세 프로그램이 가리키는 주소가 모두 일치해 있다.
이는 윈도우 9x만의 메모리 사용 방식 때문이다. 0x80000000~0xC0...에 해당하는 영역에다 모든 프로그램들이 공유하는 운영체제 시스템 DLL과 공유 메모리를 올려 놓는다. 즉, 이 영역은 윈도우 9x에서는 아무 프로그램이나 바로 접근할 수 있는 단일 주소 공간이나 마찬가지이기 때문에 어느 프로그램에서나 의미가 동일한 셈이다.

NT는 그렇지 않다. 비록 실질적으로 가리키는 물리 메모리는 동일한 위치인지 모르나 이를 가리키는 응용 프로그램의 주소는 완전히 제각각이다. 이렇게 하는 게 훨씬 더 안전하고 보안 관점에서도 유리하기 때문이다.

Win32s는 공유건 응용 프로그램의 코드건 데이터건 가리지 않고 무조건 0x80... 상위 메모리 주소가 뒤죽박죽으로 쓰이는데, 공유 메모리마저 9x와는 달리 제각각인 주소가 배당되는 건 좀 의외이다. 9x는 공유 메모리만 상위 메모리 주소가 쓰였고 NT는 보다시피 상위 메모리 주소가 전혀 등장하지 않는다. 사용자 계층과 커널 계층이 엄격하게 잘 분리되어 있음을 뜻한다.

※ 덧붙이는 말

1. 유니코드

일단 Win32s와 9x는 운영체제의 내부적으로는 유니코드 기반이 전혀 아니다. 그래도 9x는 GDI 계층 차원에서 유니코드 문자를 폰트로부터 인식하고 찍는 건 지원하며 98부터는 유니코드 IME 프로토콜까지도 지원한다. 그 반면 Win32s는 운영체제의 한계 때문에 그런 게 전혀 없다. 운영체제 차원에서 임의의 유니코드 문자를 출력할 방법이 없다는 뜻이다.
오늘날까지 살아남은 NT는 무려 1993년에 나온 3.1 버전부터 애초에 100% 유니코드 지원을 염두에 두고 16비트 wide string을 기본으로 설계했으니 과연 대인배. 물론 이렇게 하려면 메모리가 더 많이 필요하다.

9x는 TextOutW, ExtTextOutW, GetTextExtentPoint32W 같은 GDI 함수는 NT와 기능이 동일하다(비록 surrogate는 지원 안 하지만). 그리고 MessageBoxW도 지원한다. 에러 메시지 뱉고 죽는 최소한의 동작만은 유니코드 함수로도 가능하게 배려했다는 뜻이다.
이외에 리소스를 찾는 FindResourceExW, 명령 인자 옵션을 얻어오는 GetCommandLineW 같은 함수가 유니코드 버전도 간단히 구현돼 있다. 비록 문자열을 ansi 문자열로 변환해서 A 함수를 그냥 호출해 주는 수준이지만.

Win32s는 그런 거 없다. MessageBoxW도 지원하지 않으며, 오로지 WideCharToMultiByte(와 그 역함수) 처럼 문자열 변환 함수만 지원되고 나머지 W 함수는 전혀 지원되지 않는다.

2. GDI/User 계층의 32비트

NT는 순수한 32비트 운영체제인 반면 9x 계열은 아직 상당수의 코드가 16비트로 존재했다. 이런 이유로 인해 9x는 대표적으로 GDI의 좌표계가 16비트 크기로 제한되어 있었으며, NT는 GDI 함수의 실행이 실패했을 때 GetLastError() 에러값이 온 반면, 9x 계열은 그렇지 않은 경우가 많았다. 그 에러값은 32비트 코드 계층에서 설정되는 값이기 때문이다.

과거 16비트 윈도우(Win32s도 당연히 포함)에서는 어떤 GDI 오브젝트 자체와 그 오브젝트가 따로 동적으로 할당하는 메모리의 양이 모두 GDI 힙 64KB 내부에 매여 있었다면,
9x는 일단 할당 가능한 오브젝트의 개수는 64KB의 제약을 받으나 각 오브젝트가 추가로 할당하는 메모리는 ‘별도의 제약이 없는 32비트 범위’인 식으로 제한 조건이 완화된 경우가 많았다. 가령, 복잡한 path나 region 같은 경우 추가 메모리 사용량이 만만찮으리라 예상 가능하다.

제약이 완화되기는 했으나 그래도 9x는 공포의 ‘리소스 제한’이 여전했던 것이다. 리소스 퍼센티지를 아직 기억하시는 분 계시려나?
NT는 역시 애초부터 그런 개념이 없었다. 모든 게 자유로운 32비트 공간이고 지금은 64비트로 자연스럽게 확장되어 있기까지 하다. ^^;;

Posted by 사무엘

2010/05/07 07:45 2010/05/07 07:45
,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/261

멀티태스킹 운영체제에는 한 주소 공간에 여러 프로그램들이 동시에 실행될 수 있다. 그렇기 때문에 그런 환경에서 동작하는 프로그램이라면 자기가 메모리의 어느 위치에 적재되는지를 신경써야 하며, 임의의 위치에 올라가더라도 잘 실행될 준비가 되어 있어야 한다.

과거 도스 시절에는 EXE말고 COM이라는 실행 파일이 있었는데, 이것은 최소한의 헤더 같은 것도 없고 코드와 데이터가 다 16비트 공간 안에 막혀 있으며(과거 메모리 모델로 치면 제일 작은 tiny와 동일), 메모리 주소도 고정 붙박이식이어서 오늘날의 컴퓨터 환경과는 도저히 어울릴 수 없는 과거 유물이 되어 있다.

윈도우 운영체제의 실행 파일이 사용하는 PE 포맷은 position dependent code 방식이다. 즉, preferred base라는 개념이 존재하여 자기는 32비트 주소 공간에서 어디에 적재되는 게 이상적인 경우라고 이미 가정하고 만들어져 있다는 뜻이다. 어떤 EXE나 DLL이 거기에 바로 적재가 가능하다면 가장 빠르고 좋지만, 만약 이 선호하는 주소에다 적재가 못 된다면 별도의 재배치 작업이 필요하다. 즉, 마치 퀵 정렬처럼 잘 될 때와 못 될 때의 성능 편차가 크며 일종의 모험이 동반된다는 뜻이다.

이는 유닉스의 shared library와는 다른 디자인이다. 그쪽은 이렇다할 preferred base 주소가 없으며, 코드 자체가 어느 메모리 주소에 적재되든 동일한 성능으로.. 하지만 딱히 최적의 성능을 발휘하지는 않는 구도로 동작한다. 일종의 힙 정렬이나 병합 정렬처럼 동작한다는 뜻.

윈도우 PE 포맷에는 실제로 실행되는 기계어 코드인 code 섹션도 있고 data라든가 리소스(rsrc)와 더불어 재배치 정보를 나타내는 섹션(reloc)도 있다. preferred base에서 동작하지 못할 때, 코드의 어느 부분을 쫙 수정해 주면 preferred base가 아닌 다른 지점을 기준으로 이 프로그램이 잘 돌아갈 수 있는지를 따로 기록해 놓은 것이다. 사실 이런 개념의 재배치 정보는 도스 EXE나 16비트 윈도우 EXE에도 있긴 했다.

그런데 32비트 윈도우 EXE만은(물론 64비트도 포함) 원칙상 이런 재배치 정보가 필요하지 않게 됐다. 32비트 환경부터는 모든 EXE들이 나만의 독립된 주소 공간을 가지기 때문에, preferred base가 무엇이든지에 무관하게 EXE는 무조건 내가 원하는 위치에 가장 먼저 적재되는 게 보장되기 때문이다.
그래서 통상 EXE들은 재배치 정보를 넣지 않는다. 필요가 없기 때문에, 넣지 않는 게 파일 크기를 줄이는 데도 도움이 되기 때문이다.

그럼에도 불구하고 재배치 정보가 필요한 경우는 다음과 같다.

1. EXE가 아닌 DLL은 반드시 필요하다. DLL은 다른 EXE의 밑에 붙어서 동작한다는 특성상 자신만의 주소 공간을 갖고 있지 않다. 나의 preferred base에 EXE라든가 다른 DLL이 이미 선점되어 있다면 응당 재배치가 필요하다. DLL은 그런 상황을 언제든지 대비해야 한다.

2. Win32s에서 돌아가는 32비트 프로그램이라면 EXE라도 재배치 정보가 반드시 있어야 한다. Win32s는 과거 윈도우 3.1에서 일부 32비트 프로그램을 구동하기 위해 제공되었던 일종의 운영체제 익스텐더로, 32비트 프로그램을 실행만 해 줄 뿐, 16비트 윈도우 3.1이 지니고 있던 시스템적인 한계는 그대로 답습하고 있다.
멀티스레딩을 지원하지 않으며 32비트까지 포함해 모든 EXE가 독립이 아니라 단일 주소 공간을 공유한다!
프로그램을 실행할 때마다 EXE의 핸들이 제각각인 값이 들어오며, 로딩도 preferred base에 정확하게 절대로 되지 않는다. 여기에 대해서는 추후 실험해 볼 예정.
실제로 테스트를 해 보면 핸들이 0x1xxx 이렇게 아주 작은 값으로 들어온다는 게 매우 흥미롭다. 윈도우 NT에서는 그렇게 낮은 주소는 아예 무조건 에러로 간주하는 반면, Win32s에는 그게 여전히 쓰인다는 소리이다. 포인터가 아니라 진짜로 다른 번호에 가깝다.

3. 비주얼 C++ 2008에서 추가된 '시작 주소 랜덤화' 기능을 사용하려면 재배치 정보가 필요하다.
보안을 위해 성능을 희생하듯, 윈도우 비스타부터는 PE 실행 파일도 일종의 position independent (과거의 dependent가 아닌) code처럼 써먹으려는 것 같다.
이 방식으로 빌드된 EXE는 운영체제가 일부러 EXE의 preferred base와는 다른 임의의 위치에다가 로딩을 해 준다. 즉, 이 EXE의 특정 지점의 코드나 데이터를 딱 맞춰 수정하는 프로그램이 제대로 동작하지 않게 위해서이다. (스타크로 치면 맵핵 같은 프로그램)

Posted by 사무엘

2010/05/04 08:36 2010/05/04 08:36
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/258

« Previous : 1 : ... 21 : 22 : 23 : 24 : 25 : 26 : 27 : 28 : 29 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

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

Site Stats

Total hits:
1676174
Today:
90
Yesterday:
591