|
|
Процесс загрузки ядра. Часть 3.
|
|
|
================================================================================
|
|
|
|
|
|
Инициализация видеорежима и переход в защищённый режим
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
Это третья часть серии `Процесса загрузки ядра`. В предыдущей [части](linux-bootstrap-2.md#kernel-booting-process-part-2) мы остановились прямо перед вызовом функции `set_video` из [main.c](https://github.com/torvalds/linux/blob/master/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/master/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=<mode>
|
|
|
<mode> here is either an integer (in C notation, either
|
|
|
decimal, octal, or hexadecimal) or one of the strings
|
|
|
"normal" (meaning 0xFFFF), "ext" (meaning 0xFFFE) or "ask"
|
|
|
(meaning 0xFFFD). This value should be entered into the
|
|
|
vid_mode field, as it is used by the kernel before the command
|
|
|
line is parsed.
|
|
|
```
|
|
|
|
|
|
Таким образом, мы можем добавить параметр `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/master/arch/x86/boot/boot.h#L199):
|
|
|
|
|
|
```C
|
|
|
#define RESET_HEAP() ((void *)( HEAP = _end ))
|
|
|
```
|
|
|
|
|
|
Если вы читали вторую часть, то помните, что мы инициализировали кучу с помощью функции [`init_heap`](https://github.com/torvalds/linux/blob/master/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/master/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/master/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 (немаскируемое прерывание).
|
|
|
|
|
|
Прерывание является сигналом, который отправляется ЦПУ от аппаратного или программного обеспечения.
|
|
|
После получения сигнала, ЦПУ приостанавливает текущую последовательность команд, сохраняет своё состояние и передаёт управление обработчику прерываний. После того, как обработчик прерывания закончил свою работу, он передаёт управление прерванной инструкции. Немаскируемые прерывания (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/master/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:gs` с помощью функции `wrfs32`, совершаем задержку в 1 мс, а затем читаем значение из регистра `GS` по адресу `A20_TEST_ADDR+0x10`. Если это не ноль, то линия A20 уже включена. Если линия A20 отключена, мы пытаемся включить её с помощью других методов, которые вы можете найти в `a20.c`. Например, с помощью вызова BIOS прерывания `0x15` с `AH=0x2041` и т.д.
|
|
|
|
|
|
Если функция `enabled_a20` завершается неудачей, выводится сообщение об ошибке и вызывается функция `die`. Вы можете вспомнить её из первого файла исходного кода, где мы начали - [arch/x86/boot/header.S](https://github.com/torvalds/linux/blob/master/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 <stdio.h>
|
|
|
|
|
|
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, а предел (размер сегмента) равен `0xffff` (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, отключили прерывания и теперь можем переключить ЦПУ в защищённый режим. Последний шаг - вызов функции `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/master/arch/x86/boot/pmjump.S#L26). Она получает два параметра:
|
|
|
|
|
|
* адрес точки входа в защищённый режим
|
|
|
* адрес `boot_params`
|
|
|
|
|
|
Давайте заглянем внутрь `protected_mode_jump`. Как я уже писал выше, вы можете найти его в `arch/x86/boot/pmjump.S`. Первый параметр находится в регистре `eax`, второй в `edx`.
|
|
|
|
|
|
В первую очередь мы помещаем адрес `boot_params` в регистр `esi` и адрес регистра сегмента кода `cs` (0x1000) в `bx`. Далее мы сдвигаем `bx` на 4 бита и добавляем к нему адрес метки `2` (после этого мы будем иметь физический адрес метки `2` в `bx`) и переходим к метке `1`. Далее мы помещаем сегмент данных и сегмент состояния задачи в регистры `cs` и `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` - смещение сегмента
|
|
|
* `__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. В следующей части мы увидим настройки режима видео и остальные подготовки перед переходом в защищённый режим и непосредственно переход в него.
|
|
|
|
|
|
Если у вас есть вопросы или предложения, пишите мне в твиттер [0xAX](https://twitter.com/0xAX).
|
|
|
|
|
|
**Пожалуйста, имейте в виду, что английский - не мой родной язык, и я очень извиняюсь за возможные неудобства. Если вы найдёте какие-нибудь ошибки, пожалуйста, пришлите pull request в [linux-insides](https://github.com/0xAX/linux-internals).**
|
|
|
|
|
|
Ссылки
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
* [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)
|
|
|
|