原文作者 Sandeep.S
英文原文 [https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html]
本文將介紹GCC編譯環(huán)境下,在C語言代碼中嵌入?yún)R編代碼的基本方法。閱讀本文需要您具備80X86匯編語言和C語言的基礎(chǔ)知識(shí)劫拢。為了使中文描述更加清楚自然女轿,翻譯過程中加入了稍許解釋和意譯部分宵喂。
簡介
版權(quán)/反饋/勘誤/感謝等信息揍堕。[^ 1]
[^ 1]:這里信息價(jià)值不大窝剖,沒有翻譯沃饶。具體參加原文:https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html#s1
概要
在討論GCC內(nèi)聯(lián)匯編之前,我們先來搞搞清楚石抡,到底什么是內(nèi)聯(lián)匯編檐嚣?
先看在C語言中,我們可以指定編譯器將一個(gè)函數(shù)代碼直接復(fù)制到調(diào)用其代碼的地方執(zhí)行啰扛。這種函數(shù)調(diào)用方式和默認(rèn)壓棧調(diào)用方式不同嚎京,我們稱這種函數(shù)為內(nèi)聯(lián)函數(shù)嗡贺。內(nèi)聯(lián)函數(shù)看起來很像宏?兩者確實(shí)有許多共同之處鞍帝。
那么诫睬,內(nèi)聯(lián)函數(shù)有哪些優(yōu)點(diǎn)呢?
很明顯帕涌,內(nèi)聯(lián)函數(shù)降低了函數(shù)的調(diào)用開銷:如果多次被調(diào)用的某個(gè)函數(shù)實(shí)參相同摄凡,那么它的返回值一定是相同的,這就給編譯器留下了優(yōu)化空間蚓曼。此時(shí)編譯器完全可以直接用這個(gè)返回值替代這個(gè)函數(shù)亲澡,而不必把該函數(shù)的代碼插入到調(diào)用者的代碼中再去計(jì)算結(jié)果了。如此一來纫版,不但減少了代碼量谷扣,還節(jié)省了計(jì)算資源。指定編譯器將一個(gè)函數(shù)處理為內(nèi)聯(lián)函數(shù)捎琐,我們只要在函數(shù)申明前加上inline關(guān)鍵字即可。
基于對上述內(nèi)聯(lián)函數(shù)的認(rèn)知裹匙,我們大概可以想象出內(nèi)聯(lián)匯編到底是怎么一回事了瑞凑。內(nèi)聯(lián)匯編相當(dāng)于用匯編語句寫成的內(nèi)聯(lián)函數(shù)。它方便概页,快速籽御,對系統(tǒng)編程有著舉足輕重的作用。本文主要就GCC內(nèi)聯(lián)函數(shù)的格式和使用方法展開討論惰匙。在GCC中聲明一個(gè)內(nèi)聯(lián)匯編函數(shù)技掏,我們要用asm這個(gè)關(guān)鍵字。
之所以內(nèi)聯(lián)匯編如此有用项鬼,主要是因?yàn)樗梢圆僮鰿語言變量哑梳,比如可以輸出值到C語言變量。這個(gè)特性使內(nèi)聯(lián)匯編成為匯編代碼和調(diào)用其C程序之間的橋梁绘盟。
GCC匯編格式鸠真。
GCC (GNU Compiler for Linux) 使用AT&T/UNIX匯編語法。所以這篇文章將會(huì)用AT&T匯編格式來寫匯編代碼龄毡。如果你不熟悉AT&T匯編語法也沒有關(guān)系吠卷,下面會(huì)有些簡單的介紹。AT&T和Intel匯編語法差別比較大沦零,二者主要不同之處如下:
- 源操作數(shù)和目的操作數(shù)的方向
AT&T和Intel匯編語法源操作數(shù)和目的操作數(shù)的方向正好相反祭隔。Intel中第一個(gè)操作數(shù)作為目的操作數(shù),第二個(gè)操作數(shù)作為源操作數(shù)路操。而在AT&T中疾渴,第一個(gè)操作數(shù)是源操作數(shù)千贯,第二個(gè)是目的操作數(shù):
OP-code dst src //Intel語法
Op-code src dst //AT&T語法
寄存器命名
在AT&T匯編中, 寄存器名前有%前綴。例如程奠,如果要使用eax丈牢,得寫作: %eax。立即數(shù) (Immediate Operand)
在AT&T語法中瞄沙,立即數(shù)(Immediate Operand)都有'$'前綴己沛。引用的C語言靜態(tài)變量 (static C variables) 也必須放上'$'前綴;
此外距境,在Intel語法中, 16進(jìn)制的常數(shù)是以’h’作為后綴的申尼,但是在AT&T語法中, 是以'0x’作為前綴的。因此垫桂,在AT&T語法中师幕,一個(gè)16進(jìn)制常數(shù)的寫法是:首先以$開頭接著是0x,最后是常數(shù)本身诬滩。-
操作數(shù)大小
在AT&T語法中霹粥,操作符的最后一個(gè)字符決定著操作數(shù)訪問內(nèi)存的長度:以’b’, 'w'和 'l'為后綴指明內(nèi)存訪問長度是 byte(8-bit), word(16-bit)還是long(32-bit)。而Intel語法在操作數(shù)前加上'byte ptr', 'word ptr'和'dword ptr'的內(nèi)存操作符來達(dá)到相同目的疼鸟。
因此 Intel匯編寫法:
mov al, byte ptr foo
用AT&T語法寫就是:
movb foo, %al
-
內(nèi)存操作數(shù)
在Intel語法中猜惋,基址寄存器是放在方括號(hào)‘[]’中的堪遂,但AT&T是放在小括弧’()’內(nèi)的。
因此,在Intel語法中恩掷,一個(gè)間接內(nèi)存尋址是這么寫的:
section:[base + index * scale + disp]蚓土。
而在AT&T中則應(yīng)該寫成這樣:
section:disp(base, index, scale)
此外對于AT&T匯編肖粮,當(dāng)一個(gè)常數(shù)被用作disp或者scale時(shí)革半,不需要'$'前綴。這點(diǎn)需要記住洼怔。
以上就是AT&T和Intel匯編語法的一些主要不同點(diǎn)署惯。這只是一小部分,具體內(nèi)容需要參考GNU匯編文檔镣隶。為了更好理解這些不同泽台,這里給出一些實(shí)例作為對照:
Intel Code | AT&T Code |
---|---|
mov eax,1 | movl $1,%eax |
mov ebx,0ffh | movl $0xff,%ebx |
int 80h | int $0x80 |
mov ebx, eax | movl %eax, %ebx |
mov eax,[ecx] | movl (%ecx),%eax |
mov eax,[ebx+3] | movl 3(%ebx),%eax |
mov eax,[ebx+20h] | movl 0x20(%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 |
基本內(nèi)聯(lián)匯編 (Basic Inline)
基本內(nèi)聯(lián)匯編格式比較直觀,可以直接這樣寫:
asm("assembly code");
例如:
asm("movl %ecx, %eax"); /* 把 ecx 內(nèi)容移動(dòng)到 eax */ __asm__("movb %bh , (%eax)"); /* 把bh中一個(gè)字節(jié)的內(nèi)容移動(dòng)到eax指向的內(nèi)存 */
你可能注意到了這里使用了兩個(gè)不同的關(guān)鍵字 asm 和 __asm__矾缓。這兩個(gè)關(guān)鍵字都可以使用怀酷。不過當(dāng)遇到asm關(guān)鍵字與程序其他變量有沖突的時(shí)候就必須用__asm__了。如果內(nèi)聯(lián)匯編有多條指令嗜闻,則每行要加上雙引號(hào)蜕依,并且該行要以\n\t結(jié)尾。這是因?yàn)镚CC會(huì)將每行指令作為一個(gè)字符串傳給as(GAS),使用換行和TAB可以將正確且格式良好的代碼行傳遞給匯編器样眠。
舉個(gè)例子:
__asm__ ( "movl %eax, %ebx\n\t"
"movl $56, %esi\n\t"
"movl %ecx, $label(%edx,%ebx,$4)\n\t"
"movb %ah, (%ebx)");
如果在內(nèi)聯(lián)代碼中操作了一些寄存器友瘤,比如你修改了寄存器內(nèi)容(而之后也沒有進(jìn)行還原操作),程序很可能會(huì)產(chǎn)生一些難以預(yù)料的情況檐束。因?yàn)榇藭r(shí)GCC并不知道你已經(jīng)將寄存器內(nèi)容修改了辫秧。這點(diǎn)尤其是在編譯器對代碼進(jìn)行了一些優(yōu)化的情況下而導(dǎo)致問題。因?yàn)榫幾g器注意不到寄存器內(nèi)容已經(jīng)被改掉被丧,程序?qū)?dāng)作它沒有被修改過而繼續(xù)執(zhí)行盟戏。所以此時(shí)我們盡量不要使用這些會(huì)產(chǎn)生附加影響的操作,或者當(dāng)我們退出的時(shí)候還原這些操作甥桂。否則很可能會(huì)造成程序崩潰柿究。可是如果我們必須要這樣操作該怎么辦呢黄选?我們可以通過下面的討論的擴(kuò)展內(nèi)聯(lián)匯編進(jìn)行蝇摸。
擴(kuò)展內(nèi)聯(lián)匯編 (Extended Asm)
前面討論的基本內(nèi)聯(lián)匯編只涉及到嵌入?yún)R編指令,而在擴(kuò)展形式中办陷,我們還可以指定操作數(shù)貌夕,并且可以選擇輸入輸出寄存器,以及指明要修改的寄存器列表民镜。對于要訪問的寄存器啡专,并不一定要要顯式指明,也可以留給GCC自己去選擇殃恒,這可能讓GCC更好去優(yōu)化代碼。擴(kuò)展內(nèi)聯(lián)匯編格式如下:
asm ( assembler template
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);
其中assembler template為匯編指令部分辱揭。括號(hào)內(nèi)的操作數(shù)都是C語言表達(dá)式中常量字符串离唐。不同部分之間使用冒號(hào)分隔。相同部分語句中的每個(gè)小部分用逗號(hào)分隔问窃。最多可以指定10個(gè)操作數(shù)亥鬓,不過可能有的計(jì)算機(jī)平臺(tái)有額外的文檔說明可以使用超過10個(gè)操作數(shù)。
此外域庇,如果沒有輸出部分但是有輸入部分嵌戈,我們還得保留輸出部分前面的冒號(hào)。就像下面這樣:
asm ( "cld\n\t"
"rep\n\t"
"stosl"
: /* no output registers */
: "c" (count), "a" (fill_value), "D" (dest)
: "%ecx", "%edi"
);
上述代碼做了些什么呢听皿?它主要是循環(huán)count次把fill_value的值到填充到edi寄存器指定的內(nèi)存位置熟呛。并且告訴GCC,寄存器ecx[^ 2]和edi中的內(nèi)容可能已經(jīng)被改變了尉姨。 為了有一個(gè)更清晰的理解庵朝,我們再來看一個(gè)例子:
[^ 2]:原文有誤,原文是這里是eax。
int a=10, b;
asm ( "movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax" /* clobbered register */
);
上面代碼實(shí)現(xiàn)的功能就是用匯編代碼把a(bǔ)的值賦給b九府。值得注意的幾點(diǎn)有:
- “b”是輸出操作數(shù)椎瘟,用%0來訪問,”a”是輸入操作數(shù)侄旬,用%1來訪問肺蔚。
- “r” 是一個(gè)constraint, 關(guān)于constraint后面有詳細(xì)的介紹。這里我們只要記住這里”r”的意思就是讓GCC自己去選擇一個(gè)寄存器去存儲(chǔ)變量a儡羔。輸出部分constraint前必須要有個(gè) ”=”修飾宣羊,用來說明是一個(gè)這是一個(gè)輸出操作數(shù),并且是只寫(write only)的笔链。
- 你可能已經(jīng)注意到段只,有的寄存器名字前面用了”%%”,這是用來讓GCC區(qū)分操作數(shù)和寄存器的:操作數(shù)已經(jīng)用了一個(gè)%作為前綴鉴扫,寄存器只能用“%%”做前綴了赞枕。
- 第三個(gè)冒號(hào)后面的clobbered register部分有個(gè)%eax,意思是內(nèi)聯(lián)匯編代碼中會(huì)改變寄存器eax的內(nèi)容坪创,如此一來GCC在調(diào)用內(nèi)聯(lián)匯編前就不會(huì)依賴保存在寄存器eax中的內(nèi)容了炕婶。
當(dāng)這段代碼執(zhí)行結(jié)束后,變量”b”的值將會(huì)被改寫莱预,因?yàn)樗潜恢付ㄗ鳛檩敵霾僮鲾?shù)的柠掂。這里可以看出在“asm”內(nèi)部對b的改動(dòng)將影響到asm外了,正如之前所說的內(nèi)聯(lián)匯編起到橋梁作用依沮。
下面我們將對擴(kuò)展內(nèi)聯(lián)匯編各個(gè)部分分別進(jìn)行詳細(xì)的討論涯贞。
匯編模板
匯編模板部分就是嵌入在C程序中的匯編指令,格式如下:
- 每條指令放在一個(gè)雙引號(hào)內(nèi)危喉,或者將所有的指令都放著一個(gè)雙引號(hào)內(nèi)宋渔。
- 每條指令都要包含一個(gè)分隔符。合法的分隔符是換行符(\n)或者分號(hào)辜限。用換行符的時(shí)候通常后面放一個(gè)制表符\t皇拣。對此前文已經(jīng)有所說明。
- 訪問C語言變量用%0,%1…等等薄嫡。
操作數(shù)
”asm”內(nèi)部使用C語言字符串作為操作數(shù)氧急。操作數(shù)都要放在雙引號(hào)中。對于輸出操作數(shù)毫深,還要用“=”修飾吩坝。constraint和修飾都放在雙引號(hào)內(nèi)。之后是C表達(dá)式了哑蔫。就像下面這樣:
"constraint" (C expression) //"=r"(result)
對于輸出操作數(shù)一定要用 “=“修飾钾恢。 constraint主要用來指定操作數(shù)的尋址類型 (內(nèi)存尋址或寄存器尋址)手素,也用來指明使用哪個(gè)寄存器。
如果有多個(gè)操作數(shù)瘩蚪,使用逗號(hào)隔開泉懦。
在匯編模板部分,我們按順序用數(shù)字去引用操作數(shù)疹瘦,引用規(guī)則如下:
如果總共有n個(gè)操作數(shù)(包括輸入輸出操作數(shù))崩哩,那么第一個(gè)輸出操作引用數(shù)字為0,依次遞增言沐,然后最后一個(gè)操作數(shù)是n-1邓嘹。關(guān)于操作數(shù)數(shù)量限制參見前面的章節(jié)。
輸出操作數(shù)表達(dá)式必須是左值险胰,輸入操作數(shù)沒有這個(gè)限制汹押。注意這里可以使表達(dá)式,不僅僅指一個(gè)變量起便。當(dāng)編譯器不知道有這個(gè)機(jī)器指令的時(shí)候(比如新CPU指令出來的時(shí)候棚贾,編譯器還沒有支持該指令),擴(kuò)展匯編形式就能發(fā)揮其用武之地了榆综。如果輸出表達(dá)式不能直接尋址(比如是[bit-field]), constraint就必須指定一個(gè)寄存器妙痹。這種情況下,GCC將使用寄存器作為asm的輸出鼻疮。然后保存這個(gè)寄存器的值到輸出表達(dá)式中怯伊。
如前文所描述,一般輸出操作數(shù)必須是只寫 (write only)的判沟;GCC將認(rèn)為在這條指令之前耿芹,保存在這種操作數(shù)中的值已經(jīng)過期和不再需要了。當(dāng)然也支持輸入輸出類型或者可讀可寫類型的操作數(shù)挪哄。
現(xiàn)在我們來看一些例子:
要求把一個(gè)數(shù)字乘以5吧秕,我們可以使用匯編指令lea來實(shí)現(xiàn),具體方法如下:
asm ( "leal (%1,%1,4), %0"
: "=r" (five_times_x)
: "r" (x)
);
這里輸入操作數(shù)是 ‘x’中燥,因?yàn)闆]有指定具體要使用那個(gè)寄存器寇甸,GCC會(huì)自己選擇合適的輸入輸出寄存器塘偎。我們也可以修改constraint部分內(nèi)容疗涉,讓GCC固定使用同一個(gè)寄存器,具體方法如下:
asm( "lea (%0,%0,4),%0"
: "=r" (five_times_x)
: "0" (x)
);
上面例子中指定GCC始終使用在相同的寄存器來處理輸入輸出操作數(shù)吟秩。當(dāng)然這時(shí)我們也不知道GCC具體使那個(gè)寄存器咱扣,如果需要的話我們也可以像這樣指定一個(gè):
asm ( "leal (%%ecx,%%ecx,4), %%ecx"
: "=c" (x)
: "c" (x)
);
上面的三個(gè)例子中,我都沒有在clobber list部分指定何寄存器涵防。為什么闹伪?前兩個(gè)例子中,因?yàn)橹付℅CC自己選擇合適的寄存器,并且GCC知道會(huì)改寫什么偏瓤。第三個(gè)例子中我們也沒有必要把ecx放在clobber list中是因?yàn)镚CC知道x將存入其中杀怠,GCC完全知道ecx的值。所以我們也不用寫在clobber list中厅克。
Clobber List
如果某個(gè)指令改變了某個(gè)寄存器的值赔退,我們就必須在asm中第三個(gè)冒號(hào)后的Clobber List中標(biāo)示出該寄存器。為的是通知GCC证舟,讓其不再假定之前存入這些寄存器中的值依然合法硕旗。輸入輸出寄存器不用放Clobber List中(看上面就是個(gè)例子),因?yàn)镚CC能知道asm將使用這些寄存器女责。(因?yàn)樗鼈円呀?jīng)顯式被指定輸入輸出標(biāo)出在輸入輸出部分) 漆枚。其他使用到的寄存器,無論是顯示還是隱式的使用抵知,必須在clobbered list中標(biāo)明墙基。
如果指令中以無法預(yù)料的形式修改了內(nèi)存值,需要在clobbered list中加上”memory”辛藻。從而使得GCC不去緩存在這些內(nèi)存值碘橘。此外,如果要改變沒有被列在輸入和出部分的內(nèi)存內(nèi)容時(shí)吱肌,需要加上volatile關(guān)鍵字說明痘拆。clobbered list中列出的寄存器可以被多次讀寫。
來看一個(gè)內(nèi)聯(lián)匯編實(shí)現(xiàn)乘法的例子氮墨,這里內(nèi)聯(lián)匯編調(diào)用函數(shù)_foo纺蛆,并且接受存在eax和ecx值作為參數(shù):
asm( "movl %0,%%eax;
movl %1,%%ecx;
call _foo"
: /*no outputs*/
: "g" (from), "g" (to)
: "eax", "ecx"
);
Volatile
如果你熟悉內(nèi)核代碼或者一些類似優(yōu)秀的代碼,你一定見過很多在asm或者asm后的函數(shù)聲明前加了volatile 或者volatile规揪。前面已經(jīng)討論了asm和asm的用途桥氏,那volatile有什么用途呢?
如果我們要求匯編代碼必須在被放置的位置執(zhí)行(例如不能被循環(huán)優(yōu)化而移出循環(huán)),我們就要在asm之后的“()”前猛铅,放一個(gè)volatile關(guān)鍵字字支。 這樣可以禁止這些代碼被移動(dòng)或刪除,像這樣聲明:
asm volatile ( ... : ... : ... : ...);
同樣奸忽,如果擔(dān)心volatile有變量沖突堕伪,可以使用__volatile__關(guān)鍵字。
如果匯編代碼只是做一些運(yùn)算而沒有什么附加影響的時(shí)候最好不要使用volatile修飾栗菜。不用volatile能給GCC留下優(yōu)化代碼的空間欠雌。
在“常用技巧”章節(jié)中的代碼示例里有更多的關(guān)于volatile的使用詳情。[^ 3]
[^ 3]:原文有誤: 原文是clobber-list疙筹,這樣應(yīng)該是volatile
constraints詳解
你可能已經(jīng)感到我們之前經(jīng)常提到的constraint是個(gè)很重要的內(nèi)容了富俄。不過之前我們并沒有過多的討論禁炒。constraint中可以指明一個(gè)操作數(shù)是否在寄存器中,在哪個(gè)寄存器中霍比;可以指明操作數(shù)是否是內(nèi)存引用幕袱,如何尋址;可以說明操作數(shù)是否是立即數(shù)常量悠瞬,和其可能是的值(或值范圍)凹蜂。
常用constraints
雖然constraints有很多,但常用的并不多阁危。下面我們就來看看這些常用的constraints玛痊。
-
寄存器操作數(shù)constraints: r
如果操作數(shù)指定了這個(gè)constraints,操作數(shù)將被存儲(chǔ)在通用寄存器中狂打±奚罚看下面的例子:
asm ( "movl %%eax, %0" : "=r" (myval));
上面變量myval會(huì)被被保存在一個(gè)由GCC自己選擇的寄存器中,eax中的值被拷貝到這個(gè)寄存器中去趴乡,并且在內(nèi)存中的myval的值也會(huì)按這個(gè)寄存器值被更新对省。當(dāng)constraints ”r” 被指定時(shí),GCC可能會(huì)在任何一個(gè)可用的通用寄存器中保存這個(gè)值晾捏。當(dāng)然蒿涎,你也可以指定具體使用那個(gè)寄存器,用下表所列出的constraints:
r | Register(s) |
---|---|
a | %eax, %ax, %al |
b | %ebx, %bx, %bl |
c | %ecx, %cx, %cl |
d | %edx, %dx, %adl |
S | %esi, %si |
D | %edi, %di |
內(nèi)存操作數(shù)constraint: m
當(dāng)操作數(shù)在內(nèi)存中時(shí)惦辛,任何對其操作會(huì)直接在內(nèi)存中進(jìn)行劳秋。與寄存器constraint不同的是:指定寄存器constraint時(shí),內(nèi)存操作時(shí)先把值存在一個(gè)寄存器中胖齐,修改后再將該值寫回到該內(nèi)存中去玻淑。寄存器constraint通常只用于必要的匯編指令,或者用于能明顯加快操作速度的情況呀伙,因?yàn)閮?nèi)存constraint能提升C語言變量更新效率补履,完全沒必要通過一個(gè)寄存器來中轉(zhuǎn)。下面這個(gè)例子中剿另,sidt的值會(huì)被直接存儲(chǔ)到loc所指向的內(nèi)存:
asm (“sidt” %0” : : “m”(loc) );
-
匹配constraint
在某些情況下箫锤,一個(gè)變量可能被用來傳遞輸入也用來保存輸出。這種情況下我們需要用到匹配constraint雨女。
asm (“incl %0” :”=a”(var) : “0”(var) );
在之前章節(jié)中我們已經(jīng)看過類似的例子谚攒。上面的例子中,%eax被用來傳遞輸入也用來保存輸出戚篙。輸入變量先被讀入eax中五鲫,incl執(zhí)行之后溺职,%eax被更新并且保存到變量var中岔擂。這里的constraint ”0”就是指定使用和第一個(gè)輸出相同的寄存器位喂,即輸入變量指定放在eax中。這種constraint可以使用在如下場景:
- 輸入值從一個(gè)變量讀入, 這個(gè)變量將被修改并且修改過的值要寫回同一個(gè)變量乱灵;
- 沒有必要把輸入和輸出操作數(shù)分開塑崖。
使用匹配constraint最重要的好處是可以更高效地使用變量寄存器。
其他可能用到的constraint有:
- “m”: 使用一個(gè)內(nèi)存操作數(shù)痛倚,內(nèi)存地址可以是機(jī)器支持的范圍內(nèi)规婆。
- “o”: 使用一個(gè)內(nèi)存操作數(shù),但是要求內(nèi)存地址范圍在在同一段內(nèi)蝉稳。例如抒蚜,加上一個(gè)小的偏移量來形成一個(gè)可用的地址。
- “V”: 內(nèi)存操作數(shù)耘戚,但是不在同一個(gè)段內(nèi)嗡髓。換句話說,就是使用除了”o” 以外的”m”的所有的情況。
- “i”: 使用一個(gè)立即整數(shù)操作數(shù)(值固定)收津;也包含僅在編譯時(shí)才能確定其值的符號(hào)常量饿这。
- “n”: 一個(gè)確定值的立即數(shù)。很多系統(tǒng)不支持匯編常數(shù)操作數(shù)小于一個(gè)字(word)的長度的情況撞秋。這時(shí)候使用n就比使用i好长捧。
- “g”: 除了通用寄存器以外的任何寄存器,內(nèi)存和立即整數(shù)吻贿。
這里是一些x86特有的constraint:
- ”r” : Register operand constraint, look table given above.
- ”q” : Registers a, b, c or d.
- ”I” : Constant in range 0 to 31 (for 32-bit shifts).
- ”J” : Constant in range 0 to 63 (for 64-bit shifts).
- ”K” : 0xff.
- ”L” : 0xffff.
- ”M” : 0, 1, 2, or 3 (shifts for lea instruction).
- ”N” : Constant in range 0 to 255 (for out instruction).
- ”f” : Floating point register
- ”t” : First (top of stack) floating point register
- ”u” : Second floating point register
- ”A” : Specifies the “a” or “d” registers. This is primarily useful for 64-bit integer values intended to be returned with the “d” register holding the most significant bits and the “a” register holding the least significant bits.
constraint修飾符(Constraint Modifiers)
在使用constraint的時(shí)候串结,為了更精確的控制約束,GCC提供了一些修飾符舅列,常用的修飾符有:
- “=” 指明這個(gè)操作數(shù)是只寫的奉芦;之前保存在其中的值將被廢棄而被輸出值所代替。
- “&” 指明這個(gè)操作事數(shù)是一個(gè)會(huì)在使用之前被修改的操作數(shù)剧蹂,這個(gè)操作數(shù)將在輸入指令用過輸入操作數(shù)之前被修改声功。因此,該操作數(shù)不能被放在一個(gè)被用作輸入操作數(shù)的寄存器或者內(nèi)存處宠叼。只有在該操作數(shù)被寫入之前完成輸入指令的情況下先巴,可以被綁定在該操作數(shù)上。[^ 4]
[^ 4]:因?yàn)樽g者對此處內(nèi)容不了解冒冬,故翻譯的不好伸蚯。本段的英文原文是 “&” Means that this operand is an earlyclobber operand, which is modified before the instruction is finished using the input operands. Therefore, this operand may not lie in a register that is used as an input operand or as part of any memory address. An input operand can be tied to an earlyclobber operand if its only use as an input occurs before the early result is written.
對于cosntraint的解釋還遠(yuǎn)遠(yuǎn)沒完。代碼本身是理解內(nèi)聯(lián)匯編最好的老師简烤。下一小結(jié)中我們就來看一些代碼示例剂邮。通過這些示例我們能學(xué)到更多關(guān)于clobber-list和constraint使用情況。
常用代碼示例
到目前為止横侦,GCC內(nèi)聯(lián)匯編基礎(chǔ)知識(shí)就已經(jīng)講完了挥萌。接下來讓我們通過一些簡單的例子來鞏固我們所學(xué)到到知識(shí)绰姻。內(nèi)聯(lián)匯編函數(shù)可以很方便的用宏的形式來編寫,linux內(nèi)核代碼中有很多這樣的實(shí)例(在/usr/src/linux/asm/*.h)引瀑。
- 我們從一個(gè)簡單的例子看起狂芋。我們來寫一個(gè)把兩個(gè)數(shù)字加起來的一個(gè)程序。
int main(void) { int foo = 10, bar = 15; __asm__ __volatile__ ( "addl %%ebx, %%eax" : ”=a”(foo) : ”a”(foo), “b”(bar) ); prinft(“foo+bar=%d\n”, foo); return 0; }
在上面代碼中憨栽,我們強(qiáng)制讓GCC將foo的值存儲(chǔ)在%eax中帜矾,將bar的值存儲(chǔ)在%ebx中,并且讓輸出值放在%eax中屑柔。其中“=”指明這是一個(gè)輸出寄存器屡萤。我們再來看看另外一個(gè)把兩個(gè)數(shù)相加的代碼段:
```
__asm__ __volatile__ (
" lock \n"
" addl %1,%0; \n"
: "=m" (my_var)
: "ir" (my_int), "m" (my_var)
: /* no clobber-list */
);
上面代碼是一個(gè)原子加法操作。要移除該原子操作可以刪除lock指令掸宛。在輸出部分“=m”指出直接輸出到內(nèi)存my_var灭衷。類似的,”ir”是指my_int是一個(gè)整型數(shù)并且要保存到一個(gè)寄存器中(可以參考上面關(guān)于constraint的列表)旁涤。這里clobber list中沒有指定任何寄存器翔曲。
- 我們在一些寄存器或變量上來執(zhí)行一些操作來對比下它們的值。
__asm__ __volatile__ ( "decl %0; sete %1" : "=m" (my_var), "=q" (cond) : "m" (my_var) : "memory" );
上面的程序?qū)my_var~減一并且如果減一的最終結(jié)果為零就將cond置位劈愚。我們可以在匯編語句之前加上~”lock;\n\t”~讓其變成原子操作瞳遍。
類似的,我們可以用”incl %0”替換”decl %0”來增加~my_var~的值菌羽。
這里值得注意的幾點(diǎn)有:
- my_var是存在內(nèi)存中的變量掠械;
- cond存在通用寄存器中(eax,ebx,ecx,edx),因?yàn)橛邢拗茥l件”=q”注祖;
- clobber list中指定了“memory”猾蒂,說明代碼將改變內(nèi)存值。
3. 如何設(shè)置和清除寄存器中的某一位是晨?來看看下面這個(gè)技巧肚菠。
```
__asm__ __volatile__( “btsl %1, %0”
: “=m” (ADDR)
: “Ir” (pos)
: “cc”
);
上面例子中變量ADDR(一個(gè)內(nèi)存變量)的’pos’位置值被設(shè)置成了1。我們可以使用btrl指令來清除由btsl設(shè)置的位罩缴。pos變量的限定符constraint為”Ir”說明pos放在寄存器中蚊逢,并且取值范圍是0-31(I是一個(gè)x86相關(guān)constraint)。因此我們可以設(shè)置或者清除ADDR變量中從第0到第31位的值箫章。因?yàn)檫@個(gè)操作涉會(huì)改變相關(guān)寄存器的內(nèi)容烙荷,因此我們加上”cc”在clobberlist中。
- 現(xiàn)在我再來看一些更加復(fù)雜但是有用的函數(shù)檬寂。字符串拷貝函數(shù):
static inline char* strcpy (char* dest, const char* src) { int d0, d1, d2; __asm__ __volatile__( "1:/tlodsb\n\t" "stosb\n\t" "testb %%al,%%al\n\t" "jne 1b" : "=&S" (d0), "=&D" (d1), "=&a" (d2) : "0" (src),"1" (dest) : "memory"); return dest; }
上面代碼的源地址存在esi寄存器中终抽,目的地址存在EDI中。接著開始復(fù)制操作,直到遇到0結(jié)束昼伴。約束符constraint 為”&S”,”&D”,”&a”匾旭,指定了使用的寄存器為esi,edi和eax亩码。很明顯這些寄存器是clobber寄存器,因?yàn)樗鼈兊膬?nèi)容會(huì)在函數(shù)執(zhí)行后被改變野瘦。此外我們也能看出為什么memory被放在clobber list中描沟,因?yàn)閐0, d1, d2被更新了。
我們再來看一個(gè)類似的函數(shù)鞭光。該函數(shù)用來移動(dòng)一塊雙字(double word)吏廉。注意這個(gè)函數(shù)是用宏來定義的。
`#define mov_blk(src, dest, numwords) \
__asm__ __volatile__ ( \
"cld\n\t" \
"rep\n\t" \
"movsl" \
: \
: "S" (src), "D" (dest), "c" (numwords) \
: "%ecx", "%esi", "%edi" \
)`
該函數(shù)沒有輸出惰许,但是塊移動(dòng)過程導(dǎo)致ecx, esi, edi內(nèi)容被改變席覆,所以我們必須把它們放在clobber list中。
5. 在Linux中汹买,系統(tǒng)調(diào)用是用GCC內(nèi)聯(lián)匯編的形式實(shí)現(xiàn)的佩伤。讓我們來看看一個(gè)系統(tǒng)調(diào)用是如何實(shí)現(xiàn)的。所有的系統(tǒng)調(diào)用都是用宏來寫的 (在linux/unistd.h)晦毙。例如生巡,一個(gè)帶三個(gè)參數(shù)的系統(tǒng)調(diào)用的定義如下:
```
#define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \
type name(type1 arg1,type2 arg2,type3 arg3) \
{ \
long __res; \
__asm__ volatile ( "int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long) arg2)), \
"d" ((long)(arg3))); \
__syscall_return(type,__res); \
}`
所有帶三個(gè)參數(shù)的系統(tǒng)調(diào)用都會(huì)用上面這個(gè)宏來執(zhí)行。這段代碼中见妒,系統(tǒng)調(diào)用號(hào)放在eax中孤荣,參數(shù)分別放在ebx,ecx须揣,edx中盐股,最后用”int 0x80”執(zhí)行系統(tǒng)調(diào)用。返回值放在eax中耻卡。
Linux中所有的系統(tǒng)調(diào)用都是用上面類似的方式實(shí)現(xiàn)的疯汁。比如Exit系統(tǒng)調(diào)用,它是帶單個(gè)參數(shù)的系統(tǒng)調(diào)用卵酪。實(shí)現(xiàn)的代碼如下:
`{
asm("movl $1,%%eax; /* SYS_exit is 1 */
xorl %%ebx,%%ebx; /* Argument is in ebx, it is 0 */
int $0x80" /* Enter kernel mode */
);
}
Exit的系統(tǒng)調(diào)用號(hào)是1涛目,參數(shù)為0,所以我們把1放到eax中并且把0放到ebx中凛澎,最后通過調(diào)用int $0x80霹肝,exit(0)就被執(zhí)行了。這就是exit函數(shù)的全部塑煎。
結(jié)束語
這篇文章講述了GCC內(nèi)聯(lián)匯編的基礎(chǔ)知識(shí)沫换。一旦你理解了這些基礎(chǔ)內(nèi)容,自己再一步步的看下去就沒有什么困難了。通過這些例子可以更好的幫助我們理解內(nèi)聯(lián)匯編的常用特性讯赏。
GCC內(nèi)聯(lián)匯編是一個(gè)很大的主題垮兑,這片文章的討論還遠(yuǎn)遠(yuǎn)不夠。本篇文章我們提到的大多數(shù)語法都可以在官方文檔GNU Assembler中看到漱挎。完整的constraint也可以在GCC官方文檔中找到系枪。
Linux內(nèi)核大范圍內(nèi)使用了GCC內(nèi)聯(lián)匯編,我們可以從中找到各種各樣的例子來學(xué)習(xí)磕谅。這對我們也很有幫助私爷。
如果你找到任何低級(jí)的排版打字錯(cuò)誤或者過期的內(nèi)容,請聯(lián)系我膊夹。
參考文獻(xiàn)
- [Brennan’s Guide to Inline Assembly]
- [Using Assembly Language in Linux]
- Using as, The GNU Assembler(fn)
- Using and Porting the GNU Compiler Collection (GCC) (fn)
- [Linux Kernel Source]