2011年2月12日土曜日

ARMとスタックと私

さて、初めての技術的内容の投稿。
今日はとあるおじさんの発言が元で興味が湧いたのでスタック操作命令を生成しない関数を作ってみますた(´・ω・`)

そもそもC言語で関数を生成するとスタック操作命令が入っちまいます。
コール側にも入ります。CPUの呼出し規約によるだろとかいう突っ込みは受付ません。
なんでC言語の関数でスタック操作が入るかというと、

  • 戻りアドレスの格納のため
  • ローカル変数領域確保のため
  • 戻り値の格納のため

なんですが、ARMの場合(というかRISC系)の戻り値はレジスタ渡しなので上2つがスタックを使用する理由になりまふ。
ということは、

  • 呼出し元に戻らない
  • ローカル変数は使用しない
  • 戻り値返さない(上述の通り必須ではない)

という起きて破りな関数ならばスタックを一切消費しない関数が作れそうだ。
という訳でまず以下の関数を作ってみた。

コード1:
void  test(void)
{
    while(1);
}

でコンパイルすると以下のコードが生成された(関係箇所だけ抜粋)


00000000 :
   0: e52db004 push {fp} ; (str fp, [sp, #-4]!)
   4: e28db000 add fp, sp, #0
   8: eafffffe b 8

見事にスタック使ってまふ(´・ω・`)
まさにションボリ。

では次はコードを以下のように変えてみた。

コード2:

void  __attribute__((noreturn)) test(void)

{
        while(1);
}


gcc の拡張で関数属性に noreturn を設定。
さて今度はどんなコードが生成されただろうか。


00000000 :
   0: e52db004 push {fp} ; (str fp, [sp, #-4]!)
   4: e28db000 add fp, sp, #0
   8: eafffffe b 8


か、変わっとらんじゃまいか・・・( ꒪⌓꒪)
とかとかいうところでツィッターでこんなこと書いてたら、master_qからアドバイス。
”最適化しる!!”
神降臨〇(≧∀≦)o そうか、最適化で無駄コードを省いてもらえば・・・
ということで、-02 オプションをつけてコンパイルすると以下のコードになった。


00000000 :
   0: eafffffe b 0

キタ━━━━━━(゚∀゚)━━━━━━!!!!



スタック操作が消えた。最適化レベルを -O1 オプションに変えても同じコードが生成された。
とはいえ、こんな何もしない関数でスタック操作命令が排除されても嬉しくない。
ということで、外部変数の操作をちょこちょこ追加してみる。

コード3:
int  a;
int  b;
int  c;

void  __attribute__((noreturn)) test(void)
{
        a = 0;
        b = 10;
        c = a + b;

        while(1);
}

さて、どんなコードが生成されたかと言うと・・・

(最適化オプションは-01)

00000000 :
   0: e3a02000 mov r2, #0
   4: e59f3018 ldr r3, [pc, #24] ; 24
   8: e5832000 str r2, [r3]
   c: e3a0300a mov r3, #10
  10: e59f2010 ldr r2, [pc, #16] ; 28
  14: e5823000 str r3, [r2]
  18: e59f200c ldr r2, [pc, #12] ; 2c
  1c: e5823000 str r3, [r2]
  20: eafffffe b 20
...

バッチリ!!

外部変数の操作を行っていてもスタック操作命令は生成されない。
ところで、noreturn つけても生成されるコードが変わらんかったということは noreturn 付けなくてもいいんじゃまいか!?

コード4:
int  a;
int  b;
int  c;

void  test(void)
{
        a = 0;
        b = 10;
        c = a + b;

        while(1);
}




で、生成されたコードを見ると

00000000 :
   0: e3a02000 mov r2, #0
   4: e59f3018 ldr r3, [pc, #24] ; 24
   8: e5832000 str r2, [r3]
   c: e3a0300a mov r3, #10
  10: e59f2010 ldr r2, [pc, #16] ; 28
  14: e5823000 str r3, [r2]
  18: e59f200c ldr r2, [pc, #12] ; 2c
  1c: e5823000 str r3, [r2]
  20: eafffffe b 20
...

うむ、やはり noreturn は要らない子だったようだ(´・ω・`)
ところで逆アセンブル結果に含まれてる ... は一体なんなんだぜ!?

という訳でスタックを使用しない関数を生成することはできた。
ソフトウェアではとあるタイミングではスタックを使用することができないので、その部分の関数はこうやって書けばよいと。
とはいえ、これだけだと何もできないので、別の関数を呼ぶことを考えるお(´・ω・`)
手始めに以下のコードなんてどうだろう。

コード5:
void  test2(void);

void  test(void)
{
        asm volatile("b test2\n");
        while(1);
}

void  test2(void)
{
        return;
}

なんかジャンプしてるだけ( ꒪⌓꒪)
さて結果は・・・

00000000 :
   0: eafffffe b 8
   4: eafffffe b 4

00000008 :
   8: e12fff1e bx lr

なるほど、スタック消費されない
今度は下のように外部変数いじるのと関数ジャンプを入れてみる。

コード6:
int  a;
int  b;
int  c;

void  test2(void);

void  test(void)
{
        a = 0;
        b = 10;
        c = a + b;
        asm volatile("b test2\n");
        while(1);
}

void  test2(void)
{
        return;
}

生成されたコードは・・・
00000000 :
   0:   e3a02000        mov     r2, #0
   4:   e59f301c        ldr     r3, [pc, #28]   ; 28
   8:   e5832000        str     r2, [r3]
   c:   e3a0300a        mov     r3, #10
  10:   e59f2014        ldr     r2, [pc, #20]   ; 2c
  14:   e5823000        str     r3, [r2]
  18:   e59f2010        ldr     r2, [pc, #16]   ; 30
  1c:   e5823000        str     r3, [r2]
  20:   eafffffe        b       34
  24:   eafffffe        b       24
        ...

00000034 :
  34:   e12fff1e        bx      lr

いいじゃないか、いいじゃないか。
ということで、スタックを使用しない関数を生成する条件は、以下であると(´・ω・`)
  • 呼出し元に戻らない
  • ローカル変数を使用しない
  • 外部変数は触ってもよい
  • 最適化は -O1 以上
  • noreturn は付けなくてもいい
じゃあなんでこんなことやってるかと言うとスタックが触れないからだ。
そんなら初期化したらええやん(´・ω・`) ということで以下のスタック初期化して他の関数を呼ぶ関数を作ってみた。

コード7:
#define  STACK_ADDR   (0x10000000)

void  test2(void);

void  test(void)
{
        asm volatile("mov sp, %0"::"r"(STACK_ADDR));
        asm volatile("b test2\n");
        while(1);
}

void  test2(void)
{
        return;
}

するとコード生成は
00000000 :
   0:   e3a03201        mov     r3, #268435456  ; 0x10000000
   4:   e1a0d003        mov     sp, r3
   8:   eafffffe        b       10
   c:   eafffffe        b       c

00000010 :
  10:   e12fff1e        bx      lr

いいーーーじゃまいかーーーー!!!

でもどうやってもtestの最初の r3 への代入がとれなかった。効果があるのかどうかわからんが -O6 とかやってもダメだった(´・ω・`)
最初から sp に代入するようなコード生成して欲しいのに。
また誰か神が降臨するのを待つか。
ということで実験終了。

おまけ:
スタックは消費しないんだけど、関数はコールしたいじゃないか!!
あとアセンブラの中に直接呼出し関数名書きたくない!!
ということで以下のマクロを考えた。
きっと世の中には既に存在しているに違いないが・・・(´・ω・`)

#define  CALLFUNC(x) asm volatile("mov lr, pc;b "#x)

void  test2(void);

void  test(void)
{
        asm volatile("mov sp, #0x80000000;");
        CALLFUNC(test2);
        while(1);
}

void  test2(void)
{
        return;
}

生成コード
00000000 :
   0: e3a0d102 mov sp, #-2147483648 ; 0x80000000
   4: e1a0e00f mov lr, pc
   8: eafffffe b 10
   c: eafffffe b c

00000010 :
  10: e12fff1e bx lr

赤字のところがマクロに相当。いい感じじゃないか。
とかとかすると、呼び出された関数test2が lr を使って使って返ってくることもできる(´・ω・`)

・・・ふむ、直値で入れると先の余計なコードが消えてるな。
コレをマクロ化することを考えよう。

てな感じで今日の実験は終了。
おつかれさま(´・ω・`)

――――――――

今日や明日のことでは無く、もっと未来の方へ向かう。そう、明後日の方向へ!!

1 件のコメント:

masterq さんのコメント...

なんかこの方式の延長線上でプログラミングするには関数型っぽい設計、つまり無限再帰を使った方がよさそうな悪寒がしてきました。