git cloneして5分で例外・割り込みハンドラをGDBデバッグする
目的
- 今後、筆者自身が業務でRTOS/ベアメタル on Cortex-Rを扱う機会がありそうだから、関連領域の土地勘を得たい
- 特に、例外・割り込みのようなOSのコアとなる領域は、これまで担当者に任せきりだったので、コード・レベルで触れてみたい
- 個人的に、どんなソフトウェアもデバッガでブレークポイントが張れると、心理的に安寧を得られるので、例外・割り込みをステップ実行したい
- また、何となく動かすだけだと、すぐ忘れるので、自身の記憶の半永続化のために、本スクラップに関連コードの解説を書く
試みたこと
ただ、Linux on Cortex-A評価ボードのような超実践的な環境だと準備(e.g. JTAG+OpenOCDの接続など)が大変になるため、プロセッサエミュレータQEMUでサクッと試みたい。また、私の業務的にはMMUが出てくることはなさそうなので、QEMU-emulated ARM Cortex-M3で、超QuickにFreeRTOSを動かしてみる。具体的には、FreeRTOSのCORTEX_MPU_M3_MPS2_QEMU_GCCというサンプルをGDBデバッグしてみる。
(引用: freertos.org)
CORTEX_MPU_M3_MPS2_QEMU_GCCって何?
まず、背景として、ARM社ではCortex-Mシリーズを試す、Cortex-M Prototyping System (MPS2+)というFPGA基板(FPGA自体は旧Altera社製)を発売している。QEMUでは、このCortex-M3環境をエミュレート可能になっており、FreeRTOSもこの環境で動くサンプルを公開している。それがCORTEX_MPU_M3_MPS2_QEMU_GCCである。このリンク先に書かれている「1. GNU Arm Embedded Toolchain download here」は若干の手間が掛かるが、それさえ手に入れば、FreeRTOSのビルドからサンプル・アプリの実行、GDBデバッグまで、恐らく2~3分で到達できる。この点で『5分で~はタイトル詐欺』って思われる方もいらっしゃるだろうが、恐らく同オーダーのセットアップ時間で、割り込みハンドラのGDBデバッグまで辿り着ける教材は、この上なく希少だと思う。なお、このサンプル自体はあまり栄えることはしておらず、main()
到達後、
- FreeRTOS経由でメモリ属性をイジって、
- タスク上で意図的にメモリ・アクセス例外を起こし、
- MemManage Handlerでその例外から復帰し、
- 1↔️2の繰り返しをしているだけである。
全く映えないが、だからこそ教材として適していると思う。実行方法も簡単で、このReadme.mdの「How to start debugging」まで実行すれば、下記話は通じるはずである。
(引用: DigiKey)
オススメ書籍
FreeRTOSは、Linuxのような汎用OSと比べて格段にシンプルなRTOSとは言え、「普段、C/C++使っているけど、アプリしか書いていません」って人がコードを読むと、恐らくシンドいことが多い。そこで、CQ出版社の「ARM Cortex-M3システム開発ガイド」という書籍をオススメする。低レイヤは初心者お断りの本が多いが、この本は図が多く、大学でOSやコンピュータ・アーキテクチャの基礎を学んだ人なら多分読める。
(引用: CQ出版社)
ただ、この本自体、私が前職の新卒N年目でCortex-M0/M4F系の業務を始める際に推薦された本であり、やや古い感は否めない(2009年出版)。そのため、新しめの書籍が良いなら「ARMマイコンCortex-M教科書」などでも全然問題無いように見える。また、「英語でも耐えられる」という人や、私のように「O'Reillyのサブスク入っている」という人は、システム開発ガイドの原著者の最新版「Definitive Guide to Arm Cortex-M23 and Cortex-M33 Processors」が無料で読めるので、それがオススメである(こちらは2020年出版)。
(引用: O’Reilly Media, Inc.)
Reset_Handler()
コード読み [1/5]:そもそも、これ何ですか
- Linuxのアプリで言えば、main関数前に動くcrt0に該当する存在である
- Cortex-Mでは、システム・リセットが入った時に、下記図のように
- まずベクター・テーブルの1 word目からMSP(Main Stack Pointer)から読み込み、
- 次に2 word目から例外ハンドラ
Reset_Handler()
のアドレスを取得し、 -
Reset_Handler()
の処理を進める
- 本サンプル・アプリでは、これを実現するために、startup.cで割り込みベクタテーブルを定義し、このリンカー・スクリプトでEFLファイル
build/RTOSDemo.axf
の0番地に配置している
(引用: Definitive Guide to Arm Cortex-M23 and Cortex-M33 Processors FIG. 4.32)
GDBでステップ実行したい
- 実際にGDBで下記コマンドを打つと(
target
後にcontinue
してはいけない)、下記図のようにPCはReset_Handler()
の先頭を差し、grep _estack build/output.map
と同じ値(正確にはword alignされた値)がSPレジスターに入っていることを確認できるはずである - なお、ここまでのハードウェア処理は、QEMUでない実機においては、Flashメモリ上のELFファイルをSystem Bus(AHB)経由で読み込むことで実現されるはずである
(gdb) target remote :1234
(gdb) layout asm
(gdb) info registers
内部で何をしているんですか
-
Reset_Handler()
の内部では、リンカー・スクリプトのシンボルに基づき、1) FlashからRAMへの.dataセクションのコピー、2) .bssセクションのゼロ埋めを実施している - その後、
_start()
内でアプリ・コードのmain()
を呼び出す - この
_start()
では、その他にもUARTの初期化を行うが、実際的にはクロックやPLLの初期化をすることも多い
__attribute__( ( optimize( "O0" ) ) )
__attribute__( ( naked ) )
void Reset_Handler( void )
{
/* set stack pointer */
__asm volatile ( "ldr r0, =_estack" );
__asm volatile ( "mov sp, r0" );
/* copy .data section from flash to RAM */
for( uint32_t * src = &_sidata, * dest = &_sdata; dest < &_edata; )
{
*dest++ = *src++;
}
/* zero out .bss section */
for( uint32_t * dest = &_sbss; dest < &_ebss; )
{
*dest++ = 0;
}
/* jump to board initialisation */
void _start( void );
_start();
}
main()
コード読み [2/5]:-
app_main()
自体は特段面白いことはしておらず、FreeRTOSの作法に則ってタスクを2つ作って、スケジューリングを開始しているだけである - この時、2つのタスクには、.bss領域のバッファがスタックとして割り当てられると共に、
ucSharedMemory
という共有メモリへのアクセス権も提供されている - 2つのタスクは、この共有メモリへのアクセス権において異なっており、
{.pcName= "ROAccess"}
は名前の通りRead権限しかなく、一方の{.pcName= "RWAccess"}
はWrite権限も持つ
prvROAccessTask()
コード読み [3/5]:この関数は、上記の"ROAccess"
側タスクの実体であり、無限ループの中で以下を繰り返している。
-
ucSharedMemory[ 0 ]
をRead -
ucSharedMemory[ 0 ]
に0をWrite - (2.をきっかけに
MemMang_Handler()
が呼び出される) - 1秒sleep
本当はもう少しいろいろやっているが、この関数は本家コメントが充実しているため、そちらを読むことをオススメする。
MemMang_Handler()
コード読み [4/5]:どうやってこの関数に飛んでくるか
- 以下、話が込み入ってくるが、この関数が本スクラップで話したかった例外ハンドラ
- この関数は、上記の「
ucSharedMemory[ 0 ]
に0をWrite」をきっかけに- MPU (Memory Protection Unit)がMemory Management Faultを検知し、
- 下記図のように、System Exceptionsの1つとしてNVIC (Nested Vectored. Interrupt Controller)に伝達され、
-
isr_vector[3]として
MemMang_Handler()
が呼び出される
(引用: Definitive Guide to Arm Cortex-M23 and Cortex-M33 Processors FIG. 8.1)
GDBでステップ実行したい
- GDBでは、下記のように操作することで、
MemMang_Handler()
にブレークポイントを張れる - ここで実行を停められたら、以降
ni
(もしくはsi
)によって、インストラクション単位で処理を追えて楽しい
(gdb) target remote :1234
(gdb) b MemMang_Handler
(gdb) layout asm
内部で何をしているんですか (初級編)
- 少し面を喰らうが、ここの関数では、
- まず、
lr
レジスタの下位2番目のbitをチェックし、 - Armv7-M以降導入されたIF-THENで分岐をかけ、
- 1がTrueなら
MSP(Main Stack Pointer)
を、 - そうでなければ
PSP(Process Stack Pointer)
をr0
にコピーする - その後、
vHandleMemoryFault
という別の関数のアドレスをr1
に込めて、 -
MSP
orPSP
を引数として関数呼び出ししている
- まず、
static void MemMang_Handler( void ) __attribute__( ( naked ) );
void MemMang_Handler( void )
{
__asm volatile
(
" tst lr, #4 \n"
" ite eq \n"
" mrseq r0, msp \n"
" mrsne r0, psp \n"
" ldr r1, handler3_address_const \n"
" bx r1 \n"
" handler3_address_const: .word vHandleMemoryFault \n"
);
}
内部で何をしているんですか (中級編)
「それは分かるが、関数の戻り番地であるlr
の下位2番目bitを見ているの?」と言いたくなる。
ここが本スクラップ記事で一番厄介なところで、本件は下記仕様に起因する。
-
lr
はHandler ModeではEXC_RETURN
という位置付けに変わる -
EXC_RETURN
には、戻り番地ではなく、下記仕様で例外関連情報が保存され、2番目bitには例外発生時のCONTROL
レジスタのSPSEL
の値がコピーされる - この値を見ることで、例外発生前に
MSP
orPSP
どちらを使っていたか判別できる
つまり、MemMang_Handler()
は、vHandleMemoryFault()
に例外発生時のスタック・ポインターを渡していると言える。
(引用: Definitive Guide to Arm Cortex-M23 and Cortex-M33 Processors FIG. 8.25)
どうやってThread Modeに返るんですか
Handler Mode時にbx EXC_RETURN
を行うと、特殊な扱いとなり、ハードウェア側が例外契機で詰んだスタック・フレームからPCを取り出し、Thread Modeに返る。なお、この下記図のスタック・フレームは、AAPCSという仕様で規格化されており、ARM IHI 0042E
という文書で詳細を確認できる。
(引用: Definitive Guide to Arm Cortex-M23 and Cortex-M33 Processors FIG. 8.17)
vHandleMemoryFault()
コード読み [5/5]:- 本関数は、
MemMang_Handler()
からFIG. 8.17のスタック・ポインターを引数として受け取る - 「
ulPC = pulFaultStackAddress[ 6 ]
って何やねん?」と思うかもだが、これは上記の通り、例外発生時のPC
レジスタ - Thumb-2のため、命令長が16bitか32bitか分からないため、1つif文を噛ませた上で、旧
PC
をincrementして、返り先を調整している
所感
いかがでしたでしょうか。仕事でSIMD命令を使ったり、趣味でコンパイラを書いたりする人も、なかなか例外・割り込みハンドラまで触ることはあまりないから、ここら辺の理解がモヤッとしている人、少なくないのではないのでしょうか。かく言う私も、仕事で割り込みコンテキスト中のコード書く機会はあったのですが、フレームワーク化されていて、上記のvHandleMemoryFault()
相当の関数を恐る恐る書くくらいだったので、いつかステップ実行しながら落ち着いてコード読みたかったんですよね。サクッと読めて良かった良かった😊