在Linux內核代碼中,有一部分是用匯編語言編寫的。其大部分是關于中斷與異常處理的底層程序,還有就是與初始化有關的程序,以及一些核心代碼中調用的公用子程序。
用匯編語言編寫內核代碼中的部分代碼,大體上是出于如下幾個方面考慮:
(1)linux內核中的底層程序直接與硬件打交道,需要一些專用的指令,而這些指令在C語言中并無對應的語言成分。
(2)內核中實現某些操作的過程、程序段或函數,在運行時會非常頻繁的被調用,這時用匯編語言編寫,其時間效率會有大幅度提高。
(3)在某些特殊的場合,一段程序的空間效率也非常重要,比如操作系統的引導程序一定要容納在磁盤的第一個扇區中,多一個字節都不行。這時只能用匯編語言編寫。
在Linux內核代碼中,以匯編語言編寫的程序或者程序段,有兩種不同的形式。一是完全的匯編代碼,這樣的代碼采用.s作為文件名的后綴。第二種是嵌入在C程序中的匯編語言片斷。
對于新接觸linux內核源碼的讀者,哪怕他比較熟悉i386匯編語言,在理解這些匯編程序時都會感到困難,有的甚至會望而卻步。其原因是:在內核“純”匯編代碼中GNU采用不同于常用Intel i386匯編語言的AT&T格式的匯編語言;而在嵌入C程序的片斷中,更增加了一些指導匯編工具如何分配使用寄存器、以及如何與C程序中定義的變量相結合的語言成分。這些成分使得嵌入C程序的匯編語言片斷實際上變成了一種介乎386匯編和C之間的一種中間語言。
首先我們講一下AT&T格式與Intel格式匯編語言的以下主要區別,其它的詳細情況可以參考AT&T匯編語言手冊。
(1)在Intel格式中大多使用大寫字母,而AT&T格式中都使用小寫字母。
(2)在AT&T格式中,寄存器名上要加“%”作為前綴,而Intel格式則不帶前綴。
(3)在AT&T格式中,指令的源操作數在前,目標在后,恰好與Intel格式完全相反。
(4)在AT&T格式中,訪內指令的操作數大小由操作碼后綴來決定。用作操作碼后綴的字母有b(表示8位),w(表示16位),l(表示32位)。而在Intel格式中,則是在表示內存單元的操作數前面加上“BYTE PTR”,“WORD PTR”,“DWORD PTR”來表示。
當需要在C語言的程序中嵌入一段匯編語言程序段時,可以使用gcc提供的“asm”語句功能,例如,在include/asm/io.h中有這么一行:
#define __SLOW_DOWN_IO __asm__ __volatile__(“outb %a1,$0x80”)
這里,在asm和volatile前面的兩個“__”字符,這是gcc對C語言的一種補充,含義我們在前面已經講過了。下面我們看括號里面加上了引號的匯編指令,這是一條8位輸出指令,如前所述在操作符上加上后綴“b”表示是8位操作,而0x80因為是常數,所以要加上前綴“$”,而寄存器a1也加了前綴“%”。
上面這個例子還是很容易理解的,因為這就是簡單的一條匯編語句,下面這個例子就困難多了(取子include/asm/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語言中的某些變量結合時,情況就復雜多了。如此例中,i與v都是C語言函數的輸入部分,怎么將其與匯編語言結合在一起呢?因為程序員無法確切知道gcc在嵌入點的前后會把哪一個寄存器分配用于哪一個變量,而且還得有個手段把使用寄存器的要求告訴gcc,反過來影響它對寄存器的分配。針對這個問題,gcc采用的辦法是:程序員只提供具體的指令,而對寄存器的使用則只提供一個“樣板”和一些約束條件,而把到底如何與變量結合的問題留給gcc和gas去處理。
在指令部中,數字加上前綴%,表示需要使用寄存器的樣板操作數。這樣,指令部中用到了幾個不同的這種操作數,就說明有幾個變量需要與寄存器結合,由gcc和gas在編譯和匯編時根據后面的約束條件自行變通處理。那么,怎樣表達對變量結合的約束條件呢?這就是其余幾個部分的作用。“輸出部”用以規定對輸出變量的約束條件,必要時輸出部可以有多個約束,互相之間用逗號分隔。每個輸出約束以“=”號開頭,然后是一個字母表示對操作數類型的說明,然后是關于變量結合的約束。例如在本例中,輸出部里只有一個約束,“=m”表示相應的操作數(指令部中的%0)是一個內存單元v->counter。
輸出部后面是“輸入部”,輸入約束的格式與輸出約束相似,但不帶“=”號。在本例中有兩個輸入約束,第一個為”ir”(i),表示指令中的%1可以是一個寄存器中的直接操作數(i表示immediate,r表示任何寄存器),并且該操作數來自C代碼中的變量名i;第二個約束為”m”(v->counter)表示這是一個內存單元。
表示約束條件的字母有很多,主要有:“m”、”v”和”o”表示內存單元;”r”表示任何寄存器;”q”表示寄存器eax、ebx、ecx、edx之一;”i”和”h”表示直接操作數;”a”、”b”、”c”、”d”分別表示要求使用寄存器eax,ebx,ecx和edx;”S”和”D”分別表示要使用esi或edi;”I”表示常數(0至31)。
在有些操作中,除用于輸入操作和輸出操作數的寄存器以外,還要將若干寄存器用于計算或操作的中間結果。這樣,這些寄存器原有的內容就損壞了,所以要在損壞部對操作的副作用加以說明,讓gcc采取相應的措施,不過,有時候就直接把這些說明放在輸出部了。另外還應注意,當輸出部為空,即沒有輸出約束時,如果有輸入約束存在,則必須保留分隔標記“:”號。