f1429e95c3
r294765 (imp) Move all the separate copies of the same strings into paths.h. There's nothing machine specific about these. r294765 (imp) RBX_ defines are in rbx.h, move it there. r294847 (imp) Remove static from these two. They slipped through the cracks. r294925 (imp) Fix mistake when transitioning to the new defines with ZFS loader. I hate adding yet another define, but it is the lessor of the evil choices available. Kill another evil by removing PATH_BOOT3 and replacing it with PATH_LOADER or PATH_LOADER_ZFS as appropriate. Approved by: re (gjb)
752 lines
13 KiB
C
752 lines
13 KiB
C
/*-
|
|
* Copyright (c) 1998 Robert Nordier
|
|
* All rights reserved.
|
|
* Copyright (c) 2001 Robert Drehmel
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms are freely
|
|
* permitted provided that the above copyright notice and this
|
|
* paragraph and the following disclaimer are duplicated in all
|
|
* such forms.
|
|
*
|
|
* This software is provided "AS IS" and without any express or
|
|
* implied warranties, including, without limitation, the implied
|
|
* warranties of merchantability and fitness for a particular
|
|
* purpose.
|
|
*/
|
|
|
|
#include <sys/cdefs.h>
|
|
__FBSDID("$FreeBSD$");
|
|
|
|
#include <sys/param.h>
|
|
#include <sys/dirent.h>
|
|
|
|
#include <machine/elf.h>
|
|
#include <machine/stdarg.h>
|
|
|
|
#include "paths.h"
|
|
|
|
#define READ_BUF_SIZE 8192
|
|
|
|
typedef int putc_func_t(char c, void *arg);
|
|
typedef int32_t ofwh_t;
|
|
|
|
struct sp_data {
|
|
char *sp_buf;
|
|
u_int sp_len;
|
|
u_int sp_size;
|
|
};
|
|
|
|
static const char digits[] = "0123456789abcdef";
|
|
|
|
static char bootpath[128];
|
|
static char bootargs[128];
|
|
|
|
static ofwh_t bootdev;
|
|
|
|
static uint32_t fs_off;
|
|
|
|
int main(int ac, char **av);
|
|
static void exit(int) __dead2;
|
|
static void usage(void);
|
|
|
|
#ifdef ZFSBOOT
|
|
static void loadzfs(void);
|
|
static int zbread(char *buf, off_t off, size_t bytes);
|
|
#else
|
|
static void load(const char *);
|
|
#endif
|
|
|
|
static void bcopy(const void *src, void *dst, size_t len);
|
|
static void bzero(void *b, size_t len);
|
|
|
|
static int domount(const char *device);
|
|
static int dskread(void *buf, u_int64_t lba, int nblk);
|
|
|
|
static void panic(const char *fmt, ...) __dead2;
|
|
static int printf(const char *fmt, ...);
|
|
static int putchar(char c, void *arg);
|
|
static int vprintf(const char *fmt, va_list ap);
|
|
static int vsnprintf(char *str, size_t sz, const char *fmt, va_list ap);
|
|
|
|
static int __printf(const char *fmt, putc_func_t *putc, void *arg, va_list ap);
|
|
static int __puts(const char *s, putc_func_t *putc, void *arg);
|
|
static int __sputc(char c, void *arg);
|
|
static char *__uitoa(char *buf, u_int val, int base);
|
|
static char *__ultoa(char *buf, u_long val, int base);
|
|
|
|
/*
|
|
* Open Firmware interface functions
|
|
*/
|
|
typedef u_int64_t ofwcell_t;
|
|
typedef u_int32_t u_ofwh_t;
|
|
typedef int (*ofwfp_t)(ofwcell_t []);
|
|
static ofwfp_t ofw; /* the PROM Open Firmware entry */
|
|
|
|
void ofw_init(int, int, int, int, ofwfp_t);
|
|
static ofwh_t ofw_finddevice(const char *);
|
|
static ofwh_t ofw_open(const char *);
|
|
static int ofw_getprop(ofwh_t, const char *, void *, size_t);
|
|
static int ofw_read(ofwh_t, void *, size_t);
|
|
static int ofw_write(ofwh_t, const void *, size_t);
|
|
static int ofw_seek(ofwh_t, u_int64_t);
|
|
static void ofw_exit(void) __dead2;
|
|
|
|
static ofwh_t stdinh, stdouth;
|
|
|
|
/*
|
|
* This has to stay here, as the PROM seems to ignore the
|
|
* entry point specified in the a.out header. (or elftoaout is broken)
|
|
*/
|
|
|
|
void
|
|
ofw_init(int d, int d1, int d2, int d3, ofwfp_t ofwaddr)
|
|
{
|
|
ofwh_t chosenh;
|
|
char *av[16];
|
|
char *p;
|
|
int ac;
|
|
|
|
ofw = ofwaddr;
|
|
|
|
chosenh = ofw_finddevice("/chosen");
|
|
ofw_getprop(chosenh, "stdin", &stdinh, sizeof(stdinh));
|
|
ofw_getprop(chosenh, "stdout", &stdouth, sizeof(stdouth));
|
|
ofw_getprop(chosenh, "bootargs", bootargs, sizeof(bootargs));
|
|
ofw_getprop(chosenh, "bootpath", bootpath, sizeof(bootpath));
|
|
|
|
bootargs[sizeof(bootargs) - 1] = '\0';
|
|
bootpath[sizeof(bootpath) - 1] = '\0';
|
|
|
|
ac = 0;
|
|
p = bootargs;
|
|
for (;;) {
|
|
while (*p == ' ' && *p != '\0')
|
|
p++;
|
|
if (*p == '\0' || ac >= 16)
|
|
break;
|
|
av[ac++] = p;
|
|
while (*p != ' ' && *p != '\0')
|
|
p++;
|
|
if (*p != '\0')
|
|
*p++ = '\0';
|
|
}
|
|
|
|
exit(main(ac, av));
|
|
}
|
|
|
|
static ofwh_t
|
|
ofw_finddevice(const char *name)
|
|
{
|
|
ofwcell_t args[] = {
|
|
(ofwcell_t)"finddevice",
|
|
1,
|
|
1,
|
|
(ofwcell_t)name,
|
|
0
|
|
};
|
|
|
|
if ((*ofw)(args)) {
|
|
printf("ofw_finddevice: name=\"%s\"\n", name);
|
|
return (1);
|
|
}
|
|
return (args[4]);
|
|
}
|
|
|
|
static int
|
|
ofw_getprop(ofwh_t ofwh, const char *name, void *buf, size_t len)
|
|
{
|
|
ofwcell_t args[] = {
|
|
(ofwcell_t)"getprop",
|
|
4,
|
|
1,
|
|
(u_ofwh_t)ofwh,
|
|
(ofwcell_t)name,
|
|
(ofwcell_t)buf,
|
|
len,
|
|
0
|
|
};
|
|
|
|
if ((*ofw)(args)) {
|
|
printf("ofw_getprop: ofwh=0x%x buf=%p len=%u\n",
|
|
ofwh, buf, len);
|
|
return (1);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
static ofwh_t
|
|
ofw_open(const char *path)
|
|
{
|
|
ofwcell_t args[] = {
|
|
(ofwcell_t)"open",
|
|
1,
|
|
1,
|
|
(ofwcell_t)path,
|
|
0
|
|
};
|
|
|
|
if ((*ofw)(args)) {
|
|
printf("ofw_open: path=\"%s\"\n", path);
|
|
return (-1);
|
|
}
|
|
return (args[4]);
|
|
}
|
|
|
|
static int
|
|
ofw_close(ofwh_t devh)
|
|
{
|
|
ofwcell_t args[] = {
|
|
(ofwcell_t)"close",
|
|
1,
|
|
0,
|
|
(u_ofwh_t)devh
|
|
};
|
|
|
|
if ((*ofw)(args)) {
|
|
printf("ofw_close: devh=0x%x\n", devh);
|
|
return (1);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
static int
|
|
ofw_read(ofwh_t devh, void *buf, size_t len)
|
|
{
|
|
ofwcell_t args[] = {
|
|
(ofwcell_t)"read",
|
|
3,
|
|
1,
|
|
(u_ofwh_t)devh,
|
|
(ofwcell_t)buf,
|
|
len,
|
|
0
|
|
};
|
|
|
|
if ((*ofw)(args)) {
|
|
printf("ofw_read: devh=0x%x buf=%p len=%u\n", devh, buf, len);
|
|
return (1);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
static int
|
|
ofw_write(ofwh_t devh, const void *buf, size_t len)
|
|
{
|
|
ofwcell_t args[] = {
|
|
(ofwcell_t)"write",
|
|
3,
|
|
1,
|
|
(u_ofwh_t)devh,
|
|
(ofwcell_t)buf,
|
|
len,
|
|
0
|
|
};
|
|
|
|
if ((*ofw)(args)) {
|
|
printf("ofw_write: devh=0x%x buf=%p len=%u\n", devh, buf, len);
|
|
return (1);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
static int
|
|
ofw_seek(ofwh_t devh, u_int64_t off)
|
|
{
|
|
ofwcell_t args[] = {
|
|
(ofwcell_t)"seek",
|
|
3,
|
|
1,
|
|
(u_ofwh_t)devh,
|
|
off >> 32,
|
|
off,
|
|
0
|
|
};
|
|
|
|
if ((*ofw)(args)) {
|
|
printf("ofw_seek: devh=0x%x off=0x%lx\n", devh, off);
|
|
return (1);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
static void
|
|
ofw_exit(void)
|
|
{
|
|
ofwcell_t args[3];
|
|
|
|
args[0] = (ofwcell_t)"exit";
|
|
args[1] = 0;
|
|
args[2] = 0;
|
|
|
|
for (;;)
|
|
(*ofw)(args);
|
|
}
|
|
|
|
static void
|
|
bcopy(const void *src, void *dst, size_t len)
|
|
{
|
|
const char *s = src;
|
|
char *d = dst;
|
|
|
|
while (len-- != 0)
|
|
*d++ = *s++;
|
|
}
|
|
|
|
static void
|
|
memcpy(void *dst, const void *src, size_t len)
|
|
{
|
|
|
|
bcopy(src, dst, len);
|
|
}
|
|
|
|
static void
|
|
bzero(void *b, size_t len)
|
|
{
|
|
char *p = b;
|
|
|
|
while (len-- != 0)
|
|
*p++ = 0;
|
|
}
|
|
|
|
static int
|
|
strcmp(const char *s1, const char *s2)
|
|
{
|
|
|
|
for (; *s1 == *s2 && *s1; s1++, s2++)
|
|
;
|
|
return ((u_char)*s1 - (u_char)*s2);
|
|
}
|
|
|
|
int
|
|
main(int ac, char **av)
|
|
{
|
|
const char *path;
|
|
int i;
|
|
|
|
path = PATH_LOADER;
|
|
for (i = 0; i < ac; i++) {
|
|
switch (av[i][0]) {
|
|
case '-':
|
|
switch (av[i][1]) {
|
|
default:
|
|
usage();
|
|
}
|
|
break;
|
|
default:
|
|
path = av[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
#ifdef ZFSBOOT
|
|
printf(" \n>> FreeBSD/sparc64 ZFS boot block\n Boot path: %s\n",
|
|
bootpath);
|
|
#else
|
|
printf(" \n>> FreeBSD/sparc64 boot block\n Boot path: %s\n"
|
|
" Boot loader: %s\n", bootpath, path);
|
|
#endif
|
|
|
|
if (domount(bootpath) == -1)
|
|
panic("domount");
|
|
|
|
#ifdef ZFSBOOT
|
|
loadzfs();
|
|
#else
|
|
load(path);
|
|
#endif
|
|
return (1);
|
|
}
|
|
|
|
static void
|
|
usage(void)
|
|
{
|
|
|
|
printf("usage: boot device [/path/to/loader]\n");
|
|
exit(1);
|
|
}
|
|
|
|
static void
|
|
exit(int code)
|
|
{
|
|
|
|
ofw_exit();
|
|
}
|
|
|
|
#ifdef ZFSBOOT
|
|
|
|
#define VDEV_BOOT_OFFSET (2 * 256 * 1024)
|
|
static char zbuf[READ_BUF_SIZE];
|
|
|
|
static int
|
|
zbread(char *buf, off_t off, size_t bytes)
|
|
{
|
|
size_t len;
|
|
off_t poff;
|
|
off_t soff;
|
|
char *p;
|
|
unsigned int nb;
|
|
unsigned int lb;
|
|
|
|
p = buf;
|
|
soff = VDEV_BOOT_OFFSET + off;
|
|
lb = (soff + bytes + DEV_BSIZE - 1) / DEV_BSIZE;
|
|
poff = soff;
|
|
while (poff < soff + bytes) {
|
|
nb = lb - poff / DEV_BSIZE;
|
|
if (nb > READ_BUF_SIZE / DEV_BSIZE)
|
|
nb = READ_BUF_SIZE / DEV_BSIZE;
|
|
if (dskread(zbuf, poff / DEV_BSIZE, nb))
|
|
break;
|
|
if ((poff / DEV_BSIZE + nb) * DEV_BSIZE > soff + bytes)
|
|
len = soff + bytes - poff;
|
|
else
|
|
len = (poff / DEV_BSIZE + nb) * DEV_BSIZE - poff;
|
|
memcpy(p, zbuf + poff % DEV_BSIZE, len);
|
|
p += len;
|
|
poff += len;
|
|
}
|
|
return (poff - soff);
|
|
}
|
|
|
|
static void
|
|
loadzfs(void)
|
|
{
|
|
Elf64_Ehdr eh;
|
|
Elf64_Phdr ph;
|
|
caddr_t p;
|
|
int i;
|
|
|
|
if (zbread((char *)&eh, 0, sizeof(eh)) != sizeof(eh)) {
|
|
printf("Can't read elf header\n");
|
|
return;
|
|
}
|
|
if (!IS_ELF(eh)) {
|
|
printf("Not an ELF file\n");
|
|
return;
|
|
}
|
|
for (i = 0; i < eh.e_phnum; i++) {
|
|
fs_off = eh.e_phoff + i * eh.e_phentsize;
|
|
if (zbread((char *)&ph, fs_off, sizeof(ph)) != sizeof(ph)) {
|
|
printf("Can't read program header %d\n", i);
|
|
return;
|
|
}
|
|
if (ph.p_type != PT_LOAD)
|
|
continue;
|
|
fs_off = ph.p_offset;
|
|
p = (caddr_t)ph.p_vaddr;
|
|
if (zbread(p, fs_off, ph.p_filesz) != ph.p_filesz) {
|
|
printf("Can't read content of section %d\n", i);
|
|
return;
|
|
}
|
|
if (ph.p_filesz != ph.p_memsz)
|
|
bzero(p + ph.p_filesz, ph.p_memsz - ph.p_filesz);
|
|
}
|
|
ofw_close(bootdev);
|
|
(*(void (*)(int, int, int, int, ofwfp_t))eh.e_entry)(0, 0, 0, 0, ofw);
|
|
}
|
|
|
|
#else
|
|
|
|
#include "ufsread.c"
|
|
|
|
static struct dmadat __dmadat;
|
|
|
|
static void
|
|
load(const char *fname)
|
|
{
|
|
Elf64_Ehdr eh;
|
|
Elf64_Phdr ph;
|
|
caddr_t p;
|
|
ufs_ino_t ino;
|
|
int i;
|
|
|
|
if ((ino = lookup(fname)) == 0) {
|
|
printf("File %s not found\n", fname);
|
|
return;
|
|
}
|
|
if (fsread(ino, &eh, sizeof(eh)) != sizeof(eh)) {
|
|
printf("Can't read elf header\n");
|
|
return;
|
|
}
|
|
if (!IS_ELF(eh)) {
|
|
printf("Not an ELF file\n");
|
|
return;
|
|
}
|
|
for (i = 0; i < eh.e_phnum; i++) {
|
|
fs_off = eh.e_phoff + i * eh.e_phentsize;
|
|
if (fsread(ino, &ph, sizeof(ph)) != sizeof(ph)) {
|
|
printf("Can't read program header %d\n", i);
|
|
return;
|
|
}
|
|
if (ph.p_type != PT_LOAD)
|
|
continue;
|
|
fs_off = ph.p_offset;
|
|
p = (caddr_t)ph.p_vaddr;
|
|
if (fsread(ino, p, ph.p_filesz) != ph.p_filesz) {
|
|
printf("Can't read content of section %d\n", i);
|
|
return;
|
|
}
|
|
if (ph.p_filesz != ph.p_memsz)
|
|
bzero(p + ph.p_filesz, ph.p_memsz - ph.p_filesz);
|
|
}
|
|
ofw_close(bootdev);
|
|
(*(void (*)(int, int, int, int, ofwfp_t))eh.e_entry)(0, 0, 0, 0, ofw);
|
|
}
|
|
|
|
#endif /* ZFSBOOT */
|
|
|
|
static int
|
|
domount(const char *device)
|
|
{
|
|
|
|
if ((bootdev = ofw_open(device)) == -1) {
|
|
printf("domount: can't open device\n");
|
|
return (-1);
|
|
}
|
|
#ifndef ZFSBOOT
|
|
dmadat = &__dmadat;
|
|
if (fsread(0, NULL, 0)) {
|
|
printf("domount: can't read superblock\n");
|
|
return (-1);
|
|
}
|
|
#endif
|
|
return (0);
|
|
}
|
|
|
|
static int
|
|
dskread(void *buf, u_int64_t lba, int nblk)
|
|
{
|
|
|
|
/*
|
|
* The Open Firmware should open the correct partition for us.
|
|
* That means, if we read from offset zero on an open instance handle,
|
|
* we should read from offset zero of that partition.
|
|
*/
|
|
ofw_seek(bootdev, lba * DEV_BSIZE);
|
|
ofw_read(bootdev, buf, nblk * DEV_BSIZE);
|
|
return (0);
|
|
}
|
|
|
|
static void
|
|
panic(const char *fmt, ...)
|
|
{
|
|
char buf[128];
|
|
va_list ap;
|
|
|
|
va_start(ap, fmt);
|
|
vsnprintf(buf, sizeof buf, fmt, ap);
|
|
printf("panic: %s\n", buf);
|
|
va_end(ap);
|
|
|
|
exit(1);
|
|
}
|
|
|
|
static int
|
|
printf(const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
int ret;
|
|
|
|
va_start(ap, fmt);
|
|
ret = vprintf(fmt, ap);
|
|
va_end(ap);
|
|
return (ret);
|
|
}
|
|
|
|
static int
|
|
putchar(char c, void *arg)
|
|
{
|
|
char buf;
|
|
|
|
if (c == '\n') {
|
|
buf = '\r';
|
|
ofw_write(stdouth, &buf, 1);
|
|
}
|
|
buf = c;
|
|
ofw_write(stdouth, &buf, 1);
|
|
return (1);
|
|
}
|
|
|
|
static int
|
|
vprintf(const char *fmt, va_list ap)
|
|
{
|
|
int ret;
|
|
|
|
ret = __printf(fmt, putchar, 0, ap);
|
|
return (ret);
|
|
}
|
|
|
|
static int
|
|
vsnprintf(char *str, size_t sz, const char *fmt, va_list ap)
|
|
{
|
|
struct sp_data sp;
|
|
int ret;
|
|
|
|
sp.sp_buf = str;
|
|
sp.sp_len = 0;
|
|
sp.sp_size = sz;
|
|
ret = __printf(fmt, __sputc, &sp, ap);
|
|
return (ret);
|
|
}
|
|
|
|
static int
|
|
__printf(const char *fmt, putc_func_t *putc, void *arg, va_list ap)
|
|
{
|
|
char buf[(sizeof(long) * 8) + 1];
|
|
char *nbuf;
|
|
u_long ul;
|
|
u_int ui;
|
|
int lflag;
|
|
int sflag;
|
|
char *s;
|
|
int pad;
|
|
int ret;
|
|
int c;
|
|
|
|
nbuf = &buf[sizeof buf - 1];
|
|
ret = 0;
|
|
while ((c = *fmt++) != 0) {
|
|
if (c != '%') {
|
|
ret += putc(c, arg);
|
|
continue;
|
|
}
|
|
lflag = 0;
|
|
sflag = 0;
|
|
pad = 0;
|
|
reswitch: c = *fmt++;
|
|
switch (c) {
|
|
case '#':
|
|
sflag = 1;
|
|
goto reswitch;
|
|
case '%':
|
|
ret += putc('%', arg);
|
|
break;
|
|
case 'c':
|
|
c = va_arg(ap, int);
|
|
ret += putc(c, arg);
|
|
break;
|
|
case 'd':
|
|
if (lflag == 0) {
|
|
ui = (u_int)va_arg(ap, int);
|
|
if (ui < (int)ui) {
|
|
ui = -ui;
|
|
ret += putc('-', arg);
|
|
}
|
|
s = __uitoa(nbuf, ui, 10);
|
|
} else {
|
|
ul = (u_long)va_arg(ap, long);
|
|
if (ul < (long)ul) {
|
|
ul = -ul;
|
|
ret += putc('-', arg);
|
|
}
|
|
s = __ultoa(nbuf, ul, 10);
|
|
}
|
|
ret += __puts(s, putc, arg);
|
|
break;
|
|
case 'l':
|
|
lflag = 1;
|
|
goto reswitch;
|
|
case 'o':
|
|
if (lflag == 0) {
|
|
ui = (u_int)va_arg(ap, u_int);
|
|
s = __uitoa(nbuf, ui, 8);
|
|
} else {
|
|
ul = (u_long)va_arg(ap, u_long);
|
|
s = __ultoa(nbuf, ul, 8);
|
|
}
|
|
ret += __puts(s, putc, arg);
|
|
break;
|
|
case 'p':
|
|
ul = (u_long)va_arg(ap, void *);
|
|
s = __ultoa(nbuf, ul, 16);
|
|
ret += __puts("0x", putc, arg);
|
|
ret += __puts(s, putc, arg);
|
|
break;
|
|
case 's':
|
|
s = va_arg(ap, char *);
|
|
ret += __puts(s, putc, arg);
|
|
break;
|
|
case 'u':
|
|
if (lflag == 0) {
|
|
ui = va_arg(ap, u_int);
|
|
s = __uitoa(nbuf, ui, 10);
|
|
} else {
|
|
ul = va_arg(ap, u_long);
|
|
s = __ultoa(nbuf, ul, 10);
|
|
}
|
|
ret += __puts(s, putc, arg);
|
|
break;
|
|
case 'x':
|
|
if (lflag == 0) {
|
|
ui = va_arg(ap, u_int);
|
|
s = __uitoa(nbuf, ui, 16);
|
|
} else {
|
|
ul = va_arg(ap, u_long);
|
|
s = __ultoa(nbuf, ul, 16);
|
|
}
|
|
if (sflag)
|
|
ret += __puts("0x", putc, arg);
|
|
ret += __puts(s, putc, arg);
|
|
break;
|
|
case '0': case '1': case '2': case '3': case '4':
|
|
case '5': case '6': case '7': case '8': case '9':
|
|
pad = pad * 10 + c - '0';
|
|
goto reswitch;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return (ret);
|
|
}
|
|
|
|
static int
|
|
__sputc(char c, void *arg)
|
|
{
|
|
struct sp_data *sp;
|
|
|
|
sp = arg;
|
|
if (sp->sp_len < sp->sp_size)
|
|
sp->sp_buf[sp->sp_len++] = c;
|
|
sp->sp_buf[sp->sp_len] = '\0';
|
|
return (1);
|
|
}
|
|
|
|
static int
|
|
__puts(const char *s, putc_func_t *putc, void *arg)
|
|
{
|
|
const char *p;
|
|
int ret;
|
|
|
|
ret = 0;
|
|
for (p = s; *p != '\0'; p++)
|
|
ret += putc(*p, arg);
|
|
return (ret);
|
|
}
|
|
|
|
static char *
|
|
__uitoa(char *buf, u_int ui, int base)
|
|
{
|
|
char *p;
|
|
|
|
p = buf;
|
|
*p = '\0';
|
|
do
|
|
*--p = digits[ui % base];
|
|
while ((ui /= base) != 0);
|
|
return (p);
|
|
}
|
|
|
|
static char *
|
|
__ultoa(char *buf, u_long ul, int base)
|
|
{
|
|
char *p;
|
|
|
|
p = buf;
|
|
*p = '\0';
|
|
do
|
|
*--p = digits[ul % base];
|
|
while ((ul /= base) != 0);
|
|
return (p);
|
|
}
|