LINUX.ORG.RU

Динамическая кодогенерация на Си

 , ,


2

4

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

#include <stdio.h>

#define MAX_CODE_SIZE 65536

typedef int (*FN_CALLBACK)();

int invoke(int size, unsigned char *source)
{
	unsigned char buffer[MAX_CODE_SIZE];
	register unsigned char *src;
	register unsigned char *dest;
	unsigned char *limit;
	if (size <= 0 || size >= MAX_CODE_SIZE)
		return 0;
	src = source;
	dest = buffer;
	limit = buffer + size;
	while (dest < limit)
		*dest++ = *src++;
	return ((FN_CALLBACK)((void*)buffer))();
}

unsigned char prog[] = {
	0x31, 0xC0, // xor eax, eax
	0x40,       // inc eax
	0x40,       // inc eax
	0xC3        // ret
};

int main(int argc, char *argv[])
{
	int r;
	r = invoke(5, prog);
	printf("Result: %d\n", r);
	return 0;
}
Он успешно компилируется GCC, при запуске выводит 2. Вопрос в том, будет ли такой код кроссплатформенным (на x86)? У меня в Ubuntu и Windows 7 он работает, но как будет на других ОС? Везде ли можно выполнять команды из стека?



Последнее исправление: Den_Zurin (всего исправлений: 1)

Проблема в том, что на x86 данные в памяти могут быть доступны только либо для записи, либо для выполнения.

С чего ты так решил?

Deleted
()

В Си же есть вызовы для определения возможных операций с памятью. У x86 с paging нет на это каких-либо ограничений.

Deleted
()
Ответ на: комментарий от Deleted

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

http://sasm.narod.ru/docs/pm/pm_in/chap_4.htm

Т.е. нельзя выполнять инструкции из области данных или производить запись в код. Стек, как я понял, попадает сразу в два сегмента (с кодом и данными), поэтому позволяет и запись, и выполнение.

Den_Zurin
() автор топика
Ответ на: комментарий от Den_Zurin

В общем мне лень читать что там написано по ссылке, но в mmap() и mprotect() можно передать (PROT_WRITE | PROT_EXEC) и это будет работать. Про windows не знаю.

Deleted
()
Ответ на: комментарий от Den_Zurin

Не знаю. Попробуй погуглить.

Deleted
()
Ответ на: комментарий от Den_Zurin

Только вот сегментная адресация нафиг никому не сдалась (к тому же существует только на x86), поэтому все нормальные ОС выставляют базу сегмента 0, а лимит - 4 ГБ (т. е. вся доступная память), что для кода, что для данных, а правами доступа управляют лишь с помощью страничной трансляции. Не, ну конечно для каких-то служебных структур (например, Task State Segment) могут создаваться сегменты с иными параметрами, однако приложению всегда доступен один сегмент кода и один сегмент данных, которые покрывают всё виртуальное адресное пространство. Собственно, именно они и загружены в сегментные регистры при старте приложения и никто их не меняет в здравом уме. А вот на x86_64 у всех обычных сегментов (то есть не каких-то системных структур процессора типа того же TSS) база принудительно 0 и лимит максимален, так что там даже нельзя ограничить доступные приложения адреса, иначе как страничной адресацией.

А страничная адресация вполне позволяет создать страницу, которая будет одновременно и исполняться, и писаться.

Если не понял - все современные ОС настраивают сегментный механизм таким образом, чтобы он ни на что не влиял. Селекторы сегментов лишь задают текущий уровень прав кода (ядро или приложение).

P.S.: Да, 32-битная винда (на 64 битах это просто не возможно) создаёт специальный небольшой сегмент со служебными данными приложения и помещает его селектор в сегментный регистр FS, однако это не отменяет того факта, что в CS, SS, DS и ES будут селекторы сегментов, покрывающих всё адресное пространство, а работать с данными, доступными через FS можно лишь ассемблерными вставками в особых случаях.

KivApple ★★★★★
()
Последнее исправление: KivApple (всего исправлений: 2)
Ответ на: комментарий от KivApple

А вот на x86_64 у всех обычных сегментов (то есть не каких-то системных структур процессора типа того же TSS) база принудительно 0 и лимит максимален

К FS и GS это не относится. Было сделано для упрощения работы эмуляторов, виртуальных машин, а также других целей: в частности, для реализации Thread Local Storage.

SystemD-hater
()
Ответ на: комментарий от Den_Zurin

Ясен пень для любой.

man mprotect в гугле. Для каких ось там только нет

anonymous
()

Если хочется примеров для вдохновления - посмотри в сторону luajit.

Vovka-Korovka ★★★★★
()
	src = source;
	dest = buffer;
	limit = buffer + size;
	while (dest < limit)
		*dest++ = *src++;

Вот бы memcpy придумали!

anonymous
()

linux:

mprotect( ptr, len, PROT_READ | PROT_EXEC );
win32:
VirtualProtect( ptr, len, PAGE_EXECUTE_READ, &oldProtect  );

frame ★★★
()
Ответ на: комментарий от SystemD-hater

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

KivApple ★★★★★
()

будет ли такой код кроссплатформенным (на x86)

Нет. На системах, где имплементирован W^X (OpenBSD) или есть другие защиты стека, ты получишь SegFault.

beastie ★★★★★
()

У меня этот код компилируется и не работает. x86_64, mandriva

anonymous
()
Ответ на: комментарий от KivApple

на 64 битах это просто не возможно

In 64-bit mode, the FS and GS segment-base registers (unlike the DS, ES, and SS segment-base registers) can be used as non-zero data-segment base registers for address calculations, as described in [...]

http://amd-dev.wpengine.netdna-cdn.com/wordpress/media/2012/10/24592_APM_v11.pdf

On x86-64 (64-bit) Windows, GS (and not FS) is used as the segment register that points to the TIB.

https://en.wikipedia.org/wiki/Win32_Thread_Information_Block

Так что вполне возможно.

edigaryev ★★★★★
()
Ответ на: комментарий от Den_Zurin

Это не относится к современным ОС, так как они используют paging.

Deleted
()

Насколько я знаю, в некоторых ОС есть программный запрет на выставление RWX. Например, в Gentoo hardened.

Deleted
()

Проблема в том, что на x86 данные в памяти могут быть доступны только либо для записи, либо для выполнения.

А зачем тебе и то, и другое одновременно (RWX), даже в случае с JIT?

edigaryev ★★★★★
()

Хочу добавить JIT в свой интерпретатор скриптового языка. Проблема в том, что на x86

посмотри\покывыряй код pawn - как-то там автор выкрутился с самописной vm и jit компиляцией под x86_32 и ARM. пруфы: https://ru.wikipedia.org/wiki/Pawn http://www.compuphase.com/pawn/pawn.htm

MKuznetsov ★★★★★
()

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

Выполнять можно только если разрешено исполнение кода из стека. https://en.wikipedia.org/wiki/Executable_space_protection http://linux.die.net/man/8/execstack

Лучше используй mmap/mprotect как тут описано http://eli.thegreenplace.net/2013/11/05/how-to-jit-an-introduction

В виндах для этого VirtualAlloc и VirtualProtect есть

SZT ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.