SNSへはこちら

gccで既存の関数をオーバーライドする(printfの実装も兼ねて)

これまでマイコンで printf を UART で使うために、_write 関数を自力で実装してきました。しかし、ここにきて gcc の標準ライブラリを使いながらも、特定の関数をオーバーライドしたいという事案が発生。ちょっとした Tips ですが、お遊びも含めて記事にしてご紹介します。

標準ライブラリを使わない場合の実装

これまでマイコンでやってきた方法です。標準ライブラリを使わず、手動実装をしない場合は各種マイコンに必要なシステムコール関数が実装されていないため、printf しようとしても 100% 失敗します。というか、PC のように標準出力が規定されていないので、この挙動/エラーは当然です。

そこで私は手前味噌ですが、この記事を参考に各種関数の自作をしていました。
例えば以下のように。

int _write(int file, char *ptr, int len) {
    int todo;

    for (todo = 0; todo < len; todo++) {
        if( ptr[todo] == '\n' ) {
            usart_send('\r');
        }
        usart_send(ptr[todo]);
    }
    return len;
}

関数名ですが、ちょうど SSD 内から見つけたもので、このアーキテクチャではコンパイル時は関数名に _ が前置されないらしいので、C言語側で付けています。確か RX とかは _ が前置されるらしいので、その場合はC言語側で int write(...) としてもらえばいいです。

この方法はデフォルトで存在しない関数だからできる荒業(?)であったのです。

標準ライブラリが実装されている環境でのオーバーライド

既に実装が済んでいる環境の場合はちょっと特殊です。というかつい最近このパターンを経験しました。現在やっている AVR32 です。

上の関数を syscalls.c に書き込んで、ビルドをしましたが反映されていません。どうやら標準ライブラリにある関数が優先してリンクされるようです。困りました。

オーバーライドするオプション

既存の関数をオーバーライドできる? - Ryoの開発日記 Neo!を参考にしました。本当は gcc とか ld のドキュメントを読むべきなんでしょうが。

どうやらオプションで --wrap hoge と書くと、システムコールで実装されている hoge という関数(シンボル)をリンク時にオーバーライドできるみたいです。CSS でいう !important みたいな感じかな。それで、オーバーライドされる関数名は __wrap_hoge である必要があるらしいです。やってみましょう。

実装・確認してみる

まずは以下のように関数名を変更します。

int __wrap__write(int file, char *ptr, int len) {
    int todo;

    for (todo = 0; todo < len; todo++) {
        if( ptr[todo] == '\n' ) {
            usart_send('\r');
        }
        usart_send(ptr[todo]);
    }
    return len;
}

リンカに渡すオプションを追加します。--wrap _write ですね。すると _write__wrap__write に置き換わります。もっと正確に言うと、_write を参照する命令が全て __wrap__write を参照するようになります。

ただ、これが有効になる関数は static リンクをされるものに限るということに注意です。例えば Linux の libc はスタティックリンクがサポートされていないっぽいので、その点注意です。

オプションなしでの結果

まずこのオプション無しでビルドしたバイナリのシンボル名を見てみましょう。

$ nm output.elf | grep write
80002d74 T __sfvwrite_r
800038dc T __swrite
80002994 W _write
80003938 T _write_r

その中身はこんな感じです。中身としては特に中身がないという感じです。よく分からないですね。

80002994 <_write>:
80002994:   30 48           mov r8,4
80002996:   d6 73           breakpoint
80002998:   3f fc           mov r12,-1
8000299a:   35 8b           mov r11,88
8000299c:   58 0c           cp.w    r12,0
8000299e:   5e 4c           retge   r12
800029a0:   48 2a           lddpc   r10,800029a8 <_write+0x14>
800029a2:   95 0b           st.w    r10[0x0],r11
800029a4:   5e fc           retal   r12
800029a6:   00 00           add r0,r0
800029a8:   00 00           add r0,r0
800029aa:   06 0c           add r12,r3

オプションありでの結果

続いて先程言ったオプションありでやって見ましょう。シンボル一覧はこちら。

$ nm output.elf | grep write
80002da8 T __sfvwrite_r
80003910 T __swrite
80002888 T __wrap__write
8000396c T _write_r

確かに、_write__wrap__write に置き換わっていることが分かると思います。

80002888 <__wrap__write>:
80002888:   d4 21           pushm   r4-r7,lr
8000288a:   30 06           mov r6,0
8000288c:   14 97           mov r7,r10
8000288e:   16 95           mov r5,r11
80002890:   30 a4           mov r4,10
80002892:   c0 c8           rjmp    800028aa <__wrap__write+0x22>
80002894:   2f f6           sub r6,-1
80002896:   0b 88           ld.ub   r8,r5[0x0]
80002898:   e8 08 18 00     cp.b    r8,r4
8000289c:   c0 41           brne    800028a4 <__wrap__write+0x1c>
8000289e:   30 dc           mov r12,13
800028a0:   f0 1f 00 05     mcall   800028b4 <__wrap__write+0x2c>
800028a4:   0b 3c           ld.ub   r12,r5++
800028a6:   f0 1f 00 04     mcall   800028b4 <__wrap__write+0x2c>
800028aa:   0e 36           cp.w    r6,r7
800028ac:   cf 45           brlt    80002894 <__wrap__write+0xc>
800028ae:   0e 9c           mov r12,r7
800028b0:   d8 22           popm    r4-r7,pc
800028b2:   00 00           add r0,r0
800028b4:   80 00           ld.sh   r0,r0[0x0]
800028b6:   28 6c           sub r12,-122

関数を呼んでいますね。良さそうです。ちょっと詳しく見てみましょう。

0x800028b4 の正体

2箇所 mcall 800028b4 という命令がありますね。この命令はレジスタおよび即値アドレス(へのポインタ)をオペランドにとって、そのアドレスの関数をコールする命令です。
このオペランドは 0x800028b4 であります。そこを見ると、0x8000286c とあります(このアーキテクチャはビッグエンディアンなので頭から読んでいけばいいです)。つまり mcall 800028b40x8000286c に飛べ という命令になるわけです。
で、その肝心の 0x8000286c は...

$ nm output.elf | grep 8000286c
8000286c T usart_send

はい。これで正しく動作するであろうことがわかりました。