| /* |
| * This file and its contents are supplied under the terms of the |
| * Common Development and Distribution License ("CDDL"), version 1.0. |
| * You may only use this file in accordance with the terms of version |
| * 1.0 of the CDDL. |
| * |
| * A full copy of the text of the CDDL should have accompanied this |
| * source. A copy of the CDDL is also available via the Internet at |
| * http://www.illumos.org/license/CDDL. |
| */ |
| |
| /* |
| * Copyright 2015 Toomas Soome <tsoome@me.com> |
| */ |
| |
| /* |
| * Primitive linux loader, at the moment only intended to load memtest86+.bin. |
| * |
| * Note the linux kernel location conflicts with loader, so we need to |
| * read in to temporary space and relocate on exec, when btx is stopped. |
| */ |
| #include <sys/cdefs.h> |
| #include <sys/stat.h> |
| #include <stand.h> |
| #include <machine/metadata.h> |
| #include <machine/pc/bios.h> |
| |
| #include "linux.h" |
| #include "bootstrap.h" |
| #include "libi386.h" |
| #include "btxv86.h" |
| |
| static int linux_loadkernel(char *, u_int64_t, struct preloaded_file **); |
| static int linux_loadinitrd(char *, u_int64_t, struct preloaded_file **); |
| static int linux_exec(struct preloaded_file *); |
| static int linux_execinitrd(struct preloaded_file *); |
| |
| struct file_format linux = { linux_loadkernel, linux_exec }; |
| struct file_format linux_initrd = { linux_loadinitrd, linux_execinitrd }; |
| |
| uint32_t linux_text_len; |
| uint32_t linux_data_tmp_addr; |
| uint32_t linux_data_real_addr; |
| static size_t max_cmdline_size; |
| |
| static void |
| test_addr(uint64_t addr, uint64_t length, vm_offset_t *result) |
| { |
| vm_offset_t candidate; |
| |
| if (addr + length >= 0xa0000) |
| length = 0xa0000 - addr; |
| |
| candidate = addr + length - (LINUX_CL_OFFSET + max_cmdline_size); |
| if (candidate > LINUX_OLD_REAL_MODE_ADDR) |
| candidate = LINUX_OLD_REAL_MODE_ADDR; |
| if (candidate < addr) |
| return; |
| |
| if (candidate > *result || *result == (vm_offset_t)-1) |
| *result = candidate; |
| } |
| |
| static vm_offset_t |
| find_real_addr(struct preloaded_file *fp) |
| { |
| struct bios_smap *smap; |
| struct file_metadata *md; |
| int entries, i; |
| vm_offset_t candidate = -1; |
| |
| md = file_findmetadata(fp, MODINFOMD_SMAP); |
| if (md == NULL) { |
| printf("no memory smap\n"); |
| return (candidate); |
| } |
| entries = md->md_size / sizeof (struct bios_smap); |
| smap = (struct bios_smap *)md->md_data; |
| for (i = 0; i < entries; i++) { |
| if (smap[i].type != SMAP_TYPE_MEMORY) |
| continue; |
| if (smap[i].base >= 0xa0000) |
| continue; |
| test_addr(smap[i].base, smap[i].length, &candidate); |
| } |
| return (candidate); |
| } |
| |
| static int |
| linux_loadkernel(char *filename, uint64_t dest, struct preloaded_file **result) |
| { |
| struct linux_kernel_header lh; |
| struct preloaded_file *fp; |
| struct stat sb; |
| ssize_t n; |
| int fd, error = 0; |
| int setup_sects, linux_big; |
| unsigned long data, text; |
| vm_offset_t mem; |
| struct relocate_data *rdata; |
| |
| /* |
| * relocater_data is space allocated in relocater_tramp.S |
| * There is space for 3 instances + terminating zero in case |
| * all 3 entries are used. |
| */ |
| rdata = (struct relocate_data *)&relocater_data; |
| |
| if (filename == NULL) |
| return (EFTYPE); |
| |
| /* is kernel already loaded? */ |
| fp = file_findfile(NULL, NULL); |
| if (fp != NULL) |
| return (EFTYPE); |
| |
| if ((fd = open(filename, O_RDONLY)) == -1) |
| return (errno); |
| |
| if (fstat(fd, &sb) != 0) { |
| printf("stat failed\n"); |
| error = errno; |
| close(fd); |
| return (error); |
| } |
| |
| n = read(fd, &lh, sizeof (lh)); |
| if (n != sizeof (lh)) { |
| printf("error reading kernel header\n"); |
| error = EIO; |
| goto end; |
| } |
| |
| if (lh.boot_flag != BOOTSEC_SIGNATURE) { |
| printf("invalid magic number\n"); |
| error = EFTYPE; |
| goto end; |
| } |
| |
| setup_sects = lh.setup_sects; |
| linux_big = 0; |
| max_cmdline_size = 256; |
| |
| if (setup_sects > LINUX_MAX_SETUP_SECTS) { |
| printf("too many setup sectors\n"); |
| error = EFTYPE; |
| goto end; |
| } |
| |
| fp = file_alloc(); |
| if (fp == NULL) { |
| error = ENOMEM; |
| goto end; |
| } |
| |
| bios_addsmapdata(fp); |
| |
| if (lh.header == LINUX_MAGIC_SIGNATURE && lh.version >= 0x0200) { |
| linux_big = lh.loadflags & LINUX_FLAG_BIG_KERNEL; |
| lh.type_of_loader = LINUX_BOOT_LOADER_TYPE; |
| |
| if (lh.version >= 0x0206) |
| max_cmdline_size = lh.cmdline_size + 1; |
| |
| linux_data_real_addr = find_real_addr(fp); |
| if (linux_data_real_addr == -1) { |
| printf("failed to detect suitable low memory\n"); |
| file_discard(fp); |
| error = ENOMEM; |
| goto end; |
| } |
| if (lh.version >= 0x0201) { |
| lh.heap_end_ptr = LINUX_HEAP_END_OFFSET; |
| lh.loadflags |= LINUX_FLAG_CAN_USE_HEAP; |
| } |
| if (lh.version >= 0x0202) { |
| lh.cmd_line_ptr = linux_data_real_addr + |
| LINUX_CL_OFFSET; |
| } else { |
| lh.cl_magic = LINUX_CL_MAGIC; |
| lh.cl_offset = LINUX_CL_OFFSET; |
| lh.setup_move_size = LINUX_CL_OFFSET + max_cmdline_size; |
| } |
| } else { |
| /* old kernel */ |
| lh.cl_magic = LINUX_CL_MAGIC; |
| lh.cl_offset = LINUX_CL_OFFSET; |
| setup_sects = LINUX_DEFAULT_SETUP_SECTS; |
| linux_data_real_addr = LINUX_OLD_REAL_MODE_ADDR; |
| } |
| if (setup_sects == 0) |
| setup_sects = LINUX_DEFAULT_SETUP_SECTS; |
| |
| data = setup_sects << 9; |
| text = sb.st_size - data - 512; |
| |
| /* temporary location of real mode part */ |
| linux_data_tmp_addr = LINUX_BZIMAGE_ADDR + text; |
| |
| if (!linux_big && text > linux_data_real_addr - LINUX_ZIMAGE_ADDR) { |
| printf("Linux zImage is too big, use bzImage instead\n"); |
| file_discard(fp); |
| error = EFBIG; |
| goto end; |
| } |
| printf(" [Linux-%s, setup=0x%x, size=0x%x]\n", |
| (linux_big ? "bzImage" : "zImage"), data, text); |
| |
| /* copy real mode part to place */ |
| i386_copyin(&lh, linux_data_tmp_addr, sizeof (lh)); |
| n = data + 512 - sizeof (lh); |
| if (archsw.arch_readin(fd, linux_data_tmp_addr+sizeof (lh), n) != n) { |
| printf("failed to read %s\n", filename); |
| file_discard(fp); |
| error = errno; |
| goto end; |
| } |
| |
| /* Clear the heap space. */ |
| if (lh.header != LINUX_MAGIC_SIGNATURE || lh.version < 0x0200) { |
| memset(PTOV(linux_data_tmp_addr + ((setup_sects + 1) << 9)), |
| 0, (LINUX_MAX_SETUP_SECTS - setup_sects - 1) << 9); |
| } |
| |
| mem = LINUX_BZIMAGE_ADDR; |
| |
| if (archsw.arch_readin(fd, mem, text) != text) { |
| printf("failed to read %s\n", filename); |
| file_discard(fp); |
| error = EIO; |
| goto end; |
| } |
| |
| fp->f_name = strdup(filename); |
| if (linux_big) |
| fp->f_type = strdup("Linux bzImage"); |
| else |
| fp->f_type = strdup("Linux zImage"); |
| |
| /* |
| * NOTE: f_addr and f_size is used here as hint for module |
| * allocation, as module location will be f_addr + f_size. |
| */ |
| fp->f_addr = linux_data_tmp_addr; |
| fp->f_size = LINUX_SETUP_MOVE_SIZE; |
| linux_text_len = text; |
| |
| if (linux_big == 0) { |
| rdata[0].src = LINUX_BZIMAGE_ADDR; |
| rdata[0].dest = LINUX_ZIMAGE_ADDR; |
| rdata[0].size = text; |
| rdata[1].src = linux_data_tmp_addr; |
| rdata[1].dest = linux_data_real_addr; |
| rdata[1].size = LINUX_SETUP_MOVE_SIZE; |
| /* make sure the next entry is zeroed */ |
| rdata[2].src = 0; |
| rdata[2].dest = 0; |
| rdata[2].size = 0; |
| } else { |
| rdata[0].src = linux_data_tmp_addr; |
| rdata[0].dest = linux_data_real_addr; |
| rdata[0].size = LINUX_SETUP_MOVE_SIZE; |
| /* make sure the next entry is zeroed */ |
| rdata[1].src = 0; |
| rdata[1].dest = 0; |
| rdata[1].size = 0; |
| } |
| |
| *result = fp; |
| setenv("kernelname", fp->f_name, 1); |
| end: |
| close(fd); |
| return (error); |
| } |
| |
| static int |
| linux_exec(struct preloaded_file *fp) |
| { |
| struct linux_kernel_header *lh = (struct linux_kernel_header *) |
| PTOV(linux_data_tmp_addr); |
| struct preloaded_file *mfp = fp->f_next; |
| struct relocate_data *rdata; |
| char *arg, *vga; |
| char *src, *dst; |
| int linux_big; |
| uint32_t moveto, max_addr; |
| uint16_t segment; |
| struct i386_devdesc *rootdev; |
| |
| if (strcmp(fp->f_type, "Linux bzImage") == 0) |
| linux_big = 1; |
| else if (strcmp(fp->f_type, "Linux zImage") == 0) |
| linux_big = 0; |
| else |
| return (EFTYPE); |
| |
| i386_getdev((void **)(&rootdev), fp->f_name, NULL); |
| if (rootdev != NULL) |
| relocator_edx = bd_unit2bios(rootdev->d_unit); |
| |
| rdata = (struct relocate_data *)&relocater_data; |
| /* |
| * command line |
| * if not set in fp, read from boot-args env |
| */ |
| if (fp->f_args == NULL) |
| fp->f_args = getenv("boot-args"); |
| arg = fp->f_args; /* it can still be NULL */ |
| |
| /* video mode selection */ |
| if (arg && (vga = strstr(arg, "vga=")) != NULL) { |
| char *value = vga + 4; |
| uint16_t vid_mode; |
| |
| if (strncmp(value, "normal", 6) < 1) |
| vid_mode = LINUX_VID_MODE_NORMAL; |
| else if (strncmp(value, "ext", 3) < 1) |
| vid_mode = LINUX_VID_MODE_EXTENDED; |
| else if (strncmp(value, "ask", 3) < 1) |
| vid_mode = LINUX_VID_MODE_ASK; |
| else { |
| long mode; |
| errno = 0; |
| |
| /* |
| * libstand sets ERANGE as only error case; |
| * however, the actual value is 16bit, so |
| * additional check is needed. |
| */ |
| mode = strtol(value, NULL, 0); |
| if (errno != 0 || mode >> 16 != 0 || mode == 0) { |
| printf("bad value for video mode\n"); |
| return (EINTR); |
| } |
| vid_mode = (uint16_t) mode; |
| } |
| lh->vid_mode = vid_mode; |
| } |
| |
| src = arg; |
| dst = (char *)PTOV(linux_data_tmp_addr + LINUX_CL_OFFSET); |
| if (src != NULL) { |
| while (*src != 0 && dst < (char *) |
| PTOV(linux_data_tmp_addr + LINUX_CL_END_OFFSET)) |
| *(dst++) = *(src++); |
| } |
| *dst = 0; |
| |
| /* set up module relocation */ |
| if (mfp != NULL) { |
| moveto = (bios_extmem / 1024 + 0x400) << 10; |
| moveto = (moveto - mfp->f_size) & 0xfffff000; |
| max_addr = (lh->header == LINUX_MAGIC_SIGNATURE && |
| lh->version >= 0x0203 ? |
| lh->initrd_addr_max : LINUX_INITRD_MAX_ADDRESS); |
| if (moveto + mfp->f_size >= max_addr) |
| moveto = (max_addr - mfp->f_size) & 0xfffff000; |
| |
| /* |
| * XXX: Linux 2.3.xx has a bug in the memory range check, |
| * so avoid the last page. |
| * XXX: Linux 2.2.xx has a bug in the memory range check, |
| * which is worse than that of Linux 2.3.xx, so avoid the |
| * last 64kb. *sigh* |
| */ |
| moveto -= 0x10000; |
| |
| /* need to relocate initrd first */ |
| if (linux_big == 0) { |
| rdata[2].src = rdata[1].src; |
| rdata[2].dest = rdata[1].dest; |
| rdata[2].size = rdata[1].size; |
| rdata[1].src = rdata[0].src; |
| rdata[1].dest = rdata[0].dest; |
| rdata[1].size = rdata[0].size; |
| rdata[0].src = mfp->f_addr; |
| rdata[0].dest = moveto; |
| rdata[0].size = mfp->f_size; |
| } else { |
| rdata[1].src = rdata[0].src; |
| rdata[1].dest = rdata[0].dest; |
| rdata[1].size = rdata[0].size; |
| rdata[0].src = mfp->f_addr; |
| rdata[0].dest = moveto; |
| rdata[0].size = mfp->f_size; |
| } |
| lh->ramdisk_image = moveto; |
| lh->ramdisk_size = mfp->f_size; |
| } |
| |
| segment = linux_data_real_addr >> 4; |
| relocator_ds = segment; |
| relocator_es = segment; |
| relocator_fs = segment; |
| relocator_gs = segment; |
| relocator_ss = segment; |
| relocator_sp = LINUX_ESP; |
| relocator_ip = 0; |
| relocator_cs = segment + 0x20; |
| relocator_a20_enabled = 1; |
| i386_copyin(relocater, 0x600, relocater_size); |
| |
| dev_cleanup(); |
| |
| __exec((void *)0x600); |
| |
| panic("exec returned"); |
| |
| return (EINTR); /* not reached */ |
| } |
| |
| static int |
| linux_loadinitrd(char *filename, uint64_t dest, struct preloaded_file **result) |
| { |
| struct preloaded_file *mfp; |
| vm_offset_t mem; |
| |
| if (filename == NULL) |
| return (EFTYPE); |
| |
| /* check if the kernel is loaded */ |
| mfp = file_findfile(NULL, "Linux bzImage"); |
| if (mfp == NULL) |
| mfp = file_findfile(NULL, "Linux zImage"); |
| if (mfp == NULL) |
| return (EFTYPE); |
| |
| mfp = file_loadraw(filename, "module", 0, NULL, 0); |
| if (mfp == NULL) |
| return (EFTYPE); |
| *result = mfp; |
| return (0); |
| } |
| |
| static int linux_execinitrd(struct preloaded_file *pf) |
| { |
| return (EFTYPE); |
| } |