Hatred's Log Place

DON'T PANIC!

Jun 25, 2019 - 7 minute read - Comments - programming

Удалённое развёртывание драйвера/модуля ядра в Qt Creator

Для Windows драйвер писать в QtC никто в здравом уме и трезвой памяти не будет. Поэтому речь дальше по процессу разработки драйвера для Linux. Я не буду касаться вопросов использования отладчика (KGDB), в основном посмотрим на вопросы запуска модуля ядра на удалённой системе.

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

Правильным решением же будет использовать какого-нибудь qemu, virtualbox или иже с ними. Но не всегда можно отладить модуль в виртуальном окружении, особенно при работе с реальным железом (можно так его запрограммировать, что убить много чего рядом). Поэтому, в таких случаях стоит использовать отладочную машинку, желательно без сохранения состояния (stateless - это тема отдельного разговора). Да, и в общем, подход с удалённым запуском и/или отладкой будет применим и к виртуальным окружениям.

Попробуем разобраться, что нам нужно для решения задачи:

  1. Ядро может отличаться от того, что у вас локально. Можно подтягивать собранное ядро целевой машины, и его заголовочники и собирать под него. Это будет работать, если архитектура идентична. Кросс-сборкой модуля стоит озадачиться, если ресурсов на целевой машине совсем кот наплакал. Наш вариант - собирать модуль на целевой машине.
  2. Раз мы собираем модуль на целевой машине, то нам нужно для начала залить туда исходные файлы (процесс настроки и установки необходимых пакетов для разработки и сборки модуля я опущу). Хорошо бы не заливать полностью исходники, а только изменённые файлы. Наш выбор rsync+ssh. В современных дистрибутивах особо ничего делать не нужно.
  3. После сборки нужно загрузить модуль
  4. И всё это интегрировать в IDE, дабы не делать кучу мелких движений.

Qt Creator позволяет подключаться к удалённым системам для развёртывания кода. Воспользуемся данной возможностью. Удалённая машина будет иметь имя ts.local (используется zeroconf/avahi что бы сообщать свои имена в сеть).

Хочу отметить, что реализация подключения в QtC поддерживает алисы из ~/.ssh/config, поэтому для различных хостов можно будет прописать дополнительные настройки через него.

Начнём.

Для начала идём в “Tools” -> “Options…” -> “Devices”. Жмём “Add…” и выбираем “Generic Linux Device”. После чего жмём “Start Wizard” и заполняем очевидные поля в диалоге:

  • Имя для пользователя
  • Имя хоста или IP адрес
  • Имя пользователя на удалённом хосте

Start Wizard Basic Device settings

На следующем шаге предлагается настроить аутентификацию по ключу. Здесь вам лучше знать, что делать, и можно сразу нажать “Next>”.

Key authentification

После нажатия кнопки “Finish” будет произведена попытка подключения к хосту. Поэтому лучше, если он будет запущен.

Host check

Теперь нужно набору оснастки дать знать, что можно использовать удалённых хост. Выбираем раздел “Kits”. Для простоты можно взять набор, используемый по умолчанию (локальный запуск) и нажать “Clone” и переименовать его во что-то вроде “Default (remote)”.

Следующим шагом в новой оснастке задаём “Device type:” в “Genric Linux Device” и выбираем устройство (“Device:“), созданное ранее (на скрине выбрано “Pearl2 Ubuntu”, но сути не меняет):

Remote Run Kit

Тут отмечу, что если нужно переключаться между несколькими устройствами, то создавать оснастку для каждого их них дело муторное. Поэтому кликаем правой кнопкой мыши по выпадающему списку “Device:” и ставим галочку “Mark as mutable”:

Mark As Mutable

Это позволит в выпадающем списке выбора типа оснастки, конфигурации билда и деплоя/запуска указывать и устройство для подключения:

Build/Run configuration

Всё, теперь открываем проект, выбираем созданную нами оснастку. После чего в настройках проекта (точнее в настройках запуска) станут доступны настройки Deployment:

Run configuration

Доступны различные предопределённые шаги, но мне проще было использовать один “Custom Process Step” и вызывать скрипт с параметрами, формируемыми при помощи подстановочных переменных:

  • Command: %{sourceDir}/deploy-driver-remote.sh
  • Arguments: %{CurrentDevice:HostAddress} %{CurrentDevice:SshPort} %{CurrentDevice:UserName} %{CurrentDevice:PrivateKeyFile} %-CurrentRun:Name}
  • Working directory: %{sourceDir}

Смысл переменных, думаю, понятен. Отмечу, что %{CurrentRun:Name} это ровно то имя, которое показывается ниже в выпадающем списке “Run”/“Run configuration:“. Оно формируется при первом запуске проекта автоматически и может не соответствовать вашим критериям прекрасного. Тогда переименуйте.

Для чего я вообще использую имя конфирурации? У меня в одном проекте два драйвера: для PCI и для USB устройств. Выбирая конфигурацию я влияю на шаги по загрузке нужного драйвера на удалённой стороне.

Ещё одним нюансом является то, что я указал “Alternate executable on device:” в “/bin/true” и параметр “Use this command instead”. Я просто физически не могу “запустить” модуль ядра, как обычный бинарник, а вся процедура по его загрузке в область памяти ядра выполняется в скрипте, поэтому я выбрал для запуска программу гарантированно присутствующую на удалённой системе и всегда возвращающую статус успешного выполнения (в противовес /bin/false). Вы можете вписать сюда что-то своё.

Ну а теперь самое вкусное, скрипт деплоя. Можно вручную прописывать шаги загрузки в IDE, но это муторно. Плюс я использую GIT сборки QtC и шаги загрузки запросто могут слететь. Поэтому я объединил их в скрипт, который может лежать прямо в репозитории и иметь идентичные настройки в IDE на разных машинах (вся кастомизация может быть выполнена через настроки конкретных Remote Devices, как описано выше).

Мой скрипт выглядит так (комментарии инлайном, что бы избежать дублирования):

#!/usr/bin/env bash

# прервёт выполнение скрипта, как только очередная команда завершится недачно
# позволяет избежать лишних проверок на успешность выполнения предыдущих операций
set -e

#
REMOTE_HOST=$1
REMOTE_PORT=$2
REMOTE_USER=$3
REMOTE_KEY=$4
#
# %{CurrentRun:Name}
TARGET=$5

# assume, that work dir is top level source dir
LOCAL_SOURCE_DIR=$(pwd)

# просто выведем то, как мы вызваны. Чисто для отладки, если что-то работает не так
echo $@

# Detect driver: как я писал выше, у меня два драйвера, основываясь на имени
# Run Configuration я выбираю то, с каким драйвером имею дело. 
# Разные вариации из-за смены того, что подставляется вместо %{CurrentRun:Name} в QtC.
drv=
jobs=1
case "$TARGET" in
    *_usb|*_usb-build)
        drv=usb
        jobs=8
    ;;
    *_pci|*_pci-build)
        drv=pci
        jobs=1
    ;;
    *)
        echo "Unknown target: ${TARGET}"
        exit 1
    ;;
esac

# Небольшой хелпер для запуска удалённой команды через SSH. С пробросом X11 (Trusted)
ssh_cmd()
{
    ssh -YC -i "$REMOTE_KEY" -p ${REMOTE_PORT} ${REMOTE_USER}@${REMOTE_HOST} "$@"
}

MAKE_OPTS="DRIVER_DEBUG=1"

# Ещё больше информативности
set -x

# Ну а дальше логические шаги

# Step 1: подготовка директорий на удалённой стороны для загрузки кода
ssh_cmd "mkdir -p build/driver/remote-work/{vga2usb,helpers}"

# Step 2: синхронизируем локальное дерево исходников и удалённое.
#         Именно синхронизируем: удаляем удалённое, создаём созданное
#         и копируем только изменённое. Про файл rsync_excludes.txt я расскажу чуть ниже.
rsync -azv --delete --exclude-from="${LOCAL_SOURCE_DIR}/rsync_excludes.txt" "${LOCAL_SOURCE_DIR}/" -e "ssh -p ${REMOTE_PORT} -i \"${REMOTE_KEY}\"" ${REMOTE_USER}@${REMOTE_HOST}:build/driver/remote-work/vga2usb/ 

# Step 3: дерево исходного кода состоит из двух частей, поэтому повторяем для второй части тоже
rsync -azv --delete --exclude-from="${LOCAL_SOURCE_DIR}/rsync_excludes.txt" "${LOCAL_SOURCE_DIR}/../helpers/" -e "ssh -p ${REMOTE_PORT} -i \"${REMOTE_KEY}\"" ${REMOTE_USER}@${REMOTE_HOST}:build/driver/remote-work/helpers/

# Step 4: очистим дерево. Ваша билд система может позволить вам
#         не делать этого шага и немного сэкономить времени
ssh_cmd "make -C build/driver/remote-work/vga2usb/linux/driver/${drv} -j8 clean"

# Step 5: стоим драйвер удалённо
ssh_cmd "make -C build/driver/remote-work/vga2usb/linux/driver/${drv} -j$jobs $MAKE_OPTS"

# Step 6: так как у нас используется билд инфраструктура ядра Linux, этим шагом 
#         мы установим модуль в /lib/modules/KERNEL_VERSION/..., можно обойтись
#         и использовать insmod/rmmod и ручное разруливание зависимостей модулей.
#         Обратите внимание: sudo настроен для пользователя для запуска без пароля!
#         Так можно сделать только на конкретные программы и/или скрипты, но если
#         система state-less, то это, IMHO, излишне.
ssh_cmd "sudo make -C build/driver/remote-work/vga2usb/linux/driver/${drv} $MAKE_OPTS install"

# Step 6.1: sign, точно работает и реализовано на Ubuntu и производных. Имеет смысл
#           если включен Secure Boot, иначе модуль ядра не загрузится. По большому
#           счёту на тестовых системах не нужно. У меня просто как часть проверки.
ssh_cmd "which kmodsign >/dev/null 2>&1 && \
         test -f /var/lib/shim-signed/mok/MOK.priv && \
         test -f /var/lib/shim-signed/mok/MOK.der && \
         sudo kmodsign sha512 \
            /var/lib/shim-signed/mok/MOK.priv \
            /var/lib/shim-signed/mok/MOK.der \
            /lib/modules/\$(uname -r)/extra/vga2${drv}.ko || true"

# Step 7: ну однострочник для выгрузки старого модуля и загрузки нового. У нас
#         реализовано устройство ALSA и на современных дистрибутивах оно может
#         быть захвачено pulseaudio, в таком случае выгрузить модуль не получится,
#         поэтому нужно прибить пульсу (sudo - она может быть запущена от другого пользователя)
ssh_cmd "sudo depmod -aA; sudo killall -9 pulseaudio; sudo modprobe -r vga2${drv} && sudo modprobe vga2${drv} && echo OK || echo FAIL"

Если какой-то шаг завершится неудачей - скрипт прервёт работу. Вывод сообщений будет в панели “Compile Output”.

Для “запуска” драйвера достаточно нажать Ctrl+R в QtC, если есть изменения, то код сначала отстроится локально, далее, если нет ошибок, запустится процедура деплоя, в нашем случае - вызовется скрипт выше с нужным набором параметров, синхронизируются файлы на удалённой машине, отстроится и загрузится новый драйвер. Как проверять работоспособность - уже индивидуальный подход. Возможно, отстраивать какие-то утилиты в одном из шагов, а вместо /bin/true вызывать какой-то скрипт, что вызовет серию тестов. Я же просто смотрю dmesg -wsudo на последних дистрибутивах/ядрах) и выполняю нужные шаги.

На этом всё. Есть некая избыточность, но, в целом, задачу решает. Возможно, стоит рассмотреть возможность интеграции с KGDB и отладкой удалённого драйвера. Но это, когда возникнет острая необходимость.