segments
legacy
Real mode
The central processing unit(CPU) always starts in real mode, and then the main loader usually executes the code to explicitly switch it to protected mode and then to the long mode.
These are the registers usable in real mode:
- ip, flags;
- ax, bx, cx, dx, sp, bp, si, di;
- Segment registers: cs, ds, ss, es, (later also gs and fs).
As it was not straightforward to address more than 64 Kilobytes of memory, engineers came up with a solution to use special segement registers in the following way:
Each physical address consists of 20 bits(so, 5 hexadecimal digits).
Each logical address consists of two components. One is taken from a segment register and encodes the segment start. The other is an offset inside this segment. The hardware calculates the physical address from these components the following way:
physical address = segment base * 16 + offset
You can often see addresses written in form of segment:offset, for example:
1
4a40:0002, ds:0001, 7bd3:ah
Note that strictly speaking, the segment register do not hold segments' starting addresses but rather their parts(the four most significant hexadecimal digits). By adding another zero digit to multiply it by
we get the real segment starting address. Each instruction referencing memory implicitly assumes usage of one of segment registers. Documentation clarifies the default segment registers for each instruction. However, common sense can help as well. For instance, mov is used to manipulate data, so the address is relative to the data segment.
1
mov al, [0004] ; === mov al, ds:04444
It is possible to redefine the segment explicitly:
1
mov al, cs:[0004]
When the program is loaded, the loader set ip, cs, ss, and sp register to that cs:ip corresponds to the entry point, and ss:sp points on top of the stack.
Real mode has numerous drawbacks
- It makes multitasking very hard. The same address space is shared between all programs, so they should be loaded at different addresses. Their relative placement should usually be decided during compilation. :joy: But maybe we can distributed these tasks by hand.
- Programs can rewrite each other's code or even operating system as they all live in the same address space. :dog: What about only one user?
- Any program can execute any instruction, including those used to set up the processor's state. Some instructions should only be used by the operating system(like those used to set up virtual memory, perform power management, etc.) as their incorrect usage can crash the whole system. :laughing: We do not have operating system!
Protected Mode
Intel 80386 was the first processor implementing protected 32-bit mode.
It provides wider versions of registers(eax, ebx, ..., esi, edi) as well as new protection mechanisms: protection rings, virtural memory, and an improved segmentation.
Obtaining a segment starting address has changed.
Linear address = segment base(taken from system table) + offset
Each of segment registers cs, ds, ss, es, gs, and fs stores so-called segment selector, containing an index in a special segment descriptor table and a little additional information.
Two types of segment descriptor tables:
- LDT(Local Descriptor Table)
- GDT(Global Descriptor Table)
Index denotes descriptor position in either GDT or LDT. The T bit select either LDT or GDT. As LDTs are no longer used, it will be zero in all cases.
The table entries in GDT/LDT also store information about which privilege level is assigned to the described segment. When a segment is accessed through segement selector, a check of Request Privilege Level(RPL) value(stored in selector = segment register) against Descriptor Privilege Level(stored in descriptor table) is performed. If RPL is not privileged enough to access a high privileged segment, an error will occur. This way we could create numerous segments with various permissions and use RPL values in segment selectors to define which of them are accessible to us right now(given our privilege level).
G-Granularity, e.g., size is in 0=bytes, 1=pages of size 4096 bytes each.
D-Default operand size(0=16 bit, 1=32 bit).
L-Is it a 64-bit mode segment?
V-Available for use by system software.
P-Present in memory right now.
S-Is it data/code (1) or is it just some system information holder (0).
X-Data (0) or code (1).
RW-For data segment, is writing allowed? (reading is always allowed); for code segment, is reading allowed? (writing is always prohibited).
DC-Growth direction: to lower or to higher addresses? (for data segment); can it be executed from higher privilege levels? (if code segment)
A-Was it accessed?
DPL-Descriptor Privilege Level (to which ring is it attached?)
Enabling Protected Mode loader_start32.asm
1 | lgdt cs:[_gdtr] |
Accessing Parts of Registers
1 | mov rax, 0x1122334455667788 ; rax = 0x1122334455667788 |
As you see, writing in 8-bit or 16-bit parts leaves the rest of bits intact. Writing to 32-bit parts, however, fill the upper half of a wide register with sign bit!
Explanation
Let's think about instruction decoding. The part of a CPU called instruction decoder is constantly translating commands from an older CISC system to a more convenient RISC one. Pipelines allow for a simultaneous execution of up to six smaller instructions. To achieve that, however, the notion of registers should be virtualized. During microcode execution, the decoder choose an available register from a large bank of physical registers. As soon as the bigger instruction ends, the effects become visible to programmer: the value of some physical registers may be copied to those, currently assigned to be, let's say, rax.
The data interdependencies between instructions stall the pipeline, decreasing performance. The worst cases occur when the same register is read and modified by several consecutive instructions(think about rflags!).
If modifying eax means keeping upper bits of rax intact, it introduces an additional dependency between current instruction and whatever instruction modified rax or its parts before. By discarding upper 32 bits on each write to eax we eliminate this dependency, because we do not care anymore about previous rax value or its parts.
This kind of a new behavior was introduced with the latest general purpose registers' growth to 64 bits and does not affect operations with their smaller parts for the sake of compatibility. Otherwise, most older binaries would have stopped working because assigning to, for example, bl, would have modified the entire ebx, which was not true back when 64-bit registers had not yet been introduced.