ぐだぐだ低レベルプログラミング(2) ラズパイでアセンブラ、最初の一歩かな?

JosephHalfmoon

遥かな昔、結構アセンブラ書いていた時期があるのですが、この頃(といっても多分二十年くらい前から)はあまり書かなくなりました。仕事のせいもあるけれど、多分、昔に比べたら書かなくても済むことが多いから。そのせいもあってArmには時折お世話になっておりながら、ほとんどArmでアセンブラ書いたことがありませぬ。最近、Raspberry Pi 3 model B+ を触ることが結構多く、この際、ちょっとArmのアセンブラを勉強しておこうと思い立った次第。

前回はインラインアセンブラを使って、特権レジスタアクセスのコードを書いてみました。ただ、本格的にアセンブラ書こうと思うと、インラインアセンブラは何かと不便。それにCから見たら、Cのコードなのに可搬性が低くなる上に、正直、ソースの字ズラが汚くなって嫌。アセンブラからみたら、「自由」なアセンブラの世界なのに、Cコンパイラに起因するあれこれが侵食してきてイマイチ楽しくない。とは言え、小さめのアーキの8ビット・マイコン、あるいは絶滅危惧種の4ビットマイコンでもなければ今時コンパイラを全く使わないという選択は無いでしょう。大きな流れはCで書いて、Cで書き難いところ(特殊なレジスタ操作など)や、コンパイラのコード生成に不満があるところ(とは言え昨今のコンパイラの最適化を打ち負かすコードを書くのはそれなりだと思いますが)、だけアセンブラで書く、という感じでしょうか。今回は、

Cのソースから、別ファイルのアセンブラの関数を呼び出す

ところの最初の一歩をおさらいしてみたいと思います。Raspberry Piなので、使用するのは、as というコマンド名で呼び出される gas という名のアセンブラです。

課題:アセンブラで定義した関数を呼び出して、結果をCで印字

も少し細かく書くと以下のようになります。

  1. アセンブラ関数は何か数値を1個返すだけ(今回は整数「5」としてみました)。関数名はベタに get5 としてみました。
  2. Cのメイン関数は、アセンブラ関数 get5 を呼び出して、予定通り5をもらい、それを printf しておしまい。

まず、書いたのはアセンブラとCのソースを繋ぐもの、そうですヘッダファイルです。Cのための。

// sample000 header file
int get5(void);

実質1行ですが、過不足なく必要なことが記されておると。引数は無し。関数名は、get5、戻り値は整数型。Rapberry Pi 3 の上では 勿論、int型は32ビット符号付き整数であります。

これがあれば、Cのコードは赤子の手を捻るようなもの(野蛮な例えだな)、

#include <stdio.h>
#include "sample000.h"
int main() {
    int work;
    work = get5();
    printf("Result: %d\n", work);
    return 0;
}

こんなの、printfの1行でもかけますが、後でデバッガを使ったときに、止めやすく、見やすくするために「分かち書き」いたしました。

さて本題のアセンブラ。こんな感じ。

                .global         get5
                .text
                .align          4
get5:
                ldr                     r0, =5
                bx                      lr

最初の行、”.”で始まる疑似命令、

.global get5

は、これから登場する get5 というラベルは、グローバル、このソースファイルの外から参照できるようにする、という宣言であります。次の疑似命令

.text

は、「テキスト・セグメント」に以下のコードを配置するという宣言。どのアセンブラでも大抵そうですが、テキストと呼ばれるのは、機械語命令やリードオンリの定数などを格納する領域です。Flashマイコンであれば、テキストが置かれるのはFlash ROM。プログラムの実行中は普通書き換え不能な部分。Raspberry Piの場合は、立派なLinuxが走っているので、テキスト・セグメントも結局DRAMの上におかれ、物理的には書き換え出来てしまいますが、リンカその他の皆さんの手前、テキストはテキストとして分類しておかねばなりません。その次の行、

.align 4

は、4バイト境界に「整列」させよ、という疑似命令。最近のツールチェーンはお任せでもなんとかなるものも多いです。けれどミスると、CPUのアーキテクチャにもよりますが、スピードが遅くなったり、ミスアライメント例外が起こったり良いことはありません。原則、これから「配置」するものの幅を意識してアライメントしておきます。

32ビットの変数なら、32ビット=4バイトなのだから 4バイトアラインメント

こんな感じです。その次の行

get5:

はラベルです。その場所(アドレス)に名前を付けたというか、get5というシンボルにその場所のアドレスを与えた、というべきか。まさにリンカは、アセンブラソースからアセンブルされたオブジェクトコードの中の get5 を知り、Cコンパイラからコンパイル(後ろの方でアセンブルさている)されたオブジェクトコードが呼び出している get5 を知り、両者を結びつけてくれるわけであります。

次の行になって、ようやくArm の命令が登場しました。

ldr r0, =5

ldr 命令は、何か「値」をレジスタにロードするときに使う命令で、この場合、r0レジスタに値5を載せる、という意味です。ここに=記号が登場しますが、これはArm独特な記法じゃないかと思います。Armに限らずRISC系のCPUは、即値(そのまま数値として扱えるオペランド)の幅が限定されることが多く、真っ正直に書くと、何ビットまでの即値ならこの命令、それ以上はこの命令とかとてもうるさいです。流石にそういう不便をアセンブラが補完してくれることも多いです。Armにおいては、即値につけるこの=記号がその手品の種かと。そして、r0に書き込んでいますが、これは、Armが決めた「お約束」であります。

32ビットの整数型の場合、r0の値が関数戻り値だもんね

と決めた(EABI)ので、そこに書き込めば、Cコンパイラが生成した戻った後のコードも期待通りに扱ってくれる、と。最後、

bx lr

x86のアセンブラをご存知ならば RET (サブルーチンからの戻り)命令に相当します。Armを含めたRISCでは、x86のように戻り番地を勝手にスタックに積んでおくということはなく、リンク・レジスタ(それで lr )に戻り番地を書いて CALL (実際にはcall という命令もないですが)するので、戻るときは lrの内容をPC(プログラムカウンタ)へ書き戻す形をとります。

よし出来た、gasを呼び出すか? 勿論、gas(as)呼んで、gcc呼んで、ld(リンカ)呼んで、と個別にやっても良いですが、偉大な gcc は「ドライバ」であります。Cのソースをコンパイルするやり方だけでなく、アセンブルの仕方も、リンクの仕方も「知っている」ので、gccにお願いすれば、一撃でやってくれます。

$ gcc -g sample000_main.c sample000.s -o main

拡張子 “.s” がついているのが上のアセンブラコードです。-g オプション付けました。勿論、後で gdb 使ってデバッグするためです。一瞬でビルドは終わり、

$ ./main
Result: 5

問題なく実行できているので、何もすることはないのですが、一応、デバッガも起動してみます。

$ gdb ./main
GNU gdb (Raspbian 8.2.1-2) 8.2.1
~途中略~
(gdb) b main
(gdb) run
Starting program: /home/pi/asm_train/asm/main
Breakpoint 1, main () at sample000_main.c:7
7               work = get5();
(gdb) stepi
get5 () at sample000.s:6
6                       ldr                     r0, =5
(gdb) stepi
7                       bx                      lr
(gdb) c
Continuing.
Result: 5
[Inferior 1 (process 981) exited normally]
(gdb) quit

とりあえず、アセンブラで関数書いて、Cから呼び出して動きました。本日はこれまで。

ぐだぐだ低レベルプログラミング(1) Raspberry Pi 3、PMC利用のためのカーネルモジュール へ戻る

ぐだぐだ低レベルプログラミング(3) Arm 32ビット?、64ビット? へ進む