1-2. Endianness

hyunahn·2025년 12월 1일

Endianness (엔디언): 메모리 속 데이터의 줄서기

1. The Core Concept (본질 정의)

"Endianness(엔디언)는 바이트(Byte)들의 줄을 서는 순서입니다."

C언어에서 int는 보통 4바이트(32비트)입니다. 하지만 메모리 주소는 1바이트 단위로 번지가 매겨집니다.
그렇다면 4바이트짜리 정수 0x12345678을 메모리에 저장할 때, 가장 큰 자릿수(0x12)부터 저장해야 할까요, 아니면 가장 작은 자릿수(0x78)부터 저장해야 할까요?

이 순서의 차이가 바로 Big EndianLittle Endian입니다.

  • Big Endian: 사람이 읽는 순서와 같습니다. (앞에서 뒤로)
  • Little Endian: 뒤집혀서 저장됩니다. (뒤에서 앞으로)

2. Under the Hood (하드웨어 투시)

int data = 0x12345678;라는 변수가 메모리 주소 0x1000에 할당되었다고 가정해 봅시다.

  • MSB (Most Significant Byte): 0x12 (가장 큰 자릿수)
  • LSB (Least Significant Byte): 0x78 (가장 작은 자릿수)

A. Big Endian

  • 특징: 대부분의 RISC CPU, 네트워크 표준
  • 방식: 사람이 숫자를 읽는 것과 똑같이 순서대로 들어갑니다.
AddressValue비고
0x10000x12MSB (시작)
0x10010x34
0x10020x56
0x10030x78LSB (끝)

B. Little Endian

  • 특징: Intel x86, x64, 대부분의 ARM 모바일 (PC의 99%)
  • 방식: 작은 자릿수(LSB)가 낮은 주소에 먼저 들어갑니다.
AddressValue비고
0x10000x78LSB (*data 포인터 시작점)
0x10010x56
0x10020x34
0x10030x12MSB (끝)

주의: 바이트 내부의 비트 순서가 바뀌는 게 아닙니다. 바이트 단위의 순서만 바뀝니다. (0x12가 0x21이 되지는 않습니다.)


3. The "Why" & Real-world Analogy (이유와 비유)

Q: 왜 굳이 헷갈리게 Little Endian을 쓸까요?

📅 날짜 표기 비유

  • Big Endian (YYYY-MM-DD): 2023-12-01. 가장 큰 단위(년)가 먼저 나옵니다. 크기 비교(정렬)할 때 편합니다. → 네트워크 표준
  • Little Endian (DD-MM-YYYY): 01-12-2023. 가장 작은 단위(일)가 먼저 나옵니다.

🛠 하드웨어 설계자의 의도 (The Engineering Reason)

  1. 계산의 편리함 (Carry Propagation)

    • 덧셈을 할 때 우리는 일의 자리부터 더해서 올림수(Carry)를 윗자리로 보냅니다.
    • Little Endian은 하위 바이트가 메모리의 낮은 주소(먼저 읽히는 곳)에 있으므로, CPU 회로가 덧셈을 처리할 때 주소를 증가시키며 자연스럽게 올림수 처리를 할 수 있었습니다. (초기 CPU 설계의 유산)
  2. 형 변환(Casting)의 일관성

    • Little Endian에서는 4바이트(int)든, 2바이트(short)든, 1바이트(char)든, 시작 주소(&data)에 있는 값은 항상 LSB(가장 작은 값)입니다.
    • 즉, intchar로 캐스팅해서 읽어도 값의 "크기"는 변하지 않고 하위 8비트를 바로 읽을 수 있습니다.

4. Code & Best Practice

이 개념은 바이트 단위로 메모리를 조작(Manipulation)할 때 결정적입니다.

1) 내 컴퓨터의 Endianness 확인하기 (면접 단골 질문)

#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;
}

2) 네트워크 프로그래밍의 필수: ntohl, htonl

네트워크(인터넷)는 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);

3) 비트 연산자(Shift)는 안전하다

>><< 같은 비트 연산자는 Endianness를 타지 않습니다. 컴파일러와 CPU가 알아서 논리적으로 처리해 줍니다.
따라서 데이터를 파싱할 때는 포인터 캐스팅보다 비트 연산(x >> 8)을 사용하는 것이 이식성(Portability) 면에서 훨씬 안전합니다.


5. Field Guide: 자동차 & 임베디드 실무 (Advanced)

실제 임베디드 시스템이나 자동차 산업(Automotive) 현장에서는 교과서적인 내용 외에 꼭 알아야 할 "현장의 룰"이 있습니다.

🚗 1. ARM 생태계와 자동차 산업의 "정치학"

현실:
모든 현대 ARM 프로세서(Cortex-A, Cortex-M)는 기본적으로 Little Endian으로 설정되어 나옵니다. 하지만 ARM 아키텍처는 설정 가능(Configurable)하므로, 일부 특수 목적의 시스템은 Big Endian을 쓰기도 합니다.

자동차 산업의 현황:

  • ECU (Engine Control Unit): 대부분 Little Endian (ARM, PowerPC 등)
  • Network Protocol (CAN/LIN): Big Endian (Motorola 형식) ⚠️ 매우 중요
  • 소프트웨어 내부 로직: Little Endian
  • 네트워크 전송: Big Endian으로 변환 후 전송

실무 경험: 폭스바겐 같은 대형 OEM에서 CAN 메시지를 다룰 때, 엔디언 변환 실수로 인한 버그가 생각보다 자주 발생합니다. 특히 16비트 온도값 같은 Multi-byte 신호를 다룰 때 주의해야 합니다.

🔧 2. Struct Packing과 Endianness의 "치명적 조합"

하드웨어는 속도를 위해 데이터 사이에 '빈 공간(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). 프로토콜 정의용으로는 사용을 자제하거나 컴파일러 문서를 철저히 확인해야 합니다.

📊 3. 통신 프로토콜별 Endianness 매트릭스

프로토콜표준 Endian예시비고
CANBig Endian (Motorola)자동차 산업 표준
EthernetBig Endian (Network Order)TCP/IP, UDPhtonl() 필수
Serial UART시스템 의존센서 통신문서 확인 필수
I2CLittle Endian (보통)센서 칩칩 매뉴얼 확인
SPI칩 의존메모리, ADC칩별로 모두 다름
USBLittle Endian호스트 통신PC와 연결

실무 팁: "CAN은 항상 Big Endian"이라고 외워두세요. 다른 모든 프로토콜도 칩 매뉴얼을 반드시 읽어야 합니다.

⚡ 4. Endianness 변환 함수의 성능 문제

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 등으로 최적화하세요.

🐛 5. Endianness 버그를 찾는 실무 디버깅 법

전형적인 증상:

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**를 눈으로 직접 확인하는 것이 가장 빠릅니다.

도구 사용:

  • CANalyzer: 가장 신뢰할 만합니다.
  • Wireshark: Ethernet 패킷 분석.
  • 오실로스코프: 물리적 신호 레벨에서 비트 순서 확인.

경험담: 센서 값이 이상해서 3일을 헤맸는데, CANalyzer로 원본 바이트를 찍어보고 htonl() 한 줄 추가해서 해결한 적이 있습니다. Raw Data를 의심하고 직접 눈으로 확인하세요.

0개의 댓글