又偷懒了,隔了这么久没写,总算有一点点新进度了。

中断控制器

因为在早期编写的时候没有注意看数据手册,实际上树莓派3B所使用的中断控制器有两种,一个在每个CPU核心上,负责接收本地定时器、PMU、Mailbox等的中断,而另一种是一个全局的中断控制器,负责接收GPU外设的中断。因为没有对核心上的中断控制器进行处理,所以也就没有接收到对应的SD中断。

触发类型

修改了Kext并启动内核,看到设备成功地触发了中断,但是却一直在触发,并且目标设备的处理函数也被调用了多次。查询了IOInterruptEventSource的源码之后,发现该类在注册中断的时候会判断中断的触发类型,分别是边沿触发和电平触发,而在电平触发类型的中断中,该类在中断处理的时候会先软禁用这个中断。而在中断控制器处理中断的时候,需要判断中断是否软禁用,这样就可以避免设备驱动中的中断处理函数被多次调用。

SD驱动

中断处理函数被调用了,但是现在又有了一个新问题,因为数据读取是异步的,所以我需要为命令处理添加一个检测,需要上一次的数据读取结束之后才能继续下一个指令。SD驱动很多部分参考了这个驱动:acidanthera/EmeraldSDHC: SD host controller support for macOS

DMA传输

初步编写DMA传输部分之后,驱动并没有去读取SDDATA的数据,查了之后才知道BCM2837有两种地址,一个是VC总线地址(0x7E000000),另一个是ARM物理地址(0x3F000000),我是把物理地址传给DMA读取了,DMA读外设需要传递VC总线地址才行,所以需要将物理地址转换过去。

重新加载Kext并启动内核之后,可以看到DMA驱动能传输SD卡数据了。但是当内核试图去读取HFS分区的时候,提示读取到了错误的数据:

hfs_swap_BTNode: offset #0 invalid (0x0000) (blockSize 0x2000 numRecords 69)

经过调试发现,原来是IOMemoryDescriptor分配的物理地址并不完全是连续的,遇到跨页的数据之后就出这个BUG了。所以需要通过gen32IOVMSegments来依次获取物理段,分开写成DMA控制块。

大概顺序:请求读取->发送CMD18->发送DMA->接收完成(回调)->完成CMD18->完成DMA->发送CMD12

成功启动后,发现launchd开始试图加载服务了:

因为开了DMA读取速度也很慢,我没有加载dyld_cache,所以好多系统程序崩溃了。

启动Bash

因为是从SD启动的,launchd会从xpcd_cache.dylib加载守护进程列表信息,所以我需要自定义一个xpcd_cache来让系统默认加载一个bash并且先不启动其它服务,首先需要导出plist文件(位于TEXT.xpcd_cache):

cp rootmnt/System/Library/Caches/com.apple.xpcd/xpcd_cache.dylib ./

jtool2 -e TEXT.xpcd_cache xpcd_cache.dylib

然后,我通过修改导出的plist再重新编译成了一个xpcd_cache.dylib(使用cctools-port),函数就一个__xpcd_cache:

int __xpcd_cache(void)
{
        return 1;
}

plist的修改参考了这个:

Adding binaries to the restored system · TrungNguyen1909/qemu-t8030 Wiki

编译命令:

arm-apple-darwin18.7.0-clang -arch arm64 xpcd_cache.c -o xpcd_cache.dyld -Wl,-dylib  -Wl,-install_name,/System/Library/Caches/com.apple.xpcd/xpcd_cache.dylib -Wl,-sectcreate,__TEXT,__xpcd_cache,cache.plist -Wl,-e,0 -Wl,-exported_symbol,___xpcd_cache -Wl,-dead_strip -miphoneos-version-min=12.4 -Wl,-undefined,error -Wl,-mark_dead_strippable_dylib

修改过后,看到bash成功启动了:

使用uname -a可以看到内核信息:

我测试了一下,用dd命令加载dyld_cache测速,目前大概600KB/s左右(时间不太对):

使用neofetch显示设备信息,因为在串口输出的所以会有内核输出干扰:

多核心

重新添加了新的驱动之后,我尝试性地在设备树中开启其它3个核心,结果是驱动在申请注册中断时被迫阻塞了。经过调试发现,是因为添加的核心并没有被初始化:

iokit/Kernel/IOCPU.cpp:IOCPUInterruptController::registerInterrupt:

    IOTakeLock(vectors[0].interruptLock);
	if (enabledCPUs != numCPUs) {
		assert_wait(this, THREAD_UNINT);
		IOUnlock(vectors[0].interruptLock);
		thread_block(THREAD_CONTINUE_NULL);
	} else {
		IOUnlock(vectors[0].interruptLock);
	}

然后才发现,那3个核心还在固件那等待,需要给相应地址传递入口地址才能继续。

启动核心

通过分析AppleARMPlatform扩展发现了AppleARMFunction类,可以通过与设备树的属性相匹配来实现特定的功能,在AppleARMCPU中就创建了几个匹配项:

屏幕截图 2025-07-16 094015.png

对应设备树中的属性:

device-tree:
   +--cpus:
|  |  +--#address-cells 4 bytes: (nul 0x01 0x00 0x00 0x00
|  |  +--#size-cells 4 bytes: (nul 0x00 0x00 0x00 0x00
|  |  +--name 5 bytes: cpus
|  |  +--AAPL,phandle 4 bytes: (nul 0x08 0x00 0x00 0x00
      +--cpu0:
......
|  |  |  +--function-enable_core 12 bytes: (null) 0x13 0x00 0x00 0x00 0x65 0x72 0x6f 0x43 0x01 0x00 0x00 0x00
|  |  |  +--function-ipi_dispatch 12 bytes: (null) 0x11 0x00 0x00 0x00 0x44 0x49 0x50 0x49 0xc0 0x00 0x00 0x00
|  |  |  +--function-ipi_dispatch_other 12 bytes: (null) 0x11 0x00 0x00 0x00 0x44 0x49 0x50 0x49 0xc1 0x00 0x00 0x00
|  |  |  +--function-cpu_idle 8 bytes: (null) 0x13 0x00 0x00 0x00 0x49 0x75 0x70 0x63

根据代码逻辑可以将属性格式理解为:

属性 = <&功能父级设备 功能名(16进制) 若干参数>;

原参考设备的function-enable_core是由PMGR进行处理的,但是树莓派3B并没有这个,所以我就先将父级设备指向了arm-io,并且使用第三个参数存放release-address

function-enable_core = <&armio 0x436f7265 0x000000f0>;

另外,处理设备的驱动中在初始化时还需要调用AppleARMFunction::registerFunctionParent来注册父级:

AppleARMFunction::registerFunctionParent(provider, this);

接着,需要实现一个继承AppleARMFunction 的功能类,需要重写initWithTargetDataAndSymbolcallFunction,我把开启核心的部分直接写在callFunction里面了,通过将启动函数(LowResetVectorBase)的地址写入每个CPU的固定地址中即可。

当驱动调用AppleARMFunction::withProvider创建功能之后,会调用父级设备的callPlatformFunction来获取对应的功能对象,通过参数3来判断设备树中填写的功能名,我简单地仿照了一下:

    // 功能名称需要为newAppleARMFunction
    if (functionName == gAppleARMFunctionNew) {
        OSData* param3Data = OSDynamicCast(OSData, (OSObject*)param3);
        if (!param3Data)
            return kIOReturnBadArgument;
        
        UInt32 functionValue = ((UInt32*)param3Data->getBytesNoCopy())[1];
        AppleARMFunction* newFunction = nullptr;
        switch (functionValue) {
        case 'Core':
            // 功能为开启核心
            IOLog("BCM2837IO: Enable CPU Core\n");
            newFunction = new BCM2837FunctionEnableCPUCore();
            break;
        default:
            return kIOReturnUnsupported;
            break;
        }
        if(newFunction) {
            // 参数4为返回的功能对象
            *((AppleARMFunction **)param4) = newFunction;
            return kIOReturnSuccess;
        }
        return kIOReturnUnsupported;
    }

另外,在设备树中不需要function-cpu_idle属性,因为在CPU休眠的时候暂时还没有特别操作需要做,而且这个属性可以不要。

异常等级

当我重新添加新的kext之后,在调试中可以看到其它CPU核心开始去执行我指定的启动函数了,但是执行到arm_init_tramp 函数的时候,却没有成功执行。经过排查发现是因为在启动的时候每个CPU核心都默认在EL2,而操作系统运行在EL1,运行到arm_init_tramp 函数时已经使用虚拟地址了。为了能让核心运行在EL1模式,我就直接在LowResetVectorBase 上添加了一个简单的切换函数:

LEXT(LowResetVectorBase)

	// Switch ELx
	bl		switchEL
......
switchEL:
	mrs 	x0, CurrentEL
	cmp 	x0, #(PSR64_MODE_EL2)
	beq 	switchEL1
	ret
switchEL1:
	msr		ELR_EL2, lr

	// Switch EL1 to aarch64
	mov		x0, #(HCR_RW)
	msr		HCR_EL2, x0
	
	// Close MMU
	msr		SCTLR_EL1, xzr

    // Set spsr to el1h
	mov 	x1, #(DAIF_ALL | PSR64_MODE_EL1 | PSR64_MODE_SPX)
	msr		SPSR_EL2, x1

	eret

IPI

ARM核心之间需要通信时,需要通过发送IPI来通知目标核心。根据外设手册,树莓派3B所使用的BCM2837(同BCM2836)的IPI通过本地中断控制器的Mailbox发送,根据上面的设备树属性我这么写了:

function-ipi_dispatch = <&local_intc 0x49504944 10>;
function-ipi_dispatch_other = <&local_intc 0x49504944 0xF 0>;

function-ipi_dispatch用来给当前CPU进行IPI,function-ipi_dispatch_other用来给其它CPU进行IPI,我在第三个值处做了区分。四个核心均使用Mailbox0来发送IPI。

另外,在内核调度中会有个延迟IPI的接口,但是树莓派3B并没有这个,所以我还是把它关掉了(config_sched_deferred_ast)。

问题和想法

  • 现在内核的timebase频率值还不对,会导致时间速度快了。

  • 把timebase频率传递为机器值(qemu为62.50MHz)之后系统会在检测完分区之后卡住。

  • 因为没有使用Dyld缓存导致很多库没有,后续打算造轮子写一些(?)

  • dd读取Dyld好像导致内存炸了,我可能打算看看iPhone5s的Dyld缓存

  • 后续可能考虑在实体开发板上跑(可能不会是3B,内存太小了)。

  • A72/A53核心不支持16KB页表,所以原生Dyld cache可能跑不了。