Современные компиляторы агрессивно чередуют статические данные в разделах кода в двоичных файлах 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 или гаджета для атак, ориентированных на возврат.Но обычно в реальном коде уже достаточно таких гаджетов, что это не имеет большого значения.
Но да, это незначительное возражение против этого, которое действительно допустимо, в отличие от других ваших пунктов.