みんな部室で何をするのかというと、それぞれ興味を持っていることをす るだけです。エキスパートもビギナーも分けへだてありません。ゲーム作り、 ツール作り、音楽、お絵描きなど、いろんな趣味の人がいます。ゲームで盛 りあがったり、ただおしゃべりをするだけというのも、ごく日常的な光景で す。
TSG は、他大の方や一般の方の参加も歓迎します。プログラミングができ なくてもいっこうにかまいません。私にご連絡いただければ、いつでもご案 内いたします。
〒153 目黒区駒場 3-8-1 東京大学学生会館 305 号
東京大学理論科学グループ 代表 渡辺尚貴 g440056@komaba.ecc.u-tokyo.ac.jp
分科会の参加はまったく自由です。幾つもの分科会に出てもいいし、出なくても構いません。ただ楽しく一緒に活動しようというだけのものなのですから。
Pascal 分科会
C 言語分科会
C++ 分科会
Pascal 分科会、C 言語分科会、C++ 分科会の 3 つの分科会では、各プログラミン
グ言語を、初心者を対象に易しく解説していきます。分科会を開く時間は、参加者の
都合上放課後になってしまいますが、プログラムを学びたいというい方にはお勧めで
す。
分科会に入っていなくても、昼休みや空き時間には部室に人が必ずいるので、ちょっ
としたことならそのときにも教えてもらえることでしょう。(以上 NAO)
UNIX 分科会
X Window プログラム分科会
などができるかもしれません。
- はじめに
- TSG には、年間を通じて、様々なイベントがあります。こうしたイベ ントのうち、主なものについてまとめてみました。
- 新入生への説明会 & 新歓コンパ
- 5 月の上旬に、TSG に入った人、入るかどうか迷っている人のための、 説明会のらしきものがあります。去年は、分科会の説明や部員の自己紹介な どが行われました。ちなみに、新 2 年 (= 当時の新入生) 11 人のうち、こ の説明会のときにいなかった人が、8 人もいます。このことからも、TSG が、 いかに自由なサークルであるかがわかります (笑) 。
説明会の数日後には新歓コンパがあり、渋谷に飲みに行きました。「飲み に」といっても酒がバシバシ出てくるわけではありません。部員のなかには お酒が好きな人もいることはいますが、ほとんどの人は、お酒は苦手です。 そんなわけで、TSG のコンパではソフトドリンクの嵐が吹き荒れます。
- 夏合宿
- 1 学期の期末試験が終わり一段落した頃、夏合宿があります。去年は 山中湖 (2 泊 3 日) にいきました。合宿して何をするのかというと、遊ぶ わけです。夜遅くまで、スーファミや D&D をやっていました。そういえば、 基盤とハンダゴテを持ち込んで、なにか作っているすごい方がいましたね、 たしか。
すごいといえば、もっとすごい人がいます。2 日目に、山中湖 (一周十数 キロくらい) を自転車で一周したのですが、途中で、走っていた (←足で !) ある人物を追い抜いたのです。どっかで見たような気もしなくはなかったの ですが、「まさかこんなところを走っているわけは……」と思ってそのまま 通り過ぎたのです。あとでやはりその人であったことがわかり、周囲から驚 嘆の声が上がりました。
え ? 「その人」が誰かって ? この部報の○科会のところをみればわかる よ。
- 駒祭
- TSG 最大のイベントと言っていいと思います。この駒祭で、日頃の成 果を発表するわけです。
まず、10 月下旬に「駒祭総決起コンパ」が開かれます。この席で、企画 の担当者はホラをふきます。そして、そのホラが少しでも現実に近くなるよ うにするのです。
一年生は毎年、駒祭で占いをすることになっています。去年の占いがどう なったかは、去年の部報の私の原稿を見てもらえればわかるので、詳しいこ とは書きませんが、「準備は少しでも早い方がいいよ」とだけ言っておきま しょう (苦笑) 。
駒祭後には、打ち上げコンパが開かれます。ちなみに、コンパにはここまでに書いたもののほかに、12 月下旬の「ク リスマスコンパ」、3 月上旬の「追い出しコンパ」があります。(以上 ちょ もらんま)
- 冬合宿
- スキーの合宿です。冬休みの最後に行われます。昨年度は妙高高原に 行ってきました。
スキーに一度も行ったことのない人は不安かもしれませんが、スキー初心 者でもいちおう滑れるようになります。部長の NAO はそれまで雪が 20 セ ンチ以上積もっているところを見たことが無かったのですが、この合宿でプ ルークボーゲンができるようになりました。
アフタースキーで、「ああっ女神さまっ !! 」にはまっていた人物がいた のも、記憶に新しいところです。(以上 NAO)
- ケーキパーティー
- ケーキパーティーは、毎年 1 月中旬に寮食堂にて開かれます。持ち 物は、各自 1 台、或いは自分の限界量のケーキ。つまり、みんなで持ち寄っ て、心ゆくまでお菓子を食べようという幸せな企画なのです (本当か ?)。
参加する意義としては、主に次の 2 つが挙げられます。
“こんな事をしたら、体に悪そう”“ケーキが嫌いになっちゃったら、ど うしよう”などとお考えになる向きもあるでしょうが、ご心配無く。今年は、 このパーティーの数日後に某喫茶店のケーキ食べ放題に行った人がちゃんと いました。
- とにかく好きなだけケーキが食べられる。
大きなテーブルに並べられた約 20 人分のケーキが消えてゆく様は、壮観 です。幾ら食べても顰蹙を買う事はありません。却って、讃えられます。
- 血糖値の上昇による寒気を体感出来る。
こんな経験は滅多に出来るものではありません。供されるドリンクもマミ イなどのジュースなので、実に効率的にハイテンションな状態に達する事が 出来ます。物理的に寒気を加速するアイスクリームケーキを持参すると、喜 ばれます。
ただ単に、寮食堂が寒いだけだという噂もありますが。^^;
いずれにせよ、4 年間の大学生活を実り多いものとするに足る、稀有な経 験を得られる機会と言えましょう。(以上 小島 司)
はよーん。ったく☆、でーす。
SysOpやってまーす☆
もちろん、年がら年中雑談ばかりしているわけではありません。プログラ
ムの開発・テストなんかをやってる人も結構多いですね。これには、元々い
ぬ。BBS が「ゲームの開発をしよう」という目論見で開局したという経緯も
ありますし、TSG に強力なプログラマが多い、というのもあります。かくい
う私もいぬ。BBS で、自作フリーソフトウェア "lfd." のαテストをやって
たりします。
プログラムだけじゃなくて、例えば絵を描いてる人も結構いたりしますね
ー。音楽データを作ってる人は今のところ一人しかいないのですが。
ちなみに、去年 (1994 年) 1 年間の統計データを紹介しますと、ファイ
ルライブラリに登録された「自作プログラム・自作データ」の数は、約
200 本でした。
いぬ。BBS には、TSG の中心メンバーのうちのかなりの数がアクセスして
います。そんなわけで、TSG に関する連絡がいぬ。BBS で行われることは多
いですね。コンパや合宿の告知なんかは必ず電子掲示板に書き込まれるよう
です。
掲示版での告知だけじゃなくて、例えば TSG メンバーの間の連絡には、
電子メールがよく使われているみたいです。学校に行かなくても自宅から連
絡が取れるって、予想以上に便利がいいんですよね。
いぬ。BBS
300/1200/2400/9600/14400/28800(V.34/V.FC) (bps)
(1 回線、24 時間運用)
mmm Rev. 4.1
オンラインサインアップ可、会費無料
ったく☆
Tak@いぬ。BBS (SysOp)
taka@is.s.u-tokyo.ac.jp
と部報 186 号に書いた甲斐あってか、最近の Windows 対応のフリーソフトウェアには CTL3D.DLL / CTL3DV2.DLL を使ったものが 増えてきました。(おいおい (^^;)
冗談はさておき、私は部報 186 号の記事「Windows
は見栄えが一番 !! [CTL3D の使用方法]」で CTL3D.DLL の使い方を解説し
たわけですが、その時点では 1 つ不明なことがありました。それは CICA
等で配布されていた "CTL3D.DLL" と Excel 等に付属する DLL
"CTL3DV2.DLL"との違いは何かということでした。CICA 等で配布されている
ものは古いバージョンのもので、それよりも新しい CTL3DV2.DLL 等に関す
る記述は当然何もなかったのです。また、バージョンアップされた情報が
MSDN の CD-ROM にあることは判っていましたが、それを入手する手段はあ
りませんでした。
そこで、私は 1 つの仮説を立てました。「"CTL3DV2.DLL" は
"CTL3D.DLL" の機能に加えて Excel、Word 等マイクロソフト製品で使われ
ている『タブ・コントロール』 を含んでいるのではないか」と。しかし、
その予想は完全に裏切られました。SDK 付属の「スパイ」ユーティリティで
調べてみたところ「タブ・コントロール」は Excel、Word 独自のクラスに
よって実現されていたのです。私は大きく落胆し、「では違いは一体何なの
だ」と悩むあまり眠れない日々が続きました (大ウソ)。
ところが、ある日 Borland C++ 4.0 の CD-ROM のディレクトリを眺めて いたとき、"CTL3D.HLP" というファイルが私の目にとまりました。「どうせ CICA の CD-ROM に含まれているのと同じ古いドキュメントだろう。」とは 思いながらもそのファイルのオープンすると、開けてびっくり玉手箱、その ドキュメントは最近の物だったのです。そして、その中には CTL3D.DLL だ けでなく CTL3DV2.DLL、CTL3D32.DLL に関する記述、また、DLL を使わずに スタティックライブラリを用いて同様の表示を行う方法等それまで私が持っ ていたドキュメントよりもずっと詳細な情報・説明がありました (やっぱり 英語だったけど)。ついに私の悩みは解決したのです !
本記事では、"CTL3DV2.DLL" の使い方、それから "CTL3D.DLL" と "CTL3DV2.DLL" の違いについて簡単に解説したいと思います。
更に開発には CTL3D.H と CTL3DV2.LIB が必要です。CTL3D.H は CICA 等
の旧バージョンの物でもとりあえず大丈夫です。
ちなみに最近の言語製品 (Visual C++ 2.0 に含まれている Visual C++
1.5、Borland C++ 4.0) 等には CTL3DV2.DLL が再配布可能ファイルとして、
また CTL3D.H と CTL3DV2.LIB が含まれています。
CTL3D.H が入手できないときは、あとに述べる API のプロトタイプ宣言
を参考にヘッダーファイルを作ればよく、CTL3DV2.LIB が入手できないとき
はインポートライブラリアン (Visual C++ 等マイクロソフトの処理系、
Borland の処理系ともに IMPLIB.EXE) を用いれば CTL3DV2.DLL から
CTL3DV2.LIB と同等のものを生成することができます。
ちなみに旧バージョンのファイルとドキュメント (英語) 等がまとめてあ
るアーカイブ 3dctrl.zip は CICA の CD-ROM や直接 CICA から入手できま
す。VisualBasic 関連のところを探してみてください。
(注意) 旧バージョンのヘルプファイルでは以下に述べる Ctl3dUnregister 関数は 'Ctl3dUnRegister' と記述されていた (CTL3D.H 内のプロトタイプ 宣言およびサンプルプログラムでは 'Ctl3dUnregister' となっていたが)。 このヘルプファイルのバグは最近のバージョンでは修正されている。
ちなみに、2, 3 の Ctl3dRegister, Ctl3dAutoSubclass 関数は WinMain
関数の初めの方、5 の Ctl3dUnregister 関数は WM_DESTOROY メッセージ処
理中か、WinMain 関数の最後に呼ぶようにすればよいでしょう。以下に例を
示します。
#include <windows.h>
#include <ctl3d.h>
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpszCmdParam, int nCmdShow)
{
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
Ctl3dRegister(hInstance);
Ctl3dAutoSubclass(hInstance);
.
.
.
.
.
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
Ctl3dUnregister(hInstance);
return msg.wParam;
}
long PASCAL _export WndProc(HWND hwnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
switch (message)
{
.
.
.
case WM_SYSCOLORCHANGE:
Ctl3dColorChange();
break;
.
.
.
CTL3D を使うコントロールは grbit に以下の定数を設定する。or す ることで複数個設定可能。
case WM_CTLCOLOR:
return Ctl3dCtlColorEx(message, wParam, lParam);
#include <stdio.h>
void main(void)
{
printf(" TSG is a nice computer circle !! \n");
}
これがプログラムです。なんだかわけの分からない記号がたくさんあ
るでしょう。そのようなことは気にせずにこれを拡張子が .c のファ
イルにしてコンパイルしてください。コンパイルの仕方は各方言によっ
て異なるのであなたと同じ方言の C 言語を使われている方に尋ねて
ください。
まずこのプログラムの下から 2 行目にある printf という文字は
プリントエフまたはプリントフォーマットと呼ばれていまして、要す
るに文字を画面に出す命令です。画面に出したい文字を (" ") で包
んで printf のすぐ後ろに書きます。この例では文字列の最後に \n
という謎の文字があります。これには特殊な意味がありまして、この
文字を画面に出力するとその文字が表れるのではなく改行が行われる
のです。その結果次に表示する文字は前に表示した文のすぐ下の行に
表示されます。試しにこの \n を printf(" ") の中に入れないで実
行してみてください。\n の効果が良くわかることでしょう。
printf(" ")のすぐ後ろに ; (セミコロン) がありますが、これは命
令と命令との間の区切りのマークなのです。このセミコロンがないと
コンパイラは命令がどこで終わるのか判らないのでエラーを出してし
まいます。セミコロンは命令の終わりには必ず付けるようにしましょ
う。
さて printf 文以外の #include や void main(void) や { } は
今のところはまだわからなくても全然かまいません。呪文のように覚
えておいてください。
次にもう少しコンピュータらしく簡単な計算をするプログラムを作っ
てみます。
これを実行すると 1 + 2 = 3 と画面にでます。プログラムの解説
をしましょう。int a,b,c; という文は変数宣言と呼ばれる文です。
この文によってこれ以後は a b c の三文字は整数の値を記憶してお
く変数となります。次の a=1; で a に 1 が代入され b=2; で b に
2が代入されます。そして c=a+b; で 1+2 が計算されてその結果の 3
が c に代入されます。
#include <stdio.h>
void main(void)
{
int a, b, c ;
a=1;
b=2;
c=a+b;
printf(" %d + %d = %d \n", a, b, c );
}
最後の printf 文の様子が先のものとだいぶ違うでしょう。実は
printf 文というのは " " で包まれた部分のみを画面に表示するので
すが %d という変な文字はそのまま画面にでるのではなく " " の後
ろにある変数の値がその %d のある場所に置換されて画面に出力され
るのです。%d が複数ある場合は " " の後ろの変数がその並んだ順に
置換されて表示されます。
よってこの例では 1 + 2 = 3 と画面にでるわけです。このように
printf 文は変数の値の表示の仕方を書式で制御することができます。
それゆえプリントフォーマットなのです。
ここで代入について少し補足しておきます。= マークは代入演算子 と呼ばれていて、= マークの右がわの式を計算した結果を左の変数に 代入する演算子です。したがって a+b=c; のようなことはできません。 また変数にはもともとなにかしらの値が入っているのですが、代入を することによってその値は消滅して新しい値が記憶されます。極端な 例を上げると
2 行目までは a は 1 なのですがここで 2 に強制的に変更されま
す。そして 3 行目で代入演算子の定義にしたがって a は 3 になり
ます。= マークは数学と同じではなく等しいという意味は全然ありま
せん。
a=1;
a=2;
a=a+1;
#include <stdio.h>
void main(void)
{
int n;
int sum=0;
for (n=1; n<=10; n++ ){
sum+=n;
}
printf("The sum is %d \n", sum);
}
for 文の説明に入る前に int sum=0; について解説します。これは変
数の宣言と代入を一緒にしてしまう方法です。このような宣言と同時
の代入を初期化と呼びます。
for (初期化文; ループ条件文; ループ変更文){
ループ実行文(複数可);
}
となっています。
ループ文には for 文のほかにも while 文というのがあります。次
のプログラムを眺めてください。
これは平方根を求めるプログラムです。平方根ですからその値は小
数ですので整数の変数を宣言する int は使いません。替わりに
double x, y; とします。こうすると x, y は小数の値を記憶する変
数として使えるようになります。scanf (スキャンエフまたはスキャ
ンフォーマットと呼びます) という文はプログラム実行中にキーボー
ドから値を変数に代入する命令です。この文の書き方についてにはあ
まりこだわらないでください。double で宣言した変数に値を入れる
ときはこうしてください。またその値を printf で表示するにはその
位置を %lf で示します。int で宣言した変数には scanf("%d",&n);
のようにします。この & の印についはずっと後で説明します。
#include <stdio.h>
void main(void)
{
double x, y;
double a, b;
printf("Input a real number ");
scanf("%lf", &x);
a=x/2.0;
b=x;
while (a-b<-0.0001 && +0.0001<a-b){
b=a;
a=(x/a+a)/2.0;
}
y=a;
printf("\n The root of %lf is %lf \n", x, y);
}
さて本題の while 文についてですがこれは簡単で書式は
となっています。while の ( ) の中の条件文が真ならループを繰り
返すわけです。例の場合真ん中に && マークがありますがこれの意味
は「且つ」つまり「アンド」の意味です。要するにここでは a-b の
値が ±0.0001 以外ならループをするとういうわけです。なお条件の
真偽を 1 と 0 で表すこともありまして while(1) なら永久ループと
なります。
while( ループ条件文 ){
ループ実行文(複数可);
}
どうしてこれで平方根が求まるのかという数学的な話は今回抜きに
しますが、とにかく while 文の使い方を理解してください。
ループではないのですけれど非常に大事な文を忘れていました。そ
れは条件分岐文 if 文です。ある条件が満たされているときのみに命
令を行うという例外処理がよくあります。if 文は簡単です。書式を
示します。
else より後の部分はもし偽の命令群が無いのなら省くことができま
す。やはり、ひとつ例をお見せしたほうがよろしいでしょう。
if (条件文){
条件文が真のときに実行する命令群;
}else{
条件文が偽のときに実行する命令群;
}
これで絶対値が計算されるわけです。
ループの中で突然ループから出たくなる場合があります。そういう
ときには break 命令を使います。またループで次のステップに強制
的に飛びたいときは continue 命令を使います。例を見てください。
if (a<0){
a=-a;
}
実際に作ってみてその効果を確認しておいてください。
while (1){
ループ実行文
if (ループ脱出条件文) break;
}
for(a=1; a<10; a++){
if (ループネクスト条件文)continue;
ループ実行文
}
さてとちょっと横道に行きますが、この場を借りていろいろな演算
子を紹介しましょう。
#include <stdio.h>
void main(void)
{
int score1, score2;
double mean;
printf("Input the score of No1 = ");
scanf("%d", &score1);
printf("Input the score of No2 = ");
scanf("%d", &score2);
mean=(score1+score2 )/2.0;
printf("The mean score is %lf \n", mean);
}
解説は何も必要ではありませんね。あえて言うなら mean の値を計
算するときに 2 で割るのではなく 2.0 で割るということです。もし
ここで 2 で割ると期待している値にはなりません。一般に整数を整
数で割るとその結果はたとえ割り切れなくても整数になります。つま
り商を求めてしまうのです。これはこれで使えることなので覚えてお
いてください。ついでに整数の割り算の余りを求める方法も述べてお
きましょう。それには % を使うだけです。つまり c=a%b; とのよう
にするだけです。
int score[10];
のようにします。これで int 型の変数が 10 個作られ、それをまと
めた団体名が score になります。そしてその変数に値を代入したり、
また値をそこから取り出したりするには例えば
score[2]=90;
a=score[3];
の様に [ ] の中に数字を入れて配列のどの要素かを指定することに
よって実現されます。この数字のことを添え字と呼ぶのですがその数
字の範囲は 0 からその配列変数を宣言した時の [ ] の中の数字 -1
の数字までです。つまり int score[10] としたら score[0] から
score[9] までの変数ができるわけです。くれぐれも score[10] とか
score[-1] としないで下さい。プログラムが暴走することもあります
から。
#include <stdio.h>
void main(void)
{
int score[10]; /*配列の宣言*/
int n, sum;
double mean;
for (n=0; n<10 ; n++ ){ /*配列の各要素に値を入力*/
printf("Input the score of No.%d", n);
scanf("%d", &score[n]);
}
sum=0;
for (n=0; n<10; n++){ /*合計点の計算*/
sum+=score[n];
}
mean=sum/10.0; /*平均点の計算*/
printf("The mean score is %lf \n", mean);
}
どうですか。 for 文など組み合わせることにより非常にすっきり
と大量のデータを扱うことができるようになることがわかりますね。
これなら 100 人ぶんのデータの処理も、このプログラムの 10 と書
いてあるところを 100 にするだけでできます。
#define NUM 10
これでコンパイルの時には NUM のところはすべて自動的に 10 にな
ります。100 人分にしたいのなら、ここの文の 10 を 100 にするだ
けです。非常にエレガントですね。この #define のようなプログラ
ム実行時の命令ではなく、コンパイル時のコンパイラに対する命令の
ことをコンパイラ疑似命令と呼びます。
mean=(double)sum/NUM;
のように sum の前に (double) と書いてください。これによって
sum の値は double 型に変換されます。だから整数である NUM で割
られても小数の値が計算されるわけです。このような (double) をキャ
スト演算子と呼びます。キャスト演算子には他にもいろいろあります
が今紹介できるのは double 型の変数の値を int 型に変換する
(int) ぐらいでしょう。
printf("%d", (int)mean);
とすれば mean の値の整数部分だけが見られますよ。
int a[10]={10, 50, 30, 60, 90, 40, 20, 40, 20, 50};
こうするとコンパイルのときに自動的に各要素に値が初期化されます。
注意しておきますが、このように ={ , , , , } で初期化できるのは
配列変数の宣言のときだけです。宣言後にはそのようにして代入はで
きません。
#include <stdio.h>
void calc_series(void) /* 関数 calc_series の中身の定義の開始 */
{ /* 関数名の前や () の中に書いてある */
int n; /* void は気にしないでください。 */
int sum=0 ;
for(n=1; n<=10; n++){
sum+=n;
}
printf("The sum is %d \n", sum);
} /* 関数 calc_series の中身の定義の終了 */
void main(void)
{
calc_series(); /* 関数 calc_series の呼び出し(実行)*/
}
この {} (中括弧) で囲まれた部分が関数の中身です。
#include <stdio.h>
void calc_series(void);
のように #include の文のすぐ次に行ぐらいにこのように関数名を書
くのです。セミコロンを忘れずに。このなぞの void についてはすぐ
次に説明します。このように関数の中身より先にその名前だけを書い
ておくことをプロトタイプ宣言と呼びます。このプロトタイプ宣言が
あればコンパイラは関数の中身が定義される前にその関数が呼び出さ
れても、それがとりあえず関数の名前であることがわかるので、エラー
を出さずにコンパイルを完了します。
stdio.h には標準入出力関数のプロトタイプ宣言が
math.h には数学関数のそれが
conio.h にはコンソール入出力関数のそれが
string.h には文字列操作関数のそれが
dos.h には DOS 制御関数のそれが
graph.h または graphics.h にはグラフィック関数のそれが
入っています。それぞれの出番はいずれでてくるでしょう。
それから非常に大切なことなのですが calc_series 関数内で定義さ
れている変数 n や sum にアクセスできるのは、この calc_series
関数内でのみです。メイン関数からはこれらにアクセスすることはで
きません。つまりメイン関数内で n=1; とすると コンパイルの時に
「n という変数は定義されていません。」と出てコンパイルが中止さ
れるのです。
一般に関数内で宣言された変数 (ローカル変数と呼びます) はその
関数内からでしかアクセスできません。この変数のアクセス可能な範
囲を変数のスコープと呼びます。では関数外で宣言された変数 (グロー
バル変数と呼びます) のスコープはどのようなのでしょうか。このス
コープはプログラム全範囲となります。どこからでもアクセスできる
のですからとても便利そうですね。しかしそれは半分間違いです。な
ぜなら変数をたくさん作っていくうちに、そのグローバル変数と同じ
名前の変数を作ってしまって変数の衝突が起こるからです。知らない
うちに誤って別のところで変数に値を代入してしまったりするのでバ
グの原因となります。そうしたことを事前に防ぐためにもなるべくグ
ローバル変数の使用は避けましょう。ローカル変数なら別の関数内で
なら同じ名前の変数を使ってもいっこうに構わないのですから、変数
名を他と異なるように苦心して付ける必要もないのです。でもやはり、
どこからでもアクセスできるということにはある種の魅力を感じてや
まないのです。なので時々使われます。
さてもう一歩進んだ関数の使い方を習いましょう。つぎのプログラ
ムは無限等比級数の総和を求めるプログラムです。無限と言ってもあ
る有限項で見切っていますが。しかしこれは先のプログラムとは根本
的に異なっていて関数に級数の比例乗数を与えることができるように
なっています。
この前のプログラムと違って関数の定義の部分の関数名の後の括弧
の中に double r というのがありますね。また関数の呼び出しのとき
に括弧の中に変数 k がはいっています。こうすることによってメイ
ン関数の中の変数 k の値が calc_series 関数の中の変数 r に代入
されるのです。このように関数に与えられる変数のことを引き数と呼
びます。
#include <stdio.h>
void calc_series(double); /* プロトタイプ宣言 */
void main(void) /* メイン関数の定義 */
{
double k;
printf("Input the value of k ");
scanf("%lf", &k); /* 比例乗数 k の入力 */
calc_series(k); /* 関数の呼びだし */
}
void calc_series(double r) /* 関数の定義 */
{
double term=1.0, sum=0.0;
while (term<-0.0001 && +0.0001<term){
sum+=term;
term*=r;
}
/* 結果の表示 */
printf("The sum is %lf \n", sum);
}
関数の定義の部分の (double r) というのはその引き数の名前と型
を定義しているわけです。
プロトタイプ宣言の方を見てください。引き数の変数の名前を定義
していませんね。プロトタイプ宣言には引き数の型 (例えば int,
double) のみを書いておけば良いのです。
今までの関数ではこの部分は (void) となっていましたね。これは
引き数を取らないという意味だったのです。(void) の意味はこれで
良くわかりましたね。
それでは関数名の前に書いてある void の意味は何でしょうか。こ
れは関数がそれを呼び出した方に値を返さないということを意味して
います。では逆に値を返す関数はどのように作るのでしょうか。以下
に例として、相加平均値を計算する関数 mean を作りました。この関
数は引き数として二つの int 型の値を必要とし、ひとつの double
型の値を呼び出した所に返値します。
つまり return ( ) の中に返す値の変数を入れておくのです。こう
して数学と同じ意味の関数ができあがりました。関数を呼び出した側
での返値の扱い方はまさに数学関数と同じです。この場合は多変数引
き数関数ですが引き数が複数ある場合の関数の定義の仕方も理解して
おいてくださいね。多変数引き数の関数の次は多返値関数を作りたい
ところですが残念ながら複数の値を同時に返値する関数は作れません。
#include <stdio.h>
double mean(int, int); /* プロトタイプ宣言 */
void main(void) /* メイン関数 */
{
int a, b;
double c;
a=80;
b=100;
/* 関数の呼び出しと返ってきた値の変数 c への代入 */
c=mean(a, b);
printf("the mean of %d and %d is %lf \n", a, b, c);
}
double mean(int x, int y) /* 関数の定義 */
{
double z;
z=(x+y)/2.0;
return (z); /* 値を返す */
}
いままで int a; とすることによって int 型の変数が定義される
と話してきましたが、もう少し詳しい話をしましょう。そもそも int
型の変数とは 2 バイト以内の大きさの整数を記憶するメモリ−領域
のことなのです。ですから int a; とすることはメモリ−に a とい
う名前で 2 バイトぶんの記憶領域を作ることなのです。メモリ−の
全体は非常に広大で、その 1 バイトごとに数字の住所が付いていま
す。この住所というのがアドレスと呼ばれているものです。作った変
数の在るアドレスを表示してみます。
printf 関数の引き数の a, b, c に & マーク (アンパサンドと呼び
ます) が付いていますね。これを変数の前に付けることによってその
変数の在るアドレスの先頭の値を示すことができるのです。したがっ
てこれを実行して得られる a:-12 b:-14 c:-22 という結果は a, b,
c の先頭のアドレスがその数字であることを示しています。int 型の
変数の先頭アドレスも double 型の変数の先頭アドレスも同じような
数字ですね。住所という物が大きい家でも小さい家でも同じような書
き方であることと同じです。以後アドレスと言ったらこの先頭アドレ
スのことと考えてください。
#include <stdio.h>
void main(void)
{
int a, b;
double c;
printf("a:%d b:%d c:%d \n", &a, &b, &c);
}
アドレスの数字は整数で大きさが 2 バイトなので実は int 型と同
じなのです。この数字を記憶する新しい型の変数を作ってみましょう。
アドレスの数字はどの型の変数のアドレスも 2 バイトなのですがそ
の変数のメモリ上での占める大きさは型によってまちまちです。たと
えば以下のような変数の型があります。
よって変数のアドレスを記憶するための新しい変数とはその変数の
型にあったものでなければなりません。この新しい変数というものが、
有名なポインター変数と呼ばれるものなのです。具体的に言えば int
型の変数用のポインター変数は int * で宣言します。例を出しましょ
う。
int 2 バイト 単精度整数型
long 4 バイト 倍精度整数型
float 4 バイト 単精度浮動小数点型
double 8 バイト 倍精度浮動小数点型
char 1 バイト 文字型
これの実行結果は先のプログラムと同じです。& マークを普通の変
数に付けるとその変数のアドレスを表すのでしたね。それでポインター
変数にアドレスがうまく代入できるのです。このようにポインター変
数にアドレスの値が代入されることを「ポインターがその変数を指す」
と表現します。この表現からポインターという名前が付けられたので
す。逆にポインター変数の値であるアドレスからそこにある変数の値
を表すにはポインター変数に * マーク (アスタリスクと呼びます)
を付けることによって実現されます。例えば
#include <stdio.h>
void main(void)
{
int a, b;
double c;
/* int 型変数用のポインター変数の宣言 */
int *pa, *pb;
/* double 型変数用のポインター変数の宣言 */
double *pc;
/* ポインター変数にアドレスを代入 */
pa=&a;
pb=&b;
pc=&c;
/* ポインター変数の値を表示 */
printf("a:%d b:%d c:%d \n", pa, pb, pc);
}
といった具合です。実行結果は The value at -12 is 1. です。そう
なる理由はおわかりですか。a に 1 が入っていて p は a のアドレ
スが値として入っています。ここで *p というのは p の値であるア
ドレスに存在する int 型変数の値のことです。ですから *p は 1 な
のです。
#include <stdio.h>
void main(void){
int a;
int *p;
a=1;
p=&a;
printf("The value at %d is %d.\n", p, *p);
}
ここでひとつ考えてもらいたいことがあります。
のところを
a=1;
p=&a;
の様に変えたら実行結果はどのようになるでしょうか。実は変える前
と全く変わりません。その理由は良く考えてみれば解るでしょう。
p=&a;
a=1;
次の部分的なプログラムを見てください。
こうしたとき、a の値は何でしょうか。a の値は 1 です。この最後
の行で *p=1; としていますが、この行の意味はポインター変数 p の
値であるアドレスに存在する int 型変数に 1 を代入するということ
です。ここで注意です。くれぐれも次の様にしないでください。
int a;
int *p;
p=&a;
*p=1;
この場合、*p=1; の行では p の値は何になっているかわかりません。
つまりどこかわけのわからない所に 1 が代入されてしまうのです。
なんでもない所に代入されるのなら平気ですが、たまたま p がある
種の危険な所を指していたりしますとプログラムの暴走を招きます。
ポインターを扱う際にはこのことに常に気をつけておいてください。
int a;
int *p;
*p=1;
p=&a;
さて、ここまでの話でポインターが何者かがおわかり頂けたことと
思います。しかしポインターが何のためにあるのか、ポインターのど
こが面白いのかということが全然わからないことと思います。
では、これからいよいよポインターのうま味を説明していきます。
かなり前の方で scanf 関数の引き数に & マークが出てきていまし
たね。
もうおわかりでしょうが、実は scanf 関数に変数 x や score1 のア
ドレスを送っていたのです。ではなぜアドレスを送ったのでしょうか。
scanf 関数はキーボードで入力された数字ないし文字列を指定の変数
に代入する関数です。したがってこの関数が引き数として必要な物は
変数の値ではなく、そのアドレスなのです。どこに代入するかの情報
が必要なわけなのです。だから & マークを付けていたのでした。結
局 scanf("%lf", &x); とすることで x に値が入るので、scanf は値
を返す関数と同じように見えますね。しかし scanf はもっと凄いこ
とができます。多返値関数です。
scanf("%lf", &x);
scanf("%d", &score1);
これで同時に 3 つの変数に値が入ります。" " の中の書式が変数に
正しく対応することに気をつけて下さい。多返値関数を自分でも作っ
てみましょう。よくある例ですが swap 関数を作ります。swap とは
二つの変数の値をお互いに交換する命令のことです。
scanf("%lf %lf %lf", &x, &y, &z);
解説します。swap 関数の引き数には a, b のアドレスを置きます。
a, b にはあらかじめ 1, 2 の値が入っています。swap 関数は 2 つ
のアドレスをポインター変数 x, y で受けます。そしてそのポインター
をもとにメイン関数内の変数 a, b の値を swap 関数内から変更して
いるのです。実に巧妙ですね。この例は非常に大切ですからしっかり
理解してください。
#inlcude <stdio.h>
void swap int *, int * ;
void main(void)
{
int a=1, b=2;
printf("a=%d b=%d \n", a, b);
swap( &a, &b ); /* swap 関数の呼びだし */
printf("a=%d b=%d \n", a, b);
}
void swap(int *x, int *y)
{
int temp;
temp=*x; /* 値の交換の実行 */
*x=*y;
*y=temp;
}
これでポインターのおいしさを少し理解してもらえたと思います。 でもポインターをうまく使うことによってもっとダイナミックなデー タの扱いが可能になります。そのちょっと最初の部分だけの話をしま しょう。
#inlcude <stdio.h>
void movearray(int *, int *);
void printarray(int *);
void main(void)
{
/* 配列の宣言、一方は初期化する。*/
int a[10], b[10]={0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
/* 配列ごとのコピー */
movearray(a, b);
/*配列ごとの表示*/
printarray(a);
}
void movearray(int *p1 ,int *p2)
{
int n;
for (n=0; n<10; n++){
*(p1+n)=*(p2+n);
}
}
void printarray(int *p)
{
int n;
for (n=0; n<10; n++){
printf("%d ", *(p+n));
}
}
この movearray 関数で配列 b の全成分を配列 a の全成分に代入す
ることができるのです。また printarray 関数で配列 a の全成分の
表示ができるのです。ポインターと配列をうまくあわせて使うことに
よってこのような多変数引き数多返値関数がいとも簡単に作れるので
す。
次にこのことを利用した文字列の操作方法について説明します。文
字 1 文字を記憶する変数の型は char です。例えば次のようにして
使います。
つまり文字定数 A は ' ' で包んで使い、printf の書式には %c を
使うのです。文字列とは文字が複数連なったものなのですからここで
配列を使います。
char c = 'A' ;
printf("%c", c ) ;
数字の配列の初期化には ={0, 1, 2, 3} のように成分ごとに ,で
区切っていました。でも文字列でこれと同じことをすると
={'T', 'h', 'o', 'r', 'i', 't', 'i', 'c', 'a', 'l'} のようになっ
て非常に醜いです。そこで例のような " " で包むだけで文字列の初
期化ができるようになっています。これは便利ですね。実はこのとき
Group の p の後に '\0' という特殊な文字が自動的に入って配列が
初期化されます。ですから配列の大きさは文字数 +1 以上にしておか
なければなりません。そして printf の書式には %s を使い引き数に
は配列の先頭アドレスを指定するのです。%s を指定すると printf
関数はその指定されたアドレスから文字を順に表示していき '\0'の
文字で表示を終了します。つまり '\0' の文字はその文字列の終端を
表しているのです。したがって '\0' の無い文字列を表示させたら表
示が終了しなくなり、プログラムの暴走を招きます。でも大抵 '\0'
は自動的に文字列に付加されるのでその心配はいらないでしょう。
char str[30] = "Theoritical Science Group" ;
printf("%s", str);
ここで文字列操作の関数をいくつか紹介しましょう。これらの関数 はヘッダーファイル string.h にそのプロトタイプ宣言が入っていま ので #include <string.h> としてください。
#include <stdio.h>
/* プロトタイプ宣言 */
void input(int *, int *);
int calc_year(int);
int calc_month(int, int);
void print_calender(int, int, int);
/* グローバル配列変数の宣言と初期化 */
int days[13]={0 ,31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
/* メイン関数 */
void main(void)
{
int year, month; /* 変数宣言 */
int diff_y, diff_m;
input(&year, &month); /* 年と月を入力する */
/* 1 月 1 日の曜日を計算する */
diff_y=calc_year(year);
/* 月のついたちの曜日を計算する */
diff_m=calc_month(year, month);
/* カレンダーを書く */
print_calender(year, month, (diff_y+diff_m)%7 );
}
/* 年と月を入力をまとめた関数 */
/* 引き数は年と月の変数のアドレス */
void input(int *y, int *m)
{
/* 年の入力、正しい年が入力されるまでループを繰り返す。*/
while (1){
printf("Input year = ");
scanf("%d", y); /* scanf の引き数に & が無いことに注意 */
/* y の値は既にアドレスであるから。 */
/* 正の数のみを年として受け入れそれ以外なら入力し直し */
if (*y<=0){
printf("Please input a positive number. \n");
}else{
break;
}
}
/* 月の入力、正しい月が入力されるまでループを繰り返す。*/
while (1){
printf("Input month = ");
scanf("%d", m); /* scanf の引き数に & が無いことに注意 */
/* m の値は既にアドレスであるから。 */
/* 1 から 12 の数のみを月として受け入れ */
/* それ以外なら入力し直し */
if (*m<1 || 12<*m){
printf("Please input a right number. \n");
}else{
break;
}
}
}
/* 閏年の数を計算して、この年の 1 月 1 日の曜日を計算する */
int calc_year(int y)
{
int diff;
diff=(y-1)+(y-1)/4-(y-1)/100+(y-1)/400;
/* 1 月 1 日の曜日を返値する */
return (diff);
}
/* 閏年のチェックをして、*/
/* この月のついたちと 1 月 1 日との曜日のずれを計算する */
int calc_month(int y, int m)
{
int n;
int diff;
/* 閏年のチェック */
if (y%400==0 || (y%100!=0 && y%4==0) ){
days[2]=29; /* 閏年なら 2 月の日数を 29 日とする */
}
/* この月までの日数の計算 */
diff=0;
for (n=1; n<m; n++ ){
diff+=days[n];
}
/* 曜日のずれの計算し返値する */
return(diff%7);
}
/* 一月分のカレンダーの表示 */
void print_calender(int y, int m, int diff)
{
int n;
printf("\n====%4d year %2d month ====\n", y, m);
printf("MON TUE WED THU FRI SAT SUN\n");
/* ついたちまでの空白を入れる */
for (n=0; n<diff; n++){
printf(" ");
}
/* カレンダーを書く */
for (n=1; n<=days[m]; n++){
printf(" %2d ", n); /* 日の表示 */
/* 週末のチェック */
if ((diff+n)%7==0){
printf("\n"); /* 週末で改行 */
}
}
printf("\n"); /*ただの改行*/
}
このプログラムの実行結果を示します。
Input year = 1995
Input month = 4
====1995 year 4 month ====
MON TUE WED THU FRI SAT SUN
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
さあどうですか。プログラムはわりと平易に読めたことと思います。
これでもうあなたは立派な C 言語のプログラマーの仲間入りです。
TSG に入ってもっとその腕を磨きましょう。今回の私の話の続きの話として、構造体とポインターとが織りなす ダイナミックなデータ構造の話が TSG 部報の 95 年度 1 月ケーキパー ティー号に載っていますので、是非ともそちらもご覧下さい。またさ らに先の話として C++ 言語のクラスの話が TSG 部報 95 年度 3 月 追い出しコンパ号に載っていますので是非 是非そちらもご覧下さいませ。
それではこの辺でわたしの話を終わりにするとしましょう。どうも
最後まで読んでいただき有難うございました。
TSG は皆様のお越しをお待ちしております。
TSG 95 年度 代表 理科一類 二年三組 渡辺 尚貴
去年の秋、駒場東大前駅の前に、一つのビルが建ちました。情報教育南棟で す。それまでは情報教育棟(現在は情報教育北棟)で、 FM-R を使って BBS ソ フトを使って、学内のニュースを読み書きしたり、日本や世界のインターネッ トにながれているニュースを読んだり、インターネットにつながっているもの 同士でのメール交換ができただけですが、南棟では、最近パソコン関連の雑誌 を見るとたいてい載っている WWW (World Wide Web) で流れている情報を見た り、聞いたり、自分で情報を発信したり、その他様々なことができるようにな りました。ここでは、WWW に関して僕の知っていることを、新入生に分かりや すいよう努力しつつ書いてみたいと思います。
WWW で流れている情報を見聞きするには WWW ブラウザを使います。南棟で
は、xmosaic というソフトを使います。使い方は非常に簡単で、青くなってい
る文章をマウスでクリックすることを繰り返せばいいのです。青くなっている
文章は、URL というもので指し示される他の情報につながっています。
URL とはどういうものでしょう ? URL はそれだけで、どのコンピューター
の、どのディレクトリにある、どのファイルを、どうやって自分のコンピュー
ターに持ってくるかということを指定しています(telnet は少々違う。)。具
体的に、
http://www.komaba.ecc.u-tokyo.ac.jp/~g541119/TSG/TSGhome.html とい
う URL を例として説明します。はじめの http というのは、この情報を要求
する方法の指定です。http プロトコルの他にも、file (ftp プロトコル)、
gopher (gopher プロトコル)、telnet (telnet プロトコル)、etc. 色々あり
ます。次の www.komaba.ecc.u-tokyo.ac.jp は、この情報を持っているコンピュー
ターの名前です。localhost というコンピューターにすると、自分の使ってい
るコンピューターになります。www.komaba.ecc.u-tokyo.ac.jp:10000 といっ
た感じでこのプロトコルで使うポート番号を書くこともできます。次の
~g440604/TSGhome.html は、この情報を持っているコンピューターの
~g440604/ というディレクトリの TSGhome.html というファイルを指定してい
ます。結構ややこしそうに見えますが、何ということはありません。自分で情
報を発信しようと思わないのなら、全く気にする必要はないのです。
南棟から、自分で情報を発信しようとする時、どうすればいいかを次に説明
します。
まず、~/WWW というディレクトリをつくります。chmod go+rx ~/WWW をしま
す(多分。やっておけばまちがいない)。そして、自分のホームページとなる
~/WWW/index.html をつくり、chmod go+r ~/WWW/index.html としておきます
(重要・よく忘れます)。
次に index.html の書き方ですが、WWW ブラウザでみる文章は主に HTML と
いう書式(簡易言語)で書かれています。URL は http://.../...html 又は、
http://.../...html#.... となります。ファイルは、JIS で書くといいそうで
すが、南棟では EUC で書いても ShiftJIS で書いても読めます。文章の中で
は、<> で囲まれた部分が書式を指定しています。僕の良く使うのとしては、
まだまだ色々ありますが、駒場のホームページの下にある HTML について説
明してある文章を読んだり、xmosaic の File - View Source で、他の人の作っ
た文章を見て研究して下さい。HRD さんのものなど、こちらから情報を発信す
るだけでなく、相手からの反応を受けとれるようにすることもできます。
<H1>...</H1> ... の文章を大きな文字で表示。
<H1>〜<H6> がある。
TITLE>...TITLE> ... の文章を Document Title にする。
<ADDRESS>...</ADDRESS> ... がアドレスだと分かるように書体を変える。
<CODE>...</CODE> ... を等しい幅の書体で表示。
<TT>...<TT> 同上
<BR> 改行する。
<HR> 水平の線を挿入。
<IMG SRC="URL"> 絵を表示。絵は GIF。
<A HREF="URL">...</A> ...をクリックできるように色を変えて(青)
表示。クリックすれば、URL で示される情報を表示。
<A NAME="label">...</A> URL を、http://.../...html#label と
して、その情報を表示すると <A NAME="label">
のある場所から表示される。(... には 1 文字以上の
文字がないといけない)
書式 表示結果
箇条書
<UL>
<LI> .... ・....
<LI> .... ・....
<LI> .... ・....
</UL>
番号付箇条書
<OL>
<LI> .... 1. ....
<LI> .... 2. ....
<LI> .... 3. ....
</OL>
説明付箇条書
<DL>
<DT> .... ....
<DD> .............. ..............
.............. ..............
<DT> .... ....
<DD> .............. ..............
.............. ..............
</DL>
(ネスティングできます)
より良い資料としては、 こんなのや、 こんなのや、 こんなのがあります。
最後に、http://www.komaba.ecc.u-tokyo.ac.jp/~g541119/TSG/TSGhome.html につ いて。僕は、南棟が使えるようになってからしばらくは xmosaic は、首相官 邸(?)のホームページの村山総理の写真を見てみたり、Microsoft のページか ら、ソフトを持ってきたりというようなことをしていたのですが、そのうち自 分でも作れることになり、自分のホームページとこの TSG のホームページを 作ったのです。しかし、作ったはいいが書くことがありません。結局、いまで も、ほとんど情報量のないページとなっていますが、少しでも情報量を増やす ために、1. 平安京エイリアンの MS-Windows 版と X Window 版の配布、2. 部 誌を読めるようにする。ということをしたいと思っています。1.は、僕が「何 かホームページに載せる情報ありませんか」とニュースで聞いたところ出てき たもので、僕が作ることになってしまいました。少々怠けていたので、まだ出 来上がっていません。この冊子ができあがったころにはできているといいけど。 2.については、原稿がまだもらえないので、載せられません。原稿ちょーだい >ちょもらんま。みなさんも、なにか載せるべき情報があれば、僕にメールで もして下さい。お願いします。
なにしろ、WWW とつきあったのは、去年の秋からですので、間違っていると ころや、変なところがあると思いますが、御容赦下さい。文章中の間違い、内 容についての質問などがありましたら、下記へメールをください。では。
g440604@komaba.ecc.u-tokyo.ac.jp (TAGA Nayuta)
今回は、LZH の圧縮についてもう少し書こうと思います。まずは前回の記事
の訂正です。
文字列をハッシュ表に登録していく方法の場合、同じ文字が何文字も並んで
いるとスピードが非常に遅くなってしまうので、それを防ぐ方法として PKZIP
が探索する回数を制限したり、同じ文字が並んでいるところをハッシュに登録
しないんじゃないかと書きましたが、やはりうそでした。回数を制限している
というのはあってるみたいですが、やはり LHA のようにハッシュの値を計算
する位置をずらしていました。でも LHA とはかなり違うみたいです。これか
ら圧縮しようとする位置の 1 文字前からの文字列が、それよりも前の文字列
と一致しているときに、その一致している部分でそれぞれリストの次の要素と
の距離を求めて、それが一番遠くなっているリストを探索するみたいです。と
書いてもよくわかりませんね。ぼくもよくわかっていません (爆)。でも、同
じ文字が連続しているところを検出しているのは確かなのですが、それは何の
ためにやっているのかさっぱりわかりません。
もう 1 つの訂正は、ぼくも圧縮を作りたいけど気力がないと書いておきな
がら、作り始めていたことです。言い訳をさせてもらうと、原稿の締め切りが
土曜日だったので、金曜日に書いて提出したのですが、日曜日に気合いを入れ
て作ったのでした。(^^; もっとも、かなり前から LHA の C のソースをいじっ
たり、紙の上でプログラムを書いたりしていたし、スライド辞書以外の部分は
1 年前に作ったいんちき圧縮プログラムと同じなので、わりとすぐにできまし
た。うれしくなって、油すまし君に見せたのですが、どこが変わったのと言わ
れて、-lh6- になったと言ったら、意味ないとか言われたのはショックだった
なあ。(^^;
では、ぼくが現在作っている LZH の圧縮プログラムについて書きます。名
前は SFA です。今は別の名前になってしまった SF & アニメーション同好会
とは関係ないはずです。
ぼくは圧縮率がいいものよりもスピードが速いものが好きなので、何もオプ
ションをつけないときは圧縮率を少しだけ落としてわりと高速に圧縮するモー
ドにしようかなと思っています。このモードでは 486 では LHA 2.63 の 2 倍
くらいのスピードでした。解凍の場合は 286 だと 486 で比べたときより LHA
との差がかなり大きかったのですが、圧縮の場合は 286 でも 2 倍くらいみた
いです。正確に測っていないのでわかりませんが。
あとは、LHA より圧縮率を数パーセント落として 3 倍くらいのスピードで
圧縮するモードと、LHA とほぼ同じ圧縮率で 1.5 倍のスピードのモードがあ
ります。もしかしたらもう少し遅くて縮むモードをつけるかもしれません。で
も、普通の人はオプションはつけない (それどころかドキュメントを読まない)
ので、デフォルトのモードをどうするかというのは非常に重要なんですよね。
SFA のアルゴリズムですが、LHA とほとんど同じになってしまいました。な
さけない。最初は、例の遅くならない工夫は LHA と同じになるし、判定に時
間がかかるかもしれないので使いたくなかったし、遅くなるようなファイルの
ときに適当に検索を打ち切るような簡単な仕組みをいれていたので、あまり複
雑なことはしていませんでした。
やっていたのは、探索する回数を適当に制限するくらいでした。ところが、
ARJ の逆アセンブルリストを圧縮してみたら LHA とほとんどスピードが変わ
らなかったので、これは SFA でも採用しないといけないなあと思っていれて
みました。するとだいぶ速くなってしまったので、くやしいけど採用すること
にしました。今までの工夫は、圧縮率を落とすかわりにスピードを上げるもの
だったのですが、これは圧縮率とスピードが共に良くなりました。解凍の場合
はいかに判定を減らして高速化するかが問題だったのですが、圧縮の場合は、
多少判定に時間を掛けてもそのあとの処理が軽くなるようにすれば速くなるの
ですね。
ちなみに、LHA 2.66 がしている工夫を禁止する方法を書いておきます。
symdeb などで、次のように書き換えてみてください。
こうするとかなり遅くなります。ちなみに、本当はあとのほうの 1 バイト
で十分でした。(^^; 一定の回数探索した場合、リストが長いというフラグを
立てるので、フラグが絶対に立たなくすればいいんですね。また、最初のほう
の 1 バイトだけにすると、あとのほうを変えるよりは少し速くなります。
<2257:4280 7422 JZ 42A4 (リストが長くなかったらジャンプ)
>2257:4280 EB22 JMP 42A4 (無条件ジャンプに)
<2257:4327 7427 JZ 4350 (一定の回数探索したらおしまい)
>2257:4327 7400 JZ 4329 (次の命令へのジャンプに)
LHA とほぼ同じ圧縮率になるモードを作ったはずなのですが、すべてのファ イルでそうなるとは限らないんですよね。2 倍速くても LHA より縮むファイ ルがあったり、どんなにがんばっても圧縮率が LHA より数 % 悪いファイルが あったりします。LHA より縮む方は、PKZIP と同じように 3 バイトのコピー を遠くから行わないからだと思いますが、LHA より縮まない方は、たぶんハフ マン符号にするブロックが LHA より小さいからではないかと思います。SFA では高速化のためにメモリにデータを蓄える方法が違うので、これはどうしよ うもありません。常に LHA より圧縮率が良くなるものを作ろうと思ったら、 中間バッファのサイズを LHA と同じにしなくてはいけないはずなので、そう すると遅くなるのでやめました。
圧縮のアルゴリズムはだいたい固まったので、最近はディスクへの書き込み
を高速化していました。LHA の場合はテンポラリのディレクトリにアーカイブ
を作ってから、最後に目的のディレクトリに転送しています。この方がフロッ
ピーの同じドライブの中で圧縮するよりは速くなりますが、たまにテンポラリ
のディスクが足りなくなって、途中で止まってしまうんですよね。そのときに
LHA は途中まで作ったアーカイブを消してしまうので、ぼくはむかつきます。
だから現在は目的のディレクトリに直接アーカイブを作っていくようにしてい
ます。
1 年前に作った SFA では、このへんは何も考えてなくて、1 つのファイル
を圧縮し終わるごとにヘッダーの部分にシークして、圧縮後のサイズと CRC
を書き込むということをしているので、フロッピー上に圧縮するとしぬほどお
そかったんです。でも今は圧縮したデータをなるべくメモリ内にとっておくよ
うにしているので、オーバーヘッドはあまりないようです。LHA よりはなぜか
速いようなので、これでいいでしょう。本当は、テンポラリのディスクに容量
ぎりぎりまで作って、いっぱいになったら転送というようにするのがいいんで
しょうね。
あと、SFA ではディスクが足りなくなって途中で止まったときに、正常にディ
スクに書き込めたところまでは書庫を残すようにしています。フロッピーにバッ
クアップをとるときは便利かもしれませんね。でも最近は MO があるからいら
ないか。(^^;
SFA は、今は新しい書庫を作ることしか出来ません。この部分を作るのが一 番問題なんですよね。圧縮したいファイル名をメモリ内に全部覚えておかなく てはならないので、そのメモリがどのくらい必要なのかわかんないし。そもそ も LHA の動作をすべて理解しているわけではないし。ああ面倒。(^^; だから こんど NIFTY にアップロードするときも、たぶん書庫の更新はできないもの になると思います。(^^;
油すまし君も LZH の圧縮を作っている途中なので、SFA を見せたときに油 くんの LXF のソースを見せてもらったのですが、思わず笑ってしまいました。 なぜかというと、ぼくのプログラムと同じようなことをしていたからです。考 えることはみんな同じなんだなあ。(^^;
なんか (そういえば、坂井真紀は昔、なんかという言葉をよく使ってたなあ)、 ぼくがした工夫のことを全く書いてありませんが、たいしたことはしてないの で書くまでもないんです。(^^; 探索をいつ打ち切るかを決める方法をちょっ と変えただけなので。
LZH の圧縮のプログラムは、昔からいつかは作りたいなあと思っていたので すが、だいぶ現実的になってきました。でもこの先を作る気力があるかなあ。 (^^; 今の目標は、LHA 3.0 が公開される前に SFA を公開することです。も ちろん -lh6- で圧縮できることは内緒にして。:-)
新入生の皆さん、ご入学おめでとうございます。ところで、team や LHE と いうプログラムをご存じの方はいらっしゃいますか? いたらうれしいなあ。 (^^; もし知っているという方がいらしたら、メールください。アドレスは、 sada@is.s.u-tokyo.ac.jp です。もちろん、知らない方でも、プログラミング に興味があったり、森口博子さんが好きだという人はメールくださいね。(^^)
7 6 5 3 2 0
+−−−−−−−+−−−−−−−−−−+−−−−−−−−−+
| | | |
+−−−−−−−+−−−−−−−−−−+−−−−−−−−−+
↑ ↑ ↑
mod フィールド regフィールド R/M フィールド
modフィールドは、アドレッシングの大まかな形式を指定します。R/M フィー
ルドは、アドレッシングの際のベースレジスタの指定などに使われます。
000 EAX
001 ECX
010 EDX
011 EBX
100 (SIB バイトあり)
101 (ベースレジスタなし、32 ビット定数で指定)
110 ESI
111 EDI
R/M=100, 101 のときは例外です。
000 EAX
001 ECX
010 EDX
011 EBX
100 (SIB バイトあり)
101 EBP
110 ESI
111 EDI
R/M=100 は 8 ビットディスプレースメントを伴うアドレッシングにおける
SIB バイトの存在を意味します。
R/M=100 は 32 ビットディスプレースメントを伴うアドレッシングにおける SIB バイトの存在を意味します。
7 6 5 3 2 0
+−−−−−−−+−−−−−−−−−−+−−−−−−−−−+
| | | |
+−−−−−−−+−−−−−−−−−−+−−−−−−−−−+
↑ ↑ ↑
スケールファクタ インデックス ベース
ベースフィールドは、以下のようにベースレジスタの指定に使われます。
000 EAX
001 ECX
010 EDX
011 EBX
100 ESP
101 EBP
110 ESI
111 EDI
ただし、MODRM バイトが 00 のときに限り、101 は EBP を表さず、かわりに
「ベースレジスタはなく、インデックスレジスタと 32 ビットディスプレース
メントのみが存在する」ことを意味します。
インデックスフィールドは、アドレス計算でベースレジスタに加算するため
に用いられるレジスタを、以下のように決定します。
インデックスフィールドの値が 100 のとき、インデックスレジスタは存在し
ません。
000 EAX
001 ECX
010 EDX
011 EBX
100 (インデックスレジスタなし)
101 EBP
110 ESI
111 EDI
スケールファクタは、インデックスレジスタに掛ける定数を指定します。
00 インデックスレジスタを 1 倍した値を用いる
01 インデックスレジスタを 2 倍した値を用いる
10 インデックスレジスタを 4 倍した値を用いる
11 インデックスレジスタを 8 倍した値を用いる
8 ビットレジスタ 16 ビットレジスタ 32 ビットレジスタ
000 AL AX EAX
001 CL CX ECX
010 DL DX EDX
011 BL BX EBX
100 AH SP ESP
101 CH BP EBP
110 DH SI ESI
111 BH DI EDI
何ビットのレジスタを表すかは、オペレーションコードやセグメントディス
クリプタの D ビットの値で決まります。
【はじめに】
最近 MS-DOS の環境でプロテクトモードのプログラミングをすることがはやっ
ているようで、解説書の数も増えてきました。おかげで、プログラミングがま
すますやりやすくなってきています。この原稿では、そうして僕がプロテクト
モードをいじっているときに見つけた、裏技的な動作モードについて説明しま
しょう。題して、「8086 感覚で 4Gb メモリ空間を扱う方法」です。勘のいい
人はもうわかったかもしれません。そう、あれです。
この動作モードは普通の人は使う気は起こらないとは思いますが、何かの折
りに役に立つこともあるでしょう。
いちおう使用機種は 98 ですが、これはあまり本稿の主題とはあまり関係が
ないはずです。使用するアセンブラは TASM で、ideal モードを使っています。
邪悪、なんて言わないように。みりゃ大体わかるでしょう。
386 以上でサポートされるプロテクトモードについては、ここではいちいち 説明しませんので、これについてある程度以上の知識があることが前提です。 自分で勉強したい人は、最後の【参考書】で紹介している本を読んでみてくだ さい。
【概要】
8086 のプログラミングで「セグメントレジスタ」というと、普通 CS, DS
などの 16 ビット幅のレジスタを指しますが、386 内部ではセグメントレジス
タというのは全部で 80 ビット幅のレジスタであり、このうち 16 ビットを
「セレクタレジスタ」、残り 64 ビットを「ディスクリプタレジスタ」と呼び
ます。これが CS, DS, ES, FS, GS, SS それぞれに用意されています。
この様子は次の図ように表せます。これはオーム社の『80486 の使い方』と
いう本から引用したものです。
mov DS, AX などのセグメントにかかわる命令は、このセグメントレジスタ
のうち、セレクタレジスタをいじっていることになります。そして、ディスク
リプタレジスタの更新は、セレクタレジスタの更新時に同時に行われます。こ
のセレクタレジスタ更新に際して、どのようなことが 386 内部で起こってい
るかを、プロテクトモード、リアルモードの場合について考えてみることにし
ましょう。なお、以下の説明は多分に僕の推理によるもので、実際にこう動作
しているということを保証しているわけではありません。単にこう考えるとう
まくいく、という程度のものです。
16 0 64 0
+--------------+ +----------+------------------------+---------------+
| | CS | 属性 | 先頭番地(32 bit) | 大きさ(20 bit) |
+--------------+ +----------+------------------------+---------------+
+--------------+ +----------+------------------------+---------------+
| | DS | 属性 | 先頭番地(32 bit) | 大きさ(20 bit) |
+--------------+ +----------+------------------------+---------------+
+--------------+ +----------+------------------------+---------------+
| | ES | 属性 | 先頭番地(32 bit) | 大きさ(20 bit) |
+--------------+ +----------+------------------------+---------------+
+--------------+ +----------+------------------------+---------------+
| | FS | 属性 | 先頭番地(32 bit) | 大きさ(20 bit) |
+--------------+ +----------+------------------------+---------------+
+--------------+ +----------+------------------------+---------------+
| | GS | 属性 | 先頭番地(32 bit) | 大きさ(20 bit) |
+--------------+ +----------+------------------------+---------------+
+--------------+ +----------+------------------------+---------------+
| | SS | 属性 | 先頭番地(32 bit) | 大きさ(20 bit) |
+--------------+ +----------+------------------------+---------------+
セレクタレジスタ ディスクリプタレジスタ
まず、プロテクトモードではどうでしょうか。
セレクタレジスタを更新すると、GDTR の指すメモリ上のディスクリプタテー
ブルから、そのセレクタが指すディスクリプタをディスクリプタレジスタに読
み込みます。
つまり、ディスクリプタレジスタはセレクタレジスタの指すディスクリプタ
の単なるコピーで、アクセスする度にディスクリプタテーブルを見に行くのを
避けるための、いわばキャッシュのようなものなのです。これによって、高速
にメモリをアクセスすることができます。
さて次に、リアルモードではどうなっているのでしょうか。
リアルモードでは、セレクタレジスタを更新すると、更新されたセレクタの
16 倍をディスクリプタレジスタのセグメントベースアドレスのフィールドに
セットします。ディスクリプタレジスタの他のフィールドは変更されないまま
残されます。
そしてメモリアクセスは、プロテクトモードと同様、ディスクリプタレジス
タを参照しつつ行われます。セレクタレジスタはまったく参照されません。
プロテクトモードからリアルモードに切り替わる際に、CS には無意味なセ
レクタ値がそのまま残っているにもかかわらず、正しく命令を読み込むことが
できるのは、このような構造をしているからであろうと思われます。
セグメントディスクリプタはセグメントのベースアドレス/リミット/属性な
ど、様々なことを記述できるわけですが、このディスクリプタレジスタの内容
は、リアルモード、プロテクトモードにかかわらず、すべてのメモリアクセス
に関して影響します。
そのために、プロテクトモードからリアルモードに切り替える時には、リア
ルモード用のセグメントを記述したディスクリプタを指すセレクタをロードす
ることが必要になるわけです。
具体的には、CS には 64Kb の大きさの 16-bit コードセグメント、DS, ES,
FS, GS には 64Kb の大きさのデータセグメントを記述したディスクリプタを
指すセレクタをロードします。この作業は、プログラマの責任で行わなくては
なりません。ありがたいことに、386 が自動的にやってくれるわけではないの
です。
そこで、このディスクリプタの復旧を意図的にさぼることによって、リアル
モードで、つまり 8086 感覚で 4Gb メモリ空間を自由にアクセスしようとい
うのが、この原稿で説明しようとしているアイディアなのです。
このような状態でプログラムを実行するメリットは何でしょうか。
まず、プロテクトモードでなくリアルモードで実行していることになるので、
時間のかかる保護機構が働かなくなり、すべての命令を最高速で実行すること
ができます。特にセグメントセレクタの更新、割り込みの発生に際しては明ら
かな違いが現れます。リアルモードなので、CS に対してデータの書き込みも
何の問題もなく行うことができます。
また、メモリアクセスが完全に 8086 感覚なので、特に DOS 環境でのプロ
グラムが作りやすくなります。割り込みも 8086 とまったく同じしくみなので、
めんどうな割り込みディスクリプタの設定が必要ありません。
さらに、ROM BIOS や int 21h の DOS ファンクションコールなど、いまま でのリアルモード用のルーチンも利用も容易です。
さて、Intel がこのような使い方を保証しているという話は聞いたことがな いので、将来発表されるプロセッサではこのアイディアは通用しない可能性が あります。しかし、386/486/Pentium ではきちんと動くことが確認されていま すし、AMD 486DX4 でも動作することが報告されていますので、まあ当分の間 は実用になることでしょう。
【簡単なサンプル】
さて、前置きが長くなってしまいましたが、このアイディアの応用のしかた
の簡単な例として、次のプログラムを見てみましょう。
これは 98 用ですが、単にグラフィック VRAM を消去するもので簡単なので、
98 をお持ちでない方でも言っていることはだいたいわかるでしょう。GRCG を
使ったほうが速いとか野暮なことは言わないように。
________________________________________________________________________
1 ; 98 グラフィック VRAM をクリアするプログラム
2 ; 仮想 86 モードでは実行できません
3
4 ideal
5 p386
6 model use16 small, c
7 jumps
8 locals
9
10 macro switchProtectedMode
11 mov EAX,CR0
12 or AL,1
13 mov CR0,EAX
14 jmp $+2
15 endm
16
17 macro switchRealMode
18 mov EAX,CR0
19 and AL,0FEh
20 mov CR0,EAX
21 jmp $+2
22 endm
23
24
25 stack 100h
26
27 dataseg
28
29 GDTP df ?
30 GDT db 8 dup (0) ; 00h: ヌルセレクタ
31 db 0FFh, 0FFh, 0, 0, 0, 92h, 8Fh, 0 ; 08h: 4GB DATA
32 db 0FFh, 0FFh, 0, 0, 0, 92h, 00h, 0 ; 10h: 64KB DATA
33
34 codeseg
35
36 proc breakWall
37 uses DS, ES
38
39 ; GDT のポインタをロード
40 xor EAX,EAX
41 xor EBX,EBX
42 mov AX,@data
43 shl EAX,4
44 mov BX,offset GDT
45 add EAX,EBX
46 mov [dword ptr GDTP+2],EAX
47 mov [word ptr GDTP],17h
48 lgdt [GDTP]
49
50 pushf
51 cli
52 switchProtectedMode
53
54 mov AX,08h
55 mov DS,AX
56 mov ES,AX
57
58 switchRealMode
59 popf
60 ret
61
62 endp breakWall
63
64 proc makeWall
65 uses DS, ES
66
67 pushf
68 cli
69 switchProtectedMode
70
71 mov AX,10h
72 mov DS,AX
73 mov ES,AX
74
75 switchRealMode
76 popf
77 ret
78
79 endp makeWall
80
81
82 entry:
83 mov AX,@data
84 mov DS,AX
85
86 ; 64KB の壁を消す
87 call breakWall
88
89 ; G-VRAM のクリア
90 cld
91 xor EAX,EAX
92 mov ES,AX
93 mov EDI,0A8000h
94 mov ECX,32768*3/4
95 db 66h, 67h ; ECX, EDI を使用
96 rep stosd
97 mov EDI,0E0000h
98 mov ECX,32768/4
99 db 66h, 67h ; ECX, EDI を使用
100 rep stosd
101
102 ; 壁を再構築する
103 call makeWall
104
105 ; 終了
106 mov AX,4C00h
107 int 21h
108
109 end entry
------------------------------------------------------------------------
このプログラムは TASM と TLINK を使って普通にアセンブル、リンクでき
ます。ただし実行に際しては、いくつかの特権命令が使われているので、仮想
8086 モードでは動きません。リアルモードで実行してください。
このプログラムのメインルーチンは 82 行目から始まります。DS の設定を
した後、breakWall というプロシージャを呼び出します。
breakWall では、switchProtectedMode, switchRealMode という 2 つのマ
クロが使われています。これは冒頭の定義を見ればわかるとおり、CR0 レジス
タの PE ビットを 1 あるいは 0 にしたあと、命令先読みキューをフラッシュ
するというものです。このふたつのマクロでプロテクトモードとリアルモード
を行き来します。
まず breakWall では GDT を設定します。GDT は初期化済みデータセグメン
ト(dataseg)であらかじめ用意されていますので、そこを指すリニアアドレス
ポインタと、GDT のサイズをメモリにストアし、それを lgdt 命令で読み込み
ます。
その後、switchProtectedMode でプロテクトモードに移行した後、DS, ES
にセレクタ 08h をロードします。このセレクタは、ベースが 0h で、リミッ
トが 4Gb のデータセグメントを指します。これによって、DS, ES を使って
386 のリニアなアドレス空間をアクセスすることができます。
しかしこのプログラムでは、このセレクタへの更新を行ったあと何もするこ
となく、さらに、変更した DS, ES の復旧も行うこともなく、すぐにまた
switchRealMode でリアルモードに戻ってきてしまいます。
この「DS, ES の復旧も行うことなく」というのがこのプログラムで一番重
要なところです。リアルモードでも、ここのセレクタの更新によって変更され
たディスクリプタレジスタの中身はそのまま残っています。つまり、ベースア
ドレスが 0h、リミットが 4Gb のままなのです。
プロシージャ breakWall はこれでおしまいですが、このプロシージャには
uses 指定があるので、プロシージャを抜けるとき、DS, ES はコール前の値に
再び更新されます。リアルモードでは、DS, ES が更新されるときに変更され
るセグメントレジスタはそのベースアドレスだけなので、セグメントリミット
は 4Gb のままです。
メインルーチンに戻ると、リアルモードにしては見慣れない記述が続きます。
95 行で db 66h, 67h と置いていますが、これは続く命令のオペランドサイズ
を修飾するプリフィクスで、66h は rep が CX ではなく ECX をカウントする
ようにします。同様に 67h はアドレスサイズを修飾して sotsd 命令が、
ES:DI ではなく ES:EDI をディスティネーションとするようにします。
結局、ここではセグメントベース 0h, オフセット 0A8000h から 0 を
18000h 個書き込んでいるわけです。
これを普通のリアルモードで実行しようと、一般保護例外 #13 が発生して、
rep stosd を実行する瞬間に止まってしまいます。普通のリアルモードでは、
セグメントリミットは 64Kb だからです。しかし、このプログラムでは、何の
問題もなく実行できます。
続いて、n 行目からはセグメントベース 0h、オフセット 0E0000h から 0
を 8000h 個書き込みます。これで、2 つにわかれている 98 のグラフィック
VRAM をすべて消去したことになります。
VRAM の消去が完了したら、プロシージャ makeWall を呼び出します。これ
は GDT の設定をいじらないこと以外は breakWall と構成は同じで、DS, ES
のディスクリプタをリアルモードの値に戻すというものです。
これでこのプログラムはおしまいです。意外と簡単ですね。
【32-bit コードセグメントの実行】
プログラムを 32-bit コードセグメントで実行することはもちろん可能です。
これによって大量のプリフィクス 66h, 67h の削減が期待でき、プログラムが
コンパクトかつ高速になります。しかし、プログラムは途端にややこしくなり
ます。
CS を更新するには、基本的にはセグメント間ジャンプ/コールを行います。
そこで 32-bit セグメントを記述したディスクリプタを作って、そのセグメン
ト内の 32-bit ルーチンにジャンプ/コールし、そのルーチン内でリアルモー
ドに切り替えればよい、というのはだいたい想像がつくでしょう。
しかしこの方法だとプログラムの構造が複雑になりますので、ここではコー
ルしたら CS の D bit を 0 にしたり 1 にしたりして戻ってくるルーチンを
作ってみます。次のサンプルを見てください。
例によって GDT はすでに初期化済みデータセグメントの中にあります。こ
のうちセレクタ 08h と 10h がコードセグメントですが、このままでは不完全
なので、13 行目から書かれている初期化コードをあらかじめ実行して、セグ
メントのベースを INIT32 のセグメントアドレスの 16 倍にあわせておきます。
________________________________________________________________________
1 dataseg
2
3 GDT00 db 8 dup (0) ; 00h: ヌルセレクタ
4 GDT08 db 0FFh, 0FFh, 0, 0, 0, 9Ah, 0, 0 ; 08h: 64KB use16 CODE
5 GDT10 db 0FFh, 0FFh, 0, 0, 0, 9Ah, 0CFh, 0 ; 10h: 4GB use32 CODE
6 GDT18 db 0FFh, 0FFh, 0, 0, 0, 92h, 0, 0 ; 18h: 64KB DATA
7 GDT20 db 0FFh, 0FFh, 0, 0, 0, 92h, 8Fh, 0 ; 20h: 4GB DATA
8
9
10 ; (省略)
11
12 ; GDT 初期化コード
13 xor EAX,EAX
14 mov AX,INIT32
15 shl EAX,4
16 mov [word ptr GDT08+2],AX
17 mov [word ptr GDT10+2],AX
18 shr EAX,16
19 mov [GDT08+4],AL
20 mov [GDT10+4],AL
21 mov [GDT08+7],AH
22 mov [GDT10+7],AH
23
24 ; (省略)
25
26
27 segment INIT32 use32 para public 'CODE'
28 assume CS:INIT32, DS:DGROUP
29
30 proc switch32 far
31
32 switchProtectedMode
33
34 db 66h, 0EAh ; jmp far ptr _32bit_label0
35 dd offset _32bit_label0 ; オフセット
36 dw 10h ; セレクタ 10h
37 _32bit_label0: ; now you are in 32-bit segment.
38
39 switchRealMode
40
41 db 0EAh ; jmp far ptr _32bit_label1
42 dd offset _32bit_label1 ; オフセット
43 dw seg _32bit_label1 ; セグメント
44 _32bit_label1:
45
46 ret ; 32-bit far リターン
47
48 endp switch32
49
50
51 proc switch16 far
52
53 switchProtectedMode
54
55 db 0EAh ; jmp far ptr _16bit_label0
56 dd offset _16bit_label0 ; オフセット
57 dw 08h ; セレクタ 08h
58 _16bit_label0: ; now you are in 16bit-segment.
59
60 switchRealMode
61
62 db 66h, 0EAh ; jmp far ptr _16bit_label1
63 dd offset _16bit_label1 ; オフセット
64 dw seg _16bit_label1 ; セグメント
65 _16bit_label1:
66
67 db 66h ; 32-bit far リターン
68 ret
69
70 endp switch16
71
72 ends INIT32
------------------------------------------------------------------------
プロシージャ switch32 も switch16 も far として宣言されており、プロ
シージャは INIT32 とは別のセグメントから呼ばれることを前提としてます。
セグメント INIT32 は use32 指定があるので、CS の D bit=1 である
32-bit コードセグメントとしてアセンブラに解釈されています。しかし、呼
び出し側は、16-bit, 32-bit 両方の可能性があります。
プロテクトモードでは、far コールによってセレクタを更新するときには、
D bit もディスクリプタにしたがって適切に更新されます。しかしリアルモー
ドにおいては、セレクタの更新で更新されるのはセグメントのベースアドレス
だけですから、このプロシージャに制御を移すときにも、呼び出し側の CS の
D bit の状態は保たれたままということに注意してください。つまり、ソース
の記述と実行時の状態がかみ合わなくなってしまうのです。
switch32 を呼ぶのは CS の D bit=0 のとき、switch16 を呼ぶのは CS の
D bit=1 のときのみですから、66h, 67h をうまく配置して、この違いを調整
しなくてはなりません。
ここで switchProtectedMode と switchRealMode というマクロが出てきま
が、これはひとつ前のサンプルで出てきたものとまったく変わりがありません。
アセンブルしてみればわかりますが、このマクロは、コードセグメントが
16-bit であろうと 32-bit であろうとアセンブラはまったく同じコードを生
成しますので、CS の D bit がどういう状態でも共通に使うことができます。
switch16, switch32 とも、まず switchProtectedMode でプロテクトモード
に移行した後、目的のセレクタをロードするためにセグメント間ジャンプを行
います。プロテクトモードでのセグメント間ジャンプを TASM が理解できるよ
うに表現する方法はないので、このようにコードを db 命令で直接並べる必要
があります。最初の行のコメントにあるのが、このコード列の意味です。この
far ジャンプによって、CS の D bit を 1 にしたり、0 にしたりすることが
できます。
続いて switchRealMode ですぐにリアルモードに戻った後、不正なセレクタ
値を正すために再びセグメント間ジャンプを行います。ここでも db 命令を並
べていますが、これは普通のリアルモードの far ジャンプですので、普通に
jmp 命令で同じことを書くことはできます。
この 2 番目のセグメント間ジャンプによってセレクタは正しい値がロード
されます。D bit はそのままです。これで、CS の D bit だけを変更すること
ができたことがわかるでしょう。
このルーチンを利用して 32-bit コードセグメントを実行できます。
INIT16 から INIT32 のルーチンをコールするのに call switch32 としか書
かれていませんが、これは TASM が自動的に 32-bit セグメントへの far コー
ルであることを認識するので、これだけで 66h プリフィクスつきの far コー
ルを生成してくれます。
________________________________________________________________________
1 ideal
2 p386
3 model use32 small, c
4
5 codeseg ; 32-bit セグメント
6
7 proc main far
8
9 ; (省略)
10
11 ret ; 自動的に far リターン
12 endp main
13
14
15
16 segment INIT16 use16 byte public 'code'
17 assume CS:INIT16, DS:DGROUP
18
19 entry:
20 ; (省略)
21
22 call switch32
23 db 09Ah ; call far ptr main
24 dd offset main ; オフセット
25 dw seg main ; セグメント
26 db 09Ah ; call far ptr switch16
27 dd offset switch16 ; オフセット
28 dw seg switch16 ; セグメント
29
30 ; (省略)
31
32 mov AX,4C00h
33 int 21h
34
35 ends INIT16
36
37 end entry
38
------------------------------------------------------------------------
次はメインルーチンのコールです。ただのリアルモードのコールなのにいち
いち db でコードを並べていますが、これはさきほどの TASM の便利な機能が
裏目に出て、いらないところに 66h を付けてしまうからなのです。call
switch32 から帰ってきたときには、CS の D bit=1 なのですから、66h はい
りません。
その次の switch16 プロシージャの呼び出しも同じです。これで D bit=0
に戻り、普通にプログラムを終了することができます。
【割り込み】
32-bit コードセグメント実行中に一番気を付けなくてはならないのは割り
込みです。このモードで割り込みが生じるときには、たとえ 32-bit セグメン
トで実行していても、リアルモードと同じ 16-bit の割り込みが発生します。
つまり、FLAGS, CS, IP をスタックに待避し、リニアアドレス 0h から始まる
割り込みベクタテーブルから 16-bit far ポインタをロードして CS:IP にセッ
トします。これがどういう問題を引き起こすのかというと、CS:IP しか変更さ
れないので、EIP の上位 16 ビットがすべて 0 でない場合は、正しく割り込
み処理ルーチンにジャンプしない、ということなのです。
これは結局、割り込みが起きる可能性のある状態では、CS は 64kb の壁を
越えることはできない、ということを意味します。もっとも、このモードで作
られるプログラムというのはそれほどコード領域を消費するとは思えませんか
ら(small モデルでは収まらないという話を聞きますが、それはたいていは図
体の大きい高級言語ライブラリを使っているからです)、あまり大きな問題に
はならないと思われます。どうしても足りない場合は、昔ながらの方法でコー
ドを複数のセグメントに分割することになります。
また、例によって割り込みプログラムも CS の D bit は変更されずにその
まま実行されてしまいます。したがって、32-bit セグメントのプログラムを
実行する場合、リニアアドレス 0h にある既存の割り込みベクタテーブルは、
そのベクタの指す割り込みハンドラはみな 16-bit セグメントのものでしょう
から、そのまま流用することはできません。したがって、32-bit セグメント
用に書かれた割り込みハンドラを指すように書き換える必要があります。しか
し 1Kb の領域を待避して書き換えるというのも大変ですので、ここでは lidt
命令を使うと、割り込みベクタテーブルの位置を移動できるという事実を使う
といいと思います。
【16-bit ルーチン の活用】
BIOS や MS-DOS の int 21h ファンクションは 16-bit セグメント用にでき
ているので、当然のことながら 32-bit セグメントで実行されるプログラムか
らは利用できません。そこで、画面表示からなにからすべてのルーチンを自分
で作る必要があります。
しかし、さきほどの D bit 切り替えルーチンを利用すると、16-bit のサブ
ルーチンも呼び出すことが可能になります。次のプログラムを見てください。
この int16bit プロシージャは、割り込みを起こしたいベクタ番号をスタッ
クにプッシュしてコールすると、16-bit セグメントに切り替えて、その割り
込みを起こしてくれるものです。スタックの幅は 4 bytes でこと、また far
プロシージャであり、D bit=1 を前提にしていることに気を付けて読んでくだ
さい。
________________________________________________________________________
1 segment INIT32 use32 para public 'CODE'
2 assume CS:INIT32, DS:DGROUP
3
4 proc int16bit far
5
6 pushfd
7 cli
8 push EAX
9 mov EAX,[ESP+16]
10 mov [intno],AL
11 lidt [IDT16ptr]
12 call switch16 ; 同じセグメントに far コール
13 db 66h
14 pop EAX
15 db 66h
16 popfd
17
18 db 0CDh ; int
19 intno db ?
20
21 db 66h
22 pushfd
23 cli
24 db 66h
25 push EAX
26 push CS ; 同じセグメントに far コール
27 db 66h
28 call near ptr switch32
29 lidt [IDT32ptr]
30 pop EAX
31 popfd
32 ret ; 自動的に far リターン
33
34 endp int16bit
35
36 ends INIT32
------------------------------------------------------------------------
プロシージャが呼ばれると、まず lidt 命令を用いて割り込みベクタテーブ
ルをリアルモード用に切り替えた後、16-bit セグメントに切り替えます。
int 命令のオペランドは即値しか受け付けないので、このようにコード書き
換えを行って希望するベクタ番号の割り込みを起こしています。このテクニッ
クは C の geninterrupt 関数などで一般的に使われているものです。
割り込みから帰ってきたら、割り込みベクタテーブルを元に戻し、32-bit
セグメントに切り替え、ベクタテーブルを 32-bit 用にした後、終了します。
このルーチンを使えば、いくつかの制限つきながら、実際に int 21h によ
る DOS ファンクションを呼び出すことができます。呼び出すときには、アド
レスサイズは 16-bit でなくてはならないことを常に気を付けなくてはなりま
せん。たとえば、ファンクション 09h で 32-bit データセグメントにある文
字列 msg を標準出力に書き出す場合、
と書いてもよいのですが、この場合は msg はセグメントの始め 64Kb の部分
になくてはなりません。
mov EDX,offset msg
mov AH,9
push 21h
call int16bit
add ESP,4
この制限を満たせない場合や、リニアアドレス 10FFF0h 以上の領域を扱い
たい場合は、そのままコールすることはできません。この状況、特に後者は、
プロテクトメモリに確保したワークエリアにファイルを読み込みたいときなど、
往々にして生じます。この場合は、適当な領域を用いてバッファリングをする
必要があります。面倒ですが、たいして難しいプログラムでもないので、すぐ
に書けることでしょう。
この問題を完全に解決したのがいわゆる DOS エクステンダと呼ばれるもの
で、int 21h ファンクションも違和感なく 32-bit セグメントから呼び出せる
ようになっています。完璧な物を作ろうとするとなかなか面倒です。
【活用例】
実際に僕はこの 386 の動作モードを使って、PC-9801 Ap の 256 色グラフィッ
クモードで 3D ポリゴングラフィックスを行うプログラムを書いています。昨
年の駒場祭で作った 4 人対戦戦車ゲームも 3D ポリゴンでしたが、あれは完
全にリアルモードのプログラムで、プリフィクスの嵐でとても遅い上に、64Kb
の壁のため、セグメントオーバライドや、ポリゴンのワークエリアの確保など
に非常に苦労していました。
でもこのモードでは状況はいっきに好転します。セグメントリミットを 4Gb
に設定し、VRAM を含むすべてのデータを DS を使って基準にアクセスできる
ようにしました。これによってすべての領域をセグメントオーバライドを必要
することなくアクセスできます。また、CS の D bit も 1 にしてあるので、
32-bit コードが何のオーバーヘッドもなく実行できます。
256 色 VRAM はリニアアドレス 0F00000h からパックドピクセル VRAM とし
てアクセスすることができます。また Ap2/As2 以前の機種では、リニアアド
レス 0A8000h から始まるウィンドウを通して、EGC を使った 8 枚同時プレー
ンアクセスも可能となります。これらの領域をセグメントレジスタの変更をす
ることなく透過的にアクセスすることができるので、高速なゲームが作れそう
です。
メモリ管理は、1Mb のひとかたまりのブロックを確保して、そこから自分で
書いたヒープ管理ルーチンで個々に割り振るようにしています。この 1Mb の
ブロックは、デフォルトではリニアアドレス 100000h から始まる 1Mb を確保
するようにしていますが、XMS ドライバとは共存が可能なので、XMS ドライバ
がいるときはこの 1Mb は XMS ドライバからもらうようにしています。
今後の課題としては、やはり仮想 86 モードで実行できないというのは不便
なので、VCPI/DPMI に対応してすべての動作モードから実行できるようにする
ことでしょうか。幸い日本語で VCPI/DPMI 規格を説明している本が発売され
たので、苦労して英語のドキュメントを読む必要はなさそうです。
【参考書】
まず、オーム社の『80386 の使い方』と『80486 の使い方』という本。この 2 冊は書いていることはほとんど同じですので、どちらか 1 冊があればいい でしょう。この本はほんとに 386/486 の使い方だけが、過不足なく書かれて いるので、具体的なプログラムの書き方を知りたい人には不向きかもしれませ ん。もっとも、すでにあらかた理解している人にとってはこのくらいの薄さの ほうがかえって便利でしょう。
アスキー『はじめて読む 486』というのは僕が一番最初に買った本です。 486 なんて書いてありますが 386 でも Pentium でも変わりなく使えます。わ かりやすく書いてありますが、そのぶん分厚くて説明がくどすぎるというのが 難点です。タスク切り替え・ページング・仮想 8086 モードなど、486 の各機 能について動作確認のためのサンプルプログラム(IBM PC/98 に対応)が付属し ており、なかなか参考になります。ただしディスクはついているわけではない ので、試してみたかったら自分で長いリストを打ち込むかディスケットサービ スを申し込まなくてはなりません。
『X86 プロテクトモード・プログラミング』という本が CQ 出版社から 3 月 23 日に出ました。IBM PC/98 でのプロテクトモードプログラムの書き方、 VCPI/DPMI の扱い方など、網羅的に説明しています。さらにサンプルも豊富で、 ディスクもついています。プロテクトモードで動くプログラムを自分で書きた いというひとは、とりあえずこれ 1 冊あればいいでしょう。
本ではありませんが、TSG にはプロテクトモードプログラミングについて相 談に乗れる人が結構います。自力でプロテクトモードを活用する方法や、様々 な小手先のテクニックについては、僕や Applause 君に聞くといいでしょう。
【最後に】
なんか、間違いがたくさんありそうな気がします。理解できないところは直 接僕に質問してください。見つかった間違いは次の部報で訂正することにしま す。
1.Apr.'95 (Sat) 00:36:16
Written by Zephyr
発行者 渡辺 尚貴
編集者 安田 知弘
HTML化 多賀 奈由太
発行所 理論科学グループ
〒153 東京都目黒区駒場 3-8-1 東京大学教養学部内学生会館 305 TEL 03-5454-4343