| /* |
| * This file is subject to the terms and conditions of the GNU General Public |
| * License. See the file "COPYING" in the main directory of this archive |
| * for more details. |
| * |
| * Copyright (C) Hewlett-Packard (Paul Bame) paul_bame@hp.com |
| */ |
| #include <stddef.h> |
| #include "bootloader.h" |
| #include <asm/pdc.h> |
| #include <asm/byteorder.h> |
| #include "load.h" |
| |
| #undef PAGE0 |
| #define PAGE0 ((struct zeropage *)0x00000000) |
| |
| char commandline[CMDLINELEN]; |
| |
| int Debug = 0; |
| int interactive = 0; |
| |
| void flush_data_cache(char *start, size_t length) |
| { |
| char *end = start + length; |
| |
| do |
| { |
| asm volatile("fdc 0(%0)" : : "r" (start)); |
| asm volatile("fic 0(%%sr0,%0)" : : "r" (start)); |
| start += 16; |
| } while (start < end); |
| asm volatile("fdc 0(%0)" : : "r" (end)); |
| |
| asm ("sync"); |
| } |
| |
| static int |
| parse_number(char *s, char **next) |
| { |
| int n = 0; |
| |
| while (is_digit(*s)) |
| { |
| n *= 10; |
| n += *s++ - '0'; |
| } |
| |
| *next = s; |
| |
| return n; |
| } |
| |
| static char * |
| parse_pfname(char *s, int *partition, char *name) |
| { |
| char *p1, *p2 = NULL; |
| |
| if (s != NULL || *s != '\0') |
| { |
| /* parse the kernel partition number */ |
| *partition = parse_number(s, &p1); |
| |
| /* now the kernel name */ |
| p2 = strpbrk(p1, " \t"); |
| if (p2 != NULL) |
| { |
| *p2 = '\0'; |
| strcpy(name, p1); |
| *p2 = ' '; |
| } |
| else |
| { |
| strcpy(name, p1); |
| p2 = p1 + strlen(p1); |
| } |
| } |
| |
| return p2; |
| } |
| |
| /* |
| * ext2_open_dirstrip() |
| * Get filehandle for given filename. |
| * If user gave 2/boot/vmlinux, don't fail and try if he maybe actually meant 2/vmlinux. |
| */ |
| static int |
| ext2_open_dirstrip(char *filename) |
| { |
| while (filename && *filename) { |
| char *last; |
| int fh = ext2_open(filename); |
| if (fh >= 0) |
| return fh; |
| |
| last = filename; |
| filename = strpbrk(&filename[1], "/"); |
| if (filename) |
| printf("Warning: %s not found. Try %s instead...\n", last, filename); |
| } |
| |
| return -1; |
| } |
| |
| static int |
| chk_strcat(char *out, char *in, int len, int *ok) |
| { |
| if (*ok) |
| { |
| int need = strlen(out) + strlen(in) + 1; |
| if (need > len) |
| { |
| printf("Adding '%s' exceeds length (%d)\n", in, len); |
| *ok = 0; |
| } |
| else |
| { |
| strcat(out, in); |
| } |
| } |
| return *ok; |
| } |
| |
| /* return pointer to the commandline minus palo stuff */ |
| static char * |
| parse(const char *cmdline, int *kpart, char *kname, int *rdpart, char *rdname) |
| { |
| char buf[CMDLINELEN]; |
| static char lcmd[CMDLINELEN]; |
| char *suffix1, *suffix2; |
| int ok = 1; |
| |
| /* need a copy to work on */ |
| strcpy(buf, cmdline); |
| |
| *kpart = -1; |
| *rdpart = -1; |
| |
| suffix1 = parse_pfname(buf, kpart, kname); |
| |
| if (*suffix1 != '\0') |
| suffix1++; |
| strcpy(lcmd, suffix1); |
| |
| /* see if we have a ramdisk */ |
| suffix2 = suffix1; |
| if ((strncmp(suffix1, "initrd=", 7) == 0) || |
| (suffix2 = strstr(suffix1, " initrd=")) != NULL) |
| { |
| char *suffix3; |
| lcmd[suffix2 - suffix1] = '\0'; |
| if (*suffix2 == ' ') |
| suffix2++; |
| suffix3 = parse_pfname(suffix2 + 7, rdpart, rdname); |
| chk_strcat(lcmd, suffix3, sizeof lcmd, &ok); |
| } |
| |
| return lcmd; |
| } |
| |
| int |
| load_kernel(int fd, unsigned *entryp, int *wide) |
| { |
| struct loadable loadable; |
| int i; |
| |
| if (!prepare_loadable(fd, &loadable, wide)) |
| { |
| printf("Couldn't grok your kernel executable format\n"); |
| return 0; |
| } |
| |
| /* need to physicalize those huge addresses */ |
| loadable.entry = PHYS(loadable.entry); |
| loadable.first = PHYS(loadable.first); |
| |
| printf("\nEntry %08x first %08x n %d\n", |
| loadable.entry, loadable.first, loadable.n); |
| |
| for (i = 0; i < loadable.n; i++) |
| { |
| loadable.segment[i].mem = PHYS(loadable.segment[i].mem); |
| printf("Segment %d load %08x size %d mediaptr 0x%lx\n", |
| i, loadable.segment[i].mem, loadable.segment[i].length, |
| loadable.segment[i].offset); |
| } |
| |
| if (!load_loadable((char *)loadable.first, fd, &loadable)) |
| { |
| printf("Fatal error loading kernel executable\n"); |
| return 0; |
| } |
| |
| flush_data_cache((char *)loadable.first, loadable.size); |
| |
| *entryp = loadable.entry; |
| |
| return 1; |
| } |
| |
| static int |
| load_rd(int fd, int size) |
| { |
| extern char *rd_start, *rd_end; |
| char *rd; |
| |
| if (size <= 0) |
| return 0; |
| |
| /* no idea if initrd space must be aligned, but once it was before... */ |
| rd = malloc_aligned(size, 4096); |
| printf("Loading ramdisk %d bytes @ %p...", size, rd); |
| if (seekread(fd, rd, size, 0) == size) |
| { |
| rd_start = rd; |
| rd_end = rd + size; |
| } |
| flush_data_cache((char *)rd, size); |
| puts("\n"); |
| |
| return (rd_start != 0); |
| } |
| |
| static void |
| join(char *out, int argc, char *argv[], int *ok) |
| { |
| char tmpbuf[CMDLINELEN]; |
| int i; |
| |
| tmpbuf[0] = '\0'; |
| for (i = 0; i < argc; i++) |
| { |
| if (i > 0) |
| chk_strcat(tmpbuf, " ", sizeof tmpbuf, ok); |
| chk_strcat(tmpbuf, argv[i], sizeof tmpbuf, ok); |
| } |
| strcpy(out, tmpbuf); |
| } |
| |
| static struct diskpartition partition[MAXPARTS]; |
| static int ext2 = 0; |
| static int bootdev; |
| |
| static void |
| partition_transform(int *kern_part, int *rd_part) |
| { |
| int i, palo_part = -1; |
| |
| /* if the F0 partition is the same as the requested kernel partition, |
| * for now change it to zero to re-use the existing logic. Should do |
| * the reverse in the future probably. |
| */ |
| for (i = 0; i < sizeof partition / sizeof partition[0]; i++) |
| { |
| if (partition[i].id == PALO_PARTITION) |
| { |
| palo_part = i + 1; |
| if (*kern_part == palo_part) |
| *kern_part = 0; |
| |
| if (rd_part && *rd_part == palo_part) |
| *rd_part = 0; |
| |
| break; |
| } |
| } |
| |
| if (ext2 && *kern_part == 0) { |
| *kern_part = palo_part; |
| if(rd_part && *rd_part == 0) |
| *rd_part = palo_part; |
| } |
| |
| } |
| |
| static void |
| ls(char *path) |
| { |
| char *p, kern_dir[256]; |
| const char *dir; |
| int fd, part, part_fd; |
| |
| parse_pfname(path, &part, kern_dir); |
| if ((p = strrchr(kern_dir, '/')) != NULL) |
| { |
| *++p = '.'; |
| *++p = '\0'; |
| } |
| else |
| { |
| strcpy(kern_dir, "/."); |
| } |
| |
| printf("Directory listing of %s\n\n", kern_dir); |
| |
| partition_transform(&part, NULL); |
| |
| /* partition table starts from zero */ |
| part_fd = offset_open(bootdev, 512 * partition[part - 1].start, |
| 512 * partition[part - 1].length); |
| if(ext2_mount(part_fd, 0, 0) == -1) { |
| printf("Failed to mount partition %d\n", part); |
| return; |
| } |
| |
| if((fd = ext2_open(kern_dir)) == -1) { |
| printf("Failed to open directory %s\n", kern_dir); |
| return; |
| } |
| |
| while((dir = ext2_readdir(fd, 0)) != NULL) |
| if(dir[0] != '.') /* skip hidden files and . and .. */ |
| printf(" %s\n", dir); |
| |
| printf("\n"); |
| ext2_close(fd); |
| /* There's no umount ... since the next mount reuses |
| * the current one's data structures ... */ |
| } |
| |
| static void |
| interact(int *ok) |
| { |
| #define MAX_ARGV 40 |
| char *argv[MAX_ARGV], *p; |
| char orig[CMDLINELEN]; |
| const char sep[] = " \t"; |
| char numbuf[4]; |
| char fieldbuf[200]; |
| int i, argc, editfield; |
| |
| strcpy(orig, commandline); |
| |
| while (1) |
| { |
| puts("Current command line:\n"); |
| puts(commandline); |
| puts("\n"); |
| |
| p = commandline; |
| argc = 0; |
| *ok = 1; |
| do |
| { |
| if (argc >= MAX_ARGV) |
| { |
| argc = MAX_ARGV; |
| break; |
| } |
| if ((argv[argc++] = strtok(p, sep)) == NULL) |
| { |
| argc--; |
| break; |
| } |
| p = NULL; |
| } while (1); |
| for (i = 0; i < argc; i++) |
| { |
| printf("%2d: %s\n", i, argv[i]); |
| } |
| puts("\n" |
| "<#> edit the numbered field\n" |
| "'b' boot with this command line\n" |
| "'r' restore command line\n" |
| "'l' list dir\n" |
| "'x' reset and reboot machine\n" |
| "? "); |
| numbuf[0] = '0'; |
| numbuf[1] = '\0'; |
| enter_text(numbuf, sizeof numbuf - 1); |
| puts("\n"); |
| |
| if (numbuf[0] == 'b') |
| { |
| join(commandline, argc, argv, ok); |
| break; |
| } |
| |
| if (numbuf[0] == 'r') |
| { |
| strcpy(commandline, orig); |
| continue; |
| } |
| |
| if (numbuf[0] == 'l') |
| { |
| join(commandline, argc, argv, ok); |
| ls(argv[0]); |
| continue; |
| } |
| |
| if (numbuf[0] == 'x') |
| pdc_do_reset(); |
| |
| editfield = parse_number(numbuf, &p); |
| |
| if (editfield >= MAX_ARGV) |
| { |
| puts("Too many input fields.\n"); |
| } |
| else if (editfield < argc) |
| { |
| strncpy(fieldbuf, argv[editfield], sizeof(fieldbuf)); |
| fieldbuf[sizeof(fieldbuf)-1] = '\0'; |
| enter_text(fieldbuf, sizeof fieldbuf - 1); |
| puts("\n"); |
| argv[editfield] = fieldbuf; |
| } |
| |
| join(commandline, argc, argv, ok); |
| } |
| } |
| |
| /* |
| * On some server models the serial port of the GSP/Management card which |
| * mirrors the console port shows up as ttyS1 instead of ttyS0. This is due |
| * how the pci code in Linux kernel scans the PCI bus. Check the relevant |
| * models and return ttyS1 for such machines. |
| * Beware: pdc_model_sysmodel() may return a machine name which has trailing |
| * spaces. |
| */ |
| static char * |
| get_default_serial_console() |
| { |
| char sys_model_name[81]; |
| char *ttyS1_models[] = { |
| "9000/800/rp3410", |
| "9000/800/rp3420", |
| "9000/800/rp3440", |
| NULL |
| }; |
| |
| if (pdc_model_sysmodel(sys_model_name) == PDC_OK) { |
| char **check = ttyS1_models; |
| while (*check) { |
| if (strncmp(*check, sys_model_name, strlen(*check)) == 0) |
| return "ttyS1"; |
| check++; |
| } |
| } |
| |
| return "ttyS0"; |
| } |
| |
| unsigned |
| iplmain(int is_interactive, char *initialstackptr, int started_wide) |
| { |
| extern char _end, _edata; |
| int partitioned; |
| unsigned entry; |
| struct firstblock f; |
| int blocked_bootdev; |
| int wide; |
| int kern_part, rd_part; |
| char kern_name[128], rd_name[128]; |
| char kern_fullname[128]; |
| int ok = 1; |
| |
| /* BSS clear */ |
| bzero(&_edata, &_end - &_edata); |
| |
| /* heap grows down from initial stack pointer */ |
| malloc_init(initialstackptr); |
| |
| firmware_init(started_wide); |
| putchar('p'); /* if you get this p and no more, string storage */ |
| /* in $GLOBAL$ is wrong or %dp is wrong */ |
| puts("alo ipl " PALOVERSION " "); |
| puts(bld_info); |
| puts("\n"); |
| interactive = is_interactive; |
| if (Debug) printf("iplmain(%d, started %s)\n", interactive, |
| started_wide ? "wide" : "narrow"); |
| if (Debug) printf("initial-sp %p\n", initialstackptr); |
| |
| restart: |
| |
| blocked_bootdev = pdc_bootdev_open(); |
| bootdev = byteio_open(blocked_bootdev); |
| |
| STRUCTREAD(bootdev, f, 0); |
| if (strncmp(f.palomagic, PALOMAGIC, 4) != 0) |
| { |
| printf("ERROR: bad palo magic on boot device\n"); |
| while(1); |
| } |
| |
| memset(&partition, 0, sizeof partition); |
| partitioned = load_partitions(bootdev, |
| partition, sizeof partition / sizeof partition[0]); |
| |
| if (partitioned) |
| { |
| printf("\n"); |
| print_ptab_pretty(partition, sizeof partition / sizeof partition[0]); |
| } |
| |
| printf("\n%s contains:\n", |
| partitioned ? "PALO(F0) partition" : "Boot image"); |
| |
| if(partitioned && f.version >= 4 && (f.flags & PFLAG_EXT2)) { |
| printf("PALO is formatted EXT2/3\n"); |
| ext2 = 1; |
| } |
| |
| if (f.version < 3 && f.kern32_sz > 0) |
| { |
| printf(" 0/vmlinux %d bytes @ 0x%x\n", f.kern32_sz, f.kern32_offset); |
| } |
| else |
| { |
| if (f.kern32_sz > 0) |
| printf(" 0/vmlinux32 %d(%d) bytes @ 0x%x\n", f.kern32_sz, f.kern32_native_sz, f.kern32_offset); |
| if (f.kern64_sz > 0) |
| printf(" 0/vmlinux64 %d(%d) bytes @ 0x%x\n", f.kern64_sz, f.kern64_native_sz, f.kern64_offset); |
| } |
| |
| if (f.rd_sz > 0) |
| printf(" 0/ramdisk %d bytes @ 0x%x\n", f.rd_sz, f.rd_offset); |
| |
| if (f.cmdline[0] == '\0' && f.cmdline_old[0]) /* old style command line ? */ |
| { |
| strcpy(f.cmdline, f.cmdline_old); |
| f.cmdline_old[0] = 0; |
| } |
| |
| if (f.cmdline[0] == '\0') /* no command line specified */ |
| { |
| die("ERROR: No command line on boot media -- faking one\n"); |
| strcpy(f.cmdline, "0/vmlinux root=???"); |
| interactive = 1; |
| } |
| |
| if (strlen(f.cmdline) >= sizeof f.cmdline) |
| printf("WARNING: stored command line is longer than allowed\n"); |
| strcpy(commandline, f.cmdline); |
| |
| /* add the right console= if there isn't one yet */ |
| if (strstr(commandline, " console=") == 0) |
| { |
| printf("\nInformation: No console specified on kernel command line." |
| " This is normal.\nPALO will choose the console currently" |
| " used by firmware "); |
| |
| chk_strcat(commandline, " console=", CMDLINELEN, &ok); |
| if (pdc_cons_duplex()) |
| { |
| int is_mux; |
| |
| printf("(serial).\n"); |
| |
| if(pdc_cons_mux(&is_mux) != PDC_OK) |
| printf("Information: The PDC calls to query the console device failed. Assuming console=ttyS0\n"); |
| |
| if(is_mux) |
| chk_strcat(commandline, "ttyB0", CMDLINELEN, &ok); |
| else |
| chk_strcat(commandline, get_default_serial_console(), CMDLINELEN, &ok); |
| |
| if (strstr(commandline, " TERM=") == 0) |
| chk_strcat(commandline, " TERM=vt102", CMDLINELEN, &ok); |
| } |
| else |
| { |
| printf("(graphics).\n"); |
| chk_strcat(commandline, "tty0", CMDLINELEN, &ok); |
| if (strstr(commandline, " sti=") == 0) |
| { |
| struct { |
| unsigned char flags; |
| unsigned char bc[6]; |
| unsigned char mod; |
| } cons; |
| int i; |
| |
| chk_strcat(commandline, " sti=", CMDLINELEN, &ok); |
| if (pdc_read_conspath((unsigned char *)&cons) > 0) |
| { |
| char pathcomp[4]; |
| |
| for (i = 0; i < 6; i++) |
| { |
| if (cons.bc[i] < 64) |
| { |
| sprintf(pathcomp, "%d/", cons.bc[i]); |
| chk_strcat(commandline, pathcomp, CMDLINELEN, &ok); |
| } |
| } |
| sprintf(pathcomp, "%d", cons.mod); |
| chk_strcat(commandline, pathcomp, CMDLINELEN, &ok); |
| } |
| else |
| chk_strcat (commandline, "0", CMDLINELEN, &ok); |
| } |
| if (strstr(commandline, " sti_font=") == 0) |
| chk_strcat(commandline, " sti_font=VGA8x16", CMDLINELEN, &ok); |
| if (strstr(commandline, " TERM=") == 0) |
| chk_strcat(commandline, " TERM=linux", CMDLINELEN, &ok); |
| } |
| } |
| |
| if (interactive) |
| interact(&ok); |
| |
| /* If we have any failures after this, be sure we're interactive |
| * for the re-start */ |
| interactive = 1; |
| |
| strcpy(commandline, |
| parse(commandline, &kern_part, kern_name, &rd_part, rd_name)); |
| |
| sprintf(kern_fullname, "%d%s", kern_part, kern_name); |
| chk_strcat(commandline, " palo_kernel=", CMDLINELEN, &ok); |
| chk_strcat(commandline, kern_fullname, CMDLINELEN, &ok); |
| |
| printf("\nCommand line for kernel: '%s'\n", commandline); |
| |
| printf("Selected kernel: %s from partition %d\n", kern_name, kern_part); |
| |
| if (rd_part != -1) |
| printf("Selected ramdisk: %s from partition %d\n", rd_name, rd_part); |
| |
| partition_transform(&kern_part, &rd_part); |
| |
| if (kern_part > 0 && !partitioned) |
| { |
| printf("ERROR: Requesting kernel from partition %d " |
| "on unpartitioned media!\n", kern_part); |
| goto restart; |
| } |
| |
| if (rd_part != -1 && rd_part != kern_part) |
| { |
| die("ERROR:: palo does not support ramdisk on different" |
| " partition than kernel\n"); |
| goto restart; |
| } |
| |
| if (kern_part == 0) |
| { |
| int kernfd; |
| const char *wname; |
| int rdfd; |
| |
| wname = kern_name + strlen(kern_name) - 2; |
| if (wname >= kern_name && streq(wname, "32")) |
| { |
| if (f.kern32_sz == 0) |
| { |
| die("Error: can't find a 32-bit kernel here"); |
| goto restart; |
| } |
| kernfd = offset_open(bootdev, f.kern32_offset, f.kern32_sz); |
| } |
| else if (wname >= kern_name && streq(wname, "64")) |
| { |
| if (f.kern64_sz == 0) |
| { |
| die("Error: can't find a 64-bit kernel here"); |
| goto restart; |
| } |
| kernfd = offset_open(bootdev, f.kern64_offset, f.kern64_sz); |
| } |
| else |
| { |
| if (f.version > 2) |
| printf("Warning: kernel name doesn't end with 32 or 64 -- Guessing... "); |
| |
| kernfd = -1; |
| |
| if ((pdc_os_bits() & (OS_32|OS_64)) == (OS_32|OS_64)) |
| { |
| printf("\nThis box can boot either 32 or 64-bit kernels...\n"); |
| if (f.kern32_offset == 0 && f.kern64_offset != 0) |
| { |
| printf("Only see a 64-bit kernel, using that\n"); |
| kernfd = offset_open(bootdev, f.kern64_offset, f.kern64_sz); |
| } |
| else if (f.kern32_offset != 0 && f.kern64_offset == 0) |
| { |
| printf("Only see a 32-bit kernel, using that\n"); |
| kernfd = offset_open(bootdev, f.kern32_offset, f.kern32_sz); |
| } |
| else if (f.kern32_offset != 0 && f.kern64_offset != 0) |
| { |
| printf("Both kernels available, choosing 64-bit kernel\n"); |
| kernfd = offset_open(bootdev, f.kern64_offset, f.kern64_sz); |
| } |
| else |
| { |
| die("No kernels found."); |
| goto restart; |
| } |
| } |
| |
| if (kernfd == -1 && (pdc_os_bits() & OS_64)) |
| { |
| printf("Choosing 64-bit kernel\n"); |
| kernfd = offset_open(bootdev, f.kern64_offset, f.kern64_sz); |
| } |
| else if (kernfd == -1 && (pdc_os_bits() & OS_32)) |
| { |
| printf("Choosing 32-bit kernel\n"); |
| kernfd = offset_open(bootdev, f.kern32_offset, f.kern32_sz); |
| } |
| } |
| |
| /* FIXME!!! This *could* overwrite us -- probably should check */ |
| if (!load_kernel(kernfd, &entry, &wide)) |
| { |
| die("ERROR: failed to load kernel\n"); |
| goto restart; |
| } |
| |
| if (rd_part != -1) |
| { |
| rdfd = offset_open(bootdev, f.rd_offset, f.rd_sz); |
| if (!load_rd(rdfd, f.rd_sz)) |
| { |
| printf("ERROR: failed to load ramdisk - proceeding anyway\n"); |
| } |
| } |
| } |
| else /* kern_part > 0 && we're partitioned */ |
| { |
| int kern_fd; |
| int bkern_fd; |
| int rd_fd, brd_fd; |
| int part_fd; |
| int mount_fd; |
| struct diskpartition *pp; |
| |
| if (kern_part >= MAXPARTS || |
| (partition[kern_part - 1].id != LINUX_EXT2_PARTITION && |
| partition[kern_part - 1].id != LINUX_RAID_PARTITION && |
| partition[kern_part - 1].id != PALO_PARTITION) ) |
| { |
| printf("ERROR: Partition %d must be ext2\n", kern_part); |
| goto restart; |
| } |
| |
| pp = &partition[kern_part - 1]; |
| |
| part_fd = offset_open(bootdev, 512 * pp->start, 512 * pp->length); |
| |
| mount_fd = ext2_mount(part_fd, 0, 0); |
| if (0) printf("ext2_mount(partition %d) returns %d\n", |
| kern_part, mount_fd); |
| |
| kern_fd = ext2_open_dirstrip(kern_name); |
| if (0) printf("ext2_open_dirstrip(%s) = %d\n", kern_name, kern_fd); |
| if (kern_fd < 0) |
| { |
| printf("ERROR: open %s from partition %d failed\n", |
| kern_name, kern_part); |
| goto restart; |
| } |
| |
| bkern_fd = byteio_open(kern_fd); |
| if (!load_kernel(bkern_fd, &entry, &wide)) |
| { |
| die("ERROR: failed to load kernel\n"); |
| goto restart; |
| } |
| |
| if (rd_part != -1) |
| { |
| rd_fd = ext2_open_dirstrip(rd_name); |
| if(rd_fd >= 0) { |
| brd_fd = byteio_open(rd_fd); |
| |
| if (!load_rd(brd_fd, ext2_filesize(rd_fd))) |
| { |
| printf("ERROR: failed to load ramdisk - proceeding anyway\n"); |
| } |
| } else { |
| printf("ERROR: failed to open ramdisk %s\n", rd_name); |
| } |
| } |
| } |
| |
| /* FIXME!!! need to pass command line to kernel */ |
| /* could theoretically use a function pointer, but they're ugly on PA */ |
| if(pdc_default_width(wide)) |
| goto restart; |
| printf("Branching to kernel entry point 0x%08x. If this is the last\n" |
| "message you see, you may need to switch your console. This is\n" |
| "a common symptom -- search the FAQ and mailing list at parisc-linux.org\n\n", |
| entry); |
| return entry; |
| } |