What the symbol table lists¶
Level: 201 · for anyone who has opened Ghidra's Symbol Table and its filter dialog and wondered what the checkboxes select
One line: Ghidra's Symbol Table lists every name in a program with the place the name came from — the file, auto-analysis, you, or nobody — and its Symbol Table Filter dialog is a query in three parts: a checked source, and a checked type, and the advanced filters, which are or-ed together and wave through every type they do not apply to; run through Ghidra's own filter class on a real executable, that is why Unreferenced lists none of the program's functions, not even the one nothing calls, and Not In Memory lets every function through.
The program¶
One of each kind of name, and a main that uses most of them — demo/symbols.c:
/* A program with one of each kind of name Ghidra's Symbol Table lists -- the
file this page builds, strips, and hands to Ghidra's headless analyzer. */
#include <stdio.h>
/* Global data with a value. */
int counter = 7;
/* A string, and a pointer seven bytes into it: an address inside a data item
rather than at its start. */
const char greeting[] = "hello, symbols";
const char *tail = greeting + 7;
/* static: a name only this file can use -- and still a name in the file. */
static int helper(int x)
{
return x * 2;
}
int twice(int x)
{
return helper(x);
}
/* Reached only through the table below: no call instruction names it. */
int via_table(int x)
{
return x + counter;
}
int (*const table[])(int) = { via_table };
/* Nothing calls this, and nothing holds its address. */
int orphan(int x)
{
printf("nobody calls this: %d\n", x);
return x + 1;
}
int main(void)
{
printf("twice(21) = %d\n", twice(21));
printf("table[0](1) = %d\n", table[0](1));
puts(tail);
return 0;
}
| Name | What it is there to show |
|---|---|
counter |
global data with a value |
greeting, tail |
a string, and a pointer seven bytes into it — an address inside a data item rather than at its start |
helper |
static: a name no other file can use, and still a name in this one |
twice |
a function called the ordinary way |
via_table, table |
a function whose address sits in a table, so that no call instruction names it |
orphan |
a function nothing calls and nothing points to |
Build it, run it, and ask the file which names it holds — the program's own, split into global and local by the case of the letter nm prints beside each, and the library functions it calls — then strip it and ask again:
Verified output of symbol_table_names_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
$ cc -std=c17 -Wall -Wextra -O0 -o symbols symbols.c
$ ./symbols
twice(21) = 42
table[0](1) = 8
symbols
$ names symbols
global: counter greeting main orphan table tail twice via_table
local: helper
$ imports symbols
printf puts
$ grep -c -a objc_msgSend_rtp symbols
0
$ strip symbols
$ names symbols
global:
local:
$ imports symbols
printf puts
$ grep -c -a 'nobody calls this' symbols
1
$ ./symbols
twice(21) = 42
table[0](1) = 8
symbols
helper is the one local name: static made it one, and the symbol table keeps it anyway. strip removes every name the program defines and keeps the two it uses from a library — on Linux in the dynamic symbol table, on a Mac in the ordinary one — because the dynamic linker looks them up by name. The string and the program are still there. And no byte of the file spells objc_msgSend_rtp, which matters in a moment.
The table¶
In the Code Browser, Window → Symbol Table opens it. demo/symbol_table.sh builds the program, hands it to the headless analyzer, and runs SymbolTableTour.java, which prints the rows the window would show and then works through everything else on this page:
#!/usr/bin/env bash
# Build symbols.c, hand the program to Ghidra's headless analyzer, and print its
# Symbol Table as SymbolTableTour.java reads it: the rows, the queries the
# Symbol Table Filter dialog builds -- run through Ghidra's own filter class --
# and what renaming, deleting and pinning a symbol do.
#
# demo/symbol_table.sh the clang -O0 build, with its symbol table
# demo/symbol_table.sh -s the same build, stripped
# demo/symbol_table.sh <binary> an executable built elsewhere -- by GCC in Docker, say
#
# Needs a Ghidra install. Homebrew's is found through `brew --prefix ghidra`;
# any other one through GHIDRA_INSTALL_DIR, the folder holding support/.
set -eu
here=$(cd "$(dirname "$0")" && pwd)
ghidra=${GHIDRA_INSTALL_DIR:-$(brew --prefix ghidra)/libexec}
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
if [ $# -ge 1 ] && [ -f "$1" ]; then
binary=$1
else
binary=$work/symbols
cc -std=c17 -Wall -Wextra -O0 -o "$binary" "$here/symbols.c"
if [ "${1:-}" = -s ]; then
strip "$binary"
fi
fi
"$ghidra/support/analyzeHeadless" "$work" symbols -import "$binary" \
-scriptPath "$here" \
-postScript SymbolTableTour.java "$work/tour.txt" \
-deleteProject > "$work/ghidra.log" 2>&1 || { cat "$work/ghidra.log"; exit 1; }
# A post-script that throws does not fail analyzeHeadless; a missing file does.
[ -s "$work/tour.txt" ] || { cat "$work/ghidra.log"; exit 1; }
cat "$work/tour.txt"
The script does not imitate the window; it borrows from it. The rows are gathered the way SymbolTableModel ↗ gathers them, each column is computed with the code AbstractSymbolTableModel ↗ uses for it, and every filter decision on this page is made by the dialog's own NewSymbolFilter ↗ — whose setters are package-private, because the dialog is their only caller, so the script reaches them by reflection.
Everything Ghidra printed on this page was produced on 2026-09-14 by Ghidra 12.1.3 from Homebrew on OpenJDK 25, on an x86-64 Mac running macOS 26 with Apple clang 21. CI does not run it — the runners have no Ghidra — so each block is a Real output fence that says so, and the program the fences describe is the one the verified block above built. First, the table as the window opens it:
== The table under those defaults: 17 rows ==
Name Location Type Namespace Source Refs Offcut
_printf EXTERNAL:00000001 External Function /usr/lib/libSystem.B.dylib Imported 1 0
_puts EXTERNAL:00000002 External Function /usr/lib/libSystem.B.dylib Imported 1 0
_objc_msgSend_rtp fffeff00 Label Global Imported 0 0
MACH_HEADER 100000000 Data Label Global Imported 0 0 non-primary
__mh_execute_header 100000000 Data Label Global Imported 1 0
_twice 1000005b0 Function Global Imported 3 0
_helper 1000005d0 Function Global Imported 2 0
_via_table 1000005e0 Function Global Imported 4 0
_orphan 100000600 Function Global Imported 2 0
_main 100000630 Function Global Imported 0 0 non-primary
entry 100000630 Function Global Imported 2 0
_printf 100000688 Thunk Function Global Imported 5 0
_puts 10000068e Thunk Function Global Imported 3 0
_greeting 100000694 Data Label Global Imported 1 1
_table 100001010 Data Label Global Imported 2 0
_counter 100002000 Data Label Global Imported 3 0
_tail 100002008 Data Label Global Imported 2 0
Seventeen rows, for a program with nine names of its own. Read them a column at a time.
Name and Location. _printf and _puts are there twice: as an External Function at EXTERNAL:00000001, an address in no memory at all that stands for wherever the library puts the function, and as a Thunk Function in the program, the stub that jumps there. _main and entry share 100000630: _main is the file's name for it, and entry is Ghidra's name for the address the file's entry-point load command gives. __mh_execute_header and MACH_HEADER share the file's first byte: the first is a symbol the linker writes into every Mach-O executable, and the second a label Ghidra's Mach-O loader adds, its MachoProgramBuilder ↗'s HEADER_SYMBOL. The same loader creates _objc_msgSend_rtp at fffeff00, from a constant of Ghidra's Objective-C 1 support, Objc1Constants ↗ — a name the grep above found nowhere in the file.
Type. Function, Thunk Function, External Function, Data Label, and a plain Label for _objc_msgSend_rtp, which labels an address with nothing at it. The non-label types — parameters, local variables, namespaces, classes, libraries — are rows too, once their boxes are checked.
Namespace is Ghidra's scope, not C's. Every name the program defines is Global, _helper included, although static made it local to its file; the two library functions are in /usr/lib/libSystem.B.dylib, the library the file says they come from. A symbol is in a namespace other than Global when it belongs to something — a parameter to its function, a method to its class.
Source is where the name came from, and every row here says Imported: from the file — its symbol table, its list of exports, its load commands — or from the loader that read it. The others are Analysis, a name auto-analysis gave something; User Defined, a name you gave it; Default, a name nobody gave it; and AI. SourceType ↗ has those five values, and the dialog has a checkbox for each, with Default split into functions and labels; the help page installed with 12.1.3 describes the dialog without its AI box.
Refs and Offcut are the Reference Count and Offcut Ref Count columns. A reference is to an address, and the table counts it on the address's primary symbol: entry has two, and _main, at the same address, none. _greeting has one reference and one offcut reference — the column counts the addresses inside the symbol's data or instruction that something points to, and tail points seven bytes in. The window marks a non-primary name by its colour; the script writes non-primary at the end of the row.
Names nobody stored¶
Check Default (Labels) as well — the one source the dialog opens unchecked — and twenty-two more rows appear:
== Check Default (Labels) as well: 22 more rows ==
Name Location Type Namespace Source Refs Offcut
_greeting+7 10000069b Data Label Default 2 1
s_nobody_calls_this:_%d_1000006a3 1000006a3 Data Label Default 1 0
s_twice(21)_=_%d_1000006ba 1000006ba Data Label Default 1 0
s_table[0](1)_=_%d_1000006ca 1000006ca Data Label Default 1 0
PTR__printf_100001000 100001000 Data Label Default 2 0
PTR__puts_100001008 100001008 Data Label Default 1 0
s__printf_100003071 100003071 Data Label Default 1 0
s__puts_100003079 100003079 Data Label Default 1 0
nlist_1000031b8 1000031b8 Data Label Default 2 0
nlist_1000031c8 1000031c8 Data Label Default 2 0
s___mh_execute_header_1000031ea 1000031ea Data Label Default 1 0
s__counter_1000031fe 1000031fe Data Label Default 1 0
s__greeting_100003207 100003207 Data Label Default 1 0
s__main_100003211 100003211 Data Label Default 1 0
s__orphan_100003217 100003217 Data Label Default 1 0
s__table_10000321f 10000321f Data Label Default 1 0
s__tail_100003226 100003226 Data Label Default 1 0
s__twice_10000322c 10000322c Data Label Default 1 0
s__via_table_100003233 100003233 Data Label Default 1 0
s__printf_10000323e 10000323e Data Label Default 3 0
s__puts_100003246 100003246 Data Label Default 3 0
s__helper_10000324c 10000324c Data Label Default 1 0
None of these is in the program's database. A default label is dynamic: it exists wherever a reference lands and no stored symbol does, and its name is worked out from what is at that address whenever it is asked for. The window finds them by visiting every address a reference lands on, and does it only when the filter could accept one — which is why the box starts unchecked: on this small program they are already twenty-two rows of thirty-nine. The name comes from SymbolUtilities ↗. Data whose type has a label prefix gives the prefix — s_ and the text for a string, PTR_ for a pointer, nlist_ for an entry of the Mach-O symbol table — and an address inside a named item is that name and the distance, _greeting+7. An address with no such data is named for the strongest reference to it, as BigRefListV0.getRefLevel ↗ ranks them: EXT_ for an external entry point, SUB_ for a call, DAT_ for a read or a pointer, LAB_ for a jump, UNK_ for anything else — and FUN_ wherever a function starts. The nlist_ and s__ rows, from 100003071 on, are in __LINKEDIT, which starts at 100003000: the part of the file that holds what the linkers need, symbol table entries and names spelled out as strings.
Stripped¶
Strip the program and hand it over again — demo/symbol_table.sh -s:
== The table under those defaults: 12 rows ==
Name Location Type Namespace Source Refs Offcut
_printf EXTERNAL:00000001 External Function /usr/lib/libSystem.B.dylib Imported 1 0
_puts EXTERNAL:00000002 External Function /usr/lib/libSystem.B.dylib Imported 1 0
_objc_msgSend_rtp fffeff00 Label Global Imported 0 0
MACH_HEADER 100000000 Data Label Global Imported 0 0 non-primary
__mh_execute_header 100000000 Data Label Global Imported 1 0
FUN_1000005b0 1000005b0 Function Global Default 2 0
FUN_1000005d0 1000005d0 Function Global Default 2 0
FUN_1000005e0 1000005e0 Function Global Default 3 0
FUN_100000600 100000600 Function Global Default 1 0
entry 100000630 Function Global Imported 2 0
_printf 100000688 Thunk Function Global Imported 5 0
_puts 10000068e Thunk Function Global Imported 3 0
== Functions nobody has named: 4 rows ==
checked: Default (Functions) | Function Labels | no advanced filter
Name Location Type Namespace Source Refs Offcut
FUN_1000005b0 1000005b0 Function Global Default 2 0
FUN_1000005d0 1000005d0 Function Global Default 2 0
FUN_1000005e0 1000005e0 Function Global Default 3 0
FUN_100000600 100000600 Function Global Default 1 0
The program's own names are gone. What is left is what strip kept — the library functions and __mh_execute_header — and what Ghidra adds on its own: entry, MACH_HEADER and _objc_msgSend_rtp. The four functions whose names strip took are FUN_ and their address, source Default — found, as Where a function starts shows, because a table in the file lists where every function begins. So Default (Functions) with Function Labels is the list of work left: every function nobody has named. main is not on it, because entry is a name. Name one — found the way a stripped program gives a function away, by the one string only it uses — and it is User Defined, and User Defined on its own is the list of work done:
== Rename the function that uses "nobody calls this" to orphan ==
Name Location Type Namespace Source Refs Offcut
orphan 100000600 Function Global User Defined 1 0
== What the user has named: 1 row ==
checked: User Defined | the three label types | no advanced filter
Name Location Type Namespace Source Refs Offcut
orphan 100000600 Function Global User Defined 1 0
The filter is a query¶
What the dialog shows when it opens, as its filter class reports its own checkboxes:
== The Symbol Table Filter dialog, as NewSymbolFilter opens it ==
Symbol Source
[ ] Default (Labels)
[x] Default (Functions)
[x] AI
[x] Analysis
[x] User Defined
[x] Imported
Label Symbols
[x] Instruction Labels
[x] Data Labels
[x] Function Labels
Non-label Symbols
[ ] Namespaces
[ ] Classes
[ ] External Library
[ ] Parameters
[ ] Local Variables
[ ] Global Register Variables
Advanced Filters
[ ] Externals
[ ] Non-Externals
[ ] Primary Labels
[ ] Non-Primary Labels
[ ] Globals
[ ] Locals
[ ] Register Variables (grayed out)
[ ] Stack Variables (grayed out)
[ ] Entry Points
[ ] Subroutines
[ ] Not In Memory
[ ] Unreferenced
[ ] Offcut Labels
That is the dialog in the screenshot on the help page, with one box more, AI. Its query is NewSymbolFilter.accepts ↗, which comes to this:
show a symbol if
its source is one of the checked sources
and its type is one of the checked types
and ( none of the checked advanced filters applies to its type
or one of the checked advanced filters that apply accepts it )
Each advanced filter declares the types it applies to, and is grayed out until one of those is checked: Register Variables and Stack Variables apply only to parameters and local variables, and neither box is checked. Two advanced filters that both apply to a symbol give it two chances to pass, not two tests to pass. Four queries worth keeping, on the build with its symbols:
== Functions nobody has named: 0 rows ==
checked: Default (Functions) | Function Labels | no advanced filter
== Library functions the program calls: 2 rows ==
checked: every source | Function Labels | Externals
Name Location Type Namespace Source Refs Offcut
_printf EXTERNAL:00000001 External Function /usr/lib/libSystem.B.dylib Imported 1 0
_puts EXTERNAL:00000002 External Function /usr/lib/libSystem.B.dylib Imported 1 0
== External library names: 2 rows ==
checked: every source | External Library | no advanced filter
Name Location Type Namespace Source Refs Offcut
/usr/lib/libSystem.B.dylib NO ADDRESS External Library Global Imported 0 0
<EXTERNAL> NO ADDRESS External Library Global Imported 0 0
== Entry points: 11 rows ==
checked: every source | the three label types | Entry Points
Name Location Type Namespace Source Refs Offcut
MACH_HEADER 100000000 Data Label Global Imported 0 0 non-primary
__mh_execute_header 100000000 Data Label Global Imported 1 0
_twice 1000005b0 Function Global Imported 3 0
_via_table 1000005e0 Function Global Imported 4 0
_orphan 100000600 Function Global Imported 2 0
_main 100000630 Function Global Imported 0 0 non-primary
entry 100000630 Function Global Imported 2 0
_greeting 100000694 Data Label Global Imported 1 1
_table 100001010 Data Label Global Imported 2 0
_counter 100002000 Data Label Global Imported 3 0
_tail 100002008 Data Label Global Imported 2 0
Default (Functions) with Function Labels is empty, because the file named every function. Externals, applied to functions, is the imports. External Library, a non-label type, is the libraries themselves, and <EXTERNAL> is where an import goes when the file does not say which library it is from. Entry Points, on this build, is every name the verified block above called global, and not _helper, the local one. The mark is on an address, not a name, so entry has it beside _main, and MACH_HEADER beside __mh_execute_header.
Two filters that do not do what they say¶
Unreferenced¶
The help calls what it finds dead code. On this program it finds no function of the program's at all — not _orphan, which nothing calls:
== Unreferenced functions: 2 rows ==
checked: every source | Function Labels | Unreferenced
Name Location Type Namespace Source Refs Offcut
_printf EXTERNAL:00000001 External Function /usr/lib/libSystem.B.dylib Imported 1 0
_puts EXTERNAL:00000002 External Function /usr/lib/libSystem.B.dylib Imported 1 0
== References to _orphan: 2 ==
from Entry Point EXTERNAL
from 100003114 DATA __LINKEDIT uleb128 20h
_orphan has two references, and neither comes from code. Entry Point is the entry-point mark itself, kept as a reference — the delete further down removes the two together. The other is a uleb128 of 20h in __LINKEDIT — the distance from _via_table, at 1000005e0, to _orphan, at 100000600, one step of LC_FUNCTION_STARTS, the table of every function start that Where a function starts is about. Unreferenced asks whether anything at all points at the address, and a Mach-O executable's own table points at every function in it. The two it did list are the external functions, which show a reference count of 1: the column and the filter ask an external symbol different questions. Stripping takes _orphan's entry-point mark and keeps the table:
== Unreferenced functions: 2 rows ==
checked: every source | Function Labels | Unreferenced
Name Location Type Namespace Source Refs Offcut
_printf EXTERNAL:00000001 External Function /usr/lib/libSystem.B.dylib Imported 1 0
_puts EXTERNAL:00000002 External Function /usr/lib/libSystem.B.dylib Imported 1 0
== References to FUN_100000600: 1 ==
from 100003114 DATA __LINKEDIT uleb128 20h
An ELF has a table of its own. The same query on the program as GCC built it, in Ubuntu, with cc -std=c17 -Wall -Wextra -O0, and handed to demo/symbol_table.sh as a binary:
== Unreferenced functions: 7 rows ==
checked: every source | Function Labels | Unreferenced
Name Location Type Namespace Source Refs Offcut
__libc_start_main EXTERNAL:00000001 External Function <EXTERNAL> Imported 1 0
puts EXTERNAL:00000002 External Function <EXTERNAL> Imported 1 0
printf EXTERNAL:00000003 External Function <EXTERNAL> Imported 1 0
__cxa_finalize EXTERNAL:00000004 External Function <EXTERNAL> Imported 1 0
_ITM_deregisterTMCloneTable EXTERNAL:00000005 External Function <EXTERNAL> Imported 1 0
_ITM_registerTMCloneTable EXTERNAL:00000006 External Function <EXTERNAL> Imported 1 0
__gmon_start__ EXTERNAL:00000007 External Function <EXTERNAL> Imported 1 0
== References to orphan: 3 ==
from Entry Point EXTERNAL
from 00102094 INDIRECTION .eh_frame_hdr fde_table_entry
from 00102198 DATA .eh_frame ddw FFFFF016h
Seven imports, the C runtime's among them, and none of the program's functions. orphan is an entry point there too, and it has two more references, from the unwind tables: its entry in .eh_frame_hdr's sorted index and its record in .eh_frame. On a real executable, then, Unreferenced means that not even the file's own tables point at an address. To find a function nothing calls, read its references instead — the Symbol References ↗ window lists them — and look for one whose references are all an entry point and a table.
Not In Memory¶
== Not In Memory: 10 rows ==
checked: every source | the three label types | Not In Memory
Name Location Type Namespace Source Refs Offcut
_printf EXTERNAL:00000001 External Function /usr/lib/libSystem.B.dylib Imported 1 0
_puts EXTERNAL:00000002 External Function /usr/lib/libSystem.B.dylib Imported 1 0
_objc_msgSend_rtp fffeff00 Label Global Imported 0 0
_twice 1000005b0 Function Global Imported 3 0
_helper 1000005d0 Function Global Imported 2 0
_via_table 1000005e0 Function Global Imported 4 0
_orphan 100000600 Function Global Imported 2 0
entry 100000630 Function Global Imported 2 0
_printf 100000688 Thunk Function Global Imported 5 0
_puts 10000068e Thunk Function Global Imported 3 0
== Not In Memory, with Function Labels unchecked: 1 row ==
checked: every source | Instruction Labels, Data Labels | Not In Memory
Name Location Type Namespace Source Refs Offcut
_objc_msgSend_rtp fffeff00 Label Global Imported 0 0
Ten rows, and one of them is not in memory. Not In Memory applies to labels only. A function is a type it does not apply to, so every function passes it untested — externals included — while every label it does test has to be outside memory, and only _objc_msgSend_rtp is, an address the loader labelled with nothing there. It is the help's warning about advanced filters at its most misleading, because the ten rows look like an answer. Uncheck Function Labels and the answer is the one row.
The rest¶
Parameters are symbols only once they are stored:
== Parameters, before any is committed: 2 rows ==
checked: every source | Parameters | no advanced filter
Name Location Type Namespace Source Refs Offcut
param_1 RDI:8 External Parameter /usr/lib/libSystem.B.dylib::_printf Default 0 0
param_1 RDI:8 External Parameter /usr/lib/libSystem.B.dylib::_puts Default 0 0
== Commit _twice's parameters from the decompiler, as Commit Params/Return does: param_1 in EDI:4 ==
== Unreferenced, with Parameters checked too: 5 rows ==
checked: every source | Function Labels, Parameters | Unreferenced
Name Location Type Namespace Source Refs Offcut
_printf EXTERNAL:00000001 External Function /usr/lib/libSystem.B.dylib Imported 1 0
_puts EXTERNAL:00000002 External Function /usr/lib/libSystem.B.dylib Imported 1 0
param_1 RDI:8 External Parameter /usr/lib/libSystem.B.dylib::_printf Default 0 0
param_1 RDI:8 External Parameter /usr/lib/libSystem.B.dylib::_puts Default 0 0
param_1 EDI:4 Parameter _twice Default 0 0
== Parameters in a register: 3 rows ==
checked: every source | Parameters | Register Variables
Name Location Type Namespace Source Refs Offcut
param_1 RDI:8 External Parameter /usr/lib/libSystem.B.dylib::_printf Default 0 0
param_1 RDI:8 External Parameter /usr/lib/libSystem.B.dylib::_puts Default 0 0
param_1 EDI:4 Parameter _twice Default 0 0
== The help's Example 3: functions and stack parameters: 10 rows ==
checked: every source | Function Labels, Parameters | Stack Variables
Name Location Type Namespace Source Refs Offcut
_printf EXTERNAL:00000001 External Function /usr/lib/libSystem.B.dylib Imported 1 0
_puts EXTERNAL:00000002 External Function /usr/lib/libSystem.B.dylib Imported 1 0
_twice 1000005b0 Function Global Imported 3 0
_helper 1000005d0 Function Global Imported 2 0
_via_table 1000005e0 Function Global Imported 4 0
_orphan 100000600 Function Global Imported 2 0
_main 100000630 Function Global Imported 0 0 non-primary
entry 100000630 Function Global Imported 2 0
_printf 100000688 Thunk Function Global Imported 5 0
_puts 10000068e Thunk Function Global Imported 3 0
The decompiler gives _twice a param_1, but until Commit Params/Return stores it — which the script does as CommitParamsAction ↗ does — the only parameters in the table are the two library functions'. Then the help's trap, with a result: Unreferenced with Parameters checked lists all three parameters, because Unreferenced does not apply to parameters. The help's own Example 3, Stack Variables with functions and parameters, lists the functions and no parameter at all, because an int parameter on x86-64 arrives in a register — EDI — and Register Variables is the filter that finds it.
== Locals: 0 rows ==
checked: every source | the three label types | Locals
== Offcut Labels: 1 row ==
checked: every source | the three label types | Offcut Labels
Name Location Type Namespace Source Refs Offcut
_greeting+7 10000069b Data Label Default 2 1
== The help's Example 4: 39 rows with neither Primary Labels nor Non-Primary Labels, 39 with both ==
Locals is empty: it asks for a namespace other than Global that is not an import's, and C's static is not one. Offcut Labels finds _greeting+7, the address tail holds — a default label, found because Default (Labels) was among the sources checked. And the help's Example 4 holds: Primary Labels and Non-Primary Labels together select what neither selects, because every label is one or the other.
Renaming, deleting, pinning¶
The rest of the help page is actions. A rename, and then Delete, issued as AbstractSymbolTableModel.delete ↗ issues it, command for command:
== Rename _orphan to orphan ==
Name Location Type Namespace Source Refs Offcut
_orphan 100000600 Function Global Imported 2 0
orphan 100000600 Function Global User Defined 2 0
Delete: applied
== After Delete ==
Name Location Type Namespace Source Refs Offcut
LAB_100000600 100000600 Instruction Label Default 1 0
function at 100000600: none
== References to LAB_100000600: 1 ==
from 100003114 DATA __LINKEDIT uleb128 20h
== _counter, before ==
Name Location Type Namespace Source Refs Offcut
_counter 100002000 Data Label Global Imported 3 0
Delete: applied
== After Delete ==
Name Location Type Namespace Source Refs Offcut
DAT_100002000 100002000 Data Label Default 2 0
Delete DAT_100002000: skipped: the action passes over a dynamic symbol
== References to DAT_100002000: 2 ==
from 1000005ea READ __text ADD EAX,dword ptr [0x100002000]
from 100003058 DATA __LINKEDIT dyld_chained_starts_in_segment
The rename made the name User Defined. The delete did more than the help says. For a function, Delete issues DeleteFunctionCmd ↗ first and deletes the name after it, so the function itself is gone — function at 100000600: none — and what is left is LAB_, the default label of an instruction something points to. It took a reference with it as well: DeleteLabelCmd ↗ clears the entry-point mark when it deletes the only name at an address, and the one reference left is the step in LC_FUNCTION_STARTS. _counter, a data label, goes the way the help says: a default label takes its place and keeps its references — the read in _via_table, and one from the chained fixups in __LINKEDIT — less the entry point, again. And Delete on DAT_100002000 does nothing, because the action passes over a dynamic symbol: there is nothing stored to delete.
== Add a label also_twice at _twice, Primary unchecked ==
Name Location Type Namespace Source Refs Offcut
_twice 1000005b0 Function Global Imported 3 0
also_twice 1000005b0 Function Global User Defined 0 0 non-primary
== Non-Primary Labels: 3 rows ==
checked: every source | the three label types | Non-Primary Labels
Name Location Type Namespace Source Refs Offcut
MACH_HEADER 100000000 Data Label Global Imported 0 0 non-primary
also_twice 1000005b0 Function Global User Defined 0 0 non-primary
_main 100000630 Function Global Imported 0 0 non-primary
== Clear the function at _helper ==
Name Location Type Namespace Source Refs Offcut
_helper 1000005d0 Instruction Label Global Imported 2 0
== Subroutines: 1 row ==
checked: every source | Instruction Labels | Subroutines
Name Location Type Namespace Source Refs Offcut
_helper 1000005d0 Instruction Label Global Imported 2 0
== Create Function at _helper again ==
Name Location Type Namespace Source Refs Offcut
_helper 1000005d0 Function Global Imported 2 0
A label added with Primary unchecked is non-primary, and its type shows as Function because a function starts at its address; Non-Primary Labels lists it with MACH_HEADER and _main, the two that were there already. Subroutines is for a label that code calls and that is not a function, and here it took clearing the function at _helper to make one: _twice still calls the address, so the label is listed, and Create Function — F in the Listing — turns it back into a function. With Default (Labels) checked, Subroutines over Instruction Labels is a list of calls with no function behind them.
== Pin _greeting, leave _tail, and move the image base from 100000000 ==
before: _greeting 100000694 _tail 100002008
after: _greeting 100000694 _tail 110002008 image base 110000000
in memory: _greeting false _tail true
A pinned label stays at its address when the program moves. The image base moved by 0x10000000; _tail moved with it and _greeting, pinned, did not — and is now outside memory, because the bytes it named moved. A pin is for an address that means the same thing wherever the program is loaded, such as a hardware register in firmware, not for a string in the program.
The help page covers the rest of the window, none of which the script drove: sorting by a column, and control-clicking the sorted column to stop sorting a large table; the filter text field, with its Contains, Starts With, Matches Exactly and Regular Expression strategies and the Name Only checkbox; Make Selection; the two Navigate toggles that tie the table to the Listing; Edit External Location for an import; and References → Delete All.
If you are coming from another language¶
Rust. The same table, filled by the same linker: the Rust library's The linker ↗ shows an object file's list of the symbols it defines and the symbols it needs, and the mangled names Rust puts in the first list, which is what a Rust binary brings to this table. Its Reading a backtrace ↗ is where the names in a panic's backtrace come from that table when a build has no debug information.
Python. Python has a symbol table too — its compiler builds one for every scope — and the standard library's symtable module hands it over:
Verified output of symbol_table_names_py.py — regenerated by tools/run_examples.py, never hand-typed.
== top ==
_helper assigned global
twice assigned global referenced
orphan assigned global
print global referenced
== _helper ==
x parameter referenced
== twice ==
x parameter referenced
_helper global referenced
== orphan ==
x parameter referenced
print global referenced
At the top level orphan is assigned and never read — and so is _helper, whose only reader is inside twice, where it appears as a global that is referenced. Referenced answers a question about one scope, as Unreferenced in Ghidra answers one about one address, and neither is "does anything use this". The leading underscore that makes _helper private by convention changes nothing in the table, as static changed nothing in Ghidra's Namespace column.
ABAP. (Not machine-checked — CI cannot run ABAP.) The repository is the symbol table, and nothing is ever stripped from it: every program, class, method and function module is an object with a name, and the where-used list is the query — over source, as Ghidra's references are over machine code. What it cannot see is a call built from a value at run time, CALL FUNCTION lv_name, which is ABAP's via_table: reached, and named by nothing that says so.
See also¶
- The Symbol Table's help page ↗ at the installed tag — the page this lesson checks against; a running Ghidra shows it rendered under Help
NewSymbolFilter.java↗ — every checkbox in the dialog, the types each advanced filter applies to, and the defaults- Where a function starts —
LC_FUNCTION_STARTSand.eh_frame, the tables that give Unreferenced nothing to find - What the decompiler recovers — the lesson that strips a program first, and where the headless analyzer's options are introduced
- Mach-O ↗ and ELF ↗ in the encodings library — the two containers, and where each keeps the symbol table this page reads