|
|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
本帖最后由 pulbieup 于 2021-3-9 09:50 编辑 . ?1 ?2 u- _, [% E$ Z2 s2 i. B/ }
& W. p( n8 [$ u8 p" p内核调试的难点在于它不能像用户态程序调试那样打断点,随时暂停查看各个变量的状态。
* C4 L/ e/ l$ |
$ r- _- e+ |! {( H9 {$ a, M5 D也不能像用户态程序那样崩溃后迅速的重启,恢复初始状态。
$ a4 Z# b7 g Z. c; p5 a/ `9 R0 |. x8 N7 x
) t, w& B- j8 H# k* X
, j& o* o4 J7 M/ o1 \5 m
用户态程序和内核交互,用户态程序的各种状态,错误等可以由内核来捕获并显示。* {% k0 f8 B& r" ]. D
& k4 k1 {& p( O
而内核是直接和硬件交互的,内核出错之后整个系统就无法正常运行了,所以要想熟练的进行内核调试,0 k1 ], g7 p8 W k- c" j
) R- S7 I9 u) Q/ A- i: c首先要熟悉内核已经给我们提供的工具,然后实实在在的去做一些内核功能的开发,在开发的过程中不断熟悉内核代码,增加内核调试的经验。
3 [& @5 j8 b* u- s1 Y) \3 x- C0 ^. T4 @" q; z8 u4 \
. b. q* g+ F! `9 @! B! I& |* {: x* v! m, q( \7 Z% o" ^+ r* u
主要内容:
3 A7 a. i7 O) x
! A4 N5 m' W# K5 w, }( M内核调试的难点
# S2 K! i2 w" b* ~8 W内核调试的工具和方法% N! @! B' B. h
总结$ o3 M& Q: e' J2 K& k" j
* |9 y, n8 n+ b* z v! s- h# [/ V/ `2 Z6 G2 Q; u" f0 U
1. 内核调试的难点& X! A& G. L- f2 h, L
内核调试的难点大致有以下几个:$ i9 _$ ?( X8 n C
& V X' ]9 G1 L& j* v3 B3 S重现bug困难 - 如果能够重现一个bug, 相当于成功了一半. (特别是有些bug和硬件相关, 执行几百万次之后才有一次错误)
% D4 \ W& x% H a调试风险比较大 - 稍有不慎, 即造成系统崩溃
/ m9 t: q9 S$ [7 @8 s: k定位bug的初始版本困难 - 内核版本更新很快, 很难确定bug是在那个版本开始出现的& d- Y* a1 [* i J! p
; A& x; v4 `0 R7 c9 E* e7 K0 W1 G
. b8 d3 s: J3 o- Q- Y( M; @% u+ @+ M
2. 内核调试的工具和方法8 `% d8 r Q' E0 e% S3 Y7 F" _0 l
内核调试虽然困难, 但同时也极具挑战性, 如果能够解决一个困扰大家多时的内核bug, 那将会给自己带来极大的成就感. # i: l2 q7 J7 o% |
& A6 J) E( l" A) T而且, 随着内核的不断发展, 内核调试的手段和方法也在不断进步, 下面是书中提到的一些常用的调试手段.
2 Y! b$ D* l: {8 Y! U
. }* Y& g2 `& {
2 l/ e7 ^& ?3 t# x$ D( R" t" |% ]2 u: O) x& m
2.1 输出 LOG! c; T) i4 _* W# H
输出LOG不光是内核调试, 即使是在用户态程序的调试中, 也是经常使用的一个调试手段.
2 [! M1 c4 n+ c& p& k' Z4 W+ S2 F; ~$ g
通过在可疑的代码周围加上一些LOG输出, 可以准确的了解bug发生前后的一些重要信息.
4 L; H1 A. O2 a
* K9 W3 x8 l' U; d$ ?$ ? t4 {, x
& B; P" x. G# x$ g% P/ M! r5 U$ p2 w2 S# ? D/ C& M5 D3 Y
2.1.1 LOG等级4 x5 X# O9 F5 f9 p5 {3 w9 H& h
linux内核中输出LOG的函数是 printk (语法和printf几乎雷同, 唯一的区别是printk可以指定日志级别)" |' E2 |9 g c5 n" F4 i
|1 ?$ l- q2 Z
printk之所以好用, 就在与它随时都可以被调用, 没有任何限制条件.! r( t, w" L" {7 R
! J; T- @ N" P; _& S ^ ]
printk的输出日志级别如下:等级 | 描述 | | KERN_EMERG | 一个紧急情况 | | KERN_ALERT | 一个需要立即被注意到的错误 | | KERN_CRIT | 一个临界情况 | | KERN_ERR | 一个错误 | | KERN_WARNING | 一个警告 | | KERN_NOTICE | 一个普通的, 不过也有可能需要注意的情况 | | KERN_INFO | 一条非正式的消息 | | KERN_DEBUG | 一条调试信息--一般是冗余信息 |
- Q I1 U- J2 D" t; T9 `8 N0 I8 j( c6 `- j- J+ _, d
7 I8 Q" o6 D1 m G) Y8 J- ]输出示例:2 Q2 p) r0 G* f/ `4 K% @
. P: J& f& t$ ]+ A0 k+ h! Q9 z3 eprintk(KERN_WARNING "This is a warning!\n");
# |. ^6 E# b4 {. @( d3 Nprintk(KERN_DEBUG "This is a debug notice!\n");
# j/ Y) A$ [" K q" O
/ @8 ^5 [" k8 S V6 ^
' k8 t# j) _; Y. M! P2 _+ V2.1.2 LOG记录- T! s7 j% x0 X7 J* D; `1 X8 {
标准linux系统上, printk 输出log之后, 由用户空间的守护进程klogd从缓冲区中读取内核消息, 然后再通过syslogd守护进程将它们保存在系统日志文件中.
$ b: I) x3 h* ^+ ~" Q! t3 J8 P& M% K5 n* t
syslogd 将接受到的所有内核消息添加到一个文件中, 该文件默认为: /var/log/dmesg (系统Centos6.4 x86_64). v; o& H9 N, s2 j7 H) V
9 O7 O0 K; q/ ? $ L+ r+ ]& Y3 [6 f
" M5 x6 I z: g& K" i5 F/ x" p
PS. 上篇博客中的内核模块的输出LOG, 都可以在 /var/log/dmesg 中看到/ a' q. g2 Z/ `1 t" Y
4 P" ]% V1 x+ E" S$ C6 k ' B8 l- U# D" c3 K/ v
6 u) ^) v6 U& d) ]! L+ J$ u! R
2.2 oops/ r+ K1 Y" t, t3 F# J6 Y4 ?
oopss是个拟声词, 类似 "哎哟" 的意思. 它是内核通知用户有不幸发生的最常用方式.- a1 T4 d* S9 ~) D
) }6 g; l! K' n8 J8 x' }! N触发一个oops很简单, 其实只要在上篇博客中的那些内核模块示例中随便找一个, 里面加上一段给未初始化的指针赋值的代码, 就能触发一个oops( g& [+ ?9 N5 n, A
( t- V+ e, z7 W' U b
. W) H* ?1 l5 l
' v. ?( Z+ u" t* R H7 c+ n# roops中包含错误发生时的一些重要信息(比如, 寄存器上下文和回溯线索), 对调试bug很有帮助!# b6 G* C1 {/ u# ]
. a$ W1 L1 ~8 U+ H# h3 B: Y6 S: {
" Y6 F- _; u9 B# Z* a
, i a% b+ ~% i; [# ]' x8 B调试内核时, 还可以开启内核编译参数中的各种和内核调试相关的选项, 那样还可以给我们提供内核崩溃时的一些额外信息.' }7 l/ R- m( S4 B, j
J* J5 z8 `- K9 F/ L
* k' ?6 a9 U A
2 L' \- P" g9 b( x$ b2.3 主动触发bug& J6 ~! Q- r5 D1 I6 o% M9 s( L
9 M) V) m2 c8 R( ?' i8 J0 L$ p. J8 B
调试中有时将某些情况下标记为bug, 执行到这些情况时, 提供断言并输出信息.- b) W4 m3 U4 V: @. w; {" ?9 o
: \5 y! \# ~8 [+ f/ o/ e$ l, Q
BUG 和 BUG_ON 就是2个可以主动触发oops的内核调用.% A2 }- m+ Y \
' `- d: Y( c+ U* n( t
# A& B. H6 J0 p4 Y( Y$ ]& }2 l6 a+ l. a- N8 ] `
在不应该被执行到的地方使用 BUG 或者 BUG_ON 来捕获.
6 \ `. l. H% ?6 @3 l
E4 M$ O7 K) j比如:
1 B" X8 [2 P+ F. p- V3 h
/ j. m6 E( W% j% D pif (bad_thing)
3 p( x$ }6 S" A8 S5 E1 Q6 F BUG();
. M( U7 t8 L2 E- k) }( l// 或者
& u* C5 j/ P6 t6 _" B# |% M/ ABUG_ON(bad_thing);# E ]0 Q* g. n7 F' F
) k3 {" V3 ~9 Y! a, o* B. u& S& a9 I m
, C: z' y; r) l% E如果想要触发更为严重的错误, 可以使用 panic() 函数
A2 c8 ?) t, V" ]' s( m
7 N, D" {" A7 P& D) o比如:
$ R( l0 w$ ~- ?5 r3 ]3 Z8 ]6 [: u: o" X$ q9 n- w$ C6 s& a
if (terrible_thing), Q$ E8 x' T% [+ l! m, h/ s0 e5 ^
panic("terrible thing is %ld\n", terrible_thing);
2 m9 ~; l- d/ K: A4 n+ y8 D + Q, r( X3 o$ F3 n$ }9 w& p8 {
1 ~3 D* S7 r0 j
此外, 还有dump_stack 函数可以打印寄存器上下文和回溯信息.
! B. F% F5 J4 S7 Y! ^, r% i$ V
' u& ~+ L: M1 }) ~比如:
* t( X: X8 F1 y, _
* }$ L, e- A1 A' p, Kif (!debug_check) {
( l* Y' u6 C. F q3 D; L4 ]! ] printk(KERN_DEBUG "provide some information...\n");. j3 b* ?7 F" {% k: u* `% W
dump_statck();
4 u4 j$ v9 V- V; j8 [- Y}
* t3 S' h0 P9 Y- S3 _# b2 @ 1 S: {4 z P9 X' ?! B; Z8 D) O, Y$ u
; |! G) W) F. B- V0 d6 |
2.4 神奇的系统请求键: B% I/ T$ v: ^- |& ?2 i
& _6 O3 M2 b7 v2 f4 T( L
这个系统请求键之所以神奇, 在于它可以在一个快挂了的系统上输出一些有用的信息.
: O9 q, h5 H0 n# r5 J& ?7 `' _: H) ?
$ s/ D I' {2 S7 n: T这个按键一般就是标准键盘上的 [SysRq] 键 (就在 F12 键右边, 其实就是windows中截整个屏幕的按键)8 L- [, x! P, N4 g0 s# p
: O+ ~+ h# `/ w' T. z' O4 M
单独按那个键相当于截屏, 按住 ALT + [SysRq] = [SysRq]的功能8 @. b. e; k6 M6 u: h; l
. I9 D8 S0 l% z1 X* V5 L; t6 y; W
) O* _, F; t$ u: _- u* X/ C( t- X3 `7 L$ C$ N
启用这个键的功能有2个方法:
( |4 \) ^" D, h' ~6 s
9 G5 d# H' v7 Q$ s4 \5 Q' B" c. h" m- |- 开启内核编译选项 : CONFIG_MAGIC_SYSRQ
- 动态启用: echo 1 > /proc/sys/kernel/sysrq1 [1 r/ ]5 @) v) S9 X
, c6 z/ S" C8 @. @% ]* }% G9 D1 ~) o7 ]+ M
支持 SysRq 的命令如下: (注意要在控制台界面下使用这个键, 比如通过 ALT+CTRL+F2 进入一个控制台界面)
! U, V. a8 @; b- k$ y" m4 Y* Y' @7 e0 R2 s/ N4 V9 B
主要命令 | 描述 | | SysRq-b | 重新启动机器 | | SysRq-e | 向init以外的所有进程发送SIGTERM信号 | | SysRq-h | 在控制台显示SysRq的帮助信息 | | SysRq-i | 向init以外的所有进程发送SIGKILL信号 | | SysRq-k | 安全访问键:杀死这个控制台上的所有程序 | | SysRq-l | 向包括init的所有进程发送SIGKILL信号 | | SysRq-m | 把内存信息输出到控制台 | | SysRq-o | 关闭机器 | | SysRq-p | 把寄存器信息输出到控制台 | | SysRq-r | 关闭键盘原始模式 | | SysRq-s | 把所有已安装文件系统都刷新到磁盘 | | SysRq-t | 把任务信息输出到控制台 | | SysRq-u | 卸载所有已加载文件系统 | , G4 {) @$ T7 w% H3 W
3 u( Z8 h( h: q! ?3 \8 m; B k) ]
4 Y: ^& H- }6 p& V* w* X; Y- u2 d2.5 内核调试器 gdb和kgdb
@$ T/ |5 \# d0 Q: g& u; u& R
; u6 [5 h0 F* R2 e& H% F) r: J, Nlinux内核的调试器可以使用 gdb或者kgdb, 配置比较麻烦, 准备实际用调试的时候再去试试效果如何..7 x2 v% A7 n* m7 k: V% S" k* _ ^& w
/ E+ ~8 Y& r# w/ E) j" P1 I
* r# |) ?% M! J
+ o. g, P: s) A5 o& w. u2.6 探测系统3 H" ?+ K" i6 ]& t2 @2 t
2 N& T, h2 Y* B5 _下面一些方法是在修改内核后, 用来试探内核反应的小技巧.
. N! @8 k# I/ S" @6 O/ Y
s& N7 {0 c& {4 R
+ u! o; @+ {, t7 d8 | ~2.6.1 用UID控制内核执行2 ]1 `8 o$ W9 ^* I$ F% e$ \
* o. {; h. m3 l- E
比如在内核中加入了新的特性, 为了测试特性, 可以用UID来控制内核是否执行新特性.
5 h& \8 E `" J& a, s0 u: X8 U9 H. I' X0 w4 d5 i4 _
if (current->uid != 7777) {
8 \& p8 t7 h& P0 g /* 原先的代码 */4 A. T! Q$ S) d. k1 y2 t, C
} else {
5 U& R, k' A- b' Y: U: Z# L- \ /* 新的特性 */, F0 G. H' {) @( b3 ~9 `
}4 U: x3 x$ t6 R: ^ t$ S9 d9 F
1 N2 }$ }) C7 Q8 {
7 w M9 z% _- b1 b% p2.6.2 用条件变量控制内核执行
- D0 a0 \$ h/ k( l' r; S* x+ X2 y) q/ ^, W0 p
也可以设置一些条件变量来控制内核是否执行某段代码.
+ C3 d- {9 }! M, c$ D7 T
; K' Q- |% U; z) e `$ ]5 T- T条件变量可以像上篇博客中那样, 设置在 sys 文件系统的某个文件中. 当文件中的值变化时, 通知内核执行相应的代码.
# ~/ Z+ ~% j* M$ z2 }" p$ B" z7 p$ M5 E' X
5 ^- L2 m% Q% N6 G- c9 b4 { @2 Q) Q# ?7 U6 v
2.6.3 使用统计量观察内核执行某段代码的频率$ i5 `( o' V7 M( q, i
7 V+ j8 w3 q y1 n0 ? ]/ p5 z实现思路就是在内核中的设置一个全局变量, 比如 my_count, 当内核执行到某段代码时, 给 my_count + 1 就行.* z* E1 P1 s2 G! y: Y
! A$ w0 b9 p3 `& S. w2 Y8 _同时还要将 my_count 打印出来(可以用printk), 便于随时查看它的值.$ \& X% S z- z [' X+ w2 u
% T! P" L7 X+ E
4 T) M1 C$ V6 \5 p
! k: }) `( U; s- |( S* ^7 q Z
2.6.4 控制内核执行某段代码的频率8 Q! T! V2 {5 w' |
! I/ v/ S* |3 ?. D有时侯, 我们需要在内核发生错误时打印错误相关的信息, 如果这个错误不会导致内核崩溃, 并且这个错误每秒会发生几百次甚至更多.* T6 ^0 j2 ?( l2 A) a: ^
" z2 T2 B$ N n: Y- T ?" o
那么, 用printk输出的信息会非常多, 给系统造成额外的负担.& O$ e4 ?4 H( J. y8 l5 c+ T' _% `
. e Q* K! l; x/ q7 g2 O0 Z这时, 我们就需要想办法控制错误输出的频率, 有2种方法: M2 [- r5 f( s! Y$ B0 F% L2 M/ T
/ ^4 |/ G0 A2 K Z* j! @7 ^
方法1: 隔一段时间才输出一次错误4 `) g \5 `. s, |* {3 Z
* {9 P o2 j/ S5 ?0 r8 Tstatic unsigned long prev_jiffy = jiffies; /* 频率限制 *// `5 t8 F1 e3 J- C
5 p5 J" s! L8 V. S
if (time_after(jiffies, prev_jiffy + 2*HZ)) { /* 输出间隔至少 2HZ */
. @1 F' I/ v' |; ~$ j2 g prev_jiffy = jiffies;( v! \( I* R8 Y, P8 \+ o2 V
printk(KERN_ERR "错误信息....\n");2 F9 \& ^$ k& ?, Z3 k E! K# M1 Z
}
2 O7 J4 ]5 I: Y& y! m# I
0 a& @6 B# h" H# m7 k$ T4 N1 Z! I! x* `$ w& k
方法2: 输出 N 次之后, 不再输出(N是正整数)& v7 L0 V" r; e
) v; B! _- n. j1 ]- K4 @$ l# ?& ?static unsigned long limit = 0;
. ]: V: U# g% v/ ]/ e3 V$ K* M9 B! h5 y, m# L. H$ M
if (limit < 5) { /* 输出5次错误信息后就不再输出 */2 v( c# _ t7 t! a
limit++;
! O6 v3 T" @0 |+ ?+ u* m6 A$ d printk(KERN_ERR "错误信息....\n");' Q6 C' Z7 M" x) L" S9 b
}
4 T; U* e! P# ` F/ M" H' |6 e
7 D- C0 p$ r0 T, w z+ ~; ^" l/ U l8 o6 o
" Z2 v+ D0 u8 ]. H! v
2.7 二分法查找bug发生的最初内核版本" D5 b& s1 e0 f. w0 _
1 B! f F" ], f( [4 ?8 `" {3 t
在内核发生了bug之后, 如果能够知道是bug从哪个内核版本开始出现的, 那对修正这个bug会有很大的帮助.
0 b% S' ^# g: Z' O; @
2 C; i: W3 E3 A% s由于内核代码非常庞大, 即使用二分查找法, 手工去找哪个版本开始出现bug的话, 仍然是非常耗时和繁琐的.
# f1 P5 l- F/ F: z8 c4 L$ I1 \) ]1 Z& Y m8 v. Y% A) `6 f# }
% |% z' W0 Z" h# Z7 v, X- ^
1 ^9 B$ W: I. `4 w0 E; }好在 Git 给我们提供了一个非常有用的二分搜索机制.
5 O+ Z! [9 }# m5 l
2 c; ?5 _0 v0 ^9 Tgit bisect start # 开始二分搜索 |: v g9 q: Z6 S
git bisect bad <bad_revision> # 指定一个bug出现的内核版本号: }7 }4 ^: s z I. J
git bisect good <good_revision> # 指定一个没有bug的内核版本号, 此时git会检测2个版本直接的隐患; A; D5 F' D# Q0 L6 U* \7 O
5 C6 t, c* m2 L" p. s) N
# 根据结果再次设置 bad 和 good 的版本号, 缩小Git检索范围, 直至找到可疑之处为止.
8 L7 h9 w0 k" }- `
5 g4 z* N& a4 D0 o2 c/ J2 _& l+ Q! O, |4 a% ]
1 ~) {' S% S h9 i$ b; x9 C2.8 社区
4 g) i9 m9 i7 l# o8 ^8 b+ O4 h( [ J& g
当你在调试bug时用尽了一切手段仍然无济于事时, 可以考虑求助linux社区, 求助时注意一定要描述清楚bug的状况.+ y0 ^' q) H& y) \9 d n
, l$ @6 v' V2 B(可以参考一下别人汇报bug的格式)2 O/ e9 D8 {7 N' H; t
" h- t7 K7 @1 D
; a$ e. e3 @: c% N: D. v* S+ @" \
' U. Q: F% Y C# ~4 x
9 h+ }) n" W( k) ]9 x# o. E3. 总结
! f, m( `1 p# T1 y2 \0 `7 {7 u7 N$ i; c
linux内核调试必须要依靠大量的实践来掌握, 仅仅靠上面介绍的一些技巧还远远不够, 只有实实在在的去阅读内核代码, 实实在在的去修正一个个bug, 才算真正掌握内核, 真正了解内核.: R8 V4 E# A& i- a1 T5 I- e# J9 Q
; s/ z) f% A9 m1 p
多看看之前linux内核bug的修正案例, 也是个不错的积累经验的方法.
1 \& G7 X& b! ~( V3 p2 h z N" V
4 r- z1 s# ~' ]) s0 q/ D4 N
. T2 N* v" Y$ z P/ T7 W9 e0 a9 o& aPS.
2 y" m! B' p0 c% A4 o8 l$ d
/ @9 T% ?7 K) a0 s/ P8 y( E对于初学者来说, 在真机上做内核开发动辄导致机器崩溃(panic), 非常麻烦. 现在的虚拟机这么强大, 建议都在虚拟机上测试linux内核修改的效果.# Z, ^6 K! m. E% o; W- `
; P5 r, e. ^* W9 q0 j5 }
我之前的关于<<Linux内核设计与实现>>笔记的博客中的代码都是在虚拟机上运行测试的.
# H" e S& w9 H& G4 d3 F) v |
|