.text:004036A0; =============== S U B R O U T I N E ===============
.text:004036A0
.text:004036A0

Hex, binary and two's complement

.text:004036A0; author: Can Ozdemir date: 2024-02-11 size: 0xB60 tags: basics, tutorial
.text:004036A0
.text:004036A0sub_4036A0procnear
.text:004036A1

Reverse engineering means reading numbers all day. Before anything else it's worth getting fast at three things: converting between hex and binary, knowing the sizes of the common integer types, and reading negative numbers in two's complement.

.text:00403795

Hex is shorthand for binary

.text:004037B3

One hex digit is exactly four bits, so conversion is a lookup, not arithmetic:

.text:00403801
0 0000   4 0100   8 1000   C 1100
1 0001   5 0101   9 1001   D 1101
2 0010   6 0110   A 1010   E 1110
3 0011   7 0111   B 1011   F 1111
.text:00403888

0xB6 is 1011 0110. 0x80 is 1000 0000: only the top bit of a byte. That's why hex is everywhere in tools: two digits per byte, and the bit pattern is visible at a glance.

.text:00403939

Worth memorising: powers of two up to 0x10000 (65536), and that 0x100 = 256, 0x1000 = 4096 (a memory page on x86), 0x10000 = 64 KiB.

.text:004039C5

Sizes

.text:004039CD

On x86-64 Linux:

.text:004039DD
  • char: 1 byte (8 bits)
  • short: 2 bytes
  • int: 4 bytes
  • long, pointers: 8 bytes (on 64-bit Windows long is still 4)
.text:00403A5D

Assembly names them by size: byte, word (2), dword (4), qword (8). That's where DWORD PTR [rbp-0x4] comes from.

.text:00403ADE

Unsigned is easy

.text:00403AF1

An 8-bit unsigned value covers 0x00 to 0xFF, 0 to 255. A 32-bit one covers 0 to 0xFFFFFFFF = 4,294,967,295.

.text:00403B62

Signed: two's complement

.text:00403B7D

CPUs store negative numbers in two's complement. The rule for an n-bit number: the top bit counts as minus 2^(n−1) instead of plus.

.text:00403C0A

For a byte, the top bit is worth −128:

.text:00403C32
  • 0x7F = 0111 1111 = 127, the largest positive byte
  • 0x80 = 1000 0000 = −128
  • 0xFF = 1111 1111 = −128 + 127 = −1
.text:00403CB7

To negate a value by hand: flip every bit and add 1. 5 is 0000 0101; flipped it's 1111 1010; plus 1 is 1111 1011 = 0xFB = −5.

.text:00403D3E

The same bits mean different things depending on how the code uses them. 0xFFFFFFFF is 4,294,967,295 as an unsigned int and −1 as a signed int. The bytes don't say which; the instructions do. That's why you'll learn to look at the comparison: jl/jg are signed, jb/ja are unsigned.

.text:00403E66

Sign extension

.text:00403E77

Widening a signed value copies its top bit into the new bits, so the value stays the same: the byte 0xFB (−5) becomes the dword 0xFFFFFFFB, still −5. Widening an unsigned value fills with zeros instead.

.text:00403F49

You'll see both in disassembly:

.text:00403F68
movsx  eax, BYTE PTR [rdi]    ; signed char -> int   (sign-extend)
movzx  eax, BYTE PTR [rdi]    ; unsigned char -> int (zero-extend)
cdqe                          ; eax -> rax, sign-extended
.text:00404027

Spotting movsx vs movzx is one of the cheapest ways to recover whether a variable in the original source was signed.

.text:0040409F

Practice

.text:004040AA

Convert without a calculator, then check with python3 -c 'print(hex(...))':

.text:004040F7
  1. 0xC8 as an unsigned byte and as a signed byte
  2. −2 as a 16-bit value in hex
  3. 0xFFFFFF9C as a signed 32-bit int
.text:00404172

Answers: 200 and −56; 0xFFFE; −100.

.text:0040419B
.text:0040419Bretn
.text:0040419Bsub_4036A0endp
.text:0040419B; © 2026 Can Ozdemir. All rights reserved. terms
.text:0040419B
.text:0040419C; -------- cross references --------
Output
Loading ozdc.us… 34 functions, 3 data segments.
Type help in the command line below and press Enter.