Multiboot Specification описывает интерфейс между универсальным загрузчиком и операционными системами. Эталонной реализацией этого интерфейса является GRUB.
Вот статья на русском, где даётся описание интерфейса и рекомендации, как его реализовать в простейшей программе на Си. Там же замечается, что заголовок должен находиться в первых 8 килобайтах загрузочного elf-файла.
Чуть усложнив авторский пример и затащив в него компилятор C++ вместо C, я обнаружил, что последнее требование перестаёт выполняться: заголовок прыгает с авторского смещения 0x1000 на 0x2000. Соответственно, GRUB его не находит и отказывается грузить такую «ОС».
К разбору полётов была привлечена программа objdump с ключиком -x. Она отрапортовала, что вторые 4 килобайта занимает некая секция .note.gnu.build-id. Информацию по этой секции можно найти
здесь. В общем, как я понял, это какой-то уникальный идентификатор сборки.
Удаляем секцию .note.gnu.build-id:
strip -R .note.gnu.build-id mykernel
После этого наша «ОС» нормально грузится из GRUB.