mips stack

http://www.cs.umd.edu/class/sum2003/cmsc311/Notes/Mips/stack.html

http://www.cs.umd.edu/class/sum2003/cmsc311/Notes


Understanding the Stack

Introduction

Assembly languages provide you the ability to implementconditional statements. If you add and jump/branches (theassembly equivalent of "goto" statements), you can implementloops.

The next step is some kind of support for functions. Functions areperhaps the most fundamental language feature for abstraction and codereuse. It allows us to refer to some piece of code by a name. All weneed to know is how many arguments are needed, what type of arguments,and what the function returns, and what the function computes to usethe function.

In particular, it's not necessary to know how the function doeswhat it does.

How does assembly language give us this kind of support.

To think about what's required, let's think about what happensin a function call.

  • When a function call is executed, the arguments need tobe evaluated to values (at least, for C-like programming languages).
  • Then, control flow jumps to the body of the function,and code begins executing there.
  • Once a return statement has been encountered, we'redone with the function, and return back to the function call.

Programming languages make functions easy to maintain and writeby giving each function its own section of memory to operate in.

For example, suppose you have the following function.

  int pickMin( int x, int y, int z ) {
     int min = x ;
     if ( y < min )
        min = y ;
     if ( z < min )
        min = z ;
     return min ;
   }
You declare parameters x, y, and z. You alsodeclare local variables, min. You know that these variableswon't interfere with other variables in other functions, even ifthose functions use the same variable names.

In fact, you also know that these variables won't interfere withseparate invocations of itself.

For example, consider this recursive function,

  int fact( int n ) {
     if ( n == 0 )
        return 1 ;
     else
        return fact( n - 1 ) * n ;
   }

Each call to fact produces a new memory location for n.Thus, each separate call (or invocation) to fact has its owncopy ofn.

How does this get implemented? In order to understand functioncalls, you need to understand the stack, and you need to understandhow assembly languages like MIPS deal with the stack.

Stack

When a program starts executing, a certain contiguous sectionof memory is set aside for the program called the stack.

Let's look at a stack.

The stack pointer is usually a register that contains thetop of the stack. The stack pointer contains the smallestaddressx such that any address smaller thanxis considered garbage, and any address greater than or equaltox is considered valid.

In the example above, the stack pointer contains thevalue 0x0000 1000, which was somewhat arbitrarily chosen.

The shaded region of the diagram represents valid parts of the stack.

It's useful to think of the following aspects of a stack.

  • stack bottom The largest valid address of a stack. When a stack is initialized, the stack pointer points to the stack bottom.

  • stack limit The smallest valid address of a stack. If the stack pointer gets smaller than this, then there's astack overflow (this should not be confused with overflow from math operations).
Other sections of memory are used for the program and for the heap(the section of memory used for dynamic memory allocation).
Push and Pop
Like the data structure by the same name, there are two operationson the stack: push and pop.

Usually, push and pop are defined as follows:

  • push You can push one or more registers, bysetting the stack pointer to a smaller value (usually by subtracting4 times the number of registers to be pushed on the stack) andcopying the registers to the stack.

  • pop You can pop one or more registers, by copying thedata from the stack to the registers, then to add a value to the stackpointer (usually adding 4 times the number of registers to be poppedon the stack)
Thus, pushing is a way of saving the contents of the register tomemory, and popping is a way of restoring the contents of the registerfrom memory.
MIPS support for push and pop
Some ISAs have an explicit push and pop instruction.However, MIPS does not. However, you can get the same behavior as push and pop by manipulating the stack pointer directly.

The stack pointer, by convention, is $r29. That is, it'sregister 29. We say, "by convention", because MIPS ISA doesnot force you to use register 29. However, because it's a convention,you should follow it, if you expect your code to work withother pieces of code.

Here's how to implement the equivalent of push $r3 in MIPS,which is to push register$r3 onto the stack.

push:  addi $sp, $sp, -4  # Decrement stack pointer by 4
       sw   $r3, 0($sp)   # Save $r3 to stack
The label "push" is unnecessary. It's there just to make iteasier to tell it's a push. The code would work just fine withoutthis label.

Here's a diagram of a push operation.

The diagram on the left shows the stack before the push operation.

The diagram in the center shows the stack pointer beingdecremented by 4.

The diagram on the right shows the stack after the 4 bytesfrom register 3 has been copied to address0x000f fffc

You might wonder why it's necessary to update the stack pointer.Couldn't you just do the following?

push:  sw $r3, -4($sp)   # Copy $r3 to stack
Certainly, this is equivalent in behavior as far as register 3being saved to the stack.

However, we'd like to maintain the invariant that all addressesgreater than or equal to the stack pointer hold valid data, and all the addresses less than the stack pointer hold invalid data. If weran the above code, then there would be valid data at an addresssmaller than the stack pointer.

It just makes bookkeeping that much easier if we follow theseconventions.

Popping off the stack
Popping off the stack is the opposite of pushing on the stack.First, you copy the data from the stack to the register, thenyou adjust the stack pointer.

pop:  lw   $r3, 0($sp)   # Copy from stack to $r3
      addi $sp, $sp, 4   # Increment stack pointer by 4

Here's a diagram of the pop operation.

The diagram on the left is the initial state of the stack.

The data is copied from the stack to the register 3. Thus,the diagram in the center is the same as the one on the left.If I had drawn a picture of the contents of register 3, it wouldhave been updated with the data from the stack.

Then, the stack pointer is moved down (shown in the diagramon the right).

As you can see, the data still is on the stack, but once thepop operation is completed, we consider that part of the datainvalid. Thus, the next push operation overwrites this data.But that's OK, because we assume that after a pop operation,the data that's popped off is considered garbage.

If you've ever made the error of returning a pointer to alocal variable or to a parameter that was passed by valueand wondered why the value stayed valid initially, but lateron got corrupted, you should now know the reason.

The data still stays on the garbage part of the stackuntil the next push operation overwrites it (that's whenthe data gets corrupted).

Pushing and Popping, Part 2

It turns out you don't have to adjust the stack pointereach time you want to save a register to the stack. If youknow you are going to push several registers to the stack,you can adjust the stack pointer at once.

For example, suppose you want to push registers $r2, $r3, and$r2 on the stack. This is code to do that:

push:  addi $sp, $sp, -12  # Decrement stack pointer by 12
       sw   $r2, 0($sp)   # Save $r2 to stack
       sw   $r3, 4($sp)   # Save $r3 to stack
       sw   $r4, 8($sp)   # Save $r4 to stack
Since each register takes up 4 bytes and since each memory addressstores 1 byte, we need to decrement the value of the stack pointerby 12 to give us the space to store 3 registers.

At this point, we can copy the contents of registers 2, 3, and 4to the stack. It's somewhat arbitrary which order we put the registerson the stack. In the code above, register 2 is on the very top ofthe stack. We could just have easily put register 3 or 4 on the topof the stack. The way shown above just seems more "orderly".

Popping is very similar.

pop:  lw   $r2, 0($sp)   # Copy from stack to $r2
      lw   $r3, 4($sp)   # Copy from stack to $r3
      lw   $r4, 8($sp)   # Copy from stack to $r4
      addi $sp, $sp, 12  # Increment stack pointer by 12

Stacks and Functions

Let's now see how the stack is used to implement functions. For each function call, there's a section of the stack reservedfor the function. This is usually called a stack frame.

Let's imagine we're starting in main() in a C program.The stack looks something like this:

We'll call this the stack frame for main(). Itis also called theactivation record. A stack frameexists whenever a function has started, but yet to complete.

Suppose, inside of body of main() there's a call tofoo(). Supposefoo() takes two arguments. One way topass the arguments tofoo() is through the stack. Thus, thereneeds to be assembly language code inmain() to "push"arguments forfoo() onto the the stack. The result looks like:

As you can see, by placing the arguments on the stack, the stackframe for main() has increased in size. We also reservedsome space for the return value. The return value is computedbyfoo(), so it will be filled out oncefoo() isdone.

Once we get into code for foo(), the function foo()may need local variables, sofoo() needs to push some spaceon the stack, which looks like:

foo() can access the arguments passed to it frommain() because the code inmain() places the argumentsjust asfoo() expects it.

We've added a new pointer called FP which stands for frame pointer. The frame pointer points to the location wherethe stack pointerwas, just beforefoo() moved the stackpointer forfoo()'s own local variables.

Having a frame pointer is convenient when a function is likelyto move the stack pointer several times throughout the courseof running the function. The idea is to keep the frame pointerfixed for the duration offoo()'s stack frame. Thestack pointer, in the meanwhile, can change values.

Thus, we can use the frame pointer to compute the locations inmemory for both arguments as well as local variables. Since it doesn'tmove, the computations for those locations should be some fixed offsetfrom the frame pointer.

And, once it's time to exit foo(), you just have to set thestack pointer to where the frame pointer is, which effectively popsofffoo()'s stack frame. It's quite handy to have a framepointer.

We can imagine the stack growing if foo() calls anotherfunction, say,bar().foo() would push arguments on thestack just asmain() pushed arguments on the stack forfoo().

So when we exit foo() the stack looks just as it didbefore we pushed onfoo()'s stack frame, except this timethe return value has been filled in.

Once main() has the return value, it can pop that and thearguments tofoo() off the stack.

Recursive Functions
Surprisingly enough, there's very little to say aboutrecursive functions, because it behaves just as non-recursivefunctions do. To call a recursive function, push argumentsand a return value on the stack, and call it like any otherfunction.

It returns back the same way as well.

Stack Overflow

While stacks are generally large, they don't occupy all ofmemory. It is possible to run out of stack space.

For example, consider the code we had for factorial.

  int fact( int n ) {
     if ( n == 0 )
        return 1 ;
     else
        return fact( n - 1 ) * n ;
   }
Suppose fact(-1) is called. Then, the base caseis never reached (well, it might be reached once we decrementso far that n wraps around to 0 again). This causesone stack frame after another to be pushed. Once the stacklimit has been reached, we enter into invalid memory addresses,and the operating system takes over and kills your programming,telling you your program has a stack overflow.

Probably the most common cause of stack overflow is a recursivefunction that doesn't hit the base case soon enough. For fans ofrecursion, this can be a problem, so just keep that in mind.

Some languages (say, ML) can convert certain kinds ofrecursive functions (called "tail-recursive" functions) intoloops, so that only a constant amount of space is used.

How MIPS Does It

In the previous discussion of function calls, we said thatarguments are pushed on the stack and space for the returnvalue is also pushed.

This is how CPUs used to do it. With the RISC revolution (admittedly,nearly 20 years old now) and large numbers of registers used in typicalRISC machines, the goal is to (try and) avoid using the stack.

Why? The stack is in physical memory, which is RAM. Comparedto accessing registers, accessing memory is much slower---probably onthe order of 100 to 500 times as slow to access RAM than to accessa register.

MIPS has many registers, so it does the following:

  • There are four registers used to pass arguments: $a0,$a1,$a2,$a3.
  • If a function has more than four arguments, or if any ofthe arguments is a large structure that's passed by value, then the stack is used.
  • There must be a set procedure for passing arguments that'sknown to everyone based on the types of the functions. That way,the caller of the function knows how to pass the arguments, andthe function being called knows how to access them. Clearly,if this protocol is not established and followed, the functionbeing called would not get its arguments properly, and would likelycompute bogus values or, worse, crash.
  • The return value is placed in registers $v0, andif necessary, in$v1.

In general, this makes calling functions a little easier. Inparticular, the calling function usually does not need to place anythingon the stack for the function being called.

However, this is clearly not a panacea. In particular, imaginemain() callsfoo(). Arguments are passed using$a0 and$a1, say.

What happens when foo() calls bar()? If foo()has to pass arguments too, then by convention, it's supposed to passthem using$a0 and$a1, etc. What iffoo() needsthe argument values frommain() afterwards?

To prevent its own arguments from getting overwritten, foo()needs to save the arguments to the stack.

Thus, we don't entirely avoid using the stack.

Leaf Procedures

In general, using registers to pass arguments and for return valuesdoesn't prevent the use of the stack. Thus, it almost seems likewe postpone the inevitable use of the stack. Why bother usingregisters for return values and arguments?

Eventually, we have to run code from a leaf procedure. Thisis a function that does not make any calls to any other functions.Clearly, if there were no leaf procedures, we wouldn't exit theprogram, at least, not in the usual way. (If this isn't obviousto you, try to think about why there must be leaf procedures).

In a leaf procedure, there are no calls to other functions,so there's no need to save arguments on the stack. There'salso no need to save return values on the stack. You justuse the argument values from the registers, and place the returnvalue in the return value registers.

Stackless Languages?

Is the stack absolutely necessary? FORTRAN 77 (77 refers to theyear 1977) did not use a stack. Basically, each function had asection of memory for its own arguments and data. Because there wereno stacks, you couldn't write recursive functions in FORTRAN 77.However, it goes to show you that a stack isn't always needed.

There's also a complicated language feature called continuations(which appear in languages likeScheme).Continuationscan be coded in a way that doesn't require a stack, and in effect,acts like a queue.

Nevertheless, the vast majority of languages that implementfunctions use the stack.

Heaps?

What about heaps? The heap is a section of memory used fordynamic memory allocation. Heaps are more difficult to implement.Typically, you need some code that manages the heap. You requestmemory from the heap, and the heap code must have data structuresto keep track of which memory has been allocated.

The code can be written in, say, C in a few hundred lines ofcode, but is rather unwieldy to write in assembly language.

Notice that the heap in memory is not the same as the heap datastructure. Those two are different kinds of heaps, and aren't reallyrelated.

Summary

To understand how functions work, you need to understand thebehavior of the stack.

We've discussed how arguments and return values are used in thestack, and how each function call results in a stack frame beingpushed on the stack. We've also talked about how a stackcan overflow, typically, from a recursive function that's eithergot a bad base case or unexpected arguments. Occasionally,stack overflow can just occur because the stack just grows too big.


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值