arm-none-eabi-gccでundefined referenceエラーになる原因
組み込みでのコンパイルは見たこともない難しいエラーに直面することがよくあります。 Arm用のクロスコンパイラであるarm-none-eabi-gccでコンパイルしていると 次のような未定義シンボルエラーの嵐に見舞われることがありますが、この原因はなんでしょう?
エラーの原因は未定義のシステムコール
例えばarm-none-eabi-gccでprintfを使用したプログラムをコンパイルすると次のようなエラーが出力されます。 アンダーバーで始まる使った覚えがない関数がいくつも未定義となっているのが分かります。 実はこれらアンダーバーで始まる関数はシステムコールと呼ばれる本来であればOSのカーネルに存在する関数なのです。
c:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/bin/ld.exe: c:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/lib\libc.a(lib_a-exit.o): in function \`exit': exit.c:(.text+0x2c): undefined reference to \`\_exit' c:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/bin/ld.exe: c:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/lib\libc.a(lib_a-sbrkr.o): in function \`\_sbrk_r': sbrkr.c:(.text+0x18): undefined reference to \`\_sbrk' c:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/bin/ld.exe: c:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/lib\libc.a(lib_a-writer.o): in function \`\_write_r': writer.c:(.text+0x24): undefined reference to \`\_write' c:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/bin/ld.exe: c:/msys64/mingw64/bin/../lib/gcc/arm-none-eabi/8.4.0/../../../../arm-none-eabi/lib\libc.a(lib_a-closer.o): in function \`\_close_r': closer.c:(.text+0x18): undefined reference to \`\_close' ... collect2.exe: error: ld returned 1 exit status
なぜシステムコールを使おうとするのか
arm-none-eabi-gccに付属している標準CライブラリはLinux用に開発されたNewlibを組み込み用にポーティングしたものです。 このとき組み込みでは特定のOSというものが存在しないためカーネルのシステムコールを呼び出す低レベルの関数が未実装のままになりました。 そのためコンパイルの最後でプログラムをリンクするときに未定義シンボルエラーになります。
システムコールのインターフェースとなる関数
Interface | Description |
---|---|
_exit | プログラムを終了 |
_sbrk | プログラムデータスペースを増加 |
_write | ファイルへ書き込み |
_close | ファイルを閉じる |
_lseek | ファイル中での位置を設定 |
_read | ファイルから読み込み |
_fstat | 開いているファイルの状態 |
_isatty | 出力ストリームがターミナルであるか |
「undefined reference to」を解決する方法
たいていの組み込みではこれらのシステムコールを必要とする関数はMyPrintfのように代替する関数を自分で用意して使わないようにしています。 memsetやsprintf等のように最終的にシステムコールを必要とせずNewlibの実装だけで完結する関数であれば使用しても問題ありません。
もし動作が確認されているプロジェクトで未定義シンボルエラーに出くわしたのであれば、まずコンパイラが参照しているライブラリのファイル名、パス指定、環境変数等に間違いがないことをを確認するとよいでしょう。 オープンソースのRTOSではNewlibではない独自のCライブラリが用意されていてそちらをリンクするようになっていることもあります。
Pico SDKの場合
Raspberry PiのPico SDKの場合はNewlibが想定しているシステムコールについてはソースファイルの中に定義がありますので、 ボードとコンパイラを正しく選択できていればundefined referenceエラーになることはないでしょう。
定義の例
引用元: https://raw.githubusercontent.com/raspberrypi/pico-sdk/master/src/rp2_common/pico_stdio/stdio.c
int __attribute__((weak)) _write(int handle, char *buffer, int length) { if (handle == STDIO_HANDLE_STDOUT || handle == STDIO_HANDLE_STDERR) { stdio_put_string(buffer, length, false, false); return length; } return -1; }
まとめ
arm-none-eabiは特定のOSを想定していないコンパイラです。 そこへLinux用の標準Cライブラリをポーティングしたため本来であればOSに存在しているシステムコールを呼び出すところでundefined referenceエラーになってしまいます。 商用の組み込み用コンパイラでは標準Cライブラリの標準的な関数をサポートしつつも その最下層でシステムコールを呼び出すことになる部分は組み込みでも実装できる程度の限られた簡単なものだけにするか、 ユーザーが実装しなければスタブとして無害な関数がリンクされるようになっているものが多いようです。
もし将来は自分でOSを作ってみたいと考えているような人であれば、 システムコールを自作してみるというのも面白いでしょう。 まずはシステムコールの中身を空っぽのスタブとして実装する例がNewlibのドキュメントで解説されていますので この辺から試してみることをおすすめします。