[64비트 윈도우 프로그래밍] ③ 64비트 프로그램으로 가는 길

일반입력 :2008/09/11 10:35

유병인 (안철수연구소)

19세기말 노르웨이의 화가 에드바르트 뭉크가 남긴 ‘절규’는 노을이 비낀 하늘과 멀리 펼쳐진 북유럽의 해안선을 배경으로 불안한 얼굴을 한 남자를 그린 작품이다. 그동안 32비트 컴퓨팅에 안주해 있던 우리들은 이제 그림 속의 남자와 같이 혼란스러운 64비트 컴퓨팅 시대를 눈앞에 두고 있다.

이번 회에서는 혼돈 속에 작은 빛이 되기 바라는 마음으로 64비트 응용 프로그램을 개발하기 위한 환경 구축 방법과 새로운 소스를 개발하거나 기존의 32비트 소스를 포팅할 때 유념해야 할 사항들, 그리고 컴파일러의 적극적인 활용법에 대해 설명하겠다.

연재 가이드

운영체제 : 64비트 윈도우 XP/64비트 윈도우 2003

개발언어 : 에디터, 플랫폼 SDK 최신판, 비주얼 스튜디오 닷넷 2003, 비주얼 스튜디오 닷넷 2005

기초지식 : C/C++ 프로그래밍/윈도우 구조 이해

응용분야 : 64비트 윈도우에서 동작하는 모든 프로그램

지난 2회에 걸쳐 64비트 윈도우에서 32비트 응용 프로그램을 WOW64 호환모드 위에서 동작하도록 개발하는 방법과 64비트 윈도우의 달라진 점을 개발자의 관점에서 설명했다.

하지만 이번 연재를 접한 독자들은 아직까지도 막상 64비트 응용 프로그램을 어떻게 개발해야 할지 막막할 수 있을 것이다. 64비트 응용 프로그램은 64비트 윈도우에서 개발해야 하는 걸까? 컴파일은 어디서 해야 하고 테스트는 어떻게 해야 하는 걸까? 64비트 개발 환경은 있는 것인가? 등등의 수많은 물음이 생길 것인데, 이번 회에는 그 물음에 작으나마 도움이 되는 내용을 언급하려고 한다.

먼저 32비트와 64비트 환경이 공존하는 지금 응용 프로그램 개발 모델은 어떻게 설정해야 할까? MS에서 권장하고 있는 개발 모델은 <그림 1>과 같이 32비트 윈도우에서 32비트와 64비트 코드를 하나로 개발하고 컴파일하며 32비트 응용 프로그램은 32비트 윈도우에서 테스트하고 64비트 응용 프로그램은 64비트 윈도우에서 테스트하는 것이다.

심지어 완벽한 64비트 개발환경 지원을 목표로 하는 비주얼 스튜디오 닷넷 2005(아직까지는 베타이므로 변경될 수도 있다)도 64비트 윈도우에서는 WOW64에서 동작하기 때문에 32비트/64비트 소스의 작성 및 컴파일은 32비트 환경에서 하는 것이 풍부한 32비트 개발 지원 도구를 사용할 수 있다는 점에서 훨씬 유리하다. 결론적으로 32비트 윈도우에서 크로스 컴파일된 64비트 실행 파일을 64비트 윈도우에서 실행하고 테스트하면서 남아 있는 버그를 잡는 것이 훨씬 효율적인 개발 모델인 것이다.

무엇을 준비해야 하는가?

이제 드디어 64비트 응용 프로그램을 만들어야 할 시간이다. 현재 64비트 응용 프로그램을 개발하기 위한 환경은 아직 완벽하지 않은 상태라고 할 수 있다. 키를 쥐고 있는 MS에서 제공하는 방법은 몇 가지 개발 가이드 정도를 제공하고 있다. 무엇보다 64비트 응용 프로그램을 개발하기 위해 가장 어려운 점 중 하나는 아직까지 공식 64비트 컴파일러와 통합개발환경이 제공되고 있지 않다는데 있다.

MS는 올해 상반기에 출시 예정인 비주얼 스튜디오 닷넷 2005를 통해 x64 및 IA64 플랫폼에서 수행 가능한 응용 프로그램을 통합개발환경에서 원활하게 개발할 수 있도록 계획하고 있다. 그렇다면 비주얼 스튜디오 닷넷 2005가 정식 출시될 때까지 우리는 64비트와 32비트가 맞물린 이 혼란의 시대에 가만히 앉아 있어야 할 것인가? MS가 제공하는 플랫폼 SDK에 그 해답이 있다.

이가 없으면 잇몸으로 - 플랫폼 SDK와 비주얼 스튜디오 닷넷 2003

현재 플랫폼 SDK는 64비트 응용 프로그램을 개발하기 위해 필요한 가장 기본적인 툴킷이다. 플랫폼 SDK에는 64비트 응용 프로그램을 개발하기 위한 C/C++ 컴파일러와 라이브러리 및 헤더 그리고 기본적인 디버깅 도구를 포함하고 있다. 그래서 64비트 응용 프로그램을 개발하기 위해서는 플랫폼 SDK 최신판과 비주얼 스튜디오 닷넷 2003이 필요하다. 플랫폼 SDK를 설치하기 위해서는 어떻게 해야 할까? 관련된 MS 웹사이트를 통해 설치할 수 있다(방법은 그다지 어렵지 않으므로 각자 설치해 보자).

플랫폼 SDK의 설치를 마친 후에는 플랫폼 SDK에서 제공하는 64비트 빌드 환경을 이해해야 한다. 플랫폼 SDK는 미리 준비된 5개의 빌드 환경을 제공하는데 이 중에서 64비트 빌드 환경(윈도우 서버 2003 64비트 및 윈도우 XP 64비트)을 찾아 볼 수 있고 각 빌드 환경을 실행하면 지정한 빌드 환경이 설정된 커맨드 윈도우가 실행된다. 이 빌드 환경은 「시작메뉴->프로그램->플랫폼 SDK 설치 그룹->Open Build Environment Window」에서 찾아볼 수 있다.

이 빌드 환경들은 내부적으로는 플랫폼 SDK가 설치된 디렉토리에 존재하는 SetEnv.bat 파일을 특정 파라미터로 실행해 구성된 환경이다. 시작 프로그램에 미리 등록된 빌드 환경을 실행하지 않고 개발자가 직접 SetEnv.bat을 특정 파라미터로 실행해 빌드 환경을 변경할 수도 있는데 그 방법은 ‘SetEnv.bat /?’ 로 실행해 보면 어렵지 않게 알 수 있다. 이 빌드 환경을 구성하는 주요 요소는 다음과 같다.

◆ %LIB% : 라이브러리가 위치한 디렉토리

◆ %INCLUDE% : 헤더 파일이 위치한 디렉토리

◆ %PATH% : 컴파일러, 링커, 메이커, 여러 가지 SDK 도구들이 위치한 디렉토리

개발자가 특정 빌드 환경을 지정해 실행하면 그 빌드 환경애 맞도록 %LIB%, %INCLUDE%, %PATH%가 설정되어 실행된다. 이 빌드 환경을 정확하게 이해하기 위해 다음과 같은 순서로 테스트를 해보자.

[1] C나 C++로 된 간단한 응용 프로그램 하나를 작성한다.

[2] 플랫폼 SDK의 빌드 환경 중 Set Windows XP 64-bit Build Environment (Debug)을 실행해 보자.

[3] 커맨드 윈도우가 나타나면 작성한 응용 프로그램을 다음과 같이 컴파일하자. 참고로 /Wp64는 아주 유용하며 필수적인 옵션으로 작성된 소스에서 64비트 프로그램으로서 적합하지 않은 부분을 자동으로 검출해 경고해 준다.

cl /c /EHsc /Wp64 64test.cpp

[4] 생성된 오브젝트의 링킹을 수행해서 빌드를 마무리하자.

link /out:64test.exe /machine:IA64 64test.obj

[5] 생성된 실행 파일이 32비트 윈도우에서 실행되지 않고 64비트 윈도우로 복사해서 수행되는지 확인해 보자.

지금까지 한 작업을 간단히 정리해 보면 64비트 응용 프로그램을 개발하기 위해서는 MS에서 제공하는 기존의 컴파일러로는 불가능하기 때문에 플랫폼 SDK를 설치해야 하고 설치한 후에는 64비트 빌드 환경을 설정하고 간단히 커맨드 윈도우에서 소스를 컴파일 및 링킹하는 방법을 설명해 보았다.

커맨드 윈도우에서 컴파일 및 링킹 작업에 익숙한 개발자라면 여기까지만 설명을 해도 더 이상 플랫폼 SDK를 이용한 방법을 설명할 필요가 없겠지만 대부분의 개발자들은 통합개발환경인 IDE에 익숙하기 때문에 비주얼 스튜디오 2003과 플랫폼 SDK를 이용한 64비트 개발환경 구축이 필요하다. 이 방법도 다음과 같은 순서대로 따라하면 그다지 어렵지 않게 습득할 수 있다.

[1] C나 C++로 된 간단한 응용 프로그램 하나를 작성한다.

[2] 플랫폼 SDK의 빌드 환경 중 Set Windows XP 64-bit Build Environment (Debug)을 실행해 보자.

[3] 비주얼 스튜디오 닷넷을 다음과 같이 실행한다. 참고로 devenv는 일반적으로 C:Program FilesMicrosoft Visual Studio .NET 2003Common7IDE에 존재한다.

devenv /useenv

[4] 비주얼 스튜디오 닷넷 2003 통합 환경에서 작성한 응용 프로그램 프로젝트를 연다.

[5] 통합환경 메뉴의 「빌드->구성관리자」에서 <화면 1>과 같이 64BitDebug라는 새로운 솔루션 구성을 추가한다. 이때 기본 구성은 Debug 설정을 복사하도록 한다.

[6] 통합환경 메뉴의 「프로젝트->프로젝트 속성페이지」에서 몇 가지 정보를 조정한다. 먼저 디버깅 정보 형식을 <화면 2>와 같이 ‘프로그램 데이터베이스(/Zi)’로 변경한다. ‘편집하며 계속하기를 위한 프로그램 데이터베이스(/ZI)’ 형식은 플랫폼 SDK에서 제공하는 64비트 컴파일러에서는 지원하지 않기 때문이다.

[7] 계속해서 <화면 3>과 같이 프로젝트 속성 페이지의 ‘코드 생성’에 있는 ‘버퍼 보안 검사’를 ‘아니오’로 변경한다.

[8] 계속해서 <화면 4>와 같이 프로젝트 속성 페이지의 「링커-명령줄」에 ‘/MACHINE:IA64’ 추가 옵션을 입력한다.

[9] 앞 단계에서 ‘/MACHINE:IA64’로 설정했기 때문에 <화면 5>와 같이 「링커-고급」에서 ‘대상 컴퓨터’를 ‘설정 안함’으로 변경한다.

[10] 통합환경에서 프로젝트를 빌드하고 생성된 실행 파일이 32비트 윈도우에서 실행되지 않고 64비트 윈도우로 복사해서 수행되는지 확인해 보자.

이제까지 플랫폼 SDK와 비주얼 스튜디오 2003을 이용한 64비트 응용 프로그램 개발 환경 구축을 살펴봤다. 참고로 플랫폼 SDK로는 IA64 계열에 비해 아쉽게도 x64 계열은 완벽히 지원하지 않고 있는 상황이어서 x64 계열 응용 프로그램을 작성하기는 어렵다. 또한 managed C++를 사용한 프로그램을 작성할 수 없고 native C++로 작성된 프로그램만 지원한다. x64 계열과 IA64 계열을 모두 지원하고 managed C++를 지원하는 개발환경은 아직 없는 것일까? 완벽한 지원을 기대한다면 올 상반기 출시 예정인 비주얼 스튜디오 닷넷 2005를 기다려봐야 할 것 같다.

64비트 개발을 위해 준비된 개발도구 - 비주얼 스튜디오 닷넷 2005

닷넷 프레임워크 2.0과 완벽한 64비트 개발 환경 지원을 탑재한 비주얼 스튜디오 닷넷 2005(코드명 : Widbey)는 <화면 6>과 같이 통합개발환경에 존재하는 구성관리자의 플랫폼 항목에서 x64와 IA64를 선택할 수 있어서 앞서 설명한 바와 같이 플랫폼 SDK의 64비트 헤더와 라이브러리 및 컴파일러를 사용하지 않아도 된다. 또한 native C./C++ 및 managed C++도 64비트 컴파일을 할 수 있는 64비트 응용 프로그램 개발을 하기 위한 완전한 개발 환경이다.

현재까지는 베타테스트가 진행 중이므로 웹사이트에서 각자 다운받아 설치후 사용해 보기 바란다.

32비트에서 64비트로 이사가기

64비트 컴퓨팅 세상이 온다고는 하지만 아직까지는 32비트 컴퓨팅을 위한 소스가 많이 존재하는 것이 현실이고 64비트 윈도우를 위한 소스를 새로 개발하기 보다는 기존 소스를 64비트 윈도우에서도 동작할 수 있도록 포팅하는 것이 대부분의 경우 현실적일 것이다.

개발자 측면에서 볼 때 기존 16비트 윈도우 응용 프로그램 소스를 32비트 응용 프로그램 소스로 포팅하는 것보다는 32비트 소스를 64비트 소스로 포팅하는 것이 훨씬 손쉬운 작업이다, 그러나 쉽다고 해서 무작정 할 수 있는 것은 아니고 포팅 가이드라인을 정하고 계획을 세우고 개발을 한 후 실제 64비트 윈도우에서 테스트해 보는 일련의 공정을 거쳐야만 한다.

64비트 포팅 가이드라인

포팅 계획 세우기

포팅 계획을 세우기에 앞서 먼저 포팅 작업을 위한 작업량을 측정해야 한다. 작업량을 측정할 수 있는 요소로는 첫 번째로 현재 32비트 응용 프로그램 소스의 문제를 먼저 인식해야 한다. 그래서 64비트 컴파일러로 일단 컴파일을 해 보고 나타나는 오류와 경고들을 알아야 한다.

두 번째로는 공유해서 사용하는 모듈들의 리스트와 모듈간의 사용 관계를 파악해야 한다. 그래서 사용하고 있는 모듈 중 다른 팀 또는 다름 개발회사에서 개발한 모듈이 있다면 그 모듈이 64비트로 포팅할 계획을 수립하고 있는지 점검해야 한다.

세 번째로는 하위 호환을 위한 코드나 어셈블리 코드를 점검해야 한다. 예를 들어 64비트 윈도우에서는 16비트 응용 프로그램을 구동할 수 없기 때문에 16비트 윈도우 응용 프로그램 코드는 더 이상 64비트 윈도우에서 동작하지 않기 때문이다. 반면에 x86 어셈블리 코드는 WOW64에서 동작하고 x64의 경우 성능에도 별다른 차이가 없지만 아이테니엄을 사용하는 IA64의 경우 성능의 저하가 있을 수 있기 때문에 성능 향상을 위해서는 x86 어셈블리 코드를 IA64 어셈블리 코드로 변경하는 것을 고려해봐야 한다.

작업량 측정이 끝났다면 전체 응용 프로그램을 포팅할 계획을 세워야 한다. 이때 일부분만을 포팅하기는 어렵다. 만약 부득이한 경우 거의 소스를 변경하지 않고 32비트 소스를 64비트 응용 프로그램으로 컴파일할 수 있는 방법이 있는데 ‘/LARGEADDRESSAWARE:NO’ 옵션을 사용하는 것이다. 이 옵션은 생성된 코드 내의 모든 포인터와 데이터가 32비트 프로세스와 같이 2G안에서 동작하도록 하는 옵션이다. 그러나 이 옵션은 매우 단기적인 처방이며 같이 사용하는 모든 모듈이 이 방식으로 컴파일되어야 하므로 향후 더 많은 문제점을 발생시킬 수 있다.

또한 개발에 사용되는 기술들을 검토해봐야 하는데 DAO(Data Access Object)나 Jet 데이터베이스 엔진의 경우 64비트 윈도우에서 지원하지 않기 때문에 소스 내에서 이런 기술을 사용했다면 유사한 다른 기술을 사용할 것을 검토해봐야 한다. 그리고 하나의 소스에서 생성되는 32비트와 64비트 응용 프로그램을 별개의 제품군으로 취급하고 출시하는 것을 권장한다. 왜냐하면 소스는 공유하지만 테스트 및 제품 마스터링, 마케팅 및 고객지원 이슈가 다르기 때문이다.

개발하기

아직까지 비주얼 스튜디오 닷넷 2005와 같은 완전한 64비트 개발환경이 구축되어 있지는 않지만 신속히 64비트 응용 프로그램을 출시하려는 개발자라면 64비트 응용 프로그램 개발을 늦출 수는 없다. 그러므로 앞서 설명한 플랫폼 SDK를 이용해 먼저 64비트 응용 프로그램을 작성하는 것이 좋다.

그리고 여러 번 강조하지만 하나의 소스코드로 32비트와 64비트 응용 프로그램이 컴파일될 수 있도록 해야 한다. 이것이 바로 이렇게 하위호환성을 유지하고 하나의 코드로 32비트와 64비트를 개발하게 하기 위해 LLP64 데이터 모델을 채택한 이유이기 때문이다. MS의 윈도우와 SQL 서버도 하나의 소스코드에서 32비트와 64비트 제품군을 빌드했다고 한다. 그리고 뒤에 자세히 언급하겠지만 컴파일러에 새롭게 추가된 최적화 특징들을 사용할 것을 권장한다. 다음은 상세한 포팅 순서이다.

[1] 64비트 프로젝트를 구성한다. VC++ 6.0으로 작성된 프로젝트라면 VS 2003/VS 2005를 이용해서 포팅하려는 타입(APP/LIB/DLL)에 맞는 64비트 빈 프로젝트를 만들고 VC++ 6.0의 프로젝트를 참조해서 파일들을 64비트 프로젝트에 추가한다.

[2] 64비트 프로젝트에 맞게 프로젝트 설정들을 변경한다. 일반적으로 신경써서 맞춰줘야 하는 부분은 다음과 같다.

- 헤더 포함 디렉토리/라이브러리 디렉토리/라이브러리 파일

- Precompiled Header/기타 컴파일 옵션/기타 링킹 옵션 조정

[3] /Wp64 옵션을 켜고 컴파일을 해보면(/Wp64 옵션은 일반적으로 기본으로 켜져 있다) 경고/오류가 나타나는데 일반적으로 다음과 같은 내용들이 나온다.

- 최신 C++ 표준을 적용해 발생하는 템플릿 사용 또는 기본 인자에 관련된 오류/경고

- 타입 캐스팅되면서 발생하는 데이터 손실 경고

- MFC를 오버라이딩해서 사용할 때 함수 파라미터/리턴 타입의 변경된 부분에 대한 오류

- INVALID_HANDLE_VALUE 대신 0xFFFFFFFF를 사용한 경우

- 사용하는 외부 라이브러리도 모두 64비트이어야 하는데 32비트 라이브러리를 사용할 경우

[4] 발생한 오류와 경고를 MSDN을 참조해 해결한다.

[5] 여러 개의 모듈로 제품이 구성되어 있을 경우 기본 라이브러리->DLL->실행파일 순으로 전체 프로젝트를 포팅해야 한다.

테스트하기

먼저 테스트할 프로그램이 순수하게 64비트로 구성된 프로그램인지 아니면 WOW64를 사용하는 부분도 있는지를 검토해야 한다. 이러한 것은 테스트 계획 수립단계에서 결정되어야 하며 이에 따라서 테스트 도구가 64비트용만 있으면 되는지 아니면 32비트와 64비트용이 모두 필요한지가 결정이 되고 테스트 환경 구축 및 일정 산정에도 도움이 되기 때문이다.

일반적으로 32비트와 64비트 프로그램 모두 같은 소스를 기반으로 하기 때문에 테스트 단계에서의 버그 수정은 64비트 환경에서 테스트하고 버그를 수정한 후 32비트 환경에서 부작용이 없는지 재컴파일 한 후 테스트하고 다시 64비트 환경에서 재컴파일한 후 테스트해야 한다. COM이나 RPC를 사용할 경우에는 32비트와 64비트 모두 정상적으로 통신하는지 테스트돼야 하고 필요할 경우 네트워크 너머에 존재하는 16비트 컴퍼넌트들과 네트워크 통신에 이상이 없는지도 테스트돼야 한다.

때에 따라서는 32비트 윈도우를 위해 개발된 제품도 64비트 윈도우에서 테스트돼야 하는데 제품을 사용하는 고객들이 32비트 응용 프로그램을 64비트 윈도우에서 WOW64를 통해 사용할 수도 있기 때문이다. 그밖에 64비트 윈도우에서는 32비트 윈도우에 비해 메모리 사용방법이 크게 변경되었기 때문에 많은 메모리를 탑재한 시스템에서도 테스트를 해봐야 한다.

포인터를 다룰 때 신경써야 할 것들

64비트 윈도우가 채택한 LLP64 데이터 모델은 기존의 데이터 타입은 호환성을 위해 32비트를 유지하고 포인터를 64비트로 변경한 데이터 모델이다. 그래서 64비트 응용 프로그램을 개발하기 위해서는 다른 어떤 데이터보다 포인터를 다룰 때 주의해야 한다. 이번 절에서는 포인터를 다룰 때 신경써야 할 사항 몇 가지를 살펴보자.

◆ 포인터를 int, long, , UINT, ULONG, DWORD.으로 타입 캐스팅하지 말아야 한다

비트 수준의 작업을 하기 위해 포인터를 타입 캐스팅해야 할 때는 반드시 32비트와 64비트 윈도우에서 모두 사용할 수 있는 타입인 UINT_PTR, INT_PTR을 사용해야 한다. 64비트 윈도우에서는 포인터의 크기가 64비트이고 int나 long은 32비트이기 때문에 포인터를 int, long, ULONG, DWORD로 타입캐스팅 해서는 안 된다. 다음 예를 참고하자.

<포인터를 잘못 타입캐스팅 한 예>

imageBase = (PVOID)((ULONG)ImageBase

<포인터를 정상적으로 타입캐스팅 한 예>

imageBase = (PVOID)((ULONG_PTR)ImageBase

참고로 HANDLE도 void*로 정의되어 있기 때문에 HANDLE도 포인터와 마찬가지로 취급해야 한다.

◆ 부득이하게 포인터의 데이터가 손실되더라도 32비트로 타입 캐스팅해야 할 경우에는 PtrToLong()이나 PtrToUlong() 함수를 이용해야 한다

이 함수들은 Basetsd.h에 정의되어 있다. 이 함수들을 사용하면 데이터 손실 경고를 발생시키지 않는다. 이 함수들은 조심해서 사용해야 하는데 포인터가 타입 캐스팅된 이후에는 데이터가 손실된 것이므로 이 값들을 포인터로 다시는 사용해서는 안 된다. 이 함수들은 상위 32비트 값들을 얻어낸다.

◆ 값을 얻어오는 OUT 형태의 인자를 사용할 때 주의해야 한다

다음과 같이 정의된 함수를 예로 생각해 보자.

void func( OUT PULONG *PointerToUlong );

이 함수를 다음의 예와 같이 호출해서는 안 된다. &ul을 PULONG*으로 타입캐스팅을 했기 때문에 컴파일러 오류는 발생하지 않지만 함수에서는 &ul에 64비트 포인터 값을 쓰게 된다. 이 코드는 32비트 윈도우에서는 정상적으로 동작하겠지만 64비트 윈도우에서는 포인터 데이터의 손실이 발생하게 된다.

ULONG ul;

PULONG lp;

func((PULONG *)&ul);

lp = (PULONG)ul;

대신에 다음과 같이 호출해야 한다.

PULONG lp;

func(&lp);

◆ 다형성 인자를 사용할 경우 UINT_PTR이나 PVOID를 사용해야 한다

32비트 윈도우에서는 어떤 타입의 데이터를 넘길지 결정할 수 없을 때 DWORD 등을 함수의 인자로 사용해서 인자를 넘길 때 타입 캐스팅을 해서 사용하기도 했었다. 그러나 64비트 윈도우에서는 반드시 UINT_PTR이나 PVOID와 같은 64비트 윈도우와 32비트 윈도우에서 모두 사용할 수 있는 데이터 형을 사용해야 한다.

◆ 새로운 윈도우 클래스 함수를 이용해야 한다

윈도우나 클래스에 포인터 데이터를 포함하고 있다면 새로운 윈도우 클래스 함수(GetClassLongPtr/GetWindowLongPtr/SetClassLongPtr/SetWindowLongPtr)를 사용해야 한다. 또한 64비트 응용 프로그램 개발 환경의 WInUser.h에는 GWL_WNDPROC/GWL_HINSTANCE/GWL_HWDPARENT/GWL_USERDATA은 정의되어 있지 않고 대신에 GWLP_WNDPROC/GWLP_HINSTANCE/GWLP_HWNDPARENT/

GWLP_USERDATA/GWLP_ID가 정의되어 있다. 예를 들어 다음과 같은 코드는 64비트 윈도우에서는 동작하지 않는다.

SetWindowLong(hWnd, GWL_WNDPROC, (LONG)MyWndProc);

이 예를 64비트 윈도우에서 동작하도록 수정하면 다음과 같다.

SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)MyWndProc);

이밖에도 WNDCLASS 구조체의 cbWindExtra 멤버를 추가적인 값을 저장하기 위해 사용하려고 할 때 기존에는 sizeof(DWORD) 크기만큼만 할당해도 되었지만 64비트 윈도우에서는 sizeof(DWORD_PTR)만큼을 할당해 사용해야 한다.

◆ 윈도우나 클레스의 데이터를 접근할 때에 FIELD_OFFSET.을 이용해야 한다

기존에는 윈도우 데이터를 접근할 때 오프셋 크기를 4바이트로 가정하고 하드코딩해 접근하기도 했다. 하지만 64비트 윈도우에서는 절대 사용해서는 안 되고 대신에 윈도우나 클래스 데이터를 접근할 때는 FIELD_OFFSET 매크로를 사용해야 한다.

◆ 플랫폼에 따라 LPARAM, WPARAM, LRESULT 타입의 크기는 변한다

이 타입들은 포인터나 다형성 인자로 사용되는 타입이기 때문에 64비트 코드로 컴파일될 때 이 타입들은 64비트 크기로 컴파일된다. 그러므로 64비트 환경에서 정상적으로 동작하게 하려면 LPARAM, WPARAM, LRESULT 들을 32비트의 크기를 가지는 DWORD, ULONG, UINT, INT, int, long 값들과 같이 사용하면 안 된다.

구조체를 다룰 때 신경써야 할 것들

64비트 윈도우에서는 32비트 윈도우와 달리 데이터의 팩킹을 8바이트 단위를 기본으로 한다. 그래서 구조체 내의 구성요소를 적절히 배치하는 것이 구조체가 실제 메모리에서 차지하는 양을 줄일 수 있는 좋은 방법이다. 또한 #pragma pack을 지정해 소스를 작성하는 것에 주의해야 하는데 기본 패킹인 8바이트를 4바이트나 2바이트, 심지어는 1바이트로 변경할 경우 코드 크기가 증가할 뿐만 아니라 동적인 메모리의 사용량도 늘어날 수밖에 없다.

그러므로 64비트 윈도우에서 동작하는 응용 프로그램 소스를 작성할 때는 구조체의 구성요소 배치 및 8바이트 정렬에 신경을 써야 하고 팩킹 크기에 의존적인 코드를 작성하지 않아야 한다. 그러나 예를 들어 유닉스, 32비트 윈도우, 64비트 윈도우 등에서 모두 동작하게 하려는 코드의 경우 어쩔 수 없이 패킹을 최소공배수로 조정해야 할 수도 있지만 그런 경우 코드 크기 증가와 처리 속도의 저하가 있을 수 있다는 것을 항상 고려해야 한다.

신경써야 할 다른 여러 가지 것들

앞서 설명한 것들 외에도 64비트 응용 프로그램을 개발할 때는 다음과 같이 신경써야 할 것들이 몇 가지 더 있다.

◆ 32비트 윈도우에서 사용하던 라이브러리나 프레임워크 중에 DAO나 VBA와 같이 64비트에서 더 이상 사용할 수 없는 것들이 있으므로 64비트 응용 프로그램의 개발을 할 때 사용하려는 라이브러리나 프레임 워크가 64비트 윈도우를 지원하는지 확인해봐야 한다.

◆ 사용하는 API가 64비트 윈도우용인지도 살펴봐야 한다. 대부분의 윈도우 API는 64비트에서 사용 가능하지만 일부 API는 변경된 것이 있으므로 사용하는 API의 지원 여부를 MSDN을 통해서 항상 확인해야 한다.

◆ 비트연산에 대해서도 신경써야 하는데 기존의 32비트 윈도우에서는 -1을 비교할 때나 INVALID_HANDLE 대신 0xffffffff를 사용하는 소스도 있었지만 64비트 윈도우에서는 사용해서는 안 되는 값이다. 또한 32비트 소스에서 1<<33과 같이 비트 시프트 연산을 할 경우 정상 동작하겠지만 64비트 소스에서는 유효하지 않는 코드이다. 이와 유사하게 고려할만한 점으로는 기존에 사용하던 소스의 알고리즘이 비트 크기에 관련 있을 경우 수정을 해야 한다.

◆ 데이터 크기를 알아 올 때에는 반드시 컴파일 타임에 데이터 타입의 크기를 알아올 때 사용하는 sizeof()를 사용하도록 해야 한다.

◆ 구조체 내의 특정 필드의 오프셋을 알고 싶을 때에는 컴파일 타임에 오프셋을 얻어 오는 FIELD_OFFSET 매크로를 이용해야 한다.

◆ 플랫폼에 의존적인 어셈블리 코드를 소스 내에 사용하지 말아야 하며 필요할 경우 컴파일 타임에 각 플랫폼 별로 어셈블리 코드를 사용하도록 해야 한다.

◆ 꼭 필요한 경우가 아니라면 명시적인 구조체 팩킹을 하지 말고 64비트 윈도우에서는 8바이트를 기본으로 패킹되는 것이 좋다.

◆ 인터페이스를 개발할 때는 플랫폼에 의존적이지 않은 잘 분리되고 추상화된 인터페이스로 만들어야 한다.

◆ 64비트 윈도우에서는 _WIN16, _WIN32외에도 _WIN64가 추가되었기 때문에 다음과 같은 가정은 더 이상 유효하지 않다.

#ifdef _WIN32 // Win32 code

...

#else // Win16 code

...

#endif

◆ 데이터 타입 정렬에 주의해야 한다. TYPE_ALIGNMENT 매크로는 지정한 데이터 타입의 정렬 바이트를 리턴하는 기능을 한다. 다음 코드를 참조하자.

TYPE_ALIGNMENT( DWORD_PTR ) == x86에서는4, IA64/x64에서는 8

TYPE_ALIGNMENT( UCHAR ) == 플랫폼에 상관없이 1

데이터 정렬은 상당히 중요한데 32비트 윈도우에서는 커널 모드와 사용자 모드 모두에서 데이터 정렬이 되어 있지 않아도 시스템이 자동으로 처리했지만 64비트 윈도우에서는 커널 모드의 경우 자동으로 데이터 정렬 예외를 처리하지 않기 때문이다. 그래서 반드시 데이터 정렬을 맞추어 코드를 작성해야 한다.

◆ NOT 오퍼레이션을 사용할 때 주의해야 한다. 다음과 같은 경우를 생각해 보자.

UINT_PTR a; ULONG b;

a = a & ~(b - 1);

이 코드의 문제는 ~(b?1) 연산이 생성하는 결과가 0xFFFF FFFF xxxx xxxx.이 아닌 0x0000 0000 xxxx xxxx 이라는데 있다. 그래서 다음과 같이 코드를 수정해야 한다.

a = a & ~((UINT_PTR)b - 1);

◆ unsigned와 signed 데이터 연산에 주의해야 한다. 다음과 같은 경우를 생각해 보자.

ULONG x; LONG y;

LONG *pVar1; LONG *pVar2;

pVar2 = pVar1 + y * (x - 1);

x가 unsigned이기 때문에 문제가 발생하는데 x로 인해 전체 수식이 unsigned로 바뀌기 때문이다. 이 수식에서 y가 음수만 아니라면 정상적으로 동작할 수도 있다. 만일 y가 음수라면 수식에서 y는 unsigned 값으로 변경되고 32비트 정밀도로 pvar1과 곱셈과 덧셈을 수행하게 되고 32비트 unsigned 음수 값은 64비트 양수 값으로 변경되게 되어서 비정상적인 결과가 생성된다. 이 문제를 해결하려면 x를 signed 값으로 선언하거나 수식 내에서 명시적으로 LONG으로 타입캐스팅해야 한다.

◆ 작은 크기의 데이터 할당을 할 때 주의해야 한다. 다음과 같은 예를 살펴보자.

struct xx {

DWORD NumberOfPointers;

PVOID Pointers[1];

};

다음 코드는 잘못된 코드인데 컴파일러는 8바이트 정렬을 맞추기 위해 4바이트를 추가적으로 패딩해서 할당할 것이기 때문이다.

malloc(sizeof(DWORD)+100*sizeof(PVOID));

이 코드를 다음과 같이 수정하면 된다.

malloc(offsetof(struct xx, Pointers) +100*sizeof(PVOID));

◆ printf나 wsprintf를 이용해 포인터를 출력할 때는 포인터를 출력하는 형식인 %p를 사용해야 한다. 32비트 윈도우에서는 다른 형태로 포인터를 출력해도 되었지만 64비트 윈도우에서는 포인터만이 64비트이기 때문이다.

컴파일러를 적극적으로 활용하자

64비트 개발환경에서는 새로운 최적화 옵션들이 추가되었다. 이 최적화 옵션들은 x64 계열보다는 IA64 계열에서 더욱 중요한데 이전의 x86 CPU들이 컴파일러의 최적화뿐만 아니라 CPU의 최적화 수행에도 의존했었지만 IA64의 경우 CPU에서의 최적화 수행보다는 컴파일러의 최적화가 더욱 중요해졌기 때문이다.

컴파일러에 새롭게 추가된 최적화 옵션은 크게 두 가지인데 하나는 수행성능 측정데이터를 이용한 최적화(Profile Guide Optimization)이고 다른 하나는 전체 프로그램 영역 기반의 최적화(Whole Program Optimization)이다.

수행성능 측정 데이터를 이용한 최적화는 두 단계의 컴파일 단계로 진행된다. 첫 번째 컴파일 단계에서는 생성된 프로그램을 주어진 테스트 상황에 따라 구동시키고 프로그램의 실행 패턴을 기록해 두었다가 이 정보를 이용해서 두 번째 컴파일 단계에 최적화 자료로 활용한다. 기존의 컴파일 방식을 생각해 보자. 다음의 간단한 두 코드 중 첫 번째를 참조해 보면 foo()와 bar() 함수 중 어떤 함수가 실제로 많이 호출될까? 즉 실제 코드가 수행될 경우 a가 b 보다 작은 경우는 몇 %나 될까? 두 번째 코드에서는 bar()가 반복 수행 되는 횟수는 얼마나 될까? 즉, count 값은 얼마나 될까?

이 값들은 컴파일 타임에는 결정할 수 없는 값으로서 코드가 수행 중에만 알 수 있는 값이다. 이러한 테스트 시나리오에 의해 생성된 이 값들을 코드 최적화에 활용하는 것이 수행성능 측정 데이터를 이용한 최적화이다. 전체 프로그램 영역 기반의 최적화 방법은 한 프로그램이 여러 개의 소스코드로 구성될 경우 각각의 소스코드 내에서만 최적화를 수행하는 것이 아니고 프로그램을 구성하는 모든 소스코드를 대상으로 최적화를 수행한다. 전체 프로그램 영역기반 최적화와 수행 성능 측정 데이터를 이용한 최적화를 적용하는 순서는 <그림 2>를 참고하면 손쉽게 알 수 있다.

MS에서 측정한 값에 의하면 모든 아키텍처에서 수행성능 측정 데이터를 이용한 최적화를 하게 되면 정수연산과 부동소수점 연산 모두 <그림 3>과 같이 성능이 향상된다고 한다.

LTCG를 적용했을 때의 성능 향상 비율(%) 인텔 아이테니엄 2 AMD AMD64 인텔 x86 정수 연산 부동소수점 연산

그러나 수행 성능 측정 데이터를 이용한 최적화가 항상 좋은 것만은 아니다. 예를 들어 입력 조건이 일정치 않은 응용 프로그램의 경우 코드가 자주 수행되거나 코드의 반복 수행 횟수 등이 그때그때 달라질 수 있기 때문이다. 일반적으로 수행 성능 측정 데이터를 이용한 최적화는 입력이 예측 가능한 범주 내에서 일정 분포를 유지할 경우 유용한 성능 최적화 방법이라고 할 수 있다.

결론적으로 수행 성능 측정 데이터를 이용한 최적화는 실제 사용자 환경과 유사한 테스트 시나리오를 사용해야만 좋은 최적화 결과를 얻을 수 있다.

64비트 컴파일러의 경고

앞에서 64비트 응용 프로그램을 개발하기 위한 환경을 구축하는 방법과 주의해야 할 점, 그리고 최적화하는 방법을 설명했다. 그렇다면 정작 64비트 소스의 컴파일을 수행한 후에 나타나는 컴파일 경고들은 어떤 것들이 있을까?이전에 설명했듯이 32비트와 64비트 응용 프로그램은 하나의 소스로 작성되는 것을 권장하고 있다.

대신 컴파일러에 ‘/Wp64’ 옵션을 제공하는데 이 스위치를 이용하면 컴파일을 수행할 때 소스 내에서 LLP64 데이터 모델, 즉 64비트 소스로서 문제가 있을 만한 포인터 타입 캐스팅, 데이터 타입 캐스팅, 기타 여러 가지 문제점들을 경고해 준다. 자세한 경고의 종류와 내용은 <표 1>을 참고하자.

이중 가장 많이 보게 되는 경고가 C4311인데 다음의 간단한 두 코드 중 첫 번째 코드의 경우에 발생하는 경고이다. 64비트 윈도우에서는 ULONG은 32비트이고 PUCHAR는 64비트이므로 첫 번째 코드대로라면 64비트를 32비트로 형변환하게 되어서 포인터의 정보 손실이 발생할 수 있다. 그러므로 두 번째 코드와 같이 ULONG_PTR을 사용하도록 소스를 수정하면 경고를 제거할 수 있다.

64비트 응용 프로그램을 개발하기 위해서는 컴파일러가 알려주는 이러한 기본적인 경고들을 모두 제거하는 것이 64비트 소스의 잠재적인 문제를 해결하고 코드의 오류 발생 가능성을 최소화하는 방법이므로 반드시 경고를 제거하도록 해야 한다.

변화의 시대를 두려워 말자

필자는 이번 연재를 통해 먼저 64비트 윈도우에서 32비트 응용 프로그램을 구동시키기 위한 방법을 언급했고 두 번째로 64비트 윈도우가 핵심적으로 변화된 점을 설명했으며, 마지막으로 64비트 윈도우에서 동작하는 순수한 64비트 응용 프로그램을 만드는 방법과 기존 32비트 소스를 64비트로 포팅하는 방법을 설명했다. 비록 짧은 설명이었지만 64비트 세상으로 가기 위한 작은 다리는 되었으리라고 생각한다.

흔히들 소프트웨어 분야는 아직 많은 기술들이 더 많이 개발되어야 하는 신생 학문이기 때문에 새로운 기술 습득에 어려움이 많다고 한다. 필자 역시 새로운 길로 가는 것을 두려워한다. 하지만 혼돈과 변화의 시대를 두려워하기 보다는 적극적으로 받아들여 그 시대의 주인이 되는 것이 최근 개발자들이 겪고 있는 정체성에 대한 답이 되지 않을까 생각한다. 무언가 필요한 것을 만들어내는 사람은 개발자라는 것을 잊지 말고 다가오는 64비트 세상을 주인이 되기를 바라며 이번 연재를 마치려 한다.@

* 이 기사는 ZDNet Korea의 제휴매체인 마이크로소프트웨어에 게재된 내용입니다.