Тело функции на куче - PullRequest
3 голосов
/ 18 марта 2011

Программа имеет три раздела: текст, данные и стек.Тело функции живет в текстовом разделе.Можем ли мы позволить функциональному телу жить в куче?Поскольку мы можем более свободно манипулировать памятью в куче, мы можем получить больше свободы для манипулирования функциями.

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

Не могли бы вы сказать мне, почему?Если моя программа не может быть отремонтирована, не могли бы вы предоставить способ жизни функции в куче?Спасибо!

Turing.robot

#include "stdio.h"
#include "stdlib.h"
#include "string.h"

void
hello()
{
    printf( "Hello World!\n");
}

int main(void)
{
    void (*fp)();

    int size = 10000;     //  large enough to contain hello()
    char* buffer;
    buffer = (char*) malloc ( size );
    memcpy( buffer,(char*)hello,size );
    fp = buffer;
    fp();
    free (buffer);

    return 0;
}

Ответы [ 6 ]

2 голосов
/ 16 августа 2016

Мои примеры ниже для Linux x86_64 с gcc, но аналогичные соображения должны применяться в других системах.

Можем ли мы позволить функциональному телу жить в куче?

Да, мы можем. Но обычно это называется JIT (Just-in-time) компиляцией. См. this для основной идеи.

Поскольку мы можем более свободно манипулировать памятью в куче, мы можем получить больше свободы для манипулирования функциями.

Именно поэтому языки высокого уровня, такие как JavaScript, имеют JIT-компиляторы.

В следующем C-коде я копирую текст функции hello в кучу и затем указываю на нее указатель функции. Программа прекрасно компилируется с помощью gcc, но выдает "Ошибка сегментации" при запуске.

На самом деле у вас есть несколько "Segmentation fault" с в этом коде.

Первый из этой строки:

 int size = 10000;     //  large enough to contain hello()

Если вы видите x86_64 машинный код, сгенерированный gcc вашего hello функция, она компилируется до 17 байт :

0000000000400626 <hello>:
  400626:   55                      push   %rbp
  400627:   48 89 e5                mov    %rsp,%rbp
  40062a:   bf 98 07 40 00          mov    $0x400798,%edi
  40062f:   e8 9c fe ff ff          call  4004d0 <puts@plt>
  400634:   90                      nop
  400635:   5d                      pop    %rbp
  400636:   c3                      retq   

Итак, когда вы пытаетесь скопировать 10 000 байт, вы попадаете в память что не существует и получить "Segmentation fault".

Во-вторых, вы выделяете память с помощью malloc, что дает вам часть память, защищенная ЦП от выполнения в Linux x86_64, поэтому это даст вам еще "Segmentation fault".

Под капотом malloc использует системные вызовы, такие как brk, sbrk и mmap для выделения памяти. Вам нужно выделить исполняемый файл памяти, используя системный вызов mmap с защитой PROT_EXEC.

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

Например, если вы видите строку 4 скомпилированной hello функции

40062f: e8 9c fe ff ff          call  4004d0 <puts@plt>

gcc оптимизирован для использования функции puts вместо printf, но это даже не главная проблема.

На x86 архитектурах вы обычно вызываете функции, используя call сборку однако, это не отдельная инструкция, на самом деле существует множество различных машинных инструкций , которые call можно скомпилировать, см. Руководство Intel стр. Vol. 2А 3-123, для справки.

В вашем случае компилятор решил использовать относительную адресацию для call инструкции по сборке.

Вы можете видеть это, потому что ваша инструкция call имеет e8 код операции:

E8 - Call near, relative, displacement relative to next instruction. 32-bit displacement sign extended to 64-bits in 64-bit mode.

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

Теперь, когда вы перемещаете свой код с memcpy в кучу, вы просто копируете этот относительный call, который теперь переместит указатель инструкции относительно того места, куда вы скопировали код, в кучу и эта память, скорее всего, не будет существовать, и вы получите еще один "Segmentation fault".

Если моя программа не может быть отремонтирована, не могли бы вы предоставить способ жизни функции в куче? Спасибо!

Ниже приведен рабочий код, вот что я делаю:

  1. Выполните, printf один раз, чтобы убедиться, что gcc включает его в наш двоичный файл.
  2. Скопируйте правильный размер байтов в кучу, чтобы не обращаться к памяти, которой не существует.
  3. Выделите исполняемый файл память с опцией mmap и PROT_EXEC.
  4. Передайте printf функцию в качестве аргумента нашему heap_function, чтобы убедиться, что gcc использует абсолютные переходы для call инструкции.

Вот рабочий код:

#include "stdio.h"
#include "string.h"
#include <stdint.h>
#include <sys/mman.h>


typedef int (*printf_t)(char* format, char* string);
typedef int (*heap_function_t)(printf_t myprintf, char* str, int a, int b);


int heap_function(printf_t myprintf, char* str, int a, int b) {
    myprintf("%s", str);
    return a + b;
}

int heap_function_end() {
    return 0;
}


int main(void) {
    // By printing something here, `gcc` will include `printf`
    // function at some address (`0x4004d0` in my case) in our binary,
    // with `printf_t` two argument signature.
    printf("%s", "Just including printf in binary\n");

    // Allocate the correct size of
    // executable `PROT_EXEC` memory.
    size_t size = (size_t) ((intptr_t) heap_function_end - (intptr_t) heap_function);
    char* buffer = (char*) mmap(0, (size_t) size,
         PROT_EXEC | PROT_READ | PROT_WRITE,
         MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    memcpy(buffer, (char*)heap_function, size);

    // Call our function
    heap_function_t fp = (heap_function_t) buffer;
    int res = fp((void*) printf, "Hello world, from heap!\n", 1, 2);
    printf("a + b = %i\n", res);
}

Сохранить в main.c и запустить с:

gcc -o main main.c && ./main
2 голосов
/ 18 марта 2011

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

1 голос
/ 18 марта 2011

Ваша программа является segfaulting, потому что вы запоминаете больше, чем просто "привет"; эта функция не имеет длины 10000 байт, поэтому, как только вы пропустите сам привет, вы перестанете работать, потому что обращаетесь к памяти, которая вам не принадлежит.

Возможно, в какой-то момент вам также потребуется использовать mmap (), чтобы убедиться, что область памяти, которую вы пытаетесь вызвать, действительно исполняемая.

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

1 голос
/ 18 марта 2011

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

Несколько из многих требований для этой работы:

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

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

И как Адам отмечает, что существуют механизмы безопасности, по крайней мере для стека, для предотвращения вообще выполнения динамически сконструированного кода.Возможно, вам придется выяснить, как их отключить.

Возможно, вы также получили удар от memcpy().

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

f() { 
...
}

g() {
...
}

memcpy(dst, f, (intptr_t)g - (intptr_t)f)
0 голосов
/ 18 марта 2011

После malloc вы должны проверить, что указатель не равен нулю buffer = (char*) malloc ( size ); memcpy( buffer,(char*)hello,size );, и это может быть вашей проблемой, так как вы пытаетесь выделить большую область в памяти.Вы можете это проверить?

0 голосов
/ 18 марта 2011
memcpy( buffer,(char*)hello,size );

hello не является источником, скопированным в буфер. Вы обманываете компилятор и он мстит во время выполнения. При типизации от hello до char* программа заставляет компилятор поверить в это, что на самом деле не так. Никогда не переигрывайте компилятор.

...