Процесс загрузки ядра. Часть 3. ================================================================================ Инициализация видеорежима и переход в защищённый режим -------------------------------------------------------------------------------- Это третья часть серии `Процесса загрузки ядра`. В предыдущей [части](linux-bootstrap-2.md#kernel-booting-process-part-2) мы остановились прямо перед вызовом функции `set_video` из [main.c](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/main.c#L181). В этой части мы увидим: - инициализацию видеорежима в коде настройки ядра, - подготовку, сделанную перед переключением в защищённый режим, - переход в защищённый режим **ПРИМЕЧАНИЕ:** если вы ничего не знаете о защищённом режиме, вы можете найти некоторую информацию о нём в предыдущей [части](linux-bootstrap-2.md#protected-mode). Также есть несколько [ссылок](linux-bootstrap-2.md#links), которые могут вам помочь. Как я уже писал ранее, мы начнём с функции `set_video`, которая определена в [arch/x86/boot/video.c](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/video.c#L315). Как мы можем видеть, она начинает работу с получения видеорежима из структуры `boot_params.hdr`: ```C u16 mode = boot_params.hdr.vid_mode; ``` которую мы заполнили в функции `copy_boot_params` (вы можете прочитать об этом в предыдущем посте). `vid_mode` является обязательным полем, которое заполняется загрузчиком. Вы можете найти информацию об этом в протоколе загрузки ядра: ``` Offset Proto Name Meaning /Size 01FA/2 ALL vid_mode Video mode control ``` Как мы можем прочесть из протокола загрузки ядра Linux: ``` vga= может быть либо целочисленным значением (в C-нотации, десятичной, восьмеричной или шестнадцатеричной), либо одной из строк: "normal" (означает 0xFFFF), "ext" (означает 0xFFFE) или "ask" (означает 0xFFFD). Это значение должно быть введено в поле vid_mode field, так как оно используется ядром до парсинга командной строки. ``` Таким образом, мы можем добавить параметр `vga` в конфигурационный файл GRUB (или любого другого загрузчика) и он передаст его в командную строку ядра. Как говорится в описании, этот параметр может иметь разные значения. Например, это может быть целым числом `0xFFFD` или `ask`. Если передать `ask` в `vga`, вы увидите примерно такое меню: ![video mode setup menu](http://oi59.tinypic.com/ejcz81.jpg) которое попросит выбрать видеорежим. Мы посмотрим на его реализацию, но перед этим рассмотрим некоторые другие вещи. Типы данных ядра -------------------------------------------------------------------------------- Ранее мы видели определения различных типов данных в коде настройки ядра, таких как `u16` и т.д. Давайте взглянем на несколько типов данных, предоставляемых ядром: | Тип | char | short | int | long | u8 | u16 | u32 | u64 | |--------|------|-------|-----|------|----|-----|-----|-----| | Размер | 1 | 2 | 4 | 8 | 1 | 2 | 4 | 8 | Во время чтения исходного кода ядра вы будете часто встречать эти типы, так что было бы неплохо запомнить их. API кучи -------------------------------------------------------------------------------- После того как мы получим `vid_mode` из `boot_params.hdr` в функции `set_video`, мы можем видеть вызов `RESET_HEAP`. `RESET_HEAP` представляет собой макрос, определённый в [boot.h](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/boot.h#L199): ```C #define RESET_HEAP() ((void *)( HEAP = _end )) ``` Если вы читали вторую часть, то помните, что мы инициализировали кучу с помощью функции [`init_heap`](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/main.c#L116). У нас есть несколько полезных функций для кучи, которые определены в `boot.h`: ```C #define RESET_HEAP() ``` Как мы видели чуть выше, он сбрасывает кучу, установив переменную `HEAP` в `_end`, где `_end` просто `extern char _end[];` Следующий макрос - `GET_HEAP`: ```C #define GET_HEAP(type, n) \ ((type *)__get_heap(sizeof(type),__alignof__(type),(n))) ``` предназначен для выделения кучи. Он вызывает внутреннюю функцию `__get_heap` с тремя параметрами: * размер типа данных, который должен быть выделен * `__alignof__(type)` определяет, как переменные этого типа должны быть выровнены * `n` определяет сколько элементов нужно выделить Реализация `__get_heap`: ```C static inline char *__get_heap(size_t s, size_t a, size_t n) { char *tmp; HEAP = (char *)(((size_t)HEAP+(a-1)) & ~(a-1)); tmp = HEAP; HEAP += s*n; return tmp; } ``` В дальнейшем мы увидим её использование, что-то вроде: ```C saved.data = GET_HEAP(u16, saved.x * saved.y); ``` Давайте попробуем понять принцип работы `__get_heap`. Мы видим, что `HEAP` (который равен `_end` после `RESET_HEAP()`) является адресом выровненной памяти в соответствии с параметром `a`. После этого мы сохраняем адрес памяти `HEAP` в переменную `tmp`, перемещаем `HEAP` в конец выделенного блока и возвращаем `tmp`, которая является начальным адресом выделенной памяти. И последняя функция: ```C static inline bool heap_free(size_t n) { return (int)(heap_end - HEAP) >= (int)n; } ``` которая вычитает значение указателя `HEAP` из `heap_end` (мы вычисляли это в предыдущей [части](linux-bootstrap-2.md)) и возвращает 1, если имеется достаточно памяти для `n`. На этом всё. Теперь у нас есть простой API для кучи и можем перейти к настройке видеорежима. Настройка видеорежима -------------------------------------------------------------------------------- Теперь мы можем перейти непосредственно к инициализации видеорежима. Мы остановились на вызове `RESET_HEAP()` в функции `set_video`. Далее идёт вызов функции `store_mode_params`, которая сохраняет параметры видеорежима в структуре `boot_params.screen_info`, определённой в [include/uapi/linux/screen_info.h](https://github.com/0xAX/linux/blob/master/include/uapi/linux/screen_info.h). Если мы посмотрим на функцию `store_mode_params`, то увидим, что она начинается с вызова `store_cursor_position`. Как вы можете понять из названия функции, она получает информацию о курсоре и сохраняет её. В первую очередь `store_cursor_position` инициализирует две переменные, которые имеют тип `biosregs` с `AH = 0x3`, и вызывает BIOS прерывание `0x10`. После того как прерывание успешно выполнено, она возвращает строку и столбец в регистрах `DL` и `DH`. Строка и столбец будут сохранены в полях `orig_x` и `orig_y` структуры `boot_params.screen_info`. После выполнения `store_cursor_position` вызывается функция `store_video_mode`. Она просто получает текущий видеорежим и сохраняет его в `boot_params.screen_info.orig_video_mode`. Далее она проверяет текущий видеорежим и устанавливает `video_segment`. После того как BIOS передаёт контроль в загрузочный сектор, для видеопамяти выделяются следующие адреса: ``` 0xB000:0x0000 32 Кб Видеопамять для монохромного текста 0xB800:0x0000 32 Кб Видеопамять для цветного текста ``` Таким образом, мы устанавливаем переменную `video_segment` в `0xb000`, если текущий видеорежим MDA, HGC, или VGA в монохромном режиме, и в `0xb800`, если текущий видеорежим цветной. После настройки адреса видеофрагмента, размер шрифта должен быть сохранён в `boot_params.screen_info.orig_video_points`: ```C set_fs(0); font_size = rdfs16(0x485); boot_params.screen_info.orig_video_points = font_size; ``` В первую очередь мы устанавливаем регистр `FS` в 0 с помощью функции `set_fs`. В предыдущей части мы уже видели такие функции, как `set_fs`. Все они определены в [boot.h](https://github.com/0xAX/linux/blob/master/arch/x86/boot/boot.h). Далее мы читаем значение, которое находится по адресу `0x485` (эта область памяти используется для получения размера шрифта) и сохраняет размер шрифта в `boot_params.screen_info.orig_video_points`. ``` x = rdfs16(0x44a); y = (adapter == ADAPTER_CGA) ? 25 : rdfs8(0x484)+1; ``` Далее мы получаем количество столбцов по адресу `0x44a`, и строк по адресу `0x484` и сохраняем их в `boot_params.screen_info.orig_video_cols` и `boot_params.screen_info.orig_video_lines`. После этого выполнение `store_mode_params` завершается. Далее мы видим функцию `save_screen`, которая просто сохраняет содержимое экрана в куче. Эта функция собирает все данные, которые мы получили в предыдущей функции, такие как количество строк и столбцов и т.д, и сохраняет их в структуре `saved_screen`: ```C static struct saved_screen { int x, y; int curx, cury; u16 *data; } saved; ``` Затем она проверяет, есть ли свободное место в куче: ```C if (!heap_free(saved.x*saved.y*sizeof(u16)+512)) return; ``` и если места в куче достаточно, выделяет его и сохраняет в нём `saved_screen`. Следующий вызов - `probe_cards(0)` из [arch/x86/boot/video-mode.c](https://github.com/0xAX/linux/blob/master/arch/x86/boot/video-mode.c#L33). Она проходит по всем `video_cards` и собирает количество режимов, предоставляемых картой. И вот здесь интересный момент. Мы можем видеть цикл: ```C for (card = video_cards; card < video_cards_end; card++) { /* Здесь собираем количество режимов */ } ``` но `video_cards` нигде не объявлен. Ответ прост: каждый видеорежим, представленный в x86-коде настройки ядра, определён следующим образом: ```C static __videocard video_vga = { .card_name = "VGA", .probe = vga_probe, .set_mode = vga_set_mode, }; ``` где `__videocard` - макрос: ```C #define __videocard struct card_info __attribute__((used,section(".videocards"))) ``` который определяет структуру `card_info`: ```C struct card_info { const char *card_name; int (*set_mode)(struct mode_info *mode); int (*probe)(void); struct mode_info *modes; int nmodes; int unsafe; u16 xmode_first; u16 xmode_n; }; ``` которая находится в сегменте `.videocards`. Давайте посмотрим на скрипт компоновщика [arch/x86/boot/setup.ld](https://github.com/0xAX/linux/blob/master/arch/x86/boot/setup.ld), в котором мы можем найти: ``` .videocards : { video_cards = .; *(.videocards) video_cards_end = .; } ``` Это значит, что `video_cards` - просто адрес в памяти, и все структуры `card_info` размещаются в этом сегменте. Это также означает, что все структуры `card_info` размещаются между `video_cards` и `video_cards_end`, и мы можем воспользоваться этим, чтобы пройтись по ним в цикле. После выполнения `probe_cards` у нас есть все структуры `static __videocard video_vga` с заполненными `nmodes` (число видеорежимов). После завершения выполнения `probe_cards`, мы переходим в главный цикл функции `set_video`. Это бесконечный цикл, который пытается установить видеорежим с помощью функции `set_mode` и выводит меню, если установлен флаг `vid_mode=ask` командной строки ядра или если видеорежим не определён. Функция `set_mode` определена в [video-mode.c](https://github.com/0xAX/linux/blob/master/arch/x86/boot/video-mode.c#L147) и принимает только один параметр - `mode`, который определяет количество видеорежимов (мы получили его из меню или в начале `setup_video`, из заголовка настройки ядра). `set_mode` проверяет `mode` и вызывает функцию `raw_set_mode`. `raw_set_mode` вызывает `set_mode` для выбранной карты, т.е. `card->set_mode(struct mode_info*)`. Мы можем получить доступ к этой функции из структуры `card_info`. Каждый видеорежим определяет эту структуру со значениями, заполненными в зависимости от режима видео (например, для `vga` это функция `video_vga.set_mode`. См. выше пример структуры `card_info` для `vga`). `video_vga.set_mode` является `vga_set_mode`, который проверяет VGA-режим и вызывает соответствующую функцию: ```C static int vga_set_mode(struct mode_info *mode) { vga_set_basic_mode(); force_x = mode->x; force_y = mode->y; switch (mode->mode) { case VIDEO_80x25: break; case VIDEO_8POINT: vga_set_8font(); break; case VIDEO_80x43: vga_set_80x43(); break; case VIDEO_80x28: vga_set_14font(); break; case VIDEO_80x30: vga_set_80x30(); break; case VIDEO_80x34: vga_set_80x34(); break; case VIDEO_80x60: vga_set_80x60(); break; } return 0; } ``` Каждая функция, которая устанавливает видеорежим, просто вызывает BIOS прерывание `0x10` с определённым значением в регистре `AH`. После того как мы установили видеорежим, мы передаём его в `boot_params.hdr.vid_mode`. Далее вызывается `vesa_store_edid`. Эта функция сохраняет информацию о [EDID](https://en.wikipedia.org/wiki/Extended_Display_Identification_Data) (**E**xtended **D**isplay **I**dentification **D**ata) для использования ядром. После этого снова вызывается `store_mode_params`. И наконец, если установлен `do_restore`, экран восстанавливается в предыдущее состояние. Сделав это, мы завершаем настройку видеорежима и мы можем переключится в защищённый режим. Последняя подготовка перед переходом в защищённый режим -------------------------------------------------------------------------------- Мы можем видеть последний вызов функции - `go_to_protected_mode` - в [main.c](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/main.c#L184). Как говорится в комментарии: `Do the last things and invoke protected mode`, так что давайте посмотрим на эти последние вещи и перейдём в защищённый режим. Функция `go_to_protected_mode` определена в [arch/x86/boot/pm.c](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/pm.c#L104). Она содержит функции, которые совершают последние приготовления, прежде чем мы сможем перейти в защищённый режим, так что давайте посмотрим на них и попытаться понять, что они делают и как это работает. Во-первых, это вызов функции `realmode_switch_hook` в `go_to_protected_mode`. Эта функция вызывает хук переключения режима реальных адресов, если он присутствует, и выключает [NMI](http://en.wikipedia.org/wiki/Non-maskable_interrupt). Хуки используются, если загрузчик работает во "враждебной" среде. Вы можете прочитать больше о хуках в [протоколе загрузки](https://www.kernel.org/doc/Documentation/x86/boot.txt) (см. **ADVANCED BOOT LOADER HOOKS**). Хук `realmode_switch` представляет собой указатель на 16-битную удалённую подпрограмму режима реальных адресов, которая отключает немаскируемые прерывания. После проверки хука `realmode_switch`, происходит выключение Non-Maskable Interrupts (NMI): ```assembly asm volatile("cli"); outb(0x80, 0x70); /* Выключение NMI */ io_delay(); ``` Первой вызывается ассемблерная инструкция `cli`, которая очищает флаг прерывания (`IF`). После этого внешние прерывания отключены. Следующая строка отключает NMI (немаскируемые прерывания). Прерывание является сигналом, который отправляется CPU от аппаратного или программного обеспечения. После получения сигнала, CPU приостанавливает текущую последовательность команд, сохраняет своё состояние и передаёт управление обработчику прерываний. После того как обработчик прерывания закончил свою работу, он передаёт управление прерванной инструкции. Немаскируемые прерывания (NMI) - это прерывания, которые обрабатываются всегда, независимо от запретов на другие прерывания. Их нельзя игнорировать, и, как правило, они используются для подачи сигнала о невосстанавливаемых аппаратных ошибок. Сейчас мы не будем погружаться в детали прерываний, но обсудим это в следующих постах. Давайте вернёмся к коду. Мы видим, что вторая строка пишет байт `0x80` (отключённый бит) в `0x70` (регистр CMOS Address). После этого происходит вызов функции `io_delay`. `io_delay` вызывает небольшую задержку и выглядит следующим образом: ```C static inline void io_delay(void) { const u16 DELAY_PORT = 0x80; asm volatile("outb %%al,%0" : : "dN" (DELAY_PORT)); } ``` Для вывода любого байта в порт `0x80` необходима задержка в 1 мкс. Таким образом, мы можем записать любое значение (в нашем случае значение из регистра `AL`) в порт `0x80`. После задержки, функция `realmode_switch_hook` завершает выполнение и мы можем перейти к следующей функции. Следующая функция - `enable_a20` - включает [линию A20](http://en.wikipedia.org/wiki/A20_line). Она определена в [arch/x86/boot/a20.c](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/a20.c) и совершает попытку включения шлюза адресной линии A20 различными методами. Первым из них является функция `a20_test_short`, которая проверят, является ли A20 включённой или нет с помощью функции `a20_test`: ```C static int a20_test(int loops) { int ok = 0; int saved, ctr; set_fs(0x0000); set_gs(0xffff); saved = ctr = rdfs32(A20_TEST_ADDR); while (loops--) { wrfs32(++ctr, A20_TEST_ADDR); io_delay(); /* Serialize and make delay constant */ ok = rdgs32(A20_TEST_ADDR+0x10) ^ ctr; if (ok) break; } wrfs32(saved, A20_TEST_ADDR); return ok; } ``` В первую очередь мы устанавливаем регистр `FS` в `0x0000` и регистр `GS` в `0xffff`. Далее мы читаем значение по адресу `A20_TEST_ADDR` (`0x200`) и сохраняем его в переменную `saved` и `ctr`. После этого мы записываем обновлённое значение `ctr` в `fs:A20_TEST_ADDR` или `fs:0x200` с помощью функции `wrfs32`, совершаем задержку в 1 мс, а затем читаем значение из регистра `GS` по адресу `A20_TEST_ADDR+0x10`. В случае, если линия A20 отключена, адрес будет перекрыт, в противном случае, если он не равен нулю, линия A20 уже включена. Если линия A20 отключена, мы пытаемся включить её с помощью других методов, которые вы можете найти в `a20.c`. Например, это может быть сделано с помощью вызова BIOS прерывания `0x15` с `AH=0x2041` и т.д. Если функция `enabled_a20` завершается неудачей, выводится сообщение об ошибке и вызывается функция `die`. Вы можете вспомнить её из первого файла исходного кода, откуда мы начали - [arch/x86/boot/header.S](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/header.S): ```assembly die: hlt jmp die .size die, .-die ``` После того как шлюз линии A20 успешно включён, вызывается функция `reset_coprocessor`: ```C outb(0, 0xf0); outb(0, 0xf1); ``` Она очищает математический сопроцессор путём записи `0` в `0xf0`, а затем сбрасывает его при помощи записи `0` в `0xf1`. После этого вызывается функция `mask_all_interrupts`: ```C outb(0xff, 0xa1); /* Маскирует все прерывания на вторичном PIC */ outb(0xfb, 0x21); /* Маскирует все, кроме каскада на первичном PIC */ ``` Она маскирует все прерывания на вторичном PIC (программируемый контроллер прерываний) и первичном PIC, за исключением IRQ2 на первичном PIC. И теперь, после всех приготовлений, мы можем увидеть фактический переход в защищённый режим. Настройка таблицы векторов прерываний -------------------------------------------------------------------------------- Теперь мы настраиваем таблицу векторов прерываний (IDT). Функция `setup_idt`: ```C static void setup_idt(void) { static const struct gdt_ptr null_idt = {0, 0}; asm volatile("lidtl %0" : : "m" (null_idt)); } ``` настраивает таблицу векторов прерываний (описывает обработчики прерываний и т.д). В настоящее время IDT не установлена (мы увидим это позже), сейчас мы просто загрузили IDT инструкцией `lidtl`. `null_idt` содержит адрес и размер IDT, но сейчас они равны нулю. `null_idt` является структурой `gdt_ptr` и определена следующим образом: ```C struct gdt_ptr { u16 len; u32 ptr; } __attribute__((packed)); ``` где мы можем видеть 16-битную длину (`len`) IDT и 32-битный указатель на неё (более подробно о IDT и прерываниях вы увидите в следующих постах). ` __attribute__((packed))` означает, что размер `gdt_ptr` является минимальным требуемым размером. Таким образом, размер `gdt_ptr` должен быть равен 6 байтам или 48 битам. (Далее мы будем загружать указатель на `gdt_ptr` в регистр `GDTR` и вы, возможно, помните из предыдущего поста, что это 48-битный регистр). Настройка глобальной таблицы дескрипторов -------------------------------------------------------------------------------- Далее идёт настройка глобальной таблицы дескрипторов (GDT). Мы можем видеть функцию `setup_gdt`, которая настраивает GDT (вы можете прочитать про это в посте [Процесс загрузки ядра. Часть 2.](linux-bootstrap-2.md#protected-mode)). В этой функции определён массив `boot_gdt`, который содержит определение трёх сегментов: ```C static const u64 boot_gdt[] __attribute__((aligned(16))) = { [GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, 0, 0xfffff), [GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, 0, 0xfffff), [GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, 4096, 103), }; ``` для получения кода, данных и TSS (Task State Segment, сегмент состояния задачи). В данный момент мы не будем использовать сегмент состояния задачи. Как мы можем видеть в строке комментария, он был добавлен специально для Intel VT ([здесь](https://github.com/torvalds/linux/commit/88089519f302f1296b4739be45699f06f728ec31) вы можете найти коммит, который описывает его). Давайте посмотри на `boot_gdt`. Прежде всего отметим, что она имеет атрибут `__attribute__((aligned(16)))`. Это означает, что структура будет выровнена по 16 байтам. Давайте посмотрим на простой пример: ```C #include struct aligned { int a; }__attribute__((aligned(16))); struct nonaligned { int b; }; int main(void) { struct aligned a; struct nonaligned na; printf("Not aligned - %zu \n", sizeof(na)); printf("Aligned - %zu \n", sizeof(a)); return 0; } ``` Технически, структура, которая содержит одно поле типа `int`, должна иметь размер 4 байта, но для `aligned` структуры потребуется 16 байт для хранения в памяти: ``` $ gcc test.c -o test && test Not aligned - 4 Aligned - 16 ``` Здесь `GDT_ENTRY_BOOT_CS` имеет индекс - 2, `GDT_ENTRY_BOOT_DS` является `GDT_ENTRY_BOOT_CS + 1` и т.д. Он начинается с 2, поскольку первый является обязательным нулевым дескриптором (индекс - 0), а второй не используется (индекс - 1). `GDT_ENTRY` - это макрос, который принимает флаги, базовый адрес, предел и создаёт запись в GDT. Для примера посмотрим на запись сегмента кода. `GDT_ENTRY` принимает следующие значения: * базовый адрес - `0` * предел - `0xfffff` * флаги - `0xc09b` Что это значит? Базовый адрес сегмента равен 0, а предел (размер сегмента) равен `0xfffff` (1 Мб). Давайте посмотрим на флаги. В двоичном виде значение `0xc09b` будет выглядеть следующим образом: ``` 1100 0000 1001 1011 ``` Попробуем понять, что означает каждый бит. Мы пройдёмся по всем битам слева направо * 1 - (G) бит гранулярности * 1 - (D) если равен 0 - 16-битный сегмент; 1 - 32-битный сегмент * 0 - (L) если 1 - выполняется в 64-битном режиме * 0 - (AVL) доступен для использования системным ПО * 0000 - 4 бита предела в 19:16 бит в дескрипторе * 1 - (P) присутствие сегмента в памяти * 00 - (DPL) - уровень привилегий, 0 является высшей привилегией * 1 - (S) сегмент кода или данных, не системный сегмент * 101 - тип сегмента и виды доступа к нему (чтение, выполнение) * 1 - бит обращения Вы можете прочитать больше о каждом бите в предыдущем [посте](linux-bootstrap-2.md) или в [документации для разработчиков ПО на архитектуре Intel® 64 и IA-32](http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html). После этого мы получаем длину GDT: ```C gdt.len = sizeof(boot_gdt)-1; ``` Здесь мы получаем размер `boot_gdt` и вычитаем 1 (последний действительный адрес в GDT). Далее получаем указатель на GDT: ```C gdt.ptr = (u32)&boot_gdt + (ds() << 4); ``` Здесь мы просто получаем адрес `boot_gdt` и добавляем его к адресу сегмента данных, сдвинутого влево на 4 бита (не забывайте, что сейчас мы находимся в режиме реальных адресов). И наконец, мы выполняем инструкцию `lgdtl` для загрузки GDT в регистр GDTR: ```C asm volatile("lgdtl %0" : : "m" (gdt)); ``` Фактический переход в защищённый режим -------------------------------------------------------------------------------- Это конец функции `go_to_protected_mode`. Мы загрузили IDT, GDT, отключили прерывания и теперь можем переключить CPU в защищённый режим. Последний шаг - вызов функции `protected_mode_jump` с двумя параметрами: ```C protected_mode_jump(boot_params.hdr.code32_start, (u32)&boot_params + (ds() << 4)); ``` которая определена в [arch/x86/boot/pmjump.S](https://github.com/torvalds/linux/blob/v4.16/arch/x86/boot/pmjump.S#L26). Она получает два параметра: * адрес точки входа в защищённый режим * адрес `boot_params` Давайте заглянем внутрь `protected_mode_jump`. Как я уже писал выше, вы можете найти его в `arch/x86/boot/pmjump.S`. Первый параметр находится в регистре `eax`, второй в `edx`. В первую очередь мы помещаем адрес `boot_params` в регистр `esi` и адрес регистра сегмента кода `cs` в `bx`. Далее мы сдвигаем `bx` на 4 бита и добавляем к нему адрес метки `2` (`(cs << 4) + in_pm32`, физический адрес для "прыжка" после перехода в 32-битный режим) и переходим на метку `1`. После этого `in_pm32` в метке `2` будет перезаписан следующим образом: `(cs << 4) + in_pm32`. Далее мы помещаем сегмент данных и сегмент состояния задачи в регистры `cx` и `di`: ```assembly movw $__BOOT_DS, %cx movw $__BOOT_TSS, %di ``` Как вы можете прочесть выше, `GDT_ENTRY_BOOT_CS` имеет индекс 2 и каждая запись GDT имеет размер 8 байт, поэтому `CS` будет `2 * 8 = 16`, `__BOOT_DS` равен 24 и т.д. Далее мы устанавливаем бит `PE` (Protection Enable) в регистре управления `CR0`: ```assembly movl %cr0, %edx orb $X86_CR0_PE, %dl movl %edx, %cr0 ``` и совершаем длинный переход в защищённый режим: ```assembly .byte 0x66, 0xea 2: .long in_pm32 .word __BOOT_CS ``` где * `0x66` - префикс размера операнда, который позволяет смешивать как 16-битный, так и 32-битный код, * `0xea` - опкод инструкции перехода, * `in_pm32` - смещение сегмента в защищённом режиме, которое имеет значение `(cs << 4) + in_pm32`, полученное из режима реальных адресов * `__BOOT_CS` - сегмент кода, на который мы хотим перейти. После этого мы наконец-то в защищённом режиме: ```assembly .code32 .section ".text32","ax" ``` Давайте посмотрим на первые шаги в защищённом режиме. Прежде всего мы устанавливаем сегмент данных следующим образом: ```assembly movl %ecx, %ds movl %ecx, %es movl %ecx, %fs movl %ecx, %gs movl %ecx, %ss ``` Если вы обратили внимание, то можете вспомнить, что мы сохраняли `$__BOOT_DS` в регистре `cx`. Теперь мы заполнили все сегментные регистры, кроме `cs` (`cs` уже `__BOOT_CS`). Далее мы обнуляем все регистры общего назначения, кроме `eax`: ```assembly xorl %ecx, %ecx xorl %edx, %edx xorl %ebx, %ebx xorl %ebp, %ebp xorl %edi, %edi ``` И в конце переходим к 32-битной точке входа: ``` jmpl *%eax ``` Как вы помните, `eax` содержит адрес 32-битной записи (мы передали его как первый параметр в `protected_mode_jump`). На этом всё. Теперь мы находимся в защищённом режиме и останавливаемся на этой точке входа. Что произойдёт дальше, мы увидим в следующей части. Заключение -------------------------------------------------------------------------------- Это конец третьей части о внутренностях ядра Linux. В следующей части мы рассмотрим первые шаги в защищённом режиме и переход в [long mode](https://en.wikipedia.org/wiki/Long_mode). **От переводчика: пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-либо ошибки или неточности в переводе, пожалуйста, пришлите pull request в [linux-insides-ru](https://github.com/proninyaroslav/linux-insides-ru).** Ссылки -------------------------------------------------------------------------------- * [VGA](http://en.wikipedia.org/wiki/Video_Graphics_Array) * [VESA BIOS Extensions](http://en.wikipedia.org/wiki/VESA_BIOS_Extensions) * [Выравнивание данных](http://en.wikipedia.org/wiki/Data_structure_alignment) * [Немаскируемое прерывание](http://en.wikipedia.org/wiki/Non-maskable_interrupt) * [Линия A20](http://en.wikipedia.org/wiki/A20_line) * [GCC designated inits (назначенные инициализаторы)](https://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Designated-Inits.html) * [Атрибуты типов GCC](https://gcc.gnu.org/onlinedocs/gcc/Type-Attributes.html) * [Предыдущий пост](linux-bootstrap-2.md)