1. 程式人生 > 其它 >Linux Kdump 機制詳解【轉】

Linux Kdump 機制詳解【轉】

轉自:https://blog.csdn.net/pwl999/article/details/118418242

文章目錄
1. 簡介
1.1 安裝
1.2 觸發 kdump
1.3 除錯 kdump
1.3.1 安裝 debuginfo vmlinux
1.3.2 編譯 kernel
1.4 kdump-tools.service 流程分析
2. 原理分析
2.1 elf core 檔案格式
3. `/proc/kcore`
3.1 準備資料
3.2 讀取 elf core
4. `/proc/vmcore`
4.1 準備 elf header (執行在 normal kernel)
4.1.1 crash_notes 資料的更新
4.1.2 vmcoreinfo_note 資料的更新
4.2 準備 cmdline (執行在 normal kernel)
4.3 啟動 crash kernel (執行在 normal kernel)
4.4 接收 elfheadr (執行在 crash kernel)
4.5 解析整理 elfheadr (執行在 crash kernel)
4.6 讀取 elf core (執行在 crash kernel)
參考資料
1. 簡介
Kdump 提供了一種機制在核心出現故障的時候把系統的所有記憶體資訊和暫存器資訊 dump 出來成一個檔案,後續通過 gdb/crash 等工具進行分析和除錯。和使用者態程式的 coredump 機制類似。它的主要流程如下圖所示:


可以看到它的核心原理是保留一段記憶體並且預先載入了一個備用的 kernel,在主 kernel 出現故障時跳轉到備用 kernel,在備用 kernel 中把主 kernel 使用的記憶體和發生故障時的暫存器資訊 dump 到一個磁碟檔案中供後續分析。這個檔案的格式是 elf core 檔案格式。

kdump 主要還是用來捕捉純軟體的故障,在嵌入式領域還需要加上對硬體故障的捕捉,仿照其原理並進行加強和改造,就能構造出自己的 coredump 機制。

下面就來詳細的分析整個 kdump 機制的詳細原理。

1.1 安裝
之前的 kdump 安裝需要手工的一個個安裝 kexec-tools、kdump-tools、crash,手工配置 grub cmdline 引數。在現在的 ubuntu 中只需要安裝一個 linux-crashdump 軟體包就自動幫你搞定:

$ sudo apt-get install linux-crashdump
1
安裝完後,可以通過 kdump-config 命令檢查系統是否配置正確:

$ kdump-config show
DUMP_MODE: kdump
USE_KDUMP: 1
KDUMP_SYSCTL: kernel.panic_on_oops=1
KDUMP_COREDIR: /var/crash // kdump 檔案的儲存目錄
crashkernel addr: 0x
/var/lib/kdump/vmlinuz: symbolic link to /boot/vmlinuz-5.8.18+
kdump initrd:
/var/lib/kdump/initrd.img: symbolic link to /var/lib/kdump/initrd.img-5.8.18+
current state: ready to kdump // 顯示 ready 狀態,說明系統 kdmup 機制已經準備就緒

kexec command:
/sbin/kexec -p --command-line="BOOT_IMAGE=/boot/vmlinuz-5.8.18+ root=UUID=9ee42fe2-4e73-4703-8b6d-bb238ffdb003 ro find_preseed=/preseed.cfg auto noprompt priority=critical locale=en_US quiet reset_devices systemd.unit=kdump-tools-dump.service nr_cpus=1 irqpoll nousb ata_piix.prefer_ms_hyperv=0" --initrd=/var/lib/kdump/initrd.img /var/lib/kdump/vmlinuz

linux-crashdump 的本質還是由一個個分離的軟體包組成的:

$ sudo apt-get install linux-crashdump -d
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
crash efibootmgr grub-common grub-efi-arm64 grub-efi-arm64-bin
grub-efi-arm64-signed grub2-common kdump-tools kexec-tools libfreetype6
libsnappy1v5 makedumpfile os-prober
Suggested packages:
multiboot-doc xorriso desktop-base
Recommended packages:
secureboot-db
The following NEW packages will be installed:
crash efibootmgr grub-common grub-efi-arm64 grub-efi-arm64-bin
grub-efi-arm64-signed grub2-common kdump-tools kexec-tools libfreetype6
libsnappy1v5 linux-crashdump makedumpfile os-prober
0 upgraded, 14 newly installed, 0 to remove and 67 not upgraded.
Need to get 6611 kB of archives.

1.2 觸發 kdump
在 kdump 就緒以後我們手工觸發一次 panic :

$ sudo bash
# echo c > /proc/sysrq-trigger

在系統 kdump 完成,重新啟動以後。我們在 /var/crash 目錄下可以找到 kdump 生成的記憶體轉儲存檔案:

$ ls -l /var/crash/202107011353/
total 65324
-rw------- 1 root whoopsie 119480 Jul 1 13:53 dmesg.202107011353 // 系統 kernel log 資訊
-rw------- 1 root whoopsie 66766582 Jul 1 13:53 dump.202107011353 // 記憶體轉儲存檔案,壓縮格式
$ sudo file /var/crash/202107011353/dump.202107011353
/var/crash/202107011353/dump.202107011353: Kdump compressed dump v6, system Linux, node ubuntu, release 5.8.18+, version #18 SMP Thu Jul 1 11:24:39 CST 2021, machine x86_64, domain (none)

預設生成的 dump 檔案是經過 makedumpfile 壓縮過的,或者我們修改一些配置生成原始的 elf core 檔案:

$ ls -l /var/crash/202107011132/
total 1785584
-rw------- 1 root whoopsie 117052 Jul 1 11:32 dmesg.202107011132 // 系統 kernel log 資訊
-r-----r-- 1 root whoopsie 1979371520 Jul 1 11:32 vmcore.202107011132 // 記憶體轉儲存檔案,原始 Elf 格式
$ file /var/crash/202107011132/vmcore.202107011132
/var/crash/202107011132/vmcore.202107011132: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style

1.3 除錯 kdump
使用 crash 工具可以很方便對 kdump 檔案進行分析, crash 是對 gdb 進行了一些包裝,生成了更多的除錯核心的快捷命令。同樣可以利用 gdb 和 trace32 工具進行分析。

$ sudo crash /usr/lib/debug/boot/vmlinux-5.8.0-43-generic /var/crash/202106170338/dump.202106170338
1
值得注意的是,除錯需要帶 debuginfo 資訊的 vmlinux 檔案,需要額外安裝。

1.3.1 安裝 debuginfo vmlinux
參考ubuntu文件 How to use linux-crashdump to capture a kernel oops/panic 進行安裝:

// 新增 debuginfo 包源倉庫
$ sudo tee /etc/apt/sources.list.d/ddebs.list << EOF
deb http://ddebs.ubuntu.com/ $(lsb_release -cs) main restricted universe multiverse
deb http://ddebs.ubuntu.com/ $(lsb_release -cs)-security main restricted universe multiverse
deb http://ddebs.ubuntu.com/ $(lsb_release -cs)-updates main restricted universe multiverse
deb http://ddebs.ubuntu.com/ $(lsb_release -cs)-proposed main restricted universe multiverse
EOF

$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys ECDCAD72428D7C01
$ sudo apt-get update
$ sudo apt-get install linux-image-$(uname -r)-dbgsym

1.3.2 編譯 kernel
如果找不到帶 debuginfo 資訊的 vmlinux 檔案,也可以自己編譯核心來進行除錯。

1、去掉/etc/apt/sources.list檔案中關於deb-src的註釋,下載當前核心原始碼:
$ sudo apt-get update
$ sudo apt-get source linux-image-unsigned-$(uname -r)

2、參考Ubuntu BuildYourOwnKernel安裝相關工具:
$ sudo apt-get build-dep linux linux-image-$(uname -r)
$ sudo apt-get install libncurses-dev gawk flex bison openssl libssl-dev dkms libelf-dev libudev-dev libpci-dev libiberty-dev autoconf

3、核心編譯和安裝:
可以參考Ubuntu BuildYourOwnKernel中使用debian/rules的方式進行核心編譯和打包。也可以使用以下的簡便方式來進行編譯安裝:

// 編譯
$ make menuconfig
$ make bzImage modules
// 安裝
$ make INSTALL_MOD_STRIP=1 modules_install
$ sudo mkinitramfs /lib/modules/4.14.134+ -o /boot/initrd.img-4.14.134-xenomai
$ sudo cp arch/x86/boot/bzImage /boot/vmlinuz-4.14.134-xenomai
$ sudo cp System.map /boot/System.map-4.14.134-xenomai
$ sudo update-grub2

1.4 kdump-tools.service 流程分析
在前面我們說過可以把 kdump 預設的壓縮格式改成原生 ELF Core 檔案格式,本節我們就來實現這個需求。

把/proc/vmcore檔案從記憶體拷貝到磁碟是 crash kernel 中的 kdump-tools.service 服務完成的,我們來詳細分析一下其中的流程:

1、首先從 kdump-config 配置中可以看到,第二份 crash kernel 啟動後 systemd 只需要啟動一個服務 kdump-tools-dump.service:
# kdump-config show
DUMP_MODE: kdump
USE_KDUMP: 1
KDUMP_SYSCTL: kernel.panic_on_oops=1
KDUMP_COREDIR: /var/crash
crashkernel addr: 0x73000000
/var/lib/kdump/vmlinuz: symbolic link to /boot/vmlinuz-5.8.0-43-generic
kdump initrd:
/var/lib/kdump/initrd.img: symbolic link to /var/lib/kdump/initrd.img-5.8.0-43-generic
current state: ready to kdump

kexec command:
/sbin/kexec -p --command-line="BOOT_IMAGE=/boot/vmlinuz-5.8.0-43-generic root=UUID=9ee42fe2-4e73-4703-8b6d-bb238ffdb003 ro find_preseed=/preseed.cfg auto noprompt priority=critical locale=en_US quiet reset_devices systemd.unit=kdump-tools-dump.service nr_cpus=1 irqpoll nousb ata_piix.prefer_ms_hyperv=0" --initrd=/var/lib/kdump/initrd.img /var/lib/kdump/vmlinuz

2、kdump-tools-dump.service 服務本質是呼叫 kdump-tools start 指令碼:
# systemctl cat kdump-tools-dump.service
# /lib/systemd/system/kdump-tools-dump.service
[Unit]
Description=Kernel crash dump capture service
Wants=network-online.target dbus.socket systemd-resolved.service
After=network-online.target dbus.socket systemd-resolved.service

[Service]
Type=oneshot
StandardOutput=syslog+console
EnvironmentFile=/etc/default/kdump-tools
ExecStart=/etc/init.d/kdump-tools start
ExecStop=/etc/init.d/kdump-tools stop
RemainAfterExit=yes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
3、kdump-tools 呼叫了 kdump-config savecore:
# vim /etc/init.d/kdump-tools

KDUMP_SCRIPT=/usr/sbin/kdump-config

echo -n "Starting $DESC: "
$KDUMP_SCRIPT savecore

4、kdump-config 呼叫了 makedumpfile -c -d 31 /proc/vmcore dump.xxxxxx:
MAKEDUMP_ARGS=${MAKEDUMP_ARGS:="-c -d 31"}
vmcore_file=/proc/vmcore

makedumpfile $MAKEDUMP_ARGS $vmcore_file $KDUMP_CORETEMP

kdump-tools-dump.service 預設呼叫 makedumpfile 生成壓縮的 dump 檔案。但是我們想分析原始的 elf 格式的 vmcore 檔案,怎麼辦?

4.1、首先我們修改 /usr/sbin/kdump-config 檔案中的 MAKEDUMP_ARGS 引數讓其出錯。
MAKEDUMP_ARGS=${MAKEDUMP_ARGS:="-xxxxx -c -d 31"} // 其中 -xxxxx 是隨便加的選項

4.2、然後 kdump-config 就會呼叫 cp /proc/vmcore vmcore.xxxxxx 命令來生成原始 elf 格式的 vmcore 檔案了
log_action_msg "running makedumpfile $MAKEDUMP_ARGS $vmcore_file $KDUMP_CORETEMP"
makedumpfile $MAKEDUMP_ARGS $vmcore_file $KDUMP_CORETEMP // 先呼叫 makedumpfile 生成壓縮格式的 dump 檔案
ERROR=$?
if [ $ERROR -ne 0 ] ; then // 如果 makedumpfile 呼叫失敗
log_failure_msg "$NAME: makedumpfile failed, falling back to 'cp'"
logger -t $NAME "makedumpfile failed, falling back to 'cp'"
KDUMP_CORETEMP="$KDUMP_STAMPDIR/vmcore-incomplete"
KDUMP_COREFILE="$KDUMP_STAMPDIR/vmcore.$KDUMP_STAMP"
cp $vmcore_file $KDUMP_CORETEMP // 再嘗試使用 cp 拷貝原始的 vmcore elf 檔案
ERROR=$?
fi

2. 原理分析
kexec 實現了crash kernel 的載入。核心分為兩部分:

kexec_file_load()/kexec_load()。負責在起始時就把備份的 kernel 和 initrd 載入好到記憶體。
__crash_kexec()。負責在故障時跳轉到備份 kernel 中。
kdump 主要實現把 vmcore 檔案從記憶體拷貝到磁碟,並進行一些瘦身。

本次並不打算對 kexec 載入核心和地址轉換流程 以及 kdump 的拷貝裁剪 進行詳細的解析,我們只關注其中的兩個重要檔案 /proc/kcore 和 /proc/vmcore。其中:

/proc/kcore。是在 normal kernel 中把 normal kernel 的記憶體模擬成一個 elf core 檔案,可以使用gdb 對當前系統進行線上除錯,因為是自己除錯自己會存在一些限制。
/proc/vmcore。是在 crash kernel 中把 normal kernel 的記憶體模擬成一個 elf core 檔案,因為這時 normal kernel 已經停止執行,所以可以無限制的進行除錯。我們 kdump 最後得到的 dump 檔案,就是把 /proc/vmcore 檔案從記憶體簡單拷貝到了磁碟,或者再加上點裁剪和壓縮。
所以可以看到 /proc/kcore 和 /proc/vmcore 這兩個檔案是整個機制的核心,我們重點分析這兩部分的實現。

2.1 elf core 檔案格式
關於 ELF 檔案格式,我們熟知它有三種格式 .o檔案(ET_REL)、.so檔案(ET_EXEC)、exe檔案(ET_DYN)。但是關於它的第四種格式 core檔案(ET_CORE) 一直很神祕,也很神奇 gdb 一除錯就能恢復到故障現場。

以下是 elf core 檔案的大致格式:


可以看到 elf core 檔案只關注執行是狀態,所以它只有 segment 資訊,沒有 section 資訊。其主要包含兩種型別的 segment 資訊:

1、PT_LOAD。每個 segemnt 用來記錄一段 memory 區域,還記錄了這段 memory 對應的實體地址、虛擬地址和長度。
2、PT_NOTE。這個是 elf core 中新增的 segment,記錄瞭解析 memory 區域的關鍵資訊。PT_NOTE segment 被分成了多個 elf_note結構,其中 NT_PRSTATUS 型別的記錄了復位前 CPU 的暫存器資訊,NT_TASKSTRUCT 記錄了程序的 task_struct 資訊,還有一個最關鍵0型別的自定義 VMCOREINFO 結論記錄了核心的一些關鍵資訊。
elf core 檔案的大部分內容用 PT_LOAD segemnt 來記錄 memeory 資訊,但是怎麼利用這些記憶體資訊的鑰匙記錄在PT_NOTE segemnt 當中。

我們來看一個具體 vmcore 檔案的例子:

1、首先我們查詢 elf header 資訊:
$ sudo readelf -e vmcore.202107011132
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: CORE (Core file) // 可以看到檔案型別是 ET_CORE
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 64 (bytes into file)
Start of section headers: 0 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 6
Size of section headers: 0 (bytes)
Number of section headers: 0
Section header string table index: 0

There are no sections in this file.

// 可以看到包含了 PT_NOTE 和 PT_LOAD 兩種型別的 segment
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
NOTE 0x0000000000001000 0x0000000000000000 0x0000000000000000
0x0000000000001318 0x0000000000001318 0x0
LOAD 0x0000000000003000 0xffffffffb7200000 0x0000000006c00000
0x000000000202c000 0x000000000202c000 RWE 0x0
LOAD 0x000000000202f000 0xffff903a00001000 0x0000000000001000
0x000000000009d800 0x000000000009d800 RWE 0x0
LOAD 0x00000000020cd000 0xffff903a00100000 0x0000000000100000
0x0000000072f00000 0x0000000072f00000 RWE 0x0
LOAD 0x0000000074fcd000 0xffff903a7f000000 0x000000007f000000
0x0000000000ee0000 0x0000000000ee0000 RWE 0x0
LOAD 0x0000000075ead000 0xffff903a7ff00000 0x000000007ff00000
0x0000000000100000 0x0000000000100000 RWE 0x0

2、可以進一步檢視 PT_NOTE 儲存的具體內容:
$ sudo readelf -n vmcore.202107011132

Displaying notes found at file offset 0x00001000 with length 0x00001318:
Owner Data size Description
CORE 0x00000150 NT_PRSTATUS (prstatus structure) // 因為系統有8個CPU,所以儲存了8份 prstatus 資訊
CORE 0x00000150 NT_PRSTATUS (prstatus structure)
CORE 0x00000150 NT_PRSTATUS (prstatus structure)
CORE 0x00000150 NT_PRSTATUS (prstatus structure)
CORE 0x00000150 NT_PRSTATUS (prstatus structure)
CORE 0x00000150 NT_PRSTATUS (prstatus structure)
CORE 0x00000150 NT_PRSTATUS (prstatus structure)
CORE 0x00000150 NT_PRSTATUS (prstatus structure)
VMCOREINFO 0x000007dd Unknown note type: (0x00000000) // 自定義的VMCOREINFO資訊
description data: 4f 53 52 45 4c 45 41 53 45 3d 35 2e 38 2e 31 38 2b 0a 50 41 47 45 53 49 5a 45 3d 34 30 39 36 0a 53 59 4d 42 4f 4c 28 69 6e 69 74 5f 75 74 73 5f 6e 73 29 3d 66 66 66 66 66 66 66 66 ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
3、可以進一步解析 VMCOREINFO 儲存的資訊,description data後面是一段16進位制的碼流轉換以後得到:
OSRELEASE=5.8.0-43-generic
PAGESIZE=4096
SYMBOL(init_uts_ns)=ffffffffa5014620
SYMBOL(node_online_map)=ffffffffa5276720
SYMBOL(swapper_pg_dir)=ffffffffa500a000
SYMBOL(_stext)=ffffffffa3a00000
SYMBOL(vmap_area_list)=ffffffffa50f2560
SYMBOL(mem_section)=ffff91673ffd2000
LENGTH(mem_section)=2048
SIZE(mem_section)=16
OFFSET(mem_section.section_mem_map)=0
SIZE(page)=64
SIZE(pglist_data)=171968
SIZE(zone)=1472
SIZE(free_area)=88
SIZE(list_head)=16
SIZE(nodemask_t)=128
OFFSET(page.flags)=0
OFFSET(page._refcount)=52
OFFSET(page.mapping)=24
OFFSET(page.lru)=8
OFFSET(page._mapcount)=48
OFFSET(page.private)=40
OFFSET(page.compound_dtor)=16
OFFSET(page.compound_order)=17
OFFSET(page.compound_head)=8
OFFSET(pglist_data.node_zones)=0
OFFSET(pglist_data.nr_zones)=171232
OFFSET(pglist_data.node_start_pfn)=171240
OFFSET(pglist_data.node_spanned_pages)=171256
OFFSET(pglist_data.node_id)=171264
OFFSET(zone.free_area)=192
OFFSET(zone.vm_stat)=1280
OFFSET(zone.spanned_pages)=120
OFFSET(free_area.free_list)=0
OFFSET(list_head.next)=0
OFFSET(list_head.prev)=8
OFFSET(vmap_area.va_start)=0
OFFSET(vmap_area.list)=40
LENGTH(zone.free_area)=11
SYMBOL(log_buf)=ffffffffa506a6e0
SYMBOL(log_buf_len)=ffffffffa506a6dc
SYMBOL(log_first_idx)=ffffffffa55f55d8
SYMBOL(clear_idx)=ffffffffa55f55a4
SYMBOL(log_next_idx)=ffffffffa55f55c8
SIZE(printk_log)=16
OFFSET(printk_log.ts_nsec)=0
OFFSET(printk_log.len)=8
OFFSET(printk_log.text_len)=10
OFFSET(printk_log.dict_len)=12
LENGTH(free_area.free_list)=5
NUMBER(NR_FREE_PAGES)=0
NUMBER(PG_lru)=4
NUMBER(PG_private)=13
NUMBER(PG_swapcache)=10
NUMBER(PG_swapbacked)=19
NUMBER(PG_slab)=9
NUMBER(PG_hwpoison)=23
NUMBER(PG_head_mask)=65536
NUMBER(PAGE_BUDDY_MAPCOUNT_VALUE)=-129
NUMBER(HUGETLB_PAGE_DTOR)=2
NUMBER(PAGE_OFFLINE_MAPCOUNT_VALUE)=-257
NUMBER(phys_base)=1073741824
SYMBOL(init_top_pgt)=ffffffffa500a000
NUMBER(pgtable_l5_enabled)=0
SYMBOL(node_data)=ffffffffa5271da0
LENGTH(node_data)=1024
KERNELOFFSET=22a00000
NUMBER(KERNEL_IMAGE_SIZE)=1073741824
NUMBER(sme_mask)=0
CRASHTIME=1623937823

參考資料:
1.Anatomy of an ELF core file
2.Extending the ELF Core Format for Forensics Snapshots
3.ELF Coredump
4.Dumping /proc/kcore in 2019
5.readelf -n

3. /proc/kcore
有些同學在清理磁碟空間的常常會碰到 /proc/kcore 檔案,因為它顯示出來的體積非常的大,有時高達128T。但是實際上她沒有佔用任何磁碟空間,它是一個記憶體檔案系統中的檔案。它也沒有佔用多少記憶體空間,除了一些控制頭部分佔用少量記憶體,大塊的空間都是模擬的,只有在使用者讀操作的時候才會從對應的記憶體空間去讀取的。

上一節已經介紹了 /proc/kcore 是把當前系統的記憶體模擬成一個 elf core 檔案,可以使用gdb 對當前系統進行線上除錯。那本機我們就來看看具體的模擬過程。

3.1 準備資料
初始化就是構建kclist_head連結串列的一個過程,連結串列中每一個成員對應一個 PT_LOAD segment。在讀操作的時候再用elf的PT_LOAD segment 呈現這些成員。

static int __init proc_kcore_init(void)
{
/* (1) 建立 /proc/kcore 檔案 */
proc_root_kcore = proc_create("kcore", S_IRUSR, NULL, &kcore_proc_ops);
if (!proc_root_kcore) {
pr_err("couldn't create /proc/kcore\n");
return 0; /* Always returns 0. */
}
/* Store text area if it's special */
/* (2) 將核心程式碼段 _text 加入kclist_head連結串列,kclist_head連結串列中每一個成員對應一個 PT_LOAD segment */
proc_kcore_text_init();
/* Store vmalloc area */
/* (3) 將 VMALLOC 段記憶體加入kclist_head連結串列 */
kclist_add(&kcore_vmalloc, (void *)VMALLOC_START,
VMALLOC_END - VMALLOC_START, KCORE_VMALLOC);
/* (4) 將 MODULES_VADDR 模組記憶體加入kclist_head連結串列 */
add_modules_range();
/* Store direct-map area from physical memory map */
/* (5) 遍歷系統記憶體佈局表,將有效記憶體加入kclist_head連結串列 */
kcore_update_ram();
register_hotmemory_notifier(&kcore_callback_nb);

return 0;
}

static int kcore_update_ram(void)
{
LIST_HEAD(list);
LIST_HEAD(garbage);
int nphdr;
size_t phdrs_len, notes_len, data_offset;
struct kcore_list *tmp, *pos;
int ret = 0;

down_write(&kclist_lock);
if (!xchg(&kcore_need_update, 0))
goto out;

/* (5.1) 遍歷系統記憶體佈局表,將符合`IORESOURCE_SYSTEM_RAM | IORESOURCE_BUSY`記憶體加入list連結串列 */
ret = kcore_ram_list(&list);
if (ret) {
/* Couldn't get the RAM list, try again next time. */
WRITE_ONCE(kcore_need_update, 1);
list_splice_tail(&list, &garbage);
goto out;
}

/* (5.2) 刪除掉原有 kclist_head 連結串列中的 KCORE_RAM/KCORE_VMEMMAP 區域,因為全域性連結串列中已經覆蓋了 */
list_for_each_entry_safe(pos, tmp, &kclist_head, list) {
if (pos->type == KCORE_RAM || pos->type == KCORE_VMEMMAP)
list_move(&pos->list, &garbage);
}
/* (5.3) 將原有 kclist_head 連結串列 和全域性連結串列 list 拼接到一起 */
list_splice_tail(&list, &kclist_head);

/* (5.4) 更新 kclist_head 連結串列的成員個數,一個成員代表一個 PT_LOAD segment。
計算 PT_NOTE segment 的長度
計算 `/proc/kcore` 檔案的長度,這個長度是個虛值,最大是虛擬地址的最大範圍
*/
proc_root_kcore->size = get_kcore_size(&nphdr, &phdrs_len, &notes_len,
&data_offset);

out:
up_write(&kclist_lock);
/* (5.5) 釋放掉上面刪除的連結串列成員佔用的空間 */
list_for_each_entry_safe(pos, tmp, &garbage, list) {
list_del(&pos->list);
kfree(pos);
}
return ret;
}

其中的一個關鍵從遍歷系統記憶體佈局表,關鍵程式碼如下:

kcore_ram_list() → walk_system_ram_range():

int walk_system_ram_range(unsigned long start_pfn, unsigned long nr_pages,
void *arg, int (*func)(unsigned long, unsigned long, void *))
{
resource_size_t start, end;
unsigned long flags;
struct resource res;
unsigned long pfn, end_pfn;
int ret = -EINVAL;

start = (u64) start_pfn << PAGE_SHIFT;
end = ((u64)(start_pfn + nr_pages) << PAGE_SHIFT) - 1;
/* (5.1.1) 從 iomem_resource 連結串列中查詢符合 IORESOURCE_SYSTEM_RAM | IORESOURCE_BUSY 的資源段 */
flags = IORESOURCE_SYSTEM_RAM | IORESOURCE_BUSY;
while (start < end &&
!find_next_iomem_res(start, end, flags, IORES_DESC_NONE,
false, &res)) {
pfn = PFN_UP(res.start);
end_pfn = PFN_DOWN(res.end + 1);
if (end_pfn > pfn)
ret = (*func)(pfn, end_pfn - pfn, arg);
if (ret)
break;
start = res.end + 1;
}
return ret;
}

其實就相當於以下命令:

$ sudo cat /proc/iomem | grep "System RAM"
00001000-0009e7ff : System RAM
00100000-7fedffff : System RAM
7ff00000-7fffffff : System RAM
1
2
3
4
3.2 讀取 elf core
準備好資料以後,還是在讀 /proc/kcore 檔案時,以 elf core 的格式呈現。

static const struct proc_ops kcore_proc_ops = {
.proc_read = read_kcore,
.proc_open = open_kcore,
.proc_release = release_kcore,
.proc_lseek = default_llseek,
};

static ssize_t
read_kcore(struct file *file, char __user *buffer, size_t buflen, loff_t *fpos)
{
char *buf = file->private_data;
size_t phdrs_offset, notes_offset, data_offset;
size_t phdrs_len, notes_len;
struct kcore_list *m;
size_t tsz;
int nphdr;
unsigned long start;
size_t orig_buflen = buflen;
int ret = 0;

down_read(&kclist_lock);

/* (1) 獲取到PT_LOAD segment個數、PT_NOTE segment 的長度等資訊,開始動態構造 elf core 檔案了 */
get_kcore_size(&nphdr, &phdrs_len, &notes_len, &data_offset);
phdrs_offset = sizeof(struct elfhdr);
notes_offset = phdrs_offset + phdrs_len;

/* ELF file header. */
/* (2) 構造 ELF 檔案頭,並拷貝給給使用者態讀記憶體 */
if (buflen && *fpos < sizeof(struct elfhdr)) {
struct elfhdr ehdr = {
.e_ident = {
[EI_MAG0] = ELFMAG0,
[EI_MAG1] = ELFMAG1,
[EI_MAG2] = ELFMAG2,
[EI_MAG3] = ELFMAG3,
[EI_CLASS] = ELF_CLASS,
[EI_DATA] = ELF_DATA,
[EI_VERSION] = EV_CURRENT,
[EI_OSABI] = ELF_OSABI,
},
.e_type = ET_CORE,
.e_machine = ELF_ARCH,
.e_version = EV_CURRENT,
.e_phoff = sizeof(struct elfhdr),
.e_flags = ELF_CORE_EFLAGS,
.e_ehsize = sizeof(struct elfhdr),
.e_phentsize = sizeof(struct elf_phdr),
.e_phnum = nphdr,
};

tsz = min_t(size_t, buflen, sizeof(struct elfhdr) - *fpos);
if (copy_to_user(buffer, (char *)&ehdr + *fpos, tsz)) {
ret = -EFAULT;
goto out;
}

buffer += tsz;
buflen -= tsz;
*fpos += tsz;
}

/* ELF program headers. */
/* (3) 構造 ELF program 頭,並拷貝給給使用者態讀記憶體 */
if (buflen && *fpos < phdrs_offset + phdrs_len) {
struct elf_phdr *phdrs, *phdr;

phdrs = kzalloc(phdrs_len, GFP_KERNEL);
if (!phdrs) {
ret = -ENOMEM;
goto out;
}

/* (3.1) PT_NOTE segment 不需要實體地址和虛擬地址 */
phdrs[0].p_type = PT_NOTE;
phdrs[0].p_offset = notes_offset;
phdrs[0].p_filesz = notes_len;

phdr = &phdrs[1];
/* (3.2) 逐個計算 PT_LOAD segment 的實體地址、虛擬地址和長度 */
list_for_each_entry(m, &kclist_head, list) {
phdr->p_type = PT_LOAD;
phdr->p_flags = PF_R | PF_W | PF_X;
phdr->p_offset = kc_vaddr_to_offset(m->addr) + data_offset;
if (m->type == KCORE_REMAP)
phdr->p_vaddr = (size_t)m->vaddr;
else
phdr->p_vaddr = (size_t)m->addr;
if (m->type == KCORE_RAM || m->type == KCORE_REMAP)
phdr->p_paddr = __pa(m->addr);
else if (m->type == KCORE_TEXT)
phdr->p_paddr = __pa_symbol(m->addr);
else
phdr->p_paddr = (elf_addr_t)-1;
phdr->p_filesz = phdr->p_memsz = m->size;
phdr->p_align = PAGE_SIZE;
phdr++;
}

tsz = min_t(size_t, buflen, phdrs_offset + phdrs_len - *fpos);
if (copy_to_user(buffer, (char *)phdrs + *fpos - phdrs_offset,
tsz)) {
kfree(phdrs);
ret = -EFAULT;
goto out;
}
kfree(phdrs);

buffer += tsz;
buflen -= tsz;
*fpos += tsz;
}

/* ELF note segment. */
/* (4) 構造 PT_NOTE segment,並拷貝給給使用者態讀記憶體 */
if (buflen && *fpos < notes_offset + notes_len) {
struct elf_prstatus prstatus = {};
struct elf_prpsinfo prpsinfo = {
.pr_sname = 'R',
.pr_fname = "vmlinux",
};
char *notes;
size_t i = 0;

strlcpy(prpsinfo.pr_psargs, saved_command_line,
sizeof(prpsinfo.pr_psargs));

notes = kzalloc(notes_len, GFP_KERNEL);
if (!notes) {
ret = -ENOMEM;
goto out;
}

/* (4.1) 新增 NT_PRSTATUS */
append_kcore_note(notes, &i, CORE_STR, NT_PRSTATUS, &prstatus,
sizeof(prstatus));
/* (4.2) 新增 NT_PRPSINFO */
append_kcore_note(notes, &i, CORE_STR, NT_PRPSINFO, &prpsinfo,
sizeof(prpsinfo));
/* (4.3) 新增 NT_TASKSTRUCT */
append_kcore_note(notes, &i, CORE_STR, NT_TASKSTRUCT, current,
arch_task_struct_size);
/*
* vmcoreinfo_size is mostly constant after init time, but it
* can be changed by crash_save_vmcoreinfo(). Racing here with a
* panic on another CPU before the machine goes down is insanely
* unlikely, but it's better to not leave potential buffer
* overflows lying around, regardless.
* Vmcoreinfo_size在初始化後基本保持不變,但可以通過crash_save_vmcoreinfo()修改。在機器宕機之前,在另一個CPU上出現恐慌是不太可能的,但無論如何,最好不要讓潛在的緩衝區溢位到處存在。
*/
/* (4.4) 新增 VMCOREINFO */
append_kcore_note(notes, &i, VMCOREINFO_NOTE_NAME, 0,
vmcoreinfo_data,
min(vmcoreinfo_size, notes_len - i));

tsz = min_t(size_t, buflen, notes_offset + notes_len - *fpos);
if (copy_to_user(buffer, notes + *fpos - notes_offset, tsz)) {
kfree(notes);
ret = -EFAULT;
goto out;
}
kfree(notes);

buffer += tsz;
buflen -= tsz;
*fpos += tsz;
}

/*
* Check to see if our file offset matches with any of
* the addresses in the elf_phdr on our list.
*/
start = kc_offset_to_vaddr(*fpos - data_offset);
if ((tsz = (PAGE_SIZE - (start & ~PAGE_MASK))) > buflen)
tsz = buflen;

m = NULL;
/* (5) 構造 PT_LOAD segment,並拷貝給給使用者態讀記憶體 */
while (buflen) {
/*
* If this is the first iteration or the address is not within
* the previous entry, search for a matching entry.
*/
if (!m || start < m->addr || start >= m->addr + m->size) {
list_for_each_entry(m, &kclist_head, list) {
if (start >= m->addr &&
start < m->addr + m->size)
break;
}
}

if (&m->list == &kclist_head) {
if (clear_user(buffer, tsz)) {
ret = -EFAULT;
goto out;
}
m = NULL; /* skip the list anchor */
} else if (!pfn_is_ram(__pa(start) >> PAGE_SHIFT)) {
if (clear_user(buffer, tsz)) {
ret = -EFAULT;
goto out;
}
} else if (m->type == KCORE_VMALLOC) {
vread(buf, (char *)start, tsz);
/* we have to zero-fill user buffer even if no read */
if (copy_to_user(buffer, buf, tsz)) {
ret = -EFAULT;
goto out;
}
} else if (m->type == KCORE_USER) {
/* User page is handled prior to normal kernel page: */
if (copy_to_user(buffer, (char *)start, tsz)) {
ret = -EFAULT;
goto out;
}
} else {
if (kern_addr_valid(start)) {
/*
* Using bounce buffer to bypass the
* hardened user copy kernel text checks.
*/
if (copy_from_kernel_nofault(buf, (void *)start,
tsz)) {
if (clear_user(buffer, tsz)) {
ret = -EFAULT;
goto out;
}
} else {
if (copy_to_user(buffer, buf, tsz)) {
ret = -EFAULT;
goto out;
}
}
} else {
if (clear_user(buffer, tsz)) {
ret = -EFAULT;
goto out;
}
}
}
buflen -= tsz;
*fpos += tsz;
buffer += tsz;
start += tsz;
tsz = (buflen > PAGE_SIZE ? PAGE_SIZE : buflen);
}

out:
up_read(&kclist_lock);
if (ret)
return ret;
return orig_buflen - buflen;
}

4. /proc/vmcore
/proc/vmcore是在 crash kernel 中把 normal kernel 的記憶體模擬成一個 elf core 檔案。

它的檔案格式構造是和上一節的 /proc/kcore 是類似的,不同的是它的資料準備工作是分成兩部分完成的:

normal kernel 負責事先把 elf header 準備好。
crash kernel 負責把傳遞過來的 elf header 封裝成 /proc/vmcore 檔案,並且儲存到磁碟。
下面我們就來詳細分析具體的過程。

4.1 準備 elf header (執行在 normal kernel)
在系統發生故障時狀態是很不穩定的,時間也是很緊急的,所以我們在 normal kernel 中就儘可能早的把 /proc/vomcore 檔案的 elf header 資料準備好。雖然 normal kernel 不會呈現 /proc/vmcore,只會在 crash kernel 中呈現。

在 kexec_tools 使用 kexec_file_load() 系統呼叫載入 crash kernel 時,就順帶把 /proc/vmcore 的 elf header 需要的大部分資料準備好了:

kexec_file_load() → kimage_file_alloc_init() → kimage_file_prepare_segments() → arch_kexec_kernel_image_load() → image->fops->load() → kexec_bzImage64_ops.load() → bzImage64_load() → crash_load_segments() → prepare_elf_headers() → crash_prepare_elf64_headers():

static int prepare_elf_headers(struct kimage *image, void **addr,
unsigned long *sz)
{
struct crash_mem *cmem;
int ret;

/* (1) 遍歷系統記憶體佈局表統計有效記憶體區域的個數,根據個數分配 cmem 空間 */
cmem = fill_up_crash_elf_data();
if (!cmem)
return -ENOMEM;

/* (2) 再次遍歷系統記憶體佈局表統計有效記憶體區域,記錄到 cmem 空間 */
ret = walk_system_ram_res(0, -1, cmem, prepare_elf64_ram_headers_callback);
if (ret)
goto out;

/* Exclude unwanted mem ranges */
/* (3) 排除掉一些不會使用的記憶體區域 */
ret = elf_header_exclude_ranges(cmem);
if (ret)
goto out;

/* By default prepare 64bit headers */
/* (4) 開始構造 elf header */
ret = crash_prepare_elf64_headers(cmem, IS_ENABLED(CONFIG_X86_64), addr, sz);

out:
vfree(cmem);
return ret;
}

int crash_prepare_elf64_headers(struct crash_mem *mem, int kernel_map,
void **addr, unsigned long *sz)
{
Elf64_Ehdr *ehdr;
Elf64_Phdr *phdr;
unsigned long nr_cpus = num_possible_cpus(), nr_phdr, elf_sz;
unsigned char *buf;
unsigned int cpu, i;
unsigned long long notes_addr;
unsigned long mstart, mend;

/* extra phdr for vmcoreinfo elf note */
nr_phdr = nr_cpus + 1;
nr_phdr += mem->nr_ranges;

/*
* kexec-tools creates an extra PT_LOAD phdr for kernel text mapping
* area (for example, ffffffff80000000 - ffffffffa0000000 on x86_64).
* I think this is required by tools like gdb. So same physical
* memory will be mapped in two elf headers. One will contain kernel
* text virtual addresses and other will have __va(physical) addresses.
*/

nr_phdr++;
elf_sz = sizeof(Elf64_Ehdr) + nr_phdr * sizeof(Elf64_Phdr);
elf_sz = ALIGN(elf_sz, ELF_CORE_HEADER_ALIGN);

buf = vzalloc(elf_sz);
if (!buf)
return -ENOMEM;

/* (4.1) 構造 ELF 檔案頭 */
ehdr = (Elf64_Ehdr *)buf;
phdr = (Elf64_Phdr *)(ehdr + 1);
memcpy(ehdr->e_ident, ELFMAG, SELFMAG);
ehdr->e_ident[EI_CLASS] = ELFCLASS64;
ehdr->e_ident[EI_DATA] = ELFDATA2LSB;
ehdr->e_ident[EI_VERSION] = EV_CURRENT;
ehdr->e_ident[EI_OSABI] = ELF_OSABI;
memset(ehdr->e_ident + EI_PAD, 0, EI_NIDENT - EI_PAD);
ehdr->e_type = ET_CORE;
ehdr->e_machine = ELF_ARCH;
ehdr->e_version = EV_CURRENT;
ehdr->e_phoff = sizeof(Elf64_Ehdr);
ehdr->e_ehsize = sizeof(Elf64_Ehdr);
ehdr->e_phentsize = sizeof(Elf64_Phdr);

/* Prepare one phdr of type PT_NOTE for each present cpu */
/* (4.2) 構造 ELF program 頭,
每個 cpu 獨立構造一個 PT_LOAD segment
segment 的資料存放在 per_cpu_ptr(crash_notes, cpu) 變數當中
注意 crash_notes 中目前還沒有資料,當前只是記錄了實體地址。只有在 crash 發生以後,才會實際往裡面儲存資料
*/
for_each_present_cpu(cpu) {
phdr->p_type = PT_NOTE;
notes_addr = per_cpu_ptr_to_phys(per_cpu_ptr(crash_notes, cpu));
phdr->p_offset = phdr->p_paddr = notes_addr;
phdr->p_filesz = phdr->p_memsz = sizeof(note_buf_t);
(ehdr->e_phnum)++;
phdr++;
}

/* Prepare one PT_NOTE header for vmcoreinfo */
/* (4.3) 構造 ELF program 頭,VMCOREINFO 獨立構造一個 PT_LOAD segment
注意當前只是記錄了 vmcoreinfo_note 的實體地址,實際資料也是分幾部分更新的
*/
phdr->p_type = PT_NOTE;
phdr->p_offset = phdr->p_paddr = paddr_vmcoreinfo_note();
phdr->p_filesz = phdr->p_memsz = VMCOREINFO_NOTE_SIZE;
(ehdr->e_phnum)++;
phdr++;

/* Prepare PT_LOAD type program header for kernel text region */
/* (4.4) 構造 ELF program 頭,核心程式碼段對應的 PT_LOAD segment */
if (kernel_map) {
phdr->p_type = PT_LOAD;
phdr->p_flags = PF_R|PF_W|PF_X;
phdr->p_vaddr = (unsigned long) _text;
phdr->p_filesz = phdr->p_memsz = _end - _text;
phdr->p_offset = phdr->p_paddr = __pa_symbol(_text);
ehdr->e_phnum++;
phdr++;
}

/* Go through all the ranges in mem->ranges[] and prepare phdr */
/* (4.5) 遍歷 cmem,把系統中的有效記憶體建立成 PT_LOAD segment */
for (i = 0; i < mem->nr_ranges; i++) {
mstart = mem->ranges[i].start;
mend = mem->ranges[i].end;

phdr->p_type = PT_LOAD;
phdr->p_flags = PF_R|PF_W|PF_X;
phdr->p_offset = mstart;

phdr->p_paddr = mstart;
phdr->p_vaddr = (unsigned long) __va(mstart);
phdr->p_filesz = phdr->p_memsz = mend - mstart + 1;
phdr->p_align = 0;
ehdr->e_phnum++;
phdr++;
pr_debug("Crash PT_LOAD elf header. phdr=%p vaddr=0x%llx, paddr=0x%llx, sz=0x%llx e_phnum=%d p_offset=0x%llx\n",
phdr, phdr->p_vaddr, phdr->p_paddr, phdr->p_filesz,
ehdr->e_phnum, phdr->p_offset);
}

*addr = buf;
*sz = elf_sz;
return 0;
}

4.1.1 crash_notes 資料的更新
只有在發生 panic 以後,才會往 crash_notes 中儲存實際的 cpu 暫存器資料。其更新過程如下:

__crash_kexec() → machine_crash_shutdown() → crash_save_cpu():
ipi_cpu_crash_stop() → crash_save_cpu():

void crash_save_cpu(struct pt_regs *regs, int cpu)
{
struct elf_prstatus prstatus;
u32 *buf;

if ((cpu < 0) || (cpu >= nr_cpu_ids))
return;

/* Using ELF notes here is opportunistic.
* I need a well defined structure format
* for the data I pass, and I need tags
* on the data to indicate what information I have
* squirrelled away. ELF notes happen to provide
* all of that, so there is no need to invent something new.
*/
buf = (u32 *)per_cpu_ptr(crash_notes, cpu);
if (!buf)
return;
/* (1) 清零 */
memset(&prstatus, 0, sizeof(prstatus));
/* (2) 儲存 pid */
prstatus.pr_pid = current->pid;
/* (3) 儲存 暫存器 */
elf_core_copy_kernel_regs(&prstatus.pr_reg, regs);
/* (4) 以 elf_note 格式儲存到 crash_notes 中 */
buf = append_elf_note(buf, KEXEC_CORE_NOTE_NAME, NT_PRSTATUS,
&prstatus, sizeof(prstatus));
/* (5) 追加一個全零的 elf_note 當作結尾 */
final_note(buf);
}

4.1.2 vmcoreinfo_note 資料的更新
vmcoreinfo_note 分成兩部分來更新:

1、第一部分在系統初始化的時候準備好了大部分的資料:
static int __init crash_save_vmcoreinfo_init(void)
{
/* (1.1) 分配 vmcoreinfo_data 空間 */
vmcoreinfo_data = (unsigned char *)get_zeroed_page(GFP_KERNEL);
if (!vmcoreinfo_data) {
pr_warn("Memory allocation for vmcoreinfo_data failed\n");
return -ENOMEM;
}

/* (1.2) 分配 vmcoreinfo_note 空間 */
vmcoreinfo_note = alloc_pages_exact(VMCOREINFO_NOTE_SIZE,
GFP_KERNEL | __GFP_ZERO);
if (!vmcoreinfo_note) {
free_page((unsigned long)vmcoreinfo_data);
vmcoreinfo_data = NULL;
pr_warn("Memory allocation for vmcoreinfo_note failed\n");
return -ENOMEM;
}

/* (2.1) 把系統的各種關鍵資訊使用 VMCOREINFO_xxx 一系列巨集,以字串的形式保持到 vmcoreinfo_data */
VMCOREINFO_OSRELEASE(init_uts_ns.name.release);
VMCOREINFO_PAGESIZE(PAGE_SIZE);

VMCOREINFO_SYMBOL(init_uts_ns);
VMCOREINFO_SYMBOL(node_online_map);
#ifdef CONFIG_MMU
VMCOREINFO_SYMBOL_ARRAY(swapper_pg_dir);
#endif
VMCOREINFO_SYMBOL(_stext);
VMCOREINFO_SYMBOL(vmap_area_list);

#ifndef CONFIG_NEED_MULTIPLE_NODES
VMCOREINFO_SYMBOL(mem_map);
VMCOREINFO_SYMBOL(contig_page_data);
#endif
#ifdef CONFIG_SPARSEMEM
VMCOREINFO_SYMBOL_ARRAY(mem_section);
VMCOREINFO_LENGTH(mem_section, NR_SECTION_ROOTS);
VMCOREINFO_STRUCT_SIZE(mem_section);
VMCOREINFO_OFFSET(mem_section, section_mem_map);
#endif
VMCOREINFO_STRUCT_SIZE(page);
VMCOREINFO_STRUCT_SIZE(pglist_data);
VMCOREINFO_STRUCT_SIZE(zone);
VMCOREINFO_STRUCT_SIZE(free_area);
VMCOREINFO_STRUCT_SIZE(list_head);
VMCOREINFO_SIZE(nodemask_t);
VMCOREINFO_OFFSET(page, flags);
VMCOREINFO_OFFSET(page, _refcount);
VMCOREINFO_OFFSET(page, mapping);
VMCOREINFO_OFFSET(page, lru);
VMCOREINFO_OFFSET(page, _mapcount);
VMCOREINFO_OFFSET(page, private);
VMCOREINFO_OFFSET(page, compound_dtor);
VMCOREINFO_OFFSET(page, compound_order);
VMCOREINFO_OFFSET(page, compound_head);
VMCOREINFO_OFFSET(pglist_data, node_zones);
VMCOREINFO_OFFSET(pglist_data, nr_zones);
#ifdef CONFIG_FLAT_NODE_MEM_MAP
VMCOREINFO_OFFSET(pglist_data, node_mem_map);
#endif
VMCOREINFO_OFFSET(pglist_data, node_start_pfn);
VMCOREINFO_OFFSET(pglist_data, node_spanned_pages);
VMCOREINFO_OFFSET(pglist_data, node_id);
VMCOREINFO_OFFSET(zone, free_area);
VMCOREINFO_OFFSET(zone, vm_stat);
VMCOREINFO_OFFSET(zone, spanned_pages);
VMCOREINFO_OFFSET(free_area, free_list);
VMCOREINFO_OFFSET(list_head, next);
VMCOREINFO_OFFSET(list_head, prev);
VMCOREINFO_OFFSET(vmap_area, va_start);
VMCOREINFO_OFFSET(vmap_area, list);
VMCOREINFO_LENGTH(zone.free_area, MAX_ORDER);
log_buf_vmcoreinfo_setup();
VMCOREINFO_LENGTH(free_area.free_list, MIGRATE_TYPES);
VMCOREINFO_NUMBER(NR_FREE_PAGES);
VMCOREINFO_NUMBER(PG_lru);
VMCOREINFO_NUMBER(PG_private);
VMCOREINFO_NUMBER(PG_swapcache);
VMCOREINFO_NUMBER(PG_swapbacked);
VMCOREINFO_NUMBER(PG_slab);
#ifdef CONFIG_MEMORY_FAILURE
VMCOREINFO_NUMBER(PG_hwpoison);
#endif
VMCOREINFO_NUMBER(PG_head_mask);
#define PAGE_BUDDY_MAPCOUNT_VALUE (~PG_buddy)
VMCOREINFO_NUMBER(PAGE_BUDDY_MAPCOUNT_VALUE);
#ifdef CONFIG_HUGETLB_PAGE
VMCOREINFO_NUMBER(HUGETLB_PAGE_DTOR);
#define PAGE_OFFLINE_MAPCOUNT_VALUE (~PG_offline)
VMCOREINFO_NUMBER(PAGE_OFFLINE_MAPCOUNT_VALUE);
#endif

/* (2.2) 補充一些架構相關的 vmcoreinfo */
arch_crash_save_vmcoreinfo();

/* (3) 把 vmcoreinfo_data 中儲存的資料以 elf_note 的形式儲存到 vmcoreinfo_note 中 */
update_vmcoreinfo_note();

return 0;
}

2、第二部分在 panic 發生後追加了 資料:
__crash_kexec() → crash_save_vmcoreinfo():

void crash_save_vmcoreinfo(void)
{
if (!vmcoreinfo_note)
return;

/* Use the safe copy to generate vmcoreinfo note if have */
if (vmcoreinfo_data_safecopy)
vmcoreinfo_data = vmcoreinfo_data_safecopy;

/* (1) 補充 "CRASHTIME=xxx" 資訊 */
vmcoreinfo_append_str("CRASHTIME=%lld\n", ktime_get_real_seconds());
update_vmcoreinfo_note();
}

vmcoreinfo 對應 readelf -n xxx 讀出的資料:

$ readelf -n vmcore.202106170650

Displaying notes found at file offset 0x00001000 with length 0x00000ac8:
Owner Data size Description
CORE 0x00000150 NT_PRSTATUS (prstatus structure)
CORE 0x00000150 NT_PRSTATUS (prstatus structure)
VMCOREINFO 0x000007e6 Unknown note type: (0x00000000)
description data: 4f 53 52 45 4c 45 41 53 45 3d 35 2e 38 2e 30

// description data 對應 ascii:
OSRELEASE=5.8.0-43-generic
PAGESIZE=4096
SYMBOL(init_uts_ns)=ffffffffa5014620
SYMBOL(node_online_map)=ffffffffa5276720
SYMBOL(swapper_pg_dir)=ffffffffa500a000
SYMBOL(_stext)=ffffffffa3a00000
SYMBOL(vmap_area_list)=ffffffffa50f2560
SYMBOL(mem_section)=ffff91673ffd2000
LENGTH(mem_section)=2048
SIZE(mem_section)=16
OFFSET(mem_section.section_mem_map)=0
SIZE(page)=64
SIZE(pglist_data)=171968
SIZE(zone)=1472
SIZE(free_area)=88
...
CRASHTIME=1623937823

4.2 準備 cmdline (執行在 normal kernel)
準備好的 elf header 資料怎麼傳遞給 crash kernel 呢?是通過 cmdline 來進行傳遞的:

kexec_file_load() → kimage_file_alloc_init() → kimage_file_prepare_segments() → arch_kexec_kernel_image_load() → image->fops->load() → kexec_bzImage64_ops.load() → bzImage64_load() → setup_cmdline():

static int setup_cmdline(struct kimage *image, struct boot_params *params,
unsigned long bootparams_load_addr,
unsigned long cmdline_offset, char *cmdline,
unsigned long cmdline_len)
{
char *cmdline_ptr = ((char *)params) + cmdline_offset;
unsigned long cmdline_ptr_phys, len = 0;
uint32_t cmdline_low_32, cmdline_ext_32;

/* (1) 在 crask kernel 的 cmdline 中追加引數:"elfcorehdr=0x%lx " */
if (image->type == KEXEC_TYPE_CRASH) {
len = sprintf(cmdline_ptr,
"elfcorehdr=0x%lx ", image->arch.elf_load_addr);
}
memcpy(cmdline_ptr + len, cmdline, cmdline_len);
cmdline_len += len;

cmdline_ptr[cmdline_len - 1] = '\0';

pr_debug("Final command line is: %s\n", cmdline_ptr);
cmdline_ptr_phys = bootparams_load_addr + cmdline_offset;
cmdline_low_32 = cmdline_ptr_phys & 0xffffffffUL;
cmdline_ext_32 = cmdline_ptr_phys >> 32;

params->hdr.cmd_line_ptr = cmdline_low_32;
if (cmdline_ext_32)
params->ext_cmd_line_ptr = cmdline_ext_32;

return 0;
}

4.3 啟動 crash kernel (執行在 normal kernel)
在 normal kernel 發生 panic 以後會 跳轉到 carsh kernel:

die() → crash_kexec() → __crash_kexec() → machine_kexec()
1
4.4 接收 elfheadr (執行在 crash kernel)
在 carsh kernel 中首先會接收到 normal kernel 在 cmdline 中傳遞過來的 vmcore 檔案的 elf header 資訊:

static int __init setup_elfcorehdr(char *arg)
{
char *end;
if (!arg)
return -EINVAL;
elfcorehdr_addr = memparse(arg, &end);
if (*end == '@') {
elfcorehdr_size = elfcorehdr_addr;
elfcorehdr_addr = memparse(end + 1, &end);
}
return end > arg ? 0 : -EINVAL;
}
early_param("elfcorehdr", setup_elfcorehdr);
1
2
3
4
5
6
7
8
9
10
11
12
13
4.5 解析整理 elfheadr (執行在 crash kernel)
然後會讀取 vmcore 檔案的 elf header 資訊,並進行解析和整理:

static int __init vmcore_init(void)
{
int rc = 0;

/* Allow architectures to allocate ELF header in 2nd kernel */
rc = elfcorehdr_alloc(&elfcorehdr_addr, &elfcorehdr_size);
if (rc)
return rc;
/*
* If elfcorehdr= has been passed in cmdline or created in 2nd kernel,
* then capture the dump.
*/
if (!(is_vmcore_usable()))
return rc;
/* (1) 解析 normal kernel 傳遞過來的 elf header 資訊 */
rc = parse_crash_elf_headers();
if (rc) {
pr_warn("Kdump: vmcore not initialized\n");
return rc;
}
elfcorehdr_free(elfcorehdr_addr);
elfcorehdr_addr = ELFCORE_ADDR_ERR;

/* (2) 建立 /proc/vmcore 檔案介面 */
proc_vmcore = proc_create("vmcore", S_IRUSR, NULL, &vmcore_proc_ops);
if (proc_vmcore)
proc_vmcore->size = vmcore_size;
return 0;
}
fs_initcall(vmcore_init);


parse_crash_elf_headers()

static int __init parse_crash_elf64_headers(void)
{
int rc=0;
Elf64_Ehdr ehdr;
u64 addr;

addr = elfcorehdr_addr;

/* Read Elf header */
/* (1.1) 讀出傳遞過來的 elf header 資訊
注意:涉及到讀另一個系統的記憶體,我們需要對實體地址進行ioremap_cache() 建立對映以後才能讀取
後續的很多地方都是以這種方式來讀取
*/
rc = elfcorehdr_read((char *)&ehdr, sizeof(Elf64_Ehdr), &addr);
if (rc < 0)
return rc;

/* Do some basic Verification. */
/* (1.2) 對讀出的 elf header 資訊進行一些合法性判斷,防止被破壞 */
if (memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0 ||
(ehdr.e_type != ET_CORE) ||
!vmcore_elf64_check_arch(&ehdr) ||
ehdr.e_ident[EI_CLASS] != ELFCLASS64 ||
ehdr.e_ident[EI_VERSION] != EV_CURRENT ||
ehdr.e_version != EV_CURRENT ||
ehdr.e_ehsize != sizeof(Elf64_Ehdr) ||
ehdr.e_phentsize != sizeof(Elf64_Phdr) ||
ehdr.e_phnum == 0) {
pr_warn("Warning: Core image elf header is not sane\n");
return -EINVAL;
}

/* Read in all elf headers. */
/* (1.3) 在crash kernel 上分配兩個buffer,準備吧資料讀到本地
elfcorebuf 用來儲存 elf header + elf program header
elfnotes_buf 用來儲存 PT_NOTE segment
*/
elfcorebuf_sz_orig = sizeof(Elf64_Ehdr) +
ehdr.e_phnum * sizeof(Elf64_Phdr);
elfcorebuf_sz = elfcorebuf_sz_orig;
elfcorebuf = (void *)__get_free_pages(GFP_KERNEL | __GFP_ZERO,
get_order(elfcorebuf_sz_orig));
if (!elfcorebuf)
return -ENOMEM;
addr = elfcorehdr_addr;
/* (1.4) 把整個 elf header + elf program header 讀取到 elfcorebuf */
rc = elfcorehdr_read(elfcorebuf, elfcorebuf_sz_orig, &addr);
if (rc < 0)
goto fail;

/* Merge all PT_NOTE headers into one. */
/* (1.5) 整理資料把多個 PT_NOTE 合併成一個,並且把 PT_NOTE 資料拷貝到 elfnotes_buf */
rc = merge_note_headers_elf64(elfcorebuf, &elfcorebuf_sz,
&elfnotes_buf, &elfnotes_sz);
if (rc)
goto fail;
/* (1.6) 逐個除錯 PT_LOAD segment 控制頭,讓每個 segment 符合 page 對齊 */
rc = process_ptload_program_headers_elf64(elfcorebuf, elfcorebuf_sz,
elfnotes_sz, &vmcore_list);
if (rc)
goto fail;

/* (1.7) 配合上一步的 page 對齊調整,計算 vmcore_list 連結串列中的 offset 偏移 */
set_vmcore_list_offsets(elfcorebuf_sz, elfnotes_sz, &vmcore_list);
return 0;
fail:
free_elfcorebuf();
return rc;
}

static int __init merge_note_headers_elf64(char *elfptr, size_t *elfsz,
char **notes_buf, size_t *notes_sz)
{
int i, nr_ptnote=0, rc=0;
char *tmp;
Elf64_Ehdr *ehdr_ptr;
Elf64_Phdr phdr;
u64 phdr_sz = 0, note_off;

ehdr_ptr = (Elf64_Ehdr *)elfptr;

/* (1.5.1) 更新每個獨立 PT_NOTE 的長度,去除尾部全零 elf_note */
rc = update_note_header_size_elf64(ehdr_ptr);
if (rc < 0)
return rc;

/* (1.5.2) 計算 所有 PT_NOTE 資料加起來的總長度 */
rc = get_note_number_and_size_elf64(ehdr_ptr, &nr_ptnote, &phdr_sz);
if (rc < 0)
return rc;

*notes_sz = roundup(phdr_sz, PAGE_SIZE);
*notes_buf = vmcore_alloc_buf(*notes_sz);
if (!*notes_buf)
return -ENOMEM;

/* (1.5.3) 把所有 PT_NOTE 資料拷貝到一起,拷貝到 notes_buf 中 */
rc = copy_notes_elf64(ehdr_ptr, *notes_buf);
if (rc < 0)
return rc;

/* Prepare merged PT_NOTE program header. */
/* (1.5.4) 建立一個新的 PT_NOTE 控制結構來定址 notes_buf */
phdr.p_type = PT_NOTE;
phdr.p_flags = 0;
note_off = sizeof(Elf64_Ehdr) +
(ehdr_ptr->e_phnum - nr_ptnote +1) * sizeof(Elf64_Phdr);
phdr.p_offset = roundup(note_off, PAGE_SIZE);
phdr.p_vaddr = phdr.p_paddr = 0;
phdr.p_filesz = phdr.p_memsz = phdr_sz;
phdr.p_align = 0;

/* Add merged PT_NOTE program header*/
/* (1.5.5) 拷貝新的 PT_NOTE 控制結構 */
tmp = elfptr + sizeof(Elf64_Ehdr);
memcpy(tmp, &phdr, sizeof(phdr));
tmp += sizeof(phdr);

/* Remove unwanted PT_NOTE program headers. */
/* (1.5.6) 移除掉已經無用的 PT_NOTE 控制結構 */
i = (nr_ptnote - 1) * sizeof(Elf64_Phdr);
*elfsz = *elfsz - i;
memmove(tmp, tmp+i, ((*elfsz)-sizeof(Elf64_Ehdr)-sizeof(Elf64_Phdr)));
memset(elfptr + *elfsz, 0, i);
*elfsz = roundup(*elfsz, PAGE_SIZE);

/* Modify e_phnum to reflect merged headers. */
ehdr_ptr->e_phnum = ehdr_ptr->e_phnum - nr_ptnote + 1;

/* Store the size of all notes. We need this to update the note
* header when the device dumps will be added.
*/
elfnotes_orig_sz = phdr.p_memsz;

return 0;
}

4.6 讀取 elf core (執行在 crash kernel)
經過上一節的解析 elf 頭資料基本已準備好,elfcorebuf 用來儲存 elf header + elf program header,elfnotes_buf 用來儲存 PT_NOTE segment。

現在可以通過對 /proc/vmcore 檔案的讀操作來讀取 elf core 資料了:

static const struct proc_ops vmcore_proc_ops = {
.proc_read = read_vmcore,
.proc_lseek = default_llseek,
.proc_mmap = mmap_vmcore,
};


read_vmcore()

static ssize_t __read_vmcore(char *buffer, size_t buflen, loff_t *fpos,
int userbuf)
{
ssize_t acc = 0, tmp;
size_t tsz;
u64 start;
struct vmcore *m = NULL;

if (buflen == 0 || *fpos >= vmcore_size)
return 0;

/* trim buflen to not go beyond EOF */
if (buflen > vmcore_size - *fpos)
buflen = vmcore_size - *fpos;

/* Read ELF core header */
/* (1) 從 elfcorebuf 中讀取 elf header + elf program header,並拷貝給給使用者態讀記憶體 */
if (*fpos < elfcorebuf_sz) {
tsz = min(elfcorebuf_sz - (size_t)*fpos, buflen);
if (copy_to(buffer, elfcorebuf + *fpos, tsz, userbuf))
return -EFAULT;
buflen -= tsz;
*fpos += tsz;
buffer += tsz;
acc += tsz;

/* leave now if filled buffer already */
if (buflen == 0)
return acc;
}

/* Read Elf note segment */
/* (2) 從 elfnotes_buf 中讀取 PT_NOTE segment,並拷貝給給使用者態讀記憶體 */
if (*fpos < elfcorebuf_sz + elfnotes_sz) {
void *kaddr;

/* We add device dumps before other elf notes because the
* other elf notes may not fill the elf notes buffer
* completely and we will end up with zero-filled data
* between the elf notes and the device dumps. Tools will
* then try to decode this zero-filled data as valid notes
* and we don't want that. Hence, adding device dumps before
* the other elf notes ensure that zero-filled data can be
* avoided.
*/
#ifdef CONFIG_PROC_VMCORE_DEVICE_DUMP
/* Read device dumps */
if (*fpos < elfcorebuf_sz + vmcoredd_orig_sz) {
tsz = min(elfcorebuf_sz + vmcoredd_orig_sz -
(size_t)*fpos, buflen);
start = *fpos - elfcorebuf_sz;
if (vmcoredd_copy_dumps(buffer, start, tsz, userbuf))
return -EFAULT;

buflen -= tsz;
*fpos += tsz;
buffer += tsz;
acc += tsz;

/* leave now if filled buffer already */
if (!buflen)
return acc;
}
#endif /* CONFIG_PROC_VMCORE_DEVICE_DUMP */

/* Read remaining elf notes */
tsz = min(elfcorebuf_sz + elfnotes_sz - (size_t)*fpos, buflen);
kaddr = elfnotes_buf + *fpos - elfcorebuf_sz - vmcoredd_orig_sz;
if (copy_to(buffer, kaddr, tsz, userbuf))
return -EFAULT;

buflen -= tsz;
*fpos += tsz;
buffer += tsz;
acc += tsz;

/* leave now if filled buffer already */
if (buflen == 0)
return acc;
}

/* (3) 從 vmcore_list 連結串列中讀取 PT_LOAD segment,並拷貝給給使用者態讀記憶體
對實體地址進行ioremap_cache() 建立對映以後才能讀取
*/
list_for_each_entry(m, &vmcore_list, list) {
if (*fpos < m->offset + m->size) {
tsz = (size_t)min_t(unsigned long long,
m->offset + m->size - *fpos,
buflen);
start = m->paddr + *fpos - m->offset;
tmp = read_from_oldmem(buffer, tsz, &start,
userbuf, mem_encrypt_active());
if (tmp < 0)
return tmp;
buflen -= tsz;
*fpos += tsz;
buffer += tsz;
acc += tsz;

/* leave now if filled buffer already */
if (buflen == 0)
return acc;
}
}

return acc;
}

參考資料
1.kdump: usage and internals
2.Linux kdump(系統臨終快照)
3.How to use linux-crashdump to capture a kernel oops/panic
4.Documentation for Kdump - The kexec-based Crash Dumping Solution
5.Debugging the Linux kernel with the Crash Utility
6.kexec - A travel to the purgatory
7.vmcore分析和實戰
8.Crash工具實戰-結構體解析(skb相關解析)
9.Crash工具實戰-解析連結串列
————————————————
版權宣告:本文為CSDN博主「pwl999」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處連結及本宣告。
原文連結:https://blog.csdn.net/pwl999/article/details/118418242