My Project / / 2007. 3. 30. 16:43

바이트 순서(Big Endian,Little Endian)

바이트 순서

우리가 사용하는 정수, 실수 따위의 변수들은 모두 메모리에 저장된다. 메모리의 저장 단위는 8비트로 구성된 바이트인데 비해 실제 저장해야 할 값은 32비트나 64비트로 바이트 길이보다 훨씬 더 크다. 그래서 여러 개의 연속적인 바이트에 이 값들을 나누어 저장해야 하는데 어떻게 나누는가에 따라 두 가지 방식이 있다. 예를 들어 정수 0x12345678이라는 값을 저장한다고 해 보자. 이 값은 총 32비트이며 0x12, 0x34, 0x56, 0x78 8비트값 4개로 구성된다. 일련의 4바이트를 여러 바이트에 나누어 저장하는 방법으로 다음 두 가지를 생각할 수 있다.

빅 엔디안(Big Endian:순워드) : 이 방식은 높은 자리수를 먼저 저장한다. 0x12가 가장 앞쪽 바이트에, 그리고 0x34가 그 다음 바이트에 저장되는 식이다. 모토롤라 계열의 CPU와 대부분의 RISC CPU가 이 방식을 사용한다. 사람들은 오랫동안 글을 읽을 때 왼쪽에서 오른쪽으로 읽어 왔으므로 읽는 순서에 맞게 4바이트를 나누어 저장한 것이다. 메모리에 나타난 순서대로 읽을 수 있고 자연스러우며 이해하기 쉽다.

리틀 엔디안(Little Endian:역워드) : 이 방식은 낮은 자리수를 먼저 저장한다. 가장 뒤쪽 바이트인 0x78이 메모리의 가장 앞쪽 바이트에 저장되며 그 다음에 0x56, 그리고 0x12는 가장 뒤쪽에 저장된다. 인텔 계열의 CPU DEC의 알파 칩이 이 방식을 사용한다. 메모리의 값을 읽을 때 거꾸로 읽어야 하므로 사람이 직접 읽기에는 다소 불편한 면이 있지만 기계가 값을 다루기는 더 효율적이고 몇 가지 연산에서 편리한 점이 있다.

 

두 방식은 앞쪽에 어떤 바이트부터 저장하는지가 다른데 빅 엔디안은 큰 값부터 리틀 엔디안은 작은 값부터 저장한다. 언뜻 보기에는 빅 엔디안이 훨씬 더 자연스러워 보이고 리틀 엔디안은 다소 이상해 보이지만 CPU가 값을 처리하는 과정을 분석해 보면 리틀 엔디안이 몇 가지 면에서 장점이 있음을 발견할 수 있다.

0x1234라는 32비트의 정수값이 메모리에 저장되어 있을 때 이 값의 하위 2바이트만 읽는다고 해 보자. int형의 값을 short형 변수에 대입한다거나 포인터를 통해 간접적으로 값을 읽을 때 이런 일이 일어나는데 다음 그림은 수형 포인터 pi가 가리키는 32비트 값을 (short *)로 캐스팅해서 읽는 예이다.

리틀 엔디안은 pi가 가리키는 원래 번지에서 2바이트만 읽어들이면 된다. 낮은 자리수가 더 앞쪽에 있기 때문에 이 위치의 값을 그대로 읽으면 바로 16비트값이 되는 것이다. 이에 비해 빅 엔디안의 0x1234라는 값은 0x00001234 pi가 가리키는 곳에 선행 제로 2바이트가 있다. 이 상태에서 뒤쪽의 2바이트를 읽으려면 pi 자리를 뒤쪽으로 2바이트 먼저 옮겨야 하는 번거로움이 있다.

타입의 확장이 일어날 때도 마찬가지이다. 0x1234라는 16비트 정수를 4바이트 정수로 확장해야 한다고 해 보자. 이런 경우는 늘상 일어나는데 short형 변수를 인수로 전달할 때, 수식 내에서 연산될 때, short형 값을 리턴할 때 항상 32비트로 확장된다. 32비트 환경에서는 32비트 단위로 처리하는 것이 가장 유리하며 스택의 크기가 32비트로 고정되어 있기 때문이다.

리틀 엔디안은 0x34, 0x12로 저장되어 있는 뒤쪽에 0x00, 0x00를 덧붙이기만 하면 간단하게 32비트로 확장된다. 뒤쪽에 더 높은 자리수가 있으므로 뒤에 붙이는 0값은 선행 제로가 되어 값 자체에 영향을 미치지 않기 때문이다. 이에 비해 빅 엔디안은 선행 제로가 들어갈 공간을 만들기 위해 앞쪽의 0x12, 0x34를 뒤쪽의 메모리로 이동시켜야 하므로 여분의 연산이 필요하다.

보다시피 값의 임시적인 축소나 확장이 일어날 때는 리틀 엔디안이 훨씬 더 편리하고 효율적이다. 그렇다면 리틀 엔디안이 항상 좋기만 한가 하면 단점도 있다. 값을 구성하는 각 바이트를 배열처럼 다루고자 할 때는 빅 엔디안이 더 편리하다. 예를 들어 정수형 값을 8비트씩 읽어서 출력한다고 해 보자.

 

: ReadEndian

#include <TurboC.h>

 

void main()

{

     int i=0x12345678,j;

     char *p=(char *)&i;

 

     // 빅 엔디안

//  for (j=0;j<sizeof(int);j++) {

//       printf("%x ",p[j]);

//  }

 

     // 리틀 엔디안

     for (j=sizeof(int)-1;j>=0;j--) {

          printf("%x ",p[j]);

     }

     putch('\n');

}

 

빅 엔디안은 높은 자리수가 앞쪽에 있으므로 순서대로 출력하기를 길이만큼만 반복하면 된다. 반면 리틀 엔디안은 앞쪽의 높은 자리수부터 출력하기 위해 배열의 뒤쪽부터 값을 읽어야 한다. 값을 출력할 때 거꾸로 읽어야 하는 것과 마찬가지로 사람이 값을 읽을 때도 이 출력 순서대로 읽어야 한다는 점이 무척 불편하다. 사람의 상식적인 생각과는 반대로 되어 있어 때로는 이것이 황당한 실수의 원인이 되기도 하고 메모리를 직접 조작할 때 항상 주의를 기울여야 한다. 한마디로 헷갈린다는 얘기다.

바이트끼리의 순서를 정하는 방식에 두 가지가 있듯이 바이트를 구성하는 8비트를 나열하는 순서도 두 가지를 생각할 수 있다. 예를 들어 0x64(십진수로 100)을 저장할 때 왼쪽에서 오른쪽으로 나열하면 01100100이 될 것이고 오른쪽에서 왼쪽으로 나열하면 00100110이 될 것이다. 두 방식은 각 이진 자리수의 가중치가 다르게 매겨진다. 그러나 다행히 현존하는 모든 CPU 의 비트 순서는 빅 엔디안으로 통일되어 있어 이런 것까지는 신경쓰지 않아도 된다.

빅 엔디안과 리틀 엔디안 방식은 큰 값을 작은 단위에 나누어 저장하는 두 가지 방식 중의 하나일 뿐이며 어떤 방식이 절대적으로 우수하다고 할 수는 없다. 값의 조각을 저장하는 순서가 다른 것 뿐이며 CPU 설계자들은 CPU의 구조나 설계 방식, 활용 방안 등에 따라 두 방식 중 하나를 선택한 것 뿐이다.

그렇다면 개발자인 우리들은 플랫폼의 바이트 저장 순서에 관심을 가질 필요가 있을까? 사실 이런 내부적인 저장 순서는 신경쓸 필요가 거의 없다. 왜냐하면 리틀 엔디안 방식이 기록할 때 거꾸로 기록해 놓더라도 다시 읽을 때 역시 거꾸로 읽어 오기 때문에 어차피 우리가 받는 값은 최초 저장해 놓은 값이다. 0x3a991bc8 3a, 99, 1b, c8로 저장하든 c8, 1b, 99, 3a로 저장하든 다시 읽어올 때 그 값이 0x3a991bc8이기만 하면 되는 것이다. 내부적인 저장 방식만 반대로 되어 있는 것이지 값 자체를 바꿔 버리는 것은 아니므로 고급 언어 사용자들은 이를 신경쓸 필요가 없으며 심지어 이런 것들이 있다는 것조차 몰라도 별 지장이 없다.

그러나 아주 특수한 상황에서는 이 사실을 알아야 되는 경우도 있는데 디버깅 중에 변수가 저장된 메모리 영역을 직접 들여다 본다거나 아니면 변수의 값을 바이트 단위로 직접 조립해야 하는 경우 등이다. 이외에 바이트 저장 방식이 다른 이기종 컴퓨터간의 네트워크 통신을 할 때, 구체적으로 팬티엄 PC와 매킨토시가 통신할 때 엔디안을 맞추어야 하는 번거로움이 있다. 소켓은 기본적으로 빅 엔디안으로 통일되어 있으므로 인텔 계열 CPU는 보낼 때 뒤집어 보내고 받은 값도 뒤집어야 원래 값을 제대로 읽을 수 있다.

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유