Добрый день. Эта заметка посвящена моей частной борьбе против GRUB'a, ну или за него, это как посмотреть.
Понадобилось поставить grub-загрузчик на freebsd, конечно многие знают про chainloader+1, но этот способ не годился для меня.
Итак, ставим граб (первый, второй мне показался слишком монстроузным):
Теперь необходимо собственно прописать его как загрузчик
Вот здесь-то и пошли проблемы. Он сообщает нам прискорбную весть — «The file /boot/grub/stage1 not read correctly.».
Как же так? Я же сам видел этот файлик, когда проверял поставился ли граб.
Ладно, гляну логи:
Ошибка мне ни о чём не говорит, ибо раздел само собой есть. Гугл не помог, разве что говорилось о баге с большим размером инодов в ext3, но у меня же ufs, а там размер фиксирован и равен 128 байтам. Поэтому я принял решения копать сорцы самому, несмотря на то, что код написан ужасно в плане оформления и структуры.
Начинаем исследование с определения константы соответствующей этой ошибке, это просто — ERR_NO_PART, далее глядим кто может возвращать эту ошибку (на самом деле в grub'e реализован некий аналог errno, то есть глобальная переменная, которая содержит номер последней ошибки).
Таких мест всего 2, причём располагаются в 2х соседних лямбда-функциях (привет gcc!) next_bsd_partition() и next_pc_slice() по названию всё понятно — вторая перечисляет bsd-слайсы, первая — разделы в этих слайсах.
Я сразу отмел идею статического анализа, из-за структуры кода grub'a — очень, ну просто, очень много глобальных переменных и почти нет аргументов у функций. Отлаживать с gdb мне тоже не очень хотелось, ибо нет большого опыта в этом и всё происходило на удаленном серваке, так что какой-нибудь xxgdb использовать не представлялось возможным. Поэтому я избрал простой подход новичков — в непонятные места вставляем printf'ы. Здесь у меня тоже возникли трудности:
Хотелось более быстрой проверки, в самом деле — не заходить же каждый раз в консоль граба и не набирать 2 строчки. Это выполняется простой командой (которую я подсмотрел в шел-скрипте grub-install):
Где cmd содержит список команд, у меня в данном случае это:
Рабочее место готово, начинаем «отладку». Вставляем printf() в начала тех двух функций и смотрим, на каком уровне иерархии споткнемся.
Споткнулись на разделах, вот такой вот у нас здесь цикл:
Кстати, можно обратить внимание на интересную позицию разработчиков — отступы реализованы с использованием комбинации табов и пробелов, то есть 2 таба, 4 пробела, например. Что очень «нравится» редактору, в котором я правил код.
Если ни разу не сработало условие BSD_PART_TYPE (buf, i), то выводим что кончились патриции.
Этот дефайн «очень» понятен. За разъяснениями пришлось идти к «Booting, Partitioning and File Systems» доктора C. P. J. Koymans.
Там представлена структура слайса, в которой мы и видим что по смещению 12 от блока описания одного раздела лежит поле Partition type, а у нас оно по-видимому равно 0, раз условие не срабатывает.
Проверяем — bsdlabel /dev/ad8s1, действительно — написано что unused. Ну что же, поправить дело недолгое — bsdlavel -e /dev/ad8s1. А вот не хочет — «class not found».
Само собой перед установкой граба флажок я установил (sysctl kern.geom.debugflags=16), погуглил, советуют ставить 17 в последних версиях фряхи, поставил — результата нет.
Копаться еще и в сорцах bsdlabel'a не было никакого желания, поэтому поступил проще ktrace bsdlabel -e /dev/ad8s1, затем kdump | more.
«Operation not permitted», видимо не судьба отредактировать системный диск на лету с помощью bsdlabel.
Но ничего страшного — есть gpart:
Индекс 1, ибо правим раздел 'a', такая логика. /dev/ad8s1, а не /dev/ad8, потому что редактируем не таблицу слайсов (MBR в моём случае), а сам слайс.
Проверяем bsdlabel'ом — успех. Скрестив пальцы, проверяем как граб отнесется к этому. Отнесся он положительно, но не до конца. Ошибку 22 заменила новая — Error 17: Cannot mount selected partition.
Из названия всё ясно — ошибка с монтированием ufs. Однако не поленимся и поставим сигнальный printf в ufs2_mount().
Так и есть. Функция фейлится. Причина тоже ясна — не может найти суперблок. Это структура, которая содержит базовую информацию о ФС (размер нодов, размер кластеров, флажки, и так далее).
Довольно распространенная структура в архитектуре ФС, есть она и в ufs2, однако grub её не находит. Давайте-ка глянем где она «сидит» — dumpfs / | head -2 выдает такую строчку «superblock location 65536».
Смотрим в grub:
Видим заветное число, почему же не находим?
65536 — это смещение в байтах, относительно чего оно считается я точно был не уверен, поэтому брал с запасом:
Читаем 200 первых секторов (сам раздел начинался с 16 сектора, 128 (смещение суперблока) + 16 (максимальная точка отсчета) + 16 (размер суперблока) < 200, люфт для надежности).
grep 54 — это мы получаем строки, которые содержат байт 0x54, который есть в сигнатуре суперблока, полный размер подписи — 4 байта.
Но как искать dword'ами сразу в голову не пришло, поэтому просмотрел пару десятков строк. Полностью сигнатура нашлась в 69 секторе, ажно 2 раза, что меня смутило. Пригляделся к dump'у, там дальше идёт упоминание FS_UFS1_MAGIC, поэтому это скорее всего код лоадера, а не сам суперблок.
Потом мне подумалось что всё же лучше поискать внутри раздела:
И здесь меня ждал успех — по смещению 0x10550 (66896) видим нашу сигнатуру.
Смещение не равно ровно 65536, потому что сигнатура лежит в конце суперблока а не в его начале, поэтому добавляется «дельта» (FIELD_OFFSET()), которая меньше 8192.
Как видим суперблок присутствует, и именно по тому смещению от начала раздела, где и ожидается. Определим смещение относительно, Тупо пробежимся по первым n секторам с известной строкой поиска.
Получим — 0001a350 (107344), что равно 209 сектору, Занятно то, что наша прикидочная оценка не добежала совсем чуть-чуть. Это получилось из-за того, что я забыл прибавить начало самого слайса (63), 63 + 16 + 128 == 207,
А 2 сектора занимают остальные поля суперблока.
Целевой сектор вычисляется в devread(sector, byte_offset) просто:
Переменные sector и byte_offset — аргументы и в нашем случае равны 0 и 65536 соответственно, что конечно правильно. А вот part_start выдаёт 16. Что тоже верно. Но граб воспринимает это как абсолютное значение, относительно начала диска, а не начала слайса.
В мане написано "The offset of the start of the partition from the beginning of the drive in sectors", но в то же время приводится "The first partition should start at offset 16".
Но так как система работает, и всё монтируется успешно, думается что баг в грабе. Поэтому фиксим его, добавляя всего лишь один символ:
И теперь всё проходит успешно — part_start = 0x4F, и дамп выполняется.
Использовать будем пересобранный граб, чтоб мой фикс вошёл и в stage1_5/stage2.
grub_install прошёл корректно, всё отлично.
Немного не по теме, но касается ufs, grub'a и фряхи.
Максимальный размер файла, который может прочитать grub:
Параметры суперблока берутся из лога dumpfs.
У меня на системе получилось ~32 метра, что не очень радует меня, поэтому я вырезал проверку из кода.
Для этого комментируем эту строчки в fsys_ufs2.c:
Вот такая вот небольшая история о том как я запускал grub на фряхе.
Что здесь есть полезного:
Понадобилось поставить grub-загрузчик на freebsd, конечно многие знают про chainloader+1, но этот способ не годился для меня.
Прелюдия
Итак, ставим граб (первый, второй мне показался слишком монстроузным):
# cd /usr/ports/sysutils/grub && make install
Теперь необходимо собственно прописать его как загрузчик
# grub-install /dev/ad8
Вот здесь-то и пошли проблемы. Он сообщает нам прискорбную весть — «The file /boot/grub/stage1 not read correctly.».
Как же так? Я же сам видел этот файлик, когда проверял поставился ли граб.
Ладно, гляну логи:
# cat /tmp/grub-install.log.XXXXX
grub> dump (hd2,0,a)/boot/grub/stage1 /tmp/grub-install.img.XXXXX
Error 22: No such partition
grub> quit
Ошибка мне ни о чём не говорит, ибо раздел само собой есть. Гугл не помог, разве что говорилось о баге с большим размером инодов в ext3, но у меня же ufs, а там размер фиксирован и равен 128 байтам. Поэтому я принял решения копать сорцы самому, несмотря на то, что код написан ужасно в плане оформления и структуры.
Error 22
Начинаем исследование с определения константы соответствующей этой ошибке, это просто — ERR_NO_PART, далее глядим кто может возвращать эту ошибку (на самом деле в grub'e реализован некий аналог errno, то есть глобальная переменная, которая содержит номер последней ошибки).
Таких мест всего 2, причём располагаются в 2х соседних лямбда-функциях (привет gcc!) next_bsd_partition() и next_pc_slice() по названию всё понятно — вторая перечисляет bsd-слайсы, первая — разделы в этих слайсах.
Я сразу отмел идею статического анализа, из-за структуры кода grub'a — очень, ну просто, очень много глобальных переменных и почти нет аргументов у функций. Отлаживать с gdb мне тоже не очень хотелось, ибо нет большого опыта в этом и всё происходило на удаленном серваке, так что какой-нибудь xxgdb использовать не представлялось возможным. Поэтому я избрал простой подход новичков — в непонятные места вставляем printf'ы. Здесь у меня тоже возникли трудности:
- При компиляции alpha.gnu.org/gnu/grub/grub-0.97.tar.gz получался другой результат по сравнению с компиляцией из портов. Как и по бинарной структуре (например, размер ./grub/grub ~ 350Kb против 150Kb портовых), так и по функционалу — root (hd _TAB_ выдавал всего 2 диска, вместо правильных 5.
Этот момент я обошел крайне просто — скомпилировал вариант из портов, но очищать (make clean) не стал, и из /usr/src/ports/sysutils/grub/work/grub забрал корректную версию сорцев и правильно сконфигуренную. - printf у граба свой.
#ifndef WITHOUT_LIBC_STUBS ... #define printf grub_printf ... #endif
Хотел перекомпилировать без этого дефайна, но глянул на их версию, оставил так как есть. Поддерживает только d, x, X, u, c, s, и без всякой точности и ширины, просто %LITERAL, в принципе мне этого вполне хватило.
Хотелось более быстрой проверки, в самом деле — не заходить же каждый раз в консоль граба и не набирать 2 строчки. Это выполняется простой командой (которую я подсмотрел в шел-скрипте grub-install):
./grub/grub --batch < cmd
Где cmd содержит список команд, у меня в данном случае это:
dump (hd2,0,a)/boot/grub/stage1 test_img
quit
Рабочее место готово, начинаем «отладку». Вставляем printf() в начала тех двух функций и смотрим, на каком уровне иерархии споткнемся.
Споткнулись на разделах, вот такой вот у нас здесь цикл:
/* Search next valid BSD partition. */
for (i = bsd_part_no + 1; i < BSD_LABEL_NPARTS (buf); i++)
{
if (BSD_PART_TYPE (buf, i))
{
/* Note that *TYPE and *PARTITION were set
for current PC slice. */
*type = (BSD_PART_TYPE (buf, i) << 8) | (*type & 0xFF);
*start = BSD_PART_START (buf, i);
*len = BSD_PART_LENGTH (buf, i);
*partition = (*partition & 0xFF00FF) | (i << 8);
#ifndef STAGE1_5
/* XXX */
if ((drive & 0x80) && BSD_LABEL_DTYPE (buf) == DTYPE_SCSI)
bsd_evil_hack = 4;
#endif /* ! STAGE1_5 */
return 1;
}
}
Кстати, можно обратить внимание на интересную позицию разработчиков — отступы реализованы с использованием комбинации табов и пробелов, то есть 2 таба, 4 пробела, например. Что очень «нравится» редактору, в котором я правил код.
Если ни разу не сработало условие BSD_PART_TYPE (buf, i), то выводим что кончились патриции.
#define BSD_PART_TYPE(l_ptr, part) \
( *( (unsigned char *) (((int) l_ptr) + BSD_PART_OFFSET + 12 \
+ (part << 4)) ) )
Этот дефайн «очень» понятен. За разъяснениями пришлось идти к «Booting, Partitioning and File Systems» доктора C. P. J. Koymans.
Там представлена структура слайса, в которой мы и видим что по смещению 12 от блока описания одного раздела лежит поле Partition type, а у нас оно по-видимому равно 0, раз условие не срабатывает.
Проверяем — bsdlabel /dev/ad8s1, действительно — написано что unused. Ну что же, поправить дело недолгое — bsdlavel -e /dev/ad8s1. А вот не хочет — «class not found».
Само собой перед установкой граба флажок я установил (sysctl kern.geom.debugflags=16), погуглил, советуют ставить 17 в последних версиях фряхи, поставил — результата нет.
Копаться еще и в сорцах bsdlabel'a не было никакого желания, поэтому поступил проще ktrace bsdlabel -e /dev/ad8s1, затем kdump | more.
74340 bsdlabel CALL open(0x28201030,O_RDWR,[unused]0xbfbfe9e8)
74340 bsdlabel NAMI "/dev/ad8s1"
74340 bsdlabel RET open -1 errno 1 Operation not permitted
74340 bsdlabel CALL open(0x28097653,O_RDONLY,[unused]0x28050629)
74340 bsdlabel NAMI "/dev/geom.ctl"
74340 bsdlabel RET open 3
74340 bsdlabel CALL ioctl(0x3,GEOM_CTL,0x28203040)
74340 bsdlabel RET ioctl 0
«Operation not permitted», видимо не судьба отредактировать системный диск на лету с помощью bsdlabel.
Но ничего страшного — есть gpart:
gpart modify -i 1 -t freebsd-ufs /dev/ad8s1
Индекс 1, ибо правим раздел 'a', такая логика. /dev/ad8s1, а не /dev/ad8, потому что редактируем не таблицу слайсов (MBR в моём случае), а сам слайс.
Проверяем bsdlabel'ом — успех. Скрестив пальцы, проверяем как граб отнесется к этому. Отнесся он положительно, но не до конца. Ошибку 22 заменила новая — Error 17: Cannot mount selected partition.
Error 17
Из названия всё ясно — ошибка с монтированием ufs. Однако не поленимся и поставим сигнальный printf в ufs2_mount().
Так и есть. Функция фейлится. Причина тоже ясна — не может найти суперблок. Это структура, которая содержит базовую информацию о ФС (размер нодов, размер кластеров, флажки, и так далее).
Довольно распространенная структура в архитектуре ФС, есть она и в ufs2, однако grub её не находит. Давайте-ка глянем где она «сидит» — dumpfs / | head -2 выдает такую строчку «superblock location 65536».
Смотрим в grub:
#define SBLOCK_FLOPPY 0
#define SBLOCK_UFS1 8192
#define SBLOCK_UFS2 65536
#define SBLOCK_PIGGY 262144
#define SBLOCKSIZE 8192
#define SBLOCKSEARCH \
{ SBLOCK_UFS2, SBLOCK_UFS1, SBLOCK_FLOPPY, SBLOCK_PIGGY, -1 }
Видим заветное число, почему же не находим?
65536 — это смещение в байтах, относительно чего оно считается я точно был не уверен, поэтому брал с запасом:
dd if=/dev/ad8 bs=512 count=200 | hexdump -C | grep 54
Читаем 200 первых секторов (сам раздел начинался с 16 сектора, 128 (смещение суперблока) + 16 (максимальная точка отсчета) + 16 (размер суперблока) < 200, люфт для надежности).
grep 54 — это мы получаем строки, которые содержат байт 0x54, который есть в сигнатуре суперблока, полный размер подписи — 4 байта.
#define FS_UFS2_MAGIC 0x19540119 /* UFS2 fast filesystem magic number */
Но как искать dword'ами сразу в голову не пришло, поэтому просмотрел пару десятков строк. Полностью сигнатура нашлась в 69 секторе, ажно 2 раза, что меня смутило. Пригляделся к dump'у, там дальше идёт упоминание FS_UFS1_MAGIC, поэтому это скорее всего код лоадера, а не сам суперблок.
Потом мне подумалось что всё же лучше поискать внутри раздела:
dd if=/dev/ad8s1a bs=512 count=200 | hexdump -C | grep 54
И здесь меня ждал успех — по смещению 0x10550 (66896) видим нашу сигнатуру.
Смещение не равно ровно 65536, потому что сигнатура лежит в конце суперблока а не в его начале, поэтому добавляется «дельта» (FIELD_OFFSET()), которая меньше 8192.
Как видим суперблок присутствует, и именно по тому смещению от начала раздела, где и ожидается. Определим смещение относительно, Тупо пробежимся по первым n секторам с известной строкой поиска.
Получим — 0001a350 (107344), что равно 209 сектору, Занятно то, что наша прикидочная оценка не добежала совсем чуть-чуть. Это получилось из-за того, что я забыл прибавить начало самого слайса (63), 63 + 16 + 128 == 207,
А 2 сектора занимают остальные поля суперблока.
Целевой сектор вычисляется в devread(sector, byte_offset) просто:
sector + (byte_offset >> SECTOR_BITS) + part_start
Переменные sector и byte_offset — аргументы и в нашем случае равны 0 и 65536 соответственно, что конечно правильно. А вот part_start выдаёт 16. Что тоже верно. Но граб воспринимает это как абсолютное значение, относительно начала диска, а не начала слайса.
В мане написано "The offset of the start of the partition from the beginning of the drive in sectors", но в то же время приводится "The first partition should start at offset 16".
Но так как система работает, и всё монтируется успешно, думается что баг в грабе. Поэтому фиксим его, добавляя всего лишь один символ:
*start += BSD_PART_START (buf, i);
И теперь всё проходит успешно — part_start = 0x4F, и дамп выполняется.
Использовать будем пересобранный граб, чтоб мой фикс вошёл и в stage1_5/stage2.
grub_install прошёл корректно, всё отлично.
Ограничение на размер считываемого файла
Немного не по теме, но касается ufs, grub'a и фряхи.
Максимальный размер файла, который может прочитать grub:
(12 + superblock.nindir) * superblock.bsize
Параметры суперблока берутся из лога dumpfs.
У меня на системе получилось ~32 метра, что не очень радует меня, поэтому я вырезал проверку из кода.
Для этого комментируем эту строчки в fsys_ufs2.c:
fsmax = (NDADDR + NINDIR (SUPERBLOCK)) * SUPERBLOCK->fs_bsize;
Финита
Вот такая вот небольшая история о том как я запускал grub на фряхе.
Что здесь есть полезного:
- Использование связки ktrace/kdump
- Упоминание dumpfs/hexcode/gpart с usecase'ами
- Немножко о формате слайсов и фс.