Всем привет, это Тимур. Сейчас в YADRO я разрабатываю сетевую операционную систему для коммутаторов KORNFELD. В ходе этого проекта летом я получил сложную задачу: реализовать установку опции PROTO_DOWN для Ethernet-интерфейсов в ядре Linux. «Из коробки» ядро поддерживает эту опцию только для vxlan и macvlan-интерфейсов, а для Ethernet поддержка определяется драйвером сетевого устройства.
В статье я подробно расскажу о той части задачи, что касается непосредственно ядра Linux. На реализацию требования я потратил много сил, времени и хочу, чтобы мой опыт остался в сообществе. Постараюсь максимально подробно объяснить каждый шаг, чтобы вы смогли построить собственный процесс разработки на этой основе, даже если никогда не занимались разработкой для ядра. Статья будет полезна не только программистам, но и любителям Linux, так как в большей степени она посвящена тонкостям работы с этой ОС, а не программированию.

Вначале я должен был выяснить, как работает PROTO_DOWN в ядре, и понять, почему наш драйвер его не поддерживает. После — придумать, как это исправить и связать с нашим кодом.
Это была первая в моей жизни задача, связанная с кодом ядра Linux, и я ей очень обрадовался. За спиной было три года использования Arch основной системой и шесть лет других дистрибутивов Linux. Я имел базовое представление об устройстве ОС и ее компонентов. Мне всегда было интересно, как работают вещи, поэтому я когда-то начал использовать Linux, а потом перешел на Arch. И вот настал новый этап — узнать, насколько глубока кроличья нора.
Первым делом мне нужна была среда для экспериментов. Очень глупо и опасно пытаться изменять ядро своей хостовой системы для разработки — и, конечно же, это был мой первый шаг. Но я даже ничего не сломал, поэтому этот этап мы опустим. Следующий шаг — использование виртуальной машины. Устанавливать систему руками через графический инсталлятор, на мой взгляд, долго и неинтересно. Но i use arch btw, так что умею устанавливать систему с помощью инсталлятора в текстовом режиме.
Далее я нашел способ смонтировать виртуальный диск к хостовой системе и установил на него систему. Эта установка ничем не отличается от установки системы с флешки с образом live cd. Так у меня появился скрипт для автоматического создания образа виртуальной машины с Arch.

Создать образ с помощью скрипта можно очень быстро, у меня это занимает менее минуты.

Следующий шаг — сборка ядра. Она проходит в контейнере, а не в виртуальной машине. Это позволит сэкономить ресурсы и ускорить сборку. Сама сборка элементарна: вызвать make с необходимой целью и все. Самостоятельно сконфигурировать ядро очень сложно, и на это уйдет много времени. А сделать конфиг, который запустится, еще сложнее. Изначально я пытался настраивать его сам через графическую утилиту. Ядро, собранное таким образом, не запускалось, и я понял, что это не вариант. Нужна нормальная конфигурация.
Обычно в рабочей системе есть конфиг ядра, содержащий параметры конфигурации, использованные при сборке. Как правило, он лежит по пути /boot/config-*. На наших свичах он тоже есть, поэтому я взял в качестве основы именно его. Вместе с получением нормального конфига я перешел на виртуальный Debian, так как он ближе к нашей оси.
Система сборки ядра поддерживает множество разных целей для сборки:

Debian использует apt, поэтому я собирал deb-пакет. Распространение в виде пакета — стандартный способ обновления ядра во многих дистрибутивах. На тот момент я плохо понимал, как это все работает, так что это было оптимальным выбором для начала.

Я перенес пакет в виртуальную машину и установил его с помощью пакетного менеджера. После перезапуска ожидал увидеть свое новое ядро в меню загрузчика, но его там не было. К счастью, старое ядро не удалялось, поэтому получилось загрузиться в систему с его помощью.
Grub не показывал ядро, и я предположил, что проблема — в конфигурации загрузчика. Открыл grub.cfg и не нашел в нем информации о моем ядре. Вспомнил, что для Arch существует скрипт, генерирующий настройки для загрузчика, нашел такой же скрипт для Debian. Но конфиг, созданный таким способом, в моем случае не заработал: система просто не загружалась.
Чтобы не разбираться в автосгенерированном файле и не пытаться чинить его, я решил разобраться в том, как этот файл работает. После изучения синтаксиса создал конфигурацию сам, и загрузиться в свое ядро получилось. Это стало подтверждением работоспособности такого варианта, и поэтому вы сейчас читаете этот текст. :)
Команда YADRO KORNFELD ищет специалистов разного профиля, в том числе:
Мы будем рады вашим откликам!
Подготовка окружения
Вам потребуется QEMU для запуска виртуалок и Docker для разработки. Помимо очевидных вещей, Docker нужен, чтобы можно было работать с другим дистрибутивом в виртуалке через chroot. Прежде чем засунуть все в контейнеры, я пробовал делать все с хоста. Но сидя на Arch и работая с Debian, обойтись без контейнеров я не смог.
QEMU и Docker — это главное, что вам потребуется. Остальные утилиты предустановлены в большинство дистрибутивов, а если и нет, то их ручная установка не вызовет проблем. Скрипт в моем репозитории проверит вашу систему на наличие необходимых утилит и сообщит об отсутствующих командах.
Установка Debian
Я очень не люблю ручную установку Debian, поэтому обрадовался возможности ставить систему консольными командами. Это позволяет автоматизировать создание образа и ускоряет подготовку рабочего окружения. Возможно, вы используете иной дистрибутив и не знаете как или не можете установить его командами. В такой ситуации можно прибегнуть к ручной установке системы. Главное — получить на выходе qcow2-диск. Можно использовать и другой формат, но тогда придется менять мои скрипты, так как они заточены под qcow2.
Первым делом нам нужно создать виртуальный диск. На нем будет находиться виртуальная система. Создайте образ в формате raw.
qemu-img create -o preallocation=falloc -o nocow=on -f raw disk.raw 10G
Эта команда создаст файл с виртуальным диском в формате raw, удобном для начала работы. С помощью опции preallocation мы просим QEMU выделить место под метаданные, а остальное место оставить нераспределенным. 10G, очевидно, указывает на размер диска. В дальнейшем мы конвертируем raw в qcow2, и файл будет занимать столько места, сколько требует система.
Опция nocow применима только к btrfs, а на остальных файловых системах она ничего не делает. Для btrfs она должна нивелировать ухудшение производительности. Вот что об этом говорит документация QEMU:
Btrfs has low performance when hosting a VM image file, even more when the guest on the VM also using btrfs as file system. Turning off COW is a way to mitigate this bad performance.
У меня btrfs и диск на PCIe 5.0, и я не заметил никакой разницы. Но если хотите, cow можете убрать.
Далее нужно отформатировать виртуальный диск. Мы будем использовать gpt-разметку с Legacy BIOS. Почему не UEFI? Для виртуального UEFI нужен OVMF, программное обеспечение для поддержки UEFI в виртуальных машинах. Для BIOS не нужно ничего, поэтому этот вариант проще. Можно использовать и UEFI с OVMF, но если вы собираете под какую-то нестандартную архитектуру, то могут возникнуть проблемы с получением OVMF. Поэтому мы будем использовать BIOS. Раздел с ним требует 1 МБ, оставшееся пространство выделим под систему и отформатируем в ext4.
Следующая команда создаст gpt-таблицу на диске:
parted -s disk.raw mktable gpt
С помощью следующей команды в файле будет создан раздел размером 1 МБ. Этот раздел будет находиться с первого по второй мегабайт в адресном пространстве диска. Это первичный раздел, который нужен для загрузки системы.
parted -s disk.raw mkpart primary 1M 2M
Далее нужно пометить его как загрузочный. Флаг bios_grub укажет на то, что мы будем использовать BIOS. А «1» указывает, что это раздел для загрузки:
parted -s disk.raw set 1 bios_grub on
Последним шагом мы выделим все оставшееся место на диске под корень нашей системы. Раздел будет первичным и начнется с 2 МБ, сразу после раздела под BIOS. «100%» указывает на использование всего остального пространства.
parted -s disk.raw mkpart primary ext4 2M 100%
После выполнения команд виртуальный диск разбивается на два раздела. Первый нужен для загрузки, и мы не будем больше его трогать. Второй раздел будет содержать систему и все необходимое для работы.
Теперь надо подключить виртуальный диск к хостовой системе. Сделать это можно с помощью loop-устройства. Эта команда эквивалентна подключению физического диска к системе, но только работает с виртуальным:
sudo losetup --find --partscan disk.raw
В вашей системе появится новое виртуальное файловое устройство, которое связано с файлом виртуального диска. Узнайте, куда подключен диск:
losetup -a
/dev/loop0: []: (/home/mrognor/Desktop/Qemu/disk.raw)
Эта команда показывает, что девайс /dev/loop0 указывает на файл disk.raw. Название устройства может отличаться, так как это зависит от вашей системы. Здесь в статье будет использован loop0, но вам важно заменять loop0 на ваше устройство.
Отформатируйте второй раздел диска в ext4. На этом разделе будет установлена система. Не забудьте про название loop-устройства.
sudo mkfs.ext4 /dev/loop0p2
Теперь можно примонтировать loop-девайс к хостовой системе. Следующая команда подключит второй раздел виртуального диска к хостовой системе. /mnt — это путь к разделу.
sudo mount /dev/loop0p2 /mnt
Ваш виртуальный диск подключен к вашей системе так, будто это настоящий диск. Вы можете установить на него систему так же, как если бы вы устанавливали ее на физический компьютер с флешки.
Теперь надо узнать uuid системного раздела. С помощью этого идентификатора можно будет определить диск при загрузке системы. Не перепутайте loop-устройство.
lsblk -f /dev/loop0p2
NAME FSTYPE FSVER LABEL UUID FSAVAIL
FSUSE% MOUNTPOINTS
loop0p2 ext4 1.0 f0509427-31bf-4216-9155-7607141c6267 9.2G
0% /mnt
Этот uuid будет нужен для загрузки системы, сохраните его где-нибудь.
Для установки Debian существует скрипт debootstrap. Чтобы не устанавливать его в хостовую систему, будем использовать docker-контейнер. Я остановлюсь на debian:trixie, так как в более старых контейнерах не было поддержки нужных версий программ.
docker run -it --mount src=/mnt,target=/mnt,type=bind --rm --privileged debian:trixie
Эта команда запустит контейнер с Debian с повышенными привилегиями. Также в контейнер будет подключен каталог /mnt, в который смонтирован виртуальный диск. Это позволит работать с диском внутри контейнера как с простой директорией.
Мы установим все необходимые для инсталляции пакеты во временный контейнер, а не в docker-образ, так как они будут нужны нам всего один раз. Debootstrap нужен для самой системы, а grub2 — для установки загрузчика:
apt-get update -y && apt-get install -y debootstrap grub2
После этого можно установить систему с помощью команды:
debootstrap trixie /mnt
Эта команда установит trixie в /mnt. Так как в Linux все есть файл, то можно представить, что эта команда просто поместит в /mnt всю файловую структуру Debian.
Но файловой структуры недостаточно, чтобы система запустилась. За непосредственную загрузку отвечает загрузчик, поэтому его тоже нужно установить в образ. Мы будем использовать grub2. Его пакет уже установлен в контейнере, осталось установить сам загрузчик на виртуальный диск. Не забудьте поменять loop0 на ваш девайс.
grub-install --recheck --target=i386-pc --modules=part_gpt --boot-directory=/mnt/boot /dev/loop0
Эта команда установит загрузчик в /mnt/boot. На виртуальном диске это соответствует пути /boot.
Debootstrap не устанавливает ядро операционной системы. Хоть мы и будем собирать свое ядро, готовое тоже лучше установить. Оно пригодится для верификации изменений. В Debian ядро поставляется в виде deb-пакета. Этот пакет содержит все необходимое для работы ядра, в нем находится сам файл и все модули ядра.
Контейнер нужен для синхронизации операционных систем на хосте и виртуальном диске. А с помощью chroot можно вызывать команды с образом виртуальной машины в качестве корня файловой системы. Это должно быть эквивалентно вызову команд в работающей виртуальной машине. На самом деле, внутренние процессы будут немного отличаться, но нас это устраивает. Если вы запутались, то можно обратиться к следующей схеме:

Виртуальный диск подключается как loop-устройство. Оно монтируется в хостовую систему, а потом подключается внутрь docker-контейнера. Внутри контейнера используется chroot для работы с гостевой ОС.
Установим ядро из пакетного менеджера:
chroot /mnt bash -c 'apt-get install -y linux-image-amd64'
Для удобства добавим пароль пользователя:
chroot /mnt bash -c 'echo "root:root" | chpasswd'
На этом работа в контейнере закончена, и из него можно выйти:
exit
Чтобы система могла загрузиться, надо указать, какие разделы и куда надо подключить. Для этого требуется указать их в файле fstab. С помощью него происходит монтирование устройств при запуске системы. Откройте файл /mnt/etc/fstab и вставьте следующий текст:
# Static information about the filesystems.
# See fstab(5) for details.
# <file system> <dir> <type> <options> <dump> <pass>
# /dev/loop0p2
UUID=f0509427-31bf-4216-9155-7607141c6267 / ext4 rw,relatime 0 1
Не забудьте поменять uuid на ваш. С помощью этого файла мы указываем, что раздел с нашим uuid должен быть примонтирован как корень файловой системы.
Далее нужно сконфигурировать grub, чтобы он знал, где лежит ядро и initrd.img или initramfs. Initramfs — это загрузочная файловая система, с помощью которой происходит подключение основной файловой системы. Она загружается в оперативную память и содержит все необходимое для определения подключенных девайсов.
Для конфигурации загрузчика используется файл /mnt/boot/grub/grub.cfg. Вставьте в него следующий текст и не забудьте поменять uuid на свой.
set default=0
set timeout=5
uuid=f0509427-31bf-4216-9155-7607141c6267
menuentry 'Debian' --class arch --class gnu-linux --class gnu --class os 'gnulinux-simple-$uuid' {
insmod gzio
insmod part_gpt
insmod ext2
echo 'Loading Linux linux ...'
linux /vmlinuz root=UUID=$uuid rw loglevel=3 quiet console=ttyS0
echo 'Loading initial ramdisk ...'
initrd /initrd.img
}
Первая строчка указывает на выбор записи для загрузки по умолчанию. На данный момент у нас одна запись, поэтому параметр равен 0.
Вторая строчка указывает на задержку до загрузки системы. Советую оставить время, чтобы в случае ошибок в пропатченном ядре можно было загрузиться в исходное.
Опции загрузки указывают с помощью menuentry. В этом пункте важна строчка, начинающаяся с linux. Это путь к ядру и опции для его запуска. Root используется для подключения корня файловой системы. Для идентификации раздела мы будем использовать uuid. Также важной опцией является console=ttyS0. Она позволяет запускать систему в текстовом режиме, чтобы не создавать отдельное графическое окно и работать прямо в консоли.

На этом установка закончена. Отключите виртуальный диск от файловой системы хоста:
sudo umount -l /mnt
sudo losetup -d /dev/loop0
Raw не самый удобный формат для работы с виртуальными машинами, поэтому его нужно конвертировать в qcow2:
qemu-img convert -f raw -O qcow2 disk.raw disk.qcow2
После этого можно удалить raw-файл:
rm disk.raw
Для проверки установки запустите виртуальную машину с помощью команды:
qemu-system-x86_64 -enable-kvm -smp 2 -m 1024m -drive format=qcow2,file=disk.qcow2 -nographic -serial mon:stdio
Если вы используете не x86-процессор, то команда запуска QEMU будет отличаться.
-enable-kvm — включает гипервизор, встроенный в ядро для ускорения работы.
-smp 2 — количество ядер процессора, можете указывать любое количество.
-m 1024m — объем оперативной памяти.
-drive format=qcow2,file=disk.qcow2 — подключить в виртуальную машину диск.
-nographic — ключ для отключения создания графического окна.
-serial mon:stdio — ключ для запуска системы в текстовом режиме.
Логин и пароль от виртуальной машины — это root:root. Чтобы остановить виртуальную машину, можно использовать комбинацию Ctrl-A + x или shutdown 0.

На этом моменте остановите виртуальную машину.
Подготовка Docker-образа
Для минимизации ошибок и упрощения развертывания сборка будет проходить в docker-контейнере. Как уже было сказано выше, это необязательный шаг, и вы можете производить все операции на хосте, но я рекомендую использовать контейнер.
Создайте Dockerfile со следующим содержимым.
FROM debian:trixie
ARG DEBIAN_FRONTEND=noninteractive DEBCONF_NOWARNINGS=yes
RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get install -y \
build-essential \
libncurses-dev \
debhelper \
dwarves \
gcc \
bc \
bison \
flex \
libssl-dev \
libelf-dev \
python3 \
rsync \
kmod \
zstd \
clangd \
bear \
cpio \
libdw-dev \
adduser
Этот образ основан на базе debian:trixie. В него устанавливаются все необходимые для сборки пакеты. Лучше взять docker-файл из репозитория, там всегда будет актуальная версия.
Чтобы собрать образ, вам нужна команда:
docker build -t kernel-dev .
Эта команда соберет образ с названием kernel-dev.
Сборка ядра
В проекте я использовал закрытое ядро, его исходники хранятся на серверах компании. Но инструкция универсальна, поэтому в статье мы будем патчить ванильное ядро, полученное из git-репозитория kernel.org. Также я проверял работу с ядром Debian и разницы не заметил. Если вы используете свое ядро, то адаптировать инструкцию под него будет несложно.
Репозиторий с ядром весит довольно много, и если для вас это критично, то можете скачать архив с исходниками с kernel.org или из релизов проекта на GitHub. Склонируйте репозиторий к себе на компьютер:
git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
Переключите проект на нужную версию ядра. Ее можно узнать прямо в виртуальной машине. Запустите ее:
qemu-system-x86_64 -enable-kvm -smp 2 -m 1024m -drive format=qcow2,file=disk.qcow2 -nographic -serial mon:stdio
Вызовите команду uname -a:
uname -a
Я получил такой вывод:
Linux 4bc6af05e42a 6.12.43+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian
6.12.43-1 (2025-08-27) x86_64 GNU/Linux
К сожалению, не на всех зеркалах есть все теги, поэтому я переключаюсь на ядро 6.12:
cd linux
git checkout v6.12
cd ..
Теперь нужно получить конфиг ядра. Возьмите его из целевой системы, то есть из системы, ядро которой нужно пропатчить. Во время работы я брал конфиг прямо с работающего свича, а сейчас мы возьмем его из виртуальной машины. Он лежит в директории /boot и называется config-*. Этот файл нужно перенести в директорию linux и сохранить там с названием .config.
Также ядро можно сконфигурировать через готовые цели для make. Например, defconfig или tinyconfig. Из описания целей в Makefile:
Defconfig — new config with default from ARCH supplied defconfig.
Tinyconfig — configure the tiniest possible kernel.
Такие цели будут собираться значительно быстрее полноценной конфигурации, поэтому для экономии времени можете использовать их.
Так как мы будем работать с файлами на виртуальном диске, нам потребуется подключать его к хостовой системе. Losetup, к сожалению, использовать не получится, так как он не работает с qcow2. Для монтирования таких дисков будем использовать nbd. Эта технология позволяет предоставить доступ к виртуальному диску как к сетевому блочному устройству.
Загрузите модуль ядра nbd для подключения виртуального диска:
sudo modprobe nbd
После выполнения команды в систему будут добавлены nbd-устройства.

Возможно, в вашей системе уже подключены какие-нибудь nbd-устройства. Проверить это можно с помощью команды nbd-client. Она покажет, подключено ли что-то к этому nbd-устройству:
nbd-client -c /dev/nbd0
Начинайте поиск свободного устройства с нулевого. Если команда ничего не выведет, то можно подключаться к нему. В противном случае увеличьте номер девайса. Определив свободное место, подключите к нему файл с виртуальным диском и смонтируйте в хостовую файловую систему:
sudo qemu-nbd -c /dev/nbd0 disk.qcow2
sudo mount /dev/nbd0p2 /mnt
Скопируйте конфиг ядра. В виртуальной машине он лежит по пути /boot. Ее файловая система смонтирована в /mnt, поэтому полный путь — /mnt/boot. Его надо поместить в директорию с исходниками и назвать .config.
cp /mnt/boot/config-6.12.43+deb13-amd64 linux/.config
После копирования файла диск можно отключить:
sudo umount -l /mnt
sudo qemu-nbd -d /dev/nbd0
В дальнейшем подключение и отключение диска будет проводиться аналогичным образом. Помимо него, можно использовать ssh прямо во время работы виртуальной машины.
Теперь мы можем приступить к сборке. Для этого надо запустить docker-контейнер. Важно входить в него на одну директорию выше репозитория с исходниками, так как deb-пакеты создаются на один уровень выше исходников.
docker run -it --rm --mount src=.,target=$(pwd),type=bind --privileged -w $(pwd) kernel-dev
Внутри контейнера зайдите в директорию с исходниками:
cd linux
Чтобы ядро запускалось в QEMU, нужно добавить в конфиг опции:
./scripts/config -e CONFIG_64BIT \
-d CONFIG_EMBEDDED \
-d CONFIG_EXPERT \
-e CONFIG_SERIAL_8250 \
-e CONFIG_SERIAL_8250_CONSOLE \
-e CONFIG_VIRTIO \
-e CONFIG_VIRTIO_MENU \
-e CONFIG_PCI \
-e CONFIG_VIRTIO_PCI \
-e CONFIG_VIRTIO_BLK \
-e CONFIG_EFI_PARTITION \
-e CONFIG_EXT4_FS \
-e CONFIG_BINFMT_ELF \
-e CONFIG_E1000 \
-e CONFIG_VIRTIO_NET
Или же с помощью цели make:
make kvm_guest.config
Эта конфигурация позволит ядру запускаться в QEMU и добавит поддержку виртуальных сетевых устройств. Ключ -e включает опцию, а -d удаляет.
Конфигурация ядра — это самая сложная часть всего процесса. Так как это общая инструкция, которая должна удовлетворить большинство сценариев разработки, мы не будем дополнительно менять конфигурацию. Если вы не знаете, что конкретно вам нужно включить или отключить, то лучше ничего не трогать.
Дополнительный момент с конфигурацией
При сборке ванильного ядра этого достаточно, но при сборке ядра Debian я столкнулся с проблемой отсутствия одного файла при сборке. Как я понял, этого файла не было из-за лицензии. В случае если вы столкнетесь с тем, что что-то не собирается, вы можете просто отключить сборку этого компонента.
На момент написания статьи для ядра дебиана нужно удалить следующий параметр:
./scripts/config -d CONFIG_DVB_USB_AF9005
По мере развития ядра в него добавляется новый функционал, расширяющий возможную конфигурацию. Конфиг старой версии ядра не содержит эти новые опции. Перед сборкой происходит проверка всех опций, и если конфиг не актуален, то он будет актуализирован. Если запустить сборку, то значения новых опций будут выбираться в интерактивном режиме.
Для упрощения и автоматизации процесса существует специальная цель make — olddefconfig. Она обновит файл, а на все новые опции установит значение по умолчанию:
make olddefconfig
При сборке я столкнулся с проблемой. На рабочем ноутбуке с 32 ГБ оперативной памяти, опция -j команды make приводила к переполнению оперативной памяти и провалу сборки. Я проверил множество устройств с разным количеством ядер процессора и объемом оперативной памяти, но нигде больше таких проблем не было. Поэтому в инструкции будет использовано -j с $(nproc) для указания количества потоков сборки. Можно не использовать $(nproc), но это может привести к проблемам. На домашнем компьютере я собираю в 32 потока, и это требует 64 гигабайта оперативной памяти в пике.
make -j$(nproc) bindeb-pkg LOCALVERSION=-patched
Опция bindeb-pkg указывает на сборку deb-акета. Напомню, что помимо deb-пакетов доступны также rpm, snap и pacman-пакеты. Опция LOCALVERSION нужна для добавления к версии ядра пользовательской строки. Можно не указывать ее, но с ней будет удобнее: так вы сможете быстро понять, что это ваше ядро.
Сборка может занять довольно много времени, можете пойти попить чаю:

Поздравляю! Вы собрали свое ядро, и теперь оно упаковано в deb-пакет вместе со всем необходимым. Пакет с ядром имеет примерно такое название: linux-image-6.12.0-patched_6.12.0-1_amd64 (без dbg!). Помимо него, создаются и другие пакеты, но нас они не интересуют. В конце названия deb-пакета указывается цифра, которая показывает номер сборки — например, на скриншоте показана первая сборка.
Каждая новая сборка deb-пакета создает файлы с увеличенной версией в конце и не перезаписывает старые файлы. Это может быть полезно для версионирования изменений, но важно учитывать этот момент при автоматизации. Теперь можно выйти из контейнера.
Установка ядра
Ядро можно установить как обычный deb-пакет. Это самый простой и удобный способ, так как требуется вызвать всего одну команду. Подключите виртуальный диск:
sudo qemu-nbd -c /dev/nbd0 disk.qcow2
sudo mount /dev/nbd0p2 /mnt
Запустите рабочий контейнер:
docker run -it --rm --mount src=.,target=$(pwd),type=bind --mount src=/mnt,target=/mnt,type=bind -w $(pwd) --privileged kernel-dev
Скопируйте файл с новым ядром в виртуальный диск, не забудьте поменять название файла на ваше и не забудьте про номер сборки:
cp linux-image-6.12.0-patched_6.12.0-1_amd64.deb /mnt/root/
Перейдите в виртуальную машину с помощью chroot. Так как nbd-девайс примонтирован к /mnt, а хостовый путь /mnt подключен к /mnt внутри контейнера, chroot надо делать к /mnt. Это позволит установить пакет на виртуальный диск из контейнера:
chroot /mnt bash
Установите deb-пакет с новым ядром:
cd root
dpkg -i linux-image-6.12.0-patched_6.12.0-1_amd64.deb
Вернитесь в хост:
exit
exit
Вот так весь процесс выглядит схематически:

Всё, на этом установка нового ядра закончена. Ничего сложного. Осталось только сообщить загрузчику о новом ядре. Для этого нужно поменять grub.cfg, добавив в него запись о нем.
Откройте файл /mnt/boot/grub/grub.cfg. В самом начале поменяйте set default=0 на set default=1. Это установит вторую запись на загрузку по умолчанию. В этой второй записи будет загружаться новое ядро. Добавьте следующий текст в конец файла:
menuentry 'Debian patched' --class arch --class gnu-linux --class gnu --class os 'gnulinux-simple-$uuid' {
insmod gzio
insmod part_gpt
insmod ext2
echo 'Loading patched linux kernel ...'
linux /boot/vmlinuz-6.12.0-patched root=UUID=$uuid rw loglevel=3 quiet console=ttyS0
echo 'Loading initial ramdisk ...'
initrd /boot/initrd.img-6.12.0-patched
}
Тут важно указать правильный путь к ядру и initramfs в строчках linux /boot/vmlinuz-6.12.0-patched root=UUID=$uuid rw loglevel=3 quiet console=ttyS0 и initrd /boot/initrd.img-6.12.0-patched. Эти пути указывают на файлы ядра в файловой системе виртуального диска. Во время установки deb-пакета они добавляются в /boot. В вашем случае может отличаться версия, не забудьте поменять ее на правильную.
Теперь можно отключить виртуальный диск:
sudo umount -l /mnt
sudo qemu-nbd -d /dev/nbd0
Запустите виртуалку:
qemu-system-x86_64 -enable-kvm -smp 2 -m 1024m -drive format=qcow2,file=disk.qcow2 -nographic -serial mon:stdio
В меню загрузчика автоматически будет выбрана вторая запись с вашим ядром. Через 5 секунд начнется загрузка ОС.

Если все сделано правильно, то вы загрузитесь в ядро, собранное вами!

Во время работы над ядром вы можете изменять разные компоненты. Что-то является частью ядра, а что-то модулем. Deb-пакет позволяет обновить все компоненты, но это нужно не всегда. Если вы работаете с кодом, который находится в ядре, а не в модуле, то нет смысла пересобирать все и собирать deb-пакет. Достаточно просто собрать новый файл ядра, что будет значительно быстрее пересборки целого пакета.
При дальнейшей работе над кодом ядра обновление deb-пакетов не будет отличаться, а про обновление файла речь пойдет в следующей главе.
Патчинг ядра
Чтобы наглядно продемонстрировать весь процесс, внесем небольшие изменения в код ядра. Откройте файл rtnetlink.c в linux/net/core. Найдите в нем строчку Protodown not supported by device и замените ее на какой-нибудь текст:
if (!dev->change_proto_down) {
NL_SET_ERR_MSG(extack, "Hello world!");
return -EOPNOTSUPP;
}
Это весь код, который мы поменяем. После этого мы сможем вызвать одну команду и наглядно увидеть эти изменения.
Примонтируйте файл диска к хосту:
sudo qemu-nbd -c /dev/nbd0 disk.qcow2
sudo mount /dev/nbd0p2 /mnt
Перейдите в сборочный контейнер:
docker run -it --rm --mount src=.,target=$(pwd),type=bind --mount src=/mnt,target=/mnt,type=bind -w $(pwd) --privileged kernel-dev
Зайдите в папку с ядром и соберите новый бинарник. Чтобы собрать один файл ядра, а не целый пакет, нужно использовать make без цели:
cd linux
make -j$(nproc) LOCALVERSION=-patched
После этого новое ядро будет доступно по пути arch/x86/boot/bzImage. Замените файл ядра в виртуальной машине на новый:
cp arch/x86/boot/bzImage /mnt/boot/vmlinuz-6.12.0-patched
Не ошибитесь в названии. Выйдите из контейнера и размонтируйте диск:
sudo umount -l /mnt
sudo qemu-nbd -d /dev/nbd0
Запустите виртуальную машину:
qemu-system-x86_64 -enable-kvm -smp 2 -m 1024m -drive format=qcow2,file=disk.qcow2 -nographic -serial mon:stdio
Чтобы увидеть изменения, вызовите команду:
ip link set dev ens3 protodown on
Результат будет примерно такой:

На самом деле, все эти манипуляции, включая замену файла ядра и установку deb-пакета, можно проводить и в работающей системе. Перенести необходимые файлы можно по scp, а установить пакет через ssh. Это может быть удобнее, так как nbd требует root-доступа. В репозитории для этого тоже есть готовые скрипты.
Здесь можно было бы закончить, но для комфортной работы лучше подключить lsp-сервер. Он добавит богатую поддержку языка в редактор кода. Мы будем использовать clangd, так как он намного быстрее Intellisense, у него есть плагины под множество IDE, и он мне больше нравится. Это заметно увеличит комфорт от работы с кодом, особенно для новичков.
Подключение clangd
В статье я покажу пример с подключением к code oss, сборке VS Code от сообщества. Процесс на оригинальном редакторе и подключение в других редакторах не должно сильно отличаться.
Сперва установите расширение в редактор:

Intellisense не может работать одновременно с clangd, и если вы используете первый, то можно создать отдельный профиль VS Code. Создайте отдельный профиль для конкретного проекта и используйте в нем clangd. Но я советую попробовать использовать clangd вместо intellisense на постоянной основе. Когда-то давно я полностью перешел на него и ни разу не пожалел. Единственный его недостаток — это конфигурация по умолчанию, мне она не нравится. Но после настройки под свои требования это прекрасный инструмент.
Clangd довольно умен и может работать в нескольких режимах. Он способен анализировать проект локально, только с помощью доступных в проекте файлов. Но при таком подходе он может не знать, где лежат заголовочные файлы разных библиотек и системных файлов. Также он не будет знать о дополнительных заголовочных файлах, которые подключаются через систему сборки.
Второй режим работы — это использование compile_commands.json. Этот файл содержит в себе всю информацию о сборке проекта, включая опции компилятора со всеми инклюдами и дефайнами. Он позволяет lsp-серверу полноценно анализировать файлы проекта. Современные системы сборки, такие как CMake, имеют встроенные возможности для его генерации. К счастью, для ядра есть скрипт, который генерирует этот файл, но этот скрипт работает только после сборки ядра. Если вы работаете со старым ядром, в котором нет этого скрипта, то можете использовать утилиту bear. Изначально я использовал ее, но скрипт удобнее.
Так как сборка происходит в контейнере, все пути в compile_commands.json будут внутри контейнера. На самом деле можно оставить clangd и на хосте. При сборке все пути между хостом и контейнером совпадают, так что compile_commands.json будет валидным и для хоста. Но я предпочитаю изолировать сборочное окружение, поэтому clangd будет запускаться в контейнере. Это будет сделано бесшовно: достаточно просто открыть редактор, и все заработает само.
У вас может возникнуть вопрос: почему бы тогда не открыть редактор в контейнере? Есть две причины. Во-первых, code-oss так не умеет, и я просто не мог запустить редактор внутри контейнера. Во-вторых, у меня в системе много алиасов и других настроек, которые недоступны в контейнере. Поэтому я запускаю clangd внутри контейнера, а редактор вместе с терминалом остается на хостовой системе. Схематично взаимодействие можно представить так:

Создайте папку .vscode в корне вашего проекта. Напоминаю, что он должен быть на один уровень выше, чем исходный код ядра. В этой папке создайте два файла, settings.json и clangd-in-container. Название второго файла можете поменять на другое.
Первый файл — это стандартный файл с настройками проекта в VS Code. Добавьте в него следующий текст:
{
"clangd.path": "${workspaceFolder}/.vscode/clangd-in-container"
}
Эта настройка переопределит путь к бинарному файлу lsp-сервера и заменит его на скрипт. С помощью такой настройки мы сможем запускать clangd как нам угодно.
В скрипт clangd-in-container добавьте следующий текст:
# Symbolic links workaround
cd $(dirname "$0")
cd ..
# Wait compile_commands.json file
while [ ! -f linux/compile_commands.json ];
do
sleep 1
done
# Launch lsp server
docker run -i \
--rm \
--mount src=.,target=$(pwd),type=bind \
-w $(pwd) \
--name vs-code \
kernel-dev:latest bash -c "clangd $*"
Первые две строчки были добавлены для обхода проблем с символическими ссылками. После них запускается цикл, который будет ждать появления файла compile_commands.json. Этот файл создается с помощью python-скрипта. В репозитории скрипт будет вызван автоматически после завершения сборки.
Команда запуска контейнера сложнее и требует отдельного пояснения. Ключ -i нужен для запуска контейнера в интерактивном режиме. Clangd общается с IDE через стандартные дескрипторы ввода и вывода, и IDE с нашим скриптом тоже общается таким же образом. Монтирование нужно для подключения исходников в контейнер и сохранения кеша clangd. Опция --name не обязательна, но упростит вывод списка рабочих docker-контейнеров. Опция -w нужна для корректной работы с путями. Она указывает на рабочую директорию внутри контейнера. IDE и lsp-сервер обмениваются путями к файлам проекта. При этом IDE отдает локальные пути хоста, а clangd — пути внутри контейнера.
Чтобы магия произошла, нужно совместить эти пути между собой. Делается это с помощью опции -w с передачей локального пути, что позволяет подключить проект в контейнер по такому же пути, как на хостовой машине. Так как исходники монтируются с помощью $(pwd), все пути между хостом и контейнером совпадут. Таким образом, IDE даже не будет знать, что clangd запущен в контейнере.
Clangd вызывается со строкой $*. Она подставит все аргументы, переданные bash-скрипту, в аргументы запуска clangd. Это позволит настраивать его без изменения скрипта.
Выдайте скрипту право на запуск:
chmod +x clangd-in-container
Теперь нужно сгенерировать compile_commands.json. Для этого зайдите в контейнер в корне проекта:
docker run -it \
--rm \
--mount src=.,target=$(pwd),type=bind \
-w $(pwd) \
kernel-dev:latest
И вызовите скрипт генерации compile_commands.json:
cd linux
python3 scripts/clang-tools/gen_compile_commands.py
При отсутствии скрипта воспользуйтесь bear. Вам надо будет очистить сборку и вызвать bear -- make. Откройте любой c- или h-файл, и начнется индексация.

Вы можете заметить некоторые ошибки, на которые указывает редактор. Это clangd ругается на опции компиляции, так как просто не знает о них. Поэтому нужно отключить это. Создайте файл .clangd в корне проекта с такими настройками:
CompileFlags:
Remove: [-mpreferred-stack-boundary=3,
-mindirect-branch=thunk-extern,
-mindirect-branch-register,
-fno-allow-store-data-races,
-fmin-function-alignment=16,
-fconserve-stack,
-mrecord-mcount]
Эти флаги встретил я, возможно, есть и другие. Просто наведите на ошибку, чтобы узнать название флага, и добавьте в этот файл. Для передачи опций запуска в clangd откройте settings.json и укажите их в clangd.arguments. Это позволит настроить все под себя. Вот так выглядит мой файл настроек:
{
"clangd.path": "${workspaceFolder}/.vscode/clangd-in-container",
"clangd.arguments": [
"-header-insertion=never",
"--function-arg-placeholders=0"
],
"explorer.excludeGitIgnore": false,
"search.useParentIgnoreFiles": false,
"search.useIgnoreFiles": false
}
На этом ручная подготовка окружения к разработке под ядро закончена. Не самый быстрый процесс, особенно учитывая время сборки ядра, поэтому я сделал специальный репозиторий, который сводит количество необходимых ручных операций к минимуму.
Kernel dev repo
Этот проект автоматизирует все, что было описано выше, а также добавляет небольшое количество удобств. Например, ssh-сервер в виртуальной машине.
Главным файлом репозитория является config:

По умолчанию конфиг полностью готов к работе, но его можно настроить под свои потребности — например, добавить ссылку на исходники ядра. Это полезно, если вы собираетесь патчить ядро с корпоративного хранилища кода. На данный момент скрипты в репозитории поддерживают загрузку с помощью git и ссылки на tar-архивы.
Можно прописать использование defconfig с помощью USE_DEFCONFIG для максимально быстрой сборки. А также добавить свои опции конфигурации ядра с помощью ADDITIONAL_CONFIG_OPTIONS. Они будут применены после загрузки исходников и перед сборкой.
Для сборки можно ограничить количество ядер и потребляемой оперативной памяти. С памятью нужно быть осторожным, так как при недостаточном объеме сборка не завершится успешно. Ограничивайте ее, только если это действительно нужно, так как она влияет на скорость сборки.
Репозиторий полностью готов к работе с VS Code, и дополнительно настраивать clangd не нужно. В директории scripts моего репозитория содержатся скрипты для патчинга ядра четырьмя способами: замена бинарного файла с монтированием, установка deb-пакета с монтированием, замена бинарного файла с помощью scp, установка deb-пакета с помощью ssh. В дополнение к этому длинному описанию вы можете посмотреть видео всего процесса.
Заключение
Я постарался расписать все этапы максимально просто и понятно, а также раскрыть некоторые нюансы устройства операционной системы. Я люблю Linux, люблю изучать его, так что мне было интересно разобраться во всем самостоятельно, пройти и автоматизировать весь путь.
В завершение статьи повторю ссылку на свой репозиторий. После его создания у меня были еще задачи, связанные с разработкой для ядра, и эта заготовка мне очень помогла. Там я собрал все необходимое для быстрого и простого старта.