找回密码
 注册
关于网站域名变更的通知
查看: 424|回复: 1
打印 上一主题 下一主题

Linux内核设计与实现之内核调试

[复制链接]

该用户从未签到

跳转到指定楼层
1#
发表于 2021-3-9 09:42 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

EDA365欢迎您登录!

您需要 登录 才可以下载或查看,没有帐号?注册

x
本帖最后由 pulbieup 于 2021-3-9 09:50 编辑 5 M. c; }& @% U  X! h% i

: J. f1 D, R0 s+ X6 y# q& n内核调试的难点在于它不能像用户态程序调试那样打断点,随时暂停查看各个变量的状态。
: [/ N! t9 R8 W9 U2 ?/ d3 `& S4 R* t: `
也不能像用户态程序那样崩溃后迅速的重启,恢复初始状态。
! s8 t* [) h3 K% ~0 V7 H' l, c+ M* W6 K, K
6 B& S  e2 h7 T- u6 ?4 q' j
( J, U" i: E; `) `( @0 G2 ~# R
用户态程序和内核交互,用户态程序的各种状态,错误等可以由内核来捕获并显示。
* b: h" i% E! x% \; s, x; v
  i0 F1 Q, M# o+ V而内核是直接和硬件交互的,内核出错之后整个系统就无法正常运行了,所以要想熟练的进行内核调试,  g% b4 F4 S( ~1 K

; \$ L  n1 g7 P. a! Q9 z, t7 Z; ^- t首先要熟悉内核已经给我们提供的工具,然后实实在在的去做一些内核功能的开发,在开发的过程中不断熟悉内核代码,增加内核调试的经验。
* K0 {# ~, C: W& ~2 {% t' T) j1 v2 J9 Q- u$ K" u
! s- _- h5 o) e* q& b

) h( u, c0 r1 X: W0 |! C- z3 K主要内容:
2 [. ?  U) j3 `$ }2 i6 @
" e7 I* F6 l/ J/ X9 @" U0 O内核调试的难点! L5 Q7 U- V+ A8 P; }$ k" v0 U
内核调试的工具和方法$ R: W; Q: _. d0 z4 T. x
总结3 C2 F) b+ ?! t3 u" z3 }* U

) Y* f' x  I2 F+ z5 C* J; Y0 R
5 l! u2 f7 W0 K+ P( l1. 内核调试的难点4 H  R  ^. Z2 q
内核调试的难点大致有以下几个:
. v' b- b% u2 S7 G) B" d+ l9 E" n* ^  n0 ]: @
重现bug困难 - 如果能够重现一个bug, 相当于成功了一半. (特别是有些bug和硬件相关, 执行几百万次之后才有一次错误)
0 ~) ?- `& t1 ~* Z: ^* _调试风险比较大 - 稍有不慎, 即造成系统崩溃
& }# I  G2 ?: R: `" a) A  Q定位bug的初始版本困难 - 内核版本更新很快, 很难确定bug是在那个版本开始出现的' J' R' i0 Q4 m" v" c- A! \9 O

, q8 r3 l6 ~; W6 i- I
9 R, G# ?( O5 Z6 Z$ |: Y6 v2. 内核调试的工具和方法
9 }# Y5 z+ I( r内核调试虽然困难, 但同时也极具挑战性, 如果能够解决一个困扰大家多时的内核bug, 那将会给自己带来极大的成就感. 1 g& f8 P% a1 ?4 p
: H/ J8 ], a; Q0 i3 |, Y/ e; i, F
而且, 随着内核的不断发展, 内核调试的手段和方法也在不断进步, 下面是书中提到的一些常用的调试手段.
; t( P$ n, }; b- ^( G6 l1 J0 c) F- c7 i* U
/ w: P( C5 t  `6 n7 F; q" }
/ ?" M" n1 {# `0 u* N( \2 B
2.1 输出 LOG1 g& ^! z! `9 s3 Y: B+ M) P6 O/ `& h$ O
输出LOG不光是内核调试, 即使是在用户态程序的调试中, 也是经常使用的一个调试手段.
3 Z2 z  l1 o- G+ _# Q' L
# D2 H& [5 a! l7 m5 O通过在可疑的代码周围加上一些LOG输出, 可以准确的了解bug发生前后的一些重要信息.$ ?; E! c/ Z0 ]

! p8 K. ^2 j$ }& m) w
9 T4 N5 D  b- U4 n7 _8 R3 p! \0 O9 J% i, F7 B
2.1.1 LOG等级& t8 Q8 s7 e$ `
linux内核中输出LOG的函数是 printk (语法和printf几乎雷同, 唯一的区别是printk可以指定日志级别)8 s5 W. c. T% i* h& o: m
; s5 M/ {6 d  ^8 i
printk之所以好用, 就在与它随时都可以被调用, 没有任何限制条件.$ m+ S" M8 X8 s+ q8 y

8 m2 H2 S) c7 [* f  _' |printk的输出日志级别如下:
等级
描述
KERN_EMERG一个紧急情况
KERN_ALERT一个需要立即被注意到的错误
KERN_CRIT一个临界情况
KERN_ERR一个错误
KERN_WARNING一个警告
KERN_NOTICE一个普通的, 不过也有可能需要注意的情况
KERN_INFO一条非正式的消息
KERN_DEBUG一条调试信息--一般是冗余信息
- p1 q  N# d1 Q3 ~7 K, \: y

! S9 k( I2 @8 ], d( S
4 g: F- |* ?( K# H6 S! ^* t) [0 E输出示例:4 G* X# s8 X; H" U) F/ w' ^% w
6 p. {* d" w. b/ N( j& g
printk(KERN_WARNING "This is a warning!\n");
8 g- x: A: U- Q+ G0 a- X! V7 H- ~: Fprintk(KERN_DEBUG "This is a debug notice!\n");
4 y: I4 s2 P. M# a0 L5 l) ^
" H7 r5 u' [, S$ v( o8 E8 e  c- B  E8 U5 H' a% B% M
2.1.2 LOG记录# Y% \7 c) p+ {8 ?, \% |5 C# {
标准linux系统上, printk 输出log之后, 由用户空间的守护进程klogd从缓冲区中读取内核消息, 然后再通过syslogd守护进程将它们保存在系统日志文件中.
, n, g7 H% p# ~7 f& H$ K
+ ~# [* r2 Q/ j( usyslogd 将接受到的所有内核消息添加到一个文件中, 该文件默认为: /var/log/dmesg (系统Centos6.4 x86_64)
( {3 @) ^% M" ~" K9 ]6 U2 j) M5 D- ~

4 W7 e1 L2 Z# S. c+ A1 |4 G+ C, }! i; v$ t% c( Q8 |8 d' P
PS. 上篇博客中的内核模块的输出LOG, 都可以在 /var/log/dmesg 中看到/ W* v- N5 j! k5 f

5 U9 [- j7 }/ A- u
; y% M7 H) ?& y- R5 c. R" w0 v4 U# g2 S! k+ W
2.2 oops& {/ r7 V" U1 P
oopss是个拟声词, 类似 "哎哟" 的意思. 它是内核通知用户有不幸发生的最常用方式." g3 ?3 P, s, z7 i9 o

& [* j' g0 l! K6 F5 n' \触发一个oops很简单, 其实只要在上篇博客中的那些内核模块示例中随便找一个, 里面加上一段给未初始化的指针赋值的代码, 就能触发一个oops
5 Y/ p  i, k7 _( L$ x
8 U) Q9 w) i8 K& r/ F. r4 b& N   J. }. y! O- i6 x% f8 c

+ z6 }6 \$ g: B- Eoops中包含错误发生时的一些重要信息(比如, 寄存器上下文和回溯线索), 对调试bug很有帮助!
9 V9 P0 {8 f+ k
+ Q8 w. T& O, w4 ~% v) c2 [ ! p4 l! W+ M9 y2 e+ |
! E$ V( B$ p3 F: o
调试内核时, 还可以开启内核编译参数中的各种和内核调试相关的选项, 那样还可以给我们提供内核崩溃时的一些额外信息.
7 v2 c2 w) Y1 |4 x6 f1 T
1 \) @3 Q! i# k2 S6 u0 D( ~ + h) D# I2 L& q9 m  l

0 C1 E: O- M; y* S; k1 x) Z2.3 主动触发bug
  ^2 z- Q2 e3 _9 M
6 ?  |; M2 I9 I5 s调试中有时将某些情况下标记为bug, 执行到这些情况时, 提供断言并输出信息.
) q; h% z( V, A2 z' E, \2 i6 @, l7 s& O& j) h9 ~" c! F' {2 W
BUG 和 BUG_ON 就是2个可以主动触发oops的内核调用.* ^8 Z) o  Y" ?5 U

) E7 D. u  Z9 H" v$ d: j1 y$ a
0 Z' u3 z( _5 m0 G7 t- A  m7 U3 o5 l+ _( q& Q! A. B
在不应该被执行到的地方使用 BUG 或者 BUG_ON 来捕获.
7 J3 z3 \: v! n& J" e/ X* P
9 n! p- E+ Z$ V0 ]/ ]/ u9 B比如:
" _  Y: z" }, {
/ t* }$ P8 p- [- M8 s- [$ r$ b$ Fif (bad_thing); e6 s6 W5 z! m9 t3 t& M7 c7 p
    BUG();
6 |7 ]- |' ?" A, E( s$ h4 k// 或者, S+ k/ H3 a/ ?
BUG_ON(bad_thing);
8 I8 a; T; X, Y1 {7 R $ x  o3 {4 j+ A$ R* F4 ^! E2 a; s) \
4 Y6 e% Q/ w4 }3 y
如果想要触发更为严重的错误, 可以使用 panic() 函数
5 M& @2 F* f1 ^8 H
  b6 q3 l% }! z! p. c; c! b比如:- U3 X: I- c* R% K. e# e) l

9 T- b; P& c* v$ Zif (terrible_thing)
- z2 s3 b7 I+ e  Z. d    panic("terrible thing is %ld\n", terrible_thing);
0 {0 W' B3 k5 u  A* q  T- V
7 U3 Z) M" w8 ^3 l9 p' V: G- x* Z- V
此外, 还有dump_stack 函数可以打印寄存器上下文和回溯信息.
& p7 h0 A) J) |4 b' R4 y2 N5 k6 ~
比如:
, b1 F. G: e0 L- w2 e; g' g1 {( z2 @" ^" Y' M$ Q: i) i
if (!debug_check) {& I0 [1 _! o, M. {! m! `$ S
    printk(KERN_DEBUG "provide some information...\n");
8 e  w" U7 c6 m" R% ?0 G/ @    dump_statck();
  i8 D- X& l4 }0 r- o' U1 ?' R- B}
- u# y5 Q1 z2 D: Z% L 1 G0 ^- N7 m" O# q1 [# c
* j, Q- N( X$ q1 l+ R- C
2.4 神奇的系统请求键
; n. R& Y  h; B9 [
8 z  }7 k& \' L; q0 r- M5 c% ^这个系统请求键之所以神奇, 在于它可以在一个快挂了的系统上输出一些有用的信息.+ I( {6 o: h" U" U- \+ k4 F

% m! C8 a# ~/ j这个按键一般就是标准键盘上的 [SysRq] 键 (就在 F12 键右边, 其实就是windows中截整个屏幕的按键)
+ S; Z( k% u" \( P$ N* o* r" H& T- z+ |
单独按那个键相当于截屏, 按住 ALT + [SysRq] = [SysRq]的功能
) K, i: `4 i- m9 q% c7 ]2 j0 E
7 f3 }, V- f3 _% H' M; a. l

4 x/ O+ I6 \, a5 C8 m/ F+ ~: z* P启用这个键的功能有2个方法:
* [3 g7 G, c! F0 v; s! X: Y7 a: C* B- W7 w7 B2 E8 q3 V- s9 V
  • 开启内核编译选项 : CONFIG_MAGIC_SYSRQ
  • 动态启用: echo 1 > /proc/sys/kernel/sysrq
    4 }" E  e: M) q4 m

3 A/ x' e9 K# g* X! w' E
" o4 t# ~! p9 J* u+ k' |支持 SysRq 的命令如下: (注意要在控制台界面下使用这个键, 比如通过 ALT+CTRL+F2 进入一个控制台界面), T3 ~& L) }5 [! A/ r$ k

' p& G' B, M" U* Z7 i$ f8 w; j
主要命令
描述
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卸载所有已加载文件系统

: y4 z/ O+ h: ~# W  P
) H. h, E& A3 B0 S. T
* c0 }  J# i6 d- E; r2.5 内核调试器 gdb和kgdb/ U) u8 c1 L9 p* V
6 X8 L$ r9 p: N& @3 V
linux内核的调试器可以使用 gdb或者kgdb, 配置比较麻烦, 准备实际用调试的时候再去试试效果如何..0 I8 x9 A- i& E, m; |
$ M% t& i; l! R  J, z2 B

+ z1 g1 l. ^5 u6 l4 z! R
* u4 R1 a/ T+ X" _$ x2.6 探测系统: U( C& R& \( `2 k/ t) T3 |& [
5 ?0 C# ]& ~2 V- L2 j+ \# S6 V, i% d! N
下面一些方法是在修改内核后, 用来试探内核反应的小技巧.
6 s5 ~( W& p  D& G$ q% p. @3 I9 D+ H

+ p' @6 Z( h$ @1 v/ P6 _. ?2.6.1 用UID控制内核执行
- i) ^5 b/ q# ^6 S! ^. U7 M( @- H. g5 `5 v
比如在内核中加入了新的特性, 为了测试特性, 可以用UID来控制内核是否执行新特性.
: n( R# V$ b3 t) P4 S" S' A; J- W8 p
if (current->uid != 7777) {. q, H7 |4 S' D$ _9 M/ X
    /* 原先的代码 */$ D0 D8 S9 u! X1 @
} else {7 [9 `; P1 b% q7 j; N* g9 Y* L* G! ~
   /* 新的特性 */4 N% K2 {2 w5 I6 i# [5 b( U
}
, b* X1 C' N1 \# f  \0 I+ W; f# d: ~ % J, S* v2 f" P4 b9 M) T

0 p$ x( R* z, e! t2.6.2 用条件变量控制内核执行
6 d7 C; }+ w1 q) ?
2 ^% A0 Q% @1 e, Y& B也可以设置一些条件变量来控制内核是否执行某段代码.& s! j, S# _$ o' D1 g, N: @
( {6 x) s4 u/ S" \) N3 p( t( O, u
条件变量可以像上篇博客中那样, 设置在 sys 文件系统的某个文件中. 当文件中的值变化时, 通知内核执行相应的代码.
! W6 ?$ x5 l1 A6 Y2 k0 C7 @8 I: b( h: ]

; {- u( m/ }3 {! \4 H1 D8 o# I3 f# @3 i* g  D
2.6.3 使用统计量观察内核执行某段代码的频率
0 x/ D) e, G; B% v0 a" B+ M; m0 X0 s# n2 v# |9 N+ B) \' j
实现思路就是在内核中的设置一个全局变量, 比如 my_count, 当内核执行到某段代码时, 给 my_count + 1 就行.
: y/ Q  C1 E, T5 @! ]3 i( _: Z9 e% v
) k% R* K' P, A同时还要将 my_count 打印出来(可以用printk), 便于随时查看它的值.
% Y# m& g& ]) B9 b! A$ b2 @5 Y( c* o: H
5 U: \- j0 a6 @: x  ]; L. D

8 h" j# w& _" @  K# i2.6.4 控制内核执行某段代码的频率
- T! E# [9 n4 `1 B1 f8 ]( l* ~7 h3 A4 C
有时侯, 我们需要在内核发生错误时打印错误相关的信息, 如果这个错误不会导致内核崩溃, 并且这个错误每秒会发生几百次甚至更多.
/ D8 o- i0 ^- I, u5 K& [
3 n8 K  V1 {; J1 B" c0 S, O, _那么, 用printk输出的信息会非常多, 给系统造成额外的负担.- g& ~4 M4 H) {# M/ Q: F
6 t( M, I$ E# l) q: w
这时, 我们就需要想办法控制错误输出的频率, 有2种方法:9 g1 N- D, d. s
9 a$ U/ c$ x0 k5 w& g7 P. w# ^
方法1: 隔一段时间才输出一次错误
2 a& p9 ?5 }: z1 g' C. f/ X! j
2 h( _6 a4 H* J+ \9 pstatic unsigned long prev_jiffy = jiffies;  /* 频率限制 */
' }- b( x" k% ]
0 {8 u6 b! C, H7 xif (time_after(jiffies, prev_jiffy + 2*HZ)) {  /* 输出间隔至少 2HZ */
. `2 Y0 k* x  m: u    prev_jiffy = jiffies;: N6 T3 i: Z# U6 q* |2 u
    printk(KERN_ERR "错误信息....\n");
4 z  i% ~9 ?+ A. Z- P' V& e}
+ _$ _9 N! k/ e1 F0 E6 ?
  n# g, S2 d; r; r  L
1 c/ k& j' X) @' h5 [方法2: 输出 N 次之后, 不再输出(N是正整数)# X! d; A6 s% U: P8 d# k6 d/ M

  [' O' L" ^6 v# L5 nstatic unsigned long limit = 0;
& N+ k/ A9 X( a7 P9 S( E9 Z2 C4 i$ I% f. g
if (limit < 5) { /* 输出5次错误信息后就不再输出 */- h& x& F: {% N5 ?8 c
    limit++;
5 T7 _& i' d" e) }    printk(KERN_ERR "错误信息....\n");
% K! `1 Q8 `5 q- R/ g, `5 U4 C}
3 a% @. u; T+ C) V4 p" c ( |; y0 u! t5 j" E: l' K+ Z7 D

/ r2 O& Y$ l& s+ T0 p- c8 u4 w: P1 C
) N: J0 x& N8 g3 t) {2.7 二分法查找bug发生的最初内核版本1 z$ h( R3 t5 Y8 Y9 K* U4 R
8 O* Z0 j# q2 `, N# x
在内核发生了bug之后, 如果能够知道是bug从哪个内核版本开始出现的, 那对修正这个bug会有很大的帮助.
' f* k$ R, @0 L8 a6 d; W& q' j2 p! t) D  c1 @
由于内核代码非常庞大, 即使用二分查找法, 手工去找哪个版本开始出现bug的话, 仍然是非常耗时和繁琐的.* Y& H5 O' E( w  [  W' e

3 F* Z: k2 m% @) Z
0 E' s6 ]9 x. s- s" c% P3 N+ b% S7 \, y3 u1 ~0 U% {+ J; }
好在 Git 给我们提供了一个非常有用的二分搜索机制.
2 V9 Y. o0 G' Q% s8 |3 A3 }* U- D4 q( t. ]* j0 r0 ?8 Z
git bisect start  # 开始二分搜索
, G# R6 Q! s  ~+ ?git bisect bad <bad_revision> # 指定一个bug出现的内核版本号+ k" g+ t& u' I/ R0 z
git bisect good <good_revision> # 指定一个没有bug的内核版本号, 此时git会检测2个版本直接的隐患
4 C% I) d! g( F8 R$ W
# k5 n9 C$ }* e) `; O% p- A# 根据结果再次设置 bad 和 good 的版本号, 缩小Git检索范围, 直至找到可疑之处为止.
: b+ A) r7 N& I- N# @* d
- ^( _* k- s' Q: n* u7 p' S  ~; ^
) o4 e% A/ T; C
2.8 社区: J3 p8 b- V9 s4 i& R0 r
5 z& y) S% j! l: B# L) Q
当你在调试bug时用尽了一切手段仍然无济于事时, 可以考虑求助linux社区, 求助时注意一定要描述清楚bug的状况.
, D1 @  I* W7 w' `5 F9 P
& Q, x- Q! M0 w  h  e; v(可以参考一下别人汇报bug的格式)
5 N; M# ?8 F$ W
4 s1 d( a- K; k. z$ c
; I3 L. Q. l: b' D7 N& L% M5 ~  I( [6 S( q& v5 @
( L( H$ e) [4 j" Z* l
3. 总结3 Z! Z, q+ \; Q3 c2 \7 k) ]

6 c1 C6 ]* g) ~, b0 hlinux内核调试必须要依靠大量的实践来掌握, 仅仅靠上面介绍的一些技巧还远远不够, 只有实实在在的去阅读内核代码, 实实在在的去修正一个个bug, 才算真正掌握内核, 真正了解内核.
6 N/ m8 |. _. D) f7 m6 P9 a5 t
# b4 @- Q+ Q/ c6 R多看看之前linux内核bug的修正案例, 也是个不错的积累经验的方法.
4 v  O; N1 `. c1 a5 I1 E- w
  R/ {( k6 R- t! l " a5 e; V/ n* O6 L  ?" D* ?
+ m0 U+ Z  |9 H6 t4 V/ b& O# i0 g
PS.
+ u9 P. B3 H6 C& x- }: C. j0 R/ U- ^9 Q, l
对于初学者来说, 在真机上做内核开发动辄导致机器崩溃(panic), 非常麻烦. 现在的虚拟机这么强大, 建议都在虚拟机上测试linux内核修改的效果.2 Q- s$ w, C8 G' z  N5 D( g. ~

  I3 z9 c" D9 m) Y我之前的关于<<Linux内核设计与实现>>笔记的博客中的代码都是在虚拟机上运行测试的.6 b0 J5 ~/ r+ @8 N) l

该用户从未签到

2#
发表于 2021-3-9 10:44 | 只看该作者
Linux内核设计与实现之内核调试
您需要登录后才可以回帖 登录 | 注册

本版积分规则

关闭

推荐内容上一条 /1 下一条

EDA365公众号

关于我们|手机版|EDA365电子论坛网 ( 粤ICP备18020198号-1 )

GMT+8, 2025-11-24 15:47 , Processed in 0.203125 second(s), 24 queries , Gzip On.

深圳市墨知创新科技有限公司

地址:深圳市南山区科技生态园2栋A座805 电话:19926409050

快速回复 返回顶部 返回列表