在linux內核的源代碼中,以彙編語言編寫的程序或程序段,有兩種不一樣的形式。
第一種事徹底的彙編代碼,這樣的代碼採用.s做爲文件的後綴。事實上,儘管是徹底的彙編代碼,現代的彙編工具也吸取了C語言的長處,也在彙編以前加上了一趟預處理,而預處理以前的文件則以.s爲後綴。此類(.s)文件也和C程序同樣,可使用#include、#ifdef等等成分,而數據結構也同樣能夠在.h的文件中加以定義。linux
第二種是嵌在C程序中的彙編語言片段。雖然在ANSI的C語言標準中並無關於彙編片斷的規定,事實上各類實際使用的C編譯中都做了這方面的擴充,而 GNU的C編譯gcc也在這方面做了很強的擴充。程序員
在DOS/windows領域中,386彙編語言都採用Intel定義的語句格式。但是,在Unix領域中,採用的倒是由AT&T定義的格式。windows
AT&T的彙編與Intel的彙編主要有如下的區別:
在Intel格式中大多使用大寫字母,而在AT&T格式中都使用小寫字母。
在AT&T格式中,寄存器名要加上「%」做爲前綴 ,而在Intel格式中不帶前綴。
在AT&T的386彙編語言中,指令的源操做數的順序與在Intel的386彙編語言中正好相反。
在AT&T格式中,訪問指令的操做數的寬度有操做碼名稱的最後一個字母(操做碼的後綴決定)。用做操做碼後綴的字母有b(8位)。 w(16位)和1(32位)。 而在Intel格式中,則是在表示內存單元的操做數前面加上「BYTE PTR」「WORD PTR」,「DWORD PTR」來表示。
在AT$T格式中,直接操做數要加上「$」做爲前綴 ,而在Intel格式中則不帶前綴。
在AT$T格式中,絕對轉移和調用指令jump/call的操做數要加上「*」做爲前綴 ,而在intel格式則不帶。
遠程的轉移指令和子程序調用指令的操做碼名稱,在AT$T格式中爲「ljump」和「lcall」, 而在intel格式中,則爲「JMP FAR」和「CALL FAR」當轉移和調用的目標爲直接操做數時,兩種不一樣的表示以下:
CALL FAR SECTION:OFFSET(Intel 格式)
JMP FAR SECTION:OFFSET(Intel 格式)
lcall $section,$offset (AT$T格式)
lcall $secton,$offset (AT$T格式). 與之相應的遠程返回指令,則爲:
RET FAR STACK_ADJUST (Intel 格式)
Lret $stack_adjust (AT$T 格式)
間接尋址的通常格式,二者的區別以下:
SECTION :[BASE+INDEX*SCALE+DISP](Intel 格式)
Section: disp(base,index,scale)(AT$T 格式)
當須要在C語言的程序中嵌入一段彙編語言程序時,可使用gcc提供的「asm」語句功能。
通常而言,往C代碼中插入彙編語言的代碼片要比「純粹」的彙編語言代碼複雜的多,由於這裏有個怎樣分配使用寄存器,怎樣與C代碼中的變量結合的問題。爲了這個目的,必須對所用的彙編語言做更多的擴充,增長對彙編工具的指導做用。其結果是其語法實際上變成了既不一樣於彙編語言,也不一樣於C語言的某種中間語言。數據結構
插入C代碼的一個彙編語言代碼片斷能夠分爲四個部分,以「:」號加以分隔,其通常形式爲:指令部: 輸出部:輸入部:損壞部工具
1:第一部分就是彙編代碼自己,其格式和在彙編語言中使用基本相同。這一部分稱爲「指令部」,是必須有的,而其餘部分可視具體狀況而省略 。當將彙編語言代碼片斷嵌入到C代碼中時,操做數與C代碼中的變量如何結合顯然是個問題。Gcc採用的策略是:程序員提供具體的指令,而對寄存器的使用則通常只提供一個「樣板」和一些約束條件,而把到底如何與變量結合的問題留給了gcc和gas處理.優化
2:在指令部中,數字加上前綴%,如%0、%1等等,表示須要使用的寄存器的樣板操做數 。可使用的此類操做數的總數取決於具體CPU中通用寄存器的數量。這樣,指令部中用到了幾個不一樣的這種操做數,就說明有幾個變量須要與寄存器結合,由gcc和gas在編譯和彙編時根據後面的約束條件自行變通處理。因爲這些樣板操做數也使用「%」前綴,在涉及到具體的寄存器時就要在寄存器名前面加上兩個「%」符 ,以避免混淆。spa
3:緊接在指令部後面的是「輸出部」,用以規定對輸出變量如何結合的約束條件。每一個這樣的條件稱爲一個「約束」。必要是輸出部能夠有多個約束,互相以逗號分隔。每一個輸出約束以「=」號開始 ,而後是一個字母表示對操做數類型的說明,而後是關於變量結合的約束 。凡是與輸出部中說明的操做數相結合的寄存器或操做數自己,在執行嵌入的彙編代碼後均不保留執行以前的內容,這就給gcc提供了調度這些寄存器的依據。.net
4:輸出部後面是「輸入部」。輸入約束的格式和輸出約束類似,但不帶「=」號 。若是一個輸入約束要求使用寄存器,則在預處理時gcc會爲之分配一個寄存器,並自動插入必要的指將操做數即變量的值裝入該寄存器。與輸入部中說明的操做數結合的寄存器或操做數自己,在執行嵌入彙編代碼後也不保留執行以前的內容。指針
在有些操做中,除用於輸入數據操做和輸出數的寄存器之外,還要將若干個寄存器用於計算或操做的中間結果。這樣,這些寄存器原有的內容就損壞了,因此要在損壞部隊操做的反作用加以說明,讓gcc採起相應的措施。blog
操做數的編號從輸出部的第一個約束(序號爲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」爲約束條件,表示操做完成後內存中的內容已有改變,若是原來某個寄存器(也許在本次操做彙總並未使用到)的內容來自內存,則如今科能已經不一致。
還要注意,當輸出部爲空,即沒有輸出約束時,若是有輸入約束存在,則必須保留分隔標記「:」號。
備註:(舉例)
一 基本語法
語法上主要有如下幾個不一樣.
★ 寄存器命名原則
AT&T: %eax Intel: eax
★源/目的操做數順序
AT&T: movl %eax,%ebx Intel: mov ebx,eax
★常數/當即數的格式
AT&T: movl $_value,%ebx Intel: mov eax,_value
把_value的地址放入eax寄存器
AT&T: movl $0xd00d,%ebx Intel: mov ebx,0xd00d
★ 操做數長度標識
AT&T: movw %ax,%bx Intel: mov bx,ax
★尋址方式
AT&T: immed32(basepointer,indexpointer,indexscale)
Intel: [basepointer + indexpointer*indexscale + imm32)
linux工做於保護模式下,用的是32位線性地址,因此在計算地址時不用考慮segment:offset的問題.上式中的地址應爲:
imm32 + basepointer + indexpointer*indexscale
下面是一些例子:
★直接尋址
AT&T: _booga ; _booga是一個全局的C變量
注意加上$是表示地址引用,不加是表示值引用.
注:對於局部變量,能夠經過堆棧指針引用.
Intel: [_booga]
★寄存器間接尋址
AT&T: (%eax)
Intel: [eax]
★變址尋址
AT&T: _variable(%eax)
Intel: [eax + _variable]
AT&T: _array(,%eax,4)
Intel: [eax*4 + _array]
AT&T: _array(%ebx,%eax,icon_cool.gif
Intel: [ebx + eax*8 + _array]
二 基本的行內彙編
·基本的行內彙編很簡單,通常是按照下面的格式:
asm("statements");
例如:asm("nop"); asm("cli");
·asm 和 __asm__是徹底同樣的.
·若是有多行彙編,則每一行都要加上 " ".例如:
asm( "pushl %eax "
"movl $0,%eax "
"popl %eax");
實際上gcc在處理彙編時,是要把asm(...)的內容"打印"到彙編文件中,因此格式控制字符是必要的.
再例如:
asm("movl %eax,%ebx");
asm("xorl %ebx,%edx");
asm("movl $0,_booga);
在上面的例子中,因爲咱們在行內彙編中改變了edx和ebx的值,可是因爲gcc的特殊的處理方法,即先造成彙編文件,再交給GAS去彙編,因此GAS並不知道咱們已經改變了edx和ebx的值,若是程序的上下文須要edx或ebx做暫存,這樣就會引發嚴重的後果 .對於變量_booga也存在同樣的問題.爲了解決這個問題,就要用到擴展的行內彙編語法.
三 擴展的行內彙編
擴展的行內彙編相似於Watcom.
基本的格式是:
asm ( "statements" : output_regs : input_regs : clobbered_regs);
clobbered_regs指的是被改變的寄存器.
下面是一個例子(爲方便起見,我使用全局變量):
int count=1;
int value=1;
int buf[10];
void main()
{
asm(
"cld "
"rep "
"stosl"
:
: "c" (count), "a" (value) , "D" (buf[0])
: "%ecx","%edi" );
}
獲得的主要彙編代碼爲:
movl count,%ecx
movl value,%eax
movl buf,%edi
#APP
cld
rep
stosl
#NO_APP
cld,rep,stos就不用多解釋了.這幾條語句的功能是向buf中寫上count個value值.冒號後的語句指明輸入,輸出和被改變的寄存器.經過冒號之後的語句,編譯器就知道你的指令須要和改變哪些寄存器,從而能夠優化寄存器的分配.其中符號"c"(count)指示要把count的值放入ecx寄存器
幾點說明:
1.使用q指示編譯器從eax,ebx,ecx,edx分配寄存器.使用r指示編譯器從eax,ebx,ecx,edx,esi,edi分配寄存器.
2.咱們沒必要把編譯器分配的寄存器放入改變的寄存器列表,由於寄存器已經記住了它們.
3."="是標示輸出寄存器,必須這樣用.
4.數字%n的用法:數字表示的寄存器是按照出現和從左到右的順序映射到用"r"或"q"請求的寄存器.若是咱們要重用"r"或"q"請求的寄存器的話,就可使用它們.
5.若是強制使用固定的寄存器的話,如不用%1,而用ebx,則asm("leal (%%ebx,%%ebx,4),%0"
: "=r" (x)
: "0" (x) );
6.注意要使用兩個%,由於一個%的語法已經被%n用掉了.
FAQ:
一、變量加下劃線和雙下劃線有什麼特殊含義嗎?
加下劃線是指全局變量,但個人gcc中加不加都無所謂.
二、以上定義用以下調用時展開會是什麼意思?#define _syscall1(type,name,type1,arg1) type name(type1 arg1) { long __res; /* __res應該是一個全局變量 */__asm__ volatile ("int $0x80" /* volatile 的意思是不容許優化,使編譯器嚴格按照你的彙編代碼彙編*/: "=a" (__res) /* 產生代碼 movl %eax, __res */: "0" (__NR_##name),"b" ((long)(arg1))); /* 若是我沒記錯的話,這裏##指的是兩次宏展開. 即用實際的系統調用名字代替"name",而後再把__NR_...展開. 接着把展開的常數放入eax,把arg1放入ebx */if (__res >= 0) return (type) __res; errno = -__res; return -1; }