现在的位置: 首页 > 综合 > 正文

谈谈Linux打补丁的原理以及如何判别打补丁的错误 — 从补丁学内核

2014年01月26日 ⁄ 综合 ⁄ 共 8158字 ⁄ 字号 评论关闭

对于长期使用Linux的童鞋来说,不说有没有打过补丁,至少这个词大家并不陌生,下面我们通过一个实例来说说:

       前几天接触了TQ3358这块天嵌的ARM板子,想给它装个实时Linux并做测试,在自带的光盘中我找到了“Kernel_3.2_TQ3358_for_Linux_v1.2” 这样一个内核版本(从Makefile中我们可以看到这是个 3.2.0 版本的内核),我下载了实时补丁“ patch-3.2.6-rt13.patch
(因为官方的内核3.2和3.2.6好像没什么区别,所以我们使用了这个补丁),并下载了Kernel-3.2.6 官方内核(用于在打补丁出问题时查看并判断)。

注:对于一个实时补丁对应的kernel.org官网上有两个文件我们需要下载,比如说我们的这个3.2.6版本,我们需要下载一个patch-3.2.6-rt13.patch.bz2patches-3.2.6-rt13.tar.bz2,前者用于打补丁后者用于分析学习补丁。

所以在整个工作开始前我们有下面四个文件:

$ ls
Kernel_3.2_TQ3358_for_Linux_v1.2    linux-3.2.6
   patch-3.2.6-rt13.patch      patches


1. 首先我们试打补丁

     在打补丁时使用 --dry-run 参数就会试运行打补丁的过程(并没有真正的改变文件内容)!在内核文件 linux/Documentation/applying-patches.txt 中这样介绍:“--dry-run which causes patch to just print a listing of what would happen, but doesn't actually make any changes” (使用 --dry-run
会打印一系列在打补丁时要发生的事,但是不会真正的作任何改变!)

我们使用如下命令试打这个补丁并将输出记录到 /tmp/log 文件中:

                                      patch  -p1 --dry-run <../patch-3.2.6-rt13.patch >/tmp/log

打开 /tmp/log 文件我们会发现里面又出现很多类似于下面这样的Hunk:

patching file kernel/sched.c
Hunk #1 succeeded at 190 (offset 1 line).
Hunk #2 succeeded at 943 (offset 1 line).
Hunk #3 succeeded at 1283 (offset 1 line).
Hunk #4 succeeded at 2571 (offset 1 line).
Hunk #5 succeeded at 2656 (offset 1 line).
Hunk #6 succeeded at 2833 (offset 1 line).
Hunk #7 succeeded at 2909 (offset 1 line).
Hunk #8 succeeded at 2925 (offset 1 line).
Hunk #9 succeeded at 3211 (offset 1 line).
......


那么对于这样的Hunk 我们的补丁会不会有什么影响呢?会不会因为有Hunk 打不上补丁呢?这就要我们根据每个Hunk指定的文件来判断。


2. 简介 Linux 补丁的原理

我们使用vim 打开 patch 文件,就会发现patch 文件中充满了这样的结构:

Index: linux-3.2/arch/x86/kernel/apic/apic.c
===================================================================
--- linux-3.2.orig/arch/x86/kernel/apic/apic.c
+++ linux-3.2/arch/x86/kernel/apic/apic.c
@@ -876,8 +876,8 @@void __irq_entry smp_apic_timer_interrup
     * Besides, if we don't timer interrupts ignore the global
     * interrupt lock, which is the WrongThing (tm) to do.
     */
-   exit_idle();
    irq_enter();
+   exit_idle();
    local_apic_timer_interrupt();
    irq_exit();

...

那么聪明的你应该大致能猜出来这是什么个结构:

首先使用 Index: linux-3.2/arch/x86/kernel/apic/apic.c 指明下面的这段修改是什么文件中的;

再用 “--- linux-3.2.orig/arch/x86/kernel/apic/apic.c
+++ linux-3.2/arch/x86/kernel/apic/apic.c” 这么两行标注一小段补丁,这两行称之为补丁头,---开头的那行 表示旧文件,+++开头的那行 表示新文件。感觉这两行和Index 那行有点冗余!

接下来是补丁内容:@@ -876,8 +876,8 @@ 用于表述补丁所要修改的代码位于这个文件中的第几行,void __irq_entry smp_apic_timer_interrup用于指定所要修改的代码在哪个函数里面。 如上面这个补丁在文件中的状况为:

 865 void __irq_entry smp_apic_timer_interrupt(struct pt_regs *regs)    // 补丁>所要修改的函数函数
 866 {
        ...
 876      * Besides, if we don't timer interrupts ignore the global     // 补丁>起始位置,876行
 877      * interrupt lock, which is the WrongThing (tm) to do.
 878      */
 879     exit_idle();               // 现在exit_idle();是在irq_enter();之前的,>我们可以看到这个补丁所要做的其实就是把它放到irq_enter();后面去
 880     irq_enter();
 881     local_apic_timer_interrupt();
 882     irq_exit();
 883 
 884     set_irq_regs(old_regs);
 885 }
 886 
 887 int setup_profiling_timer(unsigned int multiplier)


Linux的补丁内容还包括三个部分:修改语句前三句 + 修改语句 + 修改语句后三句。

我们可以看到在正式修改的语句 -   exit_idle();之前有三句正常的代码,这是用来给补丁定位的,因为单纯只靠行数来确定补丁修改的位置是不够的,作为开源软件,Linux系统内的代码可能会被修改,所以得在补丁之前添加三句代码用来给补丁定位。

我们还能看到在+   exit_idle();语句的后面还有三句正常的代码,这是补丁结束的标志,也需要匹配以便检查我们的补丁是否合适!


3. 对Hunk的分析

好了,现在我们已经知道了Linux的补丁是如何运作的,那么我们可以根据上面使用 --dry-run 记录的打补丁记录来分析里面出现的所有Hunk,比如上面提到的:

patching file kernel/sched.c
Hunk #1 succeeded at 190 (offset 1 line).
Hunk #2 succeeded at 943 (offset 1 line).
Hunk #3 succeeded at 1283 (offset 1 line).
Hunk #4 succeeded at 2571 (offset 1 line).
Hunk #5 succeeded at 2656 (offset 1 line).
Hunk #6 succeeded at 2833 (offset 1 line).
Hunk #7 succeeded at 2909 (offset 1 line).
Hunk #8 succeeded at 2925 (offset 1 line).
Hunk #9 succeeded at 3211 (offset 1 line).
......

这很有可能就是在当前打补丁的文件中在前面有一个空白行,导致后面所有的补丁都有(offset 1 line) ! 

我们可以在补丁里面打开这个文件(kernel/sched.c)的补丁,我们可以看到第一个补丁是:

@@ -189,6 +189,7 @@
void init_rt_bandwidth(struct rt_bandwid

    hrtimer_init(&rt_b->rt_period_timer,
            CLOCK_MONOTONIC, HRTIMER_MODE_REL);
+   rt_b->rt_period_timer.irqsafe = 1;
    rt_b->rt_period_timer.function = sched_rt_period_timer;
 }

所以我们知道,这个补丁是靠 一个空白行加上hrtimer_init函数占用的两行作为定位,并且空白行位于第189行。

下面我们打开kernel/sched.c 打开这个发生Hunk 的文件,跳转到第189 行,我们可以看到:

 189     raw_spin_lock_init(&rt_b->rt_runtime_lock);
 190 
 191     hrtimer_init(&rt_b->rt_period_timer,
 192             CLOCK_MONOTONIC, HRTIMER_MODE_REL);
 193     rt_b->rt_period_timer.function = sched_rt_period_timer;
 194 }

这也应证了我们的猜想,补丁中用于定位的空白行应该在第189行,而在这个内核中的第189行并不是空白行,补丁自动上下找,在190~192这三行找到对应的定位,并试打补丁,前三句加后三句都正常,所以这个补丁虽然有 1 行的误差,但是正常了!所以 “ Hunk #1 succeeded at 190 (offset 1 line). ”提示这里虽然有Hunk但是是succeeded的!在这个文件的第一个补丁就有一个偏移,所以后面的所有的补丁都有一行偏移,这也解释了为什么这个文件那么多Hunk的原由!

同样,我们可以看到:

patching file kernel/fork.c
Hunk #2 succeeded at 212 (offset 16 lines).
Hunk #3 succeeded at 568 (offset 16 lines).
Hunk #4 succeeded at 1063 (offset 16 lines).
Hunk #5 succeeded at 1174 (offset 16 lines).
Hunk #6 succeeded at 1236 (offset 16 lines).


这样的偏移有十六行的总不能有十六个空白行吧?大家应该猜到了这样大的偏移应该是程序猿在这个文件里添加了一个函数或者类似的!


其实,在内核中这样的Hunk succeeded 都没多大问题,虽然有Hunk但是都正常了!但是如果是作产品级开发,我们不能放过任何一个Hunk,我们需要的是团队合作 + 分析patch log中所有的Hunk + 准确的记录(包括这个Hunk的原由,重不重要等)! 


那么Hunk FAILED呢?我们在这个log中找到了这样几行:

patching file arch/arm/kernel/process.c
Hunk #1 FAILED at 214.
Hunk #2 succeeded at 612 (offset 121 lines).
1 out of 2 hunks FAILED -- saving rejects to file arch/arm/kernel/process.c.rej


这里有个Hunk FAILED,说明补丁的前三句和后三句定位出问题了,按照常理,我们先找到相应的文件补丁:

--- linux-3.2.orig/arch/arm/kernel/process.c
+++ linux-3.2/arch/arm/kernel/process.c

@@ -214,9 +214,7 @@
void cpu_idle(void)

        }
        leds_event(led_idle_end);
        tick_nohz_restart_sched_tick();
-       preempt_enable_no_resched();
-       schedule();
-       preempt_disable();

+       schedule_preempt_disabled();
    }
 }

我们发现当前这个补丁是中规中矩的“前三+所做修改+后三”的格式,那么为什么会失败呢?赶紧打开内核中的这个文件一探究静:打开arm/kernel/process.c文件,找到 cpu_idle函数,我们发现所要删除的前三句和补丁中的不一样而且位置也有所偏移:

254         }
255         tick_nohz_restart_sched_tick();
256         idle_notifier_call_chain(IDLE_END);
257         preempt_enable_no_resched();
258         schedule();
259         preempt_disable();
260     }
261 }


这应该是这个文件经过厂家修改,所以我们预先准备好的官方 kernel-3.2.6就用上了: 使用 vimdiff arch/arm/kernel/process.c ../linux-3.2.6/arch/arm/kernel/process.c查看两个文件之间的区别,并找到我们补丁对应的位置:


我们可以看到官方内核中的代码对应补丁是前三句后三句都匹配的,而这个内核中的是前三句完全不匹配,从而导致patch的失败!

对于这样的情况,我们要做的工作就是调查这个补丁对应的所有文档:包括整个补丁包中还有没有类似补丁包括 patches 里对应的补丁说明;包括在
linux-stable-rt 的 git 仓库中使用git blame 查明是谁、为什么要做这个修改。

如果你自己是在闹不明白,可以上邮件列表或者直接给补丁的作者发邮件问清楚这个补丁是干什么的?为什么需要?为什么在实时内核中需要在普通内核中不需要?在这个文件中的这个补丁会引发多大的性能损失?...

在做嵌入式开发时、在做系统内核的调研时要多做查询、多作测试才能做好产品,才能做好学习!


4. 真正的打上补丁并对rej 文件处理

经过查资料 + 调研 + 测试,你会发现这个补丁是否对你有用,我在这说明这个补丁是对RT-Linux 很重要的补丁:

 1462 +/**
 1463 + * schedule_preempt_disabled - called with preemption disabled
 1464 + *
 1465 + * Returns with preemption disabled. Note: preempt_count must be 1
 1466 + */
 1467 +void __sched schedule_preempt_disabled(void)
 1468 +{
 1469 +   __preempt_enable_no_resched();
 1470 +   schedule();
 1471 +   preempt_disable();
 1472 +}


我们可以看到在补丁中的 linux-3.2/kernel/sched.c 的实时补丁中使用schedule_preempt_disabled();函数将三句“    preempt_enable_no_resched();         schedule();    
    preempt_disable();
 ” 封装在一起。那么这个封装对实时系统的抢占很重要么? 这是一个大的补丁,在这整个大的补丁中对于所有的这三句都进行替换,所以这是个对实时内核很重要的一个修改。(注:至此的判断已经足矣让我们在失败的补丁中确定这个修改的重要性!但是如果你想更好的学习内核,你需要继续找它的用途知道原理~本文不详述)

所以我们下们进行正式的打补丁: patch  -p1  <../patch-3.2.6-rt13.patch >/tmp/log1   (去掉了 --dry-run 参数)

我们会在日志中发现与上面一样的Hunk FAILED:

patching file arch/arm/kernel/process.c
Hunk #1 FAILED at 214.
Hunk #2 succeeded at 612 (offset 121 lines).
1 out of 2 hunks FAILED -- saving rejects to file arch/arm/kernel/process.c.rej

因为我们已经确定了这个failed 可能对内核产生实时性能的影响,所以我们可以去 arch/arm/kernel/process.c 手动的将    preempt_enable_no_resched();      schedule();   preempt_disable();这三句去掉,添加上  
  schedule_preempt_disabled();
 。


5. patch学习总结

      分析内核补丁是个漫长的过程,如果你是个新手,一个Hunk是需要很久的,整个log 文件可能会花上你一周甚至更久! 但是对于产品级的内核就要达到这样的精准度,字字句句斟酌斟酌再斟酌! 当你是个大牛时,你会发现前面的这个工作是非常重要的更是非常有价值的!


6. 试验自制个补丁

比如说我们目前有这么个Helloworld.c文件:

#include "stdio.h"
int main(int argc ,char **argv)
{
    printf("Hello World");
}

我想给它改成:

#include "stdio.h"
int main(int argc ,char **argv)
{
    printf("Hello World\n");
    return 0;
}

前面的helloworld程序命名hello.c,后面的命名为hello_new.c

生成补丁需要用这个命令  :  diff -uN from-file to-file >to-file.patch  ,所以我们的这个补丁需要这么用: diff -uN hello.c hello_new.c >patch.test.patch

打开生成的patch.test.patch,我们可以看到:

--- helloworld.c    2013-11-26 20:35:48.736255687 +0800
+++ helloworld1.c   2013-11-26 20:36:10.340255754 +0800
@@ -1,5 +1,6 @@
 #include "stdio.h"
 int main(int argc ,char **argv)
 {
-   printf("Hello World");
+   printf("Hello World\n");
+   return 0;
 }

恩,效果不错!!!了罢此文~~~


=====================================

引用资料:

【1】 Linux 内核文档: linux/Documentation/applying-patches.txt    & patch的 manpage(man patch)

【2】 RT_PREEMPT的维基主页 https://rt.wiki.kernel.org/index.php/RT_PREEMPT_HOWTO

【3】 linux下patch命令使用详解---linux打补丁命令
 http://www.linuxso.com/command/patch.html

【4】 补丁(patch)的制作与应用
 http://linux-wiki.cn/wiki/zh-hans/%E8%A1%A5%E4%B8%81(patch)%E7%9A%84%E5%88%B6%E4%BD%9C%E4%B8%8E%E5%BA%94%E7%94%A8


======================================

注:本文来自 著名的实时系统专家、Safety专家
Nicholas Mc Guire
的课!我这只能算作学习总结,同时也是写给众多学习Linux的童鞋们学习之用,大家好好努力,与我同进步!





抱歉!评论已关闭.