Почему компиляторы помещают данные в секцию .text (код) файлов PE и ELF и как процессор различает данные и код? - PullRequest
3 голосов
/ 10 апреля 2019

Итак, я ссылаюсь на этот документ:

Бинарное перемешивание: самостоятельная рандомизация адресов команд Legacy x86 Двоичный код

https://www.utdallas.edu/~hamlen/wartell12ccs.pdf

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

enter image description here

но у меня есть несколько вопросов:

  1. как это ускоряет работу программы ?! я могу только представить, что это только сделает выполнение процессора более сложным?

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

  3. это ОЧЕНЬ плохо для безопасности, учитывая, что секция кода является исполняемой и процессор может по ошибке исполнить вредоносные данные в виде кода? (возможно, злоумышленник перенаправляет программу на эту инструкцию?)

Ответы [ 2 ]

5 голосов
/ 10 апреля 2019

Современные компиляторы агрессивно чередуют статические данные в разделах кода в двоичных файлах PE и ELF по соображениям производительности

Требуется цитирование! Это просто ложь для x86, по моему опыту с такими компиляторами, как GCC и clang , и с некоторым опытом просмотра вывода asm из MSVC и ICC.

Обычные компиляторы помещают статические данные только для чтения вsection .rodata (платформы ELF) или section .rdata (Windows). Раздел .rodata (и раздел .text) связаны как часть текста сегмент , но все данные только для чтения для всегоисполняемый файл или библиотека сгруппированы вместе, и весь код сгруппирован отдельно. В чем разница между разделом и сегментом в формате файла ELF


Смешение кода и данных на одной и той же странице имеет преимущество, близкое к нулю, и тратит покрытие данных TLB на байтах кода,и тратит покрытие инструкций TLB на байты данных.И то же самое в 64-байтовых строках кэша для траты пространства в L1i / L1d.Единственное преимущество - код + локальность данных для унифицированных кэшей (L2 и L3), но обычно это , а не .(Например, после того, как выборка кода принесет строку в L2, выборка данных из той же строки может попасть в L2 по сравнению с необходимостью идти в ОЗУ для данных из другой строки кэша.)

Но с разделенными L1iTLB и L1dTLBs,и L2 TLB как объединенный кэш-память жертвы, x86 процессоры не оптимизированы для этого. Промежуток iTLB при получении «холодной» функции не предотвращает промах dTLB при чтении байтовиз той же строки кэша на современных процессорах Intel.

Нет никакого преимущества для размера кода на x86 .Режим относительной адресации x86-64 для ПК равен [RIP + rel32], поэтому он может адресовать что угодно в пределах + -2 ГБ от текущего местоположения.32-разрядная версия x86 даже не имеет режима адресации относительно ПК.

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

Я предполагаю, что они не означают немедленный данные, такие как mov eax, 12345, где 32-битный 12345 является частью кодирования команды.Это не статические данные, которые будут загружены с инструкцией загрузки;непосредственные данные - это отдельная вещь.

И, очевидно, это только для данных только для чтения;запись рядом с указателем инструкций вызовет очистку конвейера для обработки возможности самоизменения кода.И вы обычно хотите W ^ X (запись или exec, а не оба) для ваших страниц памяти.

и как процессор может различать код и данные?

Поэтапно.Процессор выбирает байты в RIP и декодирует их как инструкции.После запуска в точке входа в программу выполнение продолжается по выбранным ветвям и падает через незанятые ветви и т. Д.

С архитектурной точки зрения ему не нужны байты, отличные от тех, которые выполняются в данный момент, или которые являютсязагружается / сохраняется как данные по инструкции.Недавно выполненные байты останутся в кеше L1-I, если они снова понадобятся, и то же самое для данных в кеше L1-D.

Наличие данных вместо другого кода сразу послеБезусловная ветвь или ret не важны. Заполнение между функциями может быть любым.Могут быть редкие случаи, когда данные могут останавливать этапы предварительного декодирования или декодирования, если они имеют определенный шаблон (потому что современные процессоры выбирают / декодируют в широких блоках по 16 или 32 байта, например), но любые более поздние этапы процессоратолько глядя на актуальные декодированные инструкции с правильного пути.(Или из-за неправильного предположения ветви ...)

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

Code-fetch всегда проверяет разрешения в TLB, поэтому он потерпит неудачу, если RIP указывает на неисполняемую страницу.(NX бит в записи таблицы страниц).

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

Например, movzx eax, byte ptr [rip - 1] устанавливает EAX в 0x000000FF, загружая последний байт смещения rel32 = -1 = 0xffffffff.


это ОЧЕНЬ плохо для безопасности, учитывая, что секция кода является исполняемой и процессор может по ошибке выполнить вредоносные данные как код?(может быть, злоумышленник перенаправляет программу на эту инструкцию?)

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

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

4 голосов
/ 10 апреля 2019
  1. Чередование кода и данных будет держать данные ближе к коду, который их использует. Это сделает данные доступными с помощью более простых и быстрых инструкций.
  2. Процессор не выполняет, это зависит от программиста / компилятора, чтобы убедиться, что данные помещены в места вне фактического потока программы. Если поток программы случайно входит в блок данных, ЦПУ будет интерпретировать данные как инструкции. Обычно данные помещаются между функциями, но иногда компилятор может добавить дополнительную инструкцию перехода, чтобы освободить место для блока данных внутри функции.
  3. Обычно это не проблема, так как программист или компилятор следят за тем, чтобы поток данных не вводил поток данных, но вы частично правы, так как если злоумышленнику удастся обмануть процессор для выполнения данных, это не будет пойман механизмами защиты памяти.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...