Arch Linux на Xiaomi 12T Pro: стоковое ядро Android, vendor‑модули и девять граблей загрузки

в 8:54, , рубрики: android, Arch Linux, dual boot, gki, lineageos, linux на телефоне, synmobile, Wayland, wine, xiaomi 12t pro

На моём Xiaomi 12T Pro загружается Arch Linux. Это не Termux и не chroot, а полноценная система: штатное ядро телефона, systemd, Wayland. Android остаётся рядом, при включении выбор делается кнопкой громкости. Ниже: как это устроено, что работает и на какие грабли я наступил по дороге. Большинство из них не привязаны к модели и встретятся на любом Qualcomm‑телефоне с GKI.

Скриншоты
Arch Linux на Xiaomi 12T Pro: стоковое ядро Android, vendor‑модули и девять граблей загрузки - 1
Arch Linux на Xiaomi 12T Pro: стоковое ядро Android, vendor‑модули и девять граблей загрузки - 2
Arch Linux на Xiaomi 12T Pro: стоковое ядро Android, vendor‑модули и девять граблей загрузки - 3

Устройство

Xiaomi 12T Pro, он же Redmi K50 Ultra (diting): Snapdragon 8+ Gen 1, Adreno 730, 12 ГБ ОЗУ. Загрузчик разблокирован.

Подход: ядро от Android, система от Arch

Для Snapdragon 8+ Gen 1 нет готового mainline‑порта этой платы: нет ни дерева устройств, ни драйвера панели, ни тача. Собирать ядро из исходников Xiaomi тоже плохая идея: пересобранное ядро ломает ABI vendor‑модулей, и пересобирать тогда придётся всё.

Поэтому схема такая:

  • ядро — бинарный GKI от Google той же сборки, что стоит на телефоне (совпадает байт в байт, номер сборки виден в uname -r);

  • драйверы — готовые vendor‑модули из разделов vendor_boot и vendor_dlkm самого телефона;

  • всё остальное — чистый Arch Linux ARM.

От Android в системе остаются только ядро, модули и прошивки сопроцессоров. Arch лежит файлом‑образом в /data, сам Android не трогается.

Главное правило: ничего не прошивать, пока всё не работает через fastboot boot. Так система загружается один раз из памяти: если что‑то не так, перезагружаешь телефон, и он снова в Android.

Как грузится

fastboot boot boot.img  └─ загрузчик склеивает vendor_ramdisk + наш ramdisk, добавляет bootconfig       └─ ядро GKI распаковывает оба архива, наш /init перекрывает андроидовский            └─ /init (busybox): модули → /data → USB-сеть → switch_root в rootfs.img                 └─ systemd, Arch Linux ARM

Звучит просто. На деле почти каждая строчка стоила одной или нескольких перезагрузок вслепую: экрана ещё нет, логов тоже.

Как отлаживаться без экрана и логов

Пока не поднялись ни экран, ни USB, единственный канал связи с телефоном — вернулся он сам или нет. Я собирал initramfs, в котором /init делает одно: «перезагрузись через 5 секунд». Если телефон вернулся в fastboot сам через ~20 секунд, значит, наш init запустился. Если завис, проблема раньше. Дальше /init по кусочку обрастает кодом, и каждая гипотеза проверяется тем же способом.

Когда смонтирован /data, логи пишутся туда, и их можно забрать из Android через adb.

Грабли по порядку

1. Чёрный экран навсегда. Ramdisk производителя сжат lz4 -l (legacy‑формат, без маркера конца потока). Распаковав его, ядро не переключается на gzip нашего архива, и /init просто не появляется. Решение: свой архив тоже сжимать в lz4 legacy.

2. Модули «грузятся», но диска и USB нет. В modules.load от Qualcomm 118 нарушений порядка зависимостей. Android этого не замечает: его libmodprobe читает modules.dep. А голый insmod по списку работает строго по порядку. Пришлось написать рекурсивный загрузчик: сначала зависимости, потом softdep pre, потом сам модуль, потом softdep post.

3. Без softdep USB не поднимется никогда. PHY eUSB2, загруженный раньше своего репитера, не откладывает загрузку, а падает с ошибкой насовсем. Правильный порядок задан только в modules.softdep.

4. Экран «работает», но драйвер выбирает не ту панель. Параметр msm_drm.dsi_display0=… из командной строки ядра доходит до модуля, только если модуль грузит modprobe или libmodprobe: они сами читают /proc/cmdline. Busybox insmod этого не делает. Без параметра драйвер берёт эталонную панель Qualcomm 1080×2340, и кадры не идут. Решение: доставать параметры модуля из cmdline самому.

Загрузчик из пунктов 2–4 целиком (busybox sh):

loadmod() { # $1 = имя модуля (x.ko или x)    local n f d sd opts    n=$(basename "$1" .ko | tr '-' '_')    grep -q "^$n " /proc/modules && return 0    f=$(modfile "$(basename "$1" .ko)") || return 1    # жёсткие зависимости    for d in $(grep -E "^(.*/)?$(basename "$f"):" "$MODDIR/modules.dep" | cut -d: -f2); do        loadmod "$(basename "$d")"    done    # softdep pre    sd=$(sed -n "s/^softdep $n .*pre: *(.*)/1/p" "$MODDIR/modules.softdep" | sed 's/ *post:.*//')    for d in $sd; do loadmod "$d"; done    # параметры из cmdline (module.param=value), как делает modprobe    opts=$(tr ' ' 'n' < /proc/cmdline | sed -n "s/^$n.([^=]*=.*)/1/p" | tr 'n' ' ')    insmod "$f" $opts || return 1    # softdep post    sd=$(sed -n "s/^softdep $n .*post: *(.*)/1/p" "$MODDIR/modules.softdep")    for d in $sd; do loadmod "$d"; done
}

5. В GKI нет devtmpfs. Android создаёт узлы в /dev своим ueventd. У нас вместо этого tmpfs и mdev, а в Arch mdev работает демоном. Отдельная история: systemd без devtmpfs не слушает udev, и половина служб, завязанных на устройства, не стартует.

6. USB‑гаджет «поднялся», но компьютер его не видит. Причин две. В ядре есть фиктивный dummy_udc.0, и если взять первый попавшийся UDC, гаджет уедет туда. А контроллер dwc3 от Qualcomm спит, пока ему не сообщат роль порта (в Android это делает сопроцессор, которого у нас на этом этапе нет). Роль device нужно записать до ожидания UDC.

7. Android не видит файлы, созданные Linux. Всё, что Linux пишет на /data, для SELinux в Android остаётся «без метки», и adb не может такие файлы ни прочитать, ни перезаписать. Решение: каталоги и файлы создаются через adb, а Linux только дописывает в уже существующие (> сохраняет inode и метку, mv нет).

8. Телефон «сам выключился». Дважды. Кнопка питания приходит в Linux обычной клавишей, logind видит её и честно выключает систему. HandlePowerKey=ignore нужно поставить раньше, чем кто‑нибудь нажмёт кнопку.

9. pacman не ставит пакеты. pacman 7 требует Landlock, а в GKI его нет. Лечится DisableSandbox в pacman.conf.

Графика

Своего GL‑драйвера под этот GPU в Linux нет, доступен только Vulkan через turnip (драйвер Mesa) с бэкендом KGSL. Поэтому GL для композитора — это zink поверх turnip, плюс небольшой патч, чтобы turnip знал узел дисплея. Разница заметная: vkcube на весь экран на GPU даёт 60 кадров/с при 3% одного ядра, на CPU — 30 кадров/с при 54%.

Двойная загрузка: меню без экрана

Для постоянной работы свой boot.img прошивается в раздел boot. Внутри лежат и наш initramfs, и стоковый ramdisk Android. При включении /init первым делом грузит модули кнопок и вибромотора, телефон коротко вибрирует и три секунды ждёт: Vol+ — Arch, Vol− — Android. Если ничего не нажать, грузится то, что выбирали в прошлый раз. Экрана в этот момент ещё нет, так что меню — это вибрация.

Arch Linux на Xiaomi 12T Pro: стоковое ядро Android, vendor‑модули и девять граблей загрузки - 4

Счётчик попыток A/B

Самая неожиданная проблема была здесь. Примерно через семь загрузок в Arch телефон перестал загружаться совсем и остался в fastboot.

Оказалось, загрузчик (ABL) при каждом старте уменьшает счётчик попыток слота, пока система не поставит бит «загрузка успешна». Всё это хранится в атрибутах GPT‑записи boot_a/boot_b:

Биты

Значение

50

слот активен

51–53

осталось попыток

54

загрузка успешна

55

слот незагружаемый

Android ставит бит 54 сам (это делает boot_control HAL), Arch — нет. Когда счётчик доходит до нуля, слот помечается незагружаемым. На месте это лечится fastboot set_active a, навсегда — маленькой службой, которая при каждой загрузке ставит бит 54 и сбрасывает счётчик.

Менять нужно 8 байт атрибутов в первичной GPT и пересчитать два CRC (массива записей и заголовка). Ровно это делает сам ABL. sfdisk и sgdisk здесь не годятся: резервная GPT у Qualcomm лежит не там, где они её ищут, они сочтут её повреждённой и перезапишут. Суть на Python:

SUCCESS, UNBOOTABLE = 1 << 54, 1 << 55
RETRY_SHIFT, RETRY_MASK = 51, 0x7
hdr = bytearray(os.pread(fd, ss, ss))                 # заголовок первичной GPT (LBA 1)
elba, n, esz, _ = struct.unpack_from("<QIII", hdr, 72)
ents = bytearray(os.pread(fd, n * esz, elba * ss))    # массив записей
a = struct.unpack_from("<Q", ents, i * esz + 48)[0]   # атрибуты записи boot_<слот>
a = (a | SUCCESS) & ~UNBOOTABLE
a = (a & ~(RETRY_MASK << RETRY_SHIFT)) | (RETRY_MASK << RETRY_SHIFT)
struct.pack_into("<Q", ents, i * esz + 48, a)
struct.pack_into("<I", hdr, 88, zlib.crc32(ents))     # CRC записей
hsize = struct.unpack_from("<I", hdr, 12)[0]
h = bytearray(hdr[:hsize]); struct.pack_into("<I", h, 16, 0)
struct.pack_into("<I", hdr, 16, zlib.crc32(h))        # CRC заголовка

Дальше записываются обратно только изменённый сектор записей и заголовок.

Что работает

  • экран 120 Гц, тач, вибрация, автоповорот и автояркость;

  • GPU: Vulkan и композитинг на Adreno 730;

  • Wi‑Fi, Bluetooth, звук;

  • зарядка, батарея, сон;

  • камера (фото, ночной режим, видео 4K), аппаратное декодирование видео;

  • GPS, NPU Hexagon;

  • USB OTG (мышь, клавиатура, флешки), ИК‑порт.

Сверху стоит Wayland‑оболочка, рассчитанная на сенсорный экран: рабочие столы с виджетами, две шторки, меню приложений, список недавних, экранная клавиатура.

Ещё скриншоты
Arch Linux на Xiaomi 12T Pro: стоковое ядро Android, vendor‑модули и девять граблей загрузки - 5
Arch Linux на Xiaomi 12T Pro: стоковое ядро Android, vendor‑модули и девять граблей загрузки - 6
Arch Linux на Xiaomi 12T Pro: стоковое ядро Android, vendor‑модули и девять граблей загрузки - 7
Arch Linux на Xiaomi 12T Pro: стоковое ядро Android, vendor‑модули и девять граблей загрузки - 8

Windows‑игры

Windows‑игры идут через Wine 11 с патчами для ARM64EC, FEX и WoW64 (их набралось больше двадцати), DXVK работает поверх turnip. Касания работают как мышь, а экранный геймпад изображает контроллер Xbox 360.

Arch Linux на Xiaomi 12T Pro: стоковое ядро Android, vendor‑модули и девять граблей загрузки - 9
Arch Linux на Xiaomi 12T Pro: стоковое ядро Android, vendor‑модули и девять граблей загрузки - 10

Android‑приложения

Android работает в контейнере (LineageOS 20), а его приложения открываются обычными окнами рядом с Linux‑программами.

Arch Linux на Xiaomi 12T Pro: стоковое ядро Android, vendor‑модули и девять граблей загрузки - 11

Итог

Самое ценное, что я вынес: на Qualcomm с GKI не нужно ни mainline‑ядро, ни сборка ядра из исходников производителя. Достаточно ядра и модулей, которые уже лежат в телефоне. Нужно только повторить руками то, что за тебя молча делают ueventd, libmodprobe и boot_control HAL в Android. Почти все грабли выше — как раз про это.

Автор: Vitaly1387

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js