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

2009/02/15 12:26

이전에 비트 연산을 통해서 정수의 내부 비트를 엿보는 코드를 짰었습니다. 그러고부터 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 :: 정수의 내부 비트를 살짝 엿보자. :: http://hisjournal.net/blog/118
크리에이티브 커먼즈 라이센스
Creative Commons License

6l4ck3y3 #1. 내 머리 속의 노트/C 언어 마스터 Go! gO! , , , , ,

Trackback Address:http://hisjournal.net/blog/trackback/160
  1. 포인터를 unsigned char * 로 잡으시고 거꾸로 읽으시면 됩니다. (x86의 경우, little endian이므로 )

    #include <stdio.h>
    #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
    의 결과가 나옵니다.

  2. Blog Icon
    아리새의펜촉

    오! 그렇군요.

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

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

  4. Blog Icon
    아리새의펜촉

    정말 고맙습니다. 포인터는 알면 알수록 다양하게 활용할 수 있네요.

[로그인][오픈아이디란?]