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

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

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

4 ответа

Решение

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

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

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

  • __KERNEL_CS (сегмент кода ядра, база = 0, лимит =4 ГБ, тип = 10, DPL = 0)
  • __KERNEL_DS (Сегмент данных ядра, база = 0, лимит =4 ГБ, тип = 2, DPL = 0)
  • __USER_CS (сегмент кода пользователя, база = 0, лимит =4 ГБ, тип = 10, DPL = 3)
  • __USER_DS (Сегмент пользовательских данных, база = 0, лимит =4 ГБ, тип =2, DPL=3)

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

Смотрите также:

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

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

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

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

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

Ссылка от: ЗДЕСЬ

В X86 - регистры сегмента linux используются для проверки переполнения буфера [см. фрагмент кода ниже, который определил несколько массивов символов в стеке]:

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. Я думаю, что это включено по умолчанию, которое компилирует пользовательское приложение.

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

config CC_STACKPROTECTOR
 699 bool "Включить -fstack-protector обнаружение переполнения буфера (ЭКСПЕРИМЕНТАЛЬНО)"
 700 зависит от SUPERH32
 701 помощь
 702 Эта опция включает функцию GCC -fstack-protector. это
 Функция 703 помещает в начале функций канарейку в
 704 стека непосредственно перед адресом возврата и проверяет
 705 значение непосредственно перед фактическим возвратом. Стек на основе буфера
 706 переполнений (которые нужно перезаписать этот обратный адрес) теперь также
 707 перезаписать канарейку, которая будет обнаружена, а затем атака
 708 нейтрализован через панику ядра.
 709
 710 Для этой функции требуется gcc версии 4.2 или выше.

Соответствующий файл ядра, где этот gs / fs - это linux/arch/x86/include/asm/stackprotector.h

Память ядра не должна читаться программами, работающими в пространстве пользователя.

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

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

Другие вопросы по тегам