談談Linux打補丁的原理以及如何判別打補丁的錯誤 --- 從補丁學內核

    對於長期使用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:

[cpp] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. <span style="font-size:18px;">patching file kernel/sched.c  
  2. Hunk #1 succeeded at 190 (offset 1 line).  
  3. Hunk #2 succeeded at 943 (offset 1 line).  
  4. Hunk #3 succeeded at 1283 (offset 1 line).  
  5. Hunk #4 succeeded at 2571 (offset 1 line).  
  6. Hunk #5 succeeded at 2656 (offset 1 line).  
  7. Hunk #6 succeeded at 2833 (offset 1 line).  
  8. Hunk #7 succeeded at 2909 (offset 1 line).  
  9. Hunk #8 succeeded at 2925 (offset 1 line).  
  10. Hunk #9 succeeded at 3211 (offset 1 line).  
  11. ......  
  12. </span>  

那麼對於這樣的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用於指定所要修改的代碼在哪個函數裏面。 如上面這個補丁在文件中的狀況爲:

[cpp] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. <span style="font-size:18px;"> 865 void __irq_entry smp_apic_timer_interrupt(struct pt_regs *regs)    // 補丁>所要修改的函數函數  
  2.  866 {  
  3.         ...  
  4.  876      * Besides, if we don't timer interrupts ignore the global     // 補丁>起始位置,876行  
  5.  877      * interrupt lock, which is the WrongThing (tm) to do.  
  6.  878      */  
  7.  879     exit_idle();               // 現在exit_idle();是在irq_enter();之前的,>我們可以看到這個補丁所要做的其實就是把它放到irq_enter();後面去  
  8.  880     irq_enter();  
  9.  881     local_apic_timer_interrupt();  
  10.  882     irq_exit();  
  11.  883   
  12.  884     set_irq_regs(old_regs);  
  13.  885 }  
  14.  886   
  15.  887 int setup_profiling_timer(unsigned int multiplier)</span>  

Linux的補丁內容還包括三個部分:修改語句前三句 + 修改語句 + 修改語句後三句。

我們可以看到在正式修改的語句 -   exit_idle();之前有三句正常的代碼,這是用來給補丁定位的,因爲單純只靠行數來確定補丁修改的位置是不夠的,作爲開源軟件,Linux系統內的代碼可能會被修改,所以得在補丁之前添加三句代碼用來給補丁定位。

我們還能看到在+   exit_idle();語句的後面還有三句正常的代碼,這是補丁結束的標誌,也需要匹配以便檢查我們的補丁是否合適!


3. 對Hunk的分析

好了,現在我們已經知道了Linux的補丁是如何運作的,那麼我們可以根據上面使用 --dry-run 記錄的打補丁記錄來分析裏面出現的所有Hunk,比如上面提到的:

[cpp] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. <span style="font-size:18px;">patching file kernel/sched.c  
  2. Hunk #1 succeeded at 190 (offset 1 line).  
  3. Hunk #2 succeeded at 943 (offset 1 line).  
  4. Hunk #3 succeeded at 1283 (offset 1 line).  
  5. Hunk #4 succeeded at 2571 (offset 1 line).  
  6. Hunk #5 succeeded at 2656 (offset 1 line).  
  7. Hunk #6 succeeded at 2833 (offset 1 line).  
  8. Hunk #7 succeeded at 2909 (offset 1 line).  
  9. Hunk #8 succeeded at 2925 (offset 1 line).  
  10. Hunk #9 succeeded at 3211 (offset 1 line).  
  11. ......  
  12. </span>  


這很有可能就是在當前打補丁的文件中在前面有一個空白行,導致後面所有的補丁都有(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 行,我們可以看到:

[cpp] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. <span style="font-size:18px;"> 189     raw_spin_lock_init(&rt_b->rt_runtime_lock);  
  2.  190   
  3.  191     hrtimer_init(&rt_b->rt_period_timer,  
  4.  192             CLOCK_MONOTONIC, HRTIMER_MODE_REL);  
  5.  193     rt_b->rt_period_timer.function = sched_rt_period_timer;  
  6.  194 }</span>  


這也應證了我們的猜想,補丁中用於定位的空白行應該在第189行,而在這個內核中的第189行並不是空白行,補丁自動上下找,在190~192這三行找到對應的定位,並試打補丁,前三句加後三句都正常,所以這個補丁雖然有 1 行的誤差,但是正常了!所以 “ Hunk #1 succeeded at 190 (offset 1 line). ”提示這裏雖然有Hunk但是是succeeded的!在這個文件的第一個補丁就有一個偏移,所以後面的所有的補丁都有一行偏移,這也解釋了爲什麼這個文件那麼多Hunk的原由!

同樣,我們可以看到:

[cpp] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. <span style="font-size:18px;">patching file kernel/fork.c  
  2. Hunk #2 succeeded at 212 (offset 16 lines).  
  3. Hunk #3 succeeded at 568 (offset 16 lines).  
  4. Hunk #4 succeeded at 1063 (offset 16 lines).  
  5. Hunk #5 succeeded at 1174 (offset 16 lines).  
  6. Hunk #6 succeeded at 1236 (offset 16 lines).</span>  

這樣的偏移有十六行的總不能有十六個空白行吧?大家應該猜到了這樣大的偏移應該是程序猿在這個文件裏添加了一個函數或者類似的!


其實,在內核中這樣的Hunk succeeded 都沒多大問題,雖然有Hunk但是都正常了!但是如果是作產品級開發,我們不能放過任何一個Hunk,我們需要的是團隊合作 + 分析patch log中所有的Hunk + 準確的記錄(包括這個Hunk的原由,重不重要等)! 


那麼Hunk FAILED呢?我們在這個log中找到了這樣幾行:

[cpp] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. <span style="font-size:18px;">patching file arch/arm/kernel/process.c  
  2. Hunk #1 FAILED at 214.  
  3. Hunk #2 succeeded at 612 (offset 121 lines).  
  4. 1 out of 2 hunks FAILED -- saving rejects to file arch/arm/kernel/process.c.rej</span>  

這裏有個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函數,我們發現所要刪除的前三句和補丁中的不一樣而且位置也有所偏移:

[cpp] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. <span style="font-size:18px;">254         }  
  2. 255         tick_nohz_restart_sched_tick();  
  3. 256         idle_notifier_call_chain(IDLE_END);  
  4. 257         preempt_enable_no_resched();  
  5. 258         schedule();  
  6. 259         preempt_disable();  
  7. 260     }  
  8. 261 }  
  9. </span>  

這應該是這個文件經過廠家修改,所以我們預先準備好的官方 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 很重要的補丁:

[cpp] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. <span style="font-size:18px;"> 1462 +/** 
  2.  1463 + * schedule_preempt_disabled - called with preemption disabled 
  3.  1464 + * 
  4.  1465 + * Returns with preemption disabled. Note: preempt_count must be 1 
  5.  1466 + */  
  6.  1467 +void __sched schedule_preempt_disabled(void)  
  7.  1468 +{  
  8.  1469 +   __preempt_enable_no_resched();  
  9.  1470 +   schedule();  
  10.  1471 +   preempt_disable();  
  11.  1472 +}</span>  

我們可以看到在補丁中的 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,我們可以看到:

[cpp] viewplaincopy在CODE上查看代碼片派生到我的代碼片
  1. <span style="font-size:18px;">--- helloworld.c    2013-11-26 20:35:48.736255687 +0800  
  2. +++ helloworld1.c   2013-11-26 20:36:10.340255754 +0800  
  3. @@ -1,5 +1,6 @@  
  4.  #include "stdio.h"  
  5.  int main(int argc ,char **argv)  
  6.  {  
  7. -   printf("Hello World");  
  8. +   printf("Hello World\n");  
  9. +   return 0;  
  10.  }</span>  

恩,效果不錯!!!了罷此文~~~


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

引用資料:

【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的童鞋們學習之用,大家好好努力,與我同進步!



轉自:http://blog.csdn.NET/longerzone/article/details/16967579

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章