C :: 실수(Float)를 엿보려면 컴퓨터를 속여라.

이전에 비트 연산을 통해서 정수의 내부 비트를 엿보는 코드를 짰었습니다. 그러고부터 5주가 지난 지금에서야 실수의 내부 비트를 볼 수 있는 코드를 완성했네요. 딱 코드 한 줄만 추가하면 되는 문제였지만, 포인터에 대한 이해가 바탕이 되어야 했기 때문에 5주가 걸렸습니다.

우선 코드부터 보겠습니다.

#include <stdio.h>

void PrintBit(unsigned uNumber, int size);

int main(void)
{
    float fNumber = 0;
    unsigned uTemp = 0;

    puts("실수를 입력하시오.");
    scanf("%f", &fNumber);

    // fNumber의 주소를 unsigned형 포인터로 변환하여 fNumber를 컴파일러가
    // unsigned 자료형으로 착각하게 만든다.
    uTemp = *(unsigned *)&fNumber;

    PrintBit(uTemp, sizeof(float));

    return 0;
}

// 넘겨받은 unsigned 형의 정수를 비트 단위로 출력한다.
// 넘겨받은 size 값으로 float, double, long double을 구분한다.
void PrintBit(unsigned uNumber, int size)
{
    int i = 0;
    int j = 0;

    puts("\n이거 비트가 어떻게 되냐면... \n");
    for (i = size; i > 0 ; i--)
    {
        for (j = i*8-1; j >= (i-1)*8; j--)
        {
            printf("%d", (uNumber & (1 << j)) ? 1 : 0);
        }
        printf(" ");
    }
    puts("\n");
}


지난 글과 코드가 많이 달라보이는 듯 하지만 실제로는 출력부를 함수로 끄집어내고, 핵심 코드 한 줄만 추가했을 뿐입니다. 물론, 변수 하나 더 썼지요. ^^;

여기서 핵심 코드는

uTemp = *(unsigned *)&fNumber;

이것입니다. 주석에도 쓰여 있지만, fNumber의 자료형을 float이 아닌 unsigned처럼 보이게끔 컴퓨터를 속이는 코드입니다. 이 코드를 이해하려면 우선 포인터에 대한 이해가 필요합니다.

포인터에는 가리키는 자료가 시작되는 메모리 주소와 자료형에 대한 정보가 들어 있습니다. 포인터를 선언할 때 int, char, … 등을 이용하는 게 그 때문이죠. 자료형에 대한 정보가 없으면 자료가 메모리의 어디서부터 어디까지 저장되어 있는지 모르게 됩니다. 즉,

이 자료를 참조하려면 여기서부터 몇 바이트까지를 읽으면 된다.

이런 의미인 겁니다.

다시 위 코드에 대해 설명하자면, 우선 연산 순서가 오른쪽에서 왼쪽이란 것을 알아야 합니다. fNumber의 메모리 주소를 unsigned 포인터형으로 바꾸는 작업을 먼저 합니다. 원래는 float으로 읽어야 하는데, 이런 속임수를 통해서 unsigned처럼 읽게 됩니다. 이렇게 하는 것은 실수와 정수의 내부구조가 다르기 때문에 때문이죠.

가령, 3.14를 float으로 읽으면 3.14라는 실수이지만, unsigned처럼 읽으면 꽤 큰 정수가 나옵니다. 실수의 비트를 그대로 정수로 해석하는 것입니다. 여기서 unsigned로 읽는다는 것과 구분해야 합니다. unsigned로 읽으면 3이 되기 때문이죠. 컴파일러가 스스로 바꿔 버리거든요.

uTemp = (unsigned)fNumber;

이렇게 해서는 안 된다는 얘기이죠.

이런 작업을 한 후에는 * 연산자로 컴퓨터에게 속인 방식대로 자료를 참조하라고 지시합니다.

포인터에 대해서만 이해하고 있으면 되는 간단한 코드이지만 그렇지 못하면 이게 무슨 말이야 하는 코드입니다. 아래는 3.14의 비트 구조를 구한 결과입니다.

사용자 삽입 이미지
거의 만족스러운 결과를 얻었습니다. 하지만 여기서 문제가 하나 더 생깁니다. 실수에는 float만 있는 게 아니기 때문이죠. double, long double의 비트 구조도 알고 싶은데 자료형의 크기가 워낙 커서 말이죠. unsigned로는 4byte만 볼 수 있습니다. 에효… 첩첩산중이군요.

그리고 uTemp가 어떻게 비트 구조로 보이는지 모르시겠다면, 아래 참고 문헌에 링크 걸어 놓은 글을 읽어주세요.

참고 문헌

C :: 정수의 내부 비트를 살짝 엿보자. :: /?p=744

Similar Posts

  • .htaccess, UTF-8을 지원하지 않는 서버에서

    이 블로그는 태터툴즈로 운영하는 블로그다. 태터툴즈는 http://www.tattertools.com에서 다운로드 받을 수 있는 설치형 블로그다. 네이버, 이글루스 같은 서비스형 블로그와 달리 자신의 계정에 설치한다. 그래서 백업은 물론이고 DB 관리도 자신이 직접 할 수 있다. 용량도 적어서 어떠한 계정에라도 트래픽만 허용된다면 부담없이 운영할 수 있다. 이런 설치형 블로그에는 워드프레스와 태터툴즈가 대중적이다. 워드프레스는 태터툴즈에 비해 크기가 작고 기능도 더…

  • C :: [열혈강의 C 프로그래밍] p.115 char_add.c

    tothefelix 님의 블로그에서 아래의 코드를 가져왔습니다. 언뜻 보면 정상적인 코드인 것 같으나, 유심히 보면 자료형이 잘못된 것을 알 수 있습니다. 1바이트의 char 형 변수에 4바이트의 int 형 값을 넣는 오류입니다. 명백히 코드 상의 오류지요. 정말 열혈강의에 저런 코드가 예문으로 나와 있다면 무척 실망입니다. 역시 국내 C 레퍼런스로는 김상형 님의 “혼자 연구하는 C/C++“가 최고의 레퍼런스가 아닌가…

  • C :: multiple definition 에러

    카드 게임을 구현하는 코드를 짰는데, 컴파일해보니 이런 메시지가 나왔습니다. 0 errors, 0 warnings 아주 반가운 메시지죠. 하지만 실행이 되질 않더군요. 그래서 메시지를 자세히 보니 multiple definition of ‘strSuit’ multiple definition of ‘strFace’ 라고 나와 있는 겁니다. 해석하면 strSuit, strFace 변수의 정의가 중복되었다는 건데, 어디서 중복되었는지를 찾질 못하고 이틀을 헤매었습니다. 원인도 모른 채 이렇게 수정해보고 저렇게…

  • Feed43 활용. 게시판을 RSS로 구독하기

    이전 글에서 소개한 바와 같이 Feed43의 유용함은 정말 뛰어납니다. Feed43과 같은 서비스를 적극적으로 활용하면 RSS로 많은 정보를 간편하게 취할 수 있고 분류도 할 수 있습니다. 이번에는 Feed43으로 게시판을 RSS로 구독하는 방법을 알아 보겠습니다. 간단한 코드 한 줄만 이용하면 되므로 어려운 내용은 아닙니다. 위 그림같은 게시판이 있습니다. 게시판의 구성은 번호, 제목, 이름, 날짜, 조회수, 첨부파일 링크…

  • 우분투에서 자동으로 파티션을 불러오자.

    아래는 “Ubuntu 8.10:Intrepid Ibex” 환경에서 작성한 글입니다. 다른 버전에서는 다르게 적용될 수 있습니다. 저는 여분의 HDD를 자료받기용으로 파티션을 주어 사용합니다. 윈도를 사용할 때 생긴 버릇인데, 바이러스에 의한 피해를 최소화 하기 위해서였습니다. 이 버릇은 리눅스(우분투)를 사용하면서도 계속 되었지요. 문제는 리눅스(우분투)에서는 FAT32 형식의 파티션은 자동으로 불러오도록(mount) 되어 있지 않다는 것입니다. 내려받을 폴더를 Download 파티션으로 지정해놓은 상태에서 매번…

  • Visual Basic :: Unload 문을 이용한 메모리 관리

    Visual Basic으로 프로그램을 짜면 Form부터 구성합니다. 하나의 프로젝트 안에 여러 개의 Form을 구성할 수도 있죠. 여기서 2개 이상의 Form으로 구성된 프로그램을 구동시키면 모든 Form이 Load 된다는 사실이 중요합니다. “Load”라는 의미는 메모리에 저장된다는 뜻입니다. 보통 ‘종료’, ‘X’ 같은 버튼을 누르거나 코드를 다 수행하면 프로그램이 종료되지만, 이 때 활성화 되어 있던 Form만 메모리에서 삭제됩니다. 즉, 비활성화 되어…

0 Comments

  1. unsigned char* 로 잡는 것은 1바이트씩 가지고 처리하겠다는 의미입니다. 1바이트씩 처리하면 모든 경우의 수에 대해 동일한 처리를 수행할 수 있어 편리합니다. 예를 들어 4바이트짜리 포인터를 가지고 처리하다보면 1,2,6 바이트 같은 것을 처리할 때 조금 불편할 수 있겠지만, 1바이트씩 처리하면 결국 처리하는 바이트 수의 차이만 있지 1바이트 내에서는 항상 같은 처리를 할테니까요. 도움이 되셧으면 좋겠네요~

  2. 오! 그렇군요.

    그런데 unsigned char*로 잡는 이유가 무엇인가요? 혹시 a가 스택에 연속적으로 저장되니까 1byte씩 끊어서 스택을 읽기 위함인가요?

  3. 포인터를 unsigned char * 로 잡으시고 거꾸로 읽으시면 됩니다. (x86의 경우, little endian이므로 )

    #include
    #define TYPE double

    int main(void)
    {
    TYPE a = 3.14;
    unsigned char *ptr;
    int i,j;

    ptr = (unsigned char *) &a;
    for( i=1;i<= sizeof(TYPE);i++) { for(j = 7;j>=0 ;j–)
    printf(“%d”, (*(ptr-i+sizeof(TYPE)) >> j ) & 0x01);
    printf(” “);
    }
    printf(“\n”);
    return 0;
    }

    이를 실행하면,

    01000000 00001001 00011110 10111000 01010001 11101011 10000101 00011111
    의 결과가 나옵니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다