Linux boot kernel

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

Transcript of Linux boot kernel

Page 1: Linux boot kernel

Загрузка ядра ОС 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

Page 2: Linux boot kernel

start2:movw %cs, %axmovw %ax, %dsmovw %ax, %esmovw %ax, %ssxorw %sp, %spsticldmovw $bugger_off_msg, %si

msg_loop:lodsbandb %al, %aljz bs_diemovb $0xe, %ahmovw $7, %bxint $0x10jmp msg_loop

bs_die:xorw %ax, %axint $0x16int $0x19ljmp $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 hdrhdr: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, от этого адреса я в дальнейшем и буду

Page 3: Linux boot kernel

отталкиваться).Рассмотрим два поля 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, %dxmovw %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 0x102060x10206: 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

Page 4: Linux boot kernel

Большинство загрузчиков имеют свой идентификатор (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 0x102180x10218:0x01f9c000

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

(gdb) x/1xw 0x1021c0x1021c:0x00053a99 # соответственно 342681 байт$ ls -l debugrw-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 0x102380x10238: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();

}

...

Page 5: Linux boot kernel

/* 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();

Page 6: Linux boot kernel

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. Этот адрес служит

Page 7: Linux boot kernel

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

ENTRY(startup_32)...call decompress_kernel.../* * Jump to the decompressed kernel. */xorl %ebx, %ebxjmp *%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");...

}...

Page 8: Linux boot kernel

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

Page 9: Linux boot kernel