前回のおさらい
P257
MIPSの関数呼び出し
int call_complex1() { return return_arg1(0xfe) + 1; }
- jal命令を呼ぶ時点でraを(暗黙で)書き換える
- その前にプロローグ、エピローグで値を退避、復帰させる
- スタックフレーム
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
PowerPCの関数呼び出し
- blで暗黙に値を書き換える
int call_complex1() { return return_arg1(0xfe) + 1; }
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
ARMの関数呼び出し
ARMはややこしい
- IP is 何
- fp ip lr pcの順に上から積み上げられる
- フレームポインタはデバッグ用のレジスタ
- 一命令で複数のことをやっている
- lrをpcに入れて戻り値とし、ipをspに入れてスタックポインタを復旧させる
- ARMの仕様として、pcは2つ先を指すという仕様を逆利用して、命令数を節約している
00fe1564 <call_complex1>: fe1564: e52de004 push {lr} ; (str lr, [sp, #-4]!) fe1568: e3a000fe mov r0, #254 ; 0xfe fe156c: ebffffbb bl fe1460 <return_arg1> fe1570: e2800001 add r0, r0, #1 fe1574: e49df004 pop {pc} ; (ldr pc, [sp], #4)
- フレームポインタはなくてもいいけどなかったらデバッグしにくい。
- デバッグよりもコードの読みやすさを優先して、本書ではフレームポインタを使わないようにしている。*1
SHの関数呼び出し
- prは戻り先のレジスタ
- SHは2バイト固定長
- wがword lがlong
nopはアラインメント(4バイトの値は4バイトで揃えないといけない)を揃えるためのパディング
r15をprのぶんだけ引き算する
@r15にprを代入する
r4に引数 r0に関数を入れる
- 実際の処理はrtsで終わりで、あとは固定長の都合であとで参照する命令群
00fe1508 <_call_complex1>: fe1508: 4f 22 sts.l pr,@-r15 ! 退避 fe150a: 94 06 mov.w fe151a <_call_complex1+0x12>,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: 70 01 add #1,r0 fe1514: 4f 26 lds.l @r15+,pr ! 復旧 fe1516: 00 0b rts ! return fe1518: 00 09 nop fe151a: 00 fe mov.l @(r0,r15),r0 fe151c: 00 fe mov.l @(r0,r15),r0 fe151e: 14 40 mov.l r4,@(0,r4)
(jsr @r0について、デリファレンスしているわけでもないのに@がついていることもあるので注意。これは単純にr0にジャンプしている)
- SHも遅延スロット。
- 先にポインタをずらして、その後で値を入れるのがデファクトスタンダード。ので、@-r15は普通。
- @r15+はポストインクリメントで、先に取り出して、後でずらす。
アドレス指定方法
- 相対アドレスを利用しているような気がする。
PowerPC
fe1590: 4b ff fe cd bl fe145c <return_arg1>
- 0xfe145c - 0xfe1590 = 0xfffffecc
ARM
fe1588: ebffffb5 bl fe1464 <return_arg1>
計算方法はこんな感じ。
- (Oxfe1464- (Oxfe1588 + 8)) ÷4= Oxffffffb5
ここから
CISC系CPUの関数呼び出し
RISC
- レジスタベース
- パイプラインの効率的な動作
- プロセッサをスタックを自動的に利用
H8
- 0xfe1444が関数のアドレス、と思われる
- r0が第一引数、と思われる
- adds命令で、r0+1を行っている
- rtsで元に戻る
00fe14ea <_call_complex1>: fe14ea: 79 00 00 fe mov.w #0xfe,r0 fe14ee: 5e fe 14 44 jsr @0xfe1444:24 ! 定数が24ビットという意味では fe14f2: 0b 00 adds #1,r0 fe14f4: 54 70 rts
00fe1444 <_return_arg1>: fe1444: 54 70 rts
スタックの格納と復旧はどこでやるの?
→jsrを呼び出すタイミングで、スタックの退避と復旧を行われている
- :24とか:16ってなに ディスプレースメントが機械語として16だったり24だったり(即値のビットサイズが何ビットかっていう話)
- ここだと24ビット
- 戻り先アドレスはスタックに保存される
- レジスタに保存するか、最初からスタックに保存するか、のいずれか
H8だと単にjsrを実行するだけでいい
SHもH8も日立系のCPUなので、書き方の思想は似ている。
i386
- 引数もスタック経由で渡される
- i386のpcはeipという名前になっている
- i386はスタックベース
00fe150d <call_complex1>: fe150d: 68 fe 00 00 00 push $0xfe fe1512: e8 26 ff ff ff call fe143d <return_arg1> fe1517: 83 c4 04 add $0x4,%esp fe151a: 40 inc %eax ! eaxが戻り値 戻り値を加算 fe151b: c3 ret
まとめ
- 関数呼び出し時の戻り先アドレスの格納先は、レジスタであったりスタックであったりします
- 関数呼び出し命令は、戻り先アドレスの格納とジャンプを同時に行なう命令で実現されています
- たとえレジスタ渡しのCPUであっても、引数が多数になった場合にはスタックによって渡されるようになったりもします
引数の個数の検知をする術はアセンブラ側にはない。
関数のプロトタイプと実装が違うとバグる
条件分岐
基礎の最後。
構造化プログラミングでは、プログラムは最終的に「処理の連続」「分岐」「繰り返し」の 3つの要素に分解することができます
- でも繰り返しは多くの場合条件分岐で実現しているので、条件分岐がわかればいい。
int condition(int a, int b) { if (a == b) b = 1; return b + 1; }
- 今まで出てきたジャンプ命令は「必ず」「無条件に」ジャンプするので「無条件分岐」と呼ばれる
- これからやるのは条件分岐。
条件分岐の定義
特定の条件が満たされる場合にはジャンプするが、そうでない場合にはジャンプせず、次の命令を実行する
条件分岐のアセンブラフローチャート
以下のような感じのアセンブラになる
- 比較命令によりif文の条件の成立/不成立を調べ結果に応じてなんらかのフラグ(フラグレジスタ)が立てる
- フラグが立っている場合には arg=2の次の行にジャンプする
- フラグが立っていない場合にはそのまま処理が進み、arg=2が実行される.
CPUによって差が出るところでもある。
H8の条件分岐
わかりやすそうだからH8を先に紹介してみた。
- cmpが比較命令っぽい
- 分岐命令は頭にbがつく
00fe150e <_condition>: fe150e: 1d 10 cmp.w r1,r0 fe1510: 46 04 bne .+4 (0xfe1516) ! r0とr1の比較結果が等しくないならば、fe1516にジャンプする。そうでなければこのまま続ける。 fe1512: 79 01 00 01 mov.w #0x1,r1 00fe1516 <.L36>: fe1516: 0d 10 mov.w r1,r0 fe1518: 0b 00 adds #1,r0 fe151a: 54 70 rts
bneはcmpの比較結果を見ているわけだけど、どこで保存しているのだろう。
比較結果の保存先
- H8ではCCRというフラグレジスタを持っている。
- 色々な条件に基づいてフラグを立てる
CCRは8ビットレジスタ
符号なしがキャリーで、符号有りがオーバーフロー
- branch not equal
branch not zero
bne命令の場合は、ゼロフラグで判断している。
ループ処理はどうなっているのか
int loop(int n) { int i, sum = 0; for (i = 0; i < n; i++) sum += i; return sum; }
- r3が変数n
- r2が変数i
- r0が変数sum
00fe151c <_loop>: fe151c: 0d 03 mov.w r0,r3 fe151e: 19 00 sub.w r0,r0 fe1520: 19 22 sub.w r2,r2 fe1522: 1d 30 cmp.w r3,r0 fe1524: 4c 08 bge .+8 (0xfe152e) 00fe1526 <.L41>: fe1526: 09 20 add.w r2,r0 fe1528: 0b 02 adds #1,r2 fe152a: 1d 32 cmp.w r3,r2 fe152c: 4d f8 blt .-8 (0xfe1526) 00fe152e <.L43>: fe152e: 54 70 rts
ループ継続の判断も、条件分岐によって行われている(blt命令)
CCRが持つフラグの状態に応じてループ処理を継続している
blt r2 < r3 i < n
bge r3 >= r0 n >= sum
比較処理の後にゼロフラグだけでなくネガティプフラグなどの他のフラグの状態も見る
- bltの結果如何でgotoする
ループの先頭でも条件分岐を行っている(bge命令)
- 直前でr0 -= r0 r2 -= r2をやっているので、実際にやっているのはゼロとの比較 そしてそれをコンパイラは知っている。
- ループは一回も回らない場合があるので、そこらへんをチェックしている。
PowerPCの条件分岐
- r3が第一引数、r4が第二引数
- cmpwで比較した後に、bne+命令でフラグをもとにジャンプしたりしなかったりする
00fe1608 <condition>: fe1608: 7f 83 20 00 cmpw cr7,r3,r4 ! if(a==b) fe160c: 40 be 00 08 bne+ cr7,fe1614 <condition+0xc> ! if(a==b) fe1610: 38 80 00 01 li r4,1 ! b = 1 fe1614: 38 64 00 01 addi r3,r4,1 ! return b + 1 fe1618: 4e 80 00 20 blr ! return
- cmpw命令で第 1引数と第 2引数を比較する
- 比較した結果,一致しなければ 209行目の「return b + 1」の処理にジャンプすることでb=1の処理をスキップする
- 一致するならばそのまま 208行目のli r4,1の処理が実行され,b=1が行われる.
比較結果はCRレジスタに格納される
- (ここでいうCRはConditionRegisterの略)
- PowerPCはcr7に格納される。
- comparewordの略がcmpw
ループがない!!
i386の条件分岐
00fe153e <condition>: fe153e: 8b 44 24 08 mov 0x8(%esp),%eax fe1542: 39 44 24 04 cmp %eax,0x4(%esp) ! スタックポインタ+4との比較 fe1546: 75 05 jne fe154d <condition+0xf> ! フラグがたったらfe154dに飛ぶ fe1548: b8 01 00 00 00 mov $0x1,%eax ! b = 1 fe154d: 40 inc %eax ! b + 1 fe154e: c3 ret ! return
- i386ではbne(branch not equal)ではなくjne(jump not equal)
- フラグレジスタを持って、cmp命令の結果としてフラグレジスタのフラグが書きかわり、jne命令でそのビットを見て条件分岐している
ブランチとジャンプ
- 飛び先の指定が相対アドレスの場合には「分岐」、絶対アドレスの場合は「ジャンプ」
- っていうなんとなくの経験則はあるけども、ジャンプと分岐に対して違いはないように思われる
なお、switchはコンパイラがテーブルを自動生成したりしていてとてもむずかしい。
ccr(h8)
- cr(powerpc)
- eflags(i386)
彼女がフラグをおられたら 大丈夫、この体育祭は安全だから、絶対MVPを取れるわよ (講談社ラノベ文庫)
- 作者: 竹井 10日,CUTEG
- 出版社/メーカー: 講談社
- 発売日: 2015/04/02
- メディア: 文庫
- この商品を含むブログを見る
*1:サンプルのMakefileにも-fomit-frame-pointerオプションが使われている