news 2026/9/6 15:50:45

深入 Go sys/unix 系统调用代码生成:从 mkall.sh 到 zsyscall/ztypes 生成文件的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入 Go sys/unix 系统调用代码生成:从 mkall.sh 到 zsyscall/ztypes 生成文件的完整实践

深入 Go sys/unix 系统调用代码生成:从 mkall.sh 到 zsyscall/ztypes 生成文件的完整实践

【免费下载链接】goThe Go programming language项目地址: https://gitcode.com/GitHub_Trending/go/go

本文以 Go 官方仓库中 vendored 在 src/cmd/vendor/golang.org/x/sys/unix/README.md 的构建文档为主体,系统讲解sys/unix包如何通过代码生成体系,把 C 头文件中的系统调用号、错误常量与内核数据结构转换为按 GOOS/GOARCH 组织的z*生成文件。读完本文,你将能够理解旧/新两套生成构建系统(header 驱动与 Docker 容器驱动)的分工、mkall.sh/mkerrors.sh等构建脚本的实际行为,以及为某个架构新增系统调用、常量或类型时应修改哪些组件文件。

sys/unix 包在 Go 仓库中的定位

sys/unix包提供对底层操作系统原始系统调用接口(raw system call interface)的访问。它不直接暴露给普通业务代码,而是为 Go 运行时、标准库以及工具链提供跨 Unix 平台的底层原语。

在当前 Go 仓库中,这个包以 vendored 依赖的形式出现在src/cmd模块下,即路径 src/cmd/vendor/golang.org/x/sys/unix。从该目录的文件列表可以确认,vendor 副本只保留了编译该包运行时行为所需的文件:手写的syscall_*.goasm_*_*.s、以及一批z*前缀的生成文件,外加两个构建脚本 mkall.sh 和 mkerrors.sh。上游模块中的生成器程序(mksyscall.gomksysnum.gomkpost.gointernal/mkmerge等)并未随 vendor 副本保留,本文后续会结合生成文件头部的命令注释来说明它们各自的角色。

需要特别注意的是 README 中给出的一条约束:若使用新构建系统(Docker),其中的脚本/程序不能在宿主机上直接调用,必须从容器内部发起。这一点在 mkerrors.sh 中有强制保护——当GOOS=linux且环境变量GOLANG_SYS_BUILD不为docker时,脚本直接报错退出并提示参考 README:

In the Docker based build system, mkerrors should not be called directly. See README.md

两套构建系统:header 驱动与 Docker 驱动

README 明确指出,为新的架构/OS 组合移植 Go、或为既有组合新增系统调用/类型/常量,都有一些需要手工完成的工作,但工具链已经自动化了其中大部分。当前存在两套生成体系,且正按 OS 逐个迁移到可复现的容器化构建:

旧构建系统(当前用于GOOS != "linux"

旧体系以本机安装的 C 头文件为输入生成 Go 文件,这意味着:

  • 某个 GOOS/GOARCH 组合的文件必须在装有对应 OS 与架构的系统上生成;
  • 不同系统上生成的代码可能不同,差异来自头文件本身的差异。

为控制这种漂移,README 提出两条纪律:只在未修改过头文件的安装环境上生成;并记录文件所基于的 OS 版本(例如 Darwin 14 与 Darwin 15),让每次 OS 升级只对应一次变更,便于追踪。

操作方式为:正确设置GOOSGOARCH后运行mkall.sh,即可为当前系统生成文件;mkall.sh -n只打印将要执行的命令而不执行。依赖为 bash 与 go。

mkall.sh 的参数解析印证了这套语义:-n把执行器run切换为cat、把命令前缀cmd切换为echo,随后整条生成流水线(mkall.sh)只负责把各生成命令拼接出来,再统一交给$run执行。此外该脚本还提供一个 README 未单独展开的实用参数-syscalls:它遍历所有zsyscall*go文件,把每个文件首行注释中记录的生成命令重新执行一遍并 gofmt 回写(mkall.sh)——这正是“每个生成文件头部记录其生成命令”这一约定(下文多处可见Code generated by the command above; see README.md. DO NOT EDIT.)带来的可追溯性。

在旧体系下,mkall.shGOOSARCH逐个分支配置各平台参数,例如 mkall.sh 中:

  • freebsd_386mkerrors-m32mksyscall使用-l32mksysnum从 FreeBSD 源码树的sys/kern/syscalls.master(stable/12 分支)拉取系统调用表;
  • freebsd_arm/netbsd_arm/openbsd_*等 32 位 arm 目标:mktypes额外加-fsigned-char,注释说明这是为了让“裸系统调用 API 在各平台保持一致”;
  • darwin_*openbsd_*:除常规生成外还会执行mkasmgo run mkasm.go),产出与生成 syscall 配套的汇编 stub,例如 zsyscall_darwin_amd64.s、zsyscall_openbsd_amd64.s。

mkerrors.sh 还体现了对生成环境确定性的细节处理:unset LANG并固定LC_ALL=CLC_CTYPE=C,默认编译器为cc(AIX 用gcc),Solaris 下把/usr/gnu/bin前置到PATH以强制使用 GNU 版本工具。

新构建系统(当前用于GOOS == "linux"

新体系用Docker 容器直接从内核与各系统库的源码 checkout 生成 Go 文件,带来两个关键收益:任何支持 Docker 的平台都能一次性生成新体系覆盖的所有文件;生成结果不再依赖执行者本机安装了什么。

其组织结构为:

  • 各 OS 专属文件放在${GOOS}目录中,构建由${GOOS}/mkall.go程序协调;
  • 内核或系统库升级时,修改${GOOS}/Dockerfile以 checkout 新的源码版本。

执行前提是在 amd64/Linux 系统上并正确设置 GOOS/GOARCH,然后运行mkall.shmkall.sh -n同样可以预演命令。依赖为 bash、go、docker。

mkall.sh 中 linux 分支的实现与 README 描述完全一致:

if [[ "$GOOS" = "linux" ]]; then # Use the Docker-based build system set -e $cmd docker build --tag generate:$GOOS $GOOS $cmd docker run --rm --interactive --tty --volume $(cd -- "$(dirname -- "$0")/.." && pwd):/build generate:$GOOS exit fi

即先在${GOOS}(即linux)目录下构建镜像generate:linux,再把上级目录挂载进容器执行。由于 linux 走容器分支,mkall.sh 中针对 aix/darwin/freebsd/netbsd/openbsd/solaris/illumos 的case分支全部服务于旧体系。

从生成文件头部可以交叉验证容器化流程。zsysnum_linux_amd64.go 首行记录的生成命令为:

// go run linux/mksysnum.go -Wall -Werror -static -I/tmp/amd64/include -m64 /tmp/amd64/include/asm/unistd.h

/tmp/amd64/include正是容器内把 amd64 内核头文件 checkout 后的路径——这正是 README 所说“新体系下 mksysnum 在容器内解析头文件”的实物证据,同时也说明 vendor 副本中看不到linux/目录(含mkall.goDockerfile)是因为它们属于上游模块,不是运行时依赖。

组件文件详解

README 的 “Component files” 一节描述了代码生成涉及的各类文件,并给出修改指引。下面逐个结合本仓库中的实际文件展开。

asm 文件:系统调用分发

手写的汇编文件asm_${GOOS}_${GOARCH}.s实现系统调用分发,包含三个入口点:

func Syscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr) func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr) func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr)

前两者是标准入口,差别仅在于能向内核传递的参数个数(3 个对 6 个);第三个供 ForkExec 包装器做底层使用,与前两者的关键区别是不会通知调度器“当前正在执行系统调用”。移植 Go 到新架构/OS 时,每个 GOOS/GOARCH 组合都必须实现这个文件。本仓库的 vendor 副本中可以找到全套实现,如 asm_linux_amd64.s、asm_bsd_amd64.s、asm_aix_ppc64.s 等。

mksysnum:生成系统调用号常量

mksysnum是一个 Go 程序(新体系位于${GOOS}/mksysnum.go,旧体系位于mksysnum_${GOOS}.go)。它读取包含系统调用号声明的头文件列表,解析后产出对应的 Go 数值常量,写入zsysnum_${GOOS}_${GOARCH}.go

以 zsysnum_linux_amd64.go 为例,生成结果就是标准的 syscall 号表:

const ( SYS_READ = 0 SYS_WRITE = 1 SYS_OPEN = 2 SYS_CLOSE = 3 ... SYS_IOCTL = 16 )

README 指出,新增系统调用号通常只需“在足够新的目标 OS 上跑一遍构建”(新体系则是更新容器内的源码 checkout),但视 OS 不同,有时需要修改 mksysnum 的解析逻辑。旧体系下mksysnum的输入形态在 mkall.sh 中可见:传入一个syscalls.master文件的 URL,由程序自行抓取解析。

mksyscall.go:从//sys注释生成系统调用

syscall.gosyscall_${GOOS}.gosyscall_${GOOS}_${GOARCH}.go手写Go 文件,分别实现针对 unix 通用、具体 OS、具体 OS/架构组合的系统调用。其中两类内容:

  1. 需要特殊处理的系统调用,直接写成普通 Go 函数;
  2. 可生成的系统调用,以//sys注释形式声明原型。

mksyscall.go程序解析这些//sys//sysnb注释,将其转换为可执行的 syscall 包装函数。关键约束是:注释中原型的名称必须与zsysnum_${GOOS}_${GOARCH}.go中的某个 syscall 号匹配。原型名可以导出(首字母大写)也可以不导出。

vendor 副本中的 syscall_linux.go 文件头注释直接点明了这种“一个文件两种身份”的用法:

// This file is compiled as ordinary Go code, // but it is also input to mksyscall, // which parses the //sys lines and generates system call stubs. // Note that sometimes we use a lowercase //sys name and // wrap it in our own nicer implementation.

README 给出的“新增系统调用”路径在源码中有典型样本。syscall_linux.go 展示了不导出的//sys原型 + 自定义包装的模式:

//sys FanotifyInit(flags uint, event_f_flags uint) (fd int, err error) //sys fanotifyMark(fd int, flags uint, mask uint64, dirFd int, pathname *byte) (err error) func FanotifyMark(fd int, flags uint, mask uint64, dirFd int, pathname string) (err error) { if pathname == "" { return fanotifyMark(fd, flags, mask, dirFd, nil) } p, err := BytePtrFromString(pathname) if err != nil { return err } return fanotifyMark(fd, flags, mask, dirFd, p) }

这里导出的FanotifyMarkstring参数转换为内核所需的*byte,再把裸调用交给未导出的fanotifyMark。若想让接口形态与裸 syscall 不同,通常就采用这种“未导出//sys+ 手写 wrapper”的做法;而 syscall_linux.go 还展示了用= 常量显式指定 trap 号的写法:

//sys ioctl(fd int, req uint, arg uintptr) (err error) = SYS_IOCTL //sys ioctlPtr(fd int, req uint, arg unsafe.Pointer) (err error) = SYS_IOCTL

生成端的产物是zsyscall_${GOOS}_${GOARCH}.go。zsyscall_linux_amd64.go 首两行记录了生成命令与禁改声明,其后的每个函数都对应一个//sys原型,通过Syscall/Syscall6分发并把错误号经errnoErr转换:

// go run mksyscall.go -tags linux,amd64 syscall_linux.go syscall_linux_amd64.go syscall_linux_alarm.go // Code generated by the command above; see README.md. DO NOT EDIT. //go:build linux && amd64 ... func Fallocate(fd int, mode uint32, off int64, len int64) (err error) { _, _, e1 := Syscall6(SYS_FALLOCATE, uintptr(fd), uintptr(mode), uintptr(off), uintptr(len), 0, 0) if e1 != 0 { err = errnoErr(e1) } return }

可以看到//sys原型名(Fallocate)经 mksyscall 处理后映射到了zsysnum_linux_amd64.go中的SYS_FALLOCATE常量,并生成了 6 参数版本的分发调用。错误路径使用的errnoErr定义在 syscall_unix.go,它对EAGAIN/EINVAL/ENOENT等高频错误做了预装箱以避免运行时分配——这些错误常量正是下面zerrors文件的产物,可见四条生成链在运行期是互相咬合的。

types 文件:godef 管线生成内核数据结构

每个 OS 有一个手写 Go 文件(新体系为${GOOS}/types.go,旧体系为types_${GOOS}.go),其中包含标准 C 头文件,并为相应 C 类型创建 Go 类型别名;文件先被喂给godef得到 Go 兼容定义,再经mkpost.go格式化并剔除隐藏/私有标识符,最终写入ztypes_${GOOS}_${GOARCH}.go

ztypes_linux_amd64.go 的首行完整记录了这条管线:

// cgo -godefs -objdir=/tmp/amd64/cgo -- -Wall -Werror -static -I/tmp/amd64/include -m64 linux/types.go | go run mkpost.go

产物包含指针/长整型大小常量与传给 syscall 的 C 结构体定义:

const ( SizeofPtr = 0x8 SizeofLong = 0x8 ) type ( _C_long int64 ) type Timespec struct { Sec int64 Nsec int64 }

README 指出准备这个文件最难的部分是:搞清楚该包含哪些头文件、以及需要#define哪些宏才能拿到真正传给内核 syscall 的数据结构——一些 C 库出于二进制兼容预置了替代版本,会在 syscall 进出时做翻译,但几乎总存在某个#define可以取回“真实”结构。mkerrors.sh 中的includes_Darwin块就是这类宏技巧的实例:_DARWIN_C_SOURCEKERNEL_DARWIN_USE_64_BIT_INODE__APPLE_USE_RFC_3542#define前置在头文件包含之前,确保生成的是内核态数据结构而非兼容层版本。

新增类型的操作:在文件顶部按需补充 include,再加一行类型别名;若类型在不同架构上差异显著,可能需要用#if/#elif宏。README 给出的示例为types_darwin.golinux/types.go(位于上游模块,vendor 副本未包含,见文末说明)。

mkerrors.sh:错误号、信号与杂项常量

mkerrors.sh用于生成系统的各类常量,不限于错误号/错误串,还包括信号号和大量杂项常量。机制是:

  1. 常量来源是includes_${uname}变量列出的一组 include 文件;
  2. 用正则从中筛出目标#define,生成对应 Go 常量;
  3. 错误号与错误串来自#include <errno.h>,信号号与信号串来自#include <signal.h>
  4. 所有常量由一个 C 程序_errors.c打印出来,最终写入zerrors_${GOOS}_${GOARCH}.go

mkerrors.sh 中includes_AIXincludes_Darwinincludes_DragonFlyincludes_FreeBSD等变量正对应includes_${uname}的写法,每个变量即“该 OS 要参与常量提取的头文件清单 + 必要的#define前置”。

产物 zerrors_linux_amd64.go 头部的两行注释同时暴露了两次生成(mkerrors.sh 组织命令行,内部经 cgo -godefs 编译打印):

// mkerrors.sh -Wall -Werror -static -I/tmp/amd64/include -m64 // Code generated by the command above; see README.md. DO NOT EDIT. //go:build amd64 && linux // Code generated by cmd/cgo -godefs; DO NOT EDIT. // cgo -godefs -- -Wall -Werror -static -I/tmp/amd64/include -m64 _const.go

新增常量的操作:把包含该常量的头文件加入相应变量,必要时调整正则以匹配目标常量;README 特别提醒正则不要过宽,避免误匹配到不想要的常量。

internal/mkmerge:跨架构公共代码归并

internal/mkmerge程序从上述各架构专属生成文件中提取重复的constfunctype声明,合并进每个 OS 的公共文件。归并步骤:

  1. 构造在所有架构专属文件中完全相同的公共代码集合;
  2. 将这部分公共代码写入合并后的文件;
  3. 从各架构专属文件中移除公共代码。

这也解释了生成文件中“共享 + 专属”并存的结构,例如zerrors_linux.go(linux 公共部分)与各zerrors_linux_${GOARCH}.go(架构差异部分)、zsyscall_linux.go与各zsyscall_linux_${GOARCH}.go在目录中成对出现。

生成文件清单:四类z*文件及其来源

把 README 的 “Generated files” 一节整理为速查表,并映射到仓库中真实存在的示例文件:

生成文件内容生成器仓库中的实例
zerrors_${GOOS}_${GOARCH}.go系统错误号、错误串、信号号与全部杂项常量mkerrors.shzerrors_linux_amd64.go
zsyscall_${GOOS}_${GOARCH}.go该 GOOS/GOARCH 下全部生成的系统调用mksyscall.gozsyscall_linux_amd64.go
zsysnum_${GOOS}_${GOARCH}.go该 GOOS/GOARCH 全部系统调用号的数值常量表mksysnumzsysnum_linux_amd64.go
ztypes_${GOOS}_${GOARCH}.go传给(或返回自)syscall 的 Go 类型godefs(types 文件)+mkpost.goztypes_linux_amd64.go

补充两个来自 mkall.sh 的额外生成物:OpenBSD 平台还会由mksysctl_openbsd.go生成zsysctl_${GOOSARCH}.go(sysctl 常量表,见zsysctl_openbsd_*.go系列文件);darwin/openbsd 等由mkasm.go生成配套.s分发 stub(如 zsyscall_openbsd_amd64.s)。

移植与扩展速查:修改哪些文件、怎么验证

综合 README 的操作指引与仓库中的脚本行为,可以把日常修改路径归纳为:

新增一个系统调用

  1. 优先做法:在syscall_${GOOS}.go/syscall_${GOOS}_${GOARCH}.go中新增一条//sys原型(导出名即导出 API),重跑生成;
  2. 需要自定义接口时:写未导出//sys原型 + 手写包装(模式见 syscall_linux.go 的FanotifyMark与 Fchmodat 中“新 syscall 失败再回退旧 syscall”的兼容写法);
  3. 验证:生成文件中对应函数应出现,且 trap 参数引用了zsysnum_*中的常量。

新增一个常量:把目标头文件加入mkerrors.shincludes_${uname}变量,必要时收紧正则,避免误匹配。

新增一个类型:在${GOOS}/types.go(旧体系types_${GOOS}.go)补 include 与类型别名行,架构差异大时用#if/#elif;确认生成进ztypes_${GOOS}_${GOARCH}.go

内核/系统库升级(linux):修改linux/Dockerfile中的源码 checkout 版本,然后在 amd64/Linux 上运行mkall.sh重新生成全部 linux 组合;mkall.sh -n可先预演。

旧体系平台:在对应 OS/架构的“干净”安装上设置GOOS/GOARCH后运行mkall.sh,并记录所基于的 OS 版本。

vendor 副本的边界说明

为避免误用,最后明确本仓库中该目录的实际边界:

  • vendor 副本包含:README.mdmkall.shmkerrors.sh、全部syscall_*.go/asm_*_*.s手写与生成文件、全部z*生成文件;
  • vendor 副本不包含:各生成器 Go 程序(mksyscall.gomksysnum.gomkpost.gomkasm.gomksysctl_openbsd.go)、internal/mkmerge、各 OS 的types.go/types_${GOOS}.go以及 linux 的linux/目录(mkall.goDockerfile)。

这与 vendor 机制“只保留编译所需文件”的行为一致:本文引用的各生成文件首行命令注释(如go run mksyscall.go ...cgo -godefs ... | go run mkpost.go)记录的是上游模块内的真实生成命令,可在生成文件中直接查证,而生成器本身需要到上游golang.org/x/sys模块中查看。

适用前提:以上所有构建流程都要求先正确设置GOOS/GOARCH;linux 组合额外要求 amd64/Linux 宿主机与 Docker;旧体系要求各平台本机装有未修改的头文件。若你只需要使用sys/unix的 API,则无需参与任何生成流程,直接使用已提交的z*文件即可。

【免费下载链接】goThe Go programming language项目地址: https://gitcode.com/GitHub_Trending/go/go

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 15:45:11

Docker记录:误删宿主机挂载目录导致 PostgreSQL 数据丢失

Docker记录&#xff1a;误删宿主机挂载目录导致 PostgreSQL 数据丢失1. Docker启动PostgreSQL服务2. 我删了什么&#xff1f;3. Ooops&#xff01;问题来了4.问题核心&#xff1a;为什么删掉宿主机目录后&#xff0c;容器数据就访问不了&#xff1f;5.总结最近在使用 Docker 部…

作者头像 李华
网站建设 2026/9/6 15:44:23

变电站巡检机器人总体设计方案与关键技术选型指南

简介&#xff1a;一份面向电力行业与智能装备领域的《变电站巡检机器人总体设计方案》技术文档&#xff0c;系统回应人工巡检效率低、成本高且受天气影响等痛点&#xff0c;适合电力运维人员、机器人研发工程师及自动化专业学生参考。资源为单个doc文档&#xff0c;压缩包大小1…

作者头像 李华
网站建设 2026/9/6 15:41:53

Security Audit Report

Security Audit Report 【免费下载链接】agent-skills Production-grade engineering skills for AI coding agents. 项目地址: https://gitcode.com/GitHub_Trending/agentskill/agent-skills Summary Critical: [count]High: [count]Medium: [count]Low: [count] Fi…

作者头像 李华