おさらい
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。
- 分岐予測用のヒントがある
- フラグを入れるスロットがある
- 指定の場所に飛ばす(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しか使えない。
- 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 ! エピローグ
もう少し複雑な奴
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
- 呼び出し元退避(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
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言語は生成されるアセンブラが推測しやすいと言われることが多くありますが,関数の内部で行なわれている処理を実際に読み,こうしてブロック化してみると,コンパイラがどのようなアルゴリズムでアセンブラを生成しているのか,たしかにある程度推測できるようにも思えるのではないでしょうか

- アーティスト: 音速ライン
- 出版社/メーカー: NAYUTAWAVE RECORDS
- 発売日: 2007/11/14
- メディア: CD
- クリック: 23回
- この商品を含むブログ (99件) を見る