(已作廢)為我們的作業系統寫一些常用的核心庫函式

田宇發表於2015-03-16

本節將會介紹一些常用的系統核心庫函式,這些庫函式都是作者參考linux系統核心的庫函式實現的。根據多方面考慮,暫時就沒把這些庫函式寫的那麼複雜。有可能功能不完善、效能不夠或者有考慮不周的地方,都將會在以後的核心開發過程中一步一步的完善。現在把他們拿來讓讀者們學習學習這些核心庫函式的基本原理,還是非常適合的。不論是從難易程度、程式碼複雜度、還是實現原理,都相對比較簡單。那還等什麼!讓我們開始吧~~

container_of

    #define container_of(ptr,type,member)                            \
    ({                                            \
        typeof(((type *)0)->member) * p = (ptr);                    \
        (type *)((unsigned long)p - (unsigned long)&(((type *)0)->member));        \
    })  

相信看過linux核心的讀者們對container_of的作用並不陌生,它可以根據某個結構體內的成員變數的地址,準確的找到該結構體的首地址。這就相當於一個反向尋找上層結構體的過程,使用成員變數的地址偏移,反推出上層結構的地址。

可能這個巨集看起來非常的複雜,但是大家不用擔心,我們從他的引數開始,一層一層的剝去他的外衣,原理自然就顯現出來。

先從他的引數講起:

  • ptr:這個變數是一個指標,它指向一例項化後的結構體成員的地址。我們正是要根據這個地址找到它所在的結構體的首地址。

  • type:這個變數代表的是一個結構體,這個結構體就是我們要找的結構體。

  • member:這個變數代表結構體中的成員。這個結構體中的成員,與ptr所指的成員是同一個。只不過member是結構體中的成員,而ptr則是經過例項化後的結構體中的成員。

根據這三個引數,我們就能直接分析出,container_of的計算原理。其實就是用type型別的結構體到其member成員的偏移值,再用ptr指向的記憶體地址減去這個偏移值,得到的地址就是我們要的結構體的首地址啦。

瞭解了其實現原理,我們再來結合程式碼,看看他是如何實現的。

typeof(((type *)0)->member) * p = (ptr);   

這句話的作用是建立一個指標變數p,用來承載ptr變數的地址。p的型別是根據結構體成員member的型別決定的,通過typeof來獲得member的型別。其中,(type *)0是將地址0強制轉換為結構體type型別的指標,然後,使用((type *)0)->member來引用結構體成員member。最後,就是用typeof獲得member變數的型別,並定義成該型別的指標變數p,將ptr的值賦值給p。

(type *)((unsigned long)p - (unsigned long)&(((type *)0)->member));  

這句話的關鍵在 (unsigned long)&(((type *)0)->member)部分,它同上面的語句很相似,也是將地址0強制轉換為type型別的結構體指標,然後引用其成員member,並使用“&”符取出member成員的地址。這個地址是相對於結構體首地址到member成員地址的偏移量。使用0地址的妙處也在於此——將結構體的首地址指定在地址0處,那麼從首地址0處到member的地址偏移,實際上就是member的地址,省去了減結構體基地址的步驟。在獲得了這個地址偏移量以後,使用變數p裡面儲存的地址值減去這個偏移量,得到的地址就是結構體的首地址,最後,將地址強制轉化為我們要求的結構體型別type的指標,這樣一切就都搞定啦~。

memcpy

memcpy可以說是我們日常開發中出場率很高的函式之一了,您想過他是怎麼實現的嗎?一般我們都不會關注他的內部實現,只要會用就行了。但是從現在開始,整個作業系統都是由我們自己動手開發的,已經不存在系統庫函式了。所以,這些函式需要我們動手去實現。下面的函式是基於linux2.4.0核心開發的64位版本的memcpy函式。

inline void * memcpy(void *From,void * To,long Num)
{
    int d0,d1,d2;
    __asm__ __volatile__    (    "cld    \n\t"
                    "rep    \n\t"
                    "movsq    \n\t"
                    "testb    $4,%b4    \n\t"
                    "je    1f    \n\t"
                    "movsl    \n\t"
                    "1:\ttestb    $2,%b4    \n\t"
                    "je    2f    \n\t"
                    "movsw    \n\t"
                    "2:\ttestb    $1,%b4    \n\t"
                    "je    3f    \n\t"
                    "movsb    \n\t"
                    "3:    \n\t"
                    :"=&c"(d0),"=&D"(d1),"=&S"(d2)
                    :"0"(Num/8),"q"(Num),"1"(To),"2"(From)
                    :"memory"
                );
    return To;
}  

考驗我們彙編功底的時候到了,你準備好了嗎!

這個函式的功能從字面意思就能瞭解到,是拷貝記憶體。從From地址到To地址,長度為Num個位元組。函式內申請了d0,d1,d2三個引數,分別用來承載輸出引數。但是這些引數都是用來在組合語言內部使用,而臨時申請的棧空間,只在memcpy函式內部使用。

首先,讓我們先了解memcpy的實現原理,其實他的實現原理很簡單,就是使用movs彙編指令將資料轉移,並且,藉助rep指令進行傳輸計數。

下面我們來看看movs在Intel技術文件中的解釋:

Moves the byte, word, or doubleword specified with the second operand (source operand) to the location specified with the first operand (destination operand). Both the source and destination operands are located in memory. The address of the source operand is read from the DS:ESI or the DS:SI registers (depending on the address-size attribute of the instruction, 32 or 16, respectively). The address of the destination operand is read from the ES:EDI or the ES:DI registers (again depending on the address-size attribute of the instruction).

大概意思:移動拷貝一個位元組、字、雙字,從指定的源地址到目的地址。其中,源和目的地址都是記憶體。源運算元的地址從DS:ESI或DS:SI暫存器(根據資料頻寬32或16)讀入。目標運算元的地址從ES:EDI或ES:DI暫存器(與源運算元暫存器要求一致)。

After the move operation, the (E)SI and (E)DI registers are incremented or decremented automatically according to the setting of the DF flag in the EFLAGS register. (If the DF flag is 0, the (E)SI and (E)DI register are incre-mented; if the DF flag is 1, the (E)SI and (E)DI registers are decremented.) The registers are incremented or decremented by 1 for byte operations, by 2 for word operations, or by 4 for doubleword operations.

大概意思:當移動拷貝以後,(E)SI和(E)DI暫存器會自動增長或者減少,根據EFLAGS暫存器的DF標誌位。如果DF標誌位為0,(E)SI和(E)DI暫存器,將會自動增加,如果DF標誌位為1,那麼(E)SI和(E)DI暫存器,將會自動減少。暫存器的增減或者減少會根據操作的位元組數執行,1位元組,1字,雙字。

指令rep在Intel技術文件中的部分解釋:

Repeats a string instruction the number of times specified in the count register or until the indicated condition of the ZF flag is no longer met.The REP prefixes apply only to one string instruction at a time. To repeat a block of instructions, use the LOOP instruction or another looping construct. All of these repeat prefixes cause the associated instruction to be repeated until the count in register is decremented to 0.

大概意思:重複執行一串指令若干次,根據指定計數暫存器決定重複次數,或者直到指定條件置位ZF標誌位與否。rep指令一次只作用於一條指令。對於想重複一段指令,可以使用loop指令或者其他迴圈指令。所有這些重複指令功能,直到指定的計數暫存器歸零為止。

以上這些解釋,就是memcpy的基本原理。下面講解我們這個內嵌彙編語句的設計思想。由於我們不知道memcpy需要拷貝的資料量是多少,也就是說不能確定引數Num的對其方式,是以位元組、字、雙字、四字?而且,每一種對齊方式導致的movs指令執行的次數也不一樣,並且直接影響執行效率。所以,我們使用以四字(8位元組,64位)傳輸為主,以要拷貝的資料總長度除以8,算出可以傳輸8位元組的資料量。將這部分以movsq的方式將資料複製到目標地址處,然後將引數Num的低位元組位(在程式中:%b4)與4、2、1進行與操作,如果與操作後的結果為1,那麼進行相應的movsl、movsw、movsb操作,將剩餘的輸出複製到目的地址上。

在細節上,輸出部分使用了“&”符,是因為在輸入部分有暫存器約束縮寫“q”的存在,導致將有暫存器分配方面的限制。cld指令用於清除DF標誌位,使(E)SI和(E)D暫存器在rep指令的作用下自增長。對於損壞部分的“memory”,宣告這個內嵌彙編程式碼段裡有修改記憶體的操作。最後返回To的地址,這個地址只在內嵌彙編語句的輸入部分出現過,所以,整個彙編執行的過程不會修改該地址。

memset

有了前面的基礎,我想大家理解剩下的內容應該會更加有信心了吧。我們繼續看memset函式的程式碼:

inline void * memset(void * Address,unsigned char C,long Count)
{
    int d0,d1;
    __asm__    __volatile__    (    "cld    \n\t"
                    "rep    \n\t"
                    "stosq    \n\t"
                    "testb    $4,%b3    \n\t"
                    "je    1f    \n\t"
                    "stosl    \n\t"
                    "1:\ttestb    $2,%b3    \n\t"
                    "je    2f    \n\t"
                    "stosw    \n\t"
                    "2:\ttestb    $1,%b3    \n\t"
                    "je    3f    \n\t"
                    "stosb    \n\t"
                    "3:    \n\t"
                    :"=&c"(d0),"=&D"(d1) 
                    :"a"(C),"q"(Count),"0"(Count/8),"1"(Address)    
                    :"memory"                
                );
    return Address;
} 

在memcpy函式裡面涉及的內容在memset函式裡也有出現,像:rep指令,cld指令,test指令,他們的意思與上文提到的基本相同,相信讀者應該不會太過陌生,這裡就不再解釋啦。我們重點來看一下stosb指令。

指令stosb在Intel技術文件中的解釋:

In non-64-bit and default 64-bit mode; stores a byte, word, or doubleword from the AL, AX, or EAX register (respectively) into the destination operand. The destination operand is a memory location, the address of which is read from either the ES:EDI or ES:DI register (depending on the address-size attribute of the instruction and the mode of operation).

大概意思:不管是否是在64位模式下,都會從AL、AX或EAX暫存器儲存一個位元組、一個字或一個雙字到目的地址。這個目的地址是從ES:(E)DI或ES:DI暫存器中讀取出來的。

Here also ES:(E)DI is assumed to be the destination operand and AL, AX, or EAX is assumed to be the source operand. The size of the destination and source operands is selected by the mnemonic: STOSB (byte read from register AL), STOSW (word from AX), STOSD (doubleword from EAX).
STOSQ (and its explicit operands variant) store a quadword from the RAX register into the destination addressed by RDI or EDI

大概意思:這裡,ES:(E)DI被用來指定目標操作;AL、AX或EAX被用來指定源操作。源和目的運算元的大小根據指令而定,stosb(從AL暫存器讀一個位元組),stosw(從AX暫存器讀一個字),stosd(從EAX暫存器讀雙字);stosq儲存四字從RAX暫存器,到RDI或EDI所指的目標地址中。

注意:這裡gcc使用stosl代表雙字操作,Intel使用stosd代表雙字操作,不是筆誤。

相信memset的原理已經很明顯了,就是使用stos(b|w|l|q)指令結合rep重複指令,將(R|E)AX暫存器的內容儲存到(R|E)DI暫存器所指的地址中。

PS:由於是在創作初期,文章可能會有錯誤、內容遺漏、闡述的不到位或者讀者想看而我沒有寫到的內容,希望大家多指點(留言或者發資訊給我都可以)。
考慮到以上原因,希望大家能夠關注或者推薦本書,讓更多的人學習到,並避免錯過之前內容的更新與補充

相關文章