¿Cómo ejecuta un programa por sí solo sin un sistema operativo en ejecución? ¿Puede crear programas de ensamblaje que la computadora pueda cargar y ejecutar al inicio, por ejemplo, iniciar la computadora desde una unidad flash y ejecutar el programa que está en la CPU?
assembly
x86
operating-system
bootloader
osdev
usuario2320609
fuente
fuente
Respuestas:
Coloca su código binario en un lugar donde el procesador busca después de reiniciar (por ejemplo, la dirección 0 en ARM).
Respuesta general a la pregunta: se puede hacer. A menudo se le conoce como "programación básica". Para leer desde una unidad flash, desea saber qué es USB y desea tener algún controlador para trabajar con este USB. El programa en esta unidad también tendría que estar en algún formato particular, en algún sistema de archivos particular ... Esto es algo que suelen hacer los cargadores de arranque, pero su programa podría incluir su propio cargador de arranque para que sea autónomo, si el firmware solo Cargue un pequeño bloque de código.
Muchas placas ARM le permiten hacer algunas de esas cosas. Algunos tienen cargadores de arranque para ayudarlo con la configuración básica.
Aquí puede encontrar un gran tutorial sobre cómo hacer un sistema operativo básico en una Raspberry Pi.
Editar: Este artículo, y todo el wiki.osdev.org responderá la mayoría de sus preguntas http://wiki.osdev.org/Introduction
Además, si no desea experimentar directamente en hardware, puede ejecutarlo como una máquina virtual utilizando hipervisores como qemu. Vea cómo ejecutar "hello world" directamente en hardware ARM virtualizado aquí .
fuente
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!):
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: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 unahlt
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:
que salidas:
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 de0xaa
.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 demain.img
con:que muestra lo esperado:
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
GitHub aguas arriba .
link.ld
Ensamblar y vincular con:
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 bitscli
: 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:
-kernel
opción, que carga un archivo ELF completo en la memoria. Aquí hay un ejemplo ARM que he creado con ese método .kernel7.img
, al igual que QEMU-kernel
.Solo con fines educativos, aquí hay un ejemplo mínimo de C en una etapa :
C Principal
entrada S
linker.ld
correr
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:como se muestra en este ejemplo .
Por ejemplo, puede redirigir
printf
a los sistemas UART o ARM, o implementarexit()
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:
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:
Los firmwares bien conocidos incluyen:
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 quemain.img
es un disco duro conectado al hardware, yhda
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 funcionarmostrar 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 :
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 almake isoimage
usarisohybrid
.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:
He subido:
algunos ejemplos simples de QEMU C + Newlib y ensamblaje sin formato aquí en GitHub .
El ejemplo prompt.c, por ejemplo, toma la entrada de su terminal host y devuelve la salida a través del UART simulado:
Ver también: ¿Cómo hacer programas ARM bare metal y ejecutarlos en QEMU?
una configuración de Blinker Raspberry Pi completamente automatizada en: https://github.com/cirosantilli/raspberry-pi-bare-metal-blinker
Consulte también: ¿Cómo ejecutar un programa en C sin SO en la Raspberry Pi?
Para "ver" los LED en QEMU, debe compilar QEMU desde la fuente con un indicador de depuración: /raspberrypi/56373/is-it-possible-to-get-the-state-of- los-leds-y-gpios-en-a-qemu-emulation-like-t
A continuación, debes probar un UART hello world. Puede comenzar con el ejemplo de intermitencia y reemplazar el núcleo con este: https://github.com/dwelch67/raspberrypi/tree/bce377230c2cdd8ff1e40919fdedbc2533ef5a00/uart01
Primero haga que el UART trabaje con Raspbian como lo he explicado en: /raspberrypi/38/prepare-for-ssh-without-a-screen/54394#54394 Se verá así:
Asegúrese de usar los pines correctos, o de lo contrario puede quemar su convertidor UART a USB, ya lo he hecho dos veces haciendo un cortocircuito a tierra y 5V ...
Finalmente, conéctese a la serie desde el host con:
Para la Raspberry Pi, utilizamos una tarjeta Micro SD en lugar de una memoria USB para contener nuestro ejecutable, para lo cual normalmente necesita un adaptador para conectarse a su computadora:
No se olvide de desbloquear el adaptador SD como se muestra en: /ubuntu/213889/microsd-card-is-set-to-read-only-state-how-can-i-write-data -en-it / 814585 # 814585
https://github.com/dwelch67/raspberrypi parece el tutorial más popular de Raspberry Pi de metal desnudo disponible en la actualidad.
Algunas diferencias de x86 incluyen:
IO se realiza por escrito a las direcciones de magia directamente, no hay es
in
yout
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
fuente
Sistema operativo como inspiración
El sistema operativo también es un programa , por lo que también podemos crear nuestro propio programa creando desde cero o cambiando (limitando o agregando) características de uno de los pequeños sistemas operativos , y luego ejecutarlo durante el proceso de arranque (usando una imagen ISO ) .
Por ejemplo, esta página se puede usar como punto de partida:
Cómo escribir un sistema operativo simple
¡Aquí, todo el sistema operativo cabe completamente en un sector de arranque de 512 bytes ( MBR )!
Este sistema operativo simple o similar se puede utilizar para crear un marco simple que nos permita:
Sin embargo, hay muchas posibilidades. Por ejemplo, para ver un sistema operativo de lenguaje ensamblador x86 más grande , podemos explorar el sistema operativo MykeOS , x86, que es una herramienta de aprendizaje para mostrar el funcionamiento simple de los sistemas operativos en modo real de 16 bits, con código bien comentado y amplia documentación .
Boot Loader como inspiración
Otro tipo común de programas que se ejecutan sin el sistema operativo también son cargadores de arranque . Podemos crear un programa inspirado en dicho concepto, por ejemplo, usando este sitio:
Cómo desarrollar tu propio cargador de arranque
El artículo anterior presenta también la arquitectura básica de tales programas :
Como podemos ver, esta arquitectura es muy flexible y nos permite implementar cualquier programa , no necesariamente un cargador de arranque.
En particular, muestra cómo usar la técnica de "código mixto" gracias a la cual es posible combinar construcciones de alto nivel (de C o C ++ ) con comandos de bajo nivel (de Assembler ). Este es un método muy útil, pero debemos recordar que:
El artículo muestra también cómo ver el programa creado en acción y cómo realizar sus pruebas y depuración.
Aplicaciones UEFI como inspiración
Los ejemplos anteriores utilizan el hecho de cargar el sector MBR en el medio de datos. Sin embargo, podemos profundizar en las profundidades, por ejemplo, con las aplicaciones UEFI :
Si nos gustaría comenzar a crear dichos programas , podemos, por ejemplo, comenzar con estos sitios web:
Programación para EFI: Creación de un programa "Hello, World" / Programación UEFI - Primeros pasos
Explorando los problemas de seguridad como inspiración
Es bien sabido que hay un grupo completo de software malicioso (que son programas) que se ejecutan antes de que se inicie el sistema operativo .
Un gran grupo de ellos opera en el sector MBR o aplicaciones UEFI, al igual que todas las soluciones anteriores, pero también hay otras que usan otro punto de entrada, como el Registro de inicio de volumen (VBR) o el BIOS :
o quizás otro también.
Ataques antes del inicio del sistema
Diferentes formas de arrancar
También creo que en este contexto también vale la pena mencionar que hay varias formas de arrancar el sistema operativo (o el programa ejecutable destinado a esto) . Hay muchos, pero me gustaría prestar atención a cargar el código de la red utilizando la opción Network Boot ( PXE ), que nos permite ejecutar el programa en la computadora independientemente de su sistema operativo e incluso independientemente de cualquier medio de almacenamiento que sea directamente conectado a la computadora:
¿Qué es el arranque de red (PXE) y cómo puede usarlo?
fuente