EDA365电子论坛网

标题: Linux内核设计与实现之内核调试 [打印本页]

作者: pulbieup    时间: 2021-3-9 09:42
标题: Linux内核设计与实现之内核调试
本帖最后由 pulbieup 于 2021-3-9 09:50 编辑
* z* ^. f! a) ^( h8 l( |% \! F
2 x# a( R( w" l) D; D内核调试的难点在于它不能像用户态程序调试那样打断点,随时暂停查看各个变量的状态。
# u% ?' H" Q8 J& U5 B; x$ Z+ U8 C3 s  u9 J. t
也不能像用户态程序那样崩溃后迅速的重启,恢复初始状态。
) Z1 x) }7 I* E7 M: A( q. u/ K7 M9 @) G
: e  y% Z9 d+ |4 @7 A# Y) C
5 Y8 d6 [9 U, j& [0 t4 K
用户态程序和内核交互,用户态程序的各种状态,错误等可以由内核来捕获并显示。
5 h: @  r' ?0 t+ p) i6 R, X; v! c, d) p+ ~0 d
而内核是直接和硬件交互的,内核出错之后整个系统就无法正常运行了,所以要想熟练的进行内核调试,6 M2 N2 [1 R4 ~4 J+ v+ k

/ J1 V* n2 N& u6 X& m8 c$ B首先要熟悉内核已经给我们提供的工具,然后实实在在的去做一些内核功能的开发,在开发的过程中不断熟悉内核代码,增加内核调试的经验。8 t; X8 v- L5 u2 E2 L

% a; d8 O0 y% } 6 `; W  z* m) a
, y. L9 X/ F  |" t2 s: J
主要内容:, P5 q+ O; R& H7 g; r- p9 f5 D) g
) Z5 \& A/ v3 D' s6 t( j1 Q  A# o$ f
内核调试的难点
( w: _* A9 O( \2 H& {5 ^4 @2 F) Q内核调试的工具和方法
5 p" Y+ K% e. _( ]8 n总结
  u5 \* u' ?3 I/ m- r  C, B ' q7 S% E0 v0 L2 F4 q! m1 \9 s; b
, W) p/ m+ F# A) f3 \
1. 内核调试的难点
2 [, e- Y! K8 P, z内核调试的难点大致有以下几个:
3 ^6 m# ~( h/ W; O9 j8 e% y
5 \2 f2 @% r+ q: _重现bug困难 - 如果能够重现一个bug, 相当于成功了一半. (特别是有些bug和硬件相关, 执行几百万次之后才有一次错误)9 U& w/ l1 g# I, i. k+ W+ S
调试风险比较大 - 稍有不慎, 即造成系统崩溃& y( i, k+ W3 P( X: O
定位bug的初始版本困难 - 内核版本更新很快, 很难确定bug是在那个版本开始出现的
1 u# o* J3 _2 S' m2 O6 Z3 l6 f
/ [+ h4 l( v8 e: \, V7 G/ R. Q9 i/ F4 z+ j
2. 内核调试的工具和方法
8 ~/ \. k8 i) M7 r$ j/ p内核调试虽然困难, 但同时也极具挑战性, 如果能够解决一个困扰大家多时的内核bug, 那将会给自己带来极大的成就感. , X( N- E3 P/ r% U, F
! R8 S" B8 b, W# R+ K& ]9 z9 @
而且, 随着内核的不断发展, 内核调试的手段和方法也在不断进步, 下面是书中提到的一些常用的调试手段.
& }$ j3 `% t& ?9 j  \: a0 E+ r. I7 L2 e4 o$ I' m7 p' C( ]2 g" I

0 [- L3 L. h) X, |5 T  T" ^. w6 @* k
$ Y& M7 {* b: I0 t( e  A  a, `2.1 输出 LOG
+ z, p9 M9 h$ ^9 i" M输出LOG不光是内核调试, 即使是在用户态程序的调试中, 也是经常使用的一个调试手段.
9 ~8 `! R' K4 Y1 \- n- i: g. H& [
% \$ r) l  C$ C+ O, e通过在可疑的代码周围加上一些LOG输出, 可以准确的了解bug发生前后的一些重要信息.
) h8 ^; y5 Z4 [; j
: n, a3 P2 D4 ?1 f* x9 o2 k$ ^ ' L/ i4 O0 c9 B. V
- c- B# e" k5 n9 e0 A7 @1 ~) L
2.1.1 LOG等级5 [1 S/ `+ r. q9 j2 I% `
linux内核中输出LOG的函数是 printk (语法和printf几乎雷同, 唯一的区别是printk可以指定日志级别)! G& ?4 O. f* w$ z

" L! S1 v5 D! V" L& Jprintk之所以好用, 就在与它随时都可以被调用, 没有任何限制条件.6 R+ ?0 c4 p: R5 g+ E2 v

0 z) R* g) R. {5 E/ j" j8 c" j# `5 Z  Sprintk的输出日志级别如下:
等级
描述
KERN_EMERG一个紧急情况
KERN_ALERT一个需要立即被注意到的错误
KERN_CRIT一个临界情况
KERN_ERR一个错误
KERN_WARNING一个警告
KERN_NOTICE一个普通的, 不过也有可能需要注意的情况
KERN_INFO一条非正式的消息
KERN_DEBUG一条调试信息--一般是冗余信息

" B7 z4 d3 K3 ?& }( Y* d% J. D/ w
$ ^5 t! y7 Z& C. `# {/ m+ Y3 j2 S' b. b5 d9 v
输出示例:$ n$ v* B: ]  [- D. V2 }$ Y8 K

# g; z0 x- ^. {, P3 W; m7 |8 sprintk(KERN_WARNING "This is a warning!\n");
9 l6 p: }5 `/ E6 dprintk(KERN_DEBUG "This is a debug notice!\n");
: ]0 S4 X+ Z% q
. V! Z) j5 J# y- Y0 {  t* b
" D5 D/ b* ^4 n. R0 _$ ]+ n9 t$ U2.1.2 LOG记录; B% `: z$ o1 J4 n
标准linux系统上, printk 输出log之后, 由用户空间的守护进程klogd从缓冲区中读取内核消息, 然后再通过syslogd守护进程将它们保存在系统日志文件中.
6 [; r- j. U. n; F" p
. }: M5 }5 ~3 V  w9 G! ~syslogd 将接受到的所有内核消息添加到一个文件中, 该文件默认为: /var/log/dmesg (系统Centos6.4 x86_64)
$ Y8 B: v1 K( m2 p8 N- o  A! i2 C. W$ P/ r) r
6 Y& n. W; j, X' {) a- k: y
! K5 D" T2 I3 N/ x( u
PS. 上篇博客中的内核模块的输出LOG, 都可以在 /var/log/dmesg 中看到4 P1 i) S8 D( e! n8 ^! \

: i$ @( Q* H/ I5 t: q; o) t' q
$ B# [. j% k; |% p
/ F7 W5 m# K' v* |# v: @2.2 oops
0 c: Q* z/ D. O+ a( s3 Ooopss是个拟声词, 类似 "哎哟" 的意思. 它是内核通知用户有不幸发生的最常用方式.
; ^% G" N# i: c8 Q( y8 r4 c' g% N! \2 o- m& D' [
触发一个oops很简单, 其实只要在上篇博客中的那些内核模块示例中随便找一个, 里面加上一段给未初始化的指针赋值的代码, 就能触发一个oops6 n- o5 s* ~8 i! l7 o; ?  n

) B5 A+ Q8 ^9 p' o# s7 ] 2 M2 Q2 R2 e% j

* s, j5 S  R& W' ]1 |# B) g5 w+ X! yoops中包含错误发生时的一些重要信息(比如, 寄存器上下文和回溯线索), 对调试bug很有帮助!- l! j; c" ?3 b. p

- O, @8 b. l* v! V0 }; a
+ N. z4 I9 V3 @( @5 x- n, U9 M" k1 c, @- g
调试内核时, 还可以开启内核编译参数中的各种和内核调试相关的选项, 那样还可以给我们提供内核崩溃时的一些额外信息.; U8 J: o8 {7 H$ A" S
6 }- X5 ]8 Q" c( R
5 I* V6 \6 {. D4 {' s" d5 D7 f
0 f$ G5 g+ j/ s' H) r/ k: ?2 V
2.3 主动触发bug6 |) y! ?4 S% Y! f+ H
+ U/ V+ n2 ]$ a& `) Z
调试中有时将某些情况下标记为bug, 执行到这些情况时, 提供断言并输出信息.* W  ?0 u4 x' R4 ?8 c

6 U- }, i4 T- |. n/ ]+ i# \BUG 和 BUG_ON 就是2个可以主动触发oops的内核调用.) ]" ^/ }% U1 |' r1 C

0 C6 t; _- j/ Q" D2 Q
; h% E& v( o& ~: f) t9 h' M) i
+ B' F1 W- F9 E" x# U在不应该被执行到的地方使用 BUG 或者 BUG_ON 来捕获.* O" t8 g" k6 c

3 ~1 z; M/ y) ~, i7 w$ M0 _2 c比如:
! n) X3 B! r2 {7 f, H6 l9 O( i/ q+ ]! M6 o/ t
if (bad_thing)# n2 K2 U0 M0 Y5 M* m
    BUG();' x+ o7 a/ y. H  B) A+ a. o% a  f
// 或者
! k' y. [$ f. E$ wBUG_ON(bad_thing);
. w" k7 @: a1 f5 |( D& R! Q$ D9 d, p 8 ^9 b  p7 i# @" C
0 s& S/ l6 z0 E) l0 B! ]8 U, ^
如果想要触发更为严重的错误, 可以使用 panic() 函数
- W# b2 y0 q) ~- d. F$ s" g
, E: ?) _8 W4 a; C比如:
9 H! t8 H* D. D4 `
: e0 a' b2 d( h- l/ vif (terrible_thing): S5 n8 b, n9 s/ T" Q
    panic("terrible thing is %ld\n", terrible_thing);% E' s! f( P! \$ e
( n' ~/ r; \4 l7 ^% \. m0 y
/ |) `- ~& b. T: z$ ~9 c
此外, 还有dump_stack 函数可以打印寄存器上下文和回溯信息.- V( d' S9 E+ g

. b4 a' I; k7 M6 ^! ~, Z# h比如:$ C" O! W4 y; m3 h  I1 |! |+ T

. e4 H% q! A/ I. F3 Nif (!debug_check) {9 X2 x+ V  c8 f5 k# ?
    printk(KERN_DEBUG "provide some information...\n");, @1 p6 @1 S) W( d) T/ C
    dump_statck();
0 [8 F" C! i3 e, x$ ^) r}7 `% N( ]8 W! [2 \! S/ Z4 Q* l

8 }- n  d: [( b# d" x7 @% b3 u
2.4 神奇的系统请求键
$ a, |. ]  o! i# l! V) b' y+ [) ^$ E3 a# b
这个系统请求键之所以神奇, 在于它可以在一个快挂了的系统上输出一些有用的信息.- r; {9 q" [* x5 U8 z1 T
* [9 @8 T: Y" s4 c" W. s
这个按键一般就是标准键盘上的 [SysRq] 键 (就在 F12 键右边, 其实就是windows中截整个屏幕的按键)) V+ Y- x' v1 W, o) H4 d
/ `/ m! v/ M5 s
单独按那个键相当于截屏, 按住 ALT + [SysRq] = [SysRq]的功能+ F( ?% K4 R4 k
- G$ l# l4 ^, q0 G* j9 S
% @+ H, U& i4 r5 O+ A
1 y6 ~' @, k2 \! n& O
启用这个键的功能有2个方法:$ U7 |8 ^- N2 ]* h! T3 F' x

* E6 R7 V1 W9 x( F% c& `. { 3 t0 B' B2 ~/ e3 {
" w* l1 j4 D' Z' i5 ^
支持 SysRq 的命令如下: (注意要在控制台界面下使用这个键, 比如通过 ALT+CTRL+F2 进入一个控制台界面)
+ A  O- j$ ~3 q+ K5 @( N6 o$ ?2 _' @; h5 l( i  ~( E0 I8 g* X+ Y, M  s
主要命令
描述
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卸载所有已加载文件系统

! o4 S, v% N  d. a
3 ]' A0 Q! D' q. Q
9 h( a1 b) p) o! m5 ]2 ~2.5 内核调试器 gdb和kgdb8 f$ d; D: m2 G+ |2 p
  H5 S' ?# M+ J& }0 y% n2 N9 ]
linux内核的调试器可以使用 gdb或者kgdb, 配置比较麻烦, 准备实际用调试的时候再去试试效果如何..  Q' d$ Z/ t, I. l9 k2 e$ G/ Z
0 P4 [! \+ `# L4 G- y% ]* |
8 ~7 S" b( i7 c" I
7 ?$ T+ I' C7 S0 J6 s# M
2.6 探测系统
$ j! }9 L/ M& p/ W6 m/ F$ [' `2 Q/ n: e. E# P( y6 Q
下面一些方法是在修改内核后, 用来试探内核反应的小技巧.
% E" G- \* J) ]6 N+ [3 h* ~: `
' e. u  P) M7 l) h
, s0 h' a2 H$ B# D- c2.6.1 用UID控制内核执行
! c" ~' ~, N: O$ Y* d7 W
3 C8 }0 a, h) ]" }, `5 s: |比如在内核中加入了新的特性, 为了测试特性, 可以用UID来控制内核是否执行新特性.! f6 Z4 g- c4 j! |. X7 a3 v7 M

& [) f- z8 N$ Y( f) Y: }7 Lif (current->uid != 7777) {) l' K* |6 N0 {
    /* 原先的代码 */3 g/ q6 }( K3 u& ]: k' Y, A! ^
} else {
" Z8 h9 ]( p" X9 U" o) p   /* 新的特性 */6 u+ {. S1 j5 z6 s9 ~
}/ t; _( ^% Y7 n; D& T

2 d" z3 g! p0 F; j9 U  L+ h: G1 _  f$ Q
2.6.2 用条件变量控制内核执行; N/ R' M+ c5 i9 B8 E
9 ?2 i- S- B3 [# o
也可以设置一些条件变量来控制内核是否执行某段代码.: x( X- V& x) `
* p4 w7 c" j) c6 L
条件变量可以像上篇博客中那样, 设置在 sys 文件系统的某个文件中. 当文件中的值变化时, 通知内核执行相应的代码.
* p* B9 T) b+ H2 T. u0 s& q
' ^5 w" B% v/ J! b' ?* y
& ~! [4 Y7 h% @7 Z
" S, j( ?# i: z- f1 {( f' P( e2.6.3 使用统计量观察内核执行某段代码的频率6 k! n' X; ^' e! S

$ M6 k$ Z0 ~- z& i实现思路就是在内核中的设置一个全局变量, 比如 my_count, 当内核执行到某段代码时, 给 my_count + 1 就行.
! U4 H" ]# {. F# W: m& }0 b: {1 k. g1 ^$ e! a2 ^# O
同时还要将 my_count 打印出来(可以用printk), 便于随时查看它的值.7 M& o* ?: i) J# M; _2 ]% Y/ F
% A: `$ Z& c8 O% q! {  c+ A) j
4 j' V# y4 d: u3 j7 A( ~0 `3 [
" B+ q* C2 `9 E" `* F4 W9 K
2.6.4 控制内核执行某段代码的频率, u( z' j5 N6 o
- @. P5 ^  ~' a
有时侯, 我们需要在内核发生错误时打印错误相关的信息, 如果这个错误不会导致内核崩溃, 并且这个错误每秒会发生几百次甚至更多.+ C3 g: ?, o( y- x

  E$ U4 i1 }' x3 S/ j9 B8 B那么, 用printk输出的信息会非常多, 给系统造成额外的负担.0 \2 l# K; m! u9 c* V

, t3 }1 l. G3 l9 i2 c. A9 \* c这时, 我们就需要想办法控制错误输出的频率, 有2种方法:
% Z# y# I; h2 ~. A! h% r2 u  j7 P6 Q4 `
方法1: 隔一段时间才输出一次错误
  M" R1 ?- q: T& i; H5 r& c
1 Z( U$ K" T( P& x, sstatic unsigned long prev_jiffy = jiffies;  /* 频率限制 */7 k! x8 X7 }3 \3 D
9 d- Q2 F& o' q- |4 q7 A+ d. @) C3 g
if (time_after(jiffies, prev_jiffy + 2*HZ)) {  /* 输出间隔至少 2HZ */) U- B. e  K1 _
    prev_jiffy = jiffies;
$ |' B) Q1 @, U5 `& z    printk(KERN_ERR "错误信息....\n");. f1 m9 S$ h8 W& }( i
}
# s" ?% V" P7 r- s3 w2 m8 z
/ l- b1 v! A7 t0 A
7 e- P* }3 G9 [; T( v* M方法2: 输出 N 次之后, 不再输出(N是正整数)4 k  A/ m% r3 l/ [; W, E" G, g
9 ?' G" c* e7 @  S) o7 }! B, |
static unsigned long limit = 0;9 C% _8 i/ e4 ^/ N* C  J: E6 p

$ _$ a6 r0 a3 {' o0 Xif (limit < 5) { /* 输出5次错误信息后就不再输出 */
& [  o: Z. H$ t    limit++;
6 i* ~+ h9 _2 c7 U    printk(KERN_ERR "错误信息....\n");
: h9 d% s( A' S& X) Q}
7 h- O/ {' K9 |: s
3 ?. G1 U8 d0 m" m; w3 |. D
' j9 X, c/ H% L/ R2 i
. O1 y, l! h# f3 R2.7 二分法查找bug发生的最初内核版本
( H7 g# d; T2 A: Q5 u, w) G$ k+ D0 K* L
在内核发生了bug之后, 如果能够知道是bug从哪个内核版本开始出现的, 那对修正这个bug会有很大的帮助.- d7 W( H# R# J4 r! v
7 }! S$ E5 n  _- S5 S. Q
由于内核代码非常庞大, 即使用二分查找法, 手工去找哪个版本开始出现bug的话, 仍然是非常耗时和繁琐的.
, W+ x6 j$ _. {* N4 o& O
- x- N( d5 H4 f  f5 w 3 c6 C; g! M1 P8 j# D; @7 B

& X7 @9 ^. f. ~, g' T& L5 @好在 Git 给我们提供了一个非常有用的二分搜索机制.' q# |5 M8 D' S0 F

  l6 H2 b6 C! y# Vgit bisect start  # 开始二分搜索
% k. {& c# d1 }2 Z! r& ^+ P% _git bisect bad <bad_revision> # 指定一个bug出现的内核版本号: O' K2 h# B) R) t2 S1 z! |" b
git bisect good <good_revision> # 指定一个没有bug的内核版本号, 此时git会检测2个版本直接的隐患
0 q+ o+ B6 i7 {# A2 u- X! W% w! h) Z8 a! J5 x, `, l5 F7 z
# 根据结果再次设置 bad 和 good 的版本号, 缩小Git检索范围, 直至找到可疑之处为止.
3 _: c1 i( b9 e5 C$ `  W7 A ) f: M7 n- r" H1 g

7 Q6 H5 @/ K# Q" L4 S& o  f9 W) i3 t9 J- J2 U. s$ p* q
2.8 社区5 Q7 W& U3 R0 H2 j6 V, N5 ~1 ]' ]+ g
: h6 ?: l0 E7 X
当你在调试bug时用尽了一切手段仍然无济于事时, 可以考虑求助linux社区, 求助时注意一定要描述清楚bug的状况.
3 A! G3 ]8 Q9 M+ v/ W
& p4 D( ^# C; t2 t2 W(可以参考一下别人汇报bug的格式), Z5 O3 B3 K& O6 d

% Y; k9 P: Z* m4 w9 @- Z
+ K0 }7 ?* h+ `6 c, H# C, x0 J  r2 N" L( o
7 D8 ~' b9 ]+ |7 Y4 W' {
3. 总结" `( M. |# f. Z. ?; ]9 S+ ^

  Z( y* c9 m9 V, y* p3 y1 L2 Blinux内核调试必须要依靠大量的实践来掌握, 仅仅靠上面介绍的一些技巧还远远不够, 只有实实在在的去阅读内核代码, 实实在在的去修正一个个bug, 才算真正掌握内核, 真正了解内核.
! f5 |4 Y1 `( J, z: `( J3 T: V3 Y
多看看之前linux内核bug的修正案例, 也是个不错的积累经验的方法.
5 v2 Z) C8 R0 X) |' i2 X) e9 i( u6 ?! q
' j. U1 y! U, }9 n" M9 ]- Z

6 K' B, B  N6 }  r1 m3 I$ S) HPS.
6 T+ \+ l0 J9 N  B
6 [2 I7 a! u( N! x6 i0 w对于初学者来说, 在真机上做内核开发动辄导致机器崩溃(panic), 非常麻烦. 现在的虚拟机这么强大, 建议都在虚拟机上测试linux内核修改的效果.
5 M- h* p: L" R0 q
3 G3 s4 z$ `/ P% J; ~我之前的关于<<Linux内核设计与实现>>笔记的博客中的代码都是在虚拟机上运行测试的.
  q- U/ g% d9 a' O) \
作者: CCxiaom    时间: 2021-3-9 10:44
Linux内核设计与实现之内核调试




欢迎光临 EDA365电子论坛网 (https://bbs.eda365.com/) Powered by Discuz! X3.2