OpenWrt:从源码到固件,编译自己的OpenWrt系统

本文说明如何一步一步地从源码编译出自己的OpenWrt镜像,并升级到设备上。

准备工作

准备一个GNU/Linux, BSD 或 MacOSX 操作系统。并且,在环境中准备好以下官方教程中要求的工具:

https://openwrt.org/docs/guide-developer/toolchain/install-buildsystem

我的环境是Ubuntu22,执行

1
2
sudo apt update 
sudo apt install build-essential clang flex bison g++ gawk gcc-multilib g++-multilib gettext git libncurses-dev libssl-dev python3-distutils rsync unzip zlib1g-dev file wget

编译系统

下载源码

下载代码:

1
2
3
git clone https://github.com/openwrt/openwrt.git
cd openwrt
git pull

切换分支,一般选择最新的稳定版本:

1
2
3
git branch -a
git tag
git checkout v23.05.0

下载软件包

运行./scripts/feeds update -a命令,下载或更新在feeds.conf/feeds.conf.default中定义的所有最新软件包。

受限于国内的访问国际互联网的环境中,feeds update这一步特别容易失败。可以通过以下几条方式来提高成功率:
git config --global http.postBuffer 524288000
git config --global http.lowSpeedLimit 1000
git config --global http.lowSpeedTime 600
以上配置的含义为:配置git缓冲区为500M,配置git访问超时的条件为:速率小于1KB/s,且持续600秒

运行./scripts/feeds install -a命令使安装上述软件包在后续的make menuconfig中生效。

配置编译选项

使用已有固件的编译配置

网络上已编译出的固件通常都会把编译配置一并提供(config.buildinfo或config.seed),可以直接使用。

我的目标机是一台小米WR30U,使用mtk的filogic芯片方案,从OpenWrt官网找到对应的编译配置并下载,置于OpenWrt工程根目录下的.config文件中:

1
wget https://downloads.openwrt.org/releases/23.05.0/targets/mediatek/filogic/config.buildinfo -O .config

但这份配置中包含了filogic芯片方案的所有设备的配置,还需进行裁剪和修改。

在以上配置的基础上,运行make menuconfig命令来完成进一步的自定义配置。完成后配置会更新至.config文件中。

在Target Profile中,仅保留Xiaomi WR30U的设备支持(我的设备是Xiaomi Mi Router WR30U (112M UBI with NMBM-Enabled layout)),把其它设备的支持删除。

menuconfig

编译

使用make download预先下载编译过程中需要的代码和依赖项等资源。

. . . . . . . 经过漫长的等待下载完成。到此为止,我们已做好了所有的编译准备。

正式开始编译吧,运行make命令来构建固件。该命令将下载所有源代码和依赖项(即使之前已经make download,也还有其它包需要下载),构建交叉编译工具链,然后为目标系统交叉编译出OpenWrt内核和应用程序。

1
make -j4 V=s

编译选项说明

  • -jN: make命令可以加上-j参数用于指定使用多少cpu核编译,可以加速编译过程。例如:make download -j4make -j5
  • V=s:make命令可以加上V=s可以输出更多的编译错误信息。

如果顺利的话,这里make完就可以编译出镜像了。但是实际不太可能完全顺利。每个人遇到的问题可能不一样。我遇到的问题和修复方法,我放在后面附录中。

编译输出

. . . . . . . 经过漫长的编译过程(我的环境中编了5个小时),编译结果存放于openwrt/bin/targets/目录下。几类编译产出的镜像说明如下:

  1. factory:用于替换厂商的原厂固件,兼容原厂的安装包格式。通常使用原厂的web GUI进行升级。
  2. sysupgrade:用于升级替换已有的OpenWrt版本,这是最常用的镜像。
  3. initramfs-kernel:用于开发或特殊情况下的一次性引导,作为安装常规sysupgrade版本的过渡步骤。由于initramfs版本完全运行在RAM中,不会在闪存中存储任何设置,因此不适合用于操作性使用。

升级固件

主要参考刷机教程中提及的办法,在路由器断电后,用针按住 reset 不放,再接上电源,等待 10s 左右松开,就能进入 uboot。电脑用网线和 wr30u 的网口1连接,电脑在网络设置里将以太网设置为静态。IP地址:192.168.1.2,子网掩码:255.255.255.0,浏览器打开192.168.1.1访问uboot后台。

uboot下上传固件

现在选择编译出的openwrt主程序(openwrt-23.05.0-mediatek-filogic-xiaomi_mi-router-wr30u-112m-nmbm-squashfs-sysupgrade.bin),upload 后 update 更新即可。

升级完成后,把网络连到LAN口上(非1口),在控制台上用ssh root@192.168.1.1登陆,可见已成功升级至编译出的OpenWrt固件。

ssh登陆OpenWrt设备

也可以通过浏览器访问192.168.1.1,进入web管理页面,默认用户名密码是root/password

OpenWrt LuCI首页

将自己编译的固件与从官方网站上下载的固件https://downloads.openwrt.org/releases/23.05.0/targets/mediatek/filogic/openwrt-23.05.0-mediatek-filogic-xiaomi_mi-router-wr30u-112m-nmbm-squashfs-sysupgrade.bin 升级后的结果做了一下对比,路由器功能完全一致。

附录

参考资料

编译失败记录

内核编译Warning

默认情况下,内核编译有加上-Werror选项,因此遇到Warning就会停下来。例如编译失败日志:

1
2
/home/spencer/openwrt/build_dir/target-aarch64_cortex-a53_musl/linux-mediatek_filogic/dmx_usb_module-19.12.1/dmx_usb.c: In function 'dmx_usb_write':
./include/linux/kern_levels.h:5:25: error: format '%d' expects argument of type 'int', but argument 4 has type 'size_t' {aka 'long unsigned int'} [-Werror=format=]

于是找到-Werror添加的地方,删除掉。即在openwrt/build_dir/target-aarch64_cortex-a53_musl/linux-mediatek_filogic/linux-5.15.132/Makefile中,把以下这行注释掉:

1
#KBUILD_CFLAGS-$(CONFIG_WERROR) += -Werror

acl编译失败

遇到acl编译失败,到社区上搜索,已有高人给出了答案:

https://github.com/openwrt/packages/issues/21051
https://github.com/openwrt/packages/pull/21031/commits/dcc6d70f735474d49345d8d4b43a8098bb217220

cjdns编译失败

1
2
3
4
5
6
7
cc1: note: someone does not honour COPTS correctly, passed 16 times
net/SwitchPinger_admin.c: In function 'adminPing':
net/SwitchPinger_admin.c:103:133: error: dangling pointer 'err' to an unnamed temporary may be used [-Werror=dangling-pointer=]
103 | Dict d = Dict_CONST(String_CONST("error"), String_OBJ(err), NULL);
| ^
net/SwitchPinger_admin.c:82:26: note: unnamed temporary defined here
82 | err = String_CONST("path was not parsable.");

解决办法:
https://github.com/coolsnowwolf/lede/issues/10817

OpenWrt系列教程

OpenWrt:刷机小米WR30U(AX3000T)

硬件配置

小米WR30U是小米的联通专供路由器,不支持售后。也可以购买它的零售版的AX3000T,硬件配置完全一样。

主芯片:MT7981BA
射频芯片:MT7976CN
交换芯片:MT7931AE
存储:RAM 256MB ROM 128MB
开发平台:Flogic 820

准备工作

  • 小米WR30U(AX3000T)一台
  • Windows 10 PC 一台
  • 网线一根
  • MobaXterm等控制台工具

刷机过程

初始配置

路由器上电,PC通过wifi连接上路由器放出的信号(我这里是xiaomi_0342和xiaomi_0342_5G)。

在浏览器中登陆:http://192.168.31.1/init.html#/home,初始配置向导随意配置。

在“上网配置”中,做以下配置:

  • 上网配置选择“DHCP”
  • 使能“启动与智能网关无线配置同步”(会重启)
  • WAN口选择,改为“固定WAN口”,1口为固定WAN口。

在PC上,打开控制面板-网络和 Internet-网络和共享中心-选择WLAN-点击属性-共享-勾选第一个允许-确认。这个时候路由器应该能连接网络,面板上的网络灯也会从黄灯变为蓝色。

设置网络共享

通过以上方式,路由器可以通过PC的网络来访问因特网,可以直接从网络上下载一些资源。但有时有一些鸡肋。原因是:PC要通过ssh来操作WR20U,就只能通过192.168.31.1这一wifi空口IP(ssh server只在这个接口上监听),但PC又同是要访问大网,wifi又不能连到WR20U放出的信号上,应该连到其它可访问大网的信号上。

解锁SSH

在PC上,安装pycryptodome工具,这是一个加解密库。

1
pip install pycryptodome

运行server_emulator.py脚本以解锁ssh

1
python server_emulator.py

出现Device informationfinish 就完成了,现在 wr30 就打开了 ssh,默认的用户名是 root 密码是 admin。

此时断开网线,用wifi连接WR30U。用ssh root@192.168.31.1连接路由器。出现“ARE U OK”就算连接上了。

ssh连接xiaomi WR30U

然后就可以通过ssh控制台操作设备了。

如果需要固化 ssh 可以执行以下命令(wr30u 需要联网)

curl -O https://cdn.jsdelivr.net/gh/lemoeo/AX6S@main/auto_ssh.sh
chmod +x auto_ssh.sh
uci set firewall.auto_ssh=include
uci set firewall.auto_ssh.type='script'
uci set firewall.auto_ssh.path='/data/auto_ssh/auto_ssh.sh'
uci set firewall.auto_ssh.enabled='1'
uci commit firewall``` 

刷写uboot

使用WinSCP把mt7981_xiaomi_wr30u-u-boot.fip文件拷贝到/tmp目录下,并执行:mtd write mt7981_xiaomi_wr30u-u-boot.fip FIP

烧写uboot命令

刷写主程序

路由器断电后,用针按住 reset 不放,再接上电源,等待 10s 左右松开,就能进入 uboot。电脑用网线和 wr30u 的网口1连接,电脑在网络设置里将以太网设置为静态。IP地址:192.168.1.2,子网掩码:255.255.255.0,浏览器打开192.168.1.1访问uboot后台。

uboot下上传固件

需要注意一点,uboot下的默认IP 192.168.1.1与许多家庭网络的网关一致,因此,为了避免冲突,连接WR30U的uboot下,建议断开大网连接,并使用浏览器的无痕模式。

现在选择下载好的openwrt主程序,upload 后 update 更新即可,刷写完成系统会重启进入 openwrt 的系统。

主程序web界面

至此,xiaomi WR30U就刷机完成了。

新的系统默认 WiFi 是 QWRT,没有密码,后台是 192.168.1.1,默认用户名:root,密码:password。

默认IP 192.168.1.1与许多家庭网络的网关一致,因此,为了避免冲突,建议修改本地IP为192.168.8.1 。(网络-接口-LAN修改)

修改IP后,ssh root@192.168.8.1,密码password。可以进入openwrt后台环境。

主程序界面

资源下载

参考资料

OpenWrt系列教程

VPP(2):软件架构,VPP-infra,VLIB

VPP软件架构

VPP软件从下至上分为VPP infra, VLIB, VNET, Plugins四层。

报文处理图

VPP-Infra

VPP基础设施层。是一个基础服务集合。提供内存访问,向量、环、哈希表等数据结构接口,以及报文处理图节点、定时器等。VPP infra已经完成近20年,不会有经常的变更。包含:

  • 向量
  • 位图
  • 池:结合向量和位图,以便于快速分配定长的数据结构
  • 哈希表
  • 定时器
  • 单调时钟
  • 字符串format,unformat

VLIB

矢量处理库。vlib层处理各种应用程序管理功能:缓存、内存和报文处理图节点管理。维护计数器,轻量级的多任务线程,数据包跟踪。并实现了CLI。main函数也在此层。

初始化

使用 VLIB_INIT_FUNCTION (my_init_function) macros宏来定义初始化函数。默认情况下,所有初始化函数的执行顺序不定。

可以通过以下手段来约束初始化函数的执行顺序:

1
2
3
4
5
6

VLIB_INIT_FUNCTION(my_init_function) =
{
.runs_before = VLIB_INITS("we_run_before_function_1", "we_run_before_function_2"),
.runs_after = VLIB_INITS("we_run_after_function_1", "we_run_after_function_2),
};

报文处理图初始化

报文处理图定义了一系列的图节点用于处理报文。框架支持在运行时往图中添加节点,不支持移除节点。vlib提供了多种类型的节点,按分发行为来分类(vlib_node_registration_t ):

  • VLIB_NODE_TYPE_PRE_INPUT:在其它节点类型之前执行
  • VLIB_NODE_TYPE_INPUT:在pre_input节点之后,尽快执行
  • VLIB_NODE_TYPE_INTERNAL:只在针对挂起的报文,显式配置运行态时执行。
  • VLIB_NODE_TYPE_PROCESS:只在显式配置运行态时执行,在多任务线程中执行。节点会在挂起一段时间后被执行。

报文处理图节点分发

报文处理图节点分发的函数入口是:./src/vlib/main.c:vlib_main_loop.。报文分发的流程很简单,但是可能会很难理解:分发器将向量发入处理图中,如果需要的话,把它切分成更小的向量,直至原始向量中的所有工作都被处理完。

这一方案需要在向量中的报文个数上做一个权衡:如果向量中的报文个数增加,每个报文的处理时间减小。是因为:节点处理的第一个分片报文的处理过程中,把处理所需的指令加载到L1 I-cache中,后续所有报文都会此因受益,可以使用I-cache中已经加载好的指令。

参考资料

VPP(1):什么是VPP,为什么叫VPP

什么是VPP

VPP(Vector Packet Processing)是思科旗下的一款可拓展的开源框架,提供用用的、高质量交换、路由功能的应用框架。是网络协议领域常用的开源框架。

为什么叫VPP

VPP全称Vector Packet Processing(矢量报文处理)。VPP的精髓都包含在名字里了,即这个Vector。矢量报文处理与常规的报文处理有什么差别?

标量报文处理

一个标量报文处理流程的典型过程是:一次仅处理一个报文,一个报文中接口收到后,经过若干个功能模块的依次处理。每个功能模块的处理结果,通常有三类,1)丢弃;2)不作处理投递给下一功能模块;3)改写报文/转发。

1
2
3
4
+---> fooA(packet1) +---> fooB(packet1) +---> fooC(packet1)
+---> fooA(packet2) +---> fooB(packet2) +---> fooC(packet2)
...
+---> fooA(packet3) +---> fooB(packet3) +---> fooC(packet3)

标量报文处理存在以下问题:

  1. I-cache抖动:每个报文都要完整地执行完所有功能模块(foo)的处理,程序频繁地替换I-cache(instruction cache)缓存中的数据,导致缓存利用率降低,从而影响计算机性能。
  2. 每个报文处理过程中的调用栈信息随功能模块(foo)而变化,导致调用栈存放的D-cache的存取压力大。

矢量报文处理

矢量报文处理把报文组成一个“报文矢量”(可理解为报文组)。每一个功能模块(foo),都对报文矢量中的报文做一次性处理。

这样的设计:

  1. 减少了I-cache抖动,因为每个instruction重复应用于处理报文组的每个报文,提高了I-cache命中率。
  2. 调用栈信息重复应用于报文组的每个报文,D-cache的存取压力随之降低。
1
2
3
4
+---> fooA([packet1, +---> fooB([packet1, +---> fooC([packet1, +--->
packet2, packet2, packet2,
... ... ...
packet256]) packet256]) packet256])

报文处理图

VPP中的关键设计是Packet Process Graph(报文处理图)。使VPP具有以下特性:

  • 可伸缩,可插件扩展
  • 成熟的图节点架构
  • 可定制的流水线
  • 插件平等

在处理图中,开发人员可以定制插入新的处理节点,这让VPP易于扩展且可以用于定制化地对报文进行特定意途的处理。

报文处理图

VPP平台从接口上接收报文,组合成报文向量,然后把报文向量“喂”给报文处理图,图中的所有节点对报文向量进行逐一处理。

VPP插件都是在运行时加载的共享库。一个插件可以引入新的报文处理图节点或重新组织报文处理图。你可以生成一个完全独立于VPP原生的报文处理图。

参考资料

Linux CFS调度器

Linux CFS

CFS(Completely Fair Scheduler)是Linux 2.6.23之后的内核默认的线程调度器,它提供了一种相对公平的线程调度策略。它提供了多种调度算法,大致分为两类。

  • 普通调度策略
  • 实时调度策略

普通调度

普通调度策略,即最小vruntime调度。包含SCHED_OTHER, SCHED_IDLE, SCHED_BATCH这几类。在普通调度策略下,优先级(sched_priority)字段总是为0,并没有参与到调度策略的决策中。

注意:这里说的sched_priority与ps或top命令中看到的pri字段,并不是同一个含义。

vruntime

vruntime是一个线程在cpu上运行的累计时间经过归一校正计算后的结果 ,调度器会优先调度vruntime较小的线程。

理想情况下,所有线程的vruntime大小应该一致,即各线程得到了公平的调度。

线程切换原则

原则很简单:总是选择vruntime最小的线程进行调度。

线程A在执行一段时间后,它的vruntime逐渐累积。一旦A的vruntime超过另一个B的vruntime,CFS则做出线程切换,调度线程B执行。

红黑树

所有处于runnable状态的线程,基于vruntime值,组织一棵红黑树(CFS red-black tree)。以此方便地插入新的线程,或取出vruntime最小的线程。

基于nice值的动态优先级

nice值通过对vruntime的校正,进而影响线程被分配到的时间片的多少。nice的取值范围是-20(高优先级)~19(低优先级)。nice会影响线程占用CPU的时间,nice值越小被分配到的时间片越多。nice值仅会影响SCHED_OTHERSCHED_BATCH策略。

下面简要解释算法:

每一个nice值都对应一个权重系数。大约为: weight = 1024 * (1.25)^(-nice)

可以简要地理解为,权重系数为(1.25)^(-nice)

  • 当nice为0,权重系数=1,vruntime即是线程在cpu上运行的实际时间。
  • 当nice>0,权重系数>1,vruntime会比线程在cpu上运行的实际时间更长。
  • 当nice<0,权重系数<1,vruntime会比线程在cpu上运行的实际时间更短。

调度策略

SCHED_OTHER

SCHED_OTHER仅用于sched_priority为0的线程,在sched_priority为0的所有SCHED_OTHER线程中,使用CFS默认的调度策略(最小vruntime调度),上面已经描述了具体算法。

SCHED_BATCH

SCHED_BATCHSCHED_OTHER类似,仅用于sched_priority为0的线程。其不同之处在于:SCHED_BATCH会假设所有线程都是对cpu都是贪婪的,它会在线程的每一次调度之后,对线程施加一点“小惩罚”。这种策略通常应用于占用cpu较多,但又没有用户交互的线程中。

SCHED_IDLE

SCHED_IDLE仅用于sched_priority为0的线程,nice值对它也不起作用。此调度策略被应用于极低优先级的线程,仅当系统空闲时线予调度。

实时调度

SCHED_FIFOSCHED_RR都属于实时线程调度策略。

sched_priority在实时调度策略中被使用,它的取值范围是1~99。调度器针对每一个sched_priority级别,都维护一个待执行的线程的队列。调度器会从最高优先级的非空队列中,选择队头节点的线程来执行。

SCHED_FIFO

当一个SCHED_FIFO达到可执行状态,它总是会抢占SCHED_OTHER, SCHED_IDLE, SCHED_BATCH的线程。SCHED_FIFO并没有把cpu时间切分时间片,一旦开始执行,就会一直执行,除非以下状况:

  • 线程主动挂起(I/O等)。
  • 被更高的优先级线程抢占。

它遵循以下规则:

  • 正在被执行的SCHED_FIFOA线程,如果被更高优先级的B线程抢占,它会放在队列队头中。一旦B线程执行完成,A线程会马上被恢复调度。
  • SCHED_FIFO的线程从不可执行转为可执行状态,它会被加入到对应sched_priority的队尾。
  • SCHED_FIFO线程的优先级被调整时,如果是提高优先级,会被放入新优先级队列的队尾;如果是降低优先级,会被放在新优先级队列的队头。

SCHED_RR

RR(Round-robin)调度是针对FIFO调度的增强。在以上FIFO调度的策略的基础上,添加了以下规则:可以配置线程单次执行的最大时间片长度。一旦时间片被使用完,线程停止执行并放置到对应优先级队列的队尾。

参考文献

600行代码写了个小工具,替代linux中的cd和ls,取名cdls

近来用Rust写了个小工具,核心设计思路是:用方向键在各目录间跳转。兼顾了排序、文件属性显示、文件名搜索等相关功能。

考虑到在linux中,这个活儿通常是用cd和ls完成的,这个工具取名为cdls。

用法示例:

安装

在x86-64架构中,

1
2
3
wget https://xs-upload.oss-cn-hangzhou.aliyuncs.com/cdls/release/v0.3/cdls
sudo mv cdls /usr/bin/
sudo chmod +x /usr/bin/cdls

用法

1
2
3
4
5
# 启动cdls屏幕
cdls

# 显示帮助
cdls -h

在cdls屏幕中:

  1. 用方向键即可在各目录间跳转

     方向键左             上级目录
     方向键右             下级目录
     方向键上             选择前一项
     方向键下             选择后一项
    
  2. 配置屏幕,输入以下键启动配置屏幕

     c                       列显示配置
     s                       排序配置
    
     在配置屏幕中,用方向键选择配置,用空格键选定配置,用`q`保存并退出配置屏幕
    
  3. 搜索模式

     f                        启动搜索模式
     在搜索模式中,键入关键字,匹配的文件优先显示。用`enter`退出搜索模式并跳转到目的文件。
    
  4. 退出cdls

     Enter键                 退出cdls并跳转到当前目录
    

项目URL

https://github.com/SmileXie/cdls

Rust Cheat Sheet

变量

可变与不可变

1
2
let a = 10; // 不可变
let mut b = 20; // 可变

数据类型

整型

1
let a: u64 = 10; 
长度 有符号 无符号
8bits i8 u8
16bits i16 u16
32bits i32 u32
64bits i64 u64
128bits i128 u128
与cpu架构相关 isize usize

数字时指定类型,见下例中的9u32

1
println!("9 / 2 = {} but 9.0 / 2.0 = {}", 9u32 / 2, 9.0 / 2.0);

浮点型

类型为:f32f64。浮点型默认为f64

1
2
let a: f32 = 10.0;  
let b = 11.0 // 默认f64

布尔型

类型为:bool;取值为:true and false

1
let b: bool = true;

字符与字符串

1
2
3
let c: char = 'f';  // 字符
let string_c: &str = "ace"; // 字符串切片
let string_s = String::from("hello"); // 字符串

这里的string_c为“字符串切片”

元组

定义和访问元组

1
2
3
4
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;

数组

1
2
3
4
5
let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; 
// 声明一个全0数组,数据成员个数为5
let bytes = [0; 5];
// 使用数组成员
let first = days[0];

数组的两个重要特征:数组的每个元素都具有相同的数据类型。 数据类型永远不会更改。数组大小是固定的。 长度永远不会更改。

向量 Vector

与数组不同之处在于,向量的大小或长度可以随时增大或缩小。 在编译时,大小随时间更改的功能是隐式的。 因此,Rust 无法像在数组中阻止越界访问一样在向量中阻止访问无效位置。

1
2
3
4
5
6
7
8
9
10
11

let three_nums = vec![15, 3, 46];
println!("Initial vector: {:?}", three_nums);

let mut fruit = Vec::new();
fruit.push("Apple");
println!("Pop off: {:?}", fruit.pop());

// 索引
let mut index_vec = vec![15, 3, 46];
let three = index_vec[1];

哈希表

1
2
3
4
5
6
7
8
9
10
use std::collections::HashMap; 
//声明与插入元素
let mut reviews: HashMap<String, String> = HashMap::new();
reviews.insert(String::from("Ancient Roman History"), String::from("Very accurate."));
//获取键值
let book: &str = "Programming in Rust";
println!("\nReview for \'{}\': {:?}", book, reviews.get(book));

let obsolete: &str = "Ancient Roman History";
reviews.remove(obsolete);

结构体

定义结构体

1
2
3
4
// 经典结构
struct Student { name: String, level: u8, remote: bool }
// 元组结构
struct Grades(char, char, char, char, f32);

主要区别:经典结构中的每个字段都具有名称和数据类型。 元组结构中的字段没有名称。

实例化

1
2
3
4
5
6
7
8
let user_1 = Student { name: String::from("Constance Sharma"), remote: true, level: 2 };

// Instantiate tuple structs, pass values in same order as types defined
let mark_1 = Grades('A', 'A', 'B', 'A', 3.75);

println!("{}, level {}. Remote: {}. Grades: {}, {}, {}, {}. Average: {}",
user_1.name, user_1.level, user_1.remote, mark_1.0, mark_1.1, mark_1.2, mark_1.3, mark_1.4);

枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 声明
enum WebEvent {
WELoad,
WEKeys(String, char),
WEClick { x: i64, y: i64 }
}

// 或

struct KeyPress(String, char);
struct MouseClick { x: i64, y: i64 }
enum WebEvent { WELoad(bool), WEClick(MouseClick), WEKeys(KeyPress) }

// 赋值
let click = MouseClick { x: 100, y: 250 };
let keys = KeyPress(String::from("Ctrl+"), 'N');

let we_load = WebEvent::WELoad(true);
// Set the WEClick variant to use the data in the click struct
let we_click = WebEvent::WEClick(click);
// Set the WEKeys variant to use the data in the keys tuple
let we_key = WebEvent::WEKeys(keys);

泛型

1
2
3
4
5
6
7
8
9
struct Container<T> {
value: T,
}

impl<T> Container<T> {
pub fn new(value: T) -> Self {
Container { value }
}
}

函数

1
2
3
4
5
6
7
fn goodbye(message: &str) {
println!("\n{}", message);
}

fn divide_by_5(num: u32) -> u32 {
num / 5
}

所有权

所有权三原则

  1. Each value in Rust has a variable that’s called its owner. (Rust中每一个变量都有一个所有者。)
  2. There can only be one owner at a time.(在任一时刻,所有者有且仅有一个。)
  3. When the owner goes out of scope, the value will be dropped.(当所有者离开其作用域后,它所拥有的数据会被释放。)

Copy Trait

凡是拥有Copy trait的数据类型,“=”都表示数据的复制而非传有权的转移。以下数据类型有Copy trait:

  • 所有整型:u32 u64等
  • 布尔型
  • 所有浮点型:f32 f64等
  • 字符型:char
  • 元组(Tuples):如果组成元组的每个成员都有Copy trait,那么此元组也有Copy trait。

引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 引用
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
s.len()
}

// 可变引用
fn main() {
let mut s = String::from("hello");
change(&mut s);
}

fn change(some_string: &mut String) {
some_string.push_str(", world");
}

引用的原则

引用的原则:

  • 任何时刻,一个变量只能有
    • 一个可变引用,或者
    • 多个不可变引用
    • 以上两点不可同时存在
  • 引用应该总是合法的

Rust编译器在以下三个条件同时满足时,会产生数据竞争,发出编译错误:

  • 两个或两个以上的pointer(包含所有者,可变引用)指向同一份数据。
  • 其中至少一个可变引用指会向空间写入数据。
  • 没有同步数据的访问机制。

手动批注生存期(lifetime annotation)

1
2
3
4
5
6
7
fn longest_word<'a>(x: &'a String, y: &'a String) -> &'a String {
if x.len() > y.len() {
x
} else {
y
}
}

以上代码中,x和y的生命周期有可能不一样,所以函数返回值的生命周期实际上是由两个参数里生命周期较短的那个决定的。

条件判断

loop

在断点处返回一个值

1
2
3
4
5
6
7
8
9
let mut counter = 1;
// stop_loop is set when loop stops
let stop_loop = loop {
counter *= 2;
if counter > 100 {
// Stop loop, return counter value
break counter;
}
};

for

1
2
3
4
5
6
7
8
9
let big_birds = ["ostrich", "peacock", "stork"];
for bird in big_birds.iter() {
println!("The {} is a big bird.", bird);
}

// 此代码遍历数字 0、1、2、3 和 4
for number in 0..5 {
println!("{}", number * 2);
}

while

1
2
3
4
while counter < 5 {
println!("We loop a while...");
counter = counter + 1;
}

Option与Result 枚举

原型

1
2
3
4
5
6
7
8
9
enum Option<T> {
None, // The value doesn't exist
Some(T), // The value exists
}

enum Result<T, E> {
Ok(T): // A value T was obtained.
Err(E): // An error of type E was encountered instead.
}

Result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#[derive(Debug)]
struct DivisionByZeroError;

fn safe_division(dividend: f64, divisor: f64) -> Result<f64, DivisionByZeroError> {
if divisor == 0.0 {
Err(DivisionByZeroError)
} else {
Ok(dividend / divisor)
}
}


fn read_file_contents(path: PathBuf) -> Result<String, Error> {
let mut string = String::new();

// Access a file at a specified path
// ---------------------------------
// - Pass variable to `file` variable on success, or
// - Return from function early if there's an error
let mut file: File = match File::open(path) {
// Corrected code: Pass variable to `file` variable on success
Ok(file_handle) => file_handle,
// Corrected code: Return from function early if there's an error
Err(io_error) => return Err(io_error),
};

// Read file contents into `String` variable with `read_to_string`
// ---------------------------------
// Success path is already filled in
// Return from the function early if it is an error
match file.read_to_string(&mut string) {
Ok(_) => (),
// Corrected code: Return from function early if there's an error
Err(io_error) => return Err(io_error),
};

// Corrected code: Return `string` variable as expected by function signature
Ok(string)
}

match

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// vec!与match
let fruits = vec!["banana", "apple", "coconut", "orange", "strawberry"];
for &index in [0, 2, 99].iter() {
match fruits.get(index) {
Some(fruit_name) => println!("It's a delicious {}!", fruit_name),
None => println!("There is no fruit! :("),
}
}

// 仅在Option为某个值时执行print
let a_number: Option<u8> = Some(7);
match a_number {
Some(7) => println!("That's my lucky number!"),
_ => {},
}

// if let

let a_number: Option<u8> = Some(7);
if let Some(7) = a_number {
println!("That's my lucky number!");
}

Trait

Trait是一组类型可实现的通用接口。个人理解:类似于给类定义方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
trait Area {
fn area(&self) -> f64;
}

struct Circle {
radius: f64,
}

struct Rectangle {
width: f64,
height: f64,
}

impl Area for Circle {
fn area(&self) -> f64 {
use std::f64::consts::PI;
PI * self.radius.powf(2.0)
}
}

impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}

可以编写一个函数,该函数接受任何实现 AsJson Trait的类型

1
2
3
4
5
fn send_data_as_json(value: &impl AsJson) {
println!("Sending JSON data to server...");
println!("-> {}", value.as_json());
println!("Done!\n");
}

迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#[derive(Debug)]
struct Counter {
length: usize,
count: usize,
}

impl Counter {
fn new(length: usize) -> Counter {
Counter {
count: 0,
length,
}
}
}

impl Iterator for Counter {
type Item = usize;

fn next(&mut self) -> Option<Self::Item> {

self.count += 1;
if self.count <= self.length {
Some(self.count)
} else {
None
}
}
}

println

1
2
3
fn main() {
println!("The first letter of the English alphabet is {} and the last letter is {}.", 'A', 'Z');
}

derive(Debug)

通过#[derive(Debug)]语法可以在代码执行期间查看某些在标准输出中无法查看的值。 要使用 println! 宏查看调试数据,请使用语法 {:#?} 以可读的方式格式化数据。

1
2
3
4
5
#[derive(Debug)]
struct MouseClick { x: i64, y: i64 }

let m = MouseClick{ x: 20, y: 30};
println!("{:#?}", m);

todo

1
todo!("Display the message by using the println!() macro");

工具

Valgrind笔记(二):MemCheck基本原理

Valgrind的MemCheck工具,可以检测多种内存错误,它是如何做到的?

V bits

MemCheck构造出一个“虚拟CPU”。与真实CPU不同的是,虚拟CPU中的每一个bit,都有一个关联的“是否有效”bit( “valid-value” bit),用于表示它关联的bit是否有效(更确切地,应该是“是否初始化”)。以下称这个bit为V bit。

例如,当CPU从内存中加载一个4-byte int时,也同时从V-bit位图中加载32bits的V bits。当CPU把这个int写回某个地址时,这32bits的V bits也会被存回V-bit位图中。

也就是说,系统中的所有bits,都有对应的V bits。不仅仅是内存,甚至是CPU寄存器,也有它们的V bits。为了实现这样的功能,MemCheck需要为V bits提供强大的压缩能力。

仅仅是访问非法的数据,并不会直接触发MemCheck的报错。仅当这一错误影响到程序的执行结果时,才会报错。例如:

1
2
3
4
5
6
int i, j;
int a[10], b[10];
for ( i = 0; i < 10; i++ ) {
j = a[i];
b[i] = j;
}

虽然访问a[i]时,未初始化,但是它并不会报错。因为这段程序仅仅是把未初始化的a[10]复制到b[10]。并没有把这些未初始的值用于某种判断或输出。如果程序改为:

1
2
3
4
5
6

for ( i = 0; i < 10; i++ ) {
j += a[i];
}
if ( j == 77 )
printf("hello there\n");

MemCheck就会报错了。未初始化的值a[i]之和,被用于计算j并用于判断if (j == 77)

在CPU做各种运算时(加,位运算等),操作数的V bits被引用,用于计算出运算结果的V bits。但是,即使这里包含非法的V bits,MemCheck仍然不会报错。仅当以下几种情况发生时,MemCheck才会去检查V bits的有效性:

  • 运算结果被用于生成地址
  • 影响程序控制流(如条件判断)
  • 有系统调用发生

这样的机制似乎看起来过度设计了,但一旦联想到C语言中有一个常见的机制叫“结构体自动填充”,你就不会这么觉得了。以下这个例子:

1
2
3
4
5
6
struct S { int x; char c; };
struct S s1, s2;
s1.x = 42;
s1.c = 'z';
s2 = s1;
if (s2) printf("s2\n");

请问,以上代码中,是否有未初始化的赋值?答案是:有。struct S在除了x和c成员外,还有3 bytes的自动填充字段,而这部分字段是未被初始化的。作为C语言程序员,想必你不会希望MemCheck因未初始化的自动填充字段而报错。

A bits

在MemCheck构建的“虚拟CPU”之上,对应每一个数据,除了V bits外,还有表示地址有效性的Valid-address bits,简称A bits。与V bits不同的是:1)A bits仅仅针对内存中的数据存在,CPU寄存器中的数据,没有A bits。2)每一byte的内存数据,对应一bit的A bit。A bits用于表示程序是否可以合法地读写此地址的数据。

每当程序读写内存时,MemCheck会检查对应地址的A bits。如果A bits指示当前的地方访问是非法的,将抛出告警。此时,MemCheck仅仅会检查A btis,而不会修改它。

A bits是如何被构建出来的呢:

  • 进程启动时,所有全局变量的A bits被标记为“可被访问”。
  • malloc/new时,申请出来的内存,被标记为“可被访问”。它们被free时,该地址修改为“不可访问”。
  • 栈指针(stack pointer register,SP)上下移动时,A bits会被修改。在SP之上,至栈基地址之间的地址,被标记为“可被访问”。SP之下的地址,被标记为“不可访问”。
  • 系统调用时, A bits会被修改。例如:mmap把一段地址映射进进程地址空间,会被标记为“可被访问”。
  • 程序可以主动告诉MemCheck,哪些地址应该被做何种标记。

规则总结

综合A bits与V bits,MemCheck检测内存错误,会遵循以下原则:

  • 内存中的每一byte数据,都有8 bits关联的V bits和1 bit的A bits。V bits表示对应的内存数据是否被初始化;A bits表示程序对此地址空间的访问,是否合法。经过压缩,A bits + V bits通常会增加25%的内存使用。
  • 当内存读写发生时,关联的A bits被取出,用于检查地址访问的合法性。MemCheck在发生非法访问时,会抛出异常。
  • 当内存被读进CPU寄存器时,关联的V bits被读进“虚拟CPU”中;* 当CPU寄存器中的数据被写进内存时,关联的V bits也从“虚拟CPU”中被写回内存中。
  • 当数据被用于生成地址,或会影响程序控制流(如条件判断)时,或数据用于系统调用,V bits被检查。其它情况下,V bits不作检查。当发现使用未初始化的数据,MemCheck会抛出异常。
  • 一旦V bits被检查过,它们就被置为“有效”。这一手段用于避免过多地重复报错。
  • 当数据从内存中加载进CPU时,MemCheck检查 A bits的合法性,在发现非法访问时抛出异常。当发生非法访问时,V bits被强制置为“有效”(尽管A bits为“不可访问”)。这种机制用于避免过多的冗余报错。
  • MemCheck会拦截内存访问接口malloc, calloc, realloc, valloc, memalign, free, new, new[], delete, delete[],这些接口中发生系统调用时,对应地修改A bits和V bits。

参考文献

Valgrind笔记(一):安装与Quick Start

安装

基于源码安装

  • 确认Valgrind最新版本
  • 下载源码:wget https://sourceware.org/pub/valgrind/valgrind-3.17.0.tar.bz2
  • 解压:tar xvf valgrind-3.17.0.tar.bz2
  • cd valgrind-3.17.0
  • 配置: ./configure
  • 编译:make install (可能需要root权限, sudo)

基于安装包安装

Ubuntu环境:sudo apt install valgrind

Quick Start

执行一个最简单的测试:

编写一段有bug的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdlib.h>

void f(void)
{
int* x = malloc(10 * sizeof(int));
x[10] = 0; // problem 1: heap block overrun
} // problem 2: memory leak -- x not freed

int main(void)
{
f();
return 0;
}

编译之(注意要编译选项要带上-g),编译出的可执行文件为test。用valgrind来执行test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ valgrind ./test
==4597== Memcheck, a memory error detector
==4597== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==4597== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==4597== Command: ./test
==4597==
==4597== Invalid write of size 4
==4597== at 0x10916B: f (test.c:6)
==4597== by 0x109180: main (test.c:11)
==4597== Address 0x4a47068 is 0 bytes after a block of size 40 alloc'd
==4597== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==4597== by 0x10915E: f (test.c:5)
==4597== by 0x109180: main (test.c:11)
==4597==
==4597==
==4597== HEAP SUMMARY:
==4597== in use at exit: 40 bytes in 1 blocks
==4597== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==4597==
==4597== LEAK SUMMARY:
==4597== definitely lost: 40 bytes in 1 blocks
==4597== indirectly lost: 0 bytes in 0 blocks
==4597== possibly lost: 0 bytes in 0 blocks
==4597== still reachable: 0 bytes in 0 blocks
==4597== suppressed: 0 bytes in 0 blocks
==4597== Rerun with --leak-check=full to see details of leaked memory
==4597==
==4597== For lists of detected and suppressed errors, rerun with: -s
==4597== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

以上Valgrind给出的log中,已明确指示了错误的地方:

  • test.c 第6行,访问了一个超出malloc申请范围的地址。
  • 检测到一个40 bytes的内测泄漏。通过valgrind --leak-check=full ./test查看更详细的信息。

那么,我们就用valgrind --leak-check=full ./test 试试:

1
2
3
4
5
6
...
==179== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==179== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==179== by 0x10915E: f (test.c:5)
==179== by 0x109180: main (test.c:11)
...

Valgrind检测发现了在test.c第5行malloc的内存,没有被释放。

至此,Valgrind的简单demo就完成了。Valgrind(尤其是MemCheck tool)为C/C++程序员提供了很好的检查内存错误的工具。

一个小型项目的Makefile模板

最近发现一个很好用的Makefile模板,简单修改后几乎可以适配所有常用场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# tool macros
CC ?= gcc
CCFLAGS := # FILL: compile flags
DBGFLAGS := -g
CCOBJFLAGS := $(CCFLAGS) -c


# path macros
BIN_PATH := bin
OBJ_PATH := obj
SRC_PATH := src
DBG_PATH := debug


# compile macros
TARGET_NAME := test
TARGET := $(BIN_PATH)/$(TARGET_NAME)
TARGET_DEBUG := $(DBG_PATH)/$(TARGET_NAME)


# src files & obj files
SRC := $(foreach x, $(SRC_PATH), $(wildcard $(addprefix $(x)/*,.c*)))
OBJ := $(addprefix $(OBJ_PATH)/, $(addsuffix .o, $(notdir $(basename $(SRC)))))
OBJ_DEBUG := $(addprefix $(DBG_PATH)/, $(addsuffix .o, $(notdir $(basename $(SRC)))))


# clean files list
DISTCLEAN_LIST := $(OBJ) \
$(OBJ_DEBUG)
CLEAN_LIST := $(TARGET) \
$(TARGET_DEBUG) \
$(DISTCLEAN_LIST)


# default rule
default: makedir all


# non-phony targets
$(TARGET): $(OBJ)
$(CC) $(CCFLAGS) -o $@ $(OBJ)


$(OBJ_PATH)/%.o: $(SRC_PATH)/%.c*
$(CC) $(CCOBJFLAGS) -o $@ $<


$(DBG_PATH)/%.o: $(SRC_PATH)/%.c*
$(CC) $(CCOBJFLAGS) $(DBGFLAGS) -o $@ $<


$(TARGET_DEBUG): $(OBJ_DEBUG)
$(CC) $(CCFLAGS) $(DBGFLAGS) $(OBJ_DEBUG) -o $@


# phony rules
.PHONY: makedir
makedir:
@mkdir -p $(BIN_PATH) $(OBJ_PATH) $(DBG_PATH)


.PHONY: all
all: $(TARGET)


.PHONY: debug
debug: $(TARGET_DEBUG)


.PHONY: clean
clean:
@echo CLEAN $(CLEAN_LIST)
@rm -f $(CLEAN_LIST)


.PHONY: distclean
distclean:
@echo CLEAN $(DISTCLEAN_LIST)
@rm -f $(DISTCLEAN_LIST)

代码目录结构:

1
2
3
4
5
6
7
8
9
10
11
├── Makefile
├── bin
│ └── crudc
├── debug
├── obj
│ ├── crudc.o
│ └── test.o
└── src
├── crudc.c
├── list.h
└── test.c

使用步骤:

  • 按以上目录结构组织代码
  • Makefile中,配置好这几个变量:CC, CCFLAGS, DBGFLAGS, CCOBJFLAGS, TARGET_NAME
  • makemake debug