Every value wider than a single byte (a 16, 32, or 64-bit number, a memory address, etc) has to be split into individual bytes to live in memory, because memory is addressed one byte at a time.
Being used to working with large decimal numbers (e.g., 1337) in real life, with the "least significant" digit on the right and the "most significant" digit on the left, you might expect something similar in the CPU.
For example, if you were storing the 16-bit (2-byte) value 0x1234, you might expect it to be stored as two consecutive bytes, first 12 and then 34.
Some CPUs do work like this, but most do not.
Most architectures store the least significant digit on the left and the most significant on the right.
In these architectures, the value 0x1234 would be stored in two consecutive bytes as 34 12.
Because the "little" (least significant) end goes first, these are called "Little Endian" (LE) architectures, and represent essentially all modern CPU architectures.
Of course, though this seems extremely silly to anyone that encounters it for the first time, there are number of solid reasons behind it:
- A lot of arithmetic is done from the little end. Consider long addition: you start from the small values and carry the 10 to the left. An Arithmetic Logical Unit does the same thing, and Little Endian is natural here.
- A value's address is the address of its low byte, so reading a 4-byte
intas a 1-bytechar(or a 2-byteshort) is the same address --- you just read fewer bytes. In Big Endian systems, this requires fixing up the address, which is complicated.
Hopefully, you're convinced. Now, get familiar with some more examples:
| size of value | decimal value | hex value | big endian bytes (NOT x86) | little endian bytes (x86) |
|---|---|---|---|---|
| 8 (1 byte) | 65 |
0x41 |
41 |
41 |
| 16 (2 bytes) | 4660 |
0x1234 |
12 34 |
34 12 |
| 32 (4 bytes) | 1145258561 |
0x44434241 |
44 43 42 41 |
41 42 43 44 |
| 64 (8 bytes) | 1145258561 |
0x0000000044434241 |
00 00 00 00 44 43 42 41 |
41 42 43 44 00 00 00 00 |
A single byte is identical in both columns: with only one byte there's no order to pick, so endianness only matters once a value is two bytes or wider. The 32- and 64-bit rows hold the same number --- the 64-bit version just pads with zero bytes, which, being the most-significant bytes, sit at the higher addresses.
Endianness is very much a CPU-level concept.
When you move beyond it (e.g., when sending data over the network), big-endian encoding of numbers rears its head.
And even most of the time when working in assembly, you don't really have to think about endianness.
For example, you've already stored and loaded multi-byte values without reversing byte order, because a value (say mov rax, 0x1234; push rax) written to memory and read straight back (e.g., pop rax) comes out unchanged: it's written in little-endian order on the stack by push and endian-corrected when it's read back into the register by pop.
Endianness only matters in memory, but the moment you look at memory as bytes (in a hex dump, in a debugger, in an exploit) the byte order is right there, and you have to read it the way the CPU wrote it.