この章では、64 ビット Solaris オペレーティングシステムについてさらに詳しく知りたいシステムプログラマ向けに、プログラミングに関するさまざまな情報を提供します。
64 ビット環境のほとんどの新機能は、一般の 32 ビットインタフェースを拡張したものですが、一部の新機能は 64 ビット環境に固有の機能です。
64 ビットアプリケーションは、ELF64 実行可能およびリンク形式 (Executable and Linking Format) によって作成されます。この形式によって、大規模なアプリケーションおよびアドレス空間を完全に記述することができます。
SPARCV9。『SPARC Compliance Definition, Version 2.4』には、SPARC V9 ABI の詳細が含まれます。このマニュアルでは 32 ビットの SPARC V8 ABI と 64 ビット SPARC V9 ABI について説明しています。このマニュアルは、SPARC International の www.sparc.com から入手できます。
次に SPARC V9 ABI の特徴を示します。
すべての 64 ビット SPARC 命令と 64 ビット幅のレジスタを最大限有効に活用できます。関連した新しい命令の多くは、既存の V8 命令セットの拡張版です。『The SPARC Architecture Manual, Version 9』を参照してください。
基本的な呼び出し規約は同じです。呼び出し側の最初の 6 つの引数は、出力レジスタの %o0-%o5 に格納されます。SPARC V9 ABI では、関数呼び出しの動作を「軽く」するために、従来より大きいレジスタファイル上で、従来どおりレジスタウィンドウを使用しています。結果は %o0 に格納されます。すべてのレジスタは 64 ビット量として扱われるので、64 ビットの値は、一組のレジスタにではなく 1 つのレジスタに渡されます。
スタックの配置が変わりました。基本セルサイズが 32 ビットから 64 ビットに拡大されました。さまざまな隠れたパラメータ語が削除されました。戻りアドレスは %o7 + 8 のままです。
%o6 は従来どおりスタックポインタレジスタ %sp として参照され、%i6 はフレームポインタレジスタ %fp として参照されます。ただし、%sp レジスタと %fp レジスタは、スタックバイアスと呼ばれる定数だけ、スタックの実際のメモリー位置からオフセットされます。スタックバイアスのサイズは 2047 バイトです。
命令長は従来どおり 32 ビットです。したがって、アドレス定数を生成するには通常以上の命令が必要となります。CALL 命令は、アドレス空間内への分岐には使用できなくなりました。CALL 命令は、%pc から + 2G バイトまたは - 2G バイト以内までしか到達できないからです。
整数乗算機能および除算機能は、現在完全にハードウェアで実装されています。
データ構造体を渡す方法と戻す方法は異なります。小さいデータ構造体と浮動小数点引数のいくつかは、現在はレジスタに直接渡されます。
ユーザートラップ機能により、ユーザートラップハンドラが (シグナルを発信する代わりに) 非特権コードからのトラップのいくつかを取り扱うことができるようになりました。
すべてのデータ型はそれぞれのサイズに境界整列されるようになりました。
基本派生型の多くは、従来よりサイズが大きくなりました。したがって、多くのシステムコールインタフェースのデータ構造体のサイズも変わっています。
2 つの異なるライブラリセット (32 ビット SPARC アプリケーション用のライブラリと 64 ビット SPARC アプリケーション用のライブラリ) が、システムに存在します。
SPARC V9。開発者にとって重要な SPARC V9 ABI の特徴の 1 つに、スタックバイアスがあります。64 ビット の SPARC プログラムでは、2047 バイトのスタックバイアスを、フレームポインタとスタックポインタの両方に追加して、スタックフレームの実際のデータを取得する必要があります。次の図を参照してください。
スタックバイアスについては、SPARC V9 ABI を参照してください。
SPARC V9。64 ビットアプリケーションのアドレス空間の配置は、32 ビットアプリケーションのアドレス空間の配置に密接に関係しています。ただし、開始アドレスとアドレス指定の制限値は大きく変更されています。SPARC V8 と同様に、SPARC V9 のスタックはアドレス空間の上端から下方に広がり、ヒープは下端から上方にデータセグメントを拡張します。
以下の図は、64 ビットアプリケーションに与えられたデフォルトのアドレス空間を示します。「予約済み」となっているアドレス空間の領域は、アプリケーションからマップすることはできません。これらの制約は、将来のシステムで緩和される可能性があります。
上図の実際のアドレスは、ある特定のマシンの特定の実装を示しており、説明のためにだけ掲載してあります。
デフォルトでは、64 ビットプログラムは開始アドレス 0x100000000 にリンクされます。プログラム全体は、テキスト、データ、ヒープ、スタック、および共有ライブラリを含めて 4G バイトを超えるアドレスに存在します。これは、64 ビットプログラムが正しいことを検証するのに役立ちます。たとえばプログラムが関連するポインタの上位 32 ビットを切り落としてしまうと、そのプログラムはアドレス空間の下方の 4G バイトの部分へアクセスしようとして失敗します。
64 ビットプログラムは 4G バイトを超える位置でリンクされますが、リンカーのマップファイルを使用し、コンパイラまたはリンカーに -M オプションを指定して、4G バイト未満の位置でリンクすることも可能です。4G バイト未満で 64 ビット SPARC プログラムをリンクするためのリンカーマップファイルは、/usr/lib/ld/sparcv9/map.below4G にあります。
詳細は、ld(1) のリンカーのマニュアルページを参照してください。
SPARC V9。コンパイラには、性能の向上や、64 ビット SPARC プログラムでのコードサイズを小さくするなど、さまざまな目的に合わせた各種のコードモデルがあります。コードモデルは以下の要素で決定します。
位置決め方法 (絶対コード、あるいは位置に依存しないコード)
コードサイズ (2G バイト未満)
位置 (下部、中央、アドレス空間内の任意位置)
外部オブジェクト参照モデル (スモールまたはラージ)
次の表は、64 ビット SPARC プログラムで使用できる各種コードモデルを示したものです。
表 6–1 コードモデルの説明 SPARC V9
コードモデル |
位置決め方法 |
コードサイズ |
位置 |
外部オブジェクト参照モデル |
---|---|---|---|---|
abs32 |
絶対 |
2G バイト未満 |
下部 (アドレス空間の下位 32 ビット) |
なし |
abs44 |
絶対 |
2G バイト未満 |
中央 (アドレス空間の下位 44 ビット) |
なし |
abs64 |
絶対 |
2G バイト未満 |
任意 |
なし |
pic |
位置に依存しないコード |
2G バイト未満 |
任意 |
スモール (1024 以下の外部オブジェクト) |
PIC |
位置に依存しないコード |
2G バイト未満 |
任意 |
ラージ (2**29 以下の外部オブジェクト) |
スモールコードモデルを使用すると、命令シーケンスを短くできる場合があります。絶対コード内で静的データ参照を行うのに必要な命令の数は、abs32 コードモデルの場合が最も少なく、abs64 が最も多く、abs44 がその中間になります。同様に、pic コードモデルは、PIC コードモデルよりも少ない命令で静的データ参照を行います。その結果、コードモデルが小さいほどコードサイズも小さくなり、ラージコードモデルのような、より完全な機能性を必要としないプログラムの性能が向上します。
使用するコードモデルを指定するには、-xcode=<model> コンパイラオプションを使用する必要があります。現在、コンパイラは 64 ビットオブジェクトに対し、デフォルトで abs64 モデルを使用します。コードは、abs44 コードモデルの使用により最適化できます。より少ない命令を使用して、現在の UltraSPARC プラットフォームがサポートする 44 ビットのアドレス空間を利用できます。
コードモデルについては、SPARC V9 ABI およびコンパイラのマニュアルを参照してください。
abs32 コードモデルでコンパイルしたプログラムは、-M /usr/lib/ld/sparcv9/map.below4G オプションを使用して、4G バイトよりも下方にリンクする必要があります。
64 ビットアプリケーションは、ELF64 実行可能およびリンク形式 (Executable and Linking Format) によって作成されます。この形式によって、大規模なアプリケーションおよびアドレス空間を完全に記述することができます。
次に AMD ABI の特徴を示します。
すべての 64 ビット命令と 64 ビットレジスタを最大限有効に活用できます。新しい命令の多くは、既存の i386 命令セットの単純な拡張版です。汎用レジスタは 16 あります。
基本的な関数呼び出し規約は、AMD ABI では異なります。引数はレジスタに格納されます。単純な整数の引数の場合、最初の引数から順に %rdi、%rsi、%rdx、%rcx、%r8、%r9 レジスタに格納されます。
スタックの配置は、AMD では多少異なります。具体的には、スタックは常に、CALL 命令の直前で 16 バイトの境界に整列されます。
命令長は従来どおり 32 ビットです。したがって、アドレス定数を生成するには通常以上の命令が必要となります。CALL 命令は、アドレス空間内への分岐には使用できなくなりました。CALL 命令は、%rip から + 2G バイトまたは - 2G バイト以内までしか到達できないからです。
整数乗算機能および除算機能は、現在完全にハードウェアで実装されています。
データ構造体を渡す方法と戻す方法は異なります。小さいデータ構造体と浮動小数点引数のいくつかは、現在はレジスタに直接渡されます。
より効率的な位置に依存しないコードを生成できる、PC 相対の新しいアドレス指定モードがあります。
すべてのデータ型はそれぞれのサイズに境界整列されるようになりました。
基本派生型の多くは、従来よりサイズが大きくなりました。したがって、多くのシステムコールインタフェースのデータ構造体のサイズも変わっています。
2 つの異なるライブラリセット (32 ビット i386 アプリケーション用のライブラリと 64 ビット amd64 アプリケーション用のライブラリ) が、システムに存在します。
浮動小数点の機能が大幅に拡張されています。
amd64 psABI 草案文書『System V Application Binary Interface, AMD64 Architecture Processor Supplement』(草案バージョン 0.92、2004 年 9 月 9 日) を参照してください。
64 ビットアプリケーションのアドレス空間の配置は、32 ビットアプリケーションのアドレス空間の配置に密接に関係しています。ただし、開始アドレスとアドレス指定の制限値は大きく変更されています。SPARC V9 と同様に、amd64 のスタックはアドレス空間の上端から下方に広がり、ヒープは下端から上方にデータセグメントを拡張します。
以下の図は、64 ビットアプリケーションに与えられたデフォルトのアドレス空間を示します。「予約済み」となっているアドレス空間の領域は、アプリケーションからマップすることはできません。これらの制約は、将来のシステムで緩和される可能性があります。
上図の実際のアドレスは、ある特定のマシンの特定の実装を示しており、説明のためにだけ掲載してあります。
データ構造体の 32 ビット long long 要素の整列に関連してもう一つの問題があります。すなわち、i386 アプリケーションは 32 ビット境界で long long 要素を整列するだけですが、amd64 ABI は long long 要素を 64 ビット境界に配置するので、データ構造体に大きな隙間が生じる可能性があります。SPARC は 32 ビットと 64 ビットの両方で long long 項目が 64 ビット境界に整列されており、この点が異なります。
次の表は、設計アーキテクチャに対するデータ型整列を示します。
表 6–2 データ型整列
アーキテクチャ |
long long |
double |
long double |
---|---|---|---|
i386 |
4 |
4 |
4 |
amd64 |
8 |
8 |
16 |
sparcv8 |
8 |
8 |
8 |
sparcv9 |
8 |
8 |
16 |
SPARC システムで LP64 に対し問題がないと思われるコードであっても、整列の違いのために、32 ビットと 64 ビットのプログラミング環境間でデータ構造体をコピーする際に問題が生じる可能性があります。そのようなプログラミング環境には、デバイスドライバ ioctl ルーチン、doors ルーチンや、その他の IPC メカニズムがあります。整列の問題は、これらのインタフェースを慎重にコーディングしたり、#pragma pack または _Pack 指示語を適切に使用することによって回避できます。
次に示すプロセス間通信 (IPC) プリミティブは、従来どおり 64 ビットプロセスと 32 ビットプロセスとの間で動作します。
System V IPC プリミティブ (shmop(2)、semop(2)、 msgsnd(2) など)
共有ファイル上の mmap(2) への呼び出し
プロセス間の pipe(2) の使用
プロセス間の door_call(3DOOR) の使用
xdr(3NSL) に説明されている外部データ表現を使用した、同一マシン上あるいは異なるマシン上でのプロセス間の rpc(3NSL) の使用
これらのすべてのプリミティブは、32 ビットプロセスと 64 ビットプロセスとの間の通信を可能にしますが、プロセス間で交換されているデータがそれらすべてのプロセスで正しく解釈されることを、明確な手順によって確認する必要がある場合があります。たとえば、long
型の変数を含む C データ構造体で記述されるデータを 2 つのプロセスが実際に共有するには、32 ビットプロセスがこの変数を 4 バイト量とみなし、64 ビットプロセスはこの変数を 8 バイト量とみなすということを認識する必要があります。
この相違を取り扱う 1 つの方法は、両プロセス間で意味をなすようにデータが完全に同じサイズであることを保障することです。int32_t
や int64_t
のような固定幅型を使ってデータ構造を構成してください。整列に関しても配慮が必要です。共有されるデータ構造体は、パディングを追加したり、#pragma pack、_Pack などのコンパイラ指示語を使用して再パックする必要のある場合があります。「整列の問題」を参照してください。
システムで提供される派生型に対応する派生型の一群が <sys/types32.h> にあります。これらの派生型は、32 ビットシステムの基本型と同じ符号、同じサイズですが、ILP32 および LP64 のコンパイル環境でサイズが変わらないように定義されています。
32 ビットプロセスと 64 ビットプロセスとの間でポインタを共有するのは、さらに困難です。まずポインタのサイズが異なるということがあります。またそれ以上に重要なことは、既存の C の使用法に 64 ビット整数 (long long
) はありますが、64 ビットポインタには 32 ビット環境に相当するものはない、ということです。64 ビットプロセスが 32 ビットプロセスとデータを共有できるようにするため、32 ビットプロセスは共有データのうち、4G バイトまでしか一度に「見る」ことはできません。
XDR ルーチンの xdr_long(3NSL) は問題と思われるかもしれません。しかし、これは既存のプロトコルとの互換性を持たせるために従来どおり 32 ビットとして取り扱われます。64 ビットバージョンのルーチンが 32 ビットに格納できない long
値をコード化するように要求された場合、そのコード化処理は失敗します。
64 ビットバイナリは、ELF64 形式でファイルに格納されます。この ELF64 形式は、ほとんどのフィールドが完全 64 ビットアプリケーションを格納するために拡張されていることを除いて、ELF32 形式に類似しています。ELF64 ファイルは elf(3ELF) API、たとえば elf_getarhd(3ELF) を使って読むことができます。
ELF ライブラリ elf(3ELF) の 32 ビットおよび 64 ビットのバージョンは、それぞれ ELF32 および ELF64 形式と、対応する API をサポートします。これによりアプリケーションは、32 ビットシステムまたは 64 ビットシステム (64 ビットプログラムを実行するには 64 ビットシステムが必要) から、両ファイル形式を構築、読み込み、あるいは修正ができるようになります。
さらに、Solaris では GELF (Generic ELF) インタフェースを提供し、プログラマが 1 つの共通 API を使用して両方の ELF 形式を操作できるようにしています。詳細は、elf(3ELF) のマニュアルページを参照してください。
ar(1)、nm(1)、ld(1)、および dump(1) を含む、すべてのシステム ELF ユーティリティが両方の ELF 形式を使用できるように変更されています。
/proc インタフェースは、32 ビットアプリケーションおよび 64 ビットアプリケーションの両方で利用できます。32 ビットアプリケーションは、他の 32 ビットアプリケーションの状態を調べたり制御したりできます。したがって、既存の 32 ビットデバッガを 32 ビットアプリケーションのデバッグに使用できます。
64 ビットアプリケーションは、他の 32 ビットまたは 64 ビットアプリケーションの状態を調べたり制御したりできます。ただし 32 ビットアプリケーションでは、64 ビットアプリケーションを制御できません。これは、32 ビット API では 64 ビットプロセスの完全な状態を記述することができないからです。このため、64 ビットアプリケーションをデバッグするには、64 ビットのデバッガが必要となります。
Solaris S10 オペレーティングシステムの新しい sysinfo(2) サブコードによって、アプリケーションは使用可能な命令セットアーキテクチャに関する詳細な情報を確認できます。
たとえば、SI_ARCHITECTURE_64 サブコードを使用することによって、システム上に 64 ビット ABI があればその名前がわかります。詳細は、sysinfo(2) を参照してください。
64 ビットの Solaris システムは、64 ビットカーネルを使って実装されています。カーネルの内容を直接調べたり変更するアプリケーションは、64 ビットアプリケーションに変換し、64 ビットバージョンのライブラリとリンクしなければなりません。
このような変換と修正を行う前に、まずアプリケーションがカーネルのデータ構造を直接知る必要があるかどうかを検討した方がよいでしょう。プログラムが最初に移植されるかあるいは新規に作成された後に、システムコールを使って必要なデータを抽出するインタフェースが Solaris プラットフォームで利用可能になって追加された、という可能性があります。この場合は、最も一般的な代替 API として sysinfo(2)、kstat(3KSTAT)、sysconf(3C)、proc(4) を参照してください。これらのインタフェースが kvm_open(3KVM) の代わりに使用できるのなら、移植性を最大限にするために、それらを使用してアプリケーションを 32 ビットのままにしてください。さらに利点として、これらの API のほとんどは処理が速く、カーネルメモリーにアクセスするときと同じセキュリティ特権を必要としないことがあります。
32 ビットバージョンの libkvm は、64 ビットのカーネルクラッシュダンプに対して kvm_open(3KVM) を使用しようとしたときに異常終了します。同様に、64 ビットバージョンの libkvm は、32 ビットのカーネルクラッシュダンプに対して kvm_open(3KVM) を使用しようとしたときに異常終了します。
カーネルは 64 ビットプログラムなので、カーネルのシンボルテーブルを直接調べるために /dev/ksyms を開くアプリケーションは、ELF64 形式を理解するように機能を拡張する必要があります。
kvm_read() または kvm_write() へのアドレス引数がカーネルアドレスであるかユーザーアドレスであるかが曖昧であることは、64 ビットアプリケーションおよびカーネルではさらに問題となります。現在でもまだ kvm_read() と kvm_write() を使用している libkvm を利用するアプリケーションはすべて、kvm_read()、kvm_write()、kvm_uread()、kvm_uwrite() のルーチンを使用するようにする必要があります。これらのルーチンは、Solaris 2.5 から利用できるようになっています。
/dev/kmem または /dev/mem を直接読むアプリケーションは、従来どおり実行できます。ただし、これらのデバイスから読み込んだデータを解釈しようとすると、問題が発生することがあります。これは、データ構造体のオフセットおよびサイズが 32 ビットと 64 ビットのカーネル間で確実に異なるためです。
多くのカーネル統計情報のサイズは、カーネルが 64 ビットあるいは 32 ビットプログラムのどちらであるかということとは関係ありません。名前付き kstat (kstat(3KSTAT) のマニュアルページを参照) がエクスポートするデータ型は自己記述型で、符号付きまたは符号なしの 32 ビットまたは 64 ビットカウンタデータを、適切なタグを付けてエクスポートします。したがって、libkstat を使用するアプリケーションは、64 ビットカーネル上で正常に動作させるために 64 ビットアプリケーションに変換する必要はありません。
名前付き kstats を作成および管理するデバイスドライバを修正するときは、エクスポートしようとする統計情報のサイズを、固定幅の統計データ型を使って 32 ビットおよび 64 ビットカーネル間で不変にすることをお薦めします。
64 ビット環境では stdio 機能が拡張されて、256 を超える数のストリームを同時に開くことができるようになりました。32 ビットの stdio 機能では、従来どおり 256 を超える数のストリームを同時に開くことはできないという制限があります。
64 ビットアプリケーションが、FILE データ構造体のメンバーにアクセスできることに依存しないようにしてください。実装に固有な構造体メンバーに直接アクセスしようとすると、コンパイルエラーとなります。この変更で既存の 32 ビットアプリケーションが影響を受けることはありませんが、このように構造体のメンバーを直接使用する方法は、すべてのコードから取り除くべきです。
FILE 構造体には長い歴史があり、この構造体の中身を参照してストリームの状態に関する付加的な情報を収集するアプリケーションもあります。64 ビットのこの構造体は参照できないようになっているため、新しいルーチン群が 32 ビットの libc と 64 ビットの libc に加えられ、その結果、実装の内部に依存することなく同じ状態を調べることができるようになりました。たとえば __fbufsize(3C) のマニュアルページを参照してください。
64 ビットのパフォーマンスの長所および短所について説明します。
64 ビットに対する算術演算および論理演算がより効率的である
演算に、全レジスタ幅、全レジスタセット、および新しい命令が使用される
64 ビット量のパラメータ渡しがより効率的である
小さなデータ構造体および浮動小数点のパラメータ渡しがより効率的である
整数レジスタおよび浮動小数点レジスタが追加されている
amd64 の場合、より効率的な位置に依存しないコードのための、PC 相対のアドレス指定モードがある
より大きいレジスタを格納するためにより大きなスタック空間を必要とする
より大きなポインタによってより大きなキャッシュサイズを使用する
32 ビットのプラットフォームでは実行できない
システムコールの問題について説明します。
戻り値の EOVERFLOW は、カーネルからの情報を渡すために使うデータ構造体の 1 つまたは複数のフィールドが小さすぎて値を格納できない場合に、常にシステムコールから返されます。
現在、64 ビットカーネル上の大きなオブジェクトに遭遇したとき、多くの 32 ビットシステムコールは EOVERFLOW を返します。これまでも、大規模ファイルを扱う場合には同様でしたが、daddr_t
、dev_t
、time_t
、およびその派生型の struct timeval
と timespec_t
が現在では 64 ビットを格納するため、32 ビットアプリケーションにおいては、従来よりも EOVERFLOW が返される場合が増えます。
一部の ioctl(2) 呼び出しは、これまでうまく指定されていませんでした。ioctl() はコンパイル時の型検査では検出されません。そのため、ioctl() は追跡が困難なバグの原因になる可能性があります。
2 つの ioctl() 呼び出しを考えてみてください。一方は 32 ビット量 (IOP32) へのポインタを操作し、もう一方は long
(IOPLONG) へのポインタを操作します。
次のコード例は、32 ビットアプリケーションの一部として動作します。
int a, d; long b; ... if (ioctl(d, IOP32, &b) == -1) return (errno); if (ioctl(d, IOPLONG, &a) == -1) return (errno);
このコードが 32 ビットアプリケーションの一部としてコンパイルされ、実行されるとき、どちらの ioctl(2) 呼び出しも正しく動作します。
このコードが 64 ビットアプリケーションとしてコンパイルされ、実行されるとき、どちらの ioctl() 呼び出しも正常終了しますが、正しく動作しません。最初の ioctl() は、大きすぎるコンテナ (データを格納するメモリー領域) を渡します。その結果、ビッグエンディアン実装の場合は、カーネルは 64 ビットワードの誤った部分へ、あるいは誤った部分からコピーしようとします。リトルエンディアン実装の場合でも、コンテナには上位 32 ビットに意味のない値が含まれます。2 番目の ioctl() は、コピー量が多すぎるため、正しくない値を読み込むか、あるいはユーザースタック内の隣接する変数を破壊してしまいます。