演算処理を見てみよう の続き
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のドキュメントを見たらよいでしょう。
(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を抜けばオフセットになるって考えてもいいと思う。
- 位置のオフセット値=目的のアドレス-先頭アドレス
- 先頭ファイル位置-オフセット値=目的のファイル位置
→実行形式中の機械語コードの場所を特定出来る けど、よくわからなかったら一から検索してみるのも手
レジスタと即値の演算
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つに分かれている
- 読み込み(フェッチ)
- 実行
プログラムカウンタは次に実行する命令を指す。
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まで