おさらい
H8は可変長命令
- 命令の長さをケチるためにreturn 0の場合はsubを使っている。movだと4バイト
- H8は16ビットCPU
00fe1400 <_null>: fe1400: 54 70 rts 00fe1402 <_return_zero>: fe1402: 19 00 sub.w r0,r0 fe1404: 54 70 rts 00fe1406 <_return_one>: fe1406: 79 00 00 01 mov.w #0x1,r0 fe140a: 54 70 rts 00fe140c <_return_int_size>: fe140c: 79 00 00 02 mov.w #0x2,r0 ! 2バイト fe1410: 54 70 rts 00fe1412 <_return_pointer_size>: fe1412: 79 00 00 02 mov.w #0x2,r0 ! 2バイト fe1416: 54 70 rts 00fe141e <_return_long_size>: fe141e: 79 00 00 04 mov.w #0x4,r0 ! 4バイト fe1422: 54 70 rts 00fe1424 <_return_short>: fe1424: 79 00 77 88 mov.w #0x7788,r0 fe1428: 54 70 rts 00fe142a <_return_long>: fe142a: 79 00 77 88 mov.w #0x7788,r0 fe142e: 79 01 99 aa mov.w #0x99aa,r1 fe1432: 54 70 rts
引数のわたしかた
00fe1444 <_return_arg1>: fe1444: 54 70 rts 00fe1446 <_return_arg2>: fe1446: 0d 10 mov.w r1,r0 fe1448: 54 70 rts
加算命令は2オペランドになっている(から、3オペランドやりたいときは2回に分ける)
- r0 += r1
00fe144a <_add>: fe144a: 09 10 add.w r1,r0 fe144c: 54 70 rts
- r0 += r1
- r0 += r2
00fe144e <_add3>: fe144e: 09 10 add.w r1,r0 fe1450: 09 20 add.w r2,r0 fe1452: 54 70 rts
レジスタの個数は
どっちも2バイト
00fe1446 <_return_arg2>: fe1446: 0d 10 mov.w r1,r0 fe1448: 54 70 rts 00fe144e <_add3>: fe144e: 09 10 add.w r1,r0 fe1450: 09 20 add.w r2,r0 fe1452: 54 70 rts
- 1バイト目が命令、2バイト目がオペランドをあらわしているように思える。
- レジスタ番号がそのまま機械語コードになっているような感じ。
- H8はレジスタが何を使っているのか分かりやすい(2バイト目の右ひとけたと左ひとけたで確認する)
00fe1446 <_return_arg2>: fe1446: 0d 43 mov.w r4,r3 fe1448: 54 70 rts
書き換え
レジスタは0-7までのようである。
H8は省メモリサイズを意識したアーキテクチャ
機械語コードを極力小さくするようにしたアーキ。
レジスタが少ないぶんビット数が少ないから、機械語(add.wとかmov.wとかが少なくてすむ)
これが32本レジスタとかだったら5ビット*3で15ビットで16ビット機械語だとかなりつらい。ので、ひつぜんてきに4バイトくらいの機械語が必要。
可変長命令の利点がいきている。
メモリ操作を見てみる
ロードとストア
// ポインタが指すメモリから値を読み込んで返している。 // volatileは最適化対策 // *pはint // p単独ではメモリのアドレスでアドレスの番号がpに格納されていて、それを*で取り出す。 // 連続して宣言するとおかしなことになるから気をつけるように。 int load(volatile int *p) { return *p; } void store(volatile int *p) { *p = 0xff; }
- アドレスから値をロードする メモリの中身を読み込んで、それをレジスタに入れる(@r0→r0)
- メモリに値を書き込んでいる(ストア) 値をレジスタに入れて、レジスタの値をメモリに書き込んでいる(#0xff→r2 r2→@r0)
- なおr2だったりr0なのは開発者の好み
00fe1466 <_load>: fe1466: 69 00 mov.w @r0,r0 fe1468: 54 70 rts 00fe146a <_store>: fe146a: 79 02 00 ff mov.w #0xff,r2 fe146e: 69 82 mov.w r2,@r0 fe1470: 54 70 rts
H8では値の移動はすべてmov命令でおこなう
サブタイのとおりだけど、H8ではなまらmov.wがつかわれる。他のCPUと比べてみると一目瞭然。
- load word zero store word zero(なんでゼロやねん)
代入やロードもすべてmov.wで行われる。直交性を追求するCISCライクで、人間が書く(ハンドアセンブルする)ことをそんなに期待していない。
ディスプレースメントつきレジスタ間接もつかえる
long load_long(volatile long *p) { return *p; } void store_long(volatile long *p) { *p = 0x11223344; }
- @(disp:x, r0)=@(r0+disp)
- ビッグエンディアンなので若干複雑
- でかい値は処理を分割しないといけない
- しかもレジスタの処理は、一旦レジスタに値を入れる必要がある
00fe1472 <_load_long>: fe1472: 6f 01 00 02 mov.w @(0x2:16,r0),r1 ! 先にディスプレースメント計算をやっておかないとr0の値がおかしくなる fe1476: 69 00 mov.w @r0,r0 fe1478: 54 70 rts 00fe147a <_store_long>: fe147a: 79 02 11 22 mov.w #0x1122,r2 fe147e: 79 03 33 44 mov.w #0x3344,r3 fe1482: 69 82 mov.w r2,@r0 fe1484: 6f 83 00 02 mov.w r3,@(0x2:16,r0) fe1488: 54 70 rts
構造体
だいたいこんなかんじ
ディスプレースメントつきレジスタ間接が使われている。
struct structure { int a; // @r0 int b; // @(0x2:16, r0) r0+2 int c; // @(0x4:16, r0) r0+4 }; int member(struct structure *p) { p->b = 1; return p->c; }
00fe148a <_member>: fe148a: 79 02 00 01 mov.w #0x1,r2 fe148e: 6f 82 00 02 mov.w r2,@(0x2:16,r0) fe1492: 6f 00 00 04 mov.w @(0x4:16,r0),r0 fe1496: 54 70 rts
アドレスは16ビットになっている
静的変数の参照方法
int static_value = 10; long static_long = 0x12345678; int *get_static_value_addr() { return &static_value; } int get_static_value() { return static_value; } void set_static_value(int a) { static_value = a; }
00fe1498 <_get_static_value_addr>: fe1498: 79 00 18 00 mov.w #0x1800,r0 fe149c: 54 70 rts 00fe149e <_get_static_value>: fe149e: 6b 00 18 00 mov.w @0x1800:16,r0 fe14a2: 54 70 rts 00fe14a4 <_set_static_value>: fe14a4: 6b 80 18 00 mov.w r0,@0x1800:16 fe14a8: 54 70 rts
メモリ上のアドレスを即値として直接指定するようなアドレッシング・モードを「直接アドレス」と呼びます.そして set_static_value()はmov命令のオペランドが逆になっただけですから,これも「直接アドレスJによってストア先が指定され,ストア動作が行なわれているようです。このような代入ができるのは,可変長の CISC系命令ならではのように思います。
H8はCISCなので命令の長さが変えられる。
レジスタのビットサイズ=データバスの幅>=アドレスバスの幅
一度に遅れるデータサイズのことをデータバスの幅と表現される
- メモリからCPUに転送できるデータサイズはレジスタのビットサイズと一緒。
- メモリの場所をを指すことができるけども、指定できるアドレスのサイズのこと。
32ビット環境でELFを作る(リンク)処理をやっていても、作られたものは16ビット環境でしか使えない。
クロスコンパイルの特徴は別のマシンで開発を行うこと。開発環境は本番環境より高性能な場合が多い。
バスのビット幅を太くしようとするとCPUのチップ面積や機械語コードに跳ね返ってくるので、省エネが第一の組み込み系アーキだとつらい。
アドレスを直接扱うものでも少ないメモリで出来るようにしたい。
i386
(ふつうはここから入門するんだけども)
アイサンハチロクorサンハチロク
8086からの進化で、80386以降をi386 x86は80286以降
その仕様はソースをつぎ足しつぎ足ししているものなのですごく異様。
CISCとマイコンは別扱いにしたほうがいいんじゃないかっていう。
極端な可変長命令
例によってreturn系。
void null() { return; } int return_zero() { return 0; }
- AT&T記法 即値には$ レジスタには%をつける 代入は右向き GNU系で決めた書き方
- Intel記法 即値に$をつけない 代入は左向き Intelが決めた書き方
GCCの台頭でAT&T記法が強くなった。
00fe1400 <null>: fe1400: c3 ret 00fe1401 <return_zero>: fe1401: b8 00 00 00 00 mov $0x0,%eax fe1406: c3 ret
命令長が5バイトだったり1バイトだったりで若干カオス。
固定長命令だと命令を数回に分けないといけないことがあったが、i386は可変長なので1回の命令ですんでいる。
命令が一発ですむのがよいか、命令サイズがぐちゃぐちゃなのは嫌だっていうのかそのあたりは人による。
固定長なら12バイトになるやつが可変長だと6バイトですむ。
- i386ではレジスタが番号付けされていない(EAX)
- A(8080 8bit)→AX(8086 16bit)→EAX(80386 32bit)
X も Eもextendedの意味だから、これもまた若干カオス。
8086以降から、互換性があってモード切り替えで昔のバージョンが維持されている。
mov ax,0 ! b8 00 00 mov eax,0 ! b8 00 00 00 00
(そういえばH8にも32ビット版があったりする)
多バイト値をそのまま扱うことが出来る
i386は普及しすぎて歴史が長すぎるので、複雑。
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); }
00fe140d <return_int_size>: fe140d: b8 04 00 00 00 mov $0x4,%eax fe1412: c3 ret 00fe1413 <return_pointer_size>: fe1413: b8 04 00 00 00 mov $0x4,%eax fe1418: c3 ret 00fe1419 <return_short_size>: fe1419: b8 02 00 00 00 mov $0x2,%eax fe141e: c3 ret 00fe141f <return_long_size>: fe141f: b8 04 00 00 00 mov $0x4,%eax fe1424: c3 ret
リトルエンディアンです。
short return_short() { return 0x7788; } long return_long() { return 0x778899aa; }
00fe1425 <return_short>: fe1425: b8 88 77 00 00 mov $0x7788,%eax fe142a: c3 ret 00fe142b <return_long>: fe142b: b8 aa 99 88 77 mov $0x778899aa,%eax fe1430: c3 ret
- 0x1800 00 18 (16bit)
- 0x1800 00 18 00 00 (32bit)
- リトルエンディアンなので拡張されたら0をあとづけできる
- ひっくり返って気持ち悪いけどどんどんいける。
引数はスタック経由
int return_arg1(int a) { return a; } int return_arg2(int a, int b) { return b; }
00fe143d <return_arg1>: fe143d: 8b 44 24 04 mov 0x4(%esp),%eax ! mov eax, [esp+4] Intel記法のほうが分かりやすいぞ。 fe1441: c3 ret 00fe1442 <return_arg2>: fe1442: 8b 44 24 08 mov 0x8(%esp),%eax fe1446: c3 ret
spはスタックポインタ
スタックという言葉がカジュアルに出てきているけど後述。
レジスタがあって、レジスタ+2とか+4とかの値をレジスタに入れている。
メモリ経由(レジスタ経由ではない)で渡されていて、ESPというレジスタが存在する。
メモリ上の値を直接加算することができる
加算処理をみるとわりとはっきりする。
int add(int a, int b) { return a + b; } int add3(int a, int b, int c) { return a + b + c; }
レジスタを複数用意して加算するのではなく、メモリの値を直接1つのレジスタに格納している。
00fe1447 <add>: fe1447: 8b 44 24 08 mov 0x8(%esp),%eax fe144b: 03 44 24 04 add 0x4(%esp),%eax fe144f: c3 ret 00fe1450 <add3>: fe1450: 8b 44 24 08 mov 0x8(%esp),%eax fe1454: 03 44 24 04 add 0x4(%esp),%eax fe1458: 03 44 24 0c add 0xc(%esp),%eax fe145c: c3 ret
これらのCPUでメモリ上の値を加算しようとしたら,いったんレジスタにロードしてからレジスタ間で加算を行い、さらに結果をレジスタからメモリにストアする、という手順を踏む必要があります。
つまり、RISCだとこんな感じ。
add r1, r0 add r2, r0
CISCだと↑のような感じでメモリ上の値を直接計算している。
インクリメントするだけの専用命令がある
int add_two(int a) { return a + 2; } int inc(int a) { return ++a; }
00fe145d <add_two>: fe145d: 8b 44 24 04 mov 0x4(%esp),%eax fe1461: 83 c0 02 add $0x2,%eax fe1464: c3 ret 00fe1465 <inc>: fe1465: 8b 44 24 04 mov 0x4(%esp),%eax fe1469: 40 inc %eax fe146a: c3 ret
レジスタを1加算する専用命令がi386には存在する。(inc)しかもaddのエイリアスですらない。本当の専用命令である。使用頻度の高いやつは1バイトで専用命令にしているのでしょう。
簡単な処理は短い命令ですます。ことで、省エネを図っている。
今回はなまらespが出てきたような気がするので。