From 35b21a95c903bdf038af826cb98f893bd455d331 Mon Sep 17 00:00:00 2001 From: Yu-En Hsiao Date: Wed, 9 Sep 2026 21:08:31 +0800 Subject: [PATCH 1/4] Implement immediate binding for dynamic linking Since the original codebase already implemented the immediate binding flag for the x86-64 and AArch64 architectures, this commit introduces a "-z" option so that the Arm32 and RV32 targets can also use immediate binding. When the command-line arguments contain "-z now", the compiler sets its internal flag (imm_binding) to true and generates the Elf32_Dyn/Elf64_Dyn objects to enable exectuables to perform immediate binding at runtime. For 64-bit targets, immediate binding is always used under the dynamic linking mode as is. --- mk/arm64.mk | 1 - mk/x64.mk | 1 - src/defs.h | 7 ------- src/elf.c | 11 +++++++---- src/globals.c | 1 + src/main.c | 26 ++++++++++++++++++++++++-- 6 files changed, 32 insertions(+), 15 deletions(-) diff --git a/mk/arm64.mk b/mk/arm64.mk index a3a4151a..c2d96bc2 100644 --- a/mk/arm64.mk +++ b/mk/arm64.mk @@ -26,7 +26,6 @@ ARCH_DEFS = \ \#define PLT_ENT_SIZE 16\n$\ \#define RESERVED_GOT_NUM 3\n$\ \#define R_ARCH_JUMP_SLOT 1026 /* R_AARCH64_JUMP_SLOT */\n$\ - \#define DYN_BIND_NOW 1\n$\ " # An Arm64 Linux host runs this target's output itself, so nothing has to stand diff --git a/mk/x64.mk b/mk/x64.mk index 98c73210..877c1ed8 100644 --- a/mk/x64.mk +++ b/mk/x64.mk @@ -17,7 +17,6 @@ ARCH_DEFS = \ \#define RESERVED_GOT_NUM 3\n$\ \#define R_ARCH_JUMP_SLOT 7 /* R_X86_64_JUMP_SLOT */\n$\ \#define REG_CNT 11 /* rdi rsi rdx rcx r8 r9 rax rbx r14 r12 r13 */\n$\ - \#define DYN_BIND_NOW 1 /* this PLT has no lazy-resolution path */\n$\ \#define HAVE_COND_MOVE 1 /* CMOVcc */\n$\ \#define CALLEE_SAVED_REGS 4 /* the file ends rbx r14 r12 r13 */\n$\ " diff --git a/src/defs.h b/src/defs.h index 647b6799..a3be7731 100644 --- a/src/defs.h +++ b/src/defs.h @@ -192,13 +192,6 @@ #define ALIGN_UP(val, align) (((val) + (align) - 1) & ~((align) - 1)) #endif -/* Targets whose PLT has no lazy-resolution path ask the loader to bind every - * PLT entry at load time. - */ -#ifndef DYN_BIND_NOW -#define DYN_BIND_NOW 0 -#endif - #define ELF_MACHINE_ARM32 0x28 #define ELF_MACHINE_RV32 0xf3 #define ELF_MACHINE_X86_64 0x3e diff --git a/src/elf.c b/src/elf.c index c2ff605f..17098dee 100644 --- a/src/elf.c +++ b/src/elf.c @@ -1112,14 +1112,17 @@ void elf_generate_dynamic_sections(void) elf_write_dyn(dynamic_sections.elf_dynamic, 0x1, 0x1); if (libdl_name) elf_write_dyn(dynamic_sections.elf_dynamic, 0x1, libdl_name); -#if DYN_BIND_NOW == 1 + /* Resolve every PLT entry at load time. This target's PLT[0] does not * arrange the GOT[1]/GOT[2] hand-off the lazy resolver needs, so the loader * writes the final addresses straight into the GOT instead. */ - elf_write_dyn(dynamic_sections.elf_dynamic, 0x18, 0x0); /* DT_BIND_NOW */ - elf_write_dyn(dynamic_sections.elf_dynamic, 0x1e, 0x8); /* DF_BIND_NOW */ -#endif + if (imm_binding) { + elf_write_dyn(dynamic_sections.elf_dynamic, 0x18, + 0x0); /* DT_BIND_NOW */ + elf_write_dyn(dynamic_sections.elf_dynamic, 0x1e, + 0x8); /* DF_BIND_NOW */ + } elf_write_dyn(dynamic_sections.elf_dynamic, 0x0, 0x0); } diff --git a/src/globals.c b/src/globals.c index 2dad38be..3ba7e0c9 100644 --- a/src/globals.c +++ b/src/globals.c @@ -116,6 +116,7 @@ dynamic_sections_t dynamic_sections; /* Command line compilation flags */ bool dynlink = false; +bool imm_binding = false; bool libc = true; bool expand_only = false; bool dump_ir = false; diff --git a/src/main.c b/src/main.c index 4b3aa5d6..49d36311 100644 --- a/src/main.c +++ b/src/main.c @@ -121,7 +121,18 @@ int main(int argc, char *argv[]) libc = false; else if (!strcmp(argv[i], "--dynlink")) dynlink = true; - else if (!strcmp(argv[i], "-E")) + else if (!strcmp(argv[i], "-z")) { + if (i + 1 >= argc) + usage_error("-z requires \"lazy\" or \"now\""); + + if (!strcmp(argv[i + 1], "lazy")) + imm_binding = false; + else if (!strcmp(argv[i + 1], "now")) + imm_binding = true; + else + usage_error("-z requires \"lazy\" or \"now\""); + i++; + } else if (!strcmp(argv[i], "-E")) expand_only = true; else if (!strcmp(argv[i], "-I")) { if (i + 1 >= argc) @@ -145,11 +156,22 @@ int main(int argc, char *argv[]) in = argv[i]; } + if (dynlink) { + switch (ELF_MACHINE) { + /* The following 64-bit targets have no lazy-resolution path, so + * immediate binding must be used. + */ + case ELF_MACHINE_X86_64: + case ELF_MACHINE_AARCH64: + imm_binding = true; + } + } + if (!in) { printf( "Usage: shecc [-I directory] [-o output] [+m] [--dot] [--dump-ir] " "[--warn-string-literals] [--std=c99] [--no-libc] " - "[--dynlink] [-E] \n"); + "[--dynlink] [-z ] [-E] \n"); usage_error("Missing source file"); } From 0bf54df2b77829ebd1525012120080b9b51cd266 Mon Sep 17 00:00:00 2001 From: Yu-En Hsiao Date: Wed, 9 Sep 2026 21:26:17 +0800 Subject: [PATCH 2/4] Introduce a Makefile variable to determine the binding strategy Since the compiler supports the "-z" option for lazy or immediate binding, a new "BINDING" variable has benn added to the Makefile so that users can choose which binding mode to use. For example, users can build dynamically linked compilers with immediate binding as follows: $ make DYNLINK=1 BINDING=now --- Makefile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 434ca363..cfb8e4cd 100644 --- a/Makefile +++ b/Makefile @@ -84,12 +84,13 @@ TRANSLATION_DEFS = "\#define SHECC_TRANSLATION_DATE \"$(TRANSLATION_DATE)\"\n\#d STAGE0_FLAGS ?= --dump-ir STAGE1_FLAGS ?= DYNLINK ?= 0 +BINDING ?= lazy COMMENTFLOW ?= commentflow SHFMT ?= shfmt ifeq ($(DYNLINK),1) - STAGE0_FLAGS += --dynlink - STAGE1_FLAGS += --dynlink + STAGE0_FLAGS += --dynlink -z $(BINDING) + STAGE1_FLAGS += --dynlink -z $(BINDING) endif SRCS := $(wildcard $(patsubst %,%/main.c, $(SRCDIR))) From ba1aa7d514aa7914ac6f72fbdac714208221c006 Mon Sep 17 00:00:00 2001 From: Yu-En Hsiao Date: Tue, 15 Sep 2026 21:53:46 +0800 Subject: [PATCH 3/4] Improve test suites to validate immediate binding mode Since the compiler has implemented dynamic linking with immediate binding, the test suites have been enhance to validate immediate binding mode. The approach is to add an additional command-line argument to determine which mode, lazy or immediate, is enabled during the tests. --- Makefile | 8 ++++---- tests/arm-abi.sh | 6 ++++-- tests/arm64-abi.sh | 4 ++-- tests/driver.sh | 2 +- tests/riscv-abi.sh | 6 ++++-- tests/x64-abi.sh | 6 ++++-- 6 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index cfb8e4cd..594a60f3 100644 --- a/Makefile +++ b/Makefile @@ -237,11 +237,11 @@ uninstall-hooks: check-stage0: $(OUT)/$(STAGE0) tests/driver.sh $(VECHO) " TEST STAGE 0\n" - tests/driver.sh 0 $(DYNLINK) + tests/driver.sh 0 $(DYNLINK) $(BINDING) check-stage2: $(OUT)/$(STAGE2) tests/driver.sh $(VECHO) " TEST STAGE 2\n" - tests/driver.sh 2 $(DYNLINK) + tests/driver.sh 2 $(DYNLINK) $(BINDING) check-sanitizer: $(OUT)/$(STAGE0)-sanitizer tests/driver.sh $(VECHO) " TEST STAGE 0 (with sanitizers)\n" @@ -250,10 +250,10 @@ check-sanitizer: $(OUT)/$(STAGE0)-sanitizer tests/driver.sh $(Q)rm $(OUT)/shecc check-abi-stage0: $(OUT)/$(STAGE0) - tests/$(ARCH)-abi.sh 0 $(DYNLINK); + tests/$(ARCH)-abi.sh 0 $(DYNLINK) $(BINDING); check-abi-stage2: $(OUT)/$(STAGE2) - tests/$(ARCH)-abi.sh 2 $(DYNLINK); + tests/$(ARCH)-abi.sh 2 $(DYNLINK) $(BINDING); # Both prerequisites are order-only, and both exist because "make -j" would # otherwise let a compile start beside the thing it reads. Selecting a target diff --git a/tests/arm-abi.sh b/tests/arm-abi.sh index 263ccc19..911245f7 100755 --- a/tests/arm-abi.sh +++ b/tests/arm-abi.sh @@ -41,9 +41,10 @@ fi # Command Line Arguments if [ "$#" -lt 1 ]; then - echo "Usage: $0 []" + echo "Usage: $0 [ []]" echo " stage: 0 (host compiler), 1 (stage1), or 2 (stage2)" echo " dynlink: 0 (static linking), 1 (dynamic linking)" + echo " binding: lazy, now" echo "" echo "Environment Variables:" echo " VERBOSE=1 Enable verbose output" @@ -73,6 +74,7 @@ case "$1" in esac DYNLINK="${2:-0}" +BINDING="${3:-lazy}" # Banner echo -e "${BLUE}${BOLD}========================================${NC}" @@ -144,7 +146,7 @@ run_abi_test() # Compile local compile_cmd="$SHECC" if [[ "$DYNLINK" == "1" ]]; then - compile_cmd="$compile_cmd --dynlink" + compile_cmd="$compile_cmd --dynlink -z $BINDING" fi compile_cmd="$compile_cmd -o /tmp/shecc_abi_test_$$.elf $test_file" diff --git a/tests/arm64-abi.sh b/tests/arm64-abi.sh index 9a986cb4..5ad6c546 100755 --- a/tests/arm64-abi.sh +++ b/tests/arm64-abi.sh @@ -4,7 +4,7 @@ set -eu if [ "$#" -lt 1 ]; then - echo "Usage: $0 []" >&2 + echo "Usage: $0 [ []]" >&2 exit 2 fi @@ -23,7 +23,7 @@ case "$1" in esac if [ "${2:-0}" = 1 ]; then - shecc+=(--dynlink) + shecc+=(--dynlink -z "${3:-lazy}") link_mode=dynamic else link_mode=static diff --git a/tests/driver.sh b/tests/driver.sh index db80f40f..ea3380bd 100755 --- a/tests/driver.sh +++ b/tests/driver.sh @@ -106,7 +106,7 @@ case "$1" in esac if [ $# -ge 2 ] && [ "$2" = "1" ]; then - readonly SHECC_CFLAGS="--dynlink" + readonly SHECC_CFLAGS="--dynlink ${3:-lazy}" readonly LINK_MODE="dynamic" else readonly SHECC_CFLAGS="" diff --git a/tests/riscv-abi.sh b/tests/riscv-abi.sh index 34c3aadb..1ea3ca51 100755 --- a/tests/riscv-abi.sh +++ b/tests/riscv-abi.sh @@ -41,9 +41,10 @@ fi # Command Line Arguments if [ "$#" -lt 1 ]; then - echo "Usage: $0 []" + echo "Usage: $0 [ []]" echo " stage: 0 (host compiler), 1 (stage1), or 2 (stage2)" echo " dynlink: 0 (static linking), 1 (dynamic linking)" + echo " binding: lazy, now" echo "" echo "Environment Variables:" echo " VERBOSE=1 Enable verbose output" @@ -73,6 +74,7 @@ case "$1" in esac DYNLINK="${2:-0}" +BINDING="${3:-lazy}" # Banner echo -e "${BLUE}${BOLD}========================================${NC}" @@ -144,7 +146,7 @@ run_abi_test() # Compile local compile_cmd="$SHECC" if [[ "$DYNLINK" == "1" ]]; then - compile_cmd="$compile_cmd --dynlink" + compile_cmd="$compile_cmd --dynlink -z $BINDING" fi compile_cmd="$compile_cmd -o /tmp/shecc_abi_test_$$.elf $test_file" diff --git a/tests/x64-abi.sh b/tests/x64-abi.sh index cf91f280..12e75fc2 100755 --- a/tests/x64-abi.sh +++ b/tests/x64-abi.sh @@ -42,9 +42,10 @@ fi # Command Line Arguments if [ "$#" -lt 1 ]; then - echo "Usage: $0 []" + echo "Usage: $0 [ []]" echo " stage: 0 (host compiler), 1 (stage1), or 2 (stage2)" echo " dynlink: 0 (static linking), 1 (dynamic linking)" + echo " binding: lazy, now" echo "" echo "Environment Variables:" echo " VERBOSE=1 Enable verbose output" @@ -74,6 +75,7 @@ case "$1" in esac DYNLINK="${2:-0}" +BINDING="${3:-lazy}" # Banner echo -e "${BLUE}${BOLD}========================================${NC}" @@ -149,7 +151,7 @@ run_abi_test() # Compile local compile_cmd="$SHECC" if [[ "$DYNLINK" == "1" ]]; then - compile_cmd="$compile_cmd --dynlink" + compile_cmd="$compile_cmd --dynlink -z $BINDING" fi compile_cmd="$compile_cmd -o /tmp/shecc_abi_test_$$.elf $test_file" From c4fbf698d73dc337b4c39f8c98f1aa7bcd1e41ce Mon Sep 17 00:00:00 2001 From: Yu-En Hsiao Date: Sun, 20 Sep 2026 21:47:57 +0800 Subject: [PATCH 4/4] Validate immediate binding mode in CI Since the test scripts can validate either lazy binding or immediate binding via an additional argument, the CI workflows are improved so that dynamic linking with immediate binding can be validated on the Arm32 and RISC-V architectures. Primary changes and considerations: - Add an additional 'binding_mode' parameter so that the GitHub Actions workflows cab verify immediate binding for Arm32 and RISC-V. - When CI tests the static linking mode, the 'binding_mode' parameter is ignored because static linking involves neither lazy nor immediate binding. - The x86-64 and Arm64 architectures use immediate binding by default, so neither of them tests lazy binding. --- .github/workflows/main.yml | 63 +++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 81a7c242..42a3eff5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,7 @@ jobs: # Bootstrap each target and run the consolidated behavioral suite against # the stage 0 and stage 2 compilers. host-x86: - name: ${{ matrix.architecture }}/${{ matrix.link_mode }} (${{ matrix.compiler }}) + name: ${{ matrix.architecture }}/${{ matrix.link_mode }}/${{ matrix.link_mode == 'dynamic' && (matrix.binding_mode == 'immediate' && 'immediate' || 'default') || 'none' }} (${{ matrix.compiler }}) runs-on: ubuntu-24.04 timeout-minutes: 30 strategy: @@ -32,10 +32,28 @@ jobs: compiler: [gcc, clang] architecture: [arm, arm64, riscv, x64] link_mode: [static, dynamic] + include: + - compiler: gcc + architecture: arm + link_mode: dynamic + binding_mode: immediate + - compiler: clang + architecture: arm + link_mode: dynamic + binding_mode: immediate + - compiler: gcc + architecture: riscv + link_mode: dynamic + binding_mode: immediate + - compiler: clang + architecture: riscv + link_mode: dynamic + binding_mode: immediate env: CC: ${{ matrix.compiler }} ARCH: ${{ matrix.architecture }} DYNLINK: ${{ matrix.link_mode == 'dynamic' && '1' || '0' }} + BINDING: ${{ matrix.binding_mode == 'immediate' && 'now' || 'lazy' }} steps: - name: Checkout code uses: actions/checkout@v7 @@ -49,14 +67,14 @@ jobs: # environment: the build session the tree records assigns ARCH outright, # and a makefile assignment wins over the environment. - name: Build artifacts - run: make ARCH="$ARCH" DYNLINK="$DYNLINK" + run: make ARCH="$ARCH" DYNLINK="$DYNLINK" BINDING="$BINDING" - name: Unit tests - run: make check ARCH="$ARCH" DYNLINK="$DYNLINK" + run: make check ARCH="$ARCH" DYNLINK="$DYNLINK" BINDING="$BINDING" - name: Upload the test logs if: failure() uses: actions/upload-artifact@v7 with: - name: logs-${{ matrix.compiler }}-${{ matrix.architecture }}-${{ matrix.link_mode }} + name: logs-${{ matrix.compiler }}-${{ matrix.architecture }}-${{ matrix.link_mode }}-${{ matrix.link_mode == 'dynamic' && (matrix.binding_mode == 'immediate' && 'immediate' || 'default') || 'none' }} path: | out/*.log if-no-files-found: ignore @@ -68,7 +86,7 @@ jobs: # sanitizer step of host-x86 used to do. Building "sanitizer" first, in a # job that builds nothing else, is what puts the instrumentation in. sanitizer: - name: Sanitizers ${{ matrix.architecture }}/${{ matrix.link_mode }} + name: Sanitizers ${{ matrix.architecture }}/${{ matrix.link_mode }}/${{ matrix.link_mode == 'dynamic' && (matrix.binding_mode == 'immediate' && 'immediate' || 'default') || 'none' }} runs-on: ubuntu-24.04 timeout-minutes: 30 strategy: @@ -82,9 +100,17 @@ jobs: # why first. architecture: [arm, arm64, riscv, x64] link_mode: [static, dynamic] + include: + - architecture: arm + link_mode: dynamic + binding_mode: immediate + - architecture: riscv + link_mode: dynamic + binding_mode: immediate env: ARCH: ${{ matrix.architecture }} DYNLINK: ${{ matrix.link_mode == 'dynamic' && '1' || '0' }} + BINDING: ${{ matrix.binding_mode == 'immediate' && 'now' || 'lazy' }} steps: - name: Checkout code uses: actions/checkout@v7 @@ -95,9 +121,9 @@ jobs: link-mode: ${{ matrix.link_mode }} github-token: ${{ github.token }} - name: Build the stage 0 compiler with sanitizers - run: make sanitizer ARCH="$ARCH" DYNLINK="$DYNLINK" + run: make sanitizer ARCH="$ARCH" DYNLINK="$DYNLINK" BINDING="$BINDING" - name: Sanitizer-enabled stage 0 tests - run: make check-sanitizer ARCH="$ARCH" DYNLINK="$DYNLINK" + run: make check-sanitizer ARCH="$ARCH" DYNLINK="$DYNLINK" BINDING="$BINDING" # Preprocess shecc with itself, then compile the result: the stage 1 source # is the largest input the preprocessor gets, and the only one that exercises @@ -135,7 +161,7 @@ jobs: # AArch64 that is the only place the real loader is exercised: QEMU-user maps # the image itself, and on an x86-64 host it always presents 4 KiB pages. host-arm: - name: ${{ matrix.architecture }}/${{ matrix.link_mode }} on Arm64 + name: ${{ matrix.architecture }}/${{ matrix.link_mode }}/${{ matrix.link_mode == 'dynamic' && (matrix.binding_mode == 'immediate' && 'immediate' || 'default') || 'none' }} on Arm64 runs-on: ubuntu-24.04-arm timeout-minutes: 30 strategy: @@ -143,9 +169,14 @@ jobs: matrix: architecture: [arm, arm64] link_mode: [static, dynamic] + include: + - architecture: arm + link_mode: dynamic + binding_mode: immedaite env: ARCH: ${{ matrix.architecture }} DYNLINK: ${{ matrix.link_mode == 'dynamic' && '1' || '0' }} + BINDING: ${{ matrix.binding_mode == 'immediate' && 'now' || 'lazy' }} steps: - name: Checkout code uses: actions/checkout@v7 @@ -156,14 +187,14 @@ jobs: link-mode: ${{ matrix.link_mode }} github-token: ${{ github.token }} - name: Build artifacts - run: make ARCH="$ARCH" DYNLINK="$DYNLINK" + run: make ARCH="$ARCH" DYNLINK="$DYNLINK" BINDING="$BINDING" - name: Unit tests - run: make check ARCH="$ARCH" DYNLINK="$DYNLINK" + run: make check ARCH="$ARCH" DYNLINK="$DYNLINK" BINDING="$BINDING" - name: Upload the test logs if: failure() uses: actions/upload-artifact@v7 with: - name: logs-arm64-host-${{ matrix.architecture }}-${{ matrix.link_mode }} + name: logs-arm64-host-${{ matrix.architecture }}-${{ matrix.link_mode }}-${{ matrix.link_mode == 'dynamic' && (matrix.binding_mode == 'immediate' && 'immediate' || 'default') || 'none' }} path: | out/*.log out/tests/*.log @@ -174,15 +205,19 @@ jobs: # build that produces the object files, and that job has already made them # without instrumentation. host-arm-sanitizer: - name: Sanitizers arm/${{ matrix.link_mode }} on Arm64 + name: Sanitizers arm/${{ matrix.link_mode }}/${{ matrix.link_mode == 'dynamic' && (matrix.binding_mode == 'immediate' && 'immediate' || 'default') || 'none' }} on Arm64 runs-on: ubuntu-24.04-arm timeout-minutes: 30 strategy: fail-fast: false matrix: link_mode: [static, dynamic] + include: + - link_mode: dynamic + binding_mode: immediate env: DYNLINK: ${{ matrix.link_mode == 'dynamic' && '1' || '0' }} + BINDING: ${{ matrix.binding_mode == 'immediate' && 'now' || 'lazy' }} steps: - name: Checkout code uses: actions/checkout@v7 @@ -193,9 +228,9 @@ jobs: link-mode: ${{ matrix.link_mode }} github-token: ${{ github.token }} - name: Build the stage 0 compiler with sanitizers - run: make sanitizer ARCH=arm DYNLINK="$DYNLINK" + run: make sanitizer ARCH=arm DYNLINK="$DYNLINK" BINDING="$BINDING" - name: Sanitizer-enabled stage 0 tests - run: make check-sanitizer ARCH=arm DYNLINK="$DYNLINK" + run: make check-sanitizer ARCH=arm DYNLINK="$DYNLINK" BINDING="$BINDING" coding-style: name: Coding style