Saturday, February 25, 2017

ARM Assembly: Type Casting

This topic was kind of skimmed over in the last article.  It is time to take it head on.  This is about integer type casting, not casting strings to integers, integers to strings, or integers to floats.  Those will be covered later.  The biggest value in type casting is generally taking small data types and extending them to larger ones.  ARM has a series of instructions specifically for this purpose.  It does not have instructions for down casting though.  This is easy enough, if you know how.

In the documentation, type casting instructions are categorized as "Signed extend" and "Unsigned extend".  Literally, we are taking values stored as a number of bits, and extending the number of bits the values can occupy.  The simplest operation here is unsigned extend, sometimes also called "zero extend", since all it does is tacks an appropriate number of zeroes to the left side of the value.  Signed extend notes the value of the sign bit, and it tacks the appropriate number of that value to the left side of the value.  Both of these are trivial but very useful operations.

There are three standard signed extend instructions: SXTH, SXTB16, and SXTB.  The first extends a halfword to a word, or a 16 bit value to a 32 bit value, by filling the top 16 bits with whatever the top bit (or sign bit) of the 16 bit value is.  This ensures that even though the representation is changing, the actual value represented stays the same.  The second extends two bytes (8 bit values) to two half words (16 bit values).  This is similar to the parallel math instructions.  A signed 8 bit value is placed in bytes [7:0] of the register, and a second is placed in bytes [23:16].  The instruction sign extends them, and the result is two 16 bit values, one in [31:16], and the other in [15:0].  This is an especially good arrangement, if the next operation is going to be parallel addition or subtraction.  The last instruction above casts a signed byte to a signed word (8 bits to 32 bits).

There are also three standard unsigned extend instructions: UXTH, UXTB16, and UXTB.  These follow the same pattern.  The first casts a halfword to a word, the second casts two bytes to haflwords in parallel, and the third casts a byte to a word.  As with the parallel arithmetic instructions, the parallel extend instructions can be used to cast just one byte to one halfword, by just ignoring the upper half of the register.

There are also some type casting instructions that do additional operations at the same time.  These operations are all addition.  We won't discuss those here, but it is good to be aware that ARM has a number of math instructions that can do multiple operations in a single cycle.

So far, we have worked exclusively with unsigned values in the examples, and for most of this series we will stick to unsigned values for convenience.  This time though, we will work with some signed values.

printf() assumes all of its integer arguments are 32 bits.  This means that it is necessary to cast any 8 or 16 bit integers into 32 bit integers, before sending them to printf().  With unsigned values, where a register contains a single value, the 8, 16, and 32 bit interpretations are the same, but with signed values, they are not.  Let's write a small program that uses type casting to illustrate the fact that signed values can mean different things depending on data type size.
.data
string:
    .asciz "8 bit uncast value: %d, 8 bit value cast to 32 bits: %d\n"

.text
.global main
main:
    push {r12, lr}

    mov r1, #150
    sxtb r2, r1
    ldr r0, =string
    bl printf

    pop {r12, lr}
    bx lr
First let's discuss representation.  We are putting the value 150 into r1.  At least, that is what it looks like.  In reality, we are putting an 8 bit -106 into r1, but if we wrote mov r1, #-106, the assembler would put it into the register as a 32 bit -106, not an 8 bit -106.  What is the difference?  In binary, an 8 bit -106 looks like this: 10010110.  In binary a 32 bit -106 has 24 additional 1s on the left of that.  In a 32 bit register, an 8 bit -106 is 24 0s followed by 10010110, while a 32 bit -106 is 24 1s followed by 10010110.  The SXTB instruction casts to 32 bits by observing the left-most bit of 10010110 (which is 1), and then filling the left 24 bits of the register with that.  Now, the 32 bit interpretation of 24 0s followed by 10010110 is 150, which is why printf() displays 150 initially.

So, that 150 I put in r1 initially is the 8 bit representation of -106, but printf() won't interpret it that way, because it assumes all input is 32 bits.  If this sounds complicated, don't worry.  It kind of is.  Outside of computers, we don't really do anything this way.  It can take a bit to grasp.  The point with this is that remembering to type cast is important.  Let's say we want to do some math operation on our 8 bit -106, with a 32 bit value.  If we don't sign extend our 8 bit value to 32 bits first, it will act exactly like it was just a 32 bit 150.

The fact is, to the processor, values in registers are just strings of 1s and 0s or offs and ons.  It has no concept of type.  It neither knows nor cares about type.  Type is a function of how we choose to use the values stored.  To the processor an 8 bit -106 and a 32 bit 150 are identical.  It will treat the value however we tell it to.  So, if we tell it to do 32 bit math with an 8 bit -106, then that 8 bit -106 will be treated as a 32 bit 150, because the processor does not know our intent.

So now we can do up casting.  What about down casting?  ARM does not provide us with any down casting instructions.  In part this is because down casting is actually not that common.  In part this is because down casting is not horribly difficult.  We won't do an example, but I will explain how it works.

Unsigned down casting is trivial.  Merely zero out all bits above the data type size, and you have successfully down casted.  This can easily be done with an AND instruction, that masks the bits beyond the data type.  So, to cast a 32 or 16 bit unsigned integer in r1 to an 8 bit integer, you might use the instruction and r1, r1, #255.  The number 255 is 11111111 in binary.  In a 32 bit register, the left 24 bits are all 0s, so the AND will zero out all of those bits in the input value.  To cast 32 bits to 16 bits, it is a bit more complicated, because the <#imm8m> option for <Operand2> has a serious limitation.  The 8 in this option means that the immediate value must be an 8 bit value, which can be shifted to any position in the bit string.  To mask only 16 bits, we need a 16 bit value.  If you try to do this with an immediate value in an AND instruction though, you will be greeted with this error message: Error: invalid constant (ffff) after fixup.  You will have to construct this value, or you will have to store it in memory and load it.  Since memory accesses are slow, constructing the value with two instructions is probably the best option.  Start with mov r4, #0b11111111.  This will give you the bottom 8 bits.  Right after this do orr r4, #0b1111111100000000.  This will add the other 8 bits, giving you the final value.  Then, use r4 (or whatever register you have chosen to use) in the AND instruction as your mask.  Alternatively, ARM's documentation has a "Bit field" section, containing a BFC or Bit Field Clear instruction that can be used to clear a section of a register, given an index and a width.  It appears this could zero the top half or three quarters of a register in a single instruction.

Signed down casting is far less trivial.  I won't go too deep into the details here, because you will need to learn about conditionals before we can go there, but I can still describe the process.  The first thing you need to do is check the sign bit.  The easiest way to do that is to check whether or not the value is less than 0.  If it is less than 0, then the sign bit is 1.  If it is not less than 0, the sign bit is 0.  Then, mask it to the size you want.  Last, you replace the top bit (bit 7, for a 8 bit down cast, and bit 15 for a 16 bit down cast) with the value of the sign bit you checked.  Checking the sign bit requires the use of conditional logic, which we will learn later.  Writing the new sign bit can be done most easily with an ORR (to set to 1) or BIC (to set to 0) instruction.

Note that down casting often loses information.  The C compiler will generally issue a warning when it detects down casting.  Sometimes, however, it is necessary or the loss of information is even desirable.  If you feel like you need to down cast though, it is a good idea to think carefully, to make sure you are not losing anything important.

No comments:

Post a Comment