Опубликованный проект является настоящей находкой для системных программистов, а также исследователей по безопасности, которые интересуются низкоуровневыми функциями ОС. Гипервизор получил название 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)
SerJook
20.03.2016 15:55Объясните чо с этим делать теперь
khim
20.03.2016 16:30+3Это тот случай, когда говорят "если вы задаёте этот вопрос, то вам не нужен ответ".
Это — просто полуфабрикат. Поверх него можно сделать кучу интересных вещей. Но сам по себе… Как уже сказали: Ничего не происходит. Просто какой-то код работает на низком уровне и не падает. Отлично. — это достаточно полное описание происходящего. Причём последнее слово там к месте: подобную штуку самому, с нуля, по докам — написать достаточно сложно.
gonzzza
20.03.2016 17:09+1А почему малое количество строк на ассемблере преподносится как преимущество? Я всегда считал что asm используется для максимальной производительности. Все уже совсем не так?
Evengard
20.03.2016 17:15+3Код на асме оч сложно портировать (к примеру, на другие архитектуры), к тому же сейчас компиляторы гораздо лучше оптимизируют, чем это сделал бы человек. Ну и человекопонятность кода.
khim
20.03.2016 18:04+2к тому же сейчас компиляторы гораздо лучше оптимизируют, чем это сделал бы человек
Увы, но это неправда. Такого — даже и близко нету.
Все компиляторы, даже самые современные оптимизируют между «ужас как плохо» и «это такой ужас, что у меня больше нет слов, одни эмоции».
Однако при этом иногда смотришь на код, который они выдают — и просто офигеваешь: это же просто гениально! Откуда это? А это значит — ваша задача хорошо «легла» на какой-то паттерн, который часто встречается и потому кто-то из разработчиков компилятора придумал как это место особо хорошо сделать. Разработал соответствующий ассемблерный кусок и вшил в компилятор. А так как разработчиков компилятора много (тысячи если считать всех, сотни — если только тех, кому за разработку денег платят), то случается это довольно часто. Но достаточно чуть-чуть изменить исходный код — и всё: пиши пропало.
На самом деле, конечно же, человек действует так же (особенно если не гнушаться тем чтобы посмотреть на то, как компилятор решает ту или иную проблему — очень полезно, особенно если вы мало работали с той или иной архитектурой), однако «сведение» задачи к известным паттернам человек делает намного, намного лучше.
Ну и вторая проблема: человек, в общем, выдаёт порядка 30-50 строк отлаженного кода в день. Неважно — на каком языке. Если язык компактный (Haskell какой-нибудь) это может быть чуть меньше, если очень «расхлябанный» (Java или ассемблер) — чуть больше, но в общем, разница от минимума до максимума для одного и того же человека — раза 2-3, не больше.
Однако одной строке на C соответствуют десятки строк ассемблера! То есть с гипервизором на C проще и быстрее работать. Ну и переносимость — хотя это не всегда важно.Randl
21.03.2016 01:12Ну если задача одновременно "не легла" и является критичным местом, то переписать её на ассемблере — вполне реально. Писать же на ассемблере всё — оверкилл, куча времени и денег.
При этом те самые известные паттерны, встроенные в компилятор, писали программисты уровнем гораздо выше среднего. Количество программистов способных решить небанальную задачу на асме лучше компилятора невелико.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
GarryC
21.03.2016 11:20Для по-настоящему сложных систем команд типа ARM со сдвигами и хитрой организацией констант компилятор действительно побеждает человека, и намного проще подсказать ему некоторые особенности, чам переписывать на ассемблере.
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
pftbest
21.03.2016 16:48Компилятор умеет LTO. Например, если O_DIRECT — это единственный флаг который может быть на входе, то код метода сокращается до двух инструкций. А вот человеку будет тяжело вспомнить в каких файлах какие флаги использовались, и что функция которая использует другие флаги нигде не вызывается и может быть исключена.
khim
21.03.2016 17:10-1Это, как бы, то, с чего мы начинали:
Ну и вторая проблема: человек, в общем, выдаёт порядка 30-50 строк отлаженного кода в день. Неважно — на каком языке. Если язык компактный (Haskell какой-нибудь) это может быть чуть меньше, если очень «расхлябанный» (Java или ассемблер) — чуть больше, но в общем, разница от минимума до максимума для одного и того же человека — раза 2-3, не больше.
Компилятор просто сможет обработать большие объёмы кода быстрее, чем человек. Человеку зачастую требуется так много версии на "вылизывание" и "подгонку" метода под конкретные условия, что железяку соотвествующую успевают снять с производства. Так что если вы не под Вояджер программу пишите (где железо будет 40 лет неизменным ввиду физической невозможности его заменить), то вы просто не успеете воспользоваться совершенством "человеческого" кода. Но что он будет лучше — несомненно. LTO там или ГТО.
Randl
21.03.2016 22:31Назвать ваш пример небанальной задачей язык не поворачивается. Имелось ввиду что-то посложнее.
Попробуйте написать ту же нейронную сеть лучше компилятора. ;)khim
21.03.2016 22:50Да легко. Берёте вывод "clang -O3 -S" и смотрите где его улучшить. Уверяю вас — там будет куча мест, где это можно сделать. И чем сложнее будет задача — тем их будет больше.
Вопрос, как я уже сказал, во времени, больше ни в чём.Randl
21.03.2016 22:56А смысл? Делайте тогда ассемблерные вставки где надо. А самостоятельно полностью вы не напишете лучше, я почти уверен в этом. А если и напишете — вы один из немногих.
khim
22.03.2016 01:05А самостоятельно полностью вы не напишете лучше, я почти уверен в этом.
Дурацкий вопрос: вы это сами пробовали проделывать или сейчас тут только руками махаете?
Я — пробовал. Долго — это да, тут и говорить не о чем. Но несложно. Современные компиляторы всё ещё генерят довольно-таки "рыхлый" код — и вышеуказанного примера вполне достаточно для того, чтобы примерно понять — когда и почему.
А смысл?
А вот это — нужно в каждом случае решать отдельно. Да, писать на ассемблере — долго и зачастую невыгодно. Просто времени и сил уходит на порядок больше, чем если писать то же самое на C и на два и более порядка — чем если писать на языках более высокого уровня.
Но это, согласитесь, совсем другой довод, чем тот, с которого мы начали. Да, от самых страшных вещей компиляторы научились избавляться (обратите внимание на то, что в описанном примере компилятор пидумал-таки как обойтись без условных переходов — а это, в общем-то, самое главное при использовании современным CPU), но говорить о том, что «компиляторы гораздо лучше оптимизируют, чем это сделал бы человек» — увы, несколько преждевременно. Это если сказать мягко.
P.S. Та же самая ситуация и «ниже», кстати: AMD пользуется HDL-компиляторами при извотовлении своих CPU, а Intel — много разводит «руками». Но происходит это не потому, что «компиляторы ща умные — вастче». А просто потому, что на «ручную» разводку у AMD ресурсов не хватает. Результат — всем известен, не так ли? С другой стороны если некому приличную архитектуру сделать, но никакая «ручная разводка» и «написание на ассемблере» не спасут. Пример — те же AMD и Intel, но уже в области разработки GPU :-)Randl
22.03.2016 10:11Вопрос был в том, какой смысл брать вывод компилятора и улучшать его в качестве ассемблерной программы вместо того чтобы делать в ней ассемблерные вставки. С самого начала я об этом и говорил — неудачные куски можно переписать на асме в качестве вставок в С код.
Какую самую сложную задачу вы реализовали полностью на асме? Какой был её размер? Несколько быстрее это вышло, чем оптимизированный код на С?
Evengard
29.03.2016 16:09Хорошо, небольшой фикс — "компиляторы оптимизируют эффективней за единицу времени". :) Понятно, что человек может вылизать код до идеального состояния, но трудозатраты на это слишком велики. Тогда как компилятор в большинстве случаев выдаст относительно нормальный код, который может быть и не будет идеальным, но будет вполне рабочим и относительно эффективным, при этом за очень хорошие показатели времени.
interprise
21.03.2016 20:36На самом деле, очень интересно. Надо попробовать загрузить через этот супервизор 2 ОС одновременно. Всегда хотел возможность переключиться в полноценную виндовз, с полноценной GPU, при этом не выключая linux base os
Scratch
И, после его запуска система становится виртуализированной или что происходит?
zolti
В систему встраивается гипервизор, как и любой другой, к примеру Hyper-V, обычно на Ring-1 уровня ядра.
hardex
Так он встраивает себя внизу под уже работающей ОС?
Как проявляется вообще его присутствие?
zolti
Насколько видно из описания проекта — никак не проявляется, автор выложил сам гипервизор, показал как его устанавливать, а дальше дело за теми, кто захочет его использовать и в каких целях.
hardex
Автору стоило бы дополнить его хотя бы минимальным примером допиливания, хотя бы например подменой cpuid.
Scratch
т.е. ничего не происходит. Просто какой-то код работает на низком уровне и не падает. Отлично
vanxant
Судя по исходникам, можно читать и писать память, в том числе ядра.
Т.е. делать свой дебагер с блэкджеком.
IbhSvenssen
Или няшный вирус
1vanK
Который нужно устанавливать
vanxant
… но если есть ключик, то можно хоть и с апдейтом драйверов пропихнуть, никто не заметит.
Хотя по факту это ничего не меняет, производители дров и сейчас имеют полный доступ к системе.