2018年11月17日 星期六

[轉] Android Binder IPC分析

https://blog.csdn.net/windskier/article/details/6317867

1 . binder 通信概述

    binder 通信是一種 client-server 的通信結構,
    1. 從表面上來看,是 client 通過獲得一個 server 的代理接口,對 server 進行直接調用;
    2. 實際上,代理接口中定義的方法與 server 中定義的方法是一一對應的;
    3.client 調用某個代理接口中的方法時,代理接口的方法會將 client 傳遞的參數打包成為 Parcel 對象;
    4. 代理接口將該 Parcel 發送給內核中的 binder driver.
    5.server 會讀取 binder driver 中的請求數據,如果是發送給自己的,解包 Parcel 對象,處理並將結果返回;
    6. 整個的調用過程是一個同步過程,在 server 處理的時候, client 會 block 住。




 



2 . service manager
Service Manager 是一個 linux 級的進程 , 顧名思義,就是 service 的管理器。這裡的 service 是什麼概念呢?這裡的 service 的概念和 init 過程中 init.rc 中的 service 是不同, init.rc 中的 service 是都是 linux 進程,但是這裡的 service 它並不一定是一個進程,也就是說可能一個或多個 service 屬於同一個 linux 進程。在這篇文章中不加特殊說明均指 android native 端的 service 。

任何 service 在被使用之前,均要向 SM(Service Manager) 註冊,同時客戶端需要訪問某個 service 時,應該首先向 SM 查詢是否存在該服務。如果 SM 存在這個 service ,那麼會將該 service 的 handle 返回給 client , handle 是每個 service 的唯一標識符。
   
    SM 的入口函數在 service_manager.c 中,下面是 SM 的代碼部分
int main(int argc, char **argv)
{
    struct binder_state *bs;
    void *svcmgr = BINDER_SERVICE_MANAGER;

    bs = binder_open(128*1024);

    if (binder_become_context_manager(bs)) {
        LOGE("cannot become context manager (%s)/n", strerror(errno));
        return -1;
    }

    svcmgr_handle = svcmgr;
    binder_loop(bs, svcmgr_handler);
    return 0;
}

這個進程的主要工作如下:
    1. 初始化 binder ,打開 /dev/binder 設備;在內存中為 binder 映射 128K 字節空間;
    2. 指定 SM 對應的代理 binder 的 handle 為 0 ,當 client 嘗試與 SM 通信時,需要創建一個 handle 為 0 的代理 binder ,這裡的代理 binder 其實就是第一節中描述的那個代理接口;

3. 通知 binder driver(BD) 使 SM 成為 BD 的 context manager ;
4. 維護一個死循環,在這個死循環中,不停地去讀內核中 binder driver ,查看是否有可讀的內容;即是否有對 service 的操作要求 , 如果有,則調用 svcmgr_handler 回調來處理請求的操作。

5.SM 維護了一個 svclist 列表來存儲 service 的信息。






這裡需要聲明一下,當 service 在向 SM 註冊時,該 service 就是一個 client ,而 SM 則作為了 server 。而某個進程需要與 service 通信時,此時這個進程為 client , service 才作為 server 。因此 service 不一定為 server ,有時它也是作為 client 存在的。



由於下面幾節會介紹一些與 binder 通信相關的幾個概念,所以將 SM 的功能介紹放在了後面的部分來講。

應用和 service 之間的通信會涉及到 2 次 binder 通信。

1. 應用向 SM 查詢 service 是否存在,如果存在獲得該 service 的代理 binder ,此為一次 binder 通信;
2. 應用通過代理 binder 調用 service 的方法,此為第二次 binder 通信。


3 . ProcessState
ProcessState 是以單例模式設計的。每個進程在使用 binder 機制通信時,均需要維護一個 ProcessState 實例來描述當前進程在 binder 通信時的 binder 狀態。
    ProcessState 有如下 2 個主要功能:
    1. 創建一個 thread, 該線程負責與內核中的 binder 模塊進行通信,稱該線程為 Pool thread ;
    2. 為指定的 handle 創建一個 BpBinder 對象,並管理該進程中所有的 BpBinder 對象。



3.1 Pool thread
            在 Binder IPC 中,所有進程均會啟動一個 thread 來負責與 BD 來直接通信,也就是不停的讀寫 BD ,這個線程的實現主體是一個 IPCThreadState 對象,下面會介紹這個類型。

            下面是 Pool thread 的啟動方式:

ProcessState::self()->startThreadPool();

3.2 BpBinder 獲取
BpBinder 主要功能是負責 client 向 BD 發送調用請求的數據。它是 client 端 binder 通信的核心對象,通過調用 transact 函數向 BD 發送調用請求的數據,它的構造函數如下:

BpBinder(int32_t handle);
    通過 BpBinder 的構造函數發現, BpBinder 會將當前通信中 server 的 handle 記錄下來,當有數據發送時,會通知 BD 數據的發送目標。

ProcessState 通過如下方式來獲取 BpBinder 對象:

ProcessState::self()->getContextObject(handle);

在這個過程中, ProcessState 會維護一個 BpBinder 的 vector mHandleToObject ,每當 ProcessState 創建一個 BpBinder 的實例時,回去查詢 mHandleToObject ,如果對應的 handle 已經有 binder 指針,那麼不再創建,否則創建 binder 並插入到 mHandleToObject 中。
    ProcessState 創建的 BpBinder 實例,一般情況下會作為參數構建一個 client 端的代理接口,這個代理接口的形式為 BpINTERFACE , 例如在與 SM 通信時, client 會創建一個代理接口 BpServiceManager .
     
 

4 . IPCThreadState
IPCThreadState 也是以單例模式設計的。由於每個進程只維護了一個 ProcessState 實例,同時 ProcessState 只啟動一個 Pool thread ,也就是說每一個進程只會啟動一個 Pool thread ,因此每個進程則只需要一個 IPCThreadState 即可。
    Pool thread 的實際內容則為:
    IPCThreadState::self()->joinThreadPool();



ProcessState 中有 2 個 Parcel 成員, mIn 和 mOut , Pool thread 會不停的查詢 BD 中是否有數據可讀,如果有將其讀出並保存到 mIn ,同時不停的檢查 mOut 是否有數據需要向 BD 發送,如果有,則將其內容寫入到 BD 中,總而言之,從 BD 中讀出的數據保存到 mIn ,待寫入到 BD 中的數據保存在了 mOut 中。

ProcessState 中生成的 BpBinder 實例通過調用 IPCThreadState 的 transact 函數來向 mOut 中寫入數據,這樣的話這個 binder IPC 過程的 client 端的調用請求的發送過程就明了了 。



IPCThreadState 有兩個重要的函數, talkWithDriver 函數負責從 BD 讀寫數據, executeCommand 函數負責解析並執行 mIn 中的數據。



5. 主要基類
5.1 基類 IInterface

為 server 端提供接口,它的子類聲明了 service 能夠實現的所有的方法;


5.2 基類 IBinder
    BBinder 與 BpBinder 均為 IBinder 的子類,因此可以看出 IBinder 定義了 binder IPC 的通信協議, BBinder 與 BpBinder 在這個協議框架內進行的收和發操作,構建了基本的 binder IPC 機制。
5.3 基類 BpRefBase
    client 端在查詢 SM 獲得所需的的 BpBinder 後, BpRefBase 負責管理當前獲得的 BpBinder 實例。





6. 兩個接口類
6.1 BpINTERFACE
如果 client 想要使用 binder IPC 來通信,那麼首先會從 SM 出查詢並獲得 server 端 service 的 BpBinder ,在 client 端,這個對象被認為是 server 端的遠程代理。為了能夠使 client 能夠想本地調用一樣調用一個遠程 server , server 端需要向 client 提供一個接口, client 在在這個接口的基礎上創建一個 BpINTERFACE ,使用這個對象, client 的應用能夠想本地調用一樣直接調用 server 端的方法。而不用去關心具體的 binder IPC 實現。
下面看一下 BpINTERFACE 的原型:
    class BpINTERFACE : public BpInterface<IINTERFACE>

    順著繼承關係再往上看
    template<typename INTERFACE>
    class BpInterface : public INTERFACE, public BpRefBase

    BpINTERFACE 分別繼承自 INTERFACE ,和 BpRefBase ;
● BpINTERFACE 既實現了 service 中各方法的本地操作,將每個方法的參數以 Parcel 的形式發送給 BD 。
例如 BpServiceManager 的
    virtual status_t addService(const String16& name, const sp<IBinder>& service)
    {
        Parcel data, reply;
        data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
        data.writeString16(name);
        data.writeStrongBinder(service);
        status_t err = remote()->transact(ADD_SERVICE_TRANSACTION, data, &reply);
        return err == NO_ERROR ? reply.readExceptionCode() : err;
    }
● 同時又將 BpBinder 作為了自己的成員來管理,將 BpBinder 存儲在 mRemote 中, BpServiceManager 通過調用 BpRefBase 的 remote() 來獲得 BpBinder 指針。



6.2 BnINTERFACE
在定義 android native 端的 service 時,每個 service 均繼承自 BnINTERFACE(INTERFACE 為 service name) 。 BnINTERFACE 類型定義了一個 onTransact 函數,這個函數負責解包收到的 Parcel 並執行 client 端的請求的方法。

    順著 BnINTERFACE 的繼承關係再往上看,
        class BnINTERFACE: public BnInterface<IINTERFACE>

    IINTERFACE 為 client 端的代理接口 BpINTERFACE 和 server 端的 BnINTERFACE 的共同接口類,這個共同接口類的目的就是保證 service 方法在 C-S 兩端的一致性。

    再往上看
        class BnInterface : public INTERFACE, public BBinder

    同時我們發現了 BBinder 類型,這個類型又是干什麼用的呢?既然每個 service 均可視為一個 binder ,那麼真正的 server 端的 binder 的操作及狀態的維護就是通過繼承自 BBinder 來實現的。可見 BBinder 是 service 作為 binder 的本質所在。

    那麼 BBinder 與 BpBinder 的區別又是什麼呢?

    其實它們的區別很簡單, BpBinder 是 client 端創建的用於消息發送的代理,而 BBinder 是 server 端用於接收消息的通道。查看各自的代碼就會發現,雖然兩個類型均有 transact 的方法,但是兩者的作用不同, BpBinder 的 transact 方法是向 IPCThreadState 實例發送消息,通知其有消息要發送給 BD ;而 BBinder 則是當 IPCThreadState 實例收到 BD 消息時,通過 BBinder 的 transact 的方法將其傳遞給它的子類 BnSERVICE 的 onTransact 函數執行 server 端的操作。



7. Parcel
Parcel 是 binder IPC 中的最基本的通信單元,它存儲 C-S 間函數調用的參數 . 但是 Parcel 只能存儲基本的數據類型,如果是複雜的數據類型的話,在存儲時,需要將其拆分為基本的數據類型來存儲。

    簡單的 Parcel 讀寫不再介紹,下面著重介紹一下 2 個函數



7.1 writeStrongBinder
當 client 需要將一個 binder 向 server 發送時,可以調用此函數。例如
        virtual status_t addService(const String16& name, const sp<IBinder>& service)
        {
            Parcel data, reply;
            data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
            data.writeString16(name);
            data.writeStrongBinder(service);
            status_t err = remote()->transact(ADD_SERVICE_TRANSACTION, data, &reply);
            return err == NO_ERROR ? reply.readExceptionCode() : err;
        }


看一下 writeStrongBinder 的實體
status_t Parcel::writeStrongBinder(const sp<IBinder>& val)
{
    return flatten_binder(ProcessState::self(), val, this);
}

接著往裡看 flatten_binder
status_t flatten_binder(const sp<ProcessState>& proc,
    const sp<IBinder>& binder, Parcel* out)
{
    flat_binder_object obj;
     
    obj.flags = 0x7f | FLAT_BINDER_FLAG_ACCEPTS_FDS;
    if (binder != NULL) {
        IBinder *local = binder->localBinder();
        if (!local) {
            BpBinder *proxy = binder->remoteBinder();
            if (proxy == NULL) {
                LOGE("null proxy");
            }
            const int32_t handle = proxy ? proxy->handle() : 0;
            obj.type = BINDER_TYPE_HANDLE;
            obj.handle = handle;
            obj.cookie = NULL;
        } else {
            obj.type = BINDER_TYPE_BINDER;
            obj.binder = local->getWeakRefs();
            obj.cookie = local;
        }
    } else {
        obj.type = BINDER_TYPE_BINDER;
        obj.binder = NULL;
        obj.cookie = NULL;
    }
     
    return finish_flatten_binder(binder, obj, out);
}

    還是拿 addService 為例,它的參數為一個 BnINTERFACE 類型指針, BnINTERFACE 又繼承自 BBinder ,
    BBinder* BBinder::localBinder()
    {
        return this;
    }
    所以寫入到 Parcel 的 binder 類型為 BINDER_TYPE_BINDER ,同時你在閱讀 SM 的代碼時會發現如果 SM 收到的 service 的 binder 類型不為 BINDER_TYPE_HANDLE 時, SM 將不會將此 service 添加到 svclist ,但是很顯然每個 service 的添加都是成功的, addService 在開始傳遞的 binder 類型為 BINDER_TYPE_BINDER , SM 收到的 binder 類型為 BINDER_TYPE_HANDLE ,那麼這個過程當中究竟發生了什麼?
    為了搞明白這個問題,花費我很多的事件,最終發現了問題的所在,原來在 BD 中做了如下操作 (drivers/staging/android/Binder.c) :


static void binder_transaction(struct binder_proc *proc,
                   struct binder_thread *thread,
                   struct binder_transaction_data *tr, int reply)
{
..........................................

    if (fp->type == BINDER_TYPE_BINDER)
        fp->type = BINDER_TYPE_HANDLE;
    else
        fp->type = BINDER_TYPE_WEAK_HANDLE;
    fp->handle = ref->desc;
..........................................
}





閱讀完 addService 的代碼,你會發現 SM 只是保存了 service binder 的 handle 和 service 的 name ,那麼當 client 需要和某個 service 通信了,如何獲得 service 的 binder 呢?看下一個函數

7.2 readStrongBinder
當 server 端收到 client 的調用請求之後,如果需要返回一個 binder 時,可以向 BD 發送這個 binder ,當 IPCThreadState 實例收到這個返回的 Parcel 時, client 可以通過這個函數將這個被 server 返回的 binder 讀出。


sp<IBinder> Parcel::readStrongBinder() const
{
    sp<IBinder> val;
    unflatten_binder(ProcessState::self(), *this, &val);
    return val;
}


往裡查看 unflatten_binder


status_t unflatten_binder(const sp<ProcessState>& proc,
    const Parcel& in, sp<IBinder>* out)
{
    const flat_binder_object* flat = in.readObject(false);
     
    if (flat) {
        switch (flat->type) {
            case BINDER_TYPE_BINDER:
                *out = static_cast<IBinder*>(flat->cookie);
                return finish_unflatten_binder(NULL, *flat, in);
            case BINDER_TYPE_HANDLE:
                *out = proc->getStrongProxyForHandle(flat->handle);
                return finish_unflatten_binder(
                    static_cast<BpBinder*>(out->get()), *flat, in);
        }         
    }
    return BAD_TYPE;
}


發現如果 server 返回的 binder 類型為 BINDER_TYPE_BINDER 的話,也就是返回一個 binder 引用的話,直接獲取這個 binder ;如果 server 返回的 binder 類型為 BINDER_TYPE_HANDLE 時,也就是 server 返回的僅僅是 binder 的 handle ,那麼需要重新創建一個 BpBinder 返回給 client 。



    有上面的代碼可以看出, SM 保存的 service 的 binder 僅僅是一個 handle ,而 client 則是通過向 SM 獲得這個 handle ,從而重新構建代理 binder 與 server 通信。


    這裡順帶提一下一種特殊的情況, binder 通信的雙方即可作為 client ,也可以作為 server. 也就是說此時的 binder 通信是一個半雙工的通信。那麼在這種情況下,操作的過程會比單工的情況複雜,但是基本的原理是一樣的,有興趣可以分析一下 MediaPlayer 和 MediaPlayerService 的例子。



8. 經典橋段分析
main_ mediaserver.cpp
int main(int argc, char** argv)
{

// 創建進程 mediaserver 的 ProcessState 實例
    sp<ProcessState> proc(ProcessState::self());

// 獲得 SM 的 BpServiceManager
    sp<IServiceManager> sm = defaultServiceManager();
    LOGI("ServiceManager: %p", sm.get());

// 添加 mediaserver 中支持的 service 。
    AudioFlinger::instantiate();
    MediaPlayerService::instantiate();
    CameraService::instantiate();
    AudioPolicyService::instantiate();

// 啟動 ProcessState 的 pool thread
    ProcessState::self()->startThreadPool();

// 這一步有重複之嫌,加不加無關緊要。
    IPCThreadState::self()->joinThreadPool();
}



9. Java 層的 binder 機制
瞭解了 native 通信機制後,再去分析 JAVA 層的 binder 機制,就會很好理解了。它只是對 native 的 binder 做了一個封裝。這一部分基本上沒有太複雜的過程,這裡不再贅述了。
---------------------
作者:windskier
来源:CSDN
原文:https://blog.csdn.net/windskier/article/details/6317867
版权声明:本文为博主原创文章,转载请附上博文链接!

2018年11月15日 星期四

[Source Insight] export/import project file list

Source Insight 3.5

[export]
Project File List -> right click -> Poject Report... -> "Select None" -> OK -> xxxx.rpt (done)

File                             
------------------------------------------------------------------------------


Project Report (default check setting)
Files:
[Size]  [Modification Date] 
[OBJ Size]  [Normalize File Names] 
Symbols:
[Include Symbols]
[Line Numbers]



[import]
Project File List -> right click -> Add and Remove Project Files... -> Add from list... -> select file list file

2018年11月6日 星期二

[轉] weak symbol

我們用nm看動態庫時,會發現有些符號類型是"V",手冊里解釋如下:
"V" The symbol is a weak object.  When a weak defined symbol is linked with a normal  defined  symbol, the normal defined symbol is used with no error. When a weak undefined symbol is linked and the symbol is not defined, the value of the weak symbol becomes zero with no error.
說的是動態庫中的weak symbol,缺省會被normal symbol替代,如果沒有定義,則該symbol的值為0。
很抽象,是不是,我一直想找一個簡單的例子。
            
最近看過一篇文章:
http://www.cs.virginia.edu/~wh5a/blog/2006/07/20/the-weak-attribute-of-gcc/
終於對所謂的weak symbol有了一點了解。
http://gcc.gnu.org/onlinedocs/gcc-4.1.1/gcc/Function-Attributes.html
講了__attribute__的語法。
 
【weak.c】
extern void foo() __attribute__((weak));
int main() {
  if (foo) foo();
}
程序居然能夠編譯通過,甚至成功執行!讓我們來看看是為什麼?
首先聲明了一個符號foo(),屬性為weak,但並不定義它,這樣,鏈接器會將此未定義的weak symbol賦值為0,也就是說foo()並沒有真正被調用,試試看,去掉if條件,肯定core dump!
 
【strong.c】
extern void foo() ;
int main() {
  if (foo) foo();
}
這個是一般程序,編譯過不了:
strong.c: undefined reference to `foo'
再添上一個定義文件:

【foo.c】
void foo() {
  printf("in foo.\n");
}
OK!
 
用nm檢查一下:
linux:~/test/weak # nm weak.o
         w foo
00000000 T main
linux:~/test/weak # nm foo.o
00000000 T foo
         U printf
鏈接時,前面那個weak symbol會被后面這個代替,如果沒有鏈接foo.o,也沒問題,對應符號為0。
 
這就是weak symbol的意義。
 
 
 
#include <stdio.h>
#include <string.h>
/* malloc example */
#include <stdio.h>
#include <string.h>
#include <process.h>
//extern void f();
//void f () __attribute__ ((weak, alias ("__f")));
void f () __attribute__ ((weak, alias ("__f")));
void __f()
{
 printf("it789is test\n");

}

int main(void)
{
 if(f)
  f();
 else
  printf("it is test\n");
 return 0;
}

2018年11月5日 星期一

[轉] #pragma pack (push,1) and #pragma pack(pop)

#pragma pack是用來指定數據在內存中的對齊方式。
#pragma pack (n)             作用:C編譯器將按照n個字節對齊。
#pragma pack ()               作用:取消自定義字節對齊方式。

#pragma  pack (push,1)     作用:是指把原來對齊方式設置壓棧,並設新的對齊方式設置為一個字節對齊
#pragma pack(pop)            作用:恢復對齊狀態
因此可見,加入push和pop可以使對齊恢復到原來狀態,而不是編譯器默認,可以說後者更優,但是很多時候兩者差別不大
如:
#pragma pack(push) //保存對齊狀態
#pragma pack(4)//設定為4字節對齊
  相當於 #pragma  pack (push,4)  

#pragma  pack (1)           作用:調整結構體的邊界對齊,讓其以一個字節對齊;<使結構體按1字節方式對齊>
#pragma  pack ()
例如:
#pragma pack(1)
struct sample
{
char a;
double b;
};
#pragma pack()
註:若不用#pragma pack(1)和#pragma pack()括起來,則sample按編譯器默認方式對齊(成員中size最大的那個)。
即按8字節(double)對齊,則sizeof(sample)==16.成員char a佔了8個字節(其中7個是空字節);
若用#pragma pack(1),則sample按1字節方式對齊sizeof(sample)==9.(無空字節),比較節省空間啦,有些場和還可使結構體更易於控制。
應用實例
在網絡協議編程中,經常會處理不同協議的數據報文。一種方法是通過指針偏移的方法來得到各種信息,但這樣做不僅編程複雜,而且一旦協議有變化,程序修改起來也比較麻煩。在瞭解了編譯器對結構空間的分配原則之後,我們完全可以利用這一特性定義自己的協議結構,通過訪問結構的成員來獲取各種信息。這樣做,不僅簡化了編程,而且即使協議發生變化,我們也只需修改協議結構的定義即可,其它程序無需修改,省時省力。下面以TCP協議首部為例,說明如何定義協議結構。其協議結構定義如下: 

#pragma pack(1) // 按照1字節方式進行對齊struct TCPHEADER 
{
     short SrcPort; 
// 16位源端口號
     short DstPort; 
// 16位目的端口號
     int SerialNo; 
// 32位序列號
     int AckNo; 
// 32位確認號
     unsigned char HaderLen : 4; 
// 4位首部長度
     unsigned char Reserved1 : 4; 
// 保留6位中的4位
     unsigned char Reserved2 : 2; 
// 保留6位中的2位
     unsigned char URG : 1;
     unsigned char ACK : 1;
     unsigned char PSH : 1;
     unsigned char RST : 1;
     unsigned char SYN : 1;
     unsigned char FIN : 1;
     short WindowSize; 
// 16位窗口大小
     short TcpChkSum; 
// 16位TCP檢驗和
     short UrgentPointer; 
// 16位緊急指針
}; 
#pragma pack()

2018年8月5日 星期日

[轉] Makefile中的wildcard用法

在Makefile規則中,通配符會被自動展開。但在變量的定義和函數引用時,通配符將失效這種情況下如果需要通配符有效,就需要使用函數“wildcard,它的用法是:$(wildcard PATTERN...) 。在Makefile中,它被展開為已經存在的使用空格分開的匹配此模式的所有文件列表。如果不存在任何符合此模式的文件,函數會忽略模式字符並返回空。需要注意的是:這種情況下規則中通配符的展開和上一小節匹配通配符的區別。
一般我們可以使用$(wildcard *.c)”來獲取工作目錄下的所有的.c文件列表。複雜一些用法;可以使用“$(patsubst %.c,%.o,$(wildcard *.c))”,首先使用“wildcard”函數獲取工作目錄下的.c文件列表;之後將列表中所有文件名的後綴.c替換為.o。這樣我們就可以得到在當前目錄可生成的.o文件列表。因此在一個目錄下可以使用如下內容的Makefile來將工作目錄下的所有的.c文件進行編譯並最後連接成為一個可執行文件:

#sample Makefile
objects := $(patsubst %.c,%.o,$(wildcard *.c))

foo : $(objects)
cc -o foo $(objects)

這裡我們使用了make的隱含規則來編譯.c的源文件。對變量的賦值也用到了一個特殊的符號(:=)。

1、wildcard : 擴展通配符
2、notdir : 去除路徑
3、patsubst :替換通配符
例子:
建立一個測試目錄,在測試目錄下建立一個名為sub的子目錄
$ mkdir test
$ cd test
$ mkdir sub
在test下,建立a.c和b.c2個文件,在sub目錄下,建立sa.c和sb.c2 個文件
建立一個簡單的Makefile
src=$(wildcard *.c ./sub/*.c)
dir=$(notdir $(src))
obj=$(patsubst %.c,%.o,$(dir) )
all:
 @echo $(src)
 @echo $(dir)
 @echo $(obj)
 @echo "end"
 
執行結果分析:
第一行輸出:
a.c b.c ./sub/sa.c ./sub/sb.c
wildcard把 指定目錄 ./ 和 ./sub/ 下的所有後綴是c的文件全部展開。
第二行輸出:
a.c b.c sa.c sb.c
notdir把展開的文件去除掉路徑信息
第三行輸出:
a.o b.o sa.o sb.o
在$(patsubst %.c,%.o,$(dir) )中,patsubst把$(dir)中的變量符合後綴是.c的全部替換成.o,任何輸出。
或者可以使用
obj=$(dir:%.c=%.o)
效果也是一樣的。
這裡用到makefile裡的替換引用規則,即用您指定的變量替換另一個變量。
它的標準格式是
$(var:a=b) 或 ${var:a=b}
它的含義是把變量var中的每一個值結尾用b替換掉a


今天在研究makefile時在網上看到一篇文章,介紹了使用函數wildcard得到指定目錄下所有的C語言源程序文件名的方法,這下好了,不用手工一個一個指定需要編譯的.c文件了,方法如下:
SRC = $(wildcard *.c)
等於指定編譯當前目錄下所有.c文件,如果還有子目錄,比如子目錄為inc,則再增加一個wildcard函數,像這樣:
SRC = $(wildcard *.c) $(wildcard inc/*.c)
也可以指定彙編源程序: 
ASRC = $(wildcard *.S)