ARM assembler instruction "ADDS Rn, #1" versus "ADDS Rd, Rn, #1"

Viewed 202
  1. ADDS Rn, #1
  2. ADDS Rd, Rn, #1

The ARM documentation specifies that in the first instruction if the Rd is optional and if ommitted Rd equals Rn. So both instructions should be the same when compiled. But our compiler is generating:

binary 0x3F01 for SUBS R7,#1 and binary 0x1E7F for SUBS R7,R7,#1

using the instruction decoding 0x3F01 equals to 001 11 111 00000001 001

001 -> add/substract/compare/move immediate

11 -> opcode is substract imm

111 -> R7

00000001 -> value 1

using the instruction decoding

0x1E7F equals 000111 1 001 111 111

000111 -> add/substract register

1 -> substract immediate

001 -> value 1

111 -> R7

111 -> R7

So both instructions are performing the same action. Any idea why the the compiler might generate different binary?

3 Answers

The ARM documentation specifies that in the first instruction if the Rd is optional and if ommitted Rd equals Rn. So both instructions should be the same when compiled.

First of all, you should note that there are two different ARM instruction sets: The "traditional" 32-bit instruction set and the "Thumb" instruction set.

Your CPU obviously uses the "Thumb" instruction set.

Many statements about the ARM instruction set refer to the "traditional" one.

In the traditional instruction set, both forms (SUBS R7, #1 and SUBS R7, R7, #1) indeed result in the same instruction: 0xE2577001.

However, in the "Thumb" instruction set, there are two different encodings:

SUBS Rd, Rn, #imm allowing two different registers to be specified for Rd and Rn but only allowing an immediate value in the range 0...7.

SUBS Rd, #imm only allowing the one-register form but allowing an immediate value in the range 0...255.

However, SUBS R7, R7, #1 and SUBS R7, #1 do the same.

Any idea why the the compiler might generate different binary?

If you explicitly type SUBS R7, R7, #1 or SUBS R7, #1 in assembly code?

Because you might also type SUBS R7, R6, #1 or SUBS R7, #90. In these cases, the compiler must handle both variants differently!

So it always handles these two variants differently because it would make more effort to check if both registers are equal or to check if the immediate value is smaller than 8.

I found how the compiler (Greenhills compiler for ARM Cortex-M3) generated the 3F01 and 1E7F binaries.

   { 
        asm(".thumb"); 
0x2008af8  main:            b500   PUSH            {LR}
0x2008afa  main+0x2:        b081   SUB             SP, #4
        asm("SUBS R7, #1"); 
0x2008afc  main+0x4:        3f01   SUBS            R7, #1
        asm("SUBS R7, R7, #1"); 
0x2008afe  main+0x6:        1e7f   SUBS            R7, R7, #1
        asm(".thumb2"); 
        asm("SUBS R7, #1"); 
0x2008b00  main+0x8:        1e7f   SUBS            R7, R7, #1
        asm("SUBS R7, R7, #1");         
0x2008b02  main+0xa:        1e7f   SUBS            R7, R7, #1
    } 

It is an assembler not compiler. Assembly language is specific to the assembler not the target. Whether or not an assembler allows for subs r7,#1 to be functionally equal to subs r7,r7,#1 particularly if there are different encodings for two or three register vs one, is up to the authors of the assembler not the processor IP nor chip vendor.

Let's take gnu assembler for example:

.thumb

sub r7,#1
sub r7,r7,#1
sub r7,r6,#1

sub r7,#1
sub r7,r7,#1
sub r7,r6,#1

.syntax unified

sub r7,#1
sub r7,r7,#1
sub r7,r6,#1

subs r7,#1
subs r7,r7,#1
subs r7,r6,#1

.thumb2

sub r7,#1
sub r7,r7,#1
sub r7,r6,#1

subs r7,#1
subs r7,r7,#1
subs r7,r6,#1

Starting with this version

arm-none-eabi-as --version
GNU assembler (2.34-4ubuntu1+13ubuntu1) 2.34
Copyright (C) 2020 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or later.
This program has absolutely no warranty.
This assembler was configured for a target of `arm-none-eabi'.

arm-none-eabi-as so.s -o so.o
so.s: Assembler messages:
so.s:20: Error: unknown pseudo-op: `.thumb2'

so need to remove the .thumb2 stuff (assembly language is specific to the assembler not the target, what assembler and version are you using?)

.thumb

sub r7,#1
sub r7,r7,#1
sub r7,r6,#1

sub r7,#1
sub r7,r7,#1
sub r7,r6,#1

.syntax unified

sub r7,#1
sub r7,r7,#1
sub r7,r6,#1

subs r7,#1
subs r7,r7,#1
subs r7,r6,#1


Disassembly of section .text:

00000000 <.text>:
   0:   3f01        subs    r7, #1
   2:   3f01        subs    r7, #1
   4:   1e77        subs    r7, r6, #1
   6:   3f01        subs    r7, #1
   8:   3f01        subs    r7, #1
   a:   1e77        subs    r7, r6, #1
   c:   f1a7 0701   sub.w   r7, r7, #1
  10:   f1a7 0701   sub.w   r7, r7, #1
  14:   f1a6 0701   sub.w   r7, r6, #1
  18:   3f01        subs    r7, #1
  1a:   3f01        subs    r7, #1
  1c:   1e77        subs    r7, r6, #1

So it used the 0x3F01 where it could. And the other flavor when it had two different registers.

The tool still supports the pre-unified-syntax state of the world and for whatever reason (assembly language is specific to the tool not the target), they chose to support sub with the suffix. I am surprised actually because different versions of gnu assembler will complain if you try to build a thumb instruction based on adds or subs, etc...and you have to use add and sub instead. In this case it tolerated the subs.

The tail end of readelf shows this, relating to the default target if not specified.

Attribute Section: aeabi
File Attributes
  Tag_CPU_arch: v6T2
  Tag_THUMB_ISA_use: Thumb-2

That looks arm11-ish right? A full sized ARM. Let us try it with an armv6-m

.cpu cortex-m0
.thumb

sub r7,#1
sub r7,r7,#1
sub r7,r6,#1

sub r7,#1
sub r7,r7,#1
sub r7,r6,#1

.syntax unified

sub r7,#1
sub r7,r7,#1
sub r7,r6,#1

subs r7,#1
subs r7,r7,#1
subs r7,r6,#1

Not happy

so.s: Assembler messages:
so.s:16: Error: lo register required -- `sub r7,#1'
so.s:17: Error: lo register required -- `sub r7,r7,#1'
so.s:18: Error: lo register required -- `sub r7,r6,#1'

.cpu cortex-m0
.thumb

sub r7,#1
sub r7,r7,#1
sub r7,r6,#1

sub r7,#1
sub r7,r7,#1
sub r7,r6,#1

.syntax unified

subw r7,#1
subw r7,r7,#1
subw r7,r6,#1

subs r7,#1
subs r7,r7,#1
subs r7,r6,#1

so.s: Assembler messages:
so.s:16: Error: selected processor does not support `subw r7,#1' in Thumb mode
so.s:17: Error: selected processor does not support `subw r7,r7,#1' in Thumb mode
so.s:18: Error: selected processor does not support `subw r7,r6,#1' in Thumb mode

And that makes it more clear, armv6-m does not support that the T1 encoding according to the armv7-m architectural reference manual shows the 0x1Exx encoding you are talking about with two registers and T2 in that document is the 0x38xx one. The thumb2 encodings are armv7-m not armv6-m (armv6-m added a few dozen to the "all thumb variants". armv7-m added about 150 thumb2 extensions, very different experience.

.cpu cortex-m7
.thumb

sub r7,#1
sub r7,r7,#1
sub r7,r6,#1

sub r7,#1
sub r7,r7,#1
sub r7,r6,#1

.syntax unified

sub r7,#1
sub r7,r7,#1
sub r7,r6,#1

subs r7,#1
subs r7,r7,#1
subs r7,r6,#1

Which then generates:

Disassembly of section .text:

00000000 <.text>:
   0:   3f01        subs    r7, #1
   2:   3f01        subs    r7, #1
   4:   1e77        subs    r7, r6, #1
   6:   3f01        subs    r7, #1
   8:   3f01        subs    r7, #1
   a:   1e77        subs    r7, r6, #1
   c:   f1a7 0701   sub.w   r7, r7, #1
  10:   f1a7 0701   sub.w   r7, r7, #1
  14:   f1a6 0701   sub.w   r7, r6, #1
  18:   3f01        subs    r7, #1
  1a:   3f01        subs    r7, #1
  1c:   1e77        subs    r7, r6, #1

File Attributes
  Tag_CPU_name: "Cortex-M7"
  Tag_CPU_arch: v7E-M
  Tag_CPU_arch_profile: Microcontroller
  Tag_THUMB_ISA_use: Thumb-2

So again using the 0x3f01 encoding where it can.

Assembly language is specific to the tool not the target. The assembly language author(s) are free to choose between equivalent instructions. One author back in the day went so far as to claim that the encoding pattern choices were such that he could determine if you had used his assembler (it was shareware and if you did not pay for it....I can tell...)

Operation

if ConditionPassed() then
  EncodingSpecificOperations();
  (result, carry, overflow) = AddWithCarry(R[n], NOT(imm32), '1');
  R[d] = result;
  if setflags then
    APSR.N = result<31>;
    APSR.Z = IsZeroBit(result);
    APSR.C = carry;
    APSR.V = overflow;

At least according to the armv7-m architectural reference manual that I have handy, functionally these are all the same be it the two all thumb variant versions with one or two registers and different immediate rules as well as the thumb2 extensions.

Assembly language is specific to the tool not the target. The tool authors are free to define the assembly language which includes the freedom to choose equivalent encodings even if it does not match the syntax the user provided (the syntax that the tool supports is not necessarily tied to the IP vendors documentation, it is the authors choice, completely. pickle orange,apple,#1 is perfectly valid syntax, you will not have many users but there is nothing wrong with it so long as it produces proper instructions)(The IP vendors documentation is somewhere between their own internal tools assembly language (defined by the tool not target) or a generic one not supported by any assembler (this happens)).

.thumb, .thumb2, etc are all part of the particular tools language, what those and other directives do to the tool are specific to the tool and not generic to all ARM code, certainly not all ARM code for all the various ARM instruction sets and versions. (understand there are several different versions of the ARM thumb instruction set, it is rarely referenced, but when you see it, basically every next architecture full size or mcu market, got a new version).

Related