Ответ 1
Следующее обсуждение основано на 32-битном ARM Linux, а версия исходного кода ядра - 3,9
Все ваши вопросы можно решить, если вы пройдете процедуру настройки начальной таблицы страниц (которая будет перезаписана позже функцией paging_init
) и включением MMU.
Когда ядро запускается сначала загрузчиком, функция Assembly stext
(в arch\arm\kernel\head.s) является первой выполняемой функцией. Обратите внимание, что MMU еще не включен в данный момент.
Помимо прочего, двумя заданиями импорта, выполняемыми этой функцией stext
, являются:
- создайте начальную страницу (которая будет перезаписана позже
функция
paging_init
) - включить MMU
- перейти к части кода инициализации ядра и продолжить работу
Прежде чем вникать в ваши вопросы, знать:
- Прежде чем включить MMU, каждый адрес, выданный процессором, физический адрес
- После включения MMU каждый адрес, выданный CPU, виртуальный адрес
- Перед включением MMU следует настроить правильную таблицу страниц, иначе ваш код просто "сбрасывается".
- По соглашению, ядро Linux использует более высокую часть виртуального адреса на 1 ГБ, а пользовательская земля использует более низкую часть 3 ГБ
Теперь сложная часть:
Первый трюк: использование независимого от позиции кода.
Сетчатая функция stext связана с адресом "PAGE_OFFSET + TEXT_OFFSET
" (0xCxxxxxxx), который является виртуальным адресом, однако, поскольку MMU еще не включен, фактический адрес, где работает stext-функция сборки, - "PHYS_OFFSET + TEXT_OFFSET
" ( фактическое значение зависит от вашего фактического оборудования), который является физическим адресом.
Итак, вот что: программа функции stext
"думает", что она работает в адресе, таком как 0xCxxxxxxx, но фактически работает в адресе (0x00000000 + some_offeset) (скажем, ваше оборудование настраивает 0x00000000 в качестве отправной точки ОЗУ). Поэтому, прежде чем включать MMU, необходимо тщательно написать код сборки, чтобы убедиться, что во время процедуры выполнения ничего не получается. На самом деле используется техник, называемый позиционно-независимым кодом (PIC).
Чтобы пояснить выше, я извлекаю несколько фрагментов кода сборки:
ldr r13, =__mmap_switched @ address to jump to after MMU has been enabled
b __enable_mmu @ jump to function "__enable_mmu" to turn on MMU
Обратите внимание, что приведенная выше инструкция "ldr" представляет собой псевдо-инструкцию, которая означает "получить (виртуальный) адрес функции __mmap_switched и поместить его в r13"
И функция __enable_mmu в свою очередь вызывает функцию __turn_mmu_on: (Обратите внимание, что я удалил несколько инструкций из функции __turn_mmu_on, которые являются важными инструкциями функции, но не наших интересов)
ENTRY(__turn_mmu_on)
mcr p15, 0, r0, c1, c0, 0 @ write control reg to enable MMU====> This is where MMU is turned on, after this instruction, every address issued by CPU is "virtual address" which will be translated by MMU
mov r3, r13 @ r13 stores the (virtual) address to jump to after MMU has been enabled, which is (0xC0000000 + some_offset)
mov pc, r3 @ a long jump
ENDPROC(__turn_mmu_on)
Второй трюк: идентичное отображение при настройке начальной таблицы страниц перед включением MMU. Более конкретно, один и тот же диапазон адресов, в котором работает код ядра, отображается дважды.
- Первое отображение, как и ожидалось, отображает диапазон адресов 0x00000000 (опять же, этот адрес зависит от конфигурации оборудования) через (0x00000000 + смещение) до 0xCxxxxxxx через (0xCxxxxxxx + смещение)
- Второе отображение, интересно, отображает диапазон адресов 0x00000000 через (0x00000000 + смещение) к себе (то есть: 0x00000000 → (0x00000000 + смещение))
Зачем это делать?
Помните, что до включения MMU каждый адрес, выданный CPU, является физическим адресом (начиная с 0x00000000) и после включения MMU каждый адрес, выданный CPU, является виртуальным адресом (начиная с 0xC0000000).
Поскольку ARM является структурой конвейера, на момент включения MMU в ARM-канале все еще есть инструкции, которые используют (физические) адреса, которые генерируются процессором до включения MMU! Чтобы избежать этих инструкций, чтобы взорваться, необходимо настроить идентичное сопоставление, чтобы удовлетворить их.
Теперь вернемся к вашим вопросам:
- В это время, после включения таблицы страниц, пространство ядра по-прежнему составляет 1 ГБ (от 0xC0000000 до 0xFFFFFFFF)?
A: Наверное, вы имеете в виду включение MMU. Ответ: да, пространство ядра составляет 1 ГБ (на самом деле он также занимает несколько мегабайт ниже 0xC0000000, но это нас не интересует)
- И в таблицах страниц процесса ядра отображаются только записи таблицы страниц (PTE) в диапазоне от 0xC0000000 - 0xFFFFFFFF?. ПТ отсутствуют этого диапазона не будет отображаться, потому что код ядра никогда не прыгнет туда?
A: Хотя ответ на этот вопрос довольно сложный, поскольку он включает в себя множество подробностей относительно конкретных конфигураций ядра.
Чтобы полностью ответить на этот вопрос, вам нужно прочитать часть исходного кода ядра, которая устанавливает начальную таблицу страниц (функция сборки __create_page_tables
) и функцию, которая устанавливает итоговую таблицу страниц (функция C paging_init).
Проще говоря, в ARM есть два уровня таблицы страниц, первая таблица страниц - PGD, которая занимает 16 КБ. Ядро сначала возвращает этот PGD во время процесса инициализации и выполняет начальное отображение в функции сборки __create_page_tables
. В функции __create_page_tables
отображается только малая часть адресного пространства.
После этого окончательная таблица страниц настраивается в функции paging_init
, и в этой функции отображается большая часть адресного пространства. Скажем, если у вас только 512M RAM, для большинства распространенных конфигураций эта 512M-RAM будет отображать по разделу кода ядра по разделам (1 секция - 1 МБ). Если ваша оперативная память достаточно велика (скажем, 2 ГБ), будет отображаться только одна часть вашей ОЗУ.
(Я остановлюсь здесь, потому что слишком много деталей относительно Вопроса 2)
- Сопоставление адреса до и после включения таблицы страниц одинаково?
A: Я думаю, что я уже ответил на этот вопрос в своем объяснении "Второй трюк: идентичное сопоставление при настройке начальной таблицы страниц перед включением MMU".
4. Таблица страниц в пространстве ядра является глобальной и будет весь процесс в системе, включая пользовательский процесс?
A: Да и нет. Да, потому что все процессы имеют одну и ту же копию (содержимое) таблицы страниц ядра (более высокая часть 1 ГБ). Нет, потому что каждый процесс использует свою собственную память 16 КБ для хранения таблицы страниц ядра (хотя содержимое таблицы страниц для более высокой части 1 ГБ идентично для каждого процесса).
5. Этот механизм одинаковый для x86 32bit и ARM?
В разных архитектурах используется другой механизм