Skip to content

Subtract

Level: 201 · for anyone with a hex editor open

One line: X[i] -= Operand wraps below zero the way Add wraps past the top, so zero minus one is every bit set in every type, Subtract 1 writes exactly what Add 0xFF writes, and Subtract 0x20 uppercases ASCII letters while turning a space into NUL.

What the dialog does

The manual writes Subtract as X[i] -= Operand (Hex Operations). Everything Add says about widths carries over in the other direction: a borrow travels up through the bytes of one value and stops at its edge, so the same Subtract on the same two bytes writes three different results as a Byte, a little-endian Short and a big-endian Short (section 1).

In Python

Verified output of subtract_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. THE SAME BYTES, THREE DIFFERENCES
------------------------------------------------------------------------
   Subtract 0x10 from 08 01:

     Unsigned Byte            f8 f1   each byte borrows from nowhere
     Unsigned Short, little   f8 00   0108-10 is 00F8
     Unsigned Short, big      07 f1   0801-10 is 07F1

2. ZERO MINUS ONE IS EVERY BIT SET, IN EVERY TYPE
------------------------------------------------------------------------
   Subtract 1 from zero bytes, then read the result back as the same
   type:

     Unsigned Byte    ff                        255
     Signed Byte      ff                        -1
     Unsigned Short   ff ff                     65535
     Signed Int       ff ff ff ff               -1
     Unsigned Int64   ff ff ff ff ff ff ff ff   18446744073709551615

   The bytes are the same in each signed and unsigned pair. The
   number is not, and the file only ever holds the bytes.

3. SUBTRACT 1 IS ADD FF
------------------------------------------------------------------------
   Wrapping makes subtraction a special case of addition, which is why
   a dialog could offer only one of them:

     Subtract 1 == Add 0xFF,   all 256 Unsigned Bytes        True
     Subtract 1 == Add 0xFFFF, all 65,536 Unsigned Shorts    True

4. SUBTRACT 20 ON A LINE OF TEXT
------------------------------------------------------------------------
   Lowercase ASCII sits exactly 0x20 above uppercase, so Subtract 0x20
   as Unsigned Byte uppercases letters -- and does the same arithmetic
   to every other byte:

     before  63 61 66 c3 a9 20 34 32   'café 42'
     after   43 41 46 a3 89 00 14 12   'CAF��\x00\x14\x12'

   The letters came out right. The space became NUL, the digits became
   control characters, and C3 A9 became A3 89 -- a continuation byte
   where a lead byte has to be, so the é is two replacement characters.

Zero minus one

Section 2 subtracts 1 from zero in five types and gets all ones every time — FF, FF FF, four FFs, eight. Only the reading differs: 255 or −1 for the same byte, 65,535 for the Short, 18,446,744,073,709,551,615 for the Unsigned Int64. The file never holds a reading, only bytes, which is why an inspector pane beside a hex view shows signed and unsigned columns for the same bytes rather than choosing between them.

Subtract is Add, wrapped

Section 3 checks Subtract 1 against Add 0xFF on every Unsigned Byte, and Subtract 1 against Add 0xFFFF on every Unsigned Short, and gets the same bytes all 256 and all 65,536 times. Once values wrap, subtracting k and adding 2ⁿ − k are one operation. Negate is the same idea applied to zero.

Subtract 20 on text

ASCII puts every lowercase letter exactly 0x20 above its capital, so Subtract 0x20 as Unsigned Byte is an uppercase, for letters. Section 4 runs it on café 42 and shows the rest of the bill: the space, 20, became NUL; the digits 34 32 became the control characters 14 12; and the é, C3 A9, became A3 89, a continuation byte standing where UTF-8 needs a lead byte, so a decoder reports two errors. Arithmetic does not know which bytes are letters. Binary And and Binary Or change case by clearing or setting a single bit instead, and leave far more of a line alone.

What the manual does not say

  • What a result below the bottom of a signed type becomes. The programs keep the low bits.
  • What becomes of a last value too short for the type.

If you are coming from Python or ABAP

Python. (b - k) & 0xFF is the whole of Subtract as Unsigned Byte, and the mask is not optional: Python's −1 has no width, and bytes([-1]) raises ValueError rather than writing FF. To read a result as signed, struct.unpack('b', ...) or int.from_bytes(..., signed=True)Bytes, hex and int has the four conversions.

ABAP. (Not machine-checked — CI cannot run ABAP.) An integer subtraction whose result leaves the type raises CX_SY_ARITHMETIC_OVERFLOW, and i is a signed type, so 0 - 1 is −1 and never 4,294,967,295. The all-ones bytes come from the assignment instead: −1 assigned to a TYPE x LENGTH 4 field writes the integer's four bytes in big-endian order ↗, which for −1 is FF FF FF FF in either order.

Try it

  1. Select four zero bytes and Subtract 1 as Unsigned Byte, then as Signed Short, then as Unsigned Int, reading the Inspector after each. The bytes agree every time and the numbers do not.
  2. Take a text file of mixed case and Subtract 0x20 as Unsigned Byte. Search the result for bytes below 0x20 and count how many used to be spaces and digits.
  3. Subtract 0 with Operand Step 1 over sixteen zero bytes. Write down the last byte before you look.
  4. Subtract 0x10 from 08 01 as Unsigned Short under each Endian setting, and explain each result from the value the dialog read.

See also

  • Add — the same wrap, from above
  • Negate — zero minus the value, and the two values it leaves alone
  • Binary Or — changing case by a bit instead of by arithmetic
  • Bytes, hex and int — reading the same bytes signed and unsigned in Python