基本思路是自己静态编译一份 rz 程序,然后从串口发送给嵌入式设备,然后再利用这个 rz 接收 sz。然后就能双向发送文件了。
设备上至少需要有cat
命令和base64 -d
。如果没有base64可以试试openssl base64 -d
。如果还是没有那这篇文章的方法就不适用于你了。另外在设备上最好还要有一个 gzip 之类的压缩程序,可以显著缩短初次传输时间,以及一个 checksum 程序用于检查。我这里用了xz
和md5sum
。
在主机上你需要有ascii-xfr
程序。
你需要有一个能用的工具链,我的设备是 AArch64,直接使用了源里的 gcc。
1 | # 下载代码 |
如果你有对应设备的工具链也可以动态编译,这样编译出来的文件会更小。
我这里使用picocom
连接设备,其他程序比如minicom
,screen
请自行查阅手册寻找使用方法。
1 | # 接上文 |
有经验的朋友到这里可以直接右上角退出了,不过我还是记录一下做个备忘。
1 | # 按 Ctrl-A Ctrl-X 退出 picocom,然后再重新连接。 |
大约半年前在 SparkFun 上买了一块 RED-V 开发板。基本算是 HiFive1 Rev B 的克隆,比 Rev B 便宜一些,同样使用 SiFive 的 FE310-G002 处理器。SiFive 的 MCU 自带一套 SDK,不过要搭配指定的 IDE 才好用,作为有洁癖的 Linux 用户这当然是不行的,所以就有了这次的折腾。
我一开始是打算用 SDK 的代码,不过看了一圈感觉这样和玩 Arduino 也没区别了,失去了折腾的意义,所以决定放弃 SDK 直接用汇编艹寄存器。
这里是需要准备的文档和工具:
三份 PDF 文档需要自己下载,而其他三个程序应该能直接用包管理装。
简单而言,单片机不像 CPU,有各种内存保护/分页机制等。所有的硬件控制/非易失存储(硬盘)/易失存储(内存)都位于同一个 Memory space。
详细信息可以在 Manual Chapter 4 中看到,比如说,[0x20000, 0x21FFF]
就属于 “OTP Memory Region”。
另外一些地址可以用于控制硬件,比如[0x1001_2000, 0x1001_2FFF]
属于 GPIO,稍后可以看到,如果往某个特定的地址写1
,那么芯片上的某个针脚的电平(电压)就会发生变化,如果这个针脚上连了一个 LED,那么它的亮灭状态也会发生变化。这些可以控制硬件行为的地址我一般称之为寄存器,注意不要和汇编语言里的寄存器搞混了。
一个重要的问题是,单片机在启动的时候到底会执行哪条指令?查看 Manual Chapter 5.1 可知:
On power-on, the core's reset vector is 0x1004.
即会首先执行 0x1004 处的指令(练习:请翻阅 Memory Map 查看该地址属于哪片区域?)并且手册也列出了预先烧录在该地址的指令列表。不过很可惜我并没有找到文档里所说的 MSEL pin
在哪里,不过从调试结果来看,0x1018处的jr指令最终会跳转到0x10000处。
继续阅读 Manual 5.1.1, 0x10000处的指令会跳转到0x20000,继续阅读Datasheet Chapter 5 “Boot code”,我们最终会跳转到地址0x20000000,查看memory map,该地址属于QSPI 0 Flash,也就是非易失存储器,看上去这里就是我们应该写入代码的地方了。(PC程序员初次看到Program counter能直接指向外部存储设备实在是刷新三观)。
向单片机写入程序的过程被称作 program(编程),所以用来编程的硬件也就叫做“编程器”了。还有一个我无论如何都无法理解的翻译叫做“仿真器”,英文叫 emulator, 这应该也是一个上古词汇,但是比起“编程器”,这个词更强调你可以调试单片机上的程序,比如下断点,单步执行等。现在一般用来指链接单片机和电脑的那根线。总而言之,你需要一个东西把单片机和电脑连起来,以便两者通信。
一般来说,这个硬件一头用JTAG等方式连接到单片机,另一头通过USB协议连接电脑,OpenOCD则是一个开源的程序允许你通过这个“编程器”以各种方式操纵各种单片机。当然也包括向 SPI Flash 中写入我们的程序。
在 RED-V 上,最大的芯片是一块 MK22FN128 的ARM单片机,这块单片机里预先写入了一个 J-Link OB 的商业程序,这个程序让这个ARM单片机实现了编程器的功能,所以我们只要直接用普通的USB线连接板子和电脑就可以了。J-Link 使用一种特殊的协议与电脑通信,好在Segger公司公开了这种协议的spec,所以OpenOCD也能和编程器通信啦。
To be continued in part 2.
]]>https://[YOUR-DOMAIN]-my.sharepoint.com/personal/[YOUR-EMAIL]/_layouts/15/onedrive.aspx?[一堆别的东西]
然后打开 F12 找到一个叫 FedAuth
的 Cookie:
FedAuth=77u/......
然后用命令行在 rclone 里添加一个 WebDAV 的远程地址。语法是这样的:
rclone config create <name> <type> <key>=<val> <key>=<val> ...
具体到这里就是:
rclone config create <随便> webdav 'url=https://[YOUR-DOMAIN].sharepoint.com/personal/[YOUR-EMAIL]/Documents' 'headers=Cookie,FedAuth=77u/...'
细节请根据浏览器里的信息自己调节,如果设置无误就可以在 Rclone 里查看文件了:
rclone lsd '<你之前填的>:'
几个注意事项:
(由于我懒得拍实物图,本文所有图都是网上找的)
我买到的是一台 FC 接口的磁带机,注意这张图里的磁带机外面还有一个转接架,是给磁带库用的,自己用需要拆掉。
对应的光纤通道 HBA 卡
和光纤线
以及不知道从哪儿来的供电线
全部接上就可以开机了。
所有以上硬件再加上 5 盘磁带大概一共花了 300 美刀左右。顺便吐槽一下 LTO-6 磁带机的拍卖价格真疯狂(600+)。
磁带机应该会在 Linux 下显示为/dev/st0
和/dev/nst0
设备,区别在于st
设备会在任何操作后将磁带倒带回开头,而nst
设备会将磁带停留在操作结束的地方。
tar
程序可以直接对设备进行操作,比如,将文件拷贝进磁带:
tar -cvf /dev/nst0 filename
将文件从磁带还原:
tar -xvf /dev/nst0
列出磁带上的文件:
tar -tf /dev/st0
更多操作可以参考这篇文档: How to archive data using the AIT2 attached to CDF17
LTO 从 LTO-5 开始支持 LTFS,你可以先将磁带“格式化”,然后用更为熟悉的目录结构来管理磁带上的文件。你需要自己从 LTFS 的 GitHub 源码 编译。Arch 用户也可以直接用 AUR 包。具体使用方法在 Quick Start 里已经写得很清楚了:
1.列出设备(如果没有可以尝试手动modprobe sg
)
sudo ltfs -o device_list
2.格式化磁带
sudo mkltfs -d /dev/sgX
3.挂载
sudo ltfs -o devname=/dev/sgX /ltfs
4.卸载
sudo umount /ltfs
磁带机买来干什么?鉴于磁带的顺序读写以及需要手动换磁带(买带库的大佬请忽略)的特性,注定了它只适合于备份和归档用途。备份像是重疾保险,你希望永远也用不上;归档是你 6 岁时的玩具,舍不得丢但也不会再用。所以如果你说 NAS 空间不够,要用磁带来存你的电影,我觉得不太行。但如果你仓鼠症发作,打算收集世界上所有的电影,磁带大概可行。
我还顺便搜集了一下 HP 的官方文档,方便参考:
最近研究了一些关于 USB4 以及 Thunderbolt 4 的资料,在此做个备忘。目前只考虑 USB Type-C 接口,并且忽略 Type-C 可以正反随意插带来的复杂性。
最后围观 USB4 混乱的速度要求被鞭尸的现场:https://youtu.be/ly5-QHjs8Gw?t=1845
Allison Sheridan: So a Thunderbolt 4 device is a USB4 device...Brad Saunders: (nodding)Allison Sheridan: ...but a USB4 device is not necessarily a Thunderbolt 4 device?Brad Saunders: It can be ...Allison Sheridan: It can be but it isn't necessarily.Brad Saunders: It'll probably have... they may have made a choice to... maybe it's only 20 Gbps.Allison Sheridan: Right, but it's Thunderbolt 4 it's 40 [Gbps] per second, Okay.
]]>WePE 自带的安装程序除了安装必要的启动项以外还会安装一些没啥用的选项。所以记录一下手动安装启动项的方法。环境为 Windows 10 64位 UEFI 启动。你需要先制作 WePE 的 ISO,然后把 ISO 里的WEPE
目录复制到随便一个分区里,我假设是D:
。用管理员身份执行以下命令:
1 | bcdedit /create /device |
设定完启动项以后就可以把盘符删掉了。
]]>Key Exchange
A related concept is PFS (Perfect Forward Secrecy). DH offers PFS while RSA cannot.
Authentication
Also known as key-signing. Commonly used together with the PKI(Public Key Infrastructure).
Encryption
Used for data confidentiality.
A related concept is mode of operation,which turns a block cipher to a stream cipher. CBC is a commonly used one. When it's used with AES, it's expressed as AES-CBC
Message authentication
Used for data integrity. These algorithms are also called MAC(Message authentication code).
Authenticated Encryption (AE)
Combines confidentiality and integrity. Wikipedia: Authenticated Encryption.
Some commonly used AE methods:
AES128-CBC-HMAC-SHA256
.Authenticated Encryption with Associated Data (AEAD)
Similar to AE, but allows extra unencrypted data (associated data) to be authenticated. Roughly speaking:
ciphertext = Encrypt(plaintext)auth_tag = Mac(associated_data + ciphertext)
A common use case for AEAD is when encrypting a network packet, you want the packet header to stay unencrypted (for network routing purposes) but still authenticated.
Note about DH and curves of EC-based algorithms
DH-based algorithms may have a “Group” option, which specifies a prime field or an elliptic curve. If a prime field is used, such as modp2048
, it's normal DH. If an elliptic curve group is used, such as ecp256
, it's EC-based DH.
Some other curves may be used:
RouterOS 做路由器,用 DHCP 给内网设备分配192.168.50.0/24
段中的 IP 地址。并且用 IPsec 的 Tunnel 模式将所有来自此 IP 段的数据转发至服务器22.22.22.22
。路由器本身的 IP 并不重要。
在本文中我主要使用 RouterOS 命令来表示具体的配置,但是实际情况下调试 WebUI 会更方便。这里提供一张简图解释怎么阅读 RouterOS 的命令:
我这里只贴 certtool 的 template 文件,具体的生成指令请参考 Part2。注意 RouterOS 似乎还不支持 Ed25519 证书,所以需要使用 ECDSA 或者是 RSA 证书。
1 | cn = "随意填, 我们使用 DNS SAN 作为 ID" |
1 | cn = "同样随意填" |
由于这是一个 Site-to-Site 连接,所以不需要 Virtual IP 了。配置基于 Part2 中的hosta.conf
,同样也是有变化的地方做了注释
1 | connections { |
这里假设读者已经会在 RouterOS 中设置 VLAN。所以只是简单的把我的 LAN 配置列出来,方便读者调试。假设使用 ether8
,bridge 名称为 LAN
,使用 VLAN=50
,子网 IP 段是 192.168.50.100-192.168.50.200
。也可以参考我以前写的一篇《访客网络配置备忘》。
1 | # 将 ether8 加入网桥 |
注意我们没有设置任何 NAT 规则,现在将一台电脑连接到ether8
上,应该能自动获取到 IP 但是上不了网。如果能上网说明你的 NAT 规则设置得太宽了,你在之后需要参考 NAT and Fasttrack bypass 设置额外的规则。
RouterOS 使用了自己的一套术语来描述 IPsec 相关的配置,我尝试了一下把 RouterOS 中的术语和 strongSwan 的配置文件内容一一对应起来:
RouterOS | strongSwan | Note |
---|---|---|
Peer | {local,remote}_addrs | |
Profile | IKE_SA parameters | No AES GCM |
Identities | local/remote 身份认证块 | |
Policy | {local,remote}_ts | 分为普通的 policy 和 policy template |
Proposal | CHILD_SA parameters | 支持 AES GCM |
ModeConfig | Address Pool, DNS, etc. | 暂时用不到。 |
Group | 似乎用来把多个 policy template 合成一组,暂时不清楚是做什么用的。 | |
Active Peer | 已建立的 IKE_SA | |
Installed SA | 已建立的 CHILD_SA | |
Key | 暂时不清楚是做什么用的 |
需要导入三个文件,先 scp 进 RouterOS 再/certificate import file-name=xxx.pem
即可,WebUI也行。导入后确认 CA 证书显示为T
,客户端证书显示为KT
。
1 | [admin@RouterOS] > /certificate print |
先设置 Profile,Proposal,Peer,Identities
1 | # 指定 IKE_SA 的 ciphersuite |
这样设定完以后应该在 Active Peers 里看到这个 Peer 处于established
状态了。然后再设置 Policy:
1 | # 这里的 {src,dst}-address 类似 {local,remote}_ts |
取决于你的防火墙配置,你可能需要显式放行所有将被 IPsec 加密的 forward 流量:
1 | /ip firewall filter add chain=forward ipsec-policy=out,ipsec action=accept |
最后在服务器上设置 masquerade 规则,应该就能上网了。你应当检查你的外部 IP 是不是确实变成了你的服务器的 IP。
ArchLinux 源里并没有这个包,又考虑到这是个 Python 程序,所以就决定直接用venv
了:
1 | mkdir ~/.config/flexget |
配置方法可以看教程也可以看官方文档。我就只简单贴一下。
1 | templates: |
1 | [Unit] |
用户登录后 5 分钟进行同步,然后每两小时检查一次。
1 | [Unit] |
一些可能会用到的命令:
1 | # 启用并运行定时器 |
你可以尝试从 AUR 安装 GNS3,但是我之前试了几次都不是太成功,于是还是把 GNS3 装进了 virtualenv 里:
1 | # 创建并进入 virtualenv |
GNS3 支持好几种方式来运行虚拟网络中的“节点”,我这里使用 Docker 来运行虚拟的 Archlinux 系统。因此也需要先安装:
1 | sudo pacman -S docker |
同时建议安装 Wireshark 方便抓包调试。
进入 GNS3 创建 Project 以后就可以尝试把左侧列表里的设备往中间画布上拖了。但是你会发现不仅设备类型少的可怜而且也基本拖不上去。这时我们需要自己创建设备节点的模板。进 Preferences,在最下面的 Docker containers 里点 New,Image name 输archlinux:base-devel
,其他的都默认,Adapters 可以按自己需要指定数量。
点 OK 后,左侧的设备列表里就多了一个设备。拖到画布上,右键 Start,再右键 Console 就可以看到 ArchLinux 的命令行界面了。
GNS3 默认使用 xterm 作为终端模拟器,如果你像我一样使用 Gnome Terminal,需要先去 General - Console applications 把启动终端的命令改成
gnome-terminal -t "%d" -- telnet "%h" "%p"
这个新创建的 Archlinux 节点还是空空如也的,除了基本的系统什么也没有,我们可以把它连接到实际的网络上,这样就能用 pacman 安装软件包了。先从设备列表里拖一个“Cloud”设备出来,然后点左侧边栏最下面的“Add a link”切换到连线模式,点 Cloud 节点,选择物理机上用于联网的接口,再点 Archlinux 节点,选择要连接的端口,这样一条连线就接好了。Archlinux 节点被直接桥接到了外部接口上,和宿主机位于同一网段。
由于 Archlinux 的镜像不带 dhcpcd 无法自动获取 IP,只能手动设置一下咯:
ip link set eth0 upip addr add 192.168.1.222/24 dev eth0ip route add default via 192.168.1.1 dev eth0echo nameserver 1.1.1.1 > /etc/resolv.conf
不过实际测试以后发现这个速度实在是很残念,可能还是要写 Dockerfile 把镜像配置好再用才行。
]]>⚠ 天坑预警 ⚠
接 Part6。折腾完 Android 以后我们来折腾一下 iOS。基本原理都是一样的,只是需要把苹果那一套 Vendor-specific 的配置选项翻译成 strongSwan 的,这样才能和服务器上的 strongSwan 互相通信嘛。由于 iOS 上的 IKEv2 客户端不是 strongSwan 而是苹果自己魔改的不知道什么版本,所以坑比起 Android 客户端更多。幸好 strongSwan 项目已经帮我们都踩了不少坑了:见 iOS (Apple iPhone, iPad…) and macOS 和 IKEv2 Configuration Profile for Apple iOS 8 and newer。
由于本 Part 是基于 Part6 的,所以我们先来列举一下会需要我们做调整的 iOS 的坑(大部分在 strongSwan 的文档里已经提到过了):
dns_name
的 SAN。基本流程是:生成密钥和证书;生成 iOS 的mobileconfig
配置描述文件;想办法把这个文件安装到 iOS 设备上;最后尝试连接。由于 iOS 的配置文件比 Android 的复杂许多,所以我写了个 Python 脚本来负责生成,如果你用 macOS 也可以从苹果下载官方的配置工具。
生成的配置文件原始模板来自 strongSwan 的文档,原文有很详细的注释,建议过一遍。另外 Apple 的开发者手册以及开发者网站也可参考。你可以在 Apple 的网站上查到每个算法都是在哪个 iOS 版本里支持的。
另外我在脚本里有两个硬编码的地方,一个是 ciphersuite 写死了是aes256gcm128-sha256-ecp256
。另一个是证书类型写死了ECDSA256
(certtool 在使用 ECDSA 生成私钥的时候的默认值)。如果你需要设置为其他值,需要直接改脚本代码。
1 | #!/usr/bin/python3 |
iOS 的客户端证书模板如下,具体的私钥生成和证书签发和 Part2 中的一致,获得ios-key.pem
,ios-cert.pem
两个文件。
1 | # common name 似乎可以随意设置 |
然后生成 p12 文件,注意:certtool 工具生成的 p12 与 iOS 有兼容性问题,可能导致无法导入,报错“容器只能包含一个证书及其密钥”,因此只能使用openssl
生成。设置的实际密码为123456
,以及这奇怪的输出格式是为了之后可以直接喂给 Python 脚本。
1 | openssl pkcs12 \ |
生成 iOS 的配置描述文件,你也可以使用苹果官方配置工具:
1 | ./ios-config-gen.py \ |
然后导入进设备就 OK。可以用python -m http.server [端口]
临时开启一个 HTTP 服务器,然后用浏览器访问这个文件即可。你说导入失败?那是 iOS 的 BUG
服务器配置基于 Part7 的配置修改,只注释变化部分。iOS 要求服务器证书的 subject 非空,所以在生成服务器证书的时候别忘了设置 common name。
1 | connections { |
然后重启 strongSwan 测试即可。连不上?Have fun time debugging.
]]>在 IKEv2 里,一方需要认证另一方基本只需要两样信息,ID
和AUTH
。ID 就是之前配置文件里反复出现的那个,AUTH 一般是使用证书私钥对某些数据的签名,具体细节请参考 RFC。证书则是把两者关联了起来:如果对方给出的 AUTH 能通过证书验证,那么对方就是这份证书所表示的那个人,同时这份证书又是颁发给 ID 的,那么对方就是 ID。
验证者要取得对方的证书有几种方式:
/etc
目录里send_cert = always
参数)send_certreq = yes
参数)。当然被验证者也可以选择不发送证书(send_cert = never
参数),只是验证会失败就是了。另外如果验证者既不询问证书(send_certreq = no
),被验证者也不主动发送证书(send_cert = ifasked
),验证一样会失败。
strongSwan 在尝试匹配 ID 和证书的时候会检查 Subject DN 和 SubjectAltName (SAN)。我们之前一直在使用 Subject DN,而 SAN 则允许我们使用域名甚至 IP 作为 ID。另外,虽然我一直称呼“域名”或是“IP”,但是实际上只要 SAN 和 ID 匹配即可,这个“域名”到底是不是我们的并没有关系。(当然只有自签才能签出这种证书)
要颁发带有 SAN 的证书只需要在 tmpl 文件(参见 Part2)中添加如下内容:
# 域名 SANdns_name="san.hosta.com"# IP 地址 SANip_address = "fd00::1"
需要注意的是,SAN 是区分类型的,比如上文的 DNS 和 IP,而 IKEv2 使用的 ID 也是分类型的(FQDN,IP,etc.)。类型需要匹配才能认证成功。有的教程会使用形如@xxx.xxx.xx.x
这样的 IP,可能原因是,某些客户端使用 IP 作为 ID 但是却标记为 FQDN,或者是生成证书的时候将 IP 标记成了域名 SAN。这种情况就需要给 IP 添加前缀@
来强制让 strongSwan 将其当作域名 ID。具体的解析规则可见 Identity Parsing 文档。
除了在证书生成上的这些零碎注意点之外,我觉得另一点导致 IKEv2 很难配置的因素是,strongSwan 允许省略很多参数,而不同的平台会给这些被省略的参数提供不同的默认值,导致你以为的设置和程序实际使用的设置出现偏差。更不用说有的平台还有一些稀奇古怪的 BUG。
这里我们尝试重复 Part6 中的实验,只是不出现 DN,全部用域名代替。同样的,只有有变化的部分才有注释。首先重新生成证书加入 SAN,你只需要重新用 CA 私钥签发服务器证书即可:
1 | cn = "server common name" |
然后先看客户端 Profile 的变化:
1 | jq -n \ |
主要变化之一是省略了 remote.id 并将 remote.addr 从 IP 改成域名。注意这里不是不设置 ID,而是采用客户端的默认值(remote.addr)。和服务器配置中的省略 remote.id(%any)有巨大不同。之二是使用 CA 证书而不是直接使用服务器证书,至于为什么需要设定 certreq 请参考第一段。
再看服务端配置变化:
1 | connections { |
这样我们就配置好使用域名作为 ID 的 strongSwan 服务器了。
]]>基本上,服务端的配置和 Part4 中的一致,这里着重介绍如何配置客户端。配置使用 Play Store 上的 strongSwan客户端,版本为 2.3.1。
首先,这个客户端似乎还不支持 ed25519 证书,因此我们需要改为使用 ecdsa 证书。完整的证书生成操作请看 Part2,在生成私钥时指定--key-type ecdsa
即可。现在假设你已经生成好了所有 6 个文件:ca-key.pem
,ca-cert.pem
,client-key.pem
,client-cert.pem
,server-key.pem
,server-cert.pem
。
另外,这次我们会在服务器上配置 NAT,所以不再需要手动在服务端配置一个 IP 了。假设服务端eth0
接口的公网 IP 是2000::1
。首先将ca-cert.pem
,server-key.pem
,server-cert.pem
三个文件移动到服务器上正确的地方。然后编写配置文件,同样的,这里只注释和 Part4 中不同的地方。
1 | connections { |
配置完毕后systemctl restart strongswan
。
在客户端上,我决定创建一个可以直接导入的 Profile 文件,避免手工输入一大堆地址(要记得我在用 IPv6)。首先需要把 client-key.pem 和 client-cert.pem 合并成一个文件,注意 bash 本身是不支持多行命令和注释穿插写的,在执行的时候需要手动移除注释:
1 | certtool \ |
新生成的文件是client-p12bundle.pem
。然后我们参照 strongSwan 的文档创建可以直接导入应用的 Profile 文件。当然你也可以手动导入证书再手动配置 Profile。
1 | jq -n \ |
最后将生成的profile.sswan
文件拷贝到手机上并导入即可。然后试一下能否成功建立连接。
然后这样配置完了,虽然能连上服务器,但是依然不能访问网络,我们还需要在服务器上配置一下 NAT。一般是一条防火墙的masquerade
规则。这里只简单把配置放出来给自己做个备忘,不做细讲:
1 | table ip nat { |
TL;DR 在两端均放行:
最基本的 IKEv2 使用 UDP 500 端口进行通信。当密钥交换完成后,Linux 内核会将加密了的数据包封装在 ESP 中发送。所以整个包结构是:
Ethernet - 外层IP - ESP - 内层IP - <...>
但是我们也知道,所有除 UDP 和 TCP 以外的四层协议都不能很好地穿过防火墙和 NAT,于是给 ESP 套一层 UDP 就是很自然的事情了:
Ethernet - 外层IP - UDP[dport=4500] - ESP - 内层IP - <...>
这层多出来的 UDP 的端口号就被人为规定成 4500 了。另外由于一些我没有搞明白的原因,strongSwan 会在某些情况下使用 4500 进行 IKE 通信,即使并不需要 ESP UDP 封装。
在默认配置下,strongSwan 需要所有三种规则:使用 UDP 500 和 4500 (即使没有 NAT)进行 IKEv2 协商,然后内核发送 ESP 包。如果有 NAT 存在,strongSwan 会使用 UDP 4500 对数据进行封装而不使用 ESP。
如果你不想放行 4500 又不想影响非 NAT 流量,可以在配置文件中设置mobike = no
。你也可以使用encap = yes
选项在没有 NAT 的环境中模拟 NAT,强制使用 UDP ESP 封装。如果你能确保你的所有 ESP 流量都是 UDP 封装的,那么不放行 ESP 也是可以的。
如果你使用不封装的 ESP,那么你需要在连接两端都放行 ESP 数据包,不然可能会出现奇怪的现象。比如,你必须先从 A ping B,然后才能从 B ping A,直接 ping 不通,之类的。
用于nftables.conf
的放行规则如下:
1 | udp dport 500 accept |
接 Part3,有了 Tunnel 模式以后我们实际使用的 IP 地址就不用受制于机器的实际 IP 了。但是手动给每个客户端手动分配一个地址显然是不切实际的。于是我们可以使用 Virtual IP 功能自动向连入的客户端分配一个内网 IP,就像 DHCP 或者 SLAAC 那样。
与之前完全对称的配置不同,使用 Virtual IP 时需要区分服务端和客户端。先在服务端配置将要分配的 IP,然后由客户端发起连接,服务端就会将配置好的 IP 分发出去。我使用 HostA 作为服务端,HostB 作为客户端。HostA 将会给 HostB 分配 IPv4 与 IPv6 各一个。使用的 Virtual IP 段是fd01::100-fd01::200
和10.10.10.100-10.10.10.150
。
hosta$ ip -br addreth1 UP fd00::1/64 fd01::1/128 10.10.10.1/32hostb$ ip -br addreth1 UP fd00::2/64
和 Part3 相比,HostA 这里有一些与之前不同的地方,一是内部 IP 全部放在了 eth1 上(而不是 lo 上);二是内部 IP 的前缀长度都是最大值;三是增加了一个 IPv4 的内部 IP,用于和分配的 IPv4 Virtual IP 通信。同时 HostB 也不再手工分配fd01
开头的内部 IP 了,将由 strongSwan 自动配置。
当连接建立后,我们应该能看到在 HostB 的 eth1 上出现两个新的,自动分配的 IP。且分配到的 IPv4 地址与 10.10.10.1 之间的通信是加密的,分配到的 IPv6 地址与 fd01::1 之间的通信是加密的。
配置文件基于 Part3 修改而来,有变化的部分已添加注释:
1 | connections { |
1 | connections { |
与 Part3 中的对称链接不同,这次你必须从客户端发起连接,也就是从 HostB 执行如下命令:swanctl -i -c child_sa
。连接建立后,你应该就能看到自动分配的 IP 了:
hostb$ ip -br addreth1 UP fd00::2/64 fd01::100/128 10.10.10.100/32
此时再从 HostB 分别 ping fd00::1
,fd01::1
和10.10.10.1
,你应该能看到前者没有加密,而后两者的 ICMP 包被包在了fd00::1 <---> fd00::2
的 IPv6-ESP 包里:
/dev/shm
:场景 | 连续读取(MiB/s) | 连续写入(MiB/s) | 4K 随机读取(kIOPS) | 4K 随机写入(kIOPS) |
---|---|---|---|---|
980 Pro 单盘 EXT4 | 6370 | 4725 | 330 | 125 |
980 Pro 单盘 BTRFS | 6264 | 2720 | 137 | 63.7 |
980 Pro mdadm RAID-0 EXT4 | 12186 | 9426 | 298 | 115 |
980 Pro BTRFS RAID0 | 5937 | 3932 | 133 | 63.3 |
/dev/shm | 6290 | 5541 | 792 | 571 |
960 Evo 单盘 XFS | 2963 | 410 | 199 | 31.3 |
Emmmm, mdadm RAID0 比内存快……这很合理……以及 btrfs 你的 RAID 性能还能更烂一点吗?
测试用设置在此,测试/dev/shm
的时候关掉了direct
以及把文件大小改成了1g
:
1 | [global] |
前两篇中我们使用的都是 Transport 模式,但是实际使用中,更常用的是 Tunnel 模式。Transport 模式只加密四层及以上数据,而不修改 IP 头,原始的 IP 头将会原样传输。这意味着我们只能进行点对点传输,因为只有一个 IP 头,我们无法告知对方服务器我们实际要访问的地址。Tunnel 模式则是连原始的 IP 头也一起加密,然后再在前端添加一个新的 IP 头,这样服务器在收到数据包后,可以解密并读取内部的 IP 头,再转发给实际的目标服务器。
这次的场景在 Part2 的基础上略有改动:在 HostA 与 HostB 的lo
接口上分别添加fd01::1/64
与fd01::2/64
:
hosta$ ip -6 -br addrlo UNKNOWN fd01::1/64 ::1/128eth1 UP fd00::1/64 [--omit--][--omit--]hostb$ ip -6 -br addrlo UNKNOWN fd01::2/64 ::1/128eth1 UP fd00::2/64 [--omit--][--omit--]
在没有建立连接的情况下,fd00::1 和 fd00::2 可互 ping,fd01::1 和 fd01::2 不可互 ping。在建立连接后,fd00::1 和 fd00::2 可互 ping,但是不加密,fd01::1 和 fd01::2 可互 ping 且流量加密。
配置文件也是在 Part2 的基础上改动而来,变化部分已加注释
1 | # hosta |
1 | connections { |
启动 strongSwan 和 Wireshark,在 HostA 上,可以 ping fd00::2 但是不能 ping fd01::2。然后用sudo swanctl -i -c child_sa
建立连接,依然可以 ping fd00::2 但是数据不加密,同时能够 ping 通 fd01::2 了。抓包可以看出明显的ETHERNET-IP-ESP-IP-ICMP
的包头层次,并且外层 IP 使用 fd00::* 进行数据传输,内层 IP 使用 fd01::* 的实际目的地址:
我们在 Part1 中看到,PSK 认证的基本思路是使用一个只有通信双方才知道的暗号,如果能确认对方确实知道这个暗号,那么认证就成功了。证书认证的思路非常不同:假设 A 需要向 B 证明自己的身份,同时 A 知道 B 信任 C,那么 A 可以向 C 索取一份“介绍信”,当 B 询问 A 的身份时,A 可以向 B 展示这份 C 出具的“介绍信”,如果 B 能够确认这份“介绍信”确实是由 C 出具的,那么认证就成功了。注意这个认证是单向的,假设 A 也信任 C,那么 B 也可以通过向 C 索取“介绍信”来向 A 证明自己的身份。在 PKI 体系中,A 和 B 持有各自的“私钥”,C 作为 Certificate Authority (CA) 向 A/B 颁发证书(即“介绍信”)。同时,CA 也会向自己颁发一份证书并分发给 A/B,A/B 使用 CA 的证书来确认 B/A 出示的证书确实为 C 所颁发。
我使用 certtool 没有什么特别的理由,你也可以用openssl
或者 strongSwan 自带的pki
工具。我在之前的一篇文章里介绍过如何使用 certtool 创建证书:借助IPsec和strongSwan建立隧道并分配IPv6地址。不过我还是决定再写一遍现在的配置。我们的配置场景和 Part1 中的相同,只不过把 PSK 认证换成了证书认证。
首先给 HostA, HostB 和 CA 分别创建私钥,我这里用的是 ed25519,一些设备可能不支持,请自行参考文档换成 RSA:
certtool --generate-privkey --key-type ed25519 --outfile ca-key.pemcerttool --generate-privkey --key-type ed25519 --outfile hosta-key.pemcerttool --generate-privkey --key-type ed25519 --outfile hostb-key.pem
生成证书时,我们需要手动指定一些证书的信息,比如证书的名称,过期时间等等。证书的 Distinguished Name (DN) 会被用来和 IKEv2 的身份标识符进行匹配,以决定具体向对方出示哪份证书(对于发送者)以及是否接受对方的证书(对于接受者)。这些信息需要写成一个 template 文件才能被 certtool 读取,详细的 template 文件格式可以在 certtool 的帮助文档里查到。template 中的键名大小写敏感。
1 | # Common name, 是 DN 的一部分 |
1 | cn = "HOSTA_COMMON_NAME" |
1 | cn = "HOSTB_COMMON_NAME" |
然后创建证书:
1 | # 生成自签名 CA 证书 |
最终会生成的 6 个文件,你可以使用certtool --key-info < hosta-key.pem
来查看私钥信息,用certtool --certificate-info < hosta-cert.pem
来查看证书信息,用certtool --verify --load-ca-certificate ca-cert.pem < hosta-cert.pem
来检查证书是否确实是由 CA 签发的。其中:
hosta-key.pem, hosta-cert.pem, ca-cert.pem
需要拷贝到 HostA 上hostb-key.pem, hostb-cert.pem, ca-cert.pem
需要拷贝到 HostB 上ca-key.pem
留在本地好好保管不要交给任何人。对于 strongSwan,私钥*-key.pem
需要放置在/etc/swanctl/private
,私钥对应的证书host*-cert.pem
需要放置在/etc/swanctl/x509
,CA 证书ca-cert.pem
需要放置在/etc/swanctl/x509ca
。
基本上和 Part1 中的配置一样,有不同之处已经加了注释
1 | connections { |
将remote.id
设置成%any
会有一定的安全问题,比如 HostA 是服务器,HostB 和 HostC 是客户端,如果 HostB 连接 HostA 的时候不检查 id,那么如果 HostC 能劫持 IP 地址,它就能假装成 HostA。毕竟 HostB 不关心它连接的到底是 A 还是 C。解决方法也很简单,指定remote.id
为CN=HOSTB_COMMON_NAME
即可。
HostB 的配置除了local.id
外完全一致
1 | connections { |
同 Part1,在任意一侧使用sudo swanctl -i -c child_sa
建立连接即可。连接建立后抓包即可看到加密流量。
严格来说 IKEv2 不是 VPN,它的全称是 Internet Key Exchange,只是一种用于交换密钥的协议罢了。密钥在计算机里一般就表示为一串固定长度的二进制数据,密钥交换就是指在两台设备之间约定一个相同的二进制串,就像两个密友之间约定暗号一样。一旦密钥交换完毕,IKE 的使命就结束了,具体怎么用约定好的密钥加密数据不是 IKE 解决的问题。在 Linux 系统上,实际的数据包加密解密是由内核的 XFRM 框架负责的,你可以使用ip xfrm
命令看到配置好的密钥以及加解密使用的算法。事实上,不使用 IKEv2 而完全手动“交换”密钥是可行的,比如朴素VPN:一个纯内核级静态隧道。你可以看到作者直接使用ip xfrm {policy,state} add
指令设定密钥,然后内核就会自动用设定的密钥加密流量。
然而,手动管理内核状态是复杂的,人工分发密钥也不怎么安全,这时就轮到 strongSwan 登场啦(或者说,任何实现了 IKE 的 Daemon 服务)。两台服务器的 strongSwan 使用 IKEv2 协议交换密钥,解决了密钥分发的问题。随后 strongSwan 会把交换得来的密钥设定进内核,这样内核就会自动加密指定的流量了。
从数据包层面上看,IKE 是7层协议,密钥交换使用特殊的 UDP 包完成。而一般被加密的数据包会使用 ESP 封装,ESP 头一般紧跟在 IP 头后。ESP 也可以被封装进 UDP 用以穿越 NAT。
内核 XFRM 的工作方式和基于 TUN 设备的 VPN 很不一样。一般基于 TUN 的 VPN 会加密所有进入 TUN 设备的流量,因此你可以直接使用路由表来控制哪些流量走 VPN,哪些不走。而 XFRM 的匹配基于策略(i.e. 源地址+目标地址+一些别的),如果某个数据包匹配到了一个策略,这个数据包就会根据这个策略指定的方式被加密。
比方说有A [fd00::1]
和B [fd00::2]
,如果你从 A 发送一个数据包到 B,普通情况下这个数据包是明文的。如果你在 A 配置了src=fd00::1,dst=fd00::2,encrypt=<...>
的策略并再发一个数据包,这个包就会自动被加密。B 收到了这个数据包,但是它并不知道该如何解密,所以你必须同时在 B 配置一条src=fd00::1,dst=fd00::2,decrypt=<...>
的策略,这样 B 才能解密。对于从 B 到 A 的流量也需要类似的两条策略。使用 IKEv2 的话,这些策略 strongSwan 都会自动帮你设置好,无需操心。于是你会发现,尽管我们仍然在使用节点本身的 IP,但是流量却已经被加密了。
对于那些必须使用路由表或是策略匹配不是很有效的场景, Route-based IPsec VPN 也是存在的。我也许会在未来的某一期讲。
IKEv2 除了交换密钥以外,还负责包括身份认证,协议协商等一系列其他工作。实际使用的时候,我们一般需要指定这些参数:
以上所有这些参数需要在两端都配置。其中,cipher suite 需要至少指定两次,原因是 IKEv2 是一个两阶段协议,两阶段使用的 cipher suite 可以不同。在第一阶段会简单地进行一次 DH 密钥交换,建立 IKE_SA,然后进入第二阶段。二阶段中的身份认证,traffic selector 协商等均会被加密。在二阶段 IKE 会建立 CHILD_SA,也是用来加密实际数据的 SA,CHILD_SA 的协商结果,包括密钥,加密算法,traffic selector 等均会被设定入内核,以便内核进行实际的加密操作。
Traffic selector 决定了内核匹配数据包的策略,即,哪些数据需要被加密。比方说,一个 VPN 客户端可能会设定 local_ts=10.0.0.14/32, remote_ts=0.0.0.0/0
这意味着该客户端希望所有流量都被加密。而一个服务器可能会设定 local_ts=<所有非中国大陆IP>, remote_ts=10.0.0.14/32
,这说明该服务器不希望处理去往中国大陆 IP 的流量。如果这两者进行协商,结果客户端就不会将去往中国大陆 IP 的流量发送给服务端。这也被称作 Split Tunneling。
这是一份简单的 Host-to-Host 配置样例,场景和没有 TUN 设备
中描述的一致。我在本地的两台物理机上进行配置,两台机器使用网线直连,并用ip addr add fd00::{1,2}/64 dev <name>
手动配置 IP。认证方式使用 PSK。在 Archlinux 上,配置文件位于/etc/swanctl/swanctl.conf
。使用sudo systemctl start strongswan
来启动 strongSwan。
这是 HostA 的配置:
1 | connections { |
HostB 的配置,几乎一样:
1 | connections { |
测试之前需要先检查一下 IP 掩码的长度,如果和我一样使用/64
的话,可能会受到bypass-lan
插件的干扰,导致数据不被加密。需要去/etc/strongswan.d/charon/bypass-lan.conf
把它关掉。然后重启 strongSwan 即可。由于我们没有配置自动连接,所以启动 strongSwan 后数据流还是未加密的:
23:50:29.873728 IP6 (flowlabel 0xe3038, hlim 64, next-header ICMPv6 (58) payload length: 64) fd00::1 > fd00::2: [icmp6 sum ok] ICMP6, echo request, seq 1 0x0000: 600e 3038 0040 3a40 fd00 0000 0000 0000 `.08.@:@........ 0x0010: 0000 0000 0000 0001 fd00 0000 0000 0000 ................ 0x0020: 0000 0000 0000 0002 8000 02ec 001d 0001 ................ 0x0030: 15ed 9c5f 0000 0000 0457 0d00 0000 0000 ..._.....W...... 0x0040: 1011 1213 1415 1617 1819 1a1b 1c1d 1e1f ................ 0x0050: 2021 2223 2425 2627 2829 2a2b 2c2d 2e2f .!"#$%&'()*+,-./ 0x0060: 3031 3233 3435 3637 01234567
需要使用 swanctl
手动建立连接:
sudo swanctl -i -c child_sa
你应该能看到 IKEv2 的四个 UDP 包,然后再 ping,数据就是加密的了:
23:51:18.731508 IP6 (flowlabel 0xe3038, hlim 64, next-header ESP (50) payload length: 100) fd00::1 > fd00::2: ESP(spi=0xc704dbda,seq=0x1), length 100 0x0000: 600e 3038 0064 3240 fd00 0000 0000 0000 `.08.d2@........ 0x0010: 0000 0000 0000 0001 fd00 0000 0000 0000 ................ 0x0020: 0000 0000 0000 0002 c704 dbda 0000 0001 ................ 0x0030: dc51 b5d7 bef1 bce6 da9d 74b2 7e6c 482d .Q........t.~lH- 0x0040: d9db 6e37 24d6 9fc6 10bb 525c e308 bc76 ..n7$.....R\...v 0x0050: 9d26 74d6 64ff ef55 5a54 5f95 94c7 01cf .&t.d..UZT_..... 0x0060: 2ae7 51b2 db41 439b 4d37 1f1e 3075 74d1 *.Q..AC.M7..0ut. 0x0070: 25dc 2990 8c07 b484 a37e b052 e5fc 8709 %.)......~.R.... 0x0080: e229 c79d 0816 0ae5 5c8b 652f .)......\.e/
在 Archlinux 上,strongSwan 的日志级别控制在/etc/strongswan.d/charon-systemd.conf
。另外,strongSwan 支持 NULL 加密(即不加密)以方便调试,将proposals
和esp_proposals
修改为如下值即可:
proposals = null-sha-modp2048esp_proposals = null-sha-modp2048
然后在 Wireshark 中选中 Attempt to detect/decode NULL encrypted ESP payloads
即可直接查看数据包内容:
Update: Windows 的引导程序似乎有些问题,如果在同一块 U 盘上写入多个 ISO 分区的话,似乎引导会错乱,最终启动的安装程序版本不是引导程序所在分区的版本。所以暂时一个 U 盘还是只能放一个 Windows 版本。垃圾巨硬。
日常折腾中总免不了要用 LiveCD 修理一下系统,或者是重装一下 Windows 之类的。这时候制作一个引导用的 U 盘基本是最方便的选项了。有不少工具都能创建 U 盘引导,比如 ArchLinux 的 ISO 镜像可以直接用dd
写入,Windows 的安装盘也能用 Rufus 创建。不过在使用上还是有些不便,比如dd
会覆盖整个U盘,在ISO之外不能再存储其他文件。Rufus 只能在 Windows 上运行,而且一只 U 盘也只能放一份 ISO。于是尝试搞明白怎么把 Linux 的 LiveCD 和 Windows 的安装 ISO 写入到同一只 U 盘就很有必要了。
我个人使用的设备都支持 UEFI,所以这里制作的启动盘也只支持 UEFI 启动,需要 MBR 模式启动的读者请往它处寻。当然,Secure Boot 是要关掉的。制作过程我使用 Linux,纯 Windows 用户现在也可以退出了。基本上,我们需要创建一个 EFI 系统分区(EFI System Partition, ESP),其中包含基本的引导程序(Grub2)和 Linux LiveCD 的 ISO 文件。由于 Windows 的安装程序无法以 ISO 形式被引导,因此我们需要给每个 Windows ISO 文件创建一个分区,并将 ISO 中的内容解压进去。但是分区一旦创建不像文件那么好修改,所以创建每个 Windows ISO 分区的时候我都留了一些额外空间,以备以后 ISO 大小变化,这也意味着这些空间就基本浪费了。That's sad but I guess it's how things work.
另外,购买一个优质的 U 盘还是有必要的,不然不管是创建启动盘还是安装系统都会慢得让你痛不欲生。建议用之前先给 U 盘测一下速,什么拷贝速度只有 2MB/s 的金士顿可以直接进垃圾桶了。至于 U 盘大小取决于你要放多少个 ISO 文件和多少个 Windows 分区,一般 Linux 镜像大小在 500MB~3GB 的都有,Windows 10 的分区一般每个需要 5~6GB.
先给 U 盘分区,用fdisk
或者别的什么工具都行。我用的 32GB 的 U 盘,分了 6GB 给 ESP,然后是另外两个 6GB 的分区给 Windows 10 的安装程序。剩下空间留着给以后使用。
然后格式化,ESP 需要 FAT32,Windows 分区用 NTFS 即可。记得多次检查盘符,不然格式化错盘就不好玩了。我给 NTFS 设置的卷标和 ISO 的一致,可以用file <img.iso>
看到,不过我不确定这是不是必须的。
sudo mkfs.fat -F 32 /dev/sdXYsudo mkfs.ntfs -f -L CPBA_X64FRE_ZH-CN_DV9 /dev/sdXY
然后把 GRUB2 安装到 ESP 分区上,假设你的 ESP 分区挂载在了$esp
:
sudo grub-install --target=x86_64-efi --removable --boot-directory=$esp/boot --efi-directory=$esp
接着复制 ISO 文件到 U 盘。Linux 的 ISO 可以直接放在 ESP 分区里的任意位置,我放在了$esp/boot/iso/
。Windows ISO 需要用 7z 之类的工具解压到 NTFS 分区里:
7z x cn_windows_10_business_editions_version_1909_updated_dec_2019_x64_dvd_262ac8af.iso -o'/run/media/recursiveg/CPBA_X64FRE_ZH-CN_DV9'
最后需要手工编写$esp/boot/grub/grub.cfg
文件。我从我系统的配置里复制了一部分图形初始化的指令,然后写了用来引导 LiveCD 和 Windows 的菜单项。一般每个发行版的启动命令都会不一样,需要自己查询。Windows 启动项中search
的--label
参数需要和格式化时设置的一样,当然你也可以用 UUID 等别的标识符。我还另外拷贝了一个 UEFI Shell 到 U 盘里。
1 | # copied from arch boot config |
全部设置好以后可以用虚拟机测试一下是不是所有项目都能正常启动,如果都没有问题就 OK 了。
]]>ArchLinux ARM 其实已经提供了树莓派的安装教程,基本上只要跟着做即可,我用的是 AArch64 镜像,并且把根文件系统从 ext4 换成了 f2fs,希望在 SD 卡上能有一点点加成效果。装完以后发现串口没有输出,自然不能忍,继续折腾。RPi 4B 一共有两个串口控制器,一个 PL011,另一个被称作 MiniUART。默认情况下,PL011 连接到蓝牙模块,并且 MiniUART 被禁用,但是我们可以通过 config.txt
加载 dtb overlay 来调整。一些常见的配置有:
但是 ArchLinux ARM 使用 U-Boot 来启动内核,并不遵循 config.txt (╯°Д°)╯ ┻━┻
那么我们只能把 U-Boot 干掉了 (<ゝω・)☆
SD 卡的 \boot
目录里需要这么7个文件,RPi 4B 的 bootloader 才好启动 Linux 内核:
config.txt
: 主要配置文件,uboot-raspberrypi
有提供,但是我们手写。start4.elf
和 fixup4.dat
: 第二阶段 Bootloader, 由 raspberrypi-bootloader
包提供。rpi4.dtb
: RPi 4B 的 Device Tree 文件, linux-aarch64
包中提供了一个基于上游代码的,位于 /boot/dtbs
。但是我试了几次都不能正常启动,所以还是从 Raspberry Pi 官方的 Github 下载了一份。Image
: 内核可执行文件,由 linux-aarch64
包提供。initramfs-linux.img
:由 mkinitcpio
程序生成。cmdline.txt
: 内核参数文件,手写。那么直接上配置:
1 | enable_uart=1 # 启用 MiniUART |
start4.elf
、fixup4.dat
和 cmdline.txt
都是原名,就无需写进 config.txt
里了。
1 | console=serial0,115200 root=PARTUUID=e10d384f-02 rootfstype=f2fs rootflags=rw elevator=deadline audit=0 rootwait |
PARTUUID
需要改成你自己的,可以用 sudo blkid
查看rootflags
f2fs 似乎默认以只读挂载,会导致没有办法登录。audit=0
关掉 audit,否则内核信息撒得满地都是。全部折腾完以后把 SD 卡塞进树莓派,应该就能在串口看到登录界面了。
我在安装的时候碰到一个 MiniUART 的 BUG,串口的 Baudrate 不对,内核输出一片乱码。可以尝试使用 PL011 作为串口,也可以升级内核解决。使用 PL011 需要在 boot 分区里加一个新的文件
overlays/disable-bt.dtbo
: 需要从 Github 上下载,用来禁用蓝牙,并且让 PL011 负责串口通信。同时需要修改 config.txt 加上 dtoverlay=disable-bt
以启用。此时 enable_uart=1
不再是必要的了。
登录之后建议先把 uboot-raspberrypi
卸了,然后 dhcpcd 连上网 pacman -Syu
一下,再重启确认一下启动过程都正常。之后就是标准的 ArchLinux 服务器配置过程:时区, 网络, 防火墙, etc. 搞定以后我们就有一台 AArch64 服务器了。
暂时没有 GUI 的需求,相关的配置就留到下次再折腾了。
]]>
由于之前陆陆续续添置了不少电子设备,以及更换 ISP 的原因,机架上连了5台设备,每台各负责一点点事情,不管是配置还是调试都很麻烦。再加上旧路由器不能很好同时处理千兆 NAT 和 VLAN,于是最近入手了一台 RB4011iGS+5HacQ2HnD-IN,把这一堆乱七八糟的设备统统换掉。主要需求有三点:
MikroTik 家的路由器的二层交换配置是比较不统一的。受限制于不同产品的硬件,想要完全利用硬件交换,不同的型号在 Bridge 的设定上都略有不同。建议到 MikroTik Wiki: Switch Chip Features 页面查询具体型号的配置方法。由于我的大部分内网流量还是要过 CPU 三层路由的,所以我没有在这一点上做特别优化,反正 RB4011 的性能够用。我这里以两个 VLAN,每个 VLAN 里各有一个 Ethernet 接口和一个 Wireless 接口为例。
1 | /interface wireless |
1 | # IPv4 DHCP 客户端 |
1 | /ip firewall nat |
1 | /ipv6 firewall filter |
配置完了以后我对这一套设备还是挺满意的。在 IPv6 没有 Fasttrack 只能纯 CPU 转发的情况下,双向同时 900Mbps 测速,CPU 占用在 80% 左右。发热也没有什么感觉,反正平时一直丢角落里,估计整台机器最烫的部分就是那个 SFP+ 的万兆收发器了吧。
]]>本文中的所有图片及文字均使用 CC0 发布,可任意转载使用。
本文属于疫情期间的摸鱼之作,旨在推广 RAR 压缩格式的正确压缩方法,让资源分享更轻松一些。
先说一下为什么用 WinRAR 而不用 7zip, 因为 7zip 没有恢复记录。在百度网盘等平台分享文件时,文件可能发生损坏,没有恢复记录的话只能尝试重新下载浪费时间,而有恢复记录的话有大概率可以成功修复,正确解压。
国内特供版的 WinRAR 可以免费使用,但是有广告。如果不想要广告,你可以从以下链接下载官方无广告简体中文版
https://www.win-rar.com/fileadmin/winrar-versions/sc/sc20200409/rrlb/winrar-x64-590sc.exe
注意,如果你没有rarreg.key
文件进行注册的话依然是有广告的。至于具体的注册方法请自行搜寻。
恢复记录需要在创建压缩文件时添加,你需要先勾选添加恢复记录
复选框。
然后检查恢复记录的百分比设置。
对于大于 100MB 的大型文件文件来说,默认的 3% 足够使用。如果你的文件非常小,比如只有 几MB 或者 十几MB 你可以考虑增加到 5%。增大这项设置会同时增大文件体积,因此不建议设置得过大,尤其是对于几个 GB 的文件来说。如果你在压缩时忘记添加恢复记录或者是想要修改恢复记录的大小,也可以在事后进行操作:
如果你在解压时遇到“校验和错误”,那么你下载到的压缩文件就是损坏了:
你可以使用“工具”菜单尝试修复它。修复操作会生成一个新的,修复好的压缩包,你需要选择这个新文件的保存位置:
稍等片刻就会生成一个修复后的文件:
你也有可能在修复过程中遇到错误,但只要修复后的文件可以正确解压不报错,就没有问题。
但是只有添加了恢复记录的文件可以使用修复操作,因此所有人在压缩时都添加恢复记录是非常重要的。
如果修复后的文件依然不能正确解压,要么是损坏的部分过多无法修复,要么是资源发布者没有添加恢复记录。
在这种情况下就只能重新下载试试了。
给压缩文件加密是防止文件被和谐的重要方法之一。
如果你要分享一个非常巨大的压缩包,比如说十几个GB,直接作为一个文件分享一般不是一个好主意,因为下载者有可能下载到中途失败,不得不从头再来。将大压缩包切分成多个较小的文件可以有效减少这种情况的发生。
rar
而不是unrar
。用到的库列表:
1 |
|
在开始写代码之前,我们需要先准备好模拟器用于运行 Linux。我这里用了QEMU,你也可以用 VirtualBox 之类的虚拟机。一般来说,我们需要一个内核可执行文件(内核本体)和一个 initramfs 镜像(提供用户态程序)。当内核被加载后,它会自动载入 initramfs 镜像,并执行其中的/init
程序。由于这一节的重点是 QEMU 配置,所以我直接使用宿主机的内核和 initramfs。以我的 Archlinux 系统为例,内核文件是/boot/vmlinuz-linux
,initramfs 文件是/boot/initramfs-linux.img
。使用以下命令启动 QEMU,按Ctrl-A x
退出。
1 | qemu-system-x86_64 \ |
-kernel
指定内核可执行文件。-initrd
指定 initramfs disk 镜像文件。-nographic -append "console=ttyS0"
禁用视频输出并使用串口作为终端。-m 512
设定内存。--enable-kvm
使用KVM。-cpu host
使用宿主机的 CPU 特性。你应该能看到一些系统启动的信息以及无法挂载根分区的报错,这是正常现象,因为我们没有提供任何磁盘文件。你应该可以进入一个紧急修复 Shell, 执行一些简单的如 ls
cd
之类的命令。
确保 QEMU 能够正常工作后,我们来创建自己 initramfs 替换掉上一步中的/boot/initramfs-linux.img
。这一节的内容主要来自 Creating a initramfs image from scratch 一文。initramfs 镜像本质上是一个经过压缩的 cpio 归档文件(cpio 约等于 tar)。根据内核版本的不同,你可以使用不同的压缩算法。我的内核比较新,所以可以使用lz4压缩。同时,我们直接下载使用编译好的busybox
可执行文件作为我们的 Shell。
1 | mkdir initramfs_root # 创建initramfs的根目录 |
这样,一个自定义的initramfs.cpio.lz4
文件就构建好了。用其替换掉/boot/initramfs-linux.img
1 | qemu-system-x86_64 \ |
这样你应该就能得到一条欢迎信息和一个 Shell。由于我们没有创建常用命令到busybox
的软链接,所以执行指令时需要加上busybox
的前缀。以下是我的运行结果。
Hello from initramfssh: can't access tty; job control turned off/ # busybox lsbin dev etc init lib mnt proc root sbin sys tmp var/ #
替换掉 initramfs 镜像后,我们接着来替换内核。自己编译内核大概分三步:下载代码;配置内核;编译内核。Linux 的内核源代码可以在 https://www.kernel.org/ 下载。下载并解压到某个文件夹,比方说linux-5.5-rc5
。通常来说手动配置内核是个苦差事,于是我们直接使用默认配置就好。默认配置文件位于arch/x86/configs/x86_64_defconfig
。
linux-5.5-rc5$ make x86_64_defconfig # 使用64位默认配置...此处省略若干行...linux-5.5-rc5$ make -j10 # 请根据你电脑的核心数量调整...此处省略若干行...Kernel: arch/x86/boot/bzImage is ready (#1)
依葫芦画瓢,用这个编译好的bzImage
替换掉/boot/vmlinuz-linux
,你应该得到和之前一样的欢迎信息和 Shell。
使用 C 语言写 Hello world! 是一码事,不用 glibc 写 Hello world! 是另一码事。在我们创建的 initramfs 中,我们没有包含任何类似glibc
的 C 标准库,因此我们不能使用诸如printf()
之类的方便函数,需要直接使用系统调用进行输出。从汇编层面来说,要进行系统调用,只需要将系统调用号以及参数统统塞入一系列寄存器,然后执行syscall
指令即可。更详细的我在几年前的一篇《Programming with PTRACE, Part4 - 系统调用进阶》里有讲到。那么直接贴代码:
1 |
|
两个值得注意的点:使用_start()
而不是main()
;exit()
系统调用不能省。另外这个程序编译的时候也需要一些特殊的技巧,需要静态链接并且不需要标准库支持:
gcc -nostdlib -static helloworld.c -fno-stack-protector -o helloworld
将编译出的文件放入initramfs_root
并重新打包,然后在 QEMU 中运行就能看到效果了。
Hello from initramfssh: can't access tty; job control turned off/ # /bin/helloworldHello world!
至此,我们已经知道了如何在 QEMU 中启动一个包含内核和用户空间程序的 Linux 系统,并且知道了如何利用系统调用让用户态程序输出字符串。有兴趣的读者可以尝试给内核添加一个新的系统调用,当被调用时向 kernel log 输出Hello kernel world!
。就像这样:
/ # /bin/helloworld_syscall[ 4.621241] Hello kernel world!
]]>没什么好说的,AMD YES 就完事儿了。
主板选了微星的 TRX40 PRO 10G (我就是不喜欢 Asus 你来打我啊)。10G 版比 WIFI 少了无线模块,多了一张万兆以太网卡。不过不管哪样我都暂时用不上。另外还有一张 PCI-E x8 转两个 M.2 的转接卡,没那么多 SSD,同样用不上就是了。除此以外就是说明书、光污染线之类的玩意儿。
极度非主流的内存配置,4根英睿达的 16G ECC 内存。型号为CT16G4WFD8266
。
于是先把 CPU 和内存装上主板:
随便选的 九州神风堡垒360EX。少数几个当时能以合理价格买到的支持 TR4 的一体水冷。
另外现在的风扇都这么花哨了吗,怎么扇叶上都装扰流板了?
随便选的 海韵 Focus GX-850。少数几个当时能以合理价格买到的便宜的 850W 电源。另外这电源还送了一个测试器,本质就是把 ATX 接头里的某两根线短接一下,用以测试电源本身能不能工作。但是我为什么不直接把它插到主板上去测试呢?
显卡方面,AMD 好像不是很香,但我还是选了 RX590。别问为什么,问就是 Fuck you NVIDIA。
之前提到用不到万兆网卡,一是没有万兆交换机,二是有捡垃圾捡回来的 40G Infiniband 卡,台式机和 NAS 之间用 QSPF 线直连,速度反正比机械硬盘快就是了。顺带把旧机器上的系统盘也拆过来。
机箱选了毫无灯光的 Fractal Design Define C,这个机箱没有前置的 USB Type-C 接口比较可惜。显卡的 PCI-E 供电线荡在那儿还是有点丑。
背面走线随便走走,机箱能合上就是胜利。
新机器插电一次亮,不过意料之中的,进不去系统。根据 Phoronix 的报道,需要在内核参数中添加mce=off
才能正常启动。然后进 BIOS 把虚拟化、IOMMU、ECC 之类的都开起来,这机器就算装完了。随意跑了一下s-tui
,48个线程满载还是蛮恐怖的。
一般空载整机功耗 100W 左右。单烤 CPU 全核频率在 4GHz 左右,整机功耗 400W,想必加上显卡后能轻松过 500W,电费也要开始燃烧了。另外这接近 80℃ 的温度感觉有点高,难道是水冷没装好?
装机总结:AMD YES
]]>用任何你喜欢的方法安装 Glasgow Haskell Compiler (a.k.a. GHC)。Cabal 之类的
依赖管理系统就用不着了。 因为我也不会用。 保证能够执行ghc
和ghci
命令就行。
首先,把以下文件保存成helloworld.hs
。
1 | foo = "hello, world" |
然后执行ghci helloworld.hs
,然后在>
提示符后输入foo
并回车
1 | GHCi, version 8.6.3: http://www.haskell.org/ghc/ :? for help |
你可以使用:r
来重新载入文件,也可以使用:l <文件名>
来载入代码。
你可以修改你的代码文件,并在GHCi中观察程序行为。
1 | -- 这里是注释 |
这里仅列出了极少数基本用法。更多关于语言本身的以及数据结构的特定语法规则请参考相关Haskell教程。
Haskell 是一门具有类型推导的静态类型语言。每个表达式都有自己的类型,在 GHCi 的交互模式下,可以使用:t <表达式>
来检查表达式的类型。类型注记通常写为<表达式> :: <类型>
。
1 | -- 布尔型 |
函数类型以 (参数1的类型) -> (参数2的类型) -> ... -> (返回值的类型)
形式表达。不明显区分参数和返回值。Num
是代表数字的类型类,可以近似理解成Java中的接口。Num a =>
表示 “在后续的类型定义中,a
可以被替换成任何满足Num
的类型”。整数Int
和浮点数Float
都是Num
类型类的成员,所以加法函数既可以将整数相加,也可以将浮点数相加。
类型箭头都是右结合,但是你可以手动添加括号来改变类型的意义
1 | -- 接受一个整型参数,返回一个新函数。这个新函数接受一个整型,返回一个整型 |
Haskell 中定义新类型的基本语法是data <新类型名> [类型参数..] = <构造函数1> [成员类型...] | <构造函数2> [成员类型...] | ...
。类型名和构造函数都需要首字母大写。
1 | data NameAndAge = MakeNameAndAge String Int |
注意:a
是一个类型,ListOf a
是一个类型,但是 ListOf 不是类型,ListOf 不是类型,ListOf 不是类型。 这个定义表示:ListOf a
满足Eq
仅当 a
满足Eq
。要查看类型的相关信息,可以在 GHCi 中执行:info
指令。
1 | *Main> :info Eq |
Maybe a
类似Java中的Optional<T>
,常用于表示“会失败”的函数。
1 | data Maybe a = Nothing | Just a |
Monad
是个类型类
1 | *Main> :info Monad |
简化一下
1 | class Monad m where |
当我们说Maybe
是一个Monad
的时候,一方面指 Maybe 属于 Monad 这个类型类instance Monad Maybe where ...
。另一方面指Maybe(数据结构)
,(>>=)::Maybe a -> (a -> Maybe b) -> Maybe b (函数)
,return::a -> Maybe a (函数)
这三者构成了一个满足某些条件的数学结构,这些条件被称为Monad Laws。事实上,Haskell编译器不会检查 Monad Laws 是否满足,你可以胡乱写一些数据结构和函数,然后将其塞入 Monad 这个类型类中。换句话说,Haskell中的Monad就是一个接口,任何实现接口的数据类型都可以称其为Monad。
回到Maybe
上,现在你想把这两个会失败的函数连接在一起
1 | func3 :: Float -> Maybe Bool |
看上去不错,我们需要一种操作,能把任意两个可失败的函数连在一起,这样以后再碰到这种情况直接复用就行了。如果第一个函数类型是a->Maybe b
,第二个函数类型是b->Maybe c
,那么复合函数的类型应该是a->Maybe c
1 | composite :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c) |
看上去不错,不过有个小问题,执行的第一步g x
并不需要composite
函数来操心,完全可以由调用者算好了传进来,于是我们再简化下
1 | composite :: (b -> Maybe c) -> (Maybe b) -> (Maybe c) |
只要交换一下两个参数的顺序,我们就有了Monad Maybe
的>>=
函数。对于Maybe
来说,它恰好有一个操作能满足Monad的定义,于是Maybe
就是一个Monad。
do
是Haskell中的一个关于>>=
的语法糖,考虑有多个“可失败”函数需要调用的情况
1 | f1 x = Just (x+1) |
do
会按照规则展开成>>=
函数
1 | do x <- expr |
因此两者是等价的。但是do
语法更加整齐易于阅读。
常去的某资源站的某资源发布者喜欢把重要的内容加上花里胡哨的特殊效果并藏在页面的角落里。
虽说要尊重资源的发布者,不过这种给人添堵的行为实在令我感到不爽,于是研究了一下Chrome的扩展程序(Extension)。
要干的事情有两件:
很自然的,直接用Javascript操纵DOM树即可实现希望的效果。
那么要怎么自动载入脚本呢?
感谢Chrome提供了强大的扩展系统。自动载入脚本这种功能自然是小菜一碟啦。
首先编写一个脚本content_script.js
操纵页面元素:
1 | if (page_require_modification()) { |
没有魔法,想干啥就写啥,就像是HTML本身引用了一个JS文件一样。也不需要考虑document.ready
的问题,因为Chrome默认会在文档加载完成后再加载自定义的JS。
接着需要一个manifest.json
文件,这样Chrome才能将其作为一个Extension加载。
1 | { |
你觉得我会把实际的URL写出来嘛?肯定不会啦!
Chrome把这种注入到页面中的脚本称做content_scripts
。当页面的URL符合matches
中的pattern时,就自动加载js
中指定的脚本。当然,脚本的文件名可以自由决定,只要前后一致即可。
最后一步,将manifest.json
和content_script.js
放入同一个文件夹。然后在chrome://extensions
选择加载已解压的扩展程序
即可加载扩展啦。
manifest.json
文件指示了一个Chrome扩展在一年前,我写过一篇文章,介绍利用GRE隧道将一台服务器的IPv6地址“分配”给另一台电脑,令其能访问IPv6网络的方法。
不过那种方法存在一些问题:
于是热爱折腾作死的我研究了一下使用IPsec配合IKEv2对流量进行加密的方法。
服务器与本地均为ArchLinux(Arch大法好),strongSwan软件包可从AUR安装。
服务器需要至少有一个公网IPv4和一段Routed IPv6 Subnet。
我们一共需要三对“密钥-证书”对:
我使用了ECC证书,因为其具有更短的长度。如果老版本不支持ECC的,也可以使用RSA证书。
先生成三把私钥:
certtool --generate-privkey --ecc --outfile ca.keycerttool --generate-privkey --ecc --outfile server.keycerttool --generate-privkey --ecc --outfile client.key
然后自签名CA证书,Common Name
可以随意填,但是和之后的配置一定要统一:
certtool --generate-self-signed --load-privkey ca.key --outfile ca.crt
接着再用CA证书签名其它两把密钥,Common Name
同样可以随意填,但是不要一样:
certtool --generate-certificate --load-ca-privkey ca.key --load-ca-certificate ca.crt --load-privkey server.key --outfile server.crtcerttool --generate-certificate --load-ca-privkey ca.key --load-ca-certificate ca.crt --load-privkey client.key --outfile client.crt
这样就一共产生了六个文件,保存备用。
首先需要把密钥文件放到对应的位置:
ca.crt
放入/etc/ipsec.d/cacerts/
server.key
放入/etc/ipsec.d/private/
server.crt
放入/etc/ipsec.d/certs/
然后编辑/etc/ipsec.secrets
文件,注意空格
"CN=IPsec server" : ECDSA "server.key"
前面CN=...
那一串是证书的Subject,CN即Common Name,可以通过certtool -i < server.crt
查看。
最后编辑/etc/ipsec.conf
文件:
1 | config setup |
然后打开IPv6 Forwarding并启动服务
sudo sysctl net.ipv6.conf.all.forwarding=1sudo systemctl start strongswan
步骤基本相同。
ca.crt
放入/etc/ipsec.d/cacerts/
client.key
放入/etc/ipsec.d/private/
client.crt
放入/etc/ipsec.d/certs/
ipsec.secrets
为"CN=..." : ECDSA "client.key"
编辑/etc/ipsec.conf
1 | config setup |
然后执行sudo ipsec start --nofork
,如果出现keeping connection path
字样应该就连接成功了。网卡上会出现一个新的IPv6地址,然后就可以直接访问IPv6网络了。
如果连接不成功或者是无法访问网络,可以考虑检查一下防火墙是不是把数据包drop了。
1 | struct addrinfo *listen_addr; //存放解析结果。参见`man getaddrinfo` |
这种方式只能同时处理一个连接
1 | int fd = socket(AF_INET, SOCK_STREAM, 0); // int socket(int domain, int type, int protocol); 参见`man 3 socket` 创建文件描述符, 出错返回-1 |
1 | struct addrinfo *server_addr; |
1 | char *payload = "hello" |
对于每一个请求fork()一个新的进程进行处理。
1 | while(1) { |
1 | int epollfd = epoll_create(1024); |
ip_gre
支持。远程主机有一段/64的IPv6,我将其中的一段/80分配给自己的机器。$server_ipv4
$client_ipv4
。a:b:c:d::/64
a:b:c:d:e::/80
脚本如下,需要root,建议用sudo -i
:
1 | ip tunnel add gre-tunnel mode gre remote $client_ipv4 ttl 64 |
gre-tunnel
是隧道名称,可以按自己喜欢的来,记得其他的也要一起改脚本如下,和服务端配置几乎一样,同样需要root:
1 | ip tunnel add gre-tunnel mode gre remote $server_ipv4 ttl 64 |
现在,两台机器应该可以互ping了。有的比较奇葩的情况可能需要手动ip link set gre0 up
一下,gre0似乎是内核模块自动加入的玩意儿,具体怎么回事我也不清楚–_–|
但是现在还不能访问外网,还需要在服务器执行以下命令:
1 | sysctl net.ipv6.conf.all.forwarding=1 |
第一行开启forward
二三行和IPv6的NDP(邻居发现)有关,又是个没搞明白的东西真是残念……
eth0是服务器实际连接网络的接口。
要删除Tunnel,在两端均执行:
ip link set gre-tunnel downip tunnel del gre-tunnel
如果客户端IP变化:
ip tunnel change gre-tunnel remote $new_client_ipv4
虽然叫做“隧道”,但是内容依然是明文,对保密要求高的同学们要注意了。
另外直接用命令建立的隧道在重启后会没有,所以可以考虑用networkd之类的东西来管理。
Linux在访问有IPv6地址的域名时会优先使用IPv6,所以要当心服务器流量爆炸。当然配置成IPv4优先也是可以的。
如果你的本地IPv4经常变动的话,你可能需要些脚本之类的东西自动更新服务器的Remote IP。
对于每一个新的IP(新的设备),都需要在服务端执行ip -6 neigh add
,有知道怎么解决这个问题的请务必留言…
本文作于2014年末,其中记载的方法可能已经过期,请读者谨慎参考
见过不少教程都是基于Eclipse的,而基于IDEA的文章少得可怜,遂决定写此文。
本文通篇基于Linux/IntellijIDEA进行讲解,Windows/MAC/Eclipse用户请自行依葫芦画瓢。
当然,你得首先去MinecraftForge下载一份源代码。我这里用的是最新的forge-1.7.10-10.13.2.1258-src.zip
接着,找个地方建立一个文件夹,这将是你的工程目录,我的叫做Forge1.7.10-1258
。然后再在里面建立一个目录,比方说就叫forge-1.7.10-10.13.2.1258-src
,把你的Forge源码解压进去。
现在,你的文件夹层次应该看起来是这样的:
Forge1.7.10-1258└── forge-1.7.10-10.13.2.1258-src ├── build.gradle ├── CREDITS-fml.txt ├── eclipse ├── forge-1.7.10-10.13.2.1258-changelog.txt ├── gradle ├── gradlew ├── gradlew.bat ├── LICENSE-fml.txt ├── MinecraftForge-Credits.txt ├── MinecraftForge-License.txt ├── README.txt └── src
你可以先按自己喜好改动一下build.gradle
。比如,我喜欢手动指定一下mappings的版本:
1 | minecraft { |
接着,cd到forge-1.7.10-10.13.2.1258-src目录下,执行如下两条命令:
gradle -i setupDecompWorkspacegradle -i ideaModule
然后请耐心等待指令完成,可以去喝杯牛奶睡个觉什么的。
等以上操作完成后,就可以打开IDEA,选“Create New Project”,注意要建立一个空工程(Empty Project)
“Project Name”自然可以随意填写,”Project Location”则是之前创建的目录,下方的”Project Format”推荐选”Directory Based”
点”Finish”之后应该会自动打开”Project Structure”窗口,如果没有的话可以按Ctrl+Alt+Shift+S或是从菜单栏”File –> Project Structure”
打开窗口之后我们首先要选择SDK版本:先点左边的”Project”,然后在右边”Project SDK”里选一个,我是选了Java8,你当然可以选择任何版本(不要低于Java6)
接着点左边的”Modules”,再点那个绿色的“+”号,接着选”Import Module”
然后选forge-1.7.10-10.13.2.1258-src目录下的forge-1.7.10-10.13.2.1258-src.iml
文件就好。
Import完了之后检查下有没有报错,如果没问题就可以点右下角OK。
重新cd到forge-1.7.10-10.13.2.1258-src目录下,执行如下三条命令让gradle自动建立运行配置。
1 | ln -s ../.idea . |
回到IDEA,然后有必要的话重新加载一下Project。继续选菜单栏”Run –> Edit Configurations”,点左侧的”Minecraft Client”,修改”Working Directory”到forge-1.7.10-10.13.2.1258-src/eclipse
。
你也可以像我一样指定一个username。接着对”Minecraft Server”也如法炮制。然后右下角”OK”退出。
现在Forge应该就可以运行了,在IDEA的主界面右上角有这么一片区域,选”Minecraft Client”然后点右侧那个绿色的三角箭头即可
如果之前的步骤都没有问题,你就可以继续了
为了更好地演示运行配置以及发布流程,我决定写一个Mod,添加一种矿石:“Xp Ore”,顾名思义,挖掉后能得到大量经验。
我们需要新建一个Module来写我们的代码:菜单栏”File –> New Module”
我就叫XpOre好了,然后继续打开”Project Structure”,将forge-1.7.10-10.13.2.1258-src
添加成为它的依赖。
那个菜单同样是点右边的绿色加号出来,点”Module Dependency”后在弹出来的窗口里选”forge-1.7.10-10.13.2.1258-src”然后”OK”即可。
之后就可以写Mod了!至于具体Mod怎么写我就不在这里提了,请各位参考其他文章。
这是我的代码和目录层次结构,请自行调整,我就不把每一步的细节都写出来了。
请记得把java
和resources
两个目录设置成代码根目录和资源根目录,具体方法是在文件夹上右键然后”Mark Directory As”
(xp_ore.png是矿石的材质,其实就是拿金矿石的材质把几个像素涂成绿色让它看起来比较像附魔瓶的颜色)
为了防止图挂,我再拿文本形式列一下目录
XpOre├── src│ └── main│ ├── java (Sources Root)│ │ └── org│ │ └── devinprogress│ │ └── xpore│ │ └── XpOre.java│ └── resources (Resources Root)│ ├── assets│ │ └── xpore│ │ ├── lang│ │ │ └── en_US.lang│ │ └── textures│ │ └── blocks│ │ └── xp_ore.png│ └── mcmod.info└── XpOre.iml
要想让这个Mod在IDEA里运行起来,有两种方式。第一种比较简单,直接菜单栏”Run –> Edit Configurations –> ‘Minecraft Client’ –> Use classpath of mod …”下拉列表里选”XpOre”,保存退出运行即可。这种方式比较适合只开发一个Mod的情况。
当有N个Module互相依赖的时候,我推荐创建另一个Module,比方说,叫”Run”。然后令其依赖forge-1.7.10-10.13.2.1258-src
和你需要加载的其他Module,然后”Use classpath of mod”选择”Run”即可。
发布其实相当方便,把forge-1.7.10-10.13.2.1258-src文件夹里的build.gradle
复制到XpOre
文件夹下面
按自己喜好修改其中的version
,group
和archivesBaseName
即可。比方说我的是:
version = "v0.1"group= "org.devinprogress.xpore"archivesBaseName = "XpOre-1.7.10"
然后在XpOre文件夹下gradle build
即可。运行完成后,就能在XpOre/build/libs/
文件夹下找到编译好的jar了。
最后来一张Mod的效果图
关于这篇文章,不适合特别特别新的新人,我假设各位读者都有一些基础的编程经验。如果你是入门级别的,在MCBBS论坛的编程开发板块有不少不错的入门教程。
我假设各位读者都具备以下能力:
然后再来介绍一下要用到的工具:
一般来说,FML都会附带在Forge里,在某些情况下,比如现在(2014年11月5日)1.8的Forge还未完成,但FML已放出,就可以单独只安装FML,先开始Coremod的开发。
网上大部分教程都是讲Eclipse的,但是个人偏好IDEA,所以讲一下IDEA的配置流程。
安装IDEA,没有必要找破解版,免费的Community Edition足够
下载Forge代码,就是Src那个链接。请选择自己需要的版本,我以1.7.10-Recommended
为例
解压到一个你看着顺眼的地方,然后依次执行以下命令
gradle setupDecompWorkspacegradle ideagradle genIntellijRun
没有装gradle也不想装的,可以用./gradlew
强烈建议挂着代理或VPN做这事,否则将是极端痛苦的过程。
你也可以加上-i
选项看滚滚的数据输出以不至于那么无聊。
打开IDEA,直接Open Project,选择目录下的.ipr文件应该就好了,你可以试着Run一下看看有没有什么问题。
注:直接gradle idea
现在是不被推荐的,可以尝试用gradle ideaModule
代替,具体方法在我的另一篇日志里有讲。
如果需要Socks代理的,可以这么来gradle -DsocksProxyHost={代理服务器地址} -DsocksProxyPort={代理端口}
源代码和资源文件都是放在src
文件夹里的,有时要在多个不同的Mod间切换开发,我目前的解决方法是将代码统一放在别处,将src文件夹做软链接进来。同时我也非常推荐也用这种方法处理build.gradle
文件。将代码放在别处还有个好处,就是可以用git
来管理版本,而且可以用分支方便地管理对不同Minecraft版本做的修改。不管什么方式,自己习惯就好。
用了gradle ideaModule
后,代码本身就分开放置了,不再需要这种方法了。
对于开发这一部分,自己深感无力(其实就是懒),请参阅szszss的系列教程。
另外,现在已经没有Coremod文件夹了,所以所有Mod都放在Mods文件夹下,不同之处只在于MANIFEST.MF
文件。
感谢ForgeGradle,打包发布不再需要手动拷贝压缩一大堆文件了。首先,你需要修改下build.gradle
文件,这也是我为什么推荐用软链接来管理它的原因。
以原始的文件为例:
1 | version = "1.0" |
archivesBaseName
和第一行version
都可以自由修改,只会影响输出的jar文件的名字,关于group
用途不明,有了解的求留言告知。
如果你打算把Mod升级到一个新的Forge版本,请务必修改minecraft.version
和你的开发环境一致,否则会出现奇奇怪怪的问题。
修改好后,就可以用gradle build
来编译了,同样建议开代理。编译好的jar在build/libs
下。
如果是需要对MANIFEST进行修改的,比如Coremod,需要在build.gradle
中minecraft块之后添加jar块:
1 | jar { |
如果你的一个jar包里既有普通Mod(以@Mod
作Annotation的)又有Coremod,你还需要
attributes 'FMLCorePluginContainsFMLMod': true
否则普通Mod不会被载入。
因为MCP坑爹的反混淆机制,开发者在处理Method或Field时需要对付三种不同的名字:
a
这样的混淆名,obfName
func_xxxx_a
这样的半混淆名,有时也称作srgName
doTick
这样的反混淆名,或称mcpName
关于为什么要有srgName,MCP是这么解释的:因为mcpName是任何人都可以贡献的(这是真的),所以会出现这么一种情况,有时为了更好地描述某个函数的功能,在次要版本升级时(比如1.7.1升级1.7.2),mcpName会发生变化,如果直接以mcpName进行编译,那么为1.7.1编译的Mod就无法在1.7.2上使用,即使其他方面都没有问题。于是为了解决这个问题,引入了相对固定的srgName。FML是这么处理名称的,在gradle build
时,代码中所有的mcpName均会被混淆成srgName。然后在玩家运行游戏时,所有的obfName均被反混淆成srgName,即RuntimeDeobfuscation
,运行时反混淆。
在你修改某个方法之前,首先必须定位它(废话)。定位一个方法需要四个信息:
类很好确定,每当一个新的类被加载时,都会调用IClassTransformer
接口的transform
方法:
1 | public interface IClassTransformer { |
第二个参数就是反混淆了的类名,并且是点分割,大小写正确的,可以直接用equals()
来判断。
但是方法名的判断就比较复杂,因为在ASM转换时,运行时反混淆还没有被执行,所以方法名全部都是obfName。更要命的是,如果方法的参数里有Minecraft的类,那么这个类名也是被混淆了的类名。
不过谢天谢地,我们有FMLDeobfuscatingRemapper
,你可以用FMLDeobfuscatingRemapper.INSTANCE
来取得实例。这个类提供了几个重要的方法:
1 | String mapMethodName(String obfedClassName, String obfedMethodName, String obfedMethodDescription) |
其中mapMethodName
和mapFieldName
返回对应方法和字段的srgName,mapMethodDesc
返回反混淆了的Description,也就是将(Lbee;F)V
这种变为(Lnet/minecraft/client/gui/GuiMainMenu;F)V
。
如果你需要在某个方法中添加大段的代码,我极度不推荐写长长的代码将所有这些操作码全部加到目标方法里去,这种方法枯燥至极,又不直观,还难于调试。我一般的方法是,使用INVOKESTATIC
调用自己写好的函数,并将需要修改的变量作为参数传递,这样需要的代码不多,也易于调试和维护。
在向代码中添加操作,尤其是费时的操作时(比如网络IO)请务必谨慎选择插入的位置。因为大部分代码会在主线程中执行,一旦卡住轻则界面冻结,重则直接被服务器超时踢出。
我不是非常推荐让ASM自动计算栈大小和本地变量区大小,因为碰到一些比较复杂的类时会悲剧,比如AbstractClientPlayer
如果你想清空一个方法让它什么都不做,请还是不要忘记加上RETURN
有时,开发环境下编译出的class和原始的class会有区别,所以还是建议用javap之类的工具看一下原始的字节码。
有时,我们需要频繁调用某个private的Method或是Field,使用反射会有性能损失,而ASMTransformer也无效(因为无法通过编译),这就到了AccessTransformer大显身手的时候了。
AccessTransformer用于将private或是protected的Method和Field变为public,这样在代码中就可以直接使用了。
你需要首先创建一个*_at.cfg
的配置文件放在resources目录(就是放mcmod.info
的目录)下,然后将其软链接到根目录下(和build.gradle
同目录)。
配置文件的语法类似这样(这是fml_at.cfg
的一部分)
1 | public net.minecraft.entity.EntityList func_75618_a(Ljava/lang/Class;Ljava/lang/String;I)V |
接着重建工作区:
gradle clean setupDecompWorkspace idea --refresh-dependencies
同样建议挂代理,用eclipse的同学把idea
换成eclipse
,有强迫症的同学可以加上-i
选项。这样,开发环境下代码的改动就完成了。我在挂着代理的情况下大约需要8分钟。
为了让其在混淆环境下也能正常工作,你需要创建一个新的类,继承AccessTransformer
:
1 | public class MyATransformer extends AccessTransformer{ |
其中的字符串就是配置文件名,然后在实现了IFMLLoadingPlugin
的类里:
1 |
|
另外需要注意的是,AccessTransformer不会自动转换衍生类,所以在转换基类时请务必当心,否则会编译不通过。
在1.7.10及以上的版本中,可以在build.gradle文件的中加入以下内容,这样就不必再写IFMLLoadingPlugin了。其中的mymod_at.cfg
文件要放在META-INF文件夹下。
1 | jar { |
PRIVACYPLEASE
超好记有木有!程序运行会占用一小段时间(废话),事实上,我们有不止一种方法来表示一个程序运行了多长时间。最直观的应该是“墙上时间”,也就是说,你掐个秒表,看看程序从开始到结束用了多长时间。除此之外,还有“用户态时间”和“内核态时间”,这两个时间都是以CPU实际运算的时间,也就是CPU周期,来计数的。“用户态时间”就是程序在用户态执行的时间,包括程序所引用的库中的代码(比如STL),“内核态时间”就是指程序在内核态执行的时间,一般是各种系统调用(比如各种IO操作)。这两种时间和墙上时间的区别在于,因为CPU其实是在多个程序中快速切换的,所以在运行某个程序的时间里,CPU也处理了属于其他进程的任务,而且CPU切换任务也需要一定的时间(真的很短)。如果处于被调试状态,tracer的运行时间也会被计算在内,这些不属于这个进程的时间片也会被计算在这个进程的“墙上时间”里。所以一般以用户态时间和内核态时间的总和作为进程的运行时间。
在Linux系统里有一个叫time
的命令可以查看一个命令执行了多长时间。这个命令有两个版本,一个是shell内置的,另一个是独立的可执行文件,可以用type time
命令查看。虽然可执行版本功能更强一点,但内置的功能足够,这一点区别可以不管。用法是: time [命令] <参数>
。给个例子:
time ffmpeg -i sample.mp4 target.mp3...5.42s user0.10s system100% cpu5.520 total
还在对上个PART的setrlimit
耿耿于怀么?我们现在就来用它!相关的定义位于sys/resource.h
头文件里。我们这次要用到RLIMIT_CPU
,这个选项限制进程所能占用的CPU时间,以秒为单位,可以把它理解为用户态时间和内核态时间的和。我们首先要使用getrlimit
获得当前的限制:
struct rlimit TimeL;getrlimit(RLIMIT_CPU,&TimeL);
rlimit
结构有两个成员:
rlim_cur
软限制rlim_max
硬限制系统一般会用比较平和的方式对待那些达到软限制的进程,比如发个SIGSEGV什么的。而那些达到硬限制的进程会被直接SIGKILL。我们接下来要修改软限制,注意单位是秒。
TimeL.rlim_cur=Timeout;
以上工作都要在fork()
之前完成,之后要在子进程里应用这个限制(没错就是exec那里)
setrlimit(RLIMIT_CPU,&TimeL);
这样,如果子进程超过软限制,系统就会发送SIGXCPU
信号给子进程。当然,因为ptrace的原因,信号会被先发送给父进程,这样就可以用part3里介绍的方法进行处理。这样子进程是要清蒸还是油炸就都由父进程决定了。
当然,我们还有别的方法获取时间信息。一是用gettimeofday()
函数配合timeval
结构,可以获得当前时间,精确到微秒(百万分之一秒)。在程序开始时调用下,结束时调用下,相减即可得到墙上时间。另一种方法是利用wait4
里的ru
参数,它其实是个rusage
结构,成员见此。其中的ru_utime
和ru_stime
成员是timeval
结构,分别记录了用户态时间和内核态时间,同样精确到微秒。
RLIMIT_CPU
大多数情况下都能正常工作,配合timeval
结构甚至能进一步提高精度。但是有两个例外(如果有更多请务必告诉我):
sleep()
scanf()
一类的函数等待键盘输入在这两种情况下:进程不占用CPU时间,RLIMIT_CPU
管不着;没有系统调用,wait4()
不返回。为了能够在这种情况下依然能够限制时间,我想出了两种方法。一是限制和sleep()
相关的系统调用,二是父进程设置ALARM。我在这里讲一下第二种方法。
Linux提供了一个alarm()
函数,可以在指定的秒数(墙上时间)后给这个进程本身发送SIGALRM
信号。而且,我们可以给信号绑定一个处理函数(就是当信号到达时调用的函数),在这个处理函数里,可以用kill
命令给子进程发送信号(比如SIGUSR1
),这样就能使父进程里的wait4()
返回,就可以控制子进程了。以下是一个简要指导:
首先我们需要一个信号处理函数,记得把pid改成全局变量:
void AlarmIn(int sig){ if(sig==SIGALRM) kill(pid,SIGUSR1);}
然后在子程序开始执行的时候绑定信号并设置Alarm,我在这设置超时一秒:
signal(SIGALRM,AlarmIn);alarm(1);
然后请根据part3所讲的内容在while循环里正确处理SIGUSR1
。最后记得取消Alarm,如果没超时的话:
alarm(0);
表示完整代码太长了,放这儿太不美观,我会稍后贴到gist上去。代码被幽幽子吃掉了大家自己写把。
ms
然后和毫秒搞混(嘛。。。这一部分也算是现学现卖的,如果大家觉得有什么讲的不到位的请翻下方的拓展阅读部分)
大家都知道,32位系统最大可以寻址4GB的地址空间(不考虑物理地址扩展),那么这个“地址”究竟指的是哪儿的地址呢?你可以写一个小程序,malloc一点内存,然后把地址打印出来,重复几次,你会发现,分配的内存几乎都在同一个位置。这是因为,对于程序来说,这些地址都是虚拟地址,虚拟地址空间对于每个进程都是独立的,也就是说,对于不同的进程,同样虚拟地址上的数据是不同的。
当然,数据肯定是存放在内存条上的,我们把可以直接读写内存条的地址叫做物理地址。物理地址以一定的方式映射到虚拟地址上,所以当程序试图访问虚拟地址时,系统要以一定方式把虚拟地址变成物理地址,这项工作通常是由MMU(内存管理单元)来完成的。内存的映射不是大块大块的,而是一小片一小片分别映射的,所以在虚拟地址上连续的地址可能在物理地址上相差十万八千里,这些一小片一小片的内存被称为“页”。
页的存在给内存分配带来了极大的灵活性,页可以存储在内存里,也可以存储在交换分区里,可以将同一块物理内存映射到不同进程的虚拟空间里(动态库经常这么干),甚至可以映射到磁盘上的某个文件。光说可能有点抽象,于是给幅图(来自Wikipedia)
虚拟地址被分成多个段,数据有序存放于其中。这是32位Linux的新内存布局(Linux 2.6.7之后):
如果你研究过可执行文件的结构,你就会发现,虚拟地址的段就是按可执行文件的段来填充的。另外,由于代码段的起点地址是固定的(0x08048000),所以编译器就可以预先算出函数的地址了。顺带一提,因为动态库加载时的虚拟地址是不固定的,不能预先计算出函数地址,所以要在编译时使用-fPIC
选项生成位置无关代码,否则每次被一个新进程使用时都要进行重定位(可以理解为重新计算函数地址),并生成该动态库的一个副本,这样压根没有起到节约内存的作用。
扯远了,回来。尽管每个进程的虚拟地址空间时互相独立的,但并不意味着进程想访问哪儿就能访问的,比如3GB以上的区域,那儿是内核的领地。即使是堆段,也只能访问已申请的内存部分,非法的内存访问将会引发段错误(Segmentation Fault)。回到malloc()
函数上,malloc最终会调用brk
或mmap
系统调用,brk用于在堆中分配小块内存,mmap则用于在Memory Mapping Segment中分配大块内存。但是并不是每次malloc都会调用brk,这是因为分配的内存实在是太小了,而brk只能分配大一点的内存,所以C运行库(比如glibc)在收到一个malloc时会先用brk向系统“批发”一块大一点的内存,而收到后续分配请求时则把这块大内存“零售”给程序,直到售完再次brk。
如果有一个程序死循环单纯malloc内存,内存会不会被吃光呢?答案是不会(我不清楚是不是真的有如此单纯的系统真的会挂掉),因为系统发现,你只是分配了内存,却没有使用,于是它很机智地将那片内存设置为“可访问”,却没有把它映射到任何一个实际的内存页上!
还记得之前的rusage
结构么?其成员可在这里找到。事实上,这是一种非常简陋的内存使用信息获取方式,我们只关心其中的ru_maxrss
一项,RSS即”Resident Set Size”,表示该进程在物理内存中的占用大小,不包括交换分区中的内存大小,也不包含分配了却未使用而没有物理内存页的内存。为了获得更详细的内存信息,我们需要访问/proc
目录。该目录下各文件的用途在man 5 proc
里描述得很清楚,这里是网页版本。关于这个目录的作用,我就偷懒,将man手册中的描述翻译如下:
proc
文件系统是一个伪文件系统,提供了访问内核数据结构的接口。它通常被挂载在/proc
上并且大部分是只读的,除了少数文件被允许用来改变内核参数。
/proc
下有N多文件夹,大部分是按进程的pid来命名的,我们关心的是这些文件夹中的status
文件。来看一个例子:cat /proc/1/status|grep Vm
VmPeak: 173616 kBVmSize: 107968 kBVmLck: 0 kBVmPin: 0 kBVmHWM: 3816 kBVmRSS: 3744 kBVmData: 83744 kBVmStk: 136 kBVmExe: 1140 kBVmLib: 2268 kBVmPTE: 72 kBVmSwap: 0 kB
我们看到了两个令人感兴趣的东西:VmData
和VmStk
。分别代表了数据区和栈的大小,而且这两个数据是真正的可访问的虚拟内存大小,即不会像RSS那样,漏掉那些分配了而未访问的内存。当然,其他数据也都是很有趣的,有兴趣的人可以自己去翻man手册。这段代码计算给定进程的数据段和堆栈段内存使用总和。
1 | long getMemory(const pid_t pid){ |
也许你们已经知道,有一个叫做setrlimit
的函数可以用来限制资源使用,你们也许已经翻过了它的man手册,看到了RLIMIT_AS
RLIMIT_DATA
RLIMIT_RSS
等一票似乎很有用的参数。现在,请你立刻忘掉他们!既然我们之前讲了/proc
当然要用起来啦。我们不用setrlimit
是因为,这种限制策略会导致malloc失败(确切的讲是brk和mmap失败),而大部分OIer都没有检查malloc返回值的的习惯,最终导致本应是MLE(Memory Limit Exceeded)的情况变成了由访问无效内存导致的RE(Runtime Error)。更糟糕的是,如果是系统栈增长被限制了,进程会被直接SIGSEGV
,连errno都没有,这种情况下就更难分辨了。那么,有什么好的方法来限制内存呢?答案就是在每次分配内存的系统调用(不限于brk和mmap)时通过proc来检查内存使用,注意要在返回时检查哦。一旦超过,就由父进程直接杀死子进程,方法多种多样,你可以使用ptrace(PTRACE_KILL,pid,0,0)
,或是用Part3所讲的方法发送信号,或是直接用kill函数。这种方法看上去很不优雅,但确实很有效。至于那些对setrlimit
耿耿于怀的同学,不要担心,下个part时间限制,将会大量用到。
我在Part2中提到过,系统调用的参数是以一定顺序保存在寄存器里的,那么这个顺序是什么呢?在man 2 syscall
中有两张表格解释了这个问题,你也可以在这里看到,就在”Architecture calling conventions”下面。我知道很多人很懒,所以我就把这两张表格复制过来了。
arch/ABI | instruction | syscall # | retval | Notes |
---|---|---|---|---|
arm/OABI | swi NR | - | a1 | NR is syscall # |
arm/EABI | swi 0x0 | r7 | r0 | |
blackfin | excpt 0x0 | P0 | R0 | |
i386 | int $0x80 | eax | eax | |
ia64 | break 0x100000 | r15 | r10/r8 | bool error/errno value |
parisc | ble 0x100(%sr2, %r0) | r20 | r28 | |
s390 | svc 0 | r1 | r2 | See below |
s390x | svc 0 | r1 | r2 | See below |
sparc/32 | t 0x10 | g1 | o0 | |
sparc/64 | t 0x6d | g1 | o0 | |
x86_64 | syscall | rax | rax |
arch/ABI | arg1 | arg2 | arg3 | arg4 | arg5 | arg6 | arg7 |
---|---|---|---|---|---|---|---|
arm/OABI | a1 | a2 | a3 | a4 | v1 | v2 | v3 |
arm/EABI | r0 | r1 | r2 | r3 | r4 | r5 | r6 |
blackfin | R0 | R1 | R2 | R3 | R4 | R5 | - |
i386 | ebx | ecx | edx | esi | edi | ebp | - |
ia64 | out0 | out1 | out2 | out3 | out4 | out5 | - |
parisc | r26 | r25 | r24 | r23 | r22 | r21 | - |
s390 | r2 | r3 | r4 | r5 | r6 | r7 | - |
s390x | r2 | r3 | r4 | r5 | r6 | r7 | - |
sparc/32 | o0 | o1 | o2 | o3 | o4 | o5 | - |
sparc/64 | o0 | o1 | o2 | o3 | o4 | o5 | - |
x86_64 | rdi | rsi | rdx | r10 | r8 | r9 | - |
对于32位系统,系统调用号存放在EAX寄存器,参数依次放入EBX、ECX、EDX、ESI … 返回值位于EAX寄存器
对于64位系统,系统调用号存放在RAX寄存器,参数依次放入RDI、RSI、RDX、R10 … 返回值位于RAX寄存器
以64位系统下的write()
调用为例:
ssize_t write(int fd, const void *buf, size_t count);
那么RAX是1(write的调用号),RDI一般为1(stdout),RSI存储着指向用户空间中将要被输出的字符串的地址,RDX自然就是字符串长度啦。
理论讲完了,进入实战。这次我们拿open()
系统调用开刀,一是因为监视程序打开了什么文件比得知输出了什么更常用,二是因为传递给open()
的字符串没有长度信息,只能自己通过\0
判断,更有挑战性。我们这次要使用ptrace的一个新功能PTRACE_PEEKTEXT
,其实还有另外一个叫做PTRACE_PEEKDATA
的,不过根据man手册的描述,这两个的功能是一样的。它的用法是这样的
data = ptrace(PTRACE_PEEKTEXT,pid,addr,0);
即从子进程(由pid标识)的addr内存地址处取出对应字长(64位为8字节,32位4字节)的数据,做为返回值。也就是说,读取一次能得到八个字符。现在如果我们要取得从base_addr
地址开始的一个字符串,那么我们只要8个字节8个字节读取,直到碰到\0
为止。把这个功能写成函数就是这样:(32位系统不要忘记改那个define)
1 |
|
主程序的大while()
循环里的代码是这样的(我已经设置了PTRACE_O_TRACESYSGOOD
标记):
1 | wait4(pid,&sta,0,&ru); |
在这里我使用的PTRACE_GETREGS
和user_regs_struct
结构来一次性获得所有寄存器的值,该结构定义于sys/user.h
头文件中。另外,我还使用了SYS_open
来判断系统调用号,避免了Magic Number。SYS_*
宏定义于sys/syscall.h
头文件中。传递RDI寄存器也很容易理解,查询man 2 open
可知open系统调用的路径是第一个参数。现在,重新编译你的target
,不要加-static
,然后运行,你应该能看到类似这样的输出。
Parent startedChild PiD == 4717Child exec...Child execve() returned with 0open() opened: /usr/lib/tls/x86_64/libc.so.6open() opened: /usr/lib/tls/libc.so.6open() opened: /usr/lib/x86_64/libc.so.6open() opened: /usr/lib/libc.so.6Hello World!Exited with code 0
可以很明显的看到程序搜索动态链接库的过程。
如果你觉得这还不够过瘾,那么你可以看Playing with ptrace, part1,后面提供了一个配合使用PTRACE_PEEKTEXT
和PTRACE_POKETEXT
来将write
输出的字符串反转的例子
不知不觉已经写到Part4了,期间一边查资料一边写代码做验证一边写这篇文章,又发现了好多好多之前遗漏的信息和好文章。同时深深感觉自己真是个蒟蒻,好多东西觉得很重要,想讲却心有余而力不足,而且越来越像是在翻译man手册了……我是不是一开始就应该去翻译手册而不是写这系列文章呢?(笑)
下个part开始,估计就要暂时和ptrce说再见,然后和内存管理开始较劲了。
ptrace
获得系统调用信息,即在一个大循环里不断获取程序信息,如果程序退出则停止循环。当然,那个判断异常简陋,几乎无法处理任何特殊情况。我将在本Part中详细解说各种异常情况的处理,同时讲解各种信号相关的问题。在使用wait4
后,程序的信息被存储在sta
变量中,这些信息被存储在这个整数的不同二进制位上,这儿有一系列宏用于帮我们提取这些信息。以下信息是我对man 3 wait
中相关部分的翻译,同时参考了这个页面
WIFEXITED 如果进程正常退出,返回一个非0值(通常是进程调用了`exit()`或是`_exit()`)WIFSIGNALED 如果进程由于一个未被捕获的信号而被终止,返回一个非0值WIFSTOPPED 当进程被停止(非终止)时,返回一个非0值(通常发生在当进程处于`traced`状态时)WEXITSTATUS 当`WIFEXITED`为非0值,获得进程`main()`函数的返回值WTERMSIG 如果`WIFSIGNALED`为非0值,获得引起进程终止的信号代码WSTOPSIG 如果`WIFSTOPPED`为非0值,获得引起进程停止的信号代码
除了这六个,还有WIFCONTINUED
和WCOREDUMP
两个宏,不过我们用不到,我也没仔细研究,就不说了。
当进程自行终止时,WIFEXITED
即为true
,配套使用WEXITSTATUS
获得返回值,不做过多解释。当子进程进行系统调用时,WIFSTOPPED
为true
,同时WSTOPSIG
等于SIGTRAP
(信号代码为7),我们可以用这种方法区分syscall-stop
和signal-delivery-stop
。当有一个外部信号要发送给子进程,这个信号会先到达父进程,使WIFSTOPPED
为true
,同时WSTOPSIG
等于该信号的信号代码。父进程可以选择将这个信号继续传递或是不传递,甚至传递另一个信号给子进程。一旦信号真正到达子进程,就进入子进程自己的处理流程或是系统默认动作,可能触发WIFSIGNALED
,比如SIGINT
。
在所有信号中,SIGKILL
是一个例外,它不会经过父进程引发WIFSTOPPED
,而是直接传递到子进程,引发WIFSIGNALED
。
之前提到,父进程需要将信号传递给子进程,这是由ptrace(PTRACE_SYSCALL,pid,0,0)
的第四个参数决定的。如果为0,就不传递信号,否则传递对应代码的信号,比如ptrace(PTRACE_SYSCALL,pid,0,9)
就将信号9(SIGKILL)传递给了子进程。
修改信号简直信手拈来,传一个你想要传的信号即可。
strsignal()
接受一个整数参数,返回const char*
,用于把信号代码变为对应的、人类可读的字符串描述,定义于string.h
。下面给出判断程序退出的代码:
1 | wait4(pid,&sta,0,&ru); |
你也许会纠结,如果外部传递了一个SIGTRAP
信号,那么如何分辨呢?答案是使用PTRACE_SETOPTIONS
设置PTRACE_O_TRACESYSGOOD
标记,即在while之前,第一个wait之后,第一个PTRACE_SYSCALL
之前,使用ptrace(PTRACE_SETOPTIONS,pid,0,PTRACE_O_TRACESYSGOOD)
。这会使得syscall-stop
导致的WSTOPSIG
从SIGTRAP
变为SIGTRAP|0x80
,而普通的来自外部的SIGTRAP
依然是SIGTRAP
。
给32位系统的Tip
手动修改源代码)1 |
|
当然,如果你试图直接编译并运行上面这段程序肯定是失败的,因为你缺少一个用于被执行的“target”(就是execlp里的那个)。在这里,我们的第一个target是最经典的“Hello World!”程序:
1 |
|
然后,我建议你静态方式进行链接:
gcc -static target.c -o target
注意到我这里使用了-static
参数,它的作用是将c运行时库静态链接入可执行文件中。你可以比较一下用两种方式编译的文件大小(几K和几百K的区别)。虽然用动态链接也可以,但是会和我之后的输出有一点点出入(因为动态链接文件需要根据环境变量搜索动态库)。现在把target
和demo4.c
放在同一目录下,然后
gcc demo4.c -o demo4 && ./demo4
如果运行正确,你应该看到类似如下的输出
Parent startedChild PiD == 9702Child sleeping...Child exec...Child execve() returned with 0Entering SYSCALL 63 .... Exited with 0Entering SYSCALL 12 .... Exited with 31248384Entering SYSCALL 12 .... Exited with 31252928Entering SYSCALL 158 .... Exited with 0Entering SYSCALL 89 .... Exited with 55Entering SYSCALL 12 .... Exited with 31388096Entering SYSCALL 12 .... Exited with 31391744Entering SYSCALL 5 .... Exited with 0Entering SYSCALL 9 .... Exited with 140378408579072Hello World!Entering SYSCALL 1 .... Exited with 13Entering SYSCALL 231 .... Child Exited
我先介绍一下各个头文件的用途:
stdio.h
:(如果你不知道这个文件是干嘛的请重学C语言)unistd.h
:提供fork()
、pid_t
、execlp()
、sleep()
等sys/ptrace.h
:提供ptrace相关函数和宏定义sys/wait.h
:提供wait4()
和WIFEXITED
宏sys/resource.h
:提供rusage
结构定义sys/reg.h
:提供寄存器系列宏定义(ORIG_RAX
等)看到代码的第15行,一个巨大的if...else...
将代码清晰地分成了父子进程两个部分,16行的ptrace(PTRACE_TRACEME,0,0,0);
首先吸引了我们的注意力。(为什么有一种在写春游作文的感觉)这个调用使得子进程被标记为TRACED
并且使系统内核在子进程调用exec族函数之后通知父进程,这也是为什么17到19行的系统调用没有被追踪到的原因。
再看父进程部分,由于系统调用是一个从用户态到内核态再到用户态的过程,所以每进行一次系统调用都会触发两次syscall_stop
,分别是进入时的syscall_enter_stop
和离开内核时的syscall_exit_stop
。这种子进程的状态的变化可以在父进程中使用wait()
、waitpid()
、wait4()
等一票函数完成(还记得part1课后阅读中的僵尸进程么?)。值得注意的是,第一次的状态变化是由execve()
调用返回导致的syscall_exit_stop
,所以我在25到29行单独做了处理。我喜欢使用wait4()
的原因是它还可以获得子进程的当前资源占用情况(就是那个rusage
结构),这对于了解进程资源使用情况非常有用,只不过现在还用不到(我应该会在之后专门开几个part来讲系统资源的限制),所以我们只要关注那个sta
就可以了。
注意下面那个大的while
循环,在每次循环的开头等待,一旦wait4()
返回,子进程就已经进入了暂停的状态(其实是内核给子进程发送了SIGTRAP
信号,但因为子进程处于TRACED
状态,所以这个信号被转交给了父进程,使父进程的wait4()
返回,但这也意味着由其他方式引起的信号(比如kill
命令)也会引起wait4的返回)。接着在32行使用WIFEXITED
宏加上wait4收集的状态信息sta
判断子进程是否已经退出,如果已退出,那么父进程也从循环中退出。当然这是一个非常粗糙的处理方式,更具完整的处理流程将在之后的part里介绍。接着,我们就可以使用各种各样的命令来调戏子进程了,这里我们只是简单的取得系统调用号和返回值。最后,在45行,让子进程继续执行,并要求子进程在下一个系统调用(进入或返回)停住,然后父进程开始等待下一次的syscall_stop
。因为一次系统调用会导致两次syscall_stop
,所以我使用变量intocall
来分辨,并且在38到44行打印出不同的提示信息。顺带提一下,在输出中,Hello World!
应该输出在Entering SYSCALL 1
和Exited with 13
中间,但因为缓冲区刷新的问题所以被输出到了前面。
终于到最激动人心的部分了!36、37两行代码是最重要的部分,可以看出,他们做的工作是差不多的,都是从子进程的内存空间中取一些数据。为了解释好这两行,我要讲一讲系统调用的调用过程。系统调用和普通的函数调用差不多,函数调用是将参数以约定好的顺序压入栈中,而系统调用则发生了一个类似上下文切换的过程:程序将需要调用的系统调用的调用号以及参数存入寄存器中,然后将所有寄存器存入栈中,进入内核态后,内核从栈中取得调用号和调用参数,并将返回值写入栈中对应寄存器的位置,最后还原寄存器的值并返回用户态,于是返回值就这样被“还原”到了寄存器里。在x86-64平台上,负责传递系统调用号和返回值的都是RAX
寄存器,也就是说返回值会覆盖调用号,为了在系统调用返回时也能知道调用号,RAX
寄存器在保存时被入栈两遍,一个是用于保存返回值的RAX
,另一个是负责保存调用号的ORIG_RAX
。现在,我们要获得寄存器的值,只要访问栈中的对应位置就可以了。而系统内核又会在系统调用时将栈中的这些信息复制一遍到一个叫做u-area
(USER Area)的内存区域。在sys/reg.h
头文件中定义了各寄存器保存时在u-area
中的顺序,乘以每个寄存器的长度(64位系统自然就是8了嘛~~)就得到了我们所要访问的字节偏移量,PTRACE_PEEKUSER
要求ptrace从指定偏移取出一个寄存器长度的数据(也就是8字节)作为返回值,于是ptrace(PTRACE_PEEKUSER,pid,8*ORIG_RAX,0)
就能获得系统调用号啦!
要让程序通过编译,需要做两个改动:
在int main()
之前加入这两个预处理命令:
#define RAX EAX#define ORIG_RAX ORIG_EAX
把26、36、37行ptrace第三个参数中的8全部改成4
这是因为32位系统的寄存器长度是4字节,而且负责传递系统调用号和返回值的是EAX
寄存器。
strace ./target
man 7 signal
asm/unistd_64.h
头文件中找到,asm/unistd_32.h
就是32位的本人作为一个信息学竞赛的参与者,在很久之前曾经试图自己写过一个Online Judge系统(允许用户上传源代码并在服务器上编译运行),考虑到安全因素,必须要对程序的行为进行限制,因此对ptrace进行了一番研究。网上有一份关于ptrace的很好的教程(Playing with ptrace),但是时间有点久了,而且没有涉及64位操作系统。因此,我决定写这份教程,基于64位Linux,尽力介绍一些新加入的功能,同时兼顾一下32位系统。另外,由于一开始的目的是“对程序的行为进行限制”,所以不会涉及到诸如设置断点之类的内容,相反,可能会涉及到其他关于系统资源管理的内容。ptrace()
是一个由Linux内核提供的系统调用。它允许一个用户态进程检查、修改另一个进程的内存和寄存器。这种技术被广泛用于gdb
等调试器中。尽管这系列文章的标题叫做“Programming with PTRACE”,但在第一部分中,我将着重介绍Linux的进程和相关的几个重要函数。
在Linux中,每一个进程都有一个唯一的编号,被称作pid
(Process ID)。在Linux中,进程不能凭空产生(init
进程是个例外),只能从一个已有进程衍生出来。原来的进程被称做父进程,衍生出来的进程叫子进程。一个系统中所有进程以父子关系相连接,形成一棵树,这棵“树”的树根就是init
进程,它是在系统启动时被直接启动的,因此它没有父进程。并且系统中所有其他进程都直接或间接地是它的子进程。在Linux系统中,实现“把一个进程变成两个”这一功能的有三个系统调用,即fork()
、vfork()
和clone()
。
fork()
的工作流程的确和叉子有几分相似之处,它将当前进程所有数据复制一份,产生一个和父进程一模一样的子进程。并在两个进程中返回不同的返回值。比如这段代码:
1 |
|
将会输出
Program started.fork() returned 5768fork() returned 0
很明显地可以看到,puts()
只被调用了一次而printf()
被调用了两次,这说明在fork()
前的一个进程变成了两个,而且fork()
在两个进程中有不同的返回值(这就是“调用一次,返回两次”的来历)。fork()
会返回0给子进程,返回子进程的pid给父进程,因此,我们很容易判断出fork() returned 0
是由子进程打印的。在实际应用中,也通过if
语句判断返回值的方法来决定执行不同的代码:
int pid=fork();if (pid==0){ //子进程的工作}else{ //父进程的工作}
一般来说,子进程的工作就是调用exec
族函数,启动另一个程序(把自己替换掉)。如果子进程还在执行而父进程已结束,那么它就成为“孤儿”进程,成为init
进程的子进程。另外,请不要纠结那个if
判断带来的性能损失,Linux的内核开发者都不纠结,你纠结什么呢?
vfork()
的存在是一个历史遗留问题,在很久很久以前,fork()
调用是没有CoW机制的,如果fork出的一个子进程又立即调用了exec
族函数,那么辛辛苦苦拷贝出来的内存又立马被扔进了废纸篓里(这个比喻可能不太恰当,毕竟被从内存里抹去的数据是捡不回来的)。Linux的开发者当然不会允许效率如此低下的事情发生,于是他们创造出了vfork()
。它和fork()
最大的差别在于,vfork出的子进程,在执行exec
族函数前和父进程共享同一块内存。也就是说,子进程对内存的修改也会体现在父进程上。只有当子进程执行了exec
族函数,它才真正拥有一块属于自己的内存。这样就节省了fork()
中那个无意义的内存拷贝。现在因为有了CoW,fork()
和vfork()
已经几乎没有性能差异了。
1 |
|
这段代码输出,而且一定输出
X=1Child-X=2Parent-X=3
很好地说明了内存的共享,如果换成fork()
,那么父子进程就都输出X=2了。
也许有人会问,为什么不可能是父进程先输出呢?这涉及到vfork()
的另一个特点。如果使用vfork()
创建进程,那么在子进程使用exec
族函数或是_exit()
(这就是我为什么不用return 0
的原因,但没有详细研究过原因,求大神指教)之前,父进程会始终等待vfork返回。比如以下代码:
1 |
|
输出
Child Sleeping...//这里等了3秒Child Exit.Parent Exit.
而改成fork()
后输出
Parent Exit.Child Sleeping...~$//这里等了3秒Child Exit.
可以明显看出两者差别。(给Windows用户的Tip: 那个~$
是Linux终端的提示符,类似cmd)
clone()
函数提供了更多的控制选项,可自由决定要执行哪个代码片段甚至是哪些内存共享,哪些内存要复制。但我没怎么用过,不敢乱说,有兴趣的读者可以自行实验。
我在这篇文章之前的部分N次提到了一个叫exec族函数
的东西,如果我们man手册里查找(man 3 exec
),我们会得到一大堆函数(是不是开始感到困惑了?):
int execl (const char *path, const char *arg, ...);int execlp (const char *file, const char *arg, ...);int execle (const char *path, const char *arg, ..., char * const envp[]);int execv (const char *path, char *const argv[]);int execve (const char *path, char *const argv[], char *const envp[]);int execvp (const char *file, char *const argv[]);int execvpe(const char *file, char *const argv[], char *const envp[]);
exec族函数
就是这一“族”函数,全部以exec打头,他们都是对系统调用execve()
的包装。他们的作用就是把某个进程(通常是fork出来的子进程)从里到外,完完整整,包括代码、堆栈,全部换成另一个程序,然后从头开始运行。它们的调用效果是一样的,区别在于调用方式。总的来说,大致的参数顺序是这样的:exec*(可执行文件路径,程序参数表[,环境变量表])
,其中环境变量表是可选的。
去掉打头的exec,带l
(代表list)的函数使用了一种比较接近人类方法来表示程序参数表,即以NULL
作为结尾(man手册推荐使用(char *)0
)的变参列表;而带v
(代表vector)的则使用一个字符串数组来表示程序参数表,就像int main(int argc,char *argv[])
里的argv
一样。
如果结尾带e
(environment),则该函数接受一个字符串数组表示的环境变量表;反之,则会默认传递所有当前环境变量。如果带有p
,那么你就不必在第一个参数中列出完整路径,系统会自动检查当前目录和PATH
环境变量(如果你非要手贱加个路径分割符进去,那么系统就会把它当成完整路径)。
值得一提的是,不管你使用那种方法表示程序参数表,第0个参数(C的数组下标从0开始,记得么?)都应当和可执行文件路径保持一致,虽然不一致依然可以正确运行,但有可能出现奇奇怪怪的问题。(博主继续偷懒,欢迎各位读者当小白鼠自行实验)。如果你已经混乱了,或是直接跳过了上面的一大堆说明直接到了这,那么我推荐你直接使用execlp()
函数,比如说,你要运行一个叫foo
的程序:
execlp("foo","foo",NULL);
或是列举出根目录下所有文件:
execlp("ls","ls","/",NULL);
从本系列的下一篇开始,我将要开始讨论ptraec()
这一强大的工具。但是,如果你有一下现象之一的,我建议你不要继续阅读并且从头学习有关*nix
系列系统的知识:
寄存器
,堆栈
的另外,ptrace()
相当接近系统底层,对内核版本,系统构架,指令长度,库头文件等有相当大的依赖性,如果你还在使用2.x系列的内核,你可能在之后遇到问题,因为一些功能在新版本内核才被加入。我在这里列出我的编程环境:
另外,这里有更多关于进程的文章
FFmpeg
、ImageMagick
和一点点的编程小技巧就可以轻松完成。第一步当然是要去下一个视频文件,我已经下好了,叫做BadApple.mkv
。
第二步要把视频变成一帧一帧的图片,请出FFmpeg来帮忙:
ffmpeg -i BadApple.mkv -s 80x60 -r 15 Ba%d.png
然后你就会得到Ba1.png Ba2.png Ba3.png
等一大堆文件,这就是各帧了。注意我在这一步同时把大小缩小到了80*60和把帧速率调到了15帧每秒。
第三步用ImageMagick将图像转换成黑白图,然后再转换成xpm
格式。XPM格式本质上是一个文本文档,可以直接被#include
。我们这一步要用到一点点脚本技巧。
1 | for x in *.png |
最后写一段C语言小程序,利用游程编码进一步缩小文件体积。
1 | #include <stdio.h> |
当然,离不了脚本和编译器的帮助,我这里使用了tcc
进行编译
1 | for (( i=1; i<=3288; i++)) |
其中,那个3288就是总帧数。这样就得到了一个BA.dat
文件。文件内容是一堆用二进制存储的数字,正数代表连续的白色,负数代表连续的黑色,零代表换行。一帧60行,总计3288帧。这样就把一个80多兆的视频压缩到了900多K。有了数据文件剩下的就好办了。
未完待续。。。。。。
]]>这步我不打算多说,大部分Linux发行版的仓库应该都有,以我的ArchLinux为例,执行:
~# pacman -S mingw-w64
即可。如果你不需要交叉编译,要在Windows上直接编译,请自行去SourceForge上下载Windows版本。不要担心那个w64
是不是64位版本,它既可以编译32位又可以编译64位程序。还是以我的版本为例:
~# pacman -Ql mingw-w64-gcc| grep '/usr/bin/.*gcc$'mingw-w64-gcc /usr/bin/i686-w64-mingw32-gccmingw-w64-gcc /usr/bin/x86_64-w64-mingw32-gcc
可以看到有两个gcc,用i686-w64-mingw32-gcc
编译出来的程序就是32位的,而x86_64-w64-mingw32-gcc
编译出来的就是64位的。现在,随便写个Hello World(你可以用我的Hello World代码 ^_^),然后编译试试:
i686-w64-mingw32-gcc hello_world.c -o hello_world.exe
把它拿到虚拟机或扔进Wine里,如果能正常运行,那么恭喜你,第一步完成了。
很简单的步骤,如果自己搞不定的建议直接右上角。
把curl-7.35.0
和zlib-1.2.8
(可能还有openssl-1.0.1f
)这几个文件夹放在同一个目录下,然后进行下一步。
先打开zlib/win32
文件夹下的Makefile.gcc
文件,把PREFIX =
这行改成STEP1里的gcc前缀,对于我来说就是PREFIX = i686-w64-mingw32-
。把这个文件拷贝到zlib
文件夹下,然后在zlib
文件夹下make -f Makefile.gcc
,你就应该能看到libz.a
这个文件了。
如果你要编译OpenSSL,那么就去openssl文件夹下
$ ./Configure no-shared --cross-compile-prefix=i686-w64-mingw32- mingw$ make
即可,记得改prefix。生成libssl.a
和libcrypto.a
最后去libcurl里的lib文件夹里修改Makefile.m32
文件,在CC = $(CROSSPREFIX)gcc
上加一行CROSSPREFIX=i686-w64-mingw32-
(请按需修改),然后把下面CFLAGS
那行改成这样CFLAGS = -g -O2 -Wall -DCURL_DISABLE_LDAP
,最后
make -f Makefile.m32 CFG=-zlib
或是
make -f Makefile.m32 CFG=-zlib-ssl
make到最后时会报个错,是因为文件没放对地方,手动挪一下即可
1 | for x in vtls/openssl.o vtls/gtls.o vtls/vtls.o vtls/nss.o vtls/qssl.o vtls/polarssl.o vtls/polarssl_threadlock.o vtls/axtls.o vtls/cyassl.o vtls/curl_schannel.o vtls/curl_darwinssl.o vtls/gskit.o |
然后再make一下,libcurl.a
文件应该就出现了。
如果生成dll出错也不要紧,我们要的是.a
文件
现在,你可以找一段libcurl的demo来测试了。注意要加上宏定义CURL_STATICLIB
i686-w64-mingw32-gcc -I. -L. -DCURL_STATICLIB curl_demo.c -lcurl -lz -lws2_32 -o curl_demo.exe
如果你因为不知道gcc-I
和-L
选项的用法而编译不过,请自行Google。如果你加了ssl支持,你需要链接更多的库,具体请根据错误信息自行Google。最后提醒一点:请把-lcurl
选项放在源文件后面,我当初就是因为这个死活链接不过。最后把curl_demo.exe
拖进虚拟机里,如果一切正常,那么恭喜你,你成功了。
为了解释这个问题,我要先引入Python3中的字符串与字节序列的概念。在Python3中,一个字符串不存在‘编码类型’这种概念,每一个包含相同文字的字符串都是完全一样的(确切的讲,Python3中的字符串是以Unicode编码的字节序列)。而字节序列和C中的char数组很像,它是字符串保存在文件系统上的真正形态。一个固定的字符串(即包含相同的文字)可以被编码(即Python中的encode()
)成字节序列。如果对其使用不同的编码方式,生成的字节序列也不同。举个例子,一栋房子地面上的第一层,英国人叫它’ground floor’,而美国人叫它‘first floor’。这就是不同的编码方式。相反,解码(Python中的decode()
)就是把一个字节序列变回字符串。在普通情况下,乱码是由于对一个字节序列使用了错误的编码方式进行解码,解码之后的内容自然无法阅读。就像一个英国人给一个美国人留了张便条,写着‘Meet me at first floor.’(二楼),然后美国人去一楼转了半天都没找到。这种使用了错误的解码方式的问题一般是由于操作系统的默认设定造成的,比如Windows系统使用当地语言的编码(大陆GBK,台湾BIG5,日本SHIFT-JIS之类的),而Linux普遍使用UTF-8编码。解决这种乱码的方法也很简单,你不是默认以UTF8方式解读么?而现在的字节序列又需要以GBK方式解读才能获得正确的内容,那么我们只要找到一个字节序列,让它被以UTF8解码时得到的内容和现在的字节序列被以GBK解码时得到的内容一样就行了。具体方法就是把当前的字节序列先以GBK解码,再以UTF-8编码,然后写回文件系统里,就搞定了。这个命令在Linux上就是iconv -f GBK -t UTF-8
(记得加管道)。
但是,这次的乱码坑爹就坑爹在: 它的字节序列(字符串是以字节序列的形式保存在磁盘上的,还记得么?)无法以GBK方式解码,相反,它更像是一个根正苗红的UTF-8编码的字节序列。在多次尝试失败后,我开始无聊地“欣赏”乱码字符。注意到那些字符上的装饰了么?就是那些小点波浪尖角什么的?我也注意到了。然后我意识到,这是显著的西欧字符集的特征。也就是说,曾经有某个字节序列被以ISO-8859-1(一种西欧字符集)解码过一次,然后才表现出了现在的样子。那么,我们把现在的字符串用ISO-8859-1编码一次看看:iconv -f UTF-8 -t ISO-8859-1
(因为我的系统的默认编码是UTF-8,所以需要‘-f UTF-8’)。然后得到了一坨问号,这正是GBK编码以UTF-8方式解码的结果,于是再接再厉:iconv -f GBK -t UTF-8
。终于看到了熟悉的文字。最终,这个问题以两行命令被解决(因为是文件名乱码,所以用convmv
命令):
convmv -f UTF-8 -t ISO-8859-1 --notest *convmv -f GBK -t UTF-8 --notest *
总结:这个乱码的产生原因也是对字节序列使用了错误的解码方式(对GBK字节序列使用ISO-8859-1解码),但是因为最终的字节序列是一个根正苗红的UTF-8编码,所以特别难以解决。这种问题特别容易发生在网络当中,比如,一个网站允许用户上传文件,因为一些原因,这个网站把所有的文件名都转成UTF-8存储,如果用户用西欧语言,那么这样就完全没关系,但如果一个中国用户上传了一个GBK文件名编码的文件,那么这种问题就发生了。
如果大家对排版或是图片之类的有什么意见建议请务必留言。
做人要厚道,转载请注明出处
众所周知,网盘这东西对大众来说不可或缺,国内的在线存储服务也欣欣向荣。但是,由于各种原因,我们仍感到这些不能完全满足我们的要求。
比如各种限制、各种暂停分享、还有各种必须付钱才能用的VIP服务等。各大公司想挣钱无可非议,毕竟网络存储绝对是烧钱的主,但作为一个搞技术的人,决不能整天写登陆界面,对吧?
动机在新浪微盘数据结构解析中说了,在那之后我又研究了其他的网盘,萌生了这么一个设想。
简要的说,这个系统可以大大方便文件的传播与获取,延长资源的存活时间。
废话不多说,以下是我的构想:
更多可能,任君想象
可能你们已经注意到了,我尽可能的避免使用网盘
这个字眼。没错,我的目标不仅是网盘,我还希望加入一些“只读”的资源,比如通过解析视频网站的地址来下载视频文件等。正如Bilibili所做的那样(不过也许他们有合作关系?)。
目前,统一管理界面正在书写中,使用Python3, 应该不久可以放出Alpha版和API。不过,最后,我要给大家浇盆冷水,本计划仍处于设想阶段,不要期望能瞬间完成。而且,我们需要考虑遭到封杀后的应对措施,以及如何保持协议更新后库的快速升级等问题。
PS:欢迎有兴趣和有能力的同学来信交流:gzh.shadow@gmail.com
平时我们写的常量都是十进制数,但我们有时需要写一个比如十六进制数怎么办呢?我们当然可以手动计算一下,但还有更优雅的方法。
writeln($Ff,#32,&10,#32,%100);
你觉得它会输出什么呢?它输出255 8 4
!所以以$
开头的是16进制数,&
开头的是8进制数,%
开头的是二进制数。顺带一提的是,以#
开头的数会转变成对应ASCII码的字符,其实它可以和前面的三个符号共同使用,即#$20
和#%100000
都代表了空格。
关于内联的解释请自己找资料,写法如下:
function foo(bar:Type):ReturnType;inline;
即在函数头后加inline;
即可。测试证明确实有效,不过建议只用于诸如min
或max
这种函数。
Pascal也是支持重载的,甚至可以重载系统函数!演示如下:
procedure sort(var a:TArray;l,r:longint);begin ...end;procedure sort(var a:TArray;r:longint);begin sort(a,1,r)end;
这样,调用sort(arr,top)
就相当于调用sort(arr,1,top)
.
是不是对写高精度时的plus(a,b)
感到厌倦?是不是想换一种更帅的书写方式?没问题,操作符重载能满足你的愿望!它可以让你用a+b
的形式对高精度进行计算!
operator + (a,b:Type) c:Type;operator := (a:Type1) b:Type2;operator > (a,b:Type) c:boolean;
需要注意的是
Boolean
为了解决不能随意交换位置的问题,你可以这样写:
operator + (a:Type1;b:Type2)c:ReturnType;begin ...end;operator + (a:Type2;b:Type1)c:ReturnType;begin c:=b+aend;
看完前面的函数重载,你是不是迫不及待地想要重载val()
和str()
这两个你看着不爽很久的函数了?但是却发现不能调用系统原来的函数了,Pascal把它当成了递归!解决方法很简单,在要用原始系统函数的地方加上system.
即可。
1 | function str(x:longint):string; |
担心数组太大爆内存?但心数组太小存不下?动态数组解除你的忧虑!主要操作如下:
a:array of Type;
:变量声明。setlength(a,length)
:设定数组下标,范围为[0..length-1]
,会自动清零。b:=a
:看上去像是赋值,但其实不是赋值,只是复制地址而已,因此对b
的修改就是对a
的修改。c:=copy(a,0,length(a))
:这就是真正的赋值了!还记得copy()
和length()
函数么?现在它们可以用于动态数组了!d:array of array of Type
:二维动态数组声明。setlength(d,length1,length2)
:不解释。a:=d[1]
:a
是一维动态数组,对a[x]
的修改就是对d[1][x]
的修改。copy(d[x],0,length(d[x]))
:取出第x
个一维动态数组。d[x,y]
的方式访问,也可以用d[x][y]
的方式访问。想用C语言中的&
操作符?在Pascal中它是@
!估计某年的NOIP坑了不少人。
你还在定义text
类型么?你还在用查找替换功能批量替换你的readln()
么?赶快试试这个!
assign(input,'foo.in');reset(input);assign(output,'foo.out');rewrite(output);...close(input);close(output);
再也不用担心输入输出了!
是不是很羡慕C中printf
的变参?是不是很羡慕writeln
可以有好多好多参数?利用动态数组可以实现类似的功能!
1 | function average(arr:array of longint):real; |
请注意其中high()
和length()
的区别。合法调用如下:
var A:array[1..MAX]of longint;average([3]);average([1,2,3,4]);average(A);average(A[1..5]);
或许你对C++中的sort()
已有所耳闻,或许你已经知道,它的比较函数是做为参数传进去的。配合@
操作符,Pascal可以做到相同的效果。
1 | type TMyCompareFunc=Function(a,b:MyType):boolean; |
既然是变量,就能互相赋值,但是请注意:
1 | type TNoArgFunc=Function():integer; |
我就不多介绍了+=
、-=
、*=
、/=
…考试时先试试能不能用。使用有风险,偷懒须谨慎。
知道C中的(int)a
或是int(a)
么?不知道没关系,Pascal中的强制类型转换是这样写的TypeIdentifier(Variable)
。下面给几个例子:
1 | type IntPtr=^integer; |
标题写着无类型变量
,其实应该叫做多类型变量
更准确。他的主要工作原理就是在内部进行类型转换,因此效率极其低下,占用空间还特别大,连官方手册都不建议用。类型名为Variant
。
无类型指针就是上一节提到的Pointer
了。任何指针都可以赋值给它,它也可以赋值给任何指针,例子如下:
1 | var |
看出来了么?这段文字和上一段的最后一句话等效。
这是我最近才看到的一种写法,从来没用过。有愿意尝试的请自行研究。
1 | type |
关于Object Pascal,推荐一本书:Start Programming Using Object Pascal
Pascal是不少OIer最开始使用的一种语言,仅以此文献给众多正在使用和曾经使用过Pascal的OIer。
做人要厚道,转载请注明出处!
微盘内部使用一个叫做gsid
的值来识别用户。具体获取方法如下:
http://vdisk.weibo.com/wap_auth
。username
:用户名password
:用户密码{ "message": "/?gsid=..."}
。其中...
的部分就是gsid
得到gsid
后,就可以用来下载文件了。当然,要先得到下载连接。
http://vdisk.weibo.com/share/ajaxFileinfo
fid
:文件IDgsid
:GSID1 | {"id":"406458665","name":"\u5fae\u535a\u5c01\u9762\u80cc\u666f.zip","uid":"60999569","dir_id":"0","ctime":"1358820088","ltime":"1358820088","dtime":"0","size":"1662111","is_locked":"0","type":"application\/x-zip","md5":"9e8eec4a5d2d3ac5be41bb9f34dc3e40","sha1":"6e59853b198e3eae818d5b0756f47c34d4eae6df","w":"0","h":"0","hid":"0","status":"1","app_key":"139204333","source":"2","ip":"0","rev_id":"140316098","share_status":"0","share":-1,"s3_url":"http:\/\/file.data.vdisk.me\/61099569\/6e59853b198e3eae818d5b0756f47c34d4eae6df?ip=1358945643,10.75.7.27&ssig=o9x4sQ9tz6&Expires=1358944443&KID=sae,l30zoo1wmz&fn=%E5%BE%AE%E5%8D%9A%E5%B0%81%E9%9D%A2%E8%83%8C%E6%99%AF.zip","url":"http:\/\/vdisk.weibo.com\/s\/oex4F","byte":"1662111","length":"1662111"} |
其中,s3_url
就是下载地址了。顺带一提,从http://vdisk.weibo.com/file/info?fid=...
也可以得到文件信息,需要Cookie:gsid
,但似乎不是所有文件都能成功得到信息。
从上一步得到了URL就可以下载了。
s3_url
gsid
:GSID需要注意的是,这个URL可能会有很多302 Redirection
,可能是出于负载平衡的原因吧。
平时我们下载都是用http://vdisk.weibo.com/s/aMVfa
这种形式的短链接,其中aMVfa
就是fid64,说白了就是64进制表示的fid,其0~63对应如下:
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-
因此,aR_8-
解码后就是181920319
研究完上文所提到的东西后,应该就可以进行文件批量下载了。但做为一个上进的好青年,我们不会止步与此。为了能够方便地管理自己的文件夹,当然要做进一步研究。
http://vdisk.weibo.com/dir/list
dir_id
:Directionary IDgsid
:GSIDdirinfo
中的dir_num
和file_num
分别储存了在这个文件夹下有多少个目录和多少个文件。data
段是一个数组,每个元素都代表了一个文件或目录,其中保存了fid
或是dir_id
。通过是否有type
段来判断具体是文件还是目录。用户根文件夹的dir_id
是0
似乎所有的链接都可以通过在Cookie中加入device=mobile
来跳过UA检查。(就是说不用设置UA了)
凯撒密码是对称加密中的一种,他的加密方法是把A变成B,把B变成C,于是解密的时候只要把字母替换回来就行了。也就是说,任何知道加密方法的人就可以解密。
RSA是一种非对称加密算法,他的特点是任何人都可以加密,但只有我可以解密。做个比喻,人人都可以把锁头扣上,但只有拥有钥匙的人可以开锁。这个分发出去的用于加密的东西叫做公钥,也被称作证书。而留在自己身边的“钥匙”就是私钥,是绝对不能被第二个人拿到的。
于是乎,你可以把公钥发给别人,别人把数据用你的公钥加密后传给你,你用私钥解密后阅读。在这个过程中,任何人截取到数据都是无效的,因为它没有你的私钥。
RSA还有一个特点,就是可以用私钥加密,用公钥解密。你会问,公钥人人都能拿到,相当于人人都能解密,那这样加密有什么意义呢?意义在于,它可以作为身份验证。用私钥加密的过程叫签名,而验证签名就是用对应的公钥解密。因为为只有用对应的私钥签名的文件才能用公钥解密,既然它可以用公钥解密,就一定是由对应私钥签署的,而私钥只有你有,于是这份文件就一定是你发布的。网络上的HTTPS就是依靠着个。一般来说,用私钥加密的都是MD5、SHA1 之类的,加密原文太耗系统资源。
对付这种非对称的加密方式,有一种叫做“中间人攻击”的攻击方法,它会使双方之间的通信完全暴露。我就偷懒不写了,大家自己找资料。
]]>libimobiledevice, like it’s name, is a library who provides the interface to access the iDevice. It provides a higher level of access such as photo, bookmark, install/uninstall softwares and even sync music. And it doesn’t need jailbreak.
What I want to mention is how musics synchronized with an iDevice. Under the iTunes folder (You may never seen that before. That’s ordinary.), there’s a file called iTunesDB. That’s the file which libgpod really works with. This file contains the name of songs, singers’ names, your play lists and so on. Unfortunately, because Apple don’t want it be modified by any programs except iTunes, they add some hash info into the file. If iPod found the hash is incorrect, it refused to display the songs. There was once a project called iPodHash, but it seems to be die due to a DMCA notice. Apple engineers have changed the hash algorithm for several times and the latest version haven’t been reverse-engineering, as a result, now we can only sync with a old version of iOS.
If your iDevice is jailbreaked, you can change a key called DBVersion(Sorry, I forgot where it is.). It tells iPod which version of hash algorithm it should use so we could use a known hash on new iOS. This process depends on libimobiledevice too. It only support to sync with iOS 4 or older. That means it’s useless even if you changed DBVersion on your iOS5 device. By the way, you may will not find a iTunesDB file but a iTunesCDB instead. It’s a compressed version of iTunesDB using zlib.
I feel so sad that such a project is closed and now I can only sync with my iPod on Windows.
]]>This article is written mainly for those people in China. Be sure you are enough familiar with what you are reading and what you try to do.
================================================================
As we all known, in China mainland, we cannot visit sites like YouTube Facebook Twitter. And the Google sites are out of service frequently. It’s because the Chinese government used some technical methods to prevent us from visiting them. The government has setup a system to do this. It’s called the Great Firewall of China (GFW). This system keeps look on the gateway export. And if it finds something unusual. It will stop the connection.
The system usually inject a RESET into the TCP connection. To prevent this, we can use HTTPS(The S means Secure) instead of HTTP. So the system can not inject the RESET any more. It’s easy to perform. You just need to replace the “http://“ part of a URL with “https://“. And the URL will look like this “https://www.facebook.com". Most of the sites support a HTTPS connection.
Unfortunately, this will not always works. Because the system also used another method called “DNS Redirection”. As we all known, the computers on the Internet are identified by IP address. But human can’t remember them easily. So we use some meaningful phases called “Domain”. Some computers on the Internet provide the kind of service to translate the domains to IP address which is the only form computers can recognize. They are called the “DNS Server”. DNS Redirection is that the DNS servers won’t return the correct IP address (usually were instructed to do so) so that we can’t visit the particular sites.
Luckily, we can assign an IP to a domain manually. That’s the function of a file called hosts. There’s a project called smarthosts on Google Code. It provided a set of IP address you may use. Paste them to your local hosts file and enjoy the Internet.
I will write more about the Internet censorship and how to avoid it. Check back later.
]]>1 | program Hello_World; |
1 |
|
1 |
|
1 | print("Hello World!") |
1 | public class hello_world{ |