これまでマイコンで 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 800028b4
は 0x8000286c
に飛べ という命令になるわけです。
で、その肝心の 0x8000286c
は...
$ nm output.elf | grep 8000286c
8000286c T usart_send
はい。これで正しく動作するであろうことがわかりました。