Глава 4. Ядро

Эта глава рассматривает ядро.

{Прим. пер.: если вас интересует процесс сборки самого ядра и связанных с нею подробностей, рекомендуем вам ознакомиться с нашим переводом книги Программирование ядра Linux Кайваня Н Биллимория, изданной Packt Publishing в марте 2021.}

Загрузка своего ядра в оперативную память

Это занимательная глава. До сих пор мы наблюдали, что вплоть до данного этапа GRUB 2 полностью контролировал нашу процедуру запуска. Теперь он обязан передать управление своему ядру. В этой главе мы увидим как и куда наш начальный загрузчик загружает своё ядро. Иными словами, как извлекается само ядро? Далее мы рассмотрим относящиеся к запуску задач успешно выполняемых соответствующим ядром Linux и, в конце, как это ядро запускает systemd.

[Замечание]Замечание

Тот исходный код ядра, который применяется в этой главе это версия kernel-5.4.4. Когда я писал эту книгу это был самый последний код из доступных; отсылаем к https://www.kernel.org/. Исключительным ресурсом по этой теме является Inside Linux, написанная 0xAX. Я почерпнул многое из неё, и я уверен что вы также сделаете это.

Для передачи управления своему ядру нашему начальному загрузчику приходится добиться двую главных моментов.

  • Загрузить это ядро в оперативную память

  • Установить некие из имеющихся полей этого ядра согласно протоколу запуска.

Полный протокол доступен по https://www.kernel.org/doc/Documentation/x86/boot.txt. Первоначальный протокол запуска был определён никем иным как Линусом Торвальдсом.


         ~                               ~
         |  Protected-mode kernel        |
 100000  +-------------------------------+
         |  I/O memory hole              |
 0A0000  +-------------------------------+
         |  Reserved for BIOS            | Остаётся как можно дольше неиспользованным
         ~                               ~
         |  Command line                 | (Может также быть ниже отметки X+10000)
X+10000  +-------------------------------+
         |  Stack/heap                   | For use by the kernel real-mode code.
X+08000  +-------------------------------+
         |  Kernel setup                 | Код ядра реального режима.
         |  Kernel boot sector           | Наследуемый загрузочный сектор ядра.
      X  +-------------------------------+
         |  Boot loader                  | <- Указатель 0000:7C00 на точку входа в загрузочный сектор. Вы обнаружите то же
         |                               | самое положение адреса в нашем файле boot.asm, который мы создали ранее.
  001000 +-------------------------------+
         |  Reserved for MBR/BIOS        |
 000800  +-------------------------------+
         |  Typically used by MBR        |
 000600  +-------------------------------+
         |  BIOS use only                |
 000000  +-------------------------------+
 	   

Согласно установленному протоколу запуска именно начальный загрузчик обязан передать или установить некоторые поля в заголовке своего ядра. Этими полями являются название корневого устройства, параметры монтирования, такие как ro или rw, название initramfs, его размер и т.п. Эти же поля имеют название параметров командной строки ядра и мы уже знаем, что эти параметры командной строки ядра передаются GRUB/ соответствующим начальным загрузчиком в его ядро.

GRUB не будет загружать своё ядро (/boot/vmlinuz) в любом случайно выбранном месте; оно всегда будет загружаться в неком конкретном месте. Значение конкретного места будет меняться для каждого дистрибутива Linux и используемой вами версии для установленной архитектуры ЦПУ в вашей системе. vmlinuz является неким архивным файлом и этот архив составлен из трёх частей.


Vmlinuz (bZimage) =  Header   + kernel setup code + vmlinux (actual compressed kernel)
                     (part-1)   (part-2)            (part-3)
		

После загрузки ядра в память

Теперь нам необходимо представить, что GRUB 2 загрузил своё ядро в оперативную память в особом месте. Вот шаги самого начального уровня, выполняемые архивным файлом vmlinuz сразу после его загрузки в память.

  1. Как только наш начальный загрузчик загружает необходимое ядро в оперативную память в конкретное место, запускается исполняемый файл, сделанный из arch/x86/boot/header.S.

  2. Путаница случается когда vmlinuz является неким архивом, а наш начальный загрузчик ещё пока не распаковал его. Наш начальный загрузчик только загрузил это ядро в конкретное место. Тогда как же этот код внутри файла своего архива vmlinuz способен выполняться?

  3. Сначала мы рассмотрим короткий ответ, а длинный ответ мы рассмотрим в своём разделе Что распаковывает vmlinuz? в этой главе. Итак, короткий ответ состоит в исполняемом файле, сделанном из файла /x86/boot/header.S не является таким архивом; вместо этого он выступает частью заголовка который выполняет задачу kernel_setup. Этот заголовок пребывает вне архива.

    
    Vmlinuz (bZimage) = Header + kernel setup code + vmlinux (actual compressed kernel)
             --->Outside of archive<--- + -------->Inside archive<------->header.s file is here<---
    		
  4. Давайте пока будем считать что vmlinuz был извлечён и давайте продолжим свою последовательность запуска. До сих пор мы видели, что GRUB загрузил необходимое ядро в оперативную память в особом месте и запускает тот исполняемый файл, который сделан из arch/x86/boot/header.S. Этот исполняемый файл отвечает за часть kernel_setup. Этот файл kernel_setup выполняет такие задачи:

    1. Налаживает регистры своего сегмента

    2. Устанавливает стек и BSS

    В каждой главе некая блок- схема снабжает нас чётким пониманием относительно изученного нами и, с точки зрения запуска,чего мы достигли. Рисунок 4-1 показывает самое начало той блок-схемы, которую мы сконструируем в этой главе по мере её развития. Она отображает те действия, которые выполняются кодом kernel_setup из header.s..

     

    Рисунок 4-1


    Предпринимаемые kernel_setup шаги

  5. Затем он выполняет безусловный переход в функцию main() в arch/x86/boot/main.c. Этот файл main.c также является частью заголовка ядра, а этот заголовок вне своего реального архива.

    
    Vmlinuz (bZimage) = Header + kernel setup code + vmlinux (actual compressed kernel)
             --->Outside of archive<--- + -------->Inside archive<---------
             --->main.c file is here<---
    #vim arch/x86/boot/main.c
    <snip>
    134 void main(void)
    135 {
    136         /* First, copy the boot header into the "zeropage" */
    137         copy_boot_params();
    138
    139         /* Initialize the early-boot console */
    140         console_init();
    141         if (cmdline_find_option_bool("debug"))
    142                 puts("early console in setup code\n");
    143
    144         /* End of heap check */
    145         init_heap();
    146
    147         /* Make sure we have all the proper CPU support */
    148         if (validate_cpu()) {
    149                 puts("Unable to boot - please use a kernel appropriate "
    150                      "for your CPU.\n");
    151                 die();
    152         }
    153
    154         /* Tell the BIOS what CPU mode we intend to run in. */
    155         set_bios_mode();
    156
    157         /* Detect memory layout */
    158         detect_memory();
    159
    160         /* Set keyboard repeat rate (why?) and query the lock flags */
    161         keyboard_init();
    162
    163         /* Query Intel SpeedStep (IST) information */
    164         query_ist();
    165
    166         /* Query APM information */
    167 #if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
    168         query_apm_bios();
    169 #endif
    170
    171         /* Query EDD information */
    172 #if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)
    173         query_edd();
    174 #endif
    175
    176         /* Set the video mode */
    177         set_video();
    178
    179         /* Do the last things and invoke protected mode */
    180         go_to_protected_mode();
    181 }
    </snip>
    		

Как вы можете видеть, исходный код main.c отвечает за следующее:

  1. Он копирует установленные параметры запуска (параметры командной строки своего ядра) из своего начального загрузчика. Функция copy_boot_params будет применена для копирования следующих параметров запуска, передаваемых его начальным загрузчиком:

    
    vfs.zfs.vdev.cache.size=<10M>debug, earlyprintk, ro, root, ramdisk_image, ramdisk_size и т.п.
     	   
  2. Он инициализирует свою консоль и проверяет был ли передан его пользователем параметр командной строки ядра, подобный debug. Если это так, наше ядро будет отображать на своём эеране сообщения подробного уровня.

  3. Он инициализирует кучу.

  4. Когда его ЦПУ не может быть проверен, тогда код выдаёт сообщение об ошибке через функцию validate_cpu(). Такие дистрибутивы как Fedora и Ubuntu настраивают сообщения об этой ошибке в диапазоне от 'unable to boot - please use the kernel appropriate for your cpu' (невозможно выполнить запуск - пожалуйста, применяйте ядро, подходящее для вашего ЦПУ) до 'The CPU is not supported' (данный ЦПУ не поддерживается). Эта персонализация также установит панику своего ядра и данный запуск будет прерван.

  5. Затем он выявляет установленную схему оперативной памяти и выводит её на печать на экране в некой начальной стадии запуска. Эти же самые сообщения о схеме памяти можно рассмотреть позднее применив команду dmesg, как это отражено ниже:

    
    [    0.000000] BIOS-provided physical RAM map:
    [    0.000000] BIOS-e820: [mem 0x0000000000000000-0x0000000000057fff] usable
    [    0.000000] BIOS-e820: [mem 0x0000000000058000-0x0000000000058fff] reserved
    [    0.000000] BIOS-e820: [mem 0x0000000000059000-0x000000000009cfff] usable
    [    0.000000] BIOS-e820: [mem 0x000000000009d000-0x00000000000fffff] reserved
    [    0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000007e5f7fff] usable
    [    0.000000] BIOS-e820: [mem 0x000000007e5f8000-0x000000007e5f8fff] ACPI NVS
    [    0.000000] BIOS-e820: [mem 0x000000007e5f9000-0x000000007e5f9fff] reserved
    [    0.000000] BIOS-e820: [mem 0x000000007e5fa000-0x0000000087f62fff] usable
    [    0.000000] BIOS-e820: [mem 0x0000000087f63000-0x000000008952bfff] reserved
    [    0.000000] BIOS-e820: [mem 0x000000008952c000-0x0000000089599fff] ACPI NVS
    [    0.000000] BIOS-e820: [mem 0x000000008959a000-0x00000000895fefff] ACPI data
    [    0.000000] BIOS-e820: [mem 0x00000000895ff000-0x00000000895fffff] usable
    [    0.000000] BIOS-e820: [mem 0x0000000089600000-0x000000008f7fffff] reserved
    [    0.000000] BIOS-e820: [mem 0x00000000f0000000-0x00000000f7ffffff] reserved
    [    0.000000] BIOS-e820: [mem 0x00000000fe010000-0x00000000fe010fff] reserved
    [    0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000086e7fffff] usable
     	   
  6. Инициализирует имеющуюся клавиатуру и её раскладку.

  7. Устанавливает необходимый режим базового видео.

  8. Выполняет безусловный переход в свой защищённый режим посредством функции go_to_protected_mode(). Для лучшего понимания обратитесь, пожалуйста, к Рисунку 4-2.

     

    Рисунок 4-2


    Наша блок- схема

Защищённый режим

Вплоть до этого момента мы работали в реальном режиме, который обладает ограничениями в адресации 20 битами, так как он не способен выполнять доступ к памяти свыше МБ. При помощи функции go_to_protected_mode() наше ядро переключило свой ЦПУ с реального режима на его защищённый режим. Защищённый режим обладает ограничением в адресации в 32 бита, а потому его ЦПУ способен выполнять доступ вплоть до 4 ГБ памяти. Проще говоря, в реальном режиме будут исполняться лишь те программы, которые обладают 16- битным наборов инструкций, к примеру, сам BIOS. В защищённом режиме будут запускаться только 32- битные программы. В защищённом режиме имеющееся ядро выполняет некоторые относящиеся к аппаратным средствам задачи и далее запускаем ЦПУ в длинном режиме.

Обратите,пожалуйста внимание, что эта книга следует архитектуре x86 Intel и обсуждения, относящиеся к реальному, защищённому и длинному режимам основаны на 64- битной архитектуре x86 Intel.

Длинный режим

Длинный режим не накладывает никаких ограничений на свой ЦПУ. Он способен применять всю установленную память. Помещение своег ЦПУ в длинный режим будет достигаться файлом head_64.S из arch/x86/boot/compressed/head_64.S. Он отвечает за следующее:

  1. Подготовку к длинному режиму означает, что он проверяет поддеривается ли длинный режим или нет.

  2. Вход вдлинный режим.

  3. Распаковку своего ядра.

Приводимые ниже функции вызываются из файла ассемблера head_64.S:


$ cat arch/x86/boot/compressed/head_64.S | grep -i call
    call    1f
    call    verify_cpu
    call    get_sev_encryption_bit
    call    1f
    call    1f
    call    .Ladjust_got
     * this function call.
    call    paging_prepare
     * this function call.
    call    cleanup_trampoline
    call    1f
    call    .Ladjust_got
    call    1f
     * Relocate efi_config->call().
    call    make_boot_params
    call    1f
     * Relocate efi_config->call().
    call    efi_main
    call    extract_kernel    /* returns kernel location in %rax */
    .quad    efi_call
		
Таблица 4-1. Вызываемые из head_64.S функции
Функция Действие

verify_cpu

Проверяет наличие длинного режима ЦПУ

make_boot_params

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

efi_main

Относящееся к встроенному ПО наполнение

extract_kernel

Эта функция определена в arch/x86/boot/compressed_misc.c

Именно эта функция распаковывает vmlinux из vmlinuz

Для лучшего понимания обратитесь, пожалуйста к показанной на Рисунке 4-3 блок- схеме.

 

Рисунок 4-3


Наша обновлённая блок- схема

Постойте, однако: если наше ядро ещё пока не распаковано, тогда как мы продолжаем выполнение в данный момент? Тут настаёт время нашего длинного ответа.

Что распаковывает vmlinuz?

К данному моменту мы осознали, что именно GRUB загрузил необходимое ядро в память, но в то же самое время, мы отметили, что наш образ vmlinuz содержится в неком архиве. Итак, что же распаковывает этот образ? Делает ли это GRUB?

Нет, это не GRUB. Вместо этого именно само ядро распаковывает себя. Да, я сказал что именно само ядро распаковывает это ядро. Возможно, vmlinuz единственный в мире файл операционной системы, который сам извлекает себя. Но как это возможно распаковывать себя самостоятельно? Чтобы понять это давайте для начала разберёмся с vmlinuz.

"vm" из vmlinuz это сокращение для "virtual memory". на самых ранних стадиях разработки Linux само понятие виртуальной памяти ещё не было проработано, а потому когда оно было добавлено эти символы "vm" и были добавлены к общему названию ядра Linux. Символ "z" это сокращение для ziiped файла.


$ file vmlinuz-5.0.9-301.fc30.x86_64
vmlinuz-5.0.9-301.fc30.x86_64: Linux kernel x86 boot executable bzImage, version 5.0.9-301.fc30.x86_64 (mockbuild@bkernel04.phx2.fedoraproject.org) #1 SMP Tue Apr 23 23:57:35 U, RO-rootFS, swap_dev 0x8, Normal VGA
		

Как вы можете видетьvmlinuz это bzImage (bzImage сокращение для "big zimage"). vmlinuz это сжатый файл реального исполняемого файла vmlinuz. Вы не сможете распаковать этот файл при помощи gunzip/bunzip или даже tar. Самым простым способом распаковки vmlinuz является использование файла сценария extract-vmlinuz, предоставляемого пакетом kernel-devel (в случае Fedora). Этот файл будет представлен в /usr/src/kernels/<kernel_version>/scripts/extract-vmlinux.


# ./extract-vmlinux /boot/vmlinuz-5.3.7-301.fc31.x86_64 >gt; /boot/temp/vmlinux
# file /boot/temp/*
/boot/temp/vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
statically linked, BuildID[sha1]=ec96b29d8e4079950644230c0b7868942bb70366, stripped
		

Существуют разнообразные способы раскрытия файлов ядра vmlinux и vmlinuz.


$ xxd vmlinux | less
$ objdump vmlinux | less
$ objdump vmlinux -D | less
$ hexdump vmlinux | less
$ od vmlinux | less
		

Мы воспользуемся командой od с некими переключателями для открытия своего файла vmlinuz.


$ od -A d -t x1 vmlinuz-5.0.9-301.fc30.x86_64 | less
<snip> 
0000000 4d 5a ea 07 00 c0 07 8c c8 8e d8 8e c0 8e d0 31
0000016 e4 fb fc be 40 00 ac 20 c0 74 09 b4 0e bb 07 00
0000032 cd 10 eb f2 31 c0 cd 16 cd 19 ea f0 ff 00 f0 00
0000048 00 00 00 00 00 00 00 00 00 00 00 00 82 00 00 00
0000064 55 73 65 20 61 20 62 6f 6f 74 20 6c 6f 61 64 65
0000080 72 2e 0d 0a 0a 52 65 6d 6f 76 65 20 64 69 73 6b
0000096 20 61 6e 64 20 70 72 65 73 73 20 61 6e 79 20 6b
0000112 65 79 20 74 6f 20 72 65 62 6f 6f 74 2e 2e 2e 0d
0000128 0a 00 50 45 00 00 64 86 04 00 00 00 00 00 00 00
0000144 00 00 01 00 00 00 a0 00 06 02 0b 02 02 14 80 37
0000160 8e 00 00 00 00 00 80 86 26 02 f0 48 00 00 00 02
0000176 00 00 00 00 00 00 00 00 00 00 20 00 00 00 20 00
0000192 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000208 00 00 00 c0 b4 02 00 02 00 00 00 00 00 00 0a 00
0000224 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
0000256 00 00 00 00 00 00 06 00 00 00 00 00 00 00 00 00
0000272 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000288 00 00 00 00 00 00 00 00 00 00 80 39 8e 00 48 09
0000304 00 00 00 00 00 00 00 00 00 00 2e 73 65 74 75 70
0000320 00 00 e0 43 00 00 00 02 00 00 e0 43 00 00 00 02
0000336 00 00 00 00 00 00 00 00 00 00 00 00 00 00 20 00
0000352 50 60 2e 72 65 6c 6f 63 00 00 20 00 00 00 e0 45
0000368 00 00 20 00 00 00 e0 45 00 00 00 00 00 00 00 00
0000384 00 00 00 00 00 00 40 00 10 42 2e 74 65 78 74 00
0000400 00 00 80 f3 8d 00 00 46 00 00 80 f3 8d 00 00 46
0000416 00 00 00 00 00 00 00 00 00 00 00 00 00 00 20 00
0000432 50 60 2e 62 73 73 00 00 00 00 80 86 26 02 80 39
0000448 8e 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000464 00 00 00 00 00 00 80 00 00 c8 00 00 00 00 00 00
0000480 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff
0000496 ff 22 01 00 38 df 08 00 00 00 ff ff 00 00 55 aa
0000512 eb 66 48 64 72 53 0d 02 00 00 00 00 00 10 c0 37
0000528 00 01 00 80 00 00 10 00 00 00 00 00 00 00 00 00
0000544 00 00 00 00 50 5a 00 00 00 00 00 00 ff ff ff 7f
0000560 00 00 00 01 01 15 3f 00 ff 07 00 00 00 00 00 00
0000576 00 00 00 00 00 00 00 00 b1 03 00 00 11 f3 89 00
0000592 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00
0000608 00 c0 b4 02 90 01 00 00 8c d8 8e c0 fc 8c d2 39
0000624 c2 89 e2 74 16 ba 50 58 f6 06 11 02 80 74 04 8b
</snip> 
# od -A d -t x1 /boot/vmlinuz-5.3.7-301.fc31.x86_64 | grep -i '1f 8b 08 00'
0018864 8f 1f 8b 08 00 00 00 00 00 02 03 ec fd 79 7c 54
		

итак, по адресу 0018864 начинается реальное ядро (vmlinux), в то время как сам файл vmlinuz стартует с 0000000. Это означает, что с 0000000 по 0018864 то что у нас имеется, это соответствующий заголовок нашего файла, такой как header.S, misc.c и т.п.. Именно он и распаковывает само реальное ядро (vmlinux) из vmlinuz. Вы можете рассматривать некий заголовок подобным нашлёпке поверх исполняемого файла vmlinux и когда эта нашлёпка доступна, он превращается в vmlinuzю В своём следующем разделе мы увидим как соответствующая процедура ядра распаковыает vmlinuz.

extract_kernel

Давайте вернёмся назад к своей функции extract_kernel из arch/x86/boot/compressed/misc.c.


asmlinkage __visible void *extract_kernel(void *rmode, memptr heap,
                                          unsigned char *input_data,
                                          unsigned long input_len,
                                          unsigned char *output,
                                          unsigned long output_len)
 	   

Как вы можете видеть, эта функция принимает семь параметров.

Таблица 4-2. Параметры функции extract_kernel
Параметр Назначение

rmode

Указатель на структуру boot_params, которая заполняется запускающим начальным загрузчиком.

heap

Указатель на файл boot_params, который представляет стартовый адрес самой ранней кучи запуска.

input_data

Указатель на наало самого схатого ядра или, иначе говоря, указатель на arch/x86/boot/compressed/vmlinux.bin.bz2.

input_len

Размер нашего сжатого ядра

output

Значение начального адреса нашего распаковываемого дале ядра

output_len

Значение размера распаковываемого ядра

run_size

Общий объём пространства, необходимый для выполнения самого ядра, включая разделы .bss и .brk

Наряду с самим ядром, наш начальный загрузчик также подгрузил в память и initramfs. Об initramfs мы поговорим в Главе 5. итак, перед распаковкой необходимого образа ядра, наш заголовок или соответствующая процедура ядра должен позаботиться о том, что распаковка vmlinuz не перекроет уже загруженный образ initramfs. По этой причине функция extract_kernel также заботится о вычислении адресного пространства initramfs и выполнит выравнивание соответствующим образом распакованный образ своего ядра. После того как мы получили верный адрес по которому наш заголовок может распаковывать vmlinuz, он выделит это ядро там.


340 asmlinkage __visible void *extract_kernel(void *rmode, memptr heap,
341                                   unsigned char *input_data,
342                                   unsigned long input_len,
343                                   unsigned char *output,
344                                   unsigned long output_len)
345 {
346         const unsigned long kernel_total_size = VO__end - VO__text;
347         unsigned long virt_addr = LOAD_PHYSICAL_ADDR;
348         unsigned long needed_size;
349
350         /* Retain x86 boot parameters pointer passed from startup_32/64. */
351         boot_params = rmode;
352
353         /* Clear flags intended for solely in-kernel use. */
354         boot_params->hdr.loadflags &= ~KASLR_FLAG;
355
356         sanitize_boot_params(boot_params);
357
358         if (boot_params->screen_info.orig_video_mode == 7) {
359                 vidmem = (char *) 0xb0000;
360                 vidport = 0x3b4;
361         } else {
362                 vidmem = (char *) 0xb8000;
363                 vidport = 0x3d4;
364         }
365
366         lines = boot_params->screen_info.orig_video_lines;
367         cols = boot_params->screen_info.orig_video_cols;
368
369         console_init();
370
371         /*
372          * Save RSDP address for later use. Have this after console_init()
373          * so that early debugging output from the RSDP parsing code can be
374          * collected.
375          */
376         boot_params->acpi_rsdp_addr = get_rsdp_addr();
377
378         debug_putstr("early console in extract_kernel\n");
379
380         free_mem_ptr     = heap;        /* Heap */
381         free_mem_end_ptr = heap + BOOT_HEAP_SIZE;
382
383         /*
384          * The memory hole needed for the kernel is the larger of either
385          * the entire decompressed kernel plus relocation table, or the
386          * entire decompressed kernel plus .bss and .brk sections.
387          *
388          * On X86_64, the memory is mapped with PMD pages. Round the
389          * size up so that the full extent of PMD pages mapped is
390          * included in the check against the valid memory table
391          * entries. This ensures the full mapped area is usable RAM
392          * and doesnt include any reserved areas.
393          */
394         needed_size = max(output_len, kernel_total_size);
395 #ifdef CONFIG_X86_64
396         needed_size = ALIGN(needed_size, MIN_KERNEL_ALIGN);
397 #endif
398
399         /* Report initial kernel position details. */
400         debug_putaddr(input_data);
401         debug_putaddr(input_len);
402         debug_putaddr(output);
403         debug_putaddr(output_len);
404         debug_putaddr(kernel_total_size);
405         debug_putaddr(needed_size);
406
407 #ifdef CONFIG_X86_64
408         /* Report address of 32-bit trampoline */
409         debug_putaddr(trampoline_32bit);
410 #endif
411
412         choose_random_location((unsigned long)input_data, input_len,
413                                 (unsigned long *)&output,
414                                 needed_size,
415                                 &virt_addr);
416
417         /* Validate memory location choices. */
418         if ((unsigned long)output & (MIN_KERNEL_ALIGN - 1))
419                 error("Destination physical address inappropriately aligned");
420         if (virt_addr & (MIN_KERNEL_ALIGN - 1))
421                 error("Destination virtual address inappropriately aligned");
422 #ifdef CONFIG_X86_64
423         if (heap > 0x3fffffffffffUL)
424                 error("Destination address too large");
425         if (virt_addr + max(output_len, kernel_total_size) > KERNEL_IMAGE_SIZE)
426                 error("Destination virtual address is beyond the kernel mapping area");
427 #else
428         if (heap > ((-__PAGE_OFFSET-(128<<20)-1) & 0x7fffffff))
429                 error("Destination address too large");
430 #endif
431 #ifndef CONFIG_RELOCATABLE
432         if ((unsigned long)output != LOAD_PHYSICAL_ADDR)
433                 error("Destination address does not match LOAD_PHYSICAL_ADDR");
434         if (virt_addr != LOAD_PHYSICAL_ADDR)
435                 error("Destination virtual address changed when not relocatable");
436 #endif
437
438         debug_putstr("\nDecompressing Linux... ");
439         __decompress(input_data, input_len, NULL, NULL, output, output_len,
440                         NULL, error);
441         parse_elf(output);
442         handle_relocations(output, output_len, virt_addr);
443         debug_putstr("done.\nBooting the kernel.\n");
444         return output;
445 }
 	   

Необходимый метод распаковки будет выбран в соответствии с методом сжатия, использованном на момент компиляции ядра. Доступные методы распаковки можно обнаружить в том же самом файле misc.c.


<snip from misc.c>
 57 #ifdef CONFIG_KERNEL_GZIP
 58 #include "../../../../lib/decompress_inflate.c"
 59 #endif
 60
 61 #ifdef CONFIG_KERNEL_BZIP2
 62 #include "../../../../lib/decompress_bunzip2.c"
 63 #endif
 64
 65 #ifdef CONFIG_KERNEL_LZMA
 66 #include "../../../../lib/decompress_unlzma.c"
 67 #endif
 68
 69 #ifdef CONFIG_KERNEL_XZ
 70 #include "../../../../lib/decompress_unxz.c"
 71 #endif
 72
 73 #ifdef CONFIG_KERNEL_LZO
 74 #include "../../../../lib/decompress_unlzo.c"
 75 #endif
     </snip>
 	   

После того как наше ядро распаковано в памяти, из самой функции extract_kernelc будет получена точка входа этого раскрытого ядра и наш ЦПУ выполнит безусловный переход вовнутрь ядра.

Внутри установленного ядра

Наше ядро выполняет большое число вещей, но я перечислю лишь то, что представляет наибольший интерес для вас, как кого- то изучающего запуск.

  • Наше ядро настроит размер стека самого ядра на 16 кБ в случае когда архитектура 64- битная. Это означает, что всякий новый процесс будет получать сво1 собственный стек ядра, который будет обладать в размере 16 кБ.

  • page_size будет установлен на 4 кБ, что является размером страницы по умолчанию для 64- битной архитектуры Intel.

  • Наше ядро подготовит оответствующий механизм обработки прерываний и исключительных ситуаций, также именуемый таблицей дескрипторов прерываний (IDT, interrupt descriptor table).

  • Ядро настроит необходимый механизм обработки отказов страниц.

  • Установленное ядро соберёт подробности своего файла initramfs, такие как название файла, размер, адрес, адрес перемещения, старшие и младшие номера нового корневого устройства и т.п. из /arch/x86/kernel/setup.c.

  • Далее оно выделит initramfs из его исходного кода в init/initramfs.c.

  • Наконец, оно запустит systemd при помощи функции start_kernel из init/main.c.

Вы можете обратить внимание, что это самый первый случай, когда мы выходим за рамки своего каталога arch. Это означает, что мы можем рассматривать данный код как не зависимый от архитектуры. После того как наше ядро запущено, оно выполняет множество вещей и нет практически никакой возможности охватить их все в данной книге. С точки зрения запуска лозунгом нашего ядра выступает запуск systemd из initramfs. Поскольку initramfs уже была загружена в память начальным загрузчиком, выделение необходимого ядра initramfs потребует подробностей файла самой initramfs, которые наше ядро получает из /arch/x86/kernel/setup.c.


      Initramfs file name,
      Initramfs file size,
      Initramfs files address,
      Initramfs files relocation address,
      Major and minor numbers on which initramfs will be mounted.
 	   

После того как наше ядро получило подробности и своём файле initramfs, оно выделяет сам архив initramfs из файла init/initramfs.c. Подробнее мы обсудим как в точности наше ядро раскрывает в памяти initramfs в Главе 5. Для монтирования initramfs в качестве корневого устройствапотребуются виртуальные файловые системы, такие как proc, sys, dev и т.п., а потому наше ядро подготавливает их надлежащим образом.


err = register_filesystem(&proc_fs_type);
        if (err)
        return;
 	   

Наше ядро позднее смонтирует свю выделенную initramfs при помощи функции do_mount_root из init/do_mounts.c. Как только initramfs смонтирована в памяти, наше ядро запустит оттуда systemd. systemd будет запущен через ту же самую функцию start_kernel из файла init/main.c.


asmlinkage void __init start_kernel(void)
 	   

В целом, когда наша корневая файловая система готова, она включится вовнутрь своей корневой файловой системы и создаст два потока: PID 1 это процесс systemd, а PID 2 это kthread. Для лучшего понимания отсылаем вас к показанной на Рисунке 4-4 блок- схеме.

 

Рисунок 4-4



Наша вновь обновлённая блок- схема

Рисунок 4-5. отображает окончательную последовательность запуска которую мы обсудили на данный момент.

 

Рисунок 4-5


Наша последовательность запуска в некой блок- схеме

Прежде чем мы продолжим и рассмотрим как наше ядро выделяет initramfs и запускает из него systemd, нам необходимо разобраться с основами самой initramfs, например, зачем она нам нужна, какова её структура и т.п.. После того как мы разберёмся с важностью и основами initramfs мы продолжим свою последовательность запуска ролью systemd в общей последовательности запуска.