警告:如果你对黑苹果(Hackintosh)没有什么兴趣,可能这篇文章并不适合你。

认识我的人基本都知道我曾经在我的神舟K610d-i7-d1的机器上装上了Mac系统。不出意外的,我把家里的台式机X79-E5-2670也装上了Mac,完善了各种驱动,并升级到了目前最新版的系统。但是从Yosemite升级来的一路,我逐渐的发现:

  1. OS X Yosemite (10.10) 最容易安装,不需要动dsdt,额外的kext极少,显卡也天然能驱动。
  2. OS X El Capitan (10.11) 也较易容易。显卡需要补丁才能驱动。
  3. macOS Sierra (10.12) 不好安装,需要改dsdt才能正常启动。显卡也需要补丁。
  4. macOS High Sierra (10.13) 极难安装。大多数朋友都会碰到AppleACPIPlatform问题。

所以,苹果系统日趋封闭是一个不争的事实。也许目前在用的10.12.6就是家里的台式机能升级到的最后一个Mac系统了。

一、神奇的Clover EFI BootLoader

折腾黑苹果(Hackintosh)的童鞋会经常和Clover EFI BootLoader(以下简称Clover)打交道。它是一个启动器,但不像Windows Boot loader那样负责引导系统内核,而是扮演者操作系统内核与BIOS之间的“中介”。

使用Clover之前的启动过程:

  1. 初始化UEFI环境
  2. 调用bootx64.efi
  3. 调用Windows boot loader
  4. 加载Windows内核

使用Clover之后:

  1. 初始化UEFI环境
  2. 调用bootx64.efi
  3. 调用Clover EFI模拟环境
  4. 调用Windows boot loader
  5. 加载Windows内核

别忘了,操作系统内核对这个机器硬件的认知是来自于Boot loader加载的DSDT表,所以Clover可以更改任何操作系统对于机器硬件的“认知”。换句话说,可以看成它改写了UEFI/BIOS。因此,它多被用于:

  • 将任何PC模拟成苹果机器,并安装Mac系统
  • 引导多系统,如Windows+Linux+Mac
  • 让不支持NVME启动的主板引导NVME固态硬盘
  • 还可以让它加载自定义的ACPI表来模拟特殊硬件
  • Mac也可以用它来更改启动界面:)

二、奇葩的AppleACPIPlatform问题

如果你还没有装好10.12,推荐你到远景论坛上面搜一下你 电脑的型号 或者 主板型号,里面八成就有别人已经装好了10.12或10.11。然后继续往下看。

我尝试用10.12的配置直接启动10.13时遇到了问题,在Clover的Boot args中添加 debug=0x100 和 keepsyms=1 这两个参数以后,看到问题出在这里:

在Backtrace中,看起来是AppleACPIPlatform出了问题。那么哪里出的问题呢?

我录下来启动界面,发现问题出在isspace函数上:

isspace?这难道不是acpia的函数么:

#define isspace(c) (AcpiGbl_Ctypes[(unsigned char)(c)] & (_ACPI_SP))

遂用Hopper disassembler反编译了一下10.13中的AppleACPIPlatform文件。发现这个isspace不是调的ACPIA,应该是苹果自己写的:

                     _isspace:
000000000001f5cf         push       rbp                                         ; CODE XREF=_AcpiUtStrtoul64+71, _AcpiUtStrtoul64+89
000000000001f5d0         mov        rbp, rsp
000000000001f5d3         lea        rax, qword [__ctype]
000000000001f5da         mov        al, byte [rdi+rax]
000000000001f5dd         and        al, 0x20
000000000001f5df         shr        al, 0x5
000000000001f5e2         pop        rbp
000000000001f5e3         ret

翻译成C语言就是这样:

int _isspace(int arg0) {
    rax = (int8_t) __ctype[arg0];
    rax = (rax & 0x20) >> 5;
    return rax;
}

功能本来应该和acpia的一样,判断一个char是不是space(0x20),但苹果写的时候没有把arg0先转换成unsigned char再从__ctype中取值。这显然是没注意到C语言有一个“signed char”陷阱。

三、C语言Signed char & unsigned char陷阱

为了验证这个陷阱,只需要一个小小的程序:

int main(){
    char a = 0x1;
    char b = 0x7f;
    char c = 0x80;
    char d = 0x98;
    printf("0x1: %02X\n",a);
    printf("0x7f: %02X\n",b);
    printf("0x80: %02X\n",c);
    printf("0x98: %02X\n",d);
    printf("Casted 0x98: %02X\n",(unsigned char)d);
}

运行结果:

./main
0x1: 01
0x7f: 7F
0x80: FFFFFF80
0x98: FFFFFF98
Casted 0x98: 98

C语言中的char默认是signed char。如果忘记转换成unsigned char的话就会导致一旦处理到高于0x7f的值时直接内存溢出。

查看了一下__ctype有256项,而程序是逐字节(Byte, 16位)读取的,所以用一个正常的两位16进制数做索引是无论如何不会导致溢出的。但是一旦碰上负char就会导致索引值极大从而溢出。

如果主板的ACPI表书写的时候所有可验证的部分都控制在了0x7f以内则不会有问题,譬如苹果的。而别的主板厂商的程序员未必这么规范了:)

四、常规的解决方法

  1. 关键的ACPI表其实不多,可以先试着在Clover中DROP掉一些ACPI表,只留下最必须的表。通常情况下,DROP掉MATS或者BGRT这些表以后问题就能得到解决。
  2. 找出是哪个表的问题以后,根本办法即用二进制编辑器或者Maciasl打开,修改掉那些不规范的字符。
  3. 现在Clover加了一个ACPI补丁Fix_Headers20000000,也可以不修改ACPI表解决这个问题。
  4. 使用10.12中的AppleACPIPlatform.kext替换。

悲伤的是,上面的三种方法均无效。我的ACPI表DROP的只剩APIC和DSDT以后,问题依旧。而替换kext这种做法,一旦升级系统或者重装系统都是一个噩梦。而且,旧版本的kext很可能随着系统升级就不能用了。

所以问题要么在APIC,要么在DSDT。

五、走投无路时,喜出望外的DSDT

经过@benimarucd的提醒,使用tonymacx86上的这篇文章中的10.13 Better Boot中的DSDT不会有AppleACPIPlatform越界问题。但是

  1. 只能屏蔽掉一个核心以后才能成功启动
  2. 用技嘉的DSDT多少还是有点“水土不服”,启动的时候可以看到一些主板的区域不存在

遂琢磨他的DSDT与我的有何不同。因为是屏蔽处理器一个核心后才能成功启动,所以先从Processor部分下手。这是他的DSDT:

这是我的DSDT:

可以看到,区别有三:

  1. 我的Processor在_SB.SCKN下,他的直接在_SB下
  2. 我的Processor少了_MAT函数
  3. 我的Processor是从C000到C01D,他的是C000到C00F

于是做了如下实验:

1. 使用他的DSDT,但Processor部分换成我的:

10.13错误依旧isspace。10.12不用屏蔽核心也可以引导了。

这样就确定了,isspace溢出和屏蔽核心引导,这两个问题的答案都在Processor部分!

2. 使用他的DSDT,Processor部分换成我的,给每一个Processor添加_MAT函数:

10.13错误依旧isspace。10.12不用屏蔽核心也可以引导。

可以看到_MAT函数添加以后没有可见的影响。

3.使用他的DSDT,Processor部分换成我的,给每一个Processor添加_MAT函数,去掉编号为CX10-CX1D的Processor:

10.13错误依旧isspace。10.12不用屏蔽核心也可以引导。

可以看到,多出来的CX10-CX1D也没有影响。

4.使用他的DSDT,Processor部分换成我的,但去掉Device(SCK0) - Device(SCK3)这些头部和多出来的Name申明、STA函数(即上图中红框的部分):

10.13和10.12均可以全核心引导。

5. 直接使用我的DSDT,只是去掉Device(SCKN)中的_STA函数:

10.13和10.12也可以全核心引导了!

到这里问题就很清晰了,这个函数

 Method (_STA, 0, NotSerialized)
 {
 Store ("CPUSCK0", CUU0)
 Store (PSTA (Zero), Local0)
 And (Local0, 0x03, Local1)
 Store (Local1, LSTA)
 Return (Local0)
 }

中存在无法识别的字符。

进一步的实验表明,造成错误的是这几条语句:

Store ("CPUSCK0", CUU0)
Store ("CPUSCK1", CUU1)
Store ("CPUSCK2", CUU2)
Store ("CPUSCK3", CUU3)

所以只需要把DSDT中的那些行删掉即可。

六、总结

完整的讨论过程见

中文版:远景论坛-10.13寨版华南X79安装AppleACPIPlatform问题分析

英文版:new possibilities for X79 AppleACPIPlatform panic

修改好的DSDT,更新了一些kext后,完整的EFI在我的github上:clover-x79-e5-2670-gtx650

现在10.13总算是装好,不知道10.14中又会遇到什么问题:)