Linuxアプリケーションの64ビット・システムへの移植

スムーズな移行のためのヒントとテクニック

64ビット・アーキテクチャーの普及に伴い、Linux® ソフトウェアを64ビット対応にすることが今まで以上に重要となってきました。宣言と代入、ビット・シフト、型定義、文字列の書式設定などの際に移植性を阻む落とし穴を避ける方法を学びましょう。

Harsha S. Adiga, Software Engineer, IBM

Harsha Adigaは、インド・バンガロール市のIBMソフトウェア・グループに勤務し、さまざまなLinuxおよびオープン・ソース・コミュニティーとワーキング・グループに積極的に参加しています。



2006年 4月 12日

Linuxは、64ビット・プロセッサーをはじめて使用したクロスプラットフォーム・オペレーティング・システムの1つでした。現在、64ビット・システムはサーバーおよびデスクトップにおいて、ありふれたものになりつつあるため、多くの開発者が32ビットから64ビット環境にアプリケーションを移植する必要に迫られています。Intel® Itanium® やその他の64ビット・プロセッサーの導入に伴い、ソフトウェアを64ビット対応にすることがますます重要になってきました。

UNIX®やその他のUNIX系オペレーティング・システムと同様、LinuxもLP64標準を使用します。LP64標準では、ポインターと長整数は64ビットですが、通常の整数は32ビット・エンティティーのままです。こうしたサイズの違いに影響を受けない高水準言語もありますが、C言語などは影響を受けます。

アプリケーションを32ビットから64ビットに移植する作業は、アプリケーションの作成方法や保守の方法によって、ごく簡単なものから非常に困難なものまでさまざまです。入念に作成された移植性の高いアプリケーションでも、些細なことが問題になる場合が多いため、この記事では、このような問題を取り上げ、解決のヒントを提示します。

64ビットの利点

32ビット・プラットフォームには多くの制約があり、データベースなどの大規模アプリケーションの開発者、特に、コンピューター・ハードウェアの進歩を活用したいと思っている開発者をますます苛立たせています。科学計算は通常、浮動小数点数学に依存しますが、金融計算など少数のアプリケーションでは、浮動小数点よりも狭い数値範囲、しかもより高い精度を必要とします。64ビットの数学では、この高精度な固定小数点数学を妥当な範囲で提供します。今日のコンピューター業界では、32ビット・アドレスの障壁に関して、さまざまな議論があります。32ビットのポインターでアドレス指定できるのは、わずか4GBの仮想アドレス空間です。この制限を克服することはできますが、アプリケーション開発が複雑になり、パフォーマンスが大幅に低下します。

言語実装に関する限り、現在のC言語標準では、少なくとも64ビットの「long long」データ型が可能です。しかし、実装では、より大きなサイズとして定義する可能性があります。

改善が必要なもう1つの分野は、日付です。Linuxでは、日付は1970年1月1日からの秒数を表す符号付き32ビット整数として表現されます。これは2038年に負になります。しかし、64ビット・システムでは、日付は符号付き64ビット整数として表現され、使用可能な範囲が延長されます。

要約すると、64ビット・アーキテクチャーには次のような利点があります。

  • 64ビットのアプリケーションは4エクサバイトの仮想メモリーに直接アクセスでき、Intel Itaniumプロセッサーは連続したリニアなアドレス空間を提供します。
  • 64ビットのLinuxでは、4エクサバイト(2の63乗)までのファイル・サイズが可能であり、大規模データベースにアクセスするサーバーにとって非常に大きな利点です。

Linux64ビット・アーキテクチャー

あいにく、Cプログラミング言語には新しい基本データ型を追加するメカニズムがありません。したがって、64ビットのアドレッシングおよび整数演算機能を実行するには、既存のデータ型のバインディングまたはマッピングを変更するか、言語に新しいデータ型を追加する必要があります。

表1. 32ビットおよび64ビット・データ・モデル
 ILP32LP64LLP64ILP64
char8888
short16161616
int32323264
long32643264
long long64646464
pointer32646464

3つの64ビット・モデル(LP64、LLP64、およびILP64)の違いは、非ポインター・データ型にあります。1つ以上のCデータ型の幅がモデル間で異なると、アプリケーションにさまざまな影響が出ます。このような影響は主に2つのカテゴリーに分類できます。

  • データ・オブジェクトのサイズ。コンパイラーはデータ型を自然境界で揃えます。言い換えると、32ビットのデータ型は64ビット・システム上で32ビットの境界で揃えられ、64ビットのデータ型は64ビット・システム上で64ビットの境界で揃えられます。これは、構造体や共用体などのデータ・オブジェクトのサイズが32ビット・システムと64ビット・システムで違ってくることを意味します。
  • 基本データ型のサイズ。基本データ型間の関係に関する一般的な前提は、64ビット・データ・モデルではもはや通用しません。基本データ型間の関係に依存するアプリケーションは、64ビット・プラットフォームでコンパイルすると失敗します。例えば、sizeof (int) = sizeof (long) = sizeof (pointer)という前提はILP32データ・モデルでは有効ですが、それ以外では無効です。

要するに、コンパイラーがデータ型を自然境界で揃えるということは、C構造体または共用体でのように、この位置揃えを実現するためにコンパイラーによって「パディング」が挿入されることを意味します。構造体または共用体のメンバーは、最も広いメンバーに合わせて揃えられます。リスト1に、この構造体を示します。

リスト1. C構造体
struct test {
	int i1;
	double d;
	int i2;
	long l;
}

表2に、32ビットおよび64ビット・システムの構造体の各メンバーのサイズと構造体のサイズそのものを示します。

表2. 構造体と構造体メンバーのサイズ
構造体メンバー32ビット・システムでのサイズ64ビット・システムでのサイズ
struct test {  
int i1;32-bits32-bits
 32-bits filler
double d;64-bits64-bits
int i2;32 bits32 bits
 32-bits filler
long l;32 bits64 bits
};Structure size 20 bytesStructure size 32 bytes

32ビット・システムでは、コンパイラーは変数dを揃えられない可能性があることに注意してください。これは64ビット・オブジェクトですが、ハードウェアは2つの32ビット・オブジェクトとして扱うからです。しかし、64ビット・システムではdとlの両方を揃えて、2つの4バイト・フィラーが追加されます。


32ビット・システムから64ビット・システムへの移植

このセクションでは、一般的な問題箇所を是正する方法を示します。

  • 宣言
  • 代入
  • 数値定数
  • エンディアニズム
  • 型定義
  • ビット・シフト
  • 文字列の書式設定
  • 関数パラメーター

宣言

コードを32ビットと64ビットの両方のシステムで動作するようにするには、宣言に関して次の点に注意してください。

  • 整数定数は、「L」または「U」を使用して適切に宣言してください。
  • 符号の拡張を避けるために、適切な箇所で符号なし整数が使用されていることを確認してください。
  • 両方のプラットフォームで特定の変数を32ビットにする必要がある場合は、型をintとして定義してください。
  • 変数を32ビット・システムでは32ビットに、64ビット・システムでは64ビットにする必要がある場合は、longとして定義してください。
  • 数値変数は、代入とパフォーマンスのためにintまたはlongとして宣言してください。charまたはshortを使用してバイトを保存しようとしないでください。
  • 文字ポインターおよび文字バイトは、8ビット文字での符号拡張の問題を避けるために、符号なしとして宣言してください。

C/C++では、式は結合、演算子の先行、および一連の格上げルールに基づきます。式を32ビットと64ビットの両方のシステムで正しく動作するようにするには、次のルールに注意してください。

  • 2つの符号付きintを加算すると、符号付きintになります。
  • intとlongを加算すると、longになります。
  • オペランドの1つが符号なしで、もう1つが符号付きintの場合、式は符号なしになります。
  • intとdoubleを加算すると、doubleになります。ここで、intは加算の前にdoubleに変換されます。

代入

ポインター、int、およびlongは、64ビット・システムでは同じサイズでなくなるため、アプリケーションでの変数の代入と使用の仕方によっては問題が起きる可能性があります。この問題を避けるヒントを次に示します。

  • intとlongを交代で使用しないでください。有効桁が切り詰められる可能性があります。例えば、次のようにはしないでください。
    int i;
    long l;
    i = l;
  • ポインターの格納にintを使用しないでください。次の例は32ビット・システムでは有効ですが、64ビット・システムでは失敗します。32ビット整数に64ビット・ポインターを入れることはできないためです。例えば、次のようにはしないでください。
    unsigned int i, *ptr;
    i = (unsigned) ptr;
  • intの格納にポインターを使用しないでください。例えば、次のようにはしないでください。
    int *ptr;
    int i;
    ptr = (int *) i;
  • 符号なしおよび符号付き32ビット整数が式の中に混在し、符号付きlongに代入される場合は、オペランドの1つを64ビット・タイプに変換してください。これにより、もう1つのオペランドが64ビットに格上げされて、式が代入されるときに、それ以上の変換が不要になります。もうひとつの解決策は、式全体を型変換して、代入時に符号拡張が起こるようにすることです。例えば、次のような問題を考えてみてください。
    long n;
    int i = -2;
    unsigned k = 1;
    n = i + k;

    算術としては、上記の太字で示されている式の結果は-1にならなければなりません。しかし、式は符号なしなので、符号拡張は行われません。解決策としては、オペランドの1つを64ビット・タイプに型変換するか(下記の1行目)、式全体を型変換します(下記の2行目)。
    n = (long) i + k;
    n = (int) (i + k);

数値定数

16進定数は一般に、マスクまたは特定のビット値として使用されます。サフィックスのない16進定数は、32ビットに収まる場合、および上位ビットがオンの場合、符号なしintとして定義されます。

例えば、定数OxFFFFFFFFLは符号付きlongです。32ビット・システムでは、これですべてのビットがセットされますが、64ビット・システムでは、下位32ビットだけがセットされ、値0x00000000FFFFFFFFになります。

すべてのビットをオンにする場合の移植可能な方法は、値が-1の符号付きlong定数を定義することです。こうすると、2の補数の計算が使用されるため、すべてのビットがオンになります。

long x = -1L;

起きる可能性のあるもうひとつの問題は、最上位ビットのセットです。32ビット・システムでは、定数0x80000000が使用されます。しかし、より移植性の高い方法は、シフト式を使用することです。

1L << ((sizeof(long) * 8) - 1);

エンディアニズム

エンディアニズムとは、データを格納する方法を指し、整数および浮動小数点データ型でのバイトのアドレス指定方法を定義します。

リトル・エンディアンは、最下位バイトが最低位のメモリー・アドレスに格納され、最上位バイトが最高位のメモリー・アドレスに格納されることを意味します。

ビッグ・エンディアンは、最上位バイトが最低位のメモリー・アドレスに格納され、最下位バイトが最高位のメモリー・アドレスに格納されることを意味します。

表3に、64ビット長整数のサンプル・レイアウトを示します。

表3. 64ビットlong intのレイアウト
 低位アドレス      高位アドレス
リトル・エンディアンバイト 0バイト 1バイト 2バイト 3バイト 4バイト 5バイト 6バイト 7
ビッグ・エンディアンバイト 7バイト 6バイト 5バイト 4バイト 3バイト 2バイト 1バイト 0

例えば、32ビット・ワード0x12345678は、ビッグ・エンディアン・マシンでは次のように格納されます。

表4. ビッグ・エンディアン・システムでの0x12345678
メモリー・オフセット0123
メモリーの内容0x120x340x560x78

0x12345678を0x1234と0x5678の2つのハーフ・ワードとして見た場合、ビッグ・エンディアン・マシンでは次のように見えます。

表5. ビッグ・エンディアン・システムでの2つのハーフ・ワードとしての0x12345678
メモリー・オフセット02
メモリーの内容0x12340x5678

しかし、リトル・エンディアン・マシンでは、ワード0x12345678は次のように格納されます。

表6. リトル・エンディアン・システムでの0x12345678
メモリー・オフセット0123
メモリーの内容0x780x560x340x12

同様に、2つのハーフ・ワード0x1234と0x5678は次のように見えます。

表7. リトル・エンディアン・システムでの2つのハーフ・ワードとしての0x12345678
メモリー・オフセット02
メモリーの内容0x34120x7856

次の例は、ビッグ・エンディアン・マシンとリトル・エンディアン・マシンのバイト順の違いを示しています。

下記のCプログラムは、ビッグ・エンディアン・マシンでコンパイルされて実行されると、「Big endian」と出力し、リトル・エンディアン・マシンでコンパイルされて実行されると、「Little endian」と出力します。

リスト2. ビッグ・エンディアン対リトル・エンディアン
#include <stdio.h>
main () {
int i = 0x12345678;
if (*(char *)&i == 0x12)
printf ("Big endian\n");
else if (*(char *)&i == 0x78)
    		printf ("Little endian\n");
}

エンディアニズムは、次のようなときに重要です。

  • ビット・マスクが使用されるとき
  • 間接ポインターでオブジェクトの一部をアドレス指定するとき

CおよびC++には、エンディアン問題の対処に役立つビット・フィールドがあります。マスク・フィールドや16進定数ではなく、ビット・フィールドを使用することをお奨めします。16ビットおよび32ビットを「ホスト・バイト・オーダー」から「ネット・バイト・オーダー」に変換するための関数がいくつかあります。例えば、htonl(3)、ntohl(3)は、32ビット整数の変換に使用されます。同様に、htons(3)、ntohs(3)は、16ビット整数に使用されます。ただし、64ビットについては、標準の関数セットはありません。しかし、Linuxには、ビッグ・エンディアンとリトル・エンディアンの両方のシステムについて、次のようなマクロが用意されています。

  • bswap_16
  • bswap_32
  • bswap_64

型定義

アプリケーションを、64ビット・オペレーティング・システムでサイズが変わるネイティブのC/C++データ型で作成するのではなく、変数に含まれるデータのサイズと型を明示的に決める型定義またはマクロを使用することをお奨めします。いくつかの型定義は、コードの移植性を高める上で役立ちます。

  • ptrdiff_t:
    2つのポインターの減算から得られる符号付き整数型。
  • size_t:
    符号なし整数とsizeof演算子の結果。これはmalloc(3)などの関数にパラメーターを渡すときに使用され、fred(2)など、いくつかの関数から返されます。
  • int32_t、uint32_tなど:
    事前定義された幅の整数型を定義します。
  • intptr_tおよびuintptr_t:
    voidを指す任意の有効なポインターを変換できる整数型を定義します。

例1:

次のステートメントのsizeofからの64ビットのリターン値は、bufferSizeに代入されるときに32ビットに切り詰められます。

解決策は、size_tを使用してリターン値を型変換し、次のようにsize_tとして宣言されたbufferSizeに代入します。

例2:

32ビット・システムでは、intとlongは同じサイズです。このため、これらを交互に使用する開発者もいます。この結果、ポインターがintに代入されたり、その逆が起きたりします。しかし、64ビット・システムでは、ポインターをintに代入すると、上位32ビットが切り詰められます。

解決策は、ポインターをポインター型として格納するか、intptr_tやuintptr_tなど、この目的のために定義された特殊な型として格納することです。

ビット・シフト

型なし整数定数は(符号なし)int型です。このため、シフト中に予期しない切り詰めが行われることがあります。

例えば、次のコード・スニペットでは、aの最大値は31です。これは、1 < aの型がintだからです。

long t = 1 < a;

64ビット・システムでシフトが行われるようにするには、1Lを次のように使用してください。

long t = 1L < a;

文字列の書式設定

関数printf(3)と関連関数が問題の主な原因となることがあります。例えば、32ビット・プラットフォームでは、%dを使用してintまたはlongを出力する方法は、通常、うまく機能しますが、64ビット・プラットフォームでは、longが最下位32ビットに切り詰められます。longについての正しい指定は%ldです。

同様に、短精度整数(char、short、int)がprintf(3)に渡されると、64ビットに広げられ、適切な場合は符号が拡張されます。下の例では、printf(3)はポインターが32ビットであると前提しています。

char *ptr = &something;
printf (%x\n", ptr);

上記のコード・スニペットは、64ビット・システムでは失敗し、下位4バイトだけを表示します。

これに対する解決策は、下記のように%p指定を使用することです。これは、32ビットと64ビットの両方のシステムでうまく機能します。

char *ptr = &something;
printf (%p\n", ptr);

関数パラメーター

関数にパラメーターを渡すときに覚えておかなければならないいくつかの点があります。

  • パラメーターのデータ型が関数プロトタイプによって定義されている場合、パラメーターは標準のルールに従って、その型に変換されます。
  • パラメーターの型が指定されていないときには、パラメーターはより大きな型に格上げされます。
  • 64ビット・システムでは、整数型は64ビット整数型に変換され、単精度浮動小数点型は倍精度に格上げされます。
  • リターン値が特に指定されていない場合、関数のデフォルトのリターン値はintです。

問題が発生するのは、符号付きintと符号なしintの和をlongとして渡すときです。次のような場合を考えてください。

リスト3. 符号付きintと符号なしintの和をlongとして渡す
long function (long l);

int main () {
	int i = -2;
	unsigned k = 1U;
	long n = function (i + k);
}

上記のコード・スニペットは、64ビット・システムでは失敗します。式(i + k)は符号なし32ビットの式であり、longに格上げされるときに符号が拡張されないためです。解決策は、オペランドの1つを64ビット型に変換することです。

レジスター・ベースのシステムには、関数にパラメーターを渡すためにスタックではなくレジスターが使用されるという別の問題もあります。次の例を考えてください。

float f = 1.25;
printf ("The hex value of %f is %x", f, f);

スタック・ベースのシステムでは、適切な16進値が出力されます。しかし、レジスター・ベースのシステムでは、16進値は浮動小数点レジスターではなく整数レジスターから読み取られます。

解決策は、浮動小数点変数のアドレスをintのポインターに型変換することです。ポインターは、次のように逆参照されます。

printf ("The hex value of %f is %x", f, *(int *)&f);


まとめ

大手ハードウェア・ベンダーは最近、64ビット製品を拡大しています。その拡大の理由は64ビット・プラットフォームが提供できるパフォーマンス、価値、およびスケーラビリティーです。32ビット・システムの制約、特に4GBの仮想メモリーの上限は、企業が64ビット・プラットフォームへの移行を検討する要因となっています。64ビット・アーキテクチャーに対応できるようにアプリケーションを移植する方法を知ることは、移植性のある効率的なコードの作成に役立ちます。

参考文献

学ぶために

製品や技術を入手するために

  • 皆さんの次期Linux開発プロジェクトを、IBM trial softwareを使って構築してください。developerWorksから直接ダウンロードすることができます。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=229866
ArticleTitle=Linuxアプリケーションの64ビット・システムへの移植
publish-date=04122006