レベル: 中級 Jonathan Bartlett (johnnyb@eskimo.com), Director of Technology, New Medio
2007年 2月 28日 ABI (Application Binary Interface) とは、作成した言語やコンパイルしたコンパイラーが異なるプログラム同士がそれぞれの関数を互いに呼び出せるようにするための一連の規則のことです。4 回からなる連載の最終回となるこの記事では、64-ビットのELF システム (UNIX ライクなシステム) に対応した PowerPC® ABI について説明し、PowerPCABI を使用して関数を作成し、呼び出す方法についても取り上げます。64-ビットの PowerPC ABI がどのように機能するかを詳しく知ることで、アセンブリー言語でプログラミングしているかどうかに関わらず、POWER5™や他の PowerPC ベースのプロセッサーに対応した 64-ビットのプログラムを一層効率的に作成できるようになります。この記事では取り上げていませんが、32-ビットABI もあります。
簡易 ABI
前回の記事「PowerPC 分岐プロセッサーでのプログラミング」で、「簡易化された」 ABI について簡単に説明したように、この ABI を使うと特定の基準を満たす関数を最小限の手間で作成できます。簡易 ABIを使用するために関数が満たさなければならない基準は以下のとおりです。
- 別の関数を呼び出さないこと。
- レジスター 3 から 12 のみを変更すること (例外については、以下の 「不揮発性レジスター保存域」を参照)。
- レジスター・フィールド
cr0, cr1, cr5, cr6, and cr7のみを変更すること。
コードに PowerPC ベクトル演算拡張機能も併せて使用している場合には、上記の他にもいくつかの制約事項がありますが、この記事の対象範囲ではありません。
興味深いことに、簡易 ABI を使用する際に宣言する必要は一切ありません。次のセクションで説明するように、簡易 ABI は、スタック・フレームが不要な関数を対象とする標準ABI に完全に準拠したサブセットだからです。
PowerPC ABI を使用して呼び出された関数は、レジスター内の関数にパラメーターを渡します。レジスター 3 に最初の固定小数点パラメーター、レジスター4 に 2 番目の固定小数点パラメーターといった具合にレジスター 10 まで続きます。浮動小数点値も同様に、浮動小数点レジスター 1 から 13を使用して渡されます。関数が完了するとレジスター 3 によって値が返され、blr 命令で関数が終了します。
簡易 PowerPC ABI の説明として、1 つのパラメーターを取り、二乗してから返す関数を見てみましょう。以下は、アセンブリー言語での関数です(my_square.s として入力)。
リスト 1. 簡易 ABI を使用して数値を二乗する関数
###FUNCTION ENTRY POINT DECLARATION###
.section .opd, "aw"
.align 3
.global my_square
my_square: #this is the name of the function as seen
.quad .my_square, .TOC.@tocbase, 0
#Tell the linker that this is a function reference
.type my_square, @function
###FUNCTION CODE HERE###
.text
.my_square: #This is the label for the code itself (referenced in the "opd")
#Parameter 1 -- number to be squared -- in register 3
#Multiply it by itself, and store it back into register 3
mulld 3, 3, 3
#The return value is now in register 3, so we just need to leave
blr
|
以前は .opd セクションを使ってプログラムのエントリー・ポイントを宣言していましたが、上記では .opd セクションを関数の宣言にも使っています。これは正規プロシージャー記述子と呼ばれるもので、リンカーが異なる共有オブジェクト・ファイルからの位置に依存しないコードを合成するために必要な情報が含まれます。最も重要なフィールドは最初のフィールドで、これがプロシージャーのコードが開始するアドレスとなります。2番目のフィールドは関数に使用する TOC ポインターです。3 番目のフィールドは使用する言語の環境ポンターですが、通常はゼロに設定されます。ここで注目する点は、グローバルにエクスポートされるシンボル定義だけが正規プロシージャー記述子であることです。
この関数の C 言語プロトタイプは以下のようになります。
リスト 2. 数値を二乗する関数の C 言語プロトタイプ
typedef long long int64;int64 my_square(int64 val);
|
この関数を使用するための C のコードは以下のとおりです (my_square_tester.c として入力)。
リスト 3. my_square 関数を呼び出す C のコード
#include <stdio.h>
/* make declarations easier to write */
typedef long long int64;
int64 my_square(int64);
int main() {
int a = 32;
printf("The square of %lld is %lld.\n", a, my_square(a));
return 0;
}
|
このコードをコンパイルして実行する簡単な方法は、以下のとおりです。
リスト 4. my_square_tester のコンパイルと実行
gcc -m64 my_square.s my_square_tester.c -o my_square_tester
./my_square_tester
|
-m64フラグは、コンパイラーに 64-ビット命令を使用すること、64-ビット ABI とライブラリーを使用してコンパイルすること、そしてリンクに 64-ビットABI を使用することを指示します。するとコンパイラーが、リンクに関する問題をすべて処理してくれます (他にも、コマンド・ラインに -v を追加すると、完全なリンク・コマンド・ラインを確認できます)。
以上のように、簡易 PowerPC ABI では非常に簡単に関数を作成できます。問題が持ち上がってくるのは、関数が前述した基準を満たさない場合です。
スタック
ここからは、もっと複雑な ABI の部分に話題を移します。あらゆる ABI で最も肝心な部分は、スタックの利用方法についての詳細です。スタックとは、ローカル関数データを保持するメモリー内の領域のことです。
スタックの必要性
スタックがなぜ必要なのかは、再帰関数を見てみると最もよく理解できます。わかりやすい例として、以下の階乗関数で再帰を実装しているところを見てみましょう。
リスト 5. 階乗関数
typedef long long int64;
int64 factorial(int64 num) {
//BASE CASE
if (num == 0) {
return 1;
//RECURSIVE CASE
} else {
return num * factorial(num - 1);
}
}
|
上記の関数を概念的に理解するのはわけないことだと思いますが、ここでは具体的に検討してみます。この関数では何が行われているのか、そして例えば 4の階乗の値を見つけようとするとどんなことが起こるのかを以下に順を追って説明します。
まず、関数が呼び出されると num が 4 に設定されます。num が 0 より大きいことから、factorial が再度呼び出されますが、今度の factorial 呼び出しでは num が 3 に設定されます。ただし、同じ名前と同じコードを共有しているにも関わらず、前回とは異なるメモリー位置を参照します。同じコード内にあって変数名も同じですが、num が異なるからです。これは、関数が呼び出されるたびに起動レコード (スタック・フレームとも呼ばれます) が関連付けられるためです。起動レコードには関数のそれぞれの呼び出しに固有のすべてのデータ(パラメーター、ローカル変数など) が含まれます。これが、再帰的関数が他のアクティブな関数コールの変数の値を捨てないようにするための仕組みです。それぞれの呼び出しは独自の起動レコードを取得するため、関数コールのたびに、変数には起動レコード内に固有の保存スペースが与えられます。関数コールが完全に完了したときにだけ、起動レコードのスペースは再利用できるように解放されます(後で詳細を説明します)。
num の値が 3 に設定された後、この関数は続いて 2、1、0 の設定値で呼び出されますが、関数は値が 0 になるとベース・ケースに到達します。ベース・ケースとは、関数がそれ自体の呼び出しを止めて戻る時点のことです。つまり、num が 0 になると、関数は 1 の結果を返します。前の関数コールが出発点 (factorial(0) を呼び出した時点) に到達し、結果の 1 をその関数自体の num 値 (これも 1) と乗算してその結果を返すと、待機中の次の関数が再起動されます。再起動された関数は結果の 1 をその関数の num 値 2 と掛け、結果として 2 を返します。その結果を受けて次に待機していた関数コールが再起動され、前の結果がこの関数の num 値 3 を乗算されると結果は 6 になります。この結果が num 値 4 の元の関数に返されて前の結果と乗算され、最終的に 24 を得ることになります。
このように、関数が別の関数を呼び出すたびに、その値と状態は次に関数が呼び出されるまで保留されます。これは再帰関数だけでなく、すべての関数に当てはまることです。関数が再び別の関数を呼び出す場合も、その状態は同様に保留されます。関数がリターンすると、その関数を呼び出した関数を再び呼び戻し、中断された時点から続行します。つまり、実行が進むにつれて「存続」している関数コールが積み重なり、すべての関数が戻った時点でスタックから削除されるというわけです。結果は以下のようになります(factorial は fac と省略)。
-
fac(4) [アクティブ]
-
fac(4) [中断], fac(3) [アクティブ]
-
fac(4) [中断], fac(3) [中断], fac(2) [アクティブ]
-
fac(4) [中断], fac(3) [中断], fac(2) [中断], fac(1) [アクティブ]
-
fac(4) [中断], fac(3) [中断], fac(2) [中断], fac(1) [中断], fac(0) [アクティブ]
-
fac(4) [中断], fac(3) [中断], fac(2) [中断], fac(1) [アクティブ]
-
fac(4) [中断], fac(3) [中断], fac(2) [アクティブ]
-
fac(4) [中断], fac(3) [アクティブ]
-
fac(4) [アクティブ]
ご覧のように、中断されている関数の起動レコードが「積み重なり」、それぞれの関数がリターンするとスタックから取り除かれます。
スタックのレイアウト
この概念を実装するため、それぞれのプログラムにはプログラム・スタックと呼ばれるメモリー範囲が割り振られます。すべての PowerPC プログラムは、レジスター1 にあるこのスタックへのポインターから開始します。PowerPC ABI のレジスター 1 が指すのは常にスタックの最上部で、さらに関数はスタック・ポインターを基準に定義されるため、それぞれの関数は独自の起動レコードがどこにあるかを簡単に把握できます。関数が実行中の場合、スタック・ポインターはスタック全体の最上部を指します。これは、その関数の起動レコードの先頭でもあります。起動レコードはスタックに実装されることからスタック・フレームと呼ばれることもありますが、どちらの用語にしても意味は同じです。
「スタックの最上部」とは、概念的な意味です。物理的に言うと、メモリー内のスタックはメモリー番地アドレスの番号が大きいほうから小さいほうへと下に向かって拡大していきます。そのため、レジスター1 は、スタックの概念上の先頭を指すポインターであり、正のオフセットを持つスタックの位置を参照する場合は、実際には概念上のスタックの最上部より下を参照し、負のオフセットの場合は概念上は最上部より上を参照します。つまり、0(1) はスタックの概念上の最上部のことを指しており、4(1) は最上部から 4 バイト分下 (概念上) を指し、24(1) は概念上その下、100(1) はさらにその下を指すことになります。
スタックの概念的および物理的な構造が理解できたところで、今度は個別のスタック・フレームが保持する具体的な内容を見てみましょう。以下は、物理メモリーから見た64-ビット PowerPC ABI 準拠のスタック・レイアウトです (スタック・オフセットが記載されている場合は、メモリー内でのその位置の先頭を示します)。
表 1. スタック・フレームのレイアウト
| 内容 | サイズ | スタック・オフセットの開始位置 |
|---|
| 浮動小数点不揮発性レジスター保存域 | 可変 | 可変 | | 汎用不揮発性レジスター保存域 | 可変 | 可変 |
|---|
| VRSAVE | 4 バイト | 可変 |
|---|
| アライメント・パディング | 4 または 12 バイト | 可変 |
|---|
| ベクトル不揮発性レジスター保存域 | 可変 | 可変 (クワッド・ワードにアライメント) |
|---|
| ローカル変数ストレージ | 可変 | 可変 |
|---|
| 関数コール用パラメーター | 可変 (最小 64 バイト) |
48(1)
|
|---|
| TOC 保存域 | 8 |
40(1)
|
|---|
| リンク・エディター域 | 8 |
32(1)
|
|---|
| コンパイラー域 | 8 |
24(1)
|
|---|
| リンク・レジスター保存域 | 8 |
16(1)
|
|---|
| 条件レジスター保存域 | 8 |
8(1)
|
|---|
| 前のスタック・フレーム最上部へのポインター | 8 |
0(1)
|
|---|
浮動小数点、VRSAVE、ベクトル、アライメント・スペースについては取り上げません。これらの話題は浮動小数点およびベクトル演算に関わるもので、この記事の適用範囲ではないからです。すべてのスタック値はダブルワード(8 バイト) にアラインされ、フレーム全体はクワッド・ワード (16 バイト) にアラインされなければなりません。また、すべてのパラメーターはダブルワードにアラインされる必要があります。
それではここから、スタック・フレームを構成するそれぞれの部分の役割について説明します。
不揮発性レジスター保存域
スタック・フレームの最初にある部分は、不揮発性レジスター保存域です。PowerPC ABI 内のレジスターは、専用、揮発性、不揮発性の 3 つの基本クラスに分類されます。専用レジスターとは、スタック・ポンター(レジスター 1) や TOC ポインター (レジスター 2) などのように事前定義された永続的な関数を持つレジスターのことです。レジスター3 から 12 は揮発性レジスターで、このレジスターはどんな関数でも自由に変更することができますが、その際レジスターの前の値をリストアする必要はありません。これはつまり、関数が別の関数を呼び出す場合は常に、レジスター3 から 12 が呼び出された関数によって上書きされるという前提を意味します。
一方、レジスター 13 以降は不揮発性レジスターと見なされます。したがって、不揮発性レジスターを使用する関数は、関数からリターンする前にレジスターの値をリストアする前提のもとで、このレジスターを使用することができます。そのため、関数で不揮発性レジスターを使用する前に、レジスターの値をその関数のスタック・フレームに保存し、関数がリターンする前に値をリストアする必要があります。同様に、関数が別の関数を呼び出す際には、不揮発性レジスターに割り当てる値が変更されない(あるいは、少なくともリストアされる) ことを前提にできます。関数はこの保存域で、必要に応じて最小限あるいは最大限のメモリーを使用することができます。
以上の説明で、簡易 ABI の原則としてレジスター 3 から 12 しか使用できない理由がわかったはずです。これ以外のレジスターは不揮発性なので、レジスターを保存するためのスタック・スペースが必要になります。このように、他のレジスターを使用するにはスタックに保存しなければなりませんが、実はABI にはこの制限に対処する方法があります。関数は、スタック・ポインターより物理的に下にある 288 バイトを、他の関数を呼び出さない関数に自由に使えます。つまり実際には、簡易ABI を使用する関数でも、スタック・ポインターからの負のオフセットを使用することで、不揮発性レジスターを保存、使用、リストアすることができるのです。
ローカル変数ストレージ
ローカル変数保存域は、関数固有のデータを保存するための汎用の領域です。PowerPC アーキテクチャーでは多数のレジスターを使用できるので、この領域が必要となることはあまりありません。その一方、ローカル変数保存域がローカル配列に使用される場合はよくあります。この領域は関数に必要な任意のサイズにすることができます。
関数コール用パラメーター
関数パラメーターの扱いは、他のローカル・データとは多少異なります。PowerPC ABI は関数パラメーターの保存スペースを呼び出し側関数のスタック・スペース内に配置します。前に説明したように、関数コールは実際にはレジスター経由でパラメーターを渡しますが、それでもパラメーター用のスペースは確保しておく必要があります。パラメーターは揮発性レジスターを使用して渡されることから、値を保存しておかなければならない場合に備えるためです。このスペースはオーバーフロー時にも使用されます。すなわち、利用可能なレジスターより多くのパラメーターがある場合には、パラメーターをスタック・スペースに入れる必要があります。このパラメーター域は現行の関数から呼び出されるすべての関数で共有されるので、関数がスタック・スペースを設定する際には、その関数が関数コールで使用する最大数のパラメーターに対応するスペースを確保する必要があります。
関数がそのパラメーターの位置を把握できるようにするため、パラメーターはメモリーの下から上の順に格納されます。つまり、最初のパラメーターは 48(1) に、2 番目のパラメーターは 56(1) に保存されます。このように、パラメーター・リスト域がどんなに大きくても、呼び出される関数はそれぞれのパラメーターの正確なオフセットを認識できるのです。パラメーター・リスト域は、関数によって行われるすべての呼び出しに対して定義されるので、個別の関数コールに必要なサイズよりも大きくなりがちであることに注意してください。
関数に渡されるパラメーターの保存域は実際には呼び出し側の関数のスタック・フレーム内に含まれるため、関数がその関数自身のスタック・フレームを確立するときには、関数独自のスタック・フレーム・サイズが計上されるようにパラメーター・リストに対するオフセットを調整しなければなりません。例えば、関数func1 が 3 つのパラメーターを持つ関数 func2 を呼び出すとします。この func2 に 112 バイトのスタック・フレームがあるとすると、func2 がその最初のパラメーターのメモリーにアクセスするには、160(1) として参照することになります。これは、func2 がそのスタック・フレーム (112 バイト) の先にある最終フレーム (48 バイト) 内の先頭パラメーターに到達しなければならないためです。
幸い、ほとんどのパラメーターはパラメーター保存域のなかではなく、レジスターによって渡されるため、関数がパラメーター保存域にアクセスしなければならないことは滅多にありません。ただし、何も格納されるものがないとしても、パラメーター用にスペースを割り振る必要はあります。関数は最初の8 つのパラメーターについてはレジスターによってのみ渡されることを前提としますが、プログラムでパラメーターを格納する必要がある場合に備えて保存域を用意します。このスペースに必要な最小サイズも同じく64 バイトです。
TOC 保存域、リンク・エディター域、コンパイラー域
TOC 保存域、コンパイラー域、そしてリンカー域はすべてシステム用に予約されているため、プログラマーが変更することはできませんが、これらの領域のためのスペースはプログラマーが確保する必要があります。
リンク・レジスター保存域
リンク・レジスター保存域は、ABI の他の部分とは異なります。関数は開始時に、リンク・レジスターの保存が必要な場合にのみ、リンク・レジスターをその関数自体のスタック・フレームではなく、呼び出し側関数のスタック・フレームに保存します。ただし、別の関数を呼び出す関数のほとんどは、リンク・レジスターを保存する必要があります。
条件レジスター保存域
条件レジスター保存域は、条件レジスターの不揮発性フィールドが 1 つでも変更された場合にのみ必要となります。不揮発性フィールドは、cr2、cr3、および cr4 です。これらのフィールドのいずれかが変更される前に条件レジスターをスタック内の対応する領域に保存し、リストアしてからリターンします。
前のスタック・フレームへのポインター
スタック・フレーム内の最後の項目は、前のスタック・フレームへのポインターです。これは、バック・ポインターと呼ばれることがよくあります。
スタックを使用する関数の作成方法
関数は、関数を開始する時 (関数プロローグ) にスタック・フレームを作成し、終了する時 (関数エピローグ) にスタック・フレームを解放します。
通常、関数のプロローグは以下のシーケンスに従います。
-
stdu 1, -SIZE_OF_STACK(1) (ここで、SIZE_OF_STACK は関数のスタック・フレームのサイズ) を使用してスタック・スペースを確保し、古いスタック・ポインターを保存します。これにより、スタック・メモリーが最小単位で割り振られます。
- 関数が別の関数を呼び出す場合、またはリンク・レジスターを何らかの形で使用する場合、リンク・レジスターは、まず
mflr 0 命令で保存され、続いて命令 std 0, SIZE_OF_STACK+16(1) で呼び出し側関数のリンク・レジスター保存域に保存されます。
- この関数の実行中に使用されるすべての不揮発性レジスターを保存します (条件レジスターの不揮発性フィールドが 1 つでも使用される場合は、条件レジスターも保存します)。
関数のエピローグは上記のシーケンスを逆にして、保存された内容をリストアし、ld 1, 0(1) でスタック・フレームを取り壊します。これにより、前のスタック・ポインターが再びスタック・ポインター・レジスターにロードされます。
ここで、当初スタックなしで実装した関数を例として、スタックを使用した場合はどのようになるかを見てみましょう (my_square.s として入力し、以前と同じようにコンパイルして実行してください)。
リスト 6. スタックを使用して数値を二乗する関数
###FUNCTION ENTRY POINT DECLARATION###
.section .opd, "aw"
.align 3
.global my_square
my_square: #this is the name of the function as seen
.quad .my_square, .TOC.@tocbase, 0
.type my_square, @function
###FUNCTION CODE HERE###
.text
.my_square: #This is the label for the code itself (Referenced in the "opd")
##PROLOGUE##
#Set up stack frame & back pointer (112 bytes -- minimum stack)
stdu 1, -112(1)
#Save LR (optional)
mflr 0
std 0, 128(1)
#Save non-volatile registers (we don't have any)
##FUNCTION BODY##
#Parameter 1 -- number to be squared -- in register 3
mulld 3, 3, 3
#The return value is now in register 3, so we just need to leave
##EPILOGUE##
#Restore non-volatile registers (we don't have any)
#Restore LR (not needed in this function, but here anyway)
ld 0, 128(1)
mtlr 0
#Restore stack frame atomically
ld 1, 0(1)
#Return
blr
|
上記はプロローグとエピローグのコードでラップされているだけで、以前のコードとまったく変わりません。前述のとおり、このコードはプロローグとエピローグを必要としない単純なものなので、簡易ABI で十分に間に合います。ただし、スタック・フレームを設定して解放する方法を説明するには有効な例です。
ここで階乗関数を再び取り上げます。この関数はそれ自体を呼び出すため、スタック・フレームを十分に利用するからです。以下に、階乗関数がアセンブリー言語でどのように機能するかを示します(factorial.s として入力)。
リスト 7. アセンブリー言語の階乗関数
###ENTRY POINT###
.section .opd, "aw"
.align 3
.global factorial
factorial:
.quad .factorial, .TOC.@tocbase, 0
.type factorial, @function
###CODE###
.text
.factorial:
#Prologue
#Reserve Space
#48 (save areas) + 64 (parameter area) + 8 (local variable) = 120 bytes.
#aligned to 16-byte boundary = 128 bytes
stdu 1, -128(1)
#Save Link Register
mflr 0
std 0, 144(1)
#Function body
#Base Case? (register 3 == 0)
cmpdi 3, 0
bt- eq, return_one
#Not base case - recursive call
#Save local variable
std 3, 112(1)
#NOTE - it could also have been stored in the parameter save area.
# parameter 1 would have been at 176(1)
#Subtract One
subi 3, 3, 1
#Call the function (branch and set the link register to the return address)
bl factorial
#Linker word
nop
#Restore local variable (but to a different register -
#register 3 is now the return value from the last factorial
#function)
ld 4, 112(1)
#Multiply by return value
mulld 3, 3, 4
#Result is in register 3, which is the return value register
factorial_return:
#Epilogue
#Restore Link Register
ld 0, 144(1)
mtlr 0
#Restore stack
ld 1, 0(1)
#Return
blr
return_one:
#Set return value to 1
li 3, 1
#Return
b factorial_return
|
上記を C 言語でテストするには、以下を実行します (factorial_caller.c として入力)。
リスト 8. 階乗関数を呼び出すプログラム
#include <stdio.h>
typedef long long int64;
int64 factorial(int64);
int main() {
int64 a = 10;
printf("The factorial of %lld is %lld\n", factorial(a));
return 0;
}
|
以下に従ってコンパイルおよび実行します。
リスト 9. 階乗のコンパイルと実行
gcc -m64 factorial.s factorial_caller.c -o factorial
./factorial
|
この階乗関数には注目すべき点がいくつかあります。とりわけ興味深いのは、ローカル変数保存スペースを利用しているという点で、現行パラメーターは 112(1) に保存されています。これは関数パラメーターなので、スタック・スペースのダブルワードを追加で保存し、呼び出し側のパラメーター域に格納することも可能です。
このプログラムでは、関数コールの後の nop 命令も注目に値します。これは ABI に必要な命令です。この追加命令によって、リンカーはリンク・プロセス中に必要に応じて追加コードを挿入することができます。例えば、プログラムに複数のTOC を必要とするだけの十分なシンボルがある場合 (TOC については、「Assembly language for Power Architecture, Part 2: The art of loading andstoring on PowerPC」で説明しています)、リンカーが 1 つの命令 (あるいは分岐を使用した複数の命令) を発行して TOC を切り替えます。
最後に、関数コールの分岐ターゲットが関数コールを開始するコードではなく、.opd エントリー・ポイント記述子であることに注目してください。この記述子は、結局はリンカーによって的確なコードを指すように変換されますが、これによってリンカーが関数についての追加情報(使用している TOC など) を得られるため、必要に応じて TOC を切り替えるためのコードを発行することが可能になるというわけです。
動的ライブラリーの作成
関数の作成方法がわかったところで、今度は関数を 1 つのライブラリーにまとめてみます。追加のコードを作成する必要はなく、単にすべてをまとめてコンパイルすればいいだけの話です。factorial 関数と my_square 関数を単一のライブラリー (libmymath.so と呼ぶことにします) に集約するには、以下の内容を入力します。
リスト 10. 共有ライブラリーのコンパイル
gcc -m64 -shared factorial.s my_square.s -o libmymath.so
|
これにより、コンパイラーに libmymath.so という名前の共有オブジェクトを作成するように指示されます。これをリンクして実行可能プログラムにするには、コンパイル時のリンカーと実行時のダイナミック・リンカーの両方を有効にして、この共有オブジェクトを検出する必要があります。階乗関数が共有オブジェクトを使用するようにコンパイルするには、以下のようにコンパイルして実行します。
リスト 11. 共有ライブラリーの使用
#-L tells what directories to search, -l tells what libraries to find
gcc -m64 factorial_caller.c -o factorial -L. -lmymath
#Tell the dynamic linker what additional directories to search
export LD_LIBRARY_PATH=.
#Run the program
./factorial
|
当然のことながら、ライブラリーが標準ライブラリー・ロケーションにインストールされている場合は、上記のディレクトリー・フラグをすべてなくなってしまっても構いません。
「Assembly language for Power Architecture, Part 2: The art of loading andstoring on PowerPC」で説明したように、アプリケーションの TOC (コンテンツ・テーブル) には 64KB 相当のスペースでしかグローバル・データ参照を保持できません。複数の共有オブジェクトが同じアプリケーション・スペースにロードされ、コンテンツ・テーブルのサイズが大きくなりすぎた場合はどうなるかと言うと、正規プロシージャー記述子にはそのような場合に備え、.TOC.@tocbase 参照が用意されています。リンカーは単一アプリケーションの複数の TOC を管理することが可能なので、.TOC.@tocbase はリンカーに該当関数の TOC アドレスを該当する場所に書き込むように指示します。次にリンカーは、関数への参照を設定すると、現行関数の TOCを、この関数が呼び出す関数の TOC と比較します。この 2 つの TOC が同じ場合は、呼び出しはそのままになり、異なる場合は、実際にコードを変更し、関数コール時にTOC 参照を切り替えてリターンします。これが、正規プロシージャー記述子の主な存在理由の 1 つであり、関数コールに追加で nop 命令が続く主な理由の 1 つでもあります。このようなわけで、多くの共有オブジェクトでリンクしても、グローバル・シンボル・スペースが不足することを心配する必要はありません。
まとめ
簡易 64-ビット ABI はプログラムで使うには非常に手軽で、完全 ABI もそれほど難しくありません。最も難しい部分となるのは、スタック・フレームの各構成部分の位置とサイズを知った上で、それぞれに対応するオフセットを決定することです。
再利用可能なライブラリーをアセンブリー言語で作成する方法は簡単簡潔です。多少のコンパイラー・フラグを追加するだけで、64-ビット ABI を使用する関数を共有ライブラリーに変換し、すぐに使えるようになります。
この連載記事で、PowerPC プログラミングの容易さと威力を十分理解してもらえたら本望です。次のプロジェクトでは、アセンブリー言語を使ってPOWER5 チップのすべてのリソースを開発することをぜひ検討してみてください。
参考文献 学ぶために
製品や技術を入手するために
-
SEK for Linuxを注文してください。この 2 枚組 DVD セットには、Linux 対応の DB2®、Lotus®、Rational®、Tivoli®、そしてWebSphere® の最新 IBM トライアル・ソフトウェアが収録されています。
- developerWorks から直接ダウンロードできるIBM トライアル・ソフトウェアを使用して、Linux で次の開発プロジェクトを構築してください。
議論するために
著者について
記事の評価
|