Термин «заклинать» я позаимствовал у автора замечательных статей на сайте. Это серия статей про «Заклинание кода», которые я Вам рекомендую почитать после прочтения моей статьи.
Сейчас я покажу Вам как же написать простой пример заклинания кода применительно для FPU. Сразу должен предупредить, что хоть в конце и будет участвовать C#, для самого заклинания нужен С++.
Допустим нам надо вычислить такое выражение: result = arg1 – arg2 + arg3.
Есть несколько вариантов составления кода. Чтобы не усложнить понимание происходящего, я покажу сначала один, чуть позже покажу другой.
Итак, первый вариант выглядит так:
fld [arg1]
fld [arg2]
fsubp
fld [arg3]
faddp
fstp [result]
ret
Теперь поясню. В квадратных скобках мы должны указывать адреса переменных arg1, arg2, arg3, result.
Инструкция fld загружает в вершину стека (FPU работает со стеком, причем он имеет некоторые особенности) значение переменной double, адрес которой идет сразу после инструкции; fsubp – производит вычитание значения, лежащего на 1 позиции в стеке ниже, значение, лежащее на вершине стека и освобождает вершину стека, тем самым результат записывается на место значения, из которого вычитается, результат находится теперь на вершине стека; faddp – работает по аналогии с fsubp, только не вычитает, а складывает значения; fstp – выгружает из вершины стека значение double, выгружает в ячейку по адресу, указанному далее; ну и инструкция ret – интуитивно понятная – завершает выполнение функции и передает управление в функцию, вызвавшую ее. Чтобы было более понятно, покажу работу нашего кода в картинках:

Результат записывается в ячейку памяти, откуда его можно забрать. Работа инструкций надеюсь ясна. Теперь посмотрим как нам такой код создать из программы на С++.
double ExecuteMagic(double arg1, double arg2, double arg3)
{
short* code;
short* code_cursor;
short* code_end;
double* data;
double* data_cursor;
SYSTEM_INFO si;
GetSystemInfo(&si);
DWORD region_size = si.dwAllocationGranularity;
code = (short*)VirtualAlloc(NULL, region_size * 2, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
code_cursor = code;
code_end = (short*)((char*)code + region_size);
data = (double*)code_end;
data_cursor = data;
*data_cursor = arg1;
*code_cursor++ = (short)0x05DDu; //fld
*(int*)code_cursor = (int)(INT_PTR)(data_cursor); //1.0
code_cursor = (short*)((char*)code_cursor + sizeof(int)); // смещение
data_cursor++;
*data_cursor = arg2;
*code_cursor++ = (short)0x05DDu; //fld
*(int*)code_cursor = (int)(INT_PTR)data_cursor++; //-2.0
code_cursor = (short*)((char*)code_cursor + sizeof(int)); // смещение
*code_cursor++ = (short)0xE9DEu; //fsubp
*data_cursor = arg3;
*code_cursor++ = (short)0x05DDu; //fld
*(int*)code_cursor = (int)(INT_PTR)data_cursor++; //2.0
code_cursor = (short*)((char*)code_cursor + sizeof(int)); // смещение
*code_cursor++ = (short)0xC1DEu; //faddp
double *result = data_cursor;
*code_cursor++ = (short)0x1DDDu; //fstp
*(int*)code_cursor = (int)(INT_PTR)data_cursor++; //
code_cursor = (short*)((char*)code_cursor + sizeof(int)); // смещение
*code_cursor++ = (short)0x90C3u; //ret
void (*function)() = (void (*)())code;
//1-(-2)+2=5
function();
return *result;
}
* This source code was highlighted with Source Code Highlighter.Теперь давайте разберем все самое вкусное здесь. Итак, мы с помощью функции VirtualAlloc выделяем нашему коду некоторое количество памяти (а именно согласно значению структуры
SYSTEM_INFO. dwAllocationGranularity, как бы системная величина разбиения памяти); обратите внимание какие аргументы принимает функция на вход, а именно на PAGE_EXECUTE_READWRITE – именно этот параметр позволяет обратиться к вновь созданному участку памяти не только для чтения/записи, но и для выполнения кода, т.е. мы можем передать в этот участок памяти управление и процессор будет считывать дальнейшие инструкции именно отсюда.
Половина этого созданного массива выделяем для кода, вторую половину для данных – некое подобие сегмента кода и сегмента данных. Все что осталось – заполнить эти сегменты данных необходимыми значениями. Для заполнения массива кодом необходимо просто записать в этот массив опкоды (инструкции процессора) в шестнадцатеричном виде. Разберем все по порядку.
Инструкция FLD имеет опкод DD/0. Да, кстати, сразу скажу, что значения опкодов и их мнемоническое написание вы можете посмотреть в документации по архитектуре процессоров. Продолжим, FSTP имеет тоже опкод DD, но уже с приставкой /3 – это расширение опкода – mod r/m байт. Вот таблица значений mod r/m байта [http://www.sandpile.org/ia32/opc_rm32.htm] (Пытливые умы при присутствии интереса смогут разобраться во всем этом, поверьте). Так как инструкция FLD и FSTP могут оперировать с операндами разного типа, т.е. ячейками, регистрами процессора, то для этого и существует расширение опкода. Нам для работы нужен вид операнда адрес числа double, поэтому в той таблице мы смотрим значение для [sdword]. Для FLD это значение равно 05h, для FSTP 1Dh. Прибавляем эти значения к опкодам и получаем: FLD = DD05h, FSTP = DD1Dh. Инструкция FSUBP имеет опкод DE/5, и мы опять должны обратиться к таблице расширения опкода и посмотреть значение расширения для XMM1 (это ссылка элемент стека FPU) и видим, что оно равно E9h, т.е. FSUBP = DEE9h. FADDP также как и FSUBP имеет опкод DE, но уже /0, что для XMM1 имеет значение C1h, т.е. FADDP = DEC1h. Инструкция RET имеет опкод C390h.
Следует отметить, что инструкции процессор считывает с конца, поэтому их надо записывать наоборот, с учетом того, что они по 2 байта и парные, т.е. FLD = DD05h надо записывать не 50DDh, а 05DDh, это важно!
Ну вот в принципе и все по опкодам. Код на языке С++ выше показывает как заполнять массив инструкциями. Сначала записываем инструкцию, затем, если это необходимо, адрес ячеек. Обратите внимание, что адрес имеет длину 4 байта (32 бита) для 32-битных систем, поэтому после записи адреса в массив кода, необходимо сместить указатель на 4 байта вперед, вместо 2 байт в случае инструкций.
Кульминацией сего чуда является выполнение записанного в память кода. Как выполнить код из нашего массива? За помощью мы обращаемся к указателю на функцию, здесь язык С++ выручает. Создаем указатель на функцию типа void с параметрами void, далее присваиваем ему указатель на начало массива кода. Все! Запускаем наш указатель на функцию, получаем результат работы программы прям в памяти, процессор все сделал ровно так, как мы ему сказали в нашем массиве кода.
Теперь я напомню, что это 1 способ передачи параметров и возвращения результата. Второй способ заключается в том, чтобы создать указатель на функцию типа double(void), т.е. чтобы нам не в память записывался результат и мы его сами вытаскивали, а чтобы нам уже результат возвратила наша функция, созданная динамически. Для этого просто изменяем код на такой:
fld [arg1]
fld [arg2]
fsubp
fld [arg3]
faddp
//fstp [result]
ret
Т.е. просто оставляем в вершине стека результат. И наш указатель на функцию вернет нам результат из вершины стека. Все просто.
Читатель уже с середины статьи задается вопросом: «Причем тут C#??? Один С++ и Ассемблер, непонятные цифры…». Справедливо, но надо быть терпеливее :).
Итак, мы с Вами знаем, что мы можем выполнять из С# функции, написанные на С++, Delphi и др.
Реализовывать это можно с помощью ключевого слова extern и атрибута [DllImport(«*.dll»)].
Также есть вариант и проще. Программисты платформы .NET смогли подружить управляемый код и неуправляемый. Таким образом просто создаем новый класс на С++ с использованием вышеупомянутой техники, реализующий кодогенерацию, заклинание кода. Далее просто подключаем эту библиотеку к проекту, использующему управляемый код C# и совершенно беспрепятственно пользуемся. Я так и сделал. Как же я был рад, когда результат не заставил себя ждать! :)
Вот что я сделал:
#include <windows.h>
#pragma once
using namespace System;
namespace smallcodelib
{
public ref class CodeMagics
{
public:
static double ExecuteMagic(double arg1, double arg2, double arg3)
{
short* code;
short* code_cursor;
short* code_end;
double* data;
double* data_cursor;
SYSTEM_INFO si;
GetSystemInfo(&si);
DWORD region_size = si.dwAllocationGranularity;
code = (short*)VirtualAlloc(NULL, region_size * 2, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
code_cursor = code;
code_end = (short*)((char*)code + region_size);
data = (double*)code_end;
data_cursor = data;
*data_cursor = arg1;
*code_cursor++ = (short)0x05DDu; //fld
*(int*)code_cursor = (int)(INT_PTR)(data_cursor); //1.0
code_cursor = (short*)((char*)code_cursor + sizeof(int)); // смещение
data_cursor++;
*data_cursor = arg2;
*code_cursor++ = (short)0x05DDu; //fld
*(int*)code_cursor = (int)(INT_PTR)data_cursor++; //-2.0
code_cursor = (short*)((char*)code_cursor + sizeof(int)); // смещение
*code_cursor++ = (short)0xE9DEu; //fsubp
*data_cursor = arg3;
*code_cursor++ = (short)0x05DDu; //fld
*(int*)code_cursor = (int)(INT_PTR)data_cursor++; //2.0
code_cursor = (short*)((char*)code_cursor + sizeof(int)); // смещение
*code_cursor++ = (short)0xC1DEu; //faddp
double *result = data_cursor;
*code_cursor++ = (short)0x1DDDu; //fstp
*(int*)code_cursor = (int)(INT_PTR)data_cursor++; //
code_cursor = (short*)((char*)code_cursor + sizeof(int)); // смещение
*code_cursor++ = (short)0x90C3u; //ret
void (*function)() = (void (*)())code;
//1-(-2)+2=5
function();
return *result;
}
};
}
Это код для класса на С++.
И:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using smallcodelib;
namespace test_smallcodelib
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Заклинание кода! (* Выход)");
while (!Console.ReadLine().Equals("*"))
{
double arg1;
double arg2;
double arg3;
Console.Write("arg1?: "); arg1 = Convert.ToDouble(Console.ReadLine());
Console.Write("arg2?: "); arg2 = Convert.ToDouble(Console.ReadLine());
Console.Write("arg3?: "); arg3 = Convert.ToDouble(Console.ReadLine());
double result = CodeMagics.ExecuteMagic(arg1, arg2, arg3);
Console.WriteLine(String.Format("Result of arg1 - arg2 + arg3 = {0}", result));
}
}
}
}
* This source code was highlighted with Source Code Highlighter.Это уже на C#!
Проверьте! Все работает!
Ясное дело, что тут больше кода на С++, однако если у интересующихся есть определенный талант и интерес помучаться в данной области, то можно написать некоторую обертку на С++, которая будет генерировать такой код динамически, а использовать эту обертку уже из C#, наполняя ее необходимыми переменными и параметрами, ну и т.д. Можно получить довольно интересную штуку.
Добавлю еще пару приятностей.
Статья написана применительно программирования сопроцессора. На самом деле можно писать все, что душе угодно, для этого надо изучить архитектуру памяти и процессора ЭВМ, инструкции. Технологически продвинутые программисты, знающие что такое SSE (а она уже чуть ли не 5), могут писать код, используя все новшества процессорных технологий и самое приятное – это использовать это в C#. Все ограничивается фантазией =). Удачи в начинаниях!
Хочу выразить огромную благодарность своему друг Пётру Канковски, который в свое время помог мне во всем этом разобраться! У него есть свой сайт-вики, где он и его коллеги и друзья обсуждают различный способы оптимизации кода и др. [http://www.strchr.com/]
UPD: Здесь есть простой пример такого же принципа генерации нативного кода, но уже полностью на C#. Спасибо lastmsu за наводку на Marshal.GetDelegateForFunctionPointer().
Благодарю за внимание! Удачи!

