Day 4:小試身手 -- 是誰住在 Rpi 的 LED 裡?

在正式地介紹 ply 的語法之前,先看看一個例子來展示一下如何使用他來追蹤程式碼。以下要追蹤的是關於 Raspberry Pi 3 Model B 板子上的那個綠色 LED,看看背後是核心中的哪些功能在控制他的明滅。

Step 1:猜測可能有關的子系統

講既然是個 LED,可能會想猜背後是某種 GPIO。接下來的目標就是是試試看追蹤核心中跟 GPIO 有關的函式,然後看看 LED 在開啟跟關閉時,追縱到的東西有沒有不同?就可以推測是否追蹤到正確的東西。

Step 2:找到有關那個子系統的函數

目前我們知道準備要追蹤的是跟 GPIO 有關的函式,不過這樣還是有點模糊,因為現在根本不知道這些函數長什麼樣子?或是整個子系統的大致架構如何?這時就可以去核心的文件找一些提示。比如說 General Purpose Input/Output (GPIO) 這個章節中,就介紹了 GPIO 子系統。不過,這時可以注意到 Introduction 下有一段這樣的話:

...The remainder of this document applies to the new descriptor-based interface. gpio-legacy.txt contains the same information applied to the legacy integer-based interface.

也就是說,核心中有另外一套遺留的 GPIO 架構。再閱讀一下文件,可以在 GPIO Descriptor Consumer Interface 一章節中,可以知道新的 GPIO 的架構中,是以 gpio_desc 作為描述 GPIO 的資料結構,並且相關的函式的命名,都是 gpiod_* 的形式。

有了這個之後,就知道要去找哪些可能跟 GPIO 有關的函式了。而這剛好可以用 /proc 中提供資訊去看看。

Step 2-1:/proc

/proc 是一個虛擬的檔案系統,他會把 Linux 的一些核心資料結構揭露出來,讓使用者可以用讀寫檔案的方式讀取這些核心的資料結構的內容。比如說 ls 一下看看裡面有什麼檔案:

$ ls /proc

可以發現類似以下的輸出:

1     183  366  60   8            fb             partitions
10    184  373  600  80           filesystems    sched_debug
100   19   374  605  81           fs             schedstat
1010  2    378  606  82           interrupts     self
1035  20   380  61   83           iomem          slabinfo
1049  23   395  62   883          ioports        softirqs
1050  230  4    63   9            irq            stat
1052  239  409  64   942          kallsyms       swaps
1053  24   444  65   945          keys           sys
108   240  457  66   asound       key-users      sysrq-trigger
11    25   466  661  buddyinfo    kmsg           sysvipc
116   28   477  664  bus          kpagecgroup    thread-self
12    29   483  67   cgroups      kpagecount     timer_list
13    3    484  68   cmdline      kpageflags     tty
133   30   485  69   consoles     latency_stats  uptime
14    306  52   70   cpu          loadavg        version
145   32   53   71   cpuinfo      locks          vmallocinfo
147   33   54   73   crypto       meminfo        vmstat
15    34   55   75   devices      misc           zoneinfo
165   346  57   76   device-tree  modules
178   348  58   77   diskstats    mounts
18    35   59   78   driver       net
181   363  6    79   execdomains  pagetypeinfo

可以發現裡面有很的關於作業系統本身的資訊。像 device-tree(一種描述周邊硬體的語言)、cpuinfo (cpu 供應商提供的資訊),還有不同 pid 的行程對應的相關資訊等等。而這邊有興趣的是 kallsyms,裡面有核心的符號表,以及核心模組中提供出來的函數。

Step 2-2:/proc/kallsyms

/proc/kallsym 裡面是核心的符號表。可以從這裡面的內容,搭配核心的原始程式碼,藉由名字搭配核心的原始程式碼及文件,去推測哪個子系統可能用到哪些函數。可以先看看裡面有什麼:

$ cat /proc/kallsyms

會出線一大堆函數的名稱。為了方便,可以善用 grepless (或是把 tmux 設置成滑鼠可以捲動等等)。比如說:把 cat 的「導向」給 less,這樣就可以用上下鍵捲動:

$ cat /proc/kallsyms | less

又比如說如果想要找 GPIO 相關的函式,可以試著把 cat 的輸出塞給 grep 搜尋,接著再塞給less 以方便捲動閱讀:

$ cat /proc/kallsyms | grep gpio | less

預計出現類似以下的輸出:

00000000 T pinctrl_find_gpio_range_from_pin_nolock
00000000 T pinctrl_add_gpio_range
00000000 T pinctrl_add_gpio_ranges
00000000 T pinctrl_find_gpio_range_from_pin
00000000 T pinctrl_remove_gpio_range

[...]

00000000 T pinctrl_find_and_add_gpio_range
00000000 T pinmux_can_be_used_for_gpio
00000000 T pinmux_request_gpio
00000000 T pinmux_free_gpio
00000000 T pinmux_gpio_direction
00000000 t bcm2835_gpio_irq_config
00000000 t bcm2835_pmx_gpio_set_direction

[...]

00000000 t bcm2835_gpio_direction_output
00000000 t bcm2835_gpio_direction_input
00000000 t bcm2835_pmx_gpio_disable_free
00000000 T desc_to_gpio
00000000 T gpiod_to_chip
00000000 T gpiochip_line_is_valid
00000000 T gpiochip_get_data

[...]

00000000 T gpiochip_lock_as_irq
00000000 T gpiochip_irq_domain_activate
00000000 t gpiodevice_release
00000000 T gpiod_set_debounce
00000000 T gpiod_set_transitory
00000000 T gpiod_is_active_low
00000000 T gpiod_cansleep
00000000 T gpiod_set_consumer_name

[...]

就可以過濾出跟 GPIO 有關的可能函數了。

Step 2-3:插入 kprobe

比如說:有一大類函數是 pinctrl_ 開頭的,那就可以在他們上面插上 kprobe,這是一個核心當中的基礎設施,可以在執行到某一道指令前,觸發其他的程式片段,然後再繼續執行該指令。現在插上 kprobe,看看他們在閃燈時做了什麼:

$ sudo ply 'kprobe:pinctrl_* { @[stack] = count(); }'

根據 ply 的文件,可以用萬用字元 * 來一口氣把 kprobe 插在所有名字以 pinctrl_ 為開頭的函數中。而不難猜測上述的程式用意是:從給每一個名字前綴是 pinctrl_ 的函數,都插上 kprobe。而當 kprobe 被觸發時,要做的事情是統計不同的「呼叫堆疊」出現了幾次。

執行上述指令,會得到以下的輸出:

ply: active

在執行一段時間 (大概數秒) 之後,按下 Ctrl + c 終止 ply,會發現以下的輸出:

^Cply: deactivating

@:

這時就發先沒有輸出。接著試試開頭為 bcm2835_gpio_* 的函數,也會發現有類似的結果。再試到 gpiod_* 名字為開頭的相關函數時:

$ sudo ply 'kprobe:gpiod_* { @[stack] = count(); }'

就出現了有趣的東西:

ply: active
^Cply: deactivating

@:
{ 
    gpiod_set_value_nocheck
    gpio_led_set+120
    gpio_led_set_blocking+24
    set_brightness_delayed+140
    process_one_work+592
    worker_thread+96
    kthread+368
    ret_from_fork+20
 }: 10
{ 
    gpiod_set_raw_value_commit
    gpiod_set_value_cansleep+56
    gpio_led_set+120
    gpio_led_set_blocking+24
    set_brightness_delayed+140
    process_one_work+592
    worker_thread+96
    kthread+368
    ret_from_fork+20
 }: 10
{ 
    gpiod_set_value_cansleep
    gpio_led_set_blocking+24
    set_brightness_delayed+140
    process_one_work+592
    worker_thread+96
    kthread+368
    ret_from_fork+20
 }: 10

開始有呼叫堆疊的統計出現了!這其實也不奇怪,因為剛剛從核心 GPIO 子系統的文件可以知道:新的 GPIO 子系統是使用 gpio descriptorgpiod_* 系列函數作為控制,而這大類函數的命名大致上都是以 gpiod_ 為開頭。

這邊補充一個工具:如果是使用 SSH 操縱 Raspberry Pi 的話,可以考慮使用 tmux 這個工具。tmux 是個 terminal multiplexer,可以把同一個 session 的終端機畫面用分割的方式集中在一起,像下面這樣:

img

這樣就可以一邊查 kallsyms,一邊執行 ply,一邊做其他的事情,不用因為視窗被 ply 阻塞而沒辦法做其他事情。

Step 3:關 LED 並重新追蹤

為了更近一步證實這個呼叫是用來控制上面那顆綠色的 LED 燈的,可以透過讀寫 sysfs 的檔案控制 他。修改 sysfs 中的數值把他的亮度調成 0:

$ sudo su
# echo 0 >/sys/class/leds/led0/brightness
# exit

接著再執行一次 ply

$ sudo ply 'kprobe:gpiod_* { @[stack] = count(); }'

一段時間之後按下 Ctrl + c 終止 ply,就會發現這次沒有東西了:

ply: active
^Cply: deactivating

@:

然後也可以用肉眼觀察發現綠燈都不會亮了。因此可以更加確認這確實是用來控制綠燈的。順帶一提,如果要把綠燈的亮度設定回原來的樣子,可以用:

$ sudo su
# echo mmc0 >/sys/class/leds/led0/trigger
# exit

肉眼就可以發現綠燈又重新發亮了。