В данном разделе рассматриваются этапы сборки ядра Linux и обсуждается результат работы каждого из этапов. Процесс сборки в значительной степени зависит от аппаратной платформы, поэтому особое внимание будет уделено построению ядра Linux для платформы x86.
Когда пользователь дает команду 'make zImage' или
'make bzImage', результат -- загрузочный образ ядра,
записывается как arch/i386/boot/zImage
или
arch/i386/boot/bzImage
соответственно. Вот что
происходит в процессе сборки:
vmlinux
в 32-разрядном формате ELF
80386 с включенной символической информацией.System.map
, при этом все не относящиеся к делу
символы отбрасываются.arch/i386/boot
.bootsect.S
перерабатывается с
или без ключа -D__BIG_KERNEL__, в зависимости от
конечной цели bzImage или zImage, в bbootsect.s
или
bootsect.s
соответственно.bbootsect.s
ассемблируется и конвертируется в
файл формата 'raw binary' с именем bbootsect
(bootsect.s
ассемблируется в файл
bootsect
в случае сборки zImage).setup.S
(setup.S
подключает video.S
)
преобразуется в bsetup.s
для bzImage
(setup.s
для zImage). Как и в случае с кодом
bootsector, различия заключаются в использовании ключа
-D__BIG_KERNEL__, при сборке bzImage.
Результирующий файл конвертируется в формат 'raw binary'
с именем bsetup
.arch/i386/boot/compressed
.
Файл /usr/src/linux/vmlinux
переводится в файл
формата 'raw binary' с именем $tmppiggy и из него
удаляются ELF-секции .note
и
.comment
.piggy.o
.head.S
и
misc.c
(файлы находятся в каталоге
arch/i386/boot/compressed
) в объектный ELF формат
head.o
и misc.o
.head.o
, misc.o
и
piggy.o
объединяются в bvmlinux
(или
vmlinux
при сборке zImage, не путайте этот файл с
/usr/src/linux/vmlinux
!). Обратите внимание на
различие: -Ttext 0x1000, используется для
vmlinux
, а -Ttext 0x100000═-- для
bvmlinux
, т.е. bzImage загружается по более высоким
адресам памяти.bvmlinux
в файл формата 'raw
binary' с именем bvmlinux.out
, в процессе
удаляются ELF секции .note
и
.comment
.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 <= 0x4000 байт
Откуда взялось это ограничение станет понятным дальше.
На сегодняшний день верхний предел размера bzImage составляет примерно 2.5M, в случае загрузки через LILO, и 0xFFFF параграфов (0xFFFF0 = 1048560 байт) для загрузки raw-образа, например с дискеты или CD-ROM (El-Torito emulation mode).
Следует помнить, что tools/build выполняет
проверку размеров загрузочного сектора, образа ядра и нижней
границы установщика (setup), но не проверяет *верхнюю* границу
установщика (setup). Следовательно очень легко собрать
"битое" ядро, добавив несколько больший размер
".space" в конец setup.S
.
Процесс загрузки во многом зависит от аппаратной платформы, поэтому основное внимание будет уделено платформе IBM PC/IA32. Для сохранения обратной совместимости, firmware-загрузчики загружают операционную систему устаревшим способом. Процесс этот можно разделить на несколько этапов:
Для загрузки ядра Linux можно воспользоваться следующими загрузочными секторами:
arch/i386/boot/bootsect.S
),А теперь подробнее рассмотрим загрузочный сектор. В первых нескольких строках инициализируются вспомогательные макросы, используемые как значения сегментов:
29 SETUPSECS = 4 /* число секторов установщика по умолчанию */ 30 BOOTSEG = 0x07C0 /* первоначальный адрес загрузочного сектора */ 31 INITSEG = DEF_INITSEG /* сюда перемещается загрузчик - чтобы не мешал */ 32 SETUPSEG = DEF_SETUPSEG /* здесь начинается установщик */ 33 SYSSEG = DEF_SYSSEG /* система загружается по адресу 0x10000 (65536) */ 34 SYSSIZE = DEF_SYSSIZE /* размер системы: в 16-байтных блоках */
(числа в начале - это номера строк в файле bootsect.S file)
Значения DEF_INITSEG
, DEF_SETUPSEG
,
DEF_SYSSEG
и DEF_SYSSIZE
берутся из файла
include/asm/boot.h
:
/* Ничего не меняйте, если не уверены в том, что делаете. */ #define DEF_INITSEG 0x9000 #define DEF_SYSSEG 0x1000 #define DEF_SETUPSEG 0x9020 #define DEF_SYSSIZE 0x7F00
Рассмотрим поближе код bootsect.S
:
54 movw $BOOTSEG, %ax 55 movw %ax, %ds 56 movw $INITSEG, %ax 57 movw %ax, %es 58 movw $256, %cx 59 subw %si, %si 60 subw %di, %di 61 cld 62 rep 63 movsw 64 ljmp $INITSEG, $go 65 # bde - 0xff00 изменено на 0x4000 для работы отладчика с 0x6400 и выше (bde). 66 # Если мы проверили верхние адреса, то об этом можно не беспокоиться. Кроме того, 67 # мой BIOS можно сконфигурировать на загрузку таблицы дисков wini в верхнюю память 68 # вместо таблицы векторов. Старый стек может "помесить" 69 # таблицу устройств [drive table]. 70 go: movw $0x4000-12, %di # 0x4000 - произвольное значение >= 71 # длины bootsect + длины 72 # setup + место для стека; 73 # 12 - размер параметров диска. 74 movw %ax, %ds # INITSEG уже в ax и es 75 movw %ax, %ss 76 movw %di, %sp # разместим стек по INITSEG:0x4000-12.
Строки 54-63 перемещают код начального загрузчика из адреса 0x7C00 в адрес 0x90000. Для этого:
Здесь умышленно не используется инструкция rep
movsd
(обратите внимание на директиву - .code16).
В строке 64 выполняется переход на метку go:
, в
только что созданную копию загрузчика, т.е. в сегмент 0x9000. Эта,
и следующие три инструкции (строки 64-76) переустанавливают регистр
сегмента стека и регистр указателя стека на $INITSEG:0x4000-0xC,
т.е. %ss = $INITSEG (0x9000) и %sp = 0x3FF4 (0x4000-0xC). Это и
есть то самое ограничение на размер setup, которое упоминалось
ранее (см. Построение образа ядра Linux).
Для того, чтобы разрешить считывание сразу нескольких секторов (multi-sector reads), в строках 77-103 исправляются некоторые значения в таблице параметров для первого диска :
77 # Часто в BIOS по умолчанию в таблицы параметров диска не признают 78 # чтение по несколько секторов кроме максимального числа, указанного 79 # по умолчанию в таблице параметров дискеты - что может иногда равняться 80 # 7 секторам. 81 # 82 # Поскольку чтение по одному сектору отпадает (слишком медленно), 83 # необходимо позаботиться о создании в ОЗУ новой таблицы параметров 84 # (для первого диска). Мы установим максимальное число секторов 85 # равным 36 - максимум, с которым мы столкнемся на ED 2.88. 86 # 87 # Много - не мало. А мало - плохо. 88 # 89 # Сегменты устанавливаются так: ds = es = ss = cs - INITSEG, fs = 0, 90 # а gs не используется. 91 movw %cx, %fs # запись 0 в fs 92 movw $0x78, %bx # в fs:bx адрес таблицы 93 pushw %ds 94 ldsw %fs:(%bx), %si # из адреса ds:si 95 movb $6, %cl # копируется 12 байт 96 pushw %di # di = 0x4000-12. 97 rep # инструкция cld не нужна - выполнена в строке 66 98 movsw 99 popw %di 100 popw %ds 101 movb $36, 0x4(%di) # записывается число секторов 102 movw %di, %fs:(%bx) 103 movw %es, %fs:2(%bx)
Контроллер НГМД переводится в исходное состояние функцией 0 прерывания 0x13 в BIOS (reset FDC) и секторы установщика загружаются непосредственно после загрузчика, т.е. в физические адреса, начиная с 0x90200 ($INITSEG:0x200), с помощью функции 2 прерывания 0x13 BIOS (read sector(s)). Смотри строки 107-124:
107 load_setup: 108 xorb %ah, %ah # переинициализация FDC 109 xorb %dl, %dl 110 int $0x13 111 xorw %dx, %dx # диск 0, головка 0 112 movb $0x02, %cl # сектор 2, дорожка 0 113 movw $0x0200, %bx # адрес в INITSEG = 512 114 movb $0x02, %ah # функция 2, "read sector(s)" 115 movb setup_sects, %al # (все под головкой 0, на дорожке 0) 116 int $0x13 # читать 117 jnc ok_load_setup # получилось - продолжить 118 pushw %ax # запись кода ошибки 119 call print_nl 120 movw %sp, %bp 121 call print_hex 122 popw %ax 123 jmp load_setup 124 ok_load_setup:
Если загрузка по каким-либо причинам не прошла (плохая дискета или дискета была вынута в момент загрузки), то выдается сообщение об ошибке и производится переход на бесконечный цикл. Цикл будет повторяться до тех пор, пока не произойдет успешная загрузка, либо пока машина не будет перезагружена.
Если загрузка 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/{head.S,misc.c}
. Здесь
производится установка стека и вызывается
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
не исчерпает нижнюю память при считывании
данных с диска.
Специализированные загрузчики (например LILO) имеют ряд преимуществ перед чисто Linux-овым загрузчиком (bootsector):
Старые версии LILO ( версии 17 и более ранние) не в состоянии загрузить ядро bzImage. Более новые версии (не старше 2-3 лет) используют ту же методику, что и bootsect+setup, для перемещения данных из нижней в верхнюю память посредством функций BIOS. Отдельные разработчики (особенно Peter Anvin) выступают за отказ от поддержки ядер zImage. Тем не менее, поддержка zImage остается в основном из-за (согласно Alan Cox) существования некоторых BIOS-ов, которые не могут грузить ядра bzImage, в то время как zImage грузятся ими без проблем.
В заключение, LILO передает управление в setup.S
и
далее загрузка продолжается как обычно.
Под "высокоуровневой инициализацией" следует понимать
действия, непосредственно не связанные с начальной загрузкой, даже
не смотря на то, что часть кода, выполняющая ее, написана на
ассемблере, а именно в файле arch/i386/kernel/head.S
,
который является началом декомпрессированного ядра. При
инициализации выполняются следующие действия:
start_kernel()
, все
остальные -
arch/i386/kernel/smpboot.c:initialize_secondary()
,
если переменная ready=1, которая только переустанавливает
esp/eip.Функция init/main.c:start_kernel()
написана на C и
выполняет следующие действия:
kmem_cache_init()
, начало инициализации
менеджера памяти.mem_init()
которая подсчитывает
max_mapnr
, totalram_pages
и
high_memory
и выводит строку "Memory:
...".kmem_cache_sizes_init()
, завершение
инициализации менеджера памяти.fork_init()
, создает uid_cache
,
инициализируется max_threads
исходя из объема
доступной памяти и конфигурируется RLIMIT_NPROC
для
init_task
как max_threads/2
.init()
, который выполняет
execute_command, если она имеется среди параметров командной
строки в виде "init=", или пытается запустить
/sbin/init, /etc/init,
/bin/init, /bin/sh в указанном
порядке; если не удается ни один из запусков то ядро
"впадает в панику" с "предложением" задать
параметр "init=".Здесь важно обратить внимание на то, что задача
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
вручную, запуская их по одному за раз
В случае 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)
.
Характерно, что код init_task может использоваться совместно, но
каждая фоновая задача должна иметь свой собственный TSS. Именно
поэтому init_tss[NR_CPUS]
является массивом.
После выполнения инициализации операционной системы, значительная часть кода и данных становится ненужной. Некоторые системы (BSD, FreeBSD и пр.) не освобождают память, занятую этой ненужной информацией. В оправдание этому приводится (см. книгу McKusick-а по 4.4BSD): "данный код располагается среди других подсистем и поэтому нет никакой возможности избавиться от него". В Linux, конечно же такое оправдание невозможно, потому что в Linux "если что-то возможно в принципе, то это либо уже реализовано, либо над этим кто-то работает".
Как уже упоминалось ранее, ядро Linux может быть собрано только в двоичном формате ELF. Причиной тому (точнее одна из причин) - отделение инициализирующего кода/данных, для создания которых Linux предоставляет два макроса:
__init
- для кода инициализации__initdata
- для данныхМакросы подсчитывают размер этих секций в спецификаторах
аттрибутов gcc, и определены в
include/linux/init.h
:
#ifndef MODULE #define __init __attribute__ ((__section__ (".text.init"))) #define __initdata __attribute__ ((__section__ (".data.init"))) #else #define __init #define __initdata #endif
Что означает - если код скомпилирован статически (т.е. литерал
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
, но
они более тесно связаны с поддержкой модулей, и поэтому будет
описаны ниже.
Давайте посмотрим как выполняется разбор командной строки, передаваемой ядру на этапе загрузки:
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()
.arch/i386/kernel/setup.c:parse_mem_cmdline()
(вызывается из setup_arch()
, которая в свою очередь
вызывается из start_kernel()
), копирует 256 байт из
нулевой страницы в saved_command_line
, которая
отображается в /proc/cmdline
. Эта же функция
обрабатывает опцию "mem=", если она присутствует в
командной строке, и выполняет соответствующие корректировки
параметра VM.parse_options()
(вызывается из
start_kernel()
), где обрабатываются некоторые
"in-kernel" параметры (в настоящее время
"init=" и параметры для init) и каждый параметр
передается в checksetup()
.checksetup()
проходит через код в ELF-секции
.setup.init
и вызывает каждую функцию, передавая ей
полученное слово. Обратите внимание, что если функция,
зарегистрированная через __setup()
, возвращает 0, то
становится возможной передача одного и того же
"variable=value" нескольким функциям. Одни из них
воспринимают параметр как ошибочный, другие -как правильный. Jeff
Garzik говорит по этом у поводу: "hackers who do that get
spanked :)" (не уверен в точности перевода, но тем не
менее "программисты, работающие с ядром, иногда получают
щелчок по носу". прим. перев.).
Почему? Все зависит от порядка компоновки ядра, т.е. в одном
случае functionA вызывается перед functionB, порядок может быть
изменен с точностью до наоборот, результат зависит от порядка
следования вызовов.Для написания кода, обрабатывающего командную строку, следует
использовать макрос __setup()
, определенный в
include/linux/init.h
:
/* * Used for kernel command line parameter setup */ struct kernel_param { const char *str; int (*setup_func)(char *); }; extern struct kernel_param __setup_start, __setup_end; #ifndef MODULE #define __setup(str, fn) \ static char __setup_str_##fn[] __initdata = str; \ static struct kernel_param __setup_##fn __initsetup = \ { __setup_str_##fn, fn } #else #define __setup(str,func) /* nothing */ endif
Ниже приводится типичный пример, при написании собственного кода
(пример взят из реального кода драйвера BusLogic HBA
drivers/scsi/BusLogic.c
):
static int __init BusLogic_Setup(char *str) { int ints[3]; (void)get_options(str, ARRAY_SIZE(ints), ints); if (ints[0] != 0) { BusLogic_Error("BusLogic: Obsolete Command Line Entry " "Format Ignored\n", NULL); return 0; } if (str == NULL || *str == '\0') return 0; return BusLogic_ParseDriverOptions(str); } __setup("BusLogic=", BusLogic_Setup);
Обратите внимание, что __setup()
не делает ничего в
случае, когда определен литерал MODULE, так что, при необходимости
обработки командной строки начальной загрузки как модуль, так и
статически связанный код, должен вызывать функцию разбора
параметров "вручную" в функции инициализации модуля. Это
так же означает, что возможно написание кода, который обрабатывает
командную строку, если он скомпилирован как модуль, и не
обрабатывает, когда скомпилирован статически, и наоборот.