gdbによるISS その1

gdb

※ 2015年2月に執筆したものを転載

今回はgdb(GNU Debugger)のお話です。

gdbはその名の通りデバッガなのですが、サポートするCPUはそこそこ(かなり?)多いです。
サポートする・・・といっても、通常のgdbはx86(PC)用なので、これがそのままSHやらARMやらのデバッグができるわけではありません。
各CPUアーキテクチャ毎にコンフィギュレーション&ビルドして、そのCPU専用のgdbを作成するなり、出来あえのgdbをガメてくるなりする必要があります。

さて、タイトルに出てきているISSですが、命令セットシミュレータの略です。
命令セットシミュレータというのは、そのCPU専用の命令を解釈して処理をしてくれるシミュレータで、本来であれば、該当のCPU上でなければ実行できない実行モジュールをPC上で仮想実行してしまうものです。
前回までのVCやらx86-gcc上でシミュレートしていた方式は、ISSに対して、ネイティブシミュレータと呼びます。
何にに大してネイティブかというとPC(x86-CPU)の命令に対してネイティブってことだと思います。
ネイティブシミュレータの場合はソースコードレベルの動作までしかデバッグ/テストできないことに対し、ISSでは命令レベルのデバッグ/テストが可能となります。

gdbとISSがどう関係するかと言うと・・・大概、gdbはISSを内包しています。
大概・・・と言ったのはマニュアルを読むとすべてのgdbが内包してるわけではないという記述があったため、そのように表現しましたが、私が確認した限りだと全アーキテクチャに於いてISSは実装されているようです。
※まぁgdb自体のデバッグなども考えるとISSがあった方が楽だし、私でも先にISSを作ろうと思うかな?

というわけで、今回はこのISSを使用します。
ターゲットCPUはとりあえずARMでいきます。
gdbだけあったも仕方ないので、ARM用のツールチェインをそろえる必要があります。
基本は以下を用意します。
・binutils
 バイナリユーティリティ。
 アセンブラやらリンカやらオブジェクトコンバータなどのツールセットです。
 コレがないと始まらない
・gcc
 コンパイラです。
 C/C++用にコンフィグしましたが、C言語しか使わない予定です。
・libc、libstdc++
 C/C++標準ライブラリです。
 GNU libcをリンクするとGPL汚染されてしまいます。
 よって、組み込み系に於いては大概Redhat社が公開している、
 ロイヤリティフリーのlibcであるnewlibというものを使用します。
・gdb
 上で説明したデバッガです。

というわけで、コンパイラをコンパイルするというよくわからん作業をまずこなします。

使用するプログラムは恒例の状態機と制御器です。
別にgdbを使ってこれらをデバッグすることが目的ではありません。

今回の目的は「最終成果物の状態でユニットテストをする」になります。
「最終成果物」なので、ユニットテスト用コード及びフレームワークは除外されてる、または封印されていることがほとんどだと思います。

しかし、gdbを使用するとユニットテストフレームワーク無しでユニットテストを実施することができます。
gdbのコマンドの中にcallコマンドというものがあるのですが、これを使用します。
名前から想像できてしまうかもしれませんが、プログラム停止状態から強制的に関数コールを実施してくれます。
当然引数設定、戻り値の取得もできます。

ちゃんとユニットテストフレームワークを使用した方が良いと判断する方もいるとは思いますが、それはそれでこれはこれってヤツです。

組み込み系に於いてのコンパイラは割りと無茶な最適化をする、またはさせていることが多く、構成を変えた状態のものでテストしても問題なしという判定にならないことが多いです。
(構成を変えることでコンパイラの不具合がでたり出なかったりが発生するため。特に暗黙inlineなどが発生した場合はauto変数の構成が大きく変わるため、命令構成がガラチェンします)
というわけで最終成果物の形でテストして初めてOK判定が可能というパターンが多いです。

とはいえ、gdb&ISSの起動直後にcallコマンドを発行してもまともには動きません。
理由は、スタックポインタの初期化、静的変数の初期化が走っていない状態では、C言語として前提条件がそろっていないためです。
この状態でcallコマンドを発行するとアドレス例外が発生して強制終了します。
gdb上で動作してるプログラムが終了するだけでgdbは無事ですが。

といわけで、スタックポインタの初期化、静的変数の初期化が終わるあたりにブレークポイントを設置してから、そこまでプログラムを走らせたあとにcallコマンド発行という流れになります。
PLL、WDGの初期化などのHW絡みのコードまで走らせると今度はHW初期化待ち状態の無限ループに陥ることもあるので、そこらへんは構成を見て最適と思われる箇所を探す必要があります。
HW初期化が先に設置されていたり、あまり無いとは思いますが、スタックポインタの初期化と静的変数の初期化の間にHW初期化が設置されている場合はうまくプログラムカウンタを書き換えて該当処理をかわすなどの対応をしてください。

で、C言語的に動作可能な状態になったところでcallコマンドを発行です。
構文は簡単です。

call 関数名(引数)

Enterを押せば、関数が実行されます。

とりあえず、一連の実行例↓

#gdb起動↓
$ arm-none-eabi-gdb pid_test

GNU gdb (GDB) 7.8.1
Copyright (C) 2014 Free Software Foundation, Inc.
# ライセンス文がごちゃごちゃ出てくる。

# プロンプトが(gdb)になる。gdbのISSに接続↓
(gdb) target sim
Connected to the simulator.

# ISSのメモリ空間にプログラムイメージをロード↓
(gdb) load
Loading section .init, size 0x18 vma 0x8000
Loading section .text, size 0x1077c vma 0x8018
Loading section .fini, size 0x18 vma 0x18794
Loading section .rodata, size 0x888 vma 0x187b0
Loading section .ARM.exidx, size 0x8 vma 0x19038
Loading section .eh_frame, size 0x48 vma 0x19040
Loading section .init_array, size 0x20 vma 0x29088
Loading section .fini_array, size 0x4 vma 0x290a8
Loading section .jcr, size 0x4 vma 0x290ac
Loading section .data, size 0x1168 vma 0x290b0
Start address 0x8120
Transfer rate: 594080 bits in <1 sec.

# main関数にブレークポイント設置↓
(gdb) b main
Breakpoint 1 at 0x9e90: file main.c, line 37.

(gdb) r
# 起動

# ブレークポイントに引っ掛かる↓
Breakpoint 1, main () at main.c:37

#※この段階でcallコマンドがまともに動作可能な状態になる。

# ARMのスタックポインタはr13レジスタだが、
# gdb上から操作する場合、$r13、または$spで直接操作ができる。
# $spはCPUアーキテクチャ関わらず必ずスタックポインタのエイリアスなので、
# 環境に依存させたくない場合は$spで。
# ちなみにARMの場合r14がリンクレジスタでr15がプログラムカウンタ。
# それぞれ$lr、$pcがエイリアスになる。

# スタック領域の拡張方向にコンビニ変数(指令値)用領域確保↓
(gdb) set $command = $sp - sizeof(long)
# スタック領域拡張↓
(gdb) print $sp = $command
$1 = (void *) 0x1fffb4

# スタック領域の拡張方向にコンビニ変数(DutyCut指示)用領域確保↓
(gdb) set $dutycut = $sp - sizeof(int)
# スタック領域拡張↓
(gdb) print $sp = $dutycut
$2 = (void *) 0x1fffb0

#巨大な配列、構造体などの領域を確保する場合はmalloc関数をcallコマンドで呼んで、ヒープから取得した方が良いが、そもそもヒープがあるかどうかって問題がある。別にRTOSが持ってるメモリプールでも問題ないので、そこらへんは環境に合わせて。

# テストしたい関数の型の確認とかもできる↓
(gdb) ptype controller
type = void (long, long, long *, int *)

# テストしたい関数呼び出し↓
(gdb) call controller(1000,100,(long*)$command,(int*)$dutycut)
# ※戻り値があるタイプの関数だとcallコマンド実行後に戻り値が表示される。

# 結果取得↓
(gdb) print *(long*)$command
$3 = 40960
(gdb) print *(int*)$dutycut
$4 = 0

今回は手打ちしてますが、gdb自体にスクリプトを読ませることができるので、自動実行/連続実行は可能です。
ただし、やはり通常のユニットテストフレームワークを使用するのと比べると使いづらいのは否めないですね。
テスト対象が状態機、制御器なので1,2回関数呼んでもなんかよくわからん感じです。
しかも、最初の方の数回は状態機側で指令値を加工してしまうので、第1引数なんかは何を入れても結果変わらないし・・・。

まだちょっと次回に続く。

コメント

タイトルとURLをコピーしました