結論
結論を先に述べる
1.まず microLib を使わないのは、ソースコードの動作を制御可能にするためだ。ベンダーが勝手に lib を変更しても影響を受けず、C 標準ライブラリの違いによる動作差異も生じないため、ソースコードはクロスプラットフォーム対応になる。
ここから導かれるのは、microLib を使わないならオリジナルの libc が入るので、C ライブラリ内の多くの対話系関数が Semihosting コードを生成し、デバッグ中はカクカクした動作になり、デバッグなしではまったく動かない。
要するに、microLib を使わない場合、C 標準ライブラリ関数を使うのは非常に慎重でなければならず、システム対話関数が Semihosting コードを生成しないように避ける必要がある。
祖宗の法は変えられない
私がこの業界に入った当初から、上司は printf 系関数を断固として使うなと禁じていた。いや、むしろすべての libc 関数を禁じていた。mallocもmemsetもstrcpyもだ。標準 C ライブラリとしては stdint.h、stdlib.h、stdbool.h だけを導入していた。その結果、自作の文字列操作やバッファ操作や trace ログなどが大量に生まれた。malloc がスレッドのスタックオーバーフローを避けるためで、静的メモリならバグの規模を制御できると説明できるにせよ、他の自前実装はなぜ使ってはいけないのか?
とにかく、祖宗の法は変えられない。今のプロジェクトでも依然としてそうだ。
手がかり
この人の bootloader の脆弱性の紹介(実はこれもスタック操作の問題だ)を読んでいたら、次の記事を見つけた。この祖宗の法の由来への答えがあるかもしれない。
検証
その中のコードの一部は次のとおり。clock(); は典型的なシステム対話を発生させるコードだ。このプロジェクトでは microLib が無効になっている。
#include <stdio.h>
#include <time.h>
int main(void)
{
// 一部の初期化コードは省略
while (1)
{
clock_t tTime = clock();
HAL_GPIO_TogglePin(GPIOH, GPIO_PIN_5);
HAL_Delay(500);
}
}
デバッグモードで実行する
まず clock() 関数関連の逆アセンブルを示す。0x0800037C アドレスにジャンプして clock() 関数を実行する
109: clock_t tTime = clock();
0x08002386 F7FDFFF9 BL.W 0x0800037C clock
0x0800238A 9001 STR r0,[sp,#0x04]
0x0800238C F6414000 MOVW r0,#0x1C00
0x08002390 F6C50002 MOVT r0,#0x5802
0x08002394 2120 MOVS r1,#0x20
clock() 関数のある場所には、本当に BKPT ソフトブレークポイントが現れた。
BKPT は Cortex-M の Break Point(ソフトウェアブレークポイント)命令であり、定数 0xAB は Semihosting 専用の合言葉だ。使っているデバッグツールがたまたま Semihosting に対応していれば、ソフトウェアは止まらずに正常に動作し続ける。しかし、電源を切って再起動するとおかしなことになる。「BKPT 命令が非デバッグモードで実行されると、Cortex-M プロセッサは直接 Hardfault になる」。
0x0800037C 2100 MOVS r1,#0x00
0x0800037E 2010 MOVS r0,#0x10
0x08000380 BEAB BKPT 0xAB ;この行で止まる。これがSemihostingコードのソフトブレークポイントだ
0x08000382 4905 LDR r1,[pc,#20] ; @0x08000398
0x08000384 6809 LDR r1,[r1,#0x00]
0x08000386 1A40 SUBS r0,r0,r1
最適化を有効にすれば回避できるか?
実際、現代のコンパイラは非常に賢くなっており、最適化を少し有効にするだけで、自分では気づかなかった多くのバグが修正されることもある。
O3 でコンパイルしても、やはり BKPT の出現は避けられない。この libc はバイナリ形式で書き込みファイルにリンクされているはずだ。
どの関数を避けるべきか?
参考記事に書いてあるので、ここに引用する。
1. 標準入出力(Standard I/O)
- printf 系関数:printf、fprintf、sprintf など。標準出力デバイス(通常はホストのコンソール)へのフォーマット出力に使う。
- scanf 系関数:scanf、fscanf、sscanf など。標準入力デバイス(通常はホストのキーボード入力)からのフォーマット入力に使う。
2. ファイル操作(File Operations)
- fopen:ファイルを開く。
- fclose:ファイルを閉じる。
- fread:ファイルからデータを読み込む。
- fwrite:ファイルへデータを書き込む。
- fseek:ファイルポインタを指定位置へ移動する。
- ftell:ファイルポインタの現在位置を取得する。
- fflush:ファイル出力バッファをフラッシュする。
3. 時間と日付(Time and Date)
- time:現在時刻を取得する。
- clock:プロセッサ時間を取得する。
- difftime:2 つの時点の時間差を計算する。
- strftime:時刻と日付を文字列にフォーマットする。
4. エラー処理(Error Handling)
- perror:エラーメッセージを標準エラーデバイスに出力する。
- strerror:エラーコードに対応するエラーメッセージ文字列を返す。
5. システムコール(System Calls)
- exit:プログラムを終了しステータスコードを返す。
- system:システムコマンドを実行する(組み込みシステムではほとんど使われないが、ホスト上でのデバッグ時には役立つことがある)。
6. その他の補助関数(Other Auxiliary Functions)
- getenv:環境変数の値を取得する。
- putenv:環境変数を設定する(あまり一般的ではない)。
- remove:ファイルを削除する。
- rename:ファイル名を変更する。
元記事がとても面白いので、ぜひ読んでほしい
私たちは明らかにその記事が挙げる特徴 4 に当てはまり、しかももっと過激だ。
【「組み込み盲腸炎」の潜伏と誘因】
五星上将マッカーサーはかつてこう評論した:某度で病気を調べれば、がんと出る。お前のような似非専門家が Semihosting をそんなに怖いものだと言い、「しかもコンパイラがデフォルトで埋め込む」だと?どうして私は今も元気にやっていられるんだ?一度も出会ったことがないぞ?
率直に言えば、あなたは次の特徴に当てはまるかもしれない:
- ほとんどが Arm Compiler 5 を使っている;
- ほとんどがデフォルトで MicroLib を使っている;
- Arm Compiler 6 で MicroLib を選ばない場合、「デバッグ状態では全て正常なのに、プログラムをダウンロードして直接実行するとフリーズする」現象に遭遇し、そのため小さなノートに「MicroLib しか使えない」と静かにメモした;
- malloc 以外の libc 関数を一切使わない。printf も含めてだ;
- 使っているプログラムテンプレートは凄い人が作ったものだ;
- アプリケーション開発はチップメーカーが提供するサンプルプロジェクトに基づいている;
- RT-Thread のような「ワンストップサービス」を提供するソフトウェアプラットフォームを使っている。
たくさん挙げたが、実は 2 つのケースに分かれるだけだ:
- 偶然が重なってうまくいっただけ——運が良かった
- 誰かが代わりに重荷を背負ってくれていた