summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorkx <kx@radix-linux.su>2026-10-09 18:00:15 +0300
committerkx <kx@radix-linux.su>2026-10-09 18:00:15 +0300
commit065da3509019b15dd0e678072e6be629e5330bef (patch)
tree69c934c9e786ac048b4fbe8f4c4c5571661521f4 /tests
parentd130fe27fa753be02ed76fd9b277aa3c3d928eb9 (diff)
downloadlibmcpu-0.4.0.tar.xz
Version 0.4.00.4.0
Diffstat (limited to 'tests')
-rw-r--r--tests/Makefile.am156
-rw-r--r--tests/fixtures/argc-argv.s44
-rw-r--r--tests/fixtures/block-copy.s46
-rw-r--r--tests/fixtures/byte-swap.s15
-rw-r--r--tests/fixtures/call.s39
-rw-r--r--tests/fixtures/frame-pointer.s63
-rw-r--r--tests/fixtures/hll/libc-stub.S42
-rw-r--r--tests/fixtures/hll/model-pendulum-life.S133
-rw-r--r--tests/fixtures/hll/program-main.S48
-rw-r--r--tests/fixtures/indirect-control.s39
-rw-r--r--tests/fixtures/life-print.s30
-rw-r--r--tests/fixtures/model-lifecycle.s53
-rw-r--r--tests/fixtures/mpu-ops.s25
-rw-r--r--tests/fixtures/psw-call-abi.s36
-rw-r--r--tests/fixtures/radd-pi.s24
-rw-r--r--tests/fixtures/shared-libs/dynamic-liba.s18
-rw-r--r--tests/fixtures/shared-libs/dynamic-libb.s18
-rw-r--r--tests/fixtures/shared-libs/dynamic-main.s35
-rw-r--r--tests/fixtures/stack.s32
-rwxr-xr-xtests/tst-abi3-final-runner.sh78
-rw-r--r--tests/tst-aggregate.c68
-rwxr-xr-xtests/tst-ar-runner.sh122
-rw-r--r--tests/tst-asm-directives.c84
-rw-r--r--tests/tst-assembler.c60
-rwxr-xr-xtests/tst-block-copy-runner.sh9
-rw-r--r--tests/tst-block-copy.c171
-rwxr-xr-xtests/tst-bswap-runner.sh9
-rw-r--r--tests/tst-bswap.c105
-rw-r--r--tests/tst-bytecode.c56
-rw-r--r--tests/tst-byteorder.c170
-rwxr-xr-xtests/tst-call-runner.sh9
-rw-r--r--tests/tst-call.c100
-rw-r--r--tests/tst-context.c121
-rw-r--r--tests/tst-control.c80
-rw-r--r--tests/tst-conversion-flags.c204
-rwxr-xr-xtests/tst-doc-version-policy.sh44
-rw-r--r--tests/tst-dup.c187
-rwxr-xr-xtests/tst-dynamic-runner.sh96
-rw-r--r--tests/tst-dynamic.c407
-rwxr-xr-xtests/tst-env-preload-runner.sh124
-rw-r--r--tests/tst-errno-abi.c217
-rw-r--r--tests/tst-errno-map.c439
-rwxr-xr-xtests/tst-frame-runner.sh9
-rw-r--r--tests/tst-frame.c178
-rwxr-xr-xtests/tst-hll-crt-runner.sh338
-rwxr-xr-xtests/tst-immediate-exactness-runner.sh65
-rw-r--r--tests/tst-immediate-exactness.c100
-rw-r--r--tests/tst-indirect-control.c184
-rwxr-xr-xtests/tst-indirect-runner.sh9
-rw-r--r--tests/tst-instruction.c237
-rwxr-xr-xtests/tst-library-search-runner.sh118
-rwxr-xr-xtests/tst-lifecycle-runner.sh11
-rw-r--r--tests/tst-lifecycle.c63
-rwxr-xr-xtests/tst-line-runner.sh174
-rwxr-xr-xtests/tst-link-needed-runner.sh151
-rw-r--r--tests/tst-melf.c430
-rw-r--r--tests/tst-memory.c85
-rw-r--r--tests/tst-model-segments.c108
-rw-r--r--tests/tst-mpu-coverage.c580
-rwxr-xr-xtests/tst-mpu-ops-runner.sh9
-rwxr-xr-xtests/tst-needed-runtime-runner.sh193
-rwxr-xr-xtests/tst-nm-runner.sh167
-rwxr-xr-xtests/tst-objcopy-runner.sh160
-rwxr-xr-xtests/tst-objdump-runner.sh157
-rw-r--r--tests/tst-overlap.c104
-rwxr-xr-xtests/tst-partial-link-runner.sh276
-rw-r--r--tests/tst-pointer-arithmetic.c170
-rw-r--r--tests/tst-pointer-policy.c152
-rw-r--r--tests/tst-pointer.c154
-rwxr-xr-xtests/tst-psw-abi-runner.sh9
-rw-r--r--tests/tst-psw-abi.c133
-rw-r--r--tests/tst-psw-write.c255
-rwxr-xr-xtests/tst-readelf-runner.sh69
-rw-r--r--tests/tst-registers.c128
-rwxr-xr-xtests/tst-relocatable-runner.sh75
-rw-r--r--tests/tst-relocatable.c326
-rwxr-xr-xtests/tst-run-memory-options.sh121
-rwxr-xr-xtests/tst-runner.sh17
-rwxr-xr-xtests/tst-stack-runner.sh9
-rw-r--r--tests/tst-stack.c104
-rwxr-xr-xtests/tst-stage-actions-runner.sh111
-rwxr-xr-xtests/tst-static-archive-link-runner.sh263
-rwxr-xr-xtests/tst-static-option-runner.sh124
-rw-r--r--tests/tst-strip-fixture.c266
-rwxr-xr-xtests/tst-strip-runner.sh129
-rwxr-xr-xtests/tst-symbol-scope-runner.sh276
-rw-r--r--tests/tst-syscall.c229
-rwxr-xr-xtests/tst-tool-config-runner.sh97
-rw-r--r--tests/tst-tool-config.c45
-rwxr-xr-xtests/tst-tool-inspection-runner.sh80
-rwxr-xr-xtests/tst-verbose-runner.sh68
91 files changed, 11172 insertions, 0 deletions
diff --git a/tests/Makefile.am b/tests/Makefile.am
new file mode 100644
index 0000000..ba58979
--- /dev/null
+++ b/tests/Makefile.am
@@ -0,0 +1,156 @@
+
+AUTOMAKE_OPTIONS = subdir-objects
+
+AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_srcdir)/utils $(LIBMPU_CPPFLAGS) $(LIBMPUIO_CPPFLAGS)
+AM_CFLAGS = $(LIBMPU_CFLAGS) $(LIBMPUIO_CFLAGS)
+AM_LDFLAGS = $(LIBMPU_LDFLAGS) $(LIBMPUIO_LDFLAGS)
+test_programs = tst-errno-abi tst-errno-map tst-context tst-registers tst-overlap tst-instruction tst-bytecode tst-syscall tst-dup tst-assembler tst-control tst-stack tst-call tst-pointer tst-pointer-arithmetic tst-aggregate tst-frame tst-block-copy tst-memory tst-indirect-control tst-psw-abi tst-psw-write tst-byteorder tst-pointer-policy tst-bswap tst-mpu-coverage tst-immediate-exactness tst-conversion-flags tst-asm-directives tst-model-segments tst-lifecycle tst-relocatable tst-melf tst-dynamic
+check_PROGRAMS = tst-tool-config tst-strip-fixture $(test_programs)
+TESTS = $(test_programs) tst-runner.sh tst-stack-runner.sh tst-call-runner.sh tst-frame-runner.sh tst-block-copy-runner.sh tst-indirect-runner.sh tst-psw-abi-runner.sh tst-bswap-runner.sh tst-mpu-ops-runner.sh tst-immediate-exactness-runner.sh tst-lifecycle-runner.sh tst-relocatable-runner.sh tst-dynamic-runner.sh tst-line-runner.sh tst-run-memory-options.sh tst-stage-actions-runner.sh tst-readelf-runner.sh tst-tool-config-runner.sh tst-tool-inspection-runner.sh tst-library-search-runner.sh tst-link-needed-runner.sh tst-needed-runtime-runner.sh tst-symbol-scope-runner.sh tst-env-preload-runner.sh tst-abi3-final-runner.sh tst-ar-runner.sh tst-static-archive-link-runner.sh tst-partial-link-runner.sh tst-static-option-runner.sh tst-nm-runner.sh tst-objdump-runner.sh tst-strip-runner.sh tst-objcopy-runner.sh tst-hll-crt-runner.sh tst-verbose-runner.sh tst-doc-version-policy.sh
+TESTS_ENVIRONMENT = PACKAGE_VERSION="$(PACKAGE_VERSION)" MCPU_LD_PRELOAD= CPU_AS="$(abs_top_builddir)/utils/mcpu-as" CPU_RUN="$(abs_top_builddir)/utils/mcpu-run" CPU_LD="$(abs_top_builddir)/utils/mcpu-ld" CPU_READELF="$(abs_top_builddir)/utils/mcpu-readelf" CPU_AR="$(abs_top_builddir)/utils/mcpu-ar" CPU_NM="$(abs_top_builddir)/utils/mcpu-nm" CPU_OBJDUMP="$(abs_top_builddir)/utils/mcpu-objdump" CPU_STRIP="$(abs_top_builddir)/utils/mcpu-strip" CPU_OBJCOPY="$(abs_top_builddir)/utils/mcpu-objcopy" STRIP_FIXTURE="$(abs_top_builddir)/tests/tst-strip-fixture" TOOL_CONFIG_TEST="$(abs_top_builddir)/tests/tst-tool-config" TEST_SRCDIR="$(abs_top_srcdir)/tests" MCPU_TEST_ROOT="$(abs_top_builddir)/sysroot" MCPU_HLL_FIXTURE_SRCDIR="$(abs_top_srcdir)/tests/fixtures/hll"
+tst_errno_abi_SOURCES = tst-errno-abi.c
+tst_errno_abi_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/sysroot/include
+tst_errno_abi_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+tst_errno_map_SOURCES = tst-errno-map.c
+tst_errno_map_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/cpu
+tst_errno_map_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+tst_context_SOURCES = tst-context.c
+tst_context_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+tst_registers_SOURCES = tst-registers.c
+tst_registers_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+tst_overlap_SOURCES = tst-overlap.c
+tst_overlap_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+tst_instruction_SOURCES = tst-instruction.c
+tst_instruction_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+tst_bytecode_SOURCES = tst-bytecode.c
+tst_bytecode_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+tst_syscall_SOURCES = tst-syscall.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_syscall_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+tst_dup_SOURCES = tst-dup.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_dup_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/sysroot/include
+tst_dup_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+tst_assembler_SOURCES = tst-assembler.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_assembler_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+tst_tool_config_SOURCES = tst-tool-config.c $(top_srcdir)/utils/mcpu-tool-options.c $(top_srcdir)/utils/mcpu-tool-options.h $(top_srcdir)/utils/mcpu-library-search.c $(top_srcdir)/utils/mcpu-library-search.h $(top_srcdir)/utils/mcpu-runtime.c $(top_srcdir)/utils/mcpu-runtime.h $(top_srcdir)/utils/mcpu-config.c $(top_srcdir)/utils/mcpu-config.h
+tst_tool_config_LDADD = $(top_builddir)/utils/libmcpu-version.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+tst_strip_fixture_SOURCES = tst-strip-fixture.c $(top_srcdir)/utils/mcpu-melf.c $(top_srcdir)/utils/mcpu-melf.h
+tst_strip_fixture_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+EXTRA_DIST = \
+ fixtures/radd-pi.s \
+ fixtures/stack.s \
+ fixtures/call.s \
+ fixtures/frame-pointer.s \
+ fixtures/block-copy.s \
+ fixtures/indirect-control.s \
+ fixtures/psw-call-abi.s \
+ fixtures/byte-swap.s \
+ fixtures/mpu-ops.s \
+ fixtures/model-lifecycle.s \
+ fixtures/life-print.s \
+ fixtures/argc-argv.s \
+ fixtures/shared-libs/dynamic-main.s \
+ fixtures/shared-libs/dynamic-liba.s \
+ fixtures/shared-libs/dynamic-libb.s \
+ fixtures/hll/program-main.S \
+ fixtures/hll/model-pendulum-life.S \
+ fixtures/hll/libc-stub.S \
+ tst-runner.sh tst-stack-runner.sh tst-call-runner.sh tst-frame-runner.sh tst-block-copy-runner.sh tst-indirect-runner.sh tst-psw-abi-runner.sh tst-bswap-runner.sh tst-mpu-ops-runner.sh tst-immediate-exactness-runner.sh tst-lifecycle-runner.sh tst-relocatable-runner.sh tst-dynamic-runner.sh tst-line-runner.sh tst-run-memory-options.sh tst-stage-actions-runner.sh tst-readelf-runner.sh tst-tool-config-runner.sh tst-tool-inspection-runner.sh tst-library-search-runner.sh tst-link-needed-runner.sh tst-needed-runtime-runner.sh tst-symbol-scope-runner.sh tst-env-preload-runner.sh tst-abi3-final-runner.sh tst-ar-runner.sh tst-static-archive-link-runner.sh tst-partial-link-runner.sh tst-static-option-runner.sh tst-nm-runner.sh tst-objdump-runner.sh tst-strip-runner.sh tst-objcopy-runner.sh tst-hll-crt-runner.sh tst-verbose-runner.sh tst-doc-version-policy.sh
+
+tst_control_SOURCES = tst-control.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_control_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_stack_SOURCES = tst-stack.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_stack_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_call_SOURCES = tst-call.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_call_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_pointer_SOURCES = tst-pointer.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_pointer_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_pointer_arithmetic_SOURCES = tst-pointer-arithmetic.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_pointer_arithmetic_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_aggregate_SOURCES = tst-aggregate.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_aggregate_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_frame_SOURCES = tst-frame.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_frame_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_block_copy_SOURCES = tst-block-copy.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_block_copy_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_memory_SOURCES = tst-memory.c
+tst_memory_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_indirect_control_SOURCES = tst-indirect-control.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_indirect_control_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_psw_abi_SOURCES = tst-psw-abi.c
+tst_psw_abi_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_psw_write_SOURCES = tst-psw-write.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_psw_write_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_byteorder_SOURCES = tst-byteorder.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_byteorder_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_pointer_policy_SOURCES = tst-pointer-policy.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_pointer_policy_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+tst_bswap_SOURCES = tst-bswap.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_bswap_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_mpu_coverage_SOURCES = tst-mpu-coverage.c
+tst_mpu_coverage_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_immediate_exactness_SOURCES = tst-immediate-exactness.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_immediate_exactness_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_conversion_flags_SOURCES = tst-conversion-flags.c
+tst_conversion_flags_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_asm_directives_SOURCES = tst-asm-directives.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_asm_directives_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_model_segments_SOURCES = tst-model-segments.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_model_segments_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+tst_lifecycle_SOURCES = tst-lifecycle.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h
+tst_lifecycle_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_relocatable_SOURCES = tst-relocatable.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h $(top_srcdir)/utils/mcpu-link.c $(top_srcdir)/utils/mcpu-link.h $(top_srcdir)/utils/mcpu-melf.c $(top_srcdir)/utils/mcpu-melf.h
+tst_relocatable_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_melf_SOURCES = tst-melf.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h $(top_srcdir)/utils/mcpu-object.c $(top_srcdir)/utils/mcpu-object.h $(top_srcdir)/utils/mcpu-link.c $(top_srcdir)/utils/mcpu-link.h $(top_srcdir)/utils/mcpu-exec.c $(top_srcdir)/utils/mcpu-exec.h $(top_srcdir)/utils/mcpu-dyn.c $(top_srcdir)/utils/mcpu-dyn.h $(top_srcdir)/utils/mcpu-melf.c $(top_srcdir)/utils/mcpu-melf.h
+tst_melf_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
+
+
+tst_dynamic_SOURCES = tst-dynamic.c $(top_srcdir)/utils/mcpu-asm.c $(top_srcdir)/utils/mcpu-asm.h $(top_srcdir)/utils/mcpu-link.c $(top_srcdir)/utils/mcpu-link.h $(top_srcdir)/utils/mcpu-dyn.c $(top_srcdir)/utils/mcpu-dyn.h $(top_srcdir)/utils/mcpu-exec.c $(top_srcdir)/utils/mcpu-exec.h $(top_srcdir)/utils/mcpu-melf.c $(top_srcdir)/utils/mcpu-melf.h
+tst_dynamic_LDADD = $(top_builddir)/cpu/libmcpu.la $(LIBMPUIO_LIBS) $(LIBMPU_LIBS)
diff --git a/tests/fixtures/argc-argv.s b/tests/fixtures/argc-argv.s
new file mode 100644
index 0000000..4c1d2ac
--- /dev/null
+++ b/tests/fixtures/argc-argv.s
@@ -0,0 +1,44 @@
+; Minimal argc/argv and explicit "--" separator example.
+;
+; Assemble/link:
+; ../utils/mcpu-as -o argc-argv.o argc-argv.s
+; ../utils/mcpu-ld -o argc-argv --stack-size=4K argc-argv.o
+;
+; Run:
+; ../utils/mcpu-run \
+; --on-step=print-int-regs:r64@0x00000,r8@0x00020,r8@0x00021 \
+; -- \
+; argc-argv --help -x
+;
+; Guest argv is:
+; argv[0] = "argc-argv"
+; argv[1] = "--help"
+; argv[2] = "-x"
+;
+; The example prints argc, argv[1][2] ('h') and argv[2][1] ('x') as integers.
+; Expected output:
+; 3 104 120
+
+.text
+_start_life:
+loop:
+ ; Persistent McpuStartup at SP:
+ ; SP+8 = argc
+ ; SP+16 = argv (PTR64 to argv[0])
+ LDSP r64@0x00000, 8
+ LDSP r64@0x00008, 16
+
+ ; argv[1]
+ PADDI r64@0x00010, r64@0x00008, 8
+ LOADP r64@0x00018, r64@0x00010
+ PADDI r64@0x00018, r64@0x00018, 2
+ LOADP r8@0x00020, r64@0x00018
+
+ ; argv[2]
+ PADDI r64@0x00010, r64@0x00008, 16
+ LOADP r64@0x00018, r64@0x00010
+ PADDI r64@0x00018, r64@0x00018, 1
+ LOADP r8@0x00021, r64@0x00018
+
+ END
+ JMP loop
diff --git a/tests/fixtures/block-copy.s b/tests/fixtures/block-copy.s
new file mode 100644
index 0000000..81422e2
--- /dev/null
+++ b/tests/fixtures/block-copy.s
@@ -0,0 +1,46 @@
+; Build/run from examples/:
+; ../utils/mcpu-as -o block-copy.o block-copy.s
+; ../utils/mcpu-ld --stack-size=512 -o block-copy block-copy.o
+; ../utils/mcpu-run --int-reg=r64@0x00028 block-copy
+;
+; Expected output: 22
+;
+; LibCPU block-copy example.
+;
+; 1. Copy a 32-byte object from DATA to a local STACK object with BMOVI.
+; 2. Use BMOV with a runtime byte count to perform an overlapping move
+; inside that STACK object. BMOV has memmove semantics.
+; 3. Read the moved value back through LOADP. Expected result: 22.
+
+.data
+.align 8
+source_object:
+ .integer 64, 11
+ .integer 64, 22
+ .integer 64, 33
+ .integer 64, 44
+
+.text
+_start_life:
+ ; Source PTR64 -> DATA object.
+ LEA r64@0x00000, source_object
+
+ ; Reserve 32 bytes for a local object and obtain its STACK PTR64.
+ ADJSP -32
+ LEASP r64@0x00008, 0
+
+ ; Fixed-size structure copy: DATA -> STACK.
+ BMOVI r64@0x00008, r64@0x00000, 32
+
+ ; Overlapping move inside the local object:
+ ; bytes [0..15] -> [8..23]. memmove semantics are required.
+ PADDI r64@0x00010, r64@0x00008, 8
+ LDI r64@0x00018, 16
+ BMOV r64@0x00010, r64@0x00008, r64@0x00018
+
+ ; The third 64-bit slot now contains the old second slot: 22.
+ PADDI r64@0x00020, r64@0x00008, 16
+ LOADP r64@0x00028, r64@0x00020
+
+ ADJSP 32
+ HALT
diff --git a/tests/fixtures/byte-swap.s b/tests/fixtures/byte-swap.s
new file mode 100644
index 0000000..7f666d0
--- /dev/null
+++ b/tests/fixtures/byte-swap.s
@@ -0,0 +1,15 @@
+; Byte-swap example.
+;
+; Build/run from examples/:
+; ../utils/mcpu-as -o byte-swap.o byte-swap.s
+; ../utils/mcpu-ld --stack-size=512 -o byte-swap byte-swap.o
+; ../utils/mcpu-run --int-reg=r64@0x00010 byte-swap
+;
+; Expected output: 42
+;
+.text
+_start_life:
+ LDI r64@0x00000, 42
+ BSWAP r64@0x00008, r64@0x00000
+ BSWAP r64@0x00010, r64@0x00008
+ HALT
diff --git a/tests/fixtures/call.s b/tests/fixtures/call.s
new file mode 100644
index 0000000..bfc9965
--- /dev/null
+++ b/tests/fixtures/call.s
@@ -0,0 +1,39 @@
+; Build/run from examples/:
+; ../utils/mcpu-as -o call.o call.s
+; ../utils/mcpu-ld --stack-size=128 -o call call.o
+; ../utils/mcpu-run --int-reg=r64@0x00010 call
+;
+; Expected output: 33
+;
+; LibCPU CALL ABI example.
+;
+; add2(a,b): this two-r64 argument block is exactly 16 bytes, so the
+; traditional right-to-left PUSH sequence also satisfies the formal ABI.
+; CALL then pushes
+; the 64-bit return PC, therefore on callee entry:
+; SP+0 = return address
+; SP+8 = first argument a
+; SP+16 = second argument b
+;
+; Before CALL, SP is 16-byte aligned. The r64 result is returned in the canonical ABI return view
+; r64@0x7fff8. The caller removes its own arguments with ADJSP.
+
+.text
+_start_life:
+
+ LDI r64@0x00000, 11
+ LDI r64@0x00008, 22
+
+ PUSH r64@0x00008 ; second/rightmost argument b
+ PUSH r64@0x00000 ; first/leftmost argument a
+ CALL add2
+ ADJSP 16 ; caller cleanup
+
+ MOV r64@0x00010, r64@0x7fff8
+ HALT
+
+add2:
+ LDSP r64@0x00100, 8 ; a
+ LDSP r64@0x00108, 16 ; b
+ IADD r64@0x7fff8, r64@0x00100, r64@0x00108
+ RET
diff --git a/tests/fixtures/frame-pointer.s b/tests/fixtures/frame-pointer.s
new file mode 100644
index 0000000..6c6934b
--- /dev/null
+++ b/tests/fixtures/frame-pointer.s
@@ -0,0 +1,63 @@
+; Build/run from examples/:
+; ../utils/mcpu-as -o frame-pointer.o frame-pointer.s
+; ../utils/mcpu-ld --stack-size=512 -o frame-pointer frame-pointer.o
+; ../utils/mcpu-run --int-reg=r64@0x00008 frame-pointer
+;
+; Expected output: 42
+;
+; LibCPU optional frame-pointer and dynamic STACK allocation example.
+;
+; demo(5) builds a fixed frame, creates a 24-byte dynamic object below it,
+; accesses a fixed local through FP while SP is moving, and returns 42 in
+; the canonical r64 return register.
+
+.text
+_start_life:
+ LDI r64@0x00000, 5
+ ADJSP -16 ; 16-byte ABI argument block
+ STSP 0, r64@0x00000
+ CALL demo
+ ADJSP 16
+ MOV r64@0x00008, r64@0x7fff8
+ HALT
+
+demo:
+ PUSHFP
+ SETFP
+ ADJSP -16
+
+ ; First explicit argument. With FP in use the layout is:
+ ; FP+0 saved caller FP
+ ; FP+8 return PC
+ ; FP+16 first explicit argument
+ LDFP r64@0x00100, 16
+
+ ; Fixed local at FP-8.
+ STFP -8, r64@0x00100
+ LEAFP r64@0x00108, -8
+
+ ; Save the current stack top as a dynamic-scope mark.
+ LEASP r64@0x00110, 0
+
+ ; Dynamically reserve 24 bytes: new_SP = old_SP - 24.
+ LDI r64@0x00118, 24
+ PSUB r64@0x00120, r64@0x00110, r64@0x00118
+ SETSP r64@0x00120
+
+ ; Put 37 in the dynamic object at current SP.
+ LDI r64@0x00128, 37
+ LEASP r64@0x00130, 0
+ STOREP r64@0x00130, r64@0x00128
+
+ ; FP-relative access is stable although SP moved dynamically.
+ LDFP r64@0x00138, -8
+ LOADP r64@0x00140, r64@0x00130
+ IADD r64@0x7fff8, r64@0x00138, r64@0x00140
+
+ ; Restore the nested dynamic scope explicitly.
+ SETSP r64@0x00110
+
+ ; Destroy the complete frame below FP, restore caller FP and return.
+ RESTSP
+ POPFP
+ RET
diff --git a/tests/fixtures/hll/libc-stub.S b/tests/fixtures/hll/libc-stub.S
new file mode 100644
index 0000000..d4fd878
--- /dev/null
+++ b/tests/fixtures/hll/libc-stub.S
@@ -0,0 +1,42 @@
+; Test/development MCPU Libc emulator.
+;
+; This source is NEVER installed. The register marker is intentionally a
+; testing convention only; it is not part of the CRT <-> MCPU Libc ABI.
+
+.global __mcpu_libc_init
+.global __mcpu_init_array
+.global __mcpu_fini_array
+.global __mcpu_libc_fini
+
+.text
+__mcpu_libc_init:
+ CLR r64@0x70008
+ ICMP r64@0x70000, r64@0x70008
+ JNZ .L_stub_bad
+ LDI r64@0x70000, 1
+ RET
+
+__mcpu_init_array:
+ LDI r64@0x70008, 1
+ ICMP r64@0x70000, r64@0x70008
+ JNZ .L_stub_bad
+ LDI r64@0x70000, 2
+ RET
+
+__mcpu_fini_array:
+ LDI r64@0x70008, 4
+ ICMP r64@0x70000, r64@0x70008
+ JNZ .L_stub_bad
+ LDI r64@0x70000, 5
+ RET
+
+__mcpu_libc_fini:
+ LDI r64@0x70008, 5
+ ICMP r64@0x70000, r64@0x70008
+ JNZ .L_stub_bad
+ LDI r64@0x70000, 6
+ RET
+
+.L_stub_bad:
+ LDI r64@0x70000, 999
+ RET
diff --git a/tests/fixtures/hll/model-pendulum-life.S b/tests/fixtures/hll/model-pendulum-life.S
new file mode 100644
index 0000000..5ad6ddb
--- /dev/null
+++ b/tests/fixtures/hll/model-pendulum-life.S
@@ -0,0 +1,133 @@
+; Assembler emulator of future mcpu-cc MODEL output.
+;
+; The lifecycle shape is a compact pendulum-style MODEL regression fixture.
+; INIT snapshots CONTROL
+; into DATA, LIFE advances one model step and publishes one TRACE frame, and
+; fini() marks graceful model finalization.
+;
+; This reduced fixture tests the HLL MODEL/CRT protocol, not RK4 numerics.
+
+.global init
+.global life
+.global fini
+
+.control
+.align 8
+control_n:
+ .integer 64, 3
+control_error_step:
+ .integer 64, 0
+
+.data
+.align 8
+model_n:
+ .integer 64, 0
+model_error_step:
+ .integer 64, 0
+model_step:
+ .integer 64, 0
+model_fini_marker:
+ .integer 64, 0
+
+.trace
+.align 8
+trace_step:
+ .integer 64, 0
+
+.text
+; int32 init(int64 argc, PTR64 argv)
+init:
+ LDI r64@0x70200, 2
+ ICMP r64@0x70000, r64@0x70200
+ JNZ .L_init_bad_order
+
+ ; The test invokes the MODEL without extra arguments.
+ LDSP r64@0x70208, 8
+ LDI r64@0x70210, 1
+ ICMP r64@0x70208, r64@0x70210
+ JNZ .L_init_bad_args
+
+ ; Negative CONTROL.n deliberately requests an init failure.
+ LEA r64@0x70218, control_n
+ LOADP r64@0x70220, r64@0x70218
+ CLR r64@0x70228
+ ICMP r64@0x70220, r64@0x70228
+ JS .L_init_fail
+
+ STORE model_n, r64@0x70220
+ LEA r64@0x70218, control_error_step
+ LOADP r64@0x70230, r64@0x70218
+ STORE model_error_step, r64@0x70230
+
+ CLR r64@0x70238
+ STORE model_step, r64@0x70238
+ STORE model_fini_marker, r64@0x70238
+
+ LDI r64@0x70000, 3
+ CLR r32@0x7fffc
+ RET
+
+.L_init_fail:
+ LDI r64@0x70000, 3
+ LDI r32@0x7fffc, 17
+ RET
+
+.L_init_bad_order:
+ LDI r64@0x70000, 910
+ LDI r32@0x7fffc, 18
+ RET
+
+.L_init_bad_args:
+ LDI r64@0x70000, 911
+ LDI r32@0x7fffc, 19
+ RET
+
+; int32 life(void)
+life:
+ LOAD r64@0x70300, model_step
+ LDI r64@0x70308, 1
+ IADD r64@0x70300, r64@0x70300, r64@0x70308
+ STORE model_step, r64@0x70300
+
+ ; Optional positive LIFE error requested by CONTROL.error_step.
+ LOAD r64@0x70310, model_error_step
+ CLR r64@0x70318
+ ICMP r64@0x70310, r64@0x70318
+ JZ .L_no_life_error
+ ICMP r64@0x70300, r64@0x70310
+ JNZ .L_no_life_error
+ LDI r32@0x7fffc, 9
+ RET
+
+.L_no_life_error:
+ ; Pendulum-style sequential stop: step == n + 1 means natural end.
+ LOAD r64@0x70320, model_n
+ IADD r64@0x70328, r64@0x70320, r64@0x70308
+ ICMP r64@0x70300, r64@0x70328
+ JZ .L_life_end
+
+ LEA r64@0x70330, trace_step
+ STOREP r64@0x70330, r64@0x70300
+
+ ; LIFE_CONTINUE == INT32_MIN.
+ LDI r32@0x7fffc, -2147483648
+ RET
+
+.L_life_end:
+ CLR r32@0x7fffc
+ RET
+
+fini:
+ LDI r64@0x70200, 3
+ ICMP r64@0x70000, r64@0x70200
+ JNZ .L_model_fini_bad
+
+ LDI r64@0x70000, 4
+ LDI r64@0x70208, 1
+ STORE model_fini_marker, r64@0x70208
+ MOV r64@0x70010, r64@0x70208
+ RET
+
+.L_model_fini_bad:
+ LDI r64@0x70000, 912
+ RET
diff --git a/tests/fixtures/hll/program-main.S b/tests/fixtures/hll/program-main.S
new file mode 100644
index 0000000..3161343
--- /dev/null
+++ b/tests/fixtures/hll/program-main.S
@@ -0,0 +1,48 @@
+; Assembler emulator of future mcpu-cc PROGRAM output.
+;
+; It intentionally defines only main() and fini(); lifecycle symbols belong
+; to CRT and must be inserted automatically by mcpu-ld.
+
+.global main
+.global fini
+
+.text
+main:
+ ; Startup hooks must already have completed.
+ LDI r64@0x70100, 2
+ ICMP r64@0x70000, r64@0x70100
+ JNZ .L_main_bad
+
+ ; argc must include argv[0] plus the two test arguments.
+ LDSP r64@0x70108, 8
+ LDI r64@0x70110, 3
+ ICMP r64@0x70108, r64@0x70110
+ JNZ .L_main_bad
+
+ ; argv[argc] is the required NULL terminator.
+ LDSP r64@0x70118, 16
+ PADDI r64@0x70120, r64@0x70118, 24
+ LOADP r64@0x70128, r64@0x70120
+ CLR r64@0x70130
+ ICMP r64@0x70128, r64@0x70130
+ JNZ .L_main_bad
+
+ LDI r64@0x70000, 3
+ LDI r32@0x7fffc, 42
+ RET
+
+.L_main_bad:
+ LDI r64@0x70000, 900
+ LDI r32@0x7fffc, 43
+ RET
+
+fini:
+ LDI r64@0x70100, 3
+ ICMP r64@0x70000, r64@0x70100
+ JNZ .L_fini_bad
+ LDI r64@0x70000, 4
+ RET
+
+.L_fini_bad:
+ LDI r64@0x70000, 901
+ RET
diff --git a/tests/fixtures/indirect-control.s b/tests/fixtures/indirect-control.s
new file mode 100644
index 0000000..1e9d0e3
--- /dev/null
+++ b/tests/fixtures/indirect-control.s
@@ -0,0 +1,39 @@
+; Indirect CALLP/JMPP control-flow example.
+;
+; Build/run from examples/:
+; ../utils/mcpu-as -o indirect-control.o indirect-control.s
+; ../utils/mcpu-ld --stack-size=512 -o indirect-control indirect-control.o
+; ../utils/mcpu-run --int-reg=r64@0x00020 indirect-control
+;
+; Expected output: 42
+;
+.data
+
+.align 8
+dispatch:
+ .cptr indirect_function
+ .cptr done
+
+.text
+_start_life:
+
+ ; Load a PROGRAM PTR64 from DATA and call through it.
+ LEA r64@0x00000, dispatch
+ LOADP r64@0x00008, r64@0x00000
+ CALLP r64@0x00008
+
+ ; Select the second entry and jump through it.
+ PADDI r64@0x00000, r64@0x00000, 8
+ LOADP r64@0x00010, r64@0x00000
+ JMPP r64@0x00010
+
+ ; JMPP must skip this path.
+ LDI r64@0x00020, 99
+ HALT
+
+indirect_function:
+ LDI r64@0x00020, 42
+ RET
+
+done:
+ HALT
diff --git a/tests/fixtures/life-print.s b/tests/fixtures/life-print.s
new file mode 100644
index 0000000..a1de41f
--- /dev/null
+++ b/tests/fixtures/life-print.s
@@ -0,0 +1,30 @@
+; Minimal LIFE-stage action example.
+;
+; Assemble/link:
+; ../utils/mcpu-as -o life-print.o life-print.s
+; ../utils/mcpu-ld -o life-print --stack-size=1K life-print.o
+;
+; Run three LIFE cycles and print two registers on one line after each END:
+; ../utils/mcpu-run \
+; --steps=3 \
+; --on-step=print-int-regs:r64@0x00000,r64@0x00008 \
+; -- \
+; life-print
+;
+; Expected output:
+; 1 10
+; 2 20
+; 3 30
+
+.text
+_start_life:
+ LDI r64@0x00000, 1
+ LDI r64@0x00008, 10
+ LDI r64@0x00010, 1
+ LDI r64@0x00018, 10
+
+loop:
+ END
+ IADD r64@0x00000, r64@0x00000, r64@0x00010
+ IADD r64@0x00008, r64@0x00008, r64@0x00018
+ JMP loop
diff --git a/tests/fixtures/model-lifecycle.s b/tests/fixtures/model-lifecycle.s
new file mode 100644
index 0000000..4c190d5
--- /dev/null
+++ b/tests/fixtures/model-lifecycle.s
@@ -0,0 +1,53 @@
+; Build from examples/:
+; ../utils/mcpu-as -o model-lifecycle.o model-lifecycle.s
+; ../utils/mcpu-ld --stack-size=512 -o model-lifecycle model-lifecycle.o
+;
+; Run one INIT-only pass or one LIFE step:
+; ../utils/mcpu-run --init-only --int-reg=r64@0x00000 model-lifecycle
+; ../utils/mcpu-run --steps=1 --int-reg=r64@0x00000 model-lifecycle
+;
+; Expected outputs: 40 and 42 respectively.
+;
+; LibCPU lifecycle/model-segment example.
+; cpu-run --steps=1 --int-reg=r64@0x00000 model-lifecycle -> 42
+; cpu-run --init-only --int-reg=r64@0x00000 model-lifecycle -> 40
+
+.control
+model_step:
+ .real 64, 1.0
+
+.input
+external_delta:
+ .integer 64, 2
+
+.output
+current_state:
+ .integer 64, 0
+
+.trace
+telemetry_state:
+ .integer 64, 0
+
+.init
+_start:
+ LDI r64@0x00000, 40
+ END
+
+.text
+_life:
+loop:
+ LEA r64@0x00008, external_delta
+ LOADP r64@0x00010, r64@0x00008
+ IADD r64@0x00000, r64@0x00000, r64@0x00010
+
+ LEA r64@0x00018, current_state
+ STOREP r64@0x00018, r64@0x00000
+ LEA r64@0x00020, telemetry_state
+ STOREP r64@0x00020, r64@0x00000
+
+ END
+ JMP loop
+
+.fini
+_fini:
+ END
diff --git a/tests/fixtures/mpu-ops.s b/tests/fixtures/mpu-ops.s
new file mode 100644
index 0000000..49d4f97
--- /dev/null
+++ b/tests/fixtures/mpu-ops.s
@@ -0,0 +1,25 @@
+; LibMPU-backed arithmetic/conversion operation example.
+;
+; Build/run from examples/:
+; ../utils/mcpu-as -o mpu-ops.o mpu-ops.s
+; ../utils/mcpu-ld --stack-size=512 -o mpu-ops mpu-ops.o
+; ../utils/mcpu-run --int-reg=r64@0x00020 mpu-ops
+;
+; Expected output: 42
+;
+.text
+_start_life:
+
+ LDI r64@0x00000, 6
+ LDI r64@0x00008, 7
+ IMUL r128@0x00010, r64@0x00000, r64@0x00008
+ ICPY r64@0x00020, r128@0x00010
+
+ RCONST r64@0x00028, ONE
+ CRTOC c128@0x00030, r64@0x00028
+ CISEQ r64@0x00040, c128@0x00030, c128@0x00030
+ IINC r64@0x00048, r64@0x00040
+ IDEC r64@0x00050, r64@0x00048
+ ISUB r64@0x00020, r64@0x00020, r64@0x00050
+ IADD r64@0x00020, r64@0x00020, r64@0x00040
+ HALT
diff --git a/tests/fixtures/psw-call-abi.s b/tests/fixtures/psw-call-abi.s
new file mode 100644
index 0000000..3dccab3
--- /dev/null
+++ b/tests/fixtures/psw-call-abi.s
@@ -0,0 +1,36 @@
+; PSW volatility across the CALL ABI example.
+;
+; Build/run from examples/:
+; ../utils/mcpu-as -o psw-call-abi.o psw-call-abi.s
+; ../utils/mcpu-ld --stack-size=512 -o psw-call-abi psw-call-abi.o
+; ../utils/mcpu-run --int-reg=r64@0x00010 psw-call-abi
+;
+; Expected output: 42
+;
+.text
+_start_life:
+
+ ; Establish ZF=1 before the call: -1 + 1 -> 0 in signed r8.
+ LDI r8@0x00000, -1
+ LDI r8@0x00001, 1
+ IADD r8@0x00002, r8@0x00000, r8@0x00001
+
+ ; CALL itself preserves PSW, but the callee is free to clobber
+ ; PSW[15:0]. This function deliberately clears ZF with 1 + 1.
+ CALL clobber_status
+
+ ; The old ZF is not part of the calling convention. Observe the
+ ; callee-produced ZF=0 and report 42.
+ JNZ status_is_volatile
+ LDI r64@0x00010, 99
+ HALT
+
+clobber_status:
+ LDI r8@0x00003, 1
+ LDI r8@0x00004, 1
+ IADD r8@0x00005, r8@0x00003, r8@0x00004
+ RET
+
+status_is_volatile:
+ LDI r64@0x00010, 42
+ HALT
diff --git a/tests/fixtures/radd-pi.s b/tests/fixtures/radd-pi.s
new file mode 100644
index 0000000..2921d91
--- /dev/null
+++ b/tests/fixtures/radd-pi.s
@@ -0,0 +1,24 @@
+; High-precision real arithmetic example.
+;
+; Build/run from examples/:
+; ../utils/mcpu-as -o radd-pi.o radd-pi.s
+; ../utils/mcpu-ld --stack-size=512 -o radd-pi radd-pi.o
+; ../utils/mcpu-run --digits=60 --real-reg=r4096@0x00400 radd-pi
+;
+; Expected output starts with:
+; 5.14159265358979323846264338327950288419716939937510582097
+;
+.data
+
+.align 16
+result:
+ .real 4096, 0.0
+
+.text
+_start_life:
+
+ LDR r4096@0x00000, 2.0
+ RCONST r4096@0x00200, PI
+ RADD r4096@0x00400, r4096@0x00000, r4096@0x00200
+ STORE result, r4096@0x00400
+ HALT
diff --git a/tests/fixtures/shared-libs/dynamic-liba.s b/tests/fixtures/shared-libs/dynamic-liba.s
new file mode 100644
index 0000000..f35cfe2
--- /dev/null
+++ b/tests/fixtures/shared-libs/dynamic-liba.s
@@ -0,0 +1,18 @@
+; Build/run from examples/shared-libs/:
+; make
+; make run
+;
+; make run prints, in order:
+; 43
+; 43
+; 42
+; 42
+; 41
+;
+; Shared-library implementation of foo: adds 2.
+.global foo
+.text
+foo:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
diff --git a/tests/fixtures/shared-libs/dynamic-libb.s b/tests/fixtures/shared-libs/dynamic-libb.s
new file mode 100644
index 0000000..348b358
--- /dev/null
+++ b/tests/fixtures/shared-libs/dynamic-libb.s
@@ -0,0 +1,18 @@
+; Build/run from examples/shared-libs/:
+; make
+; make run
+;
+; make run prints, in order:
+; 43
+; 43
+; 42
+; 42
+; 41
+;
+; Shared-library implementation of foo: adds 3.
+.global foo
+.text
+foo:
+ LDI r64@0x00008, 3
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
diff --git a/tests/fixtures/shared-libs/dynamic-main.s b/tests/fixtures/shared-libs/dynamic-main.s
new file mode 100644
index 0000000..f296429
--- /dev/null
+++ b/tests/fixtures/shared-libs/dynamic-main.s
@@ -0,0 +1,35 @@
+; Build/run from examples/shared-libs/:
+; make
+; make run
+;
+; make run prints, in order:
+; 43
+; 43
+; 42
+; 42
+; 41
+;
+; ABI-v2 startup-dynamic executable example.
+; main-program itself defines foo(), but cpu-ld --dynamic retains CALL foo as
+; a preemptible runtime relocation. cpu-run --ldpreload can therefore replace
+; it before _start executes.
+;
+; Without preload: foo adds 1 -> result 41.
+; With dynamic-liba.so: foo adds 2 -> result 42.
+; With dynamic-liba.so:dynamic-libb.so: rightmost strong GLOBAL wins, so
+; dynamic-libb.so foo adds 3 -> result 43.
+.global _life
+.global foo
+
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL foo
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+
+foo:
+ LDI r64@0x00008, 1
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
diff --git a/tests/fixtures/stack.s b/tests/fixtures/stack.s
new file mode 100644
index 0000000..9b45785
--- /dev/null
+++ b/tests/fixtures/stack.s
@@ -0,0 +1,32 @@
+; STACK instruction example.
+;
+; Build/run from examples/:
+; ../utils/mcpu-as -o stack.o stack.s
+; ../utils/mcpu-ld --stack-size=128 -o stack stack.o
+; ../utils/mcpu-run --int-reg=r64@0x00028 stack
+;
+; Expected output: 22
+;
+.text
+_start_life:
+ LDI r64@0x00000, 11
+ LDI r64@0x00008, 22
+
+ ; STACK grows downward. cpu-run initializes SP to stack_size.
+ PUSH r64@0x00000
+ PUSH r64@0x00008
+
+ ; Current stack top contains 22, SP+8 contains 11.
+ LDSP r64@0x00010, 8
+ STSP 0, r64@0x00000
+
+ POP r64@0x00018
+ POP r64@0x00020
+
+ ; Reserve a 16-byte local frame without automatic padding.
+ ADJSP -16
+ STSP 0, r64@0x00008
+ LDSP r64@0x00028, 0
+ ADJSP 16
+
+ HALT
diff --git a/tests/tst-abi3-final-runner.sh b/tests/tst-abi3-final-runner.sh
new file mode 100755
index 0000000..7744775
--- /dev/null
+++ b/tests/tst-abi3-final-runner.sh
@@ -0,0 +1,78 @@
+#!/bin/sh
+set -eu
+
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${CPU_READELF:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-abi3-final-$$
+mkdir -p "$TMP/lib"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+
+cat > "$TMP/main.s" <<'EOS'
+.global _life
+.extern foo
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL foo
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+
+cat > "$TMP/a.s" <<'EOS'
+.global foo
+.text
+foo:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+cat > "$TMP/override.s" <<'EOS'
+.global foo
+.text
+foo:
+ LDI r64@0x00008, 3
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/main.o" "$TMP/main.s"
+"$CPU_AS" -o "$TMP/a.o" "$TMP/a.s"
+"$CPU_AS" -o "$TMP/override.o" "$TMP/override.s"
+
+# Conventional development link -> SONAME -> physical file layout.
+"$CPU_LD" --shared --soname=liba.so.1 -o "$TMP/lib/liba.so.1.2.3" "$TMP/a.o"
+ln -s liba.so.1.2.3 "$TMP/lib/liba.so.1"
+ln -s liba.so.1 "$TMP/lib/liba.so"
+"$CPU_LD" --shared --soname=liboverride.so.1 -o "$TMP/lib/liboverride.so.1" "$TMP/override.o"
+ln -s liboverride.so.1 "$TMP/lib/liboverride.so"
+
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap -L"$TMP/lib" -la \
+ -o "$TMP/main" "$TMP/main.o"
+
+"$CPU_READELF" -h -d "$TMP/lib/liba.so.1.2.3" > "$TMP/lib.txt"
+grep -q 'ABI Version: 3' "$TMP/lib.txt"
+grep -q 'DYN (Shared object file)' "$TMP/lib.txt"
+grep -q 'SONAME' "$TMP/lib.txt"
+grep -q 'Library soname: \[liba.so.1\]' "$TMP/lib.txt"
+
+"$CPU_READELF" -h -d "$TMP/main" > "$TMP/main.txt"
+grep -q 'ABI Version: 3' "$TMP/main.txt"
+grep -q 'EXEC (Executable file)' "$TMP/main.txt"
+grep -q 'NEEDED' "$TMP/main.txt"
+grep -q 'Shared library: \[liba.so.1\]' "$TMP/main.txt"
+if grep -q 'liba.so.1.2.3' "$TMP/main.txt"; then
+ echo "physical shared-library filename leaked into DT_NEEDED" >&2
+ exit 1
+fi
+
+# Frozen normal dependency path: automatic SONAME lookup and execution.
+[ "`MCPU_LD_PRELOAD= "$CPU_RUN" --library-path="$TMP/lib" \
+ --int-reg=r64@0x00018 "$TMP/main"`" = 42 ]
+
+# Frozen explicit-preload interposition: explicit preload outranks normal graph.
+[ "`MCPU_LD_PRELOAD=liboverride.so "$CPU_RUN" --library-path="$TMP/lib" \
+ --int-reg=r64@0x00018 "$TMP/main"`" = 43 ]
+
+exit 0
diff --git a/tests/tst-aggregate.c b/tests/tst-aggregate.c
new file mode 100644
index 0000000..2311d0f
--- /dev/null
+++ b/tests/tst-aggregate.c
@@ -0,0 +1,68 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+
+static const char src[]=
+".text\n"
+" ADJSP -16\n"
+" LEASP r64@0x00000, 0\n"
+" LDI r64@0x00100, 11\n"
+" LDI r64@0x00108, 22\n"
+" ADJSP -32\n"
+" STSP 0, r64@0x00000\n"
+" STSP 8, r128@0x00100\n"
+" CALL copy_pair\n"
+" ADJSP 32\n"
+" LDSP r128@0x00200, 0\n"
+" IADD r64@0x00210, r64@0x00200, r64@0x00208\n"
+" LEASP r64@0x00008, 0\n"
+" ADJSP -16\n"
+" STSP 0, r64@0x00008\n"
+" CALL first_field\n"
+" ADJSP 16\n"
+" ADJSP 16\n"
+" HALT\n"
+"copy_pair:\n"
+" LDSP r64@0x00300, 8\n"
+" LDSP r128@0x00310, 16\n"
+" STOREP r64@0x00300, r128@0x00310\n"
+" RET\n"
+"first_field:\n"
+" LDSP r64@0x00400, 8\n"
+" LOADP r64@0x7fff8, r64@0x00400\n"
+" RET\n";
+
+static int
+eq64( unsigned address, const char *text )
+{
+ mpu_int64_t x;
+ iatoi( ( mpu_int * ) x, (__mpu_char8_t *)text, NB_I64 );
+ return( !memcmp( __cpu_reg_ptr( 64, address ), x, NB_I64 ) );
+}
+
+int
+main( void )
+{
+ cpu_asm_image im;
+ char err[256];
+
+ __cpu_init();
+ CHECK( cpu_assemble_text( src, &im, err, sizeof( err ) ) == 0, err );
+ CHECK( __cpu_program_set( im.code, im.code_size ) == 0, "program set" );
+ CHECK( __cpu_stack_alloc( 512 ) == 0, "stack alloc" );
+ CHECK( __cpu_run() == CPU_EXEC_SUCCESS, "aggregate ABI program run" );
+
+ CHECK( __cpu_sp == 512, "caller released local object and all arguments" );
+ CHECK( eq64( 0x00200, "11" ), "aggregate return first field" );
+ CHECK( eq64( 0x00208, "22" ), "aggregate return second field" );
+ CHECK( eq64( 0x00210, "33" ), "aggregate return object preserved" );
+ CHECK( eq64( 0x7fff8, "11" ), "pointer argument and scalar return" );
+
+ cpu_asm_image_free( &im );
+ __cpu_free_context();
+ puts( "libcpu aggregate CALL ABI regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-ar-runner.sh b/tests/tst-ar-runner.sh
new file mode 100755
index 0000000..ffc939a
--- /dev/null
+++ b/tests/tst-ar-runner.sh
@@ -0,0 +1,122 @@
+#!/bin/sh
+set -eu
+
+: "${PACKAGE_VERSION:?}"
+
+: "${CPU_AS:?}" "${CPU_AR:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-ar-$$
+mkdir -p "$TMP"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+
+cat > "$TMP/a.s" <<'EOS'
+.global foo
+.text
+foo:
+ RET
+EOS
+
+cat > "$TMP/very-long-member-name-for-mcpu.s" <<'EOS'
+.global bar
+.text
+bar:
+ RET
+EOS
+
+cat > "$TMP/c.s" <<'EOS'
+.global baz
+.text
+baz:
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/a.o" "$TMP/a.s"
+"$CPU_AS" -o "$TMP/very-long-member-name-for-mcpu.o" \
+ "$TMP/very-long-member-name-for-mcpu.s"
+"$CPU_AS" -o "$TMP/c.o" "$TMP/c.s"
+
+# rcs creates a deterministic UNIX archive, preserves command-line member
+# order, supports a long member name, and writes a real ar symbol index.
+"$CPU_AR" rcs "$TMP/libx.a" "$TMP/a.o" \
+ "$TMP/very-long-member-name-for-mcpu.o"
+"$CPU_AR" t "$TMP/libx.a" > "$TMP/list"
+cat > "$TMP/list.expected" <<'EOS'
+a.o
+very-long-member-name-for-mcpu.o
+EOS
+cmp "$TMP/list.expected" "$TMP/list"
+
+first_member=`dd if="$TMP/libx.a" bs=1 skip=8 count=1 2>/dev/null`
+[ "$first_member" = / ]
+
+# p returns the exact member bytes.
+"$CPU_AR" p "$TMP/libx.a" a.o > "$TMP/printed.o"
+cmp "$TMP/a.o" "$TMP/printed.o"
+
+# x restores exact member bytes, including BSD extended-name members.
+mkdir "$TMP/extract"
+(
+ cd "$TMP/extract"
+ "$CPU_AR" x ../libx.a
+)
+cmp "$TMP/a.o" "$TMP/extract/a.o"
+cmp "$TMP/very-long-member-name-for-mcpu.o" \
+ "$TMP/extract/very-long-member-name-for-mcpu.o"
+
+# Archive output is deterministic across independent creations.
+"$CPU_AR" rcs "$TMP/libx2.a" "$TMP/a.o" \
+ "$TMP/very-long-member-name-for-mcpu.o"
+cmp "$TMP/libx.a" "$TMP/libx2.a"
+
+# q always appends. Existing symbol-index state remains present.
+"$CPU_AR" q "$TMP/libx.a" "$TMP/c.o"
+"$CPU_AR" t "$TMP/libx.a" > "$TMP/list"
+cat > "$TMP/list.expected" <<'EOS'
+a.o
+very-long-member-name-for-mcpu.o
+c.o
+EOS
+cmp "$TMP/list.expected" "$TMP/list"
+first_member=`dd if="$TMP/libx.a" bs=1 skip=8 count=1 2>/dev/null`
+[ "$first_member" = / ]
+
+# r replaces a member in place rather than moving it to the end.
+cat > "$TMP/c.s" <<'EOS'
+.global qux
+.text
+qux:
+ RET
+EOS
+"$CPU_AS" -o "$TMP/c.o" "$TMP/c.s"
+"$CPU_AR" r "$TMP/libx.a" "$TMP/c.o"
+"$CPU_AR" t "$TMP/libx.a" > "$TMP/list"
+cmp "$TMP/list.expected" "$TMP/list"
+"$CPU_AR" p "$TMP/libx.a" c.o > "$TMP/printed-c.o"
+cmp "$TMP/c.o" "$TMP/printed-c.o"
+
+# d removes named members and s rebuilds the symbol index explicitly.
+"$CPU_AR" d "$TMP/libx.a" a.o
+"$CPU_AR" s "$TMP/libx.a"
+"$CPU_AR" t "$TMP/libx.a" > "$TMP/list"
+cat > "$TMP/list.expected" <<'EOS'
+very-long-member-name-for-mcpu.o
+c.o
+EOS
+cmp "$TMP/list.expected" "$TMP/list"
+first_member=`dd if="$TMP/libx.a" bs=1 skip=8 count=1 2>/dev/null`
+[ "$first_member" = / ]
+
+# A static MCPU archive may contain only valid current MELF ET_REL objects.
+printf 'not MELF\n' > "$TMP/bad.o"
+if "$CPU_AR" r "$TMP/bad.a" "$TMP/bad.o" >/dev/null 2>"$TMP/bad.err"; then
+ echo "mcpu-ar unexpectedly accepted a non-MELF member" >&2
+ exit 1
+fi
+grep -q 'not a MELF file' "$TMP/bad.err"
+test ! -e "$TMP/bad.a"
+
+"$CPU_AR" --version | grep -Fq "$PACKAGE_VERSION"
+"$CPU_AR" --help | grep -q 'rcs'
+"$CPU_AR" --help | grep -q 'symbol index'
+
+exit 0
diff --git a/tests/tst-asm-directives.c b/tests/tst-asm-directives.c
new file mode 100644
index 0000000..e86f46e
--- /dev/null
+++ b/tests/tst-asm-directives.c
@@ -0,0 +1,84 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+
+#define CHECK(c,m) do { if(!(c)) { fprintf(stderr,"FAIL: %s\n",m); return 1; } } while(0)
+
+static const char src[] =
+".equ REG0, REGBASE+0\n"
+".equ REGBASE, 0x03000\n"
+".equ STEP, 4*512\n"
+".data\n"
+"ucs:\n"
+" .ucs2 \"Андрей\"\n"
+"utf:\n"
+" .utf8 \"Андрей\"\n"
+"raw:\n"
+" .byte 1, 2+3, STEP/512, 0xff\n"
+"ints:\n"
+" .integer 16, 0x0410, 0x043d, 0\n"
+"ints8:\n"
+" .int 8, 65, 66, 0\n"
+" .align 4*4\n"
+"vals:\n"
+" .integer 64, 11, 42\n"
+".text\n"
+"start:\n"
+" LDI r64@REG0, 40+2\n"
+" LOAD r64@(REG0+8), vals+8\n"
+" HALT\n";
+
+int
+main( void )
+{
+ cpu_asm_image im;
+ char err[256];
+ int rc, i;
+ static const __mpu_uint16_t ucs_expect[] =
+ {
+ 0x0410, 0x043d, 0x0434, 0x0440, 0x0435, 0x0439, 0
+ };
+ static const unsigned char utf_expect[] =
+ {
+ 0xd0, 0x90, 0xd0, 0xbd, 0xd0, 0xb4, 0xd1, 0x80, 0xd0, 0xb5, 0xd0, 0xb9, 0
+ };
+ static const unsigned char raw_expect[] =
+ {
+ 1, 5, 4, 0xff
+ };
+ static const char bad_ucs[] = ".data\n.ucs2 \"\\U0001F600\"\n.text\nHALT\n";
+ static const char bad_byte[] = ".data\n.byte 256\n.text\nHALT\n";
+ static const char bad_cycle[] = ".equ A, B+1\n.equ B, A+1\n.text\nHALT\n";
+
+ __cpu_init();
+ CHECK( cpu_assemble_text( src, &im, err, sizeof( err ) ) == 0, err );
+ CHECK( im.data_size == 80, "DATA size including NULL guard, strings, raw bytes, integer lists and alignment" );
+ for( i = 0; i < 7; i++ ) CHECK( __cpu_data_get_u16( im.data + 16 + i * 2 ) == ucs_expect[i], ".ucs2 code unit" );
+ CHECK( !memcmp( im.data + 30, utf_expect, sizeof( utf_expect ) ), ".utf8 exact UTF-8 bytes plus NUL" );
+ CHECK( !memcmp( im.data + 43, raw_expect, sizeof( raw_expect ) ), ".byte expressions" );
+ CHECK( __cpu_data_get_u16( im.data + 47 ) == 0x0410, ".integer first list element" );
+ CHECK( __cpu_data_get_u16( im.data + 49 ) == 0x043d, ".integer second list element" );
+ CHECK( __cpu_data_get_u16( im.data + 51 ) == 0, ".integer terminator" );
+ CHECK( im.data[53] == 65 && im.data[54] == 66 && im.data[55] == 0, ".int alias and list" );
+ for( i = 56; i < 64; i++ ) CHECK( im.data[i] == 0, ".align expression padding" );
+ CHECK( __cpu_data_get_u64( im.data + 64 ) == 11, "first 64-bit integer list element" );
+ CHECK( __cpu_data_get_u64( im.data + 72 ) == 42, "second 64-bit integer list element" );
+
+ CHECK( __cpu_program_set( im.code, im.code_size ) == 0, "program set" );
+ CHECK( __cpu_data_set( im.data, im.data_size ) == 0, "data set" );
+ rc = __cpu_run();
+ CHECK( rc == 0, "assembled expression program run" );
+ CHECK( __cpu_data_get_u64( __cpu_reg_ptr( 64, 0x03000 ) ) == 42, "symbolic register address via forward .equ" );
+ CHECK( __cpu_data_get_u64( __cpu_reg_ptr( 64, 0x03008 ) ) == 42, "DATA label+offset expression" );
+ cpu_asm_image_free( &im );
+
+ CHECK( cpu_assemble_text( bad_ucs, &im, err, sizeof( err ) ) != 0, "strict UCS-2 rejects non-BMP character" );
+ CHECK( strstr( err, "strict UCS-2" ) != 0, "strict UCS-2 diagnostic" );
+ CHECK( cpu_assemble_text( bad_byte, &im, err, sizeof( err ) ) != 0, ".byte rejects >8-bit value" );
+ CHECK( cpu_assemble_text( bad_cycle, &im, err, sizeof( err ) ) != 0, "cyclic .equ rejected" );
+
+ __cpu_free_context();
+ puts( "libcpu assembler directives/expressions regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-assembler.c b/tests/tst-assembler.c
new file mode 100644
index 0000000..9b31edc
--- /dev/null
+++ b/tests/tst-assembler.c
@@ -0,0 +1,60 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+static const char src[]=
+".data\n"
+"big:\n"
+" .integer 128, 123456789012345678901234567890\n"
+"rv:\n"
+" .real 128, 1.25\n"
+"cv:\n"
+" .complex 128, 1.5, -2.5\n"
+".text\n"
+" LDI r128@0x00000, -17\n"
+" LDR r128@0x00010, -17.0\n"
+" LDC c128@0x00020, -17.0r0+32.0j0\n"
+" RCONST r128@0x00030, PI\n"
+" IADD r128@0x00040, r128@0x00000, r128@0x00000\n"
+" RADD r128@0x00050, r128@0x00010, r128@0x00010\n"
+" CADD c128@0x00060, c128@0x00020, c128@0x00020\n"
+" LOAD r128@0x00070, big\n"
+" HALT\n";
+int
+main( void )
+{
+ cpu_asm_image im;
+ char err[256];
+ mpu_int128_t ie;
+ mpu_real128_t re, rr;
+ mpu_wide_real_t cr, ci, er, ei;
+ int rc;
+ __cpu_init();
+ CHECK( cpu_assemble_text( src, &im, err, sizeof( err ) ) == 0, err );
+ CHECK( im.code_size > 0, "code produced" );
+ CHECK( im.data_size == 64, "DATA NULL guard plus directives size" );
+ CHECK( !memcmp( im.data, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", CPU_DATA_NULL_GUARD_SIZE ), "DATA NULL guard bytes" );
+ CHECK( __cpu_program_set( im.code, im.code_size ) == 0, "program set" );
+ CHECK( __cpu_data_set( im.data, im.data_size ) == 0, "data set" );
+ rc = __cpu_run();
+ CHECK( rc == 0, "assembled program run" );
+ iatoi( ( mpu_int * ) ie, "-17", NB_I128 );
+ CHECK( !memcmp( __cpu_reg_ptr( 128, 0 ), ie, NB_I128 ), "LDI" );
+ ascii_to_real( ( mpu_real * ) re, "-17.0", NB_R128 );
+ CHECK( r_cmp( ( mpu_real * ) __cpu_reg_ptr( 128, 16 ), ( mpu_real * ) re, NB_R128 ) == 0, "LDR" );
+ _m_PI( ( mpu_real * ) rr, NB_R128 );
+ CHECK( r_cmp( ( mpu_real * ) __cpu_reg_ptr( 128, 48 ), ( mpu_real * ) rr, NB_R128 ) == 0, "RCONST PI" );
+ c_real_part( ( mpu_real * ) cr, ( mpu_complex * ) __cpu_reg_ptr( 128, 32 ), NB_R64 );
+ c_imaginary( ( mpu_real * ) ci, ( mpu_complex * ) __cpu_reg_ptr( 128, 32 ), NB_R64 );
+ ascii_to_real( ( mpu_real * ) er, "-17.0", NB_R64 );
+ ascii_to_real( ( mpu_real * ) ei, "32.0", NB_R64 );
+ CHECK( r_cmp( ( mpu_real * ) cr, ( mpu_real * ) er, NB_R64 ) == 0, "LDC real component" );
+ CHECK( r_cmp( ( mpu_real * ) ci, ( mpu_real * ) ei, NB_R64 ) == 0, "LDC imag component" );
+ iatoi( ( mpu_int * ) ie, "123456789012345678901234567890", NB_I128 );
+ CHECK( !memcmp( __cpu_reg_ptr( 128, 112 ), ie, NB_I128 ), "LOAD DATA integer" );
+ cpu_asm_image_free( &im );
+ __cpu_free_context();
+ puts( "libcpu test assembler regression: PASS" );
+ return( 0 );
+ }
diff --git a/tests/tst-block-copy-runner.sh b/tests/tst-block-copy-runner.sh
new file mode 100755
index 0000000..c8d02c9
--- /dev/null
+++ b/tests/tst-block-copy-runner.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${TEST_SRCDIR:?}"
+base="${TMPDIR:-/tmp}/libcpu-block-copy-$$"
+trap 'rm -f "$base.s" "$base.o" "$base.melf"' EXIT HUP INT TERM
+"$CPU_AS" -o "$base.o" "$TEST_SRCDIR/fixtures/block-copy.s" >/dev/null
+"$CPU_LD" --stack-size=512 -o "$base.melf" "$base.o" >/dev/null
+out=`"$CPU_RUN" --int-reg=r64@0x00028 "$base.melf"`
+test "x$out" = "x22"
diff --git a/tests/tst-block-copy.c b/tests/tst-block-copy.c
new file mode 100644
index 0000000..463633b
--- /dev/null
+++ b/tests/tst-block-copy.c
@@ -0,0 +1,171 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+
+static void
+regop( cpu_operand *o, unsigned address )
+{
+ memset( o, 0, sizeof( *o ) );
+ o->type = CPU_OPERAND_REGISTER;
+ o->value.reg.bits = 64;
+ o->value.reg.address = address;
+}
+
+static void
+set_reg_u64( unsigned address, __mpu_uint64_t v )
+{
+ memcpy( __cpu_reg_ptr( 64, address ), &v, sizeof( v ) );
+}
+
+static __mpu_uint64_t
+get_reg_u64( unsigned address )
+{
+ __mpu_uint64_t v = 0;
+ memcpy( &v, __cpu_reg_ptr( 64, address ), sizeof( v ) );
+ return( v );
+}
+
+int
+main( void )
+{
+ cpu_asm_image im;
+ cpu_instruction i;
+ __mpu_uint64_t p;
+ __mpu_uint32_t psw;
+ __mpu_UNIT_t before[32];
+ unsigned k;
+ char err[256];
+ static const char src[]=
+ ".data\n"
+ ".align 8\n"
+ "v:\n"
+ " .integer 64, 1\n"
+ " .integer 64, 2\n"
+ " .integer 64, 3\n"
+ " .integer 64, 4\n"
+ ".text\n"
+ " LEA r64@0x00000, v\n"
+ " ADJSP -32\n"
+ " LEASP r64@0x00008, 0\n"
+ " BMOVI r64@0x00008, r64@0x00000, 32\n"
+ " LDI r64@0x00010, 16\n"
+ " BMOV r64@0x00008, r64@0x00000, r64@0x00010\n"
+ " HALT\n";
+
+ __cpu_init();
+ CHECK( __cpu_stack_alloc( 128 ) == 0, "stack alloc" );
+ CHECK( cpu_assemble_text( src, &im, err, sizeof( err ) ) == 0, err );
+ CHECK( __cpu_program_set( im.code, im.code_size ) == 0, "program set" );
+ CHECK( __cpu_data_set( im.data, im.data_size ) == 0, "data set" );
+
+ /* Decoder knows both block-copy signatures. */
+ __cpu_pc = 0;
+ CHECK( __cpu_decode( &i ) == CPU_EXEC_SUCCESS, "decode LEA" );
+ __cpu_pc+=i.length;
+ CHECK( __cpu_decode( &i ) == CPU_EXEC_SUCCESS, "decode ADJSP" );
+ __cpu_pc+=i.length;
+ CHECK( __cpu_decode( &i ) == CPU_EXEC_SUCCESS, "decode LEASP" );
+ __cpu_pc+=i.length;
+ CHECK( __cpu_decode( &i ) == CPU_EXEC_SUCCESS, "decode BMOVI" );
+ CHECK( i.opcode == CPU_OP_BMOVI, "BMOVI opcode" );
+ CHECK( i.length == CPU_OPCODE_SIZE + 2 * CPU_REG24_SIZE + CPU_IMM64_SIZE, "BMOVI length" );
+ CHECK( i.operand[2].type == CPU_OPERAND_IMM64 && i.operand[2].value.imm64 == 32, "BMOVI count" );
+
+ __cpu_pc = 0;
+ CHECK( __cpu_run() == CPU_EXEC_SUCCESS, "assembled block-copy program" );
+ CHECK( memcmp( __cpu_stack_ptr() + 96, __cpu_data_ptr() + CPU_DATA_NULL_GUARD_SIZE, 16 ) == 0, "BMOV runtime count DATA->STACK" );
+
+ /* Direct executor: overlapping copy must have memmove semantics. */
+ for( k = 0; k < 32; k++ ) __cpu_stack_ptr()[64 + k] = (__mpu_UNIT_t)k;
+ memcpy( before, __cpu_stack_ptr() + 64, sizeof( before ) );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_STACK, 68 ) == 0, "make overlap destination" );
+ set_reg_u64( 0x00100, p );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_STACK, 64 ) == 0, "make overlap source" );
+ set_reg_u64( 0x00108, p );
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_BMOVI;
+ regop( &i.operand[0], 0x00100 );
+ regop( &i.operand[1], 0x00108 );
+ i.operand[2].type = CPU_OPERAND_IMM64;
+ i.operand[2].value.imm64 = 16;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "BMOVI overlap" );
+ CHECK( memcmp( __cpu_stack_ptr() + 68, before, 16 ) == 0, "BMOVI uses memmove semantics" );
+
+ /* Register-sized form uses an unsigned r64 byte count. */
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_DATA, CPU_DATA_NULL_GUARD_SIZE ) == 0, "make DATA source" );
+ set_reg_u64( 0x00108, p );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_STACK, 32 ) == 0, "make STACK destination" );
+ set_reg_u64( 0x00100, p );
+ set_reg_u64( 0x00110, 24 );
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_BMOV;
+ regop( &i.operand[0], 0x00100 );
+ regop( &i.operand[1], 0x00108 );
+ regop( &i.operand[2], 0x00110 );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "BMOV register count" );
+ CHECK( memcmp( __cpu_stack_ptr() + 32, __cpu_data_ptr() + CPU_DATA_NULL_GUARD_SIZE, 24 ) == 0, "BMOV copied runtime byte count" );
+
+ /* Copying from PROGRAM is permitted. */
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_PROGRAM, 0 ) == 0, "make PROGRAM source" );
+ set_reg_u64( 0x00108, p );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_DATA, CPU_DATA_NULL_GUARD_SIZE ) == 0, "make DATA destination" );
+ set_reg_u64( 0x00100, p );
+ set_reg_u64( 0x00110, 2 );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "BMOV PROGRAM->DATA" );
+ CHECK( __cpu_data_ptr()[CPU_DATA_NULL_GUARD_SIZE] == __cpu_program_ptr()[0] && __cpu_data_ptr()[CPU_DATA_NULL_GUARD_SIZE + 1] == __cpu_program_ptr()[1], "PROGRAM source bytes copied" );
+
+ /* PROGRAM is read-only as a destination; destination memory is unchanged. */
+ memcpy( before, __cpu_program_ptr(), 16 );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_PROGRAM, 0 ) == 0, "make PROGRAM destination" );
+ set_reg_u64( 0x00100, p );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_STACK, 32 ) == 0, "make STACK source" );
+ set_reg_u64( 0x00108, p );
+ set_reg_u64( 0x00110, 8 );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_MEMORY_FAULT, "BMOV rejects PROGRAM destination" );
+ CHECK( memcmp( before, __cpu_program_ptr(), 16 ) == 0, "PROGRAM unchanged on destination fault" );
+
+ /* Out-of-range source is detected before any destination bytes change. */
+ memset( __cpu_stack_ptr() + 16, 0xa5, 16 );
+ memcpy( before, __cpu_stack_ptr() + 16, 16 );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_STACK, 16 ) == 0, "make atomic destination" );
+ set_reg_u64( 0x00100, p );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_DATA, __cpu_data_size() - 4 ) == 0, "make short source range" );
+ set_reg_u64( 0x00108, p );
+ set_reg_u64( 0x00110, 8 );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_MEMORY_FAULT, "BMOV source range fault" );
+ CHECK( memcmp( before, __cpu_stack_ptr() + 16, 16 ) == 0, "destination unchanged on source fault" );
+
+ /* Reserved segment fails as a pointer fault. */
+ CHECK( __cpu_ptr_make( &p, 0x0f, 0 ) == 0, "make reserved pointer" );
+ set_reg_u64( 0x00108, p );
+ set_reg_u64( 0x00110, 1 );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_POINTER_FAULT, "BMOV reserved source segment" );
+
+ /* PSW is not modified. */
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_DATA, CPU_DATA_NULL_GUARD_SIZE ) == 0, "make PSW source" );
+ set_reg_u64( 0x00108, p );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_STACK, 0 ) == 0, "make PSW destination" );
+ set_reg_u64( 0x00100, p );
+ set_reg_u64( 0x00110, 1 );
+ __cpu_set_psw( PSW_CF_MASK | PSW_IF_MASK );
+ psw = __cpu_get_psw();
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "BMOV PSW test" );
+ CHECK( __cpu_get_psw() == psw, "BMOV preserves PSW" );
+
+ /* Zero length is a valid no-op at one-past offsets of allocated segments. */
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_DATA, __cpu_data_size() ) == 0, "make DATA one-past" );
+ set_reg_u64( 0x00108, p );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_STACK, __cpu_stack_size() ) == 0, "make STACK one-past" );
+ set_reg_u64( 0x00100, p );
+ set_reg_u64( 0x00110, 0 );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "BMOV zero byte no-op" );
+ CHECK( get_reg_u64( 0x00110 ) == 0, "zero count register retained" );
+
+ cpu_asm_image_free( &im );
+ __cpu_free_context();
+ puts( "libcpu block-copy regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-bswap-runner.sh b/tests/tst-bswap-runner.sh
new file mode 100755
index 0000000..aec1668
--- /dev/null
+++ b/tests/tst-bswap-runner.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${TEST_SRCDIR:?}"
+base="${TMPDIR:-/tmp}/libcpu-bswap-$$"
+trap 'rm -f "$base.s" "$base.o" "$base.melf"' EXIT HUP INT TERM
+"$CPU_AS" -o "$base.o" "$TEST_SRCDIR/fixtures/byte-swap.s" >/dev/null
+"$CPU_LD" --stack-size=512 -o "$base.melf" "$base.o" >/dev/null
+out=`"$CPU_RUN" --int-reg=r64@0x00010 "$base.melf"`
+test "x$out" = "x42"
diff --git a/tests/tst-bswap.c b/tests/tst-bswap.c
new file mode 100644
index 0000000..aff9cbd
--- /dev/null
+++ b/tests/tst-bswap.c
@@ -0,0 +1,105 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+
+static void
+regop( cpu_operand *o, unsigned bits, unsigned address )
+{
+ memset( o, 0, sizeof( *o ) );
+ o->type = CPU_OPERAND_REGISTER;
+ o->value.reg.bits = bits;
+ o->value.reg.address = address;
+}
+
+static int
+do_bswap( unsigned bits, unsigned d, unsigned s )
+{
+ cpu_instruction i;
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_BSWAP;
+ regop( &i.operand[0], bits, d );
+ regop( &i.operand[1], bits, s );
+ return( __cpu_exec( &i ) );
+}
+
+int
+main( void )
+{
+ cpu_asm_image im;
+ char err[256];
+ __mpu_UNIT_t *src, *dst;
+ unsigned i;
+ __mpu_uint32_t psw;
+ const char *asrc=
+ ".text\n"
+ "LDI r64@0x00000, 42\n"
+ "BSWAP r64@0x00008, r64@0x00000\n"
+ "BSWAP r64@0x00010, r64@0x00008\n"
+ "HALT\n";
+
+ __cpu_init();
+ CHECK( cpu_assemble_text( asrc, &im, err, sizeof( err ) ) == 0, err );
+ CHECK( im.code_size == 31, "BSWAP machine size is OPCODE16 + 2*REG24" );
+ CHECK( __cpu_code_get_u16( im.code + 13 ) == CPU_OP_BSWAP, "first BSWAP opcode encoded" );
+ CHECK( __cpu_program_set( im.code, im.code_size ) == 0, "set BSWAP program" );
+ CHECK( __cpu_run() == CPU_EXEC_SUCCESS, "run BSWAP program" );
+ CHECK( __cpu_data_get_u64( __cpu_reg_ptr( 64, 0x10 ) ) == 42, "double BSWAP round trip" );
+ cpu_asm_image_free( &im );
+
+ /* r8 is a defined identity operation. */
+ src = __cpu_reg_ptr( 8, 0x100 );
+ dst = __cpu_reg_ptr( 8, 0x101 );
+ *src = 0xa5;
+ *dst = 0;
+ CHECK( do_bswap( 8, 0x101, 0x100 ) == CPU_EXEC_SUCCESS, "BSWAP r8" );
+ CHECK( *dst == 0xa5, "BSWAP r8 identity" );
+
+ /* Arbitrary wide physical registers are reversed byte for byte. */
+ src = __cpu_reg_ptr( 256, 0x200 );
+ dst = __cpu_reg_ptr( 256, 0x220 );
+ for( i = 0; i < 32; i++ ) src[i] = (__mpu_UNIT_t)i;
+ memset( dst, 0, 32 );
+ psw = PSW_CF_MASK | PSW_IF_MASK | PSW_TF_MASK;
+ __cpu_set_psw( psw );
+ CHECK( do_bswap( 256, 0x220, 0x200 ) == CPU_EXEC_SUCCESS, "BSWAP r256" );
+ for( i = 0; i < 32; i++ ) CHECK( dst[i] == (__mpu_UNIT_t)( 31 - i ), "BSWAP r256 byte reversal" );
+ CHECK( __cpu_get_psw() == psw, "BSWAP preserves PSW" );
+
+ /* In-place BSWAP uses source-snapshot semantics. */
+ src = __cpu_reg_ptr( 64, 0x300 );
+ for( i = 0; i < 8; i++ ) src[i] = (__mpu_UNIT_t)( 0x10 + i );
+ CHECK( do_bswap( 64, 0x300, 0x300 ) == CPU_EXEC_SUCCESS, "in-place BSWAP" );
+ for( i = 0; i < 8; i++ ) CHECK( src[i] == (__mpu_UNIT_t)( 0x17 - i ), "in-place BSWAP snapshot result" );
+
+ /* Maximum physical width is supported. */
+ src = __cpu_reg_ptr( 65536, 0x0000 );
+ dst = __cpu_reg_ptr( 65536, 0x2000 );
+ for( i = 0; i < 8192; i++ ) src[i] = (__mpu_UNIT_t)( i & 0xffU );
+ CHECK( do_bswap( 65536, 0x2000, 0x0000 ) == CPU_EXEC_SUCCESS, "BSWAP r65536" );
+ CHECK( dst[0] == src[8191] && dst[8191] == src[0], "BSWAP r65536 endpoints" );
+
+ /* Width mismatch is rejected before modifying destination. */
+ {
+ cpu_instruction x;
+ __mpu_UNIT_t before[8];
+ memset( x.operand, 0, sizeof( x.operand ) );
+ x.opcode = CPU_OP_BSWAP;
+ regop( &x.operand[0], 64, 0x400 );
+ regop( &x.operand[1], 32, 0x408 );
+ memset( __cpu_reg_ptr( 64, 0x400 ), 0x5a, 8 );
+ memcpy( before, __cpu_reg_ptr( 64, 0x400 ), 8 );
+ CHECK( __cpu_exec( &x ) == CPU_EXEC_WIDTH_FAULT, "BSWAP width mismatch" );
+ CHECK( memcmp( before, __cpu_reg_ptr( 64, 0x400 ), 8 ) == 0, "BSWAP width fault atomic" );
+ }
+
+ /* Assembler syntax intentionally uses raw r<bits> views, not c<bits>. */
+ CHECK( cpu_assemble_text( ".text\nBSWAP c64@0x0, c64@0x8\nHALT\n", &im, err, sizeof( err ) ) != 0,
+ "assembler rejects complex spelling for raw BSWAP");
+
+ __cpu_free_context();
+ puts( "libcpu BSWAP regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-bytecode.c b/tests/tst-bytecode.c
new file mode 100644
index 0000000..c4f885e
--- /dev/null
+++ b/tests/tst-bytecode.c
@@ -0,0 +1,56 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+int
+main( void )
+{
+ cpu_register r =
+ {
+ 128, 0
+ };
+ __mpu_UNIT_t reg[3], code[18];
+ mpu_int128_t exp;
+ cpu_instruction insn;
+ int rc;
+ CHECK( __cpu_encode_reg24( reg, &r ) == 0, "encode REG24" );
+ code[0] = 0x01;
+ code[1] = 0x10;
+ memcpy( code + 2, reg, 3 );
+ /* LDI */
+ code[5] = 0xff;
+ code[6] = 0xff;
+ code[7] = 0xff;
+ code[8] = 0xff;
+ code[9] = 0xff;
+ code[10] = 0xff;
+ code[11] = 0xff;
+ code[12] = 0xef;
+ /* -17 */
+ code[13] = 0x00;
+ code[14] = 0x01;
+ /* HALT */
+ CHECK( __cpu_decode_at( code, 15, 0, &insn ) == CPU_EXEC_SUCCESS,
+ "decode-at LDI" );
+ CHECK( insn.opcode == CPU_OP_LDI && insn.length == 13,
+ "decode-at LDI fields" );
+ CHECK( __cpu_decode_at( code, 15, 13, &insn ) == CPU_EXEC_SUCCESS,
+ "decode-at HALT" );
+ CHECK( insn.opcode == CPU_OP_HALT && insn.length == 2,
+ "decode-at HALT fields" );
+ CHECK( __cpu_decode_at( code, 14, 13, &insn ) == CPU_EXEC_DECODE_FAULT,
+ "decode-at bounds" );
+ __cpu_init();
+ CHECK( __cpu_program_set( code, 15 ) == 0, "program mmap/load" );
+ CHECK( __cpu_program_ptr() != 0, "program pointer" );
+ CHECK( __cpu_program_size() == 15, "program size" );
+ rc = __cpu_run();
+ CHECK( rc == 0, "run" );
+ CHECK( __cpu_pc == 15, "PC advanced" );
+ CHECK( __cpu_gth() != 0, "HF set" );
+ iatoi( ( mpu_int * ) exp, "-17", NB_I128 );
+ CHECK( !memcmp( __cpu_reg_ptr( 128, 0 ), exp, NB_I128 ), "LDI int64 -> int128" );
+ __cpu_free_context();
+ puts( "libcpu bytecode/program-memory regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-byteorder.c b/tests/tst-byteorder.c
new file mode 100644
index 0000000..6346c9e
--- /dev/null
+++ b/tests/tst-byteorder.c
@@ -0,0 +1,170 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+
+static int
+raw_u64_matches_target( const __mpu_UNIT_t *p, __mpu_uint64_t v )
+{
+ __mpu_UNIT_t x[8];
+ __cpu_data_put_u64( x, v );
+ return( memcmp( p, x, 8 ) == 0 );
+}
+
+int
+main( void )
+{
+ static const __mpu_UNIT_t canonical64[8] =
+ {
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88
+ };
+ static const char src[]=
+ ".data\n"
+ ".align 8\n"
+ "ival:\n"
+ " .integer 64, 1234605616436508552\n"
+ ".align 8\n"
+ "rval:\n"
+ " .real 64, 1.5\n"
+ ".align 16\n"
+ "zval:\n"
+ " .complex 128, 1.5, -2.0\n"
+ ".align 8\n"
+ "target_slot:\n"
+ " .cptr done\n"
+ ".align 8\n"
+ "copy:\n"
+ " .integer 64, 0\n"
+ ".text\n"
+ " LDI r64@0x00000, 1234605616436508552\n"
+ " LOAD r64@0x00008, ival\n"
+ " LEA r64@0x00010, ival\n"
+ " LEA r64@0x00018, copy\n"
+ " BMOVI r64@0x00018, r64@0x00010, 8\n"
+ " LOAD r64@0x00020, copy\n"
+ " CALL leaf\n"
+ " JMPP r64@0x00028\n"
+ "leaf:\n"
+ " RET\n"
+ "done:\n"
+ " HALT\n";
+ static const char call_src[]=
+ ".text\n"
+ " CALL leaf\n"
+ " HALT\n"
+ "leaf:\n"
+ " RET\n";
+ cpu_asm_image im, callim, alias;
+ __mpu_UNIT_t buf[8], real_expected[8], complex_expected[16];
+ mpu_wide_real_t rr, ri;
+ mpu_wide_complex_t zz;
+ __mpu_uint64_t v = __mpu_UINT64_C( 0x1122334455667788 ), ptr, ret;
+ char err[256];
+ int rc;
+
+ __cpu_init();
+
+ /* The two byte-order domains are deliberately different concepts. */
+ memset( buf, 0, sizeof( buf ) );
+ __cpu_code_put_u64( buf, v );
+ CHECK( !memcmp( buf, canonical64, 8 ), "CODE u64 is canonical MSB-first" );
+ CHECK( __cpu_code_get_u64( buf ) == v, "CODE u64 round trip" );
+ CHECK( CPU_CODE_BYTE_ORDER_MSB_FIRST == 1, "CODE byte-order ABI marker" );
+
+ memset( buf, 0, sizeof( buf ) );
+ __cpu_data_put_u64( buf, v );
+ CHECK( __cpu_data_get_u64( buf ) == v, "DATA/object u64 round trip" );
+#if CPU_DATA_BYTE_ORDER_LITTLE_ENDIAN == 1
+ {
+ static const __mpu_UNIT_t le[8] =
+ {
+ 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11
+ };
+ CHECK( !memcmp( buf, le, 8 ), "target data u64 little-endian bytes" );
+ }
+#elif CPU_DATA_BYTE_ORDER_BIG_ENDIAN == 1
+ CHECK( !memcmp( buf, canonical64, 8 ), "target data u64 big-endian bytes" );
+#else
+# error Unsupported LibMPU byte order for LibCPU byte-order regression
+#endif
+
+ CHECK( cpu_assemble_text( src, &im, err, sizeof( err ) ) == 0, err );
+
+ /* First instruction is LDI: OPCODE16 + REG24 + canonical IMM64. */
+ CHECK( im.code_size > 13, "assembled bytecode exists" );
+ CHECK( im.code[0] == 0x01 && im.code[1] == 0x10, "LDI opcode canonical bytes" );
+ CHECK( !memcmp( im.code + 5, canonical64, 8 ), "LDI IMM64 is canonical MSB-first" );
+
+ /* DATA directives create target object representation, not bytecode order. */
+ CHECK( im.data_size >= 64, "assembled DATA image size" );
+ CHECK( __cpu_data_get_u64( im.data + 16 ) == v, ".integer64 target data object" );
+ CHECK( raw_u64_matches_target( im.data + 16, v ), ".integer64 raw target bytes" );
+
+ memset( rr, 0, sizeof( rr ) );
+ CHECK( ascii_to_real( ( mpu_real * ) rr, "1.5", NB_R64 ) >= 0, "build expected real64" );
+ memcpy( real_expected, rr, 8 );
+ CHECK( !memcmp( im.data + 24, real_expected, 8 ), ".real64 follows LibMPU object representation" );
+
+ memset( rr, 0, sizeof( rr ) );
+ memset( ri, 0, sizeof( ri ) );
+ memset( zz, 0, sizeof( zz ) );
+ CHECK( ascii_to_real( ( mpu_real * ) rr, "1.5", NB_R64 ) >= 0, "build complex real component" );
+ CHECK( ascii_to_real( ( mpu_real * ) ri, "-2.0", NB_R64 ) >= 0, "build complex imaginary component" );
+ c_gen_complex( ( mpu_complex * ) zz, ( mpu_real * ) rr, ( mpu_real * ) ri, NB_R64 );
+ memcpy( complex_expected, zz, 16 );
+ CHECK( !memcmp( im.data + 32, complex_expected, 16 ), ".complex128 follows LibMPU object representation" );
+
+ ptr = __cpu_data_get_u64( im.data + 48 );
+ CHECK( __cpu_ptr_segment( ptr ) == CPU_PTR_SEG_PROGRAM, ".cptr stores PROGRAM PTR64 data object" );
+ CHECK( __cpu_ptr_offset( ptr ) < im.code_size, ".cptr PROGRAM offset in range" );
+
+ /* Compatibility spelling remains accepted, but .cptr is canonical. */
+ CHECK( cpu_assemble_text( ".data\n .codeptr x\n.text\nx:\n HALT\n", &alias, err, sizeof( err ) ) == 0, "legacy .codeptr alias accepted" );
+ cpu_asm_image_free( &alias );
+
+ CHECK( __cpu_program_set( im.code, im.code_size ) == 0, "program set" );
+ CHECK( __cpu_data_set( im.data, im.data_size ) == 0, "data set" );
+ CHECK( __cpu_stack_alloc( 512 ) == 0, "stack alloc" );
+
+ /* Load source and BMOV destination preserve the complete object bytes. */
+ rc = __cpu_step();
+ CHECK( rc == CPU_EXEC_SUCCESS, "step LDI" );
+ CHECK( __cpu_data_get_u64( __cpu_reg_ptr( 64, 0x00000 ) ) == v, "LDI decoded canonical bits into target register object" );
+ rc = __cpu_step();
+ CHECK( rc == CPU_EXEC_SUCCESS, "step LOAD source" );
+ CHECK( !memcmp( __cpu_reg_ptr( 64, 0x00008 ), im.data + 16, 8 ), "LOAD preserves data object bytes" );
+ rc = __cpu_step();
+ CHECK( rc == CPU_EXEC_SUCCESS, "step LEA source" );
+ rc = __cpu_step();
+ CHECK( rc == CPU_EXEC_SUCCESS, "step LEA destination" );
+ rc = __cpu_step();
+ CHECK( rc == CPU_EXEC_SUCCESS, "step BMOVI" );
+ rc = __cpu_step();
+ CHECK( rc == CPU_EXEC_SUCCESS, "step LOAD copied object" );
+ CHECK( !memcmp( __cpu_reg_ptr( 64, 0x00008 ), __cpu_reg_ptr( 64, 0x00020 ), 8 ), "LOAD-BMOV-LOAD is byte preserving" );
+ CHECK( !memcmp( __cpu_data_ptr() + 56, im.data + 16, 8 ), "BMOV copied exact target object bytes" );
+
+ cpu_asm_image_free( &im );
+ __cpu_program_free();
+ __cpu_data_free();
+ __cpu_stack_free();
+
+ /* CALL return PC is a STACK data object, not a canonical CODE64 field. */
+ CHECK( cpu_assemble_text( call_src, &callim, err, sizeof( err ) ) == 0, err );
+ CHECK( __cpu_program_set( callim.code, callim.code_size ) == 0, "call program set" );
+ CHECK( __cpu_stack_alloc( 64 ) == 0, "call stack alloc" );
+ __cpu_pc = 0;
+ rc = __cpu_step();
+ CHECK( rc == CPU_EXEC_SUCCESS, "CALL step" );
+ CHECK( __cpu_sp == 56, "CALL pushed 8-byte return object" );
+ ret = __cpu_data_get_u64( __cpu_stack_ptr() + 56 );
+ CHECK( ret == 10, "CALL return PC target data representation decodes to next PC" );
+ CHECK( raw_u64_matches_target( __cpu_stack_ptr() + 56, 10 ), "CALL return PC raw stack bytes use target data order" );
+ cpu_asm_image_free( &callim );
+
+ __cpu_free_context();
+ puts( "libcpu byte-order/object-representation regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-call-runner.sh b/tests/tst-call-runner.sh
new file mode 100755
index 0000000..e0e6994
--- /dev/null
+++ b/tests/tst-call-runner.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${TEST_SRCDIR:?}"
+base="${TMPDIR:-/tmp}/libcpu-call-$$"
+trap 'rm -f "$base.s" "$base.o" "$base.melf"' EXIT HUP INT TERM
+"$CPU_AS" -o "$base.o" "$TEST_SRCDIR/fixtures/call.s" >/dev/null
+"$CPU_LD" --stack-size=128 -o "$base.melf" "$base.o" >/dev/null
+out=`"$CPU_RUN" --int-reg=r64@0x00010 "$base.melf"`
+test "x$out" = "x33"
diff --git a/tests/tst-call.c b/tests/tst-call.c
new file mode 100644
index 0000000..7363c83
--- /dev/null
+++ b/tests/tst-call.c
@@ -0,0 +1,100 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+
+static const char leaf_src[]=
+".text\n"
+" CALL leaf\n"
+" HALT\n"
+"leaf:\n"
+" RET\n";
+
+static const char add_src[]=
+".text\n"
+" LDI r64@0x00000, 11\n"
+" LDI r64@0x00008, 22\n"
+" PUSH r64@0x00008\n"
+" PUSH r64@0x00000\n"
+" CALL add2\n"
+" ADJSP 16\n"
+" MOV r64@0x00010, r64@0x7fff8\n"
+" HALT\n"
+"add2:\n"
+" LDSP r64@0x00100, 8\n"
+" LDSP r64@0x00108, 16\n"
+" IADD r64@0x7fff8, r64@0x00100, r64@0x00108\n"
+" RET\n";
+
+int
+main( void )
+{
+ cpu_asm_image im;
+ cpu_register rr;
+ mpu_int64_t thirty_three;
+ __mpu_uint64_t saved_pc;
+ __mpu_uint32_t psw;
+ char err[256];
+ unsigned int widths[] =
+ {
+ 8, 16, 32, 64, 4096, 65536
+ };
+ size_t k;
+
+ __cpu_init();
+
+ CHECK( CPU_RETURN_AREA_BEGIN == 0x0007e000U, "return area begin" );
+ CHECK( CPU_RETURN_AREA_END == 0x00080000U, "return area end" );
+ CHECK( CPU_RETURN_AREA_SIZE == 0x00002000U, "return area size" );
+ for( k = 0; k < sizeof( widths ) / sizeof( widths[0] ); k++ )
+ {
+ __mpu_size_t n = __cpu_reg_size( widths[k] );
+ CHECK( __cpu_return_reg( &rr, widths[k] ) == 0, "canonical return register" );
+ CHECK( rr.bits == widths[k], "return register width" );
+ CHECK( rr.address == CPU_RETURN_AREA_END - (unsigned)n, "return register address formula" );
+ }
+ CHECK( __cpu_return_reg( &rr, 7 ) != 0, "reject invalid return width" );
+
+ CHECK( cpu_assemble_text( leaf_src, &im, err, sizeof( err ) ) == 0, err );
+ CHECK( __cpu_program_set( im.code, im.code_size ) == 0, "leaf program set" );
+ CHECK( __cpu_stack_alloc( 64 ) == 0, "leaf stack alloc" );
+ __cpu_pc = 0;
+ __cpu_set_psw( PSW_CF_MASK | PSW_TF_MASK );
+ psw = __cpu_get_psw();
+
+ CHECK( __cpu_step() == CPU_EXEC_SUCCESS, "CALL step" );
+ CHECK( __cpu_pc == 12, "CALL target" );
+ CHECK( __cpu_sp == 56, "CALL pushed 8-byte return address" );
+ memcpy( &saved_pc, __cpu_stack_ptr() + __cpu_sp, 8 );
+ CHECK( saved_pc == 10, "CALL stored next PC" );
+ CHECK( __cpu_get_psw() == psw, "CALL preserves PSW" );
+
+ CHECK( __cpu_step() == CPU_EXEC_SUCCESS, "RET step" );
+ CHECK( __cpu_pc == 10, "RET restored PC" );
+ CHECK( __cpu_sp == 64, "RET popped return address" );
+ CHECK( __cpu_get_psw() == psw, "RET preserves PSW" );
+ cpu_asm_image_free( &im );
+
+ __cpu_program_free();
+ __cpu_stack_free();
+ memset( __cpu_registers, 0, CPU_REGISTERS_MEM_SIZE_IN_UNITS );
+ __cpu_clear_psw();
+
+ CHECK( cpu_assemble_text( add_src, &im, err, sizeof( err ) ) == 0, err );
+ CHECK( __cpu_program_set( im.code, im.code_size ) == 0, "add program set" );
+ CHECK( __cpu_stack_alloc( 128 ) == 0, "add stack alloc" );
+ __cpu_pc = 0;
+ CHECK( __cpu_run() == CPU_EXEC_SUCCESS, "CALL ABI run" );
+ CHECK( __cpu_sp == 128, "caller cleanup restored empty SP" );
+
+ iatoi( ( mpu_int * ) thirty_three, "33", NB_I64 );
+ CHECK( !memcmp( __cpu_reg_ptr( 64, 0x7fff8 ), thirty_three, NB_I64 ), "return value area result" );
+ CHECK( !memcmp( __cpu_reg_ptr( 64, 0x00010 ), thirty_three, NB_I64 ), "caller copied return value" );
+
+ cpu_asm_image_free( &im );
+ __cpu_free_context();
+ puts( "libcpu CALL ABI regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-context.c b/tests/tst-context.c
new file mode 100644
index 0000000..6384d75
--- /dev/null
+++ b/tests/tst-context.c
@@ -0,0 +1,121 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+
+#include <libmcpu.h>
+
+
+static int failures = 0;
+
+#define CHECK(cond,msg) \
+ do { \
+ if( !(cond) ) \
+ { \
+ fprintf( stderr, "FAIL: %s\n", (msg) ); \
+ ++failures; \
+ } \
+ } while( 0 )
+
+
+int
+main( void )
+{
+ __mpu_uint32_t psw;
+
+ __cpu_init();
+
+ CHECK( __cpu_pc == 0, "PC after __cpu_init()" );
+ CHECK( __cpu_sp == 0, "SP after __cpu_init()" );
+ CHECK( __cpu_fp == 0, "FP after __cpu_init()" );
+ CHECK( __cpu_get_psw() == 0, "PSW after __cpu_init()" );
+
+ /* Verify access to the complete 512 KiB register file. */
+ __cpu_registers[0] = 0x5a;
+ __cpu_registers[CPU_REGISTERS_MEM_SIZE_IN_UNITS - 1] = 0xa5;
+ CHECK( __cpu_registers[0] == 0x5a, "register file first byte" );
+ CHECK( __cpu_registers[CPU_REGISTERS_MEM_SIZE_IN_UNITS - 1] == 0xa5,
+ "register file last byte" );
+
+ /* Low PSW bits are maintained by LibMPU. */
+ __mpu_stc();
+ __mpu_stz();
+ __mpu_stinx();
+
+ psw = __cpu_get_psw();
+ CHECK( (psw & PSW_CF_MASK) != 0, "PSW reads LibMPU CF" );
+ CHECK( (psw & PSW_ZF_MASK) != 0, "PSW reads LibMPU ZF" );
+ CHECK( (psw & PSW_INXF_MASK) != 0, "PSW reads LibMPU INXF" );
+
+ /* High PSW bits belong to LibCPU. */
+ __cpu_sth();
+ __cpu_stt();
+ __cpu_sti();
+ CHECK( __cpu_stmode( 2 ) == 0, "set CPU MODE" );
+
+ psw = __cpu_get_psw();
+ CHECK( (psw & PSW_HF_MASK) != 0, "PSW HF" );
+ CHECK( (psw & PSW_TF_MASK) != 0, "PSW TF" );
+ CHECK( (psw & PSW_IF_MASK) != 0, "PSW IF" );
+ CHECK( __cpu_gtmode() == 2, "PSW MODE" );
+
+ /* CPU flag operations do not alter the LibMPU half of PSW. */
+ __cpu_clh();
+ __cpu_cmt();
+ __cpu_cmi();
+
+ psw = __cpu_get_psw();
+ CHECK( (psw & PSW_HF_MASK) == 0, "clear HF" );
+ CHECK( (psw & PSW_TF_MASK) == 0, "complement TF" );
+ CHECK( (psw & PSW_IF_MASK) == 0, "complement IF" );
+ CHECK( (psw & PSW_CF_MASK) != 0, "CF preserved by CPU flags" );
+ CHECK( (psw & PSW_ZF_MASK) != 0, "ZF preserved by CPU flags" );
+ CHECK( (psw & PSW_INXF_MASK) != 0, "INXF preserved by CPU flags" );
+
+ /* Store/restore the complete logical PSW. */
+ __cpu_set_psw( PSW_AF_MASK | PSW_OF_MASK | PSW_DOMF_MASK |
+ PSW_HF_MASK | PSW_IF_MASK |
+ ((__mpu_uint32_t)3 << PSW_MODE_SHIFT) );
+
+ psw = __cpu_get_psw();
+ CHECK( psw == (PSW_AF_MASK | PSW_OF_MASK | PSW_DOMF_MASK |
+ PSW_HF_MASK | PSW_IF_MASK |
+ ((__mpu_uint32_t)3 << PSW_MODE_SHIFT)),
+ "complete PSW store/restore" );
+ CHECK( __mpu_gta() == 1, "LibMPU AF after __cpu_set_psw()" );
+ CHECK( __mpu_gto() == 1, "LibMPU OF after __cpu_set_psw()" );
+ CHECK( __mpu_gtdom() == 1, "LibMPU DOMF after __cpu_set_psw()" );
+ CHECK( __mpu_gtc() == 0, "LibMPU CF cleared by __cpu_set_psw()" );
+ CHECK( __cpu_gth() == 1, "LibCPU HF after __cpu_set_psw()" );
+ CHECK( __cpu_gti() == 1, "LibCPU IF after __cpu_set_psw()" );
+ CHECK( __cpu_gtmode() == 3, "LibCPU MODE after __cpu_set_psw()" );
+
+ __cpu_clear_psw();
+ CHECK( __cpu_get_psw() == 0, "clear complete PSW" );
+
+ __cpu_pc = 0x123456789abcdef0ULL;
+ __cpu_sp = 0x0fedcba987654321ULL;
+ __cpu_fp = 0x0123456789abcdefULL;
+ CHECK( __cpu_pc == 0x123456789abcdef0ULL, "PC access" );
+ CHECK( __cpu_sp == 0x0fedcba987654321ULL, "SP access" );
+ CHECK( __cpu_fp == 0x0123456789abcdefULL, "FP access" );
+
+ __cpu_free_context();
+ CHECK( __cpu_pc == 0, "PC after __cpu_free_context()" );
+ CHECK( __cpu_sp == 0, "SP after __cpu_free_context()" );
+ CHECK( __cpu_fp == 0, "FP after __cpu_free_context()" );
+ CHECK( __cpu_get_psw() == 0, "PSW after __cpu_free_context()" );
+
+ if( failures != 0 )
+ {
+ fprintf( stderr, "libcpu context regression: %d failure(s)\n", failures );
+ return( 1 );
+ }
+
+ printf( "libcpu context/PSW regression: PASS\n" );
+ printf( "register file: %u bytes\n", CPU_REGISTERS_MEM_SIZE_IN_UNITS );
+ printf( "PSW: LibMPU bits 0..15, LibCPU bits 16..20\n" );
+
+ return( 0 );
+}
diff --git a/tests/tst-control.c b/tests/tst-control.c
new file mode 100644
index 0000000..7a3300f
--- /dev/null
+++ b/tests/tst-control.c
@@ -0,0 +1,80 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+
+
+static const char alias_src[]=
+".text\n"
+" JZ end\n"
+" JNZ end\n"
+" JC end\n"
+" JNC end\n"
+" JO end\n"
+" JNO end\n"
+" JS end\n"
+" JNS end\n"
+"end:\n"
+" HALT\n";
+
+static const char src[]=
+".text\n"
+" JMP start\n"
+"dead:\n"
+" HALT\n"
+"start:\n"
+" LDI r64@0x00000, 3\n"
+" LDI r64@0x00008, 1\n"
+" CLR r64@0x00010\n"
+"loop:\n"
+" ISUB r64@0x00000, r64@0x00000, r64@0x00008\n"
+" ICMP r64@0x00000, r64@0x00010\n"
+" JNZ loop\n"
+" JSET ZF, done\n"
+" LDI r64@0x00018, 77\n"
+"done:\n"
+" LDI r64@0x00018, 42\n"
+" HALT\n";
+
+int
+main( void )
+{
+ cpu_asm_image im, aliases;
+ cpu_instruction insn;
+ mpu_int64_t zero, forty_two;
+ char err[256];
+ int rc;
+
+ __cpu_init();
+ CHECK( cpu_assemble_text( alias_src, &aliases, err, sizeof( err ) ) == 0, err );
+ CHECK( aliases.code_size == 90, "all eight jump aliases assembled" );
+ cpu_asm_image_free( &aliases );
+
+ CHECK( cpu_assemble_text( src, &im, err, sizeof( err ) ) == 0, err );
+ CHECK( im.code_size > 0, "control-flow code produced" );
+ CHECK( __cpu_program_set( im.code, im.code_size ) == 0, "program set" );
+
+ __cpu_pc = 0;
+ CHECK( __cpu_decode( &insn ) == 0, "decode JMP" );
+ CHECK( insn.opcode == CPU_OP_JMP, "first opcode JMP" );
+ CHECK( insn.length == 10, "JMP length" );
+ CHECK( insn.operand[0].type == CPU_OPERAND_CODE64, "JMP CODE64 operand" );
+ CHECK( insn.operand[0].value.code64 < im.code_size, "JMP target in PROGRAM" );
+
+ rc = __cpu_run();
+ CHECK( rc == CPU_EXEC_SUCCESS, "control-flow run" );
+ CHECK( __cpu_pc == im.code_size, "PC after final HALT" );
+ CHECK( __cpu_gth() != 0, "HALT flag set" );
+
+ iatoi( ( mpu_int * ) zero, "0", NB_I64 );
+ iatoi( ( mpu_int * ) forty_two, "42", NB_I64 );
+ CHECK( !memcmp( __cpu_reg_ptr( 64, 0 ), zero, NB_I64 ), "backward JNZ loop reached zero" );
+ CHECK( !memcmp( __cpu_reg_ptr( 64, 24 ), forty_two, NB_I64 ), "forward JSET reached done label" );
+
+ cpu_asm_image_free( &im );
+ __cpu_free_context();
+ puts( "libcpu control-flow regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-conversion-flags.c b/tests/tst-conversion-flags.c
new file mode 100644
index 0000000..5244c1a
--- /dev/null
+++ b/tests/tst-conversion-flags.c
@@ -0,0 +1,204 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+
+static int failures;
+#define CHECK(c,m) do { if(!(c)){ fprintf(stderr,"FAIL: %s\n",(m)); ++failures; } } while(0)
+
+static cpu_operand
+R( unsigned bits, __mpu_uint32_t addr )
+{
+ cpu_operand o;
+ memset( &o, 0, sizeof( o ) );
+ o.type = CPU_OPERAND_REGISTER;
+ o.value.reg.bits = bits;
+ o.value.reg.address = addr;
+ return( o );
+}
+
+static cpu_operand
+IMM( __mpu_uint64_t v )
+{
+ cpu_operand o;
+ memset( &o, 0, sizeof( o ) );
+ o.type = CPU_OPERAND_IMM64;
+ o.value.imm64 = v;
+ return( o );
+}
+
+static void
+I( cpu_instruction *i, cpu_opcode op )
+{
+ memset( i, 0, sizeof( *i ) );
+ i->opcode = op;
+}
+
+static __mpu_UNIT_t *
+P( unsigned bits, __mpu_uint32_t addr )
+{
+ return( __cpu_reg_ptr( bits, addr ) );
+}
+
+static __mpu_uint32_t
+seed_psw( void )
+{
+ __mpu_uint32_t p=
+ PSW_AF_MASK | PSW_OF_MASK | PSW_PF_MASK | PSW_RF_MASK |
+ PSW_DOMF_MASK | PSW_OVFF_MASK | PSW_TLSF_MASK | PSW_INDF_MASK |
+ PSW_TF_MASK | PSW_IF_MASK |
+ ( (__mpu_uint32_t)2U << PSW_MODE_SHIFT );
+ __cpu_set_psw( p );
+ return( p );
+}
+
+static void
+preserved( const char *name )
+{
+ __mpu_uint32_t expect = seed_psw();
+ (void)name;
+ CHECK( __cpu_get_psw() == expect, "PSW seed" );
+}
+
+static void
+check_after( const char *name, __mpu_uint32_t expect )
+{
+ if( __cpu_get_psw() != expect )
+ {
+ fprintf( stderr, "FAIL: %s changed PSW: got 0x%08x expected 0x%08x\n",
+ name, (unsigned)__cpu_get_psw(), (unsigned)expect);
+ ++failures;
+ }
+}
+
+int
+main( void )
+{
+ cpu_instruction x;
+ __mpu_uint32_t psw;
+ __cpu_init();
+
+ /* LDI: runtime icvt is an implementation detail and must be flag-neutral. */
+ I( &x, CPU_OP_LDI );
+ x.operand[0] = R( 128, 0x000 );
+ x.operand[1] = IMM( 42 );
+ psw = seed_psw();
+ CHECK( __cpu_exec( &x ) == 0, "LDI exec" );
+ check_after( "LDI", psw );
+
+ /* LDR: runtime real64 -> destination conversion is also flag-neutral. */
+ {
+ __mpu_real64_t v = 2.0;
+ __mpu_uint64_t bits;
+ memcpy( &bits, &v, 8 );
+ I( &x, CPU_OP_LDR );
+ x.operand[0] = R( 128, 0x020 );
+ x.operand[1] = IMM( bits );
+ psw = seed_psw();
+ CHECK( __cpu_exec( &x ) == 0, "LDR exec" );
+ check_after( "LDR", psw );
+ }
+
+ /* LDC: two real32 components are widened internally, but PSW is preserved. */
+ {
+ __mpu_real32_t vr = 1.0f, vi = 2.0f;
+ __mpu_uint32_t br, bi;
+ __mpu_uint64_t payload;
+ memcpy( &br, &vr, 4 );
+ memcpy( &bi, &vi, 4 );
+ payload = ( (__mpu_uint64_t)br << 32 ) | bi;
+ I( &x, CPU_OP_LDC );
+ x.operand[0] = R( 256, 0x040 );
+ x.operand[1] = IMM( payload );
+ psw = seed_psw();
+ CHECK( __cpu_exec( &x ) == 0, "LDC exec" );
+ check_after( "LDC", psw );
+ }
+
+ /* Explicit integer conversion. */
+ *P( 8, 0x080 ) = 0xff;
+ I( &x, CPU_OP_ICVT );
+ x.operand[0] = R( 16, 0x082 );
+ x.operand[1] = R( 8, 0x080 );
+ psw = seed_psw();
+ CHECK( __cpu_exec( &x ) == 0, "ICVT exec" );
+ check_after( "ICVT", psw );
+
+ /* Explicit Real conversion. */
+ CHECK( ascii_to_real( ( mpu_real * ) P( 64, 0x090 ), (__mpu_char8_t *)"3.5", NB_R64 ) >= 0, "real source" );
+ I( &x, CPU_OP_RCVT );
+ x.operand[0] = R( 128, 0x0a0 );
+ x.operand[1] = R( 64, 0x090 );
+ psw = seed_psw();
+ CHECK( __cpu_exec( &x ) == 0, "RCVT exec" );
+ check_after( "RCVT", psw );
+
+ /* Integer <-> Real conversions. */
+ __cpu_data_put_u64( P( 64, 0x0c0 ), 42 );
+ I( &x, CPU_OP_ITOR );
+ x.operand[0] = R( 64, 0x0c8 );
+ x.operand[1] = R( 64, 0x0c0 );
+ psw = seed_psw();
+ CHECK( __cpu_exec( &x ) == 0, "ITOR exec" );
+ check_after( "ITOR", psw );
+
+ __cpu_data_put_u64( P( 64, 0x0d0 ), 42 );
+ I( &x, CPU_OP_UITOR );
+ x.operand[0] = R( 128, 0x0e0 );
+ x.operand[1] = R( 64, 0x0d0 );
+ psw = seed_psw();
+ CHECK( __cpu_exec( &x ) == 0, "UITOR exec" );
+ check_after( "UITOR", psw );
+
+ CHECK( ascii_to_real( ( mpu_real * ) P( 64, 0x100 ), (__mpu_char8_t *)"42.5", NB_R64 ) >= 0, "RTOI source" );
+ I( &x, CPU_OP_RTOI );
+ x.operand[0] = R( 64, 0x108 );
+ x.operand[1] = R( 64, 0x110 );
+ x.operand[2] = R( 64, 0x100 );
+ psw = seed_psw();
+ CHECK( __cpu_exec( &x ) == 0, "RTOI exec" );
+ check_after( "RTOI", psw );
+
+ CHECK( ascii_to_real( ( mpu_real * ) P( 64, 0x118 ), (__mpu_char8_t *)"42.5", NB_R64 ) >= 0, "RTOUI source" );
+ I( &x, CPU_OP_RTOUI );
+ x.operand[0] = R( 64, 0x120 );
+ x.operand[1] = R( 64, 0x128 );
+ x.operand[2] = R( 64, 0x118 );
+ psw = seed_psw();
+ CHECK( __cpu_exec( &x ) == 0, "RTOUI exec" );
+ check_after( "RTOUI", psw );
+
+ /* Complex conversion and Real -> Complex construction. */
+ {
+ mpu_real64_t rr, ri;
+ CHECK( ascii_to_real( ( mpu_real * ) rr, (__mpu_char8_t *)"3.0", NB_R64 ) >= 0, "complex real" );
+ CHECK( ascii_to_real( ( mpu_real * ) ri, (__mpu_char8_t *)"4.0", NB_R64 ) >= 0, "complex imag" );
+ c_gen_complex( ( mpu_complex * ) P( 128, 0x140 ), ( mpu_real * ) rr, ( mpu_real * ) ri, NB_R64 );
+ }
+ I( &x, CPU_OP_CCVT );
+ x.operand[0] = R( 256, 0x160 );
+ x.operand[1] = R( 128, 0x140 );
+ psw = seed_psw();
+ CHECK( __cpu_exec( &x ) == 0, "CCVT exec" );
+ check_after( "CCVT", psw );
+
+ CHECK( ascii_to_real( ( mpu_real * ) P( 64, 0x190 ), (__mpu_char8_t *)"5.0", NB_R64 ) >= 0, "CRTOC source" );
+ I( &x, CPU_OP_CRTOC );
+ x.operand[0] = R( 128, 0x1a0 );
+ x.operand[1] = R( 64, 0x190 );
+ psw = seed_psw();
+ CHECK( __cpu_exec( &x ) == 0, "CRTOC exec" );
+ check_after( "CRTOC", psw );
+
+ __cpu_free_context();
+ if( failures )
+ {
+ fprintf( stderr, "libcpu conversion flag regression: %d failure(s)\n", failures );
+ return( 1 );
+ }
+ puts( "libcpu conversion flag preservation regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-doc-version-policy.sh b/tests/tst-doc-version-policy.sh
new file mode 100755
index 0000000..cd50995
--- /dev/null
+++ b/tests/tst-doc-version-policy.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+set -e
+
+: "${TEST_SRCDIR:?}"
+
+TOP_SRCDIR=`cd "${TEST_SRCDIR}/.." && pwd`
+README="${TOP_SRCDIR}/README.md"
+MAN_DIR="${TOP_SRCDIR}/man"
+
+fail()
+{
+ echo "tst-doc-version-policy: $*" >&2
+ exit 1
+}
+
+# README is a current-interface document, not a release changelog. Keep
+# package release numbers in NEWS/ChangeLog and obtain the installed version
+# from the tools themselves.
+if grep -E 'LibMCPU [0-9]+\.[0-9]+\.[0-9]+|libmcpu-[0-9]+\.[0-9]+\.[0-9]+' \
+ "${README}" >/dev/null ; then
+ fail "README.md contains a release-bound LibMCPU version string"
+fi
+
+# Manual pages describe the interface contract. Their .TH source field is
+# deliberately version-independent so a release bump cannot leave stale
+# numbers behind.
+while IFS= read -r page ; do
+ header=`sed -n '1p' "${page}"`
+ case "${header}" in
+ *'"LibMCPU"'*)
+ ;;
+ *)
+ fail "${page}: .TH source field is not version-independent"
+ ;;
+ esac
+
+done < <(find "${MAN_DIR}" -type f \( -name '*.1' -o -name '*.7' \) | sort)
+
+if grep -ER 'LibMCPU [0-9]+\.[0-9]+\.[0-9]+' "${MAN_DIR}" >/dev/null ; then
+ fail "manual page contains a release-bound LibMCPU version string"
+fi
+
+exit 0
diff --git a/tests/tst-dup.c b/tests/tst-dup.c
new file mode 100644
index 0000000..3941378
--- /dev/null
+++ b/tests/tst-dup.c
@@ -0,0 +1,187 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+
+#include <libmcpu.h>
+#include <sys/asm/errno.h>
+#include <sys/asm/fcntl.h>
+#include <sys/asm/syscall.h>
+
+#include "mcpu-asm.h"
+
+
+#define CHECK(cond,msg) \
+ do { \
+ if( !(cond) ) \
+ { \
+ fprintf( stderr, "FAIL: %s\n", (msg) ); \
+ return( 1 ); \
+ } \
+ } while( 0 )
+
+
+static void
+put32( __mpu_uint32_t address, __mpu_uint32_t value )
+{
+ __cpu_data_put_u32( __cpu_reg_ptr( 32, address ), value );
+}
+
+
+static void
+put64( __mpu_uint32_t address, __mpu_uint64_t value )
+{
+ __cpu_data_put_u64( __cpu_reg_ptr( 64, address ), value );
+}
+
+
+static __mpu_uint64_t
+reg64( __mpu_uint32_t address )
+{
+ return( __cpu_data_get_u64( __cpu_reg_ptr( 64, address ) ) );
+}
+
+
+static int
+run_syscall( __mpu_uint32_t nargs, __mpu_uint32_t sysno,
+ __mpu_uint64_t arg0, __mpu_uint64_t arg1,
+ __mpu_uint64_t arg2, __mpu_uint64_t *ret )
+{
+ int rc;
+
+ put32( CPU_SYSCALL_NARGS_ADDR, nargs );
+ put32( CPU_SYSCALL_SYSNO_ADDR, sysno );
+ put64( CPU_SYSCALL_ARG0_ADDR, arg0 );
+ put64( CPU_SYSCALL_ARG1_ADDR, arg1 );
+ put64( CPU_SYSCALL_ARG2_ADDR, arg2 );
+
+ __cpu_pc = 0;
+ rc = __cpu_run();
+ if( rc != CPU_EXEC_SUCCESS ) return( rc );
+
+ *ret = reg64( CPU_SYSCALL_RET_ADDR );
+ return( 0 );
+
+} /* End of run_syscall() */
+
+
+static __mpu_int64_t
+signed_result( __mpu_uint64_t value )
+{
+ __mpu_int64_t ret;
+
+ memcpy( &ret, &value, sizeof( ret ) );
+ return( ret );
+
+} /* End of signed_result() */
+
+
+int
+main( void )
+{
+ static const char source[] =
+ ".text\n"
+ "SYSCALL\n"
+ "HALT\n";
+ cpu_asm_image image;
+ char err[256];
+ __mpu_uint64_t ret;
+
+ memset( &image, 0, sizeof( image ) );
+
+ CHECK( __NR_dup == 0x126, "public __NR_dup ABI number" );
+ CHECK( __NR_dup2 == 0x127, "public __NR_dup2 ABI number" );
+
+ __cpu_init();
+
+ CHECK( cpu_assemble_text( source, &image, err, sizeof( err ) ) == 0, err );
+ CHECK( __cpu_program_set( image.code, image.code_size ) == 0,
+ "program load" );
+
+ /* dup() returns the lowest free guest descriptor. */
+ CHECK( run_syscall( 1, __NR_dup, 1, 0, 0, &ret ) == 0, "dup stdout" );
+ CHECK( ret == 3, "dup returns lowest free guest fd 3" );
+
+ /* Descriptor flags are not copied by dup(). */
+ CHECK( run_syscall( 3, __NR_fcntl, 1, F_SETFD, FD_CLOEXEC, &ret ) == 0,
+ "set FD_CLOEXEC on source" );
+ CHECK( ret == 0, "F_SETFD source result" );
+ CHECK( run_syscall( 3, __NR_fcntl, 3, F_GETFD, 0, &ret ) == 0,
+ "F_GETFD duplicated descriptor" );
+ CHECK( ret == 0, "dup clears descriptor flags" );
+
+ /* dup2() installs exactly the requested guest descriptor. */
+ CHECK( run_syscall( 2, __NR_dup2, 1, 10, 0, &ret ) == 0,
+ "dup2 stdout to guest fd 10" );
+ CHECK( ret == 10, "dup2 returns requested guest fd" );
+
+ CHECK( run_syscall( 3, __NR_fcntl, 10, F_SETFD, FD_CLOEXEC, &ret ) == 0,
+ "set FD_CLOEXEC on dup2 target" );
+ CHECK( ret == 0, "set target flags" );
+
+ /* Replacing an occupied target clears descriptor flags. */
+ CHECK( run_syscall( 2, __NR_dup2, 3, 10, 0, &ret ) == 0,
+ "dup2 replaces occupied target" );
+ CHECK( ret == 10, "dup2 replacement return" );
+ CHECK( run_syscall( 3, __NR_fcntl, 10, F_GETFD, 0, &ret ) == 0,
+ "F_GETFD replaced target" );
+ CHECK( ret == 0, "dup2 replacement clears descriptor flags" );
+
+ /* oldfd == newfd is a no-op and preserves descriptor flags. */
+ CHECK( run_syscall( 3, __NR_fcntl, 1, F_GETFD, 0, &ret ) == 0,
+ "F_GETFD source before dup2 self" );
+ CHECK( ret == FD_CLOEXEC, "source flag before dup2 self" );
+ CHECK( run_syscall( 2, __NR_dup2, 1, 1, 0, &ret ) == 0,
+ "dup2 oldfd equals newfd" );
+ CHECK( ret == 1, "dup2 self return" );
+ CHECK( run_syscall( 3, __NR_fcntl, 1, F_GETFD, 0, &ret ) == 0,
+ "F_GETFD source after dup2 self" );
+ CHECK( ret == FD_CLOEXEC, "dup2 self preserves descriptor flags" );
+
+ /* Closing a borrowed standard guest fd does not close HOST stdio, and dup()
+ may subsequently reuse that lowest free guest slot. */
+ CHECK( run_syscall( 1, __NR_close, 0, 0, 0, &ret ) == 0,
+ "close borrowed guest stdin" );
+ CHECK( ret == 0, "close borrowed guest stdin result" );
+ CHECK( run_syscall( 1, __NR_dup, 1, 0, 0, &ret ) == 0,
+ "dup reuses closed guest fd 0" );
+ CHECK( ret == 0, "dup returns lowest free guest fd 0" );
+
+ /* Invalid source and target descriptors use the stable MCPU EBADF value. */
+ CHECK( run_syscall( 1, __NR_dup, 255, 0, 0, &ret ) == 0,
+ "dup invalid source" );
+ CHECK( signed_result( ret ) == -(__mpu_int64_t)EBADF,
+ "dup invalid source returns -EBADF" );
+
+ CHECK( run_syscall( 2, __NR_dup2, 1, 256, 0, &ret ) == 0,
+ "dup2 invalid target" );
+ CHECK( signed_result( ret ) == -(__mpu_int64_t)EBADF,
+ "dup2 invalid target returns -EBADF" );
+
+ CHECK( run_syscall( 3, __NR_fcntl, 10, F_SETFD, FD_CLOEXEC, &ret ) == 0,
+ "set target flag before invalid-source dup2" );
+ CHECK( run_syscall( 2, __NR_dup2, 255, 10, 0, &ret ) == 0,
+ "dup2 invalid source with occupied target" );
+ CHECK( signed_result( ret ) == -(__mpu_int64_t)EBADF,
+ "dup2 invalid source returns -EBADF" );
+ CHECK( run_syscall( 3, __NR_fcntl, 10, F_GETFD, 0, &ret ) == 0,
+ "target survives invalid-source dup2" );
+ CHECK( ret == FD_CLOEXEC,
+ "invalid-source dup2 leaves target unchanged" );
+
+ CHECK( run_syscall( 1, __NR_close, 0, 0, 0, &ret ) == 0,
+ "close reused guest fd 0" );
+ CHECK( run_syscall( 1, __NR_close, 3, 0, 0, &ret ) == 0,
+ "close dup guest fd 3" );
+ CHECK( run_syscall( 1, __NR_close, 10, 0, 0, &ret ) == 0,
+ "close dup2 guest fd 10" );
+
+ cpu_asm_image_free( &image );
+ __cpu_free_context();
+
+ puts( "libmcpu dup/dup2 regression: PASS" );
+ return( 0 );
+
+} /* End of main() */
diff --git a/tests/tst-dynamic-runner.sh b/tests/tst-dynamic-runner.sh
new file mode 100755
index 0000000..2ebc76f
--- /dev/null
+++ b/tests/tst-dynamic-runner.sh
@@ -0,0 +1,96 @@
+#!/bin/sh
+set -eu
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${TEST_SRCDIR:?}"
+TMP=${TMPDIR:-/tmp}/libcpu-dynamic-$$
+mkdir -p "$TMP"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+EXAMPLES=`cd "$TEST_SRCDIR/fixtures/shared-libs" && pwd`
+"$CPU_AS" -o "$TMP/main.o" "$EXAMPLES/dynamic-main.s"
+"$CPU_AS" -o "$TMP/a.o" "$EXAMPLES/dynamic-liba.s"
+"$CPU_AS" -o "$TMP/b.o" "$EXAMPLES/dynamic-libb.s"
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap -o "$TMP/main-program" "$TMP/main.o"
+"$CPU_LD" --shared --soname=liba.so.1 -o "$TMP/liba.so" "$TMP/a.o"
+"$CPU_LD" --shared --soname=libb.so.1 -o "$TMP/libb.so" "$TMP/b.o"
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 "$TMP/main-program"`" = 41 ]
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 --ldpreload="$TMP/liba.so" "$TMP/main-program"`" = 42 ]
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 --ldpreload="$TMP/liba.so:$TMP/libb.so" "$TMP/main-program"`" = 43 ]
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 --ldpreload="$TMP/libb.so:$TMP/liba.so" "$TMP/main-program"`" = 42 ]
+
+# LOCAL references inside ET_DYN are private to that module, but their
+# section-relative values still need startup rebasing by the module base.
+# local_helper is deliberately not exported. Without LOCAL ET_DYN runtime
+# relocation the CALL below contains the shared-object-relative offset and
+# jumps to the wrong process PROGRAM address after the library is placed
+# behind the executable.
+cat > "$TMP/local-main.s" <<'EOS'
+.global _life
+.extern local_api
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL local_api
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+cat > "$TMP/local-lib.s" <<'EOS'
+.global local_api
+.text
+local_api:
+ CALL local_helper
+ RET
+local_helper:
+ LDI r64@0x00008, 7
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+"$CPU_AS" -o "$TMP/local-main.o" "$TMP/local-main.s"
+"$CPU_AS" -o "$TMP/local-lib.o" "$TMP/local-lib.s"
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap -o "$TMP/local-program" "$TMP/local-main.o"
+"$CPU_LD" --shared --soname=local.so.1 -o "$TMP/local.so" "$TMP/local-lib.o"
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 --ldpreload="$TMP/local.so" "$TMP/local-program"`" = 47 ]
+
+# ABI-v1 static executable must reject preload rather than silently relink.
+"$CPU_LD" --stack-size=4096 --no-heap -o "$TMP/static-program" "$TMP/main.o"
+if "$CPU_RUN" --ldpreload="$TMP/liba.so" "$TMP/static-program" >/dev/null 2>&1; then
+ echo "ABI-v1 executable unexpectedly accepted --ldpreload" >&2
+ exit 1
+fi
+
+# An ABI-v3 executable with an unresolved import but no DT_NEEDED provider
+# remains a startup load error. Recursive loading cannot invent dependencies.
+cat > "$TMP/missing.s" <<'EOS'
+.global _life
+.extern missing_function
+.text
+_start_life:
+ CALL missing_function
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+"$CPU_AS" -o "$TMP/missing.o" "$TMP/missing.s"
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap -o "$TMP/missing-program" "$TMP/missing.o"
+if "$CPU_RUN" "$TMP/missing-program" >/dev/null 2>&1; then
+ echo "unresolved dynamic symbol unexpectedly reached execution" >&2
+ exit 1
+fi
+
+# Shared objects own no process STACK/HEAP policy.
+if "$CPU_LD" --shared --soname=bad.so.1 --stack-size=4096 -o "$TMP/bad.so" "$TMP/a.o" >/dev/null 2>&1; then
+ echo "cpu-ld --shared unexpectedly accepted --stack-size" >&2
+ exit 1
+fi
+if "$CPU_LD" --shared -o "$TMP/no-soname.so" "$TMP/a.o" >/dev/null 2>&1; then
+ echo "ABI-v3 shared object unexpectedly accepted without SONAME" >&2
+ exit 1
+fi
+if "$CPU_LD" --shared --soname=path/bad.so.1 -o "$TMP/bad-soname.so" "$TMP/a.o" >/dev/null 2>&1; then
+ echo "ABI-v3 shared object unexpectedly accepted pathname SONAME" >&2
+ exit 1
+fi
+if "$CPU_LD" --dynamic --soname=main.so.1 --stack-size=4096 --no-heap \
+ -o "$TMP/bad-main" "$TMP/main.o" >/dev/null 2>&1; then
+ echo "ABI-v3 executable unexpectedly accepted SONAME" >&2
+ exit 1
+fi
+exit 0
diff --git a/tests/tst-dynamic.c b/tests/tst-dynamic.c
new file mode 100644
index 0000000..a70d9e1
--- /dev/null
+++ b/tests/tst-dynamic.c
@@ -0,0 +1,407 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <libmcpu.h>
+
+#include "mcpu-asm.h"
+#include "mcpu-link.h"
+#include "mcpu-dyn.h"
+
+static int
+add_symbol( cpu_asm_object *o, const char *name, unsigned sec, unsigned bind, unsigned type, __mpu_uint64_t value )
+{
+ cpu_asm_symbol *q = (cpu_asm_symbol *)realloc( o->symbols, ( o->symbol_count + 1 ) * sizeof( *q ) );
+ if( !q ) return( -1 );
+ o->symbols = q;
+ memset( &o->symbols[o->symbol_count], 0, sizeof( *q ) );
+ o->symbols[o->symbol_count].name = strdup( name );
+ if( !o->symbols[o->symbol_count].name ) return( -1 );
+ o->symbols[o->symbol_count].section = sec;
+ o->symbols[o->symbol_count].binding = bind;
+ o->symbols[o->symbol_count].type = type;
+ o->symbols[o->symbol_count].value = value;
+ ++o->symbol_count;
+ return( 0 );
+}
+
+static int
+add_reloc( cpu_asm_object *o, unsigned sec, unsigned type, size_t sym, __mpu_uint64_t off )
+{
+ cpu_asm_reloc *q = (cpu_asm_reloc *)realloc( o->relocs, ( o->reloc_count + 1 ) * sizeof( *q ) );
+ if( !q ) return( -1 );
+ o->relocs = q;
+ o->relocs[o->reloc_count] = ( cpu_asm_reloc )
+ {
+ sec, type, sym, off, 0
+ };
+ ++o->reloc_count;
+ return( 0 );
+}
+
+static void
+init_text( cpu_asm_object *o, size_t n )
+{
+ memset( o, 0, sizeof( *o ) );
+ o->text = (__mpu_UNIT_t *)calloc( n, 1 );
+ o->text_size = n;
+ o->text_align = 1;
+ o->has_text = 1;
+}
+
+static int
+tempname( char *buf, size_t n, const char *tag )
+{
+ int fd;
+ if( snprintf( buf, n, "/tmp/libcpu-%s-XXXXXX", tag ) < 0 ) return( -1 );
+ fd = mkstemp( buf );
+ if( fd < 0 ) return( -1 );
+ close( fd );
+ return( 0 );
+}
+
+int
+main( void )
+{
+ cpu_asm_object mo =
+ {
+ 0
+ }
+ , ao =
+ {
+ 0
+ }
+ , bo =
+ {
+ 0
+ }
+ , wo =
+ {
+ 0
+ }
+ , co =
+ {
+ 0
+ };
+ cpu_asm_image mi =
+ {
+ 0
+ }
+ , ai =
+ {
+ 0
+ }
+ , bi =
+ {
+ 0
+ }
+ , wi =
+ {
+ 0
+ }
+ , ci =
+ {
+ 0
+ }
+ , si =
+ {
+ 0
+ };
+ cpu_exec_options opt =
+ {
+ 256, 0, 0, 0
+ };
+ cpu_exec_loaded ld;
+ const char *needed_a[] =
+ {
+ "liba.so.1"
+ };
+ cpu_dyn_metadata v3_main =
+ {
+ 0, 0, 0
+ };
+ cpu_dyn_metadata v3_liba =
+ {
+ "liba.so.1", 0, 0
+ };
+ cpu_dyn_metadata v3_needed =
+ {
+ 0, needed_a, 1
+ };
+ char me[128], ae[128], be[128], we[128], ce[128];
+ char v3me[128], v3ae[128], v3ne[128], pre[520], err[512];
+ char *av[] =
+ {
+ "main-program"
+ };
+ int rc = 1;
+ __mpu_init();
+ init_text( &mo, 24 );
+ init_text( &ao, 16 );
+ init_text( &bo, 16 );
+ init_text( &wo, 16 );
+ init_text( &co, 16 );
+ ao.data = (__mpu_UNIT_t *)calloc( 16, 1 );
+ ao.data_size = 16;
+ ao.data_align = 8;
+ ao.has_data = 1;
+ if( !mo.text || !ao.text || !ao.data || !bo.text || !wo.text || !co.text ) goto out;
+ if( add_symbol( &mo, "_start", CPU_OBJ_SEC_TEXT, CPU_OBJ_BIND_GLOBAL, CPU_OBJ_TYPE_FUNC, 0 ) ||
+ add_symbol( &mo, "_life", CPU_OBJ_SEC_TEXT, CPU_OBJ_BIND_GLOBAL, CPU_OBJ_TYPE_FUNC, 0 ) ||
+ add_symbol( &mo, "foo", CPU_OBJ_SEC_TEXT, CPU_OBJ_BIND_GLOBAL, CPU_OBJ_TYPE_FUNC, 16 ) ||
+ add_symbol( &mo, "shared_data", CPU_OBJ_SEC_UNDEF, CPU_OBJ_BIND_WEAK, CPU_OBJ_TYPE_OBJECT, 0 ) ||
+ add_reloc( &mo, CPU_OBJ_SEC_TEXT, CPU_RELOC_PTR64_CODE, 3, 0 ) ||
+ add_reloc( &mo, CPU_OBJ_SEC_TEXT, CPU_RELOC_CODE64, 2, 8 ) ||
+ add_symbol( &ao, "foo", CPU_OBJ_SEC_TEXT, CPU_OBJ_BIND_GLOBAL, CPU_OBJ_TYPE_FUNC, 4 ) ||
+ add_symbol( &ao, "shared_data", CPU_OBJ_SEC_DATA, CPU_OBJ_BIND_GLOBAL, CPU_OBJ_TYPE_OBJECT, 0 ) ||
+ add_symbol( &ao, "local_code", CPU_OBJ_SEC_TEXT, CPU_OBJ_BIND_LOCAL, CPU_OBJ_TYPE_FUNC, 12 ) ||
+ add_reloc( &ao, CPU_OBJ_SEC_TEXT, CPU_RELOC_CODE64, 2, 0 ) ||
+ add_symbol( &ao, "local_data", CPU_OBJ_SEC_DATA, CPU_OBJ_BIND_LOCAL, CPU_OBJ_TYPE_OBJECT, 8 ) ||
+ add_reloc( &ao, CPU_OBJ_SEC_DATA, CPU_RELOC_PTR64_DATA, 3, 0 ) ||
+ add_symbol( &bo, "foo", CPU_OBJ_SEC_TEXT, CPU_OBJ_BIND_GLOBAL, CPU_OBJ_TYPE_FUNC, 6 ) ||
+ add_symbol( &bo, "helper", CPU_OBJ_SEC_TEXT, CPU_OBJ_BIND_GLOBAL, CPU_OBJ_TYPE_FUNC, 10 ) ||
+ add_symbol( &wo, "foo", CPU_OBJ_SEC_TEXT, CPU_OBJ_BIND_WEAK, CPU_OBJ_TYPE_FUNC, 8 ) ||
+ add_symbol( &co, "helper", CPU_OBJ_SEC_UNDEF, CPU_OBJ_BIND_GLOBAL, CPU_OBJ_TYPE_FUNC, 0 ) ||
+ add_reloc( &co, CPU_OBJ_SEC_TEXT, CPU_RELOC_CODE64, 0, 0 )) goto out;
+ if( cpu_link_objects( &mo, 1, &si, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "static compatibility link: %s\n", err );
+ goto out;
+ }
+ if( si.dyn_symbol_count || si.dyn_reloc_count || __cpu_code_get_u64( si.code ) != 0 || __cpu_code_get_u64( si.code + 8 ) != 16 )
+ {
+ fprintf( stderr, "ABI-v1 static link compatibility mismatch\n" );
+ goto out;
+ }
+ if( cpu_link_objects_mode( &mo, 1, &mi, CPU_LINK_DYNAMIC_EXEC, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "main link: %s\n", err );
+ goto out;
+ }
+ if( cpu_link_objects_mode( &ao, 1, &ai, CPU_LINK_SHARED, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "a link: %s\n", err );
+ goto out;
+ }
+ if( cpu_link_objects_mode( &bo, 1, &bi, CPU_LINK_SHARED, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "b link: %s\n", err );
+ goto out;
+ }
+ if( cpu_link_objects_mode( &wo, 1, &wi, CPU_LINK_SHARED, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "weak link: %s\n", err );
+ goto out;
+ }
+ if( cpu_link_objects_mode( &co, 1, &ci, CPU_LINK_SHARED, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "cross link: %s\n", err );
+ goto out;
+ }
+ if( mi.dyn_reloc_count != 2 || mi.dyn_symbol_count != 2 )
+ {
+ fprintf( stderr, "dynamic metadata count mismatch\n" );
+ goto out;
+ }
+ if( ai.dyn_reloc_count != 2 || ai.dyn_symbol_count != 4 )
+ {
+ fprintf( stderr, "LOCAL ET_DYN metadata count mismatch\n" );
+ goto out;
+ }
+ if( tempname( me, sizeof( me ), "main" ) ||
+ tempname( ae, sizeof( ae ), "a" ) ||
+ tempname( be, sizeof( be ), "b" ) ||
+ tempname( we, sizeof( we ), "weak" ) ||
+ tempname( ce, sizeof( ce ), "cross" ) ||
+ tempname( v3me, sizeof( v3me ), "v3-main" ) ||
+ tempname( v3ae, sizeof( v3ae ), "v3-a" ) ||
+ tempname( v3ne, sizeof( v3ne ), "v3-needed" ) ) goto out;
+ if( cpu_dyn_write( me, &mi, &opt, 0, err, sizeof( err ) ) ||
+ cpu_dyn_write( ae, &ai, 0, 1, err, sizeof( err ) ) ||
+ cpu_dyn_write( be, &bi, 0, 1, err, sizeof( err ) ) ||
+ cpu_dyn_write( we, &wi, 0, 1, err, sizeof( err ) ) ||
+ cpu_dyn_write( ce, &ci, 0, 1, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "write ABI-v2: %s\n", err );
+ goto files;
+ }
+ if( cpu_dyn_write_abi3(v3me, &mi, &opt, 0, &v3_main,
+ err, sizeof( err )) ||
+ cpu_dyn_write_abi3(v3ae, &ai, 0, 1, &v3_liba,
+ err, sizeof( err )) ||
+ cpu_dyn_write_abi3(v3ne, &mi, &opt, 0, &v3_needed,
+ err, sizeof( err )) )
+ {
+ fprintf( stderr, "write ABI-v3: %s\n", err );
+ goto files;
+ }
+ __cpu_init();
+ if( cpu_dyn_load( me, NULL, 1, av, &ld, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "load main: %s\n", err );
+ __cpu_free_context();
+ goto files;
+ }
+ if( __cpu_code_get_u64( __cpu_program_ptr() + 8 ) != 16 || __cpu_code_get_u64( __cpu_program_ptr() ) != 0 )
+ {
+ fprintf( stderr, "main/weak definition mismatch\n" );
+ __cpu_free_context();
+ goto files;
+ }
+ __cpu_free_context();
+
+ snprintf( pre, sizeof( pre ), "%s:%s", ae, be );
+ __cpu_init();
+ if( cpu_dyn_load( me, pre, 1, av, &ld, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "load: %s\n", err );
+ __cpu_free_context();
+ goto files;
+ }
+ /* main PROGRAM=24, a=16, b=16; later strong b wins: 24+16+6 = 46. */
+ if( __cpu_code_get_u64( __cpu_program_ptr() + 8 ) != 46 )
+ {
+ fprintf( stderr, "interposition mismatch: got %llu\n", (unsigned long long)__cpu_code_get_u64( __cpu_program_ptr() + 8 ) );
+ __cpu_free_context();
+ goto files;
+ }
+ /* main has no DATA; loader creates the single process guard and places
+ liba DATA at offset 16. ET_DYN itself contains no per-library guard. */
+ if( __cpu_code_get_u64( __cpu_program_ptr() ) != 16 )
+ {
+ fprintf( stderr, "shared DATA base/guard mismatch\n" );
+ __cpu_free_context();
+ goto files;
+ }
+ /* LOCAL ET_DYN references are not interposable, but they still require
+ startup rebasing by the final module PROGRAM/DATA base. */
+ if( __cpu_code_get_u64( __cpu_program_ptr() + 24 ) != 36 )
+ {
+ fprintf( stderr, "LOCAL ET_DYN CODE64 rebase mismatch\n" );
+ __cpu_free_context();
+ goto files;
+ }
+ {
+ __mpu_uint64_t p = 0;
+ if( __cpu_ptr_make( &p, CPU_PTR_SEG_DATA, 24 ) || __cpu_data_get_u64( __cpu_data_ptr() + 16 ) != p )
+ {
+ fprintf( stderr, "LOCAL ET_DYN PTR64_DATA rebase mismatch\n" );
+ __cpu_free_context();
+ goto files;
+ }
+ }
+ __cpu_free_context();
+
+ /* A shared object can import from another explicitly preloaded object
+ without DT_NEEDED. cross is placed at 24, b at 40, helper at 40+10. */
+ snprintf( pre, sizeof( pre ), "%s:%s", ce, be );
+ __cpu_init();
+ if( cpu_dyn_load( me, pre, 1, av, &ld, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "cross-library load: %s\n", err );
+ __cpu_free_context();
+ goto files;
+ }
+ if( __cpu_code_get_u64( __cpu_program_ptr() + 24 ) != 50 )
+ {
+ fprintf( stderr, "cross-library explicit-preload resolution mismatch\n" );
+ __cpu_free_context();
+ goto files;
+ }
+ __cpu_free_context();
+
+ /* A later WEAK never preempts an earlier strong GLOBAL, including main. */
+ __cpu_init();
+ if( cpu_dyn_load( me, we, 1, av, &ld, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "weak load: %s\n", err );
+ __cpu_free_context();
+ goto files;
+ }
+ if( __cpu_code_get_u64( __cpu_program_ptr() + 8 ) != 16 )
+ {
+ fprintf( stderr, "weak incorrectly preempted main GLOBAL\n" );
+ __cpu_free_context();
+ goto files;
+ }
+ __cpu_free_context();
+
+ /* Strong library a beats later weak: a base=24, foo=4 => 28. */
+ snprintf( pre, sizeof( pre ), "%s:%s", ae, we );
+ __cpu_init();
+ if( cpu_dyn_load( me, pre, 1, av, &ld, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "strong/weak load: %s\n", err );
+ __cpu_free_context();
+ goto files;
+ }
+ if( __cpu_code_get_u64( __cpu_program_ptr() + 8 ) != 28 )
+ {
+ fprintf( stderr, "strong/weak binding mismatch\n" );
+ __cpu_free_context();
+ goto files;
+ }
+ __cpu_free_context();
+
+ /* ABI-v3 without NEEDED keeps the established startup-relocation
+ semantics and can still use the explicit preload compatibility path. */
+ __cpu_init();
+ if( cpu_dyn_load( v3me, v3ae, 1, av, &ld, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "ABI-v3 explicit-preload load: %s\n", err );
+ __cpu_free_context();
+ goto files;
+ }
+ if( __cpu_code_get_u64( __cpu_program_ptr() + 8 ) != 28 )
+ {
+ fprintf( stderr, "ABI-v3 explicit-preload binding mismatch\n" );
+ __cpu_free_context();
+ goto files;
+ }
+ __cpu_free_context();
+
+ /* The low-level loader still refuses to ignore unresolved DT_NEEDED.
+ mcpu-run first resolves the graph and then uses cpu_dyn_load_resolved(). */
+ __cpu_init();
+ if( cpu_dyn_load( v3ne, NULL, 1, av, &ld, err, sizeof( err ) ) == 0 )
+ {
+ fprintf( stderr, "low-level ABI-v3 DT_NEEDED unexpectedly ignored\n" );
+ __cpu_free_context();
+ goto files;
+ }
+ if( strstr( err, "DT_NEEDED loading is not implemented yet" ) == NULL )
+ {
+ fprintf( stderr, "unexpected ABI-v3 DT_NEEDED diagnostic: %s\n", err );
+ __cpu_free_context();
+ goto files;
+ }
+ __cpu_free_context();
+
+ /* A static ABI-v1 executable must not silently accept preload; this is
+ checked by cpu_exec_load_preload runner tests. Here verify no missing
+ explicit library dependency machinery is needed: the list alone resolves. */
+ printf( "libcpu ABI-v2 compatibility / ABI-v3 metadata regression: PASS\n" );
+ rc = 0;
+files: unlink( me );
+unlink( ae );
+unlink( be );
+unlink( we );
+unlink( ce );
+unlink( v3me );
+unlink( v3ae );
+unlink( v3ne );
+out: cpu_asm_image_free( &si );
+cpu_asm_image_free( &mi );
+cpu_asm_image_free( &ai );
+cpu_asm_image_free( &bi );
+cpu_asm_image_free( &wi );
+cpu_asm_image_free( &ci );
+cpu_asm_object_free( &mo );
+cpu_asm_object_free( &ao );
+cpu_asm_object_free( &bo );
+cpu_asm_object_free( &wo );
+cpu_asm_object_free( &co );
+__mpu_free_context();
+return( rc );
+}
diff --git a/tests/tst-env-preload-runner.sh b/tests/tst-env-preload-runner.sh
new file mode 100755
index 0000000..5f49e9b
--- /dev/null
+++ b/tests/tst-env-preload-runner.sh
@@ -0,0 +1,124 @@
+#!/bin/sh
+set -eu
+
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-env-preload-$$
+mkdir -p "$TMP/lib"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+
+cat > "$TMP/main.s" <<'EOS'
+.global _life
+.global foo
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL foo
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+foo:
+ LDI r64@0x00008, 1
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+cat > "$TMP/a.s" <<'EOS'
+.global foo
+.text
+foo:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+cat > "$TMP/b.s" <<'EOS'
+.global foo
+.text
+foo:
+ LDI r64@0x00008, 3
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/main.o" "$TMP/main.s"
+"$CPU_AS" -o "$TMP/a.o" "$TMP/a.s"
+"$CPU_AS" -o "$TMP/b.o" "$TMP/b.s"
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap -o "$TMP/main" "$TMP/main.o"
+"$CPU_LD" --shared --soname=liba.so.1 -o "$TMP/lib/liba.so.1" "$TMP/a.o"
+"$CPU_LD" --shared --soname=libb.so.1 -o "$TMP/lib/libb.so.1" "$TMP/b.o"
+ln -s liba.so.1 "$TMP/lib/liba.so"
+ln -s libb.so.1 "$TMP/lib/libb.so"
+
+# Empty environment is the normal no-preload baseline.
+[ "`MCPU_LD_PRELOAD= "$CPU_RUN" --library-path="$TMP/lib" \
+ --int-reg=r64@0x00018 "$TMP/main"`" = 41 ]
+
+# Environment-only preload uses the same resolver and rightmost-wins ordering.
+[ "`MCPU_LD_PRELOAD=liba.so "$CPU_RUN" --library-path="$TMP/lib" \
+ --int-reg=r64@0x00018 "$TMP/main"`" = 42 ]
+[ "`MCPU_LD_PRELOAD=liba.so:libb.so "$CPU_RUN" --library-path="$TMP/lib" \
+ --int-reg=r64@0x00018 "$TMP/main"`" = 43 ]
+[ "`MCPU_LD_PRELOAD=libb.so:liba.so "$CPU_RUN" --library-path="$TMP/lib" \
+ --int-reg=r64@0x00018 "$TMP/main"`" = 42 ]
+
+# Command-line preload entries are appended after environment entries, so the
+# established rightmost-higher-priority rule makes command line stronger.
+[ "`MCPU_LD_PRELOAD=liba.so "$CPU_RUN" --library-path="$TMP/lib" \
+ --ldpreload=libb.so --int-reg=r64@0x00018 "$TMP/main"`" = 43 ]
+[ "`MCPU_LD_PRELOAD=libb.so "$CPU_RUN" --library-path="$TMP/lib" \
+ --ldpreload=liba.so --int-reg=r64@0x00018 "$TMP/main"`" = 42 ]
+[ "`MCPU_LD_PRELOAD=liba.so:libb.so "$CPU_RUN" --library-path="$TMP/lib" \
+ --ldpreload=libb.so:liba.so --int-reg=r64@0x00018 "$TMP/main"`" = 42 ]
+
+# Environment preload is equivalent to --ldpreload for ABI compatibility: an
+# ABI-v1 static executable must reject it just as it rejects the CLI option.
+"$CPU_LD" --stack-size=4096 --no-heap -o "$TMP/static" "$TMP/main.o"
+if MCPU_LD_PRELOAD="$TMP/lib/liba.so.1" "$CPU_RUN" "$TMP/static" \
+ >/dev/null 2>"$TMP/static.err"; then
+ echo "ABI-v1 executable unexpectedly accepted MCPU_LD_PRELOAD" >&2
+ exit 1
+fi
+grep -q 'dynamic modules require' "$TMP/static.err"
+
+# The environment preload is independent of configuration suppression. An
+# explicit pathname therefore still works under --no-config.
+[ "`MCPU_LD_PRELOAD="$TMP/lib/liba.so.1" "$CPU_RUN" --no-config \
+ --int-reg=r64@0x00018 "$TMP/main"`" = 42 ]
+
+# Environment preload remains an explicit override above a normal dependency.
+cat > "$TMP/import.s" <<'EOS'
+.global _life
+.extern foo
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL foo
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+"$CPU_AS" -o "$TMP/import.o" "$TMP/import.s"
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap -L"$TMP/lib" -la \
+ -o "$TMP/import" "$TMP/import.o"
+[ "`MCPU_LD_PRELOAD=libb.so "$CPU_RUN" --library-path="$TMP/lib" \
+ --int-reg=r64@0x00018 "$TMP/import"`" = 43 ]
+
+# Invalid environment lists and missing names are hard errors.
+if MCPU_LD_PRELOAD=liba.so::libb.so "$CPU_RUN" --library-path="$TMP/lib" \
+ "$TMP/main" >/dev/null 2>"$TMP/bad-list.err"; then
+ echo "malformed MCPU_LD_PRELOAD unexpectedly accepted" >&2
+ exit 1
+fi
+grep -q 'MCPU_LD_PRELOAD' "$TMP/bad-list.err"
+
+if MCPU_LD_PRELOAD=does-not-exist.so "$CPU_RUN" --library-path="$TMP/lib" \
+ "$TMP/main" >/dev/null 2>"$TMP/missing.err"; then
+ echo "missing MCPU_LD_PRELOAD library unexpectedly accepted" >&2
+ exit 1
+fi
+grep -q 'MCPU_LD_PRELOAD' "$TMP/missing.err"
+
+MCPU_LD_PRELOAD= "$CPU_RUN" --help 2>&1 | grep -q 'MCPU_LD_PRELOAD'
+
+exit 0
diff --git a/tests/tst-errno-abi.c b/tests/tst-errno-abi.c
new file mode 100644
index 0000000..1d7dcd5
--- /dev/null
+++ b/tests/tst-errno-abi.c
@@ -0,0 +1,217 @@
+#include <stdio.h>
+
+#include <libmpu.h>
+
+/* Capture the LibMPU values before the public assembler header is included. */
+enum
+{
+ LIBMPU_EUNKNOWN = __EUNKNOWN__,
+ LIBMPU_I_ESIZE = __I_ESIZE__,
+ LIBMPU_I_ESHIFT = __I_ESHIFT__,
+ LIBMPU_I_ENUMBER = __I_ENUMBER__,
+ LIBMPU_I_ERADIX = __I_ERADIX__,
+ LIBMPU_I_MAX_ERRNO = __I_MAX_ERRNO,
+ LIBMPU_R_ESIZE = __R_ESIZE__,
+ LIBMPU_R_ETRUNC = __R_ETRUNC__,
+ LIBMPU_R_MAX_ERRNO = __R_MAX_ERRNO,
+ LIBMPU_C_EDIVZEROBYZERO = __C_EDIVZEROBYZERO__,
+ LIBMPU_C_ELOGOFZERO = __C_ELOGOFZERO__,
+ LIBMPU_C_EPOWOFZERO = __C_EPOWOFZERO__,
+ LIBMPU_C_MAX_ERRNO = __C_MAX_ERRNO,
+ LIBMPU_DOMAIN = __DOMAIN__,
+ LIBMPU_SING = __SING__,
+ LIBMPU_OVERFLOW = __OVERFLOW__,
+ LIBMPU_UNDERFLOW = __UNDERFLOW__,
+ LIBMPU_TLOSS = __TLOSS__,
+ LIBMPU_PLOSS = __PLOSS__,
+ LIBMPU_INVALID = __INVALID__,
+ LIBMPU_INEXACT = __INEXACT__,
+ LIBMPU_M_MAX_ERRNO = __M_MAX_ERRNO
+};
+
+#include "../cpu/cpu-errno.h"
+#include <sys/asm/errno.h>
+#include <sys/asm/mpu-errno.h>
+
+
+#define CHECK_SYSTEM_ERRNO(name) \
+ do { \
+ if( (name) != (__MCPU_ ## name) ) \
+ { \
+ fprintf( stderr, #name " ABI mismatch: public=%d internal=%d\n", \
+ (int)(name), (int)(__MCPU_ ## name) ); \
+ return( 1 ); \
+ } \
+ } while( 0 )
+
+
+int
+main( void )
+{
+ CHECK_SYSTEM_ERRNO( EPERM );
+ CHECK_SYSTEM_ERRNO( ENOENT );
+ CHECK_SYSTEM_ERRNO( ESRCH );
+ CHECK_SYSTEM_ERRNO( EINTR );
+ CHECK_SYSTEM_ERRNO( EIO );
+ CHECK_SYSTEM_ERRNO( ENXIO );
+ CHECK_SYSTEM_ERRNO( E2BIG );
+ CHECK_SYSTEM_ERRNO( ENOEXEC );
+ CHECK_SYSTEM_ERRNO( EBADF );
+ CHECK_SYSTEM_ERRNO( ECHILD );
+ CHECK_SYSTEM_ERRNO( EAGAIN );
+ CHECK_SYSTEM_ERRNO( ENOMEM );
+ CHECK_SYSTEM_ERRNO( EACCES );
+ CHECK_SYSTEM_ERRNO( EFAULT );
+ CHECK_SYSTEM_ERRNO( ENOTBLK );
+ CHECK_SYSTEM_ERRNO( EBUSY );
+ CHECK_SYSTEM_ERRNO( EEXIST );
+ CHECK_SYSTEM_ERRNO( EXDEV );
+ CHECK_SYSTEM_ERRNO( ENODEV );
+ CHECK_SYSTEM_ERRNO( ENOTDIR );
+ CHECK_SYSTEM_ERRNO( EISDIR );
+ CHECK_SYSTEM_ERRNO( EINVAL );
+ CHECK_SYSTEM_ERRNO( ENFILE );
+ CHECK_SYSTEM_ERRNO( EMFILE );
+ CHECK_SYSTEM_ERRNO( ENOTTY );
+ CHECK_SYSTEM_ERRNO( ETXTBSY );
+ CHECK_SYSTEM_ERRNO( EFBIG );
+ CHECK_SYSTEM_ERRNO( ENOSPC );
+ CHECK_SYSTEM_ERRNO( ESPIPE );
+ CHECK_SYSTEM_ERRNO( EROFS );
+ CHECK_SYSTEM_ERRNO( EMLINK );
+ CHECK_SYSTEM_ERRNO( EPIPE );
+ CHECK_SYSTEM_ERRNO( EDOM );
+ CHECK_SYSTEM_ERRNO( ERANGE );
+ CHECK_SYSTEM_ERRNO( EDEADLK );
+ CHECK_SYSTEM_ERRNO( ENAMETOOLONG );
+ CHECK_SYSTEM_ERRNO( ENOLCK );
+ CHECK_SYSTEM_ERRNO( ENOSYS );
+ CHECK_SYSTEM_ERRNO( ENOTEMPTY );
+ CHECK_SYSTEM_ERRNO( ELOOP );
+ CHECK_SYSTEM_ERRNO( ENOMSG );
+ CHECK_SYSTEM_ERRNO( EIDRM );
+ CHECK_SYSTEM_ERRNO( ECHRNG );
+ CHECK_SYSTEM_ERRNO( EL2NSYNC );
+ CHECK_SYSTEM_ERRNO( EL3HLT );
+ CHECK_SYSTEM_ERRNO( EL3RST );
+ CHECK_SYSTEM_ERRNO( ELNRNG );
+ CHECK_SYSTEM_ERRNO( EUNATCH );
+ CHECK_SYSTEM_ERRNO( ENOCSI );
+ CHECK_SYSTEM_ERRNO( EL2HLT );
+ CHECK_SYSTEM_ERRNO( EBADE );
+ CHECK_SYSTEM_ERRNO( EBADR );
+ CHECK_SYSTEM_ERRNO( EXFULL );
+ CHECK_SYSTEM_ERRNO( ENOANO );
+ CHECK_SYSTEM_ERRNO( EBADRQC );
+ CHECK_SYSTEM_ERRNO( EBADSLT );
+ CHECK_SYSTEM_ERRNO( EBFONT );
+ CHECK_SYSTEM_ERRNO( ENOSTR );
+ CHECK_SYSTEM_ERRNO( ENODATA );
+ CHECK_SYSTEM_ERRNO( ETIME );
+ CHECK_SYSTEM_ERRNO( ENOSR );
+ CHECK_SYSTEM_ERRNO( ENONET );
+ CHECK_SYSTEM_ERRNO( ENOPKG );
+ CHECK_SYSTEM_ERRNO( EREMOTE );
+ CHECK_SYSTEM_ERRNO( ENOLINK );
+ CHECK_SYSTEM_ERRNO( EADV );
+ CHECK_SYSTEM_ERRNO( ESRMNT );
+ CHECK_SYSTEM_ERRNO( ECOMM );
+ CHECK_SYSTEM_ERRNO( EPROTO );
+ CHECK_SYSTEM_ERRNO( EMULTIHOP );
+ CHECK_SYSTEM_ERRNO( EDOTDOT );
+ CHECK_SYSTEM_ERRNO( EBADMSG );
+ CHECK_SYSTEM_ERRNO( EOVERFLOW );
+ CHECK_SYSTEM_ERRNO( ENOTUNIQ );
+ CHECK_SYSTEM_ERRNO( EBADFD );
+ CHECK_SYSTEM_ERRNO( EREMCHG );
+ CHECK_SYSTEM_ERRNO( ELIBACC );
+ CHECK_SYSTEM_ERRNO( ELIBBAD );
+ CHECK_SYSTEM_ERRNO( ELIBSCN );
+ CHECK_SYSTEM_ERRNO( ELIBMAX );
+ CHECK_SYSTEM_ERRNO( ELIBEXEC );
+ CHECK_SYSTEM_ERRNO( EILSEQ );
+ CHECK_SYSTEM_ERRNO( ERESTART );
+ CHECK_SYSTEM_ERRNO( ESTRPIPE );
+ CHECK_SYSTEM_ERRNO( EUSERS );
+ CHECK_SYSTEM_ERRNO( ENOTSOCK );
+ CHECK_SYSTEM_ERRNO( EDESTADDRREQ );
+ CHECK_SYSTEM_ERRNO( EMSGSIZE );
+ CHECK_SYSTEM_ERRNO( EPROTOTYPE );
+ CHECK_SYSTEM_ERRNO( ENOPROTOOPT );
+ CHECK_SYSTEM_ERRNO( EPROTONOSUPPORT );
+ CHECK_SYSTEM_ERRNO( ESOCKTNOSUPPORT );
+ CHECK_SYSTEM_ERRNO( EOPNOTSUPP );
+ CHECK_SYSTEM_ERRNO( EPFNOSUPPORT );
+ CHECK_SYSTEM_ERRNO( EAFNOSUPPORT );
+ CHECK_SYSTEM_ERRNO( EADDRINUSE );
+ CHECK_SYSTEM_ERRNO( EADDRNOTAVAIL );
+ CHECK_SYSTEM_ERRNO( ENETDOWN );
+ CHECK_SYSTEM_ERRNO( ENETUNREACH );
+ CHECK_SYSTEM_ERRNO( ENETRESET );
+ CHECK_SYSTEM_ERRNO( ECONNABORTED );
+ CHECK_SYSTEM_ERRNO( ECONNRESET );
+ CHECK_SYSTEM_ERRNO( ENOBUFS );
+ CHECK_SYSTEM_ERRNO( EISCONN );
+ CHECK_SYSTEM_ERRNO( ENOTCONN );
+ CHECK_SYSTEM_ERRNO( ESHUTDOWN );
+ CHECK_SYSTEM_ERRNO( ETOOMANYREFS );
+ CHECK_SYSTEM_ERRNO( ETIMEDOUT );
+ CHECK_SYSTEM_ERRNO( ECONNREFUSED );
+ CHECK_SYSTEM_ERRNO( EHOSTDOWN );
+ CHECK_SYSTEM_ERRNO( EHOSTUNREACH );
+ CHECK_SYSTEM_ERRNO( EALREADY );
+ CHECK_SYSTEM_ERRNO( EINPROGRESS );
+ CHECK_SYSTEM_ERRNO( ESTALE );
+ CHECK_SYSTEM_ERRNO( EUCLEAN );
+ CHECK_SYSTEM_ERRNO( ENOTNAM );
+ CHECK_SYSTEM_ERRNO( ENAVAIL );
+ CHECK_SYSTEM_ERRNO( EISNAM );
+ CHECK_SYSTEM_ERRNO( EREMOTEIO );
+ CHECK_SYSTEM_ERRNO( EDQUOT );
+ CHECK_SYSTEM_ERRNO( ENOMEDIUM );
+ CHECK_SYSTEM_ERRNO( EMEDIUMTYPE );
+ CHECK_SYSTEM_ERRNO( ECANCELED );
+ CHECK_SYSTEM_ERRNO( ENOKEY );
+ CHECK_SYSTEM_ERRNO( EKEYEXPIRED );
+ CHECK_SYSTEM_ERRNO( EKEYREVOKED );
+ CHECK_SYSTEM_ERRNO( EKEYREJECTED );
+ CHECK_SYSTEM_ERRNO( EOWNERDEAD );
+ CHECK_SYSTEM_ERRNO( ENOTRECOVERABLE );
+ CHECK_SYSTEM_ERRNO( ERFKILL );
+ CHECK_SYSTEM_ERRNO( EHWPOISON );
+ CHECK_SYSTEM_ERRNO( EFTYPE );
+ CHECK_SYSTEM_ERRNO( EUNKNOWN );
+
+ if( EWOULDBLOCK != EAGAIN || __MCPU_EWOULDBLOCK != __MCPU_EAGAIN )
+ return( 1 );
+ if( EDEADLOCK != EDEADLK || __MCPU_EDEADLOCK != __MCPU_EDEADLK )
+ return( 1 );
+ if( ENOTSUP != EOPNOTSUPP || __MCPU_ENOTSUP != __MCPU_EOPNOTSUPP )
+ return( 1 );
+
+ if( __EUNKNOWN__ != LIBMPU_EUNKNOWN ) return( 1 );
+ if( __I_ESIZE__ != LIBMPU_I_ESIZE ||
+ __I_ESHIFT__ != LIBMPU_I_ESHIFT ||
+ __I_ENUMBER__ != LIBMPU_I_ENUMBER ||
+ __I_ERADIX__ != LIBMPU_I_ERADIX ||
+ __I_MAX_ERRNO != LIBMPU_I_MAX_ERRNO )
+ return( 1 );
+ if( __R_ESIZE__ != LIBMPU_R_ESIZE ||
+ __R_ETRUNC__ != LIBMPU_R_ETRUNC ||
+ __R_MAX_ERRNO != LIBMPU_R_MAX_ERRNO )
+ return( 1 );
+ if( __C_EDIVZEROBYZERO__ != LIBMPU_C_EDIVZEROBYZERO ||
+ __C_ELOGOFZERO__ != LIBMPU_C_ELOGOFZERO ||
+ __C_EPOWOFZERO__ != LIBMPU_C_EPOWOFZERO ||
+ __C_MAX_ERRNO != LIBMPU_C_MAX_ERRNO )
+ return( 1 );
+ if( __DOMAIN__ != LIBMPU_DOMAIN || __SING__ != LIBMPU_SING ||
+ __OVERFLOW__ != LIBMPU_OVERFLOW ||
+ __UNDERFLOW__ != LIBMPU_UNDERFLOW || __TLOSS__ != LIBMPU_TLOSS ||
+ __PLOSS__ != LIBMPU_PLOSS || __INVALID__ != LIBMPU_INVALID ||
+ __INEXACT__ != LIBMPU_INEXACT || __M_MAX_ERRNO != LIBMPU_M_MAX_ERRNO )
+ return( 1 );
+
+ return( 0 );
+
+} /* End of main() */
diff --git a/tests/tst-errno-map.c b/tests/tst-errno-map.c
new file mode 100644
index 0000000..c8ab42d
--- /dev/null
+++ b/tests/tst-errno-map.c
@@ -0,0 +1,439 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <errno.h>
+#include <stdio.h>
+
+#include <libmcpu.h>
+#include "../cpu/cpu-sysdeps.h"
+
+
+#define CHECK_HOST_ERRNO(host_name, mcpu_name) \
+ do { \
+ int mapped = __cpu_errno_from_host( (host_name) ); \
+ if( mapped != (__MCPU_ ## mcpu_name) ) \
+ { \
+ fprintf( stderr, #host_name " mapping mismatch: got=%d expected=%d\n", \
+ mapped, (int)(__MCPU_ ## mcpu_name) ); \
+ return( 1 ); \
+ } \
+ } while( 0 )
+
+
+int
+main( void )
+{
+ if( __cpu_errno_from_host( 0 ) != 0 ) return( 1 );
+ if( __cpu_errno_from_host( -1 ) != __MCPU_EUNKNOWN ) return( 1 );
+ if( __cpu_errno_from_host( 0x7fffffff ) != __MCPU_EUNKNOWN ) return( 1 );
+
+#ifdef EPERM
+ CHECK_HOST_ERRNO( EPERM, EPERM );
+#endif
+#ifdef ENOENT
+ CHECK_HOST_ERRNO( ENOENT, ENOENT );
+#endif
+#ifdef ESRCH
+ CHECK_HOST_ERRNO( ESRCH, ESRCH );
+#endif
+#ifdef EINTR
+ CHECK_HOST_ERRNO( EINTR, EINTR );
+#endif
+#ifdef EIO
+ CHECK_HOST_ERRNO( EIO, EIO );
+#endif
+#ifdef ENXIO
+ CHECK_HOST_ERRNO( ENXIO, ENXIO );
+#endif
+#ifdef E2BIG
+ CHECK_HOST_ERRNO( E2BIG, E2BIG );
+#endif
+#ifdef ENOEXEC
+ CHECK_HOST_ERRNO( ENOEXEC, ENOEXEC );
+#endif
+#ifdef EBADF
+ CHECK_HOST_ERRNO( EBADF, EBADF );
+#endif
+#ifdef ECHILD
+ CHECK_HOST_ERRNO( ECHILD, ECHILD );
+#endif
+#ifdef EAGAIN
+ CHECK_HOST_ERRNO( EAGAIN, EAGAIN );
+#endif
+#ifdef ENOMEM
+ CHECK_HOST_ERRNO( ENOMEM, ENOMEM );
+#endif
+#ifdef EACCES
+ CHECK_HOST_ERRNO( EACCES, EACCES );
+#endif
+#ifdef EFAULT
+ CHECK_HOST_ERRNO( EFAULT, EFAULT );
+#endif
+#ifdef ENOTBLK
+ CHECK_HOST_ERRNO( ENOTBLK, ENOTBLK );
+#endif
+#ifdef EBUSY
+ CHECK_HOST_ERRNO( EBUSY, EBUSY );
+#endif
+#ifdef EEXIST
+ CHECK_HOST_ERRNO( EEXIST, EEXIST );
+#endif
+#ifdef EXDEV
+ CHECK_HOST_ERRNO( EXDEV, EXDEV );
+#endif
+#ifdef ENODEV
+ CHECK_HOST_ERRNO( ENODEV, ENODEV );
+#endif
+#ifdef ENOTDIR
+ CHECK_HOST_ERRNO( ENOTDIR, ENOTDIR );
+#endif
+#ifdef EISDIR
+ CHECK_HOST_ERRNO( EISDIR, EISDIR );
+#endif
+#ifdef EINVAL
+ CHECK_HOST_ERRNO( EINVAL, EINVAL );
+#endif
+#ifdef ENFILE
+ CHECK_HOST_ERRNO( ENFILE, ENFILE );
+#endif
+#ifdef EMFILE
+ CHECK_HOST_ERRNO( EMFILE, EMFILE );
+#endif
+#ifdef ENOTTY
+ CHECK_HOST_ERRNO( ENOTTY, ENOTTY );
+#endif
+#ifdef ETXTBSY
+ CHECK_HOST_ERRNO( ETXTBSY, ETXTBSY );
+#endif
+#ifdef EFBIG
+ CHECK_HOST_ERRNO( EFBIG, EFBIG );
+#endif
+#ifdef ENOSPC
+ CHECK_HOST_ERRNO( ENOSPC, ENOSPC );
+#endif
+#ifdef ESPIPE
+ CHECK_HOST_ERRNO( ESPIPE, ESPIPE );
+#endif
+#ifdef EROFS
+ CHECK_HOST_ERRNO( EROFS, EROFS );
+#endif
+#ifdef EMLINK
+ CHECK_HOST_ERRNO( EMLINK, EMLINK );
+#endif
+#ifdef EPIPE
+ CHECK_HOST_ERRNO( EPIPE, EPIPE );
+#endif
+#ifdef EDOM
+ CHECK_HOST_ERRNO( EDOM, EDOM );
+#endif
+#ifdef ERANGE
+ CHECK_HOST_ERRNO( ERANGE, ERANGE );
+#endif
+#ifdef EDEADLK
+ CHECK_HOST_ERRNO( EDEADLK, EDEADLK );
+#endif
+#ifdef ENAMETOOLONG
+ CHECK_HOST_ERRNO( ENAMETOOLONG, ENAMETOOLONG );
+#endif
+#ifdef ENOLCK
+ CHECK_HOST_ERRNO( ENOLCK, ENOLCK );
+#endif
+#ifdef ENOSYS
+ CHECK_HOST_ERRNO( ENOSYS, ENOSYS );
+#endif
+#ifdef ENOTEMPTY
+ CHECK_HOST_ERRNO( ENOTEMPTY, ENOTEMPTY );
+#endif
+#ifdef ELOOP
+ CHECK_HOST_ERRNO( ELOOP, ELOOP );
+#endif
+#ifdef ENOMSG
+ CHECK_HOST_ERRNO( ENOMSG, ENOMSG );
+#endif
+#ifdef EIDRM
+ CHECK_HOST_ERRNO( EIDRM, EIDRM );
+#endif
+#ifdef ECHRNG
+ CHECK_HOST_ERRNO( ECHRNG, ECHRNG );
+#endif
+#ifdef EL2NSYNC
+ CHECK_HOST_ERRNO( EL2NSYNC, EL2NSYNC );
+#endif
+#ifdef EL3HLT
+ CHECK_HOST_ERRNO( EL3HLT, EL3HLT );
+#endif
+#ifdef EL3RST
+ CHECK_HOST_ERRNO( EL3RST, EL3RST );
+#endif
+#ifdef ELNRNG
+ CHECK_HOST_ERRNO( ELNRNG, ELNRNG );
+#endif
+#ifdef EUNATCH
+ CHECK_HOST_ERRNO( EUNATCH, EUNATCH );
+#endif
+#ifdef ENOCSI
+ CHECK_HOST_ERRNO( ENOCSI, ENOCSI );
+#endif
+#ifdef EL2HLT
+ CHECK_HOST_ERRNO( EL2HLT, EL2HLT );
+#endif
+#ifdef EBADE
+ CHECK_HOST_ERRNO( EBADE, EBADE );
+#endif
+#ifdef EBADR
+ CHECK_HOST_ERRNO( EBADR, EBADR );
+#endif
+#ifdef EXFULL
+ CHECK_HOST_ERRNO( EXFULL, EXFULL );
+#endif
+#ifdef ENOANO
+ CHECK_HOST_ERRNO( ENOANO, ENOANO );
+#endif
+#ifdef EBADRQC
+ CHECK_HOST_ERRNO( EBADRQC, EBADRQC );
+#endif
+#ifdef EBADSLT
+ CHECK_HOST_ERRNO( EBADSLT, EBADSLT );
+#endif
+#ifdef EBFONT
+ CHECK_HOST_ERRNO( EBFONT, EBFONT );
+#endif
+#ifdef ENOSTR
+ CHECK_HOST_ERRNO( ENOSTR, ENOSTR );
+#endif
+#ifdef ENODATA
+ CHECK_HOST_ERRNO( ENODATA, ENODATA );
+#endif
+#ifdef ETIME
+ CHECK_HOST_ERRNO( ETIME, ETIME );
+#endif
+#ifdef ENOSR
+ CHECK_HOST_ERRNO( ENOSR, ENOSR );
+#endif
+#ifdef ENONET
+ CHECK_HOST_ERRNO( ENONET, ENONET );
+#endif
+#ifdef ENOPKG
+ CHECK_HOST_ERRNO( ENOPKG, ENOPKG );
+#endif
+#ifdef EREMOTE
+ CHECK_HOST_ERRNO( EREMOTE, EREMOTE );
+#endif
+#ifdef ENOLINK
+ CHECK_HOST_ERRNO( ENOLINK, ENOLINK );
+#endif
+#ifdef EADV
+ CHECK_HOST_ERRNO( EADV, EADV );
+#endif
+#ifdef ESRMNT
+ CHECK_HOST_ERRNO( ESRMNT, ESRMNT );
+#endif
+#ifdef ECOMM
+ CHECK_HOST_ERRNO( ECOMM, ECOMM );
+#endif
+#ifdef EPROTO
+ CHECK_HOST_ERRNO( EPROTO, EPROTO );
+#endif
+#ifdef EMULTIHOP
+ CHECK_HOST_ERRNO( EMULTIHOP, EMULTIHOP );
+#endif
+#ifdef EDOTDOT
+ CHECK_HOST_ERRNO( EDOTDOT, EDOTDOT );
+#endif
+#ifdef EBADMSG
+ CHECK_HOST_ERRNO( EBADMSG, EBADMSG );
+#endif
+#ifdef EOVERFLOW
+ CHECK_HOST_ERRNO( EOVERFLOW, EOVERFLOW );
+#endif
+#ifdef ENOTUNIQ
+ CHECK_HOST_ERRNO( ENOTUNIQ, ENOTUNIQ );
+#endif
+#ifdef EBADFD
+ CHECK_HOST_ERRNO( EBADFD, EBADFD );
+#endif
+#ifdef EREMCHG
+ CHECK_HOST_ERRNO( EREMCHG, EREMCHG );
+#endif
+#ifdef ELIBACC
+ CHECK_HOST_ERRNO( ELIBACC, ELIBACC );
+#endif
+#ifdef ELIBBAD
+ CHECK_HOST_ERRNO( ELIBBAD, ELIBBAD );
+#endif
+#ifdef ELIBSCN
+ CHECK_HOST_ERRNO( ELIBSCN, ELIBSCN );
+#endif
+#ifdef ELIBMAX
+ CHECK_HOST_ERRNO( ELIBMAX, ELIBMAX );
+#endif
+#ifdef ELIBEXEC
+ CHECK_HOST_ERRNO( ELIBEXEC, ELIBEXEC );
+#endif
+#ifdef EILSEQ
+ CHECK_HOST_ERRNO( EILSEQ, EILSEQ );
+#endif
+#ifdef ERESTART
+ CHECK_HOST_ERRNO( ERESTART, ERESTART );
+#endif
+#ifdef ESTRPIPE
+ CHECK_HOST_ERRNO( ESTRPIPE, ESTRPIPE );
+#endif
+#ifdef EUSERS
+ CHECK_HOST_ERRNO( EUSERS, EUSERS );
+#endif
+#ifdef ENOTSOCK
+ CHECK_HOST_ERRNO( ENOTSOCK, ENOTSOCK );
+#endif
+#ifdef EDESTADDRREQ
+ CHECK_HOST_ERRNO( EDESTADDRREQ, EDESTADDRREQ );
+#endif
+#ifdef EMSGSIZE
+ CHECK_HOST_ERRNO( EMSGSIZE, EMSGSIZE );
+#endif
+#ifdef EPROTOTYPE
+ CHECK_HOST_ERRNO( EPROTOTYPE, EPROTOTYPE );
+#endif
+#ifdef ENOPROTOOPT
+ CHECK_HOST_ERRNO( ENOPROTOOPT, ENOPROTOOPT );
+#endif
+#ifdef EPROTONOSUPPORT
+ CHECK_HOST_ERRNO( EPROTONOSUPPORT, EPROTONOSUPPORT );
+#endif
+#ifdef ESOCKTNOSUPPORT
+ CHECK_HOST_ERRNO( ESOCKTNOSUPPORT, ESOCKTNOSUPPORT );
+#endif
+#ifdef EOPNOTSUPP
+ CHECK_HOST_ERRNO( EOPNOTSUPP, EOPNOTSUPP );
+#endif
+#ifdef EPFNOSUPPORT
+ CHECK_HOST_ERRNO( EPFNOSUPPORT, EPFNOSUPPORT );
+#endif
+#ifdef EAFNOSUPPORT
+ CHECK_HOST_ERRNO( EAFNOSUPPORT, EAFNOSUPPORT );
+#endif
+#ifdef EADDRINUSE
+ CHECK_HOST_ERRNO( EADDRINUSE, EADDRINUSE );
+#endif
+#ifdef EADDRNOTAVAIL
+ CHECK_HOST_ERRNO( EADDRNOTAVAIL, EADDRNOTAVAIL );
+#endif
+#ifdef ENETDOWN
+ CHECK_HOST_ERRNO( ENETDOWN, ENETDOWN );
+#endif
+#ifdef ENETUNREACH
+ CHECK_HOST_ERRNO( ENETUNREACH, ENETUNREACH );
+#endif
+#ifdef ENETRESET
+ CHECK_HOST_ERRNO( ENETRESET, ENETRESET );
+#endif
+#ifdef ECONNABORTED
+ CHECK_HOST_ERRNO( ECONNABORTED, ECONNABORTED );
+#endif
+#ifdef ECONNRESET
+ CHECK_HOST_ERRNO( ECONNRESET, ECONNRESET );
+#endif
+#ifdef ENOBUFS
+ CHECK_HOST_ERRNO( ENOBUFS, ENOBUFS );
+#endif
+#ifdef EISCONN
+ CHECK_HOST_ERRNO( EISCONN, EISCONN );
+#endif
+#ifdef ENOTCONN
+ CHECK_HOST_ERRNO( ENOTCONN, ENOTCONN );
+#endif
+#ifdef ESHUTDOWN
+ CHECK_HOST_ERRNO( ESHUTDOWN, ESHUTDOWN );
+#endif
+#ifdef ETOOMANYREFS
+ CHECK_HOST_ERRNO( ETOOMANYREFS, ETOOMANYREFS );
+#endif
+#ifdef ETIMEDOUT
+ CHECK_HOST_ERRNO( ETIMEDOUT, ETIMEDOUT );
+#endif
+#ifdef ECONNREFUSED
+ CHECK_HOST_ERRNO( ECONNREFUSED, ECONNREFUSED );
+#endif
+#ifdef EHOSTDOWN
+ CHECK_HOST_ERRNO( EHOSTDOWN, EHOSTDOWN );
+#endif
+#ifdef EHOSTUNREACH
+ CHECK_HOST_ERRNO( EHOSTUNREACH, EHOSTUNREACH );
+#endif
+#ifdef EALREADY
+ CHECK_HOST_ERRNO( EALREADY, EALREADY );
+#endif
+#ifdef EINPROGRESS
+ CHECK_HOST_ERRNO( EINPROGRESS, EINPROGRESS );
+#endif
+#ifdef ESTALE
+ CHECK_HOST_ERRNO( ESTALE, ESTALE );
+#endif
+#ifdef EUCLEAN
+ CHECK_HOST_ERRNO( EUCLEAN, EUCLEAN );
+#endif
+#ifdef ENOTNAM
+ CHECK_HOST_ERRNO( ENOTNAM, ENOTNAM );
+#endif
+#ifdef ENAVAIL
+ CHECK_HOST_ERRNO( ENAVAIL, ENAVAIL );
+#endif
+#ifdef EISNAM
+ CHECK_HOST_ERRNO( EISNAM, EISNAM );
+#endif
+#ifdef EREMOTEIO
+ CHECK_HOST_ERRNO( EREMOTEIO, EREMOTEIO );
+#endif
+#ifdef EDQUOT
+ CHECK_HOST_ERRNO( EDQUOT, EDQUOT );
+#endif
+#ifdef ENOMEDIUM
+ CHECK_HOST_ERRNO( ENOMEDIUM, ENOMEDIUM );
+#endif
+#ifdef EMEDIUMTYPE
+ CHECK_HOST_ERRNO( EMEDIUMTYPE, EMEDIUMTYPE );
+#endif
+#ifdef ECANCELED
+ CHECK_HOST_ERRNO( ECANCELED, ECANCELED );
+#endif
+#ifdef ENOKEY
+ CHECK_HOST_ERRNO( ENOKEY, ENOKEY );
+#endif
+#ifdef EKEYEXPIRED
+ CHECK_HOST_ERRNO( EKEYEXPIRED, EKEYEXPIRED );
+#endif
+#ifdef EKEYREVOKED
+ CHECK_HOST_ERRNO( EKEYREVOKED, EKEYREVOKED );
+#endif
+#ifdef EKEYREJECTED
+ CHECK_HOST_ERRNO( EKEYREJECTED, EKEYREJECTED );
+#endif
+#ifdef EOWNERDEAD
+ CHECK_HOST_ERRNO( EOWNERDEAD, EOWNERDEAD );
+#endif
+#ifdef ENOTRECOVERABLE
+ CHECK_HOST_ERRNO( ENOTRECOVERABLE, ENOTRECOVERABLE );
+#endif
+#ifdef ERFKILL
+ CHECK_HOST_ERRNO( ERFKILL, ERFKILL );
+#endif
+#ifdef EHWPOISON
+ CHECK_HOST_ERRNO( EHWPOISON, EHWPOISON );
+#endif
+#ifdef EFTYPE
+ CHECK_HOST_ERRNO( EFTYPE, EFTYPE );
+#endif
+#ifdef EWOULDBLOCK
+ CHECK_HOST_ERRNO( EWOULDBLOCK, EWOULDBLOCK );
+#endif
+#ifdef EDEADLOCK
+ CHECK_HOST_ERRNO( EDEADLOCK, EDEADLOCK );
+#endif
+#ifdef ENOTSUP
+ CHECK_HOST_ERRNO( ENOTSUP, ENOTSUP );
+#endif
+
+ return( 0 );
+
+} /* End of main() */
diff --git a/tests/tst-frame-runner.sh b/tests/tst-frame-runner.sh
new file mode 100755
index 0000000..932f15e
--- /dev/null
+++ b/tests/tst-frame-runner.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${TEST_SRCDIR:?}"
+base="${TMPDIR:-/tmp}/libcpu-frame-$$"
+trap 'rm -f "$base.s" "$base.o" "$base.melf"' EXIT HUP INT TERM
+"$CPU_AS" -o "$base.o" "$TEST_SRCDIR/fixtures/frame-pointer.s" >/dev/null
+"$CPU_LD" --stack-size=512 -o "$base.melf" "$base.o" >/dev/null
+out=`"$CPU_RUN" --int-reg=r64@0x00008 "$base.melf"`
+test "x$out" = "x42"
diff --git a/tests/tst-frame.c b/tests/tst-frame.c
new file mode 100644
index 0000000..b94b51e
--- /dev/null
+++ b/tests/tst-frame.c
@@ -0,0 +1,178 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+
+static const char src[]=
+".text\n"
+" LDI r64@0x00000, 5\n"
+" ADJSP -16\n"
+" STSP 0, r64@0x00000\n"
+" CALL demo\n"
+" ADJSP 16\n"
+" MOV r64@0x00008, r64@0x7fff8\n"
+" HALT\n"
+"demo:\n"
+" PUSHFP\n"
+" SETFP\n"
+" ADJSP -16\n"
+" LDFP r64@0x00100, 16\n"
+" STFP -8, r64@0x00100\n"
+" LEAFP r64@0x00108, -8\n"
+" LEASP r64@0x00110, 0\n"
+" LDI r64@0x00118, 24\n"
+" PSUB r64@0x00120, r64@0x00110, r64@0x00118\n"
+" SETSP r64@0x00120\n"
+" LDI r64@0x00128, 37\n"
+" LEASP r64@0x00130, 0\n"
+" STOREP r64@0x00130, r64@0x00128\n"
+" LDFP r64@0x00138, -8\n"
+" LOADP r64@0x00140, r64@0x00130\n"
+" IADD r64@0x7fff8, r64@0x00138, r64@0x00140\n"
+" SETSP r64@0x00110\n"
+" RESTSP\n"
+" POPFP\n"
+" RET\n";
+
+static __mpu_uint64_t
+reg_u64( unsigned address )
+{
+ __mpu_uint64_t v = 0;
+ memcpy( &v, __cpu_reg_ptr( 64, address ), sizeof( v ) );
+ return( v );
+}
+
+static __mpu_int64_t
+reg_i64( unsigned address )
+{
+ __mpu_int64_t v = 0;
+ memcpy( &v, __cpu_reg_ptr( 64, address ), sizeof( v ) );
+ return( v );
+}
+
+static void
+set_reg_u64( unsigned address, __mpu_uint64_t v )
+{
+ memcpy( __cpu_reg_ptr( 64, address ), &v, sizeof( v ) );
+}
+
+static void
+regop( cpu_operand *o, unsigned address )
+{
+ memset( o, 0, sizeof( *o ) );
+ o->type = CPU_OPERAND_REGISTER;
+ o->value.reg.bits = 64;
+ o->value.reg.address = address;
+}
+
+int
+main( void )
+{
+ cpu_asm_image im;
+ cpu_instruction i;
+ __mpu_uint64_t ptr, saved_sp, saved_fp, badfp;
+ __mpu_uint32_t psw;
+ char err[256];
+
+ __cpu_init();
+ CHECK( __cpu_stack_alloc( 256 ) == 0, "stack alloc" );
+ CHECK( __cpu_sp == 256, "empty stack SP" );
+ CHECK( __cpu_fp == 256, "empty stack FP" );
+
+ CHECK( cpu_assemble_text( src, &im, err, sizeof( err ) ) == 0, err );
+ CHECK( __cpu_program_set( im.code, im.code_size ) == 0, "program set" );
+ __cpu_pc = 0;
+ __cpu_set_psw( PSW_CF_MASK | PSW_IF_MASK );
+ psw = __cpu_get_psw();
+ CHECK( __cpu_run() == CPU_EXEC_SUCCESS, "frame/dynamic stack program run" );
+ CHECK( reg_i64( 0x00008 ) == 42, "frame example result" );
+ CHECK( reg_i64( 0x7fff8 ) == 42, "canonical return value" );
+ CHECK( __cpu_sp == 256, "caller cleanup restored SP" );
+ CHECK( __cpu_fp == 256, "callee restored caller FP" );
+ cpu_asm_image_free( &im );
+
+ /* LEAFP is based on FP, not on the moving SP. */
+ __cpu_sp = 160;
+ __cpu_fp = 200;
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_LEAFP;
+ regop( &i.operand[0], 0x00200 );
+ i.operand[1].type = CPU_OPERAND_OFF64;
+ i.operand[1].value.off64 = -24;
+ __cpu_set_psw( PSW_CF_MASK | PSW_IF_MASK );
+ psw = __cpu_get_psw();
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "LEAFP direct execution" );
+ CHECK( __cpu_get_psw() == psw, "LEAFP preserves PSW" );
+ ptr = reg_u64( 0x00200 );
+ CHECK( __cpu_ptr_segment( ptr ) == CPU_PTR_SEG_STACK, "LEAFP STACK segment" );
+ CHECK( __cpu_ptr_offset( ptr ) == 176, "LEAFP uses FP base" );
+ CHECK( __cpu_sp == 160, "LEAFP leaves SP unchanged" );
+ CHECK( __cpu_fp == 200, "LEAFP leaves FP unchanged" );
+
+ /* SETSP accepts only a STACK PTR64 and is atomic on faults. */
+ CHECK( __cpu_ptr_make( &ptr, CPU_PTR_SEG_STACK, 144 ) == 0, "make SETSP STACK ptr" );
+ set_reg_u64( 0x00208, ptr );
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_SETSP;
+ regop( &i.operand[0], 0x00208 );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "SETSP valid STACK ptr" );
+ CHECK( __cpu_sp == 144, "SETSP installs offset" );
+ CHECK( __cpu_fp == 200, "SETSP preserves FP" );
+
+ saved_sp = __cpu_sp;
+ CHECK( __cpu_ptr_make( &ptr, CPU_PTR_SEG_DATA, 10 ) == 0, "make DATA ptr" );
+ set_reg_u64( 0x00208, ptr );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_POINTER_FAULT, "SETSP rejects DATA ptr" );
+ CHECK( __cpu_sp == saved_sp, "SETSP pointer fault atomic" );
+
+ CHECK( __cpu_ptr_make( &ptr, CPU_PTR_SEG_STACK, 300 ) == 0, "make out-of-range STACK ptr" );
+ set_reg_u64( 0x00208, ptr );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_STACK_FAULT, "SETSP checks allocated stack size" );
+ CHECK( __cpu_sp == saved_sp, "SETSP stack fault atomic" );
+
+ /* PUSHFP/POPFP preserve exact previous FP and validate a restored FP. */
+ __cpu_sp = 128;
+ __cpu_fp = 216;
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_PUSHFP;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "PUSHFP" );
+ CHECK( __cpu_sp == 120, "PUSHFP moves SP by eight bytes" );
+ memcpy( &saved_fp, __cpu_stack_ptr() + 120, 8 );
+ CHECK( saved_fp == 216, "PUSHFP stored FP" );
+
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_SETFP;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "SETFP" );
+ CHECK( __cpu_fp == 120, "SETFP copies SP" );
+
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_RESTSP;
+ __cpu_sp = 80;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "RESTSP" );
+ CHECK( __cpu_sp == 120, "RESTSP copies FP" );
+
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_POPFP;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "POPFP" );
+ CHECK( __cpu_fp == 216, "POPFP restores caller FP" );
+ CHECK( __cpu_sp == 128, "POPFP advances SP by eight bytes" );
+ CHECK( __cpu_get_psw() == psw, "FP/SP management preserves PSW" );
+
+ /* Invalid saved FP must not partially change FP or SP. */
+ __cpu_sp = 64;
+ __cpu_fp = 100;
+ badfp = 257;
+ memcpy( __cpu_stack_ptr() + 64, &badfp, 8 );
+ saved_sp = __cpu_sp;
+ saved_fp = __cpu_fp;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_STACK_FAULT, "POPFP rejects invalid saved FP" );
+ CHECK( __cpu_sp == saved_sp, "POPFP fault preserves SP" );
+ CHECK( __cpu_fp == saved_fp, "POPFP fault preserves FP" );
+
+ __cpu_free_context();
+ CHECK( __cpu_fp == 0, "FP cleared by free context" );
+ puts( "libcpu frame-pointer/dynamic STACK regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-hll-crt-runner.sh b/tests/tst-hll-crt-runner.sh
new file mode 100755
index 0000000..ca03641
--- /dev/null
+++ b/tests/tst-hll-crt-runner.sh
@@ -0,0 +1,338 @@
+#!/bin/sh
+
+set -eu
+
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${CPU_READELF:?}"
+: "${MCPU_TEST_ROOT:?}" "${MCPU_HLL_FIXTURE_SRCDIR:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-hll-crt-$$
+mkdir -p "$TMP"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+
+as()
+{
+ "$CPU_AS" -o "$2" "$1" >/dev/null
+}
+
+must_fail_link()
+{
+ if "$CPU_LD" "$@" >/dev/null 2>&1; then
+ echo "mcpu-ld unexpectedly accepted invalid HLL input: $*" >&2
+ exit 1
+ fi
+}
+
+# ---------------------------------------------------------------------------
+# Regression fixtures for compiler-normalized HLL objects and libc hooks.
+# ---------------------------------------------------------------------------
+as "$MCPU_HLL_FIXTURE_SRCDIR/program-main.S" "$TMP/program.o"
+as "$MCPU_HLL_FIXTURE_SRCDIR/model-pendulum-life.S" "$TMP/model.o"
+as "$MCPU_HLL_FIXTURE_SRCDIR/libc-stub.S" "$TMP/libc-stub.o"
+
+# The future compiler output owns HLL functions, never MELF lifecycle symbols.
+"$CPU_READELF" -s "$TMP/program.o" > "$TMP/program.syms"
+grep -q ' main$' "$TMP/program.syms"
+grep -q ' fini$' "$TMP/program.syms"
+if grep -Eq ' (_start|_life|_fini)$' "$TMP/program.syms"; then
+ echo "PROGRAM compiler-output emulator defines lifecycle symbols" >&2
+ exit 1
+fi
+
+"$CPU_READELF" -s "$TMP/model.o" > "$TMP/model.syms"
+grep -q ' init$' "$TMP/model.syms"
+grep -q ' life$' "$TMP/model.syms"
+grep -q ' fini$' "$TMP/model.syms"
+if grep -Eq ' (_start|_life|_fini)$' "$TMP/model.syms"; then
+ echo "MODEL compiler-output emulator defines lifecycle symbols" >&2
+ exit 1
+fi
+
+# ---------------------------------------------------------------------------
+# PROGRAM: automatic classification, CRT insertion, argc/argv bridge and fini.
+# ---------------------------------------------------------------------------
+"$CPU_LD" --sys-root="$MCPU_TEST_ROOT" --stack-size=4K --no-heap \
+ -o "$TMP/program" "$TMP/program.o" "$TMP/libc-stub.o" >/dev/null
+
+"$CPU_READELF" -h "$TMP/program" > "$TMP/program.hdr"
+entry=`sed -n 's/^ Entry point address:[[:space:]]*//p' "$TMP/program.hdr"`
+init=`sed -n 's/^ INIT entry address:[[:space:]]*//p' "$TMP/program.hdr"`
+life=`sed -n 's/^ LIFE entry address:[[:space:]]*//p' "$TMP/program.hdr"`
+fini=`sed -n 's/^ FINI entry address:[[:space:]]*//p' "$TMP/program.hdr"`
+test "x$entry" = "x$life"
+test "x$init" = "x0x0000000000000000"
+test "x$fini" != "x0x0000000000000000"
+
+set +e
+out=`"$CPU_RUN" --after-run=print-int-regs:r64@0x70000 \
+ -- "$TMP/program" alpha beta`
+status=$?
+set -e
+test "$status" -eq 42
+test "x$out" = "x6"
+
+# ABI-v3 PROGRAM uses the same HLL interface with the runtime hooks supplied
+# by a normal shared MCPU Libc dependency. The four hook names must therefore
+# be exported from ET_DYN, while explicit preloads still may not interpose them.
+"$CPU_LD" --shared --soname=libhllc.so.1 \
+ -o "$TMP/libhllc.so.1" "$TMP/libc-stub.o" >/dev/null
+ln -s libhllc.so.1 "$TMP/libhllc.so"
+"$CPU_READELF" -s "$TMP/libhllc.so.1" > "$TMP/libhllc.syms"
+grep -q ' __mcpu_libc_init$' "$TMP/libhllc.syms"
+grep -q ' __mcpu_init_array$' "$TMP/libhllc.syms"
+grep -q ' __mcpu_fini_array$' "$TMP/libhllc.syms"
+grep -q ' __mcpu_libc_fini$' "$TMP/libhllc.syms"
+
+"$CPU_LD" --dynamic --sys-root="$MCPU_TEST_ROOT" \
+ --stack-size=4K --no-heap -L"$TMP" -lhllc \
+ -o "$TMP/program-dyn" "$TMP/program.o" >/dev/null
+set +e
+out=`MCPU_LD_PRELOAD= "$CPU_RUN" --library-path="$TMP" \
+ --after-run=print-int-regs:r64@0x70000 \
+ -- "$TMP/program-dyn" alpha beta`
+status=$?
+set -e
+test "$status" -eq 42
+test "x$out" = "x6"
+
+# HLL complete-program functions remain bound to the main executable. Runtime
+# hooks come from the normal libc dependency. A preload may export all of the
+# same names but cannot interpose either class of protected HLL symbol.
+cat > "$TMP/override.s" <<'EOS'
+.global main
+.global fini
+.global __mcpu_libc_init
+.global __mcpu_init_array
+.global __mcpu_fini_array
+.global __mcpu_libc_fini
+.text
+main:
+ LDI r64@0x70000, 700
+ LDI r32@0x7fffc, 70
+ RET
+fini:
+ LDI r64@0x70000, 701
+ RET
+__mcpu_libc_init:
+ LDI r64@0x70000, 702
+ RET
+__mcpu_init_array:
+ LDI r64@0x70000, 703
+ RET
+__mcpu_fini_array:
+ LDI r64@0x70000, 704
+ RET
+__mcpu_libc_fini:
+ LDI r64@0x70000, 705
+ RET
+EOS
+as "$TMP/override.s" "$TMP/override.o"
+"$CPU_LD" --shared --soname=libhll-override.so.1 \
+ -o "$TMP/libhll-override.so" "$TMP/override.o" >/dev/null
+set +e
+out=`MCPU_LD_PRELOAD= "$CPU_RUN" --library-path="$TMP" \
+ --ldpreload="$TMP/libhll-override.so" \
+ --after-run=print-int-regs:r64@0x70000 \
+ -- "$TMP/program-dyn" alpha beta`
+status=$?
+set -e
+test "$status" -eq 42
+test "x$out" = "x6"
+
+# ---------------------------------------------------------------------------
+# MODEL: compact pendulum-shaped compiler-output regression fixture.
+# ---------------------------------------------------------------------------
+"$CPU_LD" --sys-root="$MCPU_TEST_ROOT" --stack-size=4K --no-heap \
+ -o "$TMP/model" "$TMP/model.o" "$TMP/libc-stub.o" >/dev/null
+
+"$CPU_READELF" -h "$TMP/model" > "$TMP/model.hdr"
+entry=`sed -n 's/^ Entry point address:[[:space:]]*//p' "$TMP/model.hdr"`
+init=`sed -n 's/^ INIT entry address:[[:space:]]*//p' "$TMP/model.hdr"`
+life=`sed -n 's/^ LIFE entry address:[[:space:]]*//p' "$TMP/model.hdr"`
+fini=`sed -n 's/^ FINI entry address:[[:space:]]*//p' "$TMP/model.hdr"`
+test "x$entry" = "x$init"
+test "x$life" != "x$entry"
+test "x$fini" != "x0x0000000000000000"
+
+# Natural MODEL run: three LIFE_CONTINUE boundaries, then LIFE_END.
+set +e
+out=`"$CPU_RUN" --steps=10 \
+ --on-step=print-int-trace:64@0 \
+ --after-run=print-int-regs:r64@0x70000,r64@0x70010 \
+ -- "$TMP/model"`
+status=$?
+set -e
+test "$status" -eq 0
+expected='1
+2
+3
+6 1'
+test "x$out" = "x$expected"
+
+# init() failure is already RUNNING: normal HLL finalization is mandatory.
+set +e
+out=`"$CPU_RUN" --before-run=set-int-control:64@0=-1 \
+ --after-run=print-int-regs:r64@0x70000,r64@0x70010 \
+ -- "$TMP/model"`
+status=$?
+set -e
+test "$status" -eq 17
+test "x$out" = "x6 1"
+
+# Positive life() status is a MODEL error and performs normal finalization.
+set +e
+out=`"$CPU_RUN" --steps=10 \
+ --before-run=set-int-control:64@8=2 \
+ --on-step=print-int-trace:64@0 \
+ --after-run=print-int-regs:r64@0x70000,r64@0x70010 \
+ -- "$TMP/model"`
+status=$?
+set -e
+test "$status" -eq 9
+expected='1
+6 1'
+test "x$out" = "x$expected"
+
+# Diagnostic --steps stop does not synthesize FINI.
+out=`"$CPU_RUN" --steps=1 \
+ --on-step=print-int-trace:64@0 \
+ --after-run=print-int-regs:r64@0x70000,r64@0x70010 \
+ -- "$TMP/model"`
+expected='1
+3 0'
+test "x$out" = "x$expected"
+
+# ---------------------------------------------------------------------------
+# PRE_RUNTIME failure: exercise the real common CRT startup/finalization code.
+# A test-only PROGRAM adapter corrupts McpuStartup.version before entering the
+# real __mcpu_crt_startup. The normal user object and libc stub are unchanged.
+# ---------------------------------------------------------------------------
+mkdir -p "$TMP/bad-root/lib/mcpu"
+cp "$MCPU_TEST_ROOT/lib/mcpu/crti.o" "$TMP/bad-root/lib/mcpu/"
+cp "$MCPU_TEST_ROOT/lib/mcpu/crtn.o" "$TMP/bad-root/lib/mcpu/"
+cp "$MCPU_TEST_ROOT/lib/mcpu/crt1.o" "$TMP/bad-root/lib/mcpu/"
+cat > "$TMP/bad-crt0.s" <<'EOS'
+.global __mcpu_crt_startup
+.global main
+.text
+__mcpu_crt_hll_program:
+_start_life:
+ LDI r32@0x7fe00, 99
+ STSP 0, r32@0x7fe00
+ CALL __mcpu_crt_startup
+ LDI r32@0x7fe08, 88
+ EXIT r32@0x7fe08
+EOS
+as "$TMP/bad-crt0.s" "$TMP/bad-root/lib/mcpu/crt0.o"
+"$CPU_LD" --sys-root="$TMP/bad-root" --stack-size=4K --no-heap \
+ -o "$TMP/bad-startup" "$TMP/program.o" "$TMP/libc-stub.o" >/dev/null
+set +e
+out=`"$CPU_RUN" --after-run=print-int-regs:r64@0x70000 \
+ -- "$TMP/bad-startup" alpha beta`
+status=$?
+set -e
+test "$status" -ne 0
+test "x$out" = "x0"
+
+# ---------------------------------------------------------------------------
+# HLL complete-program classification errors.
+# ---------------------------------------------------------------------------
+cat > "$TMP/main-only.s" <<'EOS'
+.global main
+.text
+main:
+ CLR r32@0x7fffc
+ RET
+EOS
+as "$TMP/main-only.s" "$TMP/main-only.o"
+must_fail_link --sys-root="$MCPU_TEST_ROOT" --stack-size=4K --no-heap \
+ -o "$TMP/bad-main-only" "$TMP/main-only.o" "$TMP/libc-stub.o"
+
+cat > "$TMP/model-init.s" <<'EOS'
+.global init
+.global fini
+.text
+init:
+ CLR r32@0x7fffc
+ RET
+fini:
+ RET
+EOS
+cat > "$TMP/model-life.s" <<'EOS'
+.global life
+.text
+life:
+ CLR r32@0x7fffc
+ RET
+EOS
+as "$TMP/model-init.s" "$TMP/model-init.o"
+as "$TMP/model-life.s" "$TMP/model-life.o"
+must_fail_link --sys-root="$MCPU_TEST_ROOT" --stack-size=4K --no-heap \
+ -o "$TMP/bad-split-model" "$TMP/model-init.o" "$TMP/model-life.o" \
+ "$TMP/libc-stub.o"
+
+cat > "$TMP/mixed.s" <<'EOS'
+.global main
+.global fini
+.text
+_start_life:
+main:
+ CLR r32@0x7fffc
+ RET
+fini:
+ RET
+EOS
+as "$TMP/mixed.s" "$TMP/mixed.o"
+must_fail_link --sys-root="$MCPU_TEST_ROOT" --stack-size=4K --no-heap \
+ -o "$TMP/bad-mixed" "$TMP/mixed.o" "$TMP/libc-stub.o"
+
+cat > "$TMP/reserved.s" <<'EOS'
+.global __mcpu_crt_user_collision
+.text
+__mcpu_crt_user_collision:
+ RET
+EOS
+as "$TMP/reserved.s" "$TMP/reserved.o"
+must_fail_link --sys-root="$MCPU_TEST_ROOT" --stack-size=4K --no-heap \
+ -o "$TMP/bad-reserved" "$TMP/reserved.o"
+
+# ---------------------------------------------------------------------------
+# Frozen ET_DYN ownership rule: model/lifecycle sections belong to ET_EXEC.
+# ---------------------------------------------------------------------------
+for sec in init fini control input output trace; do
+ case "$sec" in
+ init|fini)
+ cat > "$TMP/shared-$sec.s" <<EOS
+.$sec
+shared_${sec}_object:
+ RET
+EOS
+ ;;
+ *)
+ cat > "$TMP/shared-$sec.s" <<EOS
+.$sec
+shared_${sec}_object:
+ .integer 64, 0
+EOS
+ ;;
+ esac
+ as "$TMP/shared-$sec.s" "$TMP/shared-$sec.o"
+ if "$CPU_LD" --shared --soname="libbad-$sec.so.1" \
+ -o "$TMP/libbad-$sec.so" "$TMP/shared-$sec.o" >/dev/null 2>&1; then
+ echo "ET_DYN unexpectedly accepted .$sec" >&2
+ exit 1
+ fi
+done
+
+cat > "$TMP/shared-lifecycle.s" <<'EOS'
+.text
+_start_life:
+ END
+EOS
+as "$TMP/shared-lifecycle.s" "$TMP/shared-lifecycle.o"
+if "$CPU_LD" --shared --soname=libbad-lifecycle.so.1 \
+ -o "$TMP/libbad-lifecycle.so" "$TMP/shared-lifecycle.o" >/dev/null 2>&1; then
+ echo "ET_DYN unexpectedly accepted lifecycle symbols" >&2
+ exit 1
+fi
+
+exit 0
diff --git a/tests/tst-immediate-exactness-runner.sh b/tests/tst-immediate-exactness-runner.sh
new file mode 100755
index 0000000..3fc3a42
--- /dev/null
+++ b/tests/tst-immediate-exactness-runner.sh
@@ -0,0 +1,65 @@
+#!/bin/sh
+set -eu
+: "${CPU_AS:?}"
+TMP="${TMPDIR:-/tmp}/libcpu-imm-$$"
+trap 'rm -f "$TMP".s "$TMP".o "$TMP".err "$TMP".out' EXIT HUP INT TERM
+cat > "$TMP.s" <<'EOS'
+.text
+LDR r4096@0x00000, 3.14243987129849274029174912694696912849214e-286
+HALT
+EOS
+if "$CPU_AS" -o "$TMP.o" "$TMP.s" >"$TMP.out" 2>"$TMP.err"; then
+ echo "cpu-as unexpectedly accepted inexact wide LDR" >&2
+ exit 1
+fi
+grep -F "$TMP.s:2: error:" "$TMP.err" >/dev/null
+grep -F 'LDR immediate is not exactly representable as real64;' "$TMP.err" >/dev/null
+grep -F 'use .real <bits>, value in DATA and LOAD' "$TMP.err" >/dev/null
+
+cat > "$TMP.s" <<'EOS'
+.text
+LDR r32@0x00000, 0.1
+HALT
+EOS
+if "$CPU_AS" -o "$TMP.o" "$TMP.s" >"$TMP.out" 2>"$TMP.err"; then
+ echo "cpu-as unexpectedly accepted inexact real32 LDR" >&2
+ exit 1
+fi
+grep -F 'LDR immediate is not exactly representable as real32;' "$TMP.err" >/dev/null
+
+cat > "$TMP.s" <<'EOS'
+.text
+LDC c128@0x00000, 0.1r0+32.0j0
+HALT
+EOS
+if "$CPU_AS" -o "$TMP.o" "$TMP.s" >"$TMP.out" 2>"$TMP.err"; then
+ echo "cpu-as unexpectedly accepted inexact complex immediate" >&2
+ exit 1
+fi
+grep -F 'LDC immediate component is not exactly representable as real32;' "$TMP.err" >/dev/null
+grep -F 'use .complex <bits>, real, imag in DATA and LOAD' "$TMP.err" >/dev/null
+
+cat > "$TMP.s" <<'EOS'
+.text
+LDI r8@0x00000, 255
+HALT
+EOS
+if "$CPU_AS" -o "$TMP.o" "$TMP.s" >"$TMP.out" 2>"$TMP.err"; then
+ echo "cpu-as unexpectedly accepted lossy narrow integer immediate" >&2
+ exit 1
+fi
+grep -F 'LDI immediate is not exactly representable as integer8;' "$TMP.err" >/dev/null
+grep -F 'use .integer <bits>, value in DATA and LOAD' "$TMP.err" >/dev/null
+
+cat > "$TMP.s" <<'EOS'
+.text
+LDI r128@0x00000, 9223372036854775808
+HALT
+EOS
+if "$CPU_AS" -o "$TMP.o" "$TMP.s" >"$TMP.out" 2>"$TMP.err"; then
+ echo "cpu-as unexpectedly accepted integer wider than IMM64" >&2
+ exit 1
+fi
+grep -F 'LDI immediate is not exactly representable as integer64;' "$TMP.err" >/dev/null
+
+echo 'libcpu immediate exactness runner regression: PASS'
diff --git a/tests/tst-immediate-exactness.c b/tests/tst-immediate-exactness.c
new file mode 100644
index 0000000..ed054e7
--- /dev/null
+++ b/tests/tst-immediate-exactness.c
@@ -0,0 +1,100 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+
+#define CHECK(c,m) do { if(!(c)) { fprintf(stderr,"FAIL: %s\n",m); return 1; } } while(0)
+
+static int
+rejected( const char *src, const char *needle )
+{
+ cpu_asm_image im;
+ char err[512];
+ int rc = cpu_assemble_text( src, &im, err, sizeof( err ) );
+ if( rc == 0 )
+ {
+ cpu_asm_image_free( &im );
+ return( 0 );
+ }
+ return( strstr( err, needle ) != 0 );
+}
+
+int
+main( void )
+{
+ cpu_asm_image im;
+ char err[512];
+ static const char *prec = "3.14243987129849274029174912694696912849214e-286";
+ char src[4096];
+
+ __cpu_init();
+
+ CHECK( cpu_assemble_text( ".text\nLDR r32@0x00000, -17.0\nHALT\n", &im, err, sizeof( err ) ) == 0,
+ "exact real32 LDR accepted");
+ cpu_asm_image_free( &im );
+
+ CHECK( cpu_assemble_text( ".text\nLDR r4096@0x00000, 2.0\nHALT\n", &im, err, sizeof( err ) ) == 0,
+ "exact real64 immediate widened to real4096 accepted");
+ cpu_asm_image_free( &im );
+
+ CHECK( rejected( ".text\nLDR r32@0x00000, 0.1\nHALT\n",
+ "LDR immediate is not exactly representable as real32;"),
+ "inexact real32 immediate rejected");
+
+ CHECK( rejected( ".text\nLDR r64@0x00000, 0.1\nHALT\n",
+ "LDR immediate is not exactly representable as real64;"),
+ "inexact real64 immediate rejected");
+
+ snprintf( src, sizeof( src ), ".text\nLDR r64@0x00000, %s\nHALT\n", prec );
+ CHECK( rejected( src, "LDR immediate is not exactly representable as real64;" ),
+ "inexact real64 immediate rejected");
+
+ snprintf( src, sizeof( src ), ".text\nLDR r4096@0x00000, %s\nHALT\n", prec );
+ CHECK( rejected( src, "LDR immediate is not exactly representable as real64;" ),
+ "wide LDR cannot hide binary64 precision loss");
+
+ CHECK( cpu_assemble_text( ".text\nLDC c64@0x00000, -17.0r0+32.0j0\nHALT\n", &im, err, sizeof( err ) ) == 0,
+ "exact complex64 immediate accepted");
+ cpu_asm_image_free( &im );
+
+ CHECK( rejected( ".text\nLDC c128@0x00000, 0.1r0+32.0j0\nHALT\n",
+ "LDC immediate component is not exactly representable as real32;"),
+ "inexact complex immediate component rejected");
+
+ CHECK( rejected( ".text\nLDC c32@0x00000, 1.0r0+2.0j0\nHALT\n",
+ "bad LDC destination"),
+ "complex destination below 64 bits rejected");
+
+ CHECK( rejected( ".text\nLDI r128@0x00000, 9223372036854775808\nHALT\n",
+ "LDI immediate is not exactly representable as integer64;"),
+ "integer wider than signed IMM64 rejected");
+
+ CHECK( rejected( ".text\nLDI r8@0x00000, 255\nHALT\n",
+ "LDI immediate is not exactly representable as integer8;"),
+ "narrow integer immediate that changes value rejected");
+ CHECK( cpu_assemble_text( ".text\nLDI r8@0x00000, -1\nHALT\n", &im, err, sizeof( err ) ) == 0,
+ "exact signed integer8 immediate accepted");
+ cpu_asm_image_free( &im );
+
+ snprintf( src, sizeof( src ),
+ ".data\n"
+ ".align 16\n"
+ "wide_real:\n"
+ " .real 4096, %s\n"
+ "wide_int:\n"
+ " .integer 128, 9223372036854775808\n"
+ "wide_complex:\n"
+ " .complex 4096, %s, -%s\n"
+ ".text\n"
+ " LOAD r4096@0x00000, wide_real\n"
+ " LOAD r128@0x00200, wide_int\n"
+ " LOAD c4096@0x00400, wide_complex\n"
+ " HALT\n", prec, prec, prec);
+ CHECK( cpu_assemble_text( src, &im, err, sizeof( err ) ) == 0,
+ "DATA fallback with .real/.integer/.complex and LOAD accepted");
+ cpu_asm_image_free( &im );
+
+ __cpu_free_context();
+ puts( "libcpu immediate exactness regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-indirect-control.c b/tests/tst-indirect-control.c
new file mode 100644
index 0000000..acb0a7b
--- /dev/null
+++ b/tests/tst-indirect-control.c
@@ -0,0 +1,184 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+
+static const char decode_src[]=
+".text\n"
+" JMPP r64@0x00000\n"
+" CALLP r64@0x00008\n"
+" HALT\n";
+
+static const char run_src[]=
+".data\n"
+".align 8\n"
+"table:\n"
+" .cptr leaf\n"
+" .cptr done\n"
+".text\n"
+" LEA r64@0x00000, table\n"
+" LOADP r64@0x00008, r64@0x00000\n"
+" CALLP r64@0x00008\n"
+" PADDI r64@0x00000, r64@0x00000, 8\n"
+" LOADP r64@0x00010, r64@0x00000\n"
+" JMPP r64@0x00010\n"
+" LDI r64@0x00020, 99\n"
+" HALT\n"
+"leaf:\n"
+" LDI r64@0x00020, 42\n"
+" RET\n"
+"done:\n"
+" HALT\n";
+
+static void
+put_u64_reg( unsigned addr, __mpu_uint64_t v )
+{
+ __cpu_data_put_u64( __cpu_reg_ptr( 64, addr ), v );
+}
+
+int
+main( void )
+{
+ cpu_asm_image decim, im, bad;
+ cpu_instruction insn, execi;
+ cpu_register r64 =
+ {
+ 64, 0x00100
+ }
+ , r32 =
+ {
+ 32, 0x00100
+ };
+ __mpu_uint64_t p0, p1, pvalid, pbad, ret, saved_sp, saved_pc;
+ __mpu_uint32_t psw;
+ mpu_int64_t forty_two;
+ unsigned char before[16], after[16];
+ char err[256];
+ int rc;
+
+ __cpu_init();
+
+ CHECK( cpu_assemble_text( decode_src, &decim, err, sizeof( err ) ) == 0, err );
+ CHECK( decim.code_size == 12, "JMPP/CALLP bytecode lengths" );
+ CHECK( __cpu_program_set( decim.code, decim.code_size ) == 0, "decode program set" );
+ __cpu_pc = 0;
+ CHECK( __cpu_decode( &insn ) == CPU_EXEC_SUCCESS, "decode JMPP" );
+ CHECK( insn.opcode == CPU_OP_JMPP, "JMPP opcode" );
+ CHECK( insn.length == CPU_OPCODE_SIZE + CPU_REG24_SIZE, "JMPP length 5" );
+ CHECK( insn.operand[0].type == CPU_OPERAND_REGISTER, "JMPP register operand" );
+ CHECK( insn.operand[0].value.reg.bits == 64, "JMPP r64 width" );
+ __cpu_pc = 5;
+ CHECK( __cpu_decode( &insn ) == CPU_EXEC_SUCCESS, "decode CALLP" );
+ CHECK( insn.opcode == CPU_OP_CALLP, "CALLP opcode" );
+ CHECK( insn.length == CPU_OPCODE_SIZE + CPU_REG24_SIZE, "CALLP length 5" );
+ cpu_asm_image_free( &decim );
+
+ __cpu_program_free();
+ CHECK( cpu_assemble_text( run_src, &im, err, sizeof( err ) ) == 0, err );
+ CHECK( im.data_size == CPU_DATA_NULL_GUARD_SIZE + 16, "two .cptr entries in DATA" );
+ p0 = __cpu_data_get_u64( im.data + CPU_DATA_NULL_GUARD_SIZE );
+ p1 = __cpu_data_get_u64( im.data + CPU_DATA_NULL_GUARD_SIZE + 8 );
+ CHECK( __cpu_ptr_segment( p0 ) == CPU_PTR_SEG_PROGRAM, "first .cptr PROGRAM segment" );
+ CHECK( __cpu_ptr_segment( p1 ) == CPU_PTR_SEG_PROGRAM, "second .cptr PROGRAM segment" );
+ CHECK( __cpu_ptr_offset( p0 ) < im.code_size, "first .cptr target in PROGRAM" );
+ CHECK( __cpu_ptr_offset( p1 ) < im.code_size, "second .cptr target in PROGRAM" );
+ CHECK( p0 != p1, ".cptr labels resolve to distinct targets" );
+
+ CHECK( __cpu_program_set( im.code, im.code_size ) == 0, "indirect program set" );
+ CHECK( __cpu_data_set( im.data, im.data_size ) == 0, "indirect data set" );
+ CHECK( __cpu_stack_alloc( 512 ) == 0, "indirect stack alloc" );
+ __cpu_pc = 0;
+ CHECK( __cpu_run() == CPU_EXEC_SUCCESS, "indirect CALLP/JMPP run" );
+ iatoi( ( mpu_int * ) forty_two, "42", NB_I64 );
+ CHECK( !memcmp( __cpu_reg_ptr( 64, 0x00020 ), forty_two, NB_I64 ), "CALLP returned and JMPP reached done" );
+ CHECK( __cpu_sp == 512, "CALLP/RET restored SP" );
+
+ /* Direct executor tests: JMPP must only install a validated PROGRAM PTR64. */
+ memset( &execi, 0, sizeof( execi ) );
+ execi.opcode = CPU_OP_JMPP;
+ execi.operand[0].type = CPU_OPERAND_REGISTER;
+ execi.operand[0].value.reg = r64;
+ pvalid = p0;
+ put_u64_reg( 0x00100, pvalid );
+ __cpu_pc = 7;
+ __cpu_sp = 512;
+ __cpu_set_psw( PSW_CF_MASK | PSW_TF_MASK );
+ psw = __cpu_get_psw();
+ CHECK( __cpu_exec( &execi ) == CPU_EXEC_BRANCH, "JMPP executor success" );
+ CHECK( __cpu_pc == __cpu_ptr_offset( pvalid ), "JMPP installed target PC" );
+ CHECK( __cpu_sp == 512, "JMPP does not change SP" );
+ CHECK( __cpu_get_psw() == psw, "JMPP preserves PSW" );
+
+ CHECK( __cpu_ptr_make( &pbad, CPU_PTR_SEG_DATA, CPU_DATA_NULL_GUARD_SIZE ) == 0, "make DATA pointer" );
+ put_u64_reg( 0x00100, pbad );
+ __cpu_pc = 9;
+ saved_pc = __cpu_pc;
+ saved_sp = __cpu_sp;
+ CHECK( __cpu_exec( &execi ) == CPU_EXEC_POINTER_FAULT, "JMPP rejects non-PROGRAM PTR64" );
+ CHECK( __cpu_pc == saved_pc && __cpu_sp == saved_sp, "JMPP pointer fault is atomic" );
+
+ CHECK( __cpu_ptr_make( &pbad, CPU_PTR_SEG_PROGRAM, im.code_size ) == 0, "make out-of-range PROGRAM pointer" );
+ put_u64_reg( 0x00100, pbad );
+ __cpu_pc = 11;
+ saved_pc = __cpu_pc;
+ CHECK( __cpu_exec( &execi ) == CPU_EXEC_POINTER_FAULT, "JMPP rejects PROGRAM end pointer" );
+ CHECK( __cpu_pc == saved_pc, "JMPP range fault preserves PC" );
+
+ put_u64_reg( 0x00100, CPU_NULL_PTR );
+ CHECK( __cpu_exec( &execi ) == CPU_EXEC_POINTER_FAULT, "JMPP rejects NULL PTR64" );
+
+ execi.operand[0].value.reg = r32;
+ CHECK( __cpu_exec( &execi ) == CPU_EXEC_WIDTH_FAULT, "JMPP requires physical r64" );
+
+ /* CALLP validates pointer, return PC and stack before architectural mutation. */
+ memset( &execi, 0, sizeof( execi ) );
+ execi.opcode = CPU_OP_CALLP;
+ execi.operand[0].type = CPU_OPERAND_REGISTER;
+ execi.operand[0].value.reg = r64;
+ put_u64_reg( 0x00100, pvalid );
+ __cpu_pc = 10;
+ __cpu_sp = 512;
+ memset( __cpu_stack_ptr() + 496, 0xa5, 16 );
+ __cpu_set_psw( PSW_CF_MASK | PSW_TF_MASK );
+ psw = __cpu_get_psw();
+ CHECK( __cpu_exec( &execi ) == CPU_EXEC_BRANCH, "CALLP executor success" );
+ CHECK( __cpu_pc == __cpu_ptr_offset( pvalid ), "CALLP installed target PC" );
+ CHECK( __cpu_sp == 504, "CALLP pushed 8-byte return PC" );
+ ret = __cpu_data_get_u64( __cpu_stack_ptr() + 504 );
+ CHECK( ret == 15, "CALLP return PC is next 5-byte instruction" );
+ CHECK( __cpu_get_psw() == psw, "CALLP preserves PSW" );
+
+ CHECK( __cpu_ptr_make( &pbad, CPU_PTR_SEG_STACK, 0 ) == 0, "make STACK pointer" );
+ put_u64_reg( 0x00100, pbad );
+ __cpu_pc = 20;
+ __cpu_sp = 512;
+ saved_pc = __cpu_pc;
+ saved_sp = __cpu_sp;
+ memcpy( before, __cpu_stack_ptr() + 496, 16 );
+ CHECK( __cpu_exec( &execi ) == CPU_EXEC_POINTER_FAULT, "CALLP rejects non-PROGRAM PTR64" );
+ memcpy( after, __cpu_stack_ptr() + 496, 16 );
+ CHECK( __cpu_pc == saved_pc && __cpu_sp == saved_sp && !memcmp( before, after, 16 ), "CALLP pointer fault is atomic" );
+
+ put_u64_reg( 0x00100, pvalid );
+ __cpu_pc = 20;
+ __cpu_sp = 4;
+ saved_pc = __cpu_pc;
+ saved_sp = __cpu_sp;
+ CHECK( __cpu_exec( &execi ) == CPU_EXEC_STACK_FAULT, "CALLP checks return-address stack space" );
+ CHECK( __cpu_pc == saved_pc && __cpu_sp == saved_sp, "CALLP stack fault is atomic" );
+
+ execi.operand[0].value.reg = r32;
+ __cpu_sp = 512;
+ CHECK( __cpu_exec( &execi ) == CPU_EXEC_WIDTH_FAULT, "CALLP requires physical r64" );
+
+ CHECK( cpu_assemble_text( ".text\n JMPP r32@0x00000\n", &bad, err, sizeof( err ) ) != 0, "assembler rejects JMPP r32" );
+ CHECK( cpu_assemble_text( ".text\n CALLP c64@0x00000\n", &bad, err, sizeof( err ) ) != 0, "assembler rejects CALLP c64 spelling" );
+ CHECK( cpu_assemble_text( ".data\n .cptr nowhere\n.text\n HALT\n", &bad, err, sizeof( err ) ) != 0, "assembler rejects undefined .cptr label" );
+
+ cpu_asm_image_free( &im );
+ __cpu_free_context();
+ puts( "libcpu indirect control-flow regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-indirect-runner.sh b/tests/tst-indirect-runner.sh
new file mode 100755
index 0000000..dba3b94
--- /dev/null
+++ b/tests/tst-indirect-runner.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${TEST_SRCDIR:?}"
+base="${TMPDIR:-/tmp}/libcpu-indirect-$$"
+trap 'rm -f "$base.s" "$base.o" "$base.melf"' EXIT HUP INT TERM
+"$CPU_AS" -o "$base.o" "$TEST_SRCDIR/fixtures/indirect-control.s" >/dev/null
+"$CPU_LD" --stack-size=512 -o "$base.melf" "$base.o" >/dev/null
+out=`"$CPU_RUN" --int-reg=r64@0x00020 "$base.melf"`
+test "x$out" = "x42"
diff --git a/tests/tst-instruction.c b/tests/tst-instruction.c
new file mode 100644
index 0000000..052fb9c
--- /dev/null
+++ b/tests/tst-instruction.c
@@ -0,0 +1,237 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+
+#include <libmcpu.h>
+
+
+static int failures = 0;
+
+#define CHECK(cond,msg) \
+ do { \
+ if( !(cond) ) \
+ { \
+ fprintf( stderr, "FAIL: %s\n", (msg) ); \
+ ++failures; \
+ } \
+ } while( 0 )
+
+
+static cpu_operand
+reg_operand( unsigned int bits, __mpu_uint32_t address )
+{
+ cpu_operand operand;
+
+ memset( &operand, 0, sizeof(operand) );
+ operand.type = CPU_OPERAND_REGISTER;
+ operand.value.reg.bits = bits;
+ operand.value.reg.address = address;
+
+ return( operand );
+}
+
+
+static void
+clear_instruction( cpu_instruction *instruction, cpu_opcode opcode )
+{
+ memset( instruction, 0, sizeof(*instruction) );
+ instruction->opcode = opcode;
+}
+
+
+int
+main( void )
+{
+ cpu_instruction instruction;
+ __mpu_uint32_t psw;
+
+ __cpu_init();
+
+ /* NOP must not change the logical PSW. */
+ __cpu_set_psw( PSW_CF_MASK | PSW_INXF_MASK | PSW_TF_MASK | PSW_IF_MASK );
+ psw = __cpu_get_psw();
+
+ clear_instruction( &instruction, CPU_OP_NOP );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "NOP return code" );
+ CHECK( __cpu_get_psw() == psw, "NOP preserves PSW" );
+
+ /* MOV is a raw physical-register copy and accepts overlap. */
+ {
+ __mpu_UNIT_t *src = __cpu_reg_ptr( 128, 0x00100 );
+ __mpu_UNIT_t *dst = __cpu_reg_ptr( 128, 0x00110 );
+ unsigned int i;
+
+ CHECK( src != 0 && dst != 0, "MOV register pointers" );
+
+ for( i = 0; i < 16; ++i ) src[i] = (__mpu_UNIT_t)( 0x20 + i );
+ memset( dst, 0, 16 );
+
+ clear_instruction( &instruction, CPU_OP_MOV );
+ instruction.operand[0] = reg_operand( 128, 0x00110 );
+ instruction.operand[1] = reg_operand( 128, 0x00100 );
+
+ psw = __cpu_get_psw();
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "MOV return code" );
+ CHECK( memcmp( dst, src, 16 ) == 0, "MOV register contents" );
+ CHECK( __cpu_get_psw() == psw, "MOV preserves PSW" );
+
+ /* Overlap must have memmove semantics. */
+ for( i = 0; i < 32; ++i ) __cpu_registers[0x00200 + i] = (__mpu_UNIT_t)i;
+
+ clear_instruction( &instruction, CPU_OP_MOV );
+ instruction.operand[0] = reg_operand( 128, 0x00210 );
+ instruction.operand[1] = reg_operand( 128, 0x00200 );
+
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS,
+ "overlapping MOV return code" );
+ for( i = 0; i < 16; ++i )
+ CHECK( __cpu_registers[0x00210 + i] == (__mpu_UNIT_t)i,
+ "overlapping MOV contents" );
+ }
+
+ /* CLR clears physical storage and leaves PSW unchanged. */
+ {
+ __mpu_UNIT_t *dst = __cpu_reg_ptr( 256, 0x00300 );
+ unsigned int i;
+
+ CHECK( dst != 0, "CLR register pointer" );
+ memset( dst, 0xa5, 32 );
+
+ clear_instruction( &instruction, CPU_OP_CLR );
+ instruction.operand[0] = reg_operand( 256, 0x00300 );
+
+ psw = __cpu_get_psw();
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "CLR return code" );
+ for( i = 0; i < 32; ++i ) CHECK( dst[i] == 0, "CLR register contents" );
+ CHECK( __cpu_get_psw() == psw, "CLR preserves PSW" );
+ }
+
+ /* IADD delegates arithmetic and integer FLAGS to LibMPU. */
+ {
+ __mpu_UNIT_t *dst = __cpu_reg_ptr( 8, 0x00400 );
+ __mpu_UNIT_t *a = __cpu_reg_ptr( 8, 0x00401 );
+ __mpu_UNIT_t *b = __cpu_reg_ptr( 8, 0x00402 );
+
+ *a = 0xff;
+ *b = 0x01;
+ *dst = 0x5a;
+
+ __cpu_stt();
+ __cpu_sti();
+ CHECK( __cpu_stmode( 2 ) == 0, "set CPU status before IADD" );
+
+ clear_instruction( &instruction, CPU_OP_IADD );
+ instruction.operand[0] = reg_operand( 8, 0x00400 );
+ instruction.operand[1] = reg_operand( 8, 0x00401 );
+ instruction.operand[2] = reg_operand( 8, 0x00402 );
+
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "IADD return code" );
+ CHECK( *dst == 0x00, "IADD result" );
+ CHECK( __mpu_gtc() == 1, "IADD carry flag" );
+ CHECK( __mpu_gtz() == 1, "IADD zero flag" );
+ CHECK( __cpu_gtt() == 1, "IADD preserves TF" );
+ CHECK( __cpu_gti() == 1, "IADD preserves IF" );
+ CHECK( __cpu_gtmode() == 2, "IADD preserves MODE" );
+ }
+
+ /* IADC consumes LibMPU carry from the preceding operation. */
+ {
+ __mpu_UNIT_t *dst = __cpu_reg_ptr( 8, 0x00403 );
+ __mpu_UNIT_t *a = __cpu_reg_ptr( 8, 0x00404 );
+ __mpu_UNIT_t *b = __cpu_reg_ptr( 8, 0x00405 );
+
+ *a = 1;
+ *b = 2;
+ *dst = 0;
+ __mpu_stc();
+
+ clear_instruction( &instruction, CPU_OP_IADC );
+ instruction.operand[0] = reg_operand( 8, 0x00403 );
+ instruction.operand[1] = reg_operand( 8, 0x00404 );
+ instruction.operand[2] = reg_operand( 8, 0x00405 );
+
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "IADC return code" );
+ CHECK( *dst == 4, "IADC result with carry" );
+ }
+
+ /* ISUB and ISBB use the same three-register representation. */
+ {
+ __mpu_UNIT_t *dst = __cpu_reg_ptr( 8, 0x00406 );
+ __mpu_UNIT_t *a = __cpu_reg_ptr( 8, 0x00407 );
+ __mpu_UNIT_t *b = __cpu_reg_ptr( 8, 0x00408 );
+
+ *a = 9;
+ *b = 3;
+
+ clear_instruction( &instruction, CPU_OP_ISUB );
+ instruction.operand[0] = reg_operand( 8, 0x00406 );
+ instruction.operand[1] = reg_operand( 8, 0x00407 );
+ instruction.operand[2] = reg_operand( 8, 0x00408 );
+
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "ISUB return code" );
+ CHECK( *dst == 6, "ISUB result" );
+
+ *a = 9;
+ *b = 3;
+ __mpu_stc();
+
+ clear_instruction( &instruction, CPU_OP_ISBB );
+ instruction.operand[0] = reg_operand( 8, 0x00406 );
+ instruction.operand[1] = reg_operand( 8, 0x00407 );
+ instruction.operand[2] = reg_operand( 8, 0x00408 );
+
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "ISBB return code" );
+ CHECK( *dst == 5, "ISBB result with borrow" );
+ }
+
+ /* Operand validation belongs to the common executor. */
+ clear_instruction( &instruction, CPU_OP_MOV );
+ instruction.operand[0] = reg_operand( 16, 0x00500 );
+ instruction.operand[1] = reg_operand( 32, 0x00504 );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_WIDTH_FAULT,
+ "MOV width mismatch" );
+
+ clear_instruction( &instruction, CPU_OP_IADD );
+ instruction.operand[0] = reg_operand( 32, 0x00501 ); /* unaligned */
+ instruction.operand[1] = reg_operand( 32, 0x00504 );
+ instruction.operand[2] = reg_operand( 32, 0x00508 );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_REGISTER_FAULT,
+ "invalid register operand" );
+
+ clear_instruction( &instruction, CPU_OP_CLR );
+ instruction.operand[0].type = CPU_OPERAND_IMMEDIATE;
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_OPERAND_FAULT,
+ "unsupported operand type" );
+
+ clear_instruction( &instruction, (cpu_opcode)0x7fff );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_OPCODE_FAULT,
+ "unknown opcode" );
+
+ CHECK( __cpu_exec( (const cpu_instruction *)0 ) == CPU_EXEC_INSTRUCTION_FAULT,
+ "NULL instruction" );
+
+ /* HALT sets only the CPU-owned HF part of PSW. */
+ __cpu_set_psw( PSW_CF_MASK | PSW_INXF_MASK | PSW_IF_MASK );
+ clear_instruction( &instruction, CPU_OP_HALT );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "HALT return code" );
+ psw = __cpu_get_psw();
+ CHECK( (psw & PSW_HF_MASK) != 0, "HALT sets HF" );
+ CHECK( (psw & PSW_CF_MASK) != 0, "HALT preserves CF" );
+ CHECK( (psw & PSW_INXF_MASK) != 0, "HALT preserves INXF" );
+ CHECK( (psw & PSW_IF_MASK) != 0, "HALT preserves IF" );
+
+ if( failures != 0 )
+ {
+ fprintf( stderr, "libcpu instruction regression: %d failure(s)\n", failures );
+ return( 1 );
+ }
+
+ printf( "libcpu 0.0.3 decoded instruction/executor regression: PASS\n" );
+ printf( "opcodes: NOP HALT MOV CLR IADD IADC ISUB ISBB\n" );
+ printf( "binary encoding and addressing modes are not defined yet\n" );
+
+ return( 0 );
+}
diff --git a/tests/tst-library-search-runner.sh b/tests/tst-library-search-runner.sh
new file mode 100755
index 0000000..6c5ee73
--- /dev/null
+++ b/tests/tst-library-search-runner.sh
@@ -0,0 +1,118 @@
+#!/bin/sh
+set -eu
+
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${TEST_SRCDIR:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-library-search-$$
+HOME="$TMP/home"
+export HOME
+mkdir -p "$HOME/.mcpu/etc" "$TMP/root/lib" "$TMP/first" "$TMP/second" "$TMP/user/lib"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+
+EXAMPLES=`cd "$TEST_SRCDIR/fixtures/shared-libs" && pwd`
+
+"$CPU_AS" -o "$TMP/main.o" "$EXAMPLES/dynamic-main.s"
+"$CPU_AS" -o "$TMP/a.o" "$EXAMPLES/dynamic-liba.s"
+"$CPU_AS" -o "$TMP/b.o" "$EXAMPLES/dynamic-libb.s"
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -o "$TMP/main-program" "$TMP/main.o"
+"$CPU_LD" --shared --soname=liba.so.1 -o "$TMP/liba.so" "$TMP/a.o"
+"$CPU_LD" --shared --soname=libb.so.1 -o "$TMP/libb.so" "$TMP/b.o"
+
+cp "$TMP/liba.so" "$TMP/root/lib/liba.so"
+cp "$TMP/libb.so" "$TMP/root/lib/libb.so"
+cp "$TMP/liba.so" "$TMP/root/lib/libpick.so"
+cp "$TMP/libb.so" "$TMP/first/libpick.so"
+cp "$TMP/liba.so" "$TMP/first/libfirst.so"
+cp "$TMP/liba.so" "$TMP/first/liborder.so"
+cp "$TMP/libb.so" "$TMP/second/libpick.so"
+cp "$TMP/libb.so" "$TMP/second/libsecond.so"
+cp "$TMP/libb.so" "$TMP/second/liborder.so"
+
+# A bare preload name is resolved through <sys-root>/lib.
+[ "`"$CPU_RUN" --sys-root="$TMP/root" --int-reg=r64@0x00018 \
+ --ldpreload=liba.so "$TMP/main-program"`" = 42 ]
+
+# Explicit --library-path directories have priority over the system tree.
+[ "`"$CPU_RUN" --sys-root="$TMP/root" --library-path="$TMP/first" \
+ --int-reg=r64@0x00018 --ldpreload=libpick.so "$TMP/main-program"`" = 43 ]
+
+# Colon-list order is the actual search order.
+[ "`"$CPU_RUN" --sys-root="$TMP/root" \
+ --library-path="$TMP/first:$TMP/second" --int-reg=r64@0x00018 \
+ --ldpreload=liborder.so "$TMP/main-program"`" = 42 ]
+[ "`"$CPU_RUN" --sys-root="$TMP/root" \
+ --library-path="$TMP/second:$TMP/first" --int-reg=r64@0x00018 \
+ --ldpreload=liborder.so "$TMP/main-program"`" = 43 ]
+
+# Repeated --library-path occurrences append in command-line order.
+cat > "$TMP/run-search.expected" <<EOT
+libraries:
+ $TMP/first
+ $TMP/second
+ $TMP/root/lib
+EOT
+"$CPU_RUN" --sys-root="$TMP/root" --library-path="$TMP/first" \
+ --library-path "$TMP/second" --print-search-dirs > "$TMP/run-search.txt"
+cmp "$TMP/run-search.expected" "$TMP/run-search.txt"
+
+# mcpu-ld uses the same common search state through -L.
+cat > "$TMP/ld-search.expected" <<EOT
+libraries:
+ $TMP/first
+ $TMP/second
+ $TMP/root/lib
+EOT
+"$CPU_LD" --sys-root="$TMP/root" -L"$TMP/first" -L "$TMP/second" \
+ --print-search-dirs > "$TMP/ld-search.txt"
+cmp "$TMP/ld-search.expected" "$TMP/ld-search.txt"
+
+# Explicit pathnames keep the historical behavior and bypass name lookup.
+[ "`"$CPU_RUN" --sys-root="$TMP/root" --library-path="$TMP/first" \
+ --int-reg=r64@0x00018 --ldpreload="$TMP/root/lib/liba.so" \
+ "$TMP/main-program"`" = 42 ]
+
+# Resolving a list does not change the established rightmost preload priority.
+[ "`"$CPU_RUN" --sys-root="$TMP/root" --int-reg=r64@0x00018 \
+ --ldpreload=liba.so:libb.so "$TMP/main-program"`" = 43 ]
+[ "`"$CPU_RUN" --sys-root="$TMP/root" --int-reg=r64@0x00018 \
+ --ldpreload=libb.so:liba.so "$TMP/main-program"`" = 42 ]
+
+# The configured system path participates in real name lookup.
+cp "$TMP/liba.so" "$TMP/user/lib/libuser.so"
+cat > "$HOME/.mcpu/etc/mcpu-ld.conf" <<EOC
+MCPU_LD_SYSTEM_LIBRARY_PATH = $TMP/user/lib;
+EOC
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 --ldpreload=libuser.so \
+ "$TMP/main-program"`" = 42 ]
+
+# An explicitly empty system path has no fallback, while an explicit path works.
+printf '%s\n' 'MCPU_LD_SYSTEM_LIBRARY_PATH = ;' \
+ > "$HOME/.mcpu/etc/mcpu-ld.conf"
+if "$CPU_RUN" --ldpreload=liba.so "$TMP/main-program" >/dev/null 2>&1; then
+ echo "bare preload unexpectedly resolved with empty system path" >&2
+ exit 1
+fi
+[ "`"$CPU_RUN" --library-path="$TMP/root/lib" --int-reg=r64@0x00018 \
+ --ldpreload=liba.so "$TMP/main-program"`" = 42 ]
+
+# Missing names and empty path-list elements are errors.
+if "$CPU_RUN" --sys-root="$TMP/root" --ldpreload=does-not-exist.so \
+ "$TMP/main-program" >/dev/null 2>&1; then
+ echo "missing preload name unexpectedly resolved" >&2
+ exit 1
+fi
+if "$CPU_RUN" --sys-root="$TMP/root" --library-path="$TMP/first::$TMP/second" \
+ --print-search-dirs >/dev/null 2>&1; then
+ echo "empty --library-path element unexpectedly accepted" >&2
+ exit 1
+fi
+
+# 0.2.15 exposes -l for real link-time dependency resolution. Search-dir
+# diagnostics remain independent of whether a later link uses library operands.
+"$CPU_LD" --help 2>&1 | grep -q -- '-l <name>'
+
+"$CPU_RUN" --help 2>&1 | grep -q -- '--library-path'
+"$CPU_LD" --help 2>&1 | grep -q -- '-L <dir>'
+
+exit 0
diff --git a/tests/tst-lifecycle-runner.sh b/tests/tst-lifecycle-runner.sh
new file mode 100755
index 0000000..c25a29a
--- /dev/null
+++ b/tests/tst-lifecycle-runner.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+set -eu
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${TEST_SRCDIR:?}"
+base="${TMPDIR:-/tmp}/libcpu-lifecycle-$$"
+trap 'rm -f "$base.o" "$base.melf"' EXIT HUP INT TERM
+"$CPU_AS" -o "$base.o" "$TEST_SRCDIR/fixtures/model-lifecycle.s" >/dev/null
+"$CPU_LD" --stack-size=512 -o "$base.melf" "$base.o" >/dev/null
+out=`"$CPU_RUN" --init-only --int-reg=r64@0x00000 "$base.melf"`
+test "x$out" = "x40"
+out=`"$CPU_RUN" --steps=1 --int-reg=r64@0x00000 "$base.melf"`
+test "x$out" = "x42"
diff --git a/tests/tst-lifecycle.c b/tests/tst-lifecycle.c
new file mode 100644
index 0000000..6e2a7d6
--- /dev/null
+++ b/tests/tst-lifecycle.c
@@ -0,0 +1,63 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+static const char src[]=
+".init\n"
+"_start:\n"
+" LDI r64@0x00000, 40\n"
+" END\n"
+".text\n"
+"_life:\n"
+" IINC r64@0x00000, r64@0x00000\n"
+" END\n"
+" IINC r64@0x00000, r64@0x00000\n"
+" LDI r32@0x00020, 7\n"
+" EXIT r32@0x00020\n"
+".fini\n"
+"_fini:\n"
+" LDI r64@0x00008, 99\n"
+" END\n";
+static __mpu_uint64_t
+get64( unsigned a )
+{
+ return( __cpu_data_get_u64( __cpu_reg_ptr( 64, a ) ) );
+}
+
+int
+main( void )
+{
+ cpu_asm_image im;
+ char err[256];
+ int rc;
+ __cpu_init();
+ CHECK( cpu_assemble_text( src, &im, err, sizeof( err ) ) == 0, err );
+ CHECK( im.has_init && im.has_life && im.has_fini, "three lifecycle entries present" );
+ CHECK( im.init_entry < im.life_entry && im.life_entry < im.fini_entry, "entry order in PROGRAM" );
+ CHECK( __cpu_program_set( im.code, im.code_size ) == 0, "program set" );
+ CHECK( __cpu_data_set( im.data, im.data_size ) == 0, "data set" );
+ CHECK( __cpu_stack_alloc( 256 ) == 0, "stack alloc" );
+ __cpu_pc = im.init_entry;
+ rc = __cpu_run();
+ CHECK( rc == CPU_EXEC_END, ".init END returned" );
+ CHECK( __cpu_get_stop_reason() == CPU_STOP_END, "INIT stop reason END" );
+ CHECK( get64( 0 ) == 40, "init result" );
+ __cpu_pc = im.life_entry;
+ rc = __cpu_run();
+ CHECK( rc == CPU_EXEC_END, "first LIFE END returned" );
+ CHECK( get64( 0 ) == 41, "first life step" );
+ rc = __cpu_run();
+ CHECK( rc == CPU_EXEC_EXIT, "resume reaches EXIT" );
+ CHECK( __cpu_get_stop_reason() == CPU_STOP_EXIT, "EXIT stop reason" );
+ CHECK( __cpu_get_exit_status() == 7, "EXIT status" );
+ CHECK( get64( 0 ) == 42, "resume PC is after END" );
+ __cpu_pc = im.fini_entry;
+ rc = __cpu_run();
+ CHECK( rc == CPU_EXEC_END, ".fini END returned" );
+ CHECK( get64( 8 ) == 99, "fini result" );
+ cpu_asm_image_free( &im );
+ __cpu_free_context();
+ puts( "libcpu lifecycle ABI regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-line-runner.sh b/tests/tst-line-runner.sh
new file mode 100755
index 0000000..ed62877
--- /dev/null
+++ b/tests/tst-line-runner.sh
@@ -0,0 +1,174 @@
+#!/bin/sh
+set -eu
+
+: "${CPU_AS:?CPU_AS is not set}"
+
+tmp="${TMPDIR:-/tmp}/libcpu-line-$$"
+trap 'rm -rf "$tmp"' EXIT HUP INT TERM
+mkdir -p "$tmp"
+
+# --help remains a compact public interface. Preprocessor source-control
+# details belong to the documentation, not to the command-line synopsis.
+"$CPU_AS" --help >"$tmp/help.txt" 2>&1
+grep -q -- '--help' "$tmp/help.txt"
+grep -q -- '-o <file>' "$tmp/help.txt"
+if grep -q -- '#line' "$tmp/help.txt"; then
+ echo "mcpu-as --help unexpectedly documents obsolete #line" >&2
+ exit 1
+fi
+
+# The assembler accepts the usual UNIX-style option ordering. -c is no
+# longer part of the interface because mcpu-as always produces ET_REL.
+cat >"$tmp/cli.s" <<'SRC'
+.text
+.global helper
+helper:
+ RET
+SRC
+"$CPU_AS" -o "$tmp/cli-before.o" "$tmp/cli.s" >/dev/null
+"$CPU_AS" "$tmp/cli.s" -o "$tmp/cli-after.o" >/dev/null
+test -s "$tmp/cli-before.o"
+test -s "$tmp/cli-after.o"
+cmp "$tmp/cli-before.o" "$tmp/cli-after.o"
+if "$CPU_AS" -c "$tmp/cli.s" -o "$tmp/cli-old.o" >"$tmp/out.txt" 2>"$tmp/err.txt"; then
+ echo "mcpu-as unexpectedly accepted obsolete -c" >&2
+ exit 1
+fi
+grep -Fq 'mcpu-as: unknown option -c' "$tmp/err.txt"
+
+# Native diagnostics count every ordinary physical source line, including a
+# zero-length line.
+cat >"$tmp/physical.s" <<'SRC'
+.text
+
+BAD_OPCODE
+SRC
+if "$CPU_AS" -o "$tmp/physical.o" "$tmp/physical.s" >"$tmp/out.txt" 2>"$tmp/err.txt"; then
+ echo "mcpu-as unexpectedly accepted invalid physical-line source" >&2
+ exit 1
+fi
+grep -Fq "$tmp/physical.s:3: error:" "$tmp/err.txt"
+
+# Preprocessor line markers set the logical number and optional filename of the
+# source line immediately following the marker. Trailing preprocessor flags
+# are deliberately ignored by mcpu-as.
+cat >"$tmp/marker.S" <<'SRC'
+# 7 "included source.s" 1 3 4 anything-after-the-filename-is-ignored
+.text
+THIS_IS_NOT_AN_INSTRUCTION
+SRC
+if "$CPU_AS" -o "$tmp/marker.o" "$tmp/marker.S" >"$tmp/out.txt" 2>"$tmp/err.txt"; then
+ echo "mcpu-as unexpectedly accepted invalid marker source" >&2
+ exit 1
+fi
+grep -Fq 'included source.s:8: error:' "$tmp/err.txt"
+
+# The logical counter continues across empty ordinary lines.
+cat >"$tmp/blank-lines.S" <<'SRC'
+# 4 "dyn.s"
+
+.text
+helper:
+BAD_OPCODE
+SRC
+if "$CPU_AS" -o "$tmp/blank-lines.o" "$tmp/blank-lines.S" >"$tmp/out.txt" 2>"$tmp/err.txt"; then
+ echo "mcpu-as unexpectedly accepted invalid blank-line source" >&2
+ exit 1
+fi
+grep -Fq 'dyn.s:7: error:' "$tmp/err.txt"
+
+# Omitting the filename retains the previous logical filename. Leading zeroes
+# remain decimal: 0200 means line 200, not octal 128.
+cat >"$tmp/preserve.s" <<'SRC'
+# 100 "first-name.s"
+# 0200
+.text
+BAD_OPCODE
+SRC
+if "$CPU_AS" -o "$tmp/preserve.o" "$tmp/preserve.s" >"$tmp/out.txt" 2>"$tmp/err.txt"; then
+ echo "mcpu-as unexpectedly accepted invalid preserve test" >&2
+ exit 1
+fi
+grep -Fq 'first-name.s:201: error:' "$tmp/err.txt"
+
+# Zero is accepted. Here .text is logical line 0 and the bad opcode is line 1.
+cat >"$tmp/zero.s" <<'SRC'
+# 0 "zero.s"
+.text
+BAD_OPCODE
+SRC
+if "$CPU_AS" -o "$tmp/zero.o" "$tmp/zero.s" >"$tmp/out.txt" 2>"$tmp/err.txt"; then
+ echo "mcpu-as unexpectedly accepted invalid zero-line test" >&2
+ exit 1
+fi
+grep -Fq 'zero.s:1: error:' "$tmp/err.txt"
+
+# The accepted decimal marker range remains 0..2147483647.
+cat >"$tmp/range.s" <<'SRC'
+# 2147483648 "too-large.s"
+.text
+SRC
+if "$CPU_AS" -o "$tmp/range.o" "$tmp/range.s" >"$tmp/out.txt" 2>"$tmp/err.txt"; then
+ echo "mcpu-as unexpectedly accepted an out-of-range line marker" >&2
+ exit 1
+fi
+grep -Fq 'range.s:1: error: preprocessor line marker value is outside 0..2147483647' "$tmp/err.txt"
+
+# #pragma and #lang are passed through by preprocessing stages for other
+# consumers. mcpu-as ignores them and they do not advance the logical counter.
+cat >"$tmp/meta.s" <<'SRC'
+# 100 "meta.s"
+#pragma once
+#lang "mcpu"
+BAD_OPCODE
+SRC
+if "$CPU_AS" -o "$tmp/meta.o" "$tmp/meta.s" >"$tmp/out.txt" 2>"$tmp/err.txt"; then
+ echo "mcpu-as unexpectedly accepted invalid pragma/lang source" >&2
+ exit 1
+fi
+grep -Fq 'meta.s:100: error:' "$tmp/err.txt"
+
+# Other '#' lines are discarded as assembler syntax but still count as normal
+# logical source lines.
+cat >"$tmp/hash.s" <<'SRC'
+# 50 "hash.s"
+#define SOMETHING 1
+#unknown whatever
+BAD_OPCODE
+SRC
+if "$CPU_AS" -o "$tmp/hash.o" "$tmp/hash.s" >"$tmp/out.txt" 2>"$tmp/err.txt"; then
+ echo "mcpu-as unexpectedly accepted invalid generic-hash source" >&2
+ exit 1
+fi
+grep -Fq 'hash.s:52: error:' "$tmp/err.txt"
+
+# Historical #line is no longer a source-location directive. It is simply an
+# unrecognized '#' line: ignored as syntax, counted as a normal source line,
+# and it must not replace the diagnostic filename or line counter.
+cat >"$tmp/obsolete-line.s" <<'SRC'
+#line 700 "obsolete.s"
+.text
+BAD_OPCODE
+SRC
+if "$CPU_AS" -o "$tmp/obsolete-line.o" "$tmp/obsolete-line.s" >"$tmp/out.txt" 2>"$tmp/err.txt"; then
+ echo "mcpu-as unexpectedly accepted invalid obsolete-#line source" >&2
+ exit 1
+fi
+grep -Fq "$tmp/obsolete-line.s:3: error:" "$tmp/err.txt"
+if grep -Fq 'obsolete.s:' "$tmp/err.txt"; then
+ echo "obsolete #line unexpectedly changed source coordinates" >&2
+ exit 1
+fi
+
+# A normal preprocessor marker is accepted independently of the input suffix.
+cat >"$tmp/good.s" <<'SRC'
+# 42 "generated/original source.s" 1
+.text
+.global helper
+helper:
+ RET
+SRC
+"$CPU_AS" -o "$tmp/good.o" "$tmp/good.s" >/dev/null
+test -s "$tmp/good.o"
+
+echo "libmcpu mcpu-as preprocessor source-control regression: PASS"
diff --git a/tests/tst-link-needed-runner.sh b/tests/tst-link-needed-runner.sh
new file mode 100755
index 0000000..8f33986
--- /dev/null
+++ b/tests/tst-link-needed-runner.sh
@@ -0,0 +1,151 @@
+#!/bin/sh
+set -eu
+
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${CPU_READELF:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-link-needed-$$
+HOME="$TMP/home"
+export HOME
+mkdir -p "$HOME/.mcpu/etc" "$TMP/root/lib" "$TMP/first"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+
+cat > "$TMP/main.s" <<'EOS'
+.global _life
+.extern foo
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL foo
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+
+cat > "$TMP/good.s" <<'EOS'
+.global foo
+.text
+foo:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+cat > "$TMP/alt.s" <<'EOS'
+.global foo
+.text
+foo:
+ LDI r64@0x00008, 3
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+cat > "$TMP/bad.s" <<'EOS'
+.global other
+.text
+other:
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/main.o" "$TMP/main.s"
+"$CPU_AS" -o "$TMP/good.o" "$TMP/good.s"
+"$CPU_AS" -o "$TMP/alt.o" "$TMP/alt.s"
+"$CPU_AS" -o "$TMP/bad.o" "$TMP/bad.s"
+
+# The development link name is libfoo.so, but the ABI identity recorded in
+# DT_NEEDED comes from the validated library SONAME.
+"$CPU_LD" --shared --soname=libfoo.so.1 \
+ -o "$TMP/root/lib/libfoo.so.1.2.3" "$TMP/good.o"
+ln -s libfoo.so.1.2.3 "$TMP/root/lib/libfoo.so.1"
+ln -s libfoo.so.1 "$TMP/root/lib/libfoo.so"
+
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/root/lib" -lfoo -o "$TMP/main" "$TMP/main.o"
+"$CPU_READELF" -d "$TMP/main" > "$TMP/main.dynamic"
+grep -q 'NEEDED.*\[libfoo.so.1\]' "$TMP/main.dynamic"
+[ "`grep -c 'NEEDED' "$TMP/main.dynamic"`" -eq 1 ]
+if grep -q 'libfoo.so.1.2.3' "$TMP/main.dynamic"; then
+ echo "DT_NEEDED unexpectedly contains physical library filename" >&2
+ exit 1
+fi
+
+# The separated -l NAME spelling is equivalent.
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L "$TMP/root/lib" -l foo -o "$TMP/main-separated" "$TMP/main.o"
+"$CPU_READELF" -d "$TMP/main-separated" | grep -q 'NEEDED.*\[libfoo.so.1\]'
+
+# Repeated requests resolving to the same SONAME produce one dependency.
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/root/lib" -lfoo -lfoo -o "$TMP/main-duplicate" "$TMP/main.o"
+"$CPU_READELF" -d "$TMP/main-duplicate" > "$TMP/main-duplicate.dynamic"
+[ "`grep -c 'NEEDED' "$TMP/main-duplicate.dynamic"`" -eq 1 ]
+
+# -L order is real link-name search order. The selected object's SONAME,
+# not the requested link name, is recorded.
+"$CPU_LD" --shared --soname=libfoo-alt.so.7 \
+ -o "$TMP/first/libfoo.so" "$TMP/alt.o"
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/first" -L"$TMP/root/lib" -lfoo \
+ -o "$TMP/main-first" "$TMP/main.o"
+"$CPU_READELF" -d "$TMP/main-first" | grep -q 'NEEDED.*\[libfoo-alt.so.7\]'
+
+# The configured system library tree is also used by -l.
+cat > "$HOME/.mcpu/etc/mcpu-ld.conf" <<EOC
+MCPU_LD_SYSTEM_LIBRARY_PATH = $TMP/root/lib;
+EOC
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -lfoo -o "$TMP/main-config" "$TMP/main.o"
+"$CPU_READELF" -d "$TMP/main-config" | grep -q 'NEEDED.*\[libfoo.so.1\]'
+
+# A shared object may itself record a direct ABI-v3 dependency.
+"$CPU_LD" --shared --soname=libchild.so.1 \
+ -lfoo -o "$TMP/libchild.so.1.0.0" "$TMP/bad.o"
+"$CPU_READELF" -d "$TMP/libchild.so.1.0.0" > "$TMP/child.dynamic"
+grep -q 'SONAME.*\[libchild.so.1\]' "$TMP/child.dynamic"
+grep -q 'NEEDED.*\[libfoo.so.1\]' "$TMP/child.dynamic"
+
+# A named dependency must exist and must be a valid ABI-v3 ET_DYN object.
+if "$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/root/lib" -lmissing -o "$TMP/missing" "$TMP/main.o" \
+ >/dev/null 2>&1; then
+ echo "missing -l dependency unexpectedly linked" >&2
+ exit 1
+fi
+
+cp "$TMP/main" "$TMP/root/lib/libexec.so"
+if "$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/root/lib" -lexec -o "$TMP/from-exec" "$TMP/main.o" \
+ >/dev/null 2>&1; then
+ echo "ET_EXEC unexpectedly accepted as -l provider" >&2
+ exit 1
+fi
+
+# Existing file, valid shared object, wrong exports: link must fail before
+# producing an executable.
+"$CPU_LD" --shared --soname=libbad.so.1 \
+ -o "$TMP/root/lib/libbad.so" "$TMP/bad.o"
+rm -f "$TMP/no-provider"
+if "$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/root/lib" -lbad -o "$TMP/no-provider" "$TMP/main.o" \
+ >"$TMP/no-provider.out" 2>"$TMP/no-provider.err"; then
+ echo "library without required export unexpectedly linked" >&2
+ exit 1
+fi
+grep -q 'undefined symbol not provided by linked libraries: foo' \
+ "$TMP/no-provider.err"
+[ ! -e "$TMP/no-provider" ]
+
+# ABI-v1 -l searches only for a static archive. This directory contains
+# only libfoo.so, so the static link must fail instead of using ET_DYN.
+if "$CPU_LD" -L"$TMP/root/lib" -lfoo -o "$TMP/static" "$TMP/main.o" \
+ >/dev/null 2>&1; then
+ echo "ABI-v1 unexpectedly selected a shared -l provider" >&2
+ exit 1
+fi
+
+# 0.2.16 resolves the recorded SONAME at runtime through the same system
+# library search policy. No explicit --ldpreload is required.
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 "$TMP/main"`" = 42 ]
+
+"$CPU_LD" --help 2>&1 | grep -q -- '-l <name>'
+
+exit 0
diff --git a/tests/tst-melf.c b/tests/tst-melf.c
new file mode 100644
index 0000000..c872857
--- /dev/null
+++ b/tests/tst-melf.c
@@ -0,0 +1,430 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <libmcpu.h>
+#include <mcpu-elf.h>
+
+#include "mcpu-asm.h"
+#include "mcpu-object.h"
+#include "mcpu-link.h"
+#include "mcpu-exec.h"
+#include "mcpu-melf.h"
+
+#define CHECK(COND, MSG) \
+ do { if(!(COND)) { fprintf(stderr, "FAIL: %s\n", (MSG)); goto out; } } while(0)
+
+static int
+find_symbol( const cpu_asm_object *o, const char *name, size_t *idx )
+{
+ size_t i;
+ for( i = 0; i < o->symbol_count; ++i )
+ if( !strcmp( o->symbols[i].name, name ) )
+ {
+ if( idx ) * idx = i;
+ return( 0 );
+ }
+ return( -1 );
+}
+
+static int
+write_copy( const char *path, const unsigned char *p, size_t n )
+{
+ FILE *f = fopen( path, "wb" );
+ if( !f ) return( -1 );
+ if( n && fwrite( p, 1, n, f ) != n )
+ {
+ fclose( f );
+ return( -1 );
+ }
+ return( fclose( f ) ? -1 : 0 );
+}
+
+static int
+expect_load_fail( const char *path, const char *needle )
+{
+ cpu_exec_loaded ld;
+ char err[512];
+ char *av[] = {(char *)"prog"};
+
+ if( !cpu_exec_load( path, 1, av, &ld, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "malformed executable unexpectedly loaded: %s\n", path );
+ return( -1 );
+ }
+ if( needle && !strstr( err, needle ) )
+ {
+ fprintf( stderr, "load error mismatch: expected '%s', got '%s'\n", needle, err );
+ return( -1 );
+ }
+ return( 0 );
+}
+
+int
+main( void )
+{
+ static const char main_src[] =
+ ".extern func\n"
+ ".extern shared\n"
+ ".extern scratch\n"
+ ".text\n"
+ "_start_life:\n"
+ " LDI r64@0x00000, 40\n"
+ " CALL func\n" /* CODE64 */
+ " STORE shared, r64@0x00000\n" /* DATA64 */
+ " LOAD r64@0x00018, shared\n" /* DATA64 */
+ " LEA r64@0x00020, scratch\n" /* PTR64_CODE */
+ " LDI r32@0x00030, 0\n"
+ " EXIT r32@0x00030\n";
+
+ static const char lib_src[] =
+ ".global func, shared, scratch, shared_ptr, func_ptr\n"
+ ".data\n"
+ ".align 32\n"
+ "shared:\n"
+ " .integer 64, 0\n"
+ "shared_ptr:\n"
+ " .ptr scratch\n" /* PTR64_DATA */
+ "func_ptr:\n"
+ " .cptr func\n" /* CPTR64_DATA */
+ ".bss\n"
+ ".align 64\n"
+ "scratch:\n"
+ " .zero 128\n"
+ ".text\n"
+ "func:\n"
+ " LDI r64@0x00008, 2\n"
+ " IADD r64@0x00000, r64@0x00000, r64@0x00008\n"
+ " RET\n";
+
+ char dtemplate[] = "/tmp/libcpu-melf-XXXXXX";
+ char main_o[512], lib_o[512], exe[512], life_exe[512], bad_guard[512], bad_entry[512];
+ char err[512];
+ cpu_asm_object built[2], readback[2];
+ cpu_asm_image im;
+ cpu_exec_options opt;
+ cpu_exec_loaded ld;
+ unsigned char *file = 0;
+ size_t file_size = 0;
+ size_t ix, i;
+ __mpu_uint64_t p;
+ int data;
+ int rc = 1;
+
+ memset( built, 0, sizeof( built ) );
+ memset( readback, 0, sizeof( readback ) );
+ memset( &im, 0, sizeof( im ) );
+
+ CHECK( mkdtemp( dtemplate ) != NULL, "mkdtemp" );
+ snprintf( main_o, sizeof( main_o ), "%s/main.o", dtemplate );
+ snprintf( lib_o, sizeof( lib_o ), "%s/lib.o", dtemplate );
+ snprintf( exe, sizeof( exe ), "%s/a.melf", dtemplate );
+ snprintf( life_exe, sizeof( life_exe ), "%s/lifecycle.melf", dtemplate );
+ snprintf( bad_guard, sizeof( bad_guard ), "%s/bad-guard.melf", dtemplate );
+ snprintf( bad_entry, sizeof( bad_entry ), "%s/bad-entry.melf", dtemplate );
+
+ __cpu_init();
+
+ /*
+ * LibMPU uses MPU_BYTE_ORDER / MPU_WORD_ORDER as numeric tags (for
+ * example 1234), while the *_LITTLE_ENDIAN / *_BIG_ENDIAN macros are
+ * boolean selectors. MELF must key off the boolean selectors rather
+ * than compare the tag with 0/1.
+ */
+#if CPU_DATA_BYTE_ORDER_LITTLE_ENDIAN == 1
+ CHECK( cpu_melf_data_encoding() == MELF_DATA_LSB,
+ "MELF EI_DATA follows LibMPU little-endian selector");
+#elif CPU_DATA_BYTE_ORDER_BIG_ENDIAN == 1
+ CHECK( cpu_melf_data_encoding() == MELF_DATA_MSB,
+ "MELF EI_DATA follows LibMPU big-endian selector");
+#else
+# error Unsupported LibMPU byte order for MELF regression
+#endif
+ {
+ MElf_McpuTarget mt;
+ cpu_melf_fill_target( &mt );
+#if CPU_DATA_WORD_ORDER_LITTLE_ENDIAN == 1
+ CHECK( mt.mt_data_word_order == MELF_MCPU_WORD_ORDER_LSB,
+ "MELF target metadata follows LibMPU little word-order selector");
+#elif CPU_DATA_WORD_ORDER_BIG_ENDIAN == 1
+ CHECK( mt.mt_data_word_order == MELF_MCPU_WORD_ORDER_MSB,
+ "MELF target metadata follows LibMPU big word-order selector");
+#else
+# error Unsupported LibMPU word order for MELF regression
+#endif
+ }
+
+ CHECK( cpu_assemble_object( main_src, &built[0], err, sizeof( err ) ) == 0, err );
+ CHECK( cpu_assemble_object( lib_src, &built[1], err, sizeof( err ) ) == 0, err );
+ CHECK( built[1].has_bss && built[1].bss_size == 128 && built[1].bss_align == 64,
+ "assembler .bss/NOBITS metadata");
+ CHECK( built[0].reloc_count == 4 && built[1].reloc_count == 2,
+ "all expected relocations emitted");
+
+ /* Assembler-only lifecycle pseudo-label rules. */
+ {
+ cpu_asm_object q;
+ memset( &q, 0, sizeof( q ) );
+ CHECK( cpu_assemble_object(
+ ".init\n_start:\n END\n.text\n_start_life:\n END\n",
+ &q, err, sizeof( err )) != 0 && strstr( err, "_start_life" ),
+ "_start_life is rejected when .init exists");
+ cpu_asm_object_free( &q );
+ memset( &q, 0, sizeof( q ) );
+ CHECK( cpu_assemble_object(
+ ".text\n_start:\n_start_life:\n END\n",
+ &q, err, sizeof( err )) != 0 && strstr( err, "conflicts" ),
+ "_start_life conflicts with explicit _start/_life");
+ cpu_asm_object_free( &q );
+ memset( &q, 0, sizeof( q ) );
+ CHECK( cpu_assemble_object(
+ ".weak _start\n.text\n_start_life:\n END\n",
+ &q, err, sizeof( err )) != 0 && strstr( err, "cannot be weak" ),
+ "lifecycle symbols cannot be weak");
+ cpu_asm_object_free( &q );
+ }
+
+ /* Full .init/.text/.fini ET_EXEC lifecycle metadata and loader path. */
+ {
+ static const char life_src[] =
+ ".init\n"
+ "_start:\n"
+ " END\n"
+ ".text\n"
+ "_life:\n"
+ " LDI r32@0x00000, 0\n"
+ " EXIT r32@0x00000\n"
+ ".fini\n"
+ "_fini:\n"
+ " END\n";
+ cpu_asm_object q;
+ cpu_asm_image qi;
+ cpu_exec_loaded ql;
+ cpu_exec_options qo = {512, 0, 0, 0};
+ char *qv[] = {(char *)"lifecycle.melf"};
+ memset( &q, 0, sizeof( q ) );
+ memset( &qi, 0, sizeof( qi ) );
+ memset( &ql, 0, sizeof( ql ) );
+ CHECK( cpu_assemble_object( life_src, &q, err, sizeof( err ) ) == 0, err );
+ CHECK( cpu_link_objects( &q, 1, &qi, err, sizeof( err ) ) == 0, err );
+ CHECK( qi.has_start && qi.has_life && qi.has_init && qi.has_fini &&
+ qi.start_entry == qi.init_entry && qi.start_entry != qi.life_entry,
+ "linked INIT/LIFE/FINI lifecycle");
+ CHECK( cpu_exec_write( life_exe, &qi, &qo, err, sizeof( err ) ) == 0, err );
+ CHECK( cpu_exec_load( life_exe, 1, qv, &ql, err, sizeof( err ) ) == 0, err );
+ CHECK( ql.has_init && ql.has_fini && ql.start_entry == ql.init_entry &&
+ ql.start_entry != ql.life_entry,
+ "ET_EXEC INIT/LIFE/FINI loader metadata");
+ __cpu_free_context();
+ __cpu_init();
+ cpu_asm_image_free( &qi );
+ cpu_asm_object_free( &q );
+ }
+
+ CHECK( find_symbol( &built[0], "_start", &ix ) == 0,
+ "_start_life emits _start");
+ CHECK( built[0].symbols[ix].binding == CPU_OBJ_BIND_GLOBAL &&
+ built[0].symbols[ix].type == CPU_OBJ_TYPE_FUNC,
+ "_start ABI symbol attributes");
+ {
+ __mpu_uint64_t sv = built[0].symbols[ix].value;
+ CHECK( find_symbol( &built[0], "_life", &ix ) == 0,
+ "_start_life emits _life");
+ CHECK( built[0].symbols[ix].value == sv &&
+ built[0].symbols[ix].binding == CPU_OBJ_BIND_GLOBAL &&
+ built[0].symbols[ix].type == CPU_OBJ_TYPE_FUNC,
+ "_start/_life have same GLOBAL/FUNC address");
+ }
+
+ CHECK( cpu_object_write( main_o, &built[0], err, sizeof( err ) ) == 0, err );
+ CHECK( cpu_object_write( lib_o, &built[1], err, sizeof( err ) ) == 0, err );
+ CHECK( cpu_object_read( main_o, &readback[0], err, sizeof( err ) ) == 0, err );
+ CHECK( cpu_object_read( lib_o, &readback[1], err, sizeof( err ) ) == 0, err );
+ CHECK( readback[1].has_bss && readback[1].bss_size == 128 &&
+ readback[1].bss_align == 64, "ET_REL .bss round trip");
+ CHECK( readback[0].reloc_count == 4 && readback[1].reloc_count == 2,
+ "ET_REL RELA round trip");
+
+ /* Undefined lifecycle declarations retain STT_FUNC in ET_REL. */
+ {
+ cpu_asm_object q;
+ memset( &q, 0, sizeof( q ) );
+ CHECK( cpu_assemble_object(
+ ".extern _fini\n.text\n_start_life:\n END\n",
+ &q, err, sizeof(err)) == 0, err);
+ CHECK( find_symbol( &q, "_fini", &ix ) == 0 &&
+ q.symbols[ix].section == CPU_OBJ_SEC_UNDEF &&
+ q.symbols[ix].binding == CPU_OBJ_BIND_GLOBAL &&
+ q.symbols[ix].type == CPU_OBJ_TYPE_FUNC,
+ "undefined lifecycle symbol is GLOBAL/FUNC");
+ cpu_asm_object_free( &q );
+ }
+
+ /* WEAK resolution rules fixed by the symbol/RELA ABI. */
+ {
+ static const char weak_ptr_src[] =
+ ".weak missing\n"
+ ".data\nwp:\n .ptr missing\n"
+ ".text\n_start_life:\n END\n";
+ cpu_asm_object q;
+ cpu_asm_image qi;
+ memset( &q, 0, sizeof( q ) );
+ memset( &qi, 0, sizeof( qi ) );
+ CHECK( cpu_assemble_object( weak_ptr_src, &q, err, sizeof( err ) ) == 0, err );
+ CHECK( cpu_link_objects( &q, 1, &qi, err, sizeof( err ) ) == 0, err );
+ CHECK( qi.data_size >= CPU_DATA_NULL_GUARD_SIZE + 8 &&
+ __cpu_data_get_u64( qi.data + CPU_DATA_NULL_GUARD_SIZE ) == 0,
+ "unresolved weak pointer relocation becomes NULL");
+ cpu_asm_image_free( &qi );
+ cpu_asm_object_free( &q );
+ }
+ {
+ static const char weak_code_src[] =
+ ".weak missing\n"
+ ".text\n_start_life:\n CALL missing\n END\n";
+ cpu_asm_object q;
+ cpu_asm_image qi;
+ memset( &q, 0, sizeof( q ) );
+ memset( &qi, 0, sizeof( qi ) );
+ CHECK( cpu_assemble_object( weak_code_src, &q, err, sizeof( err ) ) == 0, err );
+ CHECK( cpu_link_objects( &q, 1, &qi, err, sizeof( err ) ) != 0 &&
+ strstr( err, "unresolved weak" ),
+ "unresolved weak CODE64 is a link error");
+ cpu_asm_image_free( &qi );
+ cpu_asm_object_free( &q );
+ }
+ {
+ static const char caller_src[] =
+ ".extern foo\n"
+ ".text\n_start_life:\n CALL foo\n END\n";
+ static const char weak_def_src[] =
+ ".weak foo\n.text\nfoo:\n NOP\n RET\n";
+ static const char strong_def_src[] =
+ ".global foo\n.text\nfoo:\n RET\n";
+ cpu_asm_object q[3];
+ cpu_asm_image qi;
+ __mpu_uint64_t expected_strong, encoded;
+ memset( q, 0, sizeof( q ) );
+ memset( &qi, 0, sizeof( qi ) );
+ CHECK( cpu_assemble_object( caller_src, &q[0], err, sizeof( err ) ) == 0, err );
+ CHECK( cpu_assemble_object( weak_def_src, &q[1], err, sizeof( err ) ) == 0, err );
+ CHECK( cpu_assemble_object( strong_def_src, &q[2], err, sizeof( err ) ) == 0, err );
+ expected_strong = q[0].text_size + q[1].text_size;
+ CHECK( cpu_link_objects( q, 3, &qi, err, sizeof( err ) ) == 0, err );
+ CHECK( q[0].reloc_count == 1 && q[0].relocs[0].type == CPU_RELOC_CODE64,
+ "strong/weak test CALL relocation");
+ encoded = __cpu_code_get_u64( qi.code + q[0].relocs[0].offset );
+ CHECK( encoded == expected_strong, "strong GLOBAL overrides earlier WEAK definition" );
+ cpu_asm_image_free( &qi );
+ for( i = 0; i < 3; ++i ) cpu_asm_object_free( &q[i] );
+ }
+
+ CHECK( cpu_link_objects( readback, 2, &im, err, sizeof( err ) ) == 0, err );
+ CHECK( im.has_start && im.has_life && !im.has_init &&
+ im.start_entry == im.life_entry, "linked no-INIT lifecycle");
+ CHECK( im.data_size == 56 && im.data_mem_size == 192,
+ "DATA filesz/memsz including .bss");
+ CHECK( im.bss_present && im.bss_offset == 64 && im.bss_size == 128,
+ "final .bss placement");
+ for( i = 0; i < CPU_DATA_NULL_GUARD_SIZE; ++i )
+ CHECK( im.data[i] == 0, "linker-owned DATA NULL guard" );
+
+ p = __cpu_data_get_u64( im.data + 40 );
+ CHECK( __cpu_ptr_segment( p ) == CPU_PTR_SEG_DATA &&
+ __cpu_ptr_offset( p ) == 64, "PTR64_DATA relocation to .bss");
+ p = __cpu_data_get_u64( im.data + 48 );
+ CHECK( __cpu_ptr_segment( p ) == CPU_PTR_SEG_PROGRAM,
+ "CPTR64_DATA relocation");
+
+ opt.stack_size = 512;
+ opt.have_heap = 1;
+ opt.heap_size = 128;
+ opt.heap_limit = 1024;
+ CHECK( cpu_exec_write( exe, &im, &opt, err, sizeof( err ) ) == 0, err );
+
+ {
+ char *av[] = {(char *)"a.melf", (char *)"alpha", (char *)"beta"};
+ CHECK( cpu_exec_load( exe, 3, av, &ld, err, sizeof( err ) ) == 0, err );
+ }
+ CHECK( __cpu_program_size() == im.code_size, "PROGRAM load size" );
+ CHECK( __cpu_data_size() == 192, "DATA p_memsz load size" );
+ CHECK( __cpu_stack_size() == 512, "STACK size comes from PT_MCPU_STACK" );
+ CHECK( __cpu_heap_size() == 128 && __cpu_heap_limit() == 1024,
+ "HEAP initial/maximum sizes");
+ CHECK( __cpu_sp == ld.startup_base && __cpu_fp == ld.stack_size,
+ "initial SP/FP startup state");
+ CHECK( __cpu_data_get_u32( __cpu_stack_ptr() + ld.startup_base +
+ MCPU_STARTUP_VERSION_OFF) == MCPU_STARTUP_VERSION_CURRENT,
+ "McpuStartup.version");
+ CHECK( __cpu_data_get_u64( __cpu_stack_ptr() + ld.startup_base +
+ MCPU_STARTUP_ARGC_OFF) == 3,
+ "McpuStartup.argc");
+ p = __cpu_data_get_u64( __cpu_stack_ptr() + ld.startup_base +
+ MCPU_STARTUP_ARGV_OFF);
+ CHECK( __cpu_ptr_segment( p ) == CPU_PTR_SEG_STACK,
+ "McpuStartup.argv segment");
+ CHECK( __cpu_data_get_u64( __cpu_stack_ptr() + __cpu_ptr_offset( p ) + 3 * 8 ) == 0,
+ "argv[argc] == NULL");
+ for( i = 64; i < 192; ++i )
+ CHECK( __cpu_data_ptr()[i] == 0, "NOBITS .bss loads as zero" );
+
+ /* Negative loader checks: it validates, never repairs. */
+ CHECK( cpu_melf_readall( exe, &file, &file_size, err, sizeof( err ) ) == 0, err );
+ data = file[MELF_EI_DATA];
+ {
+ __mpu_uint64_t phoff = cpu_melf_get64( file + MELF64_EHDR_E_PHOFF_OFF, data );
+ __mpu_uint16_t phnum = cpu_melf_get16( file + MELF64_EHDR_E_PHNUM_OFF, data );
+ __mpu_uint64_t dataoff = UINT64_MAX;
+ for( i = 0; i < phnum; ++i )
+ {
+ unsigned char *q = file + (size_t)phoff + i * MELF64_PHDR_SIZE;
+ if( cpu_melf_get32( q + MELF64_PHDR_P_TYPE_OFF, data ) == MELF_PT_LOAD &&
+ MELF_MCPU_PTR64_SEG( cpu_melf_get64( q + MELF64_PHDR_P_VADDR_OFF, data ) ) ==
+ MELF_MCPU_SEG_DATA)
+ {
+ dataoff = cpu_melf_get64( q + MELF64_PHDR_P_OFFSET_OFF, data );
+ break;
+ }
+ }
+ CHECK( dataoff != UINT64_MAX && dataoff < file_size, "find DATA PT_LOAD" );
+ file[(size_t)dataoff] = 1;
+ CHECK( write_copy( bad_guard, file, file_size ) == 0, "write bad guard file" );
+ file[(size_t)dataoff] = 0;
+ }
+ CHECK( expect_load_fail( bad_guard, "DATA NULL guard" ) == 0,
+ "loader rejects corrupted DATA guard");
+
+ cpu_melf_put64( file + MELF64_EHDR_E_ENTRY_OFF,
+ MELF_MCPU_PTR64( MELF_MCPU_SEG_PROGRAM, 1 ), data);
+ CHECK( write_copy( bad_entry, file, file_size ) == 0, "write bad entry file" );
+ CHECK( expect_load_fail( bad_entry, "without INIT" ) == 0,
+ "loader rejects inconsistent lifecycle entries");
+
+ rc = 0;
+out:
+ free( file );
+ cpu_asm_image_free( &im );
+ cpu_asm_object_free( &built[0] );
+ cpu_asm_object_free( &built[1] );
+ cpu_asm_object_free( &readback[0] );
+ cpu_asm_object_free( &readback[1] );
+ __cpu_free_context();
+ if( dtemplate[0] )
+ {
+ unlink( main_o );
+ unlink( lib_o );
+ unlink( exe );
+ unlink( life_exe );
+ unlink( bad_guard );
+ unlink( bad_entry );
+ rmdir( dtemplate );
+ }
+ if( !rc ) puts( "libcpu MELF ET_REL/ET_EXEC ABI regression: PASS" );
+ return( rc );
+}
diff --git a/tests/tst-memory.c b/tests/tst-memory.c
new file mode 100644
index 0000000..ee31477
--- /dev/null
+++ b/tests/tst-memory.c
@@ -0,0 +1,85 @@
+#include <stdio.h>
+#include <signal.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <libmcpu.h>
+
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+
+static int
+page_aligned( const void *p, size_t page )
+{
+ return( p != 0 && ( (size_t)p % page ) == 0 );
+}
+
+int
+main( void )
+{
+ long ps;
+ size_t page;
+ pid_t pid;
+ int status;
+ __mpu_UNIT_t code[3] =
+ {
+ 0x00, 0x00, 0x00
+ };
+
+ __cpu_init();
+ ps = sysconf( _SC_PAGESIZE );
+ CHECK( ps > 0, "host page size" );
+ page = (size_t)ps;
+
+ CHECK( CPU_ABI_MAX_ALIGN == 16U, "ABI maximum natural alignment" );
+ CHECK( CPU_ABI_STACK_ALIGN == 16U, "ABI stack alignment" );
+ CHECK( CPU_ABI_ARGUMENT_BLOCK_ALIGN == 16U, "ABI argument block alignment" );
+ CHECK( CPU_DATA_NULL_GUARD_SIZE == 16U, "DATA NULL guard size" );
+ CHECK( CPU_NULL_PTR == __mpu_UINT64_C( 0 ), "NULL PTR64 encoding" );
+
+ CHECK( __cpu_data_alloc( 1 ) == 0, "allocate one-byte DATA" );
+ CHECK( __cpu_data_size() == 1, "DATA architectural size is exact" );
+ CHECK( page_aligned( __cpu_data_ptr(), page ), "DATA host base is page aligned" );
+ CHECK( __cpu_data_ptr()[0] == 0, "anonymous DATA mapping is zero-filled" );
+ __cpu_data_ptr()[0] = 0x5a;
+ CHECK( __cpu_data_ptr()[0] == 0x5a, "DATA mapping is writable" );
+
+ CHECK( __cpu_stack_alloc( 1 ) == 0, "allocate one-byte STACK" );
+ CHECK( __cpu_stack_size() == 1, "STACK architectural size is exact" );
+ CHECK( page_aligned( __cpu_stack_ptr(), page ), "STACK host base is page aligned" );
+ CHECK( __cpu_stack_ptr()[0] == 0, "anonymous STACK mapping is zero-filled" );
+ CHECK( __cpu_sp == 1 && __cpu_fp == 1, "empty STACK uses exact architectural size" );
+
+ CHECK( __cpu_program_alloc( sizeof( code ) ) == 0, "allocate PROGRAM for loading" );
+ CHECK( __cpu_program_size() == sizeof( code ), "PROGRAM architectural size is exact" );
+ CHECK( page_aligned( __cpu_program_ptr(), page ), "PROGRAM host base is page aligned" );
+ memcpy( __cpu_program_ptr(), code, sizeof( code ) );
+ __cpu_program_ptr()[0] = 0x12;
+ /* PROGRAM is writable during image loading. */
+ CHECK( __cpu_program_ptr()[0] == 0x12, "PROGRAM writable before protect" );
+ CHECK( __cpu_program_protect() == 0, "protect PROGRAM read-only" );
+
+ /* Verify host protection without crashing the test process itself. */
+ pid = fork();
+ CHECK( pid >= 0, "fork for PROGRAM protection test" );
+ if( pid == 0 )
+ {
+ volatile __mpu_UNIT_t *p = __cpu_program_ptr();
+ p[0] = 0x34;
+ _exit( 0 );
+ }
+ CHECK( waitpid( pid, &status, 0 ) == pid, "wait for protection child" );
+ CHECK( WIFSIGNALED( status ), "PROGRAM host write is trapped" );
+ CHECK( WTERMSIG( status ) == SIGSEGV || WTERMSIG( status ) == SIGBUS,
+ "PROGRAM write fault signal");
+
+ /* Reallocation must unmap the protected image and create a fresh RW mapping. */
+ CHECK( __cpu_program_alloc( 2 ) == 0, "reallocate PROGRAM after protection" );
+ CHECK( __cpu_program_size() == 2, "reallocated PROGRAM exact size" );
+ __cpu_program_ptr()[0] = 0x56;
+ CHECK( __cpu_program_ptr()[0] == 0x56, "new PROGRAM mapping writable for loading" );
+
+ __cpu_free_context();
+ puts( "libcpu anonymous-mmap memory regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-model-segments.c b/tests/tst-model-segments.c
new file mode 100644
index 0000000..686bee9
--- /dev/null
+++ b/tests/tst-model-segments.c
@@ -0,0 +1,108 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+
+static const char src[]=
+".control\n"
+"ctl:\n"
+" .integer 64, 10\n"
+".input\n"
+"stimulus:\n"
+" .integer 64, 32\n"
+".output\n"
+"answer:\n"
+" .integer 64, 0\n"
+".trace\n"
+"sample:\n"
+" .integer 64, 0\n"
+".text\n"
+"_life:\n"
+" LEA r64@0x00000, ctl\n"
+" LEA r64@0x00008, stimulus\n"
+" LEA r64@0x00010, answer\n"
+" LEA r64@0x00018, sample\n"
+" LOADP r64@0x00020, r64@0x00000\n"
+" LOADP r64@0x00028, r64@0x00008\n"
+" IADD r64@0x00030, r64@0x00020, r64@0x00028\n"
+" STOREP r64@0x00010, r64@0x00030\n"
+" STOREP r64@0x00018, r64@0x00030\n"
+" HALT\n";
+
+static __mpu_uint64_t
+r64( unsigned a )
+{
+ return( __cpu_data_get_u64( __cpu_reg_ptr( 64, a ) ) );
+}
+
+static void
+regop( cpu_operand *o, unsigned bits, unsigned addr )
+{
+ memset( o, 0, sizeof( *o ) );
+ o->type = CPU_OPERAND_REGISTER;
+ o->value.reg.bits = bits;
+ o->value.reg.address = addr;
+}
+
+int
+main( void )
+{
+ cpu_asm_image im;
+ cpu_instruction x;
+ char err[256];
+ __mpu_uint64_t p;
+ int rc;
+ __cpu_init();
+ CHECK( CPU_PTR_SEG_CONTROL == 3, "CONTROL selector" );
+ CHECK( CPU_PTR_SEG_INPUT == 4, "INPUT selector" );
+ CHECK( CPU_PTR_SEG_OUTPUT == 5, "OUTPUT selector" );
+ CHECK( CPU_PTR_SEG_TRACE == 6, "TRACE selector" );
+ CHECK( CPU_PTR_SEG_HEAP == 7, "HEAP selector" );
+ CHECK( CPU_PTR_SEG_FIRST_RESERVED == 8, "reserved selectors begin at 8" );
+ CHECK( cpu_assemble_text( src, &im, err, sizeof( err ) ) == 0, err );
+ CHECK( im.control_size == 8 && im.input_size == 8 && im.output_size == 8 && im.trace_size == 8, "model segment image sizes" );
+ CHECK( im.has_life && im.life_entry == 0, "_life entry metadata" );
+ CHECK( __cpu_program_set( im.code, im.code_size ) == 0, "program set" );
+ CHECK( __cpu_data_set( im.data, im.data_size ) == 0, "data set" );
+ CHECK( __cpu_control_set( im.control, im.control_size ) == 0, "control set" );
+ CHECK( __cpu_input_set( im.input, im.input_size ) == 0, "input set" );
+ CHECK( __cpu_output_set( im.output, im.output_size ) == 0, "output set" );
+ CHECK( __cpu_trace_set( im.trace, im.trace_size ) == 0, "trace set" );
+ CHECK( __cpu_stack_alloc( 256 ) == 0, "stack alloc" );
+ CHECK( __cpu_heap_alloc( 128, 1024 ) == 0, "heap alloc" );
+ __cpu_pc = im.life_entry;
+ CHECK( __cpu_run() == CPU_EXEC_SUCCESS, "segment program run" );
+ CHECK( __cpu_ptr_segment( r64( 0x00 ) ) == CPU_PTR_SEG_CONTROL, "LEA CONTROL" );
+ CHECK( __cpu_ptr_segment( r64( 0x08 ) ) == CPU_PTR_SEG_INPUT, "LEA INPUT" );
+ CHECK( __cpu_ptr_segment( r64( 0x10 ) ) == CPU_PTR_SEG_OUTPUT, "LEA OUTPUT" );
+ CHECK( __cpu_ptr_segment( r64( 0x18 ) ) == CPU_PTR_SEG_TRACE, "LEA TRACE" );
+ CHECK( __cpu_data_get_u64( __cpu_output_ptr() ) == 42, "OUTPUT guest write" );
+ CHECK( __cpu_data_get_u64( __cpu_trace_ptr() ) == 42, "TRACE guest write" );
+
+ memset( &x, 0, sizeof( x ) );
+ x.opcode = CPU_OP_STOREP;
+ regop( &x.operand[0], 64, 0x00 );
+ regop( &x.operand[1], 64, 0x30 );
+ CHECK( __cpu_exec( &x ) == CPU_EXEC_MEMORY_FAULT, "CONTROL is guest read-only" );
+ regop( &x.operand[0], 64, 0x08 );
+ CHECK( __cpu_exec( &x ) == CPU_EXEC_MEMORY_FAULT, "INPUT is guest read-only" );
+
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_HEAP, CPU_HEAP_BEGIN ) == 0, "make HEAP pointer" );
+ __cpu_data_put_u64( __cpu_reg_ptr( 64, 0x38 ), p );
+ regop( &x.operand[0], 64, 0x38 );
+ CHECK( __cpu_exec( &x ) == CPU_EXEC_MEMORY_FAULT, "HEAP is protected above initial _end break" );
+ CHECK( __cpu_heap_size() == 128 && __cpu_heap_limit() == 1024, "HEAP size/limit ABI" );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_HEAP, 0 ) == 0, "make HEAP guard pointer" );
+ __cpu_data_put_u64( __cpu_reg_ptr( 64, 0x38 ), p );
+ CHECK( __cpu_exec( &x ) == CPU_EXEC_MEMORY_FAULT, "HEAP low guard rejects access" );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_FIRST_RESERVED, 0 ) == 0, "make reserved pointer" );
+ __cpu_data_put_u64( __cpu_reg_ptr( 64, 0x38 ), p );
+ rc = __cpu_exec( &x );
+ CHECK( rc == CPU_EXEC_POINTER_FAULT, "reserved selector still faults" );
+
+ cpu_asm_image_free( &im );
+ __cpu_free_context();
+ puts( "libcpu model-segment/HEAP regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-mpu-coverage.c b/tests/tst-mpu-coverage.c
new file mode 100644
index 0000000..0c8e3a3
--- /dev/null
+++ b/tests/tst-mpu-coverage.c
@@ -0,0 +1,580 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+
+static int failures;
+#define CHECK(c,m) do { if(!(c)){ fprintf(stderr,"FAIL: %s\n",(m)); ++failures; } } while(0)
+
+static cpu_operand
+R( unsigned bits, __mpu_uint32_t addr )
+{
+ cpu_operand o;
+ memset( &o, 0, sizeof( o ) );
+ o.type = CPU_OPERAND_REGISTER;
+ o.value.reg.bits = bits;
+ o.value.reg.address = addr;
+ return( o );
+}
+
+static void
+I( cpu_instruction *i, cpu_opcode op )
+{
+ memset( i, 0, sizeof( *i ) );
+ i->opcode = op;
+}
+
+static __mpu_UNIT_t *
+P( unsigned bits, __mpu_uint32_t addr )
+{
+ return( __cpu_reg_ptr( bits, addr ) );
+}
+
+static void
+put64( unsigned addr, __mpu_uint64_t v )
+{
+ __cpu_data_put_u64( P( 64, addr ), v );
+}
+
+static __mpu_uint64_t
+get64( unsigned addr )
+{
+ return( __cpu_data_get_u64( P( 64, addr ) ) );
+}
+
+static __mpu_int64_t
+geti64( unsigned addr )
+{
+ __mpu_uint64_t u = get64( addr );
+ __mpu_int64_t v;
+ memcpy( &v, &u, 8 );
+ return( v );
+}
+
+static void
+setreal( unsigned bits, unsigned addr, const char *s )
+{
+ int nb = (int)( bits / 8U );
+ CHECK( ascii_to_real( ( mpu_real * ) P( bits, addr ), (__mpu_char8_t *)s, nb ) >= 0, "ascii_to_real" );
+}
+
+int
+main( void )
+{
+ cpu_instruction x;
+ __cpu_init();
+
+ /* Integer shifts/rotates: compare CPU output with the matching LibMPU API. */
+ {
+ struct S
+ {
+ cpu_opcode op;
+ void( *fn )( mpu_int *, mpu_int *, int );
+ const char *n;
+ }
+ v[] =
+ {
+ {CPU_OP_ISHL,ishl,"ISHL"},{CPU_OP_ISHR,ishr,"ISHR"},{CPU_OP_ISAL,isal,"ISAL"},{CPU_OP_ISAR,isar,"ISAR"},
+ {CPU_OP_IROL,irol,"IROL"},{CPU_OP_IROR,iror,"IROR"},{CPU_OP_IRCL,ircl,"IRCL"},{CPU_OP_IRCR,ircr,"IRCR"}
+ };
+ unsigned k;
+ mpu_int8_t ref, src;
+ for( k = 0; k < sizeof( v ) / sizeof( v[0] ); k++ )
+ {
+ src[0] = 0x81;
+ ref[0] = 0;
+ if( v[k].op == CPU_OP_IRCL || v[k].op == CPU_OP_IRCR ) __mpu_stc();
+ else __mpu_clc();
+ v[k].fn( ( mpu_int * ) ref, ( mpu_int * ) src, 1 );
+ *P( 8, 0x100 ) = 0x81;
+ *P( 8, 0x101 ) = 0;
+ if( v[k].op == CPU_OP_IRCL || v[k].op == CPU_OP_IRCR ) __mpu_stc();
+ else __mpu_clc();
+ I( &x, v[k].op );
+ x.operand[0] = R( 8, 0x101 );
+ x.operand[1] = R( 8, 0x100 );
+ CHECK( __cpu_exec( &x ) == 0, v[k].n );
+ CHECK( *P( 8, 0x101 ) == ref[0], v[k].n );
+ }
+ }
+ {
+ struct N
+ {
+ cpu_opcode op;
+ void( *fn )( mpu_int *, mpu_int *, unsigned int, int );
+ const char *n;
+ }
+ v[] =
+ {
+ {CPU_OP_ISHLN,ishln,"ISHLN"},{CPU_OP_ISHRN,ishrn,"ISHRN"},{CPU_OP_ISALN,isaln,"ISALN"},{CPU_OP_ISARN,isarn,"ISARN"},
+ {CPU_OP_IROLN,iroln,"IROLN"},{CPU_OP_IRORN,irorn,"IRORN"},{CPU_OP_IRCLN,ircln,"IRCLN"},{CPU_OP_IRCRN,ircrn,"IRCRN"}
+ };
+ unsigned k;
+ mpu_int16_t ref, src;
+ for( k = 0; k < sizeof( v ) / sizeof( v[0] ); k++ )
+ {
+ __cpu_data_put_u16( src, 0x8123 );
+ if( v[k].op == CPU_OP_IRCLN || v[k].op == CPU_OP_IRCRN ) __mpu_stc();
+ else __mpu_clc();
+ v[k].fn( ( mpu_int * ) ref, ( mpu_int * ) src, 3, 2 );
+ __cpu_data_put_u16( P( 16, 0x120 ), 0x8123 );
+ put64( 0x128, 3 );
+ if( v[k].op == CPU_OP_IRCLN || v[k].op == CPU_OP_IRCRN ) __mpu_stc();
+ else __mpu_clc();
+ I( &x, v[k].op );
+ x.operand[0] = R( 16, 0x122 );
+ x.operand[1] = R( 16, 0x120 );
+ x.operand[2] = R( 64, 0x128 );
+ CHECK( __cpu_exec( &x ) == 0, v[k].n );
+ CHECK( memcmp( P( 16, 0x122 ), ref, 2 ) == 0, v[k].n );
+ }
+ }
+
+ /* INC/DEC/XCHG and width-changing integer copy/conversion. */
+ *P( 8, 0x140 ) = 41;
+ I( &x, CPU_OP_IINC );
+ x.operand[0] = R( 8, 0x141 );
+ x.operand[1] = R( 8, 0x140 );
+ CHECK( __cpu_exec( &x ) == 0, "IINC" );
+ CHECK( *P( 8, 0x141 ) == 42, "IINC result" );
+ I( &x, CPU_OP_IDEC );
+ x.operand[0] = R( 8, 0x142 );
+ x.operand[1] = R( 8, 0x141 );
+ CHECK( __cpu_exec( &x ) == 0, "IDEC" );
+ CHECK( *P( 8, 0x142 ) == 41, "IDEC result" );
+ *P( 8, 0x143 ) = 0x12;
+ *P( 8, 0x144 ) = 0x34;
+ I( &x, CPU_OP_IXCHG );
+ x.operand[0] = R( 8, 0x143 );
+ x.operand[1] = R( 8, 0x144 );
+ CHECK( __cpu_exec( &x ) == 0, "IXCHG" );
+ CHECK( *P( 8, 0x143 ) == 0x34 && *P( 8, 0x144 ) == 0x12, "IXCHG result" );
+ *P( 8, 0x145 ) = 0xff;
+ I( &x, CPU_OP_ICPY );
+ x.operand[0] = R( 16, 0x146 );
+ x.operand[1] = R( 8, 0x145 );
+ CHECK( __cpu_exec( &x ) == 0, "ICPY" );
+ CHECK( __cpu_data_get_u16( P( 16, 0x146 ) ) == 255, "ICPY zero extension" );
+ I( &x, CPU_OP_ICVT );
+ x.operand[0] = R( 16, 0x148 );
+ x.operand[1] = R( 8, 0x145 );
+ CHECK( __cpu_exec( &x ) == 0, "ICVT" );
+ CHECK( __cpu_data_get_u16( P( 16, 0x148 ) ) == 0xffff, "ICVT sign extension" );
+
+ /* Full products and quotient+remainder forms. */
+ __cpu_data_put_u16( P( 16, 0x160 ), 300 );
+ __cpu_data_put_u16( P( 16, 0x162 ), 200 );
+ I( &x, CPU_OP_IMUL );
+ x.operand[0] = R( 32, 0x164 );
+ x.operand[1] = R( 16, 0x160 );
+ x.operand[2] = R( 16, 0x162 );
+ CHECK( __cpu_exec( &x ) == 0, "IMUL" );
+ CHECK( __cpu_data_get_u32( P( 32, 0x164 ) ) == 60000, "IMUL result" );
+ __cpu_data_put_u16( P( 16, 0x168 ), (__mpu_uint16_t)-3 );
+ __cpu_data_put_u16( P( 16, 0x16a ), 7 );
+ I( &x, CPU_OP_ISMUL );
+ x.operand[0] = R( 32, 0x16c );
+ x.operand[1] = R( 16, 0x168 );
+ x.operand[2] = R( 16, 0x16a );
+ CHECK( __cpu_exec( &x ) == 0, "ISMUL" );
+ CHECK( (__mpu_int32_t)__cpu_data_get_u32( P( 32, 0x16c ) ) == -21, "ISMUL result" );
+ __cpu_data_put_u16( P( 16, 0x170 ), 100 );
+ __cpu_data_put_u16( P( 16, 0x172 ), 7 );
+ I( &x, CPU_OP_IDIV );
+ x.operand[0] = R( 16, 0x174 );
+ x.operand[1] = R( 16, 0x176 );
+ x.operand[2] = R( 16, 0x170 );
+ x.operand[3] = R( 16, 0x172 );
+ CHECK( __cpu_exec( &x ) == 0, "IDIV" );
+ CHECK( __cpu_data_get_u16( P( 16, 0x174 ) ) == 14 && __cpu_data_get_u16( P( 16, 0x176 ) ) == 2, "IDIV result" );
+ {
+ mpu_int16_t qref, rref, nref, dref;
+ __cpu_data_put_u16( nref, (__mpu_uint16_t)-100 );
+ __cpu_data_put_u16( dref, 7 );
+ isdiv( ( mpu_int * ) qref, ( mpu_int * ) rref, ( mpu_int * ) nref, ( mpu_int * ) dref, 2 );
+ memcpy( P( 16, 0x17a ), nref, 2 );
+ memcpy( P( 16, 0x17c ), dref, 2 );
+ I( &x, CPU_OP_ISDIV );
+ x.operand[0] = R( 16, 0x17e );
+ x.operand[1] = R( 16, 0x180 );
+ x.operand[2] = R( 16, 0x17a );
+ x.operand[3] = R( 16, 0x17c );
+ CHECK( __cpu_exec( &x ) == 0, "ISDIV" );
+ CHECK( memcmp( P( 16, 0x17e ), qref, 2 ) == 0 && memcmp( P( 16, 0x180 ), rref, 2 ) == 0, "ISDIV result" );
+ }
+ *P( 8, 0x188 ) = 0x80;
+ I( &x, CPU_OP_IBITSLEN );
+ x.operand[0] = R( 64, 0x190 );
+ x.operand[1] = R( 8, 0x188 );
+ CHECK( __cpu_exec( &x ) == 0, "IBITSLEN" );
+ CHECK( get64( 0x190 ) == 8, "IBITSLEN result" );
+
+ /* Real conversion/utility coverage. */
+ setreal( 64, 0x200, "-3.75" );
+ I( &x, CPU_OP_RABS );
+ x.operand[0] = R( 64, 0x208 );
+ x.operand[1] = R( 64, 0x200 );
+ CHECK( __cpu_exec( &x ) == 0, "RABS" );
+ {
+ mpu_real64_t ref;
+ memcpy( ref, P( 64, 0x200 ), 8 );
+ r_abs( ( mpu_real * ) ref, 8 );
+ CHECK( memcmp( P( 64, 0x208 ), ref, 8 ) == 0, "RABS result" );
+ }
+ I( &x, CPU_OP_RNEG );
+ x.operand[0] = R( 64, 0x210 );
+ x.operand[1] = R( 64, 0x200 );
+ CHECK( __cpu_exec( &x ) == 0, "RNEG" );
+ {
+ mpu_real64_t ref;
+ memcpy( ref, P( 64, 0x200 ), 8 );
+ r_neg( ( mpu_real * ) ref, 8 );
+ CHECK( memcmp( P( 64, 0x210 ), ref, 8 ) == 0, "RNEG result" );
+ }
+ setreal( 64, 0x218, "2.0" );
+ setreal( 64, 0x220, "3.0" );
+ I( &x, CPU_OP_RCMP );
+ x.operand[0] = R( 64, 0x228 );
+ x.operand[1] = R( 64, 0x218 );
+ x.operand[2] = R( 64, 0x220 );
+ CHECK( __cpu_exec( &x ) == 0, "RCMP" );
+ CHECK( geti64( 0x228 ) == -1, "RCMP result" );
+ I( &x, CPU_OP_RCPY );
+ x.operand[0] = R( 64, 0x230 );
+ x.operand[1] = R( 64, 0x218 );
+ CHECK( __cpu_exec( &x ) == 0, "RCPY" );
+ CHECK( memcmp( P( 64, 0x230 ), P( 64, 0x218 ), 8 ) == 0, "RCPY result" );
+ I( &x, CPU_OP_RCVT );
+ x.operand[0] = R( 128, 0x240 );
+ x.operand[1] = R( 64, 0x218 );
+ CHECK( __cpu_exec( &x ) == 0, "RCVT" );
+ {
+ mpu_real128_t ref;
+ r_cvt( ( mpu_real * ) ref, ( mpu_real * ) P( 64, 0x218 ), 16, 8 );
+ CHECK( memcmp( P( 128, 0x240 ), ref, 16 ) == 0, "RCVT result" );
+ }
+
+ /* floor/ceil/round, remain and the previously missing transcendental family. */
+ {
+ struct U
+ {
+ cpu_opcode op;
+ void( *fn )( mpu_real *, mpu_real *, int );
+ const char *n;
+ const char *src;
+ }
+ u[] =
+ {
+ {CPU_OP_RFLOOR,r_floor,"RFLOOR","3.75"},{CPU_OP_RCEIL,r_ceil,"RCEIL","3.25"},{CPU_OP_RROUND,r_round,"RROUND","3.60"},
+ {CPU_OP_RLOG1P,r_log1p,"RLOG1P","0.25"},{CPU_OP_REXPM1,r_expm1,"REXPM1","0.25"},
+ {CPU_OP_RASINH,r_asinh,"RASINH","0.25"},{CPU_OP_RACOSH,r_acosh,"RACOSH","2.0"},{CPU_OP_RATANH,r_atanh,"RATANH","0.25"}
+ };
+ unsigned k;
+ for( k = 0; k < sizeof( u ) / sizeof( u[0] ); k++ )
+ {
+ mpu_real64_t ref;
+ setreal( 64, 0x250, u[k].src );
+ u[k].fn( ( mpu_real * ) ref, ( mpu_real * ) P( 64, 0x250 ), 8 );
+ I( &x, u[k].op );
+ x.operand[0] = R( 64, 0x258 );
+ x.operand[1] = R( 64, 0x250 );
+ CHECK( __cpu_exec( &x ) == 0, u[k].n );
+ CHECK( memcmp( P( 64, 0x258 ), ref, 8 ) == 0, u[k].n );
+ }
+ }
+ setreal( 64, 0x260, "5.5" );
+ setreal( 64, 0x268, "2.0" );
+ {
+ mpu_real64_t ref;
+ r_remain( ( mpu_real * ) ref, ( mpu_real * ) P( 64, 0x260 ), ( mpu_real * ) P( 64, 0x268 ), 8 );
+ I( &x, CPU_OP_RREMAIN );
+ x.operand[0] = R( 64, 0x270 );
+ x.operand[1] = R( 64, 0x260 );
+ x.operand[2] = R( 64, 0x268 );
+ CHECK( __cpu_exec( &x ) == 0, "RREMAIN" );
+ CHECK( memcmp( P( 64, 0x270 ), ref, 8 ) == 0, "RREMAIN result" );
+ }
+
+ /* frexp/ldexp/logb/modf and integer<->real conversions. */
+ setreal( 64, 0x280, "6.5" );
+ I( &x, CPU_OP_RFREXP );
+ x.operand[0] = R( 64, 0x288 );
+ x.operand[1] = R( 64, 0x290 );
+ x.operand[2] = R( 64, 0x280 );
+ CHECK( __cpu_exec( &x ) == 0, "RFREXP" );
+ {
+ mpu_real64_t rs;
+ mpu_int64_t re;
+ r_frexp( ( mpu_real * ) rs, ( mpu_int * ) re, ( mpu_real * ) P( 64, 0x280 ), 8, 8 );
+ CHECK( memcmp( P( 64, 0x288 ), rs, 8 ) == 0 && memcmp( P( 64, 0x290 ), re, 8 ) == 0, "RFREXP result" );
+ }
+ I( &x, CPU_OP_RLDEXP );
+ x.operand[0] = R( 64, 0x298 );
+ x.operand[1] = R( 64, 0x288 );
+ x.operand[2] = R( 64, 0x290 );
+ CHECK( __cpu_exec( &x ) == 0, "RLDEXP" );
+ {
+ mpu_real64_t ref;
+ r_ldexp( ( mpu_real * ) ref, ( mpu_real * ) P( 64, 0x288 ), ( mpu_int * ) P( 64, 0x290 ), 8, 8 );
+ CHECK( memcmp( P( 64, 0x298 ), ref, 8 ) == 0, "RLDEXP result" );
+ }
+ I( &x, CPU_OP_RLOGB );
+ x.operand[0] = R( 64, 0x2a0 );
+ x.operand[1] = R( 64, 0x280 );
+ CHECK( __cpu_exec( &x ) == 0, "RLOGB" );
+ {
+ mpu_int64_t ri;
+ r_logb( ( mpu_int * ) ri, ( mpu_real * ) P( 64, 0x280 ), 8, 8 );
+ CHECK( memcmp( P( 64, 0x2a0 ), ri, 8 ) == 0, "RLOGB result" );
+ }
+ I( &x, CPU_OP_RMODF );
+ x.operand[0] = R( 64, 0x2a8 );
+ x.operand[1] = R( 64, 0x2b0 );
+ x.operand[2] = R( 64, 0x280 );
+ CHECK( __cpu_exec( &x ) == 0, "RMODF" );
+ {
+ mpu_real64_t rf, ri;
+ r_modf( ( mpu_real * ) rf, ( mpu_real * ) ri, ( mpu_real * ) P( 64, 0x280 ), 8 );
+ CHECK( memcmp( P( 64, 0x2a8 ), rf, 8 ) == 0 && memcmp( P( 64, 0x2b0 ), ri, 8 ) == 0, "RMODF result" );
+ }
+ put64( 0x2b8, 42 );
+ I( &x, CPU_OP_ITOR );
+ x.operand[0] = R( 64, 0x2c0 );
+ x.operand[1] = R( 64, 0x2b8 );
+ CHECK( __cpu_exec( &x ) == 0, "ITOR" );
+ {
+ mpu_real64_t ref;
+ ltor( ( mpu_real * ) ref, ( mpu_int * ) P( 64, 0x2b8 ), 8, 8 );
+ CHECK( memcmp( P( 64, 0x2c0 ), ref, 8 ) == 0, "ITOR result" );
+ }
+ put64( 0x2c8, (__mpu_uint64_t)1844674407370955161UL );
+ I( &x, CPU_OP_UITOR );
+ x.operand[0] = R( 128, 0x2d0 );
+ x.operand[1] = R( 64, 0x2c8 );
+ CHECK( __cpu_exec( &x ) == 0, "UITOR" );
+ {
+ mpu_real128_t ref;
+ ultor( ( mpu_real * ) ref, ( mpu_int * ) P( 64, 0x2c8 ), 16, 8 );
+ CHECK( memcmp( P( 128, 0x2d0 ), ref, 16 ) == 0, "UITOR result" );
+ }
+ I( &x, CPU_OP_RTOI );
+ x.operand[0] = R( 64, 0x2e0 );
+ x.operand[1] = R( 64, 0x2e8 );
+ x.operand[2] = R( 64, 0x280 );
+ CHECK( __cpu_exec( &x ) == 0, "RTOI" );
+ {
+ mpu_int64_t li;
+ mpu_real64_t fr;
+ rtol_frac( ( mpu_int * ) li, ( mpu_real * ) fr, ( mpu_real * ) P( 64, 0x280 ), 8, 8 );
+ CHECK( memcmp( P( 64, 0x2e0 ), li, 8 ) == 0 && memcmp( P( 64, 0x2e8 ), fr, 8 ) == 0, "RTOI result" );
+ }
+ setreal( 64, 0x2f0, "42.75" );
+ I( &x, CPU_OP_RTOUI );
+ x.operand[0] = R( 64, 0x2f8 );
+ x.operand[1] = R( 64, 0x300 );
+ x.operand[2] = R( 64, 0x2f0 );
+ CHECK( __cpu_exec( &x ) == 0, "RTOUI" );
+ {
+ mpu_int64_t li;
+ mpu_real64_t fr;
+ rtoul_frac( ( mpu_int * ) li, ( mpu_real * ) fr, ( mpu_real * ) P( 64, 0x2f0 ), 8, 8 );
+ CHECK( memcmp( P( 64, 0x2f8 ), li, 8 ) == 0 && memcmp( P( 64, 0x300 ), fr, 8 ) == 0, "RTOUI result" );
+ }
+
+ /* Complete RCONST generator audit: every LibMPU value generator exposed by RCONST. */
+ {
+ struct K
+ {
+ unsigned id;
+ void( *f )( mpu_real *, int );
+ const char *n;
+ }
+ ks[] =
+ {
+ {CPU_RCONST_ZERO,_m_zero,"ZERO"},{CPU_RCONST_HALF,_m_half,"HALF"},{CPU_RCONST_ONE,_m_one,"ONE"},
+ {CPU_RCONST_TWO,_m_two,"TWO"},{CPU_RCONST_TEN,_m_ten,"TEN"},{CPU_RCONST_MTEN,_m_mten,"MTEN"},{CPU_RCONST_32,_m_32,"32"},
+ {CPU_RCONST_PI,_m_PI,"PI"},{CPU_RCONST_E,_m_E,"E"},{CPU_RCONST_1_LN2,_m_1_ln2,"1_LN2"},{CPU_RCONST_LN2,_m_ln2,"LN2"},
+ {CPU_RCONST_1_LN10,_m_1_ln10,"1_LN10"},{CPU_RCONST_LN10,_m_ln10,"LN10"},{CPU_RCONST_1_LG2,_m_1_lg2,"1_LG2"},{CPU_RCONST_LG2,_m_lg2,"LG2"},
+ {CPU_RCONST_PI_2,_m_PI_2,"PI_2"},{CPU_RCONST_PI_3,_m_PI_3,"PI_3"},{CPU_RCONST_PI_4,_m_PI_4,"PI_4"},{CPU_RCONST_PI_5,_m_PI_5,"PI_5"},{CPU_RCONST_PI_6,_m_PI_6,"PI_6"},
+ {CPU_RCONST_1_PI,_m_1_PI,"1_PI"},{CPU_RCONST_2_PI,_m_2_PI,"2_PI"},{CPU_RCONST_3_PI,_m_3_PI,"3_PI"},{CPU_RCONST_4_PI,_m_4_PI,"4_PI"},{CPU_RCONST_5_PI,_m_5_PI,"5_PI"},
+ {CPU_RCONST_2PI,_m_2PI,"2PI"},{CPU_RCONST_3PI,_m_3PI,"3PI"},{CPU_RCONST_1_2PI,_m_1_2PI,"1_2PI"},{CPU_RCONST_1_3PI,_m_1_3PI,"1_3PI"},
+ {CPU_RCONST_1_4PI,_m_1_4PI,"1_4PI"},{CPU_RCONST_1_5PI,_m_1_5PI,"1_5PI"},{CPU_RCONST_1_6PI,_m_1_6PI,"1_6PI"},{CPU_RCONST_3PI_4,_m_3PI_4,"3PI_4"},
+ {CPU_RCONST_SQRTPI,_m_SQRTPI,"SQRTPI"},{CPU_RCONST_1_SQRTPI,_m_1_SQRTPI,"1_SQRTPI"},{CPU_RCONST_2_SQRTPI,_m_2_SQRTPI,"2_SQRTPI"},
+ {CPU_RCONST_SQRT2,_m_SQRT2,"SQRT2"},{CPU_RCONST_1_SQRT2,_m_1_SQRT2,"1_SQRT2"},{CPU_RCONST_SQRT3,_m_SQRT3,"SQRT3"},{CPU_RCONST_1_SQRT3,_m_1_SQRT3,"1_SQRT3"},
+ {CPU_RCONST_DEGREE,_m_DEGREE,"DEGREE"},{CPU_RCONST_1_DEGREE,_m_1_DEGREE,"1_DEGREE"},{CPU_RCONST_GOLDENRATIO,_m_GOLDENRATIO,"GOLDENRATIO"},
+ {CPU_RCONST_EULERGAMMA,_m_EULERGAMMA,"EULERGAMMA"},{CPU_RCONST_CATALAN,_m_CATALAN,"CATALAN"},
+ {CPU_RCONST_EPSILON,_real_epsilon,"EPSILON"},{CPU_RCONST_IND,_ind,"IND"}
+ };
+ unsigned k;
+ for( k = 0; k < sizeof( ks ) / sizeof( ks[0] ); k++ )
+ {
+ mpu_real64_t ref;
+ ks[k].f( ( mpu_real * ) ref, 8 );
+ I( &x, CPU_OP_RCONST );
+ x.operand[0] = R( 64, 0x420 );
+ x.operand[1].type = CPU_OPERAND_CONST8;
+ x.operand[1].value.const8 = (__mpu_uint8_t)ks[k].id;
+ CHECK( __cpu_exec( &x ) == 0, ks[k].n );
+ CHECK( memcmp( P( 64, 0x420 ), ref, 8 ) == 0, ks[k].n );
+ }
+ {
+ struct S
+ {
+ unsigned id;
+ void( *f )( mpu_real *, unsigned int, int );
+ unsigned sign;
+ const char *n;
+ }
+ sp[] =
+ {
+ {CPU_RCONST_PNAN,_nan,0,"PNAN"},{CPU_RCONST_NNAN,_nan,1,"NNAN"},
+ {CPU_RCONST_PNANMAX,_nan_max,0,"PNANMAX"},{CPU_RCONST_NNANMAX,_nan_max,1,"NNANMAX"},
+ {CPU_RCONST_PNANMIN,_nan_min,0,"PNANMIN"},{CPU_RCONST_NNANMIN,_nan_min,1,"NNANMIN"},
+ {CPU_RCONST_PINF,_inf,0,"PINF"},{CPU_RCONST_NINF,_inf,1,"NINF"},
+ {CPU_RCONST_PREALMIN,_real_min,0,"PREALMIN"},{CPU_RCONST_NREALMIN,_real_min,1,"NREALMIN"},
+ {CPU_RCONST_PREALMAX,_real_max,0,"PREALMAX"},{CPU_RCONST_NREALMAX,_real_max,1,"NREALMAX"},
+ {CPU_RCONST_PZERO,_signull,0,"PZERO"},{CPU_RCONST_NZERO,_signull,1,"NZERO"}
+ };
+ for( k = 0; k < sizeof( sp ) / sizeof( sp[0] ); k++ )
+ {
+ mpu_real64_t ref;
+ sp[k].f( ( mpu_real * ) ref, sp[k].sign, 8 );
+ I( &x, CPU_OP_RCONST );
+ x.operand[0] = R( 64, 0x428 );
+ x.operand[1].type = CPU_OPERAND_CONST8;
+ x.operand[1].value.const8 = (__mpu_uint8_t)sp[k].id;
+ CHECK( __cpu_exec( &x ) == 0, sp[k].n );
+ CHECK( memcmp( P( 64, 0x428 ), ref, 8 ) == 0, sp[k].n );
+ }
+ }
+ I( &x, CPU_OP_RCONST );
+ x.operand[0] = R( 64, 0x430 );
+ x.operand[1].type = CPU_OPERAND_CONST8;
+ x.operand[1].value.const8 = CPU_RCONST_NINF;
+ CHECK( __cpu_exec( &x ) == 0, "NINF predicate source" );
+ I( &x, CPU_OP_RISINF );
+ x.operand[0] = R( 64, 0x438 );
+ x.operand[1] = R( 64, 0x430 );
+ CHECK( __cpu_exec( &x ) == 0, "RISINF" );
+ CHECK( geti64( 0x438 ) == 1, "RISINF result" );
+ }
+
+ /* Complex extraction, generation, conversion, conjugation and predicates. */
+ setreal( 64, 0x500, "3.0" );
+ setreal( 64, 0x508, "4.0" );
+ I( &x, CPU_OP_CGEN );
+ x.operand[0] = R( 128, 0x510 );
+ x.operand[1] = R( 64, 0x500 );
+ x.operand[2] = R( 64, 0x508 );
+ CHECK( __cpu_exec( &x ) == 0, "CGEN" );
+ {
+ mpu_complex64_t ref;
+ c_gen_complex( ( mpu_complex * ) ref, ( mpu_real * ) P( 64, 0x500 ), ( mpu_real * ) P( 64, 0x508 ), 8 );
+ CHECK( memcmp( P( 128, 0x510 ), ref, 16 ) == 0, "CGEN result" );
+ }
+ I( &x, CPU_OP_CREAL );
+ x.operand[0] = R( 64, 0x520 );
+ x.operand[1] = R( 128, 0x510 );
+ CHECK( __cpu_exec( &x ) == 0, "CREAL" );
+ CHECK( memcmp( P( 64, 0x520 ), P( 64, 0x500 ), 8 ) == 0, "CREAL result" );
+ I( &x, CPU_OP_CIMAG );
+ x.operand[0] = R( 64, 0x528 );
+ x.operand[1] = R( 128, 0x510 );
+ CHECK( __cpu_exec( &x ) == 0, "CIMAG" );
+ CHECK( memcmp( P( 64, 0x528 ), P( 64, 0x508 ), 8 ) == 0, "CIMAG result" );
+ I( &x, CPU_OP_CRTOC );
+ x.operand[0] = R( 128, 0x530 );
+ x.operand[1] = R( 64, 0x500 );
+ CHECK( __cpu_exec( &x ) == 0, "CRTOC" );
+ {
+ mpu_complex64_t ref;
+ c_real_to_complex( ( mpu_complex * ) ref, ( mpu_real * ) P( 64, 0x500 ), 8 );
+ CHECK( memcmp( P( 128, 0x530 ), ref, 16 ) == 0, "CRTOC result" );
+ }
+ I( &x, CPU_OP_CCONJ );
+ x.operand[0] = R( 128, 0x540 );
+ x.operand[1] = R( 128, 0x510 );
+ CHECK( __cpu_exec( &x ) == 0, "CCONJ" );
+ {
+ mpu_complex64_t ref;
+ c_conj( ( mpu_complex * ) ref, ( mpu_complex * ) P( 128, 0x510 ), 8 );
+ CHECK( memcmp( P( 128, 0x540 ), ref, 16 ) == 0, "CCONJ result" );
+ }
+ I( &x, CPU_OP_CABS );
+ x.operand[0] = R( 64, 0x550 );
+ x.operand[1] = R( 128, 0x510 );
+ CHECK( __cpu_exec( &x ) == 0, "CABS" );
+ {
+ mpu_real64_t ref;
+ c_abs( ( mpu_real * ) ref, ( mpu_complex * ) P( 128, 0x510 ), 8 );
+ CHECK( memcmp( P( 64, 0x550 ), ref, 8 ) == 0, "CABS result" );
+ }
+ I( &x, CPU_OP_CNORM );
+ x.operand[0] = R( 64, 0x558 );
+ x.operand[1] = R( 128, 0x510 );
+ CHECK( __cpu_exec( &x ) == 0, "CNORM" );
+ {
+ mpu_real64_t ref;
+ c_norm( ( mpu_real * ) ref, ( mpu_complex * ) P( 128, 0x510 ), 8 );
+ CHECK( memcmp( P( 64, 0x558 ), ref, 8 ) == 0, "CNORM result" );
+ }
+ I( &x, CPU_OP_CARG );
+ x.operand[0] = R( 64, 0x560 );
+ x.operand[1] = R( 128, 0x510 );
+ CHECK( __cpu_exec( &x ) == 0, "CARG" );
+ {
+ mpu_real64_t ref;
+ c_arg( ( mpu_real * ) ref, ( mpu_complex * ) P( 128, 0x510 ), 8 );
+ CHECK( memcmp( P( 64, 0x560 ), ref, 8 ) == 0, "CARG result" );
+ }
+ I( &x, CPU_OP_CISEQ );
+ x.operand[0] = R( 64, 0x568 );
+ x.operand[1] = R( 128, 0x510 );
+ x.operand[2] = R( 128, 0x510 );
+ CHECK( __cpu_exec( &x ) == 0, "CISEQ" );
+ CHECK( geti64( 0x568 ) == 1, "CISEQ result" );
+ I( &x, CPU_OP_CISNE );
+ x.operand[0] = R( 64, 0x570 );
+ x.operand[1] = R( 128, 0x510 );
+ x.operand[2] = R( 128, 0x540 );
+ CHECK( __cpu_exec( &x ) == 0, "CISNE" );
+ CHECK( geti64( 0x570 ) == 1, "CISNE result" );
+ I( &x, CPU_OP_CCPY );
+ x.operand[0] = R( 128, 0x580 );
+ x.operand[1] = R( 128, 0x510 );
+ CHECK( __cpu_exec( &x ) == 0, "CCPY" );
+ CHECK( memcmp( P( 128, 0x580 ), P( 128, 0x510 ), 16 ) == 0, "CCPY result" );
+ setreal( 64, 0x590, "5.0" );
+ setreal( 64, 0x598, "0.5" );
+ I( &x, CPU_OP_CPOLAR );
+ x.operand[0] = R( 128, 0x5a0 );
+ x.operand[1] = R( 64, 0x590 );
+ x.operand[2] = R( 64, 0x598 );
+ CHECK( __cpu_exec( &x ) == 0, "CPOLAR" );
+ {
+ mpu_complex64_t ref;
+ c_polar( ( mpu_complex * ) ref, ( mpu_real * ) P( 64, 0x590 ), ( mpu_real * ) P( 64, 0x598 ), 8 );
+ CHECK( memcmp( P( 128, 0x5a0 ), ref, 16 ) == 0, "CPOLAR result" );
+ }
+ I( &x, CPU_OP_CCVT );
+ x.operand[0] = R( 256, 0x5c0 );
+ x.operand[1] = R( 128, 0x510 );
+ CHECK( __cpu_exec( &x ) == 0, "CCVT" );
+ {
+ mpu_complex128_t ref;
+ c_cvt( ( mpu_complex * ) ref, ( mpu_complex * ) P( 128, 0x510 ), 16, 8 );
+ CHECK( memcmp( P( 256, 0x5c0 ), ref, 32 ) == 0, "CCVT result" );
+ }
+
+ __cpu_free_context();
+ if( failures )
+ {
+ fprintf( stderr, "libcpu LibMPU coverage regression: %d failure(s)\n", failures );
+ return( 1 );
+ }
+ puts( "libcpu extended LibMPU instruction coverage: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-mpu-ops-runner.sh b/tests/tst-mpu-ops-runner.sh
new file mode 100755
index 0000000..32bad1d
--- /dev/null
+++ b/tests/tst-mpu-ops-runner.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${TEST_SRCDIR:?}"
+base="${TMPDIR:-/tmp}/libcpu-mpu-ops-$$"
+trap 'rm -f "$base.s" "$base.o" "$base.melf"' EXIT HUP INT TERM
+"$CPU_AS" -o "$base.o" "$TEST_SRCDIR/fixtures/mpu-ops.s" >/dev/null
+"$CPU_LD" --stack-size=512 -o "$base.melf" "$base.o" >/dev/null
+out=`"$CPU_RUN" --int-reg=r64@0x00020 "$base.melf"`
+test "x$out" = "x42"
diff --git a/tests/tst-needed-runtime-runner.sh b/tests/tst-needed-runtime-runner.sh
new file mode 100755
index 0000000..1119ff9
--- /dev/null
+++ b/tests/tst-needed-runtime-runner.sh
@@ -0,0 +1,193 @@
+#!/bin/sh
+set -eu
+
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${CPU_READELF:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-needed-runtime-$$
+mkdir -p "$TMP/lib"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+
+cat > "$TMP/main.s" <<'EOS'
+.global _life
+.extern afoo
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL afoo
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+
+cat > "$TMP/a.s" <<'EOS'
+.global afoo
+.extern bfoo
+.text
+afoo:
+ CALL bfoo
+ RET
+EOS
+
+cat > "$TMP/b.s" <<'EOS'
+.global bfoo
+.text
+bfoo:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/main.o" "$TMP/main.s"
+"$CPU_AS" -o "$TMP/a.o" "$TMP/a.s"
+"$CPU_AS" -o "$TMP/b.o" "$TMP/b.s"
+
+# A real recursive graph: main -> liba.so.1 -> libb.so.1.
+"$CPU_LD" --shared --soname=libb.so.1 \
+ -o "$TMP/lib/libb.so.1.0.0" "$TMP/b.o"
+ln -s libb.so.1.0.0 "$TMP/lib/libb.so.1"
+ln -s libb.so.1 "$TMP/lib/libb.so"
+
+"$CPU_LD" --shared --soname=liba.so.1 \
+ -L"$TMP/lib" -lb -o "$TMP/lib/liba.so.1.0.0" "$TMP/a.o"
+ln -s liba.so.1.0.0 "$TMP/lib/liba.so.1"
+ln -s liba.so.1 "$TMP/lib/liba.so"
+
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/lib" -la -o "$TMP/main" "$TMP/main.o"
+
+"$CPU_READELF" -d "$TMP/main" | grep -q 'NEEDED.*\[liba.so.1\]'
+"$CPU_READELF" -d "$TMP/lib/liba.so.1.0.0" | \
+ grep -q 'NEEDED.*\[libb.so.1\]'
+[ "`"$CPU_RUN" --library-path="$TMP/lib" \
+ --int-reg=r64@0x00018 "$TMP/main"`" = 42 ]
+
+# A missing transitive dependency is reported by SONAME, before process load.
+mv "$TMP/lib/libb.so.1" "$TMP/lib/libb.so.1.save"
+if "$CPU_RUN" --library-path="$TMP/lib" "$TMP/main" \
+ >"$TMP/missing.out" 2>"$TMP/missing.err"; then
+ echo "missing transitive dependency unexpectedly loaded" >&2
+ exit 1
+fi
+grep -q "cannot resolve DT_NEEDED 'libb.so.1'" "$TMP/missing.err"
+mv "$TMP/lib/libb.so.1.save" "$TMP/lib/libb.so.1"
+
+# The resolved filename is not trusted: its internal SONAME must be the exact
+# DT_NEEDED identity requested by the dependent object.
+cat > "$TMP/wrong.s" <<'EOS'
+.global afoo
+.text
+afoo:
+ RET
+EOS
+"$CPU_AS" -o "$TMP/wrong.o" "$TMP/wrong.s"
+"$CPU_LD" --shared --soname=libwrong.so.9 \
+ -o "$TMP/lib/libwrong.so.9.0.0" "$TMP/wrong.o"
+rm "$TMP/lib/liba.so.1"
+ln -s libwrong.so.9.0.0 "$TMP/lib/liba.so.1"
+if "$CPU_RUN" --library-path="$TMP/lib" "$TMP/main" \
+ >"$TMP/mismatch.out" 2>"$TMP/mismatch.err"; then
+ echo "SONAME mismatch unexpectedly loaded" >&2
+ exit 1
+fi
+grep -q "DT_NEEDED 'liba.so.1' resolved to SONAME 'libwrong.so.9'" \
+ "$TMP/mismatch.err"
+rm "$TMP/lib/liba.so.1"
+ln -s liba.so.1.0.0 "$TMP/lib/liba.so.1"
+
+# Dependency cycles are legal graph edges. Registration by SONAME before
+# recursive descent prevents A -> B -> A from recursing or loading twice.
+cat > "$TMP/cycle-main.s" <<'EOS'
+.global _life
+.extern cycle_a
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL cycle_a
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+cat > "$TMP/cycle-a.s" <<'EOS'
+.global cycle_a
+.text
+cycle_a:
+ LDI r64@0x00008, 4
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+cat > "$TMP/cycle-b.s" <<'EOS'
+.global cycle_b
+.text
+cycle_b:
+ RET
+EOS
+"$CPU_AS" -o "$TMP/cycle-main.o" "$TMP/cycle-main.s"
+"$CPU_AS" -o "$TMP/cycle-a.o" "$TMP/cycle-a.s"
+"$CPU_AS" -o "$TMP/cycle-b.o" "$TMP/cycle-b.s"
+
+# Bootstrap one side of the cycle, build B -> A, then rebuild A -> B.
+"$CPU_LD" --shared --soname=libcyclea.so.1 \
+ -o "$TMP/lib/libcyclea.so.1.0.0" "$TMP/cycle-a.o"
+ln -s libcyclea.so.1.0.0 "$TMP/lib/libcyclea.so.1"
+ln -s libcyclea.so.1 "$TMP/lib/libcyclea.so"
+"$CPU_LD" --shared --soname=libcycleb.so.1 \
+ -L"$TMP/lib" -lcyclea \
+ -o "$TMP/lib/libcycleb.so.1.0.0" "$TMP/cycle-b.o"
+ln -s libcycleb.so.1.0.0 "$TMP/lib/libcycleb.so.1"
+ln -s libcycleb.so.1 "$TMP/lib/libcycleb.so"
+"$CPU_LD" --shared --soname=libcyclea.so.1 \
+ -L"$TMP/lib" -lcycleb \
+ -o "$TMP/lib/libcyclea.so.1.0.0" "$TMP/cycle-a.o"
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/lib" -lcyclea -o "$TMP/cycle-main" "$TMP/cycle-main.o"
+[ "`"$CPU_RUN" --library-path="$TMP/lib" \
+ --int-reg=r64@0x00018 "$TMP/cycle-main"`" = 44 ]
+
+# Explicit preload remains the override mechanism. An ABI-v3 preload with the
+# same SONAME satisfies the normal dependency and keeps the established
+# explicit-preload priority.
+cat > "$TMP/direct-main.s" <<'EOS'
+.global _life
+.extern foo
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL foo
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+cat > "$TMP/foo.s" <<'EOS'
+.global foo
+.text
+foo:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+cat > "$TMP/override.s" <<'EOS'
+.global foo
+.text
+foo:
+ LDI r64@0x00008, 3
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+"$CPU_AS" -o "$TMP/direct-main.o" "$TMP/direct-main.s"
+"$CPU_AS" -o "$TMP/foo.o" "$TMP/foo.s"
+"$CPU_AS" -o "$TMP/override.o" "$TMP/override.s"
+"$CPU_LD" --shared --soname=libfoo.so.1 \
+ -o "$TMP/lib/libfoo.so.1.0.0" "$TMP/foo.o"
+ln -s libfoo.so.1.0.0 "$TMP/lib/libfoo.so.1"
+ln -s libfoo.so.1 "$TMP/lib/libfoo.so"
+"$CPU_LD" --shared --soname=libfoo.so.1 \
+ -o "$TMP/override.so" "$TMP/override.o"
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/lib" -lfoo -o "$TMP/direct-main" "$TMP/direct-main.o"
+[ "`"$CPU_RUN" --library-path="$TMP/lib" \
+ --int-reg=r64@0x00018 "$TMP/direct-main"`" = 42 ]
+[ "`"$CPU_RUN" --library-path="$TMP/lib" \
+ --ldpreload="$TMP/override.so" \
+ --int-reg=r64@0x00018 "$TMP/direct-main"`" = 43 ]
+
+exit 0
diff --git a/tests/tst-nm-runner.sh b/tests/tst-nm-runner.sh
new file mode 100755
index 0000000..573edbd
--- /dev/null
+++ b/tests/tst-nm-runner.sh
@@ -0,0 +1,167 @@
+#!/bin/sh
+set -eu
+
+: "${PACKAGE_VERSION:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-nm-$$
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+mkdir -p "$TMP"
+
+cat > "$TMP/symbols.s" <<'EOS'
+.global zeta
+.global data_global
+.global bss_global
+.extern missing
+.weak weak_defined
+.weak weak_missing
+
+.text
+local_text:
+ RET
+zeta:
+ RET
+weak_defined:
+ RET
+
+.data
+data_global:
+ .integer 64, 7
+
+.bss
+bss_global:
+ .zero 16
+EOS
+
+cat > "$TMP/main.s" <<'EOS'
+.global _start
+.global _life
+.extern zeta
+.text
+_start_life:
+ CALL zeta
+ LDI r32@0x7ffc, 0
+ RET
+EOS
+
+cat > "$TMP/dyn.s" <<'EOS'
+.global dyn_alpha
+.global dyn_data
+.weak dyn_weak
+.text
+dyn_alpha:
+ RET
+dyn_weak:
+ RET
+.data
+dyn_data:
+ .integer 64, 1
+EOS
+
+cat > "$TMP/a.s" <<'EOS'
+.global archive_foo
+.text
+archive_foo:
+ RET
+EOS
+
+cat > "$TMP/b.s" <<'EOS'
+.global archive_bar
+.text
+archive_bar:
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/symbols.o" "$TMP/symbols.s"
+"$CPU_AS" -o "$TMP/main.o" "$TMP/main.s"
+"$CPU_AS" -o "$TMP/dyn.o" "$TMP/dyn.s"
+"$CPU_AS" -o "$TMP/a.o" "$TMP/a.s"
+"$CPU_AS" -o "$TMP/b.o" "$TMP/b.s"
+
+"$CPU_LD" --stack-size=4096 --no-heap -o "$TMP/program" "$TMP/main.o" "$TMP/symbols.o"
+"$CPU_LD" --shared --soname=libnm.so.1 -o "$TMP/libnm.so.1.0.0" "$TMP/dyn.o"
+"$CPU_AR" rcs "$TMP/libnm.a" "$TMP/a.o" "$TMP/b.o"
+
+"$CPU_NM" "$TMP/symbols.o" > "$TMP/default.txt"
+grep -Eq ' T zeta$' "$TMP/default.txt"
+grep -Eq ' t local_text$' "$TMP/default.txt"
+grep -Eq ' D data_global$' "$TMP/default.txt"
+grep -Eq ' B bss_global$' "$TMP/default.txt"
+grep -Eq ' U missing$' "$TMP/default.txt"
+grep -Eq ' W weak_defined$' "$TMP/default.txt"
+grep -Eq ' w weak_missing$' "$TMP/default.txt"
+
+# Default order is lexical by symbol name.
+awk '{print $NF}' "$TMP/default.txt" > "$TMP/names.txt"
+LC_ALL=C sort "$TMP/names.txt" > "$TMP/names.sorted"
+cmp "$TMP/names.txt" "$TMP/names.sorted"
+
+"$CPU_NM" -g "$TMP/symbols.o" > "$TMP/extern.txt"
+if grep -q 'local_text$' "$TMP/extern.txt"; then
+ echo "mcpu-nm -g unexpectedly displayed a local symbol" >&2
+ exit 1
+fi
+grep -q 'zeta$' "$TMP/extern.txt"
+
+"$CPU_NM" -u "$TMP/symbols.o" > "$TMP/undef.txt"
+grep -Eq ' U missing$' "$TMP/undef.txt"
+grep -Eq ' w weak_missing$' "$TMP/undef.txt"
+if grep -q 'zeta$' "$TMP/undef.txt"; then
+ echo "mcpu-nm -u unexpectedly displayed a defined symbol" >&2
+ exit 1
+fi
+
+"$CPU_NM" -n "$TMP/symbols.o" > "$TMP/numeric.txt"
+# The first two defined text symbols are ordered by value, not alphabetically.
+line_local=`grep -n ' local_text$' "$TMP/numeric.txt" | cut -d: -f1`
+line_zeta=`grep -n ' zeta$' "$TMP/numeric.txt" | cut -d: -f1`
+test "$line_local" -lt "$line_zeta"
+
+"$CPU_NM" -r "$TMP/symbols.o" > "$TMP/reverse.txt"
+first_name=`awk 'NF { print $NF; exit }' "$TMP/reverse.txt"`
+test "$first_name" = zeta
+
+"$CPU_NM" -p "$TMP/symbols.o" > "$TMP/nosort.txt"
+test -s "$TMP/nosort.txt"
+"$CPU_NM" -a "$TMP/symbols.o" > "$TMP/all.txt"
+test -s "$TMP/all.txt"
+
+"$CPU_NM" "$TMP/program" > "$TMP/program.txt"
+# ABI-v1 ET_EXEC has no retained .symtab; successful inspection is the contract.
+test ! -s "$TMP/program.txt"
+
+"$CPU_NM" -D "$TMP/libnm.so.1.0.0" > "$TMP/dynamic.txt"
+grep -q ' dyn_alpha$' "$TMP/dynamic.txt"
+grep -q ' dyn_data$' "$TMP/dynamic.txt"
+grep -Eq ' W dyn_weak$' "$TMP/dynamic.txt"
+
+"$CPU_NM" "$TMP/libnm.a" > "$TMP/archive.txt"
+grep -F "$TMP/libnm.a:a.o:" "$TMP/archive.txt" >/dev/null
+grep -F "$TMP/libnm.a:b.o:" "$TMP/archive.txt" >/dev/null
+grep -q ' archive_foo$' "$TMP/archive.txt"
+grep -q ' archive_bar$' "$TMP/archive.txt"
+
+"$CPU_NM" -A "$TMP/libnm.a" > "$TMP/archive-prefix.txt"
+grep -F "$TMP/libnm.a:a.o: " "$TMP/archive-prefix.txt" | grep -q ' archive_foo$'
+grep -F "$TMP/libnm.a:b.o: " "$TMP/archive-prefix.txt" | grep -q ' archive_bar$'
+if grep -Fx "$TMP/libnm.a:a.o:" "$TMP/archive-prefix.txt" >/dev/null; then
+ echo "mcpu-nm -A unexpectedly emitted archive member headings" >&2
+ exit 1
+fi
+
+"$CPU_NM" "$TMP/a.o" "$TMP/b.o" > "$TMP/multi.txt"
+grep -Fx "$TMP/a.o:" "$TMP/multi.txt" >/dev/null
+grep -Fx "$TMP/b.o:" "$TMP/multi.txt" >/dev/null
+
+"$CPU_NM" -A "$TMP/a.o" > "$TMP/file-prefix.txt"
+grep -F "$TMP/a.o: " "$TMP/file-prefix.txt" | grep -q ' archive_foo$'
+
+dd if=/dev/zero of="$TMP/bad" bs=256 count=1 2>/dev/null
+if "$CPU_NM" "$TMP/bad" > /dev/null 2> "$TMP/bad.err"; then
+ echo "mcpu-nm unexpectedly accepted a non-MELF file" >&2
+ exit 1
+fi
+grep -q 'not a MELF file' "$TMP/bad.err"
+
+"$CPU_NM" --help | grep -q -- '--dynamic'
+"$CPU_NM" --help | grep -q -- '--print-file-name'
+"$CPU_NM" --version | grep -Fq "$PACKAGE_VERSION"
diff --git a/tests/tst-objcopy-runner.sh b/tests/tst-objcopy-runner.sh
new file mode 100755
index 0000000..96b501b
--- /dev/null
+++ b/tests/tst-objcopy-runner.sh
@@ -0,0 +1,160 @@
+#!/bin/sh
+set -eu
+
+: "${PACKAGE_VERSION:?}"
+
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${CPU_READELF:?}"
+: "${CPU_AR:?}" "${CPU_NM:?}" "${CPU_OBJCOPY:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-objcopy-$$
+mkdir -p "$TMP"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+
+cat > "$TMP/main.s" <<'EOS'
+.global _life
+.text
+_start_life:
+ LDI r64@0x00000, 42
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+
+cat > "$TMP/add-one.s" <<'EOS'
+.global add_one
+.text
+add_one:
+ LDI r64@0x00008, 1
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/main.o" "$TMP/main.s"
+"$CPU_AS" -o "$TMP/add-one.o" "$TMP/add-one.s"
+"$CPU_LD" --stack-size=4096 --no-heap -o "$TMP/main" "$TMP/main.o"
+"$CPU_AR" rcs "$TMP/libadd.a" "$TMP/add-one.o"
+
+# Plain MELF -> MELF copying is byte-for-byte when no edit is requested.
+"$CPU_OBJCOPY" "$TMP/main" "$TMP/main.copy"
+cmp "$TMP/main" "$TMP/main.copy"
+
+printf 'metadata-one\n' > "$TMP/note-one"
+printf 'metadata-two\n' > "$TMP/note-two"
+
+# Add non-ALLOC metadata without touching the executable image.
+"$CPU_OBJCOPY" --add-section=.note.test="$TMP/note-one" \
+ "$TMP/main" "$TMP/main.note"
+"$CPU_READELF" -S "$TMP/main.note" | grep -q '\.note\.test'
+test "$("$CPU_RUN" --int-reg=r64@0x00000 "$TMP/main.note")" = 42
+
+# Dump is read-only and may inspect runtime sections as well as metadata.
+"$CPU_OBJCOPY" --dump-section=.note.test="$TMP/note.dump" \
+ "$TMP/main.note" "$TMP/main.dump-copy"
+cmp "$TMP/note-one" "$TMP/note.dump"
+"$CPU_OBJCOPY" --dump-section=.text="$TMP/text.before" \
+ "$TMP/main" "$TMP/main.text-copy"
+"$CPU_OBJCOPY" --dump-section=.text="$TMP/text.after" \
+ "$TMP/main.note" "$TMP/main.note-copy"
+cmp "$TMP/text.before" "$TMP/text.after"
+
+# Update and rename apply only to safe non-ALLOC PROGBITS metadata.
+"$CPU_OBJCOPY" --update-section=.note.test="$TMP/note-two" \
+ "$TMP/main.note" "$TMP/main.updated"
+"$CPU_OBJCOPY" --dump-section=.note.test="$TMP/updated.dump" \
+ "$TMP/main.updated" "$TMP/main.updated-copy"
+cmp "$TMP/note-two" "$TMP/updated.dump"
+
+"$CPU_OBJCOPY" --rename-section=.note.test=.note.renamed \
+ "$TMP/main.updated" "$TMP/main.renamed"
+"$CPU_READELF" -S "$TMP/main.renamed" | grep -q '\.note\.renamed'
+if "$CPU_READELF" -S "$TMP/main.renamed" | grep -q '\.note\.test'; then
+ echo "old section name survived mcpu-objcopy rename" >&2
+ exit 1
+fi
+
+# Safe metadata can be removed; runtime behavior stays unchanged.
+"$CPU_OBJCOPY" -R .note.renamed "$TMP/main.renamed" "$TMP/main.removed"
+if "$CPU_READELF" -S "$TMP/main.removed" | grep -q '\.note\.renamed'; then
+ echo "removed metadata section survived mcpu-objcopy" >&2
+ exit 1
+fi
+test "$("$CPU_RUN" --int-reg=r64@0x00000 "$TMP/main.removed")" = 42
+
+# Runtime-visible and structural sections are protected.
+if "$CPU_OBJCOPY" -R .text "$TMP/main" "$TMP/bad-remove" >/dev/null 2>&1; then
+ echo "mcpu-objcopy removed .text" >&2
+ exit 1
+fi
+if "$CPU_OBJCOPY" --update-section=.text="$TMP/note-one" \
+ "$TMP/main" "$TMP/bad-update" >/dev/null 2>&1; then
+ echo "mcpu-objcopy updated .text" >&2
+ exit 1
+fi
+if "$CPU_OBJCOPY" --rename-section=.text=.foo \
+ "$TMP/main" "$TMP/bad-rename" >/dev/null 2>&1; then
+ echo "mcpu-objcopy renamed .text" >&2
+ exit 1
+fi
+if "$CPU_OBJCOPY" --add-section=.text="$TMP/note-one" \
+ "$TMP/main" "$TMP/bad-duplicate" >/dev/null 2>&1; then
+ echo "mcpu-objcopy added duplicate .text" >&2
+ exit 1
+fi
+
+# --only-section is strict: an executable cannot lose its runtime image.
+if "$CPU_OBJCOPY" -j .note.test "$TMP/main.note" "$TMP/bad-only" \
+ >/dev/null 2>&1; then
+ echo "mcpu-objcopy created an incomplete ET_EXEC with -j" >&2
+ exit 1
+fi
+
+# ET_REL link semantics are protected, but optional metadata may be filtered.
+"$CPU_OBJCOPY" --add-section=.note.rel="$TMP/note-one" \
+ "$TMP/main.o" "$TMP/main.note.o"
+"$CPU_OBJCOPY" \
+ -j .text -j .symtab -j .strtab -j .mcpu.target -j .shstrtab \
+ "$TMP/main.note.o" "$TMP/main.filtered.o"
+if "$CPU_READELF" -S "$TMP/main.filtered.o" | grep -q '\.note\.rel'; then
+ echo "--only-section failed to remove optional ET_REL metadata" >&2
+ exit 1
+fi
+"$CPU_LD" --stack-size=4096 --no-heap -o "$TMP/main.filtered" \
+ "$TMP/main.filtered.o"
+test "$("$CPU_RUN" --int-reg=r64@0x00000 "$TMP/main.filtered")" = 42
+
+if "$CPU_OBJCOPY" -R .symtab "$TMP/main.o" "$TMP/bad-rel" \
+ >/dev/null 2>&1; then
+ echo "mcpu-objcopy removed ET_REL .symtab" >&2
+ exit 1
+fi
+
+# Archives are transformed member-by-member and retain their symbol index.
+"$CPU_OBJCOPY" --add-section=.note.arc="$TMP/note-one" \
+ "$TMP/libadd.a" "$TMP/libadd.note.a"
+"$CPU_NM" -A "$TMP/libadd.note.a" | grep -q 'add_one'
+mkdir "$TMP/extract"
+(
+ cd "$TMP/extract"
+ "$CPU_AR" x ../libadd.note.a
+ "$CPU_READELF" -S add-one.o | grep -q '\.note\.arc'
+)
+"$CPU_OBJCOPY" -R .note.arc "$TMP/libadd.note.a" "$TMP/libadd.clean.a"
+"$CPU_NM" -A "$TMP/libadd.clean.a" | grep -q 'add_one'
+if "$CPU_OBJCOPY" --dump-section=.text="$TMP/archive.dump" \
+ "$TMP/libadd.a" "$TMP/libadd.dump.a" >/dev/null 2>&1; then
+ echo "mcpu-objcopy accepted ambiguous archive --dump-section" >&2
+ exit 1
+fi
+
+"$CPU_OBJCOPY" --help | grep -q -- '--add-section=NAME=FILE'
+"$CPU_OBJCOPY" --version | grep -Fq "$PACKAGE_VERSION"
+
+if "$CPU_OBJCOPY" "$TMP/main" >/dev/null 2>&1; then
+ echo "mcpu-objcopy unexpectedly accepted a missing output file" >&2
+ exit 1
+fi
+if "$CPU_OBJCOPY" "$TMP/main" "$TMP/main" >/dev/null 2>&1; then
+ echo "mcpu-objcopy unexpectedly accepted in-place operation" >&2
+ exit 1
+fi
+
+exit 0
diff --git a/tests/tst-objdump-runner.sh b/tests/tst-objdump-runner.sh
new file mode 100755
index 0000000..94142eb
--- /dev/null
+++ b/tests/tst-objdump-runner.sh
@@ -0,0 +1,157 @@
+#!/bin/sh
+set -eu
+
+: "${PACKAGE_VERSION:?}"
+
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_AR:?}" "${CPU_OBJDUMP:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-objdump-$$
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+mkdir -p "$TMP"
+
+cat > "$TMP/unit.s" <<'EOS'
+.global entry
+.global data_global
+.extern external_function
+.text
+entry:
+ SETF CF
+ CLRF AF
+ CLRCPUF
+ CLRMPUF
+ LDI r64@0x00000, 41
+ CALL local_helper
+ CALL external_function
+ RET
+local_helper:
+ LDI r64@0x00008, 1
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+.data
+data_global:
+ .integer 64, 7
+data_pointer:
+ .ptr data_global
+EOS
+
+cat > "$TMP/dyn.s" <<'EOS'
+.global dyn_api
+.global dyn_data
+.text
+dyn_api:
+ CALL local_dyn
+ RET
+local_dyn:
+ LDI r64@0x00008, 2
+ RET
+.data
+dyn_data:
+ .integer 64, 9
+EOS
+
+cat > "$TMP/a.s" <<'EOS'
+.global archive_a
+.text
+archive_a:
+ RET
+EOS
+
+cat > "$TMP/b.s" <<'EOS'
+.global archive_b
+.text
+archive_b:
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/unit.o" "$TMP/unit.s"
+"$CPU_AS" -o "$TMP/dyn.o" "$TMP/dyn.s"
+"$CPU_AS" -o "$TMP/a.o" "$TMP/a.s"
+"$CPU_AS" -o "$TMP/b.o" "$TMP/b.s"
+"$CPU_LD" --shared --soname=libobjdump.so.1 -o "$TMP/libobjdump.so" "$TMP/dyn.o"
+"$CPU_AR" rcs "$TMP/libobjdump.a" "$TMP/a.o" "$TMP/b.o"
+
+"$CPU_OBJDUMP" -h "$TMP/unit.o" > "$TMP/headers.txt"
+grep -q '^Sections:$' "$TMP/headers.txt"
+grep -q '\.text' "$TMP/headers.txt"
+grep -q '\.data' "$TMP/headers.txt"
+grep -q 'PROGBITS.*CODE' "$TMP/headers.txt"
+
+"$CPU_OBJDUMP" -t "$TMP/unit.o" > "$TMP/symbols.txt"
+grep -q '^SYMBOL TABLE:$' "$TMP/symbols.txt"
+grep -q ' entry$' "$TMP/symbols.txt"
+grep -q ' local_helper$' "$TMP/symbols.txt"
+grep -q ' data_global$' "$TMP/symbols.txt"
+grep -q ' external_function$' "$TMP/symbols.txt"
+
+"$CPU_OBJDUMP" -r "$TMP/unit.o" > "$TMP/relocs.txt"
+grep -q 'RELOCATION RECORDS FOR \[.text\]' "$TMP/relocs.txt"
+grep -q 'R_MCPU_CODE64.*local_helper' "$TMP/relocs.txt"
+grep -q 'R_MCPU_CODE64.*external_function' "$TMP/relocs.txt"
+
+"$CPU_OBJDUMP" -d -r "$TMP/unit.o" > "$TMP/disasm.txt"
+grep -q 'Disassembly of section .text:' "$TMP/disasm.txt"
+grep -q '<entry>:' "$TMP/disasm.txt"
+grep -q '<local_helper>:' "$TMP/disasm.txt"
+grep -q 'SETF.*CF' "$TMP/disasm.txt"
+grep -q 'CLRF.*AF' "$TMP/disasm.txt"
+grep -q 'CLRCPUF' "$TMP/disasm.txt"
+grep -q 'CLRMPUF' "$TMP/disasm.txt"
+grep -q 'LDI.*r64@0x00000, 41' "$TMP/disasm.txt"
+grep -q 'CALL.*<local_helper>' "$TMP/disasm.txt"
+grep -q 'CALL.*<external_function>' "$TMP/disasm.txt"
+grep -q 'R_MCPU_CODE64.*local_helper' "$TMP/disasm.txt"
+# -r remains complete with -d: non-executable-section relocations are
+# printed separately while .text relocations stay next to instructions.
+grep -q 'RELOCATION RECORDS FOR \[.data\]' "$TMP/disasm.txt"
+grep -q 'R_MCPU_PTR64_DATA.*data_global' "$TMP/disasm.txt"
+
+"$CPU_OBJDUMP" -s -j .data "$TMP/unit.o" > "$TMP/data.txt"
+grep -q 'Contents of section .data:' "$TMP/data.txt"
+if grep -q 'Contents of section .text:' "$TMP/data.txt"; then
+ echo "mcpu-objdump -j .data unexpectedly displayed .text contents" >&2
+ exit 1
+fi
+
+"$CPU_OBJDUMP" -D -j .data "$TMP/unit.o" > "$TMP/all-data.txt"
+grep -q 'Disassembly of section .data:' "$TMP/all-data.txt"
+
+"$CPU_OBJDUMP" -T "$TMP/libobjdump.so" > "$TMP/dynamic.txt"
+grep -q '^DYNAMIC SYMBOL TABLE:$' "$TMP/dynamic.txt"
+grep -q ' dyn_api$' "$TMP/dynamic.txt"
+grep -q ' dyn_data$' "$TMP/dynamic.txt"
+
+"$CPU_OBJDUMP" -d -r "$TMP/libobjdump.so" > "$TMP/dynamic-disasm.txt"
+grep -q '<dyn_api>:' "$TMP/dynamic-disasm.txt"
+grep -q 'CALL.*<.L\$0\$0>' "$TMP/dynamic-disasm.txt"
+grep -q 'R_MCPU_CODE64.*.L\$0\$0' "$TMP/dynamic-disasm.txt"
+
+"$CPU_OBJDUMP" -t "$TMP/libobjdump.a" > "$TMP/archive.txt"
+grep -F "$TMP/libobjdump.a(a.o):" "$TMP/archive.txt" >/dev/null
+grep -F "$TMP/libobjdump.a(b.o):" "$TMP/archive.txt" >/dev/null
+grep -q ' archive_a$' "$TMP/archive.txt"
+grep -q ' archive_b$' "$TMP/archive.txt"
+
+# Grouped short options and attached -j argument are accepted.
+"$CPU_OBJDUMP" -hdr -j.text "$TMP/unit.o" > "$TMP/grouped.txt"
+grep -q 'Sections:' "$TMP/grouped.txt"
+grep -q 'Disassembly of section .text:' "$TMP/grouped.txt"
+grep -q 'R_MCPU_CODE64' "$TMP/grouped.txt"
+
+# Missing/invalid input remains a hard error.
+dd if=/dev/zero of="$TMP/bad" bs=256 count=1 2>/dev/null
+if "$CPU_OBJDUMP" -h "$TMP/bad" > /dev/null 2> "$TMP/bad.err"; then
+ echo "mcpu-objdump unexpectedly accepted a non-MELF file" >&2
+ exit 1
+fi
+grep -q 'not a MELF file' "$TMP/bad.err"
+
+if "$CPU_OBJDUMP" "$TMP/unit.o" > /dev/null 2> "$TMP/no-option.err"; then
+ echo "mcpu-objdump unexpectedly accepted no display option" >&2
+ exit 1
+fi
+grep -q 'at least one display option is required' "$TMP/no-option.err"
+
+"$CPU_OBJDUMP" --help | grep -q -- '--disassemble-all'
+"$CPU_OBJDUMP" --help | grep -q -- '--dynamic-syms'
+"$CPU_OBJDUMP" --help | grep -q -- '--section=NAME'
+"$CPU_OBJDUMP" --version | grep -Fq "$PACKAGE_VERSION"
diff --git a/tests/tst-overlap.c b/tests/tst-overlap.c
new file mode 100644
index 0000000..9de9286
--- /dev/null
+++ b/tests/tst-overlap.c
@@ -0,0 +1,104 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+
+#include <libmcpu.h>
+
+
+static int failures = 0;
+
+#define CHECK(cond,msg) \
+ do { \
+ if( !(cond) ) \
+ { \
+ fprintf( stderr, "FAIL: %s\n", (msg) ); \
+ ++failures; \
+ } \
+ } while( 0 )
+
+
+int
+main( void )
+{
+ __mpu_UNIT_t *r64;
+ __mpu_UNIT_t *r32_0;
+ __mpu_UNIT_t *r32_4;
+ __mpu_UNIT_t *r16_2;
+ __mpu_UNIT_t *r8_3;
+ unsigned int i;
+
+ __cpu_init();
+
+ r64 = __cpu_reg_ptr( 64, 0 );
+ r32_0 = __cpu_reg_ptr( 32, 0 );
+ r32_4 = __cpu_reg_ptr( 32, 4 );
+ r16_2 = __cpu_reg_ptr( 16, 2 );
+ r8_3 = __cpu_reg_ptr( 8, 3 );
+
+ CHECK( r64 != 0, "r64@0 pointer" );
+ CHECK( r32_0 != 0, "r32@0 pointer" );
+ CHECK( r32_4 != 0, "r32@4 pointer" );
+ CHECK( r16_2 != 0, "r16@2 pointer" );
+ CHECK( r8_3 != 0, "r8@3 pointer" );
+
+ CHECK( r64 == r32_0, "r64@0 and r32@0 start at same byte" );
+ CHECK( r32_4 == r64 + 4, "r32@4 overlaps upper four bytes of r64@0" );
+ CHECK( r16_2 == r64 + 2, "r16@2 overlaps bytes 2..3 of r64@0" );
+ CHECK( r8_3 == r64 + 3, "r8@3 overlaps byte 3 of r64@0" );
+
+ for( i = 0; i < 8; ++i ) r64[i] = (__mpu_UNIT_t)( 0x10 + i );
+
+ CHECK( r32_0[0] == 0x10 && r32_0[1] == 0x11 &&
+ r32_0[2] == 0x12 && r32_0[3] == 0x13,
+ "r32@0 sees bytes written through r64@0" );
+
+ CHECK( r32_4[0] == 0x14 && r32_4[1] == 0x15 &&
+ r32_4[2] == 0x16 && r32_4[3] == 0x17,
+ "r32@4 sees bytes written through r64@0" );
+
+ CHECK( r16_2[0] == 0x12 && r16_2[1] == 0x13,
+ "r16@2 sees bytes written through r64@0" );
+
+ *r8_3 = 0xa5;
+ CHECK( r64[3] == 0xa5,
+ "write through r8@3 changes r64@0 storage" );
+ CHECK( r32_0[3] == 0xa5,
+ "write through r8@3 changes r32@0 storage" );
+ CHECK( r16_2[1] == 0xa5,
+ "write through r8@3 changes r16@2 storage" );
+
+ /* Check the same property at a non-zero naturally aligned address. */
+ {
+ __mpu_UNIT_t *r128 = __cpu_reg_ptr( 128, 0x00100 );
+ __mpu_UNIT_t *r32 = __cpu_reg_ptr( 32, 0x0010c );
+ __mpu_UNIT_t *r8 = __cpu_reg_ptr( 8, 0x0010f );
+
+ CHECK( r128 != 0 && r32 != 0 && r8 != 0,
+ "overlap pointers at 0x00100" );
+ CHECK( r32 == r128 + 12,
+ "r32@0x0010c inside r128@0x00100" );
+ CHECK( r8 == r128 + 15,
+ "r8@0x0010f inside r128@0x00100" );
+
+ r128[15] = 0x5a;
+ CHECK( *r8 == 0x5a,
+ "r8 view sees r128 storage" );
+
+ r32[3] = 0xc3;
+ CHECK( r128[15] == 0xc3,
+ "r32 view changes r128 storage" );
+ }
+
+ if( failures != 0 )
+ {
+ fprintf( stderr, "libcpu overlap regression: %d failure(s)\n", failures );
+ return( 1 );
+ }
+
+ printf( "libcpu 0.0.3 overlapping register views regression: PASS\n" );
+ printf( "overlap is byte-storage aliasing and is endian independent\n" );
+
+ return( 0 );
+}
diff --git a/tests/tst-partial-link-runner.sh b/tests/tst-partial-link-runner.sh
new file mode 100755
index 0000000..2f08e16
--- /dev/null
+++ b/tests/tst-partial-link-runner.sh
@@ -0,0 +1,276 @@
+#!/bin/bash
+set -e
+
+CPU_AS=${CPU_AS:-../utils/mcpu-as}
+CPU_LD=${CPU_LD:-../utils/mcpu-ld}
+CPU_RUN=${CPU_RUN:-../utils/mcpu-run}
+CPU_READELF=${CPU_READELF:-../utils/mcpu-readelf}
+CPU_AR=${CPU_AR:-../utils/mcpu-ar}
+
+TMP=${TMPDIR:-/tmp}/libmcpu-partial-link-$$
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+mkdir -p "$TMP/lib"
+
+cat > "$TMP/main.s" <<'EOS'
+.global _life
+.extern add_two
+.extern shared_value
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL add_two
+ STORE shared_value, r64@0x00000
+ LOAD r64@0x00018, shared_value
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+
+cat > "$TMP/lib.s" <<'EOS'
+.global add_two
+.global shared_value
+.global shared_ptr
+.global add_two_ptr
+.data
+.align 32
+shared_value:
+ .integer 64, 0
+shared_ptr:
+ .ptr shared_value
+add_two_ptr:
+ .cptr add_two
+.text
+add_two:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/main.o" "$TMP/main.s"
+"$CPU_AS" -o "$TMP/lib.o" "$TMP/lib.s"
+
+# A complete partial link is still ET_REL and may be used as the sole input
+# object of a later final link.
+"$CPU_LD" -r -o "$TMP/combined.o" "$TMP/main.o" "$TMP/lib.o"
+"$CPU_READELF" -h "$TMP/combined.o" > "$TMP/combined.header"
+"$CPU_READELF" -s "$TMP/combined.o" > "$TMP/combined.symbols"
+"$CPU_READELF" -r "$TMP/combined.o" > "$TMP/combined.relocs"
+grep -q 'Type:.*REL (Relocatable file)' "$TMP/combined.header"
+grep -q 'add_two' "$TMP/combined.symbols"
+grep -q 'shared_value' "$TMP/combined.symbols"
+grep -q 'add_two' "$TMP/combined.relocs"
+grep -q 'shared_value' "$TMP/combined.relocs"
+
+"$CPU_LD" --stack-size=4096 --no-heap -o "$TMP/combined-app" "$TMP/combined.o"
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 "$TMP/combined-app"`" = 42 ]
+
+# -r does not require lifecycle completeness and preserves unresolved symbols.
+"$CPU_LD" -r -o "$TMP/main-partial.o" "$TMP/main.o"
+"$CPU_READELF" -s "$TMP/main-partial.o" | grep -q 'UND.*add_two'
+"$CPU_LD" --stack-size=4096 --no-heap \
+ -o "$TMP/late-app" "$TMP/main-partial.o" "$TMP/lib.o"
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 "$TMP/late-app"`" = 42 ]
+
+"$CPU_LD" -r -o "$TMP/library-only.o" "$TMP/lib.o"
+"$CPU_READELF" -h "$TMP/library-only.o" | grep -q 'REL (Relocatable file)'
+
+# LOCAL symbols from separate objects remain private even when they have the
+# same spelling. Their relocations must still refer to their own definitions.
+cat > "$TMP/local-a.s" <<'EOS'
+.global fa
+.text
+fa:
+ CALL local_helper
+ RET
+local_helper:
+ LDI r64@0x00008, 1
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+cat > "$TMP/local-b.s" <<'EOS'
+.global fb
+.text
+fb:
+ CALL local_helper
+ RET
+local_helper:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+cat > "$TMP/local-main.s" <<'EOS'
+.global _life
+.extern fa
+.extern fb
+.text
+_start_life:
+ LDI r64@0x00000, 39
+ CALL fa
+ CALL fb
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+"$CPU_AS" -o "$TMP/local-a.o" "$TMP/local-a.s"
+"$CPU_AS" -o "$TMP/local-b.o" "$TMP/local-b.s"
+"$CPU_AS" -o "$TMP/local-main.o" "$TMP/local-main.s"
+"$CPU_LD" -r -o "$TMP/local-combined.o" "$TMP/local-a.o" "$TMP/local-b.o"
+"$CPU_LD" --stack-size=4096 --no-heap \
+ -o "$TMP/local-app" "$TMP/local-main.o" "$TMP/local-combined.o"
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 "$TMP/local-app"`" = 42 ]
+
+# A strong GLOBAL definition replaces a WEAK definition in partial output.
+cat > "$TMP/weak.s" <<'EOS'
+.weak pick
+.text
+pick:
+ LDI r64@0x00008, 1
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+cat > "$TMP/strong.s" <<'EOS'
+.global pick
+.text
+pick:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+cat > "$TMP/pick-main.s" <<'EOS'
+.global _life
+.extern pick
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL pick
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+"$CPU_AS" -o "$TMP/weak.o" "$TMP/weak.s"
+"$CPU_AS" -o "$TMP/strong.o" "$TMP/strong.s"
+"$CPU_AS" -o "$TMP/pick-main.o" "$TMP/pick-main.s"
+"$CPU_LD" -r -o "$TMP/pick.o" "$TMP/weak.o" "$TMP/strong.o"
+"$CPU_LD" --stack-size=4096 --no-heap \
+ -o "$TMP/pick-app" "$TMP/pick-main.o" "$TMP/pick.o"
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 "$TMP/pick-app"`" = 42 ]
+
+# Two strong definitions are already an error at partial-link time.
+cat > "$TMP/clash-a.s" <<'EOS'
+.global clash
+.text
+clash:
+ RET
+EOS
+cat > "$TMP/clash-b.s" <<'EOS'
+.global clash
+.text
+clash:
+ RET
+EOS
+"$CPU_AS" -o "$TMP/clash-a.o" "$TMP/clash-a.s"
+"$CPU_AS" -o "$TMP/clash-b.o" "$TMP/clash-b.s"
+if "$CPU_LD" -r -o "$TMP/clash.o" "$TMP/clash-a.o" "$TMP/clash-b.o" \
+ > /dev/null 2> "$TMP/clash.err"; then
+ echo "partial link unexpectedly accepted duplicate GLOBAL definitions" >&2
+ exit 1
+fi
+grep -q 'multiple definition of global symbol: clash' "$TMP/clash.err"
+
+# -r may extract required ET_REL members from libNAME.a, but never consumes a
+# same-named shared object. The unused archive member must stay outside the
+# partial object even though it contains an unresolved reference of its own.
+cat > "$TMP/usepart.s" <<'EOS'
+.global call_part
+.extern part
+.text
+call_part:
+ CALL part
+ RET
+EOS
+cat > "$TMP/part-static.s" <<'EOS'
+.global part
+.text
+part:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+cat > "$TMP/part-shared.s" <<'EOS'
+.global part
+.text
+part:
+ LDI r64@0x00008, 3
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+cat > "$TMP/unused.s" <<'EOS'
+.global unused_archive_member
+.extern missing_from_unused_member
+.text
+unused_archive_member:
+ CALL missing_from_unused_member
+ RET
+EOS
+cat > "$TMP/archive-main.s" <<'EOS'
+.global _life
+.extern call_part
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL call_part
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+"$CPU_AS" -o "$TMP/usepart.o" "$TMP/usepart.s"
+"$CPU_AS" -o "$TMP/part-static.o" "$TMP/part-static.s"
+"$CPU_AS" -o "$TMP/part-shared.o" "$TMP/part-shared.s"
+"$CPU_AS" -o "$TMP/unused.o" "$TMP/unused.s"
+"$CPU_AS" -o "$TMP/archive-main.o" "$TMP/archive-main.s"
+"$CPU_AR" rcs "$TMP/lib/libpart.a" "$TMP/part-static.o" "$TMP/unused.o"
+"$CPU_LD" --shared --soname=libpart.so.1 \
+ -o "$TMP/lib/libpart.so.1.0.0" "$TMP/part-shared.o"
+ln -s libpart.so.1.0.0 "$TMP/lib/libpart.so.1"
+ln -s libpart.so.1 "$TMP/lib/libpart.so"
+
+"$CPU_LD" -r -L"$TMP/lib" -lpart -o "$TMP/from-archive.o" "$TMP/usepart.o"
+"$CPU_READELF" -s "$TMP/from-archive.o" > "$TMP/from-archive.symbols"
+grep -q ' part$' "$TMP/from-archive.symbols"
+if grep -q 'unused_archive_member' "$TMP/from-archive.symbols"; then
+ echo "partial link unexpectedly extracted an unused archive member" >&2
+ exit 1
+fi
+"$CPU_LD" --stack-size=4096 --no-heap \
+ -o "$TMP/archive-app" "$TMP/archive-main.o" "$TMP/from-archive.o"
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 "$TMP/archive-app"`" = 42 ]
+
+# The resulting partial object is an ordinary ET_REL and can itself be stored
+# in a static archive.
+"$CPU_AR" rcs "$TMP/libcombined.a" "$TMP/combined.o"
+[ "`"$CPU_AR" t "$TMP/libcombined.a"`" = combined.o ]
+
+# Options that describe final executable/shared-image state are invalid in -r.
+if "$CPU_LD" -r --dynamic -o "$TMP/bad.o" "$TMP/lib.o" >/dev/null 2>&1; then
+ echo "-r --dynamic unexpectedly accepted" >&2
+ exit 1
+fi
+if "$CPU_LD" -r --shared --soname=bad.so -o "$TMP/bad.o" "$TMP/lib.o" >/dev/null 2>&1; then
+ echo "-r --shared unexpectedly accepted" >&2
+ exit 1
+fi
+if "$CPU_LD" -r --stack-size=4096 -o "$TMP/bad.o" "$TMP/lib.o" >/dev/null 2>&1; then
+ echo "-r --stack-size unexpectedly accepted" >&2
+ exit 1
+fi
+if "$CPU_LD" -r --no-heap -o "$TMP/bad.o" "$TMP/lib.o" >/dev/null 2>&1; then
+ echo "-r --no-heap unexpectedly accepted" >&2
+ exit 1
+fi
+if "$CPU_LD" -r --soname=bad.so -o "$TMP/bad.o" "$TMP/lib.o" >/dev/null 2>&1; then
+ echo "-r --soname unexpectedly accepted" >&2
+ exit 1
+fi
+
+"$CPU_LD" --help 2>&1 | grep -q -- '-r.*partial-link MELF ET_REL'
+
+exit 0
diff --git a/tests/tst-pointer-arithmetic.c b/tests/tst-pointer-arithmetic.c
new file mode 100644
index 0000000..76414fa
--- /dev/null
+++ b/tests/tst-pointer-arithmetic.c
@@ -0,0 +1,170 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+
+static const char src[]=
+".data\n"
+".align 8\n"
+"values:\n"
+" .integer 64, 11\n"
+" .integer 64, 22\n"
+" .integer 64, 33\n"
+".text\n"
+" LEA r64@0x00000, values\n"
+" PADDI r64@0x00008, r64@0x00000, 16\n"
+" LOADP r64@0x00010, r64@0x00008\n"
+" PDIFF r64@0x00018, r64@0x00008, r64@0x00000\n"
+" PSUBI r64@0x00020, r64@0x00008, 8\n"
+" LOADP r64@0x00028, r64@0x00020\n"
+" LDI r64@0x00030, 8\n"
+" PADD r64@0x00038, r64@0x00000, r64@0x00030\n"
+" LOADP r64@0x00040, r64@0x00038\n"
+" PSUB r64@0x00048, r64@0x00038, r64@0x00030\n"
+" LOADP r64@0x00050, r64@0x00048\n"
+" PADDI r64@0x00058, r64@0x00008, -16\n"
+" LOADP r64@0x00060, r64@0x00058\n"
+" PSUBI r64@0x00068, r64@0x00000, -16\n"
+" LOADP r64@0x00070, r64@0x00068\n"
+" HALT\n";
+
+static __mpu_uint64_t
+reg_u64( unsigned address )
+{
+ __mpu_uint64_t v = 0;
+ memcpy( &v, __cpu_reg_ptr( 64, address ), sizeof( v ) );
+ return( v );
+}
+
+static __mpu_int64_t
+reg_i64( unsigned address )
+{
+ __mpu_int64_t v = 0;
+ memcpy( &v, __cpu_reg_ptr( 64, address ), sizeof( v ) );
+ return( v );
+}
+
+static void
+set_reg_u64( unsigned address, __mpu_uint64_t v )
+{
+ memcpy( __cpu_reg_ptr( 64, address ), &v, sizeof( v ) );
+}
+
+static void
+set_reg_i64( unsigned address, __mpu_int64_t v )
+{
+ memcpy( __cpu_reg_ptr( 64, address ), &v, sizeof( v ) );
+}
+
+static void
+regop( cpu_operand *o, unsigned address )
+{
+ memset( o, 0, sizeof( *o ) );
+ o->type = CPU_OPERAND_REGISTER;
+ o->value.reg.bits = 64;
+ o->value.reg.address = address;
+}
+
+int
+main( void )
+{
+ cpu_asm_image im;
+ cpu_instruction i;
+ __mpu_uint64_t p, saved, minbits;
+ __mpu_int64_t mindelta;
+ __mpu_uint32_t psw;
+ char err[256];
+
+ __cpu_init();
+
+ CHECK( cpu_assemble_text( src, &im, err, sizeof( err ) ) == 0, err );
+ CHECK( __cpu_program_set( im.code, im.code_size ) == 0, "program set" );
+ CHECK( __cpu_data_set( im.data, im.data_size ) == 0, "data set" );
+ CHECK( __cpu_run() == CPU_EXEC_SUCCESS, "pointer arithmetic program run" );
+
+ CHECK( __cpu_ptr_segment( reg_u64( 0x00008 ) ) == CPU_PTR_SEG_DATA, "PADDI preserves DATA segment" );
+ CHECK( __cpu_ptr_offset( reg_u64( 0x00008 ) ) == 32, "PADDI offset" );
+ CHECK( reg_i64( 0x00010 ) == 33, "PADDI + LOADP" );
+ CHECK( reg_i64( 0x00018 ) == 16, "PDIFF byte difference" );
+ CHECK( __cpu_ptr_offset( reg_u64( 0x00020 ) ) == 24, "PSUBI offset" );
+ CHECK( reg_i64( 0x00028 ) == 22, "PSUBI + LOADP" );
+ CHECK( __cpu_ptr_offset( reg_u64( 0x00038 ) ) == 24, "PADD register displacement" );
+ CHECK( reg_i64( 0x00040 ) == 22, "PADD + LOADP" );
+ CHECK( __cpu_ptr_offset( reg_u64( 0x00048 ) ) == 16, "PSUB register displacement" );
+ CHECK( reg_i64( 0x00050 ) == 11, "PSUB + LOADP" );
+ CHECK( __cpu_ptr_offset( reg_u64( 0x00058 ) ) == 16, "PADDI negative displacement" );
+ CHECK( reg_i64( 0x00060 ) == 11, "PADDI negative + LOADP" );
+ CHECK( __cpu_ptr_offset( reg_u64( 0x00068 ) ) == 32, "PSUBI negative displacement" );
+ CHECK( reg_i64( 0x00070 ) == 33, "PSUBI negative + LOADP" );
+
+ /* Arithmetic is defined on any structurally valid PTR64 segment selector. */
+ CHECK( __cpu_ptr_make( &p, 0x0f, 100 ) == 0, "make reserved-segment PTR64" );
+ set_reg_u64( 0x00100, p );
+ set_reg_i64( 0x00108, 23 );
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_PADD;
+ regop( &i.operand[0], 0x00110 );
+ regop( &i.operand[1], 0x00100 );
+ regop( &i.operand[2], 0x00108 );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "PADD reserved segment does not dereference" );
+ p = reg_u64( 0x00110 );
+ CHECK( __cpu_ptr_segment( p ) == 0x0f, "PADD preserves reserved segment selector" );
+ CHECK( __cpu_ptr_offset( p ) == 123, "PADD reserved segment offset" );
+
+ /* PSW is untouched by all pointer arithmetic instructions. */
+ __cpu_set_psw( PSW_CF_MASK | PSW_IF_MASK );
+ psw = __cpu_get_psw();
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "PADD for PSW preservation" );
+ CHECK( __cpu_get_psw() == psw, "PADD preserves PSW" );
+
+ /* 60-bit offset overflow/underflow faults are atomic. */
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_DATA, CPU_PTR_OFFSET_MASK ) == 0, "make max-offset PTR64" );
+ set_reg_u64( 0x00100, p );
+ set_reg_i64( 0x00108, 1 );
+ saved = __mpu_UINT64_C( 0x1122334455667788 );
+ set_reg_u64( 0x00110, saved );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_POINTER_FAULT, "PADD offset overflow" );
+ CHECK( reg_u64( 0x00110 ) == saved, "PADD overflow leaves destination unchanged" );
+
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_DATA, 0 ) == 0, "make zero-offset PTR64" );
+ set_reg_u64( 0x00100, p );
+ set_reg_i64( 0x00108, -1 );
+ set_reg_u64( 0x00110, saved );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_POINTER_FAULT, "PADD offset underflow" );
+ CHECK( reg_u64( 0x00110 ) == saved, "PADD underflow leaves destination unchanged" );
+
+ /* INT64_MIN is handled without signed-overflow undefined behaviour. */
+ minbits = __mpu_UINT64_C( 0x8000000000000000 );
+ memcpy( &mindelta, &minbits, sizeof( mindelta ) );
+ set_reg_i64( 0x00108, mindelta );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_POINTER_FAULT, "PADD INT64_MIN displacement faults cleanly" );
+
+ /* PDIFF requires equal segment selectors and is also atomic on fault. */
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_DATA, 64 ) == 0, "make DATA pointer for PDIFF" );
+ set_reg_u64( 0x00100, p );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_STACK, 16 ) == 0, "make STACK pointer for PDIFF" );
+ set_reg_u64( 0x00108, p );
+ set_reg_u64( 0x00110, saved );
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_PDIFF;
+ regop( &i.operand[0], 0x00110 );
+ regop( &i.operand[1], 0x00100 );
+ regop( &i.operand[2], 0x00108 );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_POINTER_FAULT, "PDIFF rejects different segments" );
+ CHECK( reg_u64( 0x00110 ) == saved, "PDIFF segment fault leaves destination unchanged" );
+
+ /* Same-segment PDIFF may be negative and still fits signed integer64. */
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_PROGRAM, 10 ) == 0, "make PROGRAM pointer low" );
+ set_reg_u64( 0x00100, p );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_PROGRAM, 42 ) == 0, "make PROGRAM pointer high" );
+ set_reg_u64( 0x00108, p );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "PDIFF PROGRAM pointers" );
+ CHECK( reg_i64( 0x00110 ) == -32, "PDIFF signed negative result" );
+
+ cpu_asm_image_free( &im );
+ __cpu_free_context();
+ puts( "libcpu PTR64 arithmetic regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-pointer-policy.c b/tests/tst-pointer-policy.c
new file mode 100644
index 0000000..15d3b0f
--- /dev/null
+++ b/tests/tst-pointer-policy.c
@@ -0,0 +1,152 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+
+static void
+regop( cpu_operand *o, unsigned bits, unsigned address )
+{
+ memset( o, 0, sizeof( *o ) );
+ o->type = CPU_OPERAND_REGISTER;
+ o->value.reg.bits = bits;
+ o->value.reg.address = address;
+}
+
+static void
+set_u64( unsigned address, __mpu_uint64_t v )
+{
+ __cpu_data_put_u64( __cpu_reg_ptr( 64, address ), v );
+}
+
+static __mpu_uint64_t
+get_u64( unsigned address )
+{
+ return( __cpu_data_get_u64( __cpu_reg_ptr( 64, address ) ) );
+}
+
+static int
+exec2( cpu_opcode op, unsigned a, unsigned b )
+{
+ cpu_instruction i;
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = op;
+ regop( &i.operand[0], 64, a );
+ regop( &i.operand[1], 64, b );
+ return( __cpu_exec( &i ) );
+}
+
+static int
+exec3( cpu_opcode op, unsigned d, unsigned a, unsigned b )
+{
+ cpu_instruction i;
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = op;
+ regop( &i.operand[0], 64, d );
+ regop( &i.operand[1], 64, a );
+ regop( &i.operand[2], 64, b );
+ return( __cpu_exec( &i ) );
+}
+
+int
+main( void )
+{
+ __mpu_uint64_t p, q, saved;
+ __mpu_UNIT_t code[2] =
+ {
+ 0, 0
+ };
+ cpu_instruction i;
+
+ __cpu_init();
+ CHECK( CPU_ABI_STACK_RED_ZONE_SIZE == 0U, "ABI red zone is zero bytes" );
+ CHECK( __cpu_data_alloc( 128 ) == 0, "DATA alloc" );
+ CHECK( __cpu_stack_alloc( 128 ) == 0, "STACK alloc" );
+ CHECK( __cpu_program_set( code, sizeof( code ) ) == 0, "PROGRAM set" );
+
+ /* Pointer equality is full 64-bit equality and can be tested with ICMP/ZF. */
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_DATA, 32 ) == 0, "make DATA pointer p" );
+ CHECK( __cpu_ptr_make( &q, CPU_PTR_SEG_DATA, 32 ) == 0, "make equal DATA pointer q" );
+ set_u64( 0x100, p );
+ set_u64( 0x108, q );
+ CHECK( exec2( CPU_OP_ICMP, 0x100, 0x108 ) == CPU_EXEC_SUCCESS, "ICMP equal pointers" );
+ CHECK( ( __cpu_get_psw() & PSW_ZF_MASK ) != 0, "equal PTR64 sets ZF" );
+
+ CHECK( __cpu_ptr_make( &q, CPU_PTR_SEG_STACK, 32 ) == 0, "make STACK pointer q" );
+ set_u64( 0x108, q );
+ CHECK( exec2( CPU_OP_ICMP, 0x100, 0x108 ) == CPU_EXEC_SUCCESS, "ICMP cross-segment bit patterns" );
+ CHECK( ( __cpu_get_psw() & PSW_ZF_MASK ) == 0, "different segments are not equal" );
+
+ /* Pointer ordering is defined by PDIFF only inside one selector. */
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_DATA, 64 ) == 0, "make lower DATA pointer" );
+ CHECK( __cpu_ptr_make( &q, CPU_PTR_SEG_DATA, 96 ) == 0, "make upper DATA pointer" );
+ set_u64( 0x100, p );
+ set_u64( 0x108, q );
+ CHECK( exec3( CPU_OP_PDIFF, 0x110, 0x100, 0x108 ) == CPU_EXEC_SUCCESS, "same-segment PDIFF" );
+ CHECK( (__mpu_int64_t)get_u64( 0x110 ) == -32, "PDIFF gives signed byte difference" );
+ CHECK( exec2( CPU_OP_ITEST, 0x110, 0x110 ) == CPU_EXEC_SUCCESS, "ITEST pointer difference" );
+ CHECK( ( __cpu_get_psw() & PSW_SF_MASK ) != 0, "negative pointer difference sets SF" );
+
+ saved = __mpu_UINT64_C( 0x1122334455667788 );
+ set_u64( 0x110, saved );
+ CHECK( __cpu_ptr_make( &q, CPU_PTR_SEG_STACK, 96 ) == 0, "make cross-segment pointer" );
+ set_u64( 0x108, q );
+ CHECK( exec3( CPU_OP_PDIFF, 0x110, 0x100, 0x108 ) == CPU_EXEC_POINTER_FAULT, "PDIFF rejects cross-segment ordering" );
+ CHECK( get_u64( 0x110 ) == saved, "PDIFF cross-segment fault is atomic" );
+
+ /* Reserved selectors are structurally valid and support non-dereferencing arithmetic. */
+ CHECK( __cpu_ptr_make( &p, 0x0f, 123 ) == 0, "make reserved pointer p" );
+ CHECK( __cpu_ptr_make( &q, 0x0f, 100 ) == 0, "make reserved pointer q" );
+ set_u64( 0x100, p );
+ set_u64( 0x108, q );
+ CHECK( exec3( CPU_OP_PDIFF, 0x110, 0x100, 0x108 ) == CPU_EXEC_SUCCESS, "reserved same-selector PDIFF" );
+ CHECK( (__mpu_int64_t)get_u64( 0x110 ) == 23, "reserved PDIFF difference" );
+
+ /* A reserved PTR64 may be stored as data but may not be dereferenced. */
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_STORE;
+ i.operand[0].type = CPU_OPERAND_ADDR64;
+ i.operand[0].value.addr64 = 16;
+ regop( &i.operand[1], 64, 0x100 );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "STORE preserves reserved PTR64 as ordinary data" );
+ CHECK( __cpu_data_get_u64( __cpu_data_ptr() + 16 ) == p, "stored reserved PTR64 bits" );
+
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_LOADP;
+ regop( &i.operand[0], 64, 0x118 );
+ regop( &i.operand[1], 64, 0x100 );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_POINTER_FAULT, "LOADP rejects reserved selector" );
+
+ /* Even a zero-length BMOVI requires semantically valid address-space selectors. */
+ CHECK( __cpu_ptr_make( &q, CPU_PTR_SEG_DATA, 16 ) == 0, "make valid DATA pointer" );
+ set_u64( 0x108, q );
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_BMOVI;
+ regop( &i.operand[0], 64, 0x108 );
+ regop( &i.operand[1], 64, 0x100 );
+ i.operand[2].type = CPU_OPERAND_IMM64;
+ i.operand[2].value.imm64 = 0;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_POINTER_FAULT, "zero-length BMOVI rejects reserved source" );
+
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_JMPP;
+ regop( &i.operand[0], 64, 0x100 );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_POINTER_FAULT, "JMPP rejects reserved selector" );
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_CALLP;
+ regop( &i.operand[0], 64, 0x100 );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_POINTER_FAULT, "CALLP rejects reserved selector" );
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_SETSP;
+ regop( &i.operand[0], 64, 0x100 );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_POINTER_FAULT, "SETSP rejects reserved selector" );
+
+ /* DATA offsets 1..15 are ABI-reserved guard values, but only all-zero PTR64 is NULL. */
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_DATA, 1 ) == 0, "make DATA guard pointer" );
+ CHECK( p != CPU_NULL_PTR, "DATA:1 is not NULL" );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_DATA, 0 ) == 0 && p == CPU_NULL_PTR, "DATA:0 is canonical NULL" );
+
+ __cpu_free_context();
+ puts( "libcpu pointer-comparison/red-zone/reserved-segment policy regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-pointer.c b/tests/tst-pointer.c
new file mode 100644
index 0000000..ca0ff99
--- /dev/null
+++ b/tests/tst-pointer.c
@@ -0,0 +1,154 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+
+static const char src[]=
+".data\n"
+".align 8\n"
+"global_value:\n"
+" .integer 64, 41\n"
+".text\n"
+" LEA r64@0x00000, global_value\n"
+" LOADP r64@0x00008, r64@0x00000\n"
+" LDI r64@0x00010, 42\n"
+" STOREP r64@0x00000, r64@0x00010\n"
+" LOADP r64@0x00018, r64@0x00000\n"
+" ADJSP -16\n"
+" LDI r64@0x00020, 11\n"
+" LDI r64@0x00028, 22\n"
+" STSP 0, r64@0x00020\n"
+" STSP 8, r64@0x00028\n"
+" LEASP r64@0x00030, 0\n"
+" LOADP r64@0x00038, r64@0x00030\n"
+" LEASP r64@0x00040, 8\n"
+" LOADP r64@0x00048, r64@0x00040\n"
+" LDI r64@0x00050, 33\n"
+" STOREP r64@0x00040, r64@0x00050\n"
+" LDSP r64@0x00058, 8\n"
+" ADJSP 16\n"
+" HALT\n";
+
+static int
+eq64( unsigned address, const char *text )
+{
+ mpu_int64_t x;
+ iatoi( ( mpu_int * ) x, (__mpu_char8_t *)text, NB_I64 );
+ return( !memcmp( __cpu_reg_ptr( 64, address ), x, NB_I64 ) );
+}
+
+static __mpu_uint64_t
+reg_u64( unsigned address )
+{
+ __mpu_uint64_t v = 0;
+ memcpy( &v, __cpu_reg_ptr( 64, address ), 8 );
+ return( v );
+}
+
+int
+main( void )
+{
+ cpu_asm_image im;
+ cpu_instruction i;
+ __mpu_uint64_t p;
+ __mpu_UNIT_t saved[8];
+ char err[256];
+ int rc;
+
+ __cpu_init();
+
+ CHECK( CPU_PTR_SEG_SHIFT == 60U, "PTR64 segment shift" );
+ CHECK( CPU_PTR_OFFSET_MASK == __mpu_UINT64_C( 0x0fffffffffffffff ), "PTR64 offset mask" );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_DATA, 0x1234 ) == 0, "make DATA pointer" );
+ CHECK( p == __mpu_UINT64_C( 0x0000000000001234 ), "DATA pointer encoding" );
+ CHECK( __cpu_ptr_segment( p ) == CPU_PTR_SEG_DATA, "DATA pointer segment" );
+ CHECK( __cpu_ptr_offset( p ) == 0x1234, "DATA pointer offset" );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_STACK, 0x1234 ) == 0, "make STACK pointer" );
+ CHECK( p == __mpu_UINT64_C( 0x1000000000001234 ), "STACK pointer encoding" );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_PROGRAM, 0x1234 ) == 0, "make PROGRAM pointer" );
+ CHECK( p == __mpu_UINT64_C( 0x2000000000001234 ), "PROGRAM pointer encoding" );
+ CHECK( __cpu_ptr_make( &p, 0x10, 0 ) == -1, "reject segment wider than four bits" );
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_DATA, CPU_PTR_OFFSET_MASK + 1 ) == -1, "reject offset wider than 60 bits" );
+
+ CHECK( cpu_assemble_text( src, &im, err, sizeof( err ) ) == 0, err );
+ CHECK( __cpu_program_set( im.code, im.code_size ) == 0, "program set" );
+ CHECK( __cpu_data_set( im.data, im.data_size ) == 0, "data set" );
+ CHECK( __cpu_stack_alloc( 128 ) == 0, "stack alloc" );
+ __cpu_pc = 0;
+ CHECK( __cpu_run() == CPU_EXEC_SUCCESS, "PTR64 program run" );
+
+ p = reg_u64( 0x00000 );
+ CHECK( __cpu_ptr_segment( p ) == CPU_PTR_SEG_DATA, "LEA creates DATA pointer" );
+ CHECK( __cpu_ptr_offset( p ) == CPU_DATA_NULL_GUARD_SIZE, "LEA DATA label offset after NULL guard" );
+ CHECK( eq64( 0x00008, "41" ), "LOADP reads DATA" );
+ CHECK( eq64( 0x00018, "42" ), "STOREP writes DATA and LOADP reads it back" );
+ CHECK( eq64( 0x00038, "11" ), "LEASP/LOADP first local object field" );
+ CHECK( eq64( 0x00048, "22" ), "LEASP/LOADP second local object field" );
+ CHECK( eq64( 0x00058, "33" ), "STOREP writes STACK through PTR64" );
+ CHECK( __cpu_sp == 128, "stack frame released" );
+
+ p = reg_u64( 0x00030 );
+ CHECK( __cpu_ptr_segment( p ) == CPU_PTR_SEG_STACK, "LEASP segment selector" );
+ CHECK( __cpu_ptr_offset( p ) == 112, "LEASP absolute stack offset for SP+0" );
+
+ /* PROGRAM is readable through PTR64 but is not writable. */
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_PROGRAM, 0 ) == 0, "make PROGRAM pointer for indirect access" );
+ memcpy( __cpu_reg_ptr( 64, 0x00060 ), &p, 8 );
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_LOADP;
+ i.operand[0].type = CPU_OPERAND_REGISTER;
+ i.operand[0].value.reg.bits = 16;
+ i.operand[0].value.reg.address = 0x00068;
+ i.operand[1].type = CPU_OPERAND_REGISTER;
+ i.operand[1].value.reg.bits = 64;
+ i.operand[1].value.reg.address = 0x00060;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "LOADP may read PROGRAM" );
+ CHECK( !memcmp( __cpu_reg_ptr( 16, 0x00068 ), __cpu_program_ptr(), 2 ), "LOADP PROGRAM bytes" );
+
+ memcpy( saved, __cpu_program_ptr(), sizeof( saved ) );
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_STOREP;
+ i.operand[0].type = CPU_OPERAND_REGISTER;
+ i.operand[0].value.reg.bits = 64;
+ i.operand[0].value.reg.address = 0x00060;
+ i.operand[1].type = CPU_OPERAND_REGISTER;
+ i.operand[1].value.reg.bits = 64;
+ i.operand[1].value.reg.address = 0x00010;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_MEMORY_FAULT, "STOREP rejects PROGRAM write" );
+ CHECK( !memcmp( saved, __cpu_program_ptr(), sizeof( saved ) ), "PROGRAM write fault is atomic" );
+
+
+ /* ABI NULL is all-zero PTR64 and is never dereferenceable. */
+ p = CPU_NULL_PTR;
+ memcpy( __cpu_reg_ptr( 64, 0x00060 ), &p, 8 );
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_LOADP;
+ i.operand[0].type = CPU_OPERAND_REGISTER;
+ i.operand[0].value.reg.bits = 8;
+ i.operand[0].value.reg.address = 0x00070;
+ i.operand[1].type = CPU_OPERAND_REGISTER;
+ i.operand[1].value.reg.bits = 64;
+ i.operand[1].value.reg.address = 0x00060;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_POINTER_FAULT, "NULL PTR64 dereference faults" );
+
+ /* Reserved segment selectors decode but cannot currently be dereferenced. */
+ CHECK( __cpu_ptr_make( &p, CPU_PTR_SEG_FIRST_RESERVED, 0 ) == 0, "reserved segment fits PTR64" );
+ memcpy( __cpu_reg_ptr( 64, 0x00060 ), &p, 8 );
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_LOADP;
+ i.operand[0].type = CPU_OPERAND_REGISTER;
+ i.operand[0].value.reg.bits = 8;
+ i.operand[0].value.reg.address = 0x00070;
+ i.operand[1].type = CPU_OPERAND_REGISTER;
+ i.operand[1].value.reg.bits = 64;
+ i.operand[1].value.reg.address = 0x00060;
+ rc = __cpu_exec( &i );
+ CHECK( rc == CPU_EXEC_POINTER_FAULT, "reserved PTR64 segment faults on dereference" );
+
+ cpu_asm_image_free( &im );
+ __cpu_free_context();
+ puts( "libcpu PTR64/indirect-addressing regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-psw-abi-runner.sh b/tests/tst-psw-abi-runner.sh
new file mode 100755
index 0000000..37fb5fa
--- /dev/null
+++ b/tests/tst-psw-abi-runner.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${TEST_SRCDIR:?}"
+base="${TMPDIR:-/tmp}/libcpu-psw-abi-$$"
+trap 'rm -f "$base.s" "$base.o" "$base.melf"' EXIT HUP INT TERM
+"$CPU_AS" -o "$base.o" "$TEST_SRCDIR/fixtures/psw-call-abi.s" >/dev/null
+"$CPU_LD" --stack-size=512 -o "$base.melf" "$base.o" >/dev/null
+out=`"$CPU_RUN" --int-reg=r64@0x00010 "$base.melf"`
+test "x$out" = "x42"
diff --git a/tests/tst-psw-abi.c b/tests/tst-psw-abi.c
new file mode 100644
index 0000000..9e34ef9
--- /dev/null
+++ b/tests/tst-psw-abi.c
@@ -0,0 +1,133 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+
+#include <libmcpu.h>
+
+#define CHECK(c,m) do { if(!(c)) { fprintf(stderr,"FAIL: %s\n",(m)); return 1; } } while(0)
+
+static cpu_operand
+reg_operand( unsigned int bits, __mpu_uint32_t address )
+{
+ cpu_operand o;
+ memset( &o, 0, sizeof( o ) );
+ o.type = CPU_OPERAND_REGISTER;
+ o.value.reg.bits = bits;
+ o.value.reg.address = address;
+ return( o );
+}
+
+static void
+put_u64_reg( unsigned int address, __mpu_uint64_t value )
+{
+ memcpy( __cpu_reg_ptr( 64, address ), &value, sizeof( value ) );
+}
+
+int
+main( void )
+{
+ cpu_instruction i;
+ __mpu_uint32_t before, after, control_before;
+ __mpu_uint64_t target, ret;
+ __mpu_UNIT_t *a, *b, *d;
+
+ __cpu_init();
+
+ CHECK( CPU_PSW_ABI_VOLATILE_STATUS_MASK == PSW_MPU_MASK,
+ "volatile PSW domain is exactly PSW[15:0]");
+ CHECK( CPU_PSW_ABI_PERSISTENT_CONTROL_MASK ==
+ ( PSW_TF_MASK | PSW_IF_MASK | PSW_MODE_MASK ),
+ "persistent PSW domain is TF/IF/MODE");
+ CHECK( CPU_PSW_ABI_EXECUTION_STATE_MASK == PSW_HF_MASK,
+ "HF is the execution-state PSW domain");
+ CHECK( ( CPU_PSW_ABI_VOLATILE_STATUS_MASK &
+ CPU_PSW_ABI_PERSISTENT_CONTROL_MASK)==0,
+ "volatile and persistent PSW domains do not overlap");
+ CHECK( ( CPU_PSW_ABI_VOLATILE_STATUS_MASK |
+ CPU_PSW_ABI_PERSISTENT_CONTROL_MASK |
+ CPU_PSW_ABI_EXECUTION_STATE_MASK)==PSW_USED_MASK,
+ "PSW ABI domains cover all currently used PSW bits");
+
+ CHECK( __cpu_program_alloc( 64 ) == 0, "PROGRAM allocation" );
+ CHECK( __cpu_stack_alloc( 128 ) == 0, "STACK allocation" );
+
+ /* CALL/RET are instructions, not implicit PSW save/restore operations.
+ Their own execution must leave every currently used PSW bit untouched. */
+ before = ( PSW_AF_MASK | PSW_CF_MASK | PSW_ZF_MASK | PSW_INXF_MASK |
+ PSW_HF_MASK | PSW_TF_MASK | PSW_IF_MASK |
+ ( (__mpu_uint32_t)2U << PSW_MODE_SHIFT ));
+ __cpu_set_psw( before );
+ before = __cpu_get_psw();
+
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_CALL;
+ i.operand[0].type = CPU_OPERAND_CODE64;
+ i.operand[0].value.code64 = 32;
+ __cpu_pc = 4;
+ __cpu_sp = 128;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_BRANCH, "CALL executes" );
+ CHECK( __cpu_get_psw() == before, "CALL itself is PSW-neutral" );
+ CHECK( __cpu_sp == 120, "CALL pushes return PC" );
+ memcpy( &ret, __cpu_stack_ptr() + 120, sizeof( ret ) );
+ CHECK( ret == 14, "CALL raw return PC" );
+
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_RET;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_BRANCH, "RET executes" );
+ CHECK( __cpu_get_psw() == before, "RET itself is PSW-neutral" );
+ CHECK( __cpu_pc == 14 && __cpu_sp == 128, "RET restores PC/SP call state" );
+
+ /* CALLP has the same PSW-neutral instruction semantics. */
+ CHECK( __cpu_ptr_make( &target, CPU_PTR_SEG_PROGRAM, 40 ) == 0,
+ "make PROGRAM PTR64");
+ put_u64_reg( 0x00100, target );
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_CALLP;
+ i.operand[0] = reg_operand( 64, 0x00100 );
+ __cpu_pc = 8;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_BRANCH, "CALLP executes" );
+ CHECK( __cpu_get_psw() == before, "CALLP itself is PSW-neutral" );
+
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_RET;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_BRANCH, "RET after CALLP executes" );
+ CHECK( __cpu_get_psw() == before, "RET after CALLP is PSW-neutral" );
+ CHECK( __cpu_pc == 13 && __cpu_sp == 128, "CALLP/RET restore PC/SP call state" );
+
+ /* Arithmetic status is caller-clobbered, while TF/IF/MODE are persistent
+ control state. IADD is sufficient to demonstrate both domains. */
+ a = __cpu_reg_ptr( 8, 0x00200 );
+ b = __cpu_reg_ptr( 8, 0x00201 );
+ d = __cpu_reg_ptr( 8, 0x00202 );
+ CHECK( a && b && d, "IADD register views" );
+ *a = 1;
+ *b = 1;
+ *d = 0;
+
+ __cpu_set_psw( PSW_CF_MASK | PSW_ZF_MASK | PSW_INXF_MASK |
+ PSW_TF_MASK | PSW_IF_MASK |
+ ( (__mpu_uint32_t)3U << PSW_MODE_SHIFT ));
+ before = __cpu_get_psw();
+ control_before = before & CPU_PSW_ABI_PERSISTENT_CONTROL_MASK;
+
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_IADD;
+ i.operand[0] = reg_operand( 8, 0x00202 );
+ i.operand[1] = reg_operand( 8, 0x00200 );
+ i.operand[2] = reg_operand( 8, 0x00201 );
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_SUCCESS, "IADD executes" );
+ after = __cpu_get_psw();
+ CHECK( *d == 2, "IADD result" );
+ CHECK( ( after & CPU_PSW_ABI_PERSISTENT_CONTROL_MASK ) == control_before,
+ "ordinary arithmetic preserves TF/IF/MODE control domain");
+ CHECK( ( after & CPU_PSW_ABI_VOLATILE_STATUS_MASK ) !=
+ ( before & CPU_PSW_ABI_VOLATILE_STATUS_MASK ),
+ "arithmetic may clobber volatile PSW[15:0]");
+
+ __cpu_free_context();
+ puts( "libcpu PSW calling-convention regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-psw-write.c b/tests/tst-psw-write.c
new file mode 100644
index 0000000..0410ec6
--- /dev/null
+++ b/tests/tst-psw-write.c
@@ -0,0 +1,255 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+
+
+static int failures = 0;
+
+#define CHECK(cond,msg) \
+ do { \
+ if( !(cond) ) \
+ { \
+ fprintf( stderr, "FAIL: %s\n", (msg) ); \
+ ++failures; \
+ } \
+ } while( 0 )
+
+
+static void
+clear_instruction( cpu_instruction *instruction, cpu_opcode opcode )
+{
+ memset( instruction, 0, sizeof(*instruction) );
+ instruction->opcode = opcode;
+}
+
+
+static cpu_operand
+psw_operand( unsigned int bit )
+{
+ cpu_operand operand;
+
+ memset( &operand, 0, sizeof(operand) );
+ operand.type = CPU_OPERAND_PSWBIT8;
+ operand.value.pswbit8 = bit;
+
+ return( operand );
+}
+
+
+static cpu_operand
+reg_operand( unsigned int bits, __mpu_uint32_t address )
+{
+ cpu_operand operand;
+
+ memset( &operand, 0, sizeof(operand) );
+ operand.type = CPU_OPERAND_REGISTER;
+ operand.value.reg.bits = bits;
+ operand.value.reg.address = address;
+
+ return( operand );
+}
+
+
+static void
+check_assembler_contract( void )
+{
+ static const char good_source[] =
+ ".text\n"
+ "SETF CF\n"
+ "CLRF AF\n"
+ "CLRCPUF\n"
+ "CLRMPUF\n"
+ "HALT\n";
+ static const char *bad_source[] =
+ {
+ ".text\nSETF HF\n",
+ ".text\nSETF TF\n",
+ ".text\nSETF IF\n",
+ ".text\nCLRF HF\n",
+ ".text\nCLRF TF\n",
+ ".text\nCLRF IF\n"
+ };
+ cpu_asm_image image;
+ cpu_instruction instruction;
+ char err[256];
+ __mpu_uint64_t pc = 0;
+ unsigned int i;
+
+ memset( &image, 0, sizeof(image) );
+ CHECK( cpu_assemble_text( good_source, &image, err, sizeof(err) ) == 0,
+ "assemble writable PSW flag instructions" );
+ if( image.code )
+ {
+ CHECK( __cpu_decode_at( image.code, image.code_size, pc, &instruction ) == CPU_EXEC_SUCCESS,
+ "decode SETF" );
+ CHECK( instruction.opcode == CPU_OP_SETF, "SETF opcode" );
+ CHECK( instruction.operand[0].type == CPU_OPERAND_PSWBIT8 &&
+ instruction.operand[0].value.pswbit8 == CPU_PSWBIT_CF,
+ "SETF selector" );
+ pc += instruction.length;
+
+ CHECK( __cpu_decode_at( image.code, image.code_size, pc, &instruction ) == CPU_EXEC_SUCCESS,
+ "decode CLRF" );
+ CHECK( instruction.opcode == CPU_OP_CLRF, "CLRF opcode" );
+ CHECK( instruction.operand[0].value.pswbit8 == CPU_PSWBIT_AF,
+ "CLRF selector" );
+ pc += instruction.length;
+
+ CHECK( __cpu_decode_at( image.code, image.code_size, pc, &instruction ) == CPU_EXEC_SUCCESS,
+ "decode CLRCPUF" );
+ CHECK( instruction.opcode == CPU_OP_CLRCPUF, "CLRCPUF opcode" );
+ pc += instruction.length;
+
+ CHECK( __cpu_decode_at( image.code, image.code_size, pc, &instruction ) == CPU_EXEC_SUCCESS,
+ "decode CLRMPUF" );
+ CHECK( instruction.opcode == CPU_OP_CLRMPUF, "CLRMPUF opcode" );
+ }
+ cpu_asm_image_free( &image );
+
+ for( i = 0; i < sizeof(bad_source) / sizeof(bad_source[0]); ++i )
+ {
+ memset( &image, 0, sizeof(image) );
+ CHECK( cpu_assemble_text( bad_source[i], &image, err, sizeof(err) ) != 0,
+ "assembler rejects control/execution PSW selector write" );
+ cpu_asm_image_free( &image );
+ }
+}
+
+
+int
+main( void )
+{
+ cpu_instruction instruction;
+ __mpu_uint32_t psw, expected;
+ unsigned int bit;
+
+ __cpu_init();
+
+ CHECK( CPU_OP_SETF == 0x0010, "SETF OPCODE16" );
+ CHECK( CPU_OP_CLRF == 0x0011, "CLRF OPCODE16" );
+ CHECK( CPU_OP_CLRCPUF == 0x0012, "CLRCPUF OPCODE16" );
+ CHECK( CPU_OP_CLRMPUF == 0x0013, "CLRMPUF OPCODE16" );
+ CHECK( PSW_CPUF_MASK == __mpu_UINT32_C(0x000000ff), "CPU flag mask" );
+ CHECK( PSW_MPUF_MASK == __mpu_UINT32_C(0x0000ff00), "MPU flag mask" );
+
+ /* SETF/CLRF write exactly one arithmetic/status flag in PSW[15:0]. */
+ for( bit = CPU_PSWBIT_AF; bit <= CPU_PSWBIT_WRITE_MAX; ++bit )
+ {
+ __mpu_uint32_t mask = (__mpu_uint32_t)1U << bit;
+
+ __cpu_set_psw( PSW_TF_MASK | PSW_IF_MASK | (__mpu_uint32_t)(2U << PSW_MODE_SHIFT) );
+ psw = __cpu_get_psw();
+
+ clear_instruction( &instruction, CPU_OP_SETF );
+ instruction.operand[0] = psw_operand( bit );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "SETF return code" );
+ CHECK( __cpu_get_psw() == ( psw | mask ), "SETF changes only selected flag" );
+
+ psw = __cpu_get_psw();
+ clear_instruction( &instruction, CPU_OP_CLRF );
+ instruction.operand[0] = psw_operand( bit );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "CLRF return code" );
+ CHECK( __cpu_get_psw() == ( psw & ~mask ), "CLRF changes only selected flag" );
+ }
+
+ /* HF/TF/IF are readable by branches but are not writable through SETF/CLRF. */
+ for( bit = CPU_PSWBIT_HF; bit <= CPU_PSWBIT_IF; ++bit )
+ {
+ clear_instruction( &instruction, CPU_OP_SETF );
+ instruction.operand[0] = psw_operand( bit );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_OPERAND_FAULT,
+ "SETF rejects HF/TF/IF" );
+
+ clear_instruction( &instruction, CPU_OP_CLRF );
+ instruction.operand[0] = psw_operand( bit );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_OPERAND_FAULT,
+ "CLRF rejects HF/TF/IF" );
+ }
+
+ /* CLRCPUF clears only PSW[7:0]. */
+ __cpu_set_psw( PSW_USED_MASK );
+ psw = __cpu_get_psw();
+ expected = psw & ~PSW_CPUF_MASK;
+ clear_instruction( &instruction, CPU_OP_CLRCPUF );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "CLRCPUF return code" );
+ CHECK( __cpu_get_psw() == expected, "CLRCPUF preserves PSW[15:8] and control state" );
+
+ /* CLRMPUF clears only PSW[15:8]. */
+ __cpu_set_psw( PSW_USED_MASK );
+ psw = __cpu_get_psw();
+ expected = psw & ~PSW_MPUF_MASK;
+ clear_instruction( &instruction, CPU_OP_CLRMPUF );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "CLRMPUF return code" );
+ CHECK( __cpu_get_psw() == expected, "CLRMPUF preserves PSW[7:0] and control state" );
+
+ /* SETF/CLRF provide an explicit initial carry for LibMPU IADC. */
+ {
+ __mpu_UNIT_t *dst = __cpu_reg_ptr( 8, 0x00600 );
+ __mpu_UNIT_t *a = __cpu_reg_ptr( 8, 0x00601 );
+ __mpu_UNIT_t *b = __cpu_reg_ptr( 8, 0x00602 );
+
+ *a = 1;
+ *b = 2;
+
+ clear_instruction( &instruction, CPU_OP_SETF );
+ instruction.operand[0] = psw_operand( CPU_PSWBIT_CF );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "SETF CF before IADC" );
+
+ clear_instruction( &instruction, CPU_OP_IADC );
+ instruction.operand[0] = reg_operand( 8, 0x00600 );
+ instruction.operand[1] = reg_operand( 8, 0x00601 );
+ instruction.operand[2] = reg_operand( 8, 0x00602 );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "IADC after SETF CF" );
+ CHECK( *dst == 4, "IADC consumes SETF CF" );
+
+ clear_instruction( &instruction, CPU_OP_CLRF );
+ instruction.operand[0] = psw_operand( CPU_PSWBIT_CF );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "CLRF CF before IADC" );
+ clear_instruction( &instruction, CPU_OP_IADC );
+ instruction.operand[0] = reg_operand( 8, 0x00600 );
+ instruction.operand[1] = reg_operand( 8, 0x00601 );
+ instruction.operand[2] = reg_operand( 8, 0x00602 );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "IADC after CLRF CF" );
+ CHECK( *dst == 3, "IADC consumes cleared CF" );
+ }
+
+ /* SETF CF likewise provides the input borrow consumed by LibMPU ISBB. */
+ {
+ __mpu_UNIT_t *dst = __cpu_reg_ptr( 8, 0x00610 );
+ __mpu_UNIT_t *a = __cpu_reg_ptr( 8, 0x00611 );
+ __mpu_UNIT_t *b = __cpu_reg_ptr( 8, 0x00612 );
+
+ *a = 9;
+ *b = 3;
+
+ clear_instruction( &instruction, CPU_OP_SETF );
+ instruction.operand[0] = psw_operand( CPU_PSWBIT_CF );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "SETF CF before ISBB" );
+
+ clear_instruction( &instruction, CPU_OP_ISBB );
+ instruction.operand[0] = reg_operand( 8, 0x00610 );
+ instruction.operand[1] = reg_operand( 8, 0x00611 );
+ instruction.operand[2] = reg_operand( 8, 0x00612 );
+ CHECK( __cpu_exec( &instruction ) == CPU_EXEC_SUCCESS, "ISBB after SETF CF" );
+ CHECK( *dst == 5, "ISBB consumes SETF CF" );
+ }
+
+ check_assembler_contract();
+
+ __cpu_free_context();
+
+ if( failures )
+ {
+ fprintf( stderr, "LibMCPU PSW write regression: %d failure(s)\n", failures );
+ return( 1 );
+ }
+
+ puts( "LibMCPU PSW write regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-readelf-runner.sh b/tests/tst-readelf-runner.sh
new file mode 100755
index 0000000..cf14a1d
--- /dev/null
+++ b/tests/tst-readelf-runner.sh
@@ -0,0 +1,69 @@
+#!/bin/sh
+set -eu
+
+: "${PACKAGE_VERSION:?}"
+
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_READELF:?}" "${TEST_SRCDIR:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-readelf-$$
+mkdir -p "$TMP"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+
+EXAMPLES=`cd "$TEST_SRCDIR/fixtures/shared-libs" && pwd`
+
+"$CPU_AS" -o "$TMP/main.o" "$EXAMPLES/dynamic-main.s"
+"$CPU_AS" -o "$TMP/liba.o" "$EXAMPLES/dynamic-liba.s"
+
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -o "$TMP/main" "$TMP/main.o"
+"$CPU_LD" --shared --soname=liba.so.1 -o "$TMP/liba.so" "$TMP/liba.o"
+
+"$CPU_READELF" -h -S -s -r "$TMP/main.o" > "$TMP/rel.txt"
+grep -q 'REL (Relocatable file)' "$TMP/rel.txt"
+grep -q "Symbol table '.symtab'" "$TMP/rel.txt"
+grep -q "Relocation section '.rela.text'" "$TMP/rel.txt"
+grep -q 'R_MCPU_CODE64' "$TMP/rel.txt"
+
+"$CPU_READELF" -a "$TMP/main" > "$TMP/exec.txt"
+grep -q 'ABI Version: 3' "$TMP/exec.txt"
+grep -q 'EXEC (Executable file)' "$TMP/exec.txt"
+grep -q 'Program Headers:' "$TMP/exec.txt"
+grep -q 'MCPU_STACK' "$TMP/exec.txt"
+grep -q "Symbol table '.dynsym'" "$TMP/exec.txt"
+grep -q 'MCPU target attributes:' "$TMP/exec.txt"
+grep -q 'MCPU lifecycle attributes:' "$TMP/exec.txt"
+grep -q "Dynamic section '.dynamic'" "$TMP/exec.txt"
+grep -q 'NULL' "$TMP/exec.txt"
+
+"$CPU_READELF" -hSlrsdA "$TMP/liba.so" > "$TMP/dyn.txt"
+grep -q 'DYN (Shared object file)' "$TMP/dyn.txt"
+grep -q "Symbol table '.dynsym'" "$TMP/dyn.txt"
+grep -q 'foo' "$TMP/dyn.txt"
+grep -q 'MCPU target attributes:' "$TMP/dyn.txt"
+grep -q 'ABI Version: 3' "$TMP/dyn.txt"
+grep -q "Dynamic section '.dynamic'" "$TMP/dyn.txt"
+grep -q 'SONAME' "$TMP/dyn.txt"
+grep -q 'Library soname: \[liba.so.1\]' "$TMP/dyn.txt"
+
+# Section Headers are intentionally wide: one section occupies exactly one
+# physical output line rather than a wrapped two-line record.
+"$CPU_READELF" -S "$TMP/main" > "$TMP/sections.txt"
+grep -q '^ \[Nr\] Name.*Size.*EntSize.*Flags Link Info Align$' "$TMP/sections.txt"
+section_count=`grep -c '^ \[[ 0-9][ 0-9]\] ' "$TMP/sections.txt"`
+test "$section_count" -gt 0
+if grep -q '^ [0-9a-f][0-9a-f]* ' "$TMP/sections.txt"; then
+ echo "mcpu-readelf unexpectedly wrapped a section header" >&2
+ exit 1
+fi
+
+"$CPU_READELF" --version | grep -Fq "$PACKAGE_VERSION"
+"$CPU_READELF" --help | grep -q -- '--arch-specific'
+"$CPU_READELF" --help | grep -q -- '--dynamic'
+
+printf 'not MELF\n' > "$TMP/bad"
+if "$CPU_READELF" -h "$TMP/bad" >/dev/null 2>&1; then
+ echo "mcpu-readelf unexpectedly accepted a non-MELF file" >&2
+ exit 1
+fi
+
+exit 0
diff --git a/tests/tst-registers.c b/tests/tst-registers.c
new file mode 100644
index 0000000..c8b1c99
--- /dev/null
+++ b/tests/tst-registers.c
@@ -0,0 +1,128 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+
+#include <libmcpu.h>
+
+
+static int failures = 0;
+
+#define CHECK(cond,msg) \
+ do { \
+ if( !(cond) ) \
+ { \
+ fprintf( stderr, "FAIL: %s\n", (msg) ); \
+ ++failures; \
+ } \
+ } while( 0 )
+
+
+struct width_test
+{
+ unsigned int bits;
+ __mpu_size_t bytes;
+};
+
+
+int
+main( void )
+{
+ static const struct width_test widths[] =
+ {
+ { 8, 1 },
+ { 16, 2 },
+ { 32, 4 },
+ { 64, 8 },
+ { 128, 16 },
+ { 256, 32 },
+ { 512, 64 },
+ { 1024, 128 },
+ { 2048, 256 },
+ { 4096, 512 },
+ { 8192, 1024 },
+ { 16384, 2048 },
+ { 32768, 4096 },
+ { 65536, 8192 }
+ };
+
+ __mpu_size_t i;
+
+ __cpu_init();
+
+ CHECK( CPU_REGISTERS_MEM_SIZE_IN_UNITS == 524288,
+ "512 KiB register file size" );
+ CHECK( CPU_REGISTER_ADDRESS_MAX == 0x0007ffffU,
+ "register address maximum" );
+ CHECK( CPU_REGISTER_MIN_BITS == 8,
+ "minimum register width" );
+ CHECK( CPU_REGISTER_MAX_BITS == 65536,
+ "maximum register width" );
+
+ for( i = 0; i < sizeof( widths ) / sizeof( widths[0] ); ++i )
+ {
+ __mpu_uint32_t last;
+
+ CHECK( __cpu_reg_size( widths[i].bits ) == widths[i].bytes,
+ "register width size" );
+
+ CHECK( __cpu_reg_valid( widths[i].bits, 0 ) == 1,
+ "register at address zero" );
+
+ CHECK( __cpu_reg_ptr( widths[i].bits, 0 ) == __cpu_registers,
+ "register pointer at address zero" );
+
+ last = (__mpu_uint32_t)(CPU_REGISTERS_MEM_SIZE_IN_UNITS - widths[i].bytes);
+
+ CHECK( __cpu_reg_valid( widths[i].bits, last ) == 1,
+ "last naturally aligned register" );
+
+ CHECK( __cpu_reg_ptr( widths[i].bits, last ) == &__cpu_registers[last],
+ "last register pointer" );
+
+ if( widths[i].bytes > 1 )
+ {
+ CHECK( __cpu_reg_valid( widths[i].bits, last + 1 ) == 0,
+ "natural alignment check" );
+ CHECK( __cpu_reg_ptr( widths[i].bits, last + 1 ) == 0,
+ "unaligned register pointer" );
+ }
+ }
+
+ CHECK( __cpu_reg_size( 0 ) == 0, "zero register width" );
+ CHECK( __cpu_reg_size( 7 ) == 0, "unsupported 7-bit register width" );
+ CHECK( __cpu_reg_size( 24 ) == 0, "unsupported 24-bit register width" );
+ CHECK( __cpu_reg_size( 131072 ) == 0, "register width above 65536" );
+
+ CHECK( __cpu_reg_valid( 8, CPU_REGISTERS_MEM_SIZE_IN_UNITS - 1 ) == 1,
+ "last byte is a valid r8" );
+ CHECK( __cpu_reg_valid( 8, CPU_REGISTERS_MEM_SIZE_IN_UNITS ) == 0,
+ "address one byte beyond register file" );
+ CHECK( __cpu_reg_ptr( 8, CPU_REGISTERS_MEM_SIZE_IN_UNITS ) == 0,
+ "pointer beyond register file" );
+
+ CHECK( __cpu_reg_valid( 32, 12 ) == 1,
+ "r32 naturally aligned address" );
+ CHECK( __cpu_reg_valid( 32, 13 ) == 0,
+ "r32 unaligned address" );
+
+ CHECK( __cpu_reg_valid( 65536, 0x0007e000U ) == 1,
+ "last r65536 region" );
+ CHECK( __cpu_reg_valid( 65536, 0x0007e001U ) == 0,
+ "unaligned r65536 region" );
+ CHECK( __cpu_reg_valid( 65536, 0x00080000U ) == 0,
+ "r65536 beyond register file" );
+
+ if( failures != 0 )
+ {
+ fprintf( stderr, "libcpu register regression: %d failure(s)\n", failures );
+ return( 1 );
+ }
+
+ printf( "libcpu 0.0.3 register width/alignment/bounds regression: PASS\n" );
+ printf( "register widths: r8 ... r65536\n" );
+ printf( "register address range: 0x00000 ... 0x7ffff\n" );
+
+ return( 0 );
+}
diff --git a/tests/tst-relocatable-runner.sh b/tests/tst-relocatable-runner.sh
new file mode 100755
index 0000000..20d1c35
--- /dev/null
+++ b/tests/tst-relocatable-runner.sh
@@ -0,0 +1,75 @@
+#!/bin/sh
+set -eu
+: "${CPU_AS:?}"
+: "${CPU_LD:?}"
+: "${CPU_RUN:?}"
+: "${TEST_SRCDIR:?}"
+
+D="${TMPDIR:-/tmp}/libcpu-reloc-$$"
+trap 'rm -rf "$D"' EXIT HUP INT TERM
+mkdir -p "$D"
+
+cat > "$D/main.s" <<'EOS'
+.global _start
+.global _life
+.extern add_two
+.extern shared
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL add_two
+ STORE shared, r64@0x00000
+ LOAD r64@0x00018, shared
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+
+cat > "$D/lib.s" <<'EOS'
+.global add_two
+.global shared
+.data
+.align 32
+shared:
+ .integer 64, 0
+raw_bytes:
+ .byte 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x40, 0x00
+ .byte 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+ .byte 0x00, 0x00, 0xd4, 0x37
+.text
+add_two:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+"$CPU_AS" -o "$D/main.o" "$D/main.s" >/dev/null
+"$CPU_AS" "$D/lib.s" -o "$D/lib.o" >/dev/null
+"$CPU_LD" --stack-size=512 --heap-size=1M --heap-limit=8G -o "$D/a" "$D/main.o" "$D/lib.o" >/dev/null
+"$CPU_LD" --stack-size=1KiB --heap-size=1MiB --heap-limit=1GiB -o "$D/a-units" "$D/main.o" "$D/lib.o" >/dev/null
+if "$CPU_LD" --stack-size=1KiB --heap-size=-1M --heap-limit=1GiB -o "$D/a-bad" "$D/main.o" "$D/lib.o" >/dev/null 2>&1; then
+ echo "mcpu-ld accepted negative heap size" >&2
+ exit 1
+fi
+if "$CPU_LD" --stack-size=1KiB --heap-size=1B --heap-limit=1GiB -o "$D/a-bad" "$D/main.o" "$D/lib.o" >/dev/null 2>&1; then
+ echo "mcpu-ld accepted unsupported B suffix" >&2
+ exit 1
+fi
+v=`"$CPU_RUN" --int-reg=r64@0x00018 "$D/a"`
+test "x$v" = x42
+
+# DATA integer output is target-representation aware.
+v=`"$CPU_RUN" --int-data=64@32 "$D/a"`
+test "x$v" = x42
+
+# raw_bytes starts at DATA offset 40. Request beyond the end of .data:
+# cpu-run must clip the dump to the 20 bytes that remain in .data.
+out=`"$CPU_RUN" --raw-data=64@40 "$D/a"`
+printf '%s\n' "$out" | grep -F 'DATA memory dump: (20 bytes):' >/dev/null
+printf '%s\n' "$out" | grep -F ' 0000 : 02 00 01 00 01 00 40 00 00 00 00 00 00 00 00 00 | ......@.........' >/dev/null
+printf '%s\n' "$out" | grep -F ' 0010 : 00 00 d4 37 ' >/dev/null
+
+# The linker-owned DATA NULL guard is not part of .data and must not be dumpable.
+if "$CPU_RUN" --raw-data=1@16 "$D/a" >/dev/null 2>&1; then
+ echo 'raw DATA dump escaped the linked .data section' >&2
+ exit 1
+fi
diff --git a/tests/tst-relocatable.c b/tests/tst-relocatable.c
new file mode 100644
index 0000000..a2df8c4
--- /dev/null
+++ b/tests/tst-relocatable.c
@@ -0,0 +1,326 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "mcpu-asm.h"
+#include "mcpu-link.h"
+
+static int
+sym( const cpu_asm_object *o, const char *n, size_t *idx )
+{
+ size_t i;
+ for( i = 0; i < o->symbol_count; i++ ) if( !strcmp( o->symbols[i].name, n ) )
+ {
+ *idx = i;
+ return( 0 );
+ }
+ return( -1 );
+}
+
+
+static int
+expect_link_fail( const char *a, const char *b, const char *needle )
+{
+ cpu_asm_object o[2];
+ cpu_asm_image im;
+ char err[512];
+ int ok = 0;
+ memset( o, 0, sizeof( o ) );
+ memset( &im, 0, sizeof( im ) );
+ if( cpu_assemble_object( a, &o[0], err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "negative object A did not assemble: %s\n", err );
+ goto out;
+ }
+ if( cpu_assemble_object( b, &o[1], err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "negative object B did not assemble: %s\n", err );
+ goto out;
+ }
+ if( !cpu_link_objects( o, 2, &im, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "negative link unexpectedly succeeded\n" );
+ cpu_asm_image_free( &im );
+ goto out;
+ }
+ if( needle && !strstr( err, needle ) )
+ {
+ fprintf( stderr, "negative link error mismatch: %s\n", err );
+ goto out;
+ }
+ ok = 1;
+out:
+ cpu_asm_object_free( &o[0] );
+ cpu_asm_object_free( &o[1] );
+ return( ok ? 0 : -1 );
+}
+
+int
+main( void )
+{
+ const char *main_src=
+ ".global _start, _life\n"
+ ".extern add_two\n"
+ ".extern shared\n"
+ ".text\n"
+ "_start_life:\n"
+ " LDI r64@0x00000, 40\n"
+ " CALL add_two\n"
+ " STORE shared, r64@0x00000\n"
+ " LOAD r64@0x00018, shared\n"
+ " LEA r64@0x00020, shared+0\n"
+ " LDI r32@0x00030, 0\n"
+ " EXIT r32@0x00030\n";
+ const char *lib_src=
+ ".global add_two\n"
+ ".global shared\n"
+ ".global shared_ptr\n"
+ ".global fn_ptr\n"
+ ".data\n"
+ ".align 32\n"
+ "shared:\n"
+ " .integer 64, 0\n"
+ "shared_ptr:\n"
+ " .ptr shared\n"
+ "fn_ptr:\n"
+ " .cptr add_two\n"
+ ".text\n"
+ "add_two:\n"
+ " LDI r64@0x00008, 2\n"
+ " IADD r64@0x00000, r64@0x00000, r64@0x00008\n"
+ " RET\n";
+ cpu_asm_object o[2];
+ cpu_asm_image im;
+ char err[512];
+ size_t i, ix;
+ __mpu_uint64_t p;
+ __mpu_init();
+ memset( o, 0, sizeof( o ) );
+ memset( &im, 0, sizeof( im ) );
+ if( cpu_assemble_object( main_src, &o[0], err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "main object: %s\n", err );
+ return( 1 );
+ }
+ if( cpu_assemble_object( lib_src, &o[1], err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "lib object: %s\n", err );
+ return( 1 );
+ }
+ if( o[0].data_size != 0 )
+ {
+ fprintf( stderr, "relocatable DATA unexpectedly contains NULL guard\n" );
+ return( 1 );
+ }
+ if( o[1].data_align != 32 )
+ {
+ fprintf( stderr, "DATA alignment not preserved: %llu\n", (unsigned long long)o[1].data_align );
+ return( 1 );
+ }
+ if( sym( &o[0], "add_two", &ix ) || o[0].symbols[ix].section != CPU_OBJ_SEC_UNDEF )
+ {
+ fprintf( stderr, "extern symbol missing\n" );
+ return( 1 );
+ }
+ if( sym( &o[1], "shared", &ix ) || o[1].symbols[ix].binding != CPU_OBJ_BIND_GLOBAL || o[1].symbols[ix].value != 0 )
+ {
+ fprintf( stderr, "defined global shared bad\n" );
+ return( 1 );
+ }
+ if( o[0].reloc_count != 4 || o[1].reloc_count != 2 )
+ {
+ fprintf( stderr, "unexpected relocation counts %zu %zu\n", o[0].reloc_count, o[1].reloc_count );
+ return( 1 );
+ }
+ if( cpu_link_objects( o, 2, &im, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "link: %s\n", err );
+ return( 1 );
+ }
+ if( !im.has_life || im.life_entry != 0 )
+ {
+ fprintf( stderr, "bad linked _life entry\n" );
+ return( 1 );
+ }
+ if( im.data_size != 56 )
+ {
+ fprintf( stderr, "linked DATA size/alignment/guard mismatch: %zu\n", im.data_size );
+ return( 1 );
+ }
+ for( i = 0; i < CPU_DATA_NULL_GUARD_SIZE; i++ ) if( im.data[i] )
+ {
+ fprintf( stderr, "DATA guard is not zero\n" );
+ return( 1 );
+ }
+ /* shared is aligned at final DATA offset 32; shared_ptr immediately follows. */
+ p = __cpu_data_get_u64( im.data + 40 );
+ if( __cpu_ptr_segment( p ) != CPU_PTR_SEG_DATA || __cpu_ptr_offset( p ) != 32 )
+ {
+ fprintf( stderr, ".ptr relocation bad: 0x%llx\n", (unsigned long long)p );
+ return( 1 );
+ }
+ p = __cpu_data_get_u64( im.data + 48 );
+ if( __cpu_ptr_segment( p ) != CPU_PTR_SEG_PROGRAM )
+ {
+ fprintf( stderr, ".cptr segment bad\n" );
+ return( 1 );
+ }
+ cpu_asm_image_free( &im );
+ cpu_asm_object_free( &o[0] );
+ cpu_asm_object_free( &o[1] );
+
+ /* _end is an executable linker-defined HEAP pointer at HEAP:16. */
+ {
+ cpu_asm_object x =
+ {
+ 0
+ };
+ cpu_asm_image xi;
+ const char *src=
+ ".global _start, _life\n"
+ ".extern _end\n"
+ ".data\n"
+ "end_ptr:\n"
+ " .ptr _end\n"
+ ".text\n"
+ "_start_life:\n"
+ " LEA r64@0x00000, _end\n"
+ " LDI r32@0x00010, 0\n"
+ " EXIT r32@0x00010\n";
+ memset( &xi, 0, sizeof( xi ) );
+ if( cpu_assemble_object( src, &x, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "_end object: %s\n", err );
+ return( 1 );
+ }
+ if( cpu_link_objects( &x, 1, &xi, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "_end link: %s\n", err );
+ return( 1 );
+ }
+ if( xi.data_size < 24 )
+ {
+ fprintf( stderr, "_end DATA image too small\n" );
+ return( 1 );
+ }
+ p = __cpu_data_get_u64( xi.data + CPU_DATA_NULL_GUARD_SIZE );
+ if( __cpu_ptr_segment( p ) != CPU_PTR_SEG_HEAP || __cpu_ptr_offset( p ) != CPU_HEAP_BEGIN )
+ {
+ fprintf( stderr, "_end .ptr relocation bad: 0x%llx\n", (unsigned long long)p );
+ return( 1 );
+ }
+ p = __cpu_code_get_u64( xi.code + 5 );
+ if( __cpu_ptr_segment( p ) != CPU_PTR_SEG_HEAP || __cpu_ptr_offset( p ) != CPU_HEAP_BEGIN )
+ {
+ fprintf( stderr, "_end LEA relocation bad: 0x%llx\n", (unsigned long long)p );
+ return( 1 );
+ }
+ cpu_asm_image_free( &xi );
+
+ memset( &xi, 0, sizeof( xi ) );
+ if( cpu_link_objects_mode( &x, 1, &xi, CPU_LINK_DYNAMIC_EXEC, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "_end dynamic link: %s\n", err );
+ return( 1 );
+ }
+ if( xi.data_size < 24 )
+ {
+ fprintf( stderr, "_end dynamic DATA image too small\n" );
+ return( 1 );
+ }
+ p = __cpu_data_get_u64( xi.data + CPU_DATA_NULL_GUARD_SIZE );
+ if( __cpu_ptr_segment( p ) != CPU_PTR_SEG_HEAP || __cpu_ptr_offset( p ) != CPU_HEAP_BEGIN )
+ {
+ fprintf( stderr, "_end dynamic .ptr relocation bad: 0x%llx\n", (unsigned long long)p );
+ return( 1 );
+ }
+ p = __cpu_code_get_u64( xi.code + 5 );
+ if( __cpu_ptr_segment( p ) != CPU_PTR_SEG_HEAP || __cpu_ptr_offset( p ) != CPU_HEAP_BEGIN )
+ {
+ fprintf( stderr, "_end dynamic LEA relocation bad: 0x%llx\n", (unsigned long long)p );
+ return( 1 );
+ }
+ if( xi.dyn_reloc_count != 0 )
+ {
+ fprintf( stderr, "_end must not remain a dynamic relocation\n" );
+ return( 1 );
+ }
+ cpu_asm_image_free( &xi );
+ cpu_asm_object_free( &x );
+ }
+
+ /* A hexadecimal absolute expression must not mistake the `x` in 0x20 for
+ a relocatable symbol. */
+ {
+ cpu_asm_object x =
+ {
+ 0
+ };
+ const char *src=
+ ".global _start, _life\n"
+ ".text\n"
+ "_start_life:\n"
+ " LEA r64@0x00000, 0x20\n"
+ " LDI r32@0x00010, 0\n"
+ " EXIT r32@0x00010\n";
+ if( cpu_assemble_object( src, &x, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "hex absolute expression: %s\n", err );
+ return( 1 );
+ }
+ if( x.code_size < 13 )
+ {
+ fprintf( stderr, "hex absolute expression produced short code\n" );
+ return( 1 );
+ }
+ p = __cpu_code_get_u64( x.code + 5 );
+ if( __cpu_ptr_segment( p ) != CPU_PTR_SEG_DATA || __cpu_ptr_offset( p ) != 0x20 )
+ {
+ fprintf( stderr, "hex absolute LEA bad: 0x%llx\n", (unsigned long long)p );
+ return( 1 );
+ }
+ cpu_asm_object_free( &x );
+ }
+
+ /* .equ and relocatable global/extern symbols occupy different namespaces
+ and may not reuse one name. */
+ {
+ cpu_asm_object x =
+ {
+ 0
+ };
+ const char *src=
+ ".equ N, 4*512\n"
+ ".global N\n"
+ ".global _start, _life\n"
+ ".text\n_start_life:\n LDI r32@0x00000, 0\n EXIT r32@0x00000\n";
+ if( !cpu_assemble_object( src, &x, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, ".equ/global conflict unexpectedly accepted\n" );
+ cpu_asm_object_free( &x );
+ return( 1 );
+ }
+ if( !strstr( err, "conflicts with .equ" ) )
+ {
+ fprintf( stderr, ".equ/global conflict error mismatch: %s\n", err );
+ return( 1 );
+ }
+ }
+
+ if( expect_link_fail(
+ ".global _start, _life\n.extern missing\n.text\n_start_life:\n CALL missing\n LDI r32@0x10,0\n EXIT r32@0x10\n",
+ ".data\nlocal:\n .integer 64,0\n",
+ "undefined symbol: missing")) return( 1 );
+
+ if( expect_link_fail(
+ ".global _start, _life, foo\n.data\nfoo:\n .integer 64,1\n.text\n_start_life:\n LDI r32@0x10,0\n EXIT r32@0x10\n",
+ ".global foo\n.data\nfoo:\n .integer 64,2\n",
+ "multiple definition of global symbol: foo")) return( 1 );
+
+ if( expect_link_fail(
+ ".global _start, _life\n.extern data_sym\n.data\np:\n .cptr data_sym\n.text\n_start_life:\n LDI r32@0x10,0\n EXIT r32@0x10\n",
+ ".global data_sym\n.data\ndata_sym:\n .integer 64,0\n",
+ ".cptr target is not PROGRAM: data_sym")) return( 1 );
+
+ __mpu_free_context();
+ return( 0 );
+}
diff --git a/tests/tst-run-memory-options.sh b/tests/tst-run-memory-options.sh
new file mode 100755
index 0000000..ab176ce
--- /dev/null
+++ b/tests/tst-run-memory-options.sh
@@ -0,0 +1,121 @@
+#!/bin/sh
+
+set -eu
+
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}"
+
+base="${TMPDIR:-/tmp}/libmcpu-run-memory-$$"
+trap 'rm -f "$base.s" "$base.o" "$base.melf"' EXIT HUP INT TERM
+
+cat > "$base.s" <<'EOS'
+.data
+ .integer 64, 11
+ .real 64, 1.5
+
+.control
+ .integer 64, 21
+ .real 64, 2.5
+
+.input
+ .integer 64, 31
+ .real 64, 3.5
+
+.output
+ .integer 64, 41
+ .real 64, 4.5
+
+.trace
+ .integer 64, 51
+ .real 64, 5.5
+
+.text
+_start_life:
+ HALT
+EOS
+
+"$CPU_AS" -o "$base.o" "$base.s" >/dev/null
+"$CPU_LD" -o "$base.melf" --stack-size=1K --heap-size=64K --heap-limit=1M \
+ "$base.o" >/dev/null
+
+check_integer()
+{
+ option="$1"
+ expected="$2"
+ out=`"$CPU_RUN" "$option" "$base.melf"`
+ if test "$out" != "$expected"; then
+ echo "unexpected $option result: $out" >&2
+ exit 1
+ fi
+}
+
+check_real()
+{
+ option="$1"
+ expected="$2"
+ out=`"$CPU_RUN" --digits=20 "$option" "$base.melf"`
+ case "$out" in
+ "$expected"*) ;;
+ *) echo "unexpected $option result: $out" >&2; exit 1 ;;
+ esac
+}
+
+check_raw()
+{
+ option="$1"
+ memory="$2"
+ bytes="$3"
+ out=`"$CPU_RUN" "$option" "$base.melf"`
+ case "$out" in
+ "$memory memory dump: ($bytes bytes):"*) ;;
+ *) echo "unexpected $option dump: $out" >&2; exit 1 ;;
+ esac
+}
+
+check_integer --int-data=64@16 11
+check_real --real-data=64@24 1.500000000000000E+0
+check_integer --int-control=64@0 21
+check_real --real-control=64@8 2.500000000000000E+0
+check_integer --int-heap=64@16 0
+check_real --real-heap=64@16 0.000000000000000E+0
+check_integer --int-input=64@0 31
+check_real --real-input=64@8 3.500000000000000E+0
+check_integer --int-output=64@0 41
+check_real --real-output=64@8 4.500000000000000E+0
+check_integer --int-trace=64@0 51
+check_real --real-trace=64@8 5.500000000000000E+0
+
+# Exercise every accepted binary suffix while forcing clipping to 16 bytes.
+check_raw --raw-data=1K@16 DATA 16
+check_raw --raw-control=1M@0 CONTROL 16
+check_raw --raw-input=1G@0 INPUT 16
+check_raw --raw-output=1KiB@0 OUTPUT 16
+check_raw --raw-trace=1MiB@0 TRACE 16
+check_raw --raw-heap=1GiB@65520 HEAP 16
+
+# Segment/object bounds must reject an address at or beyond the end.
+if "$CPU_RUN" --raw-control=1@16 "$base.melf" >/dev/null 2>&1; then
+ echo "--raw-control accepted address at CONTROL end" >&2
+ exit 1
+fi
+if "$CPU_RUN" --int-heap=64@65530 "$base.melf" >/dev/null 2>&1; then
+ echo "--int-heap accepted object crossing HEAP end" >&2
+ exit 1
+fi
+if "$CPU_RUN" --raw-data=1@32 "$base.melf" >/dev/null 2>&1; then
+ echo "--raw-data accepted address at .data end" >&2
+ exit 1
+fi
+if "$CPU_RUN" --raw-data=18446744073709551615GiB@16 "$base.melf" >/dev/null 2>&1; then
+ echo "--raw-data accepted overflowing byte count" >&2
+ exit 1
+fi
+
+# The descriptions in --help intentionally begin in one vertical column.
+help=`"$CPU_RUN" --help 2>&1`
+for option in int-control real-control raw-control int-heap real-heap raw-heap \
+ int-input real-input raw-input int-output real-output raw-output \
+ int-trace real-trace raw-trace; do
+ echo "$help" | grep -- "--$option=" >/dev/null || exit 1
+done
+
+exit 0
diff --git a/tests/tst-runner.sh b/tests/tst-runner.sh
new file mode 100755
index 0000000..54343b1
--- /dev/null
+++ b/tests/tst-runner.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+set -eu
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${TEST_SRCDIR:?}"
+base="${TMPDIR:-/tmp}/libcpu-radd-pi-$$"
+trap 'rm -f "$base.o" "$base.melf"' EXIT HUP INT TERM
+"$CPU_AS" -o "$base.o" "$TEST_SRCDIR/fixtures/radd-pi.s" >/dev/null
+"$CPU_LD" --stack-size=512 -o "$base.melf" "$base.o" >/dev/null
+out=`"$CPU_RUN" --digits=60 --real-reg=r4096@0x00400 "$base.melf"`
+case "$out" in
+ 5.14159265358979323846264338327950288419716939937510582097*) ;;
+ *) echo "unexpected register result: $out" >&2; exit 1 ;;
+esac
+out=`"$CPU_RUN" --digits=60 --real-data=4096@16 "$base.melf"`
+case "$out" in
+ 5.14159265358979323846264338327950288419716939937510582097*) ;;
+ *) echo "unexpected DATA result: $out" >&2; exit 1 ;;
+esac
diff --git a/tests/tst-stack-runner.sh b/tests/tst-stack-runner.sh
new file mode 100755
index 0000000..2bba333
--- /dev/null
+++ b/tests/tst-stack-runner.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${TEST_SRCDIR:?}"
+base="${TMPDIR:-/tmp}/libcpu-stack-$$"
+trap 'rm -f "$base.s" "$base.o" "$base.melf"' EXIT HUP INT TERM
+"$CPU_AS" -o "$base.o" "$TEST_SRCDIR/fixtures/stack.s" >/dev/null
+"$CPU_LD" --stack-size=128 -o "$base.melf" "$base.o" >/dev/null
+out=`"$CPU_RUN" --int-reg=r64@0x00028 "$base.melf"`
+test "x$out" = "x22"
diff --git a/tests/tst-stack.c b/tests/tst-stack.c
new file mode 100644
index 0000000..9c719ed
--- /dev/null
+++ b/tests/tst-stack.c
@@ -0,0 +1,104 @@
+#include <stdio.h>
+#include <string.h>
+#include <libmcpu.h>
+#include "../utils/mcpu-asm.h"
+
+#define CHECK(c,m) do{if(!(c)){fprintf(stderr,"FAIL: %s\n",m);return 1;}}while(0)
+
+static const char src[]=
+".text\n"
+" LDI r64@0x00000, 11\n"
+" LDI r64@0x00008, 22\n"
+" PUSH r64@0x00000\n"
+" PUSH r64@0x00008\n"
+" LDSP r64@0x00010, 8\n"
+" STSP 0, r64@0x00000\n"
+" POP r64@0x00018\n"
+" POP r64@0x00020\n"
+" ADJSP -16\n"
+" STSP 0, r64@0x00008\n"
+" LDSP r64@0x00028, 0\n"
+" ADJSP 16\n"
+" HALT\n";
+
+static int
+eq64( unsigned address, const char *text )
+{
+ mpu_int64_t x;
+ iatoi( ( mpu_int * ) x, (__mpu_char8_t *)text, NB_I64 );
+ return( !memcmp( __cpu_reg_ptr( 64, address ), x, NB_I64 ) );
+}
+
+int
+main( void )
+{
+ cpu_asm_image im;
+ cpu_instruction i;
+ cpu_operand *o;
+ char err[256];
+ __mpu_uint64_t sp;
+ __mpu_UNIT_t before[8];
+ int rc;
+
+ __cpu_init();
+ CHECK( __cpu_stack_alloc( 128 ) == 0, "allocate STACK" );
+ CHECK( __cpu_stack_ptr() != 0, "STACK pointer" );
+ CHECK( __cpu_stack_size() == 128, "STACK size" );
+ CHECK( __cpu_sp == 128, "empty STACK starts at stack_size" );
+
+ CHECK( cpu_assemble_text( src, &im, err, sizeof( err ) ) == 0, err );
+ CHECK( im.code_size > 0, "stack bytecode produced" );
+ CHECK( __cpu_program_set( im.code, im.code_size ) == 0, "program set" );
+
+ rc = __cpu_run();
+ CHECK( rc == CPU_EXEC_SUCCESS, "stack program run" );
+ CHECK( __cpu_sp == 128, "SP restored after balanced stack operations" );
+ CHECK( eq64( 16, "11" ), "LDSP reads SP+8" );
+ CHECK( eq64( 24, "11" ), "STSP overwrites top before POP" );
+ CHECK( eq64( 32, "11" ), "second POP value" );
+ CHECK( eq64( 40, "22" ), "ADJSP frame with STSP/LDSP" );
+
+ /* PUSH overflow/underflow faults are atomic. */
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_PUSH;
+ i.operand[0].type = CPU_OPERAND_REGISTER;
+ i.operand[0].value.reg.bits = 64;
+ i.operand[0].value.reg.address = 0;
+ __cpu_sp = 4;
+ memcpy( before, __cpu_stack_ptr(), sizeof( before ) );
+ sp = __cpu_sp;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_STACK_FAULT, "PUSH stack overflow fault" );
+ CHECK( __cpu_sp == sp, "PUSH fault preserves SP" );
+ CHECK( !memcmp( before, __cpu_stack_ptr(), sizeof( before ) ), "PUSH fault preserves STACK" );
+
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_POP;
+ o = &i.operand[0];
+ o->type = CPU_OPERAND_REGISTER;
+ o->value.reg.bits = 64;
+ o->value.reg.address = 48;
+ memset( __cpu_reg_ptr( 64, 48 ), 0xa5, 8 );
+ __cpu_sp = 128;
+ sp = __cpu_sp;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_STACK_FAULT, "POP empty stack fault" );
+ CHECK( __cpu_sp == sp, "POP fault preserves SP" );
+ {
+ __mpu_UNIT_t a5[8];
+ memset( a5, 0xa5, sizeof( a5 ) );
+ CHECK( !memcmp( __cpu_reg_ptr( 64, 48 ), a5, 8 ), "POP fault preserves destination" );
+ }
+
+ memset( &i, 0, sizeof( i ) );
+ i.opcode = CPU_OP_ADJSP;
+ i.operand[0].type = CPU_OPERAND_OFF64;
+ i.operand[0].value.off64 = 1;
+ __cpu_sp = 128;
+ CHECK( __cpu_exec( &i ) == CPU_EXEC_STACK_FAULT, "ADJSP beyond stack end" );
+ CHECK( __cpu_sp == 128, "ADJSP fault preserves SP" );
+
+ cpu_asm_image_free( &im );
+ __cpu_free_context();
+ CHECK( __cpu_sp == 0, "SP cleared by free context" );
+ puts( "libcpu stack/SP regression: PASS" );
+ return( 0 );
+}
diff --git a/tests/tst-stage-actions-runner.sh b/tests/tst-stage-actions-runner.sh
new file mode 100755
index 0000000..1deef01
--- /dev/null
+++ b/tests/tst-stage-actions-runner.sh
@@ -0,0 +1,111 @@
+#!/bin/sh
+
+set -eu
+
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${TEST_SRCDIR:?}"
+
+base="${TMPDIR:-/tmp}/libmcpu-stage-actions-$$"
+trap 'rm -f "$base"*' EXIT HUP INT TERM
+
+# The published LIFE example: one scalar print action, two registers, one line.
+"$CPU_AS" -o "$base-life.o" "$TEST_SRCDIR/fixtures/life-print.s" >/dev/null
+"$CPU_LD" -o "$base-life" --stack-size=1K "$base-life.o" >/dev/null
+out=`"$CPU_RUN" --steps=3 \
+ --on-step=print-int-regs:r64@0x00000,r64@0x00008 \
+ -- "$base-life"`
+expected='1 10
+2 20
+3 30'
+test "x$out" = "x$expected"
+
+# AFTER_INIT also exists when the executable has no separate .init section.
+out=`"$CPU_RUN" --steps=1 \
+ --before-run=set-int-regs:r64@0x00000=77 \
+ --after-init=print-int-regs:r64@0x00000 \
+ --on-step=print-int-regs:r64@0x00000,r64@0x00008 \
+ -- "$base-life"`
+expected='77
+1 10'
+test "x$out" = "x$expected"
+
+# Explicit -- must make option-looking tokens ordinary guest argv strings.
+"$CPU_AS" -o "$base-argv.o" "$TEST_SRCDIR/fixtures/argc-argv.s" >/dev/null
+"$CPU_LD" -o "$base-argv" --stack-size=4K "$base-argv.o" >/dev/null
+out=`"$CPU_RUN" --on-step=print-int-regs:r64@0x00000,r8@0x00020,r8@0x00021 \
+ -- "$base-argv" --help -x`
+test "x$out" = "x3 104 120"
+
+cat > "$base-model.s" <<'EOS'
+.data
+ .real 64, 0.0
+
+.input
+ .integer 64, 0
+
+.output
+ .integer 64, 0
+ .integer 64, 0
+
+.init
+_start:
+ END
+
+.text
+_life:
+ END
+ JMP _life
+EOS
+
+"$CPU_AS" -o "$base-model.o" "$base-model.s" >/dev/null
+"$CPU_LD" -o "$base-model" --stack-size=1K "$base-model.o" >/dev/null
+
+# Multiple set/print objects, stage ordering and mpu_sscanf()-based values.
+out=`"$CPU_RUN" --steps=1 --digits=20 \
+ --before-run=set-int-input:64@0=100 \
+ --before-run=set-real-data:64@16=1.5 \
+ --after-init=print-int-input:64@0 \
+ --after-init=print-real-data:64@16 \
+ --on-step=set-int-output:64@0=41,64@8=42 \
+ --on-step=print-int-output:64@0,64@8 \
+ --after-run=print-int-output:64@0,64@8 \
+ -- "$base-model"`
+case "$out" in
+ "100
+1.500000000000000"*"E+0
+41 42
+41 42") ;;
+ *) echo "unexpected stage-action output:" >&2; echo "$out" >&2; exit 1 ;;
+esac
+
+# AFTER_RUN follows FINI -> END on graceful EXIT.
+cat > "$base-fini.s" <<'EOS'
+.text
+_start_life:
+ LDI r32@0x00008, 0
+ EXIT r32@0x00008
+
+.fini
+_fini:
+ LDI r64@0x00000, 99
+ END
+EOS
+"$CPU_AS" -o "$base-fini.o" "$base-fini.s" >/dev/null
+"$CPU_LD" -o "$base-fini" --stack-size=1K "$base-fini.o" >/dev/null
+out=`"$CPU_RUN" --after-run=print-int-regs:r64@0x00000 -- "$base-fini"`
+test "x$out" = "x99"
+
+# A raw stage action is exactly one dump; comma lists are deliberately invalid.
+if "$CPU_RUN" --before-run=print-raw-data:1@16,1@17 -- "$base-model" \
+ >/dev/null 2>&1; then
+ echo "raw stage action accepted a comma-separated list" >&2
+ exit 1
+fi
+
+# Arbitrary raw writes are intentionally absent.
+if "$CPU_RUN" --before-run=set-raw-input:1@0=00 -- "$base-model" \
+ >/dev/null 2>&1; then
+ echo "set-raw-input unexpectedly accepted" >&2
+ exit 1
+fi
+
+exit 0
diff --git a/tests/tst-static-archive-link-runner.sh b/tests/tst-static-archive-link-runner.sh
new file mode 100755
index 0000000..596fa16
--- /dev/null
+++ b/tests/tst-static-archive-link-runner.sh
@@ -0,0 +1,263 @@
+#!/bin/sh
+set -eu
+
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${CPU_READELF:?}" "${CPU_AR:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-static-archive-link-$$
+mkdir -p "$TMP/lib" "$TMP/first" "$TMP/second" "$TMP/order"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+
+cat > "$TMP/main-entry.s" <<'EOS'
+.global _life
+.extern entry
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL entry
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+
+cat > "$TMP/helper.s" <<'EOS'
+.global helper
+.text
+helper:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+cat > "$TMP/entry.s" <<'EOS'
+.global entry
+.extern helper
+.text
+entry:
+ CALL helper
+ RET
+EOS
+
+cat > "$TMP/unused.s" <<'EOS'
+.global unused
+.extern missing_forever
+.text
+unused:
+ CALL missing_forever
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/main-entry.o" "$TMP/main-entry.s"
+"$CPU_AS" -o "$TMP/helper.o" "$TMP/helper.s"
+"$CPU_AS" -o "$TMP/entry.o" "$TMP/entry.s"
+"$CPU_AS" -o "$TMP/unused.o" "$TMP/unused.s"
+
+# The archive intentionally has no symbol-index member. helper.o precedes
+# entry.o: entry.o is extracted first, introduces an undefined helper, and a
+# repeated scan must then reach the earlier helper.o. unused.o must never be
+# extracted, otherwise its missing_forever reference would break the link.
+"$CPU_AR" rc "$TMP/lib/libchain.a" \
+ "$TMP/helper.o" "$TMP/entry.o" "$TMP/unused.o"
+
+"$CPU_LD" --stack-size=4096 --no-heap -L"$TMP/lib" -lchain \
+ -o "$TMP/static-chain" "$TMP/main-entry.o"
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 "$TMP/static-chain"`" = 42 ]
+
+# ABI-v3 also falls back to a static archive when no libchain.so exists.
+# Extracted members are part of the output image and therefore add no NEEDED.
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/lib" -lchain -o "$TMP/dynamic-chain" "$TMP/main-entry.o"
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 "$TMP/dynamic-chain"`" = 42 ]
+"$CPU_READELF" -d "$TMP/dynamic-chain" > "$TMP/dynamic-chain.dynamic"
+[ "`grep -c 'NEEDED' "$TMP/dynamic-chain.dynamic" || true`" -eq 0 ]
+
+# ABI-v3 ET_DYN output follows the same static-archive fallback rule. The
+# selected members become part of the shared object and do not add NEEDED.
+cat > "$TMP/wrapper.s" <<'EOS'
+.global wrapper
+.extern entry
+.text
+wrapper:
+ CALL entry
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/wrapper.o" "$TMP/wrapper.s"
+"$CPU_LD" --shared --soname=libwrapper.so.1 \
+ -L"$TMP/lib" -lchain -o "$TMP/libwrapper.so" "$TMP/wrapper.o"
+"$CPU_READELF" -d "$TMP/libwrapper.so" > "$TMP/libwrapper.dynamic"
+grep -q 'SONAME.*\[libwrapper.so.1\]' "$TMP/libwrapper.dynamic"
+[ "`grep -c 'NEEDED' "$TMP/libwrapper.dynamic" || true`" -eq 0 ]
+
+# A weak undefined symbol does not cause archive extraction. Its PTR64
+# relocation therefore remains the established NULL weak result.
+cat > "$TMP/weak-main.s" <<'EOS'
+.global _life
+.weak optional
+.data
+optional_ptr:
+ .ptr optional
+.text
+_start_life:
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+
+cat > "$TMP/optional.s" <<'EOS'
+.global optional
+.text
+optional:
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/weak-main.o" "$TMP/weak-main.s"
+"$CPU_AS" -o "$TMP/optional.o" "$TMP/optional.s"
+"$CPU_AR" rcs "$TMP/lib/liboptional.a" "$TMP/optional.o"
+"$CPU_LD" --stack-size=4096 --no-heap -L"$TMP/lib" -loptional \
+ -o "$TMP/weak-main" "$TMP/weak-main.o"
+[ "`"$CPU_RUN" --int-data=64@16 "$TMP/weak-main"`" = 0 ]
+
+# Static archive order across -l operands follows traditional one-pass library
+# semantics. A later archive can satisfy references introduced by an earlier
+# one, but an already-scanned archive is not implicitly revisited.
+cat > "$TMP/order-main.s" <<'EOS'
+.global _life
+.extern a1
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL a1
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+
+cat > "$TMP/a1.s" <<'EOS'
+.global a1
+.extern b1
+.text
+a1:
+ CALL b1
+ RET
+EOS
+
+cat > "$TMP/a2.s" <<'EOS'
+.global a2
+.text
+a2:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+cat > "$TMP/b1.s" <<'EOS'
+.global b1
+.extern a2
+.text
+b1:
+ CALL a2
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/order-main.o" "$TMP/order-main.s"
+"$CPU_AS" -o "$TMP/a1.o" "$TMP/a1.s"
+"$CPU_AS" -o "$TMP/a2.o" "$TMP/a2.s"
+"$CPU_AS" -o "$TMP/b1.o" "$TMP/b1.s"
+"$CPU_AR" rcs "$TMP/order/liba.a" "$TMP/a1.o" "$TMP/a2.o"
+"$CPU_AR" rcs "$TMP/order/libb.a" "$TMP/b1.o"
+
+if "$CPU_LD" --stack-size=4096 --no-heap -L"$TMP/order" \
+ -la -lb -o "$TMP/order-fail" "$TMP/order-main.o" \
+ >/dev/null 2>"$TMP/order-fail.err"; then
+ echo "previous archive was unexpectedly rescanned across -l operands" >&2
+ exit 1
+fi
+grep -q 'undefined symbol: a2' "$TMP/order-fail.err"
+
+"$CPU_LD" --stack-size=4096 --no-heap -L"$TMP/order" \
+ -la -lb -la -o "$TMP/order-ok" "$TMP/order-main.o"
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 "$TMP/order-ok"`" = 42 ]
+
+# Prepare two providers for -lpick: a static one returns 42, a shared one 43.
+cat > "$TMP/pick-main.s" <<'EOS'
+.global _life
+.extern pick
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL pick
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+
+cat > "$TMP/pick-static.s" <<'EOS'
+.global pick
+.text
+pick:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+cat > "$TMP/pick-shared.s" <<'EOS'
+.global pick
+.text
+pick:
+ LDI r64@0x00008, 3
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/pick-main.o" "$TMP/pick-main.s"
+"$CPU_AS" -o "$TMP/pick-static.o" "$TMP/pick-static.s"
+"$CPU_AS" -o "$TMP/pick-shared.o" "$TMP/pick-shared.s"
+
+"$CPU_AR" rcs "$TMP/first/libpick.a" "$TMP/pick-static.o"
+"$CPU_LD" --shared --soname=libpick.so.7 \
+ -o "$TMP/second/libpick.so.7.0.0" "$TMP/pick-shared.o"
+ln -s libpick.so.7.0.0 "$TMP/second/libpick.so.7"
+ln -s libpick.so.7 "$TMP/second/libpick.so"
+
+# Search is directory-major. The .a in the first -L directory wins over a
+# .so in a later directory.
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/first" -L"$TMP/second" -lpick \
+ -o "$TMP/pick-first-static" "$TMP/pick-main.o"
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 "$TMP/pick-first-static"`" = 42 ]
+"$CPU_READELF" -d "$TMP/pick-first-static" > "$TMP/pick-first-static.dynamic"
+[ "`grep -c 'NEEDED' "$TMP/pick-first-static.dynamic" || true`" -eq 0 ]
+
+# Reversing -L order selects the shared object and records its SONAME.
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/second" -L"$TMP/first" -lpick \
+ -o "$TMP/pick-first-shared" "$TMP/pick-main.o"
+"$CPU_READELF" -d "$TMP/pick-first-shared" > "$TMP/pick-first-shared.dynamic"
+grep -q 'NEEDED.*\[libpick.so.7\]' "$TMP/pick-first-shared.dynamic"
+[ "`"$CPU_RUN" --library-path="$TMP/second" \
+ --int-reg=r64@0x00018 "$TMP/pick-first-shared"`" = 43 ]
+
+# Within one directory ABI-v3 prefers .so over .a.
+cp "$TMP/first/libpick.a" "$TMP/second/libpick.a"
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/second" -lpick -o "$TMP/pick-same-dir" "$TMP/pick-main.o"
+"$CPU_READELF" -d "$TMP/pick-same-dir" | grep -q 'NEEDED.*\[libpick.so.7\]'
+
+# ABI-v1 never selects .so for -l. It uses the archive even when both exist.
+"$CPU_LD" --stack-size=4096 --no-heap \
+ -L"$TMP/second" -lpick -o "$TMP/pick-abi1" "$TMP/pick-main.o"
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 "$TMP/pick-abi1"`" = 42 ]
+
+# With only a shared provider available, ABI-v1 -l must fail rather than
+# silently introducing a dynamic dependency into a static executable.
+rm -f "$TMP/second/libpick.a"
+if "$CPU_LD" --stack-size=4096 --no-heap \
+ -L"$TMP/second" -lpick -o "$TMP/pick-abi1-bad" "$TMP/pick-main.o" \
+ >/dev/null 2>&1; then
+ echo "ABI-v1 unexpectedly selected a shared library" >&2
+ exit 1
+fi
+
+"$CPU_LD" --help 2>&1 | grep -q 'ABI-v1 and -r search .a'
+"$CPU_LD" --help 2>&1 | grep -q 'ABI-v3 searches .so then .a'
+
+exit 0
diff --git a/tests/tst-static-option-runner.sh b/tests/tst-static-option-runner.sh
new file mode 100755
index 0000000..1515a7f
--- /dev/null
+++ b/tests/tst-static-option-runner.sh
@@ -0,0 +1,124 @@
+#!/bin/sh
+set -eu
+
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${CPU_READELF:?}" "${CPU_AR:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-static-option-$$
+mkdir -p "$TMP/both" "$TMP/soonly" "$TMP/aonly"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+
+cat > "$TMP/main.s" <<'EOS'
+.global _life
+.extern pick
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL pick
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+
+cat > "$TMP/wrapper.s" <<'EOS'
+.global wrapper
+.extern pick
+.text
+wrapper:
+ CALL pick
+ RET
+EOS
+
+cat > "$TMP/pick-static.s" <<'EOS'
+.global pick
+.text
+pick:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+cat > "$TMP/pick-shared.s" <<'EOS'
+.global pick
+.text
+pick:
+ LDI r64@0x00008, 3
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/main.o" "$TMP/main.s"
+"$CPU_AS" -o "$TMP/wrapper.o" "$TMP/wrapper.s"
+"$CPU_AS" -o "$TMP/pick-static.o" "$TMP/pick-static.s"
+"$CPU_AS" -o "$TMP/pick-shared.o" "$TMP/pick-shared.s"
+
+"$CPU_AR" rcs "$TMP/both/libpick.a" "$TMP/pick-static.o"
+cp "$TMP/both/libpick.a" "$TMP/aonly/libpick.a"
+"$CPU_LD" --shared --soname=libpick.so.1 \
+ -o "$TMP/both/libpick.so.1.0.0" "$TMP/pick-shared.o"
+ln -s libpick.so.1.0.0 "$TMP/both/libpick.so.1"
+ln -s libpick.so.1 "$TMP/both/libpick.so"
+cp "$TMP/both/libpick.so.1.0.0" "$TMP/soonly/libpick.so.1.0.0"
+ln -s libpick.so.1.0.0 "$TMP/soonly/libpick.so.1"
+ln -s libpick.so.1 "$TMP/soonly/libpick.so"
+
+# Normal ABI-v3 lookup prefers .so in a directory containing both providers.
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/both" -lpick -o "$TMP/dynamic-shared" "$TMP/main.o"
+"$CPU_READELF" -d "$TMP/dynamic-shared" > "$TMP/dynamic-shared.dynamic"
+grep -q 'NEEDED.*\[libpick.so.1\]' "$TMP/dynamic-shared.dynamic"
+[ "`"$CPU_RUN" --library-path="$TMP/both" \
+ --int-reg=r64@0x00018 "$TMP/dynamic-shared"`" = 43 ]
+
+# --static changes only -l resolution. The output is still ABI-v3 ET_EXEC,
+# but libpick.a is selected and no DT_NEEDED is emitted.
+"$CPU_LD" --dynamic --static --stack-size=4096 --no-heap \
+ -L"$TMP/both" -lpick -o "$TMP/dynamic-static" "$TMP/main.o"
+"$CPU_READELF" -h "$TMP/dynamic-static" > "$TMP/dynamic-static.header"
+"$CPU_READELF" -d "$TMP/dynamic-static" > "$TMP/dynamic-static.dynamic"
+grep -q 'ABI Version:.*3' "$TMP/dynamic-static.header"
+[ "`grep -c 'NEEDED' "$TMP/dynamic-static.dynamic" || true`" -eq 0 ]
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 "$TMP/dynamic-static"`" = 42 ]
+
+# --shared --static remains ABI-v3 ET_DYN with its own SONAME; the selected
+# archive member is incorporated into the image and adds no NEEDED entry.
+"$CPU_LD" --shared --static --soname=libwrapper.so.1 \
+ -L"$TMP/both" -lpick -o "$TMP/libwrapper.so" "$TMP/wrapper.o"
+"$CPU_READELF" -d "$TMP/libwrapper.so" > "$TMP/libwrapper.dynamic"
+grep -q 'SONAME.*\[libwrapper.so.1\]' "$TMP/libwrapper.dynamic"
+[ "`grep -c 'NEEDED' "$TMP/libwrapper.dynamic" || true`" -eq 0 ]
+
+# -r already accepts static archives only; --static is therefore a harmless
+# explicit statement of the same library-selection policy.
+"$CPU_LD" -r --static -L"$TMP/both" -lpick \
+ -o "$TMP/partial.o" "$TMP/wrapper.o"
+"$CPU_READELF" -s "$TMP/partial.o" > "$TMP/partial.sym"
+grep -q '[[:space:]]pick$' "$TMP/partial.sym"
+
+# ABI-v1 is already fully static; accepting --static must preserve that path.
+"$CPU_LD" --static --stack-size=4096 --no-heap \
+ -L"$TMP/both" -lpick -o "$TMP/abi1-static" "$TMP/main.o"
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 "$TMP/abi1-static"`" = 42 ]
+
+# Forced-static lookup skips a .so-only earlier directory and continues until
+# it finds libpick.a in a later directory.
+"$CPU_LD" --dynamic --static --stack-size=4096 --no-heap \
+ -L"$TMP/soonly" -L"$TMP/aonly" -lpick \
+ -o "$TMP/later-archive" "$TMP/main.o"
+[ "`"$CPU_RUN" --int-reg=r64@0x00018 "$TMP/later-archive"`" = 42 ]
+"$CPU_READELF" -d "$TMP/later-archive" > "$TMP/later-archive.dynamic"
+[ "`grep -c 'NEEDED' "$TMP/later-archive.dynamic" || true`" -eq 0 ]
+
+# With only a shared provider available, --static must fail and must not fall
+# back to libpick.so.
+if "$CPU_LD" --dynamic --static --stack-size=4096 --no-heap \
+ -L"$TMP/soonly" -lpick -o "$TMP/missing-static" "$TMP/main.o" \
+ >/dev/null 2>"$TMP/missing-static.err"; then
+ echo "--static unexpectedly selected a shared library" >&2
+ exit 1
+fi
+grep -q 'libpick.a' "$TMP/missing-static.err"
+
+"$CPU_LD" --help 2>&1 | grep -q -- '--static'
+"$CPU_LD" --help 2>&1 | grep -q 'force every -l<name> lookup to use lib<name>.a'
+
+exit 0
diff --git a/tests/tst-strip-fixture.c b/tests/tst-strip-fixture.c
new file mode 100644
index 0000000..1d1fae3
--- /dev/null
+++ b/tests/tst-strip-fixture.c
@@ -0,0 +1,266 @@
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <libmcpu.h>
+#include <mcpu-elf.h>
+
+#include "mcpu-melf.h"
+
+
+typedef struct fixture_shdr
+{
+ __mpu_uint32_t name;
+ __mpu_uint32_t type;
+ __mpu_uint64_t flags;
+ __mpu_uint64_t addr;
+ __mpu_uint64_t offset;
+ __mpu_uint64_t size;
+ __mpu_uint32_t link;
+ __mpu_uint32_t info;
+ __mpu_uint64_t align;
+ __mpu_uint64_t entsize;
+} fixture_shdr;
+
+
+static void
+get_shdr( const unsigned char *p, int data, fixture_shdr *sh )
+{
+ memset( sh, 0, sizeof( *sh ) );
+ sh->name = cpu_melf_get32( p + MELF64_SHDR_SH_NAME_OFF, data );
+ sh->type = cpu_melf_get32( p + MELF64_SHDR_SH_TYPE_OFF, data );
+ sh->flags = cpu_melf_get64( p + MELF64_SHDR_SH_FLAGS_OFF, data );
+ sh->addr = cpu_melf_get64( p + MELF64_SHDR_SH_ADDR_OFF, data );
+ sh->offset = cpu_melf_get64( p + MELF64_SHDR_SH_OFFSET_OFF, data );
+ sh->size = cpu_melf_get64( p + MELF64_SHDR_SH_SIZE_OFF, data );
+ sh->link = cpu_melf_get32( p + MELF64_SHDR_SH_LINK_OFF, data );
+ sh->info = cpu_melf_get32( p + MELF64_SHDR_SH_INFO_OFF, data );
+ sh->align = cpu_melf_get64( p + MELF64_SHDR_SH_ADDRALIGN_OFF, data );
+ sh->entsize = cpu_melf_get64( p + MELF64_SHDR_SH_ENTSIZE_OFF, data );
+
+} /* End of get_shdr() */
+
+
+static void
+put_shdr( unsigned char *p, int data, const fixture_shdr *sh )
+{
+ memset( p, 0, MELF64_SHDR_SIZE );
+ cpu_melf_put32( p + MELF64_SHDR_SH_NAME_OFF, sh->name, data );
+ cpu_melf_put32( p + MELF64_SHDR_SH_TYPE_OFF, sh->type, data );
+ cpu_melf_put64( p + MELF64_SHDR_SH_FLAGS_OFF, sh->flags, data );
+ cpu_melf_put64( p + MELF64_SHDR_SH_ADDR_OFF, sh->addr, data );
+ cpu_melf_put64( p + MELF64_SHDR_SH_OFFSET_OFF, sh->offset, data );
+ cpu_melf_put64( p + MELF64_SHDR_SH_SIZE_OFF, sh->size, data );
+ cpu_melf_put32( p + MELF64_SHDR_SH_LINK_OFF, sh->link, data );
+ cpu_melf_put32( p + MELF64_SHDR_SH_INFO_OFF, sh->info, data );
+ cpu_melf_put64( p + MELF64_SHDR_SH_ADDRALIGN_OFF, sh->align, data );
+ cpu_melf_put64( p + MELF64_SHDR_SH_ENTSIZE_OFF, sh->entsize, data );
+
+} /* End of put_shdr() */
+
+
+static const char *
+name_at( const unsigned char *table, size_t size, __mpu_uint32_t offset )
+{
+ const unsigned char *p;
+
+ if( offset >= size )
+ return( NULL );
+ p = table + offset;
+ if( !memchr( p, 0, size - offset ) )
+ return( NULL );
+ return( (const char *)p );
+
+} /* End of name_at() */
+
+
+int
+main( int argc, char **argv )
+{
+ unsigned char *file = NULL;
+ unsigned char *out = NULL;
+ fixture_shdr *sh = NULL;
+ const unsigned char *old_shstr;
+ size_t file_size = 0;
+ size_t old_shstr_size;
+ size_t new_shstr_size;
+ size_t i;
+ size_t text_index = 0;
+ int data;
+ int overflow = 0;
+ __mpu_uint64_t old_shoff;
+ __mpu_uint64_t cur;
+ __mpu_uint64_t new_shoff;
+ __mpu_uint64_t total;
+ __mpu_uint16_t shnum;
+ __mpu_uint16_t shentsize;
+ __mpu_uint16_t shstrndx;
+ fixture_shdr strtab;
+ fixture_shdr symtab;
+ fixture_shdr shstr;
+ unsigned char symbols[2 * MELF64_SYM_SIZE];
+ const char fixture_strtab[] = "\0fixture_symbol\0";
+ const char strtab_name[] = ".strtab";
+ const char symtab_name[] = ".symtab";
+ char err[256];
+ int rc = EXIT_FAILURE;
+
+ if( argc != 3 )
+ {
+ fprintf( stderr, "usage: %s INPUT-ET_EXEC-OR-ET_DYN OUTPUT\n", argv[0] );
+ return( EXIT_FAILURE );
+ }
+
+ if( cpu_melf_readall( argv[1], &file, &file_size, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "%s\n", err );
+ return( EXIT_FAILURE );
+ }
+
+ if( file_size < MELF64_EHDR_SIZE ||
+ file[MELF_EI_MAG0] != MELFMAG0 || file[MELF_EI_MAG1] != MELFMAG1 ||
+ file[MELF_EI_MAG2] != MELFMAG2 || file[MELF_EI_MAG3] != MELFMAG3 ||
+ file[MELF_EI_CLASS] != MELF_CLASS_64 )
+ goto out;
+
+ data = file[MELF_EI_DATA];
+ if( data != MELF_DATA_LSB && data != MELF_DATA_MSB )
+ goto out;
+ {
+ __mpu_uint16_t type = cpu_melf_get16(
+ file + MELF64_EHDR_E_TYPE_OFF, data );
+
+ if( type != MELF_ET_EXEC && type != MELF_ET_DYN )
+ goto out;
+ }
+
+ old_shoff = cpu_melf_get64( file + MELF64_EHDR_E_SHOFF_OFF, data );
+ shnum = cpu_melf_get16( file + MELF64_EHDR_E_SHNUM_OFF, data );
+ shentsize = cpu_melf_get16( file + MELF64_EHDR_E_SHENTSIZE_OFF, data );
+ shstrndx = cpu_melf_get16( file + MELF64_EHDR_E_SHSTRNDX_OFF, data );
+ if( !shnum || shnum > UINT16_MAX - 2 || shentsize != MELF64_SHDR_SIZE ||
+ shstrndx >= shnum ||
+ !cpu_melf_span( file_size, old_shoff,
+ (__mpu_uint64_t)shnum * MELF64_SHDR_SIZE ) )
+ goto out;
+
+ sh = (fixture_shdr *)calloc( (size_t)shnum + 2, sizeof( *sh ) );
+ if( !sh )
+ goto out;
+ for( i = 0; i < shnum; ++i )
+ get_shdr( file + (size_t)old_shoff + i * MELF64_SHDR_SIZE,
+ data, &sh[i] );
+
+ if( sh[shstrndx].type != MELF_SHT_STRTAB ||
+ !cpu_melf_span( file_size, sh[shstrndx].offset,
+ sh[shstrndx].size ) ||
+ sh[shstrndx].size > SIZE_MAX )
+ goto out;
+
+ old_shstr = file + (size_t)sh[shstrndx].offset;
+ old_shstr_size = (size_t)sh[shstrndx].size;
+ for( i = 1; i < shnum; ++i )
+ {
+ const char *name = name_at( old_shstr, old_shstr_size, sh[i].name );
+
+ if( !name )
+ goto out;
+ if( !strcmp( name, MELF_SEC_TEXT ) )
+ text_index = i;
+ }
+ if( !text_index )
+ goto out;
+
+ cur = old_shoff;
+ memset( &strtab, 0, sizeof( strtab ) );
+ strtab.type = MELF_SHT_STRTAB;
+ strtab.offset = cur;
+ strtab.size = sizeof( fixture_strtab );
+ strtab.align = 1;
+ cur += strtab.size;
+
+ cur = cpu_melf_align_up( cur, 8, &overflow );
+ if( overflow )
+ goto out;
+ memset( &symtab, 0, sizeof( symtab ) );
+ symtab.type = MELF_SHT_SYMTAB;
+ symtab.offset = cur;
+ symtab.size = sizeof( symbols );
+ symtab.link = shnum;
+ symtab.info = 1;
+ symtab.align = 8;
+ symtab.entsize = MELF64_SYM_SIZE;
+ cur += symtab.size;
+
+ if( old_shstr_size > SIZE_MAX - sizeof( strtab_name ) ||
+ old_shstr_size + sizeof( strtab_name ) > SIZE_MAX - sizeof( symtab_name ) )
+ goto out;
+ new_shstr_size = old_shstr_size + sizeof( strtab_name ) +
+ sizeof( symtab_name );
+ shstr = sh[shstrndx];
+ shstr.offset = cur;
+ shstr.size = new_shstr_size;
+ cur += new_shstr_size;
+
+ new_shoff = cpu_melf_align_up( cur, 8, &overflow );
+ if( overflow || new_shoff > UINT64_MAX -
+ (__mpu_uint64_t)( shnum + 2 ) * MELF64_SHDR_SIZE )
+ goto out;
+ total = new_shoff + (__mpu_uint64_t)( shnum + 2 ) * MELF64_SHDR_SIZE;
+ if( total > SIZE_MAX )
+ goto out;
+
+ out = (unsigned char *)calloc( (size_t)total, 1 );
+ if( !out )
+ goto out;
+ memcpy( out, file, (size_t)old_shoff );
+ memcpy( out + (size_t)strtab.offset, fixture_strtab,
+ sizeof( fixture_strtab ) );
+
+ memset( symbols, 0, sizeof( symbols ) );
+ cpu_melf_put32( symbols + MELF64_SYM_SIZE + MELF64_SYM_ST_NAME_OFF,
+ 1, data );
+ symbols[MELF64_SYM_SIZE + MELF64_SYM_ST_INFO_OFF] =
+ MELF64_ST_INFO( MELF_STB_GLOBAL, MELF_STT_FUNC );
+ symbols[MELF64_SYM_SIZE + MELF64_SYM_ST_OTHER_OFF] = MELF_STV_DEFAULT;
+ cpu_melf_put16( symbols + MELF64_SYM_SIZE + MELF64_SYM_ST_SHNDX_OFF,
+ (__mpu_uint16_t)text_index, data );
+ cpu_melf_put64( symbols + MELF64_SYM_SIZE + MELF64_SYM_ST_VALUE_OFF,
+ sh[text_index].addr, data );
+ memcpy( out + (size_t)symtab.offset, symbols, sizeof( symbols ) );
+
+ memcpy( out + (size_t)shstr.offset, old_shstr, old_shstr_size );
+ memcpy( out + (size_t)shstr.offset + old_shstr_size,
+ strtab_name, sizeof( strtab_name ) );
+ memcpy( out + (size_t)shstr.offset + old_shstr_size + sizeof( strtab_name ),
+ symtab_name, sizeof( symtab_name ) );
+
+ strtab.name = (__mpu_uint32_t)old_shstr_size;
+ symtab.name = (__mpu_uint32_t)( old_shstr_size + sizeof( strtab_name ) );
+ sh[shstrndx] = shstr;
+ sh[shnum] = strtab;
+ sh[shnum + 1] = symtab;
+
+ for( i = 0; i < (size_t)shnum + 2; ++i )
+ put_shdr( out + (size_t)new_shoff + i * MELF64_SHDR_SIZE,
+ data, &sh[i] );
+
+ cpu_melf_put64( out + MELF64_EHDR_E_SHOFF_OFF, new_shoff, data );
+ cpu_melf_put16( out + MELF64_EHDR_E_SHNUM_OFF,
+ (__mpu_uint16_t)( shnum + 2 ), data );
+
+ if( cpu_melf_writeall( argv[2], out, (size_t)total, err, sizeof( err ) ) )
+ {
+ fprintf( stderr, "%s\n", err );
+ goto out;
+ }
+ rc = EXIT_SUCCESS;
+
+out:
+ free( out );
+ free( sh );
+ free( file );
+ return( rc );
+
+} /* End of main() */
diff --git a/tests/tst-strip-runner.sh b/tests/tst-strip-runner.sh
new file mode 100755
index 0000000..6d0005d
--- /dev/null
+++ b/tests/tst-strip-runner.sh
@@ -0,0 +1,129 @@
+#!/bin/sh
+set -eu
+
+: "${PACKAGE_VERSION:?}"
+
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${CPU_READELF:?}"
+: "${CPU_AR:?}" "${CPU_NM:?}" "${CPU_STRIP:?}" "${STRIP_FIXTURE:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-strip-$$
+mkdir -p "$TMP"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+
+cat > "$TMP/main.s" <<'EOS'
+.global _life
+.text
+_start_life:
+ LDI r64@0x00000, 42
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+
+
+cat > "$TMP/dynamic-main.s" <<'EOS'
+.global _life
+.extern add_one
+.text
+_start_life:
+ LDI r64@0x00000, 41
+ CALL add_one
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+
+cat > "$TMP/add-one.s" <<'EOS'
+.global add_one
+.text
+add_one:
+ LDI r64@0x00008, 1
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/main.o" "$TMP/main.s"
+"$CPU_AS" -o "$TMP/add-one.o" "$TMP/add-one.s"
+"$CPU_AS" -o "$TMP/dynamic-main.o" "$TMP/dynamic-main.s"
+"$CPU_LD" --stack-size=4096 --no-heap -o "$TMP/main" "$TMP/main.o"
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap -o "$TMP/dynamic-main" "$TMP/dynamic-main.o"
+"$CPU_LD" --shared --soname=libadd.so.1 -o "$TMP/libadd.so" "$TMP/add-one.o"
+"$CPU_AR" rcs "$TMP/libadd.a" "$TMP/add-one.o"
+
+# ET_REL .symtab/.strtab are link semantics and are preserved byte-for-byte.
+"$CPU_STRIP" -o "$TMP/main.stripped.o" "$TMP/main.o"
+cmp "$TMP/main.o" "$TMP/main.stripped.o"
+"$CPU_READELF" -s "$TMP/main.stripped.o" | grep -q '_life'
+
+# Current final writers already emit no ordinary .symtab, so these are no-ops.
+"$CPU_STRIP" -o "$TMP/main.clean" "$TMP/main"
+cmp "$TMP/main" "$TMP/main.clean"
+"$CPU_STRIP" -o "$TMP/libadd.clean.so" "$TMP/libadd.so"
+cmp "$TMP/libadd.so" "$TMP/libadd.clean.so"
+"$CPU_READELF" -S "$TMP/libadd.clean.so" > "$TMP/libadd.sections"
+grep -q '\.dynsym' "$TMP/libadd.sections"
+grep -q '\.dynstr' "$TMP/libadd.sections"
+grep -q '\.mcpu.target' "$TMP/libadd.sections"
+
+# The same removal policy on ET_DYN must preserve dynamic symbols/relocations.
+"$STRIP_FIXTURE" "$TMP/libadd.so" "$TMP/libadd.with-sym.so"
+"$CPU_READELF" -S -s "$TMP/libadd.with-sym.so" > "$TMP/dyn-before.txt"
+grep -q '\.symtab' "$TMP/dyn-before.txt"
+"$CPU_STRIP" -o "$TMP/libadd.really-stripped.so" "$TMP/libadd.with-sym.so"
+"$CPU_READELF" -S -s "$TMP/libadd.really-stripped.so" > "$TMP/dyn-after.txt"
+if grep -q '\.symtab' "$TMP/dyn-after.txt"; then
+ echo "ordinary ET_DYN .symtab survived mcpu-strip" >&2
+ exit 1
+fi
+grep -q '\.dynsym' "$TMP/dyn-after.txt"
+grep -q '\.dynstr' "$TMP/dyn-after.txt"
+test "`MCPU_LD_PRELOAD= "$CPU_RUN" --ldpreload="$TMP/libadd.really-stripped.so" --int-reg=r64@0x00018 "$TMP/dynamic-main"`" = 42
+
+# Deterministic ET_REL archives keep their members and symbol index intact.
+"$CPU_STRIP" -o "$TMP/libadd.clean.a" "$TMP/libadd.a"
+cmp "$TMP/libadd.a" "$TMP/libadd.clean.a"
+"$CPU_NM" -A "$TMP/libadd.clean.a" | grep -q 'add_one'
+
+# Add a conventional non-runtime .symtab/.strtab to a valid ET_EXEC fixture.
+# mcpu-strip must remove them, shrink the file and leave execution untouched.
+"$STRIP_FIXTURE" "$TMP/main" "$TMP/main.with-sym"
+chmod 755 "$TMP/main.with-sym"
+"$CPU_READELF" -S -s "$TMP/main.with-sym" > "$TMP/before.txt"
+grep -q '\.symtab' "$TMP/before.txt"
+grep -q 'fixture_symbol' "$TMP/before.txt"
+before=`wc -c < "$TMP/main.with-sym"`
+
+"$CPU_STRIP" -o "$TMP/main.really-stripped" "$TMP/main.with-sym"
+"$CPU_READELF" -S -s "$TMP/main.really-stripped" > "$TMP/after.txt"
+if grep -q '\.symtab' "$TMP/after.txt"; then
+ echo "ordinary .symtab survived mcpu-strip" >&2
+ exit 1
+fi
+if grep -q 'fixture_symbol' "$TMP/after.txt"; then
+ echo "ordinary symbol string survived mcpu-strip" >&2
+ exit 1
+fi
+after=`wc -c < "$TMP/main.really-stripped"`
+test "$after" -lt "$before"
+test "`stat -c %a "$TMP/main.really-stripped"`" = 755
+test "`"$CPU_RUN" --int-reg=r64@0x00000 "$TMP/main.really-stripped"`" = 42
+
+# In-place replacement is atomic from the caller's point of view and keeps mode.
+cp "$TMP/main.with-sym" "$TMP/main.inplace"
+chmod 751 "$TMP/main.inplace"
+"$CPU_STRIP" "$TMP/main.inplace"
+test "`stat -c %a "$TMP/main.inplace"`" = 751
+test "`"$CPU_RUN" --int-reg=r64@0x00000 "$TMP/main.inplace"`" = 42
+if "$CPU_READELF" -S "$TMP/main.inplace" | grep -q '\.symtab'; then
+ echo "in-place strip left .symtab" >&2
+ exit 1
+fi
+
+"$CPU_STRIP" --help | grep -q -- '--output=FILE'
+"$CPU_STRIP" --version | grep -Fq "$PACKAGE_VERSION"
+
+if "$CPU_STRIP" "$TMP/main" "$TMP/main.clean" >/dev/null 2>&1; then
+ echo "mcpu-strip unexpectedly accepted multiple input files" >&2
+ exit 1
+fi
+
+exit 0
diff --git a/tests/tst-symbol-scope-runner.sh b/tests/tst-symbol-scope-runner.sh
new file mode 100755
index 0000000..8a4154e
--- /dev/null
+++ b/tests/tst-symbol-scope-runner.sh
@@ -0,0 +1,276 @@
+#!/bin/sh
+set -eu
+
+: "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-symbol-scope-$$
+mkdir -p "$TMP/lib"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+
+cat > "$TMP/main-import.s" <<'EOS'
+.global _life
+.extern foo
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL foo
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+
+cat > "$TMP/a.s" <<'EOS'
+.global foo
+.text
+foo:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+cat > "$TMP/b.s" <<'EOS'
+.global foo
+.text
+foo:
+ LDI r64@0x00008, 3
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+
+"$CPU_AS" -o "$TMP/main-import.o" "$TMP/main-import.s"
+"$CPU_AS" -o "$TMP/a.o" "$TMP/a.s"
+"$CPU_AS" -o "$TMP/b.o" "$TMP/b.s"
+"$CPU_LD" --shared --soname=liba.so.1 -o "$TMP/lib/liba.so.1.0.0" "$TMP/a.o"
+"$CPU_LD" --shared --soname=libb.so.1 -o "$TMP/lib/libb.so.1.0.0" "$TMP/b.o"
+ln -s liba.so.1.0.0 "$TMP/lib/liba.so.1"
+ln -s liba.so.1 "$TMP/lib/liba.so"
+ln -s libb.so.1.0.0 "$TMP/lib/libb.so.1"
+ln -s libb.so.1 "$TMP/lib/libb.so"
+
+# Normal DT_NEEDED scope is breadth-first and left-to-right. For equal
+# GLOBAL definitions the first direct dependency wins.
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/lib" -la -lb -o "$TMP/main-ab" "$TMP/main-import.o"
+[ "`"$CPU_RUN" --library-path="$TMP/lib" --int-reg=r64@0x00018 "$TMP/main-ab"`" = 42 ]
+
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/lib" -lb -la -o "$TMP/main-ba" "$TMP/main-import.o"
+[ "`"$CPU_RUN" --library-path="$TMP/lib" --int-reg=r64@0x00018 "$TMP/main-ba"`" = 43 ]
+
+# A main-executable GLOBAL outranks normal dependencies.
+cat > "$TMP/main-own.s" <<'EOS'
+.global _life
+.global foo
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL foo
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+foo:
+ LDI r64@0x00008, 1
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+"$CPU_AS" -o "$TMP/main-own.o" "$TMP/main-own.s"
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/lib" -la -o "$TMP/main-own" "$TMP/main-own.o"
+[ "`"$CPU_RUN" --library-path="$TMP/lib" --int-reg=r64@0x00018 "$TMP/main-own"`" = 41 ]
+
+# Explicit preload is the intentional override scope. Equal-strength GLOBAL
+# preloads keep the historical rightmost-higher-priority rule.
+[ "`"$CPU_RUN" --library-path="$TMP/lib" \
+ --ldpreload=liba.so:libb.so \
+ --int-reg=r64@0x00018 "$TMP/main-own"`" = 43 ]
+[ "`"$CPU_RUN" --library-path="$TMP/lib" \
+ --ldpreload=libb.so:liba.so \
+ --int-reg=r64@0x00018 "$TMP/main-own"`" = 42 ]
+
+# Binding strength outranks scope class: a WEAK preload must not preempt a
+# normal GLOBAL definition.
+cat > "$TMP/weak.s" <<'EOS'
+.weak foo
+.text
+foo:
+ LDI r64@0x00008, 9
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+"$CPU_AS" -o "$TMP/weak.o" "$TMP/weak.s"
+"$CPU_LD" --shared --soname=libweak.so.1 -o "$TMP/libweak.so" "$TMP/weak.o"
+[ "`"$CPU_RUN" --library-path="$TMP/lib" --ldpreload="$TMP/libweak.so" \
+ --int-reg=r64@0x00018 "$TMP/main-ab"`" = 42 ]
+
+# If only WEAK definitions exist, the same scope ordering applies: first normal
+# dependency wins, while rightmost explicit preload wins.
+cat > "$TMP/weak-main.s" <<'EOS'
+.global _life
+.weak foo
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL foo
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+cat > "$TMP/wa.s" <<'EOS'
+.weak foo
+.text
+foo:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+cat > "$TMP/wb.s" <<'EOS'
+.weak foo
+.text
+foo:
+ LDI r64@0x00008, 3
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+"$CPU_AS" -o "$TMP/weak-main.o" "$TMP/weak-main.s"
+"$CPU_AS" -o "$TMP/wa.o" "$TMP/wa.s"
+"$CPU_AS" -o "$TMP/wb.o" "$TMP/wb.s"
+"$CPU_LD" --shared --soname=libwa.so.1 -o "$TMP/lib/libwa.so.1" "$TMP/wa.o"
+"$CPU_LD" --shared --soname=libwb.so.1 -o "$TMP/lib/libwb.so.1" "$TMP/wb.o"
+ln -s libwa.so.1 "$TMP/lib/libwa.so"
+ln -s libwb.so.1 "$TMP/lib/libwb.so"
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/lib" -lwa -lwb -o "$TMP/weak-main" "$TMP/weak-main.o"
+[ "`"$CPU_RUN" --library-path="$TMP/lib" --int-reg=r64@0x00018 "$TMP/weak-main"`" = 42 ]
+[ "`"$CPU_RUN" --ldpreload="$TMP/lib/libwa.so.1:$TMP/lib/libwb.so.1" \
+ --int-reg=r64@0x00018 "$TMP/weak-main"`" = 43 ]
+
+# A direct dependency outranks a conflicting transitive dependency. The
+# resolver must therefore build a true breadth-first scope rather than expose
+# DFS traversal order to symbol resolution.
+cat > "$TMP/deep.s" <<'EOS'
+.global deep_anchor
+.global foo
+.text
+deep_anchor:
+ RET
+foo:
+ LDI r64@0x00008, 9
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+cat > "$TMP/mid.s" <<'EOS'
+.global mid_anchor
+.extern deep_anchor
+.text
+mid_anchor:
+ CALL deep_anchor
+ RET
+EOS
+cat > "$TMP/direct.s" <<'EOS'
+.global foo
+.text
+foo:
+ LDI r64@0x00008, 3
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+cat > "$TMP/bfs-main.s" <<'EOS'
+.global _life
+.extern mid_anchor
+.extern foo
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL mid_anchor
+ CALL foo
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+"$CPU_AS" -o "$TMP/deep.o" "$TMP/deep.s"
+"$CPU_AS" -o "$TMP/mid.o" "$TMP/mid.s"
+"$CPU_AS" -o "$TMP/direct.o" "$TMP/direct.s"
+"$CPU_AS" -o "$TMP/bfs-main.o" "$TMP/bfs-main.s"
+"$CPU_LD" --shared --soname=libdeep.so.1 -o "$TMP/lib/libdeep.so.1.0.0" "$TMP/deep.o"
+ln -s libdeep.so.1.0.0 "$TMP/lib/libdeep.so.1"
+ln -s libdeep.so.1 "$TMP/lib/libdeep.so"
+"$CPU_LD" --shared --soname=libmid.so.1 -L"$TMP/lib" -ldeep \
+ -o "$TMP/lib/libmid.so.1.0.0" "$TMP/mid.o"
+ln -s libmid.so.1.0.0 "$TMP/lib/libmid.so.1"
+ln -s libmid.so.1 "$TMP/lib/libmid.so"
+"$CPU_LD" --shared --soname=libdirect.so.1 -o "$TMP/lib/libdirect.so.1.0.0" "$TMP/direct.o"
+ln -s libdirect.so.1.0.0 "$TMP/lib/libdirect.so.1"
+ln -s libdirect.so.1 "$TMP/lib/libdirect.so"
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/lib" -lmid -ldirect -o "$TMP/bfs-main" "$TMP/bfs-main.o"
+[ "`"$CPU_RUN" --library-path="$TMP/lib" --int-reg=r64@0x00018 "$TMP/bfs-main"`" = 43 ]
+
+# Diamond graph: A and B both depend on C. SONAME deduplication loads C once,
+# while the direct A/B scope still follows left-to-right order.
+cat > "$TMP/common.s" <<'EOS'
+.global common_anchor
+.text
+common_anchor:
+ RET
+EOS
+cat > "$TMP/da.s" <<'EOS'
+.global a_anchor
+.global foo
+.extern common_anchor
+.text
+a_anchor:
+ CALL common_anchor
+ RET
+foo:
+ LDI r64@0x00008, 2
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+cat > "$TMP/db.s" <<'EOS'
+.global b_anchor
+.global foo
+.extern common_anchor
+.text
+b_anchor:
+ CALL common_anchor
+ RET
+foo:
+ LDI r64@0x00008, 3
+ IADD r64@0x00000, r64@0x00000, r64@0x00008
+ RET
+EOS
+cat > "$TMP/diamond-main.s" <<'EOS'
+.global _life
+.extern a_anchor
+.extern b_anchor
+.extern foo
+.text
+_start_life:
+ LDI r64@0x00000, 40
+ CALL a_anchor
+ CALL b_anchor
+ CALL foo
+ MOV r64@0x00018, r64@0x00000
+ LDI r32@0x00030, 0
+ EXIT r32@0x00030
+EOS
+"$CPU_AS" -o "$TMP/common.o" "$TMP/common.s"
+"$CPU_AS" -o "$TMP/da.o" "$TMP/da.s"
+"$CPU_AS" -o "$TMP/db.o" "$TMP/db.s"
+"$CPU_AS" -o "$TMP/diamond-main.o" "$TMP/diamond-main.s"
+"$CPU_LD" --shared --soname=libcommon.so.1 -o "$TMP/lib/libcommon.so.1.0.0" "$TMP/common.o"
+ln -s libcommon.so.1.0.0 "$TMP/lib/libcommon.so.1"
+ln -s libcommon.so.1 "$TMP/lib/libcommon.so"
+"$CPU_LD" --shared --soname=libda.so.1 -L"$TMP/lib" -lcommon \
+ -o "$TMP/lib/libda.so.1.0.0" "$TMP/da.o"
+ln -s libda.so.1.0.0 "$TMP/lib/libda.so.1"
+ln -s libda.so.1 "$TMP/lib/libda.so"
+"$CPU_LD" --shared --soname=libdb.so.1 -L"$TMP/lib" -lcommon \
+ -o "$TMP/lib/libdb.so.1.0.0" "$TMP/db.o"
+ln -s libdb.so.1.0.0 "$TMP/lib/libdb.so.1"
+ln -s libdb.so.1 "$TMP/lib/libdb.so"
+"$CPU_LD" --dynamic --stack-size=4096 --no-heap \
+ -L"$TMP/lib" -lda -ldb -o "$TMP/diamond-main" "$TMP/diamond-main.o"
+[ "`"$CPU_RUN" --library-path="$TMP/lib" --int-reg=r64@0x00018 "$TMP/diamond-main"`" = 42 ]
+
+exit 0
diff --git a/tests/tst-syscall.c b/tests/tst-syscall.c
new file mode 100644
index 0000000..92c3fd3
--- /dev/null
+++ b/tests/tst-syscall.c
@@ -0,0 +1,229 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <libmcpu.h>
+
+#include "mcpu-asm.h"
+
+
+#define TEST_NR_brk 10
+
+#define CHECK(cond,msg) \
+ do { \
+ if( !(cond) ) \
+ { \
+ fprintf( stderr, "FAIL: %s\n", (msg) ); \
+ return( 1 ); \
+ } \
+ } while( 0 )
+
+
+static __mpu_uint64_t
+reg64( __mpu_uint32_t address )
+{
+ return( __cpu_data_get_u64( __cpu_reg_ptr( 64, address ) ) );
+}
+
+
+static void
+put32( __mpu_uint32_t address, __mpu_uint32_t value )
+{
+ __cpu_data_put_u32( __cpu_reg_ptr( 32, address ), value );
+}
+
+
+static void
+put64( __mpu_uint32_t address, __mpu_uint64_t value )
+{
+ __cpu_data_put_u64( __cpu_reg_ptr( 64, address ), value );
+}
+
+
+static int
+run_syscall( __mpu_uint32_t nargs, __mpu_uint32_t sysno,
+ __mpu_uint64_t arg0, __mpu_uint64_t *ret )
+{
+ __mpu_uint64_t keep1 = __mpu_UINT64_C( 0x1111111111111111 );
+ __mpu_uint64_t keep2 = __mpu_UINT64_C( 0x2222222222222222 );
+ __mpu_uint64_t keep3 = __mpu_UINT64_C( 0x3333333333333333 );
+ __mpu_uint64_t keep4 = __mpu_UINT64_C( 0x4444444444444444 );
+ __mpu_uint64_t keep5 = __mpu_UINT64_C( 0x5555555555555555 );
+ int rc;
+
+ put32( CPU_SYSCALL_NARGS_ADDR, nargs );
+ put32( CPU_SYSCALL_SYSNO_ADDR, sysno );
+ put64( CPU_SYSCALL_ARG0_ADDR, arg0 );
+ put64( CPU_SYSCALL_ARG1_ADDR, keep1 );
+ put64( CPU_SYSCALL_ARG2_ADDR, keep2 );
+ put64( CPU_SYSCALL_ARG3_ADDR, keep3 );
+ put64( CPU_SYSCALL_ARG4_ADDR, keep4 );
+ put64( CPU_SYSCALL_ARG5_ADDR, keep5 );
+
+ __cpu_pc = 0;
+ rc = __cpu_run();
+ if( rc != CPU_EXEC_SUCCESS ) return( rc );
+
+ *ret = reg64( CPU_SYSCALL_RET_ADDR );
+
+ if( reg64( CPU_SYSCALL_ARG0_ADDR ) != arg0 ||
+ reg64( CPU_SYSCALL_ARG1_ADDR ) != keep1 ||
+ reg64( CPU_SYSCALL_ARG2_ADDR ) != keep2 ||
+ reg64( CPU_SYSCALL_ARG3_ADDR ) != keep3 ||
+ reg64( CPU_SYSCALL_ARG4_ADDR ) != keep4 ||
+ reg64( CPU_SYSCALL_ARG5_ADDR ) != keep5 )
+ return( -1000 );
+
+ return( 0 );
+}
+
+
+static void
+regop( cpu_operand *o, unsigned int bits, __mpu_uint32_t address )
+{
+ memset( o, 0, sizeof( *o ) );
+ o->type = CPU_OPERAND_REGISTER;
+ o->value.reg.bits = bits;
+ o->value.reg.address = address;
+}
+
+
+int
+main( void )
+{
+ static const char source[] =
+ ".text\n"
+ "SYSCALL\n"
+ "HALT\n";
+ cpu_asm_image image;
+ cpu_instruction store;
+ char err[256];
+ __mpu_uint64_t ptr, ret;
+ __mpu_int64_t sret;
+ size_t i;
+
+ memset( &image, 0, sizeof( image ) );
+
+ __cpu_init();
+
+ CHECK( CPU_SYSCALL_NARGS_ADDR == CPU_SYSCALL_RET_ADDR,
+ "NARGS overlaps low-address half of RET storage" );
+ CHECK( CPU_SYSCALL_SYSNO_ADDR == CPU_SYSCALL_RET_ADDR + 4,
+ "SYSNO overlaps second half of RET storage" );
+ CHECK( CPU_SYSCALL_ARG0_ADDR + 8 == CPU_SYSCALL_RET_ADDR,
+ "ARG0 is immediately below RET" );
+
+ CHECK( cpu_assemble_text( source, &image, err, sizeof( err ) ) == 0, err );
+ CHECK( image.code_size == 4, "SYSCALL/HALT code size" );
+ CHECK( image.code[0] == 0x00 && image.code[1] == 0x04,
+ "SYSCALL opcode encoding" );
+ CHECK( __cpu_program_set( image.code, image.code_size ) == 0,
+ "program load" );
+ CHECK( __cpu_heap_alloc( 64, 8192 ) == 0, "heap allocation" );
+
+ CHECK( __cpu_ptr_make( &ptr, CPU_PTR_SEG_HEAP, CPU_HEAP_BEGIN ) == 0,
+ "make initial HEAP pointer" );
+ put64( 0x00000, ptr );
+ put64( 0x00008, __mpu_UINT64_C( 0xa5a5a5a5a5a5a5a5 ) );
+ memset( &store, 0, sizeof( store ) );
+ store.opcode = CPU_OP_STOREP;
+ regop( &store.operand[0], 64, 0x00000 );
+ regop( &store.operand[1], 64, 0x00008 );
+ CHECK( __cpu_exec( &store ) == CPU_EXEC_MEMORY_FAULT,
+ "initial break protects HEAP above _end" );
+
+ CHECK( run_syscall( 1, TEST_NR_brk, CPU_NULL_PTR, &ret ) == 0,
+ "SYS_brk query" );
+ CHECK( __cpu_ptr_segment( ret ) == CPU_PTR_SEG_HEAP &&
+ __cpu_ptr_offset( ret ) == CPU_HEAP_BEGIN,
+ "initial SYS_brk is HEAP:16" );
+
+ CHECK( __cpu_ptr_make( &ptr, CPU_PTR_SEG_HEAP, CPU_HEAP_BEGIN + 8 ) == 0,
+ "make grown break" );
+ CHECK( run_syscall( 1, TEST_NR_brk, ptr, &ret ) == 0,
+ "SYS_brk grow inside initial heap size" );
+ CHECK( ret == ptr, "SYS_brk grow return" );
+ CHECK( __cpu_exec( &store ) == CPU_EXEC_SUCCESS,
+ "HEAP becomes writable below new break" );
+
+ memset( __cpu_heap_ptr() + CPU_HEAP_BEGIN, 0x5a, 8 );
+ CHECK( __cpu_ptr_make( &ptr, CPU_PTR_SEG_HEAP, CPU_HEAP_BEGIN ) == 0,
+ "make shrink break" );
+ CHECK( run_syscall( 1, TEST_NR_brk, ptr, &ret ) == 0,
+ "SYS_brk shrink to _end" );
+ CHECK( ret == ptr, "SYS_brk shrink return" );
+ for( i = 0; i < 8; ++i )
+ CHECK( __cpu_heap_ptr()[CPU_HEAP_BEGIN + i] == 0,
+ "shrink zeroes released HEAP" );
+ CHECK( __cpu_heap_size() == 64,
+ "shrink does not reduce logical backing size" );
+ CHECK( __cpu_exec( &store ) == CPU_EXEC_MEMORY_FAULT,
+ "shrunk HEAP is inaccessible above _end" );
+
+ CHECK( __cpu_ptr_make( &ptr, CPU_PTR_SEG_HEAP, CPU_HEAP_BEGIN + 8 ) == 0,
+ "make regrown break" );
+ CHECK( run_syscall( 1, TEST_NR_brk, ptr, &ret ) == 0,
+ "SYS_brk regrow inside retained backing" );
+ CHECK( ret == ptr, "SYS_brk regrow return" );
+ for( i = 0; i < 8; ++i )
+ CHECK( __cpu_heap_ptr()[CPU_HEAP_BEGIN + i] == 0,
+ "regrown released HEAP remains zero" );
+
+ CHECK( __cpu_ptr_make( &ptr, CPU_PTR_SEG_HEAP, 1000 ) == 0,
+ "make break inside host capacity" );
+ CHECK( run_syscall( 1, TEST_NR_brk, ptr, &ret ) == 0,
+ "SYS_brk extends logical heap" );
+ CHECK( ret == ptr && __cpu_heap_size() == 1000,
+ "logical heap extension" );
+ __cpu_heap_ptr()[900] = 0x6b;
+
+ CHECK( __cpu_ptr_make( &ptr, CPU_PTR_SEG_HEAP, 5000 ) == 0,
+ "make break requiring backing growth" );
+ CHECK( run_syscall( 1, TEST_NR_brk, ptr, &ret ) == 0,
+ "SYS_brk grows host backing" );
+ CHECK( ret == ptr && __cpu_heap_size() == 5000,
+ "host-backed heap extension" );
+ CHECK( __cpu_heap_ptr()[900] == 0x6b,
+ "heap relocation preserves old contents" );
+ CHECK( __cpu_heap_ptr()[4999] == 0,
+ "newly allocated heap memory is zero" );
+
+ CHECK( __cpu_ptr_make( &ptr, CPU_PTR_SEG_HEAP, 8193 ) == 0,
+ "make break above immutable limit" );
+ CHECK( run_syscall( 1, TEST_NR_brk, ptr, &ret ) == 0,
+ "SYS_brk above limit returns normally" );
+ CHECK( __cpu_ptr_segment( ret ) == CPU_PTR_SEG_HEAP &&
+ __cpu_ptr_offset( ret ) == 5000 && __cpu_heap_size() == 5000,
+ "heap limit rejection preserves current break" );
+
+ CHECK( __cpu_ptr_make( &ptr, CPU_PTR_SEG_DATA, 32 ) == 0,
+ "make non-HEAP pointer" );
+ CHECK( run_syscall( 1, TEST_NR_brk, ptr, &ret ) == 0,
+ "SYS_brk rejects non-HEAP pointer" );
+ CHECK( __cpu_ptr_offset( ret ) == 5000,
+ "invalid SYS_brk returns current break" );
+
+ CHECK( __cpu_ptr_make( &ptr, CPU_PTR_SEG_HEAP, 6000 ) == 0,
+ "make break for bad NARGS" );
+ CHECK( run_syscall( 2, TEST_NR_brk, ptr, &ret ) == 0,
+ "SYS_brk bad NARGS" );
+ CHECK( __cpu_ptr_offset( ret ) == 5000,
+ "bad SYS_brk NARGS returns current break" );
+
+ CHECK( run_syscall( 0, 9, 0, &ret ) == 0,
+ "unimplemented syscall execution" );
+ memcpy( &sret, &ret, sizeof( sret ) );
+ CHECK( sret == -(__mpu_int64_t)ENOSYS,
+ "unimplemented syscall returns -ENOSYS" );
+
+ cpu_asm_image_free( &image );
+ __cpu_free_context();
+
+ puts( "libmcpu SYSCALL/SYS_brk regression: PASS" );
+ return( 0 );
+
+} /* End of main() */
diff --git a/tests/tst-tool-config-runner.sh b/tests/tst-tool-config-runner.sh
new file mode 100755
index 0000000..790da58
--- /dev/null
+++ b/tests/tst-tool-config-runner.sh
@@ -0,0 +1,97 @@
+#!/bin/sh
+set -eu
+
+: "${TOOL_CONFIG_TEST:?}" "${CPU_AS:?}" "${CPU_LD:?}" "${CPU_RUN:?}" "${TEST_SRCDIR:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-tool-config-$$
+mkdir -p "$TMP/home/.mcpu/etc" "$TMP/relative-root/lib"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+
+HOME="$TMP/home"
+export HOME
+
+# Baseline: no config files, executable-derived root remains active.
+"$TOOL_CONFIG_TEST" --no-config > "$TMP/default.txt"
+DEFAULT_LIB=$(sed -n 's/^lib=//p' "$TMP/default.txt")
+test -n "$DEFAULT_LIB"
+
+# The obsolete per-user path must be ignored.
+printf '%s\n' 'MCPU_LD_SYSTEM_LIBRARY_PATH = /obsolete/path;' \
+ > "$HOME/.mcpu/mcpu-ld.conf"
+"$TOOL_CONFIG_TEST" > "$TMP/old-user.txt"
+test "$(sed -n 's/^lib=//p' "$TMP/old-user.txt")" = "$DEFAULT_LIB"
+
+# The new per-user path has highest user-level priority and expands $HOME.
+cat > "$HOME/.mcpu/etc/mcpu-ld.conf" <<'EOC'
+MCPU_LD_SYSTEM_LIBRARY_PATH = $HOME/mcpu-next/lib;
+EOC
+"$TOOL_CONFIG_TEST" > "$TMP/user.txt"
+test "$(sed -n 's/^lib=//p' "$TMP/user.txt")" = "$HOME/mcpu-next/lib"
+
+# An explicit empty value disables the configured system library tree.
+printf '%s\n' 'MCPU_LD_SYSTEM_LIBRARY_PATH = ;' \
+ > "$HOME/.mcpu/etc/mcpu-ld.conf"
+"$TOOL_CONFIG_TEST" > "$TMP/empty.txt"
+test -z "$(sed -n 's/^lib=//p' "$TMP/empty.txt")"
+
+# --no-config ignores even a malformed user configuration and keeps default.
+printf '%s\n' 'MCPU_LD_SYSTEM_LIBRARY_PATH = broken' \
+ > "$HOME/.mcpu/etc/mcpu-ld.conf"
+"$TOOL_CONFIG_TEST" --no-config > "$TMP/no-config.txt"
+test "$(sed -n 's/^lib=//p' "$TMP/no-config.txt")" = "$DEFAULT_LIB"
+if "$TOOL_CONFIG_TEST" > /dev/null 2>&1; then
+ echo "malformed user configuration was unexpectedly accepted" >&2
+ exit 1
+fi
+
+# Absolute --sys-root implies --no-config and uses PATH/lib.
+"$TOOL_CONFIG_TEST" --sys-root="$TMP/absolute-root" > "$TMP/absolute.txt"
+test "$(sed -n 's/^root=//p' "$TMP/absolute.txt")" = "$TMP/absolute-root"
+test "$(sed -n 's/^lib=//p' "$TMP/absolute.txt")" = "$TMP/absolute-root/lib"
+
+# Relative --sys-root is resolved from the invocation working directory.
+(
+ cd "$TMP"
+ "$TOOL_CONFIG_TEST" --sys-root=relative-root > "$TMP/relative.txt"
+)
+test "$(sed -n 's/^root=//p' "$TMP/relative.txt")" = "$TMP/relative-root"
+test "$(sed -n 's/^lib=//p' "$TMP/relative.txt")" = "$TMP/relative-root/lib"
+
+
+# The production linker and runner use the same policy state.
+"$CPU_AS" -o "$TMP/radd.o" "$TEST_SRCDIR/fixtures/radd-pi.s" >/dev/null
+if "$CPU_LD" --stack-size=512 -o "$TMP/radd.melf" "$TMP/radd.o" >/dev/null 2>&1; then
+ echo "mcpu-ld unexpectedly accepted malformed user configuration" >&2
+ exit 1
+fi
+"$CPU_LD" --no-config --stack-size=512 \
+ -o "$TMP/radd.melf" "$TMP/radd.o" >/dev/null
+"$CPU_RUN" --no-config --digits=60 --real-data=4096@16 \
+ "$TMP/radd.melf" > "$TMP/run-no-config.txt"
+case "$(cat "$TMP/run-no-config.txt")" in
+ 5.14159265358979323846264338327950288419716939937510582097*) ;;
+ *) echo "unexpected --no-config execution result" >&2; exit 1 ;;
+esac
+
+if "$CPU_RUN" --digits=60 --real-data=4096@16 \
+ "$TMP/radd.melf" >/dev/null 2>&1; then
+ echo "mcpu-run unexpectedly accepted malformed user configuration" >&2
+ exit 1
+fi
+
+"$CPU_LD" --sys-root "$TMP/absolute-root" --stack-size=512 \
+ -o "$TMP/radd-sysroot.melf" "$TMP/radd.o" >/dev/null
+"$CPU_RUN" --sys-root="$TMP/absolute-root" --digits=60 \
+ --real-data=4096@16 "$TMP/radd-sysroot.melf" > "$TMP/run-sysroot.txt"
+case "$(cat "$TMP/run-sysroot.txt")" in
+ 5.14159265358979323846264338327950288419716939937510582097*) ;;
+ *) echo "unexpected --sys-root execution result" >&2; exit 1 ;;
+esac
+
+# Both production tools expose the common policy options.
+"$CPU_LD" --help 2>&1 | grep -q -- '--no-config'
+"$CPU_LD" --help 2>&1 | grep -q -- '--sys-root=PATH'
+"$CPU_RUN" --help 2>&1 | grep -q -- '--no-config'
+"$CPU_RUN" --help 2>&1 | grep -q -- '--sys-root=PATH'
+
+exit 0
diff --git a/tests/tst-tool-config.c b/tests/tst-tool-config.c
new file mode 100644
index 0000000..8ea0ec6
--- /dev/null
+++ b/tests/tst-tool-config.c
@@ -0,0 +1,45 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+
+#include "mcpu-tool-options.h"
+
+int
+main( int argc, char **argv )
+{
+ mcpu_tool_options options;
+ int i;
+ int rc = 1;
+
+ mcpu_tool_options_init( &options, argv[0] );
+
+ for( i = 1; i < argc; ++i )
+ {
+ int parsed = mcpu_tool_parse_common_option( &options, argc, argv, &i );
+
+ if( parsed < 0 )
+ goto done;
+ if( parsed == 0 )
+ {
+ fprintf( stderr, "%s: unknown option '%s'\n",
+ options.progname, argv[i] );
+ goto done;
+ }
+ }
+
+ if( mcpu_tool_options_prepare(&options, argv[0]) != 0 )
+ goto done;
+
+ printf( "root=%s\n", options.runtime.installation_root );
+ printf( "config=%s\n", options.runtime.config_file );
+ printf( "lib=%s\n", options.runtime.system_library_path );
+ rc = 0;
+
+done:
+ mcpu_tool_options_free( &options );
+ return( rc );
+
+} /* End of main() */
diff --git a/tests/tst-tool-inspection-runner.sh b/tests/tst-tool-inspection-runner.sh
new file mode 100755
index 0000000..fa34715
--- /dev/null
+++ b/tests/tst-tool-inspection-runner.sh
@@ -0,0 +1,80 @@
+#!/bin/sh
+set -eu
+
+: "${CPU_LD:?}" "${CPU_RUN:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-tool-inspection-$$
+mkdir -p "$TMP/home/.mcpu/etc" "$TMP/root/lib"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+
+HOME="$TMP/home"
+export HOME
+
+# --sys-root is self-contained, suppresses config files and needs no input.
+"$CPU_LD" --sys-root="$TMP/root" --dump-config > "$TMP/ld-sysroot-config.txt"
+"$CPU_RUN" --sys-root="$TMP/root" --dump-config > "$TMP/run-sysroot-config.txt"
+cmp "$TMP/ld-sysroot-config.txt" "$TMP/run-sysroot-config.txt"
+grep '^ runtime root: '"$TMP/root"'$' "$TMP/ld-sysroot-config.txt" >/dev/null
+grep '^ configuration: disabled by --sys-root$' "$TMP/ld-sysroot-config.txt" >/dev/null
+grep '^MCPU_LD_SYSTEM_LIBRARY_PATH = '"$TMP/root"'/lib;$' "$TMP/ld-sysroot-config.txt" >/dev/null
+
+cat > "$TMP/search.expected" <<EOT
+libraries:
+ $TMP/root/lib
+EOT
+"$CPU_LD" --sys-root="$TMP/root" --print-search-dirs > "$TMP/ld-search.txt"
+"$CPU_RUN" --sys-root="$TMP/root" --print-search-dirs > "$TMP/run-search.txt"
+cmp "$TMP/search.expected" "$TMP/ld-search.txt"
+cmp "$TMP/search.expected" "$TMP/run-search.txt"
+
+# The effective user configuration is visible and is identical in both tools.
+cat > "$HOME/.mcpu/etc/mcpu-ld.conf" <<EOC
+MCPU_LD_SYSTEM_LIBRARY_PATH = $TMP/user/lib;
+EOC
+"$CPU_LD" --dump-config > "$TMP/ld-user-config.txt"
+"$CPU_RUN" --dump-config > "$TMP/run-user-config.txt"
+cmp "$TMP/ld-user-config.txt" "$TMP/run-user-config.txt"
+grep '^ config: '"$HOME/.mcpu/etc/mcpu-ld.conf"' \[read\]$' "$TMP/ld-user-config.txt" >/dev/null
+grep '^MCPU_LD_SYSTEM_LIBRARY_PATH = '"$TMP/user/lib"';$' "$TMP/ld-user-config.txt" >/dev/null
+
+cat > "$TMP/user-search.expected" <<EOT
+libraries:
+ $TMP/user/lib
+EOT
+"$CPU_LD" --print-search-dirs > "$TMP/ld-user-search.txt"
+"$CPU_RUN" --print-search-dirs > "$TMP/run-user-search.txt"
+cmp "$TMP/user-search.expected" "$TMP/ld-user-search.txt"
+cmp "$TMP/user-search.expected" "$TMP/run-user-search.txt"
+
+# Empty effective system path means an empty search list; there is no fallback.
+printf '%s\n' 'MCPU_LD_SYSTEM_LIBRARY_PATH = ;' \
+ > "$HOME/.mcpu/etc/mcpu-ld.conf"
+printf '%s\n' 'libraries:' > "$TMP/empty.expected"
+"$CPU_LD" --print-search-dirs > "$TMP/ld-empty.txt"
+"$CPU_RUN" --print-search-dirs > "$TMP/run-empty.txt"
+cmp "$TMP/empty.expected" "$TMP/ld-empty.txt"
+cmp "$TMP/empty.expected" "$TMP/run-empty.txt"
+
+# A malformed config breaks normal inspection, but explicit suppression works.
+printf '%s\n' 'MCPU_LD_SYSTEM_LIBRARY_PATH = broken' \
+ > "$HOME/.mcpu/etc/mcpu-ld.conf"
+if "$CPU_LD" --dump-config >/dev/null 2>&1; then
+ echo "mcpu-ld --dump-config unexpectedly accepted malformed configuration" >&2
+ exit 1
+fi
+if "$CPU_RUN" --print-search-dirs >/dev/null 2>&1; then
+ echo "mcpu-run --print-search-dirs unexpectedly accepted malformed configuration" >&2
+ exit 1
+fi
+
+"$CPU_LD" --no-config --dump-config > "$TMP/no-config.txt"
+grep '^ configuration: disabled by --no-config$' "$TMP/no-config.txt" >/dev/null
+"$CPU_RUN" --sys-root="$TMP/root" --print-search-dirs > /dev/null
+
+# Both public help texts expose the same diagnostic actions.
+"$CPU_LD" --help 2>&1 | grep -q -- '--dump-config'
+"$CPU_LD" --help 2>&1 | grep -q -- '--print-search-dirs'
+"$CPU_RUN" --help 2>&1 | grep -q -- '--dump-config'
+"$CPU_RUN" --help 2>&1 | grep -q -- '--print-search-dirs'
+
+exit 0
diff --git a/tests/tst-verbose-runner.sh b/tests/tst-verbose-runner.sh
new file mode 100755
index 0000000..e1362e4
--- /dev/null
+++ b/tests/tst-verbose-runner.sh
@@ -0,0 +1,68 @@
+#!/bin/sh
+set -eu
+
+: "${CPU_AS:?}" "${CPU_LD:?}" "${TEST_SRCDIR:?}"
+
+TMP=${TMPDIR:-/tmp}/libmcpu-verbose-$$
+mkdir -p "$TMP"
+trap 'rm -rf "$TMP"' EXIT HUP INT TERM
+
+src="$TEST_SRCDIR/fixtures/radd-pi.s"
+
+# Production default is quiet: successful assembly and linking write no
+# developer summary unless -v/--verbose is requested.
+out=`"$CPU_AS" -o "$TMP/main.o" "$src"`
+[ -z "$out" ]
+
+out=`"$CPU_LD" --stack-size=512 --no-heap -o "$TMP/main" "$TMP/main.o"`
+[ -z "$out" ]
+
+# Both spellings enable the existing developer summaries without changing
+# their content or the generated object/executable semantics.
+out=`"$CPU_AS" -v -o "$TMP/main-v.o" "$src"`
+case "$out" in
+ "MELF ET_REL: "*" -> $TMP/main-v.o") ;;
+ *) echo "unexpected mcpu-as -v output: $out" >&2; exit 1 ;;
+esac
+
+out=`"$CPU_AS" --verbose -o "$TMP/main-long.o" "$src"`
+case "$out" in
+ "MELF ET_REL: "*" -> $TMP/main-long.o") ;;
+ *) echo "unexpected mcpu-as --verbose output: $out" >&2; exit 1 ;;
+esac
+
+out=`"$CPU_LD" -v --stack-size=512 --no-heap \
+ -o "$TMP/main-v" "$TMP/main-v.o"`
+case "$out" in
+ "MELF ABI1 ET_EXEC: "*" -> $TMP/main-v") ;;
+ *) echo "unexpected mcpu-ld -v output: $out" >&2; exit 1 ;;
+esac
+
+out=`"$CPU_LD" --verbose --dynamic --stack-size=512 --no-heap \
+ -o "$TMP/main-dyn" "$TMP/main-long.o"`
+case "$out" in
+ "MELF ABI3 ET_EXEC: "*" -> $TMP/main-dyn") ;;
+ *) echo "unexpected mcpu-ld --verbose dynamic output: $out" >&2; exit 1 ;;
+esac
+
+out=`"$CPU_LD" -v -r -o "$TMP/main-partial.o" "$TMP/main-v.o"`
+case "$out" in
+ "MELF ET_REL partial link: "*" -> $TMP/main-partial.o") ;;
+ *) echo "unexpected mcpu-ld -v -r output: $out" >&2; exit 1 ;;
+esac
+
+cat > "$TMP/shared.s" <<'EOS'
+.global shared_value
+.text
+shared_value:
+ RET
+EOS
+"$CPU_AS" -o "$TMP/shared.o" "$TMP/shared.s"
+out=`"$CPU_LD" --verbose --shared --soname=libverbose.so.1 \
+ -o "$TMP/libverbose.so" "$TMP/shared.o"`
+case "$out" in
+ "MELF ABI3 ET_DYN: "*" -> $TMP/libverbose.so") ;;
+ *) echo "unexpected mcpu-ld --verbose shared output: $out" >&2; exit 1 ;;
+esac
+
+exit 0