Easyboot admite varios núcleos usando complementos. Pero si no se encuentra un complemento adecuado, recurre a los binarios ELF64 o PE32+ con una variante simplificada (sin necesidad de incrustar nada) del protocolo Multiboot2.
Este es el mismo protocolo que utiliza Simpleboot, todos los núcleos de ejemplo en ese repositorio deben funcionar con Easyboot también.
Puede usar el encabezado multiboot2.h original en el repositorio de GRUB o el archivo de encabezado C/C++ easyboot.h para que sea más fácil utilizar definiciones de tipo. El formato binario de bajo nivel es el mismo, también puede usar cualquier biblioteca Multiboot2 existente, incluso con lenguajes que no sean C, como este Rust, por ejemplo (nota: no estoy afiliado a esos desarrolladores de ninguna manera, solo busqué "Rust Multiboot2" y ese fue el primer resultado).
[[TOC]]
En máquinas BIOS, el firmware carga el primer sector del disco en 0:0x7C00 y le pasa el control. En este sector Easyboot tiene boot_x86.asm, que es lo suficientemente inteligente como para localizar y cargar el cargador de segunda etapa, y también para configurar el modo largo.
En máquinas UEFI, el firmware carga directamente el mismo archivo de segunda etapa, llamado EFI/BOOT/BOOTX64.EFI
. La fuente de
este cargador se puede encontrar en loader_x86.c. Eso es todo, Easyboot no es GRUB ni syslinux, los
cuales requieren docenas y docenas de archivos del sistema en el disco. Aquí no se necesitan más archivos, solo este (los
complementos son opcionales, no se necesita ninguno para proporcionar compatibilidad con Multiboot2).
En Raspberry Pi el cargador se llama KERNEL8.IMG
, compilado desde loader_rpi.c.
Este cargador está escrito con mucho cuidado para funcionar en múltiples configuraciones. Carga la tabla de particiones GUID desde
el disco y busca una "Partición del sistema EFI". Cuando lo encuentra, busca el archivo de configuración easyboot/menu.cfg
en esa
partición de arranque. Una vez seleccionada la opción de arranque y conocido el nombre del archivo del kernel, el cargador lo
localiza y lo carga.
Luego detecta automáticamente el formato del kernel y es lo suficientemente inteligente como para interpretar la información de sección y segmento sobre dónde cargar qué (realiza mapeo de memoria bajo demanda cuando es necesario). Luego configura un entorno adecuado dependiendo del protocolo de arranque detectado (Multiboot2/Linux/etc. protegido o modo largo, argumentos ABI, etc.). Una vez que el estado de la máquina es sólido y está bien definido, como último acto, el cargador salta al punto de entrada de su kernel.
Todo lo que está escrito en la especificación Multiboot2 (en inglés) sobre el estado de la máquina se mantiene, excepto los registros de propósito general. Easyboot pasa dos argumentos al punto de entrada de su kernel de acuerdo con SysV ABI y también con Microsoft fastcall ABI. El primer parámetro es mágico, el segundo es una dirección de memoria física, que apunta a una lista de etiquetas de información de arranque múltiple (abreviada como MBI en lo sucesivo, ver más abajo).
También violamos un poco el protocolo Multiboot2 para manejar núcleos de mitad superior. Multiboot2 exige que la memoria debe tener un mapeo de identidad. Bueno, bajo Easyboot esto es sólo parcialmente cierto: solo garantizamos que toda la RAM física seguramente tenga la identidad asignada como se esperaba; sin embargo, es posible que algunas regiones superiores (dependiendo de los encabezados del programa del kernel) aún estén disponibles. Esto no interrumpe los núcleos normales compatibles con Multiboot2, que se supone que no deben acceder a la memoria fuera de la RAM física disponible.
Su kernel se carga exactamente de la misma manera en los sistemas BIOS y UEFI, así como en RPi, las diferencias de firmware son simplemente "el problema de alguien más". Lo único que verá su kernel es si el MBI contiene la etiqueta de la tabla del sistema EFI o no. Para simplificarle la vida, Easyboot tampoco genera la etiqueta del mapa de memoria EFI (tipo 17), solo proporciona la etiqueta Mapa de memoria (tipo 6) de forma indiscriminada en todas las plataformas (también en sistemas UEFI, allí, el mapa de memoria simplemente se convierte para usted, por lo que su núcleo tiene que manejar sólo un tipo de etiqueta de lista de memoria). Las etiquetas antiguas y obsoletas también se omiten y este administrador de arranque nunca las genera.
El kernel se ejecuta en el nivel de supervisor (ring 0 en x86, EL1 en ARM), posiblemente en todos los núcleos de CPU en paralelo.
GDT no especificado, pero válido. La pila está configurada en los primeros 640k y va creciendo hacia abajo (pero debería cambiar esto lo antes posible a la pila que considere adecuada). Cuando SMP está habilitado, todos los núcleos tienen sus propias pilas y el identificador del núcleo está en la parte superior de la pila (pero también puede obtener el identificador del núcleo de la manera habitual específica de la plataforma, utilizando cpuid / mpidr / etc.).
Debería considerar IDT como no especificado; IRQ, NMI e interrupciones de software deshabilitadas. Los controladores de excepciones ficticios están configurados para mostrar un volcado mínimo y detener la máquina. Sólo se debe confiar en estos para informar si su kernel sufre estragos antes de que pudiera configurar su propio IDT y sus controladores, preferiblemente lo antes posible. En ARM, vbar_el1 está configurado para llamar a los mismos controladores de excepciones ficticios (aunque, por supuesto, volcan registros diferentes).
Framebuffer también está configurado de forma predeterminada. Puede modificar la resolución en la configuración, pero si no se proporciona, el framebuffer aún está configurado.
Es importante no regresar nunca de su kernel. Eres libre de sobrescribir cualquier parte del cargador en la memoria (tan pronto como hayas terminado con las etiquetas MBI), por lo que simplemente no hay ningún lugar al que regresar. "Der Mohr hat seine Schuldigkeit getan, der Mohr kann gehen".
No es obvio al principio, pero la especificación Multiboot2 en realidad define dos conjuntos de etiquetas totalmente independientes:
Se supone que el primer conjunto está incluido en un kernel compatible con Multiboot2, llamado encabezado Multiboot2 de la imagen del sistema operativo (sección 3.1.2), por lo tanto proporcionado por el kernel. A Easyboot no le importan estas etiquetas y tampoco las analiza en su kernel. Simplemente no necesita ningún dato mágico especial incrustado en su archivo de kernel; Easyboot, los encabezados ELF y PE son suficientes.
El segundo conjunto se pasa al kernel dinámicamente al arrancar, Easyboot usa solo estas etiquetas. Sin embargo, no genera todo lo que especifica Multiboot2 (simplemente omite los antiguos, obsoletos o heredados). Estas etiquetas se denominan etiquetas MBI; consulte Información de arranque (sección 3.6).
NOTA: la especificación Multiboot2 en las etiquetas MBI tiene muchos errores. Puede encontrar una versión corregida a continuación, que se alinea con el archivo de encabezado multiboot2.h que puede encontrar en el repositorio fuente de GRUB.
El primer parámetro de su kernel es el mágico 0x36d76289 (en rax
, rcx
y rdi
). Puede ubicar las etiquetas MBI usando el segundo
parámetro (en rbx
, rdx
y rsi
). En la plataforma ARM, la magia está en x0
y la dirección está en x1
. En RISC-V y MIPS se
utilizan a0
y a1
, respectivamente. Si este cargador se traslada a otra arquitectura, siempre se deben utilizar los registros
especificados por SysV ABI para los argumentos de la función. Si hay otras ABI comunes en la plataforma que no interfieren con la
ABI de SysV, entonces los valores también deben duplicarse en los registros de esas ABI (o en la parte superior de la pila).
La dirección pasada siempre está alineada con 8 bytes y comienza con un encabezado MBI:
+-------------------+
u32 | total_size |
u32 | reserved |
+-------------------+
A esto le sigue una serie de etiquetas alineadas también de 8 bytes. Cada etiqueta comienza con los siguientes campos de encabezado de etiqueta:
+-------------------+
u32 | type |
u32 | size |
+-------------------+
type
contiene un identificador del contenido del resto de la etiqueta. size
contiene el tamaño de la etiqueta, incluidos los
campos de encabezado pero sin incluir el relleno. Las etiquetas se suceden unas a otras y se rellenan cuando es necesario para que
cada etiqueta comience en una dirección alineada de 8 bytes.
+-------------------+
u32 | type = 0 |
u32 | size = 8 |
+-------------------+
Las etiquetas terminan en una etiqueta de tipo 0
y tamaño 8
.
+-------------------+
u32 | type = 1 |
u32 | size |
u8[n] | string |
+-------------------+
string
contiene la línea de comando especificada en la línea kernel
de menuentry (sin la ruta del kernel ni el nombre de
archivo). La línea de comando es una cadena UTF-8 normal de estilo C terminada en cero.
+----------------------+
u32 | type = 2 |
u32 | size = 17 |
u8[n] | string "Easyboot" |
+----------------------+
string
contiene el nombre de un cargador de arranque que arranca el kernel. El nombre es una cadena terminada en cero UTF-8 de
estilo C normal.
+-------------------+
u32 | type = 3 |
u32 | size |
u32 | mod_start |
u32 | mod_end |
u8[n] | string |
+-------------------+
Esta etiqueta indica al kernel qué módulo de arranque se cargó junto con la imagen del kernel y dónde se puede encontrar. mod_start
y mod_end
contienen las direcciones físicas de inicio y fin del propio módulo de arranque. Nunca obtendrá un búfer comprimido con
gzip, porque Easyboot los descomprime de forma transparente (y si proporciona un complemento, también funciona con datos que no
sean comprimidos con gzip). El campo cadena
proporciona una cadena arbitraria que se asociará con ese módulo de inicio en
particular; es una cadena UTF-8 normal de estilo C terminada en cero. Se especifica en la línea module
de menuentry y su uso
exacto es específico del sistema operativo. A diferencia de la etiqueta de la línea de comando de arranque, las etiquetas del módulo
también incluyen la ruta y el nombre del archivo del módulo.
Aparece una etiqueta por módulo. Este tipo de etiqueta puede aparecer varias veces. Si se cargó un disco ram inicial junto con su kernel, aparecerá como el primer módulo.
Hay un caso especial: si el archivo es una tabla DSDT ACPI, un blob FDT (dtb) o GUDT, entonces no aparecerá como un módulo, sino que se parcheará el antiguo RSDP ACPI (tipo 14) o el nuevo RSDP ACPI (tipo 15). y su DSDT se reemplaza con el contenido de este archivo.
Esta etiqueta proporciona un mapa de memoria.
+-------------------+
u32 | type = 6 |
u32 | size |
u32 | entry_size = 24 |
u32 | entry_version = 0 |
varies | entries |
+-------------------+
size
contiene el tamaño de todas las entradas, incluido este campo. entry_size
siempre es 24. entry_version
se establece en 0
.
Cada entrada tiene la siguiente estructura:
+-------------------+
u64 | base_addr |
u64 | length |
u32 | type |
u32 | reserved |
+-------------------+
base_addr
es la dirección física inicial. longitud
es el tamaño de la región de memoria en bytes. "tipo" es la variedad del
rango de direcciones representado, donde un valor de 1
indica RAM disponible, el valor de 3
indica memoria utilizable que
contiene información ACPI, el valor de 4
indica memoria reservada que debe conservarse en hibernación, El valor de 5
indica una
memoria que está ocupada por módulos RAM defectuosos y todos los demás valores indican actualmente un área reservada. reserved
se
establece en 0
en el arranque del BIOS.
Cuando el MBI se genera en una máquina UEFI, varias entradas del mapa de memoria EFI se almacenan como tipo 1
(RAM disponible) o
2
(RAM reservada) y, si lo necesita, el tipo de memoria EFI original se coloca en el campo reserved
.
Se garantiza que el mapa proporcionado enumera toda la RAM estándar que debería estar disponible para un uso normal, y siempre está
ordenado en orden ascendente base_addr
. Sin embargo, este tipo de RAM disponible incluye las regiones ocupadas por kernel, mbi,
segmentos y módulos. El kernel debe tener cuidado de no sobrescribir estas regiones (Easyboot podría excluir fácilmente esas
regiones, pero eso rompería la compatibilidad con Multiboot2).
+----------------------------------+
u32 | type = 8 |
u32 | size = 38 |
u64 | framebuffer_addr |
u32 | framebuffer_pitch |
u32 | framebuffer_width |
u32 | framebuffer_height |
u8 | framebuffer_bpp |
u8 | framebuffer_type = 1 |
u16 | reserved |
u8 | framebuffer_red_field_position |
u8 | framebuffer_red_mask_size |
u8 | framebuffer_green_field_position |
u8 | framebuffer_green_mask_size |
u8 | framebuffer_blue_field_position |
u8 | framebuffer_blue_mask_size |
+----------------------------------+
El campo framebuffer_addr
contiene la dirección física del framebuffer. El campo framebuffer_pitch
contiene la longitud de una
fila en bytes. Los campos framebuffer_width
, framebuffer_height
contienen dimensiones del framebuffer en píxeles. El campo
framebuffer_bpp
contiene el número de bits por píxel. framebuffer_type
siempre se establece en 1 y reserved
siempre contiene 0
en la versión actual de la especificación y la imagen del sistema operativo debe ignorarlo. Los campos restantes describen el
formato de píxeles empaquetados, la posición de los canales y el tamaño en bits. Puede usar la expresión
((~(0xffffffff << size)) << position) & 0xffffffff
para obtener una máscara de canal similar a UEFI GOP.
Esta etiqueta solo existe si Easyboot se ejecuta en una máquina UEFI. En una máquina con BIOS, esta etiqueta nunca se generó.
+-------------------+
u32 | type = 12 |
u32 | size = 16 |
u64 | pointer |
+-------------------+
Esta etiqueta contiene un puntero a la tabla del sistema EFI.
Esta etiqueta solo existe si Easyboot se ejecuta en una máquina UEFI. En una máquina con BIOS, esta etiqueta nunca se generó.
+-------------------+
u32 | type = 20 |
u32 | size = 16 |
u64 | pointer |
+-------------------+
Esta etiqueta contiene un puntero al identificador de imagen EFI. Por lo general, es el identificador de imagen del cargador de arranque.
+-------------------+
u32 | type = 13 |
u32 | size |
u8 | major |
u8 | minor |
u8[6] | reserved |
| smbios tables |
+-------------------+
Esta etiqueta contiene una copia de las tablas SMBIOS así como su versión.
+-------------------+
u32 | type = 14 |
u32 | size |
| copy of RSDPv1 |
+-------------------+
Esta etiqueta contiene una copia de RSDP según lo definido por la especificación ACPI 1.0. (Con una dirección de 32 bits).
+-------------------+
u32 | type = 15 |
u32 | size |
| copy of RSDPv2 |
+-------------------+
Esta etiqueta contiene una copia de RSDP según lo definido por ACPI 2.0 o especificación posterior. (Probablemente con una dirección de 64 bits).
Estos (tipo 14 y 15) apuntan a una tabla RSDT
o XSDT
con un puntero a una tabla FACP
, que a su vez contiene dos punteros a una
tabla DSDT
, que describe la máquina. Easyboot falsifica estas tablas en máquinas que de otro modo no son compatibles con ACPI.
Además, si proporciona una tabla DSDT, un blob FDT (dtb) o GUDT como módulo, Easyboot parcheará los punteros para que apunten a
esa tabla proporcionada por el usuario. Para analizar estas tablas, puede usar mi biblioteca de encabezado único y libre de
dependencias hwdet (o la inflada apcica y
libfdt).
Las etiquetas con type
mayor o igual a 256 no forman parte de la especificación Multiboot2, no obstante las proporciona Easyboot.
Estos pueden agregarse mediante complementos opcionales a la lista, si un kernel los necesita.
+-------------------+
u32 | type = 256 |
u32 | size |
| copy of EDID |
+-------------------+
Esta etiqueta contiene una copia de la lista de resoluciones de monitores compatibles según la especificación EDID.
+-------------------+
u32 | type = 257 |
u32 | size |
u32 | numcores |
u32 | running |
u32 | bspid |
+-------------------+
Esta etiqueta existe si se proporcionó la directiva multicore
. numcores
contiene la cantidad de núcleos de CPU en el sistema,
running
es la cantidad de núcleos que se han inicializado exitosamente y ejecutan el mismo kernel en paralelo. El bspid
contiene
el identificador del núcleo BSP (en la identificación lAPIC x86), de modo que los núcleos puedan distinguir los AP y ejecutar un
código diferente en ellos. Todos los AP tienen su propia pila y encima de la pila estará la identificación del núcleo actual.
+-------------------+
u32 | type = 258 |
u32 | size = 24 / 40 |
u128 | bootuuid |
u128 | rootuuid |
+-------------------+
Esta etiqueta contiene los campos de identificador único en la GPT de las particiones arranque y de raíz. Si el arranque no utiliza
una tabla de particionamiento GUID, entonces bootuuid
se genera como
54524150-(código de dispositivo)-(número de partición)-616F6F7400000000
.
Inicio | Fin | Descripción |
---|---|---|
0x0 | 0x400 | Interrupt Vector Table (utilizable, IDT en modo real) |
0x400 | 0x4FF | BIOS Data Area (utilizable) |
0x4FF | 0x500 | Código de la unidad de arranque del BIOS (probablemente 0x80, utilizable) |
0x500 | 0x5A0 | datos de sincronización para SMP (utilizables) |
0x5A0 | 0x1000 | pila de controlador de excepciones (utilizable después de configurar su IDT) |
0x1000 | 0x8000 | tablas de paginación (utilizable después de configurar las tablas de paginación) |
0x8000 | 0x20000 | código y datos del cargador (utilizable después de configurar su IDT) |
0x20000 | 0x40000 | configuración + etiquetas (utilizable después de analizar MBI) |
0x40000 | 0x90000 | plugin ids; de arriba a abajo: pila del kernel |
0x90000 | 0x9A000 | Solo kernel de Linux: zero page + cmdline |
0x9A000 | 0xA0000 | Extended BIOS Data Area (mejor no tocar) |
0xA0000 | 0xFFFFF | VRAM y BIOS ROM (no utilizable) |
0x100000 | x | segmentos del kernel, seguidos de los módulos, cada página alineada |
Nadie lo sabe. UEFI asigna memoria como quiere. Espere cualquier cosa. Seguramente todas las áreas aparecerán en el mapa de memoria
como tipo = 1 (MULTIBOOT_MEMORY_AVAILABLE
) y reserved = 2 (EfiLoaderData
), sin embargo, esto no es exclusivo, otros tipos de
memoria también podrían aparecer así (bss del administrador de arranque). sección, por ejemplo).
Inicio | Fin | Descripción |
---|---|---|
0x0 | 0x500 | reservado por firmware (mejor no tocar) |
0x500 | 0x5A0 | datos de sincronización para SMP (utilizables) |
0x5A0 | 0x1000 | pila de controlador de excepciones (utilizable después de configurar su VBAR) |
0x1000 | 0x9000 | tablas de paginación (utilizable después de configurar las tablas de paginación) |
0x9000 | 0x20000 | código y datos del cargador (utilizable después de configurar su VBAR) |
0x20000 | 0x40000 | configuración + etiquetas (utilizable después de analizar MBI) |
0x40000 | 0x80000 | firmware proporcionado FDT (dtb); de arriba a abajo: pila del kernel |
0x100000 | x | segmentos del kernel, seguidos de los módulos, cada página alineada |
Los primeros bytes están reservados para armstub. Solo se
inició el núcleo 0, por lo que para iniciar los procesadores de aplicaciones, escriba la dirección de una función en 0xE0 (núcleo 1),
0xE8 (núcleo 2), 0xF0 (núcleo 3), cuyas direcciones se encuentran en esta área. Esto es irrelevante cuando se utiliza la directiva
multicore
, entonces todos los núcleos ejecutarán el kernel.
Aunque no es compatible de forma nativa con RPi, aún obtienes una etiqueta RSDP (tipo 14) antigua de ACPI, con tablas falsas. La
tabla APIC
se utiliza para comunicar la cantidad de núcleos de CPU disponibles al kernel. La dirección de la función de inicio se
almacena en el campo RSD PTR -> RSDT -> APIC -> cpu[x].apic_id (y el ID del núcleo en cpu[x].acpi_id, donde BSP siempre es
cpu[0].acpi_id = 0 y cpu[0].apic_id = 0xD8. Cuidado, "acpi" y "apic" se ven bastante similares).
Si el firmware pasa un blob FDT válido, o si uno de los módulos es un archivo .dtb, .gud o .aml, también se agrega una tabla FADT
(con FACP
mágica). En esta tabla, el puntero DSDT (32 bits, con desplazamiento 40) apunta al blob de árbol de dispositivos
aplanado proporcionado.
Aunque el firmware no proporciona una función de mapa de memoria, también recibirá una etiqueta de mapa de memoria (tipo 6), que enumera la RAM detectada y la región MMIO. Puede usar esto para detectar la dirección base del MMIO, que es diferente en RPi3 y RPi4.