Почему Linux на x86 использует разные сегменты для пользовательских процессов и ядра?

Итак, я знаю, что Linux использует четыре сегмента по умолчанию для процессора x86 (код ядра, данные ядра, код пользователя, пользовательские данные), но все они имеют одинаковую базу и ограничение (0x00000000 и 0xfffff), что означает, что каждая карта сегмента к одному и тому же набору линейных адресов.

Учитывая это, почему даже есть сегменты пользователя/ядра? Я понимаю, почему для кода и данных должны быть отдельные сегменты (только из-за того, как процессор x86 имеет дело с регистрами cs и ds), но почему бы не иметь один сегмент кода и один сегмент данных? Защита памяти осуществляется через пейджинг, а сегменты пользователей и ядра в любом случае сопоставляются с одинаковыми линейными адресами.

Ответ 1

Архитектура x86 связывает тип и уровень привилегий с каждым дескриптором сегмента. Тип дескриптора позволяет делать сегменты только для чтения, чтения/записи, исполняемых файлов и т.д., Но основная причина для разных сегментов с одинаковой базой и лимитом заключается в том, чтобы использовать другой уровень привилегий дескриптора (DPL).

DPL - это два бита, позволяющие кодировать значения от 0 до 3. Когда уровень привилегий равен 0, он считается ring 0, который является наиболее привилегированным. Дескрипторами сегмента для ядра Linux являются ring 0, а дескрипторы сегмента для пользовательского пространства - это кольцо 3 (наименее привилегированное). Это справедливо для большинства сегментированных операционных систем; ядром операционной системы является кольцо 0, а остальное - кольцо 3.

Ядро Linux устанавливает, как вы упомянули, четыре сегмента:

  • __ KERNEL_CS (сегмент кода ядра, base = 0, limit = 4GB, type = 10, DPL = 0)
  • __ KERNEL_DS (сегмент данных ядра, base = 0, limit = 4GB, type = 2, DPL = 0)
  • __ USER_CS (сегмент кода пользователя, base = 0, limit = 4GB, type = 10, DPL = 3)
  • __ USER_DS (сегмент пользовательских данных, base = 0, limit = 4GB, type = 2, DPL = 3)

Базис и лимит всех четырех одинаковы, но сегменты ядра DPL 0, пользовательские сегменты DPL 3, сегменты кода являются исполняемыми и читаемыми (недоступными для записи), а сегменты данных читабельны и доступны для записи ( не исполняется).

См. также:

Ответ 2

Архитектура управления памятью x86 использует как сегментацию, так и подкачку. Очень грубо говоря, сегмент представляет собой разделение адресного пространства процесса, которое имеет собственную политику защиты. Таким образом, в архитектуре x86 можно разделить диапазон адресов памяти, которые процесс видит на несколько смежных сегментов, и назначать разные режимы защиты для каждого. Пейджинг - это метод отображения небольших (обычно 4 КБ) областей адресного пространства процесса в куски реальной физической памяти. Таким образом, пейджинг управляет тем, как регионы внутри сегмента отображаются в физическую память.

Все процессы имеют два сегмента:

  • один сегмент (адреса от 0x00000000 до 0xBFFFFFFF) для пользовательских данных, зависящих от процесса, таких как программный код, статические данные, куча и стек. Каждый процесс имеет свой собственный независимый сегмент пользователя.

  • один сегмент (адреса 0xC0000000 через 0xFFFFFFFF), который содержит данные, специфичные для ядра, такие как инструкции ядра, данные, некоторые стеки, по которым может выполняться код ядра, и, что более интересно, область в этом сегменте напрямую отображается в физическую память, так что ядро ​​может напрямую обращаться к физическим ячейкам памяти, не беспокоясь о переводе адресов. Один и тот же сегмент ядра отображается в каждый процесс, но процессы могут обращаться к нему только при выполнении в защищенном режиме ядра.

Таким образом, в пользовательском режиме процесс может обращаться только к адресам, меньшим 0xC0000000; любой доступ к адресу, превышающему это, приводит к ошибке. Однако, когда процесс ядра пользователя начинает выполняться в ядре (например, после выполнения системного вызова), бит защиты в ЦП изменяется в режим супервизора (и некоторые регистры сегментации изменяются), что означает, что процесс таким образом, доступ к адресам выше 0xC0000000.

Ссылка из: ЗДЕСЬ

Ответ 3

в 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 <[email protected]>
   0x000000000040069c <+246>:   leaveq 
   0x000000000040069d <+247>:   retq 

Теперь, если мы удалим массивы на основе char из этой функции, gcc не будет генерировать эту проверку безопасности.

Я видел то же, что и gcc, даже для модулей ядра. В основном я видел сбой при обработке кода ядра, и он вибрировал с виртуальным адресом 0x28. Позже я понял, что думал, что правильно инициализировал указатель стека и правильно загрузил программу, у меня нет правильных записей в gdt, что переводит смещение fs на действительный виртуальный адрес.

Однако в случае кода ядра он просто игнорировал ошибку, вместо того, чтобы прыгать на что-то вроде __stack_chk_fail @plt > .

Соответствующий параметр компилятора, который добавляет этот защитник в gcc, является -fstack-protector. Я думаю, что это включено по умолчанию, которое компилирует пользовательское приложение.

Для ядра мы можем включить этот флаг gcc через опцию конфигурации CC_STACKPROTECTOR.

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

Ответ 4

Память ядра не должна читаться из программ, запущенных в пользовательском пространстве.

Данные программы часто не исполняются (DEP, функция процессора, которая помогает защитить от выполнения переполненного буфера и других вредоносных атак).

Все о контроле доступа - разные сегменты имеют разные права. Поэтому доступ к неправильному сегменту даст вам "ошибку сегментации".