Negative Numbers, Overflow, and What Hex Reveals About Your Data
Most developers can convert hex to decimal β but two's complement, signed overflow, and bit-width boundaries are where base conversion becomes genuinely useful for debugging real problems.
By sadiqbd Β· June 7, 2026
The number -1 in memory looks like 0xFFFFFFFF β and that's not an accident
Most developers learn to convert between decimal, binary, and hex without too much trouble. Sixteen is 0x10. Two hundred and fifty-five is 0xFF. A byte holds values from 0 to 255. Fine.
But then something weird happens. A C function returns -1 to indicate an error. Someone checks its value in a debugger and sees 0xFFFFFFFF. A network packet contains 0xFF 0xFF and the parsed value comes out as -1 in one language and 65535 in another. A JavaScript bitwise operation on a large number suddenly produces a negative result nobody expected.
These aren't bugs. They're the predictable consequence of how computers represent signed numbers β and understanding two's complement turns confusing outputs into readable information.
Signed vs. Unsigned: The Same Bits, Two Interpretations
A 16-bit pattern like 1111 1111 1111 1111 (0xFFFF) means different things depending on how you're reading it:
- Unsigned 16-bit: 65,535 (the maximum value β all bits set)
- Signed 16-bit: β1 (two's complement representation)
The bits are identical. The interpretation is what differs. This is why the same hex value produces different results depending on the data type β and why converting between bases is only half the job. You also need to know the signedness and bit width of what you're reading.
Two's Complement: How Signed Negatives Actually Work
Two's complement is the near-universal method for representing negative integers in binary. The rule: to negate a number, flip all its bits and add 1.
Negating 1 (8-bit):
0000 0001 (1 in binary)
1111 1110 (flip all bits)
+ 0000 0001 (add 1)
ββββββββββββββ
1111 1111 = 0xFF = -1
Negating 42 (8-bit):
0010 1010 (42)
1101 0101 (flip)
+ 0000 0001 (add 1)
ββββββββββββββ
1101 0110 = 0xD6 = -42
Verify: 0xD6 as an 8-bit signed value. The top bit is 1, so it's negative. To find the magnitude, apply two's complement again: flip 1101 0110 β 0010 1001, add 1 β 0010 1010 = 42. So 0xD6 = -42. β
The elegant property of two's complement: the hardware doesn't need separate circuits for addition and subtraction. 42 + (-42) is the same as 42 + 0xD6 in binary arithmetic, and the result (with overflow discarded) is correctly 0.
The Signed Range for Common Bit Widths
Two's complement reserves the top bit as the sign bit, so the range of representable values is asymmetric:
| Type | Bits | Minimum | Maximum | Hex max positive |
|---|---|---|---|---|
| int8 / signed char | 8 | β128 | 127 | 0x7F |
| uint8 / unsigned char | 8 | 0 | 255 | 0xFF |
| int16 / short | 16 | β32,768 | 32,767 | 0x7FFF |
| uint16 / unsigned short | 16 | 0 | 65,535 | 0xFFFF |
| int32 / int | 32 | β2,147,483,648 | 2,147,483,647 | 0x7FFFFFFF |
| uint32 | 32 | 0 | 4,294,967,295 | 0xFFFFFFFF |
| int64 / long | 64 | β9.2 Γ 10ΒΉβΈ | 9.2 Γ 10ΒΉβΈ | 0x7FFFFFFFFFFFFFFF |
The pattern: 0x7F...F is always the maximum positive signed value. 0x80...0 is always the most negative. 0xFF...F is always β1.
What This Looks Like in Practice
The -1 return value
Many C APIs return -1 on failure. In hex:
- 32-bit:
0xFFFFFFFF - 64-bit:
0xFFFFFFFFFFFFFFFF
When you see these in a debugger or memory dump, you're looking at β1. The base converter makes this explicit: entering 0xFFFFFFFF as a signed 32-bit integer returns β1, not 4,294,967,295.
JavaScript's 32-bit integer behaviour
JavaScript's bitwise operators coerce their operands to signed 32-bit integers before operating. This catches developers off guard with large numbers:
2147483647 | 0 // 2147483647 β fine, fits in int32
2147483648 | 0 // -2147483648 β crossed the int32 boundary (0x80000000)
4294967295 | 0 // -1 β 0xFFFFFFFF interpreted as signed int32
The number 4294967295 is 0xFFFFFFFF. It fits in a uint32 but not in a signed int32. When JavaScript's bitwise coercion forces it into a 32-bit signed integer, the result is β1.
Understanding two's complement explains exactly why this happens β and the base converter lets you verify in real time.
Network protocol fields
Network protocols frequently use fixed-width unsigned integers. A 16-bit field with value 0xFF00 is 65,280 unsigned. If your parsing code accidentally reads it into a signed 16-bit integer, you get β256 instead. This is a common source of bugs when parsing binary protocols across languages with different default integer signedness.
Status codes and flags
HTTP status code 200: 0xC8. No sign issues there. But consider a 32-bit status field where bit 31 signals error: 0x80000004. As unsigned: 2,147,483,652. As signed: β2,147,483,644. As binary: 1000 0000 0000 0000 0000 0000 0000 0100 β bit 31 set (error flag), bits 0β3 carry a code value of 4.
Reading this without understanding signed interpretation produces confusing large numbers. With it, the structure is immediately visible.
Overflow: When Arithmetic Wraps Around
Integer overflow happens when the result of an operation exceeds the maximum (or minimum) representable value for that type. The result wraps around.
Signed 8-bit overflow:
127 + 1 = 128
But 128 in 8-bit signed is 0x80 = -128
So: 127 + 1 = -128 in int8
Unsigned 8-bit overflow:
255 + 1 = 256
But 256 in 8-bit unsigned is 0x100 β that's 9 bits, truncated to 0x00
So: 255 + 1 = 0 in uint8
In languages like C and C++, signed integer overflow is undefined behaviour β compilers can (and do) optimise around it in ways that produce surprising results. In Rust, overflow panics in debug mode and wraps in release mode (explicitly). In Python, integers are arbitrary precision and never overflow.
The base converter helps verify the overflow boundary: what's the maximum value for your type? What does adding 1 to it produce?
Endianness: Another Layer of Interpretation
Beyond signedness, multi-byte integers have an endianness β the order in which bytes are stored in memory.
Big-endian (network byte order): most significant byte first.
Value 0x0A0B: stored as 0A 0B.
Little-endian (x86 native): least significant byte first.
Value 0x0A0B: stored as 0B 0A.
A packet captured on the network contains 0A 0B. Interpreted as big-endian uint16: 0x0A0B = 2,571. Interpreted as little-endian uint16: 0x0B0A = 2,826. Different value, same bytes.
Network protocols typically use big-endian (network byte order). x86/x64 processors store data in little-endian. Most language standard libraries handle this conversion with functions like htons() (C), socket.ntohs() (Python), or Buffer.readUInt16BE() / readUInt16LE() (Node.js).
When debugging protocol parsing, the base converter helps verify: are the bytes in the right order for your interpretation?
Reading Hex Values Without Converting
Once the patterns are familiar, certain hex values become immediately readable without arithmetic:
| Hex | What it almost certainly means |
|---|---|
0xFF...F |
β1 (signed) or max unsigned |
0x80...0 |
Minimum signed value / sign bit set |
0x7F...F |
Maximum signed value |
0x00...0 |
Zero |
0xDEADBEEF |
Debug sentinel / uninitialised memory marker |
0xCAFEBABE |
Java .class file magic number |
0xFEEDFACE |
macOS Mach-O binary magic number |
0x0D0A |
CRLF (\r\n) in ASCII β HTTP line endings |
0x504B0304 |
ZIP file magic number (PK..) |
Magic numbers and file signatures are always expressed in hex. Recognising them without conversion is a skill that comes from regular exposure β the base converter is the shortcut while that familiarity builds.
Using the Number Base Converter on sadiqbd.com
Beyond the basic decimal β hex β binary conversion, the converter is most useful for:
- Checking overflow boundaries β what's the max value for your integer type?
- Verifying two's complement β enter a negative decimal, see its hex equivalent
- Decoding unknown hex values β is
0xFFFFβ1 or 65,535? Depends on signedness - Confirming bit patterns β enter a hex value and inspect the binary layout
- Understanding bitmasks β which bits are set in a given value?
Enter the hex value, read all four representations simultaneously. The binary column is particularly useful for bitmask analysis β it's instantly visible which bits are set.
Tips for Working With Signed and Unsigned Values
Always know the type. A hex value without context is ambiguous. 0xFFFF could be 65,535 or β1. Before interpreting any hex value, confirm the bit width and signedness of the field or variable you're reading.
In C, prefer explicit types. int sizes are platform-dependent. Use int32_t, uint16_t etc. from <stdint.h> for portable, unambiguous code.
When debugging JavaScript bit operations, check against int32 max. If your number exceeds 2,147,483,647 (0x7FFFFFFF) and you're using bitwise operators, you'll get sign-confused results. Use >>> 0 (unsigned right shift by 0) to coerce to unsigned int32 when needed.
Remember that Python integers are arbitrary precision. ~1 in Python is β2, not 254 β because Python integers aren't bounded to 8 bits. The two's complement wrapping doesn't apply the same way.
Frequently Asked Questions
Why is 0xFFFFFFFF equal to -1 in signed 32-bit integers?
Two's complement: all 32 bits set. To decode: it's a negative number (top bit = 1). To find magnitude, flip all bits (gives 0x00000000 = 0), add 1 (gives 1). So it's β1. Additionally, 0xFFFFFFFF + 0x00000001 = 0x100000000 β but the 33rd bit is truncated in 32-bit arithmetic, leaving 0. That's exactly what you want: -1 + 1 = 0.
What's the difference between arithmetic and logical right shift?
Arithmetic right shift (>> in most languages for signed types) fills the top bits with the sign bit β preserving the sign. Logical right shift (>>> in JavaScript) fills with 0 β treating the number as unsigned. Shifting 0x80000000 right by 1: arithmetic gives 0xC0000000 (still negative); logical gives 0x40000000 (positive).
Why do compilers sometimes behave unexpectedly around integer overflow? Signed integer overflow is undefined behaviour in C/C++. Compilers can legally assume it never happens β and will optimise loops and conditionals accordingly. This produces binaries that "work" for normal inputs but behave incorrectly near overflow boundaries. Always use unsigned arithmetic or checked arithmetic functions where overflow is a realistic concern.
Does the base converter handle negative numbers? Yes β enter a negative decimal (like β1) and it shows the two's complement hex for common bit widths. Enter a hex value and choose the signed interpretation to see the corresponding negative decimal.
Is the Number Base Converter free? Yes β completely free, no sign-up required.
Hex values are only confusing when you don't know the signedness and bit width behind them. Once that context is clear, 0xFFFFFFFF isn't a mystery β it's predictably β1, every time, in every language that uses two's complement signed 32-bit integers (which is most of them).
Try the Number Base Converter free at sadiqbd.com β convert between binary, octal, decimal, and hex, and explore the bit patterns that make signed arithmetic make sense.