最近在搞模拟诺亚舟学习机,群聊里提到了 NP7000。
NP7000 蛮有意思的,它使用的是诺亚舟自制的诺亚神舟操作系统,我之前也蛮有兴趣想研究逆向一下这个系统怎么设计的,底层怎么工作的。
正好 NP7000 使用的是君正的 SoC 芯片,文档比较全,我之前模拟的学习机也是君正系列的芯片。
模拟器使用的是修改过的 QEMU 来支持各种君正的硬件外设,这里有我写的相关的 Wiki 页面:
https://github.com/OpenNoah/OpenNoah.github.io/wiki/NP7000-Emulation
识别君正 SoC 芯片型号
找到 NP7000 使用的芯片型号还蛮简单的。
从这里下载它的 TF 卡修复升级程序:
https://downloads.youxuepai.com/source/list.shtml#107-2361-0-0-0-0-0-0-
解压完后, V3.2 版本里有以下四个文件:
1 2 3
| $ ls 'NP7000(INAND V3.2).idx' 'NP7000(INAND V3.2).mop' 'NP7000(INAND V3.2).mo1' 'NP7000(INAND V3.2).mo2'
|
用十六进制编辑器打开那个 .idx 文件,然后我们直接就能看到:
1 2 3 4 5 6 7
| $ hexedit NP7000\(INAND\ V3.2\).idx 00000000 4E 4F 41 48 20 54 46 2D 43 41 52 44 2F 69 4E 41 4E 44 2D 46 4C 41 53 48 20 49 4D 41 47 45 20 46 NOAH TF-CARD/iNAND-FLASH IMAGE F 00000020 49 4C 45 20 56 32 2E 30 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ILE V2.00....................... 00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................................ 00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................................ 00000080 4A 5A 34 37 35 35 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 JZ4755.......................... 000000A0 4E 50 37 30 30 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 NP7000..........................
|
提取 .idx 和 .mop 里的文本字符串,也可以看到 JZ4755:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| $ strings NP7000\(INAND\ V3.2\).idx | grep JZ4 JZ4755 $ strings NP7000\(INAND\ V3.2\).mop | grep JZ4 ../../Peripheral/Clock/./JZ4750/DrvClock.c ../../Peripheral/iNand/./JZ4755/InandCtrl.c ../../Peripheral/Power/./JZ4750/DrvPower.c ../../Peripheral/RTC/./JZ4755/DrvRtc.c ../../Peripheral/SD/./JZ4755/SDControl.c ../../Peripheral/Touch/./JZ4755/DrvTouch_np7000.c ../../Peripheral/UART/./JZ4750/DrvUart.c JZ4755 ../../Peripheral/Clock/./JZ4750/DrvClock.c ../../Peripheral/iNand/./JZ4755/InandCtrl.c ../../Peripheral/Power/./JZ4750/DrvPower.c ../../Peripheral/RTC/./JZ4755/DrvRtc.c ../../Peripheral/SD/./JZ4755/SDControl.c ../../Peripheral/Touch/./JZ4755/DrvTouch_np7000.c ../../Peripheral/UART/./JZ4750/DrvUart.c
|
所以,基本可以确定 SoC 芯片是君正的 JZ4755。
这里有一些相关的数据文档:
https://opennoah.github.io/datasheet/JZ4755_DS.PDF
https://opennoah.github.io/datasheet/JZ4755_pm.pdf
https://opennoah.github.io/datasheet/XBurst1_Core_PM.pdf
https://opennoah.github.io/datasheet/X1000_M200_XBurst_ISA_MXU_PM.pdf
MIPS 基础知识
一些 MIPS 架构相关的基础知识这里会用到:
kseg 内存映射
https://training.mips.com/basic_mips/PDF/Memory_Map.pdf

从地址 0x80000000 开始的 0x20000000 长度的内存区域 kseg0 以及 0xa0000000 开始的 kseg1 内存区域都会映射到从地址 0x00000000 开始的硬件地址,唯一的区别是数据访问是否缓存。
在 JZ4755 文档里记录的寄存器地址都是硬件内存映射地址,比如 0x1002101c。
从内核驱动代码里访问的时候,我们需要使用不进行缓存的 kseg1 映射区域。
所以,寄存器地址比如 0x1002101c 在代码中的访问地址应该是 0xa0000000 + 0x1002101c = 0xb002101c。
初级启动程序
根据 JZ4755 的 programming manual 文档,章节 29 XBurst Boot ROM Specification,JZ4755 支持从 NAND 闪存或者 MSC0 控制器上的 SD 卡启动。
两种启动方法都会读取 8 KiB 的数据到内部 SRAM 内存地址 0x80000000。
章节 29.2 Boot Sequence:
NOTE: The JZ4755 internal SRAM is 16KB, its address is from 0x80000000 to 0x80004000.
(其实我怀疑这个 SRAM 可能是用 CPU 缓存模拟的?)
看看那个 .mop 升级文件:
1 2 3 4 5 6 7 8 9 10
| $ hd NP7000\(INAND\ V3.2\).mop | less 00000000 00 80 1f 3c 00 00 ff 27 00 90 80 40 00 98 80 40 |...<...'...@...@| 00000010 00 60 08 40 00 10 09 3c 04 04 29 35 24 40 09 01 |.`.@...<..)5$@..| ... 00001bd0 00 1b b7 00 00 36 6e 01 00 03 00 b2 00 00 00 00 |.....6n.........| 00001be0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00001bf0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| * 00002000 00 80 1f 3c 00 00 ff 27 00 90 80 40 00 98 80 40 |...<...'...@...@| 00002010 00 60 08 40 00 10 09 3c 04 04 29 35 24 40 09 01 |.`.@...<..)5$@..|
|
从 0x00001bf0 到 8 KiB 偏移量的 0x00002000 的数据全部是 0xff,一般用作没有数据时的填充数据,因为一般 NAND 闪存在擦除后的数据都是 0xff。
所以,这组 8 KiB 的数据区很像是第一级的启动程序。
注意到开头的 12 字节并不包含重复的 0x55 字节,所以这个不可能是 NAND 模式的启动程序,内容不符合:

所以我们知道这段代码肯定是从 MSC0 加载到偏移量 0x80000000,然后 CPU 也会跳转到地址 0x80000000 开始运行。
诺亚舟以前的机型比如 NP6800 也使用过类似的硬件设计,使用一个闪存控制芯片来把 NAND 芯片模拟成 SD 卡当作内部储存,很像现在流行的 eMMC 储存器:
https://github.com/OpenNoah/OpenNoah.github.io/wiki/NP6800+-Gallery
用 dd 将这段数据提取到一个单独文件里:
1
| dd if="NP7000(INAND V3.2).mop" of=boot_1st.bin bs=1024 count=8
|
接下来的 8 KiB 数据区域和开头的区域数据完全一样。
芯片的 NAND 启动方式支持一个像这样的备用启动数据区,如果开头的数据损坏了,芯片会使用备用区数据来启动。但是,因为这个设备使用的是 SD 卡启动,在这里并没有作用。
用 Ghidra 加载这个文件。
设置语言为 MIPS, default variant, 32-bit, little endian, eabi compiler.
在选项中,设置地址偏移量为 0x80000000。
反汇编出的代码看起来是正确的,有一些调试输出,我们可以猜到一些函数的作用来进行命名:

(截图中自定义的函数名字都是我自己添加的。)
分析一下那些调试输出的函数,我们可以找到它最终会写入寄存器地址 0xb0031000,这意味着调试数据会被写到 UART1 串口外设。
一般来说初级启动程序会配置下 SDRAM 控制器,这样它才能有更多的内存空间来加载后面更复杂的启动程序。
分析一下那个 mmc0_boot(0x20,0x3e0,0xa0600000) 函数:

很标准的 SD 卡初始化代码。
SD 卡协议标准可以从这里下载:
https://www.sdcard.org/downloads/pls/
Part 1 Simplified Physical Layer Simplified Specification
反汇编出的代码里这些地方比较有用:
1 2 3 4 5 6 7
|
mmc0_cmd(0x10,0x200,0x401,1);
_DAT_b002101c = param_2;
mmc0_cmd(0x12,param_1,0x409,1);
|
1 2 3 4 5
|
mmc0_boot(0x20,0x3e0,0xa0600000);
(*(code *)&SUB_80600000)();
|
所以,它会用 MSC0 外设,从偏移量 0x20 * 512 = 16 KiB 开始,读取 0x3e0 * 512 = 496 KiB 的数据到内存地址 0x80600000 里。
第二级启动程序
从 .mop 文件里提取出第二级启动程序:
1
| dd if="NP7000(INAND V3.2).mop" of=boot_2nd.bin bs=1024 skip=16 count=496
|
1 2 3 4 5 6 7 8 9 10
| $ hd NP7000\(INAND\ V3.2\).mop | less 00002000 00 80 1f 3c 00 00 ff 27 00 90 80 40 00 98 80 40 |...<...'...@...@| 00002010 00 60 08 40 00 10 09 3c 04 04 29 35 24 40 09 01 |.`.@...<..)5$@..| ... 000154a0 00 00 00 b2 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000154b0 00 00 00 00 00 00 00 00 ff ff ff ff ff ff ff ff |................| 000154c0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| * 00080000 84 80 1a 3c 90 f5 5a 27 00 60 1b 40 00 00 5b af |...<..Z'.`.@..[.| 00080010 00 40 1a 40 21 d8 40 03 82 de 1b 00 1b 00 60 17 |.@.@!.@.......`.|
|
和之前的启动程序类似,从 0x000154c0 到 0x00080000 (512 KiB 地址偏移) 的数据全部是 0xff。
所以,这个数据区域看起来也是正确的。
和之前一样,用 Ghidra 加载, mips 32-bit little-endian 但是使用地址偏移量 0x80600000。
偏移量 0x80605038 开始的代码比较有意思:

以及位于 0x80604e5c 的代码:

以及位于 0x80603fbc 的代码:

分析一下,我们可以找到从 MSC0 读取数据的 mmc0_read() 代码,然后计算出 load_image() 的函数参数单位是 16 KiB 数据块大小。
位于 0xb0010400 的寄存器是 GPIO PE 组的 PIN 输入寄存器。
我们可以看到这段代码支持几种不同的启动方式:
- 正常启动,从偏移量
0x180 = 6 MiB,最多加载 0x680 = 26 MiB
- 更新程序,从偏移量
0x20 = 512 KiB,最多加载 0x180 = 6 MiB,选择条件是 PE4 == 0
- 测试程序,从偏移量
0x20 = 512 KiB,最多加载 0x180 = 6 MiB,选择条件是 PE5 == 0
- 从
MSC1 SD 卡启动,读取文件 noahos.img,选择条件是 PE30 == 0
我们可以看到数据载入地址是 0x80000000,入口地址是 0x80000400。
数据区块的数量使用了 4 字节,记录在第一个数据区块的 0x01f0 偏移量处。
但是,在 .mop 文件偏移量 0x00600000 = 6 MiB 位置的数据并不在一个明显的数据分区上:
1 2 3 4
| 005FFFC0 07 00 65 88 40 02 A2 27 38 02 A7 A3 04 00 65 98 03 00 46 A8 0B 00 67 88 00 00 46 B8 44 02 A2 27 ..e.@..'8.....e...F...g...F.D..' 005FFFE0 0F 00 68 88 03 00 45 A8 08 00 67 98 00 00 45 B8 0C 00 68 98 48 02 A2 27 03 00 47 A8 4C 02 A6 27 ..h...E...g...E...h.H..'..G.L..' 00600000 00 00 47 B8 03 00 C8 A8 21 28 43 02 03 00 A7 88 10 00 69 90 00 00 A7 98 07 00 A3 88 58 02 A2 27 ..G.....!(C.......i.........X..' 00600020 00 00 C8 B8 50 02 A9 A3 04 00 A3 98 03 00 47 A8 0B 00 A8 88 00 00 47 B8 5C 02 A2 27 0F 00 A9 88 ....P.........G.......G.\..'....
|
试图用 MSC0 直接启动 .mop 会打印以下调试输出然后卡住:
1 2 3 4 5 6 7 8
| ======================================= NOAHOS-V2 BOOT LOADER ..... ======================================= RTC_HRCR: 0x0 eSD storage device found! LOADER: 0x0 eSD storage device found! IMG SIZE: 0xa3a802c8
|
可以看到在 0x006001f0 的数据确实是 0xa3a802c8。
特别功能内核
所以我们现在需要分析一下固件更新程序,来搞明白怎么正确读取固件更新数据包。
幸运的是加载固件更新和测试用的内核看起来正常很多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| ======================================= NOAHOS-V2 BOOT LOADER ..... ======================================= RTC_HRCR: 0x0 eSD storage device found! LOADER: 0x0 eSD storage device found! IMG SIZE: 0x200000 NOAHOS TEST UTILITIES LOADED OK!
================================================= NOAHOS-V2 BOOTING... BOOT FROM NAND ================================================= OSC = 24MHz PLL = 384MHz CCLK = 384MHz HCLK = 128MHz PCLK = 128MHz MCLK = 128MHz SDRAM size: 64MB NOAHOS IMG SIZE = 0x83c000 Time slice is 100 msec Device init: CLOCK IRQ22 attached! priority=10 Device init: UART IRQ8 attached! priority=10 Device init: LCDC LCDC: 34MHz key : 0xd Device init: NANDC DRV> nand share mode DRV> NAND ID: 0xff-0xff-0xff-0xff
!!! KERNEL PANIC: DRV> unidentify nand flash
|
不知道为什么它试图使用 NAND 驱动来读取不存在的 NAND 闪存芯片,然后失败了。
我们可以从偏移量 0x00080000 (512 KiB) 开始提取 0x00200000 (2 MiB) 大小的数据,加载到 Ghidra 地址 0x80000000。
偏移量 0x80000400 是入口地址,所以在那个位置开始创建一个新的函数。
根据代码逻辑分析到 0x8000237c 位置的函数:

条件 (0xb0010200 & 0x80000000) == 0 也就是 GPIO PC31 == 0 决定它是从 MSC0 SD 卡启动还是 NAND 闪存启动。
所以诺亚舟的 “INAND” 意思是 NAND 模拟成的 SD 卡。
在模拟器里修好这个 GPIO 值,现在我们可以看到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| ======================================= NOAHOS-V2 BOOT LOADER ..... ======================================= RTC_HRCR: 0x0 eSD storage device found! LOADER: 0x0 eSD storage device found! IMG SIZE: 0x200000 NOAHOS UPDATE UTILITIES LOADED OK!
================================================= NOAHOS-V2 BOOTING... BOOT FROM INAND ================================================= OSC = 24MHz PLL = 384MHz CCLK = 384MHz HCLK = 128MHz PCLK = 128MHz MCLK = 128MHz SDRAM size: 64MB NOAHOS IMG SIZE = 0x83c000 Time slice is 100 msec Device init: CLOCK IRQ22 attached! priority=10 Device init: UART IRQ8 attached! priority=10 Device init: LCDC LCDC: 34MHz key : 0x1b Device init: NANDC Device init: INAND Device init: SDC Device init: RTC ---BootType: 0x0 REG_RTC_RGR=0x81007fff, RSR=0x69fbdd64 IRQ6 attached! priority=8 RTC reset time ----RTC: 2010-06-01 12:00:00---- Device init: KEYBOARD Device init: TOUCH IRQ18 attached! priority=5 Device init: WATCHDOG Device init: INDICATOR Device init: POWER-MAN IRQ206 attached! priority=0 IRQ182 attached! priority=0 Device init: MEMORY Device init: USB-SLAVE IRQ27 attached! priority=7 Device init: CODEC Kernel schedule start GUI inited ok key : 0x1b DRV> eSD iNAND DRV> Use 4-bit bus width DRV> INAND: 8192M Logo package size:1777640 KERNEL: Create app: kernel Bios Utility Menu 60323, 231715, 119590 BIOS GUI: 2, User Mode <devcheck><4>DevCheck.c(399) : minute:0, AutoOff disable <devcheck><4>DevCheck.c(536) : BackLight fade disable default_print_level:3 key : 0x1b key : 0x1b key : 0x1b key : 0x1b desk:WM_CHARGE_DO desk:WM_CHARGE_DONE key : 0x1b key : 0x1b
|
分析一下 Ghidra 找到的字符串,可以找到那些被打印到 UART 调试终端的字符串比如:
Bios Utility Menu

解码中文字符串的话,设置默认编码方式为 GBK 或者 GB2312,然后设置对应的数据区为 TerminatedCString 类型:

有些奇怪的是 Ghidra 无法找到任何直接引用这些字符串的代码,肯定有更多的代码逻辑需要分析。
很有可能这里有嵌入另一个独立的可执行数据块,内核会加载它到另外的地址然后运行,特别是这些调试输出很可疑:
1 2
| Logo package size:1777640 KERNEL: Create app: kernel
|
回到 .mop 文件,在早一些的偏移量 0x00200000 位置,这里看起来像是一个单独数据区的开始位置,我们可以看到一串 0xff 字节后面跟着有效数据:
1 2 3 4
| 001FFFC0 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................................ 001FFFE0 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................................ 00200000 E0 FF BD 27 14 00 B1 AF 10 00 B0 AF 21 88 E0 00 18 00 BF AF 20 3E 10 0C 21 80 C0 00 21 20 00 02 ...'........!....... >..!...! .. 00200020 C4 00 10 0C 21 28 20 02 CB 41 10 0C 00 00 00 00 D0 41 10 0C 21 20 40 00 0A 00 10 08 00 00 00 00 ....!( ..A.......A..! @.........
|
加载到 Ghidra,因为我还不知道正确的数值,先使用偏移量 0,能看到一些解析失败的地址引用:

根据这些解析失败的地址,很可能正确的加载地址应该是 0x00400000。
使用正确地址后 Ghidra 反汇编的结果看起来就正常了:

现在我们也可以找到引用到之前那些字符串的代码:

更多痛苦的逆向分析之后,我找到了用来进行固件更新的代码,在 0x00406990,0x00407cf4 和 0x00407448 的位置:


通过分析这些代码逻辑我搞明白了它是怎么读取 .idx 索引文件和 .mop, .mo1 等数据文件的。
它可以使用任意数量的数据文件,只需要把文件后缀改成对应的数字。
这里是比较重要的文件头数据区,所有整数数值都是无符号 32 位 little-endian 格式:

使用这些信息,我终于可以完全重建一个系统镜像。
主内核现在可以启动了,除了一个君正 Q16ADD MXU1 指令造成的 QEMU 内存错误,很好修。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| ======================================= NOAHOS-V2 BOOT LOADER ..... ======================================= RTC_HRCR: 0x0 eSD storage device found! LOADER: 0x0 eSD storage device found! IMG SIZE: 0x2ec000 NOAHOS LOADED OK!
================================================= NOAHOS-V2 BOOTING... BOOT FROM INAND ================================================= OSC = 24MHz PLL = 384MHz CCLK = 384MHz HCLK = 128MHz PCLK = 128MHz MCLK = 128MHz SDRAM size: 64MB NOAHOS IMG SIZE = 0xb26000 Time slice is 100 msec Device init: CLOCK IRQ22 attached! priority=10 Device init: UART IRQ8 attached! priority=10 Device init: LCDC LCDC: 34MHz Device init: NANDC Device init: INAND Device init: SDC Device init: RTC ---BootType: 0x0 REG_RTC_RGR=0x81007fff, RSR=0x69fbf887 IRQ6 attached! priority=8 RTC reset time ----RTC: 2010-06-01 12:00:00---- Device init: KEYBOARD Device init: TOUCH IRQ18 attached! priority=5 Device init: WATCHDOG Device init: INDICATOR Device init: POWER-MAN IRQ206 attached! priority=0 IRQ182 attached! priority=0 Device init: MEMORY Device init: USB-SLAVE IRQ27 attached! priority=7 Device init: CODEC Kernel schedule start GUI inited ok DRV> eSD iNAND DRV> Use 4-bit bus width DRV> INAND: 8192M Logo package size:104840 KERNEL: Create app: kernel
|
系统调用,系统调用,还是系统调用
分析固件升级程序的时候,我注意到它并不直接操作硬件存储外设。
它会打开一个特殊文件 '\\DEVICE\BURNFILE' 然后进行一系列系统调用 syscall 指令。
从系统调用的用法来分析,它们看起来像是标准的文件访问函数,比如 fopen,fwrite,fread,fclose。

这些系统调用函数本身并不对寄存器做任何操作,所以 Ghidra 无法分析函数的一些信息,比如传入参数的数量和返回值类型。这些函数只是单纯的产生一个 syscall 中断。

同时我也在看 LCD 屏幕输出操作的逻辑,当时我还没有搞定屏幕输出的模拟,其实我主要是想修屏幕输出的问题。
和之前一样,分析相关的屏幕字符串输出函数,主要功能也是藏在系统调用的后面。



现在我们需要返回到内核代码里来分析这些系统调用到底是在做什么操作。
系统调用会产生一个中断给内核来处理,中断系统相关的内容是在 CPU programming manual 章节 3 Exceptions 描述的。
君正的文档并不是很明确,我认为内核系统调用中断使用的模式应该是 Status.BEV == 0 以及 Cause.IV == 0,所以中断处理程序入口地址应该是 0x80000180。

返回内核代码然后在那个入口地址处反汇编,会看到中断处理程序的代码:

根据程序逻辑,系统调用处理使用了指向函数指针数组的数组。


现在我们终于可以搞明白这些系统调用具体做了什么操作。
LCD 屏幕缓存和君正 MXU 指令集
分析在屏幕上显示文字的函数逻辑还蛮复杂的,因为它支持多种文字编码方式和字库。
储存这些信息的数据结构挺复杂的。

最终我搞明白了系统调用 0x4002 的功能是把更新完的屏幕缓存输出到屏幕上。
用 QEMU 来运行代码,设置断点,单步执行汇编指令,以及查看内存内容对逆向分析帮助很大。
我也可以在 LCD 控制器模拟代码里添加调试输出来显示屏幕缓存的具体物理地址:
1 2 3 4 5 6 7 8
| 2026-05-07T23:18:15.076780Z ingenic_lcd_enable 1 2026-05-07T23:18:15.076782Z ingenic_lcd_mode 800x480 mode=4565 2026-05-07T23:18:15.077742Z ingenic_lcd_schedule 611160864 2026-05-07T23:18:15.077882Z ingenic_lcd_desc dda=0x741030 dsa=0x7fe000 fid=0xbeafbeae cmd=0x10000008 offs=0x0 pw=0x0 cnum=0x0 size=0x0 2026-05-07T23:18:15.077888Z ingenic_lcd_desc dda=0x741030 dsa=0x7fe040 fid=0xbeafbead cmd=0xbb80 offs=0x0 pw=0x0 cnum=0x0 size=0x0 2026-05-07T23:18:15.077890Z ingenic_lcd_desc dda=0x741010 dsa=0x742000 fid=0xbeafbeaf cmd=0x2ee00 offs=0x0 pw=0x0 cnum=0x0 size=0x0 2026-05-07T23:18:15.078896Z ingenic_lcd_read *0x30 = 0x2000000a 2026-05-07T23:18:15.078906Z ingenic_lcd_write *0x30 = 0x2000000a
|
在系统调用 0x4002 里,0x80001a98 的位置有一个看起来很像 memcpy 的函数:

使用 gdb 连接到 QEMU,我们可以从 CPU 寄存器 a0,a1 和 a2 里看到这个函数的传入参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| (gdb) b *0x80001a98 Breakpoint 1 at 0x80001a98 (gdb) c Continuing.
Breakpoint 1, 0x80001a98 in ?? () (gdb) i r zero at v0 v1 a0 a1 a2 a3 R0 00000000 00000000 00000000 8059e000 80742000 8059e000 00000640 80742000 t0 t1 t2 t3 t4 t5 t6 t7 R8 00000000 00000640 00000000 ffffffff ffffffff ffffffff ffffffff ffffffff s0 s1 s2 s3 s4 s5 s6 s7 R16 80742000 8059e000 00000000 00000001 00000640 805a0000 000001e0 80659800 t8 t9 k0 k1 gp sp s8 ra R24 00000000 0040122c 8083f5b0 10000403 00000000 8012b904 00000000 8003a4a0 sr lo hi bad cause pc 10000401 8009c9f8 00000030 0041890c 00800000 80001a98 fsr fir 00000000 00000000 (gdb)
|
a0 = 0x80742000 和 LCD 屏幕缓存的物理地址 dsa=0x742000 是对应的,所以这个函数确实是用来更新屏幕输出的。
有趣的是,这里有一些 Ghidra 无法理解的汇编指令,显示为 SPECIAL2。
gdb 也无法理解它们:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| B+>0x80001a98 li v0,-32 0x80001a9c addiu a0,a0,-4 0x80001aa0 and v0,a2,v0 0x80001aa4 addu a3,a0,v0 0x80001aa8 sltu v1,a0,a3 0x80001aac beqz v1,0x80001b00 0x80001ab0 addiu a1,a1,-4 0x80001ab4 .word 0x70a00454 0x80001ab8 .word 0x70a00494 0x80001abc .word 0x70a004d4 0x80001ac0 .word 0x70a00514 0x80001ac4 .word 0x70a00554 0x80001ac8 .word 0x70a00594 0x80001acc .word 0x70a005d4 0x80001ad0 .word 0x70a00614 0x80001ad4 .word 0x70800455 0x80001ad8 .word 0x70800495 0x80001adc .word 0x708004d5 0x80001ae0 .word 0x70800515 0x80001ae4 .word 0x70800555 0x80001ae8 .word 0x70800595 0x80001aec .word 0x708005d5 0x80001af0 .word 0x70800615 0x80001af4 sltu v0,a0,a3 0x80001af8 bnez v0,0x80001ab4 0x80001afc nop
|
就算是支持执行君正 MXU 指令集的 QEMU 也不支持在 in_asm 调试输出里正确显示它们:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| # qemu -d in_asm,op
---------------- IN: 0x80001ab0: addiu a1,a1,-4
OP: ld_i32 loc0,env,$0xfffffffffffffff0 brcond_i32 loc0,$0x0,lt,$L0 st8_i32 $0x1,env,$0xfffffffffffffff4
---- 0000000080001ab0 0000000000011000 0000000080001b00 add_i32 a1,a1,$0xfffffffc mov_i32 hflags,$0x10 brcond_i32 bcond,$0x0,ne,$L1 mov_i32 PC,$0x80001ab4 call lookup_tb_ptr,$0x6,$1,tmp5,env goto_ptr tmp5 set_label $L1 mov_i32 PC,$0x80001b00 call lookup_tb_ptr,$0x6,$1,tmp5,env goto_ptr tmp5 set_label $L0 exit_tb $0x7fffa42e1183
---------------- IN: 0x80001ab4: udi4 a1,zero,zero,0x11
OP: ld_i32 loc0,env,$0xfffffffffffffff0 brcond_i32 loc0,$0x0,lt,$L0 st8_i32 $0x1,env,$0xfffffffffffffff4
---- 0000000080001ab4 0000000000000000 0000000000000000 mov_i32 loc2,XCR mov_i32 loc2,$0x0 brcond_i32 loc2,$0x0,ne,$L1 mov_i32 loc3,a1 mov_i32 loc4,$0x4 add_i32 loc3,loc3,loc4 qemu_ld_i32 loc4,loc3,noat+al+leul,0 mov_i32 XR1,loc4 mov_i32 a1,loc3 set_label $L1 mov_i32 PC,$0x80001ab8 call lookup_tb_ptr,$0x6,$1,tmp7,env goto_ptr tmp7 set_label $L0 exit_tb $0x7fffa42e12c3
|
这些肯定是君正特有的 MXU 指令,我们可以尝试根据文档来手工解析。
君正关于这部分的文档写得也不是很好,但是足够用了。
1 2 3 4 5 6 7 8 9 10
| 80001ab4 54 04 a0 70 SPECIAL2 zero,a1,zero,0x11,0x14
指令字节: 01010100 00000100 10100000 01110000
修正字节序: 01110000 10100000 00000100 01010100
Major code 是 special2 011100 Ext 是 010100
|
所以,这个指令是 S32LDI 或者 S32LDIR:

从文档中的另一个表格,我们可以看到这个指令的几个参数的位数:

1 2 3 4 5 6 7
| Instruction = 01110000 10100000 00000100 01010100 Major code = special2 011100 rs = 00101 = 5 op = S32LDI s12 = 0000000001 << 2 = 4 XRa = 0001 = 1 Ext = 010100
|
现在我们知道这个指令是 S32LDI,可以从文档中查到它的功能:


所以这个指令就是从内存中读取数据存到一个 MXU 的特殊寄存器里,同时更新指针寄存器的位置。
之后还有指令比如 S32SDI 会把数据写回到目标地址里:
1
| 80001ad4 55 04 80 70 SPECIAL2 zero,a0,zero,0x11,0x15
|
所以这就是一个特别的使用君正 MXU 指令集的 memcpy 实现而已。
总之我搞明白了我 LCD 屏幕模拟的问题,只是一个简单的错误,我对于这个 4bpp 调色板 + RGB565 覆盖显示的新模式使用的屏幕缓存的地址计算写错了,一直在整个屏幕重复显示第一行像素点。
我终于可以启动 NP7000 QEMU 模拟器,看到它的显示输出:
