[Quick Summary - For Global Developers]
- Symptom: MCU 간 UART/SPI 통신 또는 통신 모듈 데이터 파싱 시, 데이터 필드가 특정 바이트만큼 뒤로 밀려 수신되거나 Alignment Fault로 인해 시스템이 다운됨.
- Cause: 컴파일러가 32비트 MCU 메모리 접근 효율을 위해 구조체 부전공 데이터 멤버 사이에 무지성으로 구조체 패딩(Struct Padding) 바이트를 자동 삽입하여 물리적 크기가 변경됨.
- Solution: 구조체 선언부에 attribute((packed)) 속성을 명시하여 컴파일러의 최적화 정렬을 강제로 억제하고 데이터 스트림 바이트를 1:1로 정렬함.
구조체 패딩(Struct Padding) 데이터 정렬 불일치로 인한 통신 패킷 바이트 밀림 현상
임베디드 시스템에서 8비트 MCU(예: AVR)와 32비트 MCU(예: STM32 Cortex-M) 간에 UART, SPI, CAN 통신을 하거나 패킷 데이터를 구조체 포인터로 변환(Type Casting)하여 파싱할 때 데이터가 왜곡되는 현상이 발생합니다.
예를 들어, 송신 측에서 분명히 정수형과 문자형 데이터를 순서대로 보냈음에도 불구하고 수신 측 구조체 멤버에는 엉뚱한 값이 채워지거나 값이 한두 바이트씩 밀리는 결함이 관찰됩니다. 심지어 정렬되지 않은 메모리 주소(Unaligned Address)에 접근하여 아키텍처 수준의 하드웨어 예외인 Alignment Fault가 유발되어 MCU가 HardFault_Handler 상태로 진입하기도 합니다. 이 이슈는 코드 로직의 오류가 아닌 컴파일러의 데이터 정렬 메커니즘 차이로 발생합니다.
32비트 하드웨어 메모리 버스 접근 효율과 데이터 정렬(Data Alignment) 원인 분석
근본 원인은 컴파일러의 데이터 정렬(Data Alignment) 및 구조체 패딩(Struct Padding) 아키텍처에 있습니다. ARM Cortex-M과 같은 32비트 RISC 프로세서는 메모리 버스(Data Bus)를 통해 1사이클에 32비트(4바이트) 단위로 데이터를 읽고 씁니다. 만약 4바이트 크기의 변수가 4의 배수가 아닌 주소(예: 0x20000001)에 걸쳐 있다면, CPU는 메모리에 두 번 접근해야 하는 하드웨어 병목이 발생합니다.
이를 방지하기 위해 컴파일러는 각 데이터 타입의 크기에 맞춰 정렬 주소를 강제합니다.
- uint8_t: 1바이트 정렬 (어느 주소든 배치 가능)
- uint16_t: 2바이트 정렬 (2의 배수 주소에 배치)
- uint32_t: 4바이트 정렬 (4의 배수 주소에 배치)
구조체 내부 멤버들을 배치할 때, 컴파일러는 뒤따라오는 데이터 타입이 자기 자신의 크기 배수 주소에 위치하도록 멤버 사이에 더미 바이트(Dummy Byte)인 패딩(Padding)을 강제로 삽입합니다. 이로 인해 개발자가 계산한 순수한 데이터 크기와 컴파일러가 생성한 런타임 구조체의 물리적 메모리 크기(sizeof)가 불일치하게 되며 통신 버퍼에서 바이트 밀림 현상이 발생됩니다.
통신 데이터 밀림을 유발하는 잘못된 구조체 설계 및 포인터 캐싱 C 코드 예시 (Bad Case)
컴파일러 정렬 옵션을 고려하지 않고 통신 패킷 프로토콜을 그대로 구조체로 선언하여 수신 버퍼 포인터에 다이렉트 매핑할 때 문제가 발생하는 전형적인 코드입니다.
#include <stdint.h>
#include <stdio.h>
/* Compiler automatically inserts padding bytes into this structure */
typedef struct {
uint8_t frame_start; /* Offset 0: 1 byte */
/* 3 bytes of padding implicitly inserted here by compiler to align next 32-bit integer */
uint32_t device_id; /* Offset 4: 4 bytes (Requires 4-byte alignment) */
uint16_t command_code; /* Offset 8: 2 bytes (Requires 2-byte alignment) */
uint8_t checksum; /* Offset 10: 1 byte */
/* 1 byte of padding implicitly inserted here to match overall struct 4-byte alignment */
} PacketHeader;
uint8_t rx_buffer[12] = {0x7E, 0x12, 0x34, 0x56, 0x78, 0xAA, 0xBB, 0xCC, 0x00, 0x00, 0x00, 0x00};
void process_packet(void) {
/* Critical bug: Pointer casting over padded structure alters expected offset positions */
PacketHeader *packet = (PacketHeader *)rx_buffer;
/* Expected packet->device_id = 0x12345678, but due to padding it parses wrong bytes */
printf("Device ID: 0x%08X\n", packet->device_id);
printf("Total Struct Size: %d bytes\n", (int)sizeof(PacketHeader));
}
컴파일러 정렬을 제어하는 attribute((packed)) 기반의 방어적 구조체 C 코드 구현법 (Good Case)
구조체 멤버 정렬을 강제로 해제하고, 하드웨어 바이트 스트림과 1:1 매핑되도록 구조체를 압축(Pack)하는 해결 코드입니다.
#include <stdint.h>
#include <stdio.h>
/* Force the compiler to eliminate all padding bytes and assign 1-byte alignment */
typedef struct __attribute__((packed)) {
uint8_t frame_start; /* Offset 0: 1 byte */
uint32_t device_id; /* Offset 1: 4 bytes (Strictly mapped next to frame_start) */
uint16_t command_code; /* Offset 5: 2 bytes */
uint8_t checksum; /* Offset 7: 1 byte */
} PackedPacketHeader;
uint8_t rx_buffer[8] = {0x7E, 0x12, 0x34, 0x56, 0x78, 0xAA, 0xBB, 0xCC};
void process_packed_packet(void) {
/* Safe parsing: Structural mapping perfectly aligns with sequential communication data stream */
PackedPacketHeader *packet = (PackedPacketHeader *)rx_buffer;
/* Correctly reads 0x12345678 from exact byte coordinates */
printf("Packed Device ID: 0x%08X\n", packet->device_id);
printf("Packed Struct Size: %d bytes\n", (int)sizeof(PackedPacketHeader));
}
핵심 수정 포인트 설명
- attribute((packed)) 명시: 구조체 닫는 괄호 뒤 또는 선언부에 속성을 추가하여 GNU GCC/Clang 컴파일러가 구조체 내부에 패딩 바이트를 자동으로 삽입하는 최적화 알고리즘을 방지합니다.
- 1바이트 조밀 정렬 강제: 모든 멤버 변수들이 패딩 없이 메모리 주소 상에 연속적으로 배치되므로 통신 프로토콜 프레임 명세서 문서 파일 규격과 물리 메모리가 정확히 일치합니다.
- 통신 버퍼 매핑 안정화: 시리얼 포트나 SPI 등 하드웨어 레벨에서 바이트 스트림 단위로 들어오는 원시 배열 데이터를 별도의 디코딩 루프 없이 포인터 캐스팅만으로 고속 연산 및 안전한 파싱이 가능해집니다.
구조체 정렬 버그 디버깅 및 트러블슈팅 가이드 (Debugging Tips)
통신 과정에서 바이트 밀림 현상이나 비정상 하드폴트 예외가 의심될 때 적용 가능한 트러블슈팅 가이드라인입니다.
- sizeof 런타임 매크로 검증: 디버거(ST-LINK, J-Link)의 워치 창(Watch Window)에 구조체의 가상 크기 연산 결과인 sizeof(MyStruct) 값을 명시적으로 등록하여 모니터링하십시오. 선언한 변수들의 크기 총합보다 1바이트라도 크다면 100% 구조체 패딩 바이트가 숨겨져 삽입된 상태입니다.
- 맵 파일(MAP File) 주소 오프셋 추적: 툴체인 빌드 결과물인 .map 파일을 구동하여 전역 혹은 정적 구조체 변수의 시작 주소를 분석하십시오. 구조체 내 개별 멤버 변수들의 주소 할당 간격을 비교하면 컴파일러가 어느 분기점에서 2바이트 혹은 4바이트 경계 정렬을 시도했는지 즉각적으로 판독할 수 있습니다.
- Unaligned Access 하드폴트 예외 분석: attribute((packed))를 적용한 구조체의 특정 32비트 멤버 주소를 일반 포인터 변수로 가리킨 후 값을 참조(Dereference)할 때 시스템이 멈추는 경우가 있습니다. 32비트 ARM 프로세서 중 일부 구형 코어(Cortex-M0 등)는 비정렬 주소 접근(Unaligned Access)을 하드웨어 레벨에서 거부하여 UsageFault 또는 HardFault를 발생시킵니다. 따라서 패킹된 구조체 멤버의 주소를 추출하여 외부 포인터로 연산할 때는 주소 오프셋 오정렬 예외가 발생되지 않는지 프로세서 데이터시트를 필히 체크해야 합니다.
'Troubleshooting' 카테고리의 다른 글
| [임베디드 C] volatile 키워드 누락으로 인한 컴파일러 최적화 오류 및 무한 루프 해결 (0) | 2026.06.10 |
|---|---|
| [MCU] 워치독 타이머(Watchdog) 무한 리셋 버그 원인과 메인 루프 구조 개선 방법 (0) | 2026.06.09 |
| ARM Cortex-M 하드폴트(Hard Fault) 디버깅: 레지스터 추적으로 원인 코드 찾는 법 (0) | 2026.06.08 |
| [C언어 임베디드] 버퍼 오버플로우(Buffer Overflow) 에러 예방과 안전한 메모리 복사 (1) | 2026.06.07 |
| [MCU 디버깅] Wild Pointer 및 Dangling Pointer로 인한 시스템 다운 방지 대책 (0) | 2026.06.06 |