✍️ Статья Только самое нужное. Избавляем Linux от багажа прошивок для оборудования

The 146X Project Dublikat Web Studio Avram Lincoln AL Service Navigator Knyaz

BlackPope

Команда форума
Модератор DeepWeb ✔️
PR-Group DeepWeb 🔷
Регистрация
27.04.2020
Сообщения
230
Разработчики Linux поддерживают не только множество драйверов устройств в исходном коде ядра, но и большую коллекцию прошивок к устройствам в пакете linux-firmware. Но когда размер системы имеет значение — нужно отсечь лишнее. Модули ядра легко отключить в make menuconfig, а вот выбрать нужные прошивки сложнее. Поэтому сегодня мы напишем скрипт, который позволит тебе извлечь из полугигабайтной коллекции прошивок несколько мегабайт, нужных именно твоей машине.

С железом нередко бывает так, что одного драйвера в пространстве ядра ОС для его работы недостаточно. Нужна также прошивка (firmware), которая загружается в само устройство. Точный формат и назначения прошивки зачастую известны только производителю: иногда это программа для микроконтроллера или FPGA, а иногда просто набор данных. Пользователю это не важно, главное, что устройство не работает, если ОС не загрузит в него прошивку.

В свободных операционных системах прошивки нередко вызывают споры. Многие из них распространяются под несвободными лицензиями и без исходного кода. Авторы OpenBSD и ряда дистрибутивов GNU/Linux считают это проблемой и со свободой, и с безопасностью и принципиально не включают такие прошивки в установочный образ.

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

Полный набор из linux-firmware занимает более 500 Мбайт в распакованном виде. При этом каждой отдельно взятой системе требуется только небольшая часть этих файлов, остальное — мертвый груз.

Даже в современном мире с дисками на несколько терабайт еще много случаев, когда размер имеет значение: встраиваемые системы, образы для загрузки через PXE и подобное. Хорошо, если о board support package позаботился кто-то другой, но это не всегда так.

Если ты точно знаешь полный список нужного железа, можно извлечь файлы вручную. Впрочем, даже в этом случае найти нужные файлы может быть непросто — linux-firmware представляет собой не очень структурированную кучу файлов, и списка соответствия файлов именам модулей ядра там нет. А если ты хочешь дать пользователям возможность легко собрать свой образ, тут и вовсе нет выбора — нужно автоматическое решение.

В этой статье я расскажу о своем способе автоматической сборки. Он неидеален, но автоматизирует большую часть работы, что уже неплохо. Писать скрипт будем на Python 3.

INFO
Примеры кода в статье упрощенные. Готовый и работающий скрипт ты можешь найти на GitHub.

К примеру, можно им просмотреть список прошивок для включенных в .config драйверов сетевых карт Realtek.

$ make menuconfig
$ make prepare
$ list-required-firmware.py -s drivers/net/ethernet/realtek/
rtl_nic/rtl8168d-1.fw
rtl_nic/rtl8168d-2.fw
rtl_nic/rtl8168e-1.fw
...


Основы

В ядре Linux нет глобального списка прошивок и кода для их загрузки. Каждый модуль загружает свои прошивки с помощью функций из семейства request_firmware. Логично, если учесть, что процедура загрузки прошивки у каждого устройства разная. Соответственно, информация о нужных прошивках разбросана по множеству отдельных файлов с исходным кодом.

На первый взгляд, некоторую надежду дает опция сборки FIRMWARE_IN_KERNEL. Увы, на деле она встраивает в файл с ядром только файлы, которые ты явно укажешь в EXTRA_FIRMWARE. Так что файлы все равно сначала придется найти.

Поиск по вызовам request_firmware() тоже не очень перспективен. Некоторые модули поддерживают несколько разных прошивок, да и имя файла часто хранится в переменной. В качестве примера можно посмотреть на фрагмент кода из драйвера сетевой карты Intel e100.

К счастью для нас, модули должны указывать нужные им файлы прошивок с помощью макроса MODULE_FIRMWARE(). Пример можно найти в e100. Этот макрос определен в файле include/linux/module.h.

Именно из вывода этого макроса берется информация о прошивках, которую можно увидеть в выводе утилиты modinfo.

$ sudo modinfo e100 | grep firmware
firmware: e100/d102e_ucode.bin
firmware: e100/d101s_ucode.bin
firmware: e100/d101m_ucode.bin


В ряде случаев можно было бы обойтись одной modinfo. Если у тебя есть собранное ядро и возможность его загрузить, ты можешь просмотреть вывод modinfo для каждого нужного модуля. Это не всегда удобно или вообще возможно, так что мы продолжим искать решение, для которого понадобится только исходный код ядра.

Здесь и далее будем считать, что все ненужные модули отключены в конфиге сборки ядра (Kconfig). Если мы собираем образ для конкретной системы или ограниченного набора систем, это вполне логичное предположение.

Ищем исходники модулей

Чтобы собрать список имен файлов прошивок, мы сначала составим список всех файлов исходного кода ядра, где они используются. Затем мы прогоним эти файлы через препроцессор из GCC, чтобы раскрыть все макросы, и извлечем собственно имена нужных файлов.

Находим все включенные в конфиге модули

Это самая простая часть. Конфиг сборки ядра имеет простой формат «ключ — значение» вроде CONFIG_IWLWIFI=m. Значение может быть n (не собирать), y (встроить в ядро) или m (собрать в виде модуля).

Нас интересуют только ключи, а какое значение там, y или m, нам не важно. Поэтому мы можем выгрести нужные строки регулярным выражением (.*)=(?:y|m). В модуле re из Python синтаксис (?:...) используется для незахватывающих групп (non-capturing group), так что захвачена будет только часть в скобках из (.*)=.

def load_config(path):
with open(path, 'r') as f:
config = f.read()
targets = re.findall(r'(.*)=(?:y|m)', config)
return targets


Находим нужные каталоги и файлы исходников

В каждом Makefile можно найти выражения вида obj-$(CONFIG_SOMETHING) += .... Переменные CONFIG_SOMETHING разрешаются в их значения из конфига и, соответственно, добавляются в списки целей obj-y (встроить в ядро) и obj-m (оставить модулем).

В файлах верхнего уровня (вроде drivers/net/wireless/intel/Makefile) в качестве значения фигурирует подкаталог с модулем, вроде obj-$(CONFIG_IWLWIFI) += iwlwifi/. Внутри каталога с модулем (drivers/net/wireless/intel/iwlwifi/Makefile) упоминается конечный объектный файл, наподобие obj-$(CONFIG_IWLWIFI) += iwlwifi.o. Мы ограничимся случаем с целью-каталогом.

Цели-каталоги мы будем искать по выражению obj-\$\((.*)\)\s+\+=\s+(.*)/(?:\n|$) в Makefile.

def find_enabled_subdirs(config, makefile_path):
with open(makefile_path, 'r') as f:
makefile = f.read()

dir_stmts = re.findall(r'obj-\$\((.*)\)\s+\+=\s+(.*)/(?:\n|$)', makefile)
subdirs = []

for ds in dir_stmts:
config_key, src_dir = ds

if config_key in config:
subdirs.append(src_dir)

return subdirs


А что с целями-файлами? Если мы говорим о модулях из нескольких файлов в каталоге, то их файлы *.c мы обработаем при обходе самого каталога. Существуют модули из одного файла, в основном для старых устройств вроде drivers/net/ethernet/intel/e100.c. Таких модулей весьма немного, так что мы игнорируем их существование. При желании можно добавить обработку подобных случаев, но я не уверен, практично ли это. Лучше мы чуть позже сделаем так, чтобы их прошивки были включены по умолчанию, а не выключены.

Ну и наконец, нам нужен способ отфильтровать файлы, в которых есть ссылки на прошивки. Это совсем просто для каждого, кто знаком с регулярными выражениями.

def file_loads_firmware(file):
with open(file, 'r') as f:
source = f.read()
if re.search(r'MODULE_FIRMWARE\((.*)\)', source):
return True


Мы игнорируем аргумент макроса MODULE_FIRMWARE. Чаще всего там не явное имя файла, а другая константа, а то и целое выражение. Поэтому мы сначала составим список нужных файлов с исходниками, а потом уже будем разбираться, что с ними делать.

Осталось собрать воедино функцию для построения списка файлов из указанного каталога, в которых есть ссылки на прошивки.

def collect_source_files(config, path):
files = []

makefile = os.path.join(path, "Makefile")

c_files = glob.glob("{0}/*.c".format(path))
files = list(filter(file_loads_firmware, c_files))

enabled_subdirs = find_enabled_subdirs(config, makefile)
subdirs = glob.glob("{0}/*/".format(path))
for d in subdirs:
dir_name = d.rstrip("/")

if os.path.exists(os.path.join(d, "Makefile")):
if os.path.basename(dir_name) in enabled_subdirs:
files = files + collect_source_files(config, d)
else:
c_files = glob.iglob("{0}/**/*.c".format(d), recursive=True)
files += list(filter(file_loads_firmware, c_files))

return files


Очень экономит время библиотека glob из стандартной поставки Python 3. С версии 3.5 у нее есть поддержка рекурсии. Некоторые модули содержат вложенный каталог с исходниками, а порой и не один. Если в таком каталоге есть свой Makefile, то это независимый модуль, что мы обрабатываем в первой ветке условия. Однако, если Makefile отсутствует, это «просто» подкаталог, и нам нужны все файлы *.c из него и всех подкаталогов. Модуль glob умеет это не хуже, чем шелл, и выражение **/*.c отлично работает (если не забыть recursive=True).

Извлекаем данные о прошивках

Остается последнее — извлечь собственно имена файлов прошивок. Будем прогонять файлы с исходным кодом через препроцессор из GCC, чтобы вместо имен макросов получить готовые строки имен файлов из linux-firmware.

Вообще, остановить GCC после стадии препроцессора можно опцией -E. Увы, для этого нужен правильный список каталогов с заголовочными файлами. Некоторые модули, вроде упомянутого iwlwifi, содержат множество вложенных каталогов и используют, кроме стандартных заголовков ядра, еще и свои общие файлы, что сильно усложняет дело.

Однако в Makefile ядра есть цели для файлов *.i — это как раз файлы *.c после обработки препроцессором.

К примеру, make drivers/net/wireless/intel/iwlwifi/cfg/9000.i превратит файл 9000.c в голый C без директив и макросов. Макрос MODULE_FIRMWARE генерирует для полей структуры данных о модуле уникальные имена, которые можно узнать по подстроке __UNIQUE_ID_firmware. Эти строки мы и будем извлекать.

static const char __UNIQUE_ID_firmware338[] __attribute__((__used__)) __attribute__((section(".modinfo"), unused, aligned(1))) = "firmware" "=" "iwlwifi-9000-pu-b0-jf-b0-" "46" ".ucode";

Выражение вида const char* hello = "hello" " " "world" — немного непривычный, но вполне законный способ записи строковых констант, эквивалентный const char* hello = "hello world". Нам придется это учесть.

fw_files = []
for sf in source_files:
i_file = re.sub(r'\.c', r'.i', sf)
res = subprocess.run(["make {0} 2>&1".format(i_file)], shell=True, capture_output=True)
if res.returncode != 0:
print("Failed to preprocess file {0}".format(sf), file=sys.stderr)
print(res.stdout.decode(), file=sys.stderr)
else:
with open(i_file, 'r') as f:
source = f.read()
fw_statements = re.findall(r'__UNIQUE_ID_firmware.*"firmware"\s+"="\s+(.*);', source)
fw_files += list(map(lambda s: re.sub(r'(\s|")', r'', s), fw_statements))


Из всех способов выполнить внешнюю программу мы воспользовались самым быстрым и грязным — subprocess.run([command], shell=True, capture_output=True). В таких скриптах, где Python, по сути, отбирает работу у sh, я каждый раз радуюсь, что этот способ там есть.

Заключение

Может быть, извлечение данных о прошивках из кода ядра и достаточно экзотичная задача. Но даже если сам скрипт тебе не пригодится, вполне вероятно, ты узнал что-то новое об организации исходного кода ядра Linux и сможешь применить мой опыт в своих скриптах автоматизации.
 

📌 Золотая реклама

AnonPaste

Верх