前回までは、Armコアとは言え、Linuxが立派に走るCortex-A系のプロセッサ上で、GNUのツールチェーンを使い、「わずかに」アセンブラ世界の入口に到達しました。しかし、目の前に置いてある Arm Cortex-M系のプロセッサを搭載したマイコンボード共が、最近使ってないやんけ!とお怒りな気がします。今回は、ArmのWeb開発環境、Mbed Compiler を使ってマイコン上でアセンブラ書くための第1歩を踏み出してみます。ツールチェーンもArm純正、GCCとは一味違います。
※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら
マイコン用の組み込みプログラムを開発する場合、昔はフルアセンブラで何から何まで書くのがあたり前でしたが、今時それをしているのは、多分、マイコン界の絶滅危惧種と言える4ビット機と、機能に制限の強い下位の8ビット機種くらいでしょう。大部分をCなりC++なりで書いて、どうしても必要な部分があればアセンブラレベルでプログラミングをするというのが一般的でしょう。そんなC言語などとアセンブラレベルのプログラミングを混在させる場合、以下の4レベルの方法がある、と思います。
- コンパイラ・イントリンシックスを使用
- インライン・アセンブラを使用
- 組み込みアセンブラを使用
- アセンブラで作ったオブジェクトとCで作ったオブジェクトをリンク
第4の方法は前回使った方法(gnuツールチェーンですが)なので今回はパスです。
第1のコンパイラ・イントリンシックスは、Armでもx86でも提供されています。アセンブラレベルの命令をCコンパイラの関数か、マクロのように使えるように「ラップした」ものです。コンパイラにより実際のアセンブラレベルの命令に展開されます。アセンブラらしいコーディングをまったくせずに済むだけでなく、コンパイラはイントリンシックスの展開方法を「知っている」ため、前後のCのコードとの接続も効率がよく、また、Cコードと連続で最適化まで施してくれます。もし、イントリンシックスが準備されている範囲で所望の目的が果たされるのであれば、第1に検討すべきチョイスでしょう。使用する局面としては、SIMD命令を使いたいといった場合かと思われます。これはこれで奥深いテーマまので、ちょっと触ってみるだけの今回はスルーして、そのうち取り組みたいと思います。
第2のインラインアセンブラの使用は、ラズパイ3の上でカーネルモジュールをちょっと書いてみた回でやってみています。ただし、この時はgccをつかって、gccのインラインアセンブラを利用しました。インラインアセンブラは、前後のCのステートメントの中に、突然アセンブラ記述を潜ませることになります。そのため問題になるのが、インラインアセンブラの引数となる値をどのレジスタで受け取るか、あるいかどの値をCに戻すか、そしてどこのレジスタを書き換えてしまったかをコンパイラに知らせるかという点です。
gccの場合はコンストレイント(制約)
の記法がプロセッサアーキ毎に定義されており、入力、出力、破壊したレジスタを記述するとともに、Cの変数との対応や、アセンブラ内でレジスタを参照するための使うマクロなどを定義することになります。また、直接物理レジスタを参照できてしまいます。
ところが、Arm純正コンパイラが備えるインラインアセンブラはgccとはかなり性質が異なります。
直接物理レジスタを参照してはいけない
基本はアセンブラニーモニックで記述するのですが、Cの変数がレジスタであるかの如くに記述をします。gccのような面倒な設定は不要。vbとかiがCで定義された変数であるとすると、以下のように書くだけで済みます。
__asm("add vb, i");
かなり楽ちん。また、インラインアセンブラが生成するコードは実際の関数ではないので、returnしてはいけません。なお、誰かがarmcc用のインラインアセンブラの中で、r0とかレジスタ名に見えるものを書いていたとしても油断はできません。よく見るとインラインアセンブラの外に
unsigned int r0;
みたいな変数宣言を見つけられるでしょう。gccのインラインアセンブラとは大分違うので慣れないとなりますまい。
第3の組み込みアセンブラですが、gcc上で該当するものを私は知りません。インラインアセンブラ同様、
__asm キーワード
を使って定義するのですが、__asm()の引数的な定義をするインライン関数とは異なり、関数定義を __asmで修飾します。
__asm int add3_embasm(int i) { add r0, #3 bx lr }
Cから見たら、関数みたいに見えますが、中身はもろアセンブラです。同じキーワードで定義されるのに性質もまったく違います。
- 組み込みアセンブラが生成するのは本物の関数なので、return (bx lr命令)する必要がある。また、関数への引数、関数の戻り値などはArm EABIに基づいて呼び出し側はインタフェースしてくるので、アセンブラ側はそのつもりで書かないとならない。(例、戻り値はr0に格納するなど)
- 組み込みアセンブラは物理レジスタにアクセスできる。その代わり、内容を破壊してしまうレジスタがあればアセンブラ内で適切に退避、復帰しなければならない。
そして組み込みアセンブラ部分は、コンパイラ最適化もスルーされるようです。アセンブラのコード列を自分の思う通りに制御したい場合に便利。ブートコードのようにアセンブラがエントリポイントになって立ち上がる場合を除けば、組み込みアセンブラがあれば、ほとんど足りてしまうような気がします。
これに対してインラインアセンブラの方は、必要であればレジスタの退避も復帰もコンパイラが面倒見てくれるだけでなく、前後のCのステートメントと合わせてコンパイラ最適化の対象にしてくるようです。お任せで楽ですが、gccのインラインアセンブラと同様、無駄に見えるコードを書くと最悪消えてしまうかもしれません。
最後に、上の2つの小さなアセンブリ言語コードを、ST microelectonics製Nucleo-F401RE(CPUはCortex-M4)用にArm Mbed環境でビルドし、ボードに書き込んで動かしてみた結果を示します。INL>とあるのがインライン関数の結果で、上の変数vbにiを代入した後呼び出しており、iは毎回1づつインクリメントしているので、vb値はi値の2倍となり、それを表示しています。EMB>とあるのが、組み込みアセンブラの方で、こちらは、i(画面では2から始まっている)に対して定数3を足した結果を戻しています。