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

Popular posts from this blog

托福 TPO词汇题汇总

浮点数

缓存