/**
 * Filename:    stdrc.cc
 * Description: 一只腊鸡的技术成长。
 * Author:      Richard Chien
 * Powered By:  PurePress & Light
 */

#include <stdrc.hh>

rc::blog

ARMv8 内存系统学习笔记

创建于
分类:Note
标签:ARMARMv8内存模型内存序Memory OrderingCache

References

Cache coherency

Cacheability

Normal memory 可以设置为 cacheable 或 non-cacheable,可以按 inner 和 outer 分别设置。

Shareability

设置为 non-shareable 则该段内存只给一个特定的核使用,设置为 inner shareable 或 outer shareable 则可以被其它观测者访问(其它核、GPU、DMA 设备),inner 和 outer 的区别是要求 cache coherence 的范围,inner 观测者和 outer 观测者的划分是 implementation defined。

PoC & PoU

当 clean 或 invalidate cache 的时候,可以操作到特定的 cache 级别,具体地,可以到下面两个“点”:

Point of coherency(PoC):保证所有能访问内存的观测者(CPU 核、DSP、DMA 设备)能看到一个内存地址的同一份拷贝的“点”,一般是主存。

Point of unification(PoU):保证一个核的 icache、dcache、MMU(TLB)看到一个内存地址的同一份拷贝的“点”,例如 unified L2 cache 是下图中的核的 PoU,如果没有 L2 cache,则是主存。

当说“invalidate icache to PoU”的时候,是指 invalidate icache,使下次访问时从 L2 cache(PoU)读取。

PoU 的一个应用场景是:运行的时候修改自身代码之后,使用两步来刷新 cache,首先,clean dcache 到 PoU,然后 invalidate icache 到 PoU。

Memory consistency

ARMv8-A 采用弱内存模型,对 normal memory 的读写可能乱序执行,页表里可以配置为 non-reordering(可用于 device memory)。

Normal memory:RAM、Flash、ROM in physical memory,这些内存允许以弱内存序的方式访问,以提高性能。

单核单线程上连续的有依赖的 strldr 不会受到弱内存序的影响,比如:

str x0, [x2]
ldr x1, [x2]

Barriers

ISB

刷新当前 PE 的 pipeline,使该指令之后的指令需要重新从 cache 或内存读取,并且该指令之后的指令保证可以看到该指令之前的 context changing operation,具体地,包括修改 ASID、TLB 维护指令、修改任何系统寄存器。

DMB

保证所指定的 shareability domain 内的其它观测者在观测到 dmb 之后的数据访问之前观测到 dmb 之前的数据访问:

str x0, [x1]
dmb
str x2, [x3] // 如果观测者看到了这行 str,则一定也可以看到第 1 行 str

同时,dmb 还保证其后的所有数据访问指令能看到它之前的 dcache 或 unified cache 维护操作:

dc csw, x5
ldr x0, [x1] // 可能看不到 dcache clean
dmb ish
ldr x2, [x3] // 一定能看到 dcache clean

DSB

保证和 dmb 一样的内存序,但除了访存操作,还保证其它任何后续指令都能看到前面的数据访问的结果。

等待当前 PE 发起的所有 cache、TLB、分支预测维护操作对指定的 shareability domain 可见。

可用于在 sev 指令之前保证数据同步。

一个例子:

str x0, [x1] // update a translation table entry
dsb ishst // ensure write has completed
tlbi vae1is, x2 // invalidate the TLB entry for the entry that changes
dsb ish // ensure that TLB invalidation is complete
isb // synchronize context on this processor

DMB & DSB options

dmbdsb 可以通过 option 指定 barrier 约束的访存操作类型和 shareability domain:

option ordered accesses (before - after) shareability domain
oshld load - load, load - store outer shareable
oshst store - store outer shareable
osh any - any outer shareable
ishld load - load, load - store inner shareable
ishst store - store inner shareable
ish any - any inner shareable
nshld load - load, load - store non-shareable
nshst store - store non-shareable
nsh any - any non-shareable
ld load - load, load - store full system
st store - store full system
sy any - any full system

One-way barriers

  • Load-Acquire (LDAR): All loads and stores that are after an LDAR in program order, and that match the shareability domain of the target address, must be observed after the LDAR.
  • Store-Release (STLR): All loads and stores preceding an STLR that match the shareability domain of the target address must be observed before the STLR.
  • LDAXR
  • STAXR

Unlike the data barrier instructions, which take a qualifier to control which shareability domains see the effect of the barrier, the LDAR and STLR instructions use the attribute of the address accessed.

C++ & Rust memory order

Relaxed

Relaxed 原子操作只保证原子性,不保证同步语义。

// Thread 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B

// Thread 2:
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D

上面代码在 ARM 上编译后使用 strldr 指令,可能被乱序执行,有可能最终产生 r1 == r2 == 42 的结果,即 A 看到了 D,C 看到了 B。

典型的 relaxed ordering 的使用场景是简单地增加一个计数器,例如 std::shared_ptr 中的引用计数,只需要保证原子性,没有 memory order 的要求。

Release-acquire

Rel-acq 原子操作除了保证原子性,还保证使用 release 的 store 和使用 acquire 的 load 之间的同步,acquire 时必可以看到 release 之前的指令,release 时必看不到 acquire 之后的指令。

#include <thread>
#include <atomic>
#include <cassert>
#include <string>

std::atomic<std::string *> ptr;
int data;

void producer() {
    std::string *p = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}

void consumer() {
    std::string *p2;
    while (!(p2 = ptr.load(std::memory_order_acquire)))
        ;
    assert(*p2 == "Hello"); // never fires
    assert(data == 42); // never fires
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}

上面代码中,一旦 consumer 成功 load 到了 ptr 中的非空 string 指针,则它必可以看到 data = 42 这个写操作。

这段代码在 ARM 上会编译成使用 stlrldar,但其实 C++ 所定义的语义比 stlrldar 实际提供的要弱,C++ 只保证使用了 release 和 acquire 的两个线程间的同步。

典型的 rel-acq ordering 的使用场景是 mutex 或 spinlock,当释放锁的时候,释放之前的临界区的内存访问必须都保证对同时获取锁的观测者可见。

Release-consume

和 rel-acq 相似,但不保证 consume 之后的访存不会在 release 之前完成,只保证 consume 之后对 consume load 操作有依赖的指令不会被提前,也就是说 consume 之后不是临界区,而只是使用 release 之前访存的结果。

Note that currently (2/2015) no known production compilers track dependency chains: consume operations are lifted to acquire operations.

#include <thread>
#include <atomic>
#include <cassert>
#include <string>

std::atomic<std::string *> ptr;
int data;

void producer() {
    std::string *p = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}

void consumer() {
    std::string *p2;
    while (!(p2 = ptr.load(std::memory_order_consume)))
        ;
    assert(*p2 == "Hello"); // never fires: *p2 carries dependency from ptr
    assert(data == 42); // may or may not fire: data does not carry dependency from ptr
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}

上面代码中,由于 assert(data == 42) 不依赖 consume load 指令,因此有可能在 load 到非空指针之前执行,这时候不保证能看到 release store,也就不保证能看到 data = 42

Sequentially-consistent

Seq-cst ordering 和 rel-acq 保证相似的内存序,一个线程的 seq-cst load 如果看到了另一个线程的 seq-cst store,则必可以看到 store 之前的指令,并且 load 之后的指令不会被 store 之前的指令看到,同时,seq-cst 还保证每个线程看到的所有 seq-cst 指令有一个一致的 total order。

典型的使用场景是多个 producer 多个 consumer 的情况,保证多个 consumer 能看到 producer 操作的一致 total order。

#include <thread>
#include <atomic>
#include <cassert>

std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};

void write_x() {
    x.store(true, std::memory_order_seq_cst);
}

void write_y() {
    y.store(true, std::memory_order_seq_cst);
}

void read_x_then_y() {
    while (!x.load(std::memory_order_seq_cst))
        ;
    if (y.load(std::memory_order_seq_cst)) {
        ++z;
    }
}

void read_y_then_x() {
    while (!y.load(std::memory_order_seq_cst))
        ;
    if (x.load(std::memory_order_seq_cst)) {
        ++z;
    }
}

int main() {
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);
    a.join(); b.join(); c.join(); d.join();
    assert(z.load() != 0); // will never happen
}

上面的代码中,read_x_then_yread_y_then_x 不可能看到相反的 xy 的赋值顺序,所以必至少有一个执行到 ++z

Seq-cst 和其它 ordering 混用时可能出现不符合预期的结果,如下面例子中,对 thread 1 来说,A sequenced before B,但对别的线程来说,它们可能先看到 B,很迟才看到 A,于是 C 可能看到 B,得到 r1 = 1,D 看到 E,得到 r2 = 3,F 看不到 A,得到 r3 = 0

// Thread 1:
x.store(1, std::memory_order_seq_cst); // A
y.store(1, std::memory_order_release); // B

// Thread 2:
r1 = y.fetch_add(1, std::memory_order_seq_cst); // C
r2 = y.load(std::memory_order_relaxed); // D

// Thread 3:
y.store(3, std::memory_order_seq_cst); // E
r3 = x.load(std::memory_order_seq_cst); // F
ARMv8 内存系统学习笔记

使用 Tinc 组建虚拟局域网

创建于
分类:Ops
标签:TincVPNSDN网络虚拟局域网

以前曾经用过 ZeroTier 给自己多个分布在不同地方的设备组建大内网,后来用不着了,就没再折腾,前段时间又想重新组一下网,于是尝试了一下另一个同类的开源软件 Tinc。本文记录一下使用 Tinc 搭建虚拟网的关键步骤。

安装

Ubuntu/Debian 上直接 apt-get install tinc 安装,其它系统可以网上搜索,基本默认包管理器都可以直接安装。

节点结构

首先想好网络中的节点要如何相连,以三个节点、其中一个有公网 IP 为例,如下图,node2node3 需要主动连接到 node1,从而交换相关元信息,并在 node1 的辅助下建立连接。

目录结构

在每个节点上创建如下目录结构:

/etc/tinc
└── mynet
    ├── hosts
    │   ├── .
    │   └── ..
    ├── .
    ├── ..

这里 mynet 是网络的名字,可以随意。mynet 目录里创建一个 hosts 子目录。

编写配置文件和启动脚本

在三个节点上分别编写配置文件和启动脚本。

node1

/etc/tinc/mynet/tinc.conf

Name = node1
Interface = tinc # ip link 或 ifconfig 中显示的接口名,下同
Mode = switch
Cipher = aes-256-cbc
Digest = sha512

/etc/tinc/mynet/tinc-up(需可执行,以使用 ifconfig 为例):

#!/bin/sh
ifconfig $INTERFACE 172.30.0.1 netmask 255.255.255.0 # IP 根据需要设置,下同

/etc/tinc/mynet/tinc-down(需可执行,以使用 ifconfig 为例):

#!/bin/sh
ifconfig $INTERFACE down

node2

/etc/tinc/mynet/tinc.conf

Name = node2
Interface = tinc
Mode = switch
ConnectTo = node1
Cipher = aes-256-cbc
Digest = sha512

/etc/tinc/mynet/tinc-up(需可执行,以使用 iproute 为例):

#!/bin/sh
ip link set $INTERFACE up
ip addr add 172.30.0.2/24 dev $INTERFACE
ip route replace 172.30.0.0/24 via 172.30.0.1 dev $INTERFACE

/etc/tinc/mynet/tinc-down(需可执行,以使用 iproute 为例):

#!/bin/sh
ip link set $INTERFACE down

node3

基本和 node2 相同,除了 Name = node3 以及 IP 不同。

生成 RSA 密钥对

在每个节点上执行下面命令来生成节点的公私钥:

tincd -n mynet -K 4096

私钥默认保存在 /etc/tinc/mynet/rsa_key.priv,公钥在 /etc/tinc/mynet/hosts/<node-name>,这里 <node-name> 在每个节点上分别是 node1node2node3(Tinc 能够从 tinc.conf 中知道当前节点名)。

交换密钥

node2node3/etc/tinc/mynet/hosts/node2/etc/tinc/mynet/hosts/node3 拷贝到 node1 上的 /etc/tinc/mynet/hosts 中,此时 node1 目录结构如下:

/etc/tinc
└── mynet
    ├── hosts
    │   ├── node1
    │   ├── node2
    │   └── node3
    ├── rsa_key.priv
    ├── tinc.conf
    ├── tinc-down
    └── tinc-up

node1/etc/tinc/mynet/hosts/node1 拷贝到 node2node3,并在该文件开头加上一行:

Address = 1.2.3.4 # node1 的公网 IP

-----BEGIN RSA PUBLIC KEY-----
...
-----END RSA PUBLIC KEY-----

此时 node2 的目录结构如下:

/etc/tinc
└── mynet
    ├── hosts
    │   ├── node1 # 包含 node1 的 Address
    │   ├── node2
    ├── rsa_key.priv
    ├── tinc.conf
    ├── tinc-down
    └── tinc-up

node3node2 类似。

启动 Tinc

在每个节点上分别使用下面命令测试运行:

tincd -D -n mynet

该命令会在前台运行 Tinc,之后即可使用配置文件中配置的 IP 互相访问。

测试成功后可以杀掉刚刚运行的 tincd 进程,改用 systemctl 运行并开机自启动:

systemctl start tinc@mynet
systemctl enable tinc@mynet

参考资料

使用 Tinc 组建虚拟局域网

不要在 C++ 基类构造函数中创建新线程

创建于
分类:Dev
标签:C++多线程基类构造函数

这个问题源于之前的另一篇文章 移植树莓派驱动框架 Circle 到自制操作系统 所做的事情,移植 Circle 驱动框架的过程中有一步需要用我们的 OS 的用户态设施(其实就是 pthread API)实现 Circle 的 CTask 类,这个类是 Circle 中的线程抽象,移植后一开始可以工作,之后频繁出现执行到基类的虚函数的情况,debug 之后在这里总结一下,顺便也从反汇编的角度整理一下 C++ 虚函数的实现方式。

简化问题

当时遇到问题时的代码可以简化为如下(pthread 使用 std::thread 代替了,效果一样):

#include <cassert>
#include <chrono>
#include <iostream>
#include <thread>

using namespace std;
using namespace std::literals::chrono_literals;

struct Task {
    thread t;

    Task() {
        t = thread(task_entry, this);
        this_thread::sleep_for(500ms); // 这里 sleep 是为了稳定复现 data race
    }

    virtual ~Task() = default;

    virtual void run() {
        assert(false); // 不应该执行到这里
    }

    void join() {
        if (t.joinable()) t.join();
    }

    static void task_entry(Task *task) {
        cout << "before run" << endl;
        task->run();
        cout << "after run" << endl;
    }
};

struct TaskImpl : Task {
    void run() override {
        cout << "TaskImpl run!" << endl;
    }
};

int main() {
    Task *t1 = new TaskImpl;
    t1->join();
    return 0;
}

这里的基本逻辑是,在 new TaskImpl 创建一个 Task 子类时,会开一个线程来执行这个 task,具体的就是运行 run 方法。理想情况下,由于 C++ 所支持的运行期多态,Task::task_entry 拿到的 task 参数实际上是一个 TaskImpl,对它调用 run 应该会动态地分发到 TaskImpl::run,但实际上上面的代码并不能稳定工作,报错如下:

before run
Assertion failed: (false), function run, file thread.cpp, line 20.
[1]    22139 abort      ./test

因为 thread(task_entry, this) 创建的新线程被调度的时候,TaskImpl 对象可能还没创建完,于是虚函数表指针可能还没有指向预期的虚表。

多态的实现

去掉那行 sleep_for,编译之后看反汇编的结果(以 x86_64 举例),截取 Task::task_entry(Task*) 中调用 run 的部分如下:

Task::task_entry(Task*):
                  # ......
0000000100001d5f  movq  %rdi, -0x8(%rbp)
                  # ......
0000000100001d81  movq  -0x8(%rbp), %rcx
0000000100001d85  movq  (%rcx), %rdx
0000000100001d88  movq  %rcx, %rdi
0000000100001d8b  movq  %rax, -0x10(%rbp)
0000000100001d8f  callq *0x10(%rdx)
                  # ......

1d5f 这一行中,Task *task 参数在 %rdi,之后在 1d81 行被挪到了 %rcx1d85 行取该指针指向的对象的最开头 8 个字节放到 %rdx,这就是虚函数表指针;然后又把 %rcx 放到 %rdi,即为 run 方法的隐藏参数 this 指针;接着 1d8f 行调用 %rdx 指向的虚函数表的第 0x10 字节处的虚函数指针。

搞清楚了虚函数如何被调用,再看下虚函数表指针是怎么设置的,截取 TaskTaskImpl 构造函数的一部分如下:

TaskImpl::TaskImpl():
                  # ......
0000000100001bda  callq Task::Task()
0000000100001bdf  movq  0x243a(%rip), %rax
0000000100001be6  addq  $0x10, %rax
0000000100001bec  movq  -0x10(%rbp), %rcx
0000000100001bf0  movq  %rax, (%rcx)
                  # ......

Task::Task():
                  # ......
0000000100001c08  movq  %rdi, -0x8(%rbp)
0000000100001c0c  movq  -0x8(%rbp), %rax
0000000100001c10  movq  0x2401(%rip), %rcx
0000000100001c17  addq  $0x10, %rcx
0000000100001c1b  movq  %rcx, (%rax)
                  # ......
0000000100001c4c  callq std::__1::thread::thread<void (&)(Task*), Task*, void>(void (&)(Task*), Task*&&)
                  # ......

可以看到 TaskImpl::TaskImpl 首先调用了 Task::Task,后者的最开头首先设置了 this 的虚函数表指针(1c101c1b 行),这时的虚函数表指针是指向 Task 类的虚函数表;接着 Task::Task 调用 thread::thread 创建新线程,返回;回到 TaskImpl::TaskImpl,再次设置了 this 的虚函数表指针(1bdf1bf0 行),这次指向的是 TaskImpl 类的虚表。

于是很容易发现,创建新线程发生在 this 的虚表指针指向 TaskImpl 虚表之前,如果新线程很快被调度,会导致在里面调用 run 方法实际运行的是 Task::run

教训

这次的 bug 再次强调了多线程编程的易错性,尤其在 C++ 的构造函数中,一定要尽量避免多线程,如果真的要多线程,也一定不能在新创建的线程中访问正在创建的对象 this 指针。

不要在 C++ 基类构造函数中创建新线程

在 QEMU 上使用 U-Boot 启动自制内核

创建于
分类:Dev
标签:QEMUU-BootOSARMAArch64

为了简单了解 U-Boot 的使用,花了些时间尝试在 QEMU 的 arm64 virt 虚拟平台上使用 U-Boot 启动自制 OS 内核,这里记录一下过程,以便以后查阅。

准备 OS 内核

要想运行,肯定得先有个内核,于是搞了个极简的 helloworld:qemu-virt-hello,能够启用 ARM timer,然后打印 tick。写这个 helloworld 的时候是直接用 QEMU 的 -kernel 参数传入 ELF 格式的内核来测试运行的,所以其实这次用 U-Boot 运行纯属学习目的,本身并没有简化什么。

上述内核编译完成后得到 ELF 格式的 build/kernel.img 和 objcopy 后的纯二进制的 build/kernel.bin

然后需要使用 mkimage 命令(Ubuntu 上需安装 u-boot-tools 包)生成 U-Boot 能够识别的 image 文件:

mkimage -A arm64 -C none -T kernel -a 0x40000000 -e 0x40000000 -n qemu-virt-hello -d build/kernel.bin uImage

生成的 uImage 文件即所需的 image。

编译 U-Boot

使用下面命令下载和编译 U-Boot:

git clone git@github.com:u-boot/u-boot.git --depth 1
cd u-boot

make qemu_arm64_defconfig # 生成针对 QEMU virt 的 config
make -j16 CROSS_COMPILE=aarch64-linux-gnu- # 使用 CROSS_COMPILE 指定的工具链构建

完成之后 u-boot 目录下会生成一个 u-boot.bin 文件,这是 U-Boot 的可直接执行的纯二进制格式,使用下面命令检查是否可以正常进入 U-Boot:

qemu-system-aarch64 -machine virt -cpu cortex-a57 -bios u-boot.bin -nographic

进去之后会有个 autoboot 倒计时,按任意键之后会结束倒计时,到 U-Boot 命令行。

准备 Device Tree Blob

虽然按理说 U-Boot 可以不指定设备树直接启动不需要设备树的 kernel,但我这边一直不能成功,还没搞懂为什么,所以还是先准备一个设备树。

前面编写测试内核时针对的 QEMU 虚拟平台参数是 -machine virt -cpu cortex-a57 -smp 1 -m 2G,所以这里可以使用下面命令来 dump 出设备树:

qemu-system-aarch64 -machine virt,dumpdtb=virt.dtb -cpu cortex-a57 -smp 1 -m 2G -nographic

这会在当前文件夹生成 virt.dtb 文件。

构造 Flash Image

QEMU virt 平台有两个 flash 区域,分别是 0x0000_0000~0x0400_0000 和 0x0400_0000~0x0800_0000,U-Boot 本身被放在前一个 flash,我们可以通过 QEMU 参数 -drive if=pflash,format=raw,index=1,file=/path/to/flash.img 参数传入一个原始二进制格式的 image 文件来作为后一个 flash。

这里为了简单起见,使用 fallocate 和 cat 简单地把前面得到的 uImagevirt.dtb 拼在一起:

# 把 uImage 和 virt.dtb 分别扩展到 32M
fallocate -l 32M uImage
fallocate -l 32M virt.dtb

# 拼接
cat uImage virt.dtb > flash.img

运行

使用下面命令运行 QEMU 并进入 U-Boot 命令行:

qemu-system-aarch64 -nographic \
    -machine virt -cpu cortex-a57 -smp 1 -m 2G \
    -bios u-boot.bin \
    -drive if=pflash,format=raw,index=1,file=flash.img

使用 flinfo 命令可以查看 flash 信息。由于前面在制作 flash.img 时简单的拼接了 uImagevirt.dtb,因此现在 uImage 在 0x0400_0000 位置,virt.dtb 在 0x0600_0000 位置。

使用 iminfo 0x04000000 可以显示位于 0x0400_0000 的 uImage 信息,大致如下:

=> iminfo 0x04000000

## Checking Image at 04000000 ...
   Legacy image found
   Image Name:   qemu-virt-hello
   Created:      2021-02-22  15:54:06 UTC
   Image Type:   AArch64 Linux Kernel Image (uncompressed)
   Data Size:    12416 Bytes = 12.1 KiB
   Load Address: 40000000
   Entry Point:  40000000
   Verifying Checksum ... OK

使用 fdt addr 0x06000000fdt print / 可以检查设备树是否正确,大致输出如下:

=> fdt addr 0x06000000
=> fdt print /
/ {
    interrupt-parent = <0x00008001>;
    #size-cells = <0x00000002>;
...

确认无误之后,使用 bootm 0x04000000 - 0x06000000 命令即可运行内核,如下:

=> bootm 0x04000000 - 0x06000000
## Booting kernel from Legacy Image at 04000000 ...
   Image Name:   qemu-virt-hello
   Created:      2021-02-22  15:54:06 UTC
   Image Type:   AArch64 Linux Kernel Image (uncompressed)
   Data Size:    12416 Bytes = 12.1 KiB
   Load Address: 40000000
   Entry Point:  40000000
   Verifying Checksum ... OK
## Flattened Device Tree blob at 06000000
   Booting using the fdt blob at 0x6000000
   Loading Kernel Image
   Loading Device Tree to 00000000bede5000, end 00000000bede9cdb ... OK

Starting kernel ...

Booting...
...

参考资料

在 QEMU 上使用 U-Boot 启动自制内核

创建有多个分区的 img 文件并格式化

创建于
分类:Dev
标签:文件系统分区OS

在 QEMU 中运行 OS 并需要模拟从 SD 卡读取文件时,可以通过 -drive file=/path/to/sdcard.img,if=sd,format=raw 参数向虚拟机提供 SD 卡,这里记录一下制作 sdcard.img 的过程。

首先需要创建一个空的映像文件,可以使用 ddfallocate,这里以 fallocate 为例:

fallocate -l 200M sdcard.img

这里 200M 指的是这个映像文件占 200 MB,也就相当于是一个 200 MB 的 SD 卡。

然后使用 fdisk 对其进行分区,直接 fdisk sdcard.img 之后操作即可,比如这里分了两个分区:

Command (m for help): p
Disk sdcard.img: 200 MiB, 209715200 bytes, 409600 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xb11e63d6

Device      Boot  Start    End Sectors  Size Id Type
sdcard.img1        2048 206847  204800  100M  c W95 FAT32 (LBA)
sdcard.img2      206848 409599  202752   99M 83 Linux

下一步就是要格式化成期望的文件系统,在这之前需要先把 sdcard.img 虚拟成一个块设备,通过如下命令做到:

losetup --partscan --show --find sdcard.img

成功后会输出一个设备路径,比如 /dev/loop0,与此同时还有两个分区 /dev/loop0p1/dev/loop0p2。之后使用形如下面的命令格式化分区:

mkfs.fat -F32 /dev/loop0p1

然后移除 loop 设备:

losetup -d /dev/loop0

到这里 sdcard.img 就制作完成了。

参考资料

创建有多个分区的 img 文件并格式化