画像ファイルを「眺める」

まず、ここでは2つのパートに分けて説明します。 はじめは画像ファイルを特定する方法。 そして、次に画像ファイルを解析する方法について説明します。 画像ファイルを特定することは、画像を逆アセンブラを用いて解析する場合にも、 「眺める」ことで解析する場合にでも非常に重要な作業となります。 しかし、画像ファイルを特定することは、これから画像形式を解析しようとする 人だけでなく、グラフィックローダーを使用して目的の画像を閲覧しようとしている 人にも重要な作業です。 というのは、せっかく画像ローダーやコンバータがあっても、 画像ファイル名がわからなければ、これらを利用できないケースがあるからです。 というわけで、解析には興味がない人でも、画像ローダーを使うひとは一読してみて ください。

画像ファイルを特定する

まず、画像形式を解析する際に必要となる最初の作業は、 どのファイルに画像のデータが保存されているかを特定することです。 多くの場合、これは画像ファイルの拡張子を特定することと同義です。 画像ファイルの拡張子が特定できれば、その拡張子をもつファイルを 重点的にいくつか「眺める」ことにより、画像形式の解析を行うことができます。

ファイル名を「眺める」

画像ファイルの拡張子を特定する方法として、以下の方法があるでしょう。

ファイルの先頭を「眺める」

ファイル名から画像ファイルがどれかを大まかに特定したところで、 実際にファイルの先頭を眺めてみて、それをより確信に近づけてみます。

画像ファイルの先頭には、通常、そのファイルに収められている画像の情報が 保存されています。 例えば、画像ファイルであることを示す ID、データサイズ、 画像の大きさ(幅、高さ)、色数、パレットなどです。 ゲーム用の画像データの場合は、さらに表示位置という情報が加わることもあります。 ゲーム用の画像データの場合は、色数が固定であることが多いので、 画像の大きさ、表示位置、パレットだけしかないことがほとんどです。 画像の大きさの情報すら保存されていない特殊な形式も極希に存在しますが 眺めて解析する場合はとにかく画像サイズが保存されている場所を 特定することから始まるでしょう。 たとえば、640x480 の CG が多用されているゲームでファイルの比較的先頭から 640 と 480 が見つかれば、それは画像ファイルである可能性が非常に高いです。

「眺めて」解析

本当に「眺める」だけで解析できるの?

ほとんど無理でしょう。 とはいえ、「眺める」だけで解析できるケースもいくつかあるのも事実です。 一昔前、16色CGが全盛のころは、 各形式毎に画像を圧縮するために独特の思想で画像形式が作られていました。 したがって、どのような思想で作られた形式であるか予測することは難しく、 「眺めて」解析するのはきわめて困難でした。 しかし、256色、ハイカラー、フルカラーCGが当たり前となった現在では、 画像の圧縮に一般的なデータ圧縮、しかも最もシンプルな 1,2種類の アルゴリズムを用いている例が多くなっています。 そのため、どのような考えのもとで作成した画像形式なのか予想しやすく、 一昔前に比べれば「眺めて」みて、それを確認、解析しやすくなってはいます。 実際、私は SEGA Saturn 版「下級生」以降、 Win95/98版 WORDS WORTH までの elf のゲームの画像形式は すべて「眺める」だけで解析してきました。 とはいえ、ちらっと眺めただけで解析ができたわけではなく、 SS版下級生、SS版YU-NOでは 1日、 臭作ではとりあえず見える画像がある程度になるまで 1日、完全に解析できるまでに さらに 2日かかっています。 確かに「眺めて」解析することは無理ではないですが、 少ない紙面で説明出来るほど、そう簡単にできるわけではありません。 ただ、上記 3作品が解析できれば、それ以外の elf 作品の画像形式は 毎回異なっているにも関わらず本当の意味でちらっと眺めただけで解析できて しまうのですが。 たとえば、Win95/98版 WORDS WORTH の画像データを解析するのに 眺めはじめてから解析が完了するまで10秒かかりませんでした。

ちらっと眺めただけで解析できてしまうような画像形式は そんなに多くはないので「眺める」だけで解析ができるかと問われれば、 ほとんど無理ということになります。 ただ、私が「眺めてみた」画像形式の中で、 簡単に解析できてしまう事例がありましたので、 これを例にして説明していきたいと思います。 今回「眺めて」みる画像は、 SEGA Saturn版 機動戦艦ナデシコ The blank of 3 years(以下、ナデシコ) です。

データ圧縮の基本

実際に画像データを眺めてみる前に、 データ圧縮の基本的なことについて説明します。

データの圧縮はあるデータ列をそれと等価なより短いデータ列に 置き換えることによって実現します。 その方法として、 エントロピー符号化、ランレングス符号化、辞書ベース符号化などがあります。 また、これらは複合して用いられたりもします。

ランレングス符号化は 何文字も同じ文字が連続して出現しているデータ列があれば、 このデータ列を連続している文字(run)とその長さ(length)を 表すデータに置き換えるものです。 たとえば、A という文字が10文字続いているデータ列があれば、これを <Aが10個>というデータに置き換えます。この置き換えたデータが たとえば 2文字だったとしたら、10文字から 2文字にデータが圧縮されたことに なります。(1)

    BCDEFAAAAAAAAAABCDEF  --> BCDEF<Aが10個>BCDEF ....(1)
辞書ベース符号化には色々ありますが、 ゲーム用画像形式としては LZ77アルゴリズムの考え方をもとにしているものがほとんどでしょう。 これはあるデータ列が過去にすでに出現していた場合にこのデータ列を 過去のデータ列の出現位置とその長さを表すデータに置き換えるというものです。 たとえば、 ある 5文字のデータ列が過去 15文字前に すでに登場していた場合、これを<15文字前から5個> というデータに 置き換えます。この置き換えたデータがたとえば 2文字だったとしたら、5文字から 2文字にデータが圧縮されたことになります。 また、通常 LZ77アルゴリズムは動作としてランレングス符号化を 内包します。 (2 参照)
    BCDEFAAAAAAAAAABCDEF  --> BCDEFA<1文字前から9個><15文字前から5個> ...(2)

上記のようにデータ列を別のより短いデータ列で置き換えるで データ圧縮が行えることがわかったと思います。 しかし、すでに勘の良い方ならお気づきかもしれませんが、 実際には置き換えたデータと置き換えていないデータとの 区別をどのようにするのかという問題があります。 これを解決する方法は三者三様。 したがって、同じ考え方に基づいて作られた画像形式だとしても さまざまな画像形式が存在することになります。

実際に眺めてみよう

それでは、実際にナデシコの DISK1 に入っている CSP_455.PAC という ファイルについて眺めてみましょう。 このファイルの先頭数バイトのダンプリストを図1に示します。

0000  63 73 70 5F 34 35 35 5F-62 6D 70 00 00 00 05 01 | csp_455_bmp..... |
0010  00 00 00 20 00 06 04 36-00 00 00 00 00 00 00 00 | .......6........ |
0020  FB 42 4D 36 04 06 05 00-FB 36 04 00 00 28 04 00 | .BM6............ |
0030  FF 02 03 00 FA 03 00 00-01 00 08 07 00 F8 06 00 | ................ |
0040  13 0B 00 00 13 0B 0A 00-F0 F0 F8 F8 00 F0 F8 F8 | ................ |
0050  00 E0 F8 F8 00 D8 F8 F8-00 03 F8 EF 00 D0 F8 F8 | ................ |
0060  00 C8 F8 F8 00 C0 F8 F8-00 B8 F8 F8 00 03 F0 BF | ................ |
0070  00 B0 F8 F8 00 E0 F0 F8-00 E8 F0 F0 00 D0 F0 F8 | ................ |
0080  00 F0 F0 E8 00 F8 F0 E8-00 E8 F0 E8 00 C8 F0 F8 | ................ |

 図1 CSP_455.PAC のダンプリスト

ファイルを眺めてみると、先頭にいきなり "csp_455_bmp" というファイル名らしきものが見えます。 これはこのファイル CSP_455.PAC が csp_455.bmp という BMPファイルから 生成されたことを示していると考えるのが普通でしょう。 そして、オフセット 21Hの 2バイトにはこれが BMPファイルである ことを示す BM という文字が見えます。 というわけで、この画像形式は先頭 21Hバイトのごみがある BMPファイルだということで解析終了。 といきたいところですが、さすがにそう簡単には終わりません。

BMP ファイルには BM という文字のあとにファイルサイズが格納されている わけですが、これを確認してみると、 60436H = 394294 バイトになります。 ところが、CSP_455.PAC の実際のファイルサイズは 303632バイトです。 どうやらなんらかの方法で圧縮がかかっているようです。 しかし、伸張してみると BMPファイルになるとわかっているのですから、 比較的解析は容易でしょう。

ところで、ファイルサイズが 60436H ということがわかったわけですが、 オフセット 14H からの 4バイトにも同じく 00060436 が保存されている ことがわかります。 ということから、この位置に元のファイルサイズが保存されていると考える ことができます。 オフセット 10H からの 4バイトはどのファイルをみても 20H と かかれています。ですから別に無視してもいいのですが、 BM という文字が 21H から始まっていることを考えると、 データ開始アドレスが保存されていると考えるのが妥当でしょう。 オフセット 18H からの 8バイトはどれファイルでも 0 なのでこれは 本当に無視して構わないでしょう。 オフセット 0EH からの 2バイトは色々なファイルを眺めてみると、 0501H や 0502H などといくつか種類があることに気がつきます。 その内、いままで説明してきた傾向にあるファイルは 0501H の場合だけの ようですので、ここにはファイルの形式が保存されていると考えていいでしょう。

以上、PAC 形式の画像のヘッダ形式は C言語で記述すると以下のようになる ことがわかります。

typedef struct {
    char    name[14];      // 元ファイル名
    char    type[2];       // ファイルタイプ(0501H, 0502H など)
    char    offset[4];     // データの開始位置(通常 20H)
    char    orgSize[4];    // 元のファイルサイズ
    char    reserved[8];   // 予約またはパディング
} PACHDR;
以下、ここでは 0501Hタイプの画像形式についてのみ説明します。

PAC 0501 形式を解析

CSP_455.PAC は 256色BMPファイルを圧縮したファイルのように見えます。 なぜ、16色やフルカラーBMPファイルでないかといえば、 ダンプリスト中にパレット情報があるようにみえるからです。 フルカラーBMPファイルにはパレット情報はありませんし、 明らかに 16個以上のパレット情報があるように見えます。

画像サイズが 394294 である 256色BMPファイルの場合は、 ファイルの先頭 18 バイトは確実に次の様になります。

42 4D 36 04 06 00 00 00 00 00 36 04 00 00 28 00 00 00

今回の場合は、CSP_455.PCK の 20H からの数バイト、例えば、以下の16バイトが

FB 42 4D 36 04 06 05 00 FB 36 04 00 00 28 04 00
が上記のBMPファイルの先頭18バイトのようになるような 圧縮手法を見つければ解析できたことになります。

この 2つのバイト列はぱっとみたところところどころ一致している 部分があるように見受けられます。 一致している部分をわかりやすく分解してみると次の様になるでしょう。

  |42 4D 36 04 06|00 00 00 00 00|36 04 00 00 28|00 00 00
FB|42 4D 36 04 06|05 00 FB      |36 04 00 00 28|04 00

ここで一致していない部分、たとえば 00 00 00 00 00 と 05 00 FB との 関係を考えてみます。 00 00 00 00 00 は 00 が 5個連続したデータであることに着目し、 05 00 FB の方と見比べてみると、こちらには 05 と 00 という文字があることに 気がつきます。これは 00 が 5個連続している ことを示しているデータであると考えるのが妥当でしょう。 つまり、ランレングス符号化されているのです。 では、次の FB とは一体なんなのでしょうか? FB のさらに次の 5文字を見ると、これはもとのデータと何も変化がありません。 よくみると圧縮されたデータの最初の 1文字にも FB があり、 次の 5文字もやはりもとのデータと何も変化がありません。 つまり、FB は次の 5文字はもとのデータと何も変化がないことを 示すデータであるということになります。 さて、前者では 05 という文字は、次の文字、この場合 00 が 5個連続で出現するという意味の 5であることが推測できますが、 後者の FB ではどうして 5文字なのでしょうか。 慣れている方ならすぐに気がつくでしょう。 これは 100H - FBH = 5 の 5なのです。 つまり、この画像形式は、 ある一定値以下のデータが出現した場合は、その次に出現する文字を その値個分だけ連続して出力させ、ある一定値以上のデータが出現した場合は、 それ以降 256-<その値> 個分のデータをそのまま出力するというものだ ということがわかります。 ある一定値というのはおそらく勘のよい慣れた方なら 128であると 予想するでしょう。 実際、この値はデータ列をずっと追いかけて眺めていくと 128である ことがわかります。 以上で解析は終了しました。 リスト1 はこの解析結果と元に PAK 0501 形式の画像を BMPにコンバートする最も単純なソースファイルです。 この文章で PAC 0501 の画像形式がわからなくても、 ソースの方を見れば画像形式が一目瞭然だと思いますので そちらを参考にしてみてください。

リスト1

#include 
#include 

typedef unsigned char   LONGB[4];
typedef unsigned char   WORDB[2];
#define GETWORDB(p)     (((long)*(p)<<8)|(long)*(p+1))
#define GETLONGB(p)     ((GETWORDB(p)<<16)|GETWORDB(p+2))

typedef struct {
    char    name[14];
    WORDB   type;
    LONGB   offset;
    LONGB   orgSize;
    char    reserved[8];
} PACHDR;

void unpac(FILE *ifp, FILE *ofp, long size) {
    while ( size > 0 ) {
        int c2, ch = fgetc(ifp);
        if ( ch >= 128 ) {
            char    buf[128];
            ch  = 256-ch;
            fread (buf, 1, ch, ifp);
            fwrite(buf, 1, ch, ofp);
            size    -= ch;
        } else {
            size    -= ch;
            for ( c2 = fgetc(ifp); ch > 0; ch-- )
                fputc(c2, ofp);
        }
    }
}

int main(int argc, char **argv) {
    char    *p, *infile = argv[1];
    FILE    *ifp, *ofp;

    if ( argc < 2 ) {
        fprintf(stderr, "usage: %s pac-file\n", argv[0]);
        return 1;
    }

    if ( ifp = fopen(infile,"rb") ) {
        PACHDR  hdr;
        fread(&hdr, 1, sizeof(hdr), ifp);
        if ( GETWORDB(hdr.type) == 0x0501 && (p = strstr(hdr.name,"_bmp")) ) {
            *p  = '.';
            if ( ofp = fopen(hdr.name,"wb") ) {
                unpac(ifp, ofp, GETLONGB(hdr.orgSize));
                fclose(ofp);
            } else {
                perror(hdr.name);
            }
        } else {
            fprintf(stderr, "sorry. can not convert file.\n");
        }
        fclose(ifp);
        return 0;	
    }
    perror(infile);
    return 1;
}