把高通平台的ramdump.bin直接丢给crash工具,回车,然后看着它吐出一堆crash: read error或者WARNING: cannot access vmalloc'd address——这个场景,过去两年里我在8155、8295、8550这几个大平台上遇到过不下十次。网上很多教程把crash解析ramdump说得跟crash vmlinux ramdump.bin一行命令一样简单,实际上真正决定成败的,往往是那些藏在crash初始化流程里的隐藏参数:phys_offset、kaslr_offset、vmalloc_start、RAMDUMP_OFFSET这类。这篇文章就是要把这些参数的作用、获取方式和翻车现场一次讲清楚,主要围绕ARM64平台,尤其是高通新老平台的ramdump解析。如果你在做内核BSP、驱动稳定性调试、或者FAE现场问题分析,这篇文章应该能帮你省下好几个通宵。
1. 拿到ramdump.bin后,别急着敲crash命令
1.1 你拿到的是高通私有dump格式,不是标准ELF core
很多人第一次接触高通ramdump时,下意识把它当成一个普通的内存镜像文件,于是执行crash vmlinux ramdump.bin,结果crash直接报file format not recognized或者干脆乱读一气。这其实不是crash太弱,而是高通平台的ramdump文件格式本身就不是通用ELF core。
高通的ramdump.bin在文件开头有一段平台私有的头信息,里面记录了各个dump segment的加载地址、大小、内存类型等元数据。真正有用的内存数据是从某个偏移之后才开始排列的。上游原版crash工具不认识这套私有头部,所以你必须先经过一层“翻译”:
- 高通release包里的
ramdump_parse.py脚本,可以把带头的ramdump.bin解析成标准ELF core文件; - 高通维护的crash分支(一般从codeaurora或qcom release里带出来)内置了ramdump解析能力,能直接吃原始ramdump;
- 也可以用手工方式读取头部segment表,按加载地址逐个
dd出来再拼接成内存镜像,但这种方式只适合在没有脚本的紧急情况下用。
以线上release里最常见的做法为例,先用高通的转换脚本拿到core文件:
# 高通平台常用的转换示例,实际脚本名/参数以release包为准 python ramdump_parse.py -i ramdump.bin -o ramdump.elf --vmlinux vmlinux转换完再交给crash:
crash vmlinux ramdump.elf如果你拿到的是高通crash分支,也可以直接:
crash --ramdump vmlinux ramdump.bin这一步的核心是:先确认你的工具链到底支不支持这份ramdump格式,不要拿着任何一份文件都硬上。
1.2 符号“三件套”:vmlinux、System.map、kallsyms各管一摊
crash工具解析内核崩溃转储,本质上是把内存里的数据跟符号表做映射。ARM64平台这一套尤其依赖符号文件的质量,所以先说清楚这三样东西的定位:
| 文件 | 作用 | 能否替代其他 |
|---|---|---|
| vmlinux | 未压缩的内核镜像,包含符号表、调试段、BSS段布局信息 | 是crash的主依赖,不可替代 |
| System.map | 纯文本符号地址表,只提供地址到名称的映射 | 不能替代vmlinux,无法提供结构体布局 |
| kallsyms | 内核运行时展开的符号表,通常残留在ramdump内存里 | 可以作为偏移修正的参考 |
实际使用中,vmlinux是必须的。而且这个vmlinux必须和出问题的内核是同一次编译出来的,连编译时间对不上都可能导致符号行号错位。System.map虽然也能单独加载进crash,但它没有类型信息,你想用struct task_struct去读一个进程结构体,没有vmlinux是读不出来的。
如果你拿到手的是一个Image.gz或者Image,先别高兴,这些是自解压镜像,不是最终带符号表的vmlinux。真正要的是编译目录下那个ELF格式的vmlinux,或者从编译产物里找到带CONFIG_DEBUG_INFO的那个版本。检查一下:
file vmlinux # 期望输出类似:ELF 64-bit LSB executable, ARM aarch64, ...如果这个文件输出是MSB或者not stripped都没有,那就趁早回去找编译机吧。我在项目里见过几次用zImage硬加载crash的,crash倒是没拒,但符号全是错的,后面分析完全没法看。
2. 隐藏参数之一:phys_offset——crash找不到内存的根源
2.1 crash在ARM64上是怎么找物理内存的
ARM64 Linux的地址转换关系可以用一个粗略公式表达:
虚拟地址 = 物理地址 - PHYS_OFFSET + PAGE_OFFSETcrash在初始化阶段,需要建立“虚拟地址→ramdump里物理内存数据”的映射关系。对于标准kdump转储,ELF core文件头里的program header会直接标出每个内存段的物理加载地址和虚拟地址,crash照着读就行。但ramdump不是这样,它提供的往往是原始物理内存内容,甚至是从某个固定物理地址开始的一段连续数据,没有ELF头那样丰富的信息。
这时crash需要知道一个关键值:物理内存的起始地址,也就是phys_offset。如果你不告诉它,它会尝试从vmlinux内部的某些符号、mem_map数组或启动信息里去猜。一旦猜错,后面所有符号地址的换算全都会偏,表现出来就是:
bt回溯栈时,函数名要么全是unknown,要么地址看起来“很合理但明显不对”;- 用
struct命令读结构体,出来的字段值要么全0,要么是0xdeadd00d这类垃圾值; log命令读出来的dmesg时间线断断续续,甚至乱码。
我见过最坑的一次是:符号地址解析出来看起来完全正常,栈回溯的前几帧也像模像样,但读到第5层之后全部进入了一个不存在的地址区间。折腾了两小时才发现是phys_offset偏了4KB,导致栈上的某个链表指针被错位解读。
2.2 怎么把正确的phys_offset找出来
获取phys_offset有几种途径,按可靠性排序:
方法一:从dmesg启动日志里直接读
如果设备还能启动,或者ramdump里的log段是完整的,可以这样找:
dmesg | grep -i "memory" # 或者 dmesg | grep -i "Physical memory map"有时候启动日志会有这样一行:
Memory: 6230016K/8388608K available (14336K kernel code, ...)这只是总容量。更直接的是看/proc/iomem,第一行通常就是:
00000000-7fffffff : System RAM这种情况下phys_offset就是0x00000000。但高通平台未必都从0开始,有些平台DDR基地址是0x80000000之类,所以一定要以实际平台为准。
方法二:从vmlinux的ELF段里推算
用readelf看vmlinux的LOAD段,能拿到内核镜像链接期认为的物理加载地址:
readelf -l vmlinux | grep LOAD对比-m参数里想设置的phys_offset,看看推算出的虚拟地址段是否和vmlinux符号表里的_text地址吻合。这个方法适合手头没有dmesg的离线场景。
方法三:从ramdump内容里反推
这种方法最土但也最常救命。ARM64内核镜像的头部有固定的魔数字节(MZ或ARM\x64),你在ramdump里搜索这个特征,找到内核镜像在物理内存里的实际加载位置,再对比vmlinux里_text的链接虚拟地址,就能算出物理偏移。
我习惯先跑一两条命令做粗定位:
# 在ramdump里搜索ARM64镜像头部特征 grep -abo $'\x41\x52\x4d\x64' ramdump.bin | head -20一旦找到特征偏移,再用dd把对应字节抠出来看,或者直接在crash里配合rd验证。这种方法在KASLR场景下也同样有效,因为虽然虚拟地址随机了,但内核镜像在物理内存里的存放位置还是有规律可循的。
2.3 这个参数最容易翻车的地方
phys_offset的坑不在于“不知道原理”,而在于“你以为你设对了”。有个细节经常被忽略:高通的ramdump头部里,每个segment标出的地址可能已经包含了phys_offset的信息,也可能只给了一个相对DDR基址的偏移。
具体来说,有些平台的ramdump解析工具会在转换时自动把物理基地址加进segment地址里,导致转换出的ELF core里的物理地址范围和你手动传入的phys_offset叠加,出现“二次偏移”。我自己的习惯是:在转换之前先解析一遍ramdump头部,把segment表导出来看一遍,确认每个段的地址范围是物理地址还是相对偏移,再决定后续要传什么参数。
另外,不同平台DDR基址可能不一样,别把8155上拿到的phys_offset直接套到8550(kalama)上。高通的SoC迭代很频繁,每次新平台bringup时都要重新确认一次这个值。
3. 隐藏参数之二:KASLR偏移——所有符号地址都在撒谎
3.1 KASLR开启后,vmlinux里的符号地址已经不作数了
KASLR(内核地址空间布局随机化)在ARM64平台默认是开启的,至少在较新的内核版本里是这样。它会把内核镜像整体加载到一个随机化的虚拟地址上,而不是vmlinux链接脚本里写死的那个固定地址。
这意味着,vmlinux文件里_text符号写的是0xffff000010080000,但实际运行时它可能被加载到了0xffff800010080000或者别的什么位置。两者之间差的那个值,就是KASLR偏移。crash工具在解析ramdump时如果拿不到这个偏移,它按vmlinux里的链接地址去算,找到的内存位置自然就是错的。
有人会问:为什么crash直接从ramdump里找_text的实际地址不行?理论上它可以,通过kallsyms或者通过搜索内核镜像特征可以做到,但实际初始化时受限于ramdump内容的完整性和可读性,crash经常无法自动推导。所以这个偏移往往需要人工确认后传进去。
怎么知道内核开没开KASLR?看启动cmdline:
cat /proc/cmdline如果里面有nokaslr,说明关闭了;否则大概率开着。另外,dmesg里如果有Kernel Offset这一行,也说明KASLR生效了。
3.2 从ramdump和log里反推KASLR偏移
方法一:直接抄log里的Kernel Offset
这是最省事的方式。在能开机的情况下,开机完成后执行:
dmesg | grep "Kernel Offset"输出大概是这样的:
Kernel Offset: 0x44000000 from 0xffff000010000000 (relocation range: 0xffff000010000000-0xffff0000bfffffff)这个0x44000000就是KASLR偏移。如果你已经有crash在分析一个设备崩溃,设备开机期间没抓到这条log,那可以从ramdump里的log_buf里找。不过log_buf本身的地址也是受KASLR影响的,这就有点鸡生蛋蛋生鸡。
方法二:对比kallsyms和vmlinux符号
如果ramdump里的kallsyms数据可以访问,你可以找出某个符号在运行时的实际地址,然后和vmlinux里该符号的链接地址做差:
grep " _text" /proc/kallsyms # 设备上执行,如果权限允许如果没法在设备上执行,可以从ramdump里直接搜索符号名对应的地址数据,但这个过程比较手工,适合脚本化处理。
方法三:从内核镜像头的特征推算
这种方法跟前文说phys_offset的方法类似:在ramdump里找到ARM64内核镜像的头部魔数,确认镜像实际加载的物理位置,再对比vmlinux里_text的链接地址和链接物理地址,算出虚拟偏移。这种方法离线也能做,而且KASLR偏移、phys_offset可以一起推算出来,互相印证。
3.3 把KASLR偏移应用到crash
crash工具传入KASLR偏移的参数在不同版本里不一样,有的用-m kaslr_offset=0x...,有的通过--kaslr参数直接传,高通crash分支可能还支持其他扩展名称。我建议先跑一下crash -m help,确认当前版本支持哪些参数名。
一个典型的加载命令长这样:
crash vmlinux ramdump.elf -m kaslr_offset=0x44000000 -m phys_offset=0x80000000加载成功后,用mach命令检查:
crash> mach重点看KERNELOFFSET字段是不是你传的值,以及PAGE_OFFSET、VMALLOC_START等是否合理。如果KERNELOFFSET显示不对,你传参的方式可能不对,或者参数命名不对,先查版本。
验证符号是否对位,我习惯用这种方式:
crash> sym _text crash> sym _stext crash> sym init_task如果这几个符号地址落在预期范围内(比如0xffff...高地址段),而且bt能看到人类可读的函数名,说明符号基本对位了。
3.4 顺带把printk/log_buf偏移的坑也讲一下
很多人在crash里敲log命令,期望看到完整的崩溃前内核日志,有时候却只看到一部分,或者日志时间线从某个点开始就乱掉了。这往往也是KASLR偏移没有完全处理好的表现。
log命令读取的是__log_buf里的环形缓冲区,如果__log_buf的地址因为KASLR偏移没设对而解析错了,crash就会从一个错误的内存地址读数据。读取出来的内容有时候恰好是内存中的其他数据,看起来像日志但根本不是完整的时间线。
处理方式还是先确认KASLR偏移正确,然后用:
crash> log -T-T会打印带时间戳的日志行,如果时间戳连续、环形缓冲区首尾衔接正常,说明log读取是可靠的。如果日志还是不对,我偶尔会用rd直接去看__log_buf符号指向的内存:
crash> p (char *)__log_buf crash> rd __log_buf -p但这只能作为辅助验证手段,核心还是把KASLR偏移搞对。
4. 隐藏参数之三:vmalloc区域参数、页表参数和其他标量
4.1 vmalloc_start / vmalloc_end:栈回溯走到模块区就断
ARM64平台的vmalloc区域在PAGE_OFFSET之上、vmemmap之下,模块(第三方内核模块)、vmalloc分配的栈、ioremap内存都集中在这一带。crash初始化时会计算vmalloc区间的起止地址,用于判断某个虚拟地址是否落在可访问的内存区间内。
如果这个区间设置不正确,最典型的表现是:栈回溯的早期帧都在内核image区域,走到某个ko模块的函数时就断了,显示cannot access vmalloc'd address。这是因为crash认为那个地址不属于它的可访问内存范围,实际上数据明明就在ramdump里。
处理方式是在-m参数里显式指定:
-crash vmlinux ramdump.elf -m vmalloc_start=0xffff800080000000 -m vmalloc_end=0xffff8000bfffffff这个值从哪来?最简单的方法是看设备上的/proc/vmallocinfo或者/proc/kallsyms里的vmalloc区域边界,也可以从vmlinux链接脚本里推算。还有一招是从crash的mach命令输出里读取它算出来的VMALLOC范围,如果和实际的差太多,再手动覆盖。
4.2 页表和arm64_kernel_vmalloc_base这类细节
高通平台较新的内核里,CONFIG_ARM64_VA_BITS通常配置为39、48或者52,对应的PAGE_OFFSET、VMALLOC_START也会不一样。crash在解析arm64内核时,会尝试从vmlinux的符号和配置里推断这些值,但某些场景下需要手动确认。
我实际遇到过一次:某个平台的ramdump解析出来,整个swapper_pg_dir里的页表项看起来全是对的,但虚拟地址翻译出来的物理地址始终落在ramdump文件范围之外。排查到最后发现,是CONFIG_ARM64_VA_BITS=48的情况下,crash把PAGE_OFFSET算到了4-level页表的默认值,而实际内核启用了5-level页表。这种情况下,光是传phys_offset和kaslr_offset是不够的,还需要确认页表级别和VA_BITS是否匹配。
简单说,arm64_kernel_vmalloc_base、idmap_pg_dir、tramp_pg_dir这些符号在正常加载时不用管,但如果你用vtop命令翻译一个虚拟地址时得到的结果跟ramdump里的数据对不上,就得回头检查页表相关配置了。我的习惯是加载完先随机挑一个内核符号做vtop验证,如果翻译结果落在ramdump实际包含的物理地址范围内,才认为页表层面的设置是OK的。
4.3 RAMDUMP_OFFSET / 二次偏移的混淆陷阱
这部分我想专门提一下,因为很多人在转换脚本里见过--offset或者RAMDUMP_OFFSET之类的变量,但搞不清楚它跟phys_offset的区别,导致参数叠加出错。
phys_offset描述的是DDR物理内存的起始地址,而ramdump解析脚本里的offset,往往指的是“ramdump文件内部数据相对于segment起始位置的偏移”,或者“某些段需要额外平移的偏移量”。这两个概念一个影响地址转换的基地址,一个影响文件数据读取的位置,混在一起很容易出问题。
我的建议是:转换ramdump时,先把header解析出来,把每个segment的加载地址、文件偏移写到一个文本文件里,确认清楚再决定传什么参数。转换后的ELF core如果本身已经把所有segment的物理地址都标对了,那crash这边就不需要再叠加RAMDUMP_OFFSET,只要传phys_offset、kaslr_offset这些关键参数就够了。很多人在crash里反复调不通,其实不是crash的问题,而是转换环节就已经引入了偏移误差。
5. 完整实操:高通8550(kalama)平台ramdump从加载到出栈
5.1 一步一步来:从ramdump.bin到crash>提示符
以kalama(SM8550)平台为例,我整理了一套固定流程,每拿到一个新的ramdump都会按这个顺序走一遍。
第一步,确认ramdump的头信息。高通release包的解析工具里通常会附带一个ramdump_header文件,或者你可以在解析环境里用Python解析头部:
# 示意代码:读取ramdump头部segment表信息 import struct with open("ramdump.bin", "rb") as fp: magic = fp.read(16) print("magic:", magic) # 具体字段布局以平台release为准,这里只做思想演示第二步,用高通解析脚本转换成ELF core:
python ramdump_parse.py -i ramdump.bin -o kalama_ramdump.elf --vmlinux vmlinux第三步,确认符号文件和版本一致性。用modinfo和strings交叉验证,甚至直接对比vmlinux和ramdump里提取出的linux_banner字符串:
strings ramdump.bin | grep "Linux version" strings vmlinux | grep "Linux version"这两个字符串里的版本号、编译时间必须完全一致。
第四步,加载crash:
crash vmlinux kalama_ramdump.elf \ -m phys_offset=0x80000000 \ -m kaslr_offset=0x44000000第五步,进crash后先跑一遍基础检查。
| 检查项 | 命令 | 合格标准 |
|---|---|---|
| 架构与偏移 | mach | 显示KERNELOFFSET正确,VA_BITS正确 |
| 符号定位 | sym _stext | 地址在高地址段且和预期一致 |
| 栈回溯 | bt -a(或foreach bt) | 每个CPU都有可读栈回溯 |
| 日志 | log -T | 时间戳连续,内容与设备侧日志吻合 |
5.2 加载成功后先做三件事
第一件事是看mach输出,确认所有内存布局参数都正确。这一步不要省,哪怕你只是换了一个ramdump,参数也可能变了。
第二件事是bt,选一个CPU的栈回溯看是否有符号、是否连续。如果第一帧就是cpu_do_idle或者某个中断处理函数,并且后面的调用链能对应上平台的idle流程,说明基础状态是好的。
第三件事是log,看崩溃前的内核日志是不是完整的。高通的ramdump里一般会包含完整的log_buf,只要KASLR偏移正确,log -T能看到从开机到panic的完整时间线。这一步同时也是对kaslr偏移的二次验证——如果日志开头有乱码或者全是NULL,多半是偏移没设置对。
三件事都通过了,再开始具体的崩溃现场分析。
5.3 排查崩溃时的命令组合
真正分析崩溃原因时,我常用的命令组合是这样:
crash> bt # 当前CPU回溯 crash> foreach bt # 所有CPU回溯,适合死锁/panic多核现场 crash> log -T # 时间线日志 crash> ps -m # 进程列表及内核线程 crash> struct task_struct <addr> -x # 读进程结构体 crash> rd <地址> -p # 物理内存读取 crash> mod -s # 已加载模块列表及符号 crash> sym <函数名> # 符号定位 crash> dis <地址> # 反汇编如果你怀疑崩溃和内存故障相关,可以在设备侧提前用memtester跑一轮内存压力测试,确认硬件层面的DDR是否稳定。这在高通平台问题分析里挺常见——RAM故障和软件错误有时候在crash里的表现几乎一样,都是随机地址的栈回溯和寄存器异常值。高通平台分析内存问题还有一个特点是,ramdump本身如果出现大段全0或者全0xCC的数据,可能就是DDR读回异常,需要跟硬件同事确认采样点。
另外,我在反汇编某些关键代码路径时,会用到qemu-system-aarch64做一个辅助验证环境:把vmlinux丢进qemu启动一个最小系统,把ramdump里的某个内存段load进guest的对应物理地址,然后让crash或者gdb去分析。这样做的目的是绕开某些crash版本对高通私有ramdump格式的兼容性问题,单独验证某段内存内容的语义。虽然操作起来多几步,但在面对“crash说这地址读不到,但ramdump里明明有数据”的情况时,非常管用。
6. 踩坑回顾:最值得写进团队wiki的三条经验
6.1 一定要先对内核版本和编译ID
这是所有坑里最冤枉的。目标设备跑的内核和手头vmlinux根本不是同一个编译产物,轻则符号偏移错误,重则结构体成员完全对不上,甚至崩溃现场的任务列表都是乱的。crash虽然不会主动提示“你的vmlinux和ramdump不匹配”,但只要你做一些深一点的检查,比如看init_task的comm字段或者某个驱动结构的成员值,就会发现明显的错位。
所以我现在拿到ramdump的第一件事就是对比linux_banner字符串和build-id:
readelf -n vmlinux | grep "Build ID"如果vmlinux带build-id,这个值最好和设备的/sys/kernel/notes一致。没有build-id的老内核,至少把版本、编译时间、编译器信息都对一遍。这一步多花一分钟,后面省几小时。
6.2 别迷信“高通开箱即用”的crash分支
高通release包里的crash版本确实能直接解析自家ramdump,但缺点也很明显:版本往往偏老,对较新内核特性的支持滞后。比如某些新平台开启CONFIG_ARM64_VA_BITS=52或者新页表格式后,老版本crash可能计算错误,或者不能识别某些新的section类型。
我的做法是自己从上游编译一份crash作为主力,再保留高通的crash分支做交叉验证。遇到两边行为不一致的情况,通常是上游crash对ramdump格式支持不足或者高通的crash有私有补丁,这时候优先确认内存布局参数而不是急着换工具。
6.3 先解决“能不能读内存”,再分析“符号对不对”
很多新手在crash里遇到符号无法解析,第一反应是“符号文件坏了”,或者“vmlinux不对”。我的排查顺序永远是:
rd一个已知物理地址,确认ramdump数据能否被正确读取;vtop翻译一个内核符号的虚拟地址,看物理地址是否落在ramdump范围内;sym验证符号表是否有解析;- 最后才去纠结构体字段值为什么不对。
这个顺序的核心逻辑是:物理内存都读不到,符号再对都没用。反过来,物理内存能读了,符号错位的问题通常可以通过phys_offset、kaslr_offset这类参数修正。把定位顺序理顺,排障效率会高很多。
我自己现在分析每份高通ramdump之前,都会先写一个固定的加载脚本,把已知的phys_offset、kaslr_offset、vmalloc参数固化进去,同时把ramdump头部信息导出一份存档。每次拿到新平台,第一件事不是找代码问题,而是先更新这个脚本里的平台参数表。等参数表稳定下来,crash分析就是一条流水线,后面再复杂的崩溃现场,只要先能进去,总会找到线索。