Sky Watch

装 Mac 虚拟机

最近想整理一下我的各种配置文件,需要撸一个 Mac 虚拟机做测试。于是打开上古神器 VirtualBox。

要装系统首先要有安装盘。进入 Mac App Store,找到最新版的 macOS,并猛击“Get”。这样会下载一个安装系统的 app。正常安装的话你只需要运行这个 app 就可以了,我们要装虚拟机显然是不可能让 UEFI 运行这个 app 的,所以需要想办法做出个安装盘来引导。和所有 Mac app 一样,这个安装 app 其实是个伪装的目录。进入这个目录,并且进入 Contents/SharedSupport,几个 DMG 文件映入眼帘。这里的 BaseSystem.dmg 就是安装时的临时系统,是可以引导的,但是真正安装的内容在 InstallESD.dmg 里,真正可用的镜像文件应该是这两个文件组合起来。这个 app 的 Contents/Resources 里其实提供了制作镜像的工具 createinstallmedia,可以直接用。

  1. 首先创建一个空的镜像并 mount,

    hdiutil create -o install.dmg -size 8g -layout SPUD -fs HFS+J
    hdiutil attach install.dmg -noverify -mountpoint /Volumes/install
  2. 在镜像里写入安装盘,程序会自动重新 mount 写好的镜像,

    sudo "/Applications/Install macOS Mojave/Contents/Resources/createinstallmedia" --volume /Volumes/install
    hdiutil detach "/Volumes/Install macOS Mojave"
  3. 把写好的镜像文件转成 ISO。如果你在 macOS 里跑 VirtualBox 的话,是可以直接用 DMG 镜像的,原则上不需要这步,不过我没试过。

    hdiutil convert install.dmg -format UDTO -o install.cdr
    mv install.{cdr,iso}

接下来就可以创建虚拟机了,macOS 好像不支持 USB 1 的设备,所以要先装 VirtualBox 的 host extension pack。新建一个虚拟机,VRAM 要拉到头 128MB。同时新建一坨硬盘镜像文件(注意 Mojave 是小于 22GB 不给装⋯⋯)。在 Ports → USB 那里选 USB 3.0 controller,其他应该默认就好。

众所周知,macOS 是没法装在随便一台 x86 机器上,它得看到是个 Mac 机器才给装。我大 Mac 自有国情在,我大 Mac 的软件是开放的,任何硬件只要遵守我大 Mac 的法律法规都可以装⋯⋯(并没有。)所以有些额外的设置需要做,我直接抄的这里

vmname="macos"
resolution="1440x900"
# valid serial required for iCloud, iMessage. Structure: PPPYWWUUUMMM
# - Plant, Year, Week, Unique identifier, Model Whether the serial is
# valid depends on the device name and board, below:
serialnumber="NOTAVALIDSN0"
devicename="MacBookPro11,3" # personalize to match serial if desired
boardid="Mac-2BD1B31983FE1663"

VBoxManage setextradata "${vmname}" "VBoxInternal/Devices/efi/0/Config/DmiSystemProduct" "${devicename}"
VBoxManage setextradata "${vmname}" "VBoxInternal/Devices/efi/0/Config/DmiSystemVersion" "1.0"
VBoxManage setextradata "${vmname}" "VBoxInternal/Devices/efi/0/Config/DmiBoardProduct" "${boardid}"
VBoxManage setextradata "${vmname}" "VBoxInternal/Devices/smc/0/Config/DeviceKey" "ourhardworkbythesewordsguardedpleasedontsteal(c)AppleComputerInc"
VBoxManage setextradata "${vmname}" "VBoxInternal/Devices/smc/0/Config/GetKeyFromRealSMC" 1
VBoxManage setextradata "${vmname}" "VBoxInternal2/EfiGraphicsResolution" "${resolution}"
VBoxManage setextradata "${vmname}" "VBoxInternal/Devices/efi/0/Config/DmiSystemSerial" "${serialnumber}"
VBoxManage modifyvm "${vmname}" --cpuidset 00000001 000106e5 00100800 0098e3fd bfebfbff

这样就把这台虚拟机伪装成了一台 Mac⋯⋯

现在终于可以开始装系统了。把刚写好的安装盘塞到虚拟机里,开机,引导后会出现熟悉的安装界面。进入 Disk Utility,把硬盘 erase 成一个 APFS 盘。接着退出 Disk Utility,选择安装,并使用一路回车法。安装程序会显示还剩几分钟(一般是个很短的时间,我这里三分钟),并开始滚进度条。这时它其实是在把 base system 和数据写到硬盘里,并试图在硬盘上做引导(重点是「试图」)。过一会儿就会屏幕一黑,重启了。很神奇的是,重启以后你如果不管它,它就会再次跑到光盘上的安装程序那里,并让你大侠重新来过。实际上如果是个真实机器的话,硬盘引导应该已经做好,这次重启会直接从硬盘引导并继续安装的。我不知道这是 VirtualBox 还是水果的 bug,这里引导并没有做好,需要我们手动引导。

直接关机,(其实刚才重启的时候就可以直接关了。)并弹出安装光盘。到这里下载一个 UEFI 的 APFS 驱动,塞到光盘里,待会儿你就知道干什么用了。再次开机,系统会跑到这样的一个界面里:

uefi shell 2x

这就是传说中的 UEFI shell,如果你对这种突如其来像素风感到恐惧,不要慌,这玩意和以前的 GRUB shell 是一个档次的东西(好像并没有什么卵用⋯⋯)。这里显示的 FS 几几几是识别出来的文件系统,相当于已经 mount 好的分区,可以直接看文件的那种;而 BLK 几几几就是没识别出文件系统的 block device,相当于 Linux 里的 /dev/sda 什么的。 从这个 mapping table 可以看出,FS0 是硬盘里的一个分区(因为是 SATA0,还带 GPT 分区表的),很明显是安装时分出的区,专门用来引导,是 FAT 文件系统的,所以 UEFI 可以直接识别。FS1 是光盘(SATA1)。

一般情况下,UEFI 的逻辑很简单,就是从某个可以识别的分区里找到一些 efi 文件并执行。这些 efi 文件就相当于 UEFI 的可执行文件,里面可以包含各种逻辑,比如读一些 UEFI 本来不能读的文件系统,然后引导操作系统。这里 macOS 的安装程序不能引导,就是因为上一步没有把需要的 efi 文件和指令写到 FS0 里,而安装程序的 efi 在某个 APFS 的分区里,UEFI 看不到,所以就杯具了。这里只需要让 UEFI 加载 APFS 的驱动,我们就可以跑正确的 efi 文件了。在 UEFI shell 里输入这些命令:

load fs1:\EFI\drivers\apfs.efi
load fs1:\EFI\drivers\AppleUiSupport.efi
load fs1:\EFI\drivers\ApfsDriverLoader.efi
map -r

这样 mapping table 里就会刷出 APFS 的分区,安装程序的引导在 FS2 里(至少我这里是这样)。直接跑里面的 efi 程序引导:

fs2:\System\Library\CoreServices\boot.efi

这时系统就会继续刚才的安装。这次特别的慢,完事以后还会重启一次,依然会进入刚才的 UEFI shell,如果我们想进入系统的话还要再输入刚才的那些命令。有没有一劳永逸的方法呢?当然有~~ 如果在 FS0 里有个叫 startup.nsh 的文件,UEFI 就会自动执行文件里的命令,所以只要把上面的命令写到这个文件里即可。在这个文件中,UEFI 可以执行一些在 shell 里用不了的表达式,比如 for 循环,所以我们可以写一个循环来找有 boot.efi 的那个分区。不过在此之前,FS1 里的那些文件得复制到 FS0 里,这样我们就不需要那个 APFS 驱动镜像了。复制方法和一般的 bash 里是一样的,复制的命令就是 cp。接下来就可以写启动脚本了,UEFI 里贴心地自带了一个记事本,叫 edit,所以执行 edit fs0:\startup.nsh。我依然无耻地复制粘贴了这里的代码:

load fs0:\EFI\drivers\apfs.efi
load fs0:\EFI\drivers\AppleUiSupport.efi
load fs0:\EFI\drivers\ApfsDriverLoader.efi
map -r
for %a run (1 5)
  fs%a:
  "macOS Install Data\Locked Files\Boot Files\boot.efi"
  "System\Library\CoreServices\boot.efi"
endfor

写完后按 Ctrl+Q 保存退出。

直接 reset 机器,系统就会正常加载,并出现正常的图形界面让你输入 Apple ID 帐号什么。到此为止,这个虚拟机就算架好了。