Thomas Piacentini
← Workshop

Chapter

xrefs and the jump table

Original text · English

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 source
"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.
Original source
title: xrefs for the entry of _grade
source: capture/out/10-mcp-07-xrefs-grade.json
lang: json
note: The request arguments, the result, and two of the seven limitations.
---
"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 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 source
"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.
Original source
title: the one incoming reference of _grade
source: capture/out/06-function-grade.json
lang: json
note: Cut from incoming_references. The target_procedure block repeats the procedure block of section 4.2 and is cut.
---
"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,
      …
    }
  }
],

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 source
"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.
Original source
title: the two incoming references of _grade_label
source: capture/out/10-mcp-12-analyze-function-grade-label-O1.json
lang: json
note: Cut from incoming_references to the addresses and the reference types.
---
"source_address": "0x1000006ec",
"target_address": "0x100000588",
…
"type": "UNCONDITIONAL_CALL",
…
"source_address": "0x10000811c",
"target_address": "0x100000588",
"source_procedure": null,
…
"type": "DATA",

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
entry calls three of the four functions, the function starts table refers to all four, and nothing calls _grade because its body was inlined.
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. From the four analyze_function dossiers and capture/fixtures/ledger-arm64-O1.

Original SVG

Original source
id: fig-4-6
kind: tree
title: every reference to the four ledger functions
claim: entry calls three of the four functions, the function starts table refers to all four, and nothing calls _grade because its body was inlined.
caption: 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. From the four analyze_function dossiers and capture/fixtures/ledger-arm64-O1.

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.

Build Transfer Table Blocks in grade
ledger-x86_64-O0.o, lesson 33 fixture jmpq *%rax .rodata+0x0, five R_X86_64_PC32 relocations, targets 0x91, 0x88, 0x7f, 0x76, 0x76 8 with the table, 3 without
ledger-x86_64-O1.o, this capture none, one JA DAT_001000b4, .rodata+0x14, values 1, 2, 3, 4, 4 3
ledger-arm64-O1, this capture none, one b.hi 0x1000007d0 in __TEXT,__const, values 1, 2, 3, 4, 4 3

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
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.
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. From phases/13-tools-and-protocols/33-disassembly-and-control-flow/code/fixtures and capture/out/06-function-grade.json.

Original SVG

Original source
id: fig-4-5
kind: comparison
title: a jump table at -O0 and a value table at -O1
claim: 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.
caption: 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. From phases/13-tools-and-protocols/33-disassembly-and-control-flow/code/fixtures and capture/out/06-function-grade.json.

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.

Original source
- Resolve an `xrefs` address to its segment and load command before you call it a caller.
- Use `incoming_references` in the dossier to learn whether a reference is a call or data.
- Read `callers: []` on an exported function as a sign of inlining, then check the caller's body.
- Name what the bytes show: a jump table changes the graph, a value table changes a load.

Sources: capture/out/10-mcp-07-xrefs-grade.json, capture/out/06-function-grade.json, capture/out/10-mcp-12-analyze-function-grade-label-O1.json, capture/out/10-mcp-09-analyze-function-percent-change-O1.json, capture/out/10-mcp-11-analyze-function-total-O1.json, capture/out/06-function-grade-x86_64-O1.json, capture/out/04-inspect-macho.json, capture/fixtures/ledger-arm64-O1; phases/13-tools-and-protocols/33-disassembly-and-control-flow/code/fixtures/ledger-x86_64-O0.objdump.txt; rea 6.7.0 at 27dc42d: bridge/ghidra/ReaGhidraBridge.java, src/ghidra/GhidraProviderCapabilities.ts

Code and artifacts 1
What you take away

Saved in your notebook on this device.

Sources and limits

Reading checkpoints are self-reported. Test results are recorded separately and apply to the executed cases.

AI Engineering from Scratch · 3f9715c · MIT

Download this file ↓