C言語基礎|ファイルのダンプ

「見えないデータを“見える化”しよう―バイナリも安心して覗ける、ダンプ入門!」

テキストファイルなら、そのまま画面に表示してもだいたい読めますよね。
でもバイナリファイルはそうはいきません。中身は“文字”ではなくバイト列なので、普通に表示しようとすると文字化けしたり、端末によっては変な制御文字が混ざって困ったりします。

そこで活躍するのが ダンプ(dump)
ファイルの内容を 16進数(hex)表示できる文字(ASCII など) を並べて表示して、「どんなバイトが入っているか」を安全に確認する方法です。
デバッグや解析でめちゃくちゃ頼りになります。

ダンプって何を表示しているの?

図:ダンプ出力の1行のイメージ(16バイト表示)

00000010  48 65 6C 6C 6F 2C 20 43 21 0A 00 FF 10 20 7E 41  Hello, C!... .~A

図の説明

  • 左端:その行の先頭位置(オフセット。ここでは 0x10)
  • 中央:バイト値を 16進数で並べたもの(48 は 'H' など)
  • 右端:表示できる文字はそのまま、表示できないバイトは . に置換

この形式だと、バイナリでも安全に中身を確認できます。

テキスト表示とダンプ表示の違い

普通に表示する vs ダンプする

方法何が見える?バイナリに向く?特徴
文字として表示(fgetc→putchar)文字として解釈できる部分だけ向かない制御文字で崩れることがある
ダンプ表示(fread+hex+文字)バイト列そのもの向く何が入っていても安定表示

表の説明

  • 「読めること」より「壊れずに確認できること」がダンプの価値です。

サンプルプログラム

ここではもっとシンプルにします。

  • まず demo.bin を作る(中にわざと“表示できないバイト”も入れる)
  • その demo.bin をダンプする

サンプル:デモ用バイナリを作ってダンプする

プロジェクト名:chap13-11-1 ソースファイル名:chap13-11-1.c

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

/* 1行に表示するバイト数 */
#define BYTES_PER_LINE 16

/* --- バイナリファイルを作る(デモ用) --- */
static void make_demo_file(const char *filename)
{
    FILE *fp = fopen(filename, "wb");
    if (fp == NULL) {
        printf("ERROR: Cannot create demo file.\n");
        return;
    }

    /* 表示できる文字と、制御文字/バイナリっぽい値を混ぜる */
    unsigned char data[] = {
        'H','e','l','l','o',',',' ','C','!','\n',
        0x00, 0x01, 0x02, 0x7F, 0x80, 0xFF,
        'A','B','C','1','2','3','\r','\n'
    };

    fwrite(data, 1, sizeof(data), fp);
    fclose(fp);
}

/* --- ファイルをダンプ表示する --- */
static void dump_file(const char *filename)
{
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) {
        printf("ERROR: Cannot open input file.\n");
        return;
    }

    unsigned long offset = 0;
    unsigned char buf[BYTES_PER_LINE];
    size_t n;

    printf("Dump start.\n");

    while ((n = fread(buf, 1, BYTES_PER_LINE, fp)) > 0) {
        /* オフセット */
        printf("%08lX  ", offset);

        /* 16進数部 */
        for (size_t i = 0; i < n; i++) {
            printf("%02X ", (unsigned)buf[i]);
        }
        /* 最後の行が16バイト未満なら空白で揃える */
        for (size_t i = n; i < BYTES_PER_LINE; i++) {
            printf("   ");
        }

        printf(" ");

        /* 文字部:表示できないものは '.' */
        for (size_t i = 0; i < n; i++) {
            unsigned char c = buf[i];
            putchar(isprint((int)c) ? c : '.');
        }

        putchar('\n');

        offset += BYTES_PER_LINE;
    }

    fclose(fp);
    printf("Dump end.\n");
}

int main(void)
{
    const char *fname = "demo.bin";

    printf("Creating demo file...\n");
    make_demo_file(fname);

    printf("Dumping demo file...\n");
    dump_file(fname);

    printf("Done.\n");
    return 0;
}

このプログラムで登場する項目をしっかり解説

バイナリとして開く理由(rb)

テキストとして開く r でも読める環境は多いのですが、プラットフォームによっては改行コードの扱いなどで「読み取ったバイト列」が変わる可能性があります。
なので、ダンプは基本 rb が安心です。

モード r と rb の意図

モード意味ダンプでのおすすめ
rテキストとして読む基本避けたい
rbバイナリとして読むこれが安全

表の説明

  • “バイト列をそのまま確認したい”ので、バイナリモードが合っています。

fread で「まとめて読む」理由

1バイトずつ fgetc で読んでもいいのですが、ダンプでは 16バイト単位でまとめて読むと作りやすいです。

fread の書式

項目内容
ヘッダ#include <stdio.h>
形式size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
役割stream から最大 nmemb 個の size バイト要素を ptr へ読み込む
返却値実際に読み取れた要素数

今回の使い方はこうです:

  • size を 1(1バイト)
  • nmemb を 16(16バイト)
  • 返り値 n を「今回読めたバイト数」として扱う
n = fread(buf, 1, 16, fp);

16進数表示のコツ(%02X)

1バイトは 0x00〜0xFF なので、16進で2桁表示にするのが見やすいです。

%02X の意味

指定意味
X16進(大文字)
2幅2桁
0足りない桁は0で埋める

だから 0x0A は A ではなく 0A と表示されます。

isprint で「表示できる文字だけ出す」理由

バイナリには、改行やタブ、ESCなどの制御コードも混ざります。
それをそのまま putchar すると、端末が変な動きをする可能性があります。

そこで isprint を使い、表示できるものだけ文字として出します。

isprint の書式

項目内容
ヘッダ#include <ctype.h>
形式int isprint(int c);
意味c が空白を含む表示可能文字なら真(0以外)
返却値表示可能なら0以外、不可なら0

今回のロジックはこうです:

putchar(isprint((int)c) ? c : '.');

ダンプの出力が「揃っている」理由(空白埋め)

最後の行は 16バイト未満になることがあります。
そのときに16進数の列の幅が崩れると、右側の文字表示がズレて読みにくいです。

だから、足りない分を空白で埋めています。

図:最後の行だけ短い場合のズレ防止

(バイトが少ない) -> 16進数部が短い -> 文字部が左に詰まる -> 見づらい
空白で埋める    -> 文字部の開始位置が一定 -> 見やすい

演習問題

演習13-13:バイナリコピー(fread / fwrite)

ファイルのコピーをバイナリファイルとして行うプログラムを作成せよ。読み書きには fread 関数と fwrite 関数を利用すること。

解答例:16バイト単位でコピー

プロジェクト名:chap13-11-2 ソースファイル名:chap13-11-2.c

#include <stdio.h>

#define BUF_SIZE 1024

int main(void)
{
    FILE *in, *out;
    char src[FILENAME_MAX];
    char dst[FILENAME_MAX];
    unsigned char buf[BUF_SIZE];
    size_t n;

    printf("Source file : ");
    scanf("%s", src);
    printf("Dest file   : ");
    scanf("%s", dst);

    /* 入力ファイルをバイナリで開く */
    in = fopen(src, "rb");
    if (in == NULL) {
        printf("ERROR: Cannot open source file.\n");
        return 1;
    }

    /* 出力ファイルをバイナリで開く */
    out = fopen(dst, "wb");
    if (out == NULL) {
        printf("ERROR: Cannot open destination file.\n");
        fclose(in);
        return 1;
    }

    /* まとめて読み、まとめて書く */
    while ((n = fread(buf, 1, BUF_SIZE, in)) > 0) {
        fwrite(buf, 1, n, out);
    }

    fclose(out);
    fclose(in);

    printf("Copy complete.\n");
    return 0;
}

解説

  • テキストコピー(1文字ずつ)でも動きますが、バイナリはまとめ読みが相性抜群です。
  • fwrite の第3引数は「今回読み取れたバイト数 n」を渡すのがポイントです。最後の塊は BUF_SIZE 未満になるからです。