顯示具有 x86 Assembly Infomation 標籤的文章。 顯示所有文章
顯示具有 x86 Assembly Infomation 標籤的文章。 顯示所有文章

2010年4月1日 星期四

GCC 內嵌組合語言介紹

asm__ __volatile__("": : :"memory"); //這句語法是什麼意思,它是內嵌組合語言,我比較不明白的是因為內嵌理頭沒有任何指令,倒是"memory"比較讓我難理解。
先說明語法,內嵌彙編語法如下:
語法可參考http://topic.csdn.net/u/20090521/11/35609357-57b9-4c7c-af6a-04a76eef49a3.html
__asm__(彙編語句範本: 輸出部分: 輸入部分: 破壞描述部分)共四個部分:彙編語句範本,輸出部分,輸入部分,破壞描述部分,各部分使用":"格開,彙編語句範本必不可少,其他三部分可選,如果使用了後面的部分,而前面部分為空,也需要用":"格開,相應部分內容為空。例如:
__asm__ __volatile__("cli": : :"memory")
我一開始不理解其中的"memory"是什麼意思,後來在網上看到說是設置“記憶體屏障”的,並找到了關於"memory"的一段闡述:”memory強制gcc編譯器假設RAM所有記憶體單元均被彙編指令修改,這樣cpu中的registers和cache中已緩存的記憶體單元中的資料將作廢。cpu將不得不在需要的時候重新讀取記憶體中的資料。這就阻止了cpu又將registers,cache中的資料用於去優化指令,而避免去訪問記憶體。”
以下內容源自:http://hi.baidu.com/hilyjiang/blog/item/7db5077a8180dbec2e73b380.html
__asm__ __volatile__ GCC的內嵌彙編語法 AT&T組合語言語法(一)
開發一個OS,儘管絕大部分代碼只需要用C/C++等高階語言就可以了,但至少和硬體相關部分的代碼需要使用組合語言,另外,由於啟動部分的代碼有大小限制,使用精練的彙編可以縮小目標代碼的Size。另外,對於某些需要被經常調用的代碼,使用彙編來寫可以提高性能。所以我們必須瞭解組合語言,即使你有可能並不喜歡它。
如果你是電腦專業的話,在大學裏你應該學習過Intel格式的8086/80386彙編,這裏就不再討論。如果我們選擇的OS開發工具是GCC以及GAS的話,就必須瞭解AT&T組合語言語法,因為GCC/GAS只支援這種彙編語法。
本書不會去討論8086/80386的彙編編程,這類的書籍很多,你可以參考它們。這裏只會討論AT&T的彙編語法,以及GCC的內嵌彙編語法。
--------------------------------------------------------------------------------
0.3.2 Syntax
1.寄存器引用
引用寄存器要在寄存器號前加百分號%,如“movl %eax, %ebx”。
80386有如下寄存器:
8個32-bit寄存器 %eax,%ebx,%ecx,%edx,%edi,%esi,%ebp,%esp;
8個16-bit寄存器,它們事實上是上面8個32-bit寄存器的低16位:%ax,%bx,%cx,%dx,%di,%si,%bp,%sp;
8個8-bit寄存器:%ah,%al,%bh,%bl,%ch,%cl,%dh,%dl。它們事實上是寄存器%ax,%bx,%cx,%dx的高8位和低8位;
6個段寄存器:%cs(code),%ds(data),%ss(stack), %es,%fs,%gs;
3個控制寄存器:%cr0,%cr2,%cr3;
6個debug寄存器:%db0,%db1,%db2,%db3,%db6,%db7;
2個測試寄存器:%tr6,%tr7;
8個浮點寄存器棧:%st(0),%st(1),%st(2),%st(3),%st(4),%st(5),%st(6),%st(7)。
2. 運算元順序
運算元排列是從源(左)到目的(右),如“movl %eax(源), %ebx(目的)”
3. 立即數
使用立即數,要在數前面加符號$, 如“movl $0x04, %ebx”
或者:
para = 0x04
movl $para, %ebx
指令執行的結果是將立即數04h裝入寄存器ebx。
4. 符號常數
符號常數直接引用如
value: .long 0x12a3f2de

movl value , %ebx
指令執行的結果是將常數0x12a3f2de裝入寄存器ebx。
引用符號位址在符號前加符號$, 如“movl $value, % ebx”則是將符號value的位址裝入寄存器ebx。
5. 運算元的長度
運算元的長度用加在指令後的符號表示b(byte, 8-bit), w(word, 16-bits), l(long, 32-bits),如“movb %al, %bl”,“movw %ax, %bx”,“movl %eax, %ebx ”。
如 果沒有指定運算元長度的話,編譯器將按照目標運算元的長度來設置。比如指令“mov %ax, %bx”,由於目標運算元bx的長度為word,那麼編譯器將把此指令等同於“movw %ax, %bx”。同樣道理,指令“mov $4, %ebx”等同於指令“movl $4, %ebx”,“push %al”等同於“pushb %al”。對於沒有指定運算元長度,但編譯器又無法猜測的指令,編譯器將會報錯,比如指令“push $4”。
6. 符號擴展和零擴展指令
絕大多數面向80386的AT&T彙編指令與Intel格式的彙編指令都是相同的,符號擴展指令和零擴展指令則是僅有的不同格式指令。
符號擴展指令和零擴展指令需要指定源運算元長度和目的運算元長度,即使在某些指令中這些運算元是隱含的。
在AT& T語法中,符號擴展和零擴展指令的格式為,基本部分"movs"和"movz"(對應Intel語法的movsx和movzx),後面跟上源運算元長度和 目的運算元長度。movsbl意味著movs (from)byte (to)long;movbw意味著movs (from)byte (to)word;movswl意味著movs (from)word (to)long。對於movz指令也一樣。比如指令“movsbl %al, %edx”意味著將al寄存器的內容進行符號擴展後放置到edx寄存器中。
其他的Intel格式的符號擴展指令還有:
cbw -- sign-extend byte in %al to word in %ax;
cwde -- sign-extend word in %ax to long in %eax;
cwd -- sign-extend word in %ax to long in %dx:%ax;
cdq -- sign-extend dword in %eax to quad in %edx:%eax;
對應的AT&T語法的指令為cbtw,cwtl,cwtd,cltd。
7. 調用和跳轉指令
段內調用和跳轉指令為"call","ret"和"jmp",段間調用和跳轉指令為"lcall","lret"和"ljmp"。
段間調用和跳轉指令的格式為“lcall/ljmp $SECTION, $OFFSET”,而段間返回指令則為“lret $STACK-ADJUST”。
8. 首碼
操作碼首碼被用在下列的情況:
字串重複操作指令(rep,repne);
指定被操作的段(cs,ds,ss,es,fs,gs);
進行匯流排加鎖(lock);
指定位址和操作的大小(data16,addr16);
在AT&T彙編語法中,操作碼首碼通常被單獨放在一行,後面不跟任何運算元。例如,對於重複scas指令,其寫法為:
repne
scas
上述操作碼首碼的意義和用法如下:
指定被操作的段首碼為cs,ds,ss,es,fs,和gs。在AT&T語法中,只需要按照section:memory-operand的格式就指定了相應的段首碼。比如:lcall %cs:realmode_swtch
運算元/位址大小首碼是“data16”和"addr16",它們被用來在32-bit運算元/地址代碼中指定16-bit的運算元/地址。
總 線加鎖首碼“lock”,它是為了在多處理器環境中,保證在當前指令執行期間禁止一切中斷。這個首碼僅僅對ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG,DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD,XCHG指令有效,如果將Lock首碼用在其他指令之前,將會引起異常。
字串重複操作首碼"rep","repe","repne"用來讓字串操作重複“%ecx”次。
9. 記憶體引用
Intel語法的間接記憶體引用的格式為:
section:[base+index*scale+displacement]
而在AT&T語法中對應的形式為:
section:displacement(base,index,scale)
其 中,base和index是任意的32-bit base和index寄存器。scale可以取值1,2,4,8。如果不指定scale值,則預設值為1。section可以指定任意的段寄存器作為段首碼,默認的段寄存器在不同的情況下不一樣。如果你在指令中指定了默認的段首碼,則編譯器在目標代碼中不會產生此段首碼代碼。
下面是一些例子:
-4(%ebp):base=%ebp,displacement=-4,section沒有指定,由於base=%ebp,所以默認的section=%ss,index,scale沒有指定,則index為0。
foo(,%eax,4):index=%eax,scale=4,displacement=foo。其他域沒有指定。這裏默認的section=%ds。
foo(,1):這個運算式引用的是指標foo指向的位址所存放的值。注意這個運算式中沒有base和index,並且只有一個逗號,這是一種異常語法,但卻合法。
%gs:foo:這個運算式引用的是放置于%gs段裏變數foo的值。
如果call和jump操作在運算元前指定首碼“*”,則表示是一個絕對位址調用/跳轉,也就是說jmp/call指令指定的是一個絕對位址。如果沒有指定"*",則運算元是一個相對位址。
任何指令如果其運算元是一個記憶體操作,則指令必須指定它的操作尺寸(byte,word,long),也就是說必須帶有指令尾碼(b,w,l)。
.3 GCC Inline ASM
GCC 支持在C/C++代碼中嵌入彙編代碼,這些彙編代碼被稱作GCC Inline ASM——GCC內聯彙編。這是一個非常有用的功能,有利於我們將一些C/C++語法無法表達的指令直接潛入C/C++代碼中,另外也允許我們直接寫 C/C++代碼中使用彙編編寫簡潔高效的代碼。
1.基本內聯彙編
GCC中基本的內聯彙編非常易懂,我們先來看兩個簡單的例子:
__asm__("movl %esp,%eax"); // 看起來很熟悉吧
或者是
__asm__("
movl $1,%eax // SYS_exit
xor %ebx,%ebx
int $0x80
");
或
__asm__(
"movl $1,%eax\r\t" \
"xor %ebx,%ebx\r\t" \
"int $0x80" \
);
基本內聯彙編的格式是
__asm__ __volatile__("Instruction List");
1、__asm__
__asm__是GCC關鍵字asm的巨集定義:
#define __asm__ asm
__asm__或asm用來聲明一個內聯彙編運算式,所以任何一個內聯彙編運算式都是以它開頭的,是必不可少的。
2、Instruction List
Instruction List是彙編指令序列。它可以是空的,比如:__asm__ __volatile__(""); 或__asm__ ("");都是完全合法的內聯彙編運算式,只不過這兩條語句沒有什麼意義。但並非所有Instruction List為空的內聯彙編運算式都是沒有意義的,比如:__asm__ ("":::"memory"); 就非常有意義,它向GCC聲明:“我對記憶體作了改動”,GCC在編譯的時候,會將此因素考慮進去。
我們看一看下面這個例子:
$ cat example1.c
int main(int __argc, char* __argv[])
{
int* __p = (int*)__argc;
(*__p) = 9999;
//__asm__("":::"memory");
if((*__p) == 9999)
return 5;
return (*__p);
}
在 這段代碼中,那條內聯彙編是被注釋掉的。在這條內聯彙編之前,記憶體指標__p所指向的記憶體被賦值為9999,隨即在內聯彙編之後,一條if語句判斷__p 所指向的記憶體與9999是否相等。很明顯,它們是相等的。GCC在優化編譯的時候能夠很聰明的發現這一點。我們使用下面的命令行對其進行編譯:
$ gcc -O -S example1.c
選項-O表示優化編譯,我們還可以指定優化等級,比如-O2表示優化等級為2;選項-S表示將C/C++原始檔案編譯為彙編檔,檔案名和C/C++文件一樣,只不過副檔名由.c變為.s。
我們來查看一下被放在example1.s中的編譯結果,我們這裏僅僅列出了使用gcc 2.96在redhat 7.3上編譯後的相關函數部分彙編代碼。為了保持清晰性,無關的其他代碼未被列出。
$ cat example1.s

main:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax # int* __p = (int*)__argc
movl $9999, (%eax) # (*__p) = 9999
movl $5, %eax # return 5
popl %ebp
ret

參 照一下C源碼和編譯出的彙編代碼,我們會發現彙編代碼中,沒有if語句相關的代碼,而是在賦值語句(*__p)=9999後直接return 5;這是因為GCC認為在(*__p)被賦值之後,在if語句之前沒有任何改變(*__p)內容的操作,所以那條if語句的判斷條件(*__p) == 9999肯定是為true的,所以GCC就不再生成相關代碼,而是直接根據為true的條件生成return 5的彙編代碼(GCC使用eax作為保存返回值的寄存器)。
我們現在將example1.c中內聯彙編的注釋去掉,重新編譯,然後看一下相關的編譯結果。

$ gcc -O -S example1.c

$ cat example1.s

main:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax # int* __p = (int*)__argc
movl $9999, (%eax) # (*__p) = 9999
#APP

# __asm__("":::"memory")
#NO_APP
cmpl $9999, (%eax) # (*__p) == 9999 ?
jne .L3 # false
movl $5, %eax # true, return 5
jmp .L2
.p2align 2
.L3:
movl (%eax), %eax
.L2:
popl %ebp
ret

由於內聯彙編語句__asm__("":::"memory")向GCC聲明,在此內聯彙編語句出現的位置記憶體內容可能了改變,所以GCC在編譯時就不能像剛才那樣處理。這次,GCC老老實實的將if語句生成了彙編代碼。
可能有人會質疑:為什麼要使用__asm__("":::"memory")向GCC聲明記憶體發生了變化?明明“Instruction List”是空的,沒有任何對記憶體的操作,這樣做只會增加GCC生成彙編代碼的數量。
確 實,那條內聯彙編語句沒有對記憶體作任何操作,事實上它確實什麼都沒有做。但影響記憶體內容的不僅僅是你當前正在運行的程式。比如,如果你現在正在操作的記憶體是一塊記憶體映射,映射的內容是週邊I/O設備寄存器。那麼操作這塊記憶體的就不僅僅是當前的程式,I/O設備也會去操作這塊記憶體。既然兩者都會去操作同一塊 記憶體,那麼任何一方在任何時候都不能對這塊記憶體的內容想當然。所以當你使用高階語言C/C++寫這類程式的時候,你必須讓編譯器也能夠明白這一點,畢竟高 級語言最終要被編譯為彙編代碼。

你可能已經注意到了,這次輸出的彙編結果中,有兩個符號:#APP和#NO_APP,GCC將內聯彙編語句中"Instruction List"所列出的指令放在#APP和#NO_APP之間,由於__asm__("":::"memory")中“Instruction List”為空,所以#APP和#NO_APP中間也沒有任何內容。但我們以後的例子會更加清楚的表現這一點。
關於為什麼內聯彙編__asm__("":::"memory")是一條聲明記憶體改變的語句,我們後面會詳細討論。
剛才我們花了大量的內容來討論"Instruction List"為空是的情況,但在實際的編程中,"Instruction List"絕大多數情況下都不是空的。它可以有1條或任意多條彙編指令。
當 在"Instruction List"中有多條指令的時候,你可以在一對引號中列出全部指令,也可以將一條或幾條指令放在一對引號中,所有指令放在多對引號中。如果是前者,你可以將每一條指令放在一行,如果要將多條指令放在一行,則必須用分號(;)或換行符(\n,大多數情況下\n後還要跟一個\t,其中\n是為了換行,\t是為了 空出一個tab寬度的空格)將它們分開。比如:
__asm__("movl %eax, %ebx
sti
popl %edi
subl %ecx, %ebx");

__asm__("movl %eax, %ebx; sti
popl %edi; subl %ecx, %ebx");

__asm__("movl %eax, %ebx; sti\n\t popl %edi
subl %ecx, %ebx");

都是合法的寫法。如果你將指令放在多對引號中,則除了最後一對引號之外,前面的所有引號裏的最後一條指令之後都要有一個分號(;)或(\n)或(\n\t)。比如:
__asm__("movl %eax, %ebx
sti\n"
"popl %edi;"
"subl %ecx, %ebx");
__asm__("movl %eax, %ebx; sti\n\t"
"popl %edi; subl %ecx, %ebx");
__asm__("movl %eax, %ebx; sti\n\t popl %edi\n"
"subl %ecx, %ebx");
__asm__("movl %eax, %ebx; sti\n\t popl %edi;" "subl %ecx, %ebx");
都是合法的。
上述原則可以歸結為:
任意兩個指令間要麼被分號(;)分開,要麼被放在兩行;
放在兩行的方法既可以從通過\n的方法來實現,也可以真正的放在兩行;
可以使用1對或多對引號,每1對引號裏可以放任一多條指令,所有的指令都要被放到引號中。
在基本內聯彙編中,“Instruction List”的書寫的格式和你直接在彙編檔中寫非內聯彙編沒有什麼不同,你可以在其中定義Label,定義對齊(.align n ),定義段(.section name )。例如:
__asm__(".align 2\n\t"
"movl %eax, %ebx\n\t"
"test %ebx, %ecx\n\t"
"jne error\n\t"
"sti\n\t"
"error: popl %edi\n\t"
"subl %ecx, %ebx");
上面例子的格式是Linux內聯代碼常用的格式,非常整齊。也建議大家都使用這種格式來寫內聯彙編代碼。
3、__volatile__

__volatile__是GCC關鍵字volatile的巨集定義:

#define __volatile__ volatile

__volatile__ 或volatile是可選的,你可以用它也可以不用它。如果你用了它,則是向GCC聲明“不要動我所寫的Instruction List,我需要原封不動的保留每一條指令”,否則當你使用了優化選項(-O)進行編譯時,GCC將會根據自己的判斷決定是否將這個內聯彙編運算式中的指 令優化掉。

那麼GCC判斷的原則是什麼?我不知道(如果有哪位朋友清楚的話,請告訴我)。我試驗了一下,發現一條內聯彙編語句如果是基本 內聯彙編的話(即只有“Instruction List”,沒有Input/Output/Clobber的內聯彙編,我們後面將會討論這一點),無論你是否使用__volatile__來修飾, GCC 2.96在優化編譯時,都會原封不動的保留內聯彙編中的“Instruction List”。但或許我的試驗的例子並不充分,所以這一點並不能夠得到保證。

為了保險起見,如果你不想讓GCC的優化影響你的內聯彙編代碼,你最好在前面都加上__volatile__,而不要依賴於編譯器的原則,因為即使你非常瞭解當前編譯器的優化原則,你也無法保證這種原則將來不會發生變化。而__volatile__的含義卻是恒定的。

2、帶有C/C++運算式的內聯彙編

GCC允許你通過C/C++運算式指定內聯彙編中"Instrcuction List"中指令的輸入和輸出,你甚至可以不關心到底使用哪個寄存器被使用,完全靠GCC來安排和指定。這一點可以讓程式師避免去考慮有限的寄存器的使用,也可以提高目標代碼的效率。
我們先來看幾個例子:

__asm__ (" " : : : "memory" ); // 前面提到的

__asm__ ("mov %%eax, %%ebx" : "=b"(rv) : "a"(foo) : "eax", "ebx");

__asm__ __volatile__("lidt %0": "=m" (idt_descr));

__asm__("subl %2,%0\n\t"
"sbbl %3,%1"
: "=a" (endlow), "=d" (endhigh)
: "g" (startlow), "g" (starthigh), "0" (endlow), "1" (endhigh));

怎麼樣,有點印象了吧,是不是也有點暈?沒關係,下面討論完之後你就不會再暈了。(當然,也有可能更暈^_^)。討論開始——
帶有C/C++運算式的內聯彙編格式為:
__asm__ __volatile__("Instruction List" : Output : Input : Clobber/Modify);
從中我們可以看出它和基本內聯彙編的不同之處在於:它多了3個部分(Input,Output,Clobber/Modify)。在括弧中的4個部分通過冒號(:)分開。
這4個部分都不是必須的,任何一個部分都可以為空,其規則為:
如 果Clobber/Modify為空,則其前面的冒號(:)必須省略。比如__asm__("mov %%eax, %%ebx" : "=b"(foo) : "a"(inp) : )就是非法的寫法;而__asm__("mov %%eax, %%ebx" : "=b"(foo) : "a"(inp) )則是正確的。
如果Instruction List為空,則Input,Output,Clobber/Modify可以不為空,也可以為空。比如__asm__ ( " " : : : "memory" );和__asm__(" " : : );都是合法的寫法。
如 果Output,Input,Clobber/Modify都為空,Output,Input之前的冒號(:)既可以省略,也可以不省略。如果都省略,則此彙編退化為一個基本內聯彙編,否則,仍然是一個帶有C/C++運算式的內聯彙編,此時"Instruction List"中的寄存器寫法要遵守相關規定,比如寄存器前必須使用兩個百分號(%%),而不是像基本彙編格式一樣在寄存器前只使用一個百分號(%)。比如 __asm__( " mov %%eax, %%ebx" : : );__asm__( " mov %%eax, %%ebx" : )和__asm__( " mov %eax, %ebx" )都是正確的寫法,而__asm__( " mov %eax, %ebx" : : );__asm__( " mov %eax, %ebx" : )和__asm__( " mov %%eax, %%ebx" )都是錯誤的寫法。
如果Input,Clobber/Modify為空,但Output不為空,Input前的冒號(:)既可以省略,也可以不省略。比如 __asm__( " mov %%eax, %%ebx" : "=b"(foo) : );__asm__( " mov %%eax, %%ebx" : "=b"(foo) )都是正確的。
如果後面的部分不為空,而前面的部分為空,則前面的冒號(:)都必須保留,否則無法說 明不為空的部分究竟是第幾部分。比如, Clobber/Modify,Output為空,而Input不為空,則Clobber/Modify前的冒號必須省略(前面的規則),而Output 前的冒號必須為保留。如果Clobber/Modify不為空,而Input和Output都為空,則Input和Output前的冒號都必須保留。比如 __asm__( " mov %%eax, %%ebx" : : "a"(foo) )和__asm__( " mov %%eax, %%ebx" : : : "ebx" )。
從上面的規則可以看到另外一個事實,區分一個內聯彙編是基本格式的還是帶有C/C++運算式格式的,其規則在於在"Instruction List"後是否有冒號(:)的存在,如果沒有則是基本格式的,否則,則是帶有C/C++運算式格式的。
兩種格式對寄存器語法的要求不同:基本格式要求寄存器前只能使用一個百分號(%),這一點和非內聯彙編相同;而帶有C/C++運算式格式則要求寄存器前必須使用兩個百分號(%%),其原因我們會在後面討論。
1. Output
Output用來指定當前內聯彙編語句的輸出。我們看一看這個例子:
__asm__("movl %%cr0, %0": "=a" (cr0));
這 個內聯彙編語句的輸出部分為"=r"(cr0),它是一個“操作運算式”,指定了一個輸出操作。我們可以很清楚得看到這個輸出操作由兩部分組成:括弧括住 的部分(cr0)和引號引住的部分"=a"。這兩部分都是每一個輸出操作必不可少的。括弧括住的部分是一個C/C++運算式,用來保存內聯彙編的一個輸出值,其操作就等於C/C++的相等賦值cr0 = output_value,因此,括弧中的輸出運算式只能是C/C++的左值運算式,也就是說它只能是一個可以合法的放在C/C++賦值操作中等號(=) 左邊的運算式。那麼右值output_value從何而來呢?
答案是引號中的內容,被稱作“操作約束”(Operation Constraint),在這個例子中操作約束為"=a",它包含兩個約束:等號(=)和字母a,其中等號(=)說明括弧中左值運算式cr0是一個 Write-Only的,只能夠被作為當前內聯彙編的輸入,而不能作為輸入。而字母a是寄存器EAX / AX / AL的簡寫,說明cr0的值要從eax寄存器中獲取,也就是說cr0 = eax,最終這一點被轉化成彙編指令就是movl %eax, address_of_cr0。現在你應該清楚了吧,操作約束中會給出:到底從哪個寄存器傳遞值給cr0。
另外,需要特別說明的是,很多文檔都聲明,所有輸出操作的操作約束必須包含一個等號(=),但GCC的文檔中卻很清楚的聲明,並非如此。因為等號(=)約束說明當前的運算式是一個 Write-Only的,但另外還有一個符號——加號(+)用來說明當前運算式是一個Read-Write的,如果一個操作約束中沒有給出這兩個符號中的任何一個,則說明當前運算式是Read-Only的。因為對於輸出操作來說,肯定是必須是可寫的,而等號(=)和加號(+)都表示可寫,只不過加號(+) 同時也表示是可讀的。所以對於一個輸出操作來說,其操作約束只需要有等號(=)或加號(+)中的任意一個就可以了。
二者的區別是:等號(=)表示當前操作運算式指定了一個純粹的輸出操作,而加號(+)則表示當前操作運算式不僅僅只是一個輸出操作還是一個輸入操作。但無論是等號(=)約束還是加號(+)約束所約束的操作運算式都只能放在Output域中,而不能被用在Input域中。
另外,有些文檔聲明:儘管GCC文檔中提供了加號(+)約束,但在實際的編譯中通不過;我不知道老版本會怎麼樣,我在GCC 2.96中對加號(+)約束的使用非常正常。
我們通過一個例子看一下,在一個輸出操作中使用等號(=)約束和加號(+)約束的不同。
$ cat example2.c
int main(int __argc, char* __argv[])
{
int cr0 = 5;

__asm__ __volatile__("movl %%cr0, %0":"=a" (cr0));

return 0;
}

$ gcc -S example2.c

$ cat example2.s

main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $5, -4(%ebp) # cr0 = 5
#APP
movl %cr0, %eax
#NO_APP
movl %eax, %eax
movl %eax, -4(%ebp) # cr0 = %eax
movl $0, %eax
leave
ret

這個例子是使用等號(=)約束的情況,變數cr0被放在記憶體-4(%ebp)的位置,所以指令mov %eax, -4(%ebp)即表示將%eax的內容輸出到變數cr0中。
下面是使用加號(+)約束的情況:
$ cat example3.c
int main(int __argc, char* __argv[])
{
int cr0 = 5;
__asm__ __volatile__("movl %%cr0, %0" : "+a" (cr0));
return 0;
}
$ gcc -S example3.c
$ cat example3.s

main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $5, -4(%ebp) # cr0 = 5
movl -4(%ebp), %eax # input ( %eax = cr0 )
#APP
movl %cr0, %eax
#NO_APP
movl %eax, -4(%ebp) # output (cr0 = %eax )
movl $0, %eax
leave
ret


從編譯的結果可以看出,當使用加號(+)約束的時候,cr0不僅作為輸出,還作為輸入,所使用寄存器都是寄存器約束(字母a,表示使用eax寄存器)指定的。關於寄存器約束我們後面討論。
在Output域中可以有多個輸出操作運算式,多個操作運算式中間必須用逗號(,)分開。例如:

__asm__(
"movl %%eax, %0 \n\t"
"pushl %%ebx \n\t"
"popl %1 \n\t"
"movl %1, %2"
: "+a"(cr0), "=b"(cr1), "=c"(cr2));

2、Input

Input域的內容用來指定當前內聯彙編語句的輸入。我們看一看這個例子:

__asm__("movl %0, %%db7" : : "a" (cpu->db7));

例中Input域的內容為一個運算式"a"[cpu->db7),被稱作“輸入運算式”,用來表示一個對當前內聯彙編的輸入。
像輸出運算式一樣,一個輸入運算式也分為兩部分:帶括弧的部分(cpu->db7)和帶引號的部分"a"。這兩部分對於一個內聯彙編輸入運算式來說也是必不可少的。
括 號中的運算式cpu->db7是一個C/C++語言的運算式,它不必是一個左值運算式,也就是說它不僅可以是放在C/C++賦值操作左邊的運算式, 還可以是放在C/C++賦值操作右邊的運算式。所以它可以是一個變數,一個數位,還可以是一個複雜的運算式(比如a+b/c*d)。比如上例可以改為: __asm__("movl %0, %%db7" : : "a" (foo)),__asm__("movl %0, %%db7" : : "a" (0x1000))或__asm__("movl %0, %%db7" : : "a" (va*vb/vc))。
引號號中的部分是約束部分,和輸出運算式約束不同的是,它不允許指定加號(+)約束和等號(=)約束,也就是說它只能是默認的Read-Only的。約束中必須指定 一個寄存器約束,例中的字母a表示當前輸入變數cpu->db7要通過寄存器eax輸入到當前內聯彙編中。
我們看一個例子:

$ cat example4.c

int main(int __argc, char* __argv[])
{
int cr0 = 5;

__asm__ __volatile__("movl %0, %%cr0"::"a" (cr0));

return 0;
}

$ gcc -S example4.c

$ cat example4.s

main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $5, -4(%ebp) # cr0 = 5
movl -4(%ebp), %eax # %eax = cr0
#APP
movl %eax, %cr0
#NO_APP
movl $0, %eax
leave
ret

我們從編譯出的彙編代碼可以看到,在"Instruction List"之前,GCC按照我們的輸入約束"a",將變數cr0的內容裝入了eax寄存器。

3. Operation Constraint

每一個Input和Output運算式都必須指定自己的操作約束Operation Constraint,我們這裏來討論在80386平臺上所可能使用的操作約束。

1、寄存器約束

當你當前的輸入或輸入需要借助一個寄存器時,你需要為其指定一個寄存器約束。你可以直接指定一個寄存器的名字,比如:

__asm__ __volatile__("movl %0, %%cr0"::"eax" (cr0));

也可以指定一個縮寫,比如:

__asm__ __volatile__("movl %0, %%cr0"::"a" (cr0));

如果你指定一個縮寫,比如字母a,則GCC將會根據當前操作運算式中C/C++運算式的寬度決定使用%eax,還是%ax或%al。比如:

unsigned short __shrt;

__asm__ ("mov %0,%%bx" : : "a"(__shrt));

由於變數__shrt是16-bit short類型,則編譯出來的彙編代碼中,則會讓此變數使用%ex寄存器。編譯結果為:

movw -2(%ebp), %ax # %ax = __shrt
#APP
movl %ax, %bx
#NO_APP

無論是Input,還是Output操作運算式約束,都可以使用寄存器約束。

下表中列出了常用的寄存器約束的縮寫。

約束 Input/Output 意義
r I,O 表示使用一個通用寄存器,由GCC在%eax/%ax/%al, %ebx/%bx/%bl, %ecx/%cx/%cl, %edx/%dx/%dl中選取一個GCC認為合適的。
q I,O 表示使用一個通用寄存器,和r的意義相同。
a I,O 表示使用%eax / %ax / %al
b I,O 表示使用%ebx / %bx / %bl
c I,O 表示使用%ecx / %cx / %cl
d I,O 表示使用%edx / %dx / %dl
D I,O 表示使用%edi / %di
S I,O 表示使用%esi / %si
f I,O 表示使用浮點寄存器
t I,O 表示使用第一個浮點寄存器
u I,O 表示使用第二個浮點寄存器

2、記憶體約束
如果一個Input/Output操作運算式的C/C++運算式表現為一個記憶體位址,不想借助於任何寄存器,則可以使用記憶體約束。比如:

__asm__ ("lidt %0" : "=m"(__idt_addr)); 或 __asm__ ("lidt %0" : :"m"(__idt_addr));

我們看一下它們分別被放在一個C原始檔案中,然後被GCC編譯後的結果:

$ cat example5.c

// 本例中,變數sh被作為一個記憶體輸入

int main(int __argc, char* __argv[])
{
char* sh = (char*)&__argc;

__asm__ __volatile__("lidt %0" : : "m" (sh));

return 0;
}

$ gcc -S example5.c

$ cat example5.s

main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
leal 8(%ebp), %eax
movl %eax, -4(%ebp) # sh = (char*) &__argc
#APP
lidt -4(%ebp)
#NO_APP
movl $0, %eax
leave
ret

$ cat example6.c

// 本例中,變數sh被作為一個記憶體輸出

int main(int __argc, char* __argv[])
{
char* sh = (char*)&__argc;

__asm__ __volatile__("lidt %0" : "=m" (sh));

return 0;
}

$ gcc -S example6.c

$ cat example6.s

main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
leal 8(%ebp), %eax
movl %eax, -4(%ebp) # sh = (char*) &__argc
#APP
lidt -4(%ebp)
#NO_APP
movl $0, %eax
leave
ret
首先,你會注意到,在這兩個例子中,變數sh沒有借助任何寄存器,而是直接參與了指令lidt的操作。

其次,通過仔細觀察,你會發現一個驚人的事實,兩個例子編譯出來的彙編代碼是一樣的!雖然,一個例子中變數sh作為輸入,而另一個例子中變數sh作為輸出。這是怎麼回事?

原來,使用記憶體方式進行輸入輸出時,由於不借助寄存器,所以GCC不會按照你的聲明對其作任何的輸入輸出處理。GCC只會直接拿來用,究竟對這個C/C++運算式而言是輸入還是輸出,完全依賴與你寫在"Instruction List"中的指令對其操作的指令。
由 於上例中,對其操作的指令為lidt,lidt指令的運算元是一個輸入型的運算元,所以事實上對變數sh的操作是一個輸入操作,即使你把它放在 Output域也不會改變這一點。所以,對此例而言,完全符合語意的寫法應該是將sh放在Input域,儘管放在Output域也會有正確的執行結果。

所 以,對於記憶體約束類型的操作運算式而言,放在Input域還是放在Output域,對編譯結果是沒有任何影響的,因為本來我們將一個操作運算式放在 Input域或放在Output域是希望GCC能為我們自動通過寄存器將運算式的值輸入或輸出。既然對於記憶體約束類型的操作運算式來說,GCC不會自動為 它做任何事情,那麼放在哪兒也就無所謂了。但從程式師的角度而言,為了增強代碼的可讀性,最好能夠把它放在符合實際情況的地方。

約束 Input/Output 意義
m I,O 表示使用系統所支援的任何一種記憶體方式,不需要借助寄存器

__asm__ __volatile__ GCC的內嵌彙編語法 AT&T組合語言語法(二)
3、立即數約束

如果一個Input/Output操作運算式的C/C++運算式是一個數字常數,不想借助於任何寄存器,則可以使用立即數約束。

由於立即數在C/C++中只能作為右值,所以對於使用立即數約束的運算式而言,只能放在Input域。

比如:__asm__ __volatile__("movl %0, %%eax" : : "i" (100) );

立即數約束很簡單,也很容易理解,我們在這裏就不再贅述。

約束 Input/Output 意義
i I 表示輸入運算式是一個立即數(整數),不需要借助任何寄存器
F I 表示輸入運算式是一個立即數(浮點數),不需要借助任何寄存器


4、通用約束

約束 Input/Output 意義
g I,O 表示可以使用通用寄存器,記憶體,立即數等任何一種處理方式。
0,1,2,3,4,5,6,7,8,9 I 表示和第n個操作運算式使用相同的寄存器/記憶體。
通用約束g是一個非常靈活的約束,當程式師認為一個C/C++運算式在實際的操作中,究竟使用寄存器方式,還是使用記憶體方式或立即數方式並無所謂時,或者程式師想實現一個靈活的範本,讓GCC可以根據不同的C/C++運算式生成不同的訪問方式時,就可以使用通用約束g。比如:

#define JUST_MOV(foo) __asm__ ("movl %0, %%eax" : : "g"(foo))

JUST_MOV(100)和JUST_MOV(var)則會讓編譯器產生不同的代碼。

int main(int __argc, char* __argv[])
{
JUST_MOV(100);

return 0;
}

編譯後生成的代碼為:

main:
pushl %ebp
movl %esp, %ebp
#APP
movl $100, %eax
#NO_APP
movl $0, %eax
popl %ebp
ret

很明顯這是立即數方式。而下一個例子:

int main(int __argc, char* __argv[])
{
JUST_MOV(__argc);

return 0;
}

經編譯後生成的代碼為:

main:
pushl %ebp
movl %esp, %ebp
#APP
movl 8(%ebp), %eax
#NO_APP
movl $0, %eax
popl %ebp
ret
這個例子是使用記憶體方式。

一個帶有C/C++運算式的內聯彙編,其操作運算式被按照被列出的順序編號,第一個是0,第2個是1,依次類推,GCC最多允許有10個操作運算式。比如:
__asm__ ("popl %0 \n\t"
"movl %1, %%esi \n\t"
"movl %2, %%edi \n\t"
: "=a"(__out)
: "r" (__in1), "r" (__in2));

此例中,__out所在的Output操作運算式被編號為0,"r"(__in1)被編號為1,"r"(__in2)被編號為2。

再如:

__asm__ ("movl %%eax, %%ebx" : : "a"(__in1), "b"(__in2));

此例中,"a"(__in1)被編號為0,"b"(__in2)被編號為1。

如果某個Input操作運算式使用數字0到9中的一個數字(假設為1)作為它的操作約束,則等於向GCC聲明:“我要使用和編號為1的Output操作表達式相同的寄存器(如果Output操作運算式1使用的是寄存器),或相同的記憶體位址(如果Output操作運算式1使用的是記憶體)”。上面的描述包含兩個限定:數位0到數位9作為操作約束只能用在Input操作運算式中,被指定的操作運算式(比如某個Input操作運算式使用數位1作為約束,那麼被指定的 就是編號為1的操作運算式)只能是Output操作運算式。

由於GCC規定最多只能有10個Input/Output操作運算式,所以事實上數字9作為操作約束永遠也用不到,因為Output操作運算式排在Input操作運算式的前面,那麼如果有一個Input操作運算式指定了數字9作為 操作約束的話,那麼說明Output操作運算式的數量已經至少為10個了,那麼再加上這個Input操作運算式,則至少為11個了,以及超出GCC的限 制。

5、Modifier Characters(修飾符)

等號(=)和加號(+)用於對Output操作運算式的修飾,一個Output操作運算式要麼被等號(=)修飾,要麼被加號(+)修飾,二者必居其一。使用等號(=)說明此Output操作運算式是Write- Only的,使用加號(+)說明此Output操作運算式是Read-Write的。它們必須被放在約束字串的第一個字母。比如"a="(foo)是非 法的,而"+g"(foo)則是合法的。

當使用加號(+)的時候,此Output運算式等價於使用等號(=)約束加上一個Input運算式。比如

__asm__ ("movl %0, %%eax; addl %%eax, %0" : "+b"(foo)) 等價於

__asm__ ("movl %1, %%eax; addl %%eax, %0" : "=b"(foo) : "b"(foo))

但如果使用後一種寫法,"Instruction List"中的別名也要相應的改動。關於別名,我們後面會討論。

像等號(=)和加號(+)修飾符一樣,符號(&)也只能用於對Output操作運算式的修飾。當使用它進行修飾時,等於向GCC聲明:"GCC不得 為任何Input操作運算式分配與此Output操作運算式相同的寄存器"。其原因是&修飾符意味著被其修飾的Output操作運算式要在所有的 Input操作運算式被輸入前輸出。我們看下面這個例子:

int main(int __argc, char* __argv[])
{
int __in1 = 8, __in2 = 4, __out = 3;

__asm__ ("popl %0 \n\t"
"movl %1, %%esi \n\t"
"movl %2, %%edi \n\t"
: "=a"(__out)
: "r" (__in1), "r" (__in2));

return 0;
}
此例中,%0對應的就是Output操作運算式,它被指定的寄存器是%eax,整個Instruction List的第一條指令popl %0,編譯後就成為popl %eax,這時%eax的內容已經被修改,隨後在Instruction List後,GCC會通過movl %eax, address_of_out這條指令將%eax的內容放置到Output變數__out中。對於本例中的兩個Input操作運算式而言,它們的寄存器約 束為"r",即要求GCC為其指定合適的寄存器,然後在Instruction List之前將__in1和__in2的內容放入被選出的寄存器中,如果它們中的一個選擇了已經被__out指定的寄存器%eax,假如是__in1,那麼GCC在Instruction List之前會插入指令movl address_of_in1, %eax,那麼隨後popl %eax指令就修改了%eax的值,此時%eax中存放的已經不是Input變數__in1的值了,那麼隨後的movl %1, %%esi指令,將不會按照我們的本意——即將__in1的值放入%esi中——而是將__out的值放入%esi中了。
下面就是本例的編譯結果,很明顯,GCC為__in2選擇了和__out相同的寄存器%eax,這與我們的初衷不符。

main:
pushl %ebp
movl %esp, %ebp
subl $12, %esp
movl $8, -4(%ebp)
movl $4, -8(%ebp)
movl $3, -12(%ebp)
movl -4(%ebp), %edx # __in1使用寄存器%edx
movl -8(%ebp), %eax # __in2使用寄存器%eax
#APP
popl %eax
movl %edx, %esi
movl %eax, %edi

#NO_APP
movl %eax, %eax
movl %eax, -12(%ebp) # __out使用寄存器%eax
movl $0, %eax
leave
ret

為了避免這種情況,我們必須向GCC聲明這一點,要求GCC為所有的Input操作運算式指定別的寄存器,方法就是在Output操作運算式"=a" (__out)的操作約束中加入&約束,由於GCC規定等號(=)約束必須放在第一個,所以我們寫作"=&a"(__out)。
下面是我們將&約束加入之後編譯的結果:
main:
pushl %ebp
movl %esp, %ebp
subl $12, %esp
movl $8, -4(%ebp)
movl $4, -8(%ebp)
movl $3, -12(%ebp)
movl -4(%ebp), %edx #__in1使用寄存器%edx
movl -8(%ebp), %eax
movl %eax, %ecx # __in2使用寄存器%ecx
#APP
popl %eax
movl %edx, %esi
movl %ecx, %edi

#NO_APP
movl %eax, %eax
movl %eax, -12(%ebp) #__out使用寄存器%eax
movl $0, %eax
leave
ret

OK!這下好了,完全與我們的意圖吻合。
如 果一個Output操作運算式的寄存器約束被指定為某個寄存器,只有當至少存在一個Input操作運算式的寄存器約束為可選約束時,(可選約束的意思是可以從多個寄存器中選取一個,或使用非寄存器方式),比如"r"或"g"時,此Output操作運算式使用&修飾才有意義。如果你為所有的 Input操作運算式指定了固定的寄存器,或使用記憶體/立即數約束,則此Output操作運算式使用&修飾沒有任何意義。比如:

__asm__ ("popl %0 \n\t"
"movl %1, %%esi \n\t"
"movl %2, %%edi \n\t"
: "=&a"(__out)
: "m" (__in1), "c" (__in2));

此例中的Output操作運算式完全沒有必要使用&來修飾,因為__in1和__in2都被指定了固定的寄存器,或使用了記憶體方式,GCC無從選擇。
但如果你已經為某個Output操作運算式指定了&修飾,並指定了某個固定的寄存器,你就不能再為任何Input操作運算式指定這個寄存器,否則會出現編譯錯誤。比如:

__asm__ ("popl %0 \n\t"
"movl %1, %%esi \n\t"
"movl %2, %%edi \n\t"
: "=&a"(__out)
: "a" (__in1), "c" (__in2));

本例中,由於__out已經指定了寄存器%eax,同時使用了符號&修飾,則再為__in1指定寄存器%eax就是非法的。

反過來,你也可以為Output指定可選約束,比如"r","g"等,讓GCC為其選擇到底使用哪個寄存器,還是使用記憶體方式,GCC在選擇的時候,會首先排除掉已經被Input操作運算式使用的所有寄存器,然後在剩下的寄存器中選擇,或乾脆使用記憶體方式。比如:

__asm__ ("popl %0 \n\t"
"movl %1, %%esi \n\t"
"movl %2, %%edi \n\t"
: "=&r"(__out)
: "a" (__in1), "c" (__in2));

本例中,由於__out指定了約束"r",即讓GCC為其決定使用哪一格寄存器,而寄存器%eax和%ecx已經被__in1和__in2使用,那麼GCC在為__out選擇的時候,只會在%ebx和%edx中選擇。
前3 個修飾符只能用在Output操作運算式中,而百分號[%]修飾符恰恰相反,只能用在Input操作運算式中,用於向GCC聲明:“當前Input操作表達式中的C/C++運算式可以和下一個Input操作運算式中的C/C++運算式互換”。這個修飾符號一般用於符合交換律運算,比如加(+),乘(*), 與(&),或()等等。我們看一個例子:

int main(int __argc, char* __argv[])
{
int __in1 = 8, __in2 = 4, __out = 3;

__asm__ ("addl %1, %0\n\t"
: "=r"(__out)
: "%r" (__in1), "0" (__in2));

return 0;
}
在此例中,由於指令是一個加法運算,相當於等式__out = __in1 + __in2,而它與等式__out = __in2 + __in1沒有什麼不同。所以使用百分號修飾,讓GCC知道__in1和__in2可以互換,也就是說GCC可以自動將本例的內聯彙編改變為:

__asm__ ("addl %1, %0\n\t"
: "=r"(__out)
: "%r" (__in2), "0" (__in1));

修飾符 Input/Output 意義
= O 表示此Output操作運算式是Write-Only的
+ O 表示此Output操作運算式是Read-Write的
& O 表示此Output操作運算式獨佔為其指定的寄存器
% I 表示此Input操作運算式中的C/C++運算式可以和下一個Input操作運算式中的C/C++運算式互換

4. 占位符

什麼叫占位符?我們看一看下面這個例子:

__asm__ ("addl %1, %0\n\t"
: "=a"(__out)
: "m" (__in1), "a" (__in2));

這個例子中的%0和%1就是占位符。每一個占位符對應一個Input/Output操作運算式。我們在之前已經提到,GCC規定一個內聯彙編語句最多可以有 10個Input/Output操作運算式,然後按照它們被列出的順序依次賦予編號0到9。對於占位元符中的數字而言,和這些編號是對應的。

由於占位符前面使用一個百分號(%),為了區別占位符和寄存器,GCC規定在帶有C/C++運算式的內聯彙編中,"Instruction List"中直接寫出的寄存器前必須使用兩個百分號(%%)。
GCC對其進行編譯的時候,會將每一個占位符替換為對應的Input/Output操作運算式所指定的寄存器/記憶體位址/立即數。比如在上例中,占位符%0對應 Output操作運算式"=a"(__out),而"=a"(__out)指定的寄存器為%eax,所以把占位符%0替換為%eax,占位符%1對應 Input操作運算式"m"(__in1),而"m"(__in1)被指定為記憶體操作,所以把占位元符%1替換為變數__in1的記憶體位址。

也許有人認為,在上面這個例子中,完全可以不使用%0,而是直接寫%%eax,就像這樣:

__asm__ ("addl %1, %%eax\n\t"
: "=a"(__out)
: "m" (__in1), "a" (__in2));

和 上面使用占位符%0沒有什麼不同,那麼使用占位符%0就沒有什麼意義。確實,兩者生成的代碼完全相同,但這並不意味著這種情況下占位元符沒有意義。因為如果不使用占位符,那麼當有一天你想把變數__out的寄存器約束由a改為b時,那麼你也必須將addl指令中的%%eax改為%%ebx,也就是說你需要同 時修改兩個地方,而如果你使用占位符,你只需要修改一次就夠了。另外,如果你不使用占位符,將不利於代碼的清晰性。在上例中,如果你使用占位符,那麼你一眼就可以得知,addl指令的第二個運算元內容最終會輸出到變數__out中;否則,如果你不用占位符,而是直接將addl指令的第2個運算元寫為%% eax,那麼你需要考慮一下才知道它最終需要輸出到變數__out中。這是占位符最粗淺的意義。畢竟在這種情況下,你完全可以不用。

但對於這些情況來說,不用占位元符就完全不行了:

首先,我們看一看上例中的第1個Input操作運算式"m"(__in1),它被GCC替換之後,表現為addl address_of_in1, %%eax,__in1的地址是什麼?編譯時才知道。所以我們完全無法直接在指令中去寫出__in1的位址,這時使用占位符,交給GCC在編譯時進行替 代,就可以解決這個問題。所以這種情況下,我們必須使用占位符。

其次,如果上例中的Output操作運算式"=a"(__out)改為" =r"(__out),那麼__out在究竟使用那麼寄存器只有到編譯時才能通過GCC來決定,既然在我們寫代碼的時候,我們不知道究竟哪個寄存器被選 擇,我們也就不能直接在指令中寫出寄存器的名稱,而只能通過占位符替代來解決。

5. Clobber/Modify

有時候,你想通知GCC當前內聯彙編語句可能會對某些寄存器或記憶體進行修改,希望GCC在編譯時能夠將這一點考慮進去。那麼你就可以在Clobber/Modify域聲明這些寄存器或記憶體。

這種情況一般發生在一個寄存器出現在"Instruction List",但卻不是由Input/Output操作運算式所指定的,也不是在一些Input/Output操作運算式使用"r","g"約束時由GCC 為其選擇的,同時此寄存器被"Instruction List"中的指令修改,而這個寄存器只是供當前內聯彙編臨時使用的情況。比如:

__asm__ ("movl %0, %%ebx" : : "a"(__foo) : "bx");

寄存器%ebx出現在"Instruction List中",並且被movl指令修改,但卻未被任何Input/Output操作運算式指定,所以你需要在Clobber/Modify域指定"bx",以讓GCC知道這一點。

因為你在Input/Output操作運算式所指定的寄存器,或當你為一些Input/Output操作運算式使用"r","g"約束,讓GCC為你選擇一個寄存器時,GCC對這些寄存器是非常清楚的——它知道這些寄存器是被修改的,你根本不需要在Clobber/Modify域再聲明它們。但除此之外, GCC對剩下的寄存器中哪些會被當前的內聯彙編修改一無所知。所以如果你真的在當前內聯彙編指令中修改了它們,那麼就最好在Clobber/Modify 中聲明它們,讓GCC針對這些寄存器做相應的處理。否則有可能會造成寄存器的不一致,從而造成程式執行錯誤。

在Clobber/Modify域中指定這些寄存器的方法很簡單,你只需要將寄存器的名字使用雙引號(" ")引起來。如果有多個寄存器需要聲明,你需要在任意兩個聲明之間用逗號隔開。比如:

__asm__ ("movl %0, %%ebx; popl %%ecx" : : "a"(__foo) : "bx", "cx" );

這些串包括:

聲明的串 代表的寄存器
"al","ax","eax" %eax
"bl","bx","ebx" %ebx
"cl","cx","ecx" %ecx
"dl","dx","edx" %edx
"si","esi" %esi
"di", "edi" %edi


由上表可以看出,你只需要使用"ax","bx","cx","dx","si","di"就可以了,因為其他的都和它們中的一個是等價的。

如果你在一個內聯彙編語句的Clobber/Modify域向GCC聲明某個寄存器內容發生了改變,GCC在編譯時,如果發現這個被聲明的寄存器的內容在此 內聯彙編語句之後還要繼續使用,那麼GCC會首先將此寄存器的內容保存起來,然後在此內聯彙編語句的相關生成代碼之後,再將其內容恢復。我們來看兩個例子,然後對比一下它們之間的區別。

這個例子中聲明了寄存器%ebx內容發生了改變:

$ cat example7.c

int main(int __argc, char* __argv[])
{
int in = 8;

__asm__ ("addl %0, %%ebx"
: /* no output */
: "a" (in) : "bx");

return 0;
}

$ gcc -O -S example7.c

$ cat example7.s

main:
pushl %ebp
movl %esp, %ebp
pushl %ebx # %ebx內容被保存
movl $8, %eax
#APP
addl %eax, %ebx
#NO_APP
movl $0, %eax
movl (%esp), %ebx # %ebx內容被恢復
leave
ret

下面這個例子的C源碼與上一個例子除了沒有聲明%ebx寄存器發生了改變之外,其他都相同。
$ cat example8.c

int main(int __argc, char* __argv[])
{
int in = 8;

__asm__ ("addl %0, %%ebx"
: /* no output */
: "a" (in) );

return 0;
}

$ gcc -O -S example8.c

$ cat example8.s

main:
pushl %ebp
movl %esp, %ebp
movl $8, %eax
#APP
addl %eax, %ebx
#NO_APP
movl $0, %eax
popl %ebp
ret

仔細對比一下example7.s和example8.s,你就會明白在Clobber/Modify域聲明一個寄存器的意義。

另外需要注意的是,如果你在Clobber/Modify域聲明了一個寄存器,那麼這個寄存器將不能再被用做當前內聯彙編語句的Input/Output操 作運算式的寄存器約束,如果Input/Output操作運算式的寄存器約束被指定為"r"或"g",GCC也不會選擇已經被聲明在 Clobber/Modify中的寄存器。比如:

__asm__ ("movl %0, %%ebx" : : "a"(__foo) : "ax", "bx");

此例中,由於Output操作運算式"a"(__foo)的寄存器約束已經指定了%eax寄存器,那麼再在Clobber/Modify域中指定"ax"就是非法的。編譯時,GCC會給出編譯錯誤。

除 了寄存器的內容會被改變,記憶體的內容也可以被修改。如果一個內聯彙編語句"Instruction List"中的指令對記憶體進行了修改,或者在此內聯彙編出現的地方記憶體內容可能發生改變,而被改變的記憶體位址你沒有在其Output操作運算式使用"m" 約束,這種情況下你需要使用在Clobber/Modify域使用字串"memory"向GCC聲明:“在這裏,記憶體發生了,或可能發生了改變”。例 如:

void * memset(void * s, char c, size_t count)
{
__asm__("cld\n\t"
"rep\n\t"
"stosb"
: /* no output */
: "a" (c),"D" (s),"c" (count)
: "cx","di","memory");
return s;
}

此 例實現了標準函數庫memset,其內聯彙編中的stosb對記憶體進行了改動,而其被修改的記憶體位址s被指定裝入%edi,沒有任何Output操作表達式使用了"m"約束,以指定記憶體位址s處的內容發生了改變。所以在其Clobber/Modify域使用"memory"向GCC聲明:記憶體內容發生了變 動。

如果一個內聯彙編語句的Clobber/Modify域存在"memory",那麼GCC會保證在此內聯彙編之前,如果某個記憶體的內 容被裝入了寄存器,那麼在這個內聯彙編之後,如果需要使用這個記憶體處的內容,就會直接到這個記憶體處重新讀取,而不是使用被存放在寄存器中的拷貝。因為這個時候寄存器中的拷貝已經很可能和記憶體處的內容不一致了。

這只是使用"memory"時,GCC會保證做到的一點,但這並不是全部。因為使用"memory"是向GCC聲明記憶體發生了變化,而記憶體發生變化帶來的影響並不止這一點。比如我們在前面講到的例子:

int main(int __argc, char* __argv[])
{
int* __p = (int*)__argc;

(*__p) = 9999;

__asm__("":::"memory");

if((*__p) == 9999)
return 5;

return (*__p);
}

本 例中,如果沒有那條內聯彙編語句,那個if語句的判斷條件就完全是一句廢話。GCC在優化時會意識到這一點,而直接只生成return 5的彙編代碼,而不會再生成if語句的相關代碼,而不會生成return (*__p)的相關代碼。但你加上了這條內聯彙編語句,它除了聲明記憶體變化之外,什麼都沒有做。但GCC此時就不能簡單的認為它不需要判斷都知道 (*__p)一定與9999相等,它只有老老實實生成這條if語句的彙編代碼,一起相關的兩個return語句相關代碼。

當一個內聯彙編指令中包含影響eflags寄存器中的條件標誌(也就是那些Jxx等跳轉指令要參考的標誌位元,比如,進位元標誌,0標誌等),那麼需要在 Clobber/Modify域中使用"cc"來聲明這一點。這些指令包括adc, div,popfl,btr,bts等等,另外,當包含call指令時,由於你不知道你所call的函數是否會修改條件標誌,為了穩妥起見,最好也使用 "cc"。

我很少在相關資料中看到有關"cc"的確切用法,只有一份文檔提到了它,但還不是i386平臺的,只是說"cc"是處理器平臺 相關的,並非所有的平臺都支持它,但即使在不支持它的平臺上,使用它也不會造成編譯錯誤。我做了一些實驗,但發現使用"cc"和不使用"cc"所生成的代碼沒有任何不同。但Linux 2.4的相關代碼中用到了它。如果誰知道在i386平臺上"cc"的細節,請和我聯繫。

另外,還可以在 Clobber/Modify域指定數位0到9,以聲明第n個Input/Output操作運算式所使用的寄存器發生了變化,但正如我們在前面所提到的,如果你為某個Input/Output操作運算式指定了寄存器,或使用"g","r"等約束讓GCC為其選擇寄存器,GCC已經知道哪個寄存器內容發生了 變化,所以這麼做沒有什麼意義;我也作了相關的試驗,沒有發現使用它會對GCC生成的彙編代碼有任何影響,至少在i386平臺上是這樣。Linux 2.4的所有i386平臺相關內聯彙編代碼中都沒有使用這一點,但S390平臺相關代碼中有用到,但由於我對S390彙編沒有任何概念,所以,也不知道這麼做的意義何在。
--> 閱讀更多...

2010年1月19日 星期二

AT&T 組合語言指令詳解2

這段時間在研究Linux源代碼,遇到了AT&T彙編,故轉貼個不錯的AT&T彙編貼如下

就像軟體的真諦——“給我一個標準,我用我的邏輯舞動世界”一樣,AT&T 彙編是組合語言裏的另一種標準,這是相對於鼎鼎大名的intel的x86彙編來說。即使對於電子專業的學生來說,一旦掌握了c51單片機的彙編,x86的彙編也已經入門了,但x86畢竟有著強大的寄存器,在串操作指令和作業系統類指令方面,單片機還是望塵莫及的。

下面的兩段話就輕易的涉及到了關於首碼、運算元的方向、操作碼的尾碼這些概念的不同。關於首碼,AT&T 彙編中,寄存器前被冠以“%”,立即數前被冠以“$”,十六進位數前被冠以“0x”。所以,如果gemfield在AT&T 語境中說到386的通用寄存器時,會這樣描述:8個32-bit寄存器 %eax,%ebx,%ecx,%edx,%edi,%esi,%ebp,%esp。比如:在stage1.s中,有這麼一個定義,#define ABS(x) (x-_start+0x7c00),那麼你就會知到0x7c00是個十六進位數(_start函數的入口位址就位於記憶體的0x7c00處)。

而在設置int 0x13的0x42功能號時,它是這麼說的:movb $0x42,%ah 。這句告訴了我們一些不同之處:首先,操作碼的尾碼l表示的是操作碼的大小,l是長整數32位元,那麼相應的,movw是16位,movb是8位;其次,立即數是用$首碼來表示的,就像$0x42;再次,寄存器的名字是有%首碼的,像例子中的%ah;最後,運算元的方向有點不一樣,比如把立即數$0x42放到寄存器%ah中,用的是movb $0x42,%ah,也即源運算元在前,目的運算元在後。

對於記憶體單元運算元來說,在AT&T 中是把寄存器用()括起來,而非【】。比如,movl %ebx,8(%si),將ebx寄存器裏的值放到記憶體位址是8(%si)的記憶體單元上。正好,這裏同時遇到了另一個問題,就是在AT&T 彙編中,間接定址方式是有別於x86的。上例中的8(%si)就相當於x86中的【si+8】。

還有一種叫做label(標號)的程式控制語句,比如,在stage1.s中,有這麼一段指令:

cmpb $GRUB_INVALID_DRIVE,%al

je 1f

movb %al,%dl

1:

pushw %dx

上面就用到了標號,je1f,前面的兩個數進行比較,如果相等就跳轉到1的位置。注意,1後面的f表示的是forward,即從je指令後繼續往前走來尋找1這個標號。所以,如果程式中有好幾個叫做1的標號,就要看是1f還是1b了,b代表backward,方向和f相反。青島之光論壇裏有這麼一個例子可以更好的幫助我們理解:

je 1f或者je 1b 是跳轉到對應的標號的地方。這裏的1表示標號(label),f和b表示向前還是向後,f(forward)向前,b(backward)向後,如下例:
1行 1: cmp $0, (%si)
2行 je 1f ///////跳轉到後面的1標示的地方,也就是第6行
3行 movsb
4行 stosb
5行 jmp 1b ////////跳轉到前面1表示的地方 ,也就是第1行
6行 1: jmp 1b ////////跳轉到前面1表示的地方,第6行,其實就是個閉環

然後,在AT&T 彙編中出現最多的大概就是稱作assembler directive的東西了,我們稱作“AT&T 組合語言程式代碼控制”。所有的彙編器命令名都由句號('.')開頭。命令名的其餘是字母,通常使用小寫。在青島之光論壇 http://www.civilnet.cn/bbs/read.php?tid=970 處有一篇關於程式碼控制的詳細介紹,下面gemfield只挑出幾個常用的來說明一下。

一、.byte 運算式(expression_rs)

.byte.byte可不帶參數或者帶多個運算式參數,運算式之間由逗點分隔。每個運算式參數都被彙編成下一個位元組。在stage1.s中,有這麼一段代碼:

after_BPB:

CLI

.byte 0x80,0xca

那麼編譯器在編譯時,就會在cli指令的下面接著放上0x80和0xca,因為每個運算式要佔用1個位元組,所以此處一共佔用2個位元組。

二、.word 運算式

這個運算式表示任意一節中的一個或多個運算式(同樣用逗號分開),運算式占一個字(兩個位元組)。類似的還有.long。例:.word 0x800

三、.file 字元(string)

.file 通告編譯器我們準備開啟一個新的邏輯檔。 string 是新檔案名。總的來說,檔案名是否使用引號‘"’都可以;但如果您希望規定一個空檔案名時,必須使用引號""。本語
句將來可能不再使用—允許使用它只是為了與舊版本的as編譯器程式相容。在as的一些配置中,已經刪除了.file以避免與其它的彙編器衝突。例如stage1.s中:.file ”stage1.s”。

四、.text 小節(subsection)

通知as編譯器把後續語句彙編到編號為subsection的正文子段的末尾,subsection是一個純粹的運算式。如果省略了參數subsection,則使用編號為0的子段。例:.text

五 、.code16

告訴編譯器生成16位元的指令

六、.globl symbol

.globl使得連接程式(ld)能夠看到symbol,如果gemfield在局部程式中定義了symbol,那麼與這個局部程式鏈結的局部程式也能存取symbol,例:

.globl SYMBOL_NAME(idt) 定義idt為全局符號。

七、.fill repeat , size , value
repeat, size 和value都必須是純粹的運算式。本命令生成size個位元組的repeat個副本。
Repeat可以是0或更大的值。Size 可以是0或更大的值, 但即使size大於8,也被視作8,以
相容其他的彙編器。各個副本中的內容取自一個8位元組長的數。最高4個位元組為零,最低的
4個位元組是value,它以as正在彙編的目標電腦的整數位元組順序排列。每個副本中的size
個位元組都取值於這個數最低的size個位元組。再次說明,這個古怪的動作只是為了相容其他
的彙編器。size參數和value參數是可選的。如果不存在第2個逗號和value參數,則假定value為零。如果不存在第1個逗號和其後的參數,則假定size為1。

例如,在linux初始化的過程中,對全局描述符表GDT進行設置的最後一句為:

.fill NR_CPUS*4,8,0,意思是.fill給每個cpu留有存放4個描述符的位置,並且每個描述符是8個位元組。

等等,不一而足。不過要注意的是,這種包含程式已初始化資料的節(.data)和包含程式程式還未初始化的資料的節(.bss),編譯器會把它們在4位元組上對齊,例如,.data是25位元組,那麼編譯器會將它放在28個位元組上。.

當這種以尾碼名.s編寫的A T&T格式的彙編代碼完成後,就是編譯和鏈結了。linux下有兩種方式,一種是使用組合語言程式GAS和連接程式ld:

as filename.s –o filename.o

ld filename.o –o filename 最終將源代碼轉換為目標檔.o再連接為可執行檔filename

另一種就是大名鼎鼎的被gemfield提到過的gcc:

gcc –o gemfield gemfield.S

根源程式gemfield.S的尾碼名可以使用大寫,是因為這樣可以使gcc自動識別組合語言程式中的c預處理命令,包括頭檔中的情況,像#include、#define 、#ifdef等。

本文少了嵌入式彙編的形式,才使得AT&T 彙編看起來井井有條,而非想像中的艱難。如果想要真正鍛煉一下這種彙編,源代碼stage1.s就是一個絕佳的習題。



本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/wangshenwq/archive/2009/10/19/4700481.aspx


AT&T的巨集指令 .p2align[wl]
.p2align[wl] abs-expr, abs-expr, abs-expr
Pad the location counter (in the current subsection) to a particular storage boundary. The first expression (which must be absolute) is the number of low-order zero bits the location counter must have after advancement. For example `.p2align 3' advances the location counter until it a multiple of 8. If the location counter is already a multiple of 8, no change is needed.
The second expression (also absolute) gives the fill value to be stored in the padding bytes. It (and the comma) may be omitted. If it is omitted, the padding bytes are normally zero. However, on some systems, if the section is marked as containing code and the fill value is omitted, the space is filled with no-op instructions.
The third expression is also absolute, and is also optional. If it is present, it is the maximum number of bytes that should be skipped by this alignment directive. If doing the alignment would require skipping more bytes than the specified maximum, then the alignment is not done at all. You can omit the fill value (the second argument) entirely by simply using two commas after the required alignment; this can be useful if you want the alignment to be filled with no-op instructions when appropriate.
The .p2alignw and .p2alignl directives are variants of the .p2align directive. The .p2alignw directive treats the fill pattern as a two byte word value. The .p2alignl directives treats the fill pattern as a four byte longword value. For example, .p2alignw 2,0x368d will align to a multiple of 4. If it skips two bytes, they will be filled in with the value 0x368d (the exact placement of the bytes depends upon the endianness of the processor). If it skips 1 or 3 bytes, the fill value is undefined.

Gcc嵌入式彙編
在Linux的源代碼中,有很多C語言的函數中嵌入一段組合語言程式段,這就是gcc提供的“asm”功能,例如在include/asm-i386/system.h中定義的,讀控制寄存器CR0的一個宏read_cr0(): #define read_cr0() ({ \
unsigned int __dummy; \
__asm__( \
"movl %%cr0,%0\n\t" \
"=r" (__dummy)); \
__dummy; \
}) 這種形式看起來比較陌生,這是因為這不是標準C所定義的形式,而是gcc 對C語言的擴充。其中__dummy為C函數所定義的變數;關鍵字__asm__表示彙編代碼的開始。括弧中第一個引號中為彙編指令movl,緊接著有一個冒號,這種形式閱讀起來比較複雜。

一般而言,嵌入式組合語言片段比單純的組合語言代碼要複雜得多,因為這裏存在怎樣分配和使用寄存器,以及把C代碼中的變數應該存放在哪個寄存器中。為了達到這個目的,就必須對一般的C語言進行擴充,增加對編譯器的指導作用,因此,嵌入式彙編看起來晦澀而難以讀懂。

1. 嵌入式彙編的一般形式:

__asm__ __volatile__ ("" : output : input : modify);

其中,__asm__表示彙編代碼的開始,其後可以跟__volatile__(這是可選項),其含義是避免“asm”指令被刪除、移動或組合;然後就是小括弧,括弧中的內容是我們介紹的重點:

· ""為彙編指令部分,例如,"movl %%cr0,%0\n\t"。數位前加首碼“%“,如%1,%2等表示使用寄存器的樣板運算元。可以使用的運算元總數取決於具體CPU中通用寄存器的數量,如Intel可以有8個。指令中有幾個運算元,就說明有幾個變數需要與寄存器結合,由gcc在編譯時根據後面輸出部分和輸入部分的約束條件進行相應的處理。由於這些樣板運算元的首碼使用了”%“,因此,在用到具體的寄存器時就在前面加兩個“%”,如%%cr0。

· 輸出部分(output),用以規定對輸出變數(目標運算元)如何與寄存器結合的約束(constraint),輸出部分可以有多個約束,互相以逗號分開。每個約束以“=”開頭,接著用一個字母來表示運算元的類型,然後是關於變數結合的約束。例如,上例中:

:"=r" (__dummy)

“=r”表示相應的目標運算元(指令部分的%0)可以使用任何一個通用寄存器,並且變數__dummy 存放在這個寄存器中,但如果是:

:“=m”(__dummy)

“=m”就表示相應的目標運算元是存放在記憶體單元__dummy中。

表示約束條件的字母很多,表 2-5 給出幾個主要的約束字母及其含義:

表2.5 主要的約束字母及其含義

· 輸入部分(Input):輸入部分與輸出部分相似,但沒有“=”。如果輸入部分一個運算元所要求使用的寄存器,與前面輸出部分某個約束所要求的是同一個寄存器,那就把對應運算元的編號(如“1”,“2”等)放在約束條件中,在後面的例子中,我們會看到這種情況。
· 修改部分(modify):這部分常常以“memory”為約束條件,以表示操作完成後記憶體中的內容已有改變,如果原來某個寄存器的內容來自記憶體,那麼現在記憶體中這個單元的內容已經改變。

注意,指令部分為必選項,而輸入部分、輸出部分及修改部分為可選項,當輸入部分存在,而輸出部分不存在時,分號“:“要保留,當“memory”存在時,三個分號都要保留,例如system.h中的巨集定義__cli():

#define __cli() __asm__ __volatile__("cli": : :"memory")

2. Linux源代碼中嵌入式彙編舉例

Linux源代碼中,在arch目錄下的.h和.c檔中,很多檔都涉及嵌入式彙編,下面以system.h中的C函數為例,說明嵌入式彙編的應用。

(1)簡單應用

#define __save_flags(x) __asm__ __volatile__("pushfl ; popl %0":"=g" (x): /* no input */)
#define __restore_flags(x) __asm__ __volatile__("pushl %0 ; popfl": /* no output */
:"g" (x):"memory", "cc") 第一個巨集是保存標誌寄存器的值,第二個巨集是恢復標誌寄存器的值。第一個巨集中的pushfl指令是把標誌寄存器的值壓棧。而popl是把棧頂的值(剛壓入棧的flags)彈出到x變數中,這個變數可以存放在一個寄存器或記憶體中。這樣,你可以很容易地讀懂第二個宏。 (2) 較複雜應用

static inline unsigned long get_limit(unsigned long segment)
{
unsigned long __limit;
__asm__("lsll %1,%0"
:"=r" (__limit):"r" (segment));
return __limit+1;
} 這是一個設置段界限的函數,彙編代碼段中的輸出參數為__limit(即%0),輸入參數為segment(即%1)。Lsll是載入段界限的指令,即把segment段描述符中的段界限欄位裝入某個寄存器(這個寄存器與__limit結合),函數返回__limit加1,即段長。

(3)複雜應用

在Linux內核代碼中,有關字串操作的函數都是通過嵌入式彙編完成的,因為內核及用戶程式對字串函數的調用非常頻繁,因此,用彙編代碼實現主要是為了提高效率(當然是以犧牲可讀性和可維護性為代價的)。在此,我們僅列舉一個字串比較函數strcmp,其代碼在arch/i386/string.h中。

static inline int strcmp(const char * cs,const char * ct)
{
int d0, d1;
register int __res;
__asm__ __volatile__(
"1:\tlodsb\n\t"
"scasb\n\t"
"jne 2f\n\t"
"testb %%al,%%al\n\t"
"jne 1b\n\t"
"xorl %%eax,%%eax\n\t"
"jmp 3f\n"
"2:\tsbbl %%eax,%%eax\n\t"
"orb $1,%%al\n"
"3:"
:"=a" (__res), "=&S" (d0), "=&D" (d1)
:"1" (cs),"2" (ct));
return __res;
} 其中的“\n”是換行符,“\t”是tab符,在每條命令的結束加這兩個符號,是為了讓gcc把嵌入式彙編代碼翻譯成一般的彙編代碼時能夠保證換行和留有一定的空格。例如,上面的嵌入式彙編會被翻譯成:
1: lodsb //裝入串運算元,即從[esi]傳送到al寄存器,然後esi指向串中下一個元素
scasb //掃描串運算元,即從al中減去es:[edi],不保留結果,只改變標誌
jne2f //如果兩個字元不相等,則轉到標號2
testb %al %al
jne 1b
xorl %eax %eax
jmp 3f
2: sbbl %eax %eax
orb $1 %al
3:
這段代碼看起來非常熟悉,讀起來也不困難。其中1f 表示往前(forword)找到第一個標號為1的那一行,相應地,1b表示往後找。其中嵌入式彙編代碼中輸出和輸入部分的結合情況為:
· 返回值__res,放在al寄存器中,與%0相結合;
· 局部變數d0,與%1相結合,也與輸入部分的cs參數相對應,也存放在寄存器ESI中,即ESI中存放源字串的起始位址。
· 局部變數d1,與%2相結合,也與輸入部分的ct參數相對應,也存放在寄存器EDI中,即EDI中存放目的字串的起始位址。
通過對這段代碼的分析我們應當體會到,萬變不利其本,嵌入式彙編與一般彙編的區別僅僅是形式,本質依然不變。因此,全面掌握Intel 386 彙編指令乃突破閱讀低層代碼之根本。
Intel386彙編指令摘要
在閱讀Linux源代碼時,你可能遇到很多彙編指令,有些是你熟悉的,有些可能不熟悉,在此簡要列出一些常用的386彙編指令及其功能。
1. 位元操作指令
指令 功能
BT 位測試
BTC 位測試並求反
BTR 位測試並復位
BTS 位測試並置位
2.控制轉移類指令
指令 功能
CALL 調用過程
JMP 跳轉
LOOP 用ECX計數器的迴圈
LOOPNZ/LOOPNE 用ECX計數器且不為0的迴圈/用ECX計數器且不等的迴圈
RET 返回
3. 資料傳輸指令
指令 功能
IN 從埠輸入
LEA 裝入有效位址
MOV 傳送
OUT 從段口輸出
POP 從堆疊中彈出
POPA/POPAD 從棧彈出至所有寄存器
PUSH 壓棧
PUSH/PUSHAD 所有通用寄存器壓棧
XCHG 交換
4.標誌控制類指令
指令 功能
CLC 清0進位元標誌
CLD 清0方向標誌
CLI 清0中斷標誌
LAHF 將標誌寄存器裝入AH寄存器
POPF/POPFD 從棧中彈出至標誌位元
PUSHF/PUSHFD 將標誌壓棧
SAHF 將AH寄存器存入標誌寄存器
STC 置進位元標誌
STD 置方向標誌
STI 置中斷標誌
5.邏輯類指令
指令 功能
NOT 與
AND 非
OR 或
SAL/SHL 算術作移/邏輯左移
SAR 算術右移
SHLD 邏輯右移
TEST 邏輯比較
XOR 異或
6.串操作指令
指令 功能
CMPS/CMPSB/CMPSW/CMPSD 比較串運算元
INS/INSB/INSW/INSD 輸入串運算元
LODS/LODSB/LODSW/LODSD 裝入串運算元
MOVS/MOVSB/MOVSW/MOVSD 傳送串運算元
REP 重複
REPE/REPZ 相等時重複/為0時重複
SCAS/SCASB/SCASW/SCASD 掃描串運算元
STOS/STOSB/STOSW/STOSD 存儲串運算元
7.多段類操作指令
指令 功能
CALL 程序呼叫
INT 中斷過程的調用
INTO 溢出中斷過程的調用
IRET 中斷返回
JMP 跳轉
LDS 將指針轉入DS
LES 將指針轉入ES
LFS 將指針轉入FS
LGS 將指針轉入GS
LSS 將指針轉入SS
MOV 段寄存器的傳送
POP 從棧彈出至段寄存器
PUSH 壓棧
RET 返回

8.作業系統類指令
指令 功能
APPL 調整請求特權級
ALTS 清任務切換標誌
HLT 暫停
LAR 載入訪問權
LGDT 載入全局描述符表
LIDT 載入中斷描述符表
LLDT 載入局部描述符表
LMSW 載入機器狀態字
LSL 載入段界限
LTR 載入任務寄存器
MOV 特殊寄存器的資料傳送
SGDT 存儲全局描述符表
SIDT 存儲中斷描述符表
SMSW 存儲機器狀態字
STR 存儲任務寄存器


第六講 Linux中的組合語言
Linux中的組合語言
在閱讀Linux源代碼時,你可能碰到一些組合語言片段,有些組合語言出現在以.S為副檔名的彙編檔中,在這種檔中,整個程式全部由組合語言組成。有些彙編命令出現在以.c為副檔名的C檔中,在這種檔中,既有 C語言,也有組合語言,我們把出現在C代碼中的組合語言叫所“嵌入式”彙編。 不管這些彙編代碼出現在哪里,它在一定程度上都成為閱讀源代碼的攔路虎。
儘管C語言已經成為編寫作業系統的主要語言,但是,在作業系統與硬體打交道的過程中,在需要頻繁調用的函數中以及某些特殊的場合中,C語言顯得力不從心,這時,繁瑣但又高效的組合語言必須粉墨登場。 因此,在瞭解一些硬體的基礎上,必須對相關的組合語言知識也所有瞭解。
讀者可能有過在DOS作業系統下編寫組合語言程式的經歷,也具備一定的彙編知識。但是,在 Linux 的源代碼中,你可能看到了與 Intel的組合語言格式不一樣的形式,這就是AT&T的386組合語言。
一、AT&T與Intel組合語言的比較
我們知道,Linux是Unix家族的一員,儘管Linux的歷史不長,但與其相關的很多事情都發源於Unix。 就Linux所使用的386組合語言而言,它也是起源於Unix。 Unix最初是為PDP-11開發的,曾先後被移植到VAX及68000系列的處理器上,這些處理器上的彙編語言都採用的是AT&T的指令格式。當Unix被移植到i386時,自然也就採用了AT&T的匯編語言格式,而不是Intel的格式。儘管這兩種組合語言在語法上有一定的差異,但所基於的硬體知識是相同的,因此,如果你非常熟悉 Intel的語法格式,那麼你也可以很容易地把它“移植“到AT&T來。下面我們通過對照Intel與AT&T的語法格式,以便於你把過去的知識能很快地“移植”過來。
1.首碼
在Intel的語法中,寄存器和和立即數都沒有首碼。但是在AT&T中,寄存器前冠以“%”,而立即數前冠以“$”。在Intel的語法中,十六進位和二進位立即數尾碼分別冠以“h”和“b”,而在AT&T中,十六進位立即數前冠以“0x”,表2.2給出幾個相應的例子。
表2.2 Intel與AT&T首碼的區別
Intel語法: AT&T語法:
mov eax,8 Movl $8,%eax
mov ebx,0ffffh Movl $0xffff,%ebx
int 80h int $0x80

2. 運算元的方向
Intel與AT&T運算元的方向正好相反。在Intel語法中,第一個運算元是目的運算元,第二個運算元源運算元。而在AT&T中,第一個數是源運算元,第二個數是目的運算元。 由此可以看出,AT&T 的語法符合人們通常的閱讀習慣。
例如:
在Intel中, mov eax,[ecx] 在AT&T中,movl (%ecx),%eax

3.記憶體單元運算元
從上面的例子可以看出,記憶體運算元也有所不同。在 Intel的語法中,基寄存器用“[]”括起來,而在AT&T中,用“()”括起來。
例如:
在Intel中,mov eax,[ebx+5]
在AT&T,movl 5(%ebx),%eax
4.間接定址方式
與Intel的語法比較,AT&T間接定址方式可能更晦澀難懂一些。
Intel的指令格式是segreg:[base+index*scale+disp],而AT&T的格式是%segreg:disp(base,index,scale)。其中index/scale/disp/segreg全部是可選的,完全可以簡化掉。如果沒有指定scale而指定了index,則scale的缺省值為1。segreg段寄存器依賴于指令以及應用程式是運行在實模式還是保護模式下,在實模式下,它依賴於指令,而在保護模式下,segreg是多餘的。在AT&T中,當立即數用在scale/disp中時,不應當在其前冠以“$”首碼,表2.3給出其語法及幾個相應的例子。
表2.3 記憶體運算元的語法及舉例
Intel語法: AT&T語法:
mov eax,[ebx+20h] Movl0x20(%ebx),%eax
add eax,[ebx+ecx*2h Addl (%ebx,%ecx,0x2),%eax
lea eax,[ebx+ecx] Leal (%ebx,%ecx),%eax
sub eax,[ebx+ecx*4h-20h] Subl -0x20(%ebx,%ecx,0x4),%eax
指令 foo,segreg:[base+index*scale+disp] 指令 %segreg:disp(base,index,scale),foo

從表中可以看出,AT&T的語法比較晦澀難懂,因為[base+index*scale+disp]一眼就可以看出其含義,而disp(base,index,scale)則不可能做到這點。
這種定址方式常常用在訪問資料結構陣列中某個特定元素內的一個欄位,其中,base為陣列的起始位址,scale為每個陣列元素的大小,index為下標。如果陣列元素還是一個結構,則disp為具體欄位在結構中的位移。
5.操作碼的尾碼
在上面的例子中你可能已注意到,在AT&T的操作碼後面有一個尾碼,其含義就是指出操作碼的大小。“l”表示長整數(32位元),“w”表示字(16位元),“b”表示位元組(8位)。 而在Intel的語法中,則要在記憶體單元運算元的前面加上byte ptr、 word ptr,和dword ptr,“dword”對應“long”。表2.4給出幾個相應的例子。
表2.4 操作碼的尾碼舉例
Intel語法 AT&T語法
Mov al,bl movb %bl,%al
Mov ax,bx movw %bx,%ax
Mov eax,ebx movl %ebx,%eax
Mov eax, dword ptr [ebx] movl (%ebx),%eax

二、 AT&T組合語言的相關知識
在Linux源代碼中,以.S為副檔名的文件是“純”組合語言的文件。這裏,我們結合具體的例子再介紹一些AT&T組合語言的相關知識。
1.GNU組合語言程式GAS GNU Assembly和連接程式當你編寫了一個程式後,就需要對其進行彙編(assembly)和連接。在Linux下有兩種方式,一種是使用組合語言程式GAS和連接程式ld,一種是使用gcc。我們先來看一下GAS和ld:
GAS把組合語言原始檔案(.o)轉換為目標檔(.o),其基本語法如下:
as filename.s -o filename.o
一旦創建了一個目標檔,就需要把它連接並執行,連接一個目標檔的基本語法為:
ld filename.o -o filename
這裏 filename.o是目標檔案名,而filename 是輸出(可執行) 檔。GAS使用的是AT&T的語法而不是Intel的語法,這就再次說明了AT&T語法是Unix世界的標準,你必須熟悉它。如果要使用GNC的C編譯器gcc,就可以一步完成彙編和連接,例如:
gcc -o example example.S
這裏,example.S是你的組合語言程式,輸出檔(可執行檔)名為example。其中,擴展名必須為大寫的S,這是因為,大寫的S可以使gcc自動識別組合語言程式中的C預處理命令,像#include、 #define、 #ifdef、 #endif等,也就是說,使用gcc進行編譯,你可以在組合語言程式中使用C的預處理命令。
2. AT&T中的節(Section)
在AT&T的語法中,一個節由.section關鍵字來標識,當你編寫組合語言程式時,至少需要有以下三種節:

.section .data: 這種節包含程式已初始化的資料,也就是說,包含具有初值的那些變量,例如:
hello : .string "Hello world!\n"
hello_len : .long 13
.section .bss:這個節包含程式還未初始化的資料,也就是說,包含沒有初值的那些變量。當操作 系統裝入這個程式時將把這些變數都置為0,例如:
name : .fill 30 # 用來請求用戶輸入名字
name_len : .long 0 # 名字的長度 (尚未定義)
當這個程式被裝入時,name 和 name_len都被置為0。如果你在.bss節不小心給一個變數賦了初值,這個值也會丟失,並且變數的值仍為0。
使用.bss比使用.data的優勢在於,.bss節不佔用磁片的空間。在磁片上,一個長整數就足以存放.bss節。當程式被裝入到記憶體時,作業系統也只分配給這個節4個位元組的記憶體大小。 注意:編譯程序把.data和.bss在4位元組上對齊(align),例如,.data總共有34字節,那麼編譯程序把它對其在36位元組上,也就是說,實際給它36位元組的空間。
.section .text :這個節包含程式的代碼,它是唯讀節,而.data 和.bss是讀/寫節。
3.組合語言程式指令(Assembler Directive)
上面介紹的.section就是組合語言程式指令的一種,GNU組合語言程式提供了很多這樣的指令(directiv),這種指令都是以句點(.)為開頭,後跟指令名(小寫字母),在此,我們只介紹在內核源代碼中出現的幾個指令(以arch/i386/kernel/head.S中的代碼為例)。
(1)ascii "string"...
.ascii 表示零個或多個(用逗號隔開)字串,並把每個字串(結尾不自動加“0“位元組)中的字元放在連續的位址單元。
還有一個與.ascii類似的.asciz,z代表“0“,即每個字串結尾自動加一個”0“位元組,例如:
int_msg:
.asciz "Unknown interrupt\n"
(2).byte 運算式
.byte表示零或多個運算式(用逗號隔開),每個運算式被放在下一個位元組單元。
(3).fill 運算式
形式:.fill repeat , size , value
其中,repeat、 size 和value都是常量運算式。 Fill的含義是反復拷貝size個位元組。
Repeat可以大於等於0。 size也可以大於等於0,但不能超過8,如果超過8,也只取8。把repeat個位元組以8個為一組,每組的最高4個位元組內容為0,最低4位元組內容置為value。 Size和 value為可選項。如果第二個逗號和value值不存在,則假定value為0。如果第一個逗號和size不存在,則假定size為1。
例如,在Linux初始化的過程中,對全局描述符表GDT進行設置的最後一句為:
.fill NR_CPUS*4,8,0 /* space for TSS's and LDT's */
因為每個描述符正好占 8個位元組,因此,.fill給每個CPU留有存放4個描述符的位置。
(4).globl symbol
.globl使得連接程式(ld)能夠看到symbl。如果你的局部程式中定義了symbl,那麼,與這個局部程式連接的其他局部程式也能存取symbl,例如:
.globl SYMBOL_NAME(idt)
.globl SYMBOL_NAME(gdt)
定義idt和gdt為全局符號。
(5)quad bignums
.quad表示零個或多個bignums(用逗號分隔),對於每個bignum,其缺省值是8字節整數。如果bignum超過8位元組,則列印一個警告資訊;並只取bignum最低8位元組。例如,對全局描述符表的填充就用到這個指令:
.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */
.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */
.quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */
(6)rept count
把.rept指令與.endr指令之間的行重複count次,例如
.rept 3 .long 0
.endr
相當於
.long 0
.long 0
.long 0
(7)space size , fill
這個指令保留size個位元組的空間,每個位元組的值為fill。 size 和fill都是常量運算式。
如果逗號和fill被省略,則假定fill為0,例如在arch/i386/bootl/setup.S中有一句:
.space 1024
表示保留1024位元組的空間,並且每個位元組的值為0。
(8).word expressions
這個運算式表示任意一節中的一個或多個運算式(用逗號分開),運算式的值占兩個位元組,例如:
gdt_descr:
.word GDT_ENTRIES*8-1
表示變數gdt_descr的置為GDT_ENTRIES*8-1
(9).long expressions
這與.word類似
(10).org new-lc , fill
把當前節的位置計數器提前到new-lc(new location counter)。 new-lc或者是一個常量運算式,或者是一個與當前子節處於同一節的運算式。也就是說,你不能用.org橫跨節:如果new-lc是個錯誤的值,則.org被忽略。 .org只能增加位置計數器的值,或者讓其保持不變;但絕不能用.org來讓位置計數器倒退。 注意,位置計數器的起始值是相對於一個節的開始的,而不是子節的開始。當位置計數器被提升後,中間位置的位元組被填充值 fill(這也是一個常量運算式)。如果逗號和fill都省略,則fill的缺省值為0。 例如:.org 0x2000
ENTRY(pg0)
表示把位置計數器置為0x2000,這個位置存放的就是臨時頁表pg0。
三、 Gcc嵌入式彙編
在Linux的源代碼中,有很多C語言的函數中嵌入一段組合語言程式段,這就是 gcc提供的“asm”功能,例如在 include/asm-i386/system.h中定義的,讀控制寄存器 CR0的一個宏read_cr0():
#define read_cr0() ({ \
unsigned int __dummy; \
__asm__( \
"movl %%cr0,%0\n\t" \
:"=r" (__dummy)); \
__dummy; \
})
這種形式看起來比較陌生,這是因為這不是標準 C所定義的形式,而是gcc 對C語言的擴充。其中__dummy為C函數所定義的變數;關鍵字__asm__表示彙編代碼的開始。 括弧中第一個引號中為彙編指令movl,緊接著有一個冒號,這種形式閱讀起來比較複雜。一般而言,嵌入式組合語言片段比單純的組合語言代碼要複雜得多,因為這裏存在怎樣分配和使用寄存器,以及把C代碼中的變數應該存放在哪個寄存器中。 為了達到這個目的,就必須對一般的C語言進行擴充,增加對編譯器的指導作用,因此,嵌入式彙編看起來晦澀而難以讀懂。
1. 嵌入式彙編的一般形式:
__asm__ __volatile__ ("" : output : input : modify);
其中,__asm__表示彙編代碼的開始,其後可以跟__volatile__(這是可選項),其含義是避免“asm”指令被刪除、移動或組合;然後就是小括弧,括弧中的內容是我們介紹的重點:
· ""為彙編指令部分,例如,"movl %%cr0,%0\n\t"。數位前加首碼“%“,如%1,%2等表示使用寄存器的樣板運算元。可以使用的運算元總數取決於具體CPU中通用寄存器的數量,如Intel可以有8個。指令中有幾個運算元,就說明有幾個變量需要與寄存器結合,由gcc在編譯時根據後面輸出部分和輸入部分的約束條件進行相應的處理。 由於這些樣板運算元的首碼使用了”%“,因此,在用到具體的寄存器時就在前面加兩個“%”,如%%cr0。
· 輸出部分(output),用以規定對輸出變數(目標運算元)如何與寄存器結合的約束(constraint),輸出部分可以有多個約束,互相以逗號分開。 每個約束以“=”開頭,接著用一個字母來表示運算元的類型,然後是關於變數結合的約束。 例如,上例中:
:"=r" (__dummy)“=r”表示相應的目標運算元(指令部分的%0)可以使用任何一個通用寄存器,並且變數__dummy 存放在這個寄存器中,但如果是:
:“=m”(__dummy)
“=m”就表示相應的目標運算元是存放在記憶體單元__dummy中。
表示約束條件的字母很多,表 2-5 給出幾個主要的約束字母及其含義:
表2.5 主要的約束字母及其含義
字母: 含義:
m, v,o 表示記憶體單元
R 表示任何通用寄存器
Q 表示寄存器eax, ebx, ecx,edx之一
I, h 表示直接運算元
E, F 表示浮點數
G 表示“任意”
a, b.c d 表示要求使用寄存器eax/ax/al, ebx/bx/bl, ecx/cx/cl或edx/dx/dl
S, D 表示要求使用寄存器esi或edi
I 表示常數(0~31)
· 輸入部分(Input):輸入部分與輸出部分相似,但沒有“=”。如果輸入部分一個運算元所要求使用的寄存器,與前面輸出部分某個約束所要求的是同一個寄存器,那就把對應運算元的編號(如“1”,“2”等)放在約束條件中,在後面的例子中我們會看到這種情況。
· 修改部分(modify):這部分常常以“memory”為約束條件,以表示操作完成後記憶體中的內容已有改變,如果原來某個寄存器的內容來自記憶體,那麼現在記憶體中這個單元的內容已經改變。注意,指令部分為必選項,而輸入部分、輸出部分及修改部分為可選項,當輸入部分存在,而輸出部分不存在時,分號“:“要保留,當“memory”存在時,三個分號都要保留,例如system.h中的巨集定義__cli(): #define __cli() __asm__ __volatile__("cli": : :"memory")
2. Linux源代碼中嵌入式彙編舉例
Linux源代碼中,在arch目錄下的.h和.c檔中,很多檔都涉及嵌入式彙編,下面以system.h中的C函數為例,說明嵌入式彙編的應用。
(1)簡單應用
#define __save_flags(x) __asm__ __volatile__("pushfl ; popl %0":"=g" (x): /* no input */)
#define __restore_flags(x) __asm__ __volatile__("pushl %0 ; popfl": /* no output */ :"g" (x):"memory", "cc")
第一個巨集是保存標誌寄存器的值,第二個巨集是恢復標誌寄存器的值。第一個宏中的pushfl指令是把標誌寄存器的值壓棧。 而popl是把棧頂的值(剛壓入棧的flags)彈出到x變數中,這個變數可以存放在一個寄存器或記憶體中。這樣,你可以很容易地讀懂第二個宏。
(2) 較複雜應用
static inline unsigned long get_limit(unsigned long segment)
{
unsigned long __limit;
__asm__("lsll %1,%0"
:"=r" (__limit):"r" (segment));
return __limit+1;
}
這是一個設置段界限的函數,彙編代碼段中的輸出參數為__limit(即%0),輸入參數為segment(即%1)。Lsll是載入段界限的指令,即把segment段描述符中的段界限字段裝入某個寄存器(這個寄存器與__limit結合),函數返回__limit加1,即段長。(3)複雜應用在Linux內核代碼中,有關字串操作的函數都是通過嵌入式彙編完成的,因為內核及用戶程式對字串函數的調用非常頻繁,因此,用彙編代碼實現主要是為了提高效率(當然是以犧牲可讀性和可維護性為代價的)。在此,我們僅列舉一個字串比較函數strcmp,其代碼在arch/i386/string.h中。
static inline int strcmp(const char * cs,const char * ct)
{
int d0, d1;
register int __res;
__asm__ __volatile__(
"1:\tlodsb\n\t"
"scasb\n\t"
"jne 2f\n\t"
"testb %%al,%%al\n\t"
"jne 1b\n\t"
"xorl %%eax,%%eax\n\t"
"jmp 3f\n"
"2:\tsbbl %%eax,%%eax\n\t"
"orb $1,%%al\n"
"3:"
:"=a" (__res), "=&S" (d0), "=&D" (d1)
:"1" (cs),"2" (ct));
return __res;
}
其中的“\n”是換行符,“\t”是tab符,在每條命令的結束加這兩個符號,是為了讓gcc把嵌入式彙編代碼翻譯成一般的彙編代碼時能夠保證換行和留有一定的空格。例如,上面的嵌入式彙編會被翻譯成:
1: lodsb //裝入串運算元,即從[esi]傳送到al寄存器,然後esi指向串中下一個元素
scasb //掃描串運算元,即從al中減去es:[edi],不保留結果,只改變標誌
jne2f //如果兩個字元不相等,則轉到標號2
testb %al %al
jne 1b
xorl %eax %eax
jmp 3f
2: sbbl %eax %eax
orb $1 %al
3:
這段代碼看起來非常熟悉,讀起來也不困難。其中1f 表示往前(forword)找到第一個標號為1的那一行,相應地,1b表示往後找。其中嵌入式彙編代碼中輸出和輸入部分的結合情況為:
· 返回值__res,放在al寄存器中,與%0相結合;· 局部變數d0,與%1相結合,也與輸入部分的cs參數相對應,也存放在寄存器ESI中,即ESI中存放源字串的起始位址。· 局部變數d1, 與%2相結合,也與輸入部分的ct參數相對應,也存放在寄存器EDI中,即EDI中存放目的字串的起始位址。
通過對這段代碼的分析我們應當體會到,萬變不利其本,嵌入式彙編與一般彙編的區別僅僅是形式,本質依然不變。因此,全面掌握Intel 386 彙編指令乃突破閱讀低層代碼之根本。

http://book.csdn.net/bookfiles/824/10082424777.shtml
--> 閱讀更多...

AT&T 組合語言指令詳解

AT&T 組合語言指令詳解
以下資料皆為網路搜尋的;
在閱讀linux/unix內核源代碼的時候,必須先掌握彙編,大家都知道,內核代碼用的編譯器是gcc,而gcc採用的是AT&T的彙編格式,與MS的intel有些區別。
一 AT&T的基本語法
語法上主要有以下幾個不同.
★ 寄存器命名原則
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,8)
Intel: [ebx + eax*8 + _array]

二 基本的行內彙編

基本的行內彙編很簡單,一般是按照下面的格式
asm("statements");
例如:asm("nop"); asm("cli");
asm 和 __asm__是完全一樣的.
如果有多行彙編,則每一行都要加上 "\n\t"
例如:
asm( "pushl %eax\n\t"
"movl $0,%eax\n\t"
"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 \n\t"
"rep \n\t"
"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寄存器
類似的還有:
a eax
b ebx
c ecx
d edx
S esi
D edi
I 常數值,(0 - 31)
q,r 動態分配的寄存器
g eax,ebx,ecx,edx或內存變量
A 把eax和edx合成一個64位的寄存器(use long longs)
我們也可以讓gcc自己選擇合適的寄存器.
如下面的例子:
asm("leal (%1,%1,4),%0"
: "=r" (x)
: "0" (x) );
這段代碼實現5*x的快速乘法.
得到的主要彙編代碼為:
movl x,%eax
#APP
leal (%eax,%eax,4),%eax
#NO_APP
movl %eax,x
幾點說明:
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) );
注意要使用兩個%,因為一個%的語法已經被%n用掉了.
下面可以來解釋letter 4854-4855的問題:
1、變量加下劃線和雙下劃線有什麼特殊含義嗎?
加下劃線是指全局變量,但我的gcc中加不加都無所謂.
2、以上定義用如下調用時展開會是什麼意思?
#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; \
}

四.AT&T彙編與Intel彙編的比較

Intel和AT&T語法的區別

Intel和AT&T彙編語言的語法表面上各不相同,這將導致剛剛學會INTEL彙編的人第一次見到AT&T彙編時
會感到困惑,或者反之。因此讓我們從基礎的東西開始。
前綴
在Intel彙編中沒有寄存器前綴或者立即數前綴。而在AT&T彙編中寄存器有一個「%」前綴,立即數有
一個「$」前綴。Intel語句中十六進制和二進制數據分別帶有「h」和「b」後綴,並且如果十六進制
數字的第一位是字母的話,那麼數值的前面要加一個「0」前綴。
例如,
Intex Syntax
mov eax,1
mov ebx,0ffh
int 80h
AT&T Syntax
movl $1,%eax
movl $0xff,%ebx
int $0x80
就像你看到的,AT&T非常難懂。[base+index*scale+disp] 看起來比disp(base,index,scale)更好理解。
操作數的用法
intel語句中操作數的用法和AT&T中的用法相反。在Intel語句中,第一個操作數表示目的,第二個
操作數表示源。然而在AT&T語句中第一個操作數表示源而第二個操作數表示目的。在這種情形下AT&T語法
的好處是顯而易見的。我們從左向右讀,也從左向右寫,這樣比較自然。
例如,
Intex Syntax
instr dest,source
mov eax,[ecx]
AT&T Syntax
instr source,dest
movl (%ecx),%eax
存儲器操作數
如同上面所看到的,存儲器操作數的用法也不相同。在Intel語句中基址寄存器用「[」和「]」括起來
而在AT&T語句中是用「(」和「)」括起來的。
例如,
Intex Syntax
mov eax,[ebx]
mov eax,[ebx+3]
AT&T Syntax
movl (%ebx),%eax
movl 3(%ebx),%eax
AT&T語法中用來處理複雜的操作的指令的形式和Intel語法中的形式比較起來要難懂得多。在Intel語句
中這樣的形式是segreg:[base+index*scale+disp]。在AT&T語句中這樣的形式是
%segreg:disp(base,index,scale)。
Index/scale/disp/segreg 都是可選並且可以去掉的。Scale在本身沒有說明而index已指定的情況下
缺省值為1。segreg的確定依賴於指令本身以及程式運行在實模式還是pmode。在實模式下它依賴於
指令本身而pmode模式下它是不需要的。在AT&T語句中用作scale/disp的立即數不要加「$」前綴。
例如
Intel Syntax
instr foo,segreg:[base+index*scale+disp]
mov eax,[ebx+20h]
add eax,[ebx+ecx*2h]
lea eax,[ebx+ecx]
sub eax,[ebx+ecx*4h-20h]
AT&T Syntax
instr %segreg:disp(base,index,scale),foo
movl 0x20(%ebx),%eax
addl (%ebx,%ecx,0x2),%eax
leal (%ebx,%ecx),%eax
subl -0x20(%ebx,%ecx,0x4),%eax

後綴

就像你已經注意到的,AT&T語法中有一個後綴,它的意義是表示操作數的大小。「l」代表long,
「w」代表word,「b」代表byte。Intel語法中在處理存儲器操作數時也有類似的表示,
如byte ptr, word ptr, dword ptr。"dword" 顯然對應於「long」。這有點類似於C語言中定義的
類型,但是既然使用的寄存器的大小對應著假定的數據類型,這樣就顯得不必要了。
例子:
Intel Syntax
mov al,bl
mov ax,bx
mov eax,ebx
mov eax, dword ptr [ebx]
AT&T Syntax
movb %bl,%al
movw %bx,%ax
movl %ebx,%eax
movl (%ebx),%eax
注意:從此開始所有的例子都使用AT&T語法

AT&T組合語言的相關知識

在Linux源代碼中,以.S為副檔名的文件是“純”組合語言的文件。這裏,我們結合具體的例子再介紹一些AT&T組合語言的相關知識。
1.GNU組合語言程式GAS GNU Assembly和連接程式
當你編寫了一個程式後,就需要對其進行彙編(assembly)和連接。在Linux下有兩種方式,一種是使用組合語言程式GAS和連接程式ld,一種是使用gcc。我們先來看一下GAS和ld:
GAS把組合語言原始檔案(.o)轉換為目標檔(.o),其基本語法如下:

as filename.s -o filename.o

一旦創建了一個目標檔,就需要把它連接並執行,連接一個目標檔的基本語法為:
ld filename.o -o filename
這裏 filename.o是目標檔案名,而filename 是輸出(可執行) 檔。

GAS使用的是AT&T的語法而不是Intel的語法,這就再次說明了AT&T語法是Unix世界的標準,你必須熟悉它。

如果要使用GNC的C編譯器gcc,就可以一步完成彙編和連接,例如:

gcc -o example example.S

這裏,example.S是你的組合語言程式,輸出檔(可執行檔)名為example。其中,副檔名必須為大寫的S,這是因為,大寫的S可以使gcc自動識別組合語言程式中的C預處理命令,像#include、#define、#ifdef、 #endif等,也就是說,使用gcc進行編譯,你可以在組合語言程式中使用C的預處理命令。

2. AT&T中的節(Section)
在AT&T的語法中,一個節由.section關鍵字來標識,當你編寫組合語言程式時,至少需要有以下三種節:
.section .data: 這種節包含程式已初始化的資料,也就是說,包含具有初值的那些變數,例如:
hello : .string "Hello world!\n"

hello_len : .long 13

.section .bss:這個節包含程式還未初始化的資料,也就是說,包含沒有初值的那些變數。當操作

系統裝入這個程式時將把這些變數都置為0,例如:

name : .fill 30 # 用來請求用戶輸入名字

name_len : .long 0 # 名字的長度 (尚未定義)

當這個程式被裝入時,name 和 name_len都被置為0。如果你在.bss節不小心給一個變數賦了初值,這個值也會丟失,並且變數的值仍為0。

使用.bss比使用.data的優勢在於,.bss節不佔用磁片的空間。在磁片上,一個長整數就足以存放.bss節。當程式被裝入到記憶體時,作業系統也只分配給這個節4個位元組的記憶體大小。

注意:編譯程序把.data和.bss在4位元組上對齊(align),例如,.data總共有34位元組,那麼編譯程序把它對其在36位元組上,也就是說,實際給它36位元組的空間。

.section .text :這個節包含程式的代碼,它是唯讀節,而.data 和.bss是讀/寫節。

3.組合語言程式指令(Assembler Directive)

上面介紹的.section就是組合語言程式指令的一種,GNU組合語言程式提供了很多這樣的指令(directiv),這種指令都是以句點(.)為開頭,後跟指令名(小寫字母),在此,我們只介紹在內核源代碼中出現的幾個指令(以arch/i386/kernel/head.S中的代碼為例)。

(1)ascii "string"...

.ascii 表示零個或多個(用逗號隔開)字串,並把每個字串(結尾不自動加“0“位元組)中的字元放在連續的位址單元。

還有一個與.ascii類似的.asciz,z代表“0“,即每個字串結尾自動加一個”0“位元組,例如:

int_msg:

.asciz "Unknown interrupt\n"

(2).byte 運算式

.byte表示零或多個運算式(用逗號隔開),每個運算式被放在下一個位元組單元。

(3).fill 運算式

形式:.fill repeat , size , value

其中,repeat、size 和value都是常量運算式。Fill的含義是反復拷貝size個位元組。Repeat可以大於等於0。size也可以大於等於0,但不能超過8,如果超過8,也只取8。把repeat個位元組以8個為一組,每組的最高4個位元組內容為0,最低4位元組內容置為value。

Size和 value為可選項。如果第二個逗號和value值不存在,則假定value為0。如果第一個逗號和size不存在,則假定size為1。

例如,在Linux初始化的過程中,對全局描述符表GDT進行設置的最後一句為:

.fill NR_CPUS*4,8,0 /* space for TSS's and LDT's */

因為每個描述符正好占8個位元組,因此,.fill給每個CPU留有存放4個描述符的位置。

(4).globl symbol

.globl使得連接程式(ld)能夠看到symbl。如果你的局部程式中定義了symbl,那麼,與這個局部程式連接的其他局部程式也能存取symbl,例如:

.globl SYMBOL_NAME(idt)

.globl SYMBOL_NAME(gdt)

定義idt和gdt為全局符號。


(5)quad bignums

.quad表示零個或多個bignums(用逗號分隔),對於每個bignum,其缺省值是8位元組整數。如果bignum超過8位元組,則列印一個警告資訊;並只取bignum最低8位元組。

例如,對全局描述符表的填充就用到這個指令:

.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */

.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */

.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */

.quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */


(6)rept count

把.rept指令與.endr指令之間的行重複count次,例如

.rept 3

.long 0

.endr

相當於

.long 0

.long 0

.long 0

(7)space size , fill


這個指令保留size個位元組的空間,每個位元組的值為fill。size 和fill都是常量運算式。如果逗號和fill被省略,則假定fill為0,例如在arch/i386/bootl/setup.S中有一句:

.space 1024

表示保留1024位元組的空間,並且每個位元組的值為0。

(8).word expressions

這個運算式表示任意一節中的一個或多個運算式(用逗號分開),運算式的值占兩個位元組,例如:

gdt_descr:

.word GDT_ENTRIES*8-1

表示變數gdt_descr的置為GDT_ENTRIES*8-1

(9).long expressions

這與.word類似

(10).org new-lc , fill

把當前節的位置計數器提前到new-lc(new location counter)。new-lc或者是一個常量運算式,或者是一個與當前子節處於同一節的運算式。也就是說,你不能用.org橫跨節:如果new-lc是個錯誤的值,則.org被忽略。.org只能增加位置計數器的值,或者讓其保持不變;但絕不能用.org來讓位置計數器倒退。

注意,位置計數器的起始值是相對於一個節的開始的,而不是子節的開始。當位置計數器被提升後,中間位置的位元組被填充值fill(這也是一個常量運算式)。如果逗號和fill都省略,則fill的缺省值為0。

例如:.org 0x2000

ENTRY(pg0)

表示把位置計數器置為0x2000,這個位置存放的就是臨時頁表pg0。
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/kpgood/archive/2009/04/05/4049495.aspx
--> 閱讀更多...

2009年3月19日 星期四

●Fast A20 和92H,及8042到底是什麼關係

以下是從DOS编程技术這ㄍ大陸博客寫ㄉ資料放ㄌ上來,它裡面還有介紹一些好東西。您可以慢慢參考、品嘗。
早期的PC,控制鍵盤有一個單獨的單片機8042,現如今這個晶片已經給集成到了其它大片子中,但其功能和使用 方法還是一樣,當PC機剛剛出現A20 Gate的時候,估計實在找不到控制它的地方了,同時為這點小事也不值得增加晶片,於是工程師使用這個8042鍵盤控制器來控制A20 Gate,但A20 Gate真的和鍵盤一點關係也沒有,我們先從軟體的角度簡單介紹一下8042這個晶片。
8042有4個寄存器
• 1個8-bit長的Input buffer;Write-Only;
• 1個8-bit長的Output buffer; Read-Only;
• 1個8-bit長的Status Register;Read-Only;
• 1個8-bit長的Control Register;Read/Write。
有兩個埠位址:60h和64h。
• 讀60h埠,讀output buffer
• 寫60h埠,寫input buffer
• 讀64h埠,讀Status Register
對Control Register的操作相對要複雜一些,首先要向64h埠寫一個命令(20h為讀命令,60h為寫命令),然後根據命令從60h埠讀出Control Register的資料或者向60h埠寫入Control Register的資料(64h埠還可以接受許多其它的命令)。
先來看看Status Register的定義,我們後面要用bit 0和bit 1:
bit meaning
-----------------------------------------------------------------------
0 output register (60h) 中有數據
1 input register (60h/64h) 有數據
2 系統標誌(上電重定後被置為0)
3 data in input register is command (1) or data (0)
4 1=keyboard enabled, 0=keyboard disabled (via switch)
5 1=transmit timeout (data transmit not complete)
6 1=receive timeout (data transmit not complete)
7 1=even parity rec'd, 0=odd parity rec'd (should be odd)
除了這些資源外,8042還有3個內部埠:Input Port、Outport Port和Test Port,這三個埠的操作都是通過向64h發送命令,然後在60h進行讀寫的方式完成,其中本文要操作的A20 Gate被定義在Output Port的bit 1上,所以我們有必要對Outport Port的操作及埠定義做一個說明。
• 讀Output Port
向64h發送0d0h命令,然後從60h讀取Output Port的內容
• 寫Output Port
向64h發送0d1h命令,然後向60h寫入Output Port的資料
另外我們還應該介紹兩個命令:
• 禁止鍵盤操作命令
向64h發送0adh
• 打開鍵盤操作命令
向64h發送0aeh
有了這些命令和知識,我們可以考慮操作A20 Gate了,有關8042晶片更詳細的資料,請參考該晶片的Data Sheet。
如何打開和關閉A20 Gate。
理論上講,我們只要操作8042晶片的輸出埠(64h)的bit 1,就可以控制A20 Gate,但實際上,當你準備向8042的輸入緩衝區裡寫資料時,可能裡面還有其它資料沒有處理,所以,我們要首先禁止鍵盤操作,同時等待資料緩衝區中沒 有資料以後,才能真正地去操作8042打開或者關閉A20 Gate。打開A20 Gate的具體步驟大致如下:
1.關閉中斷;
2.等待8042 Input buffer為空;
3.發送禁止鍵盤操作命令;
4.等待8042 Input buffer為空;
5.發送讀取8042 Output Port命令;
6.等待8042 Output buffer有數據;
7.讀取8042 Output buffer,並保存得到的位元組;
8.等待8042 Input buffer為空;
9.發送Write 8042 Output Port命令到8042 Input buffer;
10.等待8042 Input buffer為空;
11.將從8042 Output Port得到的位元組的第2位置1(或清0),然後寫入8042 Input buffer;
12.等待,直到8042 Input buffer為空為止;
13.發送允許鍵盤操作命令到8042 Input buffer;
14. 打開中斷。
下面是完成打開A20 Gate的代碼:
A20Enable:
cli ;1.關閉中斷
call WaitInbufEmpty ;2.等待8042 Input buffer為空;
mov al, 0adh
mov dx, 64h
out dx, al ;3.發送禁止鍵盤操作命令
call WaitInbufEmpty ;4.等待8042 Input buffer為空;
mov al, 0d0h
mov dx, 64h
out dx, al ;5.發送讀取8042 Output Port命令;
call WaitOutbufFull ;6.等待8042 Output buffer有數據;
mov dx, 60h
in al, dx ;7.讀取8042 Output buffer
push ax ;保存讀取的資料
call WaitInbufEmpty ;8.等待8042 Input buffer為空;
mov al, 0d1h
mov dx, 64h
out dx, al ;9.發送寫 8042 Output Port命令
call WaitInbufEmpty ;10.等待8042 Input buffer為空
pop ax
or al, 02h ;11.將從8042 Output Port得到的位元組的bit 1置1
mov dx, 60h
out dx, al ;寫入Output Port
call WaitInbufEmpty ;12.等待8042 Input buffer為空
mov al, 0aeh
mov dx, 64h
out dx, al ;13.發送允許鍵盤操作命令
sti ;開中斷
ret

WaitInbufEmpty:
mov dx, 64h
in al, dx ;讀取Status Register
test al, 02h
jnz WaitInbufEmpty
ret

WaitOutbufFull:
mov dx, 64h
in al, dx
test al, 01 ;讀取Status Register
jz WaitOutbufFull
ret
後來,由於感覺使用8042控制A20運行太慢了(確實,那麼長的代碼,中間還要若干次的wait),所以後來又出現了所謂的Fast A20,實際上,現在的大多數機器都是Fast A20,Fast A20使用92h埠控制A20,同時BIOS裡提供了一個軟中斷來控制A20:
入口:ah=24h
al=0 關閉A20
1 打開A20
2 讀取A20狀態
int 15h
返回:如果BIOS支援此功能,CF=0,否則CF=1
CF=0時,AX返回當前A20狀態,1=打開,0=關閉
像8042中的Output Port中的定義一樣,92h埠的bit 1控制著A20,為1時打開,為0時關閉,從92h中讀一個byte可以看a20的當前狀態,所以對92h的操作如下:
• 讀A20狀態
mov dx, 92h
in al, dx
如果al的bit 1為1表示a20打開,否則為0
• 打開A20
mov dx, 92h
mov al, 02
out dx, al
• 關閉A20
mov dx, 92h
mov al, 0
out dx, al
特別要注意的是,大家從這篇文章的文字中可能也能感覺到,A20 Gate的設計本身就讓人感覺很彆扭,不是那麼流暢,所以和A20有關的事情就難免也會有相同的感覺,很奇怪的是,上面介紹的三種方法並不是在每台機器上 都適用,所以如果你要做一個商務軟體其中要操作A20,那一定要三種方式聯合使用才比較穩妥,否則會有意想不到的結果,LINUX公開的啟動代碼中就是這 麼做的
在DOS下有時我們會在config.sys中寫上一句:dos=high,這句就會把駐留的DOS放到高端記憶體區域去,怎麼放的呢?關鍵點就是打開 A20,然後把DOS從常設記憶體搬到100000h起始的區域去,並把在常設記憶體中佔用的記憶體釋放掉,當然說起來容易,實際做的時候還有很多細節要處理。
說到DOS=HIGH,就不得不提醒大家另一件事,如果在config.sys中有dos=high這一句,那麼恐怕92h的方法和BIOS的方法都會不 完全靈驗(至於操作8042的方法靈不靈我沒有試),這是DOS做了手腳,因為DOS被放到了高端記憶體中,為了保證DOS能正常運行,它不允許你把A20 給關掉,遇到這種情況不要驚慌,不是我寫得不對,確實是DOS太狡猾了。
還有最後一點要特別注意,92h的bit 0是給機器發重定信號的(8042 Output Port的bit 0也是),所以在向92h寫資料時,千萬不要讓bit 0為1,否則機器會重新啟動,如果你的應用程式需要重新開機機器,這也是方法之一,比jmp 0ffffh:0來的還要乾脆。
在其它我們介紹保護模式的文章中,我們會用到上面提到的打開A20的方法,屆時可能就不會做更多的解釋了。
另外,涉及操作A20的資料其實很少,有些資料我手裡也很缺乏,比如92h除bit 0和bit 1以外的定義是什麼我至今也不知道。

更多參考:Gate A20與保護模式
--> 閱讀更多...

2009年3月18日 星期三

●8042 Keyboard Controller

IO Port 60h為鍵盤的輸入暫存器 、61h為控制暫存器、64h狀態暫存器
8042 - Keyboard Controller
8042 Status Register (port 64h read)
8042 Status Register
Bit0|------output register (60h) has data for system
Bit1|------input register (60h/64h) has data for 8042
Bit2|------system flag (set to 0 after power on reset)
Bit3|------data in input register is command (1) or data (0)
Bit4|------1=keyboard enabled, 0=keyboard disabled (via switch)
Bit5|------1=transmit timeout (data transmit not complete)
Bit6|------1=receive timeout (data transmit not complete)
Bit7|------1=even parity rec'd, 0=odd parity rec'd (should be odd)

Port Mode Description

64h read 8042 status register. Can be read at any time. See table above for more information.
64h write 8042 command register. Writing this port sets Bit3 of the status register to 1 and the byte is treated as a controller command. Devices attached to the 8042 should be disabled before issuing commands that return data since data in the output register will be overwritten.
60h read 8042 output register (should only be read if Bit 0 of status port is set to 1)
60h write 8042 data register. Data should only be written if Bit 1 of the status register is zero (register is empty).
When this port is written Bit 3 of the status register is set to zero and the byte is treated as a data. The 8042 uses this byte if it's expecting data for a previous command, otherwise the data is written directly to the keyboard. See ~KEYBOARD COMMANDS~ for information on
programming the actual keyboard hardware.

8042 Commands Related to PC Systems (Port 64h)

Command Description

20 Read 8042 Command Byte: current 8042 command byte is placed in port 60h.
60 Write 8042 Command Byte: next data byte written to port 60h is placed in 8042 command register. Format:

8042 Command Byte
Bit0|------1=enable output register full interrupt
Bit1|------ should be 0
Bit2|------1=set status register system, 0=clear
Bit3|------ 1=override keyboard inhibit, 0=allow inhibit
Bit4|------disable keyboard I/O by driving clock line low
Bit5|------ disable auxiliary device, drives clock line low
Bit6|------IBM scancode translation 0=AT, 1=PC/XT
Bit7|------reserved, should be 0

A4 Password Installed Test: returned data can be read from port 60h; FA=password installed, F1=no password
A5 Load Security: bytes written to port 60h will be read until a null (0) is found.
A6 Enable Security: works only if a password is already loaded
A7 Disable Auxiliary Interface: sets Bit 5 of command register stopping auxiliary I/O by driving the clock line low
A8 Enable Auxiliary Interface: clears Bit 5 of command register
A9 Auxiliary Interface Test: clock and data lines are tested; results placed at port 60h are listed below:

00 no error
01 keyboard clock line is stuck low
02 keyboard clock line is stuck high
03 keyboard data line is stuck low
04 keyboard data line is stuck high

AA Self Test: diagnostic result placed at port 60h, 55h=OK
AB Keyboard Interface Test: clock and data lines are tested; results placed at port 60h are listed above with command A9
AC Diagnostic Dump: sends 16 bytes of 8042's RAM, current input port state, current output port state and 8042 program status
word to port 60h in scan-code format.
AD Disable Keyboard Interface: sets Bit 4 of command register stopping keyboard I/O by driving the clock line low
AE Enable Keyboard Interface: clears Bit 4 of command register enabling keyboard interface.
C0 Read Input Port: data is read from its input port (which is inaccessible to the data bus) and written to output register at port 60h; output register should be empty before call.

8042 Input Port
Bit0-3|------undefined
Bit4|------ 1=enable 2nd 256K of motherboard RAM, 0=disable
Bit5|------1=manufacturing jumper not installed, 0=installed
Bit6|------ 1=primary display is MDA, 0=primary display is CGA
Bit7|------1=keyboard not inhibited, 0=keyboard inhibited

C1 Poll Input Port Low Bits: Bits 0-3 of port 1 placed in status Bits 4-7
C2 Poll Input Port High Bits: Bits 4-7 of port 1 placed in status Bits 4-7
D0 Read Output Port: data is read from 8042 output port (which is inaccessible to the data bus) and placed in output register;
the output register should be empty. (see command D1 below)
D1 Write Output Port: next byte written to port 60h is placed in the 8042 output port (which is inaccessible to the data bus)

8042 Output Port
Bit0|------system reset line
Bit1|------gate A20
Bit2-3|------undefined
Bit4|------ output buffer full
Bit5|------input buffer empty
Bit6|------keyboard clock (output)
Bit7|------keyboard data (output)

D2 Write Keyboard Output Register: on PS/2 systems the next data
byte written to port 60h input register is written to port 60h
output register as if initiated by a device; invokes interrupt
if enabled
D3 Write Auxiliary Output Register: on PS/2 systems the next data
byte written to port 60h input register is written to port 60h
output register as if initiated by a device; invokes interrupt
if enabled
D4 Write Auxiliary Device: on PS/2 systems the next data byte
written to input register a port at 60h is sent to the
auxiliary device
E0 Read Test Inputs: 8042 reads its T0 and T1 inputs; data is
placed in output register; Bit 0 is T0, Bit 1 is T1:

Test Input Port Bits
Bit0|------keyboard clock
Bit1|------keyboard data

Fx Pulse Output Port: Bits 0-3 of the 8042 output port can be pulsed low for 6 us(微秒);Bits 0-3 of command indicate which Bits should be pulsed; 0=pulse, 1=don't pulse; pulsing
Bit 0 results in CPU reset since it is connected to system reset line.
--> 閱讀更多...

●讀網卡MAC Address有這麼難嗎?


或許你知道ipconfig,linux:ifconfig;但是很抱歉,我不想在有os的狀況讀出這些資訊,更不想拆下eeprom直接讀取IC。說穿了就是我現在工作遇上的瀕頸(不知這兩ㄍ字有沒有打錯,好久沒用)因為我知道bios有提供int 1ah中斷,從這部分我們可以去找出你想要的pci device的PFA,再從PFA中透過bus、device、function去找出真正你想找到的device,再從device中的BAR(Base Address Register)中找出你要的io port這樣就可以從port讀出你要的mac address。很幸運也很不幸rtl8139、rtl8168讀出來都正確無誤,不管是內建網卡還是外插螃蟹卡都可以,但是問題來ㄌ,Marvell Yukon的網卡卻讓我失去信心,原因是它是memory mappied而不是I/O mappied,還是說它是PCI-e的Device,不好意思,我還不確定,若你是高手高高手,煩請指點迷津。‧‧‧‧‧‧迷途羔羊--------Benson,也有人稱呼我老B,雖然這兩ㄍ字有點不雅。


這個問題終於搞定,感謝jajamboliao的熱心指導。現在可以放下它ㄌ,若沒有高人指點,上面那張圖我已經跑了3個loop,結果每次放下它,都是因為處理不了它;now;如釋重負。
--> 閱讀更多...

●Borland C++3.1 Compile時無法辨認內嵌組合語言的32bit register




這部分ㄉ解決辦法就是要設定兩ㄍ地方: 1.Options/Compiler/Code generation/Compile via assembler 2.Options/Compiler/Advanced code generation/80386 搞定。
--> 閱讀更多...

●learnning assembly

Iczelion's Win32 Assembly Homepage
小木偶

組合語言程式設計

還有很多‧‧‧‧但是希望我們都是一步一腳印的人。
--> 閱讀更多...