-
Notifications
You must be signed in to change notification settings - Fork 0
Home
Sym2srec is a command-line tool that extracts the .symtab and .strtab sections
from an ELF32 executable and embeds them as loadable segments into a new S-Record file.
Its purpose is to enable dynamic loading: external programs can reference kernel symbols
at runtime without statically linking against the kernel.
Sym2srec is used as part of the Mk build pipeline.
The generated .srec file is flashed alongside the Mk firmware and provides the symbol
table that the Mk ELF loader uses to resolve relocations at runtime.
- Concepts
- Quick start
- Command-line reference
- Output format — S-Record layout
- Symbol resolution protocol
- Integration with Mk
- Build from source
- References
Symbol — a unique identifier representing a function, variable, or object in a program.
Symbol resolution — the process of finding the exact memory address of a symbol:
- Static resolution: performed at compile time, for symbols known within a single compilation unit.
- Dynamic resolution: performed at runtime, for symbols shared across compilation units or loaded dynamically.
Relocation — adjusting a symbol's address so it can be correctly referenced after being loaded at a different address than originally linked.
Dynamic loading — copying a program from storage into RAM and resolving all symbol references so it can execute correctly.
A prebuilt Windows executable is included in the Mk repository at
Mk/Make/sym2srec.exe.
No build step is required if you are using Mk on Windows — the Mk Makefile calls
sym2srec.exe automatically as part of make all.
To use sym2srec manually:
sym2srec myFirmware.elf myFirmware.srec 0x002C0000
sym2srec <input.elf> <output.srec> <base_address>
| Argument | Description |
|---|---|
input.elf |
ELF32 executable containing the symbols to export. Must be a 32-bit ELF file. |
output.srec |
Path of the S-Record file to generate. |
base_address |
Address where the symbol table will be loaded in memory. Must be 4-byte aligned, 8 hex digits (e.g. 0x002C0000). |
Example:
sym2srec Mk.elf ../build/Mk.srec 0x002C0000
Before running sym2srec, strip local and debug symbols from the input file to reduce the size of the generated symbol table:
arm-none-eabi-strip --discard-all --strip-debug Mk.elf
This removes all non-global and debug symbols, keeping only the exported API symbols that external applications need to reference.
Sym2srec parses the input ELF file and performs the following steps:
- Builds a GNU hash table (
.gnuhash) from the.symtaband.strtabsections. - Builds a
SymbolsAreaHeader_theader pointing to all three tables. - Copies all existing loadable segments from the ELF file to the S-Record.
- Appends
.symtab,.strtab, and.gnuhashas new loadable segments.
The resulting S-Record layout is:
S-Record file
│
├── Existing loadable segments (from input ELF)
│
└── Symbol area (at <base_address>)
├── SymbolsAreaHeader_t ← loaded at <base_address>
├── .symtab ← at symtabBaseAddr
├── .strtab ← at strtabBaseAddr
└── .gnuhash ← at gnuHashBaseAddr

This section describes the complete protocol a dynamic loader must implement to resolve a symbol by name using the data produced by sym2srec.
The symbol area always begins with this header at <base_address>:
typedef struct
{
uint32_t magicNumber; /* Magic number: 0x53594D42 ('SYMB') */
uint32_t headerSize; /* Size of this header in bytes (40) */
uint32_t padding; /* Reserved: 0xFFFFFFFF */
uint32_t version; /* Header version: 0x00000001 */
uint32_t* symtabBaseAddr; /* Pointer to the symbol table */
uint32_t symtabSize; /* Size of the symbol table in bytes */
uint32_t* strtabBaseAddr; /* Pointer to the string table */
uint32_t strtabSize; /* Size of the string table in bytes */
uint32_t* gnuhashBaseAddr; /* Pointer to the GNU hash table */
uint32_t gnuhashSize; /* Size of the GNU hash table in bytes */
} SymbolsAreaHeader_t;To access the header in your loader:
SymbolsAreaHeader_t* l_header = (SymbolsAreaHeader_t*) 0x002C0000;The GNU hash table is stored contiguously in memory. Initialize the following structure from the header to navigate it:
typedef struct
{
uint32_t nbuckets; /* Number of buckets */
uint32_t symbolsOffset; /* Index of first non-local symbol */
uint32_t bloomFilterSize; /* Bloom filter size in 32-bit words */
uint32_t bloomFilterShift; /* Bloom filter shift value */
uint32_t* bloomAddr; /* Pointer to the bloom filter array */
uint32_t* bucketsAddr; /* Pointer to the bucket array */
uint32_t* hashValuesAddr; /* Pointer to the hash value array */
} ELF32GNUHashTable_t;Initialize it from the header:
ELF32GNUHashTable_t l_hashTable;
l_hashTable.nbuckets = l_header->gnuhashBaseAddr[0];
l_hashTable.symbolsOffset = l_header->gnuhashBaseAddr[1];
l_hashTable.bloomFilterSize = l_header->gnuhashBaseAddr[2];
l_hashTable.bloomFilterShift= l_header->gnuhashBaseAddr[3];
l_hashTable.bloomAddr = &l_header->gnuhashBaseAddr[4];
l_hashTable.bucketsAddr = &l_header->gnuhashBaseAddr[4 + l_hashTable.bloomFilterSize];
l_hashTable.hashValuesAddr = &l_header->gnuhashBaseAddr[4 + l_hashTable.bloomFilterSize
+ l_hashTable.nbuckets];Before scanning the hash table, use the bloom filter to quickly determine whether the symbol is definitely absent (no false negatives) or possibly present (false positives are possible but rare).
First, compute the GNU hash of the symbol name:
static uint32_t sym2srec_getGnuHash(const uint8_t* p_symbolName)
{
uint32_t l_hash = 5381;
for (; *p_symbolName != '\0'; p_symbolName++)
{
l_hash = (l_hash * 33) + (uint8_t) *p_symbolName;
}
return l_hash;
}Then query the bloom filter:
#define K_BLOOM_WORD_BITS 32
uint32_t l_hash = sym2srec_getGnuHash(p_symbolName);
/* Compute the bloom filter mask for this symbol */
uint32_t l_bloomMask =
(1u << (l_hash % K_BLOOM_WORD_BITS)) |
(1u << ((l_hash >> l_hashTable.bloomFilterShift) % K_BLOOM_WORD_BITS));
/* Compute the index of the bloom filter word to check */
uint32_t l_bloomIndex = (l_hash / K_BLOOM_WORD_BITS) & (l_hashTable.bloomFilterSize - 1);
if ((l_hashTable.bloomAddr[l_bloomIndex] & l_bloomMask) == l_bloomMask)
{
/* Symbol may exist — proceed to bucket scan */
}
else
{
/* Symbol definitely does not exist — relocation failed */
}If the bloom filter passes, scan the hash table bucket to find the symbol:
uint32_t l_symbolIndex = l_hashTable.bucketsAddr[l_hash % l_hashTable.nbuckets];
if (l_symbolIndex >= l_hashTable.symbolsOffset)
{
uint32_t l_symbolHash = 0;
int l_found = 0;
ELF32SymbolTableEntry_t* l_symbolEntry = NULL;
while (((l_symbolHash & 0x1) == 0) && !l_found)
{
/* Get the current symbol entry */
l_symbolEntry = (ELF32SymbolTableEntry_t*)
((uint8_t*) l_header->symtabBaseAddr
+ l_symbolIndex * sizeof(ELF32SymbolTableEntry_t));
/* Read its hash value */
l_symbolHash = l_hashTable.hashValuesAddr[l_symbolIndex - l_hashTable.symbolsOffset];
/* Compare hashes (ignoring the chain-end bit) */
if ((l_symbolHash | 0x1) == (l_hash | 0x1))
{
/* Compare symbol names */
const char* l_name = (const char*) l_header->strtabBaseAddr
+ l_symbolEntry->stName;
if (strcmp(p_symbolName, l_name) == 0)
{
l_found = 1; /* Symbol found — l_symbolEntry is valid */
}
}
l_symbolIndex++;
}
}When a symbol is found, its attributes are available in the standard ELF32 symbol table entry structure:
typedef struct
{
uint32_t stName; /* Index into the string table */
uint32_t* stValue; /* Symbol address in memory */
uint32_t stSize; /* Symbol size in bytes */
uint8_t stInfo; /* Symbol type and binding (STT_* / STB_*) */
uint8_t stOther; /* Visibility (reserved, currently 0) */
uint16_t stShndx; /* Section header table index */
} ELF32SymbolTableEntry_t;stValue contains the symbol's runtime address, which the loader uses to patch
the relocation entry in the loaded application.
For details on how to apply ARM relocation types (R_ARM_RELATIVE, R_ARM_ABS32,
R_ARM_GLOB_DAT) once the symbol address is known, see the
Mk ELF Loader wiki page.
Sym2srec is called automatically by the Mk Makefile as the final step of make all.
The prebuilt executable is located at Mk/Make/sym2srec.exe in the
Mk repository.
The build pipeline works as follows:
make all
├── Compile all .c and .asm sources
├── Link → Mk.elf (firmware + full debug symbol table)
├── Strip → Mk-Strip.elf (debug symbols removed, only exported API symbols kept)
└── sym2srec Mk-Strip.elf Mk.srec 0x002C0000
└── Mk.srec (firmware + Mk API symbol table → only file to flash)
Stripping produces Mk-Strip.elf from Mk.elf by removing debug and local symbols.
Mk-Strip.elf is then passed to sym2srec so that only the exported API symbols that
external applications need to reference are embedded in Mk.srec. Mk.elf is preserved
intact for use as the debug symbol file in Eclipse/GDB.
Mk.srec is the only file written to the target. It is a self-describing S-Record
file that encodes both the firmware binary and the kernel symbol table with their target
addresses. J-Link writes it verbatim to FLASH:
| File | Format | Purpose |
|---|---|---|
Mk.srec |
S-Record | Flash this. Contains firmware (at 0x00200000) and kernel symbol table (at 0x002C0000) |
Mk.elf |
ELF32 | Do not flash. Load in Eclipse/GDB as the debug symbol file to map addresses back to source code |
When debugging, load Mk.elf in your Eclipse debug configuration as the symbol file.
GDB will use its .symtab and .strtab sections to resolve addresses to function names,
variable names, and source line numbers — without it being flashed to the target.
External applications reference Mk API functions with the extern keyword. When the
loader processes a R_ARM_GLOB_DAT relocation, it calls the symbol resolution protocol
described above to find the function address in Mk.srec and patches the GOT entry
of the loaded application accordingly.
A prebuilt Windows executable is available at
Mk/Make/sym2srec.exe. Build from source only if you need to modify the tool.
| Tool | Version |
|---|---|
| MinGW-W64 | gcc 8.1.0 (x86_64-posix-sjlj) |
| GNU Make | 4.x |
| Platform | Windows only — the Makefile uses Windows-specific commands |
Note: sym2srec must be compiled as a 32-bit binary (
-m32). This is required because it manipulates 32-bit ELF structures and pointer sizes must match.
- Install MinGW-W64 and add it to your
PATH. - Open
sym2srec/make/makefileand setTOOLCHAIN_PATHto your MinGW-W64bin/directory. - Build:
make clean
make all