1. 项目概述:当CARLA引擎在你面前崩溃时
如果你正在基于CARLA仿真平台进行自动驾驶算法开发、传感器模拟或者交通流研究,那么“Segmentation fault (core dumped)”或者“Signal 11: Segmentation fault”这个弹窗,大概率是你开发路上最不想见到的“老朋友”之一。它不像普通的逻辑错误会给你一个清晰的堆栈跟踪,而是以一种近乎粗暴的方式,让整个CARLA引擎进程瞬间崩溃,留下一句冰冷的提示和一堆可能毫无头绪的日志。这个项目,就是一次从无数次崩溃中爬出来的实战复盘,目标不是简单地告诉你“重启试试”,而是带你深入CARLA引擎的内部,理解为什么一个看似稳定的仿真环境会突然“ Segmentation fault”,并建立起一套从现象到根因的系统性排查方法论。
CARLA作为一个基于Unreal Engine构建的开源自动驾驶仿真器,其复杂性在于它并非一个单纯的应用程序,而是一个由C++核心引擎、Python客户端API、渲染管线、物理计算、网络同步等多个重型模块构成的复杂系统。一个“Segmentation fault”报错,本质上是一个内存访问违规错误,即程序试图访问一块不属于它的内存区域。在CARLA的语境下,这可能是由于Python客户端与C++服务端之间的数据不同步、自定义传感器蓝图的内存管理问题、资源加载冲突,甚至是与特定硬件驱动的不兼容所导致的。Signal 11则是Unix/Linux系统下对应“Segmentation fault”的信号编号。解决这类问题,关键在于将“黑盒”崩溃转化为可追溯、可分析的线索。
本指南将假设你已经在Ubuntu系统上搭建好了CARLA环境,并且可能在运行自己的Python脚本、修改传感器配置或使用ROS桥接时遇到了崩溃。我们将从最直接的崩溃信息入手,逐步深入到利用核心转储(Core Dump)、调试器(GDB)、系统日志以及CARLA自身日志的工具链中,并结合常见的崩溃场景,为你梳理出一条清晰的排查路径。最终目的,是让你不仅能解决眼前的这一次崩溃,更能建立起预防和快速定位类似问题的能力。
2. 崩溃现场初步勘察与信息收集
当CARLA引擎崩溃时,第一反应不应该是盲目重启。现场留下的“蛛丝马迹”是后续诊断的黄金信息。一个良好的排查习惯,始于系统性地收集所有可用的日志和上下文信息。
2.1 解读终端输出与CARLA日志
崩溃通常发生在终端(如果你通过./CarlaUE4.sh启动服务端)或你的Python客户端脚本运行时。首先,请完整记录终端上崩溃瞬间前后打印的所有信息。
关键信息点包括:
- 崩溃信号:明确的“Segmentation fault”或“Signal 11”字样。
- 堆栈跟踪(Stack Trace):有时,如果程序编译时包含了调试符号,崩溃点会打印出部分堆栈跟踪。这可能指向Unreal Engine的某个模块、CARLA的某个插件(如
libcarla),或者甚至是你的Python脚本通过Boost.Python调用的C++函数。即使看起来是一串内存地址,也请记录下来。 - 崩溃前最后一条日志:CARLA服务端和客户端都会输出大量日志。崩溃前最后几条
LogCarla、LogTemp或LogBlueprint信息至关重要,它可能指示了崩溃前引擎正在执行的操作,例如“Spawning sensor...”、“Processing image data...”或“Destroying actor...”。
CARLA的服务端日志默认输出到终端,但更完整的日志可以通过启动参数指定。一个更专业的做法是,在启动CARLA服务端时,将日志重定向到文件:
./CarlaUE4.sh -carla-server -benchmark -fps=20 -quality-level=Epic 2>&1 | tee carla_server.log这样,carla_server.log文件将包含所有标准输出和错误信息,便于你事后仔细分析。
同时,不要忘记检查Unreal Engine生成的日志文件,它们通常位于~/.config/Epic/CarlaUE4/Saved/Logs/目录下,文件名类似CarlaUE4.log。这里面包含了更底层的引擎事件和可能的错误信息。
2.2 启用并分析核心转储(Core Dump)
核心转储是进程崩溃时内存状态的快照,是分析Segmentation fault最强大的武器。在Linux上,默认可能不生成核心转储文件,需要手动配置。
启用核心转储:
# 检查当前限制 ulimit -c # 如果显示为0,则表示禁止生成。将其设置为无限制(当前会话有效) ulimit -c unlimited # 永久生效,可将其添加到 ~/.bashrc 中 echo “ulimit -c unlimited” >> ~/.bashrc source ~/.bashrc # 设置核心转储文件命名模式和存储路径(可选,但推荐) echo “core.%e.%p.%t” | sudo tee /proc/sys/kernel/core_pattern # 这会将核心文件命名为 core.程序名.PID.时间戳,并保存在当前工作目录。配置完成后,当CARLA再次崩溃,你会在运行目录下找到一个名为core.CarlaUE4.<PID>.<TIMESTAMP>的文件。这个文件可能很大(与CARLA占用的内存相当),但它包含了崩溃瞬间的完整内存映像、所有线程的堆栈状态和寄存器值。
注意:在生产服务器或共享环境中,需谨慎设置核心转储路径和大小限制,避免磁盘被大文件填满。对于本地开发,
unlimited是首选。
3. 使用调试器进行深度诊断
有了核心转储文件,我们就可以请出“外科医生”——GNU调试器(GDB)。GDB可以加载核心转储,重现崩溃现场,让你像侦探一样检查程序“死亡”时的状态。
3.1 使用GDB加载核心转储与可执行文件
首先,你需要找到与生成核心转储时完全一致的CARLA可执行文件(CarlaUE4/Binaries/Linux/CarlaUE4-Linux-Shipping或CarlaUE4.sh脚本启动的那个二进制文件)以及其对应的调试符号。CARLA的发布版本通常剥离了调试符号以减小体积,这会使堆栈跟踪中的函数名显示为内存地址偏移。为了获得最佳调试体验,强烈建议从源码编译CARLA的“开发(Development)”或“调试(Debug)”构建。这虽然耗时,但对于排查棘手的崩溃问题是不可或缺的。
假设你已拥有带调试符号的可执行文件和核心转储文件,使用GDB进行分析:
# 启动GDB,并加载可执行文件和核心转储 gdb /path/to/your/CarlaUE4-Linux-Shipping /path/to/core.core.CarlaUE4.12345.1678886400 # 或者分步进行 gdb /path/to/your/CarlaUE4-Linux-Shipping (gdb) core-file /path/to/core.core.CarlaUE4.12345.1678886400加载成功后,GDB会显示程序终止的信号(SIGSEGV)和崩溃的指令地址。
3.2 解析堆栈与定位崩溃线程
最关键的一步是查看崩溃时的线程堆栈。
(gdb) bt # 或获取更详细的堆栈信息 (gdb) thread apply all bt fullbt(backtrace)命令会打印当前线程(通常是接收到SIGSEGV信号的线程)的调用堆栈。你需要从上往下看,找到第一个属于你的代码或CARLA代码的栈帧,而不是系统库的。例如,堆栈顶部可能显示崩溃发生在libc.so.6的某个函数,但往下翻,你可能会发现源头是libcarla_client.so中的某个函数,该函数正在处理从Python传过来的传感器数据。
如果崩溃发生在非主线程(在CARLA中很常见,比如渲染线程、传感器数据处理线程),你需要切换线程查看。使用info threads查看所有线程,然后thread <线程号>切换到可疑的线程(通常状态为SIGSEGV),再执行bt。
实操心得:很多时候,崩溃点只是一个“受害者”,真正的“元凶”可能在更早的调用中。例如,一个空指针(nullptr)可能在某个函数中被错误赋值,但直到几个函数调用后才被解引用导致崩溃。仔细阅读堆栈中每个帧的信息,结合函数名和参数(如果调试符号完整),尝试推断数据流在哪里出现了异常。
3.3 检查变量内存与寄存器状态
定位到可疑的栈帧后,可以进一步检查当时的变量状态。
(gdb) frame <帧编号> # 切换到具体的堆栈帧 (gdb) info locals # 查看该函数内的局部变量 (gdb) print <变量名> # 打印特定变量的值 (gdb) x/<数量><格式><单位> <内存地址> # 检查内存内容,例如 `x/10xw 0x7fffe1234567` 以十六进制查看10个字如果看到某个指针变量的值是0x0或一个非常小/奇怪的值,那很可能就是空指针或野指针访问的源头。
注意事项:分析核心转储是一个需要耐心和一定底层知识的过程。对于不熟悉的Unreal Engine或Boost内部函数,可以结合CARLA源码进行搜索。重点寻找与你当前操作相关的代码区域,比如你正在尝试生成(Spawn)一个新的车辆或传感器,那么就应该关注与Actor生成和初始化相关的堆栈帧。
4. 常见CARLA Segmentation Fault场景与排查策略
根据社区反馈和个人踩坑经验,大部分CARLA的Segmentation fault可以归因于以下几类场景。你可以对照自己的操作,进行针对性排查。
4.1 Python客户端与C++服务端通信不同步
这是最常见的一类问题。CARLA的Python客户端通过TCP与C++服务端通信。如果客户端发送的指令序列或数据与服务端的预期状态不一致,就可能导致服务端内部状态混乱,进而引发内存访问错误。
典型症状:
- 在快速连续地生成(
world.spawn_actor)、销毁(actor.destroy())Actor时崩溃。 - 在某一帧
world.tick()之后,下一帧对之前获取的Actor列表进行操作时崩溃。 - 使用了已被销毁的Actor引用。
排查与解决策略:
- 严格管理Actor生命周期:确保在销毁Actor后,立即将对应的Python变量设为
None,并避免在任何地方继续使用它。CARLA的Python API部分对象是底层C++对象的代理,持有无效引用极易导致崩溃。 - 检查异步操作:
apply_control,set_autopilot等是异步命令。确保在销毁车辆前,有适当的延迟或状态确认。一个最佳实践是,在destroy()之后,调用world.tick()几次,让服务端有时间完成清理。 - 验证数据返回:从传感器(如相机)获取数据时,检查返回的数据是否为
None。在网络延迟或高负载下,数据可能未来得及准备好。image = camera_queue.get() # 假设使用队列 if image is not None: # 处理图像 array = np.frombuffer(image.raw_data, dtype=np.uint8) ... - 简化复现步骤:如果崩溃在复杂脚本中随机出现,尝试创建一个最小复现脚本。只保留最核心的生成、Tick、销毁逻辑,逐步添加功能,直到崩溃再次出现,从而定位问题代码段。
4.2 自定义传感器或蓝图资源问题
如果你使用了自定义的传感器蓝图(.bp文件)或修改了现有传感器,资源加载失败或蓝图逻辑错误会导致引擎在初始化或运行时崩溃。
典型症状:
- 在生成特定自定义传感器时立即崩溃。
- 崩溃堆栈指向
UObject加载、UMaterial渲染相关函数。
排查与解决策略:
- 检查蓝图依赖:在Unreal Editor中打开你的自定义蓝图,检查所有引用的材质、静态网格体、纹理等资源路径是否正确,是否随项目打包。一个常见的错误是在编辑器中引用了一个绝对路径的资产,但在打包后的版本中该资产不存在。
- 审查蓝图逻辑:检查蓝图图表中的事件和函数,特别是那些在
BeginPlay或Tick中执行的逻辑。复杂的逻辑或不当的循环引用可能导致问题。 - 验证传感器配置:在Python脚本中,检查传递给
sensor的配置字典是否正确。例如,image_size_x和image_size_y应为正整数,fov应在合理范围内。不符合预期的配置值可能被底层C++代码处理时引发异常。 - 使用日志输出:在自定义蓝图的
Event BeginPlay节点后,连接一个Print String节点,输出调试信息。在CARLA服务端启动时加入-stdout参数,确保日志能输出到终端,这可以帮助你确认蓝图是否被成功加载和初始化。
4.3 内存与资源泄漏
长时间运行CARLA仿真,尤其是频繁生成/销毁带有复杂传感器(如语义分割相机、激光雷达)的Actor,可能导致内存或资源(如GPU纹理内存)泄漏,最终耗尽资源引发崩溃。
典型症状:
- 程序运行一段时间后(例如几小时后)随机崩溃。
- 崩溃前观察到系统内存或GPU内存使用率持续缓慢上升。
- 崩溃点可能在内存分配函数(如
malloc,new)或驱动层。
排查与解决策略:
- 监控系统资源:使用
htop,nvidia-smi(对于NVIDIA GPU) 等工具,在运行CARLA时监控内存和显存使用趋势。 - 实施规范的销毁流程:如前所述,确保所有Actor都被正确销毁。对于传感器,不仅要销毁传感器Actor本身,还要确保停止其数据监听(
listen返回的callback对象也应及时处理)。 - 定期重启:对于需要长时间运行的实验,规划定期的仿真重启策略,以释放累积的未管理资源。可以将实验拆分为多个批次,每批次完成后完全重启CARLA服务端。
- 排查自定义代码:如果你在Python端使用了OpenCV、PyTorch等库处理传感器数据,确保你正确地释放了这些库申请的资源(如关闭窗口、释放张量)。
4.4 系统环境与依赖冲突
CARLA依赖于特定版本的Unreal Engine、编译器、图形驱动和系统库。不匹配的环境可能导致不稳定的行为甚至崩溃。
典型症状:
- 在特定机器或特定系统更新后出现崩溃。
- 崩溃堆栈涉及
libstdc++,libc, 或显卡驱动库(如libnvidia-xxx)。
排查与解决策略:
- 核对官方文档:严格遵循CARLA官方GitHub仓库的
README或Docs中的构建和运行要求,包括Ubuntu版本、CMake版本、Clang/Unreal Engine版本等。 - 检查驱动版本:确保你的NVIDIA驱动版本与CARLA发行版或你编译所用的Unreal Engine版本兼容。过旧或过新的驱动都可能导致问题。
- 使用Docker(高级):如果环境问题难以解决,可以考虑使用CARLA官方或社区维护的Docker镜像。这能提供一个一致且隔离的运行环境,极大减少因本地环境差异导致的问题。
- 符号链接与库版本:检查
/usr/lib,/lib等目录下是否存在重复或冲突的系统库版本。有时,手动安装的库可能会干扰系统包管理器管理的库。
5. 系统性排查工作流与工具整合
面对一个棘手的Segmentation fault,遵循一个系统性的排查流程可以避免遗漏关键信息,提高效率。下面是一个推荐的实战工作流。
5.1 从现象到根因的排查流程图(文字描述版)
第一步:现场冻结与信息收集
- 动作:立即停止任何可能干扰现场的操作(如不要急着关闭终端)。
- 产出:完整截图或复制终端崩溃信息;记录下你正在执行的操作(如运行的脚本命令、步骤);保存CARLA服务端日志和Unreal Engine日志。
第二步:核心转储分析
- 前提:确保已启用核心转储并成功生成文件。
- 动作:使用GDB加载核心转储和带符号的可执行文件。
- 产出:获取崩溃线程的完整堆栈跟踪(
bt full),记录崩溃地址和可疑的栈帧。
第三步:代码与上下文关联
- 动作:根据堆栈跟踪中的函数名,在CARLA源码中搜索相关代码文件。结合你崩溃前执行的操作(如“正在生成一个激光雷达”),聚焦到相关的源码模块(如
LibCarla/sensor目录下的代码)。 - 产出:定位到可能出错的源代码行,理解其上下文逻辑(如某个指针参数是否可能为空)。
- 动作:根据堆栈跟踪中的函数名,在CARLA源码中搜索相关代码文件。结合你崩溃前执行的操作(如“正在生成一个激光雷达”),聚焦到相关的源码模块(如
第四步:简化复现与假设验证
- 动作:基于第三步的假设,编写一个最小的、可重复的测试脚本。例如,如果怀疑是某个特定传感器蓝图的问题,脚本就只生成该传感器并Tick几次。
- 产出:一个能稳定复现崩溃的最小代码示例。这是向社区求助或最终确认问题的关键。
第五步:增量修改与测试
- 动作:对最小复现代码进行细微修改(如更换传感器类型、调整生成位置、增加延迟),观察崩溃是否消失。或者,如果你有源码编译环境,可以在怀疑的代码处添加日志打印或进行防御性编程修改(如增加空指针检查),重新编译测试。
- 产出:明确导致崩溃的必要条件,甚至找到临时规避方案。
5.2 辅助工具链的使用
除了GDB,还有一些工具在特定场景下非常有用:
- Valgrind:这是一个内存调试和性能分析工具。它可以检测未初始化的内存使用、内存泄漏、非法内存读写等问题。在CARLA上运行Valgrind可能会非常慢,并且可能产生大量来自Unreal Engine和驱动本身的误报,但它对于排查你自己编写的、与CARLA链接的C++模块(如自定义的CARLA插件)中的内存问题极其有效。
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./CarlaUE4.sh - AddressSanitizer (ASan):这是一个更快速的内存错误检测器,由编译器插桩实现。如果你是从源码编译CARLA,可以尝试使用Clang编译器并开启ASan选项进行构建。这会在运行时检测到内存越界、使用释放后内存等问题,并立即打印出详细的错误报告。这比事后的核心转储分析更直接,但对性能有较大影响,且构建过程复杂。
- Strace/Ltrace:
strace跟踪系统调用,ltrace跟踪库函数调用。它们可以帮助你了解程序崩溃前最后与操作系统或哪些动态库进行了交互。例如,你可以看到程序在崩溃前是否尝试打开一个不存在的文件(资源加载失败),或者是否在某个特定的库调用中卡住。strace -f -o strace.log ./CarlaUE4.sh # 跟踪所有系统调用,输出到文件
6. 疑难案例分析与实战记录
理论需要结合实践。这里分享两个我亲身排查过的、具有代表性的Signal 11崩溃案例,希望能给你提供一些具体的排查思路。
6.1 案例一:多线程传感器数据竞争导致的偶发崩溃
现象:在一个复杂的多传感器融合实验中,CARLA服务端会随机(大约运行半小时后)发生Segmentation fault。崩溃点不固定,GDB堆栈有时显示在渲染线程,有时在网络线程。
排查过程:
- 初步查看日志,无特别明显的错误信息。
- 使用GDB分析多个核心转储文件,发现一个共同点:崩溃时,堆栈中经常出现与
TArray(Unreal Engine的动态数组)相关的操作,如Add或索引访问。 - 回顾代码,发现我们为了效率,在一个Python线程中持续从传感器队列获取数据,并在另一个线程中进行处理。同时,主线程会定期根据处理结果修改车辆的航点。传感器数据(如图像)是CARLA C++端分配并传递到Python端的。
- 假设形成:是否存在一种可能,当Python端正在处理(读取)某一帧传感器数据时,CARLA C++端认为该帧数据已经处理完毕,提前回收了这块内存,导致Python端访问了无效内存?
- 验证与解决:查阅CARLA Python API文档,发现
SensorData(如Image)对象包含一个raw_data属性,它返回的是一个Pythonbytes对象,这个对象是C++端内存的一个视图或拷贝?实际上,对于Image,raw_data是一个指向C++端内存缓冲区的ctypes指针包装,它并不持有该内存的所有权。如果C++端释放了底层缓冲区,而Python端还在使用这个bytes视图,就会导致非法访问。 - 解决方案:在从
raw_data构造NumPy数组时,立即进行深拷贝,将数据复制到Python管理的内存中。
修改后,长时间测试不再出现崩溃。# 之前(有风险): array = np.frombuffer(image.raw_data, dtype=np.uint8) array = array.reshape((image.height, image.width, 4)) # 之后(安全): buffer = bytes(image.raw_data) # 强制复制数据到新的bytes对象 array = np.frombuffer(buffer, dtype=np.uint8).reshape((image.height, image.width, 4))
心得:在多线程或异步环境下,必须非常清楚数据的所有权和生命周期。CARLA的Python API中,许多返回对象是对C++对象的“薄包装”,其底层资源可能被C++端管理。当不确定时,尽早进行数据拷贝是避免竞争条件崩溃的有效手段。
6.2 案例二:自定义蓝图材质路径错误引发的启动崩溃
现象:在导入一个自定义的雷达点云可视化蓝图后,一启动包含该蓝图的关卡,CARLA编辑器(或打包后的服务器)立即崩溃,报Segmentation fault。
排查过程:
- 崩溃发生在启动阶段,几乎没有运行日志。
- 使用GDB加载核心转储,堆栈显示在
UMaterialInterface::GetRenderProxy函数中,并伴随着一些资源加载失败的警告(在引擎日志中看到LogStreaming: Error: Couldn‘t find file for package...)。 - 检查自定义蓝图。该蓝图使用了一个自定义的材质(Material)来实现特殊的点云着色。这个材质引用了一个位于开发者绝对路径下的纹理贴图(如
D:/Work/Textures/Noise.png)。 - 问题定位:当蓝图被打包或迁移到其他机器时,这个绝对路径失效了。Unreal Engine在尝试加载这个不存在的纹理时,可能没有优雅地处理失败,导致后续在渲染管线中访问了无效的材质资源指针,进而引发崩溃。
- 解决方案:
- 将纹理等所有依赖资源正确迁移到CARLA项目的内容目录(
Unreal/CarlaUE4/Content/)下的合适位置。 - 在Unreal Editor中,使用“引用查看器”(Reference Viewer)检查蓝图的所有依赖,确保它们都是项目内的相对路径。
- 重新打开蓝图,所有资源引用应自动更新为项目内相对路径。保存并重新打包。
- 将纹理等所有依赖资源正确迁移到CARLA项目的内容目录(
心得:对于任何自定义资产(蓝图、材质、网格体),必须使用项目内的相对路径。绝对路径是导致跨环境部署崩溃的常见“杀手”。在团队协作中,应使用Unreal Engine的迁移(Migrate)功能来转移资产,它能自动处理依赖关系。
7. 预防措施与最佳实践指南
与其在崩溃后耗费大量时间排查,不如在开发初期就建立良好的习惯,预防大部分常见的Segmentation fault。
7.1 编码规范与资源管理
- 防御性编程:在Python客户端代码中,对所有从CARLA API返回的对象进行有效性检查。特别是从
world.get_actors()、sensor.listen()回调中获取的对象。 - 生命周期管理:为每个生成的Actor(尤其是传感器)建立明确的创建和销毁对应关系。使用
try...finally块或上下文管理器确保资源释放。sensor = world.spawn_actor(blueprint, transform) try: # 使用传感器 sensor.listen(lambda data: ...) # ... 运行逻辑 finally: if sensor is not None and sensor.is_alive: sensor.destroy() time.sleep(0.5) # 给服务端一点清理时间 world.tick() - 避免高频创建销毁:如果需要大量测试,考虑复用Actor。例如,重置车辆位置而不是销毁后重新生成。
- 数据深拷贝:如前所述,对于需要通过
raw_data访问的传感器数据,如果需要在回调函数之外长时间持有或进行跨线程处理,务必进行深拷贝。
7.2 环境与部署一致性
- 版本锁定:记录下所有依赖的明确版本,包括CARLA commit hash、Unreal Engine版本、Python包版本(
carla库、numpy等)、系统驱动版本。使用虚拟环境(如conda)和Docker来固化开发环境。 - 蓝图资产规范化:所有自定义蓝图及其依赖的资源,必须全部放置在CARLA项目内容目录内,并使用相对路径引用。建立资产导入和检查的流程。
- 日志体系化:在你的Python脚本中集成完善的日志模块(如Python内置的
logging),记录关键操作步骤、Actor的ID和状态变化。当崩溃发生时,这些日志能帮你快速定位到崩溃前的最后一步有效操作。
7.3 监控与测试策略
- 压力测试:在开发新功能后,设计一个长时间运行的稳定性测试脚本,模拟高强度的Actor生成、销毁和数据流。监控内存和显存增长趋势。
- 单元测试与集成测试:对于核心的交互逻辑,如传感器数据解析、车辆控制指令发送,编写单元测试。对于复杂的多Actor场景,编写集成测试脚本,并确保它们能在干净的CARLA实例中稳定运行。
- 利用社区与版本对比:如果你遇到的问题在稳定版(如CARLA 0.9.14)中不存在,而在开发版或特定自定义版本中出现,可以对比两个版本的代码变更。CARLA的GitHub Issues和Discord社区是宝贵的资源,很多崩溃问题可能已被报告甚至修复。
排查CARLA的Segmentation fault是一个融合了系统知识、调试技巧和项目经验的综合过程。它没有银弹,但通过系统性地收集信息、理性地分析线索、并借助强大的工具,绝大多数崩溃都能被定位和解决。每一次成功的排查,不仅修复了一个问题,更是对你所构建的自动驾驶仿真系统更深层次的理解。当你再看到“Signal 11”时,希望你的心态已从焦虑转变为一种面对挑战的沉着——因为你知道,通往流畅运行的道路,就藏在这些崩溃信息的细节之中。