Executable Exports Symbols

There are actually several critical scenarios where an executable must export symbols.

The confusion usually lies in the direction of the linking. You are right that Executable A rarely links dynamically to Executable B to call functions inside B.

However, the reverse happens frequently: Dynamic Libraries (Plugins) loaded by Executable A often need to call functions inside Executable A.

Here are the specific reasons why an executable needs to keep exported symbols:

1. The “Host-Plugin” Architecture (Most Common)

This is the primary reason. If your executable supports plugins (DLLs on Windows, .so files on Linux) that are loaded at runtime, those plugins often need to use functionality provided by the main executable.

  • How it works: The main executable loads the plugin (using dlopen or LoadLibrary).
  • The Problem: The plugin code needs to call GetMainAppVersion() or LogMessage(), which are defined inside the .exe.
  • The Solution: The executable exports those symbols. When the dynamic linker loads the plugin, it resolves the plugin’s undefined references by looking at the executable’s exported symbol table (the Global Symbol Table).
  • Linux Flag: On Linux (GCC/Clang), you often use -rdynamic or --export-dynamic to force the linker to add internal symbols to the dynamic symbol table so plugins can see them.

2. Runtime Introspection (dlopen(NULL))

Sometimes an application needs to look up a function within itself by string name at runtime.

  • Scenario: You have a configuration file that says action = "do_save". The code reads this string and wants to find the function void do_save() in memory to execute it.
  • Mechanism: The code calls dlsym(dlopen(NULL, ...), "do_save").
  • Requirement: do_save must be in the dynamic symbol table (exported) for dlsym to find it. If you strip the executable or hide symbols, this look-up fails.

3. Stack Traces and Crash Reporting

If you want your application to print a readable stack trace (backtrace) when it crashes without relying on external debug files (like .pdb or separate debug info), the executable needs symbols.

While “debug symbols” (DWARF) are usually used for this, many backtrace libraries (like execinfo.h on Linux) can fall back to using exported dynamic symbols (.dynsym) to give you at least the function names, even if line numbers are missing.

4. Sanitizers and Profilers (ASan, Valgrind)

Tools like Google’s AddressSanitizer (ASan) or Valgrind often intercept function calls (like malloc) or need to symbolize addresses at runtime.
If an executable is fully stripped of exports, these tools may output addresses like 0x4005d3 instead of main or process_request, making the output much harder to read or causing the tool to fail to intercept specific calls.

5. Windows Specifics (Hybrid EXE/DLL)

On Windows, it is technically possible (though rare) to treat an .exe exactly like a .dll.

  • You can run LoadLibrary("my_app.exe") from another process.
  • If my_app.exe exports functions, the other process can call GetProcAddress and use them.
  • Why do this? It is sometimes used in resource sharing or niche inter-process communication setups, though standard IPC is preferred.

Summary: Do you need them?

  1. If you are building a standard, standalone CLI tool:
    • No. You generally do not need exported symbols. You can strip them to save space and obfuscate the code.
  2. If you are building a program that loads plugins:
    • Yes. You likely need to export the API that the plugins will use.
  3. If you use dlsym / Reflection:
    • Yes. You need exports to find functions by name.

Technical Note: There is a difference between Debug Symbols (used by GDB, not loaded into memory, safe to strip) and Dynamic Symbols (used by the OS loader, loaded into memory). Your question refers to Dynamic Symbols. Standard executables usually have a very small dynamic symbol table (just libc dependencies) unless configured otherwise.




The Dynamic Symbol Table (specifically .dynsym in ELF files on Linux/Unix) is essentially a shopping list for the Dynamic Linker.

It contains two main categories of items: Things I need (Imports) and Things I have (Exports).

Here is the breakdown of exactly what is contained inside.

1. Imports (Undefined Symbols)

These are symbols that your executable uses but does not define itself. They represent external dependencies.

  • What they look like: In the symbol table, they are usually marked as UND (Undefined).
  • Purpose: They tell the OS loader: “Before you run me, please search the loaded shared libraries (like libc.so, libm.so) to find the address of these functions.”
  • Examples:
    • Standard C library functions: printf, malloc, free.
    • External variables: stdout, stderr.
    • Third-party library functions: SSL_connect (if using OpenSSL).

2. Exports (Global Defined Symbols)

These are symbols that your executable defines and has decided to expose to the outside world.

  • What they look like: They have a specific memory address or offset associated with them.
  • Purpose: They tell the OS loader: “If any other object (like a plugin or a library I loaded) asks for this name, point them to this address inside me.”
  • Caveat for Executables: By default, standard executables usually contain very few exports (often just _start or __bss_start). As discussed previously, you usually have to force the linker (via -rdynamic or __declspec(dllexport)) to move your global functions from the “debug/static symbol table” into this “dynamic symbol table.”

3. Weak Symbols

These are a special sub-category.

  • Weak Imports: “I need this symbol, but if you can’t find it, don’t crash the program; just set the address to NULL.” (Used for optional features).
  • Weak Exports: “I provide this function, but if a loaded library provides a function with the same name, use theirs instead of mine.” (Used for overridable implementations).

Visualization (How to see them)

You can see exactly what is in your executable’s dynamic symbol table using the nm command with the -D (dynamic) flag.

Command:

nm -D my_executable

Output Example:

                 U free               <-- IMPORT (Undefined): Needs 'free' from libc
0000000000404028 B __bss_start        <-- EXPORT (Global Data): Start of BSS section
                 U malloc             <-- IMPORT (Undefined): Needs 'malloc'
0000000000401126 T my_plugin_api      <-- EXPORT (Text/Code): A function I expose
                 U printf             <-- IMPORT (Undefined): Needs 'printf'

Important Distinction: .symtab vs .dynsym

To fully understand the “necessity” question, you must distinguish between the two symbol tables inside a binary:

  1. The Dynamic Symbol Table (.dynsym):
    • Contents: Only Imports and Exports.
    • Size: Small.
    • Necessity: Required for the program to run (the loader needs it to resolve printf).
    • Strippable? No (mostly). You cannot strip this, or the program breaks.
  2. The “Full” Symbol Table (.symtab):
    • Contents: Everything above PLUS local static functions, internal variables, source file names, and compilation units.
    • Size: Large.
    • Necessity: Not required for running. Only used for debugging (GDB) and static linking.
    • Strippable? Yes. When you run the strip command, this is what gets deleted.

Summary

So, to answer your specific phrasing:

  • Does it contain External Library Symbols? Yes (as “Undefined” placeholders).
  • Does it contain Exported Global Symbols? Yes (as “Defined” addresses), but only if the executable was configured to export them.

Leave a Reply

Your email address will not be published. Required fields are marked *