PowerPC アセンブリ入門

プログラミング

かつてMacやゲーム機を支えたアーキテクチャの世界へようこそ!

PowerPC(パワーピーシー)という名前を聞いたことがありますか? 🤔 かつてAppleのMacintoshシリーズ(1994年から2006年頃まで)や、任天堂のゲームキューブ、Wii、ソニーのPlayStation 3、マイクロソフトのXbox 360といったゲーム機にも搭載されていた、パワフルなCPUアーキテクチャです。最近ではパーソナルコンピュータでの採用は少なくなりましたが、組み込みシステムや特定用途では今でも活躍しています。火星探査機「キュリオシティ」や「パーサヴィアランス」にも搭載されているんですよ!🛰️

このブログでは、そんなPowerPCのアセンブリ言語について、基礎から少しずつ解説していきます。「アセンブリ言語って何?」という方も、「PowerPCって面白そう!」と思った方も、ぜひ一緒に学んでいきましょう!

1. PowerPC とアセンブリ言語について

PowerPC とは?

PowerPCは、1991年にApple、IBM、Motorola(AIM連合と呼ばれました)が共同で開発したRISC(Reduced Instruction Set Computer)ベースの命令セットアーキテクチャ(ISA)です。RISCは、命令の種類を少なく、単純化することで、高速な処理を目指す設計思想です。PowerPCは、IBMが先行して開発していたPOWERアーキテクチャをベースにしており、高い性能と電力効率を目指して作られました。

当初はパーソナルコンピュータ向けに設計されましたが、その性能から、ワークステーション、サーバー、スーパーコンピュータ、そして前述の通り多くのゲーム機や組み込みシステムで採用されることになりました。2006年以降、PowerPC命令セットは「Power ISA」と呼ばれるようになっていますが、「PowerPC」の名前も一部の実装で使われ続けています。

アセンブリ言語とは?

コンピュータの頭脳であるCPUは、「機械語(マシン語)」と呼ばれる0と1の羅列で書かれた命令しか理解できません。しかし、人間が0と1だけで複雑なプログラムを書くのは非常に困難です。

そこで登場するのがアセンブリ言語です。アセンブリ言語は、機械語の命令に、人間が理解しやすい短い英単語(ニーモニックと呼ばれます)を割り当てたものです。例えば、「データをレジスタに読み込む」という機械語の命令に `lwz` (Load Word and Zero) のようなニーモニックを対応させます。

アセンブリ言語で書かれたプログラム(ソースコード)は、「アセンブラ」というツールによって機械語に翻訳(アセンブル)され、CPUが実行できる形式になります。

ポイント: アセンブリ言語は機械語とほぼ1対1に対応しているため、ハードウェア(CPUやメモリ)の動作を非常に細かく制御できます。その代わり、C言語やPythonのような高級言語と比べて記述が複雑になり、特定のCPUアーキテクチャに依存するという特徴があります。

なぜ PowerPC アセンブリを学ぶのか?

  • 低レベルな動作の理解: コンピュータが内部でどのように動いているのか、プログラムがCPUによってどう実行されるのかを深く理解できます。
  • パフォーマンスの最適化: 特定の部分で極限までパフォーマンスを追求したい場合、アセンブリ言語による直接的な制御が有効なことがあります。
  • 組み込みシステム開発: リソースが限られた組み込みシステムでは、メモリ使用量や実行速度を最適化するためにアセンブリ言語が使われることがあります。
  • リバースエンジニアリングやデバッグ: ソフトウェアの動作解析や、コンパイラが生成したコードのデバッグなどに役立ちます。
  • 歴史的・教育的興味: かつて広く使われたアーキテクチャを学ぶことで、コンピュータの進化を体感できます。

2. PowerPC アーキテクチャの概要

PowerPCアセンブリを理解するためには、まずPowerPCの基本的な構造(アーキテクチャ)を知る必要があります。

RISC アーキテクチャ

PowerPCはRISCアーキテクチャを採用しています。主な特徴は以下の通りです。

  • 命令長の固定: ほとんどの命令が32ビット(4バイト)の固定長です。これにより、命令のデコード(解読)が単純になり、高速化に寄与します。 (一部、可変長命令エンコーディング VLE を持つ実装もあります)
  • ロード/ストア アーキテクチャ: メモリへのアクセスは、専用のロード命令(メモリからレジスタへデータを読み込む)とストア命令(レジスタからメモリへデータを書き込む)によってのみ行われます。算術演算や論理演算などの計算は、レジスタ内のデータに対して行われます。
  • 多数のレジスタ: CPU内部に高速な記憶領域であるレジスタを多数持っています。これにより、メモリへのアクセス頻度を減らし、処理を高速化します。

レジスタの種類

PowerPCにはいくつかの種類のレジスタがあります。主なものを紹介します。

種類 名称 サイズ 主な用途
汎用レジスタ GPR (General Purpose Register) 32本 (r0 – r31) 32ビットまたは64ビット 整数演算、アドレス計算、データの一時保存など。最もよく使われるレジスタ。
浮動小数点レジスタ FPR (Floating-Point Register) 32本 (f0 – f31) 64ビット 浮動小数点数(小数)の演算に使用。
特殊目的レジスタ SPR (Special Purpose Register) 複数 様々 CPUの状態や制御に関わる特殊なレジスタ群。

主な特殊目的レジスタ (SPR):

  • LR (Link Register): サブルーチン(関数)呼び出し時に、呼び出し元に戻るためのアドレス(リターンアドレス)を格納します。
  • CTR (Count Register): ループカウンタとして使用されることが多いレジスタ。特定の分岐命令と組み合わせて使われます。
  • CR (Condition Register): 比較命令の結果などを格納する32ビットのレジスタ。8つの4ビットフィールド (CR0-CR7) に分かれており、分岐命令の条件判断に使われます。
  • XER (Integer Exception Register): 整数演算での桁あふれ(オーバーフロー)などの例外情報を格納します。
  • FPSCR (Floating-Point Status and Control Register): 浮動小数点演算の状態や制御情報を保持します。
💡 GPRの規約 (ABI依存):
GPRは基本的にどれも同じように使えますが、OSやコンパイラが定める規約(ABI: Application Binary Interface)によって、特定の役割が割り当てられていることがあります。例えば、Linuxなどでは `r1` をスタックポインタとして使うのが一般的です。また、`r3` は関数の最初の引数や戻り値に使われることが多いです。`r0` は一部の命令でソースレジスタとして指定されると、レジスタの内容ではなく数値 `0` として扱われる特殊な性質を持ちます。アセンブリだけでプログラムを書く場合は `r1` 以外は比較的自由に使えますが、C言語など他の言語と連携する場合は規約を守る必要があります。

エンディアン

PowerPCアーキテクチャはバイエンディアン (Bi-Endian) をサポートしていることが多いです(一部例外あり)。これは、データのバイトオーダー(メモリ上に複数バイトのデータを格納する際の順序)として、ビッグエンディアンとリトルエンディアンの両方を切り替えて使用できることを意味します。

  • ビッグエンディアン: 最上位バイト (Most Significant Byte, MSB) が最も小さいメモリアドレスに格納される方式。人間が数字を読む順序に近いです。
  • リトルエンディアン: 最下位バイト (Least Significant Byte, LSB) が最も小さいメモリアドレスに格納される方式。x86系CPUで採用されています。

どちらのモードで動作するかはシステムによりますが、PowerPCではビッグエンディアンが伝統的に使われることが多いです。ビットの番号付けも、他の多くのCPUとは異なり、最上位ビット (MSB) をビット0とする慣習があります(IBM方式)。

3. 基本的な PowerPC 命令

ここでは、PowerPCアセンブリでよく使われる基本的な命令をいくつか紹介します。ニーモニックの後に続くオペランド(命令の対象)の順序は、一般的に「目的レジスタ, ソースレジスタA, ソースレジスタB(または即値)」の形式が多いですが、命令によって異なります。

データ転送命令 (ロード/ストア)

メモリとレジスタ間でデータをやり取りする命令です。

  • lwz rD, d(rA) : Load Word and Zero
    メモリ[rA + d] から32ビット(ワード)データを読み込み、rD に格納します。d はオフセット(即値、符号付き16ビット)です。
  • stw rS, d(rA) : Store Word
    rS の内容(32ビット)をメモリ[rA + d] に書き込みます。
  • lbz rD, d(rA) : Load Byte and Zero
    メモリ[rA + d] から8ビット(バイト)データを読み込み、上位ビットを0で埋めて rD に格納します。
  • stb rS, d(rA) : Store Byte
    rS の下位8ビットをメモリ[rA + d] に書き込みます。
  • lhz rD, d(rA) : Load Half Word and Zero
    メモリ[rA + d] から16ビット(ハーフワード)データを読み込み、上位ビットを0で埋めて rD に格納します。
  • sth rS, d(rA) : Store Half Word
    rS の下位16ビットをメモリ[rA + d] に書き込みます。
  • li rD, value : Load Immediate
    16ビットの即値 `value` を rD にロードします(上位16ビットは0)。
  • lis rD, value : Load Immediate Shifted
    16ビットの即値 `value` を16ビット左シフトして rD にロードします(下位16ビットは0)。`li` と組み合わせることで、32ビットの即値をレジスタにロードできます。
注意:即値のロード
PowerPCの命令は32ビット固定長であるため、一つの命令で32ビットの即値を直接ロードすることはできません。32ビットの値をロードするには、通常 `lis` (上位16ビットをロード) と `ori` (下位16ビットをOR演算で合成、`addi` も使われる) の2命令を組み合わせます。
# 32ビット値 0x12345678 を r3 にロードする例
lis     r3, 0x1234  # r3 = 0x12340000
ori     r3, r3, 0x5678 # r3 = r3 | 0x00005678 = 0x12345678
# または addi を使う
# addi    r3, r3, 0x5678 # r3 = r3 + 0x5678 (下位16ビットが0なので OR と同じ結果)

算術演算命令

加算、減算、乗算、除算などを行います。

  • add rD, rA, rB : Add
    rA と rB の内容を加算し、結果を rD に格納します。
  • addi rD, rA, value : Add Immediate
    rA の内容と16ビットの即値 `value` (符号拡張される) を加算し、結果を rD に格納します。rA に `r0` を指定すると、実質的に `li` と同様の即値ロードになります(`r0`が0扱いされるため)。
  • subf rD, rA, rB : Subtract From
    rB から rA を減算し(rB – rA)、結果を rD に格納します。減算のオペランド順序が直感的でない点に注意が必要です。
  • mullw rD, rA, rB : Multiply Low Word
    rA と rB を乗算し、結果の下位32ビットを rD に格納します。
  • divw rD, rA, rB : Divide Word
    rA を rB で除算し、商を rD に格納します。

論理演算命令

AND, OR, XOR などのビット単位の論理演算を行います。

  • and rD, rA, rB : AND
    rA と rB の内容のビット単位 AND を計算し、結果を rD に格納します。
  • andi. rD, rA, value : AND Immediate
    rA の内容と16ビットの即値 `value` (ゼロ拡張される) のビット単位 AND を計算し、結果を rD に格納します。末尾の `.` は、演算結果に応じて条件レジスタ(CR0)を更新することを示します。
  • or rD, rA, rB : OR
    rA と rB の内容のビット単位 OR を計算し、結果を rD に格納します。
  • ori rD, rA, value : OR Immediate
    rA の内容と16ビットの即値 `value` (ゼロ拡張される) のビット単位 OR を計算し、結果を rD に格納します。
  • xor rD, rA, rB : XOR
    rA と rB の内容のビット単位 XOR を計算し、結果を rD に格納します。
  • xori rD, rA, value : XOR Immediate
    rA の内容と16ビットの即値 `value` (ゼロ拡張される) のビット単位 XOR を計算し、結果を rD に格納します。

シフト・ローテート命令

レジスタ内のビットを左右に移動させます。

  • slw rD, rA, rB : Shift Left Word
    rA の内容を rB の下位ビットで指定されたビット数だけ左に論理シフトし、結果を rD に格納します(空いたビットには0が入る)。
  • srw rD, rA, rB : Shift Right Word
    rA の内容を rB の下位ビットで指定されたビット数だけ右に論理シフトし、結果を rD に格納します(空いたビットには0が入る)。
  • sraw rD, rA, rB : Shift Right Algebraic Word
    rA の内容を rB の下位ビットで指定されたビット数だけ右に算術シフトし、結果を rD に格納します(空いたビットには元の最上位ビットと同じ値が入る)。
  • rotlw rD, rA, rB : Rotate Left Word
    rA の内容を rB の下位ビットで指定されたビット数だけ左にローテート(回転)させ、結果を rD に格納します。

比較命令

二つの値を比較し、結果を条件レジスタ (CR) に設定します。

  • cmpw crfD, rA, rB : Compare Word
    rA と rB の内容を符号付き32ビット整数として比較し、結果を指定された条件レジスタフィールド `crfD` (CR0-CR7) に設定します。
  • cmplw crfD, rA, rB : Compare Logical Word
    rA と rB の内容を符号なし32ビット整数として比較し、結果を `crfD` に設定します。
  • cmpi crfD, rA, value : Compare Immediate
    rA の内容と16ビットの即値 `value` (符号拡張される) を比較し、結果を `crfD` に設定します。
  • cmpli crfD, rA, value : Compare Logical Immediate
    rA の内容と16ビットの即値 `value` (ゼロ拡張される) を比較し、結果を `crfD` に設定します。

比較結果は、CRフィールド内の LT (Less Than), GT (Greater Than), EQ (Equal), SO (Summary Overflow) の各ビットに反映されます。

分岐命令

プログラムの実行フローを制御します。

  • b target_label : Branch
    無条件に `target_label` で示されるアドレスにジャンプします。
  • bl target_label : Branch and Link
    `target_label` にジャンプする前に、次の命令のアドレスをリンクレジスタ (LR) に保存します。サブルーチン呼び出しに使われます。
  • blr : Branch to Link Register
    リンクレジスタ (LR) に格納されているアドレスにジャンプします。サブルーチンからの復帰に使われます。
  • bctr : Branch to Count Register
    カウントレジスタ (CTR) に格納されているアドレスにジャンプします。
  • bc BO, BI, target_label : Branch Conditional
    条件レジスタ (CR) の状態に基づいて条件分岐します。`BO` は分岐条件(常に分岐、条件が真なら分岐、条件が偽なら分岐など)と分岐予測ヒントを指定し、`BI` はどのCRビットをテストするかを指定します。複雑ですが、これにより非常に多様な条件分岐が可能です。アセンブラは、`beq`, `bne`, `blt`, `bgt` などの単純な条件分岐(拡張ニーモニック)を、この `bc` 命令に変換してくれます。
    • `beq target_label` (Branch if Equal)
    • `bne target_label` (Branch if Not Equal)
    • `blt target_label` (Branch if Less Than)
    • `bgt target_label` (Branch if Greater Than)
    • `ble target_label` (Branch if Less Than or Equal)
    • `bge target_label` (Branch if Greater Than or Equal)

4. 簡単なコード例

簡単な例を見てみましょう。ここでは、GNUアセンブラ (gas) の構文を想定しています。

例1: 二つの数を加算する

.global _start

.text
_start:
    li      r3, 10      # r3 に 10 をロード
    li      r4, 25      # r4 に 25 をロード
    add     r5, r3, r4  # r5 = r3 + r4 (10 + 25 = 35)

    # プログラム終了処理 (システムコールを使用する例)
    # Linux PowerPC (32-bit) の場合: exit(0)
    li      r0, 1       # syscall 番号 (exit) を r0 に
    li      r3, 0       # 終了コード 0 を r3 に
    sc                  # システムコール呼び出し

    # ここには到達しないはず
    b       .           # 無限ループ (念のため)

このコードは、レジスタ r3 に 10、r4 に 25 をロードし、それらを加算した結果 (35) を r5 に格納します。その後、Linux のシステムコールを使ってプログラムを終了しています(システムコールの詳細は環境によって異なります)。

例2: メモリを使った加算

.global _start

.data
value1: .long 100   # メモリ上に 32 ビット整数 100 を確保
value2: .long 200   # メモリ上に 32 ビット整数 200 を確保
result: .space 4    # 結果を格納するための 4 バイトの領域を確保

.text
_start:
    lis     r9, value1@ha # value1 のアドレスの上位16ビットを r9 へ
    la      r9, value1@l(r9) # value1 のアドレスの下位16ビットを r9 に加算 (r9 = &value1)

    lis     r10, value2@ha # value2 のアドレスの上位16ビットを r10 へ
    la      r10, value2@l(r10) # value2 のアドレスの下位16ビットを r10 に加算 (r10 = &value2)

    lis     r11, result@ha # result のアドレスの上位16ビットを r11 へ
    la      r11, result@l(r11) # result のアドレスの下位16ビットを r11 に加算 (r11 = &result)

    lwz     r3, 0(r9)   # r3 = メモリ[r9] (value1 の値 = 100)
    lwz     r4, 0(r10)  # r4 = メモリ[r10] (value2 の値 = 200)

    add     r5, r3, r4  # r5 = r3 + r4 (100 + 200 = 300)

    stw     r5, 0(r11)  # メモリ[r11] に r5 の内容 (300) を書き込む (result = 300)

    # プログラム終了処理 (Linux PowerPC 32-bit)
    li      r0, 1
    li      r3, 0
    sc
    b       .

この例では、`.data` セクションで定義されたメモリ上の値 `value1` と `value2` をレジスタにロードし、加算した結果を `result` のメモリ位置にストア(書き込み)しています。アドレスのロードには `@ha` (high adjusted) と `@l` (low) というシンボル修飾子と `lis`, `la` (load address, addiの別名) 命令を組み合わせて使用しています。これは、PowerPCで32ビットアドレスを扱う一般的な方法です。

例3: 簡単なループ

.global _start

.text
_start:
    li      r3, 0       # 合計を格納するレジスタ r3 を 0 に初期化
    li      r4, 10      # ループカウンタ r4 を 10 に初期化
    mtctr   r4          # r4 の値をカウントレジスタ (CTR) に転送

loop_start:
    addi    r3, r3, 1   # r3 に 1 を加算 (r3 = r3 + 1)
    bdnz    loop_start  # CTR の値をデクリメントし、0でなければ loop_start に分岐

    # ループ終了 (r3 には 10 が入っているはず)

    # プログラム終了処理 (Linux PowerPC 32-bit)
    li      r0, 1
    li      r3, 0       # 最終的な r3 の値ではなく、終了コード 0 を渡す
    sc
    b       .

この例では、カウントレジスタ (CTR) と `bdnz` (Branch if Count Register Decremented is Not Zero) 命令を使って、10回のループを実行し、r3 に 1 を 10 回加算しています。`mtctr` 命令でループ回数を CTR に設定し、`bdnz` 命令が CTR を1減らして、結果が0でなければ指定したラベル (`loop_start`) に分岐します。

5. 開発ツールと環境

PowerPCアセンブリでプログラムを開発するには、いくつかのツールが必要です。

  • アセンブラ (Assembler): アセンブリ言語のソースコードを機械語のオブジェクトファイルに変換するツールです。
    • GNU Assembler (gas): GCC (GNU Compiler Collection) に含まれるフリーでオープンソースのアセンブラ。多くのプラットフォームをサポートしており、PowerPCにも対応しています。Linux環境などで広く使われています。
  • リンカ (Linker): アセンブラが生成したオブジェクトファイルや、他のライブラリファイルを結合して、実行可能なファイルを作成するツールです。
    • GNU Linker (ld): GCC に含まれるリンカ。
  • デバッガ (Debugger): プログラムの実行をステップごとに追跡したり、レジスタやメモリの内容を確認したりして、バグを発見・修正するためのツールです。
    • GNU Debugger (gdb): 高機能なコマンドラインデバッガ。PowerPCを含む多くのアーキテクチャに対応しています。
  • エミュレータ/シミュレータ: PowerPCの実機がなくても、他のコンピュータ上でPowerPCの動作を模擬実行するソフトウェアです。
    • QEMU: 様々なアーキテクチャをエミュレートできるオープンソースのソフトウェア。PowerPCシステムのエミュレーションも可能です。
  • テキストエディタ: ソースコードを書くためのエディタ。お好みのものを使用できます。

Linux環境であれば、GCCツールチェイン(`build-essential` や `gcc-powerpc-linux-gnu` のようなクロスコンパイル用パッケージ)をインストールすることで、これらの基本的なツール一式を揃えることができます。

6. PowerPC の歴史と主な使用例

PowerPC は、コンピュータの歴史の中で重要な役割を果たしてきました。

  • 1970年代後半: IBMのジョン・コック氏らによるRISC研究プロジェクト「IBM 801」がPowerPCの源流となる。
  • 1991年: Apple、IBM、MotorolaがAIM連合を結成し、PowerPCアーキテクチャの開発を開始。
  • 1992年: 最初のPowerPCプロセッサ「PowerPC 601」が登場。IBM RS/6000ワークステーションなどに採用。
  • 1994年: Appleが最初のPowerPC搭載Macintosh「Power Macintosh」シリーズを発売(Power Macintosh 6100など)。
  • 1990年代後半~2000年代初頭: PowerPC 603, 604, 750 (G3), 74xx (G4) など、様々な派生プロセッサが登場。AppleのiMac G3, PowerBook G3/G4, Power Mac G4などに搭載され、Macの黄金期を支える。
  • 2000年代:
    • 任天堂のゲームキューブ (Gekko: G3ベース)、Wii (Broadway: G3ベースのカスタム)、Wii U (Espresso: カスタム) に採用。
    • ソニーのPlayStation 3 の Cell Broadband Engine の制御コア (PPE) として採用。
    • マイクロソフトの Xbox 360 (Xenon: 3コアのカスタムPowerPC) に採用。
    • 多くの組み込みシステム(ネットワーク機器、プリンタ、車載システムなど)で採用。Freescale (旧Motorolaセミコンダクタ部門) の PowerQUICC シリーズなどが有名。
  • 2003年: IBMがAppleと共同開発した64ビットプロセッサ「PowerPC 970 (G5)」が登場。Power Mac G5 に搭載される。
  • 2005-2006年: AppleがMacのCPUをPowerPCからIntel x86アーキテクチャへ移行することを発表・完了。
  • 2006年以降: Power ISAとして進化を継続。IBMのPOWERプロセッサ (Power Systemsサーバー等) はPower ISAに準拠。組み込み分野や特定用途(宇宙航空など)での利用は継続。
  • 2019年: IBMがPower ISAをオープンソース化し、Linux Foundation傘下のOpenPOWER Foundationに移管。

このように、PowerPCはパーソナルコンピュータからサーバー、ゲーム機、組み込みシステム、さらには宇宙開発まで、非常に幅広い分野で活躍してきたアーキテクチャなのです。✨

7. まとめ

今回は、PowerPCアセンブリ言語の入門として、PowerPCアーキテクチャの概要、基本的な命令、簡単なコード例、開発ツール、そしてその歴史について解説しました。

アセンブリ言語は、一見すると複雑でとっつきにくいかもしれません。しかし、CPUの動作原理を深く理解し、ハードウェアを直接操作する感覚は、他の言語ではなかなか味わえない魅力があります。特にPowerPCは、RISCアーキテクチャの代表例の一つであり、多くの歴史的な製品で使われてきた興味深い存在です。

この入門記事が、皆さんのPowerPCやアセンブリ言語への興味を深めるきっかけとなれば幸いです。さらに深く学びたい方は、参考文献などを参照して、実際にコードを書いて動かしてみることをお勧めします。Let’s feel the POWER! 💪

参考情報

より詳細な情報や学習リソースです。

(注意:リンク先は変更される可能性があります。)

コメント

タイトルとURLをコピーしました