Пишем драйвер для ноутбука for fun and profit, или как закоммитить в ядро даже если ты дурак +124


С чего всё началось


Начнём с постановки проблемы. Дано: один ноутбук. Новый ноутбук, геймерский. С RGB-подсветкой. Вот такой примерно ноутбук:

image
Картинка взята с lenovo.com

Есть ещё программа к этому ноутбуку. Программа как раз этой подсветкой и управляет.

Одна только проблема – программа под Windows, а хочется чтоб в любимом линуксе всё работало. И лампочки чтоб светились, и чтоб цвета красивые мелькали. Да вот только как это сделать, чтоб без реверс-инжиниринга и без написания своих драйверов? Простой ответ пришёл быстро – никак. Ну что ж, пошли писать драйвер.

Шаг 1 – копаемся в коде


У нас есть три места, из которых можно увидеть как подсветка мигает. В порядке возрастающей сложности:

1. Большая и накачанная геймерская программа Lenovo Nerve Center – в которой есть функция настройки всей этой подсветки.

2. Сочетание горячих клавиш Fn+Space – возможно. его обрабатывает эта же программа.

3. BIOS. Во время загрузки ноутбука подсветка тоже мелькает – но только красным, и только на секунду.

Забегая вперёд, скажу что пришлось попробовать все три, но продвинулся с каким-никаким успехом я только по первой. О ней речь и пойдёт.

Ну что ж, откроем папку с программой:

folder

Сразу замечаем, что есть DLL с интересным названием – LedSettingsPlugin.dll. Наш ли...? Давайте откроем в IDA Pro и посмотрим.

right-half

Обратите внимание на правую половину экрана. От программистов осталось много дебажной информации – воспользуемся же ей. Видим, что почему-то есть много строк, похожих на имена методов. Почему?

name-of-func

А это и есть имена методов. Как удобно! Давайте поназываем что можем своими именами, и посмотрим на список функций ещё раз. Для называния в IDA можно использовать хот-кей N, или просто щелчок правой кнопкой мыши по тому, что хотим обозвать.

setledstatusex

Смотрим на функции… Y720LedSetHelper::SetLEDStatusEx. Похоже, что нам надо! Замечаем, что тут формируется что-то вроде строки и передаётся потом в некий CHidDevHelper::HidRequestsByPath. Конкретно интересует var_38, все метаморфозы которой IDA любезно обозначил за нас.

Var_34 нам тоже интересен. Он идёт сразу после var_38 – в традициях ассемблера переменные хранятся в обратном порядке под RSP. Var_34 здесь просто название константы – -34h.

Получаем, что начиная с var_38 программа кладёт ноль, потом стиль, цвет, число три и блок – часть клавиатуры, к которой этот цвет применится. (Забегая вперёд, скажу что три здесь это значение яркости. Сделав это управляемым, мы получим драйвер ещё круче оригинала!)

Давайте же залезем в HidRequestsByPath и наконец узнаем, как оно отправляется.

devhelper

Видим две функции, HidD_GetFeature и HidD_SetFeature. Оба в файле не прослеживаются… Зато очень хорошо прослеживаются в официальной документации Майкрософт — тут и тут.

На этом можно себя поздравить – мы добрались. Это дно, глубже копать не надо. В Linux такие функции есть – надо только вызвать их с теми же аргументами, и всё должно получиться… ведь правда?

Шаг 2 – запускаем прототип...?


Не совсем. Начнём с простого, и запустим lsusb. Так найдём клавиатуру, и к ней что-нибудь пошлём.

lsusb

Integrated Technology Express, Inc. здесь выглядит самым интересным. Будем пользоваться известным инструментом /dev/hidraw. Отыскиваем подходящий… Это делается простым поиском по файлам /sys/class/hidraw/hidraw*/device/uevent.

hidraw

Вот тот. Цифры совпадают – значит это устройство есть hidraw0. Но мы пытаемся послать данные, и ничего не получается! Бред какой-то. На этом этапе руки начинают опускаться… Может быть, не для простых смертных это, этот реверс-инжиниринг?

Но продолжим. Попытаемся. Если автор бы разбирался в этом всём, он бы выреверсил поиск нашего драйвера из того же Nerve Center.… Но у нас нет мозга. Идём обратно в Windows, есть одна идея.

Есть в Windows такая вещь – Диспетчер Устройств. Многое позволяет делать – нам интересно то, что он позволяет отрубать девайсы. Просто и бескомпромиссно.

Давайте отрубать девайсы по одному, пока состояние лампочек не прекратит меняться.

disable-device

Этот отрубился. Значит, если заглянуть в Hardware ID – увидим и то, как его зовут и с чем его едят.

our-device

Смотрим – это же он.

dmesg

Девайс, который мне уже несколько лет флудил в dmesg.

[Почему же он не показывался в lsusb? А не USB он вовсе. Тут используется протокол I2C HID, который позволяет прятать девайсы от пытливых рук умельцев устанавливать HID гаджеты по шине самого компьютера.]

Лирическое отступление – давайте сделаем коммит


В поисках правильного девайса я расковырял свою установку почти до ядра. Ещё, не очень нравилось что мне эту dmesg-простыню показывало перед каждой блокировкой. Раз уж я здесь, почему бы не написать короткий коммит? Руки всё равно чешутся.

Что нам нужно посмотреть – где девайсы на I2C HID, и где писать какие странности им присущи. Здесь. Не мудрствуя лукаво, прислушаемся к ошибке – incorrect input length. Давайте сделаем его correct.

bad-input-size

Добавим новый quirk, назовём его пусть I2C_HID_QUIRK_BAD_INPUT_SIZE. По аналогии с уже существующими.

quirk

Добавим ещё наше устройство в список quirkнутых. То, что мы сделали пока:

1. Поиск в интернете по запросу “i2c hid linux kernel”. Кликнули на четвёртый ответ на DuckDuckGo.

2. Написали три (!) слова на английском – BAD_INPUT_SIZE

3. Прибавили один к BIT(4). Получилось BIT(5).

4. Добавили одну чиселку в hid-ids.h – ID нашего устройства (на иллюстрации не показано, но там так же примерно).

Давайте теперь программировать.

Видим строку — `ret_size = ihid->inbuf[0] | ihid->inbuf[1] << 8;`

А потом оно жалуется, что ret_size не равен тому что оно хочет.

Давайте если квирк задействован, делать это же самое, только наоборот.

if-condition

Отправляем патч в рассылку, не забыв потестить… (если честно, чтоб просто добавиться в рассылку мозга уже потребовалось больше. Это непросто.)

applied

И всё.

Шаг 3 – драйвер!


Тут я вспомнил — ах да, надо же драйвер ещё написать. Решил это в ядро пока не коммитить (там вопросы начали подниматься, реально думать пришлось. Если кто из хабровчан может помочь проконсультировать, напишите мне – буду рад!)

Открываем питон. (В начале bash был, но я передумал очень быстро.) Будем пользоваться известным инструментом /dev/hidraw.

/dev/hidraw0, /dev/hidraw1 — как вообще работают эти файлы? Для простого ввода-вывода их можно использовать как любой нормальный файл, и он будет работать. А вот с GetFeature и SetFeature придётся повозиться…

StackOverflow (или кто-то ещё, не помню уже) подсказал, что для этого нужен некий ioctl. Это специальный метод работы с необычными файлами вроде HID-устройств, терминалов и им подобных.

Работает ioctl так. Ему даёшь дескриптор открытого файла (кому интересно, что это такое, есть ссылка на Википедию), какое-то число и буфер – после чего он с этим буфером что-то делает, и возвращает обратно. Я не утрирую, там просто очень многое от имплементации зависит. Приведу пример: 0xC0114806, или уже нам знакомый SetFeature. Оно же

(6 << 29) | (ord('H') << 8) | (0x06 << 0) | (0x11 << 16).

Первая 6 здесь значит, открыт файл будет для записи и чтения. Почему для чтения, не очень понятно — но написано делать так, наверное так и надо. Ord(‘H’) здесь же – сокращённо HID. Может и другие вещи означать, в зависимости от файла. В этом случае HID.
0x06 – сама команда. Шестая команда с HID и есть SetFeature. Последняя часть это длина буфера.

Остаётся только вызвать ioctl с этими значениями — и на выходе получаем драйвер. Он работает.

Послесловие


Надеюсь, читать было интересно. Даже полезно, быть может. Некоторое было опущено или сокращено – зоркий читатель, быть может, обнаружит в драйвере и считывание состояния, и какой-то второй SetFeature, про который в тексте упомянуто вообще не было. Разработка драйверов и ядро Линукса – большие штуки, и за одну байку их полностью не осилить. Статья скорей не про это, а про то, что сделать что-то небольшое и приятное, в опен-сурс или для себя, совершенно не сложно. Надо только найти недельку вечеров с чаем и желание покопаться в коде.




К сожалению, не доступен сервер mySQL