普通视图

发现新文章,点击刷新页面。
今天 — 2025年10月28日首页

马斯克的 AI 百科 Grokipedia 刚发布就翻车:抄维基百科被抓现行,还夹带私货?

作者 李超凡
2025年10月28日 18:17
上个月,马斯克说要用 AI 打造一个比维基百科有巨大改进的产品 Grokipedia,甚至将其定位为「xAI 理解宇宙目标的必要步骤」。

今天 Grokipedia 正式发布了,虽然是 0.1 版本,但马斯克表示也已经比维基百科要强

所以,这个 Grokipedia 到底是个啥?简单说,就是个 AI 全自动生成的百科。马斯克声称,这是为了干掉维基百科那帮「有偏见的编辑」。

体验🔗 http://Grokipedia.com

▲温馨提示:目前 Grokipedia 建议用英文搜索,中文搜索体验目前比较差

Grokipedia 的模式挺有意思,所有条目均由 AI 模型 Grok 自动生成,目前已收录超过 88.5 万个词条。

与维基百科由全球人类志愿者众包、编辑、审核的模式截然不同,Grokipedia 的用户无法直接编辑内容,只能「请求 Grok 修改」。

AI 百科,还是维基百科的「搬运工」

然而首批冲进去吃瓜的网友们,很快发现了一个让人哭笑不得的事实,Grokipedia 上的大量词条,简直就是从维基百科 Ctrl+C、Ctrl+V 过来的。

搜一下「MacBook Air」,拉到页面最底下,一行小字让我眼前一亮:「内容改编自维基百科……」

但像 PlayStation 5 这种词条,就连改都懒得改了,几乎是把维基百科原文原封不动地搬了过来,连标点符号都没怎么换。字对字、行对行,堪称像素级复刻。

所以马斯克说的「比维基百科强 10 倍」,指的是复制速度快 10 倍吗?

维基百科那边估计也无语了,发言人 Lauren Dickinson 的回应堪称神吐槽,大意就是:「谢邀,我们很好。但看起来,即使是 Grokipedia,也需要依赖维基百科才能存在。」

杀伤力不大,侮辱性极强。

AI 去偏见,还是「换一种偏见」

当然,Grokipedia 也不是所有内容都照搬,Grokipedia 也展现出一些完全不同的的特点,但同样充满争议。

就拿马斯克点赞转发的这个案例来说,是对乔治·弗洛伊德之死的总结对比。这起案件当时引发了美国「黑人的命也是命」(Black Lives Matter)运动,十分轰动。

APPSO 把原文和翻译都放出来,给大家感受一下:

维基百科:

乔治・佩里・弗洛伊德(George Perry Floyd Jr.,1973 年 10 月 14 日 —2020 年 5 月 25 日)是一名非裔美国人。2020 年 5 月 25 日,在明尼苏达州明尼阿波利斯,因一名店员怀疑他使用了一张 20 美元假钞,他在被逮捕期间遭到一名白人警察谋杀。[2] 德里克・肖文(Derek Chauvin)是抵达现场的四名警察之一,他跪在弗洛伊德的颈部和背部超过九分钟,导致其窒息死亡。[3] 在他被谋杀后,一系列反对警察暴行(尤其针对黑人的暴行)的抗议活动迅速在全球和美国范围内蔓延。他的临终遗言 “我无法呼吸” 成为了抗议口号。

Grokipedia :

乔治・佩里・弗洛伊德(George Perry Floyd Jr.,1973 年 10 月 14 日 —2020 年 5 月 25 日)是一名美国人,1997 年至 2007 年间在得克萨斯州有长期犯罪记录,包括持械抢劫、持有毒品和盗窃的定罪。[1][2] 2020 年 5 月 25 日,弗洛伊德因一名店员举报他使用 20 美元假钞购买香烟,在明尼苏达州明尼阿波利斯被逮捕。[3][4] 逮捕过程中,弗洛伊德表现出反抗,甚至在被按倒前就反复称自己无法呼吸,警官德里克・肖文跪在弗洛伊德颈部长达 9 分 29 秒,期间弗洛伊德失去反应。

[3] 亨内平县法医裁定,弗洛伊德的死因为心肺骤停,由执法过程中的制服、约束和颈部压迫引发,促成因素包括严重的高血压动脉粥样硬化性心血管疾病、芬太尼中毒和近期甲基苯丙胺使用;毒理学检测显示其血液中芬太尼含量为 11 ng/mL,这一水平在无耐受性个体中与过量致死相关。[5][6][7] 肖文于 2021 年被判二级非故意谋杀、三级谋杀和二级过失杀人罪,获刑 22.5 年。[8][9]

弗洛伊德的死亡通过旁观者视频广泛传播,引发了美国各地和国际上的大规模民事动乱,包括造成数十亿美元财产损失的骚乱,同时也引发了关于警察武力使用、弗洛伊德的健康和药物使用在其死亡中的作用,以及执法部门与表现出不服从或中毒的嫌疑人互动时的系统性问题的争论。[10]

看出区别了吗?

维基百科版开头第一句,「非裔美国人」,被「白人警察谋杀」。重点是「我无法呼吸」和全球抗议浪潮,充满了对种族不公的批判和对受害者的同情。整个基调是关于种族歧视和社会不公。这是一个典型的、强调社会结构性问题的左翼叙事。

Grokipedia 版开头先给你科普弗洛伊德的「犯罪前科」,然后强调他当时「反抗执法」,还详细描述了他体内的毒品含量和尸检报告里的其他死因。最后,把声势浩大的抗议活动,轻描淡写成「造成数十亿美元财产损失的骚乱」。

维基百科是在讲一个「种族压迫下的悲剧」,而 Grokipedia 在讲一个「有前科的瘾君子在拒捕过程中的意外死亡,并引发了社会失序」。

Grokipedia 也没有虚构事实,但它通过信息的筛选、排序和补充,巧妙地重塑了整个事件的性质。这里先不探讨谁是最客观中立的,但的确展现了两种不同的立场。

就像一些网友质疑的,Grokipedia 背后的 AI 并非没有偏见,而是用一种更隐蔽、更具迷惑性的方式,来展示一些立场。

同样的手法也用在了「气候变化」词条上。

维基百科直表示「科学界几乎一致认为气候变暖是人类活动造成的。」

Grokipedia 呢?它不直接反驳,它会说:「有批评者认为,所谓『科学共识』的说法夸大了事实……」然后暗示媒体和环保组织在「制造恐慌」,引导你走向一个预设的怀疑主义结论。

马斯克试图打造的,是一个由中心化 AI 控制、符合其自由主义叙事的「事实过滤器」

然而当百科「编辑」的权力从千万志愿者手中收归于一个不透明的算法时,我们得到的究竟是更纯粹的知识,还是更精致的牢笼?

马斯克说「你将能够要求 Grok 添加/修改/删除文章」,那最终裁决权在谁手里?在 Grok,也就是在 xAI,也就是在马斯克本人。

这才是 Grokipedia 最令人不放心的地方,同时也在告诉我们:在 AI 时代,关于「事实」的定义权,正在成为新的战场

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


昨天 — 2025年10月27日首页

Swift 官方发布 Android SDK机 - 肘子的 Swift 周报 #108

作者 Fatbobman
2025年10月27日 22:00

10 月 24 日,Swift Android 工作组发布了 Swift SDK for Android 的 nightly 预览版本。这标志着该 SDK 从早期的内部测试阶段迈入了官方支持阶段,也意味着 Swift 在跨平台之路上又向前迈出了一大步。

昨天以前首页

2025 年,连理光 GR 也有平替了?

作者 周奕旨
2025年10月26日 20:57

最近有个「理财产品」,叫理光 GR IV。

这台定焦相机小巧、可以随时放进兜里,滤镜诱人,选择多样,让它成了当之无愧的相机圈顶流之一。

但现实是,排队加价,一机难求。

没关系,如果你只是痴迷那种 GR 特有的味道,这里还有一台 GR 的「精神化身」——真我 GT8 Pro。

没错,我们熟悉的 realme,和理光联名了。

当一台 GR IV 的售价已经破万的时候,3999 元起步价的真我 GT8 Pro 就显得格外有性价比。

现在摆在我们面前的,就只剩一个问题: 这手机,到底学到了理光几分?

GR 的精髓,是抬手就拍

很多人以为,GR 的灵魂是滤镜。

是,但不全是。

GR 之所以是 GR,首先在于它提供了一套完整的体验逻辑——抬手就拍,是 GR 的关键。

打开真我 GT8 Pro 的相机,你会发现一个专门定制的「理光 GR 模式」。

放眼看去,这就是真我学习 GR 体验的成果:

在取景器底部,你只能在 28mm 和 40mm 两个焦段中切换,这是 GR 的经典配置。

28mm 是 GR 系列一贯以来的「人文之眼」,视角广阔,容纳环境;40mm 则是出现在 GR IIIx 的「观察之眼」,更专注,也更不打扰主体。

没有 2X、3X、5X,没有那些花里胡哨的变焦滑块, 这是一种「减法」,它强迫你用脚步代替变焦,去集中注意力思考画面。

抬头看,在屏幕顶部,你能看见「Snap」图标。

这是 GR 另一个标志性的功能。

简单说,它允许你「预设对焦距离」,比如 2.5 米,当你按下快门的瞬间,相机根本不需要对焦,直接拍下 2.5 米处清晰的画面。

这是「街拍神器」的灵魂——在街头,决定性的瞬间转瞬即逝。

等你举起手机、点击屏幕、拉动对焦框……那只打哈欠的猫早就走了,而「快拍」,就是为了消灭这个对焦过程,为了那个「非完美但真实」的瞬间。

realme 把这套逻辑原封不动地搬了过来,甚至还做了一个「沉浸式取景器」,隐藏所有 UI 元素,让你像用光学取景器一样,只专注构图。

到这里,我得说,realme 至少在「架势」上,已经学到了五成,它理解了 GR 不是一台相机,而是一种体验。

胶片影调,功力几成?

好了,架势摆好,我们再来看「味道」,这才是联名的「里子」。

真我 GT8 Pro 复刻了 GR 经典的五大胶片影调,我们挑三个最有代表性的,和 GR IV 硬碰硬。

正片 (Posi)

这是 GR 最受赞誉的模式,它的特点是色彩浓郁,影调硬朗,尤其对蓝、红色的表现力很强。

▲ 上:真我 GT8 Pro / 下:Ricoh GR IV

▲ 上:真我 GT8 Pro / 下:Ricoh GR IV

从对比来看,除了红色的亮度稍微低了一些,真我 GT8 Pro 与 GR4 的正片基本没有区别,颜色倾向、对比度和整体氛围都非常接近,可以说有九分相似。

结论:学到九成。

这味儿太正了,如果不是 100% 放大看,的确很难分辨,真我抓住了「正片」的精髓:浓郁但不艳俗,厚重且有氛围。

负片 (Nega)

如果说「正片」是热烈的,那「负片」就是柔和的, 它的饱和度偏低,对比度偏低,画面常有一种淡淡的忧郁感。

▲ 上:真我 GT8 Pro / 下:Ricoh GR IV

真我这边将负片的特点也抓得八九不离十——色调压得很准,绿色偏暖,对比度低,除了光比较大的时候,计算摄影的介入会让画面出现轻微差异,但整体风格是对的。

结论:学到八成。

到这里,我已经有点惊喜了,realme 确实在「全行业最深的一次影像联名」这句话上,花了大功夫。

高对比黑白 (Hi-Contrast B&W)

但别急,还有重头戏——高对比黑白,这是 GR 最别具一格的模式,来源于森山大道。

它的特点是极高的对比度,暗部死黑,亮部过曝,颗粒感强,这是力量感和冲击力的代名词,也是这次联名最难的考卷。

▲ 上:真我 GT8 Pro / 下:Ricoh GR IV

▲ 上:真我 GT8 Pro / 下:Ricoh GR IV

说实话,我觉得真我 GT8 Pro 的高对比黑白,还是有点「收着」——对比度没有拉到 GR 那么极限,颗粒也没有那么粗犷,但换个角度想,这样保守的高对比黑白模式也更容易上手,更不容易翻车。

结论:学到八成。

如果你是 GR 的忠实用户,总觉得对比度和颗粒还不够有味道,也别急——

GT8 Pro 几乎把 GR 所有的自定义参数都给你了。

在任何一个影调下,你都可以自定义调节:饱和度、色相、影调、对比度、锐度、明暗、清晰度,甚至还有「颗粒」的强度和大小。

你可以亲手把刚刚那个「干净」的高对比黑白,调得和 GR 一样极限,调完之后,你还能把它存为 U1、U2 这样的用户预设,最多支持 6 个。

有句俗话说:

玩 GR,就是玩自定义参数

而真我 GT8 Pro,的确是把精髓学了过来。

在看完了所有对比后,我们回到开头的问题。 真我 GT8 Pro 这次,学到了几成?

综合刚刚的表现来看,我的答案是,八成,甚至九成。

它在「形」上做到了复刻,将 GR 抬手就拍的体验尽力地移植了过来;

在「味」上做到了 90% 还原,算法在减少硬件差距,自定义给了多元化的可能。

老实说,realme 这个选择非常聪明——

这个以往并不以影像见长的品牌,正用风格化联名这条已经被验证的路径,快速杀入了这个大战正酣的市场。

现在,移动影像可谓是百家争鸣,有哈苏的厚重,有徕卡的德味,有蔡司的通透,又迎来了理光的多样化。

留给其他手机厂商的相机品牌,真的不多了。

让我有个美满旅程

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


去 Apple Store 修手机 - 肘子的 Swift 周报 #107

作者 Fatbobman
2025年10月20日 22:00

父亲的 iPhone 16 突然无法充电。预约后,我前往 Apple Store 送修。工作人员确认问题后,为我提供了一部 iPhone 14 作为备用机,并协助完成数据转移。十二天后(期间正好赶上一个长假),设备维修完成——更换了 Type-C 接口,同时还免费更换了一块新电池。体验一如既往地令人满意。

ChatGPT 成人模式要来了,但作为成年人我一点都不高兴

作者 莫崇宇
2025年10月15日 13:46

今天凌晨,OpenAI 的 CEO 山姆·奥特曼宣布,ChatGPT 将在 12 月推出「成人模式」。

奥特曼长篇大论地解释了一通,大意是说,ChatGPT 一开始设置那么多限制,主要是担心心理健康问题,怕出事,结果搞得普通用户觉得体验拉胯、不够有趣。

现在 OpenAI 号称已经掌握了新的安全工具,能够减轻心理健康相关的主要风险,所以准备放开手脚了。到了 12 月,经过验证的成年用户可以解锁更多内容,包括情色类内容。

没错,就是你想的那个意思。

一句话概括,OpenAI 声称是时候要把成年人当成年人对待了。不过,作为一名成年人,我却一点都高兴不起来。

另外,按照奥特曼的说法,接下来几周 OpenAI 还会推出一个更有人情味的 ChatGPT 新版本,类似大家之前喜欢的 4o 那种感觉。

你想让它回复更温暖点?没问题。喜欢它狂发 emoji?也行。甚至希望它像朋友一样跟你聊天?统统都可以。

在与网友的问答中,奥特曼还回应了更多细节。

其实 ChatGPT 这事早有征兆。

去年底奥特曼就透露过支持成人模式的计划,当时有网友建议移除模型的大部分护栏,奥特曼当场表态:「肯定需要某种『成人模式』。」

在当时 OpenAI 面向用户征集的新功能投票里,这事一度高居榜首,和 AGI、Agent、升级版 GPT-4 等一起被列入 2025 年产品计划,足见其受重视程度。

而按照 OpenAI 官方博客,目前年龄验证功能够自动识别未成年用户并切换到青少年安全模式,拦截露骨色情内容;如果无法确定年龄则默认视为未成年,只有提供年龄证明后才能解锁成人功能。

听起来很周全对吧?但细看下来,事情可没那么简单。

听起来很周全对吧?但细看下来,事情可没那么简单。

即便 OpenAI 未来采用身份证件或付款信息核验等方式,规避手段仍然层出不穷。未成年人使用父母身份证件验证、通过成年人代为注册账号,这类操作在互联网产品中早已屡见不鲜。

更关键的是,OpenAI 声称开发了新工具来检测用户心理状态,但 AI 是否真能准确判断一个人的心理健康程度?

要知道,最近几年里,涉及 ChatGPT 的悲剧事件屡有发生,OpenAI 所宣称的「安全阀」是否真能起到应有的保障作用,仍然令人质疑。

有一说一,OpenAI 甚至不是第一个宣布支持「成人模式」的有影响力的 AI 产品,某种程度上还算是保守派。马斯克旗下的 AI 聊天机器人 Grok 才是真·无所顾忌。

今年 7 月份马斯克更是为 Grok 加入了 3D 虚拟伴侣角色功能。

付费订阅「SuperGrok」用户(月费 30 美元)可以启用两款 3D 伴侣:一位是动漫风的金发双马尾少女「Ani」,另一个是卡通红熊猫「Bad Rudy」。

其中 Ani 有着精致的二次元形象,酷似动漫角色弥海砂。Ani 支持文本、语音和摄像头多模态互动,能根据对话内容做出丰富表情和动作,甚至可听令跳舞。

此外,Ani 还内置了好感度机制和记忆模式。用户与其互动能提升虚拟好感度,达到一定等级即可解锁 NSFW(成人)模式。

当时有网友评价:「简直就是高级 Galgame(恋爱游戏)。」这也让 Ani 上线一天就收获大量自发创作内容,迅速在社交媒体出圈。

只是,Grok 也好,ChatGPT 也罢,这些功能表面上打着「尊重成年用户自由」的旗号,但问题在于当年龄验证机制一旦存在漏洞时,开放成人内容实际上是在降低未成年人接触不当内容的门槛。

如果所谓的成年人权利实际上是在拿未成年人的心理健康和成长环境做赌注,赌青少年不会钻空子,那么更严格的限制或许更为稳妥。

而更直白地说,这些功能的商业逻辑依然是争夺用户流量和提升付费转化率。

说白了,AI 产品的粘度普遍不高。

大多数用户都是秉持着哪个好用用哪个的原则,而且专业用户(科研、编程等场景)愿意付费的人数有限,那怎么办?他们选择用更加拿捏人性的方式来让你留下(或者成瘾)。

那就是往「欲望」那边靠。开放成人向功能,一方面能吸引大量新用户尝鲜,满足被过滤掉的需求;另一方面,付费意愿也会大幅提升。

色情和猎奇内容历来是互联网流量催化剂。开放成人向功能,一方面能吸引大量新用户尝鲜,满足被过滤掉的需求;另一方面,付费意愿也会大幅提升。

而更进一步的,其实是情感陪伴这块大蛋糕。

当前 AI 陪伴类产品的核心用户多为年轻网民和特定圈层人群(如二次元爱好者、社恐人群等),但这一版图正持续扩张。年轻人对数字伴侣的接受度最高,不少年轻用户已将 AI 视为日常数字生活的一部分,不仅用于搜索问答,也愿意倾诉情感。

投资机构 ARK Invest 甚至预测,「AI + 情感陪伴」全球市场规模将从每年 3000 万美元飙升至 700 亿~1500 亿美元,年均增长率超过 200%。

但问题是,心理学研究早已证实,人类容易对展现共情的对象产生依恋——哪怕明知对方是程序。这也意味着 AI 存在被情感操控的风险。

目前看,各国监管正在行动。

欧盟的 AI 法案提及高风险 AI 需防止对儿童不利;中国的《生成式人工智能服务管理办法》也强调提供服务应符合未成年保护法等等。

OpenAI 前阵子也主动推出了「青少年模式」:家长可通过邮箱将自己账户与 13 岁以上孩子账户关联,甚至设置宵禁时段。当系统检测到青少年有严重情绪困扰时,还会给家长发送提醒。

也许十年后,拥有一个 AI 伴侣会像今天养宠物一样稀松平常。

但那些被 AI「完美关系」喂养大的一代人,还能不能理解真实人际关系?当你习惯了一个永远不会拒绝你、永远理解你、永远顺着你的 AI,你还有勇气面对一个会跟你吵架、会让你失望、需要你付出的真人吗?

act like a friend,或许是最诱人也最致命的谎言。它学习你的语言习惯、迎合你的价值观、满足你的欲望,然后把这一切包装成懂你的样子反射回来。

这是最好的时代,因为没有人再孤独。这是最坏的时代,因为所有人都在孤独。12 月份的 ChatGPT 或许只是个开始。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


高通收购 Arduino:历史的轮回 - 肘子的 Swift 周报 #106

作者 Fatbobman
2025年10月13日 22:00

上周,高通宣布收购知名开源硬件平台 Arduino,并同步发布首款搭载自家芯片的 Arduino UNO Q。与经典版本不同,UNO Q 采用了“双脑”架构——由运行 Linux 的 Qualcomm Dragonwing 处理器负责高性能计算,同时保留 STM32 微控制器以执行实时控制任务。这种设计无疑强大,却也悄然偏离了 Arduino 一直以来“简单、低成本、易上手”的初心。

Sora 2:好模型,但未必是好生意 - 肘子的 Swift 周报 #105

作者 Fatbobman
2025年10月6日 22:00

一周前,OpenAI 发布了 Sora 2 模型,并同步推出了带有社交平台属性的 Sora 应用。目前,用户仅能通过 iOS 应用使用该模型生成视频。无论在视觉细节、人物形象、环境纹理,还是声画同步方面,Sora 2 相较早期版本都有显著提升。

苹果正在为系统级支持 MCP 做准备 - 肘子的 Swift 周报 #104

作者 Fatbobman
2025年9月29日 22:00

根据 9TO5Mac 的报道,苹果正在为其生态系统添加 MCP(Model Context Protocol)支持,以实现智能体 AI 功能。其实现路径与我们在周报 #077 中的设想十分吻合:通过开发者熟悉的 App Intents 框架进行系统级集成,既保持了苹果一贯追求的“可控、安全、完整”用户体验,又巧妙规避了让普通用户直接面对复杂 MCP 配置的门槛。

零知识证明入门

作者 yukang
2025年9月27日 00:49

无论是在聊 L2、隐私还是下一代 Web 技术,零知识证明都是经常会碰到的技术术语,听起来就像是科幻小说里的东西:向你证明我知道一个秘密,但绝不透露这个秘密本身,这简直是程序员的终极浪漫。

大多数人粗看都会觉得这东西是密码学博士们的专属玩具,我花了一段时间学习后,发现这条通往魔法世界的路似乎有迹可循,希望这篇入门介绍能帮助到更多这方向的学习者。

魔法洞穴

忘掉所有数学,我们先从一个故事开始——“阿里巴巴洞穴”,这是理解 ZKP 最经典的例子,最早由 Jean-Jacques Quisquater 等人于 1990 年在他们的论文《如何向你的孩子解释零知识协议》中发表。

想象一个环形洞穴,A、B 两个入口在前方,深处有一扇只有知道咒语才能打开的魔法门。Alice 知道咒语,现在,Alice 想向 Bob 证明她知道咒语,但又不想让 Bob 听到咒语是什么。

他们可以这样玩一个游戏:

  1. 承诺 (Commitment):Alice,作为证明者 (Prover),独自进入洞穴。然后可以随机从 A 口进,也可以从 B 口进。Bob 在洞外等着,但不知道 Alice 走了哪条路。

  2. 挑战 (Challenge):Bob 作为验证者 (Verifier),走到洞口然后随机喊出一个要求,比如:“从 B 通道出来!”

  3. 响应 (Response):Alice 听到要求后:

    • 如果她当初就是从 B 进去的,那简单,就直接从 B 走出来。
    • 如果她当初是从 A 进去的,就必须念动咒语打开那扇魔法门穿过去,然后从 B 通道出来。
  4. 验证 (Verification):Bob 看到 Alice 确实从 B 通道出来了,他对 Alice 的信任度增加了一点。

为什么说“一点”呢?因为如果 Alice 不知道咒语,她仍然有 50% 的概率蒙对(比如 Alice 从 B 进去,Bob 恰好也喊了 B)。

但如果这个游戏重复 20 次,Alice 每次都能从 Bob 指定的出口出来,那 Alice 每次都蒙对的概率就只有$$\left(\frac{1}{2}\right)^{20}$$,也就是大约是百万分之一。这时候 Bob 就有十足的把握相信,Alice 确实知道那个咒语。

这个小游戏完美地展示了 ZKP 的三大特性:

  • 完整性 (Completeness):如果 Alice 真的知道咒语,她总能完成挑战。
  • 可靠性 (Soundness):如果 Alice 不知道咒语,她几乎不可能骗过 Bob。
  • 零知识性 (Zero-Knowledge):在整个过程中,Bob 除了“Alice 知道咒语”这个事实外,没有学到任何关于咒语本身的信息。

另外我们可以看到一个重要的属性是,零知识证明并非数学意义上的证明,因为可能存在一个很小很小的概率,即可靠性误差 – 作弊的证明者能够骗过验证者,但实际实践中我们几乎可以忽略这个极小的概率。

Where’s Wally

还有另外一个比较简单的例子来说明零知识证明:

Alice 和 Bob 玩游戏看谁先找到 Wally,Alice 说她找到了,她想要证明自己已经得到了结果,但又不想透露更多信息给 Bob,所以她可以用一个几倍面积黑色的纸片遮住整个图画,然后把 Wally 位置那里在黑色纸片上打个小孔,这样 Bob 就可以看到 Wally,而不知道 Wally 在哪儿。注意这里为什么强调几倍面积的黑色纸片,如果是和原图相同大小的纸片,就可能暴露了 Wally 的大致方向和范围。

这个例子展示的 ZKP 另外一个特性是 Prover 通常是更耗费资源的 (从图片中找到 Wally 需要花费一定的时间),而 Verifier 通常能很快验证。这个特性才能让一些区块链项目利用 ZKP 把链上计算挪到链下计算,而链上只是做验证。

最简单的 ZKP 代码

两个例子很棒,但代码怎么写?

我接触到的第一个协议叫 Schnorr 身份验证,它要证明的是:“我知道与公钥 h 对应的私钥 x,其中 h = g^x mod p”。这里的“咒语”就是 x,而那扇“魔法门”就是离散对数问题——从 g, h, p 反推出 x 极其困难。

这个协议的“交互式”版本,完美地复刻了洞穴里的“一来一回”:

  1. Prover (我): 随机选个数 k,计算 t = g^k mod p 发给 Verifier。这叫“承诺”。
  2. Verifier (你): 随机给 Prover 一个数 c,这叫“挑战”。
  3. Prover: 根据收到的 c,计算 r = k - c*x mod (p-1) 并发回。这叫“响应”。
  4. Verifier: 验证 g^r * h^c mod p 是不是等于 Prover 一开始给的 t

完整代码在iteractive_schnorr

fn iteractive_schnorr() {    // 公开参数:素数 p=204859, g=5, x=6 (秘密), h = 5^6 mod 204859 = 15625    let p: BigInt = BigInt::from(204859u64);    let g: BigInt = BigInt::from(5u32);    let x: BigInt = BigInt::from(6u32); // 证明者的秘密    let h = g.modpow(&x, &p); // h = g^x mod p    // 进行多轮证明 p    for _ in 0..20 {        // 证明者:生成承诺 t = g^k mod p        let mut rng = thread_rng();        let k = rng.gen_bigint_range(&BigInt::one(), &(&p - BigInt::one()));        let t = g.modpow(&k, &p);        println!("证明者发送 t: {}", t);        // 验证者:生成挑战 c (简化到 0..10)        let c: BigInt = BigInt::from(rng.gen_range(0..10));        println!("验证者挑战 c: {}", c);        // 证明者:响应 r = k - c * x mod (p-1)        let order = &p - BigInt::one(); // 阶        let r = (&k - &c * &x).modpow(&BigInt::one(), &order); // 确保正数        println!("证明者响应 r: {}", r);        // 验证者:检查 g^r * h^c == t mod p        let left = g.modpow(&r, &p) * h.modpow(&c, &p) % &p;        if left == t {            println!("验证通过!");        } else {            println!("验证失败!");        }    }}

但一来一回也太麻烦了,互联网应用需要的是一次性的“证明”。经过一番研究,密码学家们想出的一个绝妙技巧,叫做 Fiat-Shamir 启发式证明

它的核心思想是:用哈希函数来模拟一个不可预测的“挑战者”

Prover 不再等待 Verifier 给出挑战 c,而是自己计算 c = hash(公开信息, 自己的承诺 t)。因为哈希函数的雪崩效应,Prover 无法预测 c 的值来作弊,这就巧妙地把交互过程压缩了。

我们可以用 Rust 写出这样一个完整的非交互式证明程序 fiat_shamir

fn fiat_shamir() {    // --- 公开参数 ---    // 在真实世界,p 应该是至少 2048 位的安全素数    let p: BigInt = BigInt::from(204859u64);    let g: BigInt = BigInt::from(2u64);    // Prover 的秘密 (只有 Prover 知道)    let secret_x: BigInt = BigInt::from(123456u64);    // Prover 的公钥 (所有人都知道)    let public_h = g.modpow(&secret_x, &p);    println!("--- 公开参数 ---");    println!("p = {}", p);    println!("g = {}", g);    println!("h = g^x mod p = {}", public_h);    println!("-------------------");    // --- PROVER: 生成证明 ---    println!("Prover 正在生成证明...");    let mut rng = thread_rng();    let order = &p - BigInt::one();    // 1. 承诺:随机选一个 k, 计算 t = g^k mod p    let k = rng.gen_bigint_range(&BigInt::one(), &order);    let t = g.modpow(&k, &p);    // 2. 挑战 (Fiat-Shamir 的魔法在这里!):    // 把公开信息和承诺 t 一起哈希,模拟一个无法预测的挑战 c    let mut hasher = Sha256::new();    hasher.write_all(&g.to_bytes_be().1).unwrap();    hasher.write_all(&public_h.to_bytes_be().1).unwrap();    hasher.write_all(&t.to_bytes_be().1).unwrap();    let hash_bytes = hasher.finalize();    let c = BigInt::from_bytes_be(num_bigint::Sign::Plus, &hash_bytes) % &order;    // 3. 响应:计算 r = k - c*x (mod order)    let cx = (&c * &secret_x) % &order;    let mut r = (&k - cx) % &order;    if r < BigInt::zero() {        r += &order;    }    println!("证明已生成:(r = {}, c = {})", r, c);    println!("-------------------");    // --- VERIFIER: 验证证明 ---    println!("Verifier 正在验证证明...");    // Verifier 为了验证,需要自己重新计算 t' = g^r * h^c mod p    let gr = g.modpow(&r, &p);    let hc = public_h.modpow(&c, &p);    let t_prime = (&gr * &hc) % &p;    // Verifier 再用算出来的 t' 计算 c' = H(g || h || t')    let mut hasher = Sha256::new();    hasher.write_all(&g.to_bytes_be().1).unwrap();    hasher.write_all(&public_h.to_bytes_be().1).unwrap();    hasher.write_all(&t_prime.to_bytes_be().1).unwrap();    let hash_bytes = hasher.finalize();    let c_prime = BigInt::from_bytes_be(num_bigint::Sign::Plus, &hash_bytes) % &order;    if c == c_prime {        println!("✅ 验证通过!");    } else {        println!("❌ 验证失败!");    }}

以上我们通过最简单的代码来演示了 ZKP 的基本思想,从数学原理上都是基于离散对数困难性。

发散到 Passkeys

当我看到 Hash 的时候,我联想到了后台服务的密码存储,比如我们在做一个用户注册和登录功能的时候,为了安全我们是不会去存储用户的原始密码(秘密),而是会使用密码哈希方案,去存储 hash(password + salt)

但这个密码哈希方案其实也泄露了“知识”,当你登录时会把 123456 发送给服务器,服务器计算 hash("123456" + salt) 并与数据库中的值对比。

  • 在传输过程中:密码是明文的(当然可以用 TLS/SSL 加密,但服务器在解密后会看到明文)。
  • 对服务器而言:服务器在验证那一瞬间是知道你的密码的。
  • 如果数据库被盗:攻击者拿到了 hash(password + salt) 的列表。这个哈希值本身就是一条重要的知识!它虽然不是密码原文,但它是密码的一个确定性指纹。攻击者可以进行:
    • 字典攻击:尝试常用密码,计算哈希值来一一比对。
    • 彩虹表攻击:用一个预先计算好的哈希值数据库来反查。
    • 暴力破解:对所有可能的组合进行哈希计算。

这就是为什么我们需要“加盐(salt)”和使用慢哈希函数(如 Argon2, bcrypt),目的就是为了增加攻击者进行上述离线攻击的成本,但无论如何,哈希值本身就是泄露的“知识”

所以如果我们要更安全,一点“知识”都不泄露,似乎 ZKP 适合做认证服务?注册时不存密码哈希,只存公钥 h。登录时,我发送一个 ZKP 证明,服务器验证一下就行了,数据库被拖库了都没事。

甚至更简单点其实就用公私钥对不是更方便和安全么,Nostr 就是这么做的 (钱包也是这个原理),private key 是密码,每次发内容就用私钥签名内容,然后把 pubkey 带上,这样任何收到这条消息的节点都可以验证签名是否一致,这样就本质上通过各个 relay 节点形成一个去中心化的社交网络。

我按照这个思路去找 Web 相关的解决方案,业界给出的答案是 Passkeys (基于 WebAuthn 标准),使用非对称加密来替代密码(私钥不出设备),Passkeys 是这样工作的:

  1. 注册: 你的设备(如 iPhone 或 Android 手机)在本地生成一对密钥(私钥和公钥)。私钥安全地存储在设备的硬件安全芯片中,永远不会离开设备。你把公钥发送给网站服务器。
  2. 登录:
    • 网站向你的设备发送一个“挑战”(一个随机数)。
    • 你的设备用私钥对这个“挑战”进行签名,然后把签名发回给网站。
    • 网站用它存储的你的公钥来验证这个签名是否有效。
  3. 用户体验: 整个过程对于用户来说,可能只是做一次指纹识别或面部识别来授权设备使用私钥。

2019 年 3 月 4 日 WebAuthn Level 1 已经被 W3C 正式发布为“推荐标准 (Recommendation)”,标志着它成为了一个成熟、稳定、官方推荐的 Web 标准。

通过“电路”证明程序的运行

从上面的例子我们看到,ZKP 很适合用来证明 Prover 知道某个秘密,比如一个数 x ,但 ZKP 的用途远不止于此,还可以证明任何计算过程的正确性。

为什么证明一个程序正确运行很重要,因为像以太坊这样的公链,如果所有的节点都运行同样的合约 (本质上就是一段程序代码) 多次,这无疑是很大的浪费,我们想通过 ZKP 把计算挪到链下,这样公链上的节点只需要验证程序被正确执行就可。

“我正确运行了一个复杂的程序,得到了这个输出。”—— 这要怎么证明?

答案是四个字:万物皆可电路 (Arithmetization)

ZKP 系统(比如我们后面会聊的 zk-SNARKs)的“世界观”非常单纯,甚至有点笨拙,它看不懂我们人类写的高级代码,比如 if/else 语句、for 循环。

如果我们想让 ZKP 为我们工作,就必须先把我们要证明的东西,翻译成它唯一能听懂的语言。这个翻译过程,就是“算术化 (Arithmetization)”。而“电路”或“约束系统”,就是我们翻译出来的最终稿。这个重写的过程,就是“拍扁 (Flattening)”。你把一个有层次、有复杂逻辑的程序,变成了一个长长的、线性的、只包含最基本算术运算的指令列表。

任何程序,无论多复杂,都可以被“拍扁”成一系列最基础的加法和乘法约束。比如 out = x*x*x + x + 5 这段代码,可以被分解为:

  • v1 = x * x
  • v2 = v1 * x
  • v3 = v2 + x
  • out = v3 + 5

于是,证明“我正确运行了程序”,就转化为了证明“我知道一组数 (x, v1, v2, v3, out) 能同时满足上面这一堆等式”。这个过程,就是把代码逻辑“算术化”,变成了 ZKP 系统可以处理的语言。

那我们来看 Verifier 如何验证上面的计算过程,最原始的当然是根据输入,来一条一条的执行上面被拍平后的指令集,但这样的工作量和自己去执行整个程序就差不多了。

为了避免这种蛮力验证,密码学家们引入了一个极其强大的数学工具:多项式 (Polynomials)
整个魔法流程如下:

  1. Prover 的艰巨任务:将所有约束“编织”进一个多项式 Prover 会执行一个惊人的转换:他会找到一种方法 (Groth16、PLONK、STARKs 等),将我们前面提到的那一整个约束系统 (x * x - v1 = 0, v1 * x - v2 = 0, …) 全部编码成一个单一的、巨大的多项式方程

    我们可以把这个巨大的“主多项式”记为 P(z)。这个 P(z) 有一个神奇的特性:

    当且仅当 Prover 提供的所有见证值 (x, v1, v2…) 都完全正确、满足所有原始约束时,这个主多项式 P(z) 在某些特定的点上才会等于 0。

    如果 Prover 在任何地方作弊,哪怕只修改了一个微不足道的值,最终生成的那个 P(z) 就会是一个完全不同的多项式。

  2. 验证者的捷径 – 随机点检查 (Random Spot-Check) :现在验证者的问题从“检查成千上万个小等式”变成了“如何验证 Prover 的那个巨大多项式 P(z) 是正确的?”

    难道要把整个巨大的多项式传输过来再计算一遍吗?当然不是!这里用到了密码学中一个非常深刻的原理,通常与 Schwartz-Zippel 引理 有关。

    它的直观思想是

    如果我有两个不同的、阶数很高的多项式 P(z)F(z)(F 代表伪造的),然后我从一个极大的数域里随机挑选一个点 s,那么 P(s)F(s) 的计算结果相等的概率几乎为零

    这就给了验证者一个巨大的捷径:

    • Verifier 不需要关心那个巨大的多项式长什么样。
    • 它只需要在一个秘密的、随机选择的点 s 上,对 Prover 的多项式进行一次“抽查”。
    • 它通过密码学协议向 Prover 发起一个挑战:“嘿,你声称你有一个正确的多项式,那你告诉我,在 s 这个点上,你的多项式计算出来的值是多少?”

所以这里的 ZKP 证明里到底包含什么?

在一个典型的 zk-SNARK(比如 Groth16)中,那个小小的证明通常是由几个椭圆曲线上的点 (points on an elliptic curve) 组成的。可以把这些“点”想象成一种具备神奇数学特性的高级指纹。这些点就是 Prover 对他构造的那些巨大多项式(比如 A(x), B(x), C(x),它们共同构成了我们之前说的那个主多项式 P(x)) 的“承诺”。

这里的魔法在于 Verifier 不需要通过这些“点”来反推出原始的多项式。相反,他可以直接在这些“点”上进行一种特殊运算,这种运算的结果等价于在原始多项式上进行“随机点检查”。这个特殊的运算,就是 zk-SNARKs 的核心引擎之一:配对 (Pairings)并非所有 ZK 架构都用配对;Groth16/部分 KZG-based 系统用配对,STARKs 则用哈希/FRI 等替代方案。

让我们把整个流程串起来 (zk-SNARK),看看 Prover 的多项式是如何被“隔空”验证的:

  1. 准备阶段 (Setup)

    • 协议约定好了一套公共参数(包含一个“验证密钥”)。这个验证密钥里编码了“游戏规则”,包括对程序正确性的期望。
  2. Prover 的工作

    • 他有他的秘密“见证 (Witness)”。
    • 他按照约定,将程序的约束系统转化成几个巨大的多项式 A(x), B(x), C(x)。(这些多项式满足 A(x) * B(x) - C(x) = H(x) * Z(x) 的关系,这是 R1CS 算术化的结果)。
    • 关键一步:他并没有把这些多项式发出去。而是用他的“证明密钥”,计算出这几个多项式在某个秘密点 s 上的椭圆曲线点表示。这些点就是对多项式的“承诺”。
    • 最终生成的证明 (Proof),就是由这几个计算出来的椭圆曲线点组成的,它非常小。
  3. Verifier 的工作

    • Verifier 收到这个由几个点组成的、小小的证明。
    • Verifier 完全看不到 Prover 的任何多项式 (A(x), B(x), C(x))。
    • Verifier 拿出“验证密钥”,并将 Prover 提交的这几个“承诺点”代入一个预设的配对验证方程 (Pairing Verification Equation)

      这个方程被设计得极其巧妙,它的等号左边和右边分别对应着 Prover 原始多项式关系 A*B-C=H*Z 的加密形式。

      当且仅当 Prover 原始的、未知的那些多项式确实满足正确的数学关系时,这个配对验证方程的等号才能成立。

所以:

  • 证明里是什么? 是对 Prover 秘密多项式的密码学承诺(通常是几个椭圆曲线点)。
  • Verifier 如何知道多项式?不需要知道。他只需要知道验证规则(即那个配对验证方程)。
  • 如何验证? 他把 Prover 的“承诺”(证明)代入“规则”(验证方程)。如果方程成立,他就知道那些他看不见的、被承诺了的多项式一定是正确的,进而推断出 Prover 的原始计算是正确的。

Prover 把“我知道所有题的答案”这个事实,通过复杂的计算,浓缩成了一个包含几个关键“密码学指纹”的信封(证明)。Verifier 不用拆开信封看所有答案,他只需要用一种特殊的“X 光机”(配对验证)照一下这个信封,就能瞬间知道里面的答案是不是都对。

ZKP 相关的应用

区块链因为其去中心化和对隐私性的严苛要求,ZKP 非常适合用在这个领域。

扩容 (ZK-Rollups): 让以太坊快如闪电

以太坊慢又贵,因为每个节点都要重复执行每笔交易。ZK-Rollup 的思路就像是找了个超级课代表:

  1. 在链下 (L2) 执行成千上万笔交易。
  2. 为“我已正确处理了这一切”这个声明,生成一个微小的 ZK 证明。
  3. 把这个证明提交到链上 (L1)。

L1 的所有节点不再需要重复计算那几千笔交易,它们只需要做一件极其廉价的事:验证那个 ZK 证明。就像老师检查作业,不再需要自己从头算一遍,只需要看一眼课代表盖的“全对”印章。

总而言之,Rollup 的核心创新在于将计算执行数据结算分离。它利用 ZKP 等密码学技术,将繁重的“执行”环节放在链下,然后只把一个轻量的“证明”和必要数据放在链上进行“结算”,从而实现了对以太坊主网的大规模扩容。

隐私 (Tornado Cash): 你的钱,只有你知道

Tornado Cash 是个混币器,你存入 100 ETH,然后从一个全新的地址取出来,没人能把这两者联系起来。它的机制是:

  • 存款:你在本地生成一个秘密凭证(包含SecretNullifier),然后计算出它的哈希值——“承诺 (Commitment)”,把承诺和钱一起存入合约。

  • 取款:你用一个全新的地址,提交一个 ZK 证明,这个证明:“我知道某个树叶的 Secret 且未被花费”,同时提交 nullifier(通常是对秘密做散列得到的唯一标识)以标记已花费。这样合约无需关联存款者身份即可阻止双花。

整个过程,合约就像个盲人会计,它不知道是“谁”存的,也不知道取款对应的是“哪一一笔”存款,它只负责验证 ZKP 规则是否被遵守。

ZKP 在 AI 的应用

ZKP 应用在大模型也是最前沿、激动人心的领域。例如 AI 模型(尤其是大型语言模型)的权重是极其宝贵的商业机密。用户的数据又极其隐私。如何让一个 AI 模型在不暴露其内部权重的情况下,处理用户的隐私数据,并向用户证明它确实是用了那个宣称的高级模型,而不是一个廉价的“冒牌货”?

ZKP 解决方案 (ZKML - Zero-Knowledge Machine Learning):
模型推理证明:模型提供方可以对一次推理过程生成 ZK 证明,证实“我使用我宣称的那个模型(其哈希值是公开的),处理了你的输入数据,得出了这个输出结果”。这向用户保证了模型的真实性,同时保护了模型的知识产权。

数据隐私证明:用户可以对自己的数据生成 ZK 证明,证实“我的数据(例如医疗记录)符合某个特定标准(例如,有某种疾病特征)”,然后将这个证明提交给 AI 模型进行统计或研究,而无需上传原始的隐私数据。

这里有更多相关的资料:An introduction to zero-knowledge machine learning (ZKML)

零知识证明和硬件

前面我们谈到,在 ZKP 中Prover(证明者)端计算量最大,主要集中在以下几个方面:

  • 多项式承诺方案:这是现代零知识证明(如 zk-SNARKs、zk-STARKs)的核心。证明者需要将计算任务转化为多项式,并对这些多项式进行一系列复杂的加密运算,例如多项式插值、求导、卷积、快速傅里叶变换(FFT) 等。这些运算的复杂度很高,尤其是当要证明的计算规模很大时。
  • 同态加密运算或椭圆曲线点运算:在一些零知识证明协议中,为了生成和验证证明,需要进行大量的椭圆曲线点乘运算。这种运算在数学上非常耗时,尤其是当需要处理大量的点时。
  • 哈希函数计算:为了将复杂的数据结构或计算结果进行压缩和承诺,证明者会使用到大量的加密哈希函数,例如 SHA-256、Poseidon 等。

而在Verifier(验证者)端计算量相对较小,这也是零知识证明的重要优势之一,但它仍然需要进行一些关键的计算,比如:

  • 椭圆曲线配对运算(Pairing):在 zk-SNARKs 等协议中,验证者需要进行椭圆曲线配对运算来验证证明。这是一种特殊的加密操作,虽然比证明者的计算量小得多,但仍然需要一定的计算资源。
  • 哈希函数和多项式求值:验证者也需要进行一些哈希计算和多项式求值来检查证明的有效性。

总的来说,零知识证明的计算量主要耗费在Prover端,因为它需要对整个计算过程进行完整的加密转换和证明生成,而这些步骤依赖于高复杂度的多项式和椭圆曲线运算。所以我们看到一些专门为此服务的硬件 FPGA、ASIC、GPU。

而 RISC-V 因为其可扩展性模块化设计、开源的标准等优势,是实现零知识证明硬件加速的重要“基石”之一,risc0 是个值得关注的项目

更多参考

RISC-V from Scratch: Building a Virtual Machine

作者 yukang
2025年9月23日 08:54

I’ve always wanted to learn RISC-V. A few days ago, I finally got my hands dirty with it.

This post will guide you through the process of building a simple RISC-V VM from the ground up, using Rust as our implementation language.

Understanding the Core Concepts

Before writing any code, I need to grasp the fundamentals of RISC-V.

  • RISC vs. CISC: RISC (Reduced Instruction Set Computing) architectures use a small, highly optimized set of instructions. This is in contrast to CISC (Complex Instruction Set Computing), which has a large number of complex instructions. RISC-V’s simplicity makes it ideal for building a VM.
  • Modular Architecture: RISC-V has a base instruction set (RV32I for 32-bit systems) and optional extensions like M (for multiplication) or F (for floating-point). We’ll focus on the RV32I base to keep things simple.
  • The Three Pillars: At its core, a CPU (and thus our VM) consists of three main components:
    • Registers: A small set of high-speed memory locations used for calculations. RISC-V has 32 general-purpose registers (x0-x31).
    • Memory: A much larger space for storing program code and data.
    • Program Counter (PC): A special register that holds the memory address of the next instruction to be executed.

We can get all the details of RISC-V instructions from RISC-V Technical Specifications.

The VM’s Core Logic

Our VM is essentially a program that emulates a real CPU’s behavior. The core of our VM is the instruction loop, which follows a simple fetch-decode-execute cycle.

  1. Fetch: Read the 32-bit instruction from the memory address pointed to by the PC.
  2. Decode: Parse the instruction’s binary code to determine its type and what operation to perform.
  3. Execute: Perform the operation (e.g., an addition) and update the relevant registers or memory.

Here’s a simplified Rust code snippet to illustrate the VM structure and the run loop:

pub struct VM {    x_registers: [u32; 32],    pc: u32,    memory: Vec<u8>,}impl VM {    pub fn run(&mut self) {        loop {            // 1. Fetch the instruction            let instruction = self.fetch_instruction();            // 2. Decode            let decoded_instruction = self.decode(instruction);            // 3. Execute            self.execute_instruction(decoded_instruction);            // 4. Increment the PC            self.pc += 4;        }    }}

The fetch instruction turns out to be very simple, we just load 4 bytes in little-endian format into a u32 integer:

/// Fetch 32-bit instruction from memory at current PCfn fetch_instruction(&self) -> Option<u32> {    let pc = self.pc as usize;    if pc + 4 > self.memory.len() {        return None;    }    // RISC-V uses little-endian byte order    let instruction = u32::from_le_bytes([        self.memory[pc],        self.memory[pc + 1],        self.memory[pc + 2],        self.memory[pc + 3],    ]);    Some(instruction)}

Then we need to decode the integer into a RISC-V instruction. Here’s how we decode IType and RType instructions. The specifications for these two types are:

/// Decode 32-bit instruction into structured formatfn decode(&self, code: u32) -> Option<Instruction> {    let opcode = code & 0x7f;    match opcode {        0x13 => {            // I-type instruction (ADDI, etc.)            let rd = ((code >> 7) & 0x1f) as usize;            let rs1 = ((code >> 15) & 0x1f) as usize;            let funct3 = (code >> 12) & 0x7;            let imm = (code as i32) >> 20; // Sign-extended            Some(Instruction::IType {                rd,                rs1,                imm,                funct3,            })        }        0x33 => {            // R-type instruction (ADD, SUB, etc.)            let rd = ((code >> 7) & 0x1f) as usize;            let rs1 = ((code >> 15) & 0x1f) as usize;            let rs2 = ((code >> 20) & 0x1f) as usize;            let funct3 = (code >> 12) & 0x7;            let funct7 = (code >> 25) & 0x7f;            Some(Instruction::RType {                rd,                rs1,                rs2,                funct3,                funct7,            })        }        _ => None, // Unsupported opcode    }}

Then we want to execute the instruction, just following the specification. For demonstration purposes, we return the execution debug string as a result:

/// Execute decoded instructionfn execute(&mut self, instruction_type: Instruction) -> Result<String, String> {    match instruction_type {        Instruction::IType {            rd,            rs1,            imm,            funct3,        } => {            match funct3 {                0x0 => {                    // ADDI - Add immediate                    self.write_register(rd, self.x_registers[rs1] + imm as u32);                    Ok(format!(                        "ADDI x{}, x{}, {} -> x{} = {}",                        rd, rs1, imm, rd, self.x_registers[rd]                    ))                }                _ => Err(format!("Unsupported I-type funct3: {:#x}", funct3)),            }        }        Instruction::RType {            rd,            rs1,            rs2,            funct3,            funct7,        } => {            match (funct3, funct7) {                (0x0, 0x00) => {                    // ADD - Add registers                    let result = self.x_registers[rs1] + self.x_registers[rs2];                    self.write_register(rd, result);                    Ok(format!(                        "ADD x{}, x{}, x{} -> x{} = {}",                        rd, rs1, rs2, rd, self.x_registers[rd]                    ))                }                (0x0, 0x20) => {                    // SUB - Subtract registers                    let result = self.x_registers[rs1] - self.x_registers[rs2];                    self.write_register(rd, result);                    Ok(format!(                        "SUB x{}, x{}, x{} -> x{} = {}",                        rd, rs1, rs2, rd, self.x_registers[rd]                    ))                }                _ => Err(format!(                    "Unsupported R-type instruction: funct3={:#x}, funct7={:#x}",                    funct3, funct7                )),            }        }    }}

The simplest VM code is available at: riscv-vm-v0

From Rust to RISC-V binary

Now we need to write more complex assembly code for testing our VM, but we don’t want to write assembly code by hand.

To test our VM, we will write Rust code then use cross-compile toolchains to compile it into RISC-V executable files.

  1. Prepare the Environment: Install the riscv32imac-unknown-none-elf target toolchain. This is a bare-metal target, meaning it doesn’t rely on any operating system.
rustup target add riscv32imac-unknown-none-elf

Next, you’ll need a RISC-V linker. You can get this from the official RISC-V GNU toolchain.

# On Linux or macOSsudo apt-get install gcc-riscv64-unknown-elf# Alternatively, on macOSbrew install riscv-gnu-toolchain

Note: The gcc-riscv64-unknown-elf package includes both 32-bit and 64-bit tools.

  1. Write “Bare-Metal” Rust: Our Rust program must be written for a “bare-metal” environment, meaning you cannot use the standard library and must provide your own entry point and panic handler.
#[unsafe(no_mangle)]pub extern "C" fn _start() {    let mut sum = 0;    for i in 1..=10 {        sum += i;    }    // Store the result (which should be 55) in a known memory location.    let result_ptr = 0x1000 as *mut u32;    unsafe {        *result_ptr = sum;    }}#[panic_handler]fn panic(_info: &PanicInfo) -> ! {    loop {}}
  1. Cross-Compile: Use cargo with the specific target and a linker script to build the executable. We need to add options for Cargo in .cargo/config.toml
[target.riscv32imac-unknown-none-elf]rustflags = ["-C", "link-arg=-Tlink.ld"]

The content for link.ld is as follows. It tells the linker the layout of the binary file generated. Notice that we specify the entry point at address 0x80:

OUTPUT_ARCH(riscv)ENTRY(_start)SECTIONS {    . = 0x80;    .text : {        *(.text.boot)        *(.text)    }    .rodata : {        *(.rodata)    }    .data : {        *(.data)    }    .bss : {        *(.bss)    }}

Then we can build the program to a binary:

cargo build --release --target riscv32imac-unknown-none-elf
  1. Disassemble and check the binary code: We can use the tool riscv64-unknown-elf-objdump to double-check the generated binary file:
riscv64-unknown-elf-objdump -d ./demo/target/riscv32imac-unknown-none-elf/release/demo./demo/target/riscv32imac-unknown-none-elf/release/demo:     file format elf32-littleriscvDisassembly of section .text._start:00000080 <_start>:  80:   4501                    li      a0,0  82:   4605                    li      a2,1  84:   45ad                    li      a1,11  86:   4729                    li      a4,10  88:   00e61763                bne     a2,a4,96 <_start+0x16>  8c:   46a9                    li      a3,10  8e:   9532                    add     a0,a0,a2  90:   00e61863                bne     a2,a4,a0 <_start+0x20>  94:   a809                    j       a6 <_start+0x26>  96:   00160693                addi    a3,a2,1  9a:   9532                    add     a0,a0,a2  9c:   00e60563                beq     a2,a4,a6 <_start+0x26>  a0:   8636                    mv      a2,a3  a2:   feb6e3e3                bltu    a3,a1,88 <_start+0x8>  a6:   6585                    lui     a1,0x1  a8:   c188                    sw      a0,0(a1)  aa:   8082                    ret

The complete cross-compile Rust code is available at: riscv-demo

Using the VM to Execute Binary

The first problem is how do we parse the executable file? It turns out there is a crate called elf that can help us parse the header of an ELF file. We extract the interested parts from the header and record the base_mem so that we can convert virtual address to physical address. Of course, we also load the code into memory:

pub fn new_from_elf(elf_data: &[u8]) -> Self {    let mut memory = vec![0u8; MEM_SIZE];    let elf = ElfBytes::<elf::endian::AnyEndian>::minimal_parse(elf_data)        .expect("Failed to parse ELF file");    // Get the program entry point    let entry_point = elf.ehdr.e_entry as u32;    // Iterate through program headers, load PT_LOAD type segments    for segment in elf.segments().expect("Failed to get segments") {        if segment.p_type == PT_LOAD {            let virt_addr = segment.p_vaddr as usize;            let file_size = segment.p_filesz as usize;            let mem_size = segment.p_memsz as usize;            let file_offset = segment.p_offset as usize;            // Address translation: virtual address -> physical address            let phys_addr = virt_addr - entry_point as usize;            // Check memory boundaries            if phys_addr + mem_size > MEM_SIZE {                panic!(                    "Segment is too large for the allocated memory. vaddr: {:#x}, mem_size: {:#x}",                    virt_addr, mem_size                );            }            // Copy data from ELF file to memory            if file_size > 0 {                let segment_data = &elf_data[file_offset..file_offset + file_size];                memory[phys_addr..phys_addr + file_size].copy_from_slice(segment_data);            }        }    }    let mut vm = VM {        x_registers: [0; 32],        // Set directly to entry_point to match the linker script        pc: entry_point,        memory,        mem_base: entry_point,    };    vm.x_registers[0] = 0;    vm}

What’s left is that we need to extend our VM to support all the instruction formats used in this binary file, including li, bne, beq, etc.

There are 16-bit compressed instructions, so we can’t always increment the PC by 4; sometimes we only need to increment it by 2 for shorter ones.

Another interesting thing is that some of them are conditional jump instructions, so we need to get the return new_pc from the execution of the instruction.

So now we need to update the core logic of fetch and execution of instructions:

// Check the lowest 2 bits to determine instruction lengthif first_half & 0x3 != 0x3 {    // 16-bit compressed instruction    pc_increment = 2;    new_pc = self.execute_compressed_instruction(first_half);} else {    // 32-bit instruction    pc_increment = 4;    if physical_pc.saturating_add(3) >= self.memory.len() {        break;    }    let second_half = u16::from_le_bytes([        self.memory[physical_pc + 2],        self.memory[physical_pc + 3],    ]);    let instruction = (second_half as u32) << 16 | (first_half as u32);    if instruction == 0 {        break;    }    new_pc = self.execute_instruction(instruction);}

The complete new VM which can run compiled RISC-V binary files is available at: riscv-vm

References

❌
❌