这一章将告诉你如何阅读并编写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,现在让我们把这段代码转成汇编来看看。
#i nclude
#i nclude
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,这里已经有很多有意思的东西了,让我们来看看。
#i nclude
这是个好主意:由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 —————————> li at,0x12345
add $4,$4,at
注意这里的“li”(load immediate)指令,在cpu提供的机器指令集中你找不到它。这是一个及其常用的宏指令,用来把32bit整数装载入寄存器,而不用程序员来操心怎幺去实现这一动作:
当这个32bit整数值介于-32k~+32k之间,assembler用addiu指令配合zero寄存器来实现“li”;
当16-31bit为0时,用ori指令来实现“li”;
当 0-15bit为0时,用lui指令来实现“li”;($:运算指令(ori、addiu)要比存取指令(sw、lui)的处理速度快)
如果以上条件都不成立,那只好用lui/ori两条指令来实现“li”了:
li $3,-5 ——————> addiu $3,$0,-5
li $4,08000 ——————> ori $4,$0,08000
li $5,120000 ——————> lui $5,0x12
li $6,0x12345 ——————> lui $6,0x1
ori $6,$6,0x2345
9.3.3 关于32/64位指令
我们在前面(2.7.3)讲过可以对32位指令的机器码进行符号扩展到64位,以保证32位的程序(mipsII)在老的机器上能正常运行。
9.4 地址模式
前面提到过,mips cpu硬件上只支持一种地址模式:寄存器基地址+立即数偏移量base_reg+offset,偏移量必须在-32768到+32767之间(16bit 带符号整型所能表示的范围)。但是assembler可以通过一些方式来支持以下几种地址模式:
Direct:由你提供的数据标号或外部变量名。
Direct+index:一个偏移量,加上由寄存器指出的标号(label)地址。
Constant:一个数字,作为一个32位的绝对地址(absolute address) 处理。
Register indirect:是寄存器加偏移量的特殊形式:偏移量为0。
9.5 assembler directives
MIPS中所有指令都被塞在32bit空间里的做法导致了一个明显的问题:访问一个确定的/嵌入在指令以内的?????(compiled-in location)内存地址往往要花费至少两条指令.例如:
Lw $2, addr -------------à lui at, %hi(addr)
Lw $2, %lo(addr)(at)
在大量使用全局或静态变量的程序中,这一缺陷往往导致最后编译出的代码臃肿而低效.
早期的MIPS编译器引入了一种技术以弥补以上缺陷,这项技术被以后的MIPS编译工具链toolchain一直沿用下来,它通常被称为”全局量指 针相对寻址”gp-relative address.这个技术要求compiler,assembler,linker以及运行时激活代码(runtime startup code)偕同配合,把程序中的’小’变量和常数汇集到一个独立的内存区间;然后设置register $28(通常称为全局量指针global pointer或简写为gp)指向该区间的中央(linker生成一个特殊符号_gp,其地址为该区间的中央,激活代码负责将_gp的地址加载到gp寄存 器,这一动作在第一个load/store指令运行之前完成).只要这些全局变量\静态编量\常量加起来不占用超过64k大小的空间,这些资料相对该区间 的中点也就不超过正负32k(偏移量15bit+符号位1bit,参见mips机器码格式),那幺我们就可以在一条指令中完成对它们的 load/store操作:
lw $2, addr ------à lw $2, addr-_gp(at)
一个问题是在编译彼此独立的模块的时候,compiler和assembler如何决定哪些变量需要通过gp来寻址,通常的做法把所有小于某个特定 长度(通常是8byte)的对象放进该区间,这个长度可以通过compiler/assembler的”-G n”选项来控制.特别需要指出: ”-G 0”将取消gp-relative寻址方式.
上面所说的gp-relative寻址是一种非常有用的技巧,然而在使用中会有一些”陷阱”值得注意.在汇编代码中声明全局量的时侯你最好小心点:
可写,且初始化过的小对象体writable,initialized small data必须显式的声明在.sdata段中.( “小对象体”一词中“小”的含意即为上面提到的”长度小于8byte”)
声明全局对象时必须指出其长度.
.comm. smallobj, 4
.comm. bigobj, 100
声明小外部变量时同样需要指出其长度.
Extern smallext, 4
大多数assembler不会对对象声明作辅助性的处理(如指出该对象的长度).
C代码中的全局变量,必须在所有使用它的模块中被声明.对于外部队列,你可以显式的指出它的长度,如:
Int cmnarray[NARRY];
也可以不指出其长度:
extern int exarray[];
有时候,程序运行的方式(环境)决定了不能采用gp-relative寻址方式.比如一些实时操作系统,还有很多固化环境下的程序(PROM monitor)是用一块单独连接link的代码实现来内核kernel,应用程序直接使用子函数(而不是通常的系统调用)调用到内核中去.这种情况下无 法找到一个和适的方法以使gp寄存器在内核和应用程序的两个小数据段.sdata中来回切换,所以内核与应用程序两者之一(没必要两个都这样做)必须使 用”-G 0”选项来进行编译.
当使用”-G 0”选项编译模块的时候,那些需要与该模块连接的库library通常也应该使用”-G 0”选项编译.在资料是否应该放在.sdata的问题上, 模块和库的声明应该彼此一致,如果发生冲突的话,linker将无法判定资料应该放在小数据段还是普通数据段,这时linker会给出奇怪而毫无价值的错 误信息.
9.5 Assembler Directives
在一开始我们就已经提到过”directive”,你也可以在附禄B里找到它的清单,不过没有详细介绍.
9.5.1 段的选择
通常的数据段和代码段的名字以及对它们的支持在不同编译工具链上可能会不一样.但愿大部分至少能够支持一般的MIPS通用的段,见图9.1.
在汇编代码中,以如下方式来选择段:
.text, .rdata, and .data
简单的把适当的段名放在数据和指令之前,就象下面的样子:
.rdata
msg: asciiz “hello world!\n”
.data
table:
.word 1
.word 2
.word 3
.text
func:sub sp, 64
………………
.lit4 and .lit8 : 隐式浮点常数段floating-point implict constants
你不能像directives一样写这些段.它们是由assembler隐式创建的只读数据段,用来放置li.s和li.d宏指令中的浮点常数型参数.一些assembler和linker会合并相同的常数以节省空间.
.bss, .comm., and .lcomm data
这个段名也不用作directive,它用来收集在C代码中声明的所有未初始化的资料.C的一个特点是: 在不同的模块中可以有同名的定义.只要其中被初始化的不要超过一个..bss段用来收集那些在所有模块中都没有初始化过的数据.fortran程序员可以 认为这个就是fortran语言中的.common段,虽然名字不一样.
你必须声明每个资料的长度(单位为byte),当程序被连结的时候,它就可以得到足够的空间(所有声明中的最大值).如果有任何模块把它声明在初始化过的数据段,这些长度将被用到,并且使用如下声明:
.comm. dbgflag, 4 #global common variable, 4 bytes
.lcomm. sum, 4 #local common variable, 8 bytes
.lcomm. array, 100 #local common variable, 100 bytes
“未初始化uninitialized”这一说法实际上并不准确:虽然这些段在编译出的目标文件中是不占地方的,但是在执行你的程序之前,运行时激 活代码run-time startup code或操作系统会将.bss段清零---------很多C程序都依赖于这一特性.
.sdata, small data, and .sbss
这些段被编译工具链用作单独放置小资料对象的.data和.bss段.MIPS编译工具链进行这个处理是因为,对一个足够紧凑的小资料对象段可以进行高效率的load/store操作,其原理是在gp寄存器中保存一个资料指针,具体说明见本书9.4.1章节.
注意,.sbss不是一个合法的directive;放在.sbss段中的资料满足两个条件:1,用.comm或.lcomm声明;2,其长度小于”G n”编译选项所指定的长度(默认为8byte).
.section
开始一个任意名称的段,并提供旗标(可能在代码中提供,也可能是工具包提供? Which are object code specify and probably toolkit specific).查看你的工具包的说明手册,????????????????????
如图9.1所示的结构可能适合做为一个运行在裸机bare cpu上的固化程序ROM program.只读段倾向于放在远离下部可擦写区间的内存位置上.
堆heap和栈stack并不是真正的能被assembler或linker所识别的段.一般的,它们在运行时由运行系统???????run- time system初始化和维持.通过把sp寄存器设置为该程序可用内存的最高地址(8byte对齐)的方式来定义栈;堆通过一个由类似于malloc函数使用 的全局指针变量来定义,通常初始化为end符号symbol,该symbol被linker赋值为已声明变量的最高位置.
特殊符号
图9.1显示了一些由linker自动声明的符号,以便程序找到自己各个段的起始\结束的位置.这最初只是习惯,后来在unix类的系统上得到发展,其中一些是MIPS环境中所独有的.你的工具包手册上可能定义了他们中的一部分或全部;下面打@的表示肯定会被定义的符号:
symbol standard value
_ftext 代码段起始点
etext @ 代码段结束点
_fdata 数据段起始点
edata @ 数据段结束点
_fbss 未初始化段起始点
end @ 未初始化段结束点
(end通常也就是程序image的结束点)
9.5.3 资料的定义与对齐
选择好正确的段之后,现在你需要用下面所说的directive来定义资料对象本身.
.byte, .half, .word, and .dword
这些directive产生1,2,4,8byte长度的整数(有些工具链-----即便是64位的-------没有提供.dword directive).可以跟随着一个值的列表,彼此以逗号分离,可以在值的后面加冒号并跟随一个重复计数,以表示连续几个相同的值,如下 (word=4byte):
.byte 3 #1 byte: 3
.half 1,2,3 #3 halfwords: 1 2 3
.byte 3 #5 words: 5 5 5 6 7
注意数据的位置(相对于段的起始处)在资料被输出之前自动对齐到合适的边界.如果你确实需要输出未对齐的资料,那幺必须自己使用西面要讲到的.align directive来说明.
.float和.double
这些directive输出单精度\双精度的浮点值.如下:
.float 1.4142175 #1个单精度浮点数
.double 1e+10, 3.1415 #1个双精度浮点数
与对整数处理相同,可以使用冒号表示重复.
.ascii和.asciiz
这些directive输出ASCII字符串, 附带\不附带结束标记.如下两行代码输出相同的字符串:
.ascii “Hello\0”
.asciiz “Hello”
.align
这个directive允许你为下一个资料指定一个大于正常要求的对齐边界??????alignment.该alignment表示为2的n次方.
.align 4 #对齐到16byte边界 (2^4)
var:
.word 0
如果标志(上例中的var)后面紧跟着.align,那幺这个标志仍然可以被正确的对齐,例如下面的例子与上面的例子作用相同:
var:
.align 4 # 对齐到16byte边界(2^4)
.word 0
对要求紧凑结构????packed的数据结构,这个directive允许你取消.half,.word的自动对齐功能,你可以指定它为0对齐,它将持续作用直到下个段开始.??????
.half 3 #正确对齐的半字
.align 0 #关掉自动对齐功能
.word 100 #按照半字对齐的字.
.comm和.lcomm
通过指定对象名和长度来声明一个common或者说未初始化的资料对象.
用.comm声明的对象对所有声明过它的模块有效,它的空间由linker分配,采用所有声明中的最大值.但是,如果其中有任何一个模块将其声明在.data,.sdata,.rdata,那幺所有的长度声明都将失效,而采用初始化定义取而代之.
??????.comm的用途是为了避免以下情况:一个资料对象要在许多文件中用到,但它与其中的每一个文件都没有更特殊的联系,但是我们不得不在 某个文件中声明它,这样就造成了不对称.但是它确实存在,因为fortran就是用这样的语意来定义它的,而我们想要经过汇编语言来编译fortran程 序(比如查看fortran程序编译出来的汇编代码).
用.lcomm声明的对象是局部对象,由assembler在.bss段或.sbss段为其分配空间,但是在所属模块之外它不可见.
.space directive增加当前段的空间计数,例如:
struc: .word 3
.space 120 #空出120byte大小的空间
.word -1
对通常的资料\代码段而言,这个空出的空间用0填充,如果assembler允许你声明内容不在对象文件中定义的段(如.bss),这个空间只影响连续的符号\变量之间的偏移.
9.5.4 符号绑定属性 symbol-binding attributes
符号symbol(在数据段或代码段中的标志)可以被调节为可见?????,并可以供linker使用以便将几个分离的模块编译成一个完整的程序.
符号有三个级别的可见度:
局部:
除了声明它的那个模块,它对外部而言是不可见的,并且不会被linker使用.你不用担心在其它模块中是否适用了相同的符号.
全局:
这些是公开的符号以供linker使用.使用.extern 关键词,你可以在其它模块中引用全局符号,而不必为它定义本地空间.
弱全局weak global:
这个晦涩的概念在一些工具链中以关键词.weakext实现.它允许你定义一个symbol,如果有同名的全局对象存在,那幺就把它连结到这个同名 的全局对象;如果不存在同名的全局对象,那幺它就作为一个局部对象存在.如果.comm段存在,你就不应该用’弱全局’这个概念.????
.globl
在C语言环境下,除非用static关键词进行声明,否则模块级的数据和函数入口都默认为全局属性.与C语言不同,一般在汇编语言环境下,除非使 用.globl directive显式的进行声明,否则标志label默认为局部属性.对于用.comm声明的对象不需要再使用.globl,因为它们已经自动具备全局 属性.
.data
.globl status #全局变量
status: .word 0
.text
.globl set_status #全局函数入口
set_status:
subu sp, 24
………………..
.extern
如果引用当前模块中未定义的标志,那幺(assembler)将假定它是在”其它模块中定义的全局对象”(外部变量).在一些情况下,如果 assembler能知道所引用的对象的长度,它就可以生成更优化的代码(见9.4.1节).外部变量的长度用.extern directive来指明:
.extern index, 4
.extern array, 100
lw $3,index #提取一个4-byte(1-word)长度的外部变量
lw $2,array($3) #提取100-byte长度外部变量的一部分
sw $2,value #装载一个未知长度的外部变量
.weakext
一些assembler和工具链支持弱全局的概念,这允许你为一个符号symbol指定一个暂时性的绑定(binding,是一个连接用的概念,指 符号与其内存地址间的对应关系?????),如果存在一个正常全局(强全局)对象定义,那幺它将把先前的这个弱全局绑定覆盖.例如:
.data
.weakext errno
errno: .word 0
.text
lw $2, errno #可能使用局部定义,也可能使用外部定义.
如果没有其它模块使用.globl来定义errno,那幺这个模块------还有其它模块------将会使用上面代码中errno的局部定义.
另外一个可能的用法是:用一个名字声明一个局部变量,而用另一个名字声明它另外的弱全局身分.
.data
myerrno: .word 0
.weakext errno, myerrno
.text
lw $2, myerrno #总是使用上面的局部定义
lw $2, errno #可能使用局部定义,也可能使用其它的
#(外部定义)
9.5.5 函数directive