ARM Cortex-M0/M0+ 處理器採用 ARMv6-M 架構,**不支援非對齊記憶體存取**。任何 16-bit/32-bit 存取若位址未對齊至 2/4 位元組邊界,將觸發 HardFault 硬錯誤異常。本文記錄在微信硬體平台 (基於 nRF51822/Cortex-M0) 二次開發過程中,因 Protobuf 封裝長度變化導致結構體指標非對齊存取,引發間歇性死機的完整排查過程。
+----------------+----------------+------------------+------------------+
| 固定包頭 (8B) | Protobuf 標籤 | BlueDemoHead (8B) | 業務負載 (N B) |
+----------------+----------------+------------------+------------------+
fix_head_len 可變(4~5B) 內部協議頭 實際資料
typedef struct {
uint8_t m_magicCode[2]; // offset 0
uint16_t m_version; // offset 2
uint16_t m_totalLength; // offset 4
uint16_t m_cmdid; // offset 6 ← 關鍵成員
uint16_t m_seq; // offset 8
uint16_t m_errorCode; // offset 10
} BlueDemoHead; // sizeof = 12 bytes (自然對齊)
// 解包流程中:
RecvDataPush *recvDatPush = epb_unpack_recv_data_push(data + fix_head_len, len - fix_head_len);
// recvDatPush->data.data 指向有效資料起始 (BlueDemoHead + 負載)
// 危險操作:直接強轉
BlueDemoHead *bledemohead = (BlueDemoHead *)recvDatPush->data.data;
if (ntohs(bledemohead->m_cmdid) == openLightPush) { ... } // 存取 offset 6
m_cmdid 在結構體內偏移 6 bytes,需 2-byte 對齊bledemohead 位址為 奇數,則 &bledemohead->m_cmdid 為奇數位址 → 非對齊存取 → HardFaultrecvDatPush->data.data 位址 = data 基址 + fix_head_len + protobuf_tag_lendata 基址:0x20004388 (4-byte 對齊 ✓)fix_head_len = 8, protobuf_tag_len = 40x20004394 (2-byte 對齊 ✓)bledemohead 2-byte 對齊 → m_cmdid 位址 0x2000439A 對齊 ✓data 基址:0x20004388 (同前)protobuf_tag_len 變為 5 bytes (Varint 編碼長度增長)0x20004395 (奇數位址 ✗)bledemohead 非 2-byte 對齊 → m_cmdid 位址 0x2000439B 非對齊 → HardFault

m_cmdid (offset 6) 要求 2-byte 對齊| 方案 | 原理 | 優點 | 缺點 |
|---|---|---|---|
| 方案一:逐位元組拷貝 (推薦) | memcpy(&local_struct, src, sizeof(local_struct)) | 編譯器自動生成 byte-wise 存取指令 (LDRB/STRB),完全規避對齊要求;效能損耗極小 | 需額外棧上結構體變數 |
| 方案二:手動位元組組裝 | cmdid = (src[6] << 8) | src[7] | 零額外記憶體、完全可控 | 程式碼冗長、維護成本高 |
| 方案三:`__attribute__((packed))` | 強制結構體 1-byte 對齊 | 語法簡單 | 危險:Cortex-M0 仍會對 packed 結構體的 16/32-bit 成員發出非對齊存取指令 → 依然 HardFault |
| 方案四:重新設計協議頭 | 確保協議頭長度為偶數、或移除結構體強轉 | 從協議層面根治 | 需協調雙端、影響相容性 |
// 安全存取封裝函式
static inline uint16_t read_u16_unaligned(const uint8_t *ptr) {
return (uint16_t)ptr[0] | ((uint16_t)ptr[1] << 8); // 小端假設
}
static inline uint32_t read_u32_unaligned(const uint8_t *ptr) {
return (uint32_t)ptr[0] | ((uint32_t)ptr[1] << 8) | ((uint32_t)ptr[2] << 16) | ((uint32_t)ptr[3] << 24);
}
// 使用範例
const uint8_t *payload = recvDatPush->data.data;
uint16_t cmdid = read_u16_unaligned(payload + 6); // m_cmdid offset 6
if (ntohs(cmdid) == openLightPush) { ... }
| 核心 | 架構 | 非對齊存取支援 | CCR 相關位 |
|---|---|---|---|
| Cortex-M0 / M0+ | ARMv6-M | ❌ 完全不支援 (HardFault) | 無 |
| Cortex-M3 / M4 / M7 / M33 | ARMv7-M / ARMv8-M | ✅ 硬體支援 (效能較慢) | CCR.UNALIGN_TRP (bit 3) 可選擇攔截 |
| Cortex-M23 / M33 (Mainline) | ARMv8-M | ✅ 硬體支援 | CCR.UNALIGN_TRP |
註:Cortex-M3 以上雖支援非對齊存取,但跨 64-bit 邊界或特定指令 (LDM/STM, LDRD/STRD) 仍可能故障。嵌入式開發建議**始終保證自然對齊**。
HardFault_Handler 保存 LR、PSP/MSP、CFSR (Configurable Fault Status Register)CFSR bit 0 (UNALIGNED) = 1 確認為非對齊存取BFAR (Bus Fault Address Register) 取得故障位址arm-none-eabi-addr2line 將 PC 定位至原始碼行號此案例揭示了嵌入式系統中「協議解析邏輯」與「硬體記憶體模型」交互產生的隱性缺陷。**任何涉及外部資料 (網路封包、檔案、Flash) 直接強轉為結構體指標的操作,皆屬高風險行為**。建立「序列化/反序列化」介面層、使用 `memcpy` 或位元組組裝函式,是跨平台、高可靠性固件開發的基本功。