第57回 プログラミングについて『DOSのプログラム3』
プログラミングについて『DOSのプログラム3』ということで、今回はDOSのプログラムの3回目になります。今回は自己解凍プログラムの作り方を考えてみましょう。
自己解凍ができるプログラムで有名なのはフリーソフトの LHA があります。自己解凍プログラムを実行すると、どこからともなくデータファイルが出来上がってしまうのです。まさかプログラムの中に最初からデータを作り付けているわけではないだろうと思っていましたが、一体どうやってこのようなEXEを作っているのだろうと長い間不思議に思っていました。
書店に行ってもこのような変則的なことを堂々と記述している本もないので、自分で考えることにしました。
まず考えられることは、DOS自体が自己解凍を行なうEXEをサポートしているかも知れないということでした。しかしながら色々と調べてみてもこのようなことはどこにも記述はありませんでした。
次に考えたことはEXEに細工をすることです。まず、既存のEXEファイルの後ろにデータファイルを追加してもDOSはEXEをそれまでと同じように実行できるか否かを実験してみたところ、問題はありませんでした。その上、追加したファイルが数メガのサイズでも正常に動作しました。ということは、DOSはEXEをロードするときにEXEファイルのすべてを無条件にロードするのではなく、EXEファイルのどこかにEXEファイルのサイズが記述されているということが予想されます。
次にどこに記述されているのかが問題です。そこで、MSーDOSのプログラマーズリファレンスマニュアルを調べてみると、『EXEファイルの構造とローディング』という章がありました。これによるとEXEファイルのヘッダーには、ファイルの先頭からのオフセットが、
02~03H 最後のページに入っているバイト数
04~05H 512バイト単位のファイルの大きさ(ヘッダも含む)。
と書いてありました。そしてDOSはEXEのこの部分を参照してロードを行なうということでした。ここまで分かれば自己解凍を行なうプログラムは出来たようなものです。
まずEXEファイルのヘッダー部分を読み込みEXEファイルのサイズを表示するプログラムを作ってみましょう。
#include <stdio.h>
main()
{
FILE *fp;
short page_number,last_page_bytes;
long exe_size;
fp=fopen("a.exe","rb"); /* EXEファイルをオープン。ここでは a.exe に
固定 */
fseek(fp,2,SEEK_SET); /* ファイルポインタを移動させます */
/* 最後のページに入っているバイト数を読み込む */
fread(&last_page_bytes,sizeof(last_page_bytes),1,fp);
/* 総ページ数を読み込む */
fread(&page_number ,sizeof(page_number ),1,fp);
/* EXEファイルのサイズを計算します */
exe_size=(long)(page_number-1)*512+last_page_bytes;
printf("%d\n",exe_size);
fclose(fp);
}
このプログラムでEXEのサイズを表示することができました。実験用ですのでファイル名は固定で、エラー処理もしていません。たいして難しいプログラムではありませんが、EXEファイルのオープンはバイナリーモードで行い、EXEのサイズを計算するときには、最後のページが512バイトではない可能性がありますので、ページ数ー1に512を乗算したものに、最後のページのバイト数を加算するようにします。
次にデータファイルを追加するプログラムを作ってみましょう。
#include <stdio.h>
#define MAX_DATA 512
main()
{
FILE *exe_fp,*data_fp;
char data[MAX_DATA];
int ldata;
exe_fp=fopen("a.exe","r+b"); /* EXEファイルを読み書きの両モード
でオープンします */
fseek(exe_fp,0,SEEK_END); /* EXEファイルのファイルポインタを
ファイルの末端に移動します */
data_fp=fopen("test.txt","rb"); /* データファイルをオープンします */
while(1){
/* データを読み込みます */
ldata=fread(data,1,MAX_DATA,data_fp);
/* 読み込んだデータをEXEファイルに
書き込みます */
if(ldata>0)fwrite(data,1,ldata,exe_fp);
/* 読み込んだバイト数が読み込もうとした
バイト数より小さいときにはデータをす
べて読み込んだことになるのでループを
終了します */
if(ldata<MAX_DATA)break;
}
/* ファイルをクローズします */
fclose( exe_fp);
fclose(data_fp);
}
これもそれほど難しいプログラムではありません。データファイルをオープンするときでも、そのファイルがテキストファイルであってもバイナリモードでオープンし、あくまでもバイナリのデータとして扱います。また読み込むデータ用のバッファのサイズは上の例では512バイトにしていますが、このサイズが大きい程ファイルとの入出力の回数が減りアクセス時間が減りますので、実際には丁度都合の良いサイズに設定して下さい。
そしてもう1つ、上のプログラムでEXEファイルに追加されたデータを取り出すプログラムを作ってみます。
#include <stdio.h>
#define MAX_DATA 512
main()
{
FILE *exe_fp,*data_fp;
short page_number,last_page_bytes;
long exe_size;
char data[MAX_DATA];
int ldata;
/* EXEファイルをオープンし、EXEの末端に
ファイルポインタを移動します */
exe_fp=fopen("a.exe","rb");
fseek(exe_fp,2,SEEK_SET);
fread(&last_page_bytes,sizeof(last_page_bytes),1,exe_fp);
fread(&page_number ,sizeof(page_number ),1,exe_fp);
exe_size=(long)(page_number-1)*512+last_page_bytes;
fseek(exe_fp,exe_size,SEEK_SET);
/* 新規にデータファイルをオープンし、EXEフ
ァイルから読み込んだデータをデータファイル
に書き込みます */
data_fp=fopen("out.txt","wb");
while(1){
ldata=fread(data,1,MAX_DATA,exe_fp);
if(ldata>0)fwrite(data,1,ldata,data_fp);
if(ldata<MAX_DATA)break;
}
/* ファイルをクローズします */
fclose( exe_fp);
fclose(data_fp);
}
このプログラムもそれまでのプログラムを参照すれば理解できると思います。この3つのプログラムで、自己解凍を行なうプログラムの基本的な機能を実現できたことになります。
実際に自己解凍のプログラムを作るときには、実行されたプログラムが自分自身のファイルをオープンして解凍の操作を行ないますので、プログラムが自分自身のファイル名を知る必要があります。これには次のようにプログラムを実行するときにDOSがメイン関数に渡す引数を調べます。
#include <stdio.h>
main(argc,argv)
int argc;
char *argv[];
{
printf("%s\n",argv[0]);
}
引数の0番目に、実行しているEXEファイルの名称が渡され、1番目以降には実際にプログラムを実行したときの引数が渡されます。要するに0番目の引数のファイルをオープンして解凍の処理を行なえばいいのです。
本来ならばある程度の完成したサンプルプログラムを紹介すればいいのかも知れませんが、あまり意味がなさそうなので今回は各部分の例にとどめます。実際には自己解凍を行なうプログラムを作る機会は殆どないと思いますが、他にも応用できる可能性がありますので、DOSのEXEの仕組みを知っておくことも無駄ではないと思います。
それでは今回はこれくらいにして、また次回。