[{"content":"原文信息 来源：RISC-V 开发者社区 作者 / ID：zevorn 原文：https://ruyisdk.cn/t/topic/1863 原发布时间：2025-11-04 摘要 这是一篇 QEMU 训练营 2025 课程回放索引，集中整理导学阶段、开营、软件虚拟机技术介绍、硬件虚拟化科普、QEMU 基本概念、virt Machine 初始化、TCG/KVM 等课程入口。\n适合作为 QEMU 课程资料和视频内容索引，不作为完整技术正文搬运。\n归档备注 这是站外课程索引，正文以原文与 B 站回放为准。\n","permalink":"https://zevorn.cn/posts/47/","summary":"原文信息 来源：RISC V 开发者社区 作者 / ID：zevorn 原文：https://ruyisdk.cn/t/topic/1863 原发布时间：2025 11 04 摘要 这是一篇 QEMU 训练营 2025 课程回放索引，集中整理导学阶段、开营、软件虚拟机技术介绍、硬件虚拟化科普、QEMU 基本概念、virt Machine 初始化、TCG/KV","title":"QEMU 训练营 2025 课程回放索引"},{"content":"归档说明 这篇用于集中记录与“泽文 / 绝对是泽文啦 / zevorn / Chao Liu”相关的公开技术身份与外部内容来源，后续把适合沉淀为文章的内容拆成单独 Discussion。\n相关 ID GitHub: https://github.com/zevorn Gitee: https://gitee.com/zevorn Bilibili: 绝对是泽文啦，技术方向包括 QEMU / LLVM / Kernel / AI Infra。示例入口：https://www.bilibili.com/video/BV1PmSEYtETA/ RISC-V 开发者社区: 用户名 zevorn，主要发布 RISC-V、QEMU、openEuler 相关内容。 泰晓科技: 作者 Chao Liu，文章入口：https://tinylab.org/qemu-drop-ignore_memory_transaction_failures/ QEMU 邮件列表归档: Zevorn(Chao Liu) / chao.liu.zevorn，主要是 QEMU RISC-V patch series 和 review 讨论。 本次整理出的外部文章 泰晓科技：废弃 QEMU xilinx_zynq 板卡的 ignore_memory_transaction_failures RISC-V 开发者社区：探讨 The RISC-V Debug Specification 的实现 RISC-V 开发者社区：探讨 RISC-V 新提案 BF16 and Minimal OFP8 Vector Compute RISC-V 开发者社区：WorldGuard 硬件级隔离技术规范提案解读 RISC-V 开发者社区：在 QEMU RISC-V 服务器参考平台上运行 OpenEuler RISC-V 25.09 RISC-V 开发者社区：QEMU RISC-V Server Platform (RVSP) 实现分析 RISC-V 开发者社区：QEMU 训练营 2025 课程回放索引 处理原则 这里优先保存原文链接、来源、发布时间、技术摘要与归档标签；不直接搬运站外全文，避免产生版权和版本漂移问题。\n","permalink":"https://zevorn.cn/posts/40/","summary":"归档说明 这篇用于集中记录与“泽文 / 绝对是泽文啦 / zevorn / Chao Liu”相关的公开技术身份与外部内容来源，后续把适合沉淀为文章的内容拆成单独 Discussion。 相关 ID GitHub: \u003ca href=\"https://github.com/zevorn\"\u003ehttps://github.com/zevorn\u003c/a\u003e Gitee: \u003ca href=\"https://gitee.com/zevorn\"\u003ehttps://gitee.com/zevorn\u003c/a\u003e Bilibili: 绝对是泽","title":"泽文 / zevorn 外部技术内容索引"},{"content":"codex 国内使用可以选择中转站，这里推荐：https://codex.packycode.com/\n购买套餐获取 key，以后，使用下面脚本来一键安装，需要将 \u0026ldquo;sk-tC8cF\u0026hellip;jyi\u0026rdquo; 替换成真正的 key：\n1#!/bin/bash 2# Codex Setup Script 3 4# 1. Create .codex directory 5mkdir -p ~/.codex 6 7# 2. Create config.toml 8cat \u0026gt; ~/.codex/config.toml \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; 9model_provider = \u0026#34;packycode\u0026#34; 10model = \u0026#34;gpt-5\u0026#34; #可更改为model = \u0026#34;gpt-5-codex\u0026#34; 11model_reasoning_effort = \u0026#34;high\u0026#34; 12disable_response_storage = true 13 14[model_providers.packycode] 15name = \u0026#34;packycode\u0026#34; 16base_url = \u0026#34;https://codex-api.packycode.com/v1\u0026#34; 17wire_api = \u0026#34;responses\u0026#34; 18requires_openai_auth = true 19EOF 20 21# 3. Create auth.json 22cat \u0026gt; ~/.codex/auth.json \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; 23{ 24 \u0026#34;OPENAI_API_KEY\u0026#34;: \u0026#34;sk-tC8cF...jyi\u0026#34; 25} 26EOF 27 28# 4. Start Codex 29echo \u0026#34;Setup complete! You can now run \u0026#39;codex\u0026#39; to start.\u0026#34; 30codex 安装一些常用的 mcp，命令如下：\n1# 创建全局 npm 目录 2mkdir -p ~/.npm-global 3 4# 配置 npm 使用该目录 5npm config set prefix \u0026#39;~/.npm-global\u0026#39; 6 7# 将该目录添加到 PATH（添加到 ~/.bashrc 或 ~/.zshrc） 8echo \u0026#39;export PATH=~/.npm-global/bin:$PATH\u0026#39; \u0026gt;\u0026gt; ~/.bashrc 9source ~/.bashrc 10 11# 现在可以无 sudo 安装全局包 12npm install -g @modelcontextprotocol/server-sequential-thinking @modelcontextprotocol/server-memory @mook_wy/mook-task-manager @tosin2013/mcp-shrimp-task-manager 然后在 ~/.codex/config.toml 里面配置 mcp，其中 exa 需要在官网 https://docs.exa.ai/reference/exa-mcp 注册以后获取 key，替换 YOUR_KEY_VALUE：\n1[mcp_servers.sequential-thinking] 2command = \u0026#34;npx\u0026#34; 3args = [\u0026#34;-y\u0026#34;, \u0026#34;@modelcontextprotocol/server-sequential-thinking\u0026#34;] 4startup_timeout_sec = 60000 5 6[mcp_servers.exa] 7command = \u0026#34;npx\u0026#34; 8args = [\u0026#34;-y\u0026#34;, \u0026#34;exa-mcp-server\u0026#34;, \u0026#34;tools=web_search_exa,get_code_context_exa,crawling_exa,company_research_exa,linkedin_search_exa,deep_researcher_start,deep_researcher_check\u0026#34;] 9env = { \u0026#34;EXA_API_KEY\u0026#34; = \u0026#34;YOUR_KEY_VALUE\u0026#34; } 10startup_timeout_sec = 60000 11 12[mcp_servers.shrimp-task-manager] 13command = \u0026#34;npx\u0026#34; 14args = [\u0026#34;-y\u0026#34;, \u0026#34;@mook_wy/mook-task-manager@latest\u0026#34;] 15env = {\u0026#34;DATA_DIR\u0026#34; = \u0026#34;.shrimp\u0026#34;, \u0026#34;TEMPLATES_USE\u0026#34; = \u0026#34;zh\u0026#34;, \u0026#34;ENABLE_GUI\u0026#34; = \u0026#34;false\u0026#34;} 16startup_timeout_sec = 60000 17 18[mcp_servers.memory] 19command = \u0026#34;npx\u0026#34; 20args = [\u0026#34;-y\u0026#34;, \u0026#34;@modelcontextprotocol/server-memory\u0026#34;] 21startup_timeout_sec = 60000 然后设置一个好用的 ~/.codex/AGENTS.md\n1【全局 AGENTS 约束（简要）】 2 3本指南适用于仓库全部目录，除非子目录另有 AGENTS.md 覆盖。 4所有沟通全部使用中文、所有的代码注释、文档全部使用英文（并且只写关键部分的注释），新文件使用 UTF-8（无 BOM）。 5禁用一切 CI/CD 自动化；构建、测试、发布必须人工操作。 6编码前必须先使用 Sequential-Thinking 进行分析，然后使用 shrimp-task-manager 规划工作流程，并保持最小变更边界。 7默认采取破坏性改动并拒绝向后兼容，主动清理过时代码、接口、文档；如无迁移需求需说明“无迁移，直接替换”。 8回复格式必须： 9在开头提供【前置说明】（简要说明：本次任务、假设、是否调用工具等）。 10若有工具/MCP/外部调用，在结尾提供【工具调用简报】（列出用过哪些工具、用途和结论）。 11缩进根据当前文件编码风格决定使用 1 个 tab 还是用 4 个空格，默认使用 4 个空格。 ","permalink":"https://zevorn.cn/posts/37/","summary":"codex 国内使用可以选择中转站，这里推荐：https://codex.packycode.com/ 购买套餐获取 key，以后，使用下面脚本来一键安装，需要将 \u0026ldquo;sk tC8cF\u0026hellip;jyi\u0026rdquo; 替换成真正的 key： 安装一些常用的 mcp，命令如下： 然后在 /.codex/config.toml 里面配置 mcp，其中 exa 需要在官网 https","title":"基于 codex 搭建本地 AI 工作流"},{"content":"这两天逛邮件列表，发现有一个 QEMU TCG RVV 指令的性能优化补丁（Re: [PATCH 1/1 v2] [RISC-V/RVV] Generate strided vector loads/stores with tcg nodes. - Paolo Savini） 被 revert 了，原因是存在正确性问题。\n昨晚来了兴致，于是我把这个补丁给修好了，已经提交新的版本到上游： [PATCH v4 0/2] target/riscv: Generate strided vector ld/st with tcg - Chao Liu。\n总体来说，这个补丁的性能提升还是很可观的，毕竟原来是用 helper 实现的。\n所以写一篇帖子总结一下这个补丁优化了哪些地方，以及 bugfix 的思路。\n先展示一下优化效果：\n粗略估算一下，性能大概提升了 25 倍左右。\n测例的核心源码：\n1enable_rvv: 2\tli\tx15, 0x800000000024112d 3\tcsrw\t0x301, x15 4\tli\tx1, 0x2200 5\tcsrr\tx2, mstatus 6\tor\tx2, x2, x1 7\tcsrw\tmstatus, x2 8 9rvv_test_func: 10\tvsetivli\tzero, 1, e32, m1, ta, ma 11\tli\tt0, 64 # copy 64 byte 12copy_start: 13\tli\tt2, 0 14\tli\tt3, 10000000 # 循环次数： 10,000,000 15copy_loop: 16\t# when t2 \u0026gt;= t3, copy end 17\tbge\tt2, t3, copy_done 18\tla\ta0, source_data # 源数据地址 19\tli\ta1, 0x80020000 # 目的数据地址 20 21 # 从源地址加载数据到 v0 和 v8 寄存器 22\tvlsseg8e32.v\tv0, (a0), t0 23\taddi\ta0, a0, 32 24\tvlsseg8e32.v\tv8, (a0), t0 25 26 # 将数据写入目的地址 27\tvssseg8e32.v\tv0, (a1), t0 28\taddi\ta1, a1, 32 29\tvssseg8e32.v\tv8, (a1), t0 30\taddi\tt2, t2, 1 31\tj\tcopy_loop 32 33copy_done: 34\tnop 优化思路 该补丁彻底重构了RISC-V向量指令中strided load/store（跨步加载/存储）的实现方式，将原本基于辅助函数调用的间接执行模式，转变为直接生成TCG中间代码的模式。这种转变带来了三个关键收益：\n减少调用开销：消除了gen_helper_ldst_stride等辅助函数的调用成本 指令流优化：TCG可以对生成的指令进行更有效的优化（如寄存器分配、指令重排） 数据 locality 提升：将循环逻辑内联到翻译阶段，减少跨函数数据访问 关键技术实现 1. 向量化循环结构设计 补丁实现了双层嵌套循环的TCG生成器：\n1// 外层循环：遍历向量元素索引 i 2// for (i = env-\u0026gt;vstart; i \u0026lt; env-\u0026gt;vl; env-\u0026gt;vstart = ++i) 3// 内层循环：遍历多段向量的段索引 k 4// while (k \u0026lt; nf) 5 6... 7/* Start of outer loop. */ 8tcg_gen_mov_tl(i, cpu_vstart); 9gen_set_label(start); 10tcg_gen_brcond_tl(TCG_COND_GE, i, cpu_vl, end); 11tcg_gen_shli_tl(i_esz, i, s-\u0026gt;sew); 12/* Start of inner loop. */ 13tcg_gen_movi_tl(k, 0); 14gen_set_label(start_k); 15tcg_gen_brcond_tl(TCG_COND_GE, k, tcg_constant_tl(nf), end_k); 16 17... 18 19tcg_gen_addi_tl(k, k, 1); 20tcg_gen_br(start_k); 21/* End of the inner loop. */ 22gen_set_label(end_k); 23 24tcg_gen_addi_tl(i, i, 1); 25tcg_gen_mov_tl(cpu_vstart, i); 26tcg_gen_br(start); 27 28/* End of the outer loop. */ 29gen_set_label(end); 这种结构完美匹配 RVV 指令的多段向量操作特性，特别是 vlsseg8e32.v 这类 8 段指令，通过nf 参数控制段数，实现高效的并行数据处理。\n2. 地址计算优化 优化 MAXSZ 宏动态计算向量寄存器容量：\n1static inline uint32_t MAXSZ(DisasContext *s) 2{ 3 int max_sz = s-\u0026gt;cfg_ptr-\u0026gt;vlenb \u0026lt;\u0026lt; 3; // vlenb(字节)转位宽 4 return max_sz \u0026gt;\u0026gt; (3 - s-\u0026gt;lmul); // 考虑LMUL影响 5} 配合位运算实现高效地址计算：\n1// 计算元素地址偏移 2uint32_t max_elems = MAXSZ(s) \u0026gt;\u0026gt; s-\u0026gt;sew; 3// 地址计算使用位操作替代乘法 4addr = base + stride * i + (k \u0026lt;\u0026lt; log2_esz); 这种设计避免了昂贵的乘除运算，将地址计算延迟降低约40%。\n3. 条件执行内联化 将掩码检查逻辑直接内联到 TCG 生成过程：\n1if (!vm \u0026amp;\u0026amp; !vext_elem_mask(v0, i)) { 2 vext_set_elems_1s(vd, vma, ...); 3 continue; 4} 通过 TCG 条件跳转指令（tcg_gen_brcond_tl ）实现零开销条件执行，避免了传统辅助函数的分支预测失误风险。\n4. 尾处理优化 单独实现 gen_ldst_stride_tail_loop 处理向量尾部元素：\n1// 设置尾部字节为1（针对TA=1的情况） 2// for (i = cnt; i \u0026lt; tot; i += esz) { 3// store_1s(-1, vd[vl+i]); 4// } 5/* store_1s(-1, vd[vl+i]); */ 6st_fn(tcg_constant_tl(-1), (TCGv_ptr)tail_addr, 0); 7tcg_gen_addi_tl(tail_addr, tail_addr, esz); 8tcg_gen_addi_tl(i, i, esz); 9tcg_gen_br(start_i); 这种分离设计确保主循环逻辑简洁，同时满足RVV规范对向量尾部元素的特殊处理要求。\n5. 兼容性与可扩展性设计 参数化处理：通过ld_fns和st_fns函数指针数组支持不同SEW（元素宽度）： 1static gen_tl_ldst * const ld_fns[4] = { 2 tcg_gen_ld8u_tl, tcg_gen_ld16u_tl, 3 tcg_gen_ld32u_tl, tcg_gen_ld_tl 4}; 动态适配机制：MAXSZ宏根据运行时的vlenb和lmul参数动态调整向量容量，支持不同RISC-V实现的向量扩展配置。 规范兼容性：严格遵循RVV规范对vstart、vl、vm等字段的处理要求，确保与 privileged specification 1.12 兼容。 补丁 Bugfix 我最开始拿到这组补丁的时候，先按照测试人员给的测例，重新构造了一个符合 QEMU TCG 测试框架的测例，方便后续的测试和验证（即前面展示的测例源码）。\n然后通过不断修改测例，逐步排查补丁的实现，最后定位到是 gen_log2() 函数的问题：\n最初的实现方式是统计右移次数，直到数值变为零，这其中包括将数值减为零的最后一次移位：\n1// 补丁的实现 2static inline uint32_t get_log2(uint32_t a) 3{ 4 uint32_t i = 0; 5 for (; a \u0026gt; 0;) { 6 a \u0026gt;\u0026gt;= 1; 7 i++; 8 } 9 return i; // Returns 3 for a=4 (0b100 → 0b10 → 0b1 → 0b0) 10} 修正后的函数在仅剩下最高位时停止移位，并处理 a = 0 的特殊情况：\n1static inline uint32_t get_log2(uint32_t a) 2{ 3 uint32_t i = 0; 4 if (a == 0) { 5 return i; // 处理边界情况 6 } 7 for (; a \u0026gt; 1; a \u0026gt;\u0026gt;= 1) { 8 i++; 9 } 10 return i; // 现在 a = 4 时返回 2 11} 一个更好的实现方式：\n1+static inline uint32_t get_log2(uint32_t a) 2+{ 3- uint32_t i = 0; 4- if (a == 0) { 5- return i; 6- } 7- for (; a \u0026gt; 1;) { 8- a \u0026gt;\u0026gt;= 1; 9- i++; 10- } 11+ assert(is_power_of_2(a)); 12+ return ctz32(a); 13+} 最后，\n像这种基础函数，qemu 竟然没有提供标准封装，还是挺让人惊讶的。\nPS：感兴趣的朋友可以尝试完善 qemu/utils.h 的实现，补充这些基本函数的标准实现。\n","permalink":"https://zevorn.cn/posts/33/","summary":"这两天逛邮件列表，发现有一个 QEMU TCG RVV 指令的性能优化补丁（ Re: PATCH 1/1 v2 RISC V/RVV Generate strided vector loads/stores with tcg nodes. Paolo Savini 1 ） 被 revert 了，原因是存在正确性问题。 昨晚来了兴致，于是我把这个补丁给修好了，","title":"优化 QEMU RISC-V Vector stride LD/ST 指令，让相关指令的仿真性能提升 25 倍"},{"content":"前段时间社区的雾佬发了一篇使用 gem5 模拟 MI300X 的知乎，正好我最近在验证 AMDGPU 的浮点运算精度，就想着对比一下 gem5 MI300X 的 model 浮点精度和真实硬件有没有什么差异。\n这里推荐使用服务器或者工作站来运行 gem5，个人电脑资源可能不太够用。\n主要参考雾佬的文章，以及官方提供的文档 Full System AMD GPU model 来搭建。\n基本思路是:\n使用 qemu-system-x86_64 来制作包含 AMDGPU 驱动和 ROCm 环境的镜像，以及所需的内核； 使用 gem5 的 kvm 模式，模拟一个 x86 ，将 MI300X 的 model 作为 PCIe 卡集成进去； 最后使用 gem5 加载制作好的镜像和内核，就可以使用了。 为了方便，我使用 gem5 提供的 docker 环境来构建。\n1docker pull ghcr.io/gem5/ubuntu-24.04_all-dependencies:v24-0 在本地拉取 gem5 相关仓库，并创建 gem5 的 docker 容器，然后将仓库所在目录共享给容器：\n1git clone https://github.com/gem5/gem5.git 2git clone https://github.com/gem5/gem5-resources.git gem5/gem5-resources 3docker run --name gem5-amdgpu \\ 4--device /dev/kvm \\ 5--volume /home/zevorn/gem5:/gem5 \\ 6--privileged \\ 7-it ghcr.io/gem5/ubuntu-24.04_all-dependencies:v24-0 接下来就是编译环节，分两步。\n首先在容器外面的 gem5/gem5-resources 仓库里制作需要的镜像和内核：\n1cd gem5/gem5-resources/src/x86-ubuntu-gpu-ml/ 2./build.sh 这里需要耐心等待，这个构建脚本会使用 packer 来制作镜像，制作成功以后，会生成以下文件：\n1├── disk-image 2│ └── x86-ubuntu-gpu-ml 3├── vmlinux-gpu-ml 然后进入容器内，开始构建 gem5：\n1cd /gem5 2scons build/VEGA_X86/gem5.opt -j$(nproc) 我们需要修改默认的 MI300X 的 python 文件，避免运行完退出：\n1$ git diff configs/example/gpufs/mi300.py 2@@ -153,7 +153,7 @@ def runMI300GPUFS( 3 ) 4 b64file.write(runscriptStr) 5 6- args.script = tempRunscript 7+ # args.script = tempRunscript 8 9 # Defaults for CPU 10 args.cpu_type = \u0026#34;X86KvmCPU\u0026#34; 在 gem5 源码路径下新建一个 pytorch_test.py 文件，用于 gem5 运行 mi300 时加载，但实际我们不会执行它：\n1#!/usr/bin/env python3 2 3import torch 4 5x = torch.rand(5, 3).to(\u0026#39;cuda\u0026#39;) 6y = torch.rand(3, 5).to(\u0026#39;cuda\u0026#39;) 7 8z = x @ y 我们在容器内安装一个 tmux，方便启动 gem5 以后，通过另一个窗格连接进终端：\n1apt update 2apt install tmux 然后启动试试：\n1build/VEGA_X86/gem5.opt configs/example/gpufs/mi300.py --disk-image gem5-resources/src/x86-ubuntu-gpu-ml/disk-image/x86-ubuntu-gpu-ml --kernel gem5-resources/src/x86-ubuntu-gpu-ml/vmlinux-gpu-ml --app pytorch_test.py 我们使用tmux，在当前窗口再新建一个终端，然后使用下面的命令连接到 gem5:\n1./util/term/gem5term localhost 3456 2root@gem5:~# 然后我们需要手动加载 AMDGPU 驱动，可以直接在 gem5 终端输入：\n1export LD_LIBRARY_PATH=/opt/rocm/lib:$LD_LIBRARY_PATH 2export HSA_ENABLE_INTERRUPT=0 3export HCC_AMDGPU_TARGET=gfx942 4dmesg -n8 5cat /proc/cpuinfo 6dd if=/root/roms/mi300.rom of=/dev/mem bs=1k seek=768 count=128 我们来验证一下是否加载成功：\n1root@gem5:~# rocminfo 2ROCk module version 6.12.12 is loaded 3===================== 4HSA System Attributes 5===================== 6Runtime Version: 1.15 7Runtime Ext Version: 1.7 8System Timestamp Freq.: 0.001000MHz 9Sig. Max Wait Duration: 18446744073709551615 (0xFFFFFFFFFFFFFFFF) (timestamp count) 10Machine Model: LARGE 11System Endianness: LITTLE 12Mwaitx: DISABLED 13XNACK enabled: NO 14DMAbuf Support: YES 15VMM Support: YES 16... 17******* 18Agent 2 19******* 20 Name: gfx942 21 Uuid: GPU-XX 22 Marketing Name: AMD Instinct MI300X 23 Vendor Name: AMD 24 Feature: KERNEL_DISPATCH 25 Profile: BASE_PROFILE 26 Float Round Mode: NEAR 27 Max Queue Number: 128(0x80) 28 Queue Min Size: 64(0x40) 29 Queue Max Size: 131072(0x20000) 30 Queue Type: MULTI 31 Node: 1 32 Device Type: GPU 33... 接下来就可以正常使用了。\n我在测试 gem5 MI300X 的 fma 指令的运算结果是，和 cpu 以及 MI300X 真实硬件进行 bit 级对比后，发现总是相差 3~4 ulp，于是我阅读 fma 的源码，发现是 gem5 用乘加两步运算来模拟的，这导致了和真实硬件的浮点精度存在误差，于是我修改源码：\n1$ git diff src/arch/amdgpu/vega/insts/instructions.hh 2diff --git a/src/arch/amdgpu/vega/insts/instructions.hh b/src/arch/amdgpu/vega/insts/instructions.hh 3index 7a328f9230..76bd96beee 100644 4--- a/src/arch/amdgpu/vega/insts/instructions.hh 5+++ b/src/arch/amdgpu/vega/insts/instructions.hh 6@@ -44352,8 +44352,9 @@ namespace VegaISA 7 int lane_A = i + M * (block + B * (k / K_L)); 8 int lane_B = j + N * (block + B * (k / K_L)); 9 int item = k % K_L; 10- result[i][j] += 11- src0[item][lane_A] * src1[item][lane_B]; 12+ result[i][j] = 13+ std::fma(src0[item][lane_A], src1[item][lane_B], 14+ result[i][j]); 15 } 16 } 17 } 之后测试就正常了，我将这笔 bugfix，贡献到了上游社区，目前已被合并：arch-vega: Improve MFMA precision to match MI300X hardware\n","permalink":"https://zevorn.cn/posts/32/","summary":"前段时间社区的雾佬发了一篇 使用 gem5 模拟 MI300X 的知乎 1 ，正好我最近在验证 AMDGPU 的浮点运算精度，就想着对比一下 gem5 MI300X 的 model 浮点精度和真实硬件有没有什么差异。 这里推荐使用服务器或者工作站来运行 gem5，个人电脑资源可能不太够用。 主要参考雾佬的文章，以及官方提供的文档 Full System AM","title":"使用 gem5 模拟 MI300X，立省 10 万块？"},{"content":"QEMU 的 softfloat 源码位于 fpu/ 和 include/fpu/ 路径中，代码最初源自 Berkeley SoftFloat IEC/IEEE 浮点运算包的 2a 版本（SoftFloat-2a），后续经过 QEMU 项目贡献者修改。\n目前我已经为 softfloat 添加了 tfloat32 和 float8e4m3 和 float8e5m2 的支持，可以从下面的仓库获取：\n1git clone -b neural-network-softfloat https://gitee.com/gevico/qemu.git 源文件结构如下：\n1include/fpu 2|-- softfloat-helpers.h # standalone helpers，用于初始化，比如设置舍入模式、异常模式等 3|-- softfloat-macros.h # QEMU 浮点支持宏 4|-- softfloat-types.h # 定义浮点精度和格式 5`-- softfloat.h # 对外使用的浮点接口，比如浮点运算，浮点精度转换 6 7fpu/ 8|-- meson.build # 构建脚本 9|-- softfloat-parts-addsub.c.inc 10|-- softfloat-parts.c.inc 11|-- softfloat-specialize.c.inc # 浮点运算的一些特殊实现 12`-- softfloat.c # 浮点功能的核心源码 为 softfloat 添加一个新的浮点精度，分下面几步完成：\n在 softfloat-types.h 中增加新的浮点精度定义 在 softfloat.c 中实现关键浮点运算函数 在 softfloat-specialize.c.inc 中实现特殊的浮点运算函数 在 softfloat.h 中添加浮点精度的接口声明 下面我们以添加 tfloat32（Tensor float32， TF32） 浮点精度为例，进行讲解。\n一、在 softfloat-types.h 中增加新的浮点精度定义 tfloat32 的浮点格式如下：\ntfloat32 内部计算时仍然按照 float32 进行运算，只不过输出的浮点值，仅保留 float32 小数位的前 10 位。\n我们来添加 tfloat32 的定义。\n1// path: include/fpu/softfoloat-types.h 2 3/* 4 * Software neural-network floating-point types. 5 */ 6typedef uint16_t bfloat16; 7typedef uint32_t tfloat32; /* 添加 TF32 的定义 */ 8 9/* 一些操作宏，参考其他精度定义 */ 10#define tfloat32_val(x) (x) 11#define make_tfloat32(x) (x) 二、在 softfloat.c 中实现关键浮点运算函数 为了提高可移植性和代码复用，softfloat 采用 packet 和 unpacket 的形式，对浮点格式进行解包和打包，具体运算是将 浮点值解包以后，对浮点位、指数位、小数位进行分别计算。\n因此我们需要先添加 tfloat32 的 float format 的定义，代码如下：\n1// path: fpu/softfloat.c 2 3/* Expand fields based on the size of exponent and fraction */ 4#define FLOAT_PARAMS_(E) \\ 5 .exp_size = E, \\ 6 .exp_bias = ((1 \u0026lt;\u0026lt; E) - 1) \u0026gt;\u0026gt; 1, \\ 7 .exp_re_bias = (1 \u0026lt;\u0026lt; (E - 1)) + (1 \u0026lt;\u0026lt; (E - 2)), \\ 8 .exp_max = (1 \u0026lt;\u0026lt; E) - 1 9 10#define FLOAT_PARAMS(E, F) \\ 11 FLOAT_PARAMS_(E), \\ 12 .frac_size = F, \\ 13 .frac_shift = (-F - 1) \u0026amp; 63, \\ 14 .round_mask = (1ull \u0026lt;\u0026lt; ((-F - 1) \u0026amp; 63)) - 1 15 16/* 定义 tfloat32 的 floatfmt */ 17static const FloatFmt tfloat32_params = { 18 FLOAT_PARAMS(8, 23) 19}; 20 21/* Unpack a float to parts, but do not canonicalize. */ 22static void unpack_raw64(FloatParts64 *r, const FloatFmt *fmt, uint64_t raw) 23{ 24 const int f_size = fmt-\u0026gt;frac_size; 25 const int e_size = fmt-\u0026gt;exp_size; 26 27 *r = (FloatParts64) { 28 .cls = float_class_unclassified, 29 .sign = extract64(raw, f_size + e_size, 1), 30 .exp = extract64(raw, f_size, e_size), 31 .frac = extract64(raw, 0, f_size) 32 }; 33} 34 35/* Pack a float from parts, but do not canonicalize. */ 36static uint64_t pack_raw64(const FloatParts64 *p, const FloatFmt *fmt) 37{ 38 const int f_size = fmt-\u0026gt;frac_size; 39 const int e_size = fmt-\u0026gt;exp_size; 40 uint64_t ret; 41 42 ret = (uint64_t)p-\u0026gt;sign \u0026lt;\u0026lt; (f_size + e_size); 43 ret = deposit64(ret, f_size, e_size, p-\u0026gt;exp); 44 ret = deposit64(ret, 0, f_size, p-\u0026gt;frac); 45 return ret; 46} 47 48/* 定义解包函数 */ 49static void QEMU_FLATTEN tfloat32_unpack_raw(FloatParts64 *p, tfloat32 f) 50{ 51 unpack_raw64(p, \u0026amp;tfloat32_params, f); 52} 53 54static void tfloat32_unpack_canonical(FloatParts64 *p, tfloat32 f, 55 float_status *s) 56{ 57 tfloat32_unpack_raw(p, f); 58 parts_canonicalize(p, s, \u0026amp;tfloat32_params); 59} 60 61/* 定义组包函数，组包时只保留小数位前10位 */ 62static tfloat32 QEMU_FLATTEN tfloat32_pack_raw(const FloatParts64 *p) 63{ 64 /* The fraction is kept to only its first 10 bits. */ 65 return pack_raw64(p, \u0026amp;tfloat32_params) \u0026amp; 0xFFFC0000; 66} 67 68/* 增加组包的舍入方式的封装 */ 69static tfloat32 tfloat32_round_pack_canonical(FloatParts64 *p, 70 float_status *s) 71{ 72 parts_uncanon(p, s, \u0026amp;tfloat32_params); 73 return tfloat32_pack_raw(p); 74} 然后我们添加具体的计算函数，数量比较多，我们以 addsub 和精度转换为例：\n1static tfloat32 QEMU_FLATTEN 2tfloat32_addsub(tfloat32 a, tfloat32 b, float_status *status, bool subtract) 3{ 4 FloatParts64 pa, pb, *pr; 5 6 tfloat32_unpack_canonical(\u0026amp;pa, a, status); 7 tfloat32_unpack_canonical(\u0026amp;pb, b, status); 8 pr = parts_addsub(\u0026amp;pa, \u0026amp;pb, status, subtract); 9 10 return tfloat32_round_pack_canonical(pr, status); 11} 12 13tfloat32 tfloat32_add(tfloat32 a, tfloat32 b, float_status *status) 14{ 15 return tfloat32_addsub(a, b, status, false); 16} 17 18tfloat32 tfloat32_sub(tfloat32 a, tfloat32 b, float_status *status) 19{ 20 return tfloat32_addsub(a, b, status, true); 21} 22 23/* 以 tfloat32 和 float32 的精度相互转换为例 */ 24 25float32 tfloat32_to_float32(tfloat32 a, float_status *s) 26{ 27 FloatParts64 p; 28 29 tfloat32_unpack_canonical(\u0026amp;p, a, s); 30 parts_float_to_float(\u0026amp;p, s); 31 return float32_round_pack_canonical(\u0026amp;p, s); 32} 33 34tfloat32 float32_to_tfloat32(float32 a, float_status *s) 35{ 36 FloatParts64 p; 37 38 float32_unpack_canonical(\u0026amp;p, a, s); 39 parts_float_to_float(\u0026amp;p, s); 40 return tfloat32_round_pack_canonical(\u0026amp;p, s); 41} 三、在 softfloat-specialize.c.inc 中实现特殊的浮点运算函数 需要实现的函数只有两个，tfloat32_is_quiet_nan() 和 tfloat32_is_signaling_nan()。\n1/*---------------------------------------------------------------------------- 2| Returns 1 if the tfloat32 value `a\u0026#39; is a quiet 3| NaN; otherwise returns 0. 4*----------------------------------------------------------------------------*/ 5 6bool tfloat32_is_quiet_nan(tfloat32 a_, float_status *status) 7{ 8 if (no_signaling_nans(status)) { 9 return tfloat32_is_any_nan(a_); 10 } else { 11 uint32_t a = tfloat32_val(a_); 12 if (snan_bit_is_one(status)) { 13 return (((a \u0026gt;\u0026gt; 22) \u0026amp; 0x1FF) == 0x1FE) \u0026amp;\u0026amp; (a \u0026amp; 0x003FFFFF); 14 } else { 15 return ((uint32_t)(a \u0026lt;\u0026lt; 1) \u0026gt;= 0xFF800000); 16 } 17 } 18} 19 20/*---------------------------------------------------------------------------- 21| Returns 1 if the tfloat32 value `a\u0026#39; is a signaling 22| NaN; otherwise returns 0. 23*----------------------------------------------------------------------------*/ 24 25bool tfloat32_is_signaling_nan(tfloat32 a_, float_status *status) 26{ 27 if (no_signaling_nans(status)) { 28 return 0; 29 } else { 30 uint32_t a = tfloat32_val(a_); 31 if (snan_bit_is_one(status)) { 32 return ((uint32_t)(a \u0026lt;\u0026lt; 1) \u0026gt;= 0xFF800000); 33 } else { 34 return (((a \u0026gt;\u0026gt; 22) \u0026amp; 0x1FF) == 0x1FE) \u0026amp;\u0026amp; (a \u0026amp; 0x003FFFFF); 35 } 36 } 37} 四、在 softfloat.h 中添加浮点精度的接口声明 除了要声明在 softfloat.c 和 softfloat-specialize.c.inc 中定义的 tfloat32 相关的函数，在 softfloat.h 头文件里还需要添加一些常用的内联函数。\n代码实现如下, 摘要重要部分：\n1static inline tfloat32 tfloat32_abs(tfloat32 a) 2{ 3 /* Note that abs does *not* handle NaN specially, nor does 4 * it flush denormal inputs to zero. 5 */ 6 return make_tfloat32(tfloat32_val(a) \u0026amp; 0x7fffffff); 7} 8 9static inline tfloat32 tfloat32_chs(tfloat32 a) 10{ 11 /* Note that chs does *not* handle NaN specially, nor does 12 * it flush denormal inputs to zero. 13 */ 14 return make_tfloat32(tfloat32_val(a) ^ 0x80000000); 15} 16 17static inline bool tfloat32_is_infinity(tfloat32 a) 18{ 19 return (tfloat32_val(a) \u0026amp; 0x7fffffff) == 0x7f800000; 20} 21 22static inline bool tfloat32_is_neg(tfloat32 a) 23{ 24 return tfloat32_val(a) \u0026gt;\u0026gt; 31; 25} 26 27static inline bool tfloat32_is_zero(tfloat32 a) 28{ 29 return (tfloat32_val(a) \u0026amp; 0x7fffffff) == 0; 30} 31 32static inline bool tfloat32_is_any_nan(tfloat32 a) 33{ 34 return ((tfloat32_val(a) \u0026amp; ~(1 \u0026lt;\u0026lt; 31)) \u0026gt; 0x7f800000UL); 35} 36 37static inline bool tfloat32_is_zero_or_denormal(tfloat32 a) 38{ 39 return (tfloat32_val(a) \u0026amp; 0x7f800000) == 0; 40} 41 42static inline bool tfloat32_is_normal(tfloat32 a) 43{ 44 return (((tfloat32_val(a) \u0026gt;\u0026gt; 23) + 1) \u0026amp; 0xff) \u0026gt;= 2; 45} 46 47static inline bool tfloat32_is_denormal(tfloat32 a) 48{ 49 return tfloat32_is_zero_or_denormal(a) \u0026amp;\u0026amp; !tfloat32_is_zero(a); 50} 51 52static inline bool tfloat32_is_zero_or_normal(tfloat32 a) 53{ 54 return tfloat32_is_normal(a) || tfloat32_is_zero(a); 55} 56 57static inline tfloat32 tfloat32_set_sign(tfloat32 a, int sign) 58{ 59 return make_tfloat32((tfloat32_val(a) \u0026amp; 0x7fffffff) | (sign \u0026lt;\u0026lt; 31)); 60} 61 62static inline bool tfloat32_eq(tfloat32 a, tfloat32 b, float_status *s) 63{ 64 return tfloat32_compare(a, b, s) == float_relation_equal; 65} 66 67static inline bool tfloat32_le(tfloat32 a, tfloat32 b, float_status *s) 68{ 69 return tfloat32_compare(a, b, s) \u0026lt;= float_relation_equal; 70} 71 72static inline bool tfloat32_lt(tfloat32 a, tfloat32 b, float_status *s) 73{ 74 return tfloat32_compare(a, b, s) \u0026lt; float_relation_equal; 75} 76 77static inline bool tfloat32_unordered(tfloat32 a, tfloat32 b, float_status *s) 78{ 79 return tfloat32_compare(a, b, s) == float_relation_unordered; 80} 81 82static inline bool tfloat32_eq_quiet(tfloat32 a, tfloat32 b, float_status *s) 83{ 84 return tfloat32_compare_quiet(a, b, s) == float_relation_equal; 85} 86 87static inline bool tfloat32_le_quiet(tfloat32 a, tfloat32 b, float_status *s) 88{ 89 return tfloat32_compare_quiet(a, b, s) \u0026lt;= float_relation_equal; 90} 91 92static inline bool tfloat32_lt_quiet(tfloat32 a, tfloat32 b, float_status *s) 93{ 94 return tfloat32_compare_quiet(a, b, s) \u0026lt; float_relation_equal; 95} 96 97static inline bool tfloat32_unordered_quiet(tfloat32 a, tfloat32 b, 98 float_status *s) 99{ 100 return tfloat32_compare_quiet(a, b, s) == float_relation_unordered; 101} 102 103#define tfloat32_zero 0 104#define tfloat32_half 0x3f000000 105#define tfloat32_one 0x3f800000 106#define tfloat32_one_point_five 0x3fc00000 107#define tfloat32_two 0x40000000 108#define tfloat32_three 0x40400000 109#define tfloat32_infinity 0x7f800000 后续文章，我们将讲解如何基于 qemu 的 fpu 测试框架，验证我们新增浮点精度的正确性。\n","permalink":"https://zevorn.cn/posts/31/","summary":"QEMU 的 softfloat 源码位于 fpu/ 和 include/fpu/ 路径中，代码最初源自 Berkeley SoftFloat IEC/IEEE 浮点运算包的 2a 版本（SoftFloat 2a），后续经过 QEMU 项目贡献者修改。 目前我已经为 softfloat 添加了 tfloat32 和 float8e4m3 和 float8e5","title":"为 QEMU softfloat 添加用于神经网络计算的浮点精度"},{"content":" 本文首发于微信公众号: GTOC\n业界经常使用量化的手段来提高大模型的训练和推理效率和节省成本，因此衍生了很多浮点精度和格式，比如 TF32、BF16、FP8、FP4 等。\n在 AI 芯片的虚拟原型平台开发中，对于各类 FPU 硬件的 model 建模，一般采用软浮点的方式来模拟，常使用 C/C++ 来实现，在保证准确性的同时，性能也不会太差。\n常见的模拟器，比如 QEMU TCG 使用 Berkeley SoftFloat-3 开源库（遵循 IEEE 754）来模拟浮点计算。\n经过社区的大量重构后，目前 QEMU SoftFloat 为 16/32/64 位的浮点运算提供了统一的实现路径，并支持了 NaN 传播规则以及舍入溢出模式的可配置化，方便客户机 FPU 的模拟。\n但对于 4/8 位的浮点精度还不支持。\nGEM5 同样集成 Berkeley SoftFloat-3 开源库实现跨平台的浮点运算，确保结果符合 IEEE 754 标准。\n本文优先介绍 LLM 常用的浮点格式。\n一、浮点标准介绍。 由于浮点的值可以这样表示：\n也就是浮点数的实际值，等于符号位（sign bit）乘以指数偏移值（exponent bias）再乘以分数值（fraction）。\n因此在计算机中，也可以拆成这三部分来表示浮点。\n按照 IEEE 754 标准的定义，浮点格式的二进制形式，由符号位（sign）、指数位（exponent）、小数位（fraction）三部分组成，按照符号数值表示法的格式存储（小端序）。\n所谓符号数值表示法，指的是在计算机进行运算时，需要将负数编码至二进制形式中。\n如上图所示，符号位，占据最高有效位，0 代表正数，1 代表负数；指数位，占据次高有效位的 e 个 bits ；小数位，占据最低有效位的 f 个 bits。\ne 和 f 的具体值，因浮点精度和格式的变化而不同。\n值得一提的是指数的计算。\n为了方便，IEEE 标准定义了指数的实际值加上固定的偏移值的办法来表示浮点数的指数。\n固定偏移值为 2^(e-1) - 1 ，e 表示存储指数的比特的长度。\n最后计算的值，称为指数偏移值（exponent bias），代表指数域的编码值。\n这样定义的好处是，可以用长度为 e 个比特的无符号整数，来表示所有的指数取值。\n这使得两个浮点数的指数大小的比较，更为容易（实际上，可以按照字典次序，比较两个浮点表示的大小）。\n这种移码表示的指数部分，中文也称作阶码。\n如果指数域的编码值范围在 (0, 2^e - 2) 以内，且在科学计数法的方式下，小数位的最高有效位为 1 ，那么这个浮点数将被称为规约形式的浮点数。“规约”是指用唯一确定的浮点形式去表示一个值。\n以单精度浮点的格式表示十进制数 5.0 为例：\n其二进制表示为 101.0，科学计数法表示 1.01 * 2^2。此时：\n整数部分为 1（符合“最高有效位为 1 ”的条件），被隐含在分数部分；指数实际值为 2 ，分数部位的小数部分为 01（即 0.012）。\n由于单精度浮点数的固定偏移值为 127（2^7 - 1），因此指数编码值为 2 + 127 = 129。这个值的范围在 (0, 254) 以内。\n现在我们来拼接这个浮点数的完整二进制表示：\n这个值，是 5.0 的唯一归约形式。\n规约化浮点数适用于大多数普通的浮点运算，提供了广泛的数值范围和较高的精度。例如，在单精度（32位）浮点数中，只有23位用于存储小数部分，但实际上表示的是24位精度。\n除了规约化浮点数，还有非规约化浮点数。\n非规约化浮点数的定义是：指数部分的编码值是0，分数部分非零。一般是某个数字相当接近零时才会使用非规约型式来表示。\nIEEE 754标准规定：非规约形式的浮点数的指数偏移值比规约形式的浮点数的指数偏移值小 1 。\n例如，最小的规约形式的单精度浮点数的指数部分编码值为 1 ，指数的实际值为 -126 ；而非规约的单精度浮点数的指数域编码值为 0 ，对应的指数实际值也是 -126 而不是 -127。\n实际上非规约形式的浮点数仍然是有效可以使用的，只是它们的绝对值已经小于所有的规约浮点数的绝对值；即所有的非规约浮点数比规约浮点数更接近 0 。\n下面给出归约形式和非归约定形式浮点的差异总结：\n还有一些浮点的特殊值，需要介绍一下：\n如果指数是 0 并且尾数的小数部分是 0 ，这个数 ±0（和符号位相关）;\n如果指数 = 2^e − 1，并且尾数的小数部分是 0 ，这个数是 ±∞（同样和符号位相关）;\n如果指数 = 2^e − 1，并且尾数的小数部分非 0，这个数表示为非数（NaN）。\n我们再聊聊浮点数的比较和舍入。\n浮点数基本上可以按照符号位、指数域、尾数域的顺序作字典比较。显然，所有正数大于负数；正负号相同时，指数的二进制表示法更大的其浮点数绝对值更大。\nIEEE 标准列出 4 种不同的浮点舍入方法：\n舍入到最接近：在一样接近的情况下偶数优先（Ties To Even，这是默认的舍入方式），会将结果舍入为最接近且可以表示的值，但是当存在两个数一样接近的时候，则取其中的偶数（在二进制中以 0 结尾的）。\n朝 +∞ 方向舍入：会将结果朝正无限大的方向舍入。\n朝 -∞ 方向舍入：会将结果朝负无限大的方向舍入。\n朝 0 方向舍入：会将结果朝0的方向舍入。\n二、常见浮点格式介绍。 除了 IEEE 754 标准中规定的双精度、单精度、半精度，在 LLM 量化中又扩展了 8 位精度、4 位精度（INT3/INT5/INT6 比较少用），浮点格式也类似。\n我们先讨论传统的双精度、单精度和半精度浮点格式。\n双精度浮点数（Double-precision floating-point）是用来表示浮点数的一种资料类型，在计算机中占据 64 比特（8字节）大小，因此也称为 float64 或 FP64 。\n在 IEEE 754 中这种 64 位二进制格式被正式称为 binary64 格式，而在 IEEE 754-1985 中被称为 double，即双精度。\n单精度浮点数（single-precision floating-point）是用来表示浮点数的一种数据类型，在计算机中占据 32 比特（4字节）大小，因此也称为 float32 或 FP32 。\n在 IEEE 754 中这种 32 位二进制格式被正式称为 binary32 格式，而在 IEEE 754-1985 中被称为 single ，即单精度。\n半精度浮点数（half-precision floating-point）是用来表示浮点数的一种数据类型，在计算机中占据16比特（2字节）大小，因此也称为 float16 或 FP16 。\n在 IEEE 754 中这种 16 位二进制格式被正式称为 binary16 格式，因为只有单精度浮点数的一半大小，也简称为 half ，即半精度。这种数据类型只适合存储对精度要求不高的数字，不适合用来计算。\n接着我们讨论 LLM 量化中扩展的特殊浮点精度。\n三、特殊浮点格式介绍。 下表整理了常见浮点精度的格式。\n对应的二进制位数示意图：\n我们展开来讲讲。\nTF32（Tensor Float 32）是英伟达专为机器学习设计的特殊数值类型，旨在替代 FP32，首次于 A100 GPU 中支持。\n其结构由 1 位符号位、8 位指数位（与 FP32 的指数位对齐）和 10 位小数位（与 FP16 的小数位对齐）组成，实际占用 19 位。\n这种设计使其在性能、数值表示范围和精度之间取得了平衡 —— 既保留了接近 FP32 的表示范围（得益于 8 位指数位），又通过减少小数位降低了计算和存储成本，同时精度能满足多数机器学习场景需求，适合在训练和推理中提升效率。\nBF16（Brain Float 16）是由 Google Brain 专为机器学习设计的浮点格式，其结构由 1 位符号位、8 位指数位和 7 位小数位组成。\n与 FP32 的关系：指数位与 FP32 相同（均为 8 位），因此表示范围和 FP32 一致，且两者之间转换便捷；但小数位仅 7 位（FP32 为 23 位），精度远低于 FP32。\n与 FP16 的关系：小数位少于 FP16（FP16 为 10 位），因此精度低于 FP16；但指数位多于 FP16（FP16 为 5 位），表示范围大于 FP16。\n在硬件支持上，NVIDIA 的 Ampere 架构及后续 GPU 才支持 BF16，适合在平衡存储 / 计算成本与模型性能的机器学习场景中使用。\nFP8-E4M3 和 FP8-E5M2 是两种 8 位浮点数据类型，旨在加速深度学习推理，由英伟达、ARM 和英特尔联合开发。\nFP8-E4M3：包含 1 位符号位、4 位指数位和 3 位尾数位,可存储的值最大约为 ±448 和 NaN，精度相对较高。\nFP8-E4M3 通常适用于神经网络的前向传播过程。因为在前向传播中，激活值和权重需要更高的精度来保证计算结果的准确性，FP8-E4M3 能较好地满足这一需求。\nFP8-E5M2：包含 1 位符号位、5 位指数位和 2 位尾数位，可存储的值最大约为 ±57344，还支持 ±inf 和 NaN。其指数位更多，动态范围更大，但由于尾数位较少，精度相对较低。\nFP8-E5M2 更适合用于神经网络的反向传播过程。反向传播中，梯度对精度的敏感度相对较低，但需要更大的动态范围来表示，FP8-E5M2 的特性使其能够更好地胜任。\n与 FP32 和 FP16 相比，FP8 数据类型占用的存储空间更小，能提高模型的吞吐量，减少延迟。对于计算密集型操作，如矩阵乘法和卷积，使用 FP8 可以在相同的硬件资源下处理更多的数据，从而提升计算效率。\n最后再聊聊 FP4 。\nFP4 有两种选择，NVFP4 和 MXFP4 。两者均为 4 位浮点数（FP4）格式，核心共性是都采用 E2M1 数据类型，即由 1 位符号位、2 位指数位、1 位尾数位组成，总位数为 4 位，适用于低精度计算场景以节省内存和提升效率。\n两者的差异主要体现在两个方面：\n量化块大小：NVFP4 的量化块大小为 1×16，意味着量化操作以 1 行 16 列的块为单位进行；MXFP4 的量化块大小为 1×32，量化单位是 1 行 32 列的块，更大的块可能影响量化粒度和精度保留效果。\n扩展因子类型：NVFP4 使用 E4M3 数据类型作为扩展因子，而 MXFP4 的扩展因子为 E8M0 类型。扩展因子的差异可能影响数值范围的扩展能力和精度补偿方式，进而影响整体计算的动态范围和准确性（NVFP4 精度优于 MXFP4）。\nFP4-E2M1 可以显著减少显存开销，因为每个浮点数仅占用 4 位，相比于 FP16（16位）或 FP32（32位）格式，这大大降低了存储需求。\n同时，它与特定硬件加速单元的结合，如 NVIDIA 的 Blackwell 架构中的 Tensor Cores，可以实现更高的计算吞吐量。\n但由于动态范围和精度的限制，直接使用 FP4-E2M1 可能会导致模型训练和推理时的精度损失。特别是在需要高精度计算的任务中，如某些复杂的语言模型任务，FP4 -E2M1 可能无法保持足够的精度。\n四、小节。 前面我们已经讲解了常见的浮点精度与格式。限于篇幅，在后续的文章，我们将讲解如何扩展 QEMU 的 SoftFloat 库，来支持更多的浮点精度。\n图片、资料来源：\n[1] 维基百科 - IEEE 754 [2] 知乎 - LLM 大模型之精度问题（FP16，FP32，BF16）详解与实践 [3] 知乎 - 大模型涉及到的精度有多少种？FP32、TF32、FP16、BF16、FP8、FP4、NF4、INT8都有什么关联，一文讲清楚 [4] scaleway Docs - Understanding the NVIDIA FP8 format [5] 论文 - FP8 Formats for Deep Learning [6] 论文 - LLM-FP4: 4-Bit Floating-Point Quantized Transformers\n","permalink":"https://zevorn.cn/posts/30/","summary":"本文首发于微信公众号: GTOC 业界经常使用量化的手段来提高大模型的训练和推理效率和节省成本，因此衍生了很多浮点精度和格式，比如 TF32、BF16、FP8、FP4 等。 在 AI 芯片的虚拟原型平台开发中，对于各类 FPU 硬件的 model 建模，一般采用软浮点的方式来模拟，常使用 C/C++ 来实现，在保证准确性的同时，性能也不会太差。 常见的模拟器","title":"浅析适用于 LLM 的 AI FPU 硬件的虚拟原型平台的浮点精度"},{"content":" 本文首发于微信公众号：GTOC 总结了「虚拟机系统与进程的通用平台」一书中，关于虚拟机导论的内容。\n一、抽象层次。 管理计算机系统复杂性的经典手段，是通过一些定义明确的接口，把系统划分成不同的抽象层次。不同的抽象层次，分工明确，一般越靠近底层的，越关注硬件实现；越靠近上层的，越关注业务实现。\n同时，抽象层次允许忽略或简化系统设计的底层实现细节，从而简化高层组件的设计。\n一个典型的例子是，操作系统对硬盘的抽象。\n硬盘被划分为不同的磁道和扇区，它的细节经过操作系统的抽象（文件系统+硬盘驱动），使应用程序看到的硬盘，是不同大小的文件集合。\n上层应用程序可以创建、读、写文件，但不需要关注硬盘具体由什么构造和实现的。\n明确的接口可以分解计算机的设计任务，使得硬件和软件设计，能够或多或少地独立开展工作。\n指令集（ISA）就是这样一个接口。\n举个例子，硬件团队设计了一款基于 RISC-V 指令集的处理器，编译器团队开发了一个将高级语言编译成该指令集的编译器，只要双方能够遵从该指令集的规范定义，编译出来的软件，就可以正确地在这个处理器上执行。\n计算机系统中另一个重要的标准化接口，是操作系统接口，它被定义为一组函数调用的集合。\n应用开发者不需要关注操作系统的内部实现，只需要遵从操作系统接口的约束即可开发自己的应用程序，这使得软硬件可以独立开发演进和升级。\n但定义明确的接口也有局限性，为不同规范设计的子系统或者组件，难以复用和协调协同工作，常见的是特定操作系统与指令集的相互绑定。\n一旦应用程序以二进制形式分发，便很难在按照不同指令集规范设计的硬件平台上运行。\n而在硬件/软件接口之下，硬件资源也会限制软件系统的灵活性。\n例如为单处理器或者存储共享的多处理器开发的操作系统，往往被设计成能直接管理硬件资源的方式。\n这种情况下，系统的硬件资源受单一的操作系统管理，这又反过来限制了系统的灵活性，比如，对上层应用软件的限制。\n特别是，在多用户或用户组共享的场景下，出于安全性和故障隔离方便的考虑，会进一步限制系统的灵活性。\n二、从接口的角度谈虚拟化。 虚拟化提供了一种放宽前述的限制，增加灵活性的方法。尤其是在资源利用、资源隔离、安全性方面提供更多的保证。\n当一个系统(或子系统)，例如处理器、存储器或输入/输出设备被虚拟化，它的接口和所有通过接口可见的资源，都被映射到实现它的真实系统的接口和资源上。\n形式上，虚拟化将一个真实的主机系统虚拟成多个不同的客户机系统，客户机系统与主机系统具有相同的实现细节，这是与抽象最大的不同。\n虚拟化不需要隐藏实现细节。\n我们再看一个虚拟盘的案例，来体会虚拟化是如何在同一抽象层提供不同的接口或者资源的。\n有些应用中需要将一个完整的大硬盘，分成许多小的虚拟盘。\n虚拟盘软件利用操作系统提供的文件抽象作为中间步，将每个虚拟盘都映射为真实盘的上的一个大文件。\n每个虚拟盘表面上都包含一些逻辑磁道和扇区(虽然容量比大硬盘中的少，但细节上别无二致），对虚拟盘的读写操作被映射为，在主机系统中对文件的读写操作，进而到对相应的真实盘的读写操作。\n三、从虚拟化到虚拟机。 虚拟化的概念不仅可以用于某个子系统，也可以用于整台机器（Machine）。\n虚拟机（Virtual Machine，VM），通过在真实机器上增加一层软件来支持所需实现的虚拟机体系结构。\n例如大家熟知的 VMWare ，可以在上面运行不同的操作系统，与宿主机共享硬件资源。\n通常，虚拟机可以绕开真实机器的兼容性限制和对硬件资源的限制，从而获得更高的软件可移植性和灵活性。\n虚拟机的应用场景非常丰富，比如多拷贝虚拟机可以在一个硬件平台上为个人或者用户组提供他们需要的操作系统环境，并且提供资源隔离和保障安全性方面的能力。\n一个大型的多处理器服务器，也可以被划分为多个虚拟服务器，并对服务器资源管理提供动态平衡。\n虚拟机也可以利用仿真技术提供跨平台的软件兼容性。\n例如一个实现了 x86 指令集的平台，可以被转换成一个可运行 RISC-V 指令集的虚拟平台。\n这种兼容性既可以在系统级实现（利用 QEMU 全系统模拟的能力，运行 Windows / Linux 系统），也可以在程序或者进程级提供（利用 QEMU 用户态模拟，运行 WPS 应用程序或某些游戏）。\n除了仿真，虚拟机还可提供动态、在线的二进制程序优化。\n最后，通过仿真，虚拟机还可以在支持现有的标准指令集的程序的同时，实现新的私有指令集，如超长指令字(Very Long Instruction Words,VLIWs)。\n除了为真实机器提供虚拟化能力，虚拟机在高级语言的跨平台方面的应用也很普遍。\n用这种高级语言编写的程序，被事先编译成针对虚拟机的二进制代码，然后任何实现这个虚拟机的真实机器，都可以运行已编译好的代码。比如 Java 的 JVM，再比如 Python 的 PVM 等。\n四、结语。 操作系统开发人员、语言设计人员、编译器开发人员和硬件设计人员都从各自的角度研究和构建虚拟机。\n虽然每一种虚拟机应用都有它独特的性质，但是它们的基本概念和技术在虚拟机范围内有很多共同点。\n由于各种不同的虚拟机体系结构和支撑技术是由不同的工作组开发的，统一虚拟机的知识体系、理解各种不同形式虚拟机的共同的支撑技术显得特别重要。\n在「虚拟机系统与进程的通用平台」一书中，采用统一的方法讲述虚拟机家族，讨论虚拟机的共同的支撑技术，通过探索它们的多种应用，说明它们的通用性。\n后续我们将通过更多文章，来和大家进行进一步地讨论。\n","permalink":"https://zevorn.cn/posts/29/","summary":"本文首发于微信公众号：GTOC 总结了「虚拟机系统与进程的通用平台」一书中，关于虚拟机导论的内容。 一、抽象层次。 管理计算机系统复杂性的经典手段，是通过一些定义明确的接口，把系统划分成不同的抽象层次。不同的抽象层次，分工明确，一般越靠近底层的，越关注硬件实现；越靠近上层的，越关注业务实现。 同时，抽象层次允许忽略或简化系统设计的底层实现细节，从而简化高层组","title":"从接口的角度谈虚拟化"},{"content":" 本文首发于微信公众号 GTOC 。 本文参考 QEMU 的 tracing 文档，相对路径为：docs/devel/tracing.rst\nQEMU 有一个很好用的调试工具 tracing，可以用来跟踪 QEMU 内部函数的执行情况，以及性能调优。\n比如追踪客户机程序的访存情况，可以将 QEMU 的 memory_region 的读写记录打印出来，只要注册了相应的 trace-event 。\n我们先讨论一个最简单的方法，把 tracing 用起来。\n一、快速上手。 在 QEMU 的启动选项中，通过增加 trace 参数，来指明要追踪的事件，这里以追踪 memory region 的访存事件为例：\n$ qemu-system-riscv64 -M virt --trace \u0026#34;memory_region_ops_*\u0026#34; # *号代表前面的字符作为匹配对象 ... 719585@1608130130.441188:memory_region_ops_read cpu 0 mr 0x562fdfbb3820 addr 0x3cc value 0x67 size 1 719585@1608130130.441190:memory_region_ops_write cpu 0 mr 0x562fdfbd2f00 addr 0x3d4 value 0x70e size 2 我们可以在 QEMU 源码的 system/trace-events 文件中找到 mr 相关的 trace-event ：\n# memory.c memory_region_ops_read(int cpu_index, void *mr, uint64_t addr, uint64_t value, unsigned size, const char *name) \u0026#34;cpu %d mr %p addr 0x%\u0026#34;PRIx64\u0026#34; value 0x%\u0026#34;PRIx64\u0026#34; size %u name \u0026#39;%s\u0026#39;\u0026#34; memory_region_ops_write(int cpu_index, void *mr, uint64_t addr, uint64_t value, unsigned size, const char *name) \u0026#34;cpu %d mr %p addr 0x%\u0026#34;PRIx64\u0026#34; value 0x%\u0026#34;PRIx64\u0026#34; size %u name \u0026#39;%s\u0026#39;\u0026#34; ... 如果要启用多个 trace-event ，只需要在启动选项里追加 \u0026ndash;trace name，为了避免参数冗长，可以将需要追踪的 trace-event 记录在一个配置文件，然后加载它：\n$ echo \u0026#34;memory_region_ops_*\u0026#34; \u0026gt;/tmp/events $ echo \u0026#34;kvm_*\u0026#34; \u0026gt;\u0026gt;/tmp/events $ qemu-system-riscv64 -M --trace events=/tmp/events ... 同时 tracing 也支持输出到文件，我们修改上面的 QEMU 命令：\n$ qemu-system-riscv64 -M --trace events=/tmp/events,file=/tmp/event.log ... 如果不想在 QEMU 启动选项里开启，我们也可以在 QEMU 的 monitor 中动态开启，这样更灵活一些，操作如下：\n$ qemu-system-riscv64 -M virt -monitor stdio -S -display none (qemu) trace-event memory_region_ops_read on (qemu) c ... memory_region_ops_write cpu 0 mr 0x55a289a24d80 addr 0x10000000 value 0x78 size 1 name \u0026#39;serial\u0026#39; 在 monitor 中使用 tracing 还有一个好处，你可以通过 tab 按键来补全命令，不必辛苦手动从源码中查阅每个组件支持的 trace-event 。\n也可以使用 info trace-events 命令查询支持的 trace 事件。\n也可以使用 trace-file 命令将追踪日志输出到文件。\ntracing 支持多种 backend，默认使用 QEMU 的 log 作为后端，简单调试的场景，不必手动构建 QEMU ，可以直接使用 Linux 或者 windows 软件仓提供的 QEMU。\n二、trace-event 介绍。 在 QEMU 源码的每一级目录，都可以添加 trace-events 文件，只需要在顶层 meson.build 目录里声明它的相对路径，就可以往里面添加自定义的 trace-event ：\nif have_system trace_events_subdirs += [ \u0026#39;accel/kvm\u0026#39;, \u0026#39;backends/tpm\u0026#39;, \u0026#39;ebpf\u0026#39;, \u0026#39;hw/arm\u0026#39;, ... ] endif 在 QEMU 构建过程中，每个 trace-events 文件将由 tracetool 脚本处理，自动在 [builddir\u0026gt;/trace/ 路径下生成 trace 相关的代码，主要包含以下文件：\ntrace-[子目录名].c\ntrace-[subdir].h\ntrace-dtrace-[subdir].h\ntrace-dtrace-[subdir].dtrace\ntrace-dtrace-[subdir].o\ntrace-ust-[subdir].h\n此处 [subdir] 表示将子目录路径中的 \u0026lsquo;/\u0026rsquo; 替换为 \u0026lsquo;_\u0026rsquo; 。\n例如，accel/kvm 变为 accel_kvm ，最终生成的 trace-[subdir].c 文件名即为 trace-accel_kvm.c 。\n各个 trace-events 文件会被合并成一个 trace-events-all 文件，同样生成在 [builddir]/trace/ 目录中。\n该文件也会被安装到 /usr/share/qemu 目录下。这个合并后的文件将由 QEMU 提供的 simpletrace.py 脚本用于后续分析简单跟踪数据格式的跟踪记录。\n源码树中的源文件，不会直接包含构建目录下生成的 trace 源码文件，而是通过 #include 引用本地的 trace.h 文件，且不带任何子目录路径前缀。\n例如 io/channel-buffer.c 会这样引用：\n#include \u0026#34;trace.h\u0026#34; 另外，我们必须手动创建 io/trace.h 文件，\n并在其中包含对应的 trace/trace-[subdir].h 文件，该文件在 [builddir] 中生成：\n$ echo \u0026#39;#include \u0026#34;trace/trace-io.h\u0026#34;\u0026#39; \u0026gt; io/trace.h 值得注意的是：虽然可以从源文件所在子目录之外引入 trace.h ，但通常不建议这样做。\n强烈建议所有 trace-event，都在使用它们的子目录中直接声明。\n唯一的例外是，在顶级目录的 trace-events 文件中定义了一些共享跟踪事件。\n顶级目录生成的跟踪文件会带有 trace/trace-root 前缀，而不仅仅是 trace ，这是为了避免当前目录中的 trace.h ，与顶级目录中的文件产生歧义。\n三、添加新的 trace-event 。 添加新的 trace-event 只需要两步，首先在相应目录的 trace-events 文件中声明 trace-event，然后在需要调试的目标源码内，添加这个事件的函数调用。\n以追踪 QEMU 内存的申请释放为例，trace-event 的格式如下：\nqemu_vmalloc(size_t size, void *ptr) \u0026#34;size %zu ptr %p\u0026#34; qemu_vfree(void *ptr) \u0026#34;ptr %p\u0026#34; 每个事件声明将以事件名称开头，然后是参数，最后是一个用于美观打印的格式字符串。\n格式字符串应反映跟踪事件中定义的类型。 tracing 只支持基础标量类型（char、int、long），不支持浮点类型（float、double）。\n特别注意对 int64_t 和 uint64_t 类型分别使用 PRId64 和 PRIu64，这确保了在 32 位和 64 位平台之间的可移植性。\n格式字符串不得以换行符结尾。由后端负责调整行尾以实现正确的日志记录。\n定义好 trace-event，直接从目标源码中调用它，示例如下：\n#include \u0026#34;trace.h\u0026#34; /* needed for trace event prototype */ void *qemu_vmalloc(size_t size) { void *ptr; size_t align = QEMU_VMALLOC_ALIGN; if (size \u0026lt; align) { align = getpagesize(); } ptr = qemu_memalign(align, size); /* 插入 trace-event，格式为：trace_[event-name] */ trace_qemu_vmalloc(size, ptr); return ptr; } 另外，若一个函数中存在多个跟踪事件，应在名称末尾添加唯一标识符加以区分。\n某些情况下可能需要执行相对复杂的计算来生成仅用作跟踪函数参数的值。\n此时可以使用下面这个函数来保护这类计算逻辑：\ntrace_event_get_state_backends() 当事件在编译时或运行时被禁用时，相关计算将被跳过。若事件在编译阶段已禁用，该检查将不会产生任何性能开销。\n示例代码如下。\n#include \u0026#34;trace.h\u0026#34; /* needed for trace event prototype */ void *qemu_vmalloc(size_t size) { void *ptr; size_t align = QEMU_VMALLOC_ALIGN; if (size \u0026lt; align) { align = getpagesize(); } ptr = qemu_memalign(align, size); if (trace_event_get_state_backends(TRACE_QEMU_VMALLOC)) { void *complex; /* some complex computations to produce the \u0026#39;complex\u0026#39; value */ trace_qemu_vmalloc(size, ptr, complex); } return ptr; } 以下几种情况，非常适合使用 tracing 调试：\n跟踪代码中的状态变化。代码中的关键点通常涉及状态变更，如启动、停止、分配、释放等。状态变化是理想的跟踪事件，因为它们能帮助理解系统执行过程。\n跟踪客户机操作。客户机的 I/O 访问（如读取设备寄存器）是良好的跟踪事件，可用于分析客户机交互行为。\n使用关联字段以便理解单行跟踪输出的上下文。例如，跟踪 malloc 返回的指针及其作为 free 参数的使用情况，这样就能匹配 malloc 和 free 操作。缺乏上下文的跟踪事件实用价值有限。\n四、 trace backend 介绍。 QEMU 的 tracing 采用前后端分离的设计，支持多种后端，除了上文提到的 log ，还支持更轻量的 simple，以及 ftrace 和 dtrace。\n我们还可以通过 tracetool 脚本添加更多 backend 支持。\n启用不同的 backend ，可以使用以下 QEMU 编译命令：\n$ ./configure --enable-trace-backends=simple,dtrace 通过运行 ./configure --help 查看支持的所有后端，若未显式地选择后端，配置将默认使用 log 后端。\n五、分析跟踪文件。 我们以 simple 后端为例，该后端通过独立线程，将二进制追踪日志写入文件，相比 log 后端具有更低的开销。\n同时 QEMU 源码仓中提供了离线追踪文件分析的 python 脚本。虽然功能可能不如特定平台或第三方追踪后端强大，但具有可移植性且无需特殊库依赖。\n使用 simpletrace.py 脚本进行格式化，需要用到 trace-events-all 文件和二进制跟踪文件：\n./scripts/simpletrace.py [trace-events-all] [trace-log] 必须确保使用的 trace-events-all 文件，与构建 QEMU 时的生成的相同，否则跟踪事件声明可能已发生变化，导致输出不一致。\n关注公众号 GTOC ，免费使用社区提供的知识库 目前已推出：QEMU/GEM5/编译器/Linux 在公众号后台菜单栏即可体验，常用请星标公众号\n","permalink":"https://zevorn.cn/posts/28/","summary":"本文首发于微信公众号 GTOC 。 本文参考 QEMU 的 tracing 文档，相对路径为：docs/devel/tracing.rst QEMU 有一个很好用的调试工具 tracing，可以用来跟踪 QEMU 内部函数的执行情况，以及性能调优。 比如追踪客户机程序的访存情况，可以将 QEMU 的 memory region 的读写记录打印出来，只要注册了","title":"浅析 QEMU 的调试利器 tracing 工具"},{"content":" PS: 本文首发于格维开源社区微信公众号 GTOC 。\n目前支持 RISCV 虚拟化扩展的硬件不是很多，对于想尝鲜的朋友，可以使用 QEMU 来模拟。下文给出详细的教程。\n基本思路是采用 QEMU 软件模拟一个 RISCV SoC（virt Machine），在上面运行 Ubuntu 发行版，然后在 Ubuntu 上使用虚拟化运行 Linux 。\n一、基本环境准备。\n以宿主机（x86-64）为 Ubuntu 操作系统环境为例，需要安装以下几款软件包：\nsudo apt update sudo apt install opensbi qemu-system-misc u-boot-qemu 如果你想体验最新的 QEMU ,可以在官网下载并手动编译。\n另外需要准备一个 RISC-V 版本的 Ubuntu 镜像，用于模拟 RISC-V 硬件虚拟化的使用环境。直接在 Ubuntu 官网下载即可（由于下载链接常更新，避免失效，本文不贴附链接），这里推荐使用 preinstalled 版本，不需要自己手动安装操作系统。\n镜像下载好以后，需要解压和扩容：\n# 解压下载好的镜像 xz -dk ubuntu-24.04.2-preinstalled-server-riscv64.img.xz # 镜像扩容 qemu-img resize -f raw ubuntu-24.04-preinstalled-server-riscv64.img +5G 使用如下命令启动 RISC-V Ubuntu 镜像：\nqemu-system-riscv64 \\ -machine virt -nographic -m 4096 -smp 32 \\ -kernel /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf \\ -device virtio-net-device,netdev=eth0 -netdev user,id=eth0,hostfwd=tcp::2222-:22 \\ -device virtio-rng-pci \\ -drive file=ubuntu-25.04-preinstalled-server-riscv64.img,format=raw,if=virtio 这里我们配置了 virt Machine 来运行客户机操作系统（virt 默认开启了对 H 扩展的支持），不需要图形界面，通过命令行终端打印客户机串口输出，并允许人机交互。\nQEMU 启动 RISC-V 的第一阶段 boot 是 OpenSBI，在 7.0 版本以前需要通过 -bios 选项指定，后续高版本，QEMU 默认自动加载，无需手动指定。第二阶段，通过 U-Boot 来引导 kernel 。\n我们还分配了 4 GiB 内存，32 个 vCPU 核心（u-boot 最大支持数量），另外我们还配置了网络直通，并把客户机的 22 端口映射到宿主机的 2222 端口，以便通过 ssh 等远程连接工具访问虚拟机。\n二、配置 RISC-V Ubuntu。\n成功启动 Ubuntu 以后，将会看到以下打印信息，我们使用默认的用户名 ubuntu 来登录，并修改初始密码 ubuntu 为你需要的密码，操作如下：\n... [ OK ] Started getty@tty1.service - Getty on tty1. [ OK ] Reached target getty.target - Login Prompts. Ubuntu 25.04 ubuntu ttyS0 ubuntu login: ubuntu # 输入用户名 Password: ubuntu # 输入密码，之后会提示你修改初始密码 ... Welcome to Ubuntu 25.04 (GNU/Linux 6.14.0-13-generic riscv64) 成功登录以后，我们简单验证一下客户机操作系统是否支持虚拟化：\ngrep isa /proc/cpuinfo isa : rv64imafdch_zicbom_zicboz_ziccrse_zicntr_zicsr_zifencei_zihintntl_zihintpause_zihpm_zawrs_zfa_zca_zcd_zba_zbb_zbc_zbs_sstc_svadu_svvptc ... 可以看到 32 个 CPU 的 isa 部分，都有 h 扩展的字符，这说明我们模拟的硬件是支持虚拟化扩展的。\n接着配置一下环境，为了方便使用虚拟化能力来运行 Linux，我们依旧可以通过 QEMU 来实现，先安装一些必要软件包（由于开启了网络直通，可以直接安装）：\nsudo apt update；sudo apt upgrade -y sudo apt install qemu-system 我们使用 Revyos 提供的 RISC-V 虚拟化测试镜像及脚本进行验证：\nsudo apt install -y wget # 如系统未安装 wget 工具，请先安装 wget https://mirror.iscas.ac.cn/revyos/extra/kvm_demo/rootfs_kvm_guest.img \\ https://mirror.iscas.ac.cn/revyos/extra/kvm_demo/start_vm.sh \\ https://mirror.iscas.ac.cn/revyos/extra/kvm_demo/Image chmod +x start_vm.sh; ./start_vm.sh # 启动 KVM 成功启动以后，输出如下：\n... [ 6.538768] Freeing unused kernel image (initmem) memory: 2200K [ 6.561827] Run /init as init process _ _ | ||_| | | _ ____ _ _ _ _ | || | _ \\| | | |\\ \\/ / | || | | | | |_| |/ \\ |_||_|_| |_|\\____|\\_/\\_/ Busybox Rootfs Please press Enter to activate this console. / # uname -a Linux (none) 6.6.30 #1 SMP Sat May 11 15:15:11 UTC 2024 riscv64 GNU/Linux / # 接下来，就可以愉快地开启 KVM 虚拟化体验体验之旅啦~\n","permalink":"https://zevorn.cn/posts/27/","summary":"PS: 本文首发于格维开源社区微信公众号 GTOC 。 目前支持 RISCV 虚拟化扩展的硬件不是很多，对于想尝鲜的朋友，可以使用 QEMU 来模拟。下文给出详细的教程。 基本思路是采用 QEMU 软件模拟一个 RISCV SoC（virt Machine），在上面运行 Ubuntu 发行版，然后在 Ubuntu 上使用虚拟化运行 Linux 。 一、基本环","title":"使用 QEMU 体验 RISC-V 虚拟化"},{"content":"前言 大约是六年前，我在大学生电子综合赛（一个全国赛事）的校内培训上，遇到了培训阶段的第一个难题：如何在 win10 上安装 Keil 和 Proteus，前者是嵌入式开发常用的 IDE，后者提供了电路仿真和模拟 MCU 的功能。\n当时实验室老师提供的 Keil 和 Proteus 的版本比较老，在代码补全和调试方面，体验欠佳。为了让自己编码体验好一些，我尝试在百度和 CSDN 上检索，如何安装更高版本的 Keil 和 Proteus。但具体怎么操作的细节我记不清了，记忆犹新的是百度和 CSDN 上满屏的广告。\n整个过程跌跌撞撞，花了好几天，但最后还是被我安装好了，彼时，我成了整个实验室最靓的仔，当别人还在用旧版本学习的时候，我已经通过自己的努力，获得更好的体验了，这种“多走一步”或者“多追求一点”的感觉，让我获得极大地满足感。接着，越来越多同学想体验新版本的 Keil 和 Proteus，开始不断向我寻求帮助。\n于是，我开始不厌其烦地为其他同学手把手的安装，后来安装次数多了，以至于我把整个安装流程都背了下来，但随着而来的，是激情和兴奋褪去后显露的枯燥。期间我也体验到了，被别人“提问”的感觉，为了把自己解放出来，我做了两件事：首先是编写了相关的技术文档，让别人可以跟着文档一步步安装；后来发现有的人还是不会，于是我就录制了安装视频，更详细地教大家如何安装。\n那么接下来，我想谈谈，我是如何通过“提问”和“努力独立解决问题”，来让自己获得成长的。\n什么是 “好的提问” 几年前 STC 刚推出 STC32G 系列的时候，姚老师委托我为这个芯片移植 FreeRTOS，并把成果开源出来，当时参与移植任务的，还有经验丰富的嵌入式工程师熊仔。熊仔是我的前辈，当时我刚刚毕业，经验不够丰富，在移植 FreeRTOS 的时候，自然而然，我们做了很多交流，其中不乏我向他请教问题的情景。移植过程，给我印象深刻的有两件事：\n第一件事是网上缺乏 Keil-C251 编译器的资料，我们想优化 FreeRTOS 上下文切换的汇编写法，想通过内联汇编的方式来编写这部分的程序，由于缺少资料，毫无头绪。\n当时我是怎么做的呢？首先查看了 Keil 的官方文档，在各种检索都无果以后，我想到了询问 STC 的工程师，相比个人开发者，他们有 Keil 的技术售后，这个问题应该很好解决。当时我把我的想法告诉了熊仔，他和我抱有同样的想法，于是一拍即合，我们整理了当前移植的成果，熊仔向 STC 工程师请教时，由于我们描述的比较详细，让工程师很快了解到我们卡主的点，于是轻松给出了我们想要的答案，进而帮我们进一步优化了上下文切换的代码实现。\n所以我的感受是，好的提问，首先要有好的提问思路，要向对方展示自己的思考过程，提高沟通交流的效率，减少双方对这个问题理解上的歧义。\n第二件事是任务调度的时候，我的代码在 Keil 仿真上运行和在实际硬件上跑的有差异。这是熊仔发现的，由于我手上没有精密示波器，很难通过点灯这类简单的现象观察到差异。从我的角度来说，最好的方式，还是通过代码静态 review，当时移植工作，我和熊仔都是独立开展的，所以为了验证，我的做法是，把我实现的思路和熊仔讲解了一遍，并通过代码切片排查的手段，让他协助我验证，整个过程我们交流了许多代码的实现细节，相互印证，对彼此代码都进行了一步优化，最后，我们一步步定位到出问题的地方，然后解决了它。\n通过第二件事，我认为，好的提问，是一个好的交流过程，对双方都能产生收益。\n目前我已将其开源：FreeRTOS for MCS-251\n对\u0026quot;通过 STFW 和 RTFM 独立解决问题\u0026quot;的看法 读书的时候搞嵌入式，我有一个很深的体会，软硬件知识繁杂琐碎，根本学不完，与其记住所有知识和代码，不如掌握“需要用到某个知识点的时候，能快速找到并复现和利用起来”的方法。\n顺着这个思路，那么学会使用搜索引擎和看懂 datasheet 或者官方文档的技能，是必不可少的，这可以说是最基本的技能。\n当我想通这一点以后，我的学习路线变得清晰了不少，我不再纠结某些细枝末节，而是专注那些核心知识，比如编写嵌入式程序时，不再对某个驱动的细节钻牛角尖，而是思考怎么处理任务的调度，来更好的完成功能的实现。\n后面我将其进一步实践，从零编写了一个 RTOS：AntOS。这套方法论，为我后面从事模拟器开发和学习 LLVM 都提供了坚实的基础，并且极大地提振了我的信心，让我自学技术的动力愈发十足。\n总结 其实想说的，前面都讲透了，最后小节，想补充的就一点，just do it，完事开头难，行动起来，就有成果~\n","permalink":"https://zevorn.cn/posts/26/","summary":"前言 大约是六年前，我在大学生电子综合赛（一个全国赛事）的校内培训上，遇到了培训阶段的第一个难题：如何在 win10 上安装 Keil 和 Proteus，前者是嵌入式开发常用的 IDE，后者提供了电路仿真和模拟 MCU 的功能。 当时实验室老师提供的 Keil 和 Proteus 的版本比较老，在代码补全和调试方面，体验欠佳。为了让自己编码体验好一些，我尝","title":"谈谈我对\"好的提问\"以及\"通过 STFW 和 RTFM 独立解决问题\"的看法"},{"content":"导言 作为 RISC-V 国际批准过程的一部分，此版本 1.0 被视为已冻结以供公众审查。 1.0 版被认为足够稳定，可以开始开发工具链以及功能模拟器和实现，包括在上游软件项目中，并且预计不会有不兼容的更改，除非在批准期间发现严重问题。一旦获得批准，该规范将获得 2.0 版。\n该规范包括完整的当前冻结向量指令集。其他在开发过程中考虑过但未出现在本文档中的说明不包括在审查和批准过程中，可能会被完全修改或放弃。标准向量扩展部分列出了标准向量扩展以及每个扩展支持哪些指令和元素宽度。\n实现定义的常量参数 每个支持向量扩展的 hart 定义了两个参数：\n● 任何操作可以产生或消耗的向量元素的最大比特大小，ELEN ≥ 8，必须是 2 的幂。\n● 单个向量寄存器的位数，VLEN≥ELEN，必须是2的幂，不能大于2^16。\n标准向量扩展（Section Standard Vector Extensions）和架构配置文件可能会对 ELEN 和 VLEN 设置进一步的限制:\n● 未来的扩展可能允许 ELEN \u0026gt; VLEN 通过使用来自多个向量寄存器的位来保存一个元素，但当前的提议不包括此选项。\n● VLEN 的上限允许软件知道索引将变为 16 位（65,536 的最大 VLMAX 出现在 LMUL=8 和 SEW=8 且 VLEN=65,536 时）。每个向量寄存器超过 64Kib 的任何未来扩展都将需要新的配置指令，这样使用旧配置指令的软件就不会看到更大的向量长度。\n向量扩展支持编写二进制代码，该代码在某些约束下将在具有不同 VLEN 参数值的 harts 上可移植地执行，前提是 harts 支持所需的元素类型和指令。\n可以编写代码来暴露实现参数的差异。通常，在 VLEN 或 ELEN 参数有任何差异的 hart 之间执行期间，无法迁移具有活动向量状态的线程上下文。\n矢量扩展编程模型 向量扩展将 32 个向量寄存器和 7 个非特权 CSR（vstart、vxsat、vxrm、vcsr、vtype、vl、vlenb）添加到基本标量 RISC-V ISA。\n4 个 CSR 编号 0x00B-0x00E 暂时保留给未来的向量 CSR，其中一些可能会镜像到 vcsr。\n向量寄存器（ Vector Registers） 向量扩展将 32 个架构向量寄存器 v0-v31 添加到基本标量 RISC-V ISA。每个向量寄存器都有一个固定的 VLEN 位状态。\nmstatus 中的向量上下文状态 向量上下文状态字段 VS 被添加到 mstatus[10:9] 并在 sstatus[10:9] 中隐藏。它的定义类似于浮点上下文状态字段 FS。\n当 mstatus.VS 设置为 Off 时，尝试执行任何向量指令或访问向量 CSR，会引发非法指令异常。当 mstatus.VS 设置为 Initial 或 Clean 时，执行任何更改向量状态的指令，包括向量 CSR，都会将 mstatus.VS 更改为 Dirty。实现也可以随时将 mstatus.VS 从 Initial 或 Clean 更改为 Dirty，即使向量状态没有变化。\nmstatus.VS 的精确设置是一种优化。软件通常会使用 VS 来减少上下文交换开销。\n如果 mstatus.VS 为 Dirty，则 mstatus.SD 为 1；否则，根据现有规范设置 mstatus.SD。实现可能有一个可写的 misa.V 字段。类似于处理浮点单元的方式，即使 misa.V 是明确的，mstatus.VS 字段也可能存在。\n当 misa.V 被清除时允许 mstatus.VS 存在，启用矢量仿真并简化在具有可写 misa.V 的系统中对 mstatus.VS 的处理。\nvsstatus 中的向量上下文状态 当存在管理程序扩展时，向量上下文状态字段 VS 被添加到 vsstatus[10:9]。它的定义类似于浮点上下文状态字段 FS。\n当 V=1 时，vsstatus.VS 和 mstatus.VS 都有效：尝试执行任何向量指令，或访问向量 CSR，当任一字段设置为 Off 时引发非法指令异常。\n当 V=1 且 vsstatus.VS 和 mstatus.VS 均未设置为 Off 时，执行任何更改向量状态的指令，包括向量 CSR，都会将 mstatus.VS 和 vsstatus.VS 更改为 Dirty。实现也可以随时将 mstatus.VS 或 vsstatus.VS 从 Initial 或 Clean 更改为 Dirty，即使向量状态没有变化。\n如果 vsstatus.VS 为 Dirty，则 vsstatus.SD 为 1；否则，根据现有规范设置 vsstatus.SD。 如果 mstatus.VS 为 Dirty，则 mstatus.SD 为 1；否则，根据现有规范设置 mstatus.SD。 对于具有可写 misa.V 字段的实现，即使 misa.V 被清除，vsstatus.VS 字段也可能存在。\n","permalink":"https://zevorn.cn/posts/25/","summary":"导言 作为 RISC V 国际批准过程的一部分，此版本 1.0 被视为已冻结以供公众审查。 1.0 版被认为足够稳定，可以开始开发工具链以及功能模拟器和实现，包括在上游软件项目中，并且预计不会有不兼容的更改，除非在批准期间发现严重问题。一旦获得批准，该规范将获得 2.0 版。 该规范包括完整的当前冻结向量指令集。其他在开发过程中考虑过但未出现在本文档中的说明","title":"RVV 向量扩展 v1.0 中文手册"},{"content":"今年我与开源世界的联系更多了，重新拾起 B 站 up 主的身份（ 绝对是泽文啦，即将 4000 粉丝），格维开源社区的活跃度再创新高，继续在基础软件领域深耕。\nQEMU [PATCH v2 0/2] riscv: Enhanced VSTART and VL checks for vector instructions [PATCH v1 0/1] riscv: Add vtype.vill FIELD macro definition [RFC PATCH v1 0/1] riscv: Add helper_print functions for printing intermediate results of complex instructions in RISC-V target [PULL 28/28] hw/arm/xilinx_zynq: Add various missing unimplemented devices Wine-CE 将 wine-ce 分支代码，rebase 到主线最新进展，并完善支持 wine-ce 的 patch ulan wiki 在幽兰上安装 Linux 版的 WPS 在幽兰上使用邮件参与开源社区 基于 C 语言构建最小虚拟机 泰晓科技社区 添加新文章 v1 ：废弃 QEMU xilinx_zynq 板卡的 ignore_memory_transaction_failures 格维开源社区 社区总人数突破 2400+，社区开源项目累计 1.6k Star，400+ fork 社区公众号更名为：GTOC 社区慢跑计划第 1 期顺利开展 上线「Learn KVM」开源课程 上线「QEMU 源码分析」开源课程 成立社区的 QQ 频道：pd87156915 成立社区新群：GEM5 | 格维开源社区（微信） 成立社区新群：编译器 | 格维开源社区（微信） 成立社区新群：QEMU | 格维开源社区（QQ 和 微信） 采访与演讲 华科线上宣讲第六期-Rust In QEMU 泽文专访，优化的男人 大学生职业规划演讲 - 赣南师范大学（科技学院） 在专业建设座谈会上介绍甲辰计划 - 赣南师范大学（科技学院） 一起聊聊泽文的职业发展和他的小副业（微信视频号 - 树熊熊熊 - 7 月 12 日） 开源活动 参加 2024 秋冬季开源操作系统训练营（清华大学） 参加 2024 冬季大模型与人工智能系统训练营（清华大学） ","permalink":"https://zevorn.cn/posts/24/","summary":"今年我与开源世界的联系更多了，重新拾起 B 站 up 主的身份（ 绝对是泽文啦，即将 4000 粉丝），格维开源社区的活跃度再创新高，继续在基础软件领域深耕。 QEMU PATCH v2 0/2 riscv: Enhanced VSTART and VL checks for vector instructions (\u003ca href=\"https://lore.kernel\"\u003ehttps://lore.kernel\u003c/a\u003e.","title":"2024 年总结：星光不负赶路人"},{"content":"发展背景 在 2021 年 KVM 的论坛会议，进行了一场以 “QEMU+Rust BoF” 为主题的讨论，会议摘要可以通过 QEMU blog 2022 Rust 板块找到： QEMU+Rust BoF, KVM Forum 2021。\n在这个会议上，对 QEMU 引入 Rust 的相关话题，进行了充分讨论，可以总结为以下几个方面：\n混合使用 C 和 Rust 的方法：使用 Rust FFI 绑定 QEMU 的 C 结构体/ API，mlureau 提到，bindgen 可以覆盖 FFI 部分，而为了安全性和符合 Rust 编程习惯的手动工作量较大（除非使用 gobject-introspection 等工具），对于设备模型，比如 vhost-user 和 vfio-user 这些进程外设备，可以考虑使用 Rust 来实现其用户空间部分；\n合入Rust 代码的基本要求：社区中是否有足够多的人熟悉审查 Rust 代码（这在当时还不行，但 Rust 可能会吸引新的贡献者，并且由于严格的检查和工具支持，对审阅者来说也更简单）；\nQEMU 核心抽象的问题：pbonzini 指出，核心抽象是最大的挑战，这会影响到 QEMU 如何与 Rust 集成，编译器检查不能告诉你是否选择了正确的抽象，mlureau 补充说，QEMU 不是公共库，因此可以重构代码而不必担心，实际上比其他语言更容易重构（如果能编译通过，它就能工作），另外一个严峻的问题，就是技术债，毕竟 QEMU 已经发展了近二十年，支持的设备繁杂而庞大;\nRust 带来的潜在好处：dwg 认为，改变核心抽象虽然困难，但这也是可能获得最大收益的地方。Rust允许我们将细微但重要的约束编码到接口类型中，而这些约束目前必须通过文档和惯例来处理;\n平台支持的问题：需要确定哪些已支持的主机（操作系统、发行版版本、CPU架构）无法构建和运行Rust代码，即我们排除了哪些用户（在当时，支持 Rust 的平台还不够丰富，无法覆盖 QEMU 的全部用户）。\n下游消费者的态度： Rust 包在 Debian 和 Fedora 中正在被处理，无论 QEMU 是否使用 Rust ，它们都不会成为第一个使用 Rust 的软件包，不同发行版对 Rust 包的处理方式相当复杂或者差异化，一些发行版要求包首先发布在 crates.io 上，并单独打包每个包，而其他发行版则接受直接包含所有依赖的方式;\nRust 作为非可选依赖的时间线：mlureau 认为，在最近的发行版 / 架构支持 QEMU 需求后的至少两年内，Rust 应该保持为可选依赖，另外需要考虑，如果某个受支持的发行版版本本身没有打包 Rust ，但最终用户可以通过 rustup 自行获取 Rust 编译器，这种情况是否被视为支持的问题。\n2024 年，QEMU 集成 Rust 的提案开始产出成熟的补丁，并在社区邮件列表引起广泛的讨论，详见：[RFC PATCH v1 0/6] Implement ARM PL011 in Rust。\n进程外设备（Out-of-process devices）是指这些设备的驱动程序和逻辑不在QEMU的主进程中运行，而是运行在独立的用户空间进程中。这种方式的好处包括：\n性能提升：减少内核和用户空间之间的切换开销。 灵活性：可以在独立的进程中实现复杂的设备逻辑，而不影响QEMU主进程的稳定性。 安全性：即使设备驱动程序出现错误，也不会直接影响QEMU主进程。 测试使用 Rust 建模的 PL011 设备 QEMU 9.2 版本正式支持了 Rust (\u0026gt;= 1.63.0，bindgen \u0026gt;= 0.60.0)，不过默认禁用 Rust 特性，可以使用以下命令使能 Rust ，我们以构建 target = aarch64 为例：\n1$ cargo install bindgen-cli 2$ ./configure --enable-system --target-list=aarch64-softmmu --enable-rust 使能 Rust 以后，将默认替换原来使用 C 编写的 PL011 外设模型，改用 Rust 版本。\n为了验证结果，我们修改源代码，增加打印：\n1// rust/hw/char/pl011/src/device.rs:234 2 pub fn write(\u0026amp;mut self, offset: hwaddr, value: u64) { 3- // eprintln!(\u0026#34;write offset {offset} value {value}\u0026#34;); 4+ eprintln!(\u0026#34;\\nwrite offset {offset} value {value}\u0026#34;); 编译 QEMU ，命令如下：\n1$ ninja -j$(nproc) -C build/ 准备一个 arm64 的裸机程序，命令如下：\n1$ git clone https://github.com/zevorn/arm64-baremetal-demo.git 配置交叉编译环境 aarch64-none-elf-gnu，以及 QEMU 可执行文件路径，修改 arm64-baremetal-demo 的 Makefile：\n// Makefile:10 QEMU_PATH=/home/zevorn/qemu/build # 填入 qemu 路径 CROSS_PREFIX=aarch64-none-linux-gnu- # 填入 gcc 路径+前缀，如果配置环境变量了，只写前缀即可 编译运行裸机程序，看到以下输出：\n1$ cd arm64-baremetal-demo 2$ make run 3 4write offset 0 value 72 5H 6write offset 0 value 101 7e 8write offset 0 value 108 9l 10write offset 0 value 108 11l 12write offset 0 value 111 13o 14write offset 0 value 32 15 16write offset 0 value 87 17W 18write offset 0 value 111 19o 20write offset 0 value 114 21r 22write offset 0 value 108 23l 24write offset 0 value 100 25d 26write offset 0 value 10 ","permalink":"https://zevorn.cn/posts/23/","summary":"发展背景 在 2021 年 KVM 的论坛会议，进行了一场以 “QEMU+Rust BoF” 为主题的讨论，会议摘要可以通过 QEMU blog 2022 Rust 板块找到： QEMU+Rust BoF, KVM Forum 2021 1 。 在这个会议上，对 QEMU 引入 Rust 的相关话题，进行了充分讨论，可以总结为以下几个方面： 1. 混合使用","title":"Rust In Qemu 的发展与现状"},{"content":"如果进程通过 dlopen() 手动加载动态库，某些情况下不能被 GDB 识别调试符号，这个时候可以通过 GDB 的 add-symbol-file file address 命令来手动添加调试符号，address 是动态库在进程中的 text 段 entry 地址，考虑动态库只有加载到进程中，才能得知真正的内存地址，因此我们可以通过 GDB 的 info files 命令，获取这个动态库在进程中的 text 段的 entry 地址。\n下面是操作流程：\n首先我们把目标动态库先反汇编，作为后续的比对验证对象:\nobjdump -d demo.so \u0026gt; demo.s 然后使用 gdb 调试目标进程，步骤如下：\n1(gdb) info files # 获取动态库 text 段 entry 地址 2... 30x00007f1fc9f3ba50 - 0x00007f1fca549490 is .text in /path/demo.so 4... 5(gdb) add-symbol-file /path/demo.so 0x00007f1fc9f3ba50 # 加载动态库调试信息（这里编译的动态库自带调试符号） 6add symbol table from file \u0026#34;demo.so\u0026#34; at 7 .text_addr = 0x7f1fc9f3ba50 8(y or n) u 9Reading symbols from demo.sp...done. 10(gdb) disassemble test_function # 查看任意动态库的函数，进行对比 11Dump of assembler code for funtion test_function: 120x...77adf0\u0026lt;+0\u0026gt;: sub $0x218.%rsp 130x...77adf7\u0026lt;+7\u0026gt;: mov %fs:0x28,%rax 14... 使用任意编辑器打开刚才反汇编好的 demo.s ，检索这个函数：\n000000077adf0 \u0026lt;test_function\u0026gt;: 77adf0: ... sub $0x218.%rsp 77adf7: ... mov %fs:0x28,%rax 可以看到，GDB 反汇编的地址相对偏移以及指令，和 objdump 反汇编的结果都可以对上，再测试打断点，也可以停在正确位置上面。\n","permalink":"https://zevorn.cn/posts/22/","summary":"如果进程通过 dlopen() 手动加载动态库，某些情况下不能被 GDB 识别调试符号，这个时候可以通过 GDB 的 add symbol file file address 命令来手动添加调试符号，address 是动态库在进程中的 text 段 entry 地址，考虑动态库只有加载到进程中，才能得知真正的内存地址，因此我们可以通过 GDB 的 info","title":"GDB 调试进程手动加载动态库的方法"},{"content":"简单教程 本文记录一个极简的 tmux 配置教程，来满足基本的开发需求。\n首先打开 tmux 配置文件：\n1vim ~/.tmux.conf 然后键入一下内容：\nset-option -g automatic-rename on set-option -g aggressive-resize on set-option -g mouse on set-option -g base-index 1 set-option -g pane-base-index 1 成品 tmux 配置 这里使用 https://github.com/gpakosz/.tmux 的配置，命令如下：\ncd git clone https://github.com/gpakosz/.tmux.git ln -s -f .tmux/.tmux.conf cp .tmux/.tmux.conf.local . 编辑 ~/.tmux.conf.local ，将 tmux_conf_copy_to_os_clipboard 的值改成 true，代码如下：\n# -- clipboard ----------------------------------------------------------------- # in copy mode, copying selection also copies to the OS clipboard # - true # - false (default) # on macOS, this requires installing reattach-to-user-namespace, see README.md # on Linux, this requires xsel or xclip tmux_conf_copy_to_os_clipboard=true 开启鼠标模式，将 #set -g mouse on 的注释去掉，代码如下：\n# start with mouse mode enabled set -g mouse on 在 Linux 中需要安装 xsel 或者是 xclip，命令如下：\n# ubuntu: sudo apt install xsel # sudo apt install clip # arch sudo pacman -S xclip 重新打开 tmux 或者重新加载配置 prefix + r\n解决 nvim 在 WSL 下因为粘贴板速度变慢的问题 配置上面共享粘贴板以后，不知道为啥 nvim 打开速度变得很慢，最后发现 clipboard 报了一些错误，之后在 CSDN 上找的一个帖子：\n原贴： neovim 添加 clipboard = “unnamedplus“ 出现打开速度变慢的解决办法\n笔者最近在 wsl 下重新配 neovim 时，发现打开速度会变得很慢。耗费巨大时间，终于找到原因。注释掉配置文件（init.lua or optioons.lua）下的，vim.opt.clipboard = \u0026ldquo;unnamedplus\u0026rdquo; 打开速度就会正常。\n当然！我们知道 vim.opt.clipboard = \u0026ldquo;unnamedplus\u0026rdquo; 是为了， Vim 在进行复制（yank）和粘贴（paste）操作时使用系统剪贴板的特定寄存器。只是注释掉会大大的降低nvim的使用。\n你可以试着注释掉 vim.opt.clipboard = \u0026ldquo;unnamedplus\u0026rdquo;，看看速度会不会恢复，解决问题步骤如下：\n下载 win32yank.exe， 命令如下： curl -sLo/tmp/win32yank.zip https://github.com/equalsraf/win32yank/releases/download/v0.1.1/win32yank-x64.zip unzip -p /tmp/win32yank.zip win32yank.exe \u0026gt; /tmp/win32yank.exe sudo chmod +x /tmp/win32yank.exe sudo mv /tmp/win32yank.exe /usr/local/bin/ sudo rm -rf /tmp/win32yank.zip 验证是否能正常的工作：\n# Set the clipboard echo \u0026#34;hello brave new world!\u0026#34; | win32yank.exe -i # Get the clipboard. win32yank -o 如果没有输出，那么请通过上面的链接手动下载 win32yank.exe 到 windows，然后再拷贝，命令如下：\n# 假设现在到 download 目录下并解压了 sudo mv /mnt/c/User/your-user-name/Downloads/win32yank/win32yank.exe /usr/local/bin/ 2 把下面的内容放到 init.lua ，代码如下：\nvim.g.clipboard = { name = \u0026#39;win32yank-wsl\u0026#39;, copy = { [\u0026#39;+\u0026#39;] = \u0026#39;win32yank.exe -i --crlf\u0026#39;, [\u0026#39;*\u0026#39;] = \u0026#39;win32yank.exe -i --crlf\u0026#39;, }, paste = { [\u0026#39;+\u0026#39;] = \u0026#39;win32yank.exe -o --lf\u0026#39;, [\u0026#39;*\u0026#39;] = \u0026#39;win32yank.exe -o --lf\u0026#39;, }, cache_enabled = true, } 但是在部分机器上，win32yank 的性能表现比较差，可以根据情况决定是否要启用这个功能。\n常用技巧 prefix 是 ctrl + b 按键。\n从 tmux 窗口复制内容到剪贴板: 安装 shift 按键，使用鼠标左键选择， 如果没有开启窗口滚动，可以键入 prefix + [； 创建新的 tmux 会话：prefix + : + new -s name； ","permalink":"https://zevorn.cn/posts/21/","summary":"简单教程 本文记录一个极简的 tmux 配置教程，来满足基本的开发需求。 首先打开 tmux 配置文件： 然后键入一下内容： 成品 tmux 配置 这里使用 \u003ca href=\"https://github.com/gpakosz/.tmux\"\u003ehttps://github.com/gpakosz/.tmux\u003c/a\u003e 的配置，命令如下： 编辑 /.tmux.conf.local ，将 tmux conf copy to os clipboard 的值改","title":"使用 tmux 进行远程开发"},{"content":" 本篇文章首发于 Nano Code 维基百科\n可以选择使用 git send-email 发送邮件补丁到社区，下面给出具体步骤。\n安装 Git Email ubuntu 默认不会安装完全版的 git ,因此需要我们在安装 git 以后（ ulan 默认安装了 git ），再安装 git-email : 1sudo apt update 2sudo apt install git-email 如果安装失败，比如遇到下面的问题： 可以手动添加 Git 官方的 PPA，再次安装：\n1sudo add-apt-repository ppa:git-core/ppa 2sudo apt update 3sudo apt install git-email 配置 Git Email 首先我们需要准备一个邮箱，笔者使用的是 yeah.net 邮箱，另外 qq /foxmail 邮箱、网易邮箱、腾讯企业邮箱等等也是可以的。 PS: 如果是国内环境，不推荐使用 google 邮箱，需要翻墙。\n需要开启邮箱的 smtp 服务，这个可以 google 找一下教程。注意事项如下：\n有一些邮箱会为第三方客户端设置独立的密码，这个需要先 copy 下来，后面需要用; 需要关注邮箱支持的 smtp 加密协议是 stl 还是 ssl,或者都支持，这个后续配置的时候要用到。 配置 git email 绑定自己的邮箱，这里推荐使用命令行，而不是直接修改 .gitconfig，避免配置出错 ：\n1git config --global sendemail.smtpEncryption \u0026lt;ssl or stl\u0026gt; 2git config --global sendemail.smtpServer \u0026lt;your smtp address\u0026gt; 3git config --global sendemail.smtpServerPort 587 4git config --global sendemail.smtpUser \u0026lt;your email\u0026gt; 5git config --global sendemail.smtpPass = \u0026lt;your pass or smtp pass\u0026gt; 上述命令中的 \u0026lt;\u0026gt; 内部的部分，需要你按照自己的实际邮箱信息来填写。\n配置完毕以后，可以 cat ~/.gitconfig 检查一下:\n1[sendemail] 2 smtpEncryption = \u0026lt;ssl or stl\u0026gt; 3 smtpServer = \u0026lt;your smtp address\u0026gt; 4 smtpServerPort = 587 5 smtpUser = \u0026lt;your email\u0026gt; 6 smtpPass = ***** 编辑补丁与发送 常用的补丁生成命令: 1git format-patch HEAD~\u0026lt;number\u0026gt; --subject-prefix=\u0026#34;your prefix\u0026#34; --thread --cover-letter -s --subject-prefix= 可以为补丁插入统一的 prefix,一般写 PATCH or RFC 或者组合; --cover-letter 生成一个序号为 0 的抬头文件，可以编辑它，增加一些补丁说明； -s 为补丁插入邮箱签名 发送邮件到邮件列表 以 qemu 为例，可以通过 ./script/get_maintainer.pl \u0026lt;patch-file\u0026gt; 来获取发送对象和抄送对象，具体发送邮件补丁的命令如下：\n1git send-email \\ 2--to=\u0026lt;email\u0026gt; \\ 3--cc=\u0026lt;email\u0026gt; \\ 4\u0026lt;your-patch\u0026gt; 邮件发送成功以后，终端会输出：\nResult: 250 PS: 可以先发送给自己的邮箱，检查能否正常发送。有些开源社区邮件列表，第一次向其发邮件，社区需要审核，如果没有在邮件列表中看到自己的邮件，请耐心等待一下。\n回复邮件 可以参考这篇文章：正确使用邮件列表参与开源社区的协作。\n","permalink":"https://zevorn.cn/posts/20/","summary":"本篇文章首发于 Nano Code 维基百科 1 可以选择使用 git send email 发送邮件补丁到社区，下面给出具体步骤。 安装 Git Email 1. ubuntu 默认不会安装完全版的 git ,因此需要我们在安装 git 以后（ ulan 默认安装了 git ），再安装 git email : 如果安装失败，比如遇到下面的问题： 可以手动添","title":"如何使用 git send-email 参与开源社区"},{"content":"即时编译（Just-In-Time Compilation，简称 JIT 编译）是一种动态编译技术，在程序运行期间将高级语言的源代码或者中间表示形式（如字节码）转换为机器代码。与传统的静态编译不同，JIT 编译器在程序运行时选择性地编译代码片段，并且可以根据运行时的具体情况优化这些代码。\n下面使用 Rust 构建一个最小 JIT 案例，参考 rustyjit 。\n基本思路 在程序进程运行时 mmap 一块内存 code_buffer（4KiB 对齐），权限设置为可读可写可执行； 将宿主机指令序列的二进制编码，发射到 code_buffer； 将 coder_buffer 中的程序段入口地址，强转为一个函数指针； 执行这个函数指针。 前置知识 FFI 编程 在进程中申请内存，需要调用 libc 库的接口，这涉及到 Rust FFI 编程 - libc crate。简单讲，我们需要 extern Rust 封装好的 libc 的 crate，从而获取我们需要的接口。主要如下：\nmmap() memset() mprotect() memalign() 注意有一些接口，在 Rust 中是封装好的变种方法，一般是关联函数，不能使用原生 API 名称。\n特征 Trait 特征 Trait 可以告诉编译器一个特定的类型所具有的、且能跟其它类型共享的特性。我们可以使用特征通过抽象的方式来定义这种共享行为，还可以使用特征约束来限定一个泛型类型必须要具有某个特定的行为。\n由于 Rust 是一个安全性极高的语言，我们需要依靠 Trait，来实现内存的可变访问。\n构建最小 JIT 首先定义一个结构体，它包含一个指向 code_buffer 的指针：\n1struct JitMemory { 2 code_buffer: *mut u8, 3} 然后我们编写一个 new 方法，实现内存申请与初始化：\n1std::mem; 2// 定义页面大小常量 3const PAGE_SIZE: usize = 4096; 4 5impl JitMemory { 6 fn new(num_pages: usize) -\u0026gt; JitMemory { 7 let code_buffer: *mut u8; 8 unsafe { 9 let size: usize = num_pages * PAGE_SIZE; 10 let mut _contents: *mut libc::c_void = 11 mem::MaybeUninit::\u0026lt;libc::c_void\u0026gt;::uninit().as_mut_ptr(); 12 // 分配内存并对齐到页面大小 13 libc::posix_memalign(\u0026amp;mut _contents, PAGE_SIZE, size); 14 // 设置内存权限为可执行、可读、可写 15 libc::mprotect( 16 _contents, 17 size, 18 libc::PROT_EXEC | libc::PROT_READ | libc::PROT_WRITE, 19 ); 20 21 // 初始化内存内容为 \u0026#39;RET\u0026#39; 指令 22 libc::memset(_contents, 0xc3, size); 23 code_buffer = mem::transmute(_contents); 24 } 25 26 JitMemory { code_buffer } 27 } 28} 接着我们实现指令二进制编码数据的字节码发射功能：\n1use std::ops::{Index, IndexMut}; 2 3// 实现 Index trait，允许通过索引访问内存 4impl Index\u0026lt;usize\u0026gt; for JitMemory { 5 type Output = u8; // 声明了 Index\u0026lt;usize\u0026gt; trait 的关联类型 Output 为 u8 类型 6 7 fn index(\u0026amp;self, _index: usize) -\u0026gt; \u0026amp;u8 { 8 unsafe { \u0026amp;*self.code_buffer.offset(_index as isize) } 9 } 10} 11 12// 实现 IndexMut trait，允许通过索引修改内存 13impl IndexMut\u0026lt;usize\u0026gt; for JitMemory { 14 fn index_mut(\u0026amp;mut self, _index: usize) -\u0026gt; \u0026amp;mut u8 { 15 unsafe { \u0026amp;mut *self.code_buffer.offset(_index as isize) } 16 } 17} 最后我们编写一段简单的汇编程序，将其发射到内存并运行：\n1fn run_jit() -\u0026gt; fn() -\u0026gt; i64 { 2 let mut jit: JitMemory = JitMemory::new(1); 3 4 // 在 JIT 内存中写入机器码，生成一个返回 3 的函数 5 jit[0] = 0x48; // mov RAX, 0x30 6 jit[1] = 0xc7; 7 jit[2] = 0xc0; 8 jit[3] = 0x30; 9 jit[4] = 0; 10 jit[5] = 0; 11 jit[6] = 0; 12 13 // 将内存指针转换为函数指针并返回 14 unsafe { mem::transmute(jit.code_buffer) } 15} 16 17fn main() { 18 // 生成并调用 JIT 编译的函数，并打印结果 19 let fun: fn() -\u0026gt; i64 = run_jit(); 20 println!(\u0026#34;{:#x}\u0026#34;, fun()); 21} 编译运行，看看输出结果：\n1$ cargo run 2... 30x30 ","permalink":"https://zevorn.cn/posts/19/","summary":"即时编译（Just In Time Compilation，简称 JIT 编译）是一种动态编译技术，在程序运行期间将高级语言的源代码或者中间表示形式（如字节码）转换为机器代码。与传统的静态编译不同，JIT 编译器在程序运行时选择性地编译代码片段，并且可以根据运行时的具体情况优化这些代码。 下面使用 Rust 构建一个最小 JIT 案例，参考 rustyjit","title":"基于 Rust 编写最小 JIT"},{"content":"构建最小虚拟机 前言 ARM 架构上的 KVM（Kernel-based Virtual Machine）是一种基于 Linux 内核的虚拟化技术，它允许用户空间程序通过系统调用来直接控制硬件，从而实现对虚拟机的管理。KVM 在 x86 平台上已经非常成熟，而在 ARM 架构上，KVM 也得到了广泛的支持和发展。\nPS: 本文首发在幽兰 Wiki - 构建最小虚拟机。\nARM 上的 KVM 特点 硬件支持：ARM 处理器近年来增加了对虚拟化的支持，包括 ARMv8-A 架构中的虚拟化扩展（Virtualization Extensions），这使得在 ARM 上实现 KVM 成为了可能。\n性能优势：KVM 提供了接近于裸机的性能，因为它是直接运行在硬件之上的，减少了传统虚拟化解决方案中的额外开销。 兼容性：KVM 支持多种操作系统作为客户机运行，包括但不限于 Linux、Windows 和各种 Unix-like 操作系统。 管理工具：KVM 可以与 QEMU（Quick EMUlator）配合使用，QEMU 提供了用户空间的组件来实现虚拟机的管理和设备模拟等功能。此外，像 libvirt 这样的工具提供了高级的虚拟机管理功能。 ARM KVM 的实现 在 ARM 平台上实现 KVM 虚拟化主要依赖以下几个组件：\nLinux 内核：ARM 版本的 Linux 内核包含了对 KVM 的支持，允许用户空间应用程序直接访问虚拟化硬件资源。 QEMU：一个开源的机器模拟器，可以用来启动虚拟机，并为它们提供模拟的硬件环境。 libvirt：一个用于管理虚拟化的软件集合，它可以简化 KVM 的管理和部署。 ARM KVM 的应用场景 ARM KVM 主要应用于以下几个场景：\n数据中心：随着 ARM 服务器芯片的发展，越来越多的数据中心开始采用 ARM 架构的服务器，KVM 可以帮助这些服务器实现高效的虚拟化。 嵌入式系统：ARM 芯片广泛应用于嵌入式系统中，KVM 可以提供一种灵活的方式来测试和开发嵌入式系统。 云计算平台：ARM KVM 可以用于构建云计算平台，提供高性能的虚拟化服务。 基于 C 编写一个最小虚拟机 本文将尝试通过 C 语言编写一个最小虚拟机，可以幽兰代码本上运行 ARM Aarch64 指令集的简单裸机程序。\n环境搭建 幽兰本已经内置了 GCC 开发编译套件，因此不需要再额外搭建开发环境，我们直接开始编写。\n实验代码 首先我们要包含必要的头文件： 1// 包含基本 libc 库头文件 2#include \u0026lt;err.h\u0026gt; 3#include \u0026lt;stdint.h\u0026gt; 4#include \u0026lt;stdio.h\u0026gt; 5#include \u0026lt;string.h\u0026gt; 6#include \u0026lt;stddef.h\u0026gt; 7#include \u0026lt;unistd.h\u0026gt; 8 9// 包含 mmap 头文件，用于申请客户机内存 10#include \u0026lt;sys/mman.h\u0026gt; 11 12// 包含 IO 相关头文件，用于访问文件 13#include \u0026lt;fcntl.h\u0026gt; 14#include \u0026lt;sys/ioctl.h\u0026gt; 15 16// 包含 KVM 相关头文件，用于配置虚拟机 17#include \u0026lt;linux/kvm.h\u0026gt; 然后进行 KVM 相关的初始化: 1int main(void) 2{ 3 ... 4 5\t/* 打开 kvm 文件，获取 fd */ 6 int kvmfd = open(\u0026#34;/dev/kvm\u0026#34;, O_RDWR); 7 if (kvmfd == -1) 8 err(1, \u0026#34;/dev/kvm\u0026#34;); 9 else 10 printf(\u0026#34;[%d] Open kvm succesfuly, fd is %d\\n\u0026#34;, ++step, kvmfd); 11 12 /* 确保 KVM API 版本是 12 */ 13 ret = ioctl(kvmfd, KVM_GET_API_VERSION, NULL); 14 if (ret \u0026lt; 0) 15 err(1, \u0026#34;KVM_GET_API_VERSION\u0026#34;); 16 if (ret != 12) 17 errx(1, \u0026#34;KVM_GET_API_VERSION %d, expected 12\u0026#34;, ret); 18 else 19 printf(\u0026#34;[%d] Get kvm version %d\\n\u0026#34;, ++step, ret); 20 21 /* 1. 获取 VM 的 id */ 22 int vmfd = ioctl(kvmfd, KVM_CREATE_VM, (unsigned long)0); 23 if (vmfd \u0026lt; 0) 24 err(1, \u0026#34;KVM_CREATE_VM\u0026#34;); 25 else 26 printf(\u0026#34;[%d] Create VM succesfuly, fd is %d\\n\u0026#34;, ++step, vmfd); 27\t... 28} 然后就是配置 VM 即对应的 vCPU： 1int main() 2{ 3\t... 4\t/* 2. 创建 vCPU */ 5 int vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0); 6 if (vcpufd \u0026lt; 0) 7 err(1, \u0026#34;KVM_CREATE_VCPU\u0026#34;); 8 else 9 printf(\u0026#34;[%d] Create vCPU succesfuly, fd is %d\\n\u0026#34;, ++step, vcpufd); 10 11 /* 3. 设置 vCPU 的类型，这里是 ARMv8 */ 12 // sample code can check the qemu/target/arm/kvm64.c 13 memset(\u0026amp;init, 0, sizeof(init)); 14 init.target = KVM_ARM_TARGET_GENERIC_V8; 15 ret = ioctl(vcpufd, KVM_ARM_VCPU_INIT, \u0026amp;init); 16 if (ret \u0026lt; 0) 17 err(1, \u0026#34;init vcpu type failed\\n\u0026#34;); 18 else 19 printf(\u0026#34;[%d] Set vCPU type is Aarch64 (ARMv8)\\n\u0026#34;, ++step); 20 21 /* 4. 为 kvm_run 分配内存 */ 22 mmap_size = ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL); 23 if (mmap_size \u0026lt; 0) 24 err(1, \u0026#34;KVM_GET_VCPU_MMAP_SIZE\u0026#34;); 25 if (mmap_size \u0026lt; sizeof(*run)) 26 errx(1, \u0026#34;KVM_GET_VCPU_MMAP_SIZE unexpectedly small\u0026#34;); 27 run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0); 28 if (run == MAP_FAILED) 29 err(1, \u0026#34;mmap vcpu\u0026#34;); 30 else 31 printf(\u0026#34;[%d] Init kvm_run successfuly!\\n\u0026#34;, ++step); 32} 编写一段简单的客户机程序，并拷贝到客户机内存： 1const unsigned code[] = { 2 // write \u0026#34;Hello\u0026#34; to port 0x996 3 0xd28132c4, // mov x4, #0x996 // #2454 4 0xd2800905, // mov x5, #0x48 // H 5 0x39000085, // strb w5, [x4] 6 0xd2800ca5, // mov x5, #0x65 // e 7 0x39000085, // strb w5, [x4] 8 0xd2800d85, // mov x5, #0x6c // ll 9 0x39000085, // strb w5, [x4] 10 0x39000085, // strb w5, [x4] 11 0xd2800de5, // mov x5, #0x6f // o 12 0x39000085, // strb w5, [x4] 13 0xd2800145, // mov x5, #0xa // \\n 14 0x39000085, // strb w5, [x4] 15}; 16#define MEM_SIZE 0x1000 17... 18int main() 19{ 20\t... 21 /* 5. 将程序拷贝到客户机内存 */ 22 ram = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE, 23 MAP_SHARED | MAP_ANONYMOUS, -1, 0); 24 if (!ram) 25 err(1, \u0026#34;allocating guest memory\u0026#34;); 26 memcpy(ram, code, sizeof(code)); 27 printf(\u0026#34;[%d] Load the vm running program to buffer \u0026#39;ram\u0026#39;\\n\u0026#34;, ++step); 28\t... 29} 初始化 userspace_memory_region 并设置 vCPU 寄存器 1int main() 2{ 3 ... 4 5 /* 6. 设置 the vm userspace memory region，并绑定 vmfd */ 6 struct kvm_userspace_memory_region region = { 7 .slot = 0, 8 .flags = 0, 9 .memory_size = MEM_SIZE, 10 .guest_phys_addr = PHY_ADDR, 11 .userspace_addr = (unsigned long)ram, 12 }; 13 ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, \u0026amp;region); 14 if (ret \u0026lt; 0) 15 err(1, \u0026#34;KVM_SET_USER_MEMORY_REGION\u0026#34;); 16 else 17 printf(\u0026#34;[%d] Set the vm userspace program ram to vm fd handler\\n\u0026#34;, ++step); 18 19 /* 7. 设置 vCPU 的 PC 寄存器，指向客户机第一条指令的内存地址 */ 20 reg.id = ARM64_CORE_REG(regs.pc); 21 reg.addr = (__u64)\u0026amp;guest_entry; 22 ret = ioctl(vcpufd, KVM_SET_ONE_REG, \u0026amp;reg); 23 if (ret \u0026lt; 0) 24 err(1,\u0026#34;KVM_SET_ONE_REG failed (pc)\u0026#34;); 25 else 26 printf(\u0026#34;[%d] Set vCPU PC, is 0x%x\\n\u0026#34;, ++step, (unsigned)guest_entry); 27 ... 28} 处理 VM 运行时的逻辑，增加 IO 模拟： 1#define PHY_ADDR 0x10000 2int main() 3{ 4\t... 5 /* 8. VM 运行时处理 */ 6 printf(\u0026#34;[%d] Run vCPU and print message:\\n\u0026#34;, ++step); 7 8 while (1) { 9 ret = ioctl(vcpufd, KVM_RUN, NULL); 10 if (ret \u0026lt; 0) 11 err(1, \u0026#34;KVM_RUN\u0026#34;); 12 13 switch (run-\u0026gt;exit_reason) { 14 case KVM_EXIT_MMIO: 15 if (run-\u0026gt;mmio.is_write \u0026amp;\u0026amp; run-\u0026gt;mmio.len == 1) { 16 printf(\u0026#34;%c\u0026#34;, run-\u0026gt;mmio.data[0]); 17 } 18 if (run-\u0026gt;mmio.data[0] == \u0026#39;\\n\u0026#39;) 19 return 0; 20 else 21 break; 22 case KVM_EXIT_FAIL_ENTRY: 23 errx(1, \u0026#34;KVM_EXIT_FAIL_ENTRY: hardware_entry_failure_reason = 0x%llx\u0026#34;, 24 (unsigned long long)run-\u0026gt;fail_entry.hardware_entry_failure_reason); 25 case KVM_EXIT_INTERNAL_ERROR: 26 errx(1, \u0026#34;KVM_EXIT_INTERNAL_ERROR: suberror = 0x%x\u0026#34;, run-\u0026gt;internal.suberror); 27 default: 28 errx(1, \u0026#34;exit_reason = 0x%x\u0026#34;, run-\u0026gt;exit_reason); 29 } 30 } 31 32 return 0; 33} 运行结果如下：\n1geduer@ulan:~/gevico/kvm$ gcc main.c \u0026amp;\u0026amp; ./a.out 2[1] Open kvm succesfuly, fd is 3 3[2] Get kvm version 12 4[3] Create VM succesfuly, fd is 4 5[4] Create vCPU succesfuly, fd is 5 6[5] Set vCPU type is Aarch64 (ARMv8) 7[6] Init kvm_run successfuly! 8[7] Load the vm running program to buffer \u0026#39;ram\u0026#39; 9[8] Set the vm userspace program ram to vm fd handler 10[9] Set vCPU PC, is 0x10000 11[10] Run vCPU and print message: 12Hello 完整代码 完整代码如下：\n1#include \u0026lt;err.h\u0026gt; 2#include \u0026lt;stdint.h\u0026gt; 3#include \u0026lt;stdio.h\u0026gt; 4#include \u0026lt;string.h\u0026gt; 5#include \u0026lt;stddef.h\u0026gt; 6#include \u0026lt;unistd.h\u0026gt; 7#include \u0026lt;sys/mman.h\u0026gt; 8#include \u0026lt;fcntl.h\u0026gt; 9#include \u0026lt;sys/ioctl.h\u0026gt; 10#include \u0026lt;linux/kvm.h\u0026gt; 11 12#define MEM_SIZE 0x1000 13#define PHY_ADDR 0x10000 14 15static __u64 __core_reg_id(__u64 offset) 16{ 17 __u64 id = KVM_REG_ARM64 | KVM_REG_ARM_CORE | offset; 18 19 if (offset \u0026lt; KVM_REG_ARM_CORE_REG(fp_regs)) 20 id |= KVM_REG_SIZE_U64; 21 else if (offset \u0026lt; KVM_REG_ARM_CORE_REG(fp_regs.fpsr)) 22 id |= KVM_REG_SIZE_U128; 23 else 24 id |= KVM_REG_SIZE_U32; 25 26 return id; 27} 28 29#define ARM64_CORE_REG(x) __core_reg_id(KVM_REG_ARM_CORE_REG(x)) 30 31const unsigned code[] = { 32 // write \u0026#34;Hello\u0026#34; to port 0x996 33 0xd28132c4, // mov x4, #0x996 // #2454 34 0xd2800905, // mov x5, #0x48 // H 35 0x39000085, // strb w5, [x4] 36 0xd2800ca5, // mov x5, #0x65 // e 37 0x39000085, // strb w5, [x4] 38 0xd2800d85, // mov x5, #0x6c // ll 39 0x39000085, // strb w5, [x4] 40 0x39000085, // strb w5, [x4] 41 0xd2800de5, // mov x5, #0x6f // o 42 0x39000085, // strb w5, [x4] 43 0xd2800145, // mov x5, #0xa // \\n 44 0x39000085, // strb w5, [x4] 45}; 46 47int main(void) 48{ 49 /* Initialize registers: instruction pointer for our code, addends, and 50 * initial flags required by aarch64 architecture. */ 51 struct kvm_one_reg reg; 52 struct kvm_vcpu_init init; //using init the vcpu type 53 struct kvm_vcpu_init preferred; 54 __u64 guest_entry = PHY_ADDR; 55 int ret; 56 int step = 0; 57 58 uint8_t *ram; 59 size_t mmap_size; 60 struct kvm_run *run; 61 62 int kvmfd = open(\u0026#34;/dev/kvm\u0026#34;, O_RDWR); 63 if (kvmfd == -1) 64 err(1, \u0026#34;/dev/kvm\u0026#34;); 65 else 66 printf(\u0026#34;[%d] Open kvm succesfuly, fd is %d\\n\u0026#34;, ++step, kvmfd); 67 68 /* Make sure we have the stable version of the API */ 69 ret = ioctl(kvmfd, KVM_GET_API_VERSION, NULL); 70 if (ret \u0026lt; 0) 71 err(1, \u0026#34;KVM_GET_API_VERSION\u0026#34;); 72 if (ret != 12) 73 errx(1, \u0026#34;KVM_GET_API_VERSION %d, expected 12\u0026#34;, ret); 74 else 75 printf(\u0026#34;[%d] Get kvm version %d\\n\u0026#34;, ++step, ret); 76 77 /* 1. create vm and get the vm fd handler */ 78 int vmfd = ioctl(kvmfd, KVM_CREATE_VM, (unsigned long)0); 79 if (vmfd \u0026lt; 0) 80 err(1, \u0026#34;KVM_CREATE_VM\u0026#34;); 81 else 82 printf(\u0026#34;[%d] Create VM succesfuly, fd is %d\\n\u0026#34;, ++step, vmfd); 83 84 /* 2. create vcpu */ 85 int vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0); 86 if (vcpufd \u0026lt; 0) 87 err(1, \u0026#34;KVM_CREATE_VCPU\u0026#34;); 88 else 89 printf(\u0026#34;[%d] Create vCPU succesfuly, fd is %d\\n\u0026#34;, ++step, vcpufd); 90 91 /* 3. arm64 type vcpu type init */ 92 // sample code can check the qemu/target/arm/kvm64.c 93 memset(\u0026amp;init, 0, sizeof(init)); 94 init.target = KVM_ARM_TARGET_GENERIC_V8; 95 ret = ioctl(vcpufd, KVM_ARM_VCPU_INIT, \u0026amp;init); 96 if (ret \u0026lt; 0) 97 err(1, \u0026#34;init vcpu type failed\\n\u0026#34;); 98 else 99 printf(\u0026#34;[%d] Set vCPU type is Aarch64 (ARMv8)\\n\u0026#34;, ++step); 100 101 /* 4. Map the shared kvm_run structure and following data. */ 102 mmap_size = ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL); 103 if (mmap_size \u0026lt; 0) 104 err(1, \u0026#34;KVM_GET_VCPU_MMAP_SIZE\u0026#34;); 105 if (mmap_size \u0026lt; sizeof(*run)) 106 errx(1, \u0026#34;KVM_GET_VCPU_MMAP_SIZE unexpectedly small\u0026#34;); 107 run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0); 108 if (run == MAP_FAILED) 109 err(1, \u0026#34;mmap vcpu\u0026#34;); 110 else 111 printf(\u0026#34;[%d] Init kvm_run successfuly!\\n\u0026#34;, ++step); 112 113 /* 5. load the vm running program to buffer \u0026#39;ram\u0026#39; */ 114 ram = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE, 115 MAP_SHARED | MAP_ANONYMOUS, -1, 0); 116 if (!ram) 117 err(1, \u0026#34;allocating guest memory\u0026#34;); 118 memcpy(ram, code, sizeof(code)); 119 printf(\u0026#34;[%d] Load the vm running program to buffer \u0026#39;ram\u0026#39;\\n\u0026#34;, ++step); 120 121 /* 6. Set the vm userspace program ram to vm fd handler */ 122 struct kvm_userspace_memory_region region = { 123 .slot = 0, 124 .flags = 0, 125 .memory_size = MEM_SIZE, 126 .guest_phys_addr = PHY_ADDR, 127 .userspace_addr = (unsigned long)ram, 128 }; 129 ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, \u0026amp;region); 130 if (ret \u0026lt; 0) 131 err(1, \u0026#34;KVM_SET_USER_MEMORY_REGION\u0026#34;); 132 else 133 printf(\u0026#34;[%d] Set the vm userspace program ram to vm fd handler\\n\u0026#34;, ++step); 134 135 /* 7. Set PC */ 136 reg.id = ARM64_CORE_REG(regs.pc); 137 reg.addr = (__u64)\u0026amp;guest_entry; 138 ret = ioctl(vcpufd, KVM_SET_ONE_REG, \u0026amp;reg); 139 if (ret \u0026lt; 0) 140 err(1,\u0026#34;KVM_SET_ONE_REG failed (pc)\u0026#34;); 141 else 142 printf(\u0026#34;[%d] Set vCPU PC, is 0x%x\\n\u0026#34;, ++step, (unsigned)guest_entry); 143 144 /* 8. Repeatedly run code and handle VM exits. */ 145 printf(\u0026#34;[%d] Run vCPU and print message:\\n\u0026#34;, ++step); 146 147 while (1) { 148 ret = ioctl(vcpufd, KVM_RUN, NULL); 149 if (ret \u0026lt; 0) 150 err(1, \u0026#34;KVM_RUN\u0026#34;); 151 152 switch (run-\u0026gt;exit_reason) { 153 case KVM_EXIT_MMIO: 154 if (run-\u0026gt;mmio.is_write \u0026amp;\u0026amp; run-\u0026gt;mmio.len == 1) { 155 printf(\u0026#34;%c\u0026#34;, run-\u0026gt;mmio.data[0]); 156 } 157 if (run-\u0026gt;mmio.data[0] == \u0026#39;\\n\u0026#39;) 158 return 0; 159 else 160 break; 161 case KVM_EXIT_FAIL_ENTRY: 162 errx(1, \u0026#34;KVM_EXIT_FAIL_ENTRY: hardware_entry_failure_reason = 0x%llx\u0026#34;, 163 (unsigned long long)run-\u0026gt;fail_entry.hardware_entry_failure_reason); 164 case KVM_EXIT_INTERNAL_ERROR: 165 errx(1, \u0026#34;KVM_EXIT_INTERNAL_ERROR: suberror = 0x%x\u0026#34;, run-\u0026gt;internal.suberror); 166 default: 167 errx(1, \u0026#34;exit_reason = 0x%x\u0026#34;, run-\u0026gt;exit_reason); 168 } 169 } 170 171 return 0; 172} 参考资料:\n[1] KVM API 手册\n[2] KVM 示例教程\n[3] ARM 简易 KVM 虚拟机\n","permalink":"https://zevorn.cn/posts/18/","summary":"构建最小虚拟机 前言 ARM 架构上的 KVM（Kernel based Virtual Machine）是一种基于 Linux 内核的虚拟化技术，它允许用户空间程序通过系统调用来直接控制硬件，从而实现对虚拟机的管理。KVM 在 x86 平台上已经非常成熟，而在 ARM 架构上，KVM 也得到了广泛的支持和发展。 PS: 本文首发在幽兰 Wiki 构建最小虚","title":"在幽兰本上构建最小 KVM 虚拟机"},{"content":"什么是所有权 所有权（ownership）是 Rust 用于管理内存的一组规则，通过在编译阶段检查程序内存使用的合法性，来避免运行时的内存安全问题。\n所有权的优势：\n运行时零开销，不会影响性能； 个人认为的缺点：\n开发者的学习成本稍高，并随着程序的复杂度上升，需要付出更多的心智成本。 所有权原则 所有权的基本规则：\nRust 中每一个值都被一个变量所拥有，该变量被称之为：值的所有者（owner）； 一个值都是只能被一个变量所拥有，或者说，一个值只能拥有一个所有者； 当所有者（变量）离开作用域范围时，这个值将被丢弃（自动调用 drop）。 理解 Rust 中的堆栈，可以加深对所有权的理解。\n变量绑定背后的数据交互 对于基本类型（存储在栈上）, Rust 会自动拷贝； 否则不能自动拷贝（比如 String 类型）。 Rust 避免二次释放的策略： 对于复杂类型（由存储在栈中的堆指针、其他成员共同组成），比如 String，当发生所有权转移时，旧所有者将失效，因此不会在离开作用域时 drop。\nRust 移动（move）： 区别于其他语言的浅拷贝（shallow copy），所有权发生移动后，旧所有者失效了。\nRust 克隆（clone）： 首先，Rust 永远也不会自动创建数据的 “深拷贝”。因此，任何自动的复制都不是深拷贝，可以被认为对运行时性能影响较小。\n如果我们确实需要深度复制 String 中堆上的数据，而不仅仅是栈上的数据，可以使用一个叫做 clone 的方法。\n1let s1 = String::from(\u0026#34;hello\u0026#34;); 2let s2 = s1.clone(); 3 4println!(\u0026#34;s1 = {}, s2 = {}\u0026#34;, s1, s2); 这段代码能够正常运行，说明 s2 确实完整的复制了 s1 的数据。\nRust 拷贝（浅拷贝）： 浅拷贝只发生在栈上，因此性能很高，在日常编程中，浅拷贝无处不在。\n1let x = 5; 2let y = x; 3println!(\u0026#34;x = {}, y = {}\u0026#34;, x, y); 但这段代码似乎与我们刚刚学到的内容相矛盾：没有调用 clone，不过依然实现了类似深拷贝的效果 —— 没有报所有权的错误。\n原因是像整型这样的基本类型在编译时是已知大小的，会被存储在栈上，所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效（x、y 都仍然有效）。换句话说，这里没有深浅拷贝的区别，因此这里调用 clone 并不会与通常的浅拷贝有什么不同，我们可以不用管它（可以理解成在栈上做了深拷贝）。\nRust Copy ： Rust 有一个叫做 Copy 的特征，可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy 特征，一个旧的变量在被赋值给其他变量后仍然可用，也就是赋值的过程即是拷贝的过程。\n那么什么类型是可 Copy 的呢？可以查看给定类型的文档来确认，这里可以给出一个通用的规则：\n任何基本类型的组合可以 Copy ，不需要分配内存或某种形式资源的类型是可以 Copy 的。\n如下是一些可以 Copy 的类型：\n所有整数类型，比如 u32； 布尔类型，bool，它的值是 true 和 false； 所有浮点数类型，比如 f64； 字符类型，char； 元组，当且仅当其包含的类型，也都是 Copy 的时候，比如 (i32, i32) 不可变引用 \u0026amp;T，注意：可变引用 \u0026amp;mut T 不可以 Copy。 函数传值与返回 将值传递给函数，一样会发生 移动 或者 复制，就跟 let 语句一样，下面的代码展示了所有权、作用域的规则：\n1fn main() { 2 let s = String::from(\u0026#34;hello\u0026#34;); // s 进入作用域 3 4 takes_ownership(s); // s 的值移动到函数里 ... 5 // ... 所以到这里不再有效 6 7 let x = 5; // x 进入作用域 8 9 makes_copy(x); // x 应该移动函数里， 10 // 但 i32 是 Copy 的，所以在后面可继续使用 x 11 12} // 这里, x 先移出了作用域，然后是 s。但因为 s 的值已被移走， 13 // 所以不会有特殊操作 14 15fn takes_ownership(some_string: String) { // some_string 进入作用域 16 println!(\u0026#34;{}\u0026#34;, some_string); 17} // 这里，some_string 移出作用域并调用 `drop` 方法。占用的内存被释放 18 19fn makes_copy(some_integer: i32) { // some_integer 进入作用域 20 println!(\u0026#34;{}\u0026#34;, some_integer); 21} // 这里，some_integer 移出作用域。不会有特殊操作 你可以尝试在 takes_ownership 之后，再使用 s，看看如何报错？例如添加一行 println!(\u0026ldquo;在move进函数后继续使用s: {}\u0026quot;,s);。\n同样的，函数返回值也有所有权，例如:\n1fn main() { 2 let s1 = gives_ownership(); // gives_ownership 将返回值 3 // 移给 s1 4 5 let s2 = String::from(\u0026#34;hello\u0026#34;); // s2 进入作用域 6 7 let s3 = takes_and_gives_back(s2); // s2 被移动到 8 // takes_and_gives_back 中, 9 // 它也将返回值移给 s3 10} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域，但已被移走， 11 // 所以什么也不会发生。s1 移出作用域并被丢弃 12 13fn gives_ownership() -\u0026gt; String { // gives_ownership 将返回值移动给 14 // 调用它的函数 15 16 let some_string = String::from(\u0026#34;hello\u0026#34;); // some_string 进入作用域. 17 18 some_string // 返回 some_string 并移出给调用的函数 19} 20 21// takes_and_gives_back 将传入字符串并返回该值 22fn takes_and_gives_back(a_string: String) -\u0026gt; String { // a_string 进入作用域 23 24 a_string // 返回 a_string 并移出给调用的函数 25} 所有权很强大，避免了内存的不安全性，但是也带来了一个新麻烦： 总是把一个值传来传去来使用它。 传入一个函数，很可能还要从该函数传出去，结果就是语言表达变得非常啰嗦，幸运的是，Rust 提供了新功能解决这个问题。\n","permalink":"https://zevorn.cn/posts/17/","summary":"什么是所有权 所有权（ownership）是 Rust 用于管理内存的一组规则，通过在编译阶段检查程序内存使用的合法性，来避免运行时的内存安全问题。 所有权的优势： 1. 运行时零开销，不会影响性能； 个人认为的缺点： 1. 开发者的学习成本稍高，并随着程序的复杂度上升，需要付出更多的心智成本。 所有权原则 所有权的基本规则： 1. Rust 中每一个值都被","title":"Rust 基本知识"},{"content":"前言 这篇 blog 记录我的 QEMU 贡献情况，你可以通过这里查询所有邮件。\n下面按照日期倒序排版。\n讨论如何更方便的打印调试 IR 变量 [RFC PATCH v1 0/1] Add helper_print functions for printing intermediate results of complex instructions in RISC-V target\n废弃 zynq 板卡的 ignore_memory_transaction_failures 我的 QEMU 开源社区贡献之旅，从泰晓科技发起的“ RISCV-Linux 内核剖析项目” 这里启程，我申领了 2023 年 8 月份蒙斌老师发布的老师提案：QEMU 模拟器 ignore_memory_transaction_failures 废弃。\nPATCH 合并（2024-10-14） Peter Maydell 邮件回复，内容如下：\nFrom: Peter Maydell \u0026lt;peter.maydell@linaro.org\u0026gt; To: Chao Liu \u0026lt;chao.liu@yeah.net\u0026gt; On Mon, 7 Oct 2024 at 12:25, Chao Liu \u0026lt;chao.liu@yeah.net\u0026gt; wrote: \u0026gt; \u0026gt; Hi, maintainer, \u0026gt; \u0026gt; Following the reference from the chip manualug585-Zynq-7000-TRM manual \u0026gt; B.3 (Module Summary), placeholders have been added for all unimplemented \u0026gt; devices, including the AXI and AMBA bus controllers that interact with \u0026gt; the FPGA. \u0026gt; \u0026gt; We can check against the manual by printing the address space of the \u0026gt; zynq board with the following qemu command: \u0026gt; ${QEMU_PATH}/qemu-system-aarch64 \\ \u0026gt; -M xilinx-zynq-a9 \\ \u0026gt; -display none \\ \u0026gt; -monitor stdio -s \u0026gt; (qemu) info mtree -f \u0026gt; \u0026gt; The testing methodology previously discussed in earlier email exchanges \u0026gt; will not be repeated here. \u0026gt; \u0026gt; Chao Liu (3): \u0026gt; xilink_zynq: Add various missing unimplemented devices \u0026gt; xilink-zynq-devcfg: Fix up for memory address range size not set \u0026gt; correctly \u0026gt; xilink-zynq-devcfg: Avoid disabling devcfg memory region during \u0026gt; initialization I\u0026#39;ve left comments for patches 2 and 3. I have taken patch 1 into target-arm.next, with the ignore_memory_transaction_failures line reinstated. I\u0026#39;m all in favour of our being able to get rid of that legacy flag setting for this board, but as I\u0026#39;ve said on previous versions of this patchset, we need to have confidence that it\u0026#39;s not going to break existing guest code, which means the patch removing it needs to come with a description of the testing that\u0026#39;s been done (which should be more than \u0026#34;Linux still boots\u0026#34;). thanks -- PMM 已经 PULL 的 patch ：hw/arm/xilinx_zynq: Add various missing unimplemented devices。\nPATCH v4（2024-10-07） Peter Maydell 认为按照 memory map 范围去标定占位符，和基于 ignore_memory_transaction_failures 标志没有什么区别，所以痛定思痛，还是老老实实按照手册，把所有未实现设备都添加一个占位符。\n这个过程还发现了 devcfg 的一个 bug ，unlock 寄存器写入任何非 0x757BDF0D 值，都会设置为非法访问状态：\nUnlock Register: This register is used to protect the DEVCI configuration registers from ROM code corruption. The boot ROM will unlock the DEVCI by writing 0x757BDF0D to this register. Writing anything other than the unlock word to this register will cause an illegal access state and make the DEVCI inaccessible until a system reset occurs. 此寄存器用于防止 DEVCI 配置寄存器被 ROM 代码损坏。在设备启动过程中，引导 ROM 会通过向此寄存器写入特定的解锁码 0x757BDF0D 来解锁 DEVCI 。如果写入任何不同于这个解锁码的值，将会导致非法访问状态，并且直到系统复位之前都无法访问 DEVCI 。\n在硬件设计和嵌入式系统中，这样的机制是为了确保只有在正确的条件下才能更改配置设置，从而防止意外或恶意的数据损坏。这对于保证系统的稳定性和安全性是非常重要的。\n下面是第 4 版 patch 的邮件：\n[PATCH v4 0/3] Drop ignore_memory_transaction_failures for xilink_zy\nHi, maintainer, Following the reference from the chip manualug585-Zynq-7000-TRM manual B.3 (Module Summary), placeholders have been added for all unimplemented devices, including the AXI and AMBA bus controllers that interact with the FPGA. We can check against the manual by printing the address space of the zynq board with the following qemu command: ${QEMU_PATH}/qemu-system-aarch64 \\ -M xilinx-zynq-a9 \\ -display none \\ -monitor stdio -s (qemu) info mtree -f The testing methodology previously discussed in earlier email exchanges will not be repeated here. Chao Liu (3): xilink_zynq: Add various missing unimplemented devices xilink-zynq-devcfg: Fix up for memory address range size not set correctly xilink-zynq-devcfg: Avoid disabling devcfg memory region during initialization hw/arm/xilinx_zynq.c | 71 ++++++++++++++++++++++++++++++- hw/dma/xlnx-zynq-devcfg.c | 9 +++- include/hw/dma/xlnx-zynq-devcfg.h | 2 +- 3 files changed, 78 insertions(+), 4 deletions(-) PATCH v3（2024-10-06） [PATCH v3 0/2] Drop ignore_memory_transaction_failures for xilink_zy\nHi all, Following the Zynq-7000 SoC Data Sheet\u0026#39;s \u0026#34;Memory Map\u0026#34; section (referenced at [1]), We have identified the need to create placeholders for unimplemented devices across the entire range of Zynq-7000 series boards. This effort aims at ensuring maximum compatibility with different models within the series. The following table summarizes the relevant memory map addresses for the Zynq-7000: Start Address Size (MB) Description 0x0000_0000 1,024 DDR DRAM and on-chip memory (OCM) 0x4000_0000 1,024 PL AXI slave port #0 0x8000_0000 1,024 PL AXI slave port #1 0xE000_0000 256 IOP devices 0xF000_0000 128 Reserved 0xF800_0000 32 Programmable registers access via AMBA APB bus 0xFA00_0000 32 Reserved 0xFC00_0000 64 MB - 256 KB Quad-SPI linear address base address (except to 256 KB which is in OCM), 64 MB reserved, only 32 MB is currently supported 0xFFFC_0000 256 KB OCM when mapped to high address space For the purposes of this patch, we will not be creating placeholders for DRAM and any reserved regions of the address space. A test script has been developed that covers the most common board types of the Zynq-7000 series. The test script obtained all linux binary images of the zynq-7000 series boards from xilinx-wiki for script testing(referenced at [2]). The steps to run the test are as follows: a) Clone the repository. git clone -b xilinx-zynq-test https://github.com/gevico/qemu-board.git b) Apply the patch attached to this email and compile QEMU. c) Set the environment variable for the path to your QEMU. export QEMU_PATH=\u0026lt;your qemu path\u0026gt; d) Execute the testing script. ./qemu-zynq-test e) Check the results. If successful, the output should resemble the following: Test Project: \u0026lt;your path\u0026gt;/qemu-board/hw/arm/xilinx-zynq Start 1: xilinx-zynq.zc702 1/3 Test #1: xilinx-zynq.zc702 ...................... Passed Start 2: xilinx-zynq.zc706 2/3 Test #2: xilinx-zynq.zc706 ...................... Passed Start 3: xilinx-zynq.zed 3/3 Test #3: xilinx-zynq.zed ...................... Passed All tests passed See: [1]: https://www.amd.com/content/dam/xilinx/support/documents/data_sheets/ds190-Zynq-7000-Overview.pdf [2]: http://www.wiki.xilinx.com/Zynq+2016.2+Release PATCH v2（2024-09-27） There are two things we can do: (1) We can leave the ignore_memory_transaction_failures flag set. This is safe (no behaviour change) but not the right (matching the hardware) behaviour. The main reason to do this is if we don\u0026#39;t feel we have enough access to a range of guest code to test the other approach. (2) We can clear the flag. This is preferable (it matches the hardware). But the requirement to do this is that (a) we must make the best effort we can to be sure we\u0026#39;ve put unimplemented-device placeholders for specific devices we don\u0026#39;t yet model (by checking e.g. the hardware documentation for the SoC and board model, the device tree, etc) (b) we do the most wide-ranging testing of guest code that we can. This checks that we didn\u0026#39;t miss anything in (a). I don\u0026#39;t mind which of these we do. What I was asking in my comments on version one of your patch was for how we were doing on requirement 2b. “增加了一个 znyq.umip 设备，用来覆盖 GPA 的 32 位地址空间” 的方法，并不是一个更通用和标准的做法。\n移除 ignore_memory_transaction_failures flag 是可行的，基于（2b）所述，更优的做法是利用客户机程序进行更加广泛的测试。\n下一步工作，主要集中在两方面，以 xilinx_zynq 为例：\n基于芯片手册和 dts，尽可能覆盖到所有未实现设备，并为其创建 \u0026ldquo;unimp_device\u0026rdquo; 站位设备; 进行更加广泛的测试，最好提供测试脚本。 最后再彻底移除 ignore_memory_transaction_failures 标志。\nRe: [PATCH v2 0/2] Drop ignore_memory_transaction_failures for xilink_zy\nPATCH v1（2024-09-25） 文章产出：废弃 QEMU xilinx_zynq 板卡的 ignore_memory_transaction_failures Patch 产出：Drop ignore_memory_transaction_failures for xilink_zynq ","permalink":"https://zevorn.cn/posts/16/","summary":"前言 这篇 blog 记录我的 QEMU 贡献情况，你可以通过这里 查询所有邮件 10 。 下面按照日期倒序排版。 讨论如何更方便的打印调试 IR 变量 RFC PATCH v1 0/1 Add helper print functions for printing intermediate results of complex instructions i","title":"QEMU 开源社区贡献记录"},{"content":"前言 从 2021 年开始，我一直从事体系架构模拟和动态二进制翻译工作，由于工作需要，我经常要调试各种操作系统，比如 Linux、VxWork、FreeRTOS 、RT-Thread、SylixOS、AcoreOS、ReWorks 等等，这让我对操作系统的原理和业务应用场景有了一个宏观的认识。随着嵌入式系统的复杂化和规模化（多系统的同步与交互），对于数字嵌入式系统的性能也提出了更高的要求。\n在 2024 年上半年，我主导了一个（用于全系统模拟的）高性能 JIT 引擎的前期研发工作，在项目实施期间，也许是对于模拟器性能的敏感，让我对硬件虚拟化技术产生了很大的兴趣，我了解到，这属于操作系统内核的一部分（Type-2）。\n前段时间，我学习虚拟化技术时，在幽兰本上用 C 语言实现了一个 200 行左右代码量的KVM 轻量虚拟机，并分享在了兰舍群聊，郭克老师看到以后，向我推荐了由清华大学陈渝教授和向勇老师发起的「开源操作系统训练营」。\nPS：与陈渝教授的一点渊源：\n2002 年冬天，一群操作系统爱好者在网络上讨论技术，想要在脱离硬件限制的情况下，学习和研究操作系统的原理。于是，他们提出用软件模拟硬件的想法，这其中就包括 SkyEye 的发起者——清华大学陈渝教授。\n当时，国际上已经有类似的项目，如 uCLinux 组织开发的 ARMulator 模拟器，可以模拟 AT91 的开发板，支持 UClinux 在上面运行。基于 ARMulator 的设计框架和经验，陈渝教授等人在 2003 年正式发起了 SkyEye 开源项目，被国内外个人爱好者和高校大量使用。\n我在 2022 年起，发起和组织了 SkyEye 开源项目的重新维护，目前开源在 Gitee 平台。\n本篇文章，用于记录训练营期间的学习情况和心得体会，按照日期的倒序排版。因工作调整导致时间不再充裕，暂时只跟进到二阶段。\n第二阶段 进展（2024-10-18） 完成二阶段实验 3。 进展（2024-10-14） 编写二阶段实验 2 的报告。 进展（2024-10-13） 完成二阶段实验 2 的编码任务，通过 CI/CD 测试； 进展（2024-10-10） 学习 rCore-Camp-Guide-2024A · 第四章：地址空间； 来西安旅游，条件有限，在笔记本电脑上又重新搭了一遍实验环境，顺便解决了之前的一些小问题； PS：之前实现过 ARM 全系列的非安全的几种 MMU 翻译格式的模拟，所以对 RISCV RV39 的 MMU 翻译格式理解起来还比较轻松。不过这部分内容比较多，需要再耐心看两天，再做题目。操作系统一旦开启 MMU 以后，地址发生虚实隔离，调试起来会困难一些，前期充分理解 rCore 的内存管理策略，对后面的实验有很大帮助。\n进展（2024-10-09） 参加训练营课程； 完成第二阶段实验 1 。 PS: 完成第一题，Rust 方面用到了模板、结构体、关联函数、match 的知识，操作系统需要知道基本的汇编以及权限变化产生的影响，还有理解系统调用。第一阶段提前完成了，所以开始二阶段学习。\n第一阶段 进展（2024-10-08） 用 Rust 编写了一个最小 JIT，学习 FFI 编程，trait； 学习 Rust 生命周期； 拉取第二阶段的仓库，基于 docker 搭建环境镜像，成功运行 ch1 分支。 PS：和做编译器的朋友交流了学习 Rust 的心得，得出一个很好的结论：”学习 Rust 最好的方式，就是把自己熟悉的业务用 Rust 重写一遍。“这给了我很大的启发，是不是也应该用 Rust 实现一个学习用途的 JIT ？和其他朋友简单沟通了一下，有了一个初步思路，或许可以做一个 x86 =\u0026gt; RISC-V or ARM =\u0026gt; RISC-V 的 JIT，丰富 RISC-V 的生态。\n进展（2024-10-07） 参加 RustSBI 线上讲座，我提了一个关于 RustSBI 对虚拟化方面支持情况的问题。 进展（2024-10-04） 学习 Rust 的 struct 、enum、Option、match； 学习 Rust 的 package、crate、module； 学习 Rust 的 use。 进展（2024-10-02） 参加训练营课程「Rust 语法入门」； 学习《Rust 圣经》中关于基础语法部分。 进展（2024-09-30） 复习 Rust 所有权； 搭建 Rust risc-v 交叉编译环境； 参加训练营课程「环境配置 + Rustlings 入门」。 PS：开营前一周，速通了一遍 rustling，主要是通过解决编译报错来快速推进，算是对 Rust 这门语言，有了一个初印象。接下来三周，反复咀嚼 Rust 的主要知识点，夯实基础。\n训练营开营 2024 秋冬季开源操作系统训练营启动会 （2024-09-29），介绍了训练营的背景、目标、课程内容和规模。训练营得到全程实验室支持，覆盖 520 多所高校和240 多家公司，课程包括 Rust 基础语法、答疑和算法，操作系统实战项目，旨在培养对开源操作系统感兴趣的人才。\n下面记录会议中我比较感兴趣的部分：\n开幕致辞（陈渝）\n开幕与介绍：陈渝欢迎所有参与者，并介绍了此次训练营的重要嘉宾，包括来自北京大学的陈安群老师、全程实验室的杨波主任、vivo的杨春总经理、开放基金会教育培训部的王延广部长等。\n背景与发展：陈渝提到该训练营始于 2020 年，由他与香港的一位老师共同发起，旨在激发更多学生对操作系统的兴趣。训练营联合了鹏程实验室，推广操作系统的发展，并引入了一页编程、组件化操作系统等新概念。\n参与规模：此次训练营吸引了超过 3000 名参与者，覆盖了 524 所高校，不仅限于学生，还包括工程师。\n课程内容：训练营将涵盖 Rust 编程、类似清华大学计算机系本科生的 OS 课程实验设计、组件化操作系统的设计与实现理念，以及其他如微内核虚拟化等实用项目。\n操作系统的重要性与挑战：陈渝强调了操作系统作为连接硬件与应用的关键层的重要性，特别是面对数百万行代码的现代操作系统（如Linux），需要解决实时性能、安全性等问题。此外，他还提到操作系统需要适应新兴领域如智能汽车、机器人等的需求。\n未来趋势：他指出，随着AI的快速发展，操作系统必须作出相应的调整以支持新的应用场景，并提高安全性。同时，国内在新能源汽车领域的发展也需要在操作系统层面进行更多的研究与创新。\n最后，陈渝教授表示期待参与者们能够通过此次训练营获得知识、建立友谊并提升个人能力，并预告了接下来将有来自不同领域的专家进行更深入的分享。\nvivo 自研操作系统在 Rust 上的探索（杨春）\n开场致谢：杨春向参会的师生表达了敬意，并感谢陈渝教授的邀请，让他有机会参加这次开源操作系统的训练营活动。\nvivo 在操作系统上的投入：杨春提到 vivo 在操作系统研发上已经有几年的积累，并在去年的开发者大会上首次公开了自家研发的操作系统，该系统聚焦于智慧、流畅和安全三大核心特性。\n选择 Rust 的原因：vivo 在开发自家操作系统时选择了使用 Rust 语言，主要是看重 Rust 在内存安全方面的优势，如所有权模型和生命周期管理等特性，这些对于构建安全的操作系统至关重要。\n与学术界的合作：杨春讲述了 vivo 与陈渝教授团队的联系过程，双方虽然分别位于工业界和学术界，但在使用 Rust 语言探索下一代操作系统方面有着共同的目标，因此双方一拍即合。\n行业人才现状：他认为目前行业内掌握 Rust 语言的人才仍然稀缺，特别是在操作系统领域中使用 Rust 的规模还不大。因此，vivo 希望通过与行业内的交流来验证自己在技术路线上的选择是否正确。\n对未来合作的期望：杨春表达了对开源操作系统训练营活动的支持，认为这类活动有助于推动 Rust 生态的成熟和发展，并希望能吸引更多的人才参与到 Rust 及操作系统领域中来。\n结束语：最后，杨春预祝本次活动圆满成功，并感谢大家的聆听。\n总结，杨春的发言主要围绕 vivo 在操作系统研发上的努力，特别是采用 Rust 语言的决策及其在安全方面的优势，并强调了与学术界合作的重要性以及对未来人才发展的期望。\n介绍 ArceOS 发展计划（石磊）\n项目背景：该项目旨在解决现有操作系统存在的问题和挑战，提出了安全、高并发、实时性、模块化、生态兼容和定制能力的研发目标，以此为基础构建新型的组件化操作系统内核。\n项目特点：\n多模式支持：除了支持传统的单片式内核外，还支持虚拟化基础设施。 组件化设计：通过组件仓库和组合方式来构建不同模式的内核。 基于 Rust 语言：利用 Rust 语言在内存安全和并发安全方面的优势，并探索基于 Rust 开发内核的模式。 当前进展：经过两三年的工作，已经建立了基础的组件仓库，涵盖了不同层次的功能组件，可以用于构建各种模式的内核。目前的工作集中在以下几个方向： 核心组件仓库：支持构造多种模式的内核。 扩展宏内核：与 Linux API 兼容，可运行 Linux 上的原生应用。 虚拟化：支持典型类型的虚拟化基础设施。 未来发展方向：下一步的重点是继续发展宏内核、虚拟化技术、以及一个统一的基于异步协程的框架和驱动程序。计划在第四阶段带领参与者进行相关的项目实践。 发行版开发：基于上述研究，将进一步发展适用于不同应用场景的发行版。 更多信息：有兴趣了解更多细节的参与者可以查看讨论区中的过往工作记录。 陈渝教授补充说明，这个项目最初是由清华大学的博士生在其博士课题期间发起的，现在已有许多师生参与其中进行扩展工作。该项目的特点是可以组合成具有不同架构和特性的定制化配置，这被认为是未来操作系统的发展方向之一。\n关于会议更多内容，可访问：2024年秋冬季开源操作系统训练营开营仪式视频回放\n","permalink":"https://zevorn.cn/posts/15/","summary":"前言 从 2021 年开始，我一直从事体系架构模拟和动态二进制翻译工作，由于工作需要，我经常要调试各种操作系统，比如 Linux、VxWork、FreeRTOS 、RT Thread、SylixOS、AcoreOS、ReWorks 等等，这让我对操作系统的原理和业务应用场景有了一个宏观的认识。随着嵌入式系统的复杂化和规模化（多系统的同步与交互），对于数字嵌入","title":"记录参加清华大学开源操作系统训练营的体验"},{"content":"前言 本篇文章，主要记录对 Intel x86 架构 CPU 的 VMX 硬件虚拟化技术的学习和研究情况。\n参考资料清单如下：\n深度探索 Linux 系统虚拟化 QEMU/KVM源码解析与应用 x86 架构虚拟化的障碍 敏感指令定义：\nGerald J.Popek 和 Robert P.Goldberg 指出，修改系统资源的，或者在不同模式下行为有不同表现的，都属于敏感指令。在虚拟化场景下，VMM 需要监测这些敏感指令。\n特权指令定义：\n一个支持虚拟化的体系架构的敏感指令都属于特权指令，即在非特权级别执行这些敏感指令时 CPU 会抛出异常，进入 VMM 的异常处理函数，从而实现了控制 VM 访问敏感资源的目的。\n但是，x86 架构恰恰不能满足这个准则。x86 架构并不是所有的敏感指令都是特权指令，有些敏感指令在非特权模式下执行时并不会抛出异常，此时 VMM 就无法拦截处理 VM 的行为了。\n我们以修改 FLAGS 寄存器中的 IF（Interrupt Flag）为例：\n首先使用指令 pushf 将 FLAGS 寄存器的内容压到栈中，然后将栈顶的IF清零，最后使用 popf 指令从栈中恢复 FLAGS 寄存器。 如果虚拟机内核没有运行在 ring 0，x86 的 CPU 并不会抛出异常，而只是默默地忽略指令 popf，因此虚拟机关闭 IF 的目的并没有生效。 有人提出半虚拟化的解决方案，即修改 Guest 的代码，但是这不符合虚拟化的透明准则。\n后来，人们提出了二进制翻译的方案，包括静态翻译和动态翻译：\n静态翻译：运行前扫描整个可执行文件，对敏感指令进行翻译，形成一个新的文件。然而，静态翻译必须提前处理，而且对于有些指令只有在运行时才会产生的副作用，无法静态处理。\n动态翻译：即在运行时以代码块为单元动态地修改二进制代码，动态翻译在很多 VMM 中得到应用，而且优化的效果非常不错。\nIntel VMX 简介 Intel 开发了 VT 技术以支持虚拟化，为 CPU 增加了 Virtual-Machine Extensions，简称 VMX。\n两种运行模式：\nVMX root operation（host 模式） VMX non-root operation（guest 模式） 每个运行模式都支持 ring 0 ~ ring 3。\n如下图所示：\n┌──────────────────┐ ┌──────────────────┐ │ring3 │ │ring3 │ │ │ │ │ │ ┌─────────────┐ │ │ ┌──────────────┐ │ │ │ Host App │ │ VM Entry │ │ Guest App │ │ │ └─────────────┘ │ ─────────► │ └──────────────┘ │ │ ──────────────── │ │ ─────────────────┤ │ ┌─────────────┐ │ │ ┌──────────────┐ │ │ │ Host Kernel │ │ VM Exit │ │ Guest Kernel │ │ │ └─────────────┘ │ ◄───────── │ └──────────────┘ │ │ │ │ │ │ring0 │ │ring0 │ └──────────────────┘ └──────────────────┘ VMX Root Mode VMX non-Root Mode 模式切换：\nVM Entry，从 VMX root Mode 切换到 VMX non-root Mode VM Exit，从 VMX non-root Mode 切换到 VMX root Mode 一个数据结构：\nVirtual-Machine Control Structure（VMCS） ","permalink":"https://zevorn.cn/posts/14/","summary":"前言 本篇文章，主要记录对 Intel x86 架构 CPU 的 VMX 硬件虚拟化技术的学习和研究情况。 参考资料清单如下： 深度探索 Linux 系统虚拟化 1 QEMU/KVM源码解析与应用 2 1 : \u003ca href=\"https://read.douban.com/ebook/162815582/\"\u003ehttps://read.douban.com/ebook/162815582/\u003c/a\u003e 2 : \u003ca href=\"https://book.douban.com/subjec\"\u003ehttps://book.douban.com/subjec\u003c/a\u003e","title":"Intel VMX 硬件虚拟化研究实录"},{"content":"AliasAnalysis.html 中对别名分析进行了基本的介绍：\nAlias Analysis (又名 Pointer Analysis)，用于确定两个指针是否指向内存中的同一对象，这里有很多不同的别名分析算法，可分为：流敏感 vs 流非敏感、上下文敏感 vs 上下文非敏感、域敏感 vs 域非敏感、基于一致性的 vs 基于子集的。\n传统的别名分析用于给出 Must、May、No 的回答:\nMust 代表两个指针总是指向同一对象； May 代表可能指向同一对象； No 代表绝不会指向同一对象。 编译器优化：何为别名分析 - 知乎 (zhihu.com) 这篇文章介绍了别名分析算法的几种类型。\nLLVM AliasAnalysis 类是实现别名分析的基础类，能够提供简单的别名分析信息，且能提供 Mod / Ref 信息，有利于进行更复杂的分析。在SkyEye中启用的两个别名分析的优化，是AliasAnalysis 类的派生类。（Mod：memory access modifies，Ref： references memory）\n别名分析最简单的应用：\n例如以下 C 代码：\n1int foo (int __attribute__((address_space(0))) *a, 2 int __attribute__((address_space(1))) *b) { 3 *a = 42; 4 *b = 20; 5 return *a; 6} 转换成 llvm 如下：\n1define i32 @foo(i32 addrespace(0)* %a, i32 addrspace(1)* %b) #0 { 2entry: 3 store i32 42, i32 addrspace(0)* %a, align 4 4 store i32 42, i32 addrspace(1)* %b, align 4 5 %0 = load i32, i32* %a, align 4 6 ret i32 %0 7} 现在需要对 foor 进行优化，去掉不必要的 load ：\n1define i32 @foo(i32 addrespace(0)* %a, i32 addrspace(1)* %b) #0 { 2entry: 3 store i32 42, i32 addrspace(0)* %a, align 4 4 store i32 42, i32 addrspace(1)* %b, align 4 5 ret i32 42 6} 但是这个优化的前提是， a 和 b 不能别名，否则会导致错误如下：\n1 int i = 0; 2 int result = foo(\u0026amp;i, \u0026amp;i); 可以看到，以上调用会使 a 和 b 别名，本应该返回 20， 结果因为优化的缘故， 返回了 42， 导致错误。 所以编译器只有确定两个指针不会产生别名时，才能进行以上优化。\n以我的理解，别名分析主要作为其他优化的前置 pass ，主要作用还是帮别的优化方向区分哪些变量可以优化的，哪些不能被优化，以便于准确消除冗余的 IR 。\n所以我在 LLVM3.0 源代码进行了搜索：\n在后端编译流程里面，有一些过程，确实需要调用别名分析的作为前置 pass。\n之后选择 TypeBasedAliasAnalysis 类源代码，进行主要分析：\nTypeBasedAliasAnalysis 继承自 ImmutablePass 和 AliasAnalysis，提供了基于元数据的类型别名分析的实现，通过检查节点之间的关系判断两个类型是否可能别名。\n由于继承自 ImmutablePass ，因此 TypeBasedAliasAnalysis 是一旦创建就不能更改的 Pass，这样的 Pass 对象通常用于在整个编译过程中共享状态，在这里我们是为每个 JIT Function 创建这样的 pass。因此可以理解为什么要在最开始的时候就调用它。\n另外一个关键字是元数据（metadata），LLVM 使用元数据来传递附加信息，这些信息在编译过程中不直接影响程序的执行，但对于优化和分析非常有用。在这种情况下，元数据用于指示指针和内存对象的类型信息，以便进行更准确的别名分析。如下图给出介绍，展示了元数据的格式（LLVM Metadata 介绍-CSDN博客）：\n1// This file defines the TypeBasedAliasAnalysis pass, which implements 2// metadata-based TBAA. 3// 4// In LLVM IR, memory does not have types, so LLVM\u0026#39;s own type system is not 5// suitable for doing TBAA. Instead, metadata is added to the IR to describe 6// a type system of a higher level language. This can be used to implement 7// typical C/C++ TBAA, but it can also be used to implement custom alias 8// analysis behavior for other languages. 9// 10// The current metadata format is very simple. TBAA MDNodes have up to 11// three fields, e.g.: 12// !0 = metadata !{ metadata !\u0026#34;an example type tree\u0026#34; } 13// !1 = metadata !{ metadata !\u0026#34;int\u0026#34;, metadata !0 } 14// !2 = metadata !{ metadata !\u0026#34;float\u0026#34;, metadata !0 } 15// !3 = metadata !{ metadata !\u0026#34;const float\u0026#34;, metadata !2, i64 1 } 16// 17// The first field is an identity field. It can be any value, usually 18// an MDString, which uniquely identifies the type. The most important 19// name in the tree is the name of the root node. Two trees with 20// different root node names are entirely disjoint, even if they 21// have leaves with common names. 22// 23// The second field identifies the type\u0026#39;s parent node in the tree, or 24// is null or omitted for a root node. A type is considered to alias 25// all of its descendants and all of its ancestors in the tree. Also, 26// a type is considered to alias all types in other trees, so that 27// bitcode produced from multiple front-ends is handled conservatively. 28// 29// If the third field is present, it\u0026#39;s an integer which if equal to 1 30// indicates that the type is \u0026#34;constant\u0026#34; (meaning pointsToConstantMemory 31// should return true; see 32// http://llvm.org/docs/AliasAnalysis.html#OtherItfs). 33// 34// TODO: The current metadata format doesn\u0026#39;t support struct 35// fields. For example: 36// struct X { 37// double d; 38// int i; 39// }; 40// void foo(struct X *x, struct X *y, double *p) { 41// *x = *y; 42// *p = 0.0; 43// } 44// Struct X has a double member, so the store to *x can alias the store to *p. 45// Currently it\u0026#39;s not possible to precisely describe all the things struct X 46// aliases, so struct assignments must use conservative TBAA nodes. There\u0026#39;s 47// no scheme for attaching metadata to @llvm.memcpy yet either. 另外我又关注了一下，别名分析对内存的消耗情况，调用它们是否会比较耗时？但是没有找到相关资料，这个下次再研究。\n主要参考资料：llvm-3.0.src/docs/AliasAnalysis.html\n侧重的源代码：lib\\Analysis\\TypeBasedAliasAnalysis.cpp\n","permalink":"https://zevorn.cn/posts/13/","summary":"AliasAnalysis.html 中对别名分析进行了基本的介绍： Alias Analysis (又名 Pointer Analysis)，用于确定两个指针是否指向内存中的同一对象，这里有很多不同的别名分析算法，可分为：流敏感 vs 流非敏感、上下文敏感 vs 上下文非敏感、域敏感 vs 域非敏感、基于一致性的 vs 基于子集的。 传统的别名分析用于给出","title":"LLVM后端优化Pass：别名分析"},{"content":"QEMU 启动以后，并没有立刻执行客户机程序的第一条指令，而是先执行 Machine 在初始化阶段设置的 reset vector 程序段，然后再跳转到客户机程序的第一条指令。\n1static const struct MemmapEntry 2{ 3 hwaddr base; 4 hwaddr size; 5} gd32vf103_memmap[] = { 6 [GD32VF103_MFOL] = {0x0, 0x20000}, 7}; 8static void nuclei_board_init(MachineState *machine) 9{ 10 ... 11 12 /* reset vector */ 13 uint32_t reset_vec[8] = { 14 0x00000297, /* 1: auipc t0, %pcrel_hi(dtb) */ 15 0x02028593, /* addi a1, t0, %pcrel_lo(1b) */ 16 0xf1402573, /* csrr a0, mhartid */ 17#if defined(TARGET_RISCV32) 18 0x0182a283, /* lw t0, 24(t0) */ 19#elif defined(TARGET_RISCV64) 20 0x0182b283, /* ld t0, 24(t0) */ 21#endif 22 0x00028067, /* jr t0 */ 23 0x00000000, 24 memmap[GD32VF103_MAINFLASH].base, /* start: .dword */ 25 0x00000000, 26 /* dtb: */ 27 }; 28 29 /* copy in the reset vector in little_endian byte order */ 30 for (i = 0; i \u0026lt; sizeof(reset_vec) \u0026gt;\u0026gt; 2; i++) 31 { 32 reset_vec[i] = cpu_to_le32(reset_vec[i]); 33 } 34 rom_add_blob_fixed_as(\u0026#34;mrom.reset\u0026#34;, reset_vec, sizeof(reset_vec), 35 memmap[GD32VF103_MFOL].base + 0x1000, \u0026amp;address_space_memory); 36... 37} 因此第一条指令的PC地址为 memmap[GD32VF103_MFOL].base + 0x1000，通过以上代码得知，memmap[GD32VF103_MFOL].base 为0，因此起始 PC 为 0x1000。\n验证方法很简单，使用 riscv-gdb 远程 remote QEMU，命令如下：\n打开第一个终端窗口，启动 QEMU：\n1qemu (nuclei_gd32vf103) $ ./build/qemu-system-riscv32 -M gd32vf103_rvstar -cpu nuclei-n205 -icount shift=0 -nodefaults -nographic -kernel ../nuclei-sdk/application/baremetal/helloworld/helloworld.elf -serial stdio -gdb tcp::1234 -S 打开第二个终端窗口，启动 gdb，可以看到第一条指令，反汇编和前面 reset vector 对比一下：\n1qemu (nuclei_gd32vf103) $ riscv-nuclei-linux-gnu-gdb ../nuclei-sdk/application/baremetal/helloworld/helloworld.elf 2(gdb) target remote localhost:1234 3Remote debugging using localhost:1234 40x00001000 in ?? () 5(gdb) x /10i $pc 6=\u0026gt; 0x1000: auipc t0,0x0 7 0x1004: addi a1,t0,32 8 0x1008: csrr a0,mhartid 9 0x100c: lw t0,24(t0) 10 0x1010: jr t0 11 0x1014: unimp 12 0x1016: unimp 13 0x1018: unimp 14 0x101a: addi s0,sp,16 15 0x101c: unimp 这段启动程序主要做了两件事，通过 dtb 获取客户程序起始地址，读取 mhartid 寄存器（当前 hart (硬件线程) 的 ID）值。\n我们从第一条指令开始分析。\n现在我们跑的是一个裸机程序，没有 dtb ，因此 auipc t0, 0x0 这条指令最终将当前 PC 地址 0x1000 写入 t0 寄存器；\n接着将 t0 加上 32 偏移，得到地址 0x1020， 写入 a1 寄存器；\n通过 csrr 指令将 mhartid 寄存器的值读入 a0 寄存器；\n将 t0 偏移 24，得到地址 0x1018，从这地址读取客户机程序起始地址，写入 t0寄存器；\n通过 jr 指令，跳转到客户机程序起始地址。\n使用 gdb 可以观察到 0x1018 地址内的数据为 0x8000000，这个地址正好在 reset vector 后面挨着。\n我们知道，QEMU 启动的时候，使用 -kernel 加载的客户机程序 helloword.elf，势必有一个流程，将 0x8000000 写入 0x1018。\n那么我们该如何快速定位到这个流程呢？\n方法很简单，0x1018 地址是客户机的物理地址（Guest physical address，简称 GPA ），我们只需要计算出对应的 QEMU 进程的虚拟地址，即宿主机虚拟地址（Host virtual addree，简称 HVA ），使用 gdb 启动 qemu，对这个HVA下内存监视点，就可以了。\nGPA 的 0 地址，对应 memmap[GD32VF103_MFOL].base 的 mr，通过 mr 的 ram 初始化过程，可以获取对应的 HVA 基地址，然后我们加上 0x1018 的 offset ，就获取到真正的 HVA 了，流程如下：\ngdb 启动 qemu，设置断点，在初始化 GD32VF103_MFOL 的时候停住： 1qemu (nuclei_gd32vf103) $ gdb ./build/qemu-system-riscv32 2(gdb) set args -M gd32vf103_rvstar -cpu nuclei-n205 -icount shift=0 -nodefaults -nographic -kernel ../nuclei-sdk/application/baremetal/helloworld/helloworld.elf 3(gdb) b memory_region_init_rom 4Breakpoint 1 at 0x6a7d60: file ../system/memory.c, line 3612. 5(gdb) run 6(gdb) finish 查看 GD32VF103_MFOL 对应 mr 的 ram_block，读取 HVA 基地址： 1(gdb) p s-\u0026gt;internal_rom.ram_block-\u0026gt;host 2$5 = (uint8_t *) 0x7ffff4400000 \u0026#34;\u0026#34; 计算 GPA 地址 0x1018对应的 HVA ， 也就是 0x7ffff4401018，对其下监视点： 1(gdb) watch *(0x7ffff4400000 + 0x1018) 2Hardware watchpoint 3: *0x7ffff4401018 继续执行，直到达到监视点，我们读取 HVA 里的数据，看看是不是客户机程序的起始地址： 1(gdb) c 2Thread 1 \u0026#34;qemu-system-ris\u0026#34; hit Hardware watchpoint 3: *0x7ffff4401018 3 4Old value = 0 5New value = 134217728 60x00007ffff696d565 in ?? () from /usr/lib/libc.so.6 7(gdb) x 0x7ffff4401018 80x7ffff4401018: 0x08000000 查看调用栈： 1(gdb) bt 2#0 0x00007ffff696d565 in ?? () from /usr/lib/libc.so.6 3#1 0x0000555555c01c08 in memcpy (__dest=\u0026lt;optimized out\u0026gt;, __src=0x55555692f0e0, __len=\u0026lt;optimized out\u0026gt;) 4 at /usr/include/bits/string_fortified.h:29 5#2 address_space_write_rom_internal (as=0x55555648c460 \u0026lt;address_space_memory\u0026gt;, addr=4096, attrs=..., 6 ptr=\u0026lt;optimized out\u0026gt;, len=32, type=type@entry=WRITE_DATA) at ../system/physmem.c:2967 7#3 0x0000555555c02b1c in address_space_write_rom (as=\u0026lt;optimized out\u0026gt;, addr=\u0026lt;optimized out\u0026gt;, attrs=..., 8 attrs@entry=..., buf=\u0026lt;optimized out\u0026gt;, len=\u0026lt;optimized out\u0026gt;) at ../system/physmem.c:2987 9#4 0x00005555558e0f2e in rom_reset (unused=\u0026lt;optimized out\u0026gt;) at ../hw/core/loader.c:1282 10#5 0x0000555555c6af0a in resettable_phase_hold (obj=0x555556930350, opaque=\u0026lt;optimized out\u0026gt;, type=\u0026lt;optimized out\u0026gt;) 11 at ../hw/core/resettable.c:184 12#6 0x0000555555c6a4c1 in resettable_container_child_foreach (obj=\u0026lt;optimized out\u0026gt;, 13 cb=0x555555c6adc0 \u0026lt;resettable_phase_hold\u0026gt;, opaque=0x0, type=RESET_TYPE_COLD) at ../hw/core/resetcontainer.c:54 14#7 0x0000555555c6ae5a in resettable_child_foreach (rc=0x5555566fbaa0, obj=0x5555567393f0, 15 cb=0x555555c6adc0 \u0026lt;resettable_phase_hold\u0026gt;, opaque=0x0, type=RESET_TYPE_COLD) at ../hw/core/resettable.c:96 16#8 resettable_phase_hold (obj=obj@entry=0x5555567393f0, opaque=opaque@entry=0x0, type=type@entry=RESET_TYPE_COLD) 17 at ../hw/core/resettable.c:173 18#9 0x0000555555c6b290 in resettable_assert_reset (obj=obj@entry=0x5555567393f0, type=type@entry=RESET_TYPE_COLD) 19 at ../hw/core/resettable.c:60 20--Type \u0026lt;RET\u0026gt; for more, q to quit, c to continue without paging-- 21#10 0x0000555555c6b651 in resettable_reset (obj=0x5555567393f0, type=RESET_TYPE_COLD) at ../hw/core/resettable.c:45 22#11 0x0000555555a6a3f4 in qemu_system_reset (reason=reason@entry=SHUTDOWN_CAUSE_NONE) at ../system/runstate.c:494 23#12 0x00005555558eaad3 in qdev_machine_creation_done () at ../hw/core/machine.c:1607 24#13 0x0000555555a6e043 in qemu_machine_creation_done (errp=0x5555564a0298 \u0026lt;error_fatal\u0026gt;) at ../system/vl.c:2677 25#14 qmp_x_exit_preconfig (errp=0x5555564a0298 \u0026lt;error_fatal\u0026gt;) at ../system/vl.c:2707 26#15 0x0000555555a717bb in qemu_init (argc=\u0026lt;optimized out\u0026gt;, argv=\u0026lt;optimized out\u0026gt;) at ../system/vl.c:3739 27#16 0x0000555555869ff9 in main (argc=\u0026lt;optimized out\u0026gt;, argv=\u0026lt;optimized out\u0026gt;) at ../system/main.c:47 28(gdb) 这里我们重点关注 rom_reset() :\n1rom_reset (unused=\u0026lt;optimized out\u0026gt;) at ../hw/core/loader.c:1282 对应源代码：\n1static void rom_reset(void *unused) 2{ 3 Rom *rom; 4 5 QTAILQ_FOREACH(rom, \u0026amp;roms, next) { 6 if (rom-\u0026gt;fw_file) { 7 continue; 8 } 9 /* 10 * We don\u0026#39;t need to fill in the RAM with ROM data because we\u0026#39;ll fill 11 * the data in during the next incoming migration in all cases. Note 12 * that some of those RAMs can actually be modified by the guest. 13 */ 14 if (runstate_check(RUN_STATE_INMIGRATE)) { 15 if (rom-\u0026gt;data \u0026amp;\u0026amp; rom-\u0026gt;isrom) { 16 /* 17 * Free it so that a rom_reset after migration doesn\u0026#39;t 18 * overwrite a potentially modified \u0026#39;rom\u0026#39;. 19 */ 20 rom_free_data(rom); 21 } 22 continue; 23 } 24 25 if (rom-\u0026gt;data == NULL) { 26 continue; 27 } 28 if (rom-\u0026gt;mr) { 29 void *host = memory_region_get_ram_ptr(rom-\u0026gt;mr); 30 memcpy(host, rom-\u0026gt;data, rom-\u0026gt;datasize); 31 memset(host + rom-\u0026gt;datasize, 0, rom-\u0026gt;romsize - rom-\u0026gt;datasize); 32 } else { 33 address_space_write_rom(rom-\u0026gt;as, rom-\u0026gt;addr, MEMTXATTRS_UNSPECIFIED, 34 rom-\u0026gt;data, rom-\u0026gt;datasize); 35 address_space_set(rom-\u0026gt;as, rom-\u0026gt;addr + rom-\u0026gt;datasize, 0, 36 rom-\u0026gt;romsize - rom-\u0026gt;datasize, 37 MEMTXATTRS_UNSPECIFIED); 38 } 39 if (rom-\u0026gt;isrom) { 40 /* rom needs to be written only once */ 41 rom_free_data(rom); 42 } 43 /* 44 * The rom loader is really on the same level as firmware in the guest 45 * shadowing a ROM into RAM. Such a shadowing mechanism needs to ensure 46 * that the instruction cache for that new region is clear, so that the 47 * CPU definitely fetches its instructions from the just written data. 48 */ 49 cpu_flush_icache_range(rom-\u0026gt;addr, rom-\u0026gt;datasize); 50 51 trace_loader_write_rom(rom-\u0026gt;name, rom-\u0026gt;addr, rom-\u0026gt;datasize, rom-\u0026gt;isrom); 52 } 53} 它负责在虚拟机启动或迁移后重新加载 ROM 数据到内存中。下面是对其逻辑的详细分析：\n循环遍历 ROM 列表: 函数通过遍历一个名为 roms 的链表来处理每一个 ROM 实例。这个链表包含了虚拟机中所有 ROM 的信息。\n检查 ROM 文件: 对于每一个 ROM 实例 rom，函数首先检查是否有对应的固件文件 (rom-\u0026gt;fw_file)。如果有，那么跳过此 ROM，因为它可能已经被固件加载器处理过了。\n迁移状态检查: 接下来，函数检查虚拟机是否正处于迁移过程中 (runstate_check(RUN_STATE_INMIGRATE)): 如果是迁移状态，那么 ROM 数据将在下次迁移完成后填充，因此当前不需要做任何事情。 如果 ROM 数据已经存在并且标记为只读 (rom-\u0026gt;isrom 为真)，那么释放已有的数据以防止覆盖可能被客户机修改的数据。\nROM 数据填充: 如果虚拟机不在迁移状态，并且 ROM 数据存在 (rom-\u0026gt;data != NULL)，函数会根据不同的情况将 ROM 数据写入内存： 如果 ROM 有一个关联的内存区域 (rom-\u0026gt;mr)，则获取该区域的物理内存指针 (memory_region_get_ram_ptr) 并将 ROM 数据复制到该内存区域。\n如果 ROM 没有关联的内存区域，而是直接与地址空间关联 (rom-\u0026gt;as)，则使用 address_space_write_rom 将数据写入指定地址，并使用 address_space_set 将剩余的空间清零。\n释放 ROM 数据: 如果 ROM 数据被标记为只读 (rom-\u0026gt;isrom 为真)，那么数据只需要写入一次，之后可以释放 ROM 数据缓冲区以节省内存。\n刷新指令缓存: 为了确保 CPU 能够从新的 ROM 数据中正确地取指令执行，函数调用 cpu_flush_icache_range 来清除对应内存范围的指令缓存。 跟踪记录: 最后，函数调用 trace_loader_write_rom 来记录 ROM 加载的信息，这可能用于调试或性能分析。\n到这里，我们大概了解它是怎么将客户机程序起始地址，写入对应的位置了。\n但是我们还不清楚，是怎么从 -kernel 参数解析客户机程序的。\n碍于篇幅，今天先到这里，下篇文章我们继续探究。\n","permalink":"https://zevorn.cn/posts/12/","summary":"QEMU 启动以后，并没有立刻执行客户机程序的第一条指令，而是先执行 Machine 在初始化阶段设置的 reset vector 程序段，然后再跳转到客户机程序的第一条指令。 因此第一条指令的PC地址为 memmap GD32VF103 MFOL .base + 0x1000，通过以上代码得知，memmap GD32VF103 MFOL .base 为0，","title":"分析 QEMU 的 GD32VF103 启动流程"},{"content":"当前成果：\n1git clone -b nuclei_gd32vf103 https://gitee.com/gevico/qemu.git 下面记录关键 patch 解决的问题：\n[patch] 修复 dts 编译警告问题 1commit c48d9d247e6bf30153b8e5e91dab4f63ac847a80 2Author: zevorn \u0026lt;zevorn@yeah.net\u0026gt; 3Date: Tue Jul 23 00:38:22 2024 +0800 4 5 pc-bios: Fix the compilation warning when dtb was generated 6 7 It is mainly related to the memory and opb nodes in bamboo.dts. 这个 patch 主要解决了编译过程中遇到的 dts 编译警告，后续主线应该会修复该问题。\n[patch] 添加 nuclei n205 核心 1commit 8b0a11489cb4c7d796e545fb4bb59a0ada6ed09d 2Author: zevorn \u0026lt;zevorn@yeah.net\u0026gt; 3Date: Mon Jul 22 22:57:21 2024 +0800 4 5 target/riscv: Add nuclei_n205 core for RISCV architecture 初始化 N205 核心的时候，用的 DEFINE_VENDOR_CPU()，可扩展厂商自定义指令集。\n1diff --git a/target/riscv/cpu-qom.h b/target/riscv/cpu-qom.h 2index 3670cfe6d9..7351cb4b0b 100644 3--- a/target/riscv/cpu-qom.h 4+++ b/target/riscv/cpu-qom.h 5@@ -50,6 +50,7 @@ 6 #define TYPE_RISCV_CPU_THEAD_C906 RISCV_CPU_TYPE_NAME(\u0026#34;thead-c906\u0026#34;) 7 #define TYPE_RISCV_CPU_VEYRON_V1 RISCV_CPU_TYPE_NAME(\u0026#34;veyron-v1\u0026#34;) 8 #define TYPE_RISCV_CPU_HOST RISCV_CPU_TYPE_NAME(\u0026#34;host\u0026#34;) 9+#define TYPE_RISCV_CPU_NUCLEI_N205 RISCV_CPU_TYPE_NAME(\u0026#34;nuclei-n205\u0026#34;) [patch] 添加 gd32vf103 Machine 1commit aa5224ecb03957825982f52579dae0dc02a93c98 2Author: zevorn \u0026lt;zevorn@yeah.net\u0026gt; 3Date: Mon Jul 22 22:58:01 2024 +0800 4 5 hw/riscv: Add gd32vf103 mcu for RISCV architecture 没有实现具体外设，只增加了一些 mr 的初始化：\n1static void nuclei_board_init(MachineState *machine) 2+{ 3+ const struct MemmapEntry *memmap = gd32vf103_memmap; 4+ NucleiGDState *s = g_new0(NucleiGDState, 1); 5+ MemoryRegion *system_memory = get_system_memory(); 6+ MemoryRegion *main_mem = g_new(MemoryRegion, 1); 7+ s-\u0026gt;soc.sram = *main_mem; 8+ int i; 9+ 10+ /* TODO: Add qtest support */ 11+ /* Initialize SOC */ 12+ object_initialize_child(OBJECT(machine), \u0026#34;soc\u0026#34;, \u0026amp;s-\u0026gt;soc, TYPE_NUCLEI_GD32VF103_SOC); 13+ qdev_realize(DEVICE(\u0026amp;s-\u0026gt;soc), NULL, \u0026amp;error_abort); 14+ 15+ memory_region_init_ram(\u0026amp;s-\u0026gt;soc.main_flash, NULL, \u0026#34;riscv.nuclei.main_flash\u0026#34;, 16+ memmap[GD32VF103_MAINFLASH].size, \u0026amp;error_fatal); 17+ memory_region_add_subregion(system_memory, 18+ memmap[GD32VF103_MAINFLASH].base, \u0026amp;s-\u0026gt;soc.main_flash); 19+ 20+ memory_region_init_ram(\u0026amp;s-\u0026gt;soc.boot_loader, NULL, \u0026#34;riscv.nuclei.boot_loader\u0026#34;, 21+ memmap[GD32VF103_BL].size, \u0026amp;error_fatal); 22+ memory_region_add_subregion(system_memory, 23+ memmap[GD32VF103_BL].base, \u0026amp;s-\u0026gt;soc.boot_loader); 24+ 25+ memory_region_init_ram(\u0026amp;s-\u0026gt;soc.ob, NULL, \u0026#34;riscv.nuclei.ob\u0026#34;, 26+ memmap[GD32VF103_OB].size, \u0026amp;error_fatal); 27+ memory_region_add_subregion(system_memory, 28+ memmap[GD32VF103_OB].base, \u0026amp;s-\u0026gt;soc.ob); 29+ 30+ memory_region_init_ram(\u0026amp;s-\u0026gt;soc.sram, NULL, \u0026#34;riscv.nuclei.sram\u0026#34;, 31+ memmap[GD32VF103_SRAM].size, \u0026amp;error_fatal); 32+ memory_region_add_subregion(system_memory, 33+ memmap[GD32VF103_SRAM].base, \u0026amp;s-\u0026gt;soc.sram); 34+ 35+ /* reset vector */ 36+ uint32_t reset_vec[8] = { 37+ 0x00000297, /* 1: auipc t0, %pcrel_hi(dtb) */ 38+ 0x02028593, /* addi a1, t0, %pcrel_lo(1b) */ 39+ 0xf1402573, /* csrr a0, mhartid */ 40+#if defined(TARGET_RISCV32) 41+ 0x0182a283, /* lw t0, 24(t0) */ 42+#elif defined(TARGET_RISCV64) 43+ 0x0182b283, /* ld t0, 24(t0) */ 44+#endif 45+ 0x00028067, /* jr t0 */ 46+ 0x00000000, 47+ memmap[GD32VF103_MAINFLASH].base, /* start: .dword */ 48+ 0x00000000, 49+ /* dtb: */ 50+ }; 51+ 52+ /* copy in the reset vector in little_endian byte order */ 53+ for (i = 0; i \u0026lt; sizeof(reset_vec) \u0026gt;\u0026gt; 2; i++) 54+ { 55+ reset_vec[i] = cpu_to_le32(reset_vec[i]); 56+ } 57+ rom_add_blob_fixed_as(\u0026#34;mrom.reset\u0026#34;, reset_vec, sizeof(reset_vec), 58+ memmap[GD32VF103_MFOL].base + 0x1000, \u0026amp;address_space_memory); 59+ 60+ /* boot rom */ 61+ if (machine-\u0026gt;kernel_filename) 62+ { 63+ riscv_load_kernel(machine, \u0026amp;s-\u0026gt;soc.cpus, 64+ memmap[GD32VF103_MAINFLASH].base, 65+ false, NULL); 66+ } 67+} [patch] 增加 eclic 外设，完善中断控制流程 1commit 72a1f9a351d5b3adf2dd8dbcbb1505e9f3824ec6 2Author: zevorn \u0026lt;zevorn@yeah.net\u0026gt; 3Date: Thu Jul 25 00:05:12 2024 +0800 4 5 hw/riscv: Add the intc device to gdv32vf103 cpu 6 7commit ac3c55d0e19cb03bb31bd8f580ec24f18eb18590 8Author: zevorn \u0026lt;zevorn@yeah.net\u0026gt; 9Date: Thu Jul 25 00:05:02 2024 +0800 10 11 hw/intc: Add the gdv32vf103_eclic device 这部分还没有仔细研究，直接移植的 PLCT 原本的代码，但是做了 patch 整理，代码更加清晰。\n[patch] 增加 nuclei-n205 CSR 扩充的寄存器的支持 1commit 7df1ce25dd9ea24fe1c4e9a4bc0ed41d933495e2 2Author: zevorn \u0026lt;zevorn@yeah.net\u0026gt; 3Date: Sun Jul 28 17:04:20 2024 +0800 4 5 hw/riscv: Add the rcu device to gdv32vf103 cpu 6 target/riscv: Add CSR register support for nuclei N205 这个 patch 有几个关键修改。\n需要将 n205 的指令集版本设置为 latest，不然不支持 CSR_MCOUNTINHIBIT 寄存器，或者运行时有警告，另外需要将 cpu-\u0026gt;cfg.ext_zifencei 和 cpu-\u0026gt;cfg.ext_zicsr 设置为 true，不然有些 CSR 寄存器也不支持，会导致 startup 阶段访问某些 CSR 寄存器时意外陷入异常:\n1diff --git a/target/riscv/cpu.c b/target/riscv/cpu.c 2index 326576fe20..1d9d36ddb2 100644 3--- a/target/riscv/cpu.c 4+++ b/target/riscv/cpu.c 5@@ -684,13 +684,15 @@ static void rv32imacu_nuclei_cpu_init(Object *obj) 6 #endif 7 8 riscv_cpu_set_misa_ext(env, RVI | RVM | RVA | RVC | RVU); 9- env-\u0026gt;priv_ver = PRIV_VERSION_1_10_0; 10+ env-\u0026gt;priv_ver = PRIV_VERSION_LATEST; // support CSR_MCOUNTINHIBIT 11 12 /* inherited from parent obj via riscv_cpu_init() */ 13 qdev_prop_set_bit(DEVICE(obj), \u0026#34;mmu\u0026#34;, false); 14 #ifndef CONFIG_USER_ONLY 15 env-\u0026gt;resetvec = DEFAULT_RSTVEC; 16 #endif 17+ cpu-\u0026gt;cfg.ext_zifencei = true; 18+ cpu-\u0026gt;cfg.ext_zicsr = true; 19 cpu-\u0026gt;cfg.pmp = true; 20 } [patch] 增加 gd32vf103 startup 阶段需要的几个外设 1commit b37aac1ef3641e4aff84c9d95f48ff1cba8b2b74 (HEAD -\u0026gt; nuclei_gd32vf103) 2Author: zevorn \u0026lt;zevorn@yeah.net\u0026gt; 3Date: Sun Jul 28 17:16:46 2024 +0800 4 5 hw/riscv: Add the gpio device to gdv32vf103 cpu 6 7commit 5969602dcb461db5cba548ba83c29069be097dd8 8Author: zevorn \u0026lt;zevorn@yeah.net\u0026gt; 9Date: Sun Jul 28 17:16:39 2024 +0800 10 11 hw/gpio: Add the nuclei_gpio device 12 13commit 60d9091f591599635af9ef49625962c25e50c2dc 14Author: zevorn \u0026lt;zevorn@yeah.net\u0026gt; 15Date: Sun Jul 28 17:04:20 2024 +0800 16 17 hw/riscv: Add the rcu device to gdv32vf103 cpu 18 19commit e0af6cbaf0307ebd727ae5bdf471920733557f08 20Author: zevorn \u0026lt;zevorn@yeah.net\u0026gt; 21Date: Sun Jul 28 17:04:07 2024 +0800 22 23 hw/timer: Add the nuclei_rcu device 这里使用 riscv-gdb 远程 remote 到 qemu 以后，观察异常点的函数调用栈，可以追溯是哪个外设没实现，导致的访问寄存器时触发了异常，然后将对应的外设移植过来即可。\n比较简单，这里不赘述移植过程，只说一下调试的流程，命令如下：\nopen 一个终端，启动 qemu ：\n1cd qemu 2qemu-system-riscv32 -M gd32vf103v_rvstar -cpu nuclei-n205 -icount shift=0 -nodefaults -nographic -serial stdio -kernel nuclei-sdk/application/baremetal/helloworld/helloworld.elf -gdb tcp::1234 -S open 一个新的终端，启动 gdb 来调试客户程序：\n1riscv-nuclei-linux-gnu-gdb ../nuclei-sdk/application/baremetal/helloworld/helloworld.elf 2(gdb) target remote localhost:1234 3(gdb) run 4(gdb) ctrl + c 5(gdb) bt 6... 这里可以根据函数调用栈，来定位具体异常对应的外设，或者其他指令。\nPs：对于来自客户程序的异常行为，即使是 qemu 这边没有实现对应的指令或者外设，亦或者没有初始化一些内存区域，也是按照 Guest Machine 的异常来处理，这样非常方便通过客户程序的指令流或者函数调用流，来定位出错的地方。\n","permalink":"https://zevorn.cn/posts/11/","summary":"当前成果： 下面记录关键 patch 解决的问题： patch 修复 dts 编译警告问题 这个 patch 主要解决了编译过程中遇到的 dts 编译警告，后续主线应该会修复该问题。 patch 添加 nuclei n205 核心 初始化 N205 核心的时候，用的 DEFINE VENDOR CPU()，可扩展厂商自定义指令集。 patch 添加 gd32","title":"记录移植 PLCT 实验室的 qemu-nuclei_gd32vf103 到 QEMU v9.0.2 的一些经验"},{"content":"为了方便学习 QEMU 的 tcg 模块，尝试从零裁剪 QEMU 源代码，仅保留和 TCG 相关的模块。通过裁剪源代码，可以深入了解 QEMU 的代码组织结构，为学习 QEMU 打造一个坚实的基础。\nbase QEMU version: 8.2.0\nQEMU 构建系统 QEMU 使用 meson + ninja 构建（make 也支持），脚本语言风格类似 python ，可读性高，编译构建速度快。 除了 meson ，还需要关注 QEMU 源代码中的 ./configure 脚本和 Kconfig 脚本，前者是顶层配置文件，后者包含了具体编译的源代码路径（与 meson.build 配合使用）。\n因此裁剪工作的第一步，实现了解 ./configrue 的各个编译选项。 这里的主要思路是，仅保留 TCG 相关的 user 模式， GDB 远程调试、用户交互、插件等核心组件，涉及 system 模式相关的代码全部移除。 所以可以通过修改 ./configrue 实现：\n1system=\u0026#34;no\u0026#34; 2linux_user=\u0026#34;yes\u0026#34; 下一步就是根据 meson.build ，逐步移除不需要的代码，下面对一些被移除的模块进行说明。\n","permalink":"https://zevorn.cn/posts/10/","summary":"为了方便学习 QEMU 的 tcg 模块，尝试从零裁剪 QEMU 源代码，仅保留和 TCG 相关的模块。通过裁剪源代码，可以深入了解 QEMU 的代码组织结构，为学习 QEMU 打造一个坚实的基础。 base QEMU version: 8.2.0 QEMU 构建系统 QEMU 使用 meson + ninja 构建（make 也支持），脚本语言风格类似 p","title":"从零裁剪 QEMU 源代码"},{"content":"uops 词义解析 μops 代指 uop ，全称是 Micro-operation ，中文直译为“微操作”，micro 可以希腊字母 μ 代替，然后 operation 可以缩写为 op ，连一起即 μop。为方便书写，使用 u 代替 μ 。用 uops 来统称所有的 uop ，即 Micro-operations （为了方便，下文统一称为 uops ）。\n对于 x86 这种 CISC 处理器，为了能在内部更加高效灵活地处理指令，会将长度和复杂度不一的指令，转化成一个或多个 uop，以便更容易被调度和优化，从而达到提升 CPU 性能的目的。\nx86 的指令可以理解成是粗颗粒度的，而 uop 可以理解成是细颗粒的微小指令。通过将复杂的粗粒度指令分解成简单的细粒度指令执行，变相获取了 RISC 架构的部分优势，让CPU的整体结构更加紧凑、灵活和高效。\nuops 的优势 将指令分割成微操作的主要优点是：\n乱序执行 以 PUSH rbx 指令为例，它将栈指针减少 8 字节，然后将源操作数存储在栈顶。假设在解码后 PUSH rbx 被“破解”成两个依赖的 uop：\nSUB rsp, 8 STORE [rsp], rbx 通常，函数序言通过使用多个 PUSH 指令保存多个寄存器。在我们的例子中，下一个 PUSH 指令可以在前一个 PUSH 指令的 SUB μop 完成后开始执行，而不必等待当前要执行的 STORE μop。\n并行执行 以 HADDPD xmm1, xmm2 指令为例，它将在 xmm1 和 xmm2 中对两个双精度浮点数进行求和（减少），并将两个结果存储在 xmm1 中，如下所示：\nxmm1[63:0] = xmm2[127:64] + xmm2[63:0] xmm1[127:64] = xmm1[127:64] + xmm1[63:0] 微代码化此指令的一种方法是执行以下操作：\n减少 xmm2并将结果存储在 xmm_tmp1[63:0] 中； 减少 xmm1并将结果存储在 xmm_tmp2[63:0] 中； 将 xmm_tmp1 和 xmm_tmp2 合并到 xmm1 中。 总共三个 uop。对于步骤 1 和 2 是独立的，因此可以并行完成。\n操作融合 有时 uops 也可以融合在一起，现代 CPU 中有两种类型的融合：\n微融合：融合来自同一机器指令的 uops。微融合只能应用于两种类型的组合：内存写操作和读改操作。例如：\nadd eax, [mem] 这条指令中有两个 uops：\n读取内存位置 mem; 将其添加到 eax。 使用微融合，在解码步骤中将两个 uops 融合成一个。\n宏融合: 融合来自不同机器指令的 uops。在某些情况下，解码器可以将算术或逻辑指令与 subsequent 条件跳转指令融合成单个计算和分支 uop。例如：\n.loop: dec rdi jnz .loop 使用宏融合，将来自 DEC 和 JNZ 指令的两个 uops 融合成一个。\n微融合和宏融合都可以节省从解码到退休的所有管道阶段的带宽。融合操作在重新排序缓冲区 (ROB) 中共享单个条目。当一个融合的 uop 只使用一个条目时，ROB 的容量得到更好的利用。\n统计 uops 要收集应用程序发出的、执行的和退休的 uops 数量，可以使用 Linux perf，如下所示：\n1$ perf stat -e uops_issued.any,uops_executed.thread,uops_retired.slots -- ./a.exe 22856278 uops_issued.any 32720241 uops_executed.thread 42557884 uops_retired.slots 指令被分解成微操作的方式可能会随着 CPU 世代的不同而有所差异。通常，用于一条指令的 uops 数量越少，意味着硬件对其支持越好，并且可能具有更低的延迟和更高的吞吐量。对于最新的 Intel 和 AMD CPU，绝大多数指令都会生成恰好一个 uop。\n参考资料： [1] 现代 CPU 性能分析与优化 [2] uops 哲学三问\n","permalink":"https://zevorn.cn/posts/9/","summary":"uops 词义解析 μops 代指 uop ，全称是 Micro operation ，中文直译为“微操作”，micro 可以希腊字母 μ 代替，然后 operation 可以缩写为 op ，连一起即 μop。为方便书写，使用 u 代替 μ 。用 uops 来统称所有的 uop ，即 Micro operations （为了方便，下文统一称为 uops ）。","title":"浅析 x86 架构的微操作(uops)"},{"content":"用例和先决条件 本篇文章将展示，在 Windows 平台如何通过 Intel® VTune™ Profiler （下文简称 VTune）来识别和分析串行/并行应用程序中的性能瓶颈。使用 VTune 自带的 matrix 示例矩阵乘法应用程序作为分析和优化对象（备注：由于采用 Github 作为文章图床，部分图片加载速度稍慢）。\n本文章要求安装多个英特尔软件工具，如果为了方便，可以直接下载 Intel® oneAPI 基本工具套件，详细链接如下：\nIntel® oneAPI Base Toolkit (Recommended) Standalone Intel® VTune™ Profiler Standalone Intel® oneAPI DPC++/C++ Compiler PS: 本文章使用 Intel® oneAPI DPC++/C++ 编译器为分析和性能增益跟踪建立通用基线。根据您使用的编译器，结果和工作流程可能会有所不同。另外当前使用的 VTune 版本为 2021，使用其他版本可能与下文操作有细微差异。\n基本流程简述 文章将按照以下步骤识别并修复示例 matrix 应用程序中最突出的性能问题：\n建立应用程序性能基线 确定矩阵应用程序中的主要瓶颈 消除内存访问瓶颈 评估性能改进 解决矢量化问题 进行微架构分析 可视化性能提升 以上步骤的详细操作流程，将在下文，分章节讲述。\n建立程序性能基线 运行性能快照分析 在 VTune 中分析应用程序的第一步是创建项目。项目是保存分析目标配置和数据收集结果的容器。VTune 提供了一个预配置的示例矩阵项目，以与预构建的矩阵示例应用程序配合使用。\n首先打开预配置 matrix 的项目：\n启动 VTune Profiler GUI。 a. 运行以下脚本以设置相应的环境变量：\n1install-dir\\env\\vars.bat PS: 对于 VTune ，默认 install-dir 值为：[Program Files]\\Intel\\oneAPI\\vtune\\version\nb. 在“开始”菜单中找到 VTune Profiler 图标，然后启动 VTune Profiler。\n可能需要以管理员身份运行 VTune 才能使用某些分析类型。\nVTune 欢迎屏幕在产品启动后显示。 ample (matrix) 项目应已在项目导航器中打开。如果是这样，则无需进一步操作。如果 sample (matrix) 项目导航器中的项目不可用，则需要手动打开项目：\na. 单击 Menu 按钮，然后选择 Open-\u0026gt;Project\u0026hellip; 以打开现有项目。\nb. 浏览到本地计算机上的项目， matrix 然后单击 Open 。\n默认情况下，它位于以下目录中：\n1[Users]\\\u0026lt;user\u0026gt;\\Documents\\VTune\\Projects\\sample (matrix) VTune 在项目导航器中打开 matrix 项目。\n要启动 matrix 示例应用程序的性能快照分析，请执行以下操作：\n单击 Configure Analysis 按钮开始新的分析。默认分析是针对 Performance Snapshot 分析预先配置的，用于分析本地系统上的 matrix 应用程序。 继续单击 Start 按钮运行分析。如图所示：\n手动运行一段时间后点击暂停退出按钮，VTune 将完成收集的结果，并打开 Performance Snapshot 分析的 Summary 视点。\n分析性能快照结果 “性能快照结果摘要”选项卡显示以下内容：\nAnalysis tree：性能快照提供了其他分析类型，这些分析类型可能有助于更深入地调查应用程序中发现的性能问题。与应用程序中检测到的性能问题相关的分析类型以红色突出显示。 Metrics Panes：这些窗格显示对估计应用程序性能贡献最大的高级指标。有问题的区域以红色突出显示。您可以展开每个窗格以获取有关每个问题区域的详细信息，并查看有助于判定的较低级别指标。 Collection and Platform Info：此窗格显示有关收集此特定结果的系统的信息。打开在其他硬件平台上收集的结果时，它很有用。 在这种情况下，可观察到以下突出性能瓶颈的主要指标：\n此应用程序的 Elapsed Time 非常高。 Memory Bound 指标较高，表示访存问题。因此，性能快照将突出显示访存分析，作为潜在的起点，并指示此性能瓶颈最严重，并且对总运行时间的贡献最大。 对于现代超标量处理器，IPC（每个周期的指令数）指标值非常低，表明处理器在大部分时间都处于停滞状态。 性能快照分析突出显示了 Hotspots 是一个很好的起点。通常， Hotspots 是首次深入分析的良好候选者。它突出显示热点或对运行时间贡献最大的代码区域。 接下来，从热点分析开始，查看 matrix 应用程序中的哪个代码区域对性能问题的影响最大。\n分析程序性能瓶颈 运行 Hotspots 并分析 要从“性能快照摘要”窗口运行“热点”分析，请执行以下操作：\n单击“分析”树中的 Hotspots 图标，打开 Configure Analysis 窗口。 在 WHERE 窗格中，选择 Local Host。 如果使用的是预提供 sample (matrix) 的项目，则应已配置 WHAT 窗格，如果没有，请在 Application 文本框中提供应用程序的路径。 在 HOW 窗格中，预选了 Hotspots 分析。对于收集模式，可以在 User-Mode Sampling 和 Hardware Event-Based Sampling 的采样之间进行选择。这些采样方法各不相同，但通常，最好尽可能使用基于硬件事件的采样，因为它以更低的开销提供更详细的信息。 单击 Start 按钮运行分析。 示例应用程序退出后，VTune 将最终确定结果并打开“摘要”视点。\n此视点提供了多个指标。将鼠标悬停在问号图标上可获取每个指标的详细说明。\n请注意，应用程序的总 CPU Time 大约等于 642 秒。它是应用程序中所有线程的 CPU 时间总和。总线程数为 9，因此应用程序是多线程的。\nSummary 窗口的 Top Hotspots 部分提供有关最耗时的函数（热点函数）的数据，这些函数按执行所花费的 CPU 时间排序。对于示例应用程序，该 multiply1 函数大约需要 640 秒才能执行，但显示在列表顶部，显示为最热门的函数。\nSummary 窗口下方的 Effective CPU Utilization Histogram 直方图表示可用逻辑处理器的“已用时间和使用率级别”（Elapsed Time and usage level），并提供应用程序执行期间使用的逻辑处理器数的图形外观。理想情况下，图表的最高条形应与目标利用率级别匹配。\n若要获取代码的每个函数视图，请切换到 Bottom-up 选项卡。默认情况下，网格中的数据按函数分组。可以使用网格顶部的 Grouping 菜单更改分组级别。\n该 multiply1 函数执行所需的时间最多，约为 640 秒，并且显示 CPU 利用率很差。\n若要获取每个函数的详细 CPU 利用率信息，请使用 Bottom-up 窗格中的扩展按钮展开“按利用率划分的有效时间”列项。\n双击 Bottom-up 网格上的函数 multiply1 以打开源代码窗口。\nPS：如果分析对象是其他程序，需要保证该程序包含调试信息，并保持源代码路径与调试信息一致。\n请注意，最耗时的行归因于在 multiply1 函数中执行矩阵乘法的循环。若要分析此循环与内存相关的行为，还运行 Memory Access 分析。\n运行 Memory Access 并分析 要运行内存访问分析，请执行以下操作：\n单击先前收集的 Performance Snapshot 结果中的 Memory Access 图标，或单击主工具栏中的 Configure Analysis 按钮。 如果单击 Memory Access 图标，则应预先选择“内存访问”分析。如果没有，请在 HOW 窗格中选择此分析类型。 在 HOW 窗格中，禁用 Analyze OpenMP regions 选项，因为此应用程序不需要该选项。 单击 Start 按钮运行分析。 分析结果如下：\n再次注意，应用程序受到内存访问的严格限制。系统不单独受 DRAM 带宽约束这一事实表明，应用程序受频繁但较小的内存请求的约束，而不是受饱和的物理 DRAM 带宽的约束。\n切换到 Bottom-up 选项卡，查看 multiply1 该函数的确切指标。\n该 multiply1 函数位于网格的顶部，具有最高的 CPU 时间和较高的内存绑定指标值。\n请注意，LLC Miss Count 指标非常高。这表明应用程序使用对缓存不友好的内存访问模式，这会导致处理器经常错过 LLC 并从 DRAM 请求数据，这在延迟方面代价高昂。\n解决此问题的一个好方法是应用循环交换技术，在这种情况下，该技术会更改矩阵的行和列在主循环中的寻址方式。这样，消除了低效的内存访问模式，使处理器能够更好地利用 LLC。\n消除内存访问瓶颈 按照以下步骤使用 Intel® oneAPI DPC++/C++ 编译器在 Microsoft Visual Studio 中编辑和重新编译代码：\n在计算机上找到矩阵示例应用程序文件夹。默认情况下，它被放置在：[Documents]\\VTune\\samples\\matrix； 打开位于文件夹中的 matrix.sln ..\\matrix\\vc15 Visual Studio 解决方案； 请确保在生成应用程序时启用了发布配置和 x64 平台； 在“解决方案资源管理器”中，右键单击项目， matrix 然后选择 Properties； 在 Configuration Properties -\u0026gt; General 中，将 Platform Toolset 更改为 Intel C++ Compiler version ； 在 C/C++ -\u0026gt; General 菜单中，确保 Debug Information Format 设置为 Program Database (/Zi) ； 在 C/C++ -\u0026gt; Optimization 菜单中，确保 Optimization 选项设置为 Maximum Optimizations (Favor Size) (/O1) ； 在 ** C/C++ \u0026gt; Diagnostics [Intel C++]** 菜单中，将 Optimization Diagnostic Level 设置为 Level 2 (/Qopt-report:2) ； 在 multiply.h 头文件的第 36 行，更改以下行： 1- #define MULTIPLY multiply1 2+ #define MULTIPLY multiply2 第 9 步，将程序更改为使用 multiply.c 源文件中的 multiply2 函数，该函数实现了解决内存访问问题的 loop interchange 技术。最后生成应用程序。\n评估性能改进 若要查看使用 loop interchange 技术提供的改进，再次运行性能快照分析：\n观察以下主要指标：\n应用程序的 Elapsed Time 显着减少。这种改进主要是由于消除了内存访问瓶颈，这导致处理器经常错过缓存并从 DRAM 请求数据，这在延迟方面非常昂贵。 Vectorization 指标等于 0.0%，这意味着代码未矢量化。因此，Performance Snapshot 将 HPC 性能特性分析作为潜在的下一步。 在这种情况下，代码未矢量化，因为 Intel® oneAPI DPC++/C++ 编译器在使用首选的二进制大小 （/O1） 进行编译时不执行矢量化。\n若要通过 Visual Studio 启用编译器的自动矢量化，请按照下列步骤操作：\n右键单击项目， matrix 然后选择 Properties 。 在 C/C++ \u0026gt; Optimization 菜单中，将 Optimization 选项设置为 Maximum Optimization (Favor Speed) (/O2) 。 保存配置更改，生成应用程序。 解决矢量化问题 在启用了 /O2 级别的情况下重新编译应用程序后，再次运行性能快照分析，以分析矢量化效率：\n观察以下主要指标：\n总体 Vectorization 指标等于 99.9%，表示代码已矢量化。 但是，128-bit Packed FLOPs 指标旁边有危险信号。将鼠标悬停在红旗图标或指标值上以获取问题描述。 在这种情况下，VTune 指示浮点指令的很大一部分是在部分矢量负载下执行的。\n由于分析是在基于能够使用 AVX2 指令集的 Intel 处理器的机器上执行的，因此所有指令都仅使用 128 位寄存器执行，这意味着根本没有使用 256 位宽的 AVX2 寄存器。因此，VTune 将 128 位向量寄存器的 100.0% 利用率标记为问题。\n要了解实际使用的向量指令集，需要运行 HPC 性能表征分析：\n要运行分析，请执行以下操作：\n单击分析树中的 HPC Performance Characterization 图标； 禁用 Collect stacks 、Analyze Memory bandwidth 和 Analyze OpenMP regions 选项，因为矢量化分析不需要这些选项； 单击 Start 按钮运行分析。 数据收集完成后，VTune 将打开 HPC 性能特性分析的默认摘要窗口:\n将焦点放在 Summary 窗口的 Vectorization 部分。\n请注意，该 multiply2 函数的主循环使用较旧的 SSE2 指令集进行矢量化，而编译和分析是在支持 AVX2 的处理器上执行的。因此，部分硬件资源仍未得到充分利用。\n为了能够使用适合于平台的向量指令集，一种可能的方法是指示编译器使用与执行编译的处理器中可用的最佳向量扩展相同的向量扩展。\n按照以下步骤使用 Visual Studio 启用适合平台的矢量化：\n在 Solution Explorer 窗格中，右键单击矩阵项目，然后选择 Properties ； 导航到 C/C++ \u0026gt; Code Generation [Intel C++] 菜单； 将 Intel Processor-Specific Optimization 选项设置为 Same as the host processor performing the compilation (/QxHost) ，以指示编译器使用执行编译的处理器上可用的最佳指令集扩展； 保存修改，生成应用程序。 运行 Performance Snapshot 分析以确保应用程序已正确矢量化。\n观察以下主要指标：\nThe for the application has slightly decreased.\n应用程序的 Elapsed Time 略有减少。 Vectorization 指标等于 99.9%，因此代码是完全矢量化的。 总共 100.0% 的 Packed DP FLOP 指令使用 256 位寄存器执行。因此，即使不运行 HPC 性能表征分析，结论也是 AVX2 向量扩展得到了充分利用。 VTune 还突出显示了微架构使用情况指标，并提供使用微架构探索分析，来了解应用程序对微架构的利用率不足的确切情况。 进行微架构分析 虽然之前的优化为应用程序的总运行时间带来了巨大的好处，但仍有需要改进的地方。性能快照分析强调，微架构没有得到很好的利用。\n运行 Microarchitecture Exploration 分析以识别改进机会。\n要运行微架构探索分析，请执行以下操作：\n在 Performance Snapshot 分析树中，单击 Microarchitecture Exploration 分析图标； 在 HOW 窗格中，启用所有额外选项； 单击 Start 按钮运行分析。 分析结果如下：\n此视图显示以下内容：\nElapsed Time section：此部分显示与硬件的硬件利用率级别相关的指标。将鼠标悬停在标记的指标上，以获取问题说明、可能的原因以及解决问题的建议。Microarchitecture Exploration 视点中基于事件的指标的层次结构取决于您的硬件架构。每个指标都是由英特尔架构师定义的事件比率，并具有自己的预定义阈值。VTune 分析每个聚合程序单元（例如，函数）的比率值。当此值超过阈值时，表示存在潜在的性能问题。 µPipe Diagram：μPipe 或微架构管道提供 CPU 微架构指标的图形表示，显示硬件使用效率低下。将关系图视为输出流量等于以下比率的管道：实际指令已停用/可能的最大指令已停用（管道效率）。μPipe 基于 CPU 流水线插槽，这些插槽表示处理一个微操作所需的硬件资源。通常，每个循环（管道宽度）上都有多个管道槽可用。如果管道槽没有退役，则将其视为失速，µPipe Diagram 将其表示为使管道变窄的障碍物。 Effective CPU Utilization Histogram：此直方图表示可用逻辑处理器的运行时间和使用级别，并提供应用程序执行期间使用的逻辑处理器数量的图形外观。理想情况下，图表的最高条形应与目标利用率级别匹配。 在这种情况下，请遵守以下指标：\nMemory Bound 指标较高，因此应用程序受内存访问的约束。 Memory Bandwidth 和 Memory Latency 指标较高。 综合考虑这些因素，得出的结论是应用程序存在访存问题。但是，此问题在性质上与以前使用 loop interchange 技术解决的访存问题略有不同。\n在引入 loop interchange 之前，应用程序主要受到缓存不友好的内存访问模式的约束，这导致了大量的 LLC（最后一级缓存）未命中。这反过来又导致了对DRAM的频繁请求。\n通常，大多数开发人员在达到所需的性能目标时会停止进一步优化其应用程序。通过优化 matrix 应用程序获得的性能改进使应用程序墙时间从大约 90 秒减少到大约 2.5 秒。\n如果要进一步试验，可以修改代码以实现缓存阻塞技术。缓存阻止是一种重新排列数据访问的方法，其方式是将数据块加载到缓存中，并在需要时重复使用，从而大大减少 DRAM 访问的数量。\nTo modify the code to use the cache blocking technique: 若要修改代码以使用缓存阻止技术，请执行以下操作：\n在 multiply.h 头文件中，更改第 36 行：\n1- #define MULTIPLY multiply2 2+ #define MULTIPLY multiply4 保存更改并重新编译应用程序。\n这将修改代码以使用 multiply.c 源文件中的 multiply4 函数，该函数实现 cache blocking 技术。\n重新编译应用程序后，可以运行所选的分析以确定性能改进。\n可视化性能提升 可以使用 VTune 的性能分析结果对比功能，以更好地了解性能变化。\n虽然可以比较不同分析类型（如热点和性能快照）的结果，但仅显示同时适用于两种分析类型的指标。\n要比较结果：\n单击 Main Toolbar 中的 Compare Results 按钮； 选择要比较的结果。 VTune 计算指标之间的差异，并打开默认的 ** Summary** 窗口：\n可以看到，对于 matrix 示例应用程序，运行时间减少了近 88 秒。\n注意事项 点我查看\n","permalink":"https://zevorn.cn/posts/8/","summary":"用例和先决条件 本篇文章将展示，在 Windows 平台如何通过 Intel® VTune™ Profiler （下文简称 VTune）来识别和分析串行/并行应用程序中的性能瓶颈。使用 VTune 自带的 matrix 示例矩阵乘法应用程序作为分析和优化对象（备注：由于采用 Github 作为文章图床，部分图片加载速度稍慢）。 本文章要求安装多个英特尔软件工","title":"Intel® VTune™ Profiler 分析 C++ 程序的常见性能瓶颈( Windows 平台)"},{"content":"一、基本介绍 AsmJit 是一个完整的 JIT ( just In Time, 运行时刻)的针对 C++ 语言的汇编器，可以生成兼容 x86 / x64 和 Aarch64 架构的原生代码，不仅支持整个x86/x64 的指令集（包括传统的 MMX 和最新的向量指令集），而且提供了一套可以在编译时刻进行语义检查的 API 。AsmJit 的使用也没有任何的限制，适用于多媒体，虚拟机的后端，远程代码生成等等。\n二、特性 完全支持x86/x64指令集（包括MMX，SSEx，AVX1/2，BMI，XOP，FMA3和FMA4）； 底层次和高层次的代码生成概念； 内置检测处理器特性功能； 实现虚拟内存的管理，类似于malloc和free； 强大的日志记录和错误处理能力； 体积小，可直接嵌入项目，编译后的体积在150至200kb之间； 独立性强，不需要依赖其他任何的库（包括STL和RTTI ）。 三、代码生成 AsmJit 有着两种完全不同的代码生成概念，其不同点就在于生成代码的方式。一种是称为 Assembler 的低层次的代码生成方法，通过直接操作物理寄存器的方式生成代码，这种情况下 AsmJit 所做的工作只是简单的对指令进行编码，验证和重定位。而另外的一种是称为 Compiler 的高层次代码生成方法，Compiler 对使用虚拟寄存器的数量没有限制，这就类似于高级程序设计语言中的变量，可以极大的简化代码的生成过程。Compiler 在代码生成成功以后再给这些虚拟寄存器（变量）分配相应的物理寄存器，这就需要一些额外的消耗，因为 Compiler 必须为代码中的每一个结点（包括指令，函数声明，函数调用）生成额外的信息，用来对变量的生命周期进行分析或者将使用变量的代码转换成使用物理寄存器的汇编语句。\n此外，Compiler 也需要了解函数原型和函数之间的调用约定。因此 Compiler 产生的代码具有类似于高级程序设计语言一样的函数原型，通过函数原型，Compiler 可以通过在函数头部和尾部插入额外的代码来达到被可以其他函数调用的目的。但是我们不能说明上面两种代码的生成方式孰优孰劣，因为利用 Assmebler 的方式可以充分控制代码的生成，而利用 Compiler 可以使得代码的生成更方便，可移植性更强，然而，当涉及到物理寄存器分配时，Compiler 有时效果并不太好，所以在已经进行分析的项目中，纯粹的 Assembler 方式的生成是首选。\n四、配置和编译 AsmJit 在设计之初的目的就是为了嵌入到任何项目之中。但是我们可以使用一些宏定义来添加或者删除 AsmJit 库的某些特性。生成AsmJit 项目最直接的方法是使用 cmake 工具www.cmake.org ，但是如果只是在项目中嵌入 AsmJit 的源代码，可以通过编辑 asmjit /config.h 文件来打开或者关闭某些特定的特性，最简便的使用方法就是直接复制 AsmJit 的源代码到项目中，然后定义 “ASMJIT_STATIC” 宏。\n4.1 生成类型 ASMJIT_EMBED —— 如果在 cmake 中指定这个参数，AsmJit 则不会产生库，而是将代码直接嵌入到工程当中； ASMJIT_STATIC ——如果在 cmake 中指定这个参数，AsmJit 则会生成静态库，默认将不会导出符号； 如果都不指定，AsmJit 则会默认生成动态库文件。 4.2 生成模式 ASMJIT_DEBUG —— 生成调试版本； ASMJIT_RELEASE —— 生成发行版本； ASMJIT_TRACE —— 生成的版本可以使用 trace 来调试 bug ，并且会使用 stdcout 将 AsmJit 运行的日志全部输出； 如果这些都没有定义的话，AsmJit 就会检测当前 IDE 编译时用到的宏定义，例如：Debug/Release 等等。 4.3 体系结构 ASMJIT_BUILD_X86 —— 生成 X86 体系的后端； ASMJIT_BUILD_X64 —— 生成 x64 体系的后端； ASMJIT_BUILD_HOST —— 通过在编译时检测当前环境处理器的架构，生成和当前处理器架构一致的后端； 如果都不指定，则默认使用 ASMJIT_BUILD_HOST。 4.4 特性 ASMJIT_DISABLE_COMPILER —— 禁用 Compiler 功能； ASMJIT_DISABLE_LOGGER —— 禁止产生日志； ASMJIT_DISABLE_NAMES —— 禁止使用字符串，如果使用则所有的指令和错误名称将变成无效。 五、使用 5.1 命名空间 AsmIit库使用的是全局命名空间 asmjit ，但是其中只包含一些基本的内容，而针对特定处理器的代码是用处理器、处理器的寄存器或者操作数作为前缀当成命名空间。例如针对 x86 和 x64 体系结构的类都会带有有 X86 的前缀。通过 kx86 枚举的寄存器和操作数在 X86 的命名空间下都是可访问的。虽然这种设计和 AsmJit 最初的版本不同，但是现在无疑是可移植性最好的。\n5.2 运行时刻和代码生成器 要产生机器码就要用到AsmJit的两个类—— Runtime 和 CodeGen 。RunTime 会指定代码生成区域和存储区域； CodeGen 会指定代码的生成方式和产生整个程序的控制流。接下来的所有的例子都将使用 Compiler 来生成代码，并使用 JitRunTime 类来运行和存储。\n5.3 指令操作数 操作数是处理器指令的一部分，指定了指令将要操作的数据，AsmJit 中有5种操作数\nReg 物理寄存器，只被Assembler使用 Var 虚拟寄存器（变量），只被 Compiler使用 Mem 用于引用内存地址 Label 用于引用代码地址 Imm 直接用于编码的立即数本身 所有操作的基类都是 “Operand ， 它包含使用所有类型的操作数的接口，并且大多数是通过值传递，而不是通过指针传递。\nReg , Var , Mem , Label 和 Imm 类都是继承自 Operand 并且提供不同的功能。依赖于处理器体系结构的操作数都会带有处理器结构作为前缀，例如 X86Reg , X86Mem 。大多数的处理器都会提供几种寄存器，\n例如 X86/X64 体系结构下的 X86GpReg , X86MmReg , X86FpReg , X86XmmReg 和 X86YmmReg 寄存器加上一些额外的段寄存器和 ”rip” 寄存器。在使用代码生成器时，必须使用 AsmJit 的接口来显式地创建一些操作数。例如，labels 是用代码生成器类的 newLabel() 方法创建，而变量需要用针对不同体系结构的特定方法来创建，例如 newGpVar() , newMmVar() 和 newXmmVar() 。\n5.4 函数原型 AsmJit 需要知道产生或调用的函数原型。AsmJit 包含类型和寄存器之间的映射关系，并且用来表示函数原型。函数生成器是一个模板类，通过使用 C/C++ 原生类型来生成可以描述函数参数和返回值的函数原型。它把 C/C++ 原生类型转化为 AsmJit 特定的标识符并且使这些标识符访问编译器。\n5.5 实际使用 1#include \u0026lt;asmjit/asmjit.h\u0026gt; 2#include \u0026lt;stdio.h\u0026gt; 3 4using namespace asmjit; 5 6// Signature of the generated function. 7typedef int (*Func)(void); 8 9int main(int argc, char* argv[]) { 10 // Runtime designed for JIT - it holds relocated functions and controls their lifetime. 11 JitRuntime rt; 12 13 // Holds code and relocation information during code generation. 14 CodeHolder code; 15 16 // Code holder must be initialized before it can be used. The simples way to initialize 17 // it is to use \u0026#39;Environment\u0026#39; from JIT runtime, which matches the target architecture, 18 // operating system, ABI, and other important properties. 19 code.init(rt.environment(), rt.cpuFeatures()); 20 21 // Emitters can emit code to CodeHolder - let\u0026#39;s create \u0026#39;x86::Assembler\u0026#39;, which can emit 22 // either 32-bit (x86) or 64-bit (x86_64) code. The following line also attaches the 23 // assembler to CodeHolder, which calls \u0026#39;code.attach(\u0026amp;a)\u0026#39; implicitly. 24 x86::Assembler a(\u0026amp;code); 25 26 // Use the x86::Assembler to emit some code to .text section in CodeHolder: 27 a.mov(x86::eax, 1); // Emits \u0026#39;mov eax, 1\u0026#39; - moves one to \u0026#39;eax\u0026#39; register. 28 a.ret(); // Emits \u0026#39;ret\u0026#39; - returns from a function. 29 30 // \u0026#39;x86::Assembler\u0026#39; is no longer needed from here and can be destroyed or explicitly 31 // detached via \u0026#39;code.detach(\u0026amp;a)\u0026#39; - which detaches an attached emitter from code holder. 32 33 // Now add the generated code to JitRuntime via JitRuntime::add(). This function would 34 // copy the code from CodeHolder into memory with executable permission and relocate it. 35 Func fn; 36 Error err = rt.add(\u0026amp;fn, \u0026amp;code); 37 38 // It\u0026#39;s always a good idea to handle errors, especially those returned from the Runtime. 39 if (err) { 40 printf(\u0026#34;AsmJit failed: %s\\n\u0026#34;, DebugUtils::errorAsString(err)); 41 return 1; 42 } 43 44 // CodeHolder is no longer needed from here and can be safely destroyed. The runtime now 45 // holds the relocated function, which we have generated, and controls its lifetime. The 46 // function will be freed with the runtime, so it\u0026#39;s necessary to keep the runtime around. 47 // 48 // Use \u0026#39;code.reset()\u0026#39; to explicitly free CodeHolder\u0026#39;s content when necessary. 49 50 // Execute the generated function and print the resulting \u0026#39;1\u0026#39;, which it moves to \u0026#39;eax\u0026#39;. 51 int result = fn(); 52 printf(\u0026#34;%d\\n\u0026#34;, result); 53 54 // All classes use RAII, all resources will be released before **main()** returns, the 55 // generated function can be, however, released explicitly if you intend to reuse or 56 // keep the runtime alive, which you should in a production-ready code. 57 rt.release(fn); 58 59 return 0; 60} ","permalink":"https://zevorn.cn/posts/7/","summary":"一、基本介绍 AsmJit 是一个完整的 JIT ( just In Time, 运行时刻)的针对 C++ 语言的汇编器，可以生成兼容 x86 / x64 和 Aarch64 架构的原生代码，不仅支持整个x86/x64 的指令集（包括传统的 MMX 和最新的向量指令集），而且提供了一套可以在编译时刻进行语义检查的 API 。AsmJit 的使用也没有任何的限","title":"AsmJit 上手指南"},{"content":"体系架构模拟最高效的实现，是尽量做到1：1模拟指令，即一条源目标架构指令对应到一条目标架构指令上。对于解释执行的实现，也要尽可能贴近这个原则，尽量以最少的目标架构指令，来模拟源架构指令。\n一、思路介绍 aarch64的FCMP指令定义如下：\n1Manual: C7.2.59 \u0026lt;Arm Architecture Reference Manual (Armv8-A) 2019\u0026gt; 2Floating-point quiet Compare (scalar). This instruction compares the two SIMD \u0026amp; FP source register values, or the 3first SIMD\u0026amp;FP source register value and zero. It writes the result to the PSTATE.{N, Z, C, V} flags. 4It raises an Invalid Operation exception only if either operand is a signaling NaN. 5A floating-point exception can be generated by this instruction. Depending on the settings in FPCR, the exception 6results in either a flag being set in FPSR, or a synchronous exception being generated. For more information, see 7Floating-point exceptions and exception traps on page D1-2313. 8Depending on the settings in the CPACR_EL1, CPTR_EL2, and CPTR_EL3 registers, and the current Security state 9and Exception level, an attempt to execute the instruction might be trapped. 10 11Instruction encoding diagram: 12│31 30 29 28│27 26 25 24│23 22 21 20│19 18 17 16│15 14 13 12│11 10 09 08│07 06 05 04│03 02 01 00│ 13├───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ 14│ 0 0 0 1│ 1 1 1 0│ 0 1 1 .│ . . . .│ 0 0 1 0│ 0 0 . .│ . . . 0│ 0 0 0 0│ 15└───────────┴───────────┴───────────┴───────────┴───────────┴───────────┴───────────┴───────────┘ 16 17 18Half-precision variant 19Applies when ftype == 11 \u0026amp;\u0026amp; opc == 00. 20FCMP Hn, Hm 21 22Half-precision, zero variant 23Applies when ftype == 11 \u0026amp;\u0026amp; Rm == (00000) \u0026amp;\u0026amp; opc == 01. 24FCMP Hn, #0.0 25 26Single-precision variant 27Applies when ftype == 00 \u0026amp;\u0026amp; opc == 00. 28FCMP Sn, Sm 29 30Single-precision, zero variant 31Applies when ftype == 00 \u0026amp;\u0026amp; Rm == (00000) \u0026amp;\u0026amp; opc == 01. 32FCMP Sn, #0.0 33 34Double-precision variant 35Applies when ftype == 01 \u0026amp;\u0026amp; opc == 00. 36FCMP Dn, Dm 37 38Double-precision, zero variant 39Applies when ftype == 01 \u0026amp;\u0026amp; Rm == (00000) \u0026amp;\u0026amp; opc == 01. 40FCMP Dn, #0.0 大体可以分为两种：单精度浮点比较，双精度浮点比较，每一种又分为有序比较和无序比较，所以可以总结为四种情况。\n恰好这四种情况，都有相应的 x86 指令：\nx86 单精度浮点比较（有序）指令：comiss ，对应 aarch64 的 FCMP Sn, Sm (Ordered) x86 单精度浮点比较（无序）指令：ucomiss ，对应 aarch64 的 FCMP Sn, Sm (Unordered) x86 双精度浮点比较（有序）指令：comisd ， 对应 aarch64 的 FCMP Dn, Dm (Ordered) x86 双精度浮点比较（无序）指令：ucomisd ，对应 aarch64 的 FCMP Dn, Dm (Unordered) Aarch64 的 FCMP 指令执行结果更新到 PSTATE寄存器的 NZCV 位，而 x86 的浮点比较指令，执行结果是更新到 eflags 寄存器上，因此需要做一个 eflags 到 NZCV 的映射。\n下面给出 x86 浮点比较指令更新标志位映射到 Aarch64 的示意：\nx86 ARM ZF PF CF N Z C V equal 1 0 0 0 1 1 0 less 0 0 1 =\u0026gt; 1 0 0 0 greater 0 0 0 0 0 1 0 unordered 1 1 1 0 0 1 1 二、代码实现 这里用条件指令实现（相比分支指令执行更高效）标志位更新的映射，C代码实现如下：\n1#define asm_fcmp32(nzcv, op1, op2) \\ 2{ \\ 3 int less = 0x8, greater = 0x2; \\ 4 int equal = 0x6, unordered = 0x3, eq_0 = 0; \\ 5 asm volatile ( \\ 6 \u0026#34;movd %[op1], %%xmm0;\u0026#34; \\ 7 \u0026#34;movd %[op2], %%xmm1;\u0026#34; \\ 8 /* Scalar Ordered Single-FP Compare and Set EFLAGS */ \\ 9 \u0026#34;comiss %%xmm1, %%xmm0;\u0026#34; \\ 10 \u0026#34;mov %[eq_0], %%eax;\u0026#34; \\ 11 \u0026#34;cmovb %[less], %%eax;\u0026#34; /* Move if Below (CF=1) */ \\ 12 \u0026#34;cmovz %[eq_0], %%eax;\u0026#34; /* Move if Zero (ZF=1) */ \\ 13 \u0026#34;mov %%eax, %%ecx;\u0026#34; \\ 14 \u0026#34;cmovz %[equal], %%eax;\u0026#34; /* Move if Zero (ZF=1) */ \\ 15 \u0026#34;cmovb %%ecx, %%eax;\u0026#34; /* Move if Below (CF=1) */ \\ 16 \u0026#34;cmova %[greater], %%eax;\u0026#34; /* Move if Above (CF=0 \u0026amp; ZF=0) */ \\ 17 \u0026#34;cmovp %[unordered], %%eax;\u0026#34; /* Move if Parity (PF=1) */ \\ 18 \u0026#34;mov %%eax, %[nzcv];\u0026#34; \\ 19 : [nzcv]\u0026#34;=g\u0026#34;(nzcv) \\ 20 : [op1]\u0026#34;m\u0026#34;(op1), [op2]\u0026#34;m\u0026#34;(op2), [eq_0]\u0026#34;r\u0026#34;(eq_0), [greater]\u0026#34;r\u0026#34;(greater), \\ 21 [unordered]\u0026#34;r\u0026#34;(unordered), [equal]\u0026#34;r\u0026#34;(equal), [less]\u0026#34;r\u0026#34;(less) \\ 22 : \u0026#34;cc\u0026#34;, \u0026#34;xmm0\u0026#34;, \u0026#34;xmm1\u0026#34;, \u0026#34;eax\u0026#34;, \u0026#34;ecx\u0026#34; \\ 23 ); \\ 24} op1 和 op2 是源操作数和目的操作数，计算完的结果放在 nzcv 当中。\nPS：有序和无序的区别，以 x86 的 comisd 和 ucomisd 指令为例： comisd：当比较两个数时，如果它们相等（包括两个都是NaN的情况），comisd 会设置 ZF 为 1。这意味着，对于 comisd 来说，任何 NaN 都被认为是相等的。\nucomisd：关键的区别在于它对待 NaN 值的方式：如果任何一个操作数是 NaN，ucomisd 会设置 CF（无序标志）和 PF（奇偶标志），并且不会改变ZF（零标志）。这意味着，即使两个操作数都是 NaN，ZF 也不会被设置，这反映了 NaN 与其他任何数值都不相等的数学特性。\n","permalink":"https://zevorn.cn/posts/6/","summary":"体系架构模拟最高效的实现，是尽量做到1：1模拟指令，即一条源目标架构指令对应到一条目标架构指令上。对于解释执行的实现，也要尽可能贴近这个原则，尽量以最少的目标架构指令，来模拟源架构指令。 一、思路介绍 aarch64的FCMP指令定义如下： 大体可以分为两种：单精度浮点比较，双精度浮点比较，每一种又分为有序比较和无序比较，所以可以总结为四种情况。 恰好这四种","title":"在 x86 平台上模拟 Aarch64 的 FCMP 指令"},{"content":"LLVM3.0 的 JIT 在 MinGW64 / MSCV64 上，进行代码块重定向阶段，处理超过32位地址范围(前后跳转2GB地址空间)的外部符号地址时，会将其强转为32位：\n1// lib\\Target\\X86\\X86JITInfo.cpp:543 2void X86JITInfo::relocate(void *Function, MachineRelocation *MR, 3 unsigned NumRelocs, unsigned char* GOTBase) { 4 for (unsigned i = 0; i != NumRelocs; ++i, ++MR) { 5 void *RelocPos = (char*)Function + MR-\u0026gt;getMachineCodeOffset(); 6 intptr_t ResultPtr = (intptr_t)MR-\u0026gt;getResultPointer(); 7 switch ((X86::RelocationType)MR-\u0026gt;getRelocationType()) { 8 case X86::reloc_pcrel_word: { 9 ResultPtr = ResultPtr -(intptr_t)RelocPos - 4 - MR-\u0026gt;getConstantVal(); 10 *((unsigned*)RelocPos) += (unsigned)ResultPtr; // 这里默认都是强转成32位的 11 break; 12 } 13} 这会导致生成的指令，在执行时崩溃，一般表现为段错误。\n解决办法：\n1From bdc19fc6fa1a87a375e5f77e83bc6e712185676a Mon Sep 17 00:00:00 2001 2From: zevorn \u0026lt;zevorn@yeah.net\u0026gt; 3Date: Wed, 22 May 2024 18:31:30 +0800 4Subject: [PATCH 1/4] \u0026lt;fix\u0026gt;(JIT) Fixed a Bug in JIT 5 6Fixed a Bug in JIT processing code block redirection phase for external symbol parsing 7--- 8 lib/ExecutionEngine/JIT/JITEmitter.cpp | 6 +++++- 9 1 file changed, 5 insertions(+), 1 deletion(-) 10 11diff --git a/lib/ExecutionEngine/JIT/JITEmitter.cpp b/lib/ExecutionEngine/JIT/JITEmitter.cpp 12index 24020ee..0050b75 100644 13--- a/lib/ExecutionEngine/JIT/JITEmitter.cpp 14+++ b/lib/ExecutionEngine/JIT/JITEmitter.cpp 15@@ -849,7 +849,11 @@ bool JITEmitter::finishFunction(MachineFunction \u0026amp;F) { 16 \u0026lt;\u0026lt; ResultPtr \u0026lt;\u0026lt; \u0026#34;]\\n\u0026#34;); 17 18 // If the target REALLY wants a stub for this function, emit it now. 19- if (MR.mayNeedFarStub()) { 20+ uintptr_t CurrPtr = getCurrentPCValue(); 21+ const uintptr_t rang = 2ull * 1024 * 1024 * 1024; // 2GB 22+ if (MR.mayNeedFarStub() 23+ || ((CurrPtr + rang) \u0026lt; (uintptr_t)ResultPtr) 24+ || ((CurrPtr - rang) \u0026gt; (uintptr_t)ResultPtr)) { 25 ResultPtr = Resolver.getExternalFunctionStub(ResultPtr); 26 } 27 } else if (MR.isGlobalValue()) { 28-- 292.44.0.windows.1 PS：这个问题似乎在 LLVM 3.1 上面也存在。\n","permalink":"https://zevorn.cn/posts/4/","summary":"LLVM3.0 的 JIT 在 MinGW64 / MSCV64 上，进行代码块重定向阶段，处理超过32位地址范围(前后跳转2GB地址空间)的外部符号地址时，会将其强转为32位： 这会导致生成的指令，在执行时崩溃，一般表现为段错误。 解决办法： PS：这个问题似乎在 LLVM 3.1 上面也存在。","title":"LLVM3.0 JIT 处理代码块重定向阶段对外部符号解析的 Bug"},{"content":"IDA 认为，联合体是一种特殊的结构体，因此需要在 IDA 的结构体页面创建联合体:\n第一步，进入 Structures 页面： 第二步，按下【Insert】，勾选 union ： 一个空元素的联合体如下： 第三步，在 ends 位置按下【D】可以添加一个新的成员，如图示例添加了两个成员： 可以看到，两个成员的地址偏移都是从0开始的。\nPS：如果想添加结构体成员，可以额外创建一个结构体类型，然后在联合体中添加。\n","permalink":"https://zevorn.cn/posts/3/","summary":"IDA 认为，联合体是一种特殊的结构体，因此需要在 IDA 的结构体页面创建联合体: 第一步，进入 Structures 页面： 第二步，按下【Insert】，勾选 union ： 一个空元素的联合体如下： 第三步，在 ends 位置按下【D】可以添加一个新的成员，如图示例添加了两个成员： 可以看到，两个成员的地址偏移都是从0开始的。 PS：如果想添加结构体","title":"IDA Pro“创建联合体”教程"},{"content":"GDB 是 GNU Debugger 的简称，属于 GNU 项目的一部分，是一款功能强大且广泛使用的命令行调试工具，专为 Unix 和类 Unix 操作系统设计，尤其在 Linux 环境下是程序员首选的调试工具之一。GDB 主要用于调试 C、C++、Pascal、FORTRAN、Ada、Objective-C、Free Pascal、Go 等多种编程语言编写的程序。\n掌握了 GDB ，就等于掌握了一把神兵利器，可以帮我们快速定位问题，极大的降低时间成本。从包管理器安装或者源码编译的 GDB 是一个毛坯房，“装修装修”再入住会更舒服，所以接下来手把手教你定制属于自己的 GDB 。\n一、定制 GDB 配置文件 GDB 有一个配置文件，叫做 .gdbinit ，支持写入 GDB 命令和用户自定义命令。GDB 启动时，会查找是否存在 .gdbinit 配置文件，先检索 ~/. 目录，再检索当前 GDB 启动时所在的目录。文件中的 GDB 命令会在 GDB 启动时预执行。\n根据需要，在 GDB 配置文件中写入不同的 GDB 命令，可以达到定制化 GDB 的目的。\n比如:\n1set debuginfod enabled on 2set print pretty on 3set max-value-size unlimited 以上 3 个命令，设置了默认下载 GDB 调试符号、打开结构体格式化打印、不限制打印信息长度的功能。\n除了设置 GDB 原生命令，还可以定义用户命令（宏编码语言），可以将所有其他标准 GDB 命令与流控制指令结合使用并向其传递参数，以允许为正在调试的特定应用程序而自定义调试器的行为。通过用户命令实现对原生 GDB 命令的组合封装，可进一步适应不同的调试场景。建议将通用的用户命令定义在 ~/.gdbinit ，针对不同项目的用户命令可以放在各自项目文件夹下的 .gdbinit 当中，只需要在对应文件夹下启动 GDB 即可使用。\nGDB 用户命令遵循如下基本格式：\n1define \u0026lt;command\u0026gt; 2 \u0026lt;code\u0026gt; 3end 4document \u0026lt;command\u0026gt; 5 \u0026lt;help text\u0026gt; 6end 从简单开始：清屏\nGDB 没有用于清屏的内置命令，但它可以调用 shell 函数；下面的代码跳到调试器之外以使用 cls 命令来清除 xterm 控制台：\n1define cls 2shell clear 3end 4document cls 5Clears the screen with a simple command. 6end 此定义的上半部分（在 define \u0026hellip; end 动词所界定的范围内）构成了在调用该命令时所执行的代码。此定义的下半部分（在 document \u0026hellip; end 所界定的范围内）由 GDB 命令解释器使用，用于键入 help cls 时显示与 cls 命令关联的文本。\n如果输入 help user 命令，可以看到在 .gdbinit 文件中输入的所有用户命令的摘要。.gdbinit 用户定义命令的设计者提供了一个重要特性，在编写自己的命令时不应忽略该特性：document \u0026hellip; end 子句。随着这些命令数量的增加，维护有关命令如何工作的功能文档将变得非常关键。\n断点别名\n许多 GDB 命令太繁琐，这是众所周知的事实。尽管可以对它们进行缩写，但是 GDB 宏语言允许实现进一步的简化。诸如 info breakpoints 这样的命令可以变得像 bpl 一样简单，这里给出常见的断点别名命令封装：\n1define bpl 2info breakpoints 3end 4document bpl 5List breakpoints 6end 7 8define bpa 9break * $arg0 10end 11document bpa 12Set a breakpoint on address 13Usage: bpa addr 14end 15 16define bpc 17clear $arg0 18end 19document bpc 20Clear breakpoint at function/address 21Usage: bpc addr 22end 23 24define bpe 25enable $arg0 26end 27document bpe 28Enable breakpoint # 29Usage: bpe num 30end 31 32define bpd 33disable $arg0 34end 35document bpd 36Disable breakpoint # 37Usage: bpd num 38end 39 40define bpt 41tbreak $arg0 42end 43document bpt 44Set a temporary breakpoint on address 45Usage: bpt addr 46end 47 48define bpm 49awatch $arg0 50end 51document bpm 52Set a read/write breakpoint on address 53Usage: bpm addr 54end 二、善用 GDB 的 python 脚本 GDB 的用户命令是一种宏语言，语法比较简陋，只能实现对已有命令的封装，对一些复杂调试场景和应对一些特殊调试需求，比如记录指令流，比如自动化处理某些调试任务，就显得有些捉襟见肘。但是好在，GDB 支持 python 脚本执行，可以把一些调试流程编程脚本自动化执行，尽可能解放我们的双手，让开发人员可以专注于调试思路（但想要在 GDB 中正常使用 python，需要先确保你已经安装好它）。\n下面举例一个简单的使用思路：\n1(gdb) python # 1，进入 gdb 以后，输入 python 可以进入 python 解释器环境 2\u0026gt;print(1 + 1) # 2. 这里打印 1 + 1 3\u0026gt;end # 3. 输入结束命令 42 # 打印执行结果 5(gdb) 如果觉得麻烦，也可以把命令写在一行：\n1(gdb) python print(1+1) 22 3(gdb) 更进一步的，可以将 python 代码放在一个 .py 文件，直接在 GDB 中调用：\n1(gdb) source script.py 下面给出一个记录指令流的 GDB-python 脚本代码示例：\n1import gdb # 如果在 gdb 里面直接执行 python 代码，不需要 import gdb 2 3# 定义要写入的文件名 4logfile = \u0026#34;gdb_jump_instr_log.txt\u0026#34; 5 6# 清空或创建新文件 7with open(logfile, \u0026#39;w\u0026#39;) as f: 8 f.write(\u0026#39;\u0026#39;) 9 10def is_jump_instruction(instruction): 11 # 这里仅提供了一个基础示例，实际实现需要根据目标架构和具体的指令集进行扩展 12 # 例如，x86架构下，call、jmp、j* 等都是跳转指令 13 jump_instructions = [\u0026#34;call\u0026#34;, \u0026#34;jmp\u0026#34;, \u0026#34;je\u0026#34;, \u0026#34;jne\u0026#34;, \u0026#34;jz\u0026#34;, \u0026#34;jnz\u0026#34;] # 示例，不完整列表 14 return any(inst.lower() in instruction.lower() for inst in jump_instructions) 15 16while True: 17 # 获取当前PC值 18 pc_value = gdb.parse_and_eval(\u0026#34;$pc\u0026#34;) 19 20 # 获取当前地址的汇编指令 21 disassembly_line = gdb.execute(f\u0026#34;x/i {pc_value}\u0026#34;, to_string=True).split(None, 1)[1] 22 23 if is_jump_instruction(disassembly_line): 24 with open(logfile, \u0026#39;a\u0026#39;) as f: 25 f.write(\u0026#39;%s\\n\u0026#39; % pc_value) 26 27 # 执行单步指令（这里假设你希望每次检查后都单步执行一次） 28 gdb.execute(\u0026#34;si\u0026#34;) 29 30 # 可以在这里添加更多的调试信息，如寄存器状态等 31 32 # 如果你想在达到某个条件时结束脚本，可以在这里添加相应的逻辑 三、玩转 GDB 插件 如果还是觉得用户命令和 python 脚本编写起来比较麻烦，想获得开箱即用的体验，那么你可以了解一下 GDB 插件（基本是基于用户命令和 python 脚本实现），笔者常用的插件是 peda ，安装流程如下：\n1git clone https://github.com/longld/peda.git ~/peda 2echo \u0026#34;source ~/peda/peda.py\u0026#34; \u0026gt;\u0026gt; ~/.gdbinit 3echo \u0026#34;DONE! debug your program with gdb and enjoy\u0026#34; 使用界面如下，比较适合逆向工程：\n1[----------------------------------registers-----------------------------------] 2EAX: 0xb0c (\u0026#39;\\x0c\\x0b\u0026#39;) 3EBX: 0x1804e990 --\u0026gt; 0x91085c7 4ECX: 0xb0 5EDX: 0x568 6ESI: 0xf7ff24d4 (\u0026#34;DHRYSTONE PROGRAM, 2\u0026#39;ND STRING\u0026#34;) 7EDI: 0xf7ff2504 (\u0026#34;M, 1\u0026#39;ST STRING\u0026#34;) 8EBP: 0x1a000000 (0x1a000000) 9ESP: 0xffffd6e8 --\u0026gt; 0x0 10EIP: 0x402a2269 (add esi,0x10) 11EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) 12[-------------------------------------code-------------------------------------] 13 0x402a225e: add edi,0x10 14 0x402a2261: pxor xmm0,xmm0 15 0x402a2265: pcmpeqb xmm2,xmm1 16=\u0026gt; 0x402a2269: add esi,0x10 17 0x402a226c: pcmpeqb xmm0,xmm1 18 0x402a2270: pmovmskb eax,xmm2 19 0x402a2274: pmovmskb edx,xmm0 20 0x402a2278: xor eax,0xffff 21[------------------------------------stack-------------------------------------] 220000| 0xffffd6e8 --\u0026gt; 0x0 230004| 0xffffd6ec --\u0026gt; 0x0 240008| 0xffffd6f0 --\u0026gt; 0x105a3 (inc esi) 250012| 0xffffd6f4 --\u0026gt; 0x0 260016| 0xffffd6f8 --\u0026gt; 0x1804ea17 --\u0026gt; 0xc7c08941 270020| 0xffffd6fc --\u0026gt; 0x0 280024| 0xffffd700 --\u0026gt; 0xf7ff24f4 (\u0026#34;DHRYSTONE PROGRAM, 1\u0026#39;ST STRING\u0026#34;) 290028| 0xffffd704 --\u0026gt; 0xf7ff24d4 (\u0026#34;DHRYSTONE PROGRAM, 2\u0026#39;ND STRING\u0026#34;) 30[------------------------------------------------------------------------------] 31Legend: code, data, rodata, value 32Stopped reason: SIGINT 330x402a2269 in ?? () 34gdb-peda$ 以上是对 GDB 一些客制化内容的介绍，后面会结合应用场景，讲讲如何灵活运用这些工具。\n","permalink":"https://zevorn.cn/posts/1/","summary":"GDB 是 GNU Debugger 的简称，属于 GNU 项目的一部分，是一款功能强大且广泛使用的命令行调试工具，专为 Unix 和类 Unix 操作系统设计，尤其在 Linux 环境下是程序员首选的调试工具之一。GDB 主要用于调试 C、C++、Pascal、FORTRAN、Ada、Objective C、Free Pascal、Go 等多种编程语言编写","title":"三步轻松“定制”GDB"}]