Media Log

[Windows]에 해당되는 글 24

  1. h Windows Runtime 이야기 (1) 2014.12.05
  2. h 개발자를 위한 윈도우즈용 소프트웨어 총 모음. 2013.12.22
  3. h 윈도우즈 PE 파일의 구조 2013.03.24
  4. h 찰스 펫졸드는 죽지 않았다. Programming Windows 6판 2012.04.24
  5. h 쉘의 파일 오퍼레이션을 잡아챌 수 있는 ICopyHook 인터페이스 2012.04.06
  6. h SetFilePointer 보다는 SetFilePointerEx를 사용해야 한다 (1) 2012.01.16
  7. h 위대한 해커들의 말말말 - 제프리 리처 2012.01.16
  8. h GetLastError 함수 사용의 흔한 실수 2012.01.13
  9. h NTFS에서 Sparse 파일을 만들기 (3) 2012.01.03
  10. h 16TB 크기의 파일을 만들어내려면 얼마나 오래 걸릴까? 2012.01.01
  11. h 레지스트리의 volatile 옵션 2011.09.27
  12. h 탭기능을 지원하는 무료 윈도탐색기 프로그램, Explorer++ (1) 2011.09.22
  13. h 알아두면 유용한 MoveFileEx 함수의 펜딩 옵션 2011.07.17
  14. h 윈도우 드라이버를 만들 때 알아야 할 기초적인 내용들 (2) 2011.05.23
  15. h Windows 시스템 프로그래밍 4판 -Johnson M. Hart 2011.03.06
  16. h PAGED_CODE 매크로 (5) 2011.02.27
  17. h 디렉터리의 읽기 전용 속성 (4) 2011.02.20
  18. h 프로세스 모니터 사용법 (6) 2011.01.14
  19. h 파일 조작 테스트를 위한 훌륭한 도구 소개 (4) 2010.12.27
  20. h 하위 디렉터리의 파일이 변경 되었는지 감지하는 법 (8) 2010.12.20
  21. h WinApi의 reserved 인자는 뭐하는 용도일까 2010.12.09
  22. h Cancel-Safe Queue를 이용하여 디바이스 드라이버에서 I/O를 취소하기 (2) 2010.10.25
  23. h Windows Internals 5th 한글판 출간 (2) 2010.07.13
  24. h Windows 시스템 실행파일의 구조와 원리 -이호동 저 2010.02.13

꽤 오랫동안 윈도 프로그래밍에서 떨어져 살다가 윈도폰은 어떨지 궁금해서 문득 윈도폰 하나를 중고로 구입해본 적이 있었다. 그게 NT 커널이 올라가있는 윈도폰 8.0 이었었고, 세컨드 폰으로 장난감 다루듯이 사용해보다가 시간이 좀 흐르고 BUILD 2014 행사에서 MS가 윈도폰 8.1을 발표했다.


그 때쯤 처음으로 윈도폰과 Windows Runtime 에 관심을 가지고 진지하게 책을 읽어보기 시작했으며, 역시 처음으로 Build 2014 에 나오는 대부분의 비디오를 다운받아서 봤는데(기존에는 키노트만 봤었다) Windows Rumtime 기반으로 한번 앱을 만들어보는 것도 좋겠다는 생각이 들었다.

마침 회사에서는 윈도폰을 위한 카카오톡을 새로 만들기로 결정을 했기 때문에, 나도 꼽사리를 껴서 같이 만들어 보기로 했다.


오래 전에는 윈도 개발에 익숙했기도 하고 이것저것 아는 것들도 조금 있었기 때문에 처음에는 재밌고 그리 오래 걸리지 않을 작업이라고 쉽게 생각했었는데 막상 Windows Runtime 과 부딪혀보고는 곧 그렇게 쉬운 일이 아니라는 것을 깨달았다. 기존에 알고 있었던 네이티브 지식들과 Win32 Api 지식들은 거의 도움이 되지 않았고, Windows Rumtime 의 새로운 프로세스, 패키지 데이터, 네비게이션 모델 등을 다 새로 공부해야만 했다. 닷넷은 책으로만 보고 작은 프로그램들 정도만 작성해봤었는데, 처음으로 정교한 프로젝트에 맞딱드려보니 UI 와 데이터를 유지보수하기 쉽게 바인딩하는게 생각처럼 쉽지 않아 고생을 많이 했었고 또 Task 기반 프로그래밍 역시 책으로 이해한 것과는 달리 실제로 적용해보면서 꽤 많이 해맸었던 것 같다. 대부분의 라이브러리들이 윈도폰용의 Windows Runtime 을 지원하지 않거나 지원 준비중이 었기 때문에 스트레스를 받기도 했다.


무엇보다 나를 괴롭혔던 것은 Windows Runtime 이라는 새로운 환경 자체였는데, 아무리 책을 보고, MSDN 을 보고 잘 이해했다고 생각을 하고 코드를 짜도 전혀 기대하지 않은 동작을 해서 미궁에 빠졌을 때가 여러번 있었다. 새로운 환경에 부딪히면서 일을 할 때 이렇게 미궁에 빠지게 되는 때의 원인은 다음과 같은 경우들이 있는 것 같다.


1. 프로그래머 자신은 잘 이해했다고 생각했지만 실제로는 잘 이해하지 못하고 있었다.

2. 문서에 정보가 애매하거나 빈약하게 써있어서 이해하기가 쉽지 않았거나 혹은 문서에 정보가 적혀있지 않았다.

3. 기반 프레임워크에 버그가 있었다.


나는 대부분의 문제는 1번에서 발생한다고 생각하는(사실은 거의 확신) 편인데, 이번 프로젝트에서는 2번과 3번의 문제가 더 많았던 것 같다. 정보를 얻을 수 있는 곳이 없어서 정말 많은 실험과 해킹을 하면서 답을 알아낸 경우가 많이 있었고, 윈도폰 자체의 버그 때문에 Windows Phone 8.1 Update 1이 나오고 나서 자연스레 해결된 문제들도 있었다. 어떤 문제는 아직도 해결되지 않아 Windows 10 에서나 해결될 수 있을 거라는 답변도 받았다.

예전에 Win32 Api 들을 다룰 때는 MSDN 이 정말 좋은 정보들이 많은 문서시스템이라는 생각을 했었는데, 닷넷 쪽과 Windows Runtime Api 문서들을 보면서 생각이 좀 바뀌었다. 파이썬이나 루비 온 레일즈와 같은 오픈소스 언어/프레임워크 들의 문서화가 훨씬 더 잘되어 있는 것 같다. Windows Runtime 용 샘플 코드들 또한 매우 조잡한 수준이었다.


그럼에도 불구하고 (수많은 삽질로 인해 시스템에 익숙함을 갖게된 상태에서 바라보면) 기존의 Win32 프로그래밍 환경과 비교했을 때, 프로그래밍이 많이 쉬워진 것은 같다. 닷넷의 풍부한 모든 기능들을 사용할 수 있고, 레이아웃을 다루는 일과 스케일링 처리도 훨씬 쉬워졌다. 클라이언트 프로그램을 작성할 때는 UI를 멈추지 않게 작성하는 것이 아주 중요한데, C#의 Task 기반 프로그래밍과 Windows Runtime의 Api 들은 이를 쉽게 가능하게 만들어주는 것도 좋은 점 중의 하나였다.


이런 저런 고생들을 하며 지난 달에 Windows Rumtime 으로 완전히 새롭게 태어난 카카오톡을 오픈하고 애프터 서비스를 좀 하다가 얼마 전 다시 서버팀으로 돌아왔다. 이제 윈도우즈로 개발 할 일이 또 거의 없어져서 집에서 사용하던 랩탑은 Windows 10 으로 새로 설치해버리고 새 운영체제를 조금씩 알아가고 있는 중이다. API 가 어떻게 바뀌었는지는 아직 잘 모르겠지만 사용자 입장에서 봤을 때, 모던앱들이 창크기가 변경 가능하다는 점은 정말 편리한 것 같고, 이제서야 뭔가를 모던앱을 만들어보는 것도 좋겠다 하는 생각이 처음으로 든다.


두줄 요약:

1. Windows Runtime 은 조금 더 개선되고 다듬어질 필요가 있다.

2. 만약 모던앱 혹은 윈도폰 앱 개발에 흥미를 느끼고 있다면 Windows 10 Developer Preview 정도는 나온 뒤에 공부를 시작하는 것을 추천한다. 안 그러면 고생만 한다.

저작자 표시 비영리 동일 조건 변경 허락
신고

'Programming' 카테고리의 다른 글

TypeScript, 타입에 대한 고찰  (0) 2015.05.05
시니어 프로그래머  (2) 2015.02.03
Windows Runtime 이야기  (1) 2014.12.05
눈물나는 그래프  (2) 2014.08.25
방준영의 블로그  (0) 2014.06.06
프로그래머의 5가지 성향  (0) 2014.03.15
  1. 송병학 at 2014.12.05 08:57 신고 [edit/del]

    서버 개발하시면서 윈도우폰까지 하시고 부지런하시네요. 좋은 글 감사합니다 ^^

    Reply

submit

Scott Hanselman 이라는 사람이 오래전 부터 그의 블로그에 좋은 윈도용 소프트웨어들을 소개해주어 유용하게 읽곤 했었는데, 오늘은 올린 글은 입이 떡 벌어질 만큼 특별히 잘 정리되어 있어서 소개를 하고 싶다.

나는 윈도를 떠나 산지 시간이 좀 흘러서 새로나온 (끝내줘보이는) 도구도 꽤나 보였고, 약간의 향수(?) 마저 느꼈다.

윈도우즈 프로그래머 혹은 윈도를 사용하는 프로그래머라면 꼭 읽어보고, 모르는 것들이 있었다면 하나씩 설치해서 연습해보면 세상 사는 게 좀 더 편해질 것이라 확신한다.

http://www.hanselman.com/blog/ScottHanselmans2014UltimateDeveloperAndPowerUsersToolListForWindows.aspx

저작자 표시 비영리 동일 조건 변경 허락
신고

submit


Portable Executable Structure이미지 출처 https://code.google.com/p/corkami/wiki/PE101

엄청나게 잘 정리된 그림을 발견했다. 아름답지 아니한가.

저작자 표시 비영리 동일 조건 변경 허락
신고

submit

한 때 윈도우 프로그래밍의 교과서로 불리우던 찰스 펫졸드의 Programming Windows 가 6판이 되어 돌아온다. 추가되는 내용은 윈도8의 메트로 앱개발.


아직 정식 책이 나오려면 많이 기다려야 하기 때문에 매트로앱에 대한 내용만을 담아서 전자책으로 10$에 파는 이벤트를 진행 한다고 한다. 5월 17일 ~ 5월 31일 사이에만 살 수 있다.


더 자세한 내용은 그의 블로그에서.

http://www.charlespetzold.com/blog/2012/04/2-4-6-8-10.html

저작자 표시 비영리 동일 조건 변경 허락
신고

submit

ICopyHook은 윈도에서 제공하는 COM 인터페이스이다. 이 인터페이스를 구현해서 시스템에 등록시키면 쉘을 통한 파일 오퍼레이션이 발생할 때 내가 설치해 놓은 코드가 실행되도록 할 수 있다.

구현해야 할 함수는 이런 모양으로 생겼다.

UINT CopyCallback(
  [in, optional] HWND hwnd,
  UINT wFunc,
  UINT wFlags,
  [in] PCTSTR pszSrcFile,
  DWORD dwSrcAttribs,
  [in, optional] LPCTSTR pszDestFile,
  DWORD dwDestAttribs
);

파일 오퍼레이션을 잡아챌 수 있다고 해서 이 훅을 사용해 파일 삭제 등을 감시하는 모니터 용도로 사용하려는 아이디어는 좋지 못하다. 이 기능은 모든 파일 시스템 오퍼레이션에 훅을 제공하는 것이 아니라 폴더 삭제와 같은 특정한 몇몇 동작에 대해서만 동작하기 때문이다. 어떤 파일 오퍼레이션이 발생하는가 궁금한 거라면 파일 시스템에서 제공하는 파일 변경 알림 기능을 사용하는 것이 올바른 방법이다.

그럼 이 인터페이스는 도대체 어디에 쓰라고 만든 물건이고.

윈도우 탐색기에서 폴더를 삭제하게 되면 탐색기는 폴더 안에 있는 파일들부터 모두 지우고 마지막에 폴더를 삭제한다. 이는 NTFS 같은 파일 시스템이 폴더에 파일이 있으면 삭제하지 못하도록 하고 있기 때문이다. RemoveDirectory 함수를 호출했을 때 해당 디렉터리 내에 파일이 존재한다면 파일 시스템 드라이버는 STATUS_DIRECTORY_IS_NOT_EMPTY 에러를 돌려주도록 구현해야 한다.
윈도 탐색기의 이런 동작은 네트워크 파일 시스템 위에서는 치명적이다. 로컬 파일 시스템이라면 Irp가 몇 백번쯤 왔다갔다해도 금새 끝나겠지만 레이턴시가 긴 네트워크 파일 시스템 같은 경우는 저 멀리 미국까지 백 번 천 번을 왔다 갔다 해야 할 수도 있는 것이다.

ICopyHook 을 사용하면 이런 폴더 삭제나 이동 명령들을 먼저 잡아채서 내 마음대로 원하는 동작을 수행 한 뒤 ID_NO를 리턴함으로서 쉘에게는 더 이상 오퍼레이션을 하지 않도록 할 수 있다. 예를 들어 네트워크 파일 시스템 드라이버를 만든다면 쉘에서 폴더 삭제 같은 오퍼레이션이 발생할 때 이를 잡아채서 서버로 폴더 삭제 명령을 딱 한번만 보낼 수 있는 것이다. 물론 서버는 하위에 파일들이 있더라도 폴더를 삭제할 수 있는 기능을 지원해야 한다.
다른 많은 경우들에도 이 기능을 응용할 수 있을 것이다. 상상의 나래를 잘 펼친다면.

주의 해야 할 점은 내 훅이 실행될 때 어떤 에러가 발생하면 에러메세지를 사용자에게 보여주는 것도 내 책임이 된다는 것이다. ID_NO를 리턴하면 쉘은 해당 오퍼레이션에 대해서 더 이상 신경쓰지 않는 것을 기억하라. 함수의 첫 번째 인자로 들어오는 HWND는 바로 그런 사용자 인터랙션을 위해서 존재한다.

ICopyHook을 구현하면서 코드에 버그를 같이 넣어놓는다면 사용자는 윈도 탐색기 프로세스가 자꾸 비정상 종료되는 경우를 겪을 수 있다. 이는 사용자뿐 아니라 개발자들에게도 귀신이 곡할 노릇일 수 있다. 어떤 머신에서는 잘 동작하는데 특정 컴퓨터에만 가면 내 응용 프로그램이 파일 오퍼레이션을 할 때마다 윈도 탐색기가 죽어버리니 말이다. 경험이 어느 정도 있는 개발자라면 먼저 윈도 탐색기에 ICopyHook이나 쉘 확장 플러그인이 설치되어 있는지를 먼저 살펴보겠지만, 잘 모르고 있다면 크래시 덤프를 눈으로 보면서도 원인을 모를 수 있다. FOFX_NOCOPYHOOKS 플래그를 사용하면 훅을 완전히 무시하고 오퍼레이션을 수행할 수도 있다.


저작자 표시 비영리 동일 조건 변경 허락
신고

submit
파일을 열 때 파일 포인터는 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라는 함수는 존재하지 않는다. 그렇다면 윈도에서 현재 가지고 있는 핸들의 파일 포인터의 오프셋은 어떻게 구할 수 있을까?

저작자 표시 비영리 동일 조건 변경 허락
신고
  1. win32API공부학생 at 2013.12.31 20:56 신고 [edit/del]

    SetFilePointer함수의 리턴값을 잘못사용하고 있었네요ㅠㅜ 좋은정보감사합니다!

    Reply

submit

윈도우즈를 잘 이해하기 위해서는 레지스트리와 친해져야 한다. - Jeffery RichterWindows via C/C++ 중에서

한 때 이 글을 읽고 레지스트리를 다루는 책을 도서관에서 몽땅 빌려서 읽었던 적이 있다. 어느 정도 숙달이 되어 레지스트리 에디터를 열면 빛의 속도로 트리를 탐색해 나갈 수 있었는데, 작년에 회사를 그만두고 2달여를 집에서 쉬다가 다시 새로운 회사에 들어갔을 때 나의 이 능력이 마법처럼 사라져 버렸다는 것을 깨닫게 되었다. 아쉬운 일이다.
저작자 표시 비영리 동일 조건 변경 허락
신고

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)를 정확히 호출해주기도 하는데, 이것에 대한 이야기는 다음 포스트에서 해보려고 한다.
저작자 표시 비영리 동일 조건 변경 허락
신고

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 함수를 사용하면 된다.

저작자 표시 비영리 동일 조건 변경 허락
신고
  1. Favicon of http://blog.spowner.com BlogIcon spowner at 2012.01.06 13:29 신고 [edit/del]

    유용한 정보 감사합니다

    Reply
  2. gcd at 2012.02.22 06:52 신고 [edit/del]

    계속해서 좋은 글 감사합니다. 재미있게 읽고 있습니다.
    다른 포스트까지 읽으면서 느낀 점이지만, 구조체 정렬까지 고려해서 reserved 인수를 때려박는 MS가, 어째서 GetCompressedFileSize() 인터페이스를 왜 GetFileSize()처럼 해놨을까요? -_-;
    Ex 붙은 버전도 넣어주든가(..)

    Reply

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

답은 여기에 있다.

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

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

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

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

submit
윈도를 쓰면서 불편한 점 중 하나는 일을 하다보면 어느새 10개 20개씩 떠있는 윈도 탐새끼이다. 물론 GNOME의 노틸러스나 KDE의 이상한 탐색기보다는 너무 너무 잘 만들었다고 생각하지만. 

어쨌거나 얼마전에 오픈소스 뉴스를 보다가 Explorer++ 이라는 프로젝트를 발견했다.

http://www.explorerplusplus.com/software/images/screenshots/screenshot_2.png


토탈 커맨더라는 이 분야 최고의(?) 프로그램이 있지만 Explorer++는 오픈소스이고 완전히 자유롭게 쓸 수 있다는 점에서 차별점이 있다.
몇 일간 써봤는데, 그럭저럭 맘에 들어서(맘에 안드는 점들도 많지만) 이제는 Windows + E 키를 Explorer++로 매핑시켜 놓고 잘 사용하고 있다.

좋은 점
  • 탭 기능을 지원한다. 물론 껐다켜면 탭들이 모두 그대로 복원되므로 자주 사용하는 위치들을 탭으로 몽땅 띄워놓고 사용하면 된다.
  • 여러 인스턴스가 뜨지 않도록 할 수 있다. 프로그램을 한번 더 실행시켜도 새 탭만 하나 더 생긴다. 내가 가장 바랐던 기능이다.
  • 폴더 사이즈도 표시할 수 있다. NTFS는 폴더 크기에 대한 정보를 가지고 있지 않으므로 Explorer++이 뒤에서 열심히 돌면서 사이즈를 구해낸다. 나는 이 기능은 사용하지 않는다.
  • 파일 1개 짜리 프로그램이다. 나는 인스톨러로 설치안하고 바로 사용할 수 있는 이런 프로그램들이 너무 좋다.
  • 오픈 소스이다.

나쁜 점

  • 잘 뒤진다.
  • 안 예쁘다. 토탈 커맨더보다는 조금 더 나은 것 같기도 하지만.

근데 난 작명할 때 ++ 이란 말 좀 쓰지 말았으면 좋겠다. 발음하기도 어려운데다가 정말 촌스럽지 않은가?

저작자 표시 비영리 동일 조건 변경 허락
신고
  1. 예상영 at 2013.05.17 12:37 신고 [edit/del]

    Windows + E 로 연결하는 것은 AutoHotKey 같은 것을 쓰신 건가요? 아니면 다른 프로그램 설치 없이 가능할까요?

    Reply

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와 거의 비슷하게 매핑되는 아주 강력한 함수이다. 파일 속성에 대한 모든 조작은 이 함수를 통해서 할 수 있다.
    하지만 워낙 저수준의 함수이기 때문에 사용법이 조금 어렵게 느껴질 수도 있다.
    아래 글에 해당 함수를 사용하여 이름 변경을 하는 코드에 대한 설명이 있다.
    하위 디렉터리의 파일이 변경 되었는지 감지하는 법
     
저작자 표시 비영리 동일 조건 변경 허락
신고

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번 이상 완료시킬 수 있다)

저작자 표시 비영리 동일 조건 변경 허락
신고
  1. 아저씨 at 2011.05.25 00:14 신고 [edit/del]

    좋은 글 감사합니다.

    Reply
  2. 오곡 at 2013.07.09 18:09 신고 [edit/del]

    WDM 의 핵심들 잘배우고 갑니다~!

    Reply

submit
Windows 시스템 프로그래밍 - 9점
Johnson M.Hart 지음, 류광 옮김/정보문화사

오래전부터 이 책이 좋은 책이라는 이야기를 많이 들었었는데, 여태까지 못보고 있다가 이번에 4판이 번역되어 나온 김에 하나 구입해서 며칠동안 재미있게 읽었다.

저자는 이 책을 리차드 스티븐스의 유명한 고전인 APUE에 대응하는 윈도우 버전이라고 말했지만 책을 읽는 내내 자꾸 비슷한 주제를 다루는 제프리 리처의 Windows via C/C++과 비교가 되는 것은 어쩔수가 없었다.

책 중간의 동기화 오브젝트를 다루는 부분에서는 이 책이 Windows via C/C++보다 훨씬 구체적이고 많은 실례들을 들어 설명을 하고 있으며(제프리보다 재밌게 설명하는 것은 아니지만) 그 이후에 나오는 소켓과 파이프, 윈도우즈 서비스 그리고 오브젝트 보안에 대한 내용들은 Windows via C/C++에서는 다루지 않는 내용이다.
맨 마지막 챕터에서 보안 디스크립터로 ACL을 조작하는 내용은 제프리의 책뿐 아니라 다른 책들에서도 쉽게 다루지 않는 보기 힘든 내용이다. 윈도의 파일들을 리눅스 스타일의 권한(rwx--로 관리되는)처럼 관리할 수 있게 하는 재미있고 실용적인 예제와 함께해서 더 좋았다.

간간히 유닉스 시스템과 비교해서 설명해주는 저자의 코멘트들 또한 이 책의 큰 장점이다.
부록에서는 윈도 API의 유닉스와 CRT 대안 함수들을 표로서 제공해주기도 한다.

제프리리처 책의 모든 챕터를 이제는 2-3번씩 읽어서 윈도우 시스템 프로그래밍에 대해 꽤 많이 알게 되었다고 생각했는데, 이 책을 읽으면서 아직도 많은 부분을 제대로 이해 못하고 있구나 하고 느꼈다. 속상한 일이다.

책에서 옮긴 단어중 신호, 일꾼쓰레드, 스택 위넘침 등은 약간 부자연스럽긴 하지만 내용을 이해하는데는 아무런 문제가 없었다. 하지만 딱 한가지 단어가 나를 힘들게 했는데 그것은 '회부' 라는 단어였다. 가상 메모리를 커밋한다고 할 때 바로 그 커밋을 뜻하는데, 잘 매치가 되지 않아서 머리 속으로 계속 Replace를 했다.

부록에는 여러가지 I/O 방식들에 대한 성능 테스트가 나와 있다.
나는 블로그나 잡지 같은 곳에서 프로그램 성능 테스트를 해봤더니 결과가 어떻더라라고 하면 의심을 하고 잘 믿지 않는 편인데, 그것은 많은 사람들이 페이지 폴트나 캐시 등의 잡음을 고려하지 않고 테스트를 하기 때문이고, 또 그런 것들을 미리 알고 있다 하더라도 완벽히 잡음을 제거하기가 힘들기 때문이다.

책을 읽는 동안 이 책의 저자가 상당히 부지런하고 꼼꼼한 사람이라는 느낌을 받아서 벤치마크 결과에 대해 믿음도 가고 기대를 많이 하고 있었는데, 이 저자 역시 실수를 범해 누군가가 이미 벤치마크의 오류를 지적했었나보다.
아래 페이지에서 저자의 코멘트를 볼 수 있다.
http://jmhartsoftware.com/comments_updates.html
램이 1.5기가가 달린 윈도 XP가 페이지 폴트의 영향 때문에 성능이 몹시 떨어져서 나왔는데,  나중에 다시 테스트해서 올리겠다고 한다. 이런, XP하고 비스타의 I/O 성능 비교가 가장 궁금했는데.
어쨌거나 이것들을 다시 테스트 해보는 일은 엄청 귀찮을텐데 5판을 낼 때쯤에나 다시 해보지 않을까 걱정이 되기도 한다.

제프리 리처의 책이 더 이상 새 버전이 안나오는 것은 정말 아쉬운 일인데, 이 책은 5판도 6판도 계속 해서 쓰여졌으면 좋겠다.
저작자 표시 비영리 동일 조건 변경 허락
신고

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 http://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
내 블로그에 프로세스 모니터 사용법이라는 검색어로 들어오는 사람들이 많아서 언젠가 한번 포스팅 해봐야겠다는 생각을 하고 있었다.
프로세스 모니터에 대해 궁금했던 사람들이 조금이라도 도움을 얻고 돌아가기를 바란다.

프로세스 모니터는 SysInternals에서 만든 수 많은 도구들 중 애플리케이션의 분석과 디버깅을 위한 가장 멋진 도구 중 하나이다.

나는 처음에 이 툴을 켜는 것을 무진장 싫어했다.
일단 실행하면 이상한 메세지들이 미친듯이 많이 내려와서 시스템을 엄청 느리게하는 것이 싫었기 때문이다.

하지만, 꾹 참고 얼마 동안만 사용하다 보면 이 툴이 왜 훌륭한지, 그리고 사용자를 충분히 많이 배려해서 만들었음을 깨닫게 될 것이다.

프로세스 모니터를 통해 그동안 너무 많은 문제를 해결했기 때문에, 나는 이제 이 툴의 신봉자가 되어버렸다.
마크 루시노비치는 그의 블로그의 한 포스트에서 스승이었던 데이비드 솔로몬이, "야 임마, 뭔가 의심가면 먼저 프로세스 모니터로 확인해 봐" 라고 말하곤 했었다 했다. 우습게도 지금 마크는 자신의 딸에게 똑같은 말을 한다고 한다.
"아빠, 이거 학교 숙제인데 잘 모르겠어요."
"그럼 프로세스 모니터로 확인해보거라."
ㅡ.ㅡㅋ

농담처럼 이야기 했지만, 정말로 이 말을 믿고 따라야한다.

  • 응용 프로그램이 내 컴퓨터에서만 오동작 할 때.
  • 프로그램 성능의 발목을 잡는 병목점이 어딘지 찾고 싶을 때.
  • 특정 프로그램이 도대체 내부에서 무슨 짓을 하는지 분석하고 싶을 때.
  • DLL이 로드되고 언로드 되는 것을 확인하고 싶을 때.

그리고 이 외 모든 이상하고 골치 아픈 문제들에 대해서 프로세스 모니터가 기꺼이 친구가 되어줄 것이다.
위 마크의 포스트에서도 시스템 부팅이 심하게 느린 원인을 프로세스 모니터를 통해 멋지게 밝혀낸 내용이 쓰여있다.

프로세스 모니터의 주요 5가지 기능은 다음과 같다.


좌측의 아이콘부터,
  • 레지스트리 활동 모니터링
  • 파일 시스템 활동 모니터링
  • 네트워크 활동 모니터링
  • 프로세스와 쓰레드 활동 모니터링
  • 이벤트 프로파일링

다섯가지 아이콘을 모두 클릭해서 프로세스의 모든 활동을 살펴 볼 수도 있지만, 로그가 너무 많아지면 문제에 집중하기 힘들기 때문에 원하는 특정 기능만을 활성화 시키면 된다.

하지만 레지스트리나 파일 시스템 둘 중 하나만 클릭해보더라도 얼마나 많은 로그가 쌓이는지 보고는 깜짝 놀라게 될 것이다. 아무 프로그램도 안 띄워놓고 있더라도 파일 시스템과 레지스트리는 눈에 안보이는 서비스나 백그라운드 애플리케이션들에 의해 항상 바쁘다.

이를 좀 더 편하게 추려내기 위해 프로세스 모니터는 강력한 필터링 기능을 또한 제공한다.


처음 프로세스 모니터를 켜면 위와 같이 필터링 대화상자나 나타난다.
초록색 아이템은 관심있어 하는 내용이고, 빨강색 아이템들은 관심이 없으니 캡쳐 하지 말라는 뜻이다.
위 그림에서 처럼 프로세스 이름으로 필터링 할 수도 있고, 파일의 경로나 쓰레드 아이디, 어떤 특정한 동작을 하는지의 여부로도 필터링 할 수가 있다. 알고 싶은 내용에 대해서만 로그가 나오도록 필터링 옵션에 숙달되는 것이 프로세스 모니터를 사용하는데 있어서 가장 중요한 점이다.

또한 위 그림에서 맨 좌측에 있는 체크박스들은 꽤 근래에 생긴 편리한 기능이다.
자신이 자주 쓰는 옵션들로 필터들을 한번 넣어 놓은 뒤, 다음번 실행시에는 체크박스만 껐다 켰다 하면서 쉽게 제어할 수 있다.


메뉴의 필터 부분을 보면 방금 설명한 필터 기능을 제외하고, 주목할 기능이 3가지가 더 있다.


Enable Advanced Output을 켜면 파일 시스템 활동을 관찰할 때 Operation이 아래 그림처럼 나타난다.


커널 쪽에서 프로그래밍 경험이 있는 사람들이라면 각 디스패치 루틴들과 대응된다는 것을 알 수 있을 것이다.

Advanced Output 옵션을 사용하지 않는다면 아래 그림처럼 일반 응용 프로그래머들에게 좀 더 익숙한 Win32 인터페이스 이름으로 출력된다.

Advanced Output 이라고 해서 항상 좋은 것은 아니며, 자신이 어떤 부분에 관심이 있는지에 따라서 양쪽을 번갈아가면서 사용하면 된다.

Drop Filtered Events 라는 것은 역시 최근에 생긴 너무 반가운 기능이다.
기본적으로 프로세스 모니터를 켜고 나면, 필터를 아주 정교하게 설정해서 원하는 로그만 출력되게 했다고 할지라도, 화면에만 안 나올 뿐이지 다른 모든 내용들이 캡쳐된다.
이는 메모리를 몹시 많이 잡아먹고 시스템을 느려지게 하는데, 위 기능을 사용하면 관심없는 이벤트들은 캡쳐하지 않고 바로 버리게 된다.
하지만, 시스템이 조금 느려지더라도 모든 내용을 버리지 않고 간직하고 있는 것이 유용할 때가 종종 있다. 로그를 보는 도중 다른 아이디어가 생각나서 필터를 변경했을 때 예전 내용들을 다 가지고 있다면 곧바로 확인해 볼 수 있지만, Drop해버리면 프로세스 모니터를 껐다 켜서 다시 캡쳐해야한다. 기능을 잘 이해한 채 적절히 사용하면 된다.


Highlight 기능 역시 로그를 쉽게 보는데 도움을 준다.

위 그림은 윈도 탐색기가 수행하는 파일 오퍼레이션 중, 파일 생성/오픈 요청이 성공한 경우만을 하이라이팅 하는 것을 보여준다.
이 정도로 간단한 경우에는 프로세스 모니터의 하이라이팅도 편리하지만 조금 더 복잡해지면 설정하는 것이 꽤나 귀찮고 색깔도 하나라서 눈도 아프다.
그래서 그런 경우에 나는 로그를 통째로 vim에 붙여넣은 뒤 vim을 통해 여러 색깔로 하이라이팅해서 보고는 한다. 이 방법에 관심이 있다면 아래 글을 참고하면 된다.
로그 뷰어로써의 Vim, 멀티 하이라이팅


프로세스 모니터로 캡쳐한 내용을 파일로 저장할 수도 있다. 보통 PML이나 CSV 형식 중 하나로 저장하는데, 나는 PML 포맷으로 저장해서 프로세스 모니터로 열어보는 것을 선호한다. CSV로 저장한 뒤 엑셀 같은 프로그램으로 열어볼 수도 있다.

이 로그들을 처음부터 바로 파일로 저장할 수는 없을까 하는 생각이 들 수도 있다.
자신이 작성하고 있는 커널 쪽 코드에 오류가 있으면 블루스크린을 만나게 되거나, 시스템 전체가 멈춰버리는 경우가 생길 수 있는데, 이런 경우 프로세스 모니터를 켜놓고 있어도 시스템이 꺼져버리는 등 로그가 날라가기 때문에 내가 지정한 파일로 저장한다면 유용할 수 있다. 이것은 File 메뉴의 Backing Files 옵션에서 설정 가능하다.
바로 밑에서 설명할 프로세스 모니터에 커스텀 로그 출력하는 방법과 함께 사용하면 더욱 도움이 될 것이다.


응용 프로그램이나 드라이버가 프로세스 모니터를 통해 자신들만의 로그 메세지를 출력하는 것도 가능하다.
이렇게 할 때의 장점은 여러가지가 있다.

  • 프로세스 모니터가 캡쳐하는 파일이나 레지스트리 작업들과 함께 내 커스텀 로그 메세지를 한 눈에 볼 수 있다. 물론 순서가 뒤죽박죽 되지 않도록 프로세스 모니터가 잘 동기화 해준다.
  • 프로세스 모니터가 시간, 프로세스 이름, 쓰레드 번호 등의 부가 정보를 다 기록해주기 때문에 자신의 Logger에서 이런 것들을 구현할 필요가 없다.
  • 프로세스 모니터의 강력한 필터링과 하이라이팅 기능도 공짜로 이용할 수 있다.
  • 프로세스 모니터를 켜야만 로그를 쓰는 작업에 대한 오버헤드가 생긴다. 즉, 프로세스 모니터가 꺼져있으면 프로세스 모니터로 로그를 전송하지 않기 때문에 응용 프로그램의 속도 저하가 발생하지 않는다. -디바이스를 한번 오픈해보는 오버헤드는 있다.

프로세스 모니터로 커스텀 로그를 출력하는 자세한 방법은 아래 글에서 확인해볼 수 있다.
프로세스 모니터에 디버깅 메세지를 인젝션하기

얼마전에 소개했던 filetest 프로그램과 프로세스 모니터는 환상의 짝궁이기도 하다. filetest.exe 프로세스만을 캡쳐하도록 필터링해 놓은 뒤 원하는 I/O를 시도해보고 프로세스 모니터의 반응을 살펴보면 파일 시스템이 어떻게 동작하는지 쉽게 배울 수 있다.

위에서 설명한 사용법 들에 익숙해지면 프로세스 모니터를 통해 직접 문제를 해결할 수 있을 것이고, 또 이를 여러번 시도 할수록 자신만의 문제 해결 기법 아이디어을 갖게 될 것이다.

이런 몇몇 참신한 기법들에 대해서는 -위에 나왔던 시스템 부팅이 느린 현상을 해결한 포스트처럼- 이미 마크 루시노비치의 블로그나 Sysinternals의 세미나 자료를 통해서 많이 공개되어 있으니 부지런히 찾아다니면서 익혀두면 피가 되고 살이 될 것이다.
저작자 표시 비영리 동일 조건 변경 허락
신고

'Softwares' 카테고리의 다른 글

리눅스 3.0 시대  (0) 2011.05.25
ACE 6.0 static build  (2) 2011.03.06
프로세스 모니터 사용법  (6) 2011.01.14
2010년 분야별 최고의 오픈소스들  (5) 2011.01.09
파일 조작 테스트를 위한 훌륭한 도구 소개  (4) 2010.12.27
VirtualBox 4.0 베타  (0) 2010.12.07
  1. Favicon of http://nyolong.egloos.com BlogIcon 뇨릉 at 2011.01.19 14:04 신고 [edit/del]

    얼마전에 FileMon을 받으려더 Process Monitor와 통합이 되어서 저도 이툴 한번 써봤었는데요. ㅋ
    전 간단히 파일모니터하는데만 썼었는데 참 괜찮은 툴 같습니다 ^^

    Reply
  2. BlogIcon tack at 2011.01.24 16:08 신고 [edit/del]

    전 크롬을 사용중인데 이상하게 Highlight 기능 역시... 아래 그림이 안나오네요 :(
    헌데 Explorer.exe에 대한 내용이 원래 이렇게 많나요? ㅎㅎ

    Reply
    • Favicon of http://www.benjaminlog.com BlogIcon 김재호 at 2011.01.24 17:14 신고 [edit/del]

      에 정말요? 저도 크롬쓰는데 잘 나오는데.
      다른 장소에서도 잘 나오고요. 용량이 조금 크긴 한데 다른 문제가 있는건지 모르겠네요.

submit
CreateFile은 Wndows Api 들 중 가장 기본적이면서도 중요한 함수이다.
이 함수는 단지 파일을 생성하는 것 뿐만이 아니라 파일을 오픈할 수도 있고 디렉터리를 오픈할 수도 있으며 또한 여러 디바이스들까지 오픈 할 수 있다. 사실 CreateFile에서 File은 꼭 파일만이 아닌 여러 디바이스들을 추상화한 Virtual File을 뜻하는 셈이다.
이 함수를 통해 파일을 여는 순간에 동기 I/O를 할지 비동기 I/O를 할지 결정하게 되며, 내가 어떤 작업을 하려는지 내가 파일을 열고 있는 동안 다른 클라이언트들에게는 어떤 작업을 허용할지도 결정하게 된다.
생성하려는 파일의 읽기 전용, 숨김 파일 등의 속성도 정할 수 있으며, 캐시를 이용 할지 말지, 쓰기를 하는 족족 플러시 하게 할지 또 I/O를 순차적으로 할지 랜덤하게 할지 등의 힌트도 파일 시스템으로 전달해줄 수 있다.

이렇게 중요한 함수이니만큼 MSDN에는 CreateFile에 대한 문서가 아주 잘 나와있는데, 페이지 내의 링크들까지 하나하나  따라가면서 차근 차근 읽다보면 시스템 프로그래밍에 대해 배울 수 있는 것들도 많고 무엇보다도 아주 재밌다.

하지만 아무리 열심히 읽어도 글만 읽고서 지식을 자기 것으로 만들 수는 없는 법이다.
언제나 마지막은 실습으로 끝나야 한다. 글을 다 이해한 것 같아도 막상 진짜로 해보려고 하면 거기서 또 어려운 문제가 닥치기 마련이며, 이 것까지 해결하고 나서야 비로소 완전히 자기 지식으로 만들었다고 할 수 있다.

CreateFile의 파라메터는 몇 개 안되는 것 같지만 엄청난 플래그들의 조합이 가능하기 때문에 사실은 적은 갯수가 아니다.
실습을 해보기 위해서 항상 무거운 비주얼 스튜디오를 켜고 그 지겨운 파라메터들을 매번 입력하는 것은 손가락도 아프고 시간도 많이 들어가는 비효율적인 방식이다.

이런 실습을 위해 누군가가 이미 아주 훌륭한 도구를 만들어서 osronline.com에 올려놓았다.
이는 프로세스 모니터와 함께 내가 가장 즐겨쓰는 도구들 중 하나인데, 병들어가는 내 손가락을 조금이나마 쉴 수 있게 해주는 아주 고마운 친구이다.


위 그림에서 보이는 것 처럼 CreateFile 함수 형태 그대로 UI를 제공하고, 실험해보고 싶은 모든 플래그 조합을 넣어볼 수 있다.
생성뿐만이 아니라 읽기 쓰기도 해볼 수 있으며 조금 더 저수준 함수인 NtCreateFile까지도 다루어볼 수 있다.

우측의 버튼들을 클릭하면 아래처럼 또 다른 대화상자가 나와서 CreateFile의 많은 옵션들을 손쉽게 넣어서 테스트 할 수 있다.



파일 시스템과 관련이 있는 일은 하는 사람들은 두말 할 것도 없고, Wndows 플랫폼에서 개발하는 모든 개발자들이 알아두면 좋을 훌륭한 도구이다.
저작자 표시 비영리 동일 조건 변경 허락
신고
  1. 재호님 팬 at 2010.12.29 12:14 신고 [edit/del]

    재호님~ 파일시스템에 관심이 많으신거 같네요!
    저도 파일시스템에 관심있어요 ^^
    이병오님의 "윈도우 파일시스템" 책과 정명수님의 커널관련글 읽으면서 공부하고 있는데 좋은거 같습니다. 재호님도 화이팅! ^^

    Reply
    • Favicon of http://www.benjaminlog.com BlogIcon 김재호 at 2010.12.29 12:37 신고 [edit/del]

      윈도 파일시스템 책은 저도 가지고 있는데 정명수님 커널 관련글은 잘 모르겠어요. 혹시 글들 정리되어 있는 URL이 있으면 좀 가르쳐주세요.^^

  2. 재호님 팬 at 2010.12.29 20:19 신고 [edit/del]

    이런 제가 센스가 없어서 ㅋㅋ
    www.swblog.net 입니다. 저도 마이크로소프트 잡지를 통해서 알게 되었구요~ 이분 글로 공부하고 있어요 ^^

    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
유저모드에서는 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
Windows Internals 제5판 - 10점
마크 러시노비치 외 지음, 안철수 연구소 기반기술팀 옮김/에이콘출판


악. 기다리고 기다리던 Windows Internals 5th가 드디어 번역본이 나왔다.

이 책을 처음 훑어 봤을 때는 데이빗과 마크 이 자식들이 대체 무슨 이야기를 하고 있는 건지, 왜 이 따위 얘기를 하는건지 아무것도 이해할 수가 없었다.

4판 서문에 저자들이 제프리리처에게 감사를 전하는 문구가 있는데 내용이 너무 웃긴다.

Thanks to our friend Jeffrey Richter, for writing the "What about .NET and WinFX" sidebar in Chapter1 and for continuing to remind us over many dinners together of his view on how few people should care about what we talk about in this book.

도대체 그런 주제로 책을 쓰면 몇 명이나 사보겠냐. Windows via C/C++ 을 이길수 있을 것 같아? 라고 밥먹는 중에 놀려대는 모습이 왠지 상상이 간다.

디바이스 드라이버를 만들어보면서 이 책의 내용들이 조금 더 잘 이해가 되기 시작했다. 그리고 특정 개발자들에게는 정말 중요한 내용이라는 것도 알았다.

내 실력이 부족한 것이 주 원인이었지만 기존 4판에서는 번역도 조금은 불만족스러웠고 잘못된 그림이나 오타 등도 많아서 답답했다.
그것보다 더 중요한 것은 비스타 이후 변경된 많은 내용들이 4판에는 없었다는 것인데, 그래서 5판이 빨리 번역되어 나오기를 간절히 기다려왔다.

실력있는 사람들이 작업한 만큼 많은 것을 배울 수 있을 것 같아서 가슴이 설렌다.
책 값이 조금 비싸긴 하지만, 뭐 좋은 책이니깐 이 정도 쯤이야.
신고
  1. Favicon of http://namoda.springnote.com BlogIcon 나모 at 2010.10.28 14:15 신고 [edit/del]

    지난 주에 교보에 들렸다가 이 책을 보고 바로 구입했습니다. 오늘 도착했는데 Windows via C/C++ 옆에 놔두니 다른 출판사의 책이지만 옆면은 비슷하네요. 둘 다 Microsoft Press 라서 그런가요?

    Reply

submit
Windows 시스템 실행파일의 구조와 원리 - 9점
이호동 지음/한빛미디어

이 책은 PE(Portable Executable)라고도 불리우는 윈도우즈 실행 파일의 내부 구조에 대해서 다룬다.
EXE, DLL, OCX, SYS, DRV 확장자들로 된 파일들이 모두 PE파일이다.

이런 PE 파일들이 실행되는 순간에 메모리에 어떻게 매핑되고 그 자료구조는 어떻게 구성되는지가 바로 이 책이 다루는 내용이다.

이 책을 읽기 전에 제프리리처의 Windows Via C/C++

13장 윈도우 메모리의 구조
14장 가상 메모리 살펴보기
17장 메모리 맵 파일
19장 DLL의 기본
20장 DLL의 고급 기법

위 장들을 읽어보는 것이 이 책을 이해하는데 많은 도움을 줄 것이다.

나는 처음에는 헥사코드를 하나씩 따라가면서 정독해서 읽었는데, 관련 구조체들이 다 비슷비슷 하게 구성되어 있어서 중간쯤 부터는 그런 부분들을 제껴가면서 조금 더 편하게 읽을 수 있었다.

개인적으로 DLL의 깊숙한 부분에 대해서 많은 것을 얻을 수 있어서 좋았고, 또 리소스에 대해서는 거의 아는 것이 없었는데, 이 부분을 많이 다루어 주어서 특히 좋았다.

이 책의 단점을 꼽자면, 그림이나 코드들이 보기에 너무 조잡하다는 것이다.
또한 느낌표나 말줄임표를 잔뜩 붙여놓은 문장들이 곳곳에서 보이는데, 이는 잘 정제되지 못한 글의 느낌을 받게 한다. 마치 인터넷에 써서 올렸던 글을 추려서 책으로 낸듯이 말이다.
저자는 프로그래머이지 글쟁이가 아니므로 이런 것들은 출판사에서 신경을 써준다면 좋을 것이다.

이 책과 비슷한 경우로 김상형의 Windows API 정복이라는 명서가 있는데, 나는 이 책을 국내에서 출판된 가장 잘 쓰여진 프로그래밍 책이라고 생각하지만, 책 안의 그림들은 저자의 깔끔한 글솜씨에 비해 조잡하게 느껴진다.
이 책은 가남사에서 출판되었다가 한빛미디어에서 개정되었는데, 그림들이나 표는 아마도 그대로 옮긴 것 같다.

저자가 그린 그림을 출판사에 제출하면 편집없이 그대로 실는 것 같은 느낌인데, 만약 정말 그러고 있다면 출판사에서 좀 더 다듬어서 주기를 바란다.
오라일리 같은 출판사에서 발행하는 책들은 그림이나 표가 참 보기 좋게 그려져있다. 그 책을 쓰는 저자들이 모두 미술에까지 일가견이 있어서 그런 것은 분명 아닐 것이다.

다음은 내가 출판사에 바라는 점들이다.
1. 모든 코드 조각은 회색 박스로 둘러싼다.
2. 코드 폰트는 고정폭 폰트를(기왕이면 프로그래머가 많이 쓰는) 사용한다.
3. 지저분한 그림이나 표는 예쁘게 다시 그린다.

쓰다보니 단점의 내용이 길어져 버렸는데, 이 것은 이 책만이 아니라 국내에서 출판되는 모든 프로그래밍 서적에 대한 내용이다.

이런 자잘한 단점을 제외하면 이 책은 읽을만한 가치가 있는 아주 훌륭한 책이다.
이런 지식을 얻기위해서, Matt Pietrek이 쓴 다음과 같은 글 들을 눈이 빠지게 읽어야 하는데
보기만해도 질려버린다.




이미 이 책의 저자가 이 문서들을 잘 읽어본 후에 우리들에게 한글로 쉽게 설명해주고 있으니 그저 고마울 뿐이다.

이 책이 개정판이 또 나올 것 같지는 않지만 혹시 개정된다면, 위에서 말한 부분들의 교정과 함께 64bit PE나 Managed PE에 대해서도 살짝 다루어주면 좋겠다.

신고

submit