news 2026/10/19 13:28:46

车机双系统ASAN实战:QNX+Android9内存崩溃定位与构建配置

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
车机双系统ASAN实战:QNX+Android9内存崩溃定位与构建配置

简介:这份资源面向高通8155p平台下从事QNX+Android9双系统开发的工程师,聚焦于开启ASAN(AddressSanitizer)以定位内存越界、泄漏等疑难问题的编译配置方法。包内同时提供Android.mk与Android.bp两套构建脚本示例,覆盖libAsanDebug、asan_debugd及其vnd变体、libclang_rt.asan运行时库等模块,帮助开发者解决ASAN无法使能、编译报错等常见障碍。资源共18个文件,包含4个mk、4个bp、4个cpp、2个so、2个h及2个mk备份文件,涵盖构建脚本、源码、头文件与预编译库,压缩包约1.21MB,结构紧凑便于对照移植。目前已有1995人学习下载。对于不熟悉Android.mk与Android.bp写法、或反复遭遇编译问题的开发者,可借此快速比对配置差异,理解ASAN在QNX+Android9环境下的接入方式,少走弯路。

1. 车机双系统下 ASAN 开启实录:为什么你的内存崩溃总是抓不到现场

做车机项目的兄弟大概率都遇到过这种场景:高通 8155p 平台上跑着 QNX 加 Android 9 的双系统,Android 侧某个 native 服务跑着跑着就挂了,logcat 里只有一行SIGSEGV或者abort,堆栈全是???,用 addr2line 反查也定位不到具体哪一行代码越界。这种内存问题在车机环境里特别难查,因为 QNX 侧和 Android 侧共享内存、跨系统通信频繁,加上 Android 9 的 native 层编译优化默认开着-O2,很多越界写、use-after-free 被优化掉了,崩溃现场根本留不下来。

ASAN(AddressSanitizer)就是专门治这个的。它能在编译期插桩,运行时检测堆栈越界、释放后使用、内存泄漏等问题,崩溃时直接打印出哪一行代码、哪个地址、什么类型的错误。但问题在于,8155p 平台的 Android 9 源码树里,有的模块用Android.mk,有的已经迁到Android.bp,两套构建系统开启 ASAN 的方式完全不一样,网上搜到的教程要么只讲 AOSP 原生,要么只讲单系统,放到车机双系统环境里直接翻车。这篇就把我在 8155p 上给 QNX+Android9 双系统开 ASAN 的完整过程拆开讲,包括Android.mk和Android.bp两种构建方式的具体改法、编译参数怎么配、运行时怎么让 ASAN 真正生效,以及那些我踩过的坑。

2. 先搞清楚 8155p 双系统下 ASAN 的生效链路

2.1 为什么车机上的 ASAN 比手机难搞

手机上的 ASAN 相对简单,因为整个 Android 系统是单内核、单用户空间,编译时全局开SANITIZE_TARGET就能让大部分模块带上 ASAN 运行时。但 8155p 是 QNX 加 Android 双系统架构,Android 跑在虚拟机或者独立核上,QNX 负责仪表、CAN 通信等实时性要求高的部分。这意味着两件事:第一,Android 侧的 native 服务可能通过共享内存或 socket 和 QNX 侧通信,内存问题可能出在跨系统边界上;第二,Android 9 的源码树里,高通加了大量 vendor 定制,很多模块的编译规则被改过,直接套 AOSP 的 ASAN 配置不一定生效。

更麻烦的是,8155p 的 Android 9 源码里,Android.mk和Android.bp混用严重。系统核心模块比如libcutils、libutils还在用Android.mk,但高通新加的 HAL 层和部分 vendor 服务已经迁到Android.bp。你要查的那个崩溃模块到底用哪套构建系统,决定了你开 ASAN 的具体写法。

2.2 ASAN 在 Android 构建系统里的三种开启层级

在 Android 9 的构建体系里,开 ASAN 有三个层级,理解这个才能知道改哪里:

全局层级:在BoardConfig.mk或build/core里设置SANITIZE_TARGET := address,这会让整个系统镜像带 ASAN。但车机项目基本不会这么干,因为 ASAN 运行时会让系统变慢、内存占用翻倍,QNX 侧和 Android 侧的实时通信可能超时,车机直接起不来。

模块层级:在单个模块的Android.mk或Android.bp里加 ASAN 编译标志。这是最常用的方式,只给你怀疑有问题的那个 native 服务或库开 ASAN,不影响其他模块。

预编译层级:如果崩溃模块依赖了预编译的.so,那些.so没带 ASAN 插桩,运行时 ASAN 可能报uninstrumented警告,甚至漏报。这种情况需要把依赖库的源码拉进来一起编,或者用 ASAN 的detect_invalid_pointer_pairs等选项做部分检测。

我这次要查的是一个 vendor 的 camera HAL 服务,它用Android.bp构建,依赖了三个预编译的 vendor 库。下面按构建系统分开讲具体改法。

2.3 确认模块用的是 Android.mk 还是 Android.bp

在改之前,先确认你要查的模块到底用哪套。进到模块目录,看有没有Android.bp或Android.mk:

# 进到疑似崩溃模块的源码目录 cd vendor/qcom/proprietary/camera-hal # 查看构建文件 ls -la Android.* # 输出示例: # Android.bp # Android.mk

如果两个都有,Android.bp优先级更高(Android 9 的 soong 构建系统会优先解析 bp 文件)。如果只有Android.mk,那就走 make 的改法。确认清楚再动手,不然改了半天发现根本没生效,白费功夫。

3. Android.mk 模块开 ASAN:从编译参数到运行时库的完整配置

3.1 Android.mk 里加 ASAN 编译标志的正确姿势

假设你的模块叫vendor.camera.hal,用Android.mk构建。最直接的方式是在Android.mk里给LOCAL_CFLAGS和LOCAL_LDFLAGS加 ASAN 标志:

# Android.mk 片段 LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := vendor.camera.hal LOCAL_SRC_FILES := camera_hal.cpp camera_ops.cpp # 开启 ASAN 编译插桩 LOCAL_CFLAGS += -fsanitize=address -fno-omit-frame-pointer -g LOCAL_LDFLAGS += -fsanitize=address # 关闭优化,避免 ASAN 插桩被优化掉 LOCAL_CFLAGS += -O0 # 链接 ASAN 运行时库 LOCAL_SHARED_LIBRARIES += libasan include $(BUILD_SHARED_LIBRARY)

这里有几个关键点。-fsanitize=address是开启 ASAN 的核心标志,编译和链接都要加。-fno-omit-frame-pointer保证堆栈回溯能拿到完整调用链,不加的话 ASAN 报错时堆栈可能断掉。-g保留调试符号,不然报错只有地址没有行号。-O0关优化,因为-O2下编译器可能把越界访问优化成合法访问,ASAN 就检测不到了。

LOCAL_SHARED_LIBRARIES += libasan是链接 ASAN 运行时。Android 9 的源码树里,libasan在external/compiler-rt下,默认可能没编进系统。你需要先确认libasan.so存在:

# 在源码根目录查找 libasan find out/target/product/ -name "libasan.so" 2>/dev/null # 如果没有输出,说明没编,需要先编 libasan make libasan -j16

3.2 全局 SANITIZE_TARGET 和模块级标志的冲突处理

有时候你在模块级加了 ASAN 标志,但编译报错说符号重复定义或者运行时库冲突。这通常是因为BoardConfig.mk里已经设了全局的SANITIZE_TARGET,或者build/core里有默认的 sanitize 配置。检查方法:

# 在源码根目录搜索 SANITIZE_TARGET grep -rn "SANITIZE_TARGET" device/qcom/ build/core/ 2>/dev/null | head -20

如果发现全局设了SANITIZE_TARGET := address,而你又只想给单个模块开,那需要把全局的关掉,或者在模块的Android.mk里用LOCAL_SANITIZE := address覆盖。Android 9 的 make 构建系统支持LOCAL_SANITIZE变量,写法更简洁:

# 推荐写法:用 LOCAL_SANITIZE LOCAL_SANITIZE := address LOCAL_SANITIZE_DIAG := address

LOCAL_SANITIZE会自动加编译和链接标志,LOCAL_SANITIZE_DIAG是诊断模式,报错更详细。但注意,LOCAL_SANITIZE在部分高通定制的 Android 9 源码里可能被改过,如果编译报错说不认识这个变量,就退回手动加LOCAL_CFLAGS的方式。

3.3 运行时让 ASAN 真正生效:环境变量和权限

编译带 ASAN 的模块只是第一步,运行时还得让 ASAN 运行时库被加载。Android 9 默认的init.rc里,native 服务启动时不会自动加载libasan.so,需要手动配。

方式一:在服务的init.rc里设环境变量:

# 在 init.rc 或 init.<target>.rc 里 service vendor_camera_hal /vendor/bin/hw/vendor.camera.hal class hal user cameraserver group camera # 开启 ASAN 选项 setenv ASAN_OPTIONS "detect_leaks=0:abort_on_error=1:symbolize=1:log_path=/data/asan" setenv LD_PRELOAD /system/lib64/libasan.so

ASAN_OPTIONS里几个关键参数:detect_leaks=0关掉内存泄漏检测,因为车机服务长期运行,泄漏检测会误报且拖慢性能;abort_on_error=1让 ASAN 检测到错误直接 abort,方便抓 core dump;symbolize=1让报错带符号;log_path指定日志输出目录,不设的话默认打到 stderr,在 logcat 里可能被冲掉。

LD_PRELOAD强制预加载libasan.so,这是让 ASAN 运行时接管内存分配的关键。不加的话,即使编译带了 ASAN 插桩,运行时也可能因为找不到运行时库而报ASan runtime does not come first in initial library list错误。

方式二:如果服务是通过Android.bp的init_rc属性配的,直接在 bp 文件里加:

// Android.bp 片段 cc_binary { name: "vendor.camera.hal", init_rc: ["vendor.camera.hal.rc"], ... }

然后在vendor.camera.hal.rc里写setenv和LD_PRELOAD,和上面一样。

提示:LD_PRELOAD的路径要确认是 64 位还是 32 位。8155p 的 Android 9 是 64 位系统,但部分 vendor 服务是 32 位的,32 位要用/system/lib/libasan.so,路径写错会报cannot locate symbol或者直接起不来。

4. Android.bp 模块开 ASAN:soong 构建系统下的 sanitize 配置

4.1 Android.bp 里 sanitize 属性的写法

Android.bp是 soong 构建系统的配置,开 ASAN 比Android.mk简洁,但坑也不少。基本写法:

// Android.bp 片段 cc_binary { name: "vendor.camera.hal", srcs: [ "camera_hal.cpp", "camera_ops.cpp", ], shared_libs: [ "libcutils", "libutils", "liblog", ], // 开启 ASAN sanitize: { address: true, // 可选:诊断模式,报错更详细 diag: { address: true, }, }, // 关闭优化 cflags: [ "-O0", "-g", "-fno-omit-frame-pointer", ], // 链接 ASAN 运行时 static_libs: [ "libasan", ], }

sanitize: { address: true }是核心,soong 会自动加-fsanitize=address编译和链接标志。diag子属性开启诊断模式,会加-fsanitize-address-use-after-scope等更严格的检测。cflags里手动加-O0和-g保证调试信息完整。

注意static_libs里加libasan还是shared_libs里加,取决于你的模块类型。如果是cc_binary,用static_libs链接 ASAN 运行时更稳,因为静态链接不会受LD_PRELOAD顺序影响。如果是cc_library_shared,用shared_libs。

4.2 soong 构建系统下 sanitize 不生效的排查

Android.bp里写了sanitize: { address: true }但编译后readelf看不到 ASAN 符号,这是最常见的问题。排查步骤:

# 编译后检查 .so 或可执行文件是否带 ASAN 符号 readelf -s out/target/product/<target>/vendor/bin/hw/vendor.camera.hal | grep asan # 如果有输出,说明 ASAN 插桩成功 # 如果没有,说明 sanitize 没生效

不生效的原因通常有三个。第一,模块被cc_prebuilt_binary或cc_prebuilt_library_shared覆盖了,预编译模块不会走 sanitize。检查Android.bp里有没有同名的 prebuilt 模块,有的话把 prebuilt 去掉或者改成从源码编。第二,sanitize属性被父级defaults模块覆盖了。soong 支持defaults继承,如果父级defaults里设了sanitize: { address: false },子模块的true可能被覆盖。检查Android.bp里有没有defaults: ["xxx_defaults"],有的话去那个 defaults 模块里看。第三,高通定制的 soong 插件改了 sanitize 行为。部分 8155p 的源码里,vendor/qcom/下有自定义的 soong 插件,会过滤掉 sanitize 属性。这种情况需要在BoardConfig.mk里加SOONG_CONFIG_<module>_SANITIZE := true之类的配置,具体看高通文档。

4.3 预编译库依赖导致 ASAN 漏报的处理

前面提到,如果崩溃模块依赖了预编译的.so,那些.so没带 ASAN 插桩,ASAN 可能漏报。典型现象是:崩溃发生在预编译库里,但 ASAN 只报SEGV不报具体错误类型,或者报uninstrumented警告。

处理方式有两种。第一种,把预编译库的源码拉进来一起编,在Android.bp里把cc_prebuilt_library_shared改成cc_library_shared,加上sanitize: { address: true }。这种方式最彻底,但需要源码。第二种,如果拿不到源码,用 ASAN 的detect_invalid_pointer_pairs选项做部分检测:

# 在 ASAN_OPTIONS 里加 setenv ASAN_OPTIONS "detect_invalid_pointer_pairs=2:detect_leaks=0:abort_on_error=1"

detect_invalid_pointer_pairs=2会检测指针比较的合法性,能抓到一部分跨库的指针误用。但这种方式覆盖不全,只能作为临时手段。

注意:预编译库和 ASAN 插桩库混用时,内存分配器可能冲突。ASAN 用自己的分配器替换了malloc/free,如果预编译库直接调用了__libc_malloc之类的底层符号,可能绕过 ASAN 检测。这种情况需要在Android.bp里加allow_undefined_symbols: true并手动处理符号重定向。

5. 避坑记录:8155p 上开 ASAN 踩过的五个坑

5.1 坑一:编译通过但运行时 ASAN 不报错

现象:模块编译带了 ASAN 符号,readelf能看到__asan_init,但运行时故意写个越界访问,ASAN 不报错,程序正常跑完。

原因:LD_PRELOAD没设或者设错了。ASAN 运行时库必须在所有其他库之前加载,否则内存分配不走 ASAN 的分配器,插桩代码检测不到。另外,如果服务是通过init启动的,init.rc里的setenv可能被服务的on property覆盖。

解决:确认LD_PRELOAD路径正确,64 位用/system/lib64/libasan.so,32 位用/system/lib/libasan.so。在init.rc里setenv之后加on property:init.svc.vendor_camera_hal=running确认环境变量生效。还可以在服务启动后cat /proc/<pid>/environ看实际环境变量。

5.2 坑二:ASAN 报错堆栈全是问号

现象:ASAN 检测到越界,但报错堆栈里函数名和行号全是???,只有地址。

原因:编译时没加-g,或者加了-g但被strip掉了。Android 9 的构建系统默认会对 release 版本的二进制做 strip,ASAN 需要的调试符号被删了。

解决:在Android.mk或Android.bp里加LOCAL_STRIP_MODULE := false或strip: { none: true },禁止 strip。另外确认-g在cflags里,且没有被后续的-O2覆盖。如果还是问号,用ndk-stack或addr2line手动符号化:

# 用 addr2line 手动符号化 addr2line -e out/target/product/<target>/symbols/vendor/bin/hw/vendor.camera.hal -f -C 0x12345

5.3 坑三:QNX 侧共享内存导致 ASAN 误报

现象:ASAN 报heap-buffer-overflow,但代码里那个地址是 QNX 侧通过共享内存映射过来的,不是 Android 堆上的内存。

原因:ASAN 只检测自己分配器管理的内存,共享内存映射的地址不在 ASAN 的 shadow memory 里,访问这些地址时 ASAN 可能误判为越界。

解决:在ASAN_OPTIONS里加detect_container_overflow=0和intercept_tls_get_addr=0,减少对非 ASAN 管理内存的检测。更彻底的方式是用__asan_poison_memory_region和__asan_unpoison_memory_region手动标记共享内存区域,告诉 ASAN 这些地址是合法的:

// 在共享内存映射后调用 extern "C" void __asan_unpoison_memory_region(void const volatile *addr, size_t size); __asan_unpoison_memory_region(shm_addr, shm_size);

5.4 坑四:ASAN 开启后车机启动超时

现象:带 ASAN 的模块编译进去后,车机启动到一半卡住,QNX 侧报 Android 侧心跳超时。

原因:ASAN 运行时会让模块启动变慢,内存占用增加。如果崩溃模块是启动关键路径上的服务,ASAN 的初始化时间可能导致启动超时。

解决:把 ASAN 模块的启动延后,在init.rc里加oneshot和disabled,等系统起来后再手动start。或者用ASAN_OPTIONS里的start_deactivated=1让 ASAN 延迟激活,减少启动时的开销。另外,detect_leaks=0必须设,泄漏检测在启动阶段特别慢。

5.5 坑五:Android.mk 和 Android.bp 混用导致配置冲突

现象:同一个模块目录下既有Android.mk又有Android.bp,改了Android.mk的 ASAN 配置,编译后发现没生效。

原因:Android 9 的 soong 构建系统优先解析Android.bp,Android.mk被忽略。如果Android.bp里没开 ASAN,只改Android.mk没用。

解决:确认模块实际用哪套构建系统。用m <module_name> -n看编译命令,或者看out/soong/build.ninja里模块的构建规则。如果Android.bp存在,改Android.bp;如果只有Android.mk,改Android.mk。两个都有的情况,要么删掉一个,要么确保改的那个是实际生效的。

6. 二次开发改造:把 ASAN 检测做成可开关的调试模块

6.1 用编译变量控制 ASAN 开关

每次查内存问题都手动改Android.mk或Android.bp太麻烦,而且容易忘记改回来。我一般会做成编译变量控制,在BoardConfig.mk里加一个开关:

# BoardConfig.mk # ASAN 调试开关,默认关闭 ENABLE_ASAN_DEBUG ?= false

然后在模块的Android.mk里判断:

# Android.mk 片段 ifeq ($(ENABLE_ASAN_DEBUG),true) LOCAL_SANITIZE := address LOCAL_CFLAGS += -O0 -g -fno-omit-frame-pointer LOCAL_STRIP_MODULE := false endif

Android.bp里没法直接读 make 变量,但可以用 soong 的soong_config_module_type做条件配置:

// Android.bp 片段 soong_config_module_type { name: "asan_cc_binary", module_type: "cc_binary", config_namespace: "vendor_asan", variables: ["enable_asan"], properties: ["sanitize"], } soong_config_string_variable { name: "enable_asan", values: ["true", "false"], } asan_cc_binary { name: "vendor.camera.hal", srcs: ["camera_hal.cpp"], soong_config_variables: { enable_asan: { true: { sanitize: { address: true, }, }, false: {}, }, }, }

这样编译时用make ENABLE_ASAN_DEBUG=true就能全局控制,不用改模块文件。

6.2 用脚本自动抓取 ASAN 日志并符号化

ASAN 报错日志默认打到logcat或者log_path指定的目录。车机环境里logcat滚动快,容易冲掉。我一般写个脚本自动抓:

#!/bin/bash # asan_catch.sh - 自动抓取 ASAN 日志并符号化 # 清空旧日志 adb shell "rm -rf /data/asan/*" # 重启目标服务 adb shell "stop vendor.camera.hal" adb shell "start vendor.camera.hal" # 等待崩溃 sleep 10 # 拉取 ASAN 日志 adb pull /data/asan/ ./asan_logs/ # 符号化 for log in ./asan_logs/*; do if grep -q "ERROR: AddressSanitizer" "$log"; then echo "Found ASAN error in $log" # 用 ndk-stack 符号化 ndk-stack -sym out/target/product/<target>/symbols/ -dump "$log" fi done

这个脚本能自动重启服务、抓日志、符号化,省去手动操作。ndk-stack需要 Android NDK,路径按实际环境配。

6.3 验证 ASAN 是否真正生效的三种方法

改完配置后,怎么确认 ASAN 真的生效了?我一般用三种方法交叉验证。

第一种,readelf查符号:

readelf -s out/target/product/<target>/vendor/bin/hw/vendor.camera.hal | grep -E "asan_init|asan_report" # 有输出说明插桩成功

第二种,运行时查环境变量:

adb shell "cat /proc/$(pidof vendor.camera.hal)/environ" | tr '\0' '\n' | grep ASAN # 有 ASAN_OPTIONS 说明环境变量生效

第三种,故意触发一个越界访问,看 ASAN 是否报错。在代码里临时加一行:

// 临时测试代码,验证后删除 char *test_buf = new char[10]; test_buf[20] = 'x'; // 越界写,ASAN 应该报 heap-buffer-overflow delete[] test_buf;

如果 ASAN 报heap-buffer-overflow并打印堆栈,说明整条链路都通了。验证完记得删掉测试代码。

6.4 一个我踩过的坑:ASAN 和 QNX 侧通信的兼容性

最后说个血泪经验。8155p 上 Android 侧和 QNX 侧通过libipc之类的库通信,这些库通常是预编译的,没带 ASAN 插桩。我一开始给 Android 侧的通信模块开了 ASAN,结果 QNX 侧发过来的消息在 Android 侧解析时,ASAN 报stack-buffer-overflow,但实际是 QNX 侧的消息格式和 Android 侧预期的不一致,不是内存越界。

这种跨系统边界的问题,ASAN 只能告诉你「这里访问了不该访问的内存」,但根因可能在 QNX 侧的消息构造。我的做法是:先在 Android 侧用 ASAN 定位到具体哪一行解析代码出问题,然后对照 QNX 侧的消息定义,确认是格式不匹配还是真的越界。如果是格式问题,改 QNX 侧的消息构造;如果是越界,改 Android 侧的解析逻辑。

从那以后我每次给车机双系统开 ASAN,都会先在 QNX 侧和 Android 侧的通信边界加一层日志,确认消息格式对齐后再开 ASAN,不然 ASAN 报的错可能把你带到沟里。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 13:26:13

open-code-review:基于CLI与LLM Agent的可落地代码审查新范式

1. 这不是又一个“代码审查工具”&#xff0c;而是一套可落地的开源协作新范式“open-code-review”这个词&#xff0c;最近在技术社区里出现频率越来越高&#xff0c;但它绝不是简单地把“code review”四个字母前面加个“open”就完事了。我从去年底开始在三个不同规模的团队…

作者头像 李华
网站建设 2026/10/19 13:25:27

PostgreSQL物理存储结构拆解:数据页、文件布局与MVCC实战

1. 整体架构思路&#xff1a;为什么必须先搞清楚物理存储结构 PostgreSQL作为老牌开源关系型数据库&#xff0c;在各类业务系统中被大量使用。很多人聊到PG的优化&#xff0c;开口就是调参数、改配置、上索引&#xff0c;但其实大家都忽略了一个最基础的根基——数据在磁盘上到…

作者头像 李华
网站建设 2026/10/19 13:24:51

开源CLI代码审查工作流:Git原生集成LLM的自动化实践

1. 项目概述&#xff1a;这不是一个“工具”&#xff0c;而是一套可落地的开源代码审查工作流 “open-code-review”这个名称乍看像某个具体软件或GitHub仓库&#xff0c;但结合当前技术生态里高频出现的关键词——CLI、LLM、git、code review——它实际指向一个正在快速成型的…

作者头像 李华
网站建设 2026/10/19 13:24:49

开源CLI代码评审工具:嵌入Git工作流的AI守门人

1. 项目概述&#xff1a;这不是又一个“AI写代码”玩具&#xff0c;而是一套可嵌入日常开发流的开源代码评审工作流“open-code-review”这个名称乍看平平无奇&#xff0c;但拆开来看——open&#xff08;开源、开放协议&#xff09;、code&#xff08;真实工程代码&#xff09…

作者头像 李华
网站建设 2026/10/19 13:24:44

3DGS + 数字人 + AIGC:农文旅数智人才培养实训平台的技术架构拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 13:24:31

快慢指针解决环形链表II:环入口的数学推导与实现

很多人在刷力扣142.环形链表 II这道题的时候&#xff0c;第一反应都是“我开个 Set 把走过的节点存下来不就行了&#xff1f;”&#xff0c;然后兴冲冲地写完一版能过的代码&#xff0c;觉得自己已经搞定了链表环的问题。但面试官往往会在你敲完哈希表解法之后追问一句&#xf…

作者头像 李华