

"Endianness(엔디언)는 바이트(Byte)들의 줄을 서는 순서입니다."
C언어에서 int는 보통 4바이트(32비트)입니다. 하지만 메모리 주소는 1바이트 단위로 번지가 매겨집니다.
그렇다면 4바이트짜리 정수 0x12345678을 메모리에 저장할 때, 가장 큰 자릿수(0x12)부터 저장해야 할까요, 아니면 가장 작은 자릿수(0x78)부터 저장해야 할까요?
이 순서의 차이가 바로 Big Endian과 Little Endian입니다.
int data = 0x12345678;라는 변수가 메모리 주소 0x1000에 할당되었다고 가정해 봅시다.
0x12 (가장 큰 자릿수)0x78 (가장 작은 자릿수)| Address | Value | 비고 |
|---|---|---|
| 0x1000 | 0x12 | MSB (시작) |
| 0x1001 | 0x34 | |
| 0x1002 | 0x56 | |
| 0x1003 | 0x78 | LSB (끝) |
| Address | Value | 비고 |
|---|---|---|
| 0x1000 | 0x78 | LSB (*data 포인터 시작점) |
| 0x1001 | 0x56 | |
| 0x1002 | 0x34 | |
| 0x1003 | 0x12 | MSB (끝) |
주의: 바이트 내부의 비트 순서가 바뀌는 게 아닙니다. 바이트 단위의 순서만 바뀝니다. (0x12가 0x21이 되지는 않습니다.)
Q: 왜 굳이 헷갈리게 Little Endian을 쓸까요?
2023-12-01. 가장 큰 단위(년)가 먼저 나옵니다. 크기 비교(정렬)할 때 편합니다. → 네트워크 표준01-12-2023. 가장 작은 단위(일)가 먼저 나옵니다.계산의 편리함 (Carry Propagation)
형 변환(Casting)의 일관성
int)든, 2바이트(short)든, 1바이트(char)든, 시작 주소(&data)에 있는 값은 항상 LSB(가장 작은 값)입니다.int를 char로 캐스팅해서 읽어도 값의 "크기"는 변하지 않고 하위 8비트를 바로 읽을 수 있습니다.이 개념은 바이트 단위로 메모리를 조작(Manipulation)할 때 결정적입니다.
#include <stdio.h>
int main() {
int x = 1; // 0x00000001
// 포인터를 char* (1바이트)로 캐스팅하여 첫 번째 바이트만 들여다봄
char *c = (char*)&x;
if (*c) {
// 첫 바이트가 1이라면 (0x01 ... ) -> Little Endian
printf("Little Endian: LSB is at low address.\n");
} else {
// 첫 바이트가 0이라면 (0x00 ... ) -> Big Endian
printf("Big Endian: MSB is at low address.\n");
}
return 0;
}
네트워크(인터넷)는 Big Endian을 표준으로 사용합니다. PC(Little Endian)에서 데이터를 그냥 보내면 상대방은 엉뚱한 숫자로 해석합니다.
htonl (Host to Network Long): 내 컴퓨터 방식 → 네트워크 방식 (전송 전 필수)ntohl (Network to Host Long): 네트워크 방식 → 내 컴퓨터 방식 (수신 후 필수)/* 잘못된 예시: Endian 무시 */
send(sock, &my_int, sizeof(my_int), 0); // 상대방이 Big Endian이면 값이 깨짐
/* 올바른 예시 */
uint32_t net_int = htonl(my_int); // Big Endian으로 변환
send(sock, &net_int, sizeof(net_int), 0);
>>나 << 같은 비트 연산자는 Endianness를 타지 않습니다. 컴파일러와 CPU가 알아서 논리적으로 처리해 줍니다.
따라서 데이터를 파싱할 때는 포인터 캐스팅보다 비트 연산(x >> 8)을 사용하는 것이 이식성(Portability) 면에서 훨씬 안전합니다.
실제 임베디드 시스템이나 자동차 산업(Automotive) 현장에서는 교과서적인 내용 외에 꼭 알아야 할 "현장의 룰"이 있습니다.
현실:
모든 현대 ARM 프로세서(Cortex-A, Cortex-M)는 기본적으로 Little Endian으로 설정되어 나옵니다. 하지만 ARM 아키텍처는 설정 가능(Configurable)하므로, 일부 특수 목적의 시스템은 Big Endian을 쓰기도 합니다.
자동차 산업의 현황:
실무 경험: 폭스바겐 같은 대형 OEM에서 CAN 메시지를 다룰 때, 엔디언 변환 실수로 인한 버그가 생각보다 자주 발생합니다. 특히 16비트 온도값 같은 Multi-byte 신호를 다룰 때 주의해야 합니다.
하드웨어는 속도를 위해 데이터 사이에 '빈 공간(Padding)'을 끼워 넣기도 합니다. 이 Padding과 Endianness가 만나면 재앙이 될 수 있습니다.
문제의 본질:
// 이 구조체는 CPU가 속도를 위해 자동으로 정렬(Align)함
struct CANMessage {
uint8_t id; // 1바이트
uint16_t temp; // 2바이트 (중간에 1바이트 패딩이 추가될 수 있음!)
uint8_t status; // 1바이트
};
// 실제 크기: 4바이트? 6바이트? 8바이트? -> 컴파일러 마음대로!
자동차에서의 재앙 시나리오:
CAN 프로토콜은 정확히 정의된 바이트 페이로드를 기대합니다. 컴파일러가 패딩을 추가하면 데이터 위치가 밀려 완전한 오작동이 발생합니다.
해결책: Packing
// CAN 통신에서는 반드시 패킹 필수
#pragma pack(1)
struct CANMessage {
uint8_t id;
uint16_t temp;
uint8_t status;
};
#pragma pack() // 정확히 4바이트 보장
폭스바겐의 CAN 메시지 정의서에는 항상 #pragma pack(1) 지시문이 명시되어 있어야 합니다.
추가 주의: Bitfield
struct Signal { uint32_t value : 16; uint32_t status : 8; };Bitfield의 내부 비트 배치 순서(Endianness)는 표준이 없으며 컴파일러마다 다릅니다. (GCC vs MSVC vs IAR). 프로토콜 정의용으로는 사용을 자제하거나 컴파일러 문서를 철저히 확인해야 합니다.
| 프로토콜 | 표준 Endian | 예시 | 비고 |
|---|---|---|---|
| CAN | Big Endian (Motorola) | 자동차 산업 표준 | |
| Ethernet | Big Endian (Network Order) | TCP/IP, UDP | htonl() 필수 |
| Serial UART | 시스템 의존 | 센서 통신 | 문서 확인 필수 |
| I2C | Little Endian (보통) | 센서 칩 | 칩 매뉴얼 확인 |
| SPI | 칩 의존 | 메모리, ADC | 칩별로 모두 다름 |
| USB | Little Endian | 호스트 통신 | PC와 연결 |
실무 팁: "CAN은 항상 Big Endian"이라고 외워두세요. 다른 모든 프로토콜도 칩 매뉴얼을 반드시 읽어야 합니다.
htonl()은 표준이지만, 임베디드 시스템(특히 CAN 메시지 처리 등 초당 1000회 이상 호출되는 루틴)에서는 성능 오버헤드가 될 수 있습니다.
최적화된 코드 예시:
// 방법 1: ARM GCC 내장 함수 (가장 효율적, CPU 단일 명령어로 처리)
#define SWAP32(x) __builtin_bswap32(x)
uint32_t net_value = SWAP32(value);
// 방법 2: 매크로로 인라인 처리 (함수 호출 비용 제거)
#define SWAP32(x) (((x) << 24) & 0xFF000000) | \
(((x) << 8) & 0xFF0000) | \
(((x) >> 8) & 0xFF00) | \
(((x) >> 24) & 0xFF)
// 방법 3: 표준 함수 (최적화 옵션 켜면 컴파일러가 알아서 할 수도 있음)
uint32_t net_value = htonl(value);
원칙: 정확성 > 성능. 먼저 htonl 등으로 정확하게 구현하고, 프로파일링 후 병목이 확인되면 __builtin_bswap 등으로 최적화하세요.
전형적인 증상:
CAN 수신값:
0x12 0x34/ 코드 해석값:0x3412(???) / 기대값:0x1234
원인 분석 체크리스트:
1. CPU Endianness 확인: 코드로 런타임 체크 (if (*(char*)&test == 1)).
2. 프로토콜 문서 재확인: CAN DB(DBC 파일)에 "Motorola Byte Order"라고 되어 있는가?
3. 메모리 덤프로 직접 검증:
```c
printf("Raw Bytes: ");
for(int i = 0; i < 8; i++) printf("%02X ", can_buffer[i]);
printf("\n");
```
로그에 찍힌 **Raw Byte**를 눈으로 직접 확인하는 것이 가장 빠릅니다.
도구 사용:
경험담: 센서 값이 이상해서 3일을 헤맸는데,
CANalyzer로 원본 바이트를 찍어보고htonl()한 줄 추가해서 해결한 적이 있습니다. Raw Data를 의심하고 직접 눈으로 확인하세요.