임베디드 면접 용어 사전
C 언어padding · 정렬

구조체 패딩

멤버를 정렬 경계에 맞추려 컴파일러가 끼워 넣는 빈 바이트. sizeof가 멤버 합보다 커지는 이유다.

CPU가 효율적으로 접근할 수 있도록 구조체 멤버 사이에 컴파일러가 빈 공간을 끼워 넣는 것(padding).

왜 생기는가 — 정렬 요구사항

CPU는 4바이트 값을 4의 배수 주소에서 읽을 때 가장 빠르다

ARM Cortex-M은 정렬되지 않은 접근이

  • Cortex-M0/M0+ : 불가능. HardFault 발생
  • Cortex-M3/M4/M7 : 가능하지만 느리다(여러 번 버스 접근)

이 제약을 지키려고 컴파일러가 빈 공간을 넣는다.

예 — 순서만 바꿔도 크기가 달라진다

struct Bad {          // 12바이트
    uint8_t  a;       // 0        1바이트
                      // 1~3      패딩 3바이트  ← b를 4의 배수에 두려고
    uint32_t b;       // 4~7
    uint8_t  c;       // 8
                      // 9~11     패딩 3바이트  ← 구조체 전체 크기를 4의 배수로
};

struct Good {         // 8바이트
    uint32_t b;       // 0~3
    uint8_t  a;       // 4
    uint8_t  c;       // 5
                      // 6~7      패딩 2바이트
};

멤버를 큰 것부터 배열하면 패딩이 줄어든다. RAM이 수십 KB뿐인 MCU에서 이 구조체가 100개짜리 배열이면 400바이트를 절약하는 셈이다.

끝에도 패딩이 붙는 이유

struct Good arr[2];

배열의 두 번째 원소도 4의 배수 주소에서 시작해야 한다. 그래서 구조체 전체 크기가 가장 큰 멤버의 정렬 단위의 배수가 되도록 꼬리에 패딩을 붙인다.

확인 방법

printf("%zu\n", sizeof(struct Bad));            // 12
printf("%zu\n", offsetof(struct Bad, b));       // 4  ← 패딩 확인
_Static_assert(sizeof(struct Packet) == 8, "크기 불일치");   // 컴파일 시 검증

_Static_assert로 크기를 못 박아 두는 것이 실무에서 유용하다. 누가 멤버를 추가해 크기가 바뀌면 컴파일이 실패한다.

attribute((packed)) — 위험한 해결책

struct __attribute__((packed)) Packet {
    uint8_t  cmd;
    uint32_t value;      // 오프셋 1 — 정렬되지 않았다!
};

패딩을 없애 통신 프로토콜 구조를 그대로 표현할 수 있다. 하지만

struct Packet *p = ...;
uint32_t v = p->value;    // Cortex-M0 에서 HardFault
                          // Cortex-M4 에서는 느린 코드로 컴파일
uint32_t *ptr = &p->value;   // ✗ 정렬 안 된 포인터. 매우 위험

멤버 주소를 다른 함수에 넘기면 그 함수는 정렬되어 있다고 가정하고 접근하다 폴트를 낸다.

안전한 대안 — 직렬화

// packed 구조체를 캐스팅하지 않고, 바이트 단위로 조립한다
void encode(uint8_t *buf, const Packet *p) {
    buf[0] = p->cmd;
    buf[1] = (uint8_t)(p->value      );
    buf[2] = (uint8_t)(p->value >>  8);
    buf[3] = (uint8_t)(p->value >> 16);
    buf[4] = (uint8_t)(p->value >> 24);
}

정렬 문제도 없고 엔디안도 명시적으로 고정된다. 통신 프로토콜에서는 이 방식이 정석이다.

통신·저장에서 구조체를 그대로 쓰면 안 되는 이유

  • ① 패딩이 컴파일러·아키텍처마다 다르다
  • ② 엔디안이 다르면 값이 뒤집힌다
  • ③ 타입 크기가 다를 수 있다 (int 가 16비트인 MCU도 있다)
send(&packet, sizeof(packet));   // ✗ 상대 기기와 레이아웃이 다를 수 있다

같은 코드를 양쪽에서 컴파일하는 경우에만 겨우 통한다. 이기종 통신에서는 반드시 직렬화한다.

비트 필드도 이식성이 없다

struct { uint8_t a : 3; uint8_t b : 5; } flags;

비트 배치 순서(LSB부터인지 MSB부터인지)가 구현 정의다. 하드웨어 레지스터 매핑에 쓰면 컴파일러를 바꿨을 때 깨진다. 비트 마스킹을 쓰는 편이 안전하다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — C 언어 핵심 — 포인터·메모리클래스·const/volatile·비트·struct 패딩