by shigemk2

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

熱血!アセンブラ入門 読書会(15) #hotasm

hotasm.connpass.com

おさらい

CISC系のCPUの関数の呼び出し

  • jsrでスタックに積み、addで解放する

  • i386では引数もスタック経由で渡される

  • jsrが自動的に戻り値をスタックに積む
  • 暗黙でスタックが積まれている
  • スタックで出たり入ったりしているわけだけど

条件分岐

int condition(int a, int b)
{
  if (a == b)
    b = 1;
  return b + 1;
}
  • condition(5, 5)→return 1 + 1
  • condition(1, 5)→return 5 + 1

これをh8でやってみる

bneはbranch not equalで、条件に等しくなければ次へ進めなさい

00fe150e <_condition>:
  fe150e:   1d 10           cmp.w   r1,r0
  fe1510:   46 04           bne .+4 (0xfe1516) ! 条件が満たされなければPCを4進めなさい
  fe1512:   79 01 00 01     mov.w   #0x1,r ! b = 1

00fe1516 <.L36>:
  fe1516:   0d 10           mov.w   r1,r0
  fe1518:   0b 00           adds    #1,r0
  fe151a:   54 70           rts 

for文

int loop(int n)
{
  int i, sum = 0;
  for (i = 0; i < n; i++)
    sum += i;
  return sum;
}
  • cmp命令は本質的に引き算で、sub命令と並びは一緒。だけどsub命令と違ってdestinationが破壊されない。
  • cmp = r0 >= r3
00fe151c <_loop>:
  fe151c:   0d 03           mov.w   r0,r3 ! r3 = n
  fe151e:   19 00           sub.w   r0,r0 ! r0 = 0
  fe1520:   19 22           sub.w   r2,r2 ! r2 = 0
  fe1522:   1d 30           cmp.w   r3,r0 ! r0 >= r3 ! ループに入るかどうかを確認。n<=0の場合はループに入らなくて、コンパイラはそれを認識している。
  fe1524:   4c 08           bge .+8 (0xfe152e)

00fe1526 <.L41>:
  fe1526:   09 20           add.w   r2,r0 ! r0 += r2 r2がiでr0がsum
  fe1528:   0b 02           adds    #1,r2 ! 即値代入のi++
  fe152a:   1d 32           cmp.w   r3,r2 ! i <= n
  fe152c:   4d f8           blt .-8 (0xfe1526) ! 条件が満たされていればもとに戻る

00fe152e <.L43>:
  fe152e:   54 70           rts 

PowerPCの条件分岐

スーパーコンピュータ向けのPowerのサブセットがPowerPC。

PowerPC - Wikipedia

  • 分岐予測用のヒントがある
  • フラグを入れるスロットがある
  • 指定の場所に飛ばす(PC+4みたいなやつじゃない)
00fe1608 <condition>:
  fe1608:   7f 83 20 00     cmpw    cr7,r3,r4 ! 比較結果がcr7というレジスタの格納される
  fe160c:   40 be 00 08     bne+    cr7,fe1614 <condition+0xc> ! 分岐予測(命令をパイプラインで読んでいるときに、分岐を予測しておいてパイプラインが無駄にならないようにする)
  fe1610:   38 80 00 01     li      r4,1
  fe1614:   38 64 00 01     addi    r3,r4,1
  fe1618:   4e 80 00 20     blr

今日はここから

i386の条件分岐

  • eax割り当て
00fe153e <condition>:
  fe153e:   8b 44 24 08             mov    0x8(%esp),%eax
  fe1542:   39 44 24 04             cmp    %eax,0x4(%esp)
  fe1546:   75 05                   jne    fe154d <condition+0xf>
  fe1548:   b8 01 00 00 00          mov    $0x1,%eax
  fe154d:   40                      inc    %eax
  fe154e:   c3                      ret    

SHの条件分岐

  • r4が第一引数 r5が第二引数
  • cmp/eqで、比較と==を同時やっている
  • bf=false
  • r0が戻り値なので、r0にbの値(r5)を入れている
  • movしてからaddしてreturnというレ点の遅延スロットが働いている。
00fe154c <_condition>:
  fe154c:   34 50           cmp/eq  r5,r4
  fe154e:   8b 00           bf  fe1552 <_condition+0x6>
  fe1550:   e5 01           mov #1,r5
  fe1552:   60 53           mov r5,r0
  fe1554:   00 0b           rts 
  fe1556:   70 01           add #1,r0

SHのループ

  • r0はsum
  • r1はi
  • オペランドの順番ってh8といっしょですね。
  • greater thanしかなくって、less thanがない。
  • btとbfでtrueとfalseで条件分岐している
  • レジスタ節約のためにフラグを一本に絞っている
00fe1558 <_loop>:
  fe1558:   e0 00           mov #0,r0
  fe155a:   e1 00           mov #0,r1
  fe155c:   30 43           cmp/ge  r4,r0 ! 0 >= n h8と一緒で、オペランドの順番は逆になる
  fe155e:   89 03           bt  fe1568 <_loop+0x10> ! 
  fe1560:   30 1c           add r1,r0
  fe1562:   71 01           add #1,r1
  fe1564:   31 43           cmp/ge  r4,r1 ! i >= n
  fe1566:   8b fb           bf  fe1560 <_loop+0x8> ! i >= nでないなら戻る
  fe1568:   00 0b           rts 
  fe156a:   00 09           nop 

MIPSの条件分岐

  • 遅延スロット。
  • 遅延スロットさえなかったらシンプルなのでは
  • MIPSはフラグレジスタがなく、全部汎用レジスタベースで行っている
  • 比較と分岐を1命令でやっている
00fe1604 <condition>:
  fe1604:   14850002    bne a0,a1,fe1610 <condition+0xc>
  fe1608:   00000000    nop
  fe160c:   24050001    li  a1,1
  fe1610:   03e00008    jr  ra
  fe1614:   24a20001    addiu   v0,a1,1

ARMの条件分岐

  • ARMは条件をmovに付け加えることが出来る
  • 最初の1ニブルをみると、eが無条件 0が条件分岐
  • moveqで、フラグが立っていたらr1=1
  • ジャンプしていない
  • でもループの時は、ジャンプって必要だよね
00fe15d8 <condition>:
  fe15d8:   e1500001    cmp r0, r1
  fe15dc:   03a01001    moveq   r1, #1
  fe15e0:   e2810001    add r0, r1, #1
  fe15e4:   e1a0f00e    mov pc, lr

ARMのループ

  • 代入は左向き
  • r0 = 0
  • r3 = r0 なぜ即値を避けているのか

  • aがge

  • bがlt
  • で、機械語を見るとaやbの意味が分かる。
00fe15e8 <loop>:
  fe15e8:   e1a02000    mov r2, r0 ! r2 = 0
  fe15ec:   e3a00000    mov r0, #0 ! sum = 0
  fe15f0:   e1a03000    mov r3, r0 ! i = 0
  fe15f4:   e1500002    cmp r0, r2
  fe15f8:   a1a0f00e    movge   pc, lr ! r0 >= r2のときはreturnせよ pc = lrだからreturnというとんでもない仕組み
  fe15fc:   e0800003    add r0, r0, r3
  fe1600:   e2833001    add r3, r3, #1
  fe1604:   e1530002    cmp r3, r2 ! r3 < r2のときは、所定の場所まで戻りましょう i < n
  fe1608:   bafffffb    blt fe15fc <loop+0x14>
  fe160c:   e1a0f00e    mov pc, lr ! 最後にreturn

ARM32に対して、CortexM0はThumbしか使えない。

ARMアーキテクチャ - Wikipedia

  • ARM64
  • ARM32
  • Thumb 16
  • CortexM0
  • 最新のiPhoneは64ビット命令で、レジスタの命令の長さだけではなく機械語も別物になっている。アセンブリだけ見ているぶんには似ているけど全く別物。
  • ARM64をみて、条件ビットが必要なくなったのではなかろうかと開発者も感じたっぽい
  • ARM64でがんばっているっぽい

まとめ

  • 構造化プログラミングのベース知識はこれで全部。
  • 基本はCPUでもどれも一緒っぽいけど、CPUごとにいろいろ工夫している
  • コンパイラは同じgccなので、生成コード自体にはそんなに違いはなかろう。比較もしやすい。
  • gccの読みこなし。
  • 条件分岐までいけばある程度読めるような感じがする。

なお

  • switch文は、バイナリ的にはif文の羅列ではなく、テーブルを作ったり二分木したりして、いろいろ工夫している。

12. 複雑な処理を読んでみよう

  • プロローグ スタック確保 レジスタ退避
  • エピローグ レジスタ復元 スタック解放

  • ロード遅延

  • 分岐遅延
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
  fe15ac:   03e00008    jr  ra
  fe15b0:   27bd0018    addiu   sp,sp,24 ! エピローグ

f:id:shigemk2:20150520212602p:plain

もう少し複雑な奴

int call_complex2(int a, int b)
{
  static_value = return_arg1(b);
  return b;
}
00fe15b4 <call_complex2>:
  fe15b4:   27bdffe8    addiu   sp,sp,-24 ! プロローグ
  fe15b8:   afbf0014    sw  ra,20(sp)
  fe15bc:   afb00010    sw  s0,16(sp) ! プロローグ
  fe15c0:   00a08021    move    s0,a1 ! a1を不揮発性のs0に入れて退避
  fe15c4:   0c3f8518    jal fe1460 <return_arg1>
  fe15c8:   00a02021    move    a0,a1 ! a0は壊しっぱなし
  fe15cc:   af82fff4    sw  v0,-12(gp)
  fe15d0:   02001021    move    v0,s0 ! s0(a1)を復帰させる
  fe15d4:   8fbf0014    lw  ra,20(sp) ! エピローグ
  fe15d8:   8fb00010    lw  s0,16(sp)
  fe15dc:   03e00008    jr  ra
  fe15e0:   27bd0018    addiu   sp,sp,24 ! エピローグ
  • ABI的に壊していいレジスタとだめなレジスタがある。壊していいレジスタのことを揮発性レジスタ、壊してはいけないレジスタを不揮発性レジスタという。
  • 不揮発性レジスタは破壊する前に退避、触ったあとに復帰しておけばおーけー。っていうABIで、触ってはいけないというわけではない。
  • 第一引数がa0 第二引数がa1

位置独立コード - Wikipedia

  • 呼び出し元退避(callersaved)と呼び出し先退避(calleesaved)
  • 揮発性(volatile)と不揮発性(non-volatile)
  • 破壊レジスタと保証レジスタ
  • 一時レジスタと保持レジスタ

  • ABIによる取り決め

  • 揮発性のレジスタの保存に不揮発性のレジスタが必要となり,そのために不揮発性のレジスタをスタックに保存する,というアルゴリズムでコード生成されている

PowerPC

プロローグ 本体 エピローグの三枚おろし

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

f:id:shigemk2:20150520214742p:plain

00fe15a8 <call_complex2>:
  fe15a8:   94 21 ff e0     stwu    r1,-32(r1)
  fe15ac:   7c 08 02 a6     mflr    r0
  fe15b0:   93 a1 00 14     stw     r29,20(r1)
  fe15b4:   90 01 00 24     stw     r0,36(r1)
  fe15b8:   7c 9d 23 78     mr      r29,r4
  fe15bc:   7c 83 23 78     mr      r3,r4
  fe15c0:   4b ff fe 9d     bl      fe145c <return_arg1>
  fe15c4:   3d 20 00 fe     lis     r9,254
  fe15c8:   90 69 18 00     stw     r3,6144(r9)
  fe15cc:   7f a3 eb 78     mr      r3,r29
  fe15d0:   80 01 00 24     lwz     r0,36(r1)
  fe15d4:   7c 08 03 a6     mtlr    r0
  fe15d8:   83 a1 00 14     lwz     r29,20(r1)
  fe15dc:   38 21 00 20     addi    r1,r1,32
  fe15e0:   4e 80 00 20     blr

ARMとSH

  • ipは揮発性なのでおざなり
  • ARMは結構複雑
00fe1578 <call_complex1>:
  fe1578:   e1a0c00d    mov ip, sp
  fe157c:   e92dd800    push    {fp, ip, lr, pc}
  fe1580:   e24cb004    sub fp, ip, #4

  fe1584:   e3a000fe    mov r0, #254    ; 0xfe
  fe1588:   ebffffb5    bl  fe1464 <return_arg1>
  fe158c:   e2800001    add r0, r0, #1

  fe1590:   e89da800    ldm sp, {fp, sp, pc}

グローバル変数を触るやつ

00fe1594 <call_complex2>:
  fe1594:   e1a0c00d    mov ip, sp
  fe1598:   e92dd810    push    {r4, fp, ip, lr, pc} ! プログラムカウンタを保存しておくやつ
  fe159c:   e24cb004    sub fp, ip, #4

  fe15a0:   e1a04001    mov r4, r1
  fe15a4:   e1a00001    mov r0, r1
  fe15a8:   ebffffad    bl  fe1464 <return_arg1>
  fe15ac:   e59f3008    ldr r3, [pc, #8]    ; fe15bc <call_complex2+0x28>
  fe15b0:   e5830000    str r0, [r3]
  fe15b4:   e1a00004    mov r0, r4

  fe15b8:   e89da810    ldm sp, {r4, fp, sp, pc}

  fe15bc:   00fe1800    .word   0x00fe1800 ! グローバル変数

SH

  • デカイ即値は近くにおいておく
  • 遅延スロットのせいで処理がごちゃごちゃ。
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)
  • アラインメントがあっていないと動かない。
  • 2バイト境界 4バイト境界 間をあける
  • 処理がごちゃごちゃになっているので、SHはプロローグ 本体 エピローグ 即値領域の四枚にわけないといけない。
00fe1520 <_call_complex2>:
  fe1520:   2f 86           mov.l   r8,@-r15
  fe1522:   4f 22           sts.l   pr,@-r15
  fe1524:   68 53           mov r5,r8
  fe1526:   d0 04           mov.l   fe1538 <_call_complex2+0x18>,r0   ! fe1440 <_return_arg1>
  fe1528:   40 0b           jsr @r0
  fe152a:   64 53           mov r5,r4
  fe152c:   d1 03           mov.l   fe153c <_call_complex2+0x1c>,r1   ! fe1800 <_static_value>
  fe152e:   21 02           mov.l   r0,@r1
  fe1530:   60 83           mov r8,r0
  fe1532:   4f 26           lds.l   @r15+,pr
  fe1534:   00 0b           rts 
  fe1536:   68 f6           mov.l   @r15+,r8
  fe1538:   00 fe           mov.l   @(r0,r15),r0
  fe153a:   14 40           mov.l   r4,@(0,r4)
  fe153c:   00 fe           mov.l   @(r0,r15),r0
  fe153e:   18 00           mov.l   r0,@(0,r8)
00fe1520 <_call_complex2>:
  fe1520:   2f 86           mov.l   r8,@-r15
  fe1522:   4f 22           sts.l   pr,@-r15

  fe1524:   68 53           mov r5,r8
  fe1526:   d0 04           mov.l   fe1538 <_call_complex2+0x18>,r0   ! fe1440 <_return_arg1>
  fe1528:   40 0b           jsr @r0
  fe152a:   64 53           mov r5,r4
  fe152c:   d1 03           mov.l   fe153c <_call_complex2+0x1c>,r1   ! fe1800 <_static_value>
  fe152e:   21 02           mov.l   r0,@r1
  fe1530:   60 83           mov r8,r0

  fe1532:   4f 26           lds.l   @r15+,pr
  fe1534:   00 0b           rts 
  fe1536:   68 f6           mov.l   @r15+,r8

  fe1538:   00 fe           mov.l   @(r0,r15),r0
  fe153a:   14 40           mov.l   r4,@(0,r4)
  fe153c:   00 fe           mov.l   @(r0,r15),r0
  fe153e:   18 00           mov.l   r0,@(0,r8)
  • デカイ即値が使えない上に遅延スロットがアレすぎてごちゃごちゃしている。

H8とi386

  • r4は不揮発性レジスタなので、r4に値を突っ込んでr0に戻り値としている
  • 可変長なので直にアドレスを書ける。
  • 戻り値に値を直に書き込める
  • bを不揮発性レジスタを経由してr0に入れる
  • おおもとなるコンパイラはgccなので結構似ているのでは
00fe14f6 <_call_complex2>:
  fe14f6:   6d f4           mov.w   r4,@-r7
  fe14f8:   0d 14           mov.w   r1,r4
  fe14fa:   0d 10           mov.w   r1,r0
  fe14fc:   5e fe 14 44     jsr @0xfe1444:24
  fe1500:   6b 80 18 00     mov.w   r0,@0x1800:16
  fe1504:   0d 40           mov.w   r4,r0
  fe1506:   6d 74           mov.w   @r7+,r4
  fe1508:   54 70           rts 

i386

  • 戻り値のアドレスをスタックに積んでいる
  • 引数もスタックに積んでいる
  • スタックに積んでコールする
  • 最後にebxを解放
  • 引数がスタック渡し
  • 引数を不揮発性レジスタにつっこんで、戻り値に入れている
  • 64ビットだとスタックに入れない。
00fe151c <call_complex2>:
  fe151c:   53                      push   %ebx
  fe151d:   8b 5c 24 0c             mov    0xc(%esp),%ebx
  fe1521:   53                      push   %ebx
  fe1522:   e8 16 ff ff ff          call   fe143d <return_arg1>
  fe1527:   83 c4 04                add    $0x4,%esp
  fe152a:   a3 00 18 fe 00          mov    %eax,0xfe1800
  fe152f:   89 d8                   mov    %ebx,%eax
  fe1531:   5b                      pop    %ebx
  fe1532:   c3                      ret    

まとめ

C言語は生成されるアセンブラが推測しやすいと言われることが多くありますが,関数の内部で行なわれている処理を実際に読み,こうしてブロック化してみると,コンパイラがどのようなアルゴリズムでアセンブラを生成しているのか,たしかにある程度推測できるようにも思えるのではないでしょうか

三枚おろし

三枚おろし