DebugMonハンドラ再び ── newlib向けシステムコールの実装


newlib for arm-none-eabi は下位層に Angel debug monitor を仮定し、thumb2 では bkpt命令の semihosting call を使う。 DebugMon_Handler() に Semihosting SWI サービスを追加するまでのメモ。

Semihosting の構図


DUI0471I_developing_for_arm_processors.pdfより。
ターゲット上のコードからホストのデバッガに向け、ファイルの入出力や時刻の取得を要求するサービスである。 本来は "bkpt 0xab"という命令を実行するところでターゲットが halt し、 そのデバッグイベントを受けてデバッガはレジスタやメモリの内容を覗いてホスト上でサービスを実行し、 結果をターゲットのレジスタに書き込んでから halt を解除する、というような仕掛け。

ターゲットから見るとホストの資源にアクセスするただのソフトウエア割込み(システムコール)に見える。 実際 (DHCSR の C_DEBUGEN を設定して) デバッガがイベントを受け取るのでなく、 (DEMCR の MON_EN を設定して) ただのシステムコールとして実装することもできる。

Demon Debug Monitor と Angel Debug Monitor

Semihosting サービスを扱う ARM 本家のモニタに Demon Debug Monitor と Angel Debug Monitor というものがある。

newlib for arm-none-eabi を特段の指定なしに configure && make するとコンパイルオプションに -DARM_RDI_MONITOR がついて Angel Debug Monitor を想定する。 他方、Makefile を書き換えて -DARM_RDP_MONITOR を付けるようにすると Demon Debug Monitor を想定するようになる。

この際 Angel Debug Monitor, Demon Debug Monitor の実態はどうでもよく、呼び出し方法が明確になっていることが本質的。 この二つ、サポートするサービスはほぼ同等だが、サービス番号や引数/結果のハンドリングが多少異なる。ホストのファイルをオープンするサービスで、

命令 引数 備考
RDP svc num num にサービス名(0x66)、r0 にファイル名、r1 に mode.
RDI bkpt 0xab r0 にサービス名(0x01)、r1 にファイル名と mode の入った構造体へのポインタ ARMv6,v7-M の thumb
svc 0x123456 全ての ARM モード
svc 0xab 上記以外。
という具合。

RDP のほうが引数/結果のハンドリングが素朴で素直だと思うが、実装するには RDI のが楽だ。新しいだけのことはある。

newlib の thumb2 向けディレクトリ (arm-none-eabi/lib/thumb/thumb2/) に含まれる libc.a のシステムコールは bkpt の RDI を呼んでいて、

らしい。それぞれの中の *-syscalls.o を読んでみた。thumb だと 3 種の区別があるから 3 つあるのかというとそういう訳でもなく、 ARM 向けライブラリ(arm-none-eabi/lib/) ではどうかというと、 で libc.a と librdimon.a は同じ流儀だった。なぜか前後のコードが微妙に違うけども。

Notations

以下のサンプルコードで頻繁につかう構造体の定義:
typedef struct {
  uint32_t r0;  
  uint32_t r1;
  uint32_t r2;
  uint32_t r3;
  uint32_t r12;
  uint32_t lr;
  uint32_t pc;
  uint32_t xpsr;
} cm3_int_frame;
割込みハンドラに突入するとスタックには r0 〜 xpsr がこの順に入っている。 つまり割込み突入時にハードウエアが保存するフレームを構造体としてアクセスするためのもの。
{
    cm3_int_frame* fp;
    uint16_t cmd;
    __asm ("mov  %0, sp \n\t" : "=r"(fp) );
    cmd = *(uint16_t *)(fp->pc);           //  割込みから復帰する予定の位置の命令を取り出す。
}
のように使う。これは以下のようにも書ける:
{
    register cm3_int_frame* fp asm ("sp");    // 変数 fp をスタックポインタに割り当てる(スタックポインタの別名とする)。
    uint16_t cmd;
    cmd = *(uint16_t *)(fp->pc);
}
newlib の真似っこだし、 レジスタが一個浮く (上の書き方は fp は sp 以外のレジスタに置かれるが、下の書き方は sp が直接使われる) のでスタックへのプッシュが禁じられているなどレジスタが枯渇しそうなところで使っているが、もちろん下の書き方はまったくお薦めではない

ソフトウエア割込みとしての svc命令と bkpt命令の差

Thumb には二種類のソフトウエア割込み命令がある:
svc #imm8
SVC例外を起こす。命令のビットパターンは (0xdf00 | #imm8) で、旧来 swi #imm と言っていたもの。
bkpt #imm8
DebugMon例外を起こす。命令のビットパターンは (0xbe00 | #imm8) で、つまり bkpt 0xab は 0xbeab.
svc のほうだと飛んだ先の実装は:
void SVC_Handler(void){
  register cm3_int_frame* fp asm ("sp");
  unsigned char svc_num = ((unsigned char *)(fp->pc))[-2];  //  svc 命令の 1 バイト目の #imm8 (たぶんサービス名) を拾う。
  switch(svc_num){    // サービスで分岐。
    ...
  };
}
となる。まあ普通だ。bkpt を使った場合こうなる:
#include <core_cm3.h>
void enable_bkpt(){
  CoreDebug->DEMCR = CoreDebug_DEMCR_MON_EN_Msk;    // これがないと割込み優先順位如何にかかわらず bkpt で HardFault か Halt する。 
}
void DebugMon_Handler(void){
  register cm3_int_frame* fp asm ("sp");
  unsigned char bkpt_num = *(unsigned char *)(fp->pc); // bkpt 命令は PC をスタックに積む時、(bkpt の次でなく) bkpt の位置を指す。

  fp->pc += 2;                  // ソフトウエア割込みから返った時、bkpt の次の位置に復帰するように修正。

      ...                       //  RDI のサービス以外もろもろ。

  switch(bkpt_num){
   case 0:
     breakpoint_work(); break;
   case 0xab:
     RDI_request(fp->r0, fp->r1);
   ...
  };
}
bkpt命令のスタックへの積み方はこのほうが都合が良いとかなんとか書いた記憶があるが、 ただのソフトウエア割込みとして使うには少し妙かもしれない。

newlib での Angel SWI semihosting 呼び出しの実際

newlib の下のほう(newlib/libc/sys/arm/syscalls.cのあたり)で、こんな感じにサービスを呼んでいる。
int _swiread (int file, char* ptr, int len){
  int fh = remap_handle (file);
  int reason = AngelSWI_Reason_Read;
  int block[3];
  
  block[0] = fh;
  block[1] = (int) ptr;
  block[2] = len;

  __asm ( "mov r0, %0  \n\t"                  // r0 に AngelSWI_Reason_Read (ちなみに 0x06) 入れて、
          "mov r1, %1  \n\t"                  // r1 に block を入れて、
          "bkpt 0xab   \n\t"                  // bkpt する。
         :: "r"(reason), "r"(block)  );
}
というわけで、サービス番号 request とレジスタ r1 に入った引数 args を受け取って r0 に結果を返す関数:
#include <newlib/libc/sys/arm/swi.h>                                 // AngelSWI_Reason_Open とかの定義。
uint32_t RDI_request(uint8_t request, uint32_t args){ 
  switch(request){
    case AngelSWI_Reason_Open:       return angel_Open(args); break;
    case AngelSWI_Reason_Close:      return angel_Close(args); break;
    case AngelSWI_Reason_WriteC:     return angel_WriteC(args); break;     // 未使用。
    case AngelSWI_Reason_Write0:     return angel_Write0(args); break;     // 未使用。
    case AngelSWI_Reason_Write:      return angel_Write(args); break;	      
    case AngelSWI_Reason_Read:       return angel_Read(args); break; 	      
    case AngelSWI_Reason_ReadC:      return angel_ReadC(args); break;      // 未使用。
    case AngelSWI_Reason_IsError:    return angel_IsError(args); break;    // 未使用。
    case AngelSWI_Reason_IsTTY:      return angel_IsTTY(args); break;	      
    case AngelSWI_Reason_Seek:       return angel_Seek(args); break;	      
    case AngelSWI_Reason_FLen:       return angel_FLen(args); break;	      
    case AngelSWI_Reason_TmpNam:     return angel_TmpNam(args); break;     // 未使用。
    case AngelSWI_Reason_Remove:     return angel_Remove(args); break;
    case AngelSWI_Reason_Rename:     return angel_Rename(args); break;
    case AngelSWI_Reason_Clock:      return angel_Clock(args); break;
    case AngelSWI_Reason_Time:       return angel_Time(args); break;
    case AngelSWI_Reason_System:     return angel_System(args); break;
    case AngelSWI_Reason_Errno:      return angel_Errno(args); break;
    case AngelSWI_Reason_GetCmdLine: return angel_GetCmdLine(args); break; // crt0.S で使用。
    case AngelSWI_Reason_HeapInfo:   return angel_HeapInfo(args); break;   // crt0.S で使用。
    case AngelSWI_Reason_EnterSVC:   return angel_EnterSVC(args); break;   // 未使用。
    case AngelSWI_Reason_Elapsed:    return angel_Elapsed(args); break;    // 未使用。
    case AngelSWI_Reason_TickFreq:   return angel_TickFreq(args); break;   // 未使用。
    case AngelSWI_Reason_ReportException: 
                                     return angel_ReportException(args);
                                     break;
    default:
      return 0; //  RDI の unknown services.
      break;
  };
  return 0;
}
newlib で使ってないもの含めて全部。トップレベルの雰囲気だけ。コメント。 もひとつコメント ──
SYS_HEAPINFO
ヒープとスタックの配置をホストから取得するサービスということになっている。ターゲットからは
{
   typedef struct {
      int heap_base;                // ヒープの下限。たぶん BSS の上限のあたりから。
      int heap_limit;
      int stack_base;               // スタックの初期値(つまり上限値)。
      int stack_limit;
   } heapinfo;
   heapinfo  *mem_block, info;

   mem_block = &info;
   __asm ( "mov r1, %0  \n\t"       // 引数 r1 には heapinfo構造体のポインタのポインタが代入される。 
           "mov r0, %1  \n\t"
           "bkpt 0xab   \n\t"
             :: "r"(mem_block), "n"(AngelSWI_Reason_HeapInfo) );
}
という使いかたをする。ここで stack_basestack_limit のどっちが sp の初期値なのか ちと迷う:
RealView Debugger User Guide の "13.10.1. The semihosting controls for RVISS targets" あたりの記述:
Heap_Base
    The lowest address of the system heap, returned by the SYS_HEAPINFO SVC.
Heap_Limit
    The highest address available for the system heap, returned by the SYS_HEAPINFO SVC.
Stack_Base
    The lowest address of the system stack, returned by the SYS_HEAPINFO SVC.
Stack_Limit
    The highest address available for the system stack, returned by the SYS_HEAPINFO SVC.
RealView Compilation Tools Developer Guide の表現:

が、newlib のコードが sp 初期値として想定するのは stack_base のほうのようだ。 そして heap_limit とか stack_limit とかで heap と stack の量と境界を管理する ── おお格好ええ、 でもメモリ管理ユニットあってもそんなの管理できないだろ特に stack_limit どうすんだと思ったら、 sbrk()malloc() のほうで heapinfo を全く(heap_baseすら)参照していなかった。意味ねぇ。

DebugMon_Handler() のお仕事一覧

これらは排他でなくいくらでも重複しうる。 実際 semihosting サービス呼び出しの bkpt 命令の上に breakpoint を置きたくなるのが、いかにもありそうなケースなのが腹立つところ。 この場合 breakpoint として挿入された bkpt 命令を処理した後、ROM/RAM 上にあった bkpt 命令によって semihosting サービスが呼び出されることになるはず。

実装上しちめんどくさいのがベンチの時間測定中に semihosting サービス呼び出しが入るケースで、 bkpt 命令の位置で、

の二つをきっちり区別して処理しつつ、しかも という要望が付く。 やはり breakpoint イベントの有無の確認が手間なのが足をひっぱりまくる。これだけで 50クロックくらいかかっている。

HardFault_Handler() の細工

DebugMon_Handler() の中で例えば変数のプリントのため newlib の printf() を使うと、中で bkpt 0xab (r0 = AngelSWI_Reason_Write) が実行される。 デバッグモニタ割込みの内側でデバッグモニタ割込みを起こしているわけだが、どうなるかというと再帰したりせずに HardFault が起きる。 そのままおなくなりになられても困るので
#include <core_cm3.h>
void HardFault_Handler(void){
  if(SCB->HFSR & SCB_HFSR_DEBUGEVT_Msk){      //  デバッグイベントがハードフォルトを引き起こした。
    register cm3_int_frame* fp asm ("sp");

    if(*((uint16_t *)(fp->pc)) == 0xbeab){     //  Flash Patch で挿入したものでない、RDI サービス呼び出しの bkpt命令をみてハードフォルトが起きた。

      SCB->HFSR = SCB_HFSR_DEBUGEVT_Msk;      //  フラグクリア。
      SCB->DFSR = SCB_DFSR_BKPT_Msk;
      fp->r0 = RDI_request(fp->r0, fp->r1);   //  サービス実行。
      fp->pc += 2;                            //  当のbkpt命令を通過。
      return;
    }
  }
  throw_or_die(GRANT_HARD_FAULT);             //  本物のハードフォルト。setjmp()で受け取れない奴だったらダンプしてリセット。
}
という感じで動かした。HardFault という超高位の優先順位で semihosting といういかにも低位っぽいサービスを動かしてよいかというと 実はあまりよくないので今は bkpt 介さす USB のバッファに直叩きしているが (HardFault を通過しないようにしている)。 ... まあタスクキュー作っておいてそっちに投げるべきなんだけども。

ちなみに RDP より RDI のが楽だ、というのはこういうところでちょいと顔を出す。 RDI_request() でなく RDP_request() だとこう書かなければならないところ:

      uint8_t num = *(uint8_t *)(fp->pc);
      fp->r0 = RDP_request(num, fp->r0, fp->r1, fp->r2, fp->r3, ...); // 引数の数は不定(numによって変わる)。
要するに、ターゲットのクライアントは引数をレジスタで渡したつもりになっていても、受け取る側はスタック上のブロックで貰っている。
割込みのテールチェーン
割込み要求が多数重なったとき、メインスレッドにいちいち復帰しては直に次のハンドラの処理うんぬんとするのは 復帰時にスタックのポップ、すぐにハンドラ突入でスタックのプッシュという手間がもったいないので 続行する場合はこれを省略する、というのが「割込みのテールチェーン」である ──

と書いてみたところでたいがい「うまくやってくれるのね、はいはい」と聞き流して終わると思う。
が、ここでその実装の副作用が表に出る。

メインスレッド上で

   mov  r1, 100       ; システムコールに 100 という引数を渡したい。 
   svc
と書いたとする。ハンドラ側で引数を受け取るのに、
SVC_Handler:
   mov hoge, r1       ; 誤り。
   ...
とは書けない。SVC_Handler に突入する瞬間、他の割込みが入り、そちらを処理してからテールチェーンで SVC_Handler に入ると、 他の割込みの終了時にレジスタを復帰していないので r1 は破壊されたままになっている。 こう書かねばならない:
SVC_Handler:
   ldr  r1, [sp, #4]  ; スタックに保存されている r1 を拾う。
   ...
Yiu 氏の本でも、テールチェーンの概説(9.4節) では「すげーだろ(超訳)」としか書いてない。副作用の件は SVC の例(11.6 節) の末尾にボソっと書いてあった。

References


[日記へ] [目次へ]