Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

libctype — CSTUB Custom-Type Functions

Part 14 · The ISS as Executable Oracle. This page documents libctype.so, the lowest layer of the ISS type system: the host-callable C-stub implementations of the codec's custom register-value types — the get / set / move / convert (loadi / storei / move / rtor) accessors for the 64 ctypes. It sits beneath the cas/fiss ISS, the codec, and the native TIE simulator. The fiss value leaves (fiss-datapath-oracle.md) operate on the ctype-typed values that this library marshals; the ctype/coproc/funcUnit ISA declarations live in ../isa/core/ctype-coproc-funcunit.md, and the scalar-type/dtype mapping in ../abi/scalartype-dtype-rosetta.md. The capstone that ties the whole oracle together is iss-oracle-synthesis.md.


0. What libctype is — and what it is not

libctype.so is the host-side value-marshalling layer for the Vision-Q7 GPSIMD config (the ncore2gp build). For every ctype the ISA declares — a scalar AR uint32, a 1-bit predicate vbool1, a 512-bit vec2Nx8, a 1536-bit wvec spill slot — there is a host representation (a small struct of consecutive uint32 words, the "register representation") and a host-simulated target memory window. libctype is the glue that copies a value between those two: it loads a value out of simulated target memory into the host reg-struct, stores it back, and converts a value of one ctype into another (widen / narrow / splat / sign-extend / reinterpret).

It is a self-contained layer. Its only dynamic imports are libc:

$ nm -D libctype.so | rg ' U '
                 U exit@GLIBC_2.2.5
                 U fprintf@GLIBC_2.2.5
                 U malloc@GLIBC_2.2.5
                 U stderr@GLIBC_2.2.5

It does not import or link libisa-core, libcas-core, or libfiss-base, and nothing in ncore2gp/config imports it (nm -D | rg ' U cstub_' is empty across the configs). The consumers — the only objects that reference the cstub_*_function symbols — are the native TIE simulator (XtensaTools/lib/tc/nativesim) and the debugger (XtensaTools/bin/xt-gdb). libctype is therefore orthogonal to the fiss/cas value+timing oracle: cas/fiss compute values and timing; libctype marshals typed values across the host↔target memory boundary for the native sim. [OBSERVED · HIGH]

NOTE. libctype is generated by the Xtensa TIE compiler from the same ctype/proto definitions that produce the ISA tables. The dispatch mechanism (3 fn-ptr tables indexed by ctype id, sorted convert lists, a base_addr memory window, a single/multi-word flag, an alignment hard-fault) is the fixed VERS_1.1 TIE-cstub ABI; only the populated tables differ per config.

0.1 Binary identity

propertyvaluesource
pathncore2gp/config/libctype.socorpus
typeELF64 LSB x86-64 .so, not strippedfile
size388,648 Bstat
sha256eb79ff…a3f91csha256sum
soname versionVERS_1.1readelf -V
fn symbols (.symtab)315 (nm | rg -c ' [Tt] ')nm

[OBSERVED · HIGH]

0.2 Section→file map (VMA vs file offset)

$ readelf -SW libctype.so | rg '\.(text|rodata|data|bss)\b'
 [12] .text    PROGBITS  00000000000006d0  0006d0  0486a4  AX
 [14] .rodata  PROGBITS  0000000000048d80  048d80  0095b0  A
 [23] .data    PROGBITS  0000000000256040  056040  000240  WA
 [24] .bss     NOBITS    0000000000256280  056280  000680  WA
sectionVMAfile offdeltaholds
.text0x6d00x6d00code: 6 dispatchers + 299 Function_TIE_* + 2 helpers
.rodata0x48d800x48d800the splat/lane LUTs (cstub_Xm_*_table) + the unalign error format string @ 0x48d80
.data0x2560400x0560400x200000cstub_ctype_multi_word[64] @ file 0x56080
.bss0x256280— (NOBITS)the 4 runtime-init globals

GOTCHA. .text and .rodata are VMA == file-offset, so objdump --start-address=<VMA> and xxd -s <VMA> work directly. .data is not — VMA − file = 0x200000 (the gpsimd config-DLL delta, not libtpu's 0x400000). To xxd the cstub_ctype_multi_word table at VMA 0x256080 you must read file offset 0x56080. Always confirm per-section with readelf -SW.


1. The runtime globals + the static size table

nm -S gives exact placement and span:

$ nm -S libctype.so | rg 'base_addr|ftable|frtor|multi_word'
0000000000256288 0000000000000008 b base_addr
0000000000256080 0000000000000040 d cstub_ctype_multi_word
00000000002562c0 0000000000000200 b cstub_loadi_ftable
00000000002564c0 0000000000000200 b cstub_storei_ftable
00000000002566c0 0000000000000008 b frtor
0000000000256700 0000000000000200 b cstub_rtor_ftable
symbolVMAspanrole
base_addr0x2562888host pointer to the simulated target-memory window
cstub_loadi_ftable0x2562c00x200 = 64×8one fn-ptr per ctype id (0 if not installed)
cstub_storei_ftable0x2564c00x200 = 64×8one fn-ptr per ctype id
frtor0x2566c08scratch: current fn ptr during the inlined rtor append
cstub_rtor_ftable0x2567000x200 = 64×8one list-head per ctype id
cstub_ctype_multi_word0x2560800x40 = 64static const u8[64] size/width flag (only .data const)

All four *_ftable/scalar globals are in .bss (zero at load), so cstub_ctypes_init must run before any dispatcher returns a valid pointer. Every table is 64 entries, indexed by ctype id 0..63 — the same 64-row index space the ISA's ctypes[] table uses. [OBSERVED · HIGH]

1.1 cstub_ctype_multi_word[64] — the "value spans > 1 host word" flag

cstub_ctype_size_function(idx) is a one-line movzbl of this byte array (§2.5). Reading the 64 bytes byte-exact (file off 0x56080):

$ xxd -s 0x56080 -l 64 libctype.so
0x56080: 00 00 00 00 00 00 01 01  00 00 00 00 00 00 00 00
0x56090: 00 00 01 01 01 01 01 01  01 01 01 01 01 01 01 01
0x560a0: 01 01 ... (all 01 through idx 63)
idx rangectypesflag
0..5uint8 int8 uint16 int16 uint32 int320 (single host word)
6..7uint64 int64 (scalar AR pair)1 (multi-word)
8..17char short unsigned int void xtbool xtbool2..160 (single word)
18..63all 46 Vision SIMD valign / vbool* / vec* / wvec / b32_pr / gvr1 (multi-word)

The only scalar multi-word types are uint64/int64 (two 32-bit AR words); every Vision SIMD type (≥ 16 bits, all wider than one host word) is multi-word; the booleans and 32-bit scalars are single-word. The native sim reads this flag to choose the reg-struct copy width / spill path. [byte values OBSERVED · HIGH; role INFERRED from the fn name and the loadi widths · MED]


2. The six exported dispatchers

The exported surface is exactly the libctype.def EXPORTS list, gated by the VERS_1.1 script:

$ nm -D libctype.so | rg ' T cstub_'
0000000000043850 T cstub_set_base_address@@VERS_1.1
0000000000043860 T cstub_loadi_function@@VERS_1.1
0000000000043870 T cstub_storei_function@@VERS_1.1
0000000000043880 T cstub_rtor_function@@VERS_1.1
00000000000438c0 T cstub_ctype_size_function@@VERS_1.1
00000000000438d0 T cstub_ctypes_init@@VERS_1.1

Six exports, six lines in libctype.def, six global: entries in libctype.map. Everything else is local: * (the 299 Function_TIE_* stubs and the two helpers cstub_vaddr_not_aligned @ 0x6d0 and cstub_write_rtor_func @ 0x3d4d0). [OBSERVED · HIGH]

2.1 cstub_set_base_address(void *ptr) @ 0x43850

43850: mov %rdi,base_addr(%rip)   ; base_addr = ptr
43857: ret

Stores the host pointer to the simulated target-memory window. Every loadi/ storei forms its host address as base_addr + (esi + edx), so the (esi,edx) pair is a 32-bit target virtual address split into {base_reg, offset} that base_addr maps into the host address space. This is the host↔target memory bridge. [OBSERVED · HIGH]

2.2 The ftable stride — cstub_loadi_function / cstub_storei_function

; cstub_loadi_function(uint32 idx) @0x43860
43860: lea cstub_loadi_ftable(%rip),%rax   ; &table
43867: mov %edi,%edi                       ; idx zero-extended to 64 bits
43869: mov (%rax,%rdi,8),%rax              ; rax = table[idx]   <-- STRIDE 8
4386d: ret

cstub_storei_function @ 0x43870 is byte-identical on cstub_storei_ftable.

The ftable stride is 8 bytes. The index expression (%rax,%rdi,8) is a scale-8 SIB: table[idx] lives at &table + idx*8 — one 8-byte function pointer per ctype id, idx zero-extended (mov %edi,%edi) before scaling so the top 32 bits cannot poison the address. There is no bounds check: the caller (the native sim) is trusted to pass idx ∈ [0,63]. [OBSERVED · HIGH]

The native sim calls this once per ctype to obtain the marshalling stub, then invokes the returned pointer.

2.3 cstub_rtor_function(uint32 srcidx, uint32 otheridx) @ 0x43880 — the convert lookup

The rtor table is indexed by the source ctype id (same stride 8), but each slot is a list head, not a function pointer. The dispatcher walks a singly-linked, sorted list keyed on the other (target) ctype id:

43880: lea cstub_rtor_ftable(%rip),%rax
43887: mov %edi,%edi
43889: mov (%rax,%rdi,8),%rax        ; node = rtor_ftable[srcidx]   (list head)
4388d: test %rax,%rax
4388f: je  null                       ; empty list -> NULL
43892: mov (%rax),%edx                ; edx = node->other_ctype_id   (+0x00)
43894: cmp %esi,%edx
43896: je  hit
43898: cmp %edx,%esi
4389a: jae loop_body
4389c: jmp null                       ; esi < head.other -> sorted miss, early-out
; loop:
438a0: cmp %esi,(%rax)                ; node->other vs otheridx
438a2: je  hit
438a4: ja  null                       ; passed it (sorted) -> miss, early-out
438a6: mov 0x10(%rax),%rax            ; node = node->next             (+0x10)
438aa: test %rax,%rax
438ad: jne loop
438af: xor %eax,%eax                  ; NULL
438b1: ret
; hit:
438b8: mov 0x8(%rax),%rax             ; return node->fn               (+0x08)
438bc: ret

The list is kept sorted by other-type-id ascending: the cmp … ja null short-circuits as soon as the walk passes the wanted id, so a miss costs O(degree) in the worst case but bails early on the common case. Returns Function_TIE_<src>_rtor_<other> for the (src,other) pair, or NULL. [OBSERVED · HIGH]

2.4 The rtor convert NODE — 24 bytes, offsets read from disasm

CORRECTION — there is NO DWARF in this binary. Despite "not stripped", the ELF has only .symtab/.strtab (function names + nm -S sizes); there are zero .debug_* sections. addr2line resolves a function name from .symtab but reports file:? for every line (no .debug_line). The struct field offsets below are therefore pinned from the disassembly displacements themselves (the literal mov encodings in the node builder and the dispatcher walk) — which is stronger than DWARF, because it is the actual code that reads the fields. The 0x18-byte node size is the literal malloc argument.

The builder cstub_write_rtor_func @ 0x3d4d0 (79 bytes) decodes:

3d4e7: mov (%r12,%rbp,8),%rbx         ; rbx = rtor_ftable[srcidx]  (r12=&table, rbp=src)
3d4eb: test %rbx,%rbx
3d4ee: jne  walk                       ; non-empty -> walk to tail
3d4f0: jmp  install_head               ; empty -> install at head
; walk:
3d4f8: mov  %rax,%rbx
3d4fb: mov  0x10(%rbx),%rax            ; rax = node->next   (+0x10)
3d4ff: test %rax,%rax
3d502: jne  3d4f8                       ; ...to tail
3d504: mov  $0x18,%edi
3d509: call malloc@plt                  ; n = malloc(24)
3d50e: mov  %r14,0x8(%rax)             ; n->fn  = fn       (+0x08)  (r14 = rdi arg)
3d512: mov  %r13d,(%rax)               ; n->other = otheridx (+0x00) (r13d = edx arg, 32-bit)
3d515: movq $0x0,0x10(%rax)            ; n->next = NULL    (+0x10)
3d51d: mov  %rax,0x10(%rbx)            ; tail->next = n
; install_head (empty list):
3d543: mov  %rax,(%r12,%rbp,8)         ; rtor_ftable[srcidx] = n
struct cstub_rtor_node {            /* 24 bytes — malloc(0x18) */
    int32_t  other_ctype_id;        /* +0x00  the convert "to" ctype id  */
    /* +0x04: 4 bytes implicit padding (alignment of the void*) */
    void    *fn;                    /* +0x08  Function_TIE_<src>_rtor_<other> */
    struct cstub_rtor_node *next;   /* +0x10  NULL-terminated singly-linked list */
};

The offsets +0x00 / +0x08 / +0x10 and the size 0x18 are read directly from the instruction encodings above, and they match the dispatcher walk in §2.3 (mov (%rax) reads +0x00, mov 0x8(%rax) returns +0x08, mov 0x10(%rax) follows +0x10). cstub_write_rtor_func is the clean API the codec/generator calls; cstub_ctypes_init inlines the same append logic ~265 times. [OBSERVED · HIGH]

QUIRK. other_ctype_id is a 32-bit field (mov %r13d,(%rax)) even though only 64 ctype ids exist, and the comparison in the dispatcher is cmp %esi,(%rax) (32-bit) — so the upper bytes of the host's 64-bit slot are never written and never read; they stay whatever malloc returned. The 4-byte pad at +0x04 exists purely to 8-align the fn pointer.

2.5 cstub_ctype_size_function(uint32 idx) @ 0x438c0

438c0: lea cstub_ctype_multi_word(%rip),%rax
438c7: mov %edi,%edi
438c9: movzbl (%rax,%rdi,1),%eax       ; return (u8) multi_word[idx]   <-- STRIDE 1
438cd: ret

GOTCHA — two different strides in one library. The three function tables are indexed scale-8 (,%rdi,8, 8-byte pointers); the size table is a u8[64] indexed scale-1 (,%rdi,1, one byte). Do not confuse the size-function's byte-array stride with the ftable's pointer stride.

Returns the single/multi-word flag of §1.1. [OBSERVED · HIGH]


3. cstub_ctypes_init — the ftable builder (4,715 instructions @ 0x438d0)

nm -S gives the span (0x54a4 bytes); disassembling that range yields exactly 4,715 instructions and exactly 265 call malloc sites:

$ nm -S libctype.so | rg ctypes_init
00000000000438d0 00000000000054a4 T cstub_ctypes_init
$ objdump -d --start-address=0x438d0 --stop-address=0x48d74 libctype.so | rg -c '^\s+[0-9a-f]+:'
4715
$ objdump -d --start-address=0x438d0 --stop-address=0x48d74 libctype.so | rg -c 'call.*malloc'
265

This is the one-shot "register all marshalling stubs" call the native sim issues at startup, after cstub_set_base_address. It runs in four stages:

  • Stage A — zero-fill. Two rep stos %rax,(%rdi) clear 0x40 qwords each into loadi_ftable and storei_ftable (both 64-slot tables to NULL).

  • Stage B — loadi populate. For each installed ctype: lea Function_TIE_*_loadi; mov %rcx, off(%rsi) with off = ctypeid * 8. The first writes, byte-exact:

    offidxfn
    0x204Function_TIE_xt_core_uint32_loadi
    0x9018Function_TIE_xt_ivp32_valign_loadi
    0x9819Function_TIE_xt_ivp32_vboolN_loadi
    0xa020Function_TIE_xt_ivp32_vbool2N_loadi
    0xa821Function_TIE_xt_ivp32_xb_vec2Nx8_loadi
    0xb022Function_TIE_xt_ivp32_xb_vec2Nx8U_loadi

    The 47 populated loadi indices are exactly {4} ∪ {18..63}: only uint32 among the scalar core types, plus every Vision SIMD type. loadi for uint8/int8/uint16/int16/char/short/unsigned/int/void/xtbool* is not installed — the sim narrows from the uint32 loadi (or never immediate-loads those). [OBSERVED · HIGH]

  • Stage C — storei populate. 47 storei impls are lea'd into storei_ftable; all 47 exist as symbols (nm \| rg -c _storei = 47).

  • Stage D — rtor build (from 0x43e14). rep stos $0 clears rtor_ftable (64 list heads), then for each (src,other) convert edge it walks rtor_ftable[src] to the tail, malloc(0x18), writes {other_id, fn, next=0}, and appends — the inlined form of §2.4. The 265 malloc(0x18) calls = 265 convert edges; with 205 distinct convert functions (a function may be registered under several source lists) the convert graph has 265 edges over 205 functions. frtor @ 0x2566c0 holds the current fn ptr during the inlined append. [OBSERVED · HIGH; the "sim-startup one-shot" role INFERRED · MED]

NOTE. In Stage B/C the populate stores target %rsi/%rdx (registers holding the table base inside ctypes_init), not the global symbol directly — the base pointers are loaded once at function entry. The decoded off displacements are absolute byte offsets into the 64-slot table, so off / 8 recovers the ctype id.


4. Representative stub decodes — the get / set / move / convert bit-layouts

Calling convention (observed across all stubs):

loadi (reg_struct *dst rdi, uint32 base esi, uint32 off edx)  -> writes *dst
storei(reg_struct *src rdi, uint32 base esi, uint32 off edx)  -> reads  *src (or edi)
rtor  (reg_struct *dst rdi, src esi)   ; esi = a scalar VALUE for scalar sources,
                                       ;       a reg_struct* POINTER for wide sources
host address = base_addr + (esi + edx)
reg_struct   = N consecutive uint32 words (the "register representation")

(a) Scalar AR — Function_TIE_xt_core_uint32_loadi @ 0x3d4a0 (ctype idx 4)

; loadi @0x3d4a0
3d4a0: add %esi,%edx              ; vaddr = base + off
3d4a2: test $0x3,%dl             ; (vaddr & 3) ?
3d4a5: jne fault                  ;   -> cstub_vaddr_not_aligned  (exit 1)
3d4a7: mov base_addr(%rip),%rax
3d4ae: and $0xfffffffc,%edx       ; vaddr &= ~3
3d4b1: mov (%rdx,%rax,1),%eax     ; load 4 bytes
3d4b4: mov %eax,(%rdi)            ; *(uint32*)dst = word
3d4b6: ret
; storei @0x3d480 (same align check):
;   *(uint32*)(base_addr + (vaddr & ~3)) = (uint32)edi

Canonical scalar-AR get/set: a 4-byte word with 4-byte alignment ENFORCED — a misaligned address is an unconditional hard-fault. [OBSERVED · HIGH]

The fault path:

; cstub_vaddr_not_aligned(uint32 vaddr) @0x6d0
6d0: ... fprintf(stderr, FMT, vaddr); exit(1)

with FMT @ .rodata 0x48d80:

"Error: The config takes exception when the address of an load/store is unaligned. The address 0x"

[OBSERVED · HIGH]

(b) Predicate vbool1Function_TIE_xt_ivp32_vbool1_loadi @ 0x810 (ctype 49)

810: lea (%rsi,%rdx,1),%ecx       ; vaddr = base + off
813: mov base_addr(%rip),%rax
81a: mov %ecx,%edx
81c: shl $0x3,%ecx                ; vaddr << 3
81f: and $0xfffffffc,%edx         ; word addr = vaddr & ~3
822: and $0x18,%ecx               ; shift = (vaddr*8) & 0x18  = (vaddr&3)*8
825: mov (%rdx,%rax,1),%eax       ; word = *(uint32*)(base_addr + word_addr)
828: movl $0x0,0x4(%rdi)          ; *(dst+4) = 0   (clear upper slot word)
82f: shr %cl,%eax                 ; word >>= shift
831: and $0x1,%eax                ; & 1
834: mov %eax,(%rdi)              ; *(uint32*)dst = predicate bit (0/1)
836: ret

1-bit predicate get: select the byte lane by (vaddr&3)*8, extract bit 0, normalize to {0,1} in a 32-bit host slot (and zero the upper slot word). storei (0x1540) is the inverse: replicate bit 0 of *src to bit 0 of each byte lane, select the lane, write one byte back. [OBSERVED · HIGH]

(c) Vector vec2Nx8 (512-bit) — loadi @ 0x3e7f0 / storei @ 0x3d5b0 (ctype 21)

3e7f0: lea (%rsi,%rdx,1),%eax
3e7f3: add base_addr(%rip),%rax
3e7fa: movdqu (%rax),%xmm3        ; 4x unaligned 16-byte loads
3e7fe: movdqu 0x10(%rax),%xmm2
3e803: movdqu 0x20(%rax),%xmm1
3e808: movdqu 0x30(%rax),%xmm0
3e80d: movups %xmm3,(%rdi)        ; 4x writes -> 64-byte dst
3e810: movups %xmm2,0x10(%rdi)
3e814: movups %xmm1,0x20(%rdi)
3e818: movups %xmm0,0x30(%rdi)
3e81c: ret

Flat 64-byte memcpy (512 bits) between the host vec slot and target memory. No alignment check — wide vectors use unaligned movdqu/movups. storei is the exact inverse. [OBSERVED · HIGH]

QUIRK. Scalars hard-fault on misalignment (a); vectors silently accept any address via unaligned moves (c). The single/multi-word flag (§1.1) is the predicate that splits these two paths in the sim.

(d) Wide spill wvecspill (1536-bit) — Function_TIE_xt_ivp32_xb_wvecspill_loadi @ 0x41d90

$ nm -S libctype.so | rg wvecspill_loadi
0000000000041d90 000000000000045a t Function_TIE_xt_ivp32_xb_wvecspill_loadi

The largest stub0x45a = 1,114 bytes. It copies 192 bytes (1536 bits) from base_addr+vaddr to *dst in three 0x40-byte chunks (offsets 0 / 0x40 / 0x80), moving DWORD-by-DWORD through GPRs and a stack staging area. This is how the codec materialises the 1536-bit accumulator spill/reload host-side. [OBSERVED · HIGH]

(e) Lane int64Function_TIE_xt_ivp32_xb_int64_loadi @ 0x3dca0

3dca0: add %esi,%edx              ; vaddr
3dca2: mov %edx,%eax
3dca4: shr $0x2,%edx
3dca7: and $0xffffffc7,%eax       ; addr = vaddr & 0xffffffc7
3dcaa: add base_addr(%rip),%rax
3dcb1: and $0xe,%edx              ; lane = (vaddr>>2) & 0xe
...                               ; copy the selected 64-bit lane

Lane-relative sub-addressing: it picks a 64-bit value within a vector lane group, distinct from the scalar AR uint64 {hi,lo} 2-word form (ctype 6). [OBSERVED · HIGH]

(f) Convert scalar→predicate — int32_rtor_xb_int32pr @ 0x113b0

113b0: mov %esi,(%rdi)            ; *(int32*)dst   = value
113b2: sar $0x1f,%esi            ; sign = value >> 31
113b5: mov %esi,0x4(%rdi)        ; *(int32*)(dst+4) = sign
113b8: ret

The b32_pr predicate is the {value, sign} 64-bit form. rtor here is a register-value convert (src in esi). [OBSERVED · HIGH]

(g) Convert scalar→vector (widen/splat) — uint8_rtor_xb_vec2Nx8 @ 0x3070

3070: mov %esi,-0xc(%rsp)
3074: movd -0xc(%rsp),%xmm3
307a: movdqa cstub_Xm_ncore2gp_table__xt_bytelane_lsb+0x40(%rip),%xmm0
3082: pshufd $0x0,%xmm3,%xmm1     ; splat scalar across lanes
3087: pand %xmm1,%xmm0            ; byte-lane mask via the .rodata LUT
308f: pslld $0x18,%xmm2           ; + lane shifts (8/0x10/0x18)
3094: por  %xmm2,%xmm0
...                               ; writes 64 bytes (4x movups) to *dst

Broadcasts the scalar esi across SIMD lanes using the .rodata LUT cstub_Xm_ncore2gp_table__xt_bytelane_lsb (region near 0x52300) — the scalar→vec widening convert. [OBSERVED · HIGH]

(h) Convert vector→scalar (narrow) — vec2Nx8_rtor_int8 @ 0x113c0

113c0: movsbl (%rsi),%eax         ; sign-extend first byte of source VECTOR (rsi = ptr)
113c3: mov %eax,(%rdi)            ; *(int32*)dst = (int8)src[0]
113c5: ret

The inverse of (g): src is a pointer (rsi) because the source is wide. [OBSERVED · HIGH]

(i) Convert narrowing int32int16int32_rtor_xb_int16 @ 0xf2a0

f2a0: and $0xffff,%esi            ; value & 0xffff
f2a6: movl $0x0,0x4(%rdi)         ; zero-fill the rest of the wide dst slot
f2ad: movl $0x0,0x8(%rdi)
f2b4: mov %esi,(%rdi)            ; *(uint32*)dst = masked value
f2b6: movl $0x0,0xc(%rdi)        ; ...

Narrow-with-zero-extend into a vec-width destination slot. [OBSERVED · HIGH]

Roles summary — the cstub's get / set / move / convert

opkindhost realisation
GET (extract value from regfile repr)loadiread target memory → host slot; sub-word types also extract the bit/byte field (vbool1 in (b))
SET (insert value)storeiwrite host slot → target memory; sub-word types pack the bit/byte lane
MOVE (same-type)(no fn)a flat struct copy (vec→vec = 64-byte memcpy); the ISA has 47 move protos in the table, but the cstub realises them as the trivial copy, not a separate function
CONVERT (cross-type)rtorthe 205 rtor stubs widen (scalar→vec splat (g)), narrow (vec→scalar (h), int32→int16 (i)), sign/zero-extend (→predicate (f)), reinterpret (vec↔vecU 64-byte copy); driven by the per-source sorted convert list (§2.3)

[OBSERVED · HIGH]


5. Kind census — 299 Function_TIE_* and the proto-kind mapping

The library implements 299 Function_TIE_* stubs. Re-grounded against the binary:

$ nm libctype.so | rg -c 'Function_TIE_'          ; 299
$ nm libctype.so | rg -c 'Function_TIE_.*_rtor'   ; 205
$ nm libctype.so | rg -c 'Function_TIE_.*_loadi'  ; 47
$ nm libctype.so | rg -c 'Function_TIE_.*_storei' ; 47
$ for k in _mtor _rtom _move _loadx _storex _loadip _storeip _loadxp _storexp; do
    nm libctype.so | rg -c "Function_TIE_.*$k"; done   ; all 0
kindctype-proto rows (the ISA TABLE)libctype cstub fnshost realisation
rtor205205register VALUE convert stubs ✓
loadi4747base+off load, align-checked (scalar) ✓
storei4747base+off store ✓
move470trivial struct copy (no fn)
mtor710TABLE only
rtom640TABLE only
loadip / storeip31 / 310host: loadi/storei + ptr update
loadx / storex27 / 270host: loadi/storei with reg offset
loadxp / storexp27 / 270not stubbed
sum651299

The match is the authoritative link. rtor 205 / loadi 47 / storei 47 are exactly the corresponding rows of the ISA's ctype_protos[] table. The cstub is the host C implementation of precisely those three proto rows; the nine other kinds have zero host stubs (the for-loop above prints 0 for every one). They live only in the libisa proto table (the instruction-sequence binding) — the native sim synthesises them caller-side: the +off in loadi/storei is the immediate, and indexed / post-increment addressing modes are computed by the caller before it calls loadi/storei; same-type moves are a trivial struct copy. [count match OBSERVED · HIGH; the "synthesised caller-side" mechanism for the 9 absent kinds INFERRED · MED]

Ftable index == ctype id (the second cross-check): the populated loadi entries decode byte-exact to the ISA ctype indices — off/8 gives idx4=uint32, idx18=valign, idx19=vboolN, idx20=vbool2N, idx21=vec2Nx8, idx22=vec2Nx8U, idx49=vbool1, idx58=int64(vec), idx60/61=int32pr/int64pr, idx63=wvecspill — in exact agreement with the ctype/coproc ISA page. [OBSERVED · HIGH]


6. Relation to the ISS value oracle (does fiss/cas call libctype?)

No. libctype is an independent host type-system / value-marshalling layer:

  • libctype.so dynamic imports = libc only (malloc, fprintf, exit, stderr); it imports no cstub_/Function_TIE_ from elsewhere, and no .so in ncore2gp/config imports it (nm -D … U cstub_ is empty across configs).
  • libisa-core.so and libcas-core.so contain zero cstub_/Function_TIE_ symbols.
  • The cstub_*_function strings appear in XtensaTools/lib/tc/nativesim (the native TIE instruction-set simulator) and XtensaTools/bin/xt-gdb (the debugger) — those are the consumers. nativesim resolves cstub_loadi_function etc. and drives the per-ctype marshalling while executing TIE semantics on the host.

Division of labour:

  • libcas-core / libfiss-base — the cycle/datapath simulators: they compute values (the fiss xdref value leaves of fiss-datapath-oracle.md) and timing (the per-operand pipeline stages / scoreboard). cas has its own _move symbols (cas-internal — unrelated to the ctype cstub).
  • libisa-core — the static ISA tables (formats/slots/regfiles/states/ ctypes[64]/ctype_protos[651]): the declarations, including the proto names whose accessors libctype implements. See ../isa/core/ctype-coproc-funcunit.md and ../abi/scalartype-dtype-rosetta.md.
  • libctype — the host-side value codec: marshals a ctype value between the host reg-struct and the host-simulated target memory (base_addr window) for the native sim / debugger. It is the get/set/move/convert implementation, not the arithmetic oracle and not the timing model.

The capstone iss-oracle-synthesis.md folds these three back together. [OBSERVED · HIGH]


7. Wide-type handling

widthtype(s)host copy path
512-bit (64 B)vecmovdqu/movups, no align check (§4c)
1024 / 2048-bitwvecsame flat SIMD copy, scaled (8/16 moves)
1536-bit (192 B)wvecspillDWORD-granular GPR + stack copy in 3× 0x40 chunks — the heaviest stub, 0x45a B (§4d)
64-bit laneint64 (vec regfile)lane-relative sub-addressing vaddr & 0xffffffc7, lane = (vaddr>>2)&0xe (§4e)
64-bit ARuint64/int64 (multi_word=1, 2 regs){hi,lo} 32-bit pair
sub-32-bitvbool1 (1 b), int8/int16bit/byte-lane extract+pack on get/set; mask+zero-extend on convert (§4b, 4i)

The cstub_ctype_multi_word[64] flag (§1.1) tells the sim which types need the multi-word (> 1 host word) copy path: idx 6,7,18..63 = 1. [OBSERVED · HIGH]


8. Per-gen stability

  • Only one libctype.so ships in the corpus (ncore2gp/config); no sibling per-gen copy exists (fd libctype.so finds this binary + the IDA sidecars only). [OBSERVED · HIGH]
  • The structure is config-generated: the TIE compiler emits ctypes_init and the Function_TIE_* set from the same ctype/proto definitions that produced the 64-ctype / 651-proto ISA tables. Any gen with the same 64-ctype / 651-proto config yields the same 299-fn geometry (rtor205/loadi47/storei47, ftables[64], 24-byte rtor nodes). Adding/removing a ctype or a convert edge would shift the ftable index range and the rtor node count, but the dispatch mechanism — 3 ftable[ctypeid] + sorted convert list + base_addr window + multi_word flag + align-fault — is gen-invariant: it is the fixed XtensaTools TIE-cstub ABI (VERS_1.1, the 6 exports in libctype.def). [MED]
  • The exported ABI is a stable XtensaTools TIE interface; the generic XtensaTools/lib/libxtctype.so shares the same export names (a stripped ~10 KB interface stub — not the populated table; the populated one is this file). So the dispatcher surface is portable across Xtensa configs; only the populated tables differ. [MED]

9. Confidence / uncertainty ledger

HIGH (proven from bytes/disasm):

  • 6 exports + their disasm (set_base_address, loadi/storei/rtor/size, ctypes_init); all six match libctype.def / libctype.map.
  • ftable geometry: 3× .bss 0x200 = 64×8, stride 8, indexed by ctype id; cstub_ctype_multi_word .data[64] byte values; base_addr/frtor placement.
  • 24-byte rtor node {other_id@+0, fn@+8, next@+0x10}; sorted-list dispatch; 265 edges (rg -c call.*malloc = 265).
  • kind census rtor205/loadi47/storei47 and the 9-zero kinds — re-grounded by nm | rg -c; the count match to the ISA proto rows.
  • loadi ftable index decode byte-exact to the ISA ctype indices (idx 4, 18..63).
  • all §4 stub decodes with their masks/shifts/widths; the align-fault path (cstub_vaddr_not_alignedfprintf+exit(1), &3 check on scalars).
  • imports = libc only; no cas/isa/fiss linkage; consumers = nativesim + xt-gdb.
  • cstub_ctypes_init = 4,715 instructions (objdump count over the 0x54a4 span).

MED (strong inference):

  • the 9 absent proto kinds are synthesised caller-side from base_addr arithmetic (counts OBSERVED=0; the "caller computes addr/move" mechanism INFERRED).
  • ctypes_init is the sim-startup one-shot (role INFERRED from .bss + the consumer set).
  • per-gen invariance of the dispatch mechanism (single gen in corpus).

LOW: none material; every decoded fn / field / mask / count is anchored to read bytes.

CORRECTION (carried into §2.4). The task brief and the heading both say "libctype is NOT stripped, use DWARF" — but the binary has no .debug_* sections at all (only .symtab/.strtab). All struct offsets here are pinned from the disassembly displacements, not from addr2line/DWARF (which returns file:?). The result is identical (and arguably stronger): the offsets are read from the literal mov encodings that touch the fields.


10. Closure — what this layer covers

libctype.so is the host value-marshalling floor beneath the codec / native TIE simulator: the C-stub get(loadi) / set(storei) / move / convert(rtor) implementations of the ISA ctype value types, with

  • a 6-export VERS_1.1 dispatch ABI (libctype.def);
  • 3 ftables[64] indexed by ctype id at stride 8 (loadi 47 / storei 47 installed) + a per-source sorted rtor convert list (205 fns / 265 edges, 24-byte nodes);
  • a base_addr host↔target memory window + a 4-byte-align hard-fault on scalars;
  • the cstub_ctype_multi_word[64] single/multi-word flag (idx 6,7,18..63 = 1);
  • representative get/set/move/convert bit-layouts decoded (scalar AR, vbool1 predicate, 512-bit vec, 1536-bit wvec spill, lane int64, scalar↔vec convert);
  • a proto-kind mapping byte-exact to the ISA ctype_protos rows (rtor205/loadi47/storei47); the 9 other kinds are table-only, synthesised by the caller;
  • an independent host type-system layer (libc-only deps; consumed by nativesim/xt-gdb; not called by cas/fiss/isa) — orthogonal to the value+timing oracle.

Combined with the ctype/proto ISA tables (../isa/core/ctype-coproc-funcunit.md), the fiss/cas value+timing oracle (fiss-datapath-oracle.md), and the dtype Rosetta (../abi/scalartype-dtype-rosetta.md), the ISS lane is near-complete; the validation harness (Part 15) cross-checks these decodes against the live simulator.