AT&T彙編學習筆記

AT&T彙編和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”。

(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, %al                   (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 SECTION: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格式)

這種尋址方式常常用於在數據結構數組中訪問特定元素內的一個字段,base爲數組的起始地址,scale爲每個數組元素的大小,index爲下標。如果數組元素是數據結構,則disp爲具體字段在結構中的偏移。

注意在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格式)


嵌入C代碼中的386彙編語言程序段

當需要在C語言的程序中嵌入一段彙編語言程序段時,可以使用gcc提供的“asm”語句功能。例如:#define __SLOW_DOWN_IO __asm__ __volatile__ ("outb %al,$0x80")

這是一條8位輸出指令,如前所述在操作符上加上後綴”b”表示這是8位的,而0x80因爲是常數,即所謂的“直接操作數”,所以要加上前綴“ $”,而寄存器名al也加了前綴”%“。

上面那條彙編語句很好理解,在來看一個稍微困難點的例子:

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代碼中的一個彙編語言片斷可以分成四部分,以“:”號加以分隔,其一般形式爲:

指令部:輸出部:輸入部:損壞部

第一部分就是彙編語句本身,其格式與在彙編語言程序中使用的基本相同,但也有區別。這一部分可以稱爲“指令部”,是必須有的,而其他各部分則可視具體的情況而省略。所以在最簡單的情況加就與常規的彙編語句基本相同。

當將彙編語言代碼片斷嵌入到C代碼中時,操作數與C代碼中的變量如何結合顯然是個問題。因爲程序員在編寫嵌入的彙編代碼時,按照程序邏輯的要求很清楚應該選用什麼指令,但是卻無法確切地知道gcc在嵌入點的前後會把那一個寄存器分配用於哪一個變量,以及哪一個或哪幾個寄存器是空閒着的。而且,光是被動地知道gcc對寄存器的分配情況也還是不夠,還得有個手段把使用寄存器的要求告知gcc,反過來影響它對寄存器的分配。當然,如果gcc的功能非常強,那麼通過分析嵌入的彙編代碼也應該能夠歸納出這些要求,再通過優化,最後也能達到目的。但是,即使這樣,所引入的不確定性也還是個問題,更何況要做到這樣還不容易,針對這個問題,gcc採取了一種折中的辦法:程序員只提供具體的指令,而對寄存器的使用則一般只提供一個樣板和一些約束條件,而把到底如何與變量結合的問題留給gcc和gas去處理。

在指令部中,數字加上前綴%,如%0、%1等等,表示需要使用寄存器的樣板操作數。可以使用此類操作數的總數取決於具體CPU中通用寄存器的數量。這樣,指令部中用到了幾個不同的這樣的操作數,就說明有幾個變量需要與寄存器結合,由gcc和gas在編譯和彙編時根據後面的約束條件自行變通處理。由於這些樣板操作數也使用“%”前綴,在涉及到具體的寄存器時就要在寄存器名前面加上兩個“%”符,以免混淆。

那麼,怎樣表達對變量結合的約束條件呢?這就是其餘幾個部分的作用。緊接在指令部後面的是“輸出部”,用以規定對輸出變量,即目標操作數如何結合的約束條件。每個這樣的條件稱爲一個“約束”。必要時輸出部中可以有多個約束,相互以逗號分隔。每個輸出約束以“=”號開頭,然後是一個字母表示對操作數類型的說明,然後是關於變量結合的約束。例如,在上面的例子中輸出部爲

:”=m”(v->counter)

這裏具有一個約束,”=m”表示相應的目標操作數(指令部中的%0)是一個內存單元。凡是與輸出部中說明的操作數相結合的寄存器或操作數本身,在執行嵌入的彙編代碼後均不保留執行之前的內容,這就給gcc提供了調度使用這些寄存器的依據。

 

輸出部後面是“輸入部”。輸入約束的格式與輸出約束相似,但不帶“=”號。在前面例子中的輸入部有兩個約束。第一個爲”ir(i)”,表示指令中的%1可以是一個在寄存器中的直接操作數(i表示immediate),並且該操作數來自於C代碼中的變量名(這裏是調用參數)i。第二個約束爲”m”(v->counter),意義與輸出約束中相同。如果一個輸入約束要求使用寄存器,則在預處理時gcc會爲之分配一個寄存器,並自動插入必要的指令將操作數即變量的值裝入該寄存器。與輸入部中說明的操作數結合的寄存器或操作數本身,在執行嵌入的彙編代碼以後也不保留執行之前的內容。例如,這裏的1%要求使用寄存器,所以gcc會爲其分配一個寄存器,並自動插入一條movl指令把參數i的數值裝入該寄存器,可是這個寄存器原來的值就不復存在了。如果這個寄存器本來就是空閒的,那倒無所謂,可是如果所有的寄存器都在使用,而只好暫時借用一個,那就得保證在使用以後恢復其原有的內容。此時gcc會自動在開頭處插入一條pushl指令,將該寄存器原來的內容保存在堆棧中,而在結束後插入一條popl指令,恢復寄存器的內容。

 

在有些操作能夠中,除用於輸入操作數和輸出操作數的寄存器以外,還要將若干個寄存器用於計算和操作的中間結果,這樣,這些寄存器原來的內容就損壞了,所以要在損壞部對操作的副作用加以說明,讓gcc採取相應的措施。不過,有時候就直接把這些說明放在輸出部了,那也並無不可。

 

操作數的編號從輸出部的第一個約束(序號爲0)開始,順序數下來,每個約束計數一次。在指令部中引用這些操作數或分配用於這些操作數的寄存器時,就用序號前面加上一個“%”號。在指令部中引用一個操作數時總是把它當成一個32位的“長字”,但是對其實施的操作,則根據需要也可以是字節操作或字操作。對操作數進行的字節操作默認爲對其低字節的操作,字操作也是一樣。不過,在一些特殊的操作中,對操作數進行字節操作時也允許明確指出是對哪一個字節操作,此時在%與序號之間插入一個“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)

此外,如果一個操作數要求使用與前面某個約束中所要求的是同一個寄存器,那就把那個約束對應的操作數編號放在約束條件中。在損壞部常常會以”memory”爲約束條件,表示操作完成後內存中的內容已有改變,如果原來某個寄存器的內容來自內存,則現在可能已經不一致。

 

還要注意,當輸出部爲空,即沒有輸出約束時,如果有輸入約束存在,則須保留分隔標記“:”號。

 

回到上面的例子,這段代碼的作用是將參數i的值加到v->counter上,代碼中的關鍵字LOCK表示在執行addl指令時要把系統總線鎖住,不讓別的CPU打擾。將兩個數相加是很簡單的操作,C語言中明明有相應的語言成分,如:“v->counter+=I;”爲什麼要用匯編呢?原因就在於,這裏要求整個操作只由一條指令完成,並且將總線鎖住,以保證操作的“原子性”。相比之下,C語句在編譯之後到底有幾條指令是沒有保證的,也無法要求在計算過程中對總線加鎖。

再看一段嵌入彙編代碼:

//取自include/asm-i386/bitops.h
static inline void set_bit(int nr, volatile void *addr)
{
       asm volatile(
       lock;
"bts %1,%0"
: "=m" (*(volatile long *) addr)
: "Ir" (nr)
: "memory");
}

這裏的指令btsl將一個32位操作數中的某一位設置成1,參數nr表示該位的位置。


再來看一個複雜一點的例子:

//取自include/asm-i386/string.h
static __always_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), "g" (n), "1" ((long) to), "2" ((long) from)
       : "memory");
return (to);
}

__memcpy是內核中對memcpy()的底層實現,用來複制一塊內存空間的內容,而忽略其數據結構。這是使用非常頻繁的一個函數,所以其運行效率十分重要。

 

先看約束條件和變量與寄存器的結合。輸出部有三個約束,對應於操作數%0至%2。其中變量d0爲操作數%0,必須放在寄存器ecx中,原因等下就會明白。同樣,d1即%1必須放在寄存器edi中;d2即2%必須放在寄存器esi中。再看輸入部,這裏有四個約束,對應於操作數%3至%6。其中操作數%3與操作數%0使用同一個寄存器,所以也必須是寄存器ecx;並且要求由gcc自動插入必要的指令,實現將其設置成n/4,實際上是將複製長度從字節個數n換算成長字個數n/4。至於n本身,則要求gcc任意分配一個寄存器存放。操作數5%與6%,即參數to與from,分別與%1和%2使用相同的寄存器,所以也必須是寄存器edi和esi。

 

再看指令部,第一條指令是“rep”,表示下一條指令movsl要重複執行,每重複一遍就把寄存器ecx中的內容減1,直到變成0爲止。所以,在這段代碼中一共執行n/4次。那麼movsl又幹些什麼呢?它從esi所指的地方複製一個長字到edi所指的地方,並使esi和edi分別加4。這樣,當代碼中的"rep ; movsl/n/t"執行完畢,所有的長字都已複製好,最多只剩下三個字節了,在這個過程中,實際上使用了ecx、edi以及esi三個寄存器。即%0(同時也是%3)、%2(同時也是%6)以及1%(同時也是%5)三個操作數,這些都隱含在指令中,從字面上看不出來。同時,這也說明了爲什麼這些操作書必須存放在指定的寄存器中。

 

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



發佈了53 篇原創文章 · 獲贊 5 · 訪問量 29萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章