【C言語】hexdump(16進ダンプ)を自作する

このページでは、C言語での hexdump の自作の仕方について解説していきます。

hexdump は Mac や Linux 等に用意されているコマンドの1つで、指定されたファイルを1バイトずつ16進数で表示するものになります。

最終的に紹介するソースコードのプログラムを実行することで、下記のような出力を得ることができるようになります(スマホだとズレるかも…)。

% ./myhexdump text.txt
496E2074686520796561722031383738 In the year 1878
204920746F6F6B206D79206465677265  I took my degre
65206F6620446F63746F72206F66204D e of Doctor of M
65646963696E65206F66207468652055 edicine of the U
6E6976657273697479206F66204C6F6E niversity of Lon
646F6E2C20616E642070726F63656564 don, and proceed
656420746F204E65746C657920746F20 ed to Netley to 
676F207468726F756768207468652063 go through the c
6F757273652070726573637269626564 ourse prescribed
20666F722073757267656F6E7320696E  for surgeons in
207468652061726D792E0A            the army..

左側に表示されているのがファイルを1バイトずつ16進数で表示した結果になります。右側に表示されているのが、その16進数を文字コードとする文字で表示した結果となります。

作成するhexdumpの表示結果の説明図

hexdump コマンドでは16進数だけでなく色んな進数に対応しているのですが、今回は16進数のみに対応するようにしていきたいと思います。

まずは、上記出力結果における左側の16進数のみの表示を行うプログラムを作成し、続いて、それに加えて右側の文字の表示も行えるようプログラムの変更を行なっていく流れで hexdump の自作を行なっていきたいと思います。

hexdump の自作

それでは、hexdump を自作していきたいと思います!

まずは、文字表示なしバージョン、つまりファイルの中身を1バイトずつ16進数表示するだけのhexdump を作成していきたいと思います。

hexdump の自作方法

単に16進数で表示するだけであれば、hexdump は簡単に実現できます。

処理の流れ

まず、1行に表示する16進数の個数は LINE_NUM としたいと思います。

LINE_NUMの説明図

処理としては、まずファイルをfopen で開き、開いたファイルから LINE_NUM バイトのデータを読み込み、そのデータを1バイトずつ横に並べる形で16進数として出力してやれば良いだけです。

ファイルを1バイトずつ16進数表示する処理の流れ

そして、LINE_NUM バイトのデータを全て16進数で出力した際には改行を行い、その後再び LINE_NUM バイトのデータの読み込みと出力を行います。以降、同様の処理をファイルの最後まで繰り返してやれば、ファイルのデータを全て1バイトずつ16進数で表示することができることになります。

データの読み込み

補足しておくと、ファイルから特定のバイト数のみデータを読み込む際には fread が利用できます。

fread

#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t ntimes, FILE *stream);

例えば下記のように fread 関数を実行すれば、開いたファイル fi から 1 バイトのデータが LINE_NUM 個読み込まれて配列 bytes の先頭から順に格納されます。

freadの実行例

size_t read_size = fread(bytes, 1, LINE_NUM, fi);

1 バイトのデータが LINE_NUM 個読み込まれるので、LINE_NUM バイトのデータがファイルから読み込まれることになります。

また、実際にファイルから読み込まれたデータの個数は fread から返却値として返却されることになります。

この返却値は基本的には第3引数で指定する LINE_NUM となるのですが、ファイルの最後まで読み込んだ際には、LINE_NUM 個のデータを読み込むことができず、LINE_NUM よりも小さい値が返却されることになります。

そのため、fread の返却値が LINE_NUM の値よりも小さくなった場合は、ファイルの最後まで読み込みが完了したと判断することができます。

16進数の出力

また、16進数で出力する際には、printf のフォーマット指定子に %X を指定します(%x でも良い)。

ただ、%X だけだと16進数の桁数がバラバラになってしまい、バイト単位の切れ目が分かりにくくなってしまいます。そのため、単に %X と指定するのではなく %02X と指定し、16進数が1桁の場合でも2桁で出力されるようにした方が良いと思います。

さらに、読み込んだデータは符号なしのデータとして扱うようにします(fread の第1引数に指定する配列の型を unsigned charにしておく)

こうすれば、1バイトずつ16進数で出力するわけですから、出力される16進数がとりうる範囲は 0x000xFF であり、必ず2桁以内の値となります。

なので、%02X と指定しておけば、必ず16進数は2桁で出力することができることになります。

スポンサーリンク

hexdump のソースコード

文字表示なしの hexdump のソースコードは下記のようになります。

LINE_NUM16 として定義していますので、1行に16バイト分の16進数が表示されます(1バイトは2桁の16進数で表示されます)。

hexdump(文字表示なし)

#include <stdio.h>

#define LINE_NUM 16

int main(int argc, char *argv[]) {

    unsigned char bytes[LINE_NUM];
    FILE *fi;
    int i;
    size_t read_size;

    if (argc != 2) {
        printf("ダンプするファイルのパスを引数で指定してください\n");
        return 0;
    }

    fi = fopen(argv[1], "rb");
    if (fi == NULL) {
        printf("%sが開けません\n", argv[1]);
        return -1;
    }

    /* ファイルから全て読み込むまでループ */
    do {
        /* LINE_NUMバイトのデータをファイルから読み込み */
        read_size = fread(bytes, 1, LINE_NUM, fi);
        
        /* 読み込んだデータを1バイトずつ16進数で出力 */
        for (i = 0; i < read_size; i++) {
            printf("%02X", bytes[i]);
        }

        if (read_size < LINE_NUM) {
            /* LINE_NUMバイトのデータが読み込めなかった場合 */
            for (i = 0; i < LINE_NUM - read_size; i++) {
                /* 足りない分だけスペースを出力してレイアウトを整える */
                printf("  ");
            }
        }

        printf("\n");

    } while(read_size == LINE_NUM);

    fclose(fi);
}

コンパイルして実行すれば、引数に指定したファイルを1バイトずつ16進表示した結果が得られます。

% ./hexdump text.txt
496E2074686520796561722031383738
204920746F6F6B206D79206465677265
65206F6620446F63746F72206F66204D
65646963696E65206F66207468652055
6E6976657273697479206F66204C6F6E
646F6E2C20616E642070726F63656564
656420746F204E65746C657920746F20
676F207468726F756768207468652063
6F757273652070726573637269626564
20666F722073757267656F6E7320696E
207468652061726D792E0A 

基本的に行っていることは hexdump の自作方法 で解説した内容の通りです。

ただ、fread 関数で読み込めたバイト数が LINE_NUM よりも少ない場合は(つまりファイルの最後までデータを読み込み終わった場合)、読み込んだバイト数が LINE_NUM に足りない分、16進数ではなく2つのスペース "  " を出力するようにしています。

これは、各行で表示される桁数を LINE_NUM * 2 に揃えるためであり、もっと言えば次に行う文字表示を行う際にレイアウトが崩れないようにするためです。

ですので、実は16進数の表示を行うだけであれば、下記の処理はなくても問題ありません。

文字表示のためのレイアウト調整

if (read_size < LINE_NUM) {
    /* LINE_NUMバイトのデータが読み込めなかった場合 */
    for (i = 0; i < LINE_NUM - read_size; i++) {
         /* 足りない分だけスペースを出力してレイアウトを整える */
         printf("  ");
    }
}

また、16進数表示を行うファイルのパスはコマンドライン引数で受け取るようにしています。

コマンドライン引数については下記ページで解説していますので、詳細な説明を読みたい方は下記ページをご参照いただければと思います。

C言語でのコマンドライン引数の扱い方の解説ページアイキャッチ【C言語】コマンドライン引数の扱い方(VSCodeでの引数指定方法も解説)

hexdump の自作(文字表示あり)

続いて、hexdump の自作 で開発した hexdump に文字表示機能を実装していきたいと思います。

hexdump の自作方法(文字表示あり)

hexdump の自作 で開発した hexdump では、ファイルから読み込んだデータを LINE_NUM バイト分ずつ横に並べて16進数表示するようにしています。

次は、その LINE_NUM バイト分の16進数の横に、LINE_NUM バイト分の文字を表示するようにしていきます。

文字を出力する位置の説明図

処理の流れ

fread 関数でのデータの読み込みにより配列 bytesLINE_NUM バイト分のデータが格納されているわけですから、LINE_NUM バイト分の16進数の表示を行なった後に、配列 bytes のデータを先頭から順に1バイトずつ LINE_NUM 個分の文字として出力してやれば、上の図のような文字表示を実現することができます。

文字の出力

ただ、16進数表示の時とは異なり、文字としてデータを表示するわけですから、printf のフォーマット指定子には %c を指定する必要があります。

printf では、同じデータであってもフォーマット指定子の指定によって異なる形式で出力を行うことができます。

また、単純に文字として printf で出力を行なった場合、その文字が改行文字の場合は変に改行が入ってしまって表示結果のレイアウトが崩れてしまいます。タブ文字などの場合も同様に表示結果のレイアウトが崩れてしまいます。

さらに、読み込み先のファイルがテキストファイルでない場合や、テキストファイルであっても全角文字がある場合などは、読み込んだデータによっては printf で出力を行なった際に文字化けが発生する可能性があります。

全ての文字をそのまま出力すると文字化けや改行が発生してしまう様子

このため、今回は出力する文字が “印字可能な文字 or スペース” である場合のみprintf で出力を行い、それ以外の場合は printf. を出力するようにしたいと思います。

下記ページで解説していますが、文字が “印字可能な文字 or スペース” であるかどうかは isprint 関数により調べることができます。

文字の種類を判別する関数の紹介ページアイキャッチ【C言語】文字の種類を判別する関数(isalnum・isspace・isprintなど)

スポンサーリンク

hexdump のソースコード(文字表示あり)

文字表示ありの hexdump のソースコードは下記のようになります。

hexdump(文字表示あり)

#include <stdio.h>
#include <ctype.h>

#define LINE_NUM 16

int main(int argc, char *argv[]) {

    unsigned char bytes[LINE_NUM];
    FILE *fi;
    int i;
    size_t read_size;

    if (argc != 2) {
        printf("ダンプするファイルのパスを引数で指定してください\n");
        return 0;
    }

    fi = fopen(argv[1], "rb");
    if (fi == NULL) {
        printf("%sが開けません\n", argv[1]);
        return -1;
    }

    /* ファイルから全て読み込むまでループ */
    do {
        /* LINE_NUMバイトのデータをファイルから読み込み */
        read_size = fread(bytes, 1, LINE_NUM, fi);
        
        /* 読み込んだデータを1バイトずつ16進数で出力 */
        for (i = 0; i < read_size; i++) {
            printf("%02X", bytes[i]);
        }

        if (read_size < LINE_NUM) {
            /* LINE_NUMバイトのデータが読み込めなかった場合 */
            for (i = 0; i < LINE_NUM - read_size; i++) {
                /* 足りない分だけスペースを出力してレイアウトを整える */
                printf("  ");
            }
        }

        printf(" ");

        /* 読み込んだデータを1バイトずつ文字で出力 */
        for (i = 0; i < read_size; i++) {
            if (isprint(bytes[i])) {
                /* 印字可能な文字の場合はそのまま出力 */
                printf("%c", bytes[i]);
            } else {
                /* 印字可能でない場合は.を出力 */
                printf(".");
            }
        }

        printf("\n");

    } while(read_size == LINE_NUM);

    fclose(fi);
}

hexdump のソースコード からの変更点は ctype.h のインクルードの追加と、下記の処理の追加のみとなります。

文字の出力

/* 読み込んだデータを1バイトずつ文字で出力 */
for (i = 0; i < read_size; i++) {
    if (isprint(bytes[i])) {
        /* 印字可能な文字の場合はそのまま出力 */
        printf("%c", bytes[i]);
    } else {
        /* 印字可能でない場合は.を出力 */
        printf(".");
    }
}

で、この追加した処理の中で、hexdump の自作方法(文字表示あり) で解説した処理を行なっています。

もし、isprint 関数を利用せずに全てのデータを printf("%c", bytes[i]) で出力した場合は、改行が含まれるファイルの場合は下記のような出力になってレイアウトが崩れてしまいます。

% myhexdump text.txt
496E2074686520796561722031383738 In the year 1878
204920746F6F6B206D79206465677265  I took my degre
65206F6620446F63746F720A6F66204D e of Doctor
of M
65646963696E65206F66207468652055 edicine of the U
6E6976657273697479206F66204C6F6E niversity of Lon
646F6E2C0A616E642070726F63656564 don,
and proceed
656420746F204E65746C657920746F20 ed to Netley to 
676F207468726F756768207468652063 go through the c
6F757273650A70726573637269626564 ourse
prescribed
20666F722073757267656F6E7320696E  for surgeons in
207468652061726D792E0A            the army.

ですが、上記のように isprint 関数を利用して文字が “印字可能な文字 or スペース” であるかどうかを判別し、その結果に応じて処理を切り替えるようにすることで、改行が含まれるファイルの場合でも下記のようにレイアウトを崩さずに文字の表示を行うことができます。

496E2074686520796561722031383738 In the year 1878
204920746F6F6B206D79206465677265  I took my degre
65206F6620446F63746F720A6F66204D e of Doctor.of M
65646963696E65206F66207468652055 edicine of the U
6E6976657273697479206F66204C6F6E niversity of Lon
646F6E2C0A616E642070726F63656564 don,.and proceed
656420746F204E65746C657920746F20 ed to Netley to 
676F207468726F756768207468652063 go through the c
6F757273650A70726573637269626564 ourse.prescribed
20666F722073757267656F6E7320696E  for surgeons in
207468652061726D792E0A            the army..

特にバイナリデータを文字として出力する場合、そのまま出力するとレイアウトが崩れたり文字化けしたりしてしまいます。

それを防ぐために、下記ページで紹介している isprint 関数などの文字を判別する関数が活躍しますので、これらの関数の存在は覚えておくと良いと思います。

文字の種類を判別する関数の紹介ページアイキャッチ【C言語】文字の種類を判別する関数(isalnum・isspace・isprintなど)

まとめ

このページでは、C言語での hexdump の自作の仕方について解説しました!

割と単純な処理で自作することが可能ですが、バイナリデータの扱いに慣れることもできますし、同じデータでも printf のフォーマット指定子によって出力形式の変更が可能であることを実感できる例だと思います。

また、今回は hexdump の自作について解説しましたが、他のコマンドを自作してみることでプログラミングの力を伸ばすこともできると思います!

オススメの参考書

C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!

まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。

  • 参考書によって、解説の仕方は異なる
  • 読み手によって、理解しやすい解説の仕方は異なる

ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?

それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。

なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。

特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。

もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!

入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示