반응형
출처 : http://www.microsoft.com/korea/msdn/msdnmag/issues/06/05/x64/default.aspx#top

X64 프라이머

64비트 윈도우를 프로그래밍 하기 위해서 알아야 할 모든 것들



이 문서 내용:
  • 64 비트 윈도우의 기초
  • X64 아키텍트의 간략한 내용
  • Visual C++ 2005로 x64용 소프트웨어 개발
  • X64용 빌드를 위한 디버깅 기술
이 문서에는 다음 기술이 사용됩니다:
Windows, Win64, Visual Studio 2005




Matt Pietrek (영문)

목차
x64 운영체제
x64의 간략한 내부 구조
Visual C++로 x64용 어플리케이션 개발
Win64 호환되는 코드 만들기
디버깅
매니지드 코드는 어떻게 하나요?
최종 정리

로운 64비트 윈도우에서 일했던 경험의 좋았던 점 중의 하나는, 새로운 기술이 어떻게 동작하는지 눈으로 확인할 수 있다는 것이었습니다. 저 자신은 특히 어떤 운영체제 밑바닥에 대해서 조금이라도 알기 전까지는, 그 운영체제에 대해서 그렇게 편안함을 느끼지 못하는 편입니다. 그래서, 64비트 Windows XP와 Windows Server™ 2003이 나타났을 때, 저는 아주 열심히 그 운영체제에 대해서 연구하였습니다.

Win64와 x64 CPU의 좋은 점은, 그 전의 CPU와 조금 다른 구조를 가지고 있지만, 그 차이를 배우는데 그렇게 많은 시간이 요구되지 않는 다는 점입니다. 저희 같은 개발자들에게는, x64로의 이동이 단지 컴파일만 다시 하면 끝나는 그런 작업이었으면 좋겠지만, 그렇게 생각하고 작업을 하신다면, 앞으로 디버거에서 너무 많은 시간을 보내셔야 할 것 같습니다.

이 글에서, 저는 Win64와 x64의 내부구조에 대한 저의 지식을 종합해서 유능한 Win32® 프로그래머가 x64로 이동하기 위해서 꼭 필요한 지식들 제공하겠습니다. 저는 여러분이 이미 Win32의 개념과 기본 x86개념, 그리고 왜 여러분의 코드가 Win64에서 동작 해야 하는지에 대해서 이미 알고 계신다고 가정하겠습니다. 그렇게 해야지 제가 좀 더 핵심적인 것들에 집중할 수 있을것 같습니다. 여기서의 이 요약이 x86 내부구조와 Win32에 대한 알고 있는 여러분의 지식에 관련된 상대적으로 중요한 x64와의 차이점에 대한 고찰이라고 생각하시기 바랍니다.

x64 시스템에서 한 가지 좋은 점은, 아이템니윰(Itanium)기반 시스템과는 다르게 여러 분이 심각한 효율 저하에 대한 고민을 하지 않고, Win32 혹은 Win64를 동일한 기계에서 이용하실 수 있다는 점입니다. 그리고 인텔과 AMD의 x64 구현에 조금 불명확한 몇 가지의 차이가 존재함에도 불구하고, x64용 윈도우는 둘 중 어느 곳에서나 동작합니다. 여러분이 AMD x64와 Intel x64 시스템을 위해서 각각 다른 버젼의 윈도우가 필요하지 않습니다.

저는 여기서 운영체제의 구현의 몇 가지 자세한 내용들, x64 CPU 내부 구조에 대한 개괄적인 설명, 그리고 Visual C++로 x64용 프로그램 개발 영역으로 이 글을 나누어서 진행하도록 하겠습니다:.


페이지 맨 위로페이지 맨 위로


x64 운영체제

저는 어떤 윈도우 내부구조를 설명하더라도, 처음에는 메모리와 주소 공간에서부터 시작합니다. 비록 64비트 프로세서가 이론적으로는 16 엑사 바이트(exabytes) 의 메모리에 접근할 수 있다고 하더라고, Win64는 현재 16 테라바이트(terabytes), 44비트 만을 지원하고 있습니다. 왜 64비트 전부를 사용해서, 16 엑사바이트 전부를 쓸 수 없었을 까요? 거기에는 몇 가지 이유가 존재합니다.

맨 처음 이유로는, 현재 x64 CPU들이 물리적인 메모리 공간에 접근할 때, 오직 40비트(1테라바이트)만을 허용합니다. 그 제한이 없어진다고 하더라고, 지금 현재의 하드웨어가 아닌, 향후에 나올 수 있는 CPU들의 내부구조들이 오직 52비트 (4페타바이트)만큼만 확장될 수 있습니다.그 정도만 해도, 그 많은 메모리를 매핑하기 위한 페이지 테이블의 사이즈는 어마 어마 할 것입니다.

Win32에서와 같이, 접근할 수 있는 주소 공간은, 사용자와 커널의 영역으로 나누어 집니다. 커널 모드의 코드가 8 테라바이트 상위에서 모든 프로세스에 의해 영역되고, 각각의 프로세서는 8 테라바이트 이하에 자신의 고유 영역을 가집니다. 64비트 윈도우의 각 버젼들은 그림 1과 그림 2에서 보여지는 것처럼, 서로 다른 물리적인 메모리 한계를 가지고 있습니다.

Win32에서와 같이, x64의 페이지 사이즈는 4KB 입니다. 처음의 64KB 공간은 절대로 매핑 되지 않기 때문에, 여러분이 볼 수 있는 맵핑 주소중 가장 낮은 번지는 0x10000입니다. Win32에서는 다르게, 시스템 DLL들은 사용자 모드 주소 공간의 제일 위 부분과 근접해 있는, 기본 로드 어드레스라는 것이 없습니다. 그 대신, 시스템 DLL들은 4GB 위의 공간에 적재 됩니다. 통상적으로, 그 주소는 0x7FF00000000 (8 테라바이트) 근처입니다.

새로운 x64 프로세서의 좋은 기능 중에 하나는, 윈도우가 하드웨어적으로 데이터 실행 보호(Data Execution Protection, 이하 DEP)를 할 수 있게 지원해 준다는 것입니다. x86 플랫폼에서는, 많은 버그와 바이러스가 CPU가 데이터를 올바른 코드 바이트로 인식하고 실행했기 때문에 존재할 수 있었습니다. 실수이든 혹은 고의적인 버퍼 오버런(buffer overrun)이 원래는 데이터 저장을 목적으로 했던 메모리 블락을 CPU에서 명령어로 인식하고 실행해 버리는 결과가 발생하곤 했었습니다. 이 DEP의 도움으로, 운영체제는, 의도한 코드 영역의 경계를 명료하게 설정할 수 있고, 이 의도된 경계를 벗어나는 코드 실행에 대해서는 CPU가 일종의 덫을 놓을 수 있게 되었습니다. 이 기능은 윈도우를 악의적인 공격에 덜 취약하게 만드는데 큰 도움을 줄 수 있습니다.

에러들을 잡기 위해서 고안된 장치들 중에 하나가, x64 링커가 실행파일에 대한 기본 적재 주소를 32비트 (4GB) 이상으로 설정한 것입니다. 이것은 코드가 Win64로 포팅된 후, 기존에 있던 코드에서 위에서 이야기한 보안에 문제를 일으키는 부분을 찾기 쉽게 만들어 줍니다. 특히, 만약 포인터가 32비트 값(예를 들어, DWORD)값으로 저장되어 있으면, 그러면, 그 값은 Win64 빌드를 동작시킬 때, 값이 일부분이 짤림 으로서, 포인터를 무효화 시키고, 접근위반(access violation)을 일으킵니다. 이러한 잔기술은 지저분한 포인터 버그를 찾기 쉽게 만들어 줍니다.

포인터와 DWORD에 대한 주제들도 Win64의 타입을 이야기하는데 빠질 수 없습니다. Win64에서 포인터의 크기가 얼마나 될까요? LONG형의 길이는요? 그리고, 핸들과 HWND의 크기는 얼마나 될까요? 다행스럽게도, 마이크로 소프트가 Win16에서 Win32로의 좀 지저분한 변환을 하면서, 새로운 자료형의 모델에 대해서는 64비트 까지의 확장이 쉽게 될 수 있도록 만들었습니다. 일반적으로 이야기 해서, 몇 가지 예외를 제외하고, 새로운 64비트 세계에서도, 포인터와 size_t를 제외한 모든 나머지 자료형 들은 Win32에서와 동일한 길이를 가지고 있습니다. 즉, 64비트 포인터는 8바이트인 반면에, int,long, DWORD는 아직도 4바이트입니다. 후반부에 Win64 개발에 대해서 이야기 하면서, 이 자료형에 대해서 좀 더 이야기 하도록 하겠습니다. [Editor's Update - 5/2/2006: 핸들은 포인터 값으로 선언되었습니다. 그래서, Win64에서는 4바이트가 아닌 8바이트 값입니다.]

Win64의 포맷은 PE32+라고 불립니다. 거의 모든 관점에서 이 포맷은 Win32PE 파일과 거의 구조적으로 동일합니다. ImageBase 같은 몇 몇의 포맷은 더 크기가 커졌고, 한 필드는 없어졌고, 그리고 다른 하나의 필드는 다른 CPU 타입을 반영할 수 있도록 변경되었습니다. 그림 3 은 바뀐 필드들을 보여 줍니다.

PE 헤더를 말고는, 그렇게 많이 바뀐 부분이 없습니다. IMAGE_LOAD_CONFIG, IMAGE_THUNK_DATA같은 몇 몇 구조체들은 단순히 필드들을 64비트로의 확장을 했을 뿐입니다. PDATA 섹션의 추가는 Win32와 Win64 구현의 주된 차이점중의 하나를 두드러지게 한다는 점에서 아주 흥미롭습니다: 그 차이는 바로 예외 처리(exception handling) 입니다.

x86 세계에서는, 예외 처리는 스택에 기반했었습니다. Win32 함수가 try/catch 혹은 try/finally 코드를 가지고 있을 때, 컴파일러는 스택에 작은 데이타 블락을 만들어 놓는 코드를 생성했었습니다. 추가로, 각각의 데이타 블락은 이전 try 데이타 구조체를 가리켰었습니다. 그래서, 최근에 추가된 구조체가 리스트의 헤드가 되는 링크드 리스트를 생성했었습니다. 함수가 불려지고, 종료될 때, 링크드 리스트의 헤더는 계속 갱신이 되었었습니다. 그러다, 예외가 발생하는 경우, 운영체제가 스택에 있는 링크드 리스트의 블락을 살펴서, 적절한 핸들러를 찾는 방식으로 이루어져 있었습니다. 저의 1997년 MSJ article (영문) 에서 좀 더 자세한 내용을 찾아 보실 수 있습니다.

Win32 예외처리와는 반대로, Win64 (x64와 아이템니움(Itanium) 버젼 둘 다 해당됩니다.)에서는 테이블 기반의 예외 처리를 사영합니다. 더 이상 스택 위에 있는 try 데이타 블락의 링크드 리스트는 없습니다. 대신, 각각의 Win64 실행파일은 런타임 함수 테이블을 가지고 있습니다. 각각의 함수 테이블은 함수의 시작 주소와 끝 주소를 가지고 있을 뿐만 아니라, 함수의 스택 프레임 레이아웃과 예외 처리 코드에 관련된 데이타들의 위치 역시 가지고 있습니다. 이 구조체들의 핵심을 보기 위해서 WINNT.H안에 들어 있는 x64 SDK안에 WINNT.H 안에 있는 IMAGE_RUNTIME_FUNCTION_ENTRY 구조를 살펴 보시기 바랍니다.

예외가 발생했을 때, 운영체제는 쓰레드 스택을 하나씩 탐색합니다. 스택을 탐색하면서 각각의 프레임을 탐색하고, 저장된 인스트럭션 포인터를 찾아서, 운영체제가 어떤 실행 모듈안에 인스트럭션 포인터가 있는지를 결정합니다. 그리고 나서, 운영체제는 런타임 함수 테이블을 그 모듈에서 찾아서, 적절한 런타임 함수를 찾아서, 데이타로 부터 적절한 예외 처리 결정을 내려 줍니다.

만약, 여러분이 로켓 과학자이고, PE32+ 모듈 없이 직접적으로 메모리에서 코드를 생성했으면 어떻게 될까요? RtlAddFunctionTable API를 이용하여, 운영체제에게 여러분이 동적으로 생성한 코드에 대해서 알려 줄 수 있습니다.

테이블 기반의 예외 핸들링의 단점은 (x86 스택 기반 모델에 비해 상대적으로) 함수 테이블을 찾아 보는 것이, 링크드 리스트에서 값을 찾는 것 보다 훨씬 시간이 많이 걸린다는 점입니다. 장점은, 함수를 실행시킬 때 마다 매 번 try 데이타 블락을 생성시키는 오버헤드가 없다는 점입니다.

꼭 기억하세요! 이 글이 아무리 재미있고, 흥미로워도, 이 글은 x64 예외 핸들링에 대한 자세한 설명이라기 보다, 간단한 소개에 불과합니다. x64의 예외 핸들러에 대한 좀 더 깊은 지식을 알고 싶으시다면, Kevin Frei의 블로그 (영문) 을 꼭 한 번 읽어 보시기 바랍니다.

x64에 호환되는 윈도우에 새로운 API가 그렇게 많지는 않습니다; 거의 모든 새로운 Win64 API들은 아이템니움(Itanium) 프로세서를 위한 윈도우 출시 때 이미 추가되었던 것들 입니다. 간단하게, 그 API들 중 가장 중요한 두 개의 API는 IsWow64Process와 GetNativeSystemInfo 입니다. 이 함수들은 Win32 어플리케이션이 자기 자신들이 Win64에서 돌고 있는지의 여부를 알려 줍니다. 그래서, 만약 64비트 환경에서 동작하고 있다면, 시스템의 진짜 사양(capability)를 올바르게 결정할 수 있게 해 줍니다. 반면에, 32비트 프로세스는 GetSystemInfo 함수를 호출하고, 오직 32비트 시스템인 것처럼, 시스템의 사양(capability)를 볼 수 있습니다. 예를 들어, GetSystemInfo는 32비트 프로세스 주소 영역만을 보고 합니다. 그림 4 는 x86에서는 사용할 수 없고, 오직 x64에서 쓸 수 있는 API들을 보여 주고 있습니다.

전부 다 64비트로 동작하는 윈도우 시스템이 아주 멋지게 들리겠지만, 현실적으로 여러분은 잠시 동안 Win32 코드를 필요로 하게 될 것 같습니다. 그러한 작업을 위해서, x64 버젼의 윈도우는 Win32와 Win64 프로세스를 동시에 동일한 시스템에서 동작시킬 수 있는 WOW64 서브시스템이 포함되어 있습니다. 그러나, 여러분의 32비트 DLL을 64비트 프로세스로 올리거나 혹은 반대의 일들은 지원되지 않습니다. (저를 믿으세요, 아주 좋은 일입니다.) 그리고 마침내 여러분은 구닥다리 16비트 코드에게 잘 가라고 인사를 할 수 있게 되었습니다!

x64 버젼의 윈도우에서는, 프로세서는 오직 Win64 DLL들만 로딩할 수 있는, Explorer.exe 같은 64비트 실행 파일에서부터 시작될 수 있습니다. 반면에, 32비트 실행 파일에서 시작한 프로세스는 오직 Win32 DLL 들만 로딩할 수 있습니다. Win32 프로세스가 커널 모드의 함수를 호출할 때-예를 들어서 파일을 읽는다든지-WOW64는 그 함수를 조용히 가로채서, 올바른 x64 코드의 주소를 주어서 호출하게 합니다.

물론, 서로 다른 종족(32비트와 64비트) 프로세서들끼리 통신할 일도 생길 수 있습니다. 운좋게도, Win32에서 여러분이 사랑하고 좋아했던 모든 프로세스간 통신 방법은 Win64에서도 동작합니다. 쉐어드 메모리(shared memory), 네임드 파이프(named pipe), 그리고, 기타 이름이 있는 동기화 객체들을 포함해서 말입니다.

여러분이 혹시 "그럼 시스템 디렉토리도 Win32와 Win64가 동일한가?"라고 생각하실 지도 모르겠습니다. 동일한 디렉토리가 32 비트와 64 비트 KERNEL32 나 USER32 등과 같은 동일한 이름의 시스템 DLL들을 동시에 가질 수 없습니다, 그렇지요? WOW64는 요술같이 파일 시스템의 리다이렉션(redirection)을 통해서 이 문제를 해결합니다. Win32 프로세스에서의 파일에 대한 쓰기 혹은 읽기 등이 발생하면, SysWow64라는 디렉토리에 있는 커널의 함수를 호출하는 것이 아닌, System32에 있는 커널의 함수를 호출하게 합니다. WOW64가 안보이게 SysWow64 디레토리로 요청한 것을 조용히 바꾸어 주는 것입니다. 그래서, Win64 시스템이 효과적으로 두 개의 시스템 디렉토리, 하나는 x64 용 바이너리들과 또 하나는 Win32용의 바이너리를 가지는 것 입니다.

약간 혼란스러울 수 있지만, 이러한 내부적인 처리는 상당히 부드러운 것처럼 보입니다. 제가 System32 디렉토리의 Kernel32.dll에서 Dir을 실행했을 때, SysWow64 디렉토리에서 했던 것과 정확히 똑같은 결과를 볼 수 있었습니다. 파일 시스템의 리다이렉션이 이런 방식으로 동작하는 것을 정확히 이해하기 까지, 제 자신은 머리가 좀 많이 아팠었습니다. 여러분이 x64 어플리케이션에서 32비트 WindowsSystem32 폴더를 알기를 정말로 원하신다면, GetSystemWow64Directory 라는 API가 여러분께 정확한 경로를 전달해 줄 것 입니다. 그래도, 전체 내용을 알기 위해서 MSDN 문서를 꼭 읽어 보시기 바랍니다.

파일 시스템의 리다이렉션이외에도, WOW64가 해주는 또 다른 마법 중의 하나가 레지스트리 리다이렉션입니다. 제가 아까 Win64 프로세스에서는 Win32 DLL들을 불러오지 않는다고 했던 말을 생각해 보시고, 그리고 COM 과 in-process 서버 DLL을 불러올 때, 레지스트리를 이용하는 것을 생각해 보시기 바랍니다. 만약, Win32 DLL에 구현되어 있는 COM 오브젝트를 64비트 어플리케이션이 CoCreateInstance를 이용해서, 생성하려고 하면 어떻게 될까요? DLL이 올라올 수 없습니다, 맞지요? WOW64는 Win32 어플리케이션으로부터의 접근을 SoftwareClasses 레지스트리 노드로 리다이렉션 해 줍니다. 결과적으로 Win32 어플리케이션에서 보는 레지스트리 구조는 x64 어플리케이션에서 보는 것과 서로 다르게 됩니다. 그리고, 여러분이 기대하시는 대로, 운영체제는 32비트 어플리케이션이 RegOpenKey와 그 계열 함수군을 이용하여, 실제로는 64비트인 레지스트리에 접근하려고 할 때, 내부적으로 새로운 플래그 값을 주어서, 그 값들에 접근할 수 있게 합니다.

약간만 더 깊숙이 들어가서, 쓰레드 로컬 데이타 영역도 살펴 보아야 합니다. x86 버젼의 윈도우에서는, FS 레지스터가 각 쓰레드의 메모리 영역과 가장 마지막 에러(GetLastError로 확인할 수 있는 에러 값), 그리고 쓰레드의 지역 저장 영역(TLS: Thread Local Storage, TlsGetValue로 값을 얻을 수 있는) 에 사용되었습니다. x64 버젼의 윈도우에서는, FS 레지스터는, GS 레지스터로 교체되었습니다. 그 외에는 거의 동일한 방식으로 x32와 x64의 운영체제가 동작합니다.

비록, 이 글이 x64의 사용자 입장에 초점을 두고 있기는 하지만, 커널 모드의 내부 구조에서 한 가지 추가된 중요한 점이 있습니다. PatchGuard라고 불리는 새로운 기술이 x64 윈도우에 추가되었습니다. 이 기술은 보안과 견고함을 위한 목적으로 추가되었습니다. 작게는 syscall 테이블이나 인터럽트 디스패치 테이블(interrupt dispatch table-IDT)를 변경하는 사용자 프로그램이나 드라이버들은 보안상의 문제와 잠재적인 안정성의 문제를 일으켜 왔었습니다. x64의 내부에서는, 그러한 방식으로 커널의 메모리를 지원되지 않는 방식으로 바꾸는 방식이 허용되지 않습니다. 이러한 것을 강화시키는 기술이 PatchGuard 입니다. 이 기술은 중요한 커널 메모리의 위치가 바뀌는 것은 커널 모드의 쓰레드에서 항상 감시합니다. 그리고 메모리가 바뀌면, 시스템은 버그체크를 통하여 멈춰 버립니다.

모든 것을 고려해 보아도, 만약 여러분이 Win32의 내부 구조에 어느 정도 알고 있고, 코드를 쓸 줄 알고, 동작하는지를 알고 있으면, Win64로의 이동에 있어서 크게 놀라지 않으실 겁니다. 거의 대부분은 좀 더 넓은 환경으로의 이동이라고 간주하셔 됩니다.


페이지 맨 위로페이지 맨 위로


x64의 간략한 내부 구조

자 이제, CPU의 구조 자체에 대해서 조금 살펴 보기로 하겠습니다. 왜냐하면, 기본적인 CPU의 명령어(instructions)에 대해서 알고 있는 것이, 개발(특히 디버깅!)을 훨씬 쉽게 만들기 때문입니다. 처음에 여러분이 알아 차릴 수 있는 것은, 컴파일러가 생성한 x64 코드가 여러분이 알고 있고, 사랑하는 x86 코드와 거의 흡사하다는 점입니다. IA64 코딩의 경우는 그렇게 유사하지 않았었습니다.

그리고, 두번째로 여러분이 알아차릴 수 있는 것은, 레지스터 이름이 여러분이 사용하던 것들과 조금씩 다르고, 레지스터 자체도 조금 많다는 점 입니다. 일반적인 용도의 x64 레지스턷들의 이름은 R로 시작합니다. 예를 들어서, RAX, RBX, 이런 것들이 있습니다. 이것들은 E이름을 가지고 있는 32비트 x86 레지스터들의 확장입니다. 아주 오래 전에, 16비트 AX 레지스터가 32비트 EAX가 되고, 16비트 BX 레지스터가 32비트 EBX가 되었던 것 처럼 말입니다. 32비트로 전이가 될 때 생겨난 E 레지스터들은 64비트로 이동하면서는 R 레지스터들이 된 것이죠. 그래서, RAX는 EAX의 계승자이고, RBX는 EBX의 계승, RSI는 ESI, 그런 식으로 확장되었습니다.

추가로, 8개의 일반적인 용도의 레지스터 (R8-R15)가 추가되었습니다. 64비트에서 주로 쓰이는 일반적인 용도의 레지스터들은 그림 5 와 같습니다.

물론 32비트 EIP 레지스터도 RIP 레지스터가 되었습니다. 그리고 32비트 명령어도 여전히 계속 동작하고 32비트 레지스터는 물론이고16비트 레지스터도 (EAX, AX, AL, AH등과 같은) 여전히 유효합니다.

그래픽 작업을 하거나 혹은 과학적인 연산이 필요한 고수들이 사용할 수 있도록, x64 CPU는 여전히 XMM0에서 XMM15로 명명된 16개의 128비트 SSE2 레지스터를 가지고 있습니다. 그 외 여기서 이야기하지 않는 다른 x64 레지스터들에 대한 모든 정보들은 WINNT.H 안에서 _CONTEXT로 적적하게 #ifdef된 구조체에서 찾아 보실 수 있습니다.

아뭏튼, x64 CPU는 언제라도 구형의 32비트 모드 혹은 64비트 모드 둘 다 에서 동작할 수 있습니다. 32비트 모드에서, CPU는 다른 x86 CPU처럼 명령어를 해석하고, 이에 기반하여 동작합니다. 64비트 모드에서는, CPU는 새로운 레지스터와 명령어를 지원하기 위해서 어떤 특정 명령어 인코딩에 대해서 약간의 사소한 조정을 하였습니다.

만약 여러분이 CPU 오피코드 인코딩 다이아그램(opcode encoding diagram)에 익숙하시다면, 아마도, 새로운 명령어 인코딩을 위한 공간은 빨리 없어진다는 것과 새로운 명령을 위해 여덟 개의 새로운 레지스터를 쥐어짜는 것은 쉬운 일은 아니라는 것을 기억하실 겁니다. 새로운 명령어를 추가하는 방법 중의 하나는, 거의 쓰이지 않는 명령어를 삭제하는 것 입니다. 그래서, x64에서는 기존의 CPU에서 사용되던 몇 개의 명령어가 삭제되었고, 지금까지, 제가 오직 그리워 하는 명령어는 스택의 일반용도 레지스터의 값을 모두 저장했다가, 다시 복원해 주는, 64비트 PUSHAD와 POPAD 입니다. 또 다른 방법 명령어 인코딩 공간을 확보하는 방법은, 64비트에서 더 이상 쓰이지 않는 세그먼트관련 레지스터들을 전부 제거해 버리는 것 입니다. 그래서, CS, DS, ES, SS, FS, 그리고 GS 레지스터가 더 이상을 쓰지 않습니다. 그렇게 많은 사람이 이 레지스터들을 그리워할 것 같지는 않군요.

64비트 주소가 사용됨에 따라, 여러분이 코드 사이즈에 대해서 궁금해 할지도 모르겠습니다. 예를 들어서, 아래의 경우는 흔한 32비트 명령어 입니다:

CALL DWORD PTR [XXXXXXXX]
위에서, X가 된 부분이 바로 32비트 주소입니다. 64비트 모드에서는, 위에서 X가 된 부분이 64비트 주소가 됩니다. 그래서, 5바이트 명령어가 9바이트로 되겠지요? 운 좋게도, 그렇지는 않습니다. 명령어는 여전히 똑같은 사이즈를 유지합니다. 64비트 모드에서는, 명령어의 32비트 오퍼랜드 부분은 현재 명령어에 상대적인 데이터의 오프셋으로 취급됩니다. 예를 하나 들어 보겠습니다. 32비트 모드에서는, 아래의 명령어가 00020000h번지에 있는 32비트 포인터 값을 부릅니다.
00401000: CALL DWORD PTR [00020000h]
64비트에서는, 똑같은 명령어가 0042100h(4010000h + 20000h)에 있는 64비트 포인터 값을 호출합니다. 만약 여러분이 여러분이 스스로 코드를 생성하고 있다면, 조금만 생각해 보아도 이러한 상대 주소 모드가 의미를 가지고 있다는 것을 알 수 있습니다. 여러분은 명령어에 8 바이트 포인트 값을 제공할 수 없습니다. 그 대신에, 여러분은 실제의 64비트 대상 주소가 존재하는 곳의 32비트 상대 경로를 제공해줘야 합니다. 그래서, 64비트 대상 포인터는 반드시 이 포인터를 이용하는 명령어와 2GB 내외로(앞뒤로 각각 2GB씩) 존재해야 한다는 말하지 않아도 알 수 있는 가정이 생깁니다. 거의 모든 사람들에게는 이것이 문제가 될 이유가 별로 없겠지만, 동적으로 코드를 생성한다든지 혹은 기존 코드를 메모리에서 변경하는 경우라면, 문제가 생길 수도 있습니다.

x64 레지스터의 가장 큰 장점 중의 하나는, 컴파일러가 스택 보다 모든 파라미터들을 레지스터에 전달하는 코드를 마침내 생성할 수 있다는 점 입니다. 스택에 파라미터를 구겨 넣는 것은 메모리 억세스를 필요로 합니다. 그리고, 우리는 CPU 캐쉬에 없는 메모리 억세스는 RMA이 그 내용을 가져올 때까지, 몇 사이클 동안 CPU를 잠시 서있게 한다는 사실도 알고 있습니다.

함수 호출 방식(Calling Convention) 의 경우, x64 내부구조를 이용하여 _stdcall, _cdecl, _fastcall, _thiscall 같은 기존에 존재하는 Win32 함수 호출 방식을 모조리 정리할 수 있는 기회를 가졌습니다. Win64의 경우는, 딱 하나의 함수 호출 방식이 존재합니다. _cdecl 같은 방식은 그냥 컴파일러에서 무시됩니다. 이러한 함수 호출 방식의 단일화는 무엇보다도 디버깅을 원활하게 하는데 큰 혜택입니다.

x64의 함수 호출 방식은 fastcall 방법과 유사합니다. x64 호출 방식에서는, 처음 네 개의 정수 인자가 이 목적을 위해 디자인된 레지스터에 전달됩니다:

 RCX: 1번째 정수 인자  
 RDX: 2번째 정수 인자  
 R8: 3 번째 정수 인자  
 R9: 4번째 정수 인자

네 개 이상의 정수 인자는 스택을 통해서 전달됩니다. 그리고 this 포인터는 정수 인자로 간주되어 항상 RCX 레지스터에서 발견될 수 있습니다. 부동 소수점 인자들에 대해서는, 처음 네 개의 인자들은 XMM0에서부터 XMM3 레지스터를 통해서 전달되고, 나머지 부동 인자들은 쓰레드 스택을 통해서 전달 됩니다.

함수 호출 방식에 대해서 조그만 더 깊숙이 들어가면, 인자들이 레지스터를 통해서 전달될 수 있음에도 불구하고, 컴파일러는 RSP 레지스터를 감소시키면서, 여전히 스택에 공간을 예약해 놓습니다. 최소한, 각각의 함수는 반드시 32바이트 (네 개의 64비트 값)을 예약해 놓아야 합니다. 이 공간은 레지스터들이 함수에 전달되어, 잘 알려진 스택 위치에 쉽게 복사되도록 합니다. 물론, 불리는 함수 측에서 함수의 인자를 채우지는 않습니다. 하지만, 필요한 경우에 이러한 스택 공간의 예약은, 함수 인자들이 레지스터에서 쉽게 스택으로의 복사를 가능하게 합니다. 물론, 네 개 이상의 인자가 전달된다면, 적절한 스택의 추가 공간이 반드시 예약되어야 합니다.

예를 한 번 들어 보겠습니다. 어떤 함수가 자식 함수에게 두 개의 정수 인자를 전달하는 경우가 있다고 가정을 한 번 해보겠습니다. 컴파일러는 두 개의 인자를 각각 RCX와 RDX에 각각 전달할 뿐만 아니라, RSP 스택 포인터 레지스터에서 32바이트를 빼놓습니다. 불리는 함수 입장에서는, 파라미터의 값을 RCX와 RDX 레지스터를 통해서 접근 가능합니다. 만약, 불리는 함수 코드가 레지스터가 다른 이유에서 필요할 경우, 이 값들은 예약된 32바이트 스택 영역에 복사됩니다. 그림 6은 6개의 정수 인자가 전달된 뒤의 레지스터와 스택을 보여 주고 있습니다. 그림 6 shows the registers and stack after six integer parameters have been passed.

Figure 6 정수의 전달
Figure 6 정수의 전달

x64 시스템에서의 파라미터 스택 정리는 약간 재밌는 모습을 보여 주고 있습니다. 기술적으로는, 불리는 함수(callee)가 아닌, 부르는 함수(caller)가 스택의 정리를 책임지고 있습니다. 그러나, 여러분은 프로롤그와 에필로그 코드를 제외하고 다른 부분에서 RSP를 조정하는 모습을 거의 보기가 힘들 것 입니다. PUSH와 POP 명령어로 스택에서 인자를 더하거나 빼주는 x86 컴파일러와 다르게, x64 코드 생성기는 (파라미터의 입장에서 보면) 얼마든지 큰 대상 함수에서도 쓸 수 있을 만큼 충분한 스택을 예약해 놓았습니다. 그래서, 자식 함수를 호출 시에, 파라미터를 설정하기 위해 똑같은 스택 영역을 계속 반복해서 씁니다.

짧게 말해서, RSP 레지스터는 거의 변하지 않습니다. 이 점은 ESP 레지스터 값이 파라미터가 스택에 추가되거나 정리되면서, 계속 변하는 x86 코드와 상당히 틀립니다.

예를 하나 들어 보겠습니다. 세 개의 다른 함수를 호출하는 x64 함수가 있다고 생각해 보시기 바랍니다. 처음 함수는 네 개의 인자(0x20 바이트=32바이트)를 받습니다. 두번째 인자는 열 두개의 인자(0x60바이트=96바이트)를 받습니다. 세번째 함수는 여덟 개의 인자(0x40=64바이트)를 받습니다. 프롤로그에서는, 생성된 코드는, 스택에 단지 96바이트만 예약해서 대상 함수가 인자들을 찾을 수 있도록, 96 바이트 안의 적절한 위치에 인자들을 복사해 놓습니다.

x86 함수 호출 방식에 대한 좀 더 자세한 내부 구조는 Raymond Chen's blog (영문) 에서 찾을 수 있습니다. 이것에 대해서 더 자세히 설명하지는 않겠습니다만, 몇 가지만 중요한 점을 더 말씀 드리자면. 첫 번째, 함수의 인자들 중, 처음 네 개의 인자들 중에서, 64비트 보다 적은 정수 인자들은 부호 확장(sign extended)이 일어나고, 적절한 레지스터를 통하여 전달할 수 있습니다. 두 번째로, 64비트 얼라인을 지키기 위해서, 절대로 8바이트의 정수배가 아닌 함수 인자가 스택에 존재해서는 안됩니다. 구조체를 포함해서, 1, 2, 4, 혹은 8 바이트가 아닌 인자들은 래퍼런스를 통해서 전달됩니다. 그리고 마지막으로, 8,16,32, 64비트의 구조체와 고용체는 동일한 크기의 정수 인 것 처럼, 전달됩니다.

함수 결과 값은 RAX 레지스터에 저장됩니다. 부동 소수점 형식의 값은 예외적으로 XMM0으로 돌려 받습니다. 함수 호출을 통하여, 제가 말씀드리는 레지스터들은 예약되어 있어야 합니다: RBX, RBP, RDI, RSI, R12, R13, R14, 그리고 R15. 그리고 지금 말씀드리는 레지스터들은 휘발성이고, 값이 없어질 수 있습니다:RAX, RCX, RDX, R8, R9, R10, 그리고 R11.

위에서 제가 예외 처리 메커니즘의 일환으로, 운영체제가 스택 프레임을 검사한다고 말씀 드렸습니다. 여러분이 한 번이라도, 스택을 검사하는 코드를 써보신 적이 있다면, 거의 임시적인 Win32 프레임의 레이아웃이 프로세스를 다루기 힘들게 한다는 것을 아실 겁니다. 이러한 상황이 x64에서는 더 좋아졌습니다. 만약 함수가 스택 공간을 할당하고, 다른 함수를 호출하고, 어떤 레지스터를 예약하거나, 예외 처리를 이용하다면, 그 함수는 반드시 표준화된 프롤로그와 에필로그를 생성하기 위하여 잘 정의된 명령어 집합(well-defined set of instructions)을 써야 합니다.

표준화된 함수의 스택 프레임을 사용하도록 강제하는 것은, 운영체제가 스택을 언제든지 탐색할 수 있는 것을 보장하는 한 방법입니다. 이러한 일관성에, 표준화된 프롤로그를 이용하여 컴파일러와 링커는 관련된 테이블에 데이터를 생성해야 합니다. 궁금하신 분들을 위해서, 좀 더 자세히 설명하면, 테이블의 모든 함수 정보들은 winnt.h에 정의되어 있는 IMAGE_FUNCTION_ENTRY64의 배열 테이블에 저장되어야 합니다. 어떻게 그 테이블을 찾는지 궁금하시다구요? 그 테이블은 PE헤더의 데이터 디렉토리 영역 안에 IMAGE_DIRECTORY_ENTRY_EXCEPTION의 엔트리가 지정하고 있습니다.

상당히 짧은 분량이지만, 내부구조의 많은 부분을 다루었습니다. 그러나, x86에 대한 큰 개념과 32비트 어셈블리 언어에 대한 지식이 있으신 분들은, 상당히 짧은 시간 안에 x64 명령어를 이해하실 수 있으실 겁니다.


페이지 맨 위로페이지 맨 위로


Visual C++로 x64용 어플리케이션 개발

Visual Studio® 2005 이전에도 마이크로 소프트에서 나온 C++ 컴파일러로 x64용 코드를 생성하는 게 가능했지만, IDE와 완벽히 통합은 되지 않았었습니다. 이 글에서는, 저는 여러분이 Visual Studio® 2005를 가지고 있고, 여러분이 x64용 도구 (기본 설치 옵션이 아닙니다.)을 선택했다고 가정하고 이 글을 진행하겠습니다. 그리고, 여러분이 기존에 Win32 사용자 모드에서 C++를 이용한 프로젝트 경험이 있다고 가정하겠습니다.

x64를 위한 첫 번째 단계는, 64비트 빌드 환경을 구축하는 것 입니다. 이미 다 아시겠지만, 여러분 프로젝트에는 기본으로 두 개의 환경이 있습니다. Debug와 Retail이 그것입니다. 여러분이 여기에서 더 해야 하는 것은, 두 개의 환경을 더 생성하는 것 뿐입니다. x64를 위한 Debug와 Retail 를 추가하는 것 입니다.

기존의 프로젝트 혹은 솔루션을 한 번 열어 보시기 바랍니다. 빌드 메뉴에서, Configuration Manager를 선택해 보십시오. Configuration Manager 다이얼로그 박스에서, Active Solution Plaftfrom 콤보 박스를 눌러서, New를 선택하시기 바랍니다(그림 7 ). 그러면, 여러분은 New Solution Plaftform이라고 명명된 다른 다이얼로그를 보실 수 있을 것 입니다.

그림7 새로운 빌드 환경의 생성
그림7 새로운 빌드 환경의 생성

x64를 새 플랫폼(그림 8)로 선택하고, 다른 설정은 그냥 놔두시기 바랍니다; 그리고 OK를 클릭하세요. 그게 전부랍니다! 여러분은 이제 네 가지의 가능한 빌드 환경을 구축하셨습니다: Win32 Debug, Win32 Retail, x64 Debug, x64 Retail. Configuration Manager 를 이용해서, 쉽게 다른 환경으로의 변경도 가능합니다.

자, 이제 여러분의 코드가 어떻게 64비트에 적용 가능한지 한 번 살펴 보기로 하겠습니다. x64 Debug 설정을 기본으로 놓으시고, 프로젝트를 빌드해 보시기 바랍니다. 프로젝트 자체가 아주 가볍지 않은 이상, Win32 환경에서 볼 수 없었던 컴파일 에러들을 보실 수 있을 겁니다. 여러분이 포팅이 불가능할 정도의 C++코드를 쓰지 않는 이상, 상대적으로 쉬운 에러 몇 개만 발생해서, 여러분은 쉽게 Win32와 x64에 대응하는 코드를 가지게 될 것 입니다. 특별히 조건부 컴파일 같은 것을 이용하지 않아도 말이죠.

그림 8 빌드 플랫폼의 선택
그림 8 빌드 플랫폼의 선택


페이지 맨 위로페이지 맨 위로


Win64 호환되는 코드 만들기

아마도, Win32 코드에서 x64로 컨번팅할 때 가장 문제가 되는 부분은, 타입 정의를 변경하는 일이 될 것 같습니다. 제가 혹시 앞에서 Win64의 자료형에 대해서 이야기 했던 것에 대해서 기억하시나요? C++ 컴파일러의 원래 자료형 (int, long 기타 등등) 을 쓰는 것 보다, 윈도우에서 정의한 typedef로 정의된 자료형을 쓰는 편이 깨끗한 Win32 x64 코드를 생성하는데 쉽습니다. 그리고 Win32의 자료형을 쓰실 때, 일관성있게 쓰셔야 할 필요가 있습니다. 예를 들어서, 윈도우가 HWND을 넘겨줄 때, 단지 편하고 쉽다고 해서, FARPROC 형식의 변수에 이 값을 저장하지 마십시요.

많은 코드를 업그레이드 하면서, 아마도 제가 흔히 그리고 쉽게 보는 에러는, 포인터 값이 32비트 데이타 타입인 int 혹은 long, 그리고 DWORD에 저장되어 있는 것 이었습니다. Win32와 Win64에서의 포인터는 사이즈는 틀리지만, 정수형은 동일한 크기를 유지합니다. 그러나, 컴파일러에게 포인터 값을 정수형에 저장하는 것은 금지하는 것 역시 가능하지 않습니다.

이런 상황을 해결하기 위해서, 윈도우 헤더에는 _PTR 타입이 선언되어 있습니다. 예를 들면, DWORD_PTR, INT_PTR, 그리고 LONG_PTR 같은 타입들이 대상 플랫폼에 따라서, 안전하게 포인터 변수를 이용하게 해줍니다. 예를 들어서, DWORD_PTR타입은 Win32에서 컴파일 되었을 때는, 32비트지만, Win64에서는 64비트입니다. 저의 경우는, 그동안의 연습으로, 어떤 자료형을 선언할 때, "내가 여기서 DWORD를 선언해야 할까? 아니면 DWORD_PTR를 선언해야 할까?"라고 물어보는 것이 버릇이 되어 버렸답니다.

기대하시는 대로, 정수형에서 얼마나 많은 바이트를 원하는지 계산하는데, 약간의 문제가 있을 수 있습니다. DWORD_PTR를 정의하는 헤더파일(basetsd.h)에서 역시 INT32, INT64, INT16, UINT32, DWORD64 같은 여러 형태의 정수를 선언해 놓고 있기 때문입니다.

자료형의 크기에 관련된 또 다른 문제는 printf와 sprintf의 포맷팅(formatting)입니다. 저는 확실히 과거에 %X 혹은 %08X등을 포인터 값을 나타내는데 사용했다는 점에서 약간의 죄책감(?)을 느끼고 있고, 그 코드는 x64 시스템에서 문제를 일으키고 있습니다. 옳은 방식은 대상 플랫폼의 포인터의 크기를 자동으로 계산해 주는 %p를 사용하는 것 입니다. 추가로, printf와 sprintf는 사이즈에 의존하지 않는 타입인 'I' 프리픽스(prefix)를 가지고 있습니다. 예를 들어서, 여러분은 %Iu를 UINT_PTR 변수를 출력하기 위해서 사용할 수 있습니다. 이와 동일한 방식으로, 여러분이 특정 변수가 언제 64비트 부호 있는 값이 될 것이라는 알고 있다면, %I64d를 쓰실 수 있습니다.

자료형의 불일치에서 일어난 에러들을 정리하는 것만으로는 Win64가 준비되었다고 말할 수 없습니다. 여러분은 아직은 아마도 x86에서만 돌아갈 수 있는 소스코드를 가지고 계실겁니다. Win64로의 포팅을 위해서, 특정 코드에서는 여러분이 Win32와 x64를 위한 두 가지 버젼의 함수를 쓰실 수도 있습니다. 이 때야말로, 전처리자(preprocessor)가 아주 유용하게 쓰입니다:

_M_IX86 
_M_AMD64 
_WIN64
전절한 전처리자의 사용이야 말로, 여러 플랫폼에서 동작하는 소프트웨어를 만들기 위해서는 필수적입니다. _M_IX86과 _M_AMD64는 특정 프로세서를 위해서 컴파일할 때를 위해서 정의되어 있습니다.

전처리자 매크로를 사용할 때, 뭘 원하는지 한 번 열심히 생각해 보시기 바랍니다. 예를 들어서, 이 코드가 정말로 x64 프로세서만을 위한 것이라면, 아래와 같이 매크로를 쓰시기 바랍니다:

#ifdef _M_AMD64
반면에, 동일한 코드가 x64와 아이템니움(Itanium)에서 동작하기를 원한다면, 여러분은 아래와 같이 쓰시는게 좋을 것 같습니다:
#ifdef _WIN64

이러한 매크로를 쓸 때 제가 유용하게 쓰는 버릇이 하나 있습니다. 바로 제가 무엇을 잊어 버렸을 때를 대비해서, 명시적으로 모든 경우에 대해서 #else 케이스문을 써주는 것입니다. 아래에 코드를 한 번 살펴봐 주시기 바랍니다:

#ifdef _M_AMD64 
// My x64 code here 
#else 
// My x86 code here 
#endif
또 다른 세번째 CPU가 나타나면? 저의 x86 코드가 저의 의도와는 다르게 컴파일 됩니다. 위의 코드를 더 좋은 방법으로 구성하는 방법은 아래와 같습니다:
#ifdef _M_AMD64 
// My x64 code here 
#elif defined (_M_IX86) 
// My x86 code here 
#else 
#error !!! Need to write code for this architecture 
#endif

그리고, 마지막으로 저의 경우는 Win32코드 중에서 x64로 쉽게 포팅되지 않는 부분 중의 하나는 인라인 어셈블러 부분이었습니다. Visual C++가 x64 를 위한 인라인 어셈블러를 지원하지 않거든요. 대신에 64비트 MASM이 제공되고, MSDN에 문서화가 되어 있습니다. ML64.exe와 다른 x64툴들은 (CL.EXE와 LINK.EXE를 포함해서) 커멘드라인에서도 쓸 수 있습니다. 단지 VCVARS64.BAT를 실행시키세요. 그러면, 여러분의 패스에 이 파일들의 경로를 포함해 줄 것 입니다.


페이지 맨 위로페이지 맨 위로


디버깅

여러분은 마침내, 여러분의 코드를 Win32와 x64 양쪽에서 깨끗하게 컴파일 할 수 있게 되었습니다. 이 대장정의 마지막은 "어떻게 디버깅을 하느냐?" 입니다. 여러분이 x64 에서 x64 버젼을 빌드 했는지의 여부에 상관없이, 여러분은 x64 모드에서 디버깅 하기 위해서는, Visual Studio의 리모트 디버깅 기능이 필요합니다. 운좋게도, 여러분이 64비트 컴퓨터에서 Visual Studio를 동작시키면, IDE가 내부적으로 이 단계를 여러분을 위해서 해줍니다. 어떤 이유로, 리모트 디버깅을 할 수 없을 때, 여러분의 또 다른 옵션은 x64용 WinDbg (영문)를 사용하시는 방법도 있습니다. 그러나, 그렇게 되면, Visual Studio 디버그의 많은 훌륭한 기능들을 포기하셔야 합니다.

여러분이 리모트 디버깅을 한 번도 해보신 적이 없다고 하더라도, 걱정하실 필요 없습니다. 한 번만 설치 되면, 리모트 디버깅도 로컬처럼 쉼없이 잘 동작합니다.

첫 번째 단계는 64비트 MSVSMON을 대상 컴퓨터에 설치하는 것 입니다. 이것은 일반적으로 VisualStudio와 같이 오는 RdbgSetup을 동작시키면, 알아서 해 줍니다. 일단 MSVSMON이 설치되면, Tools 메뉴에서 적절한 보안 설정(혹은 lack)등을 설정할 수 있습니다.

다음에는, Visual Studio 안에서, 여러분이 x64코드를 위해서 리모트 디버깅을 위한 설정을 하셔야 합니다. 이것은 프로젝트의 프로퍼티( 그림 9 )에서 할 수 있습니다.

그림 9 디버그 속성
그림 9 디버그 속성

여러분의 64비트 구성을 선택해서, Configuration 프로퍼티 아래에 있는 Debugging을 선택하시기 바랍니다. 위쪽에 Debugger to launch라고 되어 있는 부분이 있습니다. 일반적으로 이 부분이 Local Windows Debugger로 설정되어 있습니다. 이 설정을 Remote Windows Debugger로 변경하시기 바랍니다. 그 아래에서, 여러분이 디버깅을 시작할 때의 리모트 명령(예를 들어서, 프로그램 이름)과 리모트 시스템의 이름과 연결 속성을 선택해 줄 수 있습니다.

모든 것이 다 제대로 설정되었다면, 여러분의 x64 어플리케이션을 여러분이 Win32 프로그램에서 하듯이 디버깅할 수 있습니다. 여러분은 MSVSMON이 매 번 성공적으로 디버그와 연결이 될 때, "connected"라는 메세지를 보내 주기 때문에, 연결이 되었는지의 여부를 판별할 수 있습니다. 여기서 부터는, 여러분이 알고 있고, 사랑하는 Visual Studio 디버거와 거의 모든 것이 동일합니다. 64비트 레지스터를 보기 위해서, 그리고 비슷하지만 조금 다른 x64 어셈블리 코드를 보기 위해서 레지스터 창과 디스어셈블러 창을 띄우는 것을 잊지 마세요.

주의: 64비트 미니덤프는 32비트 덤프처럼 Visual Studio에서 직접 불러 올 수 없습니다. 대신, 여러분은 리모트 디버깅을 이용해야 합니다. 네이티브(native)와 매니지드(managed) 64비트 코드의 상호호환은 현재로서 Visual Studio 2005에서 지원되지 않습니다.


페이지 맨 위로페이지 맨 위로


매니지드 코드는 어떻게 하나요?

마이크로 소프트 .NET 프레임 웍에서 코딩 하는 것의 장점 중의 하나는, 깔려있는 운영제제의 많은 부분이 일반적인 목적으로 코드로 추상화 되어 사라져 버렸다는 점입니다. 추가로 IL 명령어 포맷은 CPU에 의존적이지 않습니다. 그래서, 이론적으로는, Win32에서 제작된 .NET에 기반을 둔 바이너리 파일은 x64시스템에서 수정하지 않고 동작해야 합니다. 그러나, 현실은 아주 복잡합니다.

x64버젼의 .NET 프레임웍 2.0을 제 x64시스템에 설치한 뒤에, 저는 제가 Win32에서 실행했던 똑같은.NET 실행파일을 동작시킬 수 있었습니다. 정말 좋지요? 물론, 모든 .NET에 기반을 둔 프로그램이 Win32와 x64에서 컴파일 하지 않고, 동일하게 동작 한다는 보장은 없습니다만, 다만 합리적인 범위의 시간 안에 그냥 동작합니다.

만약 여러분의 코드가 명시적으로 native code를 호출한다면 (예를 들어서, C# 혹은 Visual Basic 에서의 P/Invoke) 64비트 CLR에서 실행 시에 문제에 부딫힐 확률이 커집니다. 그러나, 다행스럽게도 컴파일러 스위치에 여러분의 코드가 어떤 플랫폼에서 동작할지를 명시하는 부분이 있습니다. 예를 들어서, 여러분은 64비트 CLR이 존재함에도 불구하고, 여러분의 코드가 WOW64에서 동작하기를 원한다고 명시할 수도 있습니다.


페이지 맨 위로페이지 맨 위로


최종 정리

모든 것을 고려해 보아도, x64 버젼의 윈도우로 이동하는 것은, 비교적 저에게는 어렵지 않은 과정이었습니다. 일단, 여러분이 운영체제 구조와 툴의 상대적으로 미미한 차이점을 한 번 훑어 보기만 하면, 하나의 코드를 바탕으로 해서 두 개의 플랫폼에서 동작하는 것이 어렵지 않습니다. Visual Studio 2005가 이 노력의 과정을 훨씬 쉽게 만들어 주고 있고, x64에 특화된 디바이스 드라이버나 툴, 예를 들면 Sysinternals.com의 Process Explorer은 매일 새롭게 나타나고 있습니다, 그러니 이 쪽으로 뛰어 들지 않을 이유가 없습니다!


페이지 맨 위로페이지 맨 위로



  • 이 문서는 한국 개발자를 위하여 Microsoft MVP 가 번역하여 제공한 문서입니다.
  • Microsoft 는 이 문서를 번역자의 의도대로 제공해 드리며 더 정확한 표현 또는 여러분의 의견을 환영합니다.
  • Microsoft 는 커뮤니티를 위해 번역자의 의도대로 이 문서를 제공합니다.



  • Matt Pietrek는 MSDN 매거진의 “Under the Hood Column” 뿐만 아니라 윈도우 시스템 레벨의 프로그래밍에 대해서 다수의 서적을 저술해 왔습니다. 과거에는 NuMega/Compuware의 BoundsChecker 시리즈의 제품을 담당하였고, 현재는 마이크포 소프트의 Visual Studio 팀에서 일하고 있습니다. 


    반응형
    반응형
    Windows XP사용시에 (다른 OS는 모르겠네요)
    VC++로 Thread Debug시에 OS가 멈추는 현상이 자주 발생합니다.
    IME쪽에 버그가 있어서 그렇다는데

    아래의 option을 Check해서 꺼주면 됩니다. 
    제어판 -> Data, Time, Language, and Regional Option ->Regional and Language option -> Languages Tab -> Detail ->Advanced Tab -> Check  the Turn off advanced text services

    영문윈도라서 영문윈도 경로로 적었으니 적절히 따라가서 고치시면 됩니다.



    Format하고나면 항상 잊어버려 정리해둡니다.
    반응형
    반응형
    MSDN에 나와있음
    http://msdn.microsoft.com/en-us/library/bb759844(VS.85).aspx
    이곳 잘 살펴보고 사용하기


    아래에도 예제가 나와있음
    출처 : http://www.eggheadcafe.com/software/aspnet/29583177/get-folder-from-full-file.aspx
    Here are some functions you may find useful (since you are using MFC):
    Tom
    //
    // Returns the file portion from a path
    //
    CString GetFileOnly(LPCTSTR Path)
    {
    // Strip off the path and return just the filename part
    CString temp = (LPCTSTR) Path; // Force CString to make a copy
    ::PathStripPath(temp.GetBuffer(0));
    temp.ReleaseBuffer(-1);
    return temp;
    }
    
    //
    // Returns the folder portion from a path
    //
    CString GetFolderOnly(LPCTSTR Path)
    {
    // Strip off the file name so we can direct the file scanning dialog to
    go
    // back to the same directory as before.
    CString temp = (LPCTSTR) Path; // Force CString to make a copy
    ::PathRemoveFileSpec(temp.GetBuffer(0));
    temp.ReleaseBuffer(-1);
    return temp;
    }
    
    //
    // Adds a backslash to the end of a path if it is needed
    //
    CString AddSlash(LPCTSTR Path)
    {
    CString cs = Path;
    ::PathAddBackslash(cs.GetBuffer(_MAX_PATH));
    cs.ReleaseBuffer(-1);
    if(cs.IsEmpty())
    cs = _T("\\");
    return cs;
    }
    
    //
    // Removes a backslash from the end of a path if it is there
    //
    CString RemoveSlash(LPCTSTR Path)
    {
    CString cs = Path;
    ::PathRemoveBackslash(cs.GetBuffer(_MAX_PATH));
    cs.ReleaseBuffer(-1);
    return cs;
    }
    
    //
    // Adds a folder path and file together to make a file path
    //
    CString AddPathAndFile(LPCTSTR Folder, LPCTSTR File)
    {
    CString cs = Folder;
    ::PathAddBackslash(cs.GetBuffer(_MAX_PATH));
    ::PathAppend(cs.GetBuffer(_MAX_PATH),File);
    cs.ReleaseBuffer(-1);
    return cs;
    }
    
    //
    // Adds a .ext to the end of a file path
    //
    CString AddExt(LPCTSTR Path, LPCTSTR Ext)
    {
    CString cs = Path;
    ::PathAddExtension(cs.GetBuffer(_MAX_PATH),Ext);
    cs.ReleaseBuffer(-1);
    return cs;
    }
    
    //
    // Returns true if file exists or false if file does not exist
    //
    BOOL FileExists(LPCTSTR Path)
    {
    return (::PathFileExists(Path));
    }
    
    //
    // Returns just the .ext part of the file path
    //
    CString GetFileExt(LPCTSTR Path)
    {
    CString cs;
    cs = ::PathFindExtension(Path);
    return cs;
    }
    
    CString GetFileName(LPCTSTR Path)
    {
    CString csFile = GetFileOnly(Path);
    if(!GetFileExt(csFile).IsEmpty()) { // Is there an extension
    ::PathRemoveExtension(csFile.GetBuffer(_MAX_PATH));
    csFile.ReleaseBuffer();
    }
    return csFile;
    }
    
    
    //
    // Exchanges one file extension for another and returns the new fiel path
    //
    CString RenameFileExt(LPCTSTR Path, LPCTSTR Ext)
    {
    CString cs = Path;
    ::PathRenameExtension(cs.GetBuffer(_MAX_PATH), Ext);
    return cs;
    }
    반응형
    반응형
    출처 : http://www.tipssoft.com/bulletin/board.php?bo_table=old_bbs&wr_id=103

    [MFC] CWinApp::ProcessShellCommand 함수의 역활은?

    [
    질문] 

    안녕하세요 

    아래는 InitInstance()함수인데여 

    if (!ProcessShellCommand(cmdInfo)) return FALSE; 

    이부분의 역활을 모르겠읍니다. 이부분때문에 프로그램실행이 안되는것같읍니다 
    이앞부분에 메세지박스를 띄우면 잘뜨고 다음에 빈문서를 만들수없다는 메세지와 
    확인버튼을 누르면 그대로끝나버립니다. 그리구 위부분뒤에 메세지박스를 넣으면 
    넣은메세지박스는안뜨고 빈문서를 만들수없다는메세지가뜹니다. 그러니까 
    위부분코드가 빈문서를생성할수없다는메세지를 띄우고 그냥리턴하는모양인데여 
    무슨역활을하는지 도움말 부탁합니다. 

    [답변] 

    안녕하세요~! 

    ProcessShellCommand 함수는 기본적으로 사용자가 실행시에 특정한 인자를 적지 않는다면 

          OnFileNew(); 

    와 동일한 기능을 수행합니다. 즉, 메뉴의 새파일을 선택했을때 호출되는 함수이죠. 
    특별한 기능을 필요로 하지 않는다면 ProcessShellCommand 대신 
    OnFileNew 를 사용해도 동일한 기능을 수행합니다. 

    일반적으로 프로그램을 그냥 실행하기도 하지만 탐색기에서 특정 파일을 더블 클릭했을때 
    해당 파일과 연결된 프로그램을 실행하기도 하죠.. 예를 들어, *.DOC 파일을 더블클릭하면 
    Microsoft Word가 실행되면서 해당 파일이  열리잖아요.. 
    이럴때는 ProcessShellCommand 함수가 OpenDocumentFile 함수를 수행하게 됩니다. 
    또, DDE 기능을 이용할때는 ProcessShellCommand 함수가 본 프로그램을 숨기는 기능도 
    수행합니다. 

    결국 프로그램 실행할때 넘어오는 컴멘드 라인 명령문(cmdInfo 변수에 있는)을 상황별로 수행하는 
    함수가 ProcessShellCommand 함수입니다. 

    님의 프로그램이 그런 오류를 발생한다니, 분명 OnFileNew로 바꾸어도 나겠네요.. 코드가 안되는 
    이유를 좀더 정확히 알려면 CDocument 관련 소스를 살펴보셔야 할것 같습니다. 해당 코드에 
    문제가 있는것 같은데, 어떻게 코드를 구성했는지 알수 없기 때문에 좀더 자세한 답변을 드리기가 
    어려울것 같네요 ^^;; 

    그럼, 즐거운 하루되세요~! 






    출처 : http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=50&MAEULNo=20&no=209255&ref=209237

     MDI에서 디버깅하면.. 문서하나가 자동으로 열리는데 ..안열리게 할려구합니다...  | VC++ 일반 2002-06-28 오전 10:20:00
    이기준 (sepiros78)  이기준님께 메시지 보내기이기준님을 내 주소록에 추가합니다.이기준님의 개인게시판 가기 번호: 209237   / 평점:  (-)  / 읽음:881
    어케 하는거죠?

    MDI 를 만들고. 그냥. 실행하면.. 문서창 하나가. 자동으로 열리어지는데..

    이 창을 안띄우고 싶어요...

    도와주세요!!!!
    이 글에 평점 주기: 
     [답변]if (!ProcessShellCommand(cmdInfo)) 2002-06-28 오전 11:06:00
    이현범 (hblee75)  이현범님께 메시지 보내기이현범님을 내 주소록에 추가합니다.이현범님의 개인게시판 가기 번호: 209255   / 평점:  (-)  
    App class 가 정의 되어 있는 파일,, 그러니깐....프로젝트 이름이 test였다면 test.cpp파일을 열고 다음 코드를 찾아
    주석 처리 하세요!!
    다음 함수에 관한 사항은 여러 책에 나와 있으니까 함 찾아 보시구여^^ 물론 MSDN에도 있습니다.
    그럼 즐프.....
    if (!ProcessShellCommand(cmdInfo))
        return FALSE;
    반응형
    반응형
    1.
    http://cafe.naver.com/asusnf.cafe?iframe_url=/ArticleRead.nhn%3Farticleid=8408

    2.
    [정보] SATA HDD의 NCQ지원(AHCI) 활성화 방법
    dolparri (dolparri) 218.148.xxx.34

    요즘 나오는 SATA HDD에는 NCQ기능이 있어서 멀티태스킹 환경에서 hdd의 부하를 줄여주고 속도를 올릴수 있습니다. 듀얼코어 등에서는 더욱 필요한 기능입니다. 이 기능을 사용하려면 NCQ지원 HDD, NCQ지원 마더보드, NCQ지원 OS라는 세가지가 동시에 갖춰져야 하죠.

     

    인텔칩셋의 보드일 경우, HDD와 M/B가 갖춰져 있더라도 OS에서 이 기능을 지원하게 하려면, 기존에 알려진 방법으로는,

     

    1. M/B의 BIOS에서 SATA as [AHCI] 모드로 설정.

    2. Floppy disket에 AHCI driver를 복사한 다음에 OS CD로 Booting하고,

    3. Setup 메뉴가 나올때 F6를 눌러서 Floppy의 AHCI Driver를 선택해 준 다음에 OS 새로 설치.

     

    겨우 이작업을 하자고 요즘 쓰지도 않는 FDD가 있어야 하고, 만일 설치 file이 있는 disket를 미리 준비하지 못했으면 OS설치 도중에 다른 컴퓨터에 가서 floppy를 만들어와야 하는 번거로움이 있죠. 그렇다고 SATA as [IDE]로 설치하면 floppy 없이 설치는 되는데 HDD의 성능을 100% 발휘하지 못하는 문제가 있고, OS를 설치한 후에는 AHCI모드로 변경도 안된다고 알려져 있습니다.

     

    그런데, 꼭 그런것은 아니고 다음의 방법을 사용하면 됩니다. 지금 SATA as [IDE] 모드로 사용중인 분들도 물론 변경 가능합니다. (물론 자신이 현재 사용중인 M/B가 AHCI 모드를 지원하는 보드여야 합니다. 인텔칩셋의 마더보드라면 사우스 브릿지가 ICH6R/7R/8R/7M/6M/7DH/7MR 같은 것... M/B를 새로 구입하시는 분은 이런점을 염두에 두고 구입하면 좋겠죠)

     

    1. BIOS에서 SATA as [IDE] 모드로 설정. (BIOS에 따라서 약간 다를수 있음)

    2. Windows XP/2003 등을 정상적인 방법으로 설치. (이런저런 칩셋 패치도 모두 해줌)

    3. 자신의 M/B 칩셋 이름의 정확한 명칭을 알기 위하여 Intel Chipset Identification Utility (chiputil.exe)를 다운 받아서 확인합니다.

    4. www.intel.com에서 Intel Matrix Storage Manager (iata60_cd.exe(Multi Language Version))도 다운받습니다.

    5. "iata60_cd.exe -a -a -pc:\temp" 를 command line 명령어로 실행하면 c:\temp 밑에 file들이 풀려 나옴. (참고:이 프로그램을 그냥 실행하면 칩셋이 않맞다고 설치가 안됩니다.)

    6. 장치관리자에서 IDE ATA/ATAPI controllers 밑에 Intel(R) 82801 XX Serial ATA...가 있음.

    7. 이것의 등록정보에서 Update Driver(또는 이와 유사한 버튼)를 누르고 적당히 선택하여 조금전에 풀린 C:\Temp\Driver에 있는 iaahci.inf를 지정해줌.

    8. 자신의 M/B 칩셋 이름을 선택해 줌. (예: Intel (R) 82801 GR/GH SATA AHCI Controller)

    9. 뭐라뭐라 경고가 뜨면 무시함.

    10. 컴퓨터 종료.

    0. BIOS 셋업으로 들어가서 SATA as [IDE]를 SATA as [AHCI]로 변경함.

    11. 재부팅.

    12. iata60_cd.exe을 실행하여 Intel Matrix Storage Manager를 정상적으로 설치하고, 설치된 Matrix Storage Console프로그램을 실행하여 NCQ가 정상적으로 동작하는지 확인. (한글로 NCQ를 "기본 명령어 대기열 삽입 지원"(???) 이라고 번역해놓았네요)

     

    말은 길은데, 결론은 처음 OS설치할때 굳이 AHCI 모드로 설치하려고 없는 플로피를 찾아 헤맬 필요가 없다는 뜻입니다. OS설치후에 수동으로 설치해주면 됩니다.


    반응형
    반응형
    반응형
    반응형
    반응형
    반응형
    반응형
    반응형

     화면 깜박거림을 제거합시다.  | VC++ 일반 2005-05-09 오후 11:34:20
    최병훈 (angleafro)  최병훈님께 메시지 보내기최병훈님을 내 주소록에 추가합니다.최병훈님의 개인게시판 가기 번호: 7084   / 평점:  (6.3)  / 읽음:3,712

    화면 깜박거림을 제거합시다.

     

    화면 떨림이란 모니터의 주사선과 그래픽카드의 잘못된 설정으로 생길수 있습니다.

    그래픽 카드를 바꾸고, 모니터를 좋은 것으로 바꾸서 쓰면 되죠..

    맞나요.. ㅋㅋ

     

    그럼 프로그램할 때 화면 조정 말고 , 사이즈 조정이나 크기조정 또는 드래그, 및 콘트롤 사용시 나오는 깜박거림은 도대체 모죠?

     

    해답은 있나요?

    네 당근 입니다, (오이 부모왈~~)

     

    그럼 시작하는 과정이므로 간단하게 가장 쉬운 폼뷰부터 시작하죠..

     

    그럼 왜 깜박거림이 생기는가?

     

    한가지 동일한 원인에 의하여 프로그램중 일부분이 그리기 작업을 하게 되고, 다른 부분이 그 곳에 다시 그리기 작업을 하게 됩니다.

    그리고, 순식간에 두 가지 페인팅 작업이 발생이 되는데, 두번째 작업을 하기 전에 사용자가 첫번째 그리기 작업을 보기 되늣것입니다.

     

    그럼 해결 방법은 있군요.

    . 두가지를 그리기를 하는데, 첫번째 작업이 끝나고 아주 빨리 두번째 그리기 작업을 하는것입니다.

    그러나 , 이 방법은 사용자 피씨가 슈퍼콤이 아니면 아주 힘들겠죠..

    어차피 두가지가 순식간의 터울로 드려지기 때문에 그리기 작업을 나눠서 하지 말고 동시에 해보자는 것입니다.

     

    그럼 깜박꺼림(Flicker)를 제거한는 간단한 프로그램을 짜 보죠.

    초보자들이 비절씨를 할 때 아장 쉽게 할수 있는 것 중 하나가, CFormView 클레스를 뷰로 하여 작성한는 것입니다.

    폼뷰로 프로그램을 작성하였을 때 쉽게 볼수 있는데요..

    OwerDraw 속성을 적용하지 않는 기본 콘트롤이 폼뷰의 사이즈를 조정하면 깜박거리죠..

     

    그럼 깜박거림을 없게하려면.

    가장 쉬운게 부모 윈도우, 즉 폼뷰가 변경될 때 콘트롤들이 다시 그려지지 않게 하면 됩니다.

    WS_CLIPCHILDREN

    이 속성을 이용하면 되죠.

    대신 이속성을 폼이 키기가 변경될 때 적용하면 더욱 효과적이죠.

    void CFlicker_freeView::OnSize(UINT nType, int cx, int cy)

    {

                 ModifyStyle(0, WS_CLIPCHILDREN); // turn on WS_CLIPCHILDREN

                 CFormView::OnSize(nType, cx, cy);           // default

                 UpdateWindow();

                 //this->Invalidate(false);

                 ModifyStyle(WS_CLIPCHILDREN, 0); // turn off WS_CLIPCHILDREN

                 GetClientRect(&m_rectDraw);

    }

    이렇게요..

    한가지 중요한게 남았는데.

    구릅박스죠..

    그릅박스 경우는 문제가 있어요.

     

     

    자신의 차일드를 포함할수 있는데,

    구름박스의 테두리 안쪽이 폼에도 속하지 않고, WS_CLIPCHILDREN 속성에 의하여 그룹박스에도

    속하지 않게되어서  화면이 요상해 집니다.

    이걸 해결 할려면. Transparent속성을 활성화 시켜 주면 됩니다.

     

    다음에는 페인팅 작업이 너무 많아서 깜박거리는 것을 예기해 보죠..

    수고 하세요.

    이 글에 평점 주기: 

    질문&답변
     formview를 이용한 다디얼로그의 크기를 변경시킬때 깜박이는 현상해결.. 방법 없나요..ㅜㅜ  | VC++ 일반 2003-12-20 오전 1:23:29
    홍성호 (dybbls)  홍성호님께 메시지 보내기홍성호님을 내 주소록에 추가합니다.홍성호님의 개인게시판 가기 번호: 406068   / 평점:  (-)  / 읽음:1,194

     formview를 이용해서 MDI 프로그램을 만들었습니다.

    formview안에는 대부분 이미지로 차 있습니다.

     

    근데 창 크기를 변경시킬때 심하게 깜박이는 현상이 나타납니다...

    물론 이미지는 변하지 않습니다. 단지 크기만 창 크기메 맞게 변합니다.

     

    정말 답답합니다. 왜 이러는건지.. 힘드네요.. 프로그램..

     

    이 글에 평점 주기: 
     [답변][MAX] Window 깜빡임의 실체 - WM_ERASEBKGND와 WM_PAINT 2003-12-20 오전 2:42:08
    박민재 (MAXIST)  박민재님께 메시지 보내기박민재님을 내 주소록에 추가합니다.박민재님의 개인게시판 가기 번호: 406075   / 평점:  (8.3)  
    Window는 뭔가 새로 그려야할 필요성 있을 때마다 WM_PAINT Message를 받습니다. OnPaint라는 이름으로 WM_PAINT에 대한 Handler Function이 보통 만들어지죠. System으로부터 WM_PAINT가 날아오는 상황은 다음과 같습니다.

        - 윈도우가 처음 생성되었을 때
        - 윈도우의 위치가 이동되었을 때 
        - 윈도우의 크기가 변경되었을 때(최소 및 최대화 포함)
        - 윈도우의 전체 또는 일부가 다른 윈도우에 가려져 있다가 나타날 때
        - 윈도우가 스크롤 될 때
        - UpdateWindow나 RedrawWindow 함수가 불렸을 때
        - InvalidateRect나 InvalidateRgn 함수가 불려서 다시 그려져야할 영역이 발생 한 후,
          Message Queue에 다른 처리할 Windows Message가 없을 때

    그런데 WM_PAINT가 날아오기 '전'에 대부분 같이 따라오는 Message가 있습니다 WM_ERASEBKGND라는 것으로, Erase Background 라는 의미죠. 즉, 배경을 지워라.

    Window의 기본 Message Procedure(DefWindowProc)는 사용자가 WM_ERASEBKGND를 받고도 아무 처리를 하지 않으면, WNDCLASS의 hbrBackground 맴버에 정의된 색상으로 배경을 지워버립니다. 깜빡이는 것은 아래 처럼 되기 때문에 발생하는 것이지요.

        1. WM_ERASEBKGND 받음
        2. 배경 지움
        3. WM_PAINT 받음
        4. Image 다시 그림

    2까지가 '깜'이고 4까지 가면 '빡'이 됩니다. 깜빡 깜빡의 실체는 이것입니다.
    손쉬운 처리는 WM_ERASEBKGND를 받았을 때, 배경을 안지우도록 하는 방법입니다. 보통 OnEraseBkgnd 정도의 이름을 갖게 되는 WM_ERASEBKGND Message Handler에서 0을 return 하면 됩니다.

        BOOL CAboutDlg::OnEraseBkgnd(CDC* pDC)
        {
            return 0;
        }

    깜빡 거리는 것이 WM_PAINT Message Handler에서 그려주는 어떤 것이 아니라, 또 다른 Window인 '자식' Control이라면, Window Styles 중에 WS_CLIPCHILDREN Style을 '부모' Window에 먹여서 자식 Control에 의해 가려지는 영역은 그리기 대상에서 아예 제외시켜버리는 것이 일반적인 해결책입니다.

     질문&답변
     RedrawWindow와 Invalidate와의 차이?  | VC++ 일반 2003-12-21 오후 8:33:13
    김성용 (mage44)  김성용님께 메시지 보내기김성용님을 내 주소록에 추가합니다.김성용님의 개인게시판 가기 번호: 406316   / 평점:  (-)  / 읽음:1,009

     RedrawWindow와 Invalidate와의 차이가 무엇인지 알고 싶습니다.

    RedrawWindow한부분에 Invalidate로 대체하면 제대로 안되는 이유를 모르겠습니다.

     

    코드

     

        CClientDC dc(this);

        CRect rect;

        CString status;

     

        GetClientRect(rect);

        status.Format( "%-20s Line %d, Col %d", msg, point.x, point.y);

     

        RedrawWindow();

        //Invalidate();

        dc.MoveTo(0,rect.bottom-30);

        dc.LineTo(rect.right, rect.bottom-30);

        dc.TextOut(0, rect.bottom-20, status);

     

    RedrawWindow();로하면 문자열이 잘 출력되는데

    Invalidate();로 하면 문자열이 출력되었다가 이내 지워 집니다.

     

    이 글에 평점 주기: 
     [답변][MAX] Win32 API RedrawWindow와 Invalidate 함수의 차이(내용 추가) 2003-12-21 오후 9:17:08
    박민재 (MAXIST)  박민재님께 메시지 보내기박민재님을 내 주소록에 추가합니다.박민재님의 개인게시판 가기 번호: 406322   / 평점:  (9.0)  
        BOOL RedrawWindow(
           LPCRECT lpRectUpdate = NULL,
           CRgn* prgnUpdate = NULL,
           UINT flags = RDW_INVALIDATE | RDW_UPDATENOW | RDW_ERASE 
        ); 

    보시다시피, MFC CWnd::RedrawWindow 함수는 Win32 API RedrawWindow 함수를 호출 할 때 아래와 같은 옵션을 '기본값'으로 씁니다.


        RDW_INVALIDATE
            - 대상 Window의 특정 영역 또는 Client 영역 전체를 Invalidate 함
        RDW_ERASE
            - 대상 Window가 Repaint될 때, WM_ERASEBKGND Message도 같이 받게 함
        RDW_UPDATENOW
            - 함수가 반환되기 전에 대상 Window들이
              WM_NCPAINT, WM_ERASEBKGND, WM_PAINT Message를 받음

    Win32 API Invalidate 함수의 역할은, OS가 Window에게 '어떤 영역이 무효화 되었으니 다음 번 WM_PAINT를 받을 때 다시 그렸으면 좋겠다'는 표시를 하는 것입니다. 보통 Invalidate가 호출되고 나면 OS는 대상 Window의 Message Queue에 WM_ERASEBKGND 그리고 WM_PAINT 순으로 Message를 보냅니다.

    Window가 실제로 Repaint를 하는 시점은, Invalidate가 호출된 시점이 아니라, Invalidate의 호출 이후에 OS가 대상 Window에 다시 WM_PAINT Message를 던져준 시점입니다.

    Invalidate를 썼을 때, TextOut으로 뿌려준 부분이 '낼름' 지워지는 것은 아마도, TextOut으루 뿌려준 이후에, WM_PAINT Message Handler 함수 안에서 TextOut으로 출력한 내용을 덮어써버리는 것 같네요.

    RedrawWindow를 쓰더라도 RDW_UPDATENOW 옵션을 빼면 그냥 Invalidate를 불러준 것과 같은 효과입니다. RedrawWindow를 쓸 때 RDW_UPDATENOW을 쓰면, 함수가 반환 되기 전에 WM_PAINT Message가 Message Queue 들어옵니다. 그렇게 되면 그 시점에서 바로 다시 그리기가 일어나고 뒤에 TextOut으로 뭔가 그려진 집니다.

    따라서, 화면에 보이고 있는 것은, RedrawWindow가 제 역할을 했다기 보다는 그냥 불필요한 Paint 동작이 다행스럽게도 TextOut 보다 먼저 발생했기 때문입니다. 차라리 Invalidate나 RedrawWindow를 모두 호출하지 않았다면 문자열들이 그냥 보였을 것입니다.

    지워진다는 것을 보니, 문제의 Code WM_PAINT Message Handler 함수 안에 있는 것이 아닐 것 같은데 맞나요? 그럼 잘 출력된 것 처럼 보여도, 출력된 문자를 다른 Window로 한 번 덮었다가 (Invalidate) 빼면 글자가 지워집니다. 확인해보세요. 화면에 보이는 것을 계속 유지하고 싶다면, WM_PAINT Message Handler 안에서 계속 그려지게 만들어야 합니다.

    이 글에 평점 주기: 
             [답변]한가지만 더요.. 2003-12-21 오후 10:22:22
    김성용 (mage44)  김성용님께 메시지 보내기김성용님을 내 주소록에 추가합니다.김성용님의 개인게시판 가기 번호: 406334   / 평점:  (-)  

     그렇다면 RedrawWindow()는 WM_NCPAINT, WM_ERASEBGD, WM_PAINT메세지가

    처리된 다음에 리턴이 된다는 것인가요?

    아니면 그냥 위 메세지들을 메세지 큐에만 던져두고 리턴을 한다는 것인가요?

     

     

    이 글에 평점 주기: 
                     [답변]RedrawWindow에 RDW_UPDATENOW 옵션이 있으면 WM_PAINT가 즉시 전달됩니다. 2003-12-21 오후 10:31:18
    박민재 (MAXIST)  박민재님께 메시지 보내기박민재님을 내 주소록에 추가합니다.박민재님의 개인게시판 가기 번호: 406335   / 평점:  (9.0)  

    RedrawWindow 함수에 현재 쓰인 옵션(RDW_INVALIDATE | RDW_UPDATENOW | RDW_ERASE)만 보면, WM_ERASEBGD Message와 WM_PAINT Message가 먼저 처리됩니다.

    Break Point를 걸어서 확인해도 되죠. RedrawWindow 함수에 RDW_UPDATENOW가 있으면, RedrawWindow 호출 시, 바로 WM_PAINT Message Handler가 처리되는 것을 볼 수 있습니다. 그 이후에 RedrawWindow 함수 이후 부분의 Code가 실행됩니다.

    Invalidate는 WM_PAINT Message를 대상 Window로 보내는 것이 아니라 System이 대상 Window에 WM_PAINT Message 보내는 작용을 유발시키는 것입니다. 정확히 언제 WM_PAINT Message가 대상 Window에 도착하는지는 알 수 없죠.
    이 글에 평점 주기: 
                             [답변]감사합니다...그런데.. 2003-12-21 오후 10:54:19
    김성용 (mage44)  김성용님께 메시지 보내기김성용님을 내 주소록에 추가합니다.김성용님의 개인게시판 가기 번호: 406337   / 평점:  (-)  

     저도 이런저런 코드를 넣고 spy++로 확인해 보니

    ReDrawWindow의 경우는 WM_PAINT를 Send시키고 바로 처리를 한후 리턴하는데

    Invalidate의 경우는 WM_PAINT를 Post시키는 군요.

     

    아무튼 알려주셔서 감사합니다. 도움이 많이 되었습니다.

    이 글에 평점 주기: 
                                     [답변]... 2003-12-21 오후 11:55:13
    박민재 (MAXIST)  박민재님께 메시지 보내기박민재님을 내 주소록에 추가합니다.박민재님의 개인게시판 가기 번호: 406347   / 평점:  (-)  
    옙 내부적으론 그렇게 처리됩니다.

    첨언하자면, WM_PAINT는 우선 순위가 낮아서, Invalidate에서 Post가 되더라도, 항상 다 도착하지는 않습니다. Queue 안에 아직 처리되지 못한 WM_PAINT가 하나라도 존재하면 그냥 무시 당합니다. (따라서 Message Queue에는 WM_PAINT는 하나가 있거나 없거나 그렇죠.)
    반응형
    반응형
    출처 : 

    CreateThread는 생성된 쓰레드 내에서 CRL 계열의 함수를 사용할 때 TLS를 사용하지 않기 때문에 Thread-Safe하지 않은 문제가 있다는 것은 널리 알려져 있다. 대신 _beginthreadex 나 AfxBeginThread를 사용하라고들 얘길 하는데....

    AfxBeginThread 함수의 문제는 아니지만... AfxBeginThread를 다른 쓰레드 생성 함수들처럼 사용해버리면 문제가 되곤 한다.

    먼저 _beginthreadex로 쓰레드를 생성하는 샘플코드이다.

    1. CWinThread* pWinThread = NULL;  
    2. pWinThread  = AfxBeginThread(pfnThreadEntryFunc, this);  
    3. if (pWinThread == NULL)  
    4.     return FALSE;  
    5.   
    6. CloseHandle (pWinThread->m_hThread);  
    7. pWinThread->m_hThread = NULL;  

    스레드 생성시에는 스레드 내부적으로 별도의 핸들을 가지고 있기 때문에 _beginthreadex가 리턴하는 핸들까지 하면 두개의 핸들이 생성된다. 따라서, 생성한 스레드를 부모 스레드에서 계속 관리할 것이 아니라면 바로 CloseHandle 해버리는 것이 자원을 관리하는 FM이다. (그러지 않는다면 나중에 스레드가 종료된 후에도 스레드 Object가 파괴되지 않을 것이다. 왜냐구? 핸들카운트가 0이 되지 않았기 때문이지!)

    그런데... MFC계열의 AfxBeginThread는 사용법이 약간 다르다.
    MSDN의 함수 Prototype은 다음과 같다.
    CWinThread* AfxBeginThread( 
                        AFX_THREADPROC
     pfnThreadProc, 
                             LPVOID pParam, 
                             int nPriority = THREAD_PRIORITY_NORMAL,
                             UINT nStackSize = 0, 
                             DWORD dwCreateFlags = 0, 
                             LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL 
                        );

    일단... 리턴값이 다르다. 스레드의 HANDLE이 아니라 CWinThread 클래스의 포인터이다.
    CWinThread? 이건 또 모야... MFC코드에는 다음과 같이 선언되어 있다.
    class CWinThread : public CCmdTarget
    {
        DECLARE_DYNAMIC(CWinThread)

    public:
    // Constructors
        CWinThread();
       // ... 중간생략

        // only valid while running
        HANDLE m_hThread;       // this thread's HANDLE
        operator HANDLE() const;
        DWORD m_nThreadID;      // this thread's ID

    아항... CWinThread의 내부에 스레드 핸들을 가지고 있군.
    흠. 그렇다면 이렇게 코드를 써주면 되겠구만

    1. CWinThread* pWinThread = NULL;  
    2. pWinThread  = AfxBeginThread(pfnThreadEntryFunc, this);  
    3. if (pWinThread == NULL)  
    4.     return FALSE;  
    5.   
    6. CloseHandle (pWinThread->m_hThread);  

    바로 이게 오바질이 되것다.
    저렇게 하면... 스레드 종료할 때(정확히 말하면 CWinThread 개체 파괴시) 원인불명의 메모리 폴트가 뜨게 된다. 왜냐구?
    AfxBeginThread에 의해 생성된 스레드가 종료될 때 CWinThread의 소멸자가 호출되는데, 이 소멸자 내에서 CWinThread 개체의 m_hHandle을 CloseHandle하도록 되어 있다. 이때 이미 Close된 핸들을 다시 Close하려고 시도하니까 문제가 발생하는 것이다.

    AfxBeginThread로 스레드를 생성했을 경우에는 굳이 CloseHandle을 해줄 필요가 없다.
    꼭 해주려면 다음과 같이 해주면 에러가 발생하지 않는다.

    1. CWinThread* pWinThread = NULL;  
    2. pWinThread  = AfxBeginThread(pfnThreadEntryFunc, this);  
    3. if (pWinThread == NULL)  
    4.     return FALSE;  
    5.   
    6. CloseHandle (pWinThread->m_hThread);  
    7. pWinThread->m_hThread = NULL;  



    ※ 만약 부모 스레드에서 자식스레드의 상태를 계속 관리해주어야 하는 경우에는
    다음과 같이 스레드 종료 후 CWinThread가 자동 파괴되지 않도록 해주어야 한다.

    1. CWinThread* pWinThread = NULL;  
    2. pWinThread  = AfxBeginThread(pfnThreadEntryFunc, this);  
    3. if (pWinThread == NULL)  
    4.     return FALSE;  
    5.   
    6. pWinThread->m_bAutoDelete = FALSE; // 자동파괴되지 않도록 설정  
    7.   
    8. // 여기서 WaitFor.. 나 GetExitCodeThread 같은 관리 코드를...  
    9. // ...  
    반응형

    + Recent posts