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

by shigemk2

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

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

勉強会

演算処理を見てみよう の続き

P96-

PowerPC おさらい 演算

P96まで

r3 = r3 + r4;
r3 = r3 + r4;
r3 = r4 + r5;

戻り値はr3 r3とr4とr5で計算する

PowerPCのおさらい

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

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

逆アセンブルするとこんな感じ

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命令のアセンブラを2進数で表示したやつ(bcとhexeditを併用するとよい)

01111000 01100011 0010001 00010100

2進数を眺めて、変化しているところを観察する

第2、第3オペランドのフォーマット

add r3, r3, r4

add r3, r4, r5

01111000 01100011 00100010 00010100
01111000 01100100 00101010 00010100

このように区切ると、わかりやすいかもしれない

011110 00011 00011 00100 010 00010100
011110 00011 00100 00101 010 00010100

01110がadd(オペコード)で、3つの区切りがオペランド、010 00010100もオペコードと推測できちゃう。

バイトごとに綺麗に区切れているわけではない。

もっと詳しく知りたい人はPowerPCのドキュメントを見たらよいでしょう。

PowerPC - Wikipedia

(Wikipediaの外部リンクは死んでました)

バイナリエディタで確認

hexeditでごにょごにょしてみる

7c632214を7cbefa14とかに変えてみる

アドレスの計算方法

P100

ELFはファイルの形式(PowerPCだろうがMIPSだろうが形式は一緒)

readelfは専用のものではなく汎用のものを使ってもいい。

readelfで情報を取り出す。全部を取り出すと時間がかかりすぎるのでやめる。

readelf -a powerpc-elf.x
ELF ヘッダ:
  マジック:  7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00 
  クラス:                            ELF32
  データ:                            2 の補数、ビッグエンディアン
  バージョン:                        1 (current)
  OS/ABI:                            UNIX - System V
  ABI バージョン:                    0
  型:                                EXEC (実行可能ファイル)
  マシン:                            PowerPC
  バージョン:                        0x1
  エントリポイントアドレス:          0xfe16b4
  プログラムの開始ヘッダ:            52 (バイト)
  セクションヘッダ始点:              10380 (バイト)
  フラグ:                            0x0
  このヘッダのサイズ:                52 (バイト)
  プログラムヘッダサイズ:            32 (バイト)
  プログラムヘッダ数:                1
  セクションヘッダ:                  40 (バイト)
  セクションヘッダサイズ:            14
  セクションヘッダ文字列表索引:      11
...続く

P102

ファイルは細かく区分けされており、それをセクションという。

実行可能ファイル→データ→データは分割されており、分割されたデータのことをセクションという。

  • 機械語コード部分のセクション
  • シンボル情報のセクション
  • デバッグ情報のセクション

機械語コードは.textというセクションに格納されている。

これらの情報は機械に読みやすくなっている。

セクションヘッダ:
  [番] 名前              タイプ          アドレス Off    サイズ ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00fe1400 001400 0002b0 00  AX  0   0 16
  [ 2] .data             PROGBITS        00fe1800 001800 000008 00  WA  0   0  4
  [ 3] .debug_abbrev     PROGBITS        00000000 001808 00012d 00      0   0  1
  [ 4] .debug_info       PROGBITS        00000000 001935 000667 00      0   0  1
  [ 5] .debug_line       PROGBITS        00000000 001f9c 0000aa 00      0   0  1
  [ 6] .debug_frame      PROGBITS        00000000 002048 0002dc 00      0   0  4
  [ 7] .debug_pubnames   PROGBITS        00000000 002324 00029c 00      0   0  1
  [ 8] .debug_aranges    PROGBITS        00000000 0025c0 000020 00      0   0  1
  [ 9] .debug_str        PROGBITS        00000000 0025e0 00020e 01  MS  0   0  1
  [10] .comment          PROGBITS        00000000 0027ee 000011 01  MS  0   0  1
  [11] .shstrtab         STRTAB          00000000 0027ff 00008d 00      0   0  1
  [12] .symtab           SYMTAB          00000000 002abc 000520 10     13  12  4
  [13] .strtab           STRTAB          00000000 002fdc 0002e7 00      0   0  1
フラグのキー:

重要なのはアドレスOff(オフセット)(英語だとADDR)

  • アドレス メモリ上に配置されているアドレス(メモリにコピーされている部分)
  • オフセット ファイル中の位置(先頭から)

ファイルの位置とアドレスの位置は違う。

41: 00fe1468     8 FUNC    GLOBAL DEFAULT    1 add

例だとadd()の場所は0x00fe1468で、機械語コードの先頭から0x68の位置にある。

0x00fe1400をベース値、0x68をオフセット値と呼ぶ

この場合、アドレスからfeを抜けばオフセットになるって考えてもいいと思う。

  1. 位置のオフセット値=目的のアドレス-先頭アドレス
  2. 先頭ファイル位置-オフセット値=目的のファイル位置

→実行形式中の機械語コードの場所を特定出来る けど、よくわからなかったら一から検索してみるのも手

レジスタと即値の演算

P104

PowerPCはレジスタを湯水のように使うという推測

2を足したりインクリメントしたり

引き続きPowerPC

C言語

int add_two(int a)
{
  return a + 2;
}

int inc(int a)
{
  return ++a;
}

逆アセンブル結果

00fe147c <add_two>:
  fe147c:   38 63 00 02     addi    r3,r3,2
  fe1480:   4e 80 00 20     blr

00fe1484 <inc>:
  fe1484:   38 63 00 01     addi    r3,r3,1
  fe1488:   4e 80 00 20     blr
  • addiが使われている
  • 即値が使われている
  • r3が使われている

アセンブラの中で直に使われている数値のことを即値という。

比較してみると…

fe147c:  38 63 00 02     addi    r3,r3,2
fe1484: 38 63 00 01     addi    r3,r3,1

3863がオペランドで、後半2バイトが即値なんだろうなあという推察

0x0002を0xffffに変えると、-1になる

fe147c:  38 63 ff ff     addi    r3,r3,-1

addi は Add Immediateの意味で、即値(Immediate Value)を加算するという意味になる。

0xffffが-1になる補数の話はここでは割愛

or命令による論理演算

P106

C言語

0b0110 || 0b0100 = 0b0110

みたいな計算

ビット演算はバイナリを処理していけば嫌でもわかるけどここでは割愛

int or(int a, int b)
{
  return a | b;
}

int or_one(int a)
{
  return a | 1;
}

逆アセンブル結果

00fe148c <or>:
  fe148c:   7c 63 23 78     or      r3,r3,r4
  fe1490:   4e 80 00 20     blr

00fe1494 <or_one>:
  fe1494:   60 63 00 01     ori     r3,r3,1
  fe1498:   4e 80 00 20     blr
  • orとoriが使われている
  • iをつけると即値になるね
7C632378
1111100011000110010001101111000
60630001
1100000011000110000000000000001

以上、PowerPCの話

MIPSではどうなっているのか?

遅延スロットを思い出そう(漢文でいうところのレ点)

  • adduとaddiu
  • orとori
00fe1470 <add>:
  fe1470:   03e00008    jr  ra
  fe1474:   00851021    addu    v0,a0,a1

00fe1478 <add3>:
  fe1478:   00851021    addu    v0,a0,a1
  fe147c:   03e00008    jr  ra
  fe1480:   00461021    addu    v0,v0,a2

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

00fe1494 <or>:
  fe1494:   03e00008    jr  ra
  fe1498:   00851025    or  v0,a0,a1

00fe149c <or_one>:
  fe149c:   03e00008    jr  ra
  fe14a0:   34820001    ori v0,a0,0x1
  • v0が戻り値、第2、第3オペランドで計算しているという考え方
  • MIPSの場合、即値計算のaddはadduiではなくaddiu
  • 即値計算の場合、オペコードにはiがつくのはPowerPCと同じ
  • (ただしMIPSのレジスタにはエイリアスがかかっている)

まとめ

  • PowerPCのレジスタは32本あるので、レジスタの指定だけで15ビットも消費することがある(MIPSも同じ)
  • addiやoriはレジスタ2本10ビット、即値16ビットで26ビットのフィールドが必要
  • オペコードに確保できるビット数が激しく少ない(6ビットなので64個しか命令が作れないことになってしまう)
  • レジスタを減らすという方法もある
  • PowerPCやMIPSでは「レジスタを湯水のように使う」ことを設計思想にしている

5 その他のCPUを見てみる(RISC編)

P118

これまでのまとめ

  • アセンブラはなんとなく読める
  • readelf hexeditなどの解析方法

  • 高機能プロセッサ MIPS PowerPC

  • 組み込みプロセッサ SH ARM

組み込み向けRISCプロセッサを見よう

SHを見てみよう

  • スーパーエッチと読む
  • 国産の組み込み向けプロセッサ
  • はやぶさやSSに組み込まれている

(でも同じ値段でラズパイとか買えちゃうよなあ…)

SHの機械語コードは2バイト長

c言語

void null()
{
  return;
}

int return_zero()
{
  return 0;
}

int return_one()
{
  return 1;
}

逆アセンブル結果

00fe1400 <_null>:
  fe1400:   00 0b           rts 
  fe1402:   00 09           nop 

00fe1404 <_return_zero>:
  fe1404:   00 0b           rts 
  fe1406:   e0 00           mov #0,r0

00fe1408 <_return_one>:
  fe1408:   00 0b           rts 
  fe140a:   e0 01           mov #1,r0
  • rtsが全ての関数についている
  • rtsはリターン命令
  • 左から右へ、即値をレジスタに代入しているぽい(PowerPCなどとは逆)
  • どちらがsourceでどちらがdestinationかはCPUによって違う
  • SHも遅延スロットを持っている
  • 何もしてほしくなかったらnopで埋めてる
  • r0が戻り値というアタリ
  • 即値には#をつける
  • SHの機械語コードは2バイト固定

組み込み系CPUは速度よりも省メモリであることを優先する

規模が小さいもの向けのCPUなので、命令の規模も小さい。(ただし最近のCPUは全体的に安くなってきているのでここまでケチケチする必要もなかったりする)

2バイト命令でどうやって即値を扱うのか

  • 仮に即値が16ビット以上だったらオペコード部分が消滅してしまう
  • オペランドも3つあると15ビット消費するMIPSとかと比べちゃうと、オペランド部分で命令が決まってしまうのでは
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);
}

逆アセンブル結果

00fe140c <_return_int_size>:
  fe140c:   00 0b           rts 
  fe140e:   e0 04           mov #4,r0

00fe1410 <_return_pointer_size>:
  fe1410:   00 0b           rts 
  fe1412:   e0 04           mov #4,r0

00fe1414 <_return_short_size>:
  fe1414:   00 0b           rts 
  fe1416:   e0 02           mov #2,r0

00fe1418 <_return_long_size>:
  fe1418:   00 0b           rts 
  fe141a:   e0 04           mov #4,r0

int型のサイズが4バイトなので、レジスタサイズは32ビットとなる。

命令長≠レジスタサイズ

2バイト(16ビット)の即値をどうやってレジスタに格納しているのか

short return_short()
{
  return 0x7788;
}
00fe141c <_return_short>:
  fe141c:   90 01           mov.w   fe1422 <_return_short+0x6>,r0 ! 7788
  fe141e:   00 0b           rts 
  fe1420:   00 09           nop 
  fe1422:   77 88           add #-120,r7

コメントを省くと、こうなる。

00fe141c <_return_short>:
  fe141c:   90 01           mov.w   fe1422
  fe141e:   00 0b           rts 
  fe1420:   00 09           nop 
  fe1422:   77 88           add #-120,r7
  • 2バイトの中に7788は入らないぞ。どうするの?
  • mov.w fe1422が場所を示しているっぽい
  • そこで指し示している場所が即値っぽい
  • メモリの別のところにある値をレジスタに代入している

ここの逆アセンブラ結果にあまり意味はなかった

  fe1422:    77 88           add #-120,r7

絶対アドレスと相対アドレス

疑問: 9001からどうやってfe1422というアドレスを指定できているの?

00fe141c <_return_short>:
  fe141c:   90 01           mov.w   fe1422
  fe141e:   00 0b           rts 
  fe1420:   00 09           nop 
  fe1422:   77 88           add #-120,r7

解答: オフセット値を即値として命令中に含ませる

SHはPC相対のアドレッシング・モードを持つ

  • PC相対 その命令が配置されているアドレスからの相対値
  • PC プログラム・カウンタ 現在実行中のアドレスを保持するためのレジスタ
  • アドレッシング・モード 命令の位置からオフセット値のぶんだけ移動させた位置にアクセスする

(x86ではPCではなくIP インストラクションポインタという)

なんか計算して9001になって相対値を計算しているんだろうなあという推測

  fe141c:    90 01           mov.w   fe1422

工程が2つに分かれている

  1. 読み込み(フェッチ)
  2. 実行

プログラムカウンタは次に実行する命令を指す。

SHのPC相対はややこしい

PCは2つ先の命令を指していたりするからややこしい(逆にPCが今の命令を指しているようなCPUはほとんどない)

00fe141c <_return_short>:
  fe141c:   90 01           mov.w   fe1422 <_return_short+0x6>,r0 ! 7788
  fe141e:   00 0b           rts 
  fe1420:   00 09           nop 
  fe1422:   77 88           add #-120,r7
  • 0x9001の後半0x01がオフセット値なんだろうなあという推測
  • PC相対は4バイト単位で計算されているという推測
  • P119はなんか混乱した記述が
  • PC(プログラムカウンタ)は,2つ先の命令を指している.
  • mov.wで2バイトをPC相対でロードする際には,PC+オフセットで アドレスを計算している.(単純に加算するだけ)

SHのPC相対について (2014/10/04) PC相対によるメモリアクセスの際に,PCの値が4バイトアラインメントされて いるという記述がありますが,すみませんちょっと筆者の勘違いが残ってしまって いたようです.(情けない…) sh-elf.dを実際に見てみたところ,以下のようになっているようです. PC(プログラムカウンタ)は,2つ先の命令を指している. mov.wで2バイトをPC相対でロードする際には,PC+オフセットで アドレスを計算している.(単純に加算するだけ) mov.lで4バイトをPC相対でロードする際には,PCを4バイトアラインメント して,さらにオフセットを加算することでアドレスを計算している. (4バイトロードなので,アラインメントされる) わりと大きな間違いが残ってしまいすみません.校正は頑張ったのですが, SHの章はわりと早い時期に書いていた部分なので,チェック漏れでそのまま 残ってしまったようです.増刷などで修正の機会があれば直したいです

今日はP119まで

熱血!アセンブラ入門 サポートページ