C Variable Scope, Linkage, and Storage in a Compiled Executable
Purpose
This document explains how C variables declared at global, source-file (module), function, and nested-block scope are represented in a final Linux ELF executable, and what can be recovered when reverse engineering an unstripped executable. It matters when the source code has gone missing and the compiled program is all that is left: what you can get back depends on what the compiler and linker left in the binary, and scope is one of the first things to disappear. That is the situation Oninit Phoenix Recover is built for. It rebuilds readable, re-compilable Informix 4GL source from compiled .4ge programs, and, as a deliberate policy, it accepts only unstripped Linux builds.
Key distinction: C scope determines where an identifier may be used in source code; linkage determines whether declarations refer to the same entity; storage duration determines how long an object exists. ELF symbol binding is related to linkage, but it is not a complete record of C lexical scope.
Example multi-file project
main.c:
int shared_count = 10; /* file scope, external linkage */
static int main_count = 20; /* file scope, internal linkage */
void run(void)
{
int call_count = 30; /* block scope, automatic storage */
static int total = 40; /* block scope, static storage */
{
int nested = 50; /* inner block scope */
(void)nested;
}
(void)call_count;
++total;
}
module.c:
extern int shared_count; /* refers to the external definition */
static int module_count = 60; /* distinct file-local object */
void update(void)
{
++shared_count;
++module_count;
}
shared_count is one object referenced by both translation units. main_count and module_count are separate file-local objects. call_count and nested are automatic variables. total has block scope but persists for the lifetime of the program.
The three independent properties
| Property | Meaning | Determined by |
|---|---|---|
| Scope | Where the identifier is visible in source | Declaration location and surrounding blocks |
| Linkage | Whether the same name in different scopes/translation units identifies the same entity | static, extern, declaration context |
| Storage duration | How long the object exists | Static, automatic, thread, or allocated storage |
A variable can have block scope and static storage duration at the same time. Consequently, storage location alone cannot recover source-level scope.
Common declarations and binary representation
| Declaration | C scope | Linkage | Storage duration | Typical machine representation |
|---|---|---|---|---|
File-scope int x = 1; | File | External | Static | .data, often ELF GLOBAL OBJECT |
File-scope int x; | File | External | Static | .bss, often ELF GLOBAL OBJECT |
File-scope static int x = 1; | File | Internal | Static | .data, often ELF LOCAL OBJECT |
Function int x = 1; | Block | None | Automatic | Stack/register; may be optimized out |
Function static int x = 1; | Block | None | Static | .data, often ELF LOCAL OBJECT |
Nested block int x; | Inner block | None | Automatic | Stack/register; may share a slot with another variable |
_Thread_local int x; | Depends on declaration | Depends on declaration | Thread | TLS sections such as .tdata / .tbss |
These are common outcomes, not mandatory layouts. The compiler can optimize away unused objects or alter how they are represented.
Global (externally linked) variables
int shared_count = 10;
- The identifier has file scope and external linkage.
- The object has static storage duration: it exists throughout program execution.
- Other translation units can reference it with
extern int shared_count;. - In an unstripped ELF symbol table, a retained definition commonly appears as
OBJECT GLOBAL. - Code ultimately accesses the object's address; the CPU does not enforce C source visibility.
Important: GLOBAL in ELF describes symbol binding, not a universal rule that the source identifier is visible in every function. A declaration still must be in scope in the source being compiled. Dynamic linking and symbol visibility add further rules.
Module-level (static file-scope) variables
static int module_count = 60;
- The identifier has file scope and internal linkage.
- Other translation units cannot name that same object through an external declaration.
- The object still has static storage duration and is normally stored in
.dataor.bss. - A retained ELF symbol commonly has
LOCALbinding. - Different
.cfiles may each definestatic int count;without colliding.
At runtime, the machine code can access these objects just like externally linked globals. The source-file restriction was applied by the compiler and linker, not by per-variable memory protection.
Ordinary function-local variables
void run(void)
{
int call_count = 30;
}
- The identifier has block scope and no linkage.
- The object normally has automatic storage duration.
- It may reside in a stack slot, register, or nowhere at all after optimization.
- Recursive or concurrent calls can have independent instances.
- Ordinary automatic locals are **not normally represented by ELF
OBJECTsymbols**. - DWARF debug information may describe their names, types, source scopes, and locations, possibly with different location expressions over different instruction ranges.
An illustrative x86-64 unoptimized stack access might use [rbp-4]; that is a stack-frame offset, not a fixed executable-wide variable address.
Function-local static variables
void run(void)
{
static int total = 40;
++total;
}
totalhas block scope and no linkage.- It has static storage duration, not automatic storage duration.
- Its value persists across calls; recursive calls refer to the same object.
- It is normally represented like file-static data, with a retained ELF
LOCAL OBJECTsymbol. - Without debug metadata, it may be impossible to distinguish conclusively from a file-static object. Compiler-generated symbol naming conventions are clues, not proof.
What is present in an unstripped ELF executable?
ELF symbol tables
readelf -Ws ./program
Common symbol-table attributes include:
- Name: symbol spelling, possibly compiler-generated or mangled.
- Value: symbol value/address relative to the ELF image (affected by relocation and PIE/ASLR at runtime).
- Size: recorded symbol size, when available.
- Type:
OBJECT,FUNC,FILE,TLS, etc. - Bind:
LOCAL,GLOBAL,WEAK, etc. - Ndx: defining section or special index.
.symtab typically contains more symbols than .dynsym; .dynsym mainly supports dynamic linking. An executable can be unstripped but still lack DWARF.
Source-file clues
Unstripped ELF symbol tables may retain STT_FILE symbols, plus local-symbol ordering that helps associate some local symbols with an original object/translation unit. This is not guaranteed to provide an exact source-file-to-variable mapping after every compilation and link workflow. Link-time optimization, symbol elimination, and linker transformations can further obscure provenance.
DWARF debug information
readelf -S ./program | grep -E '\.debug_info|\.zdebug_info'
readelf --debug-dump=info ./program
# If installed:
llvm-dwarfdump --debug-info ./program
When compiled with -g and debug information retained, DWARF may describe:
DW_TAG_compile_unit -> main.c
DW_TAG_variable -> shared_count
DW_TAG_variable -> main_count
DW_TAG_subprogram -> run
DW_TAG_variable -> call_count
DW_TAG_variable -> total
DW_TAG_lexical_block
DW_TAG_variable -> nested
This hierarchy is the strongest evidence of original lexical scope. Actual emitted information depends on compiler flags, optimization, and debug-information quality.
Recoverability matrix
| Original property | Unstripped ELF, no DWARF | ELF with DWARF |
|---|---|---|
| Retained external data symbol name | Usually | Usually |
| Retained file-static data symbol name | Usually | Usually |
ELF LOCAL versus GLOBAL binding | Yes, for retained symbols | Yes, for retained symbols |
| Original source-file ownership | Sometimes inferable | Usually recorded |
| Ordinary automatic local variable name | Generally not | Often |
| Function-static versus file-static declaration | Not reliably | Usually |
| Exact nested lexical scope | No | Often |
| Original type and declaration line | Limited | Often |
| Actual runtime value | Requires examining a running process or memory image | Same; DWARF helps locate it |
**Unstripped does not imply -g.** Retaining .symtab does not by itself retain the C compiler's complete scope hierarchy.
Practical reverse-engineering workflow
- Check sections:
readelf -S ./programto identify.symtab,.dynsym, and DWARF sections. - Inspect symbols:
readelf -Ws ./programto identify retainedOBJECT,TLS, andFILEentries and their bindings. - Group candidates: Use symbol addresses, sections, file markers, and available object-file evidence to infer module ownership; treat inferences as provisional.
- Read DWARF if present: Traverse compilation units, subprograms, lexical blocks, and variable entries to reconstruct source scopes.
- Disassemble/decompile: For variables absent from symbols, analyze stack-frame accesses, registers, function boundaries, and data references. These produce hypotheses, not guaranteed original declarations.
- Account for optimization: Variables can be folded, removed, reused, moved between registers and memory, or have unavailable location ranges.
Useful commands:
file ./program
readelf -S ./program
readelf -Ws ./program
readelf --debug-dump=info ./program
objdump -drwC ./program
Critical conclusions
- Global and file-static variables normally occupy the same kinds of storage. Their key binary-level difference is external versus local symbol binding, when symbols are retained.
- Function-static variables also occupy static storage, despite having function/block lexical scope. ELF binding alone does not distinguish them from file-static variables.
- Automatic function locals are different: they generally live in stack frames or registers and have no ordinary global ELF data symbol.
- Scope is a compiler rule, not a hardware access-control mechanism. Machine instructions access storage, not C scopes.
- DWARF is the reliable route to reconstructing original source scope, when sufficiently complete. Unstripped ELF symbols alone only recover a subset.
- Names are not unique identifiers: two file-static variables from different translation units can share a name. Use symbol identity/address and compilation-unit context, not name alone.