반응형

제가 2004년도에 윈도우 64비트 프로그래밍을 시작할 때 작성했던 글인데, 우연히 발견하게 되어서 올립니다.

요 약

윈도우즈 운영체제가 32비트에서 64비트로 확장함에 따라 처리 속도 및 효율성 측면에서 성능이 많이 좋아졌다. 그러나, 그 성능을 제대로 발휘하기 위해 대부분 기존의 32비트 프로그램을 64비트로 포팅해야 한다. 그러나 주소 지정 방식이 과거 32비트에서 64비트로 확장됨에 따라 포팅시 고려해야 할 사항들을 정확히 알아야지만, 성공적으로 진행할 수 있을 것이다. 여기서는 윈도우 운영체제의 과거 32비트에서 64비트 플랫폼으로 마이그레이션할 수 있는 포팅 테크닉을 소개한다.

27, Feb, 2004

Written by 크레이지제이

I. 서론

윈도우즈 64비트 운영체제는 개발은 몇 년 전에 되었지만, 아직 베타 테스트 중이다. 앞으로 상용화가 얼마나 잘 이루어질지 모르지만, 사용하는 곳이 많아진다면 소프트웨어나 하드웨어 관련 업체는 모두 적지 않은 영향을 받을 것이다.

64비트 운영체제에서 기존의 32비트 어플리케이션을 지원한다고는 하지만 32비트 레이어를 통해서 내부적으로 64비트 연산 작업이 이루어지기 때문에 속도가 저하될 가능성이 크다. (즉, 주소 자동 변환 과정을 한 번 더 통과해야 한다.) 따라서 64비트 어플리케이션으로 포팅을 하면 별도의 레이어를 통하지 않고, 바로 명령이 수행되기 때문에 높은 성능을 발휘할 것이다.

본 페이퍼에서는 32비트 윈도우 어플리케이션 및 디바이스 드라이버를 64비트로 포팅하기 위한 테크닉 및 Know-How를 기술하는데, 주된 목적은 어떻게 하면 보다 기존의 소스를 그대로 유지하여 32비트, 64비트를 모두 지원하는 방면을 찾는 것이다. 이로 하여금 개발자들의 소스 관리를 편하게 하여, 버그의 발생을 줄일 수 있으며 형상 관리에도 도울을 줄 수 있을 것이다.

II. 윈도우 64비트 운영체제

마이크로소프트에서 현재 64비트를 지원하는 운영체제는 Windows 2003 Enterprise Edition 64-bit, Windows 2003 Datacenter Edition 64-bit, Windows XP 64-bit 가 있다. 아직 국내에는 제대로된 64비트 윈도우 운영체제가 출시되지는 않았다.

윈도우즈 32비트 운영체제에서는 4기가까지의 물리적인 메모리를 지원하나 64비트 운영체제에서는 16 테라 바이트까지 지원할 수 있다. 이로 인해 어플리케이션은 다음과 같은 장점이 생긴다.

메모리 확장으로 인해 어플리케이션에서 더 많은 사용자를 지원할 수 있으며, 보다 높은 성능을 내고, 데이터를 저장 또는 가공 시 더 많은 메모리를 할당할 수 있어 DiskIO가 적게 발생한다.

 

 

III. 64비트 포팅시 주의 사항

64비트 포팅을 위해서는 새로운 데이터 타입이나 Helper 함수들을 사용하기 위해 Platform SDK를 설치해야 한다. Visual Studio 개발도구나 Makefile에서는 Include 디렉토리의 우선 순위를 일반 VC의 include보다 Platform SDK의 include가 높게 해야 한다. (같은 헤더 파일명이 존재할 시, 먼저 참조되도록 해야 한다.)

다음은 32비트 프로그래밍과 64비트 프로그래밍을 하는 경우에 자료구조나 프로그램 구조의 변화나 고려해야될 사항들을 설명하였다.

 

1. 데이터 타입

 

- 과거 데이터 타입 ; 대부분은 변하지 않았으나, 포인터 타입이 64비트로 확장되었다.

* 사이즈가 변경되지 않은 데이터 타입

- char/uchar=8bit, short/ushort=16bits,

- long/ulong=32bits

- LONG과 ULONG = (32 bits)

- INT, UINT, DWORD = (32bits)

- LONGLONG, ULONGLONG = (64bits)

 

* 사이즈가 변경된 데이터 타입

- pointer(char*, PDEVICE_OBJECT... ) = 32bits에서 64bits로 확장되었다.

 

- 새로운 데이터 타입 ; ULONG_PTR, LONG_PTR 타입은 32비트 컴파일러로 컴파일하면 32비트 사이즈로 인식되며, 64비트 컴파일러로 컴파일하면 64비트 사이즈로 인식된다.

 

- 다른 64비트 윈도우 플랫폼과는 달리 AMD64에서는 자료 구조의 "natural" alignment의 실패 (즉, ULONG은 32bit boundary, ULONGLONG은 64비트 boundary로 align하지 못한다.) 가 침형적 에러를 발생시키지 않는다. -> 따라서 이점을 유의하지 않으면, AMD64에서 죽지 않던 것이 다른 곳에서는 블루 스크린을 띄울 수 있다.

 

- 포인터의 정상 사용은 걱정할 필요 없다. 운영체제에 따라 32비트 또는 64비트로 자동으로 인식되기 때문이다.

 

2. 파라미터

 

- 파라미터로 패싱되는 포인터의 자동 인식 ; 32비트 어플리케이션에 의한 포인터 파라미터가 자동으로 64비트로 변경되어 처리된다.

예를 들어 32비트 또는 64비트 어플리케이션에 의한 함수 호출에 관계없이 드라이버에서는 Irp-> UserData 포인터가 64비트로 자동으로 인식될 것이다.

 

3. 버퍼 내 포인터

 

- IOCTL Buffers에 임베디드된 포인터 ;

몇 몇 드라이버들은 주의해야 될 사항이다. IOCTL 버퍼에 포인터가 포함되어 있는 경우, 드라이버는 호출자가 32비트 또는 64비트 어플리케이션 중 어떤 것인지 주의해야만 한다. IOCTL_IN_BUFFER과 OUT_ BUFFER를 가리키는 포인터는 윈도우에 의해 자동으로 변환된다. 그러나, 이 버퍼의 내용에 포인터를 포함하고 있다면, 포팅시 32비트 포인터를 64비트 사이즈로 확장해 주어야 한다.

 

4. 기타

- In-Line Assembler, MMX, 3DNow, and X87 FP 명령어는 지원하지 않는다!!! embedded assembler, MMX, 3DNow, X87 floating pointer는 쓰지 말아라. 다행히 SSE, SSE/2 명령어는 지원한다.

 

간단히 쉽게 정리하자면, 임베디드 어셈블러를 사용하지 말고, IOCTL 버퍼 내에 포인터를 사용하지 않고,(사용하면 64비트로 확장) ULONG을 ULONG_PTR 타입으로 변경(포인터와 관련있다면)하고 컴파일한다.

 

IV. 64비트 포팅 적용

1. 적절한 데이터 타입

 

- 자료 구조에서 포인터를 명시적으로 ULONG타입으로 캐스트하는 경우이다. 컴파일러에 따라 포인터의 사이즈가 달라지기 때문이다. 따라서 이 부분은 ULONG_PTR 타입으로 바꿔주어야 한다.

 

ULONG currentMdl ;

ULONG MdlPointers[10] ;

devExt->MdlPointers[currentMdl++] =

(ULONG) Irp->MdlAddress ;

 

위의 코드는 64비트에서는 작동하지 않는다. 다음과 같이 수정하면 32비트, 64비트가 컴파일러에 따라 모두 적절히 작동된다.

ULONG_PTR MdlPointers[10] ;

devExt->MdlPointers[currentMdl++] =

(ULONG_PTR) Irp->MdlAddress ;

또는

PMDL MdlPointers[10] ;

devExt->MdlPointers[currentMdl++] = (PMDL)

Irp->MdlAddress ;

- Align pointer precision 자료 구조

 

강제로 alignment를 변경하는 pragma는 편할 수 도 있지만 꼭 필요한 경우가 아니면 쓰지 말자.

// #pragma pack(4) // 4 바이트 단위로 정렬

ULONG ulTest ;

ULONG_PTR pTest ;

// #pragma pack() // 해제 (디폴트 단위로..., natural boundary)

구조체에서 필드들의 사이즈는 alignment 단위로 맞춰지기 때문에 자신도 모르게 dummy 메모리가 할당이 된다.

위의 구조체의 크기는 64비트 컴파일러에서 8 byte단위로 alignment가 맞춰지기 때문에 16바이트가 할당이 된다. (32비트 컴파일러에서도 ULONG_PTR대신 64비트 자료형을 써주면 최대 크기인 8바이트 단위로 alignment가 설정된다.) ulTest뒤에 4바이트의 메모리가 더미로 존재하고 8바이트의 pTest가 할당이 되기 때문이다. 강제로 alignment를 4바이트로 설정하는 pragma pack(4) 를 적용하면 12바이트가 되는 것이다.

따라서 명시적으로 dummy 필드를 써 주는 것이 나중을 위해서 좋다. 다음과 같이 구조체 필드를 만들어 확실히 16바이트라는 것을 인지한다.

ULONG ulTest ;

ULONG dummy0; // dummy 추가

ULONG_PTR pTest ;

 

2. The Pointer in DataBuffer

 

전에 설명했던 것과 같이, 64비트 윈도우는 파라미터로 패싱된 포인터를 32비트에서 64비트로 자동 변환한다. 그러나, 전달된 드라이버의 자료구조의 데이터 버퍼 안에 포인터가 존재한다면 어떻게 다뤄야 하는가?

그러한 예는 다음과 같다.

typedef struct _myBuffer {

ULONG Count ;

PUCHAR SecondaryBuffer ;

ULONG SecondaryBufferSize ;

UCHAR Buffer[BUF_SIZE] ;

} MY_BUFFER, *PMY_BUFFER ;

 

32비트 어플리케이션에서 다음과 같은 코드로 작성하였다.

MY_BUFFER buf ;

buf.Count = CharPassed ;

StringCchCopy(buf.Buffer, BUF_SIZE, DataSource, buf.Count) ;

buf.SecondaryBufferSize = OtherBufferSize ;

buf.SecondaryBuffer = PointerToOtherBuffer ;

worked = DeviceIOControl(hDev,

IOCTL_MYDRV_SEND_BUFFER, &buf,

sizeof(MY_BUFFER), NULL, 0,

&byteReturned, NULL) ;

64비트로 포팅하려면 어떻게 해야될 것이다.

 

+ 방법 1

&buf는 자동으로 64비트로 자동으로 인식될 것이다. (thunk buf 포인터 자동 변환)

그러나, 윈도우는 호출자의 IN_BUFFER안에 포인터가 포함되어 있는 줄 모른다.

(SecondaryBuffer) 32비트 또는 64비트 어플리케이션에서 같은 IOCTL을 보낼 때, 버퍼의 적절한 길이와 포인터의 사이즈가 변한다.

어떻게 해결할까? -> 포인터를 없앤다.

typedef struct _myNEWBuffer {

ULONG Count ;

UCHAR Buffer[BUF_SIZE] ;

} MY_NEW_BUFFER, *PMY_NEW_BUFFER ;

secondary buffer는 OUT_BUFFER 파라미터를 이용하여 다음과 같이 할 수 있다.

MY_NEW_BUFFER buf ;

buf.Count = CharsPassed ;

StringCchCopy(buf.Buffer, BUF_SIZE, DataSource, buf.Count) ;

worked = DeviceIOControl(hDev,

IOCTL_MYDRV_SEND_BUFFER, &buf,

sizeof(MY_BUFFER), PointerToOtherBuffer,

OtherBufferSize, &byteReturned, NULL) ;

소스코드와 자료구조가 깨끗해졌다.

위의 방식은 32비트 어플리케이션이 32비트, 64비트 OS 모두 사용할 수 있다. (단, 어떠한 경우라도 드라이버는 32, 64비트용으로 각 각 만들어야 한다.)

 

물론 이러한 방법은 이런 IOCTL 코드를 쓰는 모든 어플리케이션에서 IOCTL 포맷을 새롭게 바꾸고, 모두 다시 컴파일해야 하는 단점이 있다.

 

+ 방법 2

OUT_BUFFER를 사용하지 않고, IN_BUFFER 내에 포인터를 포함시킬 수 있는 방법이다.

 

A 구조체

// Structure version used by all app (and for 64-bit app in Driver)

typedef struct _myBuffer {

ULONG Count ;

PUCHAR SecondaryBuffer ;

ULONG SecondaryBufferSize ;

UCHAR Buffer[BUF_SIZE] ;

} MY_BUFFER, *PMY_BUFFER ;

B 구조체

// Structure version used exclusively by driver when

// getting data from 32-bit apps.

typedef struct _myBuffer_32 {

ULONG Count ;

PUCHAR POINTER_32 SecondaryBuffer ;

ULONG SecondaryBufferSize ;

UCHAR Buffer[BUF_SIZE] ;

} MY_BUFFER_32, *PMY_BUFFER_32 ;

A 구조체는 원본(포팅 전)과 같다.

SecondaryBuffer 포인터가 구조체 내부에 숨겨진 형태로 32비트 어플리케이션에서는 OS에 관계없이 32비트, 64비트 어플리케이션은 64비트 포인터로 인식이 된다. 따라서 32비트용 어플리케이션과 32비트용 드라이버로 컴파일 하고, 64비트용 어플리케이션과 64비트용 드라이버로 컴파일하여 따로 사용할 수 있다.

따라서 운영체제의 커널 비트에 따른 어플리케이션이 별도로 존재해야 하는 단점이 있다.

 

B 구조체는 64비트 윈도우를 위해 만들어진 구조체로써 드라이버가 사용할 것이다. 그러나 32비트 어플리케이션과도 호환된다. 드라이버에서는 호출자가 32비트 어플리케이션 인지 64비트인지를 IoIs32bitProcess()를 통해 확인하여 결정한다.

case IOCTL_MYDRV_SEND_BUFFER:

#ifdef _WIN64

// if it's a 32-bit caller, we validate the size of the 32-bit structure

if (IoIs32bitProcess(Irp))

{

if (ios->Parameters.DeviceIoControl.InputBufferLength >= sizeof(MY_BUFFER_32) )

{..... }

} else

#endif

{

if (ios->Parameters.DeviceIOControl.InputBufferLength

>= sizeof(MY_BUFFER) )

{....}

}

이러한 방식은 32비트 어플리케이션을 위한 구조체를 따로 둠으로써, 64비트 드라이버에서 32, 64비트 어플리케이션을 모두 사용 가능하도록 한다. 그러나 구조체를 각각 관리해야하고, 드라이버에서 32비트를 포인터를 위한 처리를 따로 해 주어야 하는 단점이 있다.

 

3. Inline Assembler, etc...

 

여기서 별도로 다루지는 않겠다. 보다 자세한 정보를 원한다면, Hector가 쓴 OSR Online back in June. 기사를 보면 알 수 있을 것이다.

https://www.osronline.com/article.cfm?id=244

AMD64 컴파일러는 inline assembler code를 지원하지 않는다!!!

또한 윈도우 64비트의 커널모드에서는 MMX, 3DNow, X87 floating point를 지원하지 않는다. !!!

 

 

4. New 3 Class Data Types

 

64비트 프로그래밍을 위해 다음의 세가지 클래스 데이터 타입이 추가되었다.

 

+ Fixed Precision ; 데이터 타입의 사이즈가 고정 길이

DWORD32, DWORD64, INT32, INT64, LONG32, LONG64, UINT32, UINT64, ULONG32, ULONG64

 

+ Pointer Precision ; 포인터 연산을 할 때 사용하면 좋다. 32비트 또는 64비트 윈도우즈에 따라 사이즈가 바뀐다.

DWORD_PTR, HALF_PTR (포인터의 반 크기), INT_PTR, LONG_PTR, SIZE_T, SSIZE_T, UHALF_PTR, UINT_PTR, UINT_PTR, ULONG_PTR

 

+ Specific Pointer-Precision Types

포인터의 사이즈를 명시적으로 선언한다.

POINTER_32 ; 32비트 포인터. 32비트 윈도우에서는 native pointer이지만 64비트에서는 64비트 포인터를 반으로 자른 것이다.

POINTER_64 ; 64비트 포인터. 64비트 윈도우에서는 native pointer이다. 32비트에서는 32비트 포인터를 64비트로 확장시킨 것 임.

 

 

5. Helper Functions (basetsd.h)

 

+ Predefined Macro

_WIN64 ; 64비트 플랫폼

_WIN32 ; 32비트 플랫폼

_WIN16 ; 16비트 플랫폼

 

_M_IA64 ; 64비트 인텔 플랫폼

_M_IX86 ; 32비트 인텔 플랫폼

 

가능하면 위에 있는 _WIN64, _WIN32를 사용하고, 아키텍쳐와 관련된 것을 제외하고는 _M_IA64, _M_IX86 매크로를 쓰지 마라.

 

아래의 API들은 64비트 프로그래밍시 데이터 타입 형 변환에 도움을 준다. 그러나, 꼭 필요한 경우에만 사용해야 한다. 그렇지 않으면 컴파일시 경고나, 에러가 발생하지 않기 때문에 디버깅이 어려워질 수도 있다.

 

unsigned long HandleToUlong( const void *h )

long HandleToLong( const void *h )

void *LongToHandle( const long h )

unsigned long PtrToUlong( const void *p )

unsigned int PtrToUint( const void *p )

unsigned short PtrToUshort( const void *p )

long PtrToLong( const void *p )

int PtrToInt( const void *p )

short PtrToShort( const void *p )

void * IntToPtr( const int i )

void * UIntToPtr( const unsigned int ui )

void * LongToPtr( const long l )

void * ULongToPtr( const unsigned long ul )

 

 

6. 64비트 포인터 사용 규칙

 

- int, long, ULONG, DWORD로 포인터를 타입 캐스팅하면 안 된다.

이 경우 ULONG_PTR 타입을 사용한다.

HANDLE은 void*로 정의되어 있다. 따라서 ULONG으로 캐스팅하면 안 될 것이다.

 

- 필요한 경우 PtrToLong, PtrToUlong을 사용하여 포인터를 truncate시켜라. (helper function)

포인터를 32비트 값으로 자를 때 사용한다.(상위 32비트가 날라간다.) 편하지만 경고가 발생하지 않기 때문에, 디버깅하기가 어려우므로 조심히 사용해야한다.

 

- OUT 파라미터로 사용 시 주의해야 한다.

void func(OUT PULONG *PointerToUlong) ; 이런 함수가 있다고 가정하고...

ULONG ul ;

PULONG lp ;

func((PULONG *)&ul) ;

lp = (PULONG) ul ;

위와 같이 사용하는 경우, func의 두 번째 파라미터는 이중 포인터 타입이다. 64비트인 경우, *PointerToUlong (PULONG)도 64비트이므로 ul (ULONG)과는 사이즈가 다르다.

 

위의 코드를 아래와 같이 수정해야 한다.

PULONG lp ;

func(&lp) ;

- 다중 인터페이스를 조심해야 한다.

(polymorphic interfaces)

다양한 파라미터를 받기 위해 주소 값 또는 정수 등의 값을 DWORD 파라미터를 사용하였다면, UINT_PTR이나 PVOID로 바꿔라.

 

- 새로운 윈도우 클래스 함수를 사용하라.

GetClassLongPtr, GetWindowLongPtr,

SetClassLongPtr, SetWindowLongPtr

GWL_* 대신 GWLP_*를 사용하라.

(winuser.h)

SetWindowLong(hWnd, GWL_WNDPROC,

(LONG)MyWndProc) ;

위의 코드를 다음과 같이 수정하라.

SetWindowLongPtr(hWnd, GWLP_WNDPROC,

(LONG_PTR)MyWndProc) ;

- 모든 윈도우와 클래스 데이터에 접근시 FIELD_OFFSET을 사용하라.

두 번째 포인터가 항상 옵셋 4라고 생각하지 마라. 32비트에서만 유효하다.

 

- LPARAM, WPARAM, LRESULT 타입은 플랫폼에 따라 사이즈가 변하므로 주의해야 한다.

 

7. 기타 정보

 

- 64비트 어플리케이션은 유저 모드 주소 영역이 8테라 바이트이다. 그러나, 다음과 같은 조건의 프로그램은 유저 모드 주소를 2기가 이하에서만 작동하게 할 수 있다.

> 2기가 주소 영역이면 충분하다.

> 많은 pointer truncation 경고가 많다.

> 포인터와 정수형이 마구 섞여 쓰고 있다.

> 32비트 데이터 타입을 쓰는 polymorphism이 코드에 있다.

이러한 경우 아래와 같은 링커 옵션을 준다.

/LARGEADDRESSAWARE:NO

조심할 점은 DLL을 이 옵션으로 만들었다면, 이 DLL을 사용하는 모든 프로그램도 같은 옵션으로 만들어져야만 된다. (이 DLL이 주소를 32비트로 자르기 때문이다.)

 

- Process interoperability

 

64비트 윈도우에서 32비트 어플리케이션을 에뮬레이션 모드에서 돌릴 수 있다. 그러나, 64비트 프로세스는 32비트 DLL을 로드할 수 없다. 그 반대도 마찬가지다.

64비트와 32비트 프로세스 사이의 RPC 통신이 COM 서버를 이용하는 방법이 있긴 하다.

 

- Driver

드라이버를 64비트로 포팅할 경우 다음의 사항을 고려해야 한다.

> 4G 이상을 지원하고 싶다면, Mm64BitPh ysicalAddresses를 사용하여 64비트 주소가 필요한 경우 사용한다. 또한 DEVICE_DESCRIPTI ON 구조체의 Dma64BitAddresses 멤버를 설정하여 64비트 어드레싱을 할 수 있다.

> IO status block은 ULONG_PTR이다.

> IRP stack location은 ULONG_PTR이다.

 

 

- 64비트 프로그래밍 경고 및 에러

warning C4311: 'type cast' : pointer truncation from 'unsigned char *' to 'unsigned long' -> 이러한 에러는 다음과 같은 예에서 발생한다.

buffer = (PUCHAR)srbControl;

(ULONG)buffer += srbControl->HeaderLength;

위의 코드를 아래처럼 수정하라.

buffer = (PUCHAR)srbControl;

(ULONG_PTR)buffer += srbControl->HeaderLength;

- 64-bit Compiler Switches and Warnings

 

컴파일러가 LLP64 데이터 모델인 경우

LLP64로 포팅하는데 -Wp64 -W3 경고 옵션이 도움을 줄 것이다.

C4305: Truncation warning. For example, "return": truncation from "unsigned int64" to "long."

C4311: Truncation warning. For example, "type cast": pointer truncation from "int*_ptr64" to "int"

C4312: Conversion to bigger-size warning. For example, "type cast": conversion from "int" to "int*_ptr64" of greater size.

C4318: Passing zero length. For example, passing constant zero as the length to the memset function.

C4319: Not operator. For example, "~": zero extending "unsigned long" to "unsigned _int64" of greater size.

C4313: Calling the printf family of functions with conflicting conversion type specifiers and arguments. For example, "printf": "%p" in format string conflicts with argument 2 of type "_int64." Another example is the call printf("%x", pointer_value); this causes a truncation of the upper 32 bits. The correct call is printf("%p", pointer_value).

C4244: Same as the existing warning C4242. For example, "return": conversion from "_int64" to "unsigned int," possible loss of data.

 

 V. 결론

이로써 32비트 어플리케이션 및 드라이버를 64비트로 포팅하는데 있어서 주의해야 될 사항들과 어떻게 포팅해야 할 지에 대한 소스코드 예를 몇 가지 살펴보았다.

크게는 별로 변한 게 없는 듯(포인터 사이즈 하나가 변했다고) 하지만, 실제 소스 레벨로 가서 보면 발생할 수 있는 제 2, 제 3의 문제가 연쇄적으로 발생할 수 있음을 알 수 있다. 다른 프로그램과의 통신, 구조체의 사이즈 변형, 포인터 연산, Alignment 등 생각할 것도 많고, 소스코드를 32비트, 64비트 머신 모두에게 돌아갈 수 있도록 유지할 수 있게 고뇌해야할 것이다.



참 고

 

 

1. Rock On With 64-bit Windows -- Porting Windows Drivers To AMD64 ;

http://www.osronline.com/article.cfm?id=265

 

2. Platform SDK: 64-bit Windows Programming

http://www.msdn.microsoft.com/library/default.asp?url=/library/en-us/win64/win64/changing_an_existing_interface.asp

 

 

+ Recent posts