おさらい
10.1.4 raレジスタはハードウェア的に特別なものになっている
raレジスタの実体
プロローグ
- jal命令
エピローグ
jal命令はr0を壊すので、別のところへ退避している
レジスタへのアクセスはメモリへのアクセスに比べて遅い
- 読み込みを待たない
- 読み込みが終わっていない可能性があるのはよくないので、nopで時間稼ぎをしている(ので、nopじゃなくても別の命令で時間稼ぎをすることは可能)
- lw ra,16(sp)→ addiu v0, v0 1で関係のない命令を並べ替えて時間を稼ぐ。こういうテクニックをコンパイラがやってくれることがある
- 設計上の都合でこういうことをしてる
- 古いMIPSだとnopで時間稼ぎをしている。
00fe1598 <call_complex1>: fe1598: 27bdffe8 addiu sp,sp,-24 fe159c: afbf0014 sw ra,20(sp) fe15a0: 0c3f8518 jal fe1460 <return_arg1> fe15a4: 240400fe li a0,254 fe15a8: 24420001 addiu v0,v0,1 fe15ac: 8fbf0014 lw ra,20(sp) fe15b0: 00000000 nop fe15b4: 03e00008 jr ra fe15b8: 27bd0018 addiu sp,sp,24
10.2 PowerPC
- ABI的にr3が第一引数であり戻り値でもある
- stwuはアトミックな命令
mr r0, r1 addi r1,r1,-16 stw r0, (r1)
- mflrはリンクレジスタの値をコピーする命令(リンクレジスタはMIPSでいうところのreturn address)
- blで関数を呼び出すけど値が壊されるので退避と復帰が行われている
- リンクレジスタは特別なので、専用の命令が用意されている
- MIPSは特殊レジスタと汎用レジスタで特殊な線引きをやっている
00fe1580 <call_complex1>: fe1580: 94 21 ff f0 stwu r1,-16(r1) fe1584: 7c 08 02 a6 mflr r0 fe1588: 90 01 00 14 stw r0,20(r1) fe158c: 38 60 00 fe li r3,254 fe1590: 4b ff fe cd bl fe145c <return_arg1> fe1594: 38 63 00 01 addi r3,r3,1 fe1598: 80 01 00 14 lwz r0,20(r1) fe159c: 7c 08 03 a6 mtlr r0 fe15a0: 38 21 00 10 addi r1,r1,16 fe15a4: 4e 80 00 20 blr
10.3 ARMの関数呼び出し
- なんかごちゃごちゃレジスタが登場している
- スタックは積み木で、下から積み上げていって(push)、上から取りに行く(pop)
- ビットマップというものが用意されている
00fe1578 <call_complex1>: fe1578: e1a0c00d mov ip, sp ! ipとspが同じ所を指す fe157c: e92dd800 push {fp, ip, lr, pc} ! pc lr ip fpの順にスタックに積まれる fe1580: e24cb004 sub fp, ip, #4 ! 減算 fe1584: e3a000fe mov r0, #254 ; 0xfe ! r0に0xfeを入れる fe1588: ebffffb5 bl fe1464 <return_arg1> ! return_arg1 関数呼び出し fe158c: e2800001 add r0, r0, #1 fe1590: e89da800 ldm sp, {fp, sp, pc} ! fp sp pcの値を復帰させている(ipは復帰させていない)
- ARMはすべて汎用レジスタにする
- IPはテンポラリレジスタ?のようなもので、レジスタの値の回避にしか使わないのでは
fpの扱いはABIで決まっているような感じ?(fpは後述で、スタックフレームの区切りを指している)
ARMはつめこんだような感じになっていて、初見では難しい構造になってしまっている
ポインタ経由での関数呼び出し
- ポインタで関数を呼び出す
- 単純にint *fだと戻り値がint型のポインタになってしまう
void call_pointer(int (*f)(void)) { f(); }
- プログラムカウンタを直接書き換えることでjal命令の代わりとしている
- ARMのプログラムカウンタは常に2つ先の命令を示している
- このアセンブラは結構トリッキー
- mov命令でジャンプしているので、もう狂っているとしか…
- プログラムカウンタはmovで弄るのは気持ち悪い
00fe15c0 <call_pointer>: fe15c0: e1a0c00d mov ip, sp ! プロローグ fe15c4: e92dd800 push {fp, ip, lr, pc} fe15c8: e24cb004 sub fp, ip, #4 fe15cc: e1a0e00f mov lr, pc ! 実際の処理 fe15d0: e1a0f000 mov pc, r0 fe15d4: e89da800 ldm sp, {fp, sp, pc} ! エピローグ
ここから
10.3.5 ARM の命令セットは、あまりRISCっぽくないように思える
P274
- ARMの即値はあまりビット数を取れないのでPC相対を利用している
- RISCではメモリアクセスを極力しないという思想
- メモリへのアクセスはキャッシュミスによりパイプラインの動作を止めてしまう可能性
- 定数値を扱うためにメモリアクセスが発生→ARMは他のRISC系と比べてあまりRISCぽくない
- RISC系は命令が固定長なので、命令セットのビット数の割り振りが重要になる
- ARMは条件コードで4ビット使うので、自由に使えるビット数は32ビット
- ポインタ経由での関数呼び出しリターン命令を mov命令で実現することで、命令数を節約
10.3.6 フレームポインタ(fp)とはなにか
フレームポインタはデバッガがスタックの「バックトレース」を追う場合に利用される。「バックトレース」というのはスタックフレームの構造を遡って追うことで、関数呼び出しがどのような順序で行われているのかを解析することになる。
A→B→Cの順で関数用にスタックが積まれていたとして、デバッガがCで止めた場合、BやAのスタックフレームの位置をどこで特定するのだろう
- それを特定するためのレジスタがフレームポインタ
- CPUによっては必ず先頭を指してはいない
- 専門家向けではない本
- フレームポインタは基本としてデバッガがパックトレースを追うためにあるもの
- PowerPCではフレームポインタが要らない構造になっているので、特段必要ではないけど、フレームポインタを使わないオプションをつけることが出来る(man gccも参照のこと)
framepointerつきでコンパイルしたやつ
int return_zero() { return 0; }
$ mips-elf-gcc -nostdlib -g -O -fno-omit-frame-pointer return_zero_mips.c -o test.out
00400018 <return_zero>: 400018: 27bdfff8 addiu sp,sp,-8 40001c: afbe0004 sw s8,4(sp) 400020: 03a0f021 move s8,sp 400024: 00001021 move v0,zero 400028: 03c0e821 move sp,s8 40002c: 8fbe0004 lw s8,4(sp) 400030: 03e00008 jr ra 400034: 27bd0008 addiu sp,sp,8
- もっとも return_zero()は他の関数を呼び出さない、いわゆる「リーフ関数」です。このため関数呼び出しによってフレームポインタの値が上書きされることは無く、実はフレームポインタの退避・復旧処理を行なわなくてもデバッガで追跡可能だったりします
10.4 SHの関数呼び出し
10.4.1 SHは「プリデクリメントレジスタ間接」というアドレッシングモードをもつ
- あぁ^〜 難しい。sts.lでスタックの獲得と値の保存をまとめて行っているイメージ。
「@-r15」と書くとr15レジスタをデクリメントしてからという意味が追加される
--i プリデクリメント → SH的にはフル下降
- i-- ポストデクリメント
00fe1508 <_call_complex1>: fe1508: 4f 22 sts.l pr,@-r15 fe150a: 94 05 mov.w fe1518 <_call_complex1+0x10>,r4 ! fe fe150c: d0 03 mov.l fe151c <_call_complex1+0x14>,r0 ! fe1440 <_return_arg1> fe150e: 40 0b jsr @r0 fe1510: 00 09 nop fe1512: 4f 26 lds.l @r15+,pr fe1514: 00 0b rts fe1516: 70 01 add #1,r0 fe1518: 00 fe mov.l @(r0,r15),r0 fe151a: 00 09 nop fe151c: 00 fe mov.l @(r0,r15),r0 fe151e: 14 40 mov.l r4,@(0,r4)
10.4.2 SH ではポインタ経由で関数呼び出しが行われる
- SHは即値を入れるところがものすごく少ない
- fe150aで return_arg1()のアドレスを rOレジスタに代入する
- fe150cで rOレジスタの指す先を関数として呼び出す
- jsrはちょっと謎
- movで別のアドレスの値(即値)を読み込んで、そこにジャンプする
10.4.3 関数呼び出しのアドレス指定方法
ここで突然のPowerPC
- なんらかのフラグ領域(つまりオペコードの一部)にして,実際のアドレス計算の際にはフラグ領域は無視してゼロとして計算するという思想、というか設計(作った人のこだわり)
- この本は機械語にこだわっていて、バイナリベースの本が多かったりする。
- PowerPCではジャンプ先のアドレスはその命令のアドレスを基点に計算する、という設計
- 差分計算
10.4.5 SHは多ビットのオペランドを取れない
- PowerPCや ARMでは相対アドレス指定でジャンプ先を指定
- SHは2バイト命令長のためオペランドとして指定できる即値のビット幅をそれほど大きくはとれませんから,相対アドレスで指定できる範囲が非常に狭くなってしまいます
- レジスタ経由で関数を呼び出す
P284
- 作者: 坂井弘亮
- 出版社/メーカー: 秀和システム
- 発売日: 2014/09/30
- メディア: 単行本
- この商品を含むブログを見る