0

C でコーディングする際の一般的な状況は、ポインターを返す関数を作成することです。実行時に記述された関数内で何らかのエラーが発生した場合、エラーNULLを示すために返されることがあります。NULLは特別なメモリ アドレス 0x0 であり、特別な状態の発生を示す以外には使用されません。

私の質問は、ユーザーランド アプリケーション データに決して使用されない他の特別なメモリ アドレスはありますか?

これを知りたい理由は、エラー処理に効果的に使用できるからです。このことを考慮:

#include <stdlib.h>
#include <stdio.h>

#define ERROR_NULL 0x0
#define ERROR_ZERO 0x1

int *example(int *a) {
    if (*a < 0)
        return ERROR_NULL;
    if (*a == 0)
        return (void *) ERROR_ZERO;
    return a;
}

int main(int argc, char **argv) {
    if (argc != 2) return -1;
    int *result;
    int a = atoi(argv[1]);
    switch ((int) (result = example(&a))) {
        case ERROR_NULL:
            printf("Below zero!\n");
            break;

        case ERROR_ZERO:
            printf("Is zero!\n");
            break;

        default:
            printf("Is %d!\n", *result);
            break;
    }
    return 0;
}

ユーザーランドアプリケーションによって使用されることのない特別な範囲のアドレスを知っていると、より効率的でクリーンな条件処理に効果的に利用できます。これについて知っている場合、どのプラットフォームに適用されますか?

スパンはオペレーティングシステム固有のものになると思います。私は主に Linux に興味がありますが、OS X、Windows、Android、およびその他のシステムについても知っておくとよいでしょう。

4

8 に答える 8

5

NULL は単なる特別なメモリ アドレス 0x0 であり、特別な状態の発生を示す以外には使用されません。

それは正確には正しくありません:NULLポインターが内部的にゼロではないコンピューターがあります (リンク)。

ユーザーランドアプリケーションに決して使用されない他の特別なメモリアドレスはありますか?

でもNULL普遍的ではありません。Cでプログラム可能なさまざまなプラットフォームの数を考えると、これは驚くべきことではありません.

ただし、メモリ内に独自の特別なアドレスを定義し、それをグローバル変数に設定し、それをエラー インジケーターとして扱うことを誰も止めません。これはすべてのプラットフォームで機能し、特別なアドレスの場所は必要ありません。

ヘッダー内:

extern void* ERROR_ADDRESS;

C ファイルの場合:

static int UNUSED;
void *ERROR_ADDRESS = &UNUSED;

この時点で、ERROR_ADDRESSはグローバルに一意な場所 (つまり、UNUSEDが定義されているコンパイル単位に対してローカルな の場所) を指します。これは、ポインタが等しいかどうかをテストする際に使用できます。

于 2013-03-06T00:07:28.897 に答える
1

答えは、C コンパイラと、コンパイルされた C プログラムが実行される CPU と OS に大きく依存します。

通常、ユーザーランド アプリケーションは、OS カーネルのデータとコードを指すポインターを介してデータやコードにアクセスすることはできません。また、OS は通常、そのようなポインタをアプリケーションに返しません。

通常、物理メモリによってバックアップされていない場所を指すポインターも取得しません。このようなポインターは、エラー (コードのバグ) によって、または意図的にそのようなポインターを構築することによってのみ取得できます。

C 標準では、ポインターの有効な範囲とそうでない範囲を定義していません。C では、有効なポインターは、NULLポインターまたは有効期間がまだ終了していないオブジェクトへのポインターのいずれかであり、それらはグローバル変数とローカル変数、およびmalloc()'dメモリと関数で作成されたものです。OS は、以下を返すことによってこの範囲を拡張する場合があります。

  • ソース コード レベルで C プログラムに明示的に定義されていないコードまたはデータ オブジェクトへのポインター (OS は、アプリがそのコードまたはデータの一部に直接アクセスできるようにする場合がありますが、これは一般的ではありません。アプリの読み込み時に OS によって作成されるか、アプリがコンパイルされたときにコンパイラによって作成されます。1 つの例として、Windows でアプリに実行可能 PE イメージを検査させます。イメージがメモリ内で開始する場所を Windows に問い合わせることができます)。
  • malloc()OS によって割り当てられたデータバッファーへのポインターfree()。 )
  • 逆参照できず、エラー インジケーターとしてのみ機能する OS 固有のポインター (たとえば、次のような過小参照可能なポインターが複数ある可能性があり、そのポインターが候補になる可能性がNULLあります)。ERROR_ZERO

私は通常、ハードコードされたマジック ポインターをプログラムで使用することを思いとどまらせます。

何らかの理由で、ポインターがエラー状態を伝える唯一の方法であり、それらが複数ある場合は、次のようにすることができます。

char ErrorVars[5] = { 0 };
void* ErrorPointer1 = &ErrorVars[0];
void* ErrorPointer2 = &ErrorVars[1];
...
void* ErrorPointer5 = &ErrorVars[4];

その後、さまざまなエラー条件で戻り、返さErrorPointer1ErrorPointer1た値をそれらと比較できます。ただし、ここで注意点があります。>>=<、を使用して、返されたポインターを任意のポインターと合法的に比較することはできません<=。これは、両方のポインターが同じオブジェクトを指している場合、または同じオブジェクトを指している場合にのみ有効です。したがって、次のような簡単なチェックが必要な場合:

if ((char*)(p = myFunction()) >= (char*)ErrorPointer1 &&
    (char*)p <= (char*)ErrorPointer5)
{
  // handle the error
}
else
{
  // success, do something else
}

pこれらの 5 つのエラー ポインターの 1 つに等しい場合にのみ有効です。そうでない場合、あなたのプログラムは想像もできない方法でも合法的に動作する可能性があります (これは、C 標準がそうしているためです)。この状況を回避するには、ポインタを各エラー ポインタと個別に比較する必要があります。

if ((p = myFunction()) == ErrorPointer1)
  HandleError1();
else if (p == ErrorPointer2)
  HandleError2();
else if (p == ErrorPointer3)
  HandleError3();
...
else if (p == ErrorPointer5)
  HandleError5();
else
  DoSomethingElse();

繰り返しになりますが、ポインターとその表現は、コンパイラーおよび OS/CPU 固有です。C 標準自体は、それらのポインターが C 標準で規定されているとおりに機能する限り (たとえば、ポインター演算が機能する場合)、有効および無効なポインターの特定の表現または範囲を強制しません。トピックに関する良い質問があります。

したがって、移植可能な C コードを作成することが目標である場合は、ハードコーディングされた "魔法の" ポインターを使用せず、エラー状態を伝えるために別のものを使用することをお勧めします。

于 2013-03-06T03:22:11.920 に答える
1

プログラマーとしてアドレスについて心配する必要はありません。これは、プラットフォームによって異なり、実際のハードウェア アドレスとアプリケーションの間でかなりの層があるためです。物理から仮想への変換は大きなものの 1 つであり、仮想アドレス空間はメモリにマップされ、各プロセスには独自のアドレス空間があり、ほとんどの最新のオペレーティング システムではハードウェア レベルで他のプロセスから保護されています。

ここで指定しているのは単なる 16 進数値であり、アドレスとして解釈されません。NULL に設定されたポインターは、基本的に、アドレス 0 でさえ、何も指していないことを示しています。それはNULLです。その価値が何であれ、プラットフォーム、コンパイラ、その他多くのものに依存します。

他の値へのポインターの設定は定義されていません。ポインターは、別のアドレスを格納する変数です。あなたがしようとしているのは、このポインターに有効なもの以外の値を与えることです。

于 2013-03-06T00:09:58.760 に答える
1

コンピューターとオペレーティング システムの両方に完全に依存します。たとえば、ゲーム ボーイ アドバンスのようなメモリ マップド IO を備えたコンピューターでは、「左上のピクセルは何色か」のアドレスとユーザーランド データを混同したくないでしょう。

http://www.coranac.com/tonc/text/hardware.htm#sec-memory

于 2013-03-06T00:08:52.773 に答える
0

Linuxでは、64ビットで、x86_64アーキテクチャ(IntelまたはAMDのいずれか)を使用する場合、合計64ビットアドレス空間の48ビットのみが使用されます(ハードウェア制限のAFAIK)。基本的に、2 47から 2 62までのアドレスは割り当てられないため、使用できるようになりました。

背景として、Linux プロセスの仮想アドレス空間はユーザー空間とカーネル空間で構成されています。上記のアーキテクチャでは、最初の 47 ビット (128 TB) がユーザー空間に使用されます。カーネル空間はスペクトルの最後で使用されるため、完全な 64 ビット アドレス空間の最後に最後の 128 TB が使用されます。間にあるのは未知の土地です。ただし、これは将来いつでも変更される可能性があり、移植性はありません。

しかし、あなたの方法以外にもエラーを返す方法はたくさん考えられるので、そのようなハックを使用する利点はわかりません。

于 2013-03-06T10:59:02.663 に答える
0

このコード:

#define ERROR_NULL 0x0
#define ERROR_ZERO 0x1

int *example(int *a) {
    if (*a < 0)
        return ERROR_NULL;
    if (*a == 0)
        return (void *) ERROR_ZERO;
    return a;
}

exampleは、入力パラメータを受け取りa、出力を へのポインタとして返す関数を定義しますint。同時に、エラーが発生すると、この関数はキャストを悪用しvoid*て、正しい出力データを返すのと同じ方法で呼び出し元にエラー コードを返します。呼び出し元は、有効な出力が受信される場合があることを認識している必要がありますが、実際には目的の出力ではなくエラー コードが含まれているため、このアプローチは間違っています。

決して使用されない他の特別なメモリアドレスはありますか...?
...エラー処理に効果的に使用できます

返される可能性のあるアドレスについて、何も仮定しないでください。戻りコードを呼び出し元に渡す必要がある場合は、より簡単な方法で行う必要があります。出力データへのポインターをパラメーターとして取得し、成功または失敗を識別するエラー コードを返すことができます。

#define SUCCESS     0x0
#define ERROR_NULL  0x1
#define ERROR_ZERO  0x2

int example(int *a, int** out) {
    if (...)
        return ERROR_NULL;
    if (...)
        return ERROR_ZERO;
    *out = a;
    return SUCCESS;
}
...
int* out = NULL;
int retVal = example(..., &out);
if (retVal != SUCCESS)
    ...
于 2013-03-06T00:10:27.343 に答える
0

実際には NULL(0) が有効なアドレスです。ただし、通常は書き込み可能なアドレスではありません。

非常に古い c コンパイラを使用した古い VAX ハードウェアでは、メモリから NULL が異なる値になる可能性があります。誰かがそれを確認できるかもしれません。C標準で定義されているため、現在は常に0になります-この質問を参照してくださいIs NULL always false?

通常、関数からエラーが返される方法は、errno を設定することです。特定の状況でエラーコードが意味をなす場合は、これに便乗できます。ただし、独自のエラーが必要な場合は、errno メソッドと同じことを行うことができます。

個人的には void* を返さない方が好きですが、関数が void** を取り、そこに結果を返すようにします。次に、0 = 成功のエラー コードを直接返すことができます。

例えば

int posix_memalign(void **memptr, size_t alignment, size_t size);

割り当てられたメモリが memptr に返されることに注意してください。結果コードは、関数呼び出しによって返されます。malloc とは異なります。

void *malloc(size_t size)
于 2013-03-06T00:12:16.817 に答える
0

TL;DR:

  • NULL のほかにもう 1 つ-1のエラー条件が必要な場合に使用します。

  • より特別な条件については、最下位ビットを設定するだけです。これは、malloc()ファミリまたはから返される値newが、基本的なアラインメントに対してアラインされることが保証されており、下位ビットが常にゼロであるため、自由に使用できるためです (タグ付きポインタ)

    割り当てが成功した場合は、基本アラインメントを持つ任意のオブジェクト型に適切にアラインされたポインターを返します。

    https://en.cppreference.com/w/c/memory/malloc

    char よりも幅の広い型へのポインターも常に整列されます。スタック上の char または char 配列を指す場合は、必要に応じてalignas

  • さらに多くの条件については、割り当てられたアドレスの範囲を制限できます。これにはプラットフォーム固有のコードが必要であり、移植可能なソリューションはありません


他の人が言ったように、それは非常に依存しています。ただし、動的割り当てを使用するプラットフォームを使用している場合-1は、(非常に可能性が高い) 安全な値です。

これは、メモリ アロケータが1 バイトだけではなくBIG BLOCKSでメモリを提供するためです§。したがって、返される最後のアドレスは. たとえば、is が 4 の場合、最後のブロックはアドレス { -4, -3, -2, -1 } にまたがり、最後の可能なアドレスは -4 = 0xFFFF...FFFC になります。その結果、家族から-1 が返されることはありません。-block_sizeblock_sizemalloc()

Linux のさまざまなシステム関数も、NULL の代わりに無効なポインターに対して -1 をmmap()返しますshmat()。ハンドルを返す Win32 API は、失敗した場合やハンドルの形式が正しくない場合に、NULL (0) または INVALID_HANDLE_VALUE (-1) を返すこともあります。が有効なメモリ アドレスである場合NULLがあるため、そうする必要があります。実際、ハーバード アーキテクチャを使用している場合、データ スペースのロケーション ゼロは非常に使用可能です。そして、フォン・ノイマン・アーキテクチャーでさえ、あなたが言ったこと

「NULL は単なる特別なメモリ アドレス 0x0 であり、特別な状態の発生を示す以外には使用されません」

アドレス 0 も有効なので、まだ間違っています。最近のほとんどの OS は、ページ ゼロを何らかの方法でマップして、ユーザー空間コードが逆参照するときにトラップするようにしているだけです。それでも、ページはカーネル コード内からアクセスできます。Linux カーネルのNULL ポインター逆参照バグに関連するエクスプロイトがいくつかありました。

実際、ゼロ ページの本来の優先使用とはまったく逆に、FreeBSD、Linux、Microsoft Windows などの一部の最近のオペレーティング システムでは、NULL ポインターの使用をトラップするために実際にゼロ ページにアクセスできなくなります。NULL ポインターは、何も指していない参照の値を表すために使用される方法であるため、これは便利です。

https://en.wikipedia.org/wiki/Zero_page

MSVC および GCC では、メンバーへの NULL ポインターは、32 ビット マシンではビット パターン 0xFFFFFFFF としても表されます。また、AMD GCN NULL ポインターの値も -1 です。


ポインターが通常は整列されているという事実を利用することで、さらに多くのエラー コードを返すことができます。たとえば、malloc常に「任意のオブジェクト タイプに適したメモリを配置します (これは実際には、メモリが次のように配置されることを意味しますalignof(max_align_t))

現在、 のデフォルトのアラインメントmallocは、OS が 32 ビットか 64 ビットかに応じて 8 バイトまたは 16 バイトです。これは、エラー報告または何らかの目的のために少なくとも 3 ビットを利用できることを意味します。また、char よりも幅の広い型へのポインターを使用する場合、常にaligned になります。したがって、通常、出力ではない char ポインターを返したい場合を除き、心配する必要はありませんmalloc(この場合、簡単に整列できます)。最下位ビットをチェックして、それが有効なポインターであるかどうかを確認するだけです

int* result = func();
if ((uintptr_t)result & 1)
    error_happened(); // now the high bits can be examined to check the error condition

16 バイト アラインメントの場合、有効なアドレスの最後の 4 ビットは常に 0 であり、有効なアドレスの総数はビット パターンの総数の ¹⁄₁₆ にすぎません。 2 64ビット ポインターを使用した 64 エラー コード。さらにaligned_alloc最下位ビットが必要な場合があります。

そのトリックは、ポインター自体にいくつかの情報を格納するために使用されています。多くの 64 ビット プラットフォームでは、上位ビットを使用してより多くのデータを格納することもできます。64 ビット ポインターで余分な 16 ビットを使用するを参照してください。


OS の助けを借りて、割り当てられたポインターの範囲を制限することで、極端に行くことさえできます。たとえば、ポインターを 2 ~ 3 GB の範囲で割り当てる必要があると指定した場合、2 GB 未満および 3 GB を超えるすべてのアドレスを使用して、エラー状態を示すことができます。その方法については、次を参照してください。

こちらもご覧ください


§割り当てられたブロックに関する一部の情報は簿記のために保存する必要があるため、ブロック サイズはブロック自体よりもはるかに大きくする必要があるため、これは明らかです。そうしないと、メタデータ自体が RAM の量よりもさらに大きくなります。したがって、呼び出しmalloc(1)た場合でも、完全なブロックを予約する必要があります。

于 2019-01-28T15:39:29.557 に答える