SPARCアセンブリ入門

RISCアーキテクチャの代表格、SPARCの世界へようこそ!

はじめに

コンピュータの心臓部であるCPUは、マシン語と呼ばれる低水準な命令を解釈して動作します。しかし、人間がマシン語を直接読み書きするのは非常に困難です。そこで登場するのがアセンブリ言語です。アセンブリ言語は、マシン語の命令とほぼ一対一に対応するニーモニック(覚えやすい記号)を使ってプログラムを記述する言語です。

この記事では、数あるCPUアーキテクチャの中でも、特にRISC(Reduced Instruction Set Computer)設計思想の先駆けとして知られるSPARC (Scalable Processor ARChitecture) のアセンブリ言語について、基礎から解説していきます。SPARCは、かつてSun Microsystems社(後にOracle社が買収)のワークステーションやサーバーで広く使われ、その後のプロセッサ設計に大きな影響を与えました。現在では、富士通株式会社などが開発を継続しているスーパーコンピュータ「富岳」のCPU (A64FX) の源流の一つとも言えます(ただしA64FXはARMアーキテクチャベースですが、開発経験は活かされています)。

低レイヤーの動作を理解したい方、特定のSPARCベースシステムを扱う必要がある方、あるいは単にコンピュータアーキテクチャの多様性に興味がある方にとって、この記事がSPARCアセンブリへの第一歩となることを願っています。

SPARCアーキテクチャとは?

SPARCは、1980年代半ばにSun Microsystems社によって開発が開始されたマイクロプロセッサアーキテクチャです。カリフォルニア大学バークレー校のRISC I/IIプロジェクトの影響を強く受けており、以下のようないくつかの重要な特徴を持っています。

主な特徴:

  • RISC設計: 命令セットを単純化し、各命令が基本的に1クロックサイクルで実行できるように設計されています。これにより、パイプライン処理効率を高め、高速化を図っています。
  • レジスタ・ウィンドウ (Register Windows): 関数呼び出し時のレジスタ退避・復元のオーバーヘッドを削減するための独創的な仕組みです。後ほど詳しく解説します。
  • 遅延分岐 (Delayed Branch): 分岐命令の直後の命令(遅延スロット)は、分岐するかどうかに関わらず実行されるという特徴があります。これにより、パイプラインの乱れを最小限に抑えようとしました。(近年の高性能プロセッサでは分岐予測技術が高度化したため、この方式のメリットは相対的に薄れています。)
  • 32ビット / 64ビット: 当初は32ビットアーキテクチャ (SPARC V7, V8) でしたが、後に64ビットに拡張されました (SPARC V9)。
  • オープンアーキテクチャ: SPARC仕様は公開されており、複数のベンダーがSPARCプロセッサを製造・販売していました。

SPARCは、Sunのワークステーション (SPARCstationシリーズなど) やサーバー (Sun Enterpriseシリーズなど) で長年にわたり採用され、科学技術計算、エンタープライズシステム、通信機器など幅広い分野で利用されました。Oracle社によるSun買収(2010年完了)後も開発は続けられましたが、2017年頃に発表されたSPARC M8世代を最後に、Oracle社による新規開発は終了したとされています。一方で、富士通はSPARC64シリーズとして独自に開発を続け、高性能サーバーやスーパーコンピュータ向けに提供してきました。

SPARCのレジスタ

アセンブリプログラミングにおいて、レジスタの理解は不可欠です。レジスタはCPU内部にある高速な記憶領域で、計算対象のデータやメモリアドレスなどを一時的に保持します。SPARCアーキテクチャ(特にSPARC V8/V9)には、いくつかの種類のレジスタがあります。

汎用レジスタ

SPARCは多数の汎用レジスタを持っていますが、一度に見えるのは32個です。これらのレジスタは、その役割に応じて論理的にグループ分けされています。

レジスタ名 個数 説明
%g0%g7 8個 グローバルレジスタ (Global Registers)。関数呼び出しを通じて常に同じ内容を保持します。%g0 は常に0を保持し、書き込みは無視されます(ゼロレジスタ)。
%o0%o7 8個 出力レジスタ (Output Registers)。関数呼び出し時に引数を渡したり、戻り値を返したりするのに使われます。呼び出された関数から見ると、これらは入力レジスタ (%i) になります。%o6 はスタックポインタ (%sp) として使われます。%o7call命令実行時の戻りアドレスを格納します。
%l0%l7 8個 ローカルレジスタ (Local Registers)。現在の関数内でのみ使用される一時的な値を格納します。関数呼び出し時には内容は保証されません。
%i0%i7 8個 入力レジスタ (Input Registers)。呼び出し元の関数が %o レジスタに設定した引数を受け取ります。%i6 はフレームポインタ (%fp) として使われます。%i7 は呼び出し元への戻りアドレスを保持します。

レジスタ・ウィンドウ

SPARCの最も特徴的な機能の一つがレジスタ・ウィンドウです。CPU内部には実際には32個よりも多くの汎用レジスタが存在し(例えば64個から520個など実装による)、それらが「ウィンドウ」として切り替えられて使用されます。

関数を呼び出す際、save 命令が実行されると、現在のレジスタウィンドウが一つ「回転」します。

  • 現在のウィンドウの %o レジスタが、新しいウィンドウの %i レジスタになります。
  • 現在のウィンドウの %l レジスタは退避され、新しいウィンドウでは新しい %l レジスタが使えます。
  • 現在のウィンドウの %i レジスタはアクセスできなくなります(退避されている状態)。
  • %g レジスタはウィンドウの影響を受けず、常に同じものが使われます。

関数から戻る際には restore 命令が使われ、ウィンドウが逆方向に回転し、元のレジスタセットが復元されます。これにより、関数呼び出し時のレジスタの退避・復元に必要なメモリアクセスを大幅に削減できます。ただし、ウィンドウの数が限られているため、深い関数呼び出しが発生するとウィンドウが溢れ(オーバーフロー)、OSが介入してレジスタ内容をメモリに退避させる必要があります。逆に関数が次々に戻ってくるとウィンドウが空になり(アンダーフロー)、OSがメモリから内容を復元します。

ウィンドウの概念図 (イメージ):

+-------------------+       save      +-------------------+
| Global (%g0-%g7)  | <------------> | Global (%g0-%g7)  |
+-------------------+                 +-------------------+
| Out    (%o0-%o7)  | ------------>  | In     (%i0-%i7)  | \
+-------------------+                 +-------------------+  |  新しいウィンドウ
| Local  (%l0-%l7)  | --(退避)-->    | Local  (%l0-%l7)  |  | (New Window)
+-------------------+                 +-------------------+  /
| In     (%i0-%i7)  | --(アクセス不可)-> | Out    (%o0-%o7)  | /
+-------------------+                 +-------------------+
  古いウィンドウ                        (New %o regs)
 (Old Window)                       restore は逆方向
        

特殊レジスタ

汎用レジスタ以外にも、プロセッサの状態や制御に使われる特殊なレジスタがあります。

  • %psr (Processor State Register): プロセッサの現在の状態(割り込み許可/禁止、現在のウィンドウ番号(CWP)、条件コードなど)を保持します。
  • %pc (Program Counter): 次に実行される命令のアドレスを指します。
  • %npc (Next Program Counter): 次の次に実行される命令のアドレスを指します。遅延分岐のために重要です。分岐命令自体が実行された後、%npc の値が %pc にコピーされます。
  • %y: 乗算・除算命令で使用されるレジスタ。
  • %wim (Window Invalid Mask): レジスタウィンドウのオーバーフロー/アンダーフロー検出に使われます。
  • %tbr (Trap Base Register): トラップ(例外や割り込み)発生時にジャンプする先のハンドラアドレスの基点を保持します。

基本的なSPARCアセンブリ命令

SPARCアセンブリの命令は、比較的シンプルで規則的な形式を持っています。ここでは代表的な命令の種類と例をいくつか紹介します。命令のオペランドは通常、命令 元, 元, 先 または 命令 元, 即値, 先 のように、ソースオペランドが先に来て、デスティネーションオペランドが最後に来ます。

データ転送命令 (Load/Store)

メモリとレジスタ間でデータをやり取りする命令です。SPARCは典型的なLoad/Storeアーキテクチャであり、メモリ操作はこれらの命令でのみ行い、算術演算などはレジスタ間で行います。

  • ld [アドレス], %レジスタ: メモリからレジスタへロード (Load Word: 4バイト)。アドレスは [%レジスタ + %レジスタ][%レジスタ + 即値] の形式で指定します。
    • ld [%o0 + %o1], %l1: %o0 + %o1 のアドレスから4バイト読み込み %l1 へ格納。
    • ld [%fp + 68], %l2: フレームポインタ+68バイトのアドレスから4バイト読み込み %l2 へ格納。
    • 他にも ldub (Load Unsigned Byte), ldsh (Load Signed Halfword) など、サイズや符号拡張を指定するバリエーションがあります。
  • st %レジスタ, [アドレス]: レジスタからメモリへストア (Store Word: 4バイト)。
    • st %l1, [%o0 + %o1]: %l1 の内容を %o0 + %o1 のアドレスへ4バイト書き込み。
    • st %l2, [%fp + 68]: %l2 の内容を フレームポインタ+68バイトのアドレスへ4バイト書き込み。
    • 同様に stb (Store Byte), sth (Store Halfword) などがあります。

算術演算命令

加算、減算などの計算を行います。

  • add %src1, src2, %dst: %dst = %src1 + src2src2 はレジスタ (%reg) または即値 (imm)。
    • add %l0, %l1, %l2: %l2 = %l0 + %l1
    • add %o0, 10, %o0: %o0 = %o0 + 10
  • sub %src1, src2, %dst: %dst = %src1 - src2
    • sub %l0, %l1, %l2: %l2 = %l0 - %l1
  • addcc, subcc: 演算結果に応じて条件コード (%psr 内のフラグ) をセットするバージョン。条件分岐で使われます。
  • 乗算 (mulscc) や除算 (udiv, sdiv) 命令もありますが、少し複雑です。

論理演算命令

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

  • and %src1, src2, %dst: %dst = %src1 & src2
  • or %src1, src2, %dst: %dst = %src1 | src2
  • xor %src1, src2, %dst: %dst = %src1 ^ src2
  • andcc, orcc, xorcc: 条件コードをセットするバージョン。orcc %g0, %reg, %g0 はレジスタの内容が0かどうかのテストによく使われます(結果がゼロならZフラグが立つ)。

シフト命令

ビットを左右にシフトします。

  • sll %src1, amount, %dst: 論理左シフト (Shift Left Logical)。amount はレジスタまたは即値。
  • srl %src1, amount, %dst: 論理右シフト (Shift Right Logical)。
  • sra %src1, amount, %dst: 算術右シフト (Shift Right Arithmetic)。符号ビットを維持します。

制御転送命令 (分岐・ジャンプ・呼び出し)

プログラムの実行フローを変更します。

  • sethi 定数, %レジスタ: 定数の上位22ビットをレジスタの上位22ビットにセットし、下位10ビットを0にする (Set High)。32ビット定数をレジスタにロードするために or 命令と組み合わせて使われます。
    • 例: 0x12345678%l0 にロードする場合
      sethi %hi(0x12345678), %l0  ! %l0 = 0x12345400
      or    %l0, %lo(0x12345678), %l0  ! %l0 = 0x12345400 | 0x278 = 0x12345678
  • b ラベル: 条件付き分岐 (Branch on Condition)。直前の cc 付き命令の結果(条件コード)に基づいて分岐します。 には a (Always), e (Equal/Zero), ne (Not Equal/Non-Zero), g (Greater), le (Less or Equal) などが入ります。
    • cmp %l0, %l1 (これは subcc %l0, %l1, %g0 の疑似命令)
    • be equal_label: 等しければ equal_label へ分岐
    • bne not_equal_label: 等しくなければ not_equal_label へ分岐
    • ba always_label: 無条件分岐
    • 注意: SPARCの分岐命令には通常、遅延スロット (Delay Slot) があります。分岐命令の直後にある命令は、分岐が発生するかどうかに関わらず実行されます。このスロットを有効活用(例えば、分岐先で必要な処理の一部をここに入れる)するか、何もさせたくない場合は nop (No Operation) 命令を入れます。アセンブラが自動で nop を補完してくれる場合もあります (-xO オプションなどで最適化した場合など)。
    • b,a ラベル: Annul (無効化) ビット付き分岐。条件が満たされない場合、遅延スロットの命令を実行しません。
  • call ラベル: 関数(サブルーチン)を呼び出します。戻りアドレスが %o7 に格納されます。これも遅延スロットを持ちます。
  • jmpl %アドレスレジスタ + オフセット, %戻り先レジスタ: レジスタの内容に基づいてジャンプ (Jump and Link)。関数からのリターンによく使われます。
    • ret: jmpl %i7 + 8, %g0 の疑似命令。呼び出し元に戻る。
    • retl: jmpl %o7 + 8, %g0 の疑似命令。リーフ関数(自身は他の関数を呼ばない関数)からのリターン。
  • save %sp, -サイズ, %sp: 新しいスタックフレームを作成し、レジスタウィンドウを回転させます。関数プロローグで使われます。
  • restore: スタックフレームを破棄し、レジスタウィンドウを元に戻します。関数エピローグで使われます。restore %g0, %g0, %g0 のように書かれることが多いです。
  • nop: 何もしない命令 (No Operation)。遅延スロットを埋めるためなどに使われます。実際には sethi 0, %g0 などで実装されます。

言葉だけでは分かりにくいので、簡単な例を見てみましょう。ここでは、2つの引数を受け取り、その合計を返す単純な関数 add_func をSPARCアセンブリで書いてみます。

C言語でのイメージ:


int add_func(int a, int b) {
  int sum = a + b;
  return sum;
}
      

SPARCアセンブリ (SPARC V8/V9 ABI):


        .section ".text"         ! コードセクション
        .global add_func       ! グローバルシンボルとして公開
        .align 4               ! 4バイト境界に配置

add_func:
        ! 関数プロローグ (この単純な例では不要だが、一般的にはsaveを使う)
        ! save %sp, -96, %sp   ! 例: 96バイトのスタックフレーム確保とウィンドウ回転

        ! 引数は %o0 (a) と %o1 (b) に入ってくる
        ! 呼び出された側からは %i0, %i1 として見える
        ! (save を使った場合は %i0, %i1 になるが、この例では save しないので %o0, %o1 のまま使う)

        add %o0, %o1, %o0     ! %o0 = %o0 + %o1 (結果を %o0 に格納)

        ! 関数エピローグ
        retl                   ! リーフ関数からのリターン (jmpl %o7 + 8, %g0)
        nop                    ! 遅延スロット (retl の場合は必須ではないことが多いが念のため)

        ! (save を使った場合は ret; restore となる)
        ! ret                  ! 通常のリターン (jmpl %i7 + 8, %g0)
        ! restore              ! ウィンドウを戻し、スタックフレームを解放 (遅延スロット内)
      

呼び出し規約 (Calling Convention)

複数の関数が正しく連携するためには、引数の渡し方、戻り値の返し方、レジスタの利用ルールなどを定めた呼び出し規約 (Calling Convention) に従う必要があります。SPARCでは、System V ABI (Application Binary Interface) の一部として標準的な規約が定められています。

SPARC V8/V9 ABIの主なルール:

  • 引数渡し:
    • 最初の6つの整数/ポインタ引数は、レジスタ %o0 から %o5 に順番に格納されます。
    • 7つ目以降の引数は、スタックに積まれます(通常、[%sp + 92] から順に積まれます)。
    • 浮動小数点引数は、浮動小数点レジスタ (%f0, %f1, …) を使います (ここでは詳細略)。
  • 戻り値:
    • 整数やポインタの戻り値は、%o0 に格納されます。64ビットアーキテクチャ (V9) で64ビットより大きな構造体を返す場合などは、別の方法が取られます。
    • 浮動小数点数の戻り値は、%f0 などを使います。
  • レジスタの保存:
    • %g レジスタ (%g0除く) と %o レジスタ (%o6=%sp, %o7除く) は、関数呼び出しによって内容が破壊される可能性があります (Caller-saved または Scratch registers)。呼び出し側が必要なら、呼び出す前に退避する必要があります。
    • %l レジスタと %i レジスタ (%i6=%fp, %i7除く) は、関数内で自由に使えますが、その関数が他の関数を呼び出す場合には退避が必要になるかもしれません。関数から戻る際には、呼び出された時点での内容が復元されている必要があります (Callee-saved)。ただし、レジスタウィンドウ機構により、save/restore で自動的に退避・復元されるため、通常は個別にメモリに退避する必要はありません。
    • %sp (%o6) と %fp (%i6) は特定の役割を持つため、規約に従って管理する必要があります。
  • スタックフレーム:
    • 関数は通常、save 命令で自身のスタックフレームを確保します。スタックは高位アドレスから低位アドレスに向かって伸長します。
    • スタックフレームには、ローカル変数、退避するレジスタ、7つ目以降の引数などが格納されます。
    • %fp (%i6) は、前のフレームを指すフレームポインタとして機能し、デバッグなどに役立ちます。save命令は、古い%spを新しい%fpに自動的にコピーします。
    • 最小スタックフレームサイズ (SPARC V9 ABIでは通常96バイトなど) が定められています。これには引数退避領域やレジスタ退避領域などが含まれます。
    • 
        高位アドレス
      +-------------------+ <-- [%fp + オフセット] (前のフレームの引数など)
      | ...               |
      +-------------------+ <-- %fp (前の %sp)
      | ローカル変数      |
      | レジスタ退避領域  |
      | ...               |
      +-------------------+ <-- %sp + 92 (7番目の引数)
      | 引数退避領域(6個分)|
      +-------------------+ <-- %sp + 68 (構造体戻り値ポインタ用)
      | ... (Hidden param)|
      +-------------------+ <-- %sp (現在のスタックトップ)
        低位アドレス
                    

これらの規約を理解し、遵守することで、アセンブリ言語で書かれたコードと、C言語などの高水準言語で書かれたコードとを相互に呼び出すことが可能になります。

SPARCの現在と歴史的意義

SPARCアーキテクチャは、1980年代後半から2000年代にかけて、特にUNIXワークステーションやサーバー市場で大きな成功を収めました。Sun Microsystemsの主力製品ラインを支え、Solaris OSと共に、多くの企業や研究機関で利用されました。

しかし、x86アーキテクチャ(IntelやAMD)の性能向上と低価格化、そして近年ではARMアーキテクチャの台頭により、SPARCの市場シェアは徐々に縮小していきました。OracleによるSun買収後、SPARC/Solarisの開発は継続されましたが、前述の通り、Oracleによる新規SPARCプロセッサ開発は2017年頃に終了しました。

一方で、富士通はSPARC64アーキテクチャの開発を続け、メニーコア化や高性能化を進めてきました。特に、スーパーコンピュータ分野では、理化学研究所の「京」コンピュータ(SPARC64 VIIIfx搭載、2011年稼働開始)や、その後継機である「富岳」(ARMベースのA64FX搭載、2020年頃から稼働)の開発において、SPARCで培われた技術や経験が活かされています。ただし、「富岳」のCPUコア自体はARMアーキテクチャを採用しており、SPARCからの直接的な移行ではありません。富士通のSPARCサーバー製品も、現在ではメインフレームからの移行先など、特定の需要に応える形での提供が中心となっています。

SPARCの歴史的意義は大きいです。RISC設計思想を早期に採用し、商用的に成功させたこと、レジスタ・ウィンドウのようなユニークな機構を導入したこと、オープンアーキテクチャとして複数のベンダーによる競争を促したことなどが挙げられます。現代のプロセッサ設計においても、SPARCの試みから得られた教訓(例えば、遅延分岐の有効性やレジスタ・ウィンドウの限界など)は参考にされています。

現在、SPARCアセンブリを積極的に学ぶ必要性は限定的かもしれませんが、コンピュータアーキテクチャの進化を理解する上で、また特定のレガシーシステムや組み込みシステムを扱う上で、その知識は依然として価値を持ちます。

まとめ

この記事では、SPARCアセンブリ言語の入門として、以下の内容を解説しました。

  • SPARCアーキテクチャの概要と特徴(RISC, レジスタ・ウィンドウ)
  • 汎用レジスタ、特殊レジスタ、レジスタ・ウィンドウの仕組み
  • 基本的なSPARC命令(Load/Store, 演算, 論理, シフト, 制御転送)
  • 簡単なコード例(引数を受け取り合計を返す関数)
  • 呼び出し規約の概要(引数、戻り値、レジスタ保存、スタックフレーム)
  • SPARCの歴史と現在の状況

SPARCアセンブリは、レジスタ・ウィンドウや遅延分岐といった特徴的な要素を持ち、低レイヤーでのプロセッサ動作を深く理解するための良い題材となります。この記事が、SPARCやコンピュータアーキテクチャへの興味を深める一助となれば幸いです。

さらに深く学ぶためには、SPARCアーキテクチャのマニュアルや、Solaris/Linux上でのアセンブリプログラミングに関するドキュメントを参照することをお勧めします。

参考情報

より詳細な情報については、以下の資料などが参考になります。

  • The SPARC Architecture Manual, Version 8: SPARC V8アーキテクチャの公式仕様書。 (注: 現在、公式の配布元からの入手が困難な場合があります。Web検索などで見つける必要があるかもしれません。)
  • SPARC V9 Architecture Manual: SPARC V9 (64ビット) アーキテクチャの仕様書。 (注: 同上)
  • System V Application Binary Interface – SPARC Processor Supplement: SPARCにおける呼び出し規約などのABI仕様。 (注: 同上。検索エンジンで “SPARC System V ABI” などで検索すると、関連ドキュメントが見つかることがあります。)

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です