社区讨论LinuxDo 最新
如何使用8kb实现一个论坛?
嗨,你好!这里是林泽,好久不见。 最近蒸馏finnian的模型,炉子炸掉了,整个人陷入很抑郁的情况。因此我就准备换一个方向去进行探索,也算是缓解一下心情。 于是如题所示,有了这么一篇文章。 事实上,说起论坛,肯定很多人会说,随便找一个 Web 框架,然后找 AI 去跑,大概一个多小时,或者短一点的就十几分钟,我们就可以很轻松地做出一个论坛。 这是 2026 年所有人都最喜欢的 vibe coding,对吧? 非常好,非常的美妙,完全不需要动任何的脑子,很明显就可以很轻松 但是让我们把时间倒转到 1986年。 不妨…
内容摘要
作者:#朱慧月
板块:#开发调优
嗨,你好!这里是林泽,好久不见。
最近蒸馏finnian的模型,炉子炸掉了,整个人陷入很抑郁的情况。因此我就准备换一个方向去进行探索,也算是缓解一下心情。
于是如题所示,有了这么一篇文章。
事实上,说起论坛,肯定很多人会说,随便找一个 Web 框架,然后找 AI 去跑,大概一个多小时,或者短一点的就十几分钟,我们就可以很轻松地做出一个论坛。
这是 2026 年所有人都最喜欢的 vibe coding,对吧?
非常好,非常的美妙,完全不需要动任何的脑子,很明显就可以很轻松
但是让我们把时间倒转到 1986年。
不妨想一下,在如今各种资源如此丰富、物质生活丰富的情况下,让我们忆苦思甜一下,回到 40 年前,来反思一下:40 年前的人们是怎么去实现一个论坛的呢?
其实这个议题的起源,也是因为有一个博主做的一个视频,https://www.bilibili.com/video/BV1SBe56GEPQ
他做得很好,用了大量的 trick,对吧?他叫黑魔法,我叫它 trick,来保证他所有的东西:包括 web server,包括他整个静态资源,全部都塞在了一个 QR code 里面
但是,他的这个挑战对于静态网页来说还是过于简单了。因为事实上,他只需要去维护一个静态的网页,不需要考虑任何的交互,也不需要考虑任何的数据存储。
如果说它只是把一个固定的网页吐出去,对吧?那如果让我们去做一些真正可以应用到生产里的东西呢?
比如说:有用户、有数据、有并发或者有权限之后,那 asm 还可以用、撑得住吗?
对吧?那这个当然肯定不是极简主义,但是我们不妨从这个角度来进一步延伸。
所以,我们的任务就是把大小限制在 8kb,来实现一个健全的论坛系统。
说干就干。
第一版的目标其实很朴素,这个东西只要能跑就行,对吧?
事已至此,先做hello world吧
push SYS_socket
pop rax
push AF_INET
pop rdi
push SOCK_STREAM
pop rsi
xor edx, edx
syscall
mov r12d, eax ;
事实上来说,在汇编上如果想让操作系统去帮助你跑你的软件,首先你就要先把你的需求写给 RAX 寄存器,然后把办这件事情需要的东西写到 RDI 还有 RSI。
它其实这个很约定俗成啊 你可以理解成这就像去银行办业务:
先取号,说你要办什么,对吧?
然后,你要把证件递过去。
之后,你就要把材料放到窗口里面,剩下的就是系统去处理。
系统处理完之后会给你一个回调,就像银行窗口给你回复一样,对吧?
socket 这个系统调用,其实就是"给我造一扇门"。AF_INET 和 SOCK_STREAM 的意思是:一扇走互联网的、一问一答的门,也就是 TCP。
之所以前面先push,然后再 pop 出来,主要原因是因为这样可以省两个字节。之后所有的策略基本上都是以这样的方式展开的。用这种方式我们可以省下将近几百个字节。
操作系统在 rax 里递回这扇门的号码。我把它存进 r12,后面一直要用。
然后两步:
push SYS_bind
pop rax
mov edi, r12d
mov esi, listen_address
...
push SYS_listen
pop rax
bind 是"把门牌号挂上去",也就是端口号;listen 是"开门营业"。
好,那这个门开了,对吧?客人进来了,那接下来就是他说了什么?
.accept:
push SYS_accept
pop rax
mov edi, r12d ;
xor esi, esi
xor edx, edx
syscall ;
test eax, eax
js .accept ;
mov r13d, eax ;
程序启动完之后,大部分时间其实都会停在这个 syscall 里面一动不动。
如果有人去访问它,系统就会在 RAX 里面填一个数字,也就是这个客人的号码牌。之后跟他说的每一句话,都要去 call 这个东西。
如果它返回来的是负数,那就意味着出了一些问题,比如浏览器刚连进来就断开了。那么 js 的意思就是:如果是负数就跳转,当成没发生过,让我们继续等待。
最后一条指令就是把它的返回值存到 r13 里面。从现在开始,r13 就是我们唯一可以跟这个客户端保持联系的方式
浏览器连接上了。
xor eax, eax ; 0 号系统调用:read
mov edi, r13d ; 从client那里读
mov esi, request_buffer
mov edx, REQ_SIZE
syscall
...
mov r14d, eax ; 一共收到了多少字节
mov edi, request_buffer
mov esi, r14d
call http_parse ; 拆开看看他要什么
read,它其实就是读取,它是从 r13 那边开始读,然后读到一块叫 request.buffer 的内存里面。
那么我们读这个系统调用,只需要把 eax 清零就行,对吧?这样其实它会比 move 一个零进去还会省。
我们收到多少字节,就存到 r14,然后就调那个 HTTP 的处理,把这段去拆开
浏览器发来的请求,其实就只是一个纯文本。
POST /new HTTP/1.1
Content-Length: 27
t=hello&b=my+first+post
比如说在 Python 里面,其实这些东西框架都会帮你拆得很好,你拿到的基本上直接就是 Python 里面的字段,对吧?
但是在汇编里面很明显,它太底层了,基本上没有人会给你这样做。当然,其实好像也有 ASM 的库会这样做,但是我们不引入任何的外部库,所以我们需要自己去做一些处理。
处理的方式是类似这样的:
cmp dword [rdi], 'GET '
jne .check_post
xor eax, eax ; 方法 = GET
asm可以只用了一条指令,基本上就可以把一整个单词对比完,这其实还蛮不错的对吧?
如果它不是 GET,那我们再去看它是不是 POST。因为其实我们这个论坛,它至少需要让用户去发送一些信息,然后再获取一些信息。
虽然说我们可以只用 GET,但是最好不要只用 GET,对吧?那这样的话太不优雅了。
在辨识之后,我们就可以去找路径了。
.path_scan:
mov al, [rbx] ; 读一个字节
cmp al, ' '
je .path_end ; 空格:路径结束
cmp al, '?'
je .path_end ; 问号:后面是查询参数,不要
inc rbx ; 往后挪一个字节
jmp .path_scan
这其实就是汇编里面去找一个字符的样子:基本上都是你读一个字节,对比一下,如果不是的话,就往后再挪一个,再来一遍,对吧?
高级语言里面可以一行搞定的事情,我们就只能去做一个循环来处理。
之后它就需要一行一行地扫头信息,然后只需要去找这个 context length,也就是说它正文一共有多长。我会把每个字母都转成小写再比,因为有的客户端写成全小写(对,Chrome 就是你
or al, 0x20 ; 先转成小写
.header_compare_byte:
cmp al, [header_content_length+rcx]
jne .next_header
它就会一个字母一个字母地去跟 content-length 的值和冒号去比。每一个字母在对比之前,都会先去或一个 0x20。
在 ASCII 码里面,大写和小写其实就只差那么一个比特。或上 0x20 之后,它就变成了小写。
最后,我们只需要找到那个空行,空行后面的第一个字节其实就是正文的开头。
然后,我们只需要把方法、路径、正文在哪、正文多长这四项东西,全部放到内存的固定位置,再让这个 HTTP 处理函数去回调它
然后在这个时候,它其实就出现了一个问题。并且我最开始就考虑过了,因为很多时候浏览器在发这个表单的时候,它的正文不一定会跟头部信息一起到。因为网络其实会把这个数据切成一块一块的,那你第一次读,它可能只会拿到一半。
curl不会出现类似的问题,但是任意成熟的浏览器都会采用分段的方式进行处理。事实上来说,蛮多的这种类似的服务器,它其实都会默认说读一次就够了。然后大部分情况下虽然也是够了,对吧?
但是肯定会有一些浏览器,它会只存下半条帖子,所以说我加入了相关的处理。
.read_body:
cmp r14d, REQ_SIZE
资讯来源
LinuxDo