Coding — Загрузка ядра ОС Linux
Рассмотрим как происходит загрузка ядра (рассматривалось ядро версии 2.6.35 и архитектура x86_32), но работу BIOS и загрузчиков, таких как GRUB или GRUB2, мы рассматривать не будем.
Для начала я проиллюстрирую схему организации памяти, приведенную в документации к ядру (linux/Documentation/x86/boot.txt):
К этой схеме время от времени мы будем возвращаться. Итак...
Boot loader или загрузчик — это программа, которая вызывается BIOS для загрузки образа ядра в оперативную память. Образ ядра является точной копией файла расположенного на жестком диске (vmlinuz-version или bzImage). Образ ядра разделен на две части:
Этот участок кода является загрузочным сектором (boot sector), и именно он находится в первых 512 байтах вашего vmlinuz, вы можете это проверить, использовав команду dd if=/boot/vmlinuz-version of=vmlinuz.img bs=512 count=1:
Его задача довольно скромная — вывести пользователю сообщение о том, что загрузка с дискет больше не поддерживается и перезагрузить систему. Загрузочный сектор, в соответствии с приведенной выше схемой памяти, должен располагаться по адресу X, то есть зависит от того, где его расположит загрузчик (qemu расположил загрузочный сектор по адресу 0x10000, от этого адреса я в дальнейшем и буду отталкиваться).
Рассмотрим два поля setup_sects и boot_flag:
Сразу за определением сигнатуры загрузочного сектора следует двухбайтовый прыжок (он явно указывается двумя байтами, иначе ассемблер может сгенерировать здесь 3-байтовый прыжок, который сдвинет все остальные инструкции на неправильное смещение) к адресу start_of_setup, где устанавливается стек и заполняется нулями bss (неинициализированные данные), после чего вызывается функция main.
Прежде чем перейти к функции main, обратим внимание на некоторые поля (между метками _start и start_of_setup), которые содержит header.S (в приведенном выше листинге они отсутствуют):
Итак, была вызвана функция main. Это первая функция, написанная на языке С, которая встречается на пути загрузки ядра. Ее определение находится в файле linux/arch/x86/boot/main.c:
Рассмотрим некоторые из функций:
В функции startup_32 распаковывается (decompress) ядро, а затем осуществляется к нему переход. За распаковку ядра отвечает функция decompress_kernel() (linux/arch/x86/boot/compressed/misc.c). Если во время распаковки ядра не возникло ошибок, то на экране появится надпись «Decompressing Linux... Booting the kernel». Ядро может быть распаковано или по адресу 0x100000, или, в случае, если ядро было собрано с опцией «CONFIG_RELOCATABLE=y», по любому другому адресу выше 1MB (начиная с адреса 0x100000 и выше).
В любом случае осуществляется переход к распакованному ядру, а именно к функции start_kernel(), которая определена в файле linux/init/main.c:
Если вы посмотрите на содержимое функции start_kernel(), то можете заметить, что она вызывает целую серию других функций, которые инициализируют различные подсистемы ядра и структуры данных. Часть функций с коротким описанием представлена в схеме (см. рисунок ниже), поэтому мы сразу перейдем к рассмотрению заключительного этапа — функции rest_init().
Функция rest_init() создает новый поток ядра, который, в конечном итоге вызывает программу пространства пользователя /sbin/init:
Следует иметь ввиду, что выполняются не все вызовы run_init_process() в функции init_post(), так как, если вызов был удачным, то мы из него не возвращаемся. Если все вызовы провалились, то будет выведено сообщение «No init found. Try passing init= option to kernel. See Linux Documentation/init.txt for guidance» функцией panic(). Функция panic() останавливает инициализацию системы. Процесс init получает идентификатор равный 1 (pid = 1). Но init не всегда первый процесс в системе, как было сказано, возможно его не удастся запустить или, что более вероятно в качестве параметра командной строки был задан «init=/bin/sh», то sh получит идентификатор равный 1:
Затем создается еще один поток ядра — khtreadd (он имеет pid = 2) — «...Опосредованно взаимодействуя с помощью определенных API с данным потоком, различные части ядра могут ставить в очередь на создание новые потоки, которые и создает kthreadd...» (о потоках ядра можно прочитать на rflinux.blogspot.com).
Далее вызывается планировщик (с управлением процессов можно ознакомиться на http://welinux.ru/) и функция cpu_idle(). cpu_idle() является «idle» потоком для ядра (cpu_idle() это нулевой процесс, то есть pid = 0) и всегда находится в системе. Она передает управление планировщику, а если нет задач для выполнения, то сама занимает процессор в бесконечном поиске новой задачи.
На этом инициализация ядра заканчивается. Если был успешно запущен процесс init, то он продолжает работу по загрузке системы.
Некоторые ссылки:
Для начала я проиллюстрирую схему организации памяти, приведенную в документации к ядру (linux/Documentation/x86/boot.txt):
К этой схеме время от времени мы будем возвращаться. Итак...
Boot loader или загрузчик — это программа, которая вызывается BIOS для загрузки образа ядра в оперативную память. Образ ядра является точной копией файла расположенного на жестком диске (vmlinuz-version или bzImage). Образ ядра разделен на две части:
- небольшой код, который работает в реальном режиме и загружается ниже барьера в 640K (0x0A0000);
- часть ядра, которая работает в защищенном режиме, загружается после первого мегабайта памяти (0x100000).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
... |
Этот участок кода является загрузочным сектором (boot sector), и именно он находится в первых 512 байтах вашего vmlinuz, вы можете это проверить, использовав команду dd if=/boot/vmlinuz-version of=vmlinuz.img bs=512 count=1:
Его задача довольно скромная — вывести пользователю сообщение о том, что загрузка с дискет больше не поддерживается и перезагрузить систему. Загрузочный сектор, в соответствии с приведенной выше схемой памяти, должен располагаться по адресу X, то есть зависит от того, где его расположит загрузчик (qemu расположил загрузочный сектор по адресу 0x10000, от этого адреса я в дальнейшем и буду отталкиваться).
Рассмотрим два поля setup_sects и boot_flag:
- setup_sects — размер setup кода по количеству 512 байтных секторов. Код в реальном режиме состоит из загрузочного сектора (512 байт) и setup кода. Таким образом, размер всего кода в реальном режиме составляет (setup_sects+1)*512. В файле vmlinuz (bzImage) по этому адресу ((setup_sects+1)*512) располагается начало кода в защищенном режиме.
- boot_flag — содержит значение 0xАА55 (магическое число), то есть является сигнатурой загрузочного сектора.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
... |
Сразу за определением сигнатуры загрузочного сектора следует двухбайтовый прыжок (он явно указывается двумя байтами, иначе ассемблер может сгенерировать здесь 3-байтовый прыжок, который сдвинет все остальные инструкции на неправильное смещение) к адресу start_of_setup, где устанавливается стек и заполняется нулями bss (неинициализированные данные), после чего вызывается функция main.
Прежде чем перейти к функции main, обратим внимание на некоторые поля (между метками _start и start_of_setup), которые содержит header.S (в приведенном выше листинге они отсутствуют):
- header — магическая сигнатура «HdrS» (Header Signature).
-
version — содержит версию используемого протокола (boot protocol):
1
2(gdb) x/1xh 0x10206
0x10206: 0x020a # версия протокола соответственно 2.10 -
kernel_version — содержит указатель на версию ядра (задана строкой):
1
2(gdb) x/7cb 0x133b0
0x133b0: 50 '2' 46 '.' 54 '6' 46 '.' 51 '3' 53 '5' 32 ' ' # 2.6.35 -
type_of_loader — тип загрузчика (LILO, GRUB и т.д.):
1
2(gdb) x/1xb 0x10210
0x10210: 0xb0
Большинство загрузчиков имеют свой идентификатор (ID). Значение 0xTV трактуется следующим образом T — идентификатор загрузчика, V — версия загрузчика. Для данного примера 0xb0 следует: 0xb — загрузчик Qemu, 0x0 — версия 0 (все ID загрузчиков можно посмотреть в файле linux/Documentation/x86/boot.txt). -
code32_start — адрес для перехода в защищенный режим:
1
2(gdb) x/4xb 0x10214
0x10214: 0x00100000 -
ramdisk_image — содержит 32-битный линейный адрес местоположения ramdisk или ramfs:
1
2(gdb) x/1xw 0x10218
0x10218: 0x01f9c000 -
ramdisk_size — содержит размер ramdisk или ramfs:
1
2
3
4(gdb) x/1xw 0x1021c
0x1021c: 0x00053a99 # соответственно 342681 байт
$ ls -l debug
rw-rw-r--. 1 dimm dimm 342681 Aug 7 21:29 debug -
cmd_line_ptr — содержит 32-битный указатель на параметры командной строки:
1
2
3
4
5(gdb) x/1xw 0x10228
0x10228: 0x00020000
(gdb) x/15cb 0x20000
0x20000: 114 'r' 111 'o' 111 'o' 116 't' 61 '=' 47 '/' 100 'd' 101 'e'
0x20008: 118 'v' 47 '/' 115 's' 100 'd' 97 'a' 0 '\000' 0 '\000' -
cmdline_size — максимальная длина аргументов командной строки:
1
2(gdb) x/1xw 0x10238
0x10238: 0x000007ff # соответственно 2047 байт
Итак, была вызвана функция main. Это первая функция, написанная на языке С, которая встречается на пути загрузки ядра. Ее определение находится в файле linux/arch/x86/boot/main.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
void main(void) |
Рассмотрим некоторые из функций:
-
copy_boot_params() — копирует параметры загрузки, определенные в файле header.S («структура» hdr) в структуру boot_params.hdr (определена в файле linux/arch/x86/include/asm/bootparam.h):
memcpy(&boot_params.hdr, &hdr, sizeof hdr);
- init_heap() — устанавливает конец «кучи» равным: heap_end = stack_end = esp — 0x200
- validate_cpu() — данная функция проверяет возможности процессора для работы с данным ядром;
- detect_memory() — функция detect_memory() использует прерывание int 15 и e820 (e801, 88) в качестве значения регистра ax для того, чтобы получить карту адресов памяти (System Adderss Map). В эту карту входит весь RAM (Random Access Memory) и диапазоны адресов физической памяти, зарезервированные BIOS.
Просмотреть эту карту можно при помощи команды dmesg:
Более детальное описание адресов находится в файле /proc/iomem. -
set_video() — устанавливается видео режим. Функция set_video() определена в файле linux/arch/x86/boot/video.c:
1
2
3
4
5
6
7
8
9
10
11
12void set_video(void)
{
...
for (;;) {
if (mode == ASK_VGA)
mode = mode_menu();
if (!set_mode(mode))
break;
printf("Undefined video mode number: %x\n", mode);
mode = ASK_VGA;
...
}
Проверяется, был ли установлен режим опроса (vga=«ask» в качестве параметра командной строки), если да, то выводится меню со списком видео режимов (см. рис. ниже), затем устанавливается выбранный режим. Если в командной строке режим не был задан, то будет использован стандартный 80x25 (vga=«normal»).
-
go_to_protected_mode() — функция определена в файле linux/arch/x86/boot/pm.c и производит заключительные настройки перед переходом в защищенный режим:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23void go_to_protected_mode(void)
{
/* Hook before leaving real mode, also disables interrupts */
realmode_switch_hook();
/* Enable the A20 gate */
if (enable_a20()) {
puts("A20 gate not responding, unable to boot...\n");
die();
}
/* Reset coprocessor (IGNNE#) */
reset_coprocessor();
/* Mask all interrupts in the PIC */
mask_all_interrupts();
/* Actual transition to protected mode... */
setup_idt();
setup_gdt();
protected_mode_jump(boot_params.hdr.code32_start,
(u32)&boot_params + (ds() << 4));
}
- enable_a20() — включает адресную линию A20 для полноценной 32-битной адресации;
- setup_idt()/setup_gdt() — устанавливаются Interrupt Descriptor Table (для реального режима таблица расположена по адресу 0x0, для защищенного режима этот адрес определяется регистром idtr) и Global Descriptor Table.
- protected_mode_jump() — определена в файле linux/arch/x86/boot/pmjump.S и осуществляет переход по адресу, указанному в boot_params.hdr.code32_start (0x100000).
1 2 3 4 5 6 7 8 9 10 11 12 |
ENTRY(startup_32) |
В функции startup_32 распаковывается (decompress) ядро, а затем осуществляется к нему переход. За распаковку ядра отвечает функция decompress_kernel() (linux/arch/x86/boot/compressed/misc.c). Если во время распаковки ядра не возникло ошибок, то на экране появится надпись «Decompressing Linux... Booting the kernel». Ядро может быть распаковано или по адресу 0x100000, или, в случае, если ядро было собрано с опцией «CONFIG_RELOCATABLE=y», по любому другому адресу выше 1MB (начиная с адреса 0x100000 и выше).
В любом случае осуществляется переход к распакованному ядру, а именно к функции start_kernel(), которая определена в файле linux/init/main.c:
1 2 3 4 5 6 7 8 9 |
asmlinkage void __init start_kernel(void) |
Если вы посмотрите на содержимое функции start_kernel(), то можете заметить, что она вызывает целую серию других функций, которые инициализируют различные подсистемы ядра и структуры данных. Часть функций с коротким описанием представлена в схеме (см. рисунок ниже), поэтому мы сразу перейдем к рассмотрению заключительного этапа — функции rest_init().
Функция rest_init() создает новый поток ядра, который, в конечном итоге вызывает программу пространства пользователя /sbin/init:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
static noinline void __init_refok rest_init(void) |
Следует иметь ввиду, что выполняются не все вызовы run_init_process() в функции init_post(), так как, если вызов был удачным, то мы из него не возвращаемся. Если все вызовы провалились, то будет выведено сообщение «No init found. Try passing init= option to kernel. See Linux Documentation/init.txt for guidance» функцией panic(). Функция panic() останавливает инициализацию системы. Процесс init получает идентификатор равный 1 (pid = 1). Но init не всегда первый процесс в системе, как было сказано, возможно его не удастся запустить или, что более вероятно в качестве параметра командной строки был задан «init=/bin/sh», то sh получит идентификатор равный 1:
Затем создается еще один поток ядра — khtreadd (он имеет pid = 2) — «...Опосредованно взаимодействуя с помощью определенных API с данным потоком, различные части ядра могут ставить в очередь на создание новые потоки, которые и создает kthreadd...» (о потоках ядра можно прочитать на rflinux.blogspot.com).
Далее вызывается планировщик (с управлением процессов можно ознакомиться на http://welinux.ru/) и функция cpu_idle(). cpu_idle() является «idle» потоком для ядра (cpu_idle() это нулевой процесс, то есть pid = 0) и всегда находится в системе. Она передает управление планировщику, а если нет задач для выполнения, то сама занимает процессор в бесконечном поиске новой задачи.
На этом инициализация ядра заканчивается. Если был успешно запущен процесс init, то он продолжает работу по загрузке системы.
Некоторые ссылки:
- Подробности процесса загрузки Linux
- Исследуем процесс загрузки Linux
- Перевод Linux-Init-HOWTO