カーネルのCコードが技術的に間違っていますか?

カーネルのCコードが技術的に間違っていますか?

インターネットでは、次のような複数のスレッドを見つけることができます。

http://www.gossamer-threads.com/lists/linux/kernel/972619

人々は、-O0を使用してLinuxを構築できないと文句を言い、これがサポートされていないと言いました。 Linuxは自動的に機能をインライン化し、デッドコードを削除し、成功したビルドに必要なタスクを実行するためにGCCの最適化に頼っています。

私は少なくともいくつかの3.xカーネルについてこれを直接確認しました。私が試したことは、-O0でコンパイルされた場合、数秒のビルド時間の後に終了します。

これは一般に許容されるコーディング慣行と見なされますか?コンパイラの最適化(自動インラインなど)は、信頼できるほど十分に予測可能ですか?少なくとも1つのコンパイラだけを扱うときは? GCCの将来のバージョンがデフォルトの最適化(-O2や-Osなど)を使用して現在のLinuxカーネルのビルドを中断する可能性はどのくらいですか?

もっと賢明なポイント:3.xカーネルは最適化なしでコンパイルできないので、技術的に間違ったCコードと見なすべきですか?

ベストアンサー1

いくつかの(しかし関連する)質問を組み合わせます。そのうちのいくつかは実際にはトピック(コーディング標準など)とは関係がないので無視します。

カーネルが「技術的に間違ったCコード」かどうかから始めましょう。私は答えがカーネルが占める特別な場所を説明しているので、ここから始めました。これは残りの部分を理解するために重要です。

カーネルのCコードが技術的に間違っていますか?

答えは間違いなく「間違っている」です。

Cプログラムが間違っていると見なす方法はいくつかあります。まず、いくつかの簡単な質問を取り上げます。

  • C構文に従わない(つまり、構文エラーのある)プログラムは正しくありません。カーネルは、C構文にさまざまなGNU拡張を使用します。 C標準に関する限り、これは構文エラーです。 (もちろんGCCではそうではありません。-std=c99 -pedanticまたはこれに似てコンパイルしてみてください...)
  • 設計された目的を実行しないプログラムは正しくありません。カーネルは巨大なプログラムであり、変更ログをすばやく確認しても確かに巨大なプログラムではないことを証明できます。それとも、私たちが言いたいのと同じようにバグがあります。

Cでの最適化とはどういう意味ですか?

[注:このセクションには、実際の規則に対する非常に不正確な修正が含まれています。詳細については、標準を参照してスタックオーバーフローを検索してください。 ]

今より多くの説明が必要です。 C標準では、特定のコードが特定の動作を生成する必要があると規定しています。また、構文的に有効な特定のC項目には「未定義の動作」があると言います。 1つの(残念ながら一般的な!)例は、配列の終わりを超えてアクセスすることです(たとえば、バッファオーバーフロー)。

未定義の動作は非常に強力です。プログラムに少しでも含まれていれば、C標準は、もはやプログラムがどのような振る舞いを見せるのか、コンパイラがそれに直面したときにどの出力を生成するのか気にしません。

ただし、プログラムで定義された動作のみが含まれていても、Cはまだコンパイラに多くの空き容量を許可します。簡単な例として(注:私の例では、簡潔にするために行などを省略しました#include):

void f() {
    int *i = malloc(sizeof(int));
    *i = 3;
    *i += 2;
    printf("%i\n", *i);
    free(i);
}

もちろん、5が印刷され、その後に改行文字が続くはずです。これがC標準が要求するものです。

このプログラムをコンパイルして出力を逆アセンブルすると、メモリを取得するためにmallocが呼び出され、返されたポインタがどこか(おそらくレジスタ)に格納され、値3がそのメモリに格納され、次にそのメモリに2が追加されます。予想できます。メモリ(おそらくロード、追加、保存も必要です)を実行し、メモリをスタックにコピーし、ドット"%i\n"文字列をスタックに挿入して関数をprintf呼び出します。かなり多くのことがあります。ただし、代わりに次のような書き込みが表示されることがあります。

/* Note that isn't hypothetical; gcc 4.9 at -O1 or higher does this. */
void f() { printf("%i\n", 5) }

問題は次のとおりです。 C規格ではこれを許可しています。 C 標準は、次の事項にのみ関心があります。結果、実装方法ではなく。

これがC言語最適化のすべてです。コンパイラは、C標準が必要とする結果を得るためのよりスマートな方法(フラグに応じて一般的に小さいかより速い)を提示します。 GCC-ffast-mathオプションなど、いくつかの例外がありますが、そうでない場合、最適化レベルは技術的に正しいプログラム(つまり、定義された動作のみを含むプログラム)の動作を変更しません。

定義されたアクションのみを使用してカーネルを作成できますか?

引き続きサンプルプログラムを確認しましょう。コンパイラが変換するバージョンではなく、私たちが書くバージョンです。私たちが最初にすることは、mallocメモリを取得するために呼び出すことです。 C標準は何をすべきかを教えてくれますmallocが、それがどのように行われるのかを教えてくれません。

malloc速度ではなく明確さを目的とした実装を見てみると、大きなメモリチャンクを得るためにいくつかのシステムコール(mmapwithなど)を実行することがわかります。MAP_ANONYMOUSブロックのどの部分が使用され、どの部分が使用可能かを示すいくつかのデータ構造を内部的に保持します。少なくとも要求されたサイズと同じ大きさの空きブロックを見つけ、要求された量だけ切り捨て、そのブロックへのポインタを返します。また、完全にCで書かれており、定義された動作のみが含まれています。スレッドセーフな場合は、一部のpthread呼び出しを含めることができます。

さて、最後に何が起こっているのか見てみると、mmapいろいろな興味深いものが見えます。まず、システムにマッピングに使用できる十分なRAMおよび/またはスワップスペースがあることを確認するために、いくつかのチェックを実行します。次に、ブロックを入れる空きアドレス空間を探します。その後、ページテーブルと呼ばれるデータ構造を編集し、プロセスで一連のインラインアセンブリ呼び出しを実行できます。実際には、物理​​メモリのいくつかの空きページ(たとえば、物理DRAMモジュールの実際のビット)を見つけることもできます。このプロセスでは、他のメモリを強制的に交換する必要があるかもしれません。要求されたブロック全体に対してこれを行わない場合は、そのメモリに最初にアクセスしたときに発生するように設定します。ほとんどの作業は、インラインアセンブリ、さまざまなマジックアドレスの作成などを介して行われます。また、特に交換が必要な場合は、コアの大部分を使用することにも注意してください。

インラインアセンブリ、マジックアドレスの作成などはC仕様の外にあります。これは驚くべきことではありません。 Cは、1970年代初頭にCが発明されたときに想像できなかった多くのアーキテクチャを含む、さまざまなシステムアーキテクチャで実行できます。マシン固有のコードを隠すことは、カーネル(およびある程度Cライブラリ)の重要な部分です。

もちろん、サンプルプログラムに戻ると明らかに似printfていることがわかります。標準Cですべての書式設定などを行う方法は非常に明確ですが、実際にはモニターに表示されます。それとも別のプログラムにパイプしますか?今回もカーネル(そしておそらくX11やWayland)は多くの魔法を実行します。

カーネルが実行する他の作業を考えると、多くの部分がCの外側にあります。たとえば、カーネルはディスク(Cはディスク、PCIeバス、またはSATAについては不明)のデータを物理メモリ(Cはmallocのみを知り、DIMM、MMUなどは不明)に読み込み、実行可能にします(Cは何も知らない)既知のプロセッサ実行ビットに対して)関数として呼び出されます(Cの外部だけでなく、非常に許可されていません)。

カーネルとコンパイラの関係

以前の内容を覚えている場合、プログラムに定義されていない動作が含まれている場合、C標準に関する限り、すべてが間違っています。ただし、カーネルには未定義の動作を含める必要があります。したがって、カーネルとコンパイラの間には、少なくともカーネル開発者がC標準に違反しても、カーネルが機能することを確実にするのに十分な関係が必要です。少なくともLinuxの場合、カーネルはGCCの内部動作についてある程度知っておく必要があります。

割れる確率はどのくらいですか?

将来のGCCバージョンでは、カーネルが破損する可能性があります。このようなことが以前にも何度も起こったので、私は自信を持って話すことができます。もちろん、GCCの厳格なエイリアスの最適化のようなものは、カーネルに加えて多くのものを壊します。

また、Linuxカーネルが依存するインラインは自動インラインではなく、カーネル開発者が手動で指定することに注意してください。 -O0を使用してカーネルをコンパイルし、いくつかのマイナーな問題を解決した後は、ほとんどが動作すると報告する人がたくさんいます。 (それらの1つはあなたがリンクしたスレッドにもあります)。ほとんどのカーネル開発者は、-O0副作用として最適化を要求し、コンパイルする理由がないと考え、いくつかのトリックが機能し、誰もテストに使用しないためサポートされ-O0ません。

たとえば、次の項目以上にコンパイルしてリンクします-O1が、次の項目にはリンクしません-O0

void f();

int main() {
    int x = 0, *y;
    y = &x;

    if (*y)
        f();
    return 0;
}

f()最適化により、gccは絶対に呼び出されずに無視されることを確認できます。最適化がないと、gccは呼び出しを保存し、リンカは失敗しますf()。カーネル開発者は、カーネルコードを読みやすくするために同様の動作に頼っています。

おすすめ記事