4.4

xrefs and the jump table

xrefs lists the addresses that refer to one address, and the single reference to _grade shows what the optimizer did to the switch and its callers.

main calls grade_label, and grade_label calls grade. You ask xrefs who refers to _grade at 0x100000518 and get one address, 0x10000811a, which is not in any function. The answer is right, and it tells you two things about the build.

When you finish this section, you can read an xrefs result and find the kind of each reference in the dossier. You can also tell a jump table from a value table in two builds of grade.

The call

xrefs for the entry of _gradejsonFull log
"name": "xrefs",
"arguments": {
  "address": "0x100000518"
}
…
"normalized_result": [
  "0x10000811a"
],
…
"limitations": [
  …
  "The direct address list projects exact Ghidra references to one address but does not expose their kinds; procedure_references and analyze_function preserve available kind metadata.",
  "Synthetic Ghidra entry-point references without actionable memory sources are omitted.",
  …
],
The request arguments, the result, and two of the seven limitations.

The bridge asks Ghidra's ReferenceManager for every reference to the address. It skips the synthetic entry-point references, collects the distinct source addresses, and sorts them. The result is addresses only. The incoming_references facet of the dossier keeps the kind of each reference, call, jump, or data.

What is at 0x10000811a

The address is in __LINKEDIT, which starts at 0x100008000 and maps file offset 32768. The difference 0x11a is 282, so the byte is at file offset 33050. Section 3.2 lists LC_FUNCTION_STARTS at offset 33048 with 8 bytes. The reference points into the function starts table.

Function starts: a table of unsigned LEB128 deltas, one per function, that dyld and debuggers use to find function entries in a stripped image. The eight bytes in the fixture are 80 0a 18 3c 34 48 00 00. The first delta is 0x500, which gives 0x100000500, _percent_change. The second, at offset 33050, is 0x18, which gives 0x100000518, _grade. Then 0x3c gives _total, 0x34 gives _grade_label, and 0x48 gives entry. Ghidra's Mach-O loader marks each entry as a data reference to the function it names, and the dossier shows exactly that.

the one incoming reference of _gradejsonFull log
"incoming_references": [
  {
    "source_address": "0x10000811a",
    "target_address": "0x100000518",
    "source_procedure": null,
    …
    "kind": {
      "available": true,
      "provenance": "ghidra-reference-manager",
      "type": "DATA",
      "flow": false,
      "call": false,
      "jump": false,
      "data": true,
      …
    }
  }
],
Cut from incoming_references. The target_procedure block repeats the procedure block of section 4.2 and is cut.

source_procedure is null because no function owns the table. The kind is DATA, and call is false. So nothing in the code calls _grade. Compare _grade_label, whose dossier lists two incoming references. One is an UNCONDITIONAL_CALL from 0x1000006ec inside entry. The other is a DATA reference from 0x10000811c, the fourth entry of the same table.

the two incoming references of _grade_labeljsonFull log
"source_address": "0x1000006ec",
"target_address": "0x100000588",
…
"type": "UNCONDITIONAL_CALL",
…
"source_address": "0x10000811c",
"target_address": "0x100000588",
"source_procedure": null,
…
"type": "DATA",
Cut from incoming_references to the addresses and the reference types.

The call that the source makes, grade_label to grade, is gone. At -O1 clang inlined grade into grade_label. The assembly of _grade_label repeats the whole multiply and compare sequence of section 4.3 at 0x100000588 to 0x1000005a4. The dossier of _grade says the same from the other side: callers is an empty list, and relationship_coverage.status is complete. Ghidra returned every resolved call reference, and none of them targets _grade. _grade still exists as a function because it is exported, and the only thing that refers to it is a table the linker wrote.

Fig. 4.5every reference to the four ledger functionstree
Every reference to the four ledger functions Four rows, one per function. In the middle, the function with its entry and body size. On the left, the call site inside entry that reaches it: 0x1000006a0 for _percent_change, 0x1000006bc for _total, 0x1000006ec for _grade_label, and none for _grade because its body was inlined into _grade_label. On the right, the LC_FUNCTION_STARTS entry that refers to it as data: 0x100008118, 0x10000811a, 0x10000811b and 0x10000811c, with ULEB128 deltas 0x500, 0x18, 0x3c and 0x34. CALL SITES IN ENTRY FUNCTION LC_FUNCTION_STARTS ENTRIES _percent_change 0x100000500 · 24 B entry 0x1000006a0 UNCONDITIONAL_CALL 0x100008118 delta 0x500 · DATA _grade 0x100000518 · 60 B no call site inlined into _grade_label 0x10000811a delta 0x18 · DATA _total 0x100000554 · 52 B entry 0x1000006bc UNCONDITIONAL_CALL 0x10000811b delta 0x3c · DATA _grade_label 0x100000588 · 72 B entry 0x1000006ec UNCONDITIONAL_CALL 0x10000811c delta 0x34 · DATA
entry calls three of the four functions, the function starts table refers to all four, and nothing calls _grade because its body was inlined. Read across each row. Green boxes on the left are call sites inside entry, violet boxes are the functions, green boxes on the right are entries of LC_FUNCTION_STARTS.

The jump table that is not there

Every dossier in the captures has "jump_tables": [] in its native_api facet, and the profile names the contract jump_table_evidence: "typed-case-default-blocks-v1". Ghidra would fill the field if it found one. It found none, because at -O1 neither compiler emitted a jump table for the switch in grade. Both turned the five cases into a table of the five return values and one bounds check.

BuildTransferTableBlocks in grade
ledger-x86_64-O0.o, lesson 33 fixturejmpq *%rax.rodata+0x0, five R_X86_64_PC32 relocations, targets 0x91, 0x88, 0x7f, 0x76, 0x768 with the table, 3 without
ledger-x86_64-O1.o, this capturenone, one JADAT_001000b4, .rodata+0x14, values 1, 2, 3, 4, 43
ledger-arm64-O1, this capturenone, one b.hi0x1000007d0 in __TEXT,__const, values 1, 2, 3, 4, 43

Jump table: a table of code addresses that an indirect jump indexes, so one instruction reaches one of several blocks. Value table: a table of data that a load indexes, so one instruction returns one of several values. The first changes the control-flow graph. The second changes one load. At -O0 the x86-64 object has the first, and in the object file its five entries are zero bytes with five relocations in .rela.rodata. At -O1 both builds have the second.

The outgoing_references of _grade name the two edges the dossier does see. One is a CONDITIONAL_JUMP from 0x100000538 to 0x10000054c. The other is a DATA read from 0x100000544 to 0x1000007d0. The referenced_names facet labels the targets LAB_10000054c and DAT_1000007d0. The decompiler in section 4.6 keeps the second as a byte address and never lists the five values.

Fig. 4.6a jump table at -O0 and a value table at -O1comparison
A jump table at -O0 and a value table at -O1 Two columns. Left, grade in the lesson 33 fixture ledger-x86_64-O0.o: a bounds check, a leaq with a relocation against .rodata, a scaled load, an add and jmpq through rax; the table in .rela.rodata holds five relocations whose targets are 0x91, 0x88, 0x7f, 0x76 and 0x76, so the graph has 8 blocks and 11 edges. Right, _grade in the arm64 -O1 capture: a compare and b.hi, an adrp and add that form 0x1000007d0, and one ldr that returns one of the five values 1, 2, 3, 4, 4; the dossier has 3 blocks, 2 edges and an empty jump_tables list. A · GRADE AT -O0, X86-64 OBJECT B · _GRADE AT -O1, ARM64 EXECUTABLE lesson 33 fixture ledger-x86_64-O0.o capture 06-function-grade.json 5d: subl $0x4, %eax 60: ja 0x9a 66: leaq (%rip), %rcx R_X86_64_PC32 .rodata-0x4 6d: movslq (%rcx,%rax,4), %rax 71: addq %rcx, %rax 74: jmpq *%rax 0x534: cmp w8, #0x4 0x538: b.hi 0x10000054c 0x53c: adrp x9, 0x100000000 0x540: add x9, x9, #0x7d0 0x544: ldr w0, [x9, w8, UXTW #0x2] 0x548: ret 0x54c: mov w0, #0x0 .rela.rodata: five relocations targets 0x91 0x88 0x7f 0x76 0x76 __const at 0x1000007d0 u32 values 1 2 3 4 4 · file offset 2000 jmpq *%rax reaches 5 blocks 8 blocks and 11 edges with the table ldr returns one value 3 blocks · 2 edges · jump_tables []
At -O0 an indirect jump through five relocated entries reaches five case blocks, and at -O1 one load from five values replaces all of them. Read each column top to bottom. The left column comes from the lesson 33 fixture ledger-x86_64-O0.o, the right from the arm64 dossier. The green boxes are data in the file, the violet boxes are what Ghidra saw.

Lesson 33 recovers the -O0 table from the relocations with the rule S + A - P. It shows the control-flow graph with and without the table: 8 blocks and 11 edges against 3 blocks and one unresolved jump.

When Ghidra does not resolve a jump table, the case blocks are missing from callers, callees, and basic_blocks. The dossier limitation says that unresolved computed flows are not represented as edges. The unresolved_calls facet lists each call site whose destination has no known procedure. An unresolved indirect jump through a table is not a call, so it does not appear in that facet. An empty jump_tables list does not prove the function has no switch. Read the transfer instruction and the table bytes, as you did here.

rea (rea-agents) with the Ghidra provider rea-agents 6.7.0, Ghidra 12.1.4 · 27dc42d · 2026-10-18 · Edition 2026.10