Skip to main content
Glama

get_vector_table

Read a firmware interrupt vector table to show which handler services each interrupt, find gaps, and list unhandled slots.

Instructions

Read the interrupt vector table, and say what services each interrupt.

The vector table is how an interrupt reaches code. Nothing CALLS a handler — the hardware reads a slot and jumps — so a handler has no caller, and every other tool shows it as unreferenced. This tool reads the table itself, from the assembly the build compiles.

Use it to answer "which interrupts does this firmware service", to find the handler for one interrupt, or to find the interrupts that reach the trap loop.

The slot number is the position in the table. What that position means belongs to the architecture, not to the index. On Cortex-M slots 0 to 15 are the system exceptions and slot 16 + n is external interrupt n, so TIM2_IRQHandler in slot 44 is TIM2_IRQn = 28. On other architectures the same position means something else.

The status field says what services the interrupt:

  • "c" — a definition outside assembly. Code runs. When the index also holds the weak definition that this one replaced, the row has overridden with its file and line.

  • "assembly" — an assembly definition that is not an alias. Assembly services the interrupt.

  • "unhandled" — an assembly name that is an alias (.thumb_set) of another symbol, and nothing overrode it. The row holds aliases, the name, file and line of the alias target. A CMSIS startup file aliases each unserviced interrupt to Default_Handler, which is an infinite loop. If the interrupt fires, the device stops.

  • "runtime" — the image holds that same alias, and the code installs a real handler into this slot by calling NVIC_SetVector. A target that defines CMSIS_VECTAB_VIRTUAL keeps its vector table in RAM and fills it that way, so the interrupt IS serviced once the registering code has run — and not before it. The row holds installed, one entry per call site with the handler name, its file and line, and at, where the registration happens. Follow at to see WHEN it happens: on the Mbed project us_ticker_irq_handler reaches slot 25 from us_ticker_init, so the tick source is unserviced until the ticker starts. A row with a real static definition keeps its own status and still carries installed.

  • "data" — the slot holds an address BUILT from the symbol it names (.word z_main_stack + CONFIG_MAIN_STACK_SIZE), so the symbol is a base and nothing jumps to it. On Cortex-M this is slot 0 of a Zephyr table: the initial stack pointer. Do not read it as code.

  • "linker" — the linker script gives the address and no compiled file defines the name. Slot 0 holds the initial stack pointer, not a handler, and looks like this. When the index read the script, file and line name the assignment in it — on the Mbed project, __StackTop at .link_script.ld:148. Do not read this row as code: there is no function to follow.

  • "dispatcher" — the slot reaches a function that holds more than one slot of this table AND calls through a pointer. It cannot be servicing one particular interrupt; it decides at run time where to go. Zephyr fills every external IRQ slot with _isr_wrapper, which reads the interrupt number and jumps through _sw_isr_table. Follow it: get_symbol_context on the name, then find_references on the table it uses. A handler that merely calls one registered callback is NOT this — it holds a single slot and keeps "c".

A "c" row with overridden is the CMSIS pattern: the startup file defines each handler weakly, the project defines the same name again, and the linker keeps the strong one.

Three sources are read, and source says which one a row came from:

  • "assembly" — a table of address words, .word or .long in a vector section, which is what a CMSIS startup file writes.

  • "c" — an array whose elements are addresses of functions, which is what a build that generates its table produces. Zephyr writes its external interrupts this way, with gen_isr_tables.py. These rows also carry table_name, the array the slot belongs to.

  • "build" — the registration the build itself recorded, for a slot the other two could not name. A generator writes a resolved ADDRESS into every slot that is in use, so those slots have no name in the source at all — and they are the interrupts the firmware actually services. Measured on an nRF54L application: 284 of 290 slots name the spurious stub, and the 6 without a name are IRQ 89, 198, 219, 228, 269 and 270, which these rows fill in.

    Such a row can carry argument, the symbol the build passes to the handler. Read it as an argument and not as a second handler: behind the nrfx_isr shim it is the real worker (nrfx_power_clock_irq_handler), while for another driver it is the device (__device_dts_ord_116). When the build enables run-time registration (CONFIG_DYNAMIC_INTERRUPTS), the interrupts dict says so, because an interrupt connected at run time leaves nothing to read and the rows are then not all of them. A "build" row has file empty and line 0 when the handler name has no single definition in the index.

Recognition is by shape, never by name, so any array of function addresses is reported and the row names its table. A table of interrupt handlers and a table of state machine steps are the same construct, and table_name is how they are told apart.

Slot numbers are not joined across tables. Each slot is the index inside its own table, so two tables both start at 0 — read slot together with table_name and source. They are not renumbered into one run because the index does not hold the length of the assembly table, only its occupied slots, and an offset derived from that would be silently wrong for every entry of a 290-entry table.

A coverage row follows the slots for each table longer than the number of slots that name a function. It says how many of the declared elements were named and which slot numbers were not, because a name is not always there to be read: an element can be a zero, or an address the linker resolved before the table was written.

Read it in both directions. A hole in a table of handlers is a vector nothing services. A hole in Zephyr's _sw_isr_table is the opposite — measured on an nRF54L application, 284 of 290 slots name the spurious stub and the 6 without a name are the interrupts in use. The tool reports where to look; which meaning applies depends on the table.

An interrupts row answers "which are unserviced" wherever the build recorded its registrations, and it is the answer under unhandled_only too. The row-level unhandled status is read from an alias edge, which a CMSIS startup writes and a generator does not — measured, zero unhandled rows on all eleven images of a Zephyr project against 39 to 72 on four CMSIS and Mbed ones. The registrations settle it from the other side: what the build connected is the whole list, so anything else has nothing servicing it, and no handler has to be recognised by name.

The complement is taken over the length of the table, NOT over the slots that hold a stub. Measured on an mcuboot image: its software table names 44 of 48 slots, and one of those 44 is uarte_0_direct_isr, an interrupt wired straight into the vector table. It IS serviced, and counting stubs would report it as not.

What is still not covered: an architecture that builds its table from branch instructions (arm64, Xtensa, MIPS) writes no table of addresses in either form. A handler whose address the build resolved at link time has no name to report either — coverage names its slot but not the function. For an interrupt this tool cannot show, find_references on the handler name still gives every reference the index holds.

Read-only. No side effects. Requires an index of the assembly and the reference index (fw-context index — refs on by default).

Args: project_root: Project root. Auto-detected if omitted. project: Project name or project_id — call list_projects to get them. Use it to ask about a project that is not the project of the current directory. It is an alternative to project_root, which takes a root path. Give one of the two, not both. unhandled_only: When True, return only the "unhandled" slots. limit: Maximum slots (default 400, max 1000). offset: Skip this many slots. Reads the next page; the page notice names the offset to use. variant: Variant of the build (a board, or a PlatformIO environment). On a project of more than one build, give the arguments of one build as the refusal lists them: the variant, the image, or the two. fw-context does not select a build. One query answers for ONE build. image: Image of the build: a separate program, such as the application or the bootloader. Give it when the build has an image name.

Returns: The page notice {total, offset, shown, more, hint} comes before the rows of the answer: total counts every slot of every table. Then a list of slot dicts sorted by source, then table, then slot. Each holds: slot (int), name, file, line, source ("assembly", "c" or "build"), status ("c", "assembly", "unhandled", "runtime", "data", "linker" or "dispatcher"), and table_file and table_line (where the slot is written). Every path in this answer is absolute, at included. A "c" source row also holds table_name and table_usr. A "c" status row can hold overridden, a dict with file and line. An "unhandled" row holds aliases, a dict with name, file and line. Any assembly row can hold installed, a list of dicts with name, file, line and at.

Never empty: one dict with ``error`` (no index) or ``info`` (no
vector table in this build).  Check both keys first.  A dict with
``coverage`` follows the slots for each table that has unnamed
elements, and a dict with ``interrupts`` says which are connected
and which are not — the latter in both modes.  Neither is subject
to the page: they describe the whole table, the longest table is
where they matter most, and they come after the slots of each
page.  A page after the last slot gives one ``info`` dict that
names the total, and then those two.

Input Schema

NameRequiredDescriptionDefault
imageNoImage of the build: a separate program, such as the application or the bootloader. On a project of more than one build, give it when that build has an image name, as the refusal lists it.
limitNoMaximum slots (default 400, max 1000).
offsetNoSkip this many slots. Reads the next page of a long table.
projectNoProject name or project_id — call list_projects to get them. Use it to ask about a project that is not the project of the current directory. It is an alternative to project_root, which takes a root path. Give one of the two, not both.
variantNoVariant of the build (a board, or a PlatformIO environment). On a project of more than one build, give the arguments of one build as the refusal lists them: the variant, the image, or the two. fw-context does not select a build for you. One query answers for ONE build.
project_rootNoProject root. Auto-detected if omitted. This field also accepts a project name or a project_id, but project is the clear field for those.
unhandled_onlyNoReturn only the slots with status 'unhandled' (an alias of a default handler, nothing installed at run time).

Output Schema

NameRequiredDescriptionDefault
resultYes

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed5 schema fields changedv0.32.1
    • changedInput schema / properties / image / description
      Previous value: -"Sysbuild image within the variant. Required when the variant holds several: each image is a separate program."New value: +"Image of the build: a separate program, such as the application or the bootloader. On a project of more than one build, give it when that build has an image name, as the refusal lists it."
    • changedInput schema / properties / limit / description
      Previous value: -"Maximum slots (default 400)."New value: +"Maximum slots (default 400, max 1000)."
    • addedInput schema / properties / offset
      Added value: +{
      +  "default": 0,
      +  "description": "Skip this many slots. Reads the next page of a long table.",
      +  "minimum": 0,
      +  "title": "Offset",
      +  "type": "integer"
      +}
    • changedInput schema / properties / unhandled_only / description
      Previous value: -"Return only the slots that reach the default handler."New value: +"Return only the slots with status 'unhandled' (an alias of a default handler, nothing installed at run time)."
    • changedInput schema / properties / variant / description
      Previous value: -"Build variant (multi-build project). Omit to use default_variant. One query answers for ONE build."New value: +"Variant of the build (a board, or a PlatformIO environment). On a project of more than one build, give the arguments of one build as the refusal lists them: the variant, the image, or the two. fw-context does not select a build for you. One query answers for ONE build."
  2. Changed3 schema fields changedv0.32.0
    • changedInput schema / properties / image / description
      Previous value: -"Sysbuild image name within the variant (multi-project). Omit for all images of the variant."New value: +"Sysbuild image within the variant. Required when the variant holds several: each image is a separate program."
    • addedInput schema / properties / limit / minimum
      Added value: +1
    • changedInput schema / properties / variant / description
      Previous value: -"Build variant name (multi-project). Omit to use default_variant or fail-closed. Use '*' for all variants."New value: +"Build variant (multi-build project). Omit to use default_variant. One query answers for ONE build."
  3. Addedv0.30.0

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden and does so: 'Read-only. No side effects. Requires an index of the assembly and the reference index (fw-context index — refs on by default).' It also discloses non-obvious behavior — slot numbering is not joined across tables, coverage/chunking of pages, rows that look like code but are data or linker symbols, and runtime-installed handlers that are unserviced until registration runs.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The purpose and usage are front-loaded before the reference material, and headers make it navigable. But the body is enormously oversized and spends thousands of words enumerating return-value statuses and fields despite an output schema existing — much of that does not earn its place in the selection/invocation path.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a high-complexity tool with no annotations, the description covers prerequisites, build/variant/image scoping, pagination, error/info dicts, and pagination-independent coverage/interrupts rows. Nothing an agent needs to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all seven parameters, and the Args section largely restates them. It adds marginal value (e.g., project vs project_root 'give one of the two, not both'), but the baseline of 3 applies when the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence names a specific verb and resource ('Read the interrupt vector table, and say what services each interrupt') and immediately explains why the tool exists relative to siblings ('a handler has no caller, and every other tool shows it as unreferenced'). An agent can distinguish this from lookup_symbol/find_references/dead_code without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit use cases are given ('which interrupts does this firmware service', 'find the handler for one interrupt', 'the interrupts that reach the trap loop'), plus an explicit fallback when the tool cannot help: 'For an interrupt this tool cannot show, find_references on the handler name still gives every reference the index holds.' Naming the alternative and the condition that selects it satisfies the top bar.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.