ARM assembler in Raspberry Pi – Chapter 22
Several times in previous chapters we have talked about ARM as an architecture that has several features aimed at embedding systems. In embedded systems memory is scarce and expensive, so designs that help reduce the memory footprint are very welcome. Today we will see another of these features: the Thumb instruction set.
The Thumb instruction set
In previous installments we have been working with the ARMv6 instruction set (the one implemented in the Raspberry Pi). In this instruction set, all instructions are 32-bit wide, so every instruction takes 4 bytes. This is a common design since the arrival of RISC processors. That said, in some scenarios such codification is overkill in terms of memory consumption: many platforms are very simple and rarely need all the features provided by the instruction set. If only they could use a subset of the original instruction set that can be encoded in a smaller number of bits!
So, this is what the Thumb instruction set is all about. They are a reencoded subset of the ARM instructions that take only 16 bits per instructions. This means that we will have to waive away some instructions. As a benefit our code density is higher: most of the time we will be able to encode the code of our programs in half the space.
Support of Thumb in Raspbian
While the processor of the Raspberry Pi properly supports Thumb, there is still some software support that unfortunately is not provided by Raspbian. This means that we will be able to write
some snippets in Thumb but in general this is not supported (if you try to use Thumb for a full C program you will end with a sorry, unimplemented
message by the compiler).
Instructions
Thumb provides about 45 instructions (of about 115 in ARMv6). The narrower codification of 16 bit means that we will be more limited in what we can do in our code. Registers are split into two sets: low registers, r0
to r7
, and high registers, r8
to r15
. Most instructions can only fully work with low registers and some others have limited behaviour when working with high registers.
Also, Thumb instructions cannot be predicated. Recall that almost every ARM instruction can be made conditional depending on the flags in the cpsr
register. This is not the case in Thumb where only the branch instruction is conditional.
Mixing ARM and Thumb is only possible at function level: a function must be wholly ARM or Thumb, it cannot be a mix of the two instruction sets. Recall that our Raspbian system does not support Thumb so at some point we will have to jump from ARM code to Thumb code. This is done using the instruction (available in both instruction sets) blx
. This instruction behaves like the bl
instruction we use for function calls but changes the state of the processor from ARM to Thumb (or Thumb to ARM).
We also have to tell the assembler that some portion of assembler is actually Thumb while the other is ARM. Since by default the assembler expects ARM, we will have to change to Thumb at some point.
From ARM to Thumb
Let's start with a very simple program returning an error code of 2 set in Thumb.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* thumb-first.s */
.text
.code 16 /* Here we say we will use Thumb */
.align 2 /* Make sure instructions are aligned at 2-byte boundary */
thumb_function:
mov r0, #2 /* r0 ← 2 */
bx lr /* return */
.code 32 /* Here we say we will use ARM */
.align 4 /* Make sure instructions are aligned at 4-byte boundary */
.globl main
main:
push {r4, lr}
blx thumb_function /* From ARM to Thumb we use blx */
pop {r4, lr}
bx lr
Thumb instructions in our thumb_function actually resemble ARM instructions. In fact most of the time there will not be much difference. As stated above, Thumb instructions are more limited in features than their ARM counterparts.
If we run the program, it does what we expect.
How can we tell our program actually mixes ARM and Thumb? We can use objdump -d
to dump the instructions of our thumb-first.o
file.
Check thumb_function
: its two instructions are encoded in just two bytes (instruction bx lr
is at offset 2 of mov r0, #2
. Compare this to the instructions in main
: each one is at offset 4 of its predecessor instruction. Note that some padding was added by the assembler at the end of the thumb_function
in form of nop
s (that should not be executed, anyway).
Calling functions in Thumb
In in Thumb we want to follow the AAPCS convention like we do when in ARM mode, but then some oddities happen. Consider the following snippet where thumb_function_1
calls thumb_function_2
.
Unfortunately, this will be rejected by the assembler. If you recall from chapter 10, in ARM push and pop are mnemonics for stmdb sp!
and ldmia sp!
, respectively. But in Thumb mode push
and pop
are instructions on their own and so they are more limited: push
can only use low registers and lr
, pop
can only use low registers and pc
. The behaviour of these two instructions almost the same as the ARM mnemomics. So, you are now probably wondering why these two special cases for lr
and pc
. This is the trick: in Thumb mode pop {pc}
is equivalent to pop the value val
from the stack and then do bx val
. So the two instruction sequence: pop {r4, lr}
followed by bx lr
becomes simply pop {r4, pc}
.
So, our code will look like this.
From Thumb to ARM
Finally we may want to call an ARM function from Thumb. As long as we stick to AAPCS everything should work correctly. The Thumb instruction to call an ARM function is again blx
. Following is an example of a small program that says "Hello world" four times calling printf
, a function in the C library that in Raspbian is of course implemented using ARM instructions.
To know more
In next installments we will go back to ARM, so if you are interested in Thumb, you may want to check this Thumb 16-bit Instruction Set Quick Reference Card provided by ARM. When checking that card, be aware that the processor of the Raspberry Pi only implements ARMv6T, not ARMv6T2.
That's all for today.