Computer Architecture II
Continue from Computer Architecture I
3.3 Loop
循环是各类编程语言中及其常见的功能,而在Instruction中,以MIPS为例,循环的功能一般以Loop的形式呈现。
3.3.1 "if" loop
C语言:
Loop: g = g + A[i];
i = i + j;
if(i ! = h) goto Loop;
假设g , h , i 和 j 的值分别存在$s1, $s2, $s3和$s4。数组A的初始地址为$s5。
将以上C语言转译为MIPS Instruction后为:
Loop: add $t1,$s3,$s3 // t1=i + i
add $t1,$t1,$t1 // t1= 2i + 2i
add $t1,$t1,$s5 // t1现在为A[i]的地址
lw $t0, 0($t1) // 读取A[i]的值到t0
add $s1,$s1,$t0 // g = g + A[i]
add $s3,$s3,$s4 // i = i+ j
bne $s3, $s2, Loop //如果i != j 返回loop
3.3.2 "While"loop
while是最常用的循环的之一,以C语言为例:
while( save[i] == k )
i=i+j;
假设i、j、k的值存储在register $s3、$s4、$s5中,save的初始地址存在$s6中。将以上while语句转换为MIPS Instruction后应为:
Loop: add $t1,$s3,$s3 //t1= i + i
add $t1,$t1,$t1 //t1= 2i + 2i
add $t1,$t1,$s6 //将save[i]的地址存在t1上
lw $t0,0($t1) //读取save[i]的值
bne $t0,$s5,Exit //如果i!=k,终止循环,如果相等则继续往下执行
add $s3,$s3,$s4 //i = i + j
j Loop // 跳转回循环
Exit:
3.3.3“slt”Instruction
在日常使用中,如若使用for循环或if语句,则很多情况下会使用进行数字比较的语句,这种操作在Instrcution中的名字为slt,全拼为:set on less than,表现形式如下: slt $t0,$s3,$s4 这个Instruction的意思为:set $t0的值为1,如果$s3的值小于$s4。否则,set $t0的值为0。 而对于0这个特殊的值来说,有一个专门的register负责存储它,$zero。 在实际运行中,slt常与bne联合运用,如: slt $t0,$s0,$s1 //t0=1 if s0 < s1 bne $t0,$zero,Less //如果t0!=0,则执行Less 基于冯诺依曼的simplicity理论,MIPS Instruction中并没有less than的语句,因为less than语句过于复杂,会增加时钟周期或者CPI,两个快速的指令比一个复杂的指令更好。
3.4 "Case/Switch" Statement
switch case 语句要更加复杂一点,比较直接的实现方法是,将switch case直接转换为一串if else语句的组合。但是对于机器而言过于复杂,所以在MIPS Instruction中存在一种名为jr的指令,全拼为jump register,其作用为跳到指定的register中所指定的地址。jr instruction并不会跳转到任意地址,只会在与jr配套的jump address table中跳转。
C语言:
swicth ( k ){
case 0: f = i + j ; break;
case 1: f = g + h ; break;
case 2: f = g - h ; break;
case 3: f = i - j ; break;
}
假设case0、case1、case2、case3语句均被L0、L1、L2、L3所表示,且L的地址被存储在Jump address table中,其初始地址在$t4,f、g、h、i、j、k分别存储在register$s0~$s5中,值4被存储在$t2中。将swicth语句转换为MIPS Instruction后应为:
slt $t3, $s5, $zero //if k<0,t3=1
bne $t3, $zero, Exit //if k<0, 结束程序。
slt $t3, $s5, $t2 //if k<4,t3=1
beq $t3, $zero, Exit //if k>=4, 结束程序。
add $t1, $s5, $s5 //t1 = k + k
add $t1, $t1, $t1 //t1= 4k 构建寻址基础
add $t1, $t1, $t4 //将t1置为Jumptable[k]的地址
jr $t0 //Jump到t0所指定的地址的语句
L0: add $s0, $s3, $s4
j Exit
L1: add $s0, $s1, $s2
j Exit
L2: sub $s0, $s1, $s2
j Exit
L3: sub $s0, $s3, $s4
Exit:
3.5 Supporting Procedures in Hardware
3.5.1Recognition of Procedures
Procedures和programs在中文的翻译上基本一致,这里做一下简单的区分。 Procedure: 程序中实现特定功能的一段code。 Program:程序,完整的程序。 Procedures就是完整的program中的实现特定功能的一小部分,如传递参数或返回值,只负责完成具体的任务。 为了Program能够更加快速的执行Procedure,前文中有提到register是最快的存储器,所以MIPS software为Procedure从32个寄存器中提供了以下寄存器: $a0~$a3 //负责传递parameter $v0~$v1 //负责返回值 $ra //负责返回地址 MIPS Instruction中考虑到Procedure的重要性,为其提供了一个专属指令:jal。全程jump and link, 其作用为jump至某一地址,并将下一个Instruction的地址保存在$ra中。表现形式如下: jal ProcedureAddress link体现在将地址存储在register $ra中。隐含在此过程中的一个事实是,我们需要一个register去保存正在被执行的instruction的地址,否则无法将下一个instruction的地址存储进$ra。由于历史原因,负责存储当前被执行instruction的register的名字为pc,program counter。实际上jal正是将pc+4存储进$ra中。 在switch语句中,我们提到了instruction中的jr,其作用为跳转到某register,而在完整的Procedure执行过程中,jr正是我们需要的最后一块拼图。 完整的Procedure的过程可以描述为:首先,calling program 或者叫caller将parameter的值传递或者说保存在register $a0~$a3中,然后利用jal跳转至Procedure 或者叫callee,然后callee执行procedure的具体功能然后将返回的值存在register $vo~$v1中,最后用jr $ra返回到下一个instruction。
3.5.2 Stack
在实际的Procedure执行中,很多时候我们无法保证4个值register和2个返回值register就足够用了,所以正如2.4中提到的一样,我们需要将一些值存储在memory中。而在procedure中常用的数据结构就是Stack,一种后进先出序列。
对于Stack来说,有几种常用操作需要了解一下,将值存进Stack中称为push,将值取出Stack称为pop。
MIPS Instruction为Stack设计了专用register,$sp, stack pointer,以一个字为单位进行操作。历史原因,Stack进行操作时,后进的值存在低地址中,这意味着伴随着不断的push,$sp的值会不断减小,反之则增大。
C语言: int leaf_example ( int g, int h, int i, int j )
{ int f;
f = (g + h)- (i + j);
return f;
}
假设g、h、i、j的值存储在$a0~$a3, f的值存储在$s0。将此C语言编译后写为MIPS Instruction后应为:
leaf_example:
sub $sp, $sp, 12 //为stack开辟能够存储3个值的位置
sw $t1, 8($sp) //将初始值存进memory
sw $t0, 4($sp)
sw $s0, 0($sp) //后进的值存储在低地址memory
add $t0, $a0, $a1 //g+h
add $t1,$a2, $a3 // i+j
sub $s0, $t0, $t1 // f=g+h-i-j
add $v0, $s0, $zero // 将f的值存进返回值register
lw $s0, 0($sp) //复原s0的初始值,后进Stack的值先pop
lw $t0, 4($sp) //复原t0的初始值
lw $t1, 8($sp) //复原t1的初始值
add $sp, $sp, 12 //复原$sp
jr $ra //返回值下一条Instruction
补充:MIPS Instruction将register分为两组,t组与s组。其中t组即为temporary register,procedure不会保存t组register的值,所以实际上述例子中无需重置t0、t1。而对于s组register来说,由procedure进行调用并复原且必须复原。因为callee认为caller需要s组register的值。
3.5.3 Nested Procedures
不关联其他Procedure的Procudure(即可独立运行)被称作Leaf Procedure,如若Program中只含有Leaf Procedure,life will be simpler,但是现实中编程的过程中,我们很难做到每个Procedure都是leaf,相反,我们会经常写出不断交织的Nested Procedure,甚至是自己调用自己的Procedure, 即递归函数。
例如Procedure A需要使用register $a0,而在Procedure A结束之前的instruction中含有调用Procedure B的语句。而B也需要使用register $a0,此时就会出现矛盾。为了解决这个问题,我们需要使用Stack,将所有需要被二次或多次使用的寄存器利用Stack进行调整。
例如C语言:
int fact ( int n ){
if(n<1)
return 1;
else return( n * fact( n-1 ) );
}
假设parameter n存储在$a0,将此Nested Procedure转为MIPS instruction后应为:
fact:
sub $sp, $sp, 8 //为stack分配两个字节的空间
sw $ra, 4($sp) //保存当前返回地址
sw $a0,8($sp) //保存当前n的值
slt $t0, $a0, 1 // if n<1, t=1
beq $t0, $zero, L1 //if t0!=0,即如果n>=1, goto L1
add $v0, $zero, 1 //return 1
add $sp, $sp, 8 //恢复Stack Point
jr $ra //返回到jal之后的语句
L1: sub $a0, $a0, 1 // n=n-1
jal fact //递归
lw $a0, 0($sp) //读取stack中的n
lw $ra, 4($sp) //读取stack中的返回地址
add $sp, $sp, 8 //填补两个字的stack空间
mul $v0, $a0, $v0 //n*fact(n-1)
jr $ra //返回caller
3.5.4 Preserved and not preserved
通常来说,以下register及Stack在Procedure执行的过程中会被保留:
Saved registers: $s0-$s7
Stack pointer register: $sp
Return address register: $ra
Stack above the stack pointer
以下register的值并不会被保留:
Temporary registers: $t0-$t9
Argument rsgisters: $a0-$a3
Return value registers: $v0-$v1
Stack below the stack pointer
register是否在$sp的上方为优先级最高的准则。
$sp以上的Stack,直接通过确保callee不会覆盖或修改register的值来确保值被保留,而$sp本身则通过加上被减去的地址位移。
3.5.5 Allocating Space for new data
对Stack来说复杂性最强的便是,Stack也会用来保存一些无法在register中存储的变量,比如数组变量或结构。去将Stack分割/细分来存储procedure中的变量的操作我们称作: procedure frame or activation record。
一些MIPS software会使用$fp, 在procedure frame中frame pointer指出第一个word的位置。$sp在procedure中有可能会改变位置,从而用$sp来定位其他保存的数据的值会使情况十分复杂。而$fp在activation record中就提供了一个相对十分稳定的可靠位置的register。
在C语言中,programmer需要自己对内存中的静态与动态数据结构分配空间。通常MIPS中,首段地址会被保留,之后则称为代码段,再往上则为static data segment,负责存储静态变量与常量。虽然数组等固定长度的数据结构可以与SDS匹配,但链表这样的动态结构则要复杂的多,并不用适用于SDS。这类动态数据结构会被存储在SDS后,习惯上被称为heap(堆),与栈相互增长,互相争夺剩余的空间,从而高效利用内存空间。
以下为MIPS中分配空间的基本准则(具体地址只是例子,并非必须值):
从下到上,从低地址到高地址:
地址:0~0040 0000 hex——> 保留 //到下一阶段前均为保留
地址:0040 0000hex ——>program counter //保存当前被执行的instruction的地址
地址:0040 0000hex~1000 0000hex ——>正文 //代码段
地址:1000 8000hex ——> $gp //全局指针,负责访问数据
往后则为动态数据; // 堆
再往后则是栈。 //与堆竞争空间
地址:7fff fffc hex ——> 结束。
MIPS中约定俗成的结果是:传递4个parameter,2个register用于return,8个register会被保存值。
如若所需参数超过4个,则将其余参数存储在$fp的上方,通过$fp在memory中寻址来调用。
3.6 Beyond Numbers
现如今的计算机能够处理文本内容是由于几乎全球统一使用ASCII编码来在计算机中表示字符内容。自然而然,计算机中需要特殊的读取一个byte的register,
Load byte,lb,这个instruction会从memory中读取一个字节,而store byte,sb,会从存储register的一个byte到memory中。
表现形式与lw,sw并无区别:
lb $t0,0($sp)
sb $t0,0($gp)
字符经常被组成string,而去表示一个string则又是一个问题。
一般来说有以下三种方法来表示string:
1)保留string的第一个位置(字节)来给出string的长度
2)附加一个带有string长度的variable
3)在string结尾添加标识符
C语言采用第三种,即在每个string的结尾都是字符'0'。
以C语言代码为例:
void strcpy(char x[ ],char y [ ])
{
int i;
i = 0;
while((x[i]=y[i]!=0))
i=i+1;
}
strcpy提供了string复制的功能,如若x,y的初始地址被存储在$a0,$a1,i被存储在$s0,则将此C语言代码转换为MIPS Instruction则为:
strcpy:
sub $sp,$sp, 4 //从stack中分配空间
sw $s0,0($sp) // 存储i到stack
add $s0,$zero,$zero //初始化i=0
L1:
add $t1,$a1,$s0 //计算y[i]的地址
lb $t2,0($t1) //读取y[i]到$t2中
add $t3,$a0,$s0 //计算x[i]的地址
sb $t2,0($t3) //x[i]=y[i]
add $s0,$s0,1 //i++
bne $t2,$zero,L1 //if y[i]!=0,go to L1
lw $s0,0($sp) //恢复$s0
add $sp,$sp,4 //恢复stack
jr $ra // return
3.7 Other Styles of MIPS Addressing
3.7.1 Constant or Immediate Operands
上文中提到过的instruction,比如add,当我们使用它们的时候,我们必须从内存中去提取一些数据来使用他。比如我们想要给$sp加上4,我们需要先从内存中获取constant 4的地址,然后再去加上$sp。这一类的instruction,就是Constant Operands Instruction, 当我们想要进行算数操作时,首先要通过常量地址获取常量。
而如若我们想要直接给$sp加上4,不想经过从内存中获取常量的过程,此时我们便可以使用Immediate Operands Instruction,以加法为例,add变成addi后即为IOI。
add与addi区别如下:
For add{
lw $t0, Addconstant4($zero) //用4的地址Addconstant4来获取4
add $sp,$sp,$t0 // $sp+4
}
For addi{
addi $sp,$sp,4 //$sp+4
}
对于addi来说,addi $sp,$sp,4 ,此命令的machine code,它的Operands是8,$sp的编码是29,即:
op rs rt immediate
8 29 29 4
001000 11101 11101 0000 0000 0000 0100
前文中还有很多十分常用的Instruction也经常使用immediate operand,比如:slt(set on less than)。
slt 如若是被用在和0比较,由于0本身有一个$zero寄存器,还比较方便,但是如若想和10进行比较,那还是使用slti要更好:
slti $t0, $s2 ,10
使用此语句即可完成与10的比较。
3.7.2 Addressing in Branches and Jumps
j(jump),使用此instruction时,machine code最为简洁,如:
j 10000
此MIPS语句的machine code为:
op constant
2 10000
6bits 26bits
这是MIPS中比较特殊的J-type instruction组成。
而与之相对的,也是经常会起到jump作用的branches instruction就并非这么简洁了,以bne为例:
bne $s0,$s1,Exit //if s0!=s1, go to Exit
此语句的machine code为:
op rs rt Address
5 16 17 Exit's address
6bits 5bits 5bits 16bits
如果程序的所有地址都必须去fit这个16bit的Address,那么程序最大也不会超过2的16次方。以目前的情况来看,这远远不够。
比较常用的解决办法是在Program Counter的基础上加上另一个寄存器来存储多余地址,即:
Program Counter = Register + Branch address
这样,pc便可以存储2的32次方的地址,并且可以不断增加Branch来满足要求。
3.7.2 Branching Far Away
当程序中出现了branch的时候,我们希望这两个branch离的越远越好,所以,可以采用以下方法:
beq $s0, $s1, L1 //如果s0=s1,那么go to L1
此instruction可以被改写为以下两条instruction的形式:
bne $s0,$s1,L2 //如果s0!=s1, go to L2
j L1 // jump to L1
L2:
这样,L2和L1两个branch就会在memory中分开的很远。
3.8 Starting a Program
当一个程序被execute的时候,大致是分为以下几步:
以C语言为例:C程序会先被compiler翻译成Assembly language然后再被Assembler翻译成Machine Language最后会被linker将可被execute的Machine Language program链接给loader,最终program在memory中被执行。
3.8.1 Compiler
Compiler将C program transform成assembly language program. 不同架构的计算机可以读取对应不同的assembly language program。High-level语言的程序会更容易书写,反之则相反。所以程序员直接书写assembly language的难度非常高。
3.8.2 Assembler
正因为assembly language是High-level software的interface, assembler能够处理很多种常用的machine language instruction,而这些instruction不必应用在硬件上。并且这个功能可以简化translation和programming。这种instruction被叫做pseudoinstruction。
比如:
register $zero总是代表着0,不论任何情况,这帮助了assembler去理解move这个instruction,尽管move并没有被implement在MIPS architecture里。
move $t0, $t1 //t0得到t1的值
Assembler会将move这个instruction翻译为以下语句:
add $t0,$zero,$t1
如此这般之后,t0同样可以得到t1的值,从而assembler可以理解并使用move。
同理,assembler也是将ble(branch less than)理解为slt(set less than )和bne(branch not equal)。
总之,pseudoinstruction(伪instruction)给了MIPS更多并非被implement到hardware上的instruction。唯一的cost也不过是一个register $at。如果你需要去书写assembly program,使用pseudoinstruction可以简化任务。
Assembler 会accpet许多不同base的数组,不光10进制或2进制。比如16进制,也是经常被使用的一种base,16进制可以更加简洁的代替2进制数字,并且十分容易再被convert回2进制。
Assembler的主要任务其实是将assembly language program去转换为machine code,实际上,首先,Assembler会将assembly language program转换为object file,这是一种包含了machine language instruction,data,和一些info去帮助计算机合适的在memory中存储instruction。
为了生成每个instruction对应的二进制形式,assembler必须要分清各个instruction的地址(因为有很多的branch需要被定位)。Assembler会将所有需要用到的branch的track信息和data-transfer instruction在symbol table中。所以Symbol table其实就是包含了很多一一对应的symbol和address。
Unix中非常典型的object file会包含以下六个内容:
1. object file header: 描述了size和position
2. text segment: machine code的内容
3. data segment:program中带有的各种数据
4. relocation information: 记录了instruction和data的绝对地址
5. symbol table: 同上文描述
6. debugging information: 提供一个关于modules如何去compile的简介描述,方便让debugger对machine instruction和C program建立起联系并且使data structure变得可读。
3.8.3 Linker
截止至目前,我们讨论的所有内容都在表示,即便只是一小行的procedure的改变,可能会导致整个compiling和assembling的过程发生改变。但是为了一小行的procedure的改变去将整体进行retranslation同样是浪费计算资源。一个更加好的选择是去分别compile和assemble每个procedure,此时,linker的作用便体现了出来。Linker将所有的独立的assembled machine language programs编织在一起。
Linker的工作过程分为以下三个步骤: 1. 将data和code符号化并place到memory上 2. Determine data的地址和insrtuction的label 3. Patch内部和外部的references
Linker会对每一个object module用重定位信息和symbol table去分析未定义的label。像是branch instruction或者data的地址,它们就非常需要linker。所以,linker的工作更像是一个editor:寻找旧地址并拿新地址去替代。Linker的存在理由是,patch code要比recompile和reassemble快很多。
如果所有外部的reference都被解决了,linker会继续分配每个module在memory中的位置。正因为files会被独立的assemble,assembler无法知道也无法摆放各个module的insrtuction的相对位置。当linker去place一个module在memory时,所有的absolute references,即在memory中的地址并不是和register相关的,必须要被relocate去映射他的真正地址。
Linker会生成executable file,也就是常见的exe文件,可以在电脑上被直接execute。通常来书,exe file和object file是几乎一样的,除了exe file中并不包含未被resolve的references、relocation information、symbol table或者debugging information。有一些linker files也可能是不完全的,即仍然有一些library routines存在着一些unresolved的address和result在object files中。
3.8.4 Loader
现在,exe file已经在disk中了,操作系统会将他们读进memory并start。在unix系统中,系统会按以下步骤操作:
1.读exe文件的header去决定text和data segements的大小;
2.规划足够的地址给text和data;
3.从exe文件中复制instruction和data到memory中;
4.复制main program的parameter到stack上;
5.初始化machine register并set stack pointer到第一个free location;
6.Jump到复制parameter到register并call main routine的start-up routine。当main routine也return的时候,执行exit system call终止 program。
3.9 An Example to Put It All Together
3.9.1 The Procedure, swap
首先从非常常用的procedure,即swap来讲解。Swap的作用就是简单的将memory中的两个位置进行交换。当我们C语言被translate成assembly language的时候,通常来说,计算机将遵循以下三个步骤:
1. 分配register给program中会出现的变量
2. 为procedure produce code
3.在procedure被调用的过程中保存register
C语言,swap:
swap ( int v[], int k){
int temp;
temp=v[k];
v[k]=v[k+1];
v[k+1]=temp;
}
Comments
Post a Comment