以下が代表例。
- Modifying a variable
- Modifying a data structure in place
- Setting a field on an object
- Throwing an exception or
- halting with an error
- Printing to the console or reading user input
- Reading from or writing to a file
- Drawing on the screen
以下が代表例。
P69
もっとPowerPCを見てみよう→見ない
(ただし、この本は何十種類もアセンブラをみる)
今までの知識で他のアセンブラがどの程度わかるのか
RISCの元祖となったCPUでミップスと読む。
MIPSを知れば、たいていのRISC系がなんとなく読めるようになる
cのソースからコンパイル→アセンブル→リンクで三段階の処理を行ったあとで実行可能ファイルが生まれる
gccをやると、いきなり実行可能ファイルが生まれているかのように見えるが、-vをつけてgccをやると、アセンブラが生まれてオブジェクトファイルが生まれてから、実行可能ファイルが生成されるプロセスをながめることが出来る。
dはたぶんdisassembleのことでしょう。
この本で見るのはcのソースコードと逆アセンブル結果
xをhexeditしてmakeして逆アセンブルすると内容が変わったりする
ソースコード
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で順番がちょっと違うのか?
バイト単位(8bit単位)
すべての関数にjr raがついているnull()でもjr 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
遅延分岐とは
下の例でいうと、fe1414の命令を実行してからfe1410を実行する。それが遅延分岐。
00fe1410 <return_one>: fe1410: 03e00008 jr ra fe1414: 24020001 li v0,1
ただし、正確には、
ジャンプ命令を実行したときに、パイプラインにすでに乗っている次の命令も継続して実行する。
ここでいうパイプラインとは、CPUの設計の話になってくるので、パイプラインがどうちゃらっていうと面倒なので、「ジャンプ命令はその次の命令と位置をひっくり返してアセンブラを読む」という理解でいい。
ジャンプ命令を実行する前に次の命令を実行してからジャンプする。
遅延スロット ジャンプ命令の時に実行される次の命令の位置(命令を読み込むときに、次の命令もすでに読まれている)
ぷよぷよとかテトリスでいうと、次の一手が見えている状態
無駄のカバー 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にも適用できる
遅延スロットはレ点とおぼえておくといいんじゃないのかね。微レ存
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命令なんて使っているの?という疑問
使える時には使ったほうがいいよねっていう程度の理解でいいと思う
ゼロを扱う機会が多ければ専用レジスタがあればいいけど、少ない10本のレジスタの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
(aはargumentの意 vはvalueの意 数字に意味は込められていない)
→実際は何番なの?
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に頭を切り替えろ!
ソースコード
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
r4は第二引数のレジスタである
→add r3,r4,r5 →r3 = r4 + r5
→オペコードの解析から16進数の暗算に話がスライドしていっている
16進数と2進数は、アセンブラを知る上ではおぼえておくといいんじゃないのかな
人によってやり方覚え方は違うので、ハンドアセンブルとかしてみたらいいんじゃないじゃないでしょうか
-P96