ArmAssemblyInt:RTS:C

Copyright 2018, Clinton A Staley

Concepts

  1. Post-increment
  2. Loop-bottom check
  3. Stack frame removal
  4. Return value
  5. Linking via gcc

Overview

In the prior segment we started the rangeSum call and went over code to set up rangeSum's stack frame. Let's finish that call now.

Summation loop

The loop on lines 13-24 adds up values from lo to hi, putting the total in sum.

Loop body, and postincrement

The body of the loop is at the top, on lines 13-19, implementing the C code sum = sum + lo++;

Lines 14-16 load lo (at offset -8 from fp) from the stack frame into r3, add one to r3, putting the result into r2, and write r3 back into the same stack frame location, in effect doing a lo++ operation.

Question 1

Line 17-19 add lo into sum. But, do they add the value of lo as it was before incrementation, or after incrementation? Explain your answer.

Answer 1

Before incrementation, because they use r3, which still has the pre-incrementation content of lo. It was r2 that held the incremented lo++ value.

This illustrates the exact behavior of a C postincrement operator. The C expression lo++ increases lo by 1, but its value is the original lo value, not the incremented value. The corresponding assembly retains the original value (e.g in r3) and uses it, even though the variable has been increased.

A preincrement operator: ++lo, works the other way. It increments lo and has the incremented result as its value.

And, notice something subtle about lines 17-19. Line 17 reads sum into r2, but line 18 puts the total into r3, not r2, effectively making r3 the new value of sum, which is saved back into the stack. Compilers like to do this with registers -- change their purpose during computations, to avoid needing an extra register.

Question 2 Adjust the code on lines 17-19 so that it would perform sum = sum + ++lo. This can be done with just one small change

Answer 2

Use r3 instead of r2 to store sum. This would load sum into r3, and add r2 (the incremented lo value) into it.

Loop Test

The loop test on lines 21-23 checks to see if lo < hi and jumps back to .L3 for another iteration if so.

Question 3

But, the C code specified a while loop, so the test should be at the start of the loop, before the first iteration is even run. A test at the end of the loop seems like a do-while. Is there a bug in the assembly?

Answer 3

No, because line 12 jumps down to the test, avoiding the loop body initially. Then we do the test, jump back to the loop body if it's true, do the test again, etc. So it acts like a while loop.

Question 4

So, apparently it works, but it's kind of odd. Why do it that way, instead of putting the loop test at the top, and jumping around the body past the loop, when the loop test is false? (Like we did in earlier examples.) Compare the number of branches needed in Prg4's loop vs earlier while-loops with the test on top.

Answer 4 After the one-time jump to the L2, each iteration of the loop requires only one branch, to go back to the top, not two, one at the top and one at the bottom.

Are branches such a big deal that we'd like to avoid them? Yes, as it turns out. The reason has to do with the pipelined architecture of ARM processors, a topic we'll examine later. For now, though, get used to this odd test-at-the-bottom while-loop, since compilers like to translate while-loops this way for efficiency.

Returning from rangeSum

Lines 26-29 implement the C code return sum. Specifically, they remove rangeSum's stack frame, and return back to line 47 in main, returning the value in sum in r0. ARM calling convention uses r0 for the return value of a function if the return value is just a one word value, like sum.

Returning a value

The So, line 26 loads sum into r0 to be returned.

Removing a stack frame

Line 27 changes sp to point to the old fp value that we added to the stack on line 5, effectively removing all the local state. Note that we do not deliberately erase that state, e.g. by setting it to 0. We don't need to. Since it's no longer part of the RTS, it can just be left as garbage data in memory, to be overwritten by some later stack frame.

The main function will expect its original fp to be unchanged upon return. So, line 28 restores the original fp from the stored copy on the stack, and moves sp down another word.

Question 5

To be sure you understand, describe what diagram changes we should make to reflect the result of line 28.

Answer 5

fp now points to the bottom of the main stack frame. And sp points to the return loc word.

All that now remains of the rangeSum stack frame is the stored lr value, which line 29 loads into pc, moving sp another 4 bytes down, to leave it at the top of the main stack frame where we started.

Question 6

So, how do we "branch" back to line 47, given there's no b instruction?

Answer 6

Copying a value into pc is a branch, since it changes the program counter and thus the next instruction to be performed.

Upon return to the main, line 47 stores the result from r0 into the memory location at fp-4, which is the location in main's stack frame for variable total.

Call of printf

All that is left in main is the code for printf("%d\n", total). Lines 49 through 51 do this.

Line 49 sets up the first parameter. Per C rules, the parameter "Total is %d\n" is a pointer to the start of the "Total is %d\n" string. We have that string pre-loaded at label .LC0, complete with the ASCII 0 value expected at the end of all C strings. And the string's address is stored at .L6, following patterns we saw in Prg3.s. So ldr r0, .L6 loads the string's address into r0, to provide the first parameter for the printf call.

Question 7

Replace line 49 with a different version that eliminates the need to explicitly declare the word at .L6.

Answer 7

ldr r0, =.LC0 will do this, automatically creating the .L6 memory location containing the address represented by .LC0

The second parameter, total, is easier. Line 50 loads it from the right offset in the stack frame into r1.

Finally, line 51 makes the call to printf.

Main's stack frame

That leaves us with lines 53-56, and they deserve some explanation. Main returns a 0, as is typical for C programs. But what does it return to?

The C compiler creates code that calls main, and automatically starts the program at that code. That code gets the return value from main, and uses it as the exit code for an swi call, as we've seen in the hand-written code. That main-caller is largely written in assembly, and silently added by the compiler, so we don't have to look at it closely.

But it does mean that main is called with a return location in lr, and is expected to adjust fp and sp, and to retain the lr value, etc. just like any other function. That's why those two words at the bottom of main's stack frame have been sitting there all this time.

Lines 40-43 at the start of main set up a stack frame for main in the same way we did for rangeSum, but with just one word of local state, for total. And lines 53-56 put the return value of 0 into r0, and then undo main's stack frame just as we did for rangeSum.

Linking C libraries

And a final point: where is the printf code for line 51 to branch to? Recall from earlier lecture that the Unix ld command takes incomplete code generated from as and fills in missing libraries. The ld command would see the use of printf in the assembled Prg4.s and link it into the final executable.

But, we don't even need to run the ld or the as commands to assemble Prg4.s. Any assembly program that has a main function can be assembled by gcc itself. gcc is smart enough to recognize an assembly file as a commandline argument, and run both as and ld automatically. Indeed, you have to do it that way, since as doesn't know to add that main-caller we discussed earlier.

So, to assemble Prg4.s into a working Prg4 executable, complete with printf function, we just run:

gcc Prg4.s -o Prg4