Как хранить и вызывать скомпилированную функцию в C / C ++? - PullRequest
5 голосов
/ 20 июля 2009

Для целей оптимизации очень низкого уровня мне было бы полезно, если бы я мог хранить скомпилированную функцию непосредственно в переменной, не указатель на функцию. То есть, если у меня есть функция foo, я хочу сделать буфер символов достаточно большим, чтобы в нем могли храниться машинные инструкции, сгенерированные для foo, и затем иметь возможность фактически вызывать foo, каким-то образом указав C, чтобы перейти к выполнению содержимого этого буфера. (предположим, что у меня есть встроенный компилятор, чтобы убедиться, что буфер символов правильно выровнен для моей архитектуры). В идеале я хотел бы сделать это, сводя использование сборки к минимуму (я понимаю, что некоторые из них могут потребоваться).

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

Мне не нужно компилировать новые функции во время выполнения, просто в буфере содержатся инструкции, соответствующие различным уже скомпилированным функциям во время выполнения. Например. Я мог бы иметь 3 скомпилированные функции и 1 буфер. Эти 3 функции известны во время компиляции, но во время выполнения буфер может соответствовать любому из 3 в разное время.

Редактировать: Чтобы уточнить, что можно получить: у меня есть структура, членом которой будет этот буфер, и различные указатели на экземпляры этой структуры. Каждый буфер структуры может содержать различную скомпилированную функцию. Если бы я использовал указатель на функцию вместо буфера, мне пришлось бы загрузить указатель на функцию struct, а затем разыменовать указатель на функцию. С помощью буфера я могу просто переместить программный счетчик к смещению (относительному расположению буфера) основания структуры. Это на один уровень косвенности меньше. Для очень маленьких функций это может быть экономией.

Редактировать 2: дальнейшее уточнение:

С указателем на функцию:

  1. Загрузить указатель из & struct + offsetof (pointer)
  2. Перейти к местоположению, содержащемуся в указателе

С буфером, содержащим машинный код:

  1. Перейти к & struct + offsetof (buffer)

Второй шаг меньше.

Ответы [ 12 ]

12 голосов
/ 20 июля 2009

Имейте в виду, что большинство современных архитектур поддерживают защиту от невыполнения для страниц памяти, а операционные системы с небольшим разумом используют ее для безопасности. Это означает, что вы не можете использовать, например, стековая память или случайная память malloc'd для хранения этого кода; вам нужно настроить разрешения с помощью, например, VirtualProtect в Windows или mprotect в Unices.

12 голосов
/ 20 июля 2009

Я не уверен, почему вы исключаете указатели на функции - если вы не изменяете код во время выполнения, то я не могу придумать ничего такого, что мог бы сделать буфер, содержащий функцию, что не мог бы сделать указатель на функцию. * +1001 *

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

Кроме того, вы не можете предполагать, что можете копировать буферы, содержащие исполняемые данные, как если бы они содержали обычные данные - вам придется исправлять переходы и переходы и другие обращения к памяти.

EDIT: Я понимаю, к чему вы клоните, но я не думаю, что вы сможете найти совместимый со стандартами способ передачи функции в буфер - C ++ очень мало говорит о структуре памяти и о том, где хранятся функции, довольно абстрактное понятие. Вы можете столкнуться с кучей хаков или использовать отдельные файлы сборок, которые можно собрать во время компиляции и загрузить из файлов во время выполнения.

Еще одно замечание - я думаю, что вы можете обнаружить, что быстрее выполнить двойную разыменование при сохранении указателя на функцию (вероятно, 4-8 байт), чем потерпеть неудачу в кеше при хранении буфера, способного содержать самую большую функцию, которую вы понадобится. Ваша структура с прикрепленной функцией имеет много общего с виртуальными функциями в классах. Существуют причины, по которым большинство (все?) Компиляторов C ++ реализуют виртуальные функции с помощью vtable, а не хранят полное определение функции для каждого экземпляра класса. Это не значит, что невозможно получить повышение производительности от хранения всей функции в буфере, это не так просто, как может показаться.

8 голосов
/ 20 июля 2009

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

5 голосов
/ 20 июля 2009

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

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

#include <iostream>
#include <vector>

using namespace std;

int multiply( int arg )
{
    return arg * 2;
}

int main()
{
    // Show that multiply apparently multiplies the given value by two.
    cout << multiply( 13 ) << endl;

    // Copy the bytes between &multiply and &main (which is hopefully the multiply code) into a vector
    vector<char> code( (char*)&multiply, (char*)&main );

    // Optimize it by making multiply always return 26
    code[0] = 0xB8;  // mov eax, 1Ah
    code[1] = 0x1A;
    code[2] = 0x00;
    code[3] = 0x00;
    code[4] = 0x00;
    code[5] = 0xC3;  // ret

    // Call the faster code, prints 26 even though '3' is given
    cout << ((int (*)(int))&code[0])( 3 ) << endl;
}
2 голосов
/ 21 июля 2009

Я думаю, вы могли бы оптимизировать что-то немного глупо. Кажется, вы надеетесь избежать каких-либо дополнительных инструкций, либо разыменования функции, или какой-либо другой подобной глупости. Позвольте мне порекомендовать альтернативу. Сначала объявите ваши функции как нормальные, но пометьте их как inline: встроенный спецификатор указывает компилятору указывать тело функции при вызове функции, а не вызывать функцию нормально (если это возможно).

inline int foo(int var)
{
  ...
}
inline int bar(int var)
{
  ...
}
inline int baz(int var)
{
  ...
}

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

switch(somestruct.callcondition)
{
case FOO_CASE: 
    foo(...);
    break;
case BAR_CASE:
    bar(...);
    break;
case BAZ_CASE:
    baz(...);
    break;
}

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

2 голосов
/ 20 июля 2009

Почему бы не объявить рассматриваемые функции inline ? Это позволит получить код вашей функции на месте, не жертвуя удобочитаемостью. (Хотя и с некоторым влиянием, когда дело доходит до отладки на низком уровне скомпилированного кода)

(Как правильно указал Тайлер, компилятор мог бы игнорировать это - я писал, предполагая, что флаги оптимизации, запускающие компилятор для встраивания кода, уже используются).

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

Допустим, у нас есть (совершенно бессмысленный, код):

#include <stdio.h>

static int test = 0;

int inline foo(int a) {
  test = 0x1234;
  return a + 0x1234;
}

int inline bar(int a) {
  test = 0x5678;
  return a + 0x5678;
}

int inline baz(int a) {
  test = 0x1357;
  return a + 0x1357;
}

int main(int argc, char*argv[]) {
  int i = getchar();
  int j = 0;
  switch(i) {
    case 'a': j = foo(i);
        break;
    case 'b': j = bar(i);
        break;
    case 'c': j = baz(i);
        break;
    default:
        j = 0;
  }
  printf("j: %d, test: %d\n", j, test);
}

Скомпилировав его в gcc 4.1.1 с -O2 и выполнив objdump -d для получившегося исполняемого файла, мы получим следующий фрагмент для main ():

08048480 <main>:
 8048480:   8d 4c 24 04             lea    0x4(%esp),%ecx
 8048484:   83 e4 f0                and    $0xfffffff0,%esp
 8048487:   ff 71 fc                pushl  -0x4(%ecx)
 804848a:   55                      push   %ebp
 804848b:   89 e5                   mov    %esp,%ebp
 804848d:   51                      push   %ecx
 804848e:   83 ec 14                sub    $0x14,%esp
 8048491:   a1 1c a0 04 08          mov    0x804a01c,%eax
 8048496:   89 04 24                mov    %eax,(%esp)
 8048499:   e8 b2 fe ff ff          call   8048350 <_IO_getc@plt>
 804849e:   83 f8 62                cmp    $0x62,%eax
 80484a1:   74 31                   je     80484d4 <main+0x54>
 80484a3:   83 f8 63                cmp    $0x63,%eax
 80484a6:   74 3d                   je     80484e5 <main+0x65>
 80484a8:   31 d2                   xor    %edx,%edx
 80484aa:   83 f8 61                cmp    $0x61,%eax
 80484ad:   8d 76 00                lea    0x0(%esi),%esi
 80484b0:   74 44                   je     80484f6 <main+0x76>
 80484b2:   a1 24 a0 04 08          mov    0x804a024,%eax
 80484b7:   89 54 24 04             mov    %edx,0x4(%esp)
 80484bb:   c7 04 24 e8 85 04 08    movl   $0x80485e8,(%esp)
 80484c2:   89 44 24 08             mov    %eax,0x8(%esp)
 80484c6:   e8 95 fe ff ff          call   8048360 <printf@plt>
 80484cb:   83 c4 14                add    $0x14,%esp
 80484ce:   59                      pop    %ecx
 80484cf:   5d                      pop    %ebp
 80484d0:   8d 61 fc                lea    -0x4(%ecx),%esp
 80484d3:   c3                      ret
 80484d4:   b8 78 56 00 00          mov    $0x5678,%eax
 80484d9:   ba da 56 00 00          mov    $0x56da,%edx
 80484de:   a3 24 a0 04 08          mov    %eax,0x804a024
 80484e3:   eb cd                   jmp    80484b2 <main+0x32>
 80484e5:   b8 57 13 00 00          mov    $0x1357,%eax
 80484ea:   ba ba 13 00 00          mov    $0x13ba,%edx
 80484ef:   a3 24 a0 04 08          mov    %eax,0x804a024
 80484f4:   eb bc                   jmp    80484b2 <main+0x32>
 80484f6:   b8 34 12 00 00          mov    $0x1234,%eax
 80484fb:   ba 95 12 00 00          mov    $0x1295,%edx
 8048500:   a3 24 a0 04 08          mov    %eax,0x804a024
 8048505:   eb ab                   jmp    80484b2 <main+0x32>
 8048507:   90                      nop

Итак, компилятор уже вставил "нужные goto () s". В качестве побочного эффекта, в этом случае он также уже добавил, что нужно добавить 0x1234 / 0x5678 / 0x1357 по мере необходимости - так что результаты - простые шаги.

1 голос
/ 28 июля 2009

Я знаю, что этому больше недели, но все равно:

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

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

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

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

Если вы пишете этот код для встраиваемой системы или чего-то еще, где производительность является такой сложной проблемой, вам следует подумать о написании этой части вашего приложения на ассемблере. Таким образом, вы сможете лучше контролировать поведение кода и обойти соглашения о вызовах с функциями, внутренними для этой части. Вероятно, есть причина, по которой вы не можете делать такие вещи в C: вам это никогда не нужно - и архитектура обычно не очень хорошо это поддерживает (если вообще). Указатели на функции идеально подходят для нужного вам поведения, а функции-члены еще лучше. Если вы хотите стать еще лучше, вам придется написать сборку самостоятельно, в противном случае все рухнет, и его будет почти невозможно поддерживать.

дополнение

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

Чтобы инициализировать буфер функции, который вы хотите, вам нужно скопировать целый набор исполняемых данных в буфер. Что является хитом производительности.

1 голос
/ 21 июля 2009

Чтобы фактически получить машинный код для ваших функций в автоматическом режиме во время процесса сборки, я предлагаю поместить их в отдельный файл .c, а затем скомпилировать его в независимый от позиции код в объектном файле:

gcc -Wall -c -fPIC functions.c -o functions.o

Затем используйте objdump для выгрузки разборки:

objdump -d functions.o

Выбросьте все, кроме шестнадцатеричных кодов, поместите их в структуру C с помощью xdd -i.

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

1 голос
/ 20 июля 2009

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

Лучше всего написать всю эту часть в сборке, а затем связать ваш C-файл с процедурами сборки на этапе компиляции / компоновки. C просто не поддерживает такую ​​работу без большого взлома в ... сборке.

0 голосов
/ 03 августа 2009

Вот решение на C для сохранения функции в буфере:

#include <stdio.h>
#define FUNC_SIZE(func) ((long)(& func##_end)-(long)(&func))
#define FUNC_COPY(func,buf) {buf=malloc(FUNC_SIZE(func));memcpy(buf,&func,FUNC_SIZE(func));}

int magic(int arg){
  arg+=1;
  arg-=1;
  arg*=arg;
  /* WHATEVER */
  return arg;
}
void magic_end(){
  return;
}
int main(int argc, char *argv[])
{
  int (*buf)();
  printf("Size of the function: %i\n",FUNC_SIZE(magic));
  FUNC_COPY(magic,buf);
  printf("Result: %i, %i\n",(*buf)(7),magic(7));
  return 0;
}

Но я не думаю, что это быстрее, потому что вам все еще нужен указатель.

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