lint(1) を使用する際には、すべての問題が lint(1) によって警告として検出されるわけではないこと、変更が不要な点についても lint(1) によって警告として出力されることがある、ということを憶えておいてください。警告の内容は、目的と照らし合わせて調べてください。これから示す例では、コードを変換する際に遭遇する可能性が高い問題を説明します。適切な場所で、lint(1) に相当する警告が現れます。
int
とポインタは、ILP32 環境では同じサイズであるため、多くのコードがこの仮定に基づいています。ポインタは、アドレス計算の際に int
または unsigned int
にキャストされることがあります。また、ポインタは long
にキャストすることもできます。long
とポインタは、ILP32 および LP64 で同じサイズだからです。unsigned long
を明示的に使うかわりに、uintptr_t
を使用してください。uintptr_t
は、意図することがより明確にわかり、コードをより移植可能なものにして、その結果、将来変更があっても影響されないようにするためです。次に例を示します。
char *p; p = (char *) ((int)p & PAGEOFFSET);
この場合、次の警告が出ます。
warning: conversion of pointer loses bits
次のコードを使用すると、正しい結果が出ます。
char *p; p = (char *) ((uintptr_t)p & PAGEOFFSET);
int
と long
は、ILP32 では実際には区別されないため、意図的あるいは非意図的にそれらは交換可能であると仮定して、既存のコードの多くで区別することなく使用されています。このように仮定しているコードは、ILP32 および LP64 で動作するように変更する必要があります。ILP32 データ型モデルでは int
と long
の両方が 32 ビットですが、LP64 データ型モデルでは long
は 64 ビットです。次に例を示します。
int waiting; long w_io; long w_swap; ... waiting = w_io + w_swap;
この場合、次の警告が出ます。
warning: assignment of 64-bit integer to 32-bit integer
意図しない符号の拡張は、64 ビットに変換する際によく発生する問題です。符号の拡張について lint(1) は警告を出さないため、実際に問題が発生する前に問題を検出するのは困難です。さらに、型変換および型昇格に関する規則には不明瞭な部分もあります。意図しない符号拡張の問題を解決するには、意図する結果を得ることができるように明示的なキャストを使用する必要があります。
ANSI C の変換規則を理解しておくと、なぜ符号の拡張が発生するかを理解するのに役立ちます。32 ビットおよび 64 ビットの整数値間において、符号拡張の問題の原因になることがある変換規則は、次のとおりです。
整数の昇格
char
、short
、列挙型、またはビットフィールド型は、符号付き / 符号なしに関わらず、int
を必要とする式の中に使用できます。int
が元の型の取り得る値をすべて格納できる場合、その値は int
に変換されます。そうでない場合は、unsigned int
に変換されます。
符号付きおよび符号なし整数間の変換
負の符号付き整数が、サイズが同じまたはより大きい型の符号なし整数に昇格される場合、最初に大きい型の符号付きの値に昇格され、その後符号なしの値に変換されます。
変換規則についての詳細は、ANSI C 規格を参照してください。この規格には、通常の算術変換や整数定数についての規則が規定されています。
64 ビットプログラムとしてコンパイルした場合、次の例の addr
変数は、addr
および a.base
が符号なしの型であっても符号付きの型に拡張されます。
struct foo { unsigned int base:19, rehash:13; }; main(int argc, char *argv[]) { struct foo a; unsigned long addr; a.base = 0x40000; addr = a.base << 13; /* 符号拡張が発生する */ printf("addr 0x%lx\n", addr); addr = (unsigned int)(a.base << 13); /* 符号拡張が発生しない */ printf("addr 0x%lx\n", addr); }
このように符号拡張が発生するのは、変換規則が次のように適用されるからです。
a.base が、整数の昇格規則によって、unsigned int
から int
に変換されます。このため、式 a.base << 13 は int
型ですが、符号拡張はまだ発生していません。
式 a.base << 13 は、int
型ですが、符号付きおよび符号なしの整数昇格規則によって最初に long
へ変換され、その後 unsigned long
へ変換された後、addr
に代入されます。符号拡張は、この式が int
から long
に変換されるときに発生します。
% cc -o test64 -xarch=v9 test.c % ./test64 addr 0xffffffff80000000 addr 0x80000000 % |
同じ例題が 32 ビットプログラムとしてコンパイルされた場合、符号拡張は発生しません。
% cc -o test32 test.c % ./test32 addr 0x80000000 addr 0x80000000 % |
一般に、ポインタ演算を使用した場合の方が、アドレス演算を使用した場合よりもうまく機能します。この理由は、ポインタ演算はデータ型モデルに依存しませんが、アドレス演算はデータ型モデルに依存するためです。さらにポインタ演算の方がコードを簡潔に記述することができます。次に例を示します。
int *end; int *p; p = malloc(4 * NUM_ELEMENTS); end = (int *)((unsigned int)p + 4 * NUM_ELEMENTS);
この場合、次の警告が出ます。
warning: conversion of pointer loses bits
次のコードを使用すると、正しい結果が出ます。
int *end; int *p; p = malloc(sizeof (*p) * NUM_ELEMENTS); end = p + NUM_ELEMENTS;
LP64 データ型モデルでは long およびポインタフィールドが 64 ビットに拡張されるため、コンパイラが構造体にパディングを追加して、境界を整列することがあります。SPARCV9 ABI と amd64 ABI のどちらでも、構造体の型はすべて、最低でも構造体内での最大サイズに整列されます。構造体を再構成するための簡単な規則は、long とポインタのフィールドを構造体の先頭位置に移動して、残りのフィールドを整列し直すことです。通常はサイズの大きい方から順に整列しますが、どれほどうまく詰め込めるかによって異なります。次に例を示します。
struct bar { int i; long j; int k; char *p; }; /* sizeof (struct bar) = 32 */
より良い結果を得るには、次のコードを使用します。
struct bar { char *p; long j; int i; int k; }; /* sizeof (struct bar) = 24 */
基本的な型の整列は、i386 と amd64 ABI 間で異なります。「整列の問題」を参照してください。
共用体フィールドは、ILP32 と LP64 とでサイズが変更されているので、必ず確認してください。
typedef union { double _d; long _l[2]; } llx_t;
このコードは、次のように使用してください。
typedef union { double _d; int _l[2]; } llx_t;
一部の定数式では、精度が不足するためにデータが失われる可能性があります。このような問題を検出するのは非常に困難です。定数式にデータ型を明示的に指定してください。各整数定数の後に (u、U、l、L) を組合せたものを追加します。キャストを使用して定数式のデータ型を指定することもできます。次に例を示します。
int i = 32; long j = 1 << i; /* 右辺が int 型のため j には 0 が代入される */
このコードは、次のように使用してください。
int i = 32; long j = 1L << i;
特定のコンパイルモードのコンパイラは、モジュールに使用されかつ extern
として定義または宣言されていない関数または変数に対してそのデータ型を int
とみなします。long
およびポインタがこのように使用された場合、コンパイラの暗黙的な int
宣言によって切り捨てられます。関数または変数に対する適切な extern
宣言は、C モジュール中にではなくヘッダーに置いてください。このヘッダーは、関数または変数を使用する C モジュールがインクルードするようにしてください。これがシステムヘッダーに定義されている関数または変数であっても、適当なヘッダーをコード内にインクルードしてください。
getlogin() が宣言されていないコードの例を次に示します。
int main(int argc, char *argv[]) { char *name = getlogin() printf("login = %s\n", name); return (0); }
この場合、次の警告が出ます。
warning: improper pointer/integer combination: op "=" warning: cast to pointer from 32-bit integer implicitly declared to return int getlogin printf
より良い結果を得るには、次のコードを使用します。
#include <unistd.h> #include <stdio.h> int main(int argc, char *argv[]) { char *name = getlogin(); (void) printf("login = %s\n", name); return (0); }
LP64 環境では、sizeof は、unsigned long
として実装される size_t
の実効的なデータ型を持ちます。sizeof は、int
型の引数を期待する (受け取る) 関数に渡されたり、int
に代入またはキャストされることがあります。このような切り捨てによってデータが失われることがあります。次に例を示します。
long a[50]; unsigned char size = sizeof (a);
この場合、次の警告が出ます。
warning: 64-bit constant truncated to 8 bits by assignment warning: initializer does not fit or is out of range: 0x190
関係式には変換規則があるので、注意を要します。必要に応じてキャストを追加して、式をどのように評価するかを明示的に記述してください。
printf(3C)、sprintf(3C)、 scanf(3C)、 sscanf(3C) が long
またはポインタ引数に対して使用される場合、それらを long
またはポインタ引数用に変更する必要がある場合があります。ポインタ引数を 32 ビットおよび 64 ビット環境で動作させるためには、書式文字列に指定する変換操作を %p にしてください。次に例を示します。
char *buf; struct dev_info *devi; ... (void) sprintf(buf, "di%x", (void *)devi);
この場合、次の警告が出ます。
warning: function argument (number) type inconsistent with format sprintf (arg 3) void *: (format) int
次のコードを使用すると、正しい結果が出ます。
char *buf; struct dev_info *devi; ... (void) sprintf(buf, "di%p", (void *)devi);
さらに、buf
によって示される記憶領域に 16 桁を格納できる大きさが十分にあることを確認してください。long
引数の場合は、long
サイズ指定子 l を書式化文字列の変換操作文字の前に追加してください。次に例を示します。
size_t nbytes; ulong_t align, addr, raddr, alloc; printf("kalloca:%d%%%d from heap got %x.%x returns %x\n", nbytes, align, (int)raddr, (int)(raddr + alloc), (int)addr);
この場合、次の警告が出ます。
warning: cast of 64-bit integer to 32-bit integer warning: cast of 64-bit integer to 32-bit integer warning: cast of 64-bit integer to 32-bit integer
次のコードを使用すると、正しい結果が出ます。
size_t nbytes; ulong_t align, addr, raddr, alloc; printf("kalloca:%lu%%%lu from heap got %lx.%lx returns %lx\n", nbytes, align, raddr, raddr + alloc, addr);