跳过正文
  1. 博客文章/

Windows RDS 下 Illustrator 2025 普通用户 0xc0000142 崩溃修复

·3255 字·16 分钟·
Windows 调试 Windows Server RDS Adobe Illustrator 0xc0000142 PerSessionTempDir Std::filesystem Minidump WER Debugging MSVC C++ Exception
Zayn
作者
Zayn
专注 Kubernetes、CI/CD、可观测性等云原生技术栈,记录生产环境中的实战经验与踩坑复盘。
目录
现象简单:Administrator 能开 Illustrator,普通用户一律 0xc0000142。我花了三小时走完两个错误方向、抓了一个 460 MB Full Memory Dump、亲手解析 MSVC C++ 异常结构,最后在 std::filesystem::filesystem_errorwhat() 字符串里找到了答案。这篇复盘把每一个弯路都诚实地留下来——错误路径往往比正确路径更有教训。

太长不看版 — 如果你在搜索错误码找到这里
#

本节给结论和最小修复;想看调试过程的直接跳到 §0 事发现场

症状指纹(三条全中才是本文这个问题):

  1. 宿主是 Windows Server RDS(Remote Desktop Session Host,多用户通过远程桌面共享同一台主机)
  2. Administrator 能正常打开目标软件,BUILTIN\Users 组里的普通用户一律弹 0xc0000142
  3. Windows 事件日志Application ErrorException Code0xe06d7363(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-ards-user-brds-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.lstaggressivePlugincache_v2.binAdobeFnt_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.exeAdobeIPCBroker.exeHDHelper.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 number0x19930520MSVC C++ throw 魔数,版本号日期编码(1993-05-20)
[1]抛出的对象指针0x4c472fed40指向堆上的异常对象
[2]ThrowInfo*0x7ffa02ced3a0异常类型信息结构
[3]模块 image base0x7ffa02ac0000ThrowInfo 内部 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 拿到路径(按 TMPTEMPUSERPROFILE → Windows 目录顺序),然后对这个路径调 status()。如果 status() 因为访问被拒而返回 file_type::unknownis_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 会把 TEMPTMP 重定向到:

%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-brds-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\UsersCreateFiles + 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>\EnvironmentTEMP/TMP 都是空的,于是他们完全依赖系统默认——也就是 per-session 重定向。


5. 永久修复:关闭 per-session + 绑定到用户 profile
#

本节目标:给出一个可重复执行、幂等、非破坏性的永久修复方案,包含过渡措施让正在运行的会话不必注销就能恢复。

目标是永久修复,不是只补一个会话。永久修复由四个互相配合的层面组成,缺一不可:

层面做什么副作用
系统策略PerSessionTempDir = 0未来新会话不再走 C:\Windows\TEMP\<sid>,但要求用户 env 有 TEMP
用户 profile给每个普通用户的 HKU\<SID>\EnvironmentTEMP = %USERPROFILE%\AppData\Local\Temp彻底和系统 TEMP 解耦
Default 模板reg load 加载 C:\Users\Default\NTUSER.DAT 后写 Environment未来新建的用户默认就是对的(注意:不是 HKU\.DEFAULT
当前错配 ACLtakeown + icaclsC:\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 调试的元教训
#

  1. 错误码不等于根因0xc0000142 告诉你"loader 汇报 DLL init 失败",但不告诉你哪个 DLL、在哪个进程、因为什么。同一个外显错误可以来自完全不同的根因
  2. 一个好假说要能被证伪。“RdpBypass 是元凶"这个假说可以解释观察到的所有现象,但它有一个明确的反驳实验:检查 admin 的 Adobe 进程里是否也加载了 RdpBypass.dll主动去找反驳实验,比寻找更多支持证据更能前进
  3. 信最早的那行日志,不信对话框。对话框是二次效应;第一个触发的异常、第一行崩溃日志、第一个错误返回才是根因方向。
  4. 没有调试器的时候,还有 Full Memory Dump。开 WER LocalDumps 只需要 5 行注册表,DumpType=2 给你全部内存。连 Procmon 都装不了的机器也能拿 dump,然后带到任何有 Python 的机器上做解析。
  5. 亲手解析二进制结构比等工具更快。MSVC C++ 异常的结构是公开的,Python minidump 库加 200 行脚本就能走完 ThrowInfo → CatchableTypeArray → TypeDescriptor,拿到类型名,再从异常对象里抽出 what() 字符串。整个过程比到处找 WinDbg 安装包还快。
  6. Get-ItemProperty 会帮倒忙。PowerShell 对 REG_EXPAND_SZ 的自动展开是个甜蜜的陷阱——它让你相信写进去的值和读出来的值一样,结果你差点以为写坏了。关键验证永远用 reg query

6.2 Windows RDS 的具体教训
#

  1. PerSessionTempDir 在 RDS host 上默认启用,但它假设每次启动 session ID 和用户是同一个映射。现实不是这样。跨启动 session ID 复用 + DeleteTempDirsOnExit 执行不干净 = 一颗慢性定时炸弹。
  2. 对 RDS 多用户主机,直接把 HKU\<SID>\Environment\TEMP 显式设成 %USERPROFILE%\AppData\Local\Temp 是最稳的,Default profile 同理。这样任何用户、任何 session ID 都和系统 TEMP 彻底解耦。
  3. 需要注意:关闭 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 目录不可访问:“错误对话框,会帮 IT 管理员省下好几个小时的调试时间。


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. 参考资料
#


后记。这篇文章如果能节省一个运维一小时,那就是它存在的意义。下次再看到"只有 admin 能开的 Windows 应用",请先把 HKU\<SID>\Environment\TEMPC:\Windows\TEMP\<sid> 的 ACL 拉出来看一眼——五分钟的事,比三小时调试强多了。

相关文章

把一张 RX 6400 塞进虚拟机:PVE GPU 直通 + Windows Server RDS 部署全记录
·1337 字·7 分钟
虚拟化 PVE Proxmox GPU Passthrough Windows Server RDS AMD RX 6400 VFIO IOMMU
在 RDP 远程桌面中运行 PADS:从 API Hook 到 AppInit_DLLs 的工程实践
·1165 字·6 分钟
逆向工程 Windows Windows Api-Hook Detours Pads Rdp Github-Actions Ci-Cd
每日技术实践简报 - 2026-04-07
·111 字·1 分钟
每日实践 OpenClaw Cron 博客迁移 每周总结