「z80asm」アセンブラに既知のメモリアドレスに命令を配置させる

Dec 30 2020

私は自作のZ80コンピューター用の非常に基本的なOSを書いています。絶対的なアセンブリ言語の初心者として、メモリの内容を表示し、バイトをRAMにロードできる実用的な「osplusmemorymonitor」を入手することができました。そうすることで、私はいくつかのI / Oデバイスをインターフェイスするためのいくつかの「システムルーチン」を作成しました。たとえば、バイトを読み取り、対応するASCII文字を画面に描画する「Printc」ルーチンがあります。

これは、アセンブラがルーチンの最初のバイトを配置する場所を決定し、同じラベルのjpコマンドに遭遇したときにそのアドレスを使用するため、アセンブラによって作成されたコードで機能します。

ここで、動的にロードされたプログラムからPrintcルーチンを呼び出したいと思います。-lフラグのおかげで、アセンブラがルーチンの最初のバイトをROMのどこに配置したかを知ることができます。これにより、次の内容を含む出力が生成されます。

...
Print:    equ $043a Printc: equ $043e
Readc:    equ $0442 Readline: equ $0446
...

これで、次のようなプログラムを作成できます。

ld a, 0x50     ; ASCII code for P
call 0x043e    ; Calls Printc

このプログラムは文字Pを正常に出力します。メモリアドレスを使用してPrintcルーチンを呼び出しました。

「os」のPrintc宣言の前にあるアセンブリコードを変更しない限り、これは問題ありません。そうすると、Printcラベルが別のアドレスに割り当てられ、既存のプログラムが機能しなくなります。

このタイプの問題の標準的な解決策は何ですか?私の頭に浮かぶ唯一のことは、インポートの前に、システムコールのリストを使用して、アセンブリコードの先頭に「ジャンプテーブル」を作成し、それらが毎回同じアドレスを取得することを期待することです。何かのようなもの:

...
; System routines
Sys_Print:
call Print
ret
Sys_Printc:
call Printc
ret
.... and so on

しかし、これはかなりハックのようです...z80asmルーチンの最初の命令を私が決定したメモリアドレスに配置するようにアセンブラに指示することは可能ですか?

回答

10 Raffzahn Dec 30 2020 at 04:31

このタイプの問題の標準的な解決策は何ですか?

正規の解決策はありませんが、多くのバリエーションがあり、すべて使用可能であることがわかります。

私の頭に浮かぶのは、最初に「ジャンプテーブル」を作成することだけです。

これは完璧な良いものです。ただし、通常は、呼び出しの代わりにジャンプを使用して、コードの長さを短縮し、実行を高速化し、スタックの負荷を軽減します。


JUMP_TABLE:
PRINT    JP  _I_PRINT    ; First Function
READC    JP  _I_READC    ; Second Function
...

しかし、これはかなりハックのようです...

いいえ、多くの8080およびZ80システムはそのように機能します。

先の主なステップは、すべてのエントリポイントが単一の定義された場所とシーケンスにあることです。

ルーチンの最初の命令を私が決定したメモリアドレスに配置するようにz80asmアセンブラに指示することは可能ですか?

確かに、ORGを使用して任意のアドレスに配置します(* 1)。しかし、それはハック的であるか、少なくともあまり前向きではありません。定義されたアドレスにそのようなジャンプテーブルを持つことは素晴らしいスタートです。もちろん、それはいくらかのスペースを消費します。エントリごとに3バイトですが、アドレスは2つだけです。アドレステーブルを作るだけの方がいいのではないでしょうか。お気に入り:

SYS_TABLE:
         DW    _I_PRINT    ; First Function
         DW    _I_READC    ; Second Function

関数の呼び出しは次のようになります

         LD    HL, (SYS_TABLE+0)   ; Load Address of First Function - PRINT
         JP    (HL)                ; Do it

これは、一種の関数セレクターと簡単に組み合わせることができます。

SYS_ENTRY:
         PUSH  HL
         LD    H,0
         LD    L,A
         ADD   HL,HL
         ADD   HL,SYS_TABLE
         JP    (HL)

これで、ジャンプテーブルでさえ必要に応じてROM(またはRAM)内で移動できます。

これを呼び出すには、多くのOSと同様に、関数番号を使用します。関数番号をAに入力し、デフォルトのシステムエントリポイント(SYS_ENTRY)を呼び出すだけです。

         LD    A,0   ; Print
         CALL  SYS_ENTRY

もちろん、OSが関数番号の同等物のセットを提供している場合は、より読みやすくなります:)

これまでのところ、ロードされたプログラムは、テーブルアドレス(SYS_TABLE)またはセレクターのエントリポイント(SYS_ENTRY)のいずれかを知っている必要があります。次のレベルの抽象化では、アドレスを0100hなどの定義された場所に移動します。おそらくJPの形式であるため、OSがROMやRAMのどこにあるかに関係なく、ユーザープログラムは常にその固定アドレス(0100h)を呼び出します。

はい、これがおなじみのようであれば、CP / Mがシステムコールを処理する方法、またはMS-DOSが処理する方法と同じです。

MS-DOSと言えば、よく知られているINT 21hのように、OS関数、いわゆるソフトウェア割り込みを呼び出すための追加の(そしてより一般的な既知の方法)を提供します。そして、Z80(および以前の8080)が提供するものと非常によく似たものがあります:8つの異なるReSTartベクトルのセット(0/8/16 / ...)。再起動0はリセット用に予約されており、他のすべてを使用できます。では、OSに2番目(RST 8h)を使用してみませんか?その場合、関数呼び出しは次のようになります。

         LD    A,0   ; Print
         RST   8h

現在、ユーザープログラムコードは、OS構造やメモリレイアウトから可能な限り分離されています。リロケーターなどは必要ありません。最良の部分は、少し手を加えるだけで、セレクター全体が使用可能な8バイトに収まり、最適なコーディングになることです。


ちょっとした提案:

これらのモデルのいずれかを使用する場合は、OSの最初の関数(0)が、OSに関する情報を提供する呼び出しになることを確認してください。これにより、プログラムは互換性を確認できます。少なくとも2つの基本値を返す必要があります。

  • ABIリリース番号
  • サポートされている最大機能番号。

ABIのリリース番号がまたはバージョン番号と同じであってもなくてもよいが、する必要はありません。APIを変更するたびに増やす必要があります。サポートされている最大関数数とともに、この情報をユーザープログラムで使用して、互換性がない場合に、途中でクラッシュするのではなく、正常に終了することができます。贅沢のために、関数はポインタを

  • のようなOSに関する詳細情報を含む構造
    • 読み取り可能な名前/バージョン
    • さまざまなソースのアドレス
    • 「特別な」エントリポイント
    • RAMサイズなどのマシン情報
    • 利用可能なインターフェースなど。

ただ言って...


* 1-いいえ、一部の人が想定している場合を除いて、ORGはそれ自体でパディングなどを追加してはなりません。そうするアセンブラーは悪い選択です。組織はアドレスレベルのみを変更する必要があり、「ジャンプオーバー」した領域にあるものを定義するべきではありません。そうすることで、潜在的なエラーのレベルが追加される可能性があります-少なくともいくつかの高度なORGの使用が行われるとすぐに-私を信じてください、ORGは複雑な構造を行うときに非常に用途の広いツールです。

さらに、「void」領域をいくつかのパディングで埋めると、このパディングは手つかずのメモリではなくプログラムの一部になり、後のパッチのメインツールである初期化されていないEPROMスペースがなくなります。これらの領域を定義せず、ロードしないことで、クリアされた状態(EPROMの場合はすべて)にとどまり、後でプログラムすることができます。たとえば、デバッグ中にコードを保持したり、ホットフィックスを適用せずにホットフィックスを適用したりできます。新しいデバイスをプログラミングする必要性。

したがって、未定義のメモリは、未定義のメモリである必要があります。その理由のでも早いアセンブラ出力/ローダ形式(だと思うモトローラSRECまたはインテルHEXをROM製造から何かへの番組配信のために使用される)ユーザプログラムへのすべての方法は、領域を除外するための方法を支持しました。

簡単に言えば、それを埋めたいのであれば、それは迅速に行われなければなりません。z80asmはそれを正しく行います。

12 WillHartung Dec 30 2020 at 04:55

特にZ80ASMの問題は、アセンブリ入力を受け取り、静的バイナリファイルを吐き出すことです。これは良いことでも悪いことでもあります。

「通常の」システムでは、アドレスの割り当ては必然的に、アセンブラではなくリンカの責任になります。しかし、アセンブラは非常に単純なので、多くの人がビルドサイクルのその側面をスキップします。

Z80ASMは、「オブジェクト」ファイルではなく、文字通りのバイナリイメージを吐き出すため、リンカーは必要ありません。しかし、それはあなたがやりたいことを必ずしもあなたにさせないでしょう。

ユビキタスなORGディレクティブについて考えてみましょう。

ORGは、次のアセンブリコードの開始(オリジン-したがってORG)アドレスが何であるかをアセンブラに通知します。

これは、これを行うと次のことを意味します。

    ORG 0x100
L1: jp L1

アセンブラは、JUMPへのJP命令を0x100(L1)のアドレスにアセンブルします。

しかし、バイナリファイルを吐き出すと、ファイルはわずか3バイトになります。ジャンプ命令の後に、バイナリ形式で0x100が続きます。このファイルには、「動作」するために0x100でロードする必要があることを示すものは何もありません。その情報が欠落しています。

もし、するなら:

    ORG 0x100
L1: jp L2

    ORG 0x200
L2: jp L1

これにより、6バイトの長さのファイルが生成されます。これらの2つのJP命令を次々に配置します。ORGステートメントが実行しているのは、ラベルがどうあるべきかを指示することだけです。これはあなたが期待するものではありません。

したがって、ファイルにORGを追加するだけでは、コードを配置したい特定の場所にコードをロードする別の方法がない限り、実行したいことは実行されません。

箱から出してZ80ASMでこれを行う唯一の方法は、出力ファイルにバイトのブロックと空のスペースを埋めることです。これにより、バイナリがいっぱいになり、コードが適切な場所に配置されます。

通常、これはリンカーがあなたのために行うことです。リンカの仕事は、異種のコードを取得して、結果のバイナリイメージを作成することです。それはあなたのためにこれをすべて行います。

リンカを使用しなかった私のアセンブラでは、データの各ブロックの実際のアドレスを含むIntelHEXファイル形式が生成されました。

したがって、前の例では、2つのレコードが作成されます。1つは0x100に、もう1つは0x200に向けられており、16進ロードプログラムは物事を適切な場所に配置します。これは別の選択肢ですが、Z80ASMもそれをサポートしていないようです。

そう。

Z80ASMは、たとえば任意に0x1000から始まるROMイメージを作成する場合に最適です。それをORGし、結果のバイナリを取得して、書き込まれたファイル全体をEPROMにダウンロードします。そのために最適です。

しかし、やりたいことについては、ルーチンを適切な場所に移動するためにコードをパディングするか、これを明示するために他のローダースキームを考え出す必要があります。

5 GeorgePhillips Dec 30 2020 at 03:16

orgディレクティブは、あなたが求めるものを具体的に行う必要があります。ただし、z80asmは、出力形式が少し単純化されています。代わりにds、特定のアドレスにルーチンを配置するために使用できます。

        ds     0x1000
printc:
        ...
        ret

        ds     0x1100-$
readc:
        ...
        ret

これは常にprintc0x1000とreadc0x1100に配置されます。多くの欠点があります。必要があるprintc0x100をより大きく成長プログラムが組み立てないであろうと、あなたは破るために必要がありますprintcいくつかの方法で離れてどこか他の余分なコードを置きます。そのため、およびその他の理由で、メモリ内の固定位置にあるジャンプテーブルは、管理が簡単で柔軟性があります。

           ds    0x100
v_printc:  jp    printc
v_readc:   jp    readc
           ...

もう1つの手法は、単一のエントリポイントを使用し、Aレジスタの値を使用して関数を選択することです。これは少なくとも少し遅くなりますが、オペレーティングシステムの変更時に維持する必要があるエントリポイントは1つだけであることを意味します。

またCALL、エントリポイントに対してを実行する代わりに、メモリ位置0x18へのシングルバイト呼び出しとしてRST使用できる特別な場所(0、8、0x10、0x18、0x20、0x28、0x30、0x38)RST 0x18の1つに配置します。通常RST 0、およびRST 0x38は回避されます。これは、それらがそれぞれpwoer-onエントリポイントおよび割り込みモデル1ハンドラの場所であるためです。