--- title: Steven Rostedt - Learning the Linux Kernel with tracing --- - Steven Rostedt 是 ftrace, trace-cmd, kernelshark的maintainer - https://www.linuxfoundation.org/blog/blog/linux-kernel-developer-steven-rostedt - ```c #include <stdio.h> int main() { printf("hello world! \n"); return 0; } ``` - `gcc -Wall hello-1.c -o hello.1.out ` - 用 `objdump -d hello.out > hello.objdump.txt` - 裡面會是 `<puts@GLIBC_2.2.5>` ```cpp #include <stdio.h> int main() { printf("hello world! %p\n", (void*)main); return 0; } ``` - 跑很多次main的address會不同![[Pasted image 20250608155103.png]] - <font color="#00b0f0">這是安全的feature</font> ,可以藉由 sudo echo 0 > /proc/sys/kernel/randomize_va_space - 這跟 virtual address 有關 > 避免亂access一些導致kernel壞掉 - ![[Pasted image 20250608155449.png]] | 位元範圍 | 名稱 | 在圖中代表 | 意義 | | ------- | ------ | ----- | --------------------------- | | [47–39] | PGD | 0x0ac | Page Global Directory index | | [38–30] | PUD | 0x117 | Page Upper Directory index | | [29–21] | PMD | 0x0f1 | Page Middle Directory index | | [20–12] | PTE | 0x194 | Page Table Entry index | | [11–0] | offset | 0x135 | 實體頁內的位移 offset | ![[Pasted image 20250608160437.png]] ![[Pasted image 20250608160819.png]] | 區域 | 範圍 (32-bit 範例) | 權限 | 說明 | | ---------- | ------------------------- | ------------- | ----------------------------------------------------- | | **User** | `0x00000000 – 0x7FFFFFFF` | 使用者態 (Ring 3) | 每一個行程各有一份的_私有_虛擬位址空間。執行中的程式、stack、heap、mmap 區域都配置在這裡。 | | **Kernel** | `0x80000000 – 0xFFFFFFFF` | 核心態 (Ring 0) | 同一份 _全域_ 映射,供作業系統核心與驅動程式使用。使用者態程式**不能**直接存取。 | - `0000000000001135 <main>:` 後面是一段 `main()` 的 x86-64 指令: ``` 1135: 55 push %rbp 1136: 48 89 e5 mov %rsp,%rbp ... 1155: e8 ?? ?? ?? ?? callq <printf@plt> ... 115c: 5d pop %rbp 115d: c3 retq ``` - 這段位址(`0x0000000000001135`)位在圖的 **使用者區塊** 內。 在 64-bit Linux,使用者空間其實可以用到 ~128 TiB (`0x00007fffffffffff`);這邊只是課程示意,仍以 32-bit 分界畫圖。 - 執行 `callq printf` 會觸發 _syscall_,穿越「使用者 ↔ 核心」邊界,由核心幫忙完成 I/O 後再返回。 - 延伸:64-bit 與高記憶體分佈 | 模式 | 使用者最高位址 | 核心起始位址 | 常見分佈策略 | | -------------------- | -------------------- | -------------------- | ---------------------------------------------- | | x86 32-bit (2 G/2 G) | `0x7FFFFFFF` | `0x80000000` | 嵌入式、早期桌面 | | x86 32-bit (3 G/1 G) | `0xBFFFFFFF` | `0xC0000000` | 伺服器、桌機預設 | | x86-64 (canonical) | `0x00007FFFFFFFFFFF` | `0xFFFF800000000000` | 使用者 128 TiB / 核心 128 TiB;中間 16 EiB 保留未映射(用來偵錯) | ![[Pasted image 20250608161046.png]] ![[Pasted image 20250608161144.png]] ![[Pasted image 20250608161236.png]] ![[Pasted image 20250608161453.png]] - ftrace: kernel 內的 tracer sudo mount -t tracefs nodev /sys/kernel/tracing/ cd /sys/kernel/tracing/ ls ``` /sys/kernel/tracing$ sudo cat trace # tracer: nop # # entries-in-buffer/entries-written: 0/0 #P:28 # # _-----=> irqs-off/BH-disabled # / _----=> need-resched # | / _---=> hardirq/softirq # || / _--=> preempt-depth # ||| / _-=> migrate-disable # |||| / delay # TASK-PID CPU# ||||| TIMESTAMP FUNCTION # | | | ||||| | | ``` - 看所有function echo function | sudo tee /sys/kernel/tracing/current_tracer sudo cat trace ``` # tracer: function # # entries-in-buffer/entries-written: 1374512/79865184 #P:28 # # _-----=> irqs-off/BH-disabled # / _----=> need-resched # | / _---=> hardirq/softirq # || / _--=> preempt-depth # ||| / _-=> migrate-disable # |||| / delay # TASK-PID CPU# ||||| TIMESTAMP FUNCTION # | | | ||||| | | <idle>-0 [005] d..1. 690991.928658: sched_idle_set_state <-cpuidle_enter_state <idle>-0 [005] ...1. 690991.928659: cpuidle_reflect <-cpuidle_idle_call <idle>-0 [005] ...1. 690991.928660: menu_reflect <-cpuidle_reflect <idle>-0 [005] ...1. 690991.928661: tick_nohz_idle_got_tick <-menu_reflect <idle>-0 [005] ...1. 690991.928661: arch_cpu_idle_exit <-do_idle <idle>-0 [005] d..1. 690991.928662: arch_cpu_idle_enter <-do_idle <idle>-0 [005] d..1. 690991.928662: tsc_verify_tsc_adjust <-arch_cpu_idle_enter <idle>-0 [005] d..1. 690991.928663: local_touch_nmi <-arch_cpu_idle_enter <idle>-0 [005] d..1. 690991.928663: rcu_nocb_flush_deferred_wakeup <-do_idle <idle>-0 [005] d..1. 690991.928664: cpuidle_idle_call <-do_idle <idle>-0 [005] d..1. 690991.928664: cpuidle_get_cpu_driver <-cpuidle_idle_call <idle>-0 [005] d..1. 690991.928665: cpuidle_not_available <-cpuidle_idle_call <idle>-0 [005] d..1. 690991.928665: cpuidle_select <-cpuidle_idle_call <idle>-0 [005] d..1. 690991.928665: menu_select <-cpuidle_select ``` ![[Pasted image 20250608162147.png]] ![[Pasted image 20250608162303.png]] 不用這而用 trace-cmd ``` # tracer: function # # entries-in-buffer/entries-written: 1307479/40499381 #P:28 # # _-----=> irqs-off/BH-disabled # / _----=> need-resched # | / _---=> hardirq/softirq # || / _--=> preempt-depth # ||| / _-=> migrate-disable # |||| / delay # TASK-PID CPU# ||||| TIMESTAMP FUNCTION # | | | ||||| | | <idle>-0 [007] d..1. 691258.567597: sched_idle_set_state <-cpuidle_enter_state <idle>-0 [007] ...1. 691258.567599: cpuidle_reflect <-cpuidle_idle_call <idle>-0 [007] ...1. 691258.567600: menu_reflect <-cpuidle_reflect <idle>-0 [007] ...1. 691258.567600: tick_nohz_idle_got_tick <-menu_reflect <idle>-0 [007] ...1. 691258.567601: arch_cpu_idle_exit <-do_idle <idle>-0 [007] d..1. 691258.567602: arch_cpu_idle_enter <-do_idle <idle>-0 [007] d..1. 691258.567602: tsc_verify_tsc_adjust <-arch_cpu_idle_enter <idle>-0 [007] d..1. 691258.567603: local_touch_nmi <-arch_cpu_idle_enter <idle>-0 [007] d..1. 691258.567603: rcu_nocb_flush_deferred_wakeup <-do_idle <idle>-0 [007] d..1. 691258.567604: cpuidle_idle_call <-do_idle ``` sudo trace-cmd start -p function sudo trace-cmd show - trace hello program sudo trace-cmd record -e syscalls -F ./hello.1.out hello world 地址可以在裡面找到: ``` ... hello.1.out-985823 [002] 691607.549073: sys_enter_write: fd: 0x00000001, buf: 0x5e2f6817a2a0, count: 0x0000001c hello.1.out-985823 [002] 691607.549090: sys_exit_write: 0x1c ... ``` - 只看第一個進入kernel的話: - `sudo trace-cmd record -p function_graph --max-graph-depth 1 -e syscalls -F ./hello.1.out ` ``` hello.1.out-987566 [010] 692184.131794: funcgraph_entry: | syscall_trace_enter() { hello.1.out-987566 [010] 692184.131795: sys_enter_write: fd: 0x00000001, buf: 0x62168699f2a0, count: 0x0000001c hello.1.out-987566 [010] 692184.131795: funcgraph_exit: 0.679 us | } hello.1.out-987566 [010] 692184.131795: funcgraph_entry: | x64_sys_call() { hello.1.out-987566 [010] 692184.131796: funcgraph_entry: + 23.969 us | __x64_sys_write(); hello.1.out-987566 [010] 692184.131820: funcgraph_exit: + 24.707 us | } hello.1.out-987566 [010] 692184.131820: funcgraph_entry: | syscall_exit_to_user_mode_prepare() { hello.1.out-987566 [010] 692184.131821: funcgraph_entry: | syscall_exit_work() { hello.1.out-987566 [010] 692184.131821: sys_exit_write: 0x1c hello.1.out-987566 [010] 692184.131821: funcgraph_exit: 0.607 us | } hello.1.out-987566 [010] 692184.131821: funcgraph_exit: 1.236 us | } ``` - 會有 `__x64_sys_write()` - `sudo trace-cmd record -p function_graph -g __x64_sys_write -e syscalls -F ./hello.1.out` - 因為kernel不信任userspace所以可以看到會有 `rw_verify_area` - 可以忽略它` sudo trace-cmd record -p function_graph -g __x64_sys_write -n rw_verify_area -o nofuncgraph -e syscalls -F ./hello.1.out` - 裡面會有 `n_tty_write` > `pty_write` > `do_output_char` > `tty_insert_flip_string_and_push_buffer`> - 如何trace他的scheduler - `sudo trace-cmd record -e sched_wakeup -e sched_switch -e sys_enter_write ./hello.1.out ` : - `sudo perf record -e sched:sched_switch -e syscalls:sys_enter_write ./hello_trace.out` - `sudo perf script | grep -E 'sched_switch|sys_enter_write'` - ![[Pasted image 20250608171506.png]] - ![[Pasted image 20250608171552.png]] ![[Pasted image 20250608171625.png]] ![[Pasted image 20250608171640.png]] ![[Pasted image 20250608172050.png]] ``` Virtual Address ↓ PGD (Page Global Directory) ↓ PUD (Page Upper Directory) ↓ PMD (Page Middle Directory) ↓ PTE (Page Table Entry) ↓ Physical Page Base Address + offset ↓ = Physical Address ``` - `gcc -Wall hello.c -o hello.out` - `objdump -d hello.out > hello.objdump.txt` - `.init`: 初始化區段 - 這段負責呼叫 `__gmon_start__` - `.plt` / `.plt.got` / `.plt.sec`:Procedure Linkage Table - - **目的**:延遲解析函式位址(lazy binding) ``` 0000000000001050 <printf@plt>: f3 0f 1e fa endbr64 ff 25 76 2f 00 00 jmp *0x2f76(%rip) # → 跳到 GOT ``` - `.text`:程式主邏輯區段 - `_start`:程式進入點(由 linker 設定) ``` 0000000000001060 <_start>: ... lea 0xca(%rip),%rdi # 1149 <main> call *0x2f53(%rip) # __libc_start_main ``` - 這是 C 程式的真正啟動點,不是 `main()`。會呼叫 `__libc_start_main`,再跳到 `main()`。 - `main` 函式: ``` 0000000000001149 <main>: lea -0xf(%rip),%rax # → 把 main 本身的地址放進 rsi lea 0xea2(%rip),%rax # → 字串地址放進 rdi call 1050 <printf@plt> # → 呼叫 printf ``` - 這裡在呼叫 `printf()`,傳入 `main()` 自己的位址與字串。可能你寫的是: `printf("main = %p\n", main);` - `.fini` - 這是結束函數 `_fini`,在程式退出時由 runtime 呼叫,用來做資源清理。 ``` ┌──────────────────────────────┐ │ main 函數開始 │ └────────────┬─────────────────┘ │ ▼ ┌──────────────────────────────┐ │ push %rbp │ │ mov %rsp, %rbp │ ← 建立 stack frame └────────────┬─────────────────┘ │ ▼ ┌──────────────────────────────┐ │ lea main 的地址 → %rax │ │ mov %rax → %rsi │ ← 第二個參數(%p) └────────────┬─────────────────┘ │ ▼ ┌──────────────────────────────┐ │ lea 格式字串地址 → %rax │ │ mov %rax → %rdi │ ← 第一個參數 "main = %p\n" └────────────┬─────────────────┘ │ ▼ ┌──────────────────────────────┐ │ mov $0 → %eax │ ← 清除浮點參數計數器 └────────────┬─────────────────┘ │ ▼ ┌──────────────────────────────┐ │ call printf@plt │ ← 呼叫 printf 函式 └────────────┬─────────────────┘ │ ▼ ┌──────────────────────────────┐ │ mov $0 → %eax │ ← return 0; └────────────┬─────────────────┘ │ ▼ ┌──────────────────────────────┐ │ pop %rbp │ │ ret │ ← 結束 main 函數 └──────────────────────────────┘ ``` - ELF 執行流程(_start → libc → main → printf) ``` ┌──────────────────────────────┐ │ 程式從 _start 開始 │ ← ELF header e_entry 指向這 └────────────┬─────────────────┘ │ ▼ ┌──────────────────────────────┐ │ 初始化暫存器與堆疊指標 │ │ mov %rsp → %rdx(argv) │ │ 設定 %rdi = main 函式指標 │ │ 設定 %rsi = 初始化函式 │ ← __libc_csu_init └────────────┬─────────────────┘ │ ▼ ┌──────────────────────────────┐ │ call __libc_start_main@plt │ │ 動態 linker 導向真正的函數 │ └────────────┬─────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ __libc_start_main(libc.so) │ │ → 設定環境變數、初始化堆疊保護、執行 .init │ │ → 呼叫使用者傳入的 main() 函式 │ └──────────────────────┬──────────────────────┘ │ ▼ ┌──────────────────────────────┐ │ 進入 main() │ │ printf("main = %p", main); │ │ return 0; │ └────────────┬─────────────────┘ │ ▼ ┌──────────────────────────────┐ │ call printf@plt │ ← PLT → GOT → libc.so 中 printf └────────────┬─────────────────┘ │ ▼ ┌──────────────────────────────┐ │ libc.so 的 printf() 執行 │ └────────────┬─────────────────┘ │ ▼ ┌──────────────────────────────┐ │ main() 返回 → libc 收尾動作 │ │ 執行 _fini 與 exit() 等清理 │ └──────────────────────────────┘ ``` | 名稱 | 說明 | | ------------------- | ---------------------------- | | `_start` | 由 linker 指定的 ELF 進入點(非 main) | | `__libc_start_main` | libc 提供的執行器,負責呼叫 main | | `main()` | 使用者寫的主函數 | | `printf@plt` | 由 PLT 進入 GOT,再跳到真實的 printf() | | `.init/.fini` | 初始化與清理用的段落 | | `_fini` | 程式結尾時自動執行的 destructor 區段 | ## 使用 gdb ``` // hello.c #include <stdio.h> int main() { printf("main = %p\n", main); return 0; } ``` - `gdb hello.gdb.out` - `(gdb) disassemble _start` ![[Pasted image 20250608133259.png]] - 設斷點 `(gdb) b _start` - `_start` 是什麼? - 它是 **ELF 執行檔的進入點**(entry point),由 linker 設定。 - 在程式執行時,第一個被 CPU 執行的程式碼就是 `_start` - `_start` 負責: - 設定 stack、argc/argv - 呼叫 `__libc_start_main()`,再由它去呼叫 `main()` - run: `(gdb) r` - 載入程式 - 執行到 `_start`(剛剛設的斷點) - 停下來讓你除錯(`layout asm`、`x/i $rip`、`stepi` 等都可以用了) ## 架構理解 ``` [C 原始碼] ↓ gcc -c hello.c [hello.o(物件檔,尚未連結)] ↓ gcc -o hello hello.o [ELF 可執行檔 hello] ↓ 被執行時,由 kernel 載入 ┌───────────────────────────────────────────────┐ │ User Space │ ├───────────────────────────────────────────────┤ │ │ │ ELF Entry Point: _start(在 crt1.o 裡) │ │ ↓ │ │ __libc_start_main()(glibc 初始化器) │ │ ↓ │ │ main()(你自己的程式邏輯) │ │ ↓ │ │ printf() → glibc 包裝的 libc 函式 │ │ ↓ │ │ write() → glibc wrapper │ │ ↓ │ │ syscall (write)(glibc 透過 syscall 指令) │ │ ↓ │ └───────────────────────────────────────────────┘ ↓ CPU 切換模式(syscall 指令) ┌───────────────────────────────────────────────┐ │ Kernel Space │ ├───────────────────────────────────────────────┤ │ │ │ write() → vfs → file system → fd 寫入 stdout │ │ │ └───────────────────────────────────────────────┘ ↑ 回傳執行結果給 glibc ``` | 名稱 | 說明 | 出現在哪 | | --------------------- | -------------------------------- | ------------------------------------- | | `_start` | ELF 執行的 entry point,不是 `main()` | `objdump -d` 可看到 | | `__libc_start_main()` | glibc 的初始化包裝器,會呼叫你的 `main()` | 在 `.plt` 呼叫 | | `main()` | 你寫的主程式 | 出現在 `.text` 段 | | `printf()` | glibc 提供的函數,實際會轉為 `write()` | 經過 `.plt` 間接呼叫 | | `syscall` | glibc 內部透過 `syscall` 指令觸發 kernel | 在 libc 的實作內部 | | `write()` | kernel 中處理 fd 輸出的函數 | 無法在 user space 看到,需進 kernel source 才有 | ## 繼續追蹤實際系統呼叫 `gcc -g -O0 -o hello.0.out hello.c` `strace ./hello.0.out` `strace ./hello.0.out 2> hello.strace.txt` 其中 ``` write(1, "hello world! 0x64afef659149\n", 28) = 28 exit_group(0) = ? ``` - `write(1, ...)` 表示 `glibc` 最後真的轉成 `write()` 的系統呼叫 - `1` 是 `stdout`(0 是 stdin, 2 是 stderr) - `= 28` 表示寫出 28 個 bytes 成功(包括換行 `\n`) - glibc source code: https://elixir.bootlin.com/glibc/glibc-2.29/source/sysdeps/unix/sysv/linux/write.c