Ejemplos ejecutables
Técnicamente, un programa que se ejecuta sin un sistema operativo es un sistema operativo. Así que veamos cómo crear y ejecutar algunos minúsculos sistemas operativos hello world.
El código de todos los ejemplos a continuación está presente en este repositorio de GitHub .
Sector de arranque
En x86, lo más simple y de nivel más bajo que puede hacer es crear un Master Boot Sector (MBR) , que es un tipo de sector de arranque , y luego instalarlo en un disco.
Aquí creamos uno con una sola printf
llamada:
printf '\364%509s\125\252' > main.img
sudo apt-get install qemu-system-x86
qemu-system-x86_64 -hda main.img
Salir:
Probado en Ubuntu 18.04, QEMU 2.11.1.
main.img
contiene lo siguiente:
\364
en octal == 0xf4
en hexadecimal: la codificación de una hlt
instrucción, que le dice a la CPU que deje de funcionar.
Por lo tanto, nuestro programa no hará nada: solo iniciar y detener
Usamos octal porque \x
POSIX no especifica los números hexadecimales.
Podríamos obtener esta codificación fácilmente con:
echo hlt > a.asm
nasm -f bin a.asm
hd a
pero la 0xf4
codificación también está documentada en el manual de Intel, por supuesto.
%509s
Producir 509 espacios. Necesario para completar el archivo hasta el byte 510.
\125\252
en octal == 0x55
seguido de 0xaa
: bytes mágicos requeridos por el hardware. Deben ser bytes 511 y 512.
Si no está presente, el hardware no lo tratará como un disco de arranque.
Tenga en cuenta que incluso sin hacer nada, algunos caracteres ya están impresos en la pantalla. Los imprime el firmware y sirven para identificar el sistema.
Ejecutar en hardware real
Los emuladores son divertidos, pero el hardware es el verdadero negocio.
Sin embargo, tenga en cuenta que esto es peligroso, y podría borrar su disco por error: ¡solo haga esto en máquinas viejas que no contienen datos críticos! O incluso mejor, paneles de desarrollo como Raspberry Pi, vea el ejemplo ARM a continuación.
Para una computadora portátil típica, debe hacer algo como:
Grabe la imagen en una memoria USB (¡destruirá sus datos!):
sudo dd if=main.img of=/dev/sdX
conecte el USB a una computadora
encenderlo
dile que arranque desde el USB.
Esto significa hacer que el firmware elija USB antes que el disco duro.
Si ese no es el comportamiento predeterminado de su máquina, siga presionando Intro, F12, ESC u otras teclas extrañas después del encendido hasta que obtenga un menú de inicio donde puede seleccionar iniciar desde el USB.
A menudo es posible configurar el orden de búsqueda en esos menús.
Por ejemplo, en mi viejo Lenovo Thinkpad T430, UEFI BIOS 1.16, puedo ver:
Hola Mundo
Ahora que hemos creado un programa mínimo, pasemos a un mundo hola.
La pregunta obvia es: ¿cómo hacer IO? Algunas opciones:
- pedirle al firmware, por ejemplo, BIOS o UEFI, que lo haga por nosotros
- VGA: región de memoria especial que se imprime en la pantalla si se escribe en ella. Se puede usar en modo protegido.
- escriba un controlador y hable directamente al hardware de la pantalla. Esta es la forma "adecuada" de hacerlo: más potente, pero más complejo.
puerto serie . Este es un protocolo estandarizado muy simple que envía y recupera caracteres de un terminal host.
Fuente .
Desafortunadamente, no está expuesto en la mayoría de las computadoras portátiles modernas, pero es la forma más común de obtener placas de desarrollo, consulte los ejemplos de ARM a continuación.
Esto es realmente una pena, ya que tales interfaces son realmente útiles para depurar el kernel de Linux, por ejemplo .
usar funciones de depuración de chips. ARM llama a ellos semihosting, por ejemplo. En hardware real, requiere soporte adicional de hardware y software, pero en emuladores puede ser una opción conveniente y gratuita. Ejemplo .
Aquí haremos un ejemplo de BIOS, ya que es más simple en x86. Pero tenga en cuenta que no es el método más robusto.
red eléctrica
.code16
mov $msg, %si
mov $0x0e, %ah
loop:
lodsb
or %al, %al
jz halt
int $0x10
jmp loop
halt:
hlt
msg:
.asciz "hello world"
link.ld
SECTIONS
{
. = 0x7c00;
.text :
{
__start = .;
*(.text)
. = 0x1FE;
SHORT(0xAA55)
}
}
Ensamblar y vincular con:
gcc -c -g -o main.o main.S
ld --oformat binary -o main.img -T linker.ld main.o
Salir:
Probado en: Lenovo Thinkpad T430, UEFI BIOS 1.16. Disco generado en un host Ubuntu 18.04.
Además de las instrucciones de montaje estándar del usuario, tenemos:
.code16
: le dice a GAS que muestre código de 16 bits
cli
: deshabilita las interrupciones de software. Esos podrían hacer que el procesador vuelva a funcionar después dehlt
int $0x10
: hace una llamada de BIOS. Esto es lo que imprime los personajes uno por uno.
Los indicadores de enlace importantes son:
--oformat binary
: genera el código de ensamblaje binario sin formato, no lo deforma dentro de un archivo ELF, como es el caso de los archivos ejecutables del usuario habitual.
Use C en lugar de ensamblar
Como C se compila para ensamblar, usar C sin la biblioteca estándar es bastante simple, básicamente solo necesitas:
- Un script de enlace para poner las cosas en la memoria en el lugar correcto
- banderas que le dicen a GCC que no use la biblioteca estándar
- un pequeño punto de entrada de ensamblaje que establece el estado C requerido
main
, en particular:
TODO: enlace, así que algún ejemplo x86 en GitHub. Aquí hay un ARM que he creado .
Sin embargo, las cosas se vuelven más divertidas si desea utilizar la biblioteca estándar, ya que no tenemos el kernel de Linux, que implementa gran parte de la funcionalidad de la biblioteca estándar C a través de POSIX .
Algunas posibilidades, sin recurrir a un sistema operativo completo como Linux, incluyen:
Newlib
Ejemplo detallado en: https://electronics.stackexchange.com/questions/223929/c-standard-libraries-on-bare-metal/223931
En Newlib, debe implementar las llamadas al sistema usted mismo, pero obtiene un sistema muy mínimo y es muy fácil implementarlas.
Por ejemplo, puede redirigir printf
a los sistemas UART o ARM, o implementar exit()
con semihosting .
sistemas operativos integrados como FreeRTOS y Zephyr .
Dichos sistemas operativos generalmente le permiten desactivar la programación preventiva, lo que le brinda un control total sobre el tiempo de ejecución del programa.
Pueden verse como una especie de Newlib pre-implementado.
BRAZO
En ARM, las ideas generales son las mismas. He subido:
Para Raspberry Pi, https://github.com/dwelch67/raspberrypi parece el tutorial más popular disponible en la actualidad.
Algunas diferencias de x86 incluyen:
IO se realiza por escrito a las direcciones de magia directa, no hay in
y out
las instrucciones.
Esto se llama memoria asignada IO .
para un hardware real, como Raspberry Pi, puede agregar el firmware (BIOS) usted mismo a la imagen del disco.
Eso es algo bueno, ya que hace que la actualización de ese firmware sea más transparente.
Firmware
En verdad, su sector de arranque no es el primer software que se ejecuta en la CPU del sistema.
Lo que realmente se ejecuta primero es el llamado firmware , que es un software:
- hecho por los fabricantes de hardware
- fuente típicamente cerrada pero probablemente basada en C
- almacenado en memoria de solo lectura y, por lo tanto, más difícil / imposible de modificar sin el consentimiento del proveedor.
Los firmwares bien conocidos incluyen:
- BIOS : antiguo firmware x86 totalmente presente. SeaBIOS es la implementación de código abierto predeterminada utilizada por QEMU.
- UEFI : sucesor de BIOS, mejor estandarizado, pero más capaz e increíblemente hinchado.
- Coreboot : el noble intento de código abierto de arco cruzado
El firmware hace cosas como:
haga un bucle sobre cada disco duro, USB, red, etc. hasta que encuentre algo de arranque.
Cuando ejecutamos QEMU, -hda
dice que main.img
es un disco duro conectado al hardware, y
hda
es el primero en probarse y se usa.
cargue los primeros 512 bytes en la dirección de memoria RAM 0x7c00
, coloque el RIP de la CPU allí y déjelo funcionar
mostrar cosas como el menú de inicio o las llamadas de impresión del BIOS en la pantalla
El firmware ofrece una funcionalidad similar a la de un sistema operativo, de la cual dependen la mayoría de los sistemas operativos. Por ejemplo, un subconjunto de Python ha sido portado para ejecutarse en BIOS / UEFI: https://www.youtube.com/watch?v=bYQ_lq5dcvM
Se puede argumentar que los firmwares son indistinguibles de los sistemas operativos, y que el firmware es la única programación "verdadera" de metal desnudo que uno puede hacer.
Como dice este desarrollador de CoreOS :
La parte dificil
Cuando enciende una PC, los chips que conforman el conjunto de chips (northbridge, southbridge y SuperIO) aún no se inicializan correctamente. A pesar de que la ROM del BIOS está tan alejada de la CPU como podría estar, la CPU puede acceder a esto, porque tiene que estarlo, de lo contrario la CPU no tendría instrucciones para ejecutar. Esto no significa que la ROM del BIOS esté completamente asignada, generalmente no. Pero solo se asigna lo suficiente para iniciar el proceso de arranque. Cualquier otro dispositivo, solo olvídalo.
Cuando ejecuta Coreboot en QEMU, puede experimentar con las capas superiores de Coreboot y con cargas útiles, pero QEMU ofrece pocas oportunidades para experimentar con el código de inicio de bajo nivel. Por un lado, la RAM solo funciona desde el principio.
Publicar estado inicial del BIOS
Al igual que muchas cosas en el hardware, la estandarización es débil, y una de las cosas en las que no debe confiar es en el estado inicial de los registros cuando su código comienza a ejecutarse después del BIOS.
Hágase un favor y use un código de inicialización como el siguiente: https://stackoverflow.com/a/32509555/895245
Los registros tienen gusto %ds
y %es
tienen efectos secundarios importantes, por lo que debe ponerlos a cero incluso si no los está usando explícitamente.
Tenga en cuenta que algunos emuladores son mejores que el hardware real y le dan un buen estado inicial. Luego, cuando se ejecuta en hardware real, todo se rompe.
GNU GRUB Multiboot
Los sectores de arranque son simples, pero no son muy convenientes:
- solo puede tener un sistema operativo por disco
- el código de carga debe ser realmente pequeño y encajar en 512 bytes. Esto podría resolverse con la llamada int 0x13 BIOS .
- tienes que iniciar mucho tú mismo, como pasar al modo protegido
Es por esas razones que GNU GRUB creó un formato de archivo más conveniente llamado multiboot.
Ejemplo de trabajo mínimo: https://github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world
También lo uso en mi repositorio de ejemplos de GitHub para poder ejecutar fácilmente todos los ejemplos en hardware real sin quemar el USB un millón de veces. En QEMU se ve así:
Si prepara su sistema operativo como un archivo de arranque múltiple, GRUB podrá encontrarlo dentro de un sistema de archivos normal.
Esto es lo que hacen la mayoría de las distribuciones, poner las imágenes del sistema operativo debajo /boot
.
Los archivos de arranque múltiple son básicamente un archivo ELF con un encabezado especial. GRUB los especifica en: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
Puede convertir un archivo de arranque múltiple en un disco de arranque con grub-mkrescue
.
El Torito
Formato que se puede grabar en CD: https://en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29
También es posible producir una imagen híbrida que funcione en ISO o USB. Esto se puede hacer con grub-mkrescue
( ejemplo ), y también lo hace el kernel de Linux al make isoimage
usarlo isohybrid
.
Recursos