简介:EurekaLog是面向Delphi与C++Builder开发者的异常捕获与程序漏洞分析工具,这份企业版源码包(V7.7.8.64)可让应用程序在最终用户电脑上捕获异常与内存泄露,并生成本地调用堆栈日志,指明出错的file、class、method和line,便于开发者快速定位崩溃源头。资源共1494个文件,以pas/hpp/dcu源码及编译单元为主,配合dproj/dpk工程文件、res/dfm资源、exe/dll/bpl构建产物与chm说明文档,整体约124.36MB。目录按库组件和示例工程组织,适合在IDE中直接编译对接。目前已有495人学习下载。企业版附带完整源代码,可深入剖析EurekaLog的注入与日志机制;覆盖10.2、10.3.2等主流Delphi版本,编译后仅增约300KB,兼顾轻量与实用,对追求程序稳定性、需要系统化异常排查方案的中高级Windows开发者很有价值。
1. EurekaLog 源码版是什么:为什么 Delphi 程序需要它
用 Delphi 写业务系统的团队,对 EurekaLog 这个名字不会陌生:它是被很多人称为“最好用”的程序漏洞分析检测工具,尤其适合那些已经上线、天天被用户报“偶发崩溃但本地复现不了”的存量系统。EurekaLog Enterprise 7.7.8.64 源码版的价值在于,它不只是给你一个装好的插件,而是把异常捕获、内存泄漏检测、性能剖析这套能力的完整源码都交到你手上。适合三类人:维护老旧 Delphi 项目的工程师、对崩溃报告精度有执念的客户端开发,以及想研究异常钩子机制的源码党。
2. 异常捕获与堆栈追踪原理:钩子注入到报告生成的链路
2.1 异常链接管:从 VCL 异常到 EurekaLog 报告
Delphi 程序默认有一套异常处理链:TApplication.Run 的消息循环里,未处理的异常会走到 TApplication.HandleException,弹一个默认对话框,然后程序继续跑。问题在于这个对话框只告诉你异常类和消息,没有堆栈、没有寄存器、没有模块版本,线上用户截个图发过来,开发这边只能靠猜。
EurekaLog 的思路是在这条链上做钩子。源码版能直接看到它接管异常的入口,核心逻辑类似下面这段(源码版里可以翻到对应实现):
// 源码版内部异常拦截的示意片段 procedure TExceptionHook.HandleException(AContext: TEurekaLogContext); var LStack: TStackList; begin if EurekaLogOptions.Enabled then begin // 实时抓取当前线程的寄存器上下文 LStack := TStackList.Create(AContext.Registers); try LStack.Build(EurekaLogOptions.StackTraceDepth, EurekaLogOptions.SymbolFile); // 生成报告:异常对象 + 堆栈 + 模块列表 + 系统信息 SaveToLog(LStack, AContext.ExceptionObject); finally LStack.Free; end; end; end;逻辑说明:HandleException 是 EurekaLog 自家的异常处理入口,它做的事情顺序很关键——先保存寄存器上下文,再回溯栈帧,最后把异常对象连同堆栈写入日志。如果栈回溯放在异常对象处理之后,部分寄存器已被污染,堆栈精度会下降。参数方面,StackTraceDepth 控制回溯深度,太大报告体积暴涨但信息冗余,太小则关键帧丢失;SymbolFile 指向 .map 或 .tds 文件,没有它堆栈就只剩十六进制地址。
这套钩子对线程异常同样生效,Delphi 的 TThread.SyncException 会被 EurekaLog 一并接管。实际经验是,那些“看起来像内存被写坏”的偶发崩溃,崩溃点往往不在主线程消息循环里,而是后台线程的访问违例。默认配置下,EurekaLog 也会把后台线程异常一起记录下来,省去你自己包一层 try..except 的力气。
2.2 堆栈还原的关键参数:符号文件与栈帧深度
异常被捕获只是第一步,真正决定报告有没有用的是堆栈能不能还原成函数名。EurekaLog 在生成报告时,会把异常时刻的指令地址和已加载模块的基址做差值,再查符号文件里的地址区间,映射到函数和源码行。这里有两个常见配置项容易让人忽视:
| 配置项 | 常见取值 | 作用 |
|---|---|---|
| StackTraceDepth | 25 层左右 | 栈回溯深度,x64 工程建议加深 |
| SymbolFile / MapFile | 编译时生成的详细 .map | 提供函数名与行号 |
| IncludeSystemModules | False | 是否展开系统 DLL 的栈帧 |
| StackWalkMethod | FramePointer / SEH | 栈回溯方式,优化模式下要切换 |
我在做某设备上位机时踩过一次:默认的栈回溯方式是基于 EBP 帧指针,但编译器开了优化后,很多函数不维护 EBP,回溯到第 5 层就断了。EurekaLog 的 StackWalkMethod 里有基于 SEH 链的回溯方式,对优化代码更友好,代价是回溯速度略慢。你在源码版里能找到这段调度逻辑,看它如何根据模块特征选择回溯策略,这是普通安装版看不到的东西。
另一个被忽略的点是模块列表。报告里会列出所有已加载 DLL 的版本号和基址,这在排查“用户机器上某个第三方 DLL 版本不对”时非常有用。你不需要让用户去查文件属性,报告里已经写清楚。
3. 内存泄漏检测与性能剖析:FullDebugMode 的配置边界
3.1 开启泄漏检测:哪些配置项决定报告质量
EurekaLog 的泄漏检测底层是替换 Delphi 默认内存管理器,进入 FullDebugMode。这个模式下,每次分配和释放都会记录调用来源,程序退出时扫描未释放块,生成泄漏报告。源码版的价值在于你能看到它如何接管 GetMem/FreeMem 的分配入口,并且能按需裁剪检测范围。
实际开启时,重点不是“打开开关”,而是正确设置以下参数:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| EnableLeakDetection | True | 总开关,Release 版建议关闭 |
| LeakReportMinSize | 0 | 小于该大小的泄漏块不报告,过滤噪声 |
| IgnoreLeakModules | 第三方控件 DLL | 已知泄漏但无法修改的模块 |
| SnapshotOnException | False | 异常时是否做一次内存快照 |
经验是,第一次开启泄漏检测时,先别急着清理,跑一遍完整主流程,拿到的报告可能几百条。先按尺寸排序,把那些“单次分配、尺寸固定、来自系统 DLL”的记录忽略掉,再聚焦业务代码。这套操作我一般按四步走:全量开启跑冒烟测试 → 导出报告 → 把第三方 DLL 加入 IgnoreLeakModules → 对剩余记录人工复核。判断一条泄漏是不是误报,关键是看它的调用栈来源:如果栈里只有系统分配函数而没有业务函数,那它大概率是运行时库常驻内存,而不是真正的泄漏。
注意一个边界:EurekaLog 的 FullDebugMode 会让程序内存占用明显增加,分配速度下降。有个做报表工具的同事被“泄漏报告全是异常时的残留内存”误导过,实际上那是因为他们在异常处理流程里创建了大对象,异常发生后没有走释放分支。这种不算泄漏,是流程设计问题,但报告里会被当成泄漏记录报出来。
3.2 性能剖析与调用统计:把报告当性能入口
EurekaLog 还有一个被低估的功能:性能剖析。它能统计每个函数在检测周期内的调用次数、平均耗时、最大耗时,并生成调用次数热区。源码版里可以看到它的插桩方式,常见做法是在函数入口和出口分别记录时间戳和调用计数,归并到符号名下。
我在调一个启动慢的问题时用过一次这个功能:界面初始化 3 秒,看起来是数据库连接慢,但剖析报告显示时间主要花在某个 JSON 解析函数的重复调用上——它在一个循环里被调了 2 万多次。把解析挪到循环外,启动时间降到 0.6 秒。这类问题用传统打点方式很难定位,因为均值被稀释了,EurekaLog 的调用计数能直接暴露热区。
性能剖析的开启方式和泄漏检测类似,都在 EurekaLog Options 的 Profiler 页里。一般建议只针对特定模块开启,全模块开启会产生大量日志文件,而且写日志本身会影响耗时统计。我一般用“先全量跑一次定位热点模块,再对热点模块单独深挖”的方式。
4. 源码版编译与工程集成:从源码包到出报告的完整路径
4.1 编译顺序与 IDE 集成:先 Runtime 后 Design Time
拿到 EurekaLog Enterprise 7.7.8.64 源码版,第一步不是立刻打开 Delphi 编译工程,而是理解源码包的目录结构。源码包里通常包含 Runtime 包、Design Time 包和示例工程三部分。编译顺序必须是先 Runtime 后 Design Time,因为设计期包依赖运行期包提供功能函数,反了会报“找不到单元 XXX”。
常见做法是用命令行编译,比在 IDE 里手工操作更可控:
# 编译 32 位 Runtime 包,先跑核心库 msbuild Source\EurekaLogCore.dproj /t:Build /p:Config=Release /p:Platform=Win32 # 编译设计期包,注意平台必须与 IDE 一致(多数 IDE 是 32 位) msbuild Source\EurekaLogDesignTime.dproj /t:Build /p:Config=Release /p:Platform=Win32逻辑说明:EurekaLogCore.dproj 是核心运行库,不依赖 IDE 环境;EurekaLogDesignTime.dproj 负责向 IDE 注入菜单和配置界面,它必须在核心库之后编译。参数方面,Platform=Win32 很关键——如果你的 IDE 是 32 位的,设计期包编成 Win64 会直接加载失败,IDE 菜单里看不到 EurekaLog。源码版与安装版不同的地方在于,你可以自行修改源码后重新编译,比如改掉默认报告文件名格式,或者增加自定义字段。我改过一次报告文件的命名规则,把崩溃时间放进文件名,方便用户直接按时间找文件。
4.2 工程接入:配置文件与异常对话框定制
编译完成后,接入你自己的工程有两种方式:一种是 IDE 菜单自动注入,它会在 .dpr 的 uses 列表最前面插入 EurekaLog 单元,并生成一个 .eul 配置文件;另一种是手动引用单元加手工配置。手动方式更可控,适合团队统一配置:
program Project1; uses EurekaLog, // 必须放在 uses 第一位 EurekaLogOptions, // 配置项访问 Vcl.Forms, Unit_Main in 'Unit_Main.pas';这段代码的重点是 EurekaLog 必须出现在 uses 第一位。Delphi 单元初始化顺序按 uses 顺序执行,EurekaLog 需要最早安装自己的异常钩子,否则后续单元的初始化异常它接不到。配置项集中在 .eul 文件里,格式类似下面这样:
<EurekaLogOptions> <LogFile Enable="True" Path="%APPDATA%\MyApp\Logs\" /> <Email Enable="False" /> <ExceptionDialog Style="EurekaLog" /> <StackTrace Depth="25" ShowSystemModules="False" /> <MemoryLeak Enable="True" IgnoreSize="64" /> </EurekaLogOptions>参数说明:LogFile 的 Path 建议写到 %APPDATA% 下某个子目录,不要写程序目录,避免用户装了 UAC 保护后没有写权限导致日志落不下来;ExceptionDialog 的 Style 可以选 EurekaLog 自带的对话框样式,也可以选系统默认,前者能多展示一个“发送报告”按钮,方便用户手动反馈。
工程接入后的验证路径是:在某个按钮事件里写一行 div by zero 触发生成异常,运行后到日志目录看是否产生 .elf 报告,再用 EurekaLog 自带的报告浏览器打开,确认堆栈里有你的函数名而不是一串地址。如果堆栈显示地址,说明符号文件没配置对,回到编译选项里把 Detailed map file 勾上。
5. 集成避坑与常见问题排查:五个高频翻车点
5.1 报告堆栈只有地址,没有函数名
现象:程序确实生成了 .elf 报告,但打开后堆栈区全是十六进制数字,一个函数名都看不到。原因基本是符号文件缺失或路径没对上——EurekaLog 要还原堆栈,必须在报告生成时能访问到 .map 文件,或者报告里记录了符号文件路径让浏览器二次解析。解决:在 Delphi 编译选项里勾选 Detailed map file,并把 .map 文件路径加入 EurekaLog Options 的 Symbols 页。我习惯把 .map 放在跟 exe 同目录,这样即使报告在用户机器生成,也能带着符号路径。
5.2 设计期包加载失败,IDE 菜单找不到 EurekaLog
现象:装完源码包后打开 Delphi,EurekaLog 菜单完全没有出现。原因九成是设计期包平台和 IDE 不一致,或者编译后的 .bpl 文件没放入 IDE 搜索路径。我见过某开发者把 DesignTime 包编成 Win64,结果 32 位的 IDE 根本不加载它。解决:确认 IDE 位数后重新编译对应平台的设计期包,检查 IDE 的 Library Path 是否包含输出目录。另外,源码版安装到中文用户名路径下时,IDE 偶尔会解析不了带空格的路径,把源码包释放到纯英文路径再编一次就能解决。
5.3 内存泄漏报告大量误报
现象:开启 FullDebugMode 后,跑一次完整流程出来几百条泄漏记录,其中大量来自第三方界面库。原因:第三方 DLL 用自己的内存管理器分配内存,EurekaLog 的 FastMM 接管不了,这些块在进程退出时自然不会被释放,被当成泄漏记录。解决:在 IgnoreLeakModules 里把第三方模块名加进去。注意区分“真泄漏”和“常驻块”:常驻块的调用栈里没有业务函数,而是停在某个初始化函数;真泄漏的栈里通常能追溯到某个业务函数里没有配对释放的 GetMem。
5.4 发布版本弹出“找不到 EurekaLog 运行库”
现象:开发机上跑得好好的,拷到用户机器上运行,启动即报缺 DLL。原因:工程设置里勾选了 Build with runtime packages,设计期混入了 EurekaLog 的运行时包依赖,而 EurekaLog 的 bpl 没有随发布包拷贝。解决:确认最终发布时关闭 runtime packages,选择静态链接方式编译,EurekaLog 会把自己的逻辑编进 exe,不依赖外部 bpl。这个方法有一个代价:exe 体积会增加几 MB,但换来的是不用维护一堆 bpl,值得。
5.5 x64 工程堆栈只能回溯前几帧
现象:同一个工程,x86 版报告完整,编译成 64 位后堆栈只有前 5 帧。原因:x64 的调用约定不是基于 EBP 的,栈回溯方式要改成针对 x64 的规则。解决:在 EurekaLog Options 里把 StackWalkMethod 调整为 SEH 或 x64 专用模式,同时确认符号文件是 64 位编译产生的——用 32 位的 .map 去还原 64 位堆栈,行号会错乱。这个坑在源码版里尤其容易踩,因为源码版可以自定义构建选项,改动了编译器优化级别后符号信息不配套。
6. 发布前的最后一步:符号路径与报告去噪技巧
项目要发版了,EurekaLog 也接入完成,但直接打包往往会在收到第一批用户报告时翻车。我的习惯是把以下检查清单强制走完,再放行发布。
第一,验证符号可达性。模拟用户环境:把 exe、.map、.tds 放到一个无开发工具的全新目录,运行后故意触发一次异常,看报告能否还原出函数名。这一步能挡掉大部分“开发机能解析、用户机不能解析”的问题。如果结果不理想,把符号路径改成绝对路径,或者关闭“报告内嵌符号”选项改为引用外部文件。
第二,报告去噪。在 EurekaLog Options 里把已知的无害异常加入忽略列表,例如用户关闭窗口瞬间的控件访问冲突、第三方输入法引起的布局异常。这样做不是掩盖问题,而是让真正有价值的高频崩溃浮到报告列表顶部。忽略列表的正确姿势是:先收集 100 份报告,统计异常类分布,再决定忽略哪些;不要提前猜。
第三,自定义上报入口。EurekaLog 的报告可以配置成 HTTP 提交,把 .elf 文件 POST 到内部服务器。常用做法是让报告先落到本地,再通过一个自定义事件把它转发出去:
# 解析 .elf 报告,过滤已知异常后转发到内部接口 import re report_path = "report.elf" with open(report_path, "r", encoding="utf-8", errors="replace") as f: content = f.read() known_exceptions = ["EAbort", "EControlC"] if any(ex in content for ex in known_exceptions): print("ignored: known exception") else: # 这里写实际的上报逻辑,例如压缩后 POST print("forward report: %s" % report_path)这段脚本的意义在于,线上报告量大时,EurekaLog 浏览器一个个点开看效率太低,用脚本先做一次粗过滤,把已知异常剔除,剩下的才是真正需要人工分析的崩溃。参数方面,known_exceptions 列表按你自己的业务异常维护,过滤逻辑可以再加一层“同一时间窗口内出现多次”的判断。
第四,发布包自检。确认发布目录里没有设计期文件,EurekaLog 生成的配置文件路径写的是相对路径而不是开发机绝对路径,日志目录在用户机器上有写权限。
我是从一次线上事故之后养成这个习惯的:某项目发布了一个版本,用户反馈程序闪退,但所有报告打开后堆栈全是一堆地址,因为打包时把 .map 漏了。那次排查花了整整两天,最后靠远程给用户补发符号文件才解开。从那以后,每次发布前我都强制走一遍“全新目录触发异常 → 检查堆栈可读性”的流程,顺手把发版检查清单存成模板跟源码放一起。这套方法不是银弹,但至少能让线上崩溃变成可读的堆栈而不是一堆十六进制地址。希望帮到你。
本文还有配套的精品资源,点击获取