Hotdry.

Article

用汇编写 Web 服务器:在「缺乏意义」中寻找个人工程意义

解析 imtomt 用纯 ARM64 汇编写 macOS HTTP 服务器的工程哲学:为什么放弃生产效率换个人意义,及底层系统调用裁剪实践。

2026-05-10systems

在 Hacker News 的 Show HN 板块中,有一个独特的项目引发了不少关注。imtomt 在 imtomt.github.io 上分享了他的 AArch64 汇编语言 HTTP 服务器项目,标题带着一丝自嘲式的哲学意味 ——「Building a web server in assembly to give my life (a lack of) meaning」。这个项目与昨日讨论的 x86-64 Linux 汇编 HTTP 服务器不同,它运行在 Apple Silicon 的 macOS 环境下,使用原始 Darwin 内核调用,完全不依赖任何标准 C 库。这种「放弃生产效率换取个人意义」的工程哲学,对于习惯了高生产率框架的现代开发者而言,是一个值得深思的实验。

为什么选择「无意义」的工程路径

现代软件开发强调效率、可维护性和团队协作。选择汇编语言编写生产级应用,几乎是所有工程决策树中最不被推荐的那个分支。这并非因为汇编无法完成任务,而是因为它的成本收益比在绝大多数业务场景下是负数。然而,当我们把视角从「产出」转向「过程」,从「交付」转向「理解」,情况就会发生变化。imtomt 在项目介绍中提到的「give my life meaning」,并非真正的虚无主义,而是一种对工程师成长路径的重新审视。当日常工作被胶水代码、配置管理和框架调用填满时,回到机器最原始的指令集,重新理解计算机实际在做什么,反而成了一种有价值的认知回归。

这种「反效率」工程在技术社区有着悠久的传统。从 Ken Thompson 用汇编重写 Unix 的早期探索,到 Dave Gillespie 坚持用多种奇怪语言实现同一个程序的学术实验,再到当代众多开发者用晦涩技术栈完成日常任务的博客文章,这类项目的价值不在于最终产物的生产可用性,而在于过程中的深度学习与自我挑战。对于 imtomt 这样的独立开发者而言,用三个月时间从零开始编写一个可以正确处理 HTTP 请求的汇编程序,其收获远比三个月使用 Rails 或 Spring 开发十个 CRUD 应用更有助于建立对系统的深层理解。

AArch64 与 Darwin 内核调用的特殊性

imtomt 选择 Apple Silicon 的 AArch64 架构作为目标平台,这一选择本身就带有技术挑战性。与更为常见的 x86-64 Linux 汇编相比,AArch64 在 macOS 上的系统调用接口更加封闭。Linux 的 syscall 接口虽然文档质量参差不齐,但至少是公开且相对稳定的;macOS 的 Darwin 内核调用则被明确标记为私有 API,Apple 官方不保证跨版本兼容性。这意味着项目作者必须从系统 C 头文件中提取常量定义,自己生成汇编可用的符号表,而无法依赖 man page 中完整的系统调用说明。

项目使用 raw syscall 而非 libc 包装函数,意味着作者必须手动构建符合 macOS 内核期望的参数序列。在 ARM64 架构上,系统调用通过 svc(Supervisor Call)指令触发,参数通过 x0 到 x7 寄存器传递,返回值写入 x0。创建 socket 需要在 x0 放置 AF_INET(值为 2),x1 放置 SOCK_STREAM(值为 1),x2 放置 IPPROTO_TCP(值为 6),然后执行 svc 0x2000005(macOS x86-64 的 socket 调用号是 0x2000005,AArch64 使用不同的调用号体系)。任何参数位置的偏差都会导致内核拒绝调用或返回难以追踪的错误。

最小化系统调用集合的裁剪实践

一个功能完整的 HTTP 服务器在高级语言中可能需要数百个标准库函数调用,而在 imtomt 的汇编实现中,系统调用数量被严格控制在十几次以内。这不是刻意炫耀的结果,而是汇编级开发中必然的成本计算 —— 每次系统调用都涉及寄存器状态的精确管理和错误返回值的判断,实现每个功能前都需要权衡其必要性与实现成本。

项目仅依赖的核心系统调用包括:socket 创建套接字、bind 绑定地址端口、listen 开始监听、accept 接收连接。对于每个已建立的连接,通过 fork 创建子进程处理并发请求(macOS 的 fork 在 ARM64 上的行为与 x86-64 有微妙差异,特别是线程局部存储的处理)。读取请求使用 read 系统调用,写入响应同样使用 write,关闭连接使用 close。这六个系统调用组合构成了服务器的全部 I/O 操作基础。

在 HTTP 语义层面,项目实现了 HTTP/1.1 的一个核心子集:支持 GET、HEAD、PUT、OPTIONS、DELETE 五种方法,每个方法都有对应的处理路径。对于 GET 请求,服务器根据 URL 路径查找文件系统中的对应资源;如果请求的是目录且目录下存在 index.html,则返回该文件内容;否则生成目录列表的 HTML 响应。项目还实现了 RFC 7233 定义的字节范围请求(byte range)支持,允许客户端请求资源的部分内容,这在处理大文件下载和媒体流场景中非常重要。

安全性与边界处理的汇编级实现

在高级语言中,安全防护往往通过框架内置的机制或第三方库处理,而在汇编实现中,每个安全检查都需要手动编码。项目在文件访问层面实现了两个关键的保护机制:路径遍历防护和符号链接限制。

路径遍历攻击利用 URL 编码的特殊字符(如 ../)尝试访问服务根目录之外的文件。在汇编中实现这种防护需要逐字节解析请求路径,在遇到 .. 序列时检查其前一个字符是否为路径分隔符,如果是则拒绝请求并返回 403 Forbidden。符号链接攻击则试图通过目录中的符号链接访问受保护的文件或目录,这需要在对每个路径进行文件属性检查时额外判断其是否为符号链接(通过 lstat 而非 stat 系统调用),如果是则拒绝访问。

网络层面的安全考虑同样重要。项目实现了针对 Slowloris 攻击的动态超时机制。Slowloris 攻击通过缓慢发送 HTTP 请求头的方式占用服务器连接资源,使服务器无法响应其他正常请求。在汇编中实现这一防护需要在每次 read 调用后启动计时器,如果超时时间内请求未完成则强制关闭连接。macOS 的 select 系统调用可以用于实现带超时的 I/O 操作,但在 fork-per-request 模型中,超时逻辑通常内嵌在子进程的主循环中,通过持续检查自连接建立以来的时间戳来判定是否超时。

文件系统操作的手动实现

在没有标准库的情况下,项目必须手动实现通常由库函数提供的功能。最基础的需求是字符串处理:解析 HTTP 请求行需要逐字节扫描空格分隔的各字段,比较请求方法字符串与预定义常量,提取 URL 路径部分。这些在 C 中可能只是一行 strcmpstrstr 的操作,在汇编中需要用循环和条件跳转手动编码。

数字转换是另一个必须自己实现的功能模块。HTTP 协议中的多个场景需要整数与字符串之间的双向转换:Content-Length 头需要将二进制长度值转换为十进制 ASCII 字符串,字节范围请求的起始和结束偏移需要将字符串转换为二进制值,目录列表中显示的文件大小也需要类似的转换。这些转换在汇编中的实现涉及除法循环(对于 itoa)和乘法循环(对于 atoi),每个循环都需要处理进位溢出和零值边界情况。

目录列表页面的 HTML 生成是手动实现的另一个典型场景。服务器需要读取目录内容(通过 getdirentries 系统调用),对每个条目进行文件名长度计算(用于写入 Content-Length),应用 URL 转义规则处理特殊字符(空格、引号、百分号等),拼接完整的 HTML 响应字符串(包括 HTTP 头和 HTML 标签)。任何一个步骤的错误都会导致生成的页面出现乱码或格式错误。

信号处理与进程生命周期管理

macOS 的信号处理在 ARM64 架构上有一些鲜为人知的特殊性。项目需要处理 SIGINT 信号以实现优雅关闭:当用户按 Ctrl+C 终止服务器时,主进程应该首先停止接受新连接,然后向所有子进程发送 SIGTERM 等待它们完成当前请求处理,最后才退出。

在汇编中调用 sigaction 设置信号处理函数时,需要特别注意 sigaction 结构体中的 sa_tramp 字段。这个字段是 macOS 特有的实现细节,用于在信号处理函数执行完毕后正确恢复用户态的指令指针和寄存器状态。如果不设置 sa_tramp 或设置错误,信号处理函数返回后程序可能跳转到错误的地址继续执行,导致不可预期的行为。在 Linux 的 sigaction 实现中无需考虑这个字段(通常设为 0),但在 macOS 上必须指向一个有效的 trampoline 代码片段。

原子性 PUT 操作的处理展示了进程间协作的汇编级实现。HTTP PUT 请求需要将客户端上传的数据写入服务器指定的文件路径。为了保证操作的原子性(避免部分写入的数据污染原文件),项目采用「先写临时文件再重命名」的策略:客户端数据首先写入一个临时文件(通常在目标文件同一目录下,文件名附加随机后缀),只有在数据完全接收并验证后才使用 rename 系统调用将临时文件原子性地替换为目标文件。这个过程中任何一步的失败都需要清理已创建的临时文件,并返回适当的错误状态码。

对现代工程实践的启示

imtomt 的项目可能永远不会进入生产环境,但这不代表它没有工程价值。对于习惯在高级抽象层次工作的开发者而言,理解汇编层的实现细节有助于建立更准确的 mental model。当你知道一个 HTTP 请求从网卡到达最终返回响应需要经过多少个系统调用、多少次上下文切换、多少字节的内存拷贝时,你在使用框架时就更能理解为什么某些操作有延迟,某些配置有意义。

「意义驱动」的工程哲学也值得认真对待。当技术社区越来越强调「用正确的工具做正确的事」、强调生产效率和最佳实践时,偶尔选择一条「错误」的道路并坚持走完,可能恰恰是防止职业倦怠的良药。项目标题中的「lack of meaning」是一种自嘲,但自嘲背后是对工程活动本身意义的追问:除了交付功能,除了让用户满意,除了让系统稳定运行,工程师是否还有权利用代码实现本身获得满足感?

从纯技术角度,这个项目还提醒我们注意平台差异的深度。即使是最通用的 POSIX 标准也无法涵盖所有系统调用的细节。macOS 与 Linux 在信号处理、文件描述符语义、进程创建行为上的差异,在高级语言中通常被框架或运行时隐藏,而在汇编级开发中必须直面。这些差异的存在提醒我们,跨平台抽象是有代价的,理解底层差异对于构建真正可靠的系统至关重要。

资料来源:imtomt 的 AArch64 汇编 HTTP 服务器项目(imtomt.github.io/ GitHub imtomt)以及 daily.dev 社区对该项目的介绍页面。

systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com