第35回 プログラミングについて 『バイナリファイルを扱ってみよう! その1』

『バイナリファイルを扱ってみよう! その1』
ということで、今回はバイナリファイルを扱ってみましょう。通常ファイル自体にはバイナリファイルとかテキストファイルとかの区別はOS上では何もありません。ファイルに何どのように書かれているかでこのような区別をしているだけなのです。
では実際にファイルがテキストファイルなのか否かはどのようにして判断したらいいのでしょうか。一番簡単なのは、画面にその内容を表示してみることです。人間が読めるように表示されればテキストファイル、そうでなければバイナリファイルと判断できます。
しかしながらこの方法は危険です。運良くテキストファイルであれば問題はありませんが、そうでないときには悲惨な結末になることもあります。
通常ディスプレイ装置にはその画面を制御する特別なエスケープシーケンスがあり、表示した内容がそのエスケープシーケンスに一致してしまったときには画面がぐちゃぐちゃになったり、Xウィンドウなどではウインドウの大きさが変ったり、とんでもないところに移動したりするのです。本体との通信をOFFにするなどのエスケープシーケンスのときにはうんともすんとも言わなくなってしまいます。
安全な方法はファイルをダンプするコマンドを使用することです。DOSでは dump、UNIXでは od などファイルをダンプするコマンドがありますのでこれを使用してその内容を確認します。このファイルの先頭の部分をDOSの dump コマンドでダンプしてみると次のようになっています。
00000000 0D 0A 20 20 20 20 20 20-20 20 20 20 20 20 20 20 ..
00000010 20 20 20 20 20 20 20 83-76 83 8D 83 4F 83 89 83 プログラミ
00000020 7E 83 93 83 4F 82 C9 82-C2 82 A2 82 C4 20 20 91 ングについて 第
00000030 E6 82 52 82 54 89 F1 96-DA 0D 0A 0D 0A 20 20 20 35回目....
00000040 20 20 20 20 20 20 20 20-20 20 20 20 20 20 20 20
00000050 20 81 77 83 6F 83 43 83-69 83 8A 83 74 83 40 83 『バイナリファイ
00000060 43 83 8B 82 F0 88 B5 82-C1 82 C4 82 DD 82 E6 82 ルを扱ってみよう
00000070 A4 81 49 81 78 0D 0A 0D-0A 20 20 82 C6 82 A2 82 !』.... という
00000080 A4 82 B1 82 C6 82 C5 81-41 8D A1 89 F1 82 CD 83 ことで、今回はバ
00000090 6F 83 43 83 69 83 8A 83-74 83 40 83 43 83 8B 82 イナリファイルを
000000A0 F0 88 B5 82 C1 82 C4 82-DD 82 DC 82 B5 82 E5 82 扱ってみましょう
000000B0 A4 81 42 92 CA 8F ED 83-74 83 40 83 43 83 8B 8E 。通常ファイル自
000000C0 A9 91 CC 82 C9 82 CD 0D-0A 83 6F 83 43 83 69 83 体には..バイナリ
左の部分はその行の先頭文字のファイル先頭からのオフセット値が16進数で表示されています。中央のものはファイルの内容を16進数で表示しています。右側はファイルの内容を文字で表現できるときにはその文字で、そうでないときにはドットで表示しています。
このダンプを見てみると右側に人間が読める文字が表示されていますので、9割りがたテキストファイルと判断できます。また各行の改行部分(このダンプでは右側に ..と表示されている部分)を16進数で内容が表示されている部分を見てみますと、0D 0A になっていて通常のDOSの改行形式と同じですので、ほぼテキストファイルと判断することができます。
次にバイナリファイルをダンプしてみましょう。
00000000 4D 5A F9 00 1B 00 05 00-20 00 81 00 FF FF 30 03 MZ...... .....0.
00000010 00 08 00 00 0E 08 00 00-1E 00 00 00 01 00 1C 08 ................
00000020 00 00 E4 08 00 00 02 09-00 00 00 00 33 02 4A 0C ............3.J.
00000030 38 02 00 00 00 00 00 00-00 00 00 00 00 00 00 00 8...............
00000040 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
:
途中省略
:
000001F0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000200 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000210 55 8B EC B8 0E 00 E8 EB-08 56 57 FF 76 06 FF 76 U駆ク..齏.VW.v..v
00000220 04 E8 46 02 83 C4 04 3D-00 00 74 03 E9 06 00 B8 .錻.ζ.=..t....ク
00000230 01 00 E9 58 01 83 3E 50-02 00 75 03 E9 09 00 E8 ..餽..>P..u....鐔
このファイルをみるとまずテキストファイルでないことは右側を見れば分かります。
また、16進数で表示されている部分でも 00 や FF などテキストでは使われていない値が表示されています。
さて、それでは実際にファイルにバイナリのまま値を書き込むことを考えてみましょう(例はC言語で示します)。
まず決めなければならないことは、ファイルをオープンするときに高水準入出力ので行なうか、低水準の入出力で行なうかを決めなければなりません。高水準の入出力は入出力時にバッファが介在しており、標準入力や標準出力などにも簡単にアクセスすることができます(バッファリングを行ないますのでときとして入出力が高速になることもあります)。それに対して低水準の入出力はバッファリングは行なわず、ファイルや周辺装置に対して直接(本当は直接とは言えませんが)入出力を行ないます。高水準の入出力でも実際の入出力では低水準の入出力の機能を利用しています。
ここでは普段あまり使用する頻度の少ない低水準の入出力の関数を利用することにします。
まずファイルをオープンしてみましょう。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys\stat.h>
main()
{
int fd;
fd=open("binary.dat",O_CREAT|O_RDWR|O_BINARY|O_TRUNC,S_IREAD|S_IWRITE);
close(fd);
}
なんだか面倒臭い引数ですが、
O_CREAT ファイルがないときには新しくファイルを作成することを意味します。
O_RDWR 読み書きの両方のアクセスを行なうことを意味します。
O_BINARY バイナリモードでオープンすることを意味します。
O_TRUNC オープンするファイルが既に存在しているとき、それまでのファイルの内容をすべて破棄することを意味します。
以上はファイルをオープンするときのモードでこれらはOR演算子でORをとって指定します。
S_IREAD オープンされるファイルは読み込みアクセスを許します。
S_IWRITE オープンされるファイルは書き込みアクセスを許します。
以上はオープンされたファイルに与えられる属性で、これもORをとって指定します。書き込みのアクセスを許しておかないと、ファイルがリードオンリーになってしまい削除するのが面倒になってしまいますので注意して下さい。
このプログラムを実行すると、めでたく binary.dat というファイルが出来上がります。
次にこのファイルにバイナリでデータを書き込むことを考えてみましょう。高水準の入出力では、
fprintf(fp,"%s\n",text);
などとしてファイルに書き込みますが、この関数では文字型変数 text のヌルまでを書き込むので、このような関数は使えません。低水準の入出力では書き込みに関数 write を使用します。write は、
int write(int fd,const void *buffer,unsigned int count);
となっています。第1引数は関数 open によって戻されるファイルディスクリプタで、第2引数は void 型のアドレス、第3引数はそのサイズですので、変数のアドレスとそのサイズをこの関数に渡せればファイルに書き込めることになります。そこで次のようにして実行してみます。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys\stat.h>
main()
{
int fd;
int i;
fd=open("binary.dat",O_CREAT|O_RDWR|O_BINARY|O_TRUNC,S_IREAD|S_IWRITE);
i=100;
write(fd,&i,sizeof(int));
close(fd);
}
試しに実行後の binary.dat を画面に表示してみると、
d
と表示されます。これで正しいのです。決して 100 とは表示されません。どうしてこのようなことになってしまうのでしょうか。binary.dat をダンプしてみましょう。
00000000 64 00 00 00 d...
DOSではこのようになっています。16進数で 64 は 100 という値ですので間違ってはいないようです。またこのプログラムは32ビット版のコンパイラでコンパイルしたので 64 00 00 00 と 00 が3つ続いています。これはこの32ビットのコンパイラではint 型は4バイトということになります。ちなみに16ビット版では、
00000000 64 00 d.
となります。
もう1つ変なことに気付きませんか。64 00 という並び方です。00 64 の方が正しいように思いませんか。そうです、変なのです。同じことをHPのワークステーションで行なってみると、
00000000 00 00 00 64 ...d
となり、DECのワークステーションでは、
00000000 64 00 00 00 d...
となるのです。
この違いはそれぞれの計算機のアーキテクチュアの違いによるものです。この 64 が先にあるタイプのもの、すなわち下位のバイトを先頭に置くものをリトルエンディアン、上位のバイトを先頭に置くものをビッグエンディアンと言います。インテル、DECは前者に、HPは後者になります。
同じ値をバイナリのままファイルに書き込むと、このような違いがあるということは、インテルのCPUを使っている計算機で作ったバイナリファイルを、HPの計算機で読み込もうとすると、とんでもない値になってしまうということです。ですから世間一般にはデータファイルの受け渡しにはバイナリファイルではなく、テキストファイルにすることが多いのです。しかしながら、どうしてもバイナリの状態でファイルの受け渡しを行ないたいときがあります。このようなときには次のように各バイトの並び替えを行ないます。
int ldata,bdata;
:
:
/* ldata にはリトルエンディアンで
値が入っているとします */
little_to_big_int(&ldata,&bdata);
:
:
move_long(int *ldata,int *bdata)
{
char *from,*to;
from=(char *)ldata;
to =(char *)bdata;
*(to+0)= *(from+3);
*(to+1)= *(from+2);
*(to+2)= *(from+1);
*(to+3)= *(from+0);
}
実数値(浮動少数)が書き込まれているバイナリファイルの場合には整数値で行なったバイトの並び替えの他に注意することがあります。基本的にはリトルエンディアンとビッグエンディアンの違いだけのときにはバイトの並び替えだけで変換することができます。ところが実数値の表現の方法が1つだけではないのです。現在の計算機では殆どがIEEEの浮動少数の形式をとっていますが、DECのVAXではVAX特有な形式をしています。手元に資料がないので詳細の説明はできませんが、確か仮数部か指数部のどちらかに隠しビットがあり(浮動少数を表現する際にどんな値のときにでも変化のしないビットがあるので、これを値の中には表現しないためこのような呼びかたをします)、実際にVAXからIEEEの形式に変換するときにはちょっとしたビット操作で変換できます。
話が脱線してしまいました。本題に戻しましょう。
最初の例ではファイルに 100 という値を書き込むときに、整数の変数のアドレスとその長さを関数 write に渡して行ないました。しかし実際にはあまりこのようなやり方は行ないません。通常バイナリファイルの構造を設計するときには、レコード長や1つのレコードのどの部分に何を書き込むかをきっかりと決めておき、それに従って書き込みを行ないます。
例えば、1レコードが40バイト固定の長さとし、次のようにその内容を定義したとします。
0 0 00000000 11111111 11222222 22223333 333333
0 1 23456789 01234567 89012345 67890123 456789
| | | | | | |
| | X1 Y1 X2 Y2 Not used.
| Kind2
Kind1
Kind1 はレコードの種類、Kind2 は Kind1 の中での分類、X1、Y1、X2、Y2 は座標値とします。データの方は Kind1、Kind2 は char 型、座標が double とし、Not usedにはヌルを埋め込むものとします。このようなときに、
char kind1,kind2;
double x1,y1,x2,y2;
char null[6];
:
:
write(fd,&kind1,sizeof(char ));
write(fd,&kind2,sizeof(char ));
write(fd,&x1 ,sizeof(double));
write(fd,&y1 ,sizeof(double));
write(fd,&x2 ,sizeof(double));
write(fd,&y2 ,sizeof(double));
write(fd, null ,6 );
としてもいいのですが、このやり方ではプログラムを読んだときに一体何バイト目に何を書いているのかが分かりにくいものです。こういう不便さを解消するには、
#define RECORD_SIZE 40
char kind1,kind2;
double x1,y1,x2,y2;
char data[RECORD_SIZE];
int i;
:
:
for(i=0;i<RECORD_SIZE;i++)data[i]='\0';
data[0]=kind1;
data[1]=kind2;
*(double *)(data+ 2)=x1;
*(double *)(data+10)=y1;
*(double *)(data+18)=x2;
*(double *)(data+26)=y2;
write(fd,data,RECORD_SIZE);
とした方が、読みやすくなり、レコード長の変更に対しては define 文を変更するだけで済んでしまいます。またファイルに書き込む回数が減りますので実行時間の短縮にもなるはずです。
それではまた次回。