feat(start): _start, __libc_start_main, exit/atexit/environ
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
* vlibc — startup test (todo 3).
|
||||
*
|
||||
* Exercises the static program-startup path end to end. This test is
|
||||
* COMPILED INTO A STATIC VLIBC BINARY (harness commands in
|
||||
* .omo/evidence/task-3-full-posix.log): glibc is never linked, so every
|
||||
* exit()/atexit()/environ reference resolves to vlibc's own symbols via
|
||||
* crt1.s and the src/start objects. Running it under the host libc would
|
||||
* silently test glibc, not vlibc — the static link is the test.
|
||||
*
|
||||
* Modes (argv[1]):
|
||||
* (none) atexit LIFO: register A, B, C on the atexit list, Q on the
|
||||
* at_quick_exit list, then return 42 from main. exit() must run
|
||||
* C, B, A in that order (reverse registration), must NOT run Q,
|
||||
* and the process must exit with status 42.
|
||||
* -q quick_exit(5): the at_quick_exit handler runs, the atexit
|
||||
* handler must NOT. Status must be 5.
|
||||
* -z _exit(7): neither handler list runs at all. Status 7.
|
||||
* -e argv/env plumbing: prints argc and argv, checks
|
||||
* VLIBC_TEST_VAR=hello in environ. Status 0 on match.
|
||||
* -t main-thread TLS + errno: a file-scope __thread int set in main
|
||||
* must read back through a separate function (exercises the
|
||||
* PT_TLS copy path), and errno must round-trip through the TCB
|
||||
* slot (proves the bootstrap ran before the first libc call).
|
||||
* -x handler-list limits: 32 registrations fit, the 33rd returns
|
||||
* -1, and atexit() during exit is gracefully refused (-1).
|
||||
*
|
||||
* No host headers: -Iinclude shadows GCC's internal headers (see
|
||||
* tests/test_strerror.c). <stddef.h>/<errno.h> below are vlibc's own.
|
||||
* All diagnostics go through raw SYS_write.
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "../src/internal/syscall.h"
|
||||
|
||||
/*
|
||||
* Manual declarations of the startup surface under test: <stdlib.h> and
|
||||
* <unistd.h> are later todos; consumers today declare exactly these names,
|
||||
* matching src/start/start.h.
|
||||
*/
|
||||
[[noreturn]] void
|
||||
exit(int status);
|
||||
[[noreturn]] void
|
||||
_Exit(int status);
|
||||
[[noreturn]] void
|
||||
_exit(int status);
|
||||
[[noreturn]] void
|
||||
quick_exit(int status);
|
||||
int
|
||||
atexit(void (*func)(void));
|
||||
int
|
||||
at_quick_exit(void (*func)(void));
|
||||
extern char **environ;
|
||||
|
||||
static int failures;
|
||||
|
||||
/* Write a NUL-terminated string to fd via the raw syscall layer. */
|
||||
static void
|
||||
say(int fd, const char *s)
|
||||
{
|
||||
long n = 0;
|
||||
|
||||
while (s[n] != '\0')
|
||||
{
|
||||
n++;
|
||||
}
|
||||
__syscall3(SYS_write, fd, (long)s, n);
|
||||
}
|
||||
|
||||
/* Write v in decimal to fd. */
|
||||
static void
|
||||
say_dec(int fd, unsigned long v) // NOLINT(bugprone-easily-swappable-parameters)
|
||||
{
|
||||
char buf[24];
|
||||
int i = (int)sizeof(buf);
|
||||
|
||||
buf[--i] = '\0';
|
||||
do
|
||||
{
|
||||
buf[--i] = (char)('0' + (v % 10));
|
||||
v /= 10;
|
||||
} while (v != 0);
|
||||
__syscall3(SYS_write, fd, (long)(buf + i), (long)(sizeof(buf) - 1 - i));
|
||||
}
|
||||
|
||||
static void
|
||||
check(int cond, const char *what)
|
||||
{
|
||||
if (!cond)
|
||||
{
|
||||
say(2, "FAIL: ");
|
||||
say(2, what);
|
||||
say(2, "\n");
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
/* True when s is exactly lit. Hand-rolled: this test keeps deps minimal. */
|
||||
static int
|
||||
str_is(const char *s, const char *lit)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
while (lit[i] != '\0' && s[i] == lit[i])
|
||||
{
|
||||
i++;
|
||||
}
|
||||
return lit[i] == '\0' && s[i] == '\0';
|
||||
}
|
||||
|
||||
/* True when environ holds an entry spelling "name=want". */
|
||||
static int // NOLINT(bugprone-easily-swappable-parameters)
|
||||
env_matches(const char *name, const char *want)
|
||||
{
|
||||
for (char **e = environ; e != 0 && *e != 0; e++)
|
||||
{
|
||||
const char *s = *e;
|
||||
int i = 0;
|
||||
|
||||
while (name[i] != '\0' && s[i] == name[i])
|
||||
{
|
||||
i++;
|
||||
}
|
||||
if (name[i] == '\0' && s[i] == '=')
|
||||
{
|
||||
int j = 0;
|
||||
|
||||
while (want[j] != '\0' && s[i + 1 + j] == want[j])
|
||||
{
|
||||
j++;
|
||||
}
|
||||
if (want[j] == '\0' && s[i + 1 + j] == '\0')
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Exit handlers. Each prints one distinguishing marker line. */
|
||||
static void
|
||||
hA(void)
|
||||
{
|
||||
say(1, "A\n");
|
||||
}
|
||||
|
||||
static void
|
||||
hB(void)
|
||||
{
|
||||
say(1, "B\n");
|
||||
}
|
||||
|
||||
static void
|
||||
hC(void)
|
||||
{
|
||||
say(1, "C\n");
|
||||
}
|
||||
|
||||
static void
|
||||
hQ(void)
|
||||
{
|
||||
say(1, "Q\n");
|
||||
}
|
||||
|
||||
static void
|
||||
hNoop(void)
|
||||
{}
|
||||
|
||||
/* Registered as one of the 32 in -x mode: registration during exit must fail. */
|
||||
static void
|
||||
hRegisterDuringExit(void)
|
||||
{
|
||||
if (atexit(hA) == 0)
|
||||
{
|
||||
say(2, "FAIL: atexit() during exit accepted (want graceful -1)\n");
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* File-scope __thread objects: live in PT_TLS, every access is %fs-relative
|
||||
* with a link-time offset. tls_preinit tests the .tdata image copy (nonzero
|
||||
* initial value), tls_counter tests the .tbss zeroing plus the write/read
|
||||
* round trip. The reads go through noinline helpers on opaque pointers:
|
||||
* a direct read of a static TLS object whose initializer the compiler can
|
||||
* see gets constant-folded, which would delete the variable (and with it
|
||||
* the PT_TLS image) and silently untest the copy path. The address-of and
|
||||
* the helper-force a real TLS relocation and a real memory load.
|
||||
*/
|
||||
static __thread int tls_preinit = 42;
|
||||
static __thread int tls_counter;
|
||||
|
||||
static int __attribute__((noinline))
|
||||
tls_deref(int *p)
|
||||
{
|
||||
/* Opaque barrier: prevents interprocedural folding of *p to 42, which
|
||||
* would delete the TLS image (see the block comment above). */
|
||||
__asm__ volatile("" : "+r"(p));
|
||||
return *p;
|
||||
}
|
||||
|
||||
static int __attribute__((noinline))
|
||||
tls_readback(void)
|
||||
{
|
||||
return tls_counter;
|
||||
}
|
||||
|
||||
/*
|
||||
* Scenario 1: atexit LIFO. exit() must run the LAST registered handler
|
||||
* first and skip the at_quick_exit list entirely.
|
||||
*/
|
||||
static int
|
||||
lifo_scenario(void)
|
||||
{
|
||||
atexit(hA);
|
||||
atexit(hB);
|
||||
atexit(hC);
|
||||
at_quick_exit(hQ); /* must NOT run: no "Q" on stdout */
|
||||
return 42; /* routed into exit(42) by __libc_start_main */
|
||||
}
|
||||
|
||||
/* Scenario 2: quick_exit runs ONLY at_quick_exit handlers. */
|
||||
static int
|
||||
quick_scenario(void)
|
||||
{
|
||||
atexit(hA); /* must NOT run: no "A" on stdout */
|
||||
at_quick_exit(hQ);
|
||||
quick_exit(5);
|
||||
return 1; /* unreachable */
|
||||
}
|
||||
|
||||
/* Scenario 3: _exit runs NO handlers at all. */
|
||||
static int
|
||||
raw_exit_scenario(void)
|
||||
{
|
||||
atexit(hA);
|
||||
at_quick_exit(hQ);
|
||||
_exit(7);
|
||||
return 1; /* unreachable */
|
||||
}
|
||||
|
||||
/* Scenario 4: argc/argv/environ plumbing from the kernel stack. */
|
||||
static int
|
||||
env_scenario(int argc, char **argv)
|
||||
{
|
||||
if (argc != 3)
|
||||
{
|
||||
say(2, "FAIL: argc != 3 (harness must pass -e plus one argument)\n");
|
||||
return 1;
|
||||
}
|
||||
say(1, "argc=");
|
||||
say_dec(1, (unsigned long)argc);
|
||||
say(1, "\nargv0=");
|
||||
say(1, argv[0]);
|
||||
say(1, "\nargv1=");
|
||||
say(1, argv[1]);
|
||||
say(1, "\nargv2=");
|
||||
say(1, argv[2]);
|
||||
say(1, "\n");
|
||||
check(str_is(argv[1], "-e"), "argv[1] is the -e mode flag");
|
||||
check(str_is(argv[2], "extraarg"), "argv[2] == \"extraarg\"");
|
||||
check(env_matches("VLIBC_TEST_VAR", "hello"), "environ contains VLIBC_TEST_VAR=hello");
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
/* Scenario 5: main-thread TLS image + TCB errno slot. */
|
||||
static int
|
||||
tls_scenario(void)
|
||||
{
|
||||
check(tls_deref(&tls_preinit) == 42, "initialized TLS image copied (.tdata value 42)");
|
||||
check(tls_counter == 0, "TLS BSS tail zeroed (.tbss value 0)");
|
||||
tls_counter = 777;
|
||||
check(tls_readback() == 777, "__thread round trip across a function call");
|
||||
errno = 123;
|
||||
check(errno == 123, "errno round trip through the TCB slot");
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
/* Scenario 6: fixed handler-list limits and the exiting guard. */
|
||||
static int
|
||||
limit_scenario(void)
|
||||
{
|
||||
for (int i = 0; i < 31; i++)
|
||||
{
|
||||
if (atexit(hNoop) != 0)
|
||||
{
|
||||
say(2, "FAIL: atexit slot accounting broke before the list was full\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (atexit(hRegisterDuringExit) != 0)
|
||||
{
|
||||
say(2, "FAIL: 32nd atexit registration rejected (list should just fit)\n");
|
||||
return 1;
|
||||
}
|
||||
check(atexit(hA) == -1, "33rd atexit() returns -1 (fixed list full)");
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
if (argc >= 2)
|
||||
{
|
||||
if (argv[1][0] == '-' && argv[1][1] == 'q')
|
||||
{
|
||||
return quick_scenario();
|
||||
}
|
||||
if (argv[1][0] == '-' && argv[1][1] == 'z')
|
||||
{
|
||||
return raw_exit_scenario();
|
||||
}
|
||||
if (argv[1][0] == '-' && argv[1][1] == 'e')
|
||||
{
|
||||
return env_scenario(argc, argv);
|
||||
}
|
||||
if (argv[1][0] == '-' && argv[1][1] == 't')
|
||||
{
|
||||
return tls_scenario();
|
||||
}
|
||||
if (argv[1][0] == '-' && argv[1][1] == 'x')
|
||||
{
|
||||
return limit_scenario();
|
||||
}
|
||||
}
|
||||
return lifo_scenario();
|
||||
}
|
||||
Reference in New Issue
Block a user