2010年2月18日 星期四

Makefile範例教學

Makefile和GNU make可能是linux世界裡最重要的檔案跟指令了。編譯一個小程式,可以用簡單的command來進行編譯;稍微複雜一點的程式,可以用shell script來幫忙進行編譯。如今的程式(如Apache, Linux Kernel)可能動輒數百萬行程式碼,數萬個標頭檔(headers)、函式庫(libraries)以及程式碼(source code),如果只是針對幾個檔案進行修改,卻要用shell script整個程式重新編譯,不但浪費時間也相當沒有效率。GNU make或是其他make程式的用途就在這裡: 當程式有些許變動時,我們需要一個程式幫助我們判斷哪些需要重新編譯,哪些不用;因此,撰寫一個好的Makefile便是相當重要的能力。

不過話說回來,是不是每一隻程式都需要一個Makefile呢?其實撰寫Makefile是有益無害的,只是如果你的程式就只有兩三個source codes需要編譯,其實忘掉Makefile也沒關係。本文的目的是希望以範例的方式能夠讓讀者能看得懂,並且有能力撰寫並修改Makefile,也順便當作自己的筆記。

傳統的編譯:

gcc foo1.c -o foo1
事實上,上面的這個編譯方式可以拆解成:
gcc foo1.c -c
gcc foo1.o -o foo1
編譯的過程是將原始碼(foo1.c)先利用-c參數編譯成.o(object file),然後再鏈結函式庫成為一個binary。-c即compile之意。
gcc foo1.c $SACLIB/sacio.a -O3 -g -Wall -ansi -o foo1
開始有趣了。編譯的參數開始變多:
  • -c :編譯但不進行鏈結,會產生一個跟原始碼相同名稱但副檔名為.o的目的檔。

  • -O :表示最佳化的程度
  • -O預設就是-O1,你可以指定成-O2或-O3,數字越大表示最佳化程度越好,但是會增加編譯時間。

  • -g :把偵錯資訊也編譯進去
  • 當你有需要使用GDB軟體進行偵錯,必須加入-g使GDB能夠讀取。一般情況下可以不用-g,因為他也會增加binary大小。

  • -Wall :顯示警告訊息
  • 使用這個參數會在編譯時顯示更多的警告訊息。這個參數相當有用,特別是找不到libs/headers之類的問題。

  • -ansi :使用相容ANSI標準的編譯方式
  • ANSI是American National Standards Institute,即美國國家標準學會的簡稱。-ansi可以增加程式的可移植性。

  • 其中的$SACLIB就是一個變數名稱,她必須被指定正確的值。
  • 執行這個命令前必須先確定這個變數是有被指派正確的值才行。.a檔是一個靜態函式(static library),關於靜態跟共享的觀念稍候解釋。
再來更多吧!假設你今天要編譯main這隻程式,他的source codes有main.c, foo.c, target.h,並且需要/usr/local/moreFoo/lib/libpthread.so這個共享函式,以及/usr/local/moreFoo/include裡面的headers;這麼複雜的情況又該怎麼作呢?
gcc main.c foo.c -I /usr/local/moreFoo/include -lpthread -L /usr/local/moreFoo/lib -O3 -ansi -o main
新的參數意義如下:
  • -I :需要include某些headers所在的目錄
  • 通常include目錄都放置headers,利用-I使編譯器知道去哪裡找原始碼裡宣告的header。gcc預設會去尋找headers的目錄大致有:
    • /usr/include
    • /usr/local/include
    • /usr/src/linux-headers-`uname -r`/include
    • /usr/lib/gcc/i486-linux-gnu/UR_GCC_VERSION/include
    • 當前目錄
    因此,當原始碼內有宣告
    #include <fakeFoo.h>
    但fakeFoo.h並不在上述的資料夾內,就需要利用-I引導gcc找到她。至於target.h因為在當前目錄,因此不必額外宣告。
    當然,可以利用多個-I來指定多個headers的路徑。

  • -l :表示編譯過程需要一個library。
  • -lpthread代表需要一個名為libpthread.so的函式。

  • -L :需要額外鏈結函式庫所在的目錄
  • 有時候程式碼經常會呼叫一些函數(methods, functions或是subroutines),而這些函數是使用其他人預先寫好的、已經編譯成函式(例如libpthread.so)供人使用的話,我們就不必自己從頭寫過。gcc預設會去找函式的目錄大致有:
    • /lib
    • /usr/lib
    • /lib/modules/`uname -r`/kernel/lib
    • /usr/src/linux-headers-`uname -r`/lib
    • /usr/local/lib
    • 當前目錄
    因此編譯時,利用-L指定目錄告訴編譯器可以該路徑下尋找libpthread.so。因此,若使用了-l,則必須確定所使用的lib有在預設尋找的目錄中,否則就必須利用-L來指定路徑給編譯器。
    當然,可以利用多個-L來指定多個lib路徑。

靜態、共享與動態鏈結函式庫
我們已經知道:輪子不必重複發明 -- 人家寫好的方法我們可以直接拿來用。不過很多時候,這些方法可能因為某些因素,希望提供給別人使用卻又不希望公佈原始碼,這時候編譯成libraries是最好的選擇。

  1. 靜態函式(static libraries)
  2. 靜態函式其實就是將一系列.o檔打包起來,因此她可以直接視為一個巨大的.o檔。打造出一個靜態函式的方法很簡單:
    gcc operator.c -c
    ar crsv liboperator.a operator.o
    或者
    gcc -static operator.c -loperator
    兩種方法皆能產生liboperator.a。假設這個靜態函式在/usr/local/foo/lib/裡,編譯時要與靜態函式作鏈結也很容易:
    gcc main.c /usr/local/foo/lib/liboperator.a -o main
    把靜態函式當成一般的.o檔一起納入binary,也可以像這樣:
    gcc main.c -L /usr/local/foo/lib -loperator -o main
    靜態函式將所有的功能全部打包在一起,因此binary會變得很巨大,但是執行這個程式的所有功能都已滿足,不會再有libraries相依性的問題。但是缺點在於當某些libraries的功能有所更新時,這個程式就必須重新編譯,無法享受到即時更新的優點。通常商業軟體以及嵌入式系統等功能異動較少的程式,會傾向使用靜態函式。

  3. 共享函式(shared libraries)
  4. 共享函式跟靜態函式的觀念剛好相反,程式在執行時必須能夠找到相依的函式,否則執行時會出現錯誤訊息。製作一個共享函式的方法也很簡單:
    gcc -shared operator.c -o liboperator.so
    或是先編譯出目的檔再進行鏈結:
    gcc -c operator.c
    gcc -shared operator.o -o liboperator.so
    產生出liboperator.so。假設這個共享函式在/usr/local/foo/lib/裡,使用共享函式進行鏈結也很容易:
    gcc main.c /usr/local/foo/lib/liboperator.so -o main
    也可以像這樣:
    gcc main.c -L /usr/local/foo/lib -loperator -o main
    共享函式在程式啟動時期會檢查是否存在。以一個分別鏈結了靜態函式與共享函式的binary而言,執行的結果大有差別。以靜態函式鏈結的main程式可以順利執行,但是假設系統預設尋找函式庫的路徑裡找不到liboperator.so,以共享函式鏈結的main程式則會出現錯誤訊息:
    ./main: error while loading shared libraries: liboperator.so: cannot open shared object file: No such file or directory
    這時解決的方法有四種:
    1. 把liboperator.so複製或是作一個連結到/usr/lib裡。
    2. 修改/etc/ld.so.conf,把/usr/local/foo/lib加進系統libraries的搜尋範圍內。
    3. 設定LD_LIBRARY_PATH變數,累加該路徑進來:
    4. 如果你不是系統管理員,前兩個方法根本沒辦法執行。我們只好自己加到~/.profile裡:
      export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/foo/lib
    5. 改用靜態函式進行鏈結。
    共享函式經常出現在開放原始碼的linux世界裡,由於使用所有函式皆是共享的,因此許多程式都可以重複利用既有的功能;有新功能或是bug也能簡單的替換掉該函式,所有程式都可以即時享受到這樣的改變,也是最為常見的函式型態。

  5. 動態函式(dynamic libraries)
  6. 動態函式跟共享函式非常類似,唯一的差別在於程式執行時期並不會去檢查該函式是否存在,而是程式執行到某功能時才進行檢查。這種動態載入的技術最常用在瀏覽器或是大型程式的外掛程式,當有需要用到這個功能時才載入進來。
    製作一個動態函式比較麻煩。
    gcc -c -fPIC operator.c
    gcc -shared operator.o -o liboperator.so
    其中的-fPIC是產生position-independent code,也可以用-fpic。詳細的用法已經超過筆者的理解範圍,撰寫呼叫動態函式的程式碼也需要傳入相關參數。關於更多dynamic libraries的用法請參考這裡

邁向Makefile之路
說了這麼多,尚未觸碰到Makefile本身。以上的範例如果都夠清楚了,接下來的Makefile才能夠繼續學習下去喔!
以下是Makefile的基本組成:

  1. # comments
  2. #一些變數宣告
  3.  
  4. target1: dependencies of target1
  5. <TAB>command;
  6. target2: dependencies of target2
  7. <TAB>command;
  8. ...
  9. ...
  10. clean:
  11. <TAB>rm -rf *.o      
需注意command那行前面必須是一個tab鍵,不能是tab鍵以外的任何空格。
用第一個簡單的範例來說明:假設你需要執行
gcc main.c foo1.c foo2.c -o main
才能編譯出main這隻程式,則Makefile會像是:
  1. #example 1:
  2. #usage: make main OR make
  3.        
  4. main: main.o foo1.o        
  5.     gcc main.o foo1.o -o main  
  6. main.o: main.c             
  7.     gcc main.c -c          
  8. foo1.o: foo1.c             
  9.     gcc foo1.c -c          
  10. clean:                 
  11.     rm -rf main.o foo1.o       
make讀取此Makefile的流程如下:
  1. 由於沒有變數宣告的部份,程式進入點為line 4,target即為main。
  2. main需要main.o跟foo1.o這兩個目的檔;如果gcc找得到這兩個目的檔,才會開始執行line 5的命令。
    很不巧,gcc無法找到這兩個檔案(因為還沒有編譯過!),因此gcc會尋找第一個dependency,也就是main.o,接續line 6。

  3. 到了line 6找到了main.o,他的dependency是main.c。
  4. main.c就在這個目錄下,因此gcc終於可以執行第一個command(也就是line 7),產生main.o並回到line 4

  5. 有了main.o,gcc會回到line 4繼續尋找第二個dependency--foo1.o:
  6. 於是進入line 8,找到了foo1.c,執行line 9的命令產生了foo1.o。
  7. 很高興的再次回到line 4,發現此時所有dependencies都滿足了,終於可以開始進行真正的鏈結工作,也就是line 5,把所有的obj鏈結成main這隻程式。

這個例子裡,make的效果等同於make main;可以不用指定main的原因是make會預設讀第一個target。假設你輸入make foo1.o,當然就只會執行line 8這行命令。 如果程式碼稍作修改,則編譯出來的obj檔也會有所不同(這是一個標準的廢話>.<);此時有必要先清除某些(或全部的obj檔)。
如果我們下一個make clean的指令,則程式會跑到line 10;發現clean這個target並沒有dependency,而且也沒有clean這個檔案,此時這個項目稱為假項目(fake entry)。沒有相依的檔案,因此可以快樂的執行line 11,把main.o跟foo1.o刪除。

第二個範例:假設你需要執行
gcc main.c foo.c clean.c -I /usr/foo/include -lpthread -L /usr/foo/lib -O3 -ansi -o main
,且目錄下包含target.h才能編譯出main這隻程式,則Makefile會像是:
  1. #example 2
  2. #usage: make main OR make
  3. CC = gcc                   #欲使用的C compiler
  4. CFLAGS = -O3 -ansi         #欲使用的參數
  5. INC = -I /usr/foo/include   #include headers的位置
  6. LIB = -L /usr/foo/lib       #include libraries的位置
  7.        
  8. main: main.o foo1.o                    
  9.     ${CC} main.o foo1.o ${CFLAGS} ${INC} ${LIB} -o main
  10. main.o: main.c target.h                    
  11.     ${CC} main.c ${CFLAGS} ${INC} ${LIB} -lpthread -c  
  12. foo1.o: foo1.c target.h                    
  13.     ${CC} foo1.c ${CFLAGS} ${INC} ${LIB} -c        
  14. clean:                             
  15.     @rm -rf *.o                        
這裡宣告了四個變數,在Makefile裡變數可以用$(VAR)或是${VAR}來表示皆可。但是為了跟shell script視覺上有所區隔,我個人建議盡量使用${VAR}來表示。
跟剛剛的Makefile其實是大同小異,只是利用變數使make更加的靈活;執行的流程可參考上一個範例。唯一值得注意的是在line 15的command前我用了一個@符號,這個意思是用來表示不把執行命令輸出到螢幕,僅輸出結果的意思。make預設會把命令跟結果都輸出到螢幕,利用@可簡化輸出,使make的結果更簡潔一點點。

如果你仔細觀察這兩個範例,會發現其實這個檔案本身有太多東西是重複的了。例如line {8,9}重複了main.o, foo1.o;line {10,11}重複了mian.c,而line {12,13}重複了foo1.c。想想這只是一個極小的程式,他的Makefile就要如此巨大,往後如果開發出數百個方法的中型程式,那麼Makefile可能會寫到手軟;更可怕的是程式的架構如果一改變,Makefile寫錯得機會會非常高。

在繼續第三個範例之前,我們來思考一個問題。如果你有foo{1,2,3...100}.c,要把他們寫進Makefile裡,定義targer: depencency然後定義command,加起來總共要兩百行,這實在不是絕妙的方法;make的開發者也想到了這點,因此make有隱含規則(implicit rules):

  main.o: main.c
      gcc main.c -c 
  
可以隱含簡化成
  main.o: main.c
      
  
或者是當你根本沒有定義main.o這個target時,make會自動找main.c來編譯。這是個好消息,但是我們編譯程式通常會夾帶大量參數,光是使用隱含規則是不夠用的;因此我們有需要去自訂一個隱含規則。

第三個範例:
  1. #example 3
  2. #usage: make main OR make
  3. SHELL = /usr/bin/bash         #宣告command所使用的shell環境為bash
  4. CC = gcc                               #欲使用的C compiler
  5. CFLAGS = -O3 -ansi              #欲使用的參數
  6. INC = -I /usr/foo/include  
  7. LIB = -L /usr/foo/lib      
  8.        
  9. .SUFFIXS: .c .cpp .f77 .f       #加入所列副檔名到隱含規則裡
  10. main: main.o foo1.o foo2.o                                    
  11.     ${CC} main.o foo1.o ${CFLAGS} ${INC} ${LIB} -o $@
  12. %.o: %.c target.h                      
  13.     ${CC} $< ${CFLAGS} ${INC} ${LIB} -lpthread -c          
  14. .PHONY: clean
  15. clean:                             
  16.     @rm -rf *.o                                            
這個Makefile看起來開始嚇人了!首先宣告這個Makefile所使用到的command是bash的語法。如果不需告則預設是sh,但是linux的sh就是bash,因此如果你是csh的擁護者,請你一定要宣告她。並且要注意的是,在GNU make裡,變數與變數值之間可以有空格(VAR = value,這個習慣跟csh一樣)也可以沒有空格(VAR=vlaue,這個習慣跟bash一樣);不過如果在其他平台,如Solaris、HPUX或是AIX,很可能要使用具有空格的形式宣告才行。為了Makefile的可移植性,建議使用具有空格的表示方法。

SUFFIXS與PHONY都是變數,代表隱含、內定的target。例如宣告了.c, .cpp, .f77, .f這些副檔名到SUFFIXS變數,是告訴make這些副檔名也要加入隱含規則的行列。事實上,.c, .cpp, .o都已經在make的隱含規則裡了,再次宣告只是為了讓閱讀者更加明確知道這些檔案會被隱含規則處理。而PHONY變數則是讓make知道該target不是某個檔案,只是一個標記。假設make跑到line 15,發現沒有dependency,而工作目錄內恰好有一個clean的檔案,make會認為無條件需求而不去執行我們所要求的clean的動作;為了解決這個極少發生的窘境,細心的開發者還是會把PHONY變數加進Makefile裡。

line 11所出現的$@以及line 13出現的$<稱為自動變數,$@代表target本身,$<代表第一個dependency。line 12大量出現的%則是樣式規則,她就是幫助我們簡化Makefile最好的朋友。
  1. 缺少main.o時,make跳到line 12進行我們所自訂的隱含規則進行編譯:
  2. 此時的%.o就是main.o,%.c就是main.c。line 13的$<代表main.c,執行完畢會產生main.o。

  3. 發現還是缺少foo1.o,make再次跳到line 12:
  4. 此時的%.o就是foo1.o,%.c就是foo1.c。line 13的$<代表foo1.c,執行完畢會產生foo1.o。

  5. 發現還是缺少foo2.o,make再次跳到line 12:
  6. 此時的%.o就是foo2.o,%.c就是foo2.c。line 13的$<代表foo2.c,執行完畢會產生foo2.o。

因此,利用隱含規則,不但可以應付更複雜的架構,也可以使Makefile更容易閱讀、維護。我們來看一個真實的Makefile:
  1. #-----------------------------------------------------------------------------
  2. UP_CC := g++
  3.  
  4. BIN = $(PWD)/../../bin
  5.  
  6. COPT =-g
  7.  
  8. CINC = -I. -I $(PWD)/../PPP -I $(PWD)/../include
  9.  
  10. CLIB = -L $(PWD)/../lib -largtable2 -lfftw3
  11.  
  12. OBJS = gwlCreateAxis.o gwlSignalGen.o gwlCft.o gwlCwt.o gwlConvert.o\
  13.       gwlIwt.o gwlDiffeoLin.o gwlSignalSum.o gwlSignalRead.o gwlCwtMaxLine.o\
  14.       gwlET2D.o gwlET2DFilter.o gwlET3D.o gwlET3DFilter.o gwlDispModel.o\
  15.       gwlDiffeoDisp.o gwlAutoCorr.o gwlTransFK.o gwlOptiSP.o gwlOptiSI.o\
  16.       gwlSignalFilter.o gwlNNpred.o gwlWavelets.o
  17.  
  18. EXE = $(OBJS:.o=)
  19.  
  20. #-----------------------------------------------------------------------------
  21. # clear suffix list and set new one
  22. .SUFFIXES:
  23. .SUFFIXES: .cpp .o
  24.  
  25. #-----------------------------------------------------------------------------
  26. all : shell qwtplot installshell installqwtplot
  27.  
  28. shell : $(OBJS) $(EXE)
  29.  
  30. installshell :
  31.     cp -f $(EXE) $(BIN)
  32.  
  33. qwtplot : gwlPlot.o gwlPlot
  34.  
  35. installqwtplot :
  36.     cp -f gwlPlot $(BIN)
  37.  
  38. gwlPlot.o : gwlPlot.cpp
  39.     $(QTDIR)/bin/moc gwlPlot.cpp -o gwlPlot_moc.cpp;
  40.     $(UP_CC) -c -g -Wno-deprecated gwlPlot.cpp $(CINC) -I $(QTDIR)/include
  41.  
  42. gwlPlot : gwlPlot.o
  43.     $(UP_CC) -g  $@.o $(COPT) -L $(PWD)/../lib -L $(QTDIR)/lib -largtable2 -lfftw3 -lqwt -o $@
  44.  
  45. .cpp.o :
  46.     $(UP_CC) -c -Wno-deprecated $< $(CINC)
  47.  
  48. $(EXE) : $(OBJS)
  49.     $(UP_CC)  $@.o $(COPT) $(CLIB) -o $@
  50.  
  51. clear :
  52.     rm -f $(OBJS) gwlPlot.o
  53.  
  54. clean : clear
  55.     rm -f $(EXE) gwlPlot
  56.  
  57. dependencies.make:
  58.     touch dependencies.make
  59.  
  60. dep:
  61.     $(UP_CC) $(COPT) $(CINC) -MM *.cpp > dependencies.make                                 
    這個Makefile寫得相當的完整且嚴謹,是個很值得我們學習的好範例。首先,作者使用了變數替換(Variable substitution),也就是UP_CC := g++。這個替換的條件是:如果UP_CC沒有被設定值,則自動設定變數值為g++。其次,作者使用了巨集以及巨集變數替換(Macro Variables Substitution)的語法:以此範例來說,是設定一個OBJS的巨集,包含了所有的.o檔。

    巨集變數替換的語法巧妙的使用在定義EXE變數這一行。請注意:=之間的字串:假設我定義了一個巨集JX = foo1.jx foo2.jx foo3.jx foo4.jx,然後我可以利用JAVA = ${JX:.jx=.java}定義foo{1,2,3,4}.java,利用CPP = ${JX:.jx=.cpp}定義foo{1,2,3,4}.cpp這些檔案。因此本例的$(OBJ:.o=)表示EXE巨集的所有檔案都是沒有副檔名的,可以預見他會利用這個技巧來產生binaries -- gwlCreateAxis.o產生gwlCreateAxis,gwlSignalGen.o產生gwlSignalGen,以此類推。

    line 45的.cpp.o :是%.cpp: %.o的縮寫,千萬別誤會這是一個標記或假項目喔!line 48作者自訂了一個隱含方法,與前例的line 12是相同的意義。

    作者很謹慎的利用SUFFIXES(SUFFIXS也適用在GNU make)來定義隱含規則的list。第一個.SUFFIXES:後面沒有接任何東西,表示清空suffix清單;第二個.SUFFIXES:接了.cpp .o兩個副檔名,告訴make只需要關心這兩者就好,其他不必花費心思隱含編譯。只是gcc在編譯時,-I以及-L預設都會把當前目錄包含進去,其實-I. 是多餘的;但是整體的撰寫方式算是嚴謹清晰且流暢!

了解了target與dependency的關係以及隱含規則的符號之後,還剩下一個大重點:command。每行command都是一次獨立的command,彼此毫無關聯性。如果要具有連貫性的command必須寫成連續的一行,例如:

  1. %.o: %.c
  2.     ${CC} $< -c; cd /somewhere; ./configure --prefix $(PWD); rm -rf .libs ${PWD}/share ${PWD}/bin;
如果你寫成
  1. %.o: %.c
  2.     ${CC} $< -c; cd /somewhere;
  3.     ./configure --prefix $(PWD);
  4.     rm -rf .libs ${PWD}/share ${PWD}/bin;
會變成進入到/somewhere之後,整個shell就結束並重新執行./configure --prefix $(PWD)。但是根據原意,$(PWD)是/somewhere而非其他任何路徑,因此整個command會造成預期外的結果。較恰當的寫法應該是:
  1. %.o: %.c
  2.     ${CC} $< -c; cd /somewhere; \
  3.     ./configure --prefix $(PWD); \
  4.     rm -rf .libs ${PWD}/share ${PWD}/bin;
利用跳脫字元把所有command當成同一行,在閱讀上也具有排版的效果。是否發現在Makefile裡我們並沒有定義PWD這個變數值?那是因為Makefile可以直接存取環境變數。但是每個人針對某個程式所設定環境變數不盡相同,如果有使用到非系統面的環境變數,則還是要宣告在Makefile裡比較恰當。例如以下這個不好的真實範例:
  1. CFLAGS = -O ${GMT_INC}
  2.     pssac: pssac.o sacio.o
  3.     $(LINK.c) -o $@ $@.o sacio.o $(GMT_LIBS)
  4.  
  5. clean:
  6.     rm -f pssac *.o
首先,將${VAR}跟$(VAR)混合使用是容易造成混淆的;第二,使用者並不知道什麼是LINK.c,從這個名稱實在無法猜出到底該給她什麼變數值。第三,GMT_INC與GMT_LIBS看起來像是某個程式的headers與libraries路徑,但是要碰巧有個使用者跟作者一樣有相同的環境變數名稱,是很困難的。第四,其實可以利用$<來代替$@.o,因為他是第一個dependency;第五,sacio.o是作者給的,並不是使用者可以自己編譯的。這會造成不同平台的使用者極大的困擾 --即使她所有的變數都辛苦的解決了,但是她的硬體是sparc,若這個sacio.o是linux x86或其他平台上編譯的,那麼這個程式根本就不可能編譯成功。

非常感謝我的老友焜銘給我許多指導、建議以及內容的指正,又提供我一個製作程式碼highlight以及行數的網站,讓我寫範例時能更輕鬆!
quickhighlighter.com
Advanced Syntax Highlighting

本文利用三個虛擬範例以及兩個真實範例來對Makefile作最簡單的介紹,實際上make的工具跟功能實在太多,筆者只能就自己的理解以及能力範圍作簡單的筆記跟介紹。真實範例的作者筆者都懷著十分尊敬的態度,將他們的Makefile作為優劣的教學範例也僅僅只是就事論事,絲毫不減我對這些貢獻自己程式碼的老師與前輩的尊敬之意。當然,這篇文章參考了非常多人的智慧結晶;如果您的Makefile難度超過本篇筆記更是理所當然,也許參考筆者所參考過得文章,對您而言會是個比較省時的選擇:

2010年2月15日 星期一

grub 速記

GNU GRUB(簡稱grub,stands for GRand Unified Bootloader)已經成為大部分linux distributions的預設開機管理程式了。 從前我們可能會討論grub跟lilo應用上的差異,現在幾乎只能看見關於grub跟grub2的討論了。 他的彈性與強大眾所皆知,記得在數年前Solaris OS剛釋出免費的x86版本,以及隨後幾周發布的opensolaris都是用grub來當作預設的開機管理程式。 本文就是簡單對於grub以及其設定檔作非常簡單的筆記。

一個最陽春、不包含註解又能成功開機的grub,其menu.lst(RH系的linux為grub.conf,為menu.lst的一個軟連結),大致如下:

default 0
timeout 8

title Linux Mint 8 Helena
root (hd0,1)
kernel /boot/vmlinuz root=LABEL=ROOT ro
quiet

簡單的說,timeout是倒數秒數;default是倒數結束後預設啟動第幾個entry,不用多說,跟硬碟的代號一樣,從0開始起算。這個範例可以開機的情況相當侷限:
首先,/boot跟/必須在同一個partition。
第二,核心的名稱必須為vmlinuz且位於/boot底下。
第三,/boot必須是ext2/3的檔案系統。

事實上,vmlinuz可以是一個連結。以debian系統來說,你每安裝一個新的kernel之後,她會自動幫你為最新的kernel建一個名稱為vmlinuz的軟連結, 好方便你忘記落落長的核心名稱時,敲個vmlinuz就可以指到正確的核心。就我遇到過的linux而言,似乎沒有一套linux像debian一樣貼心哩。
kernel後面的root並不是指作業系統的root,而是指核心的位置。如同在fstab定義的方式,root可以有三種方式去定義:device ID/UUID/Label。 例如我的/跟/boot在同一個分割區,他的uuid是fa2028ac-0d01-4251-bcd7-07a3df26530d,label是ROOT,裝置代號是/dev/sda2,此時root可以有三種表示方法:

  1. root=/dev/sda2
  2. root=LABEL=ROOT
  3. root=uuid=fa2028ac-0d01-4251-bcd7-07a3df26530d
我個人比較偏好使用Label,因為我經常使用usb外接硬碟開機,啟動時有些機器未必符合我的device.map的設定;uuid雖然是不太可能重複的一組id,但是長度太長,整個檔案顯得很吵雜; label則是一個折衷的好辦法,一個便於自己記憶辨認,又不至於重複性太高,也不會讓設定檔變得難以閱讀。例如台灣人最愛使用的fedora,就是使用LABEL=/1當成指定kernel所在分割區的方法。

但是,對於非ext2/3的filesystems,核心沒有外加模組可是無法辨認這些檔案系統。沒辦法辨認檔案系統,就無法從硬碟裡載入kernel並進行開機程序。因此需要動用到Initial Ram Disk(簡稱initrd,一個在記憶體上運行的虛擬的檔案系統,) 來幫助我們開機。initrd很小,約莫8MB左右,裡面包含核心啟動所需要的硬體模組,將這些modules載入完畢之後會執行真正的init,並根據/etc/inittab的設定展開了一系列的開機流程。也就是說,假如你討厭使用initrd, 你就必須手動編譯你的核心,把這台電腦上所有的硬體驅動全部都編譯到核心裡(當然,最重要的檔案系統一定要記得編譯進去!),這樣就可以不用使用到initrd。如果你不想自己重編你的kernel,你就必須指定initrd的位置:

initrd /boot/initrd.img

initrd.img的名稱是配合kernel名稱的,例如有一個vmlinuz-2.6.31-19-generic的kernel就會有一個initrd.img-2.6.31-19-generic的img。

windows的開機很簡單,不過grub不支援ntfs,所以很自然的利用chianload的方式來做:

title Windows 95/98/NT/2000
root (hd0,0)
makeactive
chainloader +1
其中,root可以用rootnoverify代替,表示讀取此分割區時不校驗。
事實上,chainloader這個grub指令非常好用,不光是用在啟動windows,當年還沒有虛擬機器時,想要用一台電腦玩很多套linux時,每一套linux都會預設把boot loader寫到mbr裡,導致最後你的grub清單會異常複雜, 因為所有找得到的kernel都被寫成兩串。如果你可以把每一套linux的開機管理程式都裝在其boot sector(每個分割區的前512bytes),在透過chianloader的引導,你就可以開到正確的linux,不用擔心某套linux核心更新之後,你要自己手動修改kernel版本號碼。
當然這已經不再是很值得被記憶的好方法,因為用虛擬機器實在是太過方便了。

幾個跟grub有關的commands:

  1. update-grub
  2. 產生一個新的menu.lst。如果檔案已經存在,將會被覆寫過去。

  3. update-initramfs
  4. 利用-c產生一個新的initrd,-u更新一個initrd,-d移除之。
    另外,你也可以用makeinitramfs來產生一個新的initrd;redhat系列則是mkinitrd。

  5. grub-install
  6. 安裝grub到某個位置,例如 grub-install /dev/sdb或是 grub-install '(hd1)'

  7. grub-mkconfig
  8. 產生對應kernel版本的config檔。

  9. grub-md5-crypt
  10. 將grub設定md5加密過的密碼。

  11. grub-mkdevicemap
  12. 產生一個對應當前裝置的device.map檔。

幾個在grub shell裡面的commands:

  1. setup (hdx,y)
  2. 把grub安裝在該partition/disk裡。

  3. find /boot/grub/menu.lst
  4. 藉由find指令,你可以得知kernel或是其他跟grub有關的檔案放的位置,幫助你取得正確的kernel root位置。

  5. map (hd0) (hd1)
    map (hd1) (hd0)
    此map的指令比較像是mirror的味道,在某些很舊的系統(例如DOS)必須要是第一顆硬碟才能開機,此時就可以用map騙過她來啟動系統。

  6. hide (hd0,1)
    隱藏這個partition。通常在擁有多個windows系統的情形下,先安裝的人先取到C drive,後安裝的windows其系統磁區卻只能拿到D,很多軟體跑起來會有問題。
    利用hide指令,可以讓windows看不到其他windows的系統磁區。

  7. lock
    如果某個entry裡面寫了lock,就表示需要密碼才能開機進入。

  8. md5crypt
    效果如同grub-md5-crypt,只是一個是外部命令,一個是grub shell內建命令。

我們用一個簡單的case來試著撰寫一個menu.lst。假設你需要安裝四套作業系統,其中一套windows xp,另一套windows vista,這兩套分別裝在第二顆硬碟的第一、二個主要分割區; Debian跟openSuSE裝在第一顆硬碟的第四、五個分割區。把Debian的grub安裝在MBR,/boot跟/在同個分割區;OpenSuSE的grub則安裝在他自己的boot sector, 但是他的/boot則是位於第六個分割區。假設第二顆硬碟每個分割區都有被指定來使用,沒有跳號問題,預設用Debian開機,倒數5秒並且grub用密碼保護,若要開進任何一個windows都需要輸入密碼才能開機; 此時我們的menu.lst該如何撰寫呢?

default 0
timeout 5
password --md5 $1$qQ7aR/$BAJyOwZW961kWL7vVGAzm

title Debian GNU/Linux, kernel 2.6.26-1-686-bigmem
root (hd0,3)
kernel /boot/vmlinuz-2.6.26-1-686-bigmem root=LABEL=DEBROOT ro quiet
initrd /boot/initrd.img-2.6.26-1-686-bigmem

title Debian GNU/Linux, kernel 2.6.26-1-686-bigmem (single-user mode)
root (hd0,3)
kernel /boot/vmlinuz-2.6.26-1-686-bigmem root=LABEL=DEBROOT ro single
initrd /boot/initrd.img-2.6.26-1-686-bigmem

title Chainload to OpenSuSE 11.2
root (hd0,5)
chainloader +1

title OpenSuSE 11.2 2.6.31.8-0.1-default
root (hd0,4)
kernel /boot/vmlinuz-2.6.31.8-0.1-default root=/dev/sda6 resume=/dev/sda5 splash=silent quiet showopts vga=0x317
initrd /boot/initrd-2.6.31.8-0.1-default

title Windows XP
lock
rootnoverify (hd0,0)
makeactive
hide (hd0,1)
chainloader +1

title Windows vista/Windows 7
lock
rootnoverify (hd0,1)
makeactive
hide (hd0,0)
chainloader +16

這個範例其實很簡單,要開進其他的linux也可以用chainload的方式,並跳進另一套grub或是其開機管理程式。唯一要注意的就是,如果/boot分割區是獨立於/之外,需注意root後面的參數以及kernel裡面的root意義大不相同。

關於這個速記,您可以從鳥哥的網站查到更多關於grub以及開機流程較詳盡的認識。

2010年2月12日 星期五

[轉載] 追求神乎其技的程式設計之道

追求神乎其技的程式設計之道是vgod大在部落格發表的一系列文章,並計畫出版成書。點閱了vgod是誰?才赫然發現原來vgod也是台中一中校友,算算畢業年份也是我學弟呢!學弟現在都在美國唸博班了,我竟然還在這裡不知何時才能出去...

哈,言歸正傳,雖然一直到退伍半年後才開始學寫程式,但是卻激起了對程式設計無比的熱愛。當然,一個好的程式設計師跟優秀的程式設計師其實是差十萬八千里,骨子裡是完全不同面向的東西;我可以寫出一個互動良好,功能完善的小程式,卻無法寫出效能優異的程式,原因在於我們沒有受過好的資料結構及演算法的訓練。作者vgod也參加過國際奧林匹亞資訊組競賽,哈哈,我以前參加地科組連校內資格都沒過呢!

vgod大有很非常值得程式設計師一讀的好文章,也不乏有很多跟我相同的成長經驗(vgod應該小我兩屆^_^),請各位參觀vgod's blog,也可以訂閱他的文章

追求神乎其技的程式設計之道(一)

追求神乎其技的程式設計之道(二)

追求神乎其技的程式設計之道(三)

追求神乎其技的程式設計之道(四)

追求神乎其技的程式設計之道(五)

追求神乎其技的程式設計之道(六)

追求神乎其技的程式設計之道(七)

追求神乎其技的程式設計之道(八)

追求神乎其技的程式設計之道(九)

追求神乎其技的程式設計之道(十)

qing大的程式設計的兩個觀點(1/2)程式設計的兩個觀點(1/2),也是非常好的文章。

當然,JosephJ大也針對vgod大的文章法表了自己的感想,也很值得一讀喔!
吃你自己的狗食、工程師當自強

我們做科學研究的,不可能有時間去把一個程式寫得讓資工背景的專家也能肯定的程式;但是對於我們必須靠撰寫程式來解決問題的人而言,越了解程式語法、進而了解資料結構、演算法的問題,就越能夠讓你的問題在短時間內得到解決。希望有越來越多人一起走入程式設計的美麗世界!