Telecamera IP con U-Boot

Nel primo articolo, abbiamo introdotto i dispositivi embedded ed iniziato a perlustrare ed estrarre informazioni dal firmware della camera IP di Reolink. Alla fine del post, abbiamo eseguito Binwalk che ha mostrato vari tipi di file: Flattened device tree, uImage, UBI File system.

Per il secondo articolo della serie Reolink, andremo ad introdurre la teoria riguardanti le varie fasi di booting del sistema operativo ed esploreremo i diversi tipi di file, cominciando così l’analisi.

Booting

La fase di booting (ovvero di caricamento) costituisce una fase critica del nostro sistema operativo. Quando accendiamo un qualsiasi dispositivo, sia esso una TV o un computer general purpose, il sistema deve eseguire una serie di routine per caricare moduli ed eseguibili che serviranno successivamente per proseguire nell’esecuzione di altri task. Gestione della rete, routine per caricare eseguibili: sono tutti servizi che garantiscono la stabilità del sistema operativo, essenziali quindi per iniziare ad utilizzare il dispositivo.

Appena prima di avviare il sistema operativo, tutti i dispositivi sono in uno stato di reset, l’interno della RAM è vuota e i dispositivi I/O sono pronti per essere chiamati ed eseguiti. Appena però si preme il pulsante di accensione ecco che una serie di azioni sono effettuate dai dispositivi per caricare il sistema operativo.

Ogni volta che si accende un dispositivo, l’esecuzione parte da un modulo chiamato ROM, fornito dal costruttore, che si preoccupa di effettuare alcuni test e iniziare ad caricare il sistema operativo. Una volta che viene caricato, il controllo passa al sistema operativo che ha piena facoltà di creare, modificare ed eliminare qualsiasi struttura dati e valore nella CPU, memoria e dispositivi I/O. Al bootloader spetta anche il compito di eseguire una serie di test (come il Power On Self Test) per verificare se tutto effettivamente funzioni a livello hardware. Test includono controlli di basso livello (come ad esempio sul voltaggio o sui circuiti) per verificare se la ROM può procedere o meno con l’avvio vero e proprio.

Senza scendere in dettagli troppo tecnici, possiamo pensare alla ROM o il bootloader come ad un programma. Il bootloader viene eseguito dalla CPU che provvede a caricare una serie di altri componenti del sistema operativo. Con i dispositivi embedded, costituiti da una specifica board (quindi una configurazione hardware particolare), possiamo ottimizzare l’avvio del sistema operativo. Il bootstrap ovvero la fase di loading deve essere molto veloce e deve richiedere quante più minime risorse per evitare di sprecare energia. Il requisito implicito che lo sviluppo di progetti embedded-based deve considerare è il tempo. Dal momento che questi dispositivi si interfacciano e vengono utilizzati in un contesto reale, il tempo di risposta deve essere molto molto breve, dal microsecondo ad arrivare a decine di nanosecondi.

U-Boot

La maggior parte dei sistemi operativi embedded utilizza come booloader U-Boot (anche chiamato Das U-Boot, da un gioco di parole basato sul classico film del 1981 Das Boot, ambientato su un sottomarino tedesco). Scritto principalmente in C ed Assembly, U-Boot è un progetto open-source sviluppato da Magnus Damm, considerato tutt’oggi il bootloader più ricco, flessibile e più attivamente sviluppato. Supporta diverse architetture, come ARM, MicroBlaze, MIPS, PPC, RISC-V, x86 e contiene anche alcuni driver per board di sviluppo di dispositivi embedded. Viene utilizzato da un vasto numero di apparati come Nintendo, Chromebooks, Raspberry PI ma anche board per automotive.

U-Boot supporta come file system FAT, ext2/3/4, CramFS, SquashFS, JFFS2, UBIF, ZFS e molti altri. Questo principalmente significa che U-Boot è molto potente, supportando sia diverse configurazioni di hardware che molteplici sistemi operativi. Il bootloader implementa un sottoinsieme di specifiche compatibili con sistemi UEFI e avvia il sistema operativo attraverso alcune variabili di ambiente (una in particolare, la più importante, bootargs) che affronteremo più avanti in questo articolo.

U-Boot richiede che i comandi specificati su bootargs siano di basso livello e pertanto specifichino in modo esplicito gli indirizzi di memoria fisica come destinazione per la copia dei dati. Questo da una parte aggiunge complessità per lo sviluppatore che deve conoscere i dettagli tecnici, ma dall’altra parte consente di avere un overhead (tempo di sovraccarico) minimo.

Un’altra caratteristica che U-Boot supporta è il Device Flattened Tree File, una struttura dati che viene usata per descrivere la configurazione hardware. Nei dispositivi general-purpose, quando si preme il pulsante di accensione, il bootloader interroga in modalità polling tutti i dispositivi per ottenere informazioni riguardo il costruttore, che tipo di dispositivo è, il suo stato e molte altre informazioni. Nei dispositivi embedded, non possiamo permetterci di sprecare tempo prezioso! Quindi includo all’interno del mio bootloader un Device Tree per consentire alla macchina di caricare già tutte le informazioni riguardo il mio hardware. Verrà esplorato in profondità il Device Tree durante la terza parte della serie.

Fase di Caricamento

Conclusa la parte più tecnica su U-Boot, specifichiamo cosa accade durante il caricamento dei sistemi operativi embedded unix-based per punti. Per descrivere meglio la fase di booting, suddividiamo la fase in 7 punti distinti.

  1. Accensione del dispositivo e routine hardware che servono a stabilizzare la tensione all’interno della board:
    La CPU, così come molti altri dispositivi all’interno di una board sono comandati attraverso segnali elettrici; questa corrente è generata da un potenziale. Una volta che il circuito è acceso, questo potenziale deve essere costante nel tempo (o per lo meno le sue fluttuazioni devono rimanere minime) per evitare di danneggiare il circuito. L’unica preoccupazione di questa fase è portare corrente elettrica ad ogni componente. I componenti (come la CPU) sono nello stato di reset, pronti per poter essere utilizzati. Se ci sono componenti rotti o danneggiati (ovvero se il potenziale non è quello stanard), l’accensione non prosegue.

  2. Esecuzione di POST (Power-on self-test):
    Questa fase prevede l’esecuzione di una serie di test che verificano l’integrità hardware prima di procedere con la fase successiva. Nel caso in cui questo test non vada a buon fine, la macchina non si accende e non dà segni di vita.

  3. Hardware device inizialization:
    Una volta effettuati i test, vengono inizializzati i controllori dei dispositivi (come SRAM), le porte seriali e la interrupt service table. Quando una CPU si avvia, esegue alcuni controlli di coerenza interni e trasferisce il controllo a un dispositivo PROM o EPROM che contiene del codice non volatile (destinata a sopravvivere a una perdita di alimentazione). Tutto è pronto per caricare il primo stadio di booting.

  4. Caricamento del bootloader minimale (prima parte):
    Lettura di una serie di routine dalla ROM che inizializza una RAM interna al chip. Questa fase consente di utilizzare il boot loader minimale che leggerà la nostra periferica e caricherà la seconda parte del bootloader. Una volta che la procedura è stata effettuata, la CPU salta ad un indirizzo di memoria predefinito per continuare l’esecuzione. Viene preparata la DRAM (ovvero la memoria RAM principale come noi la intendiamo), riempendo di zeri tutta l’area dei dati non inizializzati. Alloca lo spazio per lo stack e inizializza i registri (come lo stack pointer).

  5. Lettura del bootloader UBoot (seconda parte):
    La seconda parte del bootloader viene caricata e letta dalla memoria di massa che stiamo utilizzando (nel nostro caso, memoria flash). Due puntatori in questo caso sono fondamentali: l’indirizzo base dell’immagine eseguibile che caricherà il sistema operativo (kernel) e l’indirizzo base della RAM. L’esecuzione salta quindi all’indirizzo base della RAM che conterrà le prime istruzioni da eseguire per la terza fase.

  6. Lettura del contenuto:
    Il bootloader decomprime il contenuto del sistema operativo contenuto nella microsd. Vengono controllati i checksum (se presenti) dell’immagine e verificato se l’ambiente di esecuzione coincide con il tipo di kernel da caricare. L’immagine uImage viene caricata nello spazio degli indirizzi in RAM e viene inizializzato il dtb (Device Tree Block) utilizzando le informazioni contenute nel file dtb. Il bootloader ha finito il suo lavoro e l’esecuzione può saltare alla prima istruzione del kernel Linux.

  7. Il kernel (initrd) viene eseguito:
    Si procede a montare il file system fisico (ovvero a caricarlo in memoria), eseguire le prime routine per il sistema operativo (servizi di basso livello che si interfacciano con i controllori hardware). Viene eseguito init che consente di caricare tutti i moduli richiesti dal sistema operativo e dall’utente ad alto livello. Servizi di sistema, servizi per l’utente, di interfaccia grafica. Vengono montati anche i due file system virtuali (/dev per i dispositivi e /proc).

Il sistema operativo quindi carica tutti i binari necessari per il corretto funzionamento della telecamera (server web, daemon per la gestione del video, controllori vari) e aspetta nuovi comandi.

Variabili di Ambiente di Uboot

Di seguito una breve spiegazione dei parametri che possono essere impostati su Uboot:

NomeDescrizione
autostartse impostato, l’immagine caricata in memoria sarà eseguita automaticamente
bootcmdcontiene il comando che UBoot utilizzerà appena dopo aver caricato tutti i componenti minimi per l’esecuzione del sistema operativo
bootargscontiene gli argomenti passati al kernel Linux
bootdelaytempo in secondi da aspettare prima che uboot continui l’esecuzione
serveripè possibile specificare un indirizzo ip del server che interessa i comandi di rete
ipaddrl’indirizzo ip locale del target
ethaddrindirizzo MAC
netmaskla netmask per comunicare con il server

Per trovare i parametri di Reolink utilizzati per Uboot, senza conoscere alcuno strumento di reverse engineering, possiamo utilizzare una delle più antiche utility chiamate strings. Strings è un comando che legge i dati da un file, ricercando delle sequenze di byte rappresentanti valide stringhe di caratteri visibili. Presenta una sintassi molto semplice:

strings nomedelfile

Possiamo combinare strings con uno strumento più potente chiamato GREP (General Regular Expression Print) che cerca le linee che corrispondono a uno o più modelli specificati con RegEx o semplici stringhe. Accantonando l’idea di utilizzare le RegEx, procediamo a trovare uno dei parametri di Uboot chiamato bootargs. La maggior parte dei firmware esistenti che utilizza Uboot ha sempre impostato questo parametro perché consente di specificare le opzioni di booting.

Uniamo quindi il potere di GREP con strings, utilizzando | per combinare i due comandi:

strings firmware_rlc_810_a.pak.bak | grep "bootargs=" -A 20

Attenzione alla query che stiamo utilizzando! Cerchiamo bootargs= per evitare altre corrispondenze con bootargs. È possibile cercare un’altra variabile di ambiente, andando a sostituire bootargs con il nome della variabile a piacere. Specifichiamo inoltre che dopo aver trovato la corrispondenza, vogliamo stampare a video le successive 20 linee, tramite l’opzione -A N o --after-context N dove N rappresenta il numero di linee che vogliamo stampare. 20 è un numero approssimativo che garantisce di prendere tutte le variabili di ambiente specificate su Uboot.

Il risultato è:

bootargs=earlyprintk console=ttyS0,115200 rootwait nprofile_irq_duration=on root=ubi0:rootfs rootfstype=ubifs ubi.fm_autoconvert=1 init=/linuxrc 
bootcmd=nvt_boot
bootdelay=0
baudrate=115200
ipaddr=192.168.1.99
serverip=192.168.1.11
gatewayip=192.168.1.254
netmask=255.255.255.0
hostname=soclnx
arch=arm
cpu=armv7
board=nvt-na51055
board_name=nvt-na51055
vendor=novatek
soc=nvt-na51055_a32

Si trovano molti dettagli interessanti. Andiamo a focalizzarci su ognuna delle variabili d’ambiente:

  • bootargs – bootargs rappresenta una serie di argomenti che vengono passati all’inizio a Uboot per iniziare ad avviare il sistema operativo. Sono state specificate le seguenti flag:
    • earlyprintk – mostra i primi messaggi d’avvio del kernel (per primi messaggi si intende i messaggi che vengono stampati prima che uboot carichi la console d’avvio - passo 4)
    • console – specifica i dispositivi utilizzati come schermo/console. In questo caso troviamo “ttyS0” che è la prima porta seriale UART (COM1) e il secondo parametro indica la velocità. Le porte seriali utilizzano una serie di segnali seriali per dialogare e vengono spesse utilizzate come console di controllo/di configurazione, diagnostica o manutenzione di emergenza.
    • rootwait – uboot aspetta finché il dispositivo da cui recuperare il sistema operativo non è pronto (sia esso un hard disk, ssd o dalla rete).
    • nprofile_irq_duration – non ho trovato alcuna documentazione relativa a questa impostazione, ipotizzo sia relativo alla gestione degli interrupt
    • root – indica il dispositivo da cui prelevare il rootfs, ovvero il file system che conterrà il sistema operativo
    • rootfstype – il tipo di file system che uboot utilizzerà (ovvero ubifs), parleremo meglio di ubifs nel corso della terza parte;
    • ubi.fm_autoconvert – è una flag che specifica l’utilizzo di una caratteristica sperimentale di ubifs chiamata Fastmap che assicura tempi di avvii minimi;
    • init – specifica il binario da avviare per primo dopo aver caricato il sistema operativo in memoria.
  • bootcmd – specifica il comando da eseguire, in questo caso nvt_boot
  • bootdelay – numero in secondi che indica quanti secondi uboot deve aspettare prima di caricare il kernel, utile in alcuni contesti embedded, ma la maggior parte delle volte è posto a 0.
  • baudrate – indica velocità per le porte seriali
  • ipaddr – indirizzo IP di default della board, serve per poter instaurare una connessione IP con una macchina remota e caricare un sistema operativo tramite la rete
  • serverip – indirizzo IP di una macchina remota da cui poter caricare un sistema operativo
  • gatewayip – indirizzo IP che fruisce da gateway
  • netmask – netmask utilizzata per il server
  • hostname – nome della macchina, in questo caso soclnx
  • arch – architettura, in questo caso arm
  • cpu – tipo di cpu, in questo caso armv7
  • board e board_name – nome della board nvt-na51055 (NVT sta per Novatek)
  • vendor – azienda che ha prodotto la board novatek
  • soc – modello della board (nvt-na51055_a32)

Possiamo inoltre ricavare la versione di Uboot, cercando la stringa “U-Boot”:

strings firmware_rlc_810_a.pak | grep "U-Boot"

U-Boot 2019.04 (Oct 11 2021 - 12:40:43 +0800)

La versione del software è 2019.04 ed è stata compilata nell’Ottobre 2021. Non è troppo vecchia, anche se è sempre buona cosa tenere aggiornato Uboot. Ipotizzo che Reolink e il team dietro il firmware non abbiano aggiornato Uboot per via di qualche componente deprecato (o per il solito pensiero “se funziona, non toccarlo!”).

Appuntamento al prossimo articolo in cui esploreremo l’hardware di Reolink!