Analisi del driver Ominivision OS12D40

Durante la sesta parte della serie “Deeping into a firmware of an IP camera”, abbiamo strutturato un discorso teorico riguardante i dispositivi, come comunicano e quali sono gli elementi hardware che permettono tutto ciò.

Un’altra cartella molto interessante è /lib che comprende tutte le librerie per gli eseguibili e i device driver sviluppati da Novatek che permettono al sistema operativo di configurare e impostare correttamente tutti i dispositivi hardware connessi alla board. Per cominciare, introduciamo l’argomendo spiegando cosa sono i device driver e come essi funzionano su sistemi Linux-based.

Sviluppo del device driver in Linux

Lo sviluppo di un device driver avviene attraverso la creazione di un modulo kernel. I moduli del kernel sono pezzi di codice che possono essere caricati a tempo di esecuzione in modo dinamico. Per caricare un modulo del kernel, è possibile utilizzare lo strumento insmod nome_modulo_da_caricare che rappresenta il percorso del file .ko in cui il modulo è stato compilato e collegato.

Come indica il nome, il principale vantaggio di adottare i moduli è dato dalla modularità. Ogni modulo è un’unità a sè stante, ognuno dei quali assolve un compito specifico ed è capace di interagire con gli altri. Dobbiamo disabilitare una periferica? Possiamo disabilitare un solo modulo. Stiamo installandone un’altra? Possiamo abilitare un altro modulo. Tutto questo senza perdere gli altri moduli che gestiscono tutt’altre periferiche. Il concetto di modularità del software si avvicina molto, se non per tutto, alla modularità fisica: in una macchina “open”, sono libero di poter installare le periferiche che voglio con i dispositivi che voglio.

Lo sviluppo di un device driver però non è esente da rischi. Infatti un device driver si pone a livello tra le applicazioni, il sistema operativo e i dispositivi da controllare, pertanto ha un accesso privilegiato su alcune strutture del sistema operativo. Possono presentari problemi come race-condition, deadlock. Un’altra questione riguarda l’integrità del sistema: se, abilitando una estensione del kernel, il modulo riesce ad accedere a sezioni privilegiate, come posso prevenire attacchi informatici in caso di intrusione? Sfide come queste rimangono ancora aperte tutt’oggi.

Ritornando a Reolink, tutti i moduli e le estensioni del kernel sono contenute all’interno della cartella /lib/modules e sono caricate all’avvio del sistema. Tecnicalmente un modulo del kernel è un file oggetto, ovvero un frammento di codice eseguibile che fa riferimento a funzioni esterne. I file .ko non sono altro che file eseguibili, pertanto è possibile effettuare un’azione di reverse engineering senza adottare particolari espedienti.

Per chi fosse interessato a sviluppare nuovi moduli per Linux, c’è un’ottima risorsa a riguardo chiamata "The Linux Kernel Module Programming Guide" che introduce in modo eccellente allo sviluppo di device driver. Lettura consigliata!

Device driver sensore Omnivision

I moduli sono caricati in modo dinamico dallo script /etc/init.d/S10_SysInit2 che viene avviato appena il sistema operativo è stato caricato. All’interno di questo file sono enumerati una serie di moduli per controllare, utilizzare e impostare i diversi dispositivi della board, tra cui il sensore ottico, il motore di intelligenza artificiale, le periferiche esterne.

[...]
insmod modulo_da_caricare.ko
[...]

Quello che vogliamo fare in questa parte è scegliere un device driver e provare - tramite alcune azioni di reverse engineering - a capire come funziona e in che modo si interfaccia con il sistema. Per questo scopo, abbiamo scelto di esplorare uno dei device driver per eccellenza: il device driver per il sensore ottico Omnivision.

Il device driver per il sensore ottico nvt_sen_os12d40.ko si trova all’interno della cartella /lib/modules/4.19.91/hdal/sen_os12d40 dove os12d40 è il modello del sensore equipaggiato nella telecamera Reolink RLC-810A. Una veloce ispezione con l’utility file mostra che si tratta di un file ELF per l’architettura ARM.

nvt_sen_os12d40.ko: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), BuildID[sha1]=01ccd81e7f0982593f7ca5b5c8c31f79e0e5f0aa, not stripped

Scopriamo inoltre che il binario contiene alcune informazioni utili dato che è “not stripped”. Facciamo un passo indietro per capire meglio cosa vuol dire “stripped”. Quando creiamo un programma, se specificato, il compilatore si preoccupa di inserire all’interno del file alcune informazioni utili per il debug. Queste includono il nome della funzione utilizzata, il nome delle variabili: informazioni molto preziose per il reverse engineering.

È utile lasciare dei binari non-stripped in produzione? Sì e no. Da una parte consente a chi è più esperto di poter capire meglio come un binario funziona e di poter generare un core dump più dettagliato, dall’altra parte però sono tutti overhead che pesano sulla dimensione di ogni binario. È pratico per gli sviluppatori includere moduli del kernel con le informazioni di debug per avere più dettagli in caso di errore.

Acquisiamo ulteriori informazioni riguardo l’estensione del kernel tramite l’utility modinfo.

filename:       /lib/modules/4.19.91/hdal/sen_os12d40/nvt_sen_os12d40.ko
license:        GPL
description:    sen_os12d40
author:         Novatek Corp.
version:        1.40.000
srcversion:     E22953EBFA0A94EB836FD2B
depends:        kdrv_builtin,kwrap,kflow_videocapture
name:           nvt_sen_os12d40
vermagic:       4.19.91 SMP preempt mod_unload modversions ARMv7 
parm:           sen_cfg_path:Path of cfg file (charp)
parm:           sen_debug_level:Debug message level (int)

Spendiamo un po’ di parole per commentare il risultato dello strumento. Il driver è stato sviluppato da Novatek e come licenza è stata specificata GPL. Possiamo ignorare al momento il campo version e la srcversion. Il campo depends è molto importante perché specifica eventuali dipendenze del modulo kernel da altri, in questo caso il modulo per il sensore ottico dipende da kdrv_builtin, kwrap e kflow_videocapture. I parametri che l’estensione accetta sono due: il percorso assoluto del file di configurazione (charp: puntatore ad un array di char) e il livello di debug (int: un intero) che specifica quanto verbosa deve essere il modulo.

A proposito di licenze: quando un modulo viene dichiarato conforme alla licenza GPL, il produttore dovrebbe allegare insieme ai moduli oggetti anche il codice sorgente originale per permettere agli utenti finali di modificare o aggiungere nuove funzionalità. In questo caso, non è ben chiaro perché Novatek non abbia rilasciato il codice sorgente pur avendo dichiarato come licenza GPL.

Reverse engineering tramite Ghidra

Ghidra rappresenta un altro insieme di strumenti essenziale per il reverse engineering. Sviluppato dalla NSA tramite Java, Ghidra consente di analizzare, indagare ed effettuare diverse analisi su un file binario.

Possiamo estrarre il modulo e caricarlo su Ghidra. Per farlo, creiamo un nuovo progetto di Ghidra (Create new Project) e carichiamolo attraverso la pratica interfaccia grafica (Load new binary...). Ghidra ci chiederà quali tipo di analisi dovranno essere effettuate sul binario. Per ora, lasciamo quelle che ci propone di default che sono più che sufficienti. Avviamo l’analisi tramite il tasto Analyze.

Conclusa l’analisi da parte di Ghidra, possiamo incominciare l’esplorazione tramite il pannello Symbol Tree in basso a sinistra. È interessante poter vedere quali operazioni il modulo del kernel esegue e per questo ci interessano le funzioni. Apriamo la cartella funzioni e andiamo in cerca della funzione principale per il modulo.

Per chi ha già esperienze di programmazione, sa bene come la funzione principale sia “main”. Nel contesto dello sviluppo di moduli del kernel, non si chiama main ma init_module. Dal momento che il binario non è stripped, contiene il nome delle funzioni, per cui basta cercare all’interno del Symbol Tree la funzione init_module. Una volta trovata, doppio click per aprire il decompilatore: proviamo a commentare il codice ottenuto.

Il decompilatore è una delle caratteristiche di Ghidra e consente di avere una panoramica più leggibile delle istruzioni del programma. Ogni programma infatti ha una sezione chiamata .text contenente le istruzioni assembly. Tali istruzioni sono molto difficili da comprendere perché richiedono conoscenze non banali dell’architettura dell’esecutore e del linguaggio Assembly. Il decompilatore permette di avere un codice a più alto livello, preservando il codice originale assembly. Ovviamente il decompilatore non permette di ricavare il codice sorgente originario, ma consente di avere una buona approssimazione di tale codice.

void init_module(void){
  undefined *__haystack;
  undefined4 uVar1;
  char *pcVar2;
  size_t sVar3;
  int *piVar4;
  int iVar5;
  code *local_134;
  undefined4 local_130;
  undefined1 *local_12c;
  char local_125 [257];
  int local_24;
  
  local_24 = __stack_chk_guard;
  iVar5 = 0;
  memset(local_125,0,0x101);
  do {
    uVar1 = kdrv_builtin_is_fastboot();
    *(undefined4 *)(is_fastboot + iVar5 * 4) = uVar1;
    uVar1 = isp_builtin_get_i2c_id(iVar5);
    *(undefined4 *)(fastboot_i2c_id + iVar5 * 4) = uVar1;
    uVar1 = isp_builtin_get_i2c_addr(iVar5);
    *(undefined4 *)(fastboot_i2c_addr + iVar5 * 4) = uVar1;
    __haystack = sen_cfg_path;
    iVar5 = iVar5 + 1;
  } while (iVar5 != 8);
  pcVar2 = strstr(sen_cfg_path,"null");
  if ((pcVar2 == (char *)0x0) && (pcVar2 = strstr(__haystack,"NULL"), pcVar2 == (char *)0x0)) {
    if ((__haystack != (undefined *)0x0) && (sVar3 = strlen(__haystack), sVar3 < 0x101)) {
      strncpy(local_125,__haystack,0x100);
    }
    piVar4 = (int *)sen_common_open_cfg(local_125);
    if (piVar4 == (int *)0x0) {
      printk(&DAT_00018376,"sen_init_os12d40");
    }
    else {
      sen_common_load_cfg_map(piVar4,&sen_map);
      sen_common_load_cfg_preset(piVar4,(undefined4 *)sen_preset);
      sen_common_load_cfg_direction(piVar4,(undefined4 *)sen_direction);
      sen_common_load_cfg_power(piVar4,(undefined4 *)sen_power);
      sen_common_load_cfg_i2c(piVar4,(undefined4 *)sen_i2c);
      sen_common_close_cfg(piVar4);
    }
  }
  else {
    printk(&DAT_00018356,"sen_init_os12d40");
    local_125[0] = '\0';
  }
  local_130 = 0;
  local_134 = sen_pwr_ctrl_os12d40;
  local_12c = os12d40_sen_drv_tab;
  iVar5 = ctl_sen_reg_sendrv("nvt_sen_os12d40",&local_134);
  if (iVar5 == 0) {
    iVar5 = sensor_info_proc_init("nvt_sen_os12d40");
  }
  else {
    printk(&DAT_000183a2,"sen_init_os12d40");
  }
  if (local_24 != __stack_chk_guard) {
    __stack_chk_fail(iVar5);
  }
  return;
}

Illeggibile come codice. All’inizio, variabili come uVar1, sVar3 potrebbero spaventare il reverse engineer che c’è in voi. Questa situazione non è inaspettata dal momento che la compilazione non preserva i nomi delle variabili. È possibile però con un po’ di ingegno ottenere una simil nomenclatura del codice sorgente per migliorare la leggibilità del codice.

Incominciamo a pulire il codice e segnarci alcuni commenti. Per fare ciò abbiamo due principali possibilità: aggiungere commenti al codice all’interno di Ghidra - un po’ macchinoso - oppure copiare parte del codice sorgente e fare delle modifiche all’interno di un qualsiasi editor di testo. Personalmente, preferisco la seconda opzione dato che posso sempre vedere il codice decompilato prodotto da Ghidra.

Il primo blocco di codice che possiamo notare è dentro un ciclo do{..}while() in cui viene utilizzato un contatore chiamato iVar5 che varia da 0 a 7. Questo non è altro che un ciclo for utilizzato forse per recuperare lo stato degli 8 led messi a disposizione dal chip Omnivision. Per ogni led vengono recuperati i seguenti parametri: una flag chiamata fastboot, un id e un indirizzo. Con tutta probabilità ci troviamo di fronte a un codice del tipo:

int is_fastboot[8];
int fastboot_i2c_id[8];
int fastboot_i2c_addr[8];

Per ora non importa sapere di che tipo di dati si sta parlando dal momento che ci troviamo di fronte ad una flag (primo parametro), ad un id (un numero progressivo - secondo parametro) ed ad un indirizzo di memoria (terzo parametro). Tutti i dati possono essere rappresentati come interi; inoltre il fatto di avere dei salti nelle posizioni di memoria di 4 in 4, ci fanno presupporre che sia un intero a 32 bit. Il codice risultante è:

for(int i = 0; i<8; i++){
	is_fastboot[i] = kdrv_builtin_is_fastboot();
	fastboot_i2c_id[i] = isp_builtin_get_i2c_id(i);
	fastboot_i2c_addr[i] = isp_builtin_get_i2c_addr(i);
}

Focalizziamo i nostri sforzi per capire meglio il significato di ogni funzione. La funzione kdrv_builtin_is_fastboot restituisce vero o falso (intuibile) a seconda di una condizione chiamata fastboot di cui non conosciamo nulla al momento. Questa funzione è contenuta probabilmente in un altro modulo chiamato kdrv_builtin.ko. Le altre due funzioni isp_builtin_get_i2c_id e isp_builtin_get_i2c_addr sembrano molto più interessanti perché menzionano un sistema di comunicazione chiamato i2c che permette la comunicazione seriale tra circuiti integrati.

Tutte le informazioni dei dispositivi viaggiano in dei componenti chiamati bus sottoforma di segnali elettrici. Dal momento che più dispositivi inviano segnali elettrici attraverso un bus condiviso, è essenziale che ci sia un protocollo che determini come le informazioni devono essere comunicate. Il protocollo che è stato usato in questo caso è l’I2C, ne avevamo già parlato nell’articolo precedente.

Una volta che il programma ha popolato queste strutture, possiamo individuare il primo parametro che viene passato al modulo, ovvero il path assoluto del file di configurazione del sensore sen_cfg_path. Il secondo blocco di istruzioni interessa la lettura e l’impostazione del file di configurazione del sensore.

All’interno del secondo blocco viene utilizzata la funzione strstr che consente di trovare la prima occorrenza di una stringa s2 all’interno di una stringa s1. Definita nella libreria <string.h>, la funzione strstr ha la seguente sintassi:

const *char strstr ( const *char str1, const *char str2 );

Ritorna un puntatore NON nullo solo nel caso in cui la stringa s2 è contenuta all’interno della stringa s1. Considerato ciò, sappiamo più o meno cosa fa il nostro modulo kernel. Dal codice più in alto, si ottiene il seguente codice riscrivendo un po’ di istruzioni:

char *sen_cfg_path;
char path_name[257];
int status = strstr(sen_cfg_path, "null");
if(status == NULL){

	if(strlen(sen_cfg_path) < 257 && sen_cfg_path != NULL){
		strncpy(path_name, sen_cfg_path, 256);
	}

	file_config_fd = sen_common_open_cfg(path_name);
	
	if(file_config_fd == 0){
		printk("Error");
	} else {
		sen_common_load_cfg_map(file_config_fd, &sen_map);
		sen_common_load_cfg_preset(file_config_fd, sen_preset);
		sen_common_load_cfg_direction(file_config_fd, sen_direction);
		sen_common_load_cfg_power(file_config_fd, sen_power);
		sen_common_load_cfg_i2c(file_config_fd, sen_i2c);
		sen_common_close_cfg(file_config_fd);
	}
}

Il modulo del kernel prima compara la stringa a “null”. Se non c’è corrispondenza (ovvero se il sen_cfg_path contiene qualcosa diverso dal valore NULL), allora procede a contare quanti caratteri ci sono all’interno della path assoluta. Se sono meno di 257 (la dimensione del buffer allocato all’inizio), copia 256 caratteri di sen_cfg_path dentro il buffer path_name. 256 caratteri vengono copiati e l’ultimo, il 257esimo, viene utilizzato per il carattere di fine stringa \0.

Successivamente il modulo del kernel apre il file di configurazione del sensore, chiamando la funzione sen_common_open_cfg. La funzione sen_common_open_cfg chiama sen_cfg_open che a sua volta chiama la funzione vos_file_open. La funzione vos_file_open è una copia identica della syscall open dei sistemi operativi Unix-like. Prende in input il percorso del file da aprire, le flag per aprire il file (se in scrittura e in lettura) e opzionalmente una altro parametro chiamato mode utilizzato solo se viene specificata la creazione del file. La funzione ritorna un file descriptor, una sorta di ID univoco che permette di riferirsi all’apertura di un file tramite un comodo numero.

Questo file descriptor viene infatti utilizzato per leggere e impostare le diverse configurazioni del sensore. Per approfondire meglio cosa esegue ciascuna funzione, possiamo procedere con l’ottenere il file di configurazione dal firmware. Il file di configurazione si trova all’interno della cartella /mnt/src/sensor/ e si chiama sen_os12d40.cfg.

[MAP]
path_1 = 1                          #Path 1 Enable
path_2 = 0                          #Path 2 Disable
path_3 = 0                          #Path 3 Disable
path_4 = 0                          #Path 4 Disable
path_5 = 0                          #Path 5 Disable
path_6 = 0                          #Path 6 Disable
path_7 = 0                          #Path 7 Disable
path_8 = 0                          #Path 8 Disable

[PRESET]
id_0_expt_time = 10000              #10000us
id_0_gain_ratio = 1000              #1x gain

[DIRECTION]
id_0_mirror = 0                     #no mirror
id_0_flip = 0                       #no flip

[POWER]
id_0_mclk = 0                       #CTL_SEN_CLK_SEL_SIEMCLK
id_0_pwdn_pin = 0xFFFFFFFF          #no pwdn pin   
id_0_rst_pin = 0x44                    #S_GPIO_4
id_0_rst_time = 1                   #1ms
id_0_stable_time = 1                #1ms

[I2C]
id_0_i2c_id = 0                     #SEN_I2C_ID_1
id_0_i2c_addr = 0x36                #0x6C >> 1 = 0x36

Come possiamo notare il file è diviso in 5 sezioni principali: [MAP], [PRESET], [DIRECTION], [POWER], [I2C]. Ad ogni sezione corrisponde una particolare funzione che ha il compito di leggere i valori dal file e interpretarli. Per esempio: per la sezione [MAP], c’è la funzione sen_common_load_cfg_map, per la sezione [PRESET], c’è la funzione sen_common_load_cfg_preset e così via.

Studiamo un solo tipo di funzione dato che tutte le altre funzioni seguono lo stesso tipo di schema.

void sen_common_load_cfg_map(int cfg_fd, uint *ptr)
{
  char str_to_find [16];
  undefined result [512];
  size_t bR;

  for(int i = 0; i<8; i++){
  	sprintf(str_to_find, "path_%u", i);
  	bR = sen_cfg_get_field_str("MAP", str_to_find, result, 511, cfg_fd);
  	if(bR < 1){
  		printk("path_%u not exist \n", i+1);
  	}
  	else {
  		flag = simple_strtoul(result, 0);
  		if(flag == 1){
  			*ptr = *ptr | 1 << (i & 0xff);
  		}
  	}
  }
}

Più in dettaglio, la funzione sen_common_load_cfg_map preleva 8 righe dal file di configurazione nella sezione MAP. Attraverso la funzione sprintf concatena la stringa “path_” al numero di path da cercare (da 1 a 8). Successivamente, chiama la funzione sen_cfg_get_field_str che consente di posizionare sul buffer result la stringa che corrisponde alla chiave str_to_find, utilizzando il file descriptor cfg_fd. La funzione ritorna il numero di byte letti da file.

Una volta che il modulo è sicuro di aver letto qualcosa, tenta di convertire la stringa ad intero tramite la funzione simple_strtoul. Se tale valore è 1, allora scrivo nel puntatore che è stato dato come parametro ptr un numero. Questo numero è calcolato in uno strano modo a prima vista (1<< (i && 0xff)), ma ha un suo senso.

Siamo abituati a vedere i numeri quasi sempre tramite la notazione in base 10. La gestione delle periferiche, l’informatica e l’elettronica richiedono però l’utilizzo di un’altra base: quella binaria. Un numero in notazione binaria richiede 2 cifre per poter essere rappresentato: 0 oppure 1. Queste cifre sono molto importanti perché corrispondono allo stato fisico di un qualsiasi segnale (0: spento e 1: acceso). Possiamo quindi avere un numero (ad esempio 204 in base 10) come una sequenza di segnali da applicare al nostro circuito.

La dicitura *ptr | 1 << (i & 0xff) consente di impostare a 1 il bit in i-esima posizione del contenuto puntato da ptr. Questo è visibile come numero se stampato con %d, ma rappresenta in realtà una sorta di array di bit. Il risultato viene scritto nel puntatore sen_map. Come possiamo intuire dal file di configurazione, l’array di bit viene utilizzato per abilitare o disabilitare un’insieme di path.

Le altre sezioni sono molto simili: abbiamo alcune flag che consentono di impostare la direzione della camera (effetto specchio: id_0_mirror e effetto capovolto: id_0_flip), la sorgente (Master clock: clk, reset: RST_PIN, il tempo di reset: RST_TIME). Un compito interessante per il lettore potrebbe essere documentare cosa fanno le altre funzioni sen_common_load_cfg_preset , sen_common_load_cfg_direction, sen_common_load_cfg_power e sen_common_load_cfg_i2c.

Ritorniamo alla funzione principale init_module: una volta letto il file di configurazione, possiamo procedere con un altro blocco di codice che serve per instanziare e registrare il device driver all’interno del sistema operativo.

iVar5 = ctl_sen_reg_sendrv("nvt_sen_os12d40",&local_134);
if (iVar5 == 0) {
    iVar5 = sensor_info_proc_init("nvt_sen_os12d40");
}
else {
    printk(&DAT_000183a2,"sen_init_os12d40");
}

La funzione ctl_sen_reg_sendrv è contenuta in un altro modulo chiamato kvideocapture che molto probabilmente contiene il codice “core”, principale per la gestione del video della webcam. Abbiamo adesso due principali strade: approfondire meglio la funzione ctl_sen_reg_sendrv per capire a fondo cosa succeda dietro le quinte oppure ignorare la questione e approfondire le altre funzioni che vengono instanziate in questo modulo.

La funzione sensor_info_proc_init è abbastanza interessante. Questa funzione si preoccupa di creare una nuova entry nella cartella proc chiamata “sensor_id”. La cartella proc è basata su un file system virtuale montato ogni volta all’avvio del sistema operativo contenente alcune informazioni a tempo di esecuzione come le informazioni sui processi attivi (file descriptor). È anche definito come un’interfaccia virtuale alle strutture del kernel. Può contenere anche informazioni su periferiche esterne e sul loro utilizzo. Dopo un po’ di ritocchi, ecco il codice per questa funzione ridotta un po’ all’osso:

int sensor_info_proc_init(){
  p_sensor_dir = proc_mkdir("nvt_sen_os12d40",0);
  if (p_sensor_dir == 0) {
    puVar1 = &DAT_00018302;
  }
  else {
    p_sensor_id = proc_create("sensor_id",0x16d,p_sensor_dir, proc_sensor_id_fops);
    if (p_sensor_id != 0) {
      return 0;
    }
    puVar1 = &DAT_0001832b;
  }
  printk(puVar1);
  if (p_sensor_id != 0) {
    proc_remove();
  }
  if (p_sensor_dir != 0) {
    proc_remove();
  }
  return 0xffffffea;
}

La funzione crea quindi la cartella /proc/nvt_sen_os12d40 e crea poi l’entry /proc/nvt_sen_os12d40/sensor_id che contiene presubilmente informazioni sul tipo di sensore, costruttore e altri dati. La costruzione di una nuova entry è possibile grazie alla funzione proc_create che accetta 4 parametri: il nome dell’entry, i tipi di permessi associati, la cartella parent e proc_sensor_id_fops che rappresenta che tipo di accesso dare a quel file per i processi utenti.

Qui si conclude l’analisi della funzione init_module. Passiamo quindi ad analizzare come il driver riesce ad impostare e configurare il dispositivo. Sappiamo dalla quinta parte che i dispositivi hardware connessi alla board centrale hanno molti modi per poter essere configurati: tramite PIO o tramite interrupt. Abbiamo approfondito anche due protocolli di comunicazione asincrona: UART e I2C. Il sensore Omnivision OS12D40 utilizza proprio I2C per comunicare con la board centrale.

Possiamo quindi procede con decompilare altre due funzioni che il driver mette a disposizione: sen_write_reg e sen_read_reg. La prima serve per scrivere alcuni valori sui registri di controllo, mentre la seconda serve per leggere i registri di stato e di dato.

Le due funzioni sen_write_reg e sen_read_reg consentono di leggere scrivere dati su registri chiamando altre funzioni come i2c_transfer che viene implementata all’interno della libreria lib/i2c/i2c-core.ko.

void sen_write_reg_os12d40(int reg_number,undefined4 *param_2){
    local_32 = 0;
    local_26 = (undefined)*param_2;
    local_27 = (undefined)((uint)*param_2 >> 8);
    local_30 = 3;
    local_2c = &local_27;
    local_25 = (undefined)param_2[2];
    contenuto = (undefined2)*(int *)(sen_i2c + reg_number * 8 + 4);
    if (((*(int *)(is_fastboot + iVar1) == 0) ||
        (*(int *)(fastboot_i2c_id + iVar1) != *(int *)(sen_i2c + reg_number * 8))) ||
       (*(int *)(sen_i2c + reg_number * 8 + 4) != *(int *)(fastboot_i2c_addr + iVar1))) {
      iVar2 = 5;
      do {
        iVar1 = sen_i2c_transfer(reg_number,&contenuto,1);
        if (iVar1 == 0) goto LAB_00012bc0;
        iVar2 = iVar2 + -1;
      } while (iVar2 != 0);
      iVar1 = -5;
    }
    else {
      isp_builtin_set_transfer_i2c(reg_number,&contenuto,1);
      iVar1 = 0;
    }
}

Abbiamo quindi compreso come il driver per il sensore Omnivision OS12D40 venga inizializzato e come il sistema operativo crea l’interfaccia per poter interagire. Il passo successivo sarà capire meglio come le applicazioni di alto livello si interfacciano con il dispositivo per poterlo comandare.