本文发自 http://www.binss.me/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 盘启动来引导,十分麻烦,为此希望能够从硬盘自引导。
在制作之前,需要确保之前系统是通过以上方式安装的,尤其是系统盘在安装系统前通过重建分区表的形式格掉并使用 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 盘,重启,选择从硬盘启动
2020.03.11 更新
由于时隔已久,找不到 disk_setboot.sh 的原帖出处了,我在本地保存了一份,其内容如下:
#!/bin/sh
#
# The purpose of the script is to install the bootloader on
# a harddrive for Synology can be started from hard disk
#
#help screen
if [ $# != 2 ]; then
echo "
standard use of script is:
./disk_setboot.sh the script will install bootloader on hard disk, it will not
touch the partition tables and therefore please perserves data.
possible options are:
/dev/sd? device name for Synology bootloader disk.
/*/boot.img path to the bootloader that will be written to the disk.
example
./disk_setboot.sh /dev/sd? /volunm1/boot/boot612b.img
"
exit 1
fi
#check if the bootloader image exist
if [ ! -f $2 ]; then
echo "The bootloader image $2 does not exists."
exit 1
fi
#check if the device exist
if [ ! -e $1 ]; then
echo "$1 does not exist."
exit 1
fi
if [ -e $1$(echo 4) ]; then
part_data=$(parted --script $1$(echo 4) unit s p | grep "fat16")
free_size=$(echo $part_data |cut -d's' -f3)
if [ "$free_size" -lt 65536 ]; then
echo "The device free size is too small for bootloader"
exit 1
fi
dd if=$2 of=$1$(echo 4)
sync
sleep 1
mount $1$(echo 4) /mnt
if [ -x /mnt/grub ]; then
LD_LIBRARY_PATH=/mnt
export LD_LIBRARY_PATH
/mnt/grub-install --force-lba --root-directory=/mnt $1$(echo 4)
else
echo "The boot img is not correct."
exit 1
fi
umount /mnt
echo "Update hard disk bootloader successful"
exit 0
fi
#parted --script $1 unit s p free | grep "Free Space"
#get the partition data
part_data=$(parted --script $1 unit s p free | grep "Free Space")
part_data=$(echo $part_data | sed -e 's/Free Space/;/g')
part_data=$(echo $part_data |cut -d';' -f2)
start_at=$(echo $part_data |cut -d's' -f1)
free_size=$(echo $part_data |cut -d's' -f3)
start_at=$((start_at+2048-start_at%2048))
end_at=$((start_at+65536))
if [ "$free_size" -lt 65536 ]; then
echo "The device free size is too small for bootloader: {$((free_size*512))} bytes."
exit 1
fi
parted $1 unit s mkpart primary fat16 $start_at$(echo s) $end_at$(echo s)
if [ $? != 0 ]; then
echo "parted hard disk failure."
exit 1
fi
parted $1 set 4 boot on
dd if=$2 of=$1$(echo 4)
sync
sleep 1
mount $1$(echo 4) /mnt
if [ -x /mnt/grub ]; then
LD_LIBRARY_PATH=/mnt
export LD_LIBRARY_PATH
/mnt/grub-install --force-lba --root-directory=/mnt $1$(echo 4)
else
echo "The boot img is not correct."
exit 1
fi
umount /mnt
echo "Generate hard disk bootloader successful"
exit 0
思考
看着黑群晖欢快地跑着,我却高兴不起来。为何老骥伏枥的方法可以引导呢?他是如何实现的?
虽然老骥伏枥在帖子里给出了一些解释,但我却看的一头雾水。根据老骥伏枥帖子中的说法,其实现的引导流程主要是这样的:硬盘引导 - 硬盘分区引导 - 群晖系统,其中硬盘分区引导使用 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.:.;[email protected]|
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 |[email protected]..|
000000f0 ca c1 e2 02 88 e8 88 f4 40 89 44 08 31 c0 88 d0 |[email protected]...|
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.nasyun.com/thread-28601-1-2.html
https://thestarman.pcministry.com/asm/mbr/GRUB.htm
https://en.wikipedia.org/wiki/GNU_GRUB
https://wiki.osdev.org/MBR_(x86)
https://en.wikipedia.org/wiki/INT_13H#INT_13h_AH=02h:_Read_Sectors_From_Drive
1F Wong arrhenius 5 years, 11 months ago 回复
强,大佬威武
2F 八翼天使 5 years, 10 months ago 回复
膜拜大神 希望我以后也能跟着教程做完还能研究明白其中的道理
3F Sean 5 years, 9 months ago 回复
赞!
请教下:楼主的方案和“老骥伏枥的方法”的区别在哪儿?
另外,楼主说“我却高兴不起来”,但感觉楼主没遇到什么卡壳好像?
4F binss MOD 5 years, 9 months ago 回复
回复 [3F] Sean:我使用的方案就是 老骥伏枥 的方案。我高兴不起来是因为知其然而不知其所以然,所以文章的后半部分都在探究该方法的引导过程。
5F mjc0608 5 years, 9 months ago 回复
这tm是在下见过最硬核的黑群晖
6F april 5 years, 8 months ago 回复
厉害了,老铁
7F 拿铁 5 years, 7 months ago 回复
我对硬盘引导不感兴趣,但非常希望能有稳定的优盘引导镜像。目前1.04b使用6.2.2还是存在不小问题,虽然目前也能够重启和关机了,但开机时间过长也是个大问题。
我是按照这个教程安装的。https://hegu.app/archives/299
8F 随风 5 years, 2 months ago 回复
set netif_num=1这个参数是神马意思呢?我ESXI6.7装黑裙6.2,直通SATA控制器,硬盘认不全,昨天参照网上资料修改了一下终于认出来了,这是修改的其中之一的参数,不太明白什么意思,也没找到注释
9F binss MOD 5 years, 1 month ago 回复
回复 [8F] 随风:网卡个数
10F 时光 4 years, 11 months ago 回复
看你提到的参考贴是老骥伏枥关于群晖5.X硬盘启动的。不是说6.X不能用这种方式吗?想按你的步骤装到硬盘,但是不知道该不改用这个5.x教程中的disk_setboot.sh。能否贴一下全部内容?
11F binss MOD 4 years, 11 months ago 回复
回复 [10F] 时光:已更新
12F 时光 4 years, 11 months ago 回复
"如果安装的系统镜像版本新于引导镜像,则将最新版引导镜像中的 checksum.syno / grub_cksum.syno / zImage / rd.gz 也上传到该目录",但是我找了无论是老骥伏枥的boot61-15047.img里还是我当前安装的这个6.1.7的启动img文件里都没有checksum.syno / grub_cksum.syno 这两个文件。
13F 时光 4 years, 11 months ago 回复
我从前天开始折腾这个小瘦客户机装群晖,搜了无数帖子。你这个是唯一一个6.X版本一步一步操作下来,且关键步骤没有缺失的,其他人的都是要求至少硬盘16G,使用二合一免U镜像,要不然就是必须使用自己的专用IMG文件,却又不给提供下载。感谢你~~如果可能,能否提供一下你使用的这个IMG文件。折腾服气了,现在是系统只要是6.X,且能免U启动就好。强迫症真可怕,昨天刷了不下二十次机,折腾到凌晨1点都没睡觉。感谢~~~
14F 时光 4 years, 11 months ago 回复
感谢你提供的脚本。里面有“example
./disk_setboot.sh /dev/sd? /volunm1/boot/boot612b.img”从这里就可以看出当时他做这个脚本时用的时612b。我把nasyun论坛上老的帖子过了一遍,发现他做的6.1之后就是直接做硬盘映像了,但是在“黑群晖最新版6.1.3-15152版硬盘自启动映像和ISO启动盘”这个帖子的2楼提供了这个6.1.2b的相关文件。我的硬盘太小只有8G没法使用硬盘映像安装,但愿这个有效。我去试试了,有结果再回来。再次感谢你~
15F binss MOD 4 years, 11 months ago 回复
回复 [13F] 时光:这个镜像应该就是官网下的。我已经转白裙好久了,哈哈
16F binss MOD 4 years, 11 months ago 回复
回复 [13F] 时光:记错了。文件出处可看下 http://www.nasyun.com/forum.php?mod=viewthread&tid=31655
17F 时光 4 years, 11 months ago 回复
又折腾了一天半,回到了原点。严格按照http://www.nasyun.com/forum.php?mod=viewthread&tid=31655及关联贴执行,老是一开始装DSM就报告硬盘2有问题,强制安装最后会报错,使用群晖助手安装最后也会报错,助手是最后总会报硬盘空间不够。又折腾回不知哪位高人捣鼓的6.1.7,使用U盘启动,grub没有菜单,这个安装就一切正常顺利完成。还是要从头研究如何把它改为硬盘启动,。。。。。。。。
18F 时光 4 years, 11 months ago 回复
终于能够启动了。必须相信群晖助手。明明下的6.12b的启动文件,群晖认出的却是6.1-15047,按群晖助手的安装15047就可以了。我真想说:……*……%*%……&%
19F 时光 4 years, 11 months ago 回复
最终外接一块大硬盘,使用二合一启动搞定。我看我也快往白群的方向走了
20F fiohappy 4 years, 10 months ago 回复
回复 [9F] binss:那集成2个千兆网口,又加个pcie的4口千兆intel i350-t4.
这时set netif_num=*是该设定啥?
6?24?
21F fiohappy 4 years, 10 months ago 回复
测试8,24这俩数值不对。
这个数值是十六进制的?
还是说这指的pcie通道?3通道?
22F binss MOD 4 years, 10 months ago 回复
回复 [21F] fiohappy:有几个 mac 地址就填几。根据 grub.cfg ,这个数字会自己推断,如 if common_add_option mac2; then set netif_num=2; fi
23F fiohappy 4 years, 10 months ago 回复
回复 [22F] binss:非常感谢,目前6网口都可以正常获取DHCP的地址,只是最后两个网口的mac像是随机生成的。
就变成集成网口1和pcie的3个网口是grub.cfg指定的mac,pcie4口和集成2口,是剩下的俩网口随机生成MAC。
24F fiohappy 4 years, 10 months ago 回复
0000:00:1f.6 0200: 8086:15b8
0000:01:00.0 0200: 8086:1521 (rev 01)
0000:01:00.1 0200: 8086:1521 (rev 01)
0000:01:00.2 0200: 8086:1521 (rev 01)
0000:01:00.3 0200: 8086:1521 (rev 01)
0000:03:00.0 0200: 8086:1539 (rev 03)
25F 问个问题 4 years, 10 months ago 回复
我想知道那个rd.gz的打包方式是什么?我用7zip可以在windows下解压出来,但是不知道怎么打包回去……改配置文件时发现的
26F dd 4 years, 9 months ago 回复
大神,麻烦问问,黑群晖不识别网卡怎么增加网卡驱动?
27F 海岸 4 years, 2 months ago 回复
有价值的博文,
硬盘自引导 硬盘引导流程
28F 我 3 years, 6 months ago 回复
赞一个,网上教程基本没有分析原理的,总算看明白了为什么。
29F loser 2 years, 11 months ago 回复
如ID,如果还有人在用这个教程的话,想跟大家说的是……sh文件你复制粘贴必须注意下你的编码方式,是否未linux编码,因为执行的脚本,我是在浏览器复制txt然后强行修改扩展名的。因为win与linux的空格是两个不同的指令,所以前述的方式会跑出来6行 $\r 然后98行缺失,经过百度我找到SH文件,发现了这个问题。不过现在的我还是没有成功,因为我卡在了跑sh文件后面的一步,我的sh命令失败了,是我的盘无空间No space left on device。我想我我还是去用二合一系统把……
30F Mirage 1 year, 9 months ago 回复
"......因此每次都要从 U 盘启动来引导,十分麻烦,为此希望能够 U 盘自引导",最后是否打错了?应该是"为此希望能够 硬盘自引导",而非"U盘自引导"
31F binss MOD 1 year, 9 months ago 回复
回复 [30F] Mirage:感谢指正,已修改