Мои примеры ниже для 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"
.
Если моя программа не может быть отремонтирована, не могли бы вы предоставить способ жизни функции в куче? Спасибо!
Ниже приведен рабочий код, вот что я делаю:
- Выполните,
printf
один раз, чтобы убедиться, что gcc
включает его в наш двоичный файл.
- Скопируйте правильный размер байтов в кучу, чтобы не обращаться к памяти, которой не существует.
- Выделите исполняемый файл память с опцией
mmap
и PROT_EXEC
.
- Передайте
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