
Reverse engineering the TI AM3358 boot ROM
Sono passati probabilmente diciotto mesi da quando ho messo le mani su alcune schede Beaglebone Black, salvate dalla spazzatura. Purtroppo, le schede non funzionavano subito. Ora, era la prima volta che lavoravo con una di queste schede, o con qualsiasi computer a scheda singola, quindi non ero sicuro se il problema fossi io o qualcosa di sbagliato nelle schede stesse (forse il motivo per cui erano state destinate alla spazzatura in primo luogo). Mi ci è voluto parecchio tempo e molto impegno per far avviare davvero queste schede, ma accidenti, ce l'ho fatta. Ed ecco cosa ho imparato lungo il percorso.
Ho incluso alcune utilità in questo repository, insieme a un file xml esportato da Ghidra che contiene tutti i simboli che ho ottenuto finora dal reversing. Ho usato questo post per esportare senza il firmware vero e proprio, per evitare problemi di copyright, per sicurezza. Se vuoi eseguire il debug della boot ROM da solo, avrai già il JTAG collegato, quindi puoi fare il dump della boot ROM (da 0x20000 a 0x2BFFF) da solo.
Per caricare i simboli:
0x20000 e di poter impostare il nome del blocco a bootrom.main(), o al boot handler della scheda MMC/SD.Per cominciare, sapevo che queste erano versioni personalizzate del beaglebone black standard, quindi all'inizio ho stabilito che poteva mancare qualcosa sulla scheda stessa, come un identificatore di scheda. Quello che ho visto quando avviavo una scheda SD standard formattata con balenaEtcher era semplicemente nulla. Mi aspettavo che i LED sulla scheda iniziassero a lampeggiare e mi aspettavo che collegando un cavo UART-USB avrei potuto vedere il processo di U-Boot. Tuttavia, la UART era silenziosa. Se rimuovevo la scheda SD, emetteva la lettera C in continuazione, che è il comportamento previsto per un boot UART/seriale. Stava decisamente provando ad avviarsi, e la scheda SD stava alterando questo comportamento, ma non avevo più visibilità. La maggior parte delle guide di risoluzione problemi sul web usava l'output di U-Boot come punto di partenza per diagnosticare i problemi. Immagino che non avrei avuto quel lusso.
A questo punto ho pensato che valesse la pena collegare una sonda di debug. Sfortunatamente, non avevo un connettore compatibile con il footprint esistente, così ne ho creato uno mio.
La scheda beaglebone ha un connettore con designazione P2 che riporta le connessioni JTAG. Ho collegato dei fili a questo connettore fino a un connettore femmina, così da poter comunicare tramite il mio J-Link.



Avviando Ozone (il debugger Segger) ho configurato il J-Link e ho iniziato semplicemente cercando di trovare il punto di ingresso. Avevo pensato che un reset-halt mi avrebbe portato dove serviva, ed è così che sono arrivato alla (errata) ipotesi che il punto di ingresso fosse 0x2148a, anche se sicuramente avevo notato che non era coerente. Più tardi, ho capito che le schede AM335x non vanno molto d'accordo con il reset-halt del J-Link, quindi in realtà c'era un ritardo di forse qualche centinaio di cicli di clock, che mi faceva finire da qualche parte all'interno di un boot handler, in modo indeterministico. (Alla fine ho risolto scrivendo un file GEL per Code Composer Studio di TI che supporta il debug via J-Link - al reset, il registro PC viene impostato sul reset handler, i registri vengono azzerati e la modalità istruzione viene forzata a ARM.)
Da un thread sui forum TI (AM335x: TI employees, where can I get the ROM Bootloader source code/symbols?) ho raccolto un paio di simboli di debug: SPI Initialize a 0x231e0, SPI ReadSectors a 0x23230, e 0x24bfa è una routine che esegue una lettura UART. È un aiuto carino, immagino. Ho notato che il boot falliva finendo in un loop infinito a 0x402f0440, un dead loop. Hmm, piuttosto lontano dal resto della boot ROM, probabilmente sarà in RAM o qualcosa del genere. È probabilmente il momento di passare al technical reference manual (TRM)!
Il Capitolo 26 del TRM contiene un sacco di informazioni sull'avvio. Otteniamo la seguente vista della boot ROM:

Descrizione:
L'architettura del Public ROM Code è mostrata nella Figura 26-1. È diviso in tre livelli principali con un approccio top-down: alto livello, driver e hardware abstraction layer (HAL). Un livello comunica con un livello inferiore tramite un'interfaccia unificata. Il livello ad alto livello è responsabile dei compiti principali del Public ROM Code: configurazione di watchdog e clock e routine principale di avvio. Il livello dei driver implementa i protocolli logici e di comunicazione per qualsiasi dispositivo di avvio in conformità con la specifica dell'interfaccia. Infine, l'HAL implementa il codice di livello più basso per interagire con gli IP dell'infrastruttura hardware. I dispositivi di avvio finali sono collegati ai pad IO del dispositivo.

La Figura 26-2 illustra il flusso ad alto livello della procedura di avvio del Public ROM Code. Su questo dispositivo, il Public ROM Code parte al completamento dell'avvio sicuro (eseguito dal Secure ROM Code). Il ROM Code esegue quindi la configurazione e l'inizializzazione della piattaforma come parte della procedura di avvio pubblico. L'elenco dei dispositivi di avvio viene creato in base ai pin SYSBOOT. Un dispositivo di avvio può essere un dispositivo di avvio di memoria (memoria flash saldata o dispositivo di avvio temporaneo come una memory card) o un'interfaccia periferica collegata a un host. Il loop principale della procedura di avvio scorre l'elenco dei dispositivi di avvio e cerca di trovare un'immagine dal dispositivo di avvio attualmente selezionato. Questo loop viene terminato se viene trovata un'immagine di avvio valida e viene eseguita con successo, oppure alla scadenza del watchdog. La procedura di autenticazione dell'immagine viene eseguita prima dell'esecuzione dell'immagine su un dispositivo HS. Un fallimento nella procedura di autenticazione porta al branching verso un "dead loop" nel Secure ROM (in attesa di un reset del watchdog).
Mappa di memoria! Vettori di eccezione! Diagrammi di flusso! Un sacco di informazioni in questa sezione. Il mio lavoro è diventato molto più facile.
A questo punto ho usato la sonda JTAG per scaricare il firmware in un paio di file diversi e ho iniziato a caricare le cose in Ghidra. Non sembrava esserci alcun file SVD o altre mappature dei registri disponibili in un formato comodo, il che è davvero sfortunato, perché significa che devo definire le regioni di memoria, i registri e tutto il resto manualmente. È stato un processo noioso, ma dopo un po' avevo uno script Python che potevo usare per caricare i simboli in Ghidra per il AM3358. Una cosa in meno di cui preoccuparsi!
Sembra che i file che ho possano essere mappati come:
È interessante notare che il loop infinito a 0x402f_0440 si trova all'inizio della "immagine scaricata" nella SRAM interna, mentre i vettori di eccezione sono memorizzati altrove. Forse questo sarà un indizio importante più avanti...
Al reset, la boot ROM privata gestisce le questioni di sicurezza e salta a 0x2 0000, che contiene i vettori di reset. La prima istruzione è un branch a 0x2 08d0, che deve essere il punto di ingresso. Non è un'istruzione BX, quindi, presumibilmente, siamo ancora in modalità Arm a quel punto.
Questo è il primo codice eseguito, il che significa che non è esattamente una "funzione" con parametri, quanto piuttosto uno script di avvio generato dal compilatore. Il primo blocco di base:```arm ldr r4,[->Peripherals::CM_PER] mov r0,#0x2c ldr r6,[r4,r0]=>CM_PER.CM_PER_OCMCRAM_CLKCTRL mov r6,#0x2 str r6,[r4,r0]=>CM_PER.CM_PER_OCMCRAM_CLKCTRL mov r0,#0x2c poll: ldr r6,[r4,r0]=>CM_PER.CM_PER_OCMCRAM_CLKCTRL cmp r6,#0x2 bne poll
Questo blocco imposta il clock della RAM OCMC su abilitato:
1. Imposta `CM_PER_OCMCRAM_CLKCTRL=0x2`
2. Controlla se il registro è stato impostato; in caso contrario, continua a fare polling
Il registro `CM_PER_OCMCRAM_CLKCTRL` usa i bit 0 e 1 per il campo `MODULEMODE`; impostando questo campo a `=0x2` si abilita il clock per la RAM OCMC.
Il prossimo blocco di base:```arm
ldr r0,[PTR_control_status]
ldr r0,[r0,#0x0]=>control_status
and r0,r0,#0x700
mov r0,r0, lsr #0x8
cmp r0,#0x3
bne skip
ldr r0,[PTR_control_status]
ldr r0,[r0,#0x0]=>control_status
cpy r6,r0
and r0,r0,#0x1f
cmp r0,#0x1f
bleq GPMIC_init
skip: ...
Questo blocco esegue le seguenti operazioni:
(control_status & 0x700) >> 8 == 0x3, salta se non è cosìcontrol_status & 0x1f == 0x1f; in tal caso, chiama la funzione GPMC_init dopo aver caricato control_status in r6Il blocco successivo configura il coprocessore:```arm msr cpsr_c,#0xd3 ldr r4,[->Exceptions::ROM_RESET_VECTOR] mcr p15,0x0,r4,cr12,cr0,0x0 bl LAB_00020934 bl LAB_00020938 bl LAB_0002093c bl LAB_00020940 bl LAB_00020944 bl LAB_00020948 bl LAB_0002094c bl LAB_00020950 mrc p15,0x0,r0,cr1,cr0,0x0 orr r0,r0,#0x800 mcr p15,0x0,r0,cr1,cr0,0x0 b LAB_000207f0
Operazioni in questo blocco:
1. Sposta `11010011b` nel campo di controllo del CPSR (`I=1`,`F=1`,`T=0`,`MODE=10011`)
1. `I` è la disabilitazione degli interrupt, `F` è la disabilitazione dei fast interrupt (quindi `I=F=1` significa che gli interrupt sono disabilitati)
2. `T` è la modalità Thumb, impostata a `0`
3. `MODE=10011` imposta la modalità del processore alla modalità Supervisor ([ref](https://developer.arm.com/documentation/ddi0406/b/System-Level-Architecture/The-System-Level-Programmers--Model/ARM-processor-modes-and-core-registers/ARM-processor-modes?lang=en#CIHGHDGI))
4. Vedi [qui](https://developer.arm.com/documentation/ddi0406/b/System-Level-Architecture/The-System-Level-Programmers--Model/ARM-processor-modes-and-core-registers/Program-Status-Registers--PSRs-) per maggiori informazioni
2. Carica l'indirizzo del vettore di reset della ROM
3. Accedi al registro `c12` delle Security Extensions del [coprocessore 15](https://developer.arm.com/documentation/den0013/d/ARM-Processor-Modes-and-Registers/Registers/Coprocessor-15) (coprocessore di controllo di sistema) e carica il vettore di reset della ROM nel `VBAR` (registro dell'indirizzo base del vettore)

4. Sembrano salti `nop`? Perché `bl` invece di `b`?
5. Abilita la predizione dei salti (imposta il bit 11 del registro di controllo di sistema `SCTLR`)

*Registri `c1` del CP15 (registri di controllo di sistema) nell'implementazione VMSA*
Descrizione del registro `SCTLR`:
> L'SCTLR fornisce il controllo di primo livello del sistema, incluso il suo sistema di memoria.
> Questo registro fa parte del gruppo funzionale dei registri di controllo della memoria virtuale.
Vedi pagina B4-1687 del TRM. Il bit 11 è il bit di *abilitazione della predizione dei salti*; impostarlo abilitato significa che la [predizione dei salti](https://developer.arm.com/documentation/ddi0406/b/System-Level-Architecture/Common-Memory-System-Architecture-Features/Caches/Branch-predictors) è attiva.
6. Chiama una funzione (tramite un salto a un'istruzione di chiamata)
### Funzione all'indirizzo `0x20894` (`__main`)
A questa funzione si arriva tramite un salto da un'altra routine iniziale. Penso che inizializzi lo stack e, possibilmente, i timer o il watchdog, prima di chiamare `FUN_0002889c` (che in seguito si rivelerà essere `main()`!).```arm
ldr sp,[->RESERVED_EXCEPTION_BRANCH] ; 0x4030ce00
blx load_stack_1
ldr r12,[DWORD_1]
add r12,r12,pc
tst r12,#0x1
adrne lr,0x208bd
cpyeq lr,pc
bx r12 ;=>init_timers_maybe
adr r12,0x208bd
bx r12 ;=>LAB_000208bc
000208bc bl FUN_0002889c
000208c0 ddw 0x109
000208c4 addr RESERVED_EXCEPTION_BRANCH
...
RESERVED_EXCEPTION_BRANCH:
ldr pc=>LAB_00020090,[PTR_LAB_4030ce20]
; 20090 is a dead loop
Le operazioni qui sono:
r0,r1,r2,r3,r4,lr nello stack (stack pubblico nella mappa di memoria)
load_stack_1 salta a una funzione vuota bx lr, poi estrae r0,r1,r2,r3,r4,pc dallo stack (in pratica rimette quei dati nei registri e mette ciò che lr contiene in pc per il ritorno)pc + 0x109 è dispari; se è pari carica 0x208bd in lr, altrimenti copia pc in lrFUN_0002889cQuesta dovrebbe essere la funzione __main() a cui si fa riferimento nel diagramma di flusso di avvio:

Questo renderebbe la funzione successiva la funzione main.
Come mostrato nella parte superiore della Figura 26-8, la CPU salta al vettore di reset del Public ROM Code una volta completata l'inizializzazione dell'avvio sicuro. Una volta in modalità pubblica, all'avvio del sistema, la CPU esegue l'inizializzazione lato pubblico e la configurazione dello stack (inizializzazione C generata automaticamente dal compilatore o "scatter loading"). Quindi configura il watchdog timer 1 (impostato su tre minuti), esegue la configurazione degli orologi di sistema. Infine salta alla routine di avvio.
0x209b0)Quando main viene chiamata, il registro SP punta a 0x4030ce00. Questo è il punto in cui inizia lo stack, che cresce verso il basso fino a 0x4030 b800; e poiché puntiamo all'indirizzo 0x4030 cdf0 dopo aver inserito 4 registri (una differenza di 16 byte o 4 parole), stiamo usando uno stack pieno discendente, come in AAPCS. Cioè, SP punta alla parola più recente dello stack e cresce verso il basso.
Ecco la funzione main() decompilata:```c
int main()
{
uint local_10;
uint local_c;
local_c = 0; local_10 = 0; check_stack_prm(&local_10); update_coldreset_tracing_vector(local_10); update_current_tracing_vector(1); main_clock_init(6,0); watchdog_softreset(); watchdog_write_disable_seq_data2(); set_watchdog(300000); if ((local_10 & 1) != 0) { update_current_tracing_vector(2); local_c = local_c & 0xffff | 1; } timer_func_1(); clock_init_func_4(&local_c); run_booting_loop(&local_c,local_10 & 0xff); return 0; }
La cosa più interessante per i miei scopi è la funzione `run_booting_loop` all'indirizzo `0x20a10`.
### Note su X-Loader
Dopo aver esaminato l'avvio e raggiunto questa parte con stringhe come "ISSW" e "CHSETTINGS" e "X-LOADER", ho iniziato a cercare altri posti in cui queste stringhe potrebbero comparire in contesti legati a U-Boot. Mi sono imbattuto in [questo thread](https://forum.xda-developers.com/t/discussion-on-the-boot-loader-cracked.1378886/) di persone che fanno reverse engineering o crackano il firmware del Nook, e la [sorgente di x-loader](https://github.com/joelagnel/x-loader/blob/f3c74bc9b01dac58e553393d6ec1041353f2f1f7/scripts/signGP.c) contiene riferimenti a cose come `CHSETTINGS`. Da quello che ho visto, "ISSW" [sembra](https://github.com/u-boot/u-boot/blob/master/doc/README.ti-secure) riferirsi all'avvio da dispositivi non di memoria.
Ricordiamo dalla documentazione di inizializzazione, il codice ad alto livello:

Di interesse:
- RNDIS
- FAR
- XMODEM
- BOOTP
- TFTP
- DFT
Forse è il momento di provare di nuovo un po' di debugging dal vivo. Cercare di fare reverse engineering su tutte quelle struct sarebbe probabilmente doloroso, considerando la grande quantità di dati che non riesco a capire...
Evviva! Il debugging dal vivo funziona impostando manualmente PC e SP usando i vettori che ho trovato:

Dalla sorgente e dai vettori di tracciamento, sono riuscito a mappare le varie opzioni di avvio e a quale numero di dispositivo sono assegnate. Questo in seguito si è rivelato molto utile, dato che dovevo distinguere tra MMC0 (8) e MMCSD1 (9) quando impostavo i breakpoint nell'handler di avvio SD/MMC.
| Tipo | Dispositivo | ID dispositivo |
| ---------- | ----------------- | ----------- |
| Memoria | XIP (MUX2) | 1 |
| Memoria | XIP w/WAIT (MUX2) | 2 |
| Memoria | XIP (MUX1) | 3 |
| Memoria | XIP w/WAIT (MUX1) | 4 |
| Memoria | NAND | 5 |
| Memoria | MMCSD1 | 7, 9 (eMMC) |
| Memoria | NAND_I2C | 10 |
| Memoria | MMC0 | 8, 12 (SD) |
| Periferica | UART0 | 16 |
| Periferica | USB | 20 |
| Periferica | GPGMAC0 | 22 |
Per informazioni su come il processore si avvia, consulta il TRM e [questa risposta su Stack Exchange](https://stackoverflow.com/a/31252989/8565545). In sintesi:
1. La boot ROM ha identificato il file MLO (Mmc LOader) sulla scheda SD e lo ha copiato nella SRAM
2. Questo è il loader secondario, un bootloader più piccolo che inizializza la RAM completa e copia lì l'intero binario di U-Boot per l'esecuzione
3. Dopo che il binario di U-Boot viene eseguito, noi (o meglio U-Boot) avviamo finalmente il kernel
### `run_booting_loop()`
Questo è il ciclo di avvio principale. Viene eseguito all'infinito, o finché l'esecuzione non dirama verso un altro bootloader che verrebbe caricato in RAM.
Inizio della procedura, senza aggiornamenti del vettore di tracciamento:
- Cerca il tipo di dispositivo
- Se il tipo di dispositivo è 5 (dispositivo sicuro) esegui qualche altra inizializzazione
- Esegui `build_boot_list(int,buffer[],data[],int)`
- `buffer[]` è inizializzato a `0xff` e `data[]` include il tipo di dispositivo (probabilmente)```c
void run_booting_loop(uint32_t *r0_config,undefined4 param_2,undefined4 param_3,
undefined4 default_list)
{
int iVar1;
uint j;
uint i;
int device_type;
byte alt_list [12];
undefined4 boot_status;
byte boot_list [8];
uint8_t local_buffer [8];
update_current_tracing_vector(3);
/* Device type is 3 */
lookup_device_type(&device_type);
if ((device_type == AM335X_HIGH_SECURITY) && (iVar1 = return_zero_4(), iVar1 != 0)) {
init_something_1_small(&STATIC_DATA_1);
}
/* param1 = 1
param2 = 4030 ebc4
param3 = 4030 ebb4 */
build_boot_list(*(ushort *)r0_config,boot_list,alt_list,default_list);
do {
i = 0;
local_buffer[0] = 0xff;
local_buffer[1] = 0xff;
local_buffer[2] = 0xff;
local_buffer[3] = 0xff;
do {
if (boot_list[i] - 1 < 12) {
update_current_tracing_vector(4);
/* No return unless there is an error */
boot_device_1(r0_config,boot_list[i],local_buffer);
}
else if (boot_list[i] - 65 < 8) {
update_current_tracing_vector(5);
watchdog_write_disable_seq_data2();
boot_status = 0xffffffff;
boot_device_2((uint32_t)r0_config,boot_list[i],&boot_status,local_buffer);
watchdog_write_enable_seq_data2();
if (boot_status != 0xffffffff) {
local_buffer[0] = (undefined)boot_status;
local_buffer[1] = boot_status._1_1_;
local_buffer[2] = boot_status._2_1_;
local_buffer[3] = boot_status._3_1_;
if ((boot_status & 0xffff00ff) == 0xf0030006) {
update_current_tracing_vector(9);
boot_list[i + 1] = (byte)(boot_status >> 8);
}
else if (boot_status != 0xf0030002) {
update_current_tracing_vector(8);
j = 0;
do {
if (63 < boot_list[j]) {
boot_list[j] = 0;
}
j = j + 1 & 0xff;
} while (j < 8);
}
}
}
i = i + 1 & 0xff;
} while (i < 8);
update_current_tracing_vector(6);
} while( true );
}
Ho trovato una funzione a 0x23d7a che ho chiamato boot_into_SRAM(); era l'ultima funzione chiamata prima di saltare nella SRAM e, da lì, entrando nel gestore delle eccezioni. In precedenza avevo salvato uno stato diverso della RAM, ma durante un altro debug dal vivo (ora più di un anno dopo, luglio 2024), ho capito cosa doveva star succedendo. La scheda stava leggendo correttamente i dati dalla scheda SD e stava eseguendo codice che aveva caricato dalla scheda! Per verificarlo, ho dovuto trovare byte nella SRAM identici a quelli presenti sulla scheda SD. Si è scoperto che all'interno di am335x-evm-linux-sdk-bin-.../board-support/prebuilt-images/ c'è un file binario chiamato u-boot-spl.bin-am335x-evm, e il codice in questo binario corrisponde a quello che appare nella SRAM. Siamo arrivati allo uboot SPL!
Siamo riusciti a eseguire il boot nella SRAM; ora mi chiedo del terminale UART, che dovrebbe mostrare informazioni su uboot. La connessione hardware è di seguito.

Connettendosi al dispositivo con CuteCom, 115200 @ 8-N-1, senza una scheda SD inserita, emette semplicemente C ripetutamente.

Ma quando la scheda SD è inserita, l'UART non emette nulla. Nessun messaggio, nessun carattere. Il guasto deve verificarsi troppo presto nel processo di boot? Ma ora che so anche quale codice sta eseguendo (e ne ho il sorgente), dovrei essere in grado di generare dei simboli di debug per esso e avviare una vera sessione di debug. Potrebbe non essere banale: devo assicurarmi di compilare il codice nello stesso modo; potrebbe volerci un po' di tempo per imparare esattamente cosa l'SDK ha caricato sulla mia scheda SD e come compilarlo.
Dato che U-Boot dovrebbe inviare testo all'UART e non vedo nulla, immagino che stiamo intercettando un'eccezione da qualche parte nell'SPL.
A questo punto, ho passato molte, molte ore a fare reverse engineering e a ripulire il sorgente decompilato della boot ROM in Ghidra, esaminando le struct e come ogni membro dati veniva usato nelle varie funzioni, a volte annidato, creandomi ogni sorta di caos. Mentre quello era in secondo piano, ho pensato che fosse anche il momento di iniziare a fare debug e a compilare il mio codice. Dopotutto siamo in RAM, perché non caricare i simboli dell'SPL e vedere cosa succede?
Puoi fare debug con Ozone, il debugger Segger per J-Link. Puoi anche usare il Code Composer Studio (CCS) di TI o forse anche la sua versione "light" in stile VSCode, CCS Theia. Sono riuscito a compilare tutto nell'SDK seguendo questo video: Sitara Linux Board Porting Series: Module 6. Ci sono tre componenti da compilare:
Ho seguito il video del Modulo 7 della serie sopra citata e sono riuscito a far funzionare le cose, con un paio di note:
s_init() non è più presenteAvere i simboli? Bellissimo. Ora posso vedere cosa succede durante l'esecuzione, partendo da un gestore di reset reset(), e posso vedere dove andiamo a finire con la nostra eccezione. Per rintracciarla, ho impostato un breakpoint al gestore delle eccezioni a 0x402f 0440 e ho controllato il link register, che conservava ancora l'indirizzo della funzione più recente. Si è rivelato essere l'indirizzo 0x402f 76ce, anche se non sembra coerente. Cosa sta causando l'errore?
Nota: per il debug, seguendo il video del Modulo 7, esegui fino a 0x402f 0400, poi fai la parte Load Memory(). Questa operazione deve essere eseguita a ogni riavvio.
Entriamo nella funzione device_probe() (0x402f 74c4), poi un paio di altre funzioni? Poi da do_setup_dpll() con un branch a 0x402f 07fc, non usciamo, quindi continuiamo da lì. Procedendo passo passo, torniamo a _main() in crt0.S, situato a 0x402f 14e0. Sembra che potremmo uscire da board_init_f() e procedere verso spl_relocate_stack_gd(). Questa chiamata non ritorna. Raggiungiamo dm_fixup_for_gd_move(). Contiene un'istruzione che fallisce, a 0x402f 76c2. Penso che sia questo: sta tentando di accedere a 0x81ff ff20. Apparentemente non va bene. Ho il sospetto che ci sia un problema con la configurazione della SDRAM. Ho rintracciato tutti i thread sui forum TI relativi a problemi simili e ne ho trovati una mezza dozzina che contenevano alcuni suggerimenti per aiutarmi. Ho concluso che probabilmente era legato o (a) alla messa a punto dell'EMIF, o (b) al software leveling.
La mia scheda è quella mostrata di seguito.

La memoria è di Micron, mentre lo schema elettrico della BeagleBone Black che possiedo (rev C3) usa memoria DDR3 Kingston, in particolare la D2516EC4BXGGB. La DDR3 è U12; possiamo usare la pagina del decodificatore di marcature Micron per trovare il componente:
Per assicurarci che il componente sia vivo, ho iniziato semplicemente verificando che l'alimentazione fosse fornita. Il datasheet specifica che deve essere 1.5V +/- 0.075V. Misuro 1.506V ai capi di R6 nella parte inferiore della scheda. Abbiamo due test point, TP1 e TP2.
Nel caso possa mai essere utile, ecco un paio di test point.
Il circuito della memoria è descritto in dettaglio nella pagina di progettazione hardware.
Controlliamo la linea di clock enable. Possiamo controllare entrambi i lati di R96: un lato dovrebbe essere collegato a terra, l'altro dovrebbe essere tenuto alto.
Confermato: 1.5V su CKE.
Il passo successivo è controllare il segnale di clock. Ho fatto quello che ho potuto qui, usando il tinySA con l'antenna collegata puntata più o meno nella direzione del chip RAM. Facendo questo tipo di "sniffing", sono abbastanza sicuro che il clock sia presente, almeno per ora.
Andando avanti, è il momento di esaminare l'interfacciamento della memoria esterna. Un concetto che ricorre spesso è quello di file GEL. È un linguaggio interpretato sviluppato da Texas Instruments per Code Composer Studio; l'acronimo sta per General Extension Language.
Un file GEL è incluso nel tool di configurazione della memoria DDR.
Bene! Ho seguito la procedura di messa a punto (come meglio potevo) e sono riuscito a trovare i valori ottimali per il file GEL.```
The Slave Ratio Search Program Values are...
PARAMETER MAX | MIN | OPTIMUM | RANGE
DATA_PHY_RD_DQS_SLAVE_RATIO 0x071 | 0x005 | 0x03b | 0x06c DATA_PHY_FIFO_WE_SLAVE_RATIO 0x1b3 | 0x046 | 0x0fc | 0x16d DATA_PHY_WR_DQS_SLAVE_RATIO 0x0f7 | 0x01a | 0x088 | 0x0dd DATA_PHY_WR_DATA_SLAVE_RATIO 0x137 | 0x05a | 0x0c8 | 0x0dd
Tentare di regolare le cose relative alla RAM nel Memory Browser... Woo! Funziona!
Quindi la memoria sembra decisamente funzionare, ma l'SPL continua a fallire, quindi forse c'è qualcosa riguardo al modo in cui l'SPL sta cercando di inizializzare la SDRAM? Ah, giusto, c'è dell'altro nella procedura di tuning, ovviamente! Devi effettivamente aggiornare l'SPL...
Stiamo facendo progressi. Il file `board.c` inizializza la DDR controllando che tipo di scheda abbiamo. Ma per questa scheda, tutte le funzioni (`board_is_evm_sk()`, `board_is_icev2()`, `board_is_bone_lt()`, ecc.) restituiscono false, quindi si ripiega su `config_ddr(266, ...)` dove 266 è la frequenza di clock in MHz, e *dovrebbe* essere 400 MHz. Questo sarebbe sicuramente un problema.
`board_is_bone_lt` dovrebbe essere bypassata per restituire sempre true. L'ho fatto, e sono andato un po' più avanti, ma qualcosa mi insospettisce. Caricare il nuovo file MLO sulla scheda SD non funziona, anche se caricare il programma direttamente funziona bene. Cosa sta succedendo? Posso dire che il codice caricato nella SRAM non è lo stesso che ho compilato. In effetti, ho persino formattato la scheda SD, e un SPL predefinito sembra comunque caricarsi nella SRAM! Ho verificato che il processo di avvio non tenta di continuare quando si usa un'altra scheda SD. Quindi, il bootloader sta sicuramente cercando la partizione di boot sulla scheda SD, poi sposta l'esecuzione nella SRAM, ma non ha ancora copiato i dati? Uffa. Da dove arriva?
Questo problema mi ha causato non poche difficoltà. Ho eliminato tutte le partizioni, azzerato l'MBR, azzerato la partizione di boot e provato diverse schede SD, e solo la mia scheda era ancora in grado di avviarsi, quindi doveva esserci *qualche* dato avviabile rimasto sopra, memorizzato da qualche parte. Alla fine, sono riuscito a porre fine alla follia azzerando l'*intera* scheda.
A questo punto ho anche imparato a conoscere i **vettori di tracing** a cui puoi accedere durante la risoluzione dei problemi della boot ROM. Questo si sarebbe rivelato estremamente utile anche per il reversing, poiché sapevo da dove provenivano tutte le chiamate di tracing, e potevo quindi assegnare nomi di funzione ecc. in base a quelle chiamate. Ho creato un foglio di calcolo per interpretare questi vettori di tracing e l'ho usato per capire rapidamente come cambiava la procedura di avvio mentre modificavo i parametri della scheda. E infatti, continuava a dichiarare di trovare CHSETTINGS in continuazione mentre provavo a formattare e riformattare la scheda SD, prima che, disperato, azzerassi l'intero supporto.
Per provare a leggere la scheda come farebbe il processore TI, puoi usare `dd`. Usa block size=512 e specifica il primo settore (prova a usare GParted per controllare quale) saltando i primi `n`. Esempio: il primo settore è 2048, il dispositivo è `sda`, leggeremo solo il primo settore:```
sudo dd if=/dev/sda1 of=/home/sam/sector2048 bs=512 skip=2048 count=1
Ho usato questo per scaricare le immagini dell'MBR e dell'inizio della partizione di boot direttamente dalla scheda SD, entrambe sarebbero tornate utili in seguito.
I miei sforzi di reverse engineering in Ghidra mi avevano portato alle funzioni di gestione del boot della scheda SD, e ora potevo vedere funzioni che inviavano comandi SD alla scheda, e potevo eseguire passo-passo e vedere cosa rispondeva la scheda. Pensavo di stare guardando nel posto giusto, e che la scheda stesse restituendo tutti zeri. In seguito ho scoperto che forse stavo eseguendo passo-passo il gestore eMMC (lo stesso gestore ma con un ID dispositivo diverso), o che qualcos'altro non andava, perché non c'era niente di sbagliato nella scheda SD. Tuttavia, ho capito che era il momento di imparare come funzionano queste schede.
Ero curioso di vedere perché la scheda continuava a restituire tutti zeri durante ogni richiesta di blocco. Chiaramente, le funzioni della scheda e il software possono leggerla perché lo hanno già fatto in passato. Ma comunque, era il momento di cablare il tutto e dare un'occhiata con l'analizzatore logico. Un po' di microsaldatura, tenendo fermi i fili da 30awg con resina epossidica UV, e agganciando il mio Saleae, e abbiamo qualcosa che funziona.

Ho usato questo analizzatore per analizzare i dati. Prima ho provato senza una scheda inserita.

Per i primi comandi la frequenza di clock è 120 kHz. Ovviamente, la scheda non risponde (non c'è).``` CMD0, arg=\0 GO_IDLE_STATE CMD8, arg=\x01\xAA SEND_EXT_CSD CMD55, arg=\0 APP_CMD CMD1, arg=\0 SEND_OP_COND ... CMD0, arg=\0 GO_IDLE_STATE CMD8, arg=\x01\xAA SEND_EXT_CSD CMD55, arg=\0 APP_CMD CMD1, arg=\0 SEND_OP_COND
Quando la scheda viene effettivamente inserita, la frequenza sale a circa 6 MHz dopo la configurazione.

Dopo essere riuscito a superare alcuni crash derivanti dall'uso della modalità SD (consiglio: anche per questa scheda SD dovreste usare la modalità MMC) e ho potuto verificare che la scheda forniva dati ragionevoli. Ho messo da parte la questione dopo questo perché ho raddoppiato i miei sforzi per capire l'handler di boot della scheda SD e mi sono reso conto che i dati corretti *venivano* letti, ed erano gli stessi dati che avevo ottenuto facendo manualmente `dd` della scheda! Beh, è stata una deviazione divertente e mi ha aiutato a sentirmi sicuro che la scheda funzionasse.
### Trovare il problema
I dati provengono dall'indirizzo `0x4030c928` (una variabile di stack, array di 512 byte) dopo aver eseguito il ramo a `0x25c2e` per l'indirizzo `0x0000` e il dispositivo `8` (vedi dati statici a `0x4030d00c`). Entrando nel metodo che ho chiamato `MBR_detection`, il programma controlla i byte magici `0xaa55`. Per prima cosa carica i secondi due byte `0xaa`, poi il primo `0xaa`.```
r0 = data[0x1ff]
r1 = data[0x1fe]
orr r0,r1,r0,lsl #8
sub r1,r0,#0xaa00
subs r1,#0x55
bne <return FAIL>
Questo riesce. Il controllo successivo, però, fallisce:``` r0 = data[0xc] => 0 r1 = data[0xb] => 0 orr r0,r1,r0, lsl #8 cmp r0,#0x200 bne
Pseudocodice del decompiler:```c
if (
data[0x1fe] != 0x55aa ||
data[0xb] != 0x200 ||
(data[0xd] != 1 && // bit 0
data[0xd] != 2 && // bit 1
data[0xd] != 4 && // bit 2
data[0xd] != 8 && // bit 3
data[0xd] != 0x10 && // bit 4
data[0xd] != 0x20 && // bit 5
data[0xd] != 0x40 && // bit 6
data[0xd] != 0x80) // bit 7
)
{
return 1;
}
Questo controlla se il byte all'indirizzo 0xb = 11 è uguale a 0x200, e verifica se il byte all'indirizzo 0xd è uguale a un valore a bit singolo. Entrambe le condizioni devono essere soddisfatte, altrimenti restituisce un fallimento.
Dopo che la funzione di rilevamento restituisce un 1, il gestore di avvio tenta quindi di leggerlo come un MBR e carica l'offset della prima partizione per provare a vedere se quella è una partizione avviabile. Ecco la procedura:```C
// Call block read function
// mmc_block_read_something(boot_device *dev,blk_read_struct blk)
ret = ((code *)blockread_struct->block_read_func)(blockread_struct->device_ptr,&block_read_info);
if (ret != 0) {
return 1;
}
// Check if device doesn't use MBR
ret = MBR_check_bootable_partition((partition_struct *)block_data,blockread_struct);
if (ret != 0) {
// It uses MBR, try each partition in the partition entries for a bootable
// partition
ret = MBR_check_entries(block_data,blockread_struct);
if (ret != 0) {
return 1;
}
ret = MBR_parse_entries(block_data,&blockread_struct->part_entry);
if (ret != 0) {
return 1;
}
// Get bootable partition offset
block_read_info = (blk_read_struct *)(blockread_struct->part_entry).first_sect_pos;
uStack_220 = 1;
pbStack_21c = block_data;
// Call block read function
// mmc_block_read_something(boot_device *dev,blk_read_struct blk)
ret = ((code *)blockread_struct->block_read_func)
(blockread_struct->device_ptr,&block_read_info);
if (ret != 0) {
return 1;
}
// Try and verify bootable partition again
ret = MBR_check_bootable_partition((partition_struct *)block_data,blockread_struct);
if (ret != 0) {
return 1;
}
}
Quindi ora salto al punto in cui legge i dati a `0x800` e ottengo il dump corretto. Ma il metodo di rilevamento dell'MBR restituisce ancora 1, anche con i dati giusti (e ci sono un paio di livelli di indirezione che ho dovuto seguire, grrr), quindi è lì che deve essere il problema.
Ultimo sforzo ora. La prima lettura di memoria dalla scheda SD è l'MBR, che ha fino a quattro voci di tabella delle partizioni, vedi Tabelle 26-20, 26-21 del TRM. La voce della tabella delle partizioni per la partizione di boot dice che la partizione contiene `0x40000` settori. Ma il file system della partizione (vedi Tabella 26-23 del TRM) dice che ce ne sono solo `0x3fff8`, per qualche motivo, e la boot ROM rileva questo e fallisce.
Come esperimento, ho saltato il salto che causava problemi (potrebbe finire male... incrociando le dita...) e il programma è certamente continuato, anche se non sono sicuro di dove sia finito, sembra spazzatura. Ma ignorando ciò, il dispositivo si avvia davvero! Impostando un breakpoint a `0x402f0400` (inizio dell'immagine caricata) e tutto procede correttamente. Forse è il momento di collegare la UART? La UART è ottima!
Per quanto riguarda il problema della scheda SD? Ho fatto una [domanda su Unix SE](https://unix.stackexchange.com/questions/781715/why-do-the-mbr-partition-entry-and-partition-filesystem-disagree-on-the-number-o/781755), ma non ho ricevuto molto aiuto sul problema in questione (anche se ho ottenuto comunque buone informazioni). Da lì: l'ho finalmente risolto! Rivedendo i comandi `mkfs` per costruire il file system FAT16, ho notato che [un altro riferimento](https://blog.billvanleeuwen.ca/porting-u-boot-onto-the-beaglebone) usa il flag `-a`, che disabilita l'allineamento. Questa era la chiave. Aggiungendo quel flag e ricompilando, i conteggi dei settori coincidono (`0x40000`) e il sistema si avvia. Immagino che la boot ROM non supporti quel tipo di allineamento.
Ora ricevo i messaggi qui sotto in un boot loop, con la scheda inserita. Evviva! Devo solo capire perché il kernel non parte, e poi siamo a cavallo! Tutto quel lavoro potrebbe finalmente ripagare con un paio di board beaglebone utilizzabili. Ne vale la pena? Chi può dirlo.```
U-Boot SPL 2021.01-00001-gc59bf25a382-dirty (Jul 24 2024 - 20:38:49 -0400)
Trying to boot from MMC1
U-Boot 2021.01-00001-gc59bf25a382-dirty (Jul 28 2024 - 20:36:46 -0400)
CPU : AM335X-GP rev 2.1
Model: TI AM335x BeagleBone Black
DRAM: 512 MiB
WDT: Started with servicing (60s timeout)
NAND: 0 MiB
MMC: OMAP SD/MMC: 0, OMAP SD/MMC: 1
Loading Environment from FAT... *** Warning - bad CRC, using default environment
<ethaddr> not set. Validating first E-fuse MAC
Net: eth2: ethernet@4a100000, eth3: usb_ether
Hit any key to stop autoboot: 2 <0x08><0x08><0x08> 1 <0x08><0x08><0x08> 0
WARNING: Could not determine device tree to use
switch to partitions #0, OK
mmc0 is current device
SD/MMC found on device 0
Failed to load 'boot.scr'
Failed to load 'uEnv.txt'
switch to partitions #0, OK
mmc0 is current device
Scanning mmc 0:1...
libfdt fdt_check_header(): FDT_ERR_BADMAGIC
<0x1b>7<0x1b>[r<0x1b>[999;999H<0x1b>[6n<0x1b>8Scanning disk [email protected]...
Scanning disk [email protected]...
** Unrecognized filesystem type **
Found 4 disks
No EFI system partition
BootOrder not defined
EFI boot manager: Cannot load any image
switch to partitions #0, OK
mmc0 is current device
SD/MMC found on device 0
4997632 bytes read in 353 ms (13.5 MiB/s)
Failed to load '/boot/undefined'
Starting kernel ...
Quindi c'è un problema con il device tree ("WARNING: Could not determine device tree to use"). Questa è la mia prima esperienza con il kernel e l'avvio del kernel, quindi non ho idea di cosa significhi.
"Avviare il kernel" coinvolge i seguenti passaggi, per come lo capisco:
Un aspetto importante del processo di avvio del kernel è il device tree. Questo è memorizzato nel file .dtb (device tree binary; confronta con i file sorgente del device tree .dts) per la scheda.
Il mio problema ora è che U-Boot non sta caricando il device tree della scheda, poiché non c'è alcun messaggio reading /am335x-boneblack.dtb nel log. Invece, otteniamo WARNING: Could not determine device tree to use. Quindi questa è una buona prova! Immagino sia dovuto all'EEPROM mancante con l'ID della scheda.
Alcuni dettagli su come ottiene la scheda si trovano in questo thread sui forum TI.
Allora, come fa U-Boot a sapere come configurarsi e avviarsi correttamente? All'interno del sorgente U-Boot che abbiamo compilato, c'è una cartella chiamata configs/ che memorizza i file defconfig per varie schede. Questi file definiscono i vari parametri di configurazione per U-Boot, incluso il comando di avvio, che potrebbe assomigliare a questo:```
if test ${boot_fit} -eq 1;
then run update_to_fit;
fi;
run findfdt;
run init_console;
run envboot;
run finduuid;
run distro_bootcmd
Definiamo quale config usare quando eseguiamo il target `make <boardname>_config`. La funzione `findfdt` viene utilizzata per identificare la scheda su cui stiamo lavorando e configura correttamente il device tree. Appare così (definita in `am335x_evm.h`):```
"findfdt="\
"if test $board_name = A335BONE; then " \
"setenv fdtfile am335x-bone.dtb; fi; " \
"if test $board_name = A335BNLT; then " \
"setenv fdtfile am335x-boneblack.dtb; fi; " \
"if test $board_name = A335PBGL; then " \
"setenv fdtfile am335x-pocketbeagle.dtb; fi; " \
"if test $board_name = BBBW; then " \
"setenv fdtfile am335x-boneblack-wireless.dtb; fi; " \
"if test $board_name = BBG1; then " \
"setenv fdtfile am335x-bonegreen.dtb; fi; " \
"if test $board_name = BBGW; then " \
"setenv fdtfile am335x-bonegreen-wireless.dtb; fi; " \
"if test $board_name = BBBL; then " \
"setenv fdtfile am335x-boneblue.dtb; fi; " \
"if test $board_name = BBEN; then " \
"setenv fdtfile am335x-sancloud-bbe.dtb; fi; " \
"if test $board_name = A33515BB; then " \
"setenv fdtfile am335x-evm.dtb; fi; " \
"if test $board_name = A335X_SK; then " \
"setenv fdtfile am335x-evmsk.dtb; fi; " \
"if test $board_name = A335_ICE && test $ice_mii = rmii; then " \
"setenv fdtfile am335x-icev2.dtb; fi; " \
"if test $board_name = A335_ICE && test $ice_mii = mii; then " \
"setenv fdtfile am335x-icev2-prueth.dtb; fi; " \
"if test $fdtfile = undefined; then " \
"echo WARNING: Could not determine device tree to use; fi; \0" \
Se vogliamo un comportamento predefinito, possiamo semplicemente cambiare la variabile board_name, giusto? Beh, forse no, o almeno non so dove sia il posto migliore per cambiarla. Ma mentre impostare board_name non ha funzionato di per sé, in realtà ho anche aggiornato il file .dtb predefinito e quello ha funzionato!```
_____ _____ _ _
| _ |___ ___ ___ ___ | _ |___ ___ ||__ | |
| | _| .'| . | . | | | | . | | | -| _| _|
|||| |__,| || || || ||| ||||
|| |___|
Arago Project http://arago-project.org am335x-evm ttyS0
Arago 2021.09 am335x-evm ttyS0
am335x-evm login: root
root@am335x-evm:~#
Finalmente, siamo a un terminale. Le mie schede di recupero sono vive!
### Correzione dell'ID EEPROM mancante
Come ultimo passaggio, scriverò l'ID corretto della scheda nell'EEPROM, cosa facile da fare dallo spazio utente di Linux. Dal sorgente SPL, i vari ID di scheda sono:
- `A335BONE` - scheda Beaglebone
- `A335BNLT` - scheda Beaglebone Black
- `A335PBGL`
- `A335X_SK`
- `A33515BB`
- `A335_ICE`
E c'è anche una revisione opzionale della scheda. In base allo schema elettrico, l'EEPROM si trova su I2C0, e il chip stesso (24LC32A sul mio schema, anche se è etichettato '256Kx8) fornisce l'indirizzo del dispositivo I2C di `0x50` (binario `b1010` seguito da `000` come indirizzo del chip, poiché il package a 5 pin non ha pin di indirizzo aggiuntivi). Un'ultima nota: il pin WP è collegato a HIGH tramite una resistenza di pull-up da 10k, quindi la protezione da scrittura è abilitata per impostazione predefinita; deve essere portato a LOW prima che possa avvenire qualsiasi scrittura, altrimenti risponderà con un ACK ma non scriverà nulla.
L'EEPROM è accessibile tramite il kernel all'indirizzo `/sys/bus/i2c/devices/0-0050`, all'interno del quale c'è un file chiamato `eeprom`. Pertanto, con il pin WP portato a LOW (collega TP4 in alto vicino al connettore DC a terra), bastano poche chiamate `echo`. Il manuale di riferimento di sistema del Beaglebone Black riporta il formato. Ho adattato questa procedura [da qui](https://groups.google.com/g/beagleboard/c/di5O5JCl4yw).```sh
root@am335x-evm:~# cat fix_eeprom.sh
#!/bin/bash
# Fix board ID EEPROM
EEPROM_FILE=/tmp/eeprom.tmp
EEPROM=/sys/bus/i2c/devices/0-0050/eeprom
# header bytes
echo -ne "\xaa\x55\x33\xee" > ${EEPROM_FILE}
# Board ID
echo -n "A335BNLT" >> ${EEPROM_FILE}
# serial number (I left this basically as the template)
echo -n "000C24wwBBoxxxx" >> ${EEPROM_FILE}
dd if=${EEPROM_FILE} of=${EEPROM}
Usando less per confermare, e d'ora in poi non dovremmo avere problemi a usare una scheda SD standard. Le modifiche al SPL e alla configurazione di U-Boot possono essere annullate, una volta che tutte le schede hanno le EEPROM scritte.
| Regione | Indirizzo iniziale | Lunghezza |
|---|
| Boot ROM (Public) | 0x4002_0000 | 0xBFFF |
| Boot ROM (Public, alias) | 0x0002_0000 | 0xBFFF |
| SRAM Interna | 0x402F_0400 | 0xFC00 |
| L3 OCM0 | 0x4030_0000 | 0x10000 |
| Test Point | Connessione | Foglio dello schema | Lato della scheda |
|---|
| TP1 | DGND | 2 (D1) | Superiore |
| TP2 | VDD_MPUON (VDD_MPU_MON) | 5 (C4) | Superiore |
| TP3 | TESTOUT | 5 (B2) | Superiore |
| TP4 | Board ID WP | 11 (B1) | Superiore |