矢沢久雄 グレープシティ アドバイザリースタッフ

 皆さんが普段お使いのゲームやワープロなどのプログラムは,多くの機能を持っています。このようなプログラムの内部は,機能ごとに細分化されています。ある程度大きなプログラムは,複数の小さなプログラムの集合体として作成されるのが一般的です。

 小さなプログラムの中には,プログラムの実行開始位置となる「親分」のようなものがあり,これを「主プログラム(またはメイン・ルーチン)」と呼びます。その他のプログラムは「子分」のようなものであり,「副プログラム(またはサブ・ルーチン)」と呼ばれます。主プログラムが副プログラムの機能を呼び出す(プログラムの流れを副プログラムに移す)わけです。それをアセンブラでどのように実現するのかを説明しましょう。今回から,かなり本格的なプログラムが登場しますが,細かいことは気にせずに,雰囲気だけをつかんでいただければOKです。

●主プログラムと副プログラム

 主プログラムから副プログラムにプログラムの流れを移すには,CALL命令が使われます。副プログラムの処理が終わって主プログラムにプログラムの流れを戻すには,RET命令が使われます。CALLは「呼び出す」という意味です。RET(RETurn)は「戻る」という意味です。

 副プログラムの先頭には,その位置(メモリー・アドレス)を表すラベルを付けます。CALL命令のオペコードには,副プログラムのラベルを指定します。CALL命令を実行すると,そのラベルのメモリー・アドレスがPCレジスタに自動的に設定されるので,プログラムの流れが副プログラムに移ります。

図1●CALL命令とRET命令の仕組み
 副プログラムの最後にRET命令を置きます。RET命令には,オペコードを指定しません。それでも主プログラムに戻れるのは,主プログラムがCALL命令を実行したときに,その次の行のメモリー・アドレスがスタックに自動的にPUSHされるからです。副プログラムがRET命令を実行するとPOP命令が自動的に実行されて,主プログラムのメモリー・アドレスが取り出され,それがPCレジスタに自動的に設定されます(図1[拡大表示])。

●データの受け渡し方法

 主プログラムから副プログラムにデータを渡したり,副プログラムから主プログラムにデータを返す場合があります。例えば,主プログラムで入力したデータを副プログラムで演算して,演算結果を返す場合などです。データの受け渡しには,大きく分けて3つの方法があります。

 1つ目の方法は,CPUの持つレジスタを使うことです。例えば,主プログラムでGR1レジスタとGR2レジスタにデータを格納し,それらを副プログラムで演算した結果をGR3レジスタに格納し,それを主プログラムに返すことができます。この方法は,最も簡単ですが,使用できるレジスタの数に制限があるので(GR0~GR7の8個だけ),多くのデータを受け渡す場合には使えません。

 2つ目の方法は,DS命令で確保された特定のメモリー領域を使うことです。主プログラムで特定のメモリー領域にデータを格納し,それを副プログラムで読み出します。この方法なら多くのデータを渡すことができますが,そのためのメモリー領域を確保しておくのがちょっと面倒です。したがって,あまり使われません。

 3つ目の方法は,スタックを使うことです。主プログラムでPUSH命令を使ってデータをスタック領域に格納します。副プログラムでPOP命令を使ってデータを取り出します。この方法は,最も多用されます。必要な数だけデータをPUSHして,同じ数だけPOPすればよいからです。

 リスト1は,スタックを使って主プログラムから副プログラムに2つのデータ(123と456)を渡し,それらの値を副プログラムで加算し,その結果をスタックを使って主プログラムに返すプログラムです。スタックに123,456の順でPUSHすると,456,123の順でPOPされます。PUSHとPOPが逆順になるのは,スタックの特徴です。副プログラムから返された加算結果は,主プログラムでGR3レジスタにPOPされています。その後でGR3の値をどう処理するかは省略しています。

リスト1●主プログラムと副プログラムのデータの受け渡し

   LAD  GR1,123  ;GR1に123を格納する(主プログラムの先頭) 
   PUSH  0,GR1  ;123をスタックに格納する 
   LAD  GR2,456  ;GR2に456を格納する 
   PUSH  0,GR2  ;456をスタックに格納する 
   CALL  SUB1  ;副プログラムSUB1を呼び出す 
   POP  GR3  ;加算結果をスタックからGR3に取り出す
     :  ;これ以降の処理は省略する 
 SUB1  POP  GR4  ;戻り番地を逃がしておく 
   POP  GR1  ;GR1に456を取り出す(副プログラムの先頭) 
   POP  GR2  ;GR2に123を取り出す 
   ADDA  GR1,GR2  ;GR1とGR2の加算結果をGR1に格納する 
   PUSH  0,GR1  ;加算結果をスタックに格納する 
   PUSH  0,GR4  ;戻り番地を元通りにする 
   RET    ;主プログラムに戻る(副プログラムの末尾) 

●値渡しと参照渡し

 文字列のような大きなデータを主プログラムから副プログラムに渡すには,どうしたらよいと思いますか? 文字列とは,複数の文字が並んだものです。CASL IIでは,16ビットのメモリー領域1つに1文字を格納します。100文字の文字列があったとしましょう。レジスタは100個もありませんから,レジスタを使った方法では文字列を渡せません。1文字ずつ全部で100個のラベルを付ける方法(特定のメモリー領域を使う方法)も面倒です。100回PUSHして100回POPするのも面倒です。

 このような場合には「文字列の先頭のメモリー・アドレス」と「文字列の長さ」という2つの情報を副プログラムに渡すのです。これなら,レジスタ,特定のメモリー領域,スタックのどの方法を使っても,文字列を副プログラムに渡すことができます。副プログラムでは,文字列の先頭のメモリー・アドレスから,文字列の長さだけ繰り返して1文字ずつ取り出して処理します。主プログラムから副プログラムに渡すデータは,計算に使われる数値だけではありません。データが格納されているメモリー・アドレスを渡すこともできます。計算に使われる数値を渡すことを「値渡し」と呼び,メモリー・アドレスを渡すことを「参照渡し」と呼びます。文字列の場合は,文字列の先頭を参照渡しにして,文字列の長さを値渡しにするわけです。

 リスト2は,主プログラムのSTRというラベル以降に格納された5文字の文字列helloを副プログラムに渡して,大文字のHELLOに変換するプログラムです。主プログラムでは,GR1レジスタに”hello”の先頭のメモリー・アドレス(すなわちSTR)を格納し,GR2レジスタに文字列の長さである5を格納しています。これらの情報を受け取った副プログラムでは,GR2レジスタの値だけ繰り返して1文字ずつ取り出し,それを小文字から大文字に変換しています。CASL IIの文字コード(文字の種類を表す数値)は,小文字より大文字が32だけ小さいので,個々の文字データから32を引くことで大文字に変換できます。何度もくどいようですが,プログラムの雰囲気だけをつかんでください。

リスト2●主プログラムから副プログラムに文字列を渡す

   LAD  GR1, STR  ;文字列の先頭のメモリー・アドレスをGR1に格納する 
   LAD  GR2, 5  ;文字列の長さをGR2に格納する 
   CALL  SUB1  ;副プログラムSUB1を呼び出す 
   :    ;これ以降の処理は省略する 
 STR  DC  'hello'  ;文字列データを格納しておく 
   :     
 SUB1  LD  GR3, 0, GR1  ;1文字をGR3に取り出す(副プログラムの先頭) 
   SUBA  GR3, 32  ;小文字を大文字に変換する 
   ST  GR1, 0, GR3  ;変換後の大文字を格納する 
   SUBA  GR2, 1  ;GR2から1を引く 
   JZE  SUB2  ;GR2が0になったら繰り返しを終了する 
   ADDA  GR1, 1  ;GR1に1を加える(次の文字のメモリー・アドレス) 
   JMP  SUB1  ;SUB1に戻って繰り返す 
 SUB2  RET    ;主プログラムに戻る 

 プログラムは,命令とデータの集合体です。コンピュータは,あらゆる情報を数値で表します。命令もデータも,マシン語に変換された後は数値になっています。数値で表されたデータが表しているものには,2通りの可能性があります。そのまま演算に使われる数値か,メモリー・アドレスのいずれかです。例えば123というデータがあったとしましょう。123は,123人や123円のような演算に使われる数値か,123番地というメモリー・アドレスのいずれかなのです。この考え方は,アセンブラに限らず,あらゆるプログラミング言語でも同じです。

 次回は,スーパーバイザ・コールを使って,キー入力やディスプレイ出力を実現する方法を説明します。どうぞお楽しみに!