【栏目介绍:“玩转OurBMC”是OurBMC社区开创的知识分享类栏目,主要聚焦于社区和BMC全栈技术相关基础知识的分享,全方位涵盖了从理论原理到实践操作的知识传递。OurBMC社区将通过“玩转OurBMC”栏目,帮助开发者们深入了解到社区文化、理念及特色,增进开发者对BMC全栈技术的理解。

欢迎各位关注“玩转OurBMC”栏目,共同探索OurBMC社区的精彩世界。同时,我们诚挚地邀请各位开发者向“玩转OurBMC”栏目投稿,共同学习进步,将栏目打造成为汇聚智慧、激发创意的知识园地。】

在BMC对服务器的监控与管理过程中,为了实现对服务器的更优化管理,往往需要与其它工具紧密协同工作,以增强监控的全面性、提高管理的灵活性,并促进问题的快速解决。本期内容,我们将继续对内核崩溃转储机制kdump进行介绍。主要从kexec以及内核代码层面分析kdump的流程,下图为kdump流程图。

bd4572250753dc8cab297981194e7379.png

内核预留内存

上期内容中,提到内核通过grub的配置参数crashkernel=X@Y的形式将需要保留的内存大小提前预留出来,以便放置捕获内核,系统启动后我们通过dmesg查看相关日志:

02a09db4c15d7a35fdfa35635348e7c8.png

可以看到内核在0x00000000cfa00000 - 0x00000000efa00000这个区间保留512M的物理内存。而对应的函数reserve_crashkernel位于arch/arm64/mm/init.c:

eb31ba4558be9800d8e09a9bbc7594d5.png

Kexe解析/proc/iomem

Kexec用户程序首先会解析/proc/iomem文件,以获取当前系统中物理内存的分配情况,它会查找一个足够大的连续内存区域来放置捕获内核(crash kernel)和initrd。同时,kexec用户程序还会设置哪些内容需要在内核崩溃时被dump:

5473959e17245d53c0634d792c2c271e.png

get_memory_ranges函数获取系统中所有的System RAM,排除reserved部分,并将其保存在全局info->memory_range中。

crash_get_memory_ranges函数获取系统中的System RAM,并且排除了Crash kernel部分。

在arm64系统中物理内存从0x00000000_00000000-0xFFFFFFFF_FFFFFFFF,主内存System RAM被分割成多段分布在其中,kexec需要遍历该文件通过回调函数,解析出Crash kernel/System RAM/Kernel code/Kernel data等内容。其中, 除了crash kernel部分, System RAM其余部分都是需要被dump的。如下图所示(内容不全),图中表示有三段System RAM,其中第一段包含了内核代码段,数据段,以及reserved保留,crash kernel占用部分。

b48988859b9371aeb1b7aaf4bd9aa003.png
/proc/iomem布局示意图

根据当前系统将统计如下需要dump的内存:

b0b4ba0ffb57de5a5351aa344ebc9d6a.png

kexec_load系统调用

内核定义了两个和kexec相关的系统调用系统调用,在arm64平台下其调用号通常是通用的:

07e0c9154fa6331239fa9b6a0b9379b5.png

用户空间的kexec通过kexec_load/kexec_file_load系统调用将准备的数据传递到内核。Kexec-tools/kexec/kexec-syscall.h:

fb860f5bee6a802c773fbcc3d2f644c5.png

在默认情况下使用的是kexec_load,该函数需要kexec用户准备需要的数据,比如入口地址,kexec_segments等。后者是较新的接口。

内核中通过SYSCALL_DEFINE4宏定义了sys_kexec_load函数,在kernel/kexec.c中:

44a90e6c24fb9bb4c924c28f7c825219.png
0b1814058d9865cda07d9a4c48bcb725.png

do_kexec_load函数会判断kexec传统的flags是否有KEXEC_ON_CRASH标志,该标志表示加载的是一个用于当系统crash后需要重启捕捉内核。Kexec在内核中将信息保存为struct kimage 结构体类型,其中在kexec_core,c中定义了两个指针:

  • struct kimage *kexec_image;
  • struct kimage *kexec_crash_image;

在函数中通过dest_image根据KEXEC_ON_CRASH标志来区别引用。

在do_kexec_load中将用户空间kexec程序传递过来的参数进行处理,kimage_alloc_init会创建kimage,并将nr_segments个struct kexec_segment拷贝到内核空间。

参考Kexec.c 程序中调用kexec_load的参数;

638833eed004f82f840419be5b50fea4.png

其中info是结构体struct kexec_info:

58aa1f05e0100b03e54cb7a6b2b141c9.png

收集kexec_segments

Kexec会将用户传递的内核,initrd等信息存储在kexec_info中的segment中,其中有很多代码都是在处理这部分内容。

27a2021a9112a3c480545f0d55e34343.png

Kexec_segment 结构体中的buf是kexec申请的一段存放实际实际虚拟地址,mem是这些数据对应需要放置在物理内存地址。将由kexec传递到内核中,由内核处理,将buf指向的内容复制到mem对应的物理内存中。并且收集的这些内容最终所在的物理内存地址范围在/proc/iomem中的Crash kernel范围内:

05bddf2e389a5206bba8b649b6a5e0f7.png

my_load函数作为主要的函数入口来跟踪分析,该函数首先将crash kernel 调用slurp_decompress_file解压到kernel_buf中,arm64平台下的内核文件有多种文件格式(vmlinx,Image,uImage,zImage),在kexec/arch/arm64/kexec-arm64.c中定义了file_type数组,其中分别对应不同格式内核,在my_load中会进行probe和load调用,这些函数的实现在不同的arch目录下。

033af38fa9ed1bd6bfebbaaff9dee536.png

在arm64下,我们测试的内核是vmlinuz文件属于Image格式,因此对应的load函数为image_arm64_load函数。

4502aed33d9d39d0df739ea4b6b22feb.png

  • load_crashdump_segments: 加载elcore segment
  • arm64_load_other_segments: 加载initrd 、dtb,purgatory sengment。
  • add_segment_phys_virt: 加载kernel segment

通过GDB调试打印在my_load中打印info内容,可知nr_segments为5:

78d7bd6537fd4c4f2902e7b8b8d698b0.png

5个segment分别是:ELF core header segment,kernel segment,initrd segment,dtb segment,purgatory segment。
下面,分别介绍这几个segment:

  • ELF core header segment

功能: 为kdump生成vmcore准备ELF core header

构造ELF core header segment时, 主要构造了ELF header和program header,参考函数load_crashdump_segment()。

其中, program header包含了PT_NOTE和PT_LOAD两类

0d9ea06914fa8350233e3bc0a78f0e41.png

构造结束后, 将ELF core header的起始地址保存到elfcorehdr_mem。这部分内容将放置在crash kernel的末尾。

在load_crashdump_segment中会解析/proc/iomem内容,后期会将System RAM范围内的除Crash kernel标记的范围内存内容作为vmcore文件导出。

在arm64下通过调用crash_create_elf64_headers函数,即在kexec/crashdump-elf.c中定义的FUN函数将每个CPU的crash_note和crash_note_size信息(/sys/devices/system/cpu/cpu#/crash_notes,crash_note_size)以及vmcoreinfo信息(/sys/kernel/vmcoreinfo)(这些内容是当前normal kernel在内存中申请的内容,在系统panic后按照一定的流程填充所需要的内容,比如寄存器),和前面解析来的system ram等内容组织成ELF格式的文件,类型为ET_CORE。上述内容将作为elf文件的program header(Elf64_Phdr)的形式组织。当生成vmcore之后可以根据这些信息找到具体的内容。

  • kernel segment

功能: 读取kexec运行时指定的kernel image:

371edd982294a206f913f87843bb6812.png

kernel_buf是用户空间的加压处理的内核数据, 在kexec_segment中的mem值kernel_segment + arm64_mem.text_offset 。

  • initrd segment

功能: 读取kexec运行时指定的initrd

  • dtb segment

功能: 读取kexec运行时指定的dtb

设置属性

“linux,elfcorehdr”: 将ELF core header地址(elfcorehdr_mem)设置到此属性

“linux,usable-memory-range”: 将crash_reserved_mem设置到此属性(指定capture kernel的总内存大小)

crash kernel启动后查看/sys/firmware/fdt,用fdtdump工具可以查看其原始内容:

6169adbd100bd06ec97d13969ee0aa9e.png

arch/arm64/mm/init.c中early_init_dt_scan_elfcorehdr函数会解析”linux,elfcorehdr”字段,将设置elfcorehdr_addr和elfcorehdr_size的值,内核中用来判断 当前内核是否为crash kernel的函数is_kdump_kernel() 就是用该值来判断:

8f167fcfb6d1c13695b329525c61f119.png

  • purgatory segment

功能: 用于完成crash kernel完整性校验和kernel跳转,在arm64_load_other_segments中完成。

主要流程如下:

  1. purgatory相关源文件生成purgatory.ro
  2. bin-to-hex将purgatory.ro转换成purgatory.c(ELF内容格式到purgatory[]数组)
  3. purgatory.c和其它源文件编译生成kexec可执行程序
  4. kexec在运行过程中, 构造purgatory可重定位对象,放到purgatory buffer

下表为purgatory.ro中重要的三个符号:

4f4832e7659699905d7a685a663e845a.png

最终收集的5个segment:左侧为kexec准备的5个segment,buffer指向用户空间申请的内存和实际的数据,右侧是kexec通过系统调用后,内核将通过buffer拷贝到对应的内存位置,根据代码分析到elfcore处于crash预留内存的末端,kernel,initrd,dtb,purgat依次从前往后,kernel前预留部分。

d231d955d8646c5ef213b7b9cc24617b.png
info.segment 布局图

读取vmcore

本质上内核通过/proc/vmcore文件提供给用户读取数据,是提供了一个read数据的接口,虽然通过ls查看该文件内容和内存大小一致,实际上并不会准备一个实际的文件,不会占用内存。/proc/vmcore是在fs/proc./vmcore.c实现的。入口函数vmcore_init:

002833343423aedd4a1fd7bcfb8f0cae.jpg
b89899f3e448ff5c7c457c0b79122235.jpg

Is_vmcore_usable判断当前启动的内核是否为crash kernel。通过判断elfcorehdr_addr地址是否有效,前面分析过当crash kernel启动的时候会通过dtb传递给内核,parse_crash_elf_header会将elfcore segment解析,elfcore本质上是一个elf文件。其中包含PT_NOTE以及PT_LOAD头部信息,PT_NOTE用来收集每个cpu的寄存器信息,PT_LOAD用来收集所有System RAM内存。

调用merge_note_headers_elf64函数将所有PT_NOTE的program header合并成一个数据存放到elfnotes_buf中。

调用process_ptload_program_headers_elf64将所有PT_LOAD的program header 加入到vmcore_list,结构体vmcore中记录了某段System RAM内存的地址信息。

image.png

最终组织形式:

442f74113ca4ea158ff39a55e3d8e616.png
vmcore_init整理数据

用户读取vmcore文件时,通过read_vmcore接口获取数据,调用关系:proc_vmcore_operations->read_vmcore->__read_vmcore;

e47305b6a145358e31e888a91b0410d8.png
0c5000f5a797856809c74099b05a79e0.jpg
fb6523ef927df6bbdd85abc53d3210b3.png

该函数将分三部分进行数据的收集,第一部分为elcorebuf 的ELF数据头,第二部分是elfnotes_buf,最后是vmcore_list中的system ram部分。

通过file工具查看文件:

383d27c04d1a1d5050bbb2f426304b59.png

通过readelf 工具查看其中的program header信息,其中包含一个PT_NOTE段和多个PT_LOAD段。

4e766c2351402babb6da2817933a325f.png
d09e41d0d3b410e603eb961cbd9c00d4.png
c9b72a0affeb598251f67ab79550a593.jpg
03637491bb4fa66b0a929d128029d15e.png

为每个CPU保存寄存器信息

内核发送crash时,会将每个CPU核当前的寄存器信息保存到PT_NOTE中所指定的内存,正常内核启动后有per_cpu指针变量crash_notes在crash_notes_memory_init初始化是分配空间,地址信息kexec会收集到elfcore segment。

具体流程内核panic后会触发__kernel_crash()->machine_crash_shutdown()(kernel/kexec_core.c)-crash_smp_send_stop->handle_IPI->ipi_cpu_crash_stop->crash_save_cpu。

90ea417ea7c417d3520be709c245e7dc.png
58f8b7f9fb6cce0e4d3849b23fb3a5f3.png
3a732ae088b8e10b245829eec0c04dc3.jpg
ff3c2ecd280078c57b9080c6ec02de3c.jpg
ff3c2ecd280078c57b9080c6ec02de3c.jpg

smp_cross_call调用driver/irqchip/irq-gic-phytium-2500.c提供的gic_raise_softirq函数(gic_smp_init调用set_smp_cross_call)。该函数通过ipi发送核间中断通知其它CPU。发送类型IPI_CPU_CRASH_STOP ,最终调用对应的函数handle_IPI。

470e5b76773da57a70bd3c6fb4414a0d.jpg
205af99d5d167eb13b3668614e30b307.jpg

crash_save_cpu函数寄存器参数struct pt_regs *regs,寄存器的信息和不同的平台相关,linux内使用结构体struct pt_regs来通用表示,具体的内容在平台目录下。寄存器保存的最佳时机是异常处理过程中,arch/arm64/kernel/entry.S内vectors是arm64在EL1级使用的异常向量表入口。

本期内容为大家进行Kdump详解。通过Kdump,Linux系统管理员和开发者可以获得强大的工具来诊断和解决系统稳定性问题,从而提高系统的可靠性和安全性。

**欢迎大家关注OurBMC社区,了解更多BMC技术干货。
OurBMC社区官方网站:**
https://www.ourbmc.cn/


OurBMC
28 声望14 粉丝

OurBMC社区是由基础软硬件企业、第三方机构、高等院校、个人开发者等各方共同参与建设的开源社区,社区基于开放、平等、协作、创新的基本原则,携手社区成员,共同构建自主、先进、软硬一体的BMC技术全栈,共同推...