:ASN.1語法轉(zhuǎn)C代碼編譯器使用指南)
簡介asn1c工具 v0.9.29是一套能夠?qū)SN.1規(guī)范自動轉(zhuǎn)換為C語言代碼的編譯器資源面向通信協(xié)議棧、M2M及設(shè)備管理相關(guān)的嵌入式與網(wǎng)絡(luò)開發(fā)者。包內(nèi)共包含17257個文件以.c/.h源碼、構(gòu)建生成的.o/.lo/.a中間文件、.asn1樣例定義以及測試與構(gòu)建腳本為主并附帶大量可運行的測試程序、示例模塊和Makefile輔助文件壓縮包整體約189.42MB適合在離線環(huán)境下搭建工具鏈并深入學(xué)習(xí)BER/DER/PER等編碼規(guī)則的落地實現(xiàn)。已有1219人學(xué)習(xí)下載。內(nèi)容中既包含configure、libtool、automake等完整構(gòu)建體系也涉及XER、OER、PER、二進(jìn)制編碼等多種編解碼驗證用例讀者可對照源碼理解類型定義、編解碼函數(shù)與內(nèi)存管理的具體實現(xiàn)。通過研究這些文件能夠快速掌握從ASN.1編寫、asn1c編譯到C代碼集成的全流程同時獲得豐富的編碼規(guī)則實例與工具鏈排錯經(jīng)驗。1. asn1c工具 v0.9.29把ASN.1語法翻譯成C代碼的編譯器值不值得用做通信協(xié)議的人遲早會撞上 ASN.1車聯(lián)網(wǎng)、電信網(wǎng)管、智能電網(wǎng)、自動駕駛場景里的消息定義很多都拿它寫規(guī)范。真正動手時才發(fā)現(xiàn)規(guī)范看得懂C 代碼寫起來卻想罵人——幾百個字段的嵌套結(jié)構(gòu)手工匹配字節(jié)流能改到懷疑人生。asn1c 工具 v0.9.29 就是干這個的它把.asn1描述文件直接翻譯成可編譯的 C 語言結(jié)構(gòu)體和編解碼函數(shù)你只需要填業(yè)務(wù)邏輯。這個版本雖然不新但勝在穩(wěn)定、文檔全、踩坑記錄多新手拿來入門不會一頭霧水熟手拿來做量產(chǎn)項目也能控得住邊界。這篇就是我實際用下來的完整路徑從最小命令到參數(shù)取舍再到五個我親自翻過車的坑。2. asn1c工具 v0.9.29干了什么從.asn1到.c/.h的最小閉環(huán)先說清楚一件事asn1c 不是運行時庫也不是協(xié)議棧。它是一個編譯器輸入是 ASN.1 規(guī)范文件輸出是一堆 C 源文件和頭文件。你的代碼鏈接這些生成文件再調(diào)它生成的函數(shù)去完成 BER、PER 等編碼規(guī)則的序列化和反序列化。理解了這個分工后面所有操作都不會跑偏。2.1 最小示例一個消息描述怎么變成C結(jié)構(gòu)體創(chuàng)建一個最簡單的 ASN.1 文件叫message.asn1內(nèi)容如下Message :: SEQUENCE { id INTEGER, content OCTET STRING (SIZE(0..1024)), time UTCTime OPTIONAL }用 asn1c 生成代碼的命令是asn1c -gen-PER message.asn1-gen-PER表示同時生成 PERPacked Encoding Rules的編解碼函數(shù)。如果你不指定默認(rèn)只生成 BER。生成后目錄里會出現(xiàn)Message.c、Message.h以及一大堆以asn_開頭的支持文件。Message.h里會有這樣的結(jié)構(gòu)體typedef struct Message { long id; OCTET_STRING_t content; UTCTime_t *time; /* Context for parsing across buffer boundaries */ asn_struct_ctx_t _asn_ctx; } Message_t;邏輯說明ASN.1 里的INTEGER映射成 C 的longOCTET STRING映射成OCTET_STRING_t結(jié)構(gòu)體OPTIONAL字段映射成指針指針為空表示該字段不存在。_asn_ctx是 asn1c 內(nèi)部做分片解析用的不用管它但千萬別手動改。參數(shù)說明如果你的字段可能超過 32 位整數(shù)范圍INTEGER默認(rèn)映射成long在 64 位機器上是 64 位夠用但若目標(biāo)平臺是 32 位大整數(shù)會截斷后面避坑章我會細(xì)說。2.2 生成的四類文件頭文件、編解碼函數(shù)、PDU定義、構(gòu)建腳本跑完一次生成你得到的不只是Message.c和Message.h。按用途可以分四類第一類是類型定義文件每個頂層類型對應(yīng)一對.c/.h比如Message.c、Message.h。第二類是編解碼函數(shù)文件名字以ber_decoder.c、per_decoder.c、xer_encoder.c等開頭這些是通用框架所有類型共用。第三類是 PDU 定義asn1c 默認(rèn)把 ASN.1 文件里最后一個頂層類型當(dāng)作 PDU生成一個asn1c_use_asn1c之類的入口也可以手動用-pdu指定。第四類是構(gòu)建輔助文件包括Makefile.am、Makefile.in、configure腳本方便你在 Unix 系環(huán)境里直接make。建議你直接去看Message.c里的關(guān)鍵函數(shù)簽名asn_dec_rval_t Message_decode_ber(const asn_codec_ctx_t *opt_codec_ctx, const asn_TYPE_descriptor_t *td, void **struct_ptr, const void *buf, size_t size); asn_enc_rval_t Message_encode_der(const asn_TYPE_descriptor_t *td, const void *sptr, asn_app_consume_bytes_f *callback, void *app_key);邏輯說明每個生成函數(shù)都接收asn_TYPE_descriptor_t *td這是描述類型布局的元數(shù)據(jù)解碼時靠它知道每個字段怎么取struct_ptr是輸出參數(shù)解碼成功后指向新分配的結(jié)構(gòu)體callback是編碼時的字節(jié)輸出回調(diào)。參數(shù)說明opt_codec_ctx可以傳NULL表示不限制棧深度和緩沖區(qū)大小但如果你的消息嵌套很深最好傳一個配置了max_stack_size的上下文防止惡意輸入撐爆棧。2.3 為什么不用更新分支v0.9.29的生態(tài)位我知道有人會問現(xiàn)在 GitHub 上還有更新的分支為什么非要鎖定 v0.9.29我的習(xí)慣是編譯器這種工具穩(wěn)定性大于新功能。v0.9.29 是被最多發(fā)行版打包、被最多項目驗證過的版本。很多車聯(lián)網(wǎng)協(xié)議棧、電力標(biāo)準(zhǔn)示例里的 Makefile 直接寫死了這個版本號說明它的輸出格式與舊項目兼容性最好。新分支雖然修了一些邊角 bug但也改了生成文件的目錄結(jié)構(gòu)舊項目的構(gòu)建腳本大概率要跟著改。如果你不是非得用某個新語法特性v0.9.29 是風(fēng)險最低的選擇。3. 用asn1c工具 v0.9.29生成項目代碼命令、參數(shù)和Makefile配合這一章直接進(jìn)入實操。我把從生成到編譯進(jìn)項目的完整流程拆開每一步都給你能抄的命令。3.1 最基本的生成命令與產(chǎn)物檢查在干凈的目錄里執(zhí)行mkdir asn1_gen cd asn1_gen cp ../message.asn1 . asn1c -gen-PER message.asn1 ls -la你會看到幾十個文件不要慌。先檢查關(guān)鍵文件是否存在ls Message.c Message.h per_encoder.c per_decoder.c如果沒有per_encoder.c說明-gen-PER沒生效或者 asn1c 版本太老。確認(rèn)版本asn1c -v邏輯說明asn1c -v輸出版本號和編譯配置。這里注意v0.9.29 的-v輸出可能混在編譯信息里你搜Version關(guān)鍵字即可。參數(shù)說明-gen-PER同時生成 PER 編碼器和解碼器如果你只需要 BER可以不寫這個參數(shù)但你如果后期想切換最好一開始就生成反正不鏈接對應(yīng)的_encode_der不會增加體積。3.2 五個必調(diào)的生成參數(shù)-gen-PER、-pdu、-fcompound-names、-no-gen-example、-S只跑asn1c message.asn1能用但離項目可用還差得遠(yuǎn)。我一般至少帶這五個參數(shù)asn1c -gen-PER \ -pduMessage \ -fcompound-names \ -no-gen-example \ -S /usr/local/share/asn1c \ message.asn1逐個說明。-pduMessage指定 PDU 入口。asn1c 默認(rèn)把最后一個頂層類型當(dāng) PDU但你的文件里可能有多個頂層類型必須明確指定否則生成代碼里可能出現(xiàn)多個 PDU 入口鏈接時沖突。-fcompound-names把嵌套類型的名字展開比如Message_content而不是簡單的content避免不同結(jié)構(gòu)體里同名成員互相覆蓋。-no-gen-example不生成示例main函數(shù)否則編譯時多出一堆example相關(guān)文件干擾你的構(gòu)建。-S指定支持庫文件路徑。v0.9.29 的asn_application.h等頭文件放在單獨目錄不指定的話編譯器可能在 include 路徑上翻車。這里還有個容易忽視的點-fcompound-names不是可選的。如果你兩個結(jié)構(gòu)體都有l(wèi)ength字段不加這個參數(shù)生成的 C 代碼里兩個結(jié)構(gòu)體共用一個length的命名空間在 C 語言里不沖突但在某些代碼生成器生成的反射代碼里會沖突。加上它字段名變成StructureA_length一勞永逸。3.3 把生成代碼編進(jìn)項目編譯器選項與鏈接細(xì)節(jié)生成完成后的編譯命令我建議直接寫進(jìn) Makefile不要手動敲CC gcc CFLAGS -I. -I/usr/local/share/asn1c -Wall -Wno-unused-function -DASN_PDU_COLLECTION OBJS Message.o Message_encode.o Message_decode.o per_encoder.o per_decoder.o \ asn_application.o asn_system.o asn_codecs_prim.o asn_bit_data.o \ asn_codecs_ber.o asn_codecs_per.o ber_tlv_length.o ber_tlv_tag.o \ constraint.o xer_support.o xer_encoder.o xer_decoder.o LIBS -lm target: $(OBJS) $(CC) -o target $(OBJS) $(LIBS) main.o %.o: %.c $(CC) $(CFLAGS) -c $ -o $邏輯說明asn1c生成的文件里有些.c文件是支持庫并非每個都直接被你的代碼引用但鏈接時編解碼函數(shù)會引用它們所以全量編譯最省心。-DASN_PDU_COLLECTION是 v0.9.29 生成代碼里的一個編譯開關(guān)用來啟用 PDU 注冊表不加可能報未定義。參數(shù)說明-Wno-unused-function屏蔽未使用函數(shù)的告警因為生成代碼里許多支持函數(shù)在簡單消息里用不到但留著也不礙事-lm鏈接數(shù)學(xué)庫asn1c 的實數(shù)類型轉(zhuǎn)換會用到。4. 避坑asn1c工具 v0.9.29的五個典型翻車現(xiàn)場這章是血淚經(jīng)驗。我根據(jù)實際項目里踩過的坑按「現(xiàn)象 → 原因 → 解決」寫五條每條都能對上號。4.1 現(xiàn)象枚舉值超過31位直接編譯報錯現(xiàn)象ASN.1 里定義Color :: ENUMERATED { red(0), blue(1), huge(2147483648) }生成代碼后編譯報錯overflow in enumeration value。原因v0.9.29 對ENUMERATED默認(rèn)映射成 C 的enum而 C 標(biāo)準(zhǔn)里enum底層類型是int最多 31 位有效值符號位占一位。超過INT_MAX的值塞進(jìn)去必然溢出。解決在 ASN.1 文件里給枚舉加-D選項不對。正確做法是避免在枚舉里使用超過INT_MAX的編號或者把那個字段改成INTEGER類型再用條件判斷。如果協(xié)議規(guī)范里已經(jīng)寫死了大編號枚舉那就只能用-fno-constraints之類的參數(shù)強制跳過約束檢查這個參數(shù)不解決類型映射問題。我實際用的招是把枚舉定義拆出來用#define替代#define COLOR_HUGE 2147483648L然后在生成的Color.c里把枚舉映射的地方手動替換成long。前提是你不重新生成代碼。注意每次重新生成都要改一遍最好寫個 patch 腳本。4.2 現(xiàn)象64位機器上的大小端問題現(xiàn)象在 x86 上編碼的 BER 字節(jié)流放到 ARM 板子上解碼數(shù)值字段全反了或者OCTET STRING的字節(jié)序錯亂。原因BER 是網(wǎng)絡(luò)字節(jié)序大端asn1c 的 BER 編解碼器內(nèi)部會做字節(jié)序轉(zhuǎn)換。但 v0.9.29 里某些邊緣路徑比如對INTEGER直接內(nèi)存拷貝時依賴了宿主機的大小端宏。生成代碼里有個asn_system.h它根據(jù)__BYTE_ORDER判斷。如果你的交叉編譯工具鏈沒定義這個宏asn1c 就默認(rèn)走小端邏輯ARM 大端模式下全錯。解決在編譯命令行強制指定CFLAGS -D__BYTE_ORDER__BIG_ENDIAN -D__BIG_ENDIAN1或者更穩(wěn)妥直接在asn_system.h里找到大小端判斷那段手動改成你目標(biāo)平臺的值。這是老版本的通病更新分支修復(fù)了但 v0.9.29 就得靠這個土辦法。4.3 現(xiàn)象對可選成員直接memset清零導(dǎo)致段錯誤現(xiàn)象解碼一個Message_t結(jié)構(gòu)體后想清空重用時直接memset(msg, 0, sizeof(Message_t))然后再次解碼程序在訪問msg-time時段錯誤。原因OPTIONAL字段映射成指針解碼時會為它malloc內(nèi)存。memset清零后指針變NULL第二次解碼時 asn1c 的釋放邏輯檢測到舊指針非NULL試圖釋放一個空指針錯它檢測到NULL會認(rèn)為新字段不存在但結(jié)構(gòu)體里其他內(nèi)部指針比如_asn_ctx里的緩沖區(qū)指針也被清零導(dǎo)致解碼器內(nèi)部狀態(tài)錯亂。解決不要手動 memset 整個結(jié)構(gòu)體用 asn1c 生成的釋放函數(shù)ASN_STRUCT_FREE(asn_DEF_Message, msg); msg NULL;下次解碼前重新calloc一個新結(jié)構(gòu)體。如果實在想復(fù)用內(nèi)存只清業(yè)務(wù)字段msg-id 0; ASN_STRUCT_RESET(asn_DEF_Message, msg);ASN_STRUCT_RESET是 v0.9.29 提供的宏它會遞歸釋放可選成員并重置內(nèi)部狀態(tài)比memset安全得多。4.4 現(xiàn)象-gen-PER和BER解碼結(jié)果不一致現(xiàn)象用-gen-PER生成的編碼函數(shù)輸出字節(jié)流再用 BER 解碼器去解結(jié)果字段缺失或報錯。原因PER 和 BER 是兩套完全不同的編碼規(guī)則字節(jié)布局不同asn1c 為每個類型同時生成了多套編解碼函數(shù)你調(diào)用了錯誤的入口。常見調(diào)錯方式手寫了Message_encode_per但解碼用了Message_decode_ber。這是 API 調(diào)用問題不是工具 bug。解決嚴(yán)格配對。PER 編碼對應(yīng)Message_encode_per解碼對應(yīng)Message_decode_per。v0.9.29 的函數(shù)命名規(guī)律是類型_encode_規(guī)則、類型_decode_規(guī)則。建議在代碼里做一層封裝int my_msg_encode(const Message_t *m, uint8_t *buf, size_t *size) { return per_encode(m, buf, size); } int my_msg_decode(const uint8_t *buf, size_t size, Message_t **m) { return per_decode(buf, size, m); }這樣上下層只認(rèn)你這組函數(shù)不會混。4.5 現(xiàn)象一個ASN.1類型被多個PDU引用導(dǎo)致重復(fù)定義現(xiàn)象A.asn1 里定義Common :: SEQUENCE {...}B.asn1 和 C.asn1 都IMPORTS Common FROM A。把三個文件一起給 asn1c報錯duplicate type name。原因v0.9.29 的默認(rèn)行為是對每個.asn1文件生成一套類型定義它不去重。Common在 B 和 C 的生成代碼里被重復(fù)生成鏈接時符號沖突。解決不要一次傳入多個文件先單獨編譯 A.asn1 生成Common.c/h再編譯 B 和 C 時把Common.h放進(jìn) include 路徑asn1c -gen-PER -pduCommon A.asn1 asn1c -gen-PER -no-gen-example -pduB B.asn1 -I.注意第二個命令里多了-I.讓 B.asn1 導(dǎo)入時能找到 A.asn1 生成的Common.h。如果你用的構(gòu)建系統(tǒng)比較復(fù)雜建議把公共類型放到單獨目錄先生成公共庫再生成業(yè)務(wù)代碼。5. 驗證生成代碼能跑通回環(huán)測試與日志定位代碼生成完第一件事不是寫業(yè)務(wù)邏輯而是驗證生成器本身沒有把你的消息定義搞錯。這步省了后面全是無用功。5.1 用官方自帶example做編碼-解碼回環(huán)v0.9.29 生成代碼時如果不加-no-gen-example會生成一個 example 文件里面有一個main函數(shù)做編解碼回環(huán)。我們完全可以手動寫一個最小回環(huán)測試#include stdio.h #include Message.h int main(void) { Message_t *msg calloc(1, sizeof(Message_t)); msg-id 42; OCTET_STRING_fromBuf(msg-content, hello, 5); uint8_t buf[256]; asn_enc_rval_t er Message_encode_ber(msg, buf, 256); if (er.encoded 0) { printf(encode failed\n); return 1; } Message_t *decoded NULL; asn_dec_rval_t dr Message_decode_ber(NULL, decoded, buf, er.encoded); if (dr.code ! RC_OK) { printf(decode failed\n); return 1; } if (decoded-id ! 42 || decoded-content.size ! 5 || memcmp(decoded-content.buf, hello, 5)) { printf(roundtrip mismatch\n); return 1; } ASN_STRUCT_FREE(asn_DEF_Message, decoded); ASN_STRUCT_FREE(asn_DEF_Message, msg); printf(roundtrip ok\n); return 0; }邏輯說明Message_encode_ber把結(jié)構(gòu)體編碼到棧上buf返回的er.encoded是編碼后的字節(jié)數(shù)Message_decode_ber從 buf 解出新的decoded結(jié)構(gòu)體RC_OK表示成功。參數(shù)說明buf大小 256 只是測試用實際項目建議用動態(tài)緩沖通過回調(diào)方式把編碼結(jié)果直接寫進(jìn)發(fā)送緩沖區(qū)避免固定上限。5.2 給解碼器加日志回調(diào)快速定位失敗字段解碼失敗最痛苦的是不知道錯在哪個字段。v0.9.29 的編解碼器支持設(shè)置日志回調(diào)你可以在生成代碼里找到asn_app_logcallback_t注冊一個函數(shù)把解碼過程中的每一步打印出來static int my_log_cb(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vprintf(fmt, ap); va_end(ap); return 0; } int main(void) { asn_codec_ctx_t ctx {0}; ctx.log_cb my_log_cb; asn_dec_rval_t dr Message_decode_ber(ctx, decoded, buf, len); }邏輯說明asn_codec_ctx_t里的log_cb指針默認(rèn)是NULL不輸出日志。你把它指到自己的函數(shù)后解碼器每解析一個字段都會調(diào)用它輸出類似Decoding Message.id ...的信息。參數(shù)說明這個回調(diào)的格式串和printf一樣你可以自己加前綴。5.3 驗證的邊界BER/PER字節(jié)流怎么比對回環(huán)測試通過只說明編碼器和解碼器是配套的不代表字節(jié)流符合標(biāo)準(zhǔn)。要驗證標(biāo)準(zhǔn)合規(guī)需要和參考實現(xiàn)比對。我常用兩種方式一是拿 Wireshark 的 ASN.1 解析器打開抓包文件看它能不能解出你的字節(jié)流二是找一份公開的樣例字節(jié)流比對前面幾個字節(jié)的 TLV 結(jié)構(gòu)。例如 BER 編碼INTEGER 42應(yīng)該是02 01 2A前兩個字節(jié)是標(biāo)簽和長度2A是值。如果 asn1c 編出來的不是這個說明生成代碼出了問題先檢查-gen-PER是否被誤加。6. 把 asn1c 工具 v0.9.29 嵌入維護(hù)流程一個差分回歸的小技巧到這兒你已經(jīng)能正常生成、編譯、驗證了。但項目迭代后.asn1文件會改asn1c 重新生成的代碼會覆蓋你的手改必須有個機制防回歸。我常用一個差分測試成本極低效果很好。6.1 用兩條命令做差分回歸每次改完.asn1文件做兩步第一步先把舊生成代碼備份第二步新生成后對比關(guān)鍵頭文件。cp Message.h Message.h.bak asn1c -gen-PER -pduMessage -fcompound-names -no-gen-example message.asn1 diff Message.h.bak Message.h邏輯說明如果diff沒有任何輸出說明 ASN.1 描述的字段層面沒變化那你的 C 業(yè)務(wù)代碼可以不動。如果只有結(jié)構(gòu)體順序變化說明只是文件組織調(diào)整。如果字段名或類型變了diff會明確告訴你。這個習(xí)慣能避免大部分「重新生成后程序崩了但不知道改了什么」的尷尬。參數(shù)說明-fcompound-names必須帶上否則每次生成后頭文件里類型名的展開規(guī)則不一致diff會報一堆假差異。6.2 版本鎖定的習(xí)慣與后續(xù)動作v0.9.29 的命令行行為和生成格式已經(jīng)被很多人驗證但不同平臺打包的 asn1c 可能有小差異。我建議在項目的 README 里寫清楚需要的版本號以及生成參數(shù)像這樣ASN1C_VERSION0.9.29 ASN1C_OPTS-gen-PER -pduMessage -fcompound-names -no-gen-example這樣做有兩個好處新人拿到項目能一鍵重建生成代碼老手排查問題時能確認(rèn)當(dāng)前代碼是哪次生成的。比讓后來者瞎猜參數(shù)強得多。最后說個我的壞習(xí)慣早年我經(jīng)常手動修改生成代碼來繞過 bug后來每次重新生成都會把改動沖掉項目在發(fā)布前才發(fā)現(xiàn)功能丟了?,F(xiàn)在我定了一條規(guī)矩生成代碼一律不手改遇到問題先用-S指定自己的支持庫版本或者寫補丁腳本打完再生成。這條路我走了很久才想明白希望幫到你。本文還有配套的精品資源點擊獲取