Media Log

[Programming]에 해당되는 글 57

  1. h FIELD_OFFSET 매크로 (1) 2011.03.01
  2. h PAGED_CODE 매크로 (5) 2011.02.27
  3. h 디렉터리의 읽기 전용 속성 (4) 2011.02.20
  4. h 알쏭달쏭한 typedef (9) 2011.01.04
  5. h 하위 디렉터리의 파일이 변경 되었는지 감지하는 법 (8) 2010.12.20
  6. h WinApi의 reserved 인자는 뭐하는 용도일까 2010.12.09
  7. h 디바이스 드라이버를 단일 실행파일로 배포하는 방법 (2) 2010.12.04
  8. h 윈도우즈의 세션, 윈도우 스테이션, 데스크탑에 대해 자세히 알아보기 (12) 2010.11.07
  9. h Cancel-Safe Queue를 이용하여 디바이스 드라이버에서 I/O를 취소하기 (2) 2010.10.25
  10. h 재귀호출이 무엇입니까 (2) 2010.10.21
  11. h 유저모드에서 파일시스템 드라이버를 만들기 (1) 2010.10.17
  12. h Win32 에러 번호를 간편하게 확인하기 (1) 2010.08.15
  13. h 로그 뷰어로써의 Vim (2) -원하는 문자열만 골라내기 (3) 2010.06.20
  14. h 로그 뷰어로써의 Vim (1) -멀티 하이라이팅 2010.06.20
  15. h pragma message 지시어를 통한 실수 방지 테크닉 (2) 2010.01.13
  16. h Absolute path와 Canonical path의 차이점 (3) 2010.01.13
  17. h 2009년 최고의 프로그래밍 언어, Google의 Go (3) 2010.01.10
  18. h boost 라이브러리로 커맨드 라인 파싱 쉽게 하기 (1) 2009.09.12
  19. h TR1을 이용한 C++에서의 정규식 사용 (3) 2009.06.30
  20. h SwingX 1.0 has been released. (1) 2009.06.19
  21. h 컴퓨터 프로그래머(CPQ) 자바 1급 시험을 보고 (8) 2008.12.07
  22. h Python 3.0 Released (5) 2008.12.04
  23. h 자바 버전 히스토리 (3) 2008.11.25
  24. h Visual C++ 경고 없는 코드 작성 팁. - CRT 문자열 함수 (8) 2008.05.24
  25. h 재미로 보는 프로그래밍 언어 순위 (8) 2008.05.19
  26. h 전방선언과 컴파일 의존성 (1) 2008.05.16
  27. h Visual Studio 2008과 IronPython 2008.04.12

FIELD_OFFSET 매크로

2011.03.01 08:00 | Programming

typedef struct tagST
{
  CHAR a;
  CHAR b;
  INT* c;
  INT64 d;
  INT cbName;
  WCHAR name[1];
} ST;
위와 같은 구조체가 있다고 하자. 네번째에 있는 멤버 d의 오프셋을 어떻게 구할지 한 번 생각해보라.

INT offset = sizeof(CHAR) + sizeof(CHAR) + sizeof(INT*);
위와 같이 코드를 작성 했다면 틀렸다. 컴파일러의 구조체 멤버 정렬값에 따라서 결과가 다르게 나올 수도 있기 때문이다.

이런 구조체의 특정 멤버에 대한 오프셋을 구해주는 서비스 매크로가 바로 FIELD_OFFSET이다.
FILED_OFFSET 매크로는 다음과 같이 생겼다.
#define FIELD_OFFSET(type, field) ((LONG)(LONG_PTR)&(((type *)0)->field))
0을 type*으로 형변환 해서 field를 참조하는 부분이 포인트인데 문법 자체에는 너무 신경쓰지 않는게 좋을 것이다.

이 매크로는 아래 처럼 사용한다.
// 첫번째 인자에 구조체 이름을 넣고 두번째 인자로 멤버 이름을 넣는다.
INT offset = FIELD_OFFSET(ST, d);
이 코드는 컴파일러의 정렬 크기나, 64비트 환경들에 상관없이 모두 제대로된 결과를 반환한다.

위 구조체를 다시 한번 보자. 마지막 멤버 name[1]이 조금 이상하게 보일 것이다.
이는 C언어에서 구조체 내에 가변길이의 데이터 멤버를 포함시킬 때 메모리를 두 덩어리로 할당하지 않고 한 덩어리로 할당하기 위해 흔히 쓰이는 기법이다. 성능상의 이점이 있기 때문에 커널 레벨 드라이버등 로우레벨로 내려갈수록 많이 쓰이지만 하이레벨 계층으로 올라오면 거의 쓰이지 않는다. 사용하기 불편하기 때문이다.
이런 구조체에는 2가지 법칙이 있는데 첫번째는 언제나 그 가변 길이 멤버가 맨 아래에 위치하고 있다는 것이며 2번째는 그 가변길이 변수의 크기를 나타내는 추가적인 변수가 꼭 존재한다는 것이다. 여기서는 cbName이다.

이제 이 name이라는 변수에 L"some string"이라는 문자열을 복사해보려고 한다. 이 구조체에 메모리를 어떻게 할당하고 값을 채워넣어야 할까.

typedef struct tagST
{
    CHAR a;
    CHAR b;
    INT* c;
    INT64 d;
    INT cbName;
    WCHAR name[1]; // 널 종료 문자열이 아니다
} ST;

int _tmain(int argc, _TCHAR* argv[])
{
    CONST WCHAR* psz = L"some string";
    INT cch = wcslen(psz);
    INT cb = cch * sizeof(WCHAR);
    
    // 첫번째 방법
    {     
        // 구조체 전체의 크기에 가변 문자열의 크기를 더해서 메모리를 할당한다.
        // 구조체에 name[1]이 이미 포함되어 있으므로 WCHAR 1개 만큼을 다시 빼주어야 한다.
        ST* p = (ST*)malloc(sizeof(ST) + cb - sizeof(WCHAR)));
        memcpy(p->name, psz, cb);
        p->cbName = cb;    
    }
    // 두번째 방법  
    {       
        // 처음부터 name의 오프셋까지만 얻어낸 뒤 cb를 더해주면 조금 더 간단하다.
        // 첫번째 방법처럼 중복된 WCHAR 만큼을 다시 빼줄 필요가 없다.
        ST* p = (ST*)malloc(FIELD_OFFSET(ST, name) + cb);
        memcpy(p->name, psz, cb);
        p->cbName = cb;
    }
    // 세번째 방법
    {
        // FIELD_OFFSET에 항상 멤버의 이름만 쓸수 있는 것은 아니다.
        // 아래처럼 배열의 인덱스에 변수를 명시하는 것도 가능하다.
        // 이 때 cb가 아니라 cch를 넣고 있는 것에 유의해야 한다.
        ST* p = (ST*)malloc(FIELD_OFFSET(ST, name[cch]));
        memcpy(p->name, psz, cb);
        p->cbName = cb;
    }

    return 0;
}

FIELD_OFFSET을 모르고 있으면 첫번째 방법처럼 조금 불편하게 코딩해야 한다는 것을 보여주기 위한 예제였다.

FIELD_OFFSET은 또한 CONTAINING_RECORD 매크로를 만들기 위해서도 쓰인다.
CONTAINING_RECORD는 재미있고 또 중요한 매크로이지만 이 곳 블로그에 이미 설명이 되어 있기 때문에 따로 쓰지 않겠다.

FIELD_OFFSET의 ANSI C 버전은 offsetof이며 stddef.h에 정의되어 있다. CONTAINING_RECORD는 container_of와 같다.
리눅스를 다루는 사람들은 offsetof 매크로를 많이 사용하는 것 같지만 나는 윈도 매크로가 더 익숙해서, 코드를 다른 플랫폼으로 이식할 필요가 없다면 offsetof 보다 FIELD_OFFSET을 사용하는 것을 더 선호한다.
양쪽의 구현은 똑같다.

'Programming' 카테고리의 다른 글

Duff's Device  (0) 2011.04.07
_countof 매크로  (0) 2011.03.15
FIELD_OFFSET 매크로  (1) 2011.03.01
PAGED_CODE 매크로  (5) 2011.02.27
디렉터리의 읽기 전용 속성  (4) 2011.02.20
알쏭달쏭한 typedef  (9) 2011.01.04
  1. 오곡 at 2013.08.07 18:14 [edit/del]

    잘배우고 갑니다~!

    Reply

submit

PAGED_CODE 매크로

2011.02.27 19:09 | Programming
PAGED_CODE 는 다음과 같이 생긴 간단한 매크로이다.
#define PAGED_CODE() { \
    if (KeGetCurrentIrql() > APC_LEVEL) { \
        KdPrint(("EX: Pageable code called at IRQL %d\n", KeGetCurrentIrql())); \
        PAGED_ASSERT(FALSE); \
    } \
}
현재 IRQL을 체크해보고 IRQL이 APC_LEVEL 보다 높다면 시스템을 종료시킨다.

디바이스 드라이버는 유저 모드 프로그램들과는 다르게 텍스트 영역이 페이지 파일로 빠져나가지 않도록 설정된다. 즉 코드가 기본적으로 nonpaged 영역에 할당된다.
Making Drivers Pageable

물론 드라이버 프로그래머에게는 이를 제어할 수 있는 방법 또한 주어지며 코드나 데이터를 페이징 가능하도록 설정 할 수도 있다. #pragma alloc_text나 #pragma data_seg 디렉티브를 사용하면 컴파일 타임에 코드나 데이터들에 대한 페이징 여부를 결정할 수 있다.

드라이버 내의 특정 함수들을 페이징 가능하게 만들고 싶다면 다음처럼 쓴다.
#pragma alloc_text(PAGE, functionName)
이 구문은 해당 함수의 선언보다는 아래에 있어야 하고 정의보다는 위에 있어야 한다. 보통 c 모듈에서 헤더들을 include 한 뒤 그 바로 아래에 쓴다.

#pragma alloc_text 는 코드가 올라갈 섹션을 지정하는데, 섹션이란 PE 파일이 가지고 있는 바로 그 섹션을 뜻하며 위와 같이 쓰면 해당 PE파일에 PAGE라는 이름의 섹션이 새로 추가된다.
그리고 I/O 매니저는 드라이버 이미지를 로드할 때 이미지 내의 섹션 이름 중 앞의 4글자가 PAGE 또는 .EDA 가 있는지 찾아보고 있다면 해당 섹션을 paged pool에 넣어버린다.
이제 해당 코드는 시스템에 의해서 필요없을 땐 언제라도 페이지 파일로 빠져나가게 될 것이다.

디바이스 드라이버의 세계에서 중요한 규칙중 하나는 IRQL이 Dispatch 레벨 이상일 때는 페이지 폴트가 일어나서는 안된다는 것이다. IRQL >= DISPATCH_LEVEL 일 때 페이지 폴트가 일어나게 되면(페이지 폴트 뿐만아니라 다른 어떤 소프트웨어 예외라도 일어나게되면) 시스템은 크래시된다.

위 규칙을 알고 나면 #pragma alloc_text를 이용해서 코드를 페이징 가능하게 만들었을 경우에, IRQL이 DISPATCH_LEVEL 이상인 상태에서 해당 함수가 불릴 경우 페이지 폴트가 발생해서 블루스크린이 발생할 수도 있다는 것을 알 수 있다. 따라서 IRQL이 높을 때 불릴 수 있는 함수들은 paged pool에 할당해서는 안된다. 그런데 운이 좋아서 해당 시점에 코드가 램에 잘 존재하고 있어서 페이지 폴트가 일어나지 않는다면 이는 버그를 감추어주게 되고 나중에 더 골치아픈 문제를 가져다준다.

페이징 가능한 함수들에 PAGED_CODE 매크로를 사용하면 이런 운에 의해 감추어지는 버그들을 없애 버리고 곧장 시스템을 크래시 시켜 버림으로서 문제가 있는 부분을 쉽게 찾아낼 수 있다.
그래서 페이징 가능한 모든 코드에는 함수의 시작부에 PAGED_CODE() 매크로를 사용하는 것이다.

위에서 말한 내용들이 너무 복잡하다면 다음 규칙만 외워서 따라해도 좋다.
#pragma alloc_text를 사용했다면 짝이 맞는 함수를 찾아가 시작부분에 PAGED_CODE 매크로를 써준다.
이 둘을 항상 짝으로 같이 다니게 하면 된다.(둘다 있거나 둘다 없거나)

여기까지 읽었으면,
"빌어먹을, 코드이든 데이터이든간에 무조건 nonpaged pool에 할당하는게 편한 것 아닌가?"
하는 생각이 드는게 정상일 것이다. 물론 그렇게 하면 프로그래머는 좀 더 편해지겠지만, 오랫동안 사용되지 않고 있는 코드와 데이터들이 계속 물리 메모리를 점유하고 있기 때문에 시스템의 성능을 떨어뜨릴 수 있다. 특히 넷북같은 꼬물 컴퓨터에서는 더 많은 영향을 끼칠 것이다.

'Programming' 카테고리의 다른 글

_countof 매크로  (0) 2011.03.15
FIELD_OFFSET 매크로  (1) 2011.03.01
PAGED_CODE 매크로  (5) 2011.02.27
디렉터리의 읽기 전용 속성  (4) 2011.02.20
알쏭달쏭한 typedef  (9) 2011.01.04
하위 디렉터리의 파일이 변경 되었는지 감지하는 법  (8) 2010.12.20
  1. 오곡 at 2013.07.09 17:40 [edit/del]

    잘배우고 갑니다~!

    Reply
  2. 없다캐라 at 2014.02.11 17:50 [edit/del]

    저두 잘 배우고 갑니다. prama alloc 관련 검색을 해서 알게되어 왔는데 다른 글보다 이해하기 쉬웠습니다.

    Reply
  3. Favicon of http://minosori.tistory.com BlogIcon minosori at 2014.12.05 14:53 신고 [edit/del]

    안녕하세요,
    좋은 글 잘 읽고 있습니다. 감사합니당~
    그런데, 이해가 안돼서 질문드려요;;
    PAGED_CODE() 매크로를 써서 IRQL이 DISPATCH_LEVEL 이상이면 해당 코드가 페이지되어있든 안되어있든, 그리고 페이징 가능한 코드로 만들어져 있든 말든 무조건 크래시를 발생시키잖아요?(이것도 블루스크린으로 뜨나요?)
    그럼 메모리 절약을 위해서 해당 함수를 페이징 가능한 코드로 만들었다고 했을 때, 크래시가 일어나지 않고 다시 메모리로 로드 되도록 하려면 어떻게 해야하나요?
    현재, Driver Unload때만 섹션을 PAGE로 할당하고 나머지는 디폴트로(모두 Non Paged Area에 들어가게 되죠?) 만들고 돌려보니, NON_PAGED_AREA에서 PAGING됐다고 블루스크린이 뜹니다...ㅜ_ㅡ
    설마, 헤더파일과 소스파일을 여러개로 나눠서 작업하는거랑 관련이 있나요? 디바이스 드라이버는 전혀 생각지도 못한 부분에서 문제가 터지는 경우가 많아서 혹시나 싶어서 여쭤봅니다...;

    Reply
    • Favicon of https://www.benjaminlog.com BlogIcon 김재호 at 2014.12.05 15:37 신고 [edit/del]

      PAGED_CODE() 매크로를 써서 IRQL이 DISPATCH_LEVEL 이상이면 해당 코드가 페이지되어있든 안되어있든, 그리고 페이징 가능한 코드로 만들어져 있든 말든 무조건 크래시를 발생시키잖아요?(이것도 블루스크린으로 뜨나요?)

      -> 네 블루스크린이 발생합니다.

      그럼 메모리 절약을 위해서 해당 함수를 페이징 가능한 코드로 만들었다고 했을 때, 크래시가 일어나지 않고 다시 메모리로 로드 되도록 하려면 어떻게 해야하나요?

      질문을 잘 이해하지 못하겠지만, 원글에 써있는대로 함수를 페이징 가능한 코드로 만들었다면 해당 함수가 IRQL 이 디스패치 레벨보다 낮을 때만 불려야 합니다.
      pagefault in non paged area 라는 에러는 드라이버를 작성하면서 가장 흔히 볼수있는 오류이며, 위 규칙만 잘 지켰다고 해결되는 것은 아닐 껍니다. 동적으로 할당하는 메모리들을 페이지드 풀에 할당했는지도 다 꼼꼼하게 살펴봐야하는데, dump파일을 windbg 로 잘 분석해보시면 어디가 문제인지 찾으실 수 있을 겁니다.

    • Favicon of http://minosori.tistory.com BlogIcon minosori at 2014.12.05 16:54 신고 [edit/del]

      동적 메모리 할당할 때는 분명 NonPagedPool로 할당했는데... 아직 원인을 알 수 없네요. 메모리 할당부분을 주석처리 했더니 크래시가 나지 않는걸 보니 분명 메모리 할당부분에 문제가 있는것 같긴 한데, 아직 초보라 분석하려면 오래 걸릴것 같네요;;
      아무튼 답변 감사합니다. :)

submit
디렉터리의 읽기 전용 속성은 약간 특별하다.

윈도우 탐색기에서 디렉터리에 읽기 전용 속성을 지정하는 것은 그 디렉터리가 가진 하위 파일들(디렉터리는 제외하고)에 대한 속성을 지정하는 것이지 해당 디렉터리의 속성을 바꾸는 것은 아니다.

예를 들어, 윈도우 탐색기에서 속성창을 켜서 디렉터리의 읽기 전용 속성을 체크하고 하위의 모든 파일에 적용을 선택한다면, 해당 디렉터리의 하위와 그 모든 서브 디렉터리가 가진 파일(디렉터리가 아니라)들의 속성을 업데이트 한다. 디렉터리는 전혀 업데이트 되지 않는다.
'현재 폴더에만 적용'을 선택한다면, 해당 디렉터리의 속성을 바꾸는 것이 아니라 해당 디렉터리 바로 하위의 파일들에 대해 속성을 적용한다는 뜻이다.

비록 윈도우 탐색기에는 디렉터리의 읽기 전용 속성을 셋팅 시켜줄 수 있는 인터페이스가 없지만, 윈도우 Api(SetFileAttributes, SetFileInformationByHandle)를 사용해서 디렉터리의 읽기 전용 속성 플래그(FILE_ATTRIBUTE_READONLY)를 셋팅 시켜 줄 수는 있다. 단, 그렇게 하더라도 그것이 해당 디렉터리를 읽기 전용으로 만든다는 뜻은 아니다.
디렉터리의 읽기 전용 속성이 켜져있는 것은 윈도우 탐색기에게, 나는 특별히 커스터마이징 된 폴더이니 내 안의 desktop.ini를 열어서 읽어보고 거기에 적혀진대로 나를 꾸며줘(폴더 아이콘 따위들) 라고 말하는 것이다.

몇몇 잘못 만들어진 애플리케이션들은 디렉터리가 읽기 전용이면 삭제하지 않도록 코딩 되어져 있을 수도 있지만, 그것은 애플리케이션 구현의 오류이다.

디렉터리의 시스템 속성(FILE_ATTRIBUTE_SYSTEM) 또한 읽기 전용 속성과 같은 의미를 지닌다.
즉, 디렉터리에 대해서 읽기 전용이나 시스템 플래그가 둘 중 하나라도 켜져 있다면, 윈도우 탐색기는 그 디렉터리가 가진 desktop.ini를 스캔 한다.

이것은 응용 프로그래머들이나 네트워크 파일 시스템 프로그래머들에게 중요한 정보를 전달한다.
  • 디렉터리의 읽기 전용 속성을 잘못 이해한채 응용 프로그램을 이상한 방식으로 구현하지 말 것.
  • 읽기 전용 속성이 켜있으면 탐색기가 desktop.ini를 열어보려 시도하는데, 네트워크 리디렉터 같은 경우에는 이것이 성능에 상당한 영향을 끼칠 수도 있음을 숙지하고 있을 것.

이 글의 주제와는 상관이 없지만 다음 내용 역시 읽어볼만하며 재미있다.
디렉터리와 폴더의 차이점.

  1. 윈도우개발자 at 2011.02.20 15:37 [edit/del]

    오 그렇군요!
    말씀하신 것처럼 잘못 코딩한 적은 없었지만, 앞으로 잘 알고 있어야겠네요 ^_^;

    Reply
  2. 박태환 at 2011.02.23 13:53 [edit/del]

    재호씨 잘 지내시죠??블러그 잘 보고 갑니다. 가끔 놀러올께요^^

    Reply

submit
typedef BOOL int;
typedef int BOOL;
둘 중 어느 것이 맞는지 단박에 알아차릴 수 있겠는가?
나는 typedef만 쓰려고 하면 지금도 둘 중에 뭐가 맞는지 헷갈리고는 한다.
답은 아래것이 맞다.
그럼 앞에 있는 타입으로부터 뒤에 따라오는 새로운 타입을 만들겠다는 말인가?

아래 정의들을 보자.
typedef int BOOL, *PBOOL;

typedef struct tagFILEINFO
{
  int i;
} FILEINFO, *PFILEINFO;
이제 또 어디부터가 앞이고 뒤인지 햇갈린다.

typedef BOOL (*fn_t)(int, int*);
함수의 경우에는 조금 더 머리가 아프다.
빌어먹을, 대체 어디가 앞이고 어디부터 뒤란 말인가?

typedef을 정의할 때는 이를 헷갈리지 않기 위해서 딱 한 가지만 기억하면 된다.
변수를 적어야 할 위치에 새로운 타입을 적어라.

위에 나왔던 typedef 들을 하나씩 살펴 보겠다.
빨강색은 타입이요, 파랑색은 변수이다.

int형 변수를 선언할 때는 다음처럼 한다.
int
i;
아래처럼 한 줄에 포인터 변수와 같이 선언 할 수도 있다.
int j, *p;

이제 typedef를 다시 보면

typedef int BOOL;
typedef int BOOL, *PBOOL;


구조체를 정의함과 동시에 변수를 만들 수 있다는 것도 알고 있을 것이다.
struct FILEINFO
{
  int i;
} fileInfo; // 구조체를 선언함과 동시에 전역 공간에 fileInfo 라는 인스턴스를 생성하였다.

물론 아래처럼 여러 변수를 만들 수도 있다.
struct FILEINFO
{
  int i;
} fileInfo, *pFileInfo, ***pppFileInfo;

이제 typedef를 다시 보면

typedef struct tagFILEINFO
{
  int i;
} FILEINFO, *PFILEINFO, ***PPPFILEINFO;


아래 함수를 나타내는 타입은 무엇일까?
BOOL foo(int i, int* p);

타입은 다음과 같다.
BOOL (*)(int, int*)

타입이 있으므로 변수도 만들 수 있다.
그런데 함수의 경우에는 변수가 뒤쪽에 붙는 것이 아니라 가운데에 들어가는 것을 이해하는 것이 중요하다.
BOOL(*)(int, int*) 이라는 타입의 변수 pfn을 선언 하려면 다음과 같이 한다.

BOOL (*
pfn)(int, int*);


아래는 함수 포인터를 사용하는 예제이다.

void foo(int x)
{
  printf("%d", x);
}

int main()
{
  void (*pfn)(int) = foo; // void (*)(int) 타입의 변수 pfn을 정의하면서 동시에 foo를 대입한다.
  pfn(10); // 물론 호출도 가능하다.
}

이제 typedef를 다시 보면,

typedef BOOL (*fn_t)(int, int*); // fn_t라는 새로운 타입을 정의하였다.


함수에 호출 규약까지 넣는 경우에는 아래처럼 꼭 괄호 안에 호출규약을 넣어 주어야 한다.
typedef BOOL (__stdcall *fn_t)(int, int*);

멤버 함수의 경우에는 타입을 다음처럼 쓴다.
void (MyClass::*)(int, int*);

위에서 설명한 규칙을 잘 기억했다면 이제 typedef을 쉽게 만들어 낼 수 있다.

typedef void (MyClass::* memberfn_t)(int, int*);

징그럽지만 어쩌겠는가.


  1. Favicon of http://duckii81.wordpress.com BlogIcon 장현덕 at 2011.01.05 00:01 [edit/del]

    요즘 meta programming 공부 하면서 typedef 오질라게 쓰고 있는데, 아직도 헷갈린다.

    Reply
  2. Favicon of http://eslife.tistory.com BlogIcon esstory at 2011.01.05 08:06 [edit/del]

    자주 헷갈리는 부분인데 깔끔한 정리 감사합니다. ~

    Reply
  3. 무실 at 2011.04.07 11:23 [edit/del]

    간만에 다시 c++ 하려니 헷갈렸는데 좋은글 보고 갑니다.

    Reply
  4. 초보 at 2011.12.05 16:19 [edit/del]

    저는 typedef 와 #define 이 반대라서 헷갈리더군요.
    #define BOOL int
    typedef int BOOL ;
    왜 이렇게 만들었는지 ...

    Reply
  5. 겨울악령 at 2013.07.18 03:17 [edit/del]

    구조체 typedef랑 typedef int INT
    typedef int INT, *PINT에 함수 포인터 typedef 까지보고

    도대체 어디까지가 정의인가 몰라서 이리저리 찾아보다가 여기까지 왔는데

    "변수를 적어야 할 위치에 새로운 타입을 적어라."

    이거 보니 딱 이해가 되네요

    좋은 내용 감사합니다.

    Reply
  6. 냐앙 at 2016.11.15 15:34 [edit/del]

    마지막 말씀처럼 처음 볼때는 징그러운데 찬찬히 보니 읽을 수 있겠네요 고맙습니다~

    Reply

submit
프로그램을 짜다보면, 특정 디렉토리 내에서 파일 혹은 디렉터리가 변경되었음을 감지해내야 하는 경우가 가끔 생긴다.
윈도우즈에서는 FindFirstChangeNotification과 그 패밀리 함수들을 통해서 이를 쉽게 확인할 수 있다.
감시하고 싶은 디렉터리의 바로 하위 디렉터리 뿐만 아니라, 모든 하위 디렉터리까지 알림을 받을 수 있도록 API가 설계되어져 있다.
FindFirstChangeNotification은 파일 변경 알림을 위한 커널 오브젝트를 만들어서 돌려주는 함수이며, 다른 여느 커널 오브젝트들을 사용하듯이, 그저 생성한 뒤 시그널 되기를(파일이 변경되기를) 기다리면 된다. -WaitForSingleObject 따위의 함수들을 이용해서 말이다.

그럼 디렉터리의 어떤 파일이 어떻게 변경되었는지도 알 수 있을까?
FindFirstChangeNotification 함수로는 이를 알 수 없지만 ReadDirectoryChangesW 함수를 이용하면 알 수 있다.

ReadDirectoryChangesW 함수는 다른 함수들과는 다르게 이름 뒤에 W가 붙은 유니코드용 함수만 제공된다.
처음에 막상 이 함수를 써보려고 하면 몇 가지 어려움에 부딪치게 되는데, 알고 나면 그렇게 어렵지 않은 함수이다.

2가지의 지식만 알고 있으면 되는데 첫번째는 디렉터리의 핸들을 얻는 방법이고, 두번째는 FILE_NOTIFY_INFORMATION 데이터 구조를 이해하는 것이다.

CreateFile 함수는 파일을 생성하는 것 뿐만아니라, 파일을 열 수도 있으며 디렉터리를 열 수도 있다. 사실 CreateFile에서 File이란 의미는 VirtualFile을 의미하며, 실제 파일이 아닌 장치들도 CreateFile을 통해 열어서 I/O를 하게 된다.
CreateFile을 통해 디렉터리를 열 때는 꼭 FILE_FLAG_BACKUP_SEMANTICS 플래그를 넣어주어야 한다.

FILE_NOTIFY_INFORMATION 구조체는 다음처럼 생겼다. 한 개의 파일 변경에 대한 정보를 담을 수 있는 구조체이며, 내가 넣어준 버퍼에 여러 개의 아래 구조체가 담겨온다.
첫번째 필드인 NextEntryOffset을 통해 다음 구조체의 오프셋을 가르쳐주는데. 다음 엔트리가 없을 때까지(NextEntryOffset이 0) 하나씩 쭉쭉 읽어오면 되는 것이다.
typedef struct _FILE_NOTIFY_INFORMATION {
  DWORD NextEntryOffset;
  DWORD Action;
  DWORD FileNameLength;
  WCHAR FileName[1];
} FILE_NOTIFY_INFORMATION, *PFILE_NOTIFY_INFORMATION;

마지막에 FileName[1] 이라고 적혀있는 것은 가변 크기 데이터를 한 덩어리로 메모리를 할당해서 쓰기 위해 C언어에서 종종 사용되는 기법이다. 이런 경우 항상 가변 길이 변수(여기서는 FileName[1])의 크기를 나타내는 변수가 하나 더 존재한다.(여기서는 FileNameLength이다)

커널 모드의 많은 서비스 함수들과 유저모드로 노출된 몇몇 API 들에서 저런 데이터 구조를 사용하는데, 이상하게 생기고 어려워 보인다고 그냥 넘어가면 꼭 필요할 때 효율적인 데이터 구조를 만들 수 없을 뿐만 아니라, 남이 만들어 놓은 함수들조차 사용할 수 없다.

아래 블로그 포스트에 이에 대한 약간의 설명이 더 있으니 참고하자.
char data[1]의 역할은?

SetFileInformationByHandle 함수는 비스타 부터 제공되는 강력한 파일 조작 API인데 위와 같은 데이터 구조를 알아야 사용할 수 있다. 이 함수를 통해서 Rename을 하는 부분만 살펴보자. FIELD_OFFSET 매크로를 어떻게 사용하는지 주목해서 봐야한다.

이 함수에서 입력으로 사용되는 FILE_RENAME_INFO 구조체는 다음과 같이 생겼다.
typedef struct _FILE_RENAME_INFO {
  BOOL   ReplaceIfExists;
  HANDLE RootDirectory;
  DWORD  FileNameLength;
  WCHAR  FileName[1];
} FILE_RENAME_INFO, *PFILE_RENAME_INFO;

std::wstring newFileName = L"D:\\newfilename";
HANDLE h = CreateFileW(L"D:\\originfilename", GENERIC_READ|GENERIC_WRITE|DELETE,
    FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
 
DWORD cbBuffer = FIELD_OFFSET(FILE_RENAME_INFO, FileName[newFileName.size() + 1]);
 
PFILE_RENAME_INFO pRenameInfo = (PFILE_RENAME_INFO)malloc(cbBuffer);
pRenameInfo->ReplaceIfExists = FALSE;
pRenameInfo->FileNameLength = newFileName.size() * sizeof(WCHAR);
pRenameInfo->RootDirectory = 0;
 
StringCchCopyNW(pRenameInfo->FileName,
    newFileName.size() + 1, newFileName.c_str(), newFileName.size());
 
SetFileInformationByHandle(h, FileRenameInfo, pRenameInfo, cbBuffer);

이제 ReadDirectoryChangesW 함수도 이해할 수 있다. 바로 코드를 살펴보자. 잡스런 처리는 하지 않았다.

HANDLE hDir = CreateFileW(L"D:\\", GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE,
    0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
CONST DWORD cbBuffer = 1024*1024;
BYTE* pBuffer = (PBYTE)malloc(cbBuffer);
BOOL bWatchSubtree = FALSE;
DWORD dwNotifyFilter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME |
    FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SIZE |
    FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION;
DWORD bytesReturned;
WCHAR temp[MAX_PATH] = { 0 };
 
for(;;)
{
    FILE_NOTIFY_INFORMATION* pfni;
    BOOL fOk = ReadDirectoryChangesW(hDir, pBuffer, cbBuffer,
        bWatchSubtree, dwNotifyFilter, &bytesReturned, 0, 0);
    if(!fOk)
    {
        DWORD dwLastError = GetLastError();
        printf("error : %d\n", dwLastError);
        break;
    }
 
    pfni = (FILE_NOTIFY_INFORMATION*)pBuffer;
 
    do {
        printf("NextEntryOffset(%d)\n", pfni->NextEntryOffset);
        switch(pfni->Action)
        {
        case FILE_ACTION_ADDED:
            wprintf(L"FILE_ACTION_ADDED\n");
            break;
        case FILE_ACTION_REMOVED:
            wprintf(L"FILE_ACTION_REMOVED\n");
            break;
        case FILE_ACTION_MODIFIED:
            wprintf(L"FILE_ACTION_MODIFIED\n");
            break;
        case FILE_ACTION_RENAMED_OLD_NAME:
            wprintf(L"FILE_ACTION_RENAMED_OLD_NAME\n");
            break;
        case FILE_ACTION_RENAMED_NEW_NAME:
            wprintf(L"FILE_ACTION_RENAMED_NEW_NAME\n");
            break;
        default:
            break;
        }
        printf("FileNameLength(%d)\n", pfni->FileNameLength);
 
        StringCbCopyNW(temp, sizeof(temp), pfni->FileName, pfni->FileNameLength);
 
        wprintf(L"FileName(%s)\n", temp);
 
        pfni = (FILE_NOTIFY_INFORMATION*)((PBYTE)pfni + pfni->NextEntryOffset);
    } while(pfni->NextEntryOffset > 0);
}

위와 같이 변경된 파일의 이름과 어떤 식으로 변경되었는지(파일이 새로 생성되었는지, 시간이 바뀐건지)등의 정보를 모두 얻어낼 수 있다.

함수를 사용하는 법 이외에도 몇 가지 더 알고 있어야 하는 것들이 있다.

ReadDirectoryChangesW를 호출해서 한번 통지를 받은 후 다시 루프를 도는 동안 파일들이 변경된다면 그 사이 변경된 파일들은 모두 놓치게 되는 것인가?
함수를 통해 통지를 받을 때, 꼭 하나의 파일(혹은 디렉터리)만 튀어나오는 것은 아니라는 점을 명심해야 한다.
파일 시스템 드라이버는 내부에서 버퍼를 따로 할당해서 이 버퍼에 그 동안 변경된 파일들을 계속 모아둔다. 그리고 사용자 쪽에서 통지를 기다리면, 이 내부 버퍼에 쌓인 것들을 전부 사용자 버퍼로 복사 한뒤 I/O를 완료시켜서 사용자 쪽으로 돌려주게 된다. 따라서 혹시 루프가 천천히 돌더라도 그 사이에 변경되는 파일들은 다음 번 호출시에 모두 받을 수 있게된다. 그렇기 때문에 두번째 인자로 제공되는 버퍼에 FILE_NOTIFY_INFORMATION 구조를 여러개 담아 주도록 설계한 것이다.

또한 이 파일 시스템 드라이버의 내부 버퍼는 핸들을 닫을 때까지 유지된다. 즉, 한번 ReadDirectoryChangesW 함수를 호출하고 핸들을 닫지 않은채 그 다음 호출을 안하고 멍하니 있는다면 그 동안 드라이버 내의 내부 버퍼에 변경된 파일 정보들이 계속 쌓이게 될 것이다. 물론 얼마나 쌓이느냐는 파일 시스템 드라이버의 구현에 달려있을 것이고 NTFS가 어떻게 구현했는지는 모른다.

ReadDirectoryChangesW 함수의 모양을 보면 알 수 있지만 이 함수는 비동기 I/O도 지원을 한다.
디렉터리를 1개만 감시하고 싶을 때는 위에서 한 것 처럼 동기적으로 호출해도 되겠지만, 1개의 쓰레드만 사용하면서 여러 개의 디렉터리들을 감시하고 싶다면 비동기 I/O를 사용하는 것을 고려해봐야 할 것이다.
비동기로 함수를 호출하는 방법은 따로 설명하지 않는다.

파일 시스템 드라이버나 네트워크 리디렉터를 만들 때는 위 기능을 직접 구현해주어야 하는데 필수적으로 구현해야 하는 것은 아니다. 물론 구현하지 않으면 파일이 변경되었을 때 애플리케이션들이 보여주는 UI에서, 변경되는 파일들이 자동으로 갱신되지 않을 것이므로(ReadDirectoryChangesW가 실패할 것이다) 구현 하는 쪽이 더 나은 사용자 경험을 제공해 줄 수 있는 파일 시스템 드라이버가 될 것이다.
  1. 재호님 팬 at 2010.12.29 12:11 [edit/del]

    재호님~ 소스코드 폰트색상이 굉장히 예쁘네요
    폰트 rgb 값좀 알려주시면 안되나요?

    Reply
  2. Ji at 2011.02.10 17:48 [edit/del]

    여기저기 쓸데없는 포스트들만 엄청 찾아보다가 드디어 오아시스 같은 글을 만나네요.
    잘 배우고 갑니다. 고맙습니다.

    Reply
  3. 오곡 at 2012.12.05 00:57 [edit/del]

    정말 좋은 내용 잘보고 갑니다 ㅠㅠ

    Reply
  4. at 2013.07.21 21:49 [edit/del]

    저기 비동기식으로 감시한다는게 무슨 뜻인가요?

    Reply
  5. Mr.K at 2014.06.26 11:37 [edit/del]

    감사합니다. 파일과 관련된 처리 하다가 찾았습니다. 유용할거 같네요

    Reply
  6. BlogIcon builder at 2017.05.12 10:51 [edit/del]

    안녕하세요. 궁금한 게 있어 덧글 남깁니다ㅠ
    cbBuffer = 1048576 초기화 이유가 따로 있나용?

    Reply

submit
윈도우즈 API를 사용하다보면 종종 reserved라는 파라메터를 접하게 된다.

LONG WINAPI RegQueryValueEx(
  __in         HKEY hKey,
  __in_opt     LPCTSTR lpValueName,
  __reserved   LPDWORD lpReserved, //This parameter is reserved and must be NULL.
  __out_opt    LPDWORD lpType,
  __out_opt    LPBYTE lpData,
  __inout_opt  LPDWORD lpcbData
);

도대체 마이크로소프트는 이딴 수법을 왜 쓰는 것일까?

첫번째는 나중에 함수의 기능이 추가되거나 쉽게 확장될 수 있도록 하기 위해서이다. 이 추가적인 파라메터로 인해 인터페이스를 변경하지 않고도 쉽게 기능을 넣을 수 있다.
문서상에서 많은 reserved 파라메터들이 반드시 NULL을 넣어야 한다고 쓰여있는데, 이렇게 해두면 추후 함수가 변경될 때 이전 클라이언트 코드들과 구분을 하기가 용이해진다.

두번째는 윈도우즈 내부에서 호출하는 경우이다. 외부에서 노출된 winapi를 사용하는 클라이언트들에게는 NULL을 넣도록 하고, 내부에서는 다른 용도로 특별한 값을 넣어서 사용하는 것이다.

세번째 이유는 첫번째 이유와 반대이다.
처음 해당 함수가 생길 당시에는 reserved 파라메터는 실제 다른 어떤 용도로 쓰이고 있었다. 시간이 한참 지나고 해당 필드의 의미가 퇴색되고 더 이상 필요 없게 되어 버리자, 파라메터를 제거하는 대신 이름을 reserved로 바꾸어 버렸다. 물론 이전 코드들과의 호환성을 지켜주기 위함이다.

또 다른 이유는 구조체에 불필요한 패딩 데이터를 포함시키지 않고 차라리 reserved용도로 사용하려는 것이다.
struct IconDirectoryEntry {
    BYTE  bWidth;
    BYTE  bHeight;
    BYTE  bColorCount;
    BYTE  bReserved;
    WORD  wPlanes;
    WORD  wBitCount;
    DWORD dwBytesInRes;
    DWORD dwImageOffset;
};
위 구조체에서는 4번째 필드인 bReserved가 패딩 데이터를 채우는 대신 그 자리에 차라리 reserved용 데이터를 집어 넣었다는 것이 명백히 드러난다.

submit
SysInternals의 대부분의 유틸리티들은 하나의 실행파일로 배포된다.

프로세스 모니터 같은 도구는 시스템 내에서 일어나는 모든 I/O등을 잡아채서 보여주는데 이 역시 EXE 파일 하나로 배포된다. 그렇다면 마크 러시노비치가 이것들을 유저모드 애플리케이션에서 구현했다는 말인가? 아니, 심지어 이런 것들을 유저모드에서 구현할 수나 있는 것일까?
물론 그렇지는 않다. 프로세스 모니터는 파일 시스템 레이어 상위에 장착되는 필터 드라이버와 응용 프로그램으로 구성되어져 있다. 아마도 필터 드라이버가 열심히 I/O를 엿본 다음에 그 정보를 잘 정리해서 응용 프로그램에게 전달한 뒤 응용 프로그램이 GUI로 출력하도록 구현되어져 있을 것이다.

그렇다면 프로세스 모니터의 드라이버 이미지는 어디에 숨어있는 것일까?
드라이버는 바로 EXE 파일 내의 리소스에 있다.


응용 프로그램 내에 커스텀 리소스를 하나 만들어서 그 곳에 바이너리 파일을(여기서는 프로세스 모니터 드라이버 이미지) 쑤셔 넣어둔다.
응용 프로그램이 처음 실행되면 FindResource, LoadResource, LockResource, SizeofResource 함수들을 사용해서 이 리소스를 읽어 오고 CreateFile과 WriteFile 같은 함수를 통해서 새로운 파일을(바로 그 프로세스 모니터 드라이버) 디스크 상에 만들어낸다.
이런 식으로 말이다.
HRSRC h = FindResourceW(0, MAKEINTRESOURCEW(IDR_BIN), L"BIN");
HANDLE hRes = LoadResource(0, h);
CONST CHAR* p = (CONST CHAR*)LockResource(hRes);
HANDLE hFile = CreateFileW(L"D:\\bin", GENERIC_ALL, 0, 0, CREATE_ALWAYS, 0, 0);
DWORD cb = SizeofResource(0, h);
DWORD dw;
WriteFile(hFile, p, cb, &dw, 0);
UnlockResource(hRes);
너무 간단하지 않은가?

이제 드라이버 파일이 생겼으므로 응용 프로그램 코드에서는 CreateServiceStartService 함수들을 통해서 드라이버를 설치하고 구동시킬 수 있다.
응용 프로그램이 종료 될 때는 물론 드라이버를 잘 멈추고 제거하고 파일을 지워버리는 등의 작업 또한 해주어야 할 것이다.

  1. 관우 at 2011.01.05 23:15 [edit/del]

    너무 궁금했는데 이렇게 간단할 줄은 몰랐네요. ㅎㅎ 잘 배워 갑니다.

    Reply

submit
윈도우즈의 세션과 윈도우 스테이션 그리고 윈도우 데스크탑은 많은 프로그래머들에게 생소하지만 잘 모르고 있으면 수많은 골치거리를 안겨주는 개념들이다.
나는 지금까지 세션에 대해서만 어렴풋이 알고 있고 윈도우 스테이션과 데스크탑에 대해서는 전혀 모르고 지냈었다.
이런 단어들은 들려올 때마다 항상 나를 부끄럽게 만들었는데, 몇일 전 피드를 받아보다가 어느 훌륭한 프로그래머의 잘 설명된 글을 보고는 내 블로그에 한 번 옮겨 적어 봐야겠다는 생각을 했다.

영어 실력도 변변치 않고 처음 번역을 해보는 것이라 잘못된 부분도 더러 있을 것이다. 영어로 읽는 것이 더 편하다면 원문을 찾아가는 것이 좋을 것이고, 이 글에서 잘못된 부분을 발견해서 가르쳐 준다면 감사히 고치도록 하겠다.

이 글에서 프로그래머들이 실제로 개발하는데 있어서 가장 중요한 내용은 비스타부터 변경된 세션0의 분리이다.
만일 비스타 이상에서 윈도우 서비스를 만들 때나 커널 오브젝트와 도스 디바이스들을 다룰 때 이에 대해서 잘 모르고 있다면 수 없이 골탕을 먹게 될 것이다.
글 말미에 있는 더 읽을 거리 중에서 'The impact of Session 0 Isolation...'는 이 부분에 대해 좋은 설명을 제공한다. 제프리리쳐의 Windows via C/C++이나 윈도우 인터널의 오브젝트 매니저 챕터도 반드시 읽어봐야 한다.

자 그럼 지금부터 시작.



이 글은 윈도우즈가 어떻게 동작하는지에 대한 몇가지 의문점들에 대한 답변이다.
만일 윈도우 프로그래밍에 어느 정도 익숙하다면 글을 읽기가 훨씬 수월할 것이다.

만약 아래 질문들에 대해 완벽히 답변을 할 수 없다면, 이 글을 읽어봐야 한다.

  • 컴퓨터를 잠글 때 무슨 일이 일어나는가? 열려있는 프로그램들은 어찌 되는가? 작업표시줄은?
  • UAC의 특별한 것은 무엇인가? UAC는 어떻게 전체 스크린을 잠그면서 어둡게 만드는가? 그리고 그것이 실제로 우리를 보호해줄 수는 있나?
  • 왜 키 로거들은 잠겨있는 컴퓨터의 비밀번호를 캡쳐해내지 못하는가?
  • 스크린 세이버의 특별한 점은 무엇인가? 그것들은 어떻게 동작하는가.
  • 어떻게 같은 컴퓨터에서 동시에 1명 이상 로그온 할 수 있는가?
  • 터미널 서비스와 원격 데스크탑은 어떻게 동작하는가.
  • 왜 대부분의 원격 제어 프로그램들은 그렇게 거지같은가.
  • NT 서비스 속성창에 있는 “Allow services to interact with desktop” 체크박스는 무엇일까.
  • 왜 사람들에게 비스타는 꼬졌고 윈도7은 좋다고 인식되었을까.

이 모든 것들을 이해하기 위해서는 세션, 윈도우 스테이션, 그리고 데스크탑이라는 개념을 이해해야 한다.
조금 어려울 수도 있겠지만 윈도우즈가 어떻게 동작하는지 이해하기 위해서는 배울 가치가 있을 것이다.

컴퓨터의 들어있는 프로그램들은 그것이 실행될 때 프로세스가 된다. 프로세스는 실행되고 있는 프로그램이다. 프로세스는 프로그램 코드이며 쓰레드들의 모임이다.

윈도우즈에서 프로세스는 그 프로세스를 시작시킨 사용자에게 속하며, 또한 세션에 속한다. 세션은 프로세스들, 윈도우들, 윈도우 스테이션들, 데스크탑들 그리고 그밖의 여러 리소스들을 포함한다. 윈도우 스테이션과 데스크탑은 뒤에서 다룰 것이다.

작업관리자에서 프로세스 탭을 클릭하면 컴퓨터의 모든 프로세스들을 볼 수 있다. 여기서 누가 그 프로세스를 실행시킨 유저인지 또한 알 수 있으며, 프로세스가 속한 세션이 무엇인지도 알 수 있다.
세션ID를 보여주는 것은 디폴트로 비활성화 되어 있는데, View 메뉴에서 Select Columes를 클릭하고 Session ID 옵션을 켜면 된다.

각 프로세스들은 정확히 1개 세션에 속하고 각 세션은 세션 ID를 가지고 있다. 일단 프로세스가 한번 시작되고 나면 그 프로세스의 세션을 바꿀 수 없다. 만일 윈도우 XP나 그 이하 버전을 사용하고 있다면 작업관리자에서 적어도 1개의 세션을 확인할 수 있을 것이며, 비스타 이상 버전이라면 최소 2개의 세션을 볼 수 있다.

윈도우즈에는 많은 세션들이 있을 수 있고 그것들은 사실 일정 숫자로 제한되지만, 여기서는 무한히 많은 것으로 간주하고 이야기를 진행할 것이다.

만약 비스타 이상 버전을 사용하고 있다면, 윈도우즈 서비스가 시작하는 곳은 첫번째 세션인 Session 0이다.
두번째 세션인 Session 1은 첫번째로 로그온한 사용자의 프로그램이 시작되는 곳이다.

한 컴퓨터에서 여러 사용자를 로그온 시킨다면 지금 말한 것보다 많은 세션들이 생겨날 것이다.
여러 사용자를 한 컴퓨터에서 로그온 시키는 방법으로는 터미널 서비스, 원격 데스크탑, 또는 사용자 전환이 있다. 이런 추가적인 로그인 동작으로 인해 새로운 세션이 생겨나게 된다.

CreateProcessAsUser 함수를 사용하면 다른 세션에 프로세스를 생성할 수도 있다. 그렇게 하기 위해서는 반드시 그 세션을 포함하는 사용자 토큰을 사용해야만 한다. 사용자 토큰의 세션을 설정하기 위해서는 SetUserToken 함수를 사용하면 된다.

지금까지의 내용을 정리하면,
Session 0
Process 0.1
Process 0.2
Process 0.3
...
Process 0.N
Session 1
Process 1.1
Process 1.2
Process 1.3
...
Process 1.N
...
Session M
Process M.1
Process M.2
Process M.3
...
Process M.N

비스타는 어떻게 세션의 동작을 변경 하였나:
윈도우 비스타 이전에는 첫번째로 로그인한 사용자는 윈도우즈 서비스들과 첫번째 세션(Session 0)을 공유했었다. 이 세션은 또한 상호작용이 허용되었었다.

비스타부터는 사용자 세션과 서비스 세션이 완전히 분리되었다. 게다가 세션 0은 더이상 사용자와 상호작용도 할 수 없게 되었다.

비스타의 이러한 변화는 서비스는 애플리케이션 코드로부터 안전함을 보장하기 위한 보안적인 이유로 인해 결정된 사항이다. 그럼 왜 서비스들은 보호되어야 하는가? 서비스들은 상승된 권한에서 수행되고 따라서 사용자 프로그램은 제어할 수 없는 것들에도 접근할 수 있다. 뒤에 나올 “어떻게 윈도우즈의 보안을 우회하는가" 섹션 부분에서 여기에 관한 더 많은 내용을 다룬다.

비스타 이전 OS에서 3명의 유저가 로그온 한 경우



비스타 이후부터 3명의 유저가 로그온 한 경우


차이점은 비스타부터는 첫번째 로그온한 유저가 서비스와 별개의 독립적인 세션을 갖게 된다는 것이다.

윈도우 스테이션:
세션은 윈도우 스테이션들과 한 개의 클립보드를 포함한다. 각 윈도우 스테이션은 그것이 속한 세션에서 유일한 이름을 갖는다. 이것은 한 세션에서 각 윈도우 스테이션들은 고유하다는 것을 의미한다. 하지만 세션들 사이에서 2개의 윈도우 스테이션은 같은 이름을 가질수 있다. 물론 그것은 구별이 가능하다.

윈도우 스테이션을 보안 장벽으로 생각하는 것도 괜찮다. 일단 윈도우 스테이션이 생성되면 자신이 속해있는 세션을 변경할 수는 없다.

각 프로세스는 하나의 윈도우 스테이션에 속한다. 하지만 세션과 프로세스의 관계와는 다르게 프로세스는 시작된 이후에 자신의 윈도우 스테이션을 변경할 수 있다.

다음 API들이 이런 작업을 위해 사용 가능하다.

GetProcessWindowStation
SetProcessWindowStation
CreateWindowStation
OpenWindowStation

모든 세션에는 Winsta0이라고 불리는 특별한 윈도우 스테이션이 있다. WinSta0은 사용자 인터페이스를 보여주고 사용자 입력을 받을 수도 있는 윈도우 스테이션이다. 다른 윈도우 스테이션들은 GUI를 보여줄 수도 없고 사용자 입력을 받을 수도 없다.

프로세스는 SetProcessWindowStation함수를 통해서 자신과 연결된 윈도우 스테이션을 설정할 수 있다.
일단 프로세스가 자신의 윈도우 스테이션을 설정하게 되면 윈도우 스테이션 안에 있는 데스크탑이나 클립보드에 접근할 수 있다. 데스크탑에 대해서는 뒤에서 다룬다.

모든 프로세스는 부모 프로세스를 가지고 있다. 프로세스가 시작될 때 윈도우 스테이션을 설정하는 코드를 추가하지 않는다면 부모 프로세스의 윈도우 스테이션으로 설정된다. 또한 프로세스는 CreateWindowStation함수를 통해 새로운 윈도우 스테이션을 생성할 수도 있다.

지금까지 내용을 정리하면,
Session 0

WinSta0
Some Processes
WinSta1
Some Processes
...
WinStaN
Some Processes
Session 1
WinSta0
Some Processes
WinSta1
Some Processes
...
WinStaN
Some Processes
...
Session M
WinSta0
Some Processes
WinSta1
Some Processes
...
WinStaN
Some Processes

윈도우 데스크탑

각 윈도우 스테이션들은 데스크탑을 여러개 갖을 수 있다. 데스크탑은 커널 공간에 로드되며 논리적인 디스플레이 표면이라 할 수 있다. 모든 GUI 객체들은 이 곳에 할당된다.

각 윈도우 데스크탑은 세션에 속하게 되며 또한 윈도우 스테이션에게도 속한다.

각 세션에서는 오직 한번에 하나의 데스크탑만이 활성화 될 수 있다. 또한 그것은 반드시 WinSta0에 속해야 한다. 이런 액티브 데스크탑은 입력 데스크탑이라 불린다. 액티브 데스크탑의 핸들을 가져오기 위해서는 OpenInputDesktop 함수를 사용하면 된다.

WinSta0은 3개의 로드된 데스트탑을 가지고 있다:
  1. 로그온 데스크탑 (로그온 화면의)
  2. 기본 데스크탑 (사용자 데스크탑)
  3. 스크린 세이버

비스타 이후부터는 보안 데스크탑 이라고 불리는 4번째 데스크탑이 생겼는데 이것은 UAC 프롬프트에서 사용된다.
데스크탑을 잠글 때는 기본 데스크탑에서 로그온 데스크탑으로 스위치 되는 작업이 수행된다.

다음 함수들은 데스크탑을 다루는데 사용된다.
  • 쓰레드에 대한 데스크탑을 설정하기 위해 SetThreadDesktop 함수를 사용할 수 있다.
  • CreateDesktopEx 함수를 통해 새로운 데스크탑을 생성할 수 있다. 생성되는 데스크탑은 그것을 호출하는 프로세스와 연관되어 있는 윈도우 스테이션에 할당된다.

STARTUP info 구조체와 lpDektop 멤버를 사용하면 프로세스를 시작할 때 프로세스가 속할 윈도우 스테이션과 데스크탑을 직접 명시할 수도 있다. 보통 이것은 CreateProcessAsUser나 CreateProcess 같은 함수로부터 불린다.

지금까지 내용을 다시 정리하면,
Session 0
Station Winsta0
Desktop Winlogon
Some Processes
Desktop Default
Some Processes
Desktop Screensaver
Some Processes
Desktop UAC
Some Processes
Some other Desktops
Some Processes
Station Winsta1
Some other Desktops
Some Processes
...
Station WinstaN
Some other Desktops
Some Processes
Session 1
Station Winsta0
Desktop Winlogon
Some Processes
Desktop Default
Some Processes
Desktop Screensaver
Some Processes
Desktop UAC
Some Processes
Some other Desktops
Some Processes
Station Winsta1
Some other Desktops
Some Processes
...
Station WinstaN
Some other Desktops
Some Processes
...
Session M
Station Winsta0
Desktop Winlogon
Some Processes
Desktop Default
Some Processes
Desktop Screensaver
Some Processes
Desktop UAC
Some Processes
Some other Desktops
Some Processes
Station Winsta1
Some other Desktops
Some Processes
...
Station WinstaN
Some other Desktops
Some Processes

각 서비스의 속성 페이지에는 "Allow services to interact with Desktop"라는 이상한 체크박스가 있다.
이 체크박스는 서비스가 Winsta0에서 수행될지 그 밖의 다른 윈도우 스테이션에서 수행될지를 결정한다. 이 기능은 미래에도  계속 지원되는 것이 보장되지는 않는지만 현재 윈도7까지는 지원되고 있다.
이 기능은 레지스트리를 통해서 어느 서비스에 대해서라도 적용할 수 있다. 따라서 보안적인 문제를 야기할 수 있기 때문에 미래에는 제거될 것이라고 생각한다.

만약 체크박스가 켜져있으면 새로운 세션이과 윈도우 스테이션 Winsta0이 생성된다. 만약 서비스가 GUI를 보여주려고 한다면, 앞단의 액티브 유저 세션이 디스플레이될 다른 데스크탑이 있다는 것을 통지 받게 될 것이다.

만약 체크박스가 꺼져있으면 서비스가 GUI를 그리려 하더라도 아무일도 일어나지 않는다. 그 서비스는 Session 0에서 시작한다. GUI 호출은 성공하지만 결코 보여지지는 않을 것이다.


윈도우 핸들
윈도우즈 운영체제 안의 윈도우들은 데스크탑 객체의 자식들이다.
윈도우는 GUI 요소이며, 윈도우 핸들(HWND)로써 구분된다. 윈도우 핸들이 어디에 존재하는지 이해하는 것은 여러 데스크탑들 사이에서 할 수 있는 것과 못하는 것들을 알 수 있기 때문에 중요하다고 할 수 있다.

세션 건너로의 통신
어떤 방식을 사용하느냐에 따라서 세션간에도 통신을 할 수가 있다.
파이프, 글로벌 이벤트, 소캣들은 세션간에도 통신 할 수 있다.
윈도우 메세지, 로컬 이벤트들은 세션간에 통신할 수 없다.

윈도우 비스타부터는 모든 서비스들이 Session 0에서 시작된다고 말했었다. 이것은 GUI를 보여주는 수많은 서비스들이 더 이상 제대로 동작하지 않는 것을 의미한다.

서비스에서 GUI를 보여주기 위한 적절한 방법은 파이프 등을 사용하여 서비스와 통신하는 다른 GUI 프로그램을 하나 만드는 것이다.

다른 방법으로는 서비스에서 다른 사용자 세션의 Winsta0의 기본 데스크탑 위치에 GUI를 보여주는 애플리케이션을 새로 띄워버리는 것이다.

데스크탑 건너로의 통신
윈도우 메세지는 데스크탑 사이에서는 허용되지 않으며 같은 데스크탑 내에서만 유효하다. 여기서 더 많은 것을 확인해 볼수 있다. "메세지를 통한 데스크탑 사이의 통신은 불가능하다."

이것은 다른 프로세스로부터 윈도우 메세지를 모니터 하거나 알림을 받는 윈도우 훅 들이 데스크탑 레벨에서만 설치될 수 있다는 것을 의미한다.

따라서 키로거는 컴퓨터가 다른 데스크탑에서 잠겼을 때 무엇을 타이핑하는지 접근할 수가 없다.

데스크탑을 열거한 이후에는 각 데스크탑안에서 윈도우들을 열거해낼 수 있다.

EnumDesktopWindows 함수는 이런 데스크탑 윈도우들을 열거해보는 함수이다. 이 함수는 데스크탑의 핸들을 인자로 받아서 그 데스크탑 내에 존재하는 윈도우들을 돌려준다.
이것은 아까 말했던, 윈도우가 데스크탑의 자식이라는 점을 뒷받침해준다.

어떻게 윈도우즈의 보안을 우회하는가.
사실 윈도의 세션, 윈도우 스테이션, 데스크탑 내에서 원한다면 하고 싶은 어떤 것도 할 수 있다. 그 방법은 로컬 시스템 계정으로 동작하는 서비스를 만드는 것이다.

이런 서비스는 메니페스트 파일을 통해서 권한 상승된채 수행될 뿐 아니라 세션내의 어느 프로세스의 토큰과 링크된 토큰을 획득할 수도 있고, 원하는 작업을 하기위한 같은 토큰 내에서 프로그램을 실행시킬 수도 있다. 사실 이것이 작업관리자가 동작하는 방식이다.

//UAC creates 2 tokens.  The second one is the restricted token and 
//the first one is the one returned by LogonUser
//Vista and above links the elevated token to the Logonuser token though :)
TOKEN_LINKED_TOKEN tlt;
DWORD len;
if(GetTokenInformation(hToken
    , (TOKEN_INFORMATION_CLASS)TokenLinkedToken
    , &tlt, sizeof(TOKEN_LINKED_TOKEN)
    , &len))
{
    hLinkedToken = tlt.LinkedToken;
    //From here you can start elevated processes
}

정리하기

이제 이전의 질문들에 대한 대답을 할 차례이다.

- 컴퓨터를 잠글 때 무슨 일이 일어나는가? 열려있는 프로그램들은 어찌 되는가? 작업표시줄은?
컴퓨터를 잠글 때는 기본 데스크탑에서 로그온 데스크탑으로 전환된다. 물론 이것들은 같은 WinSta0내에 있는 데스크탑의 이야기이다. 두 데스크탑은 물론 당연히 같은 세션에 속한다.
이것이 같은 컴퓨터에서 동시에 여러 사용자가 로그인 할 수 있는 원리이다.

- UAC의 특별한 것은 무엇인가? UAC는 어떻게 전체 스크린을 잠그면서 어둡게 만드는가? 그리고 그것이 실제로 우리를 보호해줄 수는 있나?
UAC 프롬프트를 만나게 되면 기본적으로 기본 데스크탑에서 보안 데스크탑으로 변경된다. UAC는 기본 데스크탑의 스크린 샷을 가지고 있으며, 그것을 UAC 윈도우 뒤에 어둡게 보이게 한다. UAC 윈도우는 보안 데스크탑에 속한다. 사용자는 UAC 프롬프트를 보안 데스크탑에서 실행되게 할 수도 있고 현재 데스크탑(덜 안전한) 에서 실행되도록 할 수도 있다.

- 왜 키 로거들은 잠겨있는 컴퓨터의 비밀번호를 캡쳐해내지 못하는가?
내가 키로거를 작성하고 학교에서 그것을 사용했을 때의 일이다. 나는 모든 사람들의 로그인 패스워드를 알아낼 수 있었고, 그들의 파일조차도 볼 수 있었다. 그 이후로 멀티 세션 운영체제가 나타났다.
소프트웨어 키로거는 윈도우 메세지와 함께하는 윈도우 훅에 기반한다. 키로거는 윈도우 메세지에 의해 모든 키보드 입력을 잡아챈다. 키로거가 다른 데스크탑에서 실행되는 한 더 이상 패스워드를 훔칠 수 없다.
나는 세션너머로도 동작하는 키로거를 만드는 것이 가능하다고 생각하지만, 그런 것이 있는지는 모른다.
이런 것을 어떻게 하는지는 "어떻게 윈도우즈의 보안을 우회하는가" 에서 설명하였다.

- 스크린 세이버의 특별한 점은 무엇인가? 그것들은 어떻게 동작하는가.
스크린 세이버에 특별한 점이란 없다. 스크린 세이버는 GUI 요소들을 감추지도 않을 뿐더러 그 위에 무언가를 그리는 것도 아니다. 스크린 세이버는 단순히 데스크탑을 스크린 세이버 데스크탑으로 전환한다. 데스크탑이 바로 논리적인 그래픽 장치라는 것을 기억해야 한다.

- 어떻게 같은 컴퓨터에서 동시에 1명 이상 로그온 할 수 있는가?
간단하다. 각 유저는 그들만의 세션을 가지고 있고 각 세션은 그 밖의 모든 것들을 가지고 있다. 세션을 사용하는 각 유저는 그들의 데스크탑만이 보인다. 물론 그 데스크탑은 해당 세션의 WinSta0에 속할 것이다.

- 터미널 서비스와 원격 데스크탑은 어떻게 동작하는가.
터미널 서비스와 원격 데스크탑은 이미 열려있는 세션에 접근권한을 주거나 새로운 세션을 생성하는 방식으로 동작한다. 각 세션은 연결된 상태일 수도 있고 끊어진 상태일 수도 있다.

- 왜 대부분의 원격 제어 프로그램들은 그렇게 거지같은가.
몇 몇 원격 제어 프로그램은(터미널 서비스와 원격 데스크탑 말고) 세션을 인지하지 못하고 오직 첫번째 세션에서만 동작한다. 이것은 FogCreek Copilot을 포함한 대부분의 VNC 서버들에 해당된다.

만약 멀티 세션 머신을 사용하고 있다면 그런 각각의 세션들을 제어할 수 없을 것이다.

- 다른 세션들 사이에 존재하는 프로세스끼리 통신할 수가 있는가?
적절한 통신 방법을 사용한다면 가능하다.

- 다른 데스크탑 사이에 존재하는 프로그램들은 윈도우 메세지로 통신할 수 있는가?
없다.

- 왜 사람들에게 비스타는 꼬졌고 윈도7은 좋다고 인식되었을까.
윈도우 비스타는 이러한 변화들의 첫번째 구현이었기 때문이다. 비스타는 호환성을 깨버렸다. 많은 프로그램 개발 회사들은 세션 0의 분리와 같은 변화를 구현하기 위해 오랜 시간을 쏟았다. 아직도 대부분의 사람들은 이것을 완벽하게 이해하고 있지는 못하다.
이 때부터 윈도우 비스타는 꼬물이라는 불명예를 안았다. 물론 호환성을 깬 것은 비스타의 잘못이 맞다.

더 읽을 거리





  1. 은새 at 2010.11.16 19:38 [edit/del]

    감사합니다. 정말 많은 도움이 됬습니다.

    Reply
  2. 좋은정보 at 2012.08.02 14:13 [edit/del]

    잘읽었습니다~ 좋은 정보네요.

    근데 글에서 비스타이후부턴 서비스에서 GUI 갖는것을 불가능하다고 했는데..
    세션0 에도 winsta0 이 있으니까 winsta0 에 속하는 서비스를 만들면 GUI 를 가질수 있는거 아닌가요??
    왜 굳이 다른 사용자 세션의 winsta0 을 이용해야하는지...


    흠.. 쓰고보니 원글작성자도 아니시고 os 를 개발하신 것도 아닌데 엉뚱하게 ㅎㅎㅎ
    에잇. 비스타에서 직접 테스트 해봐야 답이 나오겠네요 ^^;;
    혹시 해보셨는지? ㅎㅎㅎ

    Reply
    • 좋은정보 at 2012.08.02 14:30 [edit/del]

      흠흠. 비스타에서 세션0 은 GUI 를 못갖나보네요.. 자답. ㅋ 참고 : http://secuprint.tistory.com/87

  3. 새침이 at 2013.05.07 22:05 [edit/del]

    좋은 글 잘 보고 갑니다.

    Reply
  4. Favicon of https://bezzang.tistory.com BlogIcon 삼평동베짱이™ at 2014.12.16 10:36 신고 [edit/del]

    좋은글 잘봤습니다.

    오타가 있네요
    [모든 스테이션에는 Winsta0이라고 불리는 특별한 윈도우 스테이션이 있다]는
    [모든 세션에는 Winsta0이라고 불리는 특별한 윈도우 스테이션이 있다]로 바껴야 할 것 같네요

    Reply
  5. at 2015.01.01 15:59 [edit/del]

    비밀댓글입니다

    Reply
  6. 노망난할망구 at 2015.03.21 10:51 [edit/del]

    SetUserToken 함수쓸라면 어떤 h 파일이필요한가요??

    Reply
  7. 안녕 at 2015.03.25 19:16 [edit/del]

    혹시 서로다른세션에서 프로그램실행시켜주는함수는없나요?

    ex로들면
    0번세션에서 1번세션영역에다 프로세스를 실행시키는 함수?

    Reply

submit
유저모드에서는 CancelIo 함수를 통해서 해당 장치에 들어간 모든 I/O를 취소할 수 있고 CancelIoEx 함수를 통해서 특정 비동기 I/O만을 취소할 수도 있다.
비스타 이후부터는 CancelSynchronousIo 함수를 통해 CreateFile 같은 동기 함수도 취소할 수가 있다. 비동기가 지원이 되지 않는 함수들은 CreateFile 처럼 금방 수행되는 함수들인데, 이런 함수들을 과연 취소할 필요가 있는가 생각이 들수도 있지만, Network-redirector(SMB 같은)를 이용하여 원격지에 있는 파일에 접근할 경우 네트워크가 지연될 때 응용 프로그램에 꽤 오랜 블록킹이 발생할 수가 있다. 이런 함수를 잘 알고 이용하면 조금 더 응답성이 좋은 애플리케이션을 만들 수 있다.

사실 유저모드에서야 CancelIo를 부를 필요도 없이 애플리케이션을 꺼버리거나 I/O를 하는 장치의 핸들을 닫아버리면 알아서 취소가 되기 때문에 I/O의 취소에 대해서 그다지 고민할 일이 없지만 -대부분의 응용프로그래머들은 CancelIo같은 함수에 크게 관심을 갖지 않는다- 디바이스 드라이버에서는 조금 다르다.

디바이스 드라이버가 I/O의 취소를 제대로 구현해주지 않으면, 애플리케이션이 종료될 때에 Irp가 취소되지 못하고 드라이버에게 계속 잡혀있어서 애플리케이션이 제대로 종료되지 않은 채 계속 좀비로 남아있다거나, 운영체제가 셧다운되지도 않는 몹시 나쁜 상황을 맞이할 수 있기 때문에 I/O 취소의 올바른 구현은 필수적이다. -이런 경우를 조금이라도 방지하기 위해서 윈도우즈는 5분이 지나면 해당 Irp의 데이터구조는 삭제하지 않은채 취소를 시켜준다. 이것은 엄밀히 말하면 I/O의 취소라고는 할 수 없다.

그럼 디바이스 드라이버에서는 I/O를 어떻게 취소하는가.
디바이스 드라이버에게 I/O의 취소라는 것은 단순히 STATUS_CANCELLED 상태로 Irp를 완료시키는 것이다.
Irp에는 취소 루틴의 포인터가 담겨있는데, 우리가 이곳에 취소 로직을 적절히 구현하여 넣어주면 애플리케이션이 I/O의 취소를 요청할 때 I/O 매니저가 이 취소루틴을 호출 해주고 Irp는 취소로 완료될 수 있다.
하지만 드라이버는 취소루틴에서 STATUS_SUCCESS로 완료시켜버릴 수도 있고, STATUS_CANCELLED로 리턴하더라도 그 바로 직전 I/O가 정말로 완료되었을 수도 있기 때문에 유저모드에서 CancelIo 등을 사용해서 I/O가 완전히 끝났는지 잘 취소되었는지를 검사하는 것은 사실 별로 의미가 없다. 그냥 취소 했다는 것에만 의미를 두면 된다.

취소 루틴에 대해 간단하게 이야기 했지만,  취소루틴을 구현한다는 것은 생전 만나보지 못한 어려운 경쟁 상태를 해결해야 하기 때문에 많은 어려움이 따른다.
이런 어려움을 해소해주기 위해 마이크로소프트에서는 언제부턴가 Cancel-Safe Queue 라이브러리를 제공해주고 있다.

나는 다행히도 꽤 편한 세상에서 태어났고 디바이스 드라이버의 세상에 입문한지 얼마 되지 않았기 때문에, Cancel-Safe Queue를 사용하지 않고 직접 취소를 구현하는 드라이버는 구현해보지 않았다. -진심으로 다행이라 생각한다.

Cancel-Safe Queue를 이용하면 이런 골치 아픈 동기화 처리를 직접하지 않아도 된다.
우리는 라이브러리 루틴 내에서 제공하는 몇 가지 콜백함수들만 적절히 구현해주면 되는데, Csq 라이브러리가 자신들의 취소루틴을 붙였다 떼었다 하면서 우리의 콜백 루틴들을 동기화까지 포함해서 적절히 호출해주기 때문에 우리는 취소루틴을 제공할 필요도 없다. 대단하지 않은가. 이런 방식의 라이브러리를 제공한다는 것은 보통 일이 아니다.

앞으로 점점 많이 사용될 WDF에서는 우리가 취소에 관해 알아야 할 것들이 더욱 줄어들기 때문에 이런 처리가 더욱 쉬워지는데, 아직 WDF를 공부해보지 않아서 어떻게 동작하는 것인지는 모르겠다.

Cancel-Safe Queue의 예제 코드는 WDK 샘플 코드의 /src/general/cancel 위치에 있다.

Csq를 사용하기 위해 우리가 구현해줘야 할 콜백함수들은 다음과 같다.

  • XxxCsqInsertIrp
  • XxxCsqRemoveIrp
  • XxxCsqPeekNextIrp
  • XxxCsqAcquireLock
  • XxxCsqReleaseLock
  • XxxCsqCompleteCanceledIrp

이름에서 볼 수 있듯이 자료구조에 Irp를 넣고 찾고 빼고 잠그는 루틴들을 우리가 구현해주면 되는 것이다.
즉, 자료 구조와 동기화 방식을 우리가 결정할 수 있다. 자료구조는 거의 링크드 리스트를 이용하며, 어떤 커널모드 동기화 방식을 써서 구현해도 상관없지만 성능을 위해 보통 스핀락을 사용한다.
Csq를 쓰면 전역 캔슬 락을 사용해서 구현한 기존의 드라이버들 보다 성능에도 이점이 있다.

각 콜백 함수 구현에 대한 코드는 샘플 코드에서도 찾아볼 수 있지만, 이 문서에는 설명까지 덧붙여 잘 나와있으므로 한 번쯤 읽어보는 것이 좋겠다. 보통의 경우에는 샘플 코드를 복사해서 쓰는 것으로 충분할 것이므로 여기에 따로 코드를 적지는 않는다.

이 루틴들을 다 구현했으면 IoCsqInitialize 함수로 적절한 곳에서 초기화를 하며 콜백 함수들을 등록시켜준 뒤에, Irp가 들어올 때 IoCsqInsertIrp함수를 통해 큐에 집어넣고 나서 I/O 작업을 한다. 작업이 끝나면 IoCsqRemoveIrp함수를 통해 큐에서 빼고 잘 제거된지 확인 한 후에 여느 때처럼 IoCompleteRequest 함수로 Irp를 완료시켜주면 된다. 도중에 애플리케이션들로부터 취소가 요청되면 Csq가 적절히 우리가 작성한 취소 로직들을 이용하여 취소를 수행 해줄 것이다.

위의 설명에서 몇 가지 추가 설명을 해야 할 것들이 있는데, 콜백함수는 우리가 직접 부르는 것이 아니다. 우리 코드에서는 IoCsqInsertIrp처럼 IoCsqXxx 루틴들을 사용 한다. 이 루틴들이 내부에서 우리의 콜백함수를 적절한 곳에서 호출해줄 것이다.

초기화 할 때 IoCsqInitialize가 아니라 IoCsqInitializeEx함수를 이용하면 IoCsqInsertIrpEx 함수를 통해 추가적인 컨텍스트를 담을 수 있다. 이런 추가적인 컨텍스트는 우리가 Queue를 사용하는데 있어서 더 많은 유연함을 가능하게 한다. 

Csq를 사용할 때는 Csq에 관련된 데이터구조가 Irp->Tail.Overlay.DriverContext[3] 에 보관된다.
그러므로 Csq를 사용할 때는 이름이 DriverContext라고 해서 이 곳에 함부로 아무 데이터나 담아서는 안되겠다. 참고로 유저모드 파일시스템 드라이버 프레임워크인 Dokan에서는 Csq를 사용하지 않고 직접 취소루틴을 구현했는데, DriverContext[2]와 [3]에 추가적인 데이터를 담아 사용하고 있다.

위에서 작업이 끝나면 IoCsqRemoveIrp 함수를 통해 큐에서 빼고 잘 제거되었는지 확인하라고 했는데, 이는 그 사이에 취소가 들어왔을 경우 큐에서 이미 빠져버렸을 수 있기 때문이다. 만일 NULL이 리턴되었다면 도중에 취소 요청이 들어와 큐에서 이미 빠진 것이다. 그 Irp는 곧 XxxCsqCompleteCanceledIrp 루틴에 의해 완료되게 될 것이고 이 Irp를 우리가 또 완료시켜서는 안된다.

마지막으로 IRP_MJ_CLEANUP 디스패치 루틴에서는 내 디바이스의 핸들이 닫히는 경우에 큐에 Pending되어 있는 Irp들을 모두 완료 시켜주어야 한다.

  1. 디바이스 드라이버 at 2010.10.25 12:44 [edit/del]

    우연히 찾게 되었는데 무척 좋은 글 잘 읽고 갑니다.^^

    Reply

submit
처음 프로그래밍을 배울 때 재귀함수라는 것은 정말 내 머리를 핑핑 돌게하는 어려운 장벽이었다.
데이터구조를 가르치셨던 교수님께서 어느 날 수업 중, 재귀로 프로그래밍을 짜는 것이 가장 쉽다는 말을 한 적이 있었는데 나는 그게 농담인지 진담인지 구분을 못했던 기억이 난다. -물론 진담이었다.

재귀를 이해하기 위해서 좋은 질문이 여기에 있다.
Enjoy recursion.
  1. Favicon of http://mastojun.net BlogIcon mastojun at 2010.10.22 05:15 [edit/del]

    와우 ㅋㅋ 좋은 질문 링크가 처음에 잘못된줄 알았어요 ^.^ 센스에 감탄하고 갑니다~

    Reply

submit
커널 모드에서 코드를 작성한다는 것은 유저모드에서 보다 어려운 사항이 많이 있다.
언어도 자유롭게 사용할 수 없고, 디버깅도 힘들며, 한 줄이라도 실수하면 여지없이 블루스크린이 발생한다.
그럼 커널 모드 디바이스 드라이버와 같은 것들을 유저 모드에서 구현할 수 는 없을까.

리눅스에는 FUSE라는 것이 있다.
File system in User Space 라는 뜻인데, 유저모드에서 파일 시스템을 구현하도록 제공되는 인터페이스이다.


윈도우에도 물론 비슷한 것들이 있다.
상용 제품인 Callback File System은 콜백 인터페이스를 제공하고 유저모드에서 이 콜백 인터페이스를 구현하기만 하면 CBFS가 알아서 이런 콜백들을 불러준다.


위 그림에서 보면 우리는 Your Application 부분만을 구현하면 되는 것이다.
우측에 있는 Callback File System에서 ReadFile WriteFile등 우리가 미리 등록해둔 콜백 오퍼레이션들을 호출해 줄텐데, 그런 함수들이 호출되면 파일들을 읽고 쓰도록 구현하면 된다.

내 또래의 일본인이 혼자서 열심히 만들고 있는 것 같아 보이는 Dokan 이라는 오픈소스도 있다.


파일 시스템 애플리케이션(우측 초록색)이 처음 구동되면 워커 쓰레드를 여러개 만들어 DeviceIoControl 함수를 호출해 Dokan File System Driver(아래 파랑색)에게 집어 넣어놓는다. DevceIoControl 함수는 비동기 호출도 가능하고 IOCP도 지원이 되지만 Dokan에서는 간단하게 구현하기 위해서 쓰레드를 여러개 만들어 동기적으로 호출한다.
애플리케이션들로(좌측 초록색) 부터 I/O가 들어오면 Dokan Driver(아래 파랑색)가 이 Irp들을 받아서 잘 정리한 뒤 파일 시스템 애플리케이션(우측 초록색)이 미리 넣어두었던 DeviceIoControl의 버퍼에 데이터를 복사하고 완료시킴으로 유저모드로 작업을 위임한다. 파일 시스템 애플리케이션에서는 해당 이벤트가 뭔지 확인해본 뒤에 실제로 처리를 한 후 파일 시스템 드라이버에게 다시 그 결과를 전달해준다. 그러면 파일 시스템 드라이버는 받은 결과 그대로 애플리케이션들의(좌측 초록색) Irp를 완료시킨다.

이렇게 드라이버가 유저모드의 구현을 위한 인터페이스만을 제공함으로서 파일 시스템 로직 구현을 유저모드로 옮길 수 있으며, 유저모드 개발시의 여러 장점들을 가져올 수 있다.
그림에서 보이는 것처럼 두번씩 왔다 갔다 해야하는 것이 성능의 저하를 가져올 수 있고, 유저모드로 구현이 넘어감에 따라 커널 모드 라이브러리 루틴들을 마음껏 쓸 수 없다는 것은 단점이라 할 수 있겠다.
  1. 오곡 at 2012.11.26 11:04 [edit/del]

    좋은 내용 잘봤습니다 ^^

    Reply

submit
Win32 에러번호를 간편하게 확인할 수 있는 방법이 있는데도 불구하고 많은 사람들이 비주얼 스튜디오의 Error Lookup 툴을 사용하거나 심지어 툴을 따로 만들어 쓰는 것 까지 보고는 이 방법을 모르는 사람들이 상당히 많다는 것을 알게 되었다.


이보다 더 편할 수가 있겠는가.

응용 프로그램 레벨에서는 Win32 에러를 받게 되지만 커널부에서는 NTSTATUS 에러값을 확인하곤 한다.
다음 링크에서는 커널 코드에서 돌려지는 NTSTATUS 에러가 어떤 Win32 에러 코드로 매핑되어지는지 나와있다.

  1. 지나가던 at 2011.07.28 15:03 [edit/del]

    헐.. 저런 기능이 있는지 몰랐네요! 감사합니다.
    앞으로 오류코드 보는게 더 편해지겠습니다.

    Reply

submit
/ 를 통해 쉽고 빠르게 원하는 문자열을 하이라이트 서치 할 수 있는 것만 해도 vim은 로그뷰어로서 꽤 쓸만하다.

많은 내용의 로그 파일을 읽을 때는 하이라이트 서치 외에도 보고 싶은 로그만 남도록 불필요한 부분들을 잘 쳐내는 것이 도움이 된다.

1. 특정 패턴이 존재하는 라인을 삭제
2. 특정 패턴이 존재하지 않는 라인을 삭제

불필요한 라인들을 쳐내기 위해서 위의 두가지 기능을 잘 이용할 수 있어야 한다.
물론 빔에서는 이것들을 아주 쉽게 해낼 수 있다.

그 내용을 설명하기 전에 먼저 패턴을 치환하는 방법을 살펴보자.

:%s/pattern/replace/g

위 명령어는 원하는 pattern을 replace로 전역 치환한다. 간단한 패턴이라면 머리 속에 잘 정리해서 한번에 위처럼 명령을 수행할 수 있겠지만, 조금 복잡하다면 먼저 패턴이 잘 매치되는지 부터 확인해보아야 할 것이다.
원하는대로 잘 매치가 되고 나면 이제 위에서 보이는 pattern 부분은 생략이 가능한데, 다음처럼 써서 이미 이전에 매치된 패턴을 치환시킬 수 있다.

:%s//replace/g
pattern 부분에 아무 것도 적지 않은 것을 주목해서 봐야한다. 빔은 저렇게 빈 패턴이 들어왔을 때 이전에 / 을 통해 마지막으로 매치시켰던 패턴을 기억하고 그 패턴을 대입해준다.

이제 처음 설명하기로 했던 2가지 기능을 알아보자.

특정 패턴이 존재하는 라인을 삭제
:g/pattern/d
위와 같은 간단한 입력을 통해서 특정 패턴이 존재하는 라인들을 삭제할 수 있다.
물론 위에서 설명한 것처럼 먼저 원하는 패턴을 한번 매치시켜놓고,
:g//d
라고 쓰는 것이 더 편리하다.
:g/pattern/p
위 명령은 특정 패턴이 존재하는 라인들만을 출력해준다.

특정 패턴이 존재하지 않는 라인을 삭제
위와는 반대로 특정 패턴이 존재하는 라인만을 남겨놓고 싶은 경우도 많이 생긴다.
:v/pattern/d
물론
:v//d
역시 가능하다.

v는 invert를 의미하며, 즉 :v//d는 선택되지 않은 패턴들을 삭제하겠다는 명령이 된다.

이 기능들을 얼마나 잘 사용하느냐는 정규표현식의 능숙도에 달려있다. 원하거나 원하지 않은 라인을 쳐내기 위해 해당 데이터를 잘 분석하고 정규식으로 매칭 시킬 수 있는 능력은 따로 연습해야 한다.
정규 표현식에 대해서는 따로 설명하지 않겠지만, 세상에서 가장 잘 쓰여진 정규식 책을 한 권 소개해 줄 수는 있다.


이 책은 이제는 아쉽게도 절판되었는데, 몇몇 사람들이 블로그를 통해 이 책을 팔아달라고 요청했지만 너무 아끼는 책이라서 나는 도저히 팔 수가 없었다.

빔 위키에 가면 유용하고 재미있는 팁들을 많이 배울 수 있다.
아래 처럼 vimrc에 적어주게 되면 F3 키를 한번 누름으로서 이전에 매치된 문자열이 포함된 라인들만 모아서 새창으로 자동으로 복사해준다.

nmap <F3> :redir @a<CR>:g//<CR>:redir END<CR>:new<CR>:put! a<CR><CR>

지금 설명한 것들과 그 외의 많은 기법들을 neocoin 님에게 배울 수 있었다.
KLDP에서 vim에 대해 질문을 하면 항상 그가 답해주곤 했는데, 많은 것들을 가르쳐주어서 너무나 고맙게 생각한다.
그의 위키에는 vim에 대한 많은 재밌는 이야기들이 있으니 관심이 있다면 한번씩 읽어보길 추천한다.

관련글


  1. Favicon of http://sunyzero.tistory.com BlogIcon 김선영 at 2010.07.10 04:08 [edit/del]

    "v는 Vertical을 의미하는데 나는 왜 이렇게 이름 지었는지를 깨달을 수가 없어서 그냥 '반대' 라고 해석하고는 한다."
    -> UNIX계열에서 v는 verbose나 혹은 invert로 사용됩니다.(대문자는 주로 version으로) 따라서 vim에서는 invert로 사용되었다고 생각되네요. vertical은 생소한데...아마도 아닌듯 합니다.

    참고로 grep도 -v 옵션이 invert의 의미로 사용됩니다.

    Reply
  2. Favicon of http://www.petabytes.org BlogIcon 김재호 at 2010.07.10 17:52 [edit/del]

    엇. 제가 착각했었네요. :vs로 창을 수직으로 나눌 때랑 완전히 햇갈렸나봅니다. 도움말에는 vglobal로 되어있는데 invert가 맞는 것 같네요.

    Reply

submit

vim은 내가 가장 좋아하는 에디터이다.
Windows에서나 Linux에서나 vim을 애용하곤 하는데, 거의 모든 텍스트 문서는 빔으로 열어보고 그 중의 대부분은 로그 파일이다.

오픈 소스들을 받아서 코드를 읽어볼 때도 vim을 사용하긴 하지만 vim에서 코드를 작성하지는 않기 때문에 거의 읽기 전용으로 사용하는 것이나 마찬가지이다. 빔의 쓰기 기능보다는 검색과 이동 그리고 치환 기능을 주로 사용하는데, 이것만으로도 나는 너무나 편리하다.

내가 가장 좋아하는 빔의 기능 중 하나는 *이다.
원하는 단어위에 커서를 올려놓고 *를 누르면 해당 단어들을 모두 하이라이팅 시켜주고 앞뒤로 쉽게 이동해 나갈 수 있다.

정말 편리하지만 이 기능에 한가지 불만이 있다면, 멀티 하이라이팅이 안된다는 것이다.
그래서 나는 word1\|word2 와 같이 직접 정규식의 alternation을 사용해서 멀티 하이라이팅을 시켜나가곤 했는데, 타이핑 하는 것이 꽤나 귀찮을뿐더러 색상도 다 똑같아서 보기에도 불편했다.

계속 그렇게 사용해오다가 아무래도 멍청한 짓을 하는 것 같아서 빔 위키를 한참 뒤져서 드디어 마음에 꼭 드는 플러그인을 찾았다.


설치 방법이나 사용방법은 해당 링크에서 확인하면 될 것이다.

3가지를 다 사용해봤는데 가장 마음에 드는 것은 Mark 이다.
Highlights는 gvim에서만 사용할 수 있어서 탈락.
MultipleSearch는 사용자 인터페이스가 Mark보다 지저분하다.


Mark는 단어뿐 아니라 패턴을 하이라이팅 시킬 수 있으며, 비주얼 모드로 선택한 단어들 역시 하이라이팅 시킬 수 있어서 편리하다.
물론 패턴들마다 다른 색상으로 하이라이팅되며 같은 패턴 내에서 이동시킬 수도 있고 선택된 모든 패턴간에 이동도 가능하다.

디바이스 드라이버 로그나 서버 로그 등을 읽을 때 몇몇 포인터들을 추적해 나가거나 쓰레드 별로 로그를 확인할 때에 똑같은 색상으로 하이라이팅 해놓고 눈알이 빠질 것만 같아서 너무나 불편했다.

개발자나 시스템 관리자라면 로그를 많이 읽어야 하고 또 잘 분석해야한다.
이런 기능들을 잘 사용하면 (꼬물 에디터를 쓰는)다른 사람들보다 쉽게 그리고 다른 사람들이 발견하지 못하는 문제들도 해결할 수 있을 것이다.

관련글

submit
Jeffrey RichterWindows via C/C++ 예제 코드에는 공통 헤더파일이 있는데, 이 곳을 살펴보면 유용하게 사용할 수 있을만한 팁들이 많이 있다.

그 중 가장 쉽고 편하게 쓸 수 있는 기능 하나를 소개하고자 한다.

코드를 작성하다가, '이 부분은 나중에 고쳐야지' 하고 주석으로 마킹해 놓은 뒤에 나중에 잊어버리고 그대로 릴리즈했던 경험이 있는 사람이라면 이 매크로를 아주 좋아하게 될 것이다.

그의 예제코드 중 CmnHdr.h 라는 파일을 보면 다음과 같은 코드가 있다.

//// Pragma message helper macro ////
/* 
When the compiler sees a line like this:
   #pragma chMSG(Fix this later)

it outputs a line like this:

  c:\CD\CmnHdr.h(82):Fix this later

You can easily jump directly to this line and 
 examine the surrounding code.
*/

#define chSTR2(x) #x
#define chSTR(x)  chSTR2(x)
#define chMSG(desc) message(__FILE__ "(" chSTR(__LINE__) "):" #desc)


주석에 잘 쓰여 있듯이 Pragma 지시어를 이용해서 코드 어떤 부분에,
#pragma chMSG(나중에 고칠 것)
int c = a + b;
이런 식으로 주석 대신 적어두는 것이다.

이제 빌드를 하게 되면, Output 창에 이 메세지가 나타나게 되므로 실수를 줄일수 있다.
또한 에러나 경고 메세지와 같이, 더블 클릭 하게되면 해당라인으로 바로 이동하게 된다. 이것은 pragma message의 기능이 아니라, Jeffry가 매크로에 파일과 라인수를 Output창이 알아볼 수 있는 형태로 잘 정의해두었기 때문이다.

하지만 그럼에도 불구하고, 빌드 되는 동안 다른 경고들이 화면 가득 나와서 아예 경고 메세지를 쳐다보지 않는 사람들에게는 혜택이 없다.

이 지시어는 아주 유용하긴 하지만 나는 조금 더 쓰기 편하도록 다음과 같이 매크로로 고쳐서 사용하고 있다.
 
#define chSTR2(x) #x
#define chSTR(x)  chSTR2(x)

#define chMSG(desc) message(__FILE__ "(" chSTR(__LINE__) "): --------" #desc "--------")
#define chFixLater message(__FILE__ "(" chSTR(__LINE__) "): --------Fix this later--------")

#define FixLater \
    do { \
    __pragma(chFixLater) \
    __pragma (warning(push)) \
    __pragma (warning(disable:4127)) \
    } while(0) \
    __pragma (warning(pop))

#define MSG(desc) \
    do { \
    __pragma(chMSG(desc)) \
    __pragma (warning(push)) \
    __pragma (warning(disable:4127)) \
    } while(0) \
    __pragma (warning(pop))



우선은 코드 중간 중간에 #pragma를 쑤셔넣는 것이 보기가 싫었는데, 이 pragma를 매크로 안으로 넣어버렸다. MSVC에는 __pragma라는 키워드를 사용할 수 있는데, 매크로 안에서 pragma 지시어을 사용하기 위해 고안되었다.
만일 예전에 매크로를 만들다가 매크로 안에 #pragma 지시어까지 넣을 수 없을까 고민했던 적이 있던 사람에게는 아주 좋은 소식일 것이다.

또 하나는 코드 맨 끝에 세미콜론을 붙여야 컴파일 되도록 강제하였다. #pragma 지시어는 C문법이 아니므로 세미콜론을 써줄 필요가 없는데, 코드 중간 중간에 들어갈 매크로인만큼 세미콜론이 없으면 미관상에도 안좋고, 복사해서 붙여넣기 등을 할 때 들여쓰기가 깨져버리는 문제가 있다.

그래서 보통 매크로를 만들 때는 세미콜론을 꼭 붙여야 정상적으로 컴파일 되도록 작성하는 것이 좋은데, 위 매크로에서는 do while 얍삽이를 통해서 세미콜론을 강제하고 있다.
저 얍삽이는 Ace 프레임워크의 ACE_DEBUG 매크로를 살펴보다가 알게 된 것인데, 세상에는 참 얍삽하게 머리 좋은 사람들이 많구나 하는 생각을 했다.
농담이다.

do while 얍삽이를 쓰게되면, while(0) 때문에 경고가 발생하는데, 이 역시 __pragma로 감싸버려서 없앨 수 있다.

마지막으로 앞뒤로 ----를 붙여서 좀 더 눈에 띄기 쉽도록 하였다.

이제 다음과 같이 사용할 수 있다.

#pragma chMSG(블라블라블라)
int main()
{
    FixLater;
    int a;

    MSG(나중에 고칠 것);
    return 0;
}

  1. Favicon of http://eslife.tistory.com BlogIcon eslife at 2010.01.14 23:28 [edit/del]

    재호님 글 잘 봤습니다.
    저도 가끔씩 이용하는 방식인데.. 재호님처럼 매크로와 얍샵이 방법까지 활용할 생각은 못해 봤습니다. 좋은 방법 배우고 갑니다 ^^

    Reply
  2. Favicon of http://www.voiceportal.co.kr BlogIcon 김태정 at 2010.01.21 00:43 [edit/del]

    오호~ 오랜만에 글?? ㅎㅎ

    Reply

submit
예전에 Java API에 있는 File 클래스의 메소드들을 보다가 getAbsolutePathgetCanonicalPath를 보고 둘다 사용을 해봤는데 무슨 차이가 있는 것인지 알지를 못했었다. 결과가 항상 같은 절대경로로 나왔던 것이다.
하지만 Canonical path와 Absolute path는 분명한 차이점이 있었는데, 지금부터 설명해보도록 하겠다.

문자열을 통해서 어떤 객체를 표현 한다고 해보자.
5라는 정수는 "5", "000005", "+5", "5.0", "0.5e+01" 등으로 표현할 수 있다.
이것들은 모두 적절한 표현들이지만 이 중 Canonical 표현은 딱 하나다. 여기서는 "5" 이다.

/home/originfile
이런 파일이 있다고 생각해보자. 현재 디렉토리가 /home 이라고 할 때 이 파일의 상대경로는 무수히 많다.

./originfile
../home/originfile
././././originfile

절대경로는 물론 /home/originfile이 될 것이다. 그런데 절대경로가 /home/originfile 1개만 존재할까? 그렇지만도 않다.
/home/../home/originfile
/home/./././originfile
등도 모두 적법한 절대 경로이다.

그 뿐만 아니다.
/home/originfile을 가르키는 /home/symlink 라는 파일을 하나 생성했다고 하자.
/home/symlink라는 절대경로를 통해 이 파일을 참조할 수 있게된다. /home/symlink도 역시 이 파일 객체에 대한 절대 경로라는 뜻이다.
따라서 절대 경로 역시 상대경로와 마찬가지로 무수히 많은 경우의 수를 가질 수 있다.

이들과는 다르게 Canonical Path는 어떤 파일의 경로를 나타내기 위한 유일한 심볼이다.
절대 경로가 어떻든지 상관없이 이 파일에 대한 Canonical path는 항상 /home/originfile 이며 이것은 유일하다. 또한 Canonical path는 항상 절대경로이기도 하다. 물론 역은 성립하지 않는다.

아래에 이해를 돕는 간단한 코드가 있다.
System.setProperty("user.dir", "/home");
File f = new File("/home/originfile");
System.out.println("Abs path : " + f.getAbsolutePath());
System.out.println("Can path : " + f.getCanonicalPath());
f = new File("/home/symlink");
System.out.println("Abs path : " + f.getAbsolutePath());
System.out.println("Can path : " + f.getCanonicalPath());
f = new File("./././originfile");
System.out.println("Abs path : " + f.getAbsolutePath());
System.out.println("Can path : " + f.getCanonicalPath());

Abs path : /home/originfile
Can path : /home/originfile
Abs path : /home/symlink
Can path : /home/originfile
Abs path : /home/./././originfile
Can path : /home/originfile

이제 Canonical Path와 Absolute Path가 어떤 차이가 있는지 이해할 수 있을 것이다.
이것은 Java에서 프로그래밍 할 때만 나오는 것이 아니라 일반적인 개념이기 때문에 프로그래머라면 잘 알아두는 것이 여러모로 좋다.

비스타 부터는 mklink라는 명령이 추가되었는데 심볼릭 링크와 하드링크를 만들 수 있다. 심볼릭 링크를 만들어서 테스트해보면 윈도우즈에서도 아마도 위와 같은 결과가 나올 것이다.

윈도우즈 프로그래밍을 하다보면 MSDN을 읽게 될 때 fully-qualified path라는 용어를 자주 볼 수 있다.
어떤 함수는 인자로서 꼭 fully-qualified path를 전달해야 한다와 같은 내용을 본 적이 있는가?
fully-qualified path는 그냥 절대 경로를 뜻하는 것이 아니라 canonical path와 같은 개념이다.

MSDN에는 fully-qualified path가 2개의 역슬래시로 시작하거나, 드라이브 레터와 역슬래시로 시작해야 한다고 말하고 있다.
예를 들면 다음과 같다.
\\server\share\directory\file.txt
C:\directory\file.txt

그렇지만 다음은 fully-qualified path가 아니다.
C:\directory\..\directory\file.txt
fully-qualified path 또한 하나의 파일 객체과 일대일 대응이 되어야 하며, 그러므로 정규화 되어야 한다.
  1. 다얀 at 2010.01.02 21:03 [edit/del]

    오랜만에 들어왔더니 멋진 블로그로 업글했네?
    돈좀 버나봐?ㅋ

    Reply
  2. Favicon of http://leminity.tistory.com BlogIcon leminity at 2014.01.27 15:02 [edit/del]

    짧게 잘 정리되어 있네요. 잘 보고 갑니다 ^^;

    Reply
  3. thswave at 2015.12.29 08:06 [edit/del]

    좋은 내용 잘 보고 갑니다. 확실하게 이해 되었네요! ^^

    Reply

submit
Tiobe는 내가 심심할 때 가끔씩 찾아보는 사이트 중 하나이다.
여러 검색엔진들을 통해 프로그래밍 언어에 대한 정보들을 수집해서 순위를 매겨주는 사이트이며, 한달에 한번씩 업데이트 된다.

파이썬을 만든 Guido는 아마 나보다 훨씬 더 자주 이 사이트를 들어와보면서 파이썬의 순위를 확인해 보는 것이 분명하다.
이 정도 수준의 천재 해커가 자신이 만든 언어의 순위를 확인하면서 킬킬대는 것은 조금 웃기기도 하지만, 어떤 면에서 보면 이 사이트의 공신력을 더 높여주는 일이라고도 할 수 있겠다.

3년 정도 이 사이트를 봐왔는데 Go처럼 빠르게 성장한 언어는 그나마 루비 밖에는 없었다. 그래도 Go의 성장속도와는 역시 비교할 바가 못된다.
그렇지만 실제로 Go를 사용하고 있는 프로젝트는 아직 본 적이 없다. 아마도 아직까지는 구글에서나 쓰고 있을 것으로 생각한다. 2009년 11월에 처음 발표 되었으니 그럴만도 하다.

Position
Jan 2010
Position
Jan 2009
Delta in PositionProgramming LanguageRatings
Jan 2010
Delta
Jan 2009
Status
11Java17.482%-1.54%  A
22C16.215%+0.28%  A
35PHP10.071%+1.19%  A
43C++9.709%-0.41%  A
54(Visual) Basic7.354%-1.81%  A
66C#5.767%+0.16%  A
77Python4.453%-0.28%  A
88Perl3.562%-0.74%  A
99JavaScript2.707%-0.65%  A
1011Ruby2.474%-0.67%  A
1110Delphi2.392%-0.91%  A
1237Objective-C1.379%+1.24%  A
13-Go1.247%+1.25%  A--
1414SAS0.809%+0.01%  A
1513PL/SQL0.718%-0.29%  A
1618ABAP0.641%+0.10%  A--
1715Pascal0.624%-0.04%  B
1823Lisp/Scheme0.576%+0.14%  B
1920ActionScript0.566%+0.11%  B
2024MATLAB0.540%+0.11%  B


아래 표에서 가장 돋보이는 것은 단연 PHP이다.
범용 목적의 언어도 아닌 PHP가 10년 전의 최강자였던 C++을 제치고 3위를 차지하고 있는 것을 보고 있으면 이제는 Web의 세상이라는 것을 다시 한번 더 실감하게 만든다.

Programming LanguagePosition
Jan 2010
Position
Jan 2006
Position
Jan 2000
Position
Jan 1985
Java113-
C2211
PHP3418-
C++4329
(Visual) Basic5553
C#6713-
Python7817-
Perl864-
JavaScript91012-
Ruby1020--



YearWinner
2009Go
2008C
2007Python
2006Ruby
2005Java
2004PHP
2003C++

비록 이 사이트는 한달에 한번씩 업데이트가 되긴 하지만 매년 새해에는 올해의 언어를 선정하기도 한다. 나는 2009년에는 당연히 C#이 Winner가 되리라 예상했었는데, 바로 지난달인 작년 12월까지 순위에도 없던 Go가 2009년의 언어로 선정된 것은 정말 의외였다.

Tiobe에서는 이에 대해 나름대로 변명을 하고 있기는 하다.

Is Go a hype? May be. But even if it appears to be just another language, the fact that it is a language designed by Google is sufficient to make it really popular. Nobody will be blamed to use a language that is associated with the Google brand name. Apart from that, there is also something technically promising about Go. It has native support for concurrent programming, thus fulfilling the existing need of a language that allows efficient use of multicore processors.

It is astonishing to see that a programming language can rise so fast. Go was not listed yet last month and now it is already #13. This sudden change might be considered an inevitable consequence of our current culture, in which new information is spread and used around the globe at the speed of light.



어쨌거나 나는 아직 Go를 공부해보지는 않았지만 훌륭한 사람들이 설계한 언어이고, 또 믿을만한 회사에서 지원하는 오픈소스인만큼 곧 우리들 곁에도 가까이 다가오게 될 것이라는 생각을 한다.

내년 이 맘때는 순위가 어떻게 변해 있을지 정말 궁금하다.
  1. Favicon of http://sageclarcer.textcube.com BlogIcon 세이지 at 2010.01.10 17:24 [edit/del]

    프로그래밍 언어는 어떻게 만드는 걸까요...

    Reply
  2. Favicon of http://leeua.com BlogIcon 이우아 at 2010.04.25 01:14 [edit/del]

    이런 통계를 어디선가 본 적이 있었는데요...
    거기서는 JAVA를 제치고 C가 1위를 탈환했다는...;;;
    제가 잘못 본 걸까요? ;;;
    아싸뵹. PHP. ㅎㅎ 오브젝트C도 인상적이네요.
    잘봤습니당. ^^

    Reply

submit
콘솔 프로그램을 작성하다보면 커맨드라인 인터페이스를 제공해야 하는 경우가 종종 있다.
옵션이 몇 개 없다면 대충 파싱해서 처리하면 그만이지만 그 수가 10개가 넘어가고, 순서도 유연하게 입력받을 수 있게 하고 싶다면 boost를 사용해보는 것도 좋은 생각이다.

boost에는 program_options이라는 라이브러리가 포함되어 있는데, 우리가 리눅스에서 콘솔 프로그램에 옵션을 입력하는 것과 동일한 방법으로 사용할 수 있도록 하는 기능을 제공해준다.

다음은 간단한 코드와 사용법이다.
add_option() 함수가 함수객체를 리턴하고 그 함수객체가 또 자신을 리턴하도록 되어있어,
사용자는 괄호만을 붙여가며 옵션들을 편하게 집어넣을 수 있도록 해준 아이디어가  재미있다.

using namespace boost;
using namespace boost::program_options;
using namespace std;

int _tmain(int argc, TCHAR* argv[])
{
    options_description desc("Allowed options");
    desc.add_options()
    ("help,h", "produce a help screen")
    ("version,v", "print the version number")
    ("all,a", "print all lists")
    ("number,n", boost::program_options::value(), "Number example")
    ("import,i", boost::program_options::value(), "Import path")
    ;

    variables_map vm;
    store(parse_command_line(argc, argv, desc), vm);

    if(vm.count("help"))
    {
        std::cout << "Usage: regex [options]\n";
        std::cout << desc;
        return 0;
    }
    if(vm.count("version"))
    {
        std::cout << "Version 1.\n";
        return 0;
    }
    if(vm.count("all"))
    {
        std::cout << "--All option was set." << std::endl;
    }
    if(vm.count("import"))
    {
        std::string importpath = vm["import"].as();
        std::cout << "The import path was set to \"" << importpath << "\"" << std::endl;
    }    
    if (vm.count("number"))
    {
        std::cout << "--Number option was set.(" << vm["number"].as() <<")" << std::endl;
    }

    return 0;
}



>ProgramOption.exe -h
Usage: regex [options]
Allowed options:
  -h [ --help ]         produce a help screen
  -v [ --version ]      print the version number
  -a [ --all ]          print all lists
  -n [ --number ] arg   Number example
  -i [ --import ] arg   Import path
도움말은 따로 만들지 않아도 알아서 자동으로 생성해준다.

> ProgramOption.exe -a
--All option was set.

> ProgramOption.exe -v
Version 1.

> ProgramOption.exe -iC:\
The import path was set to "C:\"

> ProgramOption.exe -n2
--Number option was set.(2)

> ProgramOption.exe -an3 -iC:\
--All option was set.
The import path was set to "C:\"
--Number option was set.(3)

> ProgramOption.exe --import=C:\
The import path was set to "C:\"
실제 사용은 대부분의 콘솔 애플리케이션들이 커맨드 라인으로 인터페이스하는 방식과 똑같이 하면 된다.
옵션의 순서도 물론 상관없으며, ps -ef 처럼 연속으로 2개씩 쓸 수도 있다.

더 자세한 내용은 튜토리얼에서 찾아보면 된다.
  1. Favicon of http://microdev.tistory.com BlogIcon microdev at 2010.10.11 17:38 [edit/del]

    좋은 글 감사합니다.

    Reply

submit

C++을 사용하다 보면 다른 언어의 라이브러리들이 부러운 경우가 많이 있다.

정규표현식은 그 중 하나였는데, 다행히도 VS2008 SP1 이상을 쓰고 있다면 TR1이 내장되어 있어 다른 어떤 써드파티 라이브러리를 연결하지 않고도 #include <regex> 한 줄만 추가해줌으로써 쉽게 사용할 수 있게 되었다.

다음은 원하는 문자열을 매치해서 결과를 받아보는 간단한 예제이다.
const std::string s("Hello World");

std::tr1::smatch m;
std::tr1::regex rx("(\\w+) (\\w+)");
bool fMatched = std::tr1::regex_match( s, m, rx );

if ( fMatched )
{
    std::cout << "size : " << m.size() << std::endl <<
                    "match0 : " << m[0] << std::endl << 
                    "match1 : " << m[1] << std::endl << 
                    "match2 : " << m[2] << std::endl;
}

size : 3
match0 : Hello World
match1 : Hello
match2 : World

match 객체의 첫번째 요소에는 매치된 모든 문자열이 담기게 되고, 그 다음 요소들 부터는 캡쳐한(괄호로 둘러싼) 문자열들이 저장된다. 위 예제에서는 첫번째 단어와 두번째 단어를 캡쳐해봤다. -물론 캡쳐 없는 괄호 (?: )을 사용해서 캡쳐 기능을 제거할 수도 있다.
파이썬이나 C# 등의 다른 정규식 라이브러리에서도 대부분 위와 흡사한 인터페이스를 제공한다.

다음 코드는 문자열을 치환한다.
const std::string s("Hello World");
std::tr1::regex rx("^\\w+");
std::string t = std::tr1::regex_replace( s, rx, std::string("Great") );
std::cout << t << std::endl;

위 코드는 첫 번째 단어를 "Great" 라는 문자열로 치환시킨다.
Great World


치환할 때 백레퍼런스도 역시 사용이 가능하다.
std::tr1::regex rx("(^\\w+)");
std::string t = std::tr1::regex_replace( s, rx, std::string("Great $1") );

첫번째 캡쳐그룹을 치환시에 재사용 하였다.
Great Hello World


간단하게 설명하기 위해서 예제의 실용 가치가 없어져 버렸지만, 잘 응용하면 많은 곳에 적용할 수 있을 것이다.

예전에 이메일 검증 함수를 골뱅이와 .을 찾아가면서 CString의 Find 함수로 떡칠을 하면서 만든적이 있었는데, 시간이 지나고 어느 날 RFC 문서에서 이메일 형식 명세를 보다가 아 내가 엉터리로 만들었구나 하고 깨달았던 기억이 난다. Email이나 URL 형식 같은 것들은 정규식 없이 검증 함수를 만들기엔 생각보다 훨씬 복잡하다.

다른 언어들보다 사용법이 조금 까다롭고 보기에도 좋지는 않지만, 이 정도만으로도 C++ 프로그래머들은 고마움에 눈물이 날만하다.

C++0x에서는 파이썬의 r""이나 C#의 @"" 같은 raw string 기능도 언어에 포함될 예정인데, 그 때가 되면 조금 더 보기 좋게 정규식을 사용할 수 있을 것이다.

  1. Favicon of http://kate.textcube.com BlogIcon Briller_Kate at 2009.07.02 00:55 [edit/del]

    태정님 블로그에서 주로 볼 수 있었던 @_ @ 포스트! ^^;

    Reply
  2. 이종협 at 2011.04.14 15:32 [edit/del]

    제법인데 ㅋㅋㅋ regex 때문에 골머리 아퍼~
    AutoCad 쉐리들은 왜케 프로그램을 잘만들어서 귀찮게 하는지..

    Reply

submit
SwingX 라는 오픈소스 프로젝트가 있는데, 꽤 오랫동안 0.9대 버전으로 개발되어 오다가 이번에 1.0 버전이 릴리즈되었다.

SwingX는 java의 기본 swing을 랩해서 조금 더 기능이 풍부한 UI 구현들을 제공하는 프로젝트이다.
java로 데스크탑 애플리케이션을 만드는 경우는 그렇게 많지 않지만, 만약 그런 일을 접하게 되면
기본 swing으로 뭔가 부족하다 싶을 때 swingx에 혹시 원하는 기능이 있는지 찾아보고 그런 기능이 있다면 직접 구현하는 수고를 덜 수 있다.

JRE 1.6.10부터 제공되는 Nimbus 룩앤필(개인적으로는 스윙에서 처음으로 그나마 봐줄만한) 역시 이 swingx 프로젝트에서 시작해서 JRE에 통합되었다.


이미 기존에 만들어둔 GUI애플리케이션이 있다면 룩앤필을 설정하는 코드 몇 줄만 추가함으로써 별 다른 수고 없이 더 보기좋은 UI로 탈바꿈 시킬 수 있다.

MFC로 GUI 프로그래밍을 하게되면 객체에 변경을 가하기 위해 주로 상속을 하거나 서브클래싱을 하게되는데 swing에서는 인터페이스를 구현하는 방법이 많이 사용된다.

C++ 프로그래밍을 하게 되면 COM같은 특수한 분야를 다루지 않는 이상 인터페이스를 구현하는 일이 그다지 많지 않지만, 자바에서는 곳곳에서 쉽게 찾아볼 수 있다.

인터페이스를 설계 및 구현하고 또 변경하고 싶은 기능들만 오버라이딩하여 잘 동작하게 될 때의 기쁨은 어쩌면 객체 지향 프로그래밍의 가장 큰 즐거움 중에 하나일 것이다.
나는 C++을 하는 동안 그런 느낌을 한 번도 느껴보지 못했는데, 자바로 프로그래밍을 하면서 많은 것을 배우고 재미를 느끼는 중이다.

인터페이스가 뭔지 혹은 가상함수가 뭔지에 대한 내용은 어려운 내용이 아니라서 쉽게 배울 수 있지만
인터페이스를 언제 사용해야 할지, 왜 사용해야 하는지에 대한 깨달음은 실제로 많이 사용해봐야만 얻을 수 있는 것 같다.

그런 면에서 객체 지향 프로그래밍을 배우기에는 확실히 C++보다는 자바가 좋은 선택이다.
  1. Favicon of http://www.voiceportal.co.kr BlogIcon 조금까칠한남자 at 2009.06.20 13:24 [edit/del]

    한국에서 Swing 쓸일이 있을까...
    외국가면 Swing 프로젝트 많은데!!
    넌 한국을 떠나!! ㅎㅎ

    Reply

submit
오늘 컴퓨터 프로그래머(CPQ) 자바 1급 시험을 보고 왔다.
이 시험에 대해서 알고 있는 사람은 아마도 많지 않을 것 같다.

한국 정보과학회에서 주관하고 있다고 하는데, 이 곳에 가면 정보를 얻을 수 있다.

나는 리눅스마스터라는 시험을 통해 우리들에게 어느 정도 알려져 있는 한국정보통신인력개발센터에서 이런 시험이 있다는 것을 알게 되었는데, 이 곳을 통해서도 역시 지원 신청을 할 수 있다.

한국정보통신인력개발센터에는 멤버쉽 제도라는 것이 있는데,
우수회원이나 특별회원들에게는 여러 특전이 있다.

그 중 하나는 1차 시험 응시료를 면제해 주는 것인데, 이 CPQ시험은 2차 시험이 없으므로 우수 회원 이상이라면 무료로 응시 할 수 있다.

나는 특별회원이라서 가끔씩 심심풀이로 무료 시험을 보러 가곤 하는데,
시간도 많이 들지 않고 자격증도 덤으로 생기게 되는 꽤 유익한 일이다.

오늘 시험은 동국대학교에서 봤다. 나는 동국대 하면 이동국밖에는 생각나지 않았었는데,
오늘은 확실히 동국대에 대한 이미지를 굳혔다.
추운 날씨의 일요일임에도 불구 하고 많은 학생들이 나와 공부를 하고 있던 모습이 인상적이었다.
허름한 건물 외관에 비해 내부 시설들은 꽤 잘 되어 있었다.

이 시험의 난이도는 그다지 어렵지 않다.
나는 C++ 1급과 Java 1급 시험을 치루어봤는데, 둘 다 대학교 때 배운 지식 정도면 충분히 합격할 수 있는 시험 수준이다.

시험 문제에 대해서는 마음에 안드는 점이 몇 가지 있다.
지난 번 C++ 시험을 볼 때에는 예문에 있는 코드가 오타 였는지 인쇄가 잘못된건지 모를 컴파일도 안되는 이상한 코드가 있었고,
오늘 자바 실기 시험에서는 위의 이미지를 보고 아래 Swing 코드를 작성하시오. 뭐 이런 문제가 있었는데, 그 이미지는 깨져서 보이지도 않았다. 어쩌라고.
이미지 주소를 보니 10.x.x.x/어쩌구 로 내부아이피로 되어 있었는데,
아마도 IHD 사무실 내에서만 테스트 한 번 해보고 시험지인 동국대로 가져와서는 정작 테스트 한 번 해보지 않고 그냥 문제를 바로 배포했는지도 모르겠다.

이런 것들은 주최측에서 조금만 신경쓰면 충분히 보완될 수 있는 문제인데 -그리고 당연히 그래야한다.
아직 그런 것 조차 신경 안쓰는 걸 보면 국가공인 자격증이 되기에는 10년은 걸릴 듯 하다.

또 한 가지 맘에 안드는 점은 문제의 20% 정도가 다른 주제의 내용이라는 것이다.
아니 자바 시험 보는데 퀵 소트하고 머지소트에 대해서 왜 물어보며 폭포수 모델 같은 건 도대체 왜 나오는 건지 모르겠다.
어디 80년대 국가고시에나 나올 법한 이런 썩어빠진 문제들은 빨리 없어져야만 한다.
좋은 자바 문제 고르기에도 바쁜 판에 자료구조하고 소프트웨어 공학은 왜 껴넣는 건지.

이런 점들이 빨리 개선되어 훌륭한 국가 공인 자격증으로 인정 받게 되기를 진심으로 바란다.

기존 기출 문제들은 이 곳에서 확인 할 수 있다.
http://exam.ihd.or.kr/pds/getFile.asp?id=869&tb=tbl_PDS_040100&code=
http://exam.ihd.or.kr/pds/getFile.asp?id=838&tb=tbl_PDS_040100&code=

멤버쉽 제도에 대해서 관심이 있는 사람들이 있을 것 같은데,
다음 링크에서 확인 할 수 있다.
http://exam.ihd.or.kr/exam_community/commu_01_2.htm
무료 응시 뿐만아니라, 시험 감독이나 시험 채점등의 재밌는 아르바이트 또한 할 수 있다.
상반기, 하반기 2회 뽑는데, 자격요건이 된다면 신청기간에 간단한 자기소개서를
제출하면 된다.

  1. Favicon of http://www.voiceportal.co.kr BlogIcon 김태정 at 2008.12.09 10:40 [edit/del]

    야 이늠시키 이런 좋은것을 혼자했단 말야!!!!!!! -0-;;;
    쓰레기 같은 늠 -0-;;
    나한테 이야기도 안하고 -0-;;

    나한테 밀릴까봐 그런거지?? ㅋㅋ

    내년에 나도 java 1급 시험본다!!
    그리고 우수회원으로 신청!! ㅎ

    Reply
  2. kk02me at 2008.12.09 16:53 [edit/del]

    재호씨, 글 웃겨요 ㅋㅋ

    어디서나 웃음을 주시네~

    what a funny you are~!

    Reply
  3. Favicon of http://www.na ver.com BlogIcon 질문~ at 2009.09.20 18:09 [edit/del]

    cpq는 제외라고 되있는데요....
    http://exam.ihd.or.kr/sub/information/information01_03.asp

    Reply
  4. at 2009.12.10 12:38 [edit/del]

    비밀댓글입니다

    Reply

submit

Python 3.0 Released

2008.12.04 18:41 | Programming



내가 파이썬을 처음 만나본 것은 2006년 12월이었다.
나는 그 때 입사한지 4개월 정도된 신입사원었는데, 파이썬이라는 언어를 생전 들어보지도 못했었다.

그 때 우리 프로젝트에서 사용되던 작은 파이썬 프로그램이 하나 있었는데,
그것은 안철수 연구소의 웹 페이지에 가서 V3 모듈을 내려 받아 업데이트 해주는 프로그램이었다.
나는 그 코드를 고칠 일이 있어 수정 하면서 별 거지 같은 언어라고 얼마나 속으로 투덜 거렸는지 모른다.

그 뒤로 언제부터 파이썬이 좋아졌는지는 잘 기억이 안난다.
아마도 도서관에서 두리번 두리번 책을 고르다가 Learning Python이라는 책을 빌려서
천천히 읽어본 후, 꽤 매력을 느꼈고 그 때 부터 파이썬에 호감을 갖기 시작했던 것 같다.

그 후에 회사에서 사람들과 파이썬 스터디도 하고, 프로젝트에도 파이썬을 조금씩 적용하기 시작했다.

나는 언어를 선택할 때, 윈도우즈 플랫폼에서만 동작해도 되고 클라이언트에 배포되지 않는 그런 서버 사이드 코드를 작성해야 한다면 주저하지 않고 C#을 선택한다.
파이썬을 선택하는 경우는 일단 리눅스 플랫폼일 때이다.
나는 리눅스에서 코드를 작성 할 때 vi, gcc, make 세트를 사용하는 것을 아주 어려워하고 또 싫어하는데,
아마도 그래서 그 대안 언어로써 파이썬을 공부했는지도 모르겠다.

그래서 리눅스에서 그다지 많지 않은 코드를 작성할 때는 파이썬 부터 머리에 떠올린다.
터미날을 통해 vim에서 편하게 코드를 작성할 수 있기 때문이다. -C보다 훨씬!
또한 파이썬은 Perl의 문법보다 깔끔하고 Java처럼 잡 것들을 설치하지 않아도 되고, Ruby보다 레퍼런스가 많아서 좋다.

그렇다고 해도 회사에서 파이썬으로 코딩하게 되는 일은 거의 10%도 되지 않는다.
하지만 나는 파이썬으로 코드를 작성하는 그 시간들이 정말 즐겁다.

오늘은 그 파이썬 3.0이 릴리즈 되었다.
나는 지금까지 작성해놓은 코드들이 그렇게 많지 않기 때문에 파이썬 3.0으로 전부 포팅할까 잠시 고민했었는데, 금방 생각을 덮었다.

파이썬 3.0은 2.x 와 호환되지 않는데, 그 만큼 아주 많은 부분이 바뀌었다.
바뀐 부분들은 다음 링크에서 찾아볼 수 있다.

http://docs.python.org/dev/3.0/whatsnew/3.0.html
http://www.python.org/download/releases/3.0/NEWS.txt

나는 이렇게 팍팍 뭔가를 뜯어고치는 파이썬 프로젝트의 스타일이 마음에 든다.
빨리 파이썬 3.0을 써먹어보는 날이 왔으면 좋겠다.
  1. Favicon of http://www.voiceportal.co.kr BlogIcon 김태정 at 2008.12.05 13:27 [edit/del]

    파이썬 쩔어!! ㅎ
    간결하고 강력하고 ㅎ
    근데 우리 회사에서는 쓸일이 없어서 아쉽다..

    Reply
  2. kkkkjul at 2008.12.08 17:31 [edit/del]

    파이썬 웹프레임워크인 django도 이제 1.0버전으로 진입했던데 ㅋ
    나중에 이걸로 블로그나 만들어볼가요 . ㅋㅋ

    Reply
  3. only2sea at 2009.06.21 22:13 [edit/del]

    작년 후반기 6개월간은 거의 파이썬 코딩만 했던 것 같습니다. 지금은 거의 C++ 코딩만 하고 있구요.

    친구가 한번 써 봐... 하고 전파 받은 것은 2003년이었는데... 그 때에는 Java에 관심이 있었습니다.

    스크립트 언어들이 좋아지기 시작한 것은 데탑을 리눅스로 갈아버리고 난 2006년부터였어요. 그렇지만 awk, bash, ruby, perl에 관심이 더 많았던 듯 합니다.

    Reply

submit
자바로 애플리케이션을 개발하다보면, 버전 때문에 골치가 아플 때가 많이 있다.

나는 자바 6으로 처음 애플리케이션을 만들어봤는데, 예전 버전들이 언제쯤 발표된건지, 6으로 개발 했을 때 사용자들이 문제없이 내 애플리케이션을 쓸 수 있을 지 항상 궁금했었다.
오늘은 드디어 자바 버전 히스토리를 찾았다.

http://en.wikipedia.org/wiki/Java_version_history

1.6이 2006년에 발표된 것인지 오늘 처음 알았다.
1.5가 2004년에 발표되었었다는 것도 역시 처음 알았는데, 아직까진 1.6 보다는 1.5으로 애플리케이션을 개발하는 것이 안정적일 것 같다. 미리 알았으면 좋았을텐데 말이다.

그나저나 자바7에서는 또 재밌는 많은 기능들이 생기게 되는 것 같아 기대 된다.

내 첫 자바 애플릿 프로그램 사진 한장.

비즈하드 애플릿

  1. kk02me at 2008.11.27 14:11 [edit/del]

    자바로 개발하시는 군요 ^^
    저도 공대나와서, 학교 다닐 때 실습은 좀 해봤어요~

    지금은 다 까먹었음 ㅋ

    Reply
  2. only2sea at 2009.06.21 22:15 [edit/del]

    오오... 인터페이스가 예쁘네요. 뜨아... 훌륭하세요...

    Reply

submit

void f()
{
    TCHAR s1[10];
    TCHAR s2[10];

    _tcscpy( s1, s2 );
}
warning C4996: 'strcpy': This function or variable may be unsafe. Consider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.

위와 같은 코드는 에러 없이 컴파일 되지만, 경고가 발생한다.
심지어 s2의 버퍼가 s1 보다 크더라도 잘 컴파일 된다.
Visual Studio 2005부터는 strcpy 같은 보안상 취약성을 가진 문자열 함수들은 모두 경고 처리 된다.

학창 시절 Visual Studio 2005가 처음 나와서 써볼 때 strcpy함수가(학부생들이 가장 즐겨쓰는 함수 10위안에는 들어갈) 줄줄이 경고가 발생해 몹시 당황스러웠던 기억이 난다.
당시 우리들은 오직 이 이유 때문에 VS2005가 꼬졌으니 쓰지 말아야 한다면서 VS2003을 사용했는데 지금 생각하니 재밌다.

많은 웹문서에서 올바로된 코드를 작성하는 방법이 아니라 _CRT_SECURE_NO_WARNINGS 옵션을 사용하여 경고 메세지를 숨기는 방법에 대해 설명하곤 한다.
나 역시 한동안 그렇게 사용했었는데 그것은 오히려 경고인 채로 내버려 두는 것보다도 더 나쁘다는 것을 존 로빈스의 책을 통해서 배웠다.
경고 레벨은 항상 최대로 올린채 경고를 숨긴 곳을 찾아 모두 지워라. (VS이던 GCC이던간에) 그리고는 하나씩 경고를 없애 나가야만 한다. - 엄청나게 많던 경고 메세지가 점점 줄어들면서 깔끔하게 컴파일되는 모습을 보는 것은 꽤나 즐거운 일이다. :)

자 그럼, 경고를 원하지 않는다면( 당연히 그래야 한다. ) secure 버전 함수들을 사용해야 한다.
위 코드는 다음과 같이 secure function을 써서 재작성 하자.

void f()
{
    TCHAR s1[10];
   
TCHAR s2[10];

    _tcscpy_s( s1, _countof( s1 ), s2 );

    // 또는 _tcscpy_s( s1, s2 );
}

이제 경고 없이 깨끗하게 컴파일 된다.

2번째 인자로는 s1 배열 요소의 갯수를 넣어주었다.
strcpy_s 함수 같은 경우 2번째 인자의 변수 이름이 _SizeInBytes 라서 sizeof 연산자를 사용하여 코드를 작성하면 될 것 같지만, 유니코드용 함수인 wcscpy_s 같은 경우에는 변수 이름이 SizeInWord로 되어있다. 즉 TCHAR 타입을 사용하는 경우에는 배열의 사이즈가 아니라 배열의 요소의 갯수(문자의 갯수)를 전달해주어야 한다. 이렇게 하면 ANSI나 유니코드 프로젝트에서 모두 잘 동작한다.

위 코드에서는 VC2005부터 제공되는 _countof 매크로를 통해서 배열의 요소 갯수를 구해주었다.

만일 매크로가 없는 환경이라면 단순히
#define _countof(_Array) (sizeof(_Array) / sizeof(_Array[0]))

이렇게 만들어 주면 된다.

빌어먹을 바이트수(cb)와 문자의 갯수(cch)는 언제나 우리를 햇갈리게 하며, 조심스럽게 접근해야 하는 부분이다. 변수 이름 앞에 cb나 cch가 붙어있다면 긴장하고 눈을 크게 뜬채 한번 더 생각해보고 코딩하는 것이 좋다.


Target( 여기선 s1 )이 컴파일 타임에 크기를 잡아둔 정적 배열일 경우에는 2번째 인자를 생략해도 상관없으나 동적으로 크기를 지정한 배열일 경우에는 컴파일러가 알 수 없으므로 인자를 생략할 수 없다.

이제 다음 함수를 보자.
void copy( TCHAR* pResultString )
{
    TCHAR str[MAX] = { 0 };
   
    // some work   

    _tcscpy( pResultString, str );
}

위 함수는 어떤 작업을 한 후에 그 결과 문자열을 pResultString에 저장해주는 기능을 한다.
역시 strcpy 를 사용했기 때문에 C4996 경고.

하지만 이런 함수들은 secure function으로 수정할 수도 없다.
호출하는 쪽에서 문자열 버퍼의 크기를 얼마로 잡았는지 알 수가 없기 때문이다.
따라서 2번째 인자를 넣어줄 수가 없다.

이런 경우에는 이런식으로 메소드 시그내쳐를 수정해야만 한다.
void copy( TCHAR* pResultString, size_t iMaxBuffer )
{
    TCHAR str[MAX] = { 0 };
    
    // some work    

    _tcscpy_s( pResultString, iMaxBuffer, str );
}

그래서 나는 다음과 같은 방법을 더 선호한다.
void copy( CString& strResult ) //또는 std::string& 으로
{
    TCHAR str[MAX] = { 0 };
   
    // some work   

    sResult = str;
}

  1. 김유리 at 2009.03.28 13:52 [edit/del]

    잘보고갑니다 ^^

    Reply
  2. at 2009.05.01 00:07 [edit/del]

    비밀댓글입니다

    Reply
  3. Favicon of http://blog.naver.com/l_need BlogIcon l_need at 2009.05.03 23:48 [edit/del]

    항상 고민하고 있던 내용입니다!! 블로그에 비공개로 담아가겠습니다

    Reply
  4. Favicon of http://www.cvi.kr BlogIcon 허창원 at 2009.05.14 22:43 [edit/del]

    VS를 잘 안쓰다가 오랫만에 쓰려니 이런게 다 문제가 되더군요.
    적절할 때에 큰 도움이 되었습니다.
    감사합니다.

    Reply
  5. only2sea at 2009.06.21 22:17 [edit/del]

    바이트 수랑 문자 개수.. ㅜㅜ 그나마 우리는 덜 틀리지만, 영어권 프로그래머들은 더 많이 틀립니다. 문자도 바이트 단위로 맘대로 싹뚝 자르고... 다행히 요즈음은 그런 경우가 많이 줄었습니다만...

    Reply
  6. 위주 at 2009.06.28 21:57 [edit/del]

    큰 도움되었습니다. 고맙습니다.

    Reply
  7. 주모 at 2014.06.25 13:14 [edit/del]

    안녕하세요. _tsccpy_s() 사용 할때 복사 대상자가 되는 첫번째 인자값이 new로 할당받는 포인터변수이면 _tcscpy_s를 사용하지 못하나요?

    Reply
    • 주모 at 2014.06.25 13:16 [edit/del]

      자답입니다. new TCHAR[len] 이면 _tcscpy_s() 2번째 인자값에 len을 넣으면 되는거였네요

submit
사용자 삽입 이미지
http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html
위 사이트에서는 프로그래밍 언어 순위에 대한 선호도를 매달 업데이트 한다.
가끔씩 들어가서 보는데 심심풀이 용으로 보기에 썩 좋다.
개인적으로는 C++이 하락세인 것이 마음이 아프지만 파이썬 같은 관심있는 언어들이 꾸준히 상위에 랭크되어 있는 것은 기분 좋은 일이다.

사용자 삽입 이미지
Basic은 닷넷 프레임워크로 들어가면서 C#에 밀려 안쓰일꺼라 생각했지만 여전히 많이 쓰이고 있나보다.

파이썬은 2004년도에 그래프가 엄청 높았던 것이 눈에 띄는데, 그 때 나는 프로그래밍에 문외한이었기 때문에 무슨일이 있었던 것인지 모르겠다. -누가 알면 좀 가르켜주십쇼.

아래는 20위 이하 랭크의 언어들.

사용자 삽입 이미지
그냥 재미로만 봅시다.^^
  1. Favicon of http://ing7.tistory.com BlogIcon 루키 at 2008.05.19 20:20 [edit/del]

    전 어쩌다보니 NesC란 언어를 알게 됐네요.
    학교에서 50위에도 못 드는 언어를 배우기 쉽지 않은데.. -0-v

    Reply
    • 김재호 at 2008.05.19 20:42 [edit/del]

      음 처음 들어보는 언어인데요.

      특이한 언어도 경험하시고 좋군요!

  2. Favicon of http://www.lbird.net BlogIcon Lbird at 2008.05.19 21:04 [edit/del]

    우왕, pascal에게는 도대체 무슨 일이 있는 거죠? ^^
    친구들 중에 웹 프로그래머들이 많아서 그런지
    java는 많이 쓰는 걸 봤어도 C 사용하는 사람은
    눈 씻고 찾아봐도 참 찾기 힘들던데 2위라니 그것도 놀랍네요.

    Reply
    • 김재호 at 2008.05.19 21:10 [edit/del]

      하하 글쎄요. 파스칼이 엄청 뛰긴 했네요.

      하지만 10위권 아래 언어들은 1%도 안되서 팍팍 뛰었다 줄었다 하는 것 같아요 -ㅠ-

      저는 cpp만 해서 그런지 java하는 사람들을 거의 못봤어요.
      자바가 정말 저렇게 많이 쓰이나~ 하고 새삼 놀라는걸요 ^^

  3. Favicon of http://fulda.cafe24.com/tet/ BlogIcon 이찬식 at 2008.05.20 14:03 [edit/del]

    텍스트 큐브 스킨 저랑 같네요. 나중에 하나 만들던지 해야겠어요.

    C언어는 죽지않네요. 무적의 C언어

    Reply
  4. 김재호 at 2008.05.20 18:25 [edit/del]

    아 그런가요? 전 설치하고 그냥 기본스킨 쓰는건데.

    무적의 C언어. 쿠쿠쿠쿠^^

    Reply
  5. kkiyak at 2008.05.21 00:41 [edit/del]

    헉! 자바가 1위구나..
    MATLAB....... 꼴도 보기 싫소

    Reply
    • 김재호 at 2008.05.21 07:58 [edit/del]

      푸하하하하하하

      지영씨 MATLAB 하면서 쩔쩔매고 있는거 상상하면 웃음터지네.

submit

이렇게 정의된 클래스가 있다.
//S.h
class S
{
public:
S();
private:
AModule a;
BModule b;
CModule c;
};

클래스 S의 인스턴스는 AModule과 BModule, CModule의 크기를 알고 있어야 생성될 수 있다.
이 모듈들의 크기를 알기 위해서는 당연히 각 모듈들에 대한 클래스 정의가 필요하다.

따라서 이렇게 되야 한다.
//S.h
#include "AModule.h" //모듈들의 정의를 끌어온다.
#include "BModule.h"
#include "CModule.h"
 
class S
{
public:
S();
private:
AModule a;
BModule b;
CModule c;
}; //이제는 컴파일이 가능하다.
하지만 AModule이나 BModule 혹은 CModule이 변경 될때에 S 클래스를 사용하는 다른 모든 파일들이 같이 컴파일 된다.
이 정도의 작은 코드라면 상관없지만, 커다란 프로젝트에서는 코드 몇 줄 고치고 컴파일 되는 동안 담배피우러 가는 일이 잦아질 것이다.

이제 이렇게 정의된 클래스를 보자.
class AModule;
class BModule;
class CModule;
 
class S
{
public:
S();
private:
AModule* a;
BModule* b;
CModule* c;
};
멤버 변수가 포인터나 레퍼런스일 경우에는 인스턴스를 담을 주소공간만이 필요하다.

따라서 클래스 정의를 끌어오지 않아도 괜찮다.
단 S가 알아볼 수는 있어야 하므로 각 모듈들에 대한 선언만은 필요하다.
그래서 전방선언이 되어 있다.


이렇게 되면 모듈들의 인스턴스를 만들기 위해 S의 구현 파일에서
각 모듈들의 정의를 끌어오면 된다.


구현 파일에서 include 했기 때문에 A, B, C 모듈의 정의가 바뀌더라도
S 클래스를 사용하는 클라이언트 코드들은 컴파일되지 않아도 된다.


포인터와 레퍼런스로 충분한 경우에는 굳이 객체를 직접 사용하지 않도록 하여
컴파일 의존성을 줄이고 모듈간의 독립성을 높일 수 있다.
  1. Favicon of http://greenfrog7.egloos.com BlogIcon greenfrog at 2009.02.13 23:16 [edit/del]

    좋은 공부가 되었습니다. 감사합니다.
    트래픽 쏘고 갑니다. ^^

    Reply

submit

사용자 삽입 이미지


python 언어 자체는 너무나 재미있지만, 몇가지 간단한 스크립트를 작성 해보면서, 비주얼 스튜디오에 익숙해 있는 내게 편리한 디버깅 인터페이스와 자동으로 메소드 목록을 보여주는 기능이 없는 환경은 너무나 불편하게 느껴졌다.

이클립스에서 사용할 수 있긴 하지만, 이클립스는 내게 익숙하지 않으므로 패스하고.
Visual Studio에서 파이썬을 붙여서 사용할 수 있다는 것은 이전부터 알고 있었지만 그다지 관심있게 보지는 않았었다.
왜냐하면 IronPython이라는 것은 닷넷 프레임워크 기반에서 동작하기 때문이었다.

어제는 리눅스에서 간단한 파이썬 스크립트를 작성하다가, vim에 그다지 익숙하지 않고 print로 밖에 디버깅 할 수 없는 나에게 조금은 화가 났었다.

곰곰히 생각을 해봤는데 이런 방법이 떠올랐다.
VS2008에 IronPython이라는 것을 붙여서, 닷넷 라이브러리를 사용하지 않은 순수 파이썬 코드만을 편하게 작성하고 리눅스나 다른 환경으로 옮겨서 실행하면 어떨까.

예상은 그럭저럭 들어맞았다.
설치 방법은 생각보다 간단하다.

1. VS2008 설치

2. Visual Studio 2008 Shell Isolated Mode Redistributable package 혹은
Visual Studio 2008 Shell Integrated Mode Redistributable package 다운로드

3. VS2008 SDK 설치

4. http://www.codeplex.com/IronPythonStudio에서 최신 설치 버전 다운로드.
 -  IronPythonStudioIsolatedSetup 혹은  IronPythonStudioIntegratedSetup

isolated 모드와 integrated 모드가 있는데 나는 isolated모드로 설치했다.
사실 잘 이해 못하고 사람들이 더 다운로드 많이 받았으므로 이걸 골랐었다. :(

설치 완료 후 비주얼 스튜디오를 켜니 python 프로젝트가 없길래 역시 쉽게 안되나 생각했었는데, 시작 -> 모든 프로그램에 보니 IronPython Studio라는게 생겨있었다.
아마도 이 부분이 isolated와 integrated에 따라 달라지는 것 같다.
intergrated를 선택해서 설치했다면 아마도 비주얼 스튜디오를 실행 하고 새 프로젝트를 눌렀을 때, C#, C++, 이 밑에 Python프로젝트가 같이 나올 것으로 예상된다.
따라서 저 옵션은 아마 비주얼 스튜디오에 독립적으로, 또는 통합적으로 라는 말이 아니었을까?

어쨌뜬 실행 화면을 보자.

사용자 삽입 이미지
사용자 삽입 이미지
디버깅은 아주 잘된다.
자동으로 메소드 목록이 나타나지 않는 것은 조금 유감이지만 이 정도만으로도 만족한다.
앞으로 좀 더 개선 될 것을 기대한다. :)

submit