Linux核心入門(七)—— 必要的編譯知識

來源:互聯網
上載者:User

所有的核心代碼,基本都包含了include/linux/compile.h這個檔案,所以它是基礎,涵蓋了分析核心所需要的一些列編譯知識,本博就分析分析這個檔案裡的代碼:

#ifndef __LINUX_COMPILER_H
#define __LINUX_COMPILER_H

#ifndef __ASSEMBLY__

首先印入眼帘的是對__ASSEMBLY__這個宏的判斷,這個變數實際是在編譯彙編代碼的時候,由編譯器使用-D這樣的參數加進去的,gcc會把這個宏定義為1。用在這裡,是因為彙編代碼裡,不會用到類似於__user這樣的屬性(關於 __user這樣的屬性是怎麼回子事,本博後面會提到),因為這樣的屬性是在定義函數參數的時候加的,這樣避免不必要的宏在編譯彙編代碼時候的引用。

#ifdef __CHECKER__

接下來是一個對__CHECKER__這個宏的判斷,這裡需要講的東西比較多,是本博的重點。
       
當編譯核心代碼的時候,使用make C=1或C=2的時候,會調用一個叫Sparse的工具,這個工具對核心代碼進行檢查,怎麼檢查呢,就是靠對那些聲明過Sparse這個工具所能識別的特性的核心功能或是變數進行檢查。在調用Sparse這個工具的同時,在Sparse代碼裡,會加上#define __CHECKER__ 1的字樣。換句話說,就是,如果使用Sparse對代碼進行檢查,那麼核心代碼就會定義__CHECKER__宏,否則就不定義。具體解釋請訪問:http://linux.die.net/man/1/sparse

 

例如:

# define __user  __attribute__((noderef, address_space(1)))
     
這個宏是重點,用來檢查是否屬於使用者空間!這裡就能看出來,類似於__attribute__((noderef, address_space(1)))這樣的屬性就是Sparse這個工具所能識別的了。
      
其他的那些個屬性是用來檢查什麼的呢,我一個個地做介紹。

__user 這個特性,即__attribute__((noderef, address_space(1))),是用來修飾一個變數的,這個變數必須是非解除參考(__attribute__((noderef))——no dereference)的,即這個變數地址必須是有效,而且變數所在的地址空間必須是1(__attribute__((address_space(1)))),即使用者程式空間的。這裡Sparse工具把程式空間分成了3個部分,0表示normal space,即普通地址空間,對核心代碼來說,當然就是核心空間地址了。1表示使用者地址空間,這個不用多講,還有一個2,表示是裝置地址映射空間,例如硬體裝置的寄存器在核心裡所映射的地址空間。

所以在核心功能裡,有一個copy_to_user的函數(我們在系統調用博文中會詳細介紹),函數的參數定義就使用了這種方式。當然,這種特性檢查,只有當機器上安裝了Sparse這個工具,而且進行了編譯的時候調用,才能起作用的。

# define __kernel /* default address space */

根據定義,就是檢查是否處於核心空間。其為預設的地址空間,即0,我想定義成__attribute__((noderef, address_space(0)))也是沒有問題的。

# define __safe  __attribute__((safe))

這個定義在sparse裡也有,核心代碼是在2.6.6-rc1版本變到2.6.6-rc2的時候被Linus加入的,原因是這樣的:

有人發現在代碼編譯的時候,編譯器對變數的檢查有些苛刻,導致代碼在編譯的時候老是出問題(我這裡沒有去檢查是編譯不通過還是有警告資訊,因為現在的編譯器已經不是當年的編譯器了,代碼也不是當年的代碼)。比如說這樣一個例子,
 int test( struct a * a, struct b * b, struct c * c ) {
  return a->a + b->b + c->c;
 }

這個編譯的時候會有問題,因為沒有檢查參數是否為空白,就直接進行調用。但是呢,在核心裡,有好多函數,當它們被調用的時候,這些個參數必定不為空白,所以根本用不著去對這些個參數進行非空的檢查,所以就增加了一個__safe的屬性,如果這樣聲明變數,
 int test( struct a * __safe a, struct b * __safe b, struct c * __safe c ) {
  return a->a + b->b + c->c;
 }

編譯就沒有問題了。

不過到目前為止,在現在的代碼裡沒有發現有使用__safe這個定義的地方,不知道是不是編譯器現在已經支援這種特殊的情況了,所以就不用再加這樣的代碼了。

# define __force __attribute__((force))

表示所定義的變數類型是可以做強制類型轉換的,在進行Sparse分析的時候,是不用報警示資訊的。

# define __nocast __attribute__((nocast))

這裡表示這個變數的參數類型與實際參數類型一定得對得上才行,要不就在Sparse的時候生產警示資訊。

# define __iomem __attribute__((noderef, address_space(2)))

這個定義與__user一樣,不過這裡的變數地址需要在裝置地址映射空間。

# define __acquires(x) __attribute__((context(x,0,1)))
# define __releases(x) __attribute__((context(x,1,0)))

這是一對相互關聯的函數定義,第一句表示參數x在執行之前,引用計數必須為0,執行後,引用計數必須為1,第二句則正好相反,這個定義是用在修飾函數定義的變數的。

# define __acquire(x) __context__(x,1)
# define __release(x) __context__(x,-1)

這是一對相互關聯的函數定義,第一句表示要增加變數x的計數,增加量為1,第二句則正好相反,這個是用來函數執行的過程中。

以上四句如果在代碼中出現了不平衡的狀況,那麼在Sparse的檢測中就會警示。當然,Sparse的檢測只是一個手段,而且是靜態檢查代碼的手段,所以它的協助有限,有可能把正確的認為是錯誤的而發出警示。要是對以上四句的意思還是不太瞭解的話,請在原始碼裡搜一下相關符號的用法就能知道了。這第一組與第二組,在本質上,是沒什麼區別的,只是使用的位置上,有所區別罷了。

# define __cond_lock(x,c) ((c) ? ({ __acquire(x); 1; }) : 0)

這句話的意思就是條件鎖。當c這個值不為0時,則讓計數值加1,並傳回值為1。不過這裡我有一個疑問,有一個__cond_lock定義,但沒有定義相應的__cond_unlock,那麼在變數的釋放上,就沒辦法做到一致。而且spin_trylock()這個函數的定義,它就用了 __cond_lock,而且裡面又用了_spin_trylock函數,在_spin_trylock函數裡,再經過幾次調用,就會使用到 __acquire函數,這樣的話,相當於一個操作,就進行了兩次計算,會導致Sparse的檢測出現警示資訊,經過寫代碼進行實驗,驗證了我的判斷,確實會出現警示資訊,如果我寫兩遍unlock指令,就沒有警示資訊了,但這是與程式的運行是不一致的。

extern void __chk_user_ptr(const volatile void __user *);
extern void __chk_io_ptr(const volatile void __iomem *);

這兩句比較有意思。只是定義了函數,但是代碼裡沒有函數的實現。這樣做的目的,就是在進行Sparse的時候,讓Sparse給代碼做必要的參數類型檢查,在實際的編譯過程中,並不需要這兩個函數的實現。

#define notrace __attribute__((no_instrument_function))

這一句,是定義了一個屬性,這個屬性可以用來修飾一個函數,指定這個函數不被跟蹤。在gcc編譯器裡面,實現了一個非常強大的功能,如果在編譯的時候把一個相應的選擇項開啟,那麼就可以在執行完程式的時候,用一些工具來顯示整個函數被調用的過程,這樣就不需要讓程式員手動在所有的函數裡一點點添加能顯示函數被調用過程的語句,這樣耗時耗力,還容易出錯。那麼對應在應用程式方面,可以使用Graphviz這個工具來進行顯示,至於使用說明與軟體實現的原理可以自己在網上查一查,很容易查到。對應於核心,因為核心一直是在運行階段,所以就不能使用這套東西了,核心是在自己的內部實現了一個ftrace的機制,編譯核心的時候,如果開啟這個選項,那麼通過掛載一個debugfs的檔案系統來進行相應內容的顯示,具體的操作步驟,可以參看核心源碼所帶的文檔。那上面說了這麼多,與notrace這個屬性有什麼關係呢?因為在進行函數調用流程的顯示過程中,是使用了兩個特殊的函數的,當函數被調用與函數被執行完返回之前,都會分別調用這兩個特別的函數。如果不把這兩個函數的函數指定為不被跟蹤的屬性,那麼整個跟蹤的過程 就會陷入一個無限迴圈當中。

#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

這兩句是一對對應關係。__builtin_expect(expr, c)這個函數是新版gcc支援的,它是用來作代碼最佳化的,用來告訴編譯器,expr的期,非常有可能是c,這樣在gcc產生對應的彙編代碼的時候,會把相應的可能執行的代碼都放在一起,這樣能少執行代碼的跳轉。為什麼這樣能提高CPU的執行效率呢?因為CPU在執行的時候,都是有預先取指令的機制的,把將要執行的指令取出一部分出來準備執行。CPU不知道程式的邏輯,所以都是從可程式程式裡挨著取的,如果這個時候,能不做跳轉,則CPU預先取出的指令都可以接著使用,反之,則預先取出來的指令都是沒有用的。還有個問題是需要注意的,在__builtin_expect的定義中,以前的版本是沒有!!這個符號的,這個符號的作用其實就是負負得正,為什麼要這樣做呢?就是為了保證非零的x的值,後來都為1,如果為零的0值,後來都為0,僅此而已。

#ifndef barrier
# define barrier() __memory_barrier()
#endif

這裡表示如果沒有定義barrier函數,則定義barrier()函數為__memory_barrier()。但在核心代碼裡,是會包含 compiler-gcc.h這個檔案的,所以在這個檔案裡,定義barrier()為__asm__ __volatile__("": : :"memory")。barrier翻譯成中文就是屏障的意思,為什麼要一個屏障呢?這是因為CPU在執行的過程中,為了最佳化指令,可能會對部分指令以它自己認為最優的方式進行執行,這個執行的順序並不一定是按照程式在源碼內寫的順序。編譯器也有可能在產生二進位指令的時候,也進行一些最佳化。這樣就有可能在多CPU,多線程或是互斥鎖的執行中遇到問題。那麼這個記憶體屏障可以看作是一條線,記憶體屏障用在這裡,就是為了保證屏障以上的操作,不會影響到屏障以下的操作。然後再看看這個屏障怎麼實現的。__asm__表示後面的東西都是彙編指令,當然,這是一種在C語言中嵌入彙編的方法,文法有其特殊性,我在這裡只講跟這條指令有關的。__volatile__表示不對此處的彙編指令做最佳化,這樣就會保證這裡代碼的正確性。""表示這裡是個空指令,那麼既然是空指令,則所對應的指令所需要的輸入與輸出都沒有。在gcc中規定,如果以這種方式嵌入彙編,如果輸出沒有,則需要兩個冒號來代替輸出運算元的位置,所以需要加兩個::,這時的指令就為"" : :。然後再加上為分隔輸入而加入的冒號,再加上空的輸入,即為"" : : :。後面的memory是gcc中的一個特殊的文法,加上它,gcc編譯器則會產生一個動作,這個動作使gcc不保留在寄存器內記憶體的值,並且對相應的記憶體不會做儲存與載入的最佳化處理,這個動作不產生額外的代碼,這個行為是由gcc編譯器來保證完成的。

#ifndef RELOC_HIDE
# define RELOC_HIDE(ptr, off)     /
  ({ unsigned long __ptr;     /
     __ptr = (unsigned long) (ptr);    /
    (typeof(ptr)) (__ptr + (off)); })
#endif

接下來好多定義都沒有實現,可以看一看注釋就知道了,所以這裡就不多說了。唉,不過再插一句,__deprecated屬性的實現是為deprecated。

#define noinline_for_stack noinline

#ifndef __always_inline
#define __always_inline inline
#endif

這裡noinline與inline屬性是兩個對立的屬性,從詞面的意思就非常好理解了。

#ifndef __cold
#define __cold
#endif

從注釋中就可以看出來,如果一個函數的屬性為__cold,那麼編譯器就會認為這個函數幾乎是不可能被調用的,在進行代碼最佳化的時候,就會考慮到這一點。不過我沒有看到在gcc裡支援這個屬性的說明。

#ifndef __section
# define __section(S) __attribute__ ((__section__(#S)))
#endif

這個比較容易理解了,用來修飾一個函數是放在哪個地區裡的,不使用編譯器預設的方式。這個地區的名字由定義者自己取,格式就是__section__加上使用者輸入的參數。

#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))

這個函數的定義很有意思,它就是訪問這個x參數所對應的東西一次,它是這樣做的:先取得這個x的地址,然後把這個地址進行變換,轉換成一個指向這個地址類型的指標,然後再取得這個指標所指向的內容。這樣就達到了訪問一次的目的。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.