GCC内嵌汇编
轉(zhuǎn)載:http://blog.chinaunix.net/uid-28685940-id-3889916.html
?
?內(nèi)核代碼絕大部分使用C語言編寫,只有一小部分使用匯編語言編寫,例如與特定體系結(jié)構(gòu)相關(guān)的代碼和對性能影響很大的代碼。GCC提供了內(nèi)嵌匯編的功能,可以在C代碼中直接內(nèi)嵌匯編語言語句,大大方便了程序設(shè)計。
一、基本內(nèi)嵌匯編
? GCC提供了很好的內(nèi)嵌匯編支持,最基本的格式是:
__asm__ __volatile__(匯編語句模板);
1、__asm__
? __asm__是GCC關(guān)鍵字asm的宏定義:
#define __asm__ asm
? __asm__或asm用來聲明一個內(nèi)嵌匯編表達式,所以任何一個內(nèi)嵌匯編表達式都是以它開頭的,是必不可少的。
2、匯編語句模板
“匯編語句模板”是一組插入到C程序中的匯編指令(可以是單個指令,也可以是一組指令)。每條指令都應(yīng)該由雙引號括起,或者整組指令應(yīng)該由雙引號括起。每 條指令還應(yīng)該用一個定界符結(jié)尾。有效的定界符為換行符(\n)和分號(;)。\n后可以跟一個制表符(\t)作為格式化符號,增加GCC在匯編文件中生成 的指令的可讀性。
? 上述原則可以歸結(jié)為:
①任意兩個指令間要么被分號(;)分開,要么被放在兩行;
②放在兩行的方法既可以通過\n的方法來實現(xiàn),也可以真正的放在兩行;
③可以使用一對或多對雙引號,每對雙引號里可以放任意多條指令,所有的指令都必須放到雙引號中。
? 在基本內(nèi)嵌匯編中,“匯編語句模板”的書寫的格式和你直接在匯編文件中使用匯編語言編程沒有什么不同,你可以在其中定義標號(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");
建議大家都使用這種格式來寫內(nèi)嵌匯編代碼。
3、__volatile__
? __volatile__是GCC關(guān)鍵字volatile的宏定義:
#define __volatile__ volatile
? __volatile__或volatile是可選的。如果不想讓GCC的優(yōu)化改動你的內(nèi)嵌匯編代碼,你最好在前面都加上__volatile__。
二、帶C語言表達式的內(nèi)嵌匯編
? 在內(nèi)嵌匯編中,可以將C語言表達式指定為匯編指令的操作數(shù),而且不用去管如何將C語言表達式的值讀入哪個寄存器,以及如何將計算結(jié)果寫回C變量,你只要告訴程序中C語言表達式與匯編指令操作數(shù)之間的對應(yīng)關(guān)系即可,GCC會自動插入代碼完成必要的操作。
? 通常嵌入到C代碼中的匯編語句很難做到與其它部分沒有任何關(guān)系,因此更多時候需要用到擴展的內(nèi)嵌匯編格式:
__asm__ __volatile__(匯編語句模板 : 輸出部分 : 輸入部分 : 破壞描述部分);
? 內(nèi)嵌匯編表達式包含4個部分,各部分由“:”分隔。這4個部分都不是必須的,任何一個部分都可以為空,其規(guī)則為:
①如果“破壞描述部分”為空,則其前面的“:”必須省略。比如:
__asm__("mov %%eax, %%ebx" : :);。
②如果“匯編語句模板”為空,則“輸出部分”,“輸入部分”以及“破壞描述部分”可以不為空,也可以為空。比如:
__asm__("" : : : "memory");。
③如果“輸出部分”,“輸入部分”以及“破壞描述部分”都為空,“輸出部分”和“輸入部分”之前的“:”既可以省略,也可以不省略。如果都省略,則此匯編退化為一個基本內(nèi)嵌匯編,否則,仍然是一個帶有C語言表達式的內(nèi)嵌匯編。
④如果“輸入部分”和“破壞描述部分”為空,但“輸出部分”不為空,“輸入部分”前的“:”既可以省略,也可以不省略。
⑤如果后面的部分不為空,而前面的部分為空,則前面的“:”都必須保留,否則無法說明不為空的部分究竟是第幾部分。
⑥如果“破壞描述部分”不為空,而“輸出部分”和“輸入部分”都為空,則“輸出部分”和“輸入部分”前的“:”都必須保留。
? 從上面的規(guī)則可以看到另外一個事實,區(qū)分一個內(nèi)嵌匯編是基本格式的還是擴展格式的,其規(guī)則在于在“匯編語句模板”后面是否有“:”的存在,如果沒有則是基本格式的,否則,就是擴展格式的。
? 這兩種格式對寄存器語法的要求不同:基本格式要求寄存器前只能使用一個%,這一點和原生匯編相同;而擴展格式則要求寄存器前必須使用兩個%%。比如:
__asm__("mov %%eax, %%ebx" :)
和
__asm__("mov %eax, %ebx")
都是正確的寫法,而
__asm__("mov %eax, %ebx" :)
和
__asm__("mov %%eax, %%ebx")
都是錯誤的寫法。任何只帶一個“%”的標識符都看成是操作數(shù),而不是寄存器。
1、內(nèi)嵌匯編舉例
? 使用內(nèi)嵌匯編,要先編寫匯編語句模板,然后將C語言表達式與指令的操作數(shù)相關(guān)聯(lián),并告訴GCC對這些操作有哪些約束條件。例如在下面的匯編語句:
__asm__("movl %1, %0" : "=r"(result) : "m"(input));
“movl %1,%0”是指令模板;“%0”和“%1”代表指令的操作數(shù),稱為占位符,內(nèi)嵌匯編靠它們將C語言表達式與指令操作數(shù)相對應(yīng)。指令模板后面用圓括號括起 來的是C語言表達式,本例中只有兩個:“result”和“input”,他們按照在輸出部分和輸入部分出現(xiàn)的順序分別與指令操作數(shù)“%0”,“%1”對 應(yīng);注意對應(yīng)順序:第一個C語言表達式對應(yīng)“%0”;第二個表達式對應(yīng)“%1”,依次類推。在每個操作數(shù)前面有一個用雙引號括起來的字符串,字符串的內(nèi)容 是對該操作數(shù)的約束或者說要求?!皉esult”前面的約束字符串是“=r”,其中“=”表示“result”在指令中是只寫的(輸出操作數(shù)),“r”表 示需要將“result”與某個通用寄存器相關(guān)聯(lián),先將操作數(shù)的值讀入寄存器,然后在指令中使用相應(yīng)寄存器,而不是“result”本身,當然指令執(zhí)行完 后需要將寄存器中的值存入變量“result”,從表面上看好像是指令直接對“result”進行操作,實際上GCC做了隱式處理,這樣我們可以少寫一些 指令?!癷nput”前面的“r”表示該表達式需要先放入某個寄存器,然后在指令中使用該寄存器參加運算。
? 由此可見,C語言表達式或者變量與寄存器的關(guān)系由GCC自動處理,我們只需使用約束字符串指導(dǎo)GCC如何處理即可。
? 內(nèi)聯(lián)匯編的重要性體現(xiàn)在它能夠靈活操作,而且可以使其輸出通過C變量顯示出來。因為它具有這種能力,所以__asm__可以用作匯編指令和包含它的C程序之間的接口。
2、匯編語句模板
◆操作數(shù)
? C語言表達式可用作內(nèi)嵌匯編中的匯編指令的操作數(shù)。在匯編指令通過對C語言表達式進行操作來執(zhí)行有意義的作業(yè)的情況下,操作數(shù)是擴展格式的內(nèi)嵌匯編的主要特性。
? 每個操作數(shù)都由操作數(shù)約束字符串指定,后面跟著用圓括號括起來的C語言表達式,例如:
"constraint"(C expression)
操作數(shù)約束的主要功能是確定操作數(shù)的尋址方式。
◆占位符
? 在擴展格式的內(nèi)嵌匯編的“匯編語句模板”中,操作數(shù)由占位符引用。如果總共有n個操作數(shù)(包括輸入和輸出),那么第一個輸出操作數(shù)的編號為0,逐項遞增,總操作數(shù)的數(shù)目限制在10個(%0、%1、…、%9)。
? 如果要處理很多輸入和輸出操作,數(shù)字型的占位符很快就會變得混亂。為了使條理清晰,GNU編譯器(從版本3.1開始)允許聲明替換的名稱作為占位符。
? 替換的名稱在“輸入部分”和“輸出部分”中聲明。格式如下:
[name] "constraint"(C expression)
? 聲明name后,使用%[name]的形式替換內(nèi)嵌匯編代碼中相應(yīng)的數(shù)字型占位符。如下面所示:
__asm__("cmoveq %1, %2, %[result]"
: [result] "=r"(result)
: "r"(test), "r"(new), "[result]"(old));
? 在內(nèi)嵌匯編中使用占位符表示的操作數(shù),總被視為long型(4個字節(jié)) ,但對其施加的操作根據(jù)指令可以是字或者字節(jié),當把操作數(shù)當作字或者字節(jié)使用時,默認為低字或者低字節(jié)。對字節(jié)操作可以顯式的指明是低字節(jié)還是高字節(jié)。方 法是在%和序號之間插入一個字母,“b”代表低字節(jié),“h”代表高字節(jié),例如:%h1。
必須使用占位符的情況:
我們看一看下面這個例子:
__asm__("addl %1, %0"
: "=a"(out)
: "m"(in1), "a"(in2));
①首先,我們看一看上例中的第1個輸入操作表達式"m"(in1),它被GCC替換之后,表現(xiàn)為addl address_of_in1, %%eax,in1的地址是什么?編譯時才知道。所以我們完全無法直接在指令中去寫出in1的地址,這時使用占位符,交給GCC在編譯時進行替代,就可以 解決這個問題。所以這種情況下,我們必須使用占位符。
②其次,如果上例中的輸出操作表達式"=a"(out)改為"=r"(out),那么out究竟會使用哪個寄存器只有到編譯時才能通過GCC來決定,既然 在我們寫代碼的時候,我們不知道究竟哪個寄存器被選擇,我們也就不能直接在指令中寫出寄存器的名稱,而只能通過占位符替代來解決。
3、輸出部分
? “輸出部分”用來指定當前內(nèi)嵌匯編語句的輸出。我們看一看這個例子:
__asm__("movl %%cr0, %0" : "=a"(cr0));
這個內(nèi)嵌匯編語句的輸出部分為"=r"(cr0),它是一個“操作表達式”,更具體地在這里叫作“輸出操作表達式”,指定了一個輸出操作?!拜敵霾僮鞅磉_式”由兩部分組成,這兩部分都是必不可少的:
①圓括號括起來的部分是一個C語言表達式,用來保存內(nèi)嵌匯編的一個輸出值,其操作就等于C的賦值表達式cr0 = output_value,因此,圓括號中的輸出表達式只能是C的左值表達式。那么右值output_value從何而來呢?
②答案是雙引號中的內(nèi)容,被稱作“操作約束”(Operation Constraint),在這個例子中操作約束為"=a",它包含兩個約束:等號(=)和字母a,其中等號(=)說明圓括號中左值表達式cr0是 Write-Only的,只能夠被作為當前內(nèi)嵌匯編的輸出,而不能作為輸入。而字母a是寄存器EAX/AX/AL的簡寫,說明cr0的值要從EAX寄存器 中獲取,也就是說cr0 = %eax,最終這一點被轉(zhuǎn)化成匯編語句就是movl %eax, address_of_cr0。
? 另外,需要特別說明的是,很多文檔都聲明,所有輸出操作的操作約束必須包含一個等號(=),但GCC的文檔中卻很清楚的聲明,并非如此。因為等號(=)約 束說明當前的表達式是Write-Only的,但另外還有一個符號——加號(+)用來說明當前表達式是Read-Write的,如果一個操作約束中沒有給 出這兩個符號中的任何一個,則說明當前表達式是Read-Only的。因為對于輸出操作來說,肯定是必須是可寫的,而等號(=)和加號(+)都表示可寫, 只不過加號(+) 同時也表示是可讀的。所以對于一個輸出操作來說,其操作約束只需要有等號(=)或加號(+)中的任意一個就可以了。二者的區(qū)別是:等號(=)表示當前操作 表達式指定了一個純粹的輸出操作,而加號(+)則表示當前操作表達式不僅僅只是一個輸出操作還是一個輸入操作。但無論是等號(=)約束還是加號(+)約束 所約束的操作表達式都只能放在“輸出部分”中,而不能被用在“輸入部分”中。
? 在“輸出部分”中可以有多個輸出操作表達式,多個操作表達式中間必須用逗號(,)分開。
4、輸入部分
? “輸入部分”的內(nèi)容用來指定當前內(nèi)嵌匯編語句的輸入。我們看一看這個例子:
__asm__("movl %0, %%db7" : : "a"(cpu->db7));
例中“輸入部分”的內(nèi)容為一個表達式"a"(cpu->db7),被稱作“輸入操作表達式”,用來表示一個對當前內(nèi)嵌匯編的輸入。
? 像輸出操作表達式一樣,一個輸入操作表達式也分為兩部分:帶圓括號的部分(cpu->db7)和帶雙引號的部分"a"。這兩部分對于一個內(nèi)嵌匯編輸入操作表達式來說也是必不可少的。
? 圓括號中的表達式cpu->db7是一個C語言的表達式,它不必是一個左值表達式,也就是說它不僅可以是放在C賦值操作左邊的表達式,還可以是放在C賦值操作右邊的表達式。所以它可以是一個變量,一個數(shù)字,還可以是一個復(fù)雜的表達式。比如上例可以改為:
__asm__("movl %0, %%db7" : : "a"(foo));
__asm__("movl %0, %%db7" : : "a"(0x1000));
__asm__("movl %0, %%db7" : : "a"(x*y/z));
? 雙引號中的部分是約束部分,和輸出操作表達式約束不同的是,它不允許指定加號(+)約束和等號(=)約束,也就是說它只能是默認的Read-Only的。 約束中必須指定一個寄存器約束,例中的"a"表示當前輸入變量cpu->db7要通過寄存器%eax輸入到當前內(nèi)嵌匯編中。
? 在“輸入部分”中可以有多個輸入操作表達式,多個操作表達式中間必須用逗號(,)分開。
5、操作約束
? 前面提到過,在內(nèi)嵌匯編中的每個操作數(shù)都應(yīng)該由操作數(shù)約束字符串描述,后面跟著用圓括號括起來的C語言表達式。操作數(shù)約束主要是確定指令中操作數(shù)的尋址方式。約束也可以指定:
①是否允許操作數(shù)位于寄存器中,以及它可以包括在哪些類型的寄存器中
②操作數(shù)是否可以是內(nèi)存引用,以及在這種情況下使用哪些類型的尋址方式
③操作數(shù)是否可以是立即數(shù)
? 約束字符必須與指令對操作數(shù)的要求相匹配,否則產(chǎn)生的匯編代碼將會有錯,在這個例子中:
__asm__("movl %1,%0" : "=r"(result) : "r"(input));
如果將那兩個"r",都改為"m"(“m”表示操作數(shù)是內(nèi)存引用)編譯后得到的結(jié)果是:
movl input, result
很明顯這是一條非法指令(mov不允許內(nèi)存到內(nèi)存的操作)。
? 每一個輸入和輸出操作表達式都必須指定自己的操作約束,下面是在80x86平臺上可能使用的操作約束:
◆寄存器約束
? 當你當前的輸入或輸出需要借助一個寄存器時,你需要為其指定一個寄存器約束。你可以直接指定一個寄存器的名字,比如:
__asm__("movl %0, %%cr0" : : "eax"(cr0));
也可以指定一個縮寫,比如:
__asm__("movl %0, %%cr0" : : "a"(cr0));
如果你指定一個縮寫,比如“a”,則GCC將會根據(jù)當前操作表達式中C語言表達式的類型決定使用%eax,還是%ax或%al。比如:
unsigned short shrt;
__asm__("mov %0,%%bx" : : "a"(shrt));
由于變量shrt是16-bit short類型,則編譯出來的匯編代碼中,會讓此變量使用%ax寄存器。
? 無論是輸入還是輸出的操作表達式,都可以使用寄存器約束。
◆內(nèi)存約束
? 如果一個輸入或輸出操作表達式的C語言表達式表現(xiàn)為一個內(nèi)存地址,并且不想借助于任何寄存器,則可以使用內(nèi)存約束。比如:
__asm__("lidt %0" : "=m"(idt_addr));
? 使用內(nèi)存方式進行輸入輸出時,由于不借助寄存器,所以GCC不會按照你的聲明對其作任何的輸入輸出處理。GCC只會直接拿來用,究竟對這個C語言表達式而言是輸入還是輸出,完全依賴與你寫在“匯編語句模板”中的指令對其操作的指令。
? 當操作數(shù)位于內(nèi)存中時,任何對它們執(zhí)行的操作都將在內(nèi)存位置中直接發(fā)生,所以,對于內(nèi)存約束類型的操作表達式而言,放在“輸入部分”還是放在“輸出部 分”,對編譯結(jié)果是沒有任何影響的,既然對于內(nèi)存約束類型的操作表達式來說,GCC不會自動為它做任何事情,那么放在哪兒也就無所謂了。但從程序員的角度 而言,為了增強代碼的可讀性,最好能夠把它放在符合實際情況的地方。
◆立即數(shù)約束
? 如果一個輸入或輸出操作表達式的C語言表達式是一個數(shù)字常數(shù),并且不想借助于任何寄存器,則可以使用立即數(shù)約束。
? 由于立即數(shù)在C中只能作為右值,所以對于使用立即數(shù)約束的操作表達式而言,只能放在“輸入部分”。比如:
__asm__("movl %0, %%eax" : : "i"(100));
◆匹配約束
? 匹配約束符是一位數(shù)字:“0”,“1”,…,“9”,表示它約束的C表達式分別與占位符%0,%1,…,%9相對應(yīng)的C變量匹配。例如使用“0”作為%1的約束字符,那么%0和%1表示同一個C變量。
? 在某些情況下,一個變量既要充當輸入操作數(shù),也要充當輸出操作數(shù)??梢酝ㄟ^使用匹配約束在內(nèi)嵌匯編中的“輸入部分”指定這種情況。
__asm__("incl %0" : "=a"(var) : "0"(var));
? 在上面的示例中,寄存器%eax既用作輸入變量,也用作輸出變量。將輸入變量var讀取到%eax,執(zhí)行inc指令后將更新了值的%eax再次存儲在var中。這里的"0"指定與第0個輸出變量相同的約束。即,它指定var的輸出實例只應(yīng)該存儲在%eax中。
該約束可以用于以下情況:
①輸入從變量中讀取,或者變量被修改后,修改寫回到同一變量中
②不需要將輸入操作數(shù)和輸出操作數(shù)的實例分開
使用匹配約束最重要的意義在于它們可以導(dǎo)致有效地使用可用寄存器。
? i386指令集中許多指令的操作數(shù)是讀寫型的,例如:
addl %1, %0
它先讀取%0與%1原來的值然后將兩者的值相加,并把結(jié)果存回%0,因此操作數(shù)%0是讀寫型操作數(shù)。老版本的GCC對這種類型操作數(shù)的支持不是很好,它將操作數(shù)嚴格分為輸入和輸出兩種,分別放在輸入部分和輸出部分,而沒有一個單獨部分描述讀寫型操作數(shù)。
__asm__("addl %1, %0" : "=r"(result) : "r"(input));
上例使用“r”約束的輸出變量,GCC會分配一個寄存器,然后用該寄存器替換占位符,但是在使用該寄存器之前并不將result變量的值先讀入寄存 器,GCC認為所有輸出變量以前的值都沒有用處,也就沒有必要將其讀入寄存器(這可能是因為AT&T匯編源于RISC架構(gòu)處理器的原故,在 RISC處理器中大部分指令的輸入輸出明顯分開,而不像CISC那樣一個操作數(shù)既做輸入又做輸出,例如:
add r0, r1, r2
r0和r1是輸入,r2是輸出,輸入和輸出分開,不使用輸入輸出型操作數(shù)。這種情況下GCC理所當然認為所有輸出變量以前的值都沒有用處,也就沒有必要先將輸出操作數(shù)的值讀入寄存器r2了)。
? 上面的內(nèi)嵌匯編指令不能奏效,因為需要在執(zhí)行addl之前把result的值入寄存器。因此在GCC中讀寫型的操作數(shù)需要在輸入和輸出部分分別描述,靠匹配約束符將兩者關(guān)聯(lián)到一起。注意僅在輸入和輸出部分使用相同的C變量,但是不用匹配約束符,例如:
__asm__("addl %2, %0" : "=r"(result) : "r"(result), "m"(input));
產(chǎn)生的代碼很可能不對。
? 看上去上面的代碼可以正常工作,因為我們知道%0和%1都和result相關(guān),應(yīng)該使用同一個寄存器,但是GCC并不去判斷%0和%1是否和同一個C語言表達式或變量相關(guān)聯(lián)(這樣易于產(chǎn)生與內(nèi)嵌匯編相應(yīng)的匯編代碼),因此%0和%1使用的寄存器可能不同。
? 使用匹配約束符后,GCC知道應(yīng)將對應(yīng)的操作數(shù)放在同一個位置(同一個寄存器或者同一個內(nèi)存變量)。使用匹配約束字符的代碼如下:
__asm__("addl %2,%0" : "=r"(result) : "0"(result), "m"(input));
相應(yīng)的匯編代碼為:
? movl $0, _result
? movl $1, _input
? movl _result, %edx
? movl %edx, %eax
#APP
? addl _input, %eax
#NO_APP
? movl %eax, %edx
? movl %edx, _result
可以看到與result相關(guān)的寄存器是%edx,在執(zhí)行指令addl之前先從%edx將result讀入%eax,執(zhí)行之后需要將結(jié)果從%eax讀 入%edx,最后存入result中。這里我們可以看出GCC處理內(nèi)嵌匯編中輸出操作數(shù)的一點點信息:addl并沒有使用%edx,可見它不是簡單的用 result對應(yīng)的寄存器%edx去替換%0,而是先分配一個寄存器,執(zhí)行運算,最后才將運算結(jié)果存入對應(yīng)的變量,因此GCC是先看該占位符對應(yīng)的變量的 約束符,發(fā)現(xiàn)是一個輸出型寄存器變量,就為它分配一個寄存器,此時沒有去管對應(yīng)的C變量,最后GCC知道還要將寄存器的值寫回變量,與此同時,它發(fā)現(xiàn)該變 量與%edx關(guān)聯(lián),因此先存入%edx,再存入變量。
? 在新版本的GCC中增加了一個約束字符“+”,它表示操作數(shù)是讀寫型的,GCC知道應(yīng)將變量值先讀入寄存器,然后計算,最后寫回變量,而無需在輸入部分再去描述該變量。
__asm__("addl %1, %0" : "+r"(result) : "m"(input));
產(chǎn)生的匯編代碼如下:
? movl $0,_result
? movl $1,_input
? movl _result,%eax
#APP
? addl _input,%eax
#NO_APP
? movl %eax,_result
L2:
? movl %ebp,%esp
處理的比使用匹配約束符的情況還要好,省去了好幾條匯編代碼。
◆修飾符
? 等號(=)和加號(+)用于對輸出操作表達式的修飾,一個輸出操作表達式要么被等號(=)修飾,要么被加號(+)修飾,二者必居其一。使用等號(=)說明 此輸出操作表達式是Write-Only的,使用加號(+)說明此輸出操作表達式是Read-Write的。它們必須是輸出操作表達式約束字符串中的第一 個字符。比如:"a="(var)是非法的,而"+g"(var)則是合法的。
? 當使用加號(+)的時候,此輸出操作表達式等價于使用等號(=)約束再加上一個輸入操作表達式。比如:
__asm__("incl %0" : "+a"(var));
等價于
__asm__("incl %0" : "=a"(var) : "0"(var));
? 像等號(=)和加號(+)修飾符一樣,符號(&)也只能用于對輸出操作表達式的修飾。
? 約束符“&”表示輸入和輸出操作數(shù)不能使用相同的寄存器,這樣可以避免很多錯誤。舉一個例子,下面代碼的作用是將函數(shù)foo的返回值存入變量ret中:
__asm__("call foo; movl %%edx, %1" : "=a"(ret) : "r"(bar));
我們知道函數(shù)的int型返回值存放在%eax中,但是GCC編譯的結(jié)果是輸入和輸出同時使用了寄存器%eax,如下:
? movl bar, %eax
#APP
? call foo
? movl %ebx, %eax
#NO_APP
? movl %eax, ret
結(jié)果顯然不對,原因是GCC并不知道%eax中的值是我們所要的。避免這種情況的方法是使用“&”修飾符,這樣bar就不會再使用%eax寄存器,因為已被ret指定使用。
__asm__("call foo; movl %%edx, %1" : "=&a"(ret) : "r"(bar));
6、破壞描述部分
? 有時在進行某些操作時,除了要用到進行數(shù)據(jù)輸入和輸出的寄存器外,還要使用多個寄存器來保存中間計算結(jié)果,這樣就難免會破壞原有寄存器的內(nèi)容。如果希望GCC在編譯時能夠?qū)⑦@一點考慮進去。那么你就可以在“破壞描述部分”聲明這些寄存器或內(nèi)存。
? 這種情況一般發(fā)生在一個寄存器出現(xiàn)在“匯編語句模板”,但卻不是由輸入或輸出操作表達式所指定的,也不是在一些輸入或輸出操作表達式使用"r"、"g"約 束時由GCC為其選擇的,同時此寄存器被“匯編語句模板”中的指令修改,而這個寄存器只是供當前內(nèi)嵌匯編臨時使用的情況。比如:
__asm__("movl %0, %%ebx" : : "a"(foo) : "%ebx");
寄存器%ebx出現(xiàn)在“匯編語句模板”中,并且被movl指令修改,但卻未被任何輸入或輸出操作表達式指定,所以你需要在“破壞描述部分”指定"%ebx",以讓GCC知道這一點。
? 因為你在輸入或輸出操作表達式所指定的寄存器,或當你為一些輸入或輸出操作表達式使用"r"、"g"約束,讓GCC為你選擇一個寄存器時,GCC對這些寄 存器是非常清楚的——它知道這些寄存器是被修改的,你根本不需要在“破壞描述部分”再聲明它們。但除此之外,GCC對剩下的寄存器中哪些會被當前的內(nèi)嵌匯 編修改一無所知。所以如果你真的在當前內(nèi)嵌匯編語句中修改了它們,那么就最好“破壞描述部分”中聲明它們,讓GCC針對這些寄存器做相應(yīng)的處理。否則有可 能會造成寄存器的不一致,從而造成程序執(zhí)行錯誤。
? 在“破壞描述部分”中指定這些寄存器的方法很簡單,你只需要將寄存器的名字使用雙引號引起來。如果有多個寄存器需要聲明,你需要在任意兩個聲明之間用逗號隔開。比如:
__asm__("movl %0, %%ebx; popl %%ecx" : : "a"(foo) : "%ebx", "%ecx" );
? 注意準備在“破壞描述部分”聲明的寄存器必須使用完整的寄存器名稱,在寄存器名稱前面使用的“%”是可選的。
? 另外需要注意的是,如果你在“破壞描述部分”聲明了一個寄存器,那么這個寄存器將不能再被用做當前內(nèi)嵌匯編語句的輸入或輸出操作表達式的寄存器約束,如果 輸入或輸出操作表達式的寄存器約束被指定為"r"或"g",GCC也不會選擇已經(jīng)被聲明在“破壞描述部分”中的寄存器。比如:
__asm__("movl %0, %%ebx" : : "a"(foo) : "%eax", "%ebx");
此例中,由于輸出操作表達式"a"(foo)的寄存器約束已經(jīng)指定了%eax寄存器,那么再在“破壞描述部分”中指定"%eax"就是非法的。編譯時,GCC會給出編譯錯誤。
? 除了寄存器的內(nèi)容會被改變,內(nèi)存的內(nèi)容也可以被修改。如果一個“匯編語句模板”中的指令對內(nèi)存進行了修改,或者在此內(nèi)嵌匯編出現(xiàn)的地方內(nèi)存內(nèi)容可能發(fā)生改 變,而被改變的內(nèi)存地址你沒有在其輸出操作表達式使用"m"約束,這種情況下你需要在“破壞描述部分”使用字符串"memory"向GCC聲明:“在這 里,內(nèi)存發(fā)生了或可能發(fā)生了改變”。例如:
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)
: "%ecx", "%edi", "memory");
return s;
}
此例實現(xiàn)了標準函數(shù)庫memset,其內(nèi)嵌匯編中的stosb對內(nèi)存進行了改動,而其被修改的內(nèi)存地址s被指定裝入%edi,沒有任何輸出操作表達式使用 了"m"約束,以指定內(nèi)存地址s處的內(nèi)容發(fā)生了改變。所以在其“破壞描述部分”使用"memory"向GCC聲明:內(nèi)存內(nèi)容發(fā)生了變動。
? 如果一個內(nèi)嵌匯編語句的“破壞描述部分”存在"memory",那么GCC會保證在此內(nèi)嵌匯編之前,如果某個內(nèi)存的內(nèi)容被裝入了寄存器,那么在這個內(nèi)嵌匯 編之后,如果需要使用這個內(nèi)存處的內(nèi)容,就會直接到這個內(nèi)存處重新讀取,而不是使用被存放在寄存器中的拷貝。因為這個時候寄存器中的拷貝已經(jīng)很可能和內(nèi)存 處的內(nèi)容不一致了。
? 當一個“匯編語句模板”中包含影響eflags寄存器中的條件標志,那么需要在“破壞描述部分”中使用"cc"來聲明這一點。這些指令包括 adc,div,popfl,btr,bts等等,另外,當包含call指令時,由于你不知道你所call的函數(shù)是否會修改條件標志,為了穩(wěn)妥起見,最好 也使用"cc"。
總結(jié)
- 上一篇: 整理就知识,你会在里面发现新的东西
- 下一篇: Android布局中涉及的一些属性