ArmAssembly:LdrStr:C

Copyright 2018, Clinton A Staley

Concepts

  1. Byte and halfword loading
  2. Address register autoincrement
  3. Multiplication
  4. ASCII -> binary translation

Prg3.s Resumed...

We resume our Prg3.s discussion from where we left off in the prior segment. And I've added a register diagram, showing just the registers we'll use, to the side. Recall that R0 and R1 already have values 2057 and -200 from the work done by lines 15 through 17.

The next job is to get the value 2097 into r3 by translating the string of digits "2097\0" into an equivalent integer. This requires a lot of code, all of lines 19-32. Be sure you see the difference between five bytes containing ASCII codes 50, 48, 57, 55, and 0, and the 32-bit binary pattern 00000000000000000000100000110001, which is 2097. Conversion from humanly readable sequences of ASCII digits to the binary integer form is a common operation, usually done automatically by I/O libraries, e.g. C's scanf function. But you should understand the logic of this conversion, and its implementation in Prg3.s illustrates several important assembly concepts.

Lines 19-32 loop through each ASCII code in numStr, one at a time. They build the equivalent binary value in r3, adding a new digit's value each time through the loop, so that R3 holds 2, then 20, then 209, and finally 2097.

Setting up the loop

Lines 19-20 start the process by initializing r3 to 0, and then loading r1 with the address represented by the label numStr, which itself was stored at location numStrLoc. Recall from the end of the prior segment that line 20 does this via a pseudoinstruction, which is really a pc-relative ldr of the value at numStrLoc.

R1 thus starts with the address of the '2' in the string '2097'. (We agreed in the earlier segment that this would be 4 if the program starts at address 0.) The loop will use r1 as an address register to get each ASCII code in the sequence, and will advance it one byte at a time through that sequence.

Question 1

Storing the value of label numStr just so we can load it into a register seems too complex. Couldn't we just write ldr r1, numStr, or perhaps mov r1, numStr? Why not?

Answer 1

Recall again that an address is a 32-bit value, so it can't fit into a machine language instruction as a constant, along with the opcode, register number, etc. And ldr is designed to copy from memory, so anything it loads must come from memory, even if it's an address.

By contrast, the mov on line 22 can copy the constant #10 straight into r4, since 10 is a small value that fits into a mov instruction.

Looping through the bytes

So, now we're ready to process each ASCII code in the string. At line 25, the top of the loop, we load the ASCII code at the address given by offset register r1.

Byte loads and address register autoincrement

But, there are two more new concepts on that line.

First, we can adjust the ldr instruction to load just a byte from memory, not a whole word, by adding b after ldr. A ldrb instruction loads the byte into the bottom byte of the target register, leaving the other bytes 0. The addition of b tells the assembler to set a special bit in the ldr instruction, which tells the CPU to load one byte. Similarly, strb stores the bottom byte from a register into memory. And you can also load or store a halfword (two bytes) by adding "h" to get ldrh or strh.

The ldrb and strb instructions allow any address alignment, not just divisible-by-4 addresses, without bus error. (Technically, they adjust the address to be divisible by 4, load the entire word in question, and then fish out the desired byte.)

Second, we added a #1 after the bracketed address register. This is an autoincrement; it tells the ldr instruction to increase the value in r1 by 1 after the load is done, thus leaving r1 holding the address of the next ASCII code (the "0") for the next time through the loop.

It's often convenient to steadily advance an address register through a range of values in memory, and the autoincrement feature of ldr makes that easy, without needing an additional add instruction. (It works for str too.) The effect is similar to C's ++ operator; line 25 effectively does an "[r1++]" The increment can be larger than 1 -- anywhere between -4095 and 4095 in fact. This is because it uses the address offset bits in the instruction, which are unused otherwise on line 25. You can even do an address offset and an autoincrement by the same amount in the same ldr instruction. For instance:

ldr r4, [r5, #4]!

This both offsets from r5 by 4 bytes, and also increases r5 by 4. You can think of the ! as meaning "Make this offset stick in the register". It's like a C preincrement operation: [++r5].

Question 2

OK, to be clear, what's the difference between ldr r4, [r5, #4]! and ldr r4, [r5], #4? What different effect, if any, do they have on r5 and what different effect on the value loaded?

Answer 2 The effect on r5 is the same, but the first ldr loads from the incremented address and the second loads from the original address.

Multiplication

Once an ASCII code byte is loaded into r2, lines 26 and 27 compare the ASCII code to 0, to see if we've reached the end of the string. They jump past the end of the loop if so. Note that the "0" digit in the middle of the string will not trigger this jump, since it has ASCII code value 48.

Line 28 subtracts 48 from the ASCII code in r2, leaving the numerical value of the digit in r2, e.g. 0 if the code was "0", 1 if it was "1", etc.

Now we need to multiply the current r3 value by 10, and then add in the new digit's value. E.g if r3 is 20, and the next digit is 9, we need to multiply r3 by 10 to get 200, and then add in the 9, arriving at 209.

Multiplication can be done with the 'mul' instruction on line 29.

mul R5, R3, R4 @ R5 = R3*10

ARM assembly has several integer multiplication instructions. Mul, for instance, does a simple multiplication of two registers, in our case, R3 and R4, and saves them into the first register, R5. Mul, unlike sub or add, can only be given registers for its operands, and never constant values. This is the reason for moving 10 into R4 from line 22.

The add instruction on line 30 then adds R2 to that product, completing the operation.

add R2, R5, R2 @ R2 = R5 + R2

Remarkably, that two-step process can be rewritten with one instruction: mla -- multiply-and-add. This instruction takes no fewer than 4 registers:

mla R2, R3, R4, R2 @ R2 = (R3*10) + R2

As the comment indicates, mla computes the product of the second and third registers, adds in the fourth, and places the entire result back into the first register. But it has some interesting restrictions. First, like mul, it works only with registers, not constant values. That's why we put 10 in r4 instead of just writing #10 as a constant in the mla instruction. And, at least on older ARM processors, the first and second registers cannot be the same, due to subtle hardware design issues. That's why we put the result into r2, and then copy it into r3 on the next line.

That completes the loop, with the b loopTop branching back to the loop test at the top for another iteration.

Wrapping up after the loop

Once the loop exits at allDone, r3 holds 2097 (the binary value, not the text string), and R0 contains valB - 200. So the subtraction on line 28 leaves 2097 - (valB - 200), or 40, in r3.

But then we have a problem. We need to compute 143 - r3 to complete the desired computation, but only the second operand in a sub can be a constant. (Recall an earlier discussion where we tore apart the binary contents of a data processing ARM instruction. Only four bits were devoted to the first operand, just enough to specify a register, not any constant.)

We could mov 143 into another register, and then subtract r3 from that register, but that takes an extra instruction and an extra register. Instead, line 38 uses rsb, which is like sub, but reverses the subtraction, with the first operand subtracted from the second. The rsb instruction is designed for exactly this scenario. It's one of many examples of how the ARM instruction set is designed for speed.

In the next segment we'll look at the remaining few instructions, which introduce several important ideas.