1.2.2. The EL3 “Sec” monitor and the EL1 hand-off¶
PH12 of xbl_a.bin is not ordinary code. It loads at 0x14950000 with
the distinctive PT_LOAD flags 0x05000005, and its entry at
0x14953000 installs an EL3 exception environment. This is the same
feature set — vaddr, flags, entry offset, SCR_EL3/VBAR_EL3 install —
as the Firehose programmer’s nested EL3 monitor that Firehose fuse read and secure-boot confirmation
proves is signature-enforced unconditionally. The segment also carries
the XBL Sec Attestation Root CA 4 string (file 0x2beb87). The
consequence for replacing XBL (you can substitute the EL1/UEFI payload but
not clean-room this EL3 root) is analysed in Boot chain and exception-level hand-off (PBL → XBL → ABL).
The ELF e_entry (0x14817908, in PH2) is not this monitor — it is
the EL1 XBLCore reset stub the monitor erets down to. On this SoC
XBLCore executes at EL1 and reaches EL3 through a single smc #0 at
0x14699024 (PH7).
1.2.2.1. EL3 “Sec” monitor — entry disassembly¶
PH12 is not laid out like the other code segments: its first ``0x3000``
bytes are a literal/data header, and executable code only begins at
0x14953000 (= 0x14950000 + 0x3000, file 0x2c17a0). A naive
linear disassembly of the segment from 0x14950000 desyncs on that
header and never re-aligns to the real instructions — which is why a
whole-image sweep can wrongly report “no scr_el3”. Disassembled from
0x14953000 the monitor’s EL3 set-up is explicit (all instructions below
are Capstone CS_ARCH_ARM64 output):
0x14953000: mov x9, x0 ; stash PBL boot-arg
0x14953004: adr x0, #0x14953000 ; self-position check:
0x14953008: ldr x1, #0x149531a8 ; literal[0x149531a8] = 0x14953000
0x1495300c: cmp x0, x1
0x14953010: b.ne #0x14953010 ; hang if not loaded where expected
0x14953014: mov x0, #0xe00
0x14953018: msr scr_el3, x0 ; SCR_EL3 = 0xE00 (RW|ST|SIF, still Secure)
0x1495301c: isb
0x14953020: ldr x0, #0x149531b0 ; literal = 0x14964800
0x14953024: msr vbar_el3, x0 ; VBAR_EL3 = 0x14964800 (EL3 vectors)
0x14953028: mov x0, #0x1c0
0x1495302c: msr daif, x0 ; mask A/I/F
0x14953030: ldr x10, #0x149531b8 ; = 0x14967080
0x14953034: str x9, [x10] ; persist boot-arg
0x1495303c: movk x0, #0x1495, lsl #16
0x14953044: bl #0x14961488 ; relocate/copy (memcpy), sp = 0x14953000
...
0x14953070: bl #0x1495313c ; sanitise EL1 register file (see below)
0x14953074: bl #0x149563d4 ; loader dispatch -> x0 = EL1 entry
0x14953078: msr elr_el3, x0 ; ELR_EL3 = computed EL1 entry point
0x1495307c: mrs x0, hcr_el2
0x14953084: orr x0, x0, #0x80000000
0x14953088: msr hcr_el2, x0 ; HCR_EL2.RW = 1 (EL1 is AArch64)
0x1495308c: mov x0, #0x1c5
0x14953090: msr spsr_el3, x0 ; SPSR_EL3 = 0x1C5 (return to EL1h, A/I/F masked)
0x14953094: mrs x0, scr_el3
0x14953098: orr x0, x0, #1
0x1495309c: msr scr_el3, x0 ; SCR_EL3 |= NS -> 0xE01 (Non-secure)
... ; x0..x30 cleared to 0
0x14953128: ldr x0, #0x149531d8 ; = 0x14967088
0x1495312c: ldr x0, [x0] ; x0 = boot-info pointer for EL1
0x14953130: dsb sy
0x14953134: isb
0x14953138: eret ; drop to EL1h at ELR_EL3
So the “Sec” monitor is the piece that owns EL3 and then hands control
down to EL1: it installs VBAR_EL3, sets SCR_EL3 (Secure 0xE00
while it runs, then flips the NS bit to 0xE01 just before
returning), masks exceptions via DAIF, then erets with
SPSR_EL3 = 0x1C5 (EL1h) and HCR_EL2.RW = 1. The eret target
(ELR_EL3) is computed at run time by the loader-dispatch routine
0x149563d4 (it walks two tables of loader callbacks at 0x149617e8
(6) and 0x149617c0 (5), tailing into 0x14958c00); the XBLCore EL1
entry 0x14817908 appears in the whole image only at file 0x18
(the ELF header e_entry field), never as an in-code constant, consistent
with a computed jump. x0 is loaded with the boot-info pointer
0x14967088 per the AArch64 SMCCC/boot convention.
Before the eret the monitor scrubs the EL1 context it is about to enter
(0x1495313c) — every EL1 system register is written with xzr:
0x1495313c: msr actlr_el1, xzr 0x14953148: msr contextidr_el1, xzr
0x1495314c: msr amair_el1, xzr 0x14953158: msr mair_el1, xzr
0x14953160: msr sctlr_el1, xzr 0x14953170: msr tcr_el1, xzr
0x14953178: msr ttbr1_el1, xzr 0x1495317c: msr ttbr0_el1, xzr
0x14953184: msr vbar_el1, xzr 0x14953190: msr elr_el1, xzr
0x14953194: msr sp_el1, xzr 0x1495319c: msr spsr_el1, xzr ... (and afsr0/1,
far, csselr, esr, par, tpidr*, cntp_ctl/tval/cval, sp_el0)
EL3 state the monitor does establish (confirmed by disassembly from
0x14953000):
Register |
Value / action |
Where |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
computed EL1 entry |
|
|
|
|
|
|
|
|
read in the EL3 handlers |
|
What it does *not* touch: there is no write to CPTR_EL3,
MDCR_EL3 or SCTLR_EL3 (and no MAIR_EL3/TCR_EL3/TTBR0_EL3)
in PH12 — the EL3 monitor runs with the EL3 MMU off / flat and does not
reprogram FP-trap or debug at EL3. (Those EL3-MMU registers do appear in
the image, but only in the EL-agnostic boot-MMU helpers reached from the EL1
XBLCore path — see EL1 XBLCore reset stub.) This entry is byte-for-byte
the same shape as the TZ tz_a EL3 stub at 0x14680000 (mov
x0,#0xe00; msr scr_el3; mov x0,#0x1c0; msr daif) documented in
Secure world and coprocessors (TZ, HYP, trustlets).
The EL3 vector table at VBAR_EL3 = 0x14964800 is a standard
16×``0x80`` AArch64 table. The Current-EL synchronous slots tail into a
common handler at 0x149614a8 (which builds a 0x130-byte frame and
saves ELR_EL3); the Lower-EL AArch64 synchronous slot (+0x400,
0x14964c00) is the SMC/lower-EL dispatcher — it re-asserts
SCR_EL3 = 0xe00 (Secure) and builds a 0x110-byte frame including
SP_EL0/SP_EL1 before branching to 0x14961558:
0x14964c00: stp x0, x1, [sp, #-0x10]!
0x14964c04: mrs x0, scr_el3
0x14964c08: mov x1, #0xe00
0x14964c0c: msr scr_el3, x1 ; handle call in Secure state
...
0x14964c70: b #0x14961558
This is the EL3 end of the SMC path whose EL1 caller is the gateway below.
1.2.2.2. EL1 XBLCore reset stub¶
The monitor’s eret lands in the EL1 XBLCore reset stub — the ELF
e_entry 0x14817908 (PH2, file 0x3908). It manipulates only the
EL1 copies of the control registers:
0x14817908: b #0x14817910 ; over a udf padding word
0x14817920: mov x7, x0 ; save boot-arg
0x14817928: mov sp, x0 ; sp = 0
0x14817934: tlbi vmalle1
0x14817938: ic iallu
0x14817944: mrs x0, sctlr_el1
0x14817948: and x0, x0, #~1 ; M=0 (MMU off)
0x1481794c: and x0, x0, #~4 ; C=0 (D-cache off)
0x14817950: orr x0, x0, #8 ; SA=1 (SP-align check)
0x14817954: msr sctlr_el1, x0
0x14817960: ldr x0, #0x148179c0 ; literal = 0x14817000
0x14817964: msr vbar_el1, x0 ; VBAR_EL1 = 0x14817000 (SBL1 vector table)
0x14817968: mrs x0, isr_el1
0x1481796c: and x0, x0, #0x100 ; pending SError?
0x14817970: cbnz x0, #0x148179bc ; -> hang if so
0x14817974: msr daifclr, #4 ; unmask SError (A)
0x1481797c: mov x0, #0xb000
0x14817980: movk x0, #0x1486, lsl #16
0x14817984: mov sp, x0 ; sp = 0x1486b000
0x14817988: mov x0, #0x8000 ; zero .bss 0x14868000..0x1486b000
0x1481798c: movk x0, #0x1486, lsl #16
0x148179a0: stp xzr, xzr, [x0], #0x10
0x148179ac: b.ne #0x148179a0
0x148179b0: mov w0, w7 ; boot-arg
0x148179b4: bl #0x148222b0 ; XBLCore C loader (enables FP via CPACR_EL1)
Every control-register write here is the EL1 encoding (sctlr_el1,
vbar_el1, isr_el1); there is no CurrentEL check and no EL3
write — the stub assumes it is already at EL1. VBAR_EL1 points at the
SBL1 vector table at 0x14817000 (16×``0x80``); only the
Current-EL-SPx slots are live (Synchronous +0x200 saves all GPRs
then bl 0x1482bec0; IRQ +0x280, FIQ +0x300, SError +0x380),
while the SP0 and Lower-EL slots are b . hangs. The synchronous
handler’s C routine prints Exception caught by SBL1 vector table!! and
dumps ELR-EL1/ESR-EL1 (strings at file 0x6c38/0x6c60) — i.e.
EL1 exception reporting, corroborating that XBLCore runs at EL1.
A boot-MMU library on this EL1 path can program the EL3 MMU: 0x14818280
writes TTBR0_EL3+TTBR0_EL1, MAIR_EL3/MAIR_EL1 =
0xAAFFF4884A4F4400, TCR_EL3/TCR_EL1 = 0x30019 (T0SZ=25 →
39-bit VA, PS=3 → 42-bit PA), and lays a 4-entry 1 GB-block identity map over
0–4 GB — these six _el3/_el1 register pairs are written back-to-back,
unconditionally, with no CurrentEL guard on the _el3 half (no direct
bl caller for 0x14818280 was found inside PH2, so its actual call
site — and thus whether it in fact runs at EL3 when it executes — is not
independently confirmed here). Only the separate SCTLR accessor pair,
0x14817a08 (getter) / 0x14817a28 (setter), which the MMU library also
calls to enable M|C|I, is CurrentEL-dispatched: each msr sctlr_el3
there is individually guarded by cmp x1,#3 ; b.ge on CurrentEL and
only runs at EL3 — that guard does not extend to the TTBR0/MAIR/TCR writes
above.
EL1 XBLCore C main. The reset stub’s bl 0x148179b4 reaches the C
entry 0x148222b0 (in PH2), identified by its two panic sites logging
the source file sbl1_mc.c (file 0x14818a2f) with a line number and
error code through a function pointer at 0x1486c1b0, and by the
SBL1, Start banner (file 0x14818a6e). It enables FP/SIMD
(0x14818510), copies a 0x180-byte boot-info block from its
argument, runs an init/milestone chain, and polls a debug cookie register
before continuing — the SBL1 main-control routine of the EL1 loader.
1.2.2.3. EL1 → EL3 SMC gateway (PH7)¶
PH7 (0x14699000, 0x1230 bytes, R E) is the scm/SMC
trampoline — and it carries the only smc instruction in the entire
image (0x14699024; the whole-image scan finds smc = 1,
hvc/eret outside PH12 = 0). This is how the dropped-to-EL1 XBLCore
calls back up into the EL3 monitor (and TZ):
0x14699000: stp x7, x8, [sp, #-0x10]! ; save x7,x8 + x15..x30 (x0..x6 = SMCCC args)
0x14699020: stp x15, x16, [sp, #-0x10]!
0x14699024: smc #0 ; trap to EL3 (SMC64)
0x14699028: cmp x0, #1
0x1469902c: b.eq #0x14699024 ; SMCCC busy/interrupted -> call again
... ; restore x15..x30, x7, x8
0x14699054: cmp x7, #0 ; x7 = result-struct pointer
0x14699058: b.eq #0x14699068
0x1469905c: str x1, [x7] ; write back return values x1..x3
0x14699064: str x3, [x7, #0x10]
0x14699068: ret
Callers pass Qualcomm SiP function IDs in w0 (e.g. 0x82000117 built
by the EL1 exception handler at 0x1482bf0c). The smc #0 lands in
the EL3 monitor’s Lower-EL synchronous vector (0x14964c00 above).
Together the two ends make the EL3/EL1 split concrete: XBLCore is EL1 code
that reaches the resident EL3 “Sec” monitor exclusively through this
gateway.
1.2.2.4. Function inventory and call graph¶
Beyond the entry/monitor stubs above, the EL3 “Sec” segment (PH12) is a
small, self-contained runtime. Following the bl/blr targets out of
the entry and the vector handlers yields the call graph below (all
addresses are PH12 vaddrs; “file” offsets map through the ELF phdrs,
0x14950000 → file 0x2be7a0). Function roles are inferred from
structure only — the image is symbol-free, so no Qualcomm names are
assigned.
EL3 monitor support routines (reached from the entry 0x14953000):
0x14961488 DC-ZVA zero-fill: `lsr x1,x1,#6; dc zva,x0; add x0,#0x40; loop`.
The monitor's memset-0. Clears the 0x14950000..0x14953000 EL3
stack from the entry, and scrubs the stack in the SMC top-half.
0x14957558 stack-protected reader whose result is stored to global
0x149660b0 (0xDEADDEAD sentinel in the file image); worker
0x149574a4, logs "(%x)" on canary failure.
0x1495f528 memscpy(dst, dst_size, src, src_size): copies min(sizes) via the
bytewise loop 0x149575d0. 0x1495f564 is the matching memset.
0x14965078 cache-info helper (reads ``CLIDR_EL1``); 0x14965178/0x14965190
wrap an I-cache invalidate-all (``ic iallu``).
0x14956560 printf-style logger: parses ``%x``/``%X``/``%s`` using the digit
table "0123456789ABCDEF" (file 0x14961819) into a 1 KiB character
ring at 0x1496c0b8 (index masked ``& 0x3ff``). The monitor's
console; called with a level in ``w0`` and a tag in ``w2``.
EL3 exception path. The VBAR_EL3 Current-EL synchronous slots
inject a code (#1/#5) and fall into the common handler
0x149614a8, which saves a 0x130 frame (x0..x30, ELR_EL3,
SPSR_EL3, SP_EL0/SP_EL1, ESR_EL3), forces
SCR_EL3 = 0xE00 (saving the old value to 0x14966f04), then calls
the dump routine 0x149546a4 and dead-loops. Any non-SMC EL3 exception
therefore terminates here — this is the “Exception caught”-style panic
sink at EL3.
EL3 SMC path. The Lower-EL AArch64 synchronous vector (0x14964c00)
saves a 0x110 frame and branches to the top-half 0x14961558:
0x1496155c: mrs x4, esr_el3
0x14961560: ubfx x4, x4, #0x1a, #6 ; EC = ESR_EL3[31:26]
0x14961564: cmp x4, #0x17 ; 0x17 = SMC from AArch64
0x14961568: b.ne #0x14961604 ; not an SMC -> re-dispatch (code #9) to 0x149614a8
0x1496156c: mov x0, sp ; x0 = saved-register frame
0x14961570: bl #0x14957614 ; SMC service dispatcher
... ; restore frame; scrub stack (0x14961488)
0x149615f0: msr scr_el3, x19 ; restore saved SCR_EL3 (from 0x14966f04)
0x14961600: eret ; return to the EL1 caller
The dispatcher 0x14957614 masks the function ID to & 0x3FFFFFFF,
rejects IDs with bits [23:16] set, then linearly scans a handler
table whose start and end pointers live at 0x149660b8/0x149660c0.
Each table entry is 0x10 bytes — {u32 funcID; u32 arg_desc; u64
handler} — and on a match the handler pointer at entry+8 is called
via blr with x0..x7 taken from the (validated, copied-in) argument
buffer. Pointer arguments are bounds-checked against allowed-region tables
by 0x14953bac and 0x14959208 before the call.
On this image the table is pre-populated in the file at
0x14962cf0..0x14962dd0 — 14 entries, all in the Qualcomm SiP
0x0200xxxx owner range (verified byte-for-byte through the phdr map):
funcID arg_desc handler funcID arg_desc handler
0x02000114 0x00000022 0x149573a8 0x02000119 0x00000001 0x14957050
0x02000117 0x00000022 0x14953728 0x02000214 0x00008017 0x14955d68
0x02000310 0x00000000 0x14956e38 0x02000215 0x00000446 0x14956060
0x02000210 0x00021048 0x14954d0c 0x0200011a 0x00000001 0x1495475c
0x02000211 0x00000083 0x149558fc 0x02000115 0x00000000 0x14956454
0x02000212 0x00000043 0x14954868 0x02000116 0x00000000 0x14958f84
0x02000213 0x00000043 0x149548fc 0x02000118 0x00000001 0x149538a0
This closes the EL1→EL3 loop: the 0x82000117 SiP ID that the EL1
exception handler builds (0x1482bf0c, in EL1 → EL3 SMC gateway (PH7))
masks to 0x02000117 — the table’s second row, handler 0x14953728.
Handler roles are only partially recoverable without symbols;
structurally, handler 0x14956454 (ID 0x02000115) zero-fills the TZ
base 0x14680000 (the tz_a EL3-stub base,
Secure world and coprocessors (TZ, HYP, trustlets)) and a 0x1C000000 staging window —
consistent with a secure-image handoff transfer.
- Source:
_READONLY/lun1/xbl_a.bin— CapstoneCS_ARCH_ARM64disasm of the EL3 “Sec” monitor0x14953000, EL1 XBLCore reset stub0x14817908, EL3 vector table0x14964800and thesmcgateway0x14699000; full listings inartifacts/re_static/item3_xbl_disasm.txt.- Cross-refs:
XBL (segment layout table), Boot chain and exception-level hand-off (PBL → XBL → ABL), Secure world and coprocessors (TZ, HYP, trustlets), Firehose fuse read and secure-boot confirmation.