• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            興海北路

            ---男兒仗劍自橫行
            <2025年7月>
            293012345
            6789101112
            13141516171819
            20212223242526
            272829303112
            3456789

            統(tǒng)計(jì)

            • 隨筆 - 85
            • 文章 - 0
            • 評(píng)論 - 17
            • 引用 - 0

            常用鏈接

            留言簿(6)

            隨筆分類

            隨筆檔案

            收藏夾

            全是知識(shí)啊

            搜索

            •  

            最新評(píng)論

            閱讀排行榜

            評(píng)論排行榜

            代碼測(cè)試、調(diào)試與優(yōu)化的小結(jié)

            by falcon<zhangjinw@gmail.com>
            2008-02-29

                代碼寫(xiě)完以后往往要做測(cè)試(或驗(yàn)證)、調(diào)試,可能還要優(yōu)化。
                關(guān)于測(cè)試(或驗(yàn)證),通常對(duì)應(yīng)著兩個(gè)英文單詞verification和validation,在資料[1]中有關(guān)于這個(gè)的定義和一些深入的討論,在資料[2]中,很多人給出了自己的看法。但是我想正如資料[2]提到的:
                “The differences between verification and validation are unimportant except to the theorist; practitioners use the term V&V to refer to all ofthe activities that are aimed at making sure the software will function as required.”
                所以,無(wú)論測(cè)試(或驗(yàn)證)目的都是為了讓軟件的功能能夠達(dá)到需求。測(cè)試和驗(yàn)證通常會(huì)通過(guò)一些形式化(貌似可以簡(jiǎn)單地認(rèn)為有數(shù)學(xué)根據(jù)的)或者非形式化的方法去驗(yàn)證程序的功能是否達(dá)到要求。
                而調(diào)試對(duì)應(yīng)英文debug,debug叫“驅(qū)除害蟲(chóng)”,也許一個(gè)軟件的功能達(dá)到了要求,但是可能會(huì)在測(cè)試或者是正常運(yùn)行時(shí)出現(xiàn)異常,因此需要處理它們。
                關(guān)于優(yōu)化:debug是為了保證程序的正確性,之后就需要考慮程序的執(zhí)行效率,對(duì)于存儲(chǔ)資源受限的嵌入式系統(tǒng),程序的大小也可能是優(yōu)化的對(duì)象。
                很多理論性的東西是在沒(méi)有研究過(guò),暫且不說(shuō)吧。這里只是想把一些需要?jiǎng)邮謱?shí)踐的東西先且記錄和總結(jié)一下,另外很多工具在這里都有提到和羅列,包括Linux內(nèi)核調(diào)試相關(guān)的方法和工具。關(guān)于更詳細(xì)更深入的內(nèi)容還是建議直接看后面的參考資料為妙。

                下面的所有演示在如下環(huán)境下進(jìn)行:

            Quote:

            $ uname -a
            Linux falcon 2.6.22-14-generic #1 SMP Tue Feb 12 07:42:25 UTC 2008 i686 GNU/Linux
            $ echo $SHELL
            /bin/bash
            $ /bin/bash --version | grep bash
            GNU bash, version 3.2.25(1)-release (i486-pc-linux-gnu)
            $ gcc --version | grep gcc
            gcc (GCC) 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)
            $ cat /proc/cpuinfo | grep "model name"
            model name      : Intel(R) Pentium(R) 4 CPU 2.80GHz



            1、代碼測(cè)試

            代 碼測(cè)試有很多方面呢,例如運(yùn)行時(shí)間、函數(shù)調(diào)用關(guān)系圖、代碼覆蓋度、性能測(cè)試(profiling)、內(nèi)存訪問(wèn)越界(segmentation fault)、緩沖區(qū)溢出(stack smashing合法地進(jìn)行非法的內(nèi)存訪問(wèn)?所以很危險(xiǎn))、內(nèi)存泄露(memory leak)等。

            1.1 測(cè)試程序的運(yùn)行時(shí)間 time

            shell提供了內(nèi)置命令time用于測(cè)試程序的執(zhí)行時(shí)間,默認(rèn)顯示結(jié)果包括三部分:實(shí)際花費(fèi)時(shí)間(real time)、用戶空間花費(fèi)時(shí)間(user time)和內(nèi)核空間花費(fèi)時(shí)間(kernel time)。

            Quote:

            $ time pstree 2>&1 >/dev/null

            real    0m0.024s
            user    0m0.008s
            sys     0m0.004s



            time 命令給出了程序本身的運(yùn)行時(shí)間。這個(gè)測(cè)試原理非常簡(jiǎn)單,就是在程序運(yùn)行(通過(guò)system函數(shù)執(zhí)行)前后記錄了系統(tǒng)時(shí)間(用times函數(shù)),然后進(jìn)行求 差就可以。如果程序運(yùn)行時(shí)間很短,運(yùn)行一次看不到效果,可以考慮采用測(cè)試紙片厚度的方法進(jìn)行測(cè)試,類似把很多紙張跌倒一起來(lái)測(cè)試紙張厚度一樣,我們可以讓 程序運(yùn)行很多次。

            如果程序運(yùn)行時(shí)間太長(zhǎng),執(zhí)行效率很低,那么得考慮程序內(nèi)部各個(gè)部分的執(zhí)行情況,從而對(duì)代碼進(jìn)行可能的優(yōu)化。具體可能會(huì)考慮到這兩點(diǎn):

          1. 對(duì)于C語(yǔ)言程序而言,一個(gè)比較宏觀的層次性的輪廓(profile)是函數(shù)調(diào)用圖、函數(shù)內(nèi)部的條件分支構(gòu)成的語(yǔ)句塊,然后就是具體的語(yǔ)句。把握好這樣一個(gè) 輪廓后,就可以有針對(duì)性地去關(guān)注程序的各個(gè)部分,包括哪些函數(shù)、哪些分支、哪些語(yǔ)句最值得關(guān)注(執(zhí)行次數(shù)越多越值得優(yōu)化,術(shù)語(yǔ)叫hotspots)。

          2. 對(duì)于Linux下的程序而言,程序運(yùn)行時(shí)涉及到的代碼會(huì)涵蓋兩個(gè)空間,即用戶空間和內(nèi)核空間。由于這兩個(gè)空間涉及到地址空間的隔離,在測(cè)試或調(diào)試時(shí),可能 涉及到兩個(gè)空間的工具。前者絕大多數(shù)是基于gcc的特定參數(shù)和系統(tǒng)的ptrace調(diào)用,而后者往往實(shí)現(xiàn)為內(nèi)核的補(bǔ)丁,它們?cè)谠砩峡赡茴愃疲珜?shí)際操作時(shí) 后者顯然會(huì)更麻煩,不過(guò)如果你不去hack內(nèi)核,那么往往無(wú)須關(guān)心后者。

            1.2 函數(shù)調(diào)用關(guān)系圖 calltree

            calltree可以非常簡(jiǎn)單方便地反應(yīng)一個(gè)項(xiàng)目的函數(shù)調(diào)用關(guān)系圖,雖然諸如gprof這樣的工具也能做到,不過(guò)如果僅僅要得到函數(shù)調(diào)用圖,calltree應(yīng)該是更好的選擇。如果要產(chǎn)生圖形化的輸出可以使用它的-dot參數(shù)也可以參考資料[12]。從這里可以下載到它,ftp://ftp.berlios.de/pub/calltree/calltree-2.3.tar.bz2
            關(guān)于calltree的實(shí)現(xiàn)原理,可以參考資料[13],關(guān)于它的詳細(xì)用法請(qǐng)參考資料[14]或者它的-h參數(shù)獲取幫助。

            這里是一份演示結(jié)果,
            Quote:

            $ calltree -b -np -m *.c
            main:
            |   close
            |   commitchanges
            |   |   err
            |   |   |   fprintf
            |   |   ferr
            |   |   ftruncate
            |   |   lseek
            |   |   write
            |   ferr
            |   getmemorysize
            |   modifyheaders
            |   open
            |   printf
            |   readelfheader
            |   |   err
            |   |   |   fprintf
            |   |   ferr
            |   |   read
            |   readphdrtable
            |   |   err
            |   |   |   fprintf
            |   |   ferr
            |   |   malloc
            |   |   read
            |   truncatezeros
            |   |   err
            |   |   |   fprintf
            |   |   ferr
            |   |   lseek
            |   |   read$



            這 樣一份結(jié)果對(duì)于“反向工程”應(yīng)該會(huì)很有幫助,它能夠呈現(xiàn)一個(gè)程序的大體結(jié)構(gòu),對(duì)于閱讀和分析源代碼來(lái)說(shuō)是一個(gè)非常好的選擇。雖然cscope和ctags 也能夠提供一個(gè)函數(shù)調(diào)用的“即時(shí)”(在編輯vim的過(guò)程中進(jìn)行調(diào)用)視圖(view),但是calltree卻給了我們一個(gè)宏觀的視圖。

            不過(guò)這樣一個(gè)視圖只涉及到用戶空間的函數(shù),如果想進(jìn)一步給出內(nèi)核空間的宏觀視圖,那么strace和KFT就可以發(fā)揮它們的作用。關(guān)于這兩個(gè)工具請(qǐng)參考條目[11]列出的相關(guān)資料。另外,該視圖也沒(méi)有給出庫(kù)中的函數(shù),如果要跟蹤呢?需要ltrace工具。

            另外,我們發(fā)現(xiàn),calltree僅僅給出了一個(gè)程序的函數(shù)調(diào)用視圖,而沒(méi)有告訴我們各個(gè)函數(shù)的執(zhí)行次數(shù)等情況。如果要關(guān)注這些呢?我們有g(shù)prof。

            1.3 性能測(cè)試工具 gprof & kprof

            參考資料[3]詳細(xì)介紹了這個(gè)工具的用法,這里僅挑選其中一個(gè)例子來(lái)演示。gprof是一個(gè)命令行的工具,而KDE桌面環(huán)境下的kprof則給出了圖形化的輸出,這里僅演示前者。

            首先來(lái)看一段代碼(來(lái)自資料[3]),算Fibonacci數(shù)列的,


            Code:

            [Ctrl+A Select All]



            通過(guò)calltree看看這段代碼的視圖,
            Quote:

            $ calltree -b -np -m *.c
            main:
            |   fibonacci
            |   |   fibonacci ....
            |   printf



            可以看出程序主要涉及到一個(gè)fibonacci函數(shù),這個(gè)函數(shù)遞歸調(diào)用自己。為了能夠使用gprof,需要編譯時(shí)加上-pg選項(xiàng),讓gcc加入相應(yīng)的調(diào)試信息以便gprof能夠產(chǎn)生函數(shù)執(zhí)行情況的報(bào)告。
            Quote:

            $ gcc -pg -o fib fib.c
            $ ls
            fib  fib.c



            運(yùn)行程序并查看執(zhí)行時(shí)間,
            Quote:

            $  time ./fib
            fibonnaci(0) = 0
            fibonnaci(1) = 1
            fibonnaci(2) = 1
            fibonnaci(3) = 2
            ...
            fibonnaci(41) = 165580141
            fibonnaci(42) = 267914296

            real    1m25.746s
            user    1m9.952s
            sys     0m0.072s
            $ ls
            fib  fib.c  gmon.out


            上面僅僅選取了部分執(zhí)行結(jié)果,程序運(yùn)行了1分多鐘,代碼運(yùn)行以后產(chǎn)生了一個(gè)gmon.out文件,這個(gè)文件可以用于gprof產(chǎn)生一個(gè)相關(guān)的性能報(bào)告。
            Quote:

            $ gprof  -b ./fib gmon.out
            Flat profile:

            Each sample counts as 0.01 seconds.
              %   cumulative   self              self     total          
             time   seconds   seconds    calls  ms/call  ms/call  name   
             96.04     14.31    14.31       43   332.80   332.80  fibonacci
              4.59     14.99     0.68                             main


                                    Call graph


            granularity: each sample hit covers 2 byte(s) for 0.07% of 14.99 seconds

            index % time    self  children    called     name
                                                             <spontaneous>
            [1]    100.0    0.68   14.31                 main [1]
                           14.31    0.00      43/43          fibonacci [2]
            -----------------------------------------------
                                         2269806252             fibonacci [2]
                           14.31    0.00      43/43          main [1]
            [2]     95.4   14.31    0.00      43+2269806252 fibonacci [2]
                                         2269806252             fibonacci [2]
            -----------------------------------------------


            Index by function name

               [2] fibonacci               [1] main


            從 這份結(jié)果中可觀察到程序中每個(gè)函數(shù)的執(zhí)行次數(shù)等情況,從而找出值得修改的函數(shù)。在對(duì)某些部分修改之后,可以再次比較程序運(yùn)行時(shí)間,查看優(yōu)化結(jié)果。另外,這 份結(jié)果還包含一個(gè)特別有用的東西,那就是程序的動(dòng)態(tài)函數(shù)調(diào)用情況,即程序運(yùn)行過(guò)程中實(shí)際執(zhí)行過(guò)的函數(shù),這和calltree產(chǎn)生的靜態(tài)調(diào)用樹(shù)有所不同,它 能夠反應(yīng)程序在該次執(zhí)行過(guò)程中的函數(shù)調(diào)用情況。而如果想反應(yīng)程序運(yùn)行的某一時(shí)刻調(diào)用過(guò)的函數(shù),可以考慮采用gdb的backtrace命令。

            類似測(cè)試紙片厚度的方法,gprof也提供了一個(gè)統(tǒng)計(jì)選項(xiàng),用于對(duì)程序的多次運(yùn)行結(jié)果進(jìn)行統(tǒng)計(jì)。另外,gprof有一個(gè)KDE下圖形化接口kprof,這兩部分請(qǐng)參考資料[3]。

            gprof雖然給出了函數(shù)級(jí)別的執(zhí)行情況,但是如果想關(guān)心具體哪些條件分支被執(zhí)行到,哪些語(yǔ)句沒(méi)有被執(zhí)行,該怎么辦?

            1.4 代碼覆蓋率測(cè)試 gcov & ggcov

            如果要使用gcov,在編譯時(shí)需要加上這兩個(gè)選項(xiàng) -fprofile-arcs -ftest-coverage,這里直接用之前的fib.c做演示。

            Quote:

            $ ls
            fib.c
            $ gcc -fprofile-arcs -ftest-coverage -o fib fib.c
            $ ls
            fib  fib.c  fib.gcno



            運(yùn)行程序,并通過(guò)gcov分析代碼的覆蓋度
            Quote:

            $ ./fib
            $ gcov fib.c
            File 'fib.c'
            Lines executed:100.00% of 12
            fib.c:creating 'fib.c.gcov'



            12行代碼100%被執(zhí)行到,再查看分支情況,
            Quote:

            $ gcov -b fib.c
            File 'fib.c'
            Lines executed:100.00% of 12
            Branches executed:100.00% of 6
            Taken at least once:100.00% of 6
            Calls executed:100.00% of 4
            fib.c:creating 'fib.c.gcov'



            發(fā) 現(xiàn)所有函數(shù),條件分支和語(yǔ)句都被執(zhí)行到,說(shuō)明代碼的覆蓋率很高,不過(guò)資料[3]gprof的演示顯示代碼的覆蓋率高并不一定說(shuō)明代碼的性能就好,因?yàn)槟切? 被覆蓋到的代碼可能能夠被優(yōu)化成性能更高的代碼。那到底那些代碼值得被優(yōu)化呢?執(zhí)行次數(shù)最多的,另外,有些分支雖然都覆蓋到了,但是這個(gè)分支的位置可能并 不是理想的,如果一個(gè)分支的內(nèi)容被執(zhí)行的次數(shù)很多,那么把它作為最后一個(gè)分支的話就會(huì)浪費(fèi)很多不必要的比較時(shí)間。因此,通過(guò)覆蓋率測(cè)試,可以嘗試著剔除那 些從未執(zhí)行過(guò)的代碼,通過(guò)性能測(cè)試,可以找出那些值得優(yōu)化的函數(shù)、分支或者是語(yǔ)句。

            如果使用-fprofile-arcs -ftest-coverage參數(shù)編譯完代碼,可以接著用-fbranch-probabilities參數(shù)對(duì)代碼進(jìn)行編譯,這樣,編譯器就可以對(duì)根據(jù)代碼的分支測(cè)試情況進(jìn)行優(yōu)化。

            Quote:

            $ wc -c fib
            16333 fib
            $ ls fib.gcda  #確保fib.gcda已經(jīng)生成,這個(gè)是運(yùn)行fib后的結(jié)果,-fbranch-probabilities一來(lái)它
            fib.gcda
            $ gcc -fbranch-probabilities -o fib fib.c #再次運(yùn)行
            $ wc -c fib
            6604 fib
            $ time ./fib
            ...
            real    0m21.686s
            user    0m18.477s
            sys     0m0.008s



            可見(jiàn)代碼量減少了,而且執(zhí)行效率會(huì)有所提高,當(dāng)然,這個(gè)代碼效率的提高可能還跟其他因素有關(guān),比如gcc還優(yōu)化了一些很平臺(tái)相關(guān)的指令。

            如 果想看看代碼中各行被執(zhí)行的情況,可以直接看fib.c.gcov文件。這個(gè)文件的各列依次表示執(zhí)行次數(shù)、行號(hào)和該行的源代碼。次數(shù)有三種情況,如果一直 沒(méi)有執(zhí)行,那么用####表示;如果該行注釋、函數(shù)聲明等,用-表示;如果是純粹的代碼行,那么用執(zhí)行次數(shù)表示。這樣我們就可以直接分析每一行的執(zhí)行情 況。

            gprof也有一個(gè)圖形化接口ggprof,是基于gtk+的,適合Gnome桌面的用戶。

            現(xiàn)在都已經(jīng)關(guān)注到代碼行 了,實(shí)際上優(yōu)化代碼的前提是保證代碼的正確性,如果代碼還有很多bug,那么先要debug。不過(guò)下面的這些"bug"用普通的工具確實(shí)不太方便,雖然可 能,不過(guò)這里還是把它們歸結(jié)為測(cè)試的內(nèi)容,并且這里剛好承接上gcov部分,gcov能夠測(cè)試到每一行的代碼覆蓋情況,而無(wú)論是內(nèi)存訪問(wèn)越界、緩沖區(qū)溢出 還是內(nèi)存泄露,實(shí)際上是發(fā)生在具體的代碼行上的。

            1.5 內(nèi)存訪問(wèn)越界 catchesegv, libSegFault.so

            "segmentation fault"是很頭痛的一個(gè)問(wèn)題,估計(jì)“糾纏”過(guò)很多人。這里僅僅演示通過(guò)catchsegv腳本測(cè)試段錯(cuò)誤的方法,其他方法見(jiàn)資料[15]。

            catchsegv利用系統(tǒng)動(dòng)態(tài)鏈接的PRELOAD機(jī)制(請(qǐng)參考man ld-linux),把庫(kù)/lib/libSegFault.so提前l(fā)oad到內(nèi)存中,然后通過(guò)它檢查程序運(yùn)行過(guò)程中的段錯(cuò)誤。

            Quote:

            $ cat test.c
            #include <stdio.h>

            int main(void)
            {
                    char str[10];

                    sprintf(str, "%s", 111);

                    printf("str = %s\n", str);
                    return 0;
            }
            $ make test
            $ LD_PRELOAD=/lib/libSegFault.so ./test  #等同于catchsegv ./test
            *** Segmentation fault
            Register dump:

             EAX: 0000006f   EBX: b7eecff4   ECX: 00000003   EDX: 0000006f
             ESI: 0000006f   EDI: 0804851c   EBP: bff9a8a4   ESP: bff9a27c

             EIP: b7e1755b   EFLAGS: 00010206

             CS: 0073   DS: 007b   ES: 007b   FS: 0000   GS: 0033   SS: 007b

             Trap: 0000000e   Error: 00000004   OldMask: 00000000
             ESP/signal: bff9a27c   CR2: 0000006f

            Backtrace:
            /lib/libSegFault.so[0xb7f0604f]
            [0xffffe420]
            /lib/tls/i686/cmov/libc.so.6(vsprintf+0x8c)[0xb7e0233c]
            /lib/tls/i686/cmov/libc.so.6(sprintf+0x2e)[0xb7ded9be]
            ./test[0x804842b]
            /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe0)[0xb7dbd050]
            ./test[0x8048391]
            ...



            從 結(jié)果中可以看出,代碼的sprintf有問(wèn)題。經(jīng)過(guò)檢查發(fā)現(xiàn)它把整數(shù)當(dāng)字符串輸出,對(duì)于字符串的輸出,需要字符串的地址作為參數(shù),而這里的111則剛好被 解釋成了字符串的地址,因此sprintf試圖訪問(wèn)111這個(gè)地址,從而發(fā)生了非法訪問(wèn)內(nèi)存的情況,出現(xiàn)"segmentation fault"。

            1.6 緩沖區(qū)溢出 libsafe.so

            緩 沖區(qū)溢出是指棧溢出(stack smashing),通常發(fā)生在對(duì)函數(shù)內(nèi)的局部變量進(jìn)行賦值操作時(shí),超出了該變量的字節(jié)長(zhǎng)度而引起對(duì)棧內(nèi)原有數(shù)據(jù)(比如eip,ebp等)的覆蓋,從而引 發(fā)內(nèi)存訪問(wèn)越界,甚至執(zhí)行非法代碼,導(dǎo)致系統(tǒng)崩潰。關(guān)于緩沖區(qū)的詳細(xì)原理和實(shí)例分析見(jiàn)資料[16]。這里僅僅演示該資料中提到的一種用于檢查緩沖區(qū)溢出的 方法,它同樣采用動(dòng)態(tài)鏈接的PRELOAD機(jī)制提前裝載一個(gè)名叫l(wèi)ibsafe.so的庫(kù),你可以從這里獲取它,http://www.sfr-fresh.com/linux/misc/libsafe-2.0-16.tgz,下載下來(lái)以后,再解壓,編譯,得到libsafe.so,

            下面,演示一個(gè)非常簡(jiǎn)單的,但可能存在緩沖區(qū)溢出的代碼,并演示libsafe.so的用法。
            Quote:

            $ cat test.c
            $ make test
            $ LD_PRELOAD=/path/to/libsafe.so ./test ABCDEFGHIJKLMN
            ABCDEFGHIJKLMN
            *** stack smashing detected ***: ./test terminated
            Aborted (core dumped)



            資 料[6]分析到,如果不能夠?qū)彌_區(qū)溢出進(jìn)行有效的處理,可能會(huì)存在很多潛在的危險(xiǎn)。雖然libsafe.so采用函數(shù)替換的方法能夠進(jìn)行對(duì)這類 stack smashing進(jìn)行一定的保護(hù),但是無(wú)法根本解決問(wèn)題,alert7大蝦在資料[17]中提出了突破它的辦法,資料[18]提出了另外一種保護(hù)機(jī)制。

            1.7 內(nèi)存泄露 Memwatch, Valgrind, mtrace

            堆 棧通常會(huì)被弄在一起叫,不過(guò)這兩個(gè)名詞卻是指進(jìn)程的內(nèi)存映像中的兩個(gè)不同的部分,棧(stack)用于函數(shù)的參數(shù)傳遞、局部變量的存儲(chǔ)等,是系統(tǒng)自動(dòng)分配 和回收的;而堆(heap)則是用戶通過(guò)malloc等方式申請(qǐng)而且需要用戶自己通過(guò)free釋放的,如果申請(qǐng)的內(nèi)存沒(méi)有釋放,那么將導(dǎo)致內(nèi)存泄露,進(jìn)而 可能導(dǎo)致堆的空間被用盡;而如果已經(jīng)釋放的內(nèi)存再次被釋放(double-free)則也會(huì)出現(xiàn)非法操作。(如果要真正理解堆和棧的區(qū)別,需要理解進(jìn)程的 內(nèi)存映像,請(qǐng)參考資料[22])

            這里演示通過(guò)Memwatch來(lái)檢測(cè)程序中可能存在內(nèi)存泄露,你可以從這里下載到這個(gè)工具,http://www.linkdata.se/sourcecode.html
            使用這個(gè)工具的方式很簡(jiǎn)單,只要把它鏈接(ld)到你的可執(zhí)行文件中去,并在編譯時(shí)加上兩個(gè)宏開(kāi)關(guān)-DMEMWATCH -DMW_STDIO。這里演示一個(gè)簡(jiǎn)單的例子。

            Quote:

            $ cat test.c
            #include <stdlib.h>
            #include <stdio.h>
            #include "memwatch.h"

            int main(void)
            {
              char *ptr1;
              char *ptr2;

              ptr1 = malloc(512);
              ptr2 = malloc(512);

              ptr2 = ptr1;
              free(ptr2);
              free(ptr1);
            }
            $ gcc -DMEMWATCH -DMW_STDIO test.c memwatch.c -o test
            $ cat memwatch.log
            ============= MEMWATCH 2.71 Copyright (C) 1992-1999 Johan Lindh =============

            Started at Sat Mar  1 07:34:33 2008

            Modes: __STDC__ 32-bit mwDWORD==(unsigned long)
            mwROUNDALLOC==4 sizeof(mwData)==32 mwDataSize==32

            double-free: <4> test.c(15), 0x80517e4 was freed from test.c(14)

            Stopped at Sat Mar  1 07:34:33 2008

            unfreed: <2> test.c(11), 512 bytes at 0x8051a14         {FE FE FE FE FE FE FE FE FE FE FE FE FE FE FE FE ................}

            Memory usage statistics (global):
             N)umber of allocations made: 2
             L)argest memory usage      : 1024
             T)otal of all alloc() calls: 1024
             U)nfreed bytes totals      : 512



            通過(guò)測(cè)試,可以看到有一個(gè)512字節(jié)的空間沒(méi)有被釋放,而另外512字節(jié)空間卻被連續(xù)釋放兩次(double-free)。valgrind和mtrace也可以做類似的工作,請(qǐng)參考資料[4]和mtrace的手冊(cè)。

            2、代碼調(diào)試

            調(diào)試的方法很多,調(diào)試往往要跟蹤代碼的運(yùn)行狀態(tài),printf是最基本的辦法,然后呢?靜態(tài)調(diào)試方法有哪些,非交互的呢?非實(shí)時(shí)的有哪些?實(shí)時(shí)的呢?用于調(diào)試內(nèi)核的方法有哪些?有哪些可以用來(lái)調(diào)試匯編代碼呢?

            2.1 靜態(tài)調(diào)試:printf + gcc -D(打印程序中的變量)

            利 用gcc的宏定義開(kāi)關(guān)(-D)和printf函數(shù)可以跟蹤程序中某個(gè)位置的狀態(tài),這個(gè)狀態(tài)包括當(dāng)前一些變量和寄存器的值。調(diào)試時(shí)需要用-D開(kāi)關(guān)進(jìn)行編譯, 在正式發(fā)布程序時(shí)則可把-D開(kāi)關(guān)去掉。這樣做比單純用printf方便很多,它可以避免清理調(diào)試代碼以及由此帶來(lái)的誤刪除代碼等問(wèn)題。

            Quote:

            $ cat test.c
            #include <stdio.h>
            #include <unistd.h>

            int main(void)
            {
                    int i = 0;

            #ifdef DEBUG
                    printf("i = %d\n", i);

                    int t;
                    __asm__ __volatile__ ("movl %%ebp, %0;":"=r"(t)::"%ebp");
                    printf("ebp = 0x%x\n", t);
            #endif

                    _exit(0);
            }
            $ gcc -DDEBUG -g -o test test.c
            $ ./test
            i = 0
            ebp = 0xbfb56d98



            上面演示了如何跟蹤普通變量和寄存器變量的辦法。跟蹤寄存器變量采用了內(nèi)聯(lián)匯編,關(guān)于Linux下的匯編語(yǔ)言開(kāi)發(fā)請(qǐng)參考資料[19]。

            不 過(guò),這種方式不夠靈活,我們無(wú)法“即時(shí)”獲取程序的執(zhí)行狀態(tài),而gdb等交互式調(diào)試工具不僅解決了這樣的問(wèn)題,而且通過(guò)把調(diào)試器拆分成調(diào)試服務(wù)器和調(diào)試客 戶端適應(yīng)了嵌入式系統(tǒng)的調(diào)試,另外,通過(guò)預(yù)先設(shè)置斷點(diǎn)以及斷點(diǎn)處需要收集的程序狀態(tài)信息解決了交互式調(diào)試不適應(yīng)實(shí)時(shí)調(diào)試的問(wèn)題。

            2.2 交互式的調(diào)試(動(dòng)態(tài)調(diào)試):gdb(支持本地和遠(yuǎn)程)/ald(匯編指令級(jí)別的調(diào)試)

            2.2.1 嵌入式系統(tǒng)調(diào)試方法 gdbserver/gdb

            估計(jì)大家已經(jīng)非常熟悉GDB(Gnu DeBugger)了,所以這里并不介紹常規(guī)的gdb用法,而是介紹它的服務(wù)器/客戶(gdbserver/gdb)調(diào)試方式。這種方式非常適合嵌入式系統(tǒng)的調(diào)試,為什么呢?先來(lái)看看這個(gè):

            Quote:

            $ wc -c /usr/bin/gdbserver
            56000 /usr/bin/gdbserver
            $ which gdb
            /usr/bin/gdb
            $ wc -c /usr/bin/gdb
            2557324 /usr/bin/gdb
            $ echo "(2557324-56000)/2557324"  | bc -l
            .97810210986171482377


            gdb 比gdbserver大了將近97%,如果把整個(gè)gdb搬到存儲(chǔ)空間受限的嵌入式系統(tǒng)中是很不合適的,不過(guò)僅僅5K左右的gdbserver即使在只有 8M Flash卡的嵌入式系統(tǒng)中也都足夠了。所以在嵌入式開(kāi)發(fā)中,我們通常先在本地主機(jī)上交叉編譯好gdbserver/gdb。

            如果是初次使用這種方法,可能會(huì)遇到麻煩,而麻煩通常發(fā)生在交叉編譯gdb和gdbserver時(shí)。在編譯gdbserver/gdb前,需要配置(./configure)兩個(gè)重要的選項(xiàng):
          3. --host,指定gdb/gdbserver本身的運(yùn)行平臺(tái),
          4. --target,指定gdb/gdbserver調(diào)試的代碼所運(yùn)行的平臺(tái),
            關(guān) 于運(yùn)行平臺(tái),通過(guò)$MACHTYPE環(huán)境變量就可獲得,對(duì)于gdbserver,因?yàn)橐阉鼜?fù)制到嵌入式目標(biāo)系統(tǒng)上,并且用它來(lái)調(diào)試目標(biāo)平臺(tái)上的代碼,因 此需要把--host和--target都設(shè)置成目標(biāo)平臺(tái);而gdb因?yàn)檫€是運(yùn)行在本地主機(jī)上,但是需要用它調(diào)試目標(biāo)系統(tǒng)上的代碼,所以需要把-- target設(shè)置成目標(biāo)平臺(tái)。

            編譯完以后就是調(diào)試,調(diào)試時(shí)需要把程序交叉編譯好,并把二進(jìn)制文件復(fù)制一份到目標(biāo)系統(tǒng)上,并在本地需要保留一份源代碼文件。調(diào)試過(guò)程大體如下,首先在目標(biāo)系統(tǒng)上啟動(dòng)調(diào)試服務(wù)器:

            Quote:

            $ gdbserver :port /path/to/binary_file
            ...



            然后在本地主機(jī)上啟動(dòng)gdb客戶端鏈接到gdb調(diào)試服務(wù)器,(gdbserver_ipaddress是目標(biāo)系統(tǒng)的IP地址,如果目標(biāo)系統(tǒng)不支持網(wǎng)絡(luò),那么可以采用串口的方式,具體看手冊(cè)
            Quote:

            $ gdb
            ...
            (gdb) target remote gdbserver_ipaddress:2345
            ...



            其他調(diào)試過(guò)程和普通的gdb調(diào)試過(guò)程類似。

            2.2.2 匯編代碼的調(diào)試 ald

            用gdb調(diào)試匯編代碼貌似會(huì)比較麻煩,不過(guò)有人正是因?yàn)檫@個(gè)原因而開(kāi)發(fā)了一個(gè)專門(mén)的匯編代碼調(diào)試器,名字就叫做assembly language debugger,簡(jiǎn)稱ald,你可以從這里下載到,http://ald.sourceforge.net/

            下載以后,解壓編譯后,我們來(lái)調(diào)試一個(gè)程序看看。

            這里是一段非常簡(jiǎn)短的匯編代碼,摘自參考資料[20]



            Code:

            [Ctrl+A Select All]



            演示一下,

            Quote:

            //匯編、鏈接、運(yùn)行
            $ as -o test.o test.s
            $ ld -o test test.o
            $ ./test "Hello World"
            Hello World
            //查看程序的入口地址
            $ readelf -h test | grep Entry
              Entry point address:               0x8048054
            //調(diào)試
            $ ald test
            ald> display
            Address 0x8048054 added to step display list
            ald> n
            eax = 0x00000000 ebx = 0x00000000 ecx = 0x00000001 edx = 0x00000000
            esp = 0xBFBFDEB4 ebp = 0x00000000 esi = 0x00000000 edi = 0x00000000
            ds  = 0x007B es  = 0x007B fs  = 0x0000 gs  = 0x0000
            ss  = 0x007B cs  = 0x0073 eip = 0x08048055 eflags = 0x00200292

            Flags: AF SF IF ID

            Dumping 64 bytes of memory starting at 0x08048054 in hex
            08048054:  59 59 59 C6 41 0C 0A 31 D2 B2 0D 31 C0 B0 04 31    YYY.A..1...1...1
            08048064:  DB CD 80 31 C0 40 CD 80 00 2E 73 79 6D 74 61 62    ...1.@....symtab
            08048074:  00 2E 73 74 72 74 61 62 00 2E 73 68 73 74 72 74    ..strtab..shstrt
            08048084:  61 62 00 2E 74 65 78 74 00 00 00 00 00 00 00 00    ab..text........

            08048055                      59                   pop ecx



            可見(jiàn)ald在啟動(dòng)時(shí)就已經(jīng)運(yùn)行了被它調(diào)試的test程序,并且進(jìn)入了程序的入口0x8048054,緊接著單步執(zhí)行時(shí),就執(zhí)行了程序的第一條指令popl ecx。

            ald的命令很少,而且跟gdb很類似,比如
          5. 這個(gè)幾個(gè)命令用法和名字都類似 help,next,continue,set args,break,file,quit,disassemble,enable,disable等
          6. 名字不太一樣,功能對(duì)等的,examine對(duì)x, enter 對(duì) set variable {int}地址=數(shù)據(jù)

            需 要提到的是:linux下的調(diào)試器包括上面的gdb和ald,以及strace等都用到了linux系統(tǒng)提供的ptrace()系統(tǒng)調(diào)用,這個(gè)調(diào)用為用戶 訪問(wèn)內(nèi)存映像提供了便利,如果想自己寫(xiě)一個(gè)調(diào)試器或者想hack一下gdb和ald,那么好好閱讀資料[10]和man ptrace吧。

            2.3 實(shí)時(shí)調(diào)試:gdb tracepoint

            對(duì) 于程序狀態(tài)受時(shí)間影響的程序,用上述普通的設(shè)置斷點(diǎn)的交互式調(diào)試方法并不合適,因?yàn)檫@種方式將由于交互時(shí)產(chǎn)生的通信延遲和用戶輸入命令的時(shí)延而完全改變程 序的行為。所以gdb提出了一種方法以便預(yù)先設(shè)置斷點(diǎn)以及在斷點(diǎn)處需要獲取的程序狀態(tài),從而讓調(diào)試器自動(dòng)執(zhí)行斷點(diǎn)處的動(dòng)作,獲取程序的狀態(tài),從而避免在斷 點(diǎn)處出現(xiàn)人機(jī)交互產(chǎn)生時(shí)延改變程序的行為。

            這種方法叫tracepoints(對(duì)應(yīng)breakpoint),它在gdb的user manual(見(jiàn)資料[21])里頭有詳細(xì)的說(shuō)明,不過(guò)在gdb的官方發(fā)行版中至今都沒(méi)有對(duì)它的實(shí)現(xiàn)。盡管如此,我們還是可以使用它,因?yàn)橛衅渌M織做了 相關(guān)的工作,并以補(bǔ)丁的方式發(fā)布它。這個(gè)補(bǔ)丁你可以從這里獲取ftp://dslab.lzu.edu.cn/pub/gdb_tracepoints

            獲 取這個(gè)補(bǔ)丁以后,要做的就是把它patch到對(duì)應(yīng)的gdb版本中,然后就是編譯。因?yàn)閠racepoints只定義在調(diào)試服務(wù)器和調(diào)試客戶端這種方式中, 因此在這個(gè)實(shí)現(xiàn)中也是這樣,如果想用它,你同樣需要編譯gdbserver和gdb,并類似嵌入式系統(tǒng)中的調(diào)試方法一樣調(diào)試它。

            編譯好以后通過(guò)ftp://dslab.lzu.edu.cn/pub/gdb_tracepoints/paper/tp.pdf和資料[21]就可以使用它。

            2.4 調(diào)試內(nèi)核

            雖然這里并不會(huì)演示如何去hack內(nèi)核,但是相關(guān)的工具還是需要簡(jiǎn)單提到的,資料[11]列出了絕大部分用于內(nèi)核調(diào)試的工具,這些對(duì)你hack內(nèi)核應(yīng)該會(huì)有幫助的。

            3、代碼優(yōu)化

            除了資料[21]中的實(shí)踐之外,我想我“應(yīng)該”沒(méi)有做過(guò)其他的項(xiàng)目?jī)?yōu)化工作吧,所以很遺憾,這里無(wú)法進(jìn)行討論了,不過(guò)我還是找了很多相關(guān)資料的,就讓大家一起分享吧,這些資料都列在條目
          7. 里。

            實(shí)際上呢?“代碼測(cè)試”部分介紹的很多工具是為代碼優(yōu)化服務(wù)的,更多具體的細(xì)節(jié)請(qǐng)參考后面的資料,自己做實(shí)驗(yàn)吧。有任何相關(guān)的感興趣的話題歡迎給我郵件zhangjinw@gmail.com。

            參考資料:

            [1] VERIFICATION AND VALIDATION
            http://satc.gsfc.nasa.gov/assure/agbsec5.txt
            [2] difference between verification and Validation
            http://www.faqs.org/qa/qa-9060.html
            [3] Coverage Measurement and Profiling(覆蓋度測(cè)量和性能測(cè)試,Gcov and Gprof)
            http://www.linuxjournal.com/article/6758
            [4] Valgrind Usage
            A. Valgrind HOWTO
            http://www.faqs.org/docs/Linux-HOWTO/Valgrind-HOWTO.html
            B. Using Valgrind to Find Memory Leaks and Invalid Memory Use
            http://www.cprogramming.com/debugging/valgrind.html
            [5] MEMWATCH
            http://www.linkdata.se/sourcecode.html
            [6] Mastering Linux debugging techniques
            http://www.ibm.com/developerworks/linux/library/l-debug/
            [7] Software Performance Analysis
            http://arxiv.org/pdf/cs.PF/0507073.pdf
            [8] Runtime debugging in embedded systems
            http://dslab.lzu.edu.cn/docs/publications/runtime_debug.pdf
            [9] Tools Provided by System
            ltrace,mtrace,strace
            [10] Write your own debugger with the support ptrace()
            A.
            Process Tracing Using Ptrace
            http://linuxgazette.net/issue81/sandeep.html
            http://linuxgazette.net/issue83/sandeep.html
            B. Playing with ptrace
            http://www.linuxjournal.com/article/6100
            http://www.linuxjournal.com/node/6210/print
            http://www.ecos.sourceware.org/ml/libc-hacker/1998-05/msg00277.html
            [11] Kernel Debugging Relative Tools
            A. KGDB
            http://dslab.lzu.edu.cn/docs/publications/kernel_gdb.pdf
            B. GCOV
            http://linuxdevices.com/files/article062/der_herr_gcov.pdf
            C. KFI & KFT
            http://dslab.lzu.edu.cn/docs/publications/kfi.pdf
            D. UML(User Mode Linux)
            http://dslab.lzu.edu.cn/docs/publications/uml.pdf
            E. GDB Tracepoint
            http://dslab.lzu.edu.cn/docs/publications/tp.pdf
            F. Tools Collections
            http://dslab.lzu.edu.cn/docs/publications/tools.pdf
            G. Linux系統(tǒng)內(nèi)核的調(diào)試
            http://www.ibm.com/developerworks/cn/linux/l-kdb/
            H. 嵌入式Linux內(nèi)核調(diào)試技術(shù)
            http://www.eepw.com.cn/article/73300.htm
            [12] 用Graphviz進(jìn)行可視化操作──繪制函數(shù)調(diào)用關(guān)系圖
            http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1425.html
            [13] 用 Graphviz 可視化函數(shù)調(diào)用
            http://www.ibm.com/developerworks/cn/linux/l-graphvis/
            [14]
            介紹一個(gè)linux下生成C代碼調(diào)用樹(shù)的好工具calltree
            http://www.linuxsir.org/bbs/printthread.php?t=246389
            [15] 可惡的"Segmentation faults"之初級(jí)總結(jié)篇
            http://oss.lzu.edu.cn/blog/article.php?tid_700.html
            [16]
            Linux下緩沖區(qū)溢出攻擊的原理及對(duì)策
            http://www.ibm.com/developerworks/cn/linux/l-overflow/index.html
            [17] 繞過(guò)libsafe的保護(hù)--覆蓋_dl_lookup_versioned_symbol技術(shù)
            http://www.xfocus.net/articles/200208/423.html
            [18] 介紹Propolice怎樣保護(hù)stack-smashing的攻擊
            http://www.xfocus.net/articles/200103/78.html
            [19] Linux 匯編語(yǔ)言開(kāi)發(fā)指南
            http://www.ibm.com/developerworks/cn/linux/l-assembly/index.html
            [20] 為你的可執(zhí)行文件“減肥”
            http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1547.html
            [21] GDB Tracepoints
            http://sourceware.org/gdb/current/onlinedocs/gdb_11.html#SEC84
            [22] C語(yǔ)言程序緩沖區(qū)注入分析(第一部分:進(jìn)程的內(nèi)存映像)
            http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1539.html
          8. Code Optimize Relatie
            A. Optimizing C Code
            http://www.jukie.net/~bart/slides/c-opt/c-opt.ps
            B. Performance programming for scientific computing
            http://www.research.ibm.com/perfprog/course/course.html
            C. Performance Programming
            http://www-cse.ucsd.edu/users/carter/perfprog.html
            D. Linux Profiling and Optimization
            http://www.cs.princeton.edu/picasso/mats/mats_S07/Lucifredi_Lecture_Feb07.pdf
            E. High-level code optimization
            http://web.abo.fi/~mats/codeopt2007/handouts/High-level-opt.pdf
            F. Code Optimization
            http://library.simugraph.com/articles/opti/optimizing.html
          9. posted @ 2008-03-14 15:32 隨意門(mén) 閱讀(1321) | 評(píng)論 (0)編輯 收藏
            動(dòng)態(tài)符號(hào)鏈接的細(xì)節(jié)

                 摘要: by falcon<zhangjinw@gmail.com>2008-02-26        Linux支持動(dòng)態(tài)連接庫(kù),不僅節(jié)省了磁盤(pán)、內(nèi)存空間,而且可以提高程序運(yùn)行效率[1]。不過(guò)引入動(dòng)態(tài)連接庫(kù)也可能會(huì)帶來(lái)很多問(wèn)題,例如動(dòng)態(tài)連接庫(kù)的調(diào)試 [4]、升級(jí)更新[5]和潛在的安全威脅[6][7]。這里主要討論符號(hào)的動(dòng)態(tài)鏈接過(guò)程...  閱讀全文

            posted @ 2008-03-14 15:30 隨意門(mén) 閱讀(815) | 評(píng)論 (0)編輯 收藏
            C語(yǔ)言程序緩沖區(qū)注入的分析(第一部分:進(jìn)程的內(nèi)存映像)

                 摘要: by falcon <zhangjinw@gmail.com>2008-2-13 閑言戲語(yǔ)    最近在寫(xiě)《Shell編程范例之進(jìn)程操作》,到現(xiàn)在也沒(méi)完。本打算介紹進(jìn)程的相關(guān)操作,后面竟寫(xiě)到Linux下的C語(yǔ)言開(kāi)發(fā)過(guò)程,想把文件是怎么變成進(jìn)程 的整個(gè)過(guò)程給全部搗弄一遍。雖然到程序加載以及動(dòng)態(tài)符號(hào)連接都已經(jīng)很理解了,但是這伙卻被進(jìn)程的內(nèi)存映像給...  閱讀全文

            posted @ 2008-03-14 15:29 隨意門(mén) 閱讀(2021) | 評(píng)論 (0)編輯 收藏
            Linux命令行上程序執(zhí)行的那一剎那!

            by falcon<zhangjinw@gmail.com>
            2008-02-15

                (這一小節(jié)應(yīng)該是作為《shell編程范例之進(jìn)程操作》的一些補(bǔ)充性質(zhì)的內(nèi)容。)

                當(dāng)我們?cè)贚inux下的命令行輸入一個(gè)命令之后,這背后發(fā)生了什么?

            1、什么是命令行接口

                用戶使用計(jì)算機(jī)有兩種常見(jiàn)的方式,一種是圖形化的接口(GUI),另外一種則是命令行接口(CLI)。對(duì)于圖形化的接口,用戶點(diǎn)擊某個(gè)圖標(biāo)就可啟動(dòng)后 臺(tái)的某個(gè)程序;對(duì)于命令行的接口,用戶鍵入某個(gè)程序的名字就可啟動(dòng)某個(gè)程序。這兩者的基本過(guò)程是類似的,都需要查找程序文件在磁盤(pán)上的位置,加載到內(nèi)存并 通過(guò)不同的解釋器進(jìn)行解析和運(yùn)行。下面以命令行為例來(lái)介紹程序執(zhí)行那一剎那發(fā)生的一些事情。
                首先來(lái)介紹什么是命令行?命令行就是command line,很直觀的概念就是系統(tǒng)啟動(dòng)后的那個(gè)黑屏幕:有一個(gè)提示符,并有光標(biāo)在閃爍的那樣一個(gè)終端,一般情況下可以用CTRL+ALT+F1-6切換到不同的終端;在GUI界 面下也會(huì)有一些偽終端,看上去和系統(tǒng)啟動(dòng)時(shí)的那個(gè)終端沒(méi)有什么區(qū)別,也會(huì)有一個(gè)提示符,并有一個(gè)光標(biāo)在閃爍。就提示符和響應(yīng)用戶的鍵盤(pán)輸入而言,它們兩者 在功能上是一樣的,實(shí)際上它們就是同一個(gè)東西,你用下面的命令就可以把它們打印出來(lái)。

            Quote:

            $ echo $SHELL   #打印當(dāng)前SHELL,當(dāng)前運(yùn)行的命令行接口程序
            /bin/bash
            $ echo $$   #該程序?qū)?yīng)的進(jìn)程ID,$$是一個(gè)比較特殊的環(huán)境變量,它存放了當(dāng)前進(jìn)程ID
            1481
            $ ps -C bash   #通過(guò)PS命令查看
              PID TTY          TIME CMD
             1481 pts/0    00:00:00 bash


                從上面的操作結(jié)果可以看出,當(dāng)前命令行接口實(shí)際上是一個(gè)程序,那就是/bin/bash,它是一個(gè)實(shí)實(shí)在在的程序,它打印提示符,接受用戶輸入的命令,分 析命令序列并執(zhí)行然后返回結(jié)果。不過(guò)/bin/bash僅僅是當(dāng)前使用的命令行程序之一,還有很多具有類似功能的程序,比如/bin/tcsh, bin/ash等。不過(guò)這里主要來(lái)討論bash了,討論它自己是怎么啟動(dòng)的,它怎么樣處理用戶的輸入命令等后臺(tái)細(xì)節(jié)?

            1.2 /bin/bash是什么時(shí)候啟動(dòng)的

            1.2.1 /bin/bash

               先通過(guò)CTRL+ALT+F1切換到一個(gè)普通終端下面,一般情況下看到的是XXX login: 提示輸入用戶名,接著是提示輸入密碼,然后呢?就直接登錄到了我們的命令 行接口。實(shí)際上正是你輸入正確的密碼后,那個(gè)程序把/bin/bash給啟動(dòng)了。那是什么東西提示"XXX login:"的呢?正是/bin/login程序,那/bin/login程序怎么知道要啟動(dòng)/bin/bash,而不是其他的/bin/tcsh呢?
                /bin/login程序?qū)嶋H上會(huì)檢查我們的/etc/passwd文件,在這個(gè)文件里頭包含了用戶名、密碼和該用戶的登錄shell。密碼和用戶名匹配用戶的登錄,而登錄shell則作為用戶登錄后的命令行程序。看看/etc/passwd中典型的這么一行:
            Quote:

            $ cat /etc/passwd | grep falcon
            falcon:x:1000:1000:falcon,,,:/home/falcon:/bin/bash


                這個(gè)是我用的帳號(hào)的相關(guān)信息哦,看到最后一行沒(méi)?/bin/bash,這正是我登錄用的命令行解釋程序。至于密碼呢,看到那個(gè)x沒(méi)?這個(gè)x說(shuō)明我的密碼被 保存在另外一個(gè)文件里頭/etc/shadow,而且密碼是經(jīng)過(guò)加密的。至于這兩個(gè)文件的更多細(xì)節(jié),看manual吧。
                我們?cè)趺粗绖偤檬?bin/login打印了"XXX login"呢?現(xiàn)在回顧一下很早以前學(xué)習(xí)的那個(gè)strace命令。我們可以用strace命令來(lái)跟蹤/bin/login程序的執(zhí)行。
                跟上面一樣,切換到一個(gè)普通終端,并切換到root用戶,用下面的命令:
            Quote:

            $ strace -f -o strace.out /bin/login


                退出以后就可以打開(kāi)strace.out文件,看看到底執(zhí)行了哪些文件,讀取了哪些文件。從中我們可以看到正是/bin/login程序用execve調(diào) 用了/bin/bash命令。通過(guò)后面的演示,我們發(fā)現(xiàn)/bin/login只是在子進(jìn)程里頭用execve調(diào)用了/bin/bash,因?yàn)樵趩?dòng) /bin/bash后,我們發(fā)現(xiàn)/bin/login并沒(méi)有退出。

            1.2.2 /bin/login

                那/bin/login又是怎么起來(lái)的呢?
                下面再來(lái)看一個(gè)演示。先在一個(gè)可以登陸的終端下執(zhí)行下面的命令。
            Quote:

            $ getty 38400 tty8 linux


                getty命令停留在那里,貌似等待用戶的什么操作,現(xiàn)在切回到第8個(gè)終端,是不是看到有"XXX login:"的提示了。輸入用戶名并登錄,之后退出,回到第一個(gè)終端,發(fā)現(xiàn)getty命令已經(jīng)退出。
                類似地,我們也可以用strace命令來(lái)跟蹤getty的執(zhí)行過(guò)程。在第一個(gè)終端下切換到root用戶。執(zhí)行如下命令,
            Quote:

            $ strace -f -o strace.out getty 38400 tty8 linux


                同樣在strace.out命令中可以找到該命令的相關(guān)啟動(dòng)細(xì)節(jié)。比如,我們可以看到正是getty程序用execve系統(tǒng)調(diào)用執(zhí)行了 /bin/login程序。這個(gè)地方,getty是在自己的主進(jìn)程里頭直接執(zhí)行了/bin/login,這樣/bin/login將把getty的進(jìn)程空 間替換掉。

            1.2.3 /sbin/getty

                這里涉及到一個(gè)非常重要的東西了:/sbin/init,通過(guò)man init你可以查看到該命令的作用,它可是“萬(wàn)物之王”(init  is  the  parent of all processes on the system)哦。它是Linux系統(tǒng)默認(rèn)啟動(dòng)的第一個(gè)程序,負(fù)責(zé)進(jìn)行Linux系統(tǒng)的一些初始化工作,而這些初始化工作的配置則是通過(guò) /etc/inittab來(lái)做的。那么來(lái)看看/etc/inittab的一個(gè)簡(jiǎn)單的example吧,可以通過(guò)man inittab查看相關(guān)幫助。
                整個(gè)配置文件的語(yǔ)法非常簡(jiǎn)單,就是下面一行的重復(fù),
            Quote:

            id:runlevels:action:process


               
          10. id就是一個(gè)唯一的編號(hào),不用管它,一個(gè)名字而言,無(wú)關(guān)緊要。
               
          11. runlevels是運(yùn)行級(jí)別,整個(gè)還是比較重要的,理解運(yùn)行級(jí)別的概念很有必要,它可以有如下的取值,
            Quote:

            0 is halt.
            1 is single-user.
            2-5 are multi-user.
            6 is reboot.


                不過(guò),真正在配置文件里頭用的是1-5了,而0和6非常特別,除了用它作為init命令的參數(shù)關(guān)機(jī)和重啟外,似乎沒(méi)有哪個(gè)“傻瓜”把它寫(xiě)在系統(tǒng)的配置文件 里頭,讓系統(tǒng)啟動(dòng)以后就關(guān)機(jī)或者重啟。1代表單用戶,而2-5則代表多用戶。對(duì)于2-5可能有不同的解釋,比如在slackware 12.0上,2,3,5被用來(lái)作為多用戶模式,但是默認(rèn)不啟動(dòng)X windows(GUI接口),而4則作為啟動(dòng)X windows的運(yùn)行級(jí)別。
               
          12. action是動(dòng)作,它也有很多選擇,我們關(guān)心幾個(gè)常用的
                initdefault 用來(lái)指定系統(tǒng)啟動(dòng)后進(jìn)入的運(yùn)行級(jí)別,通常在/etc/inittab的第一條配置,如
            Quote:

                    id:3:initdefault:


                這個(gè)說(shuō)明默認(rèn)運(yùn)行級(jí)別是3,即多用戶模式,但是不啟動(dòng)X window的那種。
                sysinit 指定那些在系統(tǒng)啟動(dòng)時(shí)將被執(zhí)行的程序,例如
            Quote:

                si:S:sysinit:/etc/rc.d/rc.S
            [quote]
                在man inittab中提到,對(duì)于sysinit,boot等動(dòng)作,runlevels選項(xiàng)是不用管的,所以我們可以很容易解讀這條配置:它的意思是系統(tǒng)啟動(dòng)時(shí) 將默認(rèn)執(zhí)行/etc/rc.d/rc.S文件,在這個(gè)文件里你可直接或者間接的執(zhí)行你想讓系統(tǒng)啟動(dòng)時(shí)執(zhí)行的任何程序,完成系統(tǒng)的初始化。
                wait,當(dāng)進(jìn)入某個(gè)特別的運(yùn)行級(jí)別時(shí),指定的程序?qū)⒈粓?zhí)行一次,init將等到它執(zhí)行完成,例如
            [quote]
            rc:2345:wait:/etc/rc.d/rc.M


                這個(gè)說(shuō)明無(wú)論是進(jìn)入運(yùn)行級(jí)別2,3,4,5中哪一個(gè),/etc/rc.d/rc.M將被執(zhí)行一次,并且有init等待它執(zhí)行完成。
                ctrlaltdel,當(dāng)init程序接收到SIGINT信號(hào)時(shí),某個(gè)指定的程序?qū)⒈粓?zhí)行,我們通常通過(guò)按下CTRL+ALT+DEL,這個(gè)默認(rèn)情況下將 給init發(fā)送一個(gè)SIGINT信號(hào)。如果我們想在按下這幾個(gè)鍵時(shí),系統(tǒng)重啟,那么可以在/etc/inittab中寫(xiě)入,
            Quote:

            ca::ctrlaltdel:/sbin/shutdown -t5 -r now


                respawn,這個(gè)指定的進(jìn)程將被重啟,任何時(shí)候當(dāng)它退出時(shí)。這意味著你沒(méi)有辦法結(jié)束它,除非init自己結(jié)束了。例如,
            Quote:

                c1:1235:respawn:/sbin/agetty 38400 tty1 linux


                這一行的意思非常簡(jiǎn)單,就是系統(tǒng)運(yùn)行在級(jí)別1,2,3,5時(shí),將默認(rèn)執(zhí)行/sbin/agetty程序(這個(gè)類似于上面提到的getty程序),這個(gè)程序非常有意思,就是無(wú)論什么時(shí)候它退出,init將再次啟動(dòng)它。這個(gè)有幾個(gè)比較有意思的問(wèn)題:
                在slackware 12.0下,當(dāng)你把默認(rèn)運(yùn)行級(jí)別修改為4的時(shí)候,只有第6個(gè)終端可以用。原因是什么呢?因?yàn)轭愃粕厦娴呐渲茫驗(yàn)槟抢镏挥?235,而沒(méi)有4,這意味著當(dāng) 系統(tǒng)運(yùn)行在第4級(jí)別時(shí),其他終端下的/sbin/agetty沒(méi)有啟動(dòng)。所以,如果想讓其他終端都可以用,把1235修改為12345即可。
                另外一個(gè)有趣的問(wèn)題就是:正是init程序在讀取這個(gè)配置行以后啟動(dòng)了/sbin/agetty,這就是我們的/sbin/agetty的秘密。
                還有一個(gè)問(wèn)題:無(wú)論我們退出哪個(gè)終端,那個(gè)"XXX login:"總是會(huì)被打印,原因是respawn動(dòng)作有趣的性質(zhì),因?yàn)樗嬖Vinit,無(wú)論/sbin/agetty什么時(shí)候退出,重新把它啟動(dòng)起來(lái), 那跟"XXX login:"有什么關(guān)系呢?從前面的內(nèi)容,我們發(fā)現(xiàn)正是/sbin/getty(同agetty)啟動(dòng)了/bin/login,而/bin/login 有啟動(dòng)了/bin/bash,即我們的命令行程序。
                而init程序作為“萬(wàn)物之王”,它是所有進(jìn)程的“父”(也可能是祖父……)進(jìn)程,那意味著其他進(jìn)程最多只能是它的兒子進(jìn)程。而這個(gè)子進(jìn)程是怎么創(chuàng)建的, fork調(diào)用,而不是之前提到的execve調(diào)用。前者創(chuàng)建一個(gè)子進(jìn)程,后者則會(huì)覆蓋當(dāng)前進(jìn)程。因?yàn)槲覀儼l(fā)現(xiàn)/sbin/getty運(yùn)行時(shí),init并沒(méi) 有退出,因此可以判斷是fork調(diào)用創(chuàng)建一個(gè)子進(jìn)程后,才通過(guò)execve執(zhí)行了/sbin/getty。
                因此,我們可以總結(jié)出這么一個(gè)調(diào)用過(guò)程:
            Quote:

                fork   execve          execve         fork            execve 
            init --> init --> /sbin/getty --> /bin/login --> /bin/login --> /bin/bash


                這里的execve調(diào)用以后,后者將直接替換前者,因此當(dāng)我們鍵入exit退出/bin/bash以后,也就相當(dāng)于/sbin/getty都已經(jīng)結(jié)束了, 因此最前面的init程序判斷/sbin/getty退出了,又會(huì)創(chuàng)建一個(gè)子進(jìn)程把/sbin/getty啟動(dòng),進(jìn)而又啟動(dòng)了/bin/login,又看 到了那個(gè)"XXX login:"。
                通過(guò)ps和pstree命令看看實(shí)際情況是不是這樣,前者打印出進(jìn)程的信息,后者則打印出調(diào)用關(guān)系。
            Quote:

            $ ps -ef | egrep "/sbin/init|/sbin/getty|bash|/bin/login"
            root         1     0  0 21:43 ?        00:00:01 /sbin/init
            root      3957     1  0 21:43 tty4     00:00:00 /sbin/getty 38400 tty4
            root      3958     1  0 21:43 tty5     00:00:00 /sbin/getty 38400 tty5
            root      3963     1  0 21:43 tty3     00:00:00 /sbin/getty 38400 tty3
            root      3965     1  0 21:43 tty6     00:00:00 /sbin/getty 38400 tty6
            root      7023     1  0 22:48 tty1     00:00:00 /sbin/getty 38400 tty1
            root      7081     1  0 22:51 tty2     00:00:00 /bin/login --      
            falcon    7092  7081  0 22:52 tty2     00:00:00 -bash


                我們過(guò)濾了一些不相干的數(shù)據(jù)。從上面的結(jié)果可以看到,除了tty2被替換成/bin/login外,其他終端都運(yùn)行著/sbin/getty,說(shuō)明終端2 上的進(jìn)程是/bin/login,它已經(jīng)把/sbin/getty替換掉,另外,我們看到-bash進(jìn)程的父進(jìn)程是7081剛好是/bin/login程 序,這說(shuō)明/bin/login啟動(dòng)了-bash,但是它并沒(méi)有替換掉/bin/login,而是成為了/bin/login的子進(jìn)程,這說(shuō)明 /bin/login通過(guò)fork創(chuàng)建了一個(gè)子進(jìn)程并通過(guò)execve執(zhí)行了-bash(后者通過(guò)strace跟蹤到)。而init呢,其進(jìn)程ID是1, 是/sbin/getty和/bin/login的父進(jìn)程,說(shuō)明init啟動(dòng)或者間接啟動(dòng)了它們。下面通過(guò)pstree來(lái)查看調(diào)用樹(shù),更清晰的看出上述關(guān) 系。
            Quote:

            $ pstree | egrep "init|getty|\-bash|login"
            init-+-5*[getty]
                 |-login---bash
                 |-xfce4-terminal-+-bash-+-grep


                結(jié)果顯示init是5個(gè)getty程序,login程序和xfce4-terminal的父進(jìn)程,而后兩者則是bash的父進(jìn)程,另外我們執(zhí)行的grep命令則在bash上運(yùn)行,是bash的子進(jìn)程,這個(gè)將是我們后面關(guān)心的問(wèn)題。
                從上面的結(jié)果發(fā)現(xiàn),init作為所有進(jìn)程的父進(jìn)程,它的父進(jìn)程ID饒有興趣的是0,它是怎么被啟動(dòng)的呢?誰(shuí)才是真正的“造物主”?

            1.2.4 誰(shuí)啟動(dòng)了/sbin/init

                如果用過(guò)Lilo或者Grub這兩個(gè)操作系統(tǒng)引導(dǎo)程序,你可能會(huì)用到Linux內(nèi)核的一個(gè)啟動(dòng)參數(shù)init,當(dāng)你忘記密碼時(shí),可能會(huì)把這個(gè)參數(shù)設(shè)置成/bin/bash,讓系統(tǒng)直接進(jìn)入命令行,而無(wú)須輸入帳號(hào)和密碼,這樣就可以方便地登錄密碼修改掉。
                這個(gè)init參數(shù)是個(gè)什么東西呢?通過(guò)man bootparam會(huì)發(fā)現(xiàn)它的秘密,init參數(shù)正好指定了內(nèi)核啟動(dòng)后要啟動(dòng)的第一個(gè)程序,而如果沒(méi)有指定該參數(shù),內(nèi)核將依次查找/sbin/init, /etc/init, /bin/init, /bin/sh,如果找不到這幾個(gè)文件中的任何一個(gè),內(nèi)核就要恐慌(panic)了,并呆在那里一動(dòng)不動(dòng)了。
                因此/sbin/init就是Linux內(nèi)核啟動(dòng)的。而Linux內(nèi)核呢?是通過(guò)Lilo或者Grub等引導(dǎo)程序啟動(dòng)的,Lilo和Grub都有相應(yīng)的配 置文件,一般對(duì)應(yīng)/etc/lilo.conf和/boot/grub/menu.lst,通過(guò)這些配置文件可以指定內(nèi)核映像文件、系統(tǒng)根目錄所在分區(qū)、 啟動(dòng)選項(xiàng)標(biāo)簽等信息,從而能夠讓它們順利把內(nèi)核啟動(dòng)起來(lái)。
                那Lilo和Grub本身又是怎么被運(yùn)行起來(lái)的呢?還記得以前介紹的MBR不?MBR就是主引導(dǎo)扇區(qū),一般情況下這里存放著Lilo和Grub的代碼,而 誰(shuí)知道正好是這里存放了它們呢?BIOS,如果你用光盤(pán)安裝過(guò)操作系統(tǒng)的話,那么應(yīng)該修改過(guò)BIOS的默認(rèn)啟動(dòng)設(shè)置,通過(guò)設(shè)置你可以讓系統(tǒng)從光盤(pán)、硬盤(pán)甚 至軟盤(pán)啟動(dòng)。正是這里的設(shè)置讓BIOS知道了MBR處的代碼需要被執(zhí)行。
                那BIOS又是什么時(shí)候被起來(lái)的呢?加電自檢就執(zhí)行到了這里。
                更多系統(tǒng)啟動(dòng)的細(xì)節(jié),看看 "man boot-scripts" 吧。

                到這里,/bin/bash的神秘面紗就被揭開(kāi)了,它只是系統(tǒng)啟動(dòng)后運(yùn)行的一個(gè)程序而已,只不過(guò)這個(gè)程序可以響應(yīng)用戶的請(qǐng)求,那它到底是如何響應(yīng)用戶請(qǐng)求的呢?

            1.3 /bin/bash如何處理用戶鍵入的命令

            1.3.0 預(yù)備知識(shí)

                在執(zhí)行磁盤(pán)上某個(gè)程序時(shí),我們通常不會(huì)指定這個(gè)程序文件的絕對(duì)路徑,比如要執(zhí)行echo命令時(shí),我們一般不會(huì)輸入/bin/echo,而僅僅是輸入 echo。那為什么這樣bash也能夠找到/bin/echo呢?原因是Linux操作系統(tǒng)支持這樣一種策略:shell的一個(gè)環(huán)境變量PATH里頭存放 了程序的一些路徑,當(dāng)shell執(zhí)行程序時(shí)有可能去這些目錄下查找。which作為shell(這里特指bash)的一個(gè)內(nèi)置命令,如果用戶輸入的命令是 磁盤(pán)上的某個(gè)程序,它會(huì)返回這個(gè)文件的全路徑。

                有三個(gè)東西和終端的關(guān)系很大,那就是標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤,它們是三個(gè)文件描述符,一般對(duì)應(yīng)描述符0,1,2。在C語(yǔ)言程序里頭,我們可以把它們 當(dāng)作文件描述符一樣進(jìn)行操作。在命令行下,則可以使用重定向字符>,<等對(duì)它們進(jìn)行操作。對(duì)于標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤,都默認(rèn)輸出到終端,對(duì)于標(biāo) 準(zhǔn)輸入,也同樣默認(rèn)從終端輸入。

            1.3.1 哪種命令先被執(zhí)行

                在C語(yǔ)言里頭要寫(xiě)一段輸入字符串的命令很簡(jiǎn)單,調(diào)用scanf或者fgets就可以。這個(gè)在bash里頭應(yīng)該是類似的。但是它獲取用戶的命令以后,如何分析命令,如何響應(yīng)不同的命令呢?
                首先來(lái)看看bash下所謂的命令,用最常見(jiàn)的test來(lái)作測(cè)試。
            Quote:

            $ test1   #隨便鍵入一個(gè)字符串test1,bash發(fā)出響應(yīng),告訴我們找不到這個(gè)程序
            bash: test1: command not found
            $ test    #當(dāng)我們鍵入test的時(shí)候,看不到任何輸出,唯一的響應(yīng)是,新的命令提示符被打印了
            $ type test
            test is a shell builtin
            #查看test這個(gè)命令的類型,即查看test將被如何解釋,type告訴我們test是一個(gè)內(nèi)置命令,如果沒(méi)有理解錯(cuò),test應(yīng)該是利用諸如case "test": do something;break;這樣的機(jī)制實(shí)現(xiàn)的。
            $ which test   #通過(guò)which查到/usr/bin下有一個(gè)test命令文件,在鍵入test時(shí),到底哪一個(gè)被執(zhí)行了呢?
            /usr/bin/test
            $ /usr/bin/test   #執(zhí)行這個(gè)呢?也沒(méi)什么反應(yīng),到底誰(shuí)先被執(zhí)行了?


                從上面的演示中發(fā)現(xiàn)一個(gè)問(wèn)題?如果輸入一個(gè)命令,這個(gè)命令要么就不存在,要么可能同時(shí)是shell的內(nèi)置命令、也有可能是磁盤(pán)上環(huán)境變量PATH所指定的目錄下的某個(gè)程序文件。
                考慮到test內(nèi)置命令和/usr/bin/test命令的響應(yīng)結(jié)果一樣,我們無(wú)法知道哪一個(gè)先被執(zhí)行了,怎么辦呢?把/usr/bin/test替換成 一個(gè)我們自己的命令,并讓它打印一些信息(比如hello,world!),這樣我們就知道到底誰(shuí)被執(zhí)行了。寫(xiě)完程序,編譯好,命名為test放到 /usr/bin下(記得備份原來(lái)那個(gè))。開(kāi)始測(cè)試:
            Quote:

            $ test #鍵入test,還是沒(méi)有效果
            $ /usr/bin/test   #而鍵入絕對(duì)路徑呢,則打印了hello, world!誒,那默認(rèn)情況下肯定是內(nèi)置命令先被執(zhí)行了
            hello, world!


                總結(jié):內(nèi)置命令比磁盤(pán)文件中的程序優(yōu)先被bash執(zhí)行

                下面看看更多有趣的東西,鍵盤(pán)鍵入的命令還有可能是什么呢?因?yàn)閎ash支持別名和函數(shù),所以還有可能是別名和函數(shù),另外,如果PATH環(huán)境變量指定的不同目錄下有相同名字的程序文件,那到底哪個(gè)被優(yōu)先找到呢?
                下面再作一些實(shí)驗(yàn),
            Quote:

            $ alias test="ls -l"   #把test命名為ls -l的別名
            $ test                 #再執(zhí)行test,竟然執(zhí)行了ls -l,而不是什么也沒(méi)有,說(shuō)明alias比內(nèi)置命令更優(yōu)先
            total 9488
            drwxr-xr-x 12 falcon falcon    4096 2008-02-21 23:43 bash-3.2
            -rw-r--r--  1 falcon falcon 2529838 2008-02-21 23:30 bash-3.2.tar.gz
            $ function test { echo "hi, I'm a function"; }   #定義一個(gè)名叫test的函數(shù)
            $ test   #執(zhí)行一下,發(fā)現(xiàn),還是執(zhí)行了ls -l,說(shuō)明function沒(méi)有alias優(yōu)先級(jí)高
            total 9488
            drwxr-xr-x 12 falcon falcon    4096 2008-02-21 23:43 bash-3.2
            -rw-r--r--  1 falcon falcon 2529838 2008-02-21 23:30 bash-3.2.tar.gz
            $ unalias test   #把別名給去掉
            $ test         #現(xiàn)在執(zhí)行的是函數(shù),說(shuō)明函數(shù)的優(yōu)先級(jí)比內(nèi)置命令也要高
            hi, I'm a function
            $ builtin test #如果在命令之前跟上builtin,那么將直接執(zhí)行內(nèi)置命令
            $ unset test   #要去掉某個(gè)函數(shù)的定義,這樣就可以


                通過(guò)這個(gè)實(shí)驗(yàn)我們得到一個(gè)命令的別名(alias)、函數(shù)(function),內(nèi)置命令(builtin)和程序(program)的執(zhí)行優(yōu)先次序:
            Quote:

            先    alias --> function --> builtin --> program   后


                實(shí)際上,type命令會(huì)告訴我們這些細(xì)節(jié),type -a會(huì)按照bash解析的順序依次打印該命令的類型,而type -t則會(huì)給出第一個(gè)將被解析的命令的類型,之所以要做上面的實(shí)驗(yàn),是為了讓大家加印象。
            Quote:

            $ type -a test
            test is a shell builtin
            test is /usr/bin/test
            $ alias test="ls -l"
            $ function test { echo "I'm a function"; }
            $ type -a test
            test is aliased to `ls -l'
            test is a function
            test ()
            {
                echo "I'm a function"
            }
            test is a shell builtin
            test is /usr/bin/test
            $ type -t test
            alias


                下面再看看PATH指定的多個(gè)目錄下有同名程序的情況。再寫(xiě)一個(gè)程序,打印“hi, world!”,以示和"hello, world!"的區(qū)別,放到PATH指定的另外一個(gè)目錄/bin下,為了保證測(cè)試的說(shuō)服力,再寫(xiě)一個(gè)放到另外一個(gè)叫/usr/local/sbin的目錄 下。
                先看看PATH環(huán)境變量,確保它有/usr/bin,/bin和/usr/local/sbin這幾個(gè)目錄,然后通過(guò)type -P(-P參數(shù)強(qiáng)制到PATH下查找,而不管是別名還是內(nèi)置命令等,可以通過(guò)help type查看該參數(shù)的含義)查看,到底哪個(gè)先被執(zhí)行。

            Quote:

            $ echo $PATH
            /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
            $ type -P test   #可以看到/usr/local/sbin下的先被找到
            /usr/local/sbin/test
            $ rm /usr/local/sbin/test #把/usr/local/sbin/test下的給刪除掉
            $ type -P test   #現(xiàn)在/usr/bin下的先被找到
            /usr/bin/test
            $ type -a test #type -a也顯示類似的結(jié)果
            test is aliased to `ls -l'
            test is a function
            test ()
            {
                echo "I'm a function"
            }
            test is a shell builtin
            test is /usr/bin/test
            test is /bin/test


                可以找出這么一個(gè)規(guī)律:shell從PATH列出的路徑中依次查找用戶輸入的命令。考慮到程序的優(yōu)先級(jí)最低,如果想優(yōu)先執(zhí)行磁盤(pán)上的程序文件test呢?那么就可以用test -P找出這個(gè)文件并執(zhí)行就可以了。

            補(bǔ)充:對(duì)于shell的內(nèi)置命令,可以通過(guò)help command的方式獲得幫助,對(duì)于程序文件,可以查看用戶手冊(cè)(當(dāng)然,這個(gè)需要安裝,一般叫做xxx-doc),man command。關(guān)于用戶手冊(cè)安裝辦法見(jiàn)在Linux下學(xué)會(huì)查看Man文檔

            1.3.2 這些特殊字符是如何解析的:|, >, <, &

                在命令行上,除了輸入各種命令以及一些參數(shù)外,比如上面type命令的各種參數(shù)-a, -P等,對(duì)于這些參數(shù),是傳遞給程序本身的,非常好處理,比如if,else條件分之或者switch, case都可以處理。當(dāng)然,在bash里頭可能使用專門(mén)的參數(shù)處理函數(shù)getopt和getopt_long來(lái)處理它們。
                而|,>,<,&等字符,則比較特別,shell是怎么處理它們的呢?它們也被傳遞給程序本身嗎?可我們的程序內(nèi)部一般都不處理這些字符的,所以應(yīng)該是shell程序自己解析了它們。
                先來(lái)看看這幾個(gè)字符在命令行的常見(jiàn)用法,
            Quote:

            $ cat < ./test.c  #<字符表示:把test.c文件重定向?yàn)闃?biāo)準(zhǔn)輸入,作為cat命令的輸入,而cat默認(rèn)把內(nèi)容輸出到標(biāo)準(zhǔn)輸出。
            #include <stdio.h>

            int main(void)
            {
                    printf("hi, myself!\n");
                    return 0;
            }
            $ cat < ./test.c > test_new.c #>表示把標(biāo)準(zhǔn)輸出重定向?yàn)槲募est_new.c,結(jié)果內(nèi)容輸出到test_new.c


                對(duì)于>,<,>>,<<,<>我們都稱之為重定向(redirect),shell到底是怎么進(jìn)行所謂的“重定向”的呢?
                這主要?dú)w功于dup/fcntl等函數(shù),它們可以實(shí)現(xiàn):復(fù)制文件描述符,讓多個(gè)文件描述符共享同一個(gè)文件表項(xiàng)。比如,當(dāng)把文件test.c重定向?yàn)闃?biāo)準(zhǔn)輸 入時(shí)。假設(shè)之前用以打開(kāi)test.c的文件描述符是5,現(xiàn)在就把5復(fù)制為了0,這樣當(dāng)cat試圖從標(biāo)準(zhǔn)輸入讀出內(nèi)容時(shí),也就訪問(wèn)了文件描述符5指向的文件 表項(xiàng),接著讀出了文件內(nèi)容。輸出重定向與此類似。其他的重定向,諸如>>, <<, <>等雖然和>,<的具體實(shí)現(xiàn)功能不太一樣,但本質(zhì)是一樣的,都是文件描述符的復(fù)制,只不過(guò)可能對(duì)文件操作有一些附加的限制,比 如>>在輸出時(shí)追加到文件末尾,而>則會(huì)從頭開(kāi)始寫(xiě)入文件,前者意味著文件的大小會(huì)增長(zhǎng),而后者則意味文件被重寫(xiě)。

                那么|呢?"|"被形象地稱為“管道”,實(shí)際上它就是通過(guò)C語(yǔ)言里頭的無(wú)名管道來(lái)實(shí)現(xiàn)的。先看一個(gè)例子,

            Quote:

            $ cat < ./test.c  | grep hi
                    printf("hi, myself!\n");



                在這個(gè)例子中,cat讀出了test.c文件中的內(nèi)容,并輸出到標(biāo)準(zhǔn)輸出上,但是實(shí)際上輸出的內(nèi)容卻只有一行,原因是這個(gè)標(biāo)準(zhǔn)輸出被“接到”了grep命令的標(biāo)準(zhǔn)輸入上,而grep命令只打印了包含“hi”字符串的一行。
                這是怎么被“接”上的。cat和grep作為兩個(gè)單獨(dú)的命令,它們本身沒(méi)有辦法把兩者的輸入和輸出“接”起來(lái)。這正是shell自己的“杰作”,它通過(guò)C 語(yǔ)言里頭的pipe函數(shù)創(chuàng)建了一個(gè)管道(一個(gè)包含兩個(gè)文件描述符的整形數(shù)組,一個(gè)描述符用于寫(xiě)入數(shù)據(jù),一個(gè)描述符用于讀入數(shù)據(jù)),并且通過(guò) dup/fcntl把cat的輸出復(fù)制到了管道的輸入,而把管道的輸出則復(fù)制到了grep的輸入。這真是一個(gè)奇妙的想法。
               
                那&呢?當(dāng)你在程序的最后跟上這個(gè)奇妙的字符以后就可以接著做其他事情了,看看效果,
            Quote:

            $ sleep 50 &   #讓程序在后臺(tái)運(yùn)行
            [1] 8261
            $ fg %1      #提示符被打印出來(lái),可以輸入東西,讓程序到前臺(tái)運(yùn)行,無(wú)法輸入東西了,按下CTRL+Z,再讓程序到后臺(tái)運(yùn)行
            sleep 50

            [1]+  Stopped                 sleep 50
            $ fg %1   #再調(diào)到前臺(tái)
            sleep 50


                實(shí)際上&正是shell支持作業(yè)控制的表征,通過(guò)作業(yè)控制,用戶在命令行上可以同時(shí)作幾個(gè)事情(把當(dāng)前不做的放到后臺(tái),用&或者CTRL +Z或者bg)并且可以自由的選擇當(dāng)前需要執(zhí)行哪一個(gè)(用fg調(diào)到前臺(tái))。這在實(shí)現(xiàn)時(shí)應(yīng)該涉及到很多東西,包括終端會(huì)話(session)、終端信號(hào)、前 臺(tái)進(jìn)程、后臺(tái)進(jìn)程等。而在命令的后面加上&后,該命令將被作為后臺(tái)進(jìn)程執(zhí)行,后臺(tái)進(jìn)程是什么呢?這類進(jìn)程無(wú)法接收用戶發(fā)送給終端的信號(hào)(如 SIGHUP,SIGQUIT,SIGINT),無(wú)法響應(yīng)鍵盤(pán)輸入(被前臺(tái)進(jìn)程占用著),不過(guò)可以通過(guò)fg切換到前臺(tái)而享受作為前臺(tái)進(jìn)程具有的特權(quán)。
                因此,當(dāng)一個(gè)命令被加上&執(zhí)行后,shell必須讓它具有后臺(tái)進(jìn)程的特征,讓它無(wú)法響應(yīng)鍵盤(pán)的輸入,無(wú)法響應(yīng)終端的信號(hào)(意味忽略這些信號(hào)),并 且比較重要的是新的命令提示符得打印出來(lái),并且讓命令行接口可以繼續(xù)執(zhí)行其他命令,這些就是shell對(duì)&的執(zhí)行動(dòng)作。

                還有什么神秘的呢?你也可以寫(xiě)自己的shell了,并且可以讓內(nèi)核啟動(dòng)后就執(zhí)行它l,在lilo或者grub的啟動(dòng)參數(shù)上設(shè)置init= /path/to/your/own/shell/program就可以。當(dāng)然,也可以把它作為自己的登錄shell,只需要放到/etc/passwd 文件中相應(yīng)用戶名所在行的最后就可以。不過(guò)貌似到現(xiàn)在還沒(méi)介紹shell是怎么執(zhí)行程序,是怎樣讓程序變成進(jìn)程的,所以繼續(xù)。

            1.3.3 /bin/bash用什么魔法讓一個(gè)普通程序變成了進(jìn)程

                當(dāng)我們從鍵盤(pán)鍵入一串命令,shell奇妙的響應(yīng)了,對(duì)于內(nèi)置命令和函數(shù),shell自身就可以解析了(通過(guò)switch case之類的C語(yǔ)言語(yǔ)句)。但是,如果這個(gè)命令是磁盤(pán)上的一個(gè)文件呢。它找到該文件以后,怎么執(zhí)行它的呢?
                還是用strace來(lái)跟蹤一個(gè)命令的執(zhí)行過(guò)程看看。
            Quote:

            $ strace -f -o strace.log /usr/bin/test
            hello, world!
            $ cat strace.log | sed -ne "1p"   #我們對(duì)第一行很感興趣
            8445  execve("/usr/bin/test", ["/usr/bin/test"], [/* 33 vars */]) = 0


                從跟蹤到的結(jié)果的第一行可以看到bash通過(guò)execve調(diào)用了/usr/bin/test,并且給它傳了33個(gè)參數(shù)。這33個(gè)vars是什么呢?看看 declare -x的結(jié)果(這個(gè)結(jié)果只有32個(gè),原因是vars的最后一個(gè)變量需要是一個(gè)結(jié)束標(biāo)志,即NULL)。

            Quote:

            $ declare -x | wc -l   #declare -x聲明的環(huán)境變量將被導(dǎo)出到子進(jìn)程中
            32
            $ export TEST="just a test"   #為了認(rèn)證declare -x和之前的vars的個(gè)數(shù)的關(guān)系,再加一個(gè)
            $ declare -x | wc -l
            33
            $ strace -f -o strace.log /usr/bin/test   #再次跟蹤,看看這個(gè)關(guān)系
            hello, world!
            $ cat strace.log | sed -ne "1p"   
            8523  execve("/usr/bin/test", ["/usr/bin/test"], [/* 34 vars */]) = 0



                通過(guò)這個(gè)演示發(fā)現(xiàn),當(dāng)前shell的環(huán)境變量中被設(shè)置為export的變量被復(fù)制到了新的程序里頭。不過(guò)雖然我們認(rèn)為shell執(zhí)行新程序時(shí)是在一個(gè)新的 進(jìn)程里頭執(zhí)行的,但是strace并沒(méi)有跟蹤到諸如fork的系統(tǒng)調(diào)用(可能是strace自己設(shè)計(jì)的時(shí)候并沒(méi)有跟蹤fork,或者是在fork之后才跟 蹤)。但是有一個(gè)事實(shí)我們不得不承認(rèn):當(dāng)前shell并沒(méi)有被新程序的進(jìn)程替換,所以說(shuō)shell肯定是先調(diào)用fork(也有可能是vfork)創(chuàng)建了一 個(gè)子進(jìn)程,然后再調(diào)用execve執(zhí)行新程序的。如果你還不相信,那么直接通過(guò)exec執(zhí)行新程序看看,這個(gè)可是直接把當(dāng)前shell的進(jìn)程替換掉的。

            Quote:

            exec /usr/bin/test


                應(yīng)該可以看到當(dāng)前shell“嘩”(聽(tīng)不到,突然沒(méi)了而已)的一下就沒(méi)有了。
                下面來(lái)模擬一下shell執(zhí)行普通程序。multiprocess相當(dāng)于當(dāng)前shell,而/usr/bin/test則相當(dāng)于通過(guò)命令行傳遞給shell的一個(gè)程序。這里是代碼:



            Code:

            [Ctrl+A Select All]


                運(yùn)行看看,
            Quote:

            $ make multiprocess
            $ ./multiprocess
            child: my pid is 2251
            child: my parent's pid is 2250
            hello, world!
            parent: my pid is 2250
            parent: wait for my child exit successfully!


                從執(zhí)行結(jié)果可以看出,/usr/bin/test在multiprocess的子進(jìn)程中運(yùn)行并不干擾父進(jìn)程,因?yàn)楦高M(jìn)程一直等到了/usr/bin/test執(zhí)行完成。
                再回頭看看代碼,你會(huì)發(fā)現(xiàn)execlp并沒(méi)有傳遞任何環(huán)境變量信息給/usr/bin/test,到底是怎么把環(huán)境變量傳送過(guò)去的呢?通過(guò)man exec我們可以看到一組exec的調(diào)用,在里頭并沒(méi)有發(fā)現(xiàn)execve,但是通過(guò)man execve可以看到該系統(tǒng)調(diào)用。實(shí)際上exec的那一組調(diào)用都只是libc庫(kù)提供的,而execve才是真正的系統(tǒng)調(diào)用,也就是說(shuō)無(wú)論使用exec調(diào)用 中的哪一個(gè),最終調(diào)用的都是execve,如果使用execlp,那么execlp將通過(guò)一定的處理把參數(shù)轉(zhuǎn)換為execve的參數(shù)。因此,雖然我們沒(méi)有 傳遞任何環(huán)境變量給execlp,但是默認(rèn)情況下,execlp把父進(jìn)程的環(huán)境變量復(fù)制給了子進(jìn)程,而這個(gè)動(dòng)作是在execlp函數(shù)內(nèi)部完成的。
                現(xiàn)在,總結(jié)一下execve,它有有三個(gè)參數(shù),
                第一個(gè)是程序本身的絕對(duì)路徑,對(duì)于剛才使用的execlp,我們沒(méi)有指定路徑,這意味著它會(huì)設(shè)法到PATH環(huán)境變量指定的路徑下去尋找程序的全路徑。
                第二個(gè)參數(shù)是一個(gè)將傳遞給被它執(zhí)行的程序的參數(shù)數(shù)組指針。正是這個(gè)參數(shù)把我們從命令行上輸入的那些參數(shù),諸如grep命令的-v等傳遞給了新程序,可以通過(guò)main函數(shù)的第二個(gè)參數(shù)char *argv[]獲得這些內(nèi)容。
                第三個(gè)參數(shù)是一個(gè)將傳遞給被它執(zhí)行的程序的環(huán)境變量,這些環(huán)境變量也可以通過(guò)main函數(shù)的第三個(gè)變量獲取,只要定義一個(gè)char *env[]就可以了,只是通常不直接用它罷了,而是通過(guò)另外的方式,通過(guò)extern char **environ全局變量(環(huán)境變量表的指針)或者getenv函數(shù)來(lái)獲取某個(gè)環(huán)境變量的值。
                當(dāng)然,實(shí)際上,當(dāng)程序被execve執(zhí)行后,它被加載到了內(nèi)存里,包括程序的各種指令、數(shù)據(jù)以及傳遞給它的各種參數(shù)、環(huán)境變量等都被存放在系統(tǒng)分配給該程序的內(nèi)存空間中。
                我們可以通過(guò)/proc/<pid>/maps把一個(gè)程序?qū)?yīng)的進(jìn)程的內(nèi)存映象看個(gè)大概。
            Quote:

            $ cat /proc/self/maps   #查看cat程序自身加載后對(duì)應(yīng)進(jìn)程的內(nèi)存映像
            08048000-0804c000 r-xp 00000000 03:01 273716     /bin/cat
            0804c000-0804d000 rw-p 00003000 03:01 273716     /bin/cat
            0804d000-0806e000 rw-p 0804d000 00:00 0          [heap]
            b7c46000-b7e46000 r--p 00000000 03:01 87528      /usr/lib/locale/locale-archive
            b7e46000-b7e47000 rw-p b7e46000 00:00 0
            b7e47000-b7f83000 r-xp 00000000 03:01 466875     /lib/libc-2.5.so
            b7f83000-b7f84000 r--p 0013c000 03:01 466875     /lib/libc-2.5.so
            b7f84000-b7f86000 rw-p 0013d000 03:01 466875     /lib/libc-2.5.so
            b7f86000-b7f8a000 rw-p b7f86000 00:00 0
            b7fa1000-b7fbc000 r-xp 00000000 03:01 402817     /lib/ld-2.5.so
            b7fbc000-b7fbe000 rw-p 0001b000 03:01 402817     /lib/ld-2.5.so
            bfcdf000-bfcf4000 rw-p bfcdf000 00:00 0          [stack]
            ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]


                關(guān)于程序加載和進(jìn)程內(nèi)存映像的更多細(xì)節(jié)請(qǐng)參考《C語(yǔ)言程序緩沖區(qū)注入分析》。

                到這里,關(guān)于命令行的秘密都被“曝光”了,可以開(kāi)始寫(xiě)自己的命令行解釋程序了。
                關(guān)于進(jìn)程的相關(guān)操作請(qǐng)參考《shell編程范例之進(jìn)程操作》。

            補(bǔ)充:上面沒(méi)有討論到一個(gè)比較重要的內(nèi)容,那就是即使execve找到了某個(gè)可執(zhí)行文件,如果該文件屬主沒(méi)有運(yùn)行該程序的權(quán)限,那么也沒(méi)有辦法運(yùn)行程序。可通過(guò)ls -l查看程序的權(quán)限,通過(guò)chmod添加或者去掉可執(zhí)行權(quán)限。

          13. 文件屬主具有可執(zhí)行權(quán)限時(shí)才可以執(zhí)行某個(gè)程序
            Quote:

            whoami
            falcon
            $ ls -l hello  #查看用戶權(quán)限(第一個(gè)x表示屬主對(duì)該程序具有可執(zhí)行權(quán)限
            -rwxr-xr-x 1 falcon users 6383 2000-01-23 07:59 hello*
            $ ./hello
            Hello World
            $ chmod -x hello  #去掉屬主的可執(zhí)行權(quán)限
            $ ls -l hello
            -rw-r--r-- 1 falcon users 6383 2000-01-23 07:59 hello
            $ ./hello
            -bash: ./hello: Permission denied


               
            參考的資料:

            [1] man boot-scripts
            Linux啟動(dòng)過(guò)程
            [2] man bootparam
            Linux內(nèi)核啟動(dòng)參數(shù)
            [3] man 5 passwd
            [4] man shadow
            [5] "UNIX環(huán)境高級(jí)編程",進(jìn)程關(guān)系一章
            [6] 2006,2007 Summer School hold by DSLab
            http://dslab.lzu.edu.cn/docs/2007summerschool/index.html
            http://dslab.lzu.edu.cn/docs/2006summerschool/index.html
          14. posted @ 2008-03-14 15:27 隨意門(mén) 閱讀(3604) | 評(píng)論 (1)編輯 收藏
            GCC編譯背后(第二部分:匯編和鏈接)

            (上接“GCC編譯的背后(第一部分:預(yù)處理和編譯)”)

            3、匯編

                開(kāi)篇:這里實(shí)際上還是翻譯過(guò)程,只不過(guò)把作為中間結(jié)果的匯編代碼翻譯成了機(jī)器代碼,即目標(biāo)代碼,不過(guò)它還不可以運(yùn)行。如果要產(chǎn)生這一中間結(jié)果,可用gcc的-c選項(xiàng),當(dāng)然,也可通過(guò)as命令_匯編_匯編語(yǔ)言源文件來(lái)產(chǎn)生。

                匯編是把匯編語(yǔ)言翻譯成目標(biāo)代碼的過(guò)程,在學(xué)習(xí)匯編語(yǔ)言開(kāi)發(fā)時(shí),大家應(yīng)該比較熟悉nasm匯編工具(支持Intel格式的匯編語(yǔ)言)了,不過(guò)這里主要用 as匯編工具來(lái)匯編AT&T格式的匯編語(yǔ)言,因?yàn)間cc產(chǎn)生的中間代碼就是AT&T格式的。下面來(lái)演示分別通過(guò)gcc的-c選項(xiàng)和as來(lái) 產(chǎn)生 目標(biāo)代碼。

            Quote:

            $ file hello.s
            hello.s: ASCII assembler program text
            $ gcc -c hello.s        #用gcc把匯編語(yǔ)言編譯成目標(biāo)代碼
            $ file hello.o            #file命令可以用來(lái)查看文件的類型,這個(gè)目標(biāo)代碼是可重定位的(relocatable),需                   #要通過(guò)ld進(jìn)行進(jìn)一步的鏈接成可執(zhí)行程序(executable)和共享庫(kù)(shared)
            hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
            $ as -o hello.o hello.s        #用as把匯編語(yǔ)言編譯成目標(biāo)代碼
            $ file hello.o
            hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped



                gcc和as默認(rèn)產(chǎn)生的目標(biāo)代碼都是ELF格式[6]的,因此這里主要討論ELF格式的目標(biāo)代碼(如果 有時(shí)間再回顧一下a.out和coff格式,當(dāng)然你也可以參考資料[15],自己先了解一下,并結(jié)合objcopy來(lái)轉(zhuǎn)換它們,比較異同)。

                目標(biāo)代碼不再是普通的文本格式,無(wú)法直接通過(guò)文本編輯器瀏覽,需要一些專門(mén)的工具。如果想了解更多目標(biāo)代碼的細(xì)節(jié),區(qū)分relocatable(可重定 位)、executable(可執(zhí)行)、shared libarary(共享庫(kù))的不同,我們得設(shè)法了解目標(biāo)代碼的組織方式和相關(guān)的閱讀和分析工具。下面我們主要介紹這部分內(nèi)容。
                "BFD is a package which allows applications to use the same routines to operate on object files whatever the object file format. A new object file format can be supported simply by creating a new BFD back end and adding it to the library."[24][25]。
                binutils(GNU Binary Utilities)的很多工具都采用這個(gè)庫(kù)來(lái)操作目標(biāo)文件,這類工具有objdump,objcopy,nm,strip等(當(dāng)然,你也可以利用它。如 果你深入了解ELF格式,那么通過(guò)它來(lái)分析和編寫(xiě)Virus程序?qū)?huì)更加方便),不過(guò)另外一款非常優(yōu)秀的分析工具readelf并不是 基于這個(gè)庫(kù),所以你也應(yīng)該可以直接用elf.h頭文件中定義的相關(guān)結(jié)構(gòu)來(lái)操作ELF文件。

                下面將通過(guò)這些輔助工具(主要是readelf和objdump,可參考本節(jié)最后列出的資料[4]),結(jié)合ELF手冊(cè)[6](建議看第三篇中文版)來(lái)分析它們。

                下面大概介紹ELF文件的結(jié)構(gòu)和三種不同類型ELF文件的區(qū)別。

            ELF文件的結(jié)構(gòu):

            ELF Header(ELF文件頭)
            Porgram Headers Table(程序頭表,實(shí)際上叫段表好一些,用于描述可執(zhí)行文件和可共享庫(kù))
            Section 1
            Section 2   
            Section 3
            ...
            Section Headers Table(節(jié)區(qū)頭部表,用于鏈接可重定位文件成可執(zhí)行文件或共享庫(kù))

                對(duì)于可重定位文件,程序頭是可選的,而對(duì)于可執(zhí)行文件和共享庫(kù)文件(動(dòng)態(tài)連接庫(kù)),節(jié)區(qū)表則是可選的。這里的可選是指沒(méi)有也可以。可以分別通過(guò) readelf文件的-h,-l和-S參數(shù)查看ELF文件頭(ELF Header)、程序頭部表(Program Headers Table,段表)和節(jié)區(qū)表(Section Headers Table)。

                文件頭說(shuō)明了文件的類型,大小,運(yùn)行平臺(tái),節(jié)區(qū)數(shù)目等。先來(lái)通過(guò)文件頭看看不同ELF的類型。為了說(shuō)明問(wèn)題,先來(lái)幾段代碼吧。



            Code:

            [Ctrl+A Select All]





            Code:

            [Ctrl+A Select All]





            Code:

            [Ctrl+A Select All]



                下面通過(guò)這幾段代碼來(lái)演示通過(guò)readelf -h參數(shù)查看ELF的不同類型。期間將演示如何創(chuàng)建動(dòng)態(tài)連接庫(kù)(即可共享文件)、靜態(tài)連接庫(kù),并比較它們的異同。
            Quote:

            $ gcc -c myprintf.c test.c        #編譯產(chǎn)生兩個(gè)目標(biāo)文件myprintf.o和test.o,它們都是可重定位文件(REL)
            $ readelf -h test.o | grep Type   
              Type:                              REL (Relocatable file)
            $ readelf -h myprintf.o | grep Type
              Type:                              REL (Relocatable file)
            $ gcc -o test myprintf.o test.o    #根據(jù)目標(biāo)代碼連接產(chǎn)生可執(zhí)行文件,這里的文件類型是可執(zhí)行的(EXEC)
            $ readelf -h test | grep Type
              Type:                              EXEC (Executable file)
            $ ar rcsv libmyprintf.a myprintf.o    #用ar命令創(chuàng)建一個(gè)靜態(tài)連接庫(kù),靜態(tài)連接庫(kù)也是可重定位文件(REL)
            $ readelf -h libmyprintf.a | grep Type    #因此,使用靜態(tài)連接庫(kù)和可重定位文件一樣,它們之間唯一不
                                                    #同是前者可以是多個(gè)可重定位文件的“集合”。
              Type:                              REL (Relocatable file)
            $ gcc -o test test.o -llib -L./        #可以直接連接進(jìn)去,也可以使用-l參數(shù),-L指定庫(kù)的搜索路徑
            $ gcc -Wall myprintf.o -shared -Wl,-soname,libmyprintf.so.0 -o libmyprintf.so.0.0
                                                #編譯產(chǎn)生動(dòng)態(tài)鏈接庫(kù),并支持major和minor版本號(hào),動(dòng)態(tài)鏈接庫(kù)類型為DYN
            $ ln -sf libmyprintf.so.0.0 libmyprintf.so.0
            $ ln -sf libmyprintf.so.0 libmyprintf.so
            $ readelf -h libmyprintf.so | grep Type
              Type:                              DYN (Shared object file)
            $ gcc -o test test.o -llib -L./        #編譯時(shí)和靜態(tài)連接庫(kù)類似,但是執(zhí)行時(shí)需要指定動(dòng)態(tài)連接庫(kù)的搜索路徑
            $ LD_LIBRARY_PATH=./ ./test            #LD_LIBRARY_PATH為動(dòng)態(tài)鏈接庫(kù)的搜索路徑
            $ gcc -static -o test test.o -llib -L./    #在不指定static時(shí)會(huì)優(yōu)先使用動(dòng)態(tài)鏈接庫(kù),指定時(shí)則阻止使用動(dòng)態(tài)連接庫(kù)
                                                    #這個(gè)時(shí)候會(huì)把所有靜態(tài)連接庫(kù)文件加入到可執(zhí)行文件中,使得執(zhí)行文件很大
                                                    #而且加載到內(nèi)存以后會(huì)浪費(fèi)內(nèi)存空間,因此不建議這么做



                經(jīng)過(guò)上面的演示基本可以看出它們之間的不同。可重定位文件本身不可以運(yùn)行,僅僅是作為可執(zhí)行文件、靜態(tài)連接庫(kù)(也是可重定位文件)、動(dòng)態(tài)連接庫(kù)的 “組件”。靜態(tài)連接庫(kù)和動(dòng)態(tài)連接庫(kù)本身也不可以執(zhí)行,作為可執(zhí)行文件的“組件”,它們兩者也不同,前者也是可重定位文件(只不過(guò)可能是多個(gè)可重定位文件的 集合),并且在連接時(shí)加入到可執(zhí)行文件中去;而動(dòng)態(tài)連接庫(kù)在連接時(shí),庫(kù)文件本身并沒(méi)有添加到可執(zhí)行文件中,只是在可執(zhí)行文件中加入了該庫(kù)的名字等信息,以 便在可執(zhí)行文件運(yùn)行過(guò)程中引用庫(kù)中的函數(shù)時(shí)由動(dòng)態(tài)連接器去查找相關(guān)函數(shù)的地址,并調(diào)用它們。從這個(gè)意義上說(shuō),動(dòng)態(tài)連接庫(kù)本身也具有可重定位的特征,含有可 重定位的信息。對(duì)于什么是重定位?如何進(jìn)行靜態(tài)符號(hào)和動(dòng)態(tài)符號(hào)的重定位,我們將在鏈接部分和《動(dòng)態(tài)符號(hào)鏈接的細(xì)節(jié)》一節(jié)介紹。

                下面來(lái)看看ELF文件的主體內(nèi)容,節(jié)區(qū)(Section)。ELF文件具有很大的靈活性,它通過(guò)文件頭組織整個(gè)文件的總體結(jié)構(gòu),通過(guò)節(jié)區(qū)表 (Section Headers Table)和程序頭(Program Headers Table或者叫段表)來(lái)分別描述可重定位文件和可執(zhí)行文件。但不管是哪種類型,它們都需要它們的主體,即各種節(jié)區(qū)。在可重定位文件中,節(jié)區(qū) 表描述的就是各種節(jié)區(qū)本身;而在可執(zhí)行文件中,程序頭描述的是由各個(gè)節(jié)區(qū)組成的段(Segment),以便程序運(yùn)行時(shí)動(dòng)態(tài)裝載器知道如何對(duì)它們進(jìn)行內(nèi)存映像,從而方便程序加載和運(yùn)行。
                下面先來(lái)看看 一些常見(jiàn)的節(jié)區(qū),而關(guān)于這些節(jié)區(qū)(section)如何通過(guò)重定位構(gòu)成成不同的段(Segments),以及有哪些常規(guī)的段,我們將在鏈接部分進(jìn)一步介紹。

                可以通過(guò)readelf的-S參數(shù)查看ELF的節(jié)區(qū)。(建議一邊操作一邊看文檔,以便加深對(duì)ELF文件結(jié)構(gòu)的理解)先來(lái)看看可重定位文件的節(jié)區(qū)信息,通過(guò)節(jié)區(qū)表來(lái)查看:

            Quote:

            $ gcc -c myprintf.c            #默認(rèn)編譯好myprintf.c,將產(chǎn)生一個(gè)可重定位的文件myprintf.o
            $ readelf -S myprintf.o        #通過(guò)查看myprintf.o的節(jié)區(qū)表查看節(jié)區(qū)信息
            There are 11 section headers, starting at offset 0xc0:

            Section Headers:
              [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
              [ 0]                   NULL            00000000 000000 000000 00      0   0  0
              [ 1] .text             PROGBITS        00000000 000034 000018 00  AX  0   0  4
              [ 2] .rel.text         REL             00000000 000334 000010 08      9   1  4
              [ 3] .data             PROGBITS        00000000 00004c 000000 00  WA  0   0  4
              [ 4] .bss              NOBITS          00000000 00004c 000000 00  WA  0   0  4
              [ 5] .rodata           PROGBITS        00000000 00004c 00000e 00   A  0   0  1
              [ 6] .comment          PROGBITS        00000000 00005a 000012 00      0   0  1
              [ 7] .note.GNU-stack   PROGBITS        00000000 00006c 000000 00      0   0  1
              [ 8] .shstrtab         STRTAB          00000000 00006c 000051 00      0   0  1
              [ 9] .symtab           SYMTAB          00000000 000278 0000a0 10     10   8  4
              [10] .strtab           STRTAB          00000000 000318 00001a 00      0   0  1
            Key to Flags:
              W (write), A (alloc), X (execute), M (merge), S (strings)
              I (info), L (link order), G (group), x (unknown)
              O (extra OS processing required) o (OS specific), p (processor specific)
            $ objdump -d -j .text   myprintf.o      #這里是程序指令部分,用objdump的-d選項(xiàng)可以看到反編譯的結(jié)果,
                                                                                    #-j指定需要查看的節(jié)區(qū)
            myprintf.o:     file format elf32-i386

            Disassembly of section .text:

            00000000 <myprintf>:
               0:   55                      push   %ebp
               1:   89 e5                   mov    %esp,%ebp
               3:   83 ec 08                sub    $0x8,%esp
               6:   83 ec 0c                sub    $0xc,%esp
               9:   68 00 00 00 00          push   $0x0
               e:   e8 fc ff ff ff          call   f <myprintf+0xf>
              13:   83 c4 10                add    $0x10,%esp
              16:   c9                      leave
              17:   c3                      ret
            $ readelf -r myprintf.o                         #用-r選項(xiàng)可以看到有關(guān)重定位的信息,這里有兩部分需要重定位

            Relocation section '.rel.text' at offset 0x334 contains 2 entries:
             Offset     Info    Type            Sym.Value  Sym. Name
            0000000a  00000501 R_386_32          00000000   .rodata
            0000000f  00000902 R_386_PC32        00000000   puts
            $ readelf -x .rodata myprintf.o         #.rodata節(jié)區(qū)包含只讀數(shù)據(jù),即我們要打印的hello, world!.

            Hex dump of section '.rodata':
              0x00000000 68656c6c 6f2c2077 6f726c64 2100     hello, world!.

            $ readelf -x .data myprintf.o           #沒(méi)有這個(gè)節(jié)區(qū),.data應(yīng)該包含一些初始化的數(shù)據(jù)

            Section '.data' has no data to dump.
            $ readelf -x .bss       mmyprintf.o             #也沒(méi)有這個(gè)節(jié)區(qū),.bss應(yīng)該包含一些未初始化的數(shù)據(jù),程序默認(rèn)初始為0

            Section '.bss' has no data to dump.
            $ readelf -x .comment myprintf.o        #是一些注釋,可以看到是是GCC的版本信息

            Hex dump of section '.comment':
              0x00000000 00474343 3a202847 4e552920 342e312e .GCC: (GNU) 4.1.
              0x00000010 3200                                2.
            $ readelf -x .note.GNU-stack myprintf.o #這個(gè)也沒(méi)有內(nèi)容

            Section '.note.GNU-stack' has no data to dump.
            $ readelf -x .shstrtab myprintf.o       #包括所有節(jié)區(qū)的名字

            Hex dump of section '.shstrtab':
              0x00000000 002e7379 6d746162 002e7374 72746162 ..symtab..strtab
              0x00000010 002e7368 73747274 6162002e 72656c2e ..shstrtab..rel.
              0x00000020 74657874 002e6461 7461002e 62737300 text..data..bss.
              0x00000030 2e726f64 61746100 2e636f6d 6d656e74 .rodata..comment
              0x00000040 002e6e6f 74652e47 4e552d73 7461636b ..note.GNU-stack
              0x00000050 00                                  .

            $ readelf -symtab myprintf.o    #符號(hào)表,包括所有用到的相關(guān)符號(hào)信息,如函數(shù)名、變量名

            Symbol table '.symtab' contains 10 entries:
               Num:    Value  Size Type    Bind   Vis      Ndx Name
                 0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
                 1: 00000000     0 FILE    LOCAL  DEFAULT  ABS myprintf.c
                 2: 00000000     0 SECTION LOCAL  DEFAULT    1
                 3: 00000000     0 SECTION LOCAL  DEFAULT    3
                 4: 00000000     0 SECTION LOCAL  DEFAULT    4
                 5: 00000000     0 SECTION LOCAL  DEFAULT    5
                 6: 00000000     0 SECTION LOCAL  DEFAULT    7
                 7: 00000000     0 SECTION LOCAL  DEFAULT    6
                 8: 00000000    24 FUNC    GLOBAL DEFAULT    1 myprintf
                 9: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND puts
            $ readelf -x .strtab myprintf.o #字符串表,用到的字符串,包括文件名、函數(shù)名、變量名等。

            Hex dump of section '.strtab':
              0x00000000 006d7970 72696e74 662e6300 6d797072 .myprintf.c.mypr
              0x00000010 696e7466 00707574 7300              intf.puts.



                從上表可以看出,對(duì)于可重定位文件,會(huì)包含這些基本節(jié)區(qū).text, .rel.text, .data, .bss, .rodata, .comment, .note.GNU-stack, .shstrtab, .symtab和.strtab。為了進(jìn)一步理解這些節(jié)區(qū)和源代碼的關(guān)系,這里來(lái)看一看myprintf.c產(chǎn)生的匯編代碼。

            Quote:

            $ gcc -S myprintf.c
            $ cat myprintf.s
                    .file   "myprintf.c"
                    .section        .rodata
            .LC0:
                    .string "hello, world!"
                    .text
            .globl myprintf
                    .type   myprintf, @function
            myprintf:
                    pushl   %ebp
                    movl    %esp, %ebp
                    subl    $8, %esp
                    subl    $12, %esp
                    pushl   $.LC0
                    call    puts
                    addl    $16, %esp
                    leave
                    ret
                    .size   myprintf, .-myprintf
                    .ident  "GCC: (GNU) 4.1.2"
                    .section        .note.GNU-stack,"",@progbits



                是不是可以從中看出可重定位文件中的那些節(jié)區(qū)和匯編語(yǔ)言代碼之間的關(guān)系?在上面的可重定位文件,可以看到有一個(gè)可重定位的節(jié)區(qū),即. rel.text,它標(biāo)記了兩個(gè)需要重定位的項(xiàng),.rodata和puts。這個(gè)節(jié)區(qū)將告訴編譯器這兩個(gè)信息在鏈接或者動(dòng)態(tài)鏈接的過(guò)程中需要重定位, 具體如何重定位?將根據(jù)重定位項(xiàng)的類型,比如上面的R_386_32和R_386_PC32(關(guān)于這些類型的更多細(xì)節(jié),請(qǐng)查看ELF手冊(cè)[6])。

                到這里,對(duì)可重定位文件應(yīng)該有了一個(gè)基本的了解,下面將介紹什么是可重定位,可重定位文件到底是如何被鏈接生成可執(zhí)行文件和動(dòng)態(tài)連接庫(kù)的,這個(gè)過(guò)程除了進(jìn)行了一些符號(hào)的重定位外,還進(jìn)行了哪些工作呢?

            本節(jié)參考資料:

            [1] 了解編譯程序的過(guò)程
            http://9iyou.com/Program_Data/linuxunix-3125.html
            http://www.host01.com/article/server/00070002/0621409075078127.htm
            [2] C track: compiling C programs.
            http://www.cs.caltech.edu/courses/cs11/material/c/mike/misc/compiling_c.html
            [3] Dissecting shared libraries
            http://www.ibm.com/developerworks/linux/library/l-shlibs.html

            4、鏈接

                開(kāi)篇:重定位是將符號(hào)引用與符號(hào)定義進(jìn)行鏈接的過(guò)程。因此鏈接是處理可重定位文件,把它們的各種符號(hào)引用和符號(hào)定義轉(zhuǎn)換為可執(zhí)行文件中的合適信息(一般是虛擬內(nèi)存地址)的過(guò)程。鏈接又 分為靜態(tài)鏈接和動(dòng)態(tài)鏈接,前者是程序開(kāi)發(fā)階段程序員用ld(gcc實(shí)際上在后臺(tái)調(diào)用了ld)靜態(tài)鏈接器手動(dòng)鏈接的過(guò)程,而動(dòng)態(tài)鏈接則是程序運(yùn)行期間系 統(tǒng)調(diào)用動(dòng)態(tài)鏈接器(ld-linux.so)自動(dòng)鏈接的過(guò)程。比如,如果鏈接到可執(zhí)行文件中的是靜態(tài)連接庫(kù)libmyprintf.a,那么. rodata節(jié)區(qū)在鏈接后需要被重定位到一個(gè)絕對(duì)的虛擬內(nèi)存地址,以便程序運(yùn)行時(shí)能夠正確訪問(wèn)該節(jié)區(qū)中的字符串信息。而對(duì)于puts,因?yàn)樗莿?dòng)態(tài)連接庫(kù)libc.so中定義的函數(shù),所 以會(huì)在程序運(yùn)行時(shí)通過(guò)動(dòng)態(tài)符號(hào)鏈接找出puts函數(shù)在內(nèi)存中的地址,以便程序調(diào)用該函數(shù)。在這里主要討論靜態(tài)鏈接過(guò)程,動(dòng)態(tài)鏈接過(guò)程見(jiàn)《動(dòng)態(tài)符號(hào)鏈接的細(xì)節(jié)》。

                靜態(tài)鏈接過(guò)程主要是把可重定位文件依次讀入,分析各個(gè)文件的文件頭,進(jìn)而依次讀入各個(gè)文件的節(jié)區(qū),并計(jì)算各個(gè)節(jié)區(qū)的虛擬內(nèi)存位置,對(duì)一些需要重定位的符號(hào) 進(jìn) 行處理,設(shè)定它們的虛擬內(nèi)存地址等,并最終產(chǎn)生一個(gè)可執(zhí)行文件或者是動(dòng)態(tài)鏈接庫(kù)。這個(gè)鏈接過(guò)程是通過(guò)ld來(lái)完成的,ld在鏈接時(shí)使用了一個(gè)鏈接腳本 (linker script), 該鏈接腳本處理鏈接的具體細(xì)節(jié)。由于靜態(tài)符號(hào)鏈接過(guò)程非常復(fù)雜,特別是計(jì)算符號(hào)地址的過(guò)程,考慮到時(shí)間關(guān)系,相關(guān)細(xì)節(jié)請(qǐng)參考ELF手冊(cè)[6]。這里主要介 紹可重定位文件中的節(jié)區(qū)(節(jié)區(qū)表描述的)和可執(zhí)行文件中段(程序頭描述的)的對(duì)應(yīng)關(guān)系以及gcc編譯時(shí)采用的一些默認(rèn)鏈接選項(xiàng)。

                下面先來(lái)看看可執(zhí)行文件的節(jié)區(qū)信息,通過(guò)程序頭(段表)來(lái)查看:

            Quote:

            $ readelf -S test.o                        #為了比較,先把test.o的節(jié)區(qū)表也列出
            There are 10 section headers, starting at offset 0xb4:

            Section Headers:
              [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
              [ 0]                   NULL            00000000 000000 000000 00      0   0  0
              [ 1] .text             PROGBITS        00000000 000034 000024 00  AX  0   0  4
              [ 2] .rel.text         REL             00000000 0002ec 000008 08      8   1  4
              [ 3] .data             PROGBITS        00000000 000058 000000 00  WA  0   0  4
              [ 4] .bss              NOBITS          00000000 000058 000000 00  WA  0   0  4
              [ 5] .comment          PROGBITS        00000000 000058 000012 00      0   0  1
              [ 6] .note.GNU-stack   PROGBITS        00000000 00006a 000000 00      0   0  1
              [ 7] .shstrtab         STRTAB          00000000 00006a 000049 00      0   0  1
              [ 8] .symtab           SYMTAB          00000000 000244 000090 10      9   7  4
              [ 9] .strtab           STRTAB          00000000 0002d4 000016 00      0   0  1
            Key to Flags:
              W (write), A (alloc), X (execute), M (merge), S (strings)
              I (info), L (link order), G (group), x (unknown)
              O (extra OS processing required) o (OS specific), p (processor specific)
            $ gcc -o test test.o libmyprintf.o
            $ readelf -l test        #我們發(fā)現(xiàn),test和test.o,libmyprintf.o相比,多了很多節(jié)區(qū),如.interp和.init等

            Elf file type is EXEC (Executable file)
            Entry point 0x80482b0
            There are 7 program headers, starting at offset 52

            Program Headers:
              Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
              PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
              INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1
                  [Requesting program interpreter: /lib/ld-linux.so.2]
              LOAD           0x000000 0x08048000 0x08048000 0x0047c 0x0047c R E 0x1000
              LOAD           0x00047c 0x0804947c 0x0804947c 0x00104 0x00108 RW  0x1000
              DYNAMIC        0x000490 0x08049490 0x08049490 0x000c8 0x000c8 RW  0x4
              NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4
              GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

             Section to Segment mapping:
              Segment Sections...
               00    
               01     .interp
               02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
               03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
               04     .dynamic
               05     .note.ABI-tag
               06    



                上表給出了可執(zhí)行文件的如下幾個(gè)段(segment),

            PHDR: 給出了程序表自身的大小和位置,不能出現(xiàn)一次以上。
            INTERP: 因?yàn)槌绦蛑姓{(diào)用了puts(在動(dòng)態(tài)鏈接庫(kù)中定義),使用了動(dòng)態(tài)連接庫(kù),因此需要?jiǎng)討B(tài)裝載器/鏈接器(ld-linux.so)
            LOAD: 包括程序的指令,.text等節(jié)區(qū)都映射在該段,只讀(R)
            LOAD: 包括程序的數(shù)據(jù),.data, .bss等節(jié)區(qū)都映射在該段,可讀寫(xiě)(RW)
            DYNAMIC: 動(dòng)態(tài)鏈接相關(guān)的信息,比如包含有引用的動(dòng)態(tài)連接庫(kù)名字等信息
            NOTE: 給出一些附加信息的位置和大小
            GNU_STACK: 這里為空,應(yīng)該是和GNU相關(guān)的一些信息

                這里的段可能包括之前的一個(gè)或者多個(gè)節(jié)區(qū),也就是說(shuō)經(jīng)過(guò)鏈接之后原來(lái)的節(jié)區(qū)被重排了,并映射到了不同的段,這些段將告訴系統(tǒng)應(yīng)該如何把它加載到內(nèi)存中。

                從上表中,通過(guò)比較可執(zhí)行文件(test)中擁有的節(jié)區(qū)和可重定位文件(test.o和myprintf.o)中擁有的節(jié)區(qū)后發(fā)現(xiàn),鏈接之后多了一些之前沒(méi)有的節(jié)區(qū),這些新的節(jié)區(qū)來(lái)自哪里?它們的作用是什么呢?先來(lái)通過(guò)gcc的-v參數(shù)看看它的后臺(tái)鏈接過(guò)程。

            Quote:

            $ gcc -v -o test test.o myprintf.o    #把可重定位文件鏈接成可執(zhí)行文件
            Reading specs from /usr/lib/gcc/i486-slackware-linux/4.1.2/specs
            Target: i486-slackware-linux
            Configured with: ../gcc-4.1.2/configure --prefix=/usr --enable-shared --enable-languages=ada,c,c++,fortran,java,objc --enable-threads=posix --enable-__cxa_atexit --disable-checking --with-gnu-ld --verbose --with-arch=i486 --target=i486-slackware-linux --host=i486-slackware-linux
            Thread model: posix
            gcc version 4.1.2
             /usr/libexec/gcc/i486-slackware-linux/4.1.2/collect2 --eh-frame-hdr -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crt1.o /usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/lib/gcc/i486-slackware-linux/4.1.2/../../../../i486-slackware-linux/lib -L/usr/lib/gcc/i486-slackware-linux/4.1.2/../../.. test.o myprintf.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crtn.o



                從上邊的演示看出,gcc在連接了我們自己的目標(biāo)文件test.o和myprintf.o之外,還連接了crt1.o,crtbegin.o等額外的目標(biāo)文件,難道那些新的節(jié)區(qū)就來(lái)自這些文件?
                另外gcc在進(jìn)行了相關(guān)配置(./configure)后,調(diào)用了collect2,卻并沒(méi)有調(diào)用ld,通過(guò)查找gcc文檔中和collect2相關(guān)的部 分發(fā)現(xiàn)collect2在后臺(tái)實(shí)際上還是去尋找ld命令的。為了理解gcc默認(rèn)連接的后臺(tái)細(xì)節(jié),這里直接把collect2替換成ld,并把一些路徑換成 絕對(duì)路徑或者簡(jiǎn)化,得到如下的ld命令以及執(zhí)行的效果。

            Quote:

            $ ld --eh-frame-hdr \
            -m elf_i386 \
            -dynamic-linker /lib/ld-linux.so.2 \
            -o test \
            /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o \
            test.o myprintf.o \
            -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/i486-slackware-linux/lib -L/usr/lib/ -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed \
            /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/crtn.o
            $ ./test
            hello, world!



            不出我們所料,它完美的運(yùn)行了。下面通過(guò)ld的手冊(cè)(man ld)來(lái)分析一下這幾個(gè)參數(shù)。

            --eh-frame-hdr

            要求創(chuàng)建一個(gè).eh_frame_hdr節(jié)區(qū)(貌似目標(biāo)文件test中并沒(méi)有這個(gè)節(jié)區(qū),所以不關(guān)心它)。

          15. -m elf_i386

            這 里指定不同平臺(tái)上的鏈接腳本,可以通過(guò)--verbose命令查看腳本的具體內(nèi)容,如ld -m elf_i386 --verbose,它實(shí)際上被存放在一個(gè)文件中(/usr/lib/ldscripts目錄下),你可以去修改這個(gè)腳本,具體如何做?請(qǐng)參考ld的手冊(cè)。 在后面我們將簡(jiǎn)要提到鏈接腳本中是如何預(yù)定義變量的,以及這些預(yù)定義變量如何在我們的程序中使用。需要提到的是,如果不是交叉編譯,那么無(wú)須指定該選項(xiàng)。

          16. -dynamic-linker /lib/ld-linux.so.2

            指定動(dòng)態(tài)裝載器/鏈接器,即程序中的INTERP段中的內(nèi)容。動(dòng)態(tài)裝載器/連接器負(fù)責(zé)連接有可共享庫(kù)的可執(zhí)行文件的裝載和動(dòng)態(tài)符號(hào)連接。

          17. -o test

            指定輸出文件,即可執(zhí)行文件名的名字

          18. /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o

            鏈 接到test文件開(kāi)頭的一些內(nèi)容,這里實(shí)際上就包含了.init等節(jié)區(qū)。.init節(jié)區(qū)包含一些可執(zhí)行代碼,在main函數(shù)之前被調(diào)用,以便進(jìn)行一些初始化操 作,在C++中完成構(gòu)造函數(shù)功能,更多細(xì)節(jié)請(qǐng)參考資料[9]

          19. test.o myprintf.o

            鏈接我們自己的可重定位文件

          20. -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/i486-slackware-linux/lib -L/usr/lib/ -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed

            鏈接libgcc庫(kù)和libc庫(kù),后者定義有我們需要的puts函數(shù)

          21. /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/crtn.o

            鏈接到test文件末尾的一些內(nèi)容,這里實(shí)際上包含了.fini等節(jié)區(qū)。.fini節(jié)區(qū)包含了一些可執(zhí)行代碼,在程序退出時(shí)被執(zhí)行,作一些清理工作,在C++中完成析構(gòu)造函數(shù)功能。我們往往可以通過(guò)atexit來(lái)注冊(cè)那些需要在程序退出時(shí)才執(zhí)行的函數(shù)。

                對(duì)于crtbegin.o和crtend.o這兩個(gè)文件,貌似完全是用來(lái)支持C++的構(gòu)造和析構(gòu)工作的[9],所以可以不鏈接到我們的可執(zhí)行文件中,鏈接時(shí)把它們?nèi)サ艨纯矗?br>
            Quote:

            $ ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o /usr/lib/crti.o test.o myprintf.o -L/usr/lib -lc /usr/lib/crtn.o    #后面發(fā)現(xiàn)不用鏈接libgcc,也不用--eh-frame-hdr參數(shù)
            $ readelf -l test

            Elf file type is EXEC (Executable file)
            Entry point 0x80482b0
            There are 7 program headers, starting at offset 52

            Program Headers:
              Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
              PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
              INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1
                  [Requesting program interpreter: /lib/ld-linux.so.2]
              LOAD           0x000000 0x08048000 0x08048000 0x003ea 0x003ea R E 0x1000
              LOAD           0x0003ec 0x080493ec 0x080493ec 0x000e8 0x000e8 RW  0x1000
              DYNAMIC        0x0003ec 0x080493ec 0x080493ec 0x000c8 0x000c8 RW  0x4
              NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4
              GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

             Section to Segment mapping:
              Segment Sections...
               00    
               01     .interp
               02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata
               03     .dynamic .got .got.plt .data
               04     .dynamic
               05     .note.ABI-tag
               06    
            $ ./test
            hello, world!



                完全可以工作,而且發(fā)現(xiàn).ctors(保存著程序中全局構(gòu)造函數(shù)的指針數(shù)組), .dtors(保存著程序中全局析構(gòu)函數(shù)的指針數(shù)組),.jcr(未知),.eh_frame節(jié)區(qū)都沒(méi)有了,所以crtbegin.o和crtend.o應(yīng)該包含了這些節(jié)區(qū)。
                而對(duì)于另外兩個(gè)文件crti.o和crtn.o,通過(guò)readelf -S查看后發(fā)現(xiàn)它們都有.init和.fini節(jié)區(qū),如果我們不需要讓程序進(jìn)行一些初始化和清理工作呢?是不是就可以不 鏈接這個(gè)兩個(gè)文件?試試看。

            Quote:

            $ ld  -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o test.o myprintf.o -L/usr/lib/ -lc
            /usr/lib/libc_nonshared.a(elf-init.oS): In function `__libc_csu_init':
            (.text+0x25): undefined reference to `_init'



            貌似不行,竟然有人調(diào)用了__libc_csu_init函數(shù),而這個(gè)函數(shù)引用了_init。這兩個(gè)符號(hào)都在哪里呢?

            Quote:

            $ readelf -s /usr/lib/crt1.o | grep __libc_csu_init
                18: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND __libc_csu_init
            $ readelf -s /usr/lib/crti.o | grep _init
                17: 00000000     0 FUNC    GLOBAL DEFAULT    5 _init



                竟然是crt1.o調(diào)用了__libc_csu_init函數(shù),而該函數(shù)卻引用了我們沒(méi)有鏈接的crti.o文件中定義的_init符號(hào)。這樣的話不鏈接 crti.o和crtn.o文件就不成了羅?不對(duì)吧,要不干脆不用crt1.o算了,看看gcc額外連接進(jìn)去的最后一個(gè)文件crt1.o到底干了個(gè)啥子?

            Quote:

            $ ld  -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib/ -lc
            ld: warning: cannot find entry symbol _start; defaulting to 00000000080481a4



                這樣卻說(shuō)沒(méi)有找到入口符號(hào)_start,難道crt1.o中定義了這個(gè)符號(hào)?不過(guò)它給默認(rèn)設(shè)置了一個(gè)地址,只是個(gè)警告,說(shuō)明test已經(jīng)生成,不管怎樣先運(yùn)行看看再說(shuō)。

            Quote:

            $ ./test
            hello, world!
            Segmentation fault



                貌似程序運(yùn)行完了,不過(guò)結(jié)束時(shí)冒出個(gè)段錯(cuò)誤?可能是程序結(jié)束時(shí)有問(wèn)題,用gdb調(diào)試看看:

            Quote:

            $ gcc -g -c test.c myprintf.c    #產(chǎn)生目標(biāo)代碼, 非交叉編譯,不指定-m也可以鏈接成功,所以下面可以去掉-m參數(shù)
            $ ld -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib -lc
            ld: warning: cannot find entry symbol _start; defaulting to 00000000080481d8   
            $ ./test
            hello, world!
            Segmentation fault
            $ gdb ./test
            ...
            (gdb) l
            1       #include "test.h"
            2
            3       int main()
            4       {
            5               myprintf();
            6               return 0;
            7       }
            (gdb) break 7            #在程序的末尾設(shè)置一個(gè)斷點(diǎn)
            Breakpoint 1 at 0x80481bf: file test.c, line 7.
            (gdb) r                    #程序都快結(jié)束了都沒(méi)問(wèn)題,怎么會(huì)到最后出個(gè)問(wèn)題呢?
            Starting program: /mnt/hda8/Temp/c/program/test
            hello, world!

            Breakpoint 1, main () at test.c:7
            7       }
            (gdb) n                    #單步執(zhí)行看看,怎么下面一條指令是0x00000001,肯定是程序退出以后出了問(wèn)題
            0x00000001 in ?? ()
            (gdb) n                    #誒,當(dāng)然找不到邊了,都跑到0x00000001了
            Cannot find bounds of current function
            (gdb) c                    #原來(lái)是這么回事,估計(jì)是return 0返回之后出問(wèn)題了,看看它的匯編去。
            Continuing.

            Program received signal SIGSEGV, Segmentation fault.
            0x00000001 in ?? ()
            $ gcc -S test.c #產(chǎn)生匯編代碼
            $ cat test.s    #后面就這么幾條指令,難不成ret返回有問(wèn)題,不讓它ret返回,把return改成_exit直接進(jìn)入內(nèi)核退出
            ...
                    call    myprintf
                    movl    $0, %eax
                    addl    $4, %esp
                    popl    %ecx
                    popl    %ebp
                    leal    -4(%ecx), %esp
                    ret
            ...
            $ vim test.c
            $ cat test.c    #就把return語(yǔ)句修改成_exit了。
            #include "test.h"
            #include <unistd.h> /* _exit */

            int main()
            {
                    myprintf();
                    _exit(0);
            }
            $ gcc -g -c test.c myprintf.c
            $  ld -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib -lc
            ld: warning: cannot find entry symbol _start; defaulting to 00000000080481d8
            $ ./test    #竟然好了,再看看匯編有什么不同
            hello, world!
            $ gcc -S test.c
            $ cat test.s    #貌似就把ret指令替換成了_exit函數(shù)調(diào)用,直接進(jìn)入內(nèi)核,然內(nèi)核讓處理了,那為什么ret有問(wèn)題呢?
            ...
                    call    myprintf
                    subl    $12, %esp
                    pushl   $0
                    call    _exit
            ...
            $ gdb ./test    #把代碼改回去(改成return 0;),再調(diào)試看看調(diào)用main函數(shù)返回時(shí)的下一條指令地址eip
            ...
            (gdb) l
            warning: Source file is more recent than executable.
            1       #include "test.h"
            2
            3       int main()
            4       {
            5               myprintf();
            6               return 0;
            7       }
            (gdb) break 5
            Breakpoint 1 at 0x80481b5: file test.c, line 5.
            (gdb) break 7
            Breakpoint 2 at 0x80481bc: file test.c, line 7.
            (gdb) r
            Starting program: /mnt/hda8/Temp/c/program/test

            Breakpoint 1, main () at test.c:5
            5               myprintf();
            (gdb) x/8x $esp    #發(fā)現(xiàn)0x00000001剛好是之前我們調(diào)試時(shí)看到的程序返回后的位置,即eip,說(shuō)明程序在初始化的時(shí)候
                            #這個(gè)eip就是錯(cuò)誤的。為什么呢?因?yàn)槲覀兏緵](méi)有鏈接進(jìn)來(lái)初始化的代碼,而是在編譯器自己給我們
                            #初始化了一個(gè)程序入口即00000000080481d8,也就是說(shuō),沒(méi)有任何人調(diào)用main,main不知道返回哪里去
                            #所以,我們直接讓main結(jié)束時(shí)進(jìn)入內(nèi)核調(diào)用_exit而退出則不會(huì)有問(wèn)題
            0xbf929510:     0xbf92953c      0x080481a4      0x00000000      0xb7eea84f
            0xbf929520:     0xbf92953c      0xbf929534      0x00000000      0x00000001 



                通過(guò)上面的演示和解釋發(fā)現(xiàn)只要把return語(yǔ)句修改為_(kāi)exit語(yǔ)句,程序即使不鏈接任何額外的目標(biāo)代碼都可以正常運(yùn)行(原因是不連接那些額外的文件時(shí) 相當(dāng)于沒(méi)有進(jìn)行初始化操作,如果在程序的最后執(zhí)行ret匯編指令,程序?qū)o(wú)法獲得正確的eip,從而無(wú)法進(jìn)行后續(xù)的動(dòng)作)。但是為什么會(huì)有“找不到 _start符號(hào)”的警告呢?通過(guò)readelf -s查看crt1.o發(fā)現(xiàn)里頭有這個(gè)符號(hào),并且crt1.o引用了main這個(gè)符號(hào),是不是意味著會(huì)從_start進(jìn)入main呢?是不是程序入口是 _start,而并非main呢?

                先來(lái)看看剛才提到的鏈接器的默認(rèn)鏈接腳本(ld -m elf_386 --verbose),它告訴我們程序的入口(entry)是_start,而一個(gè)可執(zhí)行文件必須有一個(gè)入口地址才能運(yùn)行,所以這就是說(shuō)明了為什么ld一 定要提示我們“_start找不到”,找不到以后就給默認(rèn)設(shè)置了一個(gè)地址。

            Quote:

            $ ld --verbose  | grep ^ENTRY    #非交叉編譯,可不用-m參數(shù);ld默認(rèn)找_start入口,并不是main哦!
            ENTRY(_start)



                原來(lái)是這樣,程序的入口(entry)竟然不是main函數(shù),而是_start。那干脆把匯編里頭的main給改掉算了,看行不行?

            Quote:

            $ cat test.c
            #include "test.h"
            #include <unistd.h>     /* _exit */

            int main()
            {
                    myprintf();
                    _exit(0);
            }
            $ gcc -S test.c
            $ sed -i -e "s#main#_start#g" test.s    #把匯編中的main全部修改為_(kāi)start,即修改程序入口為_(kāi)start
            $ gcc -c test.s myprintf.c
            $ ld -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib/ -lc    #果然沒(méi)問(wèn)題了 :-)
            $ ./test
            hello, world!



                _start竟然是真正的程序入口,那在有main的情況下呢?為什么在_start之后能夠找到main呢?這個(gè)看看alert7大叔的"Before main分析"[5]吧,這里不再深入介紹。總之呢,通過(guò)修改程序的return語(yǔ)句為_(kāi)exit(0)和修改程序的入口為_(kāi)start,我們的代碼不鏈接gcc默認(rèn)鏈 接的那些額外的文件同樣可以工作得很好。并且打破了一個(gè)學(xué)習(xí)C語(yǔ)言以來(lái)的常識(shí):main函數(shù)作為程序的主函數(shù),是程序的入口,實(shí)際上則不然。

                再補(bǔ)充一點(diǎn)內(nèi)容,在ld的鏈接腳本中,有一個(gè)特別的關(guān)鍵字PROVIDE,由這個(gè)關(guān)鍵字定義的符號(hào)是ld的預(yù)定義字符,我們可以在C語(yǔ)言函數(shù)中擴(kuò)展它們后直接使用。這些特別的符號(hào)可以通過(guò)下面的方法獲取,

            Quote:

            $ ld --verbose | grep PROVIDE | grep -v HIDDEN
              PROVIDE (__executable_start = 0x08048000); . = 0x08048000 + SIZEOF_HEADERS;
              PROVIDE (__etext = .);
              PROVIDE (_etext = .);
              PROVIDE (etext = .);
              _edata = .; PROVIDE (edata = .);
              _end = .; PROVIDE (end = .);


                這里面有幾個(gè)我們比較關(guān)心的,第一個(gè)是程序的入口地址__executable_start,另外三個(gè)是etext,edata,end,分別對(duì)應(yīng)程序的 代碼段(text)、初始化數(shù)據(jù)(data)和未初始化的數(shù)據(jù)(bss)(可以參考資料[6]和man etext),如何引用這些變量呢?看看這個(gè)例子。


            Code:

            [Ctrl+A Select All]



                到這里,程序鏈接過(guò)程的一些細(xì)節(jié)都介紹得差不多了。在《動(dòng)態(tài)符號(hào)鏈接的細(xì)節(jié)》中將主要介紹ELF文件的動(dòng)態(tài)符號(hào)鏈接過(guò)程。

            本節(jié)參考資料

            [1] An beginners guide to compiling programs under Linux.
            http://www.luv.asn.au/overheads/compile.html
            [2] gcc manual
            http://gcc.gnu.org/onlinedocs/gcc-4.2.2/gcc/
            [3] A Quick Tour of Compiling, Linking, Loading, and Handling Libraries on Unix
            http://efrw01.frascati.enea.it/Software/Unix/IstrFTU/cern-cnl-2001-003-25-link.html
            [4] Unix 目標(biāo)文件初探
            http://www.ibm.com/developerworks/cn/aix/library/au-unixtools.html
            [5] Before main()分析
            http://www.xfocus.net/articles/200109/269.html
            [6] A Process Viewing Its Own /proc/<PID>/map Information
            http://www.linuxforums.org/forum/linux-kernel/51790-process-viewing-its-own-proc-pid-map-information.html
          22. posted @ 2008-03-14 15:24 隨意門(mén) 閱讀(7504) | 評(píng)論 (3)編輯 收藏
            GCC編譯背后(第一部分:預(yù)處理和編譯)

            by falcon <zhangjinw@gmail.com>

            平時(shí)在Linux下寫(xiě)代碼,直接用"gcc -o out in.c"就把代碼編譯好了,但是這后面到底做了什么事情呢?如果學(xué)習(xí)過(guò)編譯原理則不難理解,一般高級(jí)語(yǔ)言程序編譯的過(guò)程莫過(guò)于:預(yù)處理、編譯、匯編、鏈 接。gcc在后臺(tái)實(shí)際上也經(jīng)歷了這幾個(gè)過(guò)程,我們可以通過(guò)-v參數(shù)查看它的編譯細(xì)節(jié),如果想看某個(gè)具體的編譯過(guò)程,則可以分別使用-E,-S,-c和- O,對(duì)應(yīng)的后臺(tái)工具則分別為cpp,cc1,as,ld。下面我們將逐步分析這幾個(gè)過(guò)程以及相關(guān)的內(nèi)容,諸如語(yǔ)法檢查、代碼調(diào)試、匯編語(yǔ)言等。

            1、預(yù)處理

                開(kāi)篇簡(jiǎn)述:預(yù)處理是C語(yǔ)言程序從源代碼變成可執(zhí)行程序的第一步,主要是C語(yǔ)言編譯器對(duì)各種預(yù)處理命令進(jìn)行處理,包括頭文件的包含、宏定義的擴(kuò)展、條件編譯的選擇等。

                以前沒(méi)怎么“深入”預(yù)處理,腦子對(duì)這些東西總是很模糊,只記得在編譯的基本過(guò)程(詞法分析、語(yǔ)法分析)之前還需要對(duì)源代碼中的宏定義、文件包含、條件編譯 等命令進(jìn)行處理。這三類的指令很常見(jiàn),主要有#define, #include和#ifdef ... #endif,要特別地注意它們的用法。(更多預(yù)處理的指令請(qǐng)查閱相關(guān)資料)

                #define除了可以獨(dú)立使用以便靈活設(shè)置一些參數(shù)外,還常常和#ifdef ... #endif結(jié)合使用,以便靈活地控制代碼塊的編譯與否,也可以用來(lái)避免同一個(gè)頭文件的多次包含。關(guān)于#include貌似比較簡(jiǎn)單,通過(guò)man找到某個(gè) 函數(shù)的頭文件,copy進(jìn)去,加上<>就okay。這里雖然只關(guān)心一些技巧,不過(guò)預(yù)處理還是蘊(yùn)含著很多潛在的陷阱(可參考<C Traps & Pitfalls>),我們也需要注意的。下面僅介紹和預(yù)處理相關(guān)的幾個(gè)簡(jiǎn)單內(nèi)容。

          23. 打印出預(yù)處理之后的結(jié)果:gcc -E hello.c

                這樣我們就可以看到源代碼中的各種預(yù)處理命令是如何被解釋的,從而方便理解和查錯(cuò)。

                實(shí)際上gcc在這里是調(diào)用了cpp的(雖然我們通過(guò)gcc的-v僅看到cc1),cpp即The C Preprocessor,主要用來(lái)預(yù)處理宏定義、文件包含、條件編譯等。下面介紹它的一個(gè)比較重要的選項(xiàng)-D。

          24. 在命令行定義宏:gcc -Dmacro hello.c

                這個(gè)等同于在文件的開(kāi)頭定義宏,即#define maco,但是在命令行定義更靈活。例如,在源代碼中有這些語(yǔ)句。
            #ifdef DEBUG
            printf("this code is for debugging\n");
            #endif

                如果編譯時(shí)加上-DDEBUG選項(xiàng),那么編譯器就會(huì)把printf所在的行編譯進(jìn)目標(biāo)代碼,從而方便地跟蹤該位置的某些程序狀態(tài)。這樣-DDEBUG就可以當(dāng)作一個(gè)調(diào)試開(kāi)關(guān),編譯時(shí)加上它就可以用來(lái)打印調(diào)試信息,發(fā)布時(shí)則可以通過(guò)去掉該編譯選項(xiàng)把調(diào)試信息去掉。

            本節(jié)參考資料:
            [1] C語(yǔ)言教程第九章:預(yù)處理
            http://www.bc-cn.net/Article/kfyy/cyy/jc/200409/9.html
            [2] 更多
            http://www.hemee.com/kfyy/c/6626.html
            http://www.91linux.com/html/article/program/cpp/20071203/8745.html
            http://www.janker.org/bbs/programmer/2006-10-13/327.html

            2、編譯(翻譯)

                開(kāi)篇簡(jiǎn)要:編譯之前,C語(yǔ)言編譯器會(huì)進(jìn)行詞法分析、語(yǔ)法分析(-fsyntax-only),接著會(huì)把源代碼翻譯成中間語(yǔ)言,即匯編語(yǔ)言。如果想看到這個(gè) 中間結(jié)果,可以用-S選項(xiàng)。需要提到的是,諸如shell等解釋語(yǔ)言也會(huì)經(jīng)歷一個(gè)詞法分析和語(yǔ)法分析的階段,不過(guò)之后并不會(huì)進(jìn)行“翻譯”,而是“解釋”, 邊解釋邊執(zhí)行。

                把源代碼翻譯成匯編語(yǔ)言,實(shí)際上是編譯的整個(gè)過(guò)程中的第一個(gè)階段,之后的階段和匯編語(yǔ)言的開(kāi)發(fā)過(guò)程沒(méi)有什么區(qū)別。這個(gè)階段涉及到對(duì)源代碼的詞法分析、語(yǔ)法檢查(通過(guò)-std指定遵循哪個(gè)標(biāo)準(zhǔn)),并根據(jù)優(yōu)化(-O)要求進(jìn)行翻譯成匯編語(yǔ)言的動(dòng)作。

                如果僅僅希望進(jìn)行語(yǔ)法檢查,可以用-fsyntax-only選項(xiàng);而為了使代碼有比較好的移植性,避免使用gcc的一些特性,可以結(jié)合-std和- pedantic(或者-pedantic-erros)選項(xiàng)讓源代碼遵循某個(gè)C語(yǔ)言標(biāo)準(zhǔn)的語(yǔ)法。這里演示一個(gè)簡(jiǎn)單的例子。

            Quote:

            $ cat hello.c
            #include <stdio.h>
            int main()
            {
                    printf("hello, world\n")
                    return 0;
            }
            $ gcc -fsyntax-only hello.c
            hello.c: In function ‘main’:
            hello.c:5: error: expected ‘;’ before ‘return’
            $ vim hello.c
            $ cat hello.c
            #include <stdio.h>
            int main()
            {
                    printf("hello, world\n");
                    int i;
                    return 0;
            }
            $ gcc -std=c89 -pedantic-errors hello.c    #默認(rèn)情況下,gcc是允許在程序中間聲明變量的,但是turboc就不支持
            hello.c: In function ‘main’:
            hello.c:5: error: ISO C90 forbids mixed declarations and code



                語(yǔ)法錯(cuò)誤是程序開(kāi)發(fā)過(guò)程中難以避免的錯(cuò)誤(人的大腦在很多條件下都容易開(kāi)小差),不過(guò)編譯器往往能夠通過(guò)語(yǔ)法檢查快速發(fā)現(xiàn)這些錯(cuò)誤,并準(zhǔn)確地告訴你語(yǔ)法錯(cuò) 誤的大概位置。因此,作為開(kāi)發(fā)人員,要做的事情不是“恐慌”(不知所措),而是認(rèn)真閱讀編譯器的提示,根據(jù)平時(shí)積累的經(jīng)驗(yàn)(最好在大腦中存一份常見(jiàn)語(yǔ)法錯(cuò) 誤索引,很多資料都提供了常見(jiàn)語(yǔ)法錯(cuò)誤列表,如<C Traps&Pitfalls>和最后面的參考資料[12]也列出了很多常見(jiàn)問(wèn)題)和編輯器提供的語(yǔ)法檢查功能(語(yǔ)法加亮、括號(hào)匹配提示 等)快速定位語(yǔ)法出錯(cuò)的位置并進(jìn)行修改。

                語(yǔ)法檢查之后就是翻譯動(dòng)作,gcc提供了一個(gè)優(yōu)化選項(xiàng)-O,以便根據(jù)不同的運(yùn)行平臺(tái)和用戶要求產(chǎn)生經(jīng)過(guò)優(yōu)化的匯編代碼。例如,

            Quote:

            $ gcc -o hello hello.c            #采用默認(rèn)選項(xiàng),不優(yōu)化
            $ gcc -O2 -o hello2 hello.c        #優(yōu)化等次是2
            $ gcc -Os -o hellos hello.c        #優(yōu)化目標(biāo)代碼的大小
            $ ls -S hello hello2 hellos        #可以看到,hellos比較小,hello2比較大
            hello2  hello  hellos
            $ time ./hello
            hello, world

            real    0m0.001s
            user    0m0.000s
            sys     0m0.000s
            $ time ./hello2                #可能是代碼比較少的緣故,執(zhí)行效率看上去不是很明顯
            hello, world

            real    0m0.001s
            user    0m0.000s
            sys     0m0.000s

            $ time ./hellos                #雖然目標(biāo)代碼小了,但是執(zhí)行效率慢了些
            hello, world

            real    0m0.002s
            user    0m0.000s
            sys     0m0.000s



                根據(jù)上面的簡(jiǎn)單演示,可以看出gcc有很多不同的優(yōu)化選項(xiàng),主要看用戶的需求了,目標(biāo)代碼的大小和效率之間貌似存在一個(gè)“糾纏”,需要開(kāi)發(fā)人員自己權(quán)衡。

                下面我們通過(guò)-S選項(xiàng)來(lái)看看編譯出來(lái)的中間結(jié)果,匯編語(yǔ)言,還是以之前那個(gè)hello.c為例。
            Quote:

            $ gcc -S hello.c        #默認(rèn)輸出是hello.s,可自己指定,輸出到屏幕-o -,輸出到其他文件-o file
            $ cat hello.s
            cat hello.s
                    .file   "hello.c"
                    .section        .rodata
            .LC0:
                    .string "hello, world"
                    .text
            .globl main
                    .type   main, @function
            main:
                    leal    4(%esp), %ecx
                    andl    $-16, %esp
                    pushl   -4(%ecx)
                    pushl   %ebp
                    movl    %esp, %ebp
                    pushl   %ecx
                    subl    $4, %esp
                    movl    $.LC0, (%esp)
                    call    puts
                    movl    $0, %eax
                    addl    $4, %esp
                    popl    %ecx
                    popl    %ebp
                    leal    -4(%ecx), %esp
                    ret
                    .size   main, .-main
                    .ident  "GCC: (GNU) 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)"
                    .section        .note.GNU-stack,"",@progbits



                不知道看出來(lái)沒(méi)?和我們?cè)谡n堂里學(xué)的intel的匯編語(yǔ)法不太一樣,這里用的是AT&T語(yǔ)法格式。如果之前沒(méi)接觸過(guò)AT&T的,可以看看 參考資料[2]。如果想學(xué)習(xí)Linux下的匯編語(yǔ)言開(kāi)發(fā),從下一節(jié)開(kāi)始哦,下一節(jié)開(kāi)始的所有章節(jié)基本上覆蓋了Linux下匯編語(yǔ)言開(kāi)發(fā)的一般過(guò)程,不過(guò)這 里不介紹匯編語(yǔ)言語(yǔ)法。

                這里需要補(bǔ)充的是,在寫(xiě)C語(yǔ)言代碼時(shí),如果能夠?qū)幾g器比較熟悉(工作原理和一些細(xì)節(jié))的話,可能會(huì)很有幫助。包括這里的優(yōu)化選項(xiàng)(有些優(yōu)化選項(xiàng)可能在匯 編時(shí)采用)和可能的優(yōu)化措施,例如字節(jié)對(duì)齊(可以看看這本書(shū)"Linux_Assembly_Language_Programming"的第六小節(jié))、 條件分支語(yǔ)句裁減(刪除一些明顯分支)等。

            本節(jié)參考資料

            [1] Guide to Assembly Language Programming in Linux(pdf教程,社區(qū)有下載)
            http://oss.lzu.edu.cn/modules/wfdownloads/singlefile.php?cid=5&lid=94
            [2] Linux匯編語(yǔ)言開(kāi)發(fā)指南(在線):
            http://www.ibm.com/developerworks/cn/linux/l-assembly/index.html
            [3] PowerPC 匯編
            http://www.ibm.com/developerworks/cn/linux/hardware/ppc/assembly/index.html
            [4] 用于 Power 體系結(jié)構(gòu)的匯編語(yǔ)言
            http://www.ibm.com/developerworks/cn/linux/l-powasm1.html
            [5] Linux Assembly HOWTO
            http://mirror.lzu.edu.cn/tldp/HOWTO/Assembly-HOWTO/
            [6] Linux 中 x86 的內(nèi)聯(lián)匯編
            http://www.ibm.com/developerworks/cn/linux/sdk/assemble/inline/index.html
            [7] Linux Assembly Language Programming
            http://mirror.lzu.edu.cn/doc/incoming/ebooks/linux-unix/Linux_EN_Original_Books

          25. posted @ 2008-03-14 15:22 隨意門(mén) 閱讀(904) | 評(píng)論 (0)編輯 收藏
            把VIM打造成源代碼的編輯器

            by falcon<zhangjinw@gmail.com>
            2008-02-22

                程序開(kāi)發(fā)過(guò)程中,源代碼的編輯主要是為了實(shí)現(xiàn)算法,結(jié)果則是一些可閱讀的、便于檢錯(cuò)的、可移植的...文本文件。如何產(chǎn)生一份良好的源代碼文件,這不僅需要一些良好的編輯工具,還需要開(kāi)發(fā)人員養(yǎng)成良好的編程修養(yǎng)[3][4]。

                Linux下有很多優(yōu)秀的程序編輯工具,包括專門(mén)的文本編輯器和一些集成開(kāi)發(fā)環(huán)境(IDE)中提供的編輯工具,前者的代表作有vim和emacs,后者的代表作則有Eclipse,Kdevelope,Anjuta等,這里主要介紹VIM的基本使用和一些基本配置。

                通過(guò)VIM進(jìn)行文本編輯的一般過(guò)程(如附圖二)包括:文件的打開(kāi)、編輯、保存、關(guān)閉,而編輯則包括插入新內(nèi)容、替換已有內(nèi)容、查找內(nèi)容,還包括復(fù)制、粘貼、刪除等基本操作。

               

          26. 打開(kāi)文件

                在命令行下輸入"vim+文件名"即可打開(kāi)一個(gè)新的文件并進(jìn)入VIM的“編輯模式”。編輯模式可以切換到命令模式(按下字符:)和插入模式(按下字母 a/A/i/I/o/O/s/S/c/C等或者Insert鍵)。編輯模式下,VIM會(huì)把鍵盤(pán)輸入解釋成VIM的編輯命令,以便實(shí)現(xiàn)諸如字符串查找(按下字母/)、文本復(fù)制(按下字母yy)、粘貼(按下字母pp)、刪除(按下字母d等)、替換(s)等各種操作。當(dāng)按下 a/A/i/I/o/O/s/S/c/C等字符時(shí),VIM先執(zhí)行這些字符對(duì)應(yīng)的命令動(dòng)作(比如移動(dòng)光標(biāo)到某個(gè)位置,刪除某些字符),然后進(jìn)入插入模式;進(jìn)入插入模式后可以通過(guò)按下ESC鍵或者是CTRL+C返回到編輯模式,當(dāng)然,在編輯模式下輸入冒號(hào)":"后可進(jìn)入命令模式,通過(guò)它可以完成一些復(fù)雜的編輯功能,比如進(jìn)行正則表達(dá)式匹配替換,執(zhí)行shell命令等。實(shí)際上,無(wú)論是插入模式還是命令模式都是編輯模式的一種。而編輯模式卻并不止它們兩個(gè),還有字符串查找、刪除、替換等。需要提到的是,如果在編輯模式按下字母v/V或者是CTRL+V,可以用光標(biāo)選擇一片代碼,進(jìn)而結(jié)合命令模式對(duì)這一片代碼進(jìn)行特定的操作。

               
          27. 編輯文件

                打開(kāi)文件以后即可進(jìn)入編輯模式,這時(shí)可以進(jìn)行各種編輯操作,包括插入、復(fù)制、刪除、替換字符。其中兩種比較重要的模式經(jīng)常被“獨(dú)立”出來(lái),即上面提到的插入模式和命令模式。

               
          28. 保存文件

                在退出之前需切換到命令模式,輸入命令w以便保存各種編輯操作,如果想取消某種操作,可以用u命令。如果打開(kāi)vim編輯器時(shí)沒(méi)有設(shè)定文件名,那么在按下w命令時(shí)會(huì)提示沒(méi)有文件名,此時(shí)需要在w命令后加上需要保存的文件名。

               
          29. 退出

                保存好內(nèi)容后就可退出,只需在命令模式下鍵入字符q。如果對(duì)文件內(nèi)容進(jìn)行了編輯,卻沒(méi)有保存,那么VIM會(huì)提示你保存內(nèi)容,如果不想保存之前的編輯動(dòng)作,那么可按下字符q并且在之后跟上一個(gè)感嘆號(hào)!,這樣會(huì)強(qiáng)制退出,不保存最近的內(nèi)容變更。

                這里需要著重提到的是VIM的命令模式,它是VIM擴(kuò)展各種新功能的接口,用戶可以通過(guò)它啟用和撤銷某個(gè)功能,開(kāi)發(fā)人員則可通過(guò)它為用戶提供新的功能。下面主要介紹通過(guò)命令模式這個(gè)接口定制VIM以便我們更好地進(jìn)行源代碼的編輯(對(duì)于其他的內(nèi)容建議看看參考資料中提到的VIM的官方教程和VIM實(shí)用技術(shù)序列,以及其他網(wǎng)友總結(jié)的VIM使用技巧等)。

                這里先提一下編碼風(fēng)格。剛學(xué)習(xí)編程時(shí),代碼寫(xiě)得很“難看”(不方便閱讀,不方便檢錯(cuò),看不出任何邏輯結(jié)構(gòu)),常常導(dǎo)致心情不好,而且排錯(cuò)也很困難,所以逐漸意識(shí)到代碼編寫(xiě)需要規(guī)范,即養(yǎng)成良好的編碼風(fēng)格,如果換成俗話,那就是代碼的排版,讓代碼好看一些。雖說(shuō)“編程的“(高雅一些則稱開(kāi)發(fā)人員)不一定懂藝術(shù),不過(guò)這個(gè)應(yīng)該不是“搞藝術(shù)的”(高雅一些應(yīng)該是文藝工作人員)的特權(quán),而是我們應(yīng)該具備的專業(yè)素養(yǎng)。在Linux下,比較流行的“行業(yè)”風(fēng)格有KR的編碼風(fēng)格、gnu的編碼風(fēng)格、linux內(nèi)核的編碼風(fēng)格(基于KR的,縮進(jìn)是8個(gè)空格)等,它們都可以通過(guò)indent命令格式化,對(duì)應(yīng)的選項(xiàng)分別是- kr,-gnu,-kr -i8。下面演示用indent把代碼格式化成上面的三種風(fēng)格。

            Quote:

            $ vim test.c
            $ cat test.c                  //這樣糟糕的編碼風(fēng)格看者會(huì)讓人想“哭”,太難閱讀啦。
            cat test.c
            /* test.c -- a test program for using indent */
            #include<stdio.h>

            int main(int argc, char *argv[])
            {
             int i=0;
             if (i != 0) {i++; }
             else {i--; };
             for(i=0;i<5;i++)j++;
             printf("i=%d,j=%d\n",i,j);

             return 0;
            }
            $ indent -kr test.c
            $ cat test.c            //好看多了
            /* test.c -- a test program for using indent */
            #include<stdio.h>

            int main(int argc, char *argv[])
            {
                int i = 0;
                if (i != 0) {
                    i++;
                } else {
                    i--;
                };
                for (i = 0; i < 5; i++)
                    j++;
                printf("i=%d,j=%d\n", i, j);
                return 0;
            }
            $ indent -gnu test.c
            $ cat test.c      //感覺(jué)不如kr的風(fēng)格,處理if語(yǔ)句時(shí)增加了代碼行,卻并沒(méi)明顯改進(jìn)效果
            /* test.c -- a test program for using indent */
            #include<stdio.h>

            int
            main (int argc, char *argv[])
            {
              int i = 0;
              if (i != 0)
                {
                  i++;
                }
              else
                {
                  i--;
                };
              for (i = 0; i < 5; i++)
                j++;
              printf ("i=%d,j=%d\n", i, j);
              return 0;
            }



                從演示中可看出編碼風(fēng)格真的很重要,但是如何養(yǎng)成良好的編碼風(fēng)格呢?經(jīng)常練習(xí),遵守某個(gè)編碼風(fēng)格,一如既往。不過(guò)這還不夠,如果沒(méi)有一個(gè)好編輯器,習(xí)慣也很難養(yǎng)成。而VIM提供了很多輔助我們養(yǎng)成良好編碼習(xí)慣的功能,這些都通過(guò)它的命令模式提供。現(xiàn)在分開(kāi)介紹幾個(gè)功能;
            語(yǔ)法加“靚”(亮) :sytax on
            自動(dòng)縮進(jìn)寬度(需要set cin才有用):set sw=8
            TAB寬度:set ts=8
            顯示行號(hào);set number
            括號(hào)自動(dòng)匹配;set sm
            C語(yǔ)言自動(dòng)縮進(jìn):set cin

                這幾個(gè)對(duì)代碼編寫(xiě)來(lái)說(shuō)非常有用,可以考慮把它們?nèi)繉?xiě)到~/.vimrc文件(vim啟動(dòng)的時(shí)候會(huì)去執(zhí)行這個(gè)文件里頭的內(nèi)容)中,如;
            Quote:

            $ vim ~/.vimrc
            $ cat ~/.vimrc
            :set number
            :set sw=8
            :set ts=8
            :set sm
            :set cin
            :syntax on
            :set textwidth=70
            :set mouse=a
            :set encoding=utf-8
            :set fileencoding=chinese
            :set fileencodings=ucs-bom, utf-8, chinese
            :set ambiwidth=double
            nmap <F2> :nohlsearch <CR>

            :abbr #b /***************************************************************************************

            :abbr #e ***************************************************************************************/



            需要補(bǔ)充的幾個(gè)技巧有;

          30. 在編輯模式下,可通過(guò)gqap命令對(duì)注釋自動(dòng)斷行(每行字符個(gè)數(shù)可通過(guò)命令模式下的"set textwidth=個(gè)數(shù)"設(shè)定)
          31. 命令模式下輸入數(shù)字可以直接跳到指定行,也可在打開(kāi)文件時(shí)用“vim +數(shù)字 文件名”實(shí)現(xiàn)相同的功能。
          32. 命令模式下的TOhtml命令可把C語(yǔ)言輸出為html文件,結(jié)合syntax on,可產(chǎn)生比較好的web page把代碼發(fā)布出去。
          33. 先切換到可視模式(編輯模式下按字母v可切換過(guò)來(lái)),用光標(biāo)選中一片代碼,然后通過(guò)命令模式下的命令“s#^#//#g"把某一片代碼給注釋掉,這非常方便調(diào)試某一片代碼的功能。
          34. 命令模式下的”set paste“可解決復(fù)制本來(lái)已有縮進(jìn)的代碼的自動(dòng)縮進(jìn)問(wèn)題,后可執(zhí)行”set nopaste“恢復(fù)自動(dòng)縮進(jìn)。
          35. 為了使用最新的vim特性,可用"set nocp"取消與老版本的vi的兼容。
          36. 如發(fā)現(xiàn)變量命名不好,想在整個(gè)代碼中修改,可在命令模式下用"%s#old_variable#new_variable#g"全局替換。替換的時(shí)注意變量名是其他變量一部分的情況。
          37. 如果想把縮進(jìn)和TAB鍵替換成空格,可考慮設(shè)置expandtab,即“set et”,如果要把以前編寫(xiě)的代碼中的縮進(jìn)和TAB鍵都替換掉,可以用retab。
          38. 為實(shí)現(xiàn)關(guān)鍵字補(bǔ)全,輸入一部分字符后,按下CTRL+P即可。比如先輸入prin,然后按下CTRL+P就可以補(bǔ)全了。
          39. 如果想在在編輯模式下查看Linux手冊(cè),可把光標(biāo)定位到在某個(gè)函數(shù),按下Shift+k就可以調(diào)出man,很有用。
          40. 刪除空行,在命令模式下輸入g/^$/d,前面g命令是擴(kuò)展到全局,中間是匹配空行,后面d命令是執(zhí)行刪除動(dòng)作。用替換也可以實(shí)現(xiàn),鍵入%s#^\ n##g,意思是把所有以換行開(kāi)頭的行全部替換為空。類似地,如果要把多個(gè)空行轉(zhuǎn)換為一個(gè)可以輸入g/^\n$/d或者%s#^\n$##g。
          41. 注意使用一些有用的插件,比如ctags, cscope等,可以提高代碼閱讀、分析的效率。特別是open source的東西。


            更多的技巧可以看看資料[2],[6],[7]。

                實(shí)際上,在源代碼編寫(xiě)時(shí)還有很多需要培養(yǎng)的“素質(zhì)”,例如源文件的開(kāi)頭注釋、函數(shù)的注釋,變量的命名等。這方面建議看看參考資料里的編程修養(yǎng)、內(nèi)核編碼風(fēng)格、網(wǎng)絡(luò)上流傳的《華為編程規(guī)范》,以及<C Traps & Pitfalls>等。

            參考的資料:

            [1] VIM官方教程,在命令行下鍵入vimtutor即可
            [2] IBM developerworks 中國(guó),VIM實(shí)用技術(shù)序列
            實(shí)用技巧,http://www.ibm.com/developerworks/cn/linux/l-tip-vim1/
            常用插件,http://www.ibm.com/developerworks/cn/linux/l-tip-vim2/
            定制VIM,http://www.ibm.com/developerworks/cn/linux/l-tip-vim3/
            [3] 編程修養(yǎng)
            http://oss.lzu.edu.cn/modules/newbb/viewtopic.php?topic_id=126&forum=13
            [4] 內(nèi)核編碼風(fēng)格
            Document/codingstyle
            [5] Linux C編程(主要介紹了一些簡(jiǎn)單的技巧)
            http://blog.ednchina.com/brucedeng/4695/message.aspx
            [6] vim配置,配置得當(dāng)可極大方便編程等工作
            http://oss.lzu.edu.cn/blog/article.php?tid_1398.html
            [7] VIM高級(jí)命令集錦
            http://oss.lzu.edu.cn/modules/newbb/viewtopic.php?topic_id=830&forum=6
            [8] C Traps & Pitfalls(英文版,不過(guò)比較簡(jiǎn)單呢)
            http://oss.lzu.edu.cn/modules/wfdownloads/singlefile.php?cid=6&lid=64
          42. posted @ 2008-03-14 15:20 隨意門(mén) 閱讀(805) | 評(píng)論 (0)編輯 收藏
            Linux下C語(yǔ)言程序開(kāi)發(fā)過(guò)的程視圖

            by falcon<zhangjinw@gmail.com>
            2008-03-01

                到今天,關(guān)于"Linux下C語(yǔ)言開(kāi)發(fā)過(guò)程"的一個(gè)簡(jiǎn)單視圖總算粗略的完成了,從寒假之前的一段時(shí)間到現(xiàn)在過(guò)了將近一個(gè)月左右吧。寫(xiě)這個(gè)主題的目的源自 “shell編程范例之進(jìn)程操作”,當(dāng)我寫(xiě)到“shell編程范例之進(jìn)程操作”這一節(jié)時(shí),“突然”對(duì)進(jìn)程的由來(lái)、本身和去向感到“迷惑不解”。所以想著好 好花些時(shí)間來(lái)弄清楚它們,現(xiàn)在發(fā)現(xiàn),這個(gè)由來(lái)就是這里的程序開(kāi)發(fā)過(guò)程,進(jìn)程來(lái)自一個(gè)普通的文本文件,在這里是C語(yǔ)言程序,C語(yǔ)言程序經(jīng)過(guò)編輯、預(yù)處理、編 譯、匯編、鏈接、執(zhí)行而成為一個(gè)進(jìn)程;而進(jìn)程本身呢?當(dāng)一個(gè)可執(zhí)行文件被執(zhí)行以后,有了exec調(diào)用,被程序解釋器映射到了內(nèi)存中,有了它的內(nèi)存映像;而 進(jìn)程的去向呢?通過(guò)不斷的執(zhí)行指令和內(nèi)存映像的變化,進(jìn)程完成著各項(xiàng)任務(wù),等任務(wù)完成以后就可以退出了(exit)。
                這樣一份視圖實(shí)際上是在寒假之前繪好的,你可以從附件中看到它;不過(guò)到現(xiàn)在才明白背后的很多細(xì)節(jié)。這些細(xì)節(jié)就是下面的這些blogs,你可以對(duì)照“視圖”來(lái)閱讀它們。
                1、把VIM打造成源代碼編輯器(源代碼編輯過(guò)程:用VIM編輯代碼的一些技巧)
                2、GCC編譯的背后 第一部分:預(yù)處理和編譯 第二部分:匯編和鏈接(編譯過(guò)程:預(yù)處理、編譯、匯編、鏈接)
                3、程序執(zhí)行的那一剎那 (執(zhí)行過(guò)程:當(dāng)我們從命令行輸入一個(gè)命令之后)
                4、進(jìn)程的內(nèi)存映像 (進(jìn)程加載過(guò)程:程序在內(nèi)存里是個(gè)什么樣子)
                5、動(dòng)態(tài)符號(hào)鏈接的細(xì)節(jié)(動(dòng)態(tài)鏈接過(guò)程:函數(shù)puts/printf的地址在哪里)
                6、代碼測(cè)試、調(diào)試與優(yōu)化小結(jié)(程序開(kāi)發(fā)過(guò)后:內(nèi)存溢出了嗎?有緩沖區(qū)溢出?代碼覆蓋率如何測(cè)試呢?怎么調(diào)試匯編代碼?有哪些代碼優(yōu)化技巧和方法呢?)
                7、    8、進(jìn)程和進(jìn)程的基本操作(關(guān)于進(jìn)程本身的相關(guān)操作,主要是介紹了一些shell命令)
                需要補(bǔ)充的是,“高等數(shù)學(xué)”(higher mathematics)、“線性代數(shù)”(linear algebra)、“數(shù)據(jù)結(jié)構(gòu)”(data structure)、“數(shù)學(xué)建模”(mathematical modeling)、“設(shè)計(jì)模式”(design pattern)、“算法”(algorithm)、“離散數(shù)學(xué)”(discrete mathematics)、“數(shù)學(xué)分析”( mathematical analysis)等應(yīng)該是程序設(shè)計(jì)必備的一些知識(shí),在掌握相關(guān)工具的同時(shí),這些相關(guān)的理論課程也需要很好的熟悉。
                歡迎大家一起交流和探討。

            PS: 因?yàn)闀r(shí)間關(guān)系,很多blog都寫(xiě)得比較倉(cāng)促,里頭有錯(cuò)別字甚至是語(yǔ)義表達(dá)不清晰的地方,敬請(qǐng)?jiān)彛視?huì)逐步花時(shí)間進(jìn)行檢查的。

            推薦資料

            [1] mathematical modeling
            http://jpkc.nwu.edu.cn/sxjm/yxal.htm
            [2] design pattern
            [3] algorithm
            http://oss.lzu.edu.cn/blog/blog.php?/do_showone/tid_338.html

            posted @ 2008-03-14 15:17 隨意門(mén) 閱讀(571) | 評(píng)論 (0)編輯 收藏
            Linux下非常命令學(xué)習(xí)

            引自 http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_62.html


            剛學(xué)linux的時(shí)候,有些東西不大熟悉,非常惱火
            為了脫離這話總困境,把自己遇到并解決的一些常用命令行操作集中寫(xiě)到這里

            1,如何刪除非空目錄?

            用rmdir嗎?不是,而是

            #rm [your directory] -rf

            意思是強(qiáng)制刪除該目錄,以及該目錄下所有文件,試試,肯定奏效,呵呵
            不過(guò)不要隨便用,毫無(wú)提示就會(huì)刪除掉的

            而rmdir只能刪除空目錄哦
            另外,如果不強(qiáng)制刪除,只用
            #rm [your directory] -r

            2,壓縮-解壓縮命令大全

            tar.gz這個(gè)比較常見(jiàn)
            解壓:tar zxvf FileName.tar.gz
            壓縮:tar zcvf FileName.tar.gz DirName

            還在為面對(duì)一大堆的壓縮文件無(wú)法解壓縮而煩惱嗎?
            這里有比較全面的信息哦
            http://www.chinaitlab.com/www/techspecial/tar/

            3,如何用命令行創(chuàng)建和刪除文件名開(kāi)頭為"-"的文件?

            讓我們來(lái)創(chuàng)建一個(gè)這樣的文件“-test”
            #touch -test
            touch:日期格式 "est" 無(wú)效
            #touch -- -test
            #rm -test
            rm:無(wú)效選項(xiàng) --t
            請(qǐng)嘗試執(zhí)行"rm --help"來(lái)獲取更多幫助
            #rm -- test
            呵呵,是不是發(fā)現(xiàn),只有加了"--"才可以正常操作阿

            4,如果,我在鍵入ls命令以后只想顯示文件的部分信息,我該怎么辦呢?
            也許你會(huì)查幫助ls --help
            可是那么多的組合確實(shí)是讓人煩惱
            不過(guò)先在不用煩惱拉
            因?yàn)槲覀冇術(shù)awk

            看看這個(gè):ls -l | gawk '{printf $9}'
            看看輸出什么出來(lái)拉
            是不是只有文件名拉
            要是我還要?jiǎng)e的呢,那就在printf后面再加一個(gè)$x(x為1到9之間的字符哦)

            呵呵,其實(shí)gawk是一個(gè)腳本語(yǔ)言哦,功能非常強(qiáng)大,有興趣看看相關(guān)的參考書(shū)去拉

            5,有個(gè)好東西,可以對(duì)linux服務(wù)進(jìn)行相關(guān)的操作

            chkconf

            6,用rpm命令安裝和卸載軟件

            RPM共有10種基本的模式:它們是安裝、查詢、驗(yàn)證、刪除等。

            安裝模式:     rpm –i [安裝選項(xiàng)] <軟件包>
            查詢模式:     rpm –q [查詢選項(xiàng)]
            驗(yàn)證模式:     rpm –V 或 –verify [驗(yàn)證選項(xiàng)]
            刪除模式:     rpm –e <軟件包>

            7,tee命令

            這個(gè)命令的強(qiáng)大指處在于它會(huì)從標(biāo)準(zhǔn)輸入設(shè)備讀取數(shù)據(jù),將其內(nèi)容輸出到標(biāo)準(zhǔn)輸出設(shè)備,同時(shí)保存成文件。
            例如,我們想把一個(gè)文件inputfile的內(nèi)容即輸出到終端上也保存成outputfile1,outputfile2,那么我們就可以這么來(lái)弄:

            Quote:

            cat inputfile | tee outputfile1 outputfile2



            參考資料:
            http://jkwx007.blogchina.com/2514993.html
            http://jordi.blogbus.com/logs/2004/10/452282.html
            http://bbs.3671041.com/dispbbs.asp?boardid=9&id=747&star=1&page=1
            http://www.knowsky.com/print.asp?id=18403

            posted @ 2008-03-14 14:10 隨意門(mén) 閱讀(189) | 評(píng)論 (0)編輯 收藏
            幾個(gè)shell程序設(shè)計(jì)小知識(shí)(shell常識(shí)部分)

            來(lái)源:http://www.chinaunix.net/jh/24/628479.html

            引用:一、用戶登陸進(jìn)入系統(tǒng)后的系統(tǒng)環(huán)境變量:
            $HOME 使用者自己的目錄
            $PATH 執(zhí)行命令時(shí)所搜尋的目錄
            $TZ 時(shí)區(qū)
            $MAILCHECK 每隔多少秒檢查是否有新的信件
            $PS1 在命令列時(shí)的提示號(hào)
            $PS2 當(dāng)命令尚未打完時(shí),Shell 要求再輸入時(shí)的提示號(hào)
            $MANPATH man 指令的搜尋路徑

            二、特殊變量:

            $0 這個(gè)程序的執(zhí)行名字
            $n 這個(gè)程序的第n個(gè)參數(shù)值,n=1..9
            $* 這個(gè)程序的所有參數(shù)
            $# 這個(gè)程序的參數(shù)個(gè)數(shù)
            $$ 這個(gè)程序的PID
            $! 執(zhí)行上一個(gè)指令的PID
            $? 執(zhí)行上一個(gè)指令的返回值

            三、shell中的變?cè)?
            * 任意字符串
            ? 一個(gè)任意字符
            [abc] a, b, c三者中之一
            [a-n] 從a到n的任一字符

            四、幾個(gè)特殊字符表示

            \b 退回
            \c 打印一行時(shí)沒(méi)有換行符 這個(gè)我們經(jīng)常會(huì)用到
            \f 換頁(yè)
            \r 回車
            \t 制表
            \v 垂直制表
            \\ 反斜線本身

            五、判斷文件的屬性

            格式:-操作符 filename
            -e 文件存在返回1, 否則返回0
            -r 文件可讀返回1,否則返回0
            -w 文件可寫(xiě)返回1,否則返回0
            -x 文件可執(zhí)行返回1,否則返回0
            -o 文件屬于用戶本人返回1, 否則返回0
            -z 文件長(zhǎng)度為0返回1, 否則返回0.
            -f 文件為普通文件返回1, 否則返回0
            -d 文件為目錄文件時(shí)返回1, 否則返回0

            六、測(cè)試字符串
            字符串1 = 字符串2 當(dāng)兩個(gè)字串相等時(shí)為真
            字符串1 != 字符串2 當(dāng)兩個(gè)字串不等時(shí)為真
            -n 字符串      當(dāng)字符串的長(zhǎng)度大于0時(shí)為真
            -z 字符串      當(dāng)字符串的長(zhǎng)度為0時(shí)為真
            字符串       當(dāng)串字符串為非空時(shí)為真

            七、測(cè)試兩個(gè)整數(shù)關(guān)系
            數(shù)字1 -eq 數(shù)字2     兩數(shù)相等為真
            數(shù)字1 -ne 數(shù)字2     兩數(shù)不等為真
            數(shù)字1 -gt 數(shù)字2     數(shù)字1大于數(shù)字2為真
            數(shù)字1 -ge 數(shù)字2     數(shù)字1大于等于數(shù)字2為真
            數(shù)字1 -lt 數(shù)字2     數(shù)字1小于數(shù)字2為真
            數(shù)字1 -le 數(shù)字2     數(shù)字1小于等于數(shù)字2為真

            八、邏輯測(cè)試
            -a         與
            -o        或
            !        非



            今天介紹shell特殊字符的引用
            ===============================
            shell中的特殊字符有

            1、$ 美元符
            2、\ 反斜杠
            3、` 反引號(hào)
            4、" 雙引號(hào)
            5、< ,>,*,?,[,]

            下面我一一舉列說(shuō)明
            一、$符號(hào)
            1、echo $? 顯示的是上一條指令退出狀態(tài)
            2、echo "$?" 效果同上
            3、echo '$?' 顯示的是$?
            4、echo \$? 顯示的是$?
            5、echo "\$?" 顯示的是$?

              大家可能已經(jīng)看出 $符號(hào)在雙引號(hào)中具有特殊意義 雙引號(hào)對(duì)$符號(hào)不起作用
            而單引號(hào)可以將特殊字符的的特殊意義屏蔽掉,使其能顯示為字符本身,反斜
            杠也可以將特殊字符的特殊含義屏蔽掉,使特殊字符失去特殊含義。

            二、\ 反斜杠
              反斜杠的作用是將特殊符號(hào)字符的特殊含義屏蔽掉,使其還是原字符
            A=1234
            echo \$A 顯示為$A 如果不加\將顯示為1234
            echo \` 顯示為`
            echo \" 顯示為雙引號(hào)
            echo \\ 顯示為\

            三、` 反引號(hào)
              反引號(hào)的功能是命令替換,將反引號(hào)中的字符串做為命令來(lái)執(zhí)行,我們?cè)谟胹hell編程時(shí)經(jīng)常用的到 將系統(tǒng)命令的執(zhí)行結(jié)果賦給一個(gè)變量

            A=`date`
            echo $A 顯示的不是date而是當(dāng)時(shí)的時(shí)間串
            比如有一文件A的內(nèi)容如下 
            ABCDEFG
            1234456
            abcdefg

            B=`cat A|grep 234` # 檢索文件A中含有字符串234的行
            echo $B 將顯示為1234456
            echo "$B" 將顯示為什么?
            echo "\$B" 將顯示為什么?讀者自己試試

            四、" 雙引號(hào)
              在系統(tǒng)中有些特殊字符,為避免引用這些特殊字符 往往用雙引號(hào)或單引號(hào)將這些特殊字符引起來(lái),使其不具有特殊含義。
              但有一部分特殊字符在引號(hào)中還是具有特殊含義,用雙引號(hào)引起來(lái)是不起作用的。本文中所列的前四個(gè)特殊字符在雙引號(hào)中還是特殊字符。為了使其不具有特殊含義一是用單引號(hào)引進(jìn)來(lái)二是用\反斜線使其失去作用。

              比如我們想原樣輸出這些特殊字符

            echo """
            echo "$"
            echo "\"
            echo "`"
               以上不是你所期望的結(jié)果,因?yàn)殡p引號(hào)對(duì)它們不起作用,你只能這樣才能輸出這些特殊字符的原形
            echo '"'
            echo '$'
            echo '\'
            echo '`'

            echo "\""
            echo "\$"
            echo "\\"
            echo "\`"
            將分別顯示為 " $ \ `
            五、其它特殊字符
              大家注意到 除了前四個(gè)特殊字符外 我將其它的特殊字符都放在一塊,這是因?yàn)榍八膫€(gè)特殊字符在雙引號(hào)中還是具有特殊含義,所以單獨(dú)拿出來(lái)講,除此以外的特殊字符如果你要輸出這些特殊字符的原形,你就可以用雙引號(hào)或單引號(hào)引起來(lái)使其失去特殊含義。
            < ,>,*,?,[,]對(duì)shell有特殊含義 但你可以用雙引號(hào)引起來(lái)輸入這些原形

              講了這么多大家是不是已經(jīng)注意到所有的特殊字符在單引號(hào)中失去特殊含義,如果你要輸出特殊字符原形但又記不清那些特殊字符在雙引號(hào)中不能輸出原形,建議你干脆用單引號(hào)引起來(lái)。

            今天介紹條件測(cè)試語(yǔ)句

            一、if 條件語(yǔ)句 
            格式:
            if 條件表達(dá)式
            then #當(dāng)條件為真時(shí)執(zhí)行以下語(yǔ)句
            命令列表
            else #為假時(shí)執(zhí)行以下語(yǔ)句
            命令列表
            fi

            if 語(yǔ)句也可以嵌套使用

            if 條件表達(dá)式1
            then
            if 條件表達(dá)式2
            then
            命令列表
            else
            if 條件表達(dá)式3
            then
            命令列表
            else
            命令列表
            fi
            fi
            else
            命令列表
            fi

            你可以進(jìn)行多層嵌套 一個(gè)if語(yǔ)句一定要跟一個(gè)fi 表示該層條件結(jié)束  否則會(huì)造成語(yǔ)法錯(cuò)誤
            結(jié)合前面講的 舉例如下:
            這里先講一個(gè)條件語(yǔ)句中用到的命令test 表示測(cè)試test后面的條件是否為真

            if test -f "$1"
            then
            lpr $1
            else
            if test -d "$1"
            then
            cd $1
            lpr $1
            else
            echo "$1不是文件或目錄"
            fi
            fi

            以上的例子還可以改成如下所示

            if test -f "$1"
            then
            lpr $1
            elif test -d "$1" #elif 同else if
            then
            (cd $1;lpr $1)
            else
            echo "$1不是文件或目錄"
            fi

            以上的例子不知您是否看懂是什么意思嗎?
            假如我們現(xiàn)在將這個(gè)例子保存為prfile
            chmod +x prfile
            執(zhí)行剛才的程序
            ./prfile aaa

            這個(gè)例子是檢查你的輸入的參數(shù)是否是一個(gè)文件 如果是就打印 如果是一個(gè)目錄 先轉(zhuǎn)目錄再打印 如果即不是文件也不是目錄給出提示

            二、多重條件測(cè)試語(yǔ)句case
            格式:
            case 字串 in
            模式) 命令列表;;
            模式) 命令列表;;
            ....
            esac

            多重條件語(yǔ)句是以case 開(kāi)始以esac結(jié)束 中間可以有多個(gè)條件列表 功能是測(cè)試字串和和里面的模式有沒(méi)有匹配的,有就執(zhí)行里面的命令列表 模式也可以是*號(hào) 表示任意字串,每個(gè)模式里面的最后要心;;雙引號(hào)結(jié)束,否則會(huì)發(fā)生語(yǔ)法錯(cuò)誤。

            現(xiàn)舉例如下:

            case $1 in
            *.c)
            cc $1
            ;;
            *.txt)
            lpr $1
            ;;
            *)
            echo "未知的類型"
            esac

            假如將以上內(nèi)容保存在文件abc中

            chmod +x abc
            執(zhí)行 ./abc a.c   將會(huì)對(duì)文件a.c進(jìn)行編譯
            執(zhí)行 ./abc readme.txt 將會(huì)把文件通過(guò)打印機(jī)
            假如我將以上內(nèi)容改一下,你是否會(huì)知道它的執(zhí)行結(jié)果?

            case $1 in
            *)
            cc $1
            ;;
            *.txt)
            lpr $1
            ;;
            *.c)
            echo "未知的類型"
            esac

            今天介紹循環(huán)語(yǔ)句
            一. while 循環(huán)
            while 命令格式

            while 條件表
            do
            命令表
            done

            執(zhí)行過(guò)程

            shell首先執(zhí)行條件表,如果條件表的最后一條語(yǔ)句的退出狀態(tài)為零,則執(zhí)行盾環(huán)體內(nèi)的命令
            表,執(zhí)行完后,再檢查條件表,如果退出狀態(tài)為零將繼續(xù)執(zhí)行,如此循環(huán)往復(fù)直到條件表的
            最后一條語(yǔ)句的退出狀態(tài)非零. 退出狀態(tài)為零就是條件為真True.

            舉例說(shuō)明 假如shell文件的內(nèi)容如下:

            Sum=0
            i=0
            while true #true是系統(tǒng)的關(guān)鍵詞 表示真
            do
            i=`expr $i + 1`
            Sum=`expr $Sum + $i`
            if [ $i = "100" ]
            then
            break;
            fi
            done
            echo $i $Sum
            最后這個(gè)程序顯示的是 100 5050
            這個(gè)程序的運(yùn)算就是將1到100加起來(lái)

            下面將這個(gè)程序再改動(dòng)一下


            Sum=0
            i=0
            while [ $i != "100" ]
            do
            i=`expr $i + 1`
            Sum=`expr $Sum + $i`
            done
            echo $i $Sum

            改動(dòng)后的程序運(yùn)算結(jié)果和上面是一樣 但程序比上面的要簡(jiǎn)練

            在這個(gè)循環(huán)中還可以以u(píng)ntil做為測(cè)試條件 它正好與while測(cè)試的條件相反,也就是當(dāng)條件為假時(shí)將繼續(xù)執(zhí)行循環(huán)體內(nèi)的語(yǔ)句,否則就退出循環(huán)體,下面還用這個(gè)例子.


            Sum=0
            i=0
            until [ $i = "100" ]
            do
            i=`expr $i + 1`
            Sum=`expr $Sum + $i`
            done
            echo $i $Sum
            當(dāng)i不等于100時(shí)循環(huán) 就是當(dāng)條件為假時(shí)循環(huán),否則就退出,而第一個(gè)例子是當(dāng)i不等于100
            時(shí)循環(huán),也就是測(cè)試條件為真時(shí)循環(huán).

            二.for 循環(huán)

            命令格式:
            for 變量 in 名字列表
            do
            命令列表
            done

            這里的名字列表是一個(gè)由空格分隔的字符串列表,shell在執(zhí)行for循環(huán)時(shí)每次依次從名字表
            中取出一個(gè)字符串賦給循環(huán)變量作為變量的值.
            在寫(xiě)for語(yǔ)句時(shí),也可以省略in 名字列表部分,這表示用當(dāng)前的位置參數(shù)來(lái)代替這時(shí)的名
            字列表.
            下面舉個(gè)例子
            比如在你的電腦中有兩個(gè)目錄,一個(gè)是aa,一個(gè)是bb在這兩個(gè)目錄中有5個(gè)相同的文件,但其
            中一個(gè)目錄中的一個(gè)或多個(gè)文件剛剛修改過(guò),現(xiàn)在我忘記剛才改的是那幾個(gè)文件 了,那么我靠梢員冉弦幌掄飭礁瞿柯嫉奈募橢懶?程序如下:

            for File in a1 a2 a3 a4 a5
            do
            diff aa/$File bb/$File
            done

            下面再舉一個(gè)不帶名字列表的例子

            for File
            do
            echo $Filw
            done

            文件內(nèi)容保存在a.sh中 并可執(zhí)行
            我們?cè)趫?zhí)行這個(gè)shell程序時(shí)命令行如下:
            a.sh a1 a2 a3 a4 a5
            執(zhí)行結(jié)果如下:
            a1
            a2
            a3
            a4
            a5
            大家從這個(gè)例子中可以看到命令行的參數(shù)被逐一讀入一次
            三.循環(huán)控制語(yǔ)句
            break 命令不執(zhí)行當(dāng)前循環(huán)體內(nèi)break下面的語(yǔ)句從當(dāng)前循環(huán)退出.
            continue 命令是程序在本循體內(nèi)忽略下面的語(yǔ)句,從循環(huán)頭開(kāi)始執(zhí)行.

            一,命令組合:圓括號(hào)和花括號(hào)
            shell中有兩種方法將命令組合在一起:圓括號(hào)和花括號(hào).圓括號(hào)使shell創(chuàng)建一個(gè)子shell
            來(lái)讀取并執(zhí)行括起來(lái)的名命令.左括號(hào)和右括號(hào)不論出現(xiàn)在命令行中的什么位置,shell都會(huì)
            認(rèn)為它們具有特殊的組合意義的.只有用雙引號(hào)將它們括起來(lái)引用,才表示圓括號(hào)或花括號(hào)
            的原義.例如:

            echo a(b)
            將出現(xiàn)語(yǔ)法上的錯(cuò)誤,要想輸出a(b)字符串 只能括起來(lái)
            echo "a(b)"
            或echo a"("b")"
            這樣才能被shell正確解釋.
            利用組合命令有什么作用呢?
            一,用圓括號(hào)組合命令
            圓括號(hào)的組合命令可以創(chuàng)建子進(jìn)程運(yùn)行組合程序,建立子進(jìn)程的功能是很有用的,因?yàn)?
            子shell在組合命令中的種種操作都不會(huì)影響到當(dāng)前shell的各變量的值.
            例如:
            子進(jìn)程在執(zhí)行組合命令時(shí)改變了工作目錄,并在新的工作目錄下執(zhí)行一系例命令,執(zhí)行
            完后它可以不必返回原工作目錄,因?yàn)樽舆M(jìn)程工作目錄的改變不會(huì)影響到當(dāng)前工作目錄.

            創(chuàng)建子進(jìn)程后將當(dāng)前的環(huán)境也同樣傳給子shell,當(dāng)前shell中用export輸出到環(huán)境中的
            各變量在子shell中同樣有效.


            花括號(hào)也可以將命令組合在一起.左 右花括號(hào)只有作為一條命令的第一個(gè)字出現(xiàn)時(shí),
            shell才它們含有特殊含義.
            與圓括號(hào)不同的是花括號(hào)并不創(chuàng)建子shell,只是由當(dāng)前的shell來(lái)讀取并執(zhí)行括起來(lái)的
            命令.有時(shí)用戶希望使用一組命令的順序輸出作為另一組命令的輸入,此時(shí)用花括號(hào)是很方
            便的.
            不論是用圓括號(hào)不是花括號(hào),退出狀態(tài)都是等于最后一條括起來(lái)的命令的退出狀態(tài).


            二,可以在當(dāng)前shell中執(zhí)行的命令

            用戶在使用shell時(shí)一定要了解那些是可以在當(dāng)前shell中執(zhí)行的命令 那些不可以
            可以在當(dāng)前shell中執(zhí)行的命令有:

            break case cd continue
            echo eval exec exit
            export for if read
            readonly return set shift
            test times trap umask
            until wait while
            : {}

            posted @ 2008-03-14 14:03 隨意門(mén) 閱讀(208) | 評(píng)論 (0)編輯 收藏
            僅列出標(biāo)題
            共9頁(yè): 1 2 3 4 5 6 7 8 9 
            久久99精品国产麻豆婷婷| www.久久热| 亚洲а∨天堂久久精品| 欧美国产精品久久高清| 亚洲伊人久久综合中文成人网| 天天影视色香欲综合久久| 色播久久人人爽人人爽人人片aV| 日韩欧美亚洲综合久久影院Ds| 亚洲精品无码久久久| 久久亚洲国产精品成人AV秋霞| 77777亚洲午夜久久多人| 中文无码久久精品| 99久久夜色精品国产网站| 一级a性色生活片久久无少妇一级婬片免费放| 久久婷婷五月综合成人D啪| 中文字幕精品无码久久久久久3D日动漫| 精品多毛少妇人妻AV免费久久| 99国产欧美精品久久久蜜芽| 狠狠人妻久久久久久综合| 亚洲国产精品一区二区久久hs| 久久免费线看线看| 一本一道久久综合狠狠老| 国产福利电影一区二区三区久久老子无码午夜伦不 | 亚洲va久久久久| 69久久夜色精品国产69| 亚洲精品无码专区久久同性男| 免费精品99久久国产综合精品 | 久久99精品国产麻豆不卡| 久久夜色精品国产网站| 欧美粉嫩小泬久久久久久久| 久久99精品国产麻豆宅宅| 国内精品久久久久久久久电影网| 久久本道综合久久伊人| 国内精品久久久久久久97牛牛| 久久久高清免费视频| 国内精品欧美久久精品| 国产欧美久久一区二区| 久久国产乱子伦免费精品| 97久久国产综合精品女不卡| 久久人人爽人人爽人人爽| 思思久久99热只有频精品66|