dementiy 03.11.2010 03:05
Coding — Загрузка ядра ОС Linux
Рассмотрим как происходит загрузка ядра (рассматривалось ядро версии 2.6.35 и архитектура x86_32), но работу BIOS и загрузчиков, таких как GRUB или GRUB2, мы рассматривать не будем.Для начала я проиллюстрирую схему организации памяти, приведенную в документации к ядру (linux/Documentation/x86/boot.txt):

К этой схеме время от времени мы будем возвращаться. Итак...
Boot loader или загрузчик — это программа, которая вызывается BIOS для загрузки образа ядра в оперативную память. Образ ядра является точной копией файла расположенного на жестком диске (vmlinuz-version или bzImage). Образ ядра разделен на две части:
небольшой код, который работает в реальном режиме и загружается ниже барьера в 640K (0x0A0000);
часть ядра, которая работает в защищенном режиме, загружается после первого мегабайта памяти (0x100000).Для того, чтобы понять что происходит ниже барьера в 640К начнем с содержимого файла linux/arch/x86/boot/header.S и рассмотрим небольшой участок кода в самом начале файла:
Этот участок кода является загрузочным сектором (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 и выполняется в том случае, если загрузка происходит с жесткого диска):
Сразу за определением сигнатуры загрузочного сектора следует двухбайтовый прыжок (он явно указывается двумя байтами, иначе ассемблер может сгенерировать здесь 3-байтовый прыжок, который сдвинет все остальные инструкции на неправильное смещение) к адресу start_of_setup, где устанавливается стек и заполняется нулями bss (неинициализированные данные), после чего вызывается функция main.
Прежде чем перейти к функции main, обратим внимание на некоторые поля (между метками _start и start_of_setup), которые содержит header.S (в приведенном выше листинге они отсутствуют):
header — магическая сигнатура «HdrS» (Header Signature).
version — содержит версию используемого протокола (boot protocol):
1 |
|
kernel_version — содержит указатель на версию ядра (задана строкой):
1 |
|
type_of_loader — тип загрузчика (LILO, GRUB и т.д.):
1 |
|
Большинство загрузчиков имеют свой идентификатор (ID). Значение 0xTV трактуется следующим образом T — идентификатор загрузчика, V — версия загрузчика. Для данного примера 0xb0 следует: 0xb — загрузчик Qemu, 0x0 — версия 0 (все ID загрузчиков можно посмотреть в файле linux/Documentation/x86/boot.txt).
code32_start — адрес для перехода в защищенный режим:
1 |
|
ramdisk_image — содержит 32-битный линейный адрес местоположения ramdisk или ramfs:
1 |
|
ramdisk_size — содержит размер ramdisk или ramfs:
1 |
(gdb) x/1xw 0x1021c
|
cmd_line_ptr — содержит 32-битный указатель на параметры командной строки:
cmdline_size — максимальная длина аргументов командной строки:
1 |
|
Итак, была вызвана функция main. Это первая функция, написанная на языке С, которая встречается на пути загрузки ядра. Ее определение находится в файле linux/arch/x86/boot/main.c:
Рассмотрим некоторые из функций:
copy_boot_params() — копирует параметры загрузки, определенные в файле header.S («структура» hdr) в структуру boot_params.hdr (определена в файле linux/arch/x86/include/asm/bootparam.h):
1 |
|
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:
Проверяется, был ли установлен режим опроса (vga=«ask» в качестве параметра командной строки), если да, то выводится меню со списком видео режимов (см. рис. ниже), затем устанавливается выбранный режим. Если в командной строке режим не был задан, то будет использован стандартный 80x25 (vga=«normal»).

go_to_protected_mode() — функция определена в файле linux/arch/x86/boot/pm.c и производит заключительные настройки перед переходом в защищенный режим:
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:
1 |
ENTRY(startup_32)
|
В функции startup_32 распаковывается (decompress) ядро, а затем осуществляется к нему переход. За распаковку ядра отвечает функция decompress_kernel() (linux/arch/x86/boot/compressed/misc.c). Если во время распаковки ядра не возникло ошибок, то на экране появится надпись «Decompressing Linux... Booting the kernel». Ядро может быть распаковано или по адресу 0x100000, или, в случае, если ядро было собрано с опцией «CONFIG_RELOCATABLE=y», по любому другому адресу выше 1MB (начиная с адреса 0x100000 и выше).
В любом случае осуществляется переход к распакованному ядру, а именно к функции start_kernel(), которая определена в файле linux/init/main.c:
Если вы посмотрите на содержимое функции start_kernel(), то можете заметить, что она вызывает целую серию других функций, которые инициализируют различные подсистемы ядра и структуры данных. Часть функций с коротким описанием представлена в схеме (см. рисунок ниже), поэтому мы сразу перейдем к рассмотрению заключительного этапа — функции rest_init().

Функция rest_init() создает новый поток ядра, который, в конечном итоге вызывает программу пространства пользователя /sbin/init:
Следует иметь ввиду, что выполняются не все вызовы 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, то он продолжает работу по загрузке системы.
Некоторые ссылки:
Подробности процесса загрузки Linux
Исследуем процесс загрузки Linux
Перевод Linux-Init-HOWTO
P.S. Во-первых традиционная pdf'ка с текстом поста. Во-вторых, если вы найдете смысловые ошибки, то пишите об этом в комментарии, разберемся и исправим, а если же это будут грамматические ошибки или просто очепятки, то пожалуйста пишите в ЛС. Спасибо за внимание, на этом все =)

+ 6 -
Вот это да, спасибо!
WeLinux торт!
Спасибо большое. Очень классный материал - не знаю как удержать свое внимание на работе, а не на копании в приведенных Вами ссылках и указанных исходниках. Еще раз большое Спасибо!
тов. dementiy - это просто как джекпот, аншлаг. какой там ресурс... я в рунете не встречал такого обилия и involve`мента в ассемблер на линукс и ядерный С.
Вам бы в Intel....
Вам бы в Intel....
это при том, что не находится людей (по комметам уже 5й(?) статьи сужу) способных по делу ответить и поговорить... ну нет слов. одни "торты" и эмоции.
Вот. Спасибо Вам и всем комментаторам за приятные слова, но, как Вы подметили нет конструктивной критики. Конечно я всегда стараюсь проверять материал, который пишу (на это уходит большая часть времени). Во-первых, сам материал складывается из двух половинок: моих личных исследований и той информации, которую я могу подчерпнуть из блогов (в основном англо-язычных). Во-вторых я не так много знаю, поэтому могу ошибиться и конечно хотелось бы, чтобы кто-то в таких случаях мог меня поправлять и указывать на эти ошибки.
а если не секрет, откуда интерес?
embedd`ите? портируете на еще одну архитектуру?
embedd`ите? портируете на еще одну архитектуру?
Если честно, то я некоторое время пытался заниматься web'ом, но меня никогда не покидало чувство... абстракции наверное. Мне просто ближе что-то системное, для меня это интересней. А интерес просто есть, мне хочется знать - как это работает.
Только дошли руки прочитать статью более-менее вдумчиво. Огрмное спасибо за проделанную работу. Так шерстить код ядра Linux, просто слов нет...
Пытался копнуть кое-где поглубже чем описано в статье (сподвигли на это дело ссылки, приведенные Вами в конце - Вы некоторые детали опустили, а там вробе и описано, но версия ядра менее актуальна, никаких setup.S, video.S уже нет, а есть setup.ld, отвечающий за конечное компоновку функций в бинарник, да video.c, котрый вызывается в совсем другом месте, чем в той же статье на OpenNet и т.д. и т.п.). Вызвало еще большее уважение.
А Вы не расскопали процесс получения параметров жесткого диска, MC, APM, PS/2 на этапе работы в реальном режиме? Хочется еще больше подробностей и детализации, а то понял отсилы 20% кода в boot :) Хотя уже через пару версий ядра все может быть переписано до неузнаваемости - они как я посмотрю ребята шустрые :)
Пытался копнуть кое-где поглубже чем описано в статье (сподвигли на это дело ссылки, приведенные Вами в конце - Вы некоторые детали опустили, а там вробе и описано, но версия ядра менее актуальна, никаких setup.S, video.S уже нет, а есть setup.ld, отвечающий за конечное компоновку функций в бинарник, да video.c, котрый вызывается в совсем другом месте, чем в той же статье на OpenNet и т.д. и т.п.). Вызвало еще большее уважение.
А Вы не расскопали процесс получения параметров жесткого диска, MC, APM, PS/2 на этапе работы в реальном режиме? Хочется еще больше подробностей и детализации, а то понял отсилы 20% кода в boot :) Хотя уже через пару версий ядра все может быть переписано до неузнаваемости - они как я посмотрю ребята шустрые :)
Спасибо за комментарий. На счет параметров (MCA, APM), если посмотрите в функцию main, то возможно найдете то, что искали (я в эту сторону особо не копал):
arch/x86/boot/main.c:
void main(void)
{
...
/* Query MCA information */
query_mca();
...
/* Query APM information */
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
query_apm_bios();
#endif
}
Спасибо. Я как раз сегодня пересматривал свежим взглядом и обнаружил эти вызовы, правда еще внутрь не заглядывал - выспаться хочу, а не ночами голову ламать, а какэто оно так тут все закручено :)
Еще раз спасибо, за такие интересные материалы.
А Вы не пробовали свои знания приложить к каким-то практическим задчам? М.б. патчики какие-то к ядру пытались писать, или еще как-нибуть его покрутить?
Еще раз спасибо, за такие интересные материалы.
А Вы не пробовали свои знания приложить к каким-то практическим задчам? М.б. патчики какие-то к ядру пытались писать, или еще как-нибуть его покрутить?
Нее, не пробовал, я же не программист =) Как я говорил чуть выше, мне просто интересно, как что-то работает.
А чем же занимаетесь, если не секрет? По-моему, для "непрограммиста" у Вас очень хорошие познания в программировании :)
Нереально здорово!
Не беспокойтесь за отсутствие критики. Я думаю со временем все будет. В том числе и лица, способные вести хорошие, аргументированные споры на Вашем уровне.
Не беспокойтесь за отсутствие критики. Я думаю со временем все будет. В том числе и лица, способные вести хорошие, аргументированные споры на Вашем уровне.
Сейчас всё идёт к упрощению, и освоению линукса виндовс-заточенными пользователями, а вы с ассемблером:)
Статья очень занимательная, понравилась. Прочитав её, в голову приходит только одно - у Торвальдса котелок очень хорошо варил.
Статья очень занимательная, понравилась. Прочитав её, в голову приходит только одно - у Торвальдса котелок очень хорошо варил.
Не просто варил - они продолжают все дальше и дальше писать, допиливать.
В принципе не такая большая сложность написать простенький загрузчик, который будет выполнять похожие действия: проверил несколько параметров, подгрузил сжатое ядро, распаковал его, но на практике учесть все то, что учитывается в этом коде очень и очень сложно. Вон у детища Таненбаума до сих пор насколько я понимаю нет поддержки многопроцессорности, потому-что там привычные академические методы управления ресурсами не действуют :)
В принципе не такая большая сложность написать простенький загрузчик, который будет выполнять похожие действия: проверил несколько параметров, подгрузил сжатое ядро, распаковал его, но на практике учесть все то, что учитывается в этом коде очень и очень сложно. Вон у детища Таненбаума до сих пор насколько я понимаю нет поддержки многопроцессорности, потому-что там привычные академические методы управления ресурсами не действуют :)
уточните,о какой именно функции идет речь,т.к startup_32() две штуки ) одна находится в arch/x86/boot/compressed/head.S вторая в arch/x86/kernel/head.S и обе они связанны с загрузкой,только первая работает до распаковки ядра,вторая - после)
Итак, мы перешли в защищенный режим по адресу 0x100000. Этот адрес служит точкой входя для функции startup_32, которая определена в файле linux/arch/x86/boot/compressed/head_32.S:
извеняюсь,не внимателен сегодня)
Нашел кстати одну мелкую ошибочку: не boot/misc.c, а boot/compressed/misc.c в тексте, хотя ссылка правильная
Только начинаю копать исходники Linux и возник вопрос.
На сколько я понимаю эта часть файла header.S является загрузчиком который находится в первом секторе HDD и которую считывает BIOS и запускает по адресу 7С00, и вроде этот код всегда будет перезапускаться и вызываться снова, а как управление передается на метку _start?
...
.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
...
На сколько я понимаю эта часть файла header.S является загрузчиком который находится в первом секторе HDD и которую считывает BIOS и запускает по адресу 7С00, и вроде этот код всегда будет перезапускаться и вызываться снова, а как управление передается на метку _start?
Ох, девичья моя память =) Итак, Вы верно подметили, что это часть файла header.S, но данная часть кода выполняется только в том случае, если загрузка происходит с дискеты (поэтому данный код при загрузке вызываться не будет), если же загрузка происходит с диска, то код выполняется с метки _start. Где именно передается управление этой метке я не могу вспомнить (вроде как определяется загрузчиком, но.... могу только посоветовать взять Qemu + GDB и попробовать это выяснить самостоятельно). И в посте отсутствуют изображения (видимо с fastpic они пропали, надо будет их перезалить), поэтому дополнительно узурпируйте pdf.