Skip to content

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:

Real output — Ghidra 12.1.3 headless on the clang -O0 build, x86-64 macOS; demo/symbol_table.sh, the table under the filter's defaults
== 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:

Real output — the same run; every source checked
== 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:

Real output — Ghidra 12.1.3 headless on the same build, stripped; demo/symbol_table.sh -s, the table under the filter's defaults and one query
== 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:

Real output — the same run; a rename, and the query for it
== 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:

Real output — Ghidra 12.1.3 headless, the clang -O0 build; the Symbol Table Filter dialog's checkboxes, as NewSymbolFilter opens them
== 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:

Real output — Ghidra 12.1.3 headless on the clang -O0 build; four queries
== 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:

Real output — the same run; Unreferenced, and where _orphan's references come from
== 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:

Real output — the stripped build; demo/symbol_table.sh -s, the same query
== 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:

Abridged — real output, Ghidra 12.1.3 on the Mac, on the ELF that GCC 13.3.0 built from the same source at -O0 in the ubuntu:24.04 image, x86-64; demo/symbol_table.sh <binary>, Unreferenced and the references to orphan
== 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

Real output — Ghidra 12.1.3 headless on the clang -O0 build; Not In Memory, with and without Function Labels
== 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:

Real output — the same run; parameters before and after committing _twice's, and three queries that involve them
== 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.

Real output — the same run; Locals, Offcut Labels, and the help's Example 4
== 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:

Real output — the same run; renaming and deleting a function, deleting a data label, and deleting a default label
== 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.

Real output — the same run; a second name at an address, and a label that is called but is not a function
== 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.

Real output — the same run; pinning, then moving the image base
== 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_STARTS and .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