독서/[ 뇌를 자극하는 윈도우즈 시스템 프로그래밍 ]

[ 뇌를 자극하는 윈도우즈 시스템 프로그래밍 ] Chapter 03. 64비트 기반 프로그래밍

coding-l7 2023. 9. 10. 02:08

01. WIN32 vs WIN64

64비트와 32비트

먼저, 64비트 컴퓨터와 32비트 컴퓨터를 구분하는 기준은 두 가지가 있다.

1. I/O 버스를 통해 CPU가 한 번에 전송 및 수신할 수 있는 데이터의 크기에 따라서 32비트 시스템과 64비트 시스템이 나누어진다.

2. CPU가 외부로부터 들어오는 데이터를 한 번에 처리할 수 있는 데이터 크기를 기준으로 32비트와 64비트 시스템을 나눈다.

, 한 번에 송/수신할 수 있는 데이터 크기와 한 번에 처리할 수 있는 데이터 크기를 기준으로 32비트 컴퓨터와 64비트 컴퓨터를 구분한다.

 

프로그래머 입장에서의 64비트 컴퓨터

프로그래머 입장에서는 표현할 수 있는 주소값의 범위가 넓을수록 좋다. 메모리 공간이 충분하다면 더 넓은 메모리 공간을 활용할 수 있기 때문이다.

예를 들어 다음과 같이 1GB 크기의 메인 메모리 공간이 있다고 가정하고, 주소값을 표현하는 데에 4비트가 사용된다면, 주소의 개수는 2^416개이고, 메모리 공간 하나당 크기는 1바이트(8비트)이므로, 사용 가능한 최대 메모리 크기는 16바이트가 된다.

32비트 컴퓨터의 경우 CPU가 한 번에 처리할 수 있는 데이터 크기가 32비트이기 때문에 주소값을 표현하는 데에 32비트를 사용한다. 따라서, 32비트 컴퓨터가 사용 가능한 최대 메모리 크기는 2^32 * 8 = 4GB가 된다. 만약, 주소의 길이가 32비트보다 크다면, 주소 값을 최소 두 번에 걸쳐서 연산해야 하고, 이는 성능에 영향을 미칠 수 있다.


02. 프로그램 구현 관점에서의 WIN32 vs WIN64

LLP64 vs LP64

기본적으로 int, long 그리고 포인터 모두 4바이트로 표현된다고 알고 있지만, 이는 32비트 환경에서 Windows가 기본 자료형과 포인터를 표현하는 방식이며, 64비트 컴퓨터에서는 다음과 같이 표현한다.

Windows에서는 LLP64라는 데이터 표현 모델을 따르며, 이는 intlong4바이트로 유지되며, 포인터만 8바이트로 변경된다. 따라서 32비트 시스템과의 호환성을 중시한 모델이다. 추가적으로, LP64 모델은 UNIX에서 사용하고 있다.

 

 

64비트와 32비트 공존의 문제점

64비트와 32비트가 동시에 존재할 경우 다음과 같은 코드에서 문제가 발생할 수 있다.

 

#include <stdio.h>
int main(void)
{
	int arr[10] = {0,};
	int arrVal = (int)arr; //데이터 손실 가능성
	printf(“pointer : %d \n”, arrVal);
	return 0;
}

 

위의 코드는 배열이 선언된 주소값을 출력하기 위해 int형 변수에 그 값을 저장하고 있다. 이러한 방식은 64비트 시스템에서 문제가 될 수 있다. 64비트 시스템에서는 주소값인 포인터는 8바이트 크기를 가지기 때문에 4바이트 크기를 가지는 int형 변환하는 과정에서 데이터 손실이 발생할 수 있기 때문이다.

 

Windows 스타일 자료형

앞장에서도 언급했듯이, Windows에서는 Windows 스타일의 자료형typedef을 이용하여 자료형을 정의했다. 그 중에는 32비트와 64비트 시스템을 위해 정의한 자료형 또한 존재한다.

ex) INT32, INT64 ..

 

WIN32 기반에서는 INT와 같은 자료형이 많이 사용되었지만, WIN64로 넘어가면서 3264로 끝나는 새로운 자료형이 추가된 것이다. 이는 WIN32WIN64에서 시스템에 상관없이 동일한 의미를 지니는 자료형을 표현하기 위한 것이다. ~32 형태의 자료형은 시스템에 상관없이 32비트로, ~64 형태의 자료형은 시스템에 상관없이 64비트로 표현된다. 이외에도 포인터 자료형이 존재한다.

 

Polymorphic 자료형

MS에서는 WIN64 기반으로 넘어가면서 Polymorphic 자료형을 정의하고 있다. 이때 Polymorphic은 다형성을 의미하며, MS에서 정의하고 있는 Polymorphic 자료형의 정의 형태는 다음과 같다.

 

#if defined(_WIN64)

    typedef __int64 LONG_PTR;
    typedef unsigned __int64 ULONG_PTR;

    typedef __int64 INT_PTR;
    typedef unsigned __int64 UINT_PTR;
#else
    typedef long LONG_PTR;
    typedef unsigned long ULONG_PTR;

    typedef int INT_PTR;
    typedef unsigned int UINT_PTR;
#endif

 

자료형의 이름에 PTR이 붙었지만, 포인터는 아니다. 이는 포인터값 기반의 산술연산을 위해 정의된 자료형이기에 PTR이라는 이름을 붙인 것이다. 32비트 시스템과 64비트 시스템의 포인터 크기가 다르기 때문에 발생할 수 있는 문제를 해결하기 위해 등장한 자료형이라는 뜻이다. 이는 다음에서 이해할 수 있다.

 

//PolymorphicType1.cpp

#include <stdio.h>
#include <tchar.h>
#include <windows.h>

UINT CalDistance(UINT a, UINT b)
{
	return a - b;
}

int _tmain(void)
{
	INT val1 = 10;
	INT val2 = 20;

	_tprintf(_T("Position %u, %u \n"), (UINT)&val1, (UINT)&val2);
	_tprintf(
		_T("distance : %u \n"),
		CalDistance((UINT)&val1, (UINT)&val2)
	);

	return 0;
}

 

위에서 CalDistance 함수UINT라는 4바이트 크기의 변수를 인자로 받고 있기 때문에 WIN32 기반 함수이다. 따라서 이를 WIN64 기반에서 사용하기 위해 다음과 같이 수정해야만 한다.

 

UINT64 CalDistance(UINT64 a, UINT64 b)
{
	return a - b;
}

 

이를 WIN32 기반에서는 위의 함수를, WIN64 기반에서는 아래의 함수를 자동으로 컴파일 및 실행하기 위해 다음과 같이 수정할 수 있다.

 

#if defined(_WIN64)
	UINT64 CalDistance(UINT64 a, UINT64 b)
#else 
	UINT CalDistance(UINT a, UINT b)
#endif
	{
		return a - b;
	}

 

목적은 달성했지만, 가독성이 떨어지고 지저분하다. 이때, Polymorphic 자료형을 다음과 같이 사용하여 간결한 코드를 작성할 수 있다.

 

//PolymorphicType2.cpp

#include <stdio.h>
#include <tchar.h>
#include <windows.h>

UINT_PTR CalDistance(UINT_PTR a, UINT_PTR b)
{
	return a - b;
}

int _tmain(void)
{
	INT val1 = 10;
	INT val2 = 20;

	_tprintf(
		_T("distance : %u \n"),
		CalDistance((UINT_PTR)&val1, (UINT_PTR)&val2)
	);

	return 0;
}

 

위에서 언급한 Polymorphic 자료형의 정의에 따라 64비트 환경에서는 64비트로, 32비트 환경에서는 32비트로 선언된다.


03. 오류의 확인

GetLastError 함수와 에러코드

Windows 시스템 함수를 호출하는 과정에서 오류가 발생하면, 바로 다음에 GetLastError 함수 호출을 통해 오류의 원인을 확인할 수 있다. 이는 오류의 원인에 해당하는 에러코드를 반환하며, MSDN을 참조하면 에러코드의 종류와 의미를 확인할 수 있다다음의 예제를 통해 에러코드를 얻어 볼 것이다.

 

//GetLastError.cpp

#include <stdio.h>
#include <tchar.h>
#include <windows.h>

int _tmain(void)
{
	HANDLE hFile =
		CreateFile(
			_T("ABC.DAT"), GENERIC_READ, FILE_SHARE_READ,
			NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
			NULL);
	if (hFile == INVALID_HANDLE_VALUE)
	{
		_tprintf(_T("error code: %d \n"), GetLastError());
		return 0;
	}
	return 0;
}

위의 코드는 ABC.DAT 파일을 개방하는 함수, 함수의 기능을 자세하게 알 필요는 없다. 다만, 존재하지 않는 파일을 열도록 설정했기 때문에 함수는 호출에 실패할 것이며 CeateFile 함수는 호출이 실패할 경우 INVALID_HANDLE_VALUE를 반환한다.

이에 따라 오류 발생이 조건문에서 확인되고, GetLastError 함수에 의해 에러코드가 반환된다. 이때 반환된 에러코드는 2이며 MSDN을 통해 확인해 보면 다음과 같은 의미임을 알 수 있다.

The system cannot find the file specified.

, 시스템이 지정된 파일을 찾을 수 없다는 것이다.

 

또한, 다음의 예제를 통해 GetLastError 함수를 통한 에러코드 확인은 오류가 발생한 직후에 해줘야 한다는 것을 알 수 있다. 이는 Windows 시스템 함수가 호출될 때마다 GetLastError 함수가 반환하는 에러코드가 갱신되기 때문이다.

//ErrorStateChange.cpp

#include <stdio.h>
#include <tchar.h>
#include <windows.h>

int _tmain(void)
{
	HANDLE hFile =
		CreateFile(
			_T("ABC.DAT"), GENERIC_READ, FILE_SHARE_READ,
			NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
			NULL);

	_tprintf(_T("error code: %d \n"), GetLastError()); //오류 확인
		
	hFile =
		CreateFile(
			_T("ABC2.DAT"), GENERIC_WRITE, FILE_SHARE_READ,
			NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL,
			NULL);

	_tprintf(_T("error code: %d \n"), GetLastError()); //오류 확인
	return 0;
}

참고 자료:

윤성우. 뇌를 자극하는 윈도우즈 시스템 프로그래밍.한빛미디어, 2007.