汇编程序设计与计算机体系结构软件工程师教程笔记:内联汇编与宏
《匯編程序設(shè)計(jì)與計(jì)算機(jī)體系結(jié)構(gòu): 軟件工程師教程》這本書是由Brain R.Hall和Kevin J.Slonka著,由愛(ài)飛翔譯。中文版是2019年出版的。個(gè)人感覺(jué)這本書真不錯(cuò),書中介紹了三種匯編器GAS、NASM、MASM異同,全部示例代碼都放在了GitHub上,包括x86和x86_64,并且給出了較多的網(wǎng)絡(luò)參考資料鏈接。這里只摘記了MASM和NASM,測(cè)試代碼僅支持Windows和Linux的x86_64。
9. 內(nèi)聯(lián)匯編與宏
9.2 內(nèi)聯(lián)匯編:是一種在高級(jí)語(yǔ)言中嵌入?yún)R編代碼的辦法。還有一種辦法也能把匯編語(yǔ)言的代碼同高級(jí)語(yǔ)言的代碼結(jié)合起來(lái),就是用匯編代碼來(lái)撰寫函數(shù),并將其放在一份文件中,然后在C++代碼中調(diào)用這些函數(shù)。不過(guò),這種辦法需要執(zhí)行相關(guān)的準(zhǔn)備工作,而且還涉及函數(shù)調(diào)用,因此會(huì)引發(fā)一些開銷。與之相比,撰寫內(nèi)聯(lián)式的匯編代碼在某些情況下可能更為簡(jiǎn)單。
各種編譯器會(huì)采用不同的做法來(lái)處理內(nèi)聯(lián)式的匯編代碼。如果高級(jí)語(yǔ)言的代碼所內(nèi)嵌的匯編代碼在編譯過(guò)程中沒(méi)有受到修改或優(yōu)化,那么GCC實(shí)際上就相當(dāng)于把這一部分匯編代碼復(fù)制到它所輸出的匯編文件中(這種文件的后綴名是.s),進(jìn)而把這些代碼與從高級(jí)語(yǔ)言轉(zhuǎn)換而來(lái)的那些匯編代碼一起交給GNU匯編器(GNU Assembler, 也就是GAS)處理。Microsoft的Visual Studio采用Visual C++內(nèi)置的匯編器處理內(nèi)聯(lián)匯編語(yǔ)句,而不會(huì)專門采用MASM那樣單獨(dú)的匯編器來(lái)做。Microsoft的x64 C/C++編譯器不支持內(nèi)聯(lián)式的匯編代碼,它改用編譯器內(nèi)部函數(shù)來(lái)完成相應(yīng)的底層指令。
C++編譯器通常用asm或__asm關(guān)鍵字表示內(nèi)聯(lián)式的匯編語(yǔ)句。內(nèi)聯(lián)匯編代碼范例如下表所示:
Clang/GCC匯編器要求asm關(guān)鍵字后面必須寫一對(duì)圓括號(hào)及一個(gè)分號(hào),而匯編代碼則要以字符串的形式寫在一對(duì)雙引號(hào)中,并放置在這對(duì)圓括號(hào)中。換行符與制表符用來(lái)分割字符串中的各條匯編語(yǔ)句。Visual C++要求__asm關(guān)鍵字的后面要么直接寫匯編語(yǔ)句,要么跟上一對(duì)花括號(hào)。如果匯編語(yǔ)句只有一條可以直接寫出來(lái),若有很多條則需放在花括號(hào)中。最后的分號(hào)可有可無(wú)。
開發(fā)者在使用Clang/GCC時(shí)可能會(huì)開啟-ansi或-std這樣的編譯選項(xiàng),從而導(dǎo)致asm及inline等關(guān)鍵字遭到禁用,這種情況下,可以把a(bǔ)sm關(guān)鍵字寫成__asm__。
Clang與GCC是有基本形式與擴(kuò)展形式之分的。盡量不要使用基本形式的asm內(nèi)聯(lián),因?yàn)樵谠L問(wèn)全局變量時(shí)可能要做不同的處理,而且,這種形式還會(huì)假設(shè)匯編代碼不修改通用的寄存器。然而基本形式的asm內(nèi)聯(lián)也有一些好處,由于它不一定非要寫在C/C++函數(shù)中,因此可以用來(lái)執(zhí)行匯編器指令并撰寫全局的匯編函數(shù)。此外,如果C/C++函數(shù)聲明成了naked函數(shù),編譯器就不會(huì)為其生成開場(chǎng)及收?qǐng)龃a(prologue and epilogue code),這就要求開發(fā)者必須使用基本形式的asm內(nèi)聯(lián)來(lái)為這樣的函數(shù)撰寫定制的開場(chǎng)及收?qǐng)龃a。擴(kuò)展形式的asm內(nèi)聯(lián)可以更精細(xì)地控制與匯編代碼有關(guān)的輸出與輸入,并指明代碼所用到的寄存器。Clang/GCC要求擴(kuò)展形式的asm代碼必須寫在C/C++函數(shù)中,Visual C++則要求所有的內(nèi)聯(lián)匯編代碼都必須這樣寫。
GCC并不會(huì)解析內(nèi)聯(lián)式的匯編指令,因此它根本就不知道這些語(yǔ)句是否有效,而且GDB等調(diào)試器也不會(huì)單步地進(jìn)入內(nèi)聯(lián)匯編語(yǔ)句中去調(diào)試,而是把整個(gè)asm視為一個(gè)步驟。如果要單步調(diào)試,可以在想插入斷點(diǎn)的地方手工添加”int $3”指令。Visual Studio無(wú)須使用INT即可單步調(diào)試x86內(nèi)聯(lián)匯編指令。
內(nèi)聯(lián)匯編中的注釋既可以按匯編代碼自身的格式來(lái)寫也可以按C++的格式來(lái)寫,建議采用后一種寫法。
Visual C++可以直接訪問(wèn)C語(yǔ)言式的變量。Clang/GCC訪問(wèn)變量的方式比較復(fù)雜,對(duì)內(nèi)聯(lián)的匯編代碼來(lái)說(shuō),用于輸出和用于輸入的變量都必須在模板中的相應(yīng)列表中說(shuō)明。當(dāng)然,列表也可以留空,如果其中有內(nèi)容,那么與clobbers列表合計(jì)起來(lái)不得超過(guò)30項(xiàng)。
在Clang/GCC的內(nèi)聯(lián)式匯編代碼中對(duì)涉及輸出與輸入的變量做出說(shuō)明:
[asmSymbolicName] “constraint” (CvariableName)
其中,asmSymbolicName可以是任何一個(gè)有效的標(biāo)識(shí)符名稱,它也可以與外圍代碼中已經(jīng)定義的C語(yǔ)言標(biāo)識(shí)符同名,從而使內(nèi)聯(lián)式的匯編代碼能夠用同樣的名稱來(lái)引用這個(gè)變量。constraint(約束或限定)這一部分用來(lái)指定與操作數(shù)的用法及放置地點(diǎn)有關(guān)的信息。它可以是一個(gè)或一系列字面。如果某個(gè)操作數(shù)是用來(lái)輸出數(shù)值的,那么它必須先以修飾符開頭,然后才能寫上一個(gè)或多個(gè)字母。最常用的兩個(gè)修飾符是”=”與”+”,前者表示該操作數(shù)在執(zhí)行匯編指令之前的初始值并不重要,僅僅是供匯編代碼寫入數(shù)據(jù)的,后者表示匯編代碼既要讀取該操作數(shù)的初始值,又要向其中寫入數(shù)據(jù)(也就是說(shuō)這個(gè)參數(shù)既用于輸入又用于輸出)。有的時(shí)候,系統(tǒng)會(huì)先把那些僅僅用于輸入的參數(shù)處理完,然后再處理那些供匯編代碼輸出數(shù)據(jù)的參數(shù),這就促使它有可能會(huì)復(fù)用某些寄存器,令其在不同的階段表示不同的參數(shù)。這樣一來(lái),如果其中有某個(gè)輸出參數(shù)在處理輸入?yún)?shù)的過(guò)程中已經(jīng)寫進(jìn)了值,那么這個(gè)值就會(huì)把用同一個(gè)寄存器所表示的輸入?yún)?shù)所具備的初始值給覆蓋掉。為了避免這種覆寫的情況,可以在”=”或”+”后面寫上”&”符號(hào),使得系統(tǒng)能夠正確地實(shí)現(xiàn)這樣的參數(shù)。
在Clang/GCC的內(nèi)聯(lián)匯編模板中,還有一個(gè)部分叫做clobbers列表,列表中需要寫上內(nèi)聯(lián)的匯編代碼所修改的寄存器,以防止系統(tǒng)誤以為這些寄存器的值不會(huì)為匯編代碼所修改,從而執(zhí)行一些相關(guān)的優(yōu)化措施。系統(tǒng)可能會(huì)生成指令來(lái)保存它們的值,以便在執(zhí)行完內(nèi)聯(lián)的asm語(yǔ)句后能夠予以恢復(fù)。系統(tǒng)在選用寄存器來(lái)實(shí)現(xiàn)匯編代碼中用于輸入及輸出的操作數(shù)時(shí)會(huì)避開clobbers列表中指明的項(xiàng)目。如果你所寫的內(nèi)聯(lián)式匯編代碼確實(shí)會(huì)順帶影響到某個(gè)寄存器的值,但你卻沒(méi)有將其寫在clobbers列表中,那么程序可能會(huì)出錯(cuò)或發(fā)生意外的結(jié)果。
下表列出了內(nèi)聯(lián)式匯編代碼中經(jīng)常用到的x86約束修飾符:
根據(jù)AT&T匯編語(yǔ)法,寄存器以一個(gè)%符號(hào)開頭。不過(guò),內(nèi)聯(lián)式的匯編代碼中寄存器需要用兩個(gè)%符號(hào)開頭,也就是要在前面加上%%,若只加一個(gè)%,表示的則是這段內(nèi)聯(lián)代碼所用的參數(shù)(或變量)。之所以要這樣寫,是因?yàn)閮?nèi)聯(lián)代碼會(huì)把%當(dāng)作轉(zhuǎn)義符使用,以便輸出它后面的符號(hào)所表示的字符,因此,”%%”輸出的是單個(gè)%字符,而”%=”則會(huì)針對(duì)代碼庫(kù)中的每一段asm代碼輸出一個(gè)獨(dú)特的數(shù)字(這個(gè)數(shù)字可以用來(lái)創(chuàng)建標(biāo)簽,以供其它代碼引用)。此外,”%{”、”%|”及”%}”這三種寫法分別用來(lái)輸出{、|及}字符。這三種字符必須用%來(lái)轉(zhuǎn)義,假如直接寫在內(nèi)聯(lián)的匯編代碼中,那么系統(tǒng)就會(huì)將其當(dāng)成特殊字符并解讀成用來(lái)表示各種匯編方言的結(jié)構(gòu)。如果要產(chǎn)生%eax這樣的匯編代碼,那么在內(nèi)聯(lián)的匯編語(yǔ)句中應(yīng)該寫成%%eax,不過(guò),在clobbers列表中提到eax時(shí),只寫一個(gè)%就好。
Clang與GCC默認(rèn)使用AT&T語(yǔ)法規(guī)則,而Visual C++則使用Intel語(yǔ)法規(guī)則。不過(guò),Clang與GCC能夠在內(nèi)聯(lián)的匯編代碼中使用多種匯編方言。由AT&T語(yǔ)法切換到Intel語(yǔ)法能夠使代碼變得好懂一些。要想采用某種特定的語(yǔ)法來(lái)撰寫內(nèi)聯(lián)匯編代碼,一種簡(jiǎn)單的辦法是在第一條命令中寫上.att_syntax以表示這段代碼用的是AT&T語(yǔ)法,或者寫上.intel_syntax以表示這段代碼用的是Intel語(yǔ)法。Clang與GCC可以通過(guò)-masm這個(gè)編譯選項(xiàng)來(lái)指定內(nèi)聯(lián)匯編代碼所采用的方言。-masm=att與-masm=intel分別表示AT&T語(yǔ)法及Intel語(yǔ)法。
9.3 宏(macro):它可以視為一個(gè)模塊或一系列指令,并根據(jù)名稱來(lái)加以調(diào)用。匯編代碼中的宏的運(yùn)作方式與C++代碼中的內(nèi)聯(lián)函數(shù)類似,都是用其中所包含的一系列語(yǔ)句或指令來(lái)替換位于調(diào)用點(diǎn)的宏名或函數(shù)名。每調(diào)用一次宏就要發(fā)生一次代碼替換或代碼展開。
對(duì)比宏與函數(shù):函數(shù)適合實(shí)現(xiàn)那種比較長(zhǎng)而且需要頻繁執(zhí)行的代碼。無(wú)論函數(shù)調(diào)用多少次,都只需要在程序中保留一份代碼。每次調(diào)用函數(shù)時(shí),系統(tǒng)會(huì)把控制權(quán)轉(zhuǎn)移給內(nèi)存中的這個(gè)函數(shù)。調(diào)用函數(shù)涉及傳遞參數(shù)、建棧以及清理?xiàng)5裙ぷ?#xff0c;從而會(huì)產(chǎn)生一定的開銷。為了提高效率,這些比較長(zhǎng)且比較復(fù)雜的函數(shù)代碼只在程序中保留一份就可以了。宏適合實(shí)現(xiàn)那種比較短而且需要頻繁執(zhí)行的代碼。對(duì)程序做匯編的時(shí)候,匯編器每看到一個(gè)宏就會(huì)把該宏所代表的那一系列指令展開到這里,于是同樣一段代碼就有可能多次出現(xiàn)在程序中,這樣做的好處是省去了調(diào)用函數(shù)所需的開銷。宏與函數(shù)有一個(gè)共同點(diǎn),就是都可以接受參數(shù)(argument或parameter),不過(guò)函數(shù)的參數(shù)是在運(yùn)行程序的時(shí)候才傳遞過(guò)去的,而宏的參數(shù)則是在做匯編的時(shí)候就已經(jīng)替換好了。
定義并調(diào)用宏:NASM采用[%#]的格式來(lái)指代參數(shù),其中的#部分是從1開始的編號(hào);MASM直接按照參數(shù)的名稱來(lái)引用。宏可以寫在程序段以外,也可以寫在其中,無(wú)論采用哪種寫法都應(yīng)該在整個(gè)程序的范圍內(nèi)保持一致。還有一種管理宏的辦法,是把所有的宏或者相關(guān)的一組宏單獨(dú)放在一份文件中,然后通過(guò)INCLUDE匯編命令將其包含進(jìn)來(lái)。MASM的宏還具備一項(xiàng)特性,就是可以在參數(shù)后面寫上:REQ,以要求調(diào)用方必須指定這個(gè)參數(shù)而不能將其忽略。
以下是測(cè)試內(nèi)聯(lián)匯編和宏的測(cè)試代碼段,這些code主要來(lái)源本書的示例代碼:
MASM的內(nèi)聯(lián)匯編代碼段如下:
int test_inline_int()
{__m128i var1 = _mm_cvtsi32_si128(1234);__m128i var2 = _mm_cvtsi32_si128(2);var1 = _mm_add_epi64(var1, var2);int output = _mm_cvtsi128_si32(var1);fprintf(stdout, "output: %d\n", output);return 0;
}
以上代碼段是在Visual Studio下執(zhí)行兩個(gè)整數(shù)的相加,向_mm_add_epi64等這些指令來(lái)自于SSE2 Intrinsics,執(zhí)行結(jié)果如下圖所示:
NASM的內(nèi)聯(lián)匯編代碼段如下:
int test_inline_int()
{int var1 = 1234, var2;asm ("mov %1,%%eax \n\t""add $2,%%eax \n\t""mov %%eax,%0 \n\t":"=r" (var2) /* %0: Out */:"r" (var1) /* %1: In */:"%eax" /* Overwrite */);fprintf(stdout, "var2: %d\n", var2);return 0;
}
以上代碼段是在Linux下執(zhí)行兩個(gè)整數(shù)的相加,執(zhí)行結(jié)果如下圖所示:
MASM的宏代碼段如下:
extrn ExitProcess : proc_printInt PROTO CintAdd MACRO dest, source1, source2mov rax, source1add rax, source2mov dest, rax
ENDM.DATA
intA QWORD 2
intB QWORD 4
intC QWORD 3
intD QWORD 7
result QWORD 0.CODE
macro_usage PROCpush rbpintAdd result, intA, intBmov rcx, resultmov rax, 0call _printIntintAdd result, intC, intDmov rcx, resultmov rax, 0call _printIntpop rbpret
macro_usage ENDP
END
執(zhí)行結(jié)果如下圖所示:
NASM的宏代碼段如下:
extern _printInt%macro intAdd 3mov rax, [%2]add rax, [%3]mov [%1], rax
%endmacrosection .data
intA: dq 2
intB: dq 4
intC: dq 3
intD: dq 7
result: dq 0section .text
global macro_usage
macro_usage:push rbp ; set up stack frame, must be allignedintAdd result, intA, intBmov rdi, qword [result]mov rax, 0 ; or can be xor rax, rax, no vector registers in usecall _printIntintAdd result, intC, intDmov rdi, qword [result]mov rax, 0call _printIntpop rbp ; restore stackmov rax, 0 ; normal exit, no error, return valueret ; return to operating system
執(zhí)行結(jié)果如下圖所示:
GitHub:https://github.com/fengbingchun/CUDA_Test
總結(jié)
以上是生活随笔為你收集整理的汇编程序设计与计算机体系结构软件工程师教程笔记:内联汇编与宏的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 汇编程序设计与计算机体系结构软件工程师教
- 下一篇: Windows下创建进程简介