El formato Portable Executable ( PE ) es un formato de archivo para ejecutables , código objeto , DLL y otros utilizados en versiones de 32 bits y 64 bits de los sistemas operativos Windows y en entornos UEFI . [2] El formato PE es una estructura de datos que encapsula la información necesaria para que el cargador del sistema operativo Windows administre el código ejecutable encapsulado . Esto incluye referencias de biblioteca dinámicas para vincular , tablas de exportación e importación de API , datos de administración de recursos y datos de almacenamiento local de subprocesos (TLS). En los sistemas operativos NT , el formato PE se utiliza para EXE , DLL , SYS ( controlador de dispositivo ), MUI y otros tipos de archivos. La especificación Unified Extensible Firmware Interface (UEFI) establece que PE es el formato ejecutable estándar en entornos EFI. [3]
En los sistemas operativos Windows NT, PE actualmente admite las arquitecturas de conjunto de instrucciones (ISA) IA-32 , x86-64 (AMD64/Intel 64), IA-64 , ARM y ARM64 . Antes de Windows 2000 , Windows NT (y por lo tanto PE) admitía las ISA MIPS , Alpha y PowerPC . Debido a que PE se utiliza en Windows CE , continúa admitiendo varias variantes de las ISA MIPS, ARM (incluida Thumb ) y SuperH . [4]
Los formatos análogos a PE son ELF (utilizado en Linux y la mayoría de las otras versiones de Unix ) y Mach-O (utilizado en macOS e iOS ).
Microsoft migró al formato PE desde los formatos NE de 16 bits con la introducción del sistema operativo Windows NT 3.1 . Todas las versiones posteriores de Windows, incluyendo Windows 95/98/ME y la incorporación de Win32s a Windows 3.1x, admiten la estructura de archivos. El formato ha conservado un soporte heredado limitado para salvar la brecha entre los sistemas basados en DOS y NT. Por ejemplo, los encabezados PE/COFF aún incluyen un programa ejecutable DOS , que es por defecto un stub DOS que muestra un mensaje como "Este programa no se puede ejecutar en modo DOS" (o similar), aunque puede ser una versión DOS completa del programa (un caso notable posterior es el instalador de Windows 98 SE). El enlazador de Microsoft tiene un /STUB
interruptor para adjuntar uno. [5] Esto constituye una forma de binario fat .
PE también sigue prestando servicio a la cambiante plataforma Windows. Algunas extensiones incluyen el formato .NET PE, una versión con compatibilidad con espacio de direcciones de 64 bits denominada PE32+ y una especificación para Windows CE.
Para saber si el código ejecutable es de 32 o 64 bits, consulte el campo Machine en IMAGE_FILE_HEADER. [6] Para saber si las direcciones en el ejecutable son de 32 o 64 bits, consulte el campo Magic en IMAGE_OPTIONAL_HEADER. 10B 16 indica un archivo PE32, mientras que 20B 16 indica un archivo PE32+. [7]
Un archivo PE consta de una serie de encabezados y secciones que informan a un enlazador dinámico sobre cómo mapear el archivo en la memoria. Una imagen ejecutable consta de varias regiones diferentes, cada una de las cuales requiere una protección de memoria diferente; por lo tanto, el inicio de cada sección debe estar alineado con un límite de página. [8] Por ejemplo, normalmente la sección .text (que contiene el código del programa) se mapea como de solo lectura/ejecución, y la sección .data (que contiene las variables globales) se mapea como de no ejecución/lectura/escritura. Sin embargo, para evitar desperdiciar espacio, las diferentes secciones no están alineadas con las páginas en el disco. Parte del trabajo del enlazador dinámico es mapear cada sección a la memoria individualmente y asignar los permisos correctos a las regiones resultantes, de acuerdo con las instrucciones que se encuentran en los encabezados. [9]
Una sección que vale la pena mencionar es la tabla de direcciones de importación (IAT), que se utiliza como tabla de búsqueda cuando la aplicación llama a una función en un módulo diferente. Puede tener la forma de importación por ordinal e importación por nombre . Debido a que un programa compilado no puede conocer la ubicación de memoria de las bibliotecas de las que depende, se requiere un salto indirecto cada vez que se realiza una llamada API. A medida que el enlazador dinámico carga módulos y los une, escribe direcciones reales en las ranuras IAT, de modo que apunten a las ubicaciones de memoria de las funciones de biblioteca correspondientes. Aunque esto agrega un salto adicional sobre el costo de una llamada intra-módulo que resulta en una penalización de rendimiento, proporciona un beneficio clave: se minimiza la cantidad de páginas de memoria que el cargador debe copiar al escribir , lo que ahorra memoria y tiempo de E/S de disco. Si el compilador sabe de antemano que una llamada será inter-módulo (a través de un atributo dllimport), puede producir un código más optimizado que simplemente da como resultado un código de operación de llamada indirecta . [9]
Los archivos PE normalmente no contienen código independiente de la posición . En su lugar, se compilan en una dirección base preferida y todas las direcciones emitidas por el compilador/vinculador se fijan de antemano. Si un archivo PE no se puede cargar en su dirección preferida (porque ya está ocupada por otra cosa), el sistema operativo lo rebase . Esto implica recalcular cada dirección absoluta y modificar el código para utilizar los nuevos valores. El cargador hace esto comparando las direcciones de carga preferidas y reales y calculando un valor delta . Luego, esto se agrega a la dirección preferida para obtener la nueva dirección de la ubicación de memoria. Las reubicaciones de base se almacenan en una lista y se agregan, según sea necesario, a una ubicación de memoria existente. El código resultante ahora es privado para el proceso y ya no se puede compartir , por lo que muchos de los beneficios de ahorro de memoria de las DLL se pierden en este escenario. También ralentiza significativamente la carga del módulo. Por esta razón, se debe evitar el rebase siempre que sea posible, y las DLL enviadas por Microsoft tienen direcciones base precalculadas para no superponerse. Por lo tanto, en el caso de no realizar un rebase, PE tiene la ventaja de contar con un código muy eficiente, pero en presencia de un rebase, el impacto en el uso de memoria puede resultar costoso. Esto contrasta con ELF , donde generalmente se prefiere un código totalmente independiente de la posición a la reubicación en tiempo de carga, con lo que se sacrifica tiempo de ejecución a favor de un menor uso de memoria.
En un ejecutable .NET, la sección de código PE contiene un stub que invoca la entrada de inicio de la máquina virtual CLR_CorExeMain
, o _CorDllMain
en mscoree.dll
, de forma muy similar a como sucedía en los ejecutables de Visual Basic . La máquina virtual utiliza entonces los metadatos .NET presentes, cuya raíz IMAGE_COR20_HEADER
(también denominada "encabezado CLR") está indicada por IMAGE_DIRECTORY_ENTRY_COMHEADER
(la entrada se utilizaba anteriormente para metadatos COM+ en aplicaciones COM+, de ahí el nombre [ cita requerida ] ) entrada en el directorio de datos del encabezado PE. IMAGE_COR20_HEADER
se parece mucho al encabezado opcional de PE, y cumple esencialmente su función para el cargador CLR. [4]
Los datos relacionados con CLR, incluida la estructura raíz en sí, suelen estar contenidos en la sección de código común, .text
. Está compuesta por unos pocos directorios: metadatos, recursos integrados, nombres seguros y algunos para la interoperabilidad del código nativo. El directorio de metadatos es un conjunto de tablas que enumeran todas las entidades .NET distintas en el ensamblado, incluidos tipos, métodos, campos, constantes, eventos, así como referencias entre ellas y a otros ensamblados.
El formato PE también lo utiliza ReactOS , ya que está pensado para ser compatible a nivel binario con Windows. También lo han utilizado históricamente otros sistemas operativos, incluidos SkyOS y BeOS R3. Sin embargo, tanto SkyOS como BeOS acabaron migrando a ELF . [ cita requerida ]
Como la plataforma de desarrollo Mono pretende ser compatible a nivel binario con Microsoft .NET Framework , utiliza el mismo formato PE que la implementación de Microsoft. Lo mismo ocurre con el propio .NET Core multiplataforma de Microsoft .
En sistemas operativos tipo Unix x86 (-64) , los binarios de Windows (en formato PE) se pueden ejecutar con Wine . HX DOS Extender también utiliza el formato PE para binarios nativos de DOS de 32 bits, además de que puede, hasta cierto punto, ejecutar binarios de Windows existentes en DOS, actuando así como un equivalente de Wine para DOS.
Mac OS X 10.5 tiene la capacidad de cargar y analizar archivos PE, pero no es compatible binariamente con Windows. [10]
El firmware UEFI y EFI utilizan archivos ejecutables portátiles, así como la convención de llamada Windows ABI x64 para aplicaciones .
... Steven Edwards describe el descubrimiento de que Leopard aparentemente contiene un cargador no documentado para ejecutables portátiles, un tipo de archivo utilizado en versiones de 32 y 64 bits de Windows. Una investigación más exhaustiva reveló que el propio cargador de Leopard intenta encontrar archivos DLL de Windows cuando intenta cargar un binario de Windows.