1 puntos por GN⁺ 2025-01-10 | 1 comentarios | Compartir por WhatsApp
  • SerenityOS funcionaba bien principalmente en QEMU, pero en una laptop real empezaron a aparecer bloqueos desde el arranque, la depuración y el acceso al almacenamiento, dejando al descubierto vacíos en el soporte de hardware
  • El equipo de prueba fue una Dell 3100 Chromebook con Intel Celeron N4020, 4 GB DDR4, eMMC de 32 GB y pantalla TN de 1366×768; la depuración con carcasa cerrada basada en Cr50, que se esperaba usar, falló en esta placa
  • Al quedar bloqueada la ruta de Cr50, se instaló internamente una Pi Pico basada en RP2040 y se conectó directamente al UART y a la flash SPI, creando con CircuitPython y serprog un dispositivo temporal de depuración y flasheo llamado PicoCCD
  • Como al inicio era difícil usar directamente el UART 16550 MMIO detrás de PCI, los primeros logs de arranque se obtuvieron usando el puerto de E/S 0x80 registrado por el EC de ChromeOS como un canal de salida temporal y lento
  • El soporte de eMMC, tras resolver diferencias de inicialización SD/MMC, la falta de control de energía en SDHCI y la desactivación de comandos exclusivos de SD, llegó hasta algunas sesiones gráficas, aunque todavía quedan pendientes el rendimiento, la estabilidad y ordenar los parches

La Dell 3100 Chromebook elegida como hardware real

  • Al intentar involucrarse más a fondo en SerenityOS, la primera debilidad que llamó la atención fue que se ejecutaba en QEMU, pero el soporte para hardware real era insuficiente
  • El soporte de UEFI ya estaba siendo trabajado por spholz, así que no se tocó; para este trabajo bastaba con que el kernel de la rama master arrancara con GRUB sobre el runtime UEFI de TianoCore
  • Se quería evitar depurar el OS en la misma máquina usada como equipo principal de desarrollo, y se eligió hardware relativamente reciente, lo bastante usable para el día a día
  • Buscando una Chromebook barata en Allegro, se compró una Dell 3100 por 95 PLN, unos 25 EUR
    • Intel Celeron N4020, 2 núcleos, sin Hyper-Threading
    • 4 GB DDR4
    • eMMC integrada de 32 GB
    • Pantalla TN de 1366×768 impulsada por una IGP UHD600
    • 2 USB-A, 2 USB-C, conector de 3.5 mm
    • Un teclado que se sintió mejor que el de las laptops empresariales de gama alta de Dell
  • A partir de entonces, este equipo se denominó con el hostname octopus

Expectativas y fracaso de la depuración basada en Cr50

  • Una de las grandes razones para elegir una Chromebook fue que el chip de seguridad Cr50 y el controlador embebido ofrecen funciones útiles para depuración con carcasa cerrada
  • Casi todas las Chromebooks posteriores a 2018 pueden usar un puerto USB-C para depuración con un cable SuzyQ, y Cr50 normalmente expone tres dispositivos ttyUSB
    • Consola interna de Cr50
    • Consola AP, es decir, el puerto serial de la Chromebook
    • Consola cros_ec, es decir, el controlador embebido
  • El objetivo era acceder a la consola serial sin dejar la laptop abierta con cables colgando; considerando la emulación de teclas de cros_ec y el control del estado de energía, incluso parecía posible una forma simple de KVM
  • En la octopus real, Cr50 CCD no funcionó
    • Se hizo un cable SuzyQ nuevo y se verificó la soldadura con otra Chromebook, pero falló
    • octopus era una de las pocas laptops en las que CCD no funciona porque Dell no colocó algunas resistencias en la placa
    • Algunas personas dijeron haberlo logrado de forma limitada con una orientación específica del puerto y con el cargador conectado, pero en este equipo no funcionó en absoluto
  • Según información revisada después, la resistencia faltante debería afectar solo al flasheo SPI y no al propio puente USB, así que la razón exacta por la que la depuración Cr50 no funcionó en absoluto sigue sin estar clara

PicoCCD hecho con una Pi Pico

  • Tras quedar bloqueada la ruta de Cr50, se revisó si cabía una placa Pi Pico común dentro del espacio vacío del equipo, y cabía con margen suficiente
  • Se consultaron esquemáticos de laptops similares, pero no había ninguno que coincidiera exactamente con octopus
    • Un puerto de depuración grande de la placa era JTAG y puntos de prueba relacionados con Intel, y no servía para el objetivo
    • El otro era Google Servo, pero Google publicó varias sondas de depuración con el nombre Servo y la documentación era limitada, por lo que fue difícil encontrarlo
    • Como documentación relacionada se usó la documentación de Servo
  • Se activaron el applet UART de Glasgow y la detección de frecuencia, y mientras Linux imprimía repetidamente por /dev/ttyS1, se sondearon directamente los pads UART TX sospechosos
    • El pad TX se encontró en unos minutos
    • RX fue más difícil porque requería transmisión activa y tocar una línea equivocada podía reiniciar la placa; de hecho, ocurrieron dos reinicios
    • Después, los pines RX/TX del EC también se encontraron en unos 10 minutos
  • Los cables soldados se fijaron con epoxi de curado UV, y durante 6 meses no hubo problemas de conexión
  • También se aprovechó el periférico SPI del RP2040 para soldar 6 cables al chip flash, y se cortó la pista que iba al pin de write-protect para conectarla a GND y obtener acceso de escritura sin autorización de Cr50
  • Para el software se eligió CircuitPython
    • Porque permite subir scripts y datos como dispositivo USB de almacenamiento masivo
    • Puenteo de UART como dispositivo USB cdc_acm fue sencillo
    • Como también se conectó la flash SPI, hacía falta una función de flasheo
  • Como herramienta open source común de flasheo EEPROM/SPI se usó flashrom, y serprog, que proxifica SPI sobre UART, encajaba con el objetivo
    • Existía la implementación en C pico-serprog de stacksmashing, pero no convenía tener que reflashear la Pico cada vez que se flasheaba el BIOS
    • En su lugar, se implementó serprog en CircuitPython, tomando mucha referencia del applet serprog de Glasgow
  • El código resultante se organizó como una solución rápida de depuración con carcasa cerrada llamada PicoCCD, y el repositorio está en PicoCCD en Forgejo
  • WeirdTreeThing también escribió código C para RP2040 con un propósito similar, y existe su versión de PicoCCD

Obtener logs de arranque de SerenityOS

  • Se instaló Alpine Linux para depuración y se configuraron utilidades básicas para traer un kernel de SerenityOS compilado externamente
  • Después, la estructura creció hasta incluir descarga automática de artefactos desde la máquina de compilación, descompresión y sobrescritura del kernel, y una entrada de GRUB que extraía el .tar de userland
  • El tiempo de iteración desde un cambio hasta la prueba era de unos 20 segundos al momento de escribir, bastante razonable para hacking en bare metal
  • La primera entrada de arranque de GRUB era, en la práctica, multiboot /Kernel serial_debug, pero no había salida ni en pantalla ni por el puerto serial
  • Para el problema de salida en pantalla se encontró la opción de agregar insmod all_video a la entrada de arranque de GRUB; no lo resolvió por sí sola, pero era la dirección correcta
  • La falta de salida serial era un problema mayor
    • Los logs de coreboot habían estado llegando hasta unos segundos antes
    • El UART de ese equipo no era el 16550 tradicional mapeado a puertos, sino un 16550A basado en MMIO
    • En los logs de Linux, ttyS0 y ttyS1 aparecían como 16550A en direcciones MMIO
    • En lspci, el Intel Celeron/Pentium Silver Processor Serial IO UART Host Controller aparecía como dispositivo PCI

UART 16550 y el desvío por el puerto 0x80

  • Tradicionalmente, los dispositivos externos de la IBM PC se mapeaban a la E/S por puertos de la CPU x86 y se accedían con instrucciones como outb e inb
  • Muchos dispositivos luego migraron a MMIO, pero el puerto serial no dependía de competir por alta velocidad, así que el método antiguo permaneció y resultó útil como puerto de depuración
  • En un entorno común, algo como outb 0x3f8, 0x41 permite recibir A del otro lado; la inicialización también es corta, por lo que es fácil de implementar en proyectos pequeños
  • El UART de octopus era un dispositivo MMIO detrás de PCI, y en las primeras etapas de arranque de SerenityOS era difícil esperar que PCI ya estuviera inicializado
    • SerenityOS tiene una implementación de bus PCI, pero el arranque estaba en una etapa demasiado temprana
    • El PCISerialDevice existente tampoco se había usado en un contexto MMIO
    • Crear este controlador sin salida de depuración no era ideal
  • El controlador embebido de los dispositivos ChromeOS registra todas las escrituras al puerto de E/S 0x80
    • Este puerto se usa tradicionalmente para reportar el estado de POST
    • Los indicadores de código de arranque de 7 segmentos en las motherboards funcionan decodificando el puerto 80
  • Se probó la hipótesis en Linux con un script que escribía bytes en /dev/port, y esos bytes pudieron leerse en la consola cros_ec
  • Se insertó código como IO::out8(0x80, 1); cerca del punto de entrada de SerenityOS, Kernel/Arch/init.cpp, para rastrear la posición de avance, y el problema se acotó hasta el punto donde fallaba en Memory::MemoryManager::initialize(0);
  • Luego se intentó cambiar la dirección de la rutina de escritura serial de 0x3f8 a 0x80
    • Al principio salían muchos bytes, pero cros_ec no podía retransmitirlos de forma estable y se producía overflow
    • Más adelante, incluso la propia salida de logs de cros_ec se corrompía
    • Esto se debía a que, a diferencia de un chip serial real, no tenía un buffer grande
  • Se rodeó el problema insertando muchos estados de espera basados en nop entre cada escritura
    • Si se imprimían todos los mensajes de arranque, un arranque que tomaba segundos pasaba a tardar minutos
    • Aun así, se consideró un costo aceptable para depuración en bare metal
  • Para parsear automáticamente las líneas de log de cros_ec y decodificarlas como ASCII, se usó un one-liner de Bash combinando picocom, watch, grep, sed, cut y xxd

Framebuffer y primera salida gráfica

  • Después de obtener logs de arranque, durante varios días se intentó entender el problema leyendo directamente el código base, pero finalmente se pidió ayuda a la comunidad
  • spholz señaló el PR #24435 de SerenityOS, que estaba abierto en ese momento, y al compilar esa rama el generic framebuffer funcionó
  • En la pantalla apareció un resultado que se veía como si el trabajo hubiera fallado pero a la vez hubiera tenido éxito, y a partir de ahí los problemas de almacenamiento empezaron a hacerse evidentes

eMMC y problemas de inicialización SD/MMC

  • El fallo existente en StorageManagement terminaba en una assertion porque, tras fallar la inicialización del SD Host Controller, la lista de controladores quedaba vacía
    • En el log aparecían PCI: Failed to initialize SD Host Controller y ASSERTION FAILED: !m_controllers.is_empty()
    • Como resultado, se producía un kernel panic en StorageManagement::enumerate_storage_devices()
  • octopus tiene un chip eMMC de 32 GB, y como SerenityOS ya tenía parte de un controlador SD, parecía que bastaría con agregar soporte MMC
  • Para usar tarjetas SD/MMC hacen falta, en general, tres elementos
    • Host Controller: en equipos modernos suele ser SDHCI, especificado por la SD Association
    • El bus conectado al Host Controller: en este caso, PCI
    • La implementación del protocolo con el que se comunican el host y la tarjeta
  • Según los logs del fallo, SerenityOS tenía los dos primeros elementos, y el problema restante estaba en el protocolo
  • El protocolo SD tiene una especificación pública, pero MMC pasó a ser un estándar JEDEC en 2007 y desde entonces el acceso formal requiere pago
  • SD y MMC tienen secuencias de inicialización distintas
    • SerenityOS empezaba enviando CMD0 y esperando respuesta, algo que debería pasar tanto en SD como en MMC
    • Luego enviaba CMD8 para configurar el voltaje, pero MMC no lo soporta, por lo que debía generar un error
    • Algunas fuentes sugieren reiniciar después la tarjeta y considerarla MMC
    • Otras fuentes proponen un flujo más completo que combina los resultados de CMD8 y CMD58 para distinguir incluso la versión SD y el tipo de capacidad
  • No se implementaron todas las verificaciones avanzadas de compatibilidad, solo las básicas

Registro de control de energía faltante y solución

  • El flujo de inicialización de MMC, resumido, era el siguiente
    • Tras el reset, configurar el reloj a 400 KHz
    • Esperar 1 ms y luego esperar 74 ciclos de reloj más
    • Enviar CMD0 y esperar respuesta
    • Repetir CMD1 hasta que el bit 31 de la respuesta sea 1
    • Al terminar el bucle, guardar el valor como registro Operating Conditions
    • Continuar con el algoritmo de inicialización SD excluyendo las consultas a registros exclusivos de SD
    • Opcionalmente, detectar compatibilidad con High-Speed y activar uno de varios modos HS
  • El código llegaba aproximadamente hasta el paso 4, pero después la eMMC no respondía a ninguna solicitud
  • Tras varios días sin encontrar la causa, al eliminar el código relacionado con el reset del controlador la eMMC empezó a responder, y el problema se acotó a la función reset_host_controller()
  • Lo extraño de esa función era que no se podía encontrar el registro host_configuration en el estándar
    • El código anterior había agrupado varios registros en dos grupos arbitrarios llamados host_configuration
    • La inicialización tampoco estaba completa, y simplemente ponía el primer grupo en 0
  • El primer grupo incluía el registro Power Control, que controla el regulador de energía de la tarjeta
    • En cierto hardware, incluidas todas las implementaciones que usan eMMC, este registro es necesario para encender la propia tarjeta
    • En otros diseños donde el riel de alimentación se conecta directamente al slot, esta configuración puede ignorarse
  • Como solución temporal se tomó y usó el valor original que tenía host_configuration_0
  • El problema central era que se intentaba comunicarse con la tarjeta sin haberla encendido
  • Después tomó algunas horas más encontrar y desactivar ciertos comandos válidos solo para tarjetas SD, y con una salida de depuración más significativa desde el controlador, el resto del trabajo avanzó de forma relativamente normal

Estado actual y trabajo pendiente

  • Finalmente, SerenityOS logró mostrar, muy lentamente, una sesión gráfica parcialmente dañada, pero se bloqueó poco después
  • El problema de esta sesión gráfica y el proceso para normalizar de nuevo el framebuffer se cubrirán en una publicación posterior
  • Todo el trabajo fue un proceso de aprendizaje de unos 6 meses, durante los cuales también se avanzó en otras cosas
  • El siguiente objetivo es ordenar los parches y enviarlos upstream antes de que termine el año

1 comentarios

 
GN⁺ 2025-01-10
Opiniones de Hacker News
  • Alguna vez leí que adaptar los drivers de NetBSD a un kernel personalizado era relativamente fácil, y me pregunto si Serenity podría seguir un camino parecido.
    Para un OS nuevo, los drivers de dispositivos son un gran obstáculo.

    • Una de las filosofías de Serenity es hacer todo desde cero por cuenta propia siempre que sea posible, así que aunque los drivers de NetBSD sean fáciles de adaptar y la licencia sea compatible, probablemente escribirían sus propios drivers en vez de tomar ese camino.
    • Me preguntaba si, para un OS nuevo o de hobby, no sería mejor apuntar desde el inicio a una computadora de placa única popular como Raspberry Pi.
      Como la configuración de hardware es casi fija, se vuelve más fácil crear o portar drivers y probar el sistema.
    • rump kernel/anykernel es precisamente ese concepto.
      Permite ejecutar drivers en espacio de usuario con solo el soporte básico mínimo.
      https://en.wikipedia.org/wiki/Rump_kernel
    • Lo de NetBSD es confiable. Su libc es muy limpia y la he usado varias veces en otros proyectos.
      Eso sí, no sé mucho sobre la parte de drivers.
    • La solución es elegir un buen conjunto de hardware y, si es posible, que sea equipo vendido directamente por quien escribe el software, para crear drivers solo para ese hardware.
      Parece que eso es más o menos lo que Apple ha hecho desde el principio, y es el único caso de gran éxito en sistemas tipo Unix para consumidores.
      System76 también es casi un ejemplo de eso, y Frame.work es similar, aunque se enfoca menos en el OS en sí.
  • Haberlo hecho funcionar en una máquina donde todo jugaba en contra es un trabajo de hacking impresionante, y parece el resultado de un esfuerzo enorme de gente muy talentosa.

  • Cuando leo textos así, me pregunto cómo empezar en el mundo de los drivers y los OS.
    Parece tan complejo que no sé bien por dónde arrancar.

    • Comunicarse con hardware moderno en realidad suele ser bastante simple; la clave es leer y escribir memoria del hardware.
      A eso se le llama entrada/salida mapeada en memoria (MMIO). En una aplicación normal no se puede hacer, porque el kernel impide el acceso directo a la memoria del hardware.
      Para empezar necesitas un lenguaje como Rust/C++/C/Zig que pueda emitir código máquina para la CPU objetivo, y conviene que no tenga runtime ni GC. Si es tu primer lenguaje de bajo nivel, recomiendo C por la cantidad de ejemplos.
      También hay que aprender ensamblador básico de la CPU objetivo, porque algunas instrucciones quizá no estén disponibles como funciones integradas de un lenguaje de alto nivel.
      Luego, al escribir un kernel de hello world, aprendes cómo la CPU arranca el kernel y cómo se dividen los modos de ejecución y los niveles de privilegio.
      Después configuras la CPU como quieres; en x86, por ejemplo, pasas a long mode para usar instrucciones de 64 bits, y normalmente en esa etapa también configuras memoria virtual.
      Al llegar hasta ahí empiezas a entender cómo encaja la CPU con el OS, cómo enumerar los dispositivos disponibles y cómo encontrar sus ubicaciones de memoria; luego todavía queda mucho trabajo, como sistemas de archivos y schedulers.
      La diferencia entre el software que corre sobre un OS y el kernel del OS es, al final, el modo de CPU en el que se está ejecutando ese código, y con el máximo privilegio se pueden usar instrucciones que una app normal no puede usar.
    • Yo empecé con LDD. Es un libro de hace unos 10 años, pero sigue siendo relevante.
      Más tarde encontré verdaderas joyas escondidas en la documentación de FreeBSD; entre ellas, FreeBSD Architecture Handbook y FreeBSD Developers' Handbook pueden ser especialmente útiles.
      https://lwn.net/Kernel/LDD3/
      https://docs.freebsd.org/en/books/
    • Hace unos 15 años tomé una clase de desarrollo de OS en la universidad y usamos Minix.
      Minix está escrito de forma muy limpia, su kernel tiene unas 5 mil líneas y aparece en varios libros de texto.
      Implementé un servidor simple y también hice hacking del kernel; como Minix es un microkernel, la mayoría de los drivers funcionan de esa manera.
      Leí el material del curso por adelantado y casi no asistí a las clases, aun así saqué 8 de 10.
      También he escuchado muchas cosas buenas de NetBSD y SerenityOS, y Andreas hizo mucho del desarrollo por livestreaming.
      Cuando sabes por dónde empezar, en realidad se vuelve más fácil.
    • Honestamente, el primer paso es entender el propósito de cada componente, como OS, drivers y dispositivos.
      Por ejemplo, un driver de dispositivo se encarga de exponer una interfaz para que otros programas que se ejecutan en la computadora puedan acceder a cierto dispositivo y controlarlo.
      https://m.youtube.com/watch?v=juGNPLdjLH4 es un buen curso intensivo.
      También puedes hacer un dispositivo USB simple con algo como Arduino para intercambiar información con una PC. Ejemplo: https://m.youtube.com/watch?v=yTc2GLXfCOY
      Después, basta con entender qué hace el subsistema que te interesa, cómo ponerlo a funcionar, y escribir código. Almacenamiento, dispositivos gráficos, etc., entran en esa categoría.
      Raspberry Pi también puede ser un buen punto de partida para estos experimentos. Ejemplo: Writing a bare metal operating system for the raspberry pi https://github.com/babbleberry/rpi4-osdev
    • Este tutorial me pareció realmente bueno.
      https://wiki.osdev.org/Bare_Bones
  • Me gusta el concepto de SerenityOS y el navegador Ladybird, así que me alegra ver este avance.

    • Lamentablemente, ahora los dos se separaron.
      Ladybird no solo se convirtió en un proyecto independiente, sino que ya no considera a SerenityOS como plataforma objetivo.
      Ladybird está eliminando poco a poco su propia capa Serenity y reemplazándola por alternativas más mainstream.
      Como usuario principalmente de Linux, me entusiasma que Ladybird se esté convirtiendo en una alternativa real en Linux.
      Pero como fan de SerenityOS, me da pena que la energía y la innovación que iban a Ladybird estén saliendo de SerenityOS.
  • Si necesitas ayuda con hacking de Chromebooks, puedes preguntar en la lista de correo chromium-os-dev.
    Creo que alguien podría ayudarte a hacer funcionar CCD.
    https://groups.google.com/a/chromium.org/g/chromium-os-dev?p...

  • El bootloader Depthcharge también soporta arranque por red vía TFTP.
    Hay que compilarlo directamente y flashearlo en el SPI, pero es una función excelente para el desarrollo iterativo de kernels.
    https://chromium.googlesource.com/chromiumos/platform/depthc...

  • Pensaba que SerenityOS ya corría en hardware real; ¿todavía corre todo solo dentro de QEMU?

    • Es cierto que en el pasado se ejecutó en hardware real.
      Pero casi no tenía drivers dignos de mención, así que solo funcionaba en el sentido más básico, únicamente en hardware específico, y probablemente no corría demasiado bien.
      Este intento busca lograr que funcione de forma confiable al menos en una plataforma de hardware real.
  • Serenity sigue siendo impresionante, incluso cuando no estoy de acuerdo con la forma en que implementa algunas cosas.

  • Vine a ver cosas aterradoras como esta.
    doas dd seek=$((0x$1)) bs=1 count=1 of=/dev/port < <(xxd -p -r <<< "$2")

    • Como persona común, de verdad me da curiosidad qué significa ese código.