by shigemk2

当面は技術的なことしか書かない

熱血!アセンブラ入門 読書会(14) 休日編 #hotasm

前回のおさらい

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の関数呼び出し

  • 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)

shigemk2.hatenablog.com

  • フレームポインタはなくてもいいけどなかったらデバッグしにくい。
  • デバッグよりもコードの読みやすさを優先して、本書ではフレームポインタを使わないようにしている。*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;
}
  • 今まで出てきたジャンプ命令は「必ず」「無条件に」ジャンプするので「無条件分岐」と呼ばれる
  • これからやるのは条件分岐。

条件分岐の定義

特定の条件が満たされる場合にはジャンプするが、そうでない場合にはジャンプせず、次の命令を実行する

条件分岐のアセンブラフローチャート

以下のような感じのアセンブラになる

  1. 比較命令によりif文の条件の成立/不成立を調べ結果に応じてなんらかのフラグ(フラグレジスタ)が立てる
  2. フラグが立っている場合には arg=2の次の行にジャンプする
  3. フラグが立っていない場合にはそのまま処理が進み、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ビットレジスタ

  • 符号なしがキャリーで、符号有りがオーバーフロー

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;
}
  • 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 
  1. cmpw命令で第 1引数と第 2引数を比較する
  2. 比較した結果,一致しなければ 209行目の「return b + 1」の処理にジャンプすることでb=1の処理をスキップする
  3. 一致するならばそのまま 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)

*1:サンプルのMakefileにも-fomit-frame-pointerオプションが使われている