by shigemk2

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

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

熱血!アセンブラ入門 読書会(3) - connpass

P69

MIPSのアセンブラを見てみよう

もっとPowerPCを見てみよう→見ない

  • 同じCPUのアセンブラばかり読むと飽きる
  • まだあまりPowerPCを知らない状態でMIPSを読むことで、アセンブラは「なんとなく」読めることを認識するため

(ただし、この本は何十種類もアセンブラをみる)

今までの知識で他のアセンブラがどの程度わかるのか

MIPSってなに

RISCの元祖となったCPUでミップスと読む。

  • 大学の講義でよく使われる
  • 読みやすい
  • 現在は組み込み向け

なぜMIPSなのか

  • RISCといえばMIPS
  • PowerPCの知識でMIPSを読んでみよう

MIPSを知れば、たいていのRISC系がなんとなく読めるようになる

http://kozos.jp/books/asm/

  • c サンプルソース
  • s コンパイルして生成されたアセンブラ
  • o アセンブルされ生成されたオブジェクトファイル
  • x リンクされ生成された実行形式ファイル
  • d 実行形式ファイルを逆アセンブルして出力された逆アセンブル結果

cのソースからコンパイル→アセンブル→リンクで三段階の処理を行ったあとで実行可能ファイルが生まれる

gccをやると、いきなり実行可能ファイルが生まれているかのように見えるが、-vをつけてgccをやると、アセンブラが生まれてオブジェクトファイルが生まれてから、実行可能ファイルが生成されるプロセスをながめることが出来る。

dはたぶんdisassembleのことでしょう。

この本で見るのはcのソースコードと逆アセンブル結果

xをhexeditしてmakeして逆アセンブルすると内容が変わったりする

MIPSのアセンブラ

ソースコード

void null()
{
  return;
}

int return_zero()
{
  return 0;
}

int return_one()
{
  return 1;
}

逆アセンブル結果(MIPS)

00fe1400 <null>:
  fe1400:   03e00008    jr  ra
  fe1404:   00000000    nop

00fe1408 <return_zero>:
  fe1408:   03e00008    jr  ra
  fe140c:   00001021    move    v0,zero

00fe1410 <return_one>:
  fe1410:   03e00008    jr  ra
  fe1414:   24020001    li  v0,1

逆アセンブル結果(PowerPC)

00fe1400 <null>:
  fe1400:   4e 80 00 20     blr

00fe1404 <return_zero>:
  fe1404:   38 60 00 00     li      r3,0
  fe1408:   4e 80 00 20     blr

00fe140c <return_one>:
  fe140c:   38 60 00 01     li      r3,1
  fe1410:   4e 80 00 20     blr

PowerPCとMIPSとで比較してみると、blrはjr raでreturnなんだなっていうアタリをつけることが出来るかもしれない

moveとliで順番がちょっと違うのか?

  • アドレス、機械語コード、アセンブラのニーモニックという並びはPowerPCと同様
  • バイト単位で区切られていなくって、くっついている(くっついている理由はよくわからない)
  • 4バイトでひとまとまりにしたかったんじゃないの?という予想
  • 関数ごとにアセンブラが生成
  • 4バイト固定
  • 固定長命令

バイト単位(8bit単位)

関数からの返りかた

すべての関数にjr raがついているnull()でもjr raって書かれているので、これが関数から戻るための命令では?という感じ

ジャンプ命令

  • jr 指定したアドレスにジャンプする
  • jはジャンプのj C言語でいうところのGOTOみたいなもの
  • 戻り先のアドレスにジャンプすることで関数の呼びもとに戻る
  • jr jump registerの意味で、raはreturn addressの略
  • raというレジスタにジャンプしているという話

命令の順序がひっくり返っている?

順番がおかしいのではないか

00fe1408 <return_zero>:
  fe1408:   03e00008    jr  ra
  fe140c:   00001021    move    v0,zero

move命令のほうが先なんじゃないのか?という疑問

00fe1410 <return_one>:
  fe1410:   03e00008    jr  ra
  fe1414:   24020001    li  v0,1
  • PowerPCだとr3に値を入れていた
  • v0というレジスタで戻り値を返すという動作
  • v0に0とか1とか代入しているのではないか
  • やっぱり順番がおかしい→遅延分岐

MIPSは遅延スロットを持つ

遅延分岐とは

  • ジャンプ命令を実行する前に次の命令を実行してからジャンプする

下の例でいうと、fe1414の命令を実行してからfe1410を実行する。それが遅延分岐。

00fe1410 <return_one>:
  fe1410:   03e00008    jr  ra
  fe1414:   24020001    li  v0,1

ただし、正確には、

ジャンプ命令を実行したときに、パイプラインにすでに乗っている次の命令も継続して実行する。

ここでいうパイプラインとは、CPUの設計の話になってくるので、パイプラインがどうちゃらっていうと面倒なので、「ジャンプ命令はその次の命令と位置をひっくり返してアセンブラを読む」という理解でいい。

ジャンプ命令を実行する前に次の命令を実行してからジャンプする。

遅延スロット ジャンプ命令の時に実行される次の命令の位置(命令を読み込むときに、次の命令もすでに読まれている)

ぷよぷよとかテトリスでいうと、次の一手が見えている状態

http://blog-imgs-48-origin.fc2.com/s/u/p/superfamicommatome/nazo-1.jpg

無駄のカバー 1命令も捨てられずに継続して実行される

パイプラインについてはP193で詳細に説明します。

遅延スロットがないCPUもある。

初見殺し 遅延スロット

null() ではnopとなっている。No OPerationの略。「何もしない」という命令で、遅延スロットの尺稼ぎでNOP命令が使われる。jr ra以降の命令が遅延スロットとして実行されて誤作動を起こすことがあるので、NOPを入れて命令を分離しておく。

jr raするだけだったらnopしておこうねっていう話。

命令を捨ててしまうと1クロックぶんがもったないので、CPUのパイプラインに穴を作らないための遅延スロット。

以下2つは同じアセンブラである

jr   ra
li  v0,1

遅延スロットを読むのが面倒なら、このように解釈するとといい。こうすると遅延スロットじゃない順番でアセンブラが実行される

li   v0,1
jr  ra
nop

遅延スロットという予備知識があれば、他のCPUにも適用できる

遅延スロットはレ点とおぼえておくといいんじゃないのかね。微レ存

http://upload.wikimedia.org/wikibooks/ja/a/ae/Reten2.JPG

ゼロの代入が特別扱いになっている?

return_zeroとreturn_oneは違う命令なのでは?

00fe1408 <return_zero>:
  fe1408:   03e00008    jr  ra
  fe140c:   00001021    move    v0,zero

00fe1410 <return_one>:
  fe1410:   03e00008    jr  ra
  fe1414:   24020001    li  v0,1

ちょっと00001021を24020000に書き換えてみると、

00fe1408 <return_zero>:
  fe1408:   03e00008    jr  ra
  fe140c:   00001021    li  v0,0

要するに、move命令はli命令で代替できるのに、なんでmove命令なんて使っているの?という疑問

MIPSはゼロレジスタを持つ

  • 値が常にゼロ
  • ゼロレジスタからの値の呼び出しは必ずゼロになる
  • ゼロレジスタへの値の書き込みは意味が無い
  • ゼロという値が頻繁に使われるので、特別なレジスタにしてしまえばいいのでは
  • という説明だけど、他の値だって頻繁に使われているので、とりあえず他の使い方もあるんだよっていう話。

move命令について

  • レジスタからレジスタへの値のコピー
  • PowerPCのmr命令みたいなもん(P66)
  • move v0,zeroはv0にゼロを代入している
  • でもli v0,0と命令は一緒である
  • ただし、結果はコンパイラによって違っており、今回はgccのコンパイル結果によるもの
  • 使える時には使ったほうがいいよねっていう程度の理解でいいと思う

  • ゼロを扱う機会が多ければ専用レジスタがあればいいけど、少ない10本のレジスタの1本を専用レジスタとして潰すのは不利とも言える

MIPSまとめ1

  • 遅延スロットがあること
  • ゼロレジスタを持っている

とてもきれいなアセンブラなんだけど、実用的なものではないし、遅延スロットが他のCPUよりも面倒。でも、うまくいけば1日で逆アセンブラを作ることだって出来る

PowerPCを最初に引き合いに出したのは、こういう特殊知識がないから。

数値の扱い方

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);
}
00fe1418 <return_int_size>:
  fe1418:   03e00008    jr  ra
  fe141c:   24020004    li  v0,4

00fe1420 <return_pointer_size>:
  fe1420:   03e00008    jr  ra
  fe1424:   24020004    li  v0,4

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

00fe1430 <return_long_size>:
  fe1430:   03e00008    jr  ra
  fe1434:   24020004    li  v0,4

バイト数はPowerPCと一緒だけど、型のバイト数はモノによって違っていたりするし、CPUのビット数とintのビット数が一致するとも限らない

→MIPSでやったことはPowerPCと一緒なので、MIPSは32ビットCPUなんだねえというゆるふわ理解でいい

short return_short()
{
  return 0x7788;
}

long return_long()
{
  return 0x778899aa;
}

short return_short_upper()
{
  return 0xffee;
}

long return_long_upper()
{
  return 0xffeeddcc;
}

でかい命令は分割されているのだねっていうアレ

00fe1438 <return_short>:
  fe1438:   03e00008    jr  ra
  fe143c:   24027788    li  v0,30600

00fe1440 <return_long>:
  fe1440:   3c027788    lui v0,0x7788
  fe1444:   03e00008    jr  ra
  fe1448:   344299aa    ori v0,v0,0x99aa

00fe144c <return_short_upper>:
  fe144c:   03e00008    jr  ra
  fe1450:   2402ffee    li  v0,-18

00fe1454 <return_long_upper>:
  fe1454:   3c02ffee    lui v0,0xffee
  fe1458:   03e00008    jr  ra
  fe145c:   3442ddcc    ori v0,v0,0xddcc

遅延スロット以外はPowerPCと一緒。luiとoriが対になって、値の大きな即値を分割して代入している

PowerPC 大きな即値対応版 lisとoriが対になっている。

00fe1450 <return_long_upper>:
  fe1450:   3c 60 ff ee     lis     r3,-18
  fe1454:   60 63 dd cc     ori     r3,r3,56780
  fe1458:   4e 80 00 20     blr

li v0,0 li v0,FFFF

引数の渡し方

引数付きの関数

これはどんなアセンブラになっているのだろう

int return_arg1(int a)
{
  return a;
}

int return_arg2(int a, int b)
{
  return b;
}

2つのCPUのやつと比べると、ちょっと違いがあることがわかる。

MIPSの場合、第一引数を代入するレジスタと戻り値のレジスタは違う。

MIPS

00fe1460 <return_arg1>:
  fe1460:   03e00008    jr  ra
  fe1464:   00801021    move    v0,a0

00fe1468 <return_arg2>:
  fe1468:   03e00008    jr  ra
  fe146c:   00a01021    move    v0,a1

PowerPC

00fe145c <return_arg1>:
  fe145c:   4e 80 00 20     blr

00fe1460 <return_arg2>:
  fe1460:   7c 83 23 78     mr      r3,r4
  fe1464:   4e 80 00 20     blr

MIPSまとめ2

  • 戻り値 v0
  • 第一引数 a0
  • 第二引数 a1

(aはargumentの意 vはvalueの意 数字に意味は込められていない)

レジスタ番号

  • MIPSのレジスタには別名が存在する
  • MIPSのレジスタは0-31番の通し番号が存在する

→実際は何番なの?

P88みたく小細工すると、レジスタのエイリアスが姿を消して本名が見える

別に統一されたルールで命名されているわけでもない

00fe1400 <null>:
  fe1400:   03e00008    jr  $31
  fe1404:   00000000    nop

00fe1408 <return_zero>:
  fe1408:   03e00008    jr  $31
  fe140c:   00001021    move    $2,$0

特殊レジスタの扱い

raの実体はr31という汎用レジスタ

関数を呼び出すときはr31に値を入れるという機能が存在しており、そのような特殊な機能が組み込まれているレジスタを特殊レジスタという。

また、何を代入してもゼロが返ってくるゼロレジスタも特殊な機能をもった特殊レジスタといえる。

汎用レジスタ 特殊レジスタ

PowerPCのlr(link register)は完全に機能だけを持ったレジスタで汎用ではない

対してMIPSの特殊レジスタは汎用レジスタとしても使える($0 $31)

v0だろうがa0だろうが使い方は一緒だけど、名前が違うとコンパイラによっては不都合が生じることがある

lrレジスタといえば関数の戻り値が入っているレジスタなんだなあという理解

本当のまとめ

  • PowerPCは遅延スロットを持たないが、MIPSは遅延スロットを持つ
  • MIPSはゼロレジスタを持っている
  • PowerPCは第一引数を渡すレジスタと戻り値を返すレジスタが同じだが、MIPSは異なる

結構似ている

4. 演算処理を見てみよう

これまでで決まった値を返すだけの簡単な処理を見てきたけど、今度は演算を見てみよう

PowerPCに頭を切り替えろ!

PowerPCでレジスタ間の演算を見る

ソースコード

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

int add3(int a, int b, int c)
{
  return a + b + c;
}

逆アセンブル結果

r3=r3+r4

r4=r3+r4 r3=r4+r5

00fe1468 <add>:
  fe1468:   7c 63 22 14     add     r3,r3,r4
  fe146c:   4e 80 00 20     blr

00fe1470 <add3>:
  fe1470:   7c 83 22 14     add     r4,r3,r4
  fe1474:   7c 64 2a 14     add     r3,r4,r5
  fe1478:   4e 80 00 20     blr
  • add命令のオペランドで計算しているんだねっていうアタリ
  • 一度に出来る計算には限度があるので、何回かに分けて演算処理をやっているというアタリ
  • RISCの4バイト長なので一度に出来る計算には限りがある
  • r3が戻り値と第一引数のレジスタを兼用している
  • r4は第二引数のレジスタである

  • →add r3,r4,r5 →r3 = r4 + r5

  • →add a,b,c だと、格納元、加算元、加算元 という感じで計算していることがわかる

オペランドの指定方法

  • addは7cじゃないのかね
  • add命令のオペランドは2バイト目、3バイト目じゃないのかな

→オペコードの解析から16進数の暗算に話がスライドしていっている

16進数と2進数は、アセンブラを知る上ではおぼえておくといいんじゃないのかな

人によってやり方覚え方は違うので、ハンドアセンブルとかしてみたらいいんじゃないじゃないでしょうか

-P96