Little endian 시스템에서 C 구조체로 access 하기

Little endian 시스템에서 스트림 데이터를 C 구조체 포인터를 이용하여 엑세스하는 방법을 공유한다.

Big endian, little endian

컴퓨터에서 2 바이트 이상인 데이터 타입을 저장할 때 MSB부터 저장하는 것을 big endian, LSB부터 저장하는 것을 little endian이라 부른다. 예를 들어 0x12345678이라는 4바이트 값을 메모리에 저장할 때, 데이터는 각 endian에 따라서 메모리 상에서 아래와 같이 저장된다.

  • Big endian: 0x12 0x34 0x56 0x78
  • Little endian: 0x78 0x56 0x34 0x12

인간이 이해하기 쉽고 이 글에서도 다루고 있는 구조체를 직접 이용하기에도 좋은 것은 big endian이다. Little endian은 과거 CPU에서 좀 더 빠른 엑세스가 되는 경우도 있었지만, 현대적인 CPU에서는 big endian 경우에도 동일 클럭 수로 메모리를 엑세스 할 수 있으므로, 사실상 두 endian의 속도는 동일하다.
CPU마다 지원하는 endian이 고정되어 있거나 bootstrap으로 결정할 수 있는데, 요즘 나의 경우는 주로 little endian 시스템을 쓰고 있다.

Little endian의 단점

사실 통상적인 프로그램을 잘 때는 endian 차이를 인지할 필요가 없는 경우도 많다. 하지만 예를 들어, 스트림으로 오는 데이터가 정해진 구조에 맞춰서 오는 경우라면, C/C++에서는 정의된 데이터 구조에 맞추어서 구조체를 선언하고 이것의 포인터를 이용하여 각 멤버를 간단히 엑세스할 수 있다.


그런데 이 방식은 big endian 시스템에서는 그냥 되지만, little endian 시스템에서는 순서가 다르므로 그냥은 되지 않는다. 그래서 보통은 bit 연산을 통해서 개별 멤버를 추출하는 방법을 사용하게 되는데, 이 방법은 endian과는 무관하게 정상 동작하는 장점은 있지만, 코드가 복잡해지고 만약 이 때 bit 연산을 실수하게 되면, 나중에 찾기 힘든 버그가 된다. 😵) 특히 데이터 구조가 여러 다른 길이의 데이터 타입으로 이루어져있고, 특히 bit로 나누어지는 경우에는 bit 연산이 복잡해지고, 연산에 버그가 있더라도 눈에 잘 띄지 않게 된다.

Big endian에서의 처리 예

예를 들어, 아래는 MPEG-2 TS(Transport Stream) 헤더에서 앞 부분 4바이트를 C에서 구조체로 나타낸 것이다. (1바이트 1개, 2바이트 1개, 1바이트 1개로 구성됨)

typedef struct __attribute__((__packed__))
{
    uint8_t sync_byte;
    uint16_t transport_error_indicator : 1;
    uint16_t payload_unit_start_indicator : 1;
    uint16_t transport_priority : 1;
    uint16_t PID : 13;
    uint8_t transport_scrambling_control : 2;
    uint8_t adaptation_field_control : 2;
    uint8_t continuity_counter : 4;
} TsHeader;

위 구조체를 이용하여 big endian 시스템에서는 아래와 같이 쉽게 구조체 멤버로 엑세스할 수 있다. (예로 테스트 데이터는 0x47 0x40 0x10 0x1C 사용)

unsigned char buf[] = {0x47, 0x40, 0x10, 0x1C};

TsHeader *header = (TsHeader *)buf;
printf("sync_byte: 0x%X\n", header->sync_byte);
printf("transport_error_indicator: %d\n", header->transport_error_indicator);
printf("payload_unit_start_indicator: %d\n", header->payload_unit_start_indicator);
printf("transport_priority: %d\n", header->transport_priority);
printf("PID: 0x%X\n", header->PID);
printf("transport_scrambling_control: %d\n", header->transport_scrambling_control);
printf("adaptation_field_control: %d\n", header->adaptation_field_control);
printf("continuity_counter: %d\n", header->continuity_counter);

코드를 실행해보면 아래와 같이 기대대로 값들이 출력된다.

sync_byte: 0x47
transport_error_indicator: 0
payload_unit_start_indicator: 1
transport_priority: 0
PID: 0x10
transport_scrambling_control: 0
adaptation_field_control: 1
continuity_counter: 12

Little endian에서의 처리 예

Little endian에서는 위와 같이 편리하게 구조체를 이용할 수 없고, 보통 아래 예와 같이 불편하게 직접 bit 연산을 통해 얻는 방법이 주로 사용된다.

unsigned char buf[] = {0x47, 0x40, 0x10, 0x1C};

printf("sync_byte: 0x%X\n", buf[0]);
printf("transport_error_indicator: %d\n", (buf[1] >> 7) & 1);
printf("payload_unit_start_indicator: %d\n", (buf[1] >> 6) & 1);
printf("transport_priority: %d\n", (buf[1] >> 5) & 1);
printf("PID: 0x%X\n", ((unsigned short)(buf[1] << 8) | buf[2]) & 0x1FFF);
printf("transport_scrambling_control: %d\n", (buf[3] >> 6) & 0x03);
printf("adaptation_field_control: %d\n", (buf[3] >> 4) & 0x03);
printf("continuity_counter: %d\n\n", buf[3] & 0x0F);

물론 실행 결과는 기대대로 나오지만 코드가 복잡해졌다.

Little endian에서 구조체 엑세스 방법

이를 위한 방법을 일단 구글링으로는 찾을 수 없었다. 그래서 직접 구현해 보기로 하였다. 🤔
Little endian에서도 구조체를 이용하여 바로 엑세스하게 하려면 각 데이터 타입에서 저장된 순서를 swap 시켜야 한다. 또, 구조체 정의시 bit field로 정의한 부분들도 endian 규칙을 따라서 저장되므로 (즉, little endian 시스템에서는 앞의 bit field가 메모리의 뒤에서부터 저장됨), 이런 bit field로 선언된 부분들도 swap 시켜주면 될 것이다.
메모리 swap을 하는 함수와 bit field 위치 swap을 구조체 정의시에 구현해 보았다. (자동화된 방법은 못 찾았음 😥)

메모리 swap

아래와 같이 swap_structure_mem() 함수를 구현하였다. 이 함수는 메모리 주소와 메모리의 데이터 길이를 입력받고, 가변 인수로 구조체에서의 각 데이터 타입의 길이를 입력받아서, 데이터 타입의 길이가 2바이트 이상인 경우에는 거꾸로 swap 시켜준다. swap_bytes() 함수는 입력 길이만큼의 데이터를 메모리에서 swap 시켜준다.

void swap_structure_mem(void *mem, size_t total_size, ...)
{
    va_list ap;
    int align_size, offset = 0;
    unsigned char *ptr = (unsigned char *)mem;

    va_start(ap, total_size);
    for (size_t i = 0; i < total_size, offset < total_size; i++)
    {
        align_size = va_arg(ap, int);
        if (align_size <= 0 || align_size > 4)
        {
            break;
        }
        swap_bytes(ptr, align_size);
        ptr += align_size;
        offset += align_size;
    }
    va_end(ap);
}

void swap_bytes(void *mem, int size)
{
    unsigned char *start, *end, swap;

    for (start = (unsigned char *)mem, end = start + size - 1; start < end; ++start, --end)
    {
        swap = *start;
        *start = *end;
        *end = swap;
    }
}

구조체에서 bit field 변수 swap

위의 TS 헤더 구조체 예에서 bit field인 경우에는 swap을 시켜준다. (GCC가 지원하는 __BYTE_ORDER__ 매크로 이용)

typedef struct __attribute__((__packed__))
{
    uint8_t sync_byte;
#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
    uint16_t transport_error_indicator : 1;
    uint16_t payload_unit_start_indicator : 1;
    uint16_t transport_priority : 1;
    uint16_t PID : 13;
#else
    uint16_t PID : 13;
    uint16_t transport_priority : 1;
    uint16_t payload_unit_start_indicator : 1;
    uint16_t transport_error_indicator : 1;
#endif
#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
    uint8_t transport_scrambling_control : 2;
    uint8_t adaptation_field_control : 2;
    uint8_t continuity_counter : 4;
#else
    uint8_t continuity_counter : 4;
    uint8_t adaptation_field_control : 2;
    uint8_t transport_scrambling_control : 2;
#endif
} TsHeader;

메모리 swap 후 엑세스

이제 아래와 같이 TS 데이터를 swap_structure_mem() 함수를 이용하여 swap 시킨 후, 구조체 멤버를 엑세스하면 올바른 값이 얻어진다.

TsHeader *header = (TsHeader *)buf;
#if __BYTE_ORDER__ != __ORDER_BIG_ENDIAN__
swap_structure_mem(buf, sizeof(TsHeader), 1, 2, 1);
#endif
printf("sync_byte: 0x%X\n", header->sync_byte);
printf("transport_error_indicator: %d\n", header->transport_error_indicator);
printf("payload_unit_start_indicator: %d\n", header->payload_unit_start_indicator);
printf("transport_priority: %d\n", header->transport_priority);
printf("PID: 0x%X\n", header->PID);
printf("transport_scrambling_control: %d\n", header->transport_scrambling_control);
printf("adaptation_field_control: %d\n", header->adaptation_field_control);
printf("continuity_counter: %d\n", header->continuity_counter);

카테고리:

업데이트: