PR

【C言語】fwrite関数のエンディアン変換の理解が乏しくやらかした話

組み込み機器

fwriteで書き込んだファイルをバイナリで見てみよう!

こうした場合の注意点を解説します。

動画でもどうぞ。

立プロ

新卒でメーカーに入り、10年間組み込みの現場で設計を行う。
今は個人事業主として自作の組み込み機器開発や、エージェント様に紹介いただき業務委託を行っています。
C,C#,JavaScript, Vue, PHP, VBA, GAS, Kotlinなど、扱う言語が増えゆく日々。

立プロをフォローする

fwrite関数とは

fwrite関数は、C言語やC++言語などでファイルにバイナリデータを書き込むための標準ライブラリ関数です。
(使用する場合は、stdio.hのインクルードが必要です)

#include <stdio.h>

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
  • ptr: 書き込むデータのポインタ
  • size: 各要素のサイズ(バイト数)
  • count: 書き込む要素の数
  • stream: 書き込み先のファイルポインタ

実際には、配列にデータ入れといて、ファイルに書き込むことが多いと思います。

バイナリファイルとして書き込む

#include <stdio.h>

int main() {
    short int data[] = {10, 20, 30, 40, 50};
    FILE *file = fopen("data.bin", "wb"); // バイナリ書き込みモードでファイルを開く

    if (file != NULL) {
        size_t elements_written = fwrite(data, sizeof(int), 5, file);

        if (elements_written == 5) {
            printf("データが正常に書き込まれました。\n");
        } else {
            printf("書き込みエラーが発生しました。\n");
        }

        fclose(file);
    } else {
        printf("ファイルを開けません。\n");
    }

    return 0;
}

こんな感じでdata配列のデータをバイナリファイルに格納しました。

バイナリファイルの中身を見ると、

0xA000, 0x1400, 0x1E00, 0x2800, 0x3200

となっているのです。

分かりますかね、short int型(=2バイトの符号付整数)の10を16進数で表すと0x000A, 20は0x0014…なのに、バイナリファイルではそうなってないのです。

0x000Aを例にすると、最初の1バイトの00と次の1バイトの0Aが逆転してファイルに保存されているんですよね。

2進数、10進数、16進数の確認はこちらをどうぞ!
https://tatepro.com/bin_dec_hex/

ビッグエンディアン/リトルエンディアン

データを格納するとき、どのように配置するのか主に2種類あります。

ビッグエンディアン、リトルエンディアンと呼ばれます。

ビッグエンディアン
データの最も重要なバイト(通常は左端のバイト)が最初に配置され、最も未使用のバイト(通常は右端のバイト)が最後に配置される方式です。
これは、通常、人間の自然な読み取り順序と一致しています。例えば、通常の十進数表現と同じように、最も重要な桁が左にあります。
元データが0xABCDだったら0xAB, 0xCDのように配置されます。

リトルエンディアン
データの最も未使用のバイト(通常は右端のバイト)が最初に配置され、最も重要なバイト(通常は左端のバイト)が最後に配置される方式です。
これは、バイトオーダーがビッグエンディアンとは逆になる方式で、データの内部表現が一見逆転しているように見えます。
元データが0xABCDだったら0xCD, 0xBAのように配置されます。

つまり、fwrite関数で10は0x000Aなのに0x0A00となったのは、リトルエンディアンで格納されていたから、だったのです!

どちらのエンディアンになるかはCPUのアーキテクチャ次第

fwrite関数を使うとリトルエンディアンで格納されるとは限りません。

どちらのエンディアンになるかは使っているパソコンのCPUアーキテクチャに依存します。

例えば以下の通り。

Intelのx86アーキテクチャ(一般家庭のパソコンのほとんどはこれ)だとリトルエンディアン
IBMのPowerアーキテクチャ(サーバコンピュータやネットワーク機器)だとビッグエンディアン

最近だとARMアーキテクチャが流行ってきており、これはバイエンディアンと呼ばれ、どちらにもなれて、CPU設計者が選択できるようになっています。

なんともややこしい…。

ただ、ネットワークにつなぐことを主目的としている場合はビッグエンディアン、その他の場合はリトルエンディアンとなる傾向があります。

何でやらかしたのか

ここまで読めばお察しかと思いますが、バイナリ変換でリトルエンディアンになっていたことを気づかずビッグエンディアンで値を読んでしまったんですね。

やらかしの元をたどりますと、お客様から提供の特殊な形式のファイルを、これまた特殊な方法でバイナリ変換し、さらにそのバイナリをCSVファイルに変換する、というのが一連の処理だったのです。

予め特殊な方法でバイナリ変換するときにはビッグエンディアンになってるよ、と事前に教えてもらってました。

それでとりあえず特殊形式→バイナリ→CSVと変換して、CSVの中身をグラフ化すると何かおかしい、ということ。

いったんバイナリで見ないとこちらもCSVが正しいのか分からないよね、とバイナリ変換したものをファイルに保存させて中身を見たわけです。

そこで、「あ!これエンディアン逆じゃん!」と分かり、バイナリを自作のエンディアン変換で反転させて、CSV化しました。

結果としてはエンディアン変換してもグラフがあまり変わらず、元データがおかしい可能性もあるから、ということで納品。

そしてそのコードを実機に組み込んで動かしたらデータがめちゃくちゃとのこと。

実機がおかしい可能性があるということだったので、そちらが最初疑われていたのですが、どうも中の処理っぽいということでコード確認の依頼がきました。

その時見せてもらった”おかしい”データは確かにおかしかったのです、連続するはずの値がぶっ飛びまくってました。

たしかに中の処理っぽいと思い、1つ1つ見返したら「バイナリファイル化したときにリトルエンディアンで格納されてるじゃん!!」と分かったのです。

そう、つまり自作のエンディアン変換が不要で、それが原因で値がおかしかったのです。

エンディアン変換処理を抜いたらキレイなデータが取れてました、これは完全に私のミス…やらかしてしまいました。

上司に迷惑かけまくってしまいました…大変申し訳ない。

自分のパソコンがどちらのエンディアンか調べる方法

#include <stdio.h>

int main() {
    unsigned short int num = 1;
    char *ptr = (char *)&num;

    if (*ptr == 1) {
        printf("リトルエンディアン\n");
    } else {
        printf("ビッグエンディアン\n");
    }

    return 0;
}

このように、2バイトの変数(上記例だとnumで0x0001)のアドレスをchar型のptrポインタ変数(=1バイト)に入れてやります。

ptrポインタの中身が1(=0x01)だと、リトルエンディアン、それ以外(と言っても0x00)だったらビッグエンディアンと分かります!

まとめ

ということで、fwriteでバイナリファイルとして保存する場合は、CPUアーキテクチャによってどちらのエンディアンで変換されるか分からないから、事前に確認してから対応しましょう!

付録

2進数だとどうなるか、変換ツールを作ってみました。

コメント

タイトルとURLをコピーしました