Linux Kdump 機制詳解

作者簡介:中年碼農,做過電信、手機、安全、芯片等行業,靠 Linux 混飯喫。

簡介

Kdump 提供了一種機制在內核出現故障的時候把系統的所有內存信息和寄存器信息 dump 出來成一個文件,後續通過 gdb/crash 等工具進行分析和調試。和用戶態程序的 coredump 機制類似。它的主要流程如下圖所示:

可以看到它的核心原理是保留一段內存並且預先加載了一個備用的 kernel,在主 kernel 出現故障時跳轉到備用 kernel,在備用 kernel 中把主 kernel 使用的內存和發生故障時的寄存器信息 dump 到一個磁盤文件中供後續分析。這個文件的格式是 elf core 文件格式。

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

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

安裝

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

sudo apt-get install linux-crashdump

安裝完後,可以通過 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.

觸發 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

調試 kdump

使用 crash 工具可以很方便對 kdump 文件進行分析, crash 是對 gdb 進行了一些包裝,生成了更多的調試內核的快捷命令。同樣可以利用 gdb 和 trace32 工具進行分析

$ sudo crash /usr/lib/debug/boot/vmlinux-5.8.0-43-generic /var/crash/202106170338/dump.202106170338

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
  1. 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. kdump-tools 調用了 kdump-config savecore:
# vim /etc/init.d/kdump-tools

KDUMP_SCRIPT=/usr/sbin/kdump-config

                echo -n "Starting $DESC: "
                $KDUMP_SCRIPT savecore
  1. 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

原理分析

kexec 實現了 crash kernel 的加載。核心分爲兩部分:

kdump 主要實現把 vmcore 文件從內存拷貝到磁盤,並進行一些瘦身。

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

所以可以看到 /proc/kcore 和 /proc/vmcore 這兩個文件是整個機制的核心,我們重點分析這兩部分的實現。

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
  1. 可以進一步查看 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. 可以進一步解析 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

/proc/kcore

有人在清理磁盤空間的常常會碰到 /proc/kcore 文件,因爲它顯示出來的體積非常的大,有時高達 128T。但是實際上她沒有佔用任何磁盤空間,它是一個內存文件系統中的文件。它也沒有佔用多少內存空間,除了一些控制頭部分佔用少量內存,大塊的空間都是模擬的,只有在用戶讀操作的時候纔會從對應的內存空間去讀取的。

上一節已經介紹了 /proc/kcore 是把當前系統的內存模擬成一個 elf core 文件,可以使用 gdb 對當前系統進行在線調試。那本節我們就來看看具體的模擬過程。

準備數據

初始化就是構建 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, ¬es_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

讀取 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, ¬es_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;
}

/proc/vmcore

/proc/vmcore 是在 crash kernel 中把 normal kernel 的內存模擬成一個 elf core 文件。

它的文件格式構造是和上一節的 /proc/kcore 是類似的,不同的是它的數據準備工作是分成兩部分完成的:

下面我們就來詳細分析具體的過程。

準備 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;
}

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);
}

2. vmcoreinfo_note 數據的更新

vmcoreinfo_note 分成兩部分來更新:

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;
}
__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

準備 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;
}

啓動 crash kernel (運行在 normal kernel)

在 normal kernel 發生 panic 以後會 跳轉到 carsh kernel:

die() → crash_kexec() → __crash_kexec() → machine_kexec()

接收 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);

解析整理 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;
}

讀取 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;
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/XpAHTFwpNGoKcDYr8TUMtQ