Linux on x86 がユーザー プロセスとカーネルに異なるセグメントを使用するのはなぜですか?

StackOverflow https://stackoverflow.com/questions/4575032

質問

つまり、Linux が x86 プロセッサに 4 つのデフォルト セグメント (カーネル コード、カーネル データ、ユーザー コード、ユーザー データ) を使用していることはわかっていますが、それらはすべて同じベースと制限 (0x00000000 と 0xfffff) を持っており、各セグメントは同じにマップされます。線形アドレスのセット。

これを考えると、なぜユーザー/カーネル セグメントがあるのでしょうか?コードとデータに別々のセグメントが必要な理由は理解できますが (x86 プロセッサが cs レジスタと ds レジスタを処理する方法のためです)、なぜ 1 つのコード セグメントと 1 つのデータ セグメントを持たないのでしょうか?メモリ保護はページングによって行われ、ユーザー セグメントとカーネル セグメントはいずれにしても同じリニア アドレスにマップされます。

役に立ちましたか?

解決

X86 Architectureは、各セグメント記述子にタイプと特権レベルを関連付けます。記述子のタイプにより、セグメントを読み取りのみ、読み取り/書き込み、実行可能などにすることができますが、同じベースと制限を持つ異なるセグメントの主な理由は、異なる記述子特権レベル(DPL)を使用できるようにすることです。

DPLは2ビットで、値0〜3をエンコードできます。特権レベルが0の場合、それは リング0, 、これは最も特権です。 Linuxカーネルのセグメント記述子はリング0ですが、ユーザースペースのセグメント記述子はリング3(最も特権が最も少ない)です。これは、ほとんどのセグメント化されたオペレーティングシステムに当てはまります。オペレーティングシステムのコアはリング0で、残りはリング3です。

Linuxカーネルは、あなたが言ったように、4つのセグメントをセットアップします。

  • __kernel_cs(カーネルコードセグメント、base = 0、limit = 4gb、type = 10、dpl = 0)
  • __kernel_ds(カーネルデータセグメント、base = 0、limit = 4gb、type = 2、dpl = 0)
  • __user_cs(ユーザーコードセグメント、ベース= 0、limit = 4gb、type = 10、dpl = 3)
  • __user_ds(ユーザーデータセグメント、ベース= 0、limit = 4gb、type = 2、dpl = 3)

4つすべてのベースと制限は同じですが、カーネルセグメントはDPL 0、ユーザーセグメントはDPL 3、コードセグメントは実行可能で読み取られ(手数料ではありません)、データセグメントは読み取り可能で書き込み可能です(実行可能ではありません) 。

参照:

他のヒント

x86 メモリ管理アーキテクチャでは、セグメンテーションとページングの両方が使用されます。非常に大まかに言うと、セグメントは、独自の保護ポリシーを持つプロセスのアドレス空間のパーティションです。そのため、x86 アーキテクチャでは、プロセスが認識するメモリ アドレスの範囲を複数の連続したセグメントに分割し、それぞれに異なる保護モードを割り当てることができます。ページングは​​、プロセスのアドレス空間の小さな (通常は 4KB) 領域を実際の物理メモリのチャンクにマッピングする手法です。したがって、ページングは​​、セグメント内の領域を物理 RAM にマッピングする方法を制御します。

すべてのプロセスには 2 つのセグメントがあります。

  1. プログラムのコード、静的データ、ヒープ、スタックなどのユーザーレベルのプロセス固有のデータ用の 1 つのセグメント (アドレス 0x00000000 ~ 0xBFFFFFFF)。すべてのプロセスには独自の独立したユーザー セグメントがあります。

  2. 1 つのセグメント (アドレス 0xC0000000 から 0xFFFFFFFF) には、カーネル命令、データ、カーネル コードを実行できるいくつかのスタックなどのカーネル固有のデータが含まれます。さらに興味深いことに、このセグメント内の領域は物理メモリに直接マップされているため、カーネルは、アドレス変換を気にすることなく、物理メモリの場所に直接アクセスできます。同じカーネル セグメントがすべてのプロセスにマップされますが、プロセスは保護カーネル モードで実行する場合にのみアクセスできます。

したがって、ユーザーモードでは、プロセスは 0xC0000000 未満のアドレスにのみアクセスできます。これより上位のアドレスにアクセスすると障害が発生します。ただし、ユーザー モード プロセスがカーネルで実行を開始すると (たとえば、システム コールを行った後)、CPU の保護ビットがスーパーバイザ モードに変更されます (および一部のセグメンテーション レジスタが変更されます)。これは、プロセスがこれにより、0xC0000000 を超えるアドレスにアクセスできるようになります。

参照元: ここ

x86 -linuxセグメントレジスタは、バッファオーバーフローチェックに使用されます[スタック内のいくつかのchar配列を定義した以下のコードスニペットを参照]:

static void
printint(int xx, int base, int sgn)
{
    char digits[] = "0123456789ABCDEF";
    char buf[16];
    int i, neg;
    uint x;

    neg = 0;
    if(sgn && xx < 0){
        neg = 1;
        x = -xx;
    } else {
        x = xx;
    }

    i = 0;
    do{
        buf[i++] = digits[x % base];
    }while((x /= base) != 0);
    if(neg)
        buf[i++] = '-';

    while(--i >= 0)
        my_putc(buf[i]);
}

これで、コードGCC生成コードの分類が表示された場合。

関数printintのアセンブラーコードのダンプ:

 0x00000000004005a6 <+0>:   push   %rbp
   0x00000000004005a7 <+1>: mov    %rsp,%rbp
   0x00000000004005aa <+4>: sub    $0x50,%rsp
   0x00000000004005ae <+8>: mov    %edi,-0x44(%rbp)


  0x00000000004005b1 <+11>: mov    %esi,-0x48(%rbp)
   0x00000000004005b4 <+14>:    mov    %edx,-0x4c(%rbp)
   0x00000000004005b7 <+17>:    mov    %fs:0x28,%rax  ------> obtaining an 8 byte guard from based on a fixed offset from fs segment register [from the descriptor base in the corresponding gdt entry]
   0x00000000004005c0 <+26>:    mov    %rax,-0x8(%rbp) -----> pushing it as the first local variable on to stack
   0x00000000004005c4 <+30>:    xor    %eax,%eax
   0x00000000004005c6 <+32>:    movl   $0x33323130,-0x20(%rbp)
   0x00000000004005cd <+39>:    movl   $0x37363534,-0x1c(%rbp)
   0x00000000004005d4 <+46>:    movl   $0x42413938,-0x18(%rbp)
   0x00000000004005db <+53>:    movl   $0x46454443,-0x14(%rbp)

...
...
  // function end

   0x0000000000400686 <+224>:   jns    0x40066a <printint+196>
   0x0000000000400688 <+226>:   mov    -0x8(%rbp),%rax -------> verifying if the stack was smashed
   0x000000000040068c <+230>:   xor    %fs:0x28,%rax  --> checking the value on stack is matching the original one based on fs
   0x0000000000400695 <+239>:   je     0x40069c <printint+246>
   0x0000000000400697 <+241>:   callq  0x400460 <__stack_chk_fail@plt>
   0x000000000040069c <+246>:   leaveq 
   0x000000000040069d <+247>:   retq 

これで、この関数からスタックベースのチャーアレイを削除した場合、GCCはこのガードチェックを生成しません。

カーネルモジュールでもGCCによって生成されたのと同じを見てきました。基本的に、私はいくつかのカーネルコードをボトラしている間にクラッシュを見ていました、そして、それは仮想アドレス0x28で障害をしていました。その後、スタックポインターを正しく初期化し、プログラムを正しくロードしたと思ったと考えました。GDTに適切なエントリがありません。これにより、FSベースのオフセットが有効な仮想アドレスに変換されます。

ただし、カーネルコードの場合、__Stack_Chk_fail@plt>のようなものにジャンプする代わりに、エラーを無視するだけでした。

GCCにこのガードを追加する関連コンパイラオプションは、-fstack -protectorです。これは、ユーザーアプリをコンパイルするデフォルトで有効になっていると思います。

カーネルの場合、config cc_stackprotectorオプションを介してこのGCCフラグを有効にすることができます。

config CC_STACKPROTECTOR
 699        bool "Enable -fstack-protector buffer overflow detection (EXPERIMENTAL)"
 700        depends on SUPERH32
 701        help
 702          This option turns on the -fstack-protector GCC feature. This
 703          feature puts, at the beginning of functions, a canary value on
 704          the stack just before the return address, and validates
 705          the value just before actually returning.  Stack based buffer
 706          overflows (that need to overwrite this return address) now also
 707          overwrite the canary, which gets detected and the attack is then
 708          neutralized via a kernel panic.
 709
 710          This feature requires gcc version 4.2 or above.

このgs/fsがLinux/arch/x86/include/asm/stackprotector.hである関連するカーネルファイル

カーネルメモリは、ユーザースペースで実行されているプログラムから読みやすくしないでください。

プログラムデータは多くの場合、実行可能ではありません(DEP、プロセッサ機能は、オーバーフローされたバッファやその他の悪意のある攻撃の実行を防ぐのに役立ちます)。

それはすべてアクセス制御に関するものです - 異なるセグメントには異なる権利があります。そのため、間違ったセグメントにアクセスすると、「セグメンテーション障害」が得られます。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top