В вычислительной технике позиционно-независимый код [1] ( PIC [1] ) или позиционно-независимый исполняемый файл ( PIE ) [2] представляет собой часть машинного кода , которая выполняется должным образом независимо от адреса его памяти . [a] PIC обычно используется для разделяемых библиотек , так что один и тот же код библиотеки может быть загружен в такое место в адресном пространстве каждой программы, где он не перекрывается с другой памятью, используемой, например, другими разделяемыми библиотеками. PIC также использовался в старых компьютерных системах, в которых отсутствовал MMU , [3] так что операционная система могла держать приложения подальше друг от друга даже в пределах одного адресного пространства системы без MMU.
Позиционно-независимый код может быть выполнен по любому адресу памяти без изменений. Это отличается от абсолютного кода, [1] который должен быть загружен в определенное место для корректной работы, [1] и кода, локируемого во время загрузки (LTL), [1] в котором компоновщик или загрузчик программ изменяет программу перед выполнением, поэтому ее можно запустить только из определенного места памяти. [1] Генерация позиционно-независимого кода часто является поведением по умолчанию для компиляторов , но они могут накладывать ограничения на использование некоторых языковых функций, таких как запрет использования абсолютных адресов (позиционно-независимый код должен использовать относительную адресацию ). Инструкции, которые напрямую ссылаются на определенные адреса памяти, иногда выполняются быстрее, и замена их эквивалентными инструкциями относительной адресации может привести к немного более медленному выполнению, хотя современные процессоры делают разницу практически незаметной. [4]
В ранних компьютерах, таких как IBM 701 [5] (29 апреля 1952 г.) или UNIVAC I (31 марта 1951 г.), код не был позиционно-независимым: каждая программа была создана для загрузки и запуска с определенного адреса. Эти ранние компьютеры не имели операционной системы и не были способны к многозадачности. Программы загружались в основную память (или даже хранились на магнитном барабане для выполнения непосредственно оттуда) и запускались по одной. В таком операционном контексте позиционно-независимый код не был необходим.
Даже в базовых и граничных [b] системах, таких как CDC 6600 , GE 625 и UNIVAC 1107 , после загрузки ОС кода в хранилище задания он мог выполняться только с того относительного адреса, по которому он был загружен.
Burroughs представил сегментированную систему B5000 (1961), в которой программы обращались к сегментам косвенно через управляющие слова в стеке или в таблице ссылок программ (PRT); общий сегмент мог быть адресован через различные местоположения PRT в различных процессах. Аналогично, в более поздней B6500 все ссылки на сегменты были через позиции в стековом кадре .
IBM System/360 (7 апреля 1964 г.) была разработана с усеченной адресацией, аналогичной адресации UNIVAC III [6] , с учетом независимости позиции кода. При усеченной адресации адреса памяти вычисляются из базового регистра и смещения. В начале программы программист должен установить адресуемость , загрузив базовый регистр; обычно программист также информирует ассемблер с помощью псевдооперации USING . Программист может загрузить базовый регистр из регистра, который, как известно, содержит адрес точки входа, обычно R15, или может использовать инструкцию BALR (Branch And Link, Register form) (со значением R2, равным 0), чтобы сохранить адрес следующей последовательной инструкции в базовом регистре, который затем явно или неявно кодировался в каждой инструкции, которая ссылалась на место хранения в программе. Можно было использовать несколько базовых регистров, для кода или для данных. Такие инструкции требуют меньше памяти, поскольку им не нужно хранить полный 24-, 31-, 32- или 64-битный адрес (4 или 8 байтов), а вместо этого нужно хранить базовый номер регистра (закодированный в 4 битах) и 12-битное смещение адреса (закодированное в 12 битах), что требует всего два байта.
Этот метод программирования является стандартным для систем типа IBM S/360. Он использовался вплоть до сегодняшней IBM System/z. При кодировании на языке ассемблера программист должен установить адресуемость для программы, как описано выше, а также использовать другие базовые регистры для динамически выделяемой памяти. Компиляторы автоматически заботятся об этом виде адресации.
Ранняя операционная система IBM DOS/360 (1966) не использовала виртуальное хранилище (поскольку ранние модели System S/360 его не поддерживали), но имела возможность помещать программы в произвольное (или автоматически выбираемое) место хранения во время загрузки с помощью имени PHASE*, оператора JCL (Job Control Language).
Итак, в системах S/360 без виртуального хранилища программа могла быть загружена в любое место хранения, но для этого требовалась непрерывная область памяти, достаточно большая для хранения этой программы. Иногда фрагментация памяти могла происходить из-за загрузки и выгрузки модулей разного размера. Виртуальное хранилище — по замыслу — не имеет такого ограничения.
Хотя DOS/360 и OS/360 не поддерживали PIC, временные процедуры SVC в OS/360 не могли содержать перемещаемые адресные константы и могли работать в любой из переходных областей без перемещения .
IBM впервые представила виртуальное хранилище на IBM System/360 model 67 в (1965) для поддержки первой многозадачной операционной системы IBM TSS/360 с разделением времени. Более поздние версии DOS/360 (DOS/VS и т. д.) и более поздние операционные системы IBM использовали виртуальное хранилище. Усеченная адресация осталась частью базовой архитектуры и по-прежнему выгодна, когда несколько модулей должны быть загружены в одно и то же виртуальное адресное пространство.
Для сравнения, в ранних сегментированных системах, таких как Burroughs MCP на Burroughs B5000 (1961) и Multics (1964), а также в страничных системах, таких как IBM TSS/360 (1967) [c] , код также был изначально позиционно-независимым, поскольку виртуальные адреса подпрограмм в программе располагались в частных данных, внешних по отношению к коду, например, в таблице ссылок программы, сегменте связей, разделе прототипа.
Изобретение динамической трансляции адресов (функция, предоставляемая MMU ) изначально уменьшило потребность в позиционно-независимом коде, поскольку каждый процесс мог иметь свое собственное независимое адресное пространство (диапазон адресов). Однако несколько одновременных заданий, использующих один и тот же код, создавали пустую трату физической памяти. Если два задания запускают совершенно идентичные программы, динамическая трансляция адресов обеспечивает решение, позволяя системе просто сопоставлять адрес двух разных заданий 32 К с теми же байтами реальной памяти, содержащей единственную копию программы.
Разные программы могут использовать общий код. Например, программа расчета заработной платы и программа учета дебиторской задолженности могут содержать одинаковую подпрограмму сортировки. Общий модуль (общая библиотека — это форма общего модуля) загружается один раз и отображается в два адресных пространства.
Вызовы процедур внутри разделяемой библиотеки обычно осуществляются через небольшие заглушки таблиц связей процедур (PLT) , которые затем вызывают окончательную функцию. Это, в частности, позволяет разделяемой библиотеке наследовать определенные вызовы функций из ранее загруженных библиотек, а не использовать свои собственные версии. [7]
Ссылки на данные из позиционно-независимого кода обычно делаются косвенно, через таблицы глобальных смещений (GOT), которые хранят адреса всех доступных глобальных переменных . На единицу компиляции или модуль объекта приходится одна GOT, и она расположена с фиксированным смещением от кода (хотя это смещение неизвестно, пока библиотека не будет связана ). Когда компоновщик связывает модули для создания общей библиотеки, он объединяет GOT и устанавливает окончательные смещения в коде. Нет необходимости корректировать смещения при последующей загрузке общей библиотеки. [7]
Позиционно-независимые функции, получающие доступ к глобальным данным, начинаются с определения абсолютного адреса GOT, заданного их собственным текущим значением счетчика программ. Это часто принимает форму фальшивого вызова функции для получения возвращаемого значения в стеке ( x86 ), в определенном стандартном регистре ( SPARC , MIPS ) или специальном регистре ( POWER / PowerPC / Power ISA ), которое затем можно переместить в предопределенный стандартный регистр или получить его в этом стандартном регистре ( PA-RISC , Alpha , ESA/390 и z/Architecture ). Некоторые архитектуры процессоров, такие как Motorola 68000 , ARM , x86-64 , более новые версии z/Architecture, Motorola 6809 , WDC 65C816 и MMIX Кнута , позволяют ссылаться на данные по смещению от счетчика программ . Это специально нацелено на то, чтобы сделать позиционно-независимый код меньше, менее требовательным к регистрам и, следовательно, более эффективным.
Динамически подключаемые библиотеки (DLL) в Microsoft Windows используют вариант E8 инструкции CALL (Call near, relation, offset related to next instructions). Эти инструкции не требуют модификации при загрузке DLL.
Некоторые глобальные переменные (например, массивы строковых литералов, таблицы виртуальных функций) должны содержать адрес объекта в разделе данных, соответственно, в разделе кода динамической библиотеки; поэтому сохраненный адрес в глобальной переменной должен быть обновлен, чтобы отражать адрес, по которому была загружена DLL. Динамический загрузчик вычисляет адрес, на который ссылается глобальная переменная, и сохраняет значение в такой глобальной переменной; это запускает копирование при записи страницы памяти, содержащей такую глобальную переменную. Страницы с кодом и страницы с глобальными переменными, которые не содержат указателей на код или глобальные данные, остаются общими для процессов. Эта операция должна выполняться в любой ОС, которая может загружать динамическую библиотеку по произвольному адресу.
В Windows Vista и более поздних версиях Windows перемещение DLL и исполняемых файлов выполняется диспетчером памяти ядра, который распределяет перемещенные двоичные файлы между несколькими процессами. Образы всегда перемещаются из своих предпочтительных базовых адресов, достигая рандомизации адресного пространства (ASLR). [8]
Версии Windows до Vista требуют, чтобы системные DLL были предварительно связаны по неконфликтующим фиксированным адресам во время связывания, чтобы избежать перемещения образов во время выполнения. Перемещение во время выполнения в этих старых версиях Windows выполняется загрузчиком DLL в контексте каждого процесса, и полученные перемещенные части каждого образа больше не могут совместно использоваться процессами.
Обработка DLL в Windows отличается от более ранней процедуры OS/2 , от которой она происходит. OS/2 представляет третью альтернативу и пытается загрузить DLL, которые не являются позиционно-независимыми, в выделенную "общую арену" в памяти и отображает их после загрузки. Все пользователи DLL могут использовать одну и ту же копию в памяти.
В Multics каждая процедура концептуально [d] имеет сегмент кода и сегмент связи. [9] [10] Сегмент кода содержит только код, а раздел связи служит шаблоном для нового сегмента связи. Регистр указателя 4 (PR4) указывает на сегмент связи процедуры. Вызов процедуры сохраняет PR4 в стеке перед загрузкой его указателем на сегмент связи вызываемой стороны. Вызов процедуры использует пару косвенных указателей [11] с флагом для вызова ловушки при первом вызове, чтобы механизм динамической связи мог добавить новую процедуру и ее сегмент связи в таблицу известных сегментов (KST), построить новый сегмент связи, поместить их номера сегментов в раздел связи вызывающей стороны и сбросить флаг в паре косвенных указателей.
В системе разделения времени IBM S/360 (TSS/360 и TSS/370) каждая процедура может иметь общедоступный CSECT только для чтения и доступный для записи частный раздел прототипа (PSECT). Вызывающий загружает V-константу для процедуры в Общий регистр 15 (GR15) и копирует R-константу для PSECT процедуры в 19-е слово области сохранения, указанной как GR13. [12]
Динамический загрузчик [13] не загружает страницы программ и не разрешает константы адресов до возникновения первой ошибки страницы.
Позиционно-независимые исполняемые файлы (PIE) — это исполняемые двоичные файлы, созданные полностью из позиционно-независимого кода. Хотя некоторые системы запускают только исполняемые файлы PIC, есть и другие причины их использования. Двоичные файлы PIE используются в некоторых дистрибутивах Linux, ориентированных на безопасность , чтобы позволить PaX или Exec Shield использовать рандомизацию адресного пространства (ASLR), чтобы не дать злоумышленникам узнать, где находится существующий исполняемый код во время атаки безопасности с использованием эксплойтов , которые полагаются на знание смещения исполняемого кода в двоичном файле, например, атак return-to-libc . (Официальное ядро Linux с версии 2.6.12 от 2005 года имеет более слабый ASLR, который также работает с PIE. Он слаб тем, что случайность применяется к целым единицам файла ELF.) [14]
MacOS и iOS от Apple полностью поддерживают исполняемые файлы PIE, начиная с версий 10.7 и 4.3 соответственно; предупреждение выдается, когда исполняемые файлы iOS, не относящиеся к PIE, отправляются на одобрение в App Store от Apple, но пока нет жестких требований [ когда? ], и не относящиеся к PIE приложения не отклоняются. [15] [16]
OpenBSD включает PIE по умолчанию на большинстве архитектур с OpenBSD 5.3, выпущенной 1 мая 2013 года. [17] Поддержка PIE в статически связанных двоичных файлах, таких как исполняемые файлы в каталогах /bin
и /sbin
, была добавлена ближе к концу 2014 года. [18] openSUSE добавил PIE по умолчанию в 2015-02. Начиная с Fedora 23, сопровождающие Fedora решили собирать пакеты с включенным PIE по умолчанию. [19] Ubuntu 17.10 включает PIE по умолчанию на всех архитектурах. [20] Новые профили Gentoo теперь поддерживают PIE по умолчанию. [ 21] Примерно в июле 2017 года Debian включил PIE по умолчанию. [22]
Android включил поддержку PIE в Jelly Bean [23] и удалил поддержку не-PIE-линкеров в Lollipop . [24]
[…] Абсолютный код и абсолютный объектный модуль — это код, который был обработан LOC86 для выполнения только в определенном месте памяти. Загрузчик загружает абсолютный объектный модуль только в определенное место, которое должен занимать модуль. Позиционно-независимый код (обычно называемый PIC) отличается от абсолютного кода тем, что PIC может быть загружен в любое место памяти. Преимущество PIC над абсолютным кодом заключается в том, что PIC не требует резервирования определенного блока памяти. Когда загрузчик загружает PIC, он получает сегменты памяти iRMX 86 из пула задания вызывающей задачи и загружает PIC в сегменты. Ограничение, касающееся PIC, заключается в том, что, как и в модели сегментации PL/M-86 COMPACT […], он может иметь только один сегмент кода и один сегмент данных, вместо того, чтобы позволять базовым адресам этих сегментов, а следовательно, и самим сегментам, динамически изменяться. Это означает, что программы PIC обязательно имеют длину менее 64 Кбайт. Код PIC может быть создан с помощью элемента управления BIND LINK86. Локализуемый во время загрузки код (обычно называемый кодом LTL) является третьей формой объектного кода. Код LTL похож на PIC тем, что код LTL может быть загружен в любое место памяти. Однако при загрузке кода LTL загрузчик изменяет базовую часть указателей, так что указатели не зависят от начального содержимого регистров в микропроцессоре. Из-за этой корректировки (корректировки базовых адресов) код LTL может использоваться задачами, имеющими более одного сегмента кода или более одного сегмента данных. Это означает, что программы LTL могут иметь длину более 64 Кбайт. FORTRAN 86 и Pascal 86 автоматически создают код LTL, даже для коротких программ. Код LTL может быть создан с помощью элемента управления BIND LINK86. […]
[…] прямая адресация без поддержки PIC всегда дешевле (читай: быстрее), чем адресация PIC. […]