Известный гуру внутреннего устройства Windows и архитектуры ARM Алекс Ионеску опубликовал на ресурсе GitHub исходный текст базовой части гипервизора (инструмент виртуализации). Гипервизор содержит в себе множество замечательных свойств, включая, компактный размер, поддержку современных архитектур микропроцессоров, минимальное использование ассемблера в исходных текстах, а также комментарии по его использованию. При его разработке упор делался на максимальную компактность.



Опубликованный проект является настоящей находкой для системных программистов, а также исследователей по безопасности, которые интересуются низкоуровневыми функциями ОС. Гипервизор получил название SimpleVisor, поддерживает только современные 64-битные системы и был успешно протестирован на совместимость с такими системами как Windows 8.1 на микропроцессоре архитектуры Intel Haswell, а также Windows 10 на архитектуре Intel Sandy Bridge.

Not counting the exhaustive comments which explain every single line of code, and specific Windows-related or Intel-related idiosyncrasies, SimpleVisor clocks in at about 500 lines of C code, and 10 lines of x64 assembly code, all while containing the ability to run on every recent version of 64-bit Windows, and supporting dynamic load/unload at runtime.

Как видно из аннотации, исходный код SimpleVisor занял всего 500 строк кода на языке C и 10 строк 64-битного ассемблера. Сам проект был собран с использованием Visual Studio 2015 и эта среда также может быть использована для его сборки.

Тестирование SimpleVisor осуществлялось на следующих платформах.

  • Windows 8.1 на микропроцессоре Haswell (настольный ПК)
  • Windows 10 Redstone 1 на микропроцессоре Sandy Bridge (ноутбук Samsung 930)
  • Windows 10 Threshold 2 на микропроцессоре Skylake (планшет Surface Pro 4 Tablet)
  • Windows 10 Threshold 2 на микропроцессоре Skylape (ноутбук Dell Inspiron 11-3153 SGX)


Рис. Структура исходных текстов SimpleVisor.


Рис. Часть кода asm64 из файла shvx64.asm, который отвечает за тонкости работы с микропроцессором AMD64.

SimpleVisor представляет из себя 64-битный драйвер, предназначенный для запуска в 64-битных версиях Windows 8.1 и Windows 10. Для успешного запуска в системе, драйвер должен быть подписан цифровой подписью, например, с использованием т. н. тестового сертификата. Далее, в Windows следует включить соответствующий режим загрузки драйвера с такой подписью с помощью известной команды bcdedit.

bcdedit /set testsigning on

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

sc create simplevisor type= kernel binPath= "<PATH_TO_SIMPLEVISOR.SYS>"

Драйвер SimpleVisor поддерживает как загрузку так и выгрузку «на лету». Для этого можно использовать следующие команды.

net start simplevisor
net stop simplevisor

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


  1. Scratch
    19.03.2016 20:30
    +7

    И, после его запуска система становится виртуализированной или что происходит?


    1. zolti
      19.03.2016 22:17

      В систему встраивается гипервизор, как и любой другой, к примеру Hyper-V, обычно на Ring-1 уровня ядра.


      1. hardex
        19.03.2016 22:50
        +2

        Так он встраивает себя внизу под уже работающей ОС?
        Как проявляется вообще его присутствие?


        1. zolti
          19.03.2016 23:01
          +1

          Насколько видно из описания проекта — никак не проявляется, автор выложил сам гипервизор, показал как его устанавливать, а дальше дело за теми, кто захочет его использовать и в каких целях.


          1. hardex
            19.03.2016 23:40
            +2

            Автору стоило бы дополнить его хотя бы минимальным примером допиливания, хотя бы например подменой cpuid.


      1. Scratch
        19.03.2016 23:15
        +13

        т.е. ничего не происходит. Просто какой-то код работает на низком уровне и не падает. Отлично


        1. vanxant
          20.03.2016 00:32
          +1

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


          1. IbhSvenssen
            20.03.2016 11:40

            Или няшный вирус


            1. 1vanK
              20.03.2016 14:18

              Который нужно устанавливать


              1. vanxant
                20.03.2016 15:21

                … но если есть ключик, то можно хоть и с апдейтом драйверов пропихнуть, никто не заметит.
                Хотя по факту это ничего не меняет, производители дров и сейчас имеют полный доступ к системе.


  1. SerJook
    20.03.2016 15:55

    Объясните чо с этим делать теперь


    1. khim
      20.03.2016 16:30
      +3

      Это тот случай, когда говорят "если вы задаёте этот вопрос, то вам не нужен ответ".

      Это — просто полуфабрикат. Поверх него можно сделать кучу интересных вещей. Но сам по себе… Как уже сказали: Ничего не происходит. Просто какой-то код работает на низком уровне и не падает. Отлично. — это достаточно полное описание происходящего. Причём последнее слово там к месте: подобную штуку самому, с нуля, по докам — написать достаточно сложно.


  1. gonzzza
    20.03.2016 17:09
    +1

    А почему малое количество строк на ассемблере преподносится как преимущество? Я всегда считал что asm используется для максимальной производительности. Все уже совсем не так?


    1. Evengard
      20.03.2016 17:15
      +3

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


      1. gonzzza
        20.03.2016 17:21

        Спасибо за ликбез!


      1. Randl
        20.03.2016 17:30

        А зачем там вообще ассемблер тогда?


        1. khim
          20.03.2016 17:51
          +3

          Есть вещи, которые на языке высокого уровня (даже на C) просто не делаются. Добавлять в компилятор 10 новых сущностей ради того, чтобы отказаться от написания 10 строк кода на ассемблере никто не будет.


      1. khim
        20.03.2016 18:04
        +2

        к тому же сейчас компиляторы гораздо лучше оптимизируют, чем это сделал бы человек
        Увы, но это неправда. Такого — даже и близко нету.

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

        Однако при этом иногда смотришь на код, который они выдают — и просто офигеваешь: это же просто гениально! Откуда это? А это значит — ваша задача хорошо «легла» на какой-то паттерн, который часто встречается и потому кто-то из разработчиков компилятора придумал как это место особо хорошо сделать. Разработал соответствующий ассемблерный кусок и вшил в компилятор. А так как разработчиков компилятора много (тысячи если считать всех, сотни — если только тех, кому за разработку денег платят), то случается это довольно часто. Но достаточно чуть-чуть изменить исходный код — и всё: пиши пропало.

        На самом деле, конечно же, человек действует так же (особенно если не гнушаться тем чтобы посмотреть на то, как компилятор решает ту или иную проблему — очень полезно, особенно если вы мало работали с той или иной архитектурой), однако «сведение» задачи к известным паттернам человек делает намного, намного лучше.

        Ну и вторая проблема: человек, в общем, выдаёт порядка 30-50 строк отлаженного кода в день. Неважно — на каком языке. Если язык компактный (Haskell какой-нибудь) это может быть чуть меньше, если очень «расхлябанный» (Java или ассемблер) — чуть больше, но в общем, разница от минимума до максимума для одного и того же человека — раза 2-3, не больше.

        Однако одной строке на C соответствуют десятки строк ассемблера! То есть с гипервизором на C проще и быстрее работать. Ну и переносимость — хотя это не всегда важно.


        1. Randl
          21.03.2016 01:12

          Ну если задача одновременно "не легла" и является критичным местом, то переписать её на ассемблере — вполне реально. Писать же на ассемблере всё — оверкилл, куча времени и денег.

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


          1. khim
            21.03.2016 02:13

            Количество программистов способных решить небанальную задачу на асме лучше компилятора невелико.
            Как раз наоборот: количество программистов, неспособных решить небанальную задачу хуже компилятора невелико. И чем «небанальнее» задача — тем их меньше.

            Простейший пример:
            Пример на C
            $ cat test.c
            #define Old_O_DIRECTORY 00040000
            #define Old_O_NOFOLLOW  00100000
            #define Old_O_DIRECT    00200000
            #define Old_O_LARGEFILE 00400000
            
            #define O_DIRECTORY     00100000
            #define O_NOFOLLOW      00040000
            #define O_DIRECT        00400000
            #define O_LARGEFILE     00200000
            
            int bar(const char *pathname, int old_flags);
            
            int foo(const char *pathname, int old_flags) {
              const int kUnstableFlags =
                  Old_O_DIRECTORY | Old_O_NOFOLLOW | Old_O_DIRECT | Old_O_LARGEFILE;
            
              int new_flags = old_flags & ~kUnstableFlags;
            
              if (old_flags & Old_O_DIRECTORY) {
                new_flags |= O_DIRECTORY;
              }
              if (old_flags & Old_O_NOFOLLOW) {
                new_flags |= O_NOFOLLOW;
              }
              if (old_flags & Old_O_DIRECT) {
                new_flags |= O_DIRECT;
              }
              if (old_flags & Old_O_LARGEFILE) {
                new_flags |= O_LARGEFILE;
              }
            
              return bar(pathname, new_flags);
            }
            $  ./clang  -c -O3 -S test.c -o-
                    .text
                    .file   "test.c"
                    .globl  foo
                    .align  16, 0x90
                    .type   foo,@function
            foo:                                    # @foo
                    .cfi_startproc
            # BB#0:
                    movl    %esi, %eax
                    andl    $-245761, %eax          # imm = 0xFFFFFFFFFFFC3FFF
                    leal    (%rsi,%rsi), %ecx
                    movl    %ecx, %edx
                    andl    $32768, %edx            # imm = 0x8000
                    orl     %edx, %eax
                    shrl    %esi
                    movl    %esi, %edx
                    andl    $16384, %edx            # imm = 0x4000
                    orl     %eax, %edx
                    andl    $131072, %ecx           # imm = 0x20000
                    orl     %ecx, %edx
                    andl    $65536, %esi            # imm = 0x10000
                    orl     %edx, %esi
                    jmp     bar                     # TAILCALL
            .Lfunc_end0:
                    .size   foo, .Lfunc_end0-foo
                    .cfi_endproc
            
                    .ident  "clang version 3.8.243773 "
                    .section        ".note.GNU-stack","",@progbits
            


            1. GarryC
              21.03.2016 11:20

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


              1. khim
                21.03.2016 14:13

                Да ладно вам. Возьмите тот же самый пример посмотрите что вам напорождает компилятор:

                Вот вам ARM
                $ ./clang --target=arm-eabi-elf -S -O3 test.c -o-
                ...
                        .type   foo,%function
                foo:                                    @ @foo
                        .fnstart
                @ BB#0:
                        mov     r3, #32768
                        mov     r2, #16384
                        and     r12, r2, r1, lsr #1
                        and     r3, r3, r1, lsl #1
                        bic     r2, r1, #245760
                        orr     r2, r3, r2
                        mov     r3, #131072
                        orr     r2, r2, r12
                        and     r3, r3, r1, lsl #1
                        orr     r2, r2, r3
                        mov     r3, #65536
                        and     r1, r3, r1, lsr #1
                        orr     r1, r2, r1
                        b       bar
                .Lfunc_end0:
                        .size   foo, .Lfunc_end0-foo
                        .cantunwind
                        .fnend
                


                1. pftbest
                  21.03.2016 16:48

                  Компилятор умеет LTO. Например, если O_DIRECT — это единственный флаг который может быть на входе, то код метода сокращается до двух инструкций. А вот человеку будет тяжело вспомнить в каких файлах какие флаги использовались, и что функция которая использует другие флаги нигде не вызывается и может быть исключена.


                  1. khim
                    21.03.2016 17:10
                    -1

                    Это, как бы, то, с чего мы начинали:

                    Ну и вторая проблема: человек, в общем, выдаёт порядка 30-50 строк отлаженного кода в день. Неважно — на каком языке. Если язык компактный (Haskell какой-нибудь) это может быть чуть меньше, если очень «расхлябанный» (Java или ассемблер) — чуть больше, но в общем, разница от минимума до максимума для одного и того же человека — раза 2-3, не больше.

                    Компилятор просто сможет обработать большие объёмы кода быстрее, чем человек. Человеку зачастую требуется так много версии на "вылизывание" и "подгонку" метода под конкретные условия, что железяку соотвествующую успевают снять с производства. Так что если вы не под Вояджер программу пишите (где железо будет 40 лет неизменным ввиду физической невозможности его заменить), то вы просто не успеете воспользоваться совершенством "человеческого" кода. Но что он будет лучше — несомненно. LTO там или ГТО.


            1. Randl
              21.03.2016 22:31

              Назвать ваш пример небанальной задачей язык не поворачивается. Имелось ввиду что-то посложнее.
              Попробуйте написать ту же нейронную сеть лучше компилятора. ;)


              1. khim
                21.03.2016 22:50

                Да легко. Берёте вывод "clang -O3 -S" и смотрите где его улучшить. Уверяю вас — там будет куча мест, где это можно сделать. И чем сложнее будет задача — тем их будет больше.

                Вопрос, как я уже сказал, во времени, больше ни в чём.


                1. Randl
                  21.03.2016 22:56

                  А смысл? Делайте тогда ассемблерные вставки где надо. А самостоятельно полностью вы не напишете лучше, я почти уверен в этом. А если и напишете — вы один из немногих.


                  1. khim
                    22.03.2016 01:05

                    А самостоятельно полностью вы не напишете лучше, я почти уверен в этом.

                    Дурацкий вопрос: вы это сами пробовали проделывать или сейчас тут только руками махаете?

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

                    А смысл?
                    А вот это — нужно в каждом случае решать отдельно. Да, писать на ассемблере — долго и зачастую невыгодно. Просто времени и сил уходит на порядок больше, чем если писать то же самое на C и на два и более порядка — чем если писать на языках более высокого уровня.

                    Но это, согласитесь, совсем другой довод, чем тот, с которого мы начали. Да, от самых страшных вещей компиляторы научились избавляться (обратите внимание на то, что в описанном примере компилятор пидумал-таки как обойтись без условных переходов — а это, в общем-то, самое главное при использовании современным CPU), но говорить о том, что «компиляторы гораздо лучше оптимизируют, чем это сделал бы человек» — увы, несколько преждевременно. Это если сказать мягко.

                    P.S. Та же самая ситуация и «ниже», кстати: AMD пользуется HDL-компиляторами при извотовлении своих CPU, а Intel — много разводит «руками». Но происходит это не потому, что «компиляторы ща умные — вастче». А просто потому, что на «ручную» разводку у AMD ресурсов не хватает. Результат — всем известен, не так ли? С другой стороны если некому приличную архитектуру сделать, но никакая «ручная разводка» и «написание на ассемблере» не спасут. Пример — те же AMD и Intel, но уже в области разработки GPU :-)


                    1. Randl
                      22.03.2016 10:11

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

                      Какую самую сложную задачу вы реализовали полностью на асме? Какой был её размер? Несколько быстрее это вышло, чем оптимизированный код на С?


        1. Evengard
          29.03.2016 16:09

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


  1. interprise
    21.03.2016 20:36

    На самом деле, очень интересно. Надо попробовать загрузить через этот супервизор 2 ОС одновременно. Всегда хотел возможность переключиться в полноценную виндовз, с полноценной GPU, при этом не выключая linux base os