汇编程序设计与计算机体系结构软件工程师教程笔记:函数、字符串、浮点运算
《匯編程序設計與計算機體系結構: 軟件工程師教程》這本書是由Brain R.Hall和Kevin J.Slonka著,由愛飛翔譯。中文版是2019年出版的。個人感覺這本書真不錯,書中介紹了三種匯編器GAS、NASM、MASM異同,全部示例代碼都放在了GitHub上,包括x86和x86_64,并且給出了較多的網絡參考資料鏈接。這里只摘記了NASM和MASM,測試代碼僅支持Windows和Linux的x86_64。
6. 函數
6.2 棧內存入門:棧內存(stack memory)是為自動變量而設的一塊區域(這里的自動變量是指局部變量,或者說非動態的變量)。調用函數的時候,需要用棧來保存函數中的局部變量,而函數結束的時候,則需要棄用這些變量。高級語言的一項特征在于它會自行管理棧內存(這有時也叫做運行時棧或運行期棧),相反,匯編語言不會這樣做,而是需要你自己去管理。
與棧內存有關的重要事項:
(1).棧會在調用函數時增長,并在調用結束時收縮。
(2).棧會在創建(或者說推入/壓入)局部變量時增長,并在棄用(或者說彈出)局部變量時收縮。
(3).每個進程或線程的棧,其大小受操作系統限制,例如Linux/Mac系統默認是8MB,Windows默認是1MB。
(4).每次調用函數(這也包括調用主函數main(),以及遞歸地調用自身)都會出現對應的棧幀(stack frame)。它是棧內存中的一塊區域,隨著函數調用而產生。
(5).棧幀用來保存函數的局部變量。
(6).棧內存是向下增長的,也就是說,推入棧中的新內容會出現在地址值較低的位置中。
(7).32位環境下,棧中的每個元素占據4個字節,64位環境下占據8個字節。
(8).數值默認按照小端序入棧。權重最高的字節保存到地址最高的位置上,權重較低的字節保存到地址最低的位置上。
(9).x86_64的函數都必須按16字節對齊(所有平臺都是這樣)。
6.3 x86與x86_64的調用約定:
調用約定也叫做調用協議,用來確定函數在底層的實現流程,包括如何傳遞參數、如何管理棧內存,以及如何返回值。
cdecl(32位):cdecl(C declaration)調用約定是32位平臺最常見的一種約定,因為它是基于C語言標準而設立的。一般來說,GCC、Clang與Visual Studio的C編譯器默認都會使用cdecl約定。有些開發環境(如Visual Studio)允許開發者在項目屬性中調整默認的約定方式。cdecl有4項主要特征:
(1).參數按照相反的順序(也就是從右到左的順序)入棧。
(2).eax、ecx與edx需要由調用方(caller, 主調方)保存(叫做易失性的寄存器),而其余的通用寄存器則需要由受調方(callee, 被調用方)來保存(叫做非易失性的寄存器)。因此,如果想令eax、ecx及edx寄存器在調用完函數之后還能保持調用之前的值,那么應該在主調函數中先將其保存起來,因為受調函數在執行過程中有可能會修改它們。
(3).在大多數情況下,返回值放在eax寄存器里,如果要返回的是浮點數,那么應該放在st(0)寄存器里。
(4).棧由調用方負責清理。
C語言支持參數數量可變的函數。由于這種函數的參數列表長度不固定,因此,受調方并不清楚自己到底接收了幾個參數。于是,就要求棧必須由主調方來清理,而且每次調用的時候都得清理。C語言里的printf()就屬于這樣的函數。
在32位環境與64位環境下引用局部變量可以采用不同的辦法。32位環境下,一般用ebp作為基準點來訪問參數,而64位環境下則不太這樣使用。其原因有三:第一,64位環境下通常使用寄存器(而不是棧)來傳遞參數;第二,64位環境下可以使用RIP相對尋址機制;第三,64位環境下,系統默認在棧內存中劃分一塊暫存空間,使我們可以直接以rsp寄存器為基準來引用局部變量。
stdcall(32位):被Windows API使用,一般來說,它的規則與cdecl是相同的,只是在一個地方有所區別(第4項):
(1).參數按照相反的順序(也就是從右到左的順序)入棧。
(2).eax、ecx與edx應該由調用方保存,而其余的通用寄存器則應該由受調方來保存。
(3).在大多數情況下,返回值放在eax寄存器里,如果要返回的是浮點數,那么應該放在st(0)寄存器里。
(4).棧由受調方負責清理。
stdcall的好處是,受調函數可以在運行RET(返回)指令的時候順便把棧清理干凈,從而使主調函數可以少寫一些代碼。由于清理棧所用的代碼是寫在受調函數中的,因此,每次調用這個函數時,這些代碼都能得到執行,而不用在主調函數里重復一遍。棧能夠由受調方清理,是因為stdcall不允許參數個數可變的函數,因此,受調方可以知道自己接收了幾個參數,從而正確地把棧清理干凈。stdcall的RET指令與cdecl稍有不同,它必須指出棧中有多少個字節的內容需要移除。
x86_64(64位):約定試圖提升函數的執行速度,它允許程序通過寄存器來傳遞某些參數,而不一定非要通過棧來做(這與32位模式下的fastcall約定是類似的,fastcall也是一種可以通過寄存器來傳遞數值的約定方式,它運用在32位環境中)。下表指出了兩種64位的函數調用約定所具備的特征:在這兩種情況下(Microsoft x64, System V AMD64(AMD64)),如果要傳遞的參數比參數寄存器的數量還多,那么其余的參數依然遵循從右至左的順序入棧。
64位模式下的函數必須按16字節對齊。x86_64支持流式SIMD擴展(Streaming SIMD Extensions, SSE),這是一套有助于并行處理的指令。SSE的操作一般都是128位的,SSE操作必須按16字節對齊。為了確保使用SSE指令的函數不會出現棧對齊錯誤,所有的棧幀都應該向16字節處對齊。此外,由于CALL指令默認會把8個字節的地址推入棧中,因此,在很多情況下,我們都需要再向棧中推入8字節,從而實現16字節對齊。
x86_64調用約定還有一個重要特征,就是在棧中預留額外的空間。Microsoft x64約定所預留的空間,叫做shadow space或home space,其長度是32個字節,用來放置參數寄存器中的數值。即便不需要傳遞任何參數,也依然得預留這么大的空間。如果受調函數經由r8、r9、rcx及rdx寄存器收到的參數還要在該函數調用其它函數的時候用到,那么這些參數的值就可以在shadow space中保存一份,除此之外,這塊暫存空間(scratch space)也可以用來做其它的事情。
AMD64調用約定沒有32個字節的shadow space這一概念,但它會把存放返回地址所用的位置(rsp就指向此處)下方的128個字節視為red-zone。這是打算留給函數使用的一塊臨時存儲空間,不會被系統侵擾。如果沒有PUSH、POP或CALL指令涉及這塊區域,那么rsp就不會改變,于是,開發者可以通過rsp來引用局部變量及標識符(例如在實現跳轉和循環的時候,就可以這樣引用相關的標識符)。預留這塊區域是為了令rsp寄存器不會因局部變量而頻繁地調整,進而節省時鐘周期,以求優化程序的執行效率。此外,它還可以用來實現末端函數(leaf function, 葉函數),也就是那種不會再繼續調用其它函數的函數。主調方可以利用red-zone來完成未端函數所要執行的計算,這樣就不用再通過CALL指令真的去調用它了,從而可以免去一些開銷。與Microsoft x64的shadow space不同,AMD 64的red-zone并不需要由開發者或編譯器專門去預留,它僅僅用來表達一項承諾,意思是說:系統信號及中斷處理程序是不會觸碰rsp下方那128個字節的。但是,如果調用了其它函數,那么主調函數的red-zone就會遭到破壞,因此,這塊區域主要還是留給那些不會再調用其它函數的末端函數來用的,那些函數可以把它當做暫存空間。
6.7 重要的寄存器(32位和64位)及命令,如下表所示:
6.9 與平臺有關的注意事項:
(1).在Windows系統中編寫代碼時,應該用PROC與ENDP命令指出例程的起點與終點。
(2).在Windows系統中編寫MASM匯編代碼時,可以通過.MODEL命令指出整個程序所遵循的調用約定(例如C或STDCALL),也可以在每個函數的PROC右側專門指出該函數所遵循的調用約定,以便更為精細地控制程序。
(3).在Windows系統中編寫MASM匯編代碼時,可以通過USES命令指出一些寄存器,使得匯編器為它們生成相應的PUSH或POP指令。這些指令會自動出現在函數的開場與收場部分,從而能夠在程序進入及退出該例程時得到執行(這些寫法對于32位及64位模式都適用)。
7. 與字符串有關的指令及結構體
7.2 輔助指令:
下表列出了兩條方向指令,分別用來給方向標志(direction flag)清零及置位。方向標志決定了內存地址在執行字符串指令的過程中是應該自動遞增還是自動遞減,這實際上也就決定了字符串的處理順序是從左至右還是從右至左。CLD與STD指令最適合用在重復操作之前,以便將方向標志調整好。
字符串的重復操作是通過重復指令實現的,下表列出了三種形式的REP(repeat)指令,它們都采用C系列的寄存器做計數器。
把調整方向標志的指令與重復指令結合起來,就能夠自動處理字符串數據而不用手工編寫循環。下面是重復指令運作方式的步驟:
(1).重復指令要判斷相關的C系列寄存器是否大于0,如果已經等于0就不再重復。若是比0大則繼續重復。
(2).執行字符串指令,根據結果設定相關的標志,并根據具體的指令與方向標志來遞增或遞減si/esi/rsi及di/edi/rdi。
(3).如果這條重復指令是REPE/REPNE或REPZ/REPNZ,就根據ZF標志來判斷是否應該繼續重復。如果不應該就不再重復。
(4).如果第2步(對于另外兩種形式來說是第2與第3步)順利執行,那就將相關的C系列寄存器減1并回到第1步。
7.3 基本字符串指令:5種處理字符串數據的指令,如下表所示:這些字符串指令都會根據方向標志(也就是DF)的取值,給si/esi/rsi及di/edi/rdi加上或減去一定的字節數,使得程序能夠按照開發者預想的方向來處理數組。即便不與REP搭配,這些字符串指令也依然會執行遞增或遞減運算。
MOVS(move string, 移動字符串):與MOV指令類似,但是有兩個區別,第一,它的源操作數與目標操作數不需要寫在指令中,而是默認由esi及edi等寄存器來表示。第二,所有的匯編器都要求在該指令后面寫上一個與元素大小相對應的后綴字母。si/esi/rsi寄存器指向待復制的數據所在的內存位置,di/edi/rdi寄存器指向目標地址。在與REP相搭配用以連續復制多個字符的過程中,為了使下一次復制操作能夠找到正確的字符,開發者必須指出每個字符所占據的字節數,從而令MOVS可以根據DF標志適當地遞增或遞減相應的寄存器。由于字符串指令每次所操作的數據可能是1、2、4、8或16個字節,則字符串指令所添加的后綴依次為B(byte)、W(word)、D(dword)、Q(qword)、O(octa)。
CMPS(compare string, 比較字符串):指令與CMP指令相似,但是它與MOVS一樣也不帶操作數,而是默認會通過si/esi/rsi及di/edi/rdi來確定有待比較的數據所在的內存地址,而且它也像MOVS那樣最好是能夠與重復指令結合起來使用。如果不搭配重復指令,而是單獨使用這條指令,那么只能比較一個字符而不是整個字符串。把重復指令與MOVS相搭配,可以按序復制字符,但若與CMPS相搭配則是按序訪問字符。為了對比兩個字符串,CMPS指令必須從一個字符移動到下一個字符,然而它在移動的同時還需要判斷:當前字符與另一個字符串里的對應字符是否相同。與CMPS搭配的重復指令是REPE/REPZ或REPNE/REPNZ。以REPE/REPZ為例,只有當兩個字符串的對應字符相等時才會繼續比較下一個字符(因為只有在這種情況下CMPS指令才會把ZF標志設置成1,從而使重復指令能夠繼續執行)。CMPS通過減法比較這兩個字符,也就是把由si/esi/rsi所表示的字符從由di/edi/rdi所表示的字符值中減去,并根據結果修改相關的處理器標志,尤其是ZF標志。REPE與PEPZ是同一個意思,因為它們執行相同的判斷邏輯。REPNE與REPNZ兩者之間也是如此。CMPS指令最基礎的用法是單獨以CMPS+后綴的格式使用。
SCAS(scan string, 掃描字符串):實際上是在執行內置的順序搜索算法,該算法在搜尋字符串的過程中查找的是單個字符。SCAS的正常流程是對di/edi/rdi所指的字符串做迭代,以便在其中尋找與al/ax/eax/rax里的目標字符相同的字符,如果能找到就提取退出,與它相搭配的是REPNE或REPNZ指令。與CMPS相搭配時,REPE/REPZ指令不會在CMPS失敗的時候遞減cx/ecx/rcx寄存器。與SCAS相搭配時,REPNE/REPNZ指令總是會遞減cx/ecx/rcx寄存器,而不考慮SCAS指令是成功還是失敗。SCAS指令最基礎的用法是單獨以SCAS+后綴的形式使用。
STOS(store string, 存儲字符串):很適合用來初始化數組。它會把累加寄存器里的值復制到由di/edi/rdi寄存器所表示的內存位置上。如果有多個位置(例如整個數組)都需要用這個值來初始化,那么可以將其與REP結合起來使用。STOS無法用不同的值來初始化數組中的不同元素,它只能將同一個值復制到數組中的每一個位置上。STOS指令最基礎的用法是單獨以STOS+后綴的形式使用。
LODS(load string, 加載字符串):與STOS相反,它不是把值從累加寄存器復制到di/edi/rdi所表示的地址上,而是從si/esi/rsi所指的地址上取值并將其復制到累加寄存器中。與STOS不同,這條指令一般不與重復指令相結合,因此那樣做會導致累加寄存器頻繁遭到覆寫。LODS指令最基礎的形式是LODS+后綴。
7.4 結構體:是復合的數據類型,與高級語言類似,匯編語言中的結構體也是由用戶所定義的數據類型,能夠包含多個字段,這些字段的數據類型可以互不相同。數組中的所有元素都必須一樣大,而結構體中的每個元素大小則未必相同。沒有能把整個結構體所占據的字節數告訴編譯器的命令,因此,它需要知道結構體的起始位置及結束位置。知道了這兩個位置,NASM就可以據此推斷出結構體的總尺寸,這就好比知道了當前位置計數器與數組的起始位置我們就可以用EQU來確定數組長度。
由于結構體不是內置的數據類型,因此必須先把它定義出來,然后才能聲明這種結構體的實例。結構體必須定義在code與data段之外,也就是稱為absolute段的地方,或者也可以定義在另一份文件中并通過INCLUDE命令將其包含進來。這兩種寫法可以確保結構體在實例化之前已經具備適當的定義。結構體定義、聲明及使用范例如下表所示:有的時候必須通過.ALIGN、.ALIGNB等對齊命令來確保結構體及其成員能夠對齊到適當的內存邊界處。
8. 浮點運算
8.2 浮點數的表示方式:計算機中的浮點值(floating-point value)是一種用來近似表示實數的數據形式,這種值的小數點前后通常都有一些數位。浮點數由有效數(significand)、基數與指數三部分組成,其中的有效數部分可以容納固定個數的有效數位,而基數則用來與指數相配合,以便對有效數放大或縮小。所謂浮點是指有效數部分只中的小數點能夠左右移動,只要相應地調整基數的指數,就能保持整個值不變。
IEEE表示法:1985年,電氣電子工程師協會發布了IEEE 754技術標準,用以規范浮點數的計算。這份標準在2008年更新為IEEE 754-2008標準。下表列出了幾種常見的IEEE浮點數格式,其中最常用的是單精度、雙精度與擴展雙精度格式。四倍精度雖然也定義在IEEE標準中,但是一般的硬件都不支持。
下表演示了把78.375轉換成IEEE 754單精度格式的過程:
特殊值:這些值包括帶符號的0、無窮,以及NaN(Not-a-Number, 非數)。下表以十六進制形式列出了這些特殊的值:NaN是一種用來表示未定義值或假想值的數據。
次正規數(subnormal number):也叫做非正規數(denormalized number),它們可以表示正規范圍以外的值。
舍入:舍入模式如下表所示:用浮點值做運算或者在不同的浮點格式之間轉換時,需要考慮舍入方法。大多數系統的默認舍入方法是向最近的值舍入(round to nearest)。
8.3 浮點數的實現:為了支持浮點數,Intel開發了8087及80386輔助處理器用以充當FPU(floating point unit, 浮點運算單元),從而與CPU協同運作。這些FPU實現了名為x87的指令集架構。從80486/487開始,FPU集成到了CPU中。
x87:定義了自己的一套指令、寄存器及浮點數格式以執行浮點運算。下表列出了x87寄存器:
浮點運算的通用寄存器是R0至R7這八個寄存器。系統每執行完一條FPU指令就會把該指令的內存地址放入Last Instruction Pointer中,如果該指令還帶有操作數,那么操作數的內存地址會放在Last Data Pointer中,如下表所示。把剛剛執行過的那條指令及其操作數所處的內存地址記錄下來,是為了給異常處理程序提供狀態信息。
狀態寄存器用來保存FPU的當前狀態,如下表所示:其中11至13號這三位合稱TOP,用來表示棧頂(top-of-stack)。FPU的8個數據寄存器(R0至R7)形成循環棧,其中任何一個寄存器都有可能充當棧頂。如果向R0推入一個值,那么下一個值就會推入R7中。從棧中刪除元素時也會出現這樣的循環現象,只不過方向與推入元素時相反。為了利用這種特性,開發者并不直接以R0至R7這樣的名稱來指代寄存器,而是以ST0至ST7等寫法來指代它們,哪個R寄存器充當棧頂就把哪個R寄存器叫做ST0.這個R寄存器在整個R系列中的序號保存在FPU狀態寄存器的TOP字段中。
狀態寄存器中的各個二進制,其含義如下:
(1).FPU busy(15號位):當FPU正在執行某條指令時,該位會設置成1.
(2).Condition codes(8至10號位,以及14號位):用來表示浮點算術的結果。
(3).TOP(11至13號位):用來表示目前充當棧頂的R寄存器是第幾個R寄存器。
(4).Error Status(7號位):如果正在處理FPU異常,那么該位的值是1.
(5).Stack Fault(6號位):如果置位,那么意味著FPU棧發生了上溢或下溢(試圖把數據加載到本身已經具備有效數值的寄存器中,或者試圖從已經不含有效數值的寄存器里獲取數據)。
(6).Exceptions(0至5號位):P位表示precision(精度受損),U位表示underflow(下溢),O位表示overflow(上溢),Z位表示zero divide(除以0),D位表示denormalized operand(非正規的操作數),I位表示invalid operation(無效操作)。其中,P、U、O三種異常,有可能發生在執行完某項操作之后,而Z、D、I這三種異常則有可能發生在即將執行某項操作之前。
下表描述了FPU的控制寄存器(Control Register),其中有一些二進制位用來控制無窮、舍入以及精度等幾個方面,還有一些二進制位充當異常掩碼。每一個掩碼位都對應于狀態寄存器中相應異常位。執行完FPU初始化(FPU initialization, FINIT)指令之后,所有的掩碼位都會默認設置成1,這意味著所有的浮點異常都交由FPU處理。如果開發者把某個掩碼位設置為0,那么發生相應的浮點異常時系統就會產生中斷,從而把異常交給程序來處理。6、7、13、14及15號位是保留位,或者說暫時沒有用到的二進制位。12號位叫做infinity control位,當今的x87處理器已經不使用這一位了。每一個異常掩碼位所控制的異常:PM控制precisioon(精度受損),UM控制underflow(下溢),OM控制overflow(上溢),ZM控制zero divide(除以0),DM控制denormalized operand(非正規的操作數),IM控制invalid operation(無效操作)。
x87 FPU接受7種格式的數字:單精度、雙精度、擴展雙精度、單字整數、雙字整數、四字整數,以及壓縮的BCD整數(packed BCD integer, 其中BCD的意思是binary coded decimal,也就是用二進制碼來表示的十進制數)。
值加載到FPU的時候默認會轉換成擴展雙精度格式(該格式所占據的二進制位個數為80),這樣可以令精確程度最高。如果要把這樣的值存入內存,可以沿用擴展雙精度格式,也可以轉換為更為短小的格式。控制寄存器里的精度控制(PC)字段能夠控制格式。擴展雙精度與壓縮BCD這兩種格式的支持情況在各平臺及匯編器之間可能有所不同。
x87 FPU采用后綴表示法(postfix notation)來做計算(這和某些計數器是一樣的)。比方說,做加法的時候是先把有待相加的兩個操作數推入棧中,然后再處理它們之間的加法。
x87的FPU指令以字母F開頭。第二個字母如果是B或I,意味著內存操作數應該解讀為壓縮的BCD格式或整數格式,若不是這兩種字母則應解讀為浮點數格式。
MASM要求棧寄存器的序號必須用一對圓括號括起來,而NASM則不要求寫出這對括號。
x87 FPU中還有兩個寄存器也用來保存與FPU有關的信息。其中一個是Tag Register(標記寄存器),它用來表示FPU數據寄存器(也就是R0至R7這8個寄存器)中存放的是何種內容。有了Tag Register,開發者就可以先判斷相關寄存器中的內容是否合適,然后再去執行更為復雜的指令或解碼任務。FSTENV/FNSTENV或FSAVE/FNASVE指令可以把FPU的Tag Register存儲到內存中。軟件不能直接修改該寄存器。與某個R寄存器相對應的tag字段如果是00,那么表示該R寄存器中是個有效的非零值。如果是01則表示其內容是0或與0等價的值。如果是10則表示其內容是NaN、inf(無窮)、-inf(負無窮)或非正規數等特殊值。如果是11則表示該R寄存器是空的。FINIT指令或彈出指令會把Tag Register中相應字段標為empty。
另外一個是Opcode Register(操作碼寄存器),它用來存放最近執行的那條非控制指令。
MMX:本身并非用于浮點數計算,而是通過FPU寄存器來操作整數。Intel在1996年發布P55C(80503)處理器時引入了這項技術。MMX處理器配有8個可用于整數運算的64位寄存器以及47條指令。這8個64位寄存器分別用MM0至MM7來表示,它們其實是80位FPU數據寄存器R0至R7中有效數那一部分的別名。
MMX技術有兩大特性,一個是壓縮的整數型數據(packed integer data),另一個是單指令流多數據流(Single Instruction Multiple Data, SIMD)。64位的MMX寄存器可以存放壓縮形式的字節、字及雙字。
向MMX寄存器中移入數據或是將MMX寄存器中的數據移出,依然要按照普通的方式來做,或者要以四字(也就是64個二進制位)為單位來做,不過,保存在寄存器中的這些壓縮數據,卻可以在同一時間分別參與各自的算術運算。像這種通過一條指令并行地操作多個數據點的理念稱為SIMD模型。
雖然MMX與FPU指令操作的是同一套寄存器,但這兩種指令其實可以用在同一個程序中,只不過這樣做會產生一些開銷。
SSE(Streaming SIMD Extensions, 流式SIMD擴展):構建在MMX技術之上。MMX所處理的數據需要以壓縮的整數形式保存在64位寄存器中,而SSE所處理的浮點數據則既可以用壓縮的形式保存又可以直接用標量的形式保存,而且是保存在128位寄存器中的。請注意,凡是提到壓縮形式的數據,就意味著這些數據能夠在程序運行過程中并行地得到處理。
SSE指令可以分成四大類:用于操作壓縮浮點數及標量浮點數的指令;SIMD整數指令;狀態管理指令;緩存控制指令、用來對指令的執行順序/內存的操作順序做出安排的指令。
可以在32位模式下使用的128位SSE寄存器是xmm0至xmm7 8個XMM寄存器。在64位模式下還可以再使用8個寄存器,也就是xmm8至xmm15。必須注意,在兼容SSE的處理器中,這些寄存器并不是其它寄存器的別名而是單獨的寄存器。XMM寄存器只能存放數據不能用來存放地址。
SSE還有一個32位的控制及狀態寄存器叫做MXCSR,它與x87的控制寄存器及狀態寄存器類似。MXCSR包含一些與浮點異常及舍入控制有關的標志與掩碼,還有各種用來控制SIMD操作的標志。
SSE只支持一種數據類型即單精度的浮點數。SSE2支持更多的數據類型,其中包括雙精度浮點數、四字整數、雙字整數、單字整數(也稱為短整數,short)、字節整數。SSE的標量指令僅適用XMM寄存器中最低的雙字或四字部分來執行浮點運算。大多數SSE指令都有兩種形式,一種針對標量形式的數據,另一種針對壓縮形式的數據。目前的大多數C++編譯器都采用標量形式的SSE指令計算浮點數,而不會自動采用壓縮形式的SSE指令來并行地執行多項計算。Intel的C++編譯器是個例外。
對于MMX、SSE及SSE2技術來說,所有的算術與比較運算都可以在寄存器所壓縮的各項數據之間并行地執行。SSE3除了能像早前的技術那樣同時給某一對寄存器內的多份數據執行計算之外,還能在不同的寄存器上同時執行各自的運算,并把運算結果匯總到目標寄存器的相應部分中。
AVX(Advanced Vector Extensions, 高級向量擴展):能夠向后兼容SSE。出現了三套高級擴展指令,它們分別是:AVX指令集、AVX2指令集以及AVX-512指令集。這些擴展指令集較大地提升了寄存器的容量,并使得開發者能夠運用更多的指令來編寫程序。
Windows下鏈接匯編代碼及C++代碼操作步驟(x64):
(1).創建一個Win32控制臺應用程序AssemblyLanguage_Test,并將其調整為x64;
(2).添加幾個新文件,AssemblyLanguage_Test.cpp里面主要包含main函數;funset.asm里面包含了一些匯編代碼;
(3).通過ml64.exe對funset.asm做匯編,產生目標文件funset.obj:
A.在C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\amd64目錄下啟動cmd.exe;
B.運行vcvars64.bat或者直接將vcvars64.bat直接拖到cmd中;
C.定位到E:\GitCode\CUDA_Test\demo\AssemblyLanguage_Test\masm,執行:$ ml64.exe /c /Cx funset.asm,將會生成funset.obj,執行結果如下圖所示:
(4).將funset.obj添加到工程中;
(5).生成并運行程序。
如果修改了funset.asm,那么必須通過ml64.exe重新生成funset.obj。
Ubuntu下鏈接匯編代碼及C++代碼操作步驟(x64):
(1).添加幾個新文件,AssemblyLanguage_Test.cpp里面主要包含main函數;funset.asm里面包含了一些匯編代碼;
(2).build.sh內容如下:
#! /bin/bashreal_path=$(realpath $0)
echo "real_path: ${real_path}"
dir_name=`dirname "${real_path}"`
echo "dir_name: ${dir_name}"new_dir_name=${dir_name}/build
if [[ -d ${new_dir_name} ]]; thenecho "directory already exists: ${new_dir_name}"
elseecho "directory does not exist: ${new_dir_name}, need to create"mkdir -p ${new_dir_name}
firc=$?
if [[ ${rc} != 0 ]]; thenecho "########## Error: some of thess commands have errors above, please check"exit ${rc}
ficd ${new_dir_name}
cmake ..
makecd -
(3).CMakeLists.txt內容如下:
PROJECT(AssemblyLanguage_Test)
CMAKE_MINIMUM_REQUIRED(VERSION 3.0)# support C++11
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c11")
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
# support C++14, when gcc version > 5.1, use -std=c++14 instead of c++1y
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++1y")IF(NOT CMAKE_BUILD_TYPE)SET(CMAKE_BUILD_TYPE "Release")SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -O2")SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -O2")
ELSE()SET(CMAKE_BUILD_TYPE "Debug")SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -Wall -O2")SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -Wall -O2")
ENDIF()
MESSAGE(STATUS "cmake build type: ${CMAKE_BUILD_TYPE}")MESSAGE(STATUS "cmake current source dir: ${CMAKE_CURRENT_SOURCE_DIR}")
SET(PATH_TEST_CPP_FILES ${CMAKE_CURRENT_SOURCE_DIR}/./../../demo/AssemblyLanguage_Test)
SET(PATH_TEST_NASM_FILES ${CMAKE_CURRENT_SOURCE_DIR}/./../../demo/AssemblyLanguage_Test/nasm)
MESSAGE(STATUS "path test files: ${PATH_TEST_CPP_FILES} ${PATH_TEST_NASM_FILES}")# head file search path
INCLUDE_DIRECTORIES(${PATH_TEST_CPP_FILES})FILE(GLOB TEST_CPP_LIST ${PATH_TEST_CPP_FILES}/*.cpp)
FILE(GLOB TEST_CPP2_LIST ${PATH_TEST_CPP_FILES}/nasm/*.cpp)
MESSAGE(STATUS "test cpp list: ${TEST_CPP_LIST} ${TEST_CPP2_LIST}")FILE(GLOB_RECURSE TEST_NASM_LIST ${PATH_TEST_NASM_FILES}/*.asm)
MESSAGE(STATUS "test nasm list: ${TEST_NASM_LIST}")# build nasm
SET(CMAKE_ASM_NASM_FLAGS_DEBUG_INIT "-g")
SET(CMAKE_ASM_NASM_FLAGS_RELWITHDEBINFO_INIT "-g")IF(NOT DEFINED CMAKE_ASM_NASM_COMPILER AND DEFINED ENV{ASM_NASM})SET(CMAKE_ASM_NASM_COMPILER $ENV{ASM_NASM})
ENDIF()ENABLE_LANGUAGE(ASM_NASM)
MESSAGE(STATUS "CMAKE_ASM_NASM_COMPILER = ${CMAKE_ASM_NASM_COMPILER}")IF(CMAKE_ASM_NASM_OBJECT_FORMAT MATCHES "elf*")SET(CMAKE_ASM_NASM_FLAGS "${CMAKE_ASM_NASM_FLAGS} -DELF")SET(CMAKE_ASM_NASM_DEBUG_FORMAT "dwarf2")
ENDIF()SET(CMAKE_ASM_NASM_FLAGS "${CMAKE_ASM_NASM_FLAGS} -D__x86_64__")MESSAGE(STATUS "CMAKE_ASM_NASM_OBJECT_FORMAT = ${CMAKE_ASM_NASM_OBJECT_FORMAT}")IF(NOT CMAKE_ASM_NASM_OBJECT_FORMAT)MESSAGE(FATAL_ERROR "could not determine NASM object format")return()
ENDIF()get_filename_component(CMAKE_ASM_NASM_COMPILER_TYPE "${CMAKE_ASM_NASM_COMPILER}" NAME_WE)
MESSAGE(STATUS "CMAKE_ASM_NASM_COMPILER_TYPE = ${CMAKE_ASM_NASM_COMPILER_TYPE}")IF(NOT WIN32 AND (CMAKE_POSITION_INDEPENDENT_CODE OR ENABLE_SHARED))SET(CMAKE_ASM_NASM_FLAGS "${CMAKE_ASM_NASM_FLAGS} -DPIC")
ENDIF()ADD_LIBRARY(simd OBJECT ${TEST_NASM_LIST})
IF(NOT WIN32 AND (CMAKE_POSITION_INDEPENDENT_CODE OR ENABLE_SHARED))set_target_properties(simd PROPERTIES POSITION_INDEPENDENT_CODE 1)
ENDIF()# build executable program
ADD_EXECUTABLE(AssemblyLanguage_Test ${TEST_CPP_LIST} ${TEST_CPP2_LIST})
TARGET_LINK_LIBRARIES(AssemblyLanguage_Test simd)
(4).執行:$ ./build.sh; ./build/AssemblyLanguage_Test
GitHub:https://github.com/fengbingchun/CUDA_Test
總結
以上是生活随笔為你收集整理的汇编程序设计与计算机体系结构软件工程师教程笔记:函数、字符串、浮点运算的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 汇编程序设计与计算机体系结构软件工程师教
- 下一篇: 汇编程序设计与计算机体系结构软件工程师教