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