by shigemk2

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

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

P43

リターン命令

c言語サンプルプログラムとpowerpcサンプルプログラムを比較する

リスト1.1

void null()
{
  return;
}

int return_zero()
{
  return 0;
}

int return_one()
{
  return 1;
}

リスト1.5

00fe1400 <null>:
  fe1400:   4e 80 00 20     blr

00fe1404 <return_zero>:
  fe1404:   38 60 00 00     li      r3,0
  fe1408:   4e 80 00 20     blr

00fe140c <return_one>:
  fe140c:   38 60 00 01     li      r3,1
  fe1410:   4e 80 00 20     blr

関数null()はblrを実行している vs returnで戻るだけの関数

blrは関数の呼び出し元に戻る=リターン命令ではないかと「推測」される

どの関数のアセンブリもblrが書かれているので、returnがblrに相当するのだろうなあというアタリがつけられる。

実際にこれを機械語で書くと4e 80 00 20で、CPUはこちらのほうを見ているわけだが、人間が読み書きするときは便宜上blrと書く(数字だと分かりづらいから)

「アセンブラ」と「アセンブリ言語」

P44

CPUとアセンブラについて復習してみる

CPUは電子回路で、特定のバイト列が来たら特定の命令をやる。

これを人間にわかりやすくしたのがニーモニック。

バイト列を全て覚えておくのはつらい。(命令が少ない時代のときは数字だけを見てどのような動きをするかわかっていたけども)

PowerPCの命令は4バイトなので43億とおりの命令があるから全部そらで覚えるのは事実上不可能だが、使われる命令はあるていど偏りがあるので、覚えられるものは数字で覚えられたりする。

ニーモニックを利用してプログラムを書く。ニーモニックを機械語のバイト列に変換するのがアセンブラ

  • 変換ツール=アセンブラ
  • ニーモニック=アセンブリ言語

アセンブリ言語をアセンブラともいうからわけがわからないことになることがあるが、アセンブラといえばニーモニックのことを言うこともあれば、変換ツールのことをアセンブラということもある

  • null()がに相当
  • null()の機械語コードは0x00fe1400というアドレスに配置
    • メモリとは巨大な配列
    • メモリの4GBとはアドレスの箱が約43億個くらいあるイメージ
    • 0x00fe1400とは、配列の0x00fe1400番目に相当するというイメージ
    • アドレス番地
    • fe1400 4e
    • fe1401 80
    • fe1402 00
    • fe1423 20
    • fe1424 38 .....
  • 4e 80 00 20がblrの実際の機械語
  • blrという命令はreturnに相当

レジスタの使われ方を推測しよう

P45

int return_zero()
{
  return 0;
}
00fe1404 <return_zero>:
  fe1404:   38 60 00 00     li      r3,0
  fe1408:   4e 80 00 20     blr

わけがわからないよ、って思う前に以下の知識を共有してみる

  • アセンブラでは戻り値0を設定して返るという推測
  • nullを見た結果からblrという命令がreturnという命令に相当するという推測
  • li r3,0は戻り値であるゼロを設定しているという推測
  • liは代入命令でr3にゼロを入れているという推測

レジスタ

  • r3はレジスタで、CPUが使える「数値の保存場所」
  • r0 r1 r2 r3....みたいな感じで命名されている
  • PowerPCの場合自由に使えるレジスタを32本持っていて、レジスタの正式名称はGPR
  • li r3,0はr3レジスタにゼロを代入しているというイメージ
int return_one()
{
  return 1;
}
00fe140c <return_one>:
  fe140c:   38 60 00 01     li      r3,1
  fe1410:   4e 80 00 20     blr

→PowerPCでは関数の戻り値はr3レジスタに格納して返す

バイナリエディタで確認しよう

  fe1404:    38 60 00 00     li      r3,0
  fe140c:   38 60 00 01     li      r3,1

違いは最下位バイトじゃないだろうか。

目grep

目視でバイナリデータをチェックすること。

$ hexedit powerpc-elf.x

f:id:shigemk2:20141105220235p:plain

colorオプションをつけると、色分けされて楽しいけど見づらい。

$ hexedit --color powerpc-elf.x

f:id:shigemk2:20141105220316p:plain

00001400というオフセット位置からアセンブリ言語が始まっている。

実行ファイルpowerpc-elf.xを逆アセンブルしたのがpowerpc-elf.d

この場合に限って言えば、先頭の0xfeを除いた位置に配置されていて、逆アセンブルするときは機械語を翻訳している。

機械語は(普段は)暗号化も難読化もされていないので、実行ファイルを読めばナニをやっているのか分析することが出来る。

ただ、暗号化されている実行ファイルもある。

機械語コードの書き換え

P50

hexeditで書き換えたらいいよ

まとめ

P52

  • 1命令は4バイトの固定長命令
  • blr命令で関数の呼びもとに戻る
  • li命令はレジスタに値を代入することができる(ただしli命令はエイリアス)
  • 戻り値はr3レジスタで返す blr命令を呼ぶときにr3レジスタに格納した値が戻り値となる

sample.cはすごく簡単なプログラムで、だいたい4-5行程度に区切られているので、 まずは適当に読んでみて、なんとなく感覚をつかんでから徐々に説明していく。

2 PowerPCの数値のあ使い方を見てみよう

P54

CPUのしごとは数値演算。CPUが扱っているのはあくまで数値である。 CPUは数値のみを扱うものであって、Aという文字すらも0x41という数値として扱う。

サイズをみてみよう

P54

実行せずにプログラムの結果を知る。

sizeof(int)をアセンブリ言語から知る。

  • 「実行が出来なくても結果が分かる!」
  • 「OSが起動していないから実行できない!」
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:   38 60 00 04     li      r3,4
  fe1418:   4e 80 00 20     blr

00fe141c <return_pointer_size>:
  fe141c:   38 60 00 04     li      r3,4
  fe1420:   4e 80 00 20     blr

00fe1424 <return_short_size>:
  fe1424:   38 60 00 02     li      r3,2
  fe1428:   4e 80 00 20     blr

00fe142c <return_long_size>:
  fe142c:   38 60 00 04     li      r3,4
  fe1430:   4e 80 00 20     blr

sizeof(int)って書くとコンパイラが4と解釈してくれている。

「このCPUは32bitです」って言っても厳密なビット数は微妙に異なっていたりして、言ったもん勝ちみたいなところがある。

「32bitといえば32bit計算が一番ラク」というふうに設計されている

PowerPCは32bit つまり4バイトなので、sizeof(int)が4になっているのも割となっとく

でも64bitだからsizeof(int)が8かといえばそうでもない。

ポインタ型のサイズ

sizeof(int *)も4であると言える話。

00fe141c <return_pointer_size>:
  fe141c:   38 60 00 04     li      r3,4
  fe1420:   4e 80 00 20     blr
  • ポインタの指す値はアドレスという理解でいい。
  • ポインタはCPUが扱うことの出来るアドレスの範囲を十分に格納出来るサイズ
  • CPUは奇数は切りが悪いので2のn乗で数えられることが多い
  • カツカツのサイズにすると計算と設計がものすごくしんどい

注意

ポインタ型のサイズはint型のサイズと等しいかそれ未満。

→64ビットCPUではそのようにならないことが多いので、この文言はあまり信じないこと。

ポインタをポインタとして扱うのは面倒なので、intptr_tが定義されている(C言語の規格)

short型とlong型のサイズ

int return_short_size()
{
  return sizeof(short);
}

int return_long_size()
{
  return sizeof(long);
}
00fe1424 <return_short_size>:
  fe1424:   38 60 00 02     li      r3,2
  fe1428:   4e 80 00 20     blr

00fe142c <return_long_size>:
  fe142c:   38 60 00 04     li      r3,4
  fe1430:   4e 80 00 20     blr
  • int型 4バイト
  • ポインタ型 4バイト
  • short型 2バイト(16ビット)
  • long型 4バイト

この本では64ビット、16ビット、8ビットのCPUで、32ビットCPUはある意味デファクトスタンダード

値の代入を見てみよう

P59

もう少し大きな値をliで代入したらどうなるのだろうか

2バイト値の代入

short return_short()
{
  return 0x7788;
}
00fe1434 <return_short>:
  fe1434:   38 60 77 88     li      r3,30600
  fe1438:   4e 80 00 20     blr

30600を代入しており、これの16進数は0x7788となる

ひとつの疑問

00fe1434 <return_short>:
  fe1434:   38 60 77 88     li      r3,30600
  fe1438:   4e 80 00 20     blr

38 60 77 88で、7788が代入する値なのは分かったけど、li r3はどこに相当するの?

→38 60がli r3部分に相当するんじゃないの?っていう推測

アセンブラの命令=命令コード(オペコード)+引数(オペランド)

30600部分という引数で定数値が4バイトの中に埋め込まれているわけで、 この命令の中に埋め込まれている定数値のことを「即値(immediate value)」という

本当の疑問

オペランドのサイズが32ビットを超えたらどうなっちゃうの?

→基本的に32ビットの値は即値で表現出来ない。。

liに相当する部分がオペコードで、liのことをオペコードというわけではない。

オペコードとオペランドを合わせて32ビットで、命令には必ずオペコード部分が存在する。 オペランドだけで32ビットになることはありえない。

32ビット固定長命令で、固定長命令のCPUはRISCプロセッサの特徴のひとつである。

このような問題はRISCプロセッサの宿命。

4バイト値の代入

→基本的に32ビットの値は即値で表現出来ない。。

4バイト(32ビット)の定数値はどうしたらいいのか。。

long return_long()
{
  return 0x778899aa;
}
00fe143c <return_long>:
  fe143c:   3c 60 77 88     lis     r3,30600
  fe1440:   60 63 99 aa     ori     r3,r3,39338
  fe1444:   4e 80 00 20     blr

推測

  • lisとoriで分割して値を代入しているのではないだろうか
00fe143c <return_long>:
  fe143c:   3c 60 77 88     lis     r3,30600
  fe1440:   60 63 99 aa     ori     r3,r3,39338

で、眺めてみると、lisで上半分、oriでした半分の値を代入しているんじゃないかというアタリがつけられる。

PowerPCは命令が4バイト固定長なので、4バイトの定数値のレジスタへの代入は2命令を使って2回に分けておこなう。

→多ビット幅の値を即値としては直接扱えないので、RISCプロセッサでは値を複数回の命令で扱う。

RISCプロセッサにおける大きな値の代入の工夫

  1. (PowerPC)2命令に分けて代入
  2. メモリ上に定数値を置いておき、そのアドレスを指定して読み込む
  3. 命令は固定長命令だが代入の時だけ例外的に4バイトの即値を取れる
  4. (ARM)大まかな値を代入してそこから加減算を繰り返して微調整する
short return_short_upper()
{
  return 0xffee;
}

long return_long_upper()
{
  return 0xffeeddcc;
}
00fe1448 <return_short_upper>:
  fe1448:   38 60 ff ee     li      r3,-18
  fe144c:   4e 80 00 20     blr

00fe1450 <return_long_upper>:
  fe1450:   3c 60 ff ee     lis     r3,-18
  fe1454:   60 63 dd cc     ori     r3,r3,56780
  fe1458:   4e 80 00 20     blr

7788は下の方の値、ffeeは上の方の値。上下は0x8000を基準に。あと、値が上だろうが下だろうが値が大きければlisとoriで二分割するし、即値で一回代入することもある。

バイト列は万万進なので、数えやすい。

http://ja.wikipedia.org/wiki/%E5%91%BD%E6%95%B0%E6%B3%95

引数の渡し方

P65

今までは関数で引数なしだったけど、引数を渡して戻り値を返す一連の流れを見たい。

第一引数と第二引数を渡す

int return_arg1(int a)
{
  return a;
}

int return_arg2(int a, int b)
{
  return b;
}

なんか処理しているんだろうけど、blrしているだけじゃん!

00fe145c <return_arg1>:
  fe145c:   4e 80 00 20     blr

とういうわけで、こちらもみてこう。

00fe1460 <return_arg2>:
  fe1460:   7c 83 23 78     mr      r3,r4
  fe1464:   4e 80 00 20     blr

→ググれば分かるかもしれないけど、とりあえず推理推測して動きを眺めてみよう。 (マイナーなCPUだとそもそもぐぐっても出てこない)

第一引数と戻り値のレジスタは同じ

  • 関数の戻り値はr3
  • r3にコピーしてblrを実行
  • li r3してもr3が関数の戻り値はr3なので、(おそらく最適化を行って)blr命令を実行すれば引数をそのままreturnしたのと同じ意味になると解釈出来る

  • PowerPCでは第一引数がr3レジスタ、第二引数はr4レジスタで関数に渡される

  • 戻り値 r3レジスタで返す

  • 第一引数 r3レジスタで渡す
  • 第二引数 r4レジスタで渡す

これらはABIという規格で決められており、コンパイラはABIに準拠したアセンブラコードを出力する。(CPUによってはコンパイラごとに動きが違ったりすることもある)

http://ja.wikipedia.org/wiki/Application_Binary_Interface

故に、PowerPCでは関数null()と関数return_arg1()は等価ということになる

→return_arg1()と全く同じ使い方でnull()を呼び出すことも出来たり出来る。

void c(void) {
}

→PowerPCベースでコードを書くなら戻り値を第一引数にしたほうが効率がいいらしい、という余談。

ビルド環境をちゃんと作ってみたらいいんだけども。

まとめ

P68

CPUの難しいドキュメントや英語の資料を読まなくても、 アセンブラはあまりきちんと調べずに少しずつ推測しながらという読み方でも読み進めていくことが出来る!

次は

MIPS

感想

最初のCPUと、C言語プログラムとアセンブラの読み方をチュートリアルでレクチャーしているので、ここを抑えたらあとは他のCPUでも応用できるんじゃなかろうか。