get_vector_table
Reads the interrupt vector table to show which interrupts a firmware services, including handler status, unhandled traps, and runtime-installed vectors.
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 hasoverriddenwith its file and line."assembly"— a strong assembly definition. Assembly services the interrupt."unhandled"— a weak assembly definition that nothing overrode. A CMSIS startup file makes this an alias ofDefault_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 callingNVIC_SetVector. A target that definesCMSIS_VECTAB_VIRTUALkeeps 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 holdsinstalled, one entry per call site with the handler name, its file and line, andat, where the registration happens. Followatto see WHEN it happens: on the Mbed projectus_ticker_irq_handlerreaches slot 25 fromus_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 carriesinstalled."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,fileandlinename the assignment in it — on the Mbed project,__StackTopat.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_contexton the name, thenfind_referenceson 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.
Two sources are read, and source says which one a row came
from:
"assembly"— a table of address words,.wordor.longin 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, withgen_isr_tables.py. These rows also carrytable_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 thenrfx_isrshim 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, a dict withinfosays so, because an interrupt connected at run time leaves nothing to read and the rows are then not all of them.
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
(fw-context index).
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).
variant: Build variant (multi-build project). Omit to use
default_variant. One query answers for ONE build.
image: Sysbuild image within the variant. Required when the
variant holds several: each image is a separate program.
Returns:
list of 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). A "c" source row also holds
table_name and table_usr. A "c" status row can hold
overridden, a dict with 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 ``limit``: they describe the whole table, and the longest table
is where they matter most. When more slots exist than ``limit``,
a dict with ``truncated`` sits between the slots and those two,
saying how many slots are not shown.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| image | No | Sysbuild image within the variant. Required when the variant holds several: each image is a separate program. | |
| limit | No | Maximum slots (default 400). | |
| project | No | 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. | |
| variant | No | Build variant (multi-build project). Omit to use default_variant. One query answers for ONE build. | |
| project_root | No | Project 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_only | No | Return only the slots that reach the default handler. |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| result | Yes |