Данная статья служит одной простой цели: помочь человеку, который вдруг решил разработать свою операционную систему (в частности, ядро) для архитектуры x86, выйти на тот этап, где он сможет просто добавлять свой функционал, не беспокоясь о сборке, запуске и прочих слабо относящихся к самой разработке деталей. В интернете и на хабре в частности уже есть материалы по данной теме, но довольно трудно написать хотя бы “Hello world”-ядро, не открывая десятков вкладок, что я и попытаюсь исправить. Примеры кода будут по большей части на языке C, но многие другие языки тоже можно адаптировать для OSDev. Давно желавшим и только что осознавшим желание разработать свою операционную систему с нуля — добро пожаловать под кат.

Теория


Чтобы понять, в чем заключается роль разработчика ОС, представим, что происходит после нажатия на кнопку включения ПК.

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

Ядро полностью подготавливает оборудование и запускает первые пользовательские процессы, обеспечивая им и их потомкам runtime-поддержку.

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

Инструментарий


Теоретически, разработку можно вести на любой ОС, но большинство инструментов рассчитаны на UNIX-подобные системы, и хотя бы собрать их на Windows уже будет страданием. Более того, поскольку WSL не поддерживает модули ядра, смонтировать образ диска не получится, и придется настраивать коммуникацию между WSL и Windows. На этом этапе уже становится проще поставить виртуальную машину с Linux. В статье будут предоставлены инструкции для Linux и macOS.

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

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

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

Со сборкой же всё не так просто: понадобится кросс-компилятор под x86. Зачем кросс-компилятор, если собирать под ту же архитектуру? Дело в том, что стандартный компилятор генерирует код, опирающийся на ту же ОС, на которой он запущен, или т.н. hosted-код. Hosted-код использует системные вызовы, взаимодействует с другими процессами, но привязан к операционной системе. Freestanding-код существует сам по себе и для запуска требует только само оборудование. Ядро ОС относится к freestanding, а программы, им запускаемые — к hosted. Кросс-компилятору достаточно соответствующего флага, и будет сгенерирован freestanding-код.

Подготовка


Сборка инструментов


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

$ export TARGET=i686-elf
$ export PREFIX=<путь к кросс-компилятору>

$TARGET — система, под которую будет собирать полученный компилятор. Обычно она называется наподобие i686-linux-gnu, но здесь результат запускается без ОС, поэтому указывается просто формат исполняемого файла. Почему i686, а не i386? Просто архитектуре 80386 уже, кхм, много лет, и с тех пор многое изменилось; в частности, появились кэши, многоядерные и многопроцессорные системы, встроенные FPU, “большие” атомарные инструкции вроде CMPXCHG, так что, собирая под i386, можно сильно потерять в быстродействии и немного приобрести в поддержке старых компьютеров.

$PREFIX — то, куда будут установлены инструменты. Обычно используются пути вроде /usr/i686-elf, /usr/local/i686-elf и подобные, но можно установить и в произвольную папку. Этот каталог также называется sysroot, поскольку он будет представлять собой корневой каталог для кросс-компилятора и утилит. Говоря точнее, это не полноправный путь, а именно префикс к пути; таким образом, для установки в корень $PREFIX будет представлять из себя пустую строку, а не /. На время сборки GCC потребуется добавить в PATH путь $PREFIX/bin.

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

Binutils


Загружаем и распаковываем последнюю версию с официального FTP. Осторожно: minor-версии давно перешагнули за 10, вследствие чего сортировка по алфавиту сломалась, для поиска актуальной версии можно использовать сортировку по дате последнего изменения. На момент написания статьи актуальной версией Binutils является 2.29.

Binutils не поддерживает сборку в каталоге с исходным кодом, поэтому создаем каталог рядом с распакованным кодом и заходим в него. Далее обычная сборка из исходников:

$ ../binutils-2.29/configure --target=$TARGET --prefix="$PREFIX" --with-sysroot --disable-nls --disable-werror

Подробнее о параметрах:

--with-sysroot — использовать sysroot;
--disable-nls — выключить поддержку родного языка. OSDev-сообщество не так велико, чтобы на какую-нибудь непонятную ошибку сборки обязательно нашёлся человек, говорящий на языке того, у кого она возникла;
--disable-werror — компилятор при сборке Binutils выдает предупреждения, а с -Werror это приводит к остановке сборки.

$ make
$ make install

GCC


Так же загружаем, распаковываем и создаем каталог для сборки. Процесс сборки немного отличается. Понадобятся библиотеки GMP, MPFR и MPC. Их можно установить из стандартных репозиториев многих пакетных менеджеров, а можно запустить из каталога с исходным кодом скрипт contrib/download_prerequisites, который их скачает и использует при сборке. Конфигурацию выполняем так:

$ ../gcc-7.2.0/configure --target=$TARGET --prefix="$PREFIX" --disable-nls --enable-languages=c,c++ --without-headers

--without-nls — то же самое, что и для Binutils;
--without-headers — не предполагать, что на целевой системе будет стандартная библиотека (этим, собственно, и отличается необходимый нам компилятор от стандартного);
--enable-languages=c,c++ — собрать компиляторы только для выбранных языков. Опционально, но существенно ускоряет сборку.

В условиях отсутствия целевой ОС обычный make && make install не подойдет, поскольку некоторые компоненты GCC ориентируются на готовую операционную систему, поэтому собираем и устанавливаем только необходимое:

$ make all-gcc all-target-libgcc
$ make install-gcc install-target-libgcc

libgcc — библиотека, в которой содержатся внутренние функции компилятора. Компилятор вправе вызывать их для некоторых вычислений, например, для 64-битного деления на 32-битной платформе.

GRUB


На большинстве дистрибутивов Linux эту секцию можно пропустить, поскольку на них уже установлены подходящие утилиты для работы с GRUB. Для других же ОС его потребуется загрузить и собрать. Также понадобится маленькая утилита objconv:

$ git clone https://github.com/vertis/objconv.git
$ cd objconv
$ g++ -o objconv -O2 src/*.cpp

На время сборки GRUB потребуется добавить в PATH только что собранный objconv
и кросс-инструменты (i686-elf-*).

$ cd ../grub
$ ./autogen.sh
$ mkdir ../build-grub
$ cd ../build-grub
$ ../grub-2.02/configure --disable-werror TARGET_CC=$TARGET-gcc TARGET_OBJCOPY=$TARGET-objcopy TARGET_STRIP=$TARGET-strip TARGET_NM=$TARGET-nm TARGET_RANLIB=$TARGET-ranlib --target=$TARGET
$ make
$ make install

GDB (для macOS)


Стандартная версия GDB не знает об ELF-файлах, поэтому при использовании GDB его потребуется пересобрать с их поддержкой. Загрузка, распаковка, сборка:

$ mkdir build-gdb
$ cd build-gdb
$ ../gdb-8.0.1/configure --target=$TARGET --prefix="$PREFIX"
$ make
$ make install

Образ диска


Процесс создания такового в разных ОС происходит по-своему, поэтому здесь я приведу отдельные инструкции.

Для Linux (из командной строки)
Создаем пустой файл:

$ dd if=/dev/zero of=disk.img bs=1048576 count=<размер в МБ>

Создаем таблицу разделов:

$ fdisk disk.img

    Welcome to fdisk (util-linux 2.27.1).
    Changes will remain in memory only, until you decide to write them
    Be careful before using the write command.

    Device does not contain a recognized partition table.
    Created a new DOS disklabel with disk identifier 0x########.

    Command (m for help): n
    Partition type
    p   primary (0 primary, 0 extended, 4 free)
    e   extended (container for logical partitions)
    Select (default p): <Enter>

    Using default response p.
    Partition number (1-4, default 1): <Enter>
    First sector (2048-N, default 2048): <Enter>
    Last sector, +sectors or +size{K,M,G,T,P} (2048-N, default N): <Enter>

    Created a new partition 1 of type 'Linux' and of size N MiB.

    Command (m for help): t
    Selected partition 1
    Partition type (type L to list all types): 0B
    Changed type of partition 'Linux' to 'W95 FAT32'.
    Command (m for help): a
    Selected partition 1
    The bootable flag on partition 1 is enabled now.

    Command (m for help): w
    The partition table has been altered.
    Syncing disks.

Создаём файловую систему:

$ losetup disk.img --show -f -o 1048576 # выведет <устройство>
$ mkfs.fat -F 32 <устройство>
$ mount <device> <точка монтирования>

В дальнейшем можно будет монтировать посредством

$ mount -o loop,offset=1048576 disk.img <точка монтирования>

Устанавливаем загрузчик (здесь GRUB):
$ grub-install --modules="part_msdos biosdisk fat multiboot configfile" --root-directory="<точка монтирования>" ./disk.img
$ sync


Для macOS (из командной строки)
Создаем пустой файл:

$ dd if=/dev/zero of=disk.img bs=1048576 count=<размер в МБ>

Таблица разделов:

$ fdisk -e disk.img
Would you like to initialize the partition table? [y] y
fdisk:*1> edit 1
Partition id ('0' to disable) [0 - FF]: [0] (? for help) 0B
Do you wish to edit in CHS mode? [n] n
Partition offset [0 - n]: [63] 2047
Partition size [1 - n]: [n] <Enter>
fdisk:*1> write
fdisk: 1> quit

Разделяем таблицу разделов и единственный раздел:

$ dd if=disk.img of=mbr.img bs=512 count=2047
$ dd if=disk.img of=fs.img bs=512 skip=2047

Подключаем раздел как диск:

$ hdiutil attach -nomount fs.img # выведет <устройство>

Создаем ФС, здесь FAT32:

$ newfs_msdos -F 32 <устройство>

Отключаем:

$ hdiutil detach <устройство>

“Склеиваем” MBR и ФС обратно:

$ cat mbr.img fs.img > disk.img

Подключаем и запоминаем точку монтирования (обычно “/Volumes/NO NAME”):

$ hdiutil attach disk.img

Устанавливаем загрузчик:

$ /usr/local/sbin/grub-install --modules="part_msdos biosdisk fat multiboot configfile" --root-directory="<точка монтирования>" ./disk.img


Образ диска после этого спокойно подключается встроенными средствами системы. Можно на собственное усмотрение создать иерархию директорий и настроить загрузчик. Например, для GRUB можно создать такой grub.cfg в /boot/grub:

set default=0
set timeout=0
menuentry "BetterThanLinux" {
	multiboot /путь/к/ядру/ядро.elf
	boot
}

Настройка сборочной системы


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

Ассемблерные файлы собираем в объектные формата ELF (32 бита):

$ nasm -f elf -o file.o file.s

C-файлы собираем при помощи кросс-компилятора с флагом -ffreestanding:

$ i686-elf-gcc -c -ffreestanding -o file.o file.c

Для компоновки используем всё тот же кросс-компилятор, но указываем чуть больше информации:

$ i686-elf-gcc -T linker.ld -o file.elf -ffreestanding -nostdlib file1.o file2.o -lgcc

-ffreestanding — генерировать freestanding-код;
-nostdlib — не включать стандартную библиотеку, поскольку ее реализация является hosted-кодом и будет совершенно бесполезна;
-lgcc — подключаем описанную выше libgcc. Ее подключение всегда идет после остальных объектных файлов, иначе компоновщик будет жаловаться на неразрешенные ссылки;
-T — поскольку нужно где-то разместить заголовок Multiboot, обычная раскладка ELF-файла не подойдёт. Ее можно изменить при помощи скрипта компоновщика, который и задает этот флаг. Вот готовый его вариант:

/* Исполнение начнется с этой функции */
ENTRY(_start)

/* Как расположить секции в файле */
SECTIONS
{
	/* Ядра обычно загружаются по смещению 1Мб. Можно указать любое значение */
	. = 1M;

	/* Сначала заголовок Multiboot, чтобы его нашел загрузчик, а также исполняемый код */
	.text BLOCK(4K) : ALIGN(4K)
	{
		*(.multiboot)
		*(.text)
	}

	/* Данные (только чтение) */
	.rodata BLOCK(4K) : ALIGN(4K)
	{
		*(.rodata)
	}

	/* Данные (чтение и запись, проинициализированные) */
	.data BLOCK(4K) : ALIGN(4K)
	{
		*(.data)
	}

	/* Неинициализированная область (данные для чтения и записи, стек) */
	.bss BLOCK(4K) : ALIGN(4K)
	{
		*(COMMON)
		*(.bss)
	}

	/* Сюда можно добавлять все, что только можно */
}

Минимальное ядро


Получаем управление


Получаем управление от загрузчика в небольшом ассемблерном файле:

FLAGS equ  0 ; пока никакие флаги не нужны
MAGIC equ  0x1BADB002        ; 'magic number' lets bootloader find the header
CHECKSUM equ -(MAGIC + FLAGS)   ; checksum of above, to prove we are multiboot
; Собственно заголовок
section .multiboot
align 4
	dd MAGIC
	dd FLAGS
	dd CHECKSUM

section .bss
align 16
stack_bottom:
resb 16384 ; 16 KiB
stack_top:

section .text
global _start:function (_start.end - _start)
_start:
	mov esp, stack_top ; настраиваем стек
	push ebx ; указатель на данные от загрузчика
	extern kernel_main
	call kernel_main
	cli ; если почему-то вышли из ядра, отключить прерывания (то, что может внезапно вернуть управление в ядро)
	.hang: hlt ; зависнуть
	jmp .hang ; если процессор пробудился, обратно зависнуть
	.end:

Proof of Work


Чтобы хоть как-то увидеть, что код действительно выполняется, можно вывести что-то на экран. Полноценный драйвер терминала — тема большая, но, вкратце, по адресу 0xB8000 располагается буфер на 2000 записей, каждая из которых состоит из атрибутов и символа. Белому тексту на черном фоне соответствует байт атрибутов 0x0F. Попробуем что-либо вывести при помощи заранее подготовленной строки:

#include <stddef.h>

void kernel_main(void* multiboot_structure) {
	const char str[] = "H\x0F""e\x0Fl\x0Fl\x0Fo\x0F \x0Fw\x0Fo\x0Fr\x0Fl\x0F""d\x0F";
	char* buf = (char*) 0xB8000;
	char c;
	for(size_t i = 0; c = str[i]; i++) {
		buf[i] = str[i];
	}
	while(1);
}

Запуск


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

Отладка


Для отладки в QEMU можно задать флаги -s -S. QEMU будет дожидаться отладчика и включит сетевую отладку. Также стоит заметить, что отладка не будет работать при использовании ускорителя, так что флаг --enable-kvm придется убрать, если он используется.

Bochs понадобится собрать с --enable-gdb-stub, а в конфиг включить строку наподобие gdbstub: enabled=1, port=1234, text_base=0, data_base=0, bss_base=0.
В GDB можно подключиться и запустить машину таким образом (kernel.elf — файл ядра):

(gdb) file kernel.elf
(gdb) target remote localhost:1234
(gdb) c

Все остальное работает так же, как и всегда — точки останова, чтение памяти и пр. Также можно включить отладчик в само ядро, что позволит производить отладку на реальной машине. Можно написать его самостоятельно, но отладка ошибки в отладчике принесет много-много радости. GNU распространяет почти готовые отладчики, требующие от ядра только несколько функций. Например, для i386. Впрочем, пока это делать рано, поскольку еще нет необходимых функций, таких как установка обработчика прерывания или получения/отправки данных через последовательный порт.

Заключение


На текущий момент до минимальной рабочей операционной системы остается настроить следующее:


Полезные ресурсы


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


  1. maisvendoo
    02.12.2017 13:42

    В свое время баловался этим, могу предложить свой цикл статей phantomexos.blogspot.com

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


    1. maisvendoo
      02.12.2017 13:50

      И да, я бы не рекомендовал крайне Джеймса Маллоя — его код содержит намеренные ошибки, а реализация многозадачности вообще сделана через одно место. В свое время намучился с этим туториалом. Лучше постить свои потуги на osdev.ru, там много толковых ребят, которые способны подсказать полезное.

      Вот например так я пришел к реализации многозадачности


      1. vodozhaba Автор
        02.12.2017 15:09

        Ой. Как раз хотел сделать об этом оговорку, но по пути на Хабр на месте фразы «не лишён изъянов» потерялась ссылка на список подобных ошибок. Исправил. Спасибо!


  1. Evgen52
    02.12.2017 15:31

    Большое спасибо за интересную статью! Было бы ещё очень здорово в таком же духе, но про x86_64.


  1. jcmvbkbc
    02.12.2017 21:17
    +1

    Вместо того чтобы руками собирать тулчейн можно взять github.com/crosstool-ng/crosstool-ng. Только непонятно, зачем собирать кросс-компилятор под x86 без libc, если всю ту же функциональность можно получить от хостового компилятора с опцией -ffreestanding.


    1. maisvendoo
      02.12.2017 21:44

      если всю ту же функциональность можно получить от хостового компилятора с опцией -ffreestanding.


      У меня аналогичный вопрос. Всё нормально собирается хостовым компилятором с соответствующими опциями.

      И ещё один вопрос — зачем NASM? Если есть as, вызываемый gcc автоматом, когда тот натыкается на ассемблерный исходник? Ну да, придется попотеть освоив AT&T-синтаксис, ну так это, во-первых несложно, а во вторых — синтаксис ассемблерных вставок и так AT&T. Зачем нужен плесневелый nasm, ради только intel-нотации?

      Еще замечание, касательно qemu. Обязательно надо упомянуть о том, что нельзя категорически использовать опцию -enable-kvm. С ней отладка не будет работать


      1. vodozhaba Автор
        02.12.2017 22:24

        В хостовом компиляторе есть хостовая libgcc, которая может попытаться обратиться к не менее хостовому ядру, к тому же появляются лишние флаги вроде -nostdinc, -nostdlib, -m32, которые легко забыть.

        По поводу NASM — это просто один из инструментов, применение которого не вызывает особых проблем. Можно было использовать GAS, FASM или TASM. А можно было NASM. Что использовать Вам — дело Ваше.

        Про -enable-kvm упомяну.


        1. a1ien_n3t
          02.12.2017 23:50

          В хостовом компиляторе есть хостовая libgcc, которая может попытаться обратиться к не менее хостовому ядру, к тому же появляются лишние флаги вроде -nostdinc, -nostdlib, -m32, которые легко забыть.

          Мне кажется, что когда настроить флаги стандартного компилятора проще и рациональнее чем собирать компилятор самому.


        1. jcmvbkbc
          02.12.2017 23:53

          В хостовом компиляторе есть хостовая libgcc, которая может попытаться обратиться к не менее хостовому ядру

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


          1. vodozhaba Автор
            03.12.2017 00:12

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


            1. jcmvbkbc
              03.12.2017 00:17

              тем не менее, для этого нужна хостовая libc

              Вовсе нет, никто не запрещает вам определить abort, memcpy, memmove, memset и memcmp в своём коде, по мере необходимости.


            1. maisvendoo
              03.12.2017 09:38

              Всё решается двумя ключами

              -nostdlib — не используем стандартных библиотек
              -nostdinc — не используем стандартных заголовков

              нет нужды собирать компилятор отдельно под задачу сборки ядра своей ос

              Ваш пример пустого ядра я собирал таким makefile

              #------------------------------------------------------
              #
              # Правила сборки кода ядра
              #
              #------------------------------------------------------

              # Исходные объектные модули
              SOURCES=init.o main.o
              # Флаги компилятора языка C
              CFLAGS=-nostdlib -nostdinc -fno-builtin -fno-stack-protector -m32 -g
              # Флаги компоновщика
              LDFLAGS=-T link.ld -m elf_i386
              # Флаги ассемблера
              ASFLAGS=--32
              # Правило сборки
              all: $(SOURCES) link
              # Правило очистки
              clean:
              -rm *.o kernel
              # Правило компоновки
              link:
              ld $(LDFLAGS) -o kernel $(SOURCES)


              и все работало


              1. vodozhaba Автор
                03.12.2017 11:18

                С -nostdinc не будет весьма полезных заголовков, которые не требуют libc, таких как stddef.h и stdarg.h. Что легче, переписать stdarg.h или собрать компилятор?


                1. maisvendoo
                  03.12.2017 11:48
                  +1

                  Переписать ашник. И вот почему: нельзя просто так взять и пересобрать компилятор. Он ещё должен тесты пройти, почитайте книгу LFS или вот этот материал и станет понятно, что гарантированно работоспособный компилятор и комплект binutils собирается не с наскока, а вдумчиво и долго с обязательным прохождением тестов. На моей машине сборка gcc с тестами заняла 2,5 часа, например + примерно столько же времени на чтение и подготовку


                  1. jcmvbkbc
                    03.12.2017 21:10

                    Переписать ашник.

                    Не, вот это точно неправильно. Скопировать стандартный в отдельный сисрут — может быть. Переписать самому — точно нет.

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

                    Порекламирую crosstool-NG ещё раз. Бэкпортированные патчи из мэйнлайнов компонентов тулчейна и коллективное тестирование идут в комплекте.


                1. a1ien_n3t
                  03.12.2017 12:03

                  Правильно переписать. Пересобрать компилятор не правильно. Если вы пишите туториал. То ненужно в них советовать неправильные подходы.


              1. jcmvbkbc
                03.12.2017 21:03

                Этот makefile будет неустойчиво работать с make -j, потому что link не зависит от $(SOURCES) и перечислен наравне с ними в пререквизитах для all. Правильнее было бы так:

                all: kernel
                kernel: $(SOURCES)


  1. neit_kas
    02.12.2017 21:49

    Спасибо. Довольно редки здесь статейки на такую тематику. Если Вы вдруг решите делать что-то вроде цикла статей, хотелось бы чего-нибудь и по поводу UEFI. В частности интересна тема написания загрузчиков (не ради самих загрузчиков, а ради того, чтобы знать, как оно робит), да и в целом как ОС используют UEFI.


    1. VioletGiraffe
      03.12.2017 01:24

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


    1. CodeRush
      03.12.2017 10:06

      Там почти не о чем писать, к сожалению, потому что любое EFI-приложение может выступать в качестве загрузчика, а написание простейшего EFI-приложения на Хабре уже не раз освещалось, и в тех примерах достаточно заменить в inf-файле DXE_DRIVER на UEFI_APPLICATION, и у вас получится нужный вам загрузчик.


  1. Hixon10
    03.12.2017 00:15

    Спасибо за статью! Крайне приятно видеть на хабре не статью о том, как надо верстать анимацию в андроид приложение на джаваскрипте, а о чём-то действительно серьезном, фундаментальном, так сказать.