C でバイナリ データを読み取って解析する方法に関するライブラリやガイドはありますか?
ネットワークソケットでTCPパケットを受信し、仕様に従ってそのバイナリデータを解析し、コードによって情報をより使いやすい形式に変換する機能を検討しています。
これを行うライブラリ、またはこの種のことを実行するための入門書はありますか?
C でバイナリ データを読み取って解析する方法に関するライブラリやガイドはありますか?
ネットワークソケットでTCPパケットを受信し、仕様に従ってそのバイナリデータを解析し、コードによって情報をより使いやすい形式に変換する機能を検討しています。
これを行うライブラリ、またはこの種のことを実行するための入門書はありますか?
ここでの多くの回答には同意できません。入力データに構造体をキャストする誘惑を避けることを強くお勧めします。説得力があり、現在のターゲットでも機能する可能性がありますが、コードが別のターゲット/環境/コンパイラに移植されると、問題が発生します。いくつかの理由:
エンディアン: 現在使用しているアーキテクチャはビッグ エンディアンかもしれませんが、次のターゲットはリトル エンディアンかもしれません。またはその逆。これはマクロ (ntoh や hton など) で解決できますが、これは余分な作業であり、フィールドを参照するたびにそれらのマクロを呼び出すようにする必要があります。
アライメント: 使用しているアーキテクチャは、奇数アドレスのオフセットでマルチバイト ワードをロードできる場合がありますが、多くのアーキテクチャではできません。4 バイトのワードが 4 バイトのアラインメント境界にまたがる場合、ロードによってガベージがプルされる可能性があります。プロトコル自体にミスアラインされたワードがなくても、バイト ストリーム自体がミスアラインされている場合があります。(たとえば、IP ヘッダー定義ではすべての 4 バイト ワードが 4 バイト境界に置かれますが、多くの場合、イーサネット ヘッダーは IP ヘッダー自体を 2 バイト境界にプッシュします。)
パディング: コンパイラは、構造体をパディングなしで密にパックすることを選択する場合や、ターゲットのアラインメント制約に対処するためにパディングを挿入する場合があります。同じコンパイラの 2 つのバージョン間でこの変更が見られました。#pragmas を使用して問題を強制することもできますが、#pragmas はもちろんコンパイラ固有です。
ビットの順序 : C ビットフィールド内のビットの順序は、コンパイラ固有です。さらに、ランタイム コードのビットを「取得」するのは困難です。構造体内のビットフィールドを参照するたびに、コンパイラは一連のマスク/シフト操作を使用する必要があります。もちろん、ある時点でそのマスキング/シフトを行う必要がありますが、速度が懸念される場合は、すべての参照でそれを行うのは最善ではありません。(スペースが最も重要な問題である場合は、ビットフィールドを使用しますが、慎重に検討してください。)
これはすべて、「構造体を使用しないでください」と言っているわけではありません。私のお気に入りのアプローチは、関連するすべてのプロトコル データの使いやすいネイティブ エンディアン構造体をビットフィールドなしで問題を気にせずに宣言し、その構造体を仲介者として使用する一連の対称的なパック/解析ルーチンを作成することです。
typedef struct _MyProtocolData
{
Bool myBitA; // Using a "Bool" type wastes a lot of space, but it's fast.
Bool myBitB;
Word32 myWord; // You have a list of base types like Word32, right?
} MyProtocolData;
Void myProtocolParse(const Byte *pProtocol, MyProtocolData *pData)
{
// Somewhere, your code has to pick out the bits. Best to just do it one place.
pData->myBitA = *(pProtocol + MY_BITS_OFFSET) & MY_BIT_A_MASK >> MY_BIT_A_SHIFT;
pData->myBitB = *(pProtocol + MY_BITS_OFFSET) & MY_BIT_B_MASK >> MY_BIT_B_SHIFT;
// Endianness and Alignment issues go away when you fetch byte-at-a-time.
// Here, I'm assuming the protocol is big-endian.
// You could also write a library of "word fetchers" for different sizes and endiannesses.
pData->myWord = *(pProtocol + MY_WORD_OFFSET + 0) << 24;
pData->myWord += *(pProtocol + MY_WORD_OFFSET + 1) << 16;
pData->myWord += *(pProtocol + MY_WORD_OFFSET + 2) << 8;
pData->myWord += *(pProtocol + MY_WORD_OFFSET + 3);
// You could return something useful, like the end of the protocol or an error code.
}
Void myProtocolPack(const MyProtocolData *pData, Byte *pProtocol)
{
// Exercise for the reader! :)
}
これで、残りのコードは使いやすく高速な構造体オブジェクト内のデータを操作し、バイト ストリームとのインターフェイスが必要な場合にのみ pack/parse を呼び出します。ntoh や hton は必要なく、コードを遅くするビットフィールドもありません。
C/C++ でこれを行う標準的な方法は、「gwaredd」が示唆するように構造体に実際にキャストすることです。
人が考えるほど危険ではありません。彼/彼女の例のように、最初に期待した構造体にキャストし、次にその構造体の有効性をテストします。最大/最小値、終了シーケンスなどをテストする必要があります。
どのプラットフォームでも、Unix Network Programming, Volume 1: The Sockets Networking APIを読む必要があります。買う、借りる、盗む(被害者は理解するだろう、食べ物か何かを盗むようなものだ...)が、読んでください。
スティーブンスを読んだ後、これらのほとんどはもっと理にかなっています.
私が正しく理解しているかどうかを確認するために、あなたの質問をもう一度述べさせてください。パケットの正式な説明を取得し、そのようなパケットを解析する「デコーダ」を生成するソフトウェアを探していますか?
その場合、そのフィールドの参照はPADSです。それを紹介する良い記事はPADS: A Domain-Specific Language for Processing Ad Hoc Dataです。PADS は非常に完成度の高いものですが、残念ながらフリーではないライセンスの下にあります。
可能な代替手段があります(C以外のソリューションについては言及しませんでした)。どうやら、完全に本番環境に対応していると見なすことはできません。
フランス語を読めば、これらの問題をGénération de décodeurs de formats binairesにまとめました。
私の経験では、最初に一連のプリミティブを書き込んで、バイナリ バッファから何らかの型の単一の値を読み書きするのが最善の方法です。これにより、視認性が高くなり、エンディアンの問題を処理するための非常に簡単な方法が得られます。関数を正しく実行するだけです。
struct
次に、たとえば、プロトコル メッセージごとに を定義し、それぞれに対してパック/アンパック (シリアライズ/デシリアライズと呼ぶ人もいます) 関数を記述できます。
基本的なケースとして、単一の 8 ビット整数を抽出するためのプリミティブは次のようになります (char
ホスト マシンが 8 ビットであると仮定すると、必要に応じてカスタム型のレイヤーを追加して、それを保証することもできます)。
const void * read_uint8(const void *buffer, unsigned char *value)
{
const unsigned char *vptr = buffer;
*value = *buffer++;
return buffer;
}
ここでは、参照によって値を返し、更新されたポインタを返すことにしました。これは好みの問題です。もちろん、値を返し、参照によってポインターを更新することもできます。これらを連鎖可能にするために、読み取り関数がポインターを更新することは、設計の重要な部分です。
これで、16 ビットの符号なし数量を読み取る同様の関数を作成できます。
const void * read_uint16(const void *buffer, unsigned short *value)
{
unsigned char lo, hi;
buffer = read_uint8(buffer, &hi);
buffer = read_uint8(buffer, &lo);
*value = (hi << 8) | lo;
return buffer;
}
ここでは、着信データがビッグエンディアンであると仮定しました。これはネットワーク プロトコルでは一般的です (主に歴史的な理由から)。もちろん、賢くなってポインター演算を実行し、一時的な必要性を取り除くこともできますが、この方法の方が明確で理解しやすいと思います。この種のプリミティブで最大の透過性を持つことは、デバッグ時に良いことです。
次のステップは、プロトコル固有のメッセージの定義を開始し、一致する読み取り/書き込みプリミティブを書き込むことです。そのレベルでは、コード生成について考えてください。プロトコルが一般的な機械可読形式で記述されている場合は、そこから読み取り/書き込み関数を生成できます。これにより、多くの手間が省けます。プロトコル形式が十分に賢い場合、これは難しくなりますが、多くの場合実行可能であり、強く推奨されます。
基本的にシリアル化フレームワークであるGoogle Protocol Buffersに興味があるかもしれません。主に C++/Java/Python (Google がサポートする言語) 向けですが、Cを含む他の言語に移植する取り組みが進行中です。(私は C ポートをまったく使用していませんが、C# ポートの 1 つを担当しています。)
C でバイナリ データを解析する必要は実際にはありません。あるべきだと思うものにポインタをキャストするだけです。
struct SomeDataFormat
{
....
}
SomeDataFormat* pParsedData = (SomeDataFormat*) pBuffer;
エンディアンの問題、型サイズ、バッファの終わりの読み取りなどに注意してください。
バイナリ構造の解析/フォーマットは、高レベル/マネージ言語よりも C の方が簡単に行える数少ない処理の 1 つです。処理したいフォーマットに対応する構造体を定義するだけで、その構造体がパーサー/フォーマッターになります。これが機能するのは、C の構造体が正確なメモリ レイアウトを表すためです (もちろん、既にバイナリになっています)。kervin と gwaredd の返信も参照してください。
基本的には動作するようにキャストすることに関する提案ですstruct
が、異なるアーキテクチャでは数値が異なる方法で表現される可能性があることに注意してください。
エンディアンの問題に対処するために、ネットワーク バイト オーダーが導入されました。一般的な方法は、データを送信する前にホスト バイト オーダーからネットワーク バイト オーダーに数値を変換し、受信時にホスト オーダーに戻すことです。関数htonl
、htons
、ntohl
およびを参照してくださいntohs
。
そして、kervin のアドバイスを本当に考慮してください - UNPを読んでください。あなたはそれを後悔しません!
あなたが探しているライブラリの種類がよくわかりません。バイナリ入力を受け取り、それを未知の形式に解析する汎用ライブラリ? そのようなライブラリがどの言語にも存在できるかどうかはわかりません。質問を少し詳しく説明する必要があると思います。
編集:わかりました。ジョンの
答え
を読んだ後、ライブラリがあるようです。まあ、ライブラリの種類はコード生成ツールのようなものです。しかし、多くの人がデータを適切なデータ構造にキャストするだけであると述べているように、適切な注意を払って、つまりパックされた構造を使用し、エンディアンの問題に対処することは良いことです。このようなツールを C で使用するのはやり過ぎです。