組み込みの埋まってるとこ

システム寄りの組み込みCプログラミングBLOG

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のドキュメントで解説されていますので この辺から試してみることをおすすめします。

sourceware.org

関連リンク