分类目录归档:Uncategorized

Go 语言实现RISC-V TLS相关重定向小计

最近想给 RISC-V 的 Go 内部链接器开 PIE 支持。看了眼 AMD64 和 ARM64 的代码,不就是判断个 iscgoflag_race 嘛,复制粘贴改改就行。

结果编译完一跑,炸了:

relocation R_RISCV_TLS_IE cannot be used in PIE

开开关,然后发现 TLS_IE 这货

其他架构开 PIE 走内部链接早就能用了,RISC-V 这边一直是个 TODO。我寻思着把外部链接器的判断逻辑抄过来:

case sys.RISCV64:
    if iscgo || flag_race {
        ldflag = "gcc"
    }

然后就被 R_RISCV_TLS_IE 糊了一脸。

先问了下 Claude,它给我讲了一通 TLS 的四种模型:LE、IE、GD、LD。我说你别讲概念了,我就想知道 Go 内部链接器怎么 patch 这个重定位。它说:"建议参考其他架构的实现。"

……我要是看得懂还问你?

翻书,《Linkers and Loaders》、ELF spec、RISC-V ABI 翻了一圈。大概搞明白了:

  • TLS_LE:本地执行,直接 tp + offset,指令是 lui + addi
  • TLS_IE:初始执行,走 GOT 拿 TLS 偏移,指令是 auipc + addi

其他架构在内部链接 PIE 时,遇到 TLS_IE 都是直接转成 LE。因为内部链接的 PIE 说白了还是静态链接,不需要动态加载器解析,直接把偏移算出来硬编码就行。

我心想,这思路简单啊,抄作业!

不过我的第一反应不是"转 LE",而是:凭什么 RISC-V 就不能原生支持 TLS_IE?

我在 cmd/link 里硬加了 R_RISCV_TLS_IE 的处理,试图在内部链接时解析 GOT 条目。写完之后编译倒是过了,运行时测试挂了。

一查,内部链接器生成 GOT 的时候,对 TLS 符号的处理跟普通全局符号不一样。RISC-V 的 GOT 条目需要 R_RISCV_64 重定位填充,但内部链接器在 dynreloc 阶段根本没给 TLS 符号生成正确的动态重定位。搞出一堆 R_RISCV_NONE 的残留,链接器自己都不认。

折腾了两天,服了。

其他架构把 IE 转 LE 是唯一的路

AMD64 的实现:

case objabi.R_TLS_IE:
    // 直接 patch 立即数,指令长度不变

ARM64实现:

case objabi.R_TLS_IE:
    // adrp + ldr 改成 movz + movk

都很优雅。然后我看 RISC-V 的 TLS_IE 序列:

auipc  t0, %pcrel_hi(symbol)
addi   t0, t0, %pcrel_lo(symbol)

而 LE 应该是:

lui    t0, %tprel_hi(symbol)
addi   t0, t0, %tprel_lo(symbol)

问题来了:RISC-V 的 auipclui 虽然都是 U-type,但 opcode 不一样,立即数编码方式也不一样。AMD64 可以直接 patch 立即数字段,RISC-V 这里必须整条指令换掉

而且 auipc 是 PC-relative,lui 是绝对值。addi 跟着的 pcrel_lotprel_lo 重定位类型也不同。

我一开始想暴力算:

auipc := ctxt.Arch.ByteOrder.Uint32(buf)
lui := (auipc &^ 0x7F) | 0x37  // 改 opcode
// 还要改立即数……

太丑了,而且容易出错。

只好读 link/obj 代码,找办法

花了一天读 cmd/link/internal/riscv64cmd/internal/obj/riscv 的源码。发现内部链接器在 reloc 阶段是可以直接改指令编码的。

关键是 ctxt.Arch.ByteOrder.PutUint32 直接写内存。我需要在 reloc 函数里:

  1. 算出 TLS 的 LE 偏移:ldr.SymValue(symIdx) - ctxt.Tlsoffset
  2. auipc + addi 替换成 lui + addi
  3. 用新的立即数写回去

lui + addi 的立即数拆分有坑。lui 是高 20 位,addi 的低 12 位是有符号的。如果低 12 位是负数(比如 0xFFF),lui 的高 20 位要加 1。

我翻了下 cmd/internal/obj/riscv,发现里面有类似的立即数拆分逻辑。抄了:

func splitImm(v int64) (hi, lo int64) {
    lo = v << 52 >> 52  // 符号扩展低 12 位
    hi = (v - lo) >> 12
    return
}

然后:

hi, lo := splitImm(off)
rd := (auipc >> 7) & 0x1F

lui := 0x37 | (rd << 7) | ((hi & 0xFFFFF) << 12)
addi := 0x13 | (rd << 7) | (rd << 15) | ((lo & 0xFFF) << 20)

ctxt.Arch.ByteOrder.PutUint32(buf, lui)
ctxt.Arch.ByteOrder.PutUint32(buf[4:], addi)

这样看起来顺眼多了。

测试

改完跑测试:

$ go test -v -run TestPIE cmd/link
=== RUN   TestPIE/riscv64
--- PASS: TestPIE/riscv64 (0.12s)

再跑 all.bash 的 riscv64 交叉编译:

$ GOARCH=riscv64 GOOS=linux go test -c -buildmode=pie fmt
# 编译成功

搞定。

回头看

RISC-V 的 TLS 处理确实跟别家不一样。AMD64、ARM64 做 IE 转 LE 基本是同长度指令替换,RISC-V 这里 auipc + addilui + addi 虽然都是两条指令,但编码完全不同,必须整条换掉。

而且 RISC-V 的 GCC 对外部 TLS 变量默认用 LE 模型(依赖 TLS copy relocations),本身就有点特立独行。

CL 741860 加了 R_RISCV_TLS_IE 的重定位处理,CL 742200 正式启用 PIE 内部链接。

GopherCon EU 2025 (Berlin) 参会小计

今年终于参加了GopherCon,之前主要是疫情期间不太想参加大型集会,又因为GopherChina今年继续开天窗了,美签也有风险,所以选了GopherCon EU。

主要先感谢一下主办方的Bill Kennedy,Natielie,我在最后1个月的时候才提交的GoBridge申请,没想到他们给了我一张门票,还帮我搞签证,在这真的非常感谢。还要感谢欧博士,他怕咱人生地不熟,一起陪我逛柏林,教授一些生存小技巧。

签证因为公司的缘故被电话,补了些材料。起飞那天正好南宁刮台风,我幸好买的是联程票,海航给我自动免费改签到了第二天:)

入境德国很顺利,边检看了看我的邀请函,随便聊了两句就让我过了。随后坐上地铁,没安检,没检票(全靠自觉,不过还是有人会随机抽查,我就碰到一个被罚款60欧的美国佬跟我们要现金,然后用paypal转)


Alexander plaza(柏林的宇宙中心,咋跟国内三线城市差不多)

修整了一下,跟欧博逛了逛地标建筑冷战一条街,晚上,对晚上,柏林夏天9点才天黑,蹭了讲师团队的小手工晚宴,吃了点披萨什么的。

这个Gopher Show 真好玩

演讲的主题从简单到复杂都有,难度是相当适中的。饮料、吃的管够,这是这些年来开得最舒服的一次会了,还有演讲的讲台离观众很近,真的很有现场感。就是纯英文的演讲让我的大脑需要花额外的算力来思考,整体参加下来比较累人。现场有个环节让10年以上的Gopher举手,咱也举了,还讲自己来自中国,就是不知道现场有没有录到。


跟Go团队的座谈,但绕不开的一个大话题就是AI,看来整个行业都在Go没办法在AI时代蹭到热度而在找出路……

第三天就是开发者峰会啦~

峰会上讨论了些话题,我关注社区发展的问题,因为Go中国这2年直接关门了,啥东西都没有留下给社区,所以在会上问了问Google的Neil Damon 的态度,他表示问题不大,只要开会Google肯定派人来的。中间还有个插曲,因为Kennedy经常戴帽子,所以我把他和Neil 搞混了,把礼物送给了他(T T),他在会上还笑说当年Austin讲你这样戴跟Kennedy一样的帽子,铁定会被人认错,果然我就是第一个那个倒霉蛋。
其他就是给pkg.go.dev团队提了些建议和bug修复,随便侃了侃,还跟Mark Ryan一起聊了下Risv Go 的维护问题,他也是第一次参加GopherCon:)

周边环境和一些碎碎念照片


优化开源NTP Pool监控节点:基于RISC-V平台的实践

开源 NTP Pool 现状:严峻的服务缺口

当前开源 NTP Pool(pool.ntp.org)在中国区面临显著的服务缺口(Under-Served)
以下就简称开源的公共 NTP Pool 为 NTP Pool。

  • 服务节点/人口比例失衡:全球 NTP Pool 约90%的服务节点位于等欧美发达国家,中国仅占不足3%(数据来源:NTP Pool Project 2024报告),却承载全球21%的互联网流量。
  • 服务延迟差异:境内用户访问境外NTP节点延迟普遍≥150ms,而本土节点因数量不足导致部分区域延迟波动超300ms
  • 监控盲区:原有监控节点多部署于海外,无法真实反映国内网络环境质量(如防火墙策略、骨干网路由抖动),进一步放大服务不稳定性。

如图所示,NTP pool中国区5千万人才拥有一个NTP Pool服务节点,这一地理与网络拓扑的错配,使国内用户难以享受低延迟、高可靠的公共授时服务。

服务缺口的核心问题:本土监控节点缺失

NTP Pool的服务节点调度机制依赖监控节点(Monitor) 实时评估节点健康度。然而:
监控节点国内为0个:2025年前健康检测数据均来自境外,这导致了

  1. 误判率高:网络波动被标记为“节点故障”
  2. 调度失衡:健康节点因跨境延迟被降权
  3. 扩容停滞:缺乏数据支撑中国区服务器准入,服务节点数常年处于50个以下

没有本土监控,NTP Pool的中国服务优化如同“无源之水”。


如图是针对腾讯 NTP 服务器的监控结果,最右边的一列就是监控节点到服务节点的延迟(RTT),越低越好。
大家可以注意到没有中国大陆的监控节点(CN开头的)。
其中的cncan1就是本博客添加的监控节点,可见如果没有cncan1,所有节点都是超过200ms以上的延迟,对于 NTP 准确性会有严重影响。

破局:基于RISC-V监控终端

这次,我选用OrangePi RV2 构建低成本、低功耗监控节点,售价才200出头,加上天线等其他硬件总成本控制在300元左右,还可以通过GPIO添加PPS功能。

相对的,某宝上一般 NTP 服务器都要600以上,还没有算天线等其他设备。

硬件架构

开发板:OrangePi RV2
GNSS模块:Wheeltech GNSS模块 + PPS输出
GNSS天线:北斗+GPS 双模蘑菇头 + 8米 SMA 馈线

开发板添加对应的DTS overlay

/dts-v1/;
/plugin/;
/ {
        compatible = "ky,orangepi-rv2\0ky,x1";
        fragment@0 {
                target-path = "/";
                __overlay__ {
                        pps_gpio: pps {
                                compatible = "pps-gpio";
                                gpios = <&gpio 91 0>;
                                assert-rising-edge;
                        };
                };
        };
};

软件栈

  1. Chrony:维护PPS同步,NTP时间服务
  2. GPSD:接收北斗卫星信号→PPS硬件时钟同步(精度±200ns)
  3. ntppool-agent: 负责监控进程(重新编译,Go语言已支持RISC-V)

效果如图,这样就可以获得一个精度在±500ns以下的Stratum 1 NTP 服务器

获得NTP Pool官方破例支持

向NTP Pool上游持续贡献代码

  1. add riscv64 binary release https://github.com/ntppool/monitor/pull/6
  2. https://github.com/abh/ntppool/pull/255
  3. https://github.com/abh/ntppool/pull/262

1年来,在跟上游管理团队来来回回好十几封邮件后,这块RISC-V开发板,获得了豁免监控节点准入限制:常规要求NTP服务节点稳定运行18个月方可成为监控节点,这次为中国区首开绿色通道。

结语:从服务缺口到技术领先

国产RISC-V开发板+北斗GNSS的组合,不仅填补了中国区NTP Pool监控节点空白,更证明了:RISC-V可承担关键基础设施角色

本博客运行平台迁移至RISC-V

RISC-V powered!

好消息:迁移非常顺利,跟x86平台迁移一样,原平台就叫server,rv板子就叫board吧:
在自己的机器上:

  1. mysqldump database > db.sql 备份mysql数据库
  2. rsync -azv -R server:/home/mzh ./ 转移家目录
  3. rsync -azv -R mzh board:/home
  4. scp db.sql board:/home/mzh

在rv板子上

  1. apt install php-fpm caddy mariadb-server
  2. 配置好caddy (其实就官方文档就好)
  3. 导入数据库 mysql -u mzh < db.sql
  4. 最关键的一步,把板子的80、443暴露给服务器

设置反向代理,添加以下文件/lib/systemd/system/reverse-tunnel\@.service

[Unit]
Description=Reverse SSH Tunnel
After=network-online.target
[Service]
EnvironmentFile=/etc/default/remote-tunnel@%i
ExecStart=/usr/bin/ssh -i ${ID_KEY} -o ServerAliveInterval=60 -o ExitOnForwardFailure=yes -nNT -R 0.0.0.0
:${REMOTE_PORT}:localhost:${LOCAL_PORT} ${REMOTE_SERVER}
RestartSec=5
Restart=always
KillMode=mixed
[Install]
WantedBy=multi-user.target

每个端口一个进程

systemctl daemon-reload && systemctl start reverse-tunnel@https

搞定,这速度杠杠的。

mzh@muse-card-1:/etc/default$ wp --info                                                                  
OS:     Linux 6.6.63 #2.2.4.2 SMP PREEMPT Thu Jun 26 05:06:32 UTC 2025 riscv64                           
Shell:  /bin/sh                                                                                          
PHP binary:     /usr/bin/php8.3                                                                          
PHP version:    8.3.6                                                                                    
php.ini used:   /etc/php/8.3/cli/php.ini                                                                 
MySQL binary:   /usr/bin/mariadb                                                                         
MySQL version:  mariadb  Ver 15.1 Distrib 10.11.8-MariaDB, for debian-linux-gnu (riscv64) using  EditLine
 wrapper                                                                                                 
SQL modes:                                                                                               
WP-CLI root dir:        phar://wp-cli.phar/vendor/wp-cli/wp-cli                                          
WP-CLI vendor dir:      phar://wp-cli.phar/vendor                                                        
WP_CLI phar path:       phar:///usr/local/bin/wp                                                         
WP-CLI packages dir:                                                                                     
WP-CLI cache dir:       /home/mzh/.wp-cli/cache                                                          
WP-CLI global config:                                                                                    
WP-CLI project config:                                                                                   
WP-CLI version: 2.12.0                                                                                   

OpenWRT 防火墙开启IPv6 端口转发的方法

前言

OpenWRT当启用IPv6时,默认情况下流量会经过主路由防火墙。在外部网络环境中,若想访问家庭局域网内的服务(如SSH、Proxy、NAS等),默认请求将被防火墙拦截,因此需在主路由防火墙中添加允许规则。
这里并不推荐禁用IPv6防火墙:虽然IPv6地址和端口扫描难度较大,但仍存在被扫描风险。所有局域网服务可能暴露于外网,即使未受攻击,家庭宽带提供公网服务也可能被ISP警告。

LUCI网页界面配置

打开OpenWRT路由管理页面,进入 网络 - 防火墙 - 流量规则。

点击 添加 创建新规则。流量规则区的规则优先级高于默认规则,无需担心与默认规则冲突,但需注意同页其他规则的优先级。可通过拖拽调整规则顺序。

规则配置示例:

  • 名称:清晰标识规则用途(例如如 NAS IPv6)。
  • 协议:按需选择协议,不确定时可同时选TCP和UDP。
  • 源区域:选择 WAN(外网流量入口)。
  • 源地址/源端口:留空。
  • 目标区域:选择 LAN(内网目标区域)。
  • 目标地址: 链路本地IPv6地址(通常以 fe80:: 开头)。

移除链路本地前缀(如 fe80::20c:29ff:fed7:fbf 改为 ::20c:29ff:fed7:fbf)。

追加掩码 /::ffff:ffff:ffff:ffff,最终填入 ::20c:29ff:fed7:fbf/::ffff:ffff:ffff:ffff。此配置确保客户端即使因ISP分配的前缀变化仍能被匹配。

目标端口:指定允许的端口(如 80-443)。留空将放行所有端口,存在重大安全风险,不建议。

动作:选择 接受(Accept)。

高级设置:地址族改为 IPv6,其他保持默认。

保存后,可测试外网访问内网服务。若在 状态 - 防火墙 的 forward_wan 链中看到规则及计数器,则规则已生效。