Media Log

[Programming]에 해당되는 글 51

  1. h 프로그래머의 5가지 성향 2014/03/15
  2. h 프로그래밍의 첫 번째 규칙: It's Always Your Fault. (1) 2014/03/06
  3. h 윈도우즈 PE 파일의 구조 2013/03/24
  4. h 윈도8에서의 UAC 관련 중요한 변화 2012/05/28
  5. h 매크로의 가변인자를 또 다른 매크로로 넘기기 2012/04/16
  6. h 내가 좋아하는 국내 프로그래밍 블로그들 (10) 2012/04/12
  7. h CString 의 비밀 (5) 2012/04/12
  8. h 쉘의 파일 오퍼레이션을 잡아챌 수 있는 ICopyHook 인터페이스 2012/04/06
  9. h 메인 쓰레드가 종료되면 프로세스도 종료되는 것 아닌가요? 2012/03/26
  10. h 어떻게 함수의 undecorated name을 추측 할 수 있을까? (3) 2012/02/12
  11. h 왜 static 멤버 함수에는 const를 붙일 수 없을까? 2012/02/06
  12. h SetFilePointer 보다는 SetFilePointerEx를 사용해야 한다 (1) 2012/01/16
  13. h GetLastError 함수 사용의 흔한 실수 2012/01/13
  14. h NTFS에서 Sparse 파일을 만들기 (3) 2012/01/03
  15. h 16TB 크기의 파일을 만들어내려면 얼마나 오래 걸릴까? 2012/01/01
  16. h 구조체의 패킹에 대한 이야기 (2) 2011/12/19
  17. h 프로그래머에게 가장 굴욕적인 순간은? 2011/12/19
  18. h C/C++ 코딩 스타일 이야기 (8) 2011/10/24
  19. h 레지스트리의 volatile 옵션 2011/09/27
  20. h 알아두면 유용한 MoveFileEx 함수의 펜딩 옵션 2011/07/17
  21. h 메모리를 해제하기 전에 왜 널 체크를 하는걸까? (6) 2011/05/29
  22. h 윈도우 드라이버를 만들 때 알아야 할 기초적인 내용들 (2) 2011/05/23
  23. h Duff's Device 2011/04/07
  24. h _countof 매크로 2011/03/15
  25. h FIELD_OFFSET 매크로 (1) 2011/03/01
  26. h PAGED_CODE 매크로 (2) 2011/02/27
  27. h 디렉터리의 읽기 전용 속성 (4) 2011/02/20
  28. h 알쏭달쏭한 typedef (8) 2011/01/04
  29. h 하위 디렉터리의 파일이 변경 되었는지 감지하는 법 (6) 2010/12/20
  30. h WinApi의 reserved 인자는 뭐하는 용도일까 2010/12/09


1. 쉬운 문제를 어렵다고 말하며 매번 포기하는 사람.
2. 어려운 문제를 쉽다고 말하며 틀린 답을 내는 사람.
3. 어려운 문제를 쉽다고 말하며 쉬운 솔루션으로 그저 그런 답을 내는 사람.
4. 어려운 문제를 어렵다고 말하며 제대로 된 솔루션을 만들어내는 사람.
5. 어려운 문제를 쉽다고 말하며 제대로 된 솔루션을 만들어내는 사람.

1,2,3 번은 많이 봤는데 4번은 몇 명 만나보지 못한 것 같다. 5번은 아직까진 한 명도 보지 못했다.


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

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

submit

여러분은 이게 어떤 기분인지 알 것입니다. 이는 우리 모두가 한번쯤 겪었던 일입니다. 여러분은 코드를 수십 번 읽어 보았으나 여전히 무엇이 문제인지 발견하지 못하고 있습니다. 하지만 그 코드에는 여러분이 도저히 처리할 수 없는 버그 혹은 오류가 존재하고 있습니다. 여러분은 코딩하고 있는 컴퓨터에 문제가 있거나 프로그램을 실행하는 운영체제, 혹은 사용하는 도구나 라이브러리에 문제가 있다고 생각해버립니다.


“분명히 그럴꺼야!”


하지만 아무리 좌절스럽더라도 그런 접근을 해서는 안됩니다. 우연에 맡기는 프로그래밍은 이제 그만하십시오. 그건 정말 나쁜 행동입니다. 어렵고 모호한 버그와 맞서는 것이 좌절감을 줄 수도 있지만, 그런 좌절감 때문에 잘못된 길로 들어서서는 안됩니다.


좋은 프로그래머가 되기 위한 필수 조건은 여러분이 작성한 코드에 문제가 있을 때마다 그것이 항상 자신의 잘못이라고 생각하는 일입니다. 이 내용은 실용주의 프로그래머에서 ‘select 함수는 잘못되지 않았다’ 부분에 잘 요약되어 있습니다:


대부분의 프로젝트에서 당신이 디버깅하고 있는 코드는 당신의 프로젝트 팀, 써드파티 결과물(데이터베이스, 네트워킹, 그래픽 라이브러리, 특화된 통신 방법들이나 알고리즘 등), 플랫폼 환경 상에서(운영체제, 시스템 라이브러리, 컴파일러) 당신과 다른 이들에 의해 작성된 응용 프로그램 코드의 결합물일 것이다.

운영체제나 컴파일러 혹은 써드파티 제품에 버그가 있을 수도 있다. 하지만 그것이 첫 번째로 의심하는 사항이 되어서는 안된다. 당신의 코드에 버그가 존재할 가능성이 훨씬 더 크다. 라이브러리 자체에 버그가 있다고 의심하는 것보다는 라이브러리를 호출하는 응용 프로그램의 코드가 문제가 있다고 생각하는 것이 보통 더 이득이다. 설사 써드 파티 코드에 문제가 있었다 할지라도, 어짜피 버그리포트를 제출하기 전에 자신의 코드를 제거하는 일을 해야만 한다.

우리는 한 프로젝트에서 솔라리스 운영체제에 있는 select 시스템 함수에 버그가 있다고 확신하는 한 고참 개발자와 일한 적이 있었다. 어떤 설득이나 논리도 그의 생각을 바꿀 수는 없었다. (같은 컴퓨터에서 다른 네트워크 프로그램들은 모두 잘 작동했지만 그건 그에게 별 상관이 없었다.)

그는 몇 주동안 문제를 해결하려고 이리저리 코드를 변경해보았지만, 이상하게도 그 문제는 해결이 되지 않는 것처럼 보였다. 그가 우리의 강요에 의해 억지로 자리에 앉아 select함수의 문서를 읽었을 때, 그제서야 그는 어떤 것이 문제였는지 깨달았고 그 문제를 몇 분만에 해결했다. 그 이후 우리는 누군가가 자신의 실수일 수 있는 문제를 시스템의 버그라고 탓하는 것을 볼 때마다 “select 함수의 버그” 라는 말을 사용하게 되었다.


코드 주인의식의 다른 말은 코드 책임의식입니다. 여러분의 소프트웨어의 문제가 무엇이던지 간에 -그 문제가 자신의 코드에 없었다 할지라도- 항상 자신의 코드안에 문제가 있다고 가정해야 하고 그에 따라 행동해야 합니다. 만약 여러분의 소프트웨어가 최고가 되길 원한다면 제품의 버그에 대해 전적으로 책임을 져야만 합니다. 사실 엄밀히 말하면 그럴 필요까지는 없습니다. 다만 이것이 여러분이 존경과 신뢰를 얻게 되는 방법입니다. 만약 그 문제에 대해 다른 사람들, 회사나 다른 소스로 그 잘못을 떠넘긴다면 여러분은 분명 존경과 신뢰를 얻지 못하게 될 것입니다.


여러분도 알다시피, 오류가 여러분의 잘못이 아닌 소프트웨어의 오류일 확률은 통계적으로 극히 드뭅니다. 스티브 맥코넬은 그의 책 코드 컴플리트에서 이 사실을 증명하는 두 가지 연구사례를 인용했습니다:


1973년과 1984년에 수행된 연구는 보고된 총 버그의 약 95%가 프로그래머에 의해서 발생한 것들이라는 것을 발견했다. 2%는 시스템 소프트웨어(컴파일러나 운영체제) 이고 2%는 그 외의 다른 소프트웨어들이었으며 1%는 하드웨어였다. 오늘날 시스템 소프트웨어와 개발도구들은 1970~80년대에 비해서 훨씬 많은 사람들에 의해 사용되어지고 있기 때문에 지금은 프로그래머의 오류 비율이 예전보다 더 높아졌을 것이라 추측한다.


여러분의 소프트웨어가 가지고 있는 문제가 무엇이든지간에 주인의식을 가지시기 바랍니다. 먼저 여러분의 코드에서부터 출발해서, 그 문제 원인에 대한 결정적 증거를 손에 넣을 때까지 계속해서 바깥으로 범위를 넓혀가며 살펴보십시오. 만약 그 문제가 자신이 컨트롤 할 수 없는 다른 코드에 존재한다면 문제 진단 및 디버깅 기술을 배워야 할 뿐만 아니라 자신의 주장을 뒷받침 해줄 수 있는 증거 또한 확보해 두어야 할 것입니다.


이 과정은 어깨를 한번 으쓱하며 운영체제, 개발도구, 프레임워크의 잘못으로 돌리는 것 보다는 훨씬 더 번거로운 일입니다. 그러나 이는 여러분에게 남 탓으로 돌리기나 문제 회피 등으로는 절대로 얻지 못하는 신뢰와 존경을 얻도록 할 것입니다.


만일 진심으로 좋은 프로그래머가 되고 싶다면 “그건 내 실수야. 내가 그 원인이 뭔지 찾아내고 말거야.” 라고 말하는 것을 꺼리지 마십시오.


이 글은 스택오버플로우를 개발한 제프 앳우드가 2008년 3월에 쓴 The First Rule of Programming: It’s Always Your Fault 를 원저자의 허락을 받고 번역한 글입니다.


--

2년여 전 즈음에, 카카오의 동료들하고 심심풀이로 기술 관련 좋은 글들을 구글 독스에서 소셜 번역(?) 을 하곤 했었는데, 그 중 하나였던 이 글이 최근 자꾸 생각나서 다시 읽어보다가 블로그에 올려야지 생각하게 되었다.(Jenny, Clare, Probe 감사!)

이 글은 코딩 호러의 이펙티브 프로그래밍이라는 번역서 내에도 담겨있으며, 이 책에는 다른 좋은 글들도 많으니 관심이 있는 사람들은 읽어보는 것도 좋겠다. 제프 앳우드의 다른 책으로는 코딩 호러가 들려주는 진짜 소프트웨어 개발 이야기라는 책도 있다. 둘 다 너무 재미있고 좋은 이야기들이 많이 담겨있는 책이다.

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

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

  1. 재호님팬 at 2014/04/01 22:07 [edit/del]

    정말 좋은 글이네요. 공유해주셔서 감사합니다..

    Reply

submit


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

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

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

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

submit

http://www.brianbondy.com/blog/id/140/

위 포스트를 읽고서 윈도8에 꽤 큰 변화가 생겼다는 것을 알았다.

지금까지는 UAC를 꺼둔다면 XP와 같은 환경이라고 생각해도 별 문제가 없었지만, 이제는 더 이상 아니라는 뜻.


If (IsUacDisabled())

{

}


만일 이런 이상한 얍삽이 코드를 즐겨 사용했다면, 이제 그 결정에 대해서 벌을 받을 시간이다.

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

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

submit

살다보면(?) 매크로에서 받는 가변 인자를 또 다른 매크로로 쑤셔넣고 싶은 경우가 있다.

#define MACRO_1(abcfn(a, b, c) #define MACRO(...) MACRO_1(__VA_ARGS__)

짠, 이렇게 하면 된다.

그렇다. 아무 테크닉이 필요없이 그냥 쑤셔넣으면 된다.

그런데 위 코드는 GCC에서는 잘 동작하지만 VC에서는 동작하지 않는다. 그렇다고 해서 가변인자는 다른 매크로로 건넬 수가 없구나 하고 오해하면 안된다. 이것은 그냥 비주얼 스튜디오의 버그일 뿐이다.

#define MACRO_1(abcfn(abc)
#define MACRO_1_(args_listMACRO_1 args_list
#define MACRO(...) MACRO_1_((__VA_ARGS__))

비주얼 스튜디오에서는 위와 같은 얍삽이를 통해서 이를 회피할 수 있다. __VA_ARGS__ 주위를 한 겹 더 괄호로 둘러싸서 또 다른 매크로로 넘기는 것을 주의해서 봐야한다.


그래서 내가 하고 싶은 말은,


이 버그가 정말 거지 같다고 생각된다면

http://connect.microsoft.com/VisualStudio/feedback/details/380090/variadic-macro-replacement

여기 가서 upvote를 해주세요.

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

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

submit

내 피드리더에는 등록되어있는 블로그가 700여개 쯤 있다.

그 중에는 새로운 글이 올라올 때마다 가슴이 설레는 몇몇 블로그들이 있다. 그 중 우리나라 저자가 운영하는 3개의 블로그를 소개하려고 한다.


1. 메아리 저널

정말 엄청난 실력을 가진 해커이다. 특정 플랫폼이나 언어에 상관없이 여러 주제로 재밌는 글을 쓴다. 가끔 기괴한 코드 골프 내용이 올라오기도 하는데 그런걸 쳐다보고 있다 보면 자괴감에 빠지기도 한다.

어쨌거나 나는 그의 글이 너무나도 좋아서 언젠가는 주말 이틀 동안 내내 방구석에 누워서 2004년부터인가 썼던 모든 글을 다 읽어본 적도 있었다. 가끔씩 이상한 오락실 얘기도 쓰고는 하는데 그런 글조차 재밌다. 블로그에 댓글로 피드백을 할 수가 없어서 애독자로서 좀 아쉽긴 하지만 본인이 그에 대해 많이 고민해 본 듯하니 어쩔 수 없는 일이다.


2. art.oriented

프로그래머가 몰랐던 멀티코어 CPU 이야기를 쓴 저자이다. 주로 윈도 프로그래밍이나 시스템 프로그래밍 이야기를 다루는데 글들이 재밌을 뿐더러 배울 점도 많다. 이 블로그 역시 거의 모든 글을 다 읽었다. 아마 프로그래밍 기술을 다루는 우리나라 블로그들 중 가장 피드백이 많이 왔다갔다 하고 방문자 수도 많은 블로그가 아닐까 싶다.


3. 김용묵의 절대 공간

이 블로그는 비교적 최근에 알게되었다. 글들은 아주 오래전부터 꾸준히 작성되어 왔는데 써있는 글의 질과 양에 비해 일방문자수는 상당히 적다. 윈도 프로그래밍에 대해서만 다룬다. 윈도 역사에 대해서 글을 쓸 때는 레이몬드 첸을 보는 느낌이 들기도 한다. 철도와 종교 이야기도 종종 꺼내는데 나는 그런 주제는 관심이 없어서 건너 뛰고 읽는다.


그러고보니 이 블로그들 이상으로 예전에 정말 좋아했던 블로그가 있었다. 바로 방준영의 블로그. 2009년도 즈음이었던가? 어느 날 아침에 그의 블로그를 발견하고는 오아시스라도 발견한 것 처럼 기뻤던 날이 있었다. 거의 매일 같이 좋은 글들이 올라와서 정말 행복하게 읽어가고 있었는데 언제부터인가 새 글이 더 이상 올라오지 않았고 심지어는 기존에 썼던 글마저 사라져 버렸다. 안타까운 일이다. 준영님. 혹시라도 이 글을 보신다면 돌아와주세요. 엉엉.


나는 미투데이나 트위터가 싫다. 페이스북도 싫다. 이 잡 것들이 나오고 나서 사람들은 블로그에 글을 잘 쓰지 않는다.

돌아오라 블로거들이여. 맛집 블로거 말고 프로그래머들 말이여.

그럼에도 불구하고 영어 블로그는 여전히 좋은 블로그들이 엄청나게 많다. 하지만 영어 블로그를 읽는 것은 정말 고통스러운 일이다. 내 피드 리더에는 나중에 읽으려고 마킹해둔 글들이 잔뜩 쌓여있는데 그것들 대부분은 영어 포스트이다. 한 번 읽으려면 크게 심호흡부터 하며 각오를 단단히 하고는 한다. 그리고 나는 그렇게 심호흡을 자주 하지 않기 때문에 오늘도 마킹만 해두는 글들이 계속 늘어간다.

그러니 이렇게 우리말로 좋은 글을 써주는 사람들이 어찌 고맙지 않겠는가. 좋은 국내 블로그들이 더 많이 생겼으면 좋겠다.

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

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

  1. Favicon of http://hongyver.tistory.com BlogIcon hongyver at 2012/04/13 08:38 [edit/del]

    제 피드리더에도 많은 블러그가 등록되어 있는데 그중에 한분이십니다. ^^
    평소 글 잘 읽고 있습니다.
    (영어 블러그는 마킹할 생각도 못하고 그냥 지나칠때가 많아요.)

    Reply
  2. Sobbungi at 2012/04/15 03:02 [edit/del]

    님도 저 세 블로그 만큼이나 충분히 훌륭하십니다..
    가끔 올라오는 글들 유용하게 잘 읽고 있습니다..
    계속 좋은 글 부탁드립니다~~.

    Reply
  3. Favicon of http://moogi.new21.org/tc BlogIcon 사무엘 at 2012/05/14 08:23 [edit/del]

    안녕하세요? 제 블로그에 흔적을 남겨 주신 것에 감사드리며, 저와 제 지인의 블로그를 소개해 주셔서 또 감사합니다. ^^

    '1. 메아리 저널의 운영자'는 저의 학교 후배이고 저와도 아주 친한 사이입니다.
    '엄청난 실력을 가진 해커' ← 사람 보는 안목이 정확하십니다. ㄷㄷ

    짐작하신 대로 저는 아주 좁은 제 전문 분야만 빼면 딱히 다른 IT 기술이나 플랫폼이나 전산학의 발전에는 큰 관심이 없는 타입이에요.
    그에 반해 진정한 전산학 덕후/해커가 어떤 사람인지를 저 친구를 보면서 저는 절실히 느낀답니다. ^^

    김민장 님 블로그는 운영자께서 대학원 생활에 너무 바쁘셔서 유명세에 비해 글이 너무 뜸하게 올라온다는 것만이 유일한 단점인 듯하고...

    제 블로그는 설치형이어서 메타블로그에 딱히 노출되는 게 없습니다. 그래서 예전부터 제가 개발한 프로그램을 사용해 온 분이나 너무 매니악한 블로그 주제에 질리지 않은 분들만 오느라 방문자수가 적은 것 같습니다. ^^;; 2년 전이나 지금이나 비슷한 수입니다.
    블로그 주제가 꽤 이상한 혼합형 타입이죠? ㅋㅋㅋㅋ
    URL에서 tc를 빼고 도메인만 입력해서 접속하면 대문 페이지도 나오는데, 그것도 보셨나 궁금합니다.

    Reply
    • Favicon of http://www.benjaminlog.com BlogIcon 김재호 at 2012/05/14 10:14 [edit/del]

      예, 메아리 저널이나 절대공간 글은 하도 많이 읽어서 친한 사이라는 것은 알고 있었습니다. 당연히 대문 내용도 읽어보았었고요.
      만나서 반가워요. 앞으로도 좋은 글 많이 써주세요.

  4. 박우석 at 2012/05/16 01:45 [edit/del]

    자괴감 블로그 보고 왓는데
    난 누구인가 여긴 어디인가
    귓속을 맴도네요 ㅋㅋ

    Reply
  5. 이매` at 2013/01/25 18:51 [edit/del]

    네이트온이메일좀 알려주세용 ㅋ

    Reply
  6. busim at 2013/02/28 06:44 [edit/del]

    와~우! 글을 너무 재미있게 써서 귀에 쏙 들어옵니다 잘 놀다 갑니다 종종 들어와도 될까요?

    Reply

submit

CString 의 비밀

2012/04/12 11:55 | Programming
다음 코드를 실행시켜보면 "abc"가 잘 출력이 될까?

CString s = L"abc";

wprintf(L"%s", s);


그렇다. 잘 출력된다.

대부분의 사람들은 이 부분을 원래 그런거 아니었어? 하고 대수롭지 않게 넘어간다.


어잉, 그런가?


그렇다면 다음 코드는 어떨까? 이번엔 CString이 아니라 std::wstring을 사용했다.

std::wstring s = L"abc";

wprintf(L"%s", s); // this is wrong


컴파일은 잘 되지만 이번엔 제대로 출력되지 않는다. 형식 문자열에 포인터를 넣지 않고 wstring 객체 자체를 쑤셔 넣었기 때문이다. 사람들이 흔하게 하는 실수이며, 경험있는 프로그래머들이 형식 문자열을 사용하지 말라고 하는 이유이다.


제대로 출력하려면

wprintf(L"%s", s.c_str());

이렇게 써주어야 한다.


아니 그런데 왜 CString은 그냥 넣어도 잘 되고 std::wstring은 .c_str() 함수를 호출해야만 되는걸까. CString에 뭔가 비밀이 있는건가.


있다.


조금 더 주의 깊은 사람들은 CString의 구현을 살펴보고 LPCWSTR에 대한 형변환 연산자가 정의되어 있는 것을 발견해낸다. 아, 형변환 연산자가 있기 때문에 자동으로 형 변환이 되는거구나.


하지만 이는 형변환 연산자 때문이 아니다.


가변 인자 함수로 들어갈 때는 형변환이 발생하지 않는다. 함수 파라메터가 '가변'이고 무슨 타입이 들어올지도 적혀 있지 않는데 어떤 타입으로 형변환을 하는가?


CString의 코드가 잘 동작할 수 있는 비밀은 CString의 멤버 변수가 실제 문자열 데이터를 가리키고 있는 포인터 m_pszData 하나 밖에 없기 때문이다. 가상 함수도 없다. 객체의 맨 앞에 포인터를 가지고 있다는 것이 중요하다.


그림으로 그려보면 이렇게 생겼다.

재밌지 않은가? 문자열 클래스가 문자열 길이를 위한 변수도 가지고 있지 않고 달랑 WCHAR* 하나만 가지고 있다니.

하지만 CString의 은밀한 곳에서는 메모리를 조금 더 할당해서 문자열 길이나 레퍼런스 카운트를 저장하고 CString에게는 pszData 하나만 노출시켜 놓는다. 그리고 이렇게 설계한 이유는 사람들이 저런 실수를 하더라도 잘 동작하도록 만들고 싶었기 때문일 것이다.


하지만 잘 동작한다고 해서 그렇게 쓰는 것이 옳다는 것은 아니다. 가변인자의 파라메터로는 POD 만 들어갈 수 있다. NON-POD 타입이 들어갈 경우에는 모두 undefined behaviour이다. 나중에는 컴파일러의 구현 변경에 따라 동작하지 않게 될 수도 있다는 뜻이다. -말은 이렇게 하지만 그런 일은 좀처럼 발생하지 않는다.


어쨌거나,

wprintf(L"%s", (LPCWSTR)s); 또는

wprintf(L"%s", s.GetString());


이라고 써주어야 올바른 C++ 문장이다.


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

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

  1. 스비 at 2012/04/12 13:42 [edit/del]

    유익한 내용 잘 배우고 갑니다. 항상 재밌게 글 읽고 있습니다. 고맙습니다.

    Reply
  2. BlogIcon Scavenger at 2012/05/27 11:02 [edit/del]

    유익한 내용 잘 보고 배웁니다.. 흥미로운 내용이네요^^

    Reply
  3. 이준배 at 2012/06/18 17:22 [edit/del]

    배우고 갑니다 글 남겨주셔서 감사합니다

    Reply

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 플래그를 사용하면 훅을 완전히 무시하고 오퍼레이션을 수행할 수도 있다.


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

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

submit
이것은 윈도우 프로그래밍 이야기.
길을 걷다 보면 간혹 듣게 되는 이야기. 는 아니고 소프트웨어 회사의 사무실이나 엘레베이터에서 가끔 들을 수 있는 이야기.
메인 쓰레드가 종료되면 프로세스도 종료되는 것 아닌가요?
어잉. 그런가?

답은 아니오이다. 메인 쓰레드가 종료된다고 프로세스가 종료되는 것은 아니다.
ExitProcess 같은 함수를 통해 명시적으로 프로세스를 종료시키거나, 모든 쓰레드가 종료되었을 때 프로세스는 종료된다.

그럼 사람들은 왜 저런 미신을 가지게 되었을까. 아마도 메인 함수에서 리턴할 때 다른 쓰레드들이 존재하더라도 프로세스가 종료되는 것을 봐왔기 때문일 것이다.
그렇다. 메인 함수가 리턴하면 다른 쓰레드들이 잘 살아 있더라도 프로세스가 종료된다.
하지만 메인 쓰레드가 종료되었기 때문에 프로세스가 종료되는 것은 아니다. 이 때 프로세스가 종료되는 이유는 메인 함수가 끝나고 CRT에서 exit 함수를 호출 해주기 때문이다. exit()는 물론 내부적으로 ExitProcess를 호출한다.

다른 쓰레드를 생성해 놓은 뒤 메인 함수 내에서 ExitThread 함수를 사용해 메인 쓰레드만 종료시켜 보면 프로세스가 종료되지 않는다는 것을 살펴 볼 수 있을 것이다.
저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

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

submit
C++에서 함수 이름이 맹글링 되는 것은 다들 알고 있는 사실일 것이다.
링커 오류를 만나게 되면 원인을 분석하기 위해 때때로 함수의 오리지날 이름이 알고 싶을 때가 있다. 마이크로소프트의 도구로 만들어진 함수들에 대해서는 undname 이라는 프로그램이 바로 그런 일을 해준다.

레이몬드 첸의 한 포스트를 읽다가 그는 단순한 함수들에 대해서는 undname을 사용하지 않고도 한눈에 척 알아볼 수 있는 것 같다는 느낌을 받았다.
어떻게 그런 일을 할 수 있는지 알아보다가 강성훈씨가 쓴 이 포스트를 보게 되었다. 그가 말하는 것처럼 아마 현재까지 가장 잘 정리된 자료라는데 동의한다. 어린 나이에 정말 대-_-단 하다고 밖에는 말할 수 없다.

...하지만 나는 한참 살펴보다가 결심했다.
'빌어먹을. 나는 그냥 undname이나 써야겠다.'

흐아, 생각처럼 간단한 일이 아니었다.
그런데 레이몬드 첸은 정말 간단한 함수 정도는 추측할 수 있는 것일까? 아직도 잘 모르겠다.
저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

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

  1. gcd at 2012/02/22 06:38 [edit/del]

    msvc로 mangling 당한 이름에 처음 접했을 때 꽤나 당황했었지요.
    저 룰도 컴파일러마다 다른 모양이라 (거기에다 msvc 9.0부터 규칙-이라기보단 인코딩-이 바뀌었다고 하고...) 링크 시간에 호환이 되긴 하나 하는 의문도 들더라구요.
    여튼, 저 암호(?)를 풀 수 있다는 사실을 알았던건 Dependency walker를 써보면서였군요. 포스트와 다르게 DLL 파일의 export/import된 함수들을 볼 수 있지만, C++ 로 만든(그것도 msvc로 컴파일 한) 몇 비공개 라이브러리에 들어있는 멤버 함수들을 다 볼 수 있어서 신났던 기억이 나네요.

    Reply
    • Favicon of http://www.benjaminlog.com BlogIcon 김재호 at 2012/02/22 10:22 [edit/del]

      저는 9부터 바뀐줄 몰랐습니다. 그런데 그런 걸 바꾸면 부작용이 엄청날텐데. 호환성은 어떻게 맞춰줬을지 궁금하군요.

    • gcd at 2012/02/22 21:19 [edit/del]

      룰이 바뀌었다기보단, 인코딩 처리하는 방법이 ANSI에서 UTF-8로 바뀌었다고 하더라구요.
      대부분 영문 명칭을 쓰기 때문에 임팩트가 거의 없을 것입니다.
      아니, 임팩트가 없었으면 좋겠는데, 8.0 이전에 만든 obj와 이후에 만든 obj 사이에서 가끔 링크 오류가 나긴 하나보더라구요. msvc 8.0용으로 땜빵(?) 패치가 나왔었고(이것도 주로 일본어권의 요구사항으로... 그러고보니 바뀐건 9가 아니라 8부터였네요.) 9.0 이후로는 지원 계획이 없다고 하네요.

      적고보니 mangling이랑 별로 상관 없네요. 헷갈리게 한 것 같아서 죄송합니다.

submit
C++에서는 멤버 함수에 const 키워드를 사용할 수 있다. 이는 메서드 내에서 멤버 변수들의 값을 바꾸지 않겠다는 약속이다.
void Clazz::foo() const
{
}
위의 const 변경자는 해당 인스턴스의 this 포인터에 영향을 끼치게 된다. 즉 멤버 함수 내에서 this 포인터의 타입은 const Clazz* 가 된다. 그러므로 해당 멤버 함수 내에서 멤버 변수의 값을 바꾸려고 하면 컴파일 에러가 발생한다.

static void Clazz::boo() const
{
}
하지만 static 멤버 함수에 대해 const를 붙일 경우에는 컴파일 에러가 발생한다. 그 이유는 static 멤버 함수는 this 포인터를 가지고 있지 않기 때문이다. this가 없는데 어떻게 this를 const로 만들겠는가.
그래서 질문에 대한 답은 '아무런 의미가 없는 짓이기 때문' 이라고 할 수 있겠다.
저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License

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

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

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

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

  1. win32API공부학생 at 2013/12/31 20:56 [edit/del]

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

    Reply

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
  2. gcd at 2012/02/22 06:52 [edit/del]

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

    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) 이라고 생각이 되어요.

  4. gcd at 2012/02/22 06:47 [edit/del]

    안녕하세요. 혹시 스캇형이 EC++11을 내놓지 않았을까, 하는 정보를 찾아 헤매다 들렀습니다.
    처음(?) 들렀다 생각했는데 변수 자리에 놓는다는 typedef 관한 글을 봤던 기억이 나네요.

    어쨌든, 말씀하신 스타일이 저와 매우 비슷해서 그냥 지나갈 수가 없겠더군요 ^^;
    은근한(?) 스페이스의 매력이 있는 것 같습니다. (웹쪽에서는 사이즈적인 손해가 좀 있지만 크게 신경 쓸 것도 아니고...)

    0 == foo는 좋다길래(?) 따라해보려 했지만 몸이 거부했기에 결국 포기하고 말았었네요.
    (마침 최근 댓글에 조엘에 관한 것이 있어서 떠오르는데, 그 사람도 '정말 효과가 있든 없든' 그런 방어적 프로그래밍 습관을 가진건 좋은 징조라고... 면접하는 법이었나... 거기에서 그랬던 것 같네요.)

    저는 bar == true는 보고 있으면 피부에 뭔가 기어가는 기분이 드는군요. (return 타입 bool일 때 int를 바로 넘기면 경고 뜨는 걸 보는 기분으로요...)
    하여간 if ( a && b == true ) 같은 상황이 되어버리면 연산자 우선순위까지 괜히 헷갈려서 좋지 않은 것 같습니다.

    가독성 면에서는 if (! boo) 하고 ! 다음에 공백을 하나 주는 것도 괜찮을 것 같네요.
    여기에 목숨 걸면 #define if_not(x) if (!(x)) 해놓고 쓰는 것도 나쁘지 않을지도..(후자는 농담입니다 ㅠㅠ)

    Reply
    • Favicon of http://www.benjaminlog.com BlogIcon 김재호 at 2012/02/22 10:16 [edit/del]

      예, 조엘이 면접하는 법에서 그런 말을 했었죠.
      1. 상수를 좌측에 쓰면 좋은 징조이다
      2. C++에서는 소멸자를 항상 가상 소멸자로 선언해야한다.

      저는 조엘 온 소프트웨어를 아주 재밌게 읽었고 거의 모든 내용에 동의하는데 딱 저 2가지에 대해서만은 반대입니다^^

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
  3. gcd at 2012/02/22 06:43 [edit/del]

    여담이지만, 여기에 대고
    if (p != NULL) ...
    이라고까지 써놓은 걸 보면 정말 몸이 움찔(?)하더군요.

    Reply
  4. Favicon of http://blog.daum.net/knightofelf BlogIcon shint at 2013/10/26 21:34 [edit/del]

    이것은 중요합니다... ㅠㅠ...
    if(p != NULL)
    {
    free(p);
    p = 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
  2. 오곡 at 2013/07/09 18:09 [edit/del]

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

    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 매크로는 다음과 같이 생겼다.
#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' 카테고리의 다른 글

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

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

  1. 오곡 at 2013/08/07 18:14 [edit/del]

    잘배우고 갑니다~!

    Reply

submit

PAGED_CODE 매크로

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

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

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

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

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

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

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

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

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

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

'Programming' 카테고리의 다른 글

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

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

  1. 오곡 at 2013/07/09 17:40 [edit/del]

    잘배우고 갑니다~!

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

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

    Reply

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
  5. 겨울악령 at 2013/07/18 03:17 [edit/del]

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

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

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

    이거 보니 딱 이해가 되네요

    좋은 내용 감사합니다.

    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
  3. 오곡 at 2012/12/05 00:57 [edit/del]

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

    Reply
  4. at 2013/07/21 21:49 [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