おさらい
スタック
メモリの中にスタックと呼ばれるものがあって、それをどうやって使いまわすか
void foo() {
int a = 1, b = 2;
}
- 変数はメモリの中に
- ローカル変数はスタックと呼ばれるメモリの中に存在している
void foo(int a) {
if(a > 1) foo(a - 1);
}
関数を呼び出すごとにスタック領域が確保され、呼び出しが終わったら領域が消される。関数の処理が終わると解放される。
右の例でいうと、間を飛ばしてfunc2()が先に終わることはない。メモリの領域は下から順番に積み上がっているから。関数が終わったら上から順番に消されていく。
スタックポインタがメモリ領域の敷居になっている。
コルーチンみたいに呼んだ順番に終わらない場合もあるがそれは割愛する。
#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;
}
-nostdlib c言語だと当たり前に使える標準ライブラリがあるけど、この環境では特にOS依存ではなくコンパイラだけ使いたいので、標準ライブラリは一切使わない。(OS依存のやつは想定していない)
どこでスタートしたらいいかわからないから適当にアドレスを指定しましたよ警告
➜ ~ mips-elf-gcc -nostdlib v.c
/home/shigemk2/bin/cross/bin/../lib/gcc/mips-elf/4.6.4/../../../../mips-elf/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000400018
gist.github.com
$ mips-elf-gcc -nostdlib -O v.c
gist.github.com
スタックの場所が特定されるとクラッキングされやすくなるので、スタックの場所は毎回違っている。
準備のaddiuがプロローグ、片付けのaddiuがエピローグと呼ばれる。
- -O 最適化
-g デバッグ情報
-S 情報をくっつける
$ mips-elf-gcc -nostdlib -O -g -Wall v.c
$ all-objdump -S a.out
- sw=store wordでwordは4バイト
- メモリの読み書きは専用の命令を使わないといけない
- メモリの値をレジスタに書き込むか、レジスタの値を読み込むか。いずれにしても、メモリの読み書きはレジスタを介さないといけない。
- 関数呼び出しのときにスタックの領域をずらす。ただずらすだけなので、メモリに書き込んだ値はまだ残っている。なので、C言語ではローカル変数を初期化しないと中身がぐちゃぐちゃになる。
gist.github.com
➜ ~ mips-elf-gcc -nostdlib -O -g -Wall v.c
v.c: In function ‘set_stack’:
v.c:4:18: warning: unused variable ‘b’ [-Wunused-variable]
v.c:3:18: warning: unused variable ‘a’ [-Wunused-variable]
/home/shigemk2/bin/cross/bin/../lib/gcc/mips-elf/4.6.4/../../../../mips-elf/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000400018
なお、マクロで使わない変数を宣言することは出来る。
gist.github.com
(なお、Javaではアノテーションで関数に属性を付与することが出来る)
Javaアノテーション機能 概要 - Java入門
警告を出して(-Wallをつけて)コンパイルしたほうがよい。
アトリビュートでマクロを使いたいために括弧は二重だったりする。アセンブラを勉強したい場合はOSも勉強しないともったいない感じが。
int use_stack()
{
volatile int a = 0xfe;
volatile int b = 0xff;
return a + b;
}
ココのnopは次の処理のための時間稼ぎだけど、最近のmipsは性能がいいので、別にnopがなくてもよかったりする。volataileをつけると読み書きは都度やる。
00fe152c <use_stack>:
fe152c: 27bdfff8 addiu sp,sp,-8
fe1530: 240200fe li v0,254
fe1534: afa20000 sw v0,0(sp)
fe1538: 240200ff li v0,255
fe153c: afa20004 sw v0,4(sp)
fe1540: 8fa30000 lw v1,0(sp)
fe1544: 8fa20004 lw v0,4(sp)
fe1548: 00000000 nop
fe154c: 00621021 addu v0,v1,v0
fe1550: 03e00008 jr ra
fe1554: 27bd0008 addiu sp,sp,8
なお、volatileを外すとこんな感じになる。
gist.github.com
以上、スタックとローカル変数の関係について。
PowerPC
下方伸張かどうかはあんまり気にしないのがこの本のポリシー。
下がゼロなのが下方伸張。世の中のCPUのだいたいは下方伸張。
スタックポインタを扱っているレジスタはr1で、MIPSはspだった。
00fe14fc <set_stack>:
fe14fc: 94 21 ff e0 stwu r1,-32(r1)
fe1500: 38 00 00 fe li r0,254
fe1504: 90 01 00 08 stw r0,8(r1)
fe1508: 38 00 00 ff li r0,255
fe150c: 90 01 00 0c stw r0,12(r1)
fe1510: 38 21 00 20 addi r1,r1,32
fe1514: 4e 80 00 20 blr
MIPSはキッチリメモリを確保しているけど、powerpcは適当。
あと、stwu命令を使ってスタック領域を確保している。
スタックフレームの直後に、スタックポインタの値が格納されているので、スタックフレームの切れ端が分かる。
で、PowerPCのスタックフレームは16バイト単位で決まっている(そういうABI)
なので処理が追いやすい。メモリの参照先も分かりやすい。キャッシュに乗りやすい。
複雑な命令も使いやすくなったりしている。
SIMD - Wikipedia
スタック操作はアトミックに行う必要がある
- stwu命令はスタックの減算と値の格納をいっぺんにやっている
- でも2命令になっちゃうと、命令の割り込みが発生したら中途半端な感じになってしまう
- このように1命令で行う必要のある処理をアトミックという
- 命令を分割することは出来ないようにしないといけない
ARM SH H8 i386 のスタック操作もみてみよう
ARM
やっていることはMIPSといっしょっぽい
subとadd
00fe1508 <set_stack>:
fe1508: e24dd008 sub sp, sp, #8
fe150c: e3a030fe mov r3, #254 ; 0xfe
fe1510: e58d3004 str r3, [sp, #4]
fe1514: e3a030ff mov r3, #255 ; 0xff
fe1518: e58d3000 str r3, [sp]
fe151c: e28dd008 add sp, sp, #8
fe1520: e12fff1e bx lr
SH
- r15のやりとりでスタック領域をごにょごにょしている
- 遅延スロット
- 減算なので下方伸張
- どんなCPUでもプロローグとエピローグは存在している
00fe14bc <_set_stack>:
fe14bc: 7f f8 add #-8,r15
fe14be: 91 04 mov.w fe14ca <_set_stack+0xe>,r1 ! fe
fe14c0: 1f 11 mov.l r1,@(4,r15)
fe14c2: 71 01 add #1,r1 ! ff
fe14c4: 2f 12 mov.l r1,@r15
fe14c6: 00 0b rts
fe14c8: 7f 08 add #8,r15
fe14ca: 00 fe mov.l @(r0,r15),r0
最後の行は、大きな即値を扱うことが出来ないので、別のアドレスに値を格納している。
このあたりからなんかだんだん本領発揮ってところ。
H8
大きな即値が扱えないためか、2回に分けてスタック領域の演算をやっている。そこらへんのアレがあんまり良く分からなかったりしている。
00fe14aa <_set_stack>:
fe14aa: 1b 87 subs #2,r7
fe14ac: 1b 87 subs #2,r7
fe14ae: 79 02 00 fe mov.w #0xfe,r2
fe14b2: 6f f2 00 02 mov.w r2,@(0x2:16,r7)
fe14b6: 0b 02 adds #1,r2
fe14b8: 69 f2 mov.w r2,@r7
fe14ba: 0b 87 adds #2,r7
fe14bc: 0b 87 adds #2,r7
fe14be: 54 70 rts
i386
RISCの場合はレジスタを介さないといけないけど、i386はそんなことはない。
subとadd
00fe14c5 <set_stack>:
fe14c5: 83 ec 08 sub $0x8,%esp
fe14c8: c7 44 24 04 fe 00 00 movl $0xfe,0x4(%esp)
fe14cf: 00
fe14d0: c7 04 24 ff 00 00 00 movl $0xff,(%esp)
fe14d7: 83 c4 08 add $0x8,%esp
fe14da: c3 ret
スタックの一般的な共通事項
- スタック獲得は、スタックポインタを減算している(メモリ領域をヒープとスタックに効率的に利用できる)
スタックポインタの指す先には、値が格納されている
プログラム
- データ
- ヒープ(mallocするのでどんどん伸びていく)
- スタック(ヒープとスタックで開いているところを食い合っているので、これがぶつかるとout of memory)
これが上方伸張だとわけがわからなくなる。
- フルスタック(使用領域の終端)
エンプティスタック(空き領域の先端)
プリデクリメント(先に減算)
- ポストインクリメント(あとで減算)
あと完全降順とかあるけどこれはそんなに覚えなくていいと思う。
まとめ
いろいろなアセンブラを読むと手数が増える。引き出しが増える。気楽な感じでアセンブラを見ていくと、いいと思う。
10. 関数呼び出し
こっからが本番
関数を呼び出せないとプログラムとして意味がないので…
関数呼び出し←スタックの理解←メモリの読み書きの理解
なんかMIPSが先になってきているけど、それはたぶん読みやすいからなのでは。
MIPS
このくらい複雑なほうが、アセンブラを見た時にいろいろ見えてくる。
int call_complex1()
{
return return_arg1(0xfe) + 1;
}
- 実際見てみると、jal命令でreturn_arg1関数を呼び出していると思われる。
- jal命令前後になんかやっているんだろうなっていう予想はつく。
- MIPSは遅延スロットなので、関数を呼ぶ前にli命令でレジスタa0に値を格納している
- レジスタv0が関数の戻り値
- 気になるのはswとlwって何?→レジスタraが壊れる前にスタック上に一時的に保存している
- なお、raは関数からの戻り先のアドレスを保持するレジスタ(return addressの意)
- 関数を呼ぶたびにraレジスタの中身が破壊されてしまうので、swとlwでraを一時的に退避している
00fe1590 <call_complex1>:
! プロローグ
fe1590: 27bdffe8 addiu sp,sp,-24
fe1594: afbf0010 sw ra,16(sp)
! 本文
fe1598: 0c3f8518 jal fe1460 <return_arg1>
fe159c: 240400fe li a0,254
fe15a0: 24420001 addiu v0,v0,1
! エピローグ
fe15a4: 8fbf0010 lw ra,16(sp)
fe15a8: 00000000 nop ! メモリへのアクセスは遅いのでnopで時間稼ぎをしている
fe15ac: 03e00008 jr ra
fe15b0: 27bd0018 addiu sp,sp,24
今まではraの保存をするまでもない関数だった(=別の関数を呼ばない)。これを末端の関数もしくはリーフ関数と呼ばれる。
raレジスタはハードウェア的に特別なものとなっている
ゼロレジスタと同じく、raレジスタも実体は$31で、ABIで決められたもの以上にハードウェアで役割の決まっているレジスタである
P261まで