RainSec 10月23日 23:43
OpenWRT Flash 存储简析
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文介绍了 OpenWRT 中 Flash 存储的相关基础知识,包括 Raw Flash 和 FTL Flash 的区别,NOR Flash 和 NAND Flash 的特性,以及 OpenWRT 中常见的文件系统布局和挂载流程。文章还探讨了如何利用 MTD 分区信息来分析基于 OpenWRT 的固件。

💾 OpenWRT 中 Flash 存储主要分为 Raw Flash 和 FTL Flash 两种类型。Raw Flash 直接与 SoC 连接,而 FTL Flash 则需要额外的控制器来进行磨损均衡和坏块管理。

🔴 NOR Flash 允许随机逐字节访问,适合作为 bootloader 的启动介质。而 NAND Flash 价格更低,但需要控制器访问,且存在坏块问题,需要额外的管理。

📊 OpenWRT 中通常使用 UBIFS 文件系统来管理 Raw NAND Flash,并配合 OverlayFS 来实现只读文件系统(SquashFS)和可写文件系统(JFFS2)的叠加。

🗺️ OpenWRT 中的 Flash 布局通常采用自定义分区方式,通过内核或 bootloader 来定义各个分区的起始地址和大小,常见的分区包括 bootloader、kernel、rootfs 和 rootfs_data 等。

🔄 OpenWRT 的文件系统挂载流程包括找到 rootfs_data 分区,挂载到 /tmp/overlay,然后通过 pivot_root 将文件系统切换到 overlayfs 格式,最终实现只读文件系统和可写文件系统的分离。

原创 邛笼石影 2023-10-31 14:19 北京

OpenWRT中的Flash简析

OpenWRT中的Flash简析

前言

  目前越来越多的路由器、网关等设备选择使用OpenWRT进行开发,那么自然绕不开对OpenWRT一些传统的沿用。如软件层的UBUS、LUCI、UCI等还有硬件层的Flash存储的使用与布局。这里主要关注OpenWRT在flash方面的组织规格并讨论其对基于OpenWRT固件分析带来的作用。

Flash相关基础

  嵌入式设备一般使用flash芯片(即闪存)做为非易失性存储器,各大厂商路由器也不例外。闪存的好处是无噪音,寻址快,低功耗。但是也有缺点,对于同一个block必须在每次写入前对整个block进行擦除。flash由不同存储原理分为NOR flash与NAND flash。NOR flash允许随机逐字节访问,因此CPU可以直接从NOR flash执行代码,对于bootloader来说非常好,不必复制到内存就能执行。而目前市面最流行的是NAND flash,因为价格更低,但是需要专门的控制器访问,所以不能直接从flash执行代码,有时候flash中有坏块,可以通过硬件或软件来识别和去除坏块。这也是为什么Nor flash可以直接作为启动流程中的一部分。

Raw Flash vs FTL 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情况。

NOR flash vs NAND 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在其生命周期中保持性能的可靠性,因此在生产和使用过程中会产生坏块)。一般解决方案包括:

Flash分区

  几乎所有嵌入式系统都包含raw flash芯片。并且他们并不会采用传统的MBRPBR方式进行分区管理(master boot record (MBR)是存储设备中的一个特殊扇区用于记录该设备的分区情况并且可以包含可执行代码。),而是通过linux内核(有时是BootLoader)完成。方式也很简单,例定义kernel区域起始地址为X结束于Y。并且用名字来寻址比直接给出起止地址更加方便。

一般来说flash的layout如下:

Linux overlayfs

  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合并之后,遵循如下规则:

  嵌入系统中(路由器中就很多)一般配合只读文件系统(SquashFS)作为lowerdir和可写文件系统(JFFS2)作为upperdir使用这一机制,效果就是似乎我们可以修改lowerdir下的文件或目录,lowerdir看上去变成了一个可读写的文件系统

openwrt文件系统挂载流程

  内核从一个已知的原始闪存分区(没有文件系统,可理解为裸设备)启动,之后运行的内核会扫描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)。一般流程如下:

  总结一下:挂载完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 完成文件系统切换。

MTD (Memory Technology Device)

  Linux 内核将raw flash芯片看做MTD设备,该设备既不是块设备也不是字符设备。在上层MTD设备由可擦除块(erase-blocks)组成,一个erase-block大小可以是64 KiB, 128 KiB等;每个erase-block又以类似page的方式进行分割。每个page在写入时需要把其所在erase-block整个擦除再写入,因此称之为erase block。

MTD 分区

  MTD设备也有逻辑分区(mtdx),每个分区都起止于一个erase-block。MTD具体分区是通过内核对应的分区信息解析模块来决定(也可以是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分区分析固件

  对于使用了mtd管理flash的固件,可以针对性的根据分区传递方式寻找分区信息来解构固件:

对于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分区如下:

这里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"

参考


阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

OpenWRT Flash 存储 UBIFS OverlayFS MTD
相关文章