<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>🐱 寒冰是喵喵的 blog</title><description>欢迎来到我的小世界~</description><link>https://blog.hanbings.io/</link><item><title>音程与和弦</title><link>https://blog.hanbings.io/posts/intervals-and-chords/</link><guid isPermaLink="true">https://blog.hanbings.io/posts/intervals-and-chords/</guid><description>音乐科学呢~</description><pubDate>Tue, 21 Jul 2026 00:00:00 GMT</pubDate><content:encoded/></item><item><title>Astro、MDX 与 Shader</title><link>https://blog.hanbings.io/posts/custom-mdx-components-shader-layout/</link><guid isPermaLink="true">https://blog.hanbings.io/posts/custom-mdx-components-shader-layout/</guid><description>在博客内的 mdx 中支持 Shader 组件，并将 Astro 的 layout 分层，允许 Shader 覆盖全站</description><pubDate>Mon, 20 Jul 2026 03:45:12 GMT</pubDate><content:encoded>&lt;p&gt;下面的 &lt;code&gt;ShaderBox&lt;/code&gt; 将 GLSL 保存在 MDX 文档内，在源码下方提供内联预览，并通过 &lt;code&gt;overlay&lt;/code&gt; 开关提供“应用到页面”按钮。&lt;/p&gt;
&lt;div&gt;&lt;div&gt;&lt;strong&gt;全局 Shader&lt;/strong&gt;&lt;span&gt;可应用到整个页面&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;#version &lt;/span&gt;&lt;span&gt;300&lt;/span&gt;&lt;span&gt; es&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;precision highp &lt;/span&gt;&lt;span&gt;float&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;uniform&lt;/span&gt;&lt;span&gt; vec2&lt;/span&gt;&lt;span&gt; u_resolution&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;uniform&lt;/span&gt;&lt;span&gt; float&lt;/span&gt;&lt;span&gt; u_time&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt; vec4&lt;/span&gt;&lt;span&gt; fragment_color&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; main&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    vec2 uv &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; gl_FragCoord&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;xy&lt;/span&gt;&lt;span&gt; /&lt;/span&gt;&lt;span&gt; u_resolution&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    vec3 color &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 0.5&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; 0.5&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt; cos&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;u_time &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; uv&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;xyx&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt; 6.28318&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; vec3&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0.0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; 2.0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; 4.0&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    fragment_color &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; vec4&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;color&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; 0.3&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;</content:encoded></item><item><title>Markdown 边注、术语与钢琴窗测试</title><link>https://blog.hanbings.io/posts/markdown-annotations/</link><guid isPermaLink="true">https://blog.hanbings.io/posts/markdown-annotations/</guid><description>MDX 富文本组件的使用说明与渲染测试，包括手绘标注、文章边注、术语原文和交互式钢琴窗。</description><pubDate>Sat, 18 Jul 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇文章既是本站 Markdown 扩展的使用说明，也是文章边注、术语原文与钢琴窗的渲染测试，当前由 &lt;strong&gt;MDX&lt;/strong&gt; 渲染。&lt;/p&gt;
&lt;h2&gt;MDX 组件&lt;/h2&gt;
&lt;p&gt;本站将复杂的富文本能力注册为全局 MDX 组件。文章可以直接使用手绘标注、术语、边注和钢琴窗组件，无需在每篇文章中重复导入。普通 Markdown 继续使用 Astro 默认渲染器，适合不需要组件的内容。&lt;/p&gt;
&lt;h2&gt;MDX 手绘标注&lt;/h2&gt;
&lt;p&gt;Rough Notation 的七种效果分别封装为 &lt;code&gt;&amp;lt;Underline&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;Box&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;Circle&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;Highlight&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;StrikeThrough&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;CrossedOff&amp;gt;&lt;/code&gt; 和 &lt;code&gt;&amp;lt;Bracket&amp;gt;&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;Underline&amp;gt;下划线&amp;lt;/Underline&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;Box&amp;gt;方框&amp;lt;/Box&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;Circle&amp;gt;圆圈&amp;lt;/Circle&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;Highlight&amp;gt;高亮&amp;lt;/Highlight&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;StrikeThrough&amp;gt;删除线&amp;lt;/StrikeThrough&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;CrossedOff&amp;gt;交叉划除&amp;lt;/CrossedOff&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;这句话中，&amp;lt;Bracket brackets={&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;left&apos;, &apos;right&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;}&amp;gt;只有这一小部分&amp;lt;/Bracket&amp;gt; 会被标注。&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;段落前文 &amp;lt;Bracket class=&quot;inline-block w-52 align-middle&quot; brackets={&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;left&apos;, &apos;right&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;}&amp;gt;这段较长的内容会自然换行，并由同一组括号包围&amp;lt;/Bracket&amp;gt; 段落后文。&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;Bracket class=&quot;block&quot; brackets={&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;&apos;left&apos;, &apos;right&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;}&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  这是一整个被括号包裹的段落。它可以自然换行，并始终只绘制一组左右括号。&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/Bracket&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&lt;div&gt;这是手绘下划线&lt;/div&gt;&lt;div&gt;这是方框&lt;/div&gt;&lt;div&gt;这是圆圈&lt;/div&gt;&lt;div&gt;这是高亮&lt;/div&gt;&lt;div&gt;这是删除线&lt;/div&gt;&lt;div&gt;这是交叉划除&lt;/div&gt;&lt;div&gt;这是一整句话，其中 只有这一小部分 会被标注。&lt;/div&gt;&lt;div&gt;段落前文 这段较长的内容会自然换行，并由同一组括号包围 段落后文。&lt;/div&gt;&lt;div&gt;这是一整个被括号包裹的段落。它使用可用的正文宽度自然换行，并始终只绘制一组左右括号。&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;所有组件都支持 &lt;code&gt;color&lt;/code&gt;、&lt;code&gt;strokeWidth&lt;/code&gt;、&lt;code&gt;iterations&lt;/code&gt;、&lt;code&gt;padding&lt;/code&gt;、&lt;code&gt;animationDuration&lt;/code&gt;、&lt;code&gt;animate&lt;/code&gt;、&lt;code&gt;multiline&lt;/code&gt;、&lt;code&gt;rtl&lt;/code&gt; 和 &lt;code&gt;class&lt;/code&gt;。&lt;code&gt;&amp;lt;Bracket&amp;gt;&lt;/code&gt; 额外支持 &lt;code&gt;brackets&lt;/code&gt; 和 &lt;code&gt;gap&lt;/code&gt;：&lt;code&gt;brackets&lt;/code&gt; 可选择 &lt;code&gt;left&lt;/code&gt;、&lt;code&gt;right&lt;/code&gt;、&lt;code&gt;top&lt;/code&gt; 和 &lt;code&gt;bottom&lt;/code&gt;；&lt;code&gt;gap&lt;/code&gt; 默认是 &lt;code&gt;0.75em&lt;/code&gt;，用于为句内括号预留左右空间，可传入像素数字、任意 CSS 长度，或用 &lt;code&gt;gap={0}&lt;/code&gt; 关闭。它默认将多行内容视为一个整体，避免为每一行重复绘制括号。无 JavaScript 时会保留对应的 CSS 降级效果；减少动态效果模式下会自动关闭绘制动画。&lt;/p&gt;
&lt;h2&gt;术语原文&lt;/h2&gt;
&lt;p&gt;MDX 页面可以使用全局注册的 &lt;code&gt;&amp;lt;Term&amp;gt;&lt;/code&gt; 组件，通过 &lt;code&gt;original&lt;/code&gt; 保留术语原文，并用 &lt;code&gt;lang&lt;/code&gt; 标记原文语言：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;Term original=&quot;virtual memory&quot; lang=&quot;en&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  虚拟内存&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/Term&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;Term original=&quot;上下文切换&quot; lang=&quot;zh-CN&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  context switch&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/Term&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，操作系统中的 &lt;ruby title=&quot;原文：virtual memory&quot;&gt;虚拟内存&lt;rt lang=&quot;en&quot;&gt;virtual memory&lt;/rt&gt;&lt;/ruby&gt; 并不等同于物理内存；Rust 的 &lt;ruby title=&quot;原文：borrow checker&quot;&gt;借用检查器&lt;rt lang=&quot;en&quot;&gt;borrow checker&lt;/rt&gt;&lt;/ruby&gt; 则会在编译期检查引用是否有效。&lt;/p&gt;
&lt;p&gt;标注也可以反过来使用。例如在英文段落中，&lt;ruby title=&quot;原文：上下文切换&quot;&gt;context switch&lt;rt lang=&quot;zh-CN&quot;&gt;上下文切换&lt;/rt&gt;&lt;/ruby&gt; 上方显示的是中文原词。&lt;code&gt;lang&lt;/code&gt; 可以省略，但在中英文混排时建议保留。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;Term&amp;gt;&lt;/code&gt; 生成标准 HTML &lt;code&gt;&amp;lt;ruby&amp;gt;&lt;/code&gt; 和 &lt;code&gt;&amp;lt;rt&amp;gt;&lt;/code&gt; 元素。&lt;code&gt;&amp;lt;rt&amp;gt;&lt;/code&gt; 可用于发音、翻译、音译或短注释。若内容需要离开本站的 Markdown pipeline，也可以直接使用标准 HTML，前提是目标渲染器允许 Markdown 中的 raw HTML：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;ruby&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;虚拟内存&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;rt&lt;/span&gt;&lt;span&gt; lang&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;en&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;virtual memory&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;rt&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;ruby&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;文章附记&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;Sidenote&amp;gt;&lt;/code&gt; 的内容就是卡片正文，紧跟其后的段落用 &lt;code&gt;&amp;lt;SidenoteText&amp;gt;&lt;/code&gt; 标记对应文字。不再需要额外的卡片组、ID 或配对名称：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;Sidenote&amp;gt;这里是附记，也支持普通的 &lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;Markdown&lt;/span&gt;&lt;span&gt;**&lt;/span&gt;&lt;span&gt;。&amp;lt;/Sidenote&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;SidenoteText&amp;gt;这里是与附记相关的正文。&amp;lt;/SidenoteText&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;aside role=&quot;note&quot;&gt;&lt;span&gt;附记&lt;/span&gt;&lt;p&gt;这是一条附记。宽屏浏览时，它位于文章右侧；可用空间不足时，它会回到正文中，因而不会挤压主要内容。&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;&lt;span&gt;这段正文用来观察附记的位置和颜色关系。鼠标停在便签或这段文字上时，文字背景会显示相同颜色。边注适合补充背景、出处或不影响主线的细节；如果内容是理解文章所必需的，就不应该藏在边注里。&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;自定义标题与 Markdown&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;title&lt;/code&gt; 可以修改卡片标题；组件内容支持强调、代码、链接和多个段落：&lt;/p&gt;
&lt;aside role=&quot;note&quot;&gt;&lt;span&gt;背景&lt;/span&gt;&lt;p&gt;附记可以包含 &lt;strong&gt;强调&lt;/strong&gt;、&lt;code&gt;inline code&lt;/code&gt; 和&lt;a href=&quot;https://www.markdownguide.org/&quot;&gt;链接&lt;/a&gt;，也可以写成多个段落。&lt;/p&gt;&lt;p&gt;不过，它仍然应该尽量简短。&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;&lt;span&gt;这段正文用于检查自定义标题、富文本和多段内容。&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;嵌套为卡片堆&lt;/h3&gt;
&lt;p&gt;在一条 &lt;code&gt;&amp;lt;Sidenote&amp;gt;&lt;/code&gt; 中继续嵌套 &lt;code&gt;&amp;lt;Sidenote&amp;gt;&lt;/code&gt;，就会按出现顺序组成卡片堆。对应的 &lt;code&gt;&amp;lt;SidenoteText&amp;gt;&lt;/code&gt; 使用相同顺序嵌套：第一张卡片对应外层文字，后面的卡片对应内层文字。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;Sidenote title=&quot;整句&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  第一张附记对应整句话。&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;Sidenote title=&quot;局部&quot;&amp;gt;第二张附记只对应其中一部分。&amp;lt;/Sidenote&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/Sidenote&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;SidenoteText&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    这是整句话，其中&amp;lt;SidenoteText&amp;gt;这一部分&amp;lt;/SidenoteText&amp;gt;另有一张附记。&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;/SidenoteText&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;aside role=&quot;note&quot;&gt;&lt;span&gt;背景&lt;/span&gt;&lt;p&gt;这是第一张卡片，用来补充整句话的背景信息。&lt;/p&gt;&lt;aside role=&quot;note&quot;&gt;&lt;span&gt;出处&lt;/span&gt;这是第二张卡片，用来记录局部文字的资料出处。&lt;/aside&gt;&lt;aside role=&quot;note&quot;&gt;&lt;span&gt;延伸&lt;/span&gt;这是第三张卡片，提供一个可以继续阅读的方向。&lt;/aside&gt;&lt;/aside&gt;
&lt;p&gt;&lt;span&gt;这句话同时关联了 &lt;span&gt;背景和出处&lt;/span&gt;，也包含 
&lt;span&gt;延伸阅读&lt;/span&gt;。&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;每张卡片保留自己的颜色和文字范围；点击当前卡片可以切换到下一张，把鼠标移到对应文字上也会切换到那张卡片。&lt;/p&gt;
&lt;h2&gt;钢琴窗&lt;/h2&gt;
&lt;p&gt;MDX 页面可以使用全局注册的 &lt;code&gt;&amp;lt;Piano&amp;gt;&lt;/code&gt; 组件。它使用科学音高记号描述按下的琴键，例如中央 C 是 &lt;code&gt;C4&lt;/code&gt;，升 C 可以写成 &lt;code&gt;C#4&lt;/code&gt; 或 &lt;code&gt;C♯4&lt;/code&gt;，降 D 也可以写成 &lt;code&gt;Db4&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;Piano title=&quot;C 大调和弦&quot; notes=&quot;C4 E4 G4&quot; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;figcaption&gt;C 大调和弦&lt;/figcaption&gt;&lt;div&gt;按下：C4、E4、G4&lt;/div&gt;&lt;/figure&gt;
&lt;p&gt;&lt;code&gt;notes&lt;/code&gt; 可以传入字符串或字符串数组，并使用空格、逗号或顿号分隔。浏览者可以点击琴键切换按下状态，钢琴窗会实时列出当前音符；“清空”按钮会释放全部琴键。&lt;/p&gt;
&lt;p&gt;钢琴窗默认从 &lt;code&gt;C4&lt;/code&gt; 开始显示两个八度。可以用 &lt;code&gt;octave&lt;/code&gt; 指定起始八度，用 &lt;code&gt;octaves&lt;/code&gt; 指定一到四个八度：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;Piano title=&quot;低音区的 F 小调和弦&quot; notes=&quot;F2 Ab2 C3&quot; octave={2} octaves={2} /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;figcaption&gt;低音区的 F 小调和弦&lt;/figcaption&gt;&lt;div&gt;按下：F2、G♯2、C3&lt;/div&gt;&lt;/figure&gt;
&lt;p&gt;降号会被规范到对应的升号琴键，因此上例中的 &lt;code&gt;Ab2&lt;/code&gt; 显示为 &lt;code&gt;G♯2&lt;/code&gt;。如果音名无法识别，或音符不在当前琴窗的音域内，构建会直接报错。省略 &lt;code&gt;notes&lt;/code&gt; 则会生成一架没有预先按下琴键的钢琴：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;Piano title=&quot;试着按下几个音&quot; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;figcaption&gt;试着按下几个音&lt;/figcaption&gt;&lt;div&gt;尚未按下音符&lt;/div&gt;&lt;/figure&gt;
&lt;h2&gt;窄屏、RSS 与无障碍&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;Sidenote&amp;gt;&lt;/code&gt; 生成 &lt;code&gt;&amp;lt;aside role=&quot;note&quot;&amp;gt;&lt;/code&gt;；嵌套卡片与嵌套正文按出现顺序配对。在手机或较窄的窗口中，单条附记会成为带有标题的正文块，多条附记仍可逐张切换；术语原文则继续显示在术语上方。&lt;/p&gt;
&lt;p&gt;钢琴窗中的每个琴键都是带有按下状态的按钮，可以通过键盘聚焦并操作。琴键较多或屏幕较窄时，键盘区域可以横向滚动；RSS 中则保留标题与初始音符文字，不依赖交互脚本。&lt;/p&gt;
&lt;p&gt;RSS 使用同一套内容组件，并保留每一张卡片对应的 &lt;code&gt;&amp;lt;aside&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;ruby&amp;gt;&lt;/code&gt; 与 &lt;code&gt;&amp;lt;rt&amp;gt;&lt;/code&gt; 语义；所有附记内容都会依次出现，因此不会依赖客户端交互脚本。&lt;/p&gt;
&lt;h2&gt;相关规范与文档&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://spec.commonmark.org/&quot;&gt;CommonMark&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/markdown-content/&quot;&gt;Astro Markdown&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/integrations-guide/mdx/&quot;&gt;Astro MDX&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://roughnotation.com/&quot;&gt;Rough Notation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/rt&quot;&gt;HTML &lt;code&gt;&amp;lt;rt&amp;gt;&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Rust：使用 uefi-rs 编写一个 UEFI 应用并加载内核</title><link>https://blog.hanbings.io/posts/rust-uefi-bootloader/</link><guid isPermaLink="true">https://blog.hanbings.io/posts/rust-uefi-bootloader/</guid><description>Rust：使用 uefi-rs 编写一个 UEFI 应用并加载内核</description><pubDate>Thu, 16 Jan 2025 03:02:16 GMT</pubDate><content:encoded>&lt;p&gt;本文主要介绍如何使用 Rust 和 &lt;a href=&quot;https://github.com/rust-osdev/uefi-rs&quot;&gt;uefi-rs&lt;/a&gt; 库编写一个 EFI 引导加载程序（Bootloader），该 Bootloader 实现加载并跳转到一个简单的内核，内核则负责在显示区域绘制颜色。&lt;/p&gt;
&lt;p&gt;整个过程分为以下步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;加载文件系统协议；&lt;/li&gt;
&lt;li&gt;使用文件系统协议从指定路径加载文件元数据；&lt;/li&gt;
&lt;li&gt;从元数据中获取文件大小；&lt;/li&gt;
&lt;li&gt;根据文件大小分配内存空间；&lt;/li&gt;
&lt;li&gt;加载文件的实际内容；&lt;/li&gt;
&lt;li&gt;初始化显示协议；&lt;/li&gt;
&lt;li&gt;设置寄存器并跳转至内核代码；&lt;/li&gt;
&lt;li&gt;在内核中绘制颜色。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;使用 uefi-rs 库&lt;/h2&gt;
&lt;h3&gt;UEFI&lt;/h3&gt;
&lt;p&gt;UEFI（Unified Extensible Firmware Interface），统一可扩展固件接口，是一个负责连接硬件和软件之间的接口。&lt;/p&gt;
&lt;p&gt;本文是为了编写了一个可以加载内核的引导器，因此将对使用 &lt;code&gt;uefi-rs&lt;/code&gt;、 &lt;code&gt;Boot Service&lt;/code&gt; 和 &lt;code&gt;Runtime Service&lt;/code&gt; 以及一些必要的 &lt;code&gt;Handle&lt;/code&gt; 和 &lt;code&gt;Protocol&lt;/code&gt; 进行说明，但不会对于 UEFI 本身进行详细的解析，如果对这一方面想要深入了解可以参考 &lt;a href=&quot;https://uefi.org/specs/UEFI/2.10/index.html&quot;&gt;UEFI 手册&lt;/a&gt;、罗冰老师的《UEFI 编程实践》和戴正华老师的《UEFI 原理与编程》。&lt;/p&gt;
&lt;h3&gt;uefi-rs&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Our mission is to provide safe and performant wrappers for UEFI interfaces, and allow developers to write idiomatic Rust code. — uefi-rs&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/tianocore/edk2&quot;&gt;EDK2&lt;/a&gt; （EFI Development Kit）是 UEFI 的开发工具包，使用 C 语言进行 UEFI 工程编程。&lt;a href=&quot;https://github.com/rust-osdev/uefi-rs&quot;&gt;uefi-rs&lt;/a&gt; 是 rust 语言下的 EDK2 封装，巧妙运用了很多 rust 语言的语言特性，使得开发效率大大提升。&lt;/p&gt;
&lt;p&gt;现有大多数的 UEFI 编程资料是基于 C 语言的，使用了很多指针特性来实现功能。在 Rust 中我们有更好的写法抽象和隐藏或安全传递这些指针，因此这里的主要目的是记录 C 语言的写法与 Rust 写法的异同，以便应对阅读参考资料代码时的语言障碍。（如果您有 C / C++ 基础且掌握 Rust 语言那就更好了！）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在本文中我们主要针对 &lt;code&gt;uefi-rs &amp;gt; 0.33&lt;/code&gt; 的版本进行代码编写&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;strong&gt;数据类型&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;从数据类型说起：&lt;/p&gt;
&lt;p&gt;在 EDK2 中，为了适配多种不同架构不同位数的 CPU 而对 C 语言的数据类型系统进行了封装，这些数据类型基本能够对应到 Rust 的类型系统中，下表是从 UEFI 手册中抽取的一部分，完整表格在&lt;a href=&quot;https://uefi.org/specs/UEFI/2.10/02_Overview.html#data-types&quot;&gt;这里&lt;/a&gt;查看。&lt;/p&gt;





















































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;EDK2 Type&lt;/th&gt;&lt;th&gt;Rust / uefi-rs Type&lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;BOOLEAN&lt;/em&gt;&lt;/td&gt;&lt;td&gt;bool&lt;/td&gt;&lt;td&gt;Logical Boolean. 1-byte value containing a 0 for &lt;strong&gt;FALSE&lt;/strong&gt; or a 1 for &lt;strong&gt;TRUE&lt;/strong&gt;. Other values are undefined.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;INTN&lt;/em&gt;&lt;/td&gt;&lt;td&gt;iszie&lt;/td&gt;&lt;td&gt;Signed value of native width. (4 bytes on supported 32-bit processor instructions, 8 bytes on supported 64-bit processor instructions, 16 bytes on supported 128-bit processor instructions)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;UINTN&lt;/em&gt;&lt;/td&gt;&lt;td&gt;usize&lt;/td&gt;&lt;td&gt;Unsigned value of native width. (4 bytes on supported 32-bit processor instructions, 8 bytes on supported 64-bit processor instructions, 16 bytes on supported 128-bit processor instructions)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;INT8&lt;/em&gt;&lt;/td&gt;&lt;td&gt;i8&lt;/td&gt;&lt;td&gt;1-byte signed value.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;UINT8&lt;/em&gt;&lt;/td&gt;&lt;td&gt;u8&lt;/td&gt;&lt;td&gt;1-byte unsigned value.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;INT16&lt;/em&gt;&lt;/td&gt;&lt;td&gt;i16&lt;/td&gt;&lt;td&gt;2-byte signed value.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;UINT16&lt;/em&gt;&lt;/td&gt;&lt;td&gt;u16&lt;/td&gt;&lt;td&gt;2-byte unsigned value.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;INT32&lt;/em&gt;&lt;/td&gt;&lt;td&gt;i32&lt;/td&gt;&lt;td&gt;4-byte signed value.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;UINT32&lt;/em&gt;&lt;/td&gt;&lt;td&gt;u32&lt;/td&gt;&lt;td&gt;4-byte unsigned value.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;INT64&lt;/em&gt;&lt;/td&gt;&lt;td&gt;i64&lt;/td&gt;&lt;td&gt;8-byte signed value.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;UINT64&lt;/em&gt;&lt;/td&gt;&lt;td&gt;u64&lt;/td&gt;&lt;td&gt;8-byte unsigned value.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;INT128&lt;/em&gt;&lt;/td&gt;&lt;td&gt;i128&lt;/td&gt;&lt;td&gt;16-byte signed value.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;UINT128&lt;/em&gt;&lt;/td&gt;&lt;td&gt;u128&lt;/td&gt;&lt;td&gt;16-byte unsigned value.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;CHAR8&lt;/em&gt;&lt;/td&gt;&lt;td&gt;CStr8&lt;/td&gt;&lt;td&gt;1-byte character. Unless otherwise specified, all 1-byte or ASCII characters and strings are stored in 8-bit ASCII encoding format, using the ISO-Latin-1 character set.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;CHAR16&lt;/em&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://docs.rs/uefi/latest/uefi/data_types/struct.CString16.html&quot;&gt;CStr16&lt;/a&gt;&lt;/td&gt;&lt;td&gt;2-byte Character. Unless otherwise specified all characters and strings are stored in the UCS-2 encoding format as defined by Unicode 2.1 and ISO/IEC 10646 standards.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;其中，CStr8 和 CStr16 可以分别使用宏 &lt;a href=&quot;https://docs.rs/uefi-macros/latest/uefi_macros/macro.cstr8.html&quot;&gt;cstr8&lt;/a&gt; 和 &lt;a href=&quot;https://docs.rs/uefi-macros/latest/uefi_macros/macro.cstr16.html&quot;&gt;cstr16&lt;/a&gt; 进行构建。&lt;/p&gt;
&lt;p&gt;此外常用的还有：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EFI_STATUS&lt;/strong&gt;，用于表达函数返回状态（是否出错，是否有值）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EFI_HANDLE&lt;/strong&gt;，即是后续我们会提到的 Handle。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;修饰符&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;在 UEFI 手册中的接口描述中，使用了一些助记词作为参数的修饰符，如下：&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;Mnemonic&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;IN&lt;/em&gt;&lt;/td&gt;&lt;td&gt;Datum is passed to the function.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;OUT&lt;/em&gt;&lt;/td&gt;&lt;td&gt;Datum is returned from the function.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;OPTIONAL&lt;/em&gt;&lt;/td&gt;&lt;td&gt;Passing the datum to the function is optional, and a &lt;em&gt;NULL&lt;/em&gt; may be passed if the value is not supplied.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;CONST&lt;/em&gt;&lt;/td&gt;&lt;td&gt;Datum is read-only.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;EFIAPI&lt;/em&gt;&lt;/td&gt;&lt;td&gt;Defines the calling convention for UEFI interfaces.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;&lt;strong&gt;入口函数&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;EDK2：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EFI_STATUS EFIAPI main (
   IN EFI_HANDLE ImageHandle,
   IN EFI_SYSTEM_TABLE *SystemTable
) { }
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;uefi-rs：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn main(image_handle: Handle, mut system_table: SystemTable&amp;lt;Boot&amp;gt;) -&amp;gt; Status { }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到 IN 类型数据写法实际上是没有什么区别的，但在 Rust 中能够隐藏指针类型和添加准确的泛型。&lt;/p&gt;
&lt;p&gt;在入口中 Image Handle 指向当前 Image（其实也就是当前 EFI 程序），System Table 是一个 UEFI 环境下的全局资源表，存有一些公共数据和函数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;新版本的 uefi-rs API 与旧版本有所不同，下面的新版本的主函数代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[entry]
fn main() -&amp;gt; Status { }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中原先作为参数传递的 &lt;code&gt;image_handle&lt;/code&gt; 和 &lt;code&gt;system_table&lt;/code&gt; 现在以 &lt;code&gt;uefi::boot::image_handler()&lt;/code&gt; 和 &lt;code&gt;uefi::table::system_table_raw()&lt;/code&gt; 或 &lt;code&gt;uefi_raw::table::system&lt;/code&gt; 的形式提供。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;调用函数&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;一般来说，在 EDK2 中函数的返回值为 EFI&lt;em&gt;STATUS 类型，（返回的）数据地址会赋值给参数类型为指针的 _OUT&lt;/em&gt; 参数中，这意味着调用一个函数的步骤是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在手册中找到函数所在的 &lt;code&gt;Table&lt;/code&gt;、&lt;code&gt;Service&lt;/code&gt;、&lt;code&gt;Handle&lt;/code&gt; 和 &lt;code&gt;Protocol&lt;/code&gt; 等对应的数据结构，以函数指针 &lt;code&gt;&amp;gt;&lt;/code&gt; 的方式访问函数。&lt;/li&gt;
&lt;li&gt;查看哪些是 &lt;em&gt;IN&lt;/em&gt; 类型参数，哪些是 &lt;em&gt;OUT&lt;/em&gt; 类型参数&lt;/li&gt;
&lt;li&gt;准备好用于 &lt;em&gt;OUT&lt;/em&gt; 类型参数的空指针&lt;/li&gt;
&lt;li&gt;调用后判断 EFI&lt;em&gt;STATUS 而得到 _OUT&lt;/em&gt; 类型参数的指针是否已指向数据&lt;/li&gt;
&lt;li&gt;从 &lt;em&gt;OUT&lt;/em&gt; 类型参数取出数据&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以获取 Graphics Output Protocol 为例子：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EDK2：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;a href=&quot;https://uefi.org/specs/UEFI/2.10/07_Services_Boot_Services.html?highlight=locateprotocol#efi-boot-services-locateprotocol&quot;&gt;LocateProtocol&lt;/a&gt; 函数获取 Graphics Output Protocol。&lt;/p&gt;
&lt;p&gt;其函数原型为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef
EFI_STATUS
(EFIAPI *EFI_LOCATE_PROTOCOL) (
  IN EFI_GUID                            *Protocol,
  IN VOID                                *Registration OPTIONAL,
  OUT VOID                               **Interface
 );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们需要关注的是第三个参数 Interface，可以看到是一个指针类型的 OUT 类型参数。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;On return, a pointer to the first interface that matches Protocol and Registration. — EFI_LOCATE_PROTOCOL - Interface&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此有代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 声明一个状态，用于接受函数表明执行状态的返回值
EFI_STATUS Status;
// 提前声明一个指针用于指向函数的返回值数据
EFI_GRAPHICS_OUTPUT_PROTOCOL *GraphicsOutput;

// gBS 是 BootService，通过 SystemTable-&amp;gt;BootService 获取
Status = gBS-&amp;gt;LocateProtocol(
    // gEfiGraphicsOutputProtocolGuid 定义在头文件中，是 Graphics Output Protocol 的 UUID
    &amp;amp;gEfiGraphicsOutputProtocolGuid,
    NULL,
    (VOID **)&amp;amp;GraphicsOutput
);
if (EFI_ERROR(Status)) {
    return Status;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;uefi-rs：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;基于 Rust 的特性，可以使用 Result 替换掉 EFI_STATUS 这种需要额外声明一个变量来存放状态的方式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let graphics_output_protocol_handle = boot::get_handle_for_protocol::&amp;lt;GraphicsOutput&amp;gt;()
    // 返回类型为 Result&amp;lt;Handle&amp;gt;
    // 这里便于理解直接使用了 unwarp，但在正常编码中，应该使用 map_or 或 expect 等方式显式处理错误。
    // 尤其是在 UEFI 这类难于调试的环境下，应该尽可能地留下有用的错误信息
    .unwrap();

let mut graphics_output_protocol = boot::open_protocol_exclusive::&amp;lt;GraphicsOutput&amp;gt;(graphics_output_protocol_handle)
    // 返回类型为 Result&amp;lt;ScopedProtocol&amp;lt;GraphicsOutputProtocol&amp;gt;&amp;gt;
    .unwrap();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;UEFI Application 与 UEFI Protocol&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;uefi::helpers::init().unwrap();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;uefi-rs&lt;/code&gt; 包中提供了 &lt;code&gt;uefi::helper::init()&lt;/code&gt; 对 &lt;code&gt;uefi::allocator::Allocator&lt;/code&gt;、&lt;code&gt;log&lt;/code&gt; 和 &lt;code&gt;print!&lt;/code&gt; 宏进行初始化以辅助开发。&lt;/p&gt;
&lt;h3&gt;SimpleFileSystem Protocol&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// load simple file system protocol
let simple_file_system_handle = 
    uefi::boot::get_handle_for_protocol::&amp;lt;SimpleFileSystem&amp;gt;().unwrap();
let mut simple_file_system_protocol = 
    uefi::boot::open_protocol_exclusive::&amp;lt;SimpleFileSystem&amp;gt;(simple_file_system_handle)
        .unwrap();
        
// open volume
let mut root = simple_file_system_protocol.open_volume().unwrap();
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;请注意，代码中使用了许多的 &lt;code&gt;unwrap()&lt;/code&gt; 来展开 &lt;code&gt;Option&lt;/code&gt; 或 &lt;code&gt;Result&lt;/code&gt;，主要是为了避免在此处编写过多的错误处理逻辑。然而，在 &lt;code&gt;uefi-rs&lt;/code&gt; 环境下调试较为困难，通常应显式地处理这些错误，例如打印日志或寄存器状态等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Memory Allocator&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;本段内容需要使用到的常量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static KERNEL_PATH: &amp;amp;str = &quot;\\kernel&quot;;
static FILE_BUFFER_SIZE: usize = 0x400;
static PAGE_SIZE: usize = 0x1000;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// open kernel file in the root using simple file system
let mut kernel_path_buffer = [0u16; FILE_BUFFER_SIZE];
let kernel_path = CStr16::from_str_with_buf(
        KERNEL_PATH, 
        &amp;amp;mut kernel_path_buffer
    ).unwrap();
let kernel_file_handle = root
    .open(kernel_path, FileMode::Read, FileAttribute::empty())
    .unwrap();
let mut kernel_file = match kernel_file_handle.into_type().unwrap() {
    FileType::Regular(f) =&amp;gt; f,
    _ =&amp;gt; panic!(&quot;This file does not exist!&quot;),
};
info!(&quot;Kernel file opened successfully!&quot;);

// load kernel file info and size
let mut kernel_file_info_buffer = [0u8; FILE_BUFFER_SIZE];
let kernel_file_info: &amp;amp;mut FileInfo =
    kernel_file.get_info(&amp;amp;mut kernel_file_info_buffer).unwrap();
info!(&quot;Kernel file info: {:?}&quot;, kernel_file_info);
let kernel_file_size = usize::try_from(kernel_file_info.file_size()).unwrap();
info!(&quot;Kernel file size: {:?}&quot;, kernel_file_size);

// load kernel file into memory
let mut kernel_file_address = uefi::boot::allocate_pages(
    AllocateType::AnyPages,
    MemoryType::LOADER_DATA,
    kernel_file_size / PAGE_SIZE + 1,
)
.unwrap();

let kernel_file_address = unsafe { kernel_file_address.as_mut() as *mut u8 };

let kernel_file_in_memory = unsafe {
    core::ptr::write_bytes(kernel_file_address, 0, kernel_file_size);
    core::slice::from_raw_parts_mut(kernel_file_address, kernel_file_size)
};
let kernel_file_loaded_size = kernel_file.read(kernel_file_in_memory).unwrap();
info!(&quot;Kernel file loaded into memory successfully!&quot;);

let kernel_content = &amp;amp;mut kernel_file_in_memory[..kernel_file_loaded_size];
let kernel_address = kernel_content.as_ptr() as *const u8 as usize;
info!(&quot;Kernel file address: 0x{:x}&quot;, kernel_address);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本步骤用于将文件加载到内存，其中需要注意的地方是文件 handle、文件 info 都不是文件本体，且文件路径本身也是需要内存空间存放的。&lt;/p&gt;
&lt;p&gt;我们一步步来解析这一大段代码。&lt;/p&gt;
&lt;p&gt;这里我们开辟一个栈上数组来进行存放文件路径，即以下代码：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果您从 Java 和 Python 等高度抽象且隐藏堆栈概念的语言转到 Rust，可能会觉得理解堆栈有些困难。在本文中，堆栈均指程序运行时的内存分布：堆内存和栈内存。它们的主要区别在于数据结构的存储方式以及程序员的操作权限。&lt;/p&gt;
&lt;p&gt;栈内存主要用于存放可执行代码和生命周期受限的局部变量。其管理由编译器处理，并通过操作系统自动分配，程序通常从栈顶开始执行代码。而堆内存则主要存储动态数据，由程序员通过向操作系统申请分配。一般情况下，长度不确定的数据会优先存放在堆内存中，并通过指针进行管理。在 Rust 中，这通常表现为使用 &lt;code&gt;Box&amp;lt;T&amp;gt;&lt;/code&gt; 等智能指针；而对于长度固定的数据，优先存储在栈内存中，例如 &lt;code&gt;let a = 0;&lt;/code&gt; 这样的变量声明。&lt;/p&gt;
&lt;p&gt;所谓“在栈上开辟数组”，指的是为固定长度的数组分配栈内存空间。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;本文使用栈上数组存储文件路径，但应该也可以申请堆内存完成这个工作，有兴趣可以进行尝试，后续加载内核文件的实际内容时也会使用堆内存。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// open kernel file in the root using simple file system
let mut kernel_path_buffer = [0u16; FILE_BUFFER_SIZE];
let kernel_path = CStr16::from_str_with_buf(
        KERNEL_PATH, 
        &amp;amp;mut kernel_path_buffer
    ).unwrap();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将路径加载到内存之后便可以加载文件的 &lt;code&gt;handle&lt;/code&gt; ，&lt;code&gt;handle&lt;/code&gt; 类似于 Java 中的 &lt;code&gt;java.io.File&lt;/code&gt; 或是 C  中的 &lt;code&gt;FILE&lt;/code&gt;，负责处理文件的读写熟悉，是否为目录等。以下代码用于加载 &lt;code&gt;handle&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let kernel_file_handle = root
    .open(kernel_path, FileMode::Read, FileAttribute::empty())
    .unwrap();
let mut kernel_file = match kernel_file_handle.into_type().unwrap() {
    FileType::Regular(f) =&amp;gt; f,
    _ =&amp;gt; panic!(&quot;This file does not exist!&quot;),
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功获取到 &lt;code&gt;handle&lt;/code&gt; 后则需要从 &lt;code&gt;handle&lt;/code&gt; 中获取文件大小，用于确定需要分配存储内核文件的内存大小。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// load kernel file info and size
let mut kernel_file_info_buffer = [0u8; FILE_BUFFER_SIZE];
let kernel_file_info: &amp;amp;mut FileInfo =
    kernel_file.get_info(&amp;amp;mut kernel_file_info_buffer).unwrap();
info!(&quot;Kernel file info: {:?}&quot;, kernel_file_info);
let kernel_file_size = usize::try_from(kernel_file_info.file_size()).unwrap();
info!(&quot;Kernel file size: {:?}&quot;, kernel_file_size);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好耶！接下来是分配堆内存空间：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;uefi::boot::allocate_pages()&lt;/code&gt; 接受三位参数，页类型、数据类型和页数量。我们需要使得数据段可执行，因此选择 &lt;code&gt;AllocateType::AnyPages&lt;/code&gt; （任意位置的可分配内存空间）和 &lt;code&gt;MemoryType::LOADER_DATA&lt;/code&gt;  （任意位置的可分配内存空间和可执行内存）。其他的类型可以在 &lt;a href=&quot;https://docs.rs/uefi/latest/uefi/boot/enum.AllocateType.html&quot;&gt;https://docs.rs/uefi/latest/uefi/boot/enum.AllocateType.html&lt;/a&gt; 和 &lt;a href=&quot;https://docs.rs/uefi/latest/uefi/mem/memory_map/struct.MemoryType.html&quot;&gt;https://docs.rs/uefi/latest/uefi/mem/memory_map/struct.MemoryType.html&lt;/a&gt; 中查看。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kernel_file_size / PAGE_SIZE + 1&lt;/code&gt; 则是按照内核文件大小向上再多取一页，保证有足够的空间来存储内核代码。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;PAGE_SIZE：页大小，本文假设机器是以 4k 为分页的 UEFI 固件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;let mut kernel_file_address = uefi::boot::allocate_pages(
    AllocateType::AnyPages,
    MemoryType::LOADER_DATA,
    kernel_file_size / PAGE_SIZE + 1,
)
.unwrap();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着使用 &lt;code&gt;handle&lt;/code&gt; 中的 &lt;a href=&quot;https://docs.rs/uefi/latest/uefi/proto/media/file/struct.RegularFile.html#method.read&quot;&gt;https://docs.rs/uefi/latest/uefi/proto/media/file/struct.RegularFile.html#method.read&lt;/a&gt; 将文件内容加载到分配出来这片内存地址即可完成加载的功能了！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let kernel_file_address = unsafe { kernel_file_address.as_mut() as *mut u8 };

let kernel_file_in_memory = unsafe {
    core::ptr::write_bytes(kernel_file_address, 0, kernel_file_size);
    core::slice::from_raw_parts_mut(kernel_file_address, kernel_file_size)
};
let kernel_file_loaded_size = kernel_file.read(kernel_file_in_memory).unwrap();
info!(&quot;Kernel file loaded into memory successfully!&quot;);

let kernel_content = &amp;amp;mut kernel_file_in_memory[..kernel_file_loaded_size];
let kernel_address = kernel_content.as_ptr() as *const u8 as usize;
info!(&quot;Kernel file address: 0x{:x}&quot;, kernel_address);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;GraphicsOutput Protocol&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// init display
let gop_handler = uefi::boot::get_handle_for_protocol::&amp;lt;GraphicsOutput&amp;gt;().unwrap();
let mut gop = 
    uefi::boot::open_protocol_exclusive::&amp;lt;GraphicsOutput&amp;gt;(gop_handler)
        .unwrap();

let graphic_info = GraphicInfo {
    frame_buffer_addr: gop.frame_buffer().as_mut_ptr() as u64,
    frame_buffer_size: gop.frame_buffer().size() as u64,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段相对简单，用于加载 Protocol 来获取显示信息。&lt;/p&gt;
&lt;h2&gt;Rust Inline ASM&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;unsafe {
    core::arch::asm!(
        &quot;
        jmp {}
        &quot;, 
        in(reg) kernel_address,
        in(&quot;rdi&quot;) graphic_info.frame_buffer_addr,
        in(&quot;rcx&quot;) graphic_info.frame_buffer_size,
        options(noreturn)
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;jmp&lt;/code&gt; 指令，并提供前面一些的代码中把内核文件的内存地址，就可以跳转至内核了。&lt;/p&gt;
&lt;p&gt;另外，为了方便内核实现绘制屏幕的功能，我们还需要将显示区域的信息提前写入到寄存器中。这里我们选择 &lt;code&gt;rdi&lt;/code&gt; 和 &lt;code&gt;rcx&lt;/code&gt; 两个寄存器，&lt;code&gt;rdi&lt;/code&gt; 写入 &lt;code&gt;frame buffer&lt;/code&gt; 的基地址，而 &lt;code&gt;rcx&lt;/code&gt; 写入 &lt;code&gt;frame buffer&lt;/code&gt; 的大小。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Rust 的内联汇编使用 &lt;code&gt;in(reg) value&lt;/code&gt; 来为寄存器赋值，使用 &lt;code&gt;out(reg) mutable_value&lt;/code&gt; 来读取寄存器的值。&lt;code&gt;options(noreturn)&lt;/code&gt; 意味着执行这段汇编永不返回，相当于 &lt;code&gt;!&lt;/code&gt; 或是说 &lt;code&gt;never&lt;/code&gt;，可能会跳入死循环或是 &lt;code&gt;hlt&lt;/code&gt; 低能耗模式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;另外还需要注意的是，Rust 并非允许自由调整所有的寄存器，本文只是恰好使用了两个空闲的寄存器。更多内容请阅读：&lt;a href=&quot;https://doc.rust-lang.org/reference/inline-assembly.html&quot;&gt;https://doc.rust-lang.org/reference/inline-assembly.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;frame buffer 以行优先的方式将显示器的像素映射到内存中，下一步中将使用一个汇编循环语句将它填充以显示特定的颜色。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.miji.bid/2025/01/16/a1851c2caa53a101a2fc0a02f4a74b8b.png&quot; alt=&quot;frame buffer&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;内核部分&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;BITS 64

mov rax, 0xff408deb

display:
    mov [rdi], rax
    add rdi, 4
    loop display

jmp $
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是内核的全部代码了。&lt;/p&gt;
&lt;p&gt;正如本文开头所提到的，我们要向屏幕填充颜色 &lt;code&gt;0xff408deb&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;0xff408deb 的顺序为 A（Alpha）RGB，GraphicsOutput Protocol 中的颜色顺序是 BGRA，但在 x86 中以小端方式表达便是 ARGB 了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先，程序将 RGB 值赋给 &lt;code&gt;rax&lt;/code&gt; 寄存器，然后在 &lt;code&gt;display&lt;/code&gt; 代码块中进行循环写入。这里，&lt;code&gt;rdi&lt;/code&gt; 寄存器由 Bootloader 代码设置为 &lt;code&gt;graphic_info.frame_buffer_addr&lt;/code&gt;，即显存映射到内存的起始地址。&lt;code&gt;rcx&lt;/code&gt; 寄存器则保存了先前设置的 &lt;code&gt;graphic_info.frame_buffer_size&lt;/code&gt;，即显存映射的总内存大小。为了在屏幕上填充这个颜色，我们需要遍历整个显存区域。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;loop&lt;/code&gt; 指令用于控制循环，确保每次写入后，&lt;code&gt;rcx&lt;/code&gt; 寄存器的值会减 1，直到其值为 0，才会跳出循环并执行下一条指令。通过这种方式，程序可以连续地将颜色写入显存，直到整个屏幕都被填充。&lt;/p&gt;
&lt;h2&gt;启动！&lt;/h2&gt;
&lt;p&gt;新建 esp 目录并拷贝 uefi 的编译结果：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;bootx64.efi 是约定名称，UEFI 固定会自动使用该文件，文件名不区分大小写。如果使用其他名字则需要进行 UEFI Shell 后手动执行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p esp/efi/boot/
cargo build --bin efi --target x86_64-unknown-uefi
mkdir -p esp/efi/boot/
cp target/x86_64-unknown-uefi/debug/efi.efi esp/efi/boot/bootx64.efi

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译和拷贝内核：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nasm kernel.asm -o kernel
cp kernel esp/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来启动它吧！‘&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意替换 &lt;code&gt;$(OVMF_CODE_PATH)&lt;/code&gt; 和 &lt;code&gt;$(OVMF_VARS_PATH)&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;qemu-system-x86_64 \
    -m 256 \
    -enable-kvm \
    -drive if=pflash,format=raw,readonly=on,file=$(OVMF_CODE_PATH) \
    -drive if=pflash,format=raw,readonly=on,file=$(OVMF_VARS_PATH) \
    -drive format=raw,file=fat:rw:esp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://i.miji.bid/2025/01/16/2027c82d740e142e3331d7aec3ecf825.png&quot; alt=&quot;test&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/hanbings/canicula/tree/blog-uefi-bootloader&quot;&gt;本文完整源码&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>oscamp：操作系统 Hypervisor 方向（Draft）</title><link>https://blog.hanbings.io/posts/oscamp-stage4-hypervisor/</link><guid isPermaLink="true">https://blog.hanbings.io/posts/oscamp-stage4-hypervisor/</guid><description>oscamp：操作系统 Hypervisor 方向</description><pubDate>Fri, 20 Dec 2024 21:01:12 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;跪了，开发环境好麻烦哦！！！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;是 OS 训练营阶段四（以前的阶段三）的虚拟化任务，借此机会来学习虚拟化相关的知识。&lt;/p&gt;
&lt;h2&gt;开发环境&lt;/h2&gt;
&lt;p&gt;我的开发环境是位于 PVE 中的虚拟机，所以如果需要虚拟机也支持 vmx 技术就需要在 PVE 上打开嵌套虚拟化。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Windows 同理，且虚拟化技术至少需要从 BIOS 打开虚拟化支持，Windows 平台中打开任务管理器切换到 CPU 详细页右下角就可以看到支持信息。WSL 本质是虚拟机，因此如果是在 WSL 中进行开发也需要打开嵌套虚拟化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在虚拟机中使用 &lt;code&gt;grep &quot;vmx|svm&quot; /proc/cpuinfo&lt;/code&gt; 如果有输出则说明已经启用了嵌套虚拟化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# INTEL
modprobe -r kvm_intel 
modprobe kvm_intel nested=1
# 这句是添加到启动时也同时加载模块，amd 也同样是这样
echo &quot;options kvm_intel nested=1&quot; &amp;gt;&amp;gt; /etc/modprobe.d/modprobe.conf

# AMD
modprobe -r kvm_amd
modprobe kvm_amd nested=1
echo &quot;options kvm_amd nested=1&quot; &amp;gt;&amp;gt; /etc/modprobe.d/modprobe.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;其实就这两条指令，但是 PVE 需要关闭了全部 kvm 虚拟机才可以进行操作。而我的 PVE 里同时还有软路由虚拟机（？），如果关闭了它意味着我无法通过网络连接到机器上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;完成后使用 &lt;code&gt;cat /sys/module/kvm_intel/parameters/nested&lt;/code&gt; 查询 PVE 是否已经打开嵌套虚拟化。输出结果可能会是 &lt;code&gt;1&lt;/code&gt; 或者 &lt;code&gt;Y&lt;/code&gt; 。&lt;/p&gt;
&lt;h2&gt;AMD 不嘻嘻&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://ice.frostsky.com/2024/12/20/f1a65c14ba02e762daeb033a14b61fb3.png&quot; alt=&quot;umhv not support amd&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据 arceos-hypervisor 社区成员的反馈，arceos-umhv 暂时还不支持 AMD 平台。😣😣😣&lt;/p&gt;
&lt;p&gt;且我换用 Intel 的 WIndows 机器也不可以，我猜这和 Windows 的内核保护有关。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ice.frostsky.com/2024/12/20/5dd8d429315e87e05ac04e30a5a41fa6.png&quot; alt=&quot;hv&quot; /&gt;&lt;/p&gt;
&lt;p&gt;AMD 不嘻嘻，Intel 也没好哪里去！&lt;/p&gt;
&lt;p&gt;考虑到关闭 Windows 内核保护在我本机容易引起蓝屏 + 不方便在教室里写代码，尝试使用 bochs 模拟器对 vmx 进行模拟和尝试写一写 AMD svm 的兼容代码。&lt;/p&gt;
&lt;h2&gt;在 Bochs 中全仿真 vmx&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 在 umhv 中使用 make 直接进行编译
$ make

# 产生文件
# ./arceos-umhv/arceos-vmm/arceos-vmm_x86_64-qemu-q35.elf
# ./arceos-umhv/arceos-vmm/arceos-vmm_x86_64-qemu-q35.bin
# ./arceos-umhv/arceos-vmm/arceos-vmm_x86_64-qemu-q35.asm

# 从文件里查询是否具有 Multiboot 头以便使用 grub 进行引导
$ hexdump -C /home/hanbings/github/arceos-umhv/arceos-vmm/arceos-vmm_x86_64-qemu-q35.elf | grep -E &apos;02 b0 ad 1b| d6 50 52 e8&apos;         
00001000  89 c7 89 de eb 22 66 90  02 b0 ad 1b 02 00 01 00  |.....&quot;f.........|

# Multiboot 1 魔数：0x1BADB002
# Multiboot 2 魔数：0xE85250D6
# 说明我们这里的是 Multiboot 1 的文件
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Intel VMX 指令集与 AMD SVM 指令集&lt;/h2&gt;




































































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;特性&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Intel VT-x (VMCS)&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;AMD V (VMCB)&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;数据结构名称&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Virtual Machine Control Structure (VMCS)&lt;/td&gt;&lt;td&gt;Virtual Machine Control Block (VMCB)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;数据结构存储&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;硬件内部（需要通过 &lt;strong&gt;&lt;code&gt;VMREAD&lt;/code&gt;&lt;/strong&gt; 和 &lt;strong&gt;&lt;code&gt;VMWRITE&lt;/code&gt;&lt;/strong&gt; 访问）&lt;/td&gt;&lt;td&gt;显式内存结构（直接可通过内存指针访问）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;数据结构大小&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;不固定（硬件实现决定），通常 ≥4KB&lt;/td&gt;&lt;td&gt;固定大小：4 KB&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;访问方式&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;使用指令：&lt;strong&gt;&lt;code&gt;VMREAD&lt;/code&gt;&lt;/strong&gt;/&lt;strong&gt;&lt;code&gt;VMWRITE&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;直接内存访问&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;组成部分&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;多个字段：控制字段、Guest 状态字段、Host 状态字段等&lt;/td&gt;&lt;td&gt;两部分：&lt;strong&gt;控制区域&lt;/strong&gt; 和 &lt;strong&gt;状态保存区域&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;指令拦截机制&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;配置拦截字段：I/O 位图、MSR 位图、控制字段等&lt;/td&gt;&lt;td&gt;精细化拦截：位图支持，对指令、I/O 操作的拦截更加灵活&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;二级地址转换支持&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;使用 EPT（Extended Page Table）&lt;/td&gt;&lt;td&gt;使用 NPT（Nested Page Table）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;灵活性&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;受限于硬件设计，部分功能依赖 Intel 固定的机制&lt;/td&gt;&lt;td&gt;更加灵活，因 VMCB 可直接通过内存访问，自定义更方便&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;事件注入功能&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;支持：通过 VMCS 的字段注入中断或异常&lt;/td&gt;&lt;td&gt;支持：通过控制区域注入中断或异常&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;性能优化&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;VMCS 缓存到硬件中，通过 VMCS Shadowing 提高性能&lt;/td&gt;&lt;td&gt;直接内存访问，无需硬件缓存，但性能略低&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;断点和调试支持&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;可在 VMCS 中设置调试寄存器和断点配置&lt;/td&gt;&lt;td&gt;可通过 VMCB 的状态保存区域直接设置调试寄存器&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;状态保存机制&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;使用 VMCS 的 Guest 状态字段保存虚拟机寄存器及状态&lt;/td&gt;&lt;td&gt;使用 VMCB 的状态保存区域存储虚拟机寄存器及状态&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;扩展性&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;添加新功能需要通过硬件升级&lt;/td&gt;&lt;td&gt;可通过软件扩展 VMCB 的内容&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;I/O 位图支持&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;支持，通过字段配置 I/O 位图&lt;/td&gt;&lt;td&gt;支持，通过控制区域设置 I/O 位图指针&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;MSR（Model Specific Register）管理&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;MSR 位图用于拦截和控制 MSR 访问&lt;/td&gt;&lt;td&gt;MSR 拦截由拦截字段和控制区域灵活配置&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;时间管理（TSC 偏移）&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;支持：通过 &lt;strong&gt;&lt;code&gt;TSC_OFFSET&lt;/code&gt;&lt;/strong&gt; 字段控制&lt;/td&gt;&lt;td&gt;支持：通过 TSC 偏移字段直接操作&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Nested Virtualization（嵌套虚拟化）&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;支持（&lt;strong&gt;&lt;code&gt;VMCS Shadowing&lt;/code&gt;&lt;/strong&gt;）&lt;/td&gt;&lt;td&gt;支持（通过嵌套 VMCB 配置）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;指令支持&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;专用指令：&lt;strong&gt;&lt;code&gt;VMXON&lt;/code&gt;&lt;/strong&gt;、&lt;strong&gt;&lt;code&gt;VMRESUME&lt;/code&gt;&lt;/strong&gt;、&lt;strong&gt;&lt;code&gt;VMEXIT&lt;/code&gt;&lt;/strong&gt;、&lt;strong&gt;&lt;code&gt;VMREAD&lt;/code&gt;&lt;/strong&gt; 等&lt;/td&gt;&lt;td&gt;专用指令：&lt;strong&gt;&lt;code&gt;VMRUN&lt;/code&gt;&lt;/strong&gt;、&lt;strong&gt;&lt;code&gt;VMEXIT&lt;/code&gt;&lt;/strong&gt;、&lt;strong&gt;&lt;code&gt;VMSAVE&lt;/code&gt;&lt;/strong&gt;、&lt;strong&gt;&lt;code&gt;VMLOAD&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;需要做的工作为：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;CPU 虚拟化支持&lt;/li&gt;
&lt;li&gt;内存虚拟化&lt;/li&gt;
&lt;li&gt;中断虚拟化&lt;/li&gt;
&lt;li&gt;设备虚拟化&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;目前思路&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;为简化环境，为 &lt;a href=&quot;https://github.com/equation314/RVM-Tutorial&quot;&gt;https://github.com/equation314/RVM-Tutorial&lt;/a&gt; 适配 SVM&lt;/li&gt;
&lt;li&gt;在 RVM 中可运行的 SVM 代码移植回 &lt;a href=&quot;https://github.com/arceos-hypervisor/x86_vcpu&quot;&gt;https://github.com/arceos-hypervisor/x86_vcpu&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;为 &lt;a href=&quot;https://github.com/arceos-hypervisor/axvcpu&quot;&gt;https://github.com/arceos-hypervisor/axvcpu&lt;/a&gt; 添加适配 SVM 的代码&lt;/li&gt;
&lt;li&gt;为 &lt;a href=&quot;https://github.com/arceos-hypervisor/arceos-umhv&quot;&gt;https://github.com/arceos-hypervisor/arceos-umhv&lt;/a&gt; 添加适配 SVM 的代码&lt;/li&gt;
&lt;li&gt;向 &lt;a href=&quot;https://github.com/gz/rust-x86&quot;&gt;https://github.com/gz/rust-x86&lt;/a&gt; 仓库贡献 svm 代码（也许）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;进度&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.hanbings.io/posts/rvm-amd-support&quot;&gt;虚拟化技术：AMD SVM Hypervisor&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>虚拟化技术：AMD SVM Hypervisor（Draft）</title><link>https://blog.hanbings.io/posts/rvm-amd-support/</link><guid isPermaLink="true">https://blog.hanbings.io/posts/rvm-amd-support/</guid><description>基于 rvm 适配 AMD SVM 的 Hypervisor</description><pubDate>Fri, 20 Dec 2024 21:01:12 GMT</pubDate><content:encoded>&lt;p&gt;本文是基于 &lt;a href=&quot;https://github.com/equation314/RVM-Tutorial&quot;&gt;https://github.com/equation314/RVM-Tutorial&lt;/a&gt; 做的 AMD SVM 适配，尽可能展示相关代码，但限于篇幅不会完全贴入代码。以及参考了 &lt;a href=&quot;https://web.archive.org/web/20220815132158/https://key08.com/index.php/2021/04/11/1004.html&quot;&gt;https://web.archive.org/web/20220815132158/https://key08.com/index.php/2021/04/11/1004.html&lt;/a&gt; 和 &lt;a href=&quot;https://web.archive.org/web/20241008083327/https://blog.back.engineering/04/08/2022/%EF%BC%8C%E6%84%9F%E8%B0%A2%E5%89%8D%E8%BE%88%E8%AF%A6%E7%BB%86%E7%9A%84%E6%96%87%E7%AB%A0%E3%80%82&quot;&gt;https://web.archive.org/web/20241008083327/https://blog.back.engineering/04/08/2022/，感谢前辈详细的文章。&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;若无特殊说明，文中提及的页数均依据 2024 版本。下称 “手册”，使用 “手册 xx 页” 标注页面，作为补充下方链接还包括一份旧版本的 AMD 虚拟化技术的独立文档。本文汇编代码均使用 nasm 格式或 Rust 内联汇编。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(24593—Rev. 3.42—March 2024) AMD64 Architecture Programmer’s Manual Volumes 1–5 &lt;a href=&quot;https://web.archive.org/web/20241114034725/https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/programmer-references/40332.pdf&quot;&gt;https://web.archive.org/web/20241114034725/https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/programmer-references/40332.pdf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;(33047–Rev. 3.02–December 2005) AMD64 Virtualization Technology Secure Virtual Machine Architecture Reference Manual &lt;a href=&quot;https://web.archive.org/web/20240828193507/http://www.0x04.net/doc/amd/33047.pdf&quot;&gt;https://web.archive.org/web/20240828193507/http://www.0x04.net/doc/amd/33047.pdf&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;检查本机是否支持虚拟化技术&lt;/h2&gt;
&lt;p&gt;在 AMD 中，虚拟化技术指令集名称为 SVM，与 Intel VT-x 的 VMX 类似，但在细节上有所不同。&lt;/p&gt;
&lt;p&gt;据手册 955 页描述，在开启虚拟化支持前需要先进行检查：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检查 CPUID 功能号 Fn 8000_0001 的 ECX[SVM] 位为 1&lt;/li&gt;
&lt;li&gt;VM CR 寄存器的 SVMDIS 位为 0（如果 CPU 支持 SVM，但 BIOS 中有对虚拟化技术的限制这一个为 1）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;然后通过将 EFER.SVME 设置为 1 即可开启虚拟化功能。&lt;/p&gt;
&lt;h3&gt;CPUID 与 SVM 特性&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;通过 CPUID 指令，可以从 CPU 中查询 CPU 支持的特性、CPU 厂商信息等。直接调用 cpuid 指令（eax = 0x0000_0000）会返回 CPU 的字符串标识符，这个标识符将拆分存放在 ebx、ecx、edx 中（在 Intel 通常是 GenuineIntel，AMD 通常是 AuthenticAMD）。通过改变 eax 的值，cpuid 将返回不同的内容。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Fn 8000_0001 传参给 ECX 寄存器的位说明在手册 1894 页，查看表 CPUID Fn8000_0001_ECX Feature Identifiers，看到 SVM 位会被 CPUID 放置在 ECX 的第三个位（index 2）。&lt;/p&gt;
&lt;p&gt;因此我们要做的操作如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mov eax, 0x80000001
cpuid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是在 rust 的 asm 中还需要 &lt;a href=&quot;https://course.rs/advance/unsafe/inline-asm.html#clobbered-%E5%AF%84%E5%AD%98%E5%99%A8&quot;&gt;处理与 LLVM 相关的部分&lt;/a&gt;，因此为了简单起见，我们使用 &lt;a href=&quot;https://github.com/gz/rust-x86&quot;&gt;x86&lt;/a&gt; 库对 cpuid 进行读取。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// asm! 写法
fn get_cpuid_80000001_ecx() -&amp;gt; u32 {
    let mut ecx: u32;

    unsafe {
        asm!(
            &quot;cpuid&quot;, 
            inout(&quot;eax&quot;) 0x80000001u32 =&amp;gt; _,
            out(&quot;ecx&quot;) ecx,
            options(nostack, preserves_flags)
        );
    }

    ecx
}

// 使用 x86::cpuid
let cpuid = x86::cpuid::CpuIdResult::from(x86::cpuid::cpuid!(0x80000001));
let ecx = cpuid.ecx;

// 然后读取 svm 位即可
let svm = cpuid.ecx &amp;amp; (1 &amp;lt;&amp;lt; 2);

info!(&quot;[RVM] CPUID: {:#x}, SVM Feature: {:#?}&quot;, ecx, svm != 0);
if svm == 0 {
    return rvm_err!(Unsupported, &quot;CPU does not support feature SVM&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;VM CR&lt;/h3&gt;
&lt;p&gt;VM CR 的描述位于手册 1039 页，SVMDIS 在第五位（index 4）。&lt;/p&gt;
&lt;p&gt;VM CR 也需要使用指令获取，rust 的 asm 代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn read_msr(msr: u32) -&amp;gt; u64 {
    let mut low: u32;
    let mut high: u32;

    unsafe {
        asm!(
            &quot;rdmsr&quot;,
            in(&quot;ecx&quot;) msr,
            out(&quot;eax&quot;) low,
            out(&quot;edx&quot;) high,
        );
    }

    ((high as u64) &amp;lt;&amp;lt; 32) | (low as u64)
}

// 获取
let msr_value = read_msr(0xC0010114);
let svm_disabled = msr_value &amp;amp; (1 &amp;lt;&amp;lt; 4);

info!(&quot;[RVM] VM_CR register value: {:#x}, SVM Enabled: {:?}&quot;, msr_value, svm_disabled == 0);
if svm_disabled != 0 {
    return rvm_err!(ResourceBusy, &quot;SVM is not enabled in VM_CR register&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;开启 EFER.SVME&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://ice.frostsky.com/2024/12/20/fbf343ec8ee5d0a1893daacf99dc4f94.png&quot; alt=&quot;efer&quot; /&gt;&lt;/p&gt;
&lt;p&gt;EFER 同样也是一种 MSR 寄存器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn enable_svm() {
    let mut low: u32;
    let mut high: u32;

    unsafe {
        asm!(
            &quot;rdmsr&quot;,
            in(&quot;ecx&quot;) 0xC0000080u32,
            out(&quot;eax&quot;) low,
            out(&quot;edx&quot;) high,
        );
    }

    low |= 1 &amp;lt;&amp;lt; 12;
    
    unsafe {
        asm!(
            &quot;wrmsr&quot;,
            in(&quot;ecx&quot;) 0xC0000080u32,
            in(&quot;eax&quot;) low,
            in(&quot;edx&quot;) high,
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;切换到虚拟化环境&lt;/h2&gt;
&lt;p&gt;为了切换进虚拟化环境，我们需要让原先 CPU 寄存器的数据先被保存在某个区域，然后使用 vmrun 进入虚拟化环境。而保存和恢复 CPU 寄存器的工作将由 CPU 自行完成，在 AMD 中，我们只需要开辟一片用于保存当前 CPU 的内存页并设置 VM_HSAVE_PA MSR 即可。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;手册 957 页：&lt;/p&gt;
&lt;p&gt;Processor implementations may store only part or none of host state in the memory area pointed to by VM_HSAVE_PA MSR and may store some or all host state in hidden on-chip memory. Different implementations may choose to save the hidden parts of the host’s segment registers as well as the selectors. For these reasons, software must not rely on the format or contents of the host state save area, nor attempt to change host state by modifying the contents of the host save area.&lt;/p&gt;
&lt;p&gt;明确指出，vmm 程序不应该依赖特定的主机状态保存顺序来修改这些数据。这点与 intel 不同，在 intel 的 vmx 中，需要 vmm 对主机状态进行保存。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;初始化与加载 VMCB&lt;/h3&gt;
&lt;h3&gt;设置退出条件&lt;/h3&gt;
&lt;p&gt;当我们用 vmrun 切换进虚拟化环境之后，&lt;/p&gt;
&lt;h3&gt;VMRUN&lt;/h3&gt;
&lt;p&gt;那么… vmrun？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ice.frostsky.com/2024/12/20/2921999a10415f1ae73683ef2a25ddc3.png&quot; alt=&quot;gp#0&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这是怎么回事呢？我们返回手册，手册 956 页，在 15.5 VMRUN Instruction 的 15.5.1 Basic Operation 节有一句：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;VMRUN is available only at CPL 0. A #GP(0) exception is raised if the CPL is greater than 0.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;再结合 log 的 error，是一个 error code 为 0 的 #GP。是不是 CPL 不为 0 导致了这个问题出现呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ice.frostsky.com/2024/12/20/e7e75dcbabfa70afd5ff816c33dbd272.png&quot; alt=&quot;trap&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;CS 为 0x8，也就是 0b1000，低两位为 00，那么说明执行指令时特权级是 0。&lt;/li&gt;
&lt;/ol&gt;</content:encoded></item><item><title>Rust：实现一份 Mimalloc（Draft）</title><link>https://blog.hanbings.io/posts/rust-implement-mimalloc/</link><guid isPermaLink="true">https://blog.hanbings.io/posts/rust-implement-mimalloc/</guid><description>Rust 实现一份 Mimalloc -- Chaos HMM (Chaos Heap Memory Manager)</description><pubDate>Thu, 12 Dec 2024 04:32:03 GMT</pubDate><content:encoded>&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bluepuni.com/archives/paper-reading-mimalloc-free-list-sharding-in-action/&quot;&gt;[论文阅读] Mimalloc: Free List Sharding in Action&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.microsoft.com/en-us/research/uploads/prod/2019/06/mimalloc-tr-v1.pdf&quot;&gt;论文原文&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;mimalloc 核心机制&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;分配（空间）局限性（ Locality of Allocation）&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;采用极端小页（mimalloc 页，与操作系统内存页有区别，前者只是在 mimalloc 系统中的页概念，除非另有声明，后文的页都指 mimalloc 页，这里的极端小页为 64 k），并为每一个页提供空闲列表。分配过程中尽可能在一个页中分配，直到本页被分配满，即优先分配本页空间。&lt;/p&gt;
&lt;p&gt;这种机制有助于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提高 CPU 缓存的命中率&lt;/li&gt;
&lt;li&gt;减少内存访问的随机性&lt;/li&gt;
&lt;li&gt;缓解内存碎片化&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;free_list、local_list 与 thread_list&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;使用分离的处理方式存储不同类别的内存。free_list 存储从未分配的内存，local_list 存储从本线程释放回来的内存空间，thread_list 存储其他线程释放回来的空间。&lt;/p&gt;
&lt;p&gt;每一个页都有自己的 free_list、local_list 与 thread_list。&lt;/p&gt;
&lt;p&gt;本地线程与其他线程的意思是，假设我们从 A 线程（主线程）和 B 线程（开辟的子线程）申请两次内存并释放，假设主线程释放的内存与 allocator 所处线程一致，那么这片内存将返还到 local_list，而 B 线程的释放的内存则会先到 thread_list 中，等待处理。&lt;/p&gt;
&lt;p&gt;在 mimalloc 中，local_list 和 thread_list 并不是马上进行回收到 free_list 的，mimalloc 优先从 free_list 分配内存，然后出现两种两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;快路径（fast allocation path）&lt;/p&gt;
&lt;p&gt;free_list 还有空闲内存，直接分配。（这块在 mimalloc 的 c 语言官方实现版本仅五行代码）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;慢路径（generic allocation path）&lt;/p&gt;
&lt;p&gt;free_list 没有空闲内存了。从 local_list 分配，如果也空闲内存了，那么将触发回收机制，从 thread_list 原子性的回收其他线程释放的内存。这里的原子性，指的不仅仅是内存回收时会移动链表指针到别的链表里，且还需要锁定几个全局性的变量：使用量、本页使用量等。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了防止存在不断申请马上又释放的内存同时占用了 free_list 和 local_list 造成了性能下降，每当固定几次申请内存（maintain a deterministic heartbeat）就会触发一次内存回收。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;不使用碰撞指针&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;是指划分一段连续内存段，定义一根在段内的指针，指针的左边是已使用内存，右边是未使用内存。当申请了一些内存后，实际分配从指针开始，指针向右边挪一点表示已使用内存增加了，反之，如果释放了一些内存就向指针左挪一点。&lt;/p&gt;
&lt;p&gt;缺点很明显：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;内存分配和释放不一定是连续的，如果采用整理算法，又增加了整体的复杂性&lt;/li&gt;
&lt;li&gt;安全性较低，容易被溢出攻击&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;&lt;strong&gt;Chaos HMM 实现&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;本实现并未过多参考 &lt;a href=&quot;https://github.com/microsoft/mimalloc&quot;&gt;c 语言版本的官方实现&lt;/a&gt;，仅对论文进行实现。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;简要描述&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;划分为堆（Heap）、段（Segment）、页（Page） 。&lt;/p&gt;
&lt;p&gt;堆由具体的用量决定，使用 &lt;code&gt;std::alloc::System::alloc&lt;/code&gt; 分配，由 Chaos HMM 决定是否要申请或是否给系统。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当内存将用满时向系统申请当前大小 x2 的内存用量&lt;/li&gt;
&lt;li&gt;当检测到已经长时间空闲了归还系统&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;段分为两种段：页段和页区域段（前者指段内存在很多页，后者一段即为一段分配出去的内存）&lt;/p&gt;
&lt;p&gt;页大小与对象大小有关：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于 8 K 以下的为小对象，页面大小为 64 K，一个段中有 64 个这样的页。&lt;/li&gt;
&lt;li&gt;对于 512 K 以下的大对象，页面大小为 512 K，一个段中有 8 个这样的页。&lt;/li&gt;
&lt;li&gt;对于大于 512 K 的超大对象，Chaos 将分配一整个段。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于前两种段来说，第一个页总是用于记录 free_list、local_list 和 thread_list 的，但在大对象页中，第一个页存在较多的浪费，也许可以使用 slab 再优化。&lt;/p&gt;
&lt;p&gt;而超大对象段则需要关注对齐问题，即使只使用了 513 K，也应该分配 4 M 段。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;数据结构&lt;/strong&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;错误的设计&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一开始，思路是建立一个栈上链表，并尽可能延长它的生命周期。但经群友的提醒，发现栈结构并不允许这样子，例如 Heap 生命周期始终会比 Segment 长，Segment 始终会比 Page 长，十分容易产生垂悬指针，并不能像堆内存那样长时间驻留内存。&lt;/p&gt;
&lt;p&gt;但栈上链表本身以下一个节点的内存地址为存储内容形成链表是可以实现的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;strong&gt;Rust 全局分配器&lt;/strong&gt;&lt;/h3&gt;</content:encoded></item><item><title>oscamp：字节分配器 Chaos</title><link>https://blog.hanbings.io/posts/oscamp-stage3-lab1-chaos-byte-allocator/</link><guid isPermaLink="true">https://blog.hanbings.io/posts/oscamp-stage3-lab1-chaos-byte-allocator/</guid><description>oscamp：字节分配器 Chaos</description><pubDate>Tue, 10 Dec 2024 14:28:48 GMT</pubDate><content:encoded> 
 源代码   
&lt;p&gt;oscamp/arceos/labs/lab_allocator/src/chaos.rs&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use core::{alloc::Layout, ptr::NonNull};

pub struct Chaos {
    pub vec_reserve: (usize, usize, usize),

    pub start: usize,
    pub end: usize,
    pub head: usize,
    pub tail: usize,
    pub is_even: bool,

    pub allocated: usize,
    pub total: usize,
}

impl Chaos {
    pub const fn new() -&amp;gt; Self {
        Chaos {
            vec_reserve: (0, 0, 0),
            start: 0,
            end: 0,
            head: 0,
            tail: 0,
            is_even: false,
            allocated: 0,
            total: 0,
        }
    }

    pub unsafe fn init(&amp;amp;mut self, start: usize, size: usize) {
        log::warn!(&quot;init_memory: start=0x{:x}, size=0x{:x}, end=0x{:x}&quot;, start, size, start + size);

        self.vec_reserve = (start, start + 96, start + 96 + 192);
        
        self.start = start + 96 + 192 + 384;
        self.head = start + 96 + 192 + 384;

        self.end = start + size;
        self.tail = start + size;

        self.allocated = 96 + 192 + 384;
        self.total = self.end - self.start;
    }

    pub fn add_to_heap(&amp;amp;mut self, start: usize, end: usize) {
        log::warn!(&quot;add_memory: start=0x{:x}, end=0x{:x}&quot;, start, end);

        self.end = end;
        self.tail = end;

        self.total += end - start;
    }

    pub fn alloc(&amp;amp;mut self, layout: Layout) -&amp;gt; Result&amp;lt;NonNull&amp;lt;u8&amp;gt;, ()&amp;gt; {
        let vec_reserve_ptr = match layout.size() {
            96 =&amp;gt; Some(self.vec_reserve.0 as *const u8),
            192 =&amp;gt; Some(self.vec_reserve.1 as *const u8),
            384 =&amp;gt; Some(self.vec_reserve.2 as *const u8),
            _ =&amp;gt; None,
        };

        if vec_reserve_ptr.is_some() &amp;amp;&amp;amp; layout.align() == 8 {
            return Ok(NonNull::new(vec_reserve_ptr.unwrap() as *mut u8).unwrap());
        }

        // get as much memory as possible
        if self.end &amp;lt;= 0xffffffc08426d000 {
            return Err(());
        }

        // check if memory is overflow
        if self.tail - layout.size() &amp;lt;= self.head {
            return Err(());
        }

        log::warn!(&quot;alloc_memory: head=0x{:x}, tail=0x{:x}, size=0x{:x}&quot;, self.head, self.tail, layout.size());

        let ptr = if self.is_even {
            let mem = self.tail - layout.size();
            self.tail = mem;

            NonNull::new(mem as *mut u8).unwrap()
        } else {
            let mem = self.head;
            self.head = mem + layout.size();

            NonNull::new(mem as *mut u8).unwrap()
        };

        log::warn!(&quot;alloc_memory: ptr=0x{:x}&quot;, ptr.as_ptr() as usize);

        self.is_even = !self.is_even;
        self.allocated += layout.size();

        Ok(ptr)
    }

    pub fn dealloc(&amp;amp;mut self, pos: NonNull&amp;lt;u8&amp;gt;, layout: Layout) {
        log::warn!(&quot;head, tail: 0x{:x}, 0x{:x}&quot;, self.head, self.tail);
        log::warn!(&quot;dealloc_memory: pos=0x{:x}, size=0x{:x}&quot;, pos.as_ptr() as usize, layout.size());

        if (pos.as_ptr() as usize) &amp;lt; self.start + 96 + 192 + 384 {
            return;
        }

        self.tail +=layout.size();
        self.allocated -= layout.size();

        log::warn!(&quot;before dealloc_memory: head=0x{:x}, tail=0x{:x}, start=0x{:x}, end=0x{:x}&quot;, self.head, self.tail, self.start, self.end);
    }

    pub fn total_bytes(&amp;amp;self) -&amp;gt; usize {
        0
    }

    pub fn used_bytes(&amp;amp;self) -&amp;gt; usize {
        self.allocated
    }

    pub fn available_bytes(&amp;amp;self) -&amp;gt; usize {
        self.total - self.allocated
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;oscamp/arceos/labs/lab_allocator/src/lib.rs&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//! Allocator algorithm in lab.
#![no_std]
#![allow(unused_variables)]

mod buddy;
mod linked_list;
mod chaos;

use allocator::{AllocError, AllocResult, BaseAllocator, ByteAllocator};
use chaos::Chaos;
use core::alloc::Layout;
use core::ptr::NonNull;

pub struct LabByteAllocator {
    inner: Chaos,
}

impl LabByteAllocator {
    pub const fn new() -&amp;gt; Self {
        Self {
            inner: Chaos::new(),
        }
    }
}

impl BaseAllocator for LabByteAllocator {
    fn init(&amp;amp;mut self, start: usize, size: usize) {
        unsafe { self.inner.init(start, size) };
    }

    fn add_memory(&amp;amp;mut self, start: usize, size: usize) -&amp;gt; AllocResult {
        self.inner.add_to_heap(start, start + size);
        Ok(())
    }
}

impl ByteAllocator for LabByteAllocator {
    fn alloc(&amp;amp;mut self, layout: Layout) -&amp;gt; AllocResult&amp;lt;NonNull&amp;lt;u8&amp;gt;&amp;gt; {
        self.inner.alloc(layout).map_err(|_| AllocError::NoMemory)
    }

    fn dealloc(&amp;amp;mut self, pos: NonNull&amp;lt;u8&amp;gt;, layout: Layout) {
        self.inner.dealloc(pos, layout)
    }

    fn total_bytes(&amp;amp;self) -&amp;gt; usize {
        self.inner.total_bytes()
    }

    fn used_bytes(&amp;amp;self) -&amp;gt; usize {
        self.inner.used_bytes()
    }

    fn available_bytes(&amp;amp;self) -&amp;gt; usize {
        self.inner.available_bytes()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;oscamp/arceos/labs/lab_allocator/src/linked_list.rs&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use core::marker::PhantomData;
use core::{fmt, ptr};

#[derive(Copy, Clone)]
pub struct LinkedList {
    head: *mut usize,
}

unsafe impl Send for LinkedList {}

impl LinkedList {
    pub const fn new() -&amp;gt; LinkedList {
        LinkedList {
            head: ptr::null_mut(),
        }
    }

    pub fn is_empty(&amp;amp;self) -&amp;gt; bool {
        self.head.is_null()
    }

    pub unsafe fn push(&amp;amp;mut self, item: *mut usize) {
        *item = self.head as usize;
        self.head = item;
    }

    pub fn pop(&amp;amp;mut self) -&amp;gt; Option&amp;lt;*mut usize&amp;gt; {
        match self.is_empty() {
            true =&amp;gt; None,
            false =&amp;gt; {
                let item = self.head;
                self.head = unsafe { *item as *mut usize };
                Some(item)
            }
        }
    }

    pub fn iter(&amp;amp;self) -&amp;gt; Iter {
        Iter {
            curr: self.head,
            list: PhantomData,
        }
    }

    pub fn iter_mut(&amp;amp;mut self) -&amp;gt; IterMut {
        IterMut {
            prev: &amp;amp;mut self.head as *mut *mut usize as *mut usize,
            curr: self.head,
            list: PhantomData,
        }
    }
}

impl fmt::Debug for LinkedList {
    fn fmt(&amp;amp;self, f: &amp;amp;mut fmt::Formatter) -&amp;gt; fmt::Result {
        f.debug_list().entries(self.iter()).finish()
    }
}

pub struct Iter&amp;lt;&apos;a&amp;gt; {
    curr: *mut usize,
    list: PhantomData&amp;lt;&amp;amp;&apos;a LinkedList&amp;gt;,
}

impl&amp;lt;&apos;a&amp;gt; Iterator for Iter&amp;lt;&apos;a&amp;gt; {
    type Item = *mut usize;

    fn next(&amp;amp;mut self) -&amp;gt; Option&amp;lt;Self::Item&amp;gt; {
        if self.curr.is_null() {
            None
        } else {
            let item = self.curr;
            let next = unsafe { *item as *mut usize };
            self.curr = next;
            Some(item)
        }
    }
}

pub struct ListNode {
    prev: *mut usize,
    curr: *mut usize,
}

impl ListNode {
    pub fn pop(self) -&amp;gt; *mut usize {
        unsafe {
            *(self.prev) = *(self.curr);
        }
        self.curr
    }

    pub fn value(&amp;amp;self) -&amp;gt; *mut usize {
        self.curr
    }
}

pub struct IterMut&amp;lt;&apos;a&amp;gt; {
    list: PhantomData&amp;lt;&amp;amp;&apos;a mut LinkedList&amp;gt;,
    prev: *mut usize,
    curr: *mut usize,
}

impl&amp;lt;&apos;a&amp;gt; Iterator for IterMut&amp;lt;&apos;a&amp;gt; {
    type Item = ListNode;

    fn next(&amp;amp;mut self) -&amp;gt; Option&amp;lt;Self::Item&amp;gt; {
        if self.curr.is_null() {
            None
        } else {
            let res = ListNode {
                prev: self.prev,
                curr: self.curr,
            };
            self.prev = self.curr;
            self.curr = unsafe { *self.curr as *mut usize };
            Some(res)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
 
&lt;h2&gt;分析内存结构&lt;/h2&gt;
&lt;h3&gt;堆中的 &lt;code&gt;Vec&amp;lt;Vec&amp;lt;u8&amp;gt;&amp;gt;&lt;/code&gt; 结构数据&lt;/h3&gt;
&lt;p&gt;使用 Slab 分配器，并在 alloc 和 dealloc 函数，中分别打印 Layout。在打印结果中可以看到一些比较特殊的内存大小分别为 96、192、384&lt;/p&gt;
&lt;p&gt;这是什么呢？根据 &lt;code&gt;lab1/src/main.rs&lt;/code&gt; 中的测例代码分析可得知 &lt;code&gt;let mut items: Vec&amp;lt;Vec&amp;lt;u8&amp;gt;&amp;gt; = Vec::new();&lt;/code&gt; 的内层 &lt;code&gt;Vec&lt;/code&gt; 将存放在堆内存中。&lt;/p&gt;
&lt;p&gt;而 &lt;a href=&quot;https://rcore-os.cn/rCore-Tutorial-Book-v3/chapter4/1rust-dynamic-allocation.html?highlight=vec#rust-heap-data-structures&quot;&gt;Rust 中的堆数据结构&lt;/a&gt; 中的一张图片展示了 Vec 的内存结构为指针、长度和容量，每一部分各占 usize，在 64 位系统中，uszie 大小为 8 字节，即一个空数组内存占用为 24 字节，这与 96 字节仍有出入。&lt;/p&gt;
&lt;p&gt;再观察分配情况，在 &lt;code&gt;Indicator: 0&lt;/code&gt; 时，96 字节的 alloc 是在第一次 32 字节 alloc 后产生的，猜测是在 &lt;code&gt;items.push&lt;/code&gt; 后进行了一次 &lt;a href=&quot;https://doc.rust-lang.org/std/vec/struct.Vec.html#capacity-and-reallocation&quot;&gt;扩容&lt;/a&gt;，&lt;code&gt;alloc_pass&lt;/code&gt; 修改为如下代码进行验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn alloc_pass(delta: usize) -&amp;gt; Vec&amp;lt;Vec&amp;lt;u8&amp;gt;&amp;gt; {
    let mut items = Vec::new();
    let mut base = 32;
    loop {
        let c = (delta % 256) as u8;
        let a = vec![c; base+delta];
        items.push(a);
        println!(&quot;Item Capacity: {}&quot;, items.capacity());
        if base &amp;gt;= 512*1024 {
            break;
        }
        base *= 2;
    }
    items
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Running bumb tests...
Indicator: 0
Item Capacity: 4
Item Capacity: 4
Item Capacity: 4
Item Capacity: 4
Item Capacity: 8
Item Capacity: 8
Item Capacity: 8
Item Capacity: 8
Item Capacity: 16
Item Capacity: 16
Item Capacity: 16
Item Capacity: 16
Item Capacity: 16
Item Capacity: 16
Item Capacity: 16

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就对上了，&lt;code&gt;24 * 4 = 96&lt;/code&gt; &lt;code&gt;24 * 8 = 192&lt;/code&gt; &lt;code&gt;24 * 16 = 384&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;同时，至少在这里来说，&lt;code&gt;Vec&lt;/code&gt; 初始容量为 0，第一次扩容时为 4，扩容机制为每次容量翻倍。&lt;/p&gt;
&lt;h3&gt;存放数据的堆空间&lt;/h3&gt;
&lt;p&gt;再回到测例代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn free_pass(items: &amp;amp;mut Vec&amp;lt;Vec&amp;lt;u8&amp;gt;&amp;gt;, delta: u8) {
    let total = items.len();
    for j in (0..total).rev() {
        if j % 2 == 0 {
            let ret = items.remove(j);
            assert_eq!(delta, ret[0]);
            assert_eq!(delta, ret[ret.len()-1]);
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码在检查是否为偶数项，偶数项数组才会被 &lt;code&gt;free_pass&lt;/code&gt; 的 &lt;code&gt;remove()&lt;/code&gt; 释放。&lt;/p&gt;
&lt;p&gt;因此针对这段代码，可以在分配器中设置一个奇偶标志位，当标志位为奇数时将数据从内存的起始位置开始分配，偶数时分配在末尾位置且向起始位置增长。扩容时，不考虑内存扩容时传入的内存地址起始位置而只改变结尾位置时也无需多余操作。&lt;/p&gt;
&lt;h2&gt;Chaos 真的不 Chaos&lt;/h2&gt;
&lt;h3&gt;整体结构&lt;/h3&gt;
&lt;p&gt;考虑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;堆中的 &lt;code&gt;Vec&amp;lt;Vec&amp;lt;u8&amp;gt;&amp;gt;&lt;/code&gt; 数据结构，大小固定为 &lt;code&gt;96 + 192 + 384 = 672&lt;/code&gt; 字节，位于可分配内存的起始位置&lt;/li&gt;
&lt;li&gt;奇数项堆，动态增长，紧跟 672 字节&lt;/li&gt;
&lt;li&gt;偶数项堆，动态增长，位于可分配内存的末尾位置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用以下结构体来描述整个可分配内存区域&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct Chaos {
    pub vec_reserve: (usize, usize, usize),

    pub start: usize,
    pub end: usize,
    pub head: usize,
    pub tail: usize,
    pub is_even: bool,

    pub allocated: usize,
    pub total: usize,
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内存结构如下：&lt;/p&gt;
&lt;p&gt;!&lt;a href=&quot;https://ice.frostsky.com/2024/12/04/31665acefb8e6fbfa3315cc85b607994.png&quot;&gt;https://ice.frostsky.com/2024/12/04/31665acefb8e6fbfa3315cc85b607994.png&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vec_reserve: (usize, usize, usize)&lt;/code&gt; 用作存储先前描述的三片特殊的堆中 Vec 元数据结构，&lt;code&gt;vec_reserve&lt;/code&gt; 为固定长度：&lt;code&gt;96 + 192 + 384&lt;/code&gt; 字节。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pub start: usize&lt;/code&gt;  和 &lt;code&gt;pub end: usize&lt;/code&gt; 分别存储 init 或是 add_to_heap 时给定的可分配内存地址范围。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pub head: usize&lt;/code&gt; 和 &lt;code&gt;pub tail: usize&lt;/code&gt; 分别存储当前奇数堆的末尾位置和偶数堆的起始位置，通过 &lt;code&gt;head - start + vec_reserve&lt;/code&gt; 可以得出奇数堆大小，通过 &lt;code&gt;end - tail&lt;/code&gt; 则可以得出偶数堆大小。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pub is_even: bool&lt;/code&gt; 用于记录上一次分配是奇数还是偶数，在分配的最后将其翻转。&lt;/p&gt;
&lt;h3&gt;实现&lt;/h3&gt;
&lt;p&gt;在开始之前，还要确定一个很重要的问题。目前的设计，并没有考虑扩容时分配在本程序堆内存前内存区域的地址，因此使用代码简单判断下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn add_to_heap(&amp;amp;mut self, start: usize, end: usize) {
    log::warn!(&quot;head: 0x{:x}, tail: 0x{:x}, start: 0x{:x}, end: 0x{:x}&quot;, self.head, self.tail, start, end);

    self.head = start;
    self.tail = end;

    panic!(&quot;add_to_heap&quot;);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从 &lt;code&gt;add_to_heap&lt;/code&gt; 的输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Running bumb tests...
Indicator: 0
[  0.082919 0 lab_allocator::chaos:66] head, tail: 0xffffffc08026d2a0, 0xffffffc080275000
[  0.087323 0 lab_allocator::chaos:44] head: 0xffffffc08026d2a0, tail: 0xffffffc080275000, start: 0xffffffc080275000, end: 0xffffffc08027d000
[  0.091091 0 axruntime::lang_items:5] panicked at labs/lab_allocator/src/chaos.rs:49:9:
add_to_heap

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到后续追加的内存与当前程序的堆尾部是连续的，只需要更改 &lt;code&gt;tail&lt;/code&gt; 字段扩容当前内存即可。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;初始化以及扩容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub unsafe fn init(&amp;amp;mut self, start: usize, size: usize) {
        self.vec_reserve = (start, start + 96, start + 96 + 192);

        self.start = start + 96 + 192 + 384;
        self.head = start + 96 + 192 + 384;

        self.end = start + size;
        self.tail = start + size;

        self.allocated = 96 + 192 + 384;
        self.total = self.end - self.start;
    }

    pub fn add_to_heap(&amp;amp;mut self, start: usize, end: usize) {
        self.end = end;
        self.tail = end;

        self.total += end - start;
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分配&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn alloc(&amp;amp;mut self, layout: Layout) -&amp;gt; Result&amp;lt;NonNull&amp;lt;u8&amp;gt;, ()&amp;gt; {
    let vec_reserve_ptr = match layout.size() {
        96 =&amp;gt; Some(self.vec_reserve.0 as *const u8),
        192 =&amp;gt; Some(self.vec_reserve.1 as *const u8),
        384 =&amp;gt; Some(self.vec_reserve.2 as *const u8),
        _ =&amp;gt; None,
    };

    if vec_reserve_ptr.is_some() {
        return Ok(NonNull::new(vec_reserve_ptr.unwrap() as *mut u8).unwrap());
    }

    // check if memory is overflow
    if self.tail - layout.size() &amp;lt;= self.head {
        return Err(());
    }

    let ptr = if self.is_even {
        let mem = self.tail - layout.size();
        self.tail = mem;

        NonNull::new(mem as *mut u8).unwrap()
    } else {
        let mem = self.head;
        self.head = mem + layout.size();

        NonNull::new(mem as *mut u8).unwrap()
    };

    self.is_even = !self.is_even;
    self.allocated += layout.size();

    Ok(ptr)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;释放&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn dealloc(&amp;amp;mut self, pos: NonNull&amp;lt;u8&amp;gt;, layout: Layout) {
    if (pos.as_ptr() as usize) &amp;lt; self.start + 96 + 192 + 384 {
        return;
    }

    self.tail +=layout.size();
    self.allocated -= layout.size();
}

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;越界访问&lt;/h3&gt;
&lt;p&gt;第 32 轮分配时发生了 &lt;code&gt;Unhandled Supervisor Page Fault&lt;/code&gt;，本机的 &lt;code&gt;panic&lt;/code&gt; 位置为 &lt;code&gt;0xffffffc0802005fc&lt;/code&gt;，远在可分配的起始地址 &lt;code&gt;0xffffffc08026d2a0&lt;/code&gt; 前，猜测本次的 &lt;code&gt;panic&lt;/code&gt; 发生在栈区或是发生在从 tail 进行分配或释放时发生的。&lt;/p&gt;
&lt;p&gt;说起来很好笑，&lt;code&gt;let mut pool = Vec::new();&lt;/code&gt; 和 items 一样，也会占用一些堆区内存，但因为其只写不读，即使内部全部都是无效的堆内存地址也没有问题。但这提醒到了一点：在分配用于 &lt;code&gt;items: Vec&amp;lt;Vec&amp;lt;u8&amp;gt;&amp;gt;&lt;/code&gt; 的堆内存时，使用了一个特判：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let vec_reserve_ptr = match layout.size() {
    96 =&amp;gt; Some(self.vec_reserve.0 as *const u8),
    192 =&amp;gt; Some(self.vec_reserve.1 as *const u8),
    384 =&amp;gt; Some(self.vec_reserve.2 as *const u8),
    _ =&amp;gt; None,
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这几个数字并没有问题，但先前我们忽略了 &lt;code&gt;let a = vec![c; base+delta];&lt;/code&gt; 的 &lt;code&gt;base&lt;/code&gt; 总是从 32 开始，每次 &lt;code&gt;alloc&lt;/code&gt; 翻倍，&lt;code&gt;delta&lt;/code&gt; 则每轮增加 &lt;code&gt;1&lt;/code&gt;。恰好， 第 32 轮的第二次 &lt;code&gt;alloc&lt;/code&gt; 时那么不恰好的 &lt;code&gt;base = 64&lt;/code&gt; 和 &lt;code&gt;delta = 32&lt;/code&gt;，这不是 &lt;code&gt;96&lt;/code&gt; 了嘛 uwu，所以到这时候，原本该分配给 &lt;code&gt;items: Vec&amp;lt;Vec&amp;lt;u8&amp;gt;&amp;gt;&lt;/code&gt;  的地址被当作了 tail 堆的起始地址，又因为 &lt;code&gt;tail&lt;/code&gt; 是 &lt;code&gt;tail = tail - size&lt;/code&gt; 取得空闲地址，所以结果是越界了。&lt;/p&gt;
&lt;p&gt;根据观察后，在 &lt;code&gt;Layout&lt;/code&gt; 中存在字段 &lt;code&gt;align&lt;/code&gt;，&lt;code&gt;align&lt;/code&gt; 字段在 &lt;code&gt;items: Vec&amp;lt;Vec&amp;lt;u8&amp;gt;&amp;gt;&lt;/code&gt; 分配中总是 &lt;code&gt;8&lt;/code&gt;，而普通分配总是 &lt;code&gt;1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;因此简单写个判断就可以解决了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if vec_reserve_ptr.is_some() &amp;amp;&amp;amp; layout.align() == 8 {
    return Ok(NonNull::new(vec_reserve_ptr.unwrap() as *mut u8).unwrap());
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;处理完这个问题后，算法成功跑到 152 轮。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Indicator: 152
[ 25.511024 0 lab_allocator::chaos:71] alloc_memory: head=0xffffffc084f352a8, tail=0xffffffc085015be2, size=0xb8
[ 25.513985 0 lab_allocator::chaos:85] alloc_memory: ptr=0xffffffc085015b2a
[ 25.516487 0 lab_allocator::chaos:71] alloc_memory: head=0xffffffc084f352a8, tail=0xffffffc085015b2a, size=0xd8
[ 25.519232 0 lab_allocator::chaos:85] alloc_memory: ptr=0xffffffc084f352a8
[ 25.521127 0 lab_allocator::chaos:71] alloc_memory: head=0xffffffc084f35380, tail=0xffffffc085015b2a, size=0x118
[ 25.523938 0 lab_allocator::chaos:85] alloc_memory: ptr=0xffffffc085015a12
[ 25.525770 0 lab_allocator::chaos:71] alloc_memory: head=0xffffffc084f35380, tail=0xffffffc085015a12, size=0x198
[ 25.528492 0 lab_allocator::chaos:85] alloc_memory: ptr=0xffffffc084f35380
[ 25.530458 0 lab_allocator::chaos:71] alloc_memory: head=0xffffffc084f35518, tail=0xffffffc085015a12, size=0x298
[ 25.533267 0 lab_allocator::chaos:85] alloc_memory: ptr=0xffffffc08501577a
[ 25.535172 0 lab_allocator::chaos:94] head, tail: 0xffffffc084f35518, 0xffffffc08501577a
[ 25.537270 0 lab_allocator::chaos:95] dealloc_memory: pos=0xffffffc08026f000, size=0x60
[ 25.540115 0 lab_allocator::chaos:71] alloc_memory: head=0xffffffc084f35518, tail=0xffffffc08501577a, size=0x498
[ 25.542925 0 lab_allocator::chaos:85] alloc_memory: ptr=0xffffffc084f35518
[ 25.544869 0 lab_allocator::chaos:71] alloc_memory: head=0xffffffc084f359b0, tail=0xffffffc08501577a, size=0x898
[ 25.547528 0 lab_allocator::chaos:85] alloc_memory: ptr=0xffffffc085014ee2
[ 25.549466 0 lab_allocator::chaos:71] alloc_memory: head=0xffffffc084f359b0, tail=0xffffffc085014ee2, size=0x1098
[ 25.552112 0 lab_allocator::chaos:85] alloc_memory: ptr=0xffffffc084f359b0
[ 25.554013 0 lab_allocator::chaos:71] alloc_memory: head=0xffffffc084f36a48, tail=0xffffffc085014ee2, size=0x2098
[ 25.558305 0 lab_allocator::chaos:85] alloc_memory: ptr=0xffffffc085012e4a
[ 25.560458 0 lab_allocator::chaos:94] head, tail: 0xffffffc084f36a48, 0xffffffc085012e4a
[ 25.562665 0 lab_allocator::chaos:95] dealloc_memory: pos=0xffffffc08026f060, size=0xc0
[ 25.564900 0 lab_allocator::chaos:71] alloc_memory: head=0xffffffc084f36a48, tail=0xffffffc085012e4a, size=0x4098
[ 25.567790 0 lab_allocator::chaos:85] alloc_memory: ptr=0xffffffc084f36a48
[ 25.569725 0 lab_allocator::chaos:71] alloc_memory: head=0xffffffc084f3aae0, tail=0xffffffc085012e4a, size=0x8098
[ 25.572420 0 lab_allocator::chaos:85] alloc_memory: ptr=0xffffffc08500adb2
[ 25.575027 0 lab_allocator::chaos:71] alloc_memory: head=0xffffffc084f3aae0, tail=0xffffffc08500adb2, size=0x10098
[ 25.577664 0 lab_allocator::chaos:85] alloc_memory: ptr=0xffffffc084f3aae0
[ 25.579570 0 lab_allocator::chaos:71] alloc_memory: head=0xffffffc084f4ab78, tail=0xffffffc08500adb2, size=0x20098
[ 25.582385 0 lab_allocator::chaos:85] alloc_memory: ptr=0xffffffc084fead1a
[ 25.584478 0 lab_allocator::chaos:71] alloc_memory: head=0xffffffc084f4ab78, tail=0xffffffc084fead1a, size=0x40098
[ 25.587429 0 lab_allocator::chaos:85] alloc_memory: ptr=0xffffffc084f4ab78
[ 25.591690 0 axruntime::lang_items:5] panicked at modules/axalloc/src/lib.rs:124:31:
Bumb: NoMemory.

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;预分配：尽可能使用更多的内存&lt;/h3&gt;
&lt;p&gt;在观察 152 轮和 &lt;code&gt;add_memory&lt;/code&gt; 的 &lt;code&gt;log&lt;/code&gt; 时发现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;本轮的偶数区并没有得到释放。&lt;/li&gt;
&lt;li&gt;内存从一开始分配起，每次扩容即为前一次空间 &lt;code&gt;x2&lt;/code&gt;，最大为 &lt;code&gt;32 MB&lt;/code&gt;。而 qemu 虚拟机分配的是 &lt;code&gt;128MB&lt;/code&gt;，只分配到了 &lt;code&gt;32MB&lt;/code&gt;，显然还不是极限。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;pub fn alloc(&amp;amp;mut self, layout: Layout) -&amp;gt; Result&amp;lt;NonNull&amp;lt;u8&amp;gt;, ()&amp;gt; { Err(()) }

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Indicator: 0
[  0.088199 0 lab_allocator::chaos:46] add_memory: start=0xffffffc080275000, end=0xffffffc08027d000 # 32768
[  0.090785 0 lab_allocator::chaos:46] add_memory: start=0xffffffc08027d000, end=0xffffffc08028d000 # 65536
[  0.093210 0 lab_allocator::chaos:46] add_memory: start=0xffffffc08028d000, end=0xffffffc0802ad000 # 131072
[  0.095720 0 lab_allocator::chaos:46] add_memory: start=0xffffffc0802ad000, end=0xffffffc0802ed000 # 262144
[  0.098368 0 lab_allocator::chaos:46] add_memory: start=0xffffffc0802ed000, end=0xffffffc08036d000 # 524288
[  0.100928 0 lab_allocator::chaos:46] add_memory: start=0xffffffc08036d000, end=0xffffffc08046d000 # 1048576
[  0.103497 0 lab_allocator::chaos:46] add_memory: start=0xffffffc08046d000, end=0xffffffc08066d000 # 2097152
[  0.106127 0 lab_allocator::chaos:46] add_memory: start=0xffffffc08066d000, end=0xffffffc080a6d000 # 4194304
[  0.108677 0 lab_allocator::chaos:46] add_memory: start=0xffffffc080a6d000, end=0xffffffc08126d000 # 8388608
[  0.111219 0 lab_allocator::chaos:46] add_memory: start=0xffffffc08126d000, end=0xffffffc08226d000 # 16777216
[  0.114057 0 lab_allocator::chaos:46] add_memory: start=0xffffffc08226d000, end=0xffffffc08426d000 # 33554432 byte = 32MB
[  0.117634 0 axruntime::lang_items:5] panicked at modules/axalloc/src/lib.rs:124:31:
Bumb: NoMemory.

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，我们可以打第一次 alloc 开始就强迫系统预分配一大堆内存给测例。比如在 &lt;code&gt;pub fn alloc(&amp;amp;mut self, layout: Layout) -&amp;gt; Result&amp;lt;NonNull&amp;lt;u8&amp;gt;, ()&amp;gt;&lt;/code&gt;  的合理位置添加下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// get as much memory as possible
if self.end &amp;lt;= 0xffffffc08426d000 {
    return Err(());
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后，轮数为 189。此时，内存分配量到达了 &lt;code&gt;64MB&lt;/code&gt;，这样下来仍有很多剩余空间。&lt;/p&gt;
&lt;h3&gt;再挤一挤，内存还是有的&lt;/h3&gt;
&lt;p&gt;看一眼 &lt;code&gt;axalloc&lt;/code&gt; 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let old_size = balloc.total_bytes();
let expand_size = old_size
    .max(layout.size())
    .next_power_of_two()
    .max(PAGE_SIZE);
let heap_ptr = match self.alloc_pages(expand_size / PAGE_SIZE, PAGE_SIZE) {
    Ok(ptr) =&amp;gt; ptr,
    Err(e) =&amp;gt; panic!(&quot;Bumb: {:?}.&quot;, e),
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不难发现 &lt;code&gt;axalloc&lt;/code&gt; 是依赖于 &lt;code&gt;total_bytes()&lt;/code&gt; 来判断该扩容多少内存。&lt;/p&gt;
&lt;p&gt;简单一些处理，只需要修改原先的 &lt;code&gt;total_bytes&lt;/code&gt; 代码，使得 &lt;code&gt;total_bytes&lt;/code&gt; 永远返回 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn total_bytes(&amp;amp;self) -&amp;gt; usize {
    0
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后的最终轮数为 245。&lt;/p&gt;</content:encoded></item><item><title>Rust：实现插件系统</title><link>https://blog.hanbings.io/posts/rust-ffi-dynlib-plugin/</link><guid isPermaLink="true">https://blog.hanbings.io/posts/rust-ffi-dynlib-plugin/</guid><description>FFI、C ABI 和 dynlib 的 Rust 插件系统</description><pubDate>Sat, 09 Nov 2024 17:50:11 GMT</pubDate><content:encoded>&lt;h2&gt;目标&lt;/h2&gt;
&lt;p&gt;实现插件系统，通常有两种方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;语言自身支持某种加载方式允许动态加载外部代码。比如加载**动态链接库，**又或是像 Java 这类语言的虚拟机允许加载中间文件甚至是源代码。&lt;/li&gt;
&lt;li&gt;通过语言引擎运行外部代码，例如 Java 中使用 ScriptEngine 和Rust 中使用 &lt;a href=&quot;https://github.com/boa-dev/boa&quot;&gt;https://github.com/boa-dev/boa&lt;/a&gt; 执行 JavaScript。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此外，还可以通过向外暴露 HTTP / WebSocket 等接口实现插件接口，也是一种可行的插件系统设计。&lt;/p&gt;
&lt;p&gt;但无论是通过语言引擎运行外部代码还是暴露 HTTP / WebSocket 接口，前者会带来数据交换的损耗而后者不仅带来数据交换所耗费的空间还会带来网络传输的延迟。这样的性能损耗对于这个插件系统的应用场景：分布式计算设施 — 大部分情况下是不可接受的，因此我们选取以动态链接库作为插件的形态，并通过 FFI（Foreign Function Interface） 向插件暴露接口。&lt;/p&gt;
&lt;p&gt;本插件系统需要实现的技术目标：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;以动态链接库作为插件的形态，即需要一个加载器允许在运行时加载插件。&lt;/li&gt;
&lt;li&gt;允许插件向加载器提供元信息（允许加载器调用插件内的函数）&lt;/li&gt;
&lt;li&gt;允许插件执行加载器提供的 API （允许插件调用加载器内的函数）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;加载器侧&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;这里我们使用到了 &lt;a href=&quot;https://github.com/OpenByteDev/dlopen2&quot;&gt;https://github.com/OpenByteDev/dlopen2&lt;/a&gt; 作为加载动态链接库的加载器，所列代码中依赖版本为 &lt;code&gt;0.7.0&lt;/code&gt;（&lt;code&gt;dlopen2 = &quot;0.7.0&quot;&lt;/code&gt;），其主要原因是 dlopen2 允许将被加载的动态链接库自动映射到一个结构体中，免去了再手动使用函数名称查找函数的步骤。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;创建一个 &lt;code&gt;src/main.rs&lt;/code&gt; 的新文件，然后定义一个主函数 &lt;code&gt;fn main() {}&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;设计一个函数 &lt;code&gt;simple_add_two_numbers&lt;/code&gt;，它的任务是将两个参数 &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 相加，并在后面的插件侧代码中输出，但是需要声明在一个看起来有点奇怪的结构体中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[repr(C)]
#[derive(WrapperApi)]
struct Adder {
    simple_add_two_numbers: unsafe extern &quot;C&quot; fn(
        a: c_int,
        b: c_int,
    ),
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意！其中的  &lt;code&gt;#[repr(C)]&lt;/code&gt; 和 &lt;code&gt;unsafe extern &quot;C&quot;&lt;/code&gt; ，前者是用于声明结构体按 C 语言的结构体的内存结构进行布局（是的，Rust 的结构体内存布局与 C 语言并不一样，在黑魔书中有提及：&lt;a href=&quot;https://doc.rust-lang.org/nomicon/other-reprs.html&quot;&gt;Rust Docs - nomicon - other-reprs&lt;/a&gt;，而 &lt;code&gt;extern&lt;/code&gt; 是 Rust 语言专为 FFI 接口设计的向外暴露函数的关键字，&lt;code&gt;extern &quot;C”&lt;/code&gt; 意为按照 C 语言的 ABI （Application Binary Interface 定义了如何在汇编语言层面调用此函数）接口标准向外暴露函数。它们都是必要的写法。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;关于结构体的内存布局&lt;/h3&gt;
&lt;p&gt;Rust 一些 unsafe 操作允许操作内存，例如 &lt;code&gt;std::mem::transmute&lt;/code&gt;，可以将结构体 A 替换为结构体 B。很好玩的是，Rust 提供了结构体 A、B 的长度检查，只有两个结构体长度一致才可以通过编译，而不检查类型是否一致。也就是说：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 是能够成功转换的
struct A {
    e: u16,
    a: u8,
    b: u8
}

struct B {
    e: u8,
    a: u8,
    b: u8,
    c: u8,
}

// 不能成功转换
struct A {
    e: u16,
    a: u8,
    b: u8
}

struct B {
    e: u16,
    a: u8,
    b: u8,
    c: u8,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct A {
    e: u16,
    a: u8,
    b: u8
}

struct B {
    e: bool,
    a: bool,
    b: bool,
    c: u8,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也是可以转换成功的，因为一个 &lt;code&gt;bool&lt;/code&gt; 值会占用 &lt;code&gt;u8&lt;/code&gt; 长度的内存空间。本文想要提醒的是，不同的 C 语言编译器也许会将多个 &lt;code&gt;boolean&lt;/code&gt; 压缩到一个 8 位或是说结构体对齐长度中来优化内存。&lt;/p&gt;
&lt;h3&gt;关于 &lt;code&gt;c_int&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.rust-lang.org/std/ffi/type.c_int.html&quot;&gt;Rust Docs - std/ffi/type.c_int&lt;/a&gt; 文档中，可以看到解释为与 C 语言中（未提及具体标准或版本）的 &lt;code&gt;signed int&lt;/code&gt;（也就是 int） 相等。实际上它只是一个类型别名 &lt;code&gt;pub type c_int = i32;&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.rust-lang.org/std/ffi/index.html#types&quot;&gt;Rust Docs - std/ffi/types&lt;/a&gt; 可以看到更多用于在 Rust 中绑定到 C 语言（的类型）的类型别名，应该在编写 FFI 代码时候尽可能使用它们来避免不经意间使用了那些不支持的类型。&lt;/p&gt;
&lt;p&gt;同时 &lt;a href=&quot;https://doc.rust-lang.org/std/ffi/index.html#structs&quot;&gt;Rust Docs - std/ffi/structs&lt;/a&gt; 还定义了一些与字符串相关的工具，使用它们可以避免不受非法字符串的攻击。比如使用 &lt;a href=&quot;https://doc.rust-lang.org/std/ffi/struct.OsString.html&quot;&gt;Rust Docs - std/ffi/structs.OsString&lt;/a&gt; 替代 &lt;code&gt;String&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;接着使用 dlopen2 进行动态链接库的加载：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let cont: Container&amp;lt;Adder&amp;gt; =
        unsafe { Container::load(&quot;./target/debug/luminous.dll&quot;) }
            .expect(&quot;Could not open library or load symbols&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;Container::load(&quot;&quot;)&lt;/code&gt; 将指定加载哪一个动态链接库文件，在 Windows 下后缀名为 &lt;code&gt;dll&lt;/code&gt;，macOS 为 &lt;code&gt;dylib&lt;/code&gt; 以及 Linux 为 &lt;code&gt;so&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;别忘了，需要声明一个泛型参数并传入定义的结构体 &lt;code&gt;let cont: Container&amp;lt;Adder&amp;gt; = …&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;好啦！然后调用它！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let a = 1;
let b = 2;

unsafe {
    cont.simple_add_two_numbers(a, b);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等等，还没有在插件侧编写这个函数的实现呢！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://picture.hanbings.io/2024/11/09/f266493fda738ea504599bf156ef1fa1.jpeg&quot; alt=&quot;f266493fda738ea504599bf156ef1fa1.jpg&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;插件侧&lt;/h2&gt;
&lt;p&gt;好的，创建 &lt;code&gt;src/lib.rs&lt;/code&gt; 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::os::raw::c_int;

#[no_mangle]
pub unsafe extern &quot;C&quot; fn simple_add_two_numbers(
    a: c_int,
    b: c_int
) {
    println!(&quot;{} + {} = {}&quot;, a, b, a + b)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并在 cargo.toml 写上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[lib]
crate-type = [&quot;cdylib&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用 &lt;code&gt;cargo build&lt;/code&gt; 即可生成动态链接库。&lt;/p&gt;
&lt;p&gt;编译，然后运行它吧！&lt;code&gt;cargo run&lt;/code&gt;！&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;关于 &lt;code&gt;#[no_mangle]&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;这里的 &lt;code&gt;#[no_mangle]&lt;/code&gt; 用于来告诉 Rust 编译器不要混淆此函数的名称。&lt;/p&gt;
&lt;p&gt;混淆是指 Rust 编译器会将函数更改为别的名称，这个名称包含更多信息，可以给编译过程的其他部分使用，但可读性较差。每种编程语言编译器对名称的混淆都略有不同，因此，为了让 Rust 函数可以被其他语言命名，必须禁用 Rust 编译器的名称混淆。&lt;/p&gt;
&lt;p&gt;文档：&lt;a href=&quot;https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#calling-rust-functions-from-other-languages&quot;&gt;https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#calling-rust-functions-from-other-languages&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;接着我们需要添加一个函数回调（Callback），其实就是加载器向插件传递函数的内存地址（也就是指针）的过程。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;src/main.rs&lt;/code&gt; 中添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub type AddCallback = unsafe extern &quot;C&quot; fn(c_int);

pub unsafe extern &quot;C&quot; fn on_two_numbers_added(result: c_int) {
    println!(&quot;Got {}!&quot;, result);
}
#[repr(C)]
#[derive(Debug, Copy, Clone, WrapperApi)]
struct Adder {
    simple_add_two_numbers: unsafe extern &quot;C&quot; fn(
        a: c_int,
        b: c_int,
        callback: AddCallback
    ),
}
unsafe {
    cont.simple_add_two_numbers(a, b, on_two_numbers_added);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 &lt;code&gt;src/lib.rs&lt;/code&gt; 也做出相应修改：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[no_mangle]
pub unsafe extern &quot;C&quot; fn simple_add_two_numbers(
    a: c_int,
    b: c_int,
    callback: AddCallback
) {
    println!(&quot;{} + {} = {}&quot;, a, b, a + b);
    callback(a + b);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://picture.hanbings.io/2024/11/09/ff37f8648bdfd032eb4c3e6583b9fb79.jpeg&quot; alt=&quot;ff37f8648bdfd032eb4c3e6583b9fb79.jpg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;至此，终于完成三个技术目标啦！&lt;/p&gt;
&lt;h2&gt;完整代码：&lt;/h2&gt;
&lt;p&gt;请留意一下有关于命名的位置，比如加载侧的动态链接库名称和 &lt;code&gt;cargo.toml&lt;/code&gt; 的包名。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;src/main.rs&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::os::raw::c_int;
use dlopen2::wrapper::{Container, WrapperApi};

pub type AddCallback = unsafe extern &quot;C&quot; fn(c_int);

#[repr(C)]
#[derive(Debug, Copy, Clone, WrapperApi)]
struct Adder {
    simple_add_two_numbers: unsafe extern &quot;C&quot; fn(
        a: c_int,
        b: c_int,
        callback: AddCallback
    ),
}

pub unsafe extern &quot;C&quot; fn on_two_numbers_added(result: c_int) {
    println!(&quot;Got {}!&quot;, result);
}

fn main() {
    let a = 1;
    let b = 2;

    let cont: Container&amp;lt;Adder&amp;gt; =
        unsafe { Container::load(&quot;./target/debug/luminous.dll&quot;) }
            .expect(&quot;Could not open library or load symbols&quot;);

    unsafe {
        cont.simple_add_two_numbers(a, b, on_two_numbers_added);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;lib.rs&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::os::raw::c_int;

pub type AddCallback = unsafe extern &quot;C&quot; fn(c_int);

#[no_mangle]
pub unsafe extern &quot;C&quot; fn simple_add_two_numbers(
    a: c_int,
    b: c_int,
    callback: AddCallback
) {
    println!(&quot;{} + {} = {}&quot;, a, b, a + b);
    callback(a + b);
}
cargo.toml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cargo.toml&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[package]
name = &quot;luminous&quot;
version = &quot;0.1.0&quot;
edition = &quot;2021&quot;

[[bin]]
name = &quot;luminous&quot;
path = &quot;src/main.rs&quot;

[lib]
crate-type = [&quot;cdylib&quot;]

[dependencies]
dlopen2 = &quot;0.7.0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.rust-lang.org/nomicon/ffi.html&quot;&gt;Foreign Function Interface&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://adventures.michaelfbryan.com/posts/rust-closures-in-ffi/&quot;&gt;Rust Closures in FFI&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Effective Rust - 类型 - 速查表（停止更新）</title><link>https://blog.hanbings.io/posts/effective-rust/</link><guid isPermaLink="true">https://blog.hanbings.io/posts/effective-rust/</guid><description>Effective Rust 速查表 - 类型</description><pubDate>Thu, 25 Apr 2024 03:25:00 GMT</pubDate><content:encoded>&lt;p&gt;本文提炼于：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.lurklurk.org/effective-rust/types.html&quot;&gt;Types - Effective Rust&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;感谢作者以及出版社为广大 Rust 提供了一本这么好的 Rust 编码参考规则。&lt;/p&gt;
&lt;h2&gt;一. 使用类型系统表达数据结构&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Rust isn’t a language where you’re going to be doing much in the way of converting between pointers and integers — 在 C / C++ 中是允许操作内存地址的，比如加减内存地址。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rust 允许在数字字面量声明类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let x = 23i32;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rust 的 &lt;code&gt;char&lt;/code&gt; 字符，在内存中仍然保持为 4 字节 &lt;code&gt;Unicode&lt;/code&gt; 值，而不是转换为 32 位整数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 Rust 中使用 &lt;code&gt;try_into()&lt;/code&gt; 进行大位数的数字类型到小位数数字类型的转换，使用 &lt;code&gt;into()&lt;/code&gt; 进行小位数到大位数的数字类型转换。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Arrays&lt;/code&gt; 用于聚合一个类型的多个实例。&lt;code&gt;Tuple&lt;/code&gt; 用于聚合多个类型的多个实例，&lt;code&gt;Structs&lt;/code&gt; 同样用于聚合多个类型的多个类型。但是允许通过名称来引用各个字段。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;enum&lt;/code&gt; 允许携带一个数字或字符串值，并且具有类型安全检查。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 &lt;code&gt;match&lt;/code&gt; 的时候建议添加 &lt;code&gt;_ ⇒ {}&lt;/code&gt; 作为默认选项。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 Rust 中，表达可能存在错误使用 &lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt;，表达可能不存在值使用 &lt;code&gt;Option&amp;lt;T&amp;gt;&lt;/code&gt; 。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;二. 使用类型系统表达共同行为&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Method&lt;/code&gt; 方法，需要依附在 &lt;code&gt;enum&lt;/code&gt; &lt;code&gt;structs&lt;/code&gt; 等结构上，通常为 &lt;code&gt;impl 结构体名称&lt;/code&gt;，在 &lt;code&gt;fn&lt;/code&gt; 前使用 &lt;code&gt;pub&lt;/code&gt; 表示该方法为公开方法，允许从 &lt;code&gt;impl&lt;/code&gt; 块外的地方调用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rust 中呀很多很有意思的写法，根据匹配 enum 的值在一个 impl 的方法中以不同的方式进行计算，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Shape {
    Rectangle { width: f64, height: f64 },
    Circle { radius: f64 },
}

impl Shape {
    pub fn area(&amp;amp;self) -&amp;gt; f64 {
        match self {
            Shape::Rectangle { width, height } =&amp;gt; width * height,
            Shape::Circle { radius } =&amp;gt; std::f64::consts::PI * radius * radius,
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rust 支持函数指针，允许将一整个函数赋值给一个变量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rust 支持闭包，即将函数作为另一个函数的参数进行传递，当然 lambda 表达式支持也少不了。Rust 的表达式语法为 &lt;code&gt;|args…| = { … };&lt;/code&gt; 。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对于 Rust 语言而言，&lt;strong&gt;这种基于语句（statement）和表达式（expression）的方式是非常重要的，你需要能明确的区分这两个概念&lt;/strong&gt;, 但是对于很多其它语言而言，这两个往往无需区分。基于表达式是函数式语言的重要特征，&lt;strong&gt;表达式总要返回值&lt;/strong&gt;。 — 圣经&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以使用一些 Trait 来对作为参数传入的函数类型作出限制。&lt;code&gt;FnOnce&lt;/code&gt; 是指移交所有权的参数；&lt;code&gt;FnMut&lt;/code&gt; 相当于 &lt;code&gt;&amp;amp;mut T&lt;/code&gt; 指传入一个可变引用；Fn 相当于 &amp;amp;T，指传入引用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn modify_all&amp;lt;F&amp;gt;(data: &amp;amp;mut [u32], mut mutator: F)
where
    F: FnMut(u32) -&amp;gt; u32,
{
    for value in data {
        *value = mutator(*value);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rust 没有函数重载，函数重载有违 Rust 设计哲学之一的 Be Explicit，而大多数情况都可以改用泛型解决。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use num::Num;

fn plus&amp;lt;T: Num&amp;gt;(a: T, b: T) -&amp;gt; T {
    a + b
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rust 可以依据 Trait 限制泛型边界（Trait bounds）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn dump_sorted&amp;lt;T&amp;gt;(mut collection: T)
where
    T: Sort + IntoIterator,
    T::Item: std::fmt::Debug,
{
    // Next line requires `T: Sort` trait bound.
    collection.sort();
    // Next line requires `T: IntoIterator` trait bound.
    for item in collection {
        // Next line requires `T::Item : Debug` trait bound
        println!(&quot;{:?}&quot;, item);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;三. 使用 match 表达式转换 Option 和 Result&lt;/h2&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[Option&amp;lt;T&amp;gt;](https://doc.rust-lang.org/std/option/enum.Option.html)&lt;/code&gt;: To express that a value (of type &lt;code&gt;T&lt;/code&gt;) may or may not be present&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[Result&amp;lt;T, E&amp;gt;](https://doc.rust-lang.org/std/result/enum.Result.html)&lt;/code&gt; : For when an operation to return a value (of type &lt;code&gt;T&lt;/code&gt;) may not succeed  and may instead return an error (of type &lt;code&gt;E&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;正如前面所提：在 Rust 中，表达可能存在错误使用 &lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt;，表达可能不存在值使用 &lt;code&gt;Option&amp;lt;T&amp;gt;&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;我们使用 match 来匹配 Option 和 Result：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Option
struct S {
    field: Option&amp;lt;i32&amp;gt;,
}

let s = S { field: Some(42) };
match &amp;amp;s.field {
    Some(i) =&amp;gt; println!(&quot;field is {i}&quot;),
    None =&amp;gt; {}
}

// Result
let result = std::fs::File::open(&quot;/etc/passwd&quot;);
let f = match result {
    Ok(f) =&amp;gt; f,
    Err(_e) =&amp;gt; panic!(&quot;Failed to open /etc/passwd!&quot;),
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 &lt;code&gt;if let&lt;/code&gt; 语句或是 &lt;code&gt;if let else&lt;/code&gt; 语句简化 &lt;code&gt;Option&lt;/code&gt; 和 &lt;code&gt;Result&lt;/code&gt; 代码的解析&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Option
if let Some(i) = &amp;amp;s.field {
    println!(&quot;field is {i}&quot;);
}

// Result
if let Ok(i) = &amp;amp;s.field {
    println!(&quot;field is {i}&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;应该处理或返回 &lt;code&gt;Option&lt;/code&gt; 或 &lt;code&gt;Result&lt;/code&gt; 而不是使用 &lt;code&gt;unwarp&lt;/code&gt; 忽略封装直接返回结果。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;However, in many situations, the right decision for error handling is to defer the decision to somebody else.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用 &lt;code&gt;unwarp&lt;/code&gt; 时候，如果遇到 &lt;code&gt;None&lt;/code&gt; 或是 &lt;code&gt;Err&lt;/code&gt;，将导致代码将在此处被 &lt;code&gt;panic!&lt;/code&gt;，除非你清楚自己在干什么，否则不要使用它。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;标记一个 &lt;code&gt;#[must_use]&lt;/code&gt; 可以迫使使用者必须处理 &lt;code&gt;Result&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;warning: unused `Result` that must be used
  --&amp;gt; src/main.rs:63:5
   |
63 |     f.set_len(0); // Truncate the file
   |     ^^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled
   = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
   |
63 |     let _ = f.set_len(0); // Truncate the file
   |     +++++++
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以使用 &lt;a href=&quot;https://doc.rust-lang.org/reference/expressions/operator-expr.html#the-question-mark-operator&quot;&gt;? 操作符&lt;/a&gt; 将错误向上传播。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;• In particular, use these transformations to convert result types into a form where the &lt;code&gt;?&lt;/code&gt; operator applies.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;pub fn find_user(username: &amp;amp;str) -&amp;gt; Result&amp;lt;UserId, std::io::Error&amp;gt; {
    let f = std::fs::File::open(&quot;/etc/passwd&quot;)?;
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;</content:encoded></item><item><title>Java 实现一个简单的 EventBus</title><link>https://blog.hanbings.io/posts/java-eventbus/</link><guid isPermaLink="true">https://blog.hanbings.io/posts/java-eventbus/</guid><description>基于 Java 编写一个 EventBus</description><pubDate>Tue, 10 Mar 2020 10:57:00 GMT</pubDate><content:encoded>&lt;h3&gt;0x00 EventBus&lt;/h3&gt;
&lt;p&gt;EventBus 是一种低耦合甚至完全分离的消息传递模块设计&lt;/p&gt;
&lt;p&gt;实现 EventBus 通常基于发布/订阅者模式或观察者模式两种设计模式
例如基于前者的 Google/Guava 基于后者的 Minecraft Bukkit Event
在这个实现中 EventBus 是基于前者的，也就是发布/订阅者模式&lt;/p&gt;
&lt;p&gt;其实写这个玩具是想给其他项目做一个 插件/模组 事件系统来着 阿巴阿巴阿巴 （小声）&lt;/p&gt;
&lt;h3&gt;0x01 发布/订阅者模式与观察者模式&lt;/h3&gt;
&lt;p&gt;前面说过了，这两个模式是常见实现设计模式，发布/订阅者模式在某种意义上是观察者模式的一个扩展和补充&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;观察者模式由消息发布者与消息监听者两方组成&lt;/strong&gt;，消息发布者和消息监听者互相知晓，也就是说这样的模式只是低耦合
&lt;strong&gt;发布/订阅者模式由消息发布者、消息传递者和消息监听者三方组成&lt;/strong&gt;，消息发布者和消息监听者互不相识，完全隔离，由一个第三方消息传递者来进行通知，是一个完全分离的设计&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;昂… 观察者模式就像课堂上老师给同学说过两天要考试，消息发布者是老师，消息监听者是同学，同学直接从老师那里得知了消息，而发布/订阅者模式就像老师把过两天要考试的消息发布在了电子消息公告栏上一样，消息监听者同学们收到了由公告栏消息传递者发布的消息，同学们是不知道消息到底是老师发的还是校长发的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;0x02 结构&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://picture.hanbings.com/2021/08/27/d855263eb4dd9.png&quot; alt=&quot;EventBus&quot; /&gt;&lt;/p&gt;
&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/Hanbings/Cynops-event&quot;&gt;https://github.com/Hanbings/Cynops-event&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;类与类功能描述&lt;/h4&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;类名/接口名&lt;/th&gt;&lt;th&gt;功能描述&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Event&lt;/td&gt;&lt;td&gt;Event模板类 注册到EventBus的事件都需要继承自Event类&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Listener&lt;/td&gt;&lt;td&gt;Listener模板类 注册到EventBus的事件都需要继承自Event类&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;@EventHandler&lt;/td&gt;&lt;td&gt;事件处理器注解 当在注册一个符合条件 Listener 后 EventBus 将自动扫描 Listener 中带有此注解的方法 并且自动识别方法内第一个参数且只能是唯一的一个参数所要求的事件 当事件向 EventBus 中发布一次事件的时候 EventBus 将通知所有订阅了这一个事件的 Listener 然后执行 Listener 中对应的 @EventHandler 方法&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cancellable&lt;/td&gt;&lt;td&gt;一个 Event 接口, 实现该接口标识事件可以取消 如果不理解 可以先阅读 Blockable 使用接口中的 setCancelled(bollean cancel) 取消一个事件后 仅仅在 Event 中可能会存在的 cancel 标志位标记事件已经取消/未取消 但事件将继续向下传递到 @EventHandler 的方法中 ignoreCancelled() 参数保持 false 的处理器方法 在声明 @EventHandler时设置参数 ignoreCancelled 为 true 即忽略已经标记取消的事件 那么将不会被执行&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Blockable&lt;/td&gt;&lt;td&gt;一个 Event 接口 实现该接口标识事件可以阻断 阻断的意思是如果一个事件实现了这个接口 并且一个 EventHandler 使用了 接口中的 setBlocked(boolean block) 方法 本次事件将会就地结束&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;EventBus&lt;/td&gt;&lt;td&gt;EventBus 提供 registerEvent() / unregisterEvent () registerListener() / unregisterListener() callEvent() 等方法注册 注销 发布事件等操作&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;EventPriority&lt;/td&gt;&lt;td&gt;这个枚举使用在 @EventHandler 注解的 priority 参数中 LOWEST, LOW, NORMAL, HIGH, HIGHEST, MONITOR 是枚举内容 LOWEST 优先级最高, 是第一个被触发的等级, MONITOR 优先级最低, 是最后一个被触发的等级&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;RegisteredHandler&lt;/td&gt;&lt;td&gt;当 @EventHandler 被扫描后注册后加载到这个类&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;RegisteredListener&lt;/td&gt;&lt;td&gt;当 Listener 被扫描后并且扫描了 @EventListener 后将 RegisteredHandler 加载到这个类 事件的优先级排序是在这个过程中完成的&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;0x03 背后的故事&lt;/h3&gt;
&lt;h4&gt;1. Event - 事件&lt;/h4&gt;
&lt;p&gt;来看看一个事件是怎么样注册和发布的&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一个类&lt;strong&gt;继承&lt;/strong&gt;了 Event&lt;/li&gt;
&lt;li&gt;使用一个  EventBus 实例的 registerEvent(Event event)&lt;/li&gt;
&lt;li&gt;EventBus 经过一些操作后&lt;strong&gt;注册事件到内部的一张 Map 中&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;使用 EventBus 实例的 &lt;strong&gt;callEvent(Event event) 发布事件&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在第一步中，一个类继承自 Event 类，这样的设计是为了限定在 EventBus 中与 Event 相关的方法所能接受的类型&lt;/p&gt;
&lt;p&gt;第二步和第三步，使用 EventBus 中的 registerEvent(Event event) 注册事件到一张Map中&lt;/p&gt;
&lt;p&gt;昂… 那张 Map大概长这个样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private final Map&amp;lt;Class&amp;lt;? extends Event&amp;gt;, RegisteredListener&amp;gt; handlers = new ConcurrentHashMap&amp;lt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Map 的第一个参数自然是 Event 的子类 而第二个参数则是集合了所有有注册到这个事件的事件处理器的监听器（有点绕）的容器，它（RegisteredListener）看上去是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package io.hanbings.cynops.event;

import java.util.ArrayList;
import java.util.List;

@SuppressWarnings(&quot;unused&quot;)
public class RegisteredListener {
    private final List&amp;lt;RegisteredHandler&amp;gt; handlerList = new ArrayList&amp;lt;&amp;gt;();
    private final List&amp;lt;Integer&amp;gt; priorityIndex = new ArrayList&amp;lt;&amp;gt;();

    public RegisteredListener() {
        for (int count = 0; count &amp;lt; 6; count++) {
            priorityIndex.add(0);
        }
    }

    public List&amp;lt;RegisteredHandler&amp;gt; getHandlerList() {
        return handlerList;
    }

    public void addHandler(RegisteredHandler handler) {
        // 这一处非常糟糕的说
        int priority = getPriorityShadow(handler.getPriority());
        handlerList.add(priorityIndex.get(priority), handler);
        for (int count = priority; count &amp;lt; 6; count++) {
            priorityIndex.set(count, priorityIndex.get(count) + 1);
        }
    }

    public void removeHandler(RegisteredHandler handler) {
        handlerList.removeIf(registeredHandler -&amp;gt; registeredHandler.getListener().equals(handler.getListener()));
    }
    
    private int getPriorityShadow(EventPriority priority) {
        switch (priority) {
            case LOWEST:
                return 0;
            case LOW:
                return 1;
            case HIGH:
                return 3;
            case HIGHEST:
                return 4;
            case MONITOR:
                return 5;
            default:
                return 2;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个类当中又又又有两个列表，其中一个是 RegisteredHandler 的列表，另一个则是用于实现优先级的列表，回到事件注册，在EventBus中有：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void registerEvent(Event event) {
    if (!handlers.containsKey(event.getClass())) {
        handlers.put(event.getClass(), new RegisteredListener());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样大概明了了吧，在一个 EventBus 示例中，有一张存储监听器的 Map ，Event 类的 Class 作为键，一个 RegisteredListener 实例作为值，在 RegisteredListener 实例中又存储了 RegisteredHandler 事件处理器方法列表和优先级列表&lt;/p&gt;
&lt;p&gt;到第四步，使用 callEvent (Event event) 触发事件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void callEvent(Event event) {
    if (handlers.containsKey(event.getClass())) {
        for (RegisteredHandler handler : handlers.get(event.getClass()).getHandlerList()) {
            if (event instanceof Blockable &amp;amp;&amp;amp; ((Blockable) event).isBlocked()) {
                return;
            }
            if (event instanceof Cancellable
                    &amp;amp;&amp;amp; ((Cancellable) event).isCancelled()
                    &amp;amp;&amp;amp; handler.isIgnoreCancelled()) {
                continue;
            }
            try {
                handler.getMethod().invoke(handler.getListener(), event);
            } catch (IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace()
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先通过 &lt;code&gt;handlers.containsKey(event.getClass())&lt;/code&gt; 来判断 前面所说的 Map 中是否存在对应的 Event ，然后通过一个循环遍历 &lt;strong&gt;Map 中的 RegisteredListener 中的 HandlerList&lt;/strong&gt; 的注册的 @EventHandler ，接着判断有没有实现 Cancellable 和 Blockable 并在判断到接口有实现时判断状态做出对应动作，最后，通过一个反射来执行注册的 @EventHandler 方法 &lt;code&gt;handler.getMethod().invoke(handler.getListener(), event)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;有关于 Method#invoke 的用法在：&lt;a href=&quot;https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/reflect/Method.html&quot;&gt;https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/reflect/Method.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这样就完成了一个事件从注册到发布的一个过程&lt;/strong&gt; 这一部分倚重解释如何发布事件的，接下来说说 RegisteredListener RegisteredHandler 和 @EventHandler 的处理&lt;/p&gt;
&lt;h4&gt;2. @EventHandler - 事件处理器注解&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;package io.hanbings.cynops.event.interfaces;

import io.hanbings.cynops.event.EventPriority;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@SuppressWarnings(&quot;unused&quot;)
public @interface EventHandler {
    EventPriority priority() default EventPriority.NORMAL;
    boolean ignoreCancelled() default false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上是 @EventHandler 注解的完整代码，它有两个参数，一个是 EventPriority 类型的 priority，另一个是 boolean 类型的 ignoreCancelled，此外 @Target(ElementType.METHOD) 标注了它只能用于方法，@Retention(RetentionPolicy.RUNTIME) 则标注它在运行时加载，@Documented 表示它将会生成在 Javadocs 中，至于 @SuppressWarnings(“unused”) 是为了屏蔽 idea 一直在给这个类画黄线说没有被其他类引用，如果造一个单独模块的话这样的情况会常常会出现，但这个注解是无关紧要的，只是为了屏蔽 warning&lt;/p&gt;
&lt;h4&gt;3. RegisteredHandler - 多了一层皮的事件处理器&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;package io.hanbings.cynops.event;

import io.hanbings.cynops.event.interfaces.Listener;

import java.lang.reflect.Method;

@SuppressWarnings(&quot;unused&quot;)
public class RegisteredHandler {
    private EventPriority priority;
    private boolean ignoreCancelled;
    private Listener listener;
    private Method method;

    private RegisteredHandler() {
    }

    public RegisteredHandler(EventPriority priority, boolean ignoreCancelled
            , Listener listener, Method method) {
        this.priority = priority;
        this.ignoreCancelled = ignoreCancelled;
        this.listener = listener;
        this.method = method;
    }

    public EventPriority getPriority() {
        return priority;
    }

    public boolean isIgnoreCancelled() {
        return ignoreCancelled;
    }

    public Listener getListener() {
        return listener;
    }

    public Method getMethod() {
        return method;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是 RegisterHandler 的完整代码，主要用于存储 @EventHandler 所注解代码的完整信息，优先级 是否忽略已经取消的事件 监听器类 监听处理器方法（真的是方法，是反射扫描注解获得的监听处理器方法 Method 对象）扫描这一块我们下面 RegisterListener 说，RegisterHandler 理解起来就是一个 Java Bean，也就是对象容器，存储反射得到的内容避免重复反射浪费性能&lt;/p&gt;
&lt;h4&gt;4. RegisterListener - 监听器容器 处理器列表 优先级排序&lt;/h4&gt;
&lt;p&gt;RegisterListener 实际上已经在第二节里面说过了，这里主要讲讲注册一个监听器到变成 RegisterListener 的部分&lt;/p&gt;
&lt;p&gt;先是在 EventBus 中的 registerListener&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void registerListener(Listener listener) {
    Class&amp;lt;?&amp;gt; clazz = listener.getClass();
    for (Method method : clazz.getDeclaredMethods()) {
        if (method.isAnnotationPresent(EventHandler.class)) {
            final Class&amp;lt;?&amp;gt; event;
            method.setAccessible(true);
            event = method.getParameterTypes()[0];
            if (handlers.containsKey(event)) {
                EventHandler annotation = method.getAnnotation(EventHandler.class);
                handlers.get(event).addHandler(
                        new RegisteredHandler(annotation.priority()
                                , annotation.ignoreCancelled(), listener, method));
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;registerListener 主要是扫描 Listener 中带有 @EventHandler 注解的方法，扫描当然是大家都喜欢的反射啦&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (handlers.containsKey(event)) {
                EventHandler annotation = method.getAnnotation(EventHandler.class);
                handlers.get(event).addHandler(
                        new RegisteredHandler(annotation.priority()
                                , annotation.ignoreCancelled(), listener, method));
            }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这部分就更是重量级（代码缩进我也不知道是怎么回事，就这样先）先询问 handlers 图中有无存储 @EventHandler 方法要求的 Event，&lt;strong&gt;如果存在，则拿到值，也就是一个 RegisterListener 实例&lt;/strong&gt;，然后使用 RegisterListener 的 addHandler 方法向 RegisterListener 的两个列表中添加 RegisterHandler 和 优先级索引&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void addHandler(RegisteredHandler handler) {
        // 这一处非常糟糕的说
        int priority = getPriorityShadow(handler.getPriority());
        handlerList.add(priorityIndex.get(priority), handler);
        for (int count = priority; count &amp;lt; 6; count++) {
            priorityIndex.set(count, priorityIndex.get(count) + 1);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. EventBus - 事件总线&lt;/h4&gt;
&lt;p&gt;到这里其实已经把整个 EventBus 完整的接受了一次了，再看看 EventBus 里面剩下的部分吧&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void unregisterListener(Listener listener) {
        Class&amp;lt;?&amp;gt; clazz = listener.getClass();
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.isAnnotationPresent(EventHandler.class)) {
                final Class&amp;lt;?&amp;gt; event;
                method.setAccessible(true);
                event = method.getParameterTypes()[0];
                if (handlers.containsKey(event)) {
                    EventHandler annotation = method.getAnnotation(EventHandler.class);
                    handlers.get(event).removeHandler(
                            new RegisteredHandler(annotation.priority()
                                    , annotation.ignoreCancelled(), listener, method));
                }
            }
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;移除一个 Listener 是对 registerListener() 的反向操作，同样是拿到 RegisterListener 然后从里面的列表移除 RegisterHandler 然后更新索引&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void unregisterEvent(Event event) {
        handlers.remove(event.getClass());
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个更简单，直接从 Map 移除 Event 完事&lt;/p&gt;
&lt;h3&gt;0x04 最后&lt;/h3&gt;
&lt;p&gt;非常感谢有耐心读到这里，至此已经是这篇文章的全部内容了，如果有错误的地方欢迎各位指出&lt;/p&gt;
&lt;p&gt;项目的开源地址是：&lt;a href=&quot;https://github.com/hanbings/cynops-event&quot;&gt;https://github.com/hanbings/cynops-event&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我的Github是：&lt;a href=&quot;https://github.com/hanbings&quot;&gt;https://github.com/hanbings&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;欢迎 follow ~&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://picture.hanbings.com/2021/06/03/b24c7abcfb26f.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;</content:encoded></item><item><title>为 Linux 服务器 SSH 添加 TOTP 动态验证码以及 Java 实现算法</title><link>https://blog.hanbings.io/posts/linux-server-authorize-and-totp/</link><guid isPermaLink="true">https://blog.hanbings.io/posts/linux-server-authorize-and-totp/</guid><description>添加 TOTP 验证到 Linux 服务器</description><pubDate>Tue, 10 Mar 2020 10:57:00 GMT</pubDate><content:encoded>&lt;h3&gt;0x00 TOTP 动态验证码？&lt;/h3&gt;
&lt;p&gt;TOTP &lt;strong&gt;基于时间的一次性密码算法&lt;/strong&gt;（Time-Based One-Time Password）是一种根据预共享的密钥与当前时间计算一次性密码的算法，利用不同设备时间相同的特性，将时间作为特定算法的一部分从而达到无需网络进行安全验证的目的。 该算法有两个输入，一个输出，&lt;strong&gt;一个输入是随机生成的密钥&lt;/strong&gt;，密钥需要被验证方和验证器同时持有，&lt;strong&gt;另一个输入即系统时间，通常是 UNIX 时&lt;/strong&gt;，输出则是两方相同的验证码。一般的验证码有效期为 30 秒，每 30&lt;/p&gt;
&lt;p&gt;秒生成一个新的验证码。当前有很多领域和行业在使用 TOTP 作为它们的安全验证，比如银行使用的实体验证器，网易的网易将军令等。&lt;/p&gt;
&lt;p&gt;这套算法在 &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6238&quot;&gt;RFC6238&lt;/a&gt; 中有详细的说明，在后面的实现算法部分详细说明。&lt;/p&gt;
&lt;h3&gt;0x01 Linux 服务器安装 Google Authenticator&lt;/h3&gt;
&lt;p&gt;服务器采用&lt;strong&gt;腾讯云轻量服务器 Debian Linux 系统&lt;/strong&gt;，腾讯云轻量服务器是腾讯云基于 CVM 服务器推出以应用为中心的轻量级服务器，优点在于快速部署应用、价格便宜等。有需要在云环境练习 Linux 技能的推荐购买 &lt;a href=&quot;https://cloud.tencent.com/act/cps/redirect?redirect=1077&amp;amp;cps_key=42eeddb226d35120b98485ba26e1009e&amp;amp;from=console&quot;&gt;【腾讯云】云产品限时秒杀，爆款2核4G云服务器首年74元&lt;/a&gt; &lt;a href=&quot;https://cloud.tencent.com/act/cps/redirect?redirect=1068&amp;amp;cps_key=42eeddb226d35120b98485ba26e1009e&amp;amp;from=console&quot;&gt;【腾讯云】境外1核2G服务器低至2折，半价续费券限量免费领取！&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;SSH 远程登录后 先更新当前源列表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt update
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后执行安装指令&lt;/p&gt;
&lt;p&gt;当然，如果已经在 &lt;a href=&quot;https://curl.qcloud.com/V468ZsNQ&quot;&gt;腾讯云里买了一堆轻量机子&lt;/a&gt; 了，不想做这些繁琐的步骤，可以用轻量的自动化助手一键完成安装后再进入机子进行 Authenticator 密钥设置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://picture.hanbings.com/2021/12/24/d3755daa74a51.PNG&quot; alt=&quot;非常好用的轻量自动化助手&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install libpam-google-authenticator
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://picture.hanbings.com/2021/12/20/59bd359263290.PNG&quot; alt=&quot;安装Google Authenticator&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输入 google-authenticator 并按照说明进行配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;google-authenticator
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://picture.hanbings.com/2021/12/20/f74f7a7aa2325.PNG&quot; alt=&quot;设置 Authenticator&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图中黄线框部分为密钥，红色线框部分为备用验证码，备用验证码是在丢失验证器的情况下输入的验证码，验证码输入后将失效。&lt;strong&gt;请妥善保管这两个内容，它们是生成验证码的关键，也请不要泄露这个二维码&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;粗略翻译下选项&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Do you want authentication tokens to be time-based

需要启用基于时间的认证吗？

Do you want me to update your &quot;/root/.google_authenticator&quot; file?

需要更新配置文件吗？

Do you want to disallow multiple uses of the same authentication
token? This restricts you to one login about every 30s, but it increases
your chances to notice or even prevent man-in-the-middle attacks ?

禁止多次使用同一个身份验证令牌吗？这将限制大约每 30 秒登录一次，它将减少中间人攻击的机会

By default, a new token is generated every 30 seconds by the mobile app.
In order to compensate for possible time-skew between the client and the server,
we allow an extra token before and after the current time. This allows for a
time skew of up to 30 seconds between authentication server and client. If you
experience problems with poor time synchronization, you can increase the window
from its default size of 3 permitted codes (one previous code, the current
code, the next code) to 17 permitted codes (the 8 previous codes, the current
code, and the 8 next codes). This will permit for a time skew of up to 4 minutes
between client and server.
Do you want to do so? 

默认情况下，移动应用程序每 30 秒生成一个新令牌。
为了补偿客户端和服务器之间可能的时间偏差，
我们允许在当前时间之前和之后一个额外的令牌。这允许一个
身份验证服务器和客户端之间的时间偏差最大为 30 秒。如果你
遇到时间同步不好的问题，可以增加窗口
从其默认大小的 3 个允许代码（一个以前的代码，当前的
代码，下一个代码）到 17 个允许的代码（前 8 个代码，当前代码
代码，以及接下来的 8 个代码）。这将允许最多 4 分钟的时间偏差
客户端和服务器之间。
要这样做吗？

If the computer that you are logging into isn&apos;t hardened against brute-force
login attempts, you can enable rate-limiting for the authentication module.
By default, this limits attackers to no more than 3 login attempts every 30s.
Do you want to enable rate-limiting?

如果您登录的计算机没有针对蛮力进行强化
登录尝试，您可以为身份验证模块启用速率限制。
默认情况下，这会将攻击者限制为每 30 秒不超过 3 次登录尝试。
您要启用速率限制吗？
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;这个时候二步认证是还没有生效的&lt;/strong&gt;，还需要修改 pam 以及 ssh 配置，&lt;strong&gt;请仔细小心地修改配置，任何一处错误都可能导致 ssh 无法连接&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;鉴于操作危险性，建议提前对机子进行备份，&lt;strong&gt;比如 &lt;a href=&quot;https://curl.qcloud.com/V468ZsNQ&quot;&gt;腾讯云轻量&lt;/a&gt; 的镜像备份&lt;/strong&gt;，一个地区五个免费配额，不用白不用是吧 （手动狗头）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://picture.hanbings.com/2021/12/24/1f289ea40f270.PNG&quot; alt=&quot;腾讯云轻量提供免费备份&quot; /&gt;&lt;/p&gt;
&lt;p&gt;文件 &lt;strong&gt;/etc/pam.d/sshd&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+auth required pam_google_authenticator.so
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://picture.hanbings.com/2021/12/20/9ce4d755ead45.PNG&quot; alt=&quot;修改 PAM 配置&quot; /&gt;&lt;/p&gt;
&lt;p&gt;文件 &lt;strong&gt;/etc/ssh/sshd_config&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-ChallengeResponseAuthentication no
+ChallengeResponseAuthentication yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://picture.hanbings.com/2021/12/20/b867e11c35126.PNG&quot; alt=&quot;修改 SSH 配置&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;保存好二维码、密钥以及备用验证码后&lt;/strong&gt;断开连接重新登录服务器，这时输入密码后将会出现二步验证&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://picture.hanbings.com/2021/12/20/bd4593c88b95e.PNG&quot; alt=&quot;尝试连接 ssh&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;0x02 设置验证码生成设备&lt;/h3&gt;
&lt;p&gt;目前有很多家验证器客户端 比较流行的有 &lt;strong&gt;Google Authenticator 、 Microsoft Authenticator、Authy&lt;/strong&gt; 等。&lt;/p&gt;
&lt;p&gt;Microsoft Authenticator、Authy 相比 Google Authenticator 多了一套云同步系统，能防止意外清除软件数据之后丢失密钥导致的各种问题出现。但请注意，选择了带有云同步功能的应用中使用的同步账号&lt;strong&gt;必须设置一个高强度密码&lt;/strong&gt;，避免同步账号被盗取泄露验证码。&lt;/p&gt;
&lt;p&gt;因为各家 Authenticator 都不允许界面截图，所以它欠咱几张图片&lt;/p&gt;
&lt;p&gt;这里选用 Google Authenticator 下载安装后进入软件，选择扫描条形码，然后扫描上面保存的图片即可，或选择手动输入，账户为备注名字，可随意，密钥则为保存的密钥。&lt;/p&gt;
&lt;h3&gt;0x03 算法原理&lt;/h3&gt;
&lt;p&gt;现有实现了 TOTP 的软件中的算法 本质上就是 HMAC-SHA-1 算法，也就是带有盐值的 SHA-1&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;以 secret 密钥为盐值取当前时间的摘要，即 HMAC-SHA-1(K,C)&lt;/p&gt;
&lt;p&gt;K 为密钥，C 为当前 UNIX 时间 / 30，之所以除以 30 就是为了取整获得一个 30 内相同的值&lt;/p&gt;
&lt;p&gt;这样就得到了一个原始的哈希值，当然得到这个哈希值还不行，因为哈希值是 20 字节长的，对于 30 秒的验证码来说太长了，所以 &lt;strong&gt;HEX = HMAC-SHA-1(K,C)&lt;/strong&gt;  等下还要用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取 HEX 的第 20 字节，也就是 **HEX[19] 的低四位（后四位）**作为偏移量 OFFEST&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 HEX 中，从偏移量 OFFEST 开始&lt;strong&gt;取四个字节&lt;/strong&gt;作为验证码中间值 WIP&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将 &lt;strong&gt;WIP mod 10^6&lt;/strong&gt; 得到 6 位数字，不够 6 位高位补 0 即验证码&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;举个例子&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://picture.hanbings.com/2021/12/20/40b25bb993265.png&quot; alt=&quot;TOTP 取验证码&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;0x04 使用 Java 实现算法&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;魔法时刻&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;核心算法 HMAC-SHA-1&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
     * HmacSHA1 计算
     *
     * @param source 数据源
     * @param key    密钥
     * @return String 计算结果
     */
    public static String sha1(String source, String key) {
        try {
            Mac mac = Mac.getInstance(&quot;HmacSHA1&quot;);
            SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), &quot;HmacSHA1&quot;);
            mac.init(keySpec);
            byte[] byteArray = mac.doFinal(source.getBytes(StandardCharsets.UTF_8));
            StringBuilder stringBuilder = new StringBuilder();
            for (byte temp : byteArray) {
                stringBuilder.append(String.format(&quot;%02x&quot;, temp));
            }
            return stringBuilder.toString();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;流程&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package io.hanbings.cynops.extra.otpauth;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.Objects;

/**
 * 实现 RFC6238 中描述的 TOTP &amp;lt;br&amp;gt;
 * Time-Based One-Time Password 基于时间的一次性密码 &amp;lt;br&amp;gt;
 * 参考 &amp;lt;br&amp;gt;
 * 1. https://datatracker.ietf.org/doc/html/rfc6238 &amp;lt;br&amp;gt;
 * 2. https://www.aqniu.com/tools-tech/4671.html
 */
@SuppressWarnings(&quot;unused&quot;)
public class Totp {
    public static String totp(byte[] secret, String algorithm, int distance, long offset) {
        // (当前时间 + 时间偏移量) / 时间片间隔
        long time = ((System.currentTimeMillis()) / 1000) / distance;
        byte[] data = sha1(longToByte(time), secret, algorithm);
        int index = Objects.requireNonNull(data)[19] &amp;amp; 0xf;
        int wip = byteToInt(data, index) &amp;amp; 0x7fffffff;
        return padding(wip);
    }

    /**
     * SHA1 计算
     *
     * @param source 数据源
     * @param key    密钥
     * @return String 计算结果
     */
    private static byte[] sha1(byte[] source, byte[] key, String algorithm) {
        try {
            Mac mac = Mac.getInstance(algorithm);
            SecretKeySpec keySpec = new SecretKeySpec(key, algorithm);
            mac.init(keySpec);
            return mac.doFinal(source);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * long 转换为 byte
     *
     * @param time long 数据
     * @return byte 数组
     */
    private static byte[] longToByte(long time) {
        byte[] bytes = new byte[8];
        for (int count = 0; count &amp;lt; 8; count++) {
            int offset = 64 - (count + 1) * 8;
            bytes[count] = (byte) ((time &amp;gt;&amp;gt; offset) &amp;amp; 0xff);
        }
        return bytes;
    }

    /**
     * byte 转换 int
     *
     * @param bytes byte 数组
     * @param start 开始位移
     * @return int
     */
    private static int byteToInt(byte[] bytes, int start) {
        DataInput input = new DataInputStream(new ByteArrayInputStream(bytes, start, bytes.length - start));
        int temp;
        try {
            temp = input.readInt();
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
        return temp;
    }

    /**
     * 高位补0
     *
     * @param wip 已经处理好的验证码
     * @return 最终结果
     */
    private static String padding(int wip) {
        StringBuilder code = new StringBuilder(String.valueOf(wip % 1000000));
        for (; ; ) {
            if (code.length() &amp;lt; 6) {
                code.insert(0, &quot;0&quot;);
            } else {
                return code.toString();
            }
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;处理符合 Google Authenticator 要求的 Base32 格式的密钥&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;注意下这里处理只是为了得到 Google Authenticator 能识别的密钥，&lt;strong&gt;事实上传入 HMAC-SHA-1 的还是需要解码 Base32 的原始密钥&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
     * 编码 Base32
     *
     * @param source 原始字符
     * @return Base 编码字符
     */
    private static String encode(byte[] source) {
        char[] chars = new char[((source.length * 8) / 5) + ((source.length % 5) != 0 ? 1 : 0)];
        for (int count = 0, handle = 0, index = 0; count &amp;lt; chars.length; count++) {
            if (index &amp;gt; 3) {
                int token = source[handle] &amp;amp; (0xFF &amp;gt;&amp;gt; index);
                index = (index + 5) % 8;
                token &amp;lt;&amp;lt;= index;
                if (handle &amp;lt; source.length - 1) {
                    token |= (source[handle + 1] &amp;amp; 0xFF) &amp;gt;&amp;gt; (8 - index);
                }
                chars[count] = ALPHABET[token];
                handle++;
            } else {
                chars[count] = ALPHABET[((source[handle] &amp;gt;&amp;gt; (8 - (index + 5))) &amp;amp; 0x1F)];
                index = (index + 5) % 8;
                if (index == 0) {
                    handle++;
                }
            }
        }
        return new String(chars);
    }

    /**
     * Base32 解码
     *
     * @param source Base32 编码字符串
     * @return 原始字符
     */
    private static byte[] decode(String source) {
        char[] stringData = source.toCharArray();
        byte[] data = new byte[(stringData.length * 5) / 8];
        for (int count = 0, handle = 0, index = 0; count &amp;lt; stringData.length; count++) {
            int token = DECODE_TABLE[stringData[count]];
            if (index &amp;lt;= 3) {
                index = (index + 5) % 8;
                if (index == 0) {
                    data[handle++] |= token;
                } else {
                    data[handle] |= token &amp;lt;&amp;lt; (8 - index);
                }
            } else {
                index = (index + 5) % 8;
                data[handle++] |= (token &amp;gt;&amp;gt; index);
                if (handle &amp;lt; data.length) {
                    data[handle] |= token &amp;lt;&amp;lt; (8 - index);
                }
            }
        }
        return data;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;计算验证码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
     * 生成符合要求的密钥
     *
     * @return 密钥
     */
    public static String secret() {
        try {
            SecureRandom random = SecureRandom.getInstance(&quot;SHA1PRNG&quot;);
            return encode(random.generateSeed(10)).toUpperCase(Locale.ROOT);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取当前验证码
     *
     * @param secret 密钥
     * @return 验证码
     */
    public static String code(String secret) {
        return Totp.totp(
                decode(secret),
                &quot;HmacSHA1&quot;
                , 30
                , 0);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;0x05 最后&lt;/h3&gt;
&lt;p&gt;文章中可能还有错误的地方，欢迎各位提出。&lt;/p&gt;
&lt;p&gt;文中提及的代码均在我的开源 Java 中间件项目 &lt;a href=&quot;https://github.com/Hanbings/Cynops&quot;&gt;Cynops&lt;/a&gt; 中 ，欢迎 Star ，也可以 Follow 我的 Github &lt;a href=&quot;https://github.com/Hanbings&quot;&gt;Hanbings&lt;/a&gt; 还请前辈们多多指教。&lt;/p&gt;</content:encoded></item></channel></rss>