Re: [PATCH 2/3] alpha: add self-extracting compressed kernel boot stub
From: Magnus Lindholm
Date: Sun Sep 13 2026 - 05:48:19 EST
Hi Matt,
On Sat, Sep 12, 2026 at 9:55 PM Matt Turner <mattst88@xxxxxxxxx> wrote:
>
> Alpha only ever supported gzip for the kernel image, and did so in a way
> out of step with every other architecture: aboot, the disk boot loader,
> decompresses vmlinux.gz itself with its own bundled inflate code, so
> adding a compression format meant writing a new decompressor inside
> aboot as well as in the kernel.
>
> Build a proper self-extracting arch/alpha/boot/compressed/vmlinux
> instead, modeled on parisc: a small ELF stub with the real, compressed
> vmlinux linked in, which decompresses the kernel, relocates it into
> place and jumps to its entry point. aboot needs no changes.
>
> The build now emits arch/alpha/boot/vmlinuz rather than vmlinux.gz,
> which was never a name aboot cared about and would be a lie for an ELF
> stub: the bootpz path in the same directory still produces real gzip
> data as vmlinux.nh.gz. Existing aboot.conf files name the image as it
> was installed in /boot, so only the command that copies it out of the
> build tree changes.
>
> Unlike parisc and mips, the stub is linked at START_ADDR rather than at
> a separate low address; see compressed/vmlinux.lds.S. It has no console
> output, since bringing up the HWRPB console callbacks needs more than
> the CRB dispatch the kernel proper does much later, so error() just
> halts.
>
> The legacy SRM netboot path (bootpz.c) is left alone: it has its own
> boot protocol (PALcode reinitialization, self-populated ZERO_PGE,
> build-time-computed image sizes) that does not fit this design without a
> separate rewrite, and keeps using plain gzip via vmlinux.nh.gz.
>
> Signed-off-by: Matt Turner <mattst88@xxxxxxxxx>
> ---
> arch/alpha/Kconfig | 1 +
> arch/alpha/Makefile | 6 +-
> arch/alpha/boot/.gitignore | 2 +
> arch/alpha/boot/Makefile | 16 ++-
> arch/alpha/boot/compressed/.gitignore | 3 +
> arch/alpha/boot/compressed/Makefile | 36 +++++++
> arch/alpha/boot/compressed/head.S | 36 +++++++
> arch/alpha/boot/compressed/misc.c | 178 +++++++++++++++++++++++++++++++
> arch/alpha/boot/compressed/trampoline.S | 46 ++++++++
> arch/alpha/boot/compressed/vmlinux.lds.S | 83 ++++++++++++++
> arch/alpha/boot/compressed/vmlinux.scr | 10 ++
> 11 files changed, 409 insertions(+), 8 deletions(-)
>
> diff --git a/arch/alpha/Kconfig b/arch/alpha/Kconfig
> index 04511242506b..2688bb00f175 100644
> --- a/arch/alpha/Kconfig
> +++ b/arch/alpha/Kconfig
> @@ -30,6 +30,7 @@ config ALPHA
> select HAVE_ASM_MODVERSIONS
> select HAVE_DEBUG_KMEMLEAK
> select HAVE_GUP_FAST
> + select HAVE_KERNEL_GZIP
> select HAVE_PAGE_SIZE_8KB
> select HAVE_PCSPKR_PLATFORM
> select HAVE_PERF_EVENTS
> diff --git a/arch/alpha/Makefile b/arch/alpha/Makefile
> index 7074602dca94..405d4bfd644c 100644
> --- a/arch/alpha/Makefile
> +++ b/arch/alpha/Makefile
> @@ -47,9 +47,9 @@ export LIBS_Y
> boot := arch/alpha/boot
>
> #Default target when executing make with no arguments
> -all boot: $(boot)/vmlinux.gz
> +all boot: $(boot)/vmlinuz
>
> -$(boot)/vmlinux.gz: vmlinux
> +$(boot)/vmlinuz: vmlinux
> $(Q)$(MAKE) $(build)=$(boot) $@
>
> bootimage bootpfile bootpzfile: vmlinux
> @@ -59,7 +59,7 @@ archheaders:
> $(Q)$(MAKE) $(build)=arch/alpha/kernel/syscalls all
>
> define archhelp
> - echo '* boot - Compressed kernel image (arch/alpha/boot/vmlinux.gz)'
> + echo '* boot -using
alpha-unknown-linux-gnu- Compressed kernel image (arch/alpha/boot/vmlinuz)'
> echo ' bootimage - SRM bootable image (arch/alpha/boot/bootimage)'
> echo ' bootpfile - BOOTP bootable image (arch/alpha/boot/bootpfile)'
> echo ' bootpzfile - compressed kernel BOOTP image (arch/alpha/boot/bootpzfile)'
> diff --git a/arch/alpha/boot/.gitignore b/arch/alpha/boot/.gitignore
> new file mode 100644
> index 000000000000..4125506521f6
> --- /dev/null
> +++ b/arch/alpha/boot/.gitignore
> @@ -0,0 +1,2 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +vmlinuz
> diff --git a/arch/alpha/boot/Makefile b/arch/alpha/boot/Makefile
> index d8dba85e606c..7c2a41c1e2a0 100644
> --- a/arch/alpha/boot/Makefile
> +++ b/arch/alpha/boot/Makefile
> @@ -9,9 +9,10 @@
> #
>
> hostprogs := tools/mkbb tools/objstrip
> -targets := vmlinux.gz vmlinux \
> +targets := vmlinuz vmlinux \
> vmlinux.nh tools/lxboot tools/bootlx tools/bootph \
> - tools/bootpzh bootloader bootpheader bootpzheader
> + tools/bootpzh bootloader bootpheader bootpzheader
> +subdir- := compressed
> OBJSTRIP := $(obj)/tools/objstrip
>
> KBUILD_HOSTCFLAGS := -Wall -I$(objtree)/usr/include
> @@ -38,9 +39,14 @@ ifdef INITRD
> cat $(INITRD) >> $@
> endif
>
> -# Compressed kernel image
> -$(obj)/vmlinux.gz: $(obj)/vmlinux FORCE
> - $(call if_changed,gzip)
> +# Self-extracting compressed kernel image: a small ELF stub (see
> +# compressed/) with the real vmlinux linked in compressed, which aboot
> +# loads and enters like a plain vmlinux.
> +$(obj)/compressed/vmlinux: vmlinux FORCE
> + $(Q)$(MAKE) $(build)=$(obj)/compressed $@
> +
> +$(obj)/vmlinuz: $(obj)/compressed/vmlinux FORCE
> + $(call if_changed,copy)
> @echo ' Kernel $@ is ready'
>
> $(obj)/main.o: $(obj)/ksize.h
> diff --git a/arch/alpha/boot/compressed/.gitignore b/arch/alpha/boot/compressed/.gitignore
> new file mode 100644
> index 000000000000..765a08f1bd77
> --- /dev/null
> +++ b/arch/alpha/boot/compressed/.gitignore
> @@ -0,0 +1,3 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +vmlinux
> +vmlinux.lds
> diff --git a/arch/alpha/boot/compressed/Makefile b/arch/alpha/boot/compressed/Makefile
> new file mode 100644
> index 000000000000..d826f7e4384d
> --- /dev/null
> +++ b/arch/alpha/boot/compressed/Makefile
> @@ -0,0 +1,36 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +#
> +# Create a self-extracting compressed vmlinux image from the original
> +# vmlinux. aboot loads and enters it like a plain vmlinux.
> +#
> +
> +OBJECTS := head.o misc.o trampoline.o piggy.o
> +targets := vmlinux.lds vmlinux vmlinux.bin vmlinux.bin.gz
> +targets += $(OBJECTS)
> +
> +# The stub runs before the kernel has identified the CPU it booted on, so
> +# build it for the base Alpha instruction set rather than inheriting the
> +# kernel's -mcpu=.
> +KBUILD_CFLAGS := -D__KERNEL__ -std=gnu11 -Os -fno-strict-aliasing
> +KBUILD_CFLAGS += -fno-PIE -ffreestanding -fno-stack-protector
> +KBUILD_CFLAGS += -fno-asynchronous-unwind-tables
> +KBUILD_CFLAGS += -pipe -mno-fp-regs -ffixed-8
> +KBUILD_CFLAGS += -DDISABLE_BRANCH_PROFILING -D__DISABLE_EXPORTS
> +KBUILD_CFLAGS += $(CC_FLAGS_DIALECT)
> +
> +LDFLAGS_vmlinux := -X --orphan-handling=warn -T
> +$(obj)/vmlinux: $(obj)/vmlinux.lds $(addprefix $(obj)/, $(OBJECTS)) $(LIBS_Y) FORCE
> + $(call if_changed,ld)
> +
> +OBJCOPYFLAGS_vmlinux.bin := -R .comment -R .note -S
> +$(obj)/vmlinux.bin: vmlinux FORCE
> + $(call if_changed,objcopy)
> +
> +suffix-$(CONFIG_KERNEL_GZIP) := gz
> +
> +$(obj)/vmlinux.bin.gz: $(obj)/vmlinux.bin FORCE
> + $(call if_changed,gzip)
> +
> +LDFLAGS_piggy.o := -r --format binary --oformat elf64-alpha -T
> +$(obj)/piggy.o: $(obj)/vmlinux.scr $(obj)/vmlinux.bin.$(suffix-y) FORCE
> + $(call if_changed,ld)
> diff --git a/arch/alpha/boot/compressed/head.S b/arch/alpha/boot/compressed/head.S
> new file mode 100644
> index 000000000000..c689ac439d15
> --- /dev/null
> +++ b/arch/alpha/boot/compressed/head.S
> @@ -0,0 +1,36 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +/*
> + * Entry point for the self-extracting compressed kernel stub. aboot
> + * jumps here with the OSF/1 parameter page (ZERO_PGE) already populated
> + * and $30 set to INIT_STACK, exactly as it would for an uncompressed
> + * vmlinux.
> + */
> +
> +#include <linux/init.h>
> +#include <asm/pal.h>
> +
> + .set noreorder
> +
> +__HEAD
> + .globl __start
> + .ent __start
> +__start:
> + br $29,1f
> +1: ldgp $29,0($29)
> +
> + /*
> + * Clear our own bss. The decompressors keep state there, and
> + * nothing guarantees that aboot zeroed the part of our PT_LOAD
> + * segment that has no file backing.
> + */
> + lda $1,_bss
> + lda $2,_ebss
> +2: cmpult $1,$2,$3
> + beq $3,3f
> + stq $31,0($1)
> + addq $1,8,$1
> + br $31,2b
> +
> +3: jsr $26,decompress_and_boot
> + call_pal PAL_halt
> + .end __start
> diff --git a/arch/alpha/boot/compressed/misc.c b/arch/alpha/boot/compressed/misc.c
> new file mode 100644
> index 000000000000..72998582be68
> --- /dev/null
> +++ b/arch/alpha/boot/compressed/misc.c
> @@ -0,0 +1,178 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Decompression and self-relocation for the compressed kernel boot stub,
> + * based on arch/parisc/boot/compressed/misc.c.
> + *
> + * aboot loads the stub as an ordinary ELF at START_ADDR and jumps to it
> + * with the parameter page and stack already set up; vmlinux.lds.S
> + * explains why the stub has to be linked there. We decompress above our
> + * own bss and never write below START_ADDR, so what aboot prepared stays
> + * valid for the real kernel.
> + */
> +
> +#include <linux/elf.h>
> +#include <linux/kernel.h>
> +#include <linux/string.h>
> +#include <linux/types.h>
> +
> +#include <asm/hwrpb.h>
> +#include <asm/page.h>
> +#include <asm/pal.h>
> +#include <asm/setup.h>
> +
> +#define STATIC static
> +#define memzero(s, n) memset((s), 0, (n))
> +
> +extern char input_data[];
> +extern int input_len;
> +/* output_len is inserted by the linker, possibly at an unaligned address */
> +extern char output_len;
> +
> +extern char _ebss[];
> +extern char trampoline_start[], trampoline_end[];
> +
> +static unsigned long free_mem_ptr;
> +static unsigned long free_mem_end_ptr;
> +static unsigned long mem_end;
> +
> +/* The stub has no console, so the message is only for debuggers. */
> +void __noreturn error(char *m)
> +{
> + while (1)
> + __halt();
> +}
> +
> +#ifdef CONFIG_KERNEL_GZIP
> +#include "../../../../lib/decompress_inflate.c"
> +#endif
> +
> +/* Scratch heap for the decompressors, larger than any of them needs. */
> +#define STUB_HEAP_SIZE (4 * 1024 * 1024)
> +
> +/*
> + * Type punning through a packed struct, to avoid the warnings the regular
> + * get_unaligned_le32() produces when reading a char-typed symbol as an
> + * integer.
> + */
> +static u32 punned_get_unaligned_le32(const void *p)
> +{
> + const struct { __le32 x; } __packed *pp = p;
> +
> + return le32_to_cpu(pp->x);
> +}
> +
> +/*
> + * End of the usable memory cluster that holds the kernel, or 0 if the
> + * HWRPB does not describe one. Used to check that the decompressed image
> + * and the stub's scratch space fit before anything is written.
> + */
> +static unsigned long usable_memory_end(void)
> +{
> + struct hwrpb_struct *hwrpb;
> + struct memdesc_struct *memdesc;
> + unsigned long pfn = (START_ADDR - PAGE_OFFSET) >> PAGE_SHIFT;
> + unsigned long i;
> +
> + hwrpb = (struct hwrpb_struct *)(PAGE_OFFSET + INIT_HWRPB->phys_addr);
> + memdesc = (struct using
alpha-unknown-linux-gnu-using
alpha-unknown-linux-gnu-memdesc_struct *)((unsigned long)hwrpb +
> + hwrpb->mddt_offset);
> +
> + for (i = 0; i < memdesc->numclusters; i++) {
> + struct memclust_struct *cluster = &memdesc->cluster[i];
> + unsigned long end_pfn = cluster->start_pfn + cluster->numpages;
> +
> + if (cluster->usage)
> + continue;
> + if (pfn >= cluster->start_pfn && pfn < end_pfn)
> + return PAGE_OFFSET + (end_pfn << PAGE_SHIFT);
> + }
> +
> + return 0;
> +}
> +
> +/*
> + * Move the decompressed kernel's single PT_LOAD segment down onto
> + * START_ADDR and jump to its entry point. The kernel is always linked
> + * with exactly one PT_LOAD whose p_vaddr equals its e_entry equals
> + * START_ADDR (see arch/alpha/kernel/vmlinux.lds.S); anything else we do
> + * not know how to boot.
> + *
> + * The copy cannot run from this stub, which is itself executing at
> + * START_ADDR; see trampoline.S.
> + */
> +static void __noreturn relocate_and_jump(void *image)
> +{
> + void (*trampoline)(unsigned long dest, unsigned long src,
> + unsigned long filesz, unsigned long memsz,
> + unsigned long entry);
> + unsigned long tramp_len = trampoline_end - trampoline_start;
> + Elf64_Phdr phdr, load_phdr = {};
> + Elf64_Ehdr ehdr;
> + int i, found_load = 0;
> + unsigned long tramp;
> +
> + memcpy(&ehdr, image, sizeof(ehdr));
> +
> + if (ehdr.e_ident[EI_MAG0] != ELFMAG0 ||
> + ehdr.e_ident[EI_MAG1] != ELFMAG1 ||
> + ehdr.e_ident[EI_MAG2] != ELFMAG2 ||
> + ehdr.e_ident[EI_MAG3] != ELFMAG3 ||
> + ehdr.e_ident[EI_CLASS] != ELFCLASS64 ||
> + ehdr.e_ident[EI_DATA] != ELFDATA2LSB ||
> + ehdr.e_machine != EM_ALPHA ||
> + ehdr.e_type != ET_EXEC ||
> + ehdr.e_entry != START_ADDR)
> + error("Not a bootable Alpha kernel image");
> +
> + for (i = 0; i < ehdr.e_phnum; i++) {
> + memcpy(&phdr, image + ehdr.e_phoff + i * ehdr.e_phentsize,
> + sizeof(phdr));
> +
> + if (phdr.p_type != PT_LOAD)
> + continue;
> + if (found_load || phdr.p_vaddr != START_ADDR)
> + error("Unexpected kernel segment layout");
> + found_load = 1;
> + load_phdr = phdr;
> + }
> + if (!found_load)
> + error("Kernel image has no PT_LOAD segment");
> +
> + /*
> + * The trampoline has to outlive both the scratch heap it is copied
> + * into and the region it is about to write, which extends past the
> + * kernel's file image to the end of its bss.
> + */
> + tramp = ALIGN(max(free_mem_end_ptr, START_ADDR + load_phdr.p_memsz), 8);
> + if (mem_end && tramp + tramp_len > mem_end)
> + error("Not enough memory to relocate the kernel");
> +
> + memcpy((void *)tramp, trampoline_start, tramp_len);
> + imb();
> +using
alpha-unknown-linux-gnu-
> + trampoline = (void *)tramp;
> + trampoline(load_phdr.p_vaddr,
> + (unsigned long)image + load_phdr.p_offset,
> + load_phdr.p_filesz, load_phdr.p_memsz, ehdr.e_entry);
> + unreachable();
> +}
> +
> +void __noreturn decompress_and_boot(void)
> +{
> + unsigned long image_len = punned_get_unaligned_le32(&output_len);
> + void *image = (void *)ALIGN((unsigned long)_ebss, PAGE_SIZE);
> +
> + free_mem_ptr = ALIGN((unsigned long)image + image_len, PAGE_SIZE);
> + free_mem_end_ptr = free_mem_ptr + STUB_HEAP_SIZE;
> +
> + mem_end = usable_memory_end();
> + if (mem_end && free_mem_end_ptr > mem_end)
> + error("Not enough memory to decompress the kernel");
> +
> + /* Some decompressors report failure only through the return value. */
> + if (__decompress(input_data, input_len, NULL, NULL, image, image_len,
> + NULL, error))
> + error("Kernel decompression failed");
> +
> + relocate_and_jump(image);
> +}
> diff --git a/arch/alpha/boot/compressed/trampoline.S b/arch/alpha/boot/compressed/trampoline.S
> new file mode 100644
> index 000000000000..bf57d3b0fd7f
> --- /dev/null
> +++ b/arch/alpha/boot/compressed/trampoline.S
> @@ -0,0 +1,46 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +/*
> + * Copy the decompressed kernel onto START_ADDR, clear its bss and jump to
> + * its entry point. See relocate_and_jump() in misc.c for why this cannot
> + * run from the stub itself. It is position independent (no data or
> + * rodata references, no gp) and clobbers only its argument registers and
> + * $1, so misc.c can copy it anywhere and call it there.
> + */
> +
> + .set noreorder
> +
> + .globl trampoline_start
> + .globl trampoline_end
> + .ent trampoline_start
> +trampoline_start:
> + /* $16 = dest, $17 = src, $18 = p_filesz, $19 = p_memsz, $20 = entry */
> +
> + /*
> + * dest and src are both page aligned, and everything above
> + * p_filesz is bss, so round both lengths up and work in quadwords
> + * only, using instructions every Alpha implements.
> + */
> + addq $18,7,$18
> + bic $18,7,$18
> + addq $19,7,$19
> + bic $19,7,$19
> + subq $19,$18,$19 /* bytes to zero after the copy */
> +
> +1: beq $18,2f
> + ldq $1,0($17)
> + stq $1,0($16)
> + addq $17,8,$17
> + addq $16,8,$16
> + subq $18,8,$18
> + br $31,1b
> +
> +2: beq $19,3f
> + stq $31,0($16)
> + addq $16,8,$16
> + subq $19,8,$19
> + br $31,2b
> +
> +3: imb
> + jmp $31,($20)
> + .end trampoline_start
> +trampoline_end:
> diff --git a/arch/alpha/boot/compressed/vmlinux.lds.S b/arch/alpha/boot/compressed/vmlinux.lds.S
> new file mode 100644
> index 000000000000..5328c1ccaccf
> --- /dev/null
> +++ b/arch/alpha/boot/compressed/vmlinux.lds.S
> @@ -0,0 +1,83 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +#include <asm-generic/vmlinux.lds.h>
> +#include <asm/page.h>
> +#include <asm/setup.h>
> +
> +OUTPUT_FORMAT("elf64-alpha")
> +OUTPUT_ARCH(alpha)
> +ENTRY(__start)
> +
> +SECTIONS
> +{
> + /*
> + * Link the stub at the address the real, uncompressed vmlinux has
> + * always occupied. aboot derives the OSF/1 parameter-page
> + * (ZERO_PGE) and initial stack (INIT_STACK) addresses from the
> + * loaded image's own p_vaddr, so a stub linked anywhere else would
> + * hand the real kernel a parameter page it does not expect.
> + */
> + . = START_ADDR;
> +
> + .head.text : {
> + _head = .;
> + HEAD_TEXT
> + _ehead = .;
> + }
> +
> + . = ALIGN(8);
> + .rodata.compressed : {
> + *(.rodata.compressed)
> + }
> +
> + . = ALIGN(PAGE_SIZE);
> + .text : {
> + _text = .;
> + *(.text)
> + *(.text.*)
> + _etext = .;
> + }
> + . = ALIGN(8);
> + .rodata : {
> + *(.rodata)
> + *(.rodata.*)
> + }
> + . = ALIGN(8);
> + .data : {
> + *(.data)
> + *(.data.*)
> + }
> + .got : {
> + *(.got)
> + }
> + .sdata : {
> + *(.sdata)
> + }
> + /*
> + * misc.c decompresses to _ebss, so nothing that the stub still
> + * needs may be placed after .bss. --orphan-handling=warn keeps a
> + * new section from silently landing there.
> + */
> + . = ALIGN(8);
> + .bss : {
> + _bss = .;
> + *(.sbss)
> + *(.scommon)
> + *(.bss)
> + *(.bss.*)
> + *(COMMON)
> + . = ALIGN(8);
> + _ebss = .;
> + }
> +
> + STABS_DEBUG
> + DWARF_DEBUG
> + ELF_DETAILS
> +
> + DISCARDS
> + /DISCARD/ : {
> + *(.eh_frame)
> + *(.modinfo)
> + *(.note)
> + *(.note.*)
> + }
> +}
> diff --git a/arch/alpha/boot/compressed/vmlinux.scr b/arch/alpha/boot/compressed/vmlinux.scr
> new file mode 100644
> index 000000000000..dac2d142bcfa
> --- /dev/nullusing
alpha-unknown-linux-gnu-
> +++ b/arch/alpha/boot/compressed/vmlinux.scr
> @@ -0,0 +1,10 @@
> +SECTIONS
> +{
> + .rodata.compressed : {
> + input_len = .;
> + LONG(input_data_end - input_data) input_data = .;
> + *(.data)
> + output_len = . - 4; /* can be at unaligned address */
> + input_data_end = .;
> + }
> +}
>
> --
> 2.54.0
>
I reviewed the self-extracting boot stub and tested the complete series
on an UP2000+ at 833 MHz through SRM and aboot
1.0_pre20040408.
The bzip2, lzma, xz and zstd self-extracting kernels all booted
successfully.
I additionally boot-tested the bzip2 image with an external 896000-byte
uncompressed cpio initramfs. aboot loaded it at
0xfffffc007ff16000, and the initramfs reached userspace intact.
Reviewed-by: Magnus Lindholm <linmag7@xxxxxxxxx>
Tested-by: Magnus Lindholm <linmag7@xxxxxxxxx>