← 블로그로 돌아가기CPU 구조

엔디언: 빅 엔디언과 리틀 엔디언(버스 다이어그램 포함)

빅 엔디언은 최상위 바이트를 가장 낮은 주소에 놓고 리틀 엔디언은 반대로 배치합니다. 0x12345678의 버스 배치와 x86·TCP/IP가 다른 이유를 알아봅니다.

DennyDenny
약 8분 소요
Chalkboard feature illustration for “엔디언: 빅 엔디언과 리틀 엔디언(버스 다이어그램 포함)”

요약: 엔디언은 여러 바이트 값을 메모리에 배치하는 순서를 정한 규칙입니다. 빅 엔디언은 최상위 바이트를 가장 낮은 주소에 두며 여러 네트워크 프로토콜 필드와 과거의 많은 PowerPC 시스템이 이 방식을 사용합니다. 리틀 엔디언은 최하위 바이트를 그 주소에 두며 x86과 표준 RISC-V 프로파일이 이 방식을 사용합니다. 바이트가 인터페이스를 통과할 때는 아키텍처, 동작 모드 및 프로토콜을 각각 확인해야 합니다.

32비트 정수는 4바이트를 차지합니다. 메모리는 한 번에 한 바이트 주소로 접근합니다. 그렇다면 32비트 값을 메모리에 쓸 때 어느 바이트를 가장 낮은 주소에 둘까요? 이 선택이 엔디언입니다. 컴퓨터 아키텍처에서 두 가지 합리적인 답이 나란히 살아남은 드문 결정 중 하나이며, 잘못 처리하면 아무 경고 없이 틀린 숫자가 나옵니다.

“엔디언”의 의미

32비트 값 0x123456780\text{x}12345678을 보겠습니다. 바이트 네 개로 이루어져 있습니다.

  • 0x120\text{x}12 — 최상위 바이트(MSB)
  • 0x340\text{x}34
  • 0x560\text{x}56
  • 0x780\text{x}78 — 최하위 바이트(LSB)

이 값을 주소 AA부터 메모리에 저장하려면 AA, A+1A+1, A+2A+2, A+3A+3에 네 바이트를 넣어야 합니다. 주된 방법은 두 가지입니다.

빅 엔디언. MSB를 먼저 저장합니다.

주소AAA+1A+1A+2A+2A+3A+3
바이트0x120\text{x}120x340\text{x}340x560\text{x}560x780\text{x}78

리틀 엔디언. LSB를 먼저 저장합니다.

주소AAA+1A+1A+2A+2A+3A+3
바이트0x780\text{x}780x560\text{x}560x340\text{x}340x120\text{x}12

같은 32비트 값과 같은 네 바이트인데 메모리 배치는 완전히 다릅니다. 빅 엔디언 기계의 프로그램이 0x123456780\text{x}12345678을 쓰고 리틀 엔디언 기계의 프로그램이 AA부터 A+3A+3까지 네 바이트를 차례로 읽어 재구성하면 0x785634120\text{x}78563412가 됩니다. 바이트는 같지만 숫자는 틀립니다.

이 이름은 대니 코언이 1980년에 쓴 글에서 나왔습니다. 그는 걸리버 여행기에서 삶은 달걀의 어느 쪽 끝부터 깨야 하는지를 두고 릴리퍼트 사람들이 전쟁을 벌인 이야기를 빌렸습니다. 비유의 요지는 선택 자체는 임의적이지만 집단은 싸움을 시작할 만큼 확고하게 한쪽을 고수할 수 있다는 것이었습니다.

읽기 동작의 버스 다이어그램

엔디언 규칙은 CPU의 load/store 장치가 구현합니다. CPU가 주소 AA에서 32비트 load를 실행하면 주소 버스에 AA를 내보내고 데이터 버스에서 4바이트를 받습니다.

   CPU                         Memory
+--------+   address=A     +---------+
|  load  | ───────────────>|  byte 0 | A    : 0x12 (big) or 0x78 (little)
| 32-bit |                 |  byte 1 | A+1  : 0x34 (big) or 0x56 (little)
|        |   data[31:0]    |  byte 2 | A+2  : 0x56 (big) or 0x34 (little)
|        | <───────────────|  byte 3 | A+3  : 0x78 (big) or 0x12 (little)
+--------+                 +---------+

빅 엔디언 CPU에서는 가장 낮은 주소의 byte 0이 32비트 레지스터의 최상위 lane으로 이동하고 byte 3은 최하위 lane으로 들어갑니다. 리틀 엔디언 CPU에서는 byte 0이 최하위 lane으로, byte 3이 최상위 lane으로 들어갑니다.

REGISTER와 RAM 부품 문서는 이 전송에 참여하는 저장 원시 소자를 설명합니다. 엔디언은 둘 사이의 순수한 배선 규칙입니다. 넓은 레지스터의 어느 바이트를 메모리의 어느 바이트 주소에 연결하는지 정합니다.

동기식 메모리 접근에서는 보통 넓은 데이터 경로를 통해 여러 바이트가 버스에 동시에 제시됩니다. 더 좁은 8비트 버스라면 같은 바이트가 네 사이클에 걸쳐 순서대로 도착합니다. 엔디언 규칙은 두 경우에 똑같이 적용됩니다.

예제: C에서 0x12345678

다음 의사 코드 조각을 생각해 봅시다.

uint32_t x = 0x12345678;
uint8_t  *p = (uint8_t *)&x;
printf("%02x %02x %02x %02x\n", p[0], p[1], p[2], p[3]);

빅 엔디언 기계에서는 12 34 56 78, 리틀 엔디언 기계에서는 78 56 34 12가 출력됩니다. 포인터 pp는 메모리를 바이트 단위로 이동하고, 바이트 순서는 엔디언에 따라 달라지는 실제 메모리 배치와 정확히 같습니다.

이는 표준적인 엔디언 확인 방법입니다. 동시에 흔한 버그의 원인이기도 합니다. 포인터 캐스트로 정수의 각 바이트를 “추출”하는 코드는 엔디언에 의존하며, 명시적으로 바이트를 바꾸지 않으면 한 호스트에서만 동작합니다.

어느 아키텍처가 어느 방식을 선택했나

아키텍처기본 엔디언
x86, x86-64리틀 엔디언
RISC-V리틀 엔디언(빅 엔디언 변형도 존재)
ARM양방향 설정 가능, 리틀 엔디언이 압도적으로 흔함
PowerPC역사적으로 빅 엔디언, 이후 코어는 양방향 지원
MIPS양방향 지원, 기본값은 SoC마다 다름
SPARC빅 엔디언(V9는 양방향 지원)
Motorola 68k빅 엔디언
z/Architecture(IBM 메인프레임)빅 엔디언
네트워크 바이트 순서(TCP/IP, IPv4, IPv6 헤더)빅 엔디언

오늘날의 무게 중심은 확실히 리틀 엔디언입니다. x86과 ARM이 현재 사용되는 CPU의 압도적 다수를 차지합니다. 네트워크 프로토콜은 역사적 이유로 빅 엔디언에 머물러 있습니다. TCP/IP가 명세된 1970년대 후반에는 빅 엔디언 DEC PDP와 IBM 메인프레임이 분위기를 주도했습니다.

두 진영이 계속 공존하는 이유

양쪽 모두 실제 장점이 있지만 어느 것도 결정적이지 않습니다.

빅 엔디언의 장점

위에서 아래로 읽는 순서가 사람이 숫자를 쓰는 방식과 같습니다. 0x120\text{x}12가 메모리에서 처음 보이는 바이트이며 종이에 처음 쓰는 숫자이기도 합니다. 빅 엔디언 구조체의 hex dump는 바로 읽을 수 있습니다.

네트워크 바이트 순서. 인터넷은 명세상 빅 엔디언을 사용합니다(RFC 791, RFC 1700). 이진 프로토콜을 선로에 직렬화하는 곳에서는 빅 엔디언이 확립된 규칙입니다.

부호를 바로 비교할 수 있습니다. 2의 보수 정수의 부호 비트는 최상위 바이트에 있습니다. 빅 엔디언 기계에서는 가장 낮은 주소에서 이 비트를 보므로 부호 확장 load는 처음 가져온 바이트만 보면 됩니다.

리틀 엔디언의 장점

LSB 주소에 LSB가 있습니다. 주소 AA의 바이트는 주소 AA에 있는 정수의 최하위 바이트입니다. 즉, 같은 주소를 가리키는 32비트 포인터와 8비트 포인터가 모두 최하위 바이트를 가리킵니다. int*를 char*로 캐스트해도 주소가 바뀌지 않으므로 16비트 레지스터 안 32비트 값의 하위 바이트를 읽는 동작이 공짜입니다.

산술의 carry 순서. 다중 정밀도 덧셈은 최하위 바이트에서 시작하여 carry를 더 높은 주소 방향으로 전파합니다. 리틀 엔디언 기계는 메모리를 자연스럽게 순회하는 순서로 피연산자를 읽고 결과를 씁니다. 버퍼가 제한적이던 1978년의 8086에서는 실제 성능 논거였습니다.

폭과 무관한 load. 주소 AA의 32비트 값은 주소 AA의 16비트 값 뒤에 A+2A+2의 다른 16비트 값이 있는 것과 같고, AA에서 시작하는 네 8비트 값과도 같습니다. 폭 사이에서 type punning을 해도 가장 낮은 바이트에서는 차이가 보이지 않습니다. x86 소프트웨어가 역사적으로 무분별한 캐스트를 견딘 이유 중 하나입니다.

어느 진영도 완전히 승리하지 않았습니다. ARM은 부팅 시 커널이 선택하는 양방향 아키텍처라는 절충안을 택했습니다.

엔디언이 중요해지는 때

한 기계에서 일상적으로 프로그래밍할 때 엔디언은 보이지 않습니다. CPU가 자신의 정수 형식에 맞게 바이트 순서를 올바르게 처리하고, 컴파일러가 알맞은 load와 store를 생성하므로 프로그램이 신경 쓸 필요가 없습니다.

정수가 아니라 바이트를 읽거나 쓰고, 그 바이트가 다른 곳에서도 같은 뜻이어야 하는 순간 엔디언이 드러납니다.

이진 파일 형식. ELF 헤더, PNG chunk, MP4 box, GIF, BMP, ZIP이 있습니다. 일부는 빅 엔디언(PNG), 일부는 리틀 엔디언(BMP, ZIP)이고 두 방식을 섞는 것도 있습니다. 바이트를 바꾸지 않고 반대 엔디언 호스트에서 ELF 헤더를 읽으면 엉뚱한 offset을 얻습니다.

네트워크 프로토콜. TCP, UDP, IP, ICMP, DNS, NTP는 모두 빅 엔디언입니다. C 라이브러리 함수 htonl, htons, ntohl, ntohs는 “host to network long/short”와 그 반대 변환을 수행하기 위해 존재합니다. 빅 엔디언 호스트에서는 아무 일도 하지 않고 리틀 엔디언 호스트에서는 바이트를 뒤집습니다.

#include <arpa/inet.h>

uint32_t host_value = 0x12345678;
uint32_t net_value  = htonl(host_value);  // 0x78563412 on x86
                                          // 0x12345678 on a big-endian host

선로에 흐르는 바이트는 두 호스트에서 같습니다. 메모리 표현만 다릅니다.

장치 프로토콜. SPI flash, I2C EEPROM, sensor register는 대부분 데이터시트에서 한 바이트 순서를 정하고 호스트가 따르도록 요구합니다. 빅 엔디언이 많지만 섞인 방식도 있습니다.

Type punning. 포인터나 union을 통해 서로 다른 폭의 정수 형식으로 캐스트하면서 바이트가 고정된 offset에 있을 것이라고 기대하는 패턴입니다. 한 호스트에서는 “그냥 동작”하지만 다른 호스트에서는 말없이 실패합니다.

혼합 엔디언 시스템의 memory-mapped register. 빅 엔디언 CPU가 리틀 엔디언 주변장치를 제어하거나 그 반대라면 레지스터에 접근할 때마다 명시적인 byte-swap 명령이 필요합니다.

일반 원칙은 이렇습니다. 바이트가 CPU의 독점적인 관리 범위를 떠나는 순간 엔디언은 반드시 지켜야 하는 계약이 됩니다. 한 호스트의 한 프로세스 안에서는 무시할 수 있지만, 기계·파일·프로토콜·주변장치 사이에서는 그럴 수 없습니다.

혼합 엔디언과 미들 엔디언

바이트 순서의 역사에는 각주가 있습니다. 일부 옛 시스템은 32비트 값을 16비트 절반 두 개로 나누어 절반끼리는 빅 엔디언 순서로 저장하되, 각 절반 내부는 리틀 엔디언으로 저장하는 미들 엔디언 방식을 사용했습니다. PDP-11이 일부 동작 모드에서 32비트 값에 이 배치를 썼습니다. 그 결과 0x123456780\text{x}12345678의 네 바이트는 0x34,0x12,0x78,0x560\text{x}34, 0\text{x}12, 0\text{x}78, 0\text{x}56으로 저장되어 순수한 빅 엔디언도 리틀 엔디언도 아닙니다. 현대 시스템은 미들 엔디언을 쓰지 않지만 PDP 시대 도구가 만든 오래된 이진 파일 형식에서 가끔 나타납니다.

일부 Arm 시스템은 과거의 BE-32와 바이트 불변 BE-8도 구분했습니다. 중요한 교훈은 CPU 이름만으로 주변장치나 파일 배치를 추측하지 않는 것입니다. 정확한 접근 방식은 아키텍처, 버스, 장치 문서를 확인해야 합니다.

인출-디코드-실행 주기에서의 엔디언

CPU가 명령어를 실행할 때는 메모리에서 명령어 워드를 가져와야 합니다. 빅 엔디언 CPU에서는 명령어 워드가 상위 바이트부터 배치되고, 리틀 엔디언 CPU에서는 하위 바이트부터 배치됩니다. 디코드 논리도 그 방식에 맞아야 합니다. 정상 동작에서는 보이지 않습니다. CPU는 자신의 명령어 스트림만 디코드하므로 엔디언이 일관되기 때문입니다. 그러나 다른 대상용으로 빌드한 바이너리를 debugger나 disassembler가 다른 호스트에서 검사할 때는 중요합니다.

전체 명령어 인출 파이프라인은 인출-디코드-실행 주기 글에서 다룹니다. 엔디언은 메모리와 프로그램 카운터 사이에서 맞아야 하는 마지막 세부 사항입니다.

어느 순서도 보편적인 하드웨어 승자가 아닌 이유

x86에서 0x123456780\text{x}12345678의 hex dump인 78 56 34 12를 보면 거꾸로 느껴집니다. 눈은 왼쪽에서 오른쪽으로 읽는데 숫자가 두 자리씩 뒤집혀 있어 머릿속 변환이 어색합니다.

리틀 엔디언은 여러 바이트 정수의 주소가 곧 최하위 바이트의 주소라는 편리한 속성을 제공합니다. 바이트 단위의 다중 정밀도 덧셈은 증가하는 주소 방향으로 이동하며 carry를 전파할 수 있습니다. 리틀 엔디언 정수를 제자리에서 확장해도 하위 부분의 주소는 유지됩니다.

빅 엔디언은 상보적인 속성을 제공합니다. 바이트가 중요도 순서로 보이므로 메모리 dump나 사전식 비교가 최상위 부분에서 시작합니다. 네트워크 바이트 순서는 여러 octet 값에 이 최상위 octet 우선 규칙을 사용합니다.

현대 프로세서는 보통 워드 전체를 load하고 필요에 따라 load/store 경로에서 바이트를 재배열합니다. 엔디언은 메모리와 인터페이스 경계에서 여전히 중요하지만 레지스터 내부의 리플 캐리 덧셈이 내는 불 결과를 바꾸지는 않습니다. 역사적인 채택은 하나의 보편적으로 우월한 회로가 아니라 아키텍처와 호환성 선택을 반영합니다.

런타임에 엔디언 감지하기

C에서는 어느 객체든 alias할 수 있도록 허용된 unsigned char 포인터로 객체 표현을 검사할 수 있습니다.

int is_little_endian(void) {
    unsigned int x = 1;
    return *(const unsigned char *)&x == 1;
}

가장 낮은 주소의 바이트가 1이면 최하위 바이트가 먼저 옵니다. 결과가 false라고 해서 특이한 혼합 엔디언 구현에서 일반적인 빅 엔디언이라고 단정할 수는 없습니다.

<endian.h>, <sys/endian.h> 또는 __BYTE_ORDER__ 같은 컴파일러 매크로를 C99 기능이라고 말하면 안 됩니다. 플랫폼이나 컴파일러가 제공하는 기능입니다. C23은 <stdbit.h>를 통해 native-endian 식별자를 추가했지만 배포된 컴파일러 지원과 프로젝트 언어 모드는 서로 다릅니다. 이식 가능한 직렬화에는 선로 바이트를 명시적으로 정의하고 shift와 mask, 또는 htonl/ntohl 같은 문서화된 라이브러리 루틴으로 정수를 조립하거나 분해하는 설계가 더 견고합니다.

DigiSim에서 시뮬레이션하려면 낮은 주소의 바이트를 load하고 레지스터의 상위 바이트에 놓은 뒤 기대 패턴과 결과를 비교하는 작은 read-modify-write 고리를 만들 수 있습니다. 배선 차이가 값에 미치는 영향을 손으로 느끼는 방법입니다. 3,500년 이진법의 여정 글은 사람이 읽기 쉬운 숫자 순서와 기계의 편의가 상호작용해 온 더 긴 역사 속에 이 문제를 놓습니다.

다음 단계

엔디언은 이진 데이터를 교환할 때 필요한 바이트 수준 규칙 중 하나입니다. 다른 규칙에는 정수 인코딩인 2의 보수 부호 있는 산술과 부동소수점 형식인 IEEE 754가 있습니다. 인출–디코드–실행 심층 분석은 이 표현들이 명령어 처리와 어떻게 연결되는지 보여 줍니다.

기본 RAM 메모리 시스템 템플릿을 열어 바이트 주소 메모리가 동작하는 모습을 보세요. 네 주소에 걸쳐 32비트 값을 쓴 다음 다른 바이트 순서로 읽어 배선 차이를 직접 느껴 보세요.

주요 참고 자료

아이디어를 실제로 작동하는 회로로 만들어 보세요.