You are on page 1of 9

Загрузка ядра ОС Linux

Рассмотрим как происходит загрузка ядра, но работу BIOS и загрузчиков,


таких как GRUB или GRUB2, мы рассматривать не будем .
Для начала я проиллюстрирую схему организации памяти, приведенную в
документации к ядру (linux/Documentation/x86/boot.txt):

К этой схеме время от времени мы будем возвращаться. Итак...


Boot loader или загрузчик — это программа, которая вызывается BIOS для
загрузки образа ядра в оперативную память. Образ ядра является точной копией файла
расположенного на жестком диске (vmlinuz-<version> или bzImage). Образ ядра
разделен на две части:
• небольшой код, который работает в реальном режиме и загружается ниже барьера
в 640K (0x0A0000);
• часть ядра, которая работает в защищенном режиме, загружается после первого
мегабайта памяти (0x100000).
Для того, чтобы понять что происходит ниже барьера в 640К начнем с
содержимого файла linux/arch/x86/boot/header.S и рассмотрим небольшой участок кода
в самом начале файла:

...
.code16
.section ".bstext", "ax"
.global bootsect_start
bootsect_start:
ljmp $BOOTSEG, $start2
start2:
movw %cs, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
xorw %sp, %sp
sti
cld
movw $bugger_off_msg, %si
msg_loop:
lodsb
andb %al, %al
jz bs_die
movb $0xe, %ah
movw $7, %bx
int $0x10
jmp msg_loop
bs_die:
xorw %ax, %ax
int $0x16
int $0x19
ljmp $0xf000,$0xfff0

.section ".bsdata", "a"


bugger_off_msg:
.ascii "Direct booting from floppy is no longer supported.\r\n"
.ascii "Please use a boot loader program instead.\r\n"
.ascii "\n"
.ascii "Remove disk and press any key to reboot . . .\r\n"
.byte 0

.section ".header", "a"


.globl hdr
hdr:
setup_sects: .byte 0
...
boot_flag: .word 0xAA55
...

Этот участок кода является загрузочным сектором (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 (магическое число), то есть является
сигнатурой загрузочного сектора.

Вернемся к файлу header.S (код, представленный ниже, следует сразу после


«магического числа» 0xАА55 и выполняется в том случае, если загрузка происходит с
жесткого диска):

...
.globl _start
_start:
.byte 0xeb
.byte start_of_setup-1f
...
start_of_setup:
...
movw %ds, %ax
movw %ax, %es
cld
movw %ss, %dx
cmpw %ax, %dx
movw %sp, %dx
...
calll main

setup_bad:
...

Сразу за определением сигнатуры загрузочного сектора следует двухбайтовый


прыжок (он явно указывается двумя байтами, иначе ассемблер может сгенерировать
здесь 3-байтовый прыжок, который сдвинет все остальные инструкции на неправильное
смещение) к адресу start_of_setup, где устанавливается стек и заполняется нулями
bss (неинициализированные данные), после чего вызывается функция main.
Прежде чем перейти к функции main, обратим внимание на некоторые поля (между
метками _start и start_of_setup), которые содержит header.S (в приведенном выше
листинге они отсутствуют):

• header — магическая сигнатура «HdrS» (Header Signature).


• version — содержит версию используемого протокола (boot protocol):

(gdb) x/1xh 0x10206


0x10206: 0x020a # версия протокола соответственно 2.10

• kernel_version — содержит указатель на версию ядра (задана строкой):

(gdb) x/7cb 0x133b0


0x133b0:50 '2' 46 '.' 54 '6' 46 '.' 51 '3' 53 '5' 32 ' ' # 2.6.35

• type_of_loader — тип загрузчика (LILO, GRUB и т.д.):

(gdb) x/1xb 0x10210


0x10210: 0xb0
Большинство загрузчиков имеют свой идентификатор (ID). Значение 0xTV
трактуется следующим образом T — идентификатор загрузчика, V — версия загрузчика.
Для данного примера 0xb0 следует: 0xb — загрузчик Qemu, 0x0 — версия 0 (все ID
загрузчиков можно посмотреть в файле linux/Documentation/x86/boot.txt).

• code32_start — адрес для перехода в защищенный режим:

(gdb) x/4xb 0x10214


0x10214:0x00100000

• ramdisk_image — содержит 32-битный линейный адрес местоположения ramdisk или


ramfs:

(gdb) x/1xw 0x10218


0x10218:0x01f9c000

• ramdisk_size — содержит размер ramdisk или ramfs:

(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-битный указатель на параметры командной строки:

(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 — максимальная длина аргументов командной строки:

(gdb) x/1xw 0x10238


0x10238:0x000007ff # соответственно 2047 байт

Описание и принимаемые значения всех остальных параметров вы можете найти в


файле linux/Documentation/x86/boot.txt.

Итак, была вызвана функция main. Это первая функция, написанная на языке С,
которая встречается на пути загрузки ядра. Ее определение находится в файле
linux/arch/x86/boot/main.c:

void main(void)
{
/* First, copy the boot header into the "zeropage" */
copy_boot_params();

/* End of heap check */


init_heap();

/* Make sure we have all the proper CPU support */


if (validate_cpu()) {
puts("Unable to boot - please use a kernel appropriate "
"for your CPU.\n");
die();
}

...
/* Detect memory layout */
detect_memory();
...
/* Set the video mode */
set_video();

/* Parse command line for 'quiet' and pass it to decompressor. */


if (cmdline_find_option_bool("quiet"))
boot_params.hdr.loadflags |= QUIET_FLAG;

/* Do the last things and invoke protected mode */


go_to_protected_mode();
}

Рассмотрим некоторые из функций:

• 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:

void 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


и производит заключительные настройки перед переходом в защищенный режим:

void 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).

Итак, мы перешли в защищенный режим по адресу 0x100000. Этот адрес служит


точкой входя для функции startup_32, которая определена в файле
linux/arch/x86/boot/compressed/head_32.S:

ENTRY(startup_32)
...
call decompress_kernel
...
/*
* Jump to the decompressed kernel.
*/
xorl %ebx, %ebx
jmp *%ebp
...
ENDPROC(startup_32)

В функции startup_32 распаковывается (decompress) ядро, а затем


осуществляется к нему переход. За распаковку ядра отвечает функция
decompress_kernel() (linux/arch/x86/boot/misc.c). Если во время распаковки ядра не
возникло ошибок, то на экране появится надпись «Decompressing Linux... Booting the
kernel». Ядро может быть распаковано или по адресу 0x100000, или, в случае, если
ядро было собрано с опцией «CONFIG_RELOCATABLE=y», по любому другому адресу выше
1MiB (начиная с адреса 0x100000 и выше).
В любом случае осуществляется переход к распакованному ядру, а именно к
функции start_kernel(), которая определена в файле linux/init/main.c:

asmlinkage void __init start_kernel(void)


{
char * command_line;
extern struct kernel_param __start___param[], __stop___param[];

...
/* Do the rest non-__init'ed, we're now alive */
rest_init();
}

Если вы посмотрите на содержимое функции start_kernel(), то можете заметить,


что она вызывает целую серию других функций, которые инициализируют различные
подсистемы ядра и структуры данных. Часть функций с коротким описанием
представлена в схеме (см. ниже), поэтому мы сразу перейдем к рассмотрению
заключительного этапа — функции rest_init().
Функция rest_init() создает новый поток ядра, который, в конечном итоге
вызывает программу пространства пользователя /sbin/init:

static noinline void __init_refok rest_init(void)


__releases(kernel_lock)
{
int pid;

...
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
|
+--> static noinline int init_post(void) {
...
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
...
}
...
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
...
schedule();
...
cpu_idle();
}

Следует иметь ввиду, что выполняются не все вызовы 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, то он продолжает работу по загрузке системы.