Инициализация static'ов и самомодифицирующийся код
От: B0FEE664  
Дата: 09.09.15 12:37
Оценка:
Существуют ли компиляторы, (или опции оптимизации), которые для кода вида:

void Fun(int a)
{
  ...

  static int n = a + SomeFunction();

  ...
}


порождают самомодифицирующийся код для инициализации n или все используют условный переход?
И каждый день — без права на ошибку...
Re: Инициализация static'ов и самомодифицирующийся код
От: Кодт Россия  
Дата: 09.09.15 12:49
Оценка: +3
Здравствуйте, B0FEE664, Вы писали:

BFE>порождают самомодифицирующийся код для инициализации n


и для этого отключают защиту памяти?

Одно дело, когда приложение генерирует JIT код, и там может делать что хочет.
И другое, когда надо существующую страницу кода на ходу поправить.
Для этого страница из расшаренной превращается в персональную, — сколько процессов, столько будет копий страницы.
В такую страницу можно пострелять, — например, из другого потока. А если защита выключается и тут же включается обратно, это дорогое удовольствие. И всё равно, остаётся маленькое окошко для возможности стрельбы.
Перекуём баги на фичи!
Re: Инициализация static'ов и самомодифицирующийся код
От: watchmaker  
Дата: 09.09.15 13:55
Оценка: 2 (1)
Здравствуйте, B0FEE664, Вы писали:

BFE>Существуют ли компиляторы, (или опции оптимизации), которые для кода вида:

BFE>порождают самомодифицирующийся код для инициализации n или все используют условный переход?

Кажется, что на современных машинах таких нет. Слишком много недостатков. Выше уже упомянули про проблемы с защитой памяти и с возрастанием требования к объёмам памяти. Последние, впрочем, можно снизить если собрать из всех функций весь код, отвечающий за инициализацию static, в несколько плотных страниц, что позволит копировать только их, а не кучу размазанных по всей программе в случайных местах (скорость, правда, несколько от этого снизится из-за пары безусловных переходов).
Но если и ещё одна неприятная проблема — многопоточное исполнение кода. Это только в одном потоке происходит самомодификация кода, в других же наблюдается кросс-модификация. А исполнение кросс-модифицированного кода без синхронизации — это уже неопределённое поведение на уровне процессора. Способы исполнять кросс-модифицированный код, конечно, есть. Но если посмотреть в документацию (например, на x86), то все они оказываются дороже банального условного перехода из классической реализации.

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


Более перспективным в этой области кажется другой подход: когда компилятор делает в ходе анализа два варианта функции: с read-write static и c read-only static — и вызывает второй вариант в точках программы, куда выполнение не может попасть не пройдя успешно через первый вариант. На намного более примитивном уровне так уже делается — если значение получается вычислить в compile-time, то компилятор не будет создавать код синхронизации, а сразу проинициализирует память нужным значением.
Впрочем отлично предсказываемое ветвление в классической реализации настолько дёшево, что, видимо, будет не очень много случаев, когда такая замена с двумя функциями окупится (в смысле отношения скорость/объём программы).
Re[2]: Инициализация static'ов и самомодифицирующийся код
От: watchmaker  
Дата: 10.09.15 12:17
Оценка:
Здравствуйте, Кодт, Вы писали:

К> А если защита выключается и тут же включается обратно

Забавно, но ведь просто так тут же включать обратно нельзя — может произойти гонка. Ведь вдруг за это время какой-нибудь другой поток пришёл в другую функцию со своим static и тоже начал аналогичную последовательность. Тогда если обе функции случайно попадут в одну страницу, то эта страница может быть закрыта на запись раньше чем второй поток закончит свои изменения. Так что придётся ещё и синхронизировать потоки, которые работают с разными статическими переменными — налицо ещё одно ухудшение по сравнению с текущей ситуацией, где разные статические переменные имеют независимые друг от друга объекты синхронизации.
Re[2]: Инициализация static'ов и самомодифицирующийся код
От: T4r4sB Россия  
Дата: 25.09.15 08:05
Оценка:
Здравствуйте, Кодт, Вы писали:

К>и для этого отключают защиту памяти?


А вот как в сишке устроены указатели на вложенные функции? Причём они ещё и совместимы с указателями на нормальные функции, то есть никакой структуры, хранящей контекст, там нету.
То, что я о них слышал — это самая настоящая самомодификация.

То есть указатель на вложенную функцию — это указатель на такой код:

mov ecx, VALUE ; в валуе пишем ссылку на контекст перед тем, как передать указатель на эту функцию
jmp real_start_of_function
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[3]: Инициализация static'ов и самомодифицирующийся код
От: watchmaker  
Дата: 25.09.15 09:22
Оценка:
Здравствуйте, T4r4sB, Вы писали:

TB>А вот как в сишке устроены указатели на вложенные функции? Причём они ещё и совместимы с указателями на нормальные функции, то есть никакой структуры, хранящей контекст, там нету.

TB>То, что я о них слышал — это самая настоящая самомодификация.

В стандартном С таких функций нет. Если же это про расширения вроде Nested Functions в gcc, то, как уже написано в документации, используются трамплины. То есть это не совсем самомодификация (она опять же не дружит с многопоточностью), но вместо этого есть стек динамически создаваемых функций, адреса которых и передаются во вложенные вызовы, и которые выглядят в общем-то именно так как у тебя и написано:
TB>mov ecx, VALUE ; в валуе пишем ссылку на контекст перед тем, как передать указатель на эту функцию
TB>jmp real_start_of_function

Разумеется, в такой программе возникает уже указанный недостаток с защитой памяти: появляются области, которые доступны как для записи, так и для исполнения. А если для скорости, такая память выделяется не в отдельном блоке, а в общем стеке, то, соответственно, весь стек потока становится доступным для исполнения.
Re[2]: Инициализация static'ов и самомодифицирующийся код
От: B0FEE664  
Дата: 16.01.23 17:56
Оценка:
Здравствуйте, watchmaker, Вы писали:

W>Впрочем отлично предсказываемое ветвление в классической реализации настолько дёшево, что, видимо, будет не очень много случаев, когда такая замена с двумя функциями окупится (в смысле отношения скорость/объём программы).


Тем не менее в C++20 добавили constinit specifier.
И каждый день — без права на ошибку...
Re: Инициализация static'ов и самомодифицирующийся код
От: Chorkov Россия  
Дата: 17.01.23 09:27
Оценка:
Здравствуйте, B0FEE664, Вы писали:

BFE>Существуют ли компиляторы, (или опции оптимизации), которые для кода вида:


BFE>
BFE>void Fun(int a)
BFE>{
BFE>  ...

BFE>  static int n = a + SomeFunction();

BFE>  ...
BFE>}
BFE>


BFE>порождают самомодифицирующийся код для инициализации n или все используют условный переход?


Если гнаться за оптимизацией, то такую переменную можно расположить на отдельной странице памяти.
При первом обращении получим SEH, который можем обработать и поменять свойства страницы.

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

С другой стороны — одна переменная на страницу — не будет дружить ни с кешем, ни с экономией памяти.

С третьей стороны, классическое решение с флаговой переменной: ветвление очень хорошо предсказывается процессором, и дружит с кешем...
Re: Инициализация static'ов и самомодифицирующийся код
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 17.01.23 20:53
Оценка: +1
Здравствуйте, B0FEE664, Вы писали:

BFE>Существуют ли компиляторы, (или опции оптимизации), которые для кода вида:


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