読者です 読者をやめる 読者になる 読者になる

by shigemk2

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

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

勉強会

前回のおさらい

P131

void null()
{
  return;
}

int return_zero()
{
  return 0;
}

int return_one()
{
  return 1;
}
00fe1400 <null>:
  fe1400:   e1a0f00e    mov pc, lr

00fe1404 <return_zero>:
  fe1404:   e3a00000    mov r0, #0
  fe1408:   e1a0f00e    mov pc, lr

00fe140c <return_one>:
  fe140c:   e3a00001    mov r0, #1
  fe1410:   e1a0f00e    mov pc, lr
  • ARM最大の特徴 条件コード(eをつけて、無条件で実行さす)
  • ARMは4バイト固定長
  • 即値に#をつける
  • ARMはリトルエンディアン
    • 機械には読みやすく人には読みづらい
  • 遅延スロットはない
  • MIPSだとjr ra(pcは汎用レジスタではなく、lrは汎用レジスタ)
  • PowerPCだとblr(pcもlrも汎用レジスタではない)
  • 戻るべき場所が書いてある

多バイト値の扱いに特徴がある

int型 ポインタ、ともに4バイト、つまり32ビットCPU

int return_int_size()
{
  return sizeof(int);
}

int return_pointer_size()
{
  return sizeof(int *);
}

int return_short_size()
{
  return sizeof(short);
}

int return_long_size()
{
  return sizeof(long);
}
00fe1414 <return_int_size>:
  fe1414:   e3a00004    mov r0, #4
  fe1418:   e1a0f00e    mov pc, lr

00fe141c <return_pointer_size>:
  fe141c:   e3a00004    mov r0, #4
  fe1420:   e1a0f00e    mov pc, lr

00fe1424 <return_short_size>:
  fe1424:   e3a00002    mov r0, #2
  fe1428:   e1a0f00e    mov pc, lr

00fe142c <return_long_size>:
  fe142c:   e3a00004    mov r0, #4
  fe1430:   e1a0f00e    mov pc, lr

命令長が4バイト固定なので、4バイトの即値はどうするんだろう。

short return_short()
{
  return 0x7788;
}

long return_long()
{
  return 0x778899aa;
}

short return_short_upper()
{
  return 0xffee;
}

long return_long_upper()
{
  return 0xffeeddcc;
}
  • ; 以降はコメント
  • ARMの逆アセンブラにはそういう機能がついている
  • 30464は16進数で0x7700(逆アセンブラは関係ない)

  • r0に一度0x7700を代入し、次にr0に0x88を加算している(0x7700 + 0x88 = 0x7788)
  • つまり、1バイトずつ値を入れて完成させているように見える(2バイト値を2回に分けて計算している)
00fe1434 <return_short>:
  fe1434:   e3a00c77    mov r0, #30464  ; 0x7700
  fe1438:   e2800088    add r0, r0, #136    ; 0x88
  fe143c:   e1a0f00e    mov pc, lr
  • SHと同じようにPC相対で即値のアドレスを指定している
  • 今までと書き方が違っていて、[pc,#0]は[PC+0]と解釈できる
  • CPUは命令をフェッチしてフェッチしてから実行する
  • つまり、実行するときプログラム・カウンタは次の命令をさしている
  • ARMはプログラム・カウンタは2つ先をさしている
  • ここでは2つ先の命令を読み込んで、値をr0に格納している
00fe1440 <return_long>:
  fe1440:   e59f0000    ldr r0, [pc, #0]    ; fe1448 <return_long+0x8>
  fe1444:   e1a0f00e    mov pc, lr
  fe1448:   778899aa    .word   0x778899aa
  • 17は符号計算のように見えて符号計算ではなく、即値として扱われている

  • (本としては)新命令mvn mov+n そのまま代入するのではなく、なんらかのネガティブな操作を行っている可能性がある
  • つまり、ビット反転させているように見える
  • 指定した即値をビット反転してからr0に入れる
>>> hex(~0x11 & 0xffff)
'0xffee'
  • 少ない命令数でやりたいことを実現しているように見える(SHと同じ)
  • ゼロ付近の値は1命令で生成できる
00fe144c <return_short_upper>:
  fe144c:   e3e00011    mvn r0, #17
  fe1450:   e1a0f00e    mov pc, lr
  • ビット反転して、2回減算している仕組み。
  • 逆算するとこうなる
  • この手の値を割り出すのは結構しんどいようにできている
  • というか、ARMは結構クセがあり、わかりにくいので、アセンブラをARMで学ぶのは結構つらい
>>> hex(~1114112 & 0xffffffff - 0x2200 - 0x33)
'0xffeeddcc'
long return_long_upper()
{
  return 0xffeeddcc;
}
00fe1454 <return_long_upper>:
  fe1454:   e3e00811    mvn r0, #1114112    ; 0x110000
  fe1458:   e2400c22    sub r0, r0, #8704   ; 0x2200
  fe145c:   e2400033    sub r0, r0, #51 ; 0x33
  fe1460:   e1a0f00e    mov pc, lr
  • ARMは即値として扱えるフィールドが1バイトしかなくて狭いので、4バイトの値を生成するのに3命令も必要になる
  • でも定数値はなるべくゼロに近いようにさせておくアーキテクチャにしておいたほうが、使う側もそうさせやすくなっている
  • 即値がやたら難しいのがARMの特徴

引数の渡し方はPowerPCと似ている

  • 戻り値のレジスタと第一引数を渡すレジスタは一緒
  • r0は第一引数、r1は第二引数を渡すレジスタ
int return_arg1(int a)
{
  return a;
}

int return_arg2(int a, int b)
{
  return b;
}
00fe1464 <return_arg1>:
  fe1464:   e1a0f00e    mov pc, lr

00fe1468 <return_arg2>:
  fe1468:   e1a00001    mov r0, r1
  fe146c:   e1a0f00e    mov pc, lr

加算処理は3オペランド方式で行われる

int add(int a, int b)
{
  return a + b;
}

int add3(int a, int b, int c)
{
  return a + b + c;
}
  • add r0=r0+r1
00fe1470 <add>:
  fe1470:   e0800001    add r0, r0, r1
  fe1474:   e1a0f00e    mov pc, lr
  • 流れとしてr2は第三引数じゃないのかっていうアタリがつく
  • add r1=r0+r1
  • add r0=r1+r2

  • add r0=r0+r1

  • add r0=r0+r2
  • でもいいんじゃないかっていう気がするんですが、同じレジスタを何度も更新すると並列処理などで不利になるんじゃないかっていう推測(よくわからない)
  • 格納元、加算元、加算元っていう並びはPowerPCやMIPSと同じ
00fe1478 <add3>:
  fe1478:   e0801001    add r1, r0, r1
  fe147c:   e0810002    add r0, r1, r2
  fe1480:   e1a0f00e    mov pc, lr

まとめ

  • 組み込み系CPUは命令やコードのサイズを少なくしている
  • とはいえ、スマホは速度向上も求められているので、スピードが二の次みたいな感じではなくなってきている
  • PowerPCやMIPSはコードの高速化を求めている
  • 重要なのは総合的なバランス

  • ARMが64ビット化されたときに命令ががらっと変わった

  • 命令体系がちょっと変わっててスピードも求められるようになってきた
  • ARM64bitは結構最近のCPUなのでこの本で話をするのに間に合わなかった
  • だんだん64bit化されている流れ

命令のエイリアス

P146

  • 機械語コードを見なおしてみよう
  • PowerPCを思い出してみよう

li命令の機械語コードを見てみる

  • PowerPCはr3が戻り値
int return_short_size()
{
  return sizeof(short);
}
00fe1424 <return_short_size>:
  fe1424:   38 60 00 02     li      r3,2
  fe1428:   4e 80 00 20     blr
>>> bin(0x38600002)
'0b111000011000000000000000000010'
'00111000 01100000 00000000 00000010'
# この部分00011を11111に変換するとr3がr31になる
# 001110がオペコードの部分で逆アセンブルするとliになる
'001110 00011 000000000000000000010'
  • 38が先頭についているのはli命令の他にaddi命令がある
00fe147c <add_two>:
  fe147c:   38 63 00 02     addi    r3,r3,2
  fe1480:   4e 80 00 20     blr
>>> bin(0x38630002)
'0b111000011000110000000000000010'
# オペランド レジスタ レジスタ 16ビット即値フィールド
'001110 00011 00011 00000000 00000010'
  • というか、liとaddiは同じ命令なのではないか、という推測

li命令はゼロ指定のaddi命令になっている

# 第2オペランドを11111にするとaddiになる
'001110 00011 11111 00000000 00000010'

つまり、liの実体はaddiであると思われる

下記2つの命令は一緒。

li r3, 0x7788
addi r3, r0, 0x7788
  • r0レジスタはゼロレジスタだったり普通の汎用レジスタだったりする
  • MIPSだとゼロレジスタが存在するが、r0レジスタがゼロレジスタとなることがある(addiで使われるとき)
  • ココでのr0はゼロ扱いになる
  • PowerPCはクセが内容に見えてこういうクセがある
  • MIPSに遅延スロットがあったり、PowerPCにこういうr0レジスタの意味が変わってくることがある
  • CASLみたく、なんのクセもないCPUはあるけど、実用的ではない
addi r3, r0, 0x7788
addi r3,r0,xxxx
r3 = r0 + xxxx
li r3,xxxx
r3 = xxxx

アセンブラのエイリアス

  • addi命令を加算命令ではなく代入命令の代用として扱うこともできる
  • 便宜上別の命令として扱うことができるものを「エイリアス」という

  • 「エイリアス」という言葉はCPUによっては簡略ニーモニックなどと呼ばれる

  • 命令の種類はオペコード部分のビット幅で上限数が決まる
  • つまり、命令の数には限りがある(6bit=64)
  • エイリアスを使うことで命令の数を節約できる

PowerPCのmr命令の場合

00fe1460 <return_arg2>:
  fe1460:   7c 83 23 78     mr      r3,r4
  fe1464:   4e 80 00 20     blr
# 第1オペランドを0x4から0x5にすると、or命令になる
>>> bin(0x7c832378)
'0b1111100100000110010001101111000'
'01111100 10000011 00100011 01111000'
'011111 00100 00011 00100 01101111000'

mr命令はor命令のエイリアスになっている

# ニーモニック 第2オペランド 第1オペランド 第3オペランド
011111 00101 00011 00100 01101111000
r3=r5|r4
r3=r4|r4
  • or命令自体はいわゆるor計算と一緒
  • mrはorを簡略化している

MIPSのli命令の場合

  • RISCプロセッサ(固定長)の場合は命令数削減のためにエイリアスが使われていることが多い
  • 可変長だとよく使う命令は省メモリ化できるのでこの限りではない

MIPSもliという命令を持っている

遅延スロット レ点を思い出そう

00fe1428 <return_short_size>:
  fe1428:   03e00008    jr  ra
  fe142c:   24020002    li  v0,2

こいつを外して有効化すると、v0が正体をあらわす

#DFLAGS-mips-elf   += -M reg-names=numeric

レジスタは32本なので、5ビット必要という決まり

>>> bin(0x24020002)
'00100100 00000010 0000000000000010'
'00100100 000 11111 0000000000000010'

レジスタを指定している部分を探す

# 00010を11111に変更してみる
'00100100 000 11111 0000000000000010'

他の命令を見ても、24がオペコードじゃないのかっていうアタリ

li命令はaddiu命令のエイリアスになっている

00fe1484 <add_two>:
  fe1484:   03e00008    jr  ra
  fe1488:   24820002    addiu   v0,a0,2

00fe148c <inc>:
  fe148c:   03e00008    jr  ra
  fe1490:   24820001    addiu   v0,a0,1
  • ぱっと見、PowerPCがli命令がゼロレジスタを使ったaddi命令だったので、MIPSもa0がゼロレジスタとしてli命令がaddiu命令のエイリアスではないかという推測
  • 似た名前だし同じものじゃないのかっていう

li命令の第1オペランド部分のビット数を変えたら、liがaddiuに代わるがわかる

li $2,30600
addiu $2,$31,30600

MIPSのmove命令の場合

MIPSの他の命令についても見てみる

move命令はPowerPCのmr命令に相当

00fe1460 <return_arg1>:
  fe1460:   03e00008    jr  ra
  fe1464:   00801021    move    v0,a0
00fe1470 <add>:
  fe1470:   03e00008    jr  ra
  fe1474:   00851021    addu    v0,a0,a1

で、move命令はaddu命令のエイリアスで、adduは桁溢れした例外は出さない。uはunsignedの意

PowerPCだとmr命令はor命令のエイリアスで、MIPSはそれをゼロレジスタとの加算で実現している

f:id:shigemk2:20150114220213p:plain

P163