一、基本 asm 格式
1. 語(yǔ)法規(guī)則
2.test1.c 插入空指令
3. test2.c操作全局變量
4. test3.c 嘗試操作局部變量
二、擴(kuò)展 asm 格式
1. 指令格式
2.輸出和輸入操作數(shù)列表
3.test4.c通過(guò)寄存器操作局部變量
4. test5.c聲明使用的寄存器
三、使用占位符來(lái)代替寄存器名稱(chēng)
1. test6.c使用占位符代替寄存器名
2. test7.c 給寄存器起別名
四、使用內(nèi)存地址
1.test8.c 使用內(nèi)存地址來(lái)操作數(shù)據(jù)
五、總結(jié)
在 Linux 代碼中,經(jīng)??梢钥吹皆?C 代碼中,嵌入部分匯編代碼,這些代碼要么是與硬件體系相關(guān)的,要么是對(duì)性能有關(guān)鍵影響的。
在很久以前,我特別懼怕內(nèi)嵌匯編代碼,直到后來(lái)把匯編部分的短板補(bǔ)上之后,才徹底終結(jié)這種心理。
也許你在工作中,幾乎不會(huì)涉及到內(nèi)嵌匯編代碼的工作,但是一旦進(jìn)入到系統(tǒng)的底層,或者需要對(duì)時(shí)間關(guān)鍵場(chǎng)景進(jìn)行優(yōu)化,這個(gè)時(shí)候你的知識(shí)儲(chǔ)備就發(fā)揮重要作用了!
這篇文章,我們就來(lái)詳細(xì)聊一聊在 C 語(yǔ)言中,如何通過(guò)asm 關(guān)鍵字來(lái)嵌入?yún)R編語(yǔ)言代碼,文中的 8 個(gè)示例代碼從簡(jiǎn)單到復(fù)雜,逐步深入地介紹內(nèi)聯(lián)匯編的關(guān)鍵語(yǔ)法規(guī)則。
希望這篇文章能夠成為你進(jìn)階高手路上的墊腳石!
PS:
示例代碼中使用的是 Linux 系統(tǒng)中 AT&T 匯編語(yǔ)法;
文章中的 8 個(gè)示例代碼,可以在公眾號(hào)后臺(tái)回復(fù)【426】,即可收到下載地址;
一、基本 asm 格式
gcc 編譯器支持2 種形式的內(nèi)聯(lián) asm 代碼:
基本 asm 格式:不支持操作數(shù);
擴(kuò)展 asm 格式:支持操作數(shù);
1. 語(yǔ)法規(guī)則
asm [volatile] ("匯編指令")
所有指令,必須用雙引號(hào)包裹起來(lái);
超過(guò)一條指令,必須用 分隔符進(jìn)行分割,為了排版,一般會(huì)加上 ;
多條匯編指令,可以寫(xiě)在一行,也可以寫(xiě)在多行;
關(guān)鍵字 asm 可以使用asm來(lái)替換;
volatile 是可選的,編譯器有可能對(duì)匯編代碼進(jìn)行優(yōu)化,使用 volatile 關(guān)鍵字之后,告訴編譯器不要優(yōu)化手寫(xiě)的內(nèi)聯(lián)匯編代碼。
2. test1.c 插入空指令
#includeint main() { asm ("nop"); printf("hello "); asm ("nop nop " "nop"); return 0; }
注意:C語(yǔ)言中會(huì)自動(dòng)把兩個(gè)連續(xù)的字符串字面量拼接成一個(gè),所以"nop nop " "nop"這兩個(gè)字符串會(huì)自動(dòng)拼接成一個(gè)字符串。
生成匯編代碼指令:
gcc -m32 -S -o test1.s test1.c
test1.s 中內(nèi)容如下(只貼出了內(nèi)聯(lián)匯編代碼相關(guān)部分的代碼):
#APP # 5 "test1.c" 1 nop # 0 "" 2 #NO_APP // 這里是 printf 語(yǔ)句生成的代碼。 #APP # 7 "test1.c" 1 nop nop nop # 0 "" 2 #NO_APP
可以看到,內(nèi)聯(lián)匯編代碼被兩個(gè)注釋(#APP ... #NO_APP)包裹起來(lái)。在源碼中嵌入了兩個(gè)匯編代碼,因此可以看到 gcc 編譯器生成的匯編代碼中包含了這兩部分代碼。
這 2 部分嵌入的匯編代碼都是空指令 nop,沒(méi)有什么意義。
3. test2.c 操作全局變量
在 C 代碼中嵌入?yún)R編指令,目的是用來(lái)計(jì)算,或者執(zhí)行一定的功能,下面我們就來(lái)看一下,如何在內(nèi)聯(lián)匯編指令中,操作全局變量。
#includeint a = 1; int b = 2; int c; int main() { asm volatile ("movl a, %eax " "addl b, %eax " "movl %eax, c"); printf("c = %d ", c); return 0; }
關(guān)于匯編指令中編譯器的基本知識(shí):
eax, ebx 都是 x86 平臺(tái)中的寄存器(32位),在基本asm格式中,寄存器的前面必須加上百分號(hào)%。
32 位的寄存器eax可以當(dāng)做 16 位來(lái)使用(ax),或者當(dāng)做 8 位來(lái)使用(ah, al),本文只會(huì)按照 32 位來(lái)使用。
代碼說(shuō)明:
movl a, %eax // 把變量a的值復(fù)制到 %eax 寄存器中;
addl b, %eax // 把變量 b 的值 與 %eax 寄存器中的值(a)相加,結(jié)果放在 %eax 寄存器中;
movl %eax, c // 把 %eax 寄存器中的值復(fù)制到變量 c 中;
生成匯編代碼指令:
gcc -m32 -S -o test2.s test2.c
test2.s 內(nèi)容如下(只貼出與內(nèi)聯(lián)匯編代碼相關(guān)部分):
#APP
# 9 "test2.c" 1 movl a, %eax addl b, %eax movl %eax, c # 0 "" 2 #NO_APP
可以看到,在內(nèi)聯(lián)匯編代碼中,可以直接使用全局變量 a, b 的名稱(chēng)來(lái)操作。執(zhí)行 test2,可以得到正確的結(jié)果。
思考一個(gè)問(wèn)題:為什么在匯編代碼中,可以使用變量a, b, c?
查看 test2.s 中內(nèi)聯(lián)匯編代碼之前的部分,可以看到:
.file"test2.c" .globla .data .align 4 .typea, @object .sizea, 4 a: .long1 .globlb .align 4 .typeb, @object .sizeb, 4 b: .long2 .commc,4,4
變量 a, b 被 .globl 修飾,c 被 .comm 修飾,相當(dāng)于是把它們導(dǎo)出為全局的,所以可以在匯編代碼中使用。
那么問(wèn)題來(lái)了:如果是一個(gè)局部變量,在匯編代代碼中就不會(huì)用 .globl 導(dǎo)出,此時(shí)在內(nèi)聯(lián)匯編指令中,還可以直接使用嗎?
眼見(jiàn)為實(shí),我們把這 3 個(gè)變量放到 main 函數(shù)的內(nèi)部,作為局部變量來(lái)試一下。
4. test3.c 嘗試操作局部變量
#includeint main() { int a = 1; int b = 2; int c; asm("movl a, %eax " "addl b, %eax " "movl %eax, c"); printf("c = %d ", c); return 0; }
生成匯編代碼指令:
gcc -m32 -S -o test3.s test3.c
在 test3.s 中可以看到?jīng)]有a, b, c 的導(dǎo)出符號(hào),a 和 b 沒(méi)有其他地方使用,因此直接把他們的數(shù)值復(fù)制到??臻g中了:
movl$1, -20(%ebp) movl$2, -16(%ebp)
我們來(lái)嘗試編譯成可執(zhí)行程序:
$ gcc -m32 -o test3 test3.c /tmp/ccuY0TOB.o: In function `main': test3.c undefined reference to `a' test3.c undefined reference to `b' test3.c undefined reference to `c' collect2: error: ld returned 1 exit status
編譯報(bào)錯(cuò):找不到對(duì) a,b,c 的引用!那該怎么辦,才能使用局部變量呢?擴(kuò)展 asm 格式!
二、擴(kuò)展 asm 格式
1. 指令格式
asm [volatile] ("匯編指令" : "輸出操作數(shù)列表" : "輸入操作數(shù)列表" : "改動(dòng)的寄存器")
格式說(shuō)明
匯編指令:與基本asm格式相同;
輸出操作數(shù)列表:匯編代碼如何把處理結(jié)果傳遞到 C 代碼中;
輸入操作數(shù)列表:C 代碼如何把數(shù)據(jù)傳遞給內(nèi)聯(lián)匯編代碼;
改動(dòng)的寄存器:告訴編譯器,在內(nèi)聯(lián)匯編代碼中,我們使用了哪些寄存器;
“改動(dòng)的寄存器”可以省略,此時(shí)最后一個(gè)冒號(hào)可以不要,但是前面的冒號(hào)必須保留,即使輸出/輸入操作數(shù)列表為空。
關(guān)于“改動(dòng)的寄存器”再解釋一下:gcc 在編譯 C 代碼的時(shí)候,需要使用一系列寄存器;我們手寫(xiě)的內(nèi)聯(lián)匯編代碼中,也使用了一些寄存器。
為了通知編譯器,讓它知道:在內(nèi)聯(lián)匯編代碼中有哪些寄存器被我們用戶(hù)使用了,可以在這里列舉出來(lái),這樣的話(huà),gcc 就會(huì)避免使用這些列舉出的寄存器
2. 輸出和輸入操作數(shù)列表的格式
在系統(tǒng)中,存儲(chǔ)變量的地方就2個(gè):寄存器和內(nèi)存。因此,告訴內(nèi)聯(lián)匯編代碼輸出和輸入操作數(shù),其實(shí)就是告訴它:
向哪些寄存器或內(nèi)存地址輸出結(jié)果;
從哪些寄存器或內(nèi)存地址讀取輸入數(shù)據(jù);
這個(gè)過(guò)程也要滿(mǎn)足一定的格式:
"[輸出修飾符]約束"(寄存器或內(nèi)存地址)
(1)約束
就是通過(guò)不同的字符,來(lái)告訴編譯器使用哪些寄存器,或者內(nèi)存地址。包括下面這些字符:
a: 使用 eax/ax/al 寄存器;
b: 使用 ebx/bx/bl 寄存器;
c: 使用 ecx/cx/cl 寄存器;
d: 使用 edx/dx/dl 寄存器;
r: 使用任何可用的通用寄存器;
m: 使用變量的內(nèi)存位置;
先記住這幾個(gè)就夠用了,其他的約束選項(xiàng)還有:D, S, q, A, f, t, u等等,需要的時(shí)候再查看文檔。
(2)輸出修飾符
顧名思義,它使用來(lái)修飾輸出的,對(duì)輸出寄存器或內(nèi)存地址提供額外的說(shuō)明,包括下面4個(gè)修飾符:
+:被修飾的操作數(shù)可以讀取,可以寫(xiě)入;
=:被修飾的操作數(shù)只能寫(xiě)入;
%:被修飾的操作數(shù)可以和下一個(gè)操作數(shù)互換;
&:在內(nèi)聯(lián)函數(shù)完成之前,可以刪除或者重新使用被修飾的操作數(shù);
語(yǔ)言描述比較抽象,直接看例子!
3. test4.c 通過(guò)寄存器操作局部變量
#includeint main() { int data1 = 1; int data2 = 2; int data3; asm("movl %%ebx, %%eax " "addl %%ecx, %%eax" : "=a"(data3) : "b"(data1),"c"(data2)); printf("data3 = %d ", data3); return 0; }
有 2 個(gè)地方需要注意一下?。?/p>
在內(nèi)聯(lián)匯編代碼中,沒(méi)有聲明“改動(dòng)的寄存器”列表,也就是說(shuō)可以省略掉(前面的冒號(hào)也不需要);
擴(kuò)展asm格式中,寄存器前面必須寫(xiě) 2 個(gè)%;
代碼解釋?zhuān)?/p>
"b"(data1),"c"(data2) ==> 把變量 data1 復(fù)制到寄存器 %ebx,變量 data2 復(fù)制到寄存器 %ecx。這樣,內(nèi)聯(lián)匯編代碼中,就可以通過(guò)這兩個(gè)寄存器來(lái)操作這兩個(gè)數(shù)了;
"=a"(data3) ==> 把處理結(jié)果放在寄存器 %eax 中,然后復(fù)制給變量data3。前面的修飾符等號(hào)意思是:會(huì)寫(xiě)入往 %eax 中寫(xiě)入數(shù)據(jù),不會(huì)從中讀取數(shù)據(jù);
通過(guò)上面的這種格式,內(nèi)聯(lián)匯編代碼中,就可以使用指定的寄存器來(lái)操作局部變量了,稍后將會(huì)看到局部變量是如何從經(jīng)過(guò)棧空間,復(fù)制到寄存器中的。
生成匯編代碼指令:
gcc -m32 -S -o test4.s test4.c
匯編代碼 test4.s 如下:
movl$1, -20(%ebp) movl$2, -16(%ebp) movl-20(%ebp), %eax movl-16(%ebp), %edx movl%eax, %ebx movl%edx, %ecx #APP # 10 "test4.c" 1 movl %ebx, %eax addl %ecx, %eax # 0 "" 2 #NO_APP movl%eax, -12(%ebp)
可以看到,在進(jìn)入手寫(xiě)的內(nèi)聯(lián)匯編代碼之前:
把數(shù)字 1 通過(guò)??臻g(-20(%ebp)),復(fù)制到寄存器 %eax,再?gòu)?fù)制到寄存器 %ebx;
把數(shù)字 2 通過(guò)棧空間(-16(%ebp)),復(fù)制到寄存器 %edx,再?gòu)?fù)制到寄存器 %ecx;
這 2 個(gè)操作正是對(duì)應(yīng)了內(nèi)聯(lián)匯編代碼中的“輸入操作數(shù)列表”部分:"b"(data1),"c"(data2)。
在內(nèi)聯(lián)匯編代碼之后(#NO_APP 之后),把%eax寄存器中的值復(fù)制到棧中的-12(%ebp)位置,這個(gè)位置正是局部變量 data3所在的位置,這樣就完成了輸出操作。
4. test5.c 聲明改動(dòng)的寄存器
在 test4.c 中,我們沒(méi)有聲明改動(dòng)的寄存器,所以編譯器可以任意選擇使用哪些寄存器。從生成的匯編代碼 test4.s 中可以看到,gcc 使用了%edx寄存器。
那么我們來(lái)測(cè)試一下:告訴 gcc 不要使用 %edx 寄存器。
#includeint main() { int data1 = 1; int data2 = 2; int data3; asm("movl %%ebx, %%eax " "addl %%ecx, %%eax" : "=a"(data3) : "b"(data1),"c"(data2) : "%edx"); printf("data3 = %d ", data3); return 0; }
代碼中,asm 指令最后部分"%edx",就是用來(lái)告訴 gcc 編譯器:在內(nèi)聯(lián)匯編代碼中,我們會(huì)使用到 %edx 寄存器,你就不要用它了。
生成匯編代碼指令:
gcc -m32 -S -o test5.s test5.c
來(lái)看一下生成的匯編代碼 test5.s:
movl$1, -20(%ebp) movl$2, -16(%ebp) movl-20(%ebp), %eax movl-16(%ebp), %ecx movl%eax, %ebx #APP # 10 "test5.c" 1 movl %ebx, %eax addl %ecx, %eax # 0 "" 2 #NO_APP movl%eax, -12(%ebp)
可以看到,在內(nèi)聯(lián)匯編代碼之前,gcc沒(méi)有選擇使用寄存器%edx。
三、使用占位符來(lái)代替寄存器名稱(chēng)
在上面的示例中,只使用了2 個(gè)寄存器來(lái)操作 2 個(gè)局部變量,如果操作數(shù)有很多,那么在內(nèi)聯(lián)匯編代碼中去寫(xiě)每個(gè)寄存器的名稱(chēng),就顯得很不方便。
因此,擴(kuò)展 asm 格式為我們提供了另一種偷懶的方法,來(lái)使用輸出和輸入操作數(shù)列表中的寄存器:占位符!
占位符有點(diǎn)類(lèi)似于批處理腳本中,利用2...來(lái)引用輸入參數(shù)一樣,內(nèi)聯(lián)匯編代碼中的占位符,從輸出操作數(shù)列表中的寄存器開(kāi)始從0編號(hào),一直編號(hào)到輸入操作數(shù)列表中的所有寄存器。
還是看例子比較直接!
1. test6.c 使用占位符代替寄存器
#includeint main() { int data1 = 1; int data2 = 2; int data3; asm("addl %1, %2 " "movl %2, %0" : "=r"(data3) : "r"(data1),"r"(data2)); printf("data3 = %d ", data3); return 0; }
代碼說(shuō)明:
輸出操作數(shù)列表"=r"(data3):約束使用字符 r, 也就是說(shuō)不指定寄存器,由編譯器來(lái)選擇使用哪個(gè)寄存器來(lái)存儲(chǔ)結(jié)果,最后復(fù)制到局部變量 data3中;
輸入操作數(shù)列表"r"(data1),"r"(data2):約束字符r, 不指定寄存器,由編譯器來(lái)選擇使用哪 2 個(gè)寄存器來(lái)接收局部變量 data1 和 data2;
輸出操作數(shù)列表中只需要一個(gè)寄存器,因此在內(nèi)聯(lián)匯編代碼中的 %0 就代表這個(gè)寄存器(即:從 0 開(kāi)始計(jì)數(shù));
輸入操作數(shù)列表中有 2 個(gè)寄存器,因此在內(nèi)聯(lián)匯編代碼中的 %1 和 %2就代表這 2 個(gè)寄存器(即:從輸出操作數(shù)列表的最后一個(gè)寄存器開(kāi)始順序計(jì)數(shù));
生成匯編代碼指令:
gcc -m32 -S -o test6.s test6.c
匯編代碼如下 test6.s:
movl$1, -20(%ebp) movl$2, -16(%ebp) movl-20(%ebp), %eax movl-16(%ebp), %edx #APP # 10 "test6.c" 1 addl %eax, %edx movl %edx, %eax # 0 "" 2 #NO_APP movl%eax, -12(%ebp)
可以看到,gcc 編譯器選擇了%eax來(lái)存儲(chǔ)局部變量 data1,%edx來(lái)存儲(chǔ)局部變量 data2,然后操作結(jié)果也存儲(chǔ)在%eax 寄存器中。
是不是感覺(jué)這樣操作就方便多了?不用我們來(lái)指定使用哪些寄存器,直接交給編譯器來(lái)選擇。
在內(nèi)聯(lián)匯編代碼中,使用%0、%1 、%2這樣的占位符來(lái)使用寄存器。
別急,如果您覺(jué)得使用編號(hào)還是麻煩,容易出錯(cuò),還有另一個(gè)更方便的操作:擴(kuò)展 asm 格式還允許給這些占位符重命名,也就是給每一個(gè)寄存器起一個(gè)別名,然后在內(nèi)聯(lián)匯編代碼中使用別名來(lái)操作寄存器。
還是看代碼!
2. test7.c 給寄存器起別名
#includeint main() { int data1 = 1; int data2 = 2; int data3; asm("addl %[v1], %[v2] " "movl %[v2], %[v3]" : [v3]"=r"(data3) : [v1]"r"(data1),[v2]"r"(data2)); printf("data3 = %d ", data3); return 0; }
代碼說(shuō)明:
輸出操作數(shù)列表:給寄存器(gcc 編譯器選擇的)取了一個(gè)別名 v3;
輸入操作數(shù)列表:給寄存器(gcc 編譯器選擇的)取了一個(gè)別名 v1 和 v2;
起立別名之后,在內(nèi)聯(lián)匯編代碼中就可以直接使用這些別名( %[v1], %[v2], %[v3])來(lái)操作數(shù)據(jù)了。
生成匯編代碼指令:
gcc -m32 -S -o test7.s test7.c
再來(lái)看一下生成的匯編代碼 test7.s:
movl$1, -20(%ebp) movl$2, -16(%ebp) movl-20(%ebp), %eax movl-16(%ebp), %edx #APP # 10 "test7.c" 1 addl %eax, %edx movl %edx, %eax # 0 "" 2 #NO_APP movl%eax, -12(%ebp)
這部分的匯編代碼與 test6.s 中完全一樣!
四、使用內(nèi)存位置
在以上的示例中,輸出操作數(shù)列表和輸入操作數(shù)列表部分,使用的都是寄存器(約束字符:a, b, c, d, r等等)。
我們可以指定使用哪個(gè)寄存器,也可以交給編譯器來(lái)選擇使用哪些寄存器,通過(guò)寄存器來(lái)操作數(shù)據(jù),速度會(huì)更快一些。
如果我們?cè)敢獾脑?huà),也可以直接使用變量的內(nèi)存地址來(lái)操作變量,此時(shí)就需要使用約束字符 m。
1. test8.c 使用內(nèi)存地址來(lái)操作數(shù)據(jù)
#includeint main() { int data1 = 1; int data2 = 2; int data3; asm("movl %1, %%eax " "addl %2, %%eax " "movl %%eax, %0" : "=m"(data3) : "m"(data1),"m"(data2)); printf("data3 = %d ", data3); return 0; }
代碼說(shuō)明:
輸出操作數(shù)列表 "=m"(data3):直接使用變量 data3 的內(nèi)存地址;
輸入操作數(shù)列表 "m"(data1),"m"(data2):直接使用變量 data1, data2 的內(nèi)存地址;
在內(nèi)聯(lián)匯編代碼中,因?yàn)樾枰M(jìn)行相加計(jì)算,因此需要使用一個(gè)寄存器(%eax),計(jì)算這個(gè)環(huán)節(jié)是肯定需要寄存器的。
在操作那些內(nèi)存地址中的數(shù)據(jù)時(shí),使用的仍然是按順序編號(hào)的占位符。
生成匯編代碼指令:
gcc -m32 -S -o test8.s test8.c
生成的匯編代碼如下 test8.s:
movl$1, -24(%ebp) movl$2, -20(%ebp) #APP # 10 "test8.c" 1 movl -24(%ebp), %eax addl -20(%ebp), %eax movl %eax, -16(%ebp) # 0 "" 2 #NO_APP movl-16(%ebp), %eax
可以看到:在進(jìn)入內(nèi)聯(lián)匯編代碼之前,把data1 和 data2的值放在了棧中,然后直接把棧中的數(shù)據(jù)與寄存器%eax進(jìn)行操作,最后再把操作結(jié)果(%eax),復(fù)制到棧中data3 的位置(-16(%ebp))。
五、總結(jié)
通過(guò)以上 8 個(gè)示例,我們把內(nèi)聯(lián)匯編代碼中的關(guān)鍵語(yǔ)法規(guī)則進(jìn)行了講解,有了這個(gè)基礎(chǔ),就可以在內(nèi)聯(lián)匯編代碼中編寫(xiě)更加復(fù)雜的指令了。
希望以上內(nèi)容對(duì)您能有所幫助!謝謝!
編輯:jq
-
寄存器
+關(guān)注
關(guān)注
31文章
5301瀏覽量
119863 -
ASM
+關(guān)注
關(guān)注
1文章
68瀏覽量
18997 -
代碼
+關(guān)注
關(guān)注
30文章
4728瀏覽量
68251 -
編譯器
+關(guān)注
關(guān)注
1文章
1617瀏覽量
49019
原文標(biāo)題:內(nèi)聯(lián)匯編很可怕嗎?看完這篇文章,終結(jié)它!
文章出處:【微信號(hào):gh_c472c2199c88,微信公眾號(hào):嵌入式微處理器】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論