官术网_书友最值得收藏!

1.2 匯編語言的使用場景

很多使用高級語言編程的人員從業之初甚至從業多年都會有這樣的疑問:現在大多數應用程序都是使用高級語言進行編程,學習匯編語言有什么用處?下面列舉幾個匯編語言的使用場景。

1.2.1 場景1——快速定位問題和分析問題

舉一個不太“燒腦”的小例子:浮點數例外。多數程序開發者在工作中都會遇到異常信號SIGFPE,當程序執行除法運算語句時,如果被除數為0,那么系統就會毫不留情地發送給你一個信號SIGFPE(中文或許顯示“浮點數例外”)。這樣一個異常的C語言代碼如下:

   int test(int a, int b){
       return a/b;                                                             
   }

當我們調用函數test時 ,故意給參數b傳入0,那么就會收到“浮點數例外”。使用調試工具GDB(GNU Debugger ,Linux系統下的調試工具)在龍芯平臺上調試這段代碼時,可以獲取如下信息:

Program received signal SIGFPE, Arithmetic exception.
0x00000001200006ec in test ()
(gdb) bt
#0  0x00000001200006ec in test ()
#1  0x0000000120000734 in main ()

這里GDB已經列出了函數調用棧,即函數main調用了函數test,在執行函數test中地址為0x00000001200006ec 處的指令時,觸發異常SIGFPE。那么0x00000001200006ec處的指令是什么呢?我們可以使用GDB進一步確認。

(gdb) x/5i $pc-12
  0x1200006e0 <test+40>: ld.w    $r12,$r22,-24(0xfe8)
  0x1200006e4 <test+44>: div.w   $r14,$r13,$r12
  0x1200006e8 <test+48>: bne     $r12,$r0,8(0x8) # 0x1200006f0 <test+56>
=> 0x1200006ec <test+52>: break  0x7
  0x1200006f0 <test+56>:  move   $r12,$r14

其中 => 標識了當前PC(Program Counter,PC用來存放當前欲執行指令的地址)位置,即當前程序停在的位置。上面的匯編指令div.w $r14,$r13,$r12 為除法指令,實現用寄存器 $r13除以$r12,將結果寫入$r14 。匯編指令bne $r12,$r0,8(0x8) # 0x1200006f0 是條件跳轉指令,判斷被除數$r12是否等于0(寄存器$r0 為特殊寄存器,其值永遠為0),如果不相等則跳轉到地址0x1200006f0 處繼續執行,否則就不跳轉,執行接下來的匯編指令break 0x7。break 指令將無條件觸發斷點例外,其參數0x7對應SIGFPE。至此,我們就知道了當前程序異常是由除法指令中的被除數為0引起的,對應的C語言代碼就是return a/b;,語句中的b為0。

其實對大型軟件的異常問題定位,基本都是這個思路。不過好在很多大型軟件都會內置一套完整的異常處理機制,在異常發生時,可自動收集異常原因、異常進程、異常位置、棧回溯等信息,比如Java虛擬機中提供的捕獲異常、Android系統中的tombstone。盡管如此,我們還是有可能遇到異常處理機制無法捕獲的異常(漏網之魚),這時掌握一些調試工具的使用方法和匯編語言的知識是很有必要的。

對于GDB工具的使用,在后面章節還會有更詳細的介紹。

1.2.2 場景2——性能分析和優化

了解計算機體系架構和匯編語言有助于我們深入分析軟件性能瓶頸。雖然編譯器已經做了大部分的性能優化工作,比如C/C++語言的編譯器GCC(GNU Compiler Collection,GNU編譯器組件)編譯時使用-O3比-O1可以帶來更進一步的性能優化;支持Java虛擬機根據函數大小及函數被使用的次數來動態調整優化策略。但是在特定場景中,這些還是不夠用,比如游戲引擎、音視頻的編解碼等領域,會經常遇到和算法相關的大數據量數學運算。這時如果我們會使用匯編語言,就可以更進一步做針對特定處理器的優化工作。比如多數處理器中都實現了單指令流多數據流(Single-Instruction stream Multiple-Data stream, SIMD)功能的匯編指令,亦稱為向量指令,其可實現一條指令操作多組數據。龍芯架構LoongArch中也實現了SIMD,包括向量擴展(Loongson SIMD Extension,LSX)和高級向量擴展(Loongson Advanced SIMD Extension,LASX),其中LSX為128位向量位寬,LASX為256位向量位寬。

舉一個使用龍芯LASX實現程序優化的小例子。下面的代碼(使用C語言實現)實現a數組與b數組中的各項數據相加,將結果寫入c數組的加法運算。這里假設數組類型為整型int(32位),循環長度為10000。

for(int i = 0; i < 10000; i++)
  c[i] = a[i] + b[i];

使用GCC編譯后,生成的最終可供CPU執行的指令如下:

//LoongArch匯編指令
L:
ld.w  t1, a1, 0         # 加載數組a[i]值到寄存器t1
add.w t3,  t1, t2       # 實現a[i]+b[i],將結果存入寄存器t3
st.w  t3,  t4, 0        # t3數據寫回c[i]
addi.d a1, a1, 4        # 數組a[]累加4,即指向a[i+1]
addi.d a1, a2, 4        # 數組b[]累加 4
addi.d t4, t4, 4        # 數組c[]累加 4
 
bne  a5, a6, L          # 判斷若for()沒有結束,跳轉到L,繼續執行

上面這段指令實現了循環操作c[i] = a[i] + b[i],相關指令格式在后面章節還會有詳細介紹。在這里可以看出要實現兩個長度為10000的整型數組加法運算,CPU要循環執行10000次,每次循環要執行8條指令,那么完成整個功能要執行80000條指令。

同樣的功能,用龍芯LASX指令實現如下:

//LoongArch 匯編指令
L:
xvld    x1, a1, 0             # 加載數組a[]中的8組整型值到向量寄存器x1
xvld    x2, a2, 0             # 加載數組b[]中的8組整型值到向量寄存器x2
xvadd.w x3, x1, x2            # a[i…i+8]+b[i…i+8],將結果存入向量寄存器x3
xvst    x3, t4, 0             # 把x3數據寫回數組c[i…i+8]
addi.d  a1, a1, 32            # 數組a[]地址累加32,即指向a[i+9]
addi.d  a1, a2, 32            # 數組b[]地址累加32
addi.d  t4, t4, 32            # 數組c[]地址累加32
bne     a5, a6, L             # 判斷若for()沒有結束,跳轉到L,繼續執行

龍芯LASX指令是256位寬(即向量寄存器的長度),故循環一次可以完成8組整型值(8×32位)的加法運算。循環一次也是執行8條指令,但總的循環次數僅為1250次(10000/8),那么完成整個功能執行10000(1250×8)條指令即可,在理論上是GCC編譯器生成的普通指令執行性能的8倍。在本書的第10章將專門介紹和指令架構相關的性能優化基本思路和方法。

1.2.3 場景3——完成高級語言無法實現的功能

在一些基礎軟件的源代碼中,比如數據庫、GCC編譯器、OpenJDK等,我們能頻繁看到匯編語言的身影。因為它們作為應用軟件的支撐或工具,相對于應用軟件在運行邏輯上更靠近CPU,也就更可能出現和計算機體系架構相關的功能要求。例如,GCC編譯器負責將C/C++語言翻譯成和計算機體系架構相關的匯編語言;Java語言開發者熟知的OpenJDK負責Java語言到機器指令的動態翻譯和執行。這方面的軟件從業者就不僅要熟知某種高級語言,還要熟知特定處理器支持的匯編語言。

例如有這樣一個問題:在C語言中如何獲取程序運行的當前PC值?不同架構有不同的方式,在龍芯平臺上可以通過如下內嵌匯編來實現。

 static long * get_PC(void){
     unsigned long *val;                                                          
     __asm__ volatile ("move %0, $r1" : "=r"(val));                               
     return val;                                                                  
  } 

這里__asm__是內嵌匯編指令,用來實現匯編語言和C語言的混合編程(后面會有專門章節來詳細介紹其語法規范)。這里只需關注核心匯編指令move %0, $r1 。在龍芯架構寄存器使用約定里,寄存器$r1存放了函數的返回地址,%0代表val,所以move %0, $r1就完成了把當前函數的返回地址存到變量val中。而當前函數的返回地址就是調用該函數時的PC,所以你就可以通過調用這個函數來獲取當前位置的PC。

匯編語言也是編寫嵌入式設備上程序的理想工具。和通用計算機處理器相比,嵌入式設備(比如電話、打印機、門禁設備等)的典型特征是沒有大容量內存,這就要求其上的程序盡量短小。如果使用高級語言編寫,經過編譯器翻譯后的機器指令可能會有一些冗余,例如大量的函數調用開銷、動態庫加載(盡管程序中僅用了某個動態庫的幾個函數)等。如果直接使用匯編語言進行針對性的編寫,那么內存占用肯定最少,因此匯編語言特別適合編寫嵌入式程序。后面章節會專門介紹如何編寫一個脫離libc庫的程序示例。

主站蜘蛛池模板: 宜州市| 兰坪| 宜川县| 微山县| 汕尾市| 葵青区| 文安县| 鄢陵县| 镇安县| 陆良县| 松潘县| 绥宁县| 高唐县| 海南省| 长宁县| 治县。| 民县| 通州区| 昂仁县| 贵州省| 莒南县| 钟祥市| 肇州县| 大石桥市| 芜湖县| 多伦县| 奉节县| 巴林左旗| 潜江市| 林周县| 无锡市| 云浮市| 沙河市| 曲阜市| 富川| 宣城市| 承德市| 洛宁县| 安陆市| 全州县| 花垣县|