На чем только уже не запускали Doom. Мы же будем запускать Linux. Да не где-нибудь, а на Python. Да-да, прямо внутри него, где в качестве среды выполнения будет выступать интерпретатор Python. Ну как... Не будем пытаться переписать ядро и другие части Linux на этот язык, а попробуем написать (точнее портировать) виртуальную машину на Python и уже в ней запускать ОС.

Начнем с позитивного, а именно с плюсов такого решения.

– Можно будет запустить Linux вообще везде, где есть интерпретатор Python.

– Можно использовать как бенчмарк конкретного интерпретатора.

– Веселимся, пока все это пишем и отлаживаем. Пожалуй, это самый главный плюс.

Минусы: будет работать оооочень не быстро (ну логично же).

Немного технических подробностей. Внезапно поработаем с нейросетями, посмотрим, что получится и насколько быстро будет работать.

Статья не претендует на какие-то сильно новые знания, и все это было сделано Just for Fun и ради эксперимента.

Приступаем!

На старт

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

Итак, эмулятор RISC-V, который умеет запускать образ Linux, у нас имеется. Первый — это простенькая виртуальная машина с поддержкой 32х-битного RISC-V процессора без MMU и прочих аппаратных «излишеств». Второй — это собранный для этой архитектуры такой же простенький Linux без поддержки MMU. Осталось дело за малым: портировать первый с Си на Python.

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

	elif (ir >> 12) & 0x7 == 1:
    	if rs1 != rs2:
        	pc = immm4
	elif (ir >> 12) & 0x7 == 4:
    	if rs1 < rs2:
        	pc = immm4
	elif (ir >> 12) & 0x7 == 5:
    	if rs1 >= rs2:
        	pc = immm4
	elif (ir >> 12) & 0x7 == 6:
	...

Ну да ладно, сами виноваты, что не пишем код сами. Главное, чтобы было корректно. А оптимизацию и рефакторинг оставим на потом.

Итак, у нас имеется переведенный с Си на Python код. Уже неплохо.

Внимание

С первой попытки ничего, конечно же, не заработало. «Переводчик» удалил почему-то части кода, которыми не знает, как пользоваться (а скармливал я ему по одному файлу). Быстренько допиливаем и запускаем.

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

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

– внутренними регистрами виртуального процессора;

– памятью виртуального процессора.

Вроде больше ничего. Плюс, конечно же, состояние периферии, но ее не так много и на начальном этапе загрузки она не используется.

Поскольку у нас есть оригинальная виртуальная машина на Си, которая точно так же работает, давайте логировать состояние системы там и одновременно в нашем эмуляторе на Python. Затем сравним эти логи и посмотрим, на каком шаге пошли отличия. Если такие отличия есть, нужно уже отлаживать конкретно этот шаг эмулятора.

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

Быстро дописываем код для логирования и там и там, ну и далее по циклу: запускаем-логируем-смотрим-дорабатываем и опять запускаем-логируем-смотрим-дорабатываем, пока все не запустится. Благо, в RISC-V команд процессора не так много и, соответственно, ошибок тоже не так много можно допустить.

Самые частые ошибки, которые сделал AI-переводчик следующие:

– Ошибки переполнения. Так как Int в Python условно безграничный, он может хранить бо'льшие значения, чем RISC-V (помним, что последний у нас 32х-битный). Поэтому нужно искусственно расставлять ограничения на длину числа после операций с регистрами.

– Отступы не всегда корректны в функциях, а в Python-e это важно. Просто молча правим.

– Некорректное поведение «переводчика». Например, это: 

uint32_t * dtb = (uint32_t*)(ram_image + dtb_ptr);
if( dtb[0x13c/4] == 0x00c0ff03 )

он преобразует в это:

  dtb = struct.unpack('<I', ram_image[dtb_ptr:dtb_ptr+4])[0]
  if dtb == 0x00c0ff03:

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

Марш

Через некоторое время отладки и запусков видим:

Ура, оно живое! На удивление, наш Linux достаточно быстро запускается, что не может не радовать.

Надо бы протестировать, как быстро такое решение работает в более конкретных цифрах. В образе Linux заботливо оставлен Coremark для тестирования производительности. Запустим его пару раз и сравним скорость работы Linux в виртуальной машине на Python и на Си. А еще, ради интереса, протестируем пару нестандартных Python интерпретаторов/компиляторов.

Итого, сравнительная табличка производительности для coremark:

Компилятор/интерпретатор

Результат в попугаях в Iterations/Sec

Visual C++ 14.0

798.258345

CPython 3.13

2.132803

CPython 3.12

2.418575

PyPy 3.10

4.362685

CPython 3.12+Nutika

3.552819

Не густо. Python не быстрый. Этого стоило ожидать. Улучшать и оптимизировать код нашей Python-виртуальной машины можно и, соответственно, улучшить результат, но примем результат, какой он есть.

Финиш

Эта статья получилась из разряда «что получится, если…». AI-переводчик сэкономил немало времени на разработку этого всего, но и добавил другой работы по исправлению. Но теперь всё позади, можем смело запускать Linux на Python. Пусть и с ограничениями по скорости, да и вообще, у нас виртуальная машина и Linux без поддержки MMU, но можем же.

В самом лучшем случае (при помощи JIT компилятора PyPy) такое решение почти в 183 раза хуже по производительности нативной виртуальной машины. И промолчим, что вообще-то можно нативно скомпилировать и запустить Linux на реальном железе и будет еще быстрее и разница будет еще существеннее. Но зато, наше решение полностью портативное и может запускаться в любой (или почти любой) Python-среде исполнения. А еще получилось крайне компактно — всего порядка 900 строк рабочего кода. Все исходники и инструкция по запуску, конечно же, есть на github.

Комментарии (3)


  1. Arsenii14
    05.11.2024 13:15

    Офигенная статья


  1. Chupaka
    05.11.2024 13:15

    Ошибку, думаю, показывать не надо

    Всё же неплохо бы...


  1. checkpoint
    05.11.2024 13:15

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

    На сколько сложно добавить поддержку MMU ?