¿Alguna vez te has preguntado qué sucede en el momento en que presionas el botón de encendido de tu computadora? Detrás de esa breve pausa, antes de que se encienda tu pantalla, se están llevando a cabo una serie compleja de procesos. Este artículo se sumergirá en el fascinante mundo del firmware , explorando cómo interactúan los diferentes componentes durante el proceso de arranque .
Al comprender estas conexiones, obtendrá una imagen más clara de los elementos fundamentales que dan vida a su sistema. Nos centraremos principalmente en la arquitectura Intel x86 , pero muchos principios también se aplican a otras arquitecturas.
Si te perdiste la primera parte de nuestra serie, haz clic aquí para ponerte al día. Ahora, descubramos los misterios detrás del firmware.
Para entender cómo interactúan los componentes del firmware, exploraremos toda la arquitectura con todas sus partes conectadas. El flujo de ejecución, que se muestra en el diagrama a continuación, comienza desde el vector de reinicio , que forma parte del cargador de arranque de primera etapa . A partir de allí, avanza a través de varias etapas del firmware:
El firmware o BIOS generalmente se puede dividir en dos partes principales, con una interfaz típicamente mínima entre ellas:
El diseño del firmware de la plataforma puede ser monolítico , combinando la inicialización del hardware y la funcionalidad de arranque, o puede seguir un flujo de arranque modular y por etapas. La elección del diseño depende de los requisitos del sistema y puede ser preferible para determinados dispositivos.
El siguiente diagrama ilustra cómo interactúan los diferentes componentes del firmware y cómo pueden usarse juntos para respaldar el proceso de arranque (las flechas indican la secuencia de ejecución):
Si estos diagramas te parecen complejos ahora, no te preocupes. Vuelve a revisarlos después de leer este artículo y te resultarán más claros.
Este firmware está diseñado para inicializar computadoras y sistemas integrados con un enfoque en la inicialización mínima del hardware : hacer solo lo que es absolutamente necesario y luego pasar el control al cargador de arranque de segunda etapa para iniciar el sistema operativo. El FSBL no carga sistemas operativos desde medios de almacenamiento que no sean el chip flash . Dado que solo inicializa el hardware subyacente y no maneja medios de arranque como discos duros, SSD o unidades flash USB, se requiere otra pieza de software para iniciar realmente un sistema operativo .
Responsabilidades clave de FSBL :
En los primeros tiempos de la informática, el software de código abierto no era muy popular y la mayoría de las implementaciones de BIOS eran propietarias. Solo hay unas pocas soluciones abiertas disponibles que proporcionan código fuente de BIOS POST, como Super PC/Turbo XT BIOS y GLaBIOS . Estos proyectos fueron diseñados para funcionar en sistemas IBM 5150/5155/5160 y la mayoría de los clones XT.
Sin embargo, las implementaciones de BIOS de código abierto más conocidas, como OpenBIOS y SeaBIOS , no realizan la inicialización del hardware porque no están diseñadas para ejecutarse en hardware desnudo. Pero se usan ampliamente como cargadores de arranque de segunda etapa y se ejecutan de forma nativa en entornos virtuales como QEMU y Bochs.
En cualquier caso, es poco probable que necesites trabajar directamente con estas primeras BIOS o profundizar en sus particularidades. Pero si te interesa explorar, los repositorios mencionados son un buen punto de partida.
En cuanto a las tendencias de desarrollo actuales, no parece haber un desarrollo continuo de soluciones BIOS propietarias, y dichos proyectos se han vuelto obsoletos frente a las alternativas modernas.
El proceso de arranque sigue un flujo escalonado, que comienza desde la izquierda y avanza hacia la derecha en la siguiente figura. La línea de tiempo del proceso de arranque de la plataforma se divide en las siguientes fases, como se indica mediante cuadros amarillos:
ExitBootServices()
.
Este proceso y sus fases de ejecución están contemplados en la Especificación de Inicialización de la Plataforma UEFI (PI) . Sin embargo, también existe la Interfaz UEFI (indicada por la línea azul en negrita en la imagen), que no forma parte del documento anterior y se describe en la Especificación UEFI . Aunque los nombres y el uso frecuente de UEFI pueden resultar confusos, estos dos documentos tienen enfoques diferentes:
Básicamente, ambas especificaciones tratan sobre interfaces, pero a diferentes niveles. Para obtener información detallada, puede acceder a ambas especificaciones en el sitio web del Foro UEFI .
UEFI PI se diseñó inicialmente como una solución de firmware unificada, sin tener en cuenta la distinción entre cargadores de arranque de primera y segunda etapa. Sin embargo, cuando nos referimos a UEFI como cargador de arranque de primera etapa , incluye las fases SEC , PEI y DXE temprana . La razón por la que dividimos DXE en etapas tempranas y tardías se debe a sus diferentes funciones en el proceso de inicialización.
En la fase inicial de DXE , los controladores suelen realizar la inicialización esencial de la CPU/PCH/placa y también generan protocolos arquitectónicos (AP) de DXE , que ayudan a aislar la fase de DXE del hardware específico de la plataforma. Los AP encapsulan los detalles específicos de la plataforma, lo que permite que la fase tardía de DXE funcione independientemente de las especificaciones del hardware.
Próximamente publicaré artículos detallados sobre cómo funciona Coreboot. ¡Sigue mis redes sociales, se publicarán muy pronto!
Una vez finalizada la configuración inicial del hardware, entra en juego la segunda etapa , cuya función principal es configurar una interfaz de software entre el sistema operativo y el firmware de la plataforma , garantizando que el SO pueda administrar los recursos del sistema e interactuar con los componentes del hardware.
El objetivo de SSBL es ocultar las variaciones de hardware tanto como sea posible, simplificando el desarrollo de sistemas operativos y aplicaciones al manejar la mayoría de las interfaces a nivel de hardware. Esta abstracción permite a los desarrolladores centrarse en funcionalidades de nivel superior sin preocuparse por las diferencias de hardware subyacentes.
Responsabilidades clave de SSBL :
Recuperación de información de la plataforma : obtiene información específica de la plataforma del cargador de arranque de primera etapa , incluido el mapeo de memoria, SMBIOS, tablas ACPI, flash SPI, etc.
Ejecutar controladores independientes de la plataforma : incluye controladores para SMM, SPI, PCI, SCSI/ATA/IDE/DISK, USB, ACPI, interfaces de red, etc.
Implementación de servicios (también conocida como interfaz) : proporciona un conjunto de servicios que facilitan la comunicación entre el sistema operativo y los componentes de hardware.
Menú de configuración : ofrece un menú de configuración para la configuración del sistema, permitiendo a los usuarios ajustar configuraciones relacionadas con el orden de arranque, preferencias de hardware y otros parámetros del sistema.
Lógica de arranque : mecanismo para localizar y cargar la carga útil (probablemente el sistema operativo) desde el medio de arranque disponible.
La interfaz del BIOS se conoce como servicios/funciones/llamadas de interrupción del BIOS . Estas funciones proporcionan un conjunto de rutinas para el acceso al hardware, pero los detalles específicos de cómo se ejecutan en el hardware particular del sistema están ocultos para el usuario.
En el modo real de 16 bits, se puede acceder a ellos fácilmente invocando una interrupción de software mediante una instrucción en lenguaje ensamblador INT x86. En el modo protegido de 32 bits, casi todos los servicios del BIOS no están disponibles debido a la forma diferente en que se manejan los valores de los segmentos .
Tomemos como ejemplo Disk Services ( INT 13h
), que proporciona servicios de lectura y escritura de discos duros y disquetes basados en sectores mediante el direccionamiento Cylinder-Head-Sector (CHS) , como ejemplo de cómo se puede utilizar esta interfaz. Supongamos que queremos leer 2 sectores (1024 bytes) y cargarlos en la dirección de memoria 0x9020 ; en ese caso, se podría ejecutar el siguiente código:
mov $0x02, %ah # Set BIOS read sector routine mov $0x00, %ch # Select cylinder 0 mov $0x00, %dh # Select head 0 [has a base of 0] mov $0x02, %cl # Select sector 2 (next after the # boot sector) [has a base of 1] mov $0x02, %al # Read 2 sectors mov $0x00, %bx # Set BX general register to 0 mov %bx, %es # Set ES segment register to 0 mov $0x9020, %bx # Load sectors to ES:BX (0:0x9020) int $0x13 # Start reading from drive jmp $0x9020 # Jump to loaded code
Si está interesado en cómo está escrito este servicio en SeaBios, eche un vistazo a src/disk.c .
Comienza a buscar un dispositivo de arranque (puede ser un disco duro, un CD-ROM, un disquete, etc.) leyendo el primer sector de 512 bytes (sector cero) de los dispositivos.
La secuencia de arranque en el BIOS carga el primer registro de arranque maestro (MBR) válido que encuentra en la memoria física de la computadora en la dirección física 0x7C00 (pista: 0x0000:0x7c00 y 0x7c0:0x0000 se refieren a la misma dirección física).
El BIOS transfiere el control a los primeros 512 bytes de la carga útil. Este sector cargado , que es demasiado pequeño para contener todo el código de la carga útil, sirve para cargar el resto de la carga útil desde el dispositivo de arranque . En este punto, la carga útil puede utilizar la interfaz expuesta por el BIOS.
Cabe destacar que en los primeros tiempos no existían especificaciones de BIOS . La BIOS es un estándar de facto : funciona de la misma manera que en las computadoras IBM reales, en la década de 1980. El resto de los fabricantes simplemente hicieron ingeniería inversa y crearon BIOS compatibles con IBM. Como resultado, no había ninguna regulación que impidiera a los fabricantes de BIOS inventar nuevas funciones de BIOS o tener funcionalidades superpuestas.
Como se mencionó anteriormente, UEFI en sí es solo una especificación y tiene muchas implementaciones. La más utilizada es TianoCore EDK II , una implementación de referencia de código abierto de las especificaciones UEFI y PI. Si bien EDKII por sí solo no es suficiente para crear un firmware de arranque completamente funcional, proporciona una base sólida para la mayoría de las soluciones comerciales.
Para admitir distintos cargadores de arranque de primera etapa y proporcionar una interfaz UEFI, se utiliza el proyecto UEFI Payload . Se basa en la configuración inicial realizada y en la información de la plataforma proporcionada por el firmware de arranque para preparar el sistema para el entorno UEFI.
La carga útil UEFI utiliza las fases DXE y BDS , que están diseñadas para ser independientes de la plataforma. Ofrece una carga útil genérica que puede adaptarse a diferentes plataformas. En la mayoría de los casos, no requiere ninguna personalización ni ajustes específicos de la plataforma y se puede utilizar tal como está consumiendo información de la plataforma desde el cargador de arranque de primera etapa .
Variantes de la carga útil UEFI :
Carga útil UEFI heredada : requiere una biblioteca de análisis para extraer la información necesaria de la plataforma específica de la implementación. Si el gestor de arranque actualiza su API, también se debe actualizar la carga útil.
Carga útil de UEFI universal : sigue la especificación de firmware escalable universal (USF) , y utiliza el formato ejecutable y enlazable (ELF) o el árbol de imágenes planas (FIT) como formato de imagen común. En lugar de analizarlos por sí mismo, espera recibir los bloques de transferencia (HOB) en la entrada de la carga útil.
Si bien la carga útil UEFI heredada funciona bien, la comunidad EDK2 está intentando cambiar la industria hacia la carga útil UEFI universal . La elección entre cargas útiles depende de los componentes de su firmware. Por ejemplo, no es posible ejecutar la carga útil heredada con soporte SMM en Slim Bootloader sin mi parche . Por otro lado, usar la carga útil universal con coreboot requiere una capa de corrección para traducir las tablas de coreboot en HOB , una característica que solo está disponible en la bifurcación EDK2 de StarLabs .
Cada sistema compatible con UEFI proporciona una tabla de sistema que se pasa a cada código que se ejecuta en el entorno UEFI (controladores, aplicaciones, cargadores de SO). Esta estructura de datos permite que un ejecutable UEFI acceda a las tablas de configuración del sistema , como ACPI , SMBIOS y una colección de servicios UEFI .
La estructura de la tabla se describe en MdePkg/Include/Uefi/UefiSpec.h :
typedef struct { EFI_TABLE_HEADER Hdr; CHAR16 *FirmwareVendor; UINT32 FirmwareRevision; EFI_HANDLE ConsoleInHandle; EFI_SIMPLE_TEXT_INPUT_PROTOCOL *ConIn; EFI_HANDLE ConsoleOutHandle; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut; EFI_HANDLE StandardErrorHandle; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr; // // A pointer to the EFI Runtime Services Table. // EFI_RUNTIME_SERVICES *RuntimeServices; // // A pointer to the EFI Boot Services Table. // EFI_BOOT_SERVICES *BootServices; UINTN NumberOfTableEntries; EFI_CONFIGURATION_TABLE *ConfigurationTable; } EFI_SYSTEM_TABLE;
Los servicios incluyen los siguientes tipos: Servicios de arranque , Servicios de tiempo de ejecución y Servicios proporcionados por protocolos .
UEFI abstrae el acceso al dispositivo mediante la configuración de los Protocolos UEFI . Estos protocolos son estructuras de datos que contienen punteros de función y se identifican mediante un Identificador único global (GUID) que permite que otros módulos los localicen y los utilicen. Se pueden descubrir a través de los Servicios de arranque.
Un controlador UEFI produce estos protocolos y las funciones reales (¡no los punteros!) están contenidas dentro del propio controlador. Este mecanismo permite que los diferentes componentes dentro del entorno UEFI se comuniquen entre sí y garantiza que el sistema operativo pueda interactuar con los dispositivos antes de cargar sus propios controladores.
Si bien algunos protocolos están predefinidos y descritos en la especificación UEFI, los proveedores de firmware también pueden crear sus propios protocolos personalizados para ampliar la funcionalidad de una plataforma.
Servicios de arranque
Proporcionar funciones que se puedan utilizar únicamente durante el arranque. Estos servicios permanecen disponibles hasta que se llama a la función EFI_BOOT_SERVICES.ExitBootServices()
( MdeModulePkg/Core/Dxe/DxeMain/DxeMain.c ).
Los punteros a todos los servicios de arranque se almacenan en la tabla de servicios de arranque ( MdePkg/Include/Uefi/UefiSpec.h ):
typedef struct { EFI_TABLE_HEADER Hdr; ... EFI_GET_MEMORY_MAP GetMemoryMap; EFI_ALLOCATE_POOL AllocatePool; EFI_FREE_POOL FreePool; ... EFI_HANDLE_PROTOCOL HandleProtocol; ... EFI_EXIT_BOOT_SERVICES ExitBootServices; ... } EFI_BOOT_SERVICES;
Servicios en tiempo de ejecución
Un conjunto mínimo de servicios sigue estando accesible mientras el sistema operativo está en funcionamiento. A diferencia de los servicios de arranque, estos servicios siguen siendo válidos después de que cualquier carga útil (por ejemplo, el cargador de arranque del sistema operativo) haya tomado el control de la plataforma mediante una llamada a EFI_BOOT_SERVICES.ExitBootServices()
.
Los punteros a todos los servicios de tiempo de ejecución se almacenan en la tabla de servicios de tiempo de ejecución ( MdePkg/Include/Uefi/UefiSpec.h ):
typedef struct { EFI_TABLE_HEADER Hdr; ... EFI_GET_TIME GetTime; EFI_SET_TIME SetTime; ... EFI_GET_VARIABLE GetVariable; EFI_GET_NEXT_VARIABLE_NAME GetNextVariableName; EFI_SET_VARIABLE SetVariable; ... EFI_GET_NEXT_HIGH_MONO_COUNT GetNextHighMonotonicCount; EFI_RESET_SYSTEM ResetSystem; ... } EFI_RUNTIME_SERVICES;
La siguiente imagen muestra la línea de tiempo de los servicios de arranque y tiempo de ejecución, para que pueda ver exactamente cuándo está activo cada uno.
La especificación UEFI define un motor de políticas de arranque denominado gestor de arranque UEFI . Intentará cargar las aplicaciones UEFI en un orden específico. Este orden y otras configuraciones se pueden configurar modificando las variables globales de NVRAM (memoria de acceso aleatorio no volátil) . Analicemos las más importantes:
Boot####
( ####
se reemplaza por un valor hexadecimal único): una opción de arranque/carga.BootCurrent
: la opción de arranque utilizada para iniciar el sistema que se está ejecutando actualmente.BootNext
: la opción de arranque solo para el siguiente arranque. Reemplaza BootOrder
solo para un arranque y el administrador de arranque la elimina después del primer uso. Esto le permite cambiar el comportamiento del siguiente arranque sin cambiar BootOrder
.BootOrder
: lista ordenada de carga de opciones de arranque. El administrador de arranque intenta arrancar la primera opción activa de esta lista. Si no lo logra, prueba con la siguiente opción, y así sucesivamente.BootOptionSupport
: los tipos de opciones de arranque compatibles con el administrador de arranque.Timeout
: el tiempo de espera de los administradores de arranque del firmware, en segundos, antes de elegir automáticamente el valor de inicio de BootNext
o BootOrder
.
Estas variables se pueden obtener fácilmente desde Linux usando efibootmgr(8) :
[root@localhost ~]# efibootmgr BootCurrent: 0000 Timeout: 5 seconds BootOrder: 0000,0001,2001,2002,2003 Boot0000* ARCHLINUX HD(5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000)/File(\EFI\ARCHLINUX\grubx64.efi) Boot0001* Windows Boot Manager HD(1,GPT,6f185443-09fc-4f15-afdf-01c523565e52,0x800,0x32000)/File(\EFI\Microsoft\Boot\bootmgfw.efi)57a94e544f5753000100000088900100780000004200430044039f0a42004a004500430054003d007b00390064006500610038003600320063002d1139006300640064002d0034006500370030102d0061006300630031002d006600330032006200330034003400640034003700390035007d00000033000300000710000000040000007fff0400 Boot0002* ARCHLINUX HD(5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000) Boot2001* EFI USB Device RC Boot2002* EFI DVD/CDROM RC Boot2003* EFI Network RC
Echemos un vistazo al arranque basándonos en el fragmento de código anterior. UEFI comenzará a iterar la lista BootOrder
. Para cada entrada de la lista, busca una variable Boot####
correspondiente: Boot0000
para 0000, Boot2003
para 2003, y así sucesivamente. Si la variable no existe, continúa con la siguiente entrada. Si la variable existe, lee el contenido de la variable. Cada variable de opción de arranque contiene un descriptor EFI_LOAD_OPTION
que es un búfer lleno de bytes de campos de longitud variable (es solo la estructura de datos).
La estructura de datos se describe en [MdePkg/Include/Uefi/UefiSpec.h][ https://github.com/tianocore/edk2/blob/edk2-stable202405/MdePkg/Include/Uefi/UefiSpec.h#L2122 )
typedef struct _EFI_LOAD_OPTION { /// The attributes for this load option entry. UINT32 Attributes; /// Length in bytes of the FilePathList. UINT16 FilePathListLength; /// The user readable description for the load option. /// Example: 'ARCHLINUX' / 'Windows Boot Manager' / `EFI USB Device` // CHAR16 Description[]; /// A packed array of UEFI device paths. /// Example: 'HD(5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000)/File(\EFI\ARCHLINUX\grubx64.efi)' // EFI_DEVICE_PATH_PROTOCOL FilePathList[]; /// The remaining bytes in the load option descriptor are a binary data buffer that is passed to the loaded image. /// Example: '57a9...0400' in Boot0001 variable // UINT8 OptionalData[]; } EFI_LOAD_OPTION;
En este punto, el firmware examinará una ruta de dispositivo ( EFI_DEVICE_PATH_PROTOCOL ). En la mayoría de los casos, nuestra computadora se inicia desde un dispositivo de almacenamiento (disco duro/SSD/NVMe/etc.). Por lo tanto, la ruta del dispositivo contendría el nodo HD(Partition Number, Type, Signature, Start sector, Size in sectors)
.
Nota : Si está interesado en saber cómo traducir otras rutas, lea la Especificación UEFI v2.10, Referencia de nodo de dispositivo de texto 10.6.1.6 .
UEFI examinará el disco y verá si tiene una partición que coincida con el nodo. Si existe, debe estar etiquetada con un identificador único global (GUID) específico que la marque como la partición del sistema EFI (ESP) . Esta está formateada con un sistema de archivos cuya especificación se basa en la versión específica del sistema de archivos FAT y se denomina sistema de archivos EFI ; en realidad, es simplemente un FAT12/16/32 normal.
File(\Path\To\The\File.efi)
, UEFI buscará ese archivo específico. Por ejemplo, la opción Boot0000
contiene File(\EFI\ARCHLINUX\grubx64.efi)
.\EFI\BOOT\BOOT{arch}.EFI
( BOOTx64.EFI
para amd64 o BOOTia32.EFI
para i386 / IA32 ). Este mecanismo permite que los medios extraíbles de arranque (por ejemplo, una unidad USB) funcionen en UEFI; solo utilizan una ruta de arranque de respaldo . Por ejemplo, la opción Boot0002
utilizará este mecanismo.
Nota: Todas las opciones Boot####
mencionadas anteriormente se refieren a las opciones de arranque que se muestran en la salida de ejemplo de efibootmgr .
En ambos casos, el gestor de arranque UEFI cargará la aplicación UEFI (puede ser el gestor de arranque del sistema operativo , el shell UEFI, el software de utilidad, la configuración del sistema, etc.) en la memoria. En este momento, el control se transfiere al punto de entrada de la aplicación UEFI . A diferencia del BIOS , la aplicación UEFI puede devolver el control al firmware (además de la situación en la que la aplicación toma el control del sistema). Si esto sucede o algo sale mal, el gestor de arranque pasa a la siguiente entrada Boot####
y sigue exactamente el mismo proceso.
La especificación menciona que el gestor de arranque puede mantener automáticamente las variables de la base de datos. Esto incluye la eliminación de las variables de opciones de carga que no están referenciadas o que no se pueden analizar. Además, puede reescribir cualquier lista ordenada para eliminar cualquier opción de carga sin las variables de opciones de carga correspondientes.
El texto anterior describe el arranque UEFI . Además, el firmware UEFI puede ejecutarse en modo Módulo de soporte de compatibilidad (CSM) que emula un BIOS.
Un programa que se inicia con el firmware (normalmente el cargador de arranque de segunda etapa ) y que utiliza su interfaz para cargar el núcleo del sistema operativo . Puede ser tan complejo como un sistema operativo y ofrecer funciones como:
Los diseños más comunes de estos programas quedan fuera del alcance de este artículo. Para obtener una comparación detallada de los cargadores de arranque de sistemas operativos más populares, puede consultar la wiki de ArchLinux y el artículo de Wikipedia .
El sistema Windows utiliza su propio gestor de arranque del sistema operativo conocido como Administrador de arranque de Windows (BOOTMGR) .
El firmware ya no es un pequeño y complejo fragmento de código, sino una enorme cantidad de código complejo , y las tendencias actuales no hacen más que contribuir a ello. Podemos ejecutar Doom , Twitter y muchas otras aplicaciones interesantes en él.
Comprender la arquitectura general ayuda a organizar estos componentes en la mente. Al examinar el diseño del firmware existente, se obtiene una idea del fascinante proceso que se desarrolla cada vez que se enciende una computadora. Esta perspectiva de arriba hacia abajo no solo aclara el papel de cada parte, sino que también destaca la naturaleza sofisticada y evolutiva de los sistemas de firmware modernos.