Fedora поддерживает множество различных архитектур, среди которых как 64-битная, так и 32-битная архитектуры Arm. В Fedora мы называем их именами «aarch64» и «armv7hl» соответственно. Этот пост представляет собой небольшую запись трудной для отладки проблемы с 32-битными сборщиками, используемыми для сборки пакетов, которые в случае Fedora запускаются как гостевые виртуальные машины на 64-битных хостах Armv8.

Проект Fedora Arm начался много лет назад с усилий по поддержке 32-разрядных поколений оборудования Arm. Вначале существовали сборки, нацеленные на 32-разрядную архитектуру v5, затем она была заменена текущим минимумом Armv7 (технически вариант, требующий дополнительного оборудования с плавающей запятой VFPv3 и обновленного связанного ABI, вместе известного как «armv7hl». »). Armv8 вводит тонкое понятие состояния выполнения, которое определяет как «AArch32», так и «AArch64». Для целей этого обсуждения вы можете рассматривать AArch32 как продолжение Armv7.

Некоторое время 32-битная архитектура была официальной целью для Arm (и даже до этого она была «вторичной» архитектурой благодаря тяжелой работе бесчисленного множества других, кто ее выполнил). В самом деле, даже спустя много времени после того, как 64-битное оборудование стало широко распространенным, 32-битное оборудование было официально поддерживаемой целью (64-битное сейчас также официально поддерживается), и по многим причинам оно остается важной и необходимой целью для множества пользователей. Не последним из них является сама Fedora.

Сборки в Fedora всегда выполняются «по назначению», что означает, что сборки Arm выполняются на оборудовании Arm. В случае 32-битных серверов это были только 32-битные серверы раннего поколения, но позже они были перемещены на 64-битные хосты с 32-битными гостевыми виртуальными машинами. Одним из преимуществ перехода на 64-битные серверные хосты Arm было то, что можно было получить гораздо более мощное оборудование серверного уровня с большим количеством ядер и памяти, доступными для 32-битных сборщиков виртуальных машин, которыми затем можно было бы управлять почти так же, как и для других архитектур (включая «aarch64», который также использует виртуальные машины для сборщиков).

В случае текущего поколения сборщиков гостевая среда работает в состоянии выполнения AArch32 (обратно совместима с Armv7), используя LPAE (большие расширения физических адресов), добавленные в Armv7 для поддержки сборщиков с более чем 32-битными физическая память. Как и в случае аналогичного подхода «PAE», используемого в 32-разрядных системах x86_64 для адресации более 4 ГБ физической памяти, отдельные программы по-прежнему ограничены использованием не более 4 ГБ, но вы можете запускать многие такие программы в системе LPAE. Это полезно при запуске сборщиков, которые хотят максимизировать доступную память для сборок.

Перенесемся в май 2018 года и обнаружим эту ошибку, влияющую на 32-битные сборщики Fedora Arm, которые работали с другим ядром хоста:



1576593 - Время от времени гости Fedorn 29 armv7« делают паузу на rhel-alt 7.6…
Изменить описание bugzilla.redhat.com»



Проблема, казалось, делится на два разных типа ошибок:

  1. Гостевые процессы иногда аварийно завершали работу с непонятным сообщением об ошибке («Необработанное прерывание предварительной выборки: ошибка реализации (прерывание блокировки) (0x234)»). Гость продолжал бежать, кроме этого.
  2. Гипервизор хоста иногда «приостанавливал» гостя с журналом в ядре хоста («декодирование инструкций загрузки / сохранения не реализовано»).

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

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

$ while true; do make clean; make ARCH=arm KCONFIG_CONFIG=../../kernel-armv7hl-debug.config olddefconfig; done

Более интересная ситуация показалась (мне), когда гипервизор (KVM) завершил «паузу» в гостевой системе в результате ошибки «декодирование инструкции загрузки / сохранения не реализовано». Когда это происходит, нам повезло, что гостевая виртуальная машина остается в состоянии, позволяющем подключить различные инструменты отладки и диагностики, чтобы понять, что она пыталась сделать в то время.

Запустив «список вирусов» на гипервизоре хоста, можно увидеть результат, подобный следующему:

1 fedora30-arm-1 paused

Это означает, что «fedora30-arm-1» больше не запускается, но и не был убит. Таким образом, мы можем запустить дамп его памяти снова на хосте:

virsh dump — memory-only — format elf fedora30-arm-1 fedora30-arm-1.core

Результирующий файл дампа будет содержать как полную «физическую» память гостя, так и его активный регистровый файл (хотя этого недостаточно, чтобы быть действительно полезным для отладки того, что будет дальше). Теперь хост представляет собой 64-битную среду, и, чтобы облегчить жизнь, я скопировал этот дамп на другую 32-битную гостевую систему для анализа. Таким образом, я мог запускать различные 32-битные собственные инструменты. Таким образом, у нас теперь два гостя и один хозяин. Я назову гостей «строитель» (тот, у которого возникли проблемы) и «отладчик» (тот, на котором я запускаю инструменты).

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

# gdb fedora30-arm-1.core

(gdb) info registers
r0 0xb6f2f640 3069376064
r1 0xb69b4138 3063628088
r2 0xb6f31000 3069382656
r3 0xb6a21958 3064076632
r4 0xb6f2f640 3069376064
r5 0x6ffffdff 1879047679
r6 0x6ffffeff 1879047935
r7 0xb69b4000 3063627776
r8 0xb6a22040 3064078400
r9 0xb6f2f79c 3069376412
r10 0xbec20574 3200386420
r11 0xbec204f4 3200386292
r12 0x6fffffff 1879048191
sp 0xbec203e0 0xbec203e0
lr 0xb6f0670c 3069208332
pc 0xb6f0b6f4 0xb6f0b6f4
cpsr 0xa0000010 2684354576

Здесь мы видим, что гость работал в пользовательском пространстве («pc» меньше 3 ГБ KERNEL_OFFSET) по адресу 0xb6f0b6f4. Обратите внимание, что этот адрес не совпадает с тем, который будет указан в task_struct для процесса, запущенного в то время, потому что гостевое ядро ​​никогда не обнаруживало сбоя, оно перешло бы прямо к гипервизору (но подробнее об этом позже).

Затем я установил точно соответствующий пакет «kernel-debuginfo» для ядра, которое работало на компоновщике. Я сделал это, вручную загрузив связанные RPM, но вы также можете использовать yum-downloader или просто команду «dnf», если это последнее ядро, соответствующее тому, что у вас уже есть.

В любом случае, я установил debuginfo и его «общую» зависимость:

rpm -ivh kernel-lpae-debuginfo-5.1.9–300.fc30.armv7hl.rpm

На самом деле все, что я хотел, это vmlinux. Имея это, я мог бы использовать отличную утилиту crash Дэйва Андерсона для извлечения полезной информации:

# crash /usr/lib/debug/lib/modules/5.1.9–300.fc30.armv7hl+lpae/vmlinux fedora30-arm-1.core

Как только запускается сбой, он отображает приглашение, из которого вы можете использовать одну из различных полезных встроенных программ для отображения информации о том, что гость делал во время перехода. В этом случае мы собираемся использовать «ps», чтобы увидеть, каков был запущенный набор задач. Результат включал:

> 725 716 0 da876900 RU 0.1 5968 972 make

Теперь я могу приказать аварийному завершению переключиться на «make» (set -x 725) в качестве текущей задачи и вывести некоторую полезную информацию о том, что она делала. Среди них все текущие карты виртуальных машин, которые использовались для исполняемого двоичного файла:

crash> vm

В выходные данные включено отображение для динамического компоновщика:

da814000 b6f00000 b6f21000 875 /usr/lib/ld-2.29.so

Первый адрес - это «VMA» (область виртуальной памяти) для памяти, определяемой областью между второй и третьей записями (0xb6f00000–0xb6f21000). Это означает, что мы знаем, что гость выполнял код, расположенный в двоичном файле ld-2.29.so, и, вычитая VMA из компьютера, мы можем узнать, что именно он был со смещением 0xb6f4 двоичного файла ld-2.29.so. Теперь мы, конечно, можем загрузить эту библиотеку с помощью различных инструментов, включая gdb.

Я предпочитаю использовать objdump (я странный):

# objdump -x -D ld-2.29.so | less

Мы видим, что 0xb6f4 находится внутри функции _dl_setup_hash:

0000b6e0 <_dl_setup_hash>:
b6e0: e5903150 ldr r3, [r0, #336] ; 0x150
b6e4: e3530000 cmp r3, #0
b6e8: 0a000013 beq b73c <_dl_setup_hash+0x5c>
b6ec: e5931004 ldr r1, [r3, #4]
b6f0: e92d4010 push {r4, lr}
b6f4: e5912000 ldr r2, [r1]

Возможно, более интересным является то, что мы можем взглянуть на предыдущие инструкции, чтобы увидеть, что произошло непосредственно перед ошибочной загрузкой (ldr). Ранее мы загрузили значение в r1, которое было смещено на 4 байта от значения в r3. Мы видим, что r3 содержит 0xb6a21958, поэтому ранее успешная загрузка выполнялась из области виртуальной памяти 0xb6a2195c. Впоследствии мы попытались загрузить из виртуальной памяти 0xb69b4138 в r2 и упали.

Итак, мы помним ранее, что причина «паузы» d у гостя заключалась в том, что хост (гипервизор) жаловался на то, что не может декодировать операцию загрузки или сохранения. Были некоторые предположения (до исследования фактического дампа), что в некоторых 32-битных двоичных файлах начала появляться какая-то сложная последовательность инструкций загрузки / сохранения, и что это было причиной проблемы. Не вдаваясь в подробности, достаточно сказать, что у вас могут быть инструкции загрузки / сохранения в 32-битной Arm, которые довольно интересны и практически невозможно эмулировать при необходимости в гипервизорах, таких как KVM.

Но, глядя на приведенную выше нагрузку, она очень проста. На самом деле, это даже проще, чем следующая за ним нагрузка. И глядя на код ядра, который выводит сообщение «load/store instruction decoding not implemented», мы видим, что оно исходит от virt/kvm/arm/mmio.c. В частности, из io_mem_abort, который вызывается kvm_handle_guest_abortvirt/kvm/arm/mmu.c) в значительной степени как провал. Мы вернемся к этому позже, но немного предвещая, что этот провал прискорбен и приводит к бесполезному сообщению об ошибке, основанному на предположениях, которые оказываются неверными.

В порядке. Так что это не странная эмуляция загрузки / сохранения, это сложная проблема. Но почему-то нам все равно не удалось выполнить загрузку с места. Давайте попробуем прочитать значение из-за сбоя. Попытка выполнить загрузку с помощью «rd» не сработает:

> rd 0xb69b4138
rd: seek error: user virtual address: b69b4138 type: “32-bit UVADDR”

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

Crash предоставляет нам полезный макрос доступа «vtop», который будет выполнять обход таблицы страниц в виртуальной памяти процесса. Используя vtop, мы можем попытаться преобразовать 0xb69b4138 в адрес гостевой физической памяти, содержащийся в файле дампа, который затем мы можем напрямую прочитать с помощью команды «rd». «vtop» вносит досадный беспорядок в перевод LPAE, но при этом выводит правильную запись таблицы страниц (PTE), из которой мы можем прочитать физический адрес страницы, содержащей нужные нам данные:

4200004387c1fdf 4387c1000 (PRESENT|DIRTY|YOUNG)

Таким образом, гостевой физический адрес будет 0x4387c1138 (добавив младшие 12 бит, взяв их из виртуального адреса выше). Мы знаем (из чтения карты памяти гостевой виртуальной машины), что она начинает просмотр физических адресов с 0x40000000. Мы также знаем, что файл аварийного дампа начинает свой дамп памяти со смещения 0x234. Мы можем выяснить это, запустив objdump:

# objdump -x fedora30-arm-1.core

Итак, для каждого гостевого виртуального адреса мы можем вычислить смещение в файле дампа, используя следующий расчет (в окне терминала Python):

>>> print hex(guest_physical_address–0x40000000+0x234)

Таким образом, значение, которое мы хотели прочитать в исходной загрузке, было:

crash> rd -f 0x3f87c136c
3f87c136c: 00000209

Гость должен был загрузить 0x209 в r2, но вместо этого он вызвал прерывание работы гипервизора. На этом этапе я начал смотреть на таблицы страниц, используемые гостем, чтобы увидеть, может ли быть какая-то проблема защиты, которая означала бы, что я могу прочитать дамп памяти, но гость не мог. Возможно, это могло быть так же просто, как странная проблема с защитой доступа, или неправильная последовательность инструкций по аннулированию в гостевой системе не смогла должным образом аннулировать обновление таблицы страниц, так что просмотрщик гостевых страниц имел несогласованное представление.

Я выполнил различные переводы vtop для успешных и неудачных загрузок, близких к прерванной, и просмотрел страницы. Ничто не выглядело слишком нестандартным. Моя надежная копия 7500-страничного Справочного руководства по архитектуре Arm пригодилась не раз, особенно в разделе G5.5 Формат таблицы преобразования длинных дескрипторов VMSAv8–32, в котором описаны таблицы подкачки, используемые при выполнении 32-битного AArch32. состояние, когда действует адресация LPAE.

Затем я начал задаваться вопросом, не виноваты ли трансляции памяти самого гипервизора. В современных средах виртуализации гостевые виртуальные машины имеют собственное «физическое» представление о памяти, но оно совершенно не совпадает с представлением хоста о своей реальной базовой физической памяти. Вместо этого гость переводит свои виртуальные адреса в то, что он считает «физическими» адресами, но на самом деле это IPA (промежуточные физические адреса). Они переводятся второй раз гипервизором с использованием собственных таблиц страниц.

Подумайте об этом так: «физическая» память гостя - это просто отображенная область в виртуальной памяти процесса хоста (qemu-kvm), который поддерживает экземпляр виртуальной машины KVM. Имея это в виду, мы можем начать исследовать представление гипервизора хоста о памяти и его вторичных таблицах трансляции.

Как я упоминал ранее, всякий раз, когда запущен гость KVM, он поддерживается процессом «qemu-kvm», запущенным на хосте. Взглянув на запущенные процессы на хосте («ps fax»), мы можем увидеть, какой из них поддерживает нашу виртуальную машину «pause» d, используя команду ps и простую команду grep:

$ ps faxwww | grep fedora30-arm-1

6458 ? Sl 180:45 /usr/libexec/qemu-kvm -name guest=fedora30-arm-1,…

На самом деле результат, конечно, не такой краткий. Затем мы можем взглянуть на отображаемую виртуальную память для этого процесса qemu-kvm. «RAM» виртуальной машины KVM реализована в виде одной или нескольких областей виртуальной памяти «ramblock», выделенных qemu с использованием обычного распределения «malloc». Мы можем увидеть, какие регионы отображаются, используя интерфейс / proc / PID / maps:

$ cat /proc/6458/maps

Просматривая выходные данные, мы видим большое сопоставление с размером 0xfffba7e00000, равным 16 ГБ (или размеру физической памяти виртуальной машины):

fffba7e00000-ffffa7e00000 rw-p 00000000 00:00 0

Таким образом, мы также можем читать гостевую физическую память, обращаясь к соответствующим смещениям в области рамблоков процесса qemu-kvm. Например, предыдущий гостевой физический адрес 0x4387c1138 может быть преобразован в отображение виртуальной памяти, используемое гипервизором до его преобразования stage2:

>>> print hex(0x4387c1138–0x40000000+0xfffba7e00000)

Мы можем запустить второй («живой») процесс «crash» на главном гипервизоре. Если установлены соответствующие RPM-пакеты debuginfo, это очень просто:

# crash

Установив для текущей задачи процесс «пауза» d qemu-kvm (с «set 6458»), мы можем начать исследовать состояние процесса с точки зрения хоста. Конечно, мы могли бы сделать то же самое с GDB, но нам понадобятся некоторые из более продвинутых функций через несколько минут. Примером этого является возможность чтения (с «rd») гостевой памяти с точки зрения гипервизора:

crash> rd 0xffffa05c1138

Мы можем использовать ту же команду «vtop» для использования таблиц страниц хоста, чтобы преобразовать этот виртуальный адрес хоста в физический адрес на стороне хоста. Это покажет нам записи таблицы страниц, которые хост использует для выполнения второго преобразования адреса в реальный физический адрес на хосте.

На этом этапе было бы полезно больше узнать о взглядах KVM на мир. Процесс qemu-kvm действительно предоставляет «монитор», и вы можете поговорить с ним, используя «virsh» в командной строке, но интерфейс довольно примитивен и не предоставляет никакой информации, которую я хотел. Однако иметь такой монитор - полезная вещь, и возможности его постоянно расширяются. Также возможно, что я упустил способ, которым «должны это делать» :)

Так или иначе. Мы можем собрать еще больше информации, просмотрев внутреннее состояние гипервизора KVM. Для этого мы должны сначала напомнить себе, что KVM использует относительно простой интерфейс управления для связи между qemu-kvm (процесс, который выполняется на хосте) и модулем ядра «kvm», который управляет фактическим состоянием виртуальной машины. API определен в документации ядра, в которой говорится, что мы выполняем ioctl на /dev/kvm.

Таким образом, чтобы управлять работающей виртуальной машиной KVM, процесс qemu-kvm должен иметь активный дескриптор файла, для которого он создает ioctls. Мы можем найти это, используя команду «files» в случае сбоя:

crash> files

Вывод включает все файлы, которые в настоящее время открыты хост-процессом qemu-kvm. Среди них этот файл, который он использует для отправки запросов в модуль ядра KVM для запуска аппаратной виртуализации и для других целей:

21 ffffeb0db06abf00 ffffeb0de549b6d0 ffffeb0de076b880 UNKN kvm-vcpu:0

Мы можем использовать команду «struct» в случае сбоя, чтобы пройти дальше к этому указателю открытого файла, который qemu-kvm использует для взаимодействия со своим гипервизором:

crash> struct file ffffeb0db06abf00

<lots of other output here>

private_data = 0xffffeb0db06ef160,

Чтение исходного кода KVM, в частности kvm_arch_vcpu_ioctl (virt/kvm/arm/arm.c), говорит нам, что KVM хранит указатель на «struct kvm_vcpu», содержащий состояние VCPU, в указателе private_data этого открытого файла. Мы можем найти указатель private_data в приведенном выше выводе и разыменовать его:

struct kvm_vcpu -x 0xffffeb0db06ef160

Мы можем использовать это для восстановления и получения любого состояния, которое мы хотим от экземпляра KVM, читая структуру, обращая особое внимание на подструктуру «arch» и такие подструктуры, как «fault». Он содержит копии различных регистров, используемых гипервизором для определения причины сбоя. Некоторые из этих регистров особенно интересны:

esr_el2 = 0x92000086,
far_el2 = 0xb69b4138,
hpfar_el2 = 0x3deb90,

Первый - это Регистр синдромов исключений (ESR_EL2). Он сообщает нам информацию о том, почему мы прервали работу с гипервизором (подробнее об этом позже). Второй регистр ошибочного адреса (FAR_EL2) сообщает нам, какой гостевой адрес ведет к ловушке. Неудивительно, что это оскорбительный адрес, который мы рассматривали ранее. Наконец, у нас есть регистр сбойных адресов гипервизора (IPA) (HPFAR_EL2). Он содержит адрес в гостевой физической памяти, который вызвал ловушку.

Я потратил намного больше часов, чем можно было бы предположить из вышеизложенного, вручную, проходя через различные части гостевой и хост-памяти. Одна из вещей, которые я сделал, - это просмотрел таблицы страниц гостя для текущего процесса внутри гостя во время сбоя. Я сделал много этого вручную, взяв базовый регистр таблицы страниц для гостевого пользовательского пространства, известный как базовый регистр таблицы трансляции (TTBR0_EL1), и выполнил около дюжины операций, необходимых для перехода к отдельным PTE (записям таблицы страниц), которые описать гостевой физический адрес для данного виртуального.

В конце концов я понял, что когда мы получаем эти ловушки в гипервизор, регистр HPFAR_EL2 всегда содержит адрес, близкий к гостевому физическому адресу последнего уровня листа PTE, необходимого для выполнения трансляции из гостевого FAR (ошибочный виртуальный адрес ) в гостевой IPA (гостевой физический адрес). Единственная разница заключалась в том, что значение в регистре HPFAR_EL2 было усечено, пропуская старшие биты гостевого IPA. 0x43deb9000 стал 0x3deb9000, опустив цифру «4».

Наконец, я предположил, что происходило то, что гости, выполняющие обход таблицы страниц на этапе 1, попали в состояние сбоя (например, страница хоста, поддерживающая PTE, нуждалась в обновлении бита доступа, которое управляется программным обеспечением в Armv8.0) во время обхода, который требовал ловушка в гипервизоре, чтобы управлять обновлением таблицы страниц (например, обновлением бит доступа). Только в этом случае были случаи, когда значение, содержащееся в HPFAR_EL2, могло быть усечено, пропуская верхние биты адреса, добавленные Arm LPAE.

Мое решение? В случае, если мы знаем, что получили прерывание гипервизора из-за перевода на этапе 1 (о котором нам полезно сообщить в ESR_EL2), мы можем восстановить значение, которое должно было быть в HPFAR_EL2, вручную пройдя таблицы гостевых страниц и сравнив листовые узлы. Если лист совпадает с ошибочным адресом за вычетом верхних битов, вместо этого введите правильный адрес и позвольте гостю продолжить обход страницы, на этот раз успешно.

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

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