MinHook 对.NET底层的 SendMessage 拦截真实案例反思

一:背景

1. 讲故事

上一篇我们说到了 minhook 的一个简单使用,这一篇给大家分享一个 minhook 在 dump 分析中的实战,先看下面的线程栈。


0:044> ~~[138c]s
win32u!NtUserMessageCall+0x14:
00007ffc`5c891184 c3              ret
0:061> k# Child-SP          RetAddr               Call Site
00 0000008c`00ffec68 00007ffc`5f21bfbe     win32u!NtUserMessageCall+0x14
01 0000008c`00ffec70 00007ffc`5f21be38     user32!SendMessageWorker+0x11e
02 0000008c`00ffed10 00007ffc`124fd4af     user32!SendMessageW+0xf8
03 0000008c`00ffed70 00007ffc`125e943b     cogxImagingDevice!DllUnregisterServer+0x3029f
04 0000008c`00ffeda0 00007ffc`125e9685     cogxImagingDevice!DllUnregisterServer+0x11c22b
05 0000008c`00ffede0 00007ffc`600b50e7     cogxImagingDevice!DllUnregisterServer+0x11c475
06 0000008c`00ffee20 00007ffc`60093ccd     ntdll!LdrpCallInitRoutine+0x6f
07 0000008c`00ffee90 00007ffc`60092eef     ntdll!LdrpProcessDetachNode+0xf5
08 0000008c`00ffef60 00007ffc`600ae319     ntdll!LdrpUnloadNode+0x3f
09 0000008c`00ffefb0 00007ffc`600ae293     ntdll!LdrpDecrementModuleLoadCountEx+0x71
0a 0000008c`00ffefe0 00007ffc`5cd7c00e     ntdll!LdrUnloadDll+0x93
0b 0000008c`00fff010 00007ffc`5d47cf78     KERNELBASE!FreeLibrary+0x1e
0c 0000008c`00fff040 00007ffc`5d447aa3     combase!CClassCache::CDllPathEntry::CFinishObject::Finish+0x28 [onecore\com\combase\objact\dllcache.cxx @ 3420] 
0d 0000008c`00fff070 00007ffc`5d4471a9     combase!CClassCache::CFinishComposite::Finish+0x4b [onecore\com\combase\objact\dllcache.cxx @ 3530] 
0e 0000008c`00fff0a0 00007ffc`5d3f1499     combase!CClassCache::FreeUnused+0xdd [onecore\com\combase\objact\dllcache.cxx @ 6547] 
0f 0000008c`00fff650 00007ffc`5d3f13c7     combase!CoFreeUnusedLibrariesEx+0x89 [onecore\com\combase\objact\dllapi.cxx @ 117] 
10 (Inline Function) --------`--------     combase!CoFreeUnusedLibraries+0xa [onecore\com\combase\objact\dllapi.cxx @ 74] 
11 0000008c`00fff690 00007ffc`6008a019     combase!CDllHost::MTADllUnloadCallback+0x17 [onecore\com\combase\objact\dllhost.cxx @ 929] 
12 0000008c`00fff6c0 00007ffc`6008bec4     ntdll!TppTimerpExecuteCallback+0xa9
13 0000008c`00fff710 00007ffc`5f167e94     ntdll!TppWorkerThread+0x644
14 0000008c`00fffa00 00007ffc`600d7ad1     kernel32!BaseThreadInitThunk+0x14

这是一个 .NET某工控自动化控制系统(https://www.cnblogs.com/huangxincheng/p/16544462.html) 的卡死故障,经过一顿分析之后,找到了最后的卡死原因,即 cogxImagingDevice.dll 中有一个 DllMain 的卸载通知,熟悉 win32 的朋友都知道,代码经过 DllMain 的时候会持有一个 LdrpAcquireLoaderLock 进程加载锁,在持锁过程中它突然向一个窗体发送 SendMessageW 消息,可惜的是这个窗体没有给予响应,一直卡死在这里,这就导致 进程加载锁 迟迟得不到释放,引发系统性卡死。。。

如果有朋友还是比较懵的话,我画一张图给大家看看,黑色加粗就是问题的核心所在。

二:寻找解决方案

1. 现有困境

我可以通过 windbg 提取到 SendMessageW 方法的 窗口句柄 hWnd,通过这个 hWnd 找到创建它的 processID 和 ThreadID,但问题是这两个关键信息 是存放在当前机器的内核态中,言外之意就是用户态dump没有这两个信息,所以关键信息的缺失导致无法有效的排查出问题。

解决办法有两个:

  • 抓内核态dump:由于 win32u 模块是闭源的,要想从内核态dump中找出还得不断的参考 reactos,费时费力。
  • SendMessageW跟踪:这个相对来说轻量级,也是本篇重点说的,即 minhook。

2. 如何跟踪 SendMessageW

我的想法是这样的,对 SendMessageW 进行拦截来获取 hWnd 参数,然后通过 hWnd 参数找到对应的 processid 和 threadid,然后再通过 processid 获取 processname,有了这三个信息就可以让对方无所遁形。

为了让大家眼见为实,我们做一个例子,新建一个 WindowsProject1 的Win32窗体,在网关函数 WndProc 中故意让程序卡死,参考代码如下:


LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{if (message == WM_CLOSE) {Sleep(1000 * 1000);}// todo....return 0;
}

接下来新建一个 ConsoleApplication 控制台程序,通过 SendMessageWindowsProject1 打close消息,来演示无故卡死,完整的代码如下:


using System;
using System.Runtime.InteropServices;
using System.Text;namespace ConsoleApplication
{public static class Program{private const uint WM_CLOSE = 0x0010;public static void Main(){// 安装 HookHookManager.InstallHook();// 测试:发送 WM_CLOSE 消息(会触发 Hook)IntPtr hWnd = FindWindow(null, "WindowsProject1");if (hWnd != IntPtr.Zero){SendMessage(hWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);Console.WriteLine("Sent WM_CLOSE to target window.");}else{Console.WriteLine("Target window not found.");}Console.ReadKey();// 卸载 HookHookManager.UninstallHook();}[DllImport("user32.dll", CharSet = CharSet.Unicode)]private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);[DllImport("user32.dll", CharSet = CharSet.Unicode)]private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);}public static class HookManager{// SendMessageW 的原始函数签名[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)]private delegate IntPtr SendMessageWDelegate(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);private static SendMessageWDelegate _originalSendMessageW;private static IntPtr _sendMessageWPtr = IntPtr.Zero;public static void InstallHook(){// 1. 获取 SendMessageW 的地址_sendMessageWPtr = MinHook.GetProcAddress(MinHook.GetModuleHandle("user32.dll"), "SendMessageW");if (_sendMessageWPtr == IntPtr.Zero){Console.WriteLine("Failed to find SendMessageW address.");return;}// 2. 初始化 MinHookvar status = MinHook.MH_Initialize();if (status != MinHook.MH_STATUS.MH_OK){Console.WriteLine($"MH_Initialize failed: {status}");return;}// 3. 创建 Hookvar detourPtr = Marshal.GetFunctionPointerForDelegate(new SendMessageWDelegate(HookedSendMessageW));status = MinHook.MH_CreateHook(_sendMessageWPtr, detourPtr, out var originalPtr);if (status != MinHook.MH_STATUS.MH_OK){Console.WriteLine($"MH_CreateHook failed: {status}");return;}_originalSendMessageW = Marshal.GetDelegateForFunctionPointer<SendMessageWDelegate>(originalPtr);// 4. 启用 Hookstatus = MinHook.MH_EnableHook(_sendMessageWPtr);if (status != MinHook.MH_STATUS.MH_OK){Console.WriteLine($"MH_EnableHook failed: {status}");return;}Console.WriteLine("SendMessageW hook installed successfully!");}public static void UninstallHook(){if (_sendMessageWPtr == IntPtr.Zero)return;// 1. 禁用 Hookvar status = MinHook.MH_DisableHook(_sendMessageWPtr);if (status != MinHook.MH_STATUS.MH_OK)Console.WriteLine($"MH_DisableHook failed: {status}");// 2. 卸载 MinHookstatus = MinHook.MH_Uninitialize();if (status != MinHook.MH_STATUS.MH_OK)Console.WriteLine($"MH_Uninitialize failed: {status}");_sendMessageWPtr = IntPtr.Zero;Console.WriteLine("Hook uninstalled.");}private static IntPtr HookedSendMessageW(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam){Console.WriteLine($"[HOOK] SendMessageW: hWnd=0x{hWnd.ToInt64():X}, Msg=0x{Msg:X}");// 获取窗口所属的线程和进程IDuint processId = 0;uint threadId = GetWindowThreadProcessId(hWnd, out processId);// 使用 System.Diagnostics.Process 获取进程信息string processName = "Unknown";try{var targetProcess = System.Diagnostics.Process.GetProcessById((int)processId);processName = targetProcess.ProcessName;Console.WriteLine($"Window belongs to - ThreadID: {threadId}, ProcessID: {processId}, ProcessName: {processName}");}catch (Exception ex){Console.WriteLine(ex.Message);}// 调用原始函数return _originalSendMessageW(hWnd, Msg, wParam, lParam);}// 需要的Win32 API声明[DllImport("user32.dll", SetLastError = true)]static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);}public static class MinHook{public enum MH_STATUS{MH_OK = 0,MH_ERROR_ALREADY_INITIALIZED,MH_ERROR_NOT_INITIALIZED,// ... 其他状态码}[DllImport("MinHook.x86.dll", CallingConvention = CallingConvention.Cdecl)]public static extern MH_STATUS MH_Initialize();[DllImport("MinHook.x86.dll", CallingConvention = CallingConvention.Cdecl)]public static extern MH_STATUS MH_Uninitialize();[DllImport("MinHook.x86.dll", CallingConvention = CallingConvention.Cdecl)]public static extern MH_STATUS MH_CreateHook(IntPtr pTarget, IntPtr pDetour, out IntPtr ppOriginal);[DllImport("MinHook.x86.dll", CallingConvention = CallingConvention.Cdecl)]public static extern MH_STATUS MH_EnableHook(IntPtr pTarget);[DllImport("MinHook.x86.dll", CallingConvention = CallingConvention.Cdecl)]public static extern MH_STATUS MH_DisableHook(IntPtr pTarget);[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]public static extern IntPtr GetModuleHandle(string lpModuleName);[DllImport("kernel32.dll", CharSet = CharSet.Ansi)]public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);}
}

最核心的代码是上面的 HookedSendMessageW,大家可以多品鉴品鉴,接下来依次运行 WindowsProject1ConsoleApplication 程序,输出如下:

从输出看,是不是一下子就把排查范围缩小了很多,最起码我知道是一个叫 WindowsProject1 的进程坏了我的好事,后续就可以针对 WindowsProject1 深入探究为何方神物。。。

3. 还能更完美一点吗

虽然排查范围极大的缩小了,还但是有一点不完美,如果这个窗口是本进程创建的还好,如果不是本进程创建的,最好能抓到对方进程的dump那就真完美了。。。

接下来的问题是怎么抓对方进程的dump呢?为了确保通用性,我建议在本进程中调 procdump 自动捕获,参考代码如下:


namespace ConsoleApplication
{public class DumpGen{// 生成进程 Dump 文件public static void GenerateProcessDump(int processId, string dumpPath){try{// ProcDump 命令行参数:// -mm: 生成 MiniDump// -accepteula: 自动接受许可协议(避免首次运行时弹出提示)string procDumpPath = $@"{Environment.CurrentDirectory}\procdump.exe";string arguments = $"-accepteula -mm {processId} \"{dumpPath}\"";var startInfo = new ProcessStartInfo{FileName = procDumpPath,Arguments = arguments,UseShellExecute = false,CreateNoWindow = true,RedirectStandardOutput = true,RedirectStandardError = true};using (var proc = new Process { StartInfo = startInfo }){proc.Start();proc.WaitForExit();Console.WriteLine("Dump captured successfully");}}catch (Exception ex){Console.WriteLine($"Failed to launch ProcDump: {ex.Message}");}}}
}

然后修改下 HookedSendMessageW 方法,如果 _originalSendMessageW 超时,将会自动抓取dump,当然这里只是一个简单的演示,更复杂的逻辑大家可以根据自己的情况编写,比如用一个 字典 来存放 hWnd,然后根据超时时间自动的抓取进程的dump,参考代码如下:


private static IntPtr HookedSendMessageW(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam)
{Console.WriteLine($"[HOOK] SendMessageW: hWnd=0x{hWnd.ToInt64():X}, Msg=0x{Msg:X}");// 获取窗口所属的线程和进程IDuint processId = 0;uint threadId = GetWindowThreadProcessId(hWnd, out processId);// 使用 System.Diagnostics.Process 获取进程信息string processName = "Unknown";try{var targetProcess = System.Diagnostics.Process.GetProcessById((int)processId);processName = targetProcess.ProcessName;Console.WriteLine($"Window belongs to - ThreadID: {threadId}, ProcessID: {processId}, ProcessName: {processName}");//定时检测代码:如果超时自动抓取dumpTask.Run(() =>{Thread.Sleep(3000);if (Msg == 0x0010){string dumpPath = Path.Combine(Environment.CurrentDirectory, $"ProcessDump_{processName}_{DateTime.Now:yyyyMMddHHmmss}.dmp");DumpGen.GenerateProcessDump(targetProcess.Id, dumpPath);Console.WriteLine($"Launching ProcDump to generate dump: {dumpPath}");}});}catch (Exception ex){Console.WriteLine(ex.Message);}// 调用原始函数return _originalSendMessageW(hWnd, Msg, wParam, lParam);
}

一切都搞定之后,运行下程序,截图如下:

打开生成好的dump文件,找到目标线程,参考如下:


0:000> ~
.  0  Id: 4f34.338c Suspend: 0 Teb: 009c2000 Unfrozen1  Id: 4f34.6470 Suspend: 0 Teb: 009d2000 Unfrozen2  Id: 4f34.62a8 Suspend: 0 Teb: 009d6000 Unfrozen
0:000> ? 4f34 ; ? 338c; k
Evaluate expression: 20276 = 00004f34
Evaluate expression: 13196 = 0000338c# ChildEBP RetAddr      
00 00b3f910 77a23999     ntdll!NtDelayExecution+0xc
01 00b3f930 776a8760     ntdll!RtlDelayExecution+0xe9
02 00b3f998 776a86ff     KERNELBASE!SleepEx+0x50
03 00b3f9a8 00f81be3     KERNELBASE!Sleep+0xf
04 00b3fae8 76b36d13     WindowsProject1!WndProc+0x43 [D:\sources\woodpecker\ConsoleApplication\WindowsProject1\WindowsProject1.cpp @ 127] 
05 00b3fb14 76b2540d     user32!_InternalCallWinProc+0x2b
06 00b3fc18 76b24eb0     user32!UserCallWinProcCheckWow+0x49d
07 00b3fc7c 76b31709     user32!DispatchClientMessage+0x190
08 00b3fcb8 77a0bb66     user32!__fnDWORD+0x39
09 00b3fcf0 76b33ef0     ntdll!KiUserCallbackDispatcher+0x36
0a 00b3fd2c 00f81e9b     user32!GetMessageW+0x30
0b 00b3fe44 00f8273d     WindowsProject1!wWinMain+0xbb [D:\sources\woodpecker\ConsoleApplication\WindowsProject1\WindowsProject1.cpp @ 46] 
0c 00b3fe64 00f8258a     WindowsProject1!invoke_main+0x2d [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 123] 
0d 00b3fec0 00f8241d     WindowsProject1!__scrt_common_main_seh+0x15a [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288] 
0e 00b3fec8 00f827b8     WindowsProject1!__scrt_common_main+0xd [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 331] 
0f 00b3fed0 76705d49     WindowsProject1!wWinMainCRTStartup+0x8 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_wwinmain.cpp @ 17] 
10 00b3fee0 779fcebb     kernel32!BaseThreadInitThunk+0x19
11 00b3ff38 779fce41     ntdll!__RtlUserThreadStart+0x2b
12 00b3ff48 00000000     ntdll!_RtlUserThreadStart+0x1b

从卦中看,原来卡死是因为主线程正在 KERNELBASE!Sleep,无语了,到此为止,这次卡死事故真相大白于天下。

三:总结

再回头看文章开头的 cogxImagingDevice.dll 导致的程序卡死,如果用本篇的解决方案,是不是非常的轻量级,从此以后再也不需要抓内核的dump,也不需要在客户的电脑上用 spy++ 捣鼓来捣鼓去了。。。完美!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/web/83590.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

qt配合海康工业相机取图开发

1.最近开发海康工业相机&#xff0c;做取图demo 2.在MVS运行目录下找到Development文件夹&#xff0c;找到下图两个文件夹一个是头文件一个是库文件 3.引用到qt项目中 4.下面是头文件跟源文件 头文件 #ifndef MVSCAMERA_H #define MVSCAMERA_H#include <QObject> #incl…

JavaScript基础学习与应用(后端了解部分)

JavaScript JavaScript原名liveScrip,由美国网景公司开发的一种用于对网页操作的脚本语言 脚本语言:(不需要编译 sql html css)由某种解释器直接解释运行的 JavaScript是一种解释性的脚本语言 JavaScript是网页的行为,可以为网页提供各种行为(图片操作) JavaScript一般一对…

Linux环境下安装和使用RAPIDS平台的cudf和cuml - pip 安装方法

‌ cuDF 和 cuML 是 RAPIDS平台 的两个核心组件&#xff0c;它们共同构成了RAPIDS平台的主要功能 1.linux环境下pip安装 pip install cuml-cu1224.6.0 --extra-index-urlhttps://pypi.nvidia.com 安装过程中可能会提示缺少包之类的&#xff0c;按提示进行包的缺失安装 2.安装…

基于 Redis 的幂等性设计:SpringBoot @Async 在高并发 MySQL 日志存储中的应用

一、问题描述 在高并发场景下,大量设备实时上报状态数据,需要异步保存到MySQL,同时需要解决幂等性校验和线程池耗尽问题。 二、解决方案 1. 幂等性控制 作用:确保同一请求无论执行多少次,结果都一致,避免重复处理。 实现方式: 唯一标识:设备ID + 时间戳组合Redis原…

ELK日志采集系统

ELK 日志采集系统指的是由 Elasticsearch、Logstash 和 Kibana 三个核心开源软件组成的套件&#xff0c;用于集中式日志的采集、处理、存储、搜索、分析和可视化。它现在更常被称为 Elastic Stack&#xff0c;因为其组件生态已经扩展&#xff08;尤其是引入了 Beats&#xff09…

什么是音频?

引言&#xff1a;声音的本质 什么是音频&#xff1f;振动与感知 音频&#xff0c;在其最核心的层面&#xff0c;即是我们通常所说的声音。它起源于物体的振动。这些振动扰动了其周围的介质&#xff08;例如空气或水&#xff09;&#xff0c;在介质中产生了微小的压力变化&…

接口 RESTful 中的超媒体:REST 架构的灵魂驱动

在 RESTful 架构中&#xff0c;** 超媒体&#xff08;Hypermedia&#xff09;** 是一个核心概念&#xff0c;它体现了 REST 的 “表述性状态转移&#xff08;Representational State Transfer&#xff09;” 的本质&#xff0c;也是区分 “真 RESTful API” 与 “伪 RESTful AP…

centos clamav 扫描及告警配置

centos clamav 扫描及告警配置 1 下载1.1官网下载1.2 在线下载2 配置3 扫描3.1 更新病毒库3.2 扫描4 告警4.1 安装 Postfix4.2 安装mail邮件工具4.3 配置4.4 发送告警邮箱信息5 定时配置(cronie)5.1 定时更新病毒库5.2 定时扫描1 下载 1.1官网下载 官网下载地址,下载rpm包…

华为WLAN概述知识点及案例试题

目录 &#x1f4d8; 华为WLAN概述知识点及案例总结✅ 一、WLAN技术背景&#x1f4cc; 为什么需要WLAN&#xff1f;&#x1f4cc; 应用趋势&#xff1a; ✅ 二、WLAN基本概念&#x1f4cc; WLAN定义&#x1f4cb; IEEE 802.11与Wi-Fi标准演进&#x1f4cb; 发展趋势&#xff08;…

MultiTalk 是一种音频驱动的多人对话视频生成模型

TL;DR&#xff1a;MultiTalk 是一种音频驱动的多人对话视频生成。它支持多人对话&#x1f4ac;、唱&#x1f3a4;歌、交互控制和&#x1f46c;卡通&#x1f64a;的视频创建。 视频演示 001.mp4 004.mp4 003.mp4 002.mp4 005.mp4 006.mp4 003.mp4 002.mp4…

实现无缝连接:EtherNet/IP转CANopen网关助力汽车制造智能化未来

在如今这个高度自动化的汽车制造行业&#xff0c;设备之间的互操作性变得越来越重要&#xff0c;在一条自动化装配线上&#xff0c;贝加莱的PLC和CANopen伺服驱动器以及通过EtherNet/IP转CANopen网关&#xff08;稳联技术的WL-EIP-COP&#xff09;紧密合作&#xff0c;带来了精…

音视频之H.264的句法和语义

系列文章&#xff1a; 1、音视频之视频压缩技术及数字视频综述 2、音视频之视频压缩编码的基本原理 3、音视频之H.264/AVC编码器原理 4、音视频之H.264的句法和语义 在编码器输出的码流中&#xff0c;数据的基本单位是句法元素。每个句法元素由若干比特组成&#xff0c;它表…

M - 中位数

Description 给定一个长度为 NN 的非负整数序列 AA&#xff0c;对于前奇数项求中位数。 Input 第一行一个正整数 NN。 第二行 NN 个正整数 A1…NA1…N​。 Output 共 ⌊N12⌋⌊2N1​⌋ 行&#xff0c;第 ii 行为 A1…2i−1A1…2i−1​ 的中位数。 Sample 1 InputcopyOu…

医疗耗材领域LCS4110R加密芯片应用

医疗器械和医美器械应用广泛&#xff0c;需求增加&#xff0c;发展迅速。医疗器械和医美器械的使用关系到人民群众的健康安全&#xff0c;以至于生命安全。假冒伪劣器械产品的混入导致对患者的健康危害&#xff0c;同时也损害了设备厂商的利益。防复制加密认证芯片LCS4110R应用…

数据结构与算法:贪心(一)

前言 有一说一贪心的题目真的ex,想不到就是想不到…… 一、贪心 贪心就是通过在过程中每次达到局部最优,从而在最后实现整体最优。贪心的题目经常要用到排序和堆。 越打cf越能感受到贪心的奇妙,很吃状态和灵感。解题的过程中往往依赖举大量例子,然后进行总结和归纳,然…

5、Spring AI(MCPServer+MCPClient+Ollama)开发环境搭建_第一篇

前言&#xff1a; 该开发环境是在 3、后端持久化&#xff08;SpringBoot3.5.0MybatisPlus3.5.5mysql8.4.0&#xff09;环境搭建 上进行改造的&#xff0c;用到了后端持久化&#xff0c;主要改造的地方为数据库把email字段改为height&#xff08;身高&#xff09;&#xff0c;…

个典型的 Java 泛型在反序列化场景下“类型擦除 + 无法推断具体类型”导致的隐性 Bug

今天遇到一个问题&#xff1a;一个典型的 Java 泛型在反序列化场景下“类型擦除 无法推断具体类型”导致的隐性 Bug&#xff0c;尤其是在 RPC&#xff08;如 Dubbo、Feign 等&#xff09;和 本地 JVM 内直连调用共存时&#xff0c;这种问题会显现得非常明显。 A 服务暴露了一…

开发指南121-微服务的弹性伸缩

平台的后台服务表现形式就是各种各样的微服务。微服务可以部署在不同的机器上。单一服务的伸缩很简单&#xff1a; 部署在不同机器上&#xff0c;直接启动关闭即可。 部署在同一机器上&#xff0c;可以复制为多个不同目录&#xff0c;其中jar包&#xff0c;启动文件是完全一样…

【C++特殊工具与技术】优化内存分配(六):运行时类型识别

目录 一、RTTI 的核心机制与设计背景 1.1 RTTI 的设计目标 1.2 RTTI 的启动条件 二、dynamic_cast&#xff1a;动态类型转换 2.1 语法与核心特性 2.2 转换场景详解 2.3 引用类型转换与异常处理 2.4 性能注意事项 三、typeid&#xff1a;类型信息查询 3.1 语法与核心特…

USB串口通信、握手协议、深度学习等技术要点

基于OpenMV的智能车牌识别系统&#xff1a;从硬件到算法的完整实现 前言 本文将详细介绍一个基于OpenMV微控制器的智能车牌识别系统的设计与实现。该系统集成了嵌入式视觉处理、串口通信协议、深度学习OCR识别等多种技术&#xff0c;实现了从图像采集到车牌识别的完整流程。 …