途中参加。
前回
[http://shigemk2.hatenablog.com/entry/2015/02/25/%E7%86%B1%E8%A1%80%EF%BC%81%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%A9%E5%85%A5%E9%96%80%E8%AA%AD%E6%9B%B8%E4%BC%9A%289%29%23hotasm:embed]
[http://shigemk2.hatenablog.com/entry/2015/02/25/%E7%86%B1%E8%A1%80%EF%BC%81%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%A9%E5%85%A5%E9%96%80%E8%AA%AD%E6%9B%B8%E4%BC%9A%289%29%23hotasm:title]
スタック
- スタックの使いかた
- 関数の呼び出しかた
関数呼び出しの際には,関数内の自動変数は呼び出し先の関数から処理が戻ってくるまで,内容が保存されている必要があります.でないと「関数呼び出しをすると変数の値が壊れてしまう」ということになってしまうからです.
- 関数呼び出しを行うたびに任意のサイズの領域を獲得する
- 関数呼び出しはネストする
呼び出しもとの関数の領域にアクセスできる必要は無い
ある程度のサイズのメモリ領域を確保する
- メモリ領域のどこまでを使っているか?を指し示すポインタを 1個,用意する.
- 関数呼び出しで自動変数の領域が必要な場合には ポインタを移動させる.す ると移動したぶんの領域は「使用中」の意昧になる
- 関数呼び出しから戻ったら ポインタを元の位置に戻す.するとそのぷんの領 域は「未使用」の意昧になる.
- 関数呼び出しがネストする場合には,どんどんポインタを移動させていく
関数のローカル変数の内容を残したまま、次の関数のメモリ領域を確保する。呼び出しが終わったら消される。
呼べば呼ぶほど積み上がる→スタックは積み木のイメージ。
図で言うと、どれだけ積み木のスタックポインタをずらすかは説明をぼかしてあったりする。
関数を使うとメモリ量が増えるので、スタックポインタの位置も変わる。
スタックポインタの位置を境にして、使用中領域と未使用領域が分割されている。
関数が呼び出されれば呼び出されるほどスタックポインタの位置は上がっていき、関数呼び出しが終了するとスタックポインタの位置は下がる。
↑の例でいうと、デファクトスタンダードとしてはアドレスの番号は上のほうが若い。
(一般論として、上が0で、下の方に向かって大きくなっていくことが多い)
以下がポイント。
スタックで行いたいのは「関数内で利用する領域の確保」で,行っているのは「スタックポインタの加減算」のみということになります。
アセンブラ的に言うと、スタックがやっていることは非常に簡単である。
mallocは結構複雑で、積み木の途中をfreeすることは出来る。
c.f. コルーチン - Wikipedia
9.2 MIPSのスタック操作
今からMIPSを見ていこうと思う。
volatile ぼらたいる
このset_stack()は本当は何もしていない。
職業としてのプログラミング volatileで最適化を抑制する
変数を「volatile」として定義しているのは、定義するだけで実際の利用が無いとコンパイラの最適化により削除されてしまうので、最適化を抑制するためです。volatileキーワードはアセンプラのサンプルが期待通りに出力されるようにチューニングする際によく利用されるので、覚えておくといいでしょう。
マルチスレッドプログラミングのときにvolatileを使うことがある。メモリ上に必ず確保されるようになる…のか。
が、この本の文脈としては、コンパイラの最適化を抑止することが目的。
あと、#define UNUSED attribute*1 はwarning抑止のためのマクロ(使ったことない変数はコンパイラが警告を出す)で、未使用変数であることをコンパイラに伝えるgccの独自拡張。
例。C言語入門ですらなく、gccの魔除け。
目指せ! Cプログラマ(16):プリプロセッサでプログラムの質を向上させよう (2/4) - @IT
[http://www.wdic.org/w/TECH/attribute:title]
二重括弧でくくるのには理由が存在し、それはGCC以外では、マクロで無視できるようにするためである。複数の引数があっても、括弧でくくってあれば、マクロは一つの引数としてそれを扱うことができ、次のようにして無視することができる。
可変長引数マクロ
attributeは普通使わないと思う。
で、use_stack()も最適化を抑止するためにvolatileを使っている。
#define UNUSED __attribute__((unused)) void set_stack() { UNUSED volatile int a = 0xfe; UNUSED volatile int b = 0xff; } int use_stack() { volatile int a = 0xfe; volatile int b = 0xff; return a + b; }
00fe1508 <set_stack>: fe1508: 27bdfff8 addiu sp,sp,-8 fe150c: 240200fe li v0,254 fe1510: afa20000 sw v0,0(sp) fe1514: 240200ff li v0,255 fe1518: afa20004 sw v0,4(sp) fe151c: 03e00008 jr ra fe1520: 27bd0008 addiu sp,sp,8 00fe1524 <use_stack>: fe1524: 27bdfff8 addiu sp,sp,-8 fe1528: 240200fe li v0,254 fe152c: afa20000 sw v0,0(sp) fe1530: 240200ff li v0,255 fe1534: afa20004 sw v0,4(sp) fe1538: 8fa30000 lw v1,0(sp) fe153c: 8fa20004 lw v0,4(sp) fe1540: 00000000 nop fe1544: 00621021 addu v0,v1,v0 fe1548: 03e00008 jr ra fe154c: 27bd0008 addiu sp,sp,8
やっていることは変数a,bに値を代入している。
関数の先頭ではスタックポインタの操作が行われている
MIPSはレ点の遅延スロットなのを思い出しましょう。
spは「スタックポインタ用のレジスタ」で、spを加算減算している。
27bdfff8 addiu sp,sp,-8 .... 27bd0008 addiu sp,sp,8
スタックの獲得、スタックの解放。加算したら減算しないとスタックオーバーフローになる。
スタックポインタを減算すればそれがそのぶんのスタックの獲得になり、スタックポインタを加算すればそれがそのぶんのスタックの解放になります。
下方伸長は、下が0で、上が最大。引き算することで値がどんどん減っていく(=スタック領域が増える) スタックを獲得するときに引き算している例のアレ。
C言語で関数を定義すれば,コンパイラがそのようなコードを自動的に生成する。
最近は上を0にするのが多い気がしている。
スタックフレーム
関数のために獲得されたスタックの領域のこと。
MIPSのlwとswはおさらいしておこう。MIPSのlwとswのオペランドの順番は、レジスタ→メモリの順。
[http://shigemk2.hatenablog.com/entry/2015/01/28/%E7%86%B1%E8%A1%80%EF%BC%81%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%A9%E5%85%A5%E9%96%80%E8%AA%AD%E6%9B%B8%E4%BC%9A%287%29%23hotasm:embed]
[http://shigemk2.hatenablog.com/entry/2015/01/28/%E7%86%B1%E8%A1%80%EF%BC%81%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%A9%E5%85%A5%E9%96%80%E8%AA%AD%E6%9B%B8%E4%BC%9A%287%29%23hotasm:title]
ローカル変数の初期化のためにメモリ領域を確保している。(1つの変数につき4バイトずつ)
初期化しているだけでjr raしてリターンしている。ちなみに、遅延スロットでレ点なので、次の命令を実行してからジャンプしている。
で、スタックポインタの加算でスタックが解放するので、今回確保したスタックフレームは事実上未使用。
スタック上の値を利用する
use_stackのアセンブラを見よう。
00fe1524 <use_stack>: fe1524: 27bdfff8 addiu sp,sp,-8 fe1528: 240200fe li v0,254 fe152c: afa20000 sw v0,0(sp) ! メモリ書き込み fe1530: 240200ff li v0,255 fe1534: afa20004 sw v0,4(sp) ! メモリ書き込み fe1538: 8fa30000 lw v1,0(sp) ! メモリからロード fe153c: 8fa20004 lw v0,4(sp) ! メモリからロード fe1540: 00000000 nop fe1544: 00621021 addu v0,v1,v0 ! v0 = v1 + v0 fe1548: 03e00008 jr ra fe154c: 27bd0008 addiu sp,sp,8
volatileをとると、逆アセンブラの内容がおそろしく簡略化され、
jr ra li v0,509
の2行になる。メモリへの書き込みとロードが省略される。ので、敢えてvolatileを使っている。なお、どちらもint a int bを使っているので、UNUSEDマクロは使わない。
- nostdlibcはライブラリを使わないオプション。
- Oをつけないと最適化しない。
- でもMIPSである限り遅延スロットはある。
- nopsを入れてちょっと待ってみることをやっている。
プログラミング :: 高速なプログラムを書く為に :: コンパイラの最適化
最適化しないとどうなるの
最適化を抑止してアセンブラをスッキリさせてもいいし、させなくてもいい。
- 自動変数を利用しているので,スタックを獲得する
- 自動変数の実体をスタック上に作成する
- 自動変数の利用時には、スタック上の変数の実体をロードする
今どきはこれくらいのコンパイラの最適化はあたりまえだけど、初見だとびっくりする。
関数の処理内容はブロック単位に分けることができる
この事前知識を知っておくと、アセンブラもかなり読みやすくなるよ。
- 関数の先頭を見る。ここにaddとかsubといった命令があればそれはたぶん加減算処理で、おそらくスタックの獲得を行っている
- 関数の先頭と終端の命令で同じ定数値を扱っていれば、それがおそらくスタックサイズ
- スタック獲得の加減算処理で操作しているレジスタが、おそらくスタックポインタ
- スタック獲得の直後にスタックへの書き込み処理があれば、それはおそらく自動変数の初期化処理
- その後に、関数の本来の処理が始まる。もしも未初期化なのに値を参照しているレジスタがあれば、おそらくそのレジスタで引数が渡ってきている
- 関数の末尾にはリターン命令があり、その直前(もしくは遅延スロット)にスタックポインタの加減算があれば、それはおそらくスタックの解放処理
- スタックの解放処理の直前には戻り値の準備があるはず。そのあたりで値を書き込んでいるレジスタが、おそらく戻り値を返すレジスタと思われる
膨大な知識量や特殊技術や膨大な経験ではなく、こういうコツでもって読むと分かりやすいかもしれない。
MIPSのスタックポインタの実体は何か
sp=$29
なお、$の意味はCPUによって違うので、i386だと即値なのがMIPSだとレジスタだったりするので、統一文法ではないことに注意する。
29番レジスタをスタック専用のレジスタとする。
ABIはバイナリの規約
Application Binary Interface
バイナリのインターフェイス
でいうと、MIPSは0番レジスタと31番レジスタのみが特殊。
レジスタの使い方がコンパイラによって違うと、モジュールをリンクしてもうまく動作しない場合があるので、人間側で決めた決まりに則ってアセンブラのやり方を決めている。
ROBOT魂 <SIDE HM> アモン・デュール“スタック” | 魂ウェブ
追記
mips-elf-gcc -nostdlib(標準ライブラリlibcを使わない) -O -g a.c
- nop
- CPU レジスタ
- メモリ
CPUとレジスタは近いけど、メモリの読み込みには時間がかかる(物理的に回路の距離も長い レジスタはフリップフロップで、メモリはSRAMなので、ちょっと遅い)
メモリから読むのに時間がかかるので、nopで時間稼ぎをしている(ロード遅延スロット)
nopがないとメモリ読み込みが間に合わなくて予期しない挙動になる場合がある。
使いドコロが難しいので、新しいMIPSでは時間稼ぎのnop(ロード遅延スロット)をやらないようにしている。
*1:unused