Assemble Language Programming(第九章)
来源:互联网 发布:spring源码怎么看 编辑:程序博客网 时间:2024/05/01 13:53
Assemble 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)上放置足夠的指令($:相當於cpu在delay-slot處做nop動作,從而影響了效率,參見1.5.5 programmer-visible pipeline effects)。
2, 每次循環只比較一個字節,使得循環過於頻繁而效率低下($:因為分支(b*)及跳轉(j*)指令會造成流水線的刷新,后續指令被失效)。
我們來修改這段代碼:首先把循環展開,每次循環比較2個字節;把一個load指令調整到循環的末尾——這只是一個小技巧,這樣我們就可以盡可能的在每個branch delay-slot和load 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個字節,在上面我們已經把a0加2了,所以這里是[-1]*/
t2 = a0[-1]; /*$:branch delay-slot*/
t1 = a1[1]; /*先不把a1加2,留到下面的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,就是上面的LEAF、END之類的東西)。上面這個匯編文件就是這樣做的。這里,在把代碼提交給assembler之前,用cpp把兩個頭文件內嵌入匯編代碼文件。Mips/asm.h定義了宏LEAF和宏END(見下面),mips/regdef.h定義了慣用的寄存器的俗稱(conventional name),比如t0和a1(section 2.2.1)。
macro
這里我們用了2個宏定義:LEAF和END。它們在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),將被替代為它的地址。
($:類似於arm里adds 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-style的cpu(例如intel386)指令中,只有2個操作數,Mips assembler也支持這種風格的寫法,目的寄存器destination register可以同時作為一個源操作數source operand:例如:addu rd, rs,這與addu rd,rd,rs相同,assembler將自動將它轉換成后者。
Mips assembler提供的指令集中有一些偽指令unary operation,比如Neg,not,這些偽指令實際上是一條或多條機器指令的組合。對這些指令,Assembler最大接受2個操作數。Negu rd, rs實際上被轉化為subu rd, zero,rs,而not rd將被轉化為or rd,zero,rs。
可能最常用的寄存器間操作register-register operation要算是move rd,rs了。這條指令實際上是or rd,zero,rs。
9.3.2: 帶立即數的運算指令
在assembler和機器語言里,嵌入在指令中的常數被稱為立即數immediate value。很多Mips的算術和邏輯指令都有另外一種形式,這種形式里rt寄存器被一個16bit的立即數所取代。在cpu的內部運算過程中,這個立即數將被擴展為32bit,可能是符號擴展sign-extend($:用最左面的bit(bit15)填充擴展的高16bit),也可能是零擴展zero-extend($:用0填充擴展的高16bit)——這取決於具體的指令。一般而言,算術指令進行符號擴展sign-extend,而邏輯指令進行零擴展zero-extend。
在機器指令的概念上,即便執行同一種運算,操作數中是否包含立即數的區別,將導致兩條不同的指令(例如add與addi)。盡管如此,對於程序員而言,還是沒有太大的必要去具體的區分那些包含立即數的指令。Assembler會找出它們,並進行轉換。比如:
addu $2,$4,64 ————————> addiu $2,$4,64
如果立即數過大而超過了16bit所能表達的範圍,機器碼中將無法容納,這時assembler會再次幫助我們:它會自動將立即數載入“編譯用臨時寄存器assembler temporary register”at/$1中,然后進行如下操作:
add $4, 0x12345 —————————>
- Assemble Language Programming(第九章)
- assemble language学习(-)
- Programming Language
- The C++ Programming Language 第二章
- The C++ Programming Language 第三章
- The C++ Programming Language 第四章
- The C++ Programming Language 第五章
- The C++ Programming Language 第五章 作业
- The C++ Programming Language 第六章 笔记
- The C++ Programming Language 第六章 作业
- The C++ Programming Language 第七章 笔记
- The C++ Programming Language 第七章 作业
- The C++ Programming Language 第八章 笔记
- 读书笔记:《The C++ programming Language》 第三章
- 读书笔记:《The C++ programming Language》 第四章
- 读书笔记:《The C++ programming Language》 第五章
- the c programming language 习题 第二章
- The C Programming Language 第1章
- Linux_Shell知多少—常用正则表达式
- SQL2005删除复制数据库的发布与订阅的方法
- .overlay
- 去除 struts2 标签中的自动生成的布局样式
- C/C++ 常量--林锐
- Assemble Language Programming(第九章)
- Something about openssh
- c# FTP的两种实现方法(一)-FtpWebRequest
- error LNK2001: unresolved external symbol _main解决办法
- 页面右下角div提示,显示效果从下往上移动
- Android NDK 程序小记
- spring自动装配
- Oracle PL/SQL 合并文本列
- js验证及限制文本框输入