본문 바로가기
MCU 일반

Byte Order - Little/Big Endian

by irmus 2011. 6. 23.
컴퓨터에서 숫자를 메모리에 저장할 때 사용되는 두가지 방식이 있습니다. Byte order에 따라 구분되는 방식이구요, 각각 Little Endian, Big Endian이라 불립니다. 이 두가지에 대한 설명은 인터넷에 많이 있습니다. 몇가지 링크를 첨부하는 것으로 설명을 대신합니다.

요즘 프로세서들은 Little Endian쪽으로 많이 통일되어 가고 있습니다... 라고 말씀드리고 싶지만, Intel(AMD), ARM 계열이 거의 대부분인 현실에서 Intel과 ARM이 little endian을 사용하고 있으니 통일되어 가는 것 처럼 보이는 것일 뿐입니다;;; 참, AVR 계열도 little endian입니다. 한편 대형 서버 쪽에는 big endian도 심심찮게 보입니다. 특히 모토롤라 계열 칩셋이 big endian의 대표주자입니다.

여튼 byte order 이야기를 꺼내놓고 자세한 설명은 생략한다링크만 덜렁 걸어둔건 성의가 없겠죠? 오늘 이야기 하고자 하는 것은 이기종 시스템간 데이터 전달에서의 byte order 이슈입니다.
참고로 하나의 컴퓨터 시스템 내에서 byte order는 그다지 신경쓰지 않아도 됩니다. 컴파일러가 다 알아서 해 주니까요. 그런거 몰라도 됩니다. 

이기종 시스템... 뭔가 한자와 영어가 섞인 표현이 나오면 조금쯤 까질해 지는데...
더 까칠해 지기 전에 예를 통해 살펴보도록 하죠~

Little endian을 사용하는 컴퓨터 A에서 시리얼 포트를 이용하여 big endian을 사용하는 컴퓨터 B로 메시지를 하나 보낸다고 했을 때 문제가 생깁니다. 무엇이 문제가 되는지 살펴보려면 좀 자세히 들여다 봐야 겠군요.
컴퓨터 A에서 32bit integer 타입의 변수 a_val에 숫자 25,000,000(16진수로 0x017D7840)을 대입하면 이 값은 메모리에 아래와 같이 저장됩니다.

이 값을 시리얼 포트로 보내는 코드를 한번 들여다 봅시다. 아마 아래와 비슷한 모양을 가지고 있을것입니다.

void SendData(int value)
{
    unsigned char *c;
    int i;

    c = (unsigned char *)&value;

    for (i = 0; i < sizeof(int); i++)
        SerialPort_SendByte(*c++);


SerialPort_SendByte() 함수가 4번 호출되겠죠. 위의 그림을 봤을 때 호출되는 순서는 0x40, 0x78, 0x7D, 0x01 순서대로 일 것입니다. 어드레스(c)를 1씩 증가(++)시켜가면서 호출했으니깐요.
시리얼 케이블을 통해 0x40이 가장 먼저 지나갑니다. 그 다음으로 0x78, 0x7D, 0x01 순서대로 지나갑니다. 컴퓨터 B에서는 어떤 일이 벌어질까요? 비슷한 코드가 있어서 수신된 데이터를 메모리에 저장할 것입니다.

void ReceiveData(int *value)
{
    unsigned char *c;
    int i;

    c = (unsigned char *)value;

    for (i = 0; i < sizeof(int); i++)
        *c++ = SerialPort_RecvByte();
} 


비슷하게 SerialPort_RecvByte() 함수가 4번 호출됩니다. 주소(c) 역시 1씩 증가(++)시키면서요. 고스란히 수신되는 순서대로 메모리에 저장되겠죠? 첫번째 그림의 little endian과 같은 모양으로 저장됩니다. 잠깐만, B 컴퓨터는 big endian이라고 했었는데??

네...byte order가 반대로 되어버렸습니다. Big endian을 사용하는 컴퓨터 B의 메모리에 0x40, 0x78, 0x7D, 0x01이 저장된다면 이 값은 integer로 표현될 때 0x40707D01로 인식될 것입니다. 10진수로 표현하면 1,081,113,857이 되네요.
전화통화하면서 나는 "대한민국"이라 말했는데 상대방은 "국민한대"라고 받아적고 있습니다.

byte order가 다른 이기종 시스템간 데이터를 주고받을 때에는 상대방의 byte order를 알고 있어야만 하는 걸까요? 둘 중 하나이긴 한데, 상대방 정체를 모르니 난감합니다. 그래서 규칙이 하나 만들어 졌습니다. 바로 network byte order라는 것입니다.

Network Byte Order는 시스템 내부에서가 아닌 네트워크 상에서의 바이트 순서를 정의하고 있으며, big endian을 사용합니다. 반대로 컴퓨터 시스템 내부에서 사용되는 바이트 순서는 Host Byte Order 라고 합니다. 위에서 설명했었던, CPU따라 little이냐 big이냐 달라지는 그녀석이 바로 host byte order입니다.
위에서 모토롤라 계열 칩셋이 big endian의 대표주자라고 말씀드렸습니다. 모토롤라 계열 칩셋이 전통적으로 전화선 모뎀이나 이더넷 같은 통신장비에 많이 사용되어 왔었고, 그러다보니 network byte order가 big endian으로 자리잡은 것입니다.


살펴본 바와 같이 시스템간 통신을 위해서는 network byte order로 주고받는 것이 정석입니다. 그럼 내가 사용하는 시스템이 big endian이 아닌 경우에는 send하거나 receive할 때 마다 byte order를 뒤집어 줘야 한다는 의미입니다. macro 함수로 간단하게 처리되니 걱정하지 마세요~ 기억해야 할 내용은 통신할 때에는 byte order를 신경써야 한다는 것입니다.

네트워크에서 바이트 순서를 변경해 주는 메크로 함수는 ntoh, hton 입니다. Network TO Host, Host TO Network의 머릿글자를 따서 만들어진 이름이죠. 이 메크로들은 big endian을 사용하는 시스템에서는 아무 일도 안합니다. host byte order와 network byte order가 모두 big endian이니깐요. 한편 little endian을 사용하는 시스템에서는 바이트 순서를 뒤집어줍니다.
바이트 순서를 뒤집는 것에는 데이터 타입에 따라 메크로들이 여러개 있습니다.
2 byte 크기를 가지는 변수를 위해서는 ntohs(), htons()를 씁니다. 마지막의 s는 short를 의미합니다. 4바이트짜리 변수에는 ntohl(), htonl()을 씁니다. l은 long을 의미합니다.
little endian 시스템에서의 ntohs(), ntohl() 메크로 코드는 아래와 같습니다.

#ifndef htons

    #define htons(a)                \

        ((((a) >> 8) & 0x00ff) |    \

         (((a) << 8) & 0xff00))

#endif


#ifndef ntohs

    #define ntohs(a)    htons((a))

#endif


#ifndef htonl

    #define htonl(a)                    \

        ((((a) >> 24) & 0x000000ff) |   \

         (((a) >>  8) & 0x0000ff00) |   \

         (((a) <<  8) & 0x00ff0000) |   \

         (((a) << 24) & 0xff000000))

#endif


#ifndef ntohl

    #define ntohl(a)    htonl((a))

#endif 

간단하죠? 뭔가 거창한게 있는게 아니라 열심히 노가다 뛰는 겁니다.



임베쟁이가 뜬금없이 network byte order 이야기를 꺼내들었습니다만 아직 원래 이야기 하려던 주제는 시작도 못했습니다;; 다음번에 숫자 데이터 타입과 사이즈, 음수 표현에 대한 이야기를 좀 한 다음에 마무리 하도록 하겠습니다~





부록
10진수와 16진수 변환 방법을 모르시는 분들을 위해...
 
시작메뉴 -> 실행 클릭한 다음 calc 라고 입력하고 enter!
그러면 윈도 계산기가 뜹니다. 디폴트는 아래와 같이 "일반용"입니다.


메뉴에서 보기 -> 공학용 을 선택하세요. 아래와 같이 공학용 화면으로 변환됩니다.


왼쪽 위쪽에 Hex, Dec, Oct, Bin이 있죠? 각각 16진수, 10진수, 8진수, 2진수를 의미합니다. 위 이미지에는 Dec가 선택되어 있으니 10진수 모드입니다. 이제 숫자 25,000,000을 입력해 보세요. 키보드도 좋고 마우스도 좋고. 다 입력한 다음 Hex를 클릭해 보세요. 입력한 숫자가 16진수로 변환되어 표시됩니다. 아래처럼.


 17D7840 이라고 7자 숫자가 표시되네요. 4 바이트로 표시하기 위해 앞에 0을 덧붙여 8자리로 만들어 봅시다. 01 7D 78 40. 본 포스팅에서 예로 든 숫자입니다. Hex->Dec 변환도 마찬가지 방식으로 하면 됩니다.
Hex와 Dec를 클릭하는 대신 F5, F6 단축키를 눌러도 됩니다.
보기 메뉴에서 "자릿수 구분 단위"를 체크해 두면 조금 보기 편합니다.