- 「メインフレーム・コンピューター」で遊ぼう - http://www.arteceed.net -

記号*を活用する


シンボル(記号)*を活用する:レベル1タイトル&#9312


アセンブラーにはロケーション・カウンターという仕組みがあります。アセンブラー言語でプログラムを書く際は、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--
    PROGNAME CSECT ,                        DEFINE CODE 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--
    PROGNAME CSECT ,                        DEFINE CODE 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--
    &PGMNAME SETC  'PROGNAME'
    &PGMNAME CSECT ,                        DEFINE CODE 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--
    PROGNAME CSECT ,                        DEFINE CODE 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--
    PROGNAME CSECT ,                        DEFINE CODE 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--
    PROGNAME CSECT ,                        DEFINE CODE 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--
    PROGNAME CSECT ,                        DEFINE CODE 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--
    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の組み合わせで飛び先の変位を直接書いてしまう方法を知りました。もちろん迂回するロジック内の命令やデータがしょっちゅう増えたり減ったりする場合は使えませんが、一度コードを書けば基本的に変わらない部分に*記号で変位を直接書くことは、OSでもやっているコーディング・テクニックなのだと学んだわけです。


データ領域の長さや個数の計算に使う

    ----+----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--
    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箇所直せば連動して関連する部分も自動計算されるようにしておくと将来のメンテナンスも楽ですし何より正確です。人間は単純な足し算引き算でも間違えます。


配列内の連続したデータの定義に使う

    ARRAY    DS    0C
             DC    256AL1(*-ARRAY)          256BYTES ARRAY AREA
    

上記のように定義すると、x00からxFFまで1ずつ増やした1バイトの値を256個並べた配列領域を定義できます。実際にアセンブルすると以下のようになります。

                              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
    
    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)であることを応用しています。変位の位置も実際にアセンブルすると以下のようになります。

    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ずつ増えていくことを利用したデータ定義の例です。