Linux除錯

charlieroro發表於2024-11-13

Linux debugging, profiling and tracing training

本文來自bootlin的公開培訓文件

Debugging, Profiling, Tracing

Debugging

▶ 查詢和修復軟體/系統中存在的問題

▶ 可能會用到不同的工具和方法:

  • 互動式除錯(如GDB)
  • 事後分析(如coredump)
  • 控制流分析(使用tracing工具)
  • 測試(整合測試)

▶ 大部分除錯都是在開發環境中完成的

▶ 通常是侵入式的,允許暫停和恢復程式執行

Profiling

▶ 透過分析程式的執行時來幫助最佳化效能

▶ 通常會採集程式執行中的計數器

▶ 使用特定的工具、庫和作業系統特性來衡量效能,如perfOProfile

▶ 首先會聚合查詢執行過程中的資料,如程式呼叫次數、記憶體使用、CPU負載、快取miss等,然後從這些資料中抽取有意義的資訊,並以此來最佳化程式

Tracing

▶ 透過跟蹤應用的執行流來了解瓶頸和問題

▶ 在編譯或執行時執行檢測程式碼。可以使用特定的tracer,如LTTng、trace-cmd、SystemTap等來檢視使用者空間到核心空間的函式呼叫

▶ 允許檢視應用執行時使用的函式和值

▶ 通常會在執行時記錄跟蹤資料,並在執行結束後展示這些資料

  • 在tracing結束之後會生成大量tracing資料
  • 通常要遠大於profiling的資料

▶ 由於可以透過tracepoints抽取資料,因此也可以用於除錯目的

Linux Application Stack

User/Kernel mode

▶ 使用者模式和核心模式通常指代執行的特權級別(privilege level)

▶ 這種模式實際上是指處理器執行模式,即硬體模式

▶ 核心可以控制完整的處理器狀態(異常處理、MMU等),而使用者空間只能在核心監督下做一些基本控制和執行。

Processes and Threads

▶ 程序是為執行一個程式而分配的資源組,如記憶體、執行緒、檔案描述符等。

▶ 一個PID表示一個程序,該程序所有的資訊都暴露在/proc/目錄中

  • /proc/self展示訪問該目錄的程序的資訊

▶ 當啟動一個程序時,它會初始化一個struct task_struct結構,表示一個可以被排程的執行執行緒

  • 一個程序在核心中體現為一個關聯到多個資源的執行緒

▶ 執行緒是個獨立的執行單元,共享程序內部的資源,如地址空間,檔案描述符等

▶ 可以使用fork()系統呼叫建立一個新的程序,使用pthread_create() 建立一個新的執行緒

▶ 任何時候,一個CPU core只能執行一個任務(使用get_current()函式檢視當前執行的任務),而一個任務也只能在一個CPU core上執行

▶ 不同的CPU core可以執行不同的任務

MMU and memory management

▶ 在Linux核心中(配置了CONFIG_MMU=y),CPU訪問的所有地址都是虛擬地址

▶ 記憶體管理單元(MMU)可以將這些虛擬記憶體對映到實體記憶體上(RAM或IO)

▶ MMU的基本對映單元成為頁(page),頁的大小是固定的(具體取決於架構/核心配置)

▶ 地址對映資訊會被插入到MMU硬體的頁表中,用於將CPU訪問的虛擬地址轉換為實體地址

▶ MMU可以透過某些屬性來限制頁對映訪問,如No Execute, Writable, Readable bits, Privileged/User bit, cacheability等

Userspace/Kernel memory layout

▶ 每個程序都有自己的虛擬地址空間(struct task_struct中的mm欄位)以及頁表(但共享相同的核心對映)

▶ 預設情況下,為了減少攻擊,所有使用者對映地址(base of heap, stack, text, data等)都是隨機的。可以透過norandmaps引數禁用該功能

image

不同的程序有不同的使用者記憶體空間:

image
Kernel memory map

▶ 核心有其特定的記憶體對映

▶ 核心啟動時會透過插入所有核心初始頁表中的元素來配置Linear mapping

▶ 透過位置來劃分不同的記憶體區域

▶ 支援隨機配置核心地址空間佈局,可以透過nokaslr命令禁用該功能

image
Userspace memory segments

▶ 當啟動一個程序時,核心會設定一些虛擬記憶體區域(由struct vm_area_struct管理的Virtual Memory Areas (VMA)),並配置不同的屬性。

▶ VMA記憶體域會對映到特定的屬性(R/W/X)

▶ 當一個程式試圖訪問未對映的記憶體域或對映到不允許訪問的記憶體域時會發生段錯誤,如

  • 向一個只讀的記憶體段寫資料
  • 嘗試執行一個不可執行的記憶體段

▶ 可以透過mmap()建立新的記憶體域

▶ 透過/proc//maps可以檢視單個應用的對映:

7f1855b2a000-7f1855b2c000 rw-p 00030000 103:01 3408650 ld-2.33.so
7ffc01625000-7ffc01646000 rw-p 00000000 00:00 0 [stack]
7ffc016e5000-7ffc016e9000 r--p 00000000 00:00 0 [vvar]
7ffc016e9000-7ffc016eb000 r-xp 00000000 00:00 0 [vdso]
Userspace memory types
image
Terms for memory in Linux tools

▶ 當使用Linux工具時,會使用如下4個術語來描述記憶體:

  • VSS/VSZ: Virtual Set Size (虛擬記憶體大小,包含共享libraries)
  • RSS: Resident Set Size (使用的總實體記憶體,包含共享libraries)
  • PSS: Proportional Set Size (指與其他程序共享的記憶體大小,如果一個進行獨佔了10MB記憶體,並確和另外一個程序共享了10MB記憶體,則PSS為15MB)
  • USS: Unique Set Size (程序佔用的實體記憶體,不包含共享對映記憶體)

▶ VSS >= RSS >= PSS >= USS.

Process context

▶ 程序上下文可以看作是與一個程序有關係的CPU暫存器中的內容:execution register, stack register

▶ 程序上下文還指定了一個執行狀態,並允許在核心模式中休眠

▶ 程序上下文中執行的程序可以被搶佔

▶ 當在這類上下文中執行程序時,可以透過get_current()訪問ss struct task_struct

image

Scheduling

▶ 有多種原因可以喚醒排程器

  • 中斷HZ引起的週期性tick(時鐘中斷)
  • 由非時鐘系統導致的程式中斷(CONFIG_NO_HZ=y)
  • 程式碼中主動呼叫schedule()
  • 隱式呼叫可以休眠的函式(如kmalloc()wait_event()等阻塞操作)

▶ 當進入排程函式後,排程器會選擇一個執行一個新的struct task_struct,最後呼叫switch_to()

switch_to()會儲存當前任務的程序上下文,並在設定新的當前任務執行時恢復下一個任務的程序上下文

The Linux Kernel Scheduler

▶ Linux核心排程器是實現實時行為的一個關鍵元件

▶ 它負責決定執行哪些可執行的任務

▶ 還負責選擇任務執行的CPU,並且和CPUidle和CPUFreq緊密耦合

▶ 同時負責核心空間和使用者空間的任務排程

▶ 每個任務會被分配一個排程型別(scheduling class)或策略

▶ 排程演算法會根據型別來選擇執行的任務

▶ 系統中可以存在不同排程型別的任務

Non-Realtime Scheduling Classes

有如下3種非實時排程類:

SCHED_OTHER: 預設策略,使用時間片演算法

SCHED_BATCH: 類似SCHED_OTHER,但主要用於執行CPU密集型任務

SCHED_IDLE: 優先順序很低。

SCHED_OTHERSCHED_BATCH都可以使用nice值來增加或減少其排程頻率

  • 較高的nice值意味著較低的排程頻率
Realtime Scheduling Classes

有如下3種實時排程型別:

▶ 可執行的任務會搶佔其他低優先順序的任務

SCHED_FIFO: 具有相同優先順序的任務會依照先進先出的原則排程

SCHED_RR: 類似SCHED_FIFO,但相同優先順序的任務間會使用時間片輪詢

SCHED_FIFOSCHED_RR 可以分配的優先順序為1到99

SCHED_DEADLINE: 用於執行重複jobs,任務會附加額外的屬性:

  • computation time,表示完成一個job所需的時間
  • deadline,允許一個job執行的最大時間
  • period,在該時間週期內只能執行一個job

▶ 僅定義任務型別並不足以實現實時行為

Changing the Scheduling Class

▶ 每個任務都有一個排程類(Scheduling Class),預設為SCHED_OTHER

man 2 sched_setscheduler 系統呼叫可以修改一個任務的排程型別

chrt工具:

  • 修改一個正在執行的任務的排程型別:chrt -f/-b/-o/-r/-d -p PRIO PID

  • 還可以使用chrt拉起一個特定排程型別的程式:chrt -f/-b/-o/-r/-d PRIO CMD

  • 展示當前程序的排程型別和優先順序:chrt -p PID

▶ 如果使用 man 2 sched_setscheduler設定了SCHED_RESET_ON_FORK標記,則新的程序會繼承父程序的排程型別

Context switching

▶ 上下文切換是一種改變處理器執行模式的行為(Kernel ↔ User):

  • 明確執行系統呼叫指令(從使用者模式同步請求到核心)
  • 隱式接收到的異常(MMU異常、中斷、斷點等)

▶ 這種狀態變更最終將體現到一個核心入口(通常是呼叫向量)中,該入口將執行必要的程式碼,併為核心模式執行設定正確的狀態。

▶ 核心會處理如暫存器儲存、切換到核心棧等行為:

  • 為了安全,核心棧的大小是固定的
Exceptions

▶ 異常為表示導致CPU進入異常模式(處理異常)的events

▶ 主要有兩種異常:同步和非同步

  • 通常在執行MMU、匯流排中斷或接收到軟硬體的中斷時會產生非同步異常
  • 當執行特定的指令,如斷點、系統呼叫等會產生同步異常

▶ 當觸發此類異常後,處理器會跳轉到異常向量中,並執該異常程式碼

Interrupts

▶ 中斷是由硬體周邊裝置生成的非同步訊號

  • 也可以是由特定指令生成的同步訊號(如(Inter Processor Interrupts )

▶ 當接收到一箇中斷時,CPU會改變其執行模式,跳轉到一個特定向量並切換到核心模式來處理該中斷

▶ 當存在多個CPU(cores)時,中斷通常會定向到某個core

▶ 可以透過"IRQ affinity"來控制每個CPU的中斷負載

  • 參見core-api/irq/irq-affinityman irqbalance(1)

▶ 當處理一箇中斷時,核心會執行一個稱為中斷上下文(interrupt context)的特殊上下文

▶ 該上下文不會進入使用者空間,且不應該使用get_current()

▶ 根據不同的架構,可能會使用一個IRQ棧

▶ 禁用中斷(不支援巢狀中斷) !

image
System Calls

▶ 系統呼叫允許使用者空間透過向核心請求服務來執行特定的指令(man 2 syscall)

  • 執行libc提供的函式(如read()write()等)時,通常會執行一個系統呼叫

▶ 透過暫存器傳入的數字識別符號來辨別不同的系統呼叫:

  • 核心透過(unistd.h中) __NR_<sycall>來定義系統呼叫識別符號,如:

    #define __NR_read 63
    #define __NR_write 64
    

▶ 核心持有指向這些識別符號的函式指標表,在完成系統呼叫的有效性驗證之後會透過這些指標來呼叫正確的處理函式

▶ 透過暫存器傳遞系統呼叫引數(最大6個引數)

▶ 當執行系統呼叫時,CPU會改變其執行狀態並切換到核心模式

▶ 每個架構都有一個特定的硬體機制(man 2 syscall)

mov w8, #__NR_getpid
svc #0
tstne x0, x1

Kernel execution contexts

▶ 核心會根據處理的event,在不同的上下文中執行程式碼

▶ 可能包括禁止中斷(透過禁止中斷,可以確保某個中斷處理程式不會搶佔當前的程式碼)、特定的棧等

Kernel threads

▶ 核心執行緒(kthreads)是一個特殊型別的struct task_struct,沒有關聯任何使用者資源(mm == NULL)

▶ 可以從kthreadd程序clone核心程序,也可以使用kthread_create建立核心程序

▶ 與使用者程序類似,可以在程序上下文中排程以及休眠核心執行緒

▶ 透過ps命令可以檢視核心執行緒的名稱(方括號表示):

$ ps --ppid 2 -p 2 -o uname,pid,ppid,cmd,cls
USER PID PPID CMD                         CLS
root 2     0 [kthreadd]                    TS
root 3     2 [rcu_gp]                      TS
root 4     2 [rcu_par_gp]                  TS
root 5     2 [netns]                       TS
root 7     2 [kworker/0:0H-events_highpr   TS
root 10    2 [mm_percpu_wq]                TS
root 11    2 [rcu_tasks_kthread]           TS
Workqueues

▶ Workqueues允許在未來的某個時間點排程執行work

▶ Workqueues在核心執行緒中執行work函式:

  • 允許在執行延遲工作時休眠。
  • 執行時可以啟用中斷

▶ 可以在特定的workqueue或多使用者共享的全域性workqueue中執行work。

softirq

▶ SoftIRQs是一種執行在軟體中斷上下文中的核心機制

▶ 可以執行需要在中斷處理後,且需要低延遲的程式碼。執行時機如下:

  • 在中斷上下文處理完硬中斷之後執行
  • 在和執行中斷處理的相同上下文中執行,因此不允許休眠。

▶ 如果需要在軟中斷上下文中執行程式碼,則應該使用現有的軟中斷實現,如tasklet,和BH workqueues(6.9之後用於替代tasklets),無需自行實現:

image
Threaded interrupts

▶ 執行緒中斷是一種允許使用一個硬中斷處理器(IRQ handler)和一個執行緒中斷處理器處理中斷的機制

▶ 一個執行緒中斷處理器可以執行可能會在kthread中休眠的work

▶ 核心會為每個請求執行緒中斷的中斷行建立一個kthread

  • kthread名為irq/<irq>-<name>,可以使用ps命令檢視
Allocations and context

▶ 可以使用下面函式在核心中申請記憶體:

void *kmalloc(size_t size, gfp_t gfp_mask);
void *kzalloc(size_t size, gfp_t gfp_mask);
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)

▶ 所有記憶體申請函式都有一個gfp_mask引數,用於指定記憶體型別:

  • GFP_KERNEL:正常分配,可以在分配記憶體時休眠(不能在中斷上下文中使用)
  • GFP_ATOMIC:自動分片,不會在分配資料時休眠

Linux Common Analysis & Observability Tools

Pseudo Filesystems

▶ 核心會暴露一些虛擬檔案系統來提供系統資訊

procfs 包含程序和系統資訊

  • 掛載位置為/proc
  • 通常會透過工具解析,以一種更友好的方式來展示原始資料

sysfs提供了與裝置和驅動有關的硬體/邏輯資訊。掛載位置為/sys

debugfs展示了與除錯有關的資訊

  • 通常掛載在/sys/kernel/debug/目錄下
  • mount -t debugfs none /sys/kernel/debug

procfs

procfs暴露了程序和系統相關的資訊(man 5 proc)

  • /proc/cpuinfo展示了CPU資訊
  • /proc/meminfo展示了記憶體資訊 (used, free, total等)
  • /proc/sys/包含可調節的系統引數。透過admin-guide/sysctl/index可以檢視能夠修改的引數列表
  • /proc/interrupts:統計了各個CPU的中斷計數
    • /proc/irq 中的每個中斷行都展示了一箇中斷的特定配置/狀態
  • /proc/<pid>/ 展示了程序相關的資訊
    • /proc/<pid>/status展示了程序的基本資訊
    • /proc/<pid>/maps展示了記憶體對映資訊
    • /proc/<pid>/fd展示了程序的檔案描述符
    • /proc/<pid>/task展示了屬於該程序的執行緒的描述符
  • /proc/self/會展示訪問該檔案的程序資訊

▶ 可以在filesystems/procman 5 proc 中檢視可用的procfs檔案和相關內容

sysfs

sysfs檔案系統暴露了關於各種核心子系統、硬體裝置和與驅動有關的資訊(man 5 sysfs)

▶ 可以透過表示核心內部裝置樹的檔案層級來檢視驅動和裝置之間的聯絡

/sys/kernel包含核心除錯的檔案:

  • irq:包含了中斷相關的資訊(對映、計數等)
  • tracing:用於tracing控制

admin-guide/abi-stable

debugfs

debugfs是一個簡單的基於RAM的檔案系統,暴露了除錯資訊

▶ 某些子系統(clk, block, dma, gpio等)會使用它來暴露內部除錯資訊

▶ 通常掛載到/sys/kernel/debug

  • 可以透過/sys/kernel/debug/dynamic_debug實現動態除錯
  • /sys/kernel/debug/clk/clk_summary暴露了時鐘樹

ELF files analysis

ELF files

ELF表示Executable and Linkable Format

▶ 檔案包含一個定義檔案的二進位制結構的首部

▶ 一系列包含資料的segments和sections:

  • .text section: 程式碼
  • .data section: 資料
  • .rodata section: 只讀資料
  • .debug_info section: 包含除錯資訊

▶ Sections是segment的一部分,可以被載入到記憶體中

▶ 核心支援的所有架構都採用相同的格式,vmlinux格式也是如此

  • 很多其他作業系統也使用ELF作為標準的可執行檔案格式
image

binutils for ELF analysis

▶ binutils用於處理二進位制檔案(物件檔案或可執行檔案)

  • 包括ldas以及其他有用的工具

readelf可以展示ELF檔案的資訊(header, section, segments等)

objdump可以展示和反彙編ELF檔案

objcopy可以將轉換ELF檔案或抽取/翻譯部分EKF檔案

nm可以展示嵌入在ELF檔案中的符號列表

addr2line可以根據ELF檔案中的地址查詢原始檔行/檔案

binutils example

▶ 使用nm查詢ksys_read()核心函式的地址

$ nm vmlinux | grep ksys_read
c02c7040 T ksys_read

▶ 使用addr2line來查詢核心OOPS地址或符號名稱對應的原始碼:

$ addr2line -s -f -e vmlinux ffffffff8145a8b0
queue_wc_show
blk-sysfs.c:516

▶ 使用readelf展示一個ELF首部:

$ readelf -h binary
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: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
...

▶ 使用objcopy將一個ELF檔案轉換為一個扁平二進位制檔案(flat binary file):

$ objcopy -O binary file.elf file.bin

ldd

▶ 可以使用ldd展示一個ELF中使用的共享庫(man 1 ldd)

▶ ldd會列出連結期間使用的所有庫

  • 不會展示在執行時使用dlopen()載入的庫
$ ldd /usr/bin/bash
linux-vdso.so.1 (0x00007ffdf3fc6000)
libreadline.so.8 => /usr/lib/libreadline.so.8 (0x00007fa2d2aef000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007fa2d2905000)
libncursesw.so.6 => /usr/lib/libncursesw.so.6 (0x00007fa2d288e000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fa2d2c88000)

Processor and CPU monitoring Tools

▶ 很多工具可以監控系統的各個部分

▶ 大部分工具都是CLI互動程式

  • 程序:ps, top, htop
  • 記憶體:Free, vmstat
  • 網路

▶ 大部分工具依賴sysfsprocfs檔案系統來獲取程序、記憶體和系統資訊

  • 網路工具使用核心網路子系統的netlink介面

ps & top(略)

mpstat

▶ 展示多處理器資訊(man 1 mpstat)

▶ 用於探測不均衡的CPU負載、錯誤的IRQ親和等

$ mpstat -P ALL
Linux 6.0.0-1-amd64 (fixe) 19/10/2022 _x86_64_ (4 CPU)
17:02:50 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
17:02:50 all 6,77 0,00 2,09 11,67 0,00 0,06 0,00 0,00 0,00 79,40
17:02:50 0 6,88 0,00 1,93 8,22 0,00 0,13 0,00 0,00 0,00 82,84
17:02:50 1 4,91 0,00 1,50 8,91 0,00 0,03 0,00 0,00 0,00 84,64
17:02:50 2 6,96 0,00 1,74 7,23 0,00 0,01 0,00 0,00 0,00 84,06
17:02:50 3 9,32 0,00 2,80 54,67 0,00 0,00 0,00 0,00 0,00 33,20
17:02:50 4 5,40 0,00 1,29 4,92 0,00 0,00 0,00 0,00 0,00 88,40

Memory monitoring tools

free

free是一個簡單的展示系統剩餘和已使用記憶體用量的程式(man 1 free)

  • 用於檢查系統記憶體是否耗盡
  • 使用 /proc/meminfo來獲取記憶體資訊
$ free -h
total used free shared buff/cache available
Mem: 15Gi 7.5Gi 1.4Gi 192Mi 6.6Gi 7.5Gi
Swap: 14Gi 20Mi 14Gi

free欄位數值較小並不意味著記憶體耗盡,為了最佳化效能,記憶體會將快取未使用的記憶體。參見 man 5 proc中的drop_caches來觀察buffers/cachefree/available記憶體的影響

vmstat

vmstat展示了系統虛擬記憶體使用資訊

▶ 還可以展示程序、記憶體、頁、阻塞IO、traps、磁碟和CPU使用情況。man 8 vmstat

▶ 可以週期性獲取資料:vmstat

$ vmstat 1 6
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
3 0 253440 1237236 194936 9286980 3 6 186 540 134 157 3 5 82 10 0

▶ 注意:vmstat將核心塊視為1024 bytes

pmap

pmap以建議的形式展示了/proc/<pid>/maps中的內容。man 1 pmap

# pmap 2002
2002: /usr/bin/dbus-daemon --session --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only
...
00007f3f958bb000  56K   r---- libdbus-1.so.3.32.1
00007f3f958c9000 192K   r-x-- libdbus-1.so.3.32.1
00007f3f958f9000  84K   r---- libdbus-1.so.3.32.1
00007f3f9590e000   8K   r---- libdbus-1.so.3.32.1
00007f3f95910000   4K   rw--- libdbus-1.so.3.32.1
00007f3f95937000   8K   rw---   [ anon ]
00007f3f95939000   8K   r---- ld-linux-x86-64.so.2
00007f3f9593b000 152K   r-x-- ld-linux-x86-64.so.2
00007f3f95961000  44K   r---- ld-linux-x86-64.so.2
00007f3f9596c000   8K   r---- ld-linux-x86-64.so.2
00007f3f9596e000   8K   rw--- ld-linux-x86-64.so.2
00007ffe13857000 132K   rw---   [ stack ]
00007ffe13934000  16K   r----   [ anon ]
00007ffe13938000   8K   r-x--   [ anon ]
total          11088K

I/O monitoring tools

iostat

iostat展示了系統上各個裝置的IOs

▶ 用於檢視一個裝置是否IOs過載

$ iostat
Linux 5.19.0-2-amd64 (fixe) 11/10/2022 _x86_64_ (12 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
          8,43  0,00  1,52    8,77    0,00   81,28
          
Device    tps  kB_read/s kB_wrtn/s kB_dscd/s kB_read kB_wrtn kB_dscd
nvme0n1 55,89  1096,88   149,33    0,00      5117334 696668  0
sda      0,03  0,92      0,00      0,00      4308    0       0
sdb    104,42  274,55    2126,64   0,00      1280853 9921488 0

iotop

iotop展示了每個程序的IOs資訊

▶ 用於檢視那個程序產生了大量I/O流

  • 需要在核心中啟用CONFIG_TASKSTATS=y, CONFIG_TASK_DELAY_ACCT=y 和 CONFIG_TASK_IO_ACCOUNTING=y
  • 還需要在執行時啟用:sysctl -w kernel.task_delayacct=1
# iotop
Total DISK READ:    20.61 K/s | Total DISK WRITE:   51.52 K/s
Current DISK READ:  20.61 K/s | Current DISK WRITE: 24.04 K/s
	TID  PRIO USER   DISK READ DISK WRITE> COMMAND
  2629 be/4 cleger 20.61 K/s 44.65 K/s firefox-esr [Cache2 I/O]
   322 be/3 root   0.00 B/s  3.43 K/s  [jbd2/nvme0n1p1-8]
 39055 be/4 cleger 0.00 B/s  3.43 K/s  firefox-esr [DOMCacheThread]
     1 be/4 root   0.00 B/s  0.00 B/s  init
     2 be/4 root   0.00 B/s  0.00 B/s  [kthreadd]
     3 be/0 root   0.00 B/s  0.00 B/s  [rcu_gp]
     4 be/0 root   0.00 B/s  0.00 B/s  [rcu_par_gp]

Networking Observability tools

ss

ss展示了網路socket的狀態

  • IPv4, IPv6, UDP, TCP, ICMP 和 UNIX domain sockets

▶ 取代netstat

▶ 從/proc/net中獲取資訊

▶ 用法:

  • ss:預設展示連線的sockets
  • ss -l展示監聽sockets
  • ss -a展示監聽和連線的sockets
  • ss -4/-6/-x 僅展示IPv4、IPv6或UNIX sockets
  • ss -t/-u僅展示TCP或UDP sockets
  • ss -p展示每個socket使用的程序
  • ss -n展示數字形式的地址
  • ss -s展示現有sockets的大概情況

▶ 參見the ss manpage

# ss
Netid State  Recv-Q Send-Q						 Local Address:Port Peer Address:Port Process
u_dgr ESTAB  0      0 													 * 304840 				* 26673
u_str ESTAB  0      0   /run/dbus/system_bus_socket 42871         * 26100
icmp6 UNCONN 0      0 											  *:ipv6-icmp 			  *:*
udp   ESTAB  0      0 		192.168.10.115%wlp0s20f3:bootpc  192.168.10.88:bootps
tcp   ESTAB  0      136 								 172.16.0.1:41376    172.16.11.42:ssh
tcp   ESTAB  0      273 							 192.168.1.77:55494    87.98.181.233:https
tcp   ESTAB  0      0 							[2a02:...:dbdc]:38466    [2001:...:9]:imap2
...
#

iftop

iftop展示一個遠端主機的頻寬使用情況

▶ 使用直方圖展示頻寬

iftop -i eth0:

  • image

▶ 可以自定義輸出

▶ 參見the iftop manpage

tcpdump

tcpdump可以捕獲網路流量並解碼很多協議

▶ 基於libpcap庫來捕獲報文

▶ 可以將捕獲的報文儲存到檔案,然後再讀取

  • 可以儲存為pcap或新的pcapng格式
  • tcpdump -i eth0 -w capture.pcap
  • tcpdump -r capture.pcap

▶ 可以使用過濾器來阻止捕獲不相關的報文

  • tcpdump -i eth0 tcp and not port 22

https://www.tcpdump.org/

Wireshark(略)

Application Debugging

Good practices

▶ 當前編譯器可以在編譯期間透過告警檢測很多錯誤

  • 如果想盡早捕獲錯誤,推薦使用-Werror -Wall -Wextra

▶ 編譯器可以提供靜態分析功能

  • GCC可以透過-fanalyzer 標誌提供該功能
  • LLVM在構建過程中提供了特定的工具

▶ 還可以使用元件特定的helper/hardening

  • 例如,在使用GNC C庫時,可以透過_FORTIFY_SOURCE 宏新增執行時輸入檢測。

Building with debug information

Debugging with ELF files

▶ GDB 可以除錯ELF檔案,ELF檔案中包含了除錯資訊

▶ 除錯資訊使用DWARF格式

▶ 允許偵錯程式根據地址和符號名稱、呼叫點等進行除錯

▶ 除錯資訊由編譯器在編譯期間透過指定-g生成到ELF檔案中

  • -g1:最小除錯資訊(呼叫棧使用)
  • -g2:指定-g時的預設除錯級別
  • -g3:包含額外的除錯資訊(宏定義)

▶ 更多除錯資訊參見GCC文件

Debugging with compiler optimizations

▶ 編譯器最佳化 (-O<level>)會導致最佳化掉某些變數和函式呼叫

▶ 在使用GDB展示這些被最佳化掉的資訊時會出現:

  • $1 = <value optimized out>

▶ 如果想要檢查變數和函式,最好使用-O0(不啟用最佳化)編譯程式碼

  • 注意:只能透過-O2-Os編譯核心

▶ 還可以使用編譯器屬性對函式進行註釋:

  • __attribute__((optimize("O0")))

▶ 移除函式的static修飾符可以避免內聯該函式

  • 注意: LTO (Link Time Optimization)可以解決這個問題

▶ 將一個特定的變數設定為volatile可以被避免編譯器最佳化

Instrumenting code crashes

▶ 可以透過GNU的擴充套件函式backtrace() (man 3 backtrace)來展示應用的呼叫棧:

char **backtrace_symbols(void *const *buffer, int size);

▶ 可以透過signal() (man signal(3)) 在特定的訊號上新增鉤子來列印呼叫棧:

  • 例如可以透過捕獲SIGSEGV訊號來dump當前呼叫棧
void (*signal(int sig, void (*func)(int)))(int);

The ptrace system call

ptrace

ptrace可以透過訪問tracee記憶體和暫存器記憶體來tracing程序

▶ 一個tracer可以觀察和控制另一個程序的執行狀態

▶ 透過將ptrace() 系統呼叫attach到一個tracee程序來實現tracing(man 2 ptrace)

▶ 可以直接呼叫ptrace(),但通常會透過工具間接呼叫:

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

▶ GDB、strace等除錯工具都可以訪問tracee程序狀態

GDB

這裡簡單過一下gdb的一般命令:

  • gdb <program>:使用gdb除錯開始除錯一個程式
  • gdb -p <pid>:透過指定程式PID,將gdb attach到一個正在執行的程式上
  • (gdb) run [prog_arg1 [prog_arg2] ...]:指定使用GDB執行一個程式時的命令
  • break foobar (b):為函式foobar()打斷點
  • break foobar.c:42:為檔案foobar.c的第42行打斷點
  • print varprint $regprint task->files[0].fd (p):列印變數var,暫存器$reg或一個複雜的引用。
  • info registers:展示暫存器資訊
  • continue (c):在斷點後繼續執行
  • next (n):繼續到下一行,跳過函式呼叫
  • step (s):繼續到下一行,進入子函式
  • stepi (si):繼續下一條指令
  • finish:返回函式
  • backtrace (bt):展示程式呼叫棧
  • info threads (i threads):展示available的執行緒列表
  • info breakpoints (i b):展示breakpoints/watchpoints列表
  • delete (d):刪除斷點
  • thread (t):選擇執行緒
  • frame (f):選擇呼叫棧的特定幀,n表示呼叫棧的幀
  • watch <variable>watch \*<address>:為特定的變數/地址新增一個watchpoint
  • print variable = value (p variable = value):修改特定變數的內容
  • break foobar.c:42 if condition == value:如果特定條件為true,則進入斷點
  • watch if condition == value:當特定條件為true時,觸發此watchpoint
  • display <expr>:在每次程式停止時,自動列印表示式
  • x/ <n><u> <address>:展示指定地址的記憶體。n為展示的記憶體量,u為展示的資料型別(b/h/w/g)。可以透過使用i型別展示指令。
  • list <expr>:展示與當前程式計數器位置有關的原始碼
  • disassemble <location,start_offset,end_offset> (disas):展示當前執行的彙編程式碼
  • p function(arguments):透過GDB執行一個函式。注意執行該函式可能帶來的副作用
  • p $newvar = value:宣告一個新的 gdb 變數,該變數可以在本地使用,也可以按照命令順序使用
  • define <command_name>:定義一個新的命令序列。 後續在GDB中就可以直接呼叫該命令序列

遠端除錯

▶ 在一個非嵌入的環境中,可以使用gdb作為除錯前端

gdb可以直接訪問使用除錯符號編譯的二進位制檔案和庫

▶ 但在一個嵌入的上下文中,目標平臺通常會限制直接使用gdb進行除錯

▶ 此時需要遠端除錯

  • ARCH-linux-gdb部署在開發工作站中,為使用者提供除錯特性
  • gdbserver部署在目標系統中(arm架構下只有400KB)
image

遠端除錯:架構

image

遠端除錯:目標配置

▶ 在目標透過gdbserver執行一個程式,此時程式不會立即執行:

gdbserver :<port> <executable> <args>
gdbserver /dev/ttyS0 <executable> <args>

▶ 或者,可以讓gdbserver attach到一個正在執行的程式上:

gdbserver --attach :<port> <pid>

▶ 或者可以無需執行程式啟動一個gdbserver(後續在client側設定目標程式):

gdbserver --multi :<port>

遠端除錯:主機配置

▶ 在主機側啟動ARCH-linux-gdb <executable>,並使用如下gdb命令:

  • 告訴gdb共享庫目錄:gdb> set sysroot <library-path>

  • 連線目標

    gdb> target remote <ip-addr>:<port> (networking)
    gdb> target remote /dev/ttyUSB0 (serial link)
    

    如果啟動gdbserver時指定了--multi選項時,則需要使用target extended-remote替換target remote

  • 如果沒有在gdbserver命令列中指定除錯的程式,則執行如下命令:

    gdb> set remote exec-file <path_to_program_on_target>
    

Coredumps

▶ 當一個程式由於段錯誤導致崩潰時,將不受偵錯程式控制

▶ 幸運的是,Linux可以生成一個ELF格式的包含程式崩潰時的記憶體的映象檔案,core檔案。gdb可以使用core檔案分析崩潰的程式狀態

▶ 在目標端

  • 透過 ulimit -c unlimited 啟動應用,這樣可以在程式崩潰時生成一個core檔案
  • 可以透過/proc/sys/kernel/core_pattern(man 5 core)修改輸出的coredump檔名稱
  • 在使用systemd的系統中,出於安全考慮,預設會禁用coredump功能,可以透過echo core > /proc/sys/kernel/core_pattern臨時啟用

▶ 在主機端

  • 程式崩潰後,將core檔案從目標端傳輸到主機端,然後執行ARCH-linux-gdb -c core-file application-binary

minicoredumper

▶ 對於複雜程式,coredump可能會比較大

minicoredumper是一個使用者空間的工具,它基於標準的core dump特性

▶ 可以將core dump輸出透過一個管道重定向到使用者空間程式

▶ 基於JSON配置,可以:

  • 僅儲存相關的sections(stack、heap、選擇的ELF sections)

  • 壓縮輸出檔案

  • /proc儲存額外的資訊

https://github.com/diamon/minicoredumper

▶ "高效實用的嵌入式系統碰撞資料採集"

  • Video:https://www.youtube.com/watch?v=q2zmwrgLJGs
  • Slides:elinux.org/images/8/81/Eoss2023_ogness_minicoredumper.pdf

GDB: going further

▶ Tutorial: Debugging Embedded Devices using GDB - Chris Simmonds, 2020

  • Slides: https://elinux.org/images/0/01/Debugging-with-gdb-csimmondselce-2020.pdf
  • Video: https://www.youtube.com/watch?v=JGhAgd2a_Ck

GDB Python Extension

▶ GDB提供了一個python integration特性,可以指令碼化一些除錯操作

▶ 當使用GDB執行Python時,會使用一個名為gdb的模組,該模組包含所有與GDB有關的類

▶ 可以新增新的命令、斷點和指標型別

▶ 可以透過在Python指令碼中的GDB能力完全控制並觀測被除錯的程式

  • 控制執行、新增斷點、watchpoints等
  • 訪問程式記憶體、幀、符號等

GDB Python Extension

class PrintOpenFD(gdb.FinishBreakpoint):
  def __init__(self, file):
    self.file = file
    super(PrintOpenFD, self).__init__()
    
  def stop (self):
    print ("---> File " + self.file + " opened with fd " + str(self.return_value))
    return False

class PrintOpen(gdb.Breakpoint):
  def stop(self):
    PrintOpenFD(gdb.parse_and_eval("file").string())
    return False

class TraceFDs (gdb.Command):
  def __init__(self):
  	super(TraceFDs, self).__init__("tracefds", gdb.COMMAND_USER)

  def invoke(self, arg, from_tty):
    print("Hooking open() with custom breakpoint")
    PrintOpen("open")

TraceFDs()

▶ 透過gdb source命令載入Python指令碼

  • 如果指令碼的名稱為 <program>-gdb.py,則它會被GDB自動載入:
(gdb) source trace_fds.py
(gdb) tracefds
Hooking open() with custom breakpoint
Breakpoint 1 at 0x33e0
(gdb) run
Starting program: /usr/bin/touch foo bar
Temporary breakpoint 2 at 0x5555555587da
---> File foo opened with fd 3
Temporary breakpoint 3 at 0x5555555587da
---> File bar opened with fd 0

Common debugging issues

▶ 在除錯時可能會遇到一些問題,如不好的地址-> 符號轉換、"optimized out"值或函式、空的呼叫棧

▶ 下面是一個checklist,可以幫助介紹一些問題解決時間:

  • 確保啟動的二進位制檔案包含debug symbols:使用gcc時,確保使用-g,在使用gdb是確保使用non-stripped版本的二進位制檔案
  • 可能的話,在最終的二進位制檔案中禁用optimizations或使用侵入性較小的級別(-Og)
    • 例如,靜態函式可以根據最佳化級別摺疊進呼叫者,因此它們可能會從呼叫棧丟失
  • 避免因為重用幀指標暫存器導致程式碼最佳化:使用GCC,確保使用-fno-omit-frame-pointer
    • 不僅僅用於除錯:很多profiling/tracing工具也會依賴呼叫棧

▶ 你的應用可能會包含很多庫:需要將這些配置應用到所有使用的元件上。

Application Tracing

strace

系統呼叫 tracer - https://strace.io

▶ 所有GNU/Linux系統可用,可以透過交叉編譯工具鏈或構建系統構建該工具

▶ 可以檢視系統正在執行的內容:訪問檔案、分配記憶體,適用於查詢簡單的問題

▶ 用法:

  • strace <command>: 啟動一個新的程序
  • strace -f <command>: 同時tracing子程序
  • strace -p <pid>: tracing一個已有的程序
  • strace -c <command>: 統計每個系統呼叫資訊
  • strace -e <expr> <command>: 使用高階過濾表示式

更多資訊檢視strace手冊

strace example output

> strace cat Makefile
[...]
fstat64(3, {st_mode=S_IFREG|0644, st_size=111585, ...}) = 0
mmap2(NULL, 111585, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7f69000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
open("/lib/tls/i686/cmov/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\320h\1\0004\0\0\0\344"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1442180, ...}) = 0
mmap2(NULL, 1451632, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb7e06000
mprotect(0xb7f62000, 4096, PROT_NONE) = 0
mmap2(0xb7f66000, 9840, PROT_READ|PROT_WRITE,
 MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb7f66000
close(3) = 0
[...]
openat(AT_FDCWD, "Makefile", O_RDONLY) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=173, ...}, AT_EMPTY_PATH) = 0
fadvise64(3, 0, 0, POSIX_FADV_SEQUENTIAL) = 0
mmap(NULL, 139264, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7290d28000
read(3, "ifneq ($(KERNELRELEASE),)\nobj-m "..., 131072) = 173
write(1, "ifneq ($(KERNELRELEASE),)\nobj-m "..., 173ifneq ($(KERNELRELEASE),)

strace -c example output

> strace -c cheese
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
 36.24 0.523807 19 27017 poll
 28.63 0.413833 5 75287 115 ioctl
 25.83 0.373267 6 63092 57321 recvmsg
 3.03 0.043807 8 5527 writev
 2.69 0.038865 10 3712 read
 2.14 0.030927 3 10807 getpid
 0.28 0.003977 1 3341 34 futex
 0.21 0.002991 3 1030 269 openat
 0.20 0.002889 2 1619 975 stat
 0.18 0.002534 4 568 mmap
 0.13 0.001851 5 356 mprotect
 0.10 0.001512 2 784 close
 0.08 0.001171 3 461 315 access
 0.07 0.001036 2 538 fstat
...

ltrace

用於tracing一個程式使用的共享庫,以及它接收到的所有訊號。

▶ 可以很好地補充strace,後者僅展示了系統呼叫

▶ 及時沒有源庫也能運作

▶ 允許透過正規表示式過濾或函式名稱列表庫呼叫

▶ 透過-S選項可以展示系統呼叫

▶ 透過-c選項可以展示摘要

手冊

▶ 用 glibc 效果更好

更多資訊檢視 https://en.wikipedia.org/wiki/Ltrace

ltrace example output

# ltrace ffmpeg -f video4linux2 -video_size 544x288 -input_format mjpeg -i /dev
/video0 -pix_fmt rgb565le -f fbdev /dev/fb0
__libc_start_main([ "ffmpeg", "-f", "video4linux2", "-video_size"... ] <unfinished ...>
setvbuf(0xb6a0ec80, nil, 2, 0) = 0
av_log_set_flags(1, 0, 1, 0) = 1
strchr("f", ':') = nil
strlen("f") = 1
strncmp("f", "L", 1) = 26
strncmp("f", "h", 1) = -2
strncmp("f", "?", 1) = 39
strncmp("f", "help", 1) = -2
strncmp("f", "-help", 1) = 57
strncmp("f", "version", 1) = -16
strncmp("f", "buildconf", 1) = 4
strncmp("f", "formats", 1) = 0
strlen("formats") = 7
strncmp("f", "muxers", 1) = -7
strncmp("f", "demuxers", 1) = 2
strncmp("f", "devices", 1) = 2
strncmp("f", "codecs", 1) = 3
...

ltrace summary

使用-c選項:

% time seconds usecs/call calls function
------ ----------- ----------- --------- --------------------
52.64 5.958660 5958660 1 __libc_start_main
20.64 2.336331 2336331 1 avformat_find_stream_info
14.87 1.682895 421 3995 strncmp
7.17 0.811210 811210 1 avformat_open_input
0.75 0.085290 584 146 av_freep
0.49 0.055150 434 127 strlen
0.29 0.033008 660 50 av_log
0.22 0.025090 464 54 strcmp
0.20 0.022836 22836 1 avformat_close_input
0.16 0.017788 635 28 av_dict_free
0.15 0.016819 646 26 av_dict_get
0.15 0.016753 440 38 strchr
0.13 0.014536 581 25 memset
...
------ ----------- ----------- --------- --------------------
100.00 11.318773 4762 total

LD_PRELOAD

Shared libraries

▶ 大部分共享庫是以.so結尾的ELF檔案

  • 啟動時被ld.so載入(動態載入器)
  • 或在執行時透過透過dlopen()載入

▶ 當啟動一個程式時(ELF檔案),核心會解析該檔案並載入對應的解析器

  • 大部分情況下,ELF檔案的PT_INTERP程式首部被設定為ld-linux.so

▶ 在載入期間,動態連結器ld.so會解析動態庫中的所有符號

▶ 動態庫只會被OS載入一次,然後對映到所有使用這些庫的應用中

  • 便於降低使用庫所需的記憶體

Hooking Library Calls

▶ 為了執行更復雜的庫呼叫鉤子,可以使用LD_PRELOAD環境變數

LD_PRELOAD用於指定一個可以在動態載入器載入其他庫之前需要載入的共享庫

▶ 可以透過預載入另一個庫來攔截所有庫呼叫

  • 覆蓋相同名稱的庫符號
  • 允許重定義一小部分符號
  • 可以透過dlsym (man 3 dlsym)載入"真實"的符號

▶ 除錯/tracing庫(libsegfault, libefence)會使用該環境變數

▶ C和C++都可以使用

LD_PRELOAD example

▶ 使用LD_PRELOAD預載入期望的庫

#include <string.h>
#include <unistd.h>

ssize_t read(int fd, void *data, size_t size) {
	memset(data, 0x42, size);
	return size;
}

▶ 使用LD_PRELOAD下的庫編譯:

$ gcc -shared -fPIC -o my_lib.so my_lib.c

▶ 使用LD_PRELOAD預載入新的庫

$ LD_PRELOAD=./my_lib.so ./exe

uprobes and perf

uprobes

uprobe是核心提供的可以tracing使用者空間程式碼的一種機制

▶ 可以在任何使用者空間符號中動態新增tracepoints

  • 核心tracing系統會在.text section中打上斷點

▶ 透過/sys/kernel/debug/tracing/uprobe_events暴露tracing資訊

▶ 通常會被perf, bcc等工具封裝使用

trace/uprobetracer

The perf tool

perf工具是一個使用效能計數器採集應用profile資訊的工具 (man 1 perf)

▶ 還可以管理tracepoints, kprobesuprobes

perf可以同時在使用者空間和核心空間執行profile

perf基於核心暴露的perf_event介面

▶ 提供了一組操作,每個操作有特定的引數

  • stat, record, report, top, annotate, ftrace, list, probe

Using perf record

perf可以記錄基於執行緒、程序和CPU的效能

▶ 只用時需要核心配置 CONFIG_PERF_EVENTS=y選項

▶ 需要從程式執行中採集資料,並輸出到perf.data檔案中

▶ 可以透過perf annotateperf report分析perf.data檔案

  • 可以在其他計算機上對嵌入式系統進行分析

Probing userspace functions

▶ 列出可在特定可執行檔案中探測的函式

$ perf probe --source=<source_dir> -x my_app -F

▶ 列出可在特定可執行檔案/函式中探測的行數

$ perf probe --source=<source_dir> -x my_app -L my_func

▶ 在使用者空間庫/可執行檔案的函式中建立uprobes

$ perf probe -x /lib/libc.so.6 printf
$ perf probe -x app my_func:3 my_var
$ perf probe -x app my_func%return ret=%r0

▶ 記錄執行的tracepoints

$ perf record -e probe_app:my_func -e probe_libc:printf

Memory issues

Usual Memory Issues

▶ 程式幾乎都需要訪問記憶體

▶ 如果操作不當,可能會產生大量錯誤

  • 當訪問無效記憶體時可能會產生段錯誤(訪問NULL指標或被釋放的記憶體)
  • 如果訪問了緩衝之外的地址可能會產生緩衝溢位
  • 申請記憶體之後忘了釋放會產生記憶體洩漏

Segmentation Faults

▶ 當程式嘗試訪問一個不允許訪問的記憶體區域,或以一種錯誤的方式訪問了一個記憶體區域時,核心會產生段錯誤:

  • 如寫入一個只讀記憶體區域
  • 嘗試執行一段無法執行的記憶體
int *ptr = NULL;
*ptr = 1;

▶ 產生段錯誤時,會在終端顯示Segmentation fault

$ ./program
Segmentation fault

Buffer Overflows

▶ 當訪問陣列越界時會產生緩衝溢位

▶ 在以下場景中,根據訪問情況可能會也可能不會導致程式崩潰:

  • malloc ()的陣列末尾之後寫入資料通常會覆蓋malloc的資料結構,導致崩潰
  • 在棧上申請的陣列末尾之後寫入資料會損壞棧資料
  • 讀取資料末尾之後的資料並不總是會產生段錯誤,具體取決於訪問的記憶體區域
uint32_t *array = malloc(10 * sizeof(*array));
array[10] = 0xDEADBEEF;

Memory Leaks

▶ 記憶體洩露是一種不會觸發程式崩潰(但遲早會),但會消耗系統記憶體的一種錯誤

▶ 這種情況發生在為程式申請了記憶體,但忘了釋放這段記憶體

▶ 在生產環境中可能執行很長時間才會被發現

  • 最好在開發階段提早發現此類問題
void func1(void) {
uint32_t *array = malloc(10 * sizeof(*array));
do_something_with_array(array);
}

Valgrind memcheck

Valgrind

Valgrind是一個用於構建動態分析工具的工具框架

Valgrind本身也是一個基於該框架的工具,提供了記憶體錯誤檢測、heap profile和其他profile功能

▶ 支援所有流行的平臺:Linux on x86, x86_64, arm(僅armv7), arm64, mips32, s390, ppc32 和 ppc64

▶ 可以將其新增到你的程式碼並執行在其虛擬CPU core上。大大減慢了執行速度,因此適合於除錯和分析

Memcheck是預設的valgrind工具,可以檢測記憶體管理錯誤:

  • 訪問無效的記憶體區域,使用未初始化的值、記憶體洩露、錯誤釋放堆塊等
  • 可以執行在任何應用中,無需編譯
$ valgrind --tool=memcheck --leak-check=full <program>

Valgrind Memcheck usage and report

$ valgrind ./mem_leak
==202104== Memcheck, a memory error detector
==202104== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==202104== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==202104== Command: ./mem_leak
==202104==
==202104== Conditional jump or move depends on uninitialised value(s)
==202104== at 0x109161: do_actual_jump (in /home/user/mem_leak)
==202104== by 0x109187: compute_address (in /home/user/mem_leak)
==202104== by 0x1091A2: do_jump (in /home/user/mem_leak)
==202104== by 0x1091D7: main (in /home/user/mem_leak)
==202104==
==202104== HEAP SUMMARY:
==202104== in use at exit: 120 bytes in 1 blocks
==202104== total heap usage: 1 allocs, 0 frees, 120 bytes allocated
==202104==
==202104== LEAK SUMMARY:
==202104== definitely lost: 120 bytes in 1 blocks
==202104== indirectly lost: 0 bytes in 0 blocks
==202104== possibly lost: 0 bytes in 0 blocks
==202104== still reachable: 0 bytes in 0 blocks
==202104== suppressed: 0 bytes in 0 blocks
==202104== Rerun with --leak-check=full to see details of leaked memory

Valgrind and VGDB

▶ Valgrind還可以作為一個接收處理命令的GDB server。使用者可以透過gdb客戶端或vgdb與valgrind gdb server進行互動。vgdb可以用於如下場景:

  • 作為一個獨立的CLI程式,向valgrind傳送"monitor"命令
  • 作為gdb客戶端和已存在的valgrind會話之間的中繼器
  • 作為一個server,處理來自遠端gdb客戶端的多個valgrind會話

▶ 更多參見man 1 vgdb

Using GDB with Memcheck

valgrind可以將GDB attach到正在分析的程序上

$ valgrind --tool=memcheck --leak-check=full --vgdb=yes --vgdb-error=0 ./mem_leak

▶ 然後將gdb attach到使用vdgb的 valgrind gdbserver上

$ gdb ./mem_leak
(gdb) target remote | vgdb

▶ 如果valgrind檢測到一個錯誤,它會停止執行並進入GDB

(gdb) continue
Continuing.
Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000000000109161 in do_actual_jump (p=0x4a52040) at mem_leak.c:5
5 if (p[1])
(gdb) bt
#0 0x0000000000109161 in do_actual_jump (p=0x4a52040) at mem_leak.c:5
#1 0x0000000000109188 in compute_address (p=0x4a52040) at mem_leak.c:11
#2 0x00000000001091a3 in do_jump (p=0x4a52040) at mem_leak.c:16
#3 0x00000000001091d8 in main () at mem_leak.c:27

Electric Fence

libefence

libefence是一個比valgrind更加輕量的應用,但精度也相對較低

▶ 可以捕獲兩種常見的記憶體錯誤

  • 緩衝溢位和使用釋放的記憶體

libefence可以在遇到第一個錯誤後觸發段錯誤,生成coredump

▶ 可以使用靜態連結或使用LD_PRELOAD方式預載入libefence共享庫

$ gcc -g program.c -o program
$ LD_PRELOAD=libefence.so.0.0 ./program
Electric Fence 2.2 Copyright (C) 1987-1999 Bruce Perens <bruce@perens.com>
Segmentation fault (core dumped)

▶ 根據段錯誤,可以在當前目錄生成一個coredump

▶ 可以使用GDB開啟該coredump,並定位到發生錯誤的位置

$ gdb ./program core-program-3485
Reading symbols from ./libefence...
[New LWP 57462]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Core was generated by `./libefence'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 main () at libefence.c:8
8 data[99] = 1;
(gdb)

Application Profiling

Profiling

▶ Profiling是一個為了透過分析程式、最佳化程式或修復程式問題而從程式執行中收集資料的動作

▶ 可以透過在程式碼中觀插入instrumentation或利用核心/使用者空間機制來實現profiling

  • profile函式呼叫和呼叫次數,以此來最佳化效能
  • profile處理器使用情況來最佳化效能並降低使用的電量
  • profile記憶體使用情況來最佳化使用的記憶體

▶ 在profiling之後,需要使用資料來分析潛在的提升點

Performance issues

▶ profiling通常用於確定和修復效能問題

▶ 記憶體使用、IOs負載或CPU使用等都會影響效能

▶ 在修復效能問題前最好能夠採集profiling資料

▶ profiling時,通常首次會使用一些典型工具進行粗粒度的定位

▶ 一旦確定了問題型別,就可以進行細粒度的profiling

Profiling metrics

▶ 可以透過多種具來採集profile指標

▶ 使用Massif, heaptrackmemusage來profile記憶體使用

▶ 使用perfcallgrind來profile函式呼叫

▶ 使用perf來profile CPU硬體使用(Cache、MMU等)

▶ profiling的資料可以同時包含使用者空間應用和核心資料

Visualizing data with flamegraphs

▶ 基於堆疊的視覺化

▶ 可以快速找到效能瓶頸以及瀏覽呼叫棧

▶ Brendan Gregg工具(因該工具而流行)可以為perf結果生成火焰圖

  • 生成火焰圖的指令碼:https://github.com/brendangregg/FlameGraph
image

Going further with Flamegraphs

▶ 更多參見如下內容(Brendann Gregg的技術演講,展示了火焰圖中的各種指標的用法):

  • Video
  • Slides

Memory profiling

▶ profiling應用的記憶體使用(堆/棧)有助於最佳化效能

▶ 申請過多的記憶體可能會導致系統記憶體耗盡

▶ 頻繁申請/釋放記憶體會導致核心花費大量時間執行 clear_page()

  • 核心需要在將記憶體頁交給程序前清理記憶體頁,避免資料洩露

▶ 降低應用記憶體佔用空間可以最佳化快取使用,如page miss

Massif usage

Massif是一個valgrind提供的工具,可以在程式執行時profile堆使用(僅用於使用者空間)

▶ 原理為建立記憶體申請快照:

$ valgrind --tool=massif --time-unit=B program

▶ 一旦執行,會在當前目錄生成一個 massif.out.<pid> 檔案

▶ 然後可以使用ms_print工具展示堆分配圖:

$ ms_print massif.out.275099

▶ #: 最高記憶體申請

▶ @: 快照細節 (可以透過 --detailed-freq調節數目)

Massif report
image
massif-visualizer - Visualizing massif profiling data
image

heaptrack usage

heaptrack是一個堆記憶體profile工具

  • 需要用到LD_PRELOAD

▶ 具有比Massif更好的tracing和視覺化能力

  • 每個記憶體申請都會關聯到一個棧
  • 可以發現記憶體洩露、記憶體申請熱點和臨時申請的記憶體

▶ 可以透過GUI (heaptrack_gui) 或 CLI 工具 (heaptrack_print)檢視結果

https://github.com/KDE/heaptrack

$ heaptrack program

▶ 最後生成一個heaptrack.<process_name>.<pid>.zst檔案,可以在另外一臺計算機上使用heaptrack_gui檢視分析

heaptrack_gui - Visualizing heaptrack profiling data

image

heaptrack_gui - Flamegraph view

image

memusage

memusage是一個使用libmemusage.so profile 記憶體使用的程式(man 1 memusage) (僅使用者空間)

▶ 可以profile heap、stack以及mmap的記憶體使用

▶ 可以在終端顯示profile資訊,也可以輸出到一個檔案或一個PGN檔案中

▶ 相比valgrind Massif來說,它更輕量(由於使用了LD_PRELOAD機制)

image

memusage usage

$ memusage convert foo.png foo.jpg
Memory usage summary: heap total: 2635857, heap peak: 2250856, stack peak: 83696
         total calls total memory failed calls
 malloc|       1496      2623648 0
realloc|          6         3744 0 (nomove:0, dec:0, free:0)
 calloc|         16         8465 0
   free|       1480      2521334
Histogram for block sizes:
     0-15           329 21% ==================================================
     16-31          239 15% ====================================
     32-47          287 18% ===========================================
     48-63          321 21% ================================================
     64-79           43  2% ======
     80-95          141  9% =====================
...
21424-21439 1 <1%
32768-32783 1 <1%
32816-32831 1 <1%
large       3 <1%

Execution profiling

▶ 為了最佳化程式,需要理解程式使用了哪些硬體資源

▶ 很多硬體元素可能會影響程式執行

  • 如果應用沒有考慮記憶體空間區域性性,則可能會導致CPU快取效能下降
  • 如果應用沒有考慮記憶體空間區域性性,則會導致快取miss
  • 執行不對齊訪問時會產生對齊錯誤

Using perf stat

perf stat可以透過採集效能計數器來profile一個應用

  • 使用效能計數器可能需要root許可權,可以透過# echo -1 > /proc/sys/kernel/perf_event_paranoid修改

▶ 硬體上的效能計數器的數目通常有限

▶ 採集過多資料可能會導致多路複用,perf會放大結果

▶ 採集效能計數器然後進行估算:

  • 為獲取更精確的數值,需要降低event數目並透過多次執行perf來修改期望觀測的events集
  • 更多參見 perf wiki
perf stat example
$ perf stat convert foo.png foo.jpg
Performance counter stats for 'convert foo.png foo.jpg':

        45,52  msec  task-clock               # 1,333 CPUs utilized
            4        context-switches         # 87,874 /sec
            0        cpu-migrations           # 0,000 /sec
        1 672        page-faults              # 36,731 K/sec
  146 154 800        cycles                   # 3,211 GHz                     (81,16%)
    6 984 741        stalled-cycles-frontend  # 4,78% frontend cycles idle    (91,21%)
   81 002 469        stalled-cycles-backend   # 55,42% backend cycles idle    (91,36%)
  222 687 505        instructions             # 1,52 insn per cycle
                                              # 0,36 stalled cycles per insn  (91,21%)
   37 776 174        branches                 # 829,884 M/sec                 (74,51%)
      567 408        branch-misses            # 1,50% of all branches         (70,62%)
      
  0,034156819   seconds time elapsed
  0,041509000   seconds user
  0,004612000   seconds sys

▶ 注意:末尾的百分比是核心計算多路複用情況下的event的持續時間

▶ 列出所有event:

$ perf list
List of pre-defined events (to be used in -e):

branch-instructions OR branches           [Hardware event]
branch-misses                             [Hardware event]
cache-misses                              [Hardware event]
cache-references                          [Hardware event]
...

▶ 統計特定命令的L1-dcache-load-missesbranch-load-misses事件:

$ perf stat -e L1-dcache-load-misses,branch-load-misses cat /etc/fstab
...
Performance counter stats for 'cat /etc/fstab':

23 418         L1-dcache-load-misses
 7 192         branch-load-misses
...

Cachegrind

Cachegrind是一個valgrind提供的用於profile應用指令和資料快取層級的工具

  • Cachegrind還可以profile分支預測成功

▶ 可以模擬一臺具有獨立 I$D$支援的機器,該機器具有統一的 L2快取

▶ 非常有助於檢測快取使用問題(過多miss等)

$ valgrind --tool=cachegrind --cache-sim=yes ./my_program

▶ 會生成一個包含測量結果的cachegrind.out.<pid>檔案

cg_annotate是一個用於展示Cachegrind模擬結果的CLI工具

▶ 它還可以透過--diff選項對比兩個測量結果檔案。

cachegrind的快取模擬存在一些精度缺陷,參見 Cachegrind accuracy

Kcachegrind - Visualizing Cachegrind profiling data
image

Callgrind

Callgrindvalgrind提供的一種可以profile呼叫圖的工具(僅使用者空間)

▶ 可以在程式執行時採集指令數目和與資料相關的原始碼行

▶ 記錄函式和函式有關的呼叫次數:

$ valgrind --tool=callgrind ./my_program

callgrind_annotate 是一個可以展示callgrind模擬結果的CLI工具

Kcachegrind也可以展示callgrind的結果

Kcachegrind - Visualizing Callgrind profiling data
image

System-wide Profiling & Tracing

▶ 優勢問題的根因並不僅限於應用本身,可能會涉及到多個層面(驅動、應用、核心)

▶ 這種情況下,需要分析整個棧

▶ 核心提供了大量可以被特定工具記錄的tracepoints

▶ 可以透過各種機制(如kprobes)來靜態或動態地建立新的tracepoints

Kprobes

▶ Kprobes幾乎可以在任何核心地址動態插入斷點,並抽取除錯和效能資訊

▶ 透過程式碼補丁的方式在文字程式碼中插入呼叫特定的handler的方法

  • kprobes可以在執行hooked指令(即需要除錯的指令)時執行特定的handler
  • 當從一個函式返回時會觸發kretprobes抽取函式的返回值,以及函式呼叫的引數

▶ 需要啟用核心選項CONFIG_KPROBES=y

▶ 由於需要透過模組插入探針,因此需要啟用選項CONFIG_MODULES=yCONFIG_MODULE_UNLOAD=y來允許註冊探針

▶ 當使用symbol_name欄位hooking探針時需要啟用CONFIG_KALLSYMS_ALL=y選項

▶ 更多參見trace/kprobes

Registering a Kprobe

▶ 可以透過載入模組的方式動態註冊kprobes,即透過register_kprobe()註冊一個struct kprobe

▶ 在模組退出時需要透過unregister_kprobe()取消註冊的探針:

struct kprobe probe = {
  .symbol_name = "do_exit",
  .pre_handler = probe_pre,
  .post_handler = probe_post,
};

register_kprobe(&probe);

Registering a kretprobe

▶ kretprobe的註冊方式與普通探針的註冊方式相同,區別是需要透過 register_kretprobe()註冊一個struct kretprobe

  • 在函式進入和退出時會呼叫提供的handler
  • 在模組退出時需要透過unregister_kretprobe()取消註冊的探針
int (*kretprobe_handler_t) (struct kretprobe_instance *, struct pt_regs *);
struct kretprobe probe = {
  .kp.symbol_name = "do_fork",
  .entry_handler = probe_entry,
  .handler = probe_exit,
};

register_kretprobe(&probe);

perf

▶ perf可以執行更大範圍的tracing,並記錄操作

▶ 核心已經包含了可以使用的events和tracepoints,可以透過perf list列出這些內容

▶ 需要透過CONFIG_FTRACE_SYSCALLS啟用syscall tracepoints

▶ 在缺少除錯資訊時,可以在所有符號和暫存器上動態建立新的tracepoint

▶ tracing函式會使用它們的名稱記錄變數和引數內容。需要開啟核心選項CONFIG_DEBUG_INFO

▶ 如果perf無法找到vmlinux,則需要透過-k <vmlinux>提供此檔案。

perf example

▶ 展示匹配syscalls:*的所有events:

$ perf list syscalls:*
List of pre-defined events (to be used in -e):

  syscalls:sys_enter_accept [Tracepoint event]
  syscalls:sys_enter_accept4 [Tracepoint event]
  syscalls:sys_enter_access [Tracepoint event]
  syscalls:sys_enter_adjtimex_time32 [Tracepoint event]
  syscalls:sys_enter_bind [Tracepoint event]
...

▶ 在perf.data檔案中記錄執行sha256sum命令產生的syscalls:sys_enter_read事件:

$ perf record -e syscalls:sys_enter_read sha256sum /bin/busybox
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.018 MB perf.data (215 samples) ]

perf report example

▶ 按照花費的時間展示採集的樣本:

$ perf report
Samples: 591 of event 'cycles', Event count (approx.): 393877062
Overhead Command       Shared Object             Symbol
 22,88%  firefox-esr   [nvidia]                  [k] _nv031568rm
  3,21%  firefox-esr   ld-linux-x86-64.so.2      [.] __minimal_realloc
  2,00%  firefox-esr   libc.so.6                 [.] __stpncpy_ssse3
  1,86%  firefox-esr   libglib-2.0.so.0.7400.0   [.] g_hash_table_lookup
  1,62%  firefox-esr   ld-linux-x86-64.so.2      [.] _dl_strtoul
  1,56%  firefox-esr   [kernel.kallsyms]         [k] clear_page_rep
  1,52%  firefox-esr   libc.so.6                 [.] __strncpy_sse2_unaligned
  1,37%  firefox-esr   ld-linux-x86-64.so.2      [.] strncmp
  1,30%  firefox-esr   firefox-esr               [.] malloc
  1,27%  firefox-esr   libc.so.6                 [.] __GI___strcasecmp_l_ssse3
  1,23%  firefox-esr   [nvidia]                  [k] _nv013165rm
  1,09%  firefox-esr   [nvidia]                  [k] _nv007298rm
  1,03%  firefox-esr   [kernel.kallsyms]         [k] unmap_page_range
  0,91%  firefox-esr   ld-linux-x86-64.so.2      [.] __minimal_free

perf probe

▶ 透過perf probe可以在核心函式和使用者空間函式中建立動態tracepoints

▶ 為了插入探針,需要在核心中啟用CONFIG_KPROBES

  • 注意:使用perf探針時需要編譯libelf檔案

▶ 在建立新的動態探針之後就可以在perf record中使用此探針

▶ 嵌入式平臺中通常不存在vmlinux,此時只能使用符號和暫存器

perf probe examples

▶ 列出所有可以被探測的核心符號:

$ perf probe --funcs

▶ 使用filename引數在do_sys_openat2上建立一個新的探針:

$ perf probe --vmlinux=vmlinux_file do_sys_openat2 filename:string
Added new event:
	probe:do_sys_openat2 (on do_sys_openat2 with filename:string)

▶ 執行tail並捕獲前面建立的探針事件:

$ perf record -e probe:do_sys_openat2 tail /var/log/messages
...
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.003 MB perf.data (19 samples) ]

▶ 使用perf script展示記錄的tracepoint:

$ perf script
tail 164 [000] 3552.956573: probe:do_sys_openat2: (c02c3750) filename_string="/etc/ld.so.cache"
tail 164 [000] 3552.956642: probe:do_sys_openat2: (c02c3750) filename_string="/lib/tls/v7l/neon/vfp/libresolv.so.2"
...

▶ 在ksys_read上建立新的探針,並使用r0(ARM)返回值(賦給ret):

$ perf probe ksys_read%return ret=%r0

▶ 執行sha256sum並捕獲前面建立的探針事件:

$ perf record -e probe:ksys_read__return sha256sum /etc/fstab

▶ 展示建立的所有探針:

$ perf probe -l
probe:ksys_read__return (on ksys_read%return with ret)

▶ 移除一個已存在的tracepoint:

$ perf probe -d probe:ksys_read__return

perf record example

▶ 記錄所有的CPU events(系統模式)

$ perf record -a
^C

▶ 使用perf script展示perf.data記錄的events

$ perf script
...
klogd   85 [000]  208.609712:  116584  cycles:  b6dd551c   memset+0x2c (/lib/libc.so.6)
klogd   85 [000]  208.609898:  121267  cycles:  c0a44c84   _raw_spin_unlock_irq+0x34 (vmlinux)
klogd   85 [000]  208.610094:  127434  cycles:  c02f3ef4   kmem_cache_alloc+0xd0 (vmlinux)
 perf   130 [000] 208.610311:  132915  cycles:  c0a44c84   _raw_spin_unlock_irq+0x34 (vmlinux)
 perf   130 [000] 208.619831:  143834  cycles:  c0a44cf4   _raw_spin_unlock_irqrestore+0x3c (vmlinux)
klogd   85 [000]  208.620048:  143834  cycles:  c01a07f8   syslog_print+0x170 (vmlinux)
klogd   85 [000]  208.620241:  126328  cycles:  c0100184   vector_swi+0x44 (vmlinux)
klogd   85 [000]  208.620434:  128451  cycles:  c096f228   unix_dgram_sendmsg+0x46c (vmlinux)
kworker/0:2-mm_ 44 [000] 208.620653: 133104 cycles: c0a44c84 _raw_spin_unlock_irq+0x34 (vmlinux)
 perf   130 [000] 208.620859:  138065  cycles:  c0198460   lock_acquire+0x184 (vmlinux)
...

Using perf trace

perf trace可以捕獲和展示在執行命令時觸發的所有tracepoints/events。

$ perf trace -e "net:*" ping -c 1 192.168.1.1
PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data.
    0.000 ping/37820 net:net_dev_queue(skbaddr: 0xffff97bbc6a17900, len: 98, name: "enp34s0")
    0.005 ping/37820 net:net_dev_start_xmit(name: "enp34s0",
      skbaddr: 0xffff97bbc6a17900, protocol: 2048, len: 98,
      network_offset: 14, transport_offset_valid: 1, transport_offset: 34)
    0.009 ping/37820 net:net_dev_xmit(skbaddr: 0xffff97bbc6a17900, len: 98,name: "enp34s0")
64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=0.867 ms

Using perf top

perf top可以實時分析核心

▶ 可以取樣函式呼叫,並按照時間消耗排序

▶ 可以profile整個系統:

$ perf top
Samples: 19K of event 'cycles', 4000 Hz, Event count (approx.): 4571734204 lost: 0/0 drop: 0/0
Overhead   Shared Object    Symbol
    2,01%  [nvidia]         [k] _nv023368rm
    0,94%  [kernel]         [k] __static_call_text_end
    0,89%  [vdso]           [.] 0x0000000000000655
    0,81%  [nvidia]         [k] _nv027733rm
    0,79%  [kernel]         [k] clear_page_rep
    0,76%  [kernel]         [k] psi_group_change
    0,70%  [kernel]         [k] check_preemption_disabled
    0,69%  code [.]         0x000000000623108f
    0,60%  code [.]         0x0000000006231083
    0,59%  [kernel]         [k] preempt_count_add
    0,54%  [kernel]         [k] module_get_kallsym
    0,53%  [kernel]         [k] copy_user_generic_string

ftrace and trace-cmd

ftrace

ftrace是一個核心tracing框架,為"Function Tracer"的簡稱

▶ 為觀測系統行為提供了廣泛的tracing能力

  • 可以跟蹤已經存在核心中的tracepoints(scheduler、interrupts等)
  • 依賴GCC 的mount() 能力和核心程式碼補丁機制來呼叫ftrace tracing handler

▶ 所有跟蹤資料都儲存在一個ring buffer中

▶ 使用tracefs檔案系統來控制和展示tracing events

  • # mount -t tracefs nodev /sys/kernel/tracing.
    

▶ 使用ftrace前必須開啟核心選項CONFIG_FTRACE=y

CONFIG_DYNAMIC_FTRACE可以讓加入的trace功能在不使用時對系統效能幾乎沒有影響。

ftrace files

ftrace透過/sys/kernel/tracing中的特定檔案來控制跟蹤的內容:

  • current_tracer: 當前使用的tracer
  • available_tracers: 列出編譯進核心的可用tracers
  • tracing_on: 啟用/禁用 tracing.
  • trace: 以可讀方式展示跟蹤。不同的tracer可能會有不同的格式
  • trace_pipe: 與trace類似,但每次讀都會消費其讀取的跟蹤資料
  • trace_marker{_raw}: 可以向跟蹤緩衝區中的使用者空間同步核心事件
  • set_ftrace_filter: 過濾特定的函式
  • set_graph_function: 以圖形方式展示特定的函式的子函式

▶ 還有其他控制跟蹤的檔案,參見trace/ftrace

▶ 可以使用trace-cmd CLI 和 Kernelshark GUI記錄和展示tracing資料

ftrace tracers

▶ ftrace提供了多種"tracers"

▶ 需要將使用的tracer寫入current_tracer檔案

  • nop:不執行跟蹤,禁用所有tracing
  • function:跟蹤所有呼叫的核心函式
  • function_graph:類似function,但會跟蹤函式的進入和退出
  • hwlat:跟蹤硬體延遲
  • irqsoff:跟蹤禁用中斷的部分,並記錄延遲
  • branch:跟蹤likely()/unlikely()分支預測呼叫
  • mmiotrace:跟蹤所有硬體訪問(read[bwlq]/write[bwlq])

▶ 警告:有些tracer開銷可能會比較大

# echo "function" > /sys/kernel/tracing/current_tracer

function_graph tracer report example

function_graph可以跟蹤所有函式及其相關的呼叫樹

▶ 可以展示程序、CPU、時間戳和函式呼叫圖

$ trace-cmd report
...
dd-113  [000]  304.526590: funcgraph_entry:                |   sys_write() {
dd-113  [000]  304.526597: funcgraph_entry:                |     ksys_write() {
dd-113  [000]  304.526603: funcgraph_entry:                |       __fdget_pos() {
dd-113  [000]  304.526609: funcgraph_entry:     6.541 us   |         __fget_light();
dd-113  [000]  304.526621: funcgraph_exit:    + 18.500 us  |       }
dd-113  [000]  304.526627: funcgraph_entry:                |       vfs_write() {
dd-113  [000]  304.526634: funcgraph_entry:     6.334 us   |         rw_verify_area();
dd-113  [000]  304.526646: funcgraph_entry:     6.208 us   |         write_null();
dd-113  [000]  304.526658: funcgraph_entry:     6.292 us   |         __fsnotify_parent();
dd-113  [000]  304.526669: funcgraph_exit:    + 43.042 us  |       }
dd-113  [000]  304.526675: funcgraph_exit:    + 78.833 us  |     }
dd-113  [000]  304.526680: funcgraph_exit:    + 91.291 us  |   }
dd-113  [000]  304.526689: funcgraph_entry:                |   sys_read() {
dd-113  [000]  304.526695: funcgraph_entry:                |     ksys_read() {
dd-113  [000]  304.526702: funcgraph_entry:                |       __fdget_pos() {
dd-113  [000]  304.526708: funcgraph_entry:     6.167 us   |         __fget_light();
dd-113  [000]  304.526719: funcgraph_exit:    + 18.083 us  |       }

irqsoff tracer

▶ ftrace irqsoff tracer可以跟蹤由於太長時間禁用中斷而導致的中斷延遲

▶ 可以幫助定位系統中斷延遲高的問題

▶ 需要啟用IRQSOFF_TRACER=y

  • preemptoffpremptirqsoff tracer可以跟蹤禁用搶佔的程式碼段
image

irqsoff tracer report example

# latency: 276 us, #104/104, CPU#0 | (M:preempt VP:0, KP:0, SP:0 HP:0 #P:2)
#    -----------------
#    | task: stress-ng-114 (uid:0 nice:0 policy:0 rt_prio:0)
#    -----------------
# => started at: __irq_usr
# => ended at: irq_exit
#
#
#                  _------=> CPU#
#                 /  _-----=> irqs-off
#                 | / _----=> need-resched
#                 || / _---=> hardirq/softirq
#                 ||| / _--=> preempt-depth
#                 |||| /     delay
#    cmd  pid     |||||   time | caller
#      \  /       |||||     \  |   /
stress-n-114      0d...     2us : __irq_usr
stress-n-114      0d...     7us : gic_handle_irq <-__irq_usr
stress-n-114      0d...    10us : __handle_domain_irq <-gic_handle_irq
...
stress-n-114      0d...   270us : __local_bh_disable_ip <-__do_softirq
stress-n-114      0d.s.   275us : __do_softirq <-irq_exit
stress-n-114      0d.s.   279us+: tracer_hardirqs_on <-irq_exit
stress-n-114      0d.s.   290us : <stack trace>

Hardware latency detector

▶ ftrace hwlat tracer 可以幫助查詢硬體是否產生延遲

  • 如,不可遮蔽的系統管理中斷會直接觸發某些韌體支援特性,導致CPU暫停執行
  • 某些安全監控產生的中斷也可能會導致延遲

▶ 如果使用該tracer發現了某種延遲,說明該系統可能不適合實時用途

▶ 原理為在禁用中斷的情況下在單核上迴圈執行指令,並計算連續的兩次讀之間的時間差

▶ 需要啟用CONFIG_HWLAT_TRACER=y

image

trace_printk()

▶ Utrace_printk()可以向跟蹤快取中輸出字串

▶ 可以跟蹤程式碼中的特定條件並將其展示在跟蹤快取中:

#include <linux/ftrace.h>
void read_hw()
{
	if (condition)
		trace_printk("Condition is true!\n");
}

▶ 在跟蹤快取中使用function_graph tracer展示如下結果:

1)             |           read_hw() {
1)             |               /* Condition is true! */
1) 2.657 us    |           }

trace-cmd

trace-cmd 是Steven Rostedt編寫的一款用於和ftrace互動的工具(man 1 trace-cmd)

trace-cmd支援的tracer為ftrace暴露的tracer

trace-cmd支援多個命令:

  • list:列出可以被記錄的各種plugins/events
  • record:將一條trace寫入trace.dat檔案
  • report:展示trace.dat獲取的結果

▶ 在採集結束之後,會生成一個 trace.dat檔案

Remote tracing with trace-cmd

trace-cmd 的輸出可能會相當大,因此很難將其儲存在儲存有限的嵌入式平臺

▶ 為此,可以使用listen命令透過網路傳送結果:

  • 在需要採集tracing的遠端系統上執行 trace-cmd listen -p 6578

  • 在目標系統上,使用trace-cmd record -N <target_ip>:6578指定採集tracing資訊的遠端系統

    image

trace-cmd examples

▶ 列出可用的tracers:

$ trace-cmd list -t
blk mmiotrace function_graph function nop

▶ 列出可用的events:

$ trace-cmd list -e
...
migrate:mm_migrate_pages_start
migrate:mm_migrate_pages
tlb:tlb_flush
syscalls:sys_exit_process_vm_writev
...

▶ 列出functionfunction_graph tracers可過濾的函式:

$ trace-cmd list -f
...
wait_for_initramfs
__ftrace_invalid_address___64
calibration_delay_done
calibrate_delay
...

▶ 啟用function tracer並在系統上記錄全域性資料:

$ trace-cmd record -p function

▶ 使用function graph tracer跟蹤dd命令:

$ trace-cmd record -p function_graph dd if=/dev/mmcblk0 of=out bs=512 count=10

▶ 展示trace.dat的資料:

$ trace-cmd report

▶ 重置所有ftrace緩衝並移除tracers:

$ trace-cmd reset

▶ 在系統上執行irqsoff tracer:

$ trace-cmd record -p irqsoff

▶ 只記錄系統的irq_handler_exit/irq_handler_entry events:

$ trace-cmd record -e irq:irq_handler_exit -e irq:irq_handler_entry

Adding ftrace tracepoints

▶ 出於自定義的需要,可以新增自定義tracepoints

▶ 首先需要在一個.h檔案中宣告該tracepoint

#undef TRACE_SYSTEM
#define TRACE_SYSTEM subsys

#if !defined(_TRACE_SUBSYS_H) || defined(TRACE_HEADER_MULTI_READ)
#define _TRACE_SUBSYS_H

#include <linux/tracepoint.h>

DECLARE_TRACE(subsys_eventname,
        TP_PROTO(int firstarg, struct task_struct *p),
        TP_ARGS(firstarg, p));

#endif /* _TRACE_SUBSYS_H */

/* This part must be outside protection */
#include <trace/define_trace.h>

▶ 然後使用上述標頭檔案注入tracepoint:

#include <trace/events/subsys.h>

#define CREATE_TRACE_POINTS
DEFINE_TRACE(subsys_eventname);

void any_func(void)
{
  ...
  trace_subsys_eventname(arg, task);
  ...
}

▶ 更多資訊,參見trace/tracepoints

Kernelshark

▶ Kernelshark是一個基於Qt的可以處理trace-cmd trace.dat報告的影像介面

▶ 可以透過trace-cmd配置和獲取資料

▶ 使用不同的顏色來展示記錄的CPU和tasks events

▶ 可以用於特定bug的進一步分析

image

LTTng

▶ LTTng是一個由EfficiOS 公司維護的Linux開源tracing框架

▶ 透過LTTng可以瞭解到核心和應用之間的互動(C、C++、Java、Python)

  • 還未應用暴露了一個/dev/lttng-logger

▶ Tracepoints會關聯一個payload

▶ LTTng注重低開銷的tracing

▶ 使用Common Trace Format(因此可以使用babeltrace或trace-compass之類的軟體讀取trace資料)

Tracepoints with LTTng

▶ LTTng有一個session守護程序,用於接收從核心和使用者空間的LTTng tracing元件產生的events

▶ LTTng可以用於跟蹤如下內容:

  • LTTng核心tracepoints
  • kprobes和kretprobes
  • Linux核心系統呼叫
  • Libux使用者空間probe
  • 使用者空間的LTTng tracepoints

Creating userspace tracepoints with LTTng

▶ 可以使用LTTng定義新的使用者空間tracepoints

▶ 可以為一個tracepoint配置多個屬性

  • 一個provider名稱空間
  • 一個辨別tracepoint的名稱
  • 各種型別引數(int、char*等)
  • 描述如何展示tracepoint引數的欄位(十進位制、十六禁止等),參見LTTng-ust

▶ 為了使用UST tracepoint,開發者需要執行多個操作:編寫一個tracepoint provider(.h),編寫一個tracepoint package(.c),構建package,在被跟蹤的應用中呼叫該tracepoint,最後構建應用,連結lttng-ust庫和package provider。

▶ LTTng提供了 lttng-gen-tp簡化這些步驟,只需要編寫一個模板(.tp)檔案即可

Defining a LTTng tracepoint

▶ Tracepoint模板(hello_world-tp.tp)

LTTNG_UST_TRACEPOINT_EVENT(
  // Tracepoint provider name
  hello_world,
  
  // Tracepoint/event name
  first_tp,
  
  // Tracepoint arguments (input)
  LTTNG_UST_TP_ARGS(
  char *, text
  ),
  
  // Tracepoint/event fields (output)
  LTTNG_UST_TP_FIELDS(
  	lttng_ust_field_string(message, text)
  )
)

▶ lttng-gen-tp會使用該模板檔案來生成/構建所需的檔案(.h,.c和.o檔案)

Defining a LTTng tracepoint

▶ 構建tracepoint provider:

$ lttng-gen-tp hello_world-tp.tp

▶ 使用Tracepoint(hello_world.c)

#include <stdio.h>
#include "hello-tp.h"

int main(int argc, char *argv[])
{
    lttng_ust_tracepoint(hello_world, my_first_tracepoint, 23, "hi there!");
    return 0;
}

▶ 編譯:

$ gcc hello_world.c hello_world-tp.o -llttng-ust -o hello_world

Using LTTng

$ lttng create my-tracing-session --output=./my_traces
$ lttng list --kernel
$ lttng list --userspace
$ lttng enable-event --userspace hello_world:my_first_tracepoint
$ lttng enable-event --kernel --syscall open,close,write
$ lttng start
$ /* Run your application or do something */
$ lttng destroy
$ babeltrace2 ./my_traces

▶ 可以使用 trace-compass來展示結果

Remote tracing with LTTng

▶ LTTng可以透過網路記錄跟蹤資料

▶ 適用於只有有限儲存的嵌入式系統

▶ 在遠端計算機上執行lttng-relayd命令

$ lttng-relayd --output=${PWD}/traces

▶ 在目標機器上建立的會話中指定--set-url:

$ lttng create my-session --set-url=net://remote-system

▶ 這樣就可以直接記錄遠端計算機的跟蹤資訊

eBPF

The ancestor: Berkeley Packet filter

▶ BPF是Berkeley Packet Filter的簡稱,一開始用於網路報文過濾

▶ BPF用於Linux的Socket過濾(參見networking/filter)

▶ tcpdump和Wireshark嚴重依賴BPF(透過libpcap)進行報文捕獲

BPF in libpcap: setup

▶ tcpdump可以將使用者的報文過濾字串傳入libpcap

▶ libpcap會將捕獲過濾器轉換為一個二進位制程式

  • 該程式使用一個抽象的機器指令集(BPF指令集)

▶ libpcap透過setsockopt()系統呼叫將該二進位制程式傳送到核心

image

BPF in libpcap: capture

image

▶ 核心實現了BPF"虛擬機器"

▶ BPF虛擬機器為每個報文執行BPF程式

▶ 程式會檢查報文資料,如果需要捕獲報文,則返回一個非0值

▶ 如果返回值非0,則除了常規的報文處理之外,還會捕獲報文

eBPF

eBPF是一種允許在核心中安全有效地執行使用者程式的新框架。於核心3.18版本引入,且仍然在演化和頻繁更新中

▶ eBPF程可以捕獲並向使用者空間暴露核心資料,以及基於一些使用者定義的規則來改變核心行為

▶ eBPF是事件驅動的:特定的核心事件可以觸發並執行eBPF程式

▶ eBPF的一個主要好處是可以重新程式設計核心行為,而無需針對核心開發:

  • 不會因為bug導致核心崩潰
  • 可以實現更快的特性開發週期

▶ eBPF值得注意的特性有:

  • 新的指令集、中斷器和校驗器
  • 更大範圍的"attach"位置,幾乎可以在核心的任何位置hook程式
  • 使用名為"maps"的特定結構來在多個eBPF程式之間或程式和使用者空間之間交換資料
  • 使用一個特定的bpf() 系統呼叫來操作eBPF程式和資料
  • eBPF程式中提供了大量核心輔助函式

eBPF program lifecycle

image

Kernel configuration for eBPF

▶ 透過CONFIG_NET啟用eBPF子程式

▶ 透過CONFIG_BPF_SYSCALL啟用bpf()系統呼叫

▶ 透過CONFIG_BPF_JIT在程式中啟用JIT,提升效能

CONFIG_BPF_JIT_ALWAYS_ON強制啟用JIT

CONFIG_BPF_UNPRIV_DEFAULT_OFF=n 可以在開發階段允許非root使用eBPF

▶ 你可能想要透過更多特性來解鎖特定的hook位置:

  • CONFIG_KPROBES可以在kprobes上hook程式
  • CONFIG_TRACING 可以在核心tracepoint上hook程式
  • CONFIG_NET_CLS_BPF可以編寫報文分類器
  • CONFIG_CGROUP_BPF 可以在cgroup hook上attach programs

eBPF ISA

▶ eBPF是一個"虛擬的" ISA,定義了其所有的指令集:載入和儲存指令、算術指令、跳轉指令等

▶ 它還定義了一組10個64位的暫存器,以及一個呼叫準則:

  • R0: 函式和BPF程式的返回值
  • R1, R2, R3, R4, R5: 函式引數
  • R6, R7, R8, R9: 呼叫儲存暫存器
  • R10: 棧指標
; bpf_printk("Hello %s\n", "World");
    0: r1 = 0x0 ll
    2: r2 = 0xa
    3: r3 = 0x0 ll
    5: call 0x6
; return 0;
    6: r0 = 0x0
    7: exit

The eBPF verifier

▶ 在將一個程式載入到核心時,eBPF verifier會校驗程式的有效性

▶ verifier是一個複雜的軟體片段,用於透過一組規則來校驗eBPF程式,確保執行的程式碼不會損害整個核心。如:

  • 程式必須返回,否則不確定的程式碼路徑可能會導致無限執行(如無限迴圈)
  • 程式必須保證引用的指標是有效的
  • 程式不能隨意訪問記憶體地址,必須透過context或有效的helpers

▶ 如果一個程式違背了verifier的規則,則拒絕該程式

▶ 除了verifier的要求之外,在編寫程式時也必須格外小心。eBPF程式啟用了搶佔(但禁用CPU遷移),因此仍然可能會受到併發問題的影響

  • 可以透過一些機制和helpers來避免這些問題,比如per-cpu maps型別

Program types and attach points

▶ eBPF可以在不同型別的位置hook一個程式:

  • 任意kprobe
  • 核心定義的靜態tracepoint
  • 特定的perf event
  • 整個網路棧
  • 更多參見bpf_attach_type

▶ 特定的attach點有可能僅支援hook一部分特定的程式,參見bpf_prog_typebpf/libbpf/program_types

▶ 程式型別定義了程式被呼叫時傳入eBPF程式的資料,如:

  • BPF_PROG_TYPE_TRACEPOINT 程式會接收一個包含目標tracepoint返回給使用者空間的所有資料的結構。
  • BPF_PROG_TYPE_SCHED_CLS 程式(用於實現報文分類器)將接收一個struct __sk_buff, 在核心中體現為一個Socket buffer
  • 更多傳遞到程式型別的上下文,參見 include/linux/bpf_types.h

eBPF maps

▶ eBPF可以透過不同的maps與使用者空間或其他程式互動資料:

  • BPF_MAP_TYPE_ARRAY:通用陣列儲存。可以劃分不同的CPU
  • BPF_MAP_TYPE_HASH:包含key-value的儲存。keys可以是不同的型別:__u32、裝置型別、IP地址等
  • BPF_MAP_TYPE_QUEUE:FIFO型別佇列
  • BPF_MAP_TYPE_CGROUP_STORAGE:使用cgroup id作為key的一種hash map。除此之外還有其他物件型別的maps(inodes、tasks、sockets等)

▶ 對於基本的資料,簡單有效的方式是直接使用eBPF的全域性變數(與maps相反,不涉及系統呼叫)

The bpf() syscall

▶ 核心透過暴露一個bpf()系統呼叫來允許和eBPF子系統進行互動

▶ 該系統呼叫有一個子命令集,並根據不同的子命令接收特定的資料:

  • BPF_PROG_LOAD:載入一個bpf程式
  • BPF_MAP_CREATE:為程式分配使用的maps
  • BPF_MAP_LOOKUP_ELEM:在map中查詢表項
  • BPF_MAP_UPDATE_ELEM:更新map中的表項

▶ 該系統呼叫使用指向eBPF資源的檔案描述符。只要至少有一個程式持有有效的檔案描述符,則這些資源(program、maps、links等)將一直有效。如果沒有程式使用,則這些資源將會被自動清理。

▶ 更多參見man 2 bpf

Writing eBPF programs

▶ 可以直接使用原始的eBPF彙編或高階語言(如C或rust)編寫eBPF程式,並使用clang編譯器進行編譯。

▶ 核心為eBPF程式提供了一個輔助函式:

  • bpf_trace_printk 將log傳遞到trace buffer
  • bpf_map_{lookup,update,delete}_elem 操作maps
  • bpf_probe_{read,write}[_user] 安全地從/向核心或使用者空間讀/寫資料
  • bpf_get_current_pid_tgid 返回當前程序ID和執行緒組ID
  • bpf_get_current_uid_gid 返回當前使用者ID和組ID
  • bpf_get_current_comm 返回當前task中的可執行檔案的名稱
  • bpf_get_current_task 返回當前 struct task_struct
  • 更多輔助函式,參見man 7 bpf-helpers

▶ 核心還暴露了kfuncs(參見bpf/kfuncs),但與bpf輔助函式相反,它們並不屬於核心的穩定介面

Manipulating eBPF program

▶ 有多種方式可以構建、載入和管理eBPF程式:

  • 一種是可以編寫一個eBPF程式,使用clang進行構建,然後載入,在attach之後,在自定義使用者空間程式中使用bpf()讀取資料
  • 還可以使用bpftool操作構建好的eBPF程式(load、attach、read maps等),無需編寫任何使用者空間工具
  • 或者可以透過一些中間庫來編寫自己的eBPF工具來處理一些負載的工作,如libbpf
  • 還可以使用特定的框架,如BCC或bpftrace

BCC

▶ BPF Compiler Collection (BCC) 是一個基於BPF的工具集

▶ BCC提供了大量現成的基於BPF的工具

▶ 還提供了比使用"原始"的BPF語言更簡單的用於編寫、載入和hook BPF的程式的介面

▶ 適用於大量平臺(但不包括ARM32)

  • 在debian架構中,所有工具名為<tool>-bpfcc

▶ BCC要求核心版本>=4.1

▶ BCC的演化很快,很多發行版的版本都比較舊:你可能需要編譯最新的原始碼。

BCC tools

image

BCC Tools example

profile.py 是一個CPU profiler,可以捕獲當前執行的棧。可以將輸出轉換為火焰圖:

$ git clone https://github.com/brendangregg/FlameGraph.git
$ profile.py -df -F 99 10 | ./FlameGraph/flamegraph.pl > flamegraph.svg

tcpconnect.py展示了所有新的TCP連線:

$ tcpconnect
PID COMM IP SADDR DADDR DPORT
220321 ssh 6 ::1 ::1 22
220321 ssh 4 127.0.0.1 127.0.0.1 22
17676 Chrome_Child 6 2a01:cb15:81e4:8100:37cf:d45b:d87d:d97d 2606:50c0:8003::154 443
[...]

▶ 更多參見https://github.com/iovisor/bcc

Using BCC with python

▶ BCC暴露了一個bcc模組,以及一個BPF

▶ eBPF程式使用C語言編寫,將其儲存到外部檔案或直接作為一個python字串

▶ 當建立一個BPF類的例項,並將其(以檔案或字串形式)提供給eBPF程式時,它會自動構建、載入並attach程式

▶ 有多種attach一個程式的方式:

  • 根據目標attach點,使用合適的程式名字首(這樣會自動執行attach步驟)
  • 透過明確呼叫之前建立的BPF例項方法

Using BCC with python

▶ 使用kprobe hook clone()系統呼叫,每次hook時列印"Hello, World!"。

from bcc import BPF

# define BPF program
prog = """
int hello(void *ctx) {
  bpf_trace_printk("Hello, World!\\n");
  return 0;
}
"""
# load BPF program
b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello")

libbpf

▶ 除使用BCC這樣的高階框架之外,還可以使用libbpf構建自定義工具,更好地控制程式的方方面面

▶ libbpf是一個基於C的庫,透過如下特性來降低eBPF程式設計的複雜度:

  • 用於處理open/load/attach/teardown bpf程式的使用者空間API
  • 用於與attach的程式互動的使用者空間API
  • 簡化編寫eBPF程式的eBPF APIs

▶ 很多發行版和構建系統(如Buildroot)都打包了libbpf

▶ 更多參見https://libbpf.readthedocs.io/en/latest/

eBPF programming with libbpf

my_prog.bpf.c

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

#define TASK_COMM_LEN 16
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, 1);
} counter_map SEC(".maps");

struct sched_switch_args {
    unsigned long long pad;
    char prev_comm[TASK_COMM_LEN];
    int prev_pid;
    int prev_prio;
    long long prev_state;
    char next_comm[TASK_COMM_LEN];
    int next_pid;
    int next_prio;
};

SEC("tracepoint/sched/sched_switch")
int sched_tracer(struct sched_switch_args *ctx)
{
    __u32 key = 0;
    __u64 *counter;
    char *file;

    char fmt[] = "Old task was %s, new task is %s\n";
    bpf_trace_printk(fmt, sizeof(fmt), ctx->prev_comm, ctx->next_comm);

    counter = bpf_map_lookup_elem(&counter_map, &key);
    if(counter) {
        *counter += 1;
        bpf_map_update_elem(&counter_map, &key, counter, 0);
    }

    return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";

Building eBPF programs

▶ eBPF使用C編寫,可以透過clang構建為一個可載入的物件:

$ clang -target bpf -O2 -g -c my_prog.bpf.c -o my_prog.bpf.o

▶ 最近的版本中也可以使用GCC:

  • 可以在Debian/Ubuntu中使用gcc-bpf安裝工具鏈
  • 它暴露了bpf-unknown-none目標

▶ 為了簡化在使用者空間程式中操作基於libbpf的程式 ,我們需要"skeleton" API,透過 bpftool 生成這些 API 可以

bpftool

bpftool是一個可以透過與bpf物件檔案和核心互動來管理bpf程式的命令列工具:

  • 將程式載入到核心
  • 列出載入的程式
  • dump程式指令,BPF程式碼或JIT程式碼
  • dump map內容
  • 將程式attach到hooks等

▶ 你可能需要mount bpf檔案系統來pin程式(即在bpftool結束執行之後仍然載入程式)

$ mount -t bpf none /sys/fs/bpf

▶ 列出載入的程式:

$ bpftool prog
348: tracepoint name sched_tracer tag 3051de4551f07909 gpl
loaded_at 2024-08-06T15:43:11+0200 uid 0
xlated 376B jited 215B memlock 4096B map_ids 146,148
btf_id 545

▶ 載入並attach 一個程式:

$ mkdir /sys/fs/bpf/myprog
$ bpftool prog loadall trace_execve.bpf.o /sys/fs/bpf/myprog autoattach

▶ 解除安裝一個程式:

$ rm -rf /sys/fs/bpf/myprog

▶ dump一個載入的程式:

$ bpftool prog dump xlated id 348
int sched_tracer(struct sched_switch_args * ctx):
; int sched_tracer(struct sched_switch_args *ctx)
  0: (bf) r4 = r1
  1: (b7) r1 = 0
; __u32 key = 0;
	2: (63) *(u32 *)(r10 -4) = r1
; char fmt[] = "Old task was %s, new task is %s\n";
  3: (73) *(u8 *)(r10 -8) = r1
  4: (18) r1 = 0xa7325207369206b
  6: (7b) *(u64 *)(r10 -16) = r1
  7: (18) r1 = 0x7361742077656e20
[...]

▶ dump eBPF程式logs:

image

▶ 列出建立的maps:

$ bpftool map
80: array name counter_map flags 0x0
    key 4B value 8B max_entries 1 memlock 256B
    btf_id 421
82: array name .rodata.str1.1 flags 0x80
    key 4B value 33B max_entries 1 memlock 288B
    frozen
96: array name libbpf_global flags 0x0
		key 4B value 32B max_entries 1 memlock 280B
[...] 

▶ 展示一個map的內容:

$ sudo bpftool map dump id 80
[{
  "key": 0,
  "value": 4877514 }
]

▶ 生成libbpf API來操作一個程式:

$ bpftool gen skeleton trace_execve.bpf.o name trace_execve > trace_execve.skel.h

▶ 我們可以使用高階API編寫自己的使用者空間程式來更好地操作自己的eBPF程式:

  • 例項化一個可以被所有程式、maps、links等引用的全域性上下文物件
  • 載入/attact/解除安裝程式
  • eBPF 程式作為位元組陣列直接嵌入到生成的header中

Userspace code with libbpf

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include "trace_sched_switch.skel.h"
int main(int argc, char *argv[])
{
    struct trace_sched_switch *skel;
    int key = 0;
    long counter = 0;

    skel = trace_sched_switch__open_and_load();
    if(!skel)
        exit(EXIT_FAILURE);
    if (trace_sched_switch__attach(skel)) {
        trace_sched_switch__destroy(skel);
        exit(EXIT_FAILURE);
    }

    while(true) {
        bpf_map__lookup_elem(skel->maps.counter_map, &key, sizeof(key), &counter, sizeof(counter), 0);
        fprintf(stderr, "Scheduling switch count: %d\n", counter);
        sleep(1);
    }

    return 0;
}

eBPF programs portability

▶ 與使用者空間API相反,核心內部不會暴露穩定的API,這意味著可以操作某些核心資料的eBPF程式並不一定可以在其他版本的核心上執行

▶ CO-RE(Compile Once - Run Everywhere)用於解決該問題,使得程式可以在不同版本的核心之間進行移植,它依賴如下特性:

  • 核心必須透過CONFIG_DEBUG_INFO_BTF=y構建來嵌入BTF。BTF是一個與dwarf類似的格式,可以高效地編碼資料佈局以及函式簽名
  • eBPF編譯器必須能發出BTF重定位(最近版本的clang和GCC都支援,使用-g引數)
  • 需要一個能夠處理基於BTF資料的BPF程式以及調節對應的資料訪問的BPF載入器。libbpf是實際上的標準bpf載入器
  • 需要eBPF API來讀/寫CO-RE重定向的變數。libbpf提供了這類輔助函式,如bpf_core_read

▶ 更多參見Andrii Nakryiko’s CO-RE guide

▶ 除了CO-RE外,由於核心主特性的引入或變更,還可能會面臨不同核心版本的不同限制(eBPF子系統在持續頻繁更新中):

  • 4.2版本中加入了eBPF尾部呼叫(可以允許一個程式呼叫一個函式),5.10版本中可以允許呼叫另一個程式
  • 5.1版本中加入了eBPF自旋鎖,防止在不同CPUs間併發訪問共享的maps
  • 不斷引入不同的attach型別,但可能存在不同架構的不同版本中。如fentry/fexit attach points在x86的5.5核心中引入,但卻在arm32的6.0版本中引入。
  • 5.3版本之前禁止任何型別的迴圈(即使是有界的)
  • 5.8版本加入的CAP_BPF可以允許執行一個eBPF任務

eBPF for tracing/profiling

▶ eBPF是一個非常強大的可以探測核心內部的框架:透過大量attach 點,幾乎可以暴露任何核心路徑和程式碼

▶ 同時,eBPF程式x和核心程式碼隔離,使之(相比核心開發)更安全更簡單

▶ 由於核心翻譯器和最佳化措施,如JIT編譯的存在,eBPF非常適合低開銷的tracing和profiling,即使在生產環境中也非常靈活

▶ 這也是為什麼eBPF在debugging、tracing和profiling中接納度不斷增加地原因。eBPF可以用於:

  • tracing框架,如BCCbpftrace
  • 網路基礎設定元件,如CiliumCalico
  • 網路報文跟蹤器,如pwrudropwatch
  • 更多例子,參見ebpf.io

eBPF: resources

▶ BCC教程:https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md

▶ libbpf-bootstrap: https://github.com/libbpf/libbpf-bootstrap

▶ A Beginner’s Guide to eBPF Programming - Liz Rice, 2020

  • 影片: https://www.youtube.com/watch?v=lrSExTfS-iQ
  • 資源: https://github.com/lizrice/ebpf-beginners

Choosing the right tool

▶ 在開始profile或trace之前,需要知道使用哪種型別的工具。

▶ 通常根據profile的級別來選擇工具

▶ 通常一開始會使用應用tracing/profiling工具(valgrind、perf等)對應用層面進行分析/最佳化

▶ 然後分析使用者空間+核心的效能

▶ 最後,如果只有在負載系統中才會出現效能問題時,需要trace或profile整個系統

  • 對於"常量"複雜問題,可以使用snapshot工具
  • 對於偶爾發生的問題,可以記錄trace並進行分析

▶ 如果在分析前需要複雜的配置,可以考慮使用自定義工具:指令碼、自定義trace、eBPF等。

Kernel Debugging

Preventing bugs

Static code analysis

▶ 可以使用sparse工具執行靜態分析

sparse使用annotation來探測編譯時存在的各種錯誤

  • 鎖問題(非均衡鎖)
  • 地址空間問題,如直接訪問使用者空間指標

▶ 使用make C=2分析需要重新編譯的檔案

▶ 或使用make C=1分析所有檔案

▶ 非均衡鎖例子

rzn1_a5psw.c:81:13: warning: context imbalance in 'a5psw_reg_rmw' - wrong count
at exit

Good practices in kernel development

▶ 當編寫驅動程式碼時,不能期望使用者能夠提供正確的值,因此總是需要對這些值進行校驗

▶ 如果想要展示一個特定場景下的呼叫棧時,可以使用WARN_ON()

  • 還可以在除錯過程中使用dump_stack()展示當前呼叫棧:
static bool check_flags(u32 flags)
{
  if (WARN_ON(flags & STATE_INVALID))
  	return -EINVAL;
  return 0;
}

▶ 如果需要在編譯期間檢查變數(配置輸入,sizeof()結構體欄位),則可以使用BUILD_BUG_ON()保證滿足條件

BUILD_BUG_ON(sizeof(ctx->__reserved) != sizeof(reserved));

▶ 如果在編譯期間得到關於未使用的變數/引數告警,則需要修復這些問題

▶ 使用checkpatch.pl --strict 可以幫助檢視程式碼的潛在問題

Linux Kernel Debugging

▶ 有多種Linux核心特性工具來幫助簡化核心除錯

  • 特定的日誌框架
  • 使用標準方式dump低階崩潰資訊
  • 多種執行時檢查器來幫助檢查各種問題:記憶體問題、鎖問題、未定義的行為等
  • 互動式或事後除錯

▶ 需要在核心menuconfig中明確啟用這些特性,它們被分配到 Kernel hacking -> Kernel debugging 配置表項中。

  • 需要將CONFIG_DEBUG_KERNEL設定為"y"來啟動其他除錯選項

Debugging using messages

有3種可用的APIs:

▶ 對於新的除錯訊息,不推薦使用老的printk()

pr_*()族函式:pr_emerg(), pr_alert(), pr_crit(), pr_err(), pr_warn(), pr_notice(), pr_info(), pr_cont(),以及特殊的pr_debug()(見後文)

  • 定義在include/linux/printk.h
  • 使用經典格式的字串作為引數,如pr_info("Booting CPU %d\n", cpu);
  • 下面是輸出的核心日誌:[ 202.350064] Booting CPU 1

print_hex_dump_debug(): 使用類似hexdump的格式dump緩衝內容

dev_*()族函式:dev_emerg(), dev_alert(), dev_crit(), dev_err(), dev_warn(), dev_notice(), dev_info() 以及特殊的 dev_dbg() (見下文):

  • 它們使用一個指向 struct device的指標作為第一個引數,後跟一個格式化字串引數

  • 定義在include/linux/dev_printk.h

  • 可以用在與Linux裝置模組整合的驅動中

  • 使用方式:dev_info(&pdev->dev, "in probe\n");

  • 核心輸出:

    [ 25.878382] serial 48024000.serial: in probe
    [ 25.884873] serial 481a8000.serial: in probe
    

*_ratelimited() 版本的方法可以基於/proc/sys/kernel/printk_ratelimit{_burst}值來限制高頻呼叫下的大量輸出

▶ 相比標準的printf(),核心定義了更多的格式說明符:

  • %p:預設展示指標的雜湊值
  • %px:總是真實指標地址(用於不敏感的地址)
  • %pK:展示雜湊指標值,根據kptr_restrict sysctl值可以是0或指標地址
  • %pOF:裝置樹節點格式說明符
  • %pr:資源結構格式說明符
  • %pa:展示實體地址(所有32/64 bits均支援)
  • %pe:錯誤指標(展示對應的錯誤值對應的字串)

▶ 為使用%pK,應該將/proc/sys/kernel/kptr_restrict設定為1

▶ 更多支援的格式說明符,參見core-api/printk-formats

pr_debug() and dev_dbg()

▶ 當使用定義的DEBUG編譯驅動時,所有這些訊息都將以debug級別進行編譯和列印。可以透過在驅動的開頭使用#define DEBUG或在Makefile中使用ccflags-$(CONFIG_DRIVER) += -DDEBUG來啟用DEBUG

▶ 當使用CONFIG_DYNAMIC_DEBUG編譯核心時,這些訊息將自動轉換為以單檔案、單模組或單訊息方式輸出(透過/proc/dynamic_debug/control設定)。預設不啟用訊息功能

  • 細節參見admin-guide/dynamic-debug-howto
  • 可以獲取僅感興趣的除錯訊息

▶ 使用DEBUGCONFIG_DYNAMIC_DEBUG時,並不會編譯這些訊息

pr_debug() and dev_dbg() usage

▶ 可以透過 /proc/dynamic_debug/control 檔案啟用除錯列印

  • cat /proc/dynamic_debug/control將顯示核心啟用的所有訊息行
  • 如:init/main.c:1427 [main]run_init_process =p " \%s\012"

▶ 透過下面語法可以啟用單獨的行、檔案或模組:

  • echo "file drivers/pinctrl/core.c +p" > /proc/dynamic_debug/control 會啟用 drivers/pinctrl/core.c 中的所有除錯資訊
  • echo "module pciehp +p" > /proc/dynamic_debug/control 會啟用pciehp 模組中的除錯列印
  • echo "file init/main.c line 1427 +p" > /proc/dynamic_debug/control 回啟用init/main.c 檔案第1247行的除錯列印
  • +p 換為 -p 即可禁用除錯列印

Debug logs troubleshooting

▶ 當使用動態除錯時,確保啟用debug呼叫:需要在debugfscontrol檔案中看到且必須啟用(=p)

▶ 日誌輸出是否僅位於核心日誌緩衝?

  • 可以透過dmesg檢視
  • 可以降低loglevel來直接輸出到終端
  • 可以在核心命令列中設定ignore_loglevel來強制所有核心日誌輸出到終端

▶ 如果正在處理外接模組,可能需要在模組原始碼或Makefile中定義DEBUG,而非使用動態除錯

▶ 如果透過核心命令列進行配置,這些配置會被正確解析嗎?

  • 從5.14開始,核心可以通知故障的命令列

    Unknown kernel command line parameters foo, will be passed to user space.
    
  • 需要小心使用特殊的字串轉義(如引號)

▶ 注意,一部分子系統使用了自身的日誌基礎設定以及特定的配置/控制,如drm.debug=0x1ff

Kernel early debug

▶ 在booting階段,核心可能會在展示系統訊息之前崩潰

▶ 在ARM上,如果核心無法boot或暫停而沒有訊息任何訊息,可以啟用early除錯選項

  • CONFIG_DEBUG_LL=y 啟用ARM early視窗輸出功能
  • CONFIG_EARLYPRINTK=y 將允許printk輸出列印資訊

▶ 需要使用earlyprintk命令列引數來啟用early printk輸出功能

Kernel crashes and oops

Kernel crashes

▶ 核心並不能免疫崩潰,很多錯誤可能會導致崩潰

  • 記憶體訪問錯誤(空指標、越界訪問等)
  • 錯誤檢測使用了panic
  • 不正確的核心執行模式(如在原子上下文使用了sleeping)
  • 核心探測到死鎖

▶ 在發生錯誤時,核心會在終端暫時一條訊息"Kernel oops"

Kernel oops

▶ 訊息內容取決於使用的架構

▶ 大部分架構會至少展示如下資訊:

  • oops發生時的CPU狀態
  • 暫存器內容
  • 導致崩潰的回溯函式呼叫
  • 棧內容(最後X位元組)

▶ 取決於架構,可以使用PC暫存器(有時稱為IP、EIP等)記憶體分辨崩潰位置

▶ 使用CONFIG_KALLSYMS=y可以將符號名稱嵌入核心映象,進而可以在回溯棧中獲得有意義的符號名稱

▶ 回溯棧中展示的符號格式為:

  • <symbol_name>+<hex_offset>/<symbol_size>

▶ 如果oops不是重要的(發生在程序上下文中),則核心會殺死程序並繼續執行

  • 必須為核心穩定性妥協

▶ hung太長時間的任務也可能產生oops(CONFIG_DETECT_HUNG_TASK)

▶ 如果支援KGDB,則在發生oops時,核心會切換到KGDB模式

Oops example

image image

Kernel oops debugging: addr2line

▶ 可以使用addr2line將展示的地址/符號轉換到原始碼行:

  • addr2line -e vmlinux <address>

▶ GNU binutils >= 2.39 會處理符號+偏移量符號

  • addr2line -e vmlinux + <symbol_name>+<off>

▶ 可以透過核心原始碼的faddr2line指令碼處理老版本的symbol+offset符號

  • scripts/faddr2line vmlinux + <symbol_name>+<off>

▶ 必須透過CONFIG_DEBUG_INFO=y編譯核心來將除錯資訊嵌入vmlinux檔案

Kernel oops debugging: decode_stacktrace.sh

▶ 可以透過核心原始碼提供的decode_stacktrace.sh實現addr2line的oops自動解碼

▶ 該指令碼可以將所有符號名稱/地址轉換到對應的檔案/行,並展示觸發崩潰的彙編程式碼

./scripts/decode_stacktrace.sh vmlinux linux_source_path/ < oops_ report.txt > decoded_oops.txt

▶ 注意:應該設定CROSS_COMPILEARCH環境變數來獲得正確的彙編dump

Oops behavior configuration

▶ 有時,崩潰可能比較嚴重,導致核心panic,並完全停止執行,處於繁忙迴圈中

▶ 可以透過 CONFIG_PANIC_TIMEOUT啟用在panic時自動重啟

  • 0:用不重啟
  • 負值:立即重啟
  • 正值:重啟前等待的秒數

▶ 可以將OOPS配置為總是panic

  • 在boot期間,將oops=panic新增到命令列
  • 在構建期間,設定CONFIG_PANIC_ON_OOPS=y

The Magic SysRq

串列埠驅動提供

▶ 在核心出現嚴重問題的情況下可以執行多個除錯/恢復命令

  • 嵌入式中:在終端傳送中斷符號(按[Ctrl]+a再按[Ctrl]+\),然後按<character>
  • /proc/sysrq-trigger中會會回應<character>

▶ 例子:

  • h:展示可用的命令
  • s:同步所有掛載的檔案系統
  • b:重啟系統
  • w:展示所有sleeping程序的核心棧
  • t:展示所有執行程序的核心棧
  • g:進入kgdb模式
  • z:重新整理trace緩衝
  • c:觸發一個崩潰(核心panic)
  • 還可以註冊自己的命令

▶ 詳情參見 admin-guide/sysrq

Built-in Kernel self tests

Kernel memory issue debugging

▶ 在使用者空間編寫核心程式碼時可能會發生記憶體問題

  • 越界訪問
  • 使用釋放的記憶體(在kfree()之後解引用一個指標)
  • 由於沒有執行kfree()導致記憶體不足

▶ 有多種工具可以捕獲這些問題

  • KASAN可以查詢使用釋放的記憶體和越界訪問問題
  • KFENCE可以在生產系統中查詢使用釋放的記憶體和越界訪問問題
  • Kmemleak可以查詢由於忘記釋放記憶體導致的記憶體洩露

KASAN

▶ 可以查詢使用釋放的記憶體和越界訪問問題

▶ 在編譯期間使用GCC檢測核心

▶ 幾乎支援所有架構(ARM, ARM64, PowerPC, RISC-V, S390, Xtensa and X86)

▶ 透過核心配置CONFIG_KASAN啟用KASAN

▶ 可以透過修改Makefile為特定檔案啟用KASAN

  • KASAN_SANITIZE_file.o := y 為特定檔案啟用KASAN
  • KASAN_SANITIZE := y 為Makefile資料夾中的所有檔案啟用KASAN

Kmemleak

▶ Kmemleakl可以查詢使用kmalloc()動態申請的物件中存在的記憶體洩漏

  • 透過掃描記憶體來檢測記憶體地址是否被引用

▶ 一旦啟用了CONFIG_DEBUG_KMEMLEAK,就可以在debugfs中檢視kmemleak控制的檔案

▶ 每10分鐘掃描一次記憶體洩露

  • 可以透過CONFIG_DEBUG_KMEMLEAK_AUTO_SCAN禁用

▶ 可以透過如下方式立即觸發一次掃描

  • # echo scan > /sys/kernel/debug/kmemleak

▶ 結果展示在debugfs中

  • # cat /sys/kernel/debug/kmemleak

▶ 更多資訊參見 dev-tools/kmemleak

Kmemleak report

# cat /sys/kernel/debug/kmemleak
unreferenced object 0x82d43100 (size 64):
  comm "insmod", pid 140, jiffies 4294943424 (age 270.420s)
  hex dump (first 32 bytes):
    b4 bb e1 8f c8 a4 e1 8f 8c ce e1 8f 88 c6 e1 8f ................
    10 a5 e1 8f 18 e2 e1 8f ac c6 e1 8f 0c c1 e1 8f ................
  backtrace:
    [<c31f5b59>] slab_post_alloc_hook+0xa8/0x1b8
    [<c8200adb>] kmem_cache_alloc_trace+0xb8/0x104
    [<1836406b>] 0x7f005038
    [<89fff56d>] do_one_initcall+0x80/0x1a8
    [<31d908e3>] do_init_module+0x50/0x210
    [<2658dd55>] load_module+0x208c/0x211c
    [<e1d48f15>] sys_finit_module+0xe4/0xf4
    [<1de12529>] ret_fast_syscall+0x0/0x54
    [<7ee81f34>] 0x7eca8c80

UBSAN

▶ UBSAN是一個執行時檢測器,檢測未定義的程式碼行為

  • 使用大於型別的值進行移位
  • 整數溢位
  • 未對齊的指標訪問
  • 越界訪問靜態陣列
  • https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html

▶ 使用編譯期間檢測來插入在執行時執行的檢查

▶ 必須啟用CONFIG_UBSAN=y

▶ 可以透過修改Makefile為特定檔案啟用UBSAN

  • UBSAN_SANITIZE_file.o := y 為特定檔案啟用UBSAN
  • UBSAN_SANITIZE := y 為Makefile資料夾的所有檔案啟用UBSAN

UBSAN: example of UBSAN report

▶ 下面報告了一個未定義的行為:使用>32的值進行移位

UBSAN: Undefined behaviour in mm/page_alloc.c:3117:19
shift exponent 51 is too large for 32-bit type 'int'
CPU: 0 PID: 6520 Comm: syz-executor1 Not tainted 4.19.0-rc2 #1
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS Bochs 01/01/2011
Call Trace:
__dump_stack lib/dump_stack.c:77 [inline]
dump_stack+0xd2/0x148 lib/dump_stack.c:113
ubsan_epilogue+0x12/0x94 lib/ubsan.c:159
__ubsan_handle_shift_out_of_bounds+0x2b6/0x30b lib/ubsan.c:425
...
RIP: 0033:0x4497b9
Code: e8 8c 9f 02 00 48 83 c4 18 c3 0f 1f 80 00 00 00 00 48 89 f8 48
89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d
01 f0 ff ff 0f 83 9b 6b fc ff c3 66 2e 0f 1f 84 00 00 00 00
RSP: 002b:00007fb5ef0e2c68 EFLAGS: 00000246 ORIG_RAX: 0000000000000010
RAX: ffffffffffffffda RBX: 00007fb5ef0e36cc RCX: 00000000004497b9
RDX: 0000000020000040 RSI: 0000000000000258 RDI: 0000000000000014
RBP: 000000000071bea0 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 00000000ffffffff
R13: 0000000000005490 R14: 00000000006ed530 R15: 00007fb5ef0e3700

Debugging locking

▶ 鎖除錯:驗證鎖的正確性

  • CONFIG_PROVE_LOCKING
  • 檢測核心鎖程式碼
  • 探測在系統生命中是否違反了鎖原則,如:
    • 要求不同的鎖順序(持續跟蹤並比較鎖順序)
    • 中斷處理器以及啟用中斷的程序上下文會獲得Spinlocks
  • 不適合生產系統
  • 細節參見locking/lockdep-design

CONFIG_DEBUG_ATOMIC_SLEEP允許檢測原子程式碼段中錯誤休眠的程式碼(通常在保持鎖的情況下)。

  • 可以透過dmesg顯示檢測出的問題

Concurrency issues

▶ 核心併發SANitizer框架

▶ Linux5.8引入CONFIG_KCSAN

▶ 基於編譯時檢測的動態競爭檢測器

▶ 可以發現系統的併發問題(主要是資料競爭)

▶ 更多參見dev-tools/kcsanhttps://lwn.net/Articles/816850/

KGDB

kgdb - A kernel debugger

CONFIG_KGDB

▶ 核心的執行完全由另一臺使用串列埠線連線的機器上的gdb控制

▶ 幾乎可以做任何事情,包括在中斷處理器上插入斷點

▶ 支援最流行的CPU架構

CONFIG_GDB_SCRIPTS 可以構建核心提供的GDB python指令碼

  • 更多參見 dev-tools/gdb-kernel-debugging

kgdb kernel config

CONFIG_DEBUG_KERNEL=y 支援KDGB

CONFIG_KGDB=y 啟用KGDB

CONFIG_DEBUG_INFO=y 使用除錯資訊編譯核心 (-g)

CONFIG_FRAME_POINTER=y 可以具有更多可靠的棧

CONFIG_KGDB_SERIAL_CONSOLE=y 啟用串列埠KGDB

CONFIG_GDB_SCRIPTS=y 啟用核心 GDB python 指令碼

CONFIG_RANDOMIZE_BASE=n 禁用 KASLR

CONFIG_WATCHDOG=n禁用 watchdog

CONFIG_MAGIC_SYSRQ=y 啟用 Magic SysReq 支援

CONFIG_STRICT_KERNEL_RWX=n 禁用核心段的記憶體保護,可以允許新增斷點

kgdb pitfalls

▶ 需要禁用KASLR,防止gdb操作隨機核心地址

  • 如果啟用kaslr,則可以使用nokaslr命令禁用kaslr模式

▶ 禁用平臺watchdog,防止在除錯時重啟

  • 當KGDB中斷時,會禁用所有中斷,watchdog不提供服務
  • 有時,高階別boot會啟用watchdog。確保在此處禁用watchdog

▶ 無法使用interrupt命令或Ctrl+C中斷核心執行

▶ 不支援在任意位置插入斷點(參見CONFIG_KGDB_HONOUR_BLOCKLIST)

▶ 需要支援polling的終端驅動

▶ 某些機構缺少相應的功能(如在arm32上沒有watchpoint),因此可能會不穩定

Using kgdb

▶ 細節參見核心文件:dev-tools/kgdb

▶ 必須包含一個kgbd I/O驅動,如透過串列埠終端使用kgdb(透過 CONFIG_KGDB_SERIAL_CONSOLE啟用kgdboc: kgdb over console)

▶ 透過傳入如下引數在boot期間配置kgdboc

  • kgdboc=<tty-device>,<bauds>,如kgdboc=ttyS0,115200

▶ 在執行時使用sysfs

  • echo ttyS0 > /sys/module/kgdboc/parameters/kgdboc
  • 如果終端不支援polling,則命令列會提示一個錯誤

▶ 然後將kgdbwait傳入核心:它會讓kgdb等到偵錯程式連線

▶ boot核心,在終端初始化之後,使用中止符號+g在串列埠終端上中斷核心(參見Magic SysRq)

▶ 在工作臺上,啟動gdb

  • arm-linux-gdb ./vmlinux
  • (gdb) set remotebaud 115200
  • (gdb) target remote /dev/ttyS0

▶ 一旦連線,就可以像除錯應用程式一樣除錯核心

▶ 在GDB側,第一個執行緒代表CPU上下文(ShadowCPU),其他執行緒則代表一個任務

Kernel GDB scripts

CONFIG_GDB_SCRIPTS可以透過構建python指令碼來簡化核心除錯(新增新命令和函式)

▶ 當使用gdb vmlinux時,會自動載入構建根目錄中的vmlinux-gdb.py檔案

  • lx-symbols: 為vmlinux和模組過載符號
  • lx-dmesg: 顯示核心 dmesg
  • lx-lsmod:顯示載入的模組
  • lx-device-{bus|class|tree}: 顯示裝置匯流排、類和樹
  • lx-ps: ps 類似檢視任務
  • $lx_current() 包含當前task_struct
  • $lx_per_cpu(var, cpu) 返回一個單-cpu變數
  • apropos lx 顯示所有可用的函式

dev-tools/gdb-kernel-debugging

KDB

CONFIG_KGDB_KDB包含一個kgdb的前端名稱"KDB"

▶ 該前端在串列埠終端上暴露了一個除錯提示,可以在不需要外部gdb的情況下除錯核心

▶ 可以使用與進入kgdb模式相同的機制進入KDB

▶ 可以同時使用KDB和KGDB

  • 使用在KDB中使用kgdb進入kgdb模式
  • 透過gdb傳送一條maintenance packet 3 維護命令,可以從kgdb切換到KDB模式

kdmx

▶ 當系統只有一個串列埠時,由於一個應用只能訪問一個埠,因此無法同時使用KGDB和串列埠線輸出終端

▶ 幸運的是,kdmx工具可以透過將GDB訊息和標準終端從一個埠切分為2個字pty(/dev/pts/x)來支援同時使用KGDB和串列埠輸出

https://git.kernel.org/pub/scm/utils/kernel/kgdb/agent-proxy.git

  • kdmx子目錄
image

Going further with KGDB

▶ 更多例子和解釋參見如下連結:

  • Video: https://www.youtube.com/watch?v=HBOwoSyRmys
  • Slides: https://elinux.org/images/1/1b/ELC19_Serial_kdb_kgdb.pdf

crash

crash是一個可以與核心(dead 或 alive)互動的CLI工具

  • 使用/dev/mem/proc/kcore
  • 要求CONFIG_STRICT_DEVMEM=n

▶ 可以使用kdump、kvmdump等生成coredump檔案

▶ 基於gdb並提供很多特定的命令來檢查核心狀態

  • 棧、dmesg、程序的記憶體對映、irqs、虛擬記憶體域等

▶ 可以檢查系統上執行的所有任務

https://github.com/crash-utility/crash

crash example

$ crash vmlinux vmcore
[...]
	TASKS: 75
NODENAME: buildroot
  RELEASE: 5.13.0
  VERSION: #1 SMP PREEMPT Tue Nov 15 14:42:25 CET 2022
  MACHINE: armv7l (unknown Mhz)
  MEMORY: 512 MB
    PANIC: "Unable to handle kernel NULL pointer dereference at virtual address 00000070"
    	PID: 127
  COMMAND: "watchdog"
    TASK: c3f163c0 [THREAD_INFO: c3f00000]
    	CPU: 1
    STATE: TASK_RUNNING (PANIC)
    
crash> mach
   MACHINE TYPE: armv7l
  	MEMORY SIZE: 512 MB
  		     CPUS: 1
PROCESSOR SPEED: (unknown)
             HZ: 100
      PAGE SIZE: 4096
KERNEL VIRTUAL BASE: c0000000
KERNEL MODULES BASE: bf000000
KERNEL VMALLOC BASE: e0000000
KERNEL STACK SIZE: 8192

post-mortem analysis

Kernel crash post-mortem analysis

▶ 有時,無法訪問崩潰的系統或在系統無法在等待除錯時保持offline狀態

▶ 核心可以在遠端生成崩潰日誌(vmcore檔案),這樣就可以快速重啟系統,並支援gdb事後分析

▶ 該特性依賴kexeckdump,在發生崩潰並dump出vmcore檔案後boot另一個核心。

  • 可以透過SSH、FTP等方式將vmcore檔案儲存到本地儲存

kexec & kdump

▶ 在panic時,核心kexec支援直接從崩潰的核心上執行一個"dump-capture kernel"操作

  • 大部分時候,會為任務編譯特定的dump-capture kerne(initramfs/initrd指定了最小配置)

▶ kexec系統在啟動時為kdump 核心執行預留了一部分RAM

  • 可以透過crashkernel引數指定崩潰核心的特定實體記憶體域

▶ 然後使用kexec-tools將dump-capture kernel載入到該記憶體域

  • 內部會使用kexec_load系統呼叫man 2 kexec_load

▶ 最後,在panic時,核心會重啟進入dump-capture Kernel,允許使用者dump核心coredump(/proc/vmcore)到任意媒介中

▶ 不同的架構可能還需要選擇性新增命令列

▶ 參見admin-guide/kdump/kdump來全面瞭解如何使用kexec配置kdump核心

▶ 此外還有使用者空間服務和工具可以自動採集並將vmcore dumo到遠端

  • kdump systemd service和makedumpfile工具還可以將vmcore壓縮成一個較小的檔案(只適用於x86, PPC, IA64, S390)
  • https://github.com/makedumpfile/makedumpfile

kdump

image

kexec config and setup

▶ On the standard kernel: •

  • CONFIG_KEXEC=y 啟用KEXEC支援

  • kexec-tools 透過kexec命令

  • kexec可以訪問的一個核心和DTB

▶ dump-capture kernel:

  • CONFIG_CRASH_DUMP=y 啟用dump崩潰的核心
  • CONFIG_PROC_VMCORE=y 啟用 /proc/vmcore 支援
  • CONFIG_AUTO_ZRELADDR=y ARM32平臺

▶ 設定正確的crashkernel命令列選項:

  • crashkernel=size[KMG][@offset[KMG]]

▶ 使用kexec將dump-capture kernel載入為第一個核心

  • kexec --type zImage -p my_zImage --dtb=my_dtb.dtb -- initrd=my_initrd --append="command line option"

Going further with kexec & kdump

▶ 關於kexec/kdump的更多資訊參見如下內容:

  • Video: https://www.youtube.com/watch?v=aUGNDJPpUUg
  • Slides: https://static.sched.com/hosted_files/ossna2022/c0/Postmortem_ Kexec%2C Kdump and Ftrace.pdf

相關文章