Pull to refresh

Сказ об одной ошибке, так и не попавшей в релиз ядра Linux

Reading time3 min
Views28K
Совсем недавно вышло исправление, устраняющее полное зависание 32-битного ядра Linux при загрузке на процессорах Intel. Здесь небольшая история о том, откуда появилась ошибка и какие проводились исследования по поиску причин её возникновения.

Начну с небольшого экскурса в процесс загрузки. Большинство из вас уже знает, что в загрузке ОС существует множество фаз, две из которых, к примеру, загрузка загрузчика (простите за тавтологию) и загрузка им ядра ОС, в нашем случае Linux. Давайте чуть копнём в сторону, что же происходит в момент после передачи управления загрузчиком ядру Linux.

Условно ядро Linux можно разделить на две части по исполнению — загрузочная и исполняемая. После получения управления ядром оно исполняет загрузочную часть, на которую приходится декомпрессия и расположение ядра в физической памяти системы. Затем происходит минимальная настройка менеджера памяти, детектирования типа процессора и его флагов и т.п. После осуществления этих шагов передаётся управление в код, где уже непосредственно начинает работать непривязанная к архитектуре часть ядра (строго говоря это не совсем так, но здесь мы подчеркиваем переход от ассемблерного кода к Си коду). Более подробно процесс описан в [1].

Теперь вспомним ещё такой факт, что в современных процессорах используется т.н. «микрокод», конфигурирующий исполнение некоторых инструкций процессора. Также он позволяет устранять некоторые недочёты железа без перевыпуска кристалла.

Естественным желанием разработчиков ядра любой ОС является возможность наложить исправления как можно раньше в процессе загрузки. Ранее, в Linux, этим вопросом занимались специальные демоны в пространстве пользователя, загружающиеся на довольно позднем этапе.

Несколько лет назад Fenghua Yu предложил (см. [2]) класть файл микрокода в начальный образ RAM-диска (initrd) и использовать его на ранних стадиях. Изменение сильно улучшило ситуацию, но остались всё ещё недостатки, в частности необходимость начального образа диска и невозможность держать микрокод для разных версий процессоров, так как имя файла фиксированное.

Совсем недавно Borislav Petkov решил исправить первый из них, опубликовав изменение [3].

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

Эффектом сего поведения стало вот что. Рассмотрим причинную функцию load_builtin_intel_microcode(), которая исполняется на раннем этапе.

static bool __init load_builtin_intel_microcode(struct cpio_data *cp)
{
        unsigned int eax = 0x00000001, ebx, ecx = 0, edx;
        unsigned int family, model, stepping;
        char name[30];

        native_cpuid(&eax, &ebx, &ecx, &edx);

        family   = __x86_family(eax);
        model    = x86_model(eax);
        stepping = eax & 0xf;

        sprintf(name, "intel-ucode/%02x-%02x-%02x", family, model, stepping);

        return get_builtin_firmware(cp, name);
}

Обратите внимание на вызов внутриядерной библиотечной функции sprintf(). Именно её вызов в независимости от параметров (при условии их корректности) рушит систему.

Что же там происходит? Мой коллега, MIka Westerberg, предположил, что причина именно в столь раннем вызове кода, когда на самом деле функции вызываются по их физическим адресам, а не виртуальным. Пока не настроено MMU и не проинициализирован менеджер памяти, виртуальные адреса не работают, поэтому для выполнения необходимо соответствие между виртуальными и физическими адресами 1-в-1, чего не наблюдается для части функционала. (Кстати, если попытаться вызвать strcpy(), то результат будет таким же.)

Невдалеке маячит merge window (о нём я немного рассказывал ранее в [4]), и Borislav решил пока что отключить своё изменение для 32-битных ядер, выслав обновление [5].

Мораль басни такова, что загрузка ОС — весьма тонкий процесс, требующий довольно глубоких знаний архитектуры, чтобы понимать происходящее там.

[1] www.ibm.com/developerworks/library/l-linuxboot
[2] lwn.net/Articles/530346
[3] www.spinics.net/lists/linux-tip-commits/msg28000.html
[4] habrahabr.ru/post/253421
[5] permalink.gmane.org/gmane.linux.kernel/1969480

UPDATE.
Совершенно забыл дописать одно важное замечание. Множество разработчиков тестирует свой код не на реальных машинах, а в виртуальных, с помощью того же QEMU. Так вот там всё прекрасно работает.

В комментариях jcmvbkbc поделился своим анализом происходящего.
Tags:
Hubs:
+41
Comments46

Articles

Change theme settings