この章では、64 ビット Solaris オペレーティング環境についてさらに詳しく知りたいシステムプログラマ向けに、プログラミングに関するさまざまな情報を提供します。
64 ビット環境のほとんどの新機能は、一般の 32 ビットインタフェースを拡張したものですが、一部の新機能は 64 ビット環境に固有の機能です。
64 ビットアプリケーションは、ELF64 実行可能およびリンク形式 (Executable and Linking Format) によって作成されます。この形式によって、大規模なアプリケーションおよびアドレス空間を完全に記述することができます。
『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 命令セットの拡張版です。『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 ABI の特徴の 1 つに、スタックバイアスがあります。64 ビットの SPARC プログラムでは、2047 バイトのスタックバイアスを、フレームポインタとスタックポインタの両方に追加して、スタックフレームの実際のデータを取得する必要があります。以下の図を参照してください。
スタックバイアスについては、SPARC V9 ABI を参照してください。
64 ビットアプリケーションのアドレス空間の配置は、32 ビットアプリケーションのアドレス空間の配置に密接に関係しています。ただし、開始アドレスとアドレス指定の制限値は大きく変更されています。SPARC V8 と同様に、SPARC V9 のスタックはアドレス空間の上端から下方に広がり、ヒープは下端から上方にデータセグメントを拡張します。
以下の図は、64 ビットアプリケーションに与えられたデフォルトのアドレス空間を示します。「予約済み」となっているアドレス空間の領域は、アプリケーションからマップすることはできません。これらの制約は、将来のシステムで緩和される可能性があります。
上図の実際のアドレスは、ある特定のマシンの特定の実装を示しており、説明のためにだけ掲載してあります。
デフォルトでは、64 ビットプログラムは開始アドレス 0x10000000 にリンクされます。プログラム全体は、テキスト、データ、ヒープ、スタック、および共用ライブラリを含めて、4G バイトを超えるアドレスに存在します。これは、64 ビットプログラムが正しいことを検証するのに役立ちます。たとえばプログラムが関連するポインタの上位 32 ビットを切り落としてしまうと、そのプログラムはアドレス空間の下方の 4G バイトの部分へアクセスしようとして失敗します。
64 ビットプログラムは 4G バイトを超える位置でリンクされますが、リンカーのマップファイルを使用し、コンパイラまたはリンカーに -M オプションを指定して、4G バイト未満の位置でリンクすることも可能です。4G バイト未満で 64 ビット SPARC プログラムをリンクするためのリンカーマップファイルは、/usr/lib/ld/sparcv9/map.below4G にあります。
詳細は、ld(1) のリンカーのマニュアルページを参照してください。
コンパイラには、性能の向上や、64 ビット SPARC プログラムでのコードサイズを小さくするなど、さまざまな目的に合わせた各種のコードモデルがあります。コードモデルは以下の要素で決定します。
位置決め方法 (絶対コード、あるいは位置に依存しないコード)
コードサイズ (2G バイト未満)
位置 (下部、中央、アドレス空間内の任意位置)
外部オブジェクト参照モデル (スモールまたはラージ)
次の表は、64 ビット SPARC プログラムで使用できる各種コードモデルを示したものです。
表 6-1 コードモデルの説明
コードモデル |
位置決め方法 |
コードサイズ |
位置 |
外部オブジェクト参照モデル |
---|---|---|---|---|
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 バイトよりも下方にリンクする必要があります。
次に示すプロセス間通信 (IPC) プリミティブは、従来どおり 64 ビットプロセスと 32 ビットプロセスとの間で動作します。
システム V IPC プリミティブ、たとえば shmop(2)、semop(2)、msgsnd(2)
共用ファイル上の mmap(2)
プロセス間の pipe(2)
プロセス間の door_call(3X)
xdr(3N) に説明されている外部データ表現を使用した、同じあるいは異なるマシン上のプロセス間の rpc(3N)
これらのすべてのプリミティブは、32 ビットプロセスと 64 ビットプロセスとの間の通信を可能にしますが、プロセス間で交換されているデータがそれらすべてのプロセスで正しく解釈されることを、明確な手順によって確認する必要がある場合があります。たとえば、long
型の変数を含む C データ構造体で記述されるデータを 2 つのプロセスが実際に共有するには、32 ビットプロセスがこの変数を 4 バイト量とみなし、64 ビットプロセスはこの変数を 8 バイト量とみなすということを認識する必要があります。
この相違を取り扱う 1 つの方法は、両プロセス間で意味をなすようにデータが完全に同じサイズであることを保障することです。int32_t
や int64_t
のような固定幅型を使ってデータ構造を構成してください。
システムで提供される派生型に対応する派生型の一群が <sys/types32.h> にあります。これらの派生型は、32 ビットシステムの基本型と同じ符号、同じサイズですが、ILP32 および LP64 のコンパイル環境でサイズが変わらないように定義されています。
32 ビットプロセスと 64 ビットプロセスとの間でポインタを共有するのは、さらに困難です。まずポインタのサイズが異なるということがあります。またそれ以上に重要なことは、既存の C の使用法に 64 ビット整数 (long long
) はありますが、64 ビットポインタには 32 ビット環境に相当するものはない、ということです。64 ビットプロセスが 32 ビットプロセスとデータを共有できるようにするため、32 ビットプロセスは共有データのうち、4G バイトまでしか一度に「見る」ことはできません。
XDR ルーチンの xdr_long(3N) は問題と思われるかもしれません。しかし、これは既存のプロトコルとの互換性を持たせるために従来どおり 32 ビットとして取り扱われます。64 ビットバージョンのルーチンが 32 ビットに格納できない long
値をコード化するように要求された場合、そのコード化処理は失敗します。
64 ビットバイナリは、ELF64 形式でファイルに格納されます。この ELF64 形式は、ほとんどのフィールドが完全 64 ビットアプリケーションを格納するために拡張されていることを除いて、ELF32 形式に類似しています。ELF64 ファイルは elf(3E) API、たとえば elf64_getehdr(3E) を使って読むことができます。
ELF ライブラリ libelf(4) の 32 ビットおよび 64 ビットのバージョンは、それぞれ ELF32 および ELF64 形式と、対応する API をサポートします。これによりアプリケーションは、32 ビットシステムまたは 64 ビットシステム (64 ビットプログラムを実行するには 64 ビットシステムが必要) から、両ファイル形式を構築、読み込み、あるいは修正ができるようになります。
さらに、Solaris では GELF (Generic ELF) インタフェースを提供し、プログラマが 1 つの共通 API を使用して両方の ELF 形式を操作できるようにしています。詳細は、elf(3E) のマニュアルページを参照してください。
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 ビットのデバッガが必要となります。
64 ビットの Solaris システムは、64 ビットカーネルを使って実装されています。カーネルの内容を直接調べたり変更するアプリケーションは、64 ビットアプリケーションに変換し、64 ビットバージョンの libkvm(4) とリンクしなければなりません。
このような変換と修正を行う前に、まずアプリケーションがカーネルのデータ構造を直接知る必要があるかどうかを検討した方がよいでしょう。プログラムが最初に移植されるかあるいは新規に作成された後に、システムコールを使って必要なデータを抽出するインタフェースが Solaris プラットフォームで利用可能になって追加された、という可能性があります。この場合は、最も一般的な代替 API として sysinfo(2)、kstat(3K)、sysconf(3c)、proc(4) を参照してください。これらのインタフェースが libkvm(4) の代わりに使用できるのなら、移植性を最大限維持するために、それらを使用してアプリケーションを 32 ビットのままにしてください。さらに利点として、これらの API のほとんどは処理が速く、カーネルメモリーにアクセスするときと同じセキュリティ特権を必要としないことがあります。
32 ビットバージョンの libkvm は、64 ビットのカーネルクラッシュダンプに対して kvm_open(3K) を使用しようとしたときに異常終了します。同様に、64 ビットバージョンの libkvm は、32 ビットカーネルクラッシュダンプに対して kvm_open(3K) を使用しようとしたときに異常終了します。
カーネルは 64 ビットプログラムなので、カーネルのシンボルテーブルを直接調べるために /dev/ksyms を開くアプリケーションは、ELF64 形式を理解するように機能を拡張する必要があります。
kvm_read(3K) または kvm_write(3K) へのアドレス引数がカーネルアドレスであるかユーザーアドレスであるかが曖昧であることは、64 ビットアプリケーションおよびカーネルではさらに問題となります。現在でもまだ kvm_read() と kvm_write() を使用している libkvm を利用するアプリケーションはすべて、kvm_kread(3K)、kvm_kwrite(3K)、kvm_uread(3K)、kvm_uwrite(3K) のルーチンを使用するようにする必要があります。これらのルーチンは、Solaris 2.5 から利用できるようになっています。
/dev/kmem または /dev/mem を直接読むアプリケーションは、従来どおり実行できます。ただし、これらのデバイスから読み込んだデータを解釈しようとすると、問題が発生します。これはデータ構造のオフセットおよびサイズは、確実に 32 ビットおよび 64 ビットカーネル間で異なるためです。
多くのカーネル統計情報のサイズは、カーネルが 64 ビットあるいは 32 ビットプログラムのどちらであるかということとは関係ありません。名前付き kstat (kstat(3K) のマニュアルページを参照) がエクスポートするデータ型は自明で、符号付きまたは符号なしの 32 ビットまたは 64 ビットカウンタデータを、適切なタグを付けてエクスポートします。したがって、libkstat を使用するアプリケーションは、64 ビットカーネル上で正常に動作させるために 64 ビットアプリケーションに変換する必要はありません。
名前付き kstats を作成および管理するデバイスドライバを修正するときは、エクスポートしようとする統計情報のサイズを、固定幅の統計データ型を使って 32 ビットおよび 64 ビットカーネル間で不変にすることをお薦めします。
64 ビット環境では stdio 機能が拡張されて、256 を超える数のストリームを同時に開くことができるようになりました。32 ビットの stdio 機能では、従来どおり 256 を超える数のストリームを同時に開くことはできないという制限があります。
64 ビットアプリケーションが、FILE データ構造体のメンバーにアクセスできることに依存しないようにしてください。実装に固有な構造体メンバーに直接アクセスしようとすると、コンパイルエラーとなります。この変更で既存の 32 ビットアプリケーションが影響を受けることはありませんが、このように構造体のメンバーを直接使用する方法は、すべてのコードから取り除く必要があります。
FILE 構造体には長い歴史があり、この構造体の中身を参照してストリームの状態に関する付加的な情報を収集するアプリケーションもあります。64 ビットのこの構造体は参照できないようになっているため、新しいルーチン群が 32 ビットの libc と 64 ビットの libc に加えられ、その結果、実装の内部に依存することなく同じ状態を調べることができるようになりました。たとえば __fbufsize(3S) のマニュアルページを参照してください。
64 ビットのパフォーマンスの長所および短所について説明します。
64 ビット量に対する算術演算および論理演算がより効率的である
演算に、全レジスタ幅、全レジスタセット、および新しい命令が使用される
64 ビット量のパラメータ渡しがより効率的である
小さなデータ構造体および浮動小数点のパラメータ渡しがより効率的である
より大きいレジスタを格納するためにより大きなスタック空間を必要とする
より大きなポインタによってより大きなキャッシュサイズを使用する
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() は、コピー量が多すぎるため、正しくない値を読み込むか、あるいはユーザースタック内の隣接する変数を破壊してしまいます。