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

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

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


person Community    schedule 01.01.2011    source источник


Ответы (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, сегменты кода являются исполняемыми и читаемыми (не доступны для записи), а сегменты данных доступны для чтения и записи (не выполняются). .

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

person Daniel Trebbien    schedule 01.01.2011
comment
Итак, DPL устанавливает минимальный уровень безопасности для каждого сегмента, но похоже, что я все равно могу получить доступ к любому линейному адресу как пользователь, так зачем нужен дополнительный сегмент для ядра? Если как пользователь я хочу получить доступ к адресу памяти x, я просто использую сегмент пользовательских данных со смещением x. Ядро может использовать сегмент данных ядра со смещением x, но он отображается на тот же линейный адрес, то есть на тот же адрес в физической памяти, так как это обеспечивает какую-либо защиту? - person ; 02.01.2011
comment
@anjruu: некоторые инструкции по сборке требуют определенного уровня привилегий, иначе возникает ошибка общей защиты (GP). Например, инструкция IN для чтения байта из порта требует, чтобы текущий PL (CPL) был меньше или равен PL ввода / вывода (IOPL; биты 12 и 13 регистра FLAGS), который для Linux равен 0. . CPL - это DPL дескриптора сегмента, соответствующего регистру CS (сегмент кода). - person Daniel Trebbien; 02.01.2011
comment
@ Даниэль: Попался, в этом есть смысл. Спасибо! - person ; 02.01.2011
comment
@ Дэниел: Подожди, нет, я не знаю, извини, что я такой тупой. Я предполагаю, что ЦП может работать в режиме ядра с уровнем привилегий 0 и иметь возможность выполнять любую инструкцию, и при этом иметь сегмент в регистрах CS, SS или DS с уровнем привилегий 3. ЦП может получить доступ к любому адресу в сегменте, а затем, как только ЦП завершит системный вызов, ЦП может переключиться на уровень 3, по-прежнему иметь доступ ко всему в сегменте и быть неспособным выполнять защищенные инструкции. Система подкачки может обеспечить защиту памяти. Извините, я все еще не вижу этого ... - person ; 02.01.2011
comment
@anjruu: извините за то, что я такой тупой. Ничего страшного. Я не против; на самом деле, это помогает мне помнить об этом. Следует уточнить, что ЦП не работает в режиме ядра. Чтобы воспользоваться преимуществами сегментации, ЦП должен находиться в защищенном режиме, но CPL является свойством каждой задачи. Каждая задача полностью описывается своим дескриптором состояния задачи, который, среди прочего, включает значения всех регистров, включая регистры сегментов ... - person Daniel Trebbien; 02.01.2011
comment
@anjruu: (продолжение) Теперь, способ, которым задача может изменить свой CPL, состоит в том, чтобы загрузить дескриптор сегмента, имеющий другой DPL, в регистр CS с помощью инструкции far RET. Задача кольца 0 может установить в своем регистре CS дескриптор сегмента с DPL 3 (таким образом, переместив задачу в кольцо 3). Однако для задачи невозможно вернуться к кольцу 0, потому что far RET проверяет, что возвращаемый PL больше или равен CPL. Таким образом, если задача ядра переместится в кольцо 3, она застрянет в кольце 3 и никогда не сможет вернуться! - person Daniel Trebbien; 02.01.2011
comment
@ Даниэль: Хорошо, я думаю, что понял. Итак, CPL - это свойство дескриптора сегмента, а не процессора в любой момент времени. Круто, спасибо большое! - person ; 02.01.2011
comment
Спасибо. Кажется, причина вам нужны отдельные сегменты ядра / пользователя для данных, если SS.DPL должен соответствовать CPL точно - он не может быть выше , в отличие от DS. (Почему существует это правило - другой вопрос, но я предполагаю, что это касается деталей, которые в настоящее время очень неясны). - person sourcejedi; 08.04.2019

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

Все процессы состоят из двух сегментов:

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

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

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

Ссылка на: ЗДЕСЬ

person Vikram.exe    schedule 01.01.2011
comment
Этот ответ касается пейджинга. Речь идет о сегментации, которая представляет собой сопоставление, выполняемое до сопоставления с разбивкой по страницам. - person TheAhmad; 28.09.2018

в 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 "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

person jithu83    schedule 18.03.2014

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

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

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

person Borealid    schedule 01.01.2011