C言語でメモリ内のデータをビット単位で表示する方法と構造体の理解

指示: ファイルをメモリにロードして、1バイトごとにメモリアドレス、ビット列、16進値を表示するプログラムを作成せよ。

出力形式:
メモリアドレス: ビット列 | 16進値

C言語でファイルをメモリにロードし、各バイトをメモリアドレスとビット列を表示するプログラムを作成する方法を説明する。 以下はそのサンプルコード。 このコードは、指定したファイルをメモリに読み込み、その各バイトに対してアドレス、ビット列、16進数の値を表示する。

 1#include <stdio.h>
 2#include <stdlib.h>
 3#include <stdint.h>
 4
 5// ビット列を表示する関数
 6void print_bits(uint8_t byte) {
 7    // バイトの各ビットを表示
 8    for (int i = 7; i >= 0; i--) {
 9        // ビットシフトして最上位ビットを取得し、1または0として表示
10        printf("%d", (byte >> i) & 1);
11    }
12}
13
14// メイン関数
15int main(int argc, char *argv[]) {
16    // コマンドライン引数のチェック
17    if (argc != 2) {
18        fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
19        return EXIT_FAILURE;
20    }
21
22    const char *filename = argv[1]; // コマンドライン引数からファイル名を取得
23    FILE *file = fopen(filename, "rb"); // バイナリモードでファイルを開く
24
25    // ファイルオープンエラーの処理
26    if (!file) {
27        perror("Error opening file");
28        return EXIT_FAILURE;
29    }
30
31    // ファイルのサイズを取得するためにファイルの終端に移動
32    fseek(file, 0, SEEK_END);
33    long file_size = ftell(file); // ファイルサイズを取得
34    fseek(file, 0, SEEK_SET); // ファイルポインタを元に戻す
35
36    // ファイルサイズ分のメモリを確保
37    uint8_t *data = (uint8_t *)malloc(file_size);
38    if (!data) {
39        perror("Error allocating memory");
40        fclose(file);
41        return EXIT_FAILURE;
42    }
43
44    // ファイルからメモリにデータを読み込む
45    fread(data, 1, file_size, file);
46    fclose(file); // ファイルを閉じる
47
48    // 各バイトを表示するループ
49    for (long i = 0; i < file_size; i++) {
50        // バイトのメモリアドレスとそのビット列、16進数表示を出力
51        printf("0x%08lx: ", (long)(uintptr_t)(data + i));
52        print_bits(data[i]); // ビット列を表示
53        printf(" | 0x%02x\n", data[i]); // 16進数表記を表示
54    }
55
56    // 確保したメモリを解放
57    free(data);
58    return EXIT_SUCCESS;
59}

簡単なテキストファイルを作成して試してみる。 上記プログラムをコンパイルして生成した実行形式ファイルを a.out とする。

 1$ cat sample.txt
 2Hello, world!
 3
 4$ ./a.out sample.txt
 50x6000029c8020: 01001000 | 0x48
 60x6000029c8021: 01100101 | 0x65
 70x6000029c8022: 01101100 | 0x6c
 80x6000029c8023: 01101100 | 0x6c
 90x6000029c8024: 01101111 | 0x6f
100x6000029c8025: 00101100 | 0x2c
110x6000029c8026: 00100000 | 0x20
120x6000029c8027: 01110111 | 0x77
130x6000029c8028: 01101111 | 0x6f
140x6000029c8029: 01110010 | 0x72
150x6000029c802a: 01101100 | 0x6c
160x6000029c802b: 01100100 | 0x64
170x6000029c802c: 00100001 | 0x21
180x6000029c802d: 00001010 | 0x0a
19
20$ hexdump -C sample.txt
2100000000  48 65 6c 6c 6f 2c 20 77  6f 72 6c 64 21 0a        |Hello, world!.|
220000000e

hexdump の出力結果と比較すればわかるが、正常に出力できているようだ。

次に、構造体のデータがメモリにどのように格納されているかについて、このプログラムや hexdump を使って確認する。
以下の構造体を宣言する。

1typedef struct {
2    char c;      // 1バイトの文字データ
3    char s[4];   // 最大3バイトの文字列(NULL終端を含む)
4    int i;       // 4バイトの整数(int型)
5    float f;     // 4バイトの浮動小数点数(float型)
6} MyStruct;

この構造体に対して、次のように値を設定する。

1MyStruct myStruct = { 'A', "あ", 0x12345678, 56.78f };

なお、char 型の変数や配列に格納される文字列は、通常、システムのデフォルトエンコーディング(例えば、UTF-8)でエンコーディングされる。念の為、以下コマンドでデフォルトエンコーディングを表示して、UTF-8 であることを確認した。(locale コマンドの出力結果の LANGLC_TYPE で確認しても良いようだ)

1$ locale charmap
2UTF-8

次に、上で作成したプログラムや hexdump を使ってこの構造体のメモリ上の様子を確認する。その前に、以下プログラムにより構造体の内容をバイナリファイルとして書き出す。

 1#include <stdio.h>
 2#include <stdint.h>
 3#include <string.h>
 4
 5// 構造体 MyStruct の定義
 6typedef struct {
 7    char c;      // 1バイトの文字データ
 8    char s[4];   // 最大3バイトの文字列(NULL終端を含む)
 9    int i;       // 4バイトの整数(int型)
10    float f;     // 4バイトの浮動小数点数(float型)
11} MyStruct;
12
13int main() {
14    // バイナリファイル "output.bin" を書き込みモードで開く
15    FILE *file = fopen("output.bin", "w");
16    if (file == NULL) {
17        // ファイルが開けなかった場合のエラーメッセージを表示
18        perror("Failed to open file");
19        return 1;  // 異常終了
20    }
21
22    // MyStruct 型の変数を初期化
23    MyStruct myStruct = {
24        'A',             // 文字データとして 'A' をセット
25        "あ",            // 文字列データとして「あ」をセット
26        0x12345678,      // 整数データとして 16進数の値 0x12345678 をセット
27        56.78f           // 浮動小数点データとして 56.78 をセット
28    };
29
30    // myStruct のデータをファイルに書き込む
31    // sizeof(myStruct) は構造体のサイズをバイト単位で取得
32    // 1 は書き込むデータの個数(ここでは1個)
33    fwrite(&myStruct, sizeof(myStruct), 1, file);
34
35    // ファイルを閉じる
36    fclose(file);
37
38    return 0;  // 正常終了
39}

上記プログラムを binout.c として、以下のようにコンパイル&実行する。out.bin が作成される。

1$ gcc binout.c -o binout
2$ ./binout
3$ ls -l output.bin
4-rw-r--r--  1 dam  wheel  16  9 15 19:14 output.bin

このバイナリファイルには先ほどの構造体データが出力されているので、メモリに読んだ時にどのようになっているか確認してみる。 a.out と hexdump を実行した結果が以下だ。
 1$ ./a.out output.bin
 20x60000089c000: 01000001 | 0x41
 30x60000089c001: 11100011 | 0xe3
 40x60000089c002: 10000001 | 0x81
 50x60000089c003: 10000010 | 0x82
 60x60000089c004: 00000000 | 0x00
 70x60000089c005: 00000000 | 0x00
 80x60000089c006: 00000000 | 0x00
 90x60000089c007: 00000000 | 0x00
100x60000089c008: 01111000 | 0x78
110x60000089c009: 01010110 | 0x56
120x60000089c00a: 00110100 | 0x34
130x60000089c00b: 00010010 | 0x12
140x60000089c00c: 10111000 | 0xb8
150x60000089c00d: 00011110 | 0x1e
160x60000089c00e: 01100011 | 0x63
170x60000089c00f: 01000010 | 0x42
18
19$ hexdump -C output.bin
2000000000  41 e3 81 82 00 00 00 00  78 56 34 12 b8 1e 63 42  |A.......xV4...cB|
2100000010
  • 1バイト目は「A」を UTF-8 でエンコードした値 0x41 が格納されている。
  • 2 ~ 5バイト目には「あ」のUTF-8エンコード値 0xe38182 と終端文字 \0 (0x00) が格納されている。 ここのサイトで「平仮名」で検索してヒットする箇所をクリックすると、平仮名の UTF-8 エンコード値を確認することができる。
  • 6 ~ 8バイト目の 0x00 はパティングが入っている。 構造体の int 型のデータは 4 の倍数のアドレスに置かなくてはいけないというアラインメント規則に従って、ゼロ埋めされているようだ。
  • 9 ~ 12バイト目には「0x12345678」が格納されている。 バイトオーダがリトルエンディアンの環境のため、メモリの低位アドレスから 0x78, 0x56, 0x34, 0x12 と格納されている。 バイトオーダとは、int などの多バイトで表現されるデータをバイト単位でメモリに格納するときの順序のこと。 ビッグエンディアンの場合は、メモリの低位アドレスから 0x12, 0x34, 0x56, 0x78 と格納される。 以下のコマンドを実行して、今回のテスト環境ではリトルエンディアンであることを確認した。
    1$ lscpu | grep "Endian"
    2バイト順序: Little Endian
    

    但し、テキストデータは多バイトであってもバイトオーダの影響を受けないようだ。(2 ~ 5バイト目において、3バイト文字である「あ」の格納され方から確認できる)

  • 13 ~ 16バイト目には「56.78f」の値が格納されている。 この値がメモリにどのようなビット列で格納されているかについては難しいので、次項でChatGPTに問い合わせた結果を編集して載せておく。(そういうものなんだな・・・という程度の理解に留めておく)

浮動小数点数のビット表現(IEEE 754単精度)

IEEE 754 単精度浮動小数点形式(32ビット)は、次のように構成されています:

  • 1ビット: 符号ビット
  • 8ビット: 指数部
  • 23ビット: 仮数部(マンティッサ)

浮動小数点数の 56.78f をこの形式で表す手順は以下の通りです。

  1. 数値を2進数に変換:

    • まず、56.78 を2進数に変換します。
    • 整数部 (56):
      156 (10進数) = 111000 (2進数)
      
    • 小数部 (0.78):
      • 小数部の2進数変換は繰り返しの操作が必要ですが、0.78 を2進数に変換すると約 0.1100 0110 0011 0011 0011... となります。
      • 通常、計算機では有限桁で丸めるので、これを32ビット形式で表すときは、一定の精度でカットします。
    • 結合:
      • 整数部と小数部を合わせた数値(仮数部)は次のようになります(実際の浮動小数点形式では丸めがあります):
      1111000.1100 0110 0011 0011 0011
      
  2. 正規化:

    • 正規化することで、数値を 1.xxxx × 2^n の形に変換します。
    • この場合は 1.11000110001100110011 × 2^5 となります。
  3. 指数部と仮数部のビット列:

    • 指数部はバイアスを加えた値で表現されます。32ビットの単精度浮動小数点数のバイアスは127です。
    • 指数値 5 の場合、バイアスを加えて 5 + 127 = 132 を2進数にすると 10000100 です。
    • 仮数部は 1 の後の部分を取り出します:
      111000110001100110011
      
  4. 全体のビット列:

    • 符号ビットは 0(正の数)
    • 指数部は 10000100
    • 仮数部は 11000110001100110011

    これを並べると:

    10 10000100 11000110001100110011
    
  5. 16進数に変換:

    • 上記のビット列を16進数に変換します:
      101000010 01100011 00011110 10111000
      
    • リトルエンディアン形式では、メモリ上に次のように格納されます:
      10x16b8334b8: 10111000 | 0xb8
      20x16b8334b9: 00011110 | 0x1e
      30x16b8334ba: 01100011 | 0x63
      40x16b8334bb: 01000010 | 0x42
      

関連ページ