Media Log

[Programming]에 해당되는 글 40

  1. SetFilePointer 보다는 SetFilePointerEx를 사용해야 한다 2012/01/16
  2. GetLastError 함수 사용의 흔한 실수 2012/01/13
  3. NTFS에서 Sparse 파일을 만들기 (1) 2012/01/03
  4. 16TB 크기의 파일을 만들어내려면 얼마나 오래 걸릴까? 2012/01/01
  5. 구조체의 패킹에 대한 이야기 (2) 2011/12/19
  6. 프로그래머에게 가장 굴욕적인 순간은? 2011/12/19
  7. C/C++ 코딩 스타일 이야기 (6) 2011/10/24
  8. 레지스트리의 volatile 옵션 2011/09/27
  9. 알아두면 유용한 MoveFileEx 함수의 펜딩 옵션 2011/07/17
  10. 메모리를 해제하기 전에 왜 널 체크를 하는걸까? (4) 2011/05/29
  11. 윈도우 드라이버를 만들 때 알아야 할 기초적인 내용들 (1) 2011/05/23
  12. Duff's Device 2011/04/07
  13. _countof 매크로 2011/03/15
  14. FIELD_OFFSET 매크로 2011/03/01
  15. PAGED_CODE 매크로 2011/02/27
  16. 디렉터리의 읽기 전용 속성 (4) 2011/02/20
  17. 알쏭달쏭한 typedef (7) 2011/01/04
  18. 하위 디렉터리의 파일이 변경 되었는지 감지하는 법 (4) 2010/12/20
  19. WinApi의 reserved 인자는 뭐하는 용도일까 2010/12/09
  20. 디바이스 드라이버를 단일 실행파일로 배포하는 방법 (2) 2010/12/04
  21. 윈도우즈의 세션, 윈도우 스테이션, 데스크탑에 대해 자세히 알아보기 (2) 2010/11/07
  22. Cancel-Safe Queue를 이용하여 디바이스 드라이버에서 I/O를 취소하기 (2) 2010/10/25
  23. 재귀호출이 무엇입니까 (2) 2010/10/21
  24. 유저모드에서 파일시스템 드라이버를 만들기 2010/10/17
  25. Win32 에러 번호를 간편하게 확인하기 (1) 2010/08/15
  26. 로그 뷰어로써의 Vim (2) -원하는 문자열만 골라내기 (3) 2010/06/20
  27. 로그 뷰어로써의 Vim (1) -멀티 하이라이팅 2010/06/20
  28. pragma message 지시어를 통한 실수 방지 테크닉 (2) 2010/01/13
  29. Absolute path와 Canonical path의 차이점 (1) 2010/01/13
  30. 2009년 최고의 프로그래밍 언어, Google의 Go (3) 2010/01/10
파일을 열 때 파일 포인터는 0으로 셋팅된다. 이후 해당 파일에 ReadFile이나 WriteFile등의 함수를 통해서 I/O를 하게 되면 파일 포인터가 자동으로 증가하게 된다. 물론 윈도우는 사용자가 직접 오프셋을 조정할 수 있는 인터페이스도 제공해주는데 SetFilePointer 함수가 바로 파일 포인터를 이동 시키는 인터페이스이다.
파일 포인터는 각 핸들별로 따로 관리된다. 즉 같은 파일이라 할지라도 2번을 열어서 핸들을 2개 가지고 있다면 각 핸들에 연결된 파일 포인터는 각각 독립적으로 움직인다.

이 SetFilePointer는 너무 복잡하게 만들어진 함수이다. 그래서 제대로 사용하기가 어렵다. 지금까지 내가 SetFilePointer 함수를 사용하는 코드를 보았던 곳에서는 제대로 작성된 코드가 거의 없었던 것 같다. 그렇다면 어떤 부분이 그렇게 SetFilePointer의 사용을 힘들게 만드는 것일까?

SetFilePointer 함수는 다음과 같이 생겼다. 32비트와 64비트를 동시에 지원하기 위해 2번째 인수와 3번째 인수를 통해 각 4바이트씩 총 64비트 만큼의 오프셋 정보를 전달할 수 있도록 만들어졌다.

DWORD WINAPI SetFilePointer(

  __in         HANDLE hFile,

  __in         LONG lDistanceToMove,

  __inout_opt  PLONG lpDistanceToMoveHigh,

  __in         DWORD dwMoveMethod

);

첫번째로 많이 하는 실수는 오프셋이 32비트 크기를 넘어갈 수 있는 경우에도 항상 lpDistanceToMoveHigh 에 NULL을 넣고 있는 경우이다. 4기가보다 큰 파일에 대해서 제대로 지원하지 못하는 경우인데 오래 전에 작성된 코드에서 흔히 볼 수 있다.
두번째. SetFilePointer의 리턴값은 변경된 오프셋 값이며 함수가 실패할 경우에는 INVALID_SET_FILE_POINTER 를 돌려주게 된다. INVALID_SET_FILE_POINTER의 값은 -1로 정의되어 있고, 이 값은 DWORD로 받아지기 때문에 0xFFFFFFFF가 된다. 그런데 만약 내가 변경하고 싶었던 위치가 0xFFFFFFFF(4기가) 였다면? 사용자는 0xFFFFFFFF위치로 오프셋을 옮겨줄 것을 요청했고 함수는 사용자가 원한 동작을 제대로 수행한 뒤 0xFFFFFFFF를 리턴했다. 이제 이 값이 에러인지 정상적인 오프셋 값인지 어떻게 구분해야할까? 사용자는 이를 확인해보기 위해서 반드시 GetLastError를 호출해야 한다. 만일 함수가 성공했고 제대로된 오프셋이라면 LastError가 ERROR_SUCCESS로 셋팅되어 있을 것이다. 지난 번에 윈도의 LastError값은 오직 함수가 실패할 때만 셋팅된다고 했었는데, SetFilePointer와 같은 몇몇 특별한 함수에서는 성공시에도 값을 0으로 만들어 준다. 물론 그렇게 하는 이유는 위처럼 리턴값만으로는 모든 정보를 전달해줄 수가 없기 때문이다.

따라서 SetFilePointer를 사용하는 곳에서는 다음 표에 있는 것처럼 리턴값을 확인해야 한다.

  If lpDistanceToMoveHigh == NULL If lpDistanceToMoveHigh != NULL
If success retVal != INVALID_SET_FILE_POINTER retVal != INVALID_SET_FILE_POINTER || GetLastError() == ERROR_SUCCESS 
If failed retVal == INVALID_SET_FILE_POINTER retVal == INVALID_SET_FILE_POINTER && GetLastError() != ERROR_SUCCESS
많은 사람들이 틀리게 사용할 만도 하다.

이제 내가 하고 싶었던 말을 정리하면,
  • SetFilePointer 함수를 사용한 곳을 보게 되면 위 내용을 유심히 살펴보는 것도 재미있다. 그리고 코드가 틀렸다면 바르게 고쳐라.
  • 위 표에 나온대로 고치려고 하지말고, SetFilePointerEx를 사용해서 고치는 것이 좋다.
  • GetFileSize 함수도 역시 비슷한 문제가 있다. GetFileSizeEx만 사용해라

다음은 프로그래밍 센스를 확인해 볼 수 있는 간단한 퀴즈이다.
윈도에는 SetFilePointer와 SetFilePointerEx라는 함수는 존재하지만 GetFilePointer라는 함수는 존재하지 않는다. 그렇다면 윈도에서 현재 가지고 있는 핸들의 파일 포인터의 오프셋은 어떻게 구할 수 있을까?

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/245 관련글 쓰기

submit
GetLastError는 윈도 Api를 호출 한 뒤 해당 함수의 Win32 에러 코드를 받아오기 위한 함수이다. 이 오류 정보는 쓰레드별로 하나만 저장되기 때문에 함수가 실패한 후 다른 함수를 실행하기 전에 에러 값을 읽어와야 한다. 다른 함수들이 호출된 이후에는 에러 값이 덮어 씌워져 버릴 수 있다.

보통은 아래와 같이 사용한다.
HANDLE h = CreateFile(...);
if (h == INVALID_HANDLE_VALUE)
{
  DWORD dw = GetLastError();
  ... Do something
}

경험이 많지 않거나 주의 깊지 않은 프로그래머들은 프로그램을 유지보수 하면서 이미 잘 만들어져있던 위와 같은 코드를 별 생각 없이 아래처럼 바꾸기도 한다.
HANDLE h = CreateFile(...);
if (h == INVALID_HANDLE_VALUE)
{
  DoSomethingElse(); // 뭔가 예외를 처리하기 위해 추가적인 코드를 여기에 쑤셔넣는다. 아니, 왜 하필 여기에.
  DWORD dw = GetLastError();
  ... Do something
}
처음에 말했듯이 DoSomethingElse()안에서 윈도 Api를 사용한다면 쓰레드 저장소에 있던 LastError 코드가 다른 값으로 바뀌어버릴 수 있다는 것을 예상할 수 있다. 항상 코드를 읽으면서 GetLastError를 호출하는 부분이 에러값을 확인하려고 했던 함수의 바로 아래에 붙어있지 않다면 섬뜩함을 느껴야 한다. 하지만 잘 모르고 있으면 보이지 않는 법.

HANDLE h = CreateXXX(...);
DWORD dw = GetLastError();
if (dw == ERROR_SUCCESS)
{
  ... 핸들을 가지고 다른 무엇인가를 한다.
}
else
{
  ... 함수의 실패처리를 한다.
}
이번에는 한 Api를 호출 한 뒤에 바로 GetLastError를 호출해서 에러값을 얻어왔다. 얼핏보면 맞는 것도 같지만 역시 틀린 코드이다. 함수의 성공 실패 여부는 함수의 스펙에 따라 리턴 값 등으로 확인해야지 GetLastError 값으로 확인해서는 안된다. 왜냐하면 Win32에서 제공되는 대부분의 Api들이 함수가 성공했을 때는 LastError 값을 건드리지 않기 때문이다. 위 코드에서는 함수가 성공할 때는 에러 값도 0(ERROR_SUCCESS)으로 셋팅시켜 줄 것이라 굳게 믿고 있다. 실상은 그렇지 않다. GetLastError는 오직 함수가 실패했을 때만(그 바로 직후에) 호출해야 한다.

대부분의 함수들은 그 성공 여부를 리턴값으로 가르쳐준다. 리턴 값으로 성공과 실패 여부를 호출자에게 전달해주기로 했다면 뭐하러 또 SetLastError(ERROR_SUCCESS) 와 같은 추가적인 코드를 호출하겠는가.
하지만 어떤 함수들은 성공시에도 SetLastError(ERROR_SUCCESS)를 정확히 호출해주기도 하는데, 이것에 대한 이야기는 다음 포스트에서 해보려고 한다.
저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/246 관련글 쓰기

submit
Sparse 파일을 만드는 것은 Win32 Api로서 제공되지는 않으며, 파일 시스템이 인터페이스를 제공한다.
콘트롤 코드를 파일 시스템 장치에 직접 보냄으로써 Sparse 파일을 만들어 낼 수 있다.

HANDLE h = CreateFileW(
    L”D:\\MySparseFile.TXT”,
    GENERIC_WRITE,
    FILE_SHARE_DELETE,
    0,
    CREATE_NEW,
    0,
    0
);

if(!DeviceIoControl(
    h,
    FSCTL_SET_SPARSE,
    NULL,
    0,
    NULL,
    0,
    &dwWritten,
    NULL))
{
    dwError = GetLastError();
    return dwError;
}

Sparse파일을 만들게 되면, 파일 포인터를 충분히 크게 이동하고 SetEndOfFile을 호출해도 실제로 데이터를 기록하지 않으며(그러므로 SetEndOfFile이 금방 반환된다) 나중에 파일의 해당 부분을 읽을 때 파일 시스템이 해당 부분의 데이터는 0으로 돌려주게 된다. WriteFile 같은 함수를 통해 실제로 데이터를 쓰는 경우에만 디스크의 용량을 차지하게 되는 이점이 있는데, 버추얼 박스나 VMware에서 생성하는 커다란 동적 하드 디스크를 구현 할 때 이런 방법을 사용하면 된다. -처음에 가상 디스크의 용량을 크게 잡아둬도 실제 하드 디스크 용량을 차지하지 않다가, 사용하면 할수록 하드 디스크의 사용량이 늘어나는 것을 본 적이 있을 것이다.

윈도에 인스톨 될 수 있는 모든 파일 시스템이 Sparse 파일을 지원한다고 가정해서는 안된다. FAT이나 다른 벤더에서 만든 인스톨러블 파일 시스템은 Sparse 기능을 지원하지 않을 수도 있다. Sparse 기능이 지원되는지 알아보기 위해서는 GetVolumeInformation 함수를 사용한다.
FAT 파일 시스템도 Sparse파일을 지원하지 않는데, 그러므로 NTFS상의 Sparse 파일을 FAT 볼륨으로 복사하게 되면 FAT볼륨에서는 더 이상 공간이 절약되지 않는다. (실제로 0을 디스크에 써버릴 것이다.)
Sparse 파일에서 실제로 디스크에 저장된 용량을 알고 싶을 때는 GetCompressedFileSize 함수를 사용하면 된다.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/243 관련글 쓰기

  1. Favicon of http://blog.spowner.com BlogIcon spowner at 2012/01/06 13:29 [edit/del]

    유용한 정보 감사합니다

    Reply

submit
NTFS에서 만들 수 있는 단일 파일의 가장 큰 크기는? 16테라. 빙고.
그럼 NTFS 상에서 16 테라 바이트의 파일을 만들기 위해서는 얼마나 시간이 걸릴까?

답은 여기에 있다.

16TB 라는 숫자의 이미지가 머리 속에 잘 안그려져서 내 추정보다 훨씬 큰 기간이 나와버렸는데, 가만히 계산해보니 그렇게 놀라운 숫자도 아니다. 16TB라는 숫자를 너무 얕봤나보다.
저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/241 관련글 쓰기

submit
MSDN에 따르면 구조체의 디폴트 패킹 값은 8이다. 간혹 32비트 운영체제에서는 4바이트이고 64비트 운영체제에서는 8바이트라고 주장하는 사람들도 있는데 디폴트 패킹 크기는 컴파일러가 결정하지 운영체제가 결정하는 것이 아니다. MSVC에서 디폴트 패킹을 8바이트로 정한 이유는(32비트 운영체제에서 조차) 기본 타입 중 가장 큰 타입이 8바이트이기 때문이다. 만약 이후에 16바이트 포인터나 INT128 같은 타입을 기본 타입으로써 사용하는 날이 온다면, 그 때는 디폴트 패킹 값도 16바이트로 변경될 것으로 예상한다.

여기에 패킹을 잘 이해하고 있는지 알아보기 위한 좋은 질문이 있다.

strcut X
{
  char c1;
  char c2;
  char c3;
  char c4;
  char c5;
  char c6;
  char c7;
};

struct Y
{
  char c;
  double d;
  int i;
};

디폴트 패킹 값인 8을 사용한다고 할 때 구조체 X와 Y의 크기는 각각 얼마일까?

잠시 생각해보고 아래를 클릭해서 답을 확인해보도록 하자.

더보기


구조체의 멤버들은 자신의 크기의 배수로 정렬되는 것이 좋다. char는 1의 배수, short은 2의 배수, int는 4의 배수, double은 8의 배수의 메모리 번지 주소에 위치하고 있을 때 우리는 해당 데이터가 정렬되어 있다고 말한다.
x86호환 아키텍쳐에서 윈도우즈 응용 프로그램을 만들 경우에는 정렬이 되어있지 않을 때 CPU가 메모리에 다시 접근하려고 시도하면서 성능이 떨어지게 된다. 다른 아키텍쳐에서는 응용이 크래시가 나거나 따로 예외 핸들링을 해주어야 할 수도 있다.

컴파일러는 데이터를 정렬시키기 위해서 구조체의 적당한 위치에 패딩을 집어넣는다. 조금 생각해보면 위 Y구조체에 마지막 4바이트 패딩은 필요가 없을 것 같다. 중간에 넣은 7바이트 패딩으로 인해 3개의 필드가 모두 잘 정렬이 된 것 같은데 말이다.
뒷 부분에 4바이트 패딩을 넣은 이유는 구조체가 배열에서 사용될 때 구조체의 멤버들이 메모리의 정렬된 위치에 올라가도록 하고 싶기 때문이다. 뒷 부분에 패딩을 넣지 않았으면 int나 double 타입이 자신의 타입에 맞게 정렬된 주소에 올라가지 못했을 것이다.

다음 Z구조체를 보자. 위의 Y구조체에서 double과 int의 위치만 바꾸었다. -위치만 바꾸었는데 패딩이 Y구조체와 다르게 들어가는 것에 대해서도 유심히 살펴 보아야 한다.
struct Z
{
    char c;
    // pad[3]
    int i;
    double d;
};

int _tmain(int argc, _TCHAR* argv[])
{
    // 다음 코드를 사용해서 어떻게 padding이 들어가 있는지 쉽게 확인해볼 수 있다.
    printf("position c:%d\n", FIELD_OFFSET(Z, c));
    printf("position i:%d\n", FIELD_OFFSET(Z, i));
    printf("position d:%d\n", FIELD_OFFSET(Z, d));
    printf("Total size:%d\n", sizeof(Z));
}
직접 코드를 실행시켜보는 것도 좋고, 아래 그림을 보고 이해해도 좋다. 이 구조체가 배열에서 사용될 때에는 아래와 같은 레이아웃을 갖게 될 것이다.

char는 1의 배수에, int는 4의 배수에, double은 8의 배수에 정렬되어져 올라가 있는 것을 주목하라. 진한 파란색으로 표시된 3바이트 패딩의 매직이다.

맨 처음 문제에서 X구조체의 크기가 8bytes가 아니라 7bytes인 이유는 모든 멤버가 char이기 때문이다. char는 1의 배수인 어느 주소에나 올라가도 되므로 padding을 집어 넣지 않아도 모든 멤버가 항상 자신이 원하는 주소에 올라가게 된다.
Y구조체가 Z구조체와 멤버 위치만 바꾸었는데 다른 레이아웃을 가지고 있는 이유도 그림을 그리면서 확인해보면 이해할 수 있을 것이다.

8로 패킹한다는 것은 구조체의 크기를 8의 배수로 맞추겠다는 것이 아니라, 크기가 8보다 큰 멤버가 있을 때는 정렬을 포기한다는 것을 뜻한다. 즉, 크기가 8보다 작은 타입에 대해서만 정렬하려고 시도하며, 이것은 다른 말로, 변수의 메모리 주소를 최대 8의 배수로 정렬한다는 뜻이 된다.
만약 패킹 크기를 4로 바꾼다면 double이나 int64_t 같은 타입들이 사용되었을 때 더 이상 정렬이 보장되지 않게 된다. 왜 디폴트 값을 8로 정했는지 이해가 되는가?

구조체를 만들 때는 어떻게 패킹이 될지 잘 예상해서 조각을 맞추듯이 만들어야지 아무 순서로나 마구 쑤셔넣는 것은 프로답지 못하다. 마이크로소프트에서 만든 거의 대부분의 구조체들은 이런 사소한 것들까지 잘 고려해서 만들어져 있다.
저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/238 관련글 쓰기

  1. 황후순 at 2011/12/19 09:02 [edit/del]

    틀린 내용인거 같은데... double이 많을까요? Pointer가 많을까요?
    포인터가 32비트에서 4바이트고 64비트 8바이트라서 패킹을 각각 하고 있습니다. 구조체 크기랑은 별개의 이야기지요.메모리크기는 당연히 데이터 사이즈대로 나오겠죠. 패킹과 메모리사이즈는 별개의 얘기죠.
    단편화 생기는 과정을 만들어서 프로그램이 어떻게 죽는지 확인해보시기 바랍니다.
    필자는 ms vs compiler만 확인해보신 것이 아닌지... 메모리 패킹은 운영체제가 변경됨에 따라도 얼마든지 바뀔수 있으니 무조간 8바이트라고 단정하는건 문제가 됩니다.

    Reply
    • Favicon of http://www.benjaminlog.com BlogIcon 김재호 at 2011/12/19 10:10 [edit/del]

      패킹이랑 메모리 사이즈는 별개의 이야기가 아닙니다. 그리고 패킹은 운영체제와는 상관없는 이야기이고요.
      무조건 8바이트라고 단정한 것이 아니라 왜 기본값을 8바이트로 정했는가에 대해서 말해본거에요.

      프로그램이 어떻게 죽는지 확인해보라는 말을 조금만 더 자세히 설명해 주실수 있을까요? 무슨 이야기를 하고 싶으신건지 궁금하네요^^

submit
프로 당구 선수에게 가장 굴욕적이고 부끄러운 순간이 있다면 바로 키스를 내는 일일 것이다. -아마추어 세계에서는 삑사리를 내는 것일지도 모르지만. 축구 선수에게는 알까기를 당하는 것, 농구에서는 블록킹을 당하는 순간일지도 모르겠다.

만일 프로그래머에도 이런 순간을 하나 꼽으라면 나는 버그 관리 시스템에서 이슈를 해결 처리 했는데 재등록이 되거나 그로인해 다른 버그를 유발하는 것이라 생각한다. 버그가 재등록된다는 것은 제대로 테스트 해보지 않았다는 것이고 다른 버그를 유발 시켰다면, 앞뒤 로직을 충분히 검토하지 않고 버그가 발생한 바로 그 곳만 바라보고 버그를 수정했다는 뜻이다. 부끄러워하고 반성해야 한다.
뒤돌려치기(우라마시)를 치는데 그냥 운에 맡기고 친 후 쫑이 날 수도 있는게 당연하다고 생각한다면 절대 당구 실력이 늘지 않는다. 프로그래머에게도 마찬가지라고 생각한다.

갑자기 이런 이야기가 생각이 난 것은 레이몬드 첸의 한 포스트를 읽고 나서였는데, 옛날 처음에 회사에 들어갔을 때의 내 모습이 떠올랐기 때문이다. (그냥 글을 읽는 중에 그런 일들이 떠올랐던 것 뿐, 지금 말하는 이야기가 레이몬드 첸이 말한 요지과 관련이 있는 것은 아니다.)

나는 프로그램을 디버그 하다가 널 포인터 접근 등의 예외로 프로그램이 크래시 된 것을 확인하였으면 해당 부분을

if (ptr)
{
 // *ptr을 사용
}

등으로 널 포인터 체크를 추가하여 수정하고는 이슈를 완료시키곤 했다. 하루에 버그를 10개 넘게 고친 날도 많았다.

꼭 널 포인터만의 이야기를 하는 것은 아니다. 버그가 발생한 앞뒤 로직을 충분히 점검하지 않고, 그 곳만 쳐다본채 코드를 수정하는 것에 대한 문제점을 이야기 하고 있는 것이다. 그 때 그 버그들이 정말 고쳐진 건지 아직도 모르겠다.
부작용도 많았지만, 또 어떻게 어떻게 메꾸어 내었고, 이슈 해결량이 많았기 때문에 나는 생산성이 좋다는 평가를 받았다. 지금 생각하면 부끄러운 일이다. 나는 그냥 버그를 숨겼을 뿐이다. 

언제부턴가 그런 것이 잘못된 것이라는 것을 깨닫게 되었다. 아마도 나의 스승(?)이 일을 처리하던 모습을 유심히 관찰하면서였던 것 같다.
그 이후로는 버그가 발생했을 때 항상 전후 함수들을 모두 따라가보면서 로직들을 점검하고 어디가 문제인지, 널 포인터 체크를 추가해서 버그를 해결해도 되는건지, 또는 널 포인터가 들어오게 하는 것이 잘못인지 등을 꼼꼼히 따져보고 코드를 작성하는 습관을 들이게 되었다.

이런 습관을 들인 후에는 버그가 재등록이 되거나 다른 버그를 유발하는 일이 점점 줄어갔다. 예전처럼 하루에 버그를 10개 넘게 고치지는 못하지만 지금도 여전히 생산성이 좋다는 평가를 받는다. 다시 재등록 되는 버그에 대한 작업을 할 필요가 없기 때문이다. 코드를 추가할 때마다 다른 버그가 발생해서 고치고 또 고치고 하면서 시간을 보내는 경우를 본 적이 있는가? 주위를 유심히 잘 살펴본다면, 얼마나 그런 경우가 많은지에 대해서 놀라게 될 것이다. 어쩌면 여러분 자신일지도 모른다.
하루에 버그를 1개를 고치더라도, 코드를 10줄을 작성하고 퇴근하더라도 제대로 하는 것이 더 프로젝트를 빠르게 진행하는 길이라는 것을 이제는 확실히 알고 있다.

만일 여러분이 팀장이나 프로젝트 관리자라면, 10분만에 버그를 고치는 사람에게는 칭찬을 해줄 것이 아니라, 혹시 저런 문제가 있는 것은 아닌지 잘 관찰해봐야한다. 그리고 만일 어떤식으로든 가르침을 주어 변화시킬 수 있다면 여러분 또한 멋진 스승이고 멘토이다.
저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/236 관련글 쓰기

submit
프로그램을 처음 배울 때는 거의 정신병자 수준으로 코딩 스타일에 집착했었는데 나이가 들어가면서 코딩 스타일에 조금씩 둔감해져가고 있다.
대학 때는 들여쓰기를 tab으로 했었는데 첫번째 회사에서 space가 규칙이라고 꼭 그렇게 쓰라고 강제했다. 나는 tab을 버리기 싫었지만 규칙을 어기지 않기위해 그렇게 쓰다보니 space가 너무 좋아져버렸다. 2번째 회사에서는 다시 tab을 사용하라고 한다. 좀 촌스럽네? 아직도 tab을 쓰는데가 있었나 정도의 생각은 들지만 거부감 없이 받아들일 수 있었다. 그 외의 다른 스타일들도 비슷하다.

그래도 여전히 눈에 거슬리고 바꿔버리고 싶은 욕구가 드는 C/C++ 코딩 스타일이 2가지가 있는데 그 중 하나는
if (0 == str.Length())
{
}
위처럼 상수를 좌측에 쓰는 코드이다.
프로그램을 읽기에도 불편하고 쓸 때 또한 불편한데 도대체 왜 상수를 왼쪽에 쓰는가.

아마 어떤 사람들은 그렇게 써야 == 연산자 대신 = 를 사용해버리는 실수를 방지할 수 있어요, 라고 대답할지도 모른다. 하지만 요즘 컴파일러는 이런 실수를 경고로 가르쳐주는 기능을 다 가지고 있는데 굳이 저렇게 쓸 필요가 있는가? 컴파일러나 정적분석툴의 도움를 받을 수 없는 상황에서나 어쩔수 없이 사용하던 구식 방법인데 이를 무작정 따라하는 사람들이 많다. 다음 코드는 조금 더 보기에 안좋다.
if (MAX_PATH <= str.Length())
{
}
'상수를 왼쪽에 써야 실수를 줄일수 있다고 했지'. 라고 생각없이 이 말을 받아들인 사람들은 == 이 아니라 비교연산자를 사용할 때에도 모든 상수는 죄다 왼쪽에 써버린다. 위 코드를 읽을 때 머리가 2번씩 돌아가는 것 같지 않은가?

또 한가지 싫어하는 C/C++ 스타일은
bool xxx = fOk;
if (xxx == true)
{
}
이렇게 불린 테스트를 true나 false와 명시적으로 비교하는 것이다.

아래 strcmp 함수의 경우 처럼 표현식의 결과가 불린 값이 아닌 경우에는(strcmp의 리턴값은 정수이다) 같은 타입으로 명시적으로 비교를 해서 표현식을 참 혹은 거짓으로 맞추어 주는 것이 의미가 있다.
if (!strcmp(xxx, yyy)) // 이보다는
if (strcmp(xxx, yyy) == 0) // 이게 더 낫다고 생각한다.
하지만 이미 어떤 변수나 식이 이미 참과 거짓을 나타내고 있다면 또 다시 그것을 true나 false와 비교하는 것은 명백한 중복이다. 나는 그런 경우는 그냥
if (xxx)
{
}

또는
if (!xxx)
{
}
이렇게 쓰는 것을 선호한다.

위에서 처럼 true와 비교하는 것보다 1로 정의된 대문자 TRUE를 비교하는 것은 훨씬 나쁘다.
if (xxx == TRUE)
{
}
xxx 값이 0도 아니고 1도 아닌 값(하지만 참인)을 가진 경우에는 골탕을 먹게 되기 때문이다.

오늘 stackoverflow를 구경하다가 재밌는 사실 하나를 알게되었다.
많은 사람들이 나만큼이나 위 두가지 스타일을 싫어한다는 것이다.
어떤 코딩 스타일이 가장 거지같다고 생각하나요?

내가 위에서 말한 2가지 스타일이 1등과 3등을 차지 했다니 아마 저런 스타일을 사용하는 사람들은 잘 믿기지 않을 것이다.
재밌는 답변과 댓글들이 많으니 관심있는 사람들은 위 포스트를 한번 읽어보기 바란다.
나는 아래 댓글이 가장 재밌고 인상적이었다.
Damn. We say "if it's raining, open your umbrella" and NEVER "if it's true that it's raining, take your umbrella"... Testing explicitely against boolean is as verbose and as un-natural as the second example
저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/223 관련글 쓰기

  1. Favicon of http://eslife.tistory.com BlogIcon esstory at 2011/10/29 14:55 [edit/del]

    상수를 앞에 쓰는 건 이제 손에 익어서 항상 지키게 되는데 ㅎㅎ
    컴파일러가 좋으니 이제 이런 학습들도 구닥다리가 되는거네요 ㅎㅎ

    Reply
    • Favicon of http://www.benjaminlog.com BlogIcon 김재호 at 2011/11/02 09:58 [edit/del]

      네, 책에서 (또는 다른 사람이) 말하는 내용을 그대로 믿지말고 한번 잘 생각해보고 걸러서 받아들이는 자세가 중요하다고 여기어집니다.

  2. Favicon of http://blog.spowner.com BlogIcon spowner at 2011/12/01 11:00 [edit/del]

    안녕하세요.
    저같은 경우도 0 == xxx 싫어합니다. 사람이 사람보기 편하게 코딩해야지 이건 뭥미.. 텍스트에디터로 코딩하는것도 아니고.. 상수도 마찬가지.
    하지만 저는 명시적으로 true/false를 적어주는것을 선호합니다. 다양한 언어들을 접하고 사용하다보니 명시적으로 적지 않으면 제가 눈에 확 안들어오더라고요

    Reply
  3. subong at 2012/01/31 12:29 [edit/del]

    if문의 경우 true를 쓰지 않는 부분에 대해선 공감합니다만
    false의 경우엔 !를 사용하는경우 상황에 따라 눈에 잘 안들와
    잘못 읽는 경우도 생기는 갓 같아 false인 경우에는 명시적으로 쓰고있네요.
    if (!variable)
    =>
    if (variable == false)

    Reply
    • Favicon of http://www.benjaminlog.com BlogIcon 김재호 at 2012/01/31 19:06 [edit/del]

      if (variable == false)는 전혀 문제가 없고 보기에도 좋은 코드입니다.
      if (!variable) 도 역시 보기에 좋습니다.

      이런 경우에 저는 최대한 다른 사람들이 많이 사용하는 스타일을 사용하려고 합니다. 이렇게 하면 많은 경우에 유리합니다. (특히 스타일을 가지고 누군가 따질 때. C/C++하는 사람들이 흔하게 맞게 되는 일이죠)
      C/C++ 세계에서는 if (boolean_variable == false)보다는 if (!boolean_variable) 이라고 생각이 되어요.

submit
윈도의 레지스트리에 무엇인가를 기록하면 항상 영구적으로 저장된다고 생각하는 사람들이 많이 있는 것 같다.
윈도는 일회용으로 레지스트리에 정보를 기록할 수 있는 방법 또한 제공한다.

RegCreateKeyEx 함수를 통해 레지스트리의 키를 생성할 때 REG_OPTION_VOLATILE 옵션을 주면 데이터를 일회용으로 저장할 수 있다.

컴퓨터가 켜져있는 동안 일회성으로 정보를 기록하고 나중에 종료될 때 지워야 할 경우를 종종 맞닥드리게 되는데, 이럴 때 해당 옵션을 사용하면 편리하다. 마치 CreateFile의 DeleteOnClose 옵션과 비슷하다고도 할 수 있겠다. 이런 옵션을 사용하지 않으면 응용이 종료될 때에 직접 데이터를 정확히 지워줘야 하며, 응용이 비정상 종료된다거나 하면 더욱 골치 아파지고 에러 처리를 하기 위해서 쓸데없는 로직을 집어 넣게 된다.
저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/208 관련글 쓰기

submit
MoveFileEx 함수는 파일 이름 변경이나 삭제를 컴퓨터가 재시작할 때 까지 지연시킬 수 있는 상당히 유용한 옵션이 있는데 꽤 많은 사람들이 잘 모르고 있는 것 같다.
이 옵션은 스마트 업데이터 같은 프로그램이 DLL을 교체 시켜야 한다거나 언인스톨러시 파일을 삭제해야 하는데 다른 곳에서 이미 파일이 사용중이어서 삭제할 수 없는 경우에 유용하게 쓸 수 있다.

MoveFile 함수는 내부적으로 CreateFile 함수를 통해 파일을 오픈하는데 이 때 DesiredAccess로 DELETE을 사용한다. 파일이 잘 열렸다면 RenameInformation IRP를 날린 후 핸들을 닫고 성공으로 반환하지만, 이미 다른 위치에서 파일이 열려있었다면 먼저 파일을 연쪽에서 FILE_SHARE_DELETE를 함께 주지 않았었을 경우 파일 열기가 ERROR_SHARING_VIOLATION 으로 실패하게 되어 MoveFile 함수 또한 실패로 리턴해버리게 되는 것이다.

재부팅 시에라도 dll 등을 교체시켜주거나 깨끗하게 삭제하기를 원한다면 MoveFileEx함수를 호출 할 때 세번째 파라메터로 MOVEFILE_DELAY_UNTIL_REBOOT 옵션을 주면 되는데, 이렇게 하면 MoveFileEx함수는 레지스트리의 HKLM\System\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations 위치에 어떤 오퍼레이션이었는지 정보를 적어 놓기만 하고 리턴한다. 시스템이 재부팅 되고 나서 응용프로그램들이 실행되기 전 운영체제에서 레지스트리를 확인해 보고 해당 동작을(이름변경 혹은 삭제) 수행해 주기 때문에 어떤 파일이던지 삭제가 가능하다. HKLM 위치에 써야 하기 때문에 관리자 권한은 필요하다.

이와 관련된 몇 가지 알아두면 좋을 지식들이 있다.
  • 다른 곳에서 파일을 열고 있다고 이름 변경을 못하는 것은 아니다. 먼저 파일을 연쪽에서 어떤 공유 모드로 파일을 열었는지가 중요하다. 파일을 먼저 오픈 하는 쪽에서 FILE_SHARE_DELETE옵션을 주어서 CreateFile을 하면 다른 위치에서 해당 파일의 이름을 변경 할 수 있다. 심지어는 삭제도 가능한데(DeleteFile을 호출하면 성공한다) 이때는 파일이 삭제 상태로만 마킹 되며 파일 시스템 드라이버는 해당 파일을 열어 놓은 모든 핸들이 닫힐 때 실제로 삭제를 수행한다. 이렇게 삭제 상태로 마킹되어 있는 동안에는 또 다른 곳에서 파일 오픈 시도가 생겼을 때 ERROR_ACCESS_DENIED 에러가 발생하게 된다. 파일 핸들을 닫기 전까지는 이런 DELETE_PENDING 상태의 파일을 삭제되지 않은 상태의 파일로 다시 돌리는 것 또한 가능하다.
  • 다른 한 쪽에서 파일 삭제를 허용하지 않고 먼저 파일을 열어두었을 시에, MoveFileEx에 MOVEFILE_DELAY_UNTIL_REBOOT 옵션을 주어 함수를 호출하더라도 파일 열기시 ERROR_SHARING_VIOLATION에러를 받게 되지만 이 때는 MoveFileEx 함수가 실패로 리턴하지 않고 레지스트리에 기록을 해주기 때문에, 어떤 파일이던지 간에 이름 변경이나 삭제를 할 수가 있는 것이다.
  • 함수 모양을 봤을 때 MoveFileEx나 DeleteFile처럼 HANDLE을 인자로 전달받지 않고 파일 경로를 전달 받는 함수는 모두 내부적으로 파일을 오픈한다.
  • SetFileInformationByHandle 함수를 사용하면 추가적으로 파일을 다시 열지 않고 Rename, Delete등의 작업을 할 수 있다. 이 함수는 파일 시스템 드라이버에 전달되는 IRP와 거의 비슷하게 매핑되는 아주 강력한 함수이다. 파일 속성에 대한 모든 조작은 이 함수를 통해서 할 수 있다.
    하지만 워낙 저수준의 함수이기 때문에 사용법이 조금 어렵게 느껴질 수도 있다.
    아래 글에 해당 함수를 사용하여 이름 변경을 하는 코드에 대한 설명이 있다.
    하위 디렉터리의 파일이 변경 되었는지 감지하는 법
     
저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/206 관련글 쓰기

submit

결론부터 이야기 하면 널 체크는 할 필요가 없다.

if (p)
{
free(p);
p = NULL;
}

또는
if (p)
{
delete p;
p = 0;
}

이렇게 메모리를 해제하기 전에 널 포인터인지 체크하는 코드를 수도 없이 많이 보았다.
아마도 이미 해제한 메모리를 또 해제 하다가 프로그램이 죽지 않게 하기 위해서 일 것이다.

그런데 free나 delete에는 널 포인터를 집어 넣어도 아무런 문제가 없이 작동한다. -아무 짓도 하지 않는다.
생각보다 많은 사람들이 잘 모르고 있는 부분이다.

위 코드는 다음처럼만 써주어도 된다.

free(p);
p = NULL;

해제 후에 0을 넣어주는 것도 많은 경우에는 필요가 없지만(지역 변수의 포인터를 해제하는 경우) 이후에 언제라도 다시 해제를 시도할 가능성이 있을 때는 써주는 것이 의미가 있다.

cppcheck 같은 정적분석 도구를 사용할 시에는 맨 위의 널 포인터를 체크하는 코드를 보면 성능이 떨어진다면서 저런 짓을 할 필요 없다고 경고해주기도 한다.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/199 관련글 쓰기

  1. Favicon of http://eslife.tistory.com BlogIcon esstory at 2011/05/30 12:58 [edit/del]

    이게 너무 오랫동안 습관이 ㅎㅎ
    처음에 null 로 초기화 안한 변수를 delete 해서 많이들 죽여 봐서.. 저렇게 하는 게 당연하다고 생각했는데 결국 '괜한 짓' 이군요 ㅎ

    Reply
  2. Favicon of http://sunyzero.tistory.com BlogIcon stevenkim at 2011/07/21 00:42 [edit/del]

    정말 사람의 습관이라는 것이 무서운게, 학생때 널체크로 배웠더니...

    나중에 초급시절을 벗어나 free(NULL)이 문제가 없는 것을 알았어도, 어느 순간 보면 기계적으로 널체크 코딩을 하고 있더군요.

    Reply

submit
이전에 윈도우 드라이버를 공부하면서 정리해놨던 내용들이다.
여기에 있는 내용들은 전부 이 책에서 배운 내용들이다.
아마 지금까지 나온 윈도우 드라이버 책 중에 가장 좋은 책일 것이며, 앞으로도 이런 책은 안나올 것 같다.

만일 드라이버를 처음 접해서 아래 내용이 전혀 이해가 안되더라도 그냥 외워서 따라할 수 있도록 정리해놨다. 경험이 쌓여가면서 하나씩 이해가 될 것이다.

  • IRP를 완료시킬 때는 STATUS_PENDING 이라는 상태 코드를 써서는 안된다.
    물론 디스패치 루틴에서 STATUS_PENDING을 리턴할 수는 있지만, IoStatus.Status에 STATUS_PENDING을 할당한 뒤 I/O를 완료시키면 안된다는 뜻이다.
  • IoCompleteRequest를 호출 하기 전에는 IRP에 대해 지정한 취소 루틴을 제거해야 한다.
    Driver Verifier는 이런 경우를 적절히 찾아내서 버그체크를 띄워준다. 단, Irp가 큐잉된다면(StartPacket을 한다던지 Cancel-Safe Queue에 들어간다던지) 디큐될 때에 자동으로 취소 루틴이 제거된다.
  • Irp를 종료할 때, IoStatus.Status와 리턴 값은 일관성이 있어야 한다.
    즉 IoStatus.Status는 성공코드를 넣고 실패코드로 리턴하는 경우가 있으면 안된다.
    Driver Verifier가 이 오류 또한 적절히 발견해줄 것이다.
  • 디스패치 루틴에서 STATUS_PENDING을 리턴하기 전에는 Irp를 Pending으로 Marking해준다. 반대로 Irp를 Peding으로 마킹해 주지 않으면 STATUS_PENDING으로 리턴하면 안된다.
    둘은 항상 짝이 맞춰져서 다녀야 한다.
  • StartPacket을 호출하고 나면 더이상 IRP를 건드리지 않는다. 함수가 리턴되자마자 Irp는 이미 완료되었을 수 있다.
  • STATUS_MORE_PROCESSING_REQUIRED를 리턴하지 않는 모든 완료루틴에 대해서 다음의 코드를 넣는다.
    if(Irp->PendingReturned)
    {
        IoMarkIrpPending(Irp);
    }

    완료루틴을 설정하지 않았을 경우에는 시스템이 Irp를 완료시켜가면서 상위 스택으로 Pending 비트를 복사해주지만,
    완료 루틴이 설정되어 있으면 시스템이 이 작업을 해주지 않기 때문에 해당 완료 루틴에서 직접 PendingReturned 값을 체크하여 Irp를 Pending으로 마크해주어야 하기 때문이다. -어떤 경우라도 시스템은 PendingReturned 변수만큼은 항상 잘 셋팅해준다.
    만약 잘 이해되지 않는다면 그냥 외우기만 해도 좋다. 이는 언제나 참이다. 단, 완료 루틴에서만이다. 다른 루틴(디스패치 루틴이라던지)과 착각하면 안된다.
  • Lower Driver로 Irp를 건네주는 과정에서 완료루틴을 설정한다면 반드시 IoCopyCurrentIrpStackLocationToNext를 호출해야 한다.(IoSkipCurrentIrpStackLocation은 안된다.)
  • Next Driver에 Irp를 전달한 이후에는 더 이상 Irp는 자신의 소유가 아니며 건드리지 말아야 한다. 그 Irp는 다른 드라이버나 쓰레드에서 free되거나 완료되었을 수 있다. 하지만 만일 드라이버가 스택 아래로 Irp를 건네준 이후에 그 Irp에 접근하고 싶다면 반드시 완료 루틴을 설정해야만 한다.
    Io 매니저가 해당 완료루틴을 호출해 줄 때 완료루틴 내에서 잠시 다시 Irp는 내 드라이버의 소유가 되고, Irp에 액세스 할 수 있다.
    만약 Next Driver가 Irp를 완료 시킨 후에, 내 드라이버의 디스패치 루틴이 그 Irp에 접근해야만 한다면 완료루틴은 반드시 STATUS_MORE_PROCESSING_REQUIRED를 리턴해야만 한다. 이는 Irp의 소유를 디스패치 루틴에게로 건네준다. IO 매니저는 Irp의 처리를 중지하고 디스패치 루틴에게 궁극적으로 해당 Irp의 Completion을 맡긴다.
    디스패치 루틴은 이후에 IoCompleteRequest를 호출해서 Irp를 완료시킬 수도 있고 처리를 더 하기 위해 Pending으로 마킹할 수 있다.
  • 드라이버는 다음과 같은 경우 반드시 STATUS_PENDING을 리턴해야만 한다.
    • Irp가 완료되기 전에 디스패치 루틴이 리턴하는 경우
    • 해당 Irp가 다른 쓰레드에서 완료되는 경우
  • Arbitrary 쓰레드에서는 비동기적 Irp만을 생성할 수 있다. NonArbitrary 쓰레드에서는 동기 Irp도 생성할 수 있다.
  • IRQL이 Dispatch Level 이상인 경우에는 쓰레드를 대기 시켜서는 안된다. 즉, KeWaitFor- 같은 함수 들을 사용할 수 없다는 이야기이다.
    단, 쓰레드를 재우지 않고 단순히 오브젝트가 시그널 되었는지 Peek 해보는 것은 상관없다. 대기 함수들에 타임아웃값을 0으로 넘겨주면 Peek만 시도할 수 있는데, 유저모드에서는 WaitFor함수 패밀리에 dwMilliseconds를 0으로 건네주면 된다.
    커널 모드(KeWaitFor 함수 패밀리)에서는 유저모드처럼 그냥 0을 넣어버려서는 안된다. KeWaitFor 함수에 NULL 포인터를 전달하게 되면 Peek하겠다는 것이 아니라 무제한 기다리겠다는 의미이다. 반드시 LARGE_INTEGER 값을 0으로 셋팅해서 함수에 넘겨주어야만 한다.
    쓰레드를 재우면 안되는 이유는, IRQL이 Dispatch Level 이상이 되면 다시 재스케줄링 될 수 없기 때문이다.
    이는 비단 WaitFor 함수 패밀리뿐 아니라, 쓰레드를 대기 상태로 만드는 모든 함수를 사용 할 수 없다는 의미임을 알아야 한다.
  • IRQL이 디스패치 레벨 이상이 된 경우에는 특정 데이터를 참조하기 위해 페이지 폴트를 일으켜서는 안된다.
    페이지 폴트는 소프트웨어 예외 중 하나인데 사실 이는 디스패치 레벨 상태에서는 페이지 폴트 뿐만이 아니라 모든 경우의 Exception이 발생해서는 안된다는 의미이다. 이는 위에서 설명한 대기 함수를 사용할 수 없는 이유와 같다.
  • #pragma alloc_text(PAGE, Function Name)
    위와 같은 디렉티브를 선언했다면 그 함수의 시작부에 PAGED_CODE 매크로를 사용한다.
    이는 반드시 한 쌍으로 있어야 한다.(둘다 있거나 둘 다 없어야 한다.)
    Pagable에 대한 의미는 따로 설명하지 않는다.
    PAGED_CODE에 대한 더 자세한 내용은 여기에 써두었다.
  • 커널 스택은 x86에서 12K만큼 할당된다. amd64에서는 24K이다.
    단지 자신의 드라이버에서만 커널 스택을 사용할 것이라 착각해서는 안된다. 자신의 드라이버의 상위에 다른 필터 드라이버들이 있을 경우, 해당 필터드라이버가 사용하고 남은 커널 스택만큼을 받아서 사용하게 된다.
    따라서 평소엔 잘 돌아가던 드라이버가 백신같은 필터 드라이버가 설치된 환경에서는 블루스크린이 발생하기도 한다.
    이것은 모든 드라이버에서 최대한 스택을 아껴써야 함을 의미한다.
    유저모드에서 하듯이 다음 줄처럼 코딩 해서는 안되며
    WCHAR sz[MAX_PATH]; // 지역변수 하나가 커널 스택의 약 5%를 써버렸다.

    이는 좀 귀찮더라도 아래와 같은 방법으로 수정되어야 한다.
    PWCHAR sz = ExAllocatePoolWithTag(PagedPool, sizeof(WCHAR) * MAX_PATH, POOL_TAG);
    if(sz == NULL)
    {
        // Process the error.
    }
     
    __try
    {
        // Do something
    }
    __finally
    {
        if(sz)
        {
            ExFreePoolWithTag(sz, POOL_TAG);
        }
    }

  • 커널 스택은 pagedpool에 할당됨을 알고 있어야 한다. -함수가 대기 상태로 되면 시스템에 의해서 언제라도 Page out 될 수 있다.
    Event 객체는 NonPagedPool에 할당되어야 하는데, 스택에 할당했을 경우에는 KeWaitFor 함수의 WaitMode인자로 KernelMode를 전달함으로써 이 스택 메모리가 Page out 되지 않도록 할 수 있다.
  • DriverEntry가 아닌 곳에서 디바이스를 생성한다면, 초기화가 끝난 이후에 DO_DEVICE_INITIALIZING 플래그를 지워줘야 한다. -이는 AddDevice 루틴도 예외가 아니다.
    deviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
    이것을 지워주지 않는다면, CreateFile같은 Api로 장치를 열 수 없다.
    만약 DriverEntry에서 만드는 경우에는 리턴되자마자 IoManager가 알아서 플래그를 지워주므로 생략해도 상관없다.
  • 캔슬 큐에 집어 넣고 pend 상태로 처리한 Irp는 나중에 완료하기 직전에 꼭 Cancel Queue에서 제거해주어야 한다.
    이 때 Cancel Queue에서 빼면서 리턴값을 꼭 검사해서 실제로 Queue에 존재했었고 제거된 경우에만 이 Irp를 완료시켜야 한다.

    // pIrpContext는 Insert할 때 넣어주었던 그 변수를 사용해야 한다.
    PIRP pPendedIrp = IoCsqRemoveIrp(&g_pDevExt->cancelIo.CancelSafeQueue, pIrpContext);
    if(pPendedIrp)
    {
        DriverLog(SHOW, "pIrp = 0x%08X\n", pIrp);
        pIrp->IoStatus.Status = status;
        pIrp->IoStatus.Information = information;
        IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    }
    이렇게 해야 여러 요청이 동시에 들어왔을 때의 미묘한 동기화 문제를 피할 수 있다.(그렇지 않으면 같은 Irp를 2번 이상 완료시킬 수 있다)

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/196 관련글 쓰기

  1. 아저씨 at 2011/05/25 00:14 [edit/del]

    좋은 글 감사합니다.

    Reply

submit

Duff's Device

2011/04/07 07:47 | Programming
Duff's Device는 Tom Duff라는 사람이 1983년도에 생각해낸 switch 문장을 사용한 loop unwinding 얍삽이이다.
위키피디아에는 다음 코드가 인용되어져 있다.
send(to, from, count)
register short *to, *from;
register count;
{
    register n = (count + 7) / 8;
    switch(count % 8) {
    case 0: do{ *to = *from++;
    case 7:     *to = *from++;
    case 6:     *to = *from++;
    case 5:     *to = *from++;
    case 4:     *to = *from++;
    case 3:     *to = *from++;
    case 2:     *to = *from++;
    case 1:     *to = *from++;
            } while(--n > 0);
    }
}
얼핏보면 switch 문장안에 do while 루프가 들어있는 것이 컴파일도 되지 않을 것처럼 보이지만 신기하게도 어느 컴파일러에서나 잘 컴파일된다.

이 코드가 컴파일 가능한 이유는 C언어의 switch 문법이
switch ( expression )
{
case constant-expression :
    statement-list
    break ;

case constant-expression :
    statement-list
    break ;

...

default :
    statement-list
    break ;
}
이 아니라 단지

switch ( expression )
    statement
이기 때문이다.

switch는 if문이나 for문 처럼 바로 뒤에 단일 문장이 올 수도 있고 블록으로된 문장들이 오는 것이 가능하다. 꼭 case 레이블이 바로 뒤따르지 않아도 된다는 뜻이다.
즉, 아래와 같은 코드도 적법하다.
switch(nn % 4)
{
for(; nn > 0; nn -= 4)
    {
case 0:    *destp++ = *srcp++;
case 3:    *destp++ = *srcp++;
case 2:    *destp++ = *srcp++;
case 1:    *destp++ = *srcp++;
    }
}

다시 처음에 나왔던 Duff's Device의 switch문을 보면, 그 코드는 마치 아래와 같이 동작할 것이다. 루프의 중간부분 부터 끼어들어갈 수 있다는 것이 중요하다.
switch(count & 7)
{
case 0 : goto lbl0;
case 1 : goto lbl1;
case 2 : goto lbl2;
case 3 : goto lbl3;
case 4 : goto lbl4;
case 5 : goto lbl5;
case 6 : goto lbl6;
case 7 : goto lbl7;
}

lbl0:
    while(count > 0)
    {
        handle_one();
        count--;
lbl1:
        handle_one();
        count--;
lbl2:
        handle_one();
        count--;
lbl3:
        handle_one();
        count--;
lbl4:
        handle_one();
        count--;
lbl5:
        handle_one();
        count--;
lbl6:
        handle_one();
        count--;
lbl7:
        handle_one();
        count--;
    }

이런 코드가 보통의 루프보다 빠른 이유는 불필요한 비교문을 줄여주기 때문이다.
do {                
    *a = *b++;
} while (--count > 0);
위 코드에서 몇번이나 count가 0과 비교 되겠는가. b의 포인터 증가 연산은 몇번 일어나겠는가. 쉽다. count 번이다.
Duff's device에서는 루프를 손으로 풀어서 씀으로써 count / 8 만큼으로 테스트 횟수를 줄일 수 있게 된다. (포인터 증가 연산은 줄일수 없고 비교연산만 줄일 수 있다)
꼭 8로 나눌 필요는 없으며, 이는 적절히 조정하면 된다. Duff는 캐시 사이즈를 고려해서 8 정도로 결정한 것 같다. 루프를 많이 풀어 쓸수록 더 많은 코드가 생성이되고 함수 크기가 커지게 될 것이다.

그런데 루프를 풀어서 쓰기 위해서 왜 꼭 switch 문을 사용한 것일까.
for(int i = 0; i < 100; i =+ 8)
{
    *a = *b++;
    *a = *b++;
    *a = *b++;
    *a = *b++;
    *a = *b++;
    *a = *b++;
    *a = *b++;
    *a = *b++;
}
위처럼 루프를 풀어내는 것도 물론 가능하다. 그런데 숫자가 딱 나누어 떨어지지 않으면(위 예에서는 100 / 8) 나머지 처리를 밑에서 한번 더 해주어야 한다. Duff의 코드에서는 n을 (count + 7) / 8 로 정하고 case 구문의 숫자 위치를 적절히 배열하여 나머지 처리를 안해도 되도록 한 것이 중요한 포인트이다. 이렇게 하면 한방에 깔끔하게 처리할 수 있고 따라서 텍스트의 크기도 약간 더 작아진다. 사실 이 포인트가 C언어의 문법으로 범용적인 loop unrolling을 구현가능하게 하는 것이며 Duff's Device라고 불리는 아이디어이다.

그런데 요즘 우리가 쓰는 거의 대부분의 컴파일러들은 최적화를 위해 loop unwinding을 지원하고 있으며, 손으로 루프를 풀어서 쓴 것보다 더 효율적으로 코드를 생성해준다. 오히려 그런 손으로 풀어쓴 루프가 컴파일러의 최적화를 방해해서 더 느린 코드가 생성될 수도 있다. 보기에 안좋은 것은 말할 필요도 없다.
이것은 이미 어딘가에 사용되고 있는 Duff's Device와 같은 코드들을 보통의 코드로 변경함으로서 오히려 더 빠른 성능을 얻을 수도 있다는 뜻이고 따라서 이런 루프 풀기를 적용하려고 할 때에는 정말 도움이 될지 미리 벤치마크를 잘 해봐야 한다. 만약 벤치마크 해보는 것이 귀찮다면 루프 풀기가 아닌 보통의 코드를 사용하는 쪽으로 베팅하는 것이 심신에도 좋고 동료들과의 관계에도 이로울 것이라 확신한다.
저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/190 관련글 쓰기

submit

_countof 매크로

2011/03/15 06:48 | Programming
_countof 매크로는 배열의 원소 개수를 돌려주는 서비스 매크로이다. 비주얼 스튜디오 2005 부터인가 제공되었던 것 같다.
StringCchCopystrcpy_s, wcscpy_s 같은 함수들을 사용할 때 편리하게 사용할 수 있다.

_countof 매크로는 다음과 같이 생겼다. C++ 문법이 괴상하다보니 syntax highlighter가 약간의 문제가 있어서 그냥 박스에 붙여넣었다.
#if !defined(_countof)
#if !defined(__cplusplus)
#define _countof(_Array) (sizeof(_Array) / sizeof(_Array[0]))
#else
extern "C++"
{
template <typename T, size_t N>
char (*__countof_helper(UNALIGNED T (&_Array)[N]))[N];
#define _countof(_Array) (sizeof(*__countof_helper(_Array)) + 0)
}
#endif
#endif

C언어를 사용할 때와 C++를 사용할 때의 구현이 다르게 되어있다.
C언어에서 사용되는 방식은 대부분의 사람들이 잘 알고 있는 방식일 것이며, C++에서는 템플릿을 이용해서 배열의 개수를 구하고 있다.

여기서 궁금해해야 할 점은
  1. 템플릿을 통해 어떻게 배열의 원소의 개수를 구할 수 있는가.
  2. 도대체 왜 C와 C++을 전처리기를 사용해서까지 따로 구현했을까. 그냥 C 구현 하나만 쓰지.

1번 질문의 답은 아래 블로그에 잘 설명이 되어있다.
C++ 템플릿으로 배열의 원소 개수를 구하는 방법

2번 질문의 답은 C 구현 방식에 약간의 문제가 있기 때문이다.
배열이 아니라 포인터일 경우에 C 방식은 제대로 개수를 구해주지 못한다.
그럼 템플릿 방식은 제대로 구해주냐 하면 물론 그럴수는 없다. 그래도 컴파일 에러를 내주기 때문에 좀 더 낫다. C 방식은 컴파일이 잘 되어버리며 잘못된 결과를 돌려준다.
아래 코드를 한번 보자. 설명을 쉽게 하기 위해 매크로 이름을 바꿨다.
#define _countof_c(_Array) (sizeof(_Array) / sizeof(_Array[0]))

template <typename T, size_t N>
char (*__countof_helper(T (&_Array)[N]))[N];
#define _countof_cpp(_Array) (sizeof(*__countof_helper(_Array)) + 0)

void c_version(int* p) // int ar[] 과 같은 형식으로 넘어올 때도 마찬가지이다.
{
    printf("%d\n", _countof_c(p));
}

void cpp_version(int* p)
{
    printf("%d\n", _countof_cpp(p));
}

int _tmain(int argc, _TCHAR* argv[])
{
    int a[100];
    printf("%d\n", _countof_c(a)); // 100 올바른 결과
    printf("%d\n", _countof_cpp(a)); // 100 올바른 결과

    c_version(a); // 1 틀린 결과를 내어줘버렸다. 다행히 경고(C6384)는 발생한다.
    cpp_version(a); // cpp_version함수는 컴파일이 안된다.

    return 0;
}

여기서 배워야 할 점은,
  • C 방식 매크로를 쓸 때 언제 문제가 되는지를 알고 있어야 한다.
  • C++를 사용함에도 불구하고 해당 매크로를 C방식으로 직접 만들어서 사용하는 경우를 보았다. 그냥 _countof를 사용한다.
  • 마지막으로, 컴파일러 경고가 발생하면 무시하지 않는다.
또한, 사실 이는 _countof 의 MSDN 페이지에도 잘 나와있는 내용이다. MSDN을 읽을 때는 꼭 개요부터 See Also까지 다 읽는 습관을 들여야 한다. 특히 Remarks 섹션은 유의해야 할 사항들이나 사용자 쪽에서 궁금해 할만한 내부 구현 방식들을 다루어주므로 항상 눈을 부릅뜨고 읽어봐야한다.
저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

'Programming' 카테고리의 다른 글

윈도우 드라이버를 만들 때 알아야 할 기초적인 내용들  (1) 2011/05/23
Duff's Device  (0) 2011/04/07
_countof 매크로  (0) 2011/03/15
FIELD_OFFSET 매크로  (0) 2011/03/01
PAGED_CODE 매크로  (0) 2011/02/27
디렉터리의 읽기 전용 속성  (4) 2011/02/20

http://www.benjaminlog.com/trackback/187 관련글 쓰기

submit

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을 사용하는 것을 더 선호한다.
양쪽의 구현은 똑같다.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

'Programming' 카테고리의 다른 글

Duff's Device  (0) 2011/04/07
_countof 매크로  (0) 2011/03/15
FIELD_OFFSET 매크로  (0) 2011/03/01
PAGED_CODE 매크로  (0) 2011/02/27
디렉터리의 읽기 전용 속성  (4) 2011/02/20
알쏭달쏭한 typedef  (7) 2011/01/04

http://www.benjaminlog.com/trackback/182 관련글 쓰기

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에 할당하는게 편한 것 아닌가?"
하는 생각이 드는게 정상일 것이다. 물론 그렇게 하면 프로그래머는 좀 더 편해지겠지만, 오랫동안 사용되지 않고 있는 코드와 데이터들이 계속 물리 메모리를 점유하고 있기 때문에 시스템의 성능을 떨어뜨릴 수 있다. 특히 넷북같은 꼬물 컴퓨터에서는 더 많은 영향을 끼칠 것이다.
저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

'Programming' 카테고리의 다른 글

_countof 매크로  (0) 2011/03/15
FIELD_OFFSET 매크로  (0) 2011/03/01
PAGED_CODE 매크로  (0) 2011/02/27
디렉터리의 읽기 전용 속성  (4) 2011/02/20
알쏭달쏭한 typedef  (7) 2011/01/04
하위 디렉터리의 파일이 변경 되었는지 감지하는 법  (4) 2010/12/20

http://www.benjaminlog.com/trackback/181 관련글 쓰기

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

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

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

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

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

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

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

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

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/180 관련글 쓰기

  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*);

징그럽지만 어쩌겠는가.


크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/112 관련글 쓰기

  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

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가 실패할 것이다) 구현 하는 쪽이 더 나은 사용자 경험을 제공해 줄 수 있는 파일 시스템 드라이버가 될 것이다.
저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/169 관련글 쓰기

  1. 재호님 팬 at 2010/12/29 12:11 [edit/del]

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

    Reply
  2. Ji at 2011/02/10 17:48 [edit/del]

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

    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용 데이터를 집어 넣었다는 것이 명백히 드러난다.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/167 관련글 쓰기

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 함수들을 통해서 드라이버를 설치하고 구동시킬 수 있다.
응용 프로그램이 종료 될 때는 물론 드라이버를 잘 멈추고 제거하고 파일을 지워버리는 등의 작업 또한 해주어야 할 것이다.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/165 관련글 쓰기

  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의 분리와 같은 변화를 구현하기 위해 오랜 시간을 쏟았다. 아직도 대부분의 사람들은 이것을 완벽하게 이해하고 있지는 못하다.
이 때부터 윈도우 비스타는 꼬물이라는 불명예를 안았다. 물론 호환성을 깬 것은 비스타의 잘못이 맞다.

더 읽을 거리




저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/160 관련글 쓰기

  1. 은새 at 2010/11/16 19:38 [edit/del]

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

    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들을 모두 완료 시켜주어야 한다.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/158 관련글 쓰기

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

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

    Reply

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

재귀를 이해하기 위해서 좋은 질문이 여기에 있다.
Enjoy recursion.
저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/155 관련글 쓰기

  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를 완료시킨다.

이렇게 드라이버가 유저모드의 구현을 위한 인터페이스만을 제공함으로서 파일 시스템 로직 구현을 유저모드로 옮길 수 있으며, 유저모드 개발시의 여러 장점들을 가져올 수 있다.
그림에서 보이는 것처럼 두번씩 왔다 갔다 해야하는 것이 성능의 저하를 가져올 수 있고, 유저모드로 구현이 넘어감에 따라 커널 모드 라이브러리 루틴들을 마음껏 쓸 수 없다는 것은 단점이라 할 수 있겠다.
저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/153 관련글 쓰기

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


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

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

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/143 관련글 쓰기

  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에 대한 많은 재밌는 이야기들이 있으니 관심이 있다면 한번씩 읽어보길 추천한다.

관련글


크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/133 관련글 쓰기

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

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

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

관련글
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/131 관련글 쓰기

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;
}

크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/113 관련글 쓰기

  1. [C/C++]유용한 #pragma directive
    // 사진찍는 프로그래머 2010/01/14 23:26 x
  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 또한 하나의 파일 객체과 일대일 대응이 되어야 하며, 그러므로 정규화 되어야 한다.
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/107 관련글 쓰기

  1. 다얀 at 2010/01/02 21:03 [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를 공부해보지는 않았지만 훌륭한 사람들이 설계한 언어이고, 또 믿을만한 회사에서 지원하는 오픈소스인만큼 곧 우리들 곁에도 가까이 다가오게 될 것이라는 생각을 한다.

내년 이 맘때는 순위가 어떻게 변해 있을지 정말 궁금하다.
크리에이티브 커먼즈 라이선스
Creative Commons License

http://www.benjaminlog.com/trackback/110 관련글 쓰기

  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