文章轉自我朋友的公眾號,以下為內容正文
====
大家好,我是你們的工具人老吳。
今天,和大家分享一下幾個 Linux 內核的調試小技巧。
當你遇到一個 bug,你調試了 1 年半載都解決不了,這其實一件好事。
java調試模式,因為它會時刻提醒你平時寫代碼時要謹慎、要多看書、多去認識一些更資深的人,別問我為什么會有這樣的感受,因為是親身經歷~
掌握一個調試工具是需要學習成本的,這里只是列舉我自己會用到的工具,如果有某個你覺得特別牛逼的工具而我沒提到的話,請原諒我。
好,下面開始正文。
調試 bug 時不要急著做實驗,先梳理一下思路。
一般可以總結成如下步驟:
1、理解問題;
UNIX/LINUX、2、重現問題;
3、定位問題,找到相關的代碼;
4、嘗試修復問題;
5、如果失敗,回到第 1 步;
bug 一般分為這幾類:
1、Crash,最常遇到的,可能是因為我是做設備驅動開發的緣故;
shell腳本調試、2、Lockup,比較少,這類問題預防比事后調試更重要;
3、Logic/implementation error,這個也比較容易遇到,一般是運行不報錯,但是運行的結果不符合預期;
4、Resource leak,偶爾會遇到;
5、Performance,偶爾會遇到,對于做驅動開發的話,一般是先考慮功能,當性能達不到要求時,再考慮優化性能。
調試工具的類別:
1、很多人不知道,調試最重要的工具是:我們的大腦。換句話說,也就是我們對內核個子系統、驅動開發的理解;
vim debug。2、Logs and dump analysis。內核很貼心,許多異常發生時都會有一堆的 Kernel Panic 的信息,經常能讓我們直接定位到引起異常的代碼;
3、Tracing/profiling。這類工具一般能讓我們理解程序的運行流程,不僅適合用來調試問題,也適合用來學習和理解內核的各種功能實現。
4、Interactive debugging。主要就是 gdb,我個人用得很少。
5、Debugging frameworks。許多的調試工具經過不斷地發展和完善后,就慢慢地形成了一整套的調試框架,例如 Ftrace、SystemTap。
下面是幾個我常用的調試技巧 / 工具。
關于打印的工具,主要是這 3 種:
LINUX教程、1、printk()
最原始的打印 api,可以用但是主流觀點已經不推薦使用了。
與之相關的是啟動參數 loglevel,它決定了可以被打印出來的信息的最低優先級。
2、pr_*()
推薦用 pr_*() 來代替 printk(),這是一個函數族:
pr_emerg(), pr_alert(), pr_crit(), pr_err(), pr_warning(), pr_notice(), pr_info(), pr_cont(), pr_debug()
怎么調試程序?例如:
pr_info("Booting?CPU?%d\n",?cpu);
內核會打印:
[?202.350064]?Booting?CPU?1
3、dev_*()
同樣是一個函數族:
dev_emerg(),?dev_alert(),?dev_crit(),?dev_err(),?dev_warn(),?dev_notice(),?dev_info(),?dev_dbg()
它們的最大特點是需要傳入一個 struct device 的參數,并且會打印出這個 device 的名字,一邊是在驅動相關的代碼里使用。
例如:
dev_info(&pdev->dev,?"in?probe\n");
軟件調試的任務,內核會打印:
[?25.878382]?serial?48024000.serial:?in?probe
關于 pr_debug() and dev_dbg()
要使用這兩個 api,需要在對應的代碼里 #deinfe DEBUG。
當內核使能了 CONFIG_DYNAMIC_DEBUG,我們就可以通過 /sys/kernel/debug/dynamic_debug/control 動態地是否要打印 log,以及打印哪些 log。
使用方法,大致如下:
$?mount?-t?debugfs?none?/sys/kernel/debug/
$?cd?/sys/kernel/debug/dynamic_debug/
$?echo?“file?xxx.c?+p”?>?control
$?echo?“file?svcsock.c?line?1603?+p”?>?control
$?echo?“file?drivers/usb/core/*?+p”?>?control
$?echo?“file?xxx.c?-p”?>?control
具體地,可以參考:
多線程調試、https://training.ti.com/sites/default/files/docs/Kernel-Debug-Series-Part4-dynamic-debug.pdf
舉個例子,下面是一次 Kernel Panic:
$?cat?/sys/class/gpio/gpio504/value
[23.688107]?Unable?to?handle?kernel?NULL?pointer?dereference?at?virtual?address?00000000
[23.696431]?pgd?=?(ptrval)
[23.699167]?[00000000]?*pgd=28bd4831,?*pte=00000000,?*ppte=00000000
[23.705596]?Internal?error:?Oops:?17?[#1]?SMP?ARM
[23.710316]?Modules?linked?in:
[23.713394]?CPU:?1?PID:?177?Comm:?cat?Not?tainted?4.19.17?#8
[23.719060]?Hardware?name:?Freescale?i.MX6?Quad/DualLite?(Device?Tree)
[23.725606]?PC?is?at?mcp23sxx_spi_read+0x34/0x84
[23.730241]?LR?is?at?_regmap_raw_read+0xfc/0x384
[23.734866]?pc?:?[<c0539c44>]
lr?:?[<c067d894>]
psr:?60040013
[23.741142]?sp?:?d8c6da48?ip?:?00000009?fp?:?d8c6da6c
[23.746375]?r10:?00000040?r9?:?d8a94000?r8?:?d8c6db30
[23.751608]?r7?:?c12ed9d4?r6?:?00000001?r5?:?c0539c10?r4?:?c1208988
[23.758145]?r3?:?d8789f41?r2?:?2afb07c1?r1?:?d8789f40?r0?:?00000000
[...]?//?省略
關鍵信息:
PC is at mcp23sxx_spi_read+0x34
pc : [<c0539c44>]
PC 是當前執行的指令的地址。
調試模式、接下來,我們可以借助 addr2line, 定位到具體是哪一行代碼引起了panic:
$?arm-linux-addr2line?-f?-e?vmlinux?0xc0539c44
mcp23sxx_spi_read
/home/sprado/elce/linux/drivers/pinctrl/pinctrl-mcp23s08.c:357
另外,還可以用 gdb 來定位代碼:
$?arm-linux-gdb?vmlinux
(gdb)?list?*(mcp23sxx_spi_read+0x34)
0xc0539c44?is?in?mcp23sxx_spi_read?(drivers/pinctrl/pinctrl-mcp23s08.c:357)
earlyprintk 一般用來處理一些發生在啟動初期時的異常。
最常見的現象就是系統打印完 Starting Kernel... 后就 hang 住了。
用法:
1、配置內核:
CONFIG_EARLY_PRINTK
CONFIG_DEBUG_LL
程序的調試、2、設置啟動參數,類似:
root=/dev/mmcblk0p2 rootwait rw earlyprintk console=ttyS0,115200
這個函數可以打印出當前的函數調用棧。
我一般會在高度可疑的地方使用它。
舉個例子:
static?int?sun6i_spi_probe(struct?platform_device?*pdev)
{struct?spi_master?*master;struct?sun6i_spi?*sspi;[...]//?用于調試
WARN_ON(1);master?=?spi_alloc_master(&pdev->dev,?sizeof(struct?sun6i_spi));[...]
當運行到 WARN_ON(1) 時,內核會打印:
[????1.847018]?WARNING:?CPU:?1?PID:?1?at?drivers/spi/spi-sun6i.c:549?sun6i_spi_probe+0x20/0x3ac
[????1.855454]?Modules?linked?in:
[????1.858525]?CPU:?1?PID:?1?Comm:?swapper/0?Not?tainted?4.14.111?#196
[????1.864781]?Hardware?name:?sun8i
[????1.868032]?[<c02287fc>]?(unwind_backtrace)?from?[<c0225398>]?(show_stack+0x10/0x14)
[????1.875776]?[<c0225398>]?(show_stack)?from?[<c0a1ba3c>]?(dump_stack+0x94/0xa8)
[????1.882997]?[<c0a1ba3c>]?(dump_stack)?from?[<c0240c24>]?(__warn+0xe8/0x100)
[????1.889953]?[<c0240c24>]?(__warn)?from?[<c0240cec>]?(warn_slowpath_null+0x20/0x28)
[????1.897517]?[<c0240cec>]?(warn_slowpath_null)?from?[<c06a03c0>]?(sun6i_spi_probe+0x20/0x3ac)
[????1.905953]?[<c06a03c0>]?(sun6i_spi_probe)?from?[<c0617980>]?(platform_drv_probe+0x4c/0xb0)
[????1.914299]?[<c0617980>]?(platform_drv_probe)?from?[<c06160dc>]?(driver_probe_device+0x234/0x2f0)
[????1.923162]?[<c06160dc>]?(driver_probe_device)?from?[<c0616244>]?(__driver_attach+0xac/0xb0)
[????1.931592]?[<c0616244>]?(__driver_attach)?from?[<c06144ec>]?(bus_for_each_dev+0x68/0x9c)
[????1.939762]?[<c06144ec>]?(bus_for_each_dev)?from?[<c0615654>]?(bus_add_driver+0x198/0x210)
[????1.948020]?[<c0615654>]?(bus_add_driver)?from?[<c0616aec>]?(driver_register+0x78/0xf8)
[????1.956017]?[<c0616aec>]?(driver_register)?from?[<c0201a70>]?(do_one_initcall+0x40/0x16c)
[????1.964193]?[<c0201a70>]?(do_one_initcall)?from?[<c1000e6c>]?(kernel_init_freeable+0x1c8/0x264)
[????1.972884]?[<c1000e6c>]?(kernel_init_freeable)?from?[<c0a2ef4c>]?(kernel_init+0x8/0x114)
[????1.981054]?[<c0a2ef4c>]?(kernel_init)?from?[<c0222058>]?(ret_from_fork+0x14/0x3c)
[????1.988686]?---[?end?trace?dc4e090f55ad2de8?]---
自我調試的方法有哪些,我們可以很清晰地看到 sun6i_spi_probe() 被調用的流程。
這個方法跑起來很簡單,但是每次使用都得編譯和更新內核,非常不方便,只適合輕度使用。
如果發生 Kernel panic 時,我們并沒有連接串口終端,那么這一次的崩潰信息就丟失了。
Pstore (persistent storage) 就可以用來處理這種情況。
當發生 Kernel painic 時,Pstore 會自動保存 oops 和 panic 的 log,并且在軟重啟后仍可以查看 log 信息。
默認情況下,log 是存儲在 RAM 的某個保留區域中,但也可以使用存儲設備,例如閃存。
ubuntu開機切換內核。
用法:
1、配置內核:
CONFIG_PSTORE
CONFIG_PSTORE_RAM
2、配置 dts,為 Pstore 預留一塊內存,類似:
reserved-memory {#address-cells = <1>;#size-cells = <1>;ranges;ramoops: ramoops@0b000000 {compatible = "ramoops";reg = <0x20000000 0x200000>; /* 2MB */record-size = <0x4000>; /* 16kB */console-size = <0x4000>; /* 16kB */};
};
3、假設剛發生了一次 Panic,并且已經軟重啟:
$?mount?-t?pstore?pstore?/sys/fs/pstore/$?ls?/sys/fs/pstore/
dmesg-ramoops-0
dmesg-ramoops-1
通過上面這兩個文件就可以看到內核的崩潰信息了。
內核文檔:
LINUX系統。Documentation/admin-guide/ramoops.rst
這是一個命令行工具,它可以在用戶空間去讀寫內存。
大多數情況,我是用它來讀寫寄存器,簡單粗暴。
用法:
$?apt-get?install?devmem2
1、查看寄存器 TMR_IRQ_EN_REG:
$?devmem2?0x0x01C20C00
/dev/mem?opened.
Memory?mapped?at?address?0xb6f38000.
Value?at?address?0x0?(0xb6f38000):?0xEA000016
2、修改 TMR_IRQ_EN_REG:
#?devmem2?0x0x01C20C00?w?0xEA000018
/dev/mem?opened.
Memory?mapped?at?address?0xb6fe8000.
Value?at?address?0x0?(0xb6fe8000):?0xEA000016
Written?0xEA000018;?readback?0xEA000018
軟件調試的方法、如果你想完全控制內核的運行,例如單步執行、查看變量等,可以用 GDB。
這里采用的是 C/S 架構,在板子上運行 server (kgdb),在 PC 機上運行 client (gdb),通訊的方式可以是串口,或者網絡,我一般是用串口。
1、配置內核:
CONFIG_KGDB
CONFIG_KGDB_SERIAL_CONSOLE
CONFIG_KGDB_KDB
2、設置啟動參數:kgdoc
console=ttyS0,115200?kgdboc=ttyS0,115200?earlyprintk?root=/dev/mmcblk0p2?oops=panic?panic=0
ttyS0 是板子的調試串口。
要使用 kgdb,必須為其設置一個 I/O driver,我一般使用 kgdb over serial console (簡稱 kgdboc)
代碼調試?oops=panic panic=0 很重要。
另外,也通過在啟動后設置 kgdboc:
echo?ttyS0?>?/sys/module/kgdboc/parameters/kgdboc
3、讓內核進入 debug 模式:
$?echo?g?>?/proc/sysrq-trigger
[?1958.025927]?sysrq:?SysRq?:?DEBUG
[?1958.029191]?KGDB:?Entering?KGDB
4、讓 PC 機連接板子
$?arm-linux-gdb?vmlinux(gdb)?set?serial?baud?115200
(gdb)?target?remote?/dev/ttyUSB0
Remote?debugging?using?/dev/ttyUSB0
0xc02c3540?in?kgdb_breakpoint?()
配置好之后,通過 gdb 調試內核跟通過 gdb 調試應用的操作是一樣的。
這里我舉一個小例子。
多線程怎么調試?首先,人為讓內核 Crash:
$?echo?WRITE_KERN?>?/sys/kernel/debug/provoke-crash/DIRECT
Entering?kdb?(current=0xffffffc0de55f040,?pid?1470)?on?processor?4?Oops:?(null)
due?to?oops?@?0xffffff80108bfa48
CPU:?4?PID:?1470?Comm:?bash?Not?tainted?5.3.0-rc2+?#13
pc?:?__memcpy+0x48/0x180
lr?:?lkdtm_WRITE_KERN+0x4c/0x90
...
下面開始調試。
1、查看調用棧:
(gdb)?bt
Call?trace:
dump_backtrace+0x0/0x138
show_stack+0x20/0x2c
kdb_show_stack+0x60/0x84
...
do_mem_abort+0x4c/0xb4
el1_da+0x20/0x94
__memcpy+0x48/0x180
lkdtm_do_action+0x24/0x44
direct_entry+0x130/0x178
2、查看棧幀的內容:
(gdb)?frame?1
#1?0xffffff801056584c?in?lkdtm_WRITE_KERN?()?at?.../drivers/misc/lkdtm/perms.c:116
116
memcpy(ptr,?(unsigned?char?*)do_nothing,?size);
基本可以確定是使用 memcpy() 時導致 Crash。
3、查看相關代碼:
(gdb)?list
112??size?=?(unsigned?long)do_overwritten?-?(unsigned?long)do_nothing;
[...]
116??memcpy(ptr,?(unsigned?char?*)do_nothing,?size);
需要核查一下 ptr、do_nothing、size,這 3 個參數是否合法。
4、打印變量值:
(gdb)?print?size
$3?=?18446744073709551584(gdb)?print?do_overwritten?-?do_nothing
$4?=?-32
最后發現 18446744073709551584 其實就是 (unsigned long) 的 -32。memcpy 的數據大小是 -32,導致了內核崩潰。
Ftrace 的作用是幫助開發人員了解 Linux 內核的運行時行為,以便進行故障調試或性能分析。
最早 Ftrace 是一個 function tracer,僅能夠記錄內核的函數調用流程。如今 ftrace 已經成為一個 framework,采用 plugin 的方式支持開發人員添加更多種類的 trace 功能。
用法:
$?mount?-t?tracefs?none?/sys/kernel/tracing
$?cd?/sys/kernel/tracing/
$?cat?available_tracers
hwlat?blk?mmiotrace?function_graph?wakeup_dl?wakeup_rt?wakeup?function?nop
跟蹤器 tracer 表示的是要跟蹤的目標。
假設我們抓一次 spi 傳輸的過程:
echo?0?>?tracing_on
echo?function_graph?>?current_tracer
echo?*spi*?>?set_ftrace_filter
echo?*dma*?>>?set_ftrace_filter
echo?*spin*?>>?set_ftrace_notrace
echo?1?>?tracing_on
./spidev_test
echo?0?>?tracing_on
cat?trace
得到的信息:
1)?+?41.292?us???|??spidev_open();1)???????????????|??spidev_ioctl()?{1)???????????????|????spi_setup()?{1)???0.417?us????|??????__spi_validate_bits_per_word.isra.0();1)???????????????|??????sunxi_spi_setup()?{1)???0.834?us????|????????sunxi_spi_check_cs();1)???0.875?us????|????????spi_set_cs();1)???0.625?us????|????????sunxi_spi_cs_control();1)?+?17.125?us???|??????}1)???0.833?us????|??????spi_set_cs();1)?+?30.458?us???|????}1)?!?699.875?us??|??}[...]
相關參考:
https://blog.csdn.net/Guet_Kite/article/details/101791125
這個工具我沒有用過,但是它似乎很強大,所以我覺得應該簡單介紹一下。
kdump 是一種基于 kexec 系統調用 的內核崩潰轉儲機制。
當系統崩潰時,kdump 使用 kexec 啟動進入到第二個內核 (dump-capture kernel),從而獲得 coredump 信息。
用法:
1、設置啟動參數:
crashkernel=64M
2、運行 kexec:
$?kexec?--type?zImage?-p?/boot/zImage?\--initrd=<initrd-for-dump-capture-kernel>?\--dtb=<dtb-for-dump-capture-kernel>?\--command-line="XXX"
運行完 kexec 后,dump-capture kernel 就被加載進內存了。
以后如果發生了 kernel panic,dump-capture kernel 會被加載并運行。
我們可以在 dump-capture kernel 下,獲得 coredump 文件:
$?cp?/proc/vmcore?<dump-file>
然后就可以在 PC 上使用 gdb/crash 來調試分析了:
$?arm-linux-gdb?path/to/vmlinux?-c?path/to//vmcore
$?crash?path/to/vmlinux?path/to/vmcore
內核文檔:
Documentation/kdump/kdump.txt
預防為主,調試為輔。
軟件開發沒有銀彈,同樣的,bug 調試也沒有銀彈。但是多熟悉一些調試工具,是有好處的。
當然還有很多調試工具、技巧是我不知道了,歡迎大家分享給我。
Anyway, what we know is a drop, what we don't know is an ocean.
祝周末愉快。
—— The End ——
感謝完成閱讀,我是喜歡打籃球的寫代碼的籃球球癡,這個是我的公眾號,感謝你關注并支持。我從大學開始接觸電子和嵌入式軟件知識,至今,已經畢業工作了9年,我喜歡嵌入式,也愿意從事這個行業。不管是從技術還是職場經驗,都積累了足夠多的經驗,目前在一個非常優秀的團隊中做開發工作。
很高興認識每一個對技術努力,對人用心的朋友。
關注公眾號,后臺回復「1024」獲取學習資料網盤鏈接。
歡迎點贊,關注,轉發,在看,您的每一次鼓勵,我都將銘記于心~
嵌入式Linux
微信掃描二維碼,關注我的公眾號
版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。
工作时间:8:00-18:00
客服电话
电子邮件
admin@qq.com
扫码二维码
获取最新动态