© Stephen Smith 2020
S. SmithProgramming with 64-Bit ARM Assembly Languagehttps://doi.org/10.1007/978-1-4842-5881-1_6

6. Functions and the Stack

Stephen Smith1 
(1)
Gibsons, BC, Canada
 

In this chapter, we will examine how to organize our code into small independent units called functions. This allows us to build reusable components, which we can call easily form anywhere we wish by setting up parameters and calling them.

Typically, in software development, we start with low-level components. Then we build on these to create higher- and higher-level modules. So far, we know how to loop, perform conditional logic, and perform some arithmetic. Now, we examine how to compartmentalize code into building blocks.

We introduce the stack, a computer science data structure for storing data. If we’re going to build useful reusable functions, we need a good way to manage register usage, so that all these functions don’t clobber each other. In Chapter 5, “Thanks for the Memories,” we studied how to store data in a data segment in main memory. The problem with this is that this memory exists for the duration that the program runs. With small functions, like converting to upper-case, they run quickly; thus they might need a few memory locations while they run, but when they’re done, they don’t need this memory anymore. Stacks provide us a tool to manage register usage across function calls and a tool to provide memory to functions for the duration of their invocation.

We introduce several low-level concepts first, and then we put them all together to effectively create and use functions. First up is the abstract data type called a stack that is a convenient mechanism to store data for the duration of a function call.

Stacks on Linux

In computer science, a stack is an area of memory where there are two operations:
  • Push: Adds an element to the area

  • Pop: Returns and removes the element that was most recently added

This behavior is also called a LIFO (last in first out) queue.

When Linux runs a program, it gives it an 8-megabyte stack. In Chapter 1, “Getting Started,” we mentioned that register X31 had a special purpose as both the zero register and the stack pointer (SP). You might have noticed that X31 is named SP in gdb and that when you debugged programs, it had a large value, something like 0x7ffffff230. This is a pointer to the current stack location.

The ARM instruction set has a handful of instructions to manipulate the stack; remember that any instruction that doesn’t operate on the stack sees it as the zero register. There are two instructions to place registers on the stack, STR and STP, and then two instructions to retrieve items from the stack into registers, LDR and LDP. We studied all these instructions in Chapter 5, “Thanks for the Memories,” but here we’ll use specific forms to copy data to and from the stack and to adjust the stack pointer appropriately.

Note

The ARM hardware requires that SP is always 16-byte aligned. This means we can only add and subtract from SP with multiples of 16. If we use SP when it isn’t 16-byte aligned, we will get a bus error and our program will terminate.

To copy the single register X0 to the stack, we use
STR   X0, [SP, #-16]!

The convention for the stack is that SP points to the last element on the stack and the stack grows downward. This is why SP contains a large address. The STR instruction copies X0 to the memory location at SP – 16 and then updates SP to contain this address since the stored value is now the last value on the stack. We’re wasting 8 bytes here, since X0 is only 8 bytes in size. To keep the proper alignment, we must use 16 bytes.

To load the value at the top of the stack into register X0, we use
LDR   X0, [SP], #16

This does the reverse operation. It moves the data pointed to by SP from the stack to X0 and then adds 16 to the SP.

We more commonly use STP/LDP to push/pop two registers at once:
STP   X0, X1, [SP, #-16]!
LDP   X0, X1, [SP], #16

since we aren’t wasting any space on the stack. But it does take longer to transfer 16 bytes to memory than 8 bytes.

Figure 6-1 shows the process of pushing a register onto the stack, and then Figure 6-2 shows the reverse operation of popping that value off the stack.
../images/494415_1_En_6_Chapter/494415_1_En_6_Fig1_HTML.jpg
Figure 6-1

Pushing X5 onto the stack

../images/494415_1_En_6_Chapter/494415_1_En_6_Fig2_HTML.jpg
Figure 6-2

Popping X4 from the stack

The LDR, LDP, STR, and STP instructions are powerful general-purpose instructions that support stacks that grow in either direction or can be based on any register. Plus, they have all the functionality we covered in Chapter 5, “Thanks for the Memories.” In our usage, we want to implement them exactly as prescribed, so we work well in the Linux environment and can interact with code written in another language by other programmers. Now we’ll get into the details of calling functions and see how the stack fits into this with the branch with link instruction.

Branch with Link

To call a function, we need to set up the ability for the function to return execution to after the point where we called the function. We do this with the other special register we listed in Chapter 1, “Getting Started,” the link register (LR) which is X30. To make use of LR, we introduce the branch with link (BL) instruction, which is the same as the branch (B) instruction, except it puts the address of the next instruction into LR before it performs the branch, giving a mechanism to return from the function.

To return from the function, we use the return (RET) instruction. This instruction branches to the address stored in LR to return from the function. It’s important to use this instruction rather than some other branch instruction, because the instruction pipeline knows about RET instructions and knows to continue processing instructions from where LR points. This way we don’t have a performance penalty for returning from functions.

In Listing 6-1, the BL instruction stores the address of the following MOV instruction into LR and then branches to myfunc. Myfunc does the useful work the function was written to do and then returns execution to the caller by having RET branch to the location stored in LR, which is the MOV instruction following the BL instruction.
      // ... other code ...
      BL    myfunc
      MOV   X1, #4
      // ... more code ...
-----------------------------
myfunc:     // do some work
            RET
Listing 6-1

Skeleton code to call a function and return

There is only one LR, so you might be wondering what happens if another function is called? How do we preserve the original value of LR when function calls are nested?

Nesting Function Calls

We successfully called and returned from a function, but we never used the stack. Why did we introduce the stack first and then not use it? First of all, think of what happens if in the course of its processing, myfunc calls another function. We would expect this to be fairly common, as we write code building on the functionality we’ve previously written. If myfunc executes a BL instruction, then BL will copy the next address into LR overwriting the return address for myfunc and myfunc won’t be able to return. What we need is a way to keep a chain of return addresses as we call function after function. Well, not a chain of return addresses, but a stack of return addresses.

If myfunc is going to call other functions, then it needs to push LR onto the stack as the first thing it does and pop it from the stack just before it returns, for example, Listing 6-2 shows this process.
      // ... other code ...
      BL    myfunc
      MOV   X1, #4
      // ... more code ...
-----------------------------
myfunc:     STR   LR, [SP, #-16]!  // PUSH LR
            // do some work ...
            BL    myfunc2
            // do some more work...
            LDR   LR, [SP], #16    // POP LR
            RET
myfunc2:    // do some work ....
            RET
Listing 6-2

Skeleton code for a function that calls another function

In this example, we see how convenient the stack is to store data that only needs to exist for the duration of a function call.

If a function, such as myfunc, calls other functions then it must save LR; if it doesn’t call other functions, such as myfunc2, then it doesn’t need to save LR. Programmers often push and pop LR regardless, since if the function is modified later to add a function call, and the programmer forgets to add LR to the list of saved registers, then the program will fail to return and either go into an infinite loop or crash. The downside is that there’s only so much bandwidth between the CPU and memory, so PUSHing and POPing more registers does take extra execution cycles. The trade-off in speed vs. maintainability is a subjective decision depending on the circumstances.

Calling and returning from the function is only half the story. Like in high-level languages, we need to pass parameters (data) into our functions to be processed and then receive the results of the processing back in return values. Now we’ll look at how to do this.

Function Parameters and Return Values

In high-level languages, functions take parameters and return their results. Assembly Language programming is no different. We could invent our own mechanisms to do this, but this is counterproductive. Eventually, we will want the code to interoperate with code written in other programming languages. We will want to call the new super-fast functions from C code, and we might want to call functions that were written in C.

To facilitate this, there are a set of design patterns for calling functions. If we follow these, the code will work reliably since others have already worked out all the bugs, plus we achieve the goal of writing interoperable code.

The caller passes the first eight parameters in X0 to X7. If there are additional parameters, then they are pushed onto the stack. If we only have two parameters, then we would only use X0 and X1. This means the first eight parameters are already loaded into registers and ready to be processed. Additional parameters need to be popped from the stack before being processed.

To return a value to the caller, place it in X0 before returning. In fact, you can return a 128-bit integer in the X0, X1 register pair. If you need to return more data, you would have one of the parameters be an address to a memory location where you can place the additional data to be returned. This is the same as C where you return data through call by reference parameters.

Since both the caller and callee are using the same set of general-purpose registers, we need a protocol or convention to ensure that one doesn’t overwrite the working data of the other. Next, we’ll look at the register management convention for the ARM processor.

Managing the Registers

If you call a function, chances are it was written by a different programmer and you don’t know what registers it will use. It would be very inefficient if you had to reload all your registers every time you call a function. As a result, there are a set of rules to govern which registers a function can use and who is responsible for saving each one.
  • X0–X7: These are the function parameters. The function can use these for any other purpose modifying them freely. If the calling routine needs them saved, it must save them itself.

  • X0X18: Corruptible registers that a function is free to use without saving. If a caller needs these, then it is responsible for saving them.

  • X19X30: These are callee saved, so must be pushed to the stack if used in a function.

  • SP: This can be freely used by the called routine. The routine must POP the stack the same number of times that it PUSHes, so it’s intact for the calling routine.

  • LR: The called routine must preserve this as we discussed in the last section.

  • Condition flags: Neither routine can make any assumptions about the condition flags. As far as the called routine is concerned, all the flags are unknown; similarly they are unknown to the caller when the function returns.

Summary of the Function Call Algorithm

Calling routine:
  1. 1.

    If we need any of X0–X18, save them.

     
  2. 2.

    Move first eight parameters into registers X0X7.

     
  3. 3.

    Push any additional parameters onto the stack.

     
  4. 4.

    Use BL to call the function.

     
  5. 5.

    Evaluate the return code in X0.

     
  6. 6.

    Restore any of X0X18 that we saved.

     
Called function:
  1. 1.

    PUSH LR and X19X30 onto the stack if used in the routine.

     
  2. 2.

    Do our work.

     
  3. 3.

    Put our return code into X0.

     
  4. 4.

    POP LR and X19X30 if pushed in step 1.

     
  5. 5.

    Use the RET instruction to return execution to the caller.

     
Note

We can save steps if we just use X0X18 for function parameters, return codes, and short-term work. Then we never have to save and restore them around function calls.

These aren’t all the rules. The coprocessors also have registers that might need saving. We’ll discuss those rules when we discuss the coprocessors.

Let’s look at a practical example by converting our upper-case program into a function that we can call with parameters to convert any strings we wish.

Upper-Case Revisited

Let’s organize our upper-case example from Chapter 5, “Thanks for the Memories,” as a proper function. We’ll move the function into its own file and modify the makefile to make both the calling program and the upper-case function.

First of all, create a file called main.s containing Listing 6-3 for the driving application.
//
// Assembler program to convert a string to
// all upper case by calling a function.
//
// X0-X2 - parameters to linux function services
// X1 - address of output string
// X0 - address of input string
// X8 - linux function number
//
.global _start     // Provide program starting address
_start: LDR X0, =instr // start of input string
      LDR   X1, =outstr // address of output string
      BL    toupper
// Setup the parameters to print our hex number
// and then call Linux to do it.
      MOV   X2, X0 // return code is the length
      MOV   X0, #1        // 1 = StdOut
      LDR   X1, =outstr   // string to print
      MOV   X8, #64       // linux write system call
      SVC   0             // Call linux to output the string
// Setup the parameters to exit the program
// and then call Linux to do it.
      MOV     X0, #0   // Use 0 return code
      MOV     X8, #93  // Service command code 93
      SVC     0        // Call linux to terminates
.data
instr:  .asciz "This is our Test String that we will convert. "
outstr:     .fill   255, 1, 0
Listing 6-3

Main program for upper-case example

Next, create a file called upper.s containing Listing 6-4, the upper-case conversion function.
//
// Assembler program to convert a string to
// all upper case.
//
// X1 - address of output string
// X0 - address of input string
// X4 - original output string for length calc.
// W5 - current character being processed
//
.global toupper // Allow other files to call this routine
toupper: MOV   X4, X1
// The loop is until byte pointed to by X1 is non-zero
loop:  LDRB W5, [X0], #1 // load character and incr ptr
// If W5 > 'z' then goto cont
      CMP   W5, #'z'        // is letter > 'z'?
      B.GT  cont
// Else if W5 < 'a' then goto end if
      CMP   W5, #'a'
      B.LT  cont  // goto to end if
// if we got here then the letter is lower case,
// so convert it.
      SUB   W5, W5, #('a'-'A')
cont: // end if
      STRB  W5, [X1], #1 // store character to output str
      CMP   W5, #0       // stop on hitting a null char
      B.NE  loop         // loop if character isn't null
      SUB   X0, X1, X4   // get the len by subing the ptrs
      RET         // Return to caller
Listing 6-4

Function to convert strings to all upper-case

To build these, use the makefile in Listing 6-5.
UPPEROBJS = main.o upper.o
ifdef DEBUG
DEBUGFLGS = -g
else
DEBUGFLGS =
endif
LSTFLGS =
all: upper
%.o : %.s
     as $(DEBUGFLGS) $(LSTFLGS) $< -o $@
upper: $(UPPEROBJS)
     ld -o upper $(UPPEROBJS)
Listing 6-5

Makefile for the upper-case function example

Note

The toupper function doesn’t call any other functions, so we don’t save LR. If we ever change it to do so, we need to push LR to the stack and pop it before we return. Since X0X18 are all corruptible, we have plenty of general-purpose registers to use without needing to save any.

Most C programmers will object that this function is dangerous. If the input string isn’t NULL terminated, then it will overrun the output string buffer—overwriting the memory past the end. The solution is to pass in a third parameter with the buffer lengths and check in the loop that we stop at the end of the buffer if there is no NULL character.

This routine only processes the core ASCII characters. It doesn’t handle the localized characters, for example, é won’t be converted to É.

In the upper-case function, we didn’t need any additional memory, since we could do all the work with the available registers. When we code larger functions, we often require more memory for the variables than fit in the registers. Rather than add clutter to the .data section, we store these variables on the stack. The section of the stack that holds our local variables is called a stack frame.

Stack Frames

Stacks work great for saving and restoring registers, but to work well for other data, we need the concept of a stack frame. Here we allocate a block or frame of memory on the stack that we use to store our variables. This is an efficient mechanism to allocate some memory at the start of a function and then release it before we return.

PUSHing variables on the stack isn’t practical, since we need to access them in a random order, rather than the strict LIFO protocol that PUSH/POP enforce.

To allocate space on the stack, we use a subtract instruction to grow the stack by the amount we need. Suppose we need three variables that are each 32-bit integers, say, a, b, and c. Therefore, we need 12 bytes allocated on the stack (3 variables x 4 bytes/word). We then need to round up to the next multiple of 16 to keep SP 16-byte aligned.
SUB   SP, SP, #16
This moves the stack pointer down by 16 bytes, providing us a region of memory on the stack to place the variables. Suppose a is in W0, b in W1, and c in W2—we can then store these using
STR    W0, [SP]         // Store a
STR    W1, [SP, #4]     // Store b
STR    W2, [SP, #8]     // Store c
Before the end of the function, we need to execute
ADD   SP, SP, #16

to release our variables from the stack. Remember, it is the responsibility of a function to restore SP to its original state before returning.

This is the simplest way to allocate some variables. However, if we are doing a lot of other things with the stack in our function, it can be hard to keep track of these offsets. The way to alleviate this is with a stack frame. Here we allocate a region on the stack and keep a pointer to this region in another register that we will refer to as the frame pointer (FP). You could use any register as the FP, but we will follow the C programming convention and use X29.

To use a stack frame, first set the frame pointer to the next free spot on the stack (it grows in descending addresses), then allocate the space as before:
SUB   FP, SP, #16
SUB   SP, SP, #16
Now address the variables using an offset from FP:
STR   W0, [FP]         // Store a
STR   W1, [FP, #-4]    // Store b
STR   W2, [FP, #-8]    // Store c

When using FP, include it in the list of registers we PUSH at the beginning of the function and then POP at the end. Since X29, the FP is one we are responsible for saving. One good thing about using FP is that it isn’t required to be 16-byte aligned.

In this book, we’ll tend to NOT use FP. This saves a couple of cycles on function entry and exit. After all, in Assembly Language programming, we want to be efficient.

Stack Frame Example

Listing 6-6 is a simple skeletal example of a function that creates three variables on the stack.
// Simple function that takes 2 parameters
// VAR1 and VAR2. The function adds them,
// storing the result in a variable SUM.
// The function returns the sum.
// It is assumed this function does other work,
// including other functions.
// Define our variables
            .EQU  VAR1, 0
            .EQU  VAR2, 4
            .EQU  SUM,  8
SUMFN:      STP   LR, FP, [SP, #-16]!
            SUB   FP, SP, #16
            SUB   SP, SP, #16       // room for 3 32-bit values
            STR   W0, [FP, #VAR1]   // save first param.
            STR   W1, [FP, #VAR2]   // save second param.
// Do a bunch of other work, but don’t change SP.
            LDR   W4, [FP, #VAR1]
            LDR   W5, [FP, #VAR2]
            ADD   W6, W4, W5
            STR   W6, [FP, #SUM]
// Do other work
// Function Epilog
            LDR   W0, [FP, #SUM]    // load sum to return
            ADD   SP, SP, #16       // Release local vars
            LDP   LR, FP, [SP], #16 // Restore LR, FP
            RET
Listing 6-6

Simple skeletal function that demonstrates a stack frame

Defining Symbols

In this example, we introduce the .EQU Assembler directive. This directive allows us to define symbols that will be substituted by the Assembler before generating the compiled code. This way we can make the code more readable. In this example, keeping track of which variable is which on the stack makes the code hard to read and error-prone. With the .EQU directive, we can define each variable’s offset on the stack once.

Sadly, .EQU only defines numbers, so we can’t define the whole “[SP, #4]” type string.

Macros

Another way to make the upper-case loop into a reusable bit of code is to use macros. The GNU Assembler has a powerful macro capability; with macros rather than calling a function, the Assembler creates a copy of the code in each place where it is called, substituting any parameters. Consider this alternate implementation of our upper-case program—the first file is mainmacro.s containing the contents of Listing 6-7.
//
// Assembler program to convert a string to
// all upper case by calling a function.
//
// X0-X2 - parameters to Linux function services
// X1 - address of output string
// X0 - address of input string
// X2 - original address of input string
// X8 - Linux function number
//
.include "uppermacro.s"
.global _start      // Provide program starting address
_start:
      // Convert tststr to upper case.
      toupper tststr, buffer
// Setup the parameters to print
// and then call Linux to do it.
      MOV   X2, X0 // return code is the len of the string
      MOV   X0, #1      // 1 = StdOut
      LDR   X1, =buffer // string to print
      MOV   X8, #64     // linux write system call
      SVC   0           // Call linux to output the string
      // Convert second string tststr2.
      toupper tststr2, buffer
// Setup the parameters to print
// and then call Linux to do it.
      MOV   X2, X0 // return code is the len of the string
      MOV   X0, #1          // 1 = StdOut
      LDR   X1, =buffer     // string to print
      MOV   X8, #64         // linux write system call
      SVC   0               // Call linux to output the string
// Setup the parameters to exit the program
// and then call Linux to do it.
      MOV     X0, #0      // Use 0 return code
      MOV     X8, #93     // Service command code 93 terms
        SVC     0         // Call Linux to terminate
.data
tststr:  .asciz  "This is our Test String that we will convert. "
tststr2: .asciz    "A second string to upper case!! "
buffer:     .fill  255, 1, 0
Listing 6-7

Program to call our toupper macro

The macro to make the string all upper-case is in uppermacro.s containing Listing 6-8.
//
// Assembler program to convert a string to
// all upper case.
//
// X1 - address of output string
// X0 - address of input string
// X2 - original output string for length calc.
// W3 - current character being processed
//
// label 1 = loop
// label 2 = cont
.MACRO      toupper      instr, outstr
      LDR   X0, =instr
      LDR   X1, =outstr
      MOV   X2, X1
// The loop is until byte pointed to by X1 is non-zero
1:    LDRB  W3, [X0], #1 // load char and incr pointer
// If R5 > 'z' then goto cont
      CMP   W3, #'z'        // is letter > 'z'?
      B.GT  2f
// Else if R5 < 'a' then goto end if
      CMP   W3, #'a'
      B.LT  2f    // goto to end if
// if we got here then the letter is lower case,
// so convert it.
      SUB   W3, W3, #('a'-'A')
2:    // end if
      STRB  W3, [X1], #1 // store char to output str
      CMP   W3, #0       // stop on hitting a null char
      B.NE  1b           // loop if character isn't null
      SUB   X0, X1, X2   // get the len by subing the ptrs
.ENDM
Listing 6-8

Macro version of our toupper function

Include Directive

The file uppermacro.s defines the macro to convert a string to upper-case. The macro doesn’t generate any code; it just defines the macro for the Assembler to insert wherever it is called from. This file doesn’t generate an object (∗.o) file; rather it is included by whichever file needs to use it.

The .include directive
.include "uppermacro.s"

takes the contents of this file and inserts it at this point, so that the source file becomes larger. This is done before any other processing. This is like the C #include preprocessor directive.

Macro Definition

A macro is defined with the .MACRO directive. This gives the name of the macro and lists its parameters. The macro ends at the following .ENDM directive. The form of the directive is
.MACRO   macroname   parameter1, parameter2, ...
Within the macro, you specify the parameters by preceding their name with a backslash, for instance, parameter1 to place the value of parameter1. The toupper macro defines two parameters instr and outstr:
.MACRO   toupper   instr, outstr

The parameters are used in the code with instr and oustr. These are text substitutions and need to result in correct Assembly syntax or you will get an error.

Labels

The labels “loop” and “cont” are replaced with the labels “1” and “2.” This takes away from the readability of the program. The reason we do this is that if we didn’t, we’d get an error that a label was defined more than once, if we use the macro more than once. The trick here is that the Assembler lets you define numeric labels as many times as you want. To reference them in the code, we used
B.GT   2f
B.NE   1b       @ loop if character isn't null

The f after the 2 means the next label 2 in the forward direction. The 1b means the next label 1 in the backward direction.

To prove that this works, we call toupper twice in the mainmacro.s file, to show everything works and that we can reuse this macro as many times as we like.

Why Macros?

Macros substitute a copy of the code at every point they are used. This will make the executable file larger, for example, when using
objdump -d mainmacro

two copies of code are inserted. With functions, there is no extra code generated each time. This is why functions are quite appealing, even with the extra work of dealing with the stack.

The reason macros get used is performance. Most ARM devices have a gigabyte or more of memory—a lot of room for multiple copies of code. Remember that whenever we branch, we must restart the execution pipeline, making branching an expensive instruction. With macros, we eliminate the BL branch to call the function and the RET branch to return. We also eliminate any instructions to save and restore the registers we use. If a macro is small and we use it a lot, there could be considerable execution time savings.

Note

Notice in the macro implementation of toupper that only the registers X0X3 were used. This avoids using any registers important to the caller. There is no standard on how to regulate register usage with macros, like there’s with functions, so it is up to you the programmer to avoid conflicts and strange bugs.

We can also use macros to make the code more readable and easier to write, as described in the next section.

Macros to Improve Code

Using LDR, LDP, STR, and STP to manipulate the stack is clumsy and error-prone. You spend a lot of time cutting and pasting the code from other places to try and get it correct. It would be nice if there were instruction aliases to push and pop the stack. In fact, there is in 32-bit ARM Assembly Language. However, with macros, we can overcome this. Consider Listing 6-9.
.MACRO    PUSH1 register
          STR    egister, [SP, #-16]!
.ENDM
.MACRO    POP1  register
          LDR    egister, [SP], #16
.ENDM
.MACRO    PUSH2 register1, register2
          STP    egister1, egister2, [SP, #-16]!
.ENDM
.MACRO    POP2  register1, register2
          LDP    egister1, egister2, [SP], #16
.ENDM
Listing 6-9

Define four macros for pushing and popping the stack

This simplifies our code since we can use these to write code like in Listing 6-10.
Myfunction:
      PUSH1 LR
      PUSH2 X20, X23
// function body ...
      POP2  X20, X23
      POP1  LR
      RET
Listing 6-10

Use our push and pop macros

This makes writing the function prologues and epilogues easier and clearer.

Summary

In this chapter, we covered the ARM stack and how it’s used to help implement functions. We covered how to write and call functions as a first step to creating libraries of reusable code. We learned how to manage register usage, so there aren’t any conflicts between calling programs and functions. We learned the function calling protocol, which allows us to interoperate with other programming languages. Also, we looked at defining stack-based storage for local variables and how to use this memory.

Finally, we covered the GNU Assembler’s macro ability as an alternative to functions in certain performance critical applications.

Exercises

  1. 1.

    If we are coding for an operating system where the stack grows upward, how would we code the LDR, LDP, STR, and STP instructions?

     
  2. 2.

    Suppose we have a function that uses registers X4, X5, W20, X23, and W27. Further this function calls other functions. Code the prologue and epilogue of this function to store and restore the correct registers to/from the stack.

     
  3. 3.

    Write a function to convert text to all lower-case. Have this function in one file and a main program in another file. In the main program, call the function three times with different test strings.

     
  4. 4.

    Convert the lower-case program in Exercise 3 to a macro. Have it run on the same three test strings to ensure it works properly.

     
  5. 5.

    Why does the function calling protocol have some registers need to be saved by the caller and some by the callee? Why not make all saved by one or the other?

     
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.234.191