新千葉 ガーベージ・コレクション

FPGA マガジンやインターフェースで書けなかったこと等をちょぼちょぼ書いてます。@ryos36

C++ を embedded な環境でつかう

結局、素の C++ を使うのがベスト。うっかりすると素ではなくていろんなものに依存するので注意。たとえば arm-xilinx-linux-gnueabi-gcc/g++ これは当然、Linux 環境に依存する。だから単純に printf とかすると Linux の /usr/include/stdio.h の printf を使おうとしたりする。
arm-xilinx-eabi-gcc/g++ 。これは(xilinxが) newlib + libstdc++ に(意図的に)依存させて作っている。したがって、printf とかすると newlib の stdio.h の printf を使おうとしたりする。なので、newlib が要求する Unix システムコール(もどき)のシミュレーションを(自分で作ったライブラリを)用意する必要がある。例えば open とか read/write とか close とか。xilinx はこれを Standalone と称して用意している。さらに、リンク時には、”コンパイラを作った時の”環境に依存する crt (C RunTime ?) をリンクする。xilinx は作った時に何も考えていなかったのか、空の crt がリンクされる。crt にはシステムの初期化用の _init が含まれる。
embedded な環境で使うには -nostdlib を指定すると、crt やら libc libstdc++ やらをリンクしない。
g++ でコンパイルするとデフォルトではデストラクタの登録に __cxa_atexit を使おうとする。cxa はどうやら c++(cxx) abi の略のようだ。g++ では -fno-use-cxa-atexit を指定すると cxa_atexit を使わないオブジェクトを生成することが出来る。__aeabi_atexit が undefine の時はこれで”一時的に”解決する。全部解決したと思わない方がよい。RTOS などの設計にもよるが、いつデストラクタを呼ぶのかはちゃんと考えないといけない。タスクやスレッドが終わった時にどうするのか?が主な注意点だろう。これは RTOS の設計。
libc を自分で設計するならリエントラントに設計する必要があるかどうか?それらの情報どうするかは考えないといけない。g++ は exception を使う設定にしておくと、__gxx_personality_v0 と __cxa_end_cleanup の実装を要求される。これはいずれも exception ハンドラに関連する関数。特に __gxx_personality_v0 は try catch を使うなら実装が必須。最近では setjmp/longjump は使わないみたいだ(未確認)。下のblogが役に立ちそう。

g++の例外を捕まえよう!!(素手で)

exception を使わないなら(不便になるけど)-fno-exceptions を使えばよい。結局、g++ ではオプションで次の設定をしておけば(コンパイラ自身が持っている環境を無視するので)だいだい使える。

-nostdinc -nostdlib -fno-use-cxa-atexit -fno-exceptions

さぁこれで vx-schemeコンパイルがどこでもできるんじゃないかな?

やってはいけないこと

そんでもって -nostdlib を選択したということはコンパイラが持つ libc や libstdc++ を使わないということ。なので、-lc なんかしちゃだめ。-lm も当然ダメ。中途半端にライブラリをリンクすると dso_handle がないとか言われる。あと __impure_ptr 。これは newlib の環境内のリエントラントな構造を保つ大域変数。つまり newlib が前提。newlib を前提としてコンパイルされた libstdc++ は exception の回収にこのポインタを使う。だから、そんなことが起こったらきっと環境が間違っている。exception を使わないことがわかっているなら -fno-exceptions で回避できる。もし、使うなら、自分で unwind するための関数である 、__gxx_personality_v0 と __cxa_end_cleanup の実装が必要。
まちがっても、いろんなものを”くっつけて”解決しようとしないように。私は dummy.c というファイルを作って、知らない関数や変数をいっぱい定義して回避している”プロジェクト”を見たことがある。当然、崩壊してたね。まぁ趣味ならいい気はする。

new や delete

これをつかうと動的にメモリ確保開放を必要とする。RTOS で使うときは注意。動的にメモリ獲得するということは結局ガベージコレクション(的な現象)を生む。これはスクリプト言語を使わなくても malloc を使えば起こるということ。malloc の実装にもよるけど、メモリが足りなくなったらエラーになるならまだよいが、他のタスクが解放してくれるのを待つという実装になっていると RTOS の意味がなくなる場合(ケースバイケースだが)がある。だからやってはいけないというわけではなくて、注意。
まぁ TCP/IP つかっちゃうと結局動的にメモリ確保をするので(mbuf とかね)ここも本来注意なんだけど、、、最近では気にする人はいなくなっちゃたね。
new や delete を使うと operator が足りないと言われるので(言われなかったら何か怪しい libstdc++ をリンクしているぞ。危険)自分で実装する。malloc で実装してもよいし、簡単に自分で RTOSAPI を使ったり、単純な配列を使ったりしてもよい。要は delete と連動していればよい。
あと、うろ覚えだけど、クラスに new や delete を関連付けることが出来たような、、、、
placement new と delete(2)

placement new

RTOS ではこちらがおすすめ。new や delete のオペレータを使わない。
placement new と delete(1)

自分専用の gcc を作る

何度かトライしたけどめちゃめんどくさい。みんないい加減にやっている(やっていた)。しかし、Android や Yocto がうまくコンパイラコンパイルを隠ぺいして開発環境を自動的に作ってくれる環境を作ったので、みんな何も考えずにできるようになったね。ぱちぱち。2000 の初めにはなんか自分で gcc をつくらなきゃいけなかったので、毎晩、nightly build を追ってた。特に sh。
Linux 用のコンパイラを作る手順はざっと書くと次のような感じ

  • build 用のコンパイラを作る(i386 -> i386 ってことね。最近では x64 -> x64 かな)
  • cross 用のコンパイラをとりあえず作る。たとえば arm-unkown-eabi-gcc みたいなもの。newlib に依存してもしなくてもよい。これはいずれ捨てる。
  • arm-unkown-eabi-gccLinux カーネルコンパイルする。これで Linux の一時環境が出来た。この時点でやろうと思えば Linux のアプリは作れる。APILinuxシステムコールだけだが。ここで用意されるのは Linux が用意する include の類でこれをどっかクロスコンパイラ用 root にインストールする。
  • libc をつくる。Linux の環境とコンパイラがあれば libc が作れる。作れるはずなのだが、、、libc を作るときに libc 入りのコンパイラを要求されたり、例によって途中でバージョンの依存関係でエラーになったりと、苦労する。jこれを Linux の クロスコンパイラ用 root にインストール。ここを明示的に参照することで Linux アプリが書ける。(はず)
  • Linux の クロスコンパイラ用 root を元に cross 用のコンパイラを作り直す。arm-linux-gnueabi-gcc みたいもの。このクロスコンパイラは include とか lib とかはこのクロスコンパイラ用の root を"自動的に"参照することになる。
  • 出来たコンパイラで次々とライブラリをコンパイル・インストールする。そうすると、クロスコンパイラ用 root がいつのまにか Linux の root のようになる。これをファイルシステムにすればよい。これはオールドスタイルの root fs だ。
  • 出来た クロスコンパイラ用 root は実は i386 で動くクロスコンパイラが出来てたりする。だから、これをファイルシステムにしてしまうと i386 オブジェクトが含まれることになる。そこで、deploy 用の root fs を別に作る。ここにこつこつライブラリなどをインストールすれば本当の root fs ができる。

これを gcc のバージョン違いやいろんなライブラリを作るときのエラーの回避なんかをしてると、らちが明かない。Yocto は上を全部自動でやってくれる。そして、注目したいところだけ、自分でつくりこめばいいのだ。これは楽だね。

基本的には RTOS も上の手順を踏めば、正しく標準ライブラリを組み込んだ gcc ができる(はず)。