logo elektroda
logo elektroda
X
logo elektroda

Running a simple MIPS emulator in Python for ALI M3801 firmware (Hello World)

p.kaczmarek2 1041 9

TL;DR

  • W Pythonie zbudowano prosty emulator MIPS32 dla firmware ALI M3801, oparty na Unicorn i Capstone, aby uruchomić „Hello World” z flasha.
  • Emulator mapuje pamięć, disasembluje kod, wykonuje instrukcje, obsługuje CP0/CP1 oraz ręcznie emuluje zapis sb/sw i rejestry UART.
  • Start odbywa się z bazą 0xAFC00000, a pierwszy testowy loop w firmware wykonuje się 64 razy.
  • Po poprawkach UART wypisywał tekst zgodnie z realnym CPU, łącznie z formatowaniem printf/fwrite i operacjami zmiennoprzecinkowymi.
  • Nadal problemem pozostają sporadyczne 16-bitowe instrukcje MIPS oraz mapowanie KSEG0/KSEG1, bo Unicorn traktuje adresy jak fizyczne 29-bitowe.
Generated by the language model.
ADVERTISEMENT
Treść została przetłumaczona polish » english Zobacz oryginalną wersję tematu
📢 Listen (AI):
  • Close-up of an ALI M3801 chip on a PCB with large “UNICORN” text at the bottom
    Here I will show my first attempt at building an emulator for the ALI M3801 microprocessor based on off-the-shelf Unicorn and Capstone modules. The developed program will load the contents of the Flash memory and execute it similarly to a real physical CPU, although it will not be without modifications and fixes, as Unicorn/Capstone do not implement the full logic of a particular SoC or its peripherals. In addition, the whole thing will be able to correctly handle sending data over the UART, i.e. it will emulate the register responsible for transmitting bytes over the hardware serial bus. In this way, we will get the same messages in the console that running a programme on a real ALI chip would show.

    Tools used
    Ghidra is an advanced reverse engineering (SRE) tool being developed by the NSA. It allows decompilation of MIPS machine code into pseudo-C code, making it much easier to analyse and understand firmware logic.
    Unicorn is a lightweight, multi-platform framework for CPU emulation based on QEMU. In the project it serves as the main engine for emulating MIPS32 instructions in Little Endian mode, although in practice much of the operation (including memory access) is handled separately in my code anyway.
    Capstone is an advanced disassembly engine supporting multiple architectures. It is used here to convert machine code into readable assembler instructions, which is essential for tracing and debugging them. It allows the user to see exactly what the processor is currently executing.
    I will do the project in the Pyhon language.

    Firmware used for demonstration
    To simplify the workflow I used a ready-made 'hello world' on ALI found on GitHub - michal4132/ali_sdk . I presented this project previously here:
    How to compile and run your own firmware for ALI M3801 and other tuner chips?
    This firmware is characterised by the absence of precompiled modules (so-called "blobs"), which makes it easier to analyse and compare the source code to the results from the emulator. For this reason, we will use it here.

    NOTE
    The topic assumes a basic knowledge of terminology and I will not explain here how the processor works. I will focus here on just presenting the construction of my simple emulator.

    Importing firmware in Ghidr
    Open Ghidra, create a new project, do File->Import File:
    Screenshot of Ghidra with the File menu open and “Import File…” highlighted.
    Before importing the file, you need to configure it properly.
    Ghidra import dialog showing “Format: Raw Binary” and program name “dump.bin”.
    Set the architecture: MIPS Little-Endian. MIPS is a RISC architecture and Little-Endian specifies the order in which bytes are stored in memory.
    Ghidra “Language” dialog listing MIPS variants, with filter “mips” and a selected 32-bit little-endian option.
    Set base address: afc00000. The base address is the fixed starting address of the memory area. It serves as a reference point for further addressing of data or registers, without a correct base address the jump instructions would go to the wrong place.
    “Options” dialog showing Block Name, Base Address afc00000, File Offset 0x0, and Length 0x400000.
    When opened, Ghidra gives us two views. The first time we have to wait for the decompilation to finish, but after that everything works smoothly.
    The first view is directly the bytes of the opened file but mapped to the base address - hence the addresses start with AFC. Next to them we have the decompiled commands with their arguments.
    The second view is C pseudo-code, which tries to show what the function would do in C - as much as it can. A lot of information is lost when compiling, so we don't even have variable names or the exact syntax here as it was in the source code.
    Screenshot of Ghidra showing MIPS assembly listing and decompiled C-like pseudocode side by side
    In this case we have the source code of the decompiled program, so we can compare and check. The startup routine is partly written in assembler and partly in C:
    start.S:
    Code: text
    Log in, to see the code

    Continued in C:
    entry.c:
    Code: C / C++
    Log in, to see the code

    You can easily compare the two files:
    Screenshot comparing MIPS disassembly listing in Ghidra with source code on the right, linked by red lines
    You can see here, for example, that FUN_afc00afc(0xffffffff,0xffffffff,1); is uart_attach
    Ghidra screenshot showing decompilation and C source for the uart_attach function


    Deassembly in Python
    Let's start with the simplest one. This example shows a simple disassembly of a binary code after a given address in Python. The binary is loaded into memory at the specified base address, without running or emulating instruction execution. The Unicorn engine is only used here to map the memory, while Capstone reads the bytes from under the specified address and translates them into MIPS assembler instructions. This allows the contents of the ROM code to be quickly previewed in human-readable form and compared with what Ghidra shows.
    Code: Python
    Log in, to see the code

    As a test, I read the first few dozen instructions. Virtually everything agrees:
    Screenshot comparing MIPS disassembly in Ghidra with console output from a program
    The only difference is li (in Ghidra) which the Python program shows as addiu. This is because li is not an actual MIPS processor instruction, but an assembler pseudo-instruction. In firmware, there is an actual addiu (or sometimes lui + ori), which Capstone shows explicitly, while Ghidra simplifies the notation to li for readability.

    First steps with the emulator
    Now you can go one step further and start executing the instructions. In this example, register operations, calculations and conditional jumps will already be running. This time Unicorn will already be performing operations, although, as I found out shortly afterwards, not everything will work correctly. But one step at a time:
    Code: Python
    Log in, to see the code

    I put a limit on the program's executed instructions and compared the emulation's "footprint" with what is seen in Ghidra. The basics match, the jumps also match:
    
    
    Executing first 40 instructions:
    
    Set CP0 Status = 0x10400000
    Memory Check at 0xafc00000: 00800840
    Starting emulation at 0xafc00000...
    0xAFC00000: 00 80 08 40     mfc0        $t0, $s0, 0
    0xAFC00004: f8 ff 09 24     addiu       $t1, $zero, -8
    0xAFC00008: 24 40 09 01     and $t0, $t0, $t1
    0xAFC0000C: 03 00 08 35     ori $t0, $t0, 3
    0xAFC00010: 00 80 88 40     mtc0        $t0, $s0, 0
    0xAFC00014: 07 80 08 40     mfc0        $t0, $s0, 7
    0xAFC00018: 30 00 08 35     ori $t0, $t0, 0x30
    0xAFC0001C: 07 80 88 40     mtc0        $t0, $s0, 7
    0xAFC00020: 00 68 80 40     mtc0        $zero, $t5, 0
    0xAFC00024: c0 00 00 00     ehb
    0xAFC00028: 00 48 80 40     mtc0        $zero, $t1, 0
    0xAFC0002C: 00 58 84 40     mtc0        $a0, $t3, 0
    0xAFC00030: c0 00 00 00     ehb
    0xAFC00034: 00 e0 80 40     mtc0        $zero, $gp, 0
    0xAFC00038: 00 e8 80 40     mtc0        $zero, $sp, 0
    0xAFC0003C: 00 20 80 40     mtc0        $zero, $a0, 0
    0xAFC00040: c0 00 00 00     ehb
    0xAFC00044: 00 60 08 40     mfc0        $t0, $t4, 0
    0xAFC00048: 00 20 09 3c     lui $t1, 0x2000
    0xAFC0004C: 25 40 09 01     or  $t0, $t0, $t1
    0xAFC00050: 00 60 88 40     mtc0        $t0, $t4, 0
    0xAFC00054: c0 00 00 00     ehb
    0xAFC00058: 00 f8 c0 44     ctc1        $zero, $31
    0xAFC0005C: 00 e0 c0 44     ctc1        $zero, $28
    0xAFC00060: ff ff 08 24     addiu       $t0, $zero, -1
    0xAFC00064: 00 00 88 44     mtc1        $t0, $f0
    0xAFC00068: 00 08 88 44     mtc1        $t0, $f1
    0xAFC0006C: 00 10 88 44     mtc1        $t0, $f2
    0xAFC00070: 00 18 88 44     mtc1        $t0, $f3
    0xAFC00074: 00 20 88 44     mtc1        $t0, $f4
    0xAFC00078: 00 28 88 44     mtc1        $t0, $f5
    0xAFC0007C: 00 30 88 44     mtc1        $t0, $f6
    0xAFC00080: 00 38 88 44     mtc1        $t0, $f7
    0xAFC00084: 00 40 88 44     mtc1        $t0, $f8
    0xAFC00088: 00 48 88 44     mtc1        $t0, $f9
    0xAFC0008C: 00 50 88 44     mtc1        $t0, $f10
    0xAFC00090: 00 58 88 44     mtc1        $t0, $f11
    0xAFC00094: 00 60 88 44     mtc1        $t0, $f12
    0xAFC00098: 00 68 88 44     mtc1        $t0, $f13
    0xAFC0009C: 00 70 88 44     mtc1        $t0, $f14
    




    Loops with emulator
    Similarly, loops also work. First we have loops copying data into RAM. I added a counter to the emulator to show how many times (globally) an instruction has been executed, this allows us to visualise a little better what is happening:
    Code: Python
    Log in, to see the code

    Let's consider the first loop from this firmware:
    
    0xAFC000E4: 40 00 04 24     addiu       $a0, $zero, 0x40
    0xAFC000E8: 00 00 05 24     addiu       $a1, $zero, 0
    0xAFC000EC: 00 60 02 24     addiu       $v0, $zero, 0x6000
    0xAFC000F0: 00 50 80 40     mtc0        $zero, $t2, 0
    0xAFC000F4: 00 10 80 40     mtc0        $zero, $v0, 0
    0xAFC000F8: 00 18 80 40     mtc0        $zero, $v1, 0
    0xAFC000FC: 00 28 82 40     mtc0        $v0, $a1, 0
    0xAFC00100: 00 00 85 40     mtc0        $a1, $zero, 0
    0xAFC00104: 01 00 a5 24     addiu       $a1, $a1, 1
    0xAFC00108: fd ff a4 14     bne $a1, $a0, 0xafc00100
    0xAFC0010C: 00 00 00 00     nop
    0xAFC00100: 00 00 85 40     mtc0        $a1, $zero, 0 [LOOP 2]
    0xAFC00104: 01 00 a5 24     addiu       $a1, $a1, 1 [LOOP 2]
    0xAFC00108: fd ff a4 14     bne $a1, $a0, 0xafc00100 [LOOP 2]
    0xAFC0010C: 00 00 00 00     nop  [LOOP 2]
    0xAFC00100: 00 00 85 40     mtc0        $a1, $zero, 0 [LOOP 3]
    0xAFC00104: 01 00 a5 24     addiu       $a1, $a1, 1 [LOOP 3]
    0xAFC00108: fd ff a4 14     bne $a1, $a0, 0xafc00100 [LOOP 3]
    

    The loop section (afc00100 - afc00108) repeats 64 times.
    This is the loop from entry.S:
    Code: text
    Log in, to see the code

    This can be compared to the source code:
    Code: C / C++
    Log in, to see the code

    This is what the end of the loop looks like:
    
    0xAFC00100: 00 00 85 40     mtc0        $a1, $zero, 0 [LOOP 63]
    0xAFC00104: 01 00 a5 24     addiu       $a1, $a1, 1 [LOOP 63]
    0xAFC00108: fd ff a4 14     bne $a1, $a0, 0xafc00100 [LOOP 63]
    0xAFC0010C: 00 00 00 00     nop  [LOOP 63]
    0xAFC00100: 00 00 85 40     mtc0        $a1, $zero, 0 [LOOP 64]
    0xAFC00104: 01 00 a5 24     addiu       $a1, $a1, 1 [LOOP 64]
    0xAFC00108: fd ff a4 14     bne $a1, $a0, 0xafc00100 [LOOP 64]
    0xAFC0010C: 00 00 00 00     nop  [LOOP 64]
    0xAFC00110: 02 00 00 42     tlbwi
    0xAFC00114: 00 00 00 00     nop
    0xAFC00118: 01 82 1d 3c     lui $sp, 0x8201
    0xAFC0011C: 00 80 bd 27     addiu       $sp, $sp, -0x8000
    0xAFC00120: c0 af 08 3c     lui $t0, 0xafc0
    0xAFC00124: 04 4a 08 25     addiu       $t0, $t0, 0x4a04
    0xAFC00128: 08 00 00 01     jr  $t0
    0xAFC0012C: 00 00 00 00     nop
    0xAFC04A04: d0 ff bd 27     addiu       $sp, $sp, -0x30
    0xAFC04A08: 2c 00 bf af     sw  $ra, 0x2c($sp)
    


    Subsequent loops:
    
    
    0xAFC04A68: 14 00 c4 af     sw  $a0, 0x14($fp) [LOOP 9]
    0xAFC04A6C: 00 00 63 8c     lw  $v1, ($v1) [LOOP 9]
    0xAFC04A70: 00 00 43 ac     sw  $v1, ($v0) [LOOP 9]
    0xAFC04A74: 18 00 c2 8f     lw  $v0, 0x18($fp) [LOOP 9]
    0xAFC04A78: 01 00 42 24     addiu       $v0, $v0, 1 [LOOP 9]
    0xAFC04A7C: 18 00 c2 af     sw  $v0, 0x18($fp) [LOOP 9]
    0xAFC04A80: 24 00 c2 8f     lw  $v0, 0x24($fp) [LOOP 10]
    0xAFC04A84: 18 00 c3 8f     lw  $v1, 0x18($fp) [LOOP 10]
    0xAFC04A88: 2b 10 62 00     sltu        $v0, $v1, $v0 [LOOP 10]
    0xAFC04A8C: f1 ff 40 14     bnez        $v0, 0xafc04a54 [LOOP 10]
    0xAFC04A90: 00 00 00 00     nop  [LOOP 10]
    0xAFC04A54: 10 00 c3 8f     lw  $v1, 0x10($fp) [LOOP 10]
    0xAFC04A58: 04 00 62 24     addiu       $v0, $v1, 4 [LOOP 10]
    0xAFC04A5C: 10 00 c2 af     sw  $v0, 0x10($fp) [LOOP 10]
    




    Stopping at an instruction
    Another useful mechanism that I have decided to implement is to stop the program on a command with a given address. This allows me to easily check if the executed program reaches a certain point, whose address I find in Ghidra. You could say that this is a simple breakpoint, like in a debugger. For the moment, it is enough for me to define the STOP_INSTR variable in the code.
    Code: text
    Log in, to see the code

    This is where Ghidra came in handy again. There I selected the address at which I want to stop the execution of the commands (afc04adc) and then verified the program trace to make sure everything was correct. This is very convenient and useful for testing and verifying that the program is running correctly.
    Ghidra screenshot showing MIPS disassembly and C pseudocode; the line at address AFC04ADC is highlighted.

    Screenshot of a terminal showing MIPS emulation trace and stop at address 0xAFC04ADC


    Fine UART initialization fix

    The program, prepared in this way, was already reaching uart_set_mode, but was showing an access error when trying to write data to the UART register.
    Code: C / C++
    Log in, to see the code

    These registers were not mapped to memory:
    Code: C / C++
    Log in, to see the code

    I had to add their mapping:
    Code: Python
    Log in, to see the code

    After this change, the emulator gets as far as 0xAFC04B04, which is where the text data will be sent:
    Ghidra screenshot showing MIPS disassembly and C pseudocode with a highlighted function call
    What's more, the function itself from the display also executes. I set the endpoint right after it, and there are no errors.
    Terminal screenshot showing MIPS emulation trace with assembly instructions and a stop message at an address

    Interrogating the text display
    Unfortunately, full support for text display would require emulation of the UART along with reading its register used to send data. For now, we'll keep it simple and just capture the printf function itself. In Ghidra, it is easy to trace it because its argument is a character string:
    Ghidra screenshot with function calls; the line containing “Booting...” is underlined in red.
    We can intercept its call and artificially skip its execution:
    Code: Python
    Log in, to see the code

    Result:
    Terminal screenshot showing MIPS instruction trace and “[PYTHON HOOK] Printf Detected!” message
    Terminal screenshot showing MIPS emulation trace and “[PYTHON HOOK] Printf Detected!” messages
    Well, yes, but now printf-style formatting of variables doesn't work. No wonder, our Python function displays blindly. Maybe it's better to look at the printf source code:
    Code: C / C++
    Log in, to see the code

    The formatting can be emulated and we plug in fwrite though. Here, however, was a problem that took me a long time, but I will keep it to a minimum for you. It seems that the execution of the sb/sw commands , i.e. the instructions responsible for writing to memory, is not working.
    I have implemented their manual execution:
    Code: Python
    Log in, to see the code

    And this is what the hook on fwrite looks like:
    Code: Python
    Log in, to see the code

    Result:
    Console screenshot showing Python hook “fwrite Detected” and a MIPS instruction trace with 0xAFC00AAC addresses
    Agrees with the one from the CPU:
    Screenshot of RealTerm showing UART logs: “Booting…”, stack/heap addresses, and a calculation result.
    All text, after turning off showing instructions:
    Terminal screenshot with emulator logs and “Invalid memory read (UC_ERR_READ_UNMAPPED)” error
    Not too bad, even operations on floating point numbers work.

    Faster UART emulation
    Capturing kprintf or there fwrite is nice for testing, but not at all practical. The address of these functions can probably change with each compilation. It is true that at compile time you can force a function to have a given address, but I wouldn't expect that here.
    The UART needs to be handled better - you need to know where the hardware UART register is and it's from there that you read the data.
    Fortunately we already have this information - it can be found in many SDKs on GitHub.
    The UART addresses are:
    Code: C / C++
    Log in, to see the code

    The register for the character is:
    Code: C / C++
    Log in, to see the code

    However, let us focus on the posting itself. We can easily conclude that all we need to do is capture the write to this address and display it as output from the UART.
    This is where a small technical problem arose, because as it turned out, Unicorn does not execute some of the commands correctly, so I had to implement them manually:

    Code: Python
    Log in, to see the code

    Only then are the operations executed.
    Terminal screenshot showing MIPS emulation logs and intercepted UART writes
    Eventually the UART sends the data, but something is wrong. The data is repeated three times. An explanation of this will be found below:
    Screenshot of Ghidra showing C-like pseudocode with a highlighted memory assignment line
    C function uart_write_char code for UART communication with retry mechanism
    The firmware checks to see if the UART acknowledges the sending of the data and, if not, performs the transmission again. All in a loop, in a blocking manner. So we still need to include the transmission acknowledgement flag.
    To do this, we need to simulate bit 0x20 of SCI_16550_ULSR. We can do this as soon as a byte is sent. Very simple:
    Python code fragment with UART memory write condition and status simulation
    Full code:
    Spoiler:

    Code: Python
    Log in, to see the code


    As of now, the UART is sending data correctly.


    Problem to be solved in the next topic
    The main problem that is still to be solved is the 16-bit MIPS commands, these occur sporadically on the original upload from ALI:
    Screenshot of disassembled code showing mixed 16- and 32-bit MIPS instructions
    And moments later:
    Screenshot of decompiled MIPS code with mixed 16- and 32-bit instructions
    The emulator used does not seem to support this, so there will be further combinations.

    Summary Summary
    This managed to run Hello world completely as if the target CPU was doing it - no shortcuts or simplifications. My program emulates the base of the ALI M3801 and is able to show what would be sent via UART 1.
    The whole thing turned out to be more difficult than I thought, as I had to reimplement some of the commands myself to get the read/write to work correctly, and on addresses as MIPS sees them - Unicorn does not implement KSEG0/KSEG1 segmentation and masks the address to 29 bits, treating it as a physical address. This is well demonstrated in this example:
    Spoiler:

    0xAFC00E18: 07 00 09 a1 sb $t1, 7($t0)
    0xAFC00E20: 03 40 29 35 ori $t1, $t1, 0x4003
    0xAFC00E24: 00 00 09 ad sw $t1, ($t0)
    0xAFC00E28: 0c 00 09 24 addiu $t1, $zero, 0xc
    0xAFC00E2C: 04 00 09 a1 sb $t1, 4($t0)
    0xAFC00E30: 00 a0 0a 3c lui $t2, 0xa000
    0xAFC00E34: a0 26 4a 35 ori $t2, $t2, 0x26a0
    0xAFC00E38: 00 00 49 8d lw $t1, ($t2)

    [!] INVALID MEMORY ACCESS
    Type: 19
    Address: 0x000026A0
    Size: 4
    PC: 0xAFC00E38
    Unicorn Error: Invalid memory read (UC_ERR_READ_UNMAPPED)
    PC at error: 0xafc00e38

    The code shows 0xa00026a0 and the emulator wants access to 0x000026A0. Maybe I should tweak this to hold the physical conversion, but that's in the next section. Initially I thought a triple mapping into the same memory section would suffice:
    Code: Python
    Log in, to see the code

    but the operations weren't performing anyway - at this point it's not clear to me what I was doing wrong.

    Follow up soon, all suggestions welcome - this is my first approach to emulation.
    Here's a little preview of the next topic:
    MIPS debugger window showing assembly, registers, and UART output

    Cool? Ranking DIY
    Helpful post? Buy me a coffee.
    About Author
    p.kaczmarek2
    Moderator Smart Home
    Offline 
    p.kaczmarek2 wrote 14387 posts with rating 12308, helped 650 times. Been with us since 2014 year.
  • ADVERTISEMENT
  • #2 21814012
    p.kaczmarek2
    Moderator Smart Home
    Posts: 14387
    Help: 650
    Rate: 12308
    A small update as to the fun of emulating.

    As I suspected, I'm stuck on those 16-bit instructions for now. Without them, I won't even for a good while start executing the actual bootloader from the DVB receiver, and Unicorn/Capstone won't decode them for me. I'm trying manually for now. Below is a screenshot from me of the program with a screenshot from Ghidr superimposed:
    Screenshot of MIPS debugger with highlighted SAVE instruction and disassembled code view
    Much of the bootloader is written this way, so there is no way out, you have to support 16-bit instructions too.
    I'm testing on an insert from here:
    https://github.com/openshwprojects/FlashDumps/tree/main/Sat/Comsat%20TE%201050%20HD
    Helpful post? Buy me a coffee.
  • ADVERTISEMENT
  • #3 21819808
    bulek01
    Level 17  
    Posts: 336
    Help: 12
    Rate: 293
    Board Language: polish
    Thanks for this description, I've been wanting to get on with it myself to analyse another decoder on MIPS too. You made it very easy for me to go further by showing the base. Cool that you mapped the UARTU registers and handled the display. So far I've only had it written down in my notes that Unicorn and Capstone exist. I plan to figure out how I2C works and what the register addresses are. I already have the addresses of the functions in memory found, but unfortunately it is complex and it will be most convenient to analyse the operation by assembler stepping.
  • #4 21819824
    p.kaczmarek2
    Moderator Smart Home
    Posts: 14387
    Help: 650
    Rate: 12308
    At the moment the problem is with 16-bit inserts. Capstone/Unicorn doesn't seem to support them (I couldn't get it to do so), so I have to combine manually. Unfortunately they are repeated repeatedly in the actual bootloader. What I have can extract it to RAM and start executing it, but then there are problems.

    Custom compiled batches work 100% because they don't have 16-bit instructions.

    If you want to join forces and experiment with it, I'll show you how many I have, but it's a mess and on top of that I warn you that I backed up with LLMs.
    https://github.com/openshwprojects/AliSimulator
    In addition, there are some simple program tests - run_all_tests.py. These verify that the right system is decompiling the instructions (also the 16-bit ones) according to Ghidra, as well as checking that I haven't messed up the UART in the program from the first post. This allows you to experiment and iterate quickly without worrying about messing things up.

    This is what the program test from the first post looks like:
    Code: Python
    Log in, to see the code


    And yes a test if the right first bootloader from ALI copies well to RAM:
    Code: Python
    Log in, to see the code


    And this is what the mips decoding test looks like - verifying that my code sees the same thing as Ghidra:
    Code: Python
    Log in, to see the code
    Helpful post? Buy me a coffee.
  • #5 21820382
    MarcinBukat
    Level 11  
    Posts: 26
    Board Language: polish
    MIPS distinguishes (assuming the core supports this at all) whether it should interpret an instruction as 16bit or 32bit by looking at the youngest bit of the instruction address. Jumping to a function in 16bit mode requires the youngest bit of the address to be set. The same applies to manual manipulation of the PC register, so mu.reg_write(UC_MIPS_REG_PC, target_addr | 1) should set the code to execute in 16bit mode, and mu.reg_write(UC_MIPS_REG_PC, target_addr & ~1) should set the code to execute in 32bit mode, which might be useful for some hookups. How does the jump to the MIPS16 function look at all in the analysed code ? jalx, or jalr ? Another issue comes to mind - what core is in all this ALI ? A quick google search claims there is a MIPS32R2 sitting there. As far as I know, by default Uc(UC_ARCH_MIPS, UC_MODE_MIPS32 + UC_MODE_LITTLE_ENDIAN) does NOT use a core compatible with MIPS32R2. This needs to be enforced with mu.ctl_set_cpu_model(UC_CPU_MIPS32_24KF) e.g. This should e.g. solve the sb/sw problem, which is an extension available in MIPS32R2. This may admittedly have side effects, as it forces a specific TLB configuration e.g. It is nevertheless worth checking.
  • #6 21886365
    KT361A
    Level 2  
    Posts: 3
    Rate: 1
    Hi p.kaczmarek,
    thanks for pointing to https://github.com/qttest1/PDK_GoBian/
    I have a DVB-T receiver with M3801, but board layout is slightly different.
    I'll test a different approach - compiling the u-boot in buildroot.
    I plan to mount another spi-nor flash, I'm afraid a bit by OTP changes behind my back ;-)

    Here in Belgium is no more DVB-T/DVB-T2, I hope I could receive some DVB from FR/Lille.

    Regards,
  • ADVERTISEMENT
  • #7 21886391
    p.kaczmarek2
    Moderator Smart Home
    Posts: 14387
    Help: 650
    Rate: 12308
    Let me know how it goes, can you also share photos of your device?

    I can also try to help more. Here's some interesting stuff:
    - source code for related chip (bootloader + app):
    ali3602-m..ter.7z (65.21 MB)You must be logged in to download this attachment.
    - instruction on using native bootloader to flash data to memory:
    https://www.elektroda.com/rtvforum/topic4156384.html#21797046
    - this simulator can go now much futher, but mips16 is painful to handle, I had to hack it manually:
    https://github.com/openshwprojects/AliSimulator

    It also runs test on build.
    Running a simple MIPS emulator in Python for ALI M3801 firmware (Hello World)
    Helpful post? Buy me a coffee.
  • ADVERTISEMENT
  • #8 21886452
    KT361A
    Level 2  
    Posts: 3
    Rate: 1
    >>21886391
    Thanks for archives!
    My device is the same as of maciej_333 here:
    https://www.elektroda.com/rtvforum/topic4156384.html#21788762
    I soldered an UART, this appears at boot:
    APP init!
    bl_panel_init!
    bl_flash_init!
    bl_verify_sw
    success!
    MC: APP init ok
    << SDK4.0ba.4.0_20101217 >>

    Libcore version 8.9.0(_at_)SDK4.0bd.8.9_20130409(gcc version 3.4.4 mipssde-6.06.01-20070420)(vic.wang@ Mon Apr 1 19:08:06 2013)

    Application version 1.0.0(_at_)SDK4.0ba.7.4_20120227

    And further - nothing. Pausing with 'c' also works.
    Time to remove the SPDIF conn and to dump the NOR with a clipse
    :-)
  • #9 21886453
    p.kaczmarek2
    Moderator Smart Home
    Posts: 14387
    Help: 650
    Rate: 12308
    Is clips working for you on such tuners? I think I had to desolder flash from mine...

    When you share flash dump copy, I may be able to try it with my emulator.
    Helpful post? Buy me a coffee.
  • #10 21886820
    KT361A
    Level 2  
    Posts: 3
    Rate: 1
    [postid:b812a02163]21886452[/posti[postid:b812a02163]21886452
    [/postid:b812a02163]

    See attached an archive with the dump. I used the 'classic' i2c/spi adapter with Winchip CH341
    "1a86:5512 QinHeng Electronics CH341 in EPP/MEM/I2C mode, EPP/I2C adapter "
    https://www.amazon.com.be/-/en/Efficient-CH341A-programmer-supports-accurately/dp/B0D99DXFZY
    set to 3.3V I/O, the DVB-T board was not powered from 230V.

    I think to stop currently with further investigations of M3801. I have a long experience with linux/buildroot/embedded devices ... IMHO it is futile work. We do not have even a block diagram of the chip. If there is no correctly working MMU, starting Linux will be a great PITA. Next, the processor is slow (600MHz). Even a ssh connection will take 2-5sec, and max transfer speed will not be faster than 1.5Mb/sec. Indeed, there is an USB 2.0 host, so that some USB2LAN 100Mbit adapter can be used.
    Finally, the consumption is continuous 5-7W. Current chips consume less than 2W, and are 2-5 times faster. (RPI etc)

    I think to keep it as default DVB-T receiver, able to show jpgs, play H264 + record mpegTS streams.
    Greetings[/postid:b812a02163]
    Attachments:
    • dvb-t-M3801-N2.tgz (1.75 MB) You must be logged in to download this attachment.
📢 Listen (AI):

FAQ

TL;DR: Build a Python MIPS emulator for ALI M3801 that boots Hello World; 64 TLB entries are initialized, and "This managed to run Hello world completely as if the target CPU was doing it." UART 16550 at 115200 and MMIO fixes included. [Elektroda, p.kaczmarek2, post #21813183]

Why it matters: It shows how to go from raw firmware to a working UART-visible boot on a PC, helpful for reverse engineering and testing.

Quick Facts

Who is this FAQ for, and what problem does it solve?

For firmware hackers, embedded engineers, and reverse engineers who need to run ALI M3801 code on a PC. It explains how to load a flash image, execute startup code, and capture UART output using Python with Unicorn and Capstone. It also covers MMIO mapping and store/load fixes. [Elektroda, p.kaczmarek2, post #21813183]

How do I load and disassemble ALI M3801 firmware in Python?

Map 8 MB at 0xAFC00000, write the binary, and use Capstone with MIPS32 Little-Endian to disassemble bytes from that base. Unicorn handles memory mapping; Capstone decodes instructions for trace output. This mirrors what Ghidra shows, with li appearing as addiu/lui+ori in raw form. [Elektroda, p.kaczmarek2, post #21813183]

How do I actually execute the firmware and trace instructions?

Create a Unicorn MIPS32 LE instance, map ROM at 0xAFC00000 and its 0x0FC00000 mirror, set CP0 Status (CU0, BEV), and start emulation from the base. Add a UC_HOOK_CODE callback to print each instruction and stop after N instructions for comparison with Ghidra. [Elektroda, p.kaczmarek2, post #21813183]

What causes the repeated UART characters, and how do I fix it?

Firmware polls the UART Line Status Register and resends until TX-empty is set. Without LSR bit 0x20, each byte prints multiple times. Initialize LSR at base+5 to 0x20 after mapping 0xB8018300, or set the flag on each successful byte write. This removes duplicates. [Elektroda, p.kaczmarek2, post #21813183]

How can I hook printf/fwrite to see strings quickly?

Add a UC_HOOK_CODE at kprintf or fwrite addresses, read arguments from $a0–$a3, and dump the buffer. Optionally skip the function by writing $pc = $ra. Quote: “Capturing kprintf...is nice for testing, but not at all practical” for changing builds. [Elektroda, p.kaczmarek2, post #21813183]

Why don’t some store/load instructions work, and what’s the workaround?

Unicorn may mishandle certain MIPS stores/loads in this setup. Implement a manual decoder in a code hook for SB/SH/SW and LB/LH/LW, perform mem_write/mem_read yourself, and advance PC. This restores RAM writes and enables UART byte captures reliably. [Elektroda, p.kaczmarek2, post #21813183]

How do I map MMIO so UART and other peripherals are accessible?

Map 16 MB regions for 0x18000000 (physical), 0x98000000 (KSEG0), and 0xB8000000 (KSEG1). These mirror the same hardware. Ensure 0xB8018300 is writable, and add a UC_HOOK_MEM_WRITE to log bytes as they hit the TX register. [Elektroda, p.kaczmarek2, post #21813183]

What is KSEG0/KSEG1 in MIPS, and why does Unicorn read 29-bit addresses?

KSEG0/KSEG1 are unmapped kernel segments that alias physical memory with cached/uncached behavior. Unicorn masks to 29 bits and treats addresses as physical, which can shift accesses (e.g., 0xA00026A0 → 0x000026A0). Mirror mappings or implement address translation logic. [Elektroda, p.kaczmarek2, post #21813183]

How can I set a breakpoint at a specific instruction address?

Track the current address in a UC_HOOK_CODE callback. If it equals your STOP_INSTR (e.g., 0xAFC04ADC), call emu_stop. Use Ghidra to locate the target, then verify the hit by examining the trace output. [Elektroda, p.kaczmarek2, post #21813183]

What does the boot loop with 64 iterations actually do?

The startup code initializes the TLB: it sets masks, iterates index writes 64 times, and then performs tlbwi. Statistic: 64 iterations total. This matches the C defines TLB_TABLE_NUM=64 and PAGE16K_MASK settings in the demo firmware. [Elektroda, p.kaczmarek2, post #21813183]

What is Ghidra, Unicorn Engine, and Capstone in this workflow?

Ghidra decompiles and labels MIPS code for analysis. Unicorn emulates MIPS32 Little-Endian execution. Capstone decodes machine code into assembly for readable tracing. Together, they load firmware, run startup, and validate behavior against source code. [Elektroda, p.kaczmarek2, post #21813183]

How do I fix 'Invalid memory read' at 0x000026A0 when code shows 0xA00026A0?

Mirror RAM across 0x80000000, 0xA0000000, and 0x00000000, or implement a translation layer that converts KSEG addresses to physical. This aligns Unicorn’s 29-bit masking with firmware expectations and prevents unmapped reads. [Elektroda, p.kaczmarek2, post #21813183]

What is MIPS16e (16-bit) instruction support status here?

The current emulator path does not support 16-bit MIPS instructions found in some ALI uploads. You must extend decoding/execution or switch to a core with MIPS16e support. This limitation is identified as a next-step problem. [Elektroda, p.kaczmarek2, post #21813183]

Can you show a 3-step how-to to get UART 'Booting...' output?

  1. Map ROM at 0xAFC00000 (mirror 0x0FC00000) and RAM at 0x81000000, then write the binary.
  2. Map MMIO at 0xB8000000 and set LSR (base+5) to 0x20; add a write hook at 0xB8018300.
  3. Start emulation, trace until kprintf/fwrite or UART writes appear. [Elektroda, p.kaczmarek2, post #21813183]

Why hook fwrite instead of relying solely on UART?

Hooking fwrite reveals fully formatted strings even before UART emulation is perfect. It speeds debugging when function addresses are known, though builds may relocate them. Quote: “Capturing kprintf or there fwrite is nice for testing.” [Elektroda, p.kaczmarek2, post #21813183]

What edge cases should I expect during early runs?

Expect triple-printed characters if LSR 0x20 isn’t set, missing RAM writes without manual SB/SH/SW handling, and address aliasing due to KSEG masking. These can stall boot or corrupt output until hooks and mirrors are in place. [Elektroda, p.kaczmarek2, post #21813183]
Generated by the language model.
ADVERTISEMENT