Código independiente de la posición - Position-independent code
En informática , el código independiente de la posición ( PIC ) o el ejecutable independiente de la posición ( PIE ) es un cuerpo de código de máquina que, al estar ubicado en algún lugar de la memoria primaria , se ejecuta correctamente independientemente de su dirección absoluta . PIC se usa comúnmente para bibliotecas compartidas , de modo que el mismo código de biblioteca se puede cargar en una ubicación en el espacio de direcciones de cada programa donde no se superpone con otra memoria en uso (por ejemplo, otras bibliotecas compartidas). PIC también se utilizó en sistemas informáticos más antiguos que carecían de una MMU , de modo que el sistema operativo podía mantener las aplicaciones alejadas entre sí incluso dentro del espacio de direcciones único de un sistema sin MMU.
El código independiente de la posición se puede ejecutar en cualquier dirección de memoria sin modificación. Esto difiere del código absoluto , que debe cargarse en una ubicación específica para funcionar correctamente, y del código localizable en tiempo de carga (LTL), en el que un vinculador o cargador de programas modifica un programa antes de su ejecución para que pueda ejecutarse solo desde una memoria en particular. localización. La generación de código independiente de la posición suele ser el comportamiento predeterminado de los compiladores , pero pueden imponer restricciones al uso de algunas características del lenguaje, como no permitir el uso de direcciones absolutas (el código independiente de la posición tiene que utilizar direccionamiento relativo ). Las instrucciones que se refieren directamente a direcciones de memoria específicas a veces se ejecutan más rápido, y reemplazarlas con instrucciones equivalentes de direccionamiento relativo puede resultar en una ejecución ligeramente más lenta, aunque los procesadores modernos hacen que la diferencia sea prácticamente insignificante.
Historia
En los primeros ordenadores, como el IBM 701 (29 de abril de 1952) o el UNIVAC I (31 de marzo de 1951), el código dependía de la posición: cada programa se creó para cargarse y ejecutarse desde una dirección particular. Esas primeras computadoras no tenían un sistema operativo y no eran capaces de realizar múltiples tareas. Los programas se cargaron en el almacenamiento principal (o incluso se almacenaron en un tambor magnético para su ejecución directamente desde allí) y se ejecutaron uno a la vez. En tal contexto operativo, no era necesario un código independiente de la posición.
El IBM System / 360 (7 de abril de 1964) se diseñó con direccionamiento truncado similar al de UNIVAC III , teniendo en cuenta la independencia de la posición del código. En el direccionamiento truncado, las direcciones de memoria se calculan a partir de un registro base y un desplazamiento. Al comienzo de un programa, el programador debe establecer la direccionabilidad cargando un registro base; normalmente, el programador también informa al ensamblador con una pseudo-operación USANDO . El programador puede cargar el registro base desde un registro que se sabe que contiene la dirección del punto de entrada, normalmente R15, o puede usar la instrucción BALR (rama y enlace, formulario de registro) (con un valor R2 de 0) para almacenar la siguiente dirección secuencial de la instrucción. en el registro base, que luego se codificó explícita o implícitamente en cada instrucción que se refería a una ubicación de almacenamiento dentro del programa. Se pueden usar múltiples registros de base, para código o para datos. Dichas instrucciones requieren menos memoria porque no tienen que contener una dirección completa de 24, 31, 32 o 64 bits (4 u 8 bytes), sino un número de registro base (codificado en 4 bits) y un desplazamiento de dirección de 12 bits. (codificado en 12 bits), requiriendo solo dos bytes.
Esta técnica de programación es estándar en los sistemas de tipo IBM S / 360. Ha estado en uso hasta IBM System / z de hoy. Al codificar en lenguaje ensamblador, el programador tiene que establecer la direccionabilidad del programa como se describió anteriormente y también usar otros registros base para el almacenamiento asignado dinámicamente. Los compiladores se encargan automáticamente de este tipo de direccionamiento.
El primer sistema operativo de IBM, DOS / 360 (1966), no utilizaba almacenamiento virtual (ya que los primeros modelos de System S / 360 no lo admitían), pero tenía la capacidad de colocar programas en una ubicación de almacenamiento arbitraria (o elegida automáticamente). durante la carga a través del nombre PHASE, declaración * JCL (Lenguaje de control de trabajos).
Por lo tanto, en los sistemas S / 360 sin almacenamiento virtual, se podía cargar un programa en cualquier ubicación de almacenamiento, pero esto requería un área de memoria contigua lo suficientemente grande para contener ese programa. A veces, la fragmentación de la memoria se produce al cargar y descargar módulos de diferentes tamaños. El almacenamiento virtual, por diseño, no tiene esa limitación.
Si bien DOS / 360 y OS / 360 no admitían PIC, las rutinas SVC transitorias en OS / 360 no podían contener constantes de direcciones reubicables y podían ejecutarse en cualquiera de las áreas transitorias sin reubicación .
El almacenamiento virtual se introdujo por primera vez en IBM System / 360 modelo 67 en (1965) para admitir el primer sistema operativo multitarea y de tiempo compartido de IBM, TSS / 360. Las versiones posteriores de DOS / 360 (DOS / VS, etc.) y los sistemas operativos IBM posteriores utilizaron almacenamiento virtual. El direccionamiento truncado permaneció como parte de la arquitectura base y sigue siendo ventajoso cuando se deben cargar varios módulos en el mismo espacio de direcciones virtuales.
Otros primeros sistemas segmentados como Burroughs MCP en Burroughs B5000 (1961) y Multics (1964), sistemas de paginación como IBM TSS / 360 (1967) o sistemas de base y límites como GECOS en GE 625 y EXEC en UNIVAC 1107 , el código también era inherentemente independiente de la posición, ya que las direcciones en un programa eran relativas al segmento actual en lugar de absolutas.
La invención de la traducción dinámica de direcciones (la función proporcionada por una MMU ) redujo originalmente la necesidad de un código independiente de la posición porque cada proceso podría tener su propio espacio de direcciones independiente (rango de direcciones). Sin embargo, varios trabajos simultáneos que usaban el mismo código generaban un desperdicio de memoria física. Si dos trabajos ejecutan programas completamente idénticos, la traducción dinámica de direcciones proporciona una solución al permitir que el sistema simplemente asigne la dirección 32K de dos trabajos diferentes a los mismos bytes de memoria real, que contienen una única copia del programa.
Los diferentes programas pueden compartir un código común. Por ejemplo, el programa de nómina y el programa de cuentas por cobrar pueden contener una subrutina de clasificación idéntica. Un módulo compartido (una biblioteca compartida es una forma de módulo compartido) se carga una vez y se asigna a los dos espacios de direcciones.
Detalles técnicos
Llamadas de procedimiento dentro de una biblioteca compartida se hacen típicamente a través de una pequeña mesa procedimiento de vinculación talones , que luego llaman a la función definitiva. En particular, esto permite que una biblioteca compartida herede ciertas llamadas a funciones de bibliotecas cargadas previamente en lugar de usar sus propias versiones.
Las referencias de datos del código independiente de la posición generalmente se hacen de manera indirecta, a través de tablas de compensación global (GOT), que almacenan las direcciones de todas las variables globales a las que se accede . Hay un GOT por unidad de compilación o módulo de objeto, y está ubicado en un desplazamiento fijo del código (aunque este desplazamiento no se conoce hasta que se vincula la biblioteca ). Cuando un vinculador vincula módulos para crear una biblioteca compartida, fusiona los GOT y establece las compensaciones finales en el código. No es necesario ajustar las compensaciones al cargar la biblioteca compartida más tarde.
Las funciones independientes de posición que acceden a datos globales comienzan determinando la dirección absoluta del GOT dado su propio valor de contador de programa actual. Esto a menudo toma la forma de una llamada de función falsa para obtener el valor de retorno en la pila ( x86 ), en un registro estándar específico ( SPARC , MIPS ) o un registro especial ( POWER / PowerPC / Power ISA ), que luego puede ser trasladado a un registro estándar predefinidos, o para obtener en ese registro estándar ( PA-RISC , Alfa , eSA / 390 y z / Architecture ). Algunas arquitecturas de procesador, como Motorola 68000 , Motorola 6809 , WDC 65C816 , Knuth's MMIX , ARM y x86-64 permiten referenciar datos por compensación del contador del programa . Esto tiene como objetivo específico hacer que el código independiente de la posición sea más pequeño, menos exigente en cuanto a registros y, por lo tanto, más eficiente.
DLL de Windows
Las bibliotecas de vínculos dinámicos (DLL) en Microsoft Windows utilizan la variante E8 de la instrucción CALL (llamada cercana, relativa, desplazamiento relativo a la siguiente instrucción). No es necesario corregir estas instrucciones cuando se carga una DLL.
Se espera que algunas variables globales (por ejemplo, matrices de cadenas literales, tablas de funciones virtuales) contengan una dirección de un objeto en la sección de datos respectivamente en la sección de código de la biblioteca dinámica; por lo tanto, la dirección almacenada en la variable global debe actualizarse para reflejar la dirección donde se cargó la DLL. El cargador dinámico calcula la dirección a la que hace referencia una variable global y almacena el valor en dicha variable global; esto activa la copia al escribir de una página de memoria que contiene dicha variable global. Las páginas con código y las páginas con variables globales que no contienen punteros al código o datos globales permanecen compartidas entre procesos. Esta operación debe realizarse en cualquier sistema operativo que pueda cargar una biblioteca dinámica en una dirección arbitraria.
En Windows Vista y versiones posteriores de Windows, la reubicación de archivos DLL y ejecutables la realiza el administrador de memoria del kernel, que comparte los binarios reubicados en varios procesos. Las imágenes siempre se reubican desde sus direcciones base preferidas, logrando la aleatorización del diseño del espacio de direcciones (ASLR).
Las versiones de Windows anteriores a Vista requieren que los archivos DLL del sistema estén preenlazados en direcciones fijas no conflictivas en el momento del enlace para evitar la reubicación de imágenes en tiempo de ejecución. El cargador de DLL realiza la reubicación en tiempo de ejecución en estas versiones anteriores de Windows dentro del contexto de cada proceso, y las porciones reubicadas resultantes de cada imagen ya no se pueden compartir entre procesos.
El manejo de archivos DLL en Windows difiere del procedimiento anterior de OS / 2 del que deriva. OS / 2 presenta una tercera alternativa e intenta cargar archivos DLL que no son independientes de la posición en una "arena compartida" dedicada en la memoria y los asigna una vez que se cargan. Todos los usuarios de la DLL pueden utilizar la misma copia en memoria.
Multics
En Multics, cada procedimiento tiene conceptualmente un segmento de código y un segmento de enlace. El segmento de código contiene solo código y la sección de vinculación sirve como plantilla para un nuevo segmento de vinculación. El registro de puntero 4 (PR4) apunta al segmento de enlace del procedimiento. Una llamada a un procedimiento guarda PR4 en la pila antes de cargarlo con un puntero al segmento de vinculación del destinatario. La llamada de procedimiento utiliza un par de punteros indirectos con una bandera para provocar una trampa en la primera llamada de modo que el mecanismo de enlace dinámico pueda agregar el nuevo procedimiento y su segmento de enlace a la Tabla de segmentos conocidos (KST), construir un nuevo segmento de enlace, poner sus números de segmento en la sección de vinculación de la persona que llama y restablecer la bandera en el par de punteros indirectos.
TSS
En IBM S / 360 Time Sharing System (TSS / 360 y TSS / 370) cada procedimiento puede tener un CSECT público de solo lectura y una Sección de Prototipo privada de escritura (PSECT). Una persona que llama carga una constante V para la rutina en el Registro general 15 (GR15) y copia una constante R para el PSECT de la rutina en la palabra 19 del área de guardado que apunta a GR13.
Dynamic Loader no carga páginas de programa ni resuelve constantes de dirección hasta el fallo de la primera página.
Ejecutables independientes de la posición
Los ejecutables independientes de la posición (PIE) son binarios ejecutables hechos completamente de código independiente de la posición. Si bien algunos sistemas solo ejecutan ejecutables PIC, existen otras razones por las que se utilizan. Los binarios PIE se utilizan en algunas distribuciones de Linux centradas en la seguridad para permitir que PaX o Exec Shield utilicen la aleatorización del diseño del espacio de direcciones para evitar que los atacantes sepan dónde está el código ejecutable existente durante un ataque de seguridad utilizando exploits que se basan en conocer el desplazamiento del código ejecutable en el binario, como los ataques de retorno a libc .
MacOS e iOS de Apple son totalmente compatibles con los ejecutables PIE a partir de las versiones 10.7 y 4.3, respectivamente; Se emite una advertencia cuando los ejecutables de iOS que no son PIE se envían para su aprobación a la App Store de Apple, pero aún no hay un requisito estricto y las aplicaciones que no son PIE no se rechazan.
OpenBSD tiene PIE habilitado de forma predeterminada en la mayoría de las arquitecturas desde OpenBSD 5.3, lanzado el 1 de mayo de 2013. El soporte para PIE en binarios vinculados estáticamente , como los ejecutables en /biny /sbindirectorios, se agregó a finales de 2014. openSUSE agregó PIE como predeterminado en 2015-02. Comenzando con Fedora 23, los mantenedores de Fedora decidieron construir paquetes con PIE habilitado como predeterminado. Ubuntu 17.10 tiene PIE habilitado de forma predeterminada en todas las arquitecturas. Los nuevos perfiles de Gentoo ahora soportan PIE por defecto. Alrededor de julio de 2017, Debian habilitó PIE de forma predeterminada.
Android habilitó la compatibilidad con PIE en Jelly Bean y eliminó la compatibilidad con enlazadores que no son PIE en Lollipop .