Linux核心入門(四)—— 核心組合語言規則

來源:互聯網
上載者:User

    任何一個用進階語言編寫的作業系統,其核心原始碼中總有少部分代碼是用組合語言編寫的。讀
過Unix Sys V原始碼的讀者都知道,在其約3萬行的核心代碼中用組合語言編寫的代碼約2000行,分
成不到20個副檔名為.s和.m的檔案,其中大部分是關於中斷與異常處理的底層程式,還有就是與初始
化有關的程式以及一些核心代碼中調用的公用子程式。

    用組合語言編寫核心代碼中的部分代碼,大體上是出於如下幾個方面的考慮的:

   ●   作業系統核心中的底層程式直接與硬體打交道,需要用到一些專用的指令,而這些指令在C
語言中並無對應的語言成分。例如,在386系統結構中,對外設的輸入/輸出指令如inb, outb
等均無對應的C語言語句。因此,這些底層的操作需要用組合語言來編寫。CPU中的一些對
寄存器的操作也是一樣,例如,要設定一個段寄存器時,也只好用組合語言來編寫。
   ●   CPU中的一些特殊指令也沒有對應的C語言成分,如關中斷,開中斷等等。此外,在同一種
系統結構的不同CPU晶片中,特別是新開發出來的晶片中,往往會增加一些新的指令,例如
Pentium, Pentium II和Pentium MMX,都在原來的基礎土擴充了新的指令,對這些指令的使用
也得用組合語言。
   ●   核心中實現某些操作的過程、程式段或函數,在運行時會非常頻繁地被調用,因此其(時間)
效率就顯得很重要。而用組合語言編寫的程式,在演算法和資料結構相同的條件下,其效率通常
要比用進階語言編寫的高。在此類程式或程式段中,往往每一條彙編指令的使用都需要經過推
敲。系統調用的進入和返回就是一個典型的例子。系統調用的進出是非常頻繁用到的過程,每
秒鐘可能會用到成千上萬次,其時間效率可謂舉足輕重。再說,系統調用的進出過程還牽涉到
使用者空間和系統空間之間的來回切換,而用於這個目的的一些指令在C語言中本來就沒有對
應的語言成分,所以,系統調用的進入和返回顯然必須用組合語言來編寫。
   ●   在某些特殊的場合,一段程式的空間效率也會顯得非常重要。作業系統的引異程式就是一個例
子。系統的引導程式通常一定要能容納在磁碟上的第一個扇區中。這時候,哪怕這段程式的大
小多出一個位元組也不行,所以就只能以組合語言編寫。

    在Linux核心的原始碼中,以組合語言編寫的程式或程式段,有幾種不同的形式:

    第一種是完全的彙編代碼,這樣的代碼採用.s作為檔案名稱的尾碼。事實上,儘管是“純粹”的彙編代碼,現代的彙編工具也吸收了C語言預先處理的長處,也在彙編之前加上了一趟預先處理,而預先處理
之前的檔案則以.S為尾碼。此類(.S)檔案也和C程式一樣,可以使用#include, #ifdef等等成分,而
資料結構也一樣可以在.h檔案中加以定義。

    第二種是嵌入在C程式中的組合語言片段。雖然在ANSI的C語言標準中並沒有關於彙編片段的
規定,事實上各種實際使用的C編譯中都作了這方面的擴充,而GNU的C編譯gcc也在這方面作了
很強的擴充。

    此外,核心代碼中也有幾個Intel格式的組合語言程式,是用於系統引導的。
由於我們專註於Intel i386系統結構下的Linux核心,下面我們只介紹GNU對i386組合語言的支援

    對於新接觸Linux核心原始碼的讀者,哪怕他比較熟悉i386組合語言,在理解這兩種組合語言的
程式或片段時都會感到困難,有的甚至會望而卻步。其原因是:在核心“純”彙編代碼中GNU採用了
不同於常用386組合語言的句法;而在嵌入C程式的片段中,則更增加了一些指導彙編工具如何分配
使用寄存器、以及如何與C程式中定義的變數相結合的語言成分。這些成分使得嵌入C程式中的彙編
語言片段實際上變成了一種介乎386彙編和C之間的一種中繼語言。

    所以,我們先集中地介紹一下在核心中這兩種情況下使用的386組合語言,以後在具體的情景中
涉及具體的組合語言代碼時還會加以解釋。

 

1 GNU的386組合語言



    在Dos/Windows領域中,386組合語言都採用由Intel定義的語句(指令)格式,這也是幾乎在所
有的有關386組合語言程式設計的教科書或參考書中所使用的格式。可是,在Unix領域中,採用的卻
是由AT&T定義的格式。當初,當AT&T將Unix移植到80386處理器上時,根據Unix圈內人上的習
慣和需要而定義了這樣的格式。Unix最初是在PDP-11機器上開發的,先後移植到VAX和68000系列
的處理器上。這些機器的組合語言在風格上、從而在格式上與Intel的有所不同。而AT&T定義的386
組合語言就比較接近那些組合語言。後來,在Unixware中保留了這種格式。GNU主要是在Unix領域
內活動的(雖然GNU是“GNU is Not Unix”的縮寫)。為了與先前的各種Unix版本與工具有儘可能好
的相容性,由GNU開發的各種系統工具自然地繼承了AT&T的386組合語言格式,而不採用Intel的
格式

    那麼,這兩種組合語言之間的差距到底有多大呢?其實是大同小異。可是有時候小異也是很重要
的,不加重視就會造成困擾。具體講,主要有下面這麼一些差別:

   (1)   在Intel格式中大多使用大寫字母,而在AT&T格式中都使用小寫字母。
   (2)   在AT&T格式中,寄存器名要加上“%”作為首碼,而在Intel格式中則不帶首碼。
   (3)   在AT&T的386組合語言中,指令的源運算元與目標運算元的順序與在Intel的386組合語言
中正好相反。在Intel格式中是目標在前,源在後;而在AT&T格式中則是源在前,目標在後。
例如,將寄存器eax的內容送入ebx,在Intel格式中為"MOVE EBX,EAX",而在AT&T格
式中為"move %eax, %ebx"看來,Intel格式的設計者所想的是"EBX=EAX", 而AT&T
格式的設計者所想的是“%eax一>%ebx”。
   (4)   在AT&T格式中,訪內指令的運算元大小(寬度)由作業碼名稱的最後一個字母(也就是操
作碼的尾碼)來決定。用作作業碼尾碼的字母有b(表示8位),w(表示16位)和l(表示
32位)。而在Intel格式中,則是在表示記憶體單元的運算元前面加蔔"BYTE PTR","WORD
PTR",或"DWORD PTR"來表示。例如,將FOO所指記憶體單元中的位元組取入8位的寄存
器AL,在兩種格式中不同的表示如下:
      MOV AL, BYTE PTR FOO(Intel格式)
      movb FOO,%a1(AT&T格式)
   (5)   在AT&T格式中,直接運算元要加上“$”作為首碼,而在Intel格式中則不帶首碼。所以,
Intel格式中的"PUSH 4",在AT&T格式中就變為"pushl $4"。
   (6)   在AT&T格式中,絕對轉移或調用指令jump/call的運算元(也即轉移或調用的目標地址),
要加上“*”作為首碼(讀者大概會聯想到C語言中的指標吧),而在Intel格式中則不帶。
   (7)   遠端轉移指令和子程式調用指令的作業碼名稱,在AT&T格式中為“ljmp”和“lcall氣而
在Intel格式中,則為"JMP FAR"和"CALL FAR"。當轉移和調用的目標為直接運算元時,
兩種不同的表示如下:
      CALL FAR SECTION:OFFSET (Intel格式)
      JMP FAR SECTIOM:OFFSET (Intel格式)
      lcall $section, $offset (AT&T格式)
      ljmp $section,$offset (AT&T格式)
與之相應的遠程返回指令,則為:
      RET FAR STACK_ADJUST(Intel格式)
      lret $stack_adjust (AT&T格式)
   (8)   間接定址的一般格式,兩者區別如下:
      SECTION: [BASE + INDEX*SCALE + DISP](Intel格式)
      section: disp (base, index, scale)(AT&T格式)

    注意在AT&T格式中隱含了所進行的計算。例如,當SECTION省略,INDEX和SCALE也省略,
BASE為EBP,而DISP(位移)為4時,表示如下:
      [ebp-4](Intel格式)
      -4(%ebp) (AT&T格式)
在AT&T格式的括弧中如果只有一項base,就可以省略逗號,否則不能省略,所以(%ebp)相當
+(%ebp,,),進一步相當於(ebp, 0, 0)。又如,當INDEX為EAX, SCALE為4 (32位),DISP
為foo,而其他均省略,則表示為:
      [foo+EAX*4](Intel格式)
      foo(,%EAX,4)(AT&T格式)
這種定址方式常常用於在資料結構數組中訪問特定元素內的一個欄位,base為數組的起始地址,
scale為每個數組元素的大小,index為下標。如果數組元素是資料結構,則disp為具體欄位在結構中
的位移。

 

2 嵌入在C語言中的組合語言



    當需要在C語言的程式中嵌入一段組合語言程式段時,可以使用gcc提供的“asm”語句功能。其具體格式如下:

__asm__ ("彙編程式碼片段")
__asm__ __volatile__ (指定操作 + "彙編程式碼片段")

    由於具體的組合語言規則相當複雜,所以我們只關心與核心原始碼相關主要規則,並通過幾個例子來加以描述,其他規則具體請參考相關CPU的手冊。

    例1
:在include/asm-i386/io.h中有這麼一行:
    
#define __SLOW_DOWN_IO __asm__ __volatile__ ("outb %al, $0x80")

    表示8位輸出指令。b表示這是8位的,而0x80是常數,即所謂“直接運算元”,所以要加上首碼“$”,而寄存器名al也加了首碼“%”。

    例2
:在同一個asm語句中也可以插入多行組譯工具。就在同一個檔案中,在不同的條件下,__SLOW_DOWN_IO又有不同的定義:
    
#define __SLOW_DOWN_IO __asm__ __volatile__("/njmp 1f/n1:/tjmp 1f/n1:")
    這裡就不那麼直觀了,這裡,一種插入了三行彙編語句,“/n”就是分行符號,而“/t”則表示TAB符。這些規則跟printf語句中逸出字元的規則一樣:

      jmp lf

l:    jmp lf

l:

    這裡轉移指令的目標lf表示前往(f表示forward)找到第一個標號為l的那一行。相應地,如果是lb就表示往後找。所以這一小段代碼的用意就在於使CPU空做兩條轉移指令而消耗一些時間。

    例3
:下面看一段來自include/asm-i386/atomic.h的代碼。

static __inline__ void atomic_add(int i, atomic_t *v)

{
    __asm__ __volatile__(

        LOCK "addl %1,%0"

        :"=m" (v->counter)

        :"ir" (i), "m" (v->counter));

}

一般而言,往C代碼中插入組合語言的代碼是很複雜的,因為這裡有個分配寄存器呵與C語言代碼中的變數結合的問題。為了這個目的,必須對所使用的組合語言做更多的擴充,增加對彙編工具的指導作用。
    
下面,先介紹一下插入C代碼中的彙編成分的一般格式,並加以解釋。以後在我們碰到具體代碼時還會加以提示:

    插入C代碼中的一個組合語言代碼片斷可以分成四部分,以“:”號加以分隔,其一般形式為:
                指令部:輸出部:輸入部:損壞部

   
注意不要把這些“:”和程式標號中所用的(如前面的1:)混淆。
    第一部分就是彙編語句本身,其格式與組譯工具中使用的基本相同,但也有區別,不同支出馬上會講到。這一部分可以稱為“指令部”,是必須有的,而其他各部分則可視具體情況而省略,所以最簡單的情況下就與常規的彙編語句基本相同,如前面兩個例子那樣。
   
在指令部中,數字加上首碼%,如%0、%1等等,表示需要使用寄存器的樣板運算元。那麼,可以使用此類運算元的總數取決於具體CPU中通用寄存器的數量,
這樣,指令部中用到了幾個不同的運算元,就說明有幾個變數需要與寄存器結合,由gcc和gas在編譯時間根據後面的約束條件變通處理。
    
那麼,怎樣表達對變數結合的約束條件呢?這就是其餘幾個部分的作用。“輸出部
”,用以規定對輸出變數,即目標運算元
如何結合的約束條件。必要時輸出部中可以有多個約束,以逗號分隔。每個輸出約束以“=”號開頭,然後時以個字母表示對運算元類型的說明,然後時關於變數結合的約束。例如:
:"=m" (v->counter),這裡只有一個約束,“=m”表示相應的目標運算元(指令部中的%0)是一個記憶體單元
v->counter。凡是與輸出部中說明的運算元相結合的寄存器或運算元本身,在實行嵌入彙編代碼以後均部保留執行之前的內容,這就給gcc提供了調度使用這些寄存器的依據。

    
輸出部後面是“輸入部
”。
輸入約束的格式與輸出約束相似,但不帶“=”號。在前面例子中的輸入部有兩個約束。第一個為“ir”(i),表示指令中的%1可以是一個在寄存器中的“直
接運算元”,並且該運算元來自於C代碼中的變數名i(括弧中)。第二個約束為"m" (v->counter),意義與輸出約束中相同。

    
回過頭來,我們再來看指令部中的%號加數字,其代表指令的運算元的編號,表示從輸出部的第一個約束(序號為0)開始,順序數下來,每個約束計數一次。
    
另外,在一些特殊的操作中,對運算元進行位元組操作時也允許明確指出是對哪一個位元組操作,此時在%與序號之間插入一個”b“表示最低位元組,插入一個”h“表示次低位元組。

常用約束條件一覽
m, v, o —— 表示記憶體單元;
r —— 表示任何寄存器;
q —— 表示寄存器eax、ebx、ecx、edx之一;
i, h —— 表示直接運算元;
E, F —— 表示浮點數;
g —— 表示”任意“;
a, b, c, d —— 分表表示要求使用寄存器eax、ebx、ecx和edx;
S, D —— 分別表示要求使用寄存器esi和edi;
I —— 表示常數(0到31)。

回到上面的例子,讀者現在應該很容易理解這段代碼的作用是將參數I的值加到v->counter上。代碼中的關鍵字LOCK表示在執行addl指令時要把系統的匯流排鎖住,保證操作的”原子性(atomic)“

    例4
:再看一段嵌入彙編代碼,這一次取自include/asm-i386/bitops.h

#ifdef CONFIG_SMP
#define LOCK_PREFIX "lock ; "
#else
#define LOCK_PREFIX ""
#endif

#define ADDR (*(volatile long *) addr)

static __inline__ void set_bit(int nr, volatile void * addr)

{
    __asm__ __volatile__( LOCK_PREFIX

        "btsl %1,%0"

        :"=m" (ADDR)

        :"Ir" (nr));

}

   這裡的指令btsl將一個32位運算元中的某一位設定成1。參數nr和addr表示將記憶體位址為addr的32位元的nr位設定成1。

    例5
:再來看一個複雜,但又非常重要的例子,來自include/asn-i386/string.h:

static inline void * __memcpy(void * to, const void * from, size_t n)

{
int d0, d1, d2;

__asm__ __volatile__(

    "rep ; movsl/n/t"

    "testb $2,%b4/n/t"

    "je 1f/n/t"

    "movsw/n"
    "1:/ttestb $1,%b4/n/t"

    "je 2f/n/t"

    "movsb/n"
    "2:"
    : "=&c" (d0), "=&D" (d1), "=&S" (d2)

    :"0" (n/4), "q" (n),"1" ((long) to),"2" ((long) from)

    : "memory");

return (to);
}

    這裡的__memcpy函數就是我們經常調用的memcpy函數的核心底層實現,用來複製記憶體空間的內容。參數to是複製的目的地址,from是源地址,n位複製的內容的長度,單位是位元組。gcc產生以下代碼:

rep ; movsl

      testb $2, %b4

      je 1f

      movsw
1:    testb $1, %b4

      je 2f

      movsb
2:

    其中輸出部有三個約束,函數內部變數d0、d1、d2分別對應運算元%0至%2,其中d0必須放在ecx寄存器中;d1必須放在edi寄存器中;d2必須放在esi寄存器中。再看輸入部,這裡又有四個約束分別對應運算元%3、
%4、%5、%6。其中運算元%3與運算元%0使用同一個寄存器ecx,表示將複製長度從位元組個數換算成長字個數(n/4);%4表示n本身,要求任意分配一個寄存器存放;%5、%6即參數to和from,分別與%1和%2使用相同的寄存器(edi和esi)
    
再看指令部。第一條指令是”rep“,只是一個標號,表示下一條指令movsl要重複執行,每重複一遍就把寄存器ecx中的內容減1,直到變成0為止。所
以,在這段代碼中一共執行n/4次。movsl是386指令系統中一條很重要的複雜指令,它從esi所指到的地方複製一個長字到edi所指的地方,並使
esi和edi分別加4。這樣,當代碼中的movsl指令執行完畢,準備執行testb指令的時候,所有的長字都複製好了,最多隻剩下三個位元組了。在這個
過程中隱含用到了上述三個寄存器,這就說明了為什麼這些運算元必須在輸入和輸出部中指定必須存放的寄存器。

    接著就是處理剩下的位元組了(最多三個)。先通過testb測試運算元%4,即複製長度n的最低位元組中的bit2,如果這一位位1就說明至少還有兩
個位元組,所以就通過movesw複製一個短字(esi和edi則分別加2),否則就把它跳過。再通過testb測試運算元%4的bit1,如果這一位為
1,就說明還剩一個位元組,所以通過指令movsb再複製一個位元組,否則跳過。當達到標號2的時候,執行就結束了。

    

    在include/asm-i386中有許多最基本的彙編函數,有時間的話,大家不妨隨便找幾個練習一下。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.