目前越来越多的路由器、网关等设备选择使用OpenWRT进行开发,那么自然绕不开对OpenWRT一些传统的沿用。如软件层的UBUS、LUCI、UCI等还有硬件层的Flash存储的使用与布局。这里主要关注OpenWRT在flash方面的组织规格并讨论其对基于OpenWRT固件分析带来的作用。
嵌入式设备一般使用flash芯片(即闪存)做为非易失性存储器,各大厂商路由器也不例外。闪存的好处是无噪音,寻址快,低功耗。但是也有缺点,对于同一个block必须在每次写入前对整个block进行擦除。flash由不同存储原理分为NOR flash与NAND flash。NOR flash允许随机逐字节访问,因此CPU可以直接从NOR flash执行代码,对于bootloader
来说非常好,不必复制到内存就能执行。而目前市面最流行的是NAND flash,因为价格更低,但是需要专门的控制器访问,所以不能直接从flash执行代码,有时候flash中有坏块,可以通过硬件或软件来识别和去除坏块。这也是为什么Nor flash可以直接作为启动流程中的一部分。
在存储器中经常擦写的部分容易出现类似机械中的"磨损"情况,就算存储器中固定部分也会因为电磁效应出现"磨损"。也称之为Non-mechanical wear
。在openwrt中根据flash芯片与SoC连接方式,分为两种情况:一种情况是raw flash/host-managed
(或者直接称之为使用raw flash芯片);此时flash芯片直接和SoC连接。还有一种就是两者之间需要经过一个额外的控制芯片,这种情况称为FTL (Flash Translation Layer) flash/self-managed
(或者直接称之为使用FTL Flash芯片);此时这个额外的控制器主要负责flash芯片的磨损均衡
以及坏块管理
。
注:绝大多数嵌入式系统属于raw flash情况,而电脑上使用的SSD硬盘和USB几乎属于FTL Flash情况。
一般路由器中使用的Raw NOR Flash的内存较小(4 MiB – 16 MiB)并且不会出现坏块情况(error-free);而由于这种芯片error-free所以在此基础之上的系统、SquashFS、JFFS2都不需要考虑坏块管理。 因此使用堆叠技术SquashFS和JFFS2组成的OverlayFS在raw NOR flash配合使用几乎不会出差错。而对于使用Raw NAND flash(32 MiB – 256 MiB)的情况就需要考虑坏块了(由于nand flash的工艺不能保证nand的memory array在其生命周期中保持性能的可靠性,因此在生产和使用过程中会产生坏块)。一般解决方案包括:
• raw NAND flash的生产厂商必须保证某部分区域是error-free的;这部分区域往往就是BootLoader、Kernel、SquashFS存放的地方(这就是固件的主要部分了,这部分就不是flash芯片厂商出厂的东西了而是用户,因此生产厂商需要保证并且告诉用户error-free区域在哪)
• 控制固件大小
• ubifs是一个新兴的应用于mtd上的有效的文件系统。可以有效的组织flash的坏块和负载平衡,同时提高访问速度,减小内存消耗,具有日志的功能,是JFFS2的后续增强版。对于Nand flash芯片,openwrt使用UBIFS管理整个Raw NAND flash,然后在openwrt系统之上的所有文件写都会通过UBIFS
几乎所有嵌入式系统都包含raw flash芯片。并且他们并不会采用传统的MBR
或PBR
方式进行分区管理(master boot record (MBR)是存储设备中的一个特殊扇区用于记录该设备的分区情况并且可以包含可执行代码。),而是通过linux内核(有时是BootLoader)完成。方式也很简单,例定义kernel
区域起始地址为X
结束于Y
。并且用名字来寻址比直接给出起止地址更加方便。
一般来说flash的layout如下:
OverlayFS,顾名思义是一种堆叠文件系统,可以将多个目录的内容叠加到另一个目录上。OverlayFS并不直接涉及磁盘空间结构,看起来像是将多个目录的文件按照规则合并到同一个目录。且对多个源目录具体使用文件系统类型没有要求,即使各个源目录的文件系统类型不同也不影响使用。使用如下命令挂载一个OverlayFS文件系统:
mount -t overlay -o lowerdir=/lower1:/lower2,upperdir=/upper,workdir=/workoverlay /merged
上面的命令可以将"lowerdir"和"upper"指定的目录堆叠到/merged目录,"workdir"指定的工作目录要求是和"upperdir"目录同一类型文件系统的空目录。lowerdir的多层目录使用":"分隔开,其中层级关系为/lower1> /lower2。示意图如下:
在使用如上mount进行OverlayFS合并之后,遵循如下规则:
• lowerdir和upperdir两个目录存在同名文件时,lowerdir的文件将会被隐藏,用户只能看到upperdir的文件。
• lowerdir低优先级的同目录同名文件将会被隐藏。
• 如果存在同名目录,那么lowerdir和upperdir目录中的内容将会合并。
• 当用户修改mergedir中来自upperdir的数据时,数据将直接写入upperdir中原来目录中,删除文件也同理。
• 当用户修改mergedir中来自lowerdir的数据时,lowerdir中内容均不会发生任何改变。因为lowerdir是只读的,用户想修改来自lowerdir数据时,overlayfs会首先拷贝一份lowerdir中文件副本到upperdir中(这也被称作OverlayFS的copy-up特性)。后续修改或删除将会在upperdir下的副本中进行,lowerdir中原文件将会被隐藏。
• 如果某一个目录单纯来自lowerdir或者lowerdir和upperdir合并,默认无法进行rename系统调用。但是可以通过mv重命名。如果要支持rename,需要CONFIG_OVERLAY_FS_REDIRECT_DIR。
嵌入系统中(路由器中就很多)一般配合只读文件系统(SquashFS)作为lowerdir和可写文件系统(JFFS2)作为upperdir使用这一机制,效果就是似乎我们可以修改lowerdir下的文件或目录,lowerdir看上去变成了一个可读写的文件系统。
内核从一个已知的原始闪存分区(没有文件系统,可理解为裸设备)启动,之后运行的内核会扫描rootmfs这个mtd分区查找一个有效的超级块,并挂载这个SquashFS分区(这个分区包含了/etc),挂载后执行/etc/preinit
:
#!/bin/sh
# Copyright (C) 2006-2016 OpenWrt.org
# Copyright (C) 2010 Vertical Communications
[ -z "$PREINIT" ] && exec /sbin/init
export PATH="/usr/sbin:/usr/bin:/sbin:/bin"
# 类似c中的include
. /lib/functions.sh
. /lib/functions/preinit.sh
. /lib/functions/system.sh
#boot_hook_init list_name //初始化一个名字为list_name的回调列表
boot_hook_init preinit_essential
boot_hook_init preinit_main
boot_hook_init failsafe
boot_hook_init initramfs
boot_hook_init preinit_mount_root #boot_add_hook list_name cb_name 向名字为list_name的回调列表中添加一个回调函数cb_name
#从/lib/preinit/目录下按照名字顺序添加回调
for pi_source_file in /lib/preinit/*; do
. $pi_source_file
done
boot_run_hook preinit_essential
pi_mount_skip_next=false
pi_jffs2_mount_success=false
pi_failsafe_net_message=false
boot_run_hook preinit_main #boot_run_hook list_name 调用回调列表list_name里面的回调函数
openwrt在/lib/preinit/
下面存放的多数脚本是用于在preinit_main列表中添加回调如:
$ grep -rn 'preinit_main'
99_10_run_init:9:boot_hook_add preinit_main run_init
70_initramfs_test:13:boot_hook_add preinit_main initramfs_test
02_sysinfo:10:boot_hook_add preinit_main do_sysinfo_generic
02_default_set_state:7:boot_hook_add preinit_main define_default_set_state
10_indicate_preinit:154:boot_hook_add preinit_main preinit_ip
10_indicate_preinit:155:boot_hook_add preinit_main pi_indicate_preinit
40_run_failsafe_hook:17:boot_hook_add preinit_main run_failsafe_hook
81_urandom_seed:24:boot_hook_add preinit_main do_urandom_seed
80_mount_root:15:[ "$INITRAMFS" = "1" ] || boot_hook_add preinit_main do_mount_root
01_preinit_do_ramips.sh:9:boot_hook_add preinit_main do_ramips
04_handle_checksumming:56:boot_hook_add preinit_main do_checksumming_disable
50_indicate_regular_preinit:10:boot_hook_add preinit_main indicate_regular_preinit
07_set_preinit_iface_ramips:34:boot_hook_add preinit_main ramips_set_preinit_iface
30_failsafe_wait:100:boot_hook_add preinit_main failsafe_wait
这里主要关注mount操作即如下脚本:
#!/bin/sh
# Copyright (C) 2006 OpenWrt.org
# Copyright (C) 2010 Vertical Communications
do_mount_root() {
mount_root
boot_run_hook preinit_mount_root
[ -f /sysupgrade.tgz ] && {
echo "- config restore -"
cd /
tar xzf /sysupgrade.tgz
}
}
[ "$INITRAMFS" = "1" ] || boot_hook_add preinit_main do_mount_root
那么实际负责挂载的就是/sbin/mount_root
程序。相关核心源码Sources/fstools/mount_root.c (openwrt.org)如下:
26 /*
27 * Called in the early (PREINIT) stage, when we immediately need some writable
28 * filesystem.
29 */
30 static int
31 start(int argc, char *argv[1])
32 {
33 struct volume *root;
34 struct volume *data = volume_find("rootfs_data");
35 struct stat s;
36
37 {...}
53 /* There isn't extroot, so just try to mount "rootfs_data" */
54 volume_init(data);
55 switch (volume_identify(data)) {
56 case FS_NONE:
57 ULOG_WARN("no usable overlay filesystem found, using tmpfs overlay\n");
58 return ramoverlay();
59
60 case FS_DEADCODE:
61 /*
62 * Filesystem isn't ready yet and we are in the preinit, so we
63 * can't afford waiting for it. Use tmpfs for now and handle it
64 * properly in the "done" call.
65 */
66 ULOG_NOTE("jffs2 not ready yet, using temporary tmpfs overlay\n");
67 return ramoverlay();
68
69 case FS_EXT4:
70 case FS_F2FS:
71 case FS_JFFS2:
72 case FS_UBIFS:
73 mount_overlay(data); <==========
74 break;
75
76 {...}
79 }
80
81 return 0;
82 }
int mount_overlay(struct volume *v)
417 {
418 const char *overlay_mp = "/tmp/overlay";
419 {...}
431 err = overlay_mount_fs(v, overlay_mp); <==========
432 if (err)
433 return err;
434
435 /*
436 * Check for extroot config in overlay (rootfs_data) and if present then
437 * prefer it over rootfs_data.
438 */
439 if (!mount_extroot(overlay_mp)) {
440 ULOG_INFO("switched to extroot\n");
441 return 0;
442 }
443
444 {...}
459 fs_name = overlay_fs_name(volume_identify(v));
460 ULOG_INFO("switching to %s overlay\n", fs_name);
461 if (mount_move("/tmp", "", "/overlay") || fopivot("/overlay", "/rom")) { <==========
462 ULOG_ERR("switching to %s failed - fallback to ramoverlay\n", fs_name);
463 return ramoverlay();
464 }
465
466 return -1;
467 }
347 static int overlay_mount_fs(struct volume *v, const char *overlay_mp)
348 {
349 char *fstype = overlay_fs_name(volume_identify(v));
350
351 if (mkdir(overlay_mp, 0755)) {
352 ULOG_ERR("failed to mkdir /tmp/overlay: %m\n");
353 return -1;
354 }
355
356 if (mount(v->blk, overlay_mp, fstype, <==========
357 #ifdef OVL_MOUNT_FULL_ACCESS_TIME
358 MS_RELATIME,
359 #else
360 MS_NOATIME,
361 #endif
362 #ifdef OVL_MOUNT_COMPRESS_ZLIB
363 "compr=zlib"
364 #else
365 NULL
366 #endif
367 )) {
368 ULOG_ERR("failed to mount -t %s %s /tmp/overlay: %m\n",
369 fstype, v->blk);
370 return -1;
371 }
372
373 return 0;
374 }
/**
108 * fopivot - switch to overlay using passed dir as upper one
109 *
110 * @rw_root: writable directory that will be used as upper dir
111 * @ro_root: directory where old root will be put
112 */
113 int
114 fopivot(char *rw_root, char *ro_root)
115 {
116 char overlay[64], mount_options[64], upperdir[64], workdir[64], upgrade[64], upgrade_dest[64];
117 struct stat st;
118
119 if (find_filesystem("overlay")) {
120 ULOG_ERR("BUG: no suitable fs found\n");
121 return -1;
122 }
123
124 {...}
154
155 if (mount(overlay, "/mnt", "overlay", MS_NOATIME, mount_options)) {
156 ULOG_ERR("mount failed: %m, options %s\n", mount_options);
157 return -1;
158 }
159
160 return pivot("/mnt", ro_root);
161 }
63 int
64 pivot(char *new, char *old)
65 {
66 char pivotdir[64];
67 int ret;
68
69 if (mount_move("", new, "/proc"))
70 return -1;
71
72 snprintf(pivotdir, sizeof(pivotdir), "%s%s", new, old);
73
74 ret = pivot_root(new, pivotdir);
75
76 if (ret < 0) {
77 ULOG_ERR("pivot_root failed %s %s: %m\n", new, pivotdir);
78 return -1;
79 }
80
81 mount_move(old, "", "/dev");
82 mount_move(old, "", "/tmp");
83 mount_move(old, "", "/sys");
84 mount_move(old, "", "/overlay");
85
86 return 0;
87 }
代码中rootfs_data
/rootfs
和layout示意图对应起来,即rootfs_data
指的就是可写文件系统JFFS2,rootfs
指的是整个文件系统(SquashFS+JFFS2)。一般流程如下:
• volume_find("rootfs_data")找到flash中rootfs_data的位置,可识别的文件系统有:EXT4、F2FS、JFFS2、UBIFS
• 调用mount_overlay
• mount -n /proc -o noatime,--move /mnt/proc
• pivot_root /mnt /mnt/rom
• mount -n /rom/dev -o noatime,--move /dev
• mount -n /rom/tmp -o noatime,--move /tmp
• mount -n /rom/sys -o noatime,--move /sys
• mount -n /rom/overlay -o noatime,--move /overlay
• tmpfs,名字已经反映出这个文件系统的作用,是一个临时文件系统。这种文件系统没有设计负载均衡,并且是一种易失性的文件系统(也就是说设备重启后,这中文件系统中的内容将会丢失)。所以一般,/tmp将会挂载为tmpfs格式,且/var会软连接到其上。/dev位于自身的一个小型tmpfs分区。
• overlay_mount_fs:将rootfs_data文件系统挂载到/tmp/overlay
;这里squashFS中的tmp为何可写?这是因为这里初始化时会被挂载为一个tmpfs类型的文件夹
• mount_move:mount_move("/tmp", "", "/overlay"),等效于mount -n -t NULL /tmp/overlay -o noatime,--move /overlay
• fopivot:fopivot("/overlay", "/rom");首先通过/proc/filesystem确认是否支持overlay,然后mount -n -t overlayfs overlayfs:/overlay -o rw,noatime,lowerdir=/,upperdir=/overlay /mnt
初步形成OverlayFS。pivot("/mnt", "/rom")如下:
• pivot_root(new_root, put_old)系统调用改变当前进程所在mount namespace内的所有进程的root mount移到put_old,然后将new_root作为新的root mount;
总结一下:挂载完squashfs后首先找到rootfs_data的位置(flash)识别其文件系统类型,若支持调用mount_overlay进行后续操作;将rootfs_data挂载到 /tmp/overlay(tmpfs),然后/tmp/overlay迁移到/overlay节点。调用mount -n -t overlayfs overlayfs:/overlay -o rw,noatime,lowerdir=/,upperdir=/overlay /mnt
在/mnt下面构建OverlayFS,然后把一些sys、proc、dev迁移到/mnt下面并且通过系统调用pivot_root 完成文件系统切换。
Linux 内核将raw flash芯片看做MTD设备,该设备既不是块设备也不是字符设备。在上层MTD设备由可擦除块(erase-blocks)组成,一个erase-block大小可以是64 KiB, 128 KiB等;每个erase-block又以类似page的方式进行分割。每个page在写入时需要把其所在erase-block整个擦除再写入,因此称之为erase block。
MTD设备也有逻辑分区(mtdx),每个分区都起止于一个erase-block。MTD具体分区是通过内核对应的分区信息解析模块来决定(也可以是BootLoader完成分区)。在内核启动过程中分区信息可以通过下面几种方式传递:
• bootloaders可以在特定位置存放分区表信息
• 通过内核命令行发送分区信息(bootargs)
• 通过设备树
• 编译内核时在内核命令中写死分区信息(这种情况会覆盖BootLoader发送的命令)
例如:
cat /proc/mtd
dev: size erasesize name
mtd0: 00020000 00010000 "u-boot"
mtd1: 00140000 00010000 "kernel"
mtd2: 00690000 00010000 "rootfs"
mtd3: 00530000 00010000 "rootfs_data"
mtd4: 00010000 00010000 "art"
mtd5: 007d0000 00010000 "firmware"
erasesize就是erase-block的大小(00010000 == 64KB),而size指的是该mtd分区容量也是16进制。
对于使用了mtd管理flash的固件,可以针对性的根据分区传递方式寻找分区信息来解构固件:
• bootloaders可以在特定位置存放分区表信息
• 通过内核命令行发送分区信息(bootargs)
• 通过设备树
• 编译内核时在内核命令中写死分区信息(这种情况会覆盖BootLoader发送的命令)
对于BootLoader传递分区信息最常见的就是uboot的mtdparts
参数了,使用格式如下:
* From https://github.com/u-boot/u-boot/blob/master/cmd/mtdparts.c
* mtdparts=[mtdparts=]<mtd-def>[;<mtd-def>...] //可以有多个mtd-def(mtd定义)';'隔开
*
* <mtd-def> := <mtd-id>:<part-def>[,<part-def>...] //一个mtd设备组成部分,'mtd-id:part-def'必要
* <mtd-id> := unique device tag used by linux kernel to find mtd device (mtd->name) //flash设备id
* <part-def> := <size>[@<offset>][<name>][<ro-flag>] //分区定义,必须要size指定大小后面几个可选
* <size> := standard linux memsize OR '-' to denote all remaining space //单位使用标准linux memsize,'-'表示剩余所有空间
* <offset> := partition start offset within the device //分区偏移
* <name> := '(' NAME ')' //分区名
* <ro-flag> := when set to 'ro' makes partition read-only (not used, passed to kernel) //告诉内核分区只读
* Notes:
* - each <mtd-id> used in mtdparts must albo exist in 'mtddis' mapping //mtdids环境变量指定了flash硬件平台,mtd-id使用时需要存在于mtdids中
* - if the above variables are not set defaults for a given target are used
* Examples:
* //这里定义了一块flash芯片,并且只有一个mtd分区
* 1 NOR Flash, with 1 single writable partition:
* mtdids=nor0=edb7312-nor
* mtdparts=[mtdparts=]edb7312-nor:-
* //这里定义了两块flash芯片分别是有两个分区的nor flash和一个分区的nand flash
* 1 NOR Flash with 2 partitions, 1 NAND with one
* mtdids=nor0=edb7312-nor,nand0=edb7312-nand
* mtdparts=[mtdparts=]edb7312-nor:256k(ARMboot)ro,-(root);edb7312-nand:-(home)
例如下面是从某款路由器固件(基于openwrt)的内核中提取到的mtdparts参数:
console=ttyS1,57600n8 root=/dev/mtdblock6 mtdparts=raspi:320k(u-boot)ro,64k(u-boot-env),64k(Factory),64k(product_info),64k(kdump),-(firmware)
得到flash分区如下:
• mtd0:u-boot,大小320k,只读
• mtd1:u-boot-env,大小64k
• mdt2:Factory,大小64K
• mdt3:product_info,大小64K
• mtd4:kdump,64K
• mdt5:firmware,剩余空间
这里firmware包含kernel和rootfs并且在固件包(升级)中也只包含这部分,但是加载完内核后一般会在初始化文件系统的过程中继续对firmware进行分区,进一步分出'rootfs'和'rootfs_data'用来构建OverlayFS系统:
~# cat /proc/mtd
dev: size erasesize name
mtd0: 00050000 00010000 "u-boot"
mtd1: 00010000 00010000 "u-boot-env"
mtd2: 00010000 00010000 "Factory"
mtd3: 00010000 00010000 "product_info"
mtd4: 00010000 00010000 "kdump"
mtd5: 00770000 00010000 "firmware"
mtd6: 00545481 00010000 "rootfs"
mtd7: 000c0000 00010000 "rootfs_data"
• Linux overlayfs文件系统介绍-电子工程专辑 (eet-china.com)
• [OpenWrt Wiki] The OpenWrt Flash Layout
• L&Z|Iceway's Sharing and Recording.
• OpenWrt 根文件系统启动过程分析(一) - thammer - 博客园 (cnblogs.com)
• Sources/fstools/mount_root.c (openwrt.org)
• u-boot/cmd/mtdparts.c at master · u-boot/u-boot (github.com)
• MTD设备简介 (mickyching.github.io)