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

by shigemk2

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

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

勉強会

X86アセンブラ/GASでの文法 - Wikibooks

おさらい

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(なんでゼロやねん)

f:id:shigemk2:20150225204353p:plain

代入やロードもすべて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が出てきたような気がするので。