Простой пример использования inline assembler в D

Сегодня мы попробуем применить один из интересных инструментов, встроенных в D, а именно про встроенный (или как еще его называют inline) ассемблер. Поскольку D – системный язык программирования, то его создали встроили в него ассемблер для осуществления прямого взаимодействия с системой и оборудованием. Более того, ассемблер является частью спецификации самого D, что не может не радовать.

Язык ассемблера – это довольно сложная тема, и как правило, большую часть вещей в программировании можно сделать и без его применения. Однако, иногда требуется получить информацию, которую не получить больше никаким другим способом или к примеру, иногда возникает необходимость обращения к процессору или иному оборудованию напрямую. Вот именно в этих случаях может выручить ассемблер. В большинстве своем, ассемблерный код собирается вне основной программы и используется отдельно, но в некоторых языках программирования есть возможность использовать ассемблерный вставки в код напрямую, которые или обрабатываются препроцессором или извлекаются на раннем этапе компиляции программы. В D нет отдельных вставок, поскольку ассемблер – это естественная часть самого языка, пусть и отличающаяся по синтаксису и возможностям от привычного D-кода.

Мы не будем рассматривать подробности взаимодействия с ассемблером и программирования на нем, а просто покажем вам, что такое в D возможно и иногда даже без особого погружения в низкоуровневое программирование. Для примера нами была выбрана очень простая инструкция ассемблера – cpuid, которая позволяет через манипуляцию регистрами процессора извлекать различную системную информацию о процессоре. В нашем эксперименте мы не будем извлекать всю информацию, поскольку инструкция cpuid в действительности позволяет получать самый различный набор сведений, мы будем получать данные о том, к какому производителю относится процессор.

Наша цель проста – нам требуется верно записать то, что требует для своей работы cpuid, а также верно определить местонахождение результата и корректно его декодировать. Инструкция cpuid для того, чтобы получить информацию от процессора, требует помещения в один или несколько регистров процессора ряда аргументов, а свои результаты сохраняет в другие регистры. В частности, для получения информации о том, к какому производителю принадлежит процессор, нам нужно разместить в регистр EAX число 0, а результаты будут размещены в регистры EBX, ECX и EDX.

Важный момент: ассемблер – это архитектурно-зависимая вещь, и в этой статье подразумевается, что речь идет об архитектуре x86_64.

Однако, вернемся к D, в котором код, который делает все манипуляции, описанные выше выглядит так:

import std.stdio;

void main()
{
   uint b, c, d;

   asm {
     mov EAX, 0;
     cpuid;
     mov  b, EBX;
     mov  c, ECX;
     mov  d, EDX;
   }

   auto bytesAsChars = (uint x) {
	    import std.algorithm : each;
	    import std.range : iota;

		iota(0, 32, 8).each!(a => write(cast(char) ((x >> a) & 0xFF)));
   };
   
   bytesAsChars(b);
   bytesAsChars(d);
   bytesAsChars(c);
   writeln;
}

Результатом работы кода будет надпись либо “GenuineIntel”, либо “AuthenticAMD”, в зависимости от того, процессор у вас от Intel или от AMD.

И пара слов о том, как это работает. Весь код, который относится к inline-assembler, размещен в блоке asm {} и в нем происходит основное действие. С помощью инструкции MOV в регистр EAX помещается 0, затем выполняется cpuid, потом происходит (также с помощью инструкций MOV) перемещений значений из регистров EBX, ECX, EDX процессора в заранее определенные переменные. Перенос в переменные сделан из-за того, что иначе прочитать значения нельзя, так как после выполнения других инструкций программы содержимое регистров будет безвозвратно изменено.

Но это не самое интересное место в коде, поскольку после получения в переменные значений от cpuid, их нужно декодировать. Дело в том, что те строки, которые мы получаем на выходе программы представлены как несколько 32-разрядных чисел, байты которых надо рассматривать как символы из ASCII. Но это только одна сторона проблемы, так как байты идут в обратном порядке и регистры не в том порядке содержат нужную информацию. Поэтому есть функция-помощник bytesAsChars, которая берет 32-битное число и с помощью диапазонов, которые генерируют индексы для побитового сдвига и маски, превращает одно число в строку из символов ASCII. Проблема с порядком регистров решается просто последовательностью вызова этой функции применительно к нашим переменным, а именно: сначала в строку переводим переменную b, затем – d и в последнюю очередь c.

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

На этом все и большое спасибо вот этому руководству за подсказку по cpuid.