本文发自 http://www./blog/build-hack-synology/,转载请注明出处。 折腾硬件一直以来我都对 NAS 无感。在我看来,机械硬盘这种一通电就嘎嘎嘎响的设备早就应该退出历史舞台,在我新组装的 PC 上,直接上了双块 m2 接口的 nvme SSD。猛然想起就快要毕业了,我几年来辛苦从学校 PT 上拖下来各种视频需要打包带走才行。我把目光转到了 NUC 上,这台小巧的机器上装着一块 2TB 的 2.5 寸 SSHD 作为数据盘,连上 df 一看,发现已经住满了大姐姐。看着实验室机器中将近 10 TB 的文件,再看看干瘪的钱包,此时的我只能说句:机械硬盘真香。转身就在亚马逊上海淘了一块 10T 硬盘。 实际上我购买的是 西数的 10TB Elements 桌面硬盘,里面是一块型号为 WD100EMAZ 的氦气盘,完税到手 1565,比单独买硬盘便宜多了。我最开始的想法是 NUC 通过 USB 3.0 连接外置硬盘,将其作为扩展存储。然而硬盘还没到手就被土豪同学吐槽了:既然买了 3.5 寸盘,当然上 NAS 啊。你看我的群晖巴拉巴拉多方便。的确,NAS 是更优的解决方案。一来硬盘直接通过 SATA 线相连,传输速度比 USB 3.0 更快。二来 NAS 能够控制硬盘智能启停,更有利于减少硬盘损耗。三来就是老生常谈的问题了:低耦合,NUC 挂掉或重启时,NAS 依然能够提供服务。嗯,那就上 NAS 吧。 提起 NAS,群晖的大名就如雷贯耳了。然而网上一搜,被群晖的高昂的价格吓尿了。2000 以下的两盘位机器甚至不是 Intel 的 CPU,唯一觉得还行的采用 Intel J3455 CPU 的 DS918+,售价高达 4680。要知道 Intel 自家配备 J3455 CPU 的 NUC 才卖 800,群晖何德何能卖那么贵?只能说打扰了。这时在电工论坛上发现有人在开车:原价 5999 的暴风播酷云,不要 999,也不要 599,只需 590! 
为什么会有此等好事?乘着区块链的东风,国内很多厂商都搞出了各种“矿机”用来挖自己的山寨币。典型的有迅雷的玩客云、极路由的极路由X、暴风的暴风播酷云等等等等。这些“矿机”的典型特征就是高价低配,但由于虚拟货币的火热,不断有新韭菜入场,因此“矿机”出现了供不应求的现象,一上架就被哄抢而空。二道贩子倒手赚差价,矿老板收机器开 farm,“投资者”哄炒挖出来的山寨币,一副欣欣向荣的景象。然而,随着下半年虚拟货币市场的遇冷,各大虚拟币开始暴跌,即使是虚拟货币始祖比特币也享受到 1 年内 80% OFF 的暴跌。于是乎,挖矿的收益赶不上电费,矿老板们开始卖矿机了。 暴风播酷云,正是矿机中的一员。但区别于其他矿机,其本身具有较高的使用价值。它由知名 NAS 机箱厂商万由代工,采用了华擎的 J3455-ITX,内有金士顿 8G 内存一条,具有 4 个 SATA 口,除了可抽拉的两块 3.5 寸硬盘外,内部还能装下两块 2.5 寸硬盘。这批机器在今年 2 月左右出厂,如今矿老板将硬盘拆出单卖后,就把“外壳”放到咸鱼出掉,顺丰到付 590。配置比 DS918+ 还高,价格却还不到群晖的 1/7,要啥自行车,于是我上车了。 到货后,拆机清灰,清掉 BIOS 密码,内部塞上两块 2.5 寸的 256G SSD,两个盘位插进 2TB 的老硬盘和刚购入的 10TB 氦气盘,硬件上就折腾完了。 
折腾软件为 NAS 装什么系统呢?最省事当然是上和 NUC 一样的 Ubuntu 18.04,但 NAS 嘛,就要上 NAS 的系统。于是我折腾了 openmediavault,发现不怎么好用。正当我准备老老实实装回 Ubuntu 时,看到网上的黑群晖教程。 黑群晖,顾名思义的就是安装群晖的系统。群晖的系统镜像在其官网能够下载到,虽然它是基于 debian 的 Linux ,然而安装方式却不同于常规。这个系统镜像是没有引导的,而是需要在群晖通电后,按照流程通过浏览器上传安装。而群晖的引导是烧在主板的 ROM 上的。根据 IFIXIT 的拆解,DS918+ 的主板上带有“Flash memory with pre-loaded DSM 6.1 kernel to allow the NAS to boot before full installation of the OS”,就在下图绿框的部分: 
不愧是专业的,这块主板的体积比我的 j3455-itx 小巧得多。显然这里面存放的就是群晖的引导了。那么对于黑群晖来说,没有这个 ROM 来引导怎么办呢?没关系,我们可以 U 盘引导。这个网上有很多教程,我采用的是 nasyun 论坛上老骥伏枥的教程。前后折腾了两天,在此分享整个折腾过程的笔记。 制作启动盘这里选用 ds3617xs 的镜像。在一切开始之前,请确保用于安装系统的硬盘被清空,最后是通过重建分区表的形式格掉,并使用 msdos 分区表 - 通过 rescuecd 盘启动,运行 startx 启动图形界面
- 运行 gparted,重建分区表为 msdos,会强制格盘
- 创建两个分区,第一个占大空间,格式为 fat32。第二个大于 32M,格式为 fat16
- 上传 ds3617xs_v612b.img ,可通过新 U 盘传入也可以将其拷贝到第一个分区里
- 挂载 boot 镜像
mount ds3617xs_v612b.img /mnt ,执行安装脚本 cd /mnt 后 ./usb_inst.sh /dev/sdd2 ,将数据写到 sdd2 分区 - 挂载
/dev/sdd2 :mount /dev/sdd2 /boot - 查询
cat /sys/kernel/debug/usb/devices ,得到 U 盘的 vid 和 pid - 修改
/boot/boot/grub/grub.cfg 中的 vid 和 pid 为 U 盘的 vid 和 pid - 如果需要安装更新版本的系统,需要用最新版镜像中的 checksum.syno / grub_cksum.syno / zImage / rd.gz 替换掉
/boot/boot/grub/DS3617xs/ 中相应的文件 umount /boot ,拔出 U 盘,启动 U 盘制作完成
安装系统- 插入 U 盘,从 U 盘启动,进入 grub 后选择第一个启动黑群晖选项,看到 Booting kernel 代表系统已启动
- 使用群晖助手连接设备或路由器查看设备 ip,然后通过 http://ip:5000 访问
- 按界面上传系统镜像(pat后缀),安装,如果发现安装到一半失败,请:(1) 确保当前机器和黑群晖机器处于同一子网下 (2)将系统盘通过重建分区表的形式格掉
- 安装完后,黑群晖机器自动重启,此时记得选择从 U 盘来引导,看到 Booting kernel 代表系统已启动
- 由于系统需要初始化工作,此时继续观察上传界面的倒计时。一旦系统就绪,倒计时页面将跳转到创建账号等的初始化界面,完成初始化后,系统安装完成
硬盘自引导由于安装方式的原因,安装群晖的系统盘上没有引导,因此每次都要从 U 盘启动来引导,十分麻烦,为此希望能够 U 盘自引导。 在制作之前,需要确保之前系统是通过以上方式安装的,尤其是系统盘在安装系统前通过重建分区表的形式格掉并使用 msdos 分区表,否则可能无法硬盘自引导,出现 error file: /boot/grub/i386-pc/normal.mod not found 的问题。 - 创建 RAID Group,用系统盘创建一个类型为 BASIC 的 Group,然后在其之上创建一个 Volume,文件系统随便。Volume 初始化完成后,创建一个 Shared Folder,比如 boot,给予当前用户读写权限。
- 在 File Station 中打开该文件夹,上传 ds3617xs_v612b.img (刚刚做引导盘的那个镜像)和 disk_setboot.sh 。如果安装的系统镜像版本新于引导镜像,则将最新版引导镜像中的 checksum.syno / grub_cksum.syno / zImage / rd.gz 也上传到该目录
- Control Panel - Terminal & SNMP 中开启 ssh
- 使用群晖账号密码 ssh 登陆,通过
sudo -i 切换到 root cd 到刚刚创建的文件夹,一般为 /volume1/boot ,确保文件存在的情况下,执行: | chown root:root ds3617xs_v612b.img | | chown root:root disk_setboot.sh | | chmod 666 ds3617xs_v612b.img | | chmod 777 disk_setboot.sh |
找到系统盘设备路径(如 /dev/sdd ),执行 ./disk_setboot.sh /dev/sdd ./ds3617xs_v612b.img - 如果脚本执行成功,通过
parted --script /dev/sdd p free 检查系统盘是否新增了一个 flags 为 boot + lba 的分区,假设为 /dev/sdd4 - 挂载该设备
mount /dev/sdd4 /mnt - cd 到
/mnt/boot/grub/DS3617xs ,用刚上传的 checksum.syno / grub_cksum.syno / zImage / rd.gz 替换掉该目录下的相应文件 - 完成。拔出引导 U 盘,重启,选择从硬盘启动
思考看着黑群晖欢快地跑着,我却高兴不起来。为何老骥伏枥的方法可以引导呢?他是如何实现的? 虽然老骥伏枥在帖子里给出了一些解释,但我却看的一头雾水。根据老骥伏枥帖子中的说法,其实现的引导流程主要是这样的:硬盘引导 - 硬盘分区引导 - 群晖系统,其中硬盘分区引导使用 GRUB 。因此,在对 GRUB 引导流程进行复习,总结出 Linux启动流程:从启动到 GRUB 后,我开始了对群晖引导流程的分析。 硬盘引导流程我目前的硬盘已经按照上述流程安装了群晖的系统并做了硬盘引导。先来研究第一步:硬盘引导。 在 Legacy mode 中,存储在硬盘第一个扇区中的 MBR 用于决定在启动时磁盘中那部分的代码会被加载运行。其构成如下: - 0-445(0x1bd) :Bootstrap Code,又称 bootloader
- 446(0x1be)-509(0x1fd) :磁盘分区表(Disk Partition table),共四项,每个项 16 byte
- 510(0x1fe)-511(0x1ff) :MBR 结束标志。如果值为 0x55 0xaa,表明该设备可以用于启动,否则 BIOS 会将控制权转交给启动顺序中的下一个设备
我们把黑群晖硬盘的 MBR dump 出来看: | sudo dd if=/dev/sda of=mbr bs=512 count=1 |
dump 出来内容如下: | binss@g1:~/work$ od -x mbr | | 00000000 fa b8 00 10 8e d0 bc 00 b0 b8 00 00 8e d8 8e c0 |................| | | 00000010 fb be 00 7c bf 00 06 b9 00 02 f3 a4 ea 21 06 00 |...|.........!..| | | 00000020 00 be be 07 38 04 75 0b 83 c6 10 81 fe fe 07 75 |....8.u........u| | | 00000030 f3 eb 16 b4 02 b0 01 bb 00 7c b2 80 8a 74 01 8b |.........|...t..| | | 00000040 4c 02 cd 13 ea 00 7c 00 00 eb fe 00 00 00 00 00 |L.....|.........| | | 00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| | | * | | 000001b0 00 00 00 00 00 00 00 00 a0 0e 7b 0e 00 00 00 20 |..........{.... | | | 000001c0 21 00 fd 25 6f 36 00 08 00 00 00 ff 4b 00 00 25 |!..%o6......K..%| | | 000001d0 70 36 fd 3a bf 3b 00 07 4c 00 00 00 40 00 00 6f |p6.:.;..L...@..o| | | 000001e0 a5 4b fd fe ff ff 00 00 90 00 60 39 89 1c 80 3f |.K........`9...?| | | 000001f0 85 3b 0e 53 95 3f 00 08 8c 00 01 00 01 00 55 aa |.;.S.?........U.| | | 00000200 |
因此分区表内容为: | 00 20 21 00 fd 25 6f 36 00 08 00 00 00 ff 4b 00 | | 00 25 70 36 fd 3a bf 3b 00 07 4c 00 00 00 40 00 | | 00 6f a5 4b fd fe ff ff 00 00 90 00 60 39 89 1c | | 80 3f 85 3b 0e 53 95 3f 00 08 8c 00 01 00 01 00 |
分区表项定义如下: - 0 :flag,如果为0x80,就表示该分区是活动分区(一块硬盘上只可以有一个活动分区),可引导
- 1 :分区起始磁头号(HEAD)
- 2-3 :分区起始扇区号(SECTOR, bit 0-5) 和 起始柱面号(CYLINDER, 6-15)
- 4 :分区文件系统。0b 为 FAT32 with CHS,0e 为 FAT16B with LBA,07 为 NTFS,fd 为 LINUX RAID
- 5 :分区结束磁头号(HEAD)
- 6-7 :分区结束扇区号(SECTOR, bit 0-5) 和 结束柱面号(CYLINDER, 6-15)
- 8-11 :分区起始相对扇区号
- 12-15 :分区总的扇区数
CHS(Cylinder-Head-Sector) 和 LBA(Logical Block Addressing) 是扇区的编址方式。前者直接使用 (c,h,s) 来表示一个扇区的位置。扇区号(s)从1开始,柱面号(c)和磁头(h)从 0 开始,因此CHS编址的起始地址为 (0, 0, 1),只能寻址约 8 GB (255, 1024, 64) = 255 * 1024 * 64 sector。后者的 (c, h, s) 表示的位置为 (c * 硬盘中的磁头数目 + h) * 每条磁道上可以划分的最大的扇区数 + (s - 1),其中扇区号从 0 开始。 因此对于上述分区表,最后一项说明了它是活动分区,文件系统为 FAT16B with LBA,大小约为 32 MB(0x10001(65537) sectors) 在 parted 中解析为: | Number Start End Size Type File system Flags | | 1 1049kB 2551MB 2550MB primary raid | | 2 2551MB 4699MB 2147MB primary raid | | 4 4699MB 4732MB 33.6MB primary boot, lba | | 3 4832MB 250GB 245GB primary raid |
当 BIOS 发现该磁盘的第一个扇区以 55 aa 结束,确定它是一个 MBR,于是会执行存放在 byte 0-455 的 bootloader 。 bootloader硬盘第一个扇区 MBR 中的 bootloader 好像并非是 GRUB stage 1 的代码。经过实验,发现是 gparted 在初始化磁盘时所创建。 把它们粘到 ODA 中,架构选 16位(i8086),得到汇编如下: | .data:00000000 fa cli | | .data:00000001 b8 00 10 mov $0x1000,%ax | | .data:00000004 8e d0 mov %ax,%ss | | .data:00000006 bc 00 b0 mov $0xb000,%sp | | .data:00000009 b8 00 00 mov $0x0,%ax | | .data:0000000c 8e d8 mov %ax,%ds | | .data:0000000e 8e c0 mov %ax,%es | | .data:00000010 fb sti | | .data:00000011 be 00 7c mov $0x7c00,%si | | .data:00000014 bf 00 06 mov $0x600,%di | | .data:00000017 b9 00 02 mov $0x200,%cx | | .data:0000001a f3 a4 rep movsb %ds:(%si),%es:(%di) | | .data:0000001c ea 21 06 00 00 ljmp $0x0,$0x621 | | .data:00000021 be be 07 mov $0x7be,%si | | .data:00000024 38 04 cmp %al,(%si) | | .data:00000026 75 0b jne 0x00000033 | | .data:00000028 83 c6 10 add $0x10,%si | | .data:0000002b 81 fe fe 07 cmp $0x7fe,%si | | .data:0000002f 75 f3 jne 0x00000024 | | .data:00000031 eb 16 jmp 0x00000049 | | .data:00000033 b4 02 mov $0x2,%ah | | .data:00000035 b0 01 mov $0x1,%al | | .data:00000037 bb 00 7c mov $0x7c00,%bx | | .data:0000003a b2 80 mov $0x80,%dl | | .data:0000003c 8a 74 01 mov 0x1(%si),%dh | | .data:0000003f 8b 4c 02 mov 0x2(%si),%cx | | .data:00000042 cd 13 int $0x13 | | .data:00000044 ea 00 7c 00 00 ljmp $0x0,$0x7c00 | | .data:00000049 eb fe jmp 0x00000049 | | .data:0000004b 00 00 add %al,(%bx,%si) | | .data:0000004d 00 00 add %al,(%bx,%si) | | .data:0000004f 00 .byte 0x0 |
这里在关中断环境下设置 ss = 0x1000,sp = 0xb000,ax = ds = es = 0x0。随后通过循环将位于 0x0:0x7c00 的 512 byte 代码(也就是当前正在执行的 MBR 区域代码)拷贝到 0x0:0x600,然后跳转过去执行后续代码。检查 0x7be (MBR 的 1be,位于 MBR 分区表的第一项)地址是否为 0,如果不为 0,表示第一分区是活动分区,跳转到后续代码执行。否则将地址加 16 后比较下一个表项。如果比较完 4 个表象都没找到目标,则执行 jmp 0x00000049 自己跳转自己无限 busy loop。假设找到了活动分区,看后续代码: | .data:00000033 b4 02 mov $0x2,%ah | | .data:00000035 b0 01 mov $0x1,%al | | .data:00000037 bb 00 7c mov $0x7c00,%bx | | .data:0000003a b2 80 mov $0x80,%dl | | .data:0000003c 8a 74 01 mov 0x1(%si),%dh | | .data:0000003f 8b 4c 02 mov 0x2(%si),%cx | | .data:00000042 cd 13 int $0x13 | | .data:00000044 ea 00 7c 00 00 ljmp $0x0,$0x7c00 |
这里发送了 0x13 号中断,ah = 2 为 Read Sectors From Drive,al = 1 指定要读的扇区数为 1,Drive 为 0x80,读取位置 (c,h,s) 为活动分区表项中指定的那些,将其读取到 0x0:0x7c00。随后跳转过去执行代码。 根据文档,磁盘分区中起引导作用的第一个扇区称为 VBR(Volume Boot Record)。因此这段 MBR bootloader 代码的本质目的是找到活动分区,然后执行它的 VBR。 研究老骥伏枥提供的 disk_setboot.sh 脚本,发现其中有一行: | /mnt/grub-install --force-lba --root-directory=/mnt $1$(echo 4) |
发现是把 grub 装到第四个分区(在我这里为 /dev/sdd4 )去了。因此接下来执行的是 /dev/sdd4 第一个扇区的代码。 硬盘分区引导阶段我们把 /dev/sdd4 的第一个扇区 dump 出来看看: | 00000000 eb 48 90 6d 6b 66 73 2e 66 61 74 00 02 04 01 00 |.H.mkfs.fat.....| | | 00000010 02 00 02 00 00 f8 40 00 3f 00 ff 00 00 30 02 00 |......@.?....0..| | | 00000020 00 00 01 00 80 01 29 0c fe a6 53 20 20 20 20 20 |......)...S | | | 00000030 20 20 20 20 20 20 46 41 54 31 36 20 20 20 03 02 | FAT16 ..| | | 00000040 ff 01 00 80 4d 86 8c 00 00 08 fa 90 90 f6 c2 80 |....M...........| | | 00000050 75 02 b2 80 ea 59 7c 00 00 31 c0 8e d8 8e d0 bc |u....Y|..1......| | | 00000060 00 20 fb a0 40 7c 3c ff 74 02 88 c2 52 be 7f 7d |. ..@|<.t...R..}| | | 00000070 e8 34 01 f6 c2 80 74 54 b4 41 bb aa 55 cd 13 5a |.4....tT.A..U..Z| | | 00000080 52 72 49 81 fb 55 aa 75 43 a0 41 7c 84 c0 75 05 |RrI..U.uC.A|..u.| | | 00000090 83 e1 01 74 37 66 8b 4c 10 be 05 7c c6 44 ff 01 |...t7f.L...|.D..| | | 000000a0 66 8b 1e 44 7c c7 04 10 00 c7 44 02 01 00 66 89 |f..D|.....D...f.| | | 000000b0 5c 08 c7 44 06 00 70 66 31 c0 89 44 04 66 89 44 |\..D..pf1..D.f.D| | | 000000c0 0c b4 42 cd 13 72 05 bb 00 70 eb 7d b4 08 cd 13 |..B..r...p.}....| | | 000000d0 73 0a f6 c2 80 0f 84 ea 00 e9 8d 00 be 05 7c c6 |s.............|.| | | 000000e0 44 ff 00 66 31 c0 88 f0 40 66 89 44 04 31 d2 88 |D..f1...@f.D.1..| | | 000000f0 ca c1 e2 02 88 e8 88 f4 40 89 44 08 31 c0 88 d0 |........@.D.1...| | | 00000100 c0 e8 02 66 89 04 66 a1 44 7c 66 31 d2 66 f7 34 |...f..f.D|f1.f.4| | | 00000110 88 54 0a 66 31 d2 66 f7 74 04 88 54 0b 89 44 0c |.T.f1.f.t..T..D.| | | 00000120 3b 44 08 7d 3c 8a 54 0d c0 e2 06 8a 4c 0a fe c1 |;D.}<.T.....L...| | | 00000130 08 d1 8a 6c 0c 5a 8a 74 0b bb 00 70 8e c3 31 db |...l.Z.t...p..1.| | | 00000140 b8 01 02 cd 13 72 2a 8c c3 8e 06 48 7c 60 1e b9 |.....r*....H|`..| | | 00000150 00 01 8e db 31 f6 31 ff fc f3 a5 1f 61 ff 26 42 |....1.1.....a.&B| | | 00000160 7c be 85 7d e8 40 00 eb 0e be 8a 7d e8 38 00 eb ||..}.@.....}.8..| | | 00000170 06 be 94 7d e8 30 00 be 99 7d e8 2a 00 eb fe 47 |...}.0...}.*...G| | | 00000180 52 55 42 20 00 47 65 6f 6d 00 48 61 72 64 20 44 |RUB .Geom.Hard D| | | 00000190 69 73 6b 00 52 65 61 64 00 20 45 72 72 6f 72 00 |isk.Read. Error.| | | 000001a0 bb 01 00 b4 0e cd 10 ac 3c 00 75 f4 c3 00 00 00 |........<.u.....| | | 000001b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| | | * | | 000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.| |
这段代码就是为我们熟悉的 GRUB 2 boot.img 了,把 GRUB 2 clone 下来,对应代码位于 grub-core/boot/i386/pc/boot.S 这里的逻辑和上述的 bootloader 很像,只是容错性更强,考虑了很多 BIOS 有 bug 的情况并进行相应处理。它将 diskboot.img 拷贝到 0x0:GRUB_BOOT_MACHINE_KERNEL_ADDR 并跳转。后续流程就很常规了,和 Linux启动流程:从启动到 GRUB 分析的一样, grub.cfg 中的脚本会被解析执行。 该脚本加载了各种文件系统的驱动,设置了成吨的环境变量。随后用户将选择菜单第一项启动: | menuentry '启动DS3617xs黑群晖 6.1,2版' --unrestricted { | | set img=$prefix/DS3617xs | | savedefault | | loadlinux 3617 usb | | loadinitrd | | } |
其主要调用的几个函数也同样在 grub.cfg 中: | function loadlinux { | | set model=$1 | | set bootdev=$2 | | shift 2 | | if [ -n $vid -a -n $pid ]; then | | set usb_args='vid=$vid pid=$pid' | | fi | | eval 'set common_args=\'\$common_args_$model\'' | | eval 'set extra_args=\'\$extra_args_$model\'' | | eval 'set bootdev_args=\'\${bootdev}_args\'' | | common_add_option_ex rootdev root | | common_add_option sn | | if common_add_option mac1; then set netif_num=1; fi | | if common_add_option mac2; then set netif_num=2; fi | | if common_add_option mac3; then set netif_num=3; fi | | if common_add_option mac4; then set netif_num=4; fi | | common_add_option netif_num | | if [ -z $zImage ]; then | | set zImage=zImage | | fi | | linux $img/$zImage $common_args $bootdev_args $extra_args $@ | | } | | | | function loadinitrd { | | if [ -s $img/$info ]; then | | cat $img/$info | | fi | | showtips | | if [ -s $img/$extra_initrd ]; then | | initrd $img/rd.gz $img/$extra_initrd | | else | | initrd $img/rd.gz | | fi | | } |
loadlinux 执行的是 linux command。其参数展开后如下: | linux /boot/grub/DS3617xs/zImage root=/dev/md0 sn=A8ODN01234 mac1=0011322CA785 netif_num=1 vid=0x1234 pid=0x1234 |
因此群晖的 kernel 位于 /boot/grub/DS3617xs/zImage,而 /dev/md0 将被挂载为根目录。相比 Ubuntu 引导项的 linux command,多了 sn=A8ODN01234 mac1=0011322CA785 netif_num=1 vid=1234 pid=1234 这一串参数,估计是群晖的 kernel 在启动时需要对这些参数进行校验,猜测如果校验失败,那么就无法启动。 loadinitrd 执行的是 initrd command,GRUB 会将其传入的 initrd 文件读入到内存中,并将地址和大小填到 linux_kernel_params,这样 kernel 启动后,就知道去哪里加载 initrd 了。相比 Ubuntu 的 initrd 只有一个 initrd.img-XXX 文件,群晖有两个,分别为 /boot/grub/DS3617xs/rd.gz 和 /boot/grub/DS3617xs/extra.lzma 。将两者解压,得到目录结构。 rd.gz 是典型的 initrd : | $ tree rd -L 2 | | rd | | ├── bin -> usr/bin | | ├── dev | | │ └── net | | ├── etc | | │ ├── AHAtasks | | │ ├── VERSION | | │ ├── avahi | | │ ├── crontab | | │ ├── dhclient | | │ ├── dhcpc | | │ ├── extensionPorts | | │ ├── fstab | | │ ├── ftpusers | | │ ├── group | | │ ├── group_desc | | │ ├── host.conf | | │ ├── hosts | | │ ├── hosts.allow | | │ ├── hosts.deny | | │ ├── inetd.conf | | │ ├── mke2fs.conf | | │ ├── modules.conf | | │ ├── mtab | | │ ├── nsswitch.conf | | │ ├── passwd | | │ ├── profile | | │ ├── protocols | | │ ├── rc | | │ ├── rc.fan | | │ ├── rc.network | | │ ├── rc.network_dualhead | | │ ├── rc.network_routing | | │ ├── rc.sas | | │ ├── rc.scanusbdev | | │ ├── rc.subr | | │ ├── rc.volume | | │ ├── rc.wifi | | │ ├── resolv.conf | | │ ├── securetty | | │ ├── services | | │ ├── shadow | | │ ├── shells | | │ ├── ssl | | │ ├── synogrinst.sh | | │ ├── synoinfo.conf | | │ ├── synouser.conf | | │ ├── sysconfig | | │ ├── sysctl.conf | | │ └── termcap | | ├── etc.defaults -> etc | | ├── init -> bin/busybox | | ├── lib -> usr/lib | | ├── lib32 -> usr/lib32 | | ├── lib64 -> usr/lib | | ├── linuxrc -> bin/busybox | | ├── linuxrc.syno | | ├── mnt | | ├── proc | | ├── root | | ├── run | | │ └── lock | | ├── sbin -> usr/sbin | | ├── sys | | ├── tmp | | ├── usr | | │ ├── bin | | │ ├── lib | | │ ├── lib32 | | │ ├── lib64 -> lib | | │ ├── local | | │ ├── sbin | | │ ├── share | | │ └── syno | | ├── var | | │ ├── cache | | │ ├── crash | | │ ├── lib | | │ ├── lock -> ../run/lock | | │ ├── log | | │ ├── packages | | │ ├── run -> ../run | | │ ├── services | | │ ├── spool | | │ └── tmp | | └── volume1 |
extra.lzma 是对 initrd 中内核模块的补充,支持了更多类型的设备: | $ tree extra -L 4 | | extra | | ├── etc | | │ ├── jun.patch | | │ └── rc.modules | | ├── extra.lzma | | ├── init | | └── usr | | ├── bin | | │ └── patch | | ├── lib | | │ ├── firmware | | │ │ ├── bnx2 | | │ │ └── tigon | | │ └── modules | | │ ├── BusLogic.ko | | │ ├── alx.ko | | │ ├── ata_piix.ko | | │ ├── atl1.ko | | │ ├── atl1c.ko | | │ ├── atl1e.ko | | │ ├── ax88179_178a.ko | | │ ├── bnx2.ko | | │ ├── bnx2x.ko | | │ ├── button.ko | | │ ├── cnic.ko | | │ ├── e1000.ko | | │ ├── ehci-hcd.ko | | │ ├── ehci-pci.ko | | │ ├── ipg.ko | | │ ├── jme.ko | | │ ├── libcrc32c.ko | | │ ├── libphy.ko | | │ ├── mdio.ko | | │ ├── megaraid.ko | | │ ├── megaraid_mbox.ko | | │ ├── megaraid_mm.ko | | │ ├── megaraid_sas.ko | | │ ├── mii.ko | | │ ├── mptbase.ko | | │ ├── mptctl.ko | | │ ├── mptsas.ko | | │ ├── mptscsih.ko | | │ ├── mptspi.ko | | │ ├── netxen_nic.ko | | │ ├── ohci-hcd.ko | | │ ├── pch_gbe.ko | | │ ├── pcnet32.ko | | │ ├── ptp_pch.ko | | │ ├── qla3xxx.ko | | │ ├── qlcnic.ko | | │ ├── qlge.ko | | │ ├── r8168.ko | | │ ├── r8169.ko | | │ ├── scsi_transport_spi.ko | | │ ├── sfc.ko | | │ ├── skge.ko | | │ ├── sky2.ko | | │ ├── tg3.ko | | │ ├── uio.ko | | │ ├── usbnet.ko | | │ ├── vmw_pvscsi.ko | | │ └── vmxnet3.ko | | └── sbin | | └── modprobe |
这其中我只认识 e1000,它是虚拟化中最常见的网卡。 至此我们完成了老骥伏枥版群晖引导的分析。 总结老骥伏枥的硬盘引导法,本质上为三级加载:MBR => VBR => 群晖的 Linux kernel 。在这次分析过程中,我被 MBR 中的奇怪 bootloader 所惑,百思不得其解为什么能够引导活动分区,最后还是网上找了个反汇编工具才明白其原理。当然,这样做的目的老骥伏枥也说了: 我们做黑群晖硬盘自启动时,最不希望影响硬盘主引导分区,而希望所有的改装都放在硬盘子分区中。
有点道理,但这样做容易有坑:如果用户一开始没有使用 gparted 来重建分区表,那么 MBR 中 bootloader 的逻辑可能就不是这样的,甚至可能由于该盘之前装了 Linux 导致 bootloader 被写入为 GRUB 的 boot.img 。但如果将 VBR 中的 boot.img 写入到 MBR 中,那么根据 boot.img 的实现,其会加载位于后面的 core.img,因此前 63 个扇区我们都要改,理论上可以实现 MBR => 群晖的 Linux kernel 的加载。如果之后有时间,会尝试这样折腾下。 无论如何,非常感谢老骥伏枥提供的引导,不仅让我成功实现硬盘引导群晖,更重要的是让我把引导相关的知识复习了一遍,非常有意思。 后记自从 18 年 12 月装机完成后,至今已经平稳运行了两个多月。在我将其搬回家后,更是作为了家中的存储中枢。家中电视能够方便地通过 dlna 访问其中照片和视频,用过的都说好。 唯一的缺点就是机械硬盘太吵了,放房间里夜深人静的时候嘎嘎嘎响,于是把它搬到客厅了。 参考http://www./thread-28601-1-2.html https://thestarman./asm/mbr/GRUB.htm https://en./wiki/GNU_GRUB https://wiki./MBR_(x86) https://en./wiki/INT_13H#INT_13h_AH=02h:_Read_Sectors_From_Drive
|