- Se construye paso a paso el flujo mínimo de un bootloader que comienza en un sector de arranque de 512 bytes y lleva una CPU x86_64 desde el real mode de 16 bits hasta el long mode de 64 bits
- Primero se verifica la salida mediante interrupciones de BIOS y luego se conectan
nasm,ld,objcopyy QEMU para comprobar que la imagen de arranque realmente se ejecute - Debido al límite de tamaño del sector de arranque, se divide en stage 1/stage 2, y antes de entrar en protected mode se lee desde el disco el siguiente código con
int 0x13de BIOS - A partir del protected mode de 32 bits ya no se pueden usar rutinas de BIOS, por lo que se requiere inicialización de hardware como GDT, flat segmentation y salida directa al buffer VGA
- Entrar en long mode de 64 bits requiere ajustar correctamente las tablas de páginas, PAE,
EFER.LME,cr0.PGy la GDT para 64 bits; después se puede llamar código C freestanding como si fuera un kernel
Entorno de 16 bits que comienza en BIOS
- Después del reset, una CPU x86 está en real mode, y el tamaño predeterminado de los operandos es de 16 bits
- El real mode crea un espacio de direcciones de 20 bits mediante segmentation y puede manejar hasta 1 MB de memoria
- El primer código que ejecuta el BIOS está en el sector de arranque del disco
- El BIOS busca un disco cuyo primer sector termine con el número mágico
0xaa55 - Carga ese sector en la dirección de memoria
0x7c00
- El BIOS busca un disco cuyo primer sector termine con el número mágico
- El espacio que entrega el BIOS es de solo 512 bytes, así que este código se centra en actuar como bootstrap para cargar el resto del bootloader
- Las rutinas de BIOS solo pueden usarse mientras se permanece en real mode
Requisitos y entorno de compilación
- Para seguir el ejemplo se necesita el Intel 64 and IA-32 Architectures Software Developer’s Manual, un assembler y QEMU
- El assembler del ejemplo es nasm, y se asume conocimiento de assembly x86 y de la sintaxis de nasm
- Aunque no se tenga una CPU x86_64, se puede ejecutar emulando una CPU x86 en QEMU
- Como material básico hasta el modo de 32 bits, se toma como referencia Writing a Simple Operating System — from Scratch
Crear el sector de arranque y verificar la ejecución
- El primer sector de arranque imprime
"Hello, world!"usando rutinas de BIOS y luego se detiene conhlty un bucle - Para imprimir la cadena se usa
int 0x10, la llamada a los video services de BIOS, conah = 0x0e - El
Makefilegenera un objeto con nasm, lo enlaza con un linker script y luego crea una imagen de arranque raw conobjcopy -O binary make bootejecuta la imagen con QEMUqemu-system-x86_64 -no-reboot -drive file=$<,format=raw,index=0,media=disk
- El linker script configura el sector de arranque para que quede ubicado con base en
0x7c00- El origin de
boot_sectorenMEMORYes0x7c00y su length es512 - La sección
.bootsignagrega0x55,0xaaen la posición0x7c00 + 510
- El origin de
- Aunque también se podrían manejar directamente el offset y el número mágico dentro del assembly del sector de arranque, aquí ese rol lo cumple el linker script
Stage 1 lee stage 2 desde el disco
- Stage 1 es el código del sector de arranque que carga el BIOS, y su objetivo es traer stage 2 a memoria
- Stage 2 contiene el código que pasa del real mode de 16 bits al protected mode de 32 bits
- Después de entrar en protected mode ya no se pueden usar rutinas de BIOS, así que la lectura de sectores del disco debe terminarse antes de la transición
- Para el acceso al disco se usa
int 0x13, los disk services de BIOSah = 0x42es la función de extended read de BIOSdl = 0x80es el número de unidad- El disk address packet contiene la cantidad de sectores a leer, la dirección de destino y el sector inicial
- El ejemplo lee 64 sectores con
READ_SECTORS_NUM equ 64- Como el sector de arranque es el sector 0, stage 2 se lee desde el sector 1
- La dirección de destino es
BOOT_LOAD_ADDR + SECTOR_SIZE, es decir,0x7c00 + 512
- En el código queda un manejo temporal que acepta el caso en que se lean menos sectores de los solicitados
- Al comienzo, stage 2 copia
print_stringpara real mode, imprime"Hello from stage 2"y confirma que el salto desde stage 1 continúa correctamente
Transición a protected mode de 32 bits
- Para entrar en protected mode, primero se debe definir la Global Descriptor Table(GDT)
- En protected mode, por defecto, segmentation se usa para la protección de memoria
- En long mode de 64 bits se necesita paging, pero en el paso anterior, la entrada a protected mode, primero se requiere configurar segmentation
- La GDT del ejemplo sigue el flat model del manual de Intel
- Incluye un code segment y un data segment
- Ambos segments se mapean a todo el linear address space
- Como es una etapa intermedia hacia long mode, se usa el modelo más simple
- La GDT es una estructura contigua ubicada en memoria
- Al inicio hay un null descriptor para capturar traducciones inválidas
- Después le siguen el code segment descriptor y el data segment descriptor
- La transición sigue este orden
- Se deshabilitan las interrupciones con
cli - Se cargan la dirección y la longitud de la GDT en GDTR con
lgdt [gdt32_pseudo_descriptor] - Se activa protected mode estableciendo
cr0.PE, es decir, el bit 0 decr0 - Se vacía el instruction pipeline con un far jump y se actualiza
csal nuevo code segment
- Se deshabilitan las interrupciones con
- Tras entrar en protected mode, los valores de segment existentes ya no tienen significado, por lo que
ds,ss,es,fsygsse configuran con el nuevo data segment selector - Para volver a habilitar interrupciones después de toda la configuración se requiere trabajo adicional
Imprimir en pantalla sin BIOS
- En protected mode ya no se pueden llamar rutinas de BIOS
- La salida de texto pasa a escribirse directamente en el VGA buffer
print_string32escribe caracteres y bytes de color en la dirección0xb8000- El valor de color es
0xf - Cada celda de carácter usa 2 bytes
- El valor de color es
- Esta función de salida es muy simple, por lo que el mensaje siempre aparece en la esquina superior izquierda de la pantalla
Tablas de páginas para entrar en long mode
- El IA-32e mode de la documentación de Intel corresponde al long mode del manual de AMD64
- Para cambiar a long mode, la CPU debe estar en protected mode y paging también debe estar activado
- Para los conceptos de paging se consultan Introduction to Paging y OSTEP
- En long mode con PAE activado se usan tablas de páginas de 4 niveles
build_page_tablecrea una tabla de páginas de 4 niveles en la dirección indicada- El tamaño de page es
0x1000 - El tamaño de cada page table es
0x1000 - La cantidad de entries es
512 - Primero inicializa cuatro tables en cero, dejando todos los entries en estado not present
- Conecta los primeros entries de PML4 → PDP → PD → page table
- En la capa más baja de page table configura 512 entries
- El tamaño de page es
GDT para 64 bits y secuencia de transición a long mode
- Aunque paging se encarga del virtual address space y la gestión de permisos, en long mode también se necesita la GDT
- La GDT para 64 bits también sigue el flat model y es casi igual a la GDT para protected mode
- La diferencia está en la configuración de bits relacionados con long mode
- Se establece el 64-bit code segment flag del code segment
- Cuando este flag está activado, el default operation size bit debe ser 0
- La transición a long mode sigue este flujo
- Se crea una page table de 4 niveles en la dirección
0x1000 - Se coloca la dirección de la PML4 table en
cr3 - Se activa PAE estableciendo el bit 5 de
cr4 - Se lee el MSR
0xc0000080y se estableceEFER.LME, es decir, el bit 8 - Se activa paging estableciendo el flag
PG, el bit 31 decr0 - Se carga la GDT para 64 bits con
lgdt - Se entra en modo de 64 bits con un far jump al 64-bit code segment
- Se crea una page table de 4 niveles en la dirección
- Justo después de activar paging, el estado es IA-32e compatibility mode; al saltar a una GDT con el 64-bit segment flag configurado, se cambia a modo de 64 bits
- El mensaje de confirmación de éxito se muestra en la esquina superior izquierda de la pantalla mediante el VGA buffer
Llamar código C freestanding
- Una vez que se llega al long mode de 64 bits, se puede llamar código C freestanding
kernel.cborra el VGA buffer0xb8000e imprime"Hello from C"- En el lado de assembly,
start_long_modedeclaraextern _start_kerneldespués de la salida de texto para 64 bits y llama a_start_kernel - El linker script divide el área de memoria en tres partes
boot_sector:0x7c00, length512stage2:0x7e00, length512kernel:0x8000, length0x10000
- Las secciones
.text,.data,.rodatay.bssse ubican en el área de kernel - El
Makefilese modifica para compilar tanto assembly como C- El compilador C es
gcc - Las CFLAGS principales son
-std=c99 -ffreestanding -m64 -mno-red-zone -fno-builtin -nostdinc -Wall -Wextra
- El compilador C es
- El código completo del ejemplo se ofrece mediante un enlace de descarga
1 comentarios
Opiniones de Hacker News
Se puede entrar directamente en modo largo con mucho menos código, sin pasar por el modo protegido: https://wiki.osdev.org/Entering_Long_Mode_Directly
Había un bootloader para un pequeño kernel de 64 bits hecho de esta forma y, aun incluyendo el código para leer el kernel desde el disco y configurar el modo VESA, cabía holgadamente en el sector de arranque. Ni siquiera hacía falta un cargador de segunda etapa.
Si vas a usar discos duros modernos reales, conviene mirar GPT en lugar de MBR. Permite superar los límites de la tabla de particiones y manejar discos grandes de más de 2 TB. UEFI elimina estos problemas y permite usar un diseño de disco correcto sin demasiada dificultad.
Para entrar en modo de 64 bits no hace falta el modo protegido. Pero es mejor no usar BIOS. Es sucio y solo complica más el trabajo.
Es mejor usar UEFI con EDK2 o GnuEFI; ambos son bastante fáciles y cómodos de implementar. Toma un poco acostumbrarse a los conceptos iniciales de UEFI, pero viendo proyectos de ejemplo en GitHub se entiende fácilmente la estructura. En EDK los archivos .dec y .inf y demás son medio desagradables, y en GnuEFI hay que leer los headers para encontrar las funciones, pero aun así es mucho mejor que las interfaces de BIOS con especificación ambigua. En hardware real ni siquiera puedes asumir que int 0x10, int 0x15, etc. existan correctamente.
En sistemas UEFI puedes asumir una base mínima estable y enumerar de forma razonable las funciones de hardware o de la plataforma. Además, como UEFI ya deja la plataforma bastante configurada, no hace falta inicializar mucho en el componente cargador del SO: puedes pasar directo a cargar los componentes adecuados para el diseño del SO, los drivers y el kernel. Basta con obtener el mapa de memoria y acceder al sistema de archivos EFI para leer lo necesario.
El 80286 tiene una palabra de estado de máquina, Machine Status Word (MSW), que es un registro de 16 bits, y el 80386 la amplió al registro de 32 bits CR0. Más tarde, el modo largo de 64 bits agregó el MSR EFER y extendió CR0 a 64 bits, pero incluso hoy en CR0 solo se usan 11 bits, y EFER solo tiene 8 bits activos.
Me pregunto por qué Intel/AMD eligieron dos veces una opción nueva en vez de usar simplemente los bits sobrantes de los registros existentes: https://wiki.osdev.org/CPU_Registers_x86-64#CR0
Que CR1 y CR5~CR7 sigan reservados mientras existe CR8 parece ir por la misma línea.
Lo que parece más innecesariamente complejo en este artículo son el Makefile y el script de linker. NASM soporta salida binaria plana, pero parece que lo consideraron demasiado “hacky”.
Antes, en mi SO tenía un archivo llamado make.sh para burlarme de esto. Ahora uso cosas sofisticadas como “formatos de archivo”, así que -fbin y --oformat=binary son recuerdos efímeros. Durante mucho tiempo intenté separar archivos C de datos y archivos C de código, volcarlos como binarios y luego ensamblar ese monstruo, pero el enlazado y la carga se volvieron demasiado difíciles. Es mejor usar ELF o PE, y en la práctica parece que esos formatos justamente hacen ese trabajo.
Se ve genial y como un buen ejercicio, pero no sé si sea útil. Me pregunto si siquiera tiene una UX tipo juguete Fisher-Price para revisar o cambiar la configuración mientras se ejecuta.
El arranque es el proceso de pasar de modo mínimo, modo de usuario único o modo de recuperación a un estado completamente operativo.
He usado Unix junto con productos de Microsoft desde la época de Xenix/DOS, creo que desde hace unos 40 años. Me pregunto cuánto se ha avanzado en todo ese tiempo.
También usé Linux desde la versión sueca, es decir, desde el primer lanzamiento, y también probé GNU 0.1.
Pido disculpas por haber llamado Unix a Xenix. Xenix fue, desde poco después de su lanzamiento hasta su decadencia, un producto desastroso de tipo me-too que ya quería pertenecer al pasado.
Microsoft no parece una empresa que lance productos, sino una que vacía la caja de arena del gato encima de sus clientes. Ejemplos recientes son Copilot y 22H2.
Si vemos cómo han evolucionado los autos de F1, los lápices y las calculadoras de bolsillo, me pregunto qué tan cerca estamos de un ideal utilizable.
También me pregunto por qué el bootloader no es un modo de kernel estático. Antes lo era, y hace poco alguien propuso que debería volver a serlo; yo estuve de acuerdo.
https://wiki.osdev.org/A20_Line
Me sorprende que todos los pasos necesarios para poner la CPU en el modo correcto parezcan innecesarios. La mayoría parecen procedimientos necesarios por compatibilidad hacia atrás
Me pregunto si Intel no pudo haber ofrecido desde el principio una bandera o instrucción para arrancar en el modo correcto, o si no pudo haber eliminado la compatibilidad hacia atrás
Recuerdo que ARM64 también tenía algunos problemas similares. Me pregunto si existe alguna CPU diseñada desde el inicio para 64 bits, que no necesite compatibilidad hacia atrás y entre por defecto en el estado deseado. Me pregunto si ese era el objetivo o el diseño de Itanium
Lo mismo pasó con Itanium, es decir, Itanic
La compatibilidad hacia atrás es precisamente la razón clave para elegir x86 en lugar de ARM, MIPS, RISC-V, etc. Lamentablemente, parece que algunas personas en Intel y AMD no lo entienden bien
Es un proyecto excelente. Si los defensores de UEFI aquí se preguntan por qué alguien crearía otro método de bootloader, creo que se están perdiendo la razón por la que la gente hace este tipo de cosas
Tal como escribió el autor al final, “si llegaste hasta aquí, genial”: realmente es genial
Me pregunto cuánto tiempo ha pasado desde que salió UEFI. Qué lástima; ojalá hubieran descartado también BIOS junto con el modo largo
Que esté deprecado no significa que haya sido eliminado, sino que ya no se actualiza ni se desarrolla, con el objetivo de retirarlo
Me pregunto si este procedimiento de arranque también funciona en EFI/UEFI. Si funciona, también me pregunto si el supervisor de UEFI emula las transiciones entre modo real, modo protegido y modo largo, o si se realizan en el hardware real
Escribí aquí el proceso para crear un bootloader UEFI para un OS de hobby: https://0xc0ffee.netlify.app/osdev/05-bootloader-p1.html
Me pregunto si esto es más simple en ARM