動開發(fā):用 Minifilter 檢測文件與流刪除——Windows-driver-samples delete 示例深度解析)
示例工程【免費(fèi)下載鏈接】Windows-driver-samplesThis repo contains driver samples prepared for use with Microsoft Visual Studio and the Windows Driver Kit (WDK). It contains both Universal Windows Driver and desktop-only driver samples.項(xiàng)目地址https://gitcode.com/gh_mirrors/wi/Windows-driver-samples點(diǎn)擊查看免費(fèi)下載Delete 是一個以文件刪除/流刪除檢測為核心目標(biāo)的文件系統(tǒng)微過濾驅(qū)動Minifilter示例位于本倉庫 filesys/miniFilter/delete 目錄。它不攔截刪除而是在 IRP_MJ_CREATE、IRP_MJ_SET_INFORMATION 與 IRP_MJ_CLEANUP 的回調(diào)中標(biāo)記刪除候選并在操作完成后驗(yàn)證刪除是否真正發(fā)生最終以內(nèi)核調(diào)試輸出DbgPrint的形式上報。閱讀本文后你將掌握文件刪除在 Windows 上不可提前預(yù)測的根本原因、兩種刪除路徑FILE_DELETE_ON_CLOSE 與 FileDispositionInformation/FileDispositionInformationEx的檢測原理、并發(fā) SetDisposition 競態(tài)的識別方法、整文件刪除與單流刪除的區(qū)分技巧以及事務(wù)TxF環(huán)境下刪除上報的提交/回滾處理。概述一個只做事后確認(rèn)的刪除檢測過濾器該示例在 README 中定義為 Demonstrates how to detect deletions of files or streams. Deletions are reported as debug output.README.md即檢測對象文件file與流stream含命名數(shù)據(jù)流 ADS的刪除輸出方式刪除事件通過DbgPrintExDPFLTR_FLTMGR_ID、DPFLTR_ERROR_LEVEL寫入內(nèi)核調(diào)試輸出參見 delete.c 中的DF_PRINT宏運(yùn)行形態(tài)構(gòu)建出的delete.sys是一個完整的文件系統(tǒng)微過濾驅(qū)動全部代碼集中在一個 delete.c約 3282 行中。整個驅(qū)動只有兩個事件入口和一個驗(yàn)證出口階段關(guān)注對象作用IRP_MJ_CREATEFILE_DELETE_ON_CLOSE標(biāo)志標(biāo)記關(guān)閉即刪的刪除候選IRP_MJ_SET_INFORMATIONFileDispositionInformation/FileDispositionInformationEx記錄刪除處置delete disposition狀態(tài)變化IRP_MJ_CLEANUPpost刪除候選的最終狀態(tài)驗(yàn)證文件/流是否真的被刪除并上報這三個入口在 Callbacks 操作注冊表 中登記CONST FLT_OPERATION_REGISTRATION Callbacks[] { { IRP_MJ_CREATE, 0, DfPreCreateCallback, DfPostCreateCallback }, { IRP_MJ_SET_INFORMATION, FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO, DfPreSetInfoCallback, DfPostSetInfoCallback }, { IRP_MJ_CLEANUP, 0, DfPreCleanupCallback, DfPostCleanupCallback }, { IRP_MJ_OPERATION_END } };注意IRP_MJ_SET_INFORMATION使用了FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO跳過分頁 I/O避免在內(nèi)存不足的換頁路徑上增加額外負(fù)擔(dān)。核心難點(diǎn)為什么刪除無法提前預(yù)知README 特別強(qiáng)調(diào)了一個設(shè)計約束README.md 中的 NOTE由于 Windows 操作系統(tǒng)刪除文件的機(jī)制minifilter 無法提前預(yù)知某個文件或流將被刪除它只能檢測可能導(dǎo)致刪除的操作然后在該操作完成后判斷刪除是否真的發(fā)生了。原因在于 Windows 的刪除模型是延遲刪除刪除并不是在發(fā)出刪除請求時立即完成而是先把文件/流置為刪除待定delete-pending待最后一個句柄關(guān)閉后才真正從卷上移除。因此一次FILE_DELETE_ON_CLOSE創(chuàng)建可能最終沒有刪除例如后續(xù)又通過FileDispositionInformationEx清除了刪除標(biāo)志一次SetFileDisposition也可能因?yàn)楹罄m(xù)句柄仍打開而遲遲不生效同一流上的多個刪除處置操作還可能互相覆蓋race。所以本示例的全部邏輯都遵循同一思路先標(biāo)記候選再在 post-cleanup 時做最終驗(yàn)證驗(yàn)證手段是查詢FileStandardInformation返回STATUS_FILE_DELETED即證明已刪除必要時再通過文件 ID 打開或查詢對象 ID 來區(qū)分整文件刪除與僅流刪除。三種上下文實(shí)例、流與事務(wù)驅(qū)動通過 FltMgr 上下文Context機(jī)制保存跨操作的狀態(tài)注冊于 Contexts 注冊表FLT_INSTANCE_CONTEXTDF_INSTANCE_CONTEXT緩存卷 GUID 名稱VolumeGuidName避免反復(fù)查詢FLT_STREAM_CONTEXTDF_STREAM_CONTEXT這是刪除檢測的核心載體字段包括NameInfo最后一次 pre-cleanup 時取得的打開名FLT_FILE_NAME_INFORMATION用于上報FileId文件 IDDF_FILE_REFERENCE兼容 NTFS 64 位與 ReFS 128 位NumOps正在進(jìn)行的刪除處置操作計數(shù)競態(tài)檢測用IsNotified是否已上報過刪除防止重復(fù)上報SetDisp/DeleteOnClose刪除處置與關(guān)閉即刪狀態(tài)。FLT_TRANSACTION_CONTEXTDF_TRANSACTION_CONTEXT維護(hù)事務(wù)內(nèi)待上報的刪除通知鏈表DeleteNotifyList與保護(hù)它的ERESOURCE。一個值得學(xué)習(xí)的細(xì)節(jié)DF_TRANSACTION_CONTEXT中的ERESOURCE被設(shè)計成指針 單獨(dú)分配而不是直接內(nèi)嵌在結(jié)構(gòu)里delete.c 的注釋說明了原因。因?yàn)镋RESOURCE必須從NonPagedPool分配若直接內(nèi)嵌整個事務(wù)上下文都要提升為非分頁內(nèi)存用指針方式則主體仍可放在PagedPoolDF_CONTEXT_POOL_TYPE PagedPool僅ERESOURCE本身走非分頁池從而節(jié)省非分頁內(nèi)存。上下文的獲取/分配/附加統(tǒng)一封裝在DfGetOrSetContextdelete.c中它實(shí)現(xiàn)了無則取、有則建、建后設(shè)、競態(tài)則復(fù)用他人已設(shè)置的那份的完整語義當(dāng)上下文類型是FLT_TRANSACTION_CONTEXT時還會額外調(diào)用FltEnlistInTransaction加入事務(wù)注冊DF_NOTIFICATION_MASK即TRANSACTION_NOTIFY_COMMIT_FINALIZE | TRANSACTION_NOTIFY_ROLLBACK見 delete.c。檢測路徑一FILE_DELETE_ON_CLOSE創(chuàng)建即刪應(yīng)用以FILE_DELETE_ON_CLOSE打開文件時文件會在最后一個句柄關(guān)閉時被刪除。DfPreCreateCallbackdelete.c檢查創(chuàng)建選項(xiàng)if (FlagOn( Data-Iopb-Parameters.Create.Options, FILE_DELETE_ON_CLOSE )) { status DfAllocateContext( FLT_STREAM_CONTEXT, streamContext ); if (NT_SUCCESS( status )) { *CompletionContext (PVOID)streamContext; return FLT_PREOP_SYNCHRONIZE; } ... } *CompletionContext NULL; return FLT_PREOP_SUCCESS_NO_CALLBACK;關(guān)鍵點(diǎn)只有命中FILE_DELETE_ON_CLOSE才預(yù)分配流上下文并通過CompletionContext傳給 post 回調(diào)返回FLT_PREOP_SYNCHRONIZE強(qiáng)制同步 post 回調(diào)保證創(chuàng)建結(jié)果立即可見在DfPostCreateCallbackdelete.c中只有創(chuàng)建真正成功且不是重解析點(diǎn)STATUS_REPARSE時才把上下文附加到流并置streamContext-DeleteOnClose TRUE。代碼中的FLTFL_POST_OPERATION_DRAINING斷言則用于確保沒有處于排空unload 中的殘余操作狀態(tài)。檢測路徑二FileDispositionInformation / FileDispositionInformationEx另一種刪除方式是IRP_MJ_SET_INFORMATION設(shè)置刪除處置。DfPreSetInfoCallbackdelete.c只對FileDispositionInformation與FileDispositionInformationEx兩類信息類感興趣并在此完成競態(tài)檢測race (InterlockedIncrement( streamContext-NumOps ) 1); if (!race) { // 唯一在途操作結(jié)果是確定的做 postop *CompletionContext (PVOID)streamContext; return FLT_PREOP_SYNCHRONIZE; } else { // 存在并發(fā)操作刪除處置的最終狀態(tài)不可知 FltReleaseContext( streamContext ); } // FALL_THROUGH return FLT_PREOP_SUCCESS_NO_CALLBACK;這里的邏輯非常精巧NumOps記錄在途的刪除處置修改數(shù)。當(dāng)檢測到并發(fā)race時post 回調(diào)不會被調(diào)用NumOps也就永遠(yuǎn)不會被遞減于是該值保持 2永久地把這個流標(biāo)記為必須檢查刪除的候選——因?yàn)榧热粺o法確定處置狀態(tài)的最終結(jié)果就寧可多做一次刪除驗(yàn)證。而在非競態(tài)情況下DfPostSetInfoCallbackdelete.c會在操作成功后把結(jié)果寫入上下文FileDispositionInformationEx若設(shè)置了FILE_DISPOSITION_ON_CLOSE則按FILE_DISPOSITION_DELETE更新DeleteOnClose否則更新SetDispFileDispositionInformation直接用FILE_DISPOSITION_INFORMATION.DeleteFile更新SetDisp。最后InterlockedDecrement( streamContext-NumOps )并釋放上下文引用保持計數(shù)與引用平衡。驗(yàn)證出口post-cleanup 才是真相時刻無論文件是通過哪種方式進(jìn)入刪除候選狀態(tài)真正的判定都在DfPostCleanupCallbackdelete.c。前置的DfPreCleanupCallbackdelete.c只做一件事為帶流上下文的文件抓取名稱信息DfGetFileNameInformation內(nèi)部使用FltGetFileNameInformation(FLT_FILE_NAME_OPENED | FLT_FILE_NAME_QUERY_DEFAULT)FltParseFileNameInformation以便上報時有名字可用。post-cleanup 中的候選判定條件源碼注釋原話歸納NumOps 0—— 存在過競態(tài)處置狀態(tài)未知保守起見必須檢查SetDisp TRUE—— 已設(shè)置刪除處置DeleteOnClose TRUE—— 曾以FILE_DELETE_ON_CLOSE打開注意FileDispositionInformationEx可清除該標(biāo)志。三者滿足其一且IsNotified 0尚未上報過時執(zhí)行驗(yàn)證status FltQueryInformationFile( Data-Iopb-TargetInstance, Data-Iopb-TargetFileObject, fileInfo, sizeof(fileInfo), FileStandardInformation, NULL ); if (STATUS_FILE_DELETED status) { status DfProcessDelete( Data, FltObjects, streamContext ); ... }FileStandardInformation查詢返回STATUS_FILE_DELETED即證明流已被刪除隨后進(jìn)入DfProcessDelete做最終定性。整文件刪除 vs 單流刪除的區(qū)分這是本示例最有技術(shù)含量的一環(huán)。場景是當(dāng)最后一個句柄恰好是某個刪除待定的命名數(shù)據(jù)流ADS的句柄時關(guān)閉它會連帶整個文件一起消失——此時應(yīng)上報整文件刪除而不是流刪除。DfProcessDeletedelete.c先判斷是否處于事務(wù)中FltObjects-Transaction ! NULL然后調(diào)用DfIsFileDeleteddelete.c來區(qū)分。該函數(shù)按文件系統(tǒng)類型與事務(wù)狀態(tài)選擇驗(yàn)證手段場景手段判定非事務(wù) NTFSFSCTL_GET_OBJECT_IDFltFsControlFileSTATUS_OBJECTID_NOT_FOUND→ 文件仍存在只是沒有對象 ID其余錯誤碼透傳STATUS_FILE_DELETED表示已刪除事務(wù)中 或 ReFS按文件 ID 打開DfDetectDeleteByFileIdSTATUS_INVALID_PARAMETER→ 文件已刪除映射為STATUS_FILE_DELETEDSTATUS_DELETE_PENDING→ 文件仍在但刪除待定映射為成功之所以分兩條路徑代碼注釋給出了兩個關(guān)鍵原因FSCTL_GET_OBJECT_ID在事務(wù)內(nèi)刪除時不會返回STATUS_FILE_DELETED所以事務(wù)場景必須改用打開文件 ID 的方式ReFS 不支持對象 ID因此 ReFS 卷上始終走按 ID 打開。按文件 ID 打開的核心實(shí)現(xiàn)在DfDetectDeleteByFileIddelete.cstatus DfBuildFileIdString( Data, FltObjects, StreamContext, fileIdString ); ... IoInitializeDriverCreateContext( driverCreateContext ); driverCreateContext.TxnParameters IoGetTransactionParameterBlock( Data-Iopb-TargetFileObject ); status FltCreateFileEx2( gFilterHandle, Data-Iopb-TargetInstance, handle, NULL, FILE_READ_ATTRIBUTES, objectAttributes, ioStatus, (PLARGE_INTEGER) NULL, 0L, FILE_SHARE_VALID_FLAGS, FILE_OPEN, FILE_OPEN_REPARSE_POINT | FILE_OPEN_BY_FILE_ID, (PVOID) NULL, 0L, IO_IGNORE_SHARE_ACCESS_CHECK, driverCreateContext ); if (NT_SUCCESS( status )) { status FltClose( handle ); ... }關(guān)鍵點(diǎn)打開路徑由卷 GUID 名 反斜杠 文件 ID 組成DfBuildFileIdStringdelete.c必須用FILE_OPEN_BY_FILE_ID按 ID 打開必須從當(dāng)前文件對象繼承事務(wù)參數(shù)塊IoGetTransactionParameterBlock以保證打開操作發(fā)生在同一事務(wù)內(nèi)文件已刪除時DfBuildFileIdString內(nèi)部查詢文件 ID 的FltQueryInformationFile會直接返回STATUS_FILE_DELETED短路整個打開過程見 delete.c 注釋而按 ID 打開一個不存在的文件則返回STATUS_INVALID_PARAMETER。NTFS 與 ReFS 的文件 ID 兼容DF_FILE_REFERENCE聯(lián)合體delete.c專門處理兩種文件 IDNTFS 的 64 位 ID 與 ReFS 的 128 位 ID。DfGetFileIddelete.c先查詢FileInternalInformation若返回FILE_INVALID_FILE_ID說明該 ID 必須用 128 位表示再改用FileIdInformation查詢。代碼還通過KeMemoryBarrier()保證寫入 128 位 ID與置位FileIdSet的次序因?yàn)榫幾g器不支持原生 128 位值并用DfSizeofFileId宏動態(tài)判斷 ID 長度確保 ReFS 按 ID 打開時 64/128 位皆可用。事務(wù)TxF環(huán)境下的延遲上報文件在事務(wù)內(nèi)被刪除時提交前文件仍是活著的只有提交才真正生效回滾則相當(dāng)于沒刪。因此驅(qū)動不能在 post-cleanup 立刻上報而要掛起等待事務(wù)終局。機(jī)制如下DfProcessDelete檢測到處于事務(wù)中時通過DfGetOrSetContext取得事務(wù)上下文并經(jīng)由FltEnlistInTransaction登記了TRANSACTION_NOTIFY_COMMIT_FINALIZE | TRANSACTION_NOTIFY_ROLLBACK通知DfNotifyDeletedelete.c此時不直接打印deleted而是打印deleted in a transaction!并調(diào)用DfAddTransDeleteNotifydelete.c把一條DF_DELETE_NOTIFY內(nèi)含StreamContext引用與FileDelete標(biāo)志掛入事務(wù)上下文的DeleteNotifyList該鏈表由獨(dú)立分配的ERESOURCE保護(hù)DfTransactionNotificationCallbackdelete.c在事務(wù)提交或回滾時被 FltMgr 回調(diào)提交則打印 deleted due to a transaction commit!回滾則先把IsNotified遞減讓刪除候選回到未上報狀態(tài)再打印 saved due to a transaction rollback!見DfNotifyDeleteOnTransactionEnddelete.c事務(wù)上下文清理回調(diào)DfTransactionContextCleanupCallbackdelete.c負(fù)責(zé)清空鏈表、釋放每個DF_DELETE_NOTIFY及其StreamContext引用、銷毀ERESOURCE。這一整套掛起—提交/回滾時結(jié)算的設(shè)計是事務(wù)型文件刪除上報的正確范式也解釋了為什么事務(wù)上下文要同時維護(hù)列表與鎖。只掛接可寫 NTFS/ReFS 卷DfInstanceSetupdelete.c決定了過濾器掛接范圍先調(diào)用FltIsVolumeWritable判斷卷是否可寫——掛接到只讀卷沒有意義因?yàn)橹蛔x卷上不可能刪除文件再按VolumeFilesystemType限定為FLT_FSTYPE_NTFS與FLT_FSTYPE_REFS其他情況一律返回STATUS_FLT_DO_NOT_ATTACH。INF 安裝配置解讀delete.inf 展示了典型的 UWD minifilter 安裝配置[Version] Signature $Windows NT$ Class ActivityMonitor ;This is determined by the work this filter driver does ClassGuid {b86dff51-a31e-4bac-b3cf-e8cfe75c9fc2} ;This value is determined by the Class Provider %ProviderString% DriverVer 06/16/2007,1.0.0.1 CatalogFile delete.cat PnpLockdown 1設(shè)備類ActivityMonitor活動監(jiān)視類與觀察但不攔截的定位一致服務(wù)配置MiniFilter.ServiceServiceType 2SERVICE_FILE_SYSTEM_DRIVER、StartType 3SERVICE_DEMAND_START 按需啟動、ErrorControl 1、Dependencies FltMgr、LoadOrderGroup FSFilter Activity Monitor注冊表配置MiniFilter.AddRegistrySupportedFeatures 0x3并在Parameters\Instances下注冊默認(rèn)實(shí)例實(shí)例名delete Instance、海拔Altitude 370150、Flags 0x0允許所有掛接同時提供面向新版 Windows 的DefaultInstall.NT$ARCH$.10.0...25952通用安裝節(jié)以及面向舊系統(tǒng)的 downlevel 安裝/卸載節(jié)含LegacyUninstall與DelService0x200 停止后刪除。海拔 370150 落在 INF 中聲明的FSFilter Activity Monitor加載順序組區(qū)間內(nèi)且與LoadOrderGroup聲明保持一致確保過濾器在文件系統(tǒng)棧中的加載次序正確。工程配置與構(gòu)建工程文件 delete.vcxproj 聲明DriverTargetPlatform Universal、PlatformToolset WindowsKernelModeDriver10.0、ConfigurationType Driver支持 Debug/Release × x64/ARM64 四套配置鏈接fltMgr.lib啟用POOL_NX_OPTIN1、WarningLevel Level4且TreatWarningAsError true驅(qū)動簽名摘要算法為 sha256delete.sln 與 delete.rcVFT_DRV / VFT2_DRV_SYSTEM文件描述 Delete Notification Filter Driver配套齊全README 明確說明該示例是Universal Windows DriverUWD僅使用 OneCoreUAP 包含的 API/DDI這意味著它可被用于需要通用驅(qū)動形態(tài)的 Windows 版本與架構(gòu)組合整個倉庫的構(gòu)建流程可參考根目錄的 Building-Locally.md 與 Build-Samples.ps1配合 Visual Studio WDK 完成編譯與簽名。調(diào)試輸出與觀察方法驅(qū)動的所有刪除事件通過DF_DBG_PRINT(DFDBG_TRACE_ERRORS, ...)宏輸出delete.c。gTraceFlags默認(rèn)值為DFDBG_TRACE_ERRORS三個可用位為標(biāo)志值用途DFDBG_TRACE_ERRORS0x00000001錯誤與刪除事件上報默認(rèn)開啟DFDBG_TRACE_ROUTINES0x00000002例程進(jìn)入/離開軌跡DFDBG_TRACE_OPERATION_STATUS0x00000004操作狀態(tài)跟蹤輸出示例源碼實(shí)際格式包括delete!DfPostCleanupCallback: A file %wZ (%p) has been deleted!delete!DfPostCleanupCallback: An alternate data stream %wZ (%p) has been deleted!事務(wù)場景A file %wZ (%p) has been deleted in a transaction!隨后在提交/回滾時打印deleted due to a transaction commit!或saved due to a transaction rollback!上報同時打印文件打開名與流上下文指針%p后者可用于在重命名等邊界情況下輔助區(qū)分對象參見DfPreCleanupCallback注釋。若在 WinDbg/內(nèi)核調(diào)試會話中加載該驅(qū)動并執(zhí)行刪除操作即可在調(diào)試輸出流中觀察到上述信息。源碼結(jié)構(gòu)速查函數(shù)均在 delete.c職責(zé)DriverEntry/DfUnloadFltMgr 注冊、開始過濾 / 反注冊DfInstanceSetup系列實(shí)例掛接策略與拆解回調(diào)DfGetOrSetContext/DfAllocateContext/DfSetContext/DfGetContext三類上下文的生命周期管理DfGetFileNameInformation/DfGetFileId/DfBuildFileIdString/DfGetVolumeGuidName名稱、文件 ID、卷 GUID 獲取與緩存DfDetectDeleteByFileId/DfIsFileDeleted按 ID 打開探測 / 整文件 vs 流刪除判定DfProcessDelete/DfNotifyDelete/DfAddTransDeleteNotify/DfNotifyDeleteOnTransactionEnd刪除定性、上報與事務(wù)掛起結(jié)算DfPre/PostCreateCallbackFILE_DELETE_ON_CLOSE 候選標(biāo)記DfPre/PostSetInfoCallback刪除處置跟蹤與競態(tài)檢測DfPre/PostCleanupCallback名稱采集與最終刪除驗(yàn)證DfTransactionNotificationCallback提交/回滾時的延遲上報小結(jié)Delete 示例用約三千行代碼完整演示了文件系統(tǒng)微過濾驅(qū)動中刪除檢測這一經(jīng)典難題的工業(yè)級解法候選標(biāo)記CREATE 的FILE_DELETE_ON_CLOSE SET_INFORMATION 的處置信息→ 競態(tài)兜底NumOps永不歸零策略→ 事后驗(yàn)證FileStandardInformation/ 對象 ID / 按文件 ID 打開→ 事務(wù)結(jié)算commit/rollback 通知。它同時兼顧了 NTFS 與 ReFS 的文件 ID 差異、整文件與流刪除的語義區(qū)分以及 UWD 兼容性要求是學(xué)習(xí)IRP_MJ_CLEANUP語義、FltMgr 上下文與 TxF 事務(wù)通知的最佳起點(diǎn)之一。對于要在自己的驅(qū)動中實(shí)現(xiàn)刪除監(jiān)控如審計、數(shù)據(jù)保護(hù)、同步備份觸發(fā)的開發(fā)者可以直接以本示例為骨架把DfNotifyDelete中的DbgPrint替換為實(shí)際業(yè)務(wù)上報邏輯。贊分享示例工程【免費(fèi)下載鏈接】Windows-driver-samplesThis repo contains driver samples prepared for use with Microsoft Visual Studio and the Windows Driver Kit (WDK). It contains both Universal Windows Driver and desktop-only driver samples.項(xiàng)目地址https://gitcode.com/gh_mirrors/wi/Windows-driver-samples點(diǎn)擊查看免費(fèi)下載相關(guān)推薦Windows-driver-samples I2C驅(qū)動SkeletonI2C示例深度解析Windows driver samples I2C驅(qū)動SkeletonI2C示例深度解析 概述 SkeletonI2C是Windows驅(qū)動示例項(xiàng)目中的一個關(guān)示例工程性能優(yōu)化指南Encrypted Core Data緩存配置與數(shù)據(jù)庫遷移技巧 性能優(yōu)化指南Encrypted Core Data緩存配置與數(shù)據(jù)庫遷移技巧 Encrypted Core Data 是一個強(qiáng)大的iOS加密數(shù)據(jù)庫解決方案應(yīng)用安全密碼學(xué)Windows-driver-samples智慧博物館文物展示設(shè)備驅(qū)動開發(fā)Windows driver samples智慧博物館文物展示設(shè)備驅(qū)動開發(fā) 傳統(tǒng)博物館展覽常面臨文物保護(hù)與觀眾體驗(yàn)的矛盾Windows driver sam示例工程上一篇米哈游游戲字體完全指南11款架空文字字體快速上手教程下一篇11款米哈游游戲字體完全指南讓游戲文字走進(jìn)現(xiàn)實(shí)創(chuàng)作創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考