Re: master: Extend save-lisp-and-die to write an ELF .o file

Stas Boukarev <[email protected]>
Newsgroups gmane.lisp.steel-bank.cvs,gmane.lisp.steel-bank.devel
Message-ID <CAF63=10XMFNZ4wkYKmHnZmEAqkL8StF2m+si3FeG25mTNPOMMw@mail.gmail.com>
Undefined symbols for architecture arm64:
"_generate_elfcore_obj", referenced from:
_gc_and_save in save.o
ld: symbol(s) not found for architecture arm64

On Sat, Jan 31, 2026 at 12:50 AM snuglas via Sbcl-commits
<[email protected]> wrote:
>
> The branch "master" has been updated in SBCL:
>        via  fcd4d116fe701f3da0865c6023a25c0d63c415e2 (commit)
>       from  d61e53f2b1c22213bc0cb0f09daa469bc6f10515 (commit)
>
> - Log -----------------------------------------------------------------
> commit fcd4d116fe701f3da0865c6023a25c0d63c415e2
> Author: Douglas Katzman <[email protected]>
> Date:   Fri Jan 30 16:46:29 2026 -0500
>
>     Extend save-lisp-and-die to write an ELF .o file
>
>     which embeds an SBCL core and allows linking without --export-dynamic.
>     This is a lighter-weight and possibly easier-to-use incarnation of elftool.
> ---
>  src/code/save.lisp      |   4 +-
>  src/runtime/GNUmakefile |  13 +++++
>  src/runtime/os-common.c | 146 ++++++++++++++++++++++++++++++++++++++++++++++++
>  src/runtime/save.c      |  63 +++++++++++++++++++--
>  tests/elfcore.test.sh   |  22 ++++++++
>  5 files changed, 241 insertions(+), 7 deletions(-)
>
> diff --git a/src/code/save.lisp b/src/code/save.lisp
> index 639507fc8..8d7bc30eb 100644
> --- a/src/code/save.lisp
> +++ b/src/code/save.lisp
> @@ -123,6 +123,8 @@ The following &KEY arguments are defined:
>       to create a standalone executable.  If false (the default), the
>       core image will not be executable on its own. Executable images
>       always behave as if they were passed the --noinform runtime option.
> +     If :EXECUTABLE is :ELF-OBJECT, then the resulting core will be
> +     wrapped in a .o which requires further linking. (EXPERIMENTAL)
>
>    :SAVE-RUNTIME-OPTIONS
>       If true, values of runtime options --dynamic-space-size and
> @@ -256,7 +258,7 @@ sufficiently motivated to do lengthy fixes."
>            ;; since the GC will invalidate the stack.
>            (sb-kernel::unsafe-clear-roots sb-vm:+highest-normal-generation+)
>            (gc-and-save name
> -                       (foreign-bool executable)
> +                       (if (eq executable :elf-object) 2 (foreign-bool executable))
>                         (foreign-bool purify)
>                         (case save-runtime-options
>                           (:accept-runtime-options 2)
> diff --git a/src/runtime/GNUmakefile b/src/runtime/GNUmakefile
> index ad682adbf..0858f8c43 100644
> --- a/src/runtime/GNUmakefile
> +++ b/src/runtime/GNUmakefile
> @@ -159,6 +159,19 @@ pie-shrinkwrap-sbcl: pie-shrinkwrap-sbcl.s pie-shrinkwrap-sbcl-core.o $(PIC_OBJS
>  semiwrap-sbcl: shrinkwrap-sbcl.s $(LIBSBCL)
>         $(CC) $(LINKFLAGS) $(CFLAGS) -o $@ $^ $(LIBS)
>
> +# simpler version of core embedding where the core is just a section of the ELF file.
> +# tools-for-build/elftool could kinda do this but it didn't work as well as it
> +# does using the ELF writer from C, because firstly the interface is less
> +# polished and secondly it did remove the need for -Wl,--export-dynamic.
> +# Note about $(LIBS) - we don't really need -ldl except os-common does reference it
> +# so removing it would break that. Also certain platforms need -lm and -lpthread
> +# to get in there so using the same link options as for the ordinary "sbcl"
> +# is just easiest.
> +embedcore-sbcl: $(LIBSBCL)
> +       ../../run-sbcl.sh --eval '(save-lisp-and-die "wrappedcore.o" :executable :elf-object)'
> +       $(CC) -no-pie $(filter-out -Wl$(comma)--export-dynamic, $(LINKFLAGS)) \
> + $(CFLAGS) -o $@ wrappedcore.o $^ $(LIBS)
> +
>  sbcl.mk: Config
>         ( echo 'CC=$(CC)' ; \
>           echo 'LD=$(LD)' ; \
> diff --git a/src/runtime/os-common.c b/src/runtime/os-common.c
> index 71495298e..60dd17e2f 100644
> --- a/src/runtime/os-common.c
> +++ b/src/runtime/os-common.c
> @@ -596,4 +596,150 @@ int apply_pie_relocs(long code_space_translation,
>      free(ptrs);
>      return success;
>  }
> +
> +#ifdef LISP_FEATURE_ARM64
> +# define ELF_MACHINE EM_AARCH64
> +# define CORE_ALIGNMENT 65536
> +# define OUR_RELOC_KIND R_AARCH64_ABS64
> +#else
> +# define ELF_MACHINE EM_X86_64
> +# define CORE_ALIGNMENT 32768
> +# define OUR_RELOC_KIND R_X86_64_64
> +#endif
> +
> +static uint32_t add_string(char *buffer, uint32_t *current_size, const char *str) {
> +    uint32_t offset = *current_size;
> +    strcpy(buffer + offset, str);
> +    *current_size += strlen(str) + 1;
> +    return offset;
> +}
> +
> +static void put_section(FILE* f, long offset, void* data, int size, int nmemb)
> +{
> +    fseek(f, offset, SEEK_SET);
> +    fwrite(data, size, nmemb, f);
> +}
> +
> +void generate_elfcore_obj(const char *filename,
> +                          FILE* input_core,
> +                          char **symbol_names, int symbol_count)
> +{
> +    size_t core_size = ftell(input_core);
> +    rewind(input_core);
> +
> +    // Prepare Section Header String Table
> +    char shstrtab[256] = {0};
> +    uint32_t shstr_size = 0;
> +    add_string(shstrtab, &shstr_size, "");
> +    uint32_t core_shname   = add_string(shstrtab, &shstr_size, "lisp.core");
> +    uint32_t table_shname  = add_string(shstrtab, &shstr_size, ".data"); // alien_table");
> +    uint32_t rela_shname   = add_string(shstrtab, &shstr_size, ".rela.data"); // alien_table");
> +    uint32_t note_shname   = add_string(shstrtab, &shstr_size, ".note.GNU-stack");
> +    uint32_t shstr_shname  = add_string(shstrtab, &shstr_size, ".shstrtab");
> +    uint32_t sym_shname = add_string(shstrtab, &shstr_size, ".symtab");
> +    uint32_t str_shname = add_string(shstrtab, &shstr_size, ".strtab");
> +
> +    // Prepare String Table
> +    const char anchor[] = "alien_linkage_values";
> +    uint32_t strtab_size = 1 + sizeof anchor;
> +    for (int i = 0; i < symbol_count; i++) strtab_size += strlen(symbol_names[i]) + 1;
> +    char* strtab = malloc(strtab_size);
> +    uint32_t str_ct = 0; // how many chars are written into strtab
> +    add_string(strtab, &str_ct, "");
> +    uint32_t anchor_strname = add_string(strtab, &str_ct, anchor);
> +
> +    // Prepare Symbol Table
> +    //   Index 1: alien_linkage_values (Defined, Global)
> +    //   Index 2...N: The External C symbols (Undefined, Global)
> +    int total_elf_syms = symbol_count + 2;
> +    Elf64_Sym *elf_syms = calloc(total_elf_syms, sizeof(Elf64_Sym));
> +
> +    elf_syms[1].st_name  = anchor_strname;
> +    elf_syms[1].st_info  = ELF64_ST_INFO(STB_GLOBAL, STT_NOTYPE);
> +    elf_syms[1].st_shndx = 2; // Index of .alien_table
> +    elf_syms[1].st_size  = 0;//symbol_count * N_WORD_BYTES;
> +    for (int i = 0; i < symbol_count; i++) {
> +        elf_syms[i+2].st_name  = add_string(strtab, &str_ct, symbol_names[i]);
> +        elf_syms[i+2].st_info  = ELF64_ST_INFO(STB_GLOBAL, STT_NOTYPE);
> +        elf_syms[i+2].st_shndx = SHN_UNDEF;
> +    }
> +    gc_assert(str_ct == strtab_size);
> +
> +    // Prepare alien_linkage_values
> +    size_t table_data_size = symbol_count * N_WORD_BYTES;
> +    uint64_t *table_data = calloc((unsigned int)symbol_count, 8); // Initialized to 0
> +    Elf64_Rela *relocs = calloc((unsigned int)symbol_count, sizeof(Elf64_Rela));
> +    for (int i = 0; i < symbol_count; i++) {
> +        relocs[i].r_offset = i * 8;
> +        // Syms start at index 2
> +        relocs[i].r_info   = ELF64_R_INFO(i + 2, OUR_RELOC_KIND);
> +        relocs[i].r_addend = 0;
> +    }
> +
> +    // Compute placement of data in the ELF file
> +    // Force the core to start at the first 32K boundary after the ELF header
> +    long core_offset  = CORE_ALIGNMENT;
> +    long table_offset = core_offset + core_size; // alien_linkage_values array goes here
> +    long rela_offset  = table_offset + ALIGN_UP(table_data_size, N_WORD_BYTES);
> +    long note_offset  = rela_offset + ALIGN_UP(symbol_count * sizeof(Elf64_Rela), N_WORD_BYTES);
> +    long shstr_offset = note_offset;
> +    long sym_offset   = shstr_offset + ALIGN_UP(shstr_size, N_WORD_BYTES);
> +    long str_offset   = sym_offset + ALIGN_UP(total_elf_syms * sizeof(Elf64_Sym), N_WORD_BYTES);
> +    long header_table_offset = str_offset + ALIGN_UP(strtab_size, N_WORD_BYTES);
> +
> +    /* in a .symtab section:
> +     *   sh_link is the index of its corresponding string section
> +     *   sh_info is the index of the first global symbol
> +     * in a .rela section:
> +     *   sh_link is the index of the symbol section
> +     *   sh_info is the section to patch
> +     */
> +    // 5. Section Headers (8 total: 0..7)
> +    Elf64_Shdr shdr[8] = {0};
> +    shdr[1] = (Elf64_Shdr){ .sh_name=core_shname,  .sh_type=SHT_PROGBITS, .sh_flags=SHF_ALLOC,
> +      .sh_offset=core_offset, .sh_size=core_size, .sh_addralign=CORE_ALIGNMENT
> +    };
> +    shdr[2] = (Elf64_Shdr){ .sh_name=table_shname, .sh_type=SHT_PROGBITS, .sh_flags=SHF_ALLOC|SHF_WRITE,
> +      .sh_offset=table_offset, .sh_size=table_data_size, .sh_addralign=8
> +    };
> +    shdr[3] = (Elf64_Shdr){ .sh_name=rela_shname,  .sh_type=SHT_RELA,     .sh_offset=rela_offset,
> +      .sh_size=symbol_count*sizeof(Elf64_Rela), .sh_link=6, .sh_info=2, .sh_entsize=sizeof(Elf64_Rela)
> +    };
> +    shdr[4] = (Elf64_Shdr){ .sh_name=note_shname,  .sh_type=SHT_PROGBITS, .sh_offset=note_offset };
> +    shdr[5] = (Elf64_Shdr){ .sh_name=shstr_shname, .sh_type=SHT_STRTAB,   .sh_offset=shstr_offset,
> +      .sh_size=shstr_size
> +    };
> +    shdr[6] = (Elf64_Shdr){ .sh_name=sym_shname,   .sh_type=SHT_SYMTAB,   .sh_offset=sym_offset,
> +      .sh_size=total_elf_syms*sizeof(Elf64_Sym), .sh_link=7, .sh_info=1, .sh_entsize=sizeof(Elf64_Sym)
> +    };
> +    shdr[7] = (Elf64_Shdr){ .sh_name=str_shname,   .sh_type=SHT_STRTAB,   .sh_offset=str_offset,
> +      .sh_size=strtab_size
> +    };
> +
> +    Elf64_Ehdr ehdr = {
> +        .e_ident = { ELFMAG0, ELFMAG1, ELFMAG2, ELFMAG3, ELFCLASS64, ELFDATA2LSB, EV_CURRENT },
> +        .e_type = ET_REL, .e_machine = ELF_MACHINE, .e_version = EV_CURRENT,
> +        .e_ehsize = sizeof(Elf64_Ehdr), .e_shentsize = sizeof(Elf64_Shdr),
> +        .e_shnum = 8, .e_shstrndx = 5, .e_shoff = header_table_offset
> +    };
> +
> +
> +    FILE *f = fopen(filename, "wb");
> +    char buffer[4096];
> +    int nread;
> +    fwrite(&ehdr, 1, sizeof(ehdr), f);
> +    fseek(f, core_offset, SEEK_SET);
> +    while ((nread = fread(buffer, 1, 4096, input_core)) > 0)
> +      fwrite(buffer, 1, nread, f);
> +
> +    put_section(f, table_offset, table_data, table_data_size, 1);
> +    put_section(f, rela_offset, relocs, sizeof(Elf64_Rela), symbol_count);
> +    put_section(f, shstr_offset, shstrtab, shstr_size, 1);
> +    put_section(f, sym_offset, elf_syms, sizeof(Elf64_Sym), total_elf_syms);
> +    put_section(f, str_offset, strtab, strtab_size, 1);
> +    put_section(f, header_table_offset, shdr, sizeof(Elf64_Shdr), 8);
> +    fclose(f);
> +    // Should really free() some stuff but it's not a leak since this program is now exiting
> +}
> +
>  #endif
> diff --git a/src/runtime/save.c b/src/runtime/save.c
> index c048ed665..f38712658 100644
> --- a/src/runtime/save.c
> +++ b/src/runtime/save.c
> @@ -33,6 +33,7 @@
>  #include "gc.h"
>  #include "thread.h"
>  #include "arch.h"
> +#include "genesis/hash-table.h"
>  #include "genesis/static-symbols.h"
>  #include "genesis/symbol.h"
>  #include "genesis/vector.h"
> @@ -279,6 +280,9 @@ void save_to_filehandle(FILE *file, char *filename, lispobj init_function,
>      /* (Now we can actually start copying ourselves into the output file.) */
>
>      if (verbose) {
> +        /* This shows the temporary file name if writing a .o file. I think that's
> +         * a reasonable choice, though we could certainly make an extra copy
> +         * of the name as originally specified */
>          printf("[saving current Lisp image into %s:\n", filename);
>          fflush(stdout);
>      }
> @@ -707,6 +711,8 @@ static void prepare_dynamic_space_for_final_gc(struct thread* thread)
>  char gc_coalesce_string_literals = 0;
>
>  extern void move_rospace_to_dynamic(int), prepare_readonly_space(int,int);
> +extern bool generate_elfcore_obj(const char *filename, FILE* input_core,
> +                                 char **syms, int sym_count);
>
>  /* Do a non-conservative GC twice, and then save a core with the initial
>   * function being set to the value of 'lisp_init_function'.
> @@ -743,10 +749,13 @@ extern void move_rospace_to_dynamic(int), prepare_readonly_space(int,int);
>   *  as empty pages, because we can't represent discontiguous ranges.
>   */
>  void
> -gc_and_save(char *filename, bool prepend_runtime, bool purify,
> +gc_and_save(char *filename, int core_format, bool purify,
>              int save_runtime_options, bool compressed,
>              int compression_level, int application_type)
>  {
> +    int prepend_runtime = core_format == 1;
> +    int elf_object = core_format == 2;
> +
>      // FIXME: Instead of disabling purify for static space relocation,
>      // we should make r/o space read-only after fixing up pointers to
>      // static space instead.
> @@ -768,14 +777,22 @@ gc_and_save(char *filename, bool prepend_runtime, bool purify,
>      extern void coalesce_similar_objects();
>      bool verbose = !lisp_startup_options.noinform;
>
> +    if (!elf_object) {
> +        /* The filename might come from Lisp, and be moved by the now
> +         * non-conservative GC. */
> +      filename = strdup(filename);
> +    } else {
> +      int tempnamelen = strlen(filename) + 5; // ".tmp"
> +      char* copy = checked_malloc(tempnamelen);
> +      snprintf(copy, tempnamelen, "%s.tmp", filename);
> +      filename = copy;
> +    }
>      file = prepare_to_save(filename, prepend_runtime, &runtime_bytes,
>                             &runtime_size);
> -    if (file == NULL)
> +    if (file == NULL) {
> +       free(filename);
>         return;
> -
> -    /* The filename might come from Lisp, and be moved by the now
> -     * non-conservative GC. */
> -    filename = strdup(filename);
> +    }
>
>      /* We're destined for process exit at this point, and interrupts can not
>       * possibly be handled in Lisp. The installed signal handler closures should
> @@ -868,8 +885,42 @@ gc_and_save(char *filename, bool prepend_runtime, bool purify,
>          save_runtime_to_filehandle(file, runtime_bytes, runtime_size,
>                                     application_type);
>
> +    char** elf_c_symbols = 0;
> +    int n_symbols = 0;
> +    if (elf_object) {
> +        // Find SB-SYS:*LINKAGE-INFO*
> +        lispobj* sym = find_symbol("*LINKAGE-INFO*", get_package_by_id(PACKAGE_ID_SYS));
> +        lispobj value = ((struct symbol*)sym)->value;
> +        gc_assert(instancep(CONS(value)->car));
> +        struct hash_table* ht = (void*)native_pointer(CONS(value)->car);
> +        gc_assert(simple_vector_p(ht->pairs));
> +        struct vector* kvv = (void*)native_pointer(ht->pairs);
> +        n_symbols = fixnum_value(ht->_count);
> +        gc_assert(fixnum_value(kvv->data[0]) == n_symbols); // KVV's high-water mark
> +        if (verbose) {
> +            printf("[linkage info: %d symbols]\n", n_symbols);
> +            fflush(stdout);
> +        }
> +        elf_c_symbols = calloc(n_symbols, sizeof (char*));
> +        int i;
> +        for (i=0; i<n_symbols; ++i) {
> +            lispobj key = kvv->data[(1+i)<<1];
> +            // string is code symbol, singleton cons of string is a data symbol
> +            if (listp(key)) key = CONS(key)->car;
> +            struct vector* c_symbol = VECTOR(key);
> +            elf_c_symbols[i] = (char*)c_symbol->data;
> +        }
> +    }
>      save_to_filehandle(file, filename, lisp_init_function,
>                         prepend_runtime, save_runtime_options,
>                         compressed ? compression_level : COMPRESSION_LEVEL_NONE);
> +    if (elf_object) {
> +        file = fopen(filename, "r"); // reopen it for reading
> +        unlink(filename);
> +        filename[strlen(filename)-4] = '\0'; // chop ".tmp" from the end
> +        fseek(file, 0, SEEK_END);
> +        generate_elfcore_obj(filename, file, elf_c_symbols, n_symbols);
> +        printf("[Converted to ELF]\n");
> +    }
>      exit(0);
>  }
> diff --git a/tests/elfcore.test.sh b/tests/elfcore.test.sh
> index 2b3a3251c..801d99a8f 100755
> --- a/tests/elfcore.test.sh
> +++ b/tests/elfcore.test.sh
> @@ -15,6 +15,28 @@
>
>  . ./subr.sh
>
> +# The 'embedcore-sbcl' test works for more configurations than the rest of the
> +# tests in this file do, so try it first. If this can't be run, neither can
> +# anything else.
> +run_sbcl <<EOF
> +  #+(and linux (or arm64 x86-64)) (exit :code 0) ; good
> +  (exit :code 2) ; otherwise
> +EOF
> +status=$?
> +if [ $status != 0 ]; then # test can't be executed
> +    # we don't have a way to exit shell tests with "inapplicable" as the result
> +    exit $EXIT_TEST_WIN
> +fi
> +# Ensure that we're not running a stale embedcore-sbcl
> +(cd $SBCL_PWD/../src/runtime ; rm -f embedcore-sbcl ; make embedcore-sbcl)
> +
> +set -e # exit on error
> +$SBCL_PWD/../src/runtime/embedcore-sbcl --disable-debugger --no-sysinit --no-userinit --noprint <<EOF
> +(format t "~&ELF-embedded core starts OK~%")
> +(exit :code 0)
> +EOF
> +set +e # no exit on error
> +
>  run_sbcl <<EOF
>    #+(and linux elf sb-thread)
>    (let ((s (find-symbol "IMMOBILE-SPACE-OBJ-P" "SB-KERNEL")))
>
> -----------------------------------------------------------------------
>
>
> hooks/post-receive
> --
> SBCL
>
>
> _______________________________________________
> Sbcl-commits mailing list
> [email protected]
> https://lists.sourceforge.net/lists/listinfo/sbcl-commits


_______________________________________________
Sbcl-commits mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/sbcl-commits
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.