En informática , el código independiente de la posición [1] ( PIC [1] ) o ejecutable independiente de la posición ( PIE ) [2] es un cuerpo de código de máquina que se ejecuta correctamente independientemente de su dirección de memoria . [a] El 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 superponga con otra memoria en uso por, por ejemplo, otras bibliotecas compartidas. El PIC también se usó en sistemas informáticos más antiguos que carecían de una MMU , [3] de modo que el sistema operativo pudiera mantener las aplicaciones alejadas entre sí incluso dentro del único espacio de direcciones de un sistema sin MMU.
El código independiente de la posición se puede ejecutar en cualquier dirección de memoria sin modificaciones. Esto difiere del código absoluto, [1] que debe cargarse en una ubicación específica para funcionar correctamente, [1] y el código localizable en tiempo de carga (LTL), [1] en el que un enlazador o cargador de programas modifica un programa antes de la ejecución, por lo que solo se puede ejecutar desde una ubicación de memoria particular. [1] Generar código independiente de la posición es a menudo el comportamiento predeterminado para los compiladores , pero pueden imponer restricciones en el 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 usar 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. [4]
En los primeros ordenadores, como el IBM 701 [5] (29 de abril de 1952) o el UNIVAC I (31 de marzo de 1951), el código no era independiente de la posición: cada programa se creaba para cargarse y ejecutarse desde una dirección determinada. Esos primeros ordenadores no tenían un sistema operativo y no eran capaces de realizar múltiples tareas. Los programas se cargaban en la memoria principal (o incluso se almacenaban en un tambor magnético para su ejecución directa desde allí) y se ejecutaban de uno en uno. En un contexto operativo de este tipo, el código independiente de la posición no era necesario.
Incluso en sistemas base y límites [b] como el CDC 6600 , el GE 625 y el UNIVAC 1107 , una vez que el sistema operativo cargaba el código en el almacenamiento de un trabajo, solo podía ejecutarse desde la dirección relativa en la que se había cargado.
Burroughs introdujo un sistema segmentado , el B5000 (1961), en el que los programas direccionaban segmentos indirectamente a través de palabras de control en la pila o en la tabla de referencia de programa (PRT); un segmento compartido podía ser direccionado a través de diferentes ubicaciones de PRT en diferentes procesos. De manera similar, en el posterior B6500 , todas las referencias de segmento se hacían a través de posiciones en un marco de pila .
El IBM System/360 (7 de abril de 1964) fue diseñado con direccionamiento truncado similar al del UNIVAC III , [6] teniendo en mente 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 pseudooperación USING . 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 utilizar la instrucción BALR (Branch And Link, Register form) (con un valor R2 de 0) para almacenar la dirección de la siguiente instrucción secuencial en el registro base, que luego se codificaba explícita o implícitamente en cada instrucción que hacía referencia a una ubicación de almacenamiento dentro del programa. Se podían utilizar múltiples registros base, para código o para datos. Estas 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 tipo IBM S/360 y se ha utilizado hasta el actual IBM System/z. Al codificar en lenguaje ensamblador, el programador tiene que establecer la direccionabilidad del programa como se describió anteriormente y también utilizar 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 del System S/360 no lo soportaban), 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 de la declaración JCL (Job Control Language, nombre PHASE).
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 como para albergar ese programa. A veces se producía fragmentación de la memoria debido a la carga y descarga de módulos de distintos 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 dirección reubicables y podían ejecutarse en cualquiera de las áreas transitorias sin reubicación .
IBM introdujo por primera vez el almacenamiento virtual en el IBM System/360 modelo 67 en 1965 para dar soporte al 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 posteriores de IBM utilizaron almacenamiento virtual. El direccionamiento truncado permaneció como parte de la arquitectura base y sigue siendo ventajoso cuando se deben cargar múltiples módulos en el mismo espacio de direcciones virtuales.
A modo de comparación, en los primeros sistemas segmentados , como Burroughs MCP en Burroughs B5000 (1961) y Multics (1964), y en sistemas de paginación como IBM TSS/360 (1967) [c] , el código también era inherentemente independiente de la posición, ya que las direcciones virtuales de subrutina en un programa estaban ubicadas en datos privados externos al código, por ejemplo, tabla de referencia del programa, segmento de enlace, sección de prototipo.
La invención de la traducción dinámica de direcciones (la función proporcionada por una MMU ) redujo originalmente la necesidad de código independiente de la posición porque cada proceso podía tener su propio espacio de direcciones independiente (rango de direcciones). Sin embargo, múltiples trabajos simultáneos que utilizaban el mismo código creaban 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 de 32K de dos trabajos diferentes a los mismos bytes de memoria real, que contienen la única copia del programa.
Diferentes programas pueden compartir un código común. Por ejemplo, el programa de nóminas y el programa de cuentas por cobrar pueden contener una subrutina de ordenació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.
Las llamadas a procedimientos dentro de una biblioteca compartida se realizan normalmente a través de pequeños fragmentos de tabla de enlace de procedimientos (PLT) , que luego invocan la función definitiva. Esto permite, en particular, que una biblioteca compartida herede ciertas llamadas a funciones de bibliotecas cargadas previamente en lugar de utilizar sus propias versiones. [7]
Las referencias a datos de código independiente de la posición se realizan generalmente de forma indirecta, a través de tablas de desplazamiento global (GOT), que almacenan las direcciones de todas las variables globales a las que se accede . Hay una GOT por unidad de compilación o módulo de objeto, y se encuentra en un desplazamiento fijo con respecto al código (aunque este desplazamiento no se conoce hasta que se vincula la biblioteca ). Cuando un enlazador vincula módulos para crear una biblioteca compartida, fusiona las GOT y establece los desplazamientos finales en el código. No es necesario ajustar los desplazamientos al cargar la biblioteca compartida más tarde. [7]
Las funciones independientes de la 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 se puede mover a un registro estándar predefinido, o para obtenerlo en ese registro estándar ( PA-RISC , Alpha , ESA/390 y z/Architecture ). Algunas arquitecturas de procesador, como Motorola 68000 , ARM , x86-64 , versiones más nuevas de z/Architecture, Motorola 6809 , WDC 65C816 y MMIX de Knuth permiten hacer referencia a los datos por desplazamiento desde el contador de programa . Esto está específicamente dirigido a 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.
Las bibliotecas de vínculos dinámicos (DLL) de Microsoft Windows utilizan la variante E8 de la instrucción CALL (llamada cercana, relativa, desplazamiento relativo a la siguiente instrucción). Estas instrucciones no necesitan modificación cuando se carga la DLL.
Se espera que algunas variables globales (por ejemplo, matrices de literales de cadena, 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 en escritura 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 a 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 núcleo, que comparte los archivos binarios reubicados entre varios procesos. Las imágenes siempre se reubican desde sus direcciones base preferidas, lo que permite lograr la aleatorización del diseño del espacio de direcciones (ASLR). [8]
Las versiones de Windows anteriores a Vista requieren que las DLL del sistema se vinculen previamente en direcciones fijas que no generen conflictos en el momento del vínculo para evitar la reubicación de imágenes en tiempo de ejecución. La reubicación en tiempo de ejecución en estas versiones anteriores de Windows la realiza el cargador de DLL dentro del contexto de cada proceso, y las partes reubicadas resultantes de cada imagen ya no se pueden compartir entre procesos.
El manejo de las DLL en Windows difiere del procedimiento anterior de OS/2 del que deriva. OS/2 presenta una tercera alternativa e intenta cargar las DLL que no son independientes de la posición en una "zona compartida" dedicada en la memoria y las asigna una vez cargadas. Todos los usuarios de la DLL pueden usar la misma copia en memoria.
En Multics, cada procedimiento conceptualmente [d] tiene un segmento de código y un segmento de enlace. [9] [10] El segmento de código contiene solo código y la sección de enlace sirve como plantilla para un nuevo segmento de enlace. 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 enlace del llamado. La llamada al procedimiento utiliza un par de punteros indirectos [11] con un indicador para causar 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 enlace del llamador y restablecer el indicador en el par de punteros indirectos.
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 escribible (PSECT). Un llamador 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. [12]
El cargador dinámico [13] no carga páginas de programa ni resuelve constantes de dirección hasta que se produce el primer fallo de página.
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 usan. Los binarios PIE se usan en algunas distribuciones de Linux enfocadas en la seguridad para permitir que PaX o Exec Shield usen la aleatorización del diseño del espacio de direcciones (ASLR) para evitar que los atacantes sepan dónde se encuentra 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 . (El núcleo oficial de Linux desde la versión 2.6.12 de 2005 tiene una ASLR más débil que también funciona con PIE. Es débil en el sentido de que la aleatoriedad se aplica a unidades de archivos ELF completas). [14]
Los sistemas operativos macOS e iOS de Apple son totalmente compatibles con ejecutables PIE a partir de las versiones 10.7 y 4.3, respectivamente; se emite una advertencia cuando se envían ejecutables iOS que no son PIE para su aprobación a la App Store de Apple, pero aún no hay un requisito estricto [ ¿cuándo? ] y las aplicaciones que no son PIE no se rechazan. [15] [16]
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. [17] El soporte para PIE en binarios enlazados estáticamente , como los ejecutables en directorios /bin
y /sbin
, se agregó cerca del final de 2014. [18] openSUSE agregó PIE como predeterminado en 2015-02. A partir de Fedora 23, los mantenedores de Fedora decidieron crear paquetes con PIE habilitado como predeterminado. [19] Ubuntu 17.10 tiene PIE habilitado de forma predeterminada en todas las arquitecturas. [20] Los nuevos perfiles de Gentoo ahora admiten PIE de forma predeterminada. [21] Alrededor de julio de 2017, Debian habilitó PIE de forma predeterminada. [22]
Android habilitó el soporte para PIE en Jelly Bean [23] y eliminó el soporte de enlazadores que no son PIE en Lollipop . [24]
[…] El código absoluto y un módulo de objeto absoluto son códigos que LOC86 ha procesado para ejecutarse solo en una ubicación específica de la memoria. El cargador carga un módulo de objeto absoluto solo en la ubicación específica que debe ocupar el módulo. El código independiente de la posición (comúnmente conocido como PIC) se diferencia del código absoluto en que el PIC se puede cargar en cualquier ubicación de memoria. La ventaja del PIC sobre el código absoluto es que el PIC no requiere que reserve un bloque específico de memoria. Cuando el cargador carga el PIC, obtiene segmentos de memoria iRMX 86 del grupo del trabajo de la tarea que realiza la llamada y carga el PIC en los segmentos. Una restricción que afecta a PIC es que, como en el modelo de segmentación PL/M-86 COMPACT […], puede tener solo un segmento de código y un segmento de datos, en lugar de dejar que las direcciones base de estos segmentos, y por lo tanto los segmentos mismos, varíen dinámicamente. Esto significa que los programas PIC tienen necesariamente menos de 64K bytes de longitud. El código PIC se puede producir mediante el control BIND de LINK86. El código localizable en tiempo de carga (comúnmente conocido como código LTL) es la tercera forma de código objeto. El código LTL es similar a PIC en que el código LTL se puede cargar en cualquier lugar de la memoria. Sin embargo, al cargar código LTL, el cargador cambia la parte base de los punteros para que los punteros sean independientes del contenido inicial de los registros en el microprocesador. Debido a esta corrección (ajuste de las direcciones base), el código LTL se puede utilizar en tareas que tienen más de un segmento de código o más de un segmento de datos. Esto significa que los programas LTL pueden tener más de 64K bytes de longitud. FORTRAN 86 y Pascal 86 generan automáticamente código LTL, incluso para programas cortos. El código LTL se puede generar mediante el control BIND de LINK86. […]
[…] El direccionamiento directo sin reconocimiento de PIC siempre es más económico (léase: más rápido) que el direccionamiento con PIC. […]