Project RC

一种标准阿西的设计与实现。

使用 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

/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 文件并格式化

2020 → 2021

创建于
分类:Misc
标签:年度总结

虽然现在已经是 2021 年的第二个月中旬了,却还是觉得需要补一个年度总结。而且越来越觉得,农历新年比元旦更适合用来总结过去的一年,因为往往这时候结束了过去一年的学习和工作,吃了年夜饭,看完了春晚,才觉得好像过去一年才算真的过去,身心也终于得到真正的放松。相比起来,元旦短短的假期,最多能匆匆忙忙出去吃一两顿好的,总还是缺了点“新年”的味道。

无聊的本科生活的落幕

回首 2020 年,最显然的一件事就是大学毕业,糊完了极其坑爹的杰普公司校企合作培训和毕业设计,终于可以离开那个浪费了我四年最好时光的学校。无聊的课堂教学、敷衍的课程实验、落后的课本,真的令人失望,当然了,这只能怪我自己初中和高中没有努力接受应试教育。

不过,大学期间也算是做了些积极的尝试,比如试图搞开源软件协会来促进计算机社团水平的发展,也尝试给大一新生做了 Python 相关的培训,都是一些有趣的经历。

另一方面,如果说我的大学还有什么值得怀念的话,那就只剩下自由的课余时间了吧。那个时候,可以随便翘课,整天待在宿舍学自己想学的东西,写自己想写的项目,几乎一切时间安排都是自由的,现在那样的状态已经一去不复返了。

研究生之梦的实现和趋于平淡

去年的另一件大事就是考研上岸,经历了出分前的紧张、出分后的激动、机试的完美感觉、面试前的浪、面试后的自我怀疑,最后听到海波老师在电话里的“欢迎加入 IPADS”,证明了为此付出的十个月没有白费。最终的初试+复试总成绩排名第一,似乎是很值得吹的了。

但对研究生生活的热情却逐渐变淡了。开学前的暑期培训时候还是非常有激情的,提前加入项目后更是晚上常常肝到 12 点之后,刚开学的一段时间也每天晚上基本都 11 点才从实验室回宿舍。可是渐渐地开始怀疑了,觉得好像实验室的项目也没有那么有意思,虽然对操作系统真的很感兴趣,但被分配任务去按部就班地完成好像终究缺了点乐趣。总结来说给的补贴太少,占用的时间太多,写的是屎山代码,又没有什么自由发挥的空间,有点憋屈。于是当初那个幻想着“为中国操作系统事业添砖加瓦、为中华民族伟大复兴尽一份力”的自己,逐渐开始变得现实了,现在只想在实验室规定的工作时间干活,其它时间都用来做自己的事,或者写写个人项目,或者出去耍一耍放松,总之是不想碰实验室的事情了。

曾经希望能搞点科研,现在也觉得好像很无趣,其实现在只能打廉价劳动力的工,并没有机会参与科研,但也并不期待。现在只想安安静静地混个毕业,期间能多点时间学自己想学的东西,最后找个还算不错的工作,也就足够了。我终究不是一个能够无私奉献自己的人。谁又是呢。

QQ 机器人

暑假的时候还有一件算蛮大的事,就是 CKYU 关闭,起因是晨风机器人作者据传被公安局喝茶,然后 CKYU 作者为了避免遭殃,主动撤了。其实这说起来非常好笑,本来就是灰色产业,为了避险,不再做了,明明很正常的事情,网上居然有人称之为“8.2 事件”,然后各种讲情怀,说得好像是什么不得了的情况一样。

CKYU 关了之后,CQHTTP 也就没用了,其实本来也没什么精力维护了,想着趁机也就不再管了。结果后来广大用户们在其它机器人平台发明了各种轮子,有的兼容 CQHTTP API,有的甚至可以直接加载 CQHTTP 插件。于是最后还是决定再做一件事情,就是把 CQHTTP 的文档重新整理了一下,做成了“OneBot 聊天机器人接口标准”,整理到最后甚至来了兴致,觉得把这玩意就作为接口标准继续维护也不错,可以推动各兼容项目长久保持兼容。可是这个兴致终究抵不过实验室各种事情的忙碌,最后也就没时间顾及了。

现在 OneBot 标准依然是许多项目开发时的参考文档,但未来何去何从,已经没有明确的方向了。

操作系统

虽然说前面吐槽了有些时候对实验室项目感到无趣,但写操作系统这件事本身一直是很有意思的,当不小心把精力投入到实验室的 OS 项目里之后,也常常会忘记自己已经说过要“堕落”。其实以前就一直梦想着以后写一个自己的 OS,去年经过 μCore 和 ChCore 实验的练习,以及翻阅了些 Linux 源码,似乎已经具备了写 OS 的能力,再加上对实验室的 OS 不太满意,没有发挥空间,于是年末开了个新坑,打算尝试写一个叫做 rcOS 的简易操作系统,算是满足自己的心愿和完美主义,希望 2021 年能写到自己满意的程度。

新的一年?

其实经过半年的研究生生活,对新的一年已经没有多少激动人心的期待,只希望能更好地调整好 work-life balance,多抽出些时间陪女朋友,以及写自己的 OS 和其它项目,多运动保持健康,就够了。

2020 → 2021