Análisis del proceso de arranque del kernel de Linux

Hola a todos!

Mientras Leonid se está preparando para su primera lección abierta en nuestro curso de Administrador de Linux , seguimos hablando de cargar el kernel de Linux.

Vamos!

Comprender cómo funciona un sistema sin fallas: preparación para solucionar las averías inevitables

La broma más antigua en el campo de código abierto es la declaración de que "el código se documenta a sí mismo". La experiencia ha demostrado que leer el código fuente es como escuchar las previsiones meteorológicas: las personas inteligentes aún saldrán a mirar el cielo. A continuación se presentan sugerencias para verificar y examinar el arranque del sistema Linux utilizando herramientas de depuración familiares. Un análisis del proceso de arranque de un sistema que funciona bien prepara a los usuarios y desarrolladores para resolver fallas inevitables.

Por un lado, el proceso de descarga es sorprendentemente simple. El núcleo del sistema operativo (kernel) se ejecuta con un solo subproceso y sincrónicamente en un núcleo (núcleo), lo que puede parecer comprensible incluso para una mente humana patética. Pero, ¿cómo comienza el núcleo del sistema operativo? ¿Qué funciones hacen initrd ( un disco RAM para la inicialización inicial ) y los cargadores de arranque? Y espera, ¿por qué el LED en el puerto Ethernet siempre está encendido?



Siga leyendo para obtener respuestas a estas y algunas otras preguntas; El código para las demostraciones y ejercicios descritos también está disponible en GitHub .

Inicio del arranque: estado desactivado

Wake-on-LAN

Un estado de APAGADO significa que el sistema no tiene energía, ¿verdad? La aparente simplicidad es engañosa. Por ejemplo, el LED de Ethernet está encendido incluso en este estado, porque la activación en LAN (WOL, activación en [señal desde] red local) está activada en su sistema. Asegúrate escribiendo:

$# sudo ethtool <interface name> 

Donde, en cambio, puede ser, por ejemplo, eth0 (ethtool está en paquetes Linux con el mismo nombre). Si el "Wake-on" en la salida muestra g, los hosts remotos pueden iniciar el sistema enviando MagicPacket . Si no desea encender su sistema de forma remota y dar esta oportunidad a otros, desactive WOL en el menú del BIOS del sistema o use:

 $# sudo ethtool -s <interface name> wol d 

Un procesador que responde a MagicPacket puede ser un controlador de administración de placa base (BMC) o parte de una interfaz de red.

Intel Management Engine, Platform Controller Hub y Minix

BMC no es el único microcontrolador (MCU) que puede "escuchar" un sistema apagado nominalmente. Los sistemas X86_64 tienen el paquete de software Intel Management Engine (IME) para la administración remota de sistemas. Una amplia gama de dispositivos, desde servidores hasta computadoras portátiles, tienen tecnología que tiene características como KVM Remote Control o Intel Capability Licensing Service. Según la herramienta de Inte l, IME tiene vulnerabilidades sin parches. La mala noticia es que deshabilitar IME es difícil. Trammell Hudson creó el proyecto me_cleaner, que borra algunos de los componentes IME más atroces, como el servidor web incorporado, pero al mismo tiempo existe la posibilidad de que el uso del proyecto convierta el sistema en el que se está ejecutando.

El firmware IME y el programa de Modo de administración del sistema (SMM) que lo sigue en el arranque se basan en el sistema operativo Minix y se ejecutan en un procesador Hub de controlador de plataforma separado, no en la CPU principal del sistema. Luego, SMM lanza el programa Universal Extensible Firmware Interface (UEFI) en el procesador principal, que se ha escrito más de una vez . El grupo Coreboot lanzó un proyecto de firmware reducido no extensible (NERF) espectacularmente ambicioso en Google, que tiene como objetivo reemplazar no solo UEFI, sino también los primeros componentes del espacio de usuario de Linux, como systemd. Mientras tanto, estamos esperando los resultados, los usuarios de Linux pueden comprar computadoras portátiles de Purism, System76 o Dell, en las que IME está desactivado , además, podemos esperar computadoras portátiles con un procesador ARM de 64 bits .

Cargadores

¿Qué hace el firmware de arranque además de lanzar el presunto spyware? La tarea del gestor de arranque es proporcionar al procesador que se acaba de encender con los recursos necesarios para ejecutar un sistema operativo de propósito general como Linux. Durante el encendido, no solo hay memoria virtual, sino también DRAM hasta el momento de elevar su controlador. Luego, el cargador de arranque enciende las fuentes de alimentación y escanea los buses e interfaces para encontrar la imagen del núcleo y el sistema de archivos raíz. Los cargadores de arranque populares, como U-Boot y GRUB, admiten interfaces comunes como USB, PCI y NFS, así como otros dispositivos integrados más especializados, como NOR y NAND-flash. Los cargadores también interactúan con dispositivos de hardware de seguridad, como Trusted Platform Module (TPM) , para establecer una cadena de confianza desde el inicio de la descarga.


Ejecutando el cargador U-boot en el sandbox en el servidor de compilación.

El popular cargador de arranque U-Boot de código abierto es compatible con sistemas desde Raspberry Pi hasta dispositivos Nintendo, tableros de automóviles y Chromebooks. No hay registro del sistema, y ​​si algo sale mal, puede que ni siquiera haya salida de consola. Para facilitar la depuración, el equipo de U-Boot proporciona un entorno limitado para probar parches en el host de compilación o incluso en el sistema de integración continua. En un sistema con herramientas de desarrollo comunes como Git y GNU Compiler Collection (GCC) instaladas, comprender el entorno limitado de U-Boot es fácil.

 $# git clone git://git.denx.de/u-boot; cd u-boot $# make ARCH=sandbox defconfig $# make; ./u-boot => printenv => help 

Eso es todo: lanzó U-Boot en x86_64 y puede probar características complicadas, por ejemplo, repartición de dispositivos de almacenamiento ficticios , manipulación de claves secretas basadas en TPM y conexión en caliente de dispositivos USB. El entorno limitado U-Boot puede ser de una etapa dentro del depurador GDB. El desarrollo utilizando el sandbox es 10 veces más rápido que las pruebas sobrescribiendo el gestor de arranque en el tablero, además, el sandbox "ladrillo" se puede restaurar presionando Ctrl + C.

Lanzamiento de Kernel

Suministro de kernel de arranque

Una vez completadas sus tareas, el gestor de arranque cambiará al código del núcleo que cargó en la memoria principal y comenzará a ejecutarlo, pasando todos los parámetros de línea de comando que especificó el usuario. ¿Qué programa es el kernel? file / boot / vmlinuz muestra que esto es bzImage. El árbol fuente de Linux tiene una herramienta extract-vmlinux que puede usar para extraer el archivo:

 $# scripts/extract-vmlinux /boot/vmlinuz-$(uname -r) > vmlinux $# file vmlinux vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped 

El kernel es un archivo binario de formato ejecutable y de enlace (ELF) , como los programas de espacio de usuario de Linux. Esto significa que podemos usar comandos binutils como readelf para aprenderlo. Compare, por ejemplo, las siguientes conclusiones:

 $# readelf -S /bin/date $# readelf -S vmlinux 

La lista de particiones en archivos binarios es en su mayor parte similar.

Entonces, el kernel debería lanzar otros binarios ELF de Linux ... ¿Pero cómo se ejecutan los programas de espacio de usuario? En la función main() , ¿verdad? En realidad no

Antes de ejecutar la función main() , los programas necesitan un contexto de ejecución, que incluye memoria heap- (heap) y stack- (stack), además de descriptores de archivo para stdio , stdout y stderr . Los programas de espacio de usuario obtienen estos recursos de la biblioteca estándar ( glibc para la mayoría de los sistemas Linux). Considere lo siguiente:

 $# file /bin/date /bin/date: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=14e8563676febeb06d701dbee35d225c5a8e565a, stripped 

Los archivos binarios ELF tienen un intérprete, al igual que los scripts de Bash y Python. ¡Pero no necesita especificarse a través de #! como en los scripts, porque ELF es un formato nativo de Linux. El intérprete ELF proporciona al archivo binario todos los recursos necesarios llamando a _start() , una función disponible en el glibc fuente glibc , que se puede aprender a través de GDB . El núcleo, obviamente, no tiene un intérprete, y debería suministrarse de forma independiente, pero ¿cómo?

Un estudio sobre cómo iniciar un núcleo con GDB proporciona una respuesta a esta pregunta. Para comenzar, instale el paquete de depuración del núcleo, que contiene la versión sin cortar de vmlinux , por ejemplo, apt-get install linux-image-amd64-dbg . O compile e instale su propio núcleo desde alguna fuente, por ejemplo, siguiendo las instrucciones del excelente Manual del núcleo de Debian . gdb vmlinux seguido de info files muestra la sección ELF init.text . Indique el inicio de la ejecución del programa en init.text con l *(address) , donde dirección es el inicio hexadecimal de init.text . GDB indicará que el núcleo x86_64 se inicia en el arch/x86/kernel/head_64.S , donde encontramos la función de compilación start_cpu0() y el código que crea explícitamente la pila y descomprime zImage antes de llamar a x86_64 start_kernel() . Los núcleos ARM de 32 bits tienen un arch/arm/kernel/head.S. start_kernel() arch/arm/kernel/head.S. start_kernel() es independiente de la arquitectura, por lo que la función se encuentra en el núcleo init/main.c Podemos decir que start_kernel() es una función real main() Linux.

De start_kernel () a PID 1
Manifiesto del hardware del núcleo: tablas ACPI y árboles de dispositivos

Al arrancar, el kernel necesita información sobre el hardware además del tipo de procesador para el que fue compilado. Las instrucciones en el código se complementan con datos de configuración, que se almacenan por separado. Hay dos métodos principales para almacenar datos: árboles de dispositivos y tablas ACPI . A partir de estos archivos, el kernel descubre qué equipo debe ejecutarse en cada arranque.

Para dispositivos integrados, el árbol de dispositivos (DU) es un manifiesto del equipo instalado. DU es un archivo que se compila al mismo tiempo que la fuente del núcleo y generalmente se encuentra en / boot junto con vmlinux . Para ver qué hay en el árbol de dispositivos binarios en el dispositivo ARM, simplemente use el comando de strings del paquete binutils en el archivo cuyo nombre corresponde a /boot/*.dtb , ya que dtb significa el archivo binario del árbol de dispositivos (Device-Tree Binary). Puede cambiar el control remoto editando los archivos similares a JSON en los que se compone y reiniciando el compilador dtc especial provisto con la fuente del núcleo. DU es un archivo estático cuya ruta generalmente se pasa al núcleo por los gestores de arranque en la línea de comando, pero en los últimos años se ha agregado una superposición de árbol de dispositivos donde el núcleo puede cargar dinámicamente fragmentos adicionales en respuesta a eventos de conexión en caliente después de la carga.

La familia x86 y muchos dispositivos de nivel empresarial ARM64 utilizan el mecanismo alternativo de Interfaz avanzada de configuración y energía ( ACPI) . A diferencia del control remoto, la información ACPI se almacena en el sistema de archivos virtual /sys/firmware/acpi/tables , que es creado por el núcleo al inicio al acceder a la ROM interna. Para leer las tablas ACPI, use el comando acpica-tools paquete acpica-tools . Aquí hay un ejemplo:


Las tablas ACPI en las computadoras portátiles Lenovo están listas para Windows 2001.

Sí, su sistema Linux está listo para Windows 2001 si desea instalarlo. ACPI tiene métodos y datos, en contraste con el control remoto, que es más como un lenguaje de descripción de hardware. Los métodos ACPI continúan activos después del arranque. Por ejemplo, si ejecuta el comando acpi_listen (desde el paquete apcid) y luego cierra y abre la tapa de la computadora portátil, verá que la funcionalidad ACPI continuó funcionando todo este tiempo. La reescritura temporal y dinámica de las tablas ACPI es posible, pero el cambio permanente requerirá la interacción con el menú del BIOS al arrancar o flashear la ROM. En lugar de tales complejidades, quizás debería simplemente instalar coreboot , un reemplazo para el firmware de código abierto.

De start_kernel () al espacio de usuario

El código en init/main.c es sorprendentemente fácil de leer y, curiosamente, todavía usa los derechos de autor originales de Linus Torvalds de 1991-1992. Líneas encontradas en dmesg | head dmesg | head sistema en ejecución se origina básicamente en este archivo fuente. El sistema registra la primera CPU, las estructuras de datos globales se inicializan, una tras otra, se activan el planificador, los controladores de interrupciones (IRQ), los temporizadores y la consola. Todas las marcas de tiempo antes de ejecutar timekeeping_init() son cero. Esta parte de la inicialización del núcleo es síncrona, es decir, la ejecución se produce en un solo subproceso. Las funciones no se ejecutan hasta que se completa y se devuelve la última. Como resultado, la salida de dmesg será totalmente reproducible incluso entre los dos sistemas, siempre que tengan el mismo control remoto o tablas ACPI. Linux también se comporta como un sistema operativo en tiempo real (RTOS) que se ejecuta en una MCU, como QNX o VxWorks. Esta situación se almacena en la función rest_init() , que es llamada por start_kernel() en el momento de su finalización.


Una breve descripción del proceso inicial de arranque del kernel

El modesto nombre rest_init() crea un nuevo hilo que ejecuta kernel_init() , que a su vez llama a do_initcalls() . Los usuarios pueden monitorear la operación de initcalls agregando initcalls_debug a la línea de comando del kernel. Como resultado, obtendrá la entidad dmesg cada vez que ejecute la función initcall . initcalls pasa por siete niveles consecutivos: temprano, núcleo, postcore, arch, subsys, fs, dispositivo y tarde. La parte más notable de initcalls para los usuarios es la identificación e instalación de dispositivos periféricos del procesador: buses, red, almacenamiento, pantallas, etc., acompañados de la carga de sus módulos de kernel. rest_init() también crea un segundo subproceso en el procesador de arranque, que comienza ejecutando cpu_idle() mientras el programador distribuye su trabajo.

kernel_init() también configura el multiprocesamiento simétrico (SMP). En los núcleos modernos, puede encontrar este momento en la salida de dmesg con la línea "Activando CPU secundarias ...". SMP luego realiza la conexión en caliente de la CPU, lo que significa que gestiona su ciclo de vida utilizando una máquina de estado condicionalmente similar a la utilizada en dispositivos como memorias USB con detección automática. El sistema de administración de energía del núcleo a menudo apaga los núcleos individuales (núcleos) y los activa según sea necesario para que el mismo código de CPU de conexión en caliente se llame repetidamente en una máquina desocupada. Eche un vistazo a cómo un sistema de administración de energía offcputime.py CPU utilizando una herramienta BCC llamada offcputime.py .

Tenga en cuenta que el código en init/main.c casi terminó de ejecutarse cuando se smp_init() . El procesador de arranque completó la mayor parte de la inicialización única, que otros núcleos no necesitan repetir. Sin embargo, se deben crear subprocesos para cada núcleo para controlar las interrupciones (IRQ), la cola de trabajo, los temporizadores y los eventos de potencia en cada uno. Por ejemplo, observe los hilos del procesador que sirven softirqs y colas de trabajo con el comando ps -o psr. psr ps -o psr.

 $\# ps -o pid,psr,comm $(pgrep ksoftirqd) PID PSR COMMAND 7 0 ksoftirqd/0 16 1 ksoftirqd/1 22 2 ksoftirqd/2 28 3 ksoftirqd/3 $\# ps -o pid,psr,comm $(pgrep kworker) PID PSR COMMAND 4 0 kworker/0:0H 18 1 kworker/1:0H 24 2 kworker/2:0H 30 3 kworker/3:0H [ . . . ] 

donde el campo PSR significa "procesador". Cada núcleo debe tener sus propios temporizadores y controladores de conexión en caliente cpuhp.

Y finalmente, ¿cómo se lanza el espacio de usuario? Hacia el final, kernel_init() buscando un initrd que pueda iniciar el proceso init en su nombre. Si no, el núcleo ejecuta init sí solo. ¿Por qué entonces se puede necesitar initrd ?

Espacio de usuario temprano: ¿quién ordenó initrd?

Además del árbol de dispositivos, otra ruta de inicio al archivo, opcionalmente proporcionada por el núcleo en el arranque, pertenece a initrd . initrd menudo se encuentra en / boot junto con el archivo bzImage vmlinuz en x86, o con un árbol similar de imágenes y dispositivos para ARM. Se puede ver una lista de contenidos intrd usando la herramienta lsinitramfs , que es parte del paquete initramfs-tools-core . La imagen de distribución initrd contiene los directorios mínimos /bin , /sbin y /etc , así como los módulos del núcleo y los archivos en /scripts . Todo debería parecer más o menos familiar, ya que initrd en su mayor parte similar al sistema simplificado de archivos raíz de Linux. Esta similitud es un poco engañosa, ya que casi todos los ejecutables en /bin y /sbin dentro de ramdisk son enlaces simbólicos al binario BusyBox , lo que hace que los directorios / bin y / sbin sean 10 veces más pequeños que en glibc .

¿Por qué intentar crear un initrd si lo único que hace es cargar algunos módulos y ejecutar init en un sistema de archivos raíz normal? Considere un sistema de archivos raíz encriptado. El descifrado puede depender de cargar el módulo del núcleo almacenado en /lib/modules sistema de archivos raíz ... y, como se esperaba, en initrd . El módulo criptográfico puede compilarse estáticamente en el kernel y no cargarse desde un archivo, pero hay varias razones para rechazar esto. Por ejemplo, la compilación estática de un núcleo con módulos puede hacer que sea demasiado grande para caber en el almacenamiento disponible, o la compilación estática puede violar los términos de la licencia de software. Como era de esperar, los controladores de almacenamiento, las redes y los HID (dispositivos de entrada humanos) también se pueden representar en initrd , esencialmente cualquier código que no sea una parte necesaria del núcleo necesario para montar el sistema de archivos raíz. También en initrd, los usuarios pueden almacenar su propio código ACPI para tablas .


Diversión con shell de rescate e initrd personalizado.

initrd también initrd ideal para probar sistemas de archivos y dispositivos de almacenamiento. Coloque las herramientas de prueba en initrd y ejecute las pruebas desde la memoria, no desde el objeto de prueba.

Finalmente, cuando init ejecutando, ¡el sistema se está ejecutando! Dado que los procesadores secundarios ya están en funcionamiento, la máquina se ha convertido en una criatura asíncrona, paginada, impredecible y de alto rendimiento que todos conocemos y amamos. De hecho, ps -o pid,psr,comm -p indica que el proceso de init espacio de usuario ya no se ejecuta en el procesador de arranque.

Resumen

El proceso de arranque de Linux parece prohibido, dada la cantidad de software afectado, incluso en un dispositivo integrado simple. Por otro lado, el proceso de arranque es bastante simple, ya que no hay una complejidad excesiva causada por el desplazamiento de multitarea, RCU y condiciones de carrera. Prestando atención solo al kernel y al PID 1, se puede pasar por alto el gran trabajo realizado por los cargadores de arranque y los procesadores auxiliares para preparar la plataforma para el lanzamiento del kernel. El kernel es ciertamente diferente de otros programas de Linux, pero el uso de herramientas para trabajar con otros binarios ELF ayudará a comprender mejor su estructura. Estudiar un proceso de arranque viable preparará para futuros bloqueos.

El fin

Estamos esperando sus comentarios y preguntas, como de costumbre, ya sea aquí o en nuestra lección abierta donde Leonid quedará impresionado.

Source: https://habr.com/ru/post/es425505/


All Articles