Сегментация памяти Linux - PullRequest
6 голосов
/ 20 мая 2019

Рассматривая внутреннюю часть Linux и управление памятью, я только что наткнулся на модель сегментированной подкачки, которую использует Linux.

Поправьте меня, если я ошибаюсь, но Linux (защищенный режим) использует подкачку для отображения линейного виртуального адресного пространства в физическое адресное пространство. Это линейное адресное пространство, состоящее из страниц, разделено на четыре сегмента для модели плоской памяти процесса, а именно:

  • Сегмент кода ядра (__KERNEL_CS);
  • Сегмент данных ядра (__KERNEL_DS);
  • Сегмент кода пользователя (__USER_CS);
  • Сегмент пользовательских данных (__USER_DS);

Пятый сегмент памяти, известный как нулевой сегмент, присутствует, но не используется.

Эти сегменты имеют CPL (текущий уровень привилегий) 0 (супервизор) или 3 (пользовательское пространство).

Для простоты я сконцентрируюсь на отображении 32-битной памяти с адресным пространством 4 ГБ, 3GiB - для пространства процессов пользовательского пространства (показано зеленым), 1GiB - для пространства ядра супервизора (показано красным). :

Virtual Memory Space

Таким образом, красная часть состоит из двух сегментов __KERNEL_CS и __KERNEL_DS, а зеленая часть из двух сегментов __USER_CS и __USER_DS.

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

Однако, как извлечено из Википедии здесь :

[...] многие 32-разрядные операционные системы имитируют модель плоской памяти, устанавливая базы всех сегментов в 0, чтобы сделать сегментацию нейтральной для программ.

Просмотр кода ядра Linux для GDT здесь :

[GDT_ENTRY_KERNEL32_CS]       = GDT_ENTRY_INIT(0xc09b, 0, 0xfffff),
[GDT_ENTRY_KERNEL_CS]         = GDT_ENTRY_INIT(0xa09b, 0, 0xfffff),
[GDT_ENTRY_KERNEL_DS]         = GDT_ENTRY_INIT(0xc093, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER32_CS] = GDT_ENTRY_INIT(0xc0fb, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER_DS]   = GDT_ENTRY_INIT(0xc0f3, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER_CS]   = GDT_ENTRY_INIT(0xa0fb, 0, 0xfffff),

Как указал Питер, каждый сегмент начинается с 0, но что это за флаги, а именно 0xc09b, 0xa09b и так далее? Я склонен полагать, что они являются селекторами сегментов, если нет, то как бы я мог получить доступ к сегменту пользовательского пространства из сегмента ядра, если оба их адресных пространства начинаются с 0?

Сегментация не используется. Используется только подкачка. У сегментов seg_base адреса установлены 0, расширяя их пространство до 0xFFFFF и, таким образом, давая полное линейное адресное пространство. Это означает, что логические адреса не отличаются от линейных адресов.

Кроме того, поскольку все сегменты перекрывают друг друга, является ли блок разбиения на страницы, который обеспечивает защиту памяти (то есть разделение памяти)?

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

1 Ответ

8 голосов
/ 20 мая 2019

Да, в Linux используется подкачка, поэтому все адреса всегда виртуальные. (Чтобы получить доступ к памяти по известному физическому адресу, Linux сохраняет всю физическую память 1: 1, сопоставленную с диапазоном виртуального адресного пространства ядра, поэтому она может просто проиндексировать этот «массив», используя физический адрес в качестве смещения. Сложности по модулю для 32 ядра в системах с большим количеством физической оперативной памяти, чем адресное пространство ядра.)

Это линейное адресное пространство, состоящее из страниц, разбито на четыре сегмента

Нет, Linux использует модель с плоской памятью. Основание и предел для всех 4 из этих дескрипторов сегмента - 0 и -1 (неограниченно). то есть все они полностью перекрываются, охватывая все 32-битное виртуальное линейное адресное пространство.

Таким образом, красная часть состоит из двух сегментов __KERNEL_CS и __KERNEL_DS

Нет, здесь вы ошиблись. регистры сегмента x86 не используются для сегментации; это устаревший багаж x86, который используется только для выбора режима процессора и уровня привилегий на x86-64 . Вместо того, чтобы добавлять новые механизмы для этого и отбрасывать сегменты полностью для длинного режима, AMD просто стерилизовала сегментацию в длинном режиме (базовая точка была установлена ​​на 0, как и у всех, кто все равно использовался в 32-битном режиме) и продолжала использовать сегменты только для целей конфигурации машины, которые не являются особенно интересно, если вы на самом деле не пишете код, который переключается в 32-битный режим или что-то еще.

(За исключением того, что вы можете установить ненулевое основание для FS и / или GS, и Linux делает это для локального хранилища потоков. Но это не имеет ничего общего с реализацией copy_from_user() или чем-то подобным. чтобы проверить это значение указателя, а не со ссылкой на какой-либо сегмент или CPL / RPL дескриптора сегмента.)

В 32-битном унаследованном режиме можно написать ядро, использующее модель сегментированной памяти, но ни одна из основных операционных систем на самом деле этого не делала. Некоторые люди хотят, чтобы это стало чем-то, например, например. см. этот ответ, оплакивающий x86-64, делающий невозможной ОС в стиле Multics . Но это не , как работает Linux.

Linux - это https://wiki.osdev.org/Higher_Half_Kernel,, где указатели ядра имеют один диапазон значений (красная часть), а адреса пространства пользователя находятся в зеленой части. Ядро может просто разыменовать адреса пользовательского пространства, если сопоставлены нужные таблицы страниц пользовательского пространства, ему не нужно переводить их или делать что-либо с сегментами; это то, что значит иметь плоскую модель памяти . (Ядро может использовать «пользовательские» записи таблицы страниц, но не наоборот). В частности, для x86-64 см. https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt для действительной карты памяти.


Единственная причина, по которой все эти 4 записи GDT должны быть разделены, - по причинам уровня привилегий, и что дескрипторы данных и сегментов кода имеют разные форматы. (Запись GDT содержит больше, чем просто основание / предел; это те части, которые должны отличаться. См. https://wiki.osdev.org/Global_Descriptor_Table)

И особенно https://wiki.osdev.org/Segmentation#Notes_Regarding_C, который описывает, как и почему GDT обычно используется «нормальной» ОС для создания плоской модели памяти с парой кодов и дескрипторов данных для каждого уровня привилегий.

Для 32-битного ядра Linux только gs получает ненулевое основание для локального хранилища потока (поэтому режимы адресации, такие как [gs: 0x10], получат доступ к линейному адресу, который зависит от потока, который его выполняет). Или в 64-битном ядре (и 64-битном пространстве пользователя) Linux использует fs. (Поскольку x86-64 сделал GS особенным с инструкцией swapgs, предназначенной для использования с syscall для ядра, чтобы найти стек ядра.)

Но в любом случае ненулевое основание для FS или GS не из записи GDT, оно устанавливается с помощью инструкции wrgsbase. (Или на процессорах, которые этого не поддерживают, с записью в MSR).


но что это за флаги, а именно 0xc09b, 0xa09b и так далее? Я склонен полагать, что они являются селекторами сегментов

Нет, селекторы сегментов - это индексы в GDT. Ядро определяет GDT как массив C, используя синтаксис назначенного инициализатора, такой как [GDT_ENTRY_KERNEL32_CS] = initializer_for_that_selector.

(На самом деле младшие 2 бита селектора, то есть значение регистра сегмента, являются текущим уровнем привилегий. Поэтому GDT_ENTRY_DEFAULT_USER_CS должно быть `__USER_CS >> 2.)

mov ds, eax вызывает аппаратное обеспечение дляиндексировать GDT, а не линейный поиск соответствующих данных в памяти!

Формат данных GDT:

Вы просматриваете исходный код x86-64 Linux, поэтому ядро ​​будет работать в длинном режиме, незащищенный режим.Мы можем сказать, потому что есть отдельные записи для USER_CS и USER32_CS.У дескриптора сегмента 32-битного кода будет очищен бит L.Текущее описание сегмента CS - это то, что переводит процессор x86-64 в режим 32-разрядного сжатия по сравнению с 64-разрядным длинным режимом.Чтобы ввести 32-битное пространство пользователя, iret или sysret установит CS: RIP в 32-битный селектор сегмента пользовательского режима.

I думаю, вы также можетеиметь процессор в режиме 16-разрядного сжатия (например, режим сжатия не в реальном режиме, но размер операнда и размер адреса по умолчанию равны 16).Linux, однако, этого не делает.

В любом случае, как объяснено в https://wiki.osdev.org/Global_Descriptor_Table и Сегментация,

Каждый дескриптор сегмента содержит следующую информацию:

  • Базовый адрес сегмента
  • Размер операции по умолчанию в сегменте (16-битный / 32-битный)
  • Уровень привилегий дескриптора (кольцо 0 ->Кольцо 3)
  • Степень детализации (предел сегмента в байтах / 4 КБ)
  • Лимит сегмента (максимальное допустимое смещение внутри сегмента)
  • Наличие сегмента (Isприсутствует или нет)
  • тип дескриптора (0 = система; 1 = код / ​​данные)
  • тип сегмента (код / ​​данные / чтение / запись / доступ / доступ / соответствие / несоответствие/ Expand-Up / Expand-Down)

Это дополнительные биты.Меня не особо интересует, какие биты какие, потому что (я думаю, я) понимаю картину высокого уровня того, для чего нужны разные записи GDT и что они делают, не вдаваясь в детали того, как это на самом деле кодируется.

Но если вы посмотрите руководства по x86 или вики osdev и определения этих макросов init, вы обнаружите, что они приводят к записи GDT с битом L, установленным для сегментов 64-битного кода, очищенным для 32-битногосегменты кода.И, очевидно, тип (код и данные) и уровень привилегий различаются.

...