前回のおさらい
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はそれをゼロレジスタとの加算で実現している
P163