С чего всё началось
Начнём с постановки проблемы. Дано: один ноутбук. Новый ноутбук, геймерский. С RGB-подсветкой. Вот такой примерно ноутбук:
Картинка взята с lenovo.com
Есть ещё программа к этому ноутбуку. Программа как раз этой подсветкой и управляет.
Одна только проблема – программа под Windows, а хочется чтоб в любимом линуксе всё работало. И лампочки чтоб светились, и чтоб цвета красивые мелькали. Да вот только как это сделать, чтоб без реверс-инжиниринга и без написания своих драйверов? Простой ответ пришёл быстро – никак. Ну что ж, пошли писать драйвер.
Шаг 1 – копаемся в коде
У нас есть три места, из которых можно увидеть как подсветка мигает. В порядке возрастающей сложности:
1. Большая и накачанная геймерская программа Lenovo Nerve Center – в которой есть функция настройки всей этой подсветки.
2. Сочетание горячих клавиш Fn+Space – возможно. его обрабатывает эта же программа.
3. BIOS. Во время загрузки ноутбука подсветка тоже мелькает – но только красным, и только на секунду.
Забегая вперёд, скажу что пришлось попробовать все три, но продвинулся с каким-никаким успехом я только по первой. О ней речь и пойдёт.
Ну что ж, откроем папку с программой:
Сразу замечаем, что есть DLL с интересным названием – LedSettingsPlugin.dll. Наш ли…? Давайте откроем в IDA Pro и посмотрим.
Обратите внимание на правую половину экрана. От программистов осталось много дебажной информации – воспользуемся же ей. Видим, что почему-то есть много строк, похожих на имена методов. Почему?
А это и есть имена методов. Как удобно! Давайте поназываем что можем своими именами, и посмотрим на список функций ещё раз. Для называния в IDA можно использовать хот-кей N, или просто щелчок правой кнопкой мыши по тому, что хотим обозвать.
Смотрим на функции… Y720LedSetHelper::SetLEDStatusEx. Похоже, что нам надо! Замечаем, что тут формируется что-то вроде строки и передаётся потом в некий CHidDevHelper::HidRequestsByPath. Конкретно интересует var_38, все метаморфозы которой IDA любезно обозначил за нас.
Var_34 нам тоже интересен. Он идёт сразу после var_38 – в традициях ассемблера переменные хранятся в обратном порядке под RSP. Var_34 здесь просто название константы – -34h.
Получаем, что начиная с var_38 программа кладёт ноль, потом стиль, цвет, число три и блок – часть клавиатуры, к которой этот цвет применится. (Забегая вперёд, скажу что три здесь это значение яркости. Сделав это управляемым, мы получим драйвер ещё круче оригинала!)
Давайте же залезем в HidRequestsByPath и наконец узнаем, как оно отправляется.
Видим две функции, HidD_GetFeature и HidD_SetFeature. Оба в файле не прослеживаются… Зато очень хорошо прослеживаются в официальной документации Майкрософт — тут и docs.microsoft.com/en-us/windows-hardware/drivers/ddi/hidsdi/nf-hidsdi-hidd_getfeature>тут.
На этом можно себя поздравить – мы добрались. Это дно, глубже копать не надо. В Linux такие функции есть – надо только вызвать их с теми же аргументами, и всё должно получиться… ведь правда?
Шаг 2 – запускаем прототип…?
Не совсем. Начнём с простого, и запустим lsusb. Так найдём клавиатуру, и к ней что-нибудь пошлём.
Integrated Technology Express, Inc. здесь выглядит самым интересным. Будем пользоваться известным инструментом /dev/hidraw. Отыскиваем подходящий… Это делается простым поиском по файлам /sys/class/hidraw/hidraw*/device/uevent.
Вот тот. Цифры совпадают – значит это устройство есть hidraw0. Но мы пытаемся послать данные, и ничего не получается! Бред какой-то. На этом этапе руки начинают опускаться… Может быть, не для простых смертных это, этот реверс-инжиниринг?
Но продолжим. Попытаемся. Если автор бы разбирался в этом всём, он бы выреверсил поиск нашего драйвера из того же Nerve Center.… Но у нас нет мозга. Идём обратно в Windows, есть одна идея.
Есть в Windows такая вещь – Диспетчер Устройств. Многое позволяет делать – нам интересно то, что он позволяет отрубать девайсы. Просто и бескомпромиссно.
Давайте отрубать девайсы по одному, пока состояние лампочек не прекратит меняться.
Этот отрубился. Значит, если заглянуть в Hardware ID – увидим и то, как его зовут и с чем его едят.
Смотрим – это же он.
Девайс, который мне уже несколько лет флудил в dmesg.
[Почему же он не показывался в lsusb? А не USB он вовсе. Тут используется протокол I2C HID, который позволяетЛирическое отступление – давайте сделаем коммит
В поисках правильного девайса я расковырял свою установку почти до ядра. Ещё, не очень нравилось что мне эту dmesg-простыню показывало перед каждой блокировкой. Раз уж я здесь, почему бы не написать короткий коммит? Руки всё равно чешутся.
Что нам нужно посмотреть – где девайсы на I2C HID, и где писать какие странности им присущи. Здесь. Не мудрствуя лукаво, прислушаемся к ошибке – incorrect input length. Давайте сделаем его correct.
Добавим новый quirk, назовём его пусть I2C_HID_QUIRK_BAD_INPUT_SIZE. По аналогии с уже существующими.
Добавим ещё наше устройство в список 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 не равен тому что оно хочет. Давайте если квирк задействован, делать зто же самое, только наоборот.
Отправляем патч в рассылку, не забыв потестить… (если честно, чтоб просто добавиться в рассылку мозга уже потребовалось больше. Это непросто.)
И всё.
Шаг 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, про который в тексте упомянуто вообще не было. Разработка драйверов и ядро Линукса – большие штуки, и за одну байку их полностью не осилить. Статья скорей не про это, а про то, что сделать что-то небольшое и приятное, в опен-сурс или для себя, совершенно не сложно. Надо только найти недельку вечеров с чаем и желание покопаться в коде.