跳转至

博客

在 k8s 中部署 Drone 用于 CI

实验了一下在 k8s 中部署 CI,在 drone gitlab-ci 和 jenkins 三者中选择了 drone,因为它比较轻量,并且基于 docker,可以用 GitHub 上的仓库,比较方便。

首先,配置 helm:

helm repo add drone https://charts.drone.io
kubectl create ns drone

参考 drone 的文档,编写 drone-values.yml:

ingress:
  enabled: true
  annotations:
    kubernetes.io/ingress.class: "nginx"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
  hosts:
    - host: drone.example.com
      paths:
        - "/"
  tls:
  - hosts:
    - drone.example.com
    secretName: drone-tls
env:
  DRONE_SERVER_HOST: drone.example.com
  DRONE_SERVER_PROTO: https
  DRONE_USER_CREATE: username:YOUR_GITHUB_USERNAME,admin:true
  DRONE_USER_FILTER: YOUR_GITHUB_USERNAME
  DRONE_RPC_SECRET: REDACTED
  DRONE_GITHUB_CLIENT_ID: REDACTED
  DRONE_GITHUB_CLIENT_SECRET: REDACTED

需要首先去 GitHub 上配置 OAuth application,具体参考 drone 的文档。然后,生成一个 secret,设置 admin 用户并只允许 admin 用户使用 drone,防止其他人使用。然后应用:

helm install --namespace drone drone drone/drone -f drone-values.yml
# or, to upgrade
helm upgrade --namespace drone drone drone/drone --values drone-values.yml 

然后就可以访问上面配好的域名了。遇到了 cert manager 最近的一个 bug,来回折腾几次就好了。

接着配 drone 的 k8s runnner,也是参考 drone 的文档,编写 drone-runner-kube-values.yml:

rbac:
  buildNamespaces:
    - drone
env:
  DRONE_RPC_SECRET: REDACTED
  DRONE_NAMESPACE_DEFAULT: drone

然后应用:

helm install --namespace drone drone-runner-kube drone/drone-runner-kube -f drone-runner-kube-values.yml

然后就可以去 drone 界面上操作了。

需要注意的是,drone 需要 pv,所以我先在腾讯云里面配置了 CFS 的 storage class,然后它就会自动 provision 一个新的 pv 和 pvc 出来。

接着尝试了一下在 drone 里面构建 docker 镜像并且 push 到 registry 上。以腾讯云为例:

kind: pipeline
type: kubernetes
name: default

steps:
- name: build
  image: alpine
  commands:
  - make

- name: publish
  image: plugins/docker
  settings:
    registry: ccr.ccs.tencentyun.com
    repo: ccr.ccs.tencentyun.com/abc/def
    tags: ["${DRONE_COMMIT_SHA:0:7}","latest"]
    username:
      from_secret: docker_username
    password:
      from_secret: docker_password

然后在网页里配置好 docker username 和 password,它就会自动构建 docker 镜像并且 push 到 registry 上,然后再 rollout 一下 deployment 就能部署最新的 image 了。实际上可以在 drone 里面把部署这一步也完成,但目前还没有去实践。

参考文档:

Drone provider: GitHub

Drone helm chart

Drone runner kube helm chat

Building a CD pipeline with drone CI and kubernetes

在 k8s 内用 Cert Manager 配合 Nginx Ingress Controller 配置 Let's Encrypt HTTPS 证书

上一篇博客讲了 nginx ingress 的配置,那自然第一步要配上 https。首先配置 cert-manager:

$ kubectl create namespace cert-manager
$ kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v0.14.1/cert-manager.crds.yaml
$ helm repo add jetstack https://charts.jetstack.io
$ helm repo update
$ helm install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --version v0.14.1

然后,配置 Cluster Issuer,应用以下的 yaml:

apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
  namespace: cert-manager
spec:
  acme:
    email: example@example.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: nginx

然后在 ingress 里面进行配置:

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ingress-example
  annotations:
    kubernetes.io/ingress.class: "nginx"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  tls:
  - hosts:
    - example.com
    secretName: example-tls
  rules:
  - host: example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: example-service
          servicePort: 80

应用以后,用 kubectl describe certificate 查看证书获取进度。成功后,访问改域名的 HTTP,就会自动跳转到 HTTPS,并且提供了正确的证书。

在 TKE 上配置不使用 LB 的 Nginx Ingress Controller

背景

想要在 k8s 里面 host 一个网站,但又不想额外花钱用 LB,想直接用节点的 IP。

方法

首先安装 nginx-ingress:

$ helm repo add nginx-stable https://helm.nginx.com/stable
$ helm repo update
$ helm install ingress-controller nginx-stable/nginx-ingress --set controller.service.type=NodePort --set controller.hostNetwork=true

这里给 ingress controller chart 传了两个参数:第一个指定 service 类型是 NodePort,替代默认的 LoadBalancer;第二个指定 ingress controller 直接在节点上监听,这样就可以用节点的公网 IP 访问了。

然后配一个 ingress:

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ingress-example
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: example-service
          servicePort: 80

然后就可以发现请求被正确路由到 example-service 的 80 端口了。

在 sbt 中 fork 并且并行运行测试

问题

最近在 sbt 使用遇到一个问题,有两个测试,分别用 testOnly 测试的时候没有问题,如果同时测试就会出问题,应该是全局的状态上出现了冲突。一个自然的解决思路是 fork,但是 sbt 默认 fork 之后 test 是顺序执行的,这会很慢。所以搜索了一下,找到了 fork 并且并行测试的方法。

解决方法

解决方法在 sbt 文档中其实就有(原文)。简单来说就是:把每个 test 放到单独的 TestGroup 中,每个 TestGroup 分别用一个 forked JVM 去运行;然后让 sbt 的并行限制设高一些:

// move each test into a group and fork them to avoid race condition
import Tests._
def singleTests(tests: Seq[TestDefinition]) =
  tests map { test =>
    new Group(
      name = test.name,
      tests = Seq(test),
      SubProcess(ForkOptions()))
  }

Test / testGrouping := singleTests( (Test / definedTests).value )
// allow multiple concurrent tests
concurrentRestrictions in Global := Seq(Tags.limitAll(4))

这样就可以了。

在命令行中进行 Vivado 仿真

已有 Vivado 项目

想要在命令行里进行 Vivado 仿真,所以查了下 Xilinx 的 UG900 文档,找到了命令行仿真的方法。首先是生成仿真所需的文件:

# assuming batch mode
open_project xxx.xpr
set_property top YOUR_SIM_TOP [current_fileset -simset]
export_ip_user_files -no_script -force
export_simulation -simulator xsim -force

可以把这些语句放到 tcl 文件里然后用 batch mode 执行。执行成功以后,会在 export_sim/xsim 目录下生成一些文件。里面会有生成的脚本以供仿真:

cd export_sim/xsim && ./YOUR_SIM_TOP.sh

默认情况下它会执行 export_sim/xsim/cmd.tcl 里面的命令。如果想要记录 vcd 文件,修改内容为:

open_vcd
log_vcd
run 20us
close_vcd
quit

这样就可以把仿真的波形输出到 dump.vcd 文件,拖到本地然后用 GTKWave 看。更多支持的命令可以到 UG900 里找。

无项目模式

如果没有创建 Vivado 项目,也可以单独进行仿真,具体分为三个步骤:

  1. 第一步,对每个源 Verilog 文件,运行 xvlog module.v 命令
  2. 第二步,生成 snapshot,运行 xelab -debug all --snapshot snapshot_name top_module_name
  3. 第三步,仿真,运行 xsim snapshot_name

如果想要生成波形文件,编辑 xsim.tcl 为以下内容:

open_vcd
log_vcd *
run -all
close_vcd
quit

把第三步运行的命令改为:xsim snapshot_name -tclbatch xsim.tcl 即可。

体验 Tencent Kubernetes Engine

之前在机器上试验了一下 kubernetes,感觉挺不错的,所以就想把腾讯云上面的机器也交给 kubernetes 管理。找到容器服务,新建集群,选择模板里的标准托管集群就可以了。然后开启下面的公网访问,设置一个比较小的 IP 地址段,按照页面下面的要求合并一下 kube config(因为还有别的 k8s cluster):

$ KUBECONFIG=~/.kube/config:/path/to/new/config kubectl config view --merge --flatten > new_config
$ cp new_config ~/.kube/config

覆盖之前先确认原来的配置还在,然后就可以用 kubectl 切换到新的 context:

$ kubectl config get-contexts
$ kubectl config use-context new-context-here
$ kubectl get node
NAME          STATUS   ROLES    AGE   VERSION
172.21.0.17   Ready    <none>   75m   v1.16.3-tke.3

可以看到我们的 k8s node 已经上线了。我一般习惯先配好 kubernetes-dashboard:

$ kubectl create -f https://raw.githubusercontent.com/cilium/cilium/v1.6/install/kubernetes/quick-install.yaml
$ kubectl proxy &
$ kubectl -n kubernetes-dashboard describe secret (kubectl -n kubernetes-dashboard get secret | grep admin-user | awk '{print \$1}') | tail -n1 | awk '{print \$2}' | pbcopy

然后在浏览器里访问 http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/#/overview?namespace=default 然后把剪贴板里的 token 粘贴进去即可。

默认情况下 kubernetes-dashboard 的权限比较少,可以让它获得更多权限:

$ kubectl edit clusterrole.rbac.authorization.k8s.io/kubernetes-dashboard
# use '*' for ultimate 
# use `kubectl get clusterrole.rbac.authorization.k8s.io/cluster-admin -o yaml` to see full permissions

接下来配置 metrics-server。下载 metrics-server 仓库,然后修改镜像地址:

$ wget https://github.com/kubernetes-sigs/metrics-server/archive/v0.3.6.zip
$ unar v0.3.6.zip
$ cd metrics-server-0.3.6/deploy/1.8+
$ vim metrics-server-deployment
# change: k8s.gcr.io/metrics-server-amd64:v0.3.6
# to: registry.cn-hangzhou.aliyuncs.com/google_containers/metrics-server-amd64:v0.3.6
# add line below image: args: ["--kubelet-insecure-tls"]
$ kubectl apply -f .

等一段时间,就可以看到 metrics server 正常运行了。

参考:https://tencentcloudcontainerteam.github.io/tke-handbook/

在 Rocket Chip 上挂接 TLRAM

最近遇到一个需求,需要在 Rocket Chip 里面开辟一块空间,通过 verilog 的 $readmemh 来进行初始化而不是用 BootROM,这样每次修改内容不需要重新跑一次 Chisel -> Verilog 的流程。然后到处研究了一下,找到了解决的方案:

首先是新建一个 TLRAM 然后挂接到 cbus 上:

import freechips.rocketchip.tilelink.TLRAM
import freechips.rocketchip.tilelink.TLFragmenter
import freechips.rocketchip.diplomacy.LazyModule
import freechips.rocketchip.diplomacy.AddressSet

trait HasTestRAM { this: BaseSubsystem =>
  val testRAM = LazyModule(
    new TLRAM(AddressSet(0x40000000, 0x1FFF), beatBytes = cbus.beatBytes)
  )

  testRAM.node := cbus.coupleTo("bootrom") { TLFragmenter(cbus) := _ }
}

这里的地址和大小都可以自由定义。然后添加到自己的 Top Module 中:

class TestTop(implicit p:Parameters)
    extends RocketSystem
    // ...
    with HasTestRAM
    //...
    {
    override lazy ...    
}

实际上这时候 TLRAM 就已经加入到了 TileLink 总线中。接着,为了让 firrtl 生成 $readmemh 的代码,需要两个步骤:

首先是用 chisel3.util.experimental.loadMemoryFromFile 函数(文档在 https://github.com/freechipsproject/chisel3/wiki/Chisel-Memories):

UPDATE:现在的文档在 Loading Memories for simulation or FPGA initialization 处,并且可以用 loadMemoryFromFileInline。

class TestTopImp(outer: TestTop)
    extends RocketSubsystemModuleImp(outer)
    // ...
    {
    loadMemoryFromFile(outer.testRAM.module.mem, "test.hex")    
}

这个函数会生成一个 FIRRTL Annotation,记录了在这里需要对这个 mem 生成对应的 readmemh 调用。然后在 firrtl 的调用里传入 .anno.json 和 transform:

$ runMain firrtl.stage.Main -i xxx -o xxx -X verilog -faf /path/to/xxx.anno.json -fct chisel3.util.experimental.LoadMemoryTransform

UPDATE: 现在不需要 -fct chisel3.util.experimental.LoadMemoryTransform 参数。目前这个功能和生成 blackbox memory 有冲突,不能同时使用,需要等 chisel3 后续修复。

这里的 chisel3.util.experimental.LoadMemoryTransform 会找到 anno.json 里面对应的 Annotation,然后生成类似下面这样的 verilog 代码:

module xxx(
    // ...
);
  // ...
    $readmemh(path, mem_xxx);
endmodule

bind TLRAM xxx xxx(.*);

这里采用了 Verilog 的 bind 功能,可以在不修改模块代码的时候注入,比如上面,就是注入了一个语句 $readmemh,从而达到目的。

在 Kubernetes 集群上部署 gitlab—runner

按照 GitLab 上的教程试着把 gitlab-runner 部署到 k8s 集群上,发现异常地简单,所以简单做个笔记:

编辑 values.yaml

gitlabUrl: GITLAB_URL
runnerRegistrationToken: "REDACTED"
rbac:
    create: true

此处的信息按照 "Set up a specific Runner manually" 下面的提示填写。然后用 Helm 进行安装:

$ helm repo add gitlab https://charts.gitlab.io
$ kubectl create namespace gitlab-runner
$ helm install --namespace gitlab-runner gitlab-runner -f values.yaml gitlab/gitlab-runner

然后去 Kubernetes Dashboard 就可以看到部署的情况,回到 GitLab 也可以看到出现了“Runners activated for this project” ,表示配置成功。

参考配置:https://docs.gitlab.com/runner/install/kubernetes.html

用 Kubernetes 部署无状态服务

背景

最近需要部署一个用来跑编译的服务,服务从 MQ 取任务,编译完以后提交任务。最初的做法是包装成 docker,然后用 docker-compose 来 scale up。但既然有 k8s 这么好的工具,就试着学习了一下,踩了很多坑,总结了一些需要用到的命令。

搭建 Docker Registry

首先搭建一个本地的 Docker Repository,首先设置密码:

$ mkdir auth
$ htpasswd user pass > auth/passwd

然后运行 registry:

$ docker run -d -p 5000:5000 \
        --restart=always \
        --name registry \
        -v "$(pwd)/registry":/var/lib/registry \
        -v "$(pwd)/auth":/auth \
        -e "REGISTRY_AUTH=htpasswd" \
        -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
        -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
        registry:2

简单起见没有配 tls。然后吧本地的 image push 上去:

$ docker tag $image localhost:5000/$image
$ docker push localhost:5000/$image

这样就可以了。

搭建 k8s 环境

考虑到只用了单个物理机,所以采用的是 minikube。首先下载 minikube,下载方法略去。

接着新建 minikube 虚拟机:

$ minikube start --registry-mirror=https://registry.docker-cn.com --image-mirror-country=cn --image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers --vm-driver=kvm2 --insecure-registry="0.0.0.0/0" --disk-size=50GB --cpus 128 --memory 131072

这里的 0.0.0.0/0 可以缩小,磁盘、CPU 和内存需要在这里就设好,之后不能改,要改只能重新开个虚拟机,不过这个过程也挺快的。

然后初始化一些组件(metrics server 和 kubernetes dashboard):

$ minikube addons enable metrics-server
$ minikube dashboard

如果要访问 dashboard 的话,可以用上面命令输出的链接,或者用 kubectl proxy 然后打开 http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ (注意 http 还是 https)。

如果问到 Access Token,可以用以下 alias 获得(fish/macOS):

$ alias kubedashboard="kubectl -n kubernetes-dashboard describe secret (kubectl -n kubernetes-dashboard get secret | grep admin-user | awk '{print \$1}') | tail -n1 | awk '{print \$2}' | pbcopy"

接着,配置一下 docker registry 的密钥:

$ kubectl create secret generic regcred --from-file=.dockerconfigjson=/path/to/config.json  --type=kubernetes.io/dockerconfigjson

然后,在 Pod/Deployment 里面设定镜像:

containers:
  - name: name
    image: IP:5000/image
imagePullSecrets:
  - name: regcred

然后部署即可。

部署水平自动伸缩(HPA)

这一步配置的是自带的 HPA 功能,需要上述的 metrics-server 打开,并且在 Pod/Deployment 里面写明 resources.requests.cpu:

- name: name
  resources:
    requests:
      cpu: "xxx"

然后创建 HPA 即可:

$ kubectl autoscale deployment $deployment --cpu-percent=50 --min=1 --max=10

通过压测,可以看到自动伸缩的记录:

$ kubectl describe hpa
Normal  SuccessfulRescale  22s   horizontal-pod-autoscaler  New size: 4; reason: cpu resource utilization (percentage of request) above target
Normal  SuccessfulRescale  6s     horizontal-pod-autoscaler  New size: 1; reason: All metrics below target

参考:Kubernetes 官方文档

用 jailkit 限制用户仅 scp

最近需要用 scp 部署到生产机器,但又不想出现安全问题,所以用了 jailkit 的方法。首先是创建单独的用户,然后生成 ssh key 来认证,不再赘述。此时是可以 scp 了,但用户依然可以获得 shell,不够安全。

然后找到了下面参考链接,大概摘录一下所需要的命令和配置:

mkdir /path/to/jail
chown root:root /path/to/jail
chmod 701 /path/to/jail
jk_init -j /path/to/jail scp sftp jk_lsh
jk_jailuser -m -j /path/to/jail jailed_user
vim /path/to/jail/etc/jailkit/jk_lsh.ini
# Add following lines
[jailed_user]
paths = /usr/bin, /usr/lib
exectuables = /usr/bin/scp

之后可以发现该用户的 shell 已经更改 jk_chrootsh,并且只能用 scp。

参考:https://blog.tinned-software.net/restrict-linux-user-to-scp-to-his-home-directory/

每周分享第 56 期

咕咕咕

  1. SystemVerilog linter https://github.com/dalance/svlint
  2. 东北方言编程语言 https://github.com/zhanyong-wan/dongbei
  3. JS LaTeX 渲染到 HTML https://github.com/michael-brade/LaTeX.js
  4. 一种对语音助手的攻击 https://surfingattack.github.io/
  5. 在线打铃网站 http://thulpwan.net/timer/
  6. 网络学堂 PC 端 App https://github.com/jiegec/learn_tsinghua_app/releases
  7. Rust 2020 roadmap https://github.com/rust-lang/rfcs/pull/2857/files

通过 BSCAN JTAG 对 Rocket Chip 进行调试

前言

在上一个 post 里研究了原理,今天也是成功在 Artix 7 上实现了调试。效果如下:

OpenOCD 输出:

Info : JTAG tap: riscv.cpu tap/device found: 0x0362d093 (mfg: 0x049 (Xilinx), part: 0x362d, ver: 0x0)
Info : datacount=1 progbufsize=16
Info : Disabling abstract command reads from CSRs.
Info : Examined RISC-V core; found 1 harts
Info :  hart 0: XLEN=32, misa=0x40801105
Info : Listening on port 3333 for gdb connections

GDB 输出:

Remote debugging using localhost:3333
0x0001018c in getc () at bootloader.c:36
36        while (!(*UART_LSR & 0x1))
(gdb) 

这里用的 OpenOCD 和 GDB 都是 riscv 版本,上游的支持尚不完善。对于 Homebrew 用户,我在 jiegec/homebrew-formulas 维护了需要的 Formula。

过程

代码基本借鉴了 sequencer/rocket-playgroundKireinaHoro/rocket-zcu102 而来,代码方面主要是添加了 BscanJTAG.scala,然后在 Top 模块下把它连接到内部的 JTAG 中:

val boardJTAG = Module(new BscanJTAG)
val jtagBundle = target.debug.head.systemjtag.head

// set JTAG parameters
jtagBundle.reset := reset
jtagBundle.mfr_id := 0x233.U(11.W)
jtagBundle.part_number := 0.U(16.W)
jtagBundle.version := 0.U(4.W)
// connect to BSCAN
jtagBundle.jtag.TCK := boardJTAG.tck
jtagBundle.jtag.TMS := boardJTAG.tms
jtagBundle.jtag.TDI := boardJTAG.tdi
boardJTAG.tdo := jtagBundle.jtag.TDO.data
boardJTAG.tdoEnable := jtagBundle.jtag.TDO.driven

代码方面就足够了。然后,需要一个 riscv-openocd 和 riscv-gdb,分别从上游 repo 编译得来。然后采用以下的 openocd.cfg:

adapter_khz 20000
interface ftdi
ftdi_vid_pid 0x0403 0x6014
ftdi_layout_init 0x00e8 0x60eb
ftdi_tdo_sample_edge falling
reset_config none

set _CHIPNAME riscv
jtag newtap $_CHIPNAME cpu -irlen 6

set _TARGETNAME $_CHIPNAME.cpu

target create $_TARGETNAME.0 riscv -chain-position $_TARGETNAME
$_TARGETNAME.0 configure -work-area-phys 0x80000000 -work-area-size 10000 -work-area-backup 1
riscv use_bscan_tunnel 5

然后就可以用 GDB 调试了。

在 Vivado 中对 chisel3 产生的 verilog 代码仿真

默认情况下,chisel3 生成的 verilog 代码在 Vivado 中仿真会出现很多信号大面积变成 X。解决方法在一个不起眼的 Wiki 页面:Randomization flags

`define RANDOMIZE_REG_INIT
`define RANDOMIZE_MEM_INIT
`define RANDOMIZE_GARBAGE_ASSIGN
`define RANDOMIZE_INVALID_ASSIGN

在生成的 verilog 前面加上这四句,就可以正常仿真了。

在 macOS 烧写 Artix7 FPGA

首先安装好 openocd:

brew install openocd --HEAD

测试所用版本为 0.10.0+dev-01052-g09580964 (2020-02-08-15:09)

然后编写如下的 openocd.cfg:

adapter driver ftdi
adapter speed 10000
ftdi_vid_pid 0x0403 0x6014
ftdi_layout_init 0x0008 0x004b

source [find cpld/xilinx-xc7.cfg]
init
xc7_program xc7.tap
pld load 0 /path/to/bitstream.bit
shutdown

上面的 ftdi 开头的两行按照实际的 JTAG Adapter 修改。可以参考 openocd 自带的一些 cfg。

然后在 openocd.cfg 的目录运行 openocd 即可:

$ openocd
Open On-Chip Debugger 0.10.0+dev-01052-g09580964 (2020-02-08-15:09)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "jtag". To override use 'transport select <transport>'.
Info : ftdi: if you experience problems at higher adapter clocks, try the command "ftdi_tdo_sample_edge falling"
Info : clock speed 10000 kHz
Info : JTAG tap: xc7.tap tap/device found: 0x0362d093 (mfg: 0x049 (Xilinx), part: 0x362d, ver: 0x0)
Warn : gdb services need one or more targets defined
shutdown command invoked
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections

这时 FPGA 已经烧写成功。

参考:

  1. https://pansila.github.io/posts/7db4884d
  2. https://numato.com/kb/programming-mimas-a7-using-openocd-and-xc3sprog/

更新:OpenOCD 已经更新到 0.11.0,对于 Arty A7,采用下面的脚本进行烧写:

# OpenOCD 0.11.0
# Adapted from: interface/ftdi/digilent-hs1.cfg
# See also: board/arty_s7.cfg
adapter driver ftdi
adapter speed 25000
ftdi_vid_pid 0x0403 0x6010
ftdi_channel 0
ftdi_layout_init 0x0088 0x008b
reset_config none

source [find cpld/xilinx-xc7.cfg]
init
pld load 0 ./bitstream.bit
shutdown

成功输出:

$ openocd
Open On-Chip Debugger 0.11.0
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "jtag". To override use 'transport select <transport>'.
Info : ftdi: if you experience problems at higher adapter clocks, try the command "ftdi_tdo_sample_edge falling"
Info : clock speed 25000 kHz
Info : JTAG tap: xc7.tap tap/device found: 0x0362d093 (mfg: 0x049 (Xilinx), part: 0x362d, ver: 0x0)
Warn : gdb services need one or more targets defined
shutdown command invoked
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections

研究 Rocket Chip 的 BSCAN 调试原理

前言

最近 @jsteward 在研究如何通过 JTAG 对 FPGA 里的 Rocket Chip 进行调试。之前 @sequencer 已经做了一些实践,我们在重复他的工作,同时也研究了一下这是怎么工作的。

原理

我们从 @sequencer 得到了一份可用的 Scala 代码OpenOCD 配置,并且了解到:

  1. 可以通过 openocd 找到并调试 Rocket Chip
  2. openocd 是通过 JTAG 向 FPGA 的 TAP 的 IR 写入 USER4,然后往 DR 写入特定格式的数据,然后控制 Rocket Chip 的 JTAG。

这里涉及到一个“封装”的过程,在一个仅可以控制 DR 的 JTAG 中控制另一个 JTAG。首先可以找到 OpenOCD 端的操作代码

tunneled_ir[3].num_bits = 3;
tunneled_ir[3].out_value = bscan_zero;
tunneled_ir[3].in_value = NULL;
tunneled_ir[2].num_bits = bscan_tunnel_ir_width;
tunneled_ir[2].out_value = ir_dtmcontrol;
tunneled_ir[1].in_value = NULL;
tunneled_ir[1].num_bits = 7;
tunneled_ir[1].out_value = tunneled_ir_width;
tunneled_ir[2].in_value = NULL;
tunneled_ir[0].num_bits = 1;
tunneled_ir[0].out_value = bscan_zero;
tunneled_ir[0].in_value = NULL;

如果画成图,大概是这个样子(IR):

3 bits IR Width bits 7 bits 1 bit TDI Data Register TDO
0 Payload Tunneled IR Width 0 -> Rocket Chip TAP ->

DR:

3 bits DR Width bits 7 bits 1 bit TDI Data Register TDO
0 Payload Tunneled DR Width 1 -> Rocket Chip TAP ->

这里 TDI 和 TDO 是直接接到 Rocket Chip 的 JTAG 中的,所以我们期望,当 Rocket Chip TAP 在 Shift-IR/Shift-DR 阶段的时候,刚好通过的是 Payload 部分。而控制 TAP 状态机,需要控制 TMS,这个则是通过一段 HDL 来完成的:

always@(*) begin 
        if (counter_neg == 8'h04) begin 
                jtag_tms = TDI_REG; 
        end else if (counter_neg == 8'h05) begin 
                jtag_tms = 1'b1; 
        end else if ((counter_neg == (8'h08 + shiftreg_cnt)) || (counter_neg == (8'h08 + shiftreg_cnt - 8'h01))) begin 
                jtag_tms = 1'b1; 
        end else begin 
                jtag_tms = 1'b0; 
        end 
end

这里 TDI_REG 取的是第一个 bit 的反(也就是上面 IR 对应 0,DR 对应 1 的那一位),shiftreg_cnf 则是之后 7 个 bit,对应上面的 Tunneled IR/DR Width。那么,在选择 IR 时 TMS 的序列为:

4 cycles 1 cycle 1 cycle 2 cycles shiftreg_cnt-1 cycles 2 cycles rest cycles
0 1 1 0 0 1 0
Run-Test/Idle Select-DR-Scan Select-IR-Scan Capture-IR, Shift-IR Shift-IR Exit1-IR, Update-IR Run-Test/Idle

类似地,如果是选择 DR:

4 cycles 1 cycle 1 cycle 2 cycles shiftreg_cnt-1 cycles 2 cycles rest cycles
0 0 1 0 0 1 0
Run-Test/Idle Run-Test/Idle Select-DR-Scan Capture-DR, Shift-DR Shift-DR Exit1-DR, Update-DR Run-Test/Idle

这样,刚好在 Shift-IR/DR 状态下,Payload 会被写入 IR/DR,从而完成了期望的操作。通过规定一个特定格式的 Data Register,可以实现嵌套的 TAP 的 IR 和 DR 的操作。

参考

  1. JTAG Standard
  2. sequencer/rocket-playground
  3. SiFive's JTAG Tunnel: https://github.com/sifive/fpga-shells/blob/c099bd9b4f916bc0ba88030939a9614d0b0daf2d/src/main/scala/ip/xilinx/Xilinx.scala#L13
  4. https://github.com/watz0n/arty_xjtag
  5. https://github.com/riscv/riscv-openocd/blob/7cb8843794a258380b7c37509e5c693977675b2a/src/target/riscv/riscv.c#L361
  6. UG740: 7 Series FPGAs Configuration

在 macOS 上带执行权限 mmap 一个已删除文件遇到的问题和解决方案

背景

实验环境:macOS Catalina 10.15.2

最近在 rcore-rs/zircon-rs 项目中遇到一个比较玄学的问题,首先需求是在 macOS 的用户进程里开辟一段地址空间,然后把这个地址空间多次映射(权限可能不同、同一块内存可能被映射到多个地址),通过 mmap 模拟虚拟地址的映射。采用的是如下的方案:

  1. 在临时目录创建一个文件,把文件大小设为 16M(暂不考虑扩容)
  2. 需要映射一个虚拟地址到物理地址的时候,就对这个文件的物理地址偏移进行 FIXED 映射,虚拟地址就是期望的虚拟地址。

这样的方案在 Linux 下运行地很好,但在 macOS 下总是以一定概率在第二部出现 EPERM。网上搜了很多,但也没搜到相关的信息,于是自己断断续续地研究了一下,现在有一个比较初步的结果。

TL;DR

先说结论:调用一个带 PROT_EXEC 并且映射文件的 mmap 时,macOS 会进行安全检测,如果此时发现文件在文件系统上消失了,它会认为这可能是一个恶意软件行为,进行拦截,返回 EPERM。

而代码实际上在第一步和第二步之间,把临时目录删了:由于进程持有 fd,所以文件并不会真的删掉,当软件退出的时候文件自然会删除,这是临时文件的常见做法(见 tmpfile(3))。

研究过程

查看 Console

在网上一番搜索未果后,就尝试在 Console 里面寻找信息。照着程序名字搜索,可以找到一些信息:

temporarySigning type=1 matchFlags=0x0 path=/path/to/executable

这是编译这个 executable 的时候出现的,好像也没啥问题。然后解除过滤,在这个信息前后按照 syspolicyd 寻找:

initiating malware scan (... info_path: /path/to/temp/file proc_path: /path/to/executable)
Unable (errno: 2) to read file at <private> for process path: <private> library path: <private>
Disallowing load of <private> in 50001, <private>
Library load (/path/to/temp/file) rejected: library load denied by system policy

这几条记录比较可疑,每次运行程序,如果跑挂了,就会出现这几条,如果没跑挂,就不会出现这一条。所以很大概率是被 macOS 拦截了。错误信息的用词是 library,所以大概率是被当成加载动态库了,但既然内容是空的,所以我想的是文件名触碰到了什么奇怪的规则,然后文件名又是随机的,随机导致 EPERM 是概率性出现的,这好像很有道理。于是我把 tmpfile 换成了固定的路径,忽然就好了。但固定的路径只能保证同时只有一个程序在跑,如果路径拼接上 pid,怎么删,谁来删又是一个问题。虽然可以放到 /tmp 下面然后随便搞,但 /tmp 的回收并不是那么积极,在临时目录下丢太多文件也会出现问题。

一丝曙光

这时候,@wangrunji0408 提供了一个方案:在 System Preferences -> Security & Privacy -> Privacy -> Developer Tools 中添加编译该 executable 的程序(如 iTerm、CLion)可以解决这个问题。那么问题应该比较明确了,就是 malware scan 的问题,如果信任了这个 App 为 Developer Tools,它产生的 executable 也是可信的,应该不是恶意软件。但在 tmux 环境下,它哪个 App 也不属于,没法继承,况且把这个权限开放出去也有潜在的安全问题。并且让每个开发者都要这么操作一遍很不方便。

回到 Console

今天刚好看到一个 post,内容是如何在 macOS Catalina 中查看 log 中标记为 private 的内容。如果你注意到的话,上面的 log 中出现了几处 private,这并不是我改的,而是 macOS 自带的隐私机制(当然这种机制似乎并没有采用的很完全,一些消息源没有打上 private 的标签)。

然后按照上面的 post 的方法(另一个 post)开启了一下标记为 private 的内容,正好我的系统没有升级到 10.15.3 所以还能用。此时上面的第二条和第三条就出现了具体内容:

Unable (errno: 2) to read file at /path/to/temp/file for process path: /path/to/executable library path: /path/to/temp/file
Disallowing load of /path/to/temp/file in 61254, /path/to/executable

这个时候问题就很明显了:读取不到文件。这时候回想起 tmpfile 的工作原理,它会删除生成的文件,在删除文件之后,macOS 进行扫描,发现找不到文件,于是 disallow 了,mmap 就会返回 EPERM。

解决方案也很显然了:把删除目录延后,或者放在 /tmp 下等待清理等待。

我也写了一段 C 代码来验证这个现象:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>


int main() {
    int fd = open("mmap", O_RDWR | O_CREAT, 0777);
    uint64_t addr = 0x200000000;
    ftruncate(fd, 16*1024*1024);
    // might not work if unlink is put here (race condition)
    // you can use sleep to reproduce
    unlink("mmap");
    void * res = mmap((void *)addr, 16*1024*1024, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_FIXED, fd, 0);
    // always works if unlink is put here
    // unlink("mmap");
    if (res == MAP_FAILED) {
        perror("mmap");
    } else {
        printf("good");
    }
    return 0;
}

每周分享第 55 期

一个月后终于复更

  1. 退出 vim 教程 https://github.com/hakluke/how-to-exit-vim
  2. SHA-1 攻击新进展 https://sha-mbles.github.io/
  3. gmane 近况 https://lars.ingebrigtsen.no/2020/01/06/whatever-happened-to-news-gmane-org/
  4. 浏览器能做的事情 https://github.com/luruke/browser-2020
  5. 一个 ext2 和 FAT 为一体的 fs https://github.com/NieDzejkob/cursedfs
  6. iptables 规则调试工具 https://github.com/x-way/iptables-tracer
  7. Qt 2020 的变化 https://www.qt.io/blog/qt-offering-changes-2020
  8. 后缀自动机可视化 https://yeah.moe/p/a8e74947/

MacBookPro 14,3 Wi-Fi 驱动问题解决方案

之前在 MacBookPro 14,3 安装 Linux 后,很多东西的驱动都有了解决方法,参考 1参考 2,触摸板和键盘等等都可以正常使用,触摸板的使用效果比我意料要好一些,虽然还是没有 macOS 原生那么好。但 Wi-Fi 一直有能扫到信号但连不上的问题,最近终于有了比较完善的解决方案,地址,也是两个月前才出来的方案,我测试了一下,确实可以很好的解决网络问题,网卡型号是 BCM43602,驱动用的是 brcmfmac。

另一方面,带 T2 的 MacBook 似乎也有了支持,见 aunali1/linux-mbp-arch,有一些尚未 upstream 的 patch,但我没有设备,就没有测试了。需要吐槽的是 ArchWiki 不怎么更新新 Model 的 MacBook 的教程,都是到处找散落的 github repo 和 gist 找别人的方案。

P.S. 可以正常工作的有:Wi-Fi,键盘,触摸板,Touchbar,内置摄像头,键盘背光,蓝牙 P.P.S MacBookPro11,2 的网卡是 BCM4360,直接用 broadcom-wl 驱动就可以。

JieLabs 是如何工作的

简介

JieLabs 是陈嘉杰、高一川、刘晓义(按姓氏拼音首字母排序)于 2020 年新型冠状病毒疫情期间开发的在线数字逻辑电路实验系统,用于清华大学 2020 年春季学期数字逻辑电路实验课程。其包括前端、后端和固件三部分,分别主要由刘晓义、陈嘉杰和高一川负责开发。核心功能实现用时一周,后续界面和稳定性优化用时两周。本文会详细地介绍 JieLabs 的工作原理和一些技术细节,希望对各位同学有所帮助。

太长;不看。

采用了如下的技术方案:

前端:React 框架 + Redux 状态管理 + Monaco 编辑器 + WebAssembly 运行 Rust 代码 + WebSocket 实时通信 + SASS 样式

后端:Actix-Web 框架 + Diesel/PostgreSQL 数据库 + Redis 消息队列 + Quartus 构建 + Kubernetes 构建容器编排

固件:Xilinx FPGA 控制 + Buildroot 系统 + Linux 内核

前端

前端大部分都是刘晓义同学编写的,也是这个项目工作量最大的一部分。除了本文,还可以阅读刘晓义同学自己写的总结。主要分以下几部分来谈前端的技术实现:

第三方库

整体上采用了时下比较流行的 React 框架,配合 Redux 进行状态管理,用 React Hooks 编写组件的逻辑。为了实现 VHDL/Verilog 代码的编辑,采用了来自 VSCode 的独立编辑器空间 Monaco,并自行编写了 VHDL 和 Verilog 语言的支持,一部分在 JS 实现,另一部分在 Rust 中实现,通过 wasm-pack 打包到 JS 中执行。另外为了实现 gzip 格式的解压缩也引入了 pako 库。

在这些第三方库里,Monaco 的体积最大,后面我们针对 JS 体积做了许多优化,在下面会再提。

Rust 在前端的应用

由于开发者里刘晓义和陈嘉杰都是 Rust 语言的爱好者,考虑到目前 Rust to WASM 的技术比较成熟,WebAssembly 的可用程度也很高,我们把一些功能挪到了 Rust 中执行:

一是布线的计算。这是一个比较纯粹的算法问题,一方面对性能有一定的要求,一方面开发者比较喜欢 Rust,所以就用 Rust 实现了。这里要特别感谢刘光哲同学对布线算法的指点。在此基础上,我们用 Rust 实现了几个论文中的布线算法(Maze Router),并且通过和 JS 代码的配合得到了一个比较优秀的效果。

二是 VHDL/Verilog 的语言支持。学过编译原理的同学应该知道,如果要实现一个能够解析代码里的信号的程序,一般是不能通过正则表达式来解决的,况且我们还实现了一些错误信息的显示。VHDL 语言支持采用了已有的比较完善的库,Verilog 由于现有的库都比较庞大,不适合放于前端,于是我们编写了一个最小的 Verilog(实际上算是 SystemVerilog)的解析,仅仅足够满足我们的需求。如果同学们遇到了一些语法上功能的缺少,欢迎提出。

Canvas 的应用

连线部分因为是动态生成的,所以也是动态绘制的,Canvas 就可以派上用场了。我们也利用了 Canvas 的特性,针对每一个网络都画在一个 Canvas 上,那么在检测鼠标位置的时候,只要检查 Canvas 在鼠标所在的点上是否颜色,就知道鼠标是否在它上面了。

前端加载速度的优化

优化前前端 JS 和 WASM 总大小大约是 4MB,对于网络不好的用户来说,它的加载时间是不能容忍的。于是我们采用了以下的措施:

  1. 打开 gzip:有很显著的效果,但因为一些未知的原因,在实际部署的时候未能打开
  2. 缩小 JS 体积:通过 Webpack Analyzer 分析程序各个部分的大小,删掉了 Monaco 中一些没有用到的功能
  3. 缩小 WASM 体积:打开 LTO 和 -Os 选项
  4. 代码分割:把不同功能的代码分割开,先让一部分代码加载进来,可以绘制一个部分功能的界面,然后再继续加载剩下的组件
  5. CDN:把一部分外部的依赖放到国内,后续如果有需求的话也可以把内部的依赖也放到国内的 CDN 上

后端

后端用 Rust 语言编写,采用了目前比较成熟的 actix-web 框架,大量使用了 async/await。除此之外,用 Redis 作为消息队列,在 Docker 容器中运行 Quartus,用 Kubernetes 进行容器的动态调度。

任务调度

对于用户提交的代码和约束,后端需要进行任务的调度,生成一个新的任务,放入到 Redis 消息队列中。另一方面,Docker 中运行的 python 脚本会从 Redis 中取任务,任务完成后把结果上传并回传给后端表示确认。如果一个任务一直没有完成,后端会进行回收并重新分配一个任务到队列中。为了防止这个过程中出现重复任务的提交,为每个提交设置了一个足够长的随机 ID。Docker 容器一开始是通过 docker-compose 进行配置,后来考虑到这个场景比较适合 kubernetes,于是使用了一下,还挺好用的。一开始用的是 minikube,搭好 docker registry,然后往里面部署几个 pod 并设置 hpa,具体可以看我的另一篇博客,后来迁移到了 kubeadm 直接配置。现在迁移到了一个 k3s 搭建的 k8s 集群上。

板子通信

第二个主要功能是进行板子的分配和通信。每个用户会创建一个 WebSocket 连接到后端,每个板子也是一个 WebSocket。当一个用户分配到一个板子的时候,它可以通过后端发送命令给对应的板子,板子的回复也会原路返回,相当于一个 WebSocket Proxy。另外为了保证资源的利用率,添加了一些限制、心跳包和认证。

状态监控

为了可以直观地看到各个数据,实现了一个简单的监控接口,接入 Telegraf+InfluxDB+Grafana 的监控系统,可以实时看到各个资源的情况,如用户、板子和任务等等,也方便我们在在线用户比较少的时候进行更新。

板子

这个平台虽然是用于数字逻辑实验课程,但实际用的板子来自数字逻辑设计课程。我们把其上一个 Altera FPGA 作为实验 FPGA,在控制的 Xilinx FPGA 上运行我们的固件,负责读取和写入 GPIO、下载 bitstream 等等功能。

每周分享第 54 期

咕了两周

  1. ES2019 https://javascript.christmas/2019/7
  2. CSS 技巧 https://github.com/chokcoco/iCSS
  3. Rust 编译器加速 https://blog.mozilla.org/nnethercote/2019/12/11/how-to-speed-up-the-rust-compiler-one-last-time-in-2019/
  4. OSXFuse 不开源 https://colatkinson.site/macos/fuse/2019/09/29/osxfuse/
  5. 嵌入式 Rust 的 fmt 优化 https://jamesmunns.com/blog/fmt-unreasonably-expensive/
  6. Docker base image 更新工具 https://github.com/containrrr/watchtower
  7. 运行 Linux 的名片 https://www.thirtythreeforty.net/posts/2019/12/my-business-card-runs-linux/

每周分享第 53 期

  1. GDB Enhanced Features https://github.com/hugsy/gef
  2. Lisp on Lua https://fennel-lang.org/
  3. Django 3.0 https://docs.djangoproject.com/en/3.0/releases/3.0/
  4. Rust Constant Propagation https://blog.rust-lang.org/inside-rust/2019/12/02/const-prop-on-by-default.html
  5. ES2019 features https://javascript.christmas/2019/7

每周分享第 52 期

  1. 传递 Rust 闭包到 C https://readhacker.news/s/4dbWL

  2. SystemVerilog Online http://sv-lang.com/

  3. Java 14 新特性 https://www.javaworld.com/article/3437797/work-begins-on-java-14.html

  4. 在线 or1k 的模拟器 https://readhacker.news/s/4dfqc

  5. 在 macOS 上运行 virt-manager https://github.com/jeffreywildman/homebrew-virt-manager

  6. 关于 SystemVerilog 的博客 http://systemverilog.io/

  7. 结合 VSCode 和 Docker 的开发环境 https://github.com/cdr/sail

每周分享第 51 期

  1. 一个 LaTeX 的 LSP https://github.com/latex-lsp/texlab

  2. Rope 数据结构 https://github.com/cessen/ropey

  3. 一个把 Vivado 工程放 git 中管理的方法 https://github.com/jhallen/vivado_setup

  4. https://github.com/athre0z/color-backtrace

  5. 拿 Arch 当路由器 https://github.com/archwrt

  6. Sourcetrail 开源了 https://www.sourcetrail.com/blog/open_source/

  7. NodeJS 正式支持 ES Module https://medium.com/@nodejs/announcing-core-node-js-support-for-ecmascript-modules-c5d6dc29b663

  8. Rust 的错误处理 https://blog.yoshuawuyts.com/error-handling-survey/

每周分享第 50 期

时间过得真快,忽然就 50 期了。。

  1. CLion 的 C++20 Concept 支持 https://blog.jetbrains.com/clion/2019/11/cpp20-concepts-in-clion/
  2. TypeScript 一些工具 https://github.com/pirix-gh/ts-toolbelt
  3. Rust 编写的 SystemVerilog Parser https://github.com/dalance/sv-parser
  4. MacBookPro 16 英寸 发布
  5. 用 Rust 写 eBPF 程序 https://blog.redsift.com/labs/putting-rust-in-the-kernel-with-ebpf/
  6. 终端里玩蜘蛛纸牌 https://github.com/chrisbouchard/klondike-rs
  7. Rust 的 coverage 工具 https://github.com/mozilla/grcov
  8. 在 Menu Bar 或者 Touch Bar 控制 AirPods Pro 模式 https://github.com/insidegui/NoiseBuddy
  9. Demangle Rust 符号的工具 https://github.com/luser/rustfilt