Как стать автором
Обновить

FreeBSD versus GRUB

Время на прочтение6 мин
Количество просмотров4.8K
Добрый день. Эта заметка посвящена моей частной борьбе против GRUB'a, ну или за него, это как посмотреть.
Понадобилось поставить 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'ами
  • Немножко о формате слайсов и фс.
Теги:
Хабы:
Всего голосов 68: ↑63 и ↓5+58
Комментарии8

Публикации

Истории

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань