|
|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
本帖最后由 pulbieup 于 2021-3-9 09:50 编辑 & g* g: f" M. T) f# P0 G2 v
( T8 m: m# y) g8 X
内核调试的难点在于它不能像用户态程序调试那样打断点,随时暂停查看各个变量的状态。
; W2 I; n; F! g
, s6 Q7 d! l( N; c也不能像用户态程序那样崩溃后迅速的重启,恢复初始状态。, G- R) S' p" {/ p, N
- l* ?0 j8 A# G7 P
, C1 g @' ?3 \( ?! ], L( p
: {0 h8 h& p3 ~$ R$ V
用户态程序和内核交互,用户态程序的各种状态,错误等可以由内核来捕获并显示。! W% j: R0 h$ L* }. @, ^1 M* Z
) u, b- Q7 }0 o0 k* A! G. X/ l
而内核是直接和硬件交互的,内核出错之后整个系统就无法正常运行了,所以要想熟练的进行内核调试,3 I$ x; D% z7 S% W j
7 v& _6 X- f# X8 I3 O8 e
首先要熟悉内核已经给我们提供的工具,然后实实在在的去做一些内核功能的开发,在开发的过程中不断熟悉内核代码,增加内核调试的经验。
+ j" R: z/ j1 O h- \; R t, {
. \0 A2 ^* o7 g2 ]9 f 4 @. o! m! X, _
( U& H9 ?1 {* [) I5 D" p) F) @( j主要内容:
) I' t0 o9 |& q; \! R3 K
* A% X6 [5 t2 e8 d+ t# j内核调试的难点
. o$ z7 t! V' I9 M4 e5 R3 V内核调试的工具和方法, V5 s1 @2 B* r. B6 c
总结& h, p! k, p6 Y
0 N& L3 v+ L- |2 q, h. P
3 p3 K) J( W% i0 |) {1. 内核调试的难点
' ] _( u. y2 i: L6 N内核调试的难点大致有以下几个:
. X9 f; n6 r( o/ X, n0 I. c& x
4 d6 N3 v4 b% Z2 P重现bug困难 - 如果能够重现一个bug, 相当于成功了一半. (特别是有些bug和硬件相关, 执行几百万次之后才有一次错误)
3 w; R8 k& Z8 S1 j$ B3 c% s& i调试风险比较大 - 稍有不慎, 即造成系统崩溃7 ]' L( Z4 F7 \) v: Y# ]8 ~
定位bug的初始版本困难 - 内核版本更新很快, 很难确定bug是在那个版本开始出现的
1 D, X1 G: b! r* n) p+ V; X$ `- `
$ ^, H2 G y2 ]0 G7 d
7 p& N8 z: m4 w+ b& P$ \5 T% Y2. 内核调试的工具和方法
2 g; Y R7 G& `" @内核调试虽然困难, 但同时也极具挑战性, 如果能够解决一个困扰大家多时的内核bug, 那将会给自己带来极大的成就感. 1 M# a9 z7 w( e: Z: v
8 Y9 B& w; H7 a" p. Z
而且, 随着内核的不断发展, 内核调试的手段和方法也在不断进步, 下面是书中提到的一些常用的调试手段.2 b$ j: G& h D# R4 Z7 ]1 \
+ u. x$ Y X4 @& s
2 {! n% J5 R4 Q3 {0 }
: Z; }7 y2 j$ p
2.1 输出 LOG! }0 P) x8 H7 ~8 B& i3 @
输出LOG不光是内核调试, 即使是在用户态程序的调试中, 也是经常使用的一个调试手段.
- Y. K* `% ~, C) d f, L5 L- ?* u8 H& R# b3 z7 y }2 J
通过在可疑的代码周围加上一些LOG输出, 可以准确的了解bug发生前后的一些重要信息.
9 g0 y' J8 f% p6 \( M4 ~6 S% Z4 w, Q7 |4 y( S% b, w; K
9 k; K: C2 U6 {: R$ _( m; T
! w$ e# k, L7 u: X+ m. [% [* J" u2.1.1 LOG等级
( o* I& N( P& vlinux内核中输出LOG的函数是 printk (语法和printf几乎雷同, 唯一的区别是printk可以指定日志级别)3 j& A# p3 z( P2 Q- E* V8 `9 Q8 `
; u' k5 }" `1 s+ n m5 a! ]
printk之所以好用, 就在与它随时都可以被调用, 没有任何限制条件.# I( b+ _7 w( [8 _8 Y; g
4 w m) S2 S6 x" K& Kprintk的输出日志级别如下:等级 | 描述 | | KERN_EMERG | 一个紧急情况 | | KERN_ALERT | 一个需要立即被注意到的错误 | | KERN_CRIT | 一个临界情况 | | KERN_ERR | 一个错误 | | KERN_WARNING | 一个警告 | | KERN_NOTICE | 一个普通的, 不过也有可能需要注意的情况 | | KERN_INFO | 一条非正式的消息 | | KERN_DEBUG | 一条调试信息--一般是冗余信息 | % Y F" n& i9 J/ Y- @9 `4 z) V8 @
( L2 _* \- }7 W8 g+ f
4 N: R. \, f; h O" l输出示例:- P4 n% j, @* I; H
1 {7 ]) Z& [/ E. V. w! Eprintk(KERN_WARNING "This is a warning!\n");1 s8 Y# ^5 x% m/ j" r9 g* @
printk(KERN_DEBUG "This is a debug notice!\n");" [7 r# R6 m! _: {5 l6 x/ k
" f, y$ H8 `+ t( h7 w# P/ T) U; p* |
2.1.2 LOG记录
y2 ] p. s5 I4 E标准linux系统上, printk 输出log之后, 由用户空间的守护进程klogd从缓冲区中读取内核消息, 然后再通过syslogd守护进程将它们保存在系统日志文件中.
0 c# Y1 |' V0 J6 R* G
9 y8 O3 Y: j2 I0 Gsyslogd 将接受到的所有内核消息添加到一个文件中, 该文件默认为: /var/log/dmesg (系统Centos6.4 x86_64)7 J3 V' H9 d$ h) o/ c; s& | e2 U
7 K1 R$ `1 Y9 x* G 6 [& k2 B6 x! C+ G, I
9 N+ F9 B! Y1 G' R
PS. 上篇博客中的内核模块的输出LOG, 都可以在 /var/log/dmesg 中看到
4 | W, p1 Z4 S5 A# S$ [) e. I6 k$ g$ M6 D4 G
/ |" h( y3 V/ }8 Q% [3 {" F- i, o/ J3 V9 w" J* w
2.2 oops: ~6 {! a7 z* g; n) j$ u; I
oopss是个拟声词, 类似 "哎哟" 的意思. 它是内核通知用户有不幸发生的最常用方式.. U& |# J" @' p, ?' v
& L! {. |6 m4 x/ }9 e9 Z4 Y
触发一个oops很简单, 其实只要在上篇博客中的那些内核模块示例中随便找一个, 里面加上一段给未初始化的指针赋值的代码, 就能触发一个oops6 \, W( ]( t/ |8 ~- @& Y
7 R; ~& p& _3 g* H6 v, O+ O ; ]: e6 Q' T2 e; i# A: b
8 O8 ^9 g, A1 P% [& foops中包含错误发生时的一些重要信息(比如, 寄存器上下文和回溯线索), 对调试bug很有帮助!8 R( F) \/ j- H3 A5 r8 M C
, e$ ^6 ?" n: J6 V! G. x
: b: z( Z7 h3 F& }. w S
4 [. T9 C5 v2 k1 U- l3 H5 {! q调试内核时, 还可以开启内核编译参数中的各种和内核调试相关的选项, 那样还可以给我们提供内核崩溃时的一些额外信息. s# f3 p$ `: F$ Y/ a
2 M6 E% J( H1 U& x& }# ? & }1 J: O1 _' `2 ^0 |8 c
* T, K/ m% b9 c- m6 m8 u( [9 n2.3 主动触发bug8 s' r1 N6 v# }. w& `
6 N2 M( d8 e% w1 f' I, ]9 S; B% L5 Q" E8 R
调试中有时将某些情况下标记为bug, 执行到这些情况时, 提供断言并输出信息.% k( y: e: g; @' H0 J8 r7 v/ A; i
. q0 R {) J* }4 x' b7 m0 TBUG 和 BUG_ON 就是2个可以主动触发oops的内核调用.+ N5 G; r, n0 z1 ^
5 w* a; e; @% d# z; y# ~2 c' L+ x
1 U4 N5 R: t' U' G9 _/ s
2 g* Z8 T3 v' `6 I- F) d( r在不应该被执行到的地方使用 BUG 或者 BUG_ON 来捕获.
8 o5 | T s9 N3 ~+ r3 {" ^( h9 f7 F- @5 U: }
比如:
9 E% \# `4 f7 v Z m7 m' ?1 G# x! f7 t. T
if (bad_thing)# ~' T5 {+ a2 [% H
BUG();
$ y' x5 l9 i5 n9 |& ?! K5 B# P// 或者
7 N/ w: y( x- H+ A& j( w8 ?* S8 VBUG_ON(bad_thing);
. }7 ? M0 L Y. f
; R8 C" k3 f6 @$ @/ x0 ]* Q! E; ?6 C" ?; c0 u% l
如果想要触发更为严重的错误, 可以使用 panic() 函数1 M9 T# r! x+ N* w& b- A6 V% v
% O' |( G, D/ l8 }7 {
比如:" ?8 o4 N H+ g+ e+ c4 C
/ k8 W: G# M* l V
if (terrible_thing)
5 p2 e1 G7 n+ E) K7 Y panic("terrible thing is %ld\n", terrible_thing);
$ {1 G+ y0 Z9 \& y
# u5 Q# q! J% q
4 ~, ?) o$ V) l4 B$ B! c7 J此外, 还有dump_stack 函数可以打印寄存器上下文和回溯信息.
0 `- ]- z6 v4 G+ k1 S
0 b' _8 r: o8 X5 }" d比如:8 A; l m: N1 H* @
/ v# f0 @! t7 Q& Kif (!debug_check) {5 F$ @/ _; u" p& R% ^/ i3 F! `
printk(KERN_DEBUG "provide some information...\n");1 y% ]/ y8 {- Y. \2 u* ?
dump_statck();
) H4 L) o- Q' k3 C}! a8 b' `- z0 x7 V1 O4 K' n' ?$ k
) G% D$ l% t5 G' m
W `- B. ^+ ^. V/ J
2.4 神奇的系统请求键
6 L. H( K! G) z, m+ B- Z. K( ?. _+ \
这个系统请求键之所以神奇, 在于它可以在一个快挂了的系统上输出一些有用的信息.; I0 ^) S) `6 y# s7 W5 p' M2 n) I
/ C1 H5 l2 S9 G2 R5 ^4 t+ C* f
这个按键一般就是标准键盘上的 [SysRq] 键 (就在 F12 键右边, 其实就是windows中截整个屏幕的按键)
' x, `: f: o3 r- B# E
/ K; h4 e6 U9 ~3 I( O1 p0 z# F单独按那个键相当于截屏, 按住 ALT + [SysRq] = [SysRq]的功能% z. f- H L& m( ~ @
& X1 Y. O% Q7 }# x
0 v: j. }. H6 }$ W
6 s! [8 ~7 B2 Z& e启用这个键的功能有2个方法:) q% o0 ~3 R' {
7 p2 e+ z1 l. c9 `$ `- 开启内核编译选项 : CONFIG_MAGIC_SYSRQ
- 动态启用: echo 1 > /proc/sys/kernel/sysrq
4 l7 T' B! d" O - Z9 Y5 F# l! S
$ U7 l3 B1 N! ?2 D9 `# x# K E+ h7 B支持 SysRq 的命令如下: (注意要在控制台界面下使用这个键, 比如通过 ALT+CTRL+F2 进入一个控制台界面)
' b$ l( K8 r a; y, ]( R/ Y/ d$ b% Z- }
主要命令 | 描述 | | 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 | 卸载所有已加载文件系统 |
& e4 K& x0 h( ]' @4 m8 @
) l* m1 y/ H6 n3 F9 v! U- o
$ W8 Y6 j C$ o3 E0 q2.5 内核调试器 gdb和kgdb
; | |, y( ~" [
9 s5 t" r5 F H% A& t. Z2 T' |" i' nlinux内核的调试器可以使用 gdb或者kgdb, 配置比较麻烦, 准备实际用调试的时候再去试试效果如何..
+ @9 P6 p& Z: N! q+ C
) ]4 {, f. N0 V0 l
/ m7 ]4 a; H. O& C9 Q! ~: O8 m
2.6 探测系统
0 j$ |$ x. Z' ~) M5 x& G9 H! M- j# ~% v. Q
下面一些方法是在修改内核后, 用来试探内核反应的小技巧.
0 f. ~' n3 O* P& H; B2 e8 l% R1 r, h* Z
9 F" Q2 N* y' k b. z0 P* Q
2.6.1 用UID控制内核执行 V1 ?9 P# u5 n+ d! G
! m! \% O' x" W/ _
比如在内核中加入了新的特性, 为了测试特性, 可以用UID来控制内核是否执行新特性.; P6 D7 C m2 R' B- D
' m3 S2 _0 u* g" u) {# r! @( q3 b
if (current->uid != 7777) {8 m4 u1 G) k* I( c) ?
/* 原先的代码 */
) X. i/ P6 u0 Y" B8 w2 g" `} else {
# j3 [- V" v. O% [! ]6 s, @8 E /* 新的特性 */) k; z9 R8 p8 a7 r( c. ~' J
}
% v5 J/ j4 u, G7 o' [) X" y, ^5 Z
0 r3 ~2 t! H' H" n3 c. T; G; I8 l2 Z$ M; |( F r
2.6.2 用条件变量控制内核执行
9 t, N! V2 U/ T. s0 m& j5 p$ [1 \$ K7 ~
也可以设置一些条件变量来控制内核是否执行某段代码.( n2 q1 p6 r; W4 V% E
6 E$ t9 G3 x( O4 T8 p. E条件变量可以像上篇博客中那样, 设置在 sys 文件系统的某个文件中. 当文件中的值变化时, 通知内核执行相应的代码.
6 F6 j7 g" x) t& h* M( |" N: H* M# W5 b& H8 [; b
% a3 _( G( i6 ?4 |" D1 }& J6 k1 V
1 j" L7 f1 m3 e' L0 P r2.6.3 使用统计量观察内核执行某段代码的频率
$ w4 ^' R! C/ V. m) j. o0 V, r5 B8 m) V `* ^3 a& u( q2 P; F
实现思路就是在内核中的设置一个全局变量, 比如 my_count, 当内核执行到某段代码时, 给 my_count + 1 就行.
6 k3 K+ G. Z* j
' u' o$ D. K7 p, f8 g8 t7 {; G同时还要将 my_count 打印出来(可以用printk), 便于随时查看它的值.0 G$ I4 C2 s& j( M7 F
- ~- Z2 V; t3 \1 \+ d4 M
# j; C; h) M/ [) m7 v: q, F/ r9 m+ U3 C, V5 S
2.6.4 控制内核执行某段代码的频率
: b5 X0 n9 E3 k( W; }
- {; F) Y! r) Y% {' _' P有时侯, 我们需要在内核发生错误时打印错误相关的信息, 如果这个错误不会导致内核崩溃, 并且这个错误每秒会发生几百次甚至更多./ R$ I) y- f, |* X' _7 u
5 @( `0 r, n; G$ x+ c# J0 Y" b/ Z) g那么, 用printk输出的信息会非常多, 给系统造成额外的负担.2 q" J, `- g( U6 C% k0 D
6 R. m9 q; ]. A4 ]: z: |这时, 我们就需要想办法控制错误输出的频率, 有2种方法:; W& v& l, A# J# S) O, |
) N" ? F1 m; d3 y' z" m方法1: 隔一段时间才输出一次错误8 k+ n' J. M- t8 g: H4 L1 D* c' e
$ s! D# W: Z- g/ Zstatic unsigned long prev_jiffy = jiffies; /* 频率限制 */
$ G. ]) t' n( a4 ]4 \: _4 ~( f5 o# J! ]/ w" e: B* E% q# K8 r
if (time_after(jiffies, prev_jiffy + 2*HZ)) { /* 输出间隔至少 2HZ */
0 Q3 g( \2 I$ \2 B0 i5 H: t prev_jiffy = jiffies;
$ X0 r% v; W8 ]/ R% Y printk(KERN_ERR "错误信息....\n");
" C$ X+ y, d/ X$ u! A( v}
' z9 W. c! ]- t* A* B5 X
" \9 |) L, E7 h9 Z; ~" N( V
- v/ U/ q0 C: \9 [0 Q$ i方法2: 输出 N 次之后, 不再输出(N是正整数)
; u3 m2 Z1 Y. o3 e
0 ^) o$ e, O e& ~9 L0 w* L$ pstatic unsigned long limit = 0;# f- M, w6 d: @1 O
: N* [$ D6 J7 X- ]- n
if (limit < 5) { /* 输出5次错误信息后就不再输出 */
' k2 o" Y/ h# u! y( K$ ? limit++;
. z3 |8 [7 Z, x8 e0 P, _ printk(KERN_ERR "错误信息....\n");+ `& f7 i( J% l& g) w" Q
}
# A# \2 k% _( t # [9 P. ^' M% i/ Z4 x
! y/ C* K9 n2 x$ d8 ~ u( C& b
, w) J" X. o/ h% M+ b
2.7 二分法查找bug发生的最初内核版本6 t* Y2 Q% m. Y( A5 |0 x+ s6 E8 G y
) T7 u* [4 m$ B8 @3 |3 C3 z6 H在内核发生了bug之后, 如果能够知道是bug从哪个内核版本开始出现的, 那对修正这个bug会有很大的帮助.: A! E6 c+ A, u( D) v7 M
2 p3 _ b% i7 i: Z, t1 b! S由于内核代码非常庞大, 即使用二分查找法, 手工去找哪个版本开始出现bug的话, 仍然是非常耗时和繁琐的.
" W3 N+ f4 @' O! [
3 w" d0 Q) q2 A/ l& J
% y, P) q1 U# e# L
! G8 Z6 j" q% ]8 c Q好在 Git 给我们提供了一个非常有用的二分搜索机制.
9 R5 P Q0 f. k* H. {" ]1 g: [6 ~9 u* Y2 Z" |$ n; g- X
git bisect start # 开始二分搜索
2 i, s( \2 p. T% ?8 P$ S& ]. tgit bisect bad <bad_revision> # 指定一个bug出现的内核版本号
+ i1 \' d- h7 h5 W4 Lgit bisect good <good_revision> # 指定一个没有bug的内核版本号, 此时git会检测2个版本直接的隐患4 x& {/ n$ Q% N5 i4 k8 {- q
8 n( I: I. w+ }% q8 e) B
# 根据结果再次设置 bad 和 good 的版本号, 缩小Git检索范围, 直至找到可疑之处为止., w8 T: C H& f; J
4 t" O* }. i+ I6 w& ~7 y1 e; R2 f5 y5 k
6 i6 Q7 m# ]. }5 m3 I b& |& y/ J4 O
( e4 @" q% ~. ~8 v* s* }! `" [1 @2.8 社区5 k) h3 ]" h: N
! f; d5 O" B) p) \+ Z* y; t
当你在调试bug时用尽了一切手段仍然无济于事时, 可以考虑求助linux社区, 求助时注意一定要描述清楚bug的状况.
- ?/ l4 y7 A# w9 G5 R4 ]
5 c+ U, S0 q4 }0 Y3 C(可以参考一下别人汇报bug的格式)
; Z3 R! X$ ~/ Y6 \) J) U9 ^% U( p$ \. m6 p+ o" v
. F u6 H; o; \% m5 g* \" {( _0 Q& v( [% H& ^ D% j' J
/ R) K- N/ r5 X3 g/ s9 h0 i3. 总结% g4 ?* W9 o) ]) b! t
% l! j2 X2 H8 J3 B/ e) z1 ]
linux内核调试必须要依靠大量的实践来掌握, 仅仅靠上面介绍的一些技巧还远远不够, 只有实实在在的去阅读内核代码, 实实在在的去修正一个个bug, 才算真正掌握内核, 真正了解内核.
4 q" x/ V6 e* C" C% `& \3 {7 c0 { q8 y, ?6 _1 H; {$ V9 }
多看看之前linux内核bug的修正案例, 也是个不错的积累经验的方法.8 z/ U- {: o _
: i S k0 H/ [8 V- o8 H2 F$ U
; m$ ^1 S) e3 V8 r
- }, m$ I. I1 w2 L; g- I6 R& e+ X1 KPS.3 `, ~. z0 V$ @6 r3 w: Y$ T
& x7 w: K: Z+ t4 P, ?
对于初学者来说, 在真机上做内核开发动辄导致机器崩溃(panic), 非常麻烦. 现在的虚拟机这么强大, 建议都在虚拟机上测试linux内核修改的效果.
4 J# Q9 R! b6 L! V% K
) f( {" N# P l9 |& B }* b* _3 |" ^% F) f* ^我之前的关于<<Linux内核设计与实现>>笔记的博客中的代码都是在虚拟机上运行测试的.
, K% A, I( I* T- L# d |
|