Как загружается ядро linux

Как загружается ядро linux

В данном разделе рассматриваются этапы сборки ядра Linux и обсуждается результат работы каждого из этапов. Процесс сборки в значительной степени зависит от аппаратной платформы, поэтому особое внимание будет уделено построению ядра Linux для платформы x86.

Когда пользователь дает команду ‘make zImage’ или ‘make bzImage’, результат — загрузочный образ ядра, записывается как arch/i386/boot/zImage или arch/i386/boot/bzImage соответственно. Вот что происходит в процессе сборки:

  1. Исходные файлы на C и ассемблере компилируются в перемещаемый [relocatable] объектный код в формате ELF (файлы с расширением .o), при этом некоторые файлы, с помощью утилиты ar(1), дополнительно группируются в архивы (с раширением .a)
  2. Созданные на предыдущем шаге, файлы .o и .a объединяются утилитой ld(1) в статически скомпонованный исполняемый файл vmlinux в 32-разрядном формате ELF 80386 с включенной символической информацией.
  3. Далее, посредством nm vmlinux, создается файл System.map , при этом все не относящиеся к делу символы отбрасываются.
  4. Переход в каталог arch/i386/boot .
  5. Текст asm-файла bootsect.S перерабатывается с или без ключа -D__BIG_KERNEL__, в зависимости от конечной цели bzImage или zImage, в bbootsect.s или bootsect.s соответственно.
  6. bbootsect.s ассемблируется и конвертируется в файл формата ‘raw binary’ с именем bbootsect ( bootsect.s ассемблируется в файл bootsect в случае сборки zImage).
  7. Содержимое установщика setup.S ( setup.S подключает video.S ) преобразуется в bsetup.s для bzImage ( setup.s для zImage). Как и в случае с кодом bootsector, различия заключаются в использовании ключа —D__BIG_KERNEL__, при сборке bzImage. Результирующий файл конвертируется в формат ‘raw binary’ с именем bsetup .
  8. Переход в каталог arch/i386/boot/compressed . Файл /usr/src/linux/vmlinux переводится в файл формата ‘raw binary’ с именем $tmppiggy и из него удаляются ELF-секции .note и .comment .
  9. gzip -9 $tmppiggy.gz
  10. Связывание $tmppiggy.gz в перемещаемый ELF-формат (ld -r) piggy.o .
  11. Компиляция процедур сжатия данных head.S и misc.c (файлы находятся в каталоге arch/i386/boot/compressed ) в объектный ELF формат head.o и misc.o .
  12. Объектные файлы head.o , misc.o и piggy.o объединяются в bvmlinux (или vmlinux при сборке zImage, не путайте этот файл с /usr/src/linux/vmlinux !). Обратите внимание на различие: -Ttext 0x1000, используется для vmlinux , а -Ttext 0x100000═— для bvmlinux , т.е. bzImage загружается по более высоким адресам памяти.
  13. Преобразование bvmlinux в файл формата ‘raw binary’ с именем bvmlinux.out , в процессе удаляются ELF секции .note и .comment .
  14. Возврат в каталог arch/i386/boot и, с помощью программы tools/build, bbootsect , bsetup и compressed/bvmlinux.out объединяются в bzImage (справедливо и для zImage , только в именах файлов отсутствует начальный символ ‘b’). В конец bootsector записываются такие важные переменные, как setup_sects и root_dev .

Размер загрузочного сектора (bootsector) всегда равен 512 байт. Размер установщика (setup) должен быть не менее чем 4 сектора, и ограничивается сверху размером около 12K — по правилу:

512 + setup_sects * 512 + место_для_стека_bootsector/setup setup.S .

1.2 Загрузка: Обзор

Процесс загрузки во многом зависит от аппаратной платформы, поэтому основное внимание будет уделено платформе IBM PC/IA32. Для сохранения обратной совместимости, firmware-загрузчики загружают операционную систему устаревшим способом. Процесс этот можно разделить на несколько этапов:

  1. BIOS выбирает загрузочное устройство.
  2. BIOS загружает bootsector с загрузочного устройства.
  3. Код bootsector-а загружает установщика, процедуры декомпрессии и сжатый образ ядра.
  4. Ядро декомпрессируется в защищенном режиме (protected mode).
  5. Выполняется низкоуровневый инициализирующий ассемблерный код.
  6. Выполняется высокоуровневый инициализирующий C код.

1.3 Загрузка: BIOS POST

  1. При включении питания запускается тактовый генератор и схема контроля питания устанавливает на шине сигнал #POWERGOOD.
  2. На вывод CPU #RESET подается сигнал (после чего CPU переходит в реальный режим 8086).
  3. %ds=%es=%fs=%gs=%ss=0, %cs=0xFFFF0000,%eip = 0x0000FFF0 (запуск кода Power On Self Test в ROM BIOS).
  4. На время выполнения проверок, прерывания запрещены.
  5. По адресу 0 инициализируется таблица векторов прерываний (IVT, Interrupts Vector Table).
  6. По прерыванию 0x19 вызывается начальный (bootstrap) загрузчик BIOS, регистр %dl содержит ‘номер загрузочного устройства’. В результате по физическому адресу 0x7C00 (0x07C0:0000) загружается содержимое первого сектора нулевой дорожки.

1.4 Загрузка: bootsector и setup

Для загрузки ядра Linux можно воспользоваться следующими загрузочными секторами:

  • Загрузочным сектором Linux из ( arch/i386/boot/bootsect.S ),
  • Загрузочный сектор LILO (или другого менеджера загрузки), или
  • обойтись без загрузочного сектора (loadlin и т.п.)

А теперь подробнее рассмотрим загрузочный сектор. В первых нескольких строках инициализируются вспомогательные макросы, используемые как значения сегментов:

(числа в начале — это номера строк в файле bootsect.S file) Значения DEF_INITSEG , DEF_SETUPSEG , DEF_SYSSEG и DEF_SYSSIZE берутся из файла include/asm/boot.h :

Рассмотрим поближе код bootsect.S :

Строки 54-63 перемещают код начального загрузчика из адреса 0x7C00 в адрес 0x90000. Для этого:

  1. в регистровую пару %ds:%si заносится значение $BOOTSEG:0 (0x7C0:0 = 0x7C00)
  2. в регистровую пару %es:%di заносится значение $INITSEG:0 (0x9000:0 = 0x90000)
  3. в регистр %cx записывается число 16-битовых слов (256 слов = 512 байт = 1 сектор)
  4. В регистре флагов EFLAGS сбрасывается флаг направления DF (Direction Flag) (копирование с автоинкрементом адресных регистров) (cld)
  5. копируется 512 байт (rep movsw)

Здесь умышленно не используется инструкция rep movsd (обратите внимание на директиву — .code16).

Читайте также:  Sync windows and android

В строке 64 выполняется переход на метку go: , в только что созданную копию загрузчика, т.е. в сегмент 0x9000. Эта, и следующие три инструкции (строки 64-76) переустанавливают регистр сегмента стека и регистр указателя стека на $INITSEG:0x4000-0xC, т.е. %ss = $INITSEG (0x9000) и %sp = 0x3FF4 (0x4000-0xC). Это и есть то самое ограничение на размер setup, которое упоминалось ранее (см. Построение образа ядра Linux).

Для того, чтобы разрешить считывание сразу нескольких секторов (multi-sector reads), в строках 77-103 исправляются некоторые значения в таблице параметров для первого диска :

Контроллер НГМД переводится в исходное состояние функцией 0 прерывания 0x13 в BIOS (reset FDC) и секторы установщика загружаются непосредственно после загрузчика, т.е. в физические адреса, начиная с 0x90200 ($INITSEG:0x200), с помощью функции 2 прерывания 0x13 BIOS (read sector(s)). Смотри строки 107-124:

Если загрузка по каким-либо причинам не прошла (плохая дискета или дискета была вынута в момент загрузки), то выдается сообщение об ошибке и производится переход на бесконечный цикл. Цикл будет повторяться до тех пор, пока не произойдет успешная загрузка, либо пока машина не будет перезагружена.

Если загрузка setup_sects секторов кода установщика прошла благополучно, то производится переход на метку ok_load_setup: .

Далее производится загрузка сжатого образа ядра в физические адреса начиная с 0x10000, чтобы не затереть firmware-данные в нижних адресах памяти (0-64K). После загрузки ядра управление передается в точку $SETUPSEG:0 ( arch/i386/boot/setup.S ). Поскольку обращений к BIOS больше не будет, данные в нижней памяти уже не нужны, поэтому образ ядра перемещается из 0x10000 в 0x1000 (физические адреса, конечно). И наконец, установщик setup.S завершает свою работу, переводя процессор в защищенный режим и передает управление по адресу 0x1000 где находится точка входа в сжатое ядро, т.е. arch/386/boot/compressed/ . Здесь производится установка стека и вызывается decompress_kernel() , которая декомпрессирует ядро в адреса, начиная с 0x100000, после чего управление передается туда.

Следует отметить, что старые загрузчики (старые версии LILO) в состоянии загружать только первые 4 сектора установщика (setup), это объясняет присутствие кода, «догружающего» остальные сектора в случае необходимости. Кроме того, установщик содержит код, обрабатывающий различные комбинации типов/версий загрузчиков и zImage/bzImage.

Теперь рассмотрим хитрость, позволяющую загрузчику выполнить загрузку «больших» ядер, известных под именем «bzImage». Установщик загружается как обычно, в адреса с 0x90200, а ядро, с помощью специальной вспомогательной процедуры, вызывающей BIOS для перемещения данных из нижней памяти в верхнюю, загружается кусками по 64К. Эта процедура определена в setup.S как bootsect_helper , а вызывается она из bootsect.S как bootsect_kludge . Метка bootsect_kludge , определенная в setup.S , содержит значение сегмента установщика и смещение bootsect_helper в нем же, так что для передачи управления загрузчик должен использовать инструкцию lcall (межсегментный вызов). Почему эта процедура помещена в setup.S ? Причина банальна — в bootsect.S просто больше нет места (строго говоря это не совсем так, поскольку в bootsect.S свободно примерно 4 байта и по меньшей мере еще 1 байт, но вполне очевидно, что этого недостаточно) Эта процедура использует функцию прерывания BIOS 0x15 (ax=0x8700) для перемещения в верхнюю память и переустанавливает %es так, что он всегда указывает на 0x10000. Это гарантирует, что bootsect.S не исчерпает нижнюю память при считывании данных с диска.

1.5 LILO в качестве загрузчика.

Специализированные загрузчики (например LILO) имеют ряд преимуществ перед чисто Linux-овым загрузчиком (bootsector):

  1. Возможность выбора загрузки одного из нескольких ядер Linux или одной из нескольких ОС.
  2. Возможность передачи параметров загрузки в ядро (существует патч BCP который добавляет такую же возможность и к чистому bootsector+setup).
  3. Возможность загружать большие ядра (bzImage) — до 2.5M (против обычного 1M).

Старые версии LILO ( версии 17 и более ранние) не в состоянии загрузить ядро bzImage. Более новые версии (не старше 2-3 лет) используют ту же методику, что и bootsect+setup, для перемещения данных из нижней в верхнюю память посредством функций BIOS. Отдельные разработчики (особенно Peter Anvin) выступают за отказ от поддержки ядер zImage. Тем не менее, поддержка zImage остается в основном из-за (согласно Alan Cox) существования некоторых BIOS-ов, которые не могут грузить ядра bzImage, в то время как zImage грузятся ими без проблем.

В заключение, LILO передает управление в setup.S и далее загрузка продолжается как обычно.

1.6 Высокоуровневая инициализация

Под «высокоуровневой инициализацией» следует понимать действия, непосредственно не связанные с начальной загрузкой, даже не смотря на то, что часть кода, выполняющая ее, написана на ассемблере, а именно в файле arch/i386/kernel/head.S , который является началом декомпрессированного ядра. При инициализации выполняются следующие действия:

  1. Устанавливаются сегментные регистры (%ds = %es = %fs = %gs = __KERNEL_DS = 0x18).
  2. Инициализируются таблицы страниц.
  3. Разрешается листание страниц, установкой бита PG в %cr0.
  4. Обнуляется BSS (для SMP (мультипроцессорных систем (прим. перев.)), это действие выполняет только первый CPU).
  5. Копируются первые 2k bootup параметров (kernel commandline).
  6. Проверяется тип CPU, используя EFLAGS и, если возможно, cpuid, позволяющие обнаружить процессор 386 и выше.
  7. Первый CPU вызывает start_kernel() , все остальные — arch/i386/kernel/smpboot.c:initialize_secondary() , если переменная ready=1, которая только переустанавливает esp/eip.

Функция init/main.c:start_kernel() написана на C и выполняет следующие действия:

  1. Выполняется глобальная блокировка (необходимая для того, чтобы через процесс инициализации проходил только один CPU)
  2. Выполняются платформо-зависимые настройки (анализируется раскладка памяти, копируется командная строка и пр.).
  3. Вывод «баннера» ядра, который содержит версию, компилятор, использованные при сборке, и пр., в кольцевой буфер для сообщений. Текст «баннера» задается в переменной linux_banner, определенной в init/version.c. Текст этот можно вывести на экран командой cat /proc/version.
  4. Инициализация ловушек.
  5. Инициализация аппаратных прерываний (irqs).
  6. Инициализация данных для планировщика.
  7. Инициализация данных хранения времени.
  8. Инициализация подсистемы программных прерываний (softirq).
  9. Разбор параметров командной строки.
  10. Инициализация консоли.
  11. Если ядро было скомпилировано с поддержкой загружаемых модулей, инициализируется подсистема динамической загрузки модулей.
  12. Инициализируются профилирующие буферы, если командная строка содержит указание «profile=».
  13. kmem_cache_init() , начало инициализации менеджера памяти.
  14. Разрешаются прерывания.
  15. Подсчет значения BogoMips для данного CPU.
  16. Вызывается mem_init() которая подсчитывает max_mapnr , totalram_pages и high_memory и выводит строку «Memory: . «.
  17. kmem_cache_sizes_init() , завершение инициализации менеджера памяти.
  18. Инициализация структур данных для procfs.
  19. fork_init() , создает uid_cache , инициализируется max_threads исходя из объема доступной памяти и конфигурируется RLIMIT_NPROC для init_task как max_threads/2 .
  20. Создаются различные кэши для VFS, VM, кэш буфера и пр..
  21. Инициализируется подсистема IPC, если имеется поддержка System V IPC. Обратите внимание, что для System V shm, это включает монтирование внутреннего (in-kernel) экземпляра файловой системы shmfs.
  22. Создается и инициализируется специальный кэш, если поддержка квот (quota) включена.
  23. Выполняется платформо-зависимая «проверка ошибок» («check for bugs») и, если это возможно, активируется обработка ошибок процессора/шины/проч. Сравнение различных архитектур показывает, что «ia64 не имеет ошибок» а «ia32 имеет несколько дефектов», хороший пример — «дефект f00f» который проверен только для ядра, собранного под процессор ниже, чем 686.
  24. Устанавливается флаг, указывающий на то, что планировщик должен быть вызван «при первой возможности» и создается поток ядра init() , который выполняет execute_command, если она имеется среди параметров командной строки в виде «init=», или пытается запустить /sbin/init, /etc/init, /bin/init, /bin/sh в указанном порядке; если не удается ни один из запусков то ядро «впадает в панику» с «предложением» задать параметр «init=».
  25. Переход в фоновый поток с pid=0.
Читайте также:  Собираем образ windows 10

Здесь важно обратить внимание на то, что задача init() вызывает функцию do_basic_setup() , которая в свою очередь вызывает do_initcalls() для поочередного (в цикле) вызова функций, зарегистрированных макросом __initcall или module_init() Эти функции либо являются независимыми друг от друга, либо их взаимозависимость должна быть учтена при задании порядка связывания в Makefile — ах. Это означает, что порядок вызова функций инициализации зависит от положения каталогов в дереве и структуры Makefile — ов. Иногда порядок вызова функций инициализации очень важен. Представим себе две подсистемы: А и Б, причем Б существенным образом зависит от того как была проинициализирована подсистема А. Если А скомпилирована как статическая часть ядра, а Б как подгружаемый модуль, то вызов функции инициализации подсистемы Б будет гарантированно произведен после инициализации подсистемы А. Если А — модуль, то и Б так же должна быть модулем, тогда проблем не будет. Но что произойдет, если и А, и Б скомпилировать с ядром статически? Порядок, в котором они будут вызываться (иницализироваться) зависит от смещения относительно точки .initcall.init ELF секции в образе ядра (грубо говоря — от порядка вызова макроса __initcall или module_init() прим. перев.). Rogier Wolff предложил ввести понятие «приоритетной» инфраструктуры, посредством которой модули могли бы задавать компоновщику порядок связывания, но пока отсутствуют заплаты, которые реализовали бы это качество достаточно изящным способом, чтобы быть включенным в ядро. А посему необходимо следить за порядком компоновки. Если А и Б (см. пример выше) скомпилированы статически и работают корректно, то и при каждой последующей пересборке ядра они будут работать, если порядок следования их в Makefile не изменяется. Если же они не функционируют, то стоит изменить порядок следования объектных файлов.

Еще одна замечательная особенность Linux — это возможность запуска «альтернативной программы инициализации», если ядру передается командная строка «init=». Эта особенность может применяться для перекрытия /sbin/init или для отладки скриптов инициализации (rc) и /etc/inittab вручную, запуская их по одному за раз

1.7 SMP Загрузка на x86

В случае SMP (многопроцессорной системы), первичный процессор проходит обычную последовательность — bootsector, setup и т.д., пока не встретится вызов функции start_kernel() , в которой стоит вызов функции smp_init() , откуда вызывается arch/i386/kernel/smpboot.c:smp_boot_cpus() . Функция smp_boot_cpus() в цикле (от 0 до NR_CPUS ) вызывает do_boot_cpu() для каждого apicid. Функция do_boot_cpu() создает (т.е. fork_by_hand ) фоновую задачу для указанного CPU и записывает, согласно спецификации Intel MP (в 0x467/0x469) трамплин-код, содержащийся в trampoline.S . Затем генерирует STARTUP IPI, заставляя вторичный процессор выполнить код из trampoline.S .

Ведущий процессор создает трамплин-код для каждого процессора в нижней памяти. Ведомый процессор, при исполнении «трамплина», записывает «магическое число», чтобы известить ведущий процессор, что код исполнен. Требование, по размещению трамплин-кода в нижней памяти, обусловлено спецификацией Intel MP.

Трамплин-код просто записывает 1 в %bx, переводит процессор в защищенный режим и передает управление на метку startup_32, которая является точкой входа в arch/i386/kernel/head.S .

При исполнении кода head.S , ведомый CPU обнаруживает, что он не является ведущим, перепрыгивает через очистку BSS и входит в initialize_secondary() которая переходит в фоновую задачу для данного CPU — минуя вызов init_tasks[cpu] , поскольку она уже была проинициирована ведущим процессором при исполнении do_boot_cpu(cpu) .

Читайте также:  Активация windows loader что это

Характерно, что код init_task может использоваться совместно, но каждая фоновая задача должна иметь свой собственный TSS. Именно поэтому init_tss[NR_CPUS] является массивом.

1.8 Освобождение памяти после инициализации

После выполнения инициализации операционной системы, значительная часть кода и данных становится ненужной. Некоторые системы (BSD, FreeBSD и пр.) не освобождают память, занятую этой ненужной информацией. В оправдание этому приводится (см. книгу McKusick-а по 4.4BSD): «данный код располагается среди других подсистем и поэтому нет никакой возможности избавиться от него». В Linux, конечно же такое оправдание невозможно, потому что в Linux «если что-то возможно в принципе, то это либо уже реализовано, либо над этим кто-то работает».

Как уже упоминалось ранее, ядро Linux может быть собрано только в двоичном формате ELF. Причиной тому (точнее одна из причин) — отделение инициализирующего кода/данных, для создания которых Linux предоставляет два макроса:

  • __init — для кода инициализации
  • __initdata — для данных

Макросы подсчитывают размер этих секций в спецификаторах аттрибутов gcc, и определены в include/linux/init.h :

Что означает — если код скомпилирован статически (т.е. литерал MODULE не определен), то он размещается в ELF-секции .text.init , которая объявлена в карте компоновки arch/i386/vmlinux.lds . В противном случае (т.е. когда компилируется модуль) макрос ничего не делает.

Таким образом, в процессе загрузки, поток ядра «init» (функция init/main.c:init() ) вызывает функцию free_initmem() , которая и освобождает все страницы памяти между адресами __init_begin и __init_end .

На типичной системе (на моей рабочей станции) это дает примерно 260K памяти.

Код, регистрирующийся через module_init() , размещается в секции .initcall.init , которая так же освобождается. Текущая тенденция в Linux — при проектировании подсистем (не обязательно модулей) закладывать точки входа/выхода на самых ранних стадиях с тем, чтобы в будущем, рассматриваемая подсистема, могла быть модулем. Например: pipefs, см. fs/pipe.c . Даже если подсистема никогда не будет модулем напрмер bdflush (см. fs/buffer.c ), все равно считается хорошим тоном использовать макрос module_init() вместо прямого вызова функции инициализации, при условии, что не имеет значения когда эта функция будет вызвана.

Имеются еще две макрокоманды, работающие подобным образом. Называются они __exit и __exitdata , но они более тесно связаны с поддержкой модулей, и поэтому будет описаны ниже.

1.9 Разбор командной строки

Давайте посмотрим как выполняется разбор командной строки, передаваемой ядру на этапе загрузки:

  1. LILO (или BCP) воспринимает командную строку через сервис клавиатуры BIOS-а, и размещает ее в физической памяти.
  2. Код arch/i386/kernel/head.S копирует первые 2k в нулевую страницу (zeropage). Примечательно, что текущая версия LILO (21) ограничивает размер командной строки 79-ю символами. Это не просто ошибка в LILO (в случае включенной поддержки EBDA(LARGE_EBDA (Extended BIOS Data Area) —необходима для некоторых современных мультипроцессорных систем. Заставляет LILO загружаться в нижние адреса памяти, с целью оставить как можно больше пространства для EBDA, но ограничивает максимальный размер для «малых» ядер — т.е. «Image» и «zImage» прим. перев. )). Werner пообещал убрать это ограничение в ближайшее время. Если действительно необходимо передать ядру командную строку длиной более 79 символов, то можно использовать в качестве загрузчика BCP или подправить размер командной строки в функции arch/i386/kernel/setup.c:parse_mem_cmdline() .
  3. arch/i386/kernel/setup.c:parse_mem_cmdline() (вызывается из setup_arch() , которая в свою очередь вызывается из start_kernel() ), копирует 256 байт из нулевой страницы в saved_command_line , которая отображается в /proc/cmdline . Эта же функция обрабатывает опцию «mem=», если она присутствует в командной строке, и выполняет соответствующие корректировки параметра VM.
  4. далее, командная строка передается в parse_options() (вызывается из start_kernel() ), где обрабатываются некоторые «in-kernel» параметры (в настоящее время «init=» и параметры для init) и каждый параметр передается в checksetup() .
  5. checksetup() проходит через код в ELF-секции .setup.init и вызывает каждую функцию, передавая ей полученное слово. Обратите внимание, что если функция, зарегистрированная через __setup() , возвращает 0, то становится возможной передача одного и того же «variable=value» нескольким функциям. Одни из них воспринимают параметр как ошибочный, другие -как правильный. Jeff Garzik говорит по этом у поводу: «hackers who do that get spanked :)» (не уверен в точности перевода, но тем не менее «программисты, работающие с ядром, иногда получают щелчок по носу». прим. перев.). Почему? Все зависит от порядка компоновки ядра, т.е. в одном случае functionA вызывается перед functionB, порядок может быть изменен с точностью до наоборот, результат зависит от порядка следования вызовов.

Для написания кода, обрабатывающего командную строку, следует использовать макрос __setup() , определенный в include/linux/init.h :

Ниже приводится типичный пример, при написании собственного кода (пример взят из реального кода драйвера BusLogic HBA drivers/scsi/BusLogic.c ):

Обратите внимание, что __setup() не делает ничего в случае, когда определен литерал MODULE, так что, при необходимости обработки командной строки начальной загрузки как модуль, так и статически связанный код, должен вызывать функцию разбора параметров «вручную» в функции инициализации модуля. Это так же означает, что возможно написание кода, который обрабатывает командную строку, если он скомпилирован как модуль, и не обрабатывает, когда скомпилирован статически, и наоборот.

Источник

Оцените статью