返回 AI 资讯
社区讨论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

原文链接

打开原文