Booting Linux from CP/M-68K
This is just a pointless proof-of-concept for fun, but I found a way to boot the Linux kernel directly from a running CP/M-68K instance. The method is similar to LOADLIN which can boot Linux directly from DOS on an Intel x86 system. The proper way is just to boot Linux directly as seen in my previous article.
When CP/M-68K runs a program it will be run as "user" instead of the "supervisor" and that will prevent many privileged 68000 instructions from executing, typically instructions needed by a kernel. The switch from supervisor to user happens through an RTE (Return from Exception) instruction, but by replacing this with an RTR (Return and Restore) instruction the supervisor bit is preserved. If there is no MMU to protect the kernel memory, the instruction can be overwritten from user space.
The booting happens in two stages, first a program is run that overwrites that instruction is so that supervisor mode is active for the next program. Afterwards the Linux kernel is loaded into memory and started as if it was a regular CP/M-68K program. Here is a demo video of the concept running on my CP/M-68K emulator.
For the first stage, here an assembly program that can be assembled with RMAC:
org $3e4 ; Compensate for header ($400 - $1c)
; CP/M-68K Header
header:
dc.w $601A ; Magic
dc.l (data - text) ; Text Size
dc.l (end - data) ; Data Size
dc.l $0 ; BSS Size
dc.l $0 ; Symbol Size
dc.l $0 ; Reserved
dc.l $400 ; Start Address
dc.w $FFFF ; Relocation
; Program
text:
move.w #9, D0
move.l #msg1, D1
trap #2 ; Display message on console.
move.w #9, D0
move.l #msg2, D1
trap #2 ; Display message on console.
move.l #$ff4b80, A0
move.w #$4e77, (A0)
rts
; Data
data:
msg1:
dc.b 'Replacing RTE with RTR instruction.',13,10,'$'
msg2:
dc.b 'Next program will run as supervisor!',13,10,'$'
; End
end:
For the second stage, the Linux kernel must be prefixed by a CP/M-68K header to be loaded from disk into memory and then executed. Here is a C program to do that:
#include <arpa/inet.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#pragma pack(1)
typedef struct cpm68k_header_s {
uint16_t magic;
uint32_t text_size;
uint32_t data_size;
uint32_t bss_size;
uint32_t sym_size;
uint32_t reserved;
uint32_t start_addr;
uint16_t relocation;
} cpm68k_header_t;
#pragma pack()
int main(int argc, char *argv[])
{
cpm68k_header_t header;
struct stat st;
FILE *fh_in;
FILE *fh_out;
int c;
if (argc != 3) {
fprintf(stderr, "Usage: %s <in-bin> <out-68k>\n", argv[0]);
return EXIT_FAILURE;
}
if (stat(argv[1], &st) != 0) {
fprintf(stderr, "stat(%s) failed with errno: %d\n", argv[1], errno);
return EXIT_FAILURE;
}
fh_in = fopen(argv[1], "rb");
if (fh_in == NULL) {
fprintf(stderr, "fopen(%s) failed\n", argv[1]);
return EXIT_FAILURE;
}
fh_out = fopen(argv[2], "wb");
if (fh_out == NULL) {
fprintf(stderr, "fopen(%s) failed\n", argv[2]);
fclose(fh_in);
return EXIT_FAILURE;
}
header.magic = htons(0x601A);
header.text_size = htonl(st.st_size);
header.data_size = 0;
header.bss_size = 0;
header.sym_size = 0;
header.reserved = 0;
header.start_addr = htonl(0x400);
header.relocation = htons(0xFFFF);
fwrite(&header, sizeof(cpm68k_header_t), 1, fh_out);
while ((c = fgetc(fh_in)) != EOF) {
fputc(c, fh_out);
}
fclose(fh_in);
fclose(fh_out);
return EXIT_SUCCESS;
}
And a Makefile to tie it together:
all: vmlinux.68k cpmhack.68k
bin2cpm: bin2cpm.c
gcc -o bin2cpm bin2cpm.c -Wall -Wextra
vmlinux.68k: bin2cpm
./bin2cpm vmlinux.bin vmlinux.68k
cpmhack.68k: cpmhack.asm
rmac -fr cpmhack.asm -o cpmhack.68k
.PHONY: clean
clean:
rm -f bin2cpm vmlinux.68k cpmhack.68k