Skip to content
EmbSoft3 edited this page Feb 20, 2026 · 3 revisions

Sym2srec

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.


Table of contents


Concepts

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.


Quick start

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

Command-line reference

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

Stripping the input file first (recommended)

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.


Output format — S-Record layout

Sym2srec parses the input ELF file and performs the following steps:

  1. Builds a GNU hash table (.gnuhash) from the .symtab and .strtab sections.
  2. Builds a SymbolsAreaHeader_t header pointing to all three tables.
  3. Copies all existing loadable segments from the ELF file to the S-Record.
  4. Appends .symtab, .strtab, and .gnuhash as 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

S-Record layout


Symbol resolution protocol

This section describes the complete protocol a dynamic loader must implement to resolve a symbol by name using the data produced by sym2srec.

SymbolsAreaHeader_t

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;

GNU hash table

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];

Bloom filter lookup

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 */
}

Bucket scan

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++;
    }
}

Symbol entry

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.


Integration with Mk

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.


Build from source

A prebuilt Windows executable is available at Mk/Make/sym2srec.exe. Build from source only if you need to modify the tool.

Requirements

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.

Steps

  1. Install MinGW-W64 and add it to your PATH.
  2. Open sym2srec/make/makefile and set TOOLCHAIN_PATH to your MinGW-W64 bin/ directory.
  3. Build:
make clean
make all

References