Keyboard Shortcuts

Ctrl+K
Open search
Esc
Close search / dialog
↑
Back to top

T0SZ = 64 - 48, and the Field With a Floor

25 minadvancedModule 2: The Memory: Regimes, Descriptors, and What Was MeasuredContinues from a64-interrupts
25 min · The AArch64 Machine: Modes, Memory and Faults

Why this matters

Concept 2 told you how to read a fault. This concept is about the thing that made the fault: the translation regime, which is the pair of questions "where do I look" and "how much of the address is a position in it." The second question is the interesting one, and on AArch64 it is answered by a five-bit field in a register — which means the answer is not a number the architecture chose but a number the platform chose, and the field has a floor.

So this concept is two subjects that look unrelated and are one. The first is the split: two translation base registers, two regions, and a field each that says how big its region is. The second is the reach: a fixed 32-bit instruction with a 21-bit field can address a page four gibibytes away or a byte one mebibyte away, and every page table in a system is on the first side of that line while every array index is on the second. The second subject is what makes the first one expensive, and concept 5 is where the bill arrives.

THE METHOD ON THIS PAGE, stated first There is no AArch64 machine, emulator or linker on the host that wrote this course. QUOTED the field positions (T0SZ at [63:48], T1SZ at [47:32], TG0 at [15:14]) and the region formula 2^(64 - TnSZ). MEASURED the identity TnSZ = 64 - (bits of VA), checked in seven configurations, and the ADR boundary, found by asking the assembler to refuse a distance. NOT the ADRP boundary, and the page says so MEASURED where a reader would look for it: the assembler does not check ADRP at all and the check belongs to the linker, which does not exist here.

The model: two registers, two regions, one formula

Three registers, and the encodings name them with nine bits. The first measured result on the page is that they are all CRn = 2 and are told apart by a two-bit op2 field:

$ python3 a64sys.py --section 7 | sed -n '/three registers/,/^$/p' | head -10
  [MEASURED-ON-BYTES] three registers, and the encodings that name them
     mrs x0, TTBR0_EL1  0xd5382000  CRn=0x2 CRm=0x0 op2=0  op1=0  mrs      x0, TTBR0_EL1
     mrs x0, TTBR1_EL1  0xd5382020  CRn=0x2 CRm=0x0 op2=1  op1=0  mrs      x0, TTBR1_EL1
     mrs x0, TCR_EL1    0xd5382040  CRn=0x2 CRm=0x0 op2=2  op1=0  mrs      x0, TCR_EL1
     mrs x0, SCTLR_EL1  0xd5381000  CRn=0x1 CRm=0x0 op2=0  op1=0  mrs      x0, SCTLR_EL1
     msr TTBR0_EL1, x0  0xd5182000  CRn=0x2 CRm=0x0 op2=0  op1=0  msr      TTBR0_EL1, x0
     mrs x0, TCR_EL2    0xd53c2040  CRn=0x2 CRm=0x0 op2=2  op1=4  mrs      x0, TCR_EL2
                

Three registers whose addresses a kernel sets once at boot, whose names every reader will look up, and whose encodings are adjacent numbers 0, 1, 2 in a two-bit field. Read that as a gift to a decoder and a trap for a page: there is nothing about the relationship between the three registers in the encoding. The relationship — TTBR0 and TTBR1 describe two halves of one address space — is architecture, and it is quoted.

Now the formula, and it is the entire measured content of the concept:

     T0SZ is bits[63:48] of TCR_EL1 and T1SZ is bits[47:32]
     (QUOTED), and the region TTBRn addresses is 2^(64-TnSZ)
     bytes.  Every row below is that identity and nothing else.

     configuration   VA bits   T0SZ  T1SZ   TTBR0 region                 TTBR1 region
     48-bit, no LPA2  48       16    16    2^48 = 0x1000000000000  2^48 = 0x1000000000000
     48/52, with LPA2 48       16    12    2^48 = 0x1000000000000  2^52 = 0x10000000000000
     39/39, the 3-level case 39       25    25    2^39 = 0x008000000000  2^39 = 0x008000000000
     39/48            39       25    16    2^39 = 0x008000000000  2^48 = 0x1000000000000
     42/48            42       22    16    2^42 = 0x040000000000  2^48 = 0x1000000000000
     52/52            52       12    12    2^52 = 0x10000000000000  2^52 = 0x10000000000000
     33/33, the SMALLEST 33       31    31    2^33 = 0x000200000000  2^33 = 0x000200000000

  [RESULT]           T0SZ = 64 - 48 = 16, and T1SZ = 64 - 52 = 12 with LPA2,
                     64 - 48 = 16 without
                

The section plan gave three of those numbers — T0SZ = 16, T1SZ = 16 without LPA2, T1SZ = 12 with it — and every one is right. What the plan did not have is the last row, and the last row is the concept.

The field has a floor, and the floor killed a row

     and the field is FIVE bits, so the SIZE of a regime is
     bounded below as well as above:
       64-bit VA  ->  T0SZ = 0    region 2^64 bytes, FITS
       60-bit VA  ->  T0SZ = 4    region 2^60 bytes, FITS
       52-bit VA  ->  T0SZ = 12   region 2^52 bytes, FITS
       48-bit VA  ->  T0SZ = 16   region 2^48 bytes, FITS
       44-bit VA  ->  T0SZ = 20   region 2^44 bytes, FITS
       42-bit VA  ->  T0SZ = 22   region 2^42 bytes, FITS
       39-bit VA  ->  T0SZ = 25   region 2^39 bytes, FITS
       36-bit VA  ->  T0SZ = 28   region 2^36 bytes, FITS
       33-bit VA  ->  T0SZ = 31   region 2^33 bytes, FITS
       32-bit VA  ->  T0SZ = 32   region 2^32 bytes, DOES NOT FIT in a 5-bit field
       30-bit VA  ->  T0SZ = 34   region 2^30 bytes, DOES NOT FIT in a 5-bit field
       25-bit VA  ->  T0SZ = 39   region 2^25 bytes, DOES NOT FIT in a 5-bit field
                
R16, and it was in this course's own first draft DRAFT a configuration table of regimes, with a "30-bit, the 2-level case" row in it. MEASURED T0SZ is a FIVE-BIT field. 64 - 30 = 34 does not fit in it. With a 4 KiB granule the smallest regime the field can name is 2^33 bytes -- eight gibibytes. A 32-bit virtual address space is NOT expressible on AArch64 with 4 KiB pages at all. The two-level configurations all use the 64 KiB granule, where T0SZ is six bits wide and the floor moves to 2^32. A table of block sizes presented as a MENU rather than as a consequence of a FIELD WIDTH will always contain a row the field cannot express, and the row looks entirely reasonable. This is the third time in this section that a plausible-looking number was killed by a constraint printed in the same table.

And the other direction, because it is the one that matters for understanding why 48: a regime can cover the whole address space, 264 bytes, with T0SZ = 0. So the 48/52 split is a default and not a limit. The reason a real system uses 48 is not that 52 will not fit in the field — it is that each extra level of table costs another 4 KiB of memory and four more dependent memory accesses for every translation. That cost is a real number on a real machine and it is not measurable here. The five-bit field and the identity are, and they are what this page can defend.

The measurement: where the two regions meet, and the two reaches

The identity says how big each region is. It does not say where, and the constraint that decides where is that the two must not overlap. That is arithmetic, and the gap it leaves is the first thing a reader should know about the 48/52 configuration:

     the boundary, as pure arithmetic.  TTBRn is at the END of
     its region, so the high regime is mapped at the TOP of the
     address space -- that is the whole content of R10.
       48/48    TTBR0 covers 0x0000000000000000..0x0000ffffffffffff
                TTBR1 covers 0xffff000000000000..0xffffffffffffffff
                between them: 0xfffe000000000000 bytes, which is UNTRANSLATED
       48/52    TTBR0 covers 0x0000000000000000..0x0000ffffffffffff
                TTBR1 covers 0xfff0000000000000..0xffffffffffffffff
                between them: 0xffef000000000000 bytes, which is UNTRANSLATED
       39/48    TTBR0 covers 0x0000000000000000..0x0000007fffffffff
                TTBR1 covers 0xffff000000000000..0xffffffffffffffff
                between them: 0xfffeff8000000000 bytes, which is UNTRANSLATED
                

Three things in that block, and the first is the one the plan did not have.

The two reaches, and the asymmetry that decides everything

Now the second subject of the concept, and it is the one a backend author needs. Two instructions, one field shape, two units:

     instruction   immediate field   signed bits   unit       reach
     ADR           bits[20:5]+[30:29] 21          1 BYTE     +-2^20 = 1 MiB
     ADRP          bits[20:5]+[30:29] 21          4096 BYTES +-2^20 pages = 4 GiB
                

And one of those two numbers is measured and the other is not, and the difference is worth understanding rather than papering over.

$ python3 - <<'PY'   # bisecting the distance the assembler will accept
ADR   largest accepted distance 0xffff8   -> from the insn: 0xffffc
ADR   next step up            0xffffc       REFUSED: fixup value out of range
ADR   backward max D=0xffff8   -> from the insn: -0x100000
ADRP  the assembler does NOT check it: every distance tried up to
      0x1fff000 ACCEPTED, and a 4 GiB gap is ACCEPTED too
                

0xffffc and then a refusal at 0x100000 is exactly a 21-bit signed field — the largest positive value is 0xfffff, and the next multiple of four below it is 0xffffc. The assembler found the field's edge for us, and it found it in the only way that works: by refusing.

The ADRP row is the interesting one, and it is a measurement of an absence. The assembler does not range-check ADRP at all — a 4 GiB reference is accepted — because the value is not known until link time and the check belongs to the linker. There is no AArch64 linker on this host, so the 4 GiB figure is an arithmetic identity on a 21-bit field, not a measurement. Say that to yourself every time you see a reach quoted for an instruction whose target is not yet known: who checks it, and is that person present?

And the arithmetic the compiler emits, which is the whole structure of a walk expressed as five shifts:

$ python3 a64sys.py --section 7 | sed -n '/the compiler emits for the regime/,/^$/p'
     t0sz_for     2   sub x0, xzr, x1 ; ret
     regime_bytes 2   lsl x0, x1, x0 ; ret
     ttbr1_52     2   movz/movk pair ; ret
     l0_index     2   lsr x0, x0, #39 ; and x0, x0, #0x1ff ; ret
     l1_index     2   lsr x0, x0, #30 ; and x0, x0, #0x1ff ; ret
     l2_index     2   lsr x0, x0, #21 ; and x0, x0, #0x1ff ; ret
     l3_index     2   lsr x0, x0, #12 ; and x0, x0, #0x1ff ; ret
     page_off     2   and x0, x0, #0xfff ; ret
                

Nine bits per level, four levels, twelve bits of offset — and that is the 48-bit split arriving as a table shape. x86-64 needs the same five extractions and neither architecture stores the level in the descriptor: the level is implied by which table you are in, which is why the same three bits in a descriptor mean different things at different depths. The neutral paging course owns the principle; the x86-64 paging concept is the sibling reference; and this page owns the AArch64 numbers.

Worked: the 48/52 layout, end to end

Put the pieces together for the configuration a 64 KiB-granule system actually uses, because the numbers are more instructive than the general case:

  STAGE 1, EL1 & EL0, 64 KiB granule, 48/52 split (FEAT_LPA2)

  TCR_EL1.TCR_EL1 fields, QUOTED with their bit positions:
     [63:48] T0SZ   = 16      TTBR0 region  = 2^(64-16) = 2^48
     [47:32] T1SZ   = 12      TTBR1 region  = 2^(64-12) = 2^52
     [31:30] TG1   = 10      64 KiB
     [23:22] SH0   = 11      inner shareable
     [15:14] TG0   = 10      64 KiB
     [9:8]   IRGN0 = 01      inner cacheable
     [7:6]   ORGN0 = 01      inner cacheable

  TLB:  64 entries, 4 KiB pages, 64 sets
        -> 4 x 4 KiB = 16 KiB of page tables before you
           have mapped a single byte, and that number is
           NOT measurable on this host.
                

Three observations, and only the first is a fact this course can stand behind.

What this page cannot show

It cannot show a page walk, a TLB hit, a translation, or a fault. Not one address in this concept was translated by anything. The field positions, the granule encodings and the region formula are quoted with document and section. The encodings of the seven mrs/msr words, the five shifts, the seven-configuration identity and the ADR boundary are measured. The one measurement a reader should hold on to is the reach asymmetry: a fixed 32-bit instruction with a 21-bit field can address a page 4 GiB away or a byte 1 MiB away, and that single fact is why the ELF side of the subject — the relocation pair, the alignment, the linker script — is the half worth studying and the half a toolchain can show you.

Apply it

  1. Bisect the ADR boundary yourself. Write a .s file with adr x0, far, a .space and a far: label; assemble at a range of distances and find where it stops working. (Expect the last accepted distance to be 0xffffc and the first refused to be 0x100000, with fixup value out of range. Then read the field: 21 bits, signed, in bytes, and 0xfffff rounded down to a multiple of four is 0xffffc. Exactly. Now repeat with adrp at 32 MiB, 256 MiB, 2 GiB and 4 GiB, and expect all four to be accepted — the assembler does not check ADRP, and finding out that for yourself is worth more than being told.)
  2. Compute the seven identities, then break one. Take the configuration table on this page and verify T0SZ = 64 - VA in every row by hand. (Expect all seven to check. Then put a 30-bit row in and verify that — and find T0SZ = 34, which does not fit in five bits. That is retraction R16 reproduced in thirty seconds, and reproducing a retraction is worth more than reading one, because the thirty seconds is what convinces you the arithmetic is the reason and not the author's preference.)
  3. Draw the two regions and find the hole. For 48/48, 48/52 and 39/48, draw the address space as a bar and mark the two regions. (Expect 48/48 to fill it exactly, 48/52 to leave a 256 TiB hole in the middle, and 39/48 to leave a 128 TiB hole between the top of the user region and the bottom of the kernel region. Then ask what a load from the hole does: not an error, an unmapped hole, and a translation fault. The hole is a feature — it is address space reserved by not being translated.)
  4. Count the descriptors a regime needs, and watch it stop being possible. For a 48-bit address space, divide by the block size at each level: 248 / 221 = 227 entries for 2 MiB blocks, 248 / 230 = 218 for 1 GiB blocks, and remember that a table has 512. (Expect 227 to be wildly too many and 218 to still be too many, which is the discovery: a 48-bit regime cannot use 2 MiB blocks at level 1, and the block size follows from the VA size rather than being a menu. That is retraction R13, and concept 5 does the whole calculation.)
  5. Find the reach asymmetry in your own object files. Take any C file of yours that indexes a large array and another that points at a large structure, compile both for aarch64-linux-gnu, and read llvm-readelf-21 -r on each. (Expect every reference to be an adrp plus a :lo12: partner, and expect the partner's relocation name to be R_AARCH64_ADD_ABS_LO12_NC when the second instruction is an add and R_AARCH64_LDST64_ABS_LO12_NC when it is a ldr. Two instructions, two relocations, one address — and the address is a page that cannot be named in one instruction on this architecture at all.)

Connect

Backwards, tightly. Concept 3 is where a translation fault arrives, and this is where it came from. Concept 2 is where its address is read: the FAR holds a virtual address, and everything on this page exists to explain what that word means. The ABI course's calling convention is needed for one number only, and it is a number you already know: x0 holds a pointer, and a pointer is a virtual address, and so every function call in your program is a translation. The ABI did not say so; the ABI just moved the number.

Sideways, three neighbours that own the edges. The neutral paging course owns the walk as a mechanism, in terms neither architecture requires; the x86-64 virtual-memory concept is the sibling reference, where the split is a CR3 bit rather than two base registers and the field is a page-directory-pointer count rather than a region size. The relocation course's PC-relative concept owns what adrpis, and this page's reach table is the same subject measured on a different architecture — which is exactly the division of labour this course is for.

Outward, and the general form is the thing to take away. A field width is a constraint, and a constraint is more useful than a default.T0SZ being five bits is quoted; T0SZ = 16 being what a 48-bit system writes is a choice; and the difference between the two is the difference between "this is impossible" and "this is a decision somebody made". A reader who has only the second will assume a system could be configured any way and will not notice a configuration that the field forbids. That instinct — look for the field width before you look for the value — is what killed this course's own first draft, and it is the third time in this section that it has been the thing that worked.

Forward, the hinge. Concept 5 is the ELF side of this page: the tables this regime points at have an alignment requirement and an address, and both are things an object file has to record and a linker has to honour. That is the step of the chain where the contract becomes checkable — parsing and IR are about meaning, and a page table is the first object in this course whose requirements are a property of its role and not of its type.

render_lesson_js(&mut page)