又偷懒了,隔了这么久没写,总算有一点点新进度了。
中断控制器
因为在早期编写的时候没有注意看数据手册,实际上树莓派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中就创建了几个匹配项:

对应设备树中的属性:
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 的功能类,需要重写initWithTargetDataAndSymbol和callFunction,我把开启核心的部分直接写在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
eretIPI
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可能跑不了。