Ejemplos ejecutables
Vamos a crear y ejecutar algunos minúsculos programas hello world que se ejecutan sin un sistema operativo en:
También los probaremos en el emulador QEMU tanto como sea posible, ya que es más seguro y más conveniente para el desarrollo. Las pruebas de QEMU se realizaron en un host Ubuntu 18.04 con la QEMU 2.11.1 preempaquetada.
El código de todos los ejemplos x86 a continuación y más está presente en este repositorio de GitHub .
Cómo ejecutar los ejemplos en hardware real x86
Recuerde que ejecutar ejemplos en hardware real puede ser peligroso, por ejemplo, podría borrar su disco o bloquear el hardware por error: ¡solo haga esto en máquinas viejas que no contienen datos críticos! O incluso mejor, use paneles de control semi desechables baratos como el Raspberry Pi, vea el ejemplo ARM a continuación.
Para una laptop x86 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 seleccione USB antes que el disco duro.
Si ese no es el comportamiento predeterminado de su máquina, siga presionando Enter, 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 T430 veo lo siguiente.
Después de encender, es cuando tengo que presionar Enter para ingresar al menú de arranque:
Entonces, aquí tengo que presionar F12 para seleccionar el USB como dispositivo de arranque:
A partir de ahí, puedo seleccionar el USB como dispositivo de arranque de esta manera:
Alternativamente, para cambiar el orden de arranque y elegir el USB para tener mayor prioridad para que no tenga que seleccionarlo manualmente cada vez, presionaría F1 en la pantalla "Menú de interrupción de inicio" y luego navegaría a:
Sector de arranque
En x86, lo más simple y de nivel más bajo que puede hacer es crear un Sector de arranque maestro (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:
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.
Y en el T430 solo tenemos una pantalla en blanco con un cursor parpadeante:
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.S
as -o a.o a.S
objdump -S a.o
que salidas:
a.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: f4 hlt
pero también está documentado en el manual de Intel, por supuesto.
%509s
Produce 509 espacios. Necesario para completar el archivo hasta el byte 510.
\125\252
en octal == 0x55
seguido de 0xaa
.
Estos son 2 bytes mágicos requeridos que deben ser los bytes 511 y 512.
El BIOS revisa todos nuestros discos en busca de dispositivos de arranque, y solo considera aquellos de arranque que tienen esos dos bytes mágicos.
Si no está presente, el hardware no lo tratará como un disco de arranque.
Si no eres un printf
maestro, puedes confirmar el contenido de main.img
con:
hd main.img
que muestra lo esperado:
00000000 f4 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 |. |
00000010 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |
*
000001f0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 55 aa | U.|
00000200
donde 20
hay un espacio en ASCII.
El firmware del BIOS lee esos 512 bytes del disco, los guarda en la memoria y configura la PC en el primer byte para comenzar a ejecutarlos.
Hola sector de arranque mundial
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 recibe caracteres desde un terminal host.
En los escritorios, se ve así:
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 .
utilizar 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"
GitHub aguas arriba .
link.ld
SECTIONS
{
/* The BIOS loads the code from the disk to this location.
* We must tell that to the linker so that it can properly
* calculate the addresses of symbols we might jump to.
*/
. = 0x7c00;
.text :
{
__start = .;
*(.text)
/* Place the magic boot bytes at the end of the first 512 sector. */
. = 0x1FE;
SHORT(0xAA55)
}
}
Ensamblar y vincular con:
as -g -o main.o main.S
ld --oformat binary -o main.img -T link.ld main.o
qemu-system-x86_64 -hda main.img
Salir:
Y en el T430:
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 un código de ensamblaje binario sin formato, no lo envuelva dentro de un archivo ELF, como es el caso de los archivos ejecutables del usuario habitual.
Para comprender mejor la parte del script del enlazador, familiarícese con el paso de reubicación del enlace: ¿Qué hacen los enlazadores?
Programas de metal desnudo Cooler x86
Aquí hay algunas configuraciones de metal desnudo más complejas que he logrado:
Use C en lugar de ensamblar
Resumen: use el arranque múltiple de GRUB, que resolverá muchos problemas molestos en los que nunca pensó. Vea la sección a continuación.
La principal dificultad en x86 es que el BIOS solo carga 512 bytes del disco a la memoria, ¡y es probable que explote esos 512 bytes cuando usa C!
Para resolver eso, podemos usar un gestor de arranque de dos etapas . Esto hace más llamadas de BIOS, que cargan más bytes del disco a la memoria. Aquí hay un ejemplo mínimo de ensamblaje de la etapa 2 desde cero usando las llamadas int 0x13 BIOS :
Alternativamente:
- si solo necesita que funcione en QEMU pero no en hardware real, use la
-kernel
opción, que carga un archivo ELF completo en la memoria. Aquí hay un ejemplo ARM que he creado con ese método .
- para el Raspberry Pi, el firmware predeterminado se encarga de la carga de imágenes para nosotros desde un archivo ELF llamado
kernel7.img
, al igual que QEMU -kernel
.
Solo con fines educativos, aquí hay un ejemplo mínimo de C en una etapa :
C Principal
void main(void) {
int i;
char s[] = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
for (i = 0; i < sizeof(s); ++i) {
__asm__ (
"int $0x10" : : "a" ((0x0e << 8) | s[i])
);
}
while (1) {
__asm__ ("hlt");
};
}
entrada S
.code16
.text
.global mystart
mystart:
ljmp $0, $.setcs
.setcs:
xor %ax, %ax
mov %ax, %ds
mov %ax, %es
mov %ax, %ss
mov $__stack_top, %esp
cld
call main
linker.ld
ENTRY(mystart)
SECTIONS
{
. = 0x7c00;
.text : {
entry.o(.text)
*(.text)
*(.data)
*(.rodata)
__bss_start = .;
/* COMMON vs BSS: /programming/16835716/bss-vs-common-what-goes-where */
*(.bss)
*(COMMON)
__bss_end = .;
}
/* /programming/53584666/why-does-gnu-ld-include-a-section-that-does-not-appear-in-the-linker-script */
.sig : AT(ADDR(.text) + 512 - 2)
{
SHORT(0xaa55);
}
/DISCARD/ : {
*(.eh_frame)
}
__stack_bottom = .;
. = . + 0x1000;
__stack_top = .;
}
correr
set -eux
as -ggdb3 --32 -o entry.o entry.S
gcc -c -ggdb3 -m16 -ffreestanding -fno-PIE -nostartfiles -nostdlib -o main.o -std=c99 main.c
ld -m elf_i386 -o main.elf -T linker.ld entry.o main.o
objcopy -O binary main.elf main.img
qemu-system-x86_64 -drive file=main.img,format=raw
Biblioteca estándar C
Sin embargo, las cosas se vuelven más divertidas si también desea utilizar la biblioteca estándar de C, ya que no tenemos el kernel de Linux, que implementa gran parte de la funcionalidad de la biblioteca estándar de C a través de POSIX .
Algunas posibilidades, sin recurrir a un sistema operativo completo como Linux, incluyen:
Escribe lo tuyo. Es solo un montón de encabezados y archivos C al final, ¿verdad? ¿¿Correcto??
Newlib
Ejemplo detallado en: /electronics/223929/c-standard-libraries-on-bare-metal/223931
Implementos NEWLIB todas las cosas aburridas no OS específicas para que, por ejemplo memcmp
, memcpy
, etc.
Luego, proporciona algunos apéndices para que pueda implementar las llamadas al sistema que necesita.
Por ejemplo, podemos implementar exit()
en ARM a través de semihosting con:
void _exit(int status) {
__asm__ __volatile__ ("mov r0, #0x18; ldr r1, =#0x20026; svc 0x00123456");
}
como se muestra en este ejemplo .
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.
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
- tienes que iniciar mucho tu 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.
Resultado QEMU:
T430:
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, colocar las imágenes del sistema operativo /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
.
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 se 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.
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
usar isohybrid
.
BRAZO
En ARM, las ideas generales son las mismas.
No hay un firmware preinstalado semi estandarizado ampliamente disponible como BIOS para que podamos usar para el IO, por lo que los dos tipos más simples de IO que podemos hacer son:
- serial, que está ampliamente disponible en devboards
- parpadea el LED
He subido:
Algunas diferencias de x86 incluyen:
IO se realiza por escrito a las direcciones de magia directamente, no hay es 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.
Recursos