De forma predeterminada, Easyboot arranca núcleos compatibles con Multiboot2 en formatos ELF y PE desde la partición de arranque. Si su kernel usa un formato de archivo diferente, un protocolo de inicio diferente o no está ubicado en la partición de inicio, necesitará complementos en la partición de inicio. Puede encontrarlos en el directorio src/plugins.
[[TOC]]
Para instalar complementos, simplemente cópielos en el directorio especificado en el parámetro (indir)
, en el subdirectorio
easyboot
al lado del archivo menu.cfg.
Por ejemplo:
bootpart
|-- easyboot
| |-- linux_x86.plg
| |-- minix_x86.plg
| `-- menu.cfg
|-- EFI
| `-- BOOT
|-- kernel
`-- initrd
$ easyboot bootpart disco.img
Era obvio desde el principio que ELF no es adecuado para esta tarea. Es demasiado inflado y demasiado complejo. Originalmente quería usar struct exec (el formato clásico a.out de UNIX), pero desafortunadamente las cadenas de herramientas modernas ya no pueden crear eso. Así que he decidido crear mi propio formato y mi propio enlazador para los complementos.
Puede compilar el código fuente del complemento con cualquier compilador cruzado ANSI C estándar en un archivo objeto ELF, pero luego tendrá que usar el vinculador plgld para crear el binario final. Este es un entrelazador independiente de la arquitectura y funcionará sin importar para qué código de máquina se compiló el complemento. El .plg final tiene solo una fracción del tamaño del .o ELF a partir del cual se generó.
El código fuente C de un complemento debe incluir el archivo de encabezado src/loader.h
y debe contener una línea EASY_PLUGIN
.
Tiene un parámetro, el tipo de complemento, seguido de la especificación de coincidencia del identificador. El cargador utiliza
este último para determinar cuándo usar ese complemento en particular.
Por ejemplo:
#include "../loader.h"
/* bytes mágicos que identifican un kernel de Linux */
EASY_PLUGIN(PLG_T_KERNEL) {
/* compensar talla tipo de partido bytes mágicos */
{ 0x1fe, 2, PLG_M_CONST, { 0xAA, 0x55, 0, 0 } },
{ 0x202, 4, PLG_M_CONST, { 'H', 'd', 'r', 'S' } }
};
/* punto de entrada, el prototipo está definido por el tipo de complemento */
PLG_API void _start(uint8_t *buf, uint64_t size);
{
/* preparar el entorno para un kernel de Linux */
}
Los complementos pueden usar varias variables y funciones, todas ellas definidas en el archivo de encabezado y vinculadas en tiempo de ejecución.
uint32_t verbose;
Nivel de verbosidad. Un complemento solo puede imprimir cualquier salida si es distinta de cero, excepto mensajes de error. Cuanto mayor sea su valor, más detalles se deberán imprimir.
uint64_t file_size;
El tamaño total del archivo abierto (consulte open
y loadfile
a continuación).
uint8_t *root_buf;
Cuando se inicializa un complemento del sistema de archivos, contiene los primeros 128k de la partición (con suerte, incluido el superbloque). Más tarde, un complemento del sistema de archivos puede reutilizar este búfer de 128k para cualquier propósito (caché FAT, caché de inodo, etc.)
uint8_t *tags_buf;
Contiene las etiquetas Multiboot2. Un complemento del kernel puede analizar esto para convertir los datos proporcionados por el administrador de arranque al formato que espera el kernel. Este puntero apunta al inicio del búfer.
uint8_t *tags_ptr;
Este puntero apunta al final del búfer de etiquetas Multiboot2. Los complementos de etiquetas pueden agregar nuevas etiquetas aquí y ajustar este puntero.
uint8_t *rsdp_ptr;
Apunta al puntero ACPI de RSDP.
uint8_t *dsdt_ptr;
Apunta al blob de descripción de hardware DSDT (o GUDT, FDT).
efi_system_table_t *ST;
En máquinas UEFI, apunta a la tabla del sistema EFI; de lo contrario, NULL
.
void memset(void *dst, uint8_t c, uint32_t n);
void memcpy(void *dst, const void *src, uint32_t n);
int memcmp(const void *s1, const void *s2, uint32_t n);
Funciones de memoria obligatorias (el compilador de C puede generar llamadas a estas, incluso cuando no hay una llamada directa).
void *alloc(uint32_t num);
Asigna num
páginas (4k) de memoria. Los complementos no deben asignar mucho, deben apuntar a un uso mínimo de memoria.
void free(void *buf, uint32_t num);
Libere memoria previamente asignada de páginas num
.
void printf(char *fmt, ...);
Imprima una cadena formateada en la consola de inicio.
uint64_t pb_init(uint64_t size);
Inicia la barra de progreso, size
es el tamaño total que representa. Devuelve cuántos bytes vale un píxel.
void pb_draw(uint64_t curr);
Dibuja la barra de progreso para el valor actual. curr
debe estar entre 0 y el tamaño total.
void pb_fini(void);
Cierra la barra de programas, borra su lugar en la pantalla.
void loadsec(uint64_t sec, void *dst);
Utilizado por los complementos del sistema de archivos, carga un sector del disco en la memoria. sec
es el número del sector,
relativo a la partición raíz.
void sethooks(void *o, void *r, void *c);
Utilizado por los complementos del sistema de archivos, establece los enlaces de las funciones de open / read / close para el sistema de archivos de la partición raíz.
int open(char *fn);
Abra un archivo en la partición raíz (o de inicio) para leerlo; devuelve 1 en caso de éxito. Sólo se puede abrir un archivo a la vez.
Cuando no hubo una llamada sethooks
de antemano, opera en la partición de inicio.
uint64_t read(uint64_t offs, uint64_t size, void *buf);
Lee datos del archivo abierto en la posición de búsqueda offs
en la memoria y devuelve el número de bytes realmente leídos.
void close(void);
Cierra el archivo abierto.
uint8_t *loadfile(char *path);
Cargue un archivo completamente desde la partición raíz (o de inicio) en un búfer de memoria recién asignado y descomprímalo de
forma transparente si se encuentra el complemento. Tamaño devuelto en file_size
.
int loadseg(uint32_t offs, uint32_t filesz, uint64_t vaddr, uint32_t memsz);
Cargue un segmento del búfer del kernel. Esto verifica si la memoria vaddr
está disponible y asigna el segmento si es la mitad
superior. Los offs
son el desplazamiento del archivo, por lo que son relativos al búfer del kernel. Si memsz
es mayor que
filesz
, entonces la diferencia se completa con ceros.
void _start(void);
Punto de entrada para complementos del sistema de archivos (PLG_T_FS
). Debería analizar el superbloque en root_buf
y llamar a
sethooks
. En caso de error, debería regresar sin fijar sus ganchos.
void _start(uint8_t *buf, uint64_t size);
Punto de entrada para complementos del kernel (PLG_T_KERNEL
). Recibe la imagen del kernel en la memoria, debe reubicar sus
segmentos, configurar el entorno adecuado y transferir el control. Cuando no hay ningún error, nunca regresa.
uint8_t *_start(uint8_t *buf);
Punto de entrada para complementos de descompresor (PLG_T_DECOMP
). Recibe el búfer comprimido (y su tamaño en file_size
) y debe
devolver un nuevo búfer asignado con los datos sin comprimir (y establecer el nuevo tamaño del búfer también en file_size
). Debe
liberar el búfer antiguo (cuidado, file_size
está en bytes, pero free() espera el tamaño en páginas). En caso de error,
file_size
no se debe cambiar y debe devolver el búfer original sin modificar.
void _start(void);
Punto de entrada para complementos de etiquetas (PLG_T_TAG
). Podrían agregar nuevas etiquetas en tags_ptr
y ajustar ese puntero
a una nueva posición alineada de 8 bytes.
Los complementos pueden usar funciones locales; sin embargo, debido a un error de CLang, deben declararse como static
. (El
error es que CLang genera registros PLT para esas funciones, aunque se pasa el indicador "-fno-plt" en la línea de comando. El uso
de static
soluciona esto).
En caso de que alguien quiera escribir un complemento en un lenguaje que no sea C (en Assembly, por ejemplo), aquí está la descripción de bajo nivel del formato de archivo.
Es muy similar al formato a.out. El archivo consta de un encabezado de tamaño fijo, seguido de secciones de longitud variable. No hay encabezado de sección, los datos de cada sección siguen directamente a los datos de la sección anterior en el siguiente orden:
(encabezamiento)
(registros de coincidencia de identificadores)
(registros de reubicación)
(codigo de maquina)
(datos de solo lectura)
(datos legibles y grabables inicializados)
Para la primera sección real, código máquina, se incluye la alineación. Para todas las demás secciones, se agrega relleno al tamaño de la sección anterior.
SUGERENCIA: si pasa un complemento como argumento único a plgld
, volcará las secciones del archivo con una salida similar a
readelf -a
u objdump -xd
.
Todos los números están en formato little-endian independientemente de la arquitectura.
Inicio | Talle | Descripción |
---|---|---|
0 | 4 | bytes mágicos EPLG |
4 | 4 | tamaño total del archivo |
8 | 4 | memoria total requerida cuando se carga el archivo |
12 | 4 | tamaño de la sección de código |
16 | 4 | tamaño de la sección de datos de solo lectura |
20 | 4 | punto de entrada del complemento |
24 | 2 | Código de arquitectura (igual que los ELF) |
26 | 2 | número de registros de reubicación |
28 | 1 | número de registros de coincidencia de identificadores |
29 | 1 | entrada GOT con mayor referencia |
30 | 1 | revisión del formato de archivo (0 por ahora) |
31 | 1 | tipo de complemento (1=sistema de archivos, 2=kernel, 3=descompresor, 4=tag) |
El código de arquitectura es el mismo que en los encabezados ELF, por ejemplo 62 = x86_64, 183 = Aarch64 y 243 = RISC-V.
El tipo de complemento especifica el prototipo del punto de entrada, la ABI siempre es SysV.
Esta sección contiene tantos de los siguientes registros como se especifican en el campo "número de registros de coincidencia de identificadores" del encabezado.
Inicio | Talle | Descripción |
---|---|---|
0 | 2 | compensar |
2 | 1 | talle |
3 | 1 | tipo |
4 | 4 | bytes mágicos para igualar |
Primero, el comienzo del tema se carga en un búfer. Se configura un acumulador, inicialmente con un 0. Las compensaciones en estos registros siempre son relativas a este acumulador y abordan ese byte en el búfer.
El campo Tipo indica cómo interpretar el desplazamiento. Si es 1, entonces el desplazamiento más el acumulador se utiliza como valor. Si es 2, entonces se toma un valor de byte de 8 bits en el desplazamiento, 3 significa tomar un valor de palabra de 16 bits y 4 significa tomar un valor de palabra d de 32 bits. 5 significa tomar un valor de byte de 8 bits y agregarle el acumulador, 6 significa tomar un valor de palabra de 16 bits y agregarle el acumulador, y 7 es lo mismo pero con un valor de 32 bits. 8 buscará los bytes mágicos desde el byte del acumulador hasta el final del búfer en pasos de desplazamiento y, si los encuentra, devuelve el desplazamiento coincidente como valor.
Si el tamaño es cero, entonces el acumulador se establece en el valor. Si el tamaño no es cero, entonces se verifica esa cantidad de bytes si coinciden con los bytes mágicos dados.
Por ejemplo, para comprobar si un ejecutable PE comienza con una instrucción NOP:
EASY_PLUGIN(PLG_T_KERNEL) {
/* compensar talla tipo de partido bytes mágicos */
{ 0, 2, PLG_M_CONST, { 'M', 'Z', 0, 0 } }, /* comprobar bytes mágicos */
{ 60, 0, PLG_M_DWORD, { 0, 0, 0, 0 } }, /* obtener el desplazamiento del encabezado PE al acumulador */
{ 0, 4, PLG_M_CONST, { 'P', 'E', 0, 0 } }, /* comprobar bytes mágicos */
{ 40, 1, PLG_M_DWORD, { 0x90, 0, 0, 0 } } /* verifique la instrucción NOP en el punto de entrada */
};
Esta sección contiene tantos de los siguientes registros como se especifican en el campo "número de registros de reubicación" del encabezado.
Inicio | Talle | Descripción |
---|---|---|
0 | 4 | inicio |
4 | 4 | tipo de reubicación |
Significado de los bits en tipo:
Desde | Hasta | Descripción |
---|---|---|
0 | 7 | símbolo (0 - 255) |
8 | 8 | direccionamiento relativo de PC |
9 | 9 | GOT direccionamiento indirecto relativo |
10 | 13 | índice de máscara inmediata (0 - 15) |
14 | 19 | bit de inicio (0 - 63) |
20 | 25 | bit final (0 - 63) |
26 | 31 | posición del indicador bit de dirección negada (0 - 63) |
El campo de desplazamiento es relativo a la magia en el encabezado del complemento y selecciona un número entero en la memoria donde se debe realizar la reubicación.
El símbolo indica qué dirección utilizar. 0 significa la dirección BASE donde se cargó el complemento en la memoria, también
conocido como. la dirección de la magia del encabezado en la memoria. Otros valores seleccionan una dirección de símbolo externo del
GOT, definido en el cargador o en otro complemento, eche un vistazo a la matriz plg_got
en la fuente de plgld.c para ver qué valor
corresponde a qué símbolo. Si el bit relativo GOT es 1, entonces se utiliza la dirección de la entrada GOT del símbolo, en lugar de
la dirección real del símbolo.
Si el bit relativo de la PC es 1, entonces el desplazamiento se resta primero de la dirección (modo de direccionamiento relativo del puntero de intrusión).
El índice de máscara inmediata indica qué bits almacenan la dirección en la instrucción. Si es 0, entonces la dirección se escribe tal cual en el desplazamiento, independientemente de la arquitectura. Para x86_64, solo se permite el índice 0. Para ARM Aarch64: 0 = tal cual, 1 = 0x07ffffe0 (desplazamiento a la izquierda 5 bits), 2 = 0x07fffc00 (desplazamiento a la izquierda 10 bits), 3 = 0x60ffffe0 (con instrucciones ADR/ADRP, el inmediato se desplaza y se divide en dos grupos de bits). Las arquitecturas futuras podrían definir más y diferentes máscaras de bits inmediatas.
Usando la máscara inmediata, se toman de la memoria los bits final - inicio + 1 y se firman como extendidos. Este valor se agrega a la dirección (se suma, y en el caso de referencias internas, la dirección del símbolo interno también se codifica aquí).
Si el bit indicador de dirección negada no es 0 y la dirección es positiva, entonces ese bit se borra. Si la dirección es negativa, entonces se establece ese bit y se niega la dirección.
Finalmente, los bits de inicio y fin seleccionan qué parte de la dirección escribir en el entero seleccionado. Esto también define el tamaño de la reubicación, los bits fuera de este rango y los que no forman parte de la máscara inmediata se dejan sin cambios.
Esta sección contiene instrucciones de máquina para la arquitectura especificada en el encabezado y tiene tantos bytes como indica el campo de tamaño del código.
Esta es una sección opcional, puede que falte. Es tan largo como lo indica el campo de tamaño de sección de solo lectura en el encabezado. Todas las variables constantes se colocan en esta sección.
Esta es una sección opcional, puede que falte. Si todavía hay bytes en el archivo después de la sección de código (o la sección de datos opcional de solo lectura), entonces todos esos bytes se consideran la sección de datos. Si una variable se inicializa con un valor distinto de cero, se coloca en esta sección.
Esta es una sección opcional, puede que falte. Esta sección nunca se almacena en el archivo. Si el campo de tamaño en la memoria es mayor que el campo de tamaño del archivo en el encabezado, entonces su diferencia se completará con ceros en la memoria. Si una variable no se inicializa o se inicializa como cero, se coloca en esta sección.