1. 導入
この記事はあくまで感覚で読んでいただけると嬉しいです。
時にみなさんは考えないでしょうか、「コーディングとはアートみたいなものだ」 と。
エレガントなアルゴリズム、洗練された設計、読みやすいコード。プログラミングには確かに美しさがあります。
しかし、それはあくまで人間が目で見て扱う「高級言語」においての話ではないでしょうか。
それでは実際にコンピュータが実行するバイナリファイルではどうでしょう。
例えば、HexEditorなどで覗いてみることにします。
一見、無機質な 0 と 1 の羅列。無味乾燥なバイト列の大群に見えると思います。(強者にはそうではないのかもしれない。少なくとも私にはそうです。)
しかし、このバイト列の奥底にも、人間には見えないパターン性や規則性、あるいは美しさが隠れているのではないかと思います。
そこで、今回はバイナリファイルを「色」として可視化・画像出力することで、そこに存在するかもしれない「アート」を探求してみます。
2. 実装
バイナリを可視化するために、シンプルなC++の bin_to_image を作成しました。
これは、バイナリファイルの各バイト(0x00〜0xFF)を1つのピクセル(色)に変換し、PNG画像として出力します。
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
enum ColorMode { COLOR, GRAY_SCALE, HEATMAP, INSTRUCTION, ENTROPY };
struct RGB {
uint8_t r, g, b;
};
RGB get_instruction_color(uint8_t val) {
if (val == 0x00)
return {0, 0, 0};
if (val == 0x90)
return {255, 255, 0};
uint8_t f = val & 0xF0;
switch (f) {
case 0x00:
return {128, 128, 128};
case 0x10:
return {100, 100, 100};
case 0x20:
return {150, 150, 150};
case 0x30:
return {200, 200, 200};
case 0x40:
return {255, 100, 100};
case 0x50:
return {0, 255, 0};
case 0x60:
return {100, 255, 100};
case 0x70:
return {255, 0, 255};
case 0x80:
return {255, 165, 0};
case 0x90:
return {255, 255, 0};
case 0xA0:
return {0, 255, 255};
case 0xB0:
return {0, 0, 255};
case 0xC0:
return {255, 192, 203};
case 0xD0:
return {128, 0, 128};
case 0xE0:
return {255, 0, 0};
case 0xF0:
return {0, 128, 0};
default:
return {128, 128, 128};
}
}
RGB get_heatmap_color(uint8_t val) {
float n = val / 255.0f;
if (n < 0.25f) {
float t = n / 0.25f;
return {0, (uint8_t)(255 * t), 255};
} else if (n < 0.5f) {
float t = (n - 0.25f) / 0.25f;
return {0, 255, (uint8_t)(255 * (1 - t))};
} else if (n < 0.75f) {
float t = (n - 0.5f) / 0.25f;
return {(uint8_t)(255 * t), 255, 0};
} else {
float t = (n - 0.75f) / 0.25f;
return {255, (uint8_t)(255 * (1 - t)), 0};
}
}
RGB get_entropy_color(const std::vector<uint8_t> &data, size_t idx) {
int r = 4, diff = 0, count = 0;
for (int i = -r; i <= r; i++) {
if (i == 0)
continue;
int j = (int)idx + i;
if (j >= 0 && j < (int)data.size()) {
diff += std::abs(data[idx] - data[j]);
count++;
}
}
uint8_t e = count > 0 ? std::min(255, diff / count * 4) : 0;
if (e < 64) {
uint8_t b = (uint8_t)std::min(255, e * 4);
return {b, b, b};
} else
return get_heatmap_color(data[idx]);
}
RGB get_color(const std::vector<uint8_t> &data, size_t idx, ColorMode mode) {
uint8_t val = data[idx];
switch (mode) {
case COLOR:
return val == 0x90
? RGB{255, 255, 0}
: RGB{val, (uint8_t)(255 - val), (uint8_t)(val / 2 + 64)};
case GRAY_SCALE:
return {val, val, val};
case HEATMAP:
return get_heatmap_color(val);
case INSTRUCTION:
return get_instruction_color(val);
case ENTROPY:
return get_entropy_color(data, idx);
default:
return {val, val, val};
}
}
int main(int argc, char *argv[]) {
const char *infile = argv[1]; // 入力バイナリ
const char *outfile = argv[2]; // 出力画像
ColorMode mode = INSTRUCTION; // 初期モード指定
// コマンドライン引数でモードを指定
if (argc >= 4) {
std::string m = argv[3];
if (m == "grayscale")
mode = GRAY_SCALE;
else if (m == "heatmap")
mode = HEATMAP;
else if (m == "instruction")
mode = INSTRUCTION;
else if (m == "entropy")
mode = ENTROPY;
}
std::ifstream binary_file(infile,
std::ios::binary); // バイナリモードで読み込み
if (!binary_file) {
std::cerr << "ファイルが開けません!!!\n";
return 1;
}
std::vector<uint8_t> data((std::istreambuf_iterator<char>(binary_file)),
std::istreambuf_iterator<char>());
binary_file.close();
int width;
if (argc >= 5) {
width = std::atoi(argv[4]);
} else {
// 幅を計算(64~1024)
width = std::max(64, std::min(1024, (int)(1 << ((int)std::ceil(std::log2(
std::sqrt(data.size())))))));
}
// 高さを計算
int height = (int)std::ceil(data.size() / (float)width);
std::vector<uint8_t> image(width * height * 3, 0);
// 縦線・横線強調用の閾値
const int vert_thresh = 8; // NOP連続で縦線
const int horiz_thresh = 4; // 同じ命令横連続で横線
for (size_t i = 0; i < data.size(); i++) {
// 座標の計算
int x = i % width;
int y = i / width;
RGB c = get_color(data, i, mode);
// 縦線判定(NOPスレッド)
int nop_count = 0;
for (size_t j = i; j < data.size() && j < i + vert_thresh; j++) {
if (data[j] == 0x90)
nop_count++;
}
if (nop_count >= vert_thresh) {
c = {255, 255, 0};
} // 太い黄色縦線
// 横線判定(同じ命令連続)
int same_count = 1;
for (size_t j = i + 1; j < data.size() && j < i + horiz_thresh; j++) {
if (data[j] == data[i])
same_count++;
}
if (same_count >= horiz_thresh) {
c.r = std::min(255, c.r + 50);
c.g = std::min(255, c.g + 50);
c.b = std::min(255, c.b + 50);
}
image[(y * width + x) * 3 + 0] = c.r;
image[(y * width + x) * 3 + 1] = c.g;
image[(y * width + x) * 3 + 2] = c.b;
}
if (!stbi_write_png(outfile, width, height, 3, image.data(), width * 3)) {
std::cerr << "画像出力できません!!!\n";
return 1;
}
std::cout << "画像を出力しました。\n"
<< outfile << "\nSize: " << width << "x" << height
<< "\nMode: " << mode << "\n";
return 0;
}
基本設計
- バイナリファイルの各バイトを1ピクセル分の色に変換
- png形式で出力することで、視覚的に命令パターンを認識できるように。
- ライブラリはstb_image_writeのみを使用。
- 入力:任意のバイナリファイル
- 出力:png画像ファイル
- カラーモードを選択可能に。
- RAINBOW: カラフルなマッピング
- GRAYSCALE: グレースケール
- HEATMAP: 青→緑→黄→赤のヒートマップ
- INSTRUCTION: x86の命令別に色分け
- ENTROPY: 周辺バイトとの差分によるエントロピー可視化
カラーリング
「機械語の意味」 を色に反映させるために、x86-64命令セットに基づいた独自のカラーリストを設計しました。
| 色 | 対象バイト | 意味(x86命令) |
|---|---|---|
| 黄色 | 0x90 |
NOP (No Operation) |
| 緑 | 0x50-0x6F |
PUSH / POP (スタック操作) |
| 紫 | 0x70-0x7F |
Jcc (条件分岐) |
| 赤 | 0xE0-0xEF |
CALL / JMP (関数呼び出し・制御) |
| 黒 | 0x00 |
Zero (パディング・空領域) |
3. 実験
作成したツールを使って、いくつかの初歩的なプログラムを可視化してみます。
| Hello World | フィボナッチ数列 | バブルソート |
|---|---|---|
![]() |
![]() |
![]() |
小さなプログラムでは、全体がコンパクトにまとまっています。色の塊が機能のまとまりを表しているようです。
非常に単純なプログラムであるため、大きな差異を見ることはできないように思えます。しかし、関数のサイズが大きいバブルソートの画像については命令の範囲が大きくなっていることもわかると思います。
Linuxコマンドの解剖
次に、より大きなソースコードを見るために、実用的なLinuxコマンドの中身を覗いてみます。
/bin/ls

上部の命令部分や、下部の文字列や定数部分が見え、プログラムの構造が地層のように分かれているのが見て取れます。
/bin/gzip

圧縮アルゴリズムを含むため、即値の計算(オレンジ系)が多く計算密度の高さが視覚的に伝わってきます。
/usr/bin/python
4. コンパイラの最適化レベルによる違い
同じC言語のソースコードでも、コンパイルオプション(最適化レベル)を変えると、生成されるバイナリの「見た目」は変化します。
アートでいえば画風のようなものかもしれません。
最適化レベルの比較
| 最適化 | 画像 | 特徴 |
|---|---|---|
|
-O0 |
![]() |
メモリ操作(青・緑系)が多く、無駄な動きが多い印象。 |
|
-O1 |
![]() |
中段下位にパターンが見える。 |
|
-O2 |
![]() |
非常に高密度。 |
|
-O3 |
![]() |
ループアンローリングにより、幾何学的な模様が現れているように見える。 |
特に -O3 の画像を見てください。人間が書いたループ処理を、コンパイラが展開して並べた結果、密度の高い幾何学模様が生まれています。まるで一種のアートではないでしょうか。(私は、アートが一切わかりませんが...)
5. 最後に
プログラマーの中には普段無機質な文字列ばかりと格闘されている方も少なくないと思いますが、色のついた絵としてみると、意外と興味深かったのではないでしょうか。
今回は、バイナリの可視化自体に注力したため、細かな実装やそれがもたらす効果の分析は十分に行えているとは言えません。しかし、通常であれば0と1の文字列だけのものを違った見方へと変換することで、異なる可能性を模索できるかもしれません。
本記事ではこのツールの提案というよりも、発想や考え方のシェアになればよいと感じております。
また、すでに同じアイディアで実用されている例などありましたら、ぜひ教えていただけますと幸いです。








