アセンブラーにはロケーション・カウンターという仕組みがあります。アセンブラー言語でプログラムを書く際は、B ERROR、ST R1,AREA2のように命令やデータ領域を名前で指し示すことができます。本来ならば B 324(,R12)、ST R1,248(,R12) のように、分岐先の命令やデータ領域のアドレスをベース・レジスター番号と変位もしくはベース・レジスター番号とインデックス・レジスター番号および変位の組み合わせで書かなければなりません。
しかしながら、ベース・レジスターやインデックス・レジスターの番号はともかくとして、変位に関しては簡単には求められません。参照しようとしているデータ領域が、プログラムやテーブルの先頭から何バイト離れているところにあるのかを正確に求めることはとても大変です。プログラムの先頭から書いた1つ1つの命令の長さを積み上げ、境界調整などが入っていればそこでのアドレスのずれを調整し、データ領域なら変数や定数の長さなどを正確に計算して行かなければなりません。作業自体も大変ですが、一番の問題は間違えやすいということでしょう。プログラムの先頭に近い命令の変位計算で間違えれば、そこから後ろ、つまりそのプログラムで書いた命令やデータ領域のほとんどの変位も間違ってしまいます。こうなるとアセンブラー言語は難しいとか、嫌いとかではなく、そもそも使い物にならなくなってしまいます。
それ故、アセンブラー言語では命令やデータ領域を機械命令が規定する直接の形式(ベース・レジスター番号と変位の組み合わせ)だけでなく、記号(名前)で指定できるようになっているのです。しかし、最終的なオブジェクト・モジュールにする際は、記号が示す命令やデータ領域の位置を機械命令が規定するベース・レジスター番号と変位の組み合わせに変換しなければなりません。正確な変位を計算するために使用されるのがロケーション・カウンターです。
プログラム内の各モジュールは、セクションという単位に区分され、命令やデータの集合体はCSECT(制御セクション)、データ領域のレイアウト定義はDSECT(見かけセクション)と呼ばれます。一般的にはソース・コードの最初のCSECTがプログラム・モジュールの先頭になり、アセンブラーの内部カウンターであるロケーション・カウンターも最初のCSECTが先頭(0)になります。LRなどの2バイト命令を1つ書けばロケーション・カウンターは2つ進み、STやAなどの4バイト命令を1つ書けばロケーション・カウンターは4つ進みます。F型定数を1つ書けばロケーション・カウンターは4つ進み、境界調整が入れば4バイト・バウンダリーに合わせるためのずれの分だけ1~3つ進みます。20バイトの文字列領域を定義すればロケーション・カウンターは20進みます。このようにして、プログラム中に記述された各々の命令やデータ領域がセクションの先頭からどの位離れているかの離れ度合いをロケーション・カウンターによって管理します。
上記に述べたように、アセンブラー言語でもCOBOLやPL/Iなどと同じように命令やデータ領域を記号(名前)で示すことができます。しかし、アセンブラー言語では名前ではなく変位(ロケーション・カウンター)の値自体を積極的に参照して命令やデータを定義したい場合もあります。その際に使用されるのが記号文字 * です。
*は、現在のロケーション・カウンターの値を示します。今自分がいる位置から何バイト先といった表現をしたい場合や、今自分がいる位置からテーブルの先頭位置を引き算してテーブル領域の長さを求めたいといったことが簡単にかつ正確に記述できます。
*によって、ベース・アドレスを示す
1 2 3 4 5 6 7 |
----+----1----+----2----+----3----+----4----+----5----+----6----+----7-- PROGNAME CSECT , DEFINE CONTROL SECTION USING *,R12 DEFINE BASE REGISTER STM R14,R12,12(R13) SAVE CALLER REGISTERS LR R12,R15 GR12 --> OUR 1ST BASE ADDRESS LR R15,R13 SAVE CALLER SAVEAREA : |
*記号の使用例の最も代表的なものの1つです。CSECTの直後の*は、プログラム(CSECT)の先頭を示します。つまり、プログラムからの変位0の位置を示すことになります。USING *,12 は、レジスター12番をUSING命令を書いた位置のベース・アドレスを示すベース・レジスターにするという定義です。プログラムの先頭でベース・レジスターを定義する場合は、プログラム名の代わりに*を指定できます。
1 2 3 4 5 6 7 |
----+----1----+----2----+----3----+----4----+----5----+----6----+----7-- PROGNAME CSECT , DEFINE CONTROL SECTION USING PROGNAME,R12 DEFINE BASE REGISTER STM R14,R12,12(R13) SAVE CALLER REGISTERS LR R12,R15 GR12 --> OUR 1ST BASE ADDRESS LR R15,R13 SAVE CALLER SAVEAREA : |
上記と全く同じプログラム・コードを*記号を使わないで書き換えたものです。PROGNAMEを変更する場合は、2箇所を同時に直す必要があります。2箇所でも3箇所でもエディターのChangeコマンドで簡単にできるとは考えないで下さい。将来の保守を考えて修正箇所をなるべく少なくするという意味では、記号*を使用する方がベターです。
1 2 3 4 5 6 7 8 |
----+----1----+----2----+----3----+----4----+----5----+----6----+----7-- &PGMNAME SETC 'PROGNAME' &PGMNAME CSECT , DEFINE CONTROL SECTION USING &PGMNAME,R12 DEFINE BASE REGISTER STM R14,R12,12(R13) SAVE CALLER REGISTERS LR R12,R15 GR12 --> OUR 1ST BASE ADDRESS LR R15,R13 SAVE CALLER SAVEAREA : |
同じ名前を繰り返し記述することを避ける方法として、名前をそのものを直接記述するのではなく可変記号で記述して、実際の値(この場合はプログラム名)をSETC命令で可変記号に設定するという方法もあります。このようなコーディング方法も保守性を高める手段の1つですが、この記事は記号*を活用するということなので、あくまでもアドレス位置の指定に*記号を使うことでの説明を続けます。どのように書いてもわかりやすく、間違えにくく、保守性も高いというコーディングをすればいいのです。そのためにもいろいろな書き方があること知っておくといいです。
1 2 3 4 5 6 7 |
----+----1----+----2----+----3----+----4----+----5----+----6----+----7-- PROGNAME CSECT , DEFINE CONTROL SECTION STM R14,R12,12(R13) SAVE CALLER REGISTERS BALR R12,0 LOAD OUR BASE ADDRESS --> R12 USING *,R12 DEFINE BASE REGISTER LR R15,R13 SAVE CALLER SAVEAREA : |
比較的古いプログラムに多い書き方ですが、GR15に格納されている入口点アドレスを使わずにBALR命令で自分自身のメモリー上のアドレスをロードする方法です。この場合、ベース・アドレスはプログラムの先頭ではなくプログラムの先頭+6番地になります。STM命令で4バイト、BALR命令で2バイトの合わせて6バイト分の命令コードが埋め込まれるからです。BALR命令の結果、BALR命令の次の命令アドレスがGR12に入ります。したがって、プログラムのベース・アドレスも先頭から6バイト離れた位置にUSING命令で設定する必要があります。
1 2 3 4 5 6 7 8 |
----+----1----+----2----+----3----+----4----+----5----+----6----+----7-- PROGNAME CSECT , DEFINE CONTROL SECTION USING PROGNAME+6,R12 DEFINE BASE REGISTER 誤りではないが、このようなUSING命令は基本的に書かない。 STM R14,R12,12(R13) SAVE CALLER REGISTERS BALR R12,0 LOAD OUR BASE ADDRESS --> R12 LR R15,R13 SAVE CALLER SAVEAREA : |
「CSECTの直後にUSINGを書く」と固定化して考えてしまうと上記のような定石とはかけ離れたコーディングをすることになります。誤りではありませんが、わかりにくく、次にそのプログラムをメンテナンスする人が不思議がることになります。1つ前の例のように、BALR命令の次に*記号を使ったUSING命令を書く方が自然です。ベース・レジスターにはBALR命令の次のアドレスが読み込まれるので、その位置をベース・アドレスに設定するUSING命令を書くわけです。その際にロケーション・カウンターの値を示す*記号を使えば、プログラムの先頭をベース・アドレスにする場合でも少し離れた位置をベース・アドレスにする場合でも同じコーディング方法でUSING命令を書くことができます。記号*は、今現在の自分自身の位置(変位)を示すということを知っていれば、アセンブラー・プログラムのコーディングの様々な場面で応用できます。
命令列内のデータ領域を迂回する
1 2 3 4 5 6 7 8 9 10 11 12 |
----+----1----+----2----+----3----+----4----+----5----+----6----+----7-- PROGNAME CSECT , DEFINE CONTROL SECTION USING *,R12 DEFINE BASE REGISTER STM R14,R12,12(R13) SAVE CALLER REGISTERS LR R12,R15 GR12 --> OUR 1ST BASE ADDRESS LR R15,R13 SAVE CALLER SAVEAREA CNOP 0,4 INSURE FULL WORD BOUNDARY BAS R13,*+4+72 AROUND OUR SAVEAREA DC 18F'-1' OUR GPR SAVEAREA ST R15,4(,R13) SAVE CALLER SAVEAREA POINTER ST R13,8(,R15) SET BACK CHAIN FOR SA TRACE : |
レジスター保管域をハウス・キーピングのコードの中に定義した例です。レジスター保管域のように実行中に参照することがないデータ領域にいちいち名前を付けるのも面倒です。筆者自身は好んで使っていましたが、間違いの元だからきちんとラベルを付けてジャンプするようにと言う人もいるでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
----+----1----+----2----+----3----+----4----+----5----+----6----+----7-- PROGNAME CSECT , DEFINE CONTROL SECTION USING *,R12 DEFINE BASE REGISTER STM R14,R12,12(R13) SAVE CALLER REGISTERS LR R12,R15 GR12 --> OUR 1ST BASE ADDRESS LR R15,R13 SAVE CALLER SAVEAREA CNOP 0,4 INSURE FULL WORD BOUNDARY BAS R13,CHAINSA AROUND OUR SAVEAREA DC 18F'-1' OUR GPR SAVEAREA CHAINSA DS 0H ST R15,4(,R13) SAVE CALLER SAVEAREA POINTER ST R13,8(,R15) SET BACK CHAIN FOR SA TRACE : |
きちんとラベルを付けるとこんな感じです。個人的には好きになれない書き方です(あくまでも面倒という理由です)。レジスター保管域の長さなどは基本的に変わらないから、飛び先の変位を直接書いても問題ないと思います。分岐先を *+4+72 のように書くことは、元々はOSのマクロ命令から習ったことです。OSでもやってるんだから、まぁいいよねと。もし、誰かに突っ込まれてもOSのマクロでもこういうコーディングしてますと返せます。OSでもやってると言うとたいていの人は反論しません。でも、OSはこういう理由でやっている、アプリケーションはこういう理由からそのような方法は採らないと説明されれば素直に従いましょう。そのような説明ができる人ならエンジニア、プログラマーとして信頼できるでしょう。
1 2 3 4 5 6 7 8 9 10 |
----+----1----+----2----+----3----+----4----+----5----+----6----+----7-- 48 OPEN (SYSPRINT,OUTPUT) OPEN THE DATASET 49+ CNOP 0,4 ALIGN LIST TO FULLWORD 01-OPEN 50+ BAL 1,*+8 LOAD REG1 W/LIST ADDR. @L2A 01-OPEN 51+ DC AL1(143) OPTION BYTE 01-OPEN 52+ DC AL3(SYSPRINT) DCB ADDRESS 01-OPEN 53+ SVC 19 ISSUE OPEN SVC 01-OPEN 54 LTR R15,R15 FUNCTION SUCCESSFUL ? 55 BNZ OPENERRO DO OPEN ERROR PROCESSING... : |
OPENマクロの展開形です。SVC呼び出しのパラメーター領域を、BAL *+8で8バイト先に(BAL命令が4バイト、パラメーターが4バイト)分岐して命令列中のパラメーター領域を迂回しています。昔これを見て *と+n の組み合わせで飛び先の変位を直接書いてしまう方法を知りました。もちろん、迂回するロジック内の命令やデータがしょっちゅう増えたり減ったりする場合は使えませんが、1度コードを書けば基本的に変わらない部分に*記号で変位を直接書くことはOSでもやっているコーディング・テクニックなのだと学んだわけです。
データ領域の長さや個数の計算に使う
1 2 3 4 5 6 7 8 9 10 11 12 |
----+----1----+----2----+----3----+----4----+----5----+----6----+----7-- TABLE DS 0F DATA TABLE ITEMID DC AL1(0) ITEM NUMBER CODE DC AL2(0) ITEM CODE DC XL1'00' (FILLER) NAME DC CL16' ' ITEM NAME ADDR DC CL50' ' ITEM PLACE PHONE DC P'99988887777' PHONE NUMBER TABENTLN EQU *-TABLE TABLE ENTRY LENGTH TABENT# EQU 256 NUMBER OF TABLE ENTRIES DC (TABENTLN*(TABENT#-1))X'00' (REMAINING ENTRIES SPACE) ENDTABLE DC A(*) ADDRESS OF END OF TABLE |
データ領域の定義でも、現在位置を示す記号*はいろいろな目的に使えます。テーブル構造の領域を定義するような際、テーブルを構成するエントリーの長さは、計算しなくても*記号とテーブルの先頭を示す名前の引き算の式で定義できます。この例では、TABENTLN EQU *-TABLEの部分です。なお、DC (TABENTLN*(TABENT#-1))X’00’における*は、位置を示す記号ではなく掛け算を示す記号です。
1 2 3 4 5 6 7 8 9 10 |
----+----1----+----2----+----3----+----4----+----5----+----6----+----7-- RECORDLN EQU 256 RECORD LENGTH RECORD DS 0F RECORD AREA ITEMID DC AL1(0) ITEM NUMBER CODE DC AL2(0) ITEM CODE DC XL1'00' (FILLER) NAME DC CL16' ' ITEM NAME ADDR DC CL50' ' ITEM PLACE PHONE DC P'99988887777' PHONE NUMBER DC (RECORD+RECORDLN-*)X'00' --- RESERVED SPACE --- |
同じような領域の定義ですが、こちらはレコードのレイアウト定義です。レコードの長さは256バイトとして決めてあります。先頭から必要な項目を並べていき、残りを予約領域として定義するような場合、残りが何バイトあるかをいちいち計算する必要はありません。DC (RECORD+RECORDLN-*)X’00’のように、最後のフィールドの次の位置からRECORDの先頭位置を引き算してそれをレコード長から引けば残りのバイト数が計算できます。それを1バイト領域の複写因数にすれば残り領域を正確に埋めることができます。
アセンブラー言語でのデータ領域の定義では、記号*を使う使わないに関わらず、長さや個数などは直接自分で計算して書くのではなく式を使ってアセンブラーに計算させることを覚えましょう。EXCELでの表計算のように、1箇所直せば連動して他の関連する部分も自動計算されるようにしておくと将来のメンテナンスも楽ですし何より正確です。人間は単純な足し算引き算でも間違えます。
配列内の連続したデータの定義に使う
1 2 |
ARRAY DS 0C DC 256AL1(*-ARRAY) 256BYTES ARRAY AREA |
上記のように定義すると、x00からxFFまで1ずつ増やした1バイトの値を256個並べた配列領域を定義できます。実際にアセンブルすると以下のようになります。
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 31 32 33 34 |
138 PRINT DATA 000100 139 ARRAY DS 0C 000100 0001020304050607 140 DC 256AL1(*-ARRAY) 256BYTES ARRAY AREA 000108 08090A0B0C0D0E0F 000110 1011121314151617 000118 18191A1B1C1D1E1F 000120 2021222324252627 000128 28292A2B2C2D2E2F 000130 3031323334353637 000138 38393A3B3C3D3E3F 000140 4041424344454647 000148 48494A4B4C4D4E4F 000150 5051525354555657 000158 58595A5B5C5D5E5F 000160 6061626364656667 000168 68696A6B6C6D6E6F 000170 7071727374757677 000178 78797A7B7C7D7E7F 000180 8081828384858687 000188 88898A8B8C8D8E8F 000190 9091929394959697 000198 98999A9B9C9D9E9F 0001A0 A0A1A2A3A4A5A6A7 0001A8 A8A9AAABACADAEAF 0001B0 B0B1B2B3B4B5B6B7 0001B8 B8B9BABBBCBDBEBF 0001C0 C0C1C2C3C4C5C6C7 0001C8 C8C9CACBCCCDCECF 0001D0 D0D1D2D3D4D5D6D7 0001D8 D8D9DADBDCDDDEDF 0001E0 E0E1E2E3E4E5E6E7 0001E8 E8E9EAEBECEDEEEF 0001F0 F0F1F2F3F4F5F6F7 0001F8 F8F9FAFBFCFDFEFF |
1 2 3 4 |
ALPHA DS 0C DC 9AL1(193+*-ALPHA) DEFINE A-I DC 9AL1(209-9+*-ALPHA) DEFINE J-R DC 8AL1(226-18+*-ALPHA) DEFINE S-Z |
上記のように定義すると、xC1からxC9、xD1からxD9、xE2からxE9までの1バイトの値を26個並べた配列領域を定義できます。これはEBCDICコードの文字AからZを示します。文字AはEBCDICコードでxC1(193)、文字JはEBCDICコードでxD1(209)、文字SはEBCDICコードでxE2(226)であることを応用しています。変位の位置も実際にアセンブルすると以下のようになります。
1 2 3 4 5 6 |
000200 143 ALPHA DS 0C 000200 C1C2C3C4C5C6C7C8 144 DC 9AL1(193+*-ALPHA) DEFINE A-I 000208 C9 000209 D1D2D3D4D5D6D7D8 145 DC 9AL1(209-9+*-ALPHA) DEFINE J-R 000211 D9 000212 E2E3E4E5E6E7E8E9 146 DC 8AL1(226-18+*-ALPHA) DEFINE S-Z |
ARRAYもALPHAも、それぞれの領域内の定数の長さは1ですから(複写因数が個数を示す、1バイトが何個という定義をしている)、*が示す変位も1ずつ増えます。ALPHAやARRAYが示すデータ領域の先頭の位置は変わりませんから、記号*が示すロケーション・カウンターの値は1ずつ増えていくことを利用したデータ定義の例です。