← Voltar aos artigos

Thumb e Thumb-2: instruções de 16/32 bits

Até agora todas as instruções que vimos tinham 32 bits fixos. O ARM quebrou essa regra em 1994 com o Thumb: instruções de 16 bits que cabem em metade do espaço. Depois, o Thumb-2 misturou 16 e 32 bits no mesmo programa. O resultado: binários menores, melhor uso de cache e, no caso do arm-box, uma nova arquitetura (--arch=thumb2) que você pode exercitar hoje. Vamos ver os bytes reais.

Por que Thumb existe

Instruções de 32 bits ocupam 4 bytes cada. Num microcontrolador com pouca RAM ou num pipeline estreito, isso é caro. O Thumb comprime as instruções mais comuns para 16 bits (2 bytes), perdendo funcionalidade (menos registradores acessíveis, limites de imediato) mas ganhando densidade de código — tipicamente 65% do tamanho do ARM puro para o mesmo programa. Em cache, menos bytes = menos misses.

Thumb-1 vs Thumb-2

  • Thumb-1 (ARMv4T, ARMv5TE): só 16 bits. Subconjunto reduzido do ARM — não tem todos os registradores, nem todos os modos de endereçamento.
  • Thumb-2 (ARMv6T2, ARMv7): mistura 16 e 32 bits. Quase toda funcionalidade do ARM puro, com densidade do Thumb. É o que o arm-box executa com --arch=thumb2.

Como o processador sabe qual modo

O bit T (bit 5) do CPSR indica o modo:

CPSR bit 5 (T):
  0 = modo ARM   (instruções de 32 bits)
  1 = modo Thumb  (instruções de 16/32 bits)

Quando o processador inicia, o T é definido pelo endereço de reset (bit 0 do vetor). A partir daí, o BX (branch exchange) é quem troca: se o bit 0 do endereço-alvo é 1, entra em Thumb (T=1); se é 0, entra em ARM (T=0). É o interworking — misturar ARM e Thumb no mesmo binário.

O exemplo: a mesma função em ARM e Thumb

Montamos a mesma função simples (soma dois números) nos dois modos. Primeiro o ARM (já conhecido):

00008018<_start>:
8018:e3a00003movr0, #3
801c:e3a01004movr1, #4
8020:eb000000bl8028 <add_arm>
8024:ef000000svc0
00008028<add_arm>:
8028:e0800001addr0, r0, r1
802c:e12fff1ebxlr

Agora o Thumb — bytes reais do devkitARM:

00008000<_start>:
8000:2003movsr0, #3
8002:2104movsr1, #4
8004:f000 f802bl800c <add_thumb>
8008:2701movsr7, #1
800a:df00svc0
0000800c<add_thumb>:
800c:1840addsr0, r0, r1
800e:4770bxlr

Repare: as instruções do _start em Thumb ocupam 2 bytes cada (0x2003, 0x2104, 0x2701, 0xdf00), enquanto as do ARM ocupam 4 bytes. A exceção é o BL — que em Thumb-2 é uma instrução de 32 bits (dois halfwords: 0xf000 + 0xf802).

Decodificando instruções Thumb (16 bits)

Instruções Thumb de 16 bits têm a estrutura diferente do ARM. O código da operação vive nos bits mais altos (15..6 ou 15..9), e os registradores nos bits mais baixos. Exemplos reais:

movs r0, #3 = 0x2003

0x2003 = movs r0, #3 — 16 bits
  • O opcode ocupa 5 bits (15..11) — muito menor que os 8 bits do ARM.
  • O registrador destino é 3 bits (r0–r7) — só acessa metade dos registradores (diferente do ARM que tem r0–r15).
  • O imediato é 8 bits (0–255) — não tem rotação como o ARM.

adds r0, r0, r1 = 0x1840

0x1840 = adds r0, r0, r1 — 16 bits
  • O adds (com S implícito em Thumb) atualiza as flags N/Z/C/V — por isso o processador sabe se o resultado é zero, negativo, etc.
  • Os três registradores cabem em 3 bits cada — por isso Thumb-1 só acessa r0–r7 nestas formas.

bx lr = 0x4770

0x4770 = bx lr — 16 bits
  • O Rm está nos bits 6..3 (não nos bits 2..0 como no ARM). r14 = lr = 1110 → bate.
  • O bit 7 distingue BX (0) de BLX (1) — o BLX troca para ARM e salva retorno em LR.

A instrução de 32 bits: BL em Thumb-2

O BL (branch with link) não cabe em 16 bits porque precisa de um offset grande + o bit de link. No Thumb-2, ele ocupa dois halfwords (4 bytes):

f000 f802 = bl 800c <add_thumb> — dois halfwords, 32 bits
  • O offset é codificado nos dois halfwords e, como no ARM, o alvo é (PC + 4) + (offset << 1). No caso: 0x8008 + 4 + (offset << 1) = 0x800c.
  • É por isso que o BL em Thumb-2 tem o mesmo alcance que o BL em ARM (±16 MB) — os 32 bits dão espaço suficiente.

Tamanho: ARM vs Thumb

A mesma função (add_arm vs add_thumb):

Comparação de tamanho
ModoInstruçõesTamanho
ARMadd r0, r0, r1 + bx lr2 × 4 = 8 bytes
Thumbadds r0, r0, r1 + bx lr2 × 2 = 4 bytes

Metade do tamanho. Num programa real com centenas de funções, a economia é significativa — e menos código = menos fetch = menos cache misses.

Glossário: instruções e comandos

Comandos para trabalhar com Thumb
Comando / InstruçãoO que faz
.thumbDiretiva: entra no modo Thumb no assembly.
.thumb_funcMarca o rótulo como função Thumb (bit 0 = 1 no ELF).
bl alvoChama função (Thumb-2: 32 bits; Thumb-1: via trampoléim).
bx regTroca de modo: bit 0 de reg define ARM/Thumb.
-march=armv6k -mthumbMonta em modo Thumb com a toolchain.
arm-none-eabi-objdump -d -j .textMostra os bytes (16/32 bits) para decodificar.

O que o arm-jitter faz com isso

Quando você passa --arch=thumb2 para o arm-box, o arm-jitter ativa o pipeline Thumb-2: ele lê 2 bytes, verifica se é uma instrução de 16 bits ou se precisa de mais 2 bytes (32 bits), e decoda de acordo. O bit T do CPSR é mantido pelo core emulado — e o BX troca o modo exatamente como no hardware. É por isso que o arm-box consegue rodar binários Thumb-2 reais: o decodificador faz a mesma distinção que o processador faria.

Próximo passo

Com Thumb e Thumb-2 no arsenal, o próximo texto pode explorar instruções condicionais em Thumb (onde quase tudo é condicional sem precisar de branch), ou Thumb-2 completo no arm-box (exercitando --arch=thumb2 com binários reais de GCC). Até lá, tente montar uma função sua em Thumb e compare o tamanho com a versão ARM — a diferença vai falar por si.