前回のおさらい
P257
hotasm.connpass.com
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の関数呼び出し
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)
shigemk2.hatenablog.com
- フレームポインタはなくてもいいけどなかったらデバッグしにくい。
- デバッグよりもコードの読みやすさを優先して、本書ではフレームポインタを使わないようにしている。*1
SHの関数呼び出し
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を呼び出すタイミングで、スタックの退避と復旧を行われている
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であっても、引数が多数になった場合にはスタックによって渡されるようになったりもします
引数の個数の検知をする術はアセンブラ側にはない。
関数のプロトタイプと実装が違うとバグる
条件分岐
int condition(int a, int b)
{
if (a == b)
b = 1;
return b + 1;
}
- 今まで出てきたジャンプ命令は「必ず」「無条件に」ジャンプするので「無条件分岐」と呼ばれる
- これからやるのは条件分岐。
条件分岐の定義
特定の条件が満たされる場合にはジャンプするが、そうでない場合にはジャンプせず、次の命令を実行する
条件分岐のアセンブラフローチャート
以下のような感じのアセンブラになる
- 比較命令によりif文の条件の成立/不成立を調べ結果に応じてなんらかのフラグ(フラグレジスタ)が立てる
- フラグが立っている場合には arg=2の次の行にジャンプする
- フラグが立っていない場合にはそのまま処理が進み、arg=2が実行される.
CPUによって差が出るところでもある。
H8の条件分岐
わかりやすそうだからH8を先に紹介してみた。
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ビットレジスタ
符号なしがキャリーで、符号有りがオーバーフロー
d.hatena.ne.jp
- 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;
}
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命令)
ループの先頭でも条件分岐を行っている(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命令でそのビットを見て条件分岐している
ブランチとジャンプ