Reducing Code Size on ARM/Thumb processors



Some ARM processor cores support two different instruction sets. They can run the standard ARM instruction set, where each instruction occupies 32 bits, and also the reduced size Thumb instruction set, where each instruction occupies 16 bits. Thumb instructions are more limited in what they can do compared to ARM instructions, but they can represent a considerable saving in the amount of space occupied by the instructions. It is possible to dynamically switch between the two instruction-sets, thus gaining the advantages of both.

The GNUPro compilers are able to produce code for both the ARM and Thumb instruction sets, but only at a file level of granularity. Thus the programmer can choose whether individual functions should be encoded as either ARM or Thumb instructions, but they cannot specify that specific parts of a function should be ARM or Thumb. The other parts of the GNUPro tools (the assembler and linker and so on), all support mixing ARM and Thumb code at any level.

The GNUPro toolkit has two compilers, one of which produces ARM assembler, and the other produces Thumb assembler. If the programmer wants to use just one instruction set to compile their program then no special procedures need to be followed, other than selecting the correct compiler. If the programmer wants to use both instruction sets in their program, then they must separate the ARM and Thumb parts of their program into separate files and compile each individually, while also specifiying the '-mthumb-interwork' command line flag. When the assembled object files are linked together the linker will generate special code to switch between the two instruction sets whenever a call is made from an ARM function to a Thumb function or vice versa.

1. Create source code

Create the following sample source code and save it as 'arm_stuff.c'.

extern int thumb_func (int);
int main (void) { return 7 + thumb_func (7); }

Create the following sample source code and save it as 'thumb_stuff.c'.

int thumb_func (int arg) { return arg + 7; }

2. Compile and link from source code

First compile the ARM sample code:


% arm-elf-gcc -g -c -O2 -mthumb-interwork arm_stuff.c


Then compile the Thumb sample code:

% thumb-elf-gcc -g -c -O2 -mthumb-interwork thumb_stuff.c


Then link

% arm-elf-gcc arm_stuff.o thumb_stuff.o -mthumb-interwork -o program


The '-g' and '-O2' command-line options are optional. They are used here to make the output of the debugger easier to understand.

3. Run on the GNUPro Simulator

The following sample debugging session was run on the GNUPro ARM simulator. Although the GNUPro ARM simulator is not a supported platform under eCos, the following sample GDB session demonstrates the resulting code size reduction. The GDB release date, the host configuration, and the 'target' command will vary.


%arm-elf-gdb program

GNU gdb 4.18-ecos-99r1-991015

Copyright 1998 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. This version of GDB is supported for customers of Cygnus Solutions. Type "show warranty" for details. 

This GDB was configured as "--host=sparc-solaris-2.6 --target=arm-elf"...

(gdb) target sim

Connected to the simulator.

(gdb) load

Loading section .text, size 0x1858 vma 0x8000

Loading section .rodata, size 0x2c vma 0x9858

Loading section .data, size 0x850 vma 0x9984

Loading section .ctors, size 0x8 vma 0xa1d4

Loading section .dtors, size 0x8 vma 0xa1dc

Start address 0x8000

Transfer rate: 67360 bits in <1 sec.

(gdb) break main

Breakpoint 1 at 0x80ec: file arm_stuff.c, line 2.

(gdb) break thumb_func

Breakpoint 2 at 0x8104: file thumb_stuff.c, line 1.

(gdb) run

Starting program: /tmp/program 

Breakpoint 1, 0x80ec in main () at arm_stuff.c:2

2 int main (void) { return 7 + thumb_func(7) ; }

(gdb) disassemble

Dump of assembler code for function main:

0x80e0 <main>: mov ip, sp

0x80e4 <main+4>: stmdb sp!, {fp, ip, lr, pc}

0x80e8 <main+8>: sub fp, ip, #4

0x80ec <main+12>: bl 0x81ec <__gccmain>

0x80f0 <main+16>: mov r0, #7

0x80f4 <main+20>: bl 0x984c <__thumb_func_from_arm>

0x80f8 <main+24>: add r0, r0, #7

0x80fc <main+28>: ldmdb fp, {fp, sp, lr}

0x8100 <main+32>: bx lr

End of assembler dump.

(gdb) continue


Breakpoint 2, thumb_func (arg=7) at thumb_stuff.c:1

1 int thumb_func (int arg) { return arg + 7 ; }

(gdb) disassemble

Dump of assembler code for function thumb_func:

0x8104 <thumb_func>: 3007 add r0, #7

0x8106 <thumb_func+2>: 4770 bx lr

End of assembler dump.

(gdb) quit

The program is running. Exit anyway? (y or n) y

Note how the ARM 'add' instruction at '<main+24>' occupies 4 bytes, whereas the Thumb 'add' instruction at '<thumb_func>' occupies 2 bytes.