TL; DR:ESP8266用のGDBサーバーを実装しました。これにより、コールスタックを表示し、限定的なデバッグを実行できます。
友人からよく聞かれるのは、組み込みプラットフォーム向けのソフトウェアを開発することです。 通常、それらは限られたリソース(実行可能コードのストレージのサイズ、利用可能なメモリの容量など)を意味し、そのような開発には特定のスキルが必要な場合があります。
ただし、最近では、最初のパーソナルコンピューターを問題なくエミュレートでき、音楽をバックグラウンドで再生できるウェアラブルデバイスの影響で組み込みプラットフォームの認識が変化したため、開発者が日々直面する問題を忘れがちです組み込みソフトウェア。
Cesantaは、組み込みプログラミングの「魔法の」世界に入るためのしきい値を下げるプラットフォームを開発していますが、このプラットフォームを開発するとき、私たちは残りを妨げる問題にさらに直面しています。
今日は、開発ツール、つまりほとんどすべての開発環境によって提供される部分についてお話したいと思います。 当たり前だと思うのは簡単です:呼び出しスタックトレース。
私はデバッガの大ファンではありません。 さらに、デバッグにはprintfを使用することを好みます。 私の意見では、多くの場合、デバッガを接続することは実用的というよりも面倒です。
過去10年間、私はC / C ++ / Python / Java / Scala / Goでシステムを記述および保守し、非常にまれな場合にのみデバッガーを使用しました。 そして、それでも、デバッガーは、この失敗につながった一連の呼び出しを理解する手段としてのみ使用されていました。
ほとんどの言語ではコールスタックは組み込み機能であるため、このような状況は主にC / C ++プログラムのデバッグ時に発生しました。
今日戻ってきます:組み込み開発に乗り出し、プラットフォームとしてESP8266を選択し、デバッガーを使わずにいると、私は困難な立場になります。
突然これが起こりました:
- 出力のデバッグは非常に高価です。 デバイスとの唯一のインターフェイスはシリアルポート接続であり、これは他の操作にも使用されます。
- 「編集/編集/点滅/起動」という遅いサイクルにより、今回はカードが適切に動作することを祈ります。
- ロギングコードを書きすぎると、メモリが不足します。
そして、私が何かを終わらせたい場合、私はこのいまいましいスタックが必要であることに気付きました。
そもそも、なぜこの問題が発生したのでしょうか? ESP8266のストーリーは何ですか?
ESP8266
ESP8266は、非常にクールなシリコンです。 そして、その最もクールな部分は価格です:あなたは3.60ドルで完全に機能するボードを得ることができます。 また、開発に高価なマザーボードは必要ありません。最小限のツールセットで、そのまま使用できます。
この製品は、シリアルポートを介してATコマンドを理解するWiFiモジュールとしてその旅を始めました。 Arduinoに接続するだけで開始できます。
しかし、それを娘ボードとして使用することは、物語の始まりにすぎません。 ボード自体には、32ビットプロセッサ、32Kb IRAM、80Kb DRAM、および512Kb(またはそれ以上)のフラッシュが搭載されています。 すぐに、Espressifは、プログラムをデバイスに直接書き込むことができるSDKの配布を開始しました。 デバイスの機能は、よく知られているAVRの機能よりも優れており、消費電力は同程度です。
それでは、あまり知られていない中国の会社がこれをどのようにして達成したのでしょうか? ASICと無線モジュールの品質を判断することはできませんが、それらは私の経験の浅い外観にはかなり良いようです。 しかし、SDKとドキュメントの品質は驚くべきものです。
ESP8266EXのコアは、テンシリカが開発した非常に興味深いXtensaプロセッサです。 テンシリカは2013年にケイデンスに買収されました。 この会社は、構成可能なプロセッサコアで最もよく知られています。
また、コンパイラ、デバッガ、エミュレータなどの優れたツールを提供しています...
問題は、EspressifがこれらのツールにSDKを提供できないことです。 さらに、ケイデンスからXtensa SDKを直接購入した場合でも、ESP8266EXの生産で使用される正確なパラメーターは、生成されたファイルのヒープによってのみ決定できます。 Xtensa SDKの試用ライセンスが終了するまで、これをすべて取り消すことができるかどうかを理解することさえ困難です。 そして、これが可能であっても、ESP8266があなたの興味のすべてである場合にのみ、ゲームはろうそくの価値があります。
また、EspressifはXtensaツールを使用してバイナリライブラリを構築しますが、ユーザーに最適な選択肢はGCCポートを使用することです。 ESP8266で使用される特定のアーキテクチャは、lx106と呼ばれます。
Xtensaプラットフォームの構成可能な性質は、Xtensa CPUに基づくさまざまなデバイスの実際の機能セット(命令セットを含む)が非常に異なることを意味します。 これにより、ツールの再利用と、これがどのように機能するかを理解することが非常に複雑になります。
lx106の最初の機能は、この構成がXtensaの最も重要な機能の1つであるレジスタウィンドウを使用しないことです。 これは、ESP8266が使用する呼び出し規約に大きな影響を与え、その結果、すべての機器に影響を与えます。
Max FilippovでサポートされているGCCポートが積極的に開発されており、 githubで入手できます。 まだ完璧にはほど遠いですが、飛躍的に発展しています。 コミュニティへの献身のためにMaxに敬意を表します。
ESP8266の既存のデバッグオプションはかなり控えめです。
オンチップデバッガーを試すことができます(Xtensaのxt-ocdまたはopenocdのいずれかを使用しますが、これにはJTAG接続と十分な数のピン留めピンを備えたESPが必要です(つまり、このアプローチはESP-01では機能しません)。
Qemuポートはまだ初期段階です。
呼び出し履歴を表示
必要なのがスタックをトレースできることだけである場合、最も簡単な方法は、この機能をコードに直接実装することです。 libunwindのようなもの。 残念ながら、lx106用の同様のライブラリ用のポートはありません。 さらに、ほとんどのコードがフレームポインターなしでコンパイルされることを考えると、このような妥当なサイズのライブラリの実装は...になります。 複雑だとしましょう。
GDBは、コード、命令ごとの分析、関数のプロローグの検索、スタックの変更のロールバックなどによって問題を解決します。 GDBにメモリの内容を供給し、ハードワークを行わせようとするとどうなりますか?
GDBサーバープロトコル
GDBは、単純なテキストプロトコルによるリモートデバッグをサポートしています。 ネットワークまたはシリアルポートを介して動作します。 プロトコルの簡単な説明はここにあります 。
サポートする必要がある2つの主要なチームは次のとおりです。
- 'g'-レジスタの内容をアンロードします
- 'm'-メモリからバイトのパケットを読み取ります
したがって、必要なのは、例外が発生した場合に実行されるコードを記述し、レジスタのステータスを確認し、シリアルポートにGDBプロトコルを実装することだけです。
制御を得ます
最初は、Xtensa CPUの低レベルの例外ベクトルを直接変更しようとしましたが、役に立ちませんでした。
次に、リンカースクリプトが_xtos_set_exception_handler関数に言及していることに気付きました。 XTOSは、Xtensa SDKによって提供される非常に薄いレイヤーです。
_xtos_set_exception_handlerを使用すると、指定された例外が発生した場合に呼び出されるC関数を登録できることがわかりました 。
ICACHE_FLASH_ATTR void gdb_init() { char causes[] = {EXCCAUSE_ILLEGAL, EXCCAUSE_INSTR_ERROR, EXCCAUSE_LOAD_STORE_ERROR, EXCCAUSE_DIVIDE_BY_ZERO, EXCCAUSE_UNALIGNED, EXCCAUSE_INSTR_PROHIBITED, EXCCAUSE_LOAD_PROHIBITED, EXCCAUSE_STORE_PROHIBITED}; int i; for (i = 0; i < (int) sizeof(causes); i++) { _xtos_set_exception_handler(causes[i], gdb_exception_handler); } }
低レベルの例外ハンドラーは、レジスターの状態をスタック上の構造体に保存し、Cハンドラーを呼び出して、この構造体のアドレスをパラメーターとして渡します。
Xtensaはパラメータ化可能であるため、ドキュメントは関連するものを理解するのがそれほど簡単ではありません。 Xtensaのドキュメントは非常に一般的であり、他のXtensa構成で利用可能なコードから多くを理解できますが、これがlx106に適用されるかどうかはわかりません。
その結果、私は混乱し、最終的にどこに表示されるかを確認するために特定の値をレジスタに書き込むことにしました。 レジスタa2〜a16を見つけることができましたが、a1(スタックポインタ)はa0(リターンアドレス)の内容で上書きされているようです。
その後、推測を確認し、レジスタa1が失われたことを説明するリンクをいくつか見つけました。
まあ、すべてを一緒に収集する:
struct xtos_saved_regs { uint32_t pc; /* instruction causing the trap */ uint32_t ps; uint32_t sar; uint32_t vpri; /* current xtos virtual priority */ uint32_t a0; /* when __XTENSA_CALL0_ABI__ is true */ uint32_t a[16]; /* a2 - a15 */ };
LITBASEレジスタはありませんが、低レベルの例外ハンドラはそれを変更しないため、GDBに現在の値を単純に与えることができます。
ここでの主要な機能は、スタックポインターがないにもかかわらず、ハンドラーCに渡されるxtos_saved_regs構造体のアドレスによって計算できることです。これは、スタックポインターの256バイト下です。
これで、割り込みを無効にして、GDBからのリクエストを待つことができます
/* The user should detach and let gdb do the talkin' */ ICACHE_FLASH_ATTR void gdb_server() { printf("waiting for gdb\n"); /* * polling since we cannot wait for interrupts inside * an interrupt handler of unknown level. * * Interrupts disabled so that the user (or v7 prompt) * uart interrupt handler doesn't interfere. */ xthal_set_intenable(0); for (;;) { int ch = gdb_read_uart(); if (ch != -1) gdb_handle_char(ch); } }
GDBとの通信
ここで、gコマンドに対する応答の形式がGDBを待機していることを理解する必要があります。
特定のGDBに依存します。 lx106の下のポートを使用する必要があります。
レジスタの説明は、 gdb / regformats / reg-xtensa.datファイルにあります。
それから私達は得る:
struct regfile { uint32_t a[16]; uint32_t pc; uint32_t sar; uint32_t litbase; uint32_t sr176; uint32_t sr208; uint32_t ps; };
GDBプロトコルとメモリへの安全なアクセス(メモリの一部はバイトアドレス指定に使用できません)に関しては、それほど興味深い技術的微妙さはありませんが、全体としてはそれだけです。
何が起こったのか見てみましょう:
#0 0x40242557 in crash (v7=<optimized out>, this_obj=18445899648779419648, args=18446462599806581592) at user/v7_esp.c:371 #1 0x4023c321 in i_eval_call (v7=v7@entry=0x3fff5c28, a=a@entry=0x3fff96f0, pos=pos@entry=0x3ffffe94, scope=<optimized out>, this_object=<error reading variable: can't compute CFA for this frame>, is_constructor=<optimized out>, is_constructor@entry=0) at user/v7.c:9977 #2 0x40239962 in i_eval_expr (v7=0x3fff5c28, v7@entry=<error reading variable: can't compute CFA for this frame>, a=0x3fff96f0, a@entry=<error reading variable: can't compute CFA for this frame>, pos=0x3ffffe94, pos@entry=<error reading variable: can't compute CFA for this frame>,scope=<optimized out>) at user/v7.c:9595 #3 0x4023bcf0 in i_eval_stmt (v7=<error reading variable: can't compute CFA for this frame>, a=<error reading variable: can't compute CFA for this frame>, pos=<error reading variable: can't compute CFA for this frame>, pos@entry=0x3ffffe94, scope=<optimized out>, brk=<optimized out>,brk@entry=0x3ffffe90) at user/v7.c:10487 #4 0x4023bd4a in i_eval_stmts (v7=<error reading variable: can't compute CFA for this frame>, a=<error reading variable: can't compute CFA for this frame>, pos=0x3ffffe94, pos@entry=<error reading variable: can't compute CFA for this frame>, end=15, scope=<optimized out>, brk=<error reading variable: can't compute CFA for this frame>) at user/v7.c:10053 #5 0x4023b104 in i_eval_stmt (v7=<optimized out>, a=a@entry=0x3fff96f0, pos=pos@entry=0x3ffffe94, scope=<optimized out>, brk=<optimized out>, brk@entry=0x3ffffe90) at user/v7.c:10088 #6 0x4024140a in v7_exec_with (v7=<optimized out>, res=res@entry=0x3fffff30, src=<optimized out>, w=<optimized out>) at user/v7.c:10607 #7 0x4024148a in v7_exec (v7=<optimized out>, res=res@entry=0x3fffff30, src=<optimized out>) at user/v7.c:10631 #8 0x402421c4 in process_js (cmd=<optimized out>) at user/v7_cmd.c:66 #9 0x4024234a in process_command (cmd=cmd@entry=0x3ffebc14 <recv_buf$3591> "crash()") at user/v7_cmd.c:128 #10 0x402423f7 in process_prompt_char (symb=<optimized out>) at user/v7_cmd.c:163 #11 0x40244a59 in rx_task (events=<optimized out>) at user/v7_uart.c:151 #12 0x40000f49 in ?? () #13 0x40000f49 in ??
これは、-Og -g3でコンパイルされたコードのスタックトレースです。
-Osを使用することはまだできません。 「エラー読み取り変数:このフレームのCFAを計算できません」にも注意してください。 lx106 GDBにはいくつかの変更が必要なようです(まあ、何か見落としていました)。
注:CFAの問題はgdb 7.9.1で修正され、 このリポジトリの「lx106-g ++-1.21.0」ブランチで利用可能です 。 新しいgdbを試してくださいと指摘してくれたAngusに感謝します。 ただし、-Osの問題は修正されません。
時間を見つけたら、作業を続け、ブレークポイントを設定し、例外の後に実行を再開する機能を追加しますが、現在の実装は緊急の問題を解決します:呼び出しスタックを表示します。 これがあなたのお役に立てば幸いです。
ソースコード(GPLv2)はこちらです。
そして、 ここで使用するための指示。
お楽しみください。