Assemble Language Programming(第九章)

来源:互联网 发布:spring源码怎么看 编辑:程序博客网 时间:2024/05/01 13:53

Assemble Language Programming 第九章

分類:MIPS架構學習筆記
2009/01/26 15:50
Assembler language programming

這一章將告訴你如何閱讀並編寫MIPS體系下的匯編代碼。MIPS匯編代碼看上去與實際的代碼差異很大,這主要是因為以下原因:

1,      MIPS匯編編譯器(assembler)提供了大量的已經預定義的宏指令(extra macro-instruction)。所以編譯器的指令集(instruction set)要比CPU實際提供的指令集大的多。

2,在MIPS匯編代碼中有許多偽操作符,放在代碼開始和結束的地方,用來預定義常用數據,控制指令排列順序,以及控制對代碼的優化。通常它們被稱為“directives”或“pseudops”。

3,實際應用中,匯編代碼往往要經過C語言預處理器(C preprocessor)的處理后,才被提交給assembler進行編譯。C語言預處理器將匯編代碼中的宏,用它自己的頭文件中的定義進行替換。這可以使匯編代碼書寫起來稍微方便一點。

在你繼續看下去之前,最好先回去溫習一下Chapter-2的內容,包括低層機器碼的構造,數據類型,尋址方式。($:流水線pipeline的知識很值得溫習一下,主要看一下那些該死的延遲點delay-slot)

 

 

9.1      A Simple Example

我們仍然採用在Chapter-8見過的那個例子: C庫函數strcmp(1)。這一次我們的演示的重點是: 匯編語法所必需的符號,以及一些人工優化(hand-optimized)並重排序(hand-scheduled)的代碼。

Int

Strcmp(char* a0, char* a1)

{

char t0, t1;

while(1){

         t0 = a0[0];

         a0 += 1;

         t1 = a1[0];

         a1 += 1;

         if( t0 == 0 )

                 break;

         if( t0 != t1 )

                 break;

}

return ( t0 - t1 );

}

 

這段代碼的運行速度因為以下原因而比較低:

1,           每個循環都會經過兩個條件分支(conditional branch)和兩個提取指令(load),而我們沒有在分支延遲點(branch delay-slot)和提取延遲點(load delay-slot)上放置足夠的指令($:相當於cpudelay-slot處做nop動作,從而影響了效率,參見1.5.5 programmer-visible pipeline effects)

2,           每次循環只比較一個字節,使得循環過於頻繁而效率低下($:因為分支(b*)及跳轉(j*)指令會造成流水線的刷新,后續指令被失效)。

我們來修改這段代碼:首先把循環展開,每次循環比較2個字節;把一個load指令調整到循環的末尾——這只是一個小技巧,這樣我們就可以盡可能的在每個branch delay-slotload delay-slot處都放上有效的指令了。

       

Int

Strcmp(char* a0, char* a1)

{

char t0, t1,t2;

 

/*因為第一個load被調整到循環的末尾處,所以這里要先取一次值*/

t0 = a0[0];

 

while(1){

         t1 = a1[0];                        /*第一個字節*/

         if( t0 == 0 )

                 break;

         a0 += 2;                           /*$branch delay-slot*/

         if( t0 != t1 )

                 break;

/*2個字節,在上面我們已經把a02了,所以這里是[-1]*/

         t2 = a0[-1];                      /*$branch delay-slot*/

         t1 = a1[1];                        /*先不把a12,留到下面的delay-slot處再加*/

         if( t2 == 0 )

                 return t2-t1;              /*下面匯編代碼里的標志.t21*/

         a1 += 2;                           /*$branch delay-slot*/

         if( t1 != t2 )

                 return t2-t1;              /*下面匯編代碼里的標志.t21*/

         t0 = a0[0];                        /*$branch delay-slot*/

}

/*下面匯編代碼里的標志.t01*/

return ( t0 - t1 );

}

 

ok,現在讓我們把這段代碼轉成匯編來看看。

 

#include <mips/asm.h>

#include <mips/regdef.h>

 

LEAF(strcmp)

        .set           nowarn

        .set           noreorder

        lbu            t1,    0(a1);

1:    

beq           t0,    zero, .t01                  #load delay-slot

        addu         a0,   a0,2                         #branch delay-slot

        bne           t0,    t1,.t01

        lbu            t2,    -1(a0)                      #branch delay-slot

        lbu            t1,    1(a1)                        #load delay-slot

        beq          t2,    zero,.t21

        addu         a1,   a1,2                         #branch delay-slot

        beq          t2,    t1,1b

        lbu            t0,    0(a0)                        #branch delay-slot

 

.t21:

        j               ra

        subu         v0,   t2,t1                         #branch delay-slot

.t01:

        j               ra

        subu         v0,   t0,t1                         #branch delay-slot

        .set           reorder

END(strcmp)

 

Even without all the scheduling,這里已經有很多有意思的東西了,讓我們來看看。

 

#include   

這是個好主意:由C語言預處理器cpp來對常量進行宏定義,並引入一些預定義的文本宏($text-subsitution macro,就是上面的LEAFEND之類的東西)。上面這個匯編文件就是這樣做的。這里,在把代碼提交給assembler之前,用cpp把兩個頭文件內嵌入匯編代碼文件。Mips/asm.h定義了宏LEAF和宏END(見下面),mips/regdef.h定義了慣用的寄存器的俗稱(conventional name),比如t0a1(section 2.2.1)

 

macro

        這里我們用了2個宏定義:LEAFEND。它們在mips/asm.h中定義被如下:

 #define           LEAF(name)    /

        .text; /

        .globl        name;       /

        .ent           name;       /

name:

        LEAF被用來定義一個簡單子函數(simple subroutine),如果一個函數體內不調用其它函數,那么相對於整個調用樹(calling tree)而言,這個函數就是調用樹上的一片“葉子”,因此得名“leaf”。相對的,一個需要調用其它函數的函數,叫“nonleaf”,nonleaf函數必須多做很多麻煩的事情例如保存寄存器和返回地址,不過很少會真的需要自己寫一個nonleaf 的匯編代碼($:這通常用 C語言來寫)。注意下面:

        .text          表示這段用匯編寫成的代碼應該放在“.text”段中,“.text”是C語言程序的代碼段。

        .globl        聲明“name”為全局變量,在模塊的符號表(symbol table)中作為全局唯一的符號而存在($:全局變量在整個程序內唯一;局部變量在其所在函數體中唯一;static變量在其所在文件內唯一)

        .ent           對程序而言沒有實際意義,只是告訴assembler將這一點標志為“name”函數的起始點,為調試提供信息。

        .name       將其所在地址命名為“name”,作為assmbler的輸出。名為“name”的函數調用將從該地址開始。

END定義了兩個assembler需要的信息,都不是必須的。

#define     END(name)      /

        .size          name,       .-name;     /

        .end          name

        .size          表示在symbol table中,“name”函數體的大小(字節數)將與“name”符號一道列出。

        .end          指出函數尾。調試用信息。

 

.set           偽操作符(directive),用來告訴assembler如何編譯。

在本例中,.noreorder表示禁止對代碼重排序,讓代碼嚴格保持其書寫的順序,否則MIPS assembler會嘗試將代碼重新排序——填補那些delay-slot以獲得較好的運行效率。Nowarn要求assembler不要費心去指出那些應該被重排序的地方,相信程序員已經處理好這些事情了。通常這不是個好主意——除非你確信你肯定正確。基本上這是個不必要的directive

Labels:“1:”是數字標志label,大多數的assembler都會把它當作**局部**label來處理。像“1:”這種label,在程序里你想用多少都可以:你可以用“1f”引用reference下一個“1:”;用“1b”來引用前一個“1:”。這會很常用。

Instructions:一些指令的順序會有出乎預料的問題,你必須注意。.set noreorder這一directive使得delay-slot問題變得非常敏感而容易出問題,我們必須確保load的數據不會馬上被下一條指令用到。比如說:

        bne           t0,    t1,.t01

lbu             t2,    -1(a0)

        ……………

.t01:

        j               ra

subu          v0,   t0,t1        

這里lbu            t2,    -1(a0)一句中,用t2不能用t0,因為要執行的下一條指令subu             v0,   t0,t1         中要用到t0

 

好,已經看過了一個例子,讓我們再看一些語法方面的東西。

 

9.2      語法概要Syntax Overview

在附錄B中你可以找到MIPS匯編器的語法列表,大多數的其它廠商的編譯器也都遵循這個列表的規則。當然,可能少數的directive的具體含義會有少許的差別。如果你以前在類unix(unix-like)的系統上用過assembler,那這個列表你應該會很熟悉。

 

9.2.1    Layout,     Delimiters,       and Identifiers

首先你得熟悉C語言,如果你熟悉C,那么注意,匯編代碼與C代碼有一些區別。

匯編代碼以行為分界,換行(end-of_line)表示一個指令或偽操作符directive的結束。你也可以在一行里寫多條指令或偽操作符,只要它們中間用“;”隔離開來。

以“#”開頭的行是注釋,assembler將忽略它。但是**不要把“#”放在行的最左面**:這將激活C預處理器cpp(C preprocessor),有時候你可能會用到它。如果你確定你的代碼會經過C預處理器的預處理,那么你可以在你的匯編代碼中使用C風格的注釋方式:“/*…*/”,可以跨越多行,只要你樂意。

變量和label的名字(identifiers)可以隨意——只要在C語言里合法就行,甚至可以包含“$”和“。”。

在代碼中你可以使用0~99之間的數字作為label,它會被視為臨時性的符號,所以你可以在代碼中重復使用同一個數字作為label。在一個分支指令(branch instruction)中“1f”指向下一個“1:”,而“1b”指向前一個“1:”,這樣就不用費心為那些隨手而寫的跳轉和循環起名字了,省下這些名稱可以去命名那些子程序、還有那些比較關鍵的跳轉。

MIPS/SGI assembler通過C preprocessor的宏定義來提供寄存器的俗稱(conventional name)($zero,t0,~,ra),所以你必須用C preprocessor來對你的匯編代碼進行預處理,為此需要在代碼中包含include頭文件mips/regdef.h。雖然說規範的assembler通常可以識別這些寄存器的俗稱,但是為了代碼的通用性起見,還是不要把寶壓在這上面為好。

assembler的定位計數器指向正在編譯的當前指令的地址,你可以在匯編代碼中引用assembler的定位計數器的值。標識符“。”代表assembler當前的定位計數器的值。你甚至可以對它做有限的一些操作。在上下文中,label(或者其它什么可復位位的符號relocatable symbol),將被替代為它的地址。

$:類似於armadds r0,pc,symbol address - (+8)這樣的操作。)

        固定字符和字符串的定義方式與C相同。

 

9.3指令規則 General Rules for Instructions

Mips assembler允許一些指令的簡略寫法。有時候,你提供的操作數operand少於機器碼所要求的,或者機器碼要求使用寄存器而你卻使用了常數,在某些情況下,assembler也會允許這種寫法,並自動進行調整。你將會發現,在真正的匯編代碼中這種情況非常頻繁。這一節我們將討論這個問題。

 

9.3.1    寄存器間運算指令

Mips 的運算指令有3個操作數。算術arithmetical或邏輯logical指令有2個輸入和一個輸出,例如:Rd        =      rs     +      rt,被寫成addu     rd,    rs,rt

        這里的3個寄存器可以重復(例如addu      rd,    rd,rd)。在CISC-stylecpu(例如intel386)指令中,只有2個操作數,Mips assembler也支持這種風格的寫法,目的寄存器destination register可以同時作為一個源操作數source operand:例如:addu    rd rs,這與addu         rdrdrs相同,assembler將自動將它轉換成后者。

        Mips assembler提供的指令集中有一些偽指令unary operation,比如Negnot,這些偽指令實際上是一條或多條機器指令的組合。對這些指令,Assembler最大接受2個操作數。Negu   rd rs實際上被轉化為subu        rd zero,rs,而not        rd將被轉化為or    rdzerors

        可能最常用的寄存器間操作register-register operation要算是move  rdrs了。這條指令實際上是or rdzerors

 

9.3.2:   帶立即數的運算指令

assembler和機器語言里,嵌入在指令中的常數被稱為立即數immediate value。很多Mips的算術和邏輯指令都有另外一種形式,這種形式里rt寄存器被一個16bit的立即數所取代。在cpu的內部運算過程中,這個立即數將被擴展為32bit,可能是符號擴展sign-extend$:用最左面的bit(bit15)填充擴展的高16bit),也可能是零擴展zero-extend$:用0填充擴展的高16bit)——這取決於具體的指令。一般而言,算術指令進行符號擴展sign-extend,而邏輯指令進行零擴展zero-extend

在機器指令的概念上,即便執行同一種運算,操作數中是否包含立即數的區別,將導致兩條不同的指令(例如addaddi)。盡管如此,對於程序員而言,還是沒有太大的必要去具體的區分那些包含立即數的指令。Assembler會找出它們,並進行轉換。比如:

addu          $2$464    ————————>      addiu        $2$464

如果立即數過大而超過了16bit所能表達的範圍,機器碼中將無法容納,這時assembler會再次幫助我們:它會自動將立即數載入“編譯用臨時寄存器assembler temporary registerat/$1中,然后進行如下操作:

add   $4        0x12345  —————————>       

原创粉丝点击