среда, 10 июля 2013 г.

PhantomEx: Взгляд внутрь ядра

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

Речь идет о том, чтобы заглянуть внутрь сгенерированного компилятором кода, пока его не слишком уж много, и уяснить для себя во что конкретно превращаются исходные тексты в итоге. И если для прикладного программиста такое занятие может и пустая трата времени, то для системного - наоборот, приобретение необходимого опыта.

Воспользуемся редактором HT-editor для того чтобы изучить бинарный файл kernel, а так же дизассемблируем его, чтобы посмотреть структуру сгенерированного кода.


1. Ищем заголовок Multiboot


Установка HT-editor не представляет особого труда, поэтому её описывать мы не будем. Это неплохой двоичный редактор с функцией дизасемблера. Откроем в нем файл ядра kernel

$ ht ~/myOSkernel/src/kernel

 и увидим мы вот это

ELF-заголовок ядра


сплошную абракадабру из чисел. Но не все так страшно - в самом начале файла ядра расположен ELF-заголовок, добавленный туда компилятором. Наше ядро имеет формат ELF32 - исполняемый файл для 32-разрядных версий Linux. GRUB2 понимает такой формат и в состоянии загрузить его в память и передать управление в нужную точку.

А где же наш Multiboot-заголовок? Помните о том, что мы специально настроили компоновщик таким образом, что содержимое исполняемого файла ядра выравнено по границам 4 Кб блоков. На приведенном скриншоте - некий массив ненулевых значений, а вот дальше - сплошные нули. Ими компоновщик забил все пространство блока размером 4 Кб, которое оказалось не занято ELF-заголовком. Значит искать сигнатуру Multiboot необходимо в следующем 4 Кб блоке, то есть по смещению 0x1000 (4096 байт) от начала файла. Идем туда, и туда же, заметьте, пойдет и загрузчик, прочитав ELF-заголовок и поняв что это не Mulltiboot-сигнатуры.

Продвинемся по файлу, нажав в редакторе F5 и в выскочившем окне введя смещение 4096 или 1000h (вообще привыкайте к 16-ричным числам, это удобно на самом деле)


 и мы попадем в начало блока, где снова лежит что-то осмысленное

Сигнатура Multiboot
Посмотрим внимательнее - да-да-да, по смещению 0x1000 мы нашли число 0x1BADB002 - "магическое число" заголовка Multiboot. Порядок байт обратный - такова особенность процессоров Intel - младший байт расположен по младшему адресу. Дальше следует поле флагов 0x00000001, и контрольная сумма 0xE4524FFD. На ней задержимся чуть подольше.

Если вы помните алгоритм вычисления контрольной суммы, то для её вычисления компилятор делает следующее

0x00000000 - (0x1BADB002 + 0x00000001) = 0xFFE4524FFD

а это значение не 32- а 40-битное, старший байт (0xFF), не помещающиеся в разрядную сетку, обрезается. В итоге и образуется найденное нами значение  0xE4524FFD.

GRUB2 в своем алгоритме очевидно учитывает этот потерянный байт 0xFF, вычисляя контрольную сумму на каких-нибудь MMX-регистрах разрядность 64-бит, в итоге убеждается в правильности контрольной суммы и заголовка в целом и дает добро на загрузку ядра.

Если мы заглянем чуть выше смещения 0x1000 - там одни нули вплоть до ELF-заголовка, можете проверить это, в принципе не долго пролистать эти 4 килобайта.

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

Дальше GRUB просто грузит ядро в память, по адресу 0x100000 (мы его тоже задали компоновщику!) и передает управление на entrypoint - точку входа, которую берет из ELF-заголовка.

2. ELF-заголовок. Передача управления ядру.


Чтобы посмотреть на структуру ELF-заголовка, нажмем в редакторе F6 и в появившемся меню выберем пункт "elf/header".

и вместо абракадабры, которую мы видели в начале получим структурированную таблицу полей ELF-заголовка.


и среди прочей, несомненное полезной, информации видим поле entrypoint c с адресом 0x0010000C. Это и есть точка входа, на которую передает управление загрузчик.

Идем по этому адресу - жмем F6 и в меню выбираем пункт "elf/image".


и видим мы... дизасссемблерный листинг нашего ядра! И стоим мы как раз на указанном в entrypoint адресе 0x0010000C.Весь код на скриншоте ниже, и мы можем сравнить его с тем что совсем недавно написали сами.

Здесь присутствуют настоящие, данные нами имена функций и меток. Это не случайно - мы сказали компилятору добавить отладочную информацию в код, а таблица символов - соответствие между адресами и символическими именами в исходном коде - входит в число такой информации. К таблице символов мы ещё вернемся, а пока сравним сгенерированный код с исходным.

Так как мы не использовали опций оптимизации, в ассемблерной части мы получили тоже самое что и писали сами. Любопытно выглядит дизассемблированная функция main(), не делающая ничего кроме возврата нуля через регистр eax.

По этому листингу можно без труда отследить выполнение кода ядра после загрузки: выключаются прерывания, вызывается функция main(), которая, отработав, возвращает ноль. Дальше процессор останавливается и переходит в бесконечный цикл.

3. Анализ символьной информации


Рассмотрим теперь таблицу символов - F6 в меню выбираем "elf/symbol table"


Эта таблица соответствий между адресами функций, меток и их символическими именами, между значениями констант и их именами. Здесь к примеру мы обнаруживаем константы полей заголовка Multiboot,

но самое интересное заключено в именах с пометкой global.

эти имена, а точнее адреса по которым они расположены, образно говоря "торчат наружу" из объектных модулей, где они определены, то есть доступны из других объектных модулей при ипользовании директивы extern языка С. Это дает возможность связывать между собой объектные модули.
Что мы делали когда писали код? Функция main() объявлена у нас глобальной в файле main.c. Это дает возможность нам, с помощью директивы .extern увидеть эту функцию в файле init.s. Там же метка init объявлена глобальной, дабы быть доступной компоновщику при установке точки входа (вспоминаем директиву ENTRY(init) в файле link.ld). 

Ну и уж отдельного внимания заслуживает вот эта метка


Сгенерировали мы её сами, дав соответствующее указание компоновщику в файле link.ld. Для чего?
Самая важная информация тут - адрес 0x001005C - эта та точка, в которой мы уже не столкнемся ни с данными ни с кодом, принадлежащим ядру. Весь код ядра ниже этого адреса, это начало того места в памяти, с которого она свободна для использования.

Ядро будет расти - пока что его код занимает 0x5C = 92 байта. Но он будет расти, первый свободный адрес будет уходить всё выше и выше. А компоновщик будет связывать его с глобальной меткой end, пользуясь которой мы сможем рассчитывать данный адрес автоматически. Это пригодится нам, когда мы начнем динамически выделять память для работы ядра (организуем heap - "кучу" ядра), а так же для организации страничной адресации, без которой немыслима многозадачность. Но это будет дальше, пока же предлагаю просто запомнить предназначение метки end.

Заключение


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