суббота, 27 ноября 2010 г.

Как сделать невозможное?

Ответ на вопрос прост - не останавливаться, какой бы сложной не была задача. Даже если все вокруг говорят, что это невозможно.
Моя задача была заменить драйвер устройства в Linux, вшитый намертво в ядро. Были бы исходники этого ядра - все решалось бы сравнительно просто, но в нашем случае (ядро читалки Nook) исходники были не рабочие и годичной давности. На x86 я бы дизассемблировал место инициализации драйвера в ядре и забил NOPами, но тут уже ARM архитектура и ядро в zImage..

Сам процесс написания и отладки драйвера был довольно долгим - пришлось допиливать исходники ядра, чтобы устройство хоть как-то загружалось, но это тема для другого рассказа. Важно то, что получился собственный драйвер (бекпорт с другого устройства с дополнительными фозможностями), который может работать как модуль ядра и инициализируется 1в1 так же, как и тот, что надо заменить.

Первые эксперименты были прозаичны - хитрые вызовы insmod/rmmod, шаманство с именем драйвера и пр. Затем я обратился за советом к друзьям-линуксоидам и они подтвердили, что стандартными средствами такого не добиться. Исключения очень редки - если драйвер может принимать параметры с адресами устройств, командами включиться/выключиться и пр., то можно еще что-то сделать, но в нашем случае никаких параметров не было.

Затем начались эксперименты на уровне исходников. Мое внимание привлек такой код:

static int __devinit synaptics_ts_init(void)
{
synaptics_wq = create_singlethread_workqueue("synaptics_wq");
if (!synaptics_wq)
return -ENOMEM;
return i2c_add_driver(&synaptics_ts_driver);
}

static void __exit synaptics_ts_exit(void)
{
i2c_del_driver(&synaptics_ts_driver);
if (synaptics_wq)
destroy_workqueue(synaptics_wq);
}

Если существует функция i2c_del_driver для выгрузки драйвера, то почему нельзя вызвать ее из своего модуля, чтобы выгрузить чужой? К сожалению, в ядре Linux есть ситуации, когда не имея указателя никак нельзя обратиться к обьекту. Например, метод destroy_workqueue можно вызывать только с указателем очереди, а он есть лишь у того, кто эту очередь создал и без получить его малореально - никаких find_workqueue в природе нет.

В нашем же случае шанс был - пусть и i2c не позволяет искать драйвер по имени, но по коду i2c-core.c стало понятно, что используются низкоуровневые вызовы device_register/device_unregister из linux/device.h. Там же нашлась функция driver_find, которая ищет драйвер по имени и идентификатору шины. Имя встроенного драйвера известно, а шина используется i2c и ее идентификатор в переменной i2c_bus_type:

struct device_driver * other;

other = driver_find(SYNAPTICS_I2C_RMI_NAME, &i2c_bus_type);

if (other)
{
printk("Previous driver found: %s\n", other->name);
return -ENOMEM;
}

Этот код корректно находит предыдущий драйвер и выводит его имя. Конечно же, этого было мало, потому я добавил driver_unregister(other) и получил Kernel Panic :)
Логично предположить, что раз мы регистрируем драйвер через i2c_add_driver, то и удалять его надо через i2c_del_driver, а не напрямую. Из описания структуры i2c_driver видно, что она содержит device_driver, которая и возвращается функцией driver_find. Для преобразования из указателя на вложенную структуру к родительской есть несколько подходов, но проще всего использовать стандартную конверсию to_i2c_driver. Получился вот такой код:

struct i2c_driver * otherDriver;
struct device_driver * other;

other = driver_find(SYNAPTICS_I2C_RMI_NAME, &i2c_bus_type);

if (other)
{
otherDriver = to_i2c_driver(other);
printk(KERN_ERR "Previous driver found: %s, addr 0x%x, owner %x\n", other->name, (int)otherDriver, (int)other->owner);
i2c_del_driver(otherDriver);
}

Что характерно, он тоже приводит к Kernel Panic. На этом этапе я уже было решил, что дерегистрация невозможна, но случайно заметил в drivers/base/core.c, что после вызова driver_find обычно вызывается функция put_driver. Оказалось, что у драйверов, как обектов ядра, есть интрузивный подсчет ссылок и после driver_find счетчик увеличивается на единицу, что не дает выгрузить драйвер во время i2c_del_driver. Добавление этого вызова поставило все на свои места и встроенный драйвер стал корректно выгружаться.

Конечно, сразу все не заработало, т.к. еще существовала workqueue с таким же именем, да и встроенный драйвер оказался "нечист на руку" - не удалял sysfs файлы при выгрузке, но это уже все было решаемо.

Результатом этих "танцев с бубном" стал собственный драйвер тачскрина для Nook с поддержкой Multitouch. Подробнее о самом драйвере можно почитать по этим ссылкам:
http://nookdevs.com/Multitouch
http://www.the-ebook.org/forum/viewtopic.php?t=16728