0xc0000142。我花了三小时走完两个错误方向、抓了一个 460 MB Full Memory Dump、亲手解析 MSVC C++ 异常结构,最后在 std::filesystem::filesystem_error 的 what() 字符串里找到了答案。这篇复盘把每一个弯路都诚实地留下来——错误路径往往比正确路径更有教训。太长不看版 — 如果你在搜索错误码找到这里#
本节给结论和最小修复;想看调试过程的直接跳到 §0 事发现场。
症状指纹(三条全中才是本文这个问题):
- 宿主是 Windows Server RDS(Remote Desktop Session Host,多用户通过远程桌面共享同一台主机)
Administrator能正常打开目标软件,BUILTIN\Users组里的普通用户一律弹0xc0000142- Windows 事件日志里
Application Error的Exception Code是0xe06d7363(MSVC C++ throw),而不是对话框上写的0xc0000142
30 秒自检(在宿主上以管理员身份执行):
# 1. 是否启用了 per-session TEMP 重定向
reg query "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server" /v PerSessionTempDir
# 期望看到:PerSessionTempDir REG_DWORD 0x1 ← 这是问题的大前提
# 2. 当前所有 RDP 会话的用户和 session ID
query user
# 3. 枚举 C:\Windows\TEMP\<sid> 下每个数字目录的 owner
# 与上一步的 (session ID → user) 映射做对比
Get-ChildItem C:\Windows\TEMP -Directory |
Where-Object Name -match '^\d+$' |
ForEach-Object {
'{0,3}: owner={1}' -f $_.Name, (Get-Acl $_.FullName).Owner
}
如果你看到任何 session ID 的 owner 不是当前用这个 session 的用户 —— 中奖,就是本文的问题。根因是 RDS 的"每会话独立临时目录"策略下 session ID 跨启动周期被复用,而旧的 C:\Windows\TEMP\<sid> 目录没被清理,ACL (Access Control List,访问控制列表) 还停留在前一轮的老用户身上。新登录的用户拿到同一个 session ID、TEMP 环境变量指向这个目录,却没有读权限 —— Adobe Illustrator 的 std::filesystem::temp_directory_path() 直接抛未捕获异常。
最小永久修复(按 HKU\<SID> 和当前会话信息替换占位符):
# [1] 永久关掉 per-session TEMP 重定向
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server" `
/v PerSessionTempDir /t REG_DWORD /d 0 /f
# [2] 给每个普通用户的 HKU\<SID>\Environment 显式绑定 TEMP 到 profile
# REG_EXPAND_SZ + %USERPROFILE% token:每个用户登录时展开到自己的 profile
reg add "HKU\<user_sid>\Environment" /v TEMP /t REG_EXPAND_SZ `
/d "%USERPROFILE%\AppData\Local\Temp" /f
reg add "HKU\<user_sid>\Environment" /v TMP /t REG_EXPAND_SZ `
/d "%USERPROFILE%\AppData\Local\Temp" /f
# [3] 修改 Default 用户模板(未来新建的用户就不会再踩)
# 注意:真正的 Default 模板是 C:\Users\Default\NTUSER.DAT,
# 不是 HKU\.DEFAULT(后者对应 LocalSystem 账号,与新建用户无关)。
# 必须先 reg load 离线 hive,改完再 unload
reg load "HKU\_DefaultTemplate" "C:\Users\Default\NTUSER.DAT"
reg add "HKU\_DefaultTemplate\Environment" /v TEMP /t REG_EXPAND_SZ /d "%USERPROFILE%\AppData\Local\Temp" /f
reg add "HKU\_DefaultTemplate\Environment" /v TMP /t REG_EXPAND_SZ /d "%USERPROFILE%\AppData\Local\Temp" /f
[gc]::Collect() # 释放 PowerShell 可能持有的 hive 句柄
reg unload "HKU\_DefaultTemplate"
# [4] 过渡:修正当前错配目录的 ACL,让正在登录中的用户不必注销就能立即用
takeown /f C:\Windows\TEMP\<stale_sid> /r /d Y
icacls C:\Windows\TEMP\<stale_sid> /grant "<current_session_user>:(OI)(CI)F"
完成后,让受影响的用户注销重新登录一次——他们的进程环境块会从 HKU\<SID>\Environment 重新读到正确的 TEMP。
如果你只想快速修复,到这里就够了。下面是完整的调试过程——包含三小时的弯路、两次被自证其非的假设、以及一段手工解析 MSVC C++ 异常结构的 Python 代码。
0. 事发现场#
- 宿主:Windows Server 2022 (Build 20348.4893),Remote Desktop Session Host,多名普通用户通过 RDP 共享使用
- 软件栈:Adobe Illustrator 2025 (29.8.2)、PADS VX.2.4、Cadence SPB 16.6
- 症状:今天 14:04 开始,所有普通用户(
rds-user-a、rds-user-b、rds-user-c)尝试打开 Illustrator 时立即弹出:
Illustrator.exe - 应用程序错误 应用程序无法正常启动 (0xc0000142)。请单击"确定"关闭应用程序。
- 而
Administrator账号可以正常打开,进程稳定运行 - 前一天、前两天都是正常的,症状是"今天突然开始"
初始信息很简单,但每一条都值得看两眼:
| 事实 | 为什么重要 |
|---|---|
| 只有管理员能用 | 第一反应会怀疑权限,但也可能是别的 per-user 状态 |
| 突然发生 | 意味着某个环境状态改变了,不是安装问题 |
0xc0000142 = STATUS_DLL_INIT_FAILED | 表面看是进程加载阶段崩溃,但后面会看到这是个假象 |
| 机器装着 PADS | 这家机器最近刚部署过 PADS RDP bypass hook,用的是 AppInit_DLLs 全局注入。太巧了 |
最后这一条几乎直接把我带进了第一个错误方向。
1. 第一个错误方向:RdpBypass.dll 是元凶吗?#
1.1 “AppInit_DLLs 注入挂了” —— 一个看上去完美的因果链#
结论先行:这一节介绍一个基于 AppInit_DLLs 的漂亮假说,看起来能完美解释所有现象,最后会被一个关键反例击穿。
这台机器昨天刚根据我们自己写的一篇 blog 部署了 PADS 的 RDP license bypass hook。AppInit_DLLs 是 Windows 早期的一个 DLL 注入机制:注册表里配了路径之后,任何加载 user32.dll 的进程在 DllMain 阶段都会被强制注入这个 DLL。下面是这个 hook 的工作流程:
flowchart LR
A[任意进程启动
加载 user32.dll] --> B[Windows Loader 读取
AppInit_DLLs 注册表]
B --> C[注入 C:/RdpBypass/
RdpBypass.dll]
C --> D[DllMain 用 Detours
Hook GetSystemMetrics]
扫一眼注册表立刻看到铁证:
HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows NT\CurrentVersion\Windows
AppInit_DLLs REG_SZ C:\RdpBypass\RdpBypass.dll
LoadAppInit_DLLs REG_DWORD 0x1
再把源码拉下来一看,最近一次 commit 是 fix: address codex review findings——这个 commit 把 Detours Hook 失败的处理从"默认返回 TRUE"改成了"return FALSE"。这在纯 PADS 场景下是正确的错误传播,但叠加 AppInit_DLLs 后变成一颗地雷:
BOOL WINAPI DllMain(HINSTANCE hinst, DWORD dwReason, LPVOID reserved) {
if (dwReason == DLL_PROCESS_ATTACH) {
DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
LONG status = DetourAttach(&(PVOID &)TrueGetSystemMetrics, FakeGetSystemMetrics);
if (status != NO_ERROR) {
DetourTransactionAbort();
return FALSE; // ← 这里
}
if (DetourTransactionCommit() != NO_ERROR) {
return FALSE; // ← 这里
}
}
return TRUE;
}
叙述起来非常漂亮:
Detours 在 Hook 的时候要做
VirtualProtect改写user32!GetSystemMetrics的内存页。在 Windows Server 2022 上 user32.dll 有 CFG (Control Flow Guard) 保护,普通用户 token 下这一步可能失败 →DetourAttach返回 error →DllMain返回 FALSE → loader 把整个进程标记成STATUS_DLL_INIT_FAILED(0xc0000142)。Administrator token 有足够特权绕过这些检查,所以 admin 不挂、普通用户挂。
现象、码、时间点、逻辑全部对上。我几乎就要收工了。
1.2 差点翻车:基于未证实假说的两次修复都失败了#
基于上面的假说我做了两轮越来越精细的错误干预,每一轮都被下一次测试打脸。
第一轮:清空 rds-user-b 的 Illustrator Prefs 字体缓存(IllustratorFnt.lst、aggressivePlugincache_v2.bin、AdobeFnt_OSFonts.lst 等),理论上让 AI 重新扫描字体来"绕过"损坏状态。结果——问题一模一样。
第二轮:更精细,用 Windows 的 PROCESS_MITIGATION_EXTENSION_POINT_DISABLE_POLICY(通过 IFEO —— Image File Execution Options,映像劫持注册表路径 —— 写 MitigationOptions = 0x100000000)给 16 个 Adobe 的 x86 helper 进程打上"禁用扩展点注入"的 mitigation。这个 mitigation 正好针对 AppInit_DLLs、Winsock LSP (Layered Service Provider)、AppCertDlls、IAT (Import Address Table) patching 这类老式注入的精准屏蔽。
# 示例:给 Illustrator 的 licensing helper 禁用扩展点注入
reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\adobe_licensing_helper.exe" `
/v MitigationOptions /t REG_QWORD /d 0x100000000 /f
我把它应用到了全部 Adobe 的 x86 helper(adobe_licensing_helper.exe、AdobeIPCBroker.exe、HDHelper.exe 等等),外加 Illustrator.exe 本身做防御性处理。技术上这个 mitigation 是 100% 对的,工程上这一套也够 surgical。
让用户再试——还是同一个错。
1.3 一句 “admin 为什么没事” 把假说彻底击穿#
僵局被用户一句问话打破:
如果 RdpBypass 会有影响,administrator 为什么可以打开?
这句话看起来简单,但它是一把匕首。AppInit_DLLs 是全局机制,它不分用户身份;只要 LoadAppInit_DLLs=1 并且 loader 校验通过,任何加载 user32.dll 的进程都会被注入——包括 admin 的进程。
我立刻验证:去 admin 当前运行的 Illustrator 进程里枚举已加载模块。
$procs = Get-Process -Name AdobeIPCBroker,CCXProcess,node,Illustrator
foreach ($p in $procs) {
$hasRdp = $p.Modules | Where-Object { $_.ModuleName -match 'RdpBypass' }
$loaded = if ($hasRdp) { "YES" } else { 'no' }
Write-Host "$($p.Name) RdpBypass loaded: $loaded"
}
结果是全部 no:
AdobeIPCBroker RdpBypass loaded: no
CCXProcess RdpBypass loaded: no
Illustrator RdpBypass loaded: no
node RdpBypass loaded: no
RdpBypass.dll 根本没有注入到 Adobe 的任何进程。AppInit_DLLs 从头到尾就不是因果链的一部分。RequireSignedAppInit_DLLs 在 Windows Server 2022 上被隐式强制,未签名的 RdpBypass.dll 在 loader 阶段就被静默跳过了——PADS 能工作可能是因为别的原因(甚至可能 hook 从未生效但 PADS 的另一条路径让它 work),但对 Adobe 根本不相关。
三个小时的调试里最重要的一刻:一句"如果假说是对的那为什么 X",把数据驱动的假设一刀切掉。
教训一:错误码(0xc0000142)告诉你"loader 说 DLL 初始化失败",但它没告诉你哪个 DLL 在哪个进程里。
教训二:当你的假说能解释所有已知现象时,主动寻找一个它不能解释的反例——而不是继续堆积能支持它的证据。
回滚了 IFEO(16 条都干净删除),清掉 Prefs 改动,系统回到初始状态。开始从零重新想。
2. 第二个错误方向:Adobe NGL License 状态过期#
2.1 线索:Adobe 的后端目录严重不完整#
结论先行:这一节会发现 Adobe 安装里缺失了大量后端服务和 license 目录,引出一个"per-user DPAPI 加密的 license 状态过期导致普通用户解密失败"的假说。
先解释一个关键概念:NGL (Next Generation Licensing) 是 Adobe 从 2019 年起用的新一代许可系统,它用 DPAPI (Windows Data Protection API) 把 license 令牌加密存到用户 profile 里。DPAPI 的 master key 是 per-user 的,理论上一个用户的 DPAPI 不能解另一个用户的数据。
重新扫一圈 Adobe 安装目录和 C:\ProgramData\Adobe\:
C:\ProgramData\Adobe\
Installer\ (只有这一个子目录)
C:\Users\Administrator\AppData\Local\Adobe\
Color\
licflags\
NGL\ (只有这 3 个)
对比正常 Adobe 安装应该有:
- AdobeGCClient
- CoreSync
- OOBE
- SLStore
- Sign-In
- 多个后台服务 (AGSService, AGMService, AdobeUpdateService, ...)
Get-Service 过滤 Adobe|AGS|AGM 返回空。application.xml 里看到:
<Data key="APPLICATION_CUSTOMIZATION_DATA">SuppressSerialWF</Data>
<Data key="LicensingCode">V7{}Illustrator-29-Win-GM</Data>
<Data key="TrialSerialNumber">XXXXXXXXXXXXXXXXXXXXXXXX</Data>
<Data key="InstallDate">1775636579</Data> <!-- 2026-04-06 06:22 UTC -->
V7 是 Adobe 2019 之前的老 serial licensing 格式,SuppressSerialWF + 预置的 trial serial number、再加上完全没有任何 Adobe 后台服务——这明显是一个非标准/VIP volume license 安装。合理的假设链变成:
flowchart TD
A[AI 2025 实际使用 NGL + 残留 V7 Trial 双通道] --> B[license 状态用 DPAPI 加密在
%LocalAppData%\Adobe\NGL\ASNPsv2]
B --> C[DPAPI master key 是 per-user 的]
C --> D[admin 的 license state 用 admin DPAPI 加密
只有 admin 能解]
D --> E[普通用户解密失败 → 尝试联网重新获取 → 无外网 → throw]
并且 W32Time 从 4/8 起就无法访问 time.windows.com,机器靠 CMOS 本地时钟运行。Adobe 的 license 期限对时钟非常敏感,这个机器今早还出过一次"时钟异常后修正"的事件。
2.2 读 NGL 日志:信号越来越强,但还是错的#
Admin 的 Illustrator 恰好还在跑(Get-Process Illustrator PID 19264,working set 960 MB),先拿它的 NGL 客户端日志看看 license 流程:
C:\Users\Administrator\AppData\Local\Temp\2\NGL\NGLClient_Illustrator129.8.2.log
关键几行:
15:06:07:179 ReadFileWithLockAndDecrypt: Finished with status: 0 ← 解密成功
15:06:07:185 DP API CNG functions success ← DPAPI 工作正常
15:06:07:188 ReadFileWithLockAndDecrypt: Finished with status: 0 ← 第二次解密
15:06:07:226 InitialProfileTimer: app is licensed ← 声明已授权
15:06:07:372 ...
15:06:07:372 Calling endpoint https://lcs-cops.adobe.io/... ← 开始联网
15:07:10:316 OnCallBack: Asynchronous error occured
15:07:10:320 MapLastErrorToStatusCode: Error is 12002 ← ERROR_INTERNET_TIMEOUT
15:07:10:403 GetUnauthenticatedProfile: Failed to retrieve ASNP
15:07:10:421 FetchAccessTokenFromDeviceToken: Empty device token given!!
叙述再次变得很顺:
Admin 在 41 毫秒内就凭本地缓存 + DPAPI 判定 “licensed”。后续所有网络调用全部 timeout,但那只是 telemetry 上报,不阻塞启动。DPAPI 是 per-user 的,普通用户无法解密 admin 那套缓存,也无法联网重建(12002),于是 license 检查失败 → throw → 崩溃。
这个理论解释了为什么 admin 能用、为什么今天才出问题(缓存过期)、甚至解释了为什么 W32Time 事件和崩溃时间相关。
2.3 NGL 日志不存在:证明崩溃发生在 NGL 初始化之前#
License 假说有一个必须成立的前提——如果崩溃发生在 NGL 的 license 校验代码里,至少应该有第一行 NGL 客户端日志被写入。但是——普通用户的 %TEMP%\NGL\ 目录下连一行 NGL 客户端日志都没有。
Get-ChildItem "C:\Users\rds-user-a\AppData\Local\Temp" -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match 'NGLClient' }
# (空)
NGL 客户端是 C++ 写的有自己的 logger,哪怕崩溃也会先写一行 SessionID=xxx Timestamp=...。日志为空 = 崩溃发生在 NGL 初始化之前。license 理论被自己的证据否定。
教训三:一个假说覆盖的"触发时机"必须和观测到的事实匹配。license 检查会留下日志,日志没留下,就说明崩溃在 license 检查之前。
3. 正确方向:用 WER Full Memory Dump 让崩溃自己说话#
本节目标:放弃所有基于猜想的修复,直接拿到崩溃时的完整进程内存,精确定位抛出异常的那一行代码。
两次错判之后我不再猜。机器上没有 Procmon、没有 WinDbg、没有外网。Windows 系统自带的最有价值的事后调试工具是 WER (Windows Error Reporting) 的 LocalDumps 机制——它可以在任何进程崩溃时自动写下一个完整内存转储文件,不需要预装任何第三方工具。
3.1 开启 WER LocalDumps Full Memory Dump#
# 让 Illustrator.exe 下次崩溃时写一个完整的内存转储
$wer = 'HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\Illustrator.exe'
New-Item -Path $wer -Force | Out-Null
New-ItemProperty -Path $wer -Name 'DumpFolder' -PropertyType ExpandString -Value 'C:\temp\dumps' -Force
New-ItemProperty -Path $wer -Name 'DumpType' -PropertyType DWord -Value 2 -Force # 2 = full memory
New-ItemProperty -Path $wer -Name 'DumpCount' -PropertyType DWord -Value 5 -Force
# WER 会在崩溃进程的用户上下文里写 dump,因此需要给目标用户写入权限。
# 注意:Full memory dump 里可能包含 token、凭据、业务敏感内存,
# 所以 ACL 必须收紧到**最小集合**——绝不要用 Everyone:(F)
New-Item -ItemType Directory -Path 'C:\temp\dumps' -Force | Out-Null
# 清空默认继承的宽权限
icacls 'C:\temp\dumps' /inheritance:r | Out-Null
# 只授予:SYSTEM + Administrators 完全控制(便于管理员读取并清理)
icacls 'C:\temp\dumps' /grant:r 'SYSTEM:(OI)(CI)F' 'Administrators:(OI)(CI)F' | Out-Null
# 仅授予将要触发崩溃的那个具体用户最小的"写入 + 新建子项 + 同步"权限
# 不授予 READ,避免同机其他低权限用户窃取 dump 内容
icacls 'C:\temp\dumps' /grant:r '<target-user>:(OI)(CI)(WD,AD,S)' | Out-Null
Restart-Service WerSvc
DumpType = 2 是完整内存转储(MiniDumpWithFullMemory),包含进程所有已提交内存。会比较大,但只有 full memory 才能让我们读到异常对象的数据和 ThrowInfo 结构。
让用户再点一次 AI 图标,拿到一个 460 MB 的 Illustrator.exe.35648.dmp。
3.2 把 dump 搬到 Linux 上分析#
目标机器上没有任何调试器,也没有外网。最简洁的办法就是把 dump 弄回来本地分析。scp 最初被某个 hook 截断到 200 KB,换成 sftp 直接一次 460 MB 到位:
# 推荐:基于密钥的认证 + 保留主机指纹校验
# 预先 ssh-copy-id 把公钥放到目标机 administrator 账号里,
# known_hosts 会在首次连接时记录对方指纹
sftp administrator@rds-host.internal <<'EOF'
lcd /md0/vibe/ai_dump
get /C:/temp/dumps/Illustrator.exe.35648.dmp
bye
EOF
不要在生产运维脚本里用 sshpass -p '...' sftp -o StrictHostKeyChecking=no:明文口令会落到 shell history、ps 列表、系统审计日志里,StrictHostKeyChecking=no 又会让你对 MITM 攻击完全失去防御。上面这种临时抓 dump 的场景也建议一次性生成密钥对:
ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_dump_debug
ssh-copy-id -i ~/.ssh/id_dump_debug.pub administrator@rds-host.internal
sftp -i ~/.ssh/id_dump_debug administrator@rds-host.internal <<'EOF'
...
用完把这把密钥从目标机 authorized_keys 里删除即可。
3.3 用 Python minidump 库解析#
Linux 上没有 WinDbg,但有个非常好用的 Python 库 minidump,纯 Python 实现的 Windows minidump 解析器,可以读取系统信息、模块列表、异常记录和原始内存段。
python3 -m venv .venv
.venv/bin/pip install minidump
先读基本信息:
from minidump.minidumpfile import MinidumpFile
mf = MinidumpFile.parse('Illustrator.exe.35648.dmp')
print(mf.exception)
print(f"Modules: {len(mf.modules.modules)}")
输出:
ThreadId | ExceptionCode | ExceptionAddress | ExceptionInformation
0x00003724 | ExceptionCode.EXCEPTION_UNKNOWN | 0x7ffa49eaf18c | [429065504, 327611837760, 140711765660576, 140711763378176]
Modules: 138
3.4 解读 MSVC C++ 异常的四个 ExceptionInformation#
本节目标:理解 MSVC C++ throw 在 Windows SEH (Structured Exception Handling,结构化异常处理) 里的二进制表示,进而从异常记录中定位抛出模块。
ExceptionCode 在高层库里显示成 UNKNOWN 因为它不是 Windows 内置的 SEH 码——数值其实是 0xe06d7363,后 3 字节 ASCII 反过来是 "msc",这是 MSVC C++ throw 的固定异常码(前缀 0xe0 表示应用层自定义异常)。
64 位 MSVC 编译器 throw 一个 C++ 对象时,RaiseException 的 4 个 ExceptionInformation 参数有固定含义:
| 下标 | 内容 | 值 | 解读 |
|---|---|---|---|
[0] | Magic number | 0x19930520 | MSVC C++ throw 魔数,版本号日期编码(1993-05-20) |
[1] | 抛出的对象指针 | 0x4c472fed40 | 指向堆上的异常对象 |
[2] | ThrowInfo* | 0x7ffa02ced3a0 | 异常类型信息结构 |
[3] | 模块 image base | 0x7ffa02ac0000 | ThrowInfo 内部 RVA 相对的基址(x64 专有) |
把 0x7ffa02ac0000 拿去匹配模块列表——完美命中:
0x00007ffa02ac0000 - 0x00007ffa02d30000 AITextViewModel.dll
抛异常的模块是 AITextViewModel.dll——Illustrator 的 Text View Model 组件,负责文本渲染和字体逻辑。不是 licensing、不是 DLL loader、不是 AppInit。
3.5 亲手走 MSVC ThrowInfo 结构#
本节目标:从 ThrowInfo* 指针出发,一层层解引用到异常对象的 C++ 类型名字。
MSVC 的 C++ 异常结构在公开头文件 <ehdata.h> 里有定义。关键结构体(RVA = Relative Virtual Address,相对于模块基址的偏移):
struct _ThrowInfo {
DWORD attributes;
int32_t pmfnUnwind; // RVA to destructor
int32_t pForwardCompat; // RVA
int32_t pCatchableTypeArray; // RVA → CatchableTypeArray
};
struct _CatchableTypeArray {
int32_t nCatchableTypes;
int32_t arrayOfCatchableTypes[]; // RVAs → _CatchableType
};
struct _CatchableType {
DWORD properties;
int32_t pType; // RVA → TypeDescriptor
// ... 还有 _PMD、sizeOrOffset、copyFunction
};
struct TypeDescriptor {
PVOID pVFTable;
PVOID spare;
char name[]; // mangled name, 例如 ".?AVfilesystem_error@filesystem@std@@"
};
写一段 Python 走这个结构:
import struct
from minidump.minidumpfile import MinidumpFile
mf = MinidumpFile.parse('Illustrator.exe.35648.dmp')
buf = mf.get_reader().get_buffered_reader()
IB = 0x7ffa02ac0000 # AITextViewModel.dll image base
TI_ADDR = 0x7ffa02ced3a0 # ThrowInfo
OBJ = 0x4c472fed40 # exception object
def rd(addr, n):
buf.move(addr)
return buf.read(n)
# ThrowInfo
attrs, unwind_rva, fwd_rva, cta_rva = struct.unpack('<Iiii', rd(TI_ADDR, 16))
cta_addr = IB + cta_rva
cnt = struct.unpack('<i', rd(cta_addr, 4))[0]
ct_rvas = struct.unpack(f'<{cnt}i', rd(cta_addr + 4, cnt * 4))
# 枚举所有 catchable type(继承链),最具体的在第一个
for i, ct_rva in enumerate(ct_rvas):
ct_addr = IB + ct_rva
props, ptype_rva = struct.unpack('<Ii', rd(ct_addr, 8))
td_addr = IB + ptype_rva
buf.move(td_addr + 16) # 跳过 pVFTable + spare
name = b''
while True:
c = buf.read(1)
if c == b'\x00' or not c: break
name += c
print(f" [{i}] {name.decode()}")
输出:
[0] .?AVfilesystem_error@filesystem@std@@
[1] .?AVsystem_error@std@@
[2] .?AV_System_error@std@@
[3] .?AVruntime_error@std@@
[4] .?AVexception@std@@
第一个就是最具体的派生类——std::filesystem::filesystem_error。继承链一路向上:filesystem_error → system_error → _System_error → runtime_error → exception。
3.6 从异常对象里抽 what() 字符串#
知道了类型,下一步读对象内存。前 8 字节是 vtable 指针:
obj = rd(OBJ, 256)
vptr = struct.unpack('<Q', obj[0:8])[0]
# 找 vptr 属于哪个模块 → AITextViewModel.dll +0x1c5bd0
filesystem_error 内部有一个 std::string _Msg(错误消息)。MSVC STL 的 std::string 用的是 SSO (Small String Optimization,短字符串优化) 布局:16 字节 inline buffer + 8 字节 size + 8 字节 capacity。如果 capacity > 15,前 8 字节从 inline 变成堆指针。
写一个扫描函数,从对象偏移 8 开始每 8 字节试探:
for off in range(8, 200, 8):
chunk = obj[off:off+32]
size = struct.unpack_from('<Q', chunk, 16)[0]
cap = struct.unpack_from('<Q', chunk, 24)[0]
if 16 <= cap < 65536 and 16 <= size < cap:
ptr = struct.unpack_from('<Q', chunk, 0)[0]
s = rd(ptr, size).decode('ascii', errors='replace')
print(f" +{off:#x} size={size} cap={cap}: '{s}'")
输出:
+0x68 heap str size=58 cap=63 ptr=0x1bcc09614f0:
'temp_directory_path: not a directory: "C:\Windows\TEMP\5\"'
就是这一行。
std::filesystem::filesystem_error:
what(): temp_directory_path: not a directory: "C:\Windows\TEMP\5\"
这是 MSVC 实现的 std::filesystem::temp_directory_path() 在发现返回的临时目录路径 status() 检查不通过时抛出的错误。对应的 MSVC STL 源码片段大致是:
path temp_directory_path() {
path result;
_Temp_directory_path(result._Text);
const file_status st = status(result);
if (!exists(st) || !is_directory(st)) {
_Throw_fs_error("temp_directory_path",
make_error_code(errc::not_a_directory),
result);
}
return result;
}
它会先调 GetTempPath2W/GetTempPathW 拿到路径(按 TMP → TEMP → USERPROFILE → Windows 目录顺序),然后对这个路径调 status()。如果 status() 因为访问被拒而返回 file_type::unknown,is_directory 就会是 false,抛出这个 filesystem_error。
异常的路径清清楚楚是 C:\Windows\TEMP\5\。从 Illustrator.exe 主线程早期 AITextViewModel.dll 静态初始化期间抛出,先于任何 Adobe license/NGL 代码。对应的那个 0xc0000142 弹窗是副作用——主进程异常后,某个被拉起的 x86 helper 也顺势报了自己的 DLL init 失败。
教训四:当错误码(0xc0000142)和实际事件日志(
0xe06d7363)不一致时,信事件日志。用户看到的对话框可能是第二层效应。
4. 根因:Per-Session TEMP + Session ID 复用 + ACL 错配#
4.1 Windows Server RDS 的 per-session TEMP 机制#
Windows Server 的 RDS 主机有一个默认策略:
Computer Configuration → Administrative Templates → Windows Components → Remote Desktop Services → Remote Desktop Session Host → Temporary folders → “Do not use temporary folders per session”
对应注册表:
HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server
PerSessionTempDir REG_DWORD 0x1
当这个值为 1(默认)时,每个 RDP 会话登录时,Windows 会把 TEMP 和 TMP 重定向到:
%SystemRoot%\TEMP\<session_id>\
即 C:\Windows\Temp\3\、C:\Windows\Temp\4\、C:\Windows\Temp\5\ 这样的按 session ID 子目录。正常情况下:
- 用户登录时 Windows 创建此目录并把 owner 设为该用户
- 用户注销时 Windows 清理(如果
DeleteTempDirsOnExit=1) - 下次登录如果拿到同一个 session ID,会重新创建
4.2 问题场景:session ID 跨启动复用#
Session ID 在同一次系统启动周期内通常是递增分配、不复用的。但是跨启动周期 session ID 会从低位(通常是 2 或 3 开始,0/1 是 console 和 services)重新分配——也就是 session ID 是会被复用的。
这台机器最近几天的实际情况:
- 4/9 早上 重启过,一组用户以 session ID 3/4/5 登录
- 期间 Windows 创建了
C:\Windows\TEMP\{3,4,5},每个目录的 owner 分别是当时登录的用户 - 某个时间点(注销 +
DeleteTempDirsOnExit执行不干净,或者意外断连),目录没有被清理 - 4/10 又重启过一次
- 今天 14:00 左右,一组新用户重新登录,session ID 又从低位分配
结果是:
| 时间 | Session 5 的用户 | C:\Windows\TEMP\5 owner |
|---|---|---|
| 上次启动周期 | rds-user-b | rds-user-b (✓) |
| 今天 | rds-user-a | 仍然是 rds-user-b (✗) |
我抓的 ACL 直接坐实了这一点:
C:\Windows\TEMP\5
Owner: RDSHOST01\rds-user-b
NT AUTHORITY\SYSTEM Allow FullControl
BUILTIN\Administrators Allow FullControl
RDSHOST01\rds-user-b Allow FullControl ← 错配
BUILTIN\Users Allow CreateFiles, AppendData, ExecuteFile, Synchronize
CREATOR OWNER Allow (inherit)
C:\Windows\TEMP\4
Owner: RDSHOST01\rds-user-c ← 当前 session 4 是 rds-user-b
RDSHOST01\rds-user-c Allow FullControl
三条里两条错配。C:\Windows\TEMP\5 的 ACL 里只有 rds-user-b 有 FullControl,而今天 session 5 的用户是 rds-user-a。rds-user-a 对这个目录只继承了 BUILTIN\Users 的 CreateFiles + ExecuteFile,没有 ReadData/ListDirectory/ReadAttributes。
4.3 为什么 status() 失败会被翻译成 “not a directory”#
本节目标:把 Windows 的权限拒绝错误串联到 MSVC STL 的 filesystem_error 消息,解释为什么错误文本让人误以为目录不存在。
MSVC STL 的 std::filesystem::status() 最终走到 Win32 GetFileAttributesW(),这个 API 需要对目标路径有 FILE_READ_ATTRIBUTES 权限。对一个 ACL 只给了 CreateFiles + ExecuteFile 的目录调 GetFileAttributesW 时会得到:
ERROR_ACCESS_DENIED (5)
MSVC STL 对 ERROR_ACCESS_DENIED 的翻译路径最终落到 errc::not_a_directory——于是错误消息里看到的是 “not a directory”,看起来像目录不存在,但实际是目录存在但无权限 stat。
拼起来完整因果:
flowchart TD
A[rds-user-a 今天登录
分到 session ID 5] --> B[Windows 发现
C:\Windows\TEMP\5 已存在
不重建]
B --> C[rds-user-a 的进程环境变量
TEMP = C:\Windows\TEMP\5\
TMP = C:\Windows\TEMP\5\]
C --> D[双击 Illustrator]
D --> E[AITextViewModel.dll 静态初始化
调用 std::filesystem::temp_directory_path]
E --> F[GetTempPath → C:\Windows\TEMP\5\]
F --> G[status 调用 GetFileAttributesW
ACL 拒绝 FILE_READ_ATTRIBUTES]
G --> H[status 返回 unknown
is_directory false]
H --> I[throw filesystem_error
not a directory]
I --> J[未捕获 → RaiseException 0xe06d7363
→ 进程崩溃]
4.4 为什么 Administrator 没事#
Administrator 账号在 RDS 上经常有以下任一特性(视组策略 GPO (Group Policy Object) 和账号类型而定):
- Administrator 的 profile 里
HKCU\Environment通常已经显式设置了TEMP=%USERPROFILE%\AppData\Local\Temp,覆盖了 per-session 重定向 - 即便走 per-session,admin 因为属于
BUILTIN\Administrators,任何C:\Windows\TEMP\<sid>\他都有 FullControl(ACL 里永远有这条) - Administrator 的 session ID 是 2,而
C:\Windows\TEMP\2要么不存在(fall back 到 user temp),要么本来就属于 admin
我抓到的 admin HKU\S-1-5-21-...-500\Environment 确实显式设置了 TEMP = %USERPROFILE%\AppData\Local\Temp,所以 admin 根本不进 per-session TEMP 这条路径。
而所有普通用户的 HKU\<SID>\Environment 里 TEMP/TMP 都是空的,于是他们完全依赖系统默认——也就是 per-session 重定向。
5. 永久修复:关闭 per-session + 绑定到用户 profile#
本节目标:给出一个可重复执行、幂等、非破坏性的永久修复方案,包含过渡措施让正在运行的会话不必注销就能恢复。
目标是永久修复,不是只补一个会话。永久修复由四个互相配合的层面组成,缺一不可:
| 层面 | 做什么 | 副作用 |
|---|---|---|
| 系统策略 | PerSessionTempDir = 0 | 未来新会话不再走 C:\Windows\TEMP\<sid>,但要求用户 env 有 TEMP |
| 用户 profile | 给每个普通用户的 HKU\<SID>\Environment 写 TEMP = %USERPROFILE%\AppData\Local\Temp | 彻底和系统 TEMP 解耦 |
| Default 模板 | reg load 加载 C:\Users\Default\NTUSER.DAT 后写 Environment | 未来新建的用户默认就是对的(注意:不是 HKU\.DEFAULT) |
| 当前错配 ACL | takeown + icacls 把 C:\Windows\TEMP\{3,4,5} 的 FullControl 重新授予当前 session 用户 | 让还没注销的现有会话不注销就能用 |
5.1 一键修复脚本(PowerShell)#
# [1] 永久关闭 per-session TEMP 重定向
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' `
-Name PerSessionTempDir -Value 0
# [2] 给每个目标用户的 HKU Environment 写 TEMP/TMP
# 注意:必须用 ExpandString 类型,保留 %USERPROFILE% 这个 token,
# 这样每个用户登录时会用各自的 USERPROFILE 展开
$targetUsers = @(
@{Name='rds-user-a'; Sid='S-1-5-21-XXXX-XXXX-XXXX-1003'},
@{Name='rds-user-b'; Sid='S-1-5-21-XXXX-XXXX-XXXX-1009'},
@{Name='rds-user-c';Sid='S-1-5-21-XXXX-XXXX-XXXX-1004'}
)
foreach ($u in $targetUsers) {
$envKey = "Registry::HKEY_USERS\$($u.Sid)\Environment"
if (Test-Path $envKey) {
Set-ItemProperty -Path $envKey -Name TEMP -Value '%USERPROFILE%\AppData\Local\Temp' -Type ExpandString
Set-ItemProperty -Path $envKey -Name TMP -Value '%USERPROFILE%\AppData\Local\Temp' -Type ExpandString
}
}
# [3] 修 Default 用户模板(未来新建用户会以这个模板创建 NTUSER.DAT)
# 重要:真正的模板是 C:\Users\Default\NTUSER.DAT 这个离线 hive,
# 不是 HKU\.DEFAULT(那是 LocalSystem 账号的活跃 hive,与新用户无关)
try {
reg load 'HKU\_DefaultTpl' 'C:\Users\Default\NTUSER.DAT' | Out-Null
Set-ItemProperty -Path 'Registry::HKEY_USERS\_DefaultTpl\Environment' -Name TEMP `
-Value '%USERPROFILE%\AppData\Local\Temp' -Type ExpandString
Set-ItemProperty -Path 'Registry::HKEY_USERS\_DefaultTpl\Environment' -Name TMP `
-Value '%USERPROFILE%\AppData\Local\Temp' -Type ExpandString
} finally {
[gc]::Collect() # 释放 PowerShell 对 hive 的隐式引用
reg unload 'HKU\_DefaultTpl' | Out-Null
}
# [4] 修正当前错配的 stale C:\Windows\TEMP\<sid> ACL
# 这样"现在正登录"的用户不用注销就能立即开 AI
$sessionMap = @{
'3' = 'rds-user-c'
'4' = 'rds-user-b'
'5' = 'rds-user-a'
}
foreach ($sid in $sessionMap.Keys) {
$dir = "C:\Windows\TEMP\$sid"
$owner = $sessionMap[$sid]
if (Test-Path $dir) {
takeown /f $dir /r /d Y | Out-Null
icacls $dir /grant "${owner}:(OI)(CI)F" | Out-Null
}
}
5.2 两个关键坑#
坑 1:Set-ItemProperty -Type ExpandString 会让你怀疑人生
写完之后用 Get-ItemProperty 读回来验证:
Get-ItemProperty "Registry::HKEY_USERS\$sid\Environment" | Select TEMP, TMP
看到的是:
TEMP : C:\Users\Administrator\AppData\Local\Temp
TMP : C:\Users\Administrator\AppData\Local\Temp
吓一跳——为什么所有人的 TEMP 都是 Administrator 的路径?几秒钟后才反应过来:Get-ItemProperty 对 REG_EXPAND_SZ 值会自动用当前进程的环境展开。我是以 Administrator 在运行脚本,所以 %USERPROFILE% 展开成了 Administrator 的路径。存储在注册表里的实际值是 token 原文,只有读的时候被 PowerShell “帮忙"展开了。
验证方式:用 reg query 而不是 Get-ItemProperty:
reg query "HKU\$sid\Environment" /v TEMP
这个不会展开,能看到真正的存储:
HKEY_USERS\S-1-5-21-...-1003\Environment
TEMP REG_EXPAND_SZ %USERPROFILE%\AppData\Local\Temp
教训:验证 REG_EXPAND_SZ 时永远用 reg query 读真值。
坑 2:HKU\.DEFAULT 不是"默认用户模板”
这是一个让很多 Windows 管理员多年摸不清的陷阱。HKU\.DEFAULT 不是新创建用户的 registry 模板,它是 LocalSystem 账号(SID S-1-5-18)的活跃 hive,主要在登录屏幕、系统服务等没有交互用户的场景下被使用。你往 HKU\.DEFAULT 里写 TEMP,影响的是 LocalSystem,完全不会被下一次新建的普通用户继承。
真正的"Default 用户模板"是离线 hive 文件:C:\Users\Default\NTUSER.DAT。新用户首次登录时 Windows 会把这个文件复制到 C:\Users\<newuser>\NTUSER.DAT 作为该用户的 HKCU。要修改模板就必须用 reg load 把这个离线 hive 临时挂到 HKU 下,改完立刻 reg unload —— 而且为了避免 hive handle 被 PowerShell 隐式持有,unload 前要先 [gc]::Collect(),否则 reg unload 会报 “被其他进程使用”。
坑 3:现有会话的环境变量不会热更新
HKU\<SID>\Environment 只在用户登录时(userinit.exe / LsaLogonUser)被读取,写入到用户进程的初始环境块。一旦登录完成,已经跑起来的 shell(explorer.exe)拿到的就是那时候的 TEMP。它之后派生的所有子进程继承的也是那个 TEMP。不注销重连就不会刷新。
所以步骤 [4] 的 ACL 修正是必须的过渡方案——让还没重连的用户可以立即用,同时不强制他们必须注销。
5.3 验证#
[1] PerSessionTempDir 0 ← PASS
[2] rds-user-a HKU\<SID>\Environment\TEMP (raw) %USERPROFILE%\AppData\Local\Temp ← PASS
[2] rds-user-b HKU\<SID>\Environment\TEMP (raw) %USERPROFILE%\AppData\Local\Temp ← PASS
[2] rds-user-c HKU\<SID>\Environment\TEMP (raw) %USERPROFILE%\AppData\Local\Temp ← PASS
[3] Default template (offline NTUSER.DAT) TEMP (raw) %USERPROFILE%\AppData\Local\Temp ← PASS
[4] C:\Windows\TEMP\3 FullControl for rds-user-c PASS
[4] C:\Windows\TEMP\4 FullControl for rds-user-b PASS
[4] C:\Windows\TEMP\5 FullControl for rds-user-a PASS
关于 [3] 的验证方式:因为 C:\Users\Default\NTUSER.DAT 是离线 hive(没被任何 session 挂载),reg query 没法直接查它。正确的校验流程是临时挂载 → 查 → 立即卸载:
reg load 'HKU\_Check' 'C:\Users\Default\NTUSER.DAT' | Out-Null
reg query 'HKU\_Check\Environment' /v TEMP
reg query 'HKU\_Check\Environment' /v TMP
[gc]::Collect()
reg unload 'HKU\_Check' | Out-Null
期望看到两条 REG_EXPAND_SZ %USERPROFILE%\AppData\Local\Temp。不要去看 HKU\.DEFAULT 下的值——那是 LocalSystem 的 hive,永远和 Default 用户模板无关。
用户自己选择了注销重连,拿到新的 session ID(6 和 7)。新 session 不会再落到 C:\Windows\TEMP\<sid>(因为 PerSessionTempDir=0 已经生效),而是直接用各自的 %USERPROFILE%\AppData\Local\Temp。Illustrator 正常打开。
6. 教训集#
6.1 调试的元教训#
- 错误码不等于根因。
0xc0000142告诉你"loader 汇报 DLL init 失败",但不告诉你哪个 DLL、在哪个进程、因为什么。同一个外显错误可以来自完全不同的根因。 - 一个好假说要能被证伪。“RdpBypass 是元凶"这个假说可以解释观察到的所有现象,但它有一个明确的反驳实验:检查 admin 的 Adobe 进程里是否也加载了
RdpBypass.dll。主动去找反驳实验,比寻找更多支持证据更能前进。 - 信最早的那行日志,不信对话框。对话框是二次效应;第一个触发的异常、第一行崩溃日志、第一个错误返回才是根因方向。
- 没有调试器的时候,还有 Full Memory Dump。开 WER LocalDumps 只需要 5 行注册表,
DumpType=2给你全部内存。连 Procmon 都装不了的机器也能拿 dump,然后带到任何有 Python 的机器上做解析。 - 亲手解析二进制结构比等工具更快。MSVC C++ 异常的结构是公开的,Python
minidump库加 200 行脚本就能走完ThrowInfo → CatchableTypeArray → TypeDescriptor,拿到类型名,再从异常对象里抽出what()字符串。整个过程比到处找 WinDbg 安装包还快。 - Get-ItemProperty 会帮倒忙。PowerShell 对 REG_EXPAND_SZ 的自动展开是个甜蜜的陷阱——它让你相信写进去的值和读出来的值一样,结果你差点以为写坏了。关键验证永远用
reg query。
6.2 Windows RDS 的具体教训#
PerSessionTempDir在 RDS host 上默认启用,但它假设每次启动 session ID 和用户是同一个映射。现实不是这样。跨启动 session ID 复用 +DeleteTempDirsOnExit执行不干净 = 一颗慢性定时炸弹。- 对 RDS 多用户主机,直接把
HKU\<SID>\Environment\TEMP显式设成%USERPROFILE%\AppData\Local\Temp是最稳的,Default profile 同理。这样任何用户、任何 session ID 都和系统 TEMP 彻底解耦。 - 需要注意:关闭
PerSessionTempDir后,如果某个用户的HKU\Environment没有 TEMP,系统会 fall back 到HKLM里的默认%SystemRoot%\TEMP——也就是C:\Windows\TEMP,普通用户对它只有CreateFiles + ExecuteFile——又会踩同一个坑。所以永远是"关掉 per-session + 给每个用户显式 TEMP"这两件事一起做。
6.3 给 Adobe Illustrator 的一点评价#
AITextViewModel.dll 在静态初始化期间就调用 std::filesystem::temp_directory_path() 而不 catch 异常——这对一个桌面 GUI 应用来说是相对脆弱的模式。如果 temp 目录访问失败(权限、只读文件系统、路径不存在等),用户看到的就是一个没有任何上下文的 WER 对话框。在 RDS 多用户环境下这种提前失败尤其不友好,因为用户完全没有线索知道是 TEMP 坏了。
如果你在做类似 Windows 桌面应用,在 early init 阶段对 std::filesystem::temp_directory_path() 加 try/catch,并且给出一个明确的"temp 目录不可访问:
7. 附录:ThrowInfo 解析脚本完整版#
把上面那段 Python 脚本拼成一个可以直接运行的工具,对任何 MSVC C++ 异常 dump 都适用:
#!/usr/bin/env python3
"""
Parse MSVC C++ throw info from a Windows minidump with full memory.
Usage: python3 parse_cpp_throw.py <dump.dmp>
"""
import sys
import struct
from minidump.minidumpfile import MinidumpFile
def find_mod(mf, addr):
for m in mf.modules.modules:
if m.baseaddress <= addr < m.baseaddress + m.size:
return m
return None
def parse(dump_path):
mf = MinidumpFile.parse(dump_path)
buf = mf.get_reader().get_buffered_reader()
exc = mf.exception.exception_records[0].exception_record
code = exc.ExceptionCode
params = exc.ExceptionInformation
if code != 0xe06d7363:
print(f"Exception is not MSVC C++ throw: 0x{code:x}")
return
magic, obj_ptr, ti_ptr, image_base = params[0], params[1], params[2], params[3]
print(f"Magic: 0x{magic:x} (expected 0x19930520)")
print(f"Object: 0x{obj_ptr:x}")
print(f"ThrowInfo: 0x{ti_ptr:x}")
print(f"ImageBase: 0x{image_base:x}")
mod = find_mod(mf, image_base)
if mod:
print(f"Throwing module: {mod.name}")
def rd(addr, n):
buf.move(addr)
return buf.read(n)
# ThrowInfo
attrs, unwind_rva, fwd_rva, cta_rva = struct.unpack('<Iiii', rd(ti_ptr, 16))
cta_addr = image_base + cta_rva
cnt = struct.unpack('<i', rd(cta_addr, 4))[0]
ct_rvas = struct.unpack(f'<{cnt}i', rd(cta_addr + 4, cnt * 4))
print(f"\nCatchable types ({cnt}):")
for i, ct_rva in enumerate(ct_rvas):
ct_addr = image_base + ct_rva
props, ptype_rva = struct.unpack('<Ii', rd(ct_addr, 8))
td_addr = image_base + ptype_rva
buf.move(td_addr + 16)
name = b''
while True:
c = buf.read(1)
if c == b'\x00' or not c:
break
name += c
print(f" [{i}] {name.decode('ascii', errors='replace')}")
# Exception object: scan for embedded std::string
print("\nScanning exception object for std::string fields:")
obj = rd(obj_ptr, 256)
for off in range(8, 200, 8):
try:
size = struct.unpack_from('<Q', obj, off + 16)[0]
cap = struct.unpack_from('<Q', obj, off + 24)[0]
if 16 <= cap < 65536 and 16 <= size < cap:
ptr = struct.unpack_from('<Q', obj, off)[0]
s = rd(ptr, size).decode('ascii', errors='replace')
if all(32 <= b < 127 for b in s.encode()[:min(size, 50)]):
print(f" +0x{off:02x} size={size}: {s!r}")
except Exception:
pass
if __name__ == '__main__':
parse(sys.argv[1])
对我们这个 dump 跑一次:
$ python3 parse_cpp_throw.py Illustrator.exe.35648.dmp
Magic: 0x19930520 (expected 0x19930520)
Object: 0x4c472fed40
ThrowInfo: 0x7ffa02ced3a0
ImageBase: 0x7ffa02ac0000
Throwing module: C:\Program Files\Adobe\Adobe Illustrator 2025\Support Files\Contents\Windows\AITextViewModel.dll
Catchable types (5):
[0] .?AVfilesystem_error@filesystem@std@@
[1] .?AVsystem_error@std@@
[2] .?AV_System_error@std@@
[3] .?AVruntime_error@std@@
[4] .?AVexception@std@@
Scanning exception object for std::string fields:
+0x68 size=58: 'temp_directory_path: not a directory: "C:\\Windows\\TEMP\\5\\"'
两行输出,三小时调试。值得。
8. 参考资料#
- Windows Error Reporting - Collecting User-Mode Dumps
- Process Mitigation Policy - Extension Points Disable
- Remote Desktop Services - Use temporary folders per session
- MSVC C++ Exception Implementation - ehdata.h (公开头文件)
minidumpPython library - skelsec/minidumpstd::filesystem::temp_directory_path— cppreference- 内部参考:在 RDP 远程桌面中运行 PADS:从 API Hook 到 AppInit_DLLs 的工程实践
HKU\<SID>\Environment\TEMP 和 C:\Windows\TEMP\<sid> 的 ACL 拉出来看一眼——五分钟的事,比三小时调试强多了。