mm学习笔记_05:虚拟内存核心与系统调用
mm学习笔记_05虚拟内存核心与系统调用基于 ReactOS ARM3 MM 源码ntoskrnl/mm/ARM3/virtual.c1. virtual.c 概述文件位置d:\reactos\ntoskrnl\mm\ARM3\virtual.c职责虚拟内存管理的核心实现以下关键系统调用NtAllocateVirtualMemory— 分配虚拟内存NtFreeVirtualMemory— 释放虚拟内存NtProtectVirtualMemory— 修改内存保护属性MmGetPhysicalAddress— 虚拟地址转物理地址MmCopyVirtualMemory— 跨进程内存拷贝2. NtAllocateVirtualMemory——完整路径文件virtual.c:44572.1 函数签名NTSTATUS NTAPINtAllocateVirtualMemory(IN HANDLE ProcessHandle,IN OUT PVOID*BaseAddress,IN ULONG_PTR ZeroBits,IN OUT PSIZE_T RegionSize,IN ULONG AllocationType,IN ULONG Protect);2.2 步骤一参数验证// 1. ZeroBits 检查不能超过最大值if(ZeroBitsMI_MAX_ZERO_BITS)returnSTATUS_INVALID_PARAMETER_3;// 2. AllocationType 合法性检查必须包含 MEM_COMMIT / MEM_RESERVE / MEM_RESETif(AllocationType~(MEM_COMMIT|MEM_RESERVE|MEM_RESET|...))returnSTATUS_INVALID_PARAMETER_5;if(!(AllocationType(MEM_COMMIT|MEM_RESERVE|MEM_RESET)))returnSTATUS_INVALID_PARAMETER_5;// 3. MEM_RESET 独占检查if((AllocationTypeMEM_RESET)(AllocationType!MEM_RESET))returnSTATUS_INVALID_PARAMETER_5;// 4. MEM_LARGE_PAGES 必须带 MEM_COMMITif((AllocationTypeMEM_LARGE_PAGES)!(AllocationTypeMEM_COMMIT))returnSTATUS_INVALID_PARAMETER_5;// 5. MEM_WRITE_WATCH 必须带 MEM_RESERVEif((AllocationTypeMEM_WRITE_WATCH)!(AllocationTypeMEM_RESERVE))returnSTATUS_INVALID_PARAMETER_5;// 6. MEM_PHYSICAL 必须带 MEM_RESERVE 且 Protect PAGE_READWRITEif((AllocationTypeMEM_PHYSICAL)...)...// 7. 保护属性验证ProtectionMaskMiMakeProtectionMask(Protect);if(ProtectionMaskMM_INVALID_PROTECTION)returnSTATUS_INVALID_PAGE_PROTECTION;// 8. 用户态参数捕获SEH 保护_SEH2_TRY{if(PreviousMode!KernelMode){ProbeForWritePointer(UBaseAddress);ProbeForWriteSize_t(URegionSize);}PBaseAddress*UBaseAddress;PRegionSize*URegionSize;}_SEH2_EXCEPT(...);// 9. 地址范围检查if(PBaseAddressMM_HIGHEST_VAD_ADDRESS)returnSTATUS_INVALID_PARAMETER_2;if(!PRegionSize)returnSTATUS_INVALID_PARAMETER_4;2.3 步骤二进程获取与附加// 如果是当前进程直接使用if(ProcessHandleNtCurrentProcess())ProcessCurrentProcess;else{// 否则以 PROCESS_VM_OPERATION 权限引用目标进程StatusObReferenceObjectByHandle(ProcessHandle,PROCESS_VM_OPERATION,PsProcessType,...);// 如果目标进程不是当前进程附加到目标进程地址空间if(CurrentProcess!Process){KeStackAttachProcess(Process-Pcb,ApcState);AttachedTRUE;}}2.4 步骤三MEM_RESERVE 主路径当未指定基址或指定 MEM_RESERVE 时1. 配额计费 PsChargeProcessNonPagedPoolQuota(sizeof(MMVAD_LONG)) 2. 分配 VADExAllocatePoolWithTag(NonPagedPool, sizeof(MMVAD_LONG), SdaV) 3. 初始化 VAD - Vad-u.VadFlags.PrivateMemory 1 - Vad-u.VadFlags.Protection ProtectionMask - if (AllocationType MEM_COMMIT) Vad-u.VadFlags.MemCommit 1 4. 插入 VAD自动查找空闲地址 Status MiInsertVadEx(Vad, StartingAddress, PRegionSize, HighestAddress, MM_VIRTMEM_GRANULARITY, AllocationType) 5. 写回用户参数SEH 保护 *URegionSize PRegionSize; *UBaseAddress (PVOID)StartingAddress;2.5 步骤四MEM_COMMIT 主路径当指定已有地址即对已预留区进行提交1. 锁定地址空间MmLockAddressSpace(AddressSpace) 2. 检查进程是否将终止 3. 查找冲突/匹配 VAD Result MiCheckForConflictingNode(StartVpn, EndVpn, Process-VadRoot, ...) 4. 验证 VAD 类型不能是 VadAwe/VadDevicePhysicalMemory/VadLargePages 5. 验证地址范围在 VAD 内 6. 如果是非私有内存Section 映射 - 获取原型 PTE - 锁定 MmSectionCommitMutex - 遍历 PTE 写入模板 PTE - 更新 NumberOfCommittedPages 7. 如果是私有内存 - 更新 CommitCharge 和 Process-CommitCharge - MiLockProcessWorkingSetUnsafe - 遍历每个 PTE - MiMakePdeExistAndMakeValid确保页表存在 - 写入 DemandZero 格式的 PTE保护属性 软件 PTE 标记 - MiUnlockProcessWorkingSetUnsafe完整流程图NtAllocateVirtualMemory │ ├─ 参数验证 │ ├─ ZeroBits 范围检查 │ ├─ AllocationType 组合检查 │ ├─ Protect 合法性检查 │ ├─ 用户态参数 Probe │ └─ 地址范围检查 │ ├─ 进程获取与附加KeStackAttachProcess │ ├─ [MEM_RESERVE 分支] │ ├─ 配额计费 │ ├─ 分配 VADExAllocatePoolWithTag │ ├─ VAD 初始化 │ └─ MiInsertVadEx自动搜索 Avl 插入 │ ├─ [MEM_COMMIT 分支] │ ├─ MmLockAddressSpace │ ├─ 查找已有 VADMiCheckForConflictingNode │ ├─ 验证 VAD 类型 │ ├─ [Section 内存] → 写原型 PTE → 更新段提交计数 │ ├─ [私有内存] → 更新 CommitCharge │ │ └─ 锁定工作集 │ │ └─ 遍历 PTE确保 PDE 有效写 DemandZero PTE │ └─ 解锁地址空间 │ └─ 返回 BaseAddress STATUS_SUCCESS3. NtFreeVirtualMemory——完整路径3.1 MEM_RELEASE 分支完整 VAD 释放四种释放情况情况1释放整个 VADBaseAddress VAD.StartingVpn, Size VAD 全范围 ─ 直接删除 VAD释放所有物理页 情况2释放 VAD 头部部分BaseAddress VAD.StartingVpn, Size VAD 全范围 ─ 调整 VAD.StartingVpn 原起始地址 Size ─ 释放对应 PTE 情况3释放 VAD 尾部部分BaseAddress VAD.StartingVpn, 释放到结束 ─ 调整 VAD.EndingVpn BaseAddress - 1 ─ 释放对应 PTE 情况4释放 VAD 中间部分释放中间一块两边保留 ─ 需要创建新 VAD 保存剩余后半部分 ─ 原 VAD 缩小到前半部分释放前 ┌──────────────────────────────┐ │ 原始 VAD │ └──────────────────────────────┘ 情况1 情况2 ┌──────────────────┐ ┌─────┐ ┌──────────┐ │ 全部释放 → 删除 │ │释放 │ │ 保留 │ └──────────────────┘ └─────┘ └──────────┘ Base StartingVpn 调整 情况3 情况4 ┌──────────┐ ┌─────┐ ┌─────┐ ┌──────┐ ┌─────┐ │ 保留 │ │释放 │ │释放 │ │ 保留1│ │保留2│ └──────────┘ └─────┘ └─────┘ └──────┘ └─────┘ (需要新建 VAD)3.2 MEM_DECOMMIT 分支取消提交// 1. 锁定地址空间// 2. 查找 VAD确认地址范围有效// 3. 调用 MiDecommitPages 取消提交// - 遍历指定范围的 PTE// - 对每个有效 PTE → 释放物理页、更新 PFN// - 将 PTE 写为 decommit 状态// 4. 更新进程 CommitCharge4. NtProtectVirtualMemory 路径通过MiProtectVirtualMemory实现1. 验证参数保护属性合法性 2. 锁定地址空间 3. 查找覆盖整个范围的 VAD 4. 调用 MiCheckSecuredVad 检查安全 VAD 限制 5. 锁定工作集 6. 遍历范围内的每个 PTE a. 如果 PTE 有效 - 修改硬件 PTE 的保护位 - 刷新 TLBKeFlushCurrentTb b. 如果 PTE 无效 - 修改软件 PTE 的保护位 c. 写时复制COW处理 - 如果新保护为只读但原为可写清除脏位 7. 更新 VAD 的保护属性 8. 解锁5. MiDeletePte——底层 PTE 删除逻辑文件virtual.c:369功能删除单个 PTE释放其引用的物理页。VOID NTAPIMiDeletePte(IN PMMPTE PointerPte,IN PVOID VirtualAddress,IN PEPROCESS CurrentProcess,IN PMMPTE PrototypePte){TempPte*PointerPte;if(TempPte.u.Hard.Valid0){// 无效 PTE可能是 Transition 或 PageFile 状态if(TempPte.u.Soft.Transition){// Transition PTE → 从列表中拆除递减引用MiUnlinkPageFromList(Pfn1);MiDecrementReferenceCount(Pfn1,PageFrameIndex);}// 忽略已换出PageFile的 PTE}else{// 有效 PTEPageFrameIndexPFN_FROM_PTE(TempPte);Pfn1MiGetPfnEntry(PageFrameIndex);if(Pfn1-u3.e1.PrototypePte1){// 原型 PTE → 递减页表共享计数和 PFN 共享计数MiDecrementShareCount(PageTablePfn,...);MiDecrementShareCount(Pfn1,PageFrameIndex);}else{// 普通 PTE → 标记删除递减共享计数MI_SET_PFN_DELETED(Pfn1);MiDecrementShareCount(Pfn1,PageFrameIndex);}// 擦除 PTEMI_ERASE_PTE(PointerPte);}KeFlushCurrentTb();}6. MiDeleteVirtualAddresses——批量 PTE 删除文件virtual.c:530功能批量删除指定虚拟地址范围内的所有 PTE。VOID NTAPIMiDeleteVirtualAddresses(IN ULONG_PTR Va,IN ULONG_PTR EndingAddress,IN OPTIONAL PMMVAD Vad){while(VaEndingAddress){// 逐级检查 PXE → PPE → PDE → PTE 的有效性// 跳过无效的大页表区域AddressGap// 对每个有效 PTE 调用 MiDeletePte// 递减页表引用计数必要时删除 PDEVaPAGE_SIZE;}}关键特点支持多级页表x86 PAE / x86-64的地址间隙跳过对 Section VAD 的间隙处理通过MI_GET_PROTOTYPE_PTE_FOR_VPN跳到下一个原型 PTE批量删除过程中保持 PFN 锁定7. MiDecommitPages——页面取消提交功能将已提交的页面取消提交释放物理内存但保留 VAD 结构。1. 遍历指定地址范围的每个 PTE 2. 对每个 PTE a. 如果 PTE 有效Valid1 - 获取 PFN - 标记 PFN 已删除MI_SET_PFN_DELETED - 递减 ShareCount - 擦除 PTE b. 如果 PTE 是 Transition 状态 - 从列表中拆除 PFN - 递减引用计数 c. 如果 PTE 是 DemandZero 状态 - 直接擦除 PTE 3. 更新进程/系统提交计数 4. 刷新 TLB8. MmGetPhysicalAddress——虚拟地址转物理地址文件virtual.c:5681PHYSICAL_ADDRESS NTAPIMmGetPhysicalAddress(PVOID Address){// 1. 检查 PDE 是否有效对大页 SPI 还检查 PPE/PXEif(MiAddressToPde(Address)-u.Hard.Valid){// 2. 检查是否为大页LargePageTempPde*MiAddressToPde(Address);if(TempPde.u.Hard.LargePage){// 大页偏移 地址低 22 位4MB 大页PhysicalAddress.QuadPart(ULONG64)TempPde.u.Hard.PageFrameNumberPAGE_SHIFT;PhysicalAddress.QuadPart((ULONG_PTR)Address(PAGE_SIZE*PTE_PER_PAGE-1));returnPhysicalAddress;}// 3. 检查 PTE 是否有效TempPte*MiAddressToPte(Address);if(TempPte.u.Hard.Valid){// 物理地址 PFN 12 页内偏移PhysicalAddress.QuadPart(ULONG64)TempPte.u.Hard.PageFrameNumberPAGE_SHIFT;PhysicalAddress.QuadPart((ULONG_PTR)Address(PAGE_SIZE-1));returnPhysicalAddress;}}// 4. 无效地址返回 0PhysicalAddress.QuadPart0;returnPhysicalAddress;}9. MmCopyVirtualMemory——跨进程内存拷贝文件virtual.c:1269两种策略基于数据量选择9.1 大拷贝 512 字节MDL 映射方式MiDoMappedCopyBufferSize MI_POOL_COPY_BYTES (512) → MiDoMappedCopy 流程 1. 循环拷贝每次最大 14 页MI_MAPPED_COPY_PAGES * PAGE_SIZE 56KB 2. 附加到源进程 → ProbeForRead → MmProbeAndLockPages → MmMapLockedPagesSpecifyCache 3. 分离源进程 4. 附加到目标进程 → ProbeForWrite → RtlCopyMemory 5. 分离目标进程 6. MmUnmapLockedPages → MmUnlockPages 7. 继续下一批数据9.2 小拷贝≤ 512 字节池缓冲方式MiDoPoolCopyBufferSize MI_POOL_COPY_BYTES (512) → MiDoPoolCopy 流程 1. 如果 BufferSize 512使用栈上缓冲StackBuffer否则分配 NonPagedPool 2. 附加到源进程 → ProbeForRead → RtlCopyMemory(池, 源) 3. 分离源进程 4. 附加到目标进程 → ProbeForWrite → RtlCopyMemory(目标, 池) 5. 分离目标进程 6. 重复直到所有数据拷贝完毕入口函数NTSTATUS NTAPIMmCopyVirtualMemory(PEPROCESS SourceProcess,PVOID SourceAddress,PEPROCESS TargetProcess,PVOID TargetAddress,SIZE_T BufferSize,KPROCESSOR_MODE PreviousMode,PSIZE_T ReturnSize){if(!BufferSize)returnSTATUS_SUCCESS;// 获取目标进程的 rundown protectionif(!ExAcquireRundownProtection(Process-RundownProtect))returnSTATUS_PROCESS_IS_TERMINATING;if(BufferSizeMI_POOL_COPY_BYTES)StatusMiDoMappedCopy(SourceProcess,SourceAddress,...);elseStatusMiDoPoolCopy(SourceProcess,SourceAddress,...);ExReleaseRundownProtection(Process-RundownProtect);returnStatus;}附录virtual.c 函数一览函数行号功能MiCalculatePageCommitment40计算指定地址范围内的已提交页数MiMakeSystemAddressValid181确保系统页表地址有效触发缺页MiDeleteSystemPageableVm273删除可分页系统内存MiDeletePte367删除单个 PTEMiDeleteVirtualAddresses528批量删除 PTEMiDoMappedCopy793MDL 方式跨进程拷贝MiDoPoolCopy1018池缓冲方式跨进程拷贝MmCopyVirtualMemory1269跨进程内存拷贝入口MiProtectVirtualMemory22 (extern)修改内存保护属性MiGetPageProtection1359获取页面的保护属性NtAllocateVirtualMemory4457虚拟内存分配系统调用MmGetPhysicalAddress5681虚拟地址→物理地址转换

相关新闻

【Claude Code】使用政策拒绝 Claude Code unable to respond Usage Policy refusal 应对

【Claude Code】使用政策拒绝 Claude Code unable to respond Usage Policy refusal 应对

文章目录 一、问题描述 1.1 环境信息 1.2 报错现象 二、根因分析 2.1 错误链路追踪 2.2 可能原因列举 三、解决方案 方案一:/rewind 回退到触发轮次之前(推荐) 方案二:/clear 启动全新会话 方案三:非交互模式(-p)的特殊处理 四、验证与回归测试 五、总结与预防 5.1 核心…

2026/7/22 22:30:00阅读更多 →
07.20.每日总结

07.20.每日总结

7月20日总结 Linux基础 考试相关 复习前面讲过的所有命令 linux考试:91分 错题与不熟练的题: 存放了linux系统引导和启动时候使用的一些文件和目录的目录是/boot useradd添加用户时哪个选项可以指定描述信息-c 去掉vi编辑器中上一步错误的操作&#xff1…

2026/7/22 22:30:00阅读更多 →
达梦数据库搭建(windows+Linux,图形化和命令行)

达梦数据库搭建(windows+Linux,图形化和命令行)

Windows上单机环境搭建Cmd命令systeminfo查看电脑信息确保1G可用内存根据电脑cpu和操作系统下载对应的数据库,并解压点击.iso光盘镜像文件安装选择时区并确定DM 数据库配置助手 来完成数据库初始化创建数据库实例选择数据库模板Linux上单机环境搭建获取对应的安装包…

2026/7/22 22:30:00阅读更多 →
AI 模型水印与溯源技术深度解析:从 Kirchenbauer 绿名单到 SynthID 的内容可信认证与知识产权保护体系

AI 模型水印与溯源技术深度解析:从 Kirchenbauer 绿名单到 SynthID 的内容可信认证与知识产权保护体系

AI 模型水印与溯源技术深度解析:从 Kirchenbauer 绿名单到 SynthID 的内容可信认证与知识产权保护体系 核心痛点:随着 ChatGPT、Claude、Gemini 等大模型深度融入内容生产与软件工程链路,AI 生成文本/图像/视频的真实性鉴别、模型本身的知识产权归属、训练数据的合法溯源成为…

2026/7/22 23:22:23阅读更多 →
智能体蜂群实验:新框架性能提升,不同模型组合成本差异巨大!

智能体蜂群实验:新框架性能提升,不同模型组合成本差异巨大!

Cursor产品导航Cursor提供多种功能与服务,包括产品(智能体、云端、移动端、自动化、CLI、应用市场、代码审查)、企业、定价、资源(更新日志、Blog、文档、社区、帮助、工作坊、论坛、招聘)等板块,还有登录、…

2026/7/22 23:22:23阅读更多 →
“阅后即焚”的PrivateBin与乌龙事件

“阅后即焚”的PrivateBin与乌龙事件

网管小贾 / sysadm.cc最近新上任的薛总,虽是一介女流,却是雷厉风行,言谈举止一丝不苟,颇有高端职场女性的风范。 听有人说,她可是董事长亲自指派来这当总经理的,嗯,怎么说我也应该巴结好啊&…

2026/7/22 23:22:23阅读更多 →
知网/万方/PubMed三库查重结果竟相差41.2%?揭秘AI搜索底层语义锚点偏移机制(含TensorFlow调试日志)

知网/万方/PubMed三库查重结果竟相差41.2%?揭秘AI搜索底层语义锚点偏移机制(含TensorFlow调试日志)

更多请点击: https://codechina.net 第一章:知网/万方/PubMed三库查重结果差异的实证现象与问题提出 在科研写作与学术出版实践中,同一文献在知网、万方与PubMed三大数据库中呈现显著不同的重复率数值,已成为普遍且亟待解析的技术…

2026/7/22 23:22:23阅读更多 →
如何5分钟掌握biliTickerBuy:B站会员购智能抢票终极指南

如何5分钟掌握biliTickerBuy:B站会员购智能抢票终极指南

如何5分钟掌握biliTickerBuy:B站会员购智能抢票终极指南 【免费下载链接】biliTickerBuy b站会员购购票辅助工具 项目地址: https://gitcode.com/GitHub_Trending/bi/biliTickerBuy 你是否曾经在B站会员购抢票时,面对心仪的商品却总是慢人一步&am…

2026/7/22 23:22:23阅读更多 →
你的AI编程工具API Key安全吗?从一次密钥泄露事件聊到BYOK机制

你的AI编程工具API Key安全吗?从一次密钥泄露事件聊到BYOK机制

前几天有个朋友跟我吐槽:他们团队用的某个 AI 编程工具,每个开发者的机器上都配了一份 OpenAI 的 API Key。结果有个实习生的电脑中了挖矿木马,API Key 被偷了,一晚上刷掉了几千美金的额度。这事儿听着极端,但其实特别…

2026/7/22 23:20:23阅读更多 →
Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/22 0:53:59阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/22 0:53:59阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/22 0:53:59阅读更多 →
中小企业小程序开发公司怎么选:预算、上手和售后避坑指南

中小企业小程序开发公司怎么选:预算、上手和售后避坑指南

中小企业做小程序,最常见的矛盾是预算有限,但又不希望功能太单薄;没有技术团队,但又希望后续能自己运营;想快速上线,又担心隐性收费和售后失联。选型时如果只看“低价套餐”或“案例数量”,很容…

2026/7/22 0:01:17阅读更多 →
GEO优化如何沉淀长期内容资产?广拓时代谈AI搜索时代的内容ROI

GEO优化如何沉淀长期内容资产?广拓时代谈AI搜索时代的内容ROI

企业做营销,最怕钱花完了,资产没有留下。 效果广告能带来一段时间的曝光,但预算停止后,流量往往也随之停止。短视频内容可能在几天内冲高,也可能很快沉下去。AI搜索时代,企业需要重新思考一个问题&#xff…

2026/7/22 0:01:17阅读更多 →
Agent 终态判定:何时该停止思考、给出最终回复

Agent 终态判定:何时该停止思考、给出最终回复

Agent 终态判定:何时该停止思考、给出最终回复 一、你的 Agent 在"再想想"的循环里绕了 12 轮,用户已经关窗口了 Agent 与人最大的区别是:人知道什么时候该停下来给答案,Agent 会一直"想"下去。你给 Agent 接…

2026/7/22 0:01:17阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/22 22:56:18阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/22 18:55:50阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/22 18:55:50阅读更多 →