トラ技BIOSのファンクションコールのしくみ

平成20年7月26日

    トラ技BIOSを使うにはヘッダファイル trgbios.h をインクルードしますが、このヘッダファイルには次のようなマクロのようなものが定義されています。

      // USBをオープンする
      #define trg_usbopen() \
          (( void (*)(               ) ) ( 0x18C0 )) \
          ( )
       
      // USBをクローズする
      #define trg_usbclose() \
          (( void (*)(               ) ) ( 0x18C4 )) \
          ( )
       

    このページではこの怪しげなマクロについて解説します。

これは何?

    これは、関数のような形式をしたC言語のマクロです。

    void (*)( )型という関数ポインタをイメージしてください。関数ポインタというのは、C言語から任意の番地をコールするためのしくみです。trg_usbopenというマクロは、0x18C0という数値を、関数ポインタにキャストし、それを呼び出します。

    つまり、0x18C0という値を指し示す関数ポインタを作って、それを呼び出しているわけです。

どのように実行されるの?

    trg_usbopen()を実行すると、call !018C0Hという命令コードが置かれます。

    18C0番地に何が書かれているかについては、付録CD-ROM内にあるトラ技BIOSのソースファイル中にある"BIOSstart.asm"の157行目を参照してください。

      TRGOPEN     CSEG        AT      18C0H
              br    !_usbOpen
              nop 
       

    このコードを解説すると、

      ・TRGIOPENはただの名前です。
      ・CSEGは、このコードをコードセグメントに配置することを示しています。
      ・AT 18C0Hは、このコードを絶対番地の18C0に配置することを指示しています。

    これをアセンブルすると、絶対番地の18C0に、br命令とnop命令が配置されます。このコードを実行すると、CPUは_usbOpenというラベルで示されたアドレスへジャンプします。

    _usbOpenというのはC言語で書いたusbOpen()という関数のアドレスを指します。一般に、C言語で書いたグローバルな名前は、アセンブラでは頭に_つけることでアクセスすることができます。

    usbOpen()という関数は、トラ技BIOSの本体のソースコードで定義されています。

      void usbOpen(void)
      {
          // すでにポートが開かれていれば何もしない
          if(usb_connected()) return;
       
          svUSB_comportopen();
      }
       

    このようにして、trg_usbopen()を実行すると、トラ技BIOSの中のusbOpen()関数が呼び出されるわけです。

    注目すべき点は、usbOpenという関数がどこのアドレスに配置されるかを知る必要がないという点です。

なんでこんな面倒なことをしているの?

    ここでは、2つのテクニックを使いました。

     @ 固定のアドレスに、サービスコールの入り口を作ったこと。
     A その固定の入り口をコールするコードをC言語に埋め込むこと。

    その理由は、トラ技BIOSのようにROM内に固定されたルーチンと、ユーザアプリのようにRAM上のアプリをリンクするためです。

    本来は、78Kマイコンのアプリケーションを開発する場合、ブートコードとアプリケーションを一緒にコンパイルしてリンクします。ブートコードが用意する各種サービス関数のアドレスがわかっているので、アプリケーションからそのまま名前でアクセスできます。もし機能を拡張したりして、サービス関数のアドレスが変わってしまっても一緒にリンクするのですから問題はありません。

    しかし、付録基板では、トラ技BIOSを後から機能拡張したりすることもありうるので、各種サービス関数が配置されるアドレスは、変わるかもしれません。そのため、アセンブラを使って固定のアドレスに入り口を作る必要があります。

    ところが、78K0のリンカには、関数の呼び出し先アドレスを指定するしくみがありません。
    (H8などでGCCを使う場合にはリンカスクリプトで指定できます)

    そのため、このような「数値を関数ポインタにキャストする」というトリッキーなことをしています。

 

引数を取る場合は?

    trg_puts()を例として紹介します。 

      // USBに文字列を送信する
      #define trg_puts(SRC) \
          (( void (*)(const char *src) ) ( 0x18C8 )) \
          (SRC)
       

    このマクロは、void (*)(const char *src)型の関数ポインタをつくり、その関数ポインタが絶対アドレス0x18C8を指すようにキャストし、さらにそれをコールしています。

    最後の行の(SRC)というのが、引数にSRCを積んでコールすることを指示しています。

    78KのC言語の呼び出し規約については別のページで解説する予定ですが、
    簡単に説明すると、

     @ 引数が1個ならレジスタAXに積まれます。2個以上ならスタックに積まれます。
     A 戻り値はBCレジスタに入っています。

    となります。しかし、それを意識する必要はないようです。

 

この方法の問題点

    @ 今回の78Kマイコンでは、仮想アドレスのしくみがないのでうまく動いたという意見もあります。

    32bitマイコンとかで同様に記述しても任意のアドレスをコールできるとは限りません。ただし、Borland C++ CompilerでIntel86のコードをコンパイルした場合、下記のようになりました。

    ちゃんと動いているようです。

    ソース

    コンパイル結果

    typedef void (*functype)(void);
    functype func = (functype)0x12345678;
    func();
     
    mov [ebp-0x94], 0x12345678
    call dword ptr [ebp-0x94]

     

     

    ソース

    コンパイル結果

    ((void (*)())0x12345678)();
     
    mov edx.0x12345678
    call edx
     

    これ以外のCPUではどうなるかわかりません。RISC CPUでは、任意のアドレスから任意のアドレスに常にジャンプできるとは限らないので、この方法で必ずしもうまくいく保証はありません。

     

    A PM+が関数として認識しないので、コードの補完機能が働かない。

    コード補完が働かないので、少々面倒です。

 

 

chickens_back.gif 戻る

 Copyright(C) 2008 NAITOU Ryuji. All rights reserved. 無断転載を禁ず