|
|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
本帖最后由 pulbieup 于 2021-3-9 09:50 编辑 ( Y, V+ t, s3 a
9 V2 H# b5 v5 Q; p$ ~- L$ z+ ]
内核调试的难点在于它不能像用户态程序调试那样打断点,随时暂停查看各个变量的状态。
% \5 X% u* y3 I+ ]( [; a: e5 X
* e* r9 x2 v: H# o% r9 T% q也不能像用户态程序那样崩溃后迅速的重启,恢复初始状态。
6 f( \8 s4 \' d. g) f. `; l+ k: f# m% d' S) @0 \ C/ k4 a- A
. f6 N, z. k! S# H& a
8 I" y; L0 O J9 z# u9 }4 r用户态程序和内核交互,用户态程序的各种状态,错误等可以由内核来捕获并显示。& h$ T' u1 w* f3 n1 U
% ^& ]* m- P, K' p
而内核是直接和硬件交互的,内核出错之后整个系统就无法正常运行了,所以要想熟练的进行内核调试,9 I/ @% H: E/ L9 Q( e. B3 X
; q2 |, @5 m# X7 F2 q
首先要熟悉内核已经给我们提供的工具,然后实实在在的去做一些内核功能的开发,在开发的过程中不断熟悉内核代码,增加内核调试的经验。0 q- ]. C1 Y$ y# Y: R
, a- S! r* n9 ] S' C8 r7 ?
: N- Y. O4 L- H I( ~! Y7 ~3 \1 E+ |$ s
主要内容:5 q# g# A6 F' G; t
+ x2 e( E* u- @0 o! S0 G7 g% U内核调试的难点
; H% w1 ^4 P m$ ?# u4 p内核调试的工具和方法
* ~2 \! z6 |- X: k) X总结
& R' R7 f. l$ Y
, Q P. R9 \ r/ {- ~6 B) v: } I
' T% |, }+ g7 P4 O, e' b! I3 w1. 内核调试的难点
+ o) A- Y6 Q2 {9 u; @1 D: u内核调试的难点大致有以下几个:
, K, J0 u. ? w1 ~7 s& h+ Q0 s& E- I7 e+ {! F2 {. t V
重现bug困难 - 如果能够重现一个bug, 相当于成功了一半. (特别是有些bug和硬件相关, 执行几百万次之后才有一次错误)3 T& w+ j; k, a% H
调试风险比较大 - 稍有不慎, 即造成系统崩溃
# O( f K1 `8 d' f) {2 a- u) Z定位bug的初始版本困难 - 内核版本更新很快, 很难确定bug是在那个版本开始出现的# J1 I# X. A1 n8 I' u! o3 ?; h' d7 h2 M
3 A: q: q6 E1 L M+ u8 t9 _
" Q( A: o$ o$ D' d2. 内核调试的工具和方法
8 k8 U& ^; l6 d, {+ g8 o7 k: J% q' l内核调试虽然困难, 但同时也极具挑战性, 如果能够解决一个困扰大家多时的内核bug, 那将会给自己带来极大的成就感. ! m" D; Y9 K: K# Z. g
4 O. Z* R! ^+ t9 r ]/ j/ G4 l& J而且, 随着内核的不断发展, 内核调试的手段和方法也在不断进步, 下面是书中提到的一些常用的调试手段.
$ w& _. |8 s, q. N" V3 h' R$ T! `- R; p0 T* X5 K; E i% v0 t: q* x
S& u) x+ r/ m& v" X5 o
) R2 ^' P( c% X
2.1 输出 LOG
! l% Z0 C: B: I: a- M$ o4 A8 ~输出LOG不光是内核调试, 即使是在用户态程序的调试中, 也是经常使用的一个调试手段./ m# r, [. ?8 C5 M
2 k0 q/ L' V! I! L9 q4 ^& W$ O5 D
通过在可疑的代码周围加上一些LOG输出, 可以准确的了解bug发生前后的一些重要信息.
+ z# M% q3 q: j2 f
% u# f, y( ?# A9 N : ?( i4 E( ^" K' Q; I- X1 P
1 e0 Q4 ^! L8 v4 K& |* c' E' g. j2.1.1 LOG等级
$ `/ u# w, I t2 Z1 K5 o. qlinux内核中输出LOG的函数是 printk (语法和printf几乎雷同, 唯一的区别是printk可以指定日志级别)
3 u! {$ e5 z, O& U: ?. g* T6 Y4 D/ T7 ~2 N' C3 R
printk之所以好用, 就在与它随时都可以被调用, 没有任何限制条件.
( @4 ]% n j+ Z1 Y; W$ n* Y
0 W! S. `9 M/ U0 t3 f/ Vprintk的输出日志级别如下:等级 | 描述 | | KERN_EMERG | 一个紧急情况 | | KERN_ALERT | 一个需要立即被注意到的错误 | | KERN_CRIT | 一个临界情况 | | KERN_ERR | 一个错误 | | KERN_WARNING | 一个警告 | | KERN_NOTICE | 一个普通的, 不过也有可能需要注意的情况 | | KERN_INFO | 一条非正式的消息 | | KERN_DEBUG | 一条调试信息--一般是冗余信息 |
; B# T% |* s% s/ \& g6 E7 q( @( [7 @2 A( t8 y9 ]" @3 U1 l% W' y
3 F$ @$ ~/ j1 @4 f# J: l/ m
输出示例:
' T( \/ n9 H/ o+ k" [7 ]5 q
9 U J0 G8 i" aprintk(KERN_WARNING "This is a warning!\n");4 t; h$ F/ G% Q; |, F' C
printk(KERN_DEBUG "This is a debug notice!\n");, @5 [- h$ T( d
1 u' K E' s; F0 e0 O
{; [! M0 G) j8 ~4 `: z4 c- S' w2.1.2 LOG记录# ]) v: H* P; K5 g
标准linux系统上, printk 输出log之后, 由用户空间的守护进程klogd从缓冲区中读取内核消息, 然后再通过syslogd守护进程将它们保存在系统日志文件中.6 K6 Q( j$ s5 T) B& k
. s8 I" P4 S& Y; gsyslogd 将接受到的所有内核消息添加到一个文件中, 该文件默认为: /var/log/dmesg (系统Centos6.4 x86_64)! E( J7 t# b3 T9 r1 ^9 u
' k, W5 F/ o, O! Q" U9 h
0 P( \" l. O6 P* t( ]0 }/ ?& c# F1 B+ h) ~$ d+ h
PS. 上篇博客中的内核模块的输出LOG, 都可以在 /var/log/dmesg 中看到
( t5 ~3 K: }8 r" L' v1 `: E4 b0 _9 a2 G, w& x H% s4 {" @
7 A$ I+ ?) @! s- t5 o& X
0 s; z: I9 A$ [5 N
2.2 oops0 X9 D- P$ k; Q& l. \: u
oopss是个拟声词, 类似 "哎哟" 的意思. 它是内核通知用户有不幸发生的最常用方式.
6 A+ B& R" Z2 r% i4 j' G% v( b
9 [1 g4 P& T5 R1 g触发一个oops很简单, 其实只要在上篇博客中的那些内核模块示例中随便找一个, 里面加上一段给未初始化的指针赋值的代码, 就能触发一个oops0 |5 K9 j' z# |" P* W- x4 ~
4 x( F* l8 D% M
8 X+ V/ F- ]; f& f7 U& o+ p! O
. q+ C. W6 o6 Z* ?2 o& M" ~6 B2 yoops中包含错误发生时的一些重要信息(比如, 寄存器上下文和回溯线索), 对调试bug很有帮助!, F+ z3 O0 ]! H9 m( d! u1 a8 G
! H9 q, r; g* c/ T; G; r
! L7 A' a* \6 @1 m. U& N$ Z
% r- ^' c4 R0 ^6 T) h. b6 v! q
调试内核时, 还可以开启内核编译参数中的各种和内核调试相关的选项, 那样还可以给我们提供内核崩溃时的一些额外信息.' l) w; F9 e) V2 Z# B& T
! @' D H r; \+ m
$ |9 U5 f; e0 K; ~* \2 {
& z% [1 a& H4 {. T# Y2.3 主动触发bug7 m6 y3 Q; `; E; B' F
+ R7 q f' z% n6 @3 }调试中有时将某些情况下标记为bug, 执行到这些情况时, 提供断言并输出信息.* V$ L9 z: i9 U' O& d
2 D0 L) T1 }( d$ ]
BUG 和 BUG_ON 就是2个可以主动触发oops的内核调用.
( X- Q I6 l$ A% V: P* v
4 H5 ]; P% D9 f1 H7 @% l+ Z& v
5 k5 ?4 `# F3 r( \- s
5 F" Q6 L( X7 Q" g7 N在不应该被执行到的地方使用 BUG 或者 BUG_ON 来捕获.: w1 |2 n& j, l8 O7 u% k9 ^4 K7 L
M+ m2 W) [7 ~6 J/ z
比如:, I" E& t$ F, F6 R+ {
& T/ Z. M% d& a3 s9 N& dif (bad_thing)+ k/ d8 B, N( J: b
BUG();
y6 [2 e: _8 j// 或者* d3 [1 I% s+ V" P3 y" P8 H, u
BUG_ON(bad_thing);
& P$ i' F+ F% O) a+ Q
/ B+ I! H7 x/ P8 }; ?* r' Q7 N5 h4 Y
如果想要触发更为严重的错误, 可以使用 panic() 函数. g6 [/ i T7 {0 w: R
/ P( d5 V3 M N4 X: O+ n$ z! m5 ~
比如:& O% f+ y: c$ V( r; `8 E
& N: j( w k7 b4 g
if (terrible_thing)
2 |& y( ~. _5 N/ j9 j panic("terrible thing is %ld\n", terrible_thing);
0 K, ~3 Q! n. }2 u% a) S , ]' ^ H7 `$ m( Z( p- N
/ Y, e: A W/ [& ]
此外, 还有dump_stack 函数可以打印寄存器上下文和回溯信息.1 r( s* [8 G C3 N& U8 C. q, A' C2 B
& R. X) n5 |5 F% z1 N比如: _: K" T) @3 d0 Z& u# r. {) K
* Q" a7 U- G9 @if (!debug_check) {- C2 r% @. }; K0 T% W: i+ m8 s: S
printk(KERN_DEBUG "provide some information...\n");, x0 c: d* @+ Z/ C% }1 d6 r) B5 U1 A
dump_statck();0 L6 ~* X) X: U7 S# m
}
: M+ A3 d4 b; b , K- u4 r, h- s7 d
! ~& p, D: r9 a' y+ ~( a/ z
2.4 神奇的系统请求键) G, a$ j# t/ ~# I# J: R' A
& p2 ]8 m6 U& l' o1 ?* [4 f7 X" R这个系统请求键之所以神奇, 在于它可以在一个快挂了的系统上输出一些有用的信息.
7 ^1 t4 o1 I8 S- ?1 J3 W+ x+ r4 _- r- a$ w9 y: m
这个按键一般就是标准键盘上的 [SysRq] 键 (就在 F12 键右边, 其实就是windows中截整个屏幕的按键)
; I0 y2 A5 N, k% r' M; O4 z* e3 u- P- j
单独按那个键相当于截屏, 按住 ALT + [SysRq] = [SysRq]的功能5 p5 N. m3 H1 j8 ]; X1 }
' ^3 t M( a7 N0 r) ~: }
" w1 A$ g! h4 [4 p' T" T0 X" G8 S9 [+ d
启用这个键的功能有2个方法:" ~8 ~2 K% o) q5 ]( @
/ C6 M) h! Y( x3 _: t
- 开启内核编译选项 : CONFIG_MAGIC_SYSRQ
- 动态启用: echo 1 > /proc/sys/kernel/sysrq) z$ r( t2 T$ E0 g$ l
9 [6 ~3 j0 T5 _6 [5 {4 z7 K
' u- L2 Z$ {" I; h H支持 SysRq 的命令如下: (注意要在控制台界面下使用这个键, 比如通过 ALT+CTRL+F2 进入一个控制台界面)6 f8 V5 t( U1 g! I
) }- A. Q7 _# D5 j2 L
主要命令 | 描述 | | 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 | 卸载所有已加载文件系统 |
m+ M/ s4 r( @$ ^& \. I
5 i' g( q$ G( d0 j; l) y
1 W" [- `1 t4 P) B. M1 F& d/ N2.5 内核调试器 gdb和kgdb
# c3 v0 i- S( W0 i/ O
% w: V0 m2 c3 V8 Olinux内核的调试器可以使用 gdb或者kgdb, 配置比较麻烦, 准备实际用调试的时候再去试试效果如何..
3 V' V& |. k: m' z0 F; d6 U- [% m
6 U6 H; g1 z6 c6 m- s/ f
6 J' s5 C2 Z' q9 j/ |2.6 探测系统 e- a/ J% {! W ]7 X7 l8 [5 X$ a
' T+ i- G3 n5 i下面一些方法是在修改内核后, 用来试探内核反应的小技巧.
' K+ t$ M0 }1 r# h# _- W5 c9 @$ V; T9 v, V
5 n1 N1 z9 I9 W, _) j) O+ c
2.6.1 用UID控制内核执行
$ a: [' {: j! p$ P! N4 S; n
1 C& I4 U8 R* i/ V比如在内核中加入了新的特性, 为了测试特性, 可以用UID来控制内核是否执行新特性.
- F$ `/ u3 l0 }& o
: U& M) o6 |( i4 }if (current->uid != 7777) {
6 i9 R0 R7 l d' D /* 原先的代码 */
; q3 b' J& c: i; X5 `( j} else {3 v W" ?$ }$ r `
/* 新的特性 */
% b+ _2 m- R- w" a, {4 E}$ w+ J+ `, c) w: E: B# ^9 K
: u7 e; J# i6 a0 \' k
; v, F$ o: @& M9 W9 I' c2.6.2 用条件变量控制内核执行/ V! G" q7 ^; ?- K
6 x# o5 f; l+ Q- d$ q, W
也可以设置一些条件变量来控制内核是否执行某段代码.
, g" ] p6 w% L k. O
6 E/ Y6 G2 a; P* E' ~" M4 ]( ~% S条件变量可以像上篇博客中那样, 设置在 sys 文件系统的某个文件中. 当文件中的值变化时, 通知内核执行相应的代码.
* W" x, ^9 [# [& y. U6 a2 R g+ t! W% Z
8 `7 `$ r, C, L* n. T/ J: a
" y# i: i$ D! j+ @- L* j* @* b2.6.3 使用统计量观察内核执行某段代码的频率! F+ v$ j0 f+ q6 E/ d
; } R. ~# B4 A6 F- r
实现思路就是在内核中的设置一个全局变量, 比如 my_count, 当内核执行到某段代码时, 给 my_count + 1 就行.$ c& o! e9 ^% ?0 V8 o8 }. l% y
4 y/ i5 V7 ?8 i. x. r
同时还要将 my_count 打印出来(可以用printk), 便于随时查看它的值.
3 C, g3 ~* q) e r
( u' N' j( ]$ _0 G* C" ^ 5 b; R+ O5 `3 U7 s# u1 D5 E9 [2 a
& s5 a5 W6 n0 T3 {. ]2 u) F
2.6.4 控制内核执行某段代码的频率% L# M! }6 |+ R+ O3 X* t$ K4 I+ o
% V; _- ]# a4 c/ |& S有时侯, 我们需要在内核发生错误时打印错误相关的信息, 如果这个错误不会导致内核崩溃, 并且这个错误每秒会发生几百次甚至更多.
7 {* D( @0 F+ R/ H
: z; L7 p4 E) a那么, 用printk输出的信息会非常多, 给系统造成额外的负担.6 [& [6 ?% B; u R4 p, R+ D
+ h. G" p" A+ p( U1 S这时, 我们就需要想办法控制错误输出的频率, 有2种方法:
: Y& m6 h5 L. L- \6 e$ T, D2 n d) T3 m X* I
方法1: 隔一段时间才输出一次错误
4 R% s3 u; s! T; D" U! E9 u, t* v: ^
static unsigned long prev_jiffy = jiffies; /* 频率限制 */
$ ~6 u( S% r8 ?$ E, W
3 q; S4 q- h- p0 {( tif (time_after(jiffies, prev_jiffy + 2*HZ)) { /* 输出间隔至少 2HZ */
" F+ I0 J% ]- H4 n prev_jiffy = jiffies;
$ Z* J6 x2 S* y4 R; z( z printk(KERN_ERR "错误信息....\n");$ g7 h$ f3 x- ^: K
}# u+ B6 w4 }( s# @$ g0 j! V) w' h- Z; L
$ v. Z! i9 i: b( l
7 p# t4 X+ U O4 e3 V2 ?方法2: 输出 N 次之后, 不再输出(N是正整数)
' X; a0 G c, _6 g& g5 r
V, F) k2 X, f1 j- t1 F1 x1 i( mstatic unsigned long limit = 0;
% J: m" c! w0 s2 P) |- ^
( r9 ]1 x1 j0 pif (limit < 5) { /* 输出5次错误信息后就不再输出 */
0 A0 B8 ?1 `9 {" [( V limit++;- y! J& E9 D& f' }7 g2 d' ^5 s
printk(KERN_ERR "错误信息....\n");
! F7 Z F. f9 D) d& R! f2 S}
8 \1 s8 r4 f, I" G7 k! |6 ~/ H4 y ! e# Q6 i; W1 V( S' Z$ r0 k
, i0 s* c/ j; }5 X( d: c; ^+ a( t$ P1 d. L/ \
2.7 二分法查找bug发生的最初内核版本! s" Y7 Z, h3 s* `" n
7 [% s7 ~% ?* y) ]$ L; Q在内核发生了bug之后, 如果能够知道是bug从哪个内核版本开始出现的, 那对修正这个bug会有很大的帮助./ }3 a% m6 H3 y1 T( o
8 i- |/ J0 m. D' {$ ~
由于内核代码非常庞大, 即使用二分查找法, 手工去找哪个版本开始出现bug的话, 仍然是非常耗时和繁琐的.
% v0 `8 i* Q+ f/ ]; J9 [( L' C8 X5 y
' Y9 H1 s4 @. H ! w' R! b8 @" {: k( T# c# I
z( {( d. M5 |+ ~# W
好在 Git 给我们提供了一个非常有用的二分搜索机制.6 v+ L( b( S- w: Z" S% s
H* N/ W5 H2 T' `+ tgit bisect start # 开始二分搜索
4 P# Y; D! ~4 u: I$ {6 e/ dgit bisect bad <bad_revision> # 指定一个bug出现的内核版本号' n" Y+ d: [( b$ h' ^. c
git bisect good <good_revision> # 指定一个没有bug的内核版本号, 此时git会检测2个版本直接的隐患
8 I+ n7 s6 `' `7 F
* Q8 V( I& S2 N" \7 d P# 根据结果再次设置 bad 和 good 的版本号, 缩小Git检索范围, 直至找到可疑之处为止.7 V" I s( L9 ^0 {8 K& o( s
, K/ h) p" B& o @) }
- p/ J7 }2 Y; _& `) p; K+ i/ S* s5 J2 [0 K B
2.8 社区: B- h/ U" u: x5 i* u# h
?( o& @$ @$ P3 M0 E- C2 E
当你在调试bug时用尽了一切手段仍然无济于事时, 可以考虑求助linux社区, 求助时注意一定要描述清楚bug的状况.- w/ M( J: m8 d" A
7 F- d9 c5 n) I% h: c3 M7 [" F, {(可以参考一下别人汇报bug的格式)
' M- u% t8 j9 N4 q" S2 L- U
# j& O/ T: q: M 0 ?6 Z( ]6 v5 n: u2 ~( o
) T. u" g: h' Y' X9 w9 Q' p" f' o; m- R# M
3. 总结
! ?' w8 V" r: W. N
% Q3 W" {. p! q0 h$ V+ elinux内核调试必须要依靠大量的实践来掌握, 仅仅靠上面介绍的一些技巧还远远不够, 只有实实在在的去阅读内核代码, 实实在在的去修正一个个bug, 才算真正掌握内核, 真正了解内核.9 D/ n: g, r8 i' e' ]9 t0 l; `/ A* M
: t9 I: @% }% H, q$ E多看看之前linux内核bug的修正案例, 也是个不错的积累经验的方法.
" N' t; Z+ W' F) U A3 S* D. e( r% }& G, A
3 s5 p- A- H4 ~0 p+ i( N5 @# J
+ ^9 v4 k: g3 {$ U
PS.
0 B3 A. e! J& l0 N% x3 B0 w
, p- R8 k1 I3 W9 H9 Q; o对于初学者来说, 在真机上做内核开发动辄导致机器崩溃(panic), 非常麻烦. 现在的虚拟机这么强大, 建议都在虚拟机上测试linux内核修改的效果./ R8 T0 r6 Z) b: e+ P% m1 |9 k" F' U+ R
+ |' @6 x$ F; k4 I V p2 Q' U我之前的关于<<Linux内核设计与实现>>笔记的博客中的代码都是在虚拟机上运行测试的.
W; h' B: Z* j |
|