1
0
mirror of https://github.com/skeeto/w64devkit.git synced 2025-11-04 13:11:39 +03:00

w64devkit.c: Improve legibility and maintenance

I've learned a lot since I wrote this two and half years ago. This was
one of my earliest serious attempts at building a CRT-free program, and
I had not yet learned the techniques to do so effectively. I lacked the
experience for dealing with certain common problems, and in some areas
approached it with the wrong mindset. For example, much of the original
program concerns tracking many little string lengths, which is finicky,
hard to follow, and difficult to modify. Better to operate in terms of
appending to buffers.

I also relied far too much on stack allocation, which has multiple
problems. First is friction with the toolchain. By default on Windows,
GCC inserts stack probes for frames larger than 4kiB in order to safely
commit more stack. Otherwise the program might skip the guard page. To
avoid this, I turned off probes and increased the committed stack size,
which requires multiple compiler/linker arguments. I'd like to avoid
that.

The second is that stack allocations have fixed, static sizes if you're
not using VLAs, which you're not if you're sane. So I need to figure out
and use the worst cast maximum sizes for everything. The stack has a
small, limited size, so I can't overestimate by too much either.

The third is that the stack remains committed until exit. There's no
giving it back before waiting on the long-lived shell process. It's
hardly anything, but I'd at least like the option.

All these problems are solved with region-based allocation. I get all
the nice stack semantics and lifetimes without any of the downsides. I
only really got the hang of arenas in the past couple years, which is
why I didn't use them in the original version. Now the entire program
probably uses under 1kiB of its stack.

I've also honed my style, in this case most notably in type names and
Win32 declarations. Along with no const, I find it much more readable
than the official names and style. As a bonus there's a nice, short
listing of all the required external functionality. This could be
plucked out as a .def file and used to build an import library, which
would eliminate the final dependency on Mingw-w64. Another major benefit
is faster build times. The new w64devkit.c cuts the build time by more
than 99%, merely by not including windows.h.

I've also learned other subtleties these past couple years:

1. GCC assumes the stack is 16-byte aligned, even on x86, but does not
guarantee 4-byte alignment, and such functions are usually called by
code not generated by GCC (e.g. called by Windows). This isn't only a
problem for CRT-free programs. It comes up most often with CreateThread,
and many GCC-compiled programs are subtly broken because of this. MSVC
and Clang do not have this issue.

2. GCC does not reliably generate correct code without -fno-builtin when
CRT-free, particularly for standard functions. Despite -fno-builtin, it
may generate calls to these functions, so sometimes you must provide
them. Clang also generates such calls despite being asked not to do so,
but it does not require -fno-builtin for correctness. You cannot even
ask MSVC not to call standard functions, but like Clang, it's careful
not to generate the wrong code for your own implementations. So always
use -fno-builtin with GCC when not linking a standard library.

3. While returning from the process entrypoint is technically valid and
generally works (in console applications), calling ExitProcess is more
robust and reliable. On Windows it's not uncommon for other processes to
meddle, including spawning uncooperative threads. These are bugs in the
other program, but they go unnoticed since virtually nothing actually
exits by returning from the entrypoint. It pays to play along.

4. In documentation, -nostartfiles is better than -nostdlib. It's
simpler and more fool-proof. When something's not working quite right,
it tends to quietly fill in the gaps. I still use -nostdlib in scripts
because it's stricter, and I can fix the problems that might arise.
This commit is contained in:
Christopher Wellons
2023-08-28 15:55:30 -04:00
parent 0a652a6c80
commit 568efdcd2c
3 changed files with 519 additions and 138 deletions

View File

@@ -472,12 +472,9 @@ RUN rm -rf $PREFIX/share/man/ $PREFIX/share/info/ $PREFIX/share/gcc-* \
COPY README.md Dockerfile src/w64devkit.ini $PREFIX/
RUN printf "id ICON \"$PREFIX/src/w64devkit.ico\"" >w64devkit.rc \
&& $ARCH-windres -o w64devkit.o w64devkit.rc \
&& $ARCH-gcc -DVERSION=$VERSION \
-mno-stack-arg-probe -Xlinker --stack=0x10000,0x10000 \
-Os -fno-asynchronous-unwind-tables \
-Wl,--gc-sections -s -nostdlib \
-o $PREFIX/w64devkit.exe $PREFIX/src/w64devkit.c w64devkit.o \
-lkernel32 \
&& $ARCH-gcc -DVERSION=$VERSION -nostdlib -fno-asynchronous-unwind-tables \
-fno-builtin -Wl,--gc-sections -s -o $PREFIX/w64devkit.exe \
$PREFIX/src/w64devkit.c w64devkit.o -lkernel32 -luser32 \
&& $ARCH-gcc \
-Os -fno-asynchronous-unwind-tables \
-Wl,--gc-sections -s -nostdlib \

View File

@@ -1,168 +1,552 @@
/* Tiny, standalone launcher for w64devkit
* This avoids running a misbehaving monitor cmd.exe in the background.
*
* $ gcc -DVERSION="$VERSION" \
* -mno-stack-arg-probe -Xlinker --stack=0x10000,0x10000 \
* -Os -fno-asynchronous-unwind-tables -Wl,--gc-sections \
* -s -nostdlib -o w64devkit.exe w64devkit.c -lkernel32
*
* This is free and unencumbered software released into the public domain.
*/
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
// Tiny, standalone launcher for w64devkit
// * Sets $W64DEVKIT to the release version (-DVERSION)
// * Sets $W64DEVKIT_HOME to the install location
// * Maybe sets $HOME according to w64devkit.ini
// * Starts a login shell with "sh -l"
//
// $ gcc -DVERSION="$VERSION" -nostartfiles -fno-builtin
// -o w64devkit.exe w64devkit.c
//
// This is free and unencumbered software released into the public domain.
#define COUNTOF(a) (sizeof(a) / sizeof(0[a]))
#define LSTR(s) XSTR(s)
#define XSTR(s) L ## # s
#define MAX_VAR 32767
#define sizeof(a) (size)(sizeof(a))
#define alignof(a) (size)(_Alignof(a))
#define countof(a) (sizeof(a) / sizeof(*(a)))
#define lengthof(s) (countof(s) - 1)
enum err {ERR_PATH, ERR_EXEC};
static const char err_path[] = "w64devkit: failed to set $PATH\n";
static const char err_exec[] = "w64devkit: failed to start shell\n";
#define new(...) newx(__VA_ARGS__, new4, new3, new2)(__VA_ARGS__)
#define newx(a,b,c,d,e,...) e
#define new2(a, t) (t *)alloc(a, sizeof(t), alignof(t), 1, 0)
#define new3(a, t, n) (t *)alloc(a, sizeof(t), alignof(t), n, 0)
#define new4(a, t, n, f) (t *)alloc(a, sizeof(t), alignof(t), n, f)
static int
fatal(enum err e)
typedef unsigned char byte;
typedef __UINT8_TYPE__ u8;
typedef unsigned short u16;
typedef signed int i32;
typedef signed int b32;
typedef unsigned int u32;
typedef __UINTPTR_TYPE__ uptr;
typedef __PTRDIFF_TYPE__ size;
typedef __SIZE_TYPE__ usize;
typedef unsigned short char16_t; // for GDB
typedef char16_t c16;
typedef struct {} *handle;
typedef struct {
u32 cb;
uptr a, b, c;
i32 d, e, f, g, h, i, j, k;
u16 l, m;
uptr n, o, p, q;
} si;
typedef struct {
handle process;
handle thread;
u32 pid;
u32 tid;
} pi;
#define MAX_PATH 260
#define MAX_ENVVAR 32767
#define MAX_CMDLINE 32767
#define MAX_INI (1<<18)
#define CP_UTF8 65001
#define PAGE_READWRITE 0x04
#define MEM_COMMIT 0x1000
#define MEM_RESERVE 0x2000
#define MEM_RELEASE 0x8000
#define GENERIC_READ 0x80000000
#define OPEN_EXISTING 3
#define FILE_SHARE_ALL 7
#define W32 __attribute((dllimport,stdcall))
W32 b32 CloseHandle(handle);
W32 handle CreateFileW(c16 *, u32, u32, void *, u32, u32, handle);
W32 b32 CreateProcessW(c16*,c16*,void*,void*,i32,u32,c16*,c16*,si*,pi*);
W32 void ExitProcess(u32) __attribute((noreturn));
W32 u32 ExpandEnvironmentStringsW(c16 *, c16 *, u32);
W32 u32 GetEnvironmentVariableW(c16 *, c16 *, u32);
W32 i32 GetExitCodeProcess(handle, u32 *);
W32 u32 GetFullPathNameW(c16 *, u32, c16 *, c16 *);
W32 u32 GetModuleFileNameW(handle, c16 *, u32);
W32 i32 MessageBoxW(handle, c16 *, c16 *, u32);
W32 i32 MultiByteToWideChar(u32, u32, u8 *, i32, c16 *, i32);
W32 b32 ReadFile(handle, u8 *, u32, u32 *, void *);
W32 b32 SetConsoleTitleW(c16 *);
W32 b32 SetCurrentDirectoryW(c16 *);
W32 b32 SetEnvironmentVariableW(c16 *, c16 *);
W32 byte *VirtualAlloc(byte *, usize, u32, u32);
W32 b32 VirtualFree(byte *, usize, u32);
W32 u32 WaitForSingleObject(handle, u32);
#define S(s) (s8){(u8 *)s, lengthof(s)}
typedef struct {
u8 *s;
size len;
} s8;
#define U(s) (s16){s, lengthof(s)}
typedef struct {
c16 *s;
size len;
} s16;
static s8 s8span(u8 *beg, u8 *end)
{
size_t len = 0;
const char *msg = 0;
switch (e) {
case ERR_PATH: msg = err_path; len = sizeof(err_path) - 1; break;
case ERR_EXEC: msg = err_exec; len = sizeof(err_exec) - 1; break;
s8 s = {};
s.s = beg;
s.len = end - beg;
return s;
}
static b32 s8equals(s8 a, s8 b)
{
if (a.len != b.len) {
return 0;
}
HANDLE out = GetStdHandle(STD_ERROR_HANDLE);
DWORD dummy;
WriteFile(out, msg, len, &dummy, 0);
return 2;
}
static void
lmemmove(WCHAR *dst, const WCHAR *src, size_t len)
{
for (size_t i = 0; i < len; i++) dst[i] = src[i];
}
static WCHAR *
findfile(WCHAR *s)
{
for (WCHAR *r = s; ; s++) {
switch (*s) {
case 0: return r;
case '/':
case '\\': r = s + 1;
for (size i = 0; i < a.len; i++) {
if (a.s[i] != b.s[i]) {
return 0;
}
}
return 1;
}
/* Read and process "w64devkit.home" from "w64devkit.ini". Environment
* variables are expanded, and if relative, the result is converted into
* an absolute path. The destination length must be MAX_PATH. If this
* fails for any reason, the string will be zero length.
*
* Before calling, the current working directory must be changed to the
* location of w64devkit.exe.
*/
static void
homeconfig(WCHAR *path)
static void fatal(c16 *msg)
{
char home[MAX_PATH*3]; /* UTF-8 MAX_PATH */
WCHAR whome[MAX_PATH*2]; /* extra room for variables */
WCHAR expanded[MAX_PATH];
/* If anything fails, leave empty. */
path[0] = 0;
/* Windows thinks this is a narrow, "ANSI"-encoded file, but it's
* really UTF-8. It's decoded to UTF-16 after reading. This means
* the INI path must be a narrow string, hence the requirement of
* changing the current directory before this call, in case the
* INI's absolute path contains non-ASCII characters.
*
* NOTE(Chris): This Win32 function works fine for now, but in the
* future I may replace it with a simple, embedded INI parser. I
* already have one written and available in my scratch repository.
*/
GetPrivateProfileStringA(
"w64devkit",
"home",
0,
home,
sizeof(home),
"./w64devkit.ini"
);
if (!MultiByteToWideChar(CP_UTF8, 0, home, -1, whome, MAX_PATH*2)) {
return;
}
/* Process INI string into a final HOME path */
if (ExpandEnvironmentStringsW(whome, expanded, MAX_PATH) > MAX_PATH) {
return;
}
GetFullPathNameW(expanded, MAX_PATH, path, 0);
MessageBoxW(0, msg, u"w64devkit launcher", 0x10);
ExitProcess(2);
}
static DWORD w64devkit(void)
typedef struct {
byte *mem;
size cap;
size off;
} arena;
static arena *newarena(size cap)
{
WCHAR path[MAX_PATH + MAX_VAR];
arena *a = 0;
byte *mem = VirtualAlloc(0, cap, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE);
if (mem) {
a = (arena *)mem;
a->mem = mem;
a->cap = cap;
a->off = sizeof(arena);
}
return a;
}
/* Construct a path to bin/ directory */
static const WCHAR bin[] = L"bin;";
GetModuleFileNameW(0, path, MAX_PATH);
WCHAR *tail = findfile(path);
{
/* Set W64DEVKIT_HOME to this module's directory */
int save = tail[-1];
tail[-1] = 0;
SetEnvironmentVariableW(L"W64DEVKIT_HOME", path); // ignore errors
static void freearena(arena *a)
{
VirtualFree(a->mem, 0, MEM_RELEASE);
}
/* Maybe set HOME from w64devkit.ini */
if (SetCurrentDirectoryW(path)) {
WCHAR home[MAX_PATH];
homeconfig(home);
if (home[0]) {
SetEnvironmentVariableW(L"HOME", home); // ignore errors
#define NOZERO (1<<0)
#define SOFTFAIL (1<<1)
__attribute((malloc))
__attribute((alloc_align(3)))
__attribute((alloc_size(2, 4)))
static byte *alloc(arena *a, size objsize, size align, size count, i32 flags)
{
size avail = a->cap - a->off;
size pad = -a->off & (align - 1);
if (count > (avail - pad)/objsize) {
if (flags & SOFTFAIL) {
return 0;
}
fatal(u"Out of memory");
}
size total = count*objsize;
byte *p = a->mem + a->off + pad;
if (!(flags & NOZERO)) {
for (size i = 0; i < total; i++) {
p[i] = 0;
}
}
a->off += pad + total;
return p;
}
static arena splitarena(arena *a, i32 div)
{
size avail = a->cap - a->off;
size cap = avail / div;
arena sub = {};
sub.mem = alloc(a, 1, 32, cap, NOZERO);
sub.cap = cap;
return sub;
}
typedef enum {
INI_eof,
INI_section,
INI_key,
INI_value
} initype;
typedef struct {
s8 name;
initype type;
} initoken;
typedef struct {
u8 *beg;
u8 *end;
b32 invalue;
} iniparser;
static b32 inidone(iniparser *p)
{
return p->beg == p->end;
}
static u8 ininext(iniparser *p)
{
return *p->beg++;
}
static b32 iniblank(u8 c)
{
return c==' ' || c=='\t' || c=='\r';
}
static void iniskip(iniparser *p)
{
for (; !inidone(p) && iniblank(*p->beg); p->beg++) {}
}
static u8 *initok(iniparser *p, u8 term)
{
u8 *end = p->beg;
while (!inidone(p)) {
u8 c = ininext(p);
if (c == term) {
return end;
} else if (c == '\n') {
break;
} else if (!iniblank(c)) {
end = p->beg;
}
}
return term=='\n' ? end : 0;
}
static b32 iniquoted(s8 s)
{
return s.len>1 && s.s[0]=='"' && s.s[s.len-1]=='"';
}
// Parses like GetPrivateProfileString except sections cannot contain
// newlines. Invalid input lines are ignored. Comment lines begin with
// ';' following any whitespace. No trailing comments. Trims leading and
// trailing whitespace from sections, keys, and values. To preserve
// whitespace, values may be double-quoted. No quote escapes. Content on
// a line following a closing section ']' is ignored. Token encoding
// matches input encoding. An INI_value always follows an INI_key key.
static initoken iniparse(iniparser *p)
{
initoken r = {};
if (p->invalue) {
p->invalue = 0;
iniskip(p);
u8 *beg = p->beg;
u8 *end = initok(p, '\n');
r.name = s8span(beg, end);
r.type = INI_value;
if (iniquoted(r.name)) {
r.name.s++;
r.name.len -= 2;
}
return r;
}
for (;;) {
iniskip(p);
if (inidone(p)) {
return r;
}
u8 *end;
u8 *beg = p->beg;
switch (ininext(p)) {
case ';':
while (!inidone(p) && ininext(p)!='\n') {}
break;
case '[':
iniskip(p);
beg = p->beg;
end = initok(p, ']');
if (end) {
// skip over anything else on the line
while (!inidone(p) && ininext(p)!='\n') {}
r.name = s8span(beg, end);
r.type = INI_section;
return r;
}
break;
case '\n':
break;
default:
end = initok(p, '=');
if (end) {
p->invalue = 1;
r.name = s8span(beg, end);
r.type = INI_key;
return r;
}
}
tail[-1] = save;
}
lmemmove(tail, bin, COUNTOF(bin));
size_t binlen = tail - path + COUNTOF(bin) - 1;
}
/* Preprend bin/ path to $PATH */
GetEnvironmentVariableW(L"PATH", path+binlen, MAX_VAR);
if (!SetEnvironmentVariableW(L"PATH", path)) {
return fatal(ERR_PATH);
typedef enum {
sym_null = 0,
sym_w64devkit,
sym_home,
} symbol;
static symbol intern(s8 s)
{
static struct {
s8 name;
symbol symbol;
} symbols[] = {
{S("w64devkit"), sym_w64devkit},
{S("home"), sym_home},
};
for (size i = 0; i < countof(symbols); i++) {
if (s8equals(symbols[i].name, s)) {
return symbols[i].symbol;
}
}
return sym_null;
}
static u8 *makecstr(arena *a, s8 s)
{
u8 *r = new(a, u8, s.len+1);
for (size i = 0; i < s.len; i++) {
r[i] = s.s[i];
}
return r;
}
// Read and process "w64devkit.home" from "w64devkit.ini". Environment
// variables are expanded, and if relative, the result is converted into
// an absolute path. Returns null on error.
//
// Before calling, the current working directory must be changed to the
// location of w64devkit.exe.
static c16 *homeconfig(arena *perm, arena scratch)
{
handle h = CreateFileW(
u"w64devkit.ini",
GENERIC_READ,
FILE_SHARE_ALL,
0,
OPEN_EXISTING,
0,
0
);
if (h == (handle)-1) {
return 0;
}
iniparser *p = new(&scratch, iniparser);
p->beg = new(&scratch, u8, MAX_INI, NOZERO);
u32 inilen;
b32 r = ReadFile(h, p->beg, MAX_INI, &inilen, 0);
CloseHandle(h);
if (!r || inilen == MAX_INI) {
return 0;
}
p->end = p->beg + inilen;
u8 *home = 0;
u32 len = 0;
for (symbol section = 0, key = 0;;) {
initoken t = iniparse(p);
switch (t.type) {
case INI_eof:
break;
case INI_section:
section = intern(t.name);
continue;
case INI_key:
key = intern(t.name);
continue;
case INI_value:
if (!home && section==sym_w64devkit && key==sym_home) {
home = makecstr(&scratch, t.name);
len = (u32)(t.name.len + 1); // include terminator
}
continue;
}
break;
}
c16 *whome = new(&scratch, c16, len);
if (!MultiByteToWideChar(CP_UTF8, 0, home, len, whome, len)) {
return 0;
}
// Process INI string into a final HOME path. Allocate a bit more
// than MAX_PATH, because GetFullPathNameW could technically reduce
// it to within MAX_PATH if there are lots of relative components.
u32 cap = MAX_PATH*4;
c16 *expanded = new(&scratch, c16, cap);
len = ExpandEnvironmentStringsW(whome, expanded, cap);
if (!len || len>cap) {
return 0;
}
// The final result must fit within MAX_PATH in order to be useful.
c16 *path = new(perm, c16, MAX_PATH);
len = GetFullPathNameW(expanded, MAX_PATH, path, 0);
if (!len || len>=MAX_PATH) {
return 0;
}
return path;
}
typedef struct {
c16 *buf;
size cap;
size len;
b32 err;
} buf16;
static buf16 newbuf16(arena *a, size cap)
{
buf16 buf = {};
buf.buf = new(a, c16, cap, NOZERO);
buf.cap = cap;
return buf;
}
static void buf16cat(buf16 *buf, s16 s)
{
size avail = buf->cap - buf->len;
size count = s.len<avail ? s.len : avail;
c16 *dst = buf->buf + buf->len;
for (size i = 0; i < count; i++) {
dst[i] = s.s[i];
}
buf->len += count;
buf->err |= count < s.len;
}
static void buf16c16(buf16 *buf, c16 c)
{
s16 s = {&c, 1};
buf16cat(buf, s);
}
static void buf16moduledir(buf16 *buf, arena scratch)
{
c16 *path = new(&scratch, c16, MAX_PATH);
size len = GetModuleFileNameW(0, path, MAX_PATH);
for (; len; len--) {
switch (path[len-1]) {
case '/':
case '\\': buf16cat(buf, (s16){path, len-1});
return;
}
}
}
static void buf16getenv(buf16 *buf, c16 *key, arena scratch)
{
s16 var = {};
var.s = new(&scratch, c16, MAX_ENVVAR, NOZERO);
u32 len = GetEnvironmentVariableW(key, var.s, MAX_ENVVAR);
var.len = len>=MAX_ENVVAR ? 0 : len;
buf16cat(buf, var);
}
static void toslashes(c16 *path)
{
for (size i = 0; i < path[i]; i++) {
path[i] = path[i]=='\\' ? '/' : path[i];
}
}
static u32 w64devkit(void)
{
arena *perm = newarena(1<<22);
if (!perm) {
fatal(u"Out of memory on startup");
}
arena scratch = splitarena(perm, 2);
// First load the module directory into the fresh buffer, and use it
// for a few different operations.
buf16 path = newbuf16(perm, MAX_ENVVAR);
buf16moduledir(&path, scratch);
buf16 moduledir = path; // to truncate back to the module dir
buf16c16(&path, 0); // null terminator
SetEnvironmentVariableW(u"W64DEVKIT_HOME", path.buf); // ignore errors
// Maybe set HOME from w64devkit.ini
if (SetCurrentDirectoryW(path.buf)) {
c16 *home = homeconfig(perm, scratch);
if (home) {
toslashes(home);
SetEnvironmentVariableW(u"HOME", home); // ignore errors
}
}
// Continue building PATH
path = moduledir;
buf16cat(&path, U(u"\\bin;"));
buf16getenv(&path, u"PATH", scratch);
buf16c16(&path, 0); // null terminator
if (path.err || !SetEnvironmentVariableW(u"PATH", path.buf)) {
fatal(u"Failed to configure $PATH");
}
#ifdef VERSION
SetEnvironmentVariableW(L"W64DEVKIT", LSTR(VERSION)); // ignore errors
#define LSTR(s) XSTR(s)
#define XSTR(s) u ## # s
SetEnvironmentVariableW(u"W64DEVKIT", LSTR(VERSION)); // ignore errors
#endif
// Set the console title as late as possible, but not after starting
// the shell because .profile might change it.
SetConsoleTitleA("w64devkit");
SetConsoleTitleW(u"w64devkit"); // ignore errors
/* Start a BusyBox login shell */
STARTUPINFOW si;
GetStartupInfoW(&si);
PROCESS_INFORMATION pi;
static const WCHAR busybox[] = L"bin\\busybox.exe";
lmemmove(tail, busybox, COUNTOF(busybox));
if (!CreateProcessW(path, L"sh -l", 0, 0, TRUE, 0, 0, 0, &si, &pi)) {
return fatal(ERR_EXEC);
path = moduledir;
buf16cat(&path, U(u"\\bin\\busybox.exe"));
buf16c16(&path, 0); // null terminator
// Start a BusyBox login shell
si si = {};
si.cb = sizeof(si);
pi pi;
c16 cmdline[] = u"sh -l"; // NOTE: must be mutable!
if (!CreateProcessW(path.buf, cmdline, 0, 0, 1, 0, 0, 0, &si, &pi)) {
fatal(u"Failed to launch a login shell");
}
/* Wait for shell to exit */
DWORD ret;
WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &ret);
// Wait for shell to exit
freearena(perm);
u32 ret;
WaitForSingleObject(pi.process, -1);
GetExitCodeProcess(pi.process, &ret);
return ret;
}
#if __i386
__attribute((force_align_arg_pointer))
#endif
void mainCRTStartup(void)
{
DWORD r = w64devkit();
u32 r = w64devkit();
ExitProcess(r);
}

View File

@@ -15,4 +15,4 @@
; paths, so be mindful when using non-ASCII paths for this value.
;
;home = ..\home
;home = %HOMEDRIVE%\%HOMEPATH%
;home = %HOMEDRIVE%%HOMEPATH%