From b9f8d9bf199496c47d5cc357638fb603f00e4c0e Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Sun, 30 Aug 2026 04:07:58 -0400 Subject: [PATCH] Add libc implementation stubs Real string core and process teardown; thin read/write/close/getpid/_exit syscall wrappers; the rest follows the stub convention (documented error return, errno = ENOSYS, TODO). syscall.c is the only kernel ABI surface; crt0.S is the static entry point, kept out of libc.a. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/crt/crt0.S | 40 +++++++++++++++++++ src/errno.c | 9 +++++ src/internal.h | 18 +++++++++ src/stdio.c | 85 +++++++++++++++++++++++++++++++++++++++ src/stdlib.c | 85 +++++++++++++++++++++++++++++++++++++++ src/string.c | 105 +++++++++++++++++++++++++++++++++++++++++++++++++ src/syscall.c | 51 ++++++++++++++++++++++++ src/unistd.c | 47 ++++++++++++++++++++++ 8 files changed, 440 insertions(+) create mode 100644 src/crt/crt0.S create mode 100644 src/errno.c create mode 100644 src/internal.h create mode 100644 src/stdio.c create mode 100644 src/stdlib.c create mode 100644 src/string.c create mode 100644 src/syscall.c create mode 100644 src/unistd.c diff --git a/src/crt/crt0.S b/src/crt/crt0.S new file mode 100644 index 0000000..bf44155 --- /dev/null +++ b/src/crt/crt0.S @@ -0,0 +1,40 @@ +/* + * crt0.S — process entry point for statically linked nulsl-libc binaries. + * + * The kernel hands control to _start with the stack already set up: + * + * rsp -> argc + * rsp + 8 -> argv[0], argv[1], ..., argv[argc-1], NULL + * after NULL -> envp[0], ... (we compute it, the libc does not use + * it yet, but a correct entry point is cheap) + * + * We set up a valid frame, call main(argc, argv), and hand the exit code + * to exit(). No dynamic linker is involved anywhere: this object + libc.a + * is the entire runtime (see docs/architecture.md). + * + * This file is assembled with `gcc -c`, so it passes through the C + * preprocessor and can carry the same arch guards as the C sources. + */ + +#if defined(__x86_64__) + + .section .text + .globl _start + .type _start, @function +_start: + xor %ebp, %ebp /* outermost frame: ebp = 0 */ + mov (%rsp), %edi /* argc */ + lea 8(%rsp), %rsi /* argv */ + lea 16(%rsp,%rdi,8), %rdx /* envp = &argv[argc + 1] */ + and $-16, %rsp /* ABI: 16-byte stack alignment */ + call main + mov %eax, %edi /* exit(status) */ + call exit +1: jmp 1b /* exit() is noreturn; never reached */ + .size _start, .-_start + + .section .note.GNU-stack,"",@progbits + +#else +#error "nulsl-libc: no crt0 for this architecture yet — see src/arch/" +#endif diff --git a/src/errno.c b/src/errno.c new file mode 100644 index 0000000..e89eab7 --- /dev/null +++ b/src/errno.c @@ -0,0 +1,9 @@ +/* + * errno.c — the errno global. + * + * Single-threaded for now; see include/errno.h for the threading note. + */ + +#include + +int errno = 0; diff --git a/src/internal.h b/src/internal.h new file mode 100644 index 0000000..0e3460f --- /dev/null +++ b/src/internal.h @@ -0,0 +1,18 @@ +/* + * internal.h — private declarations shared between nulsl-libc translation + * units. NOT installed; src/ only. + */ + +#ifndef _NULSL_INTERNAL_H +#define _NULSL_INTERNAL_H + +#include + +/* + * The FILE type: a file descriptor until a real buffering layer lands. + * Defined here (not in the public stdio.h) so the layout stays private. + */ +struct nulsl_file { + int fd; +}; +#endif /* _NULSL_INTERNAL_H */ diff --git a/src/stdio.c b/src/stdio.c new file mode 100644 index 0000000..5571bd7 --- /dev/null +++ b/src/stdio.c @@ -0,0 +1,85 @@ +/* + * stdio.c — standard I/O. + * + * What is real today: unbuffered character/string output to a descriptor. + * What is stubbed: formatting (printf), and anything that would need a + * buffer or the open() path. All stubs follow the same convention — + * return the documented error value and set errno = ENOSYS. + */ + +#include +#include +#include +#include + +#include "internal.h" + +FILE *stdin = &(struct nulsl_file){0}; +FILE *stdout = &(struct nulsl_file){1}; +FILE *stderr = &(struct nulsl_file){2}; + +int puts(const char *s) +{ + if (write(STDOUT_FILENO, s, strlen(s)) < 0) + return EOF; + if (write(STDOUT_FILENO, "\n", 1) < 0) + return EOF; + return 0; +} + +int putchar(int c) +{ + unsigned char b = (unsigned char)c; + return write(STDOUT_FILENO, &b, 1) == 1 ? c : EOF; +} + +/* Stub: formatting engine is on the roadmap. */ +int printf(const char *fmt, ...) +{ + (void)fmt; + errno = ENOSYS; + return -1; +} + +/* Stub: needs open() and a buffering layer. */ +FILE *fopen(const char *path, const char *mode) +{ + (void)path; + (void)mode; + errno = ENOSYS; + return NULL; +} + +int fclose(FILE *f) +{ + (void)f; + errno = ENOSYS; + return EOF; +} + +size_t fread(void *ptr, size_t size, size_t nmemb, FILE *f) +{ + (void)ptr; + (void)size; + (void)nmemb; + (void)f; + errno = ENOSYS; + return 0; +} + +size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *f) +{ + (void)ptr; + (void)size; + (void)nmemb; + (void)f; + errno = ENOSYS; + return 0; +} + +/* Trivially correct: nothing buffers yet, so there is nothing to flush. */ +int fflush(FILE *f) +{ + (void)f; + return 0; +} diff --git a/src/stdlib.c b/src/stdlib.c new file mode 100644 index 0000000..0fb4e73 --- /dev/null +++ b/src/stdlib.c @@ -0,0 +1,85 @@ +/* + * stdlib.c — general utilities. + * + * Real: process teardown (exit/_exit/abort) and atoi. + * Stubs: the allocator (malloc & friends wait for a brk/mmap allocator) + * and strtol (waits for proper errno/base handling). + */ + +#include +#include +#include + +void exit(int status) +{ + /* TODO: run atexit() handlers and flush stdio once buffering exists. */ + _exit(status); + for (;;) + ; /* unreachable */ +} + +void abort(void) +{ + /* TODO: raise SIGABRT once signals exist; 128+6 mirrors the shell + * convention for "killed by signal 6" without any signal machinery. */ + _exit(134); + for (;;) + ; /* unreachable */ +} + +int atoi(const char *s) +{ + int sign = 1; + int v = 0; + + while (*s == ' ' || (*s >= '\t' && *s <= '\r')) + s++; + if (*s == '-' || *s == '+') { + if (*s == '-') + sign = -1; + s++; + } + while (*s >= '0' && *s <= '9') + v = v * 10 + (*s++ - '0'); + return sign * v; +} + +/* Stub: brk()/mmap() allocator is on the roadmap. */ +void *malloc(size_t size) +{ + (void)size; + errno = ENOSYS; + return NULL; +} + +void *calloc(size_t nmemb, size_t size) +{ + (void)nmemb; + (void)size; + errno = ENOSYS; + return NULL; +} + +void *realloc(void *ptr, size_t size) +{ + (void)ptr; + (void)size; + errno = ENOSYS; + return NULL; +} + +void free(void *ptr) +{ + (void)ptr; + /* Nothing to do until the allocator exists. */ +} + +/* Stub: needs errno/base handling (ERANGE, EINVAL, 0x/0o prefixes). */ +long strtol(const char *s, char **endptr, int base) +{ + (void)s; + (void)endptr; + (void)base; + errno = ENOSYS; + return 0; +} diff --git a/src/string.c b/src/string.c new file mode 100644 index 0000000..50cc931 --- /dev/null +++ b/src/string.c @@ -0,0 +1,105 @@ +/* + * string.c — real, minimal implementations of the memory/string core. + * + * Byte-at-a-time on purpose: for the sizes Null Linux deals with, and for + * the goal of keeping the code obviously correct, word-at-a-time tricks + * are not worth the branch soup. If profiling ever says otherwise, the + * benchmark suite will say so (see benchmarks/). + */ + +#include + +size_t strlen(const char *s) +{ + const char *p = s; + while (*p) + p++; + return (size_t)(p - s); +} + +int strcmp(const char *a, const char *b) +{ + while (*a && *a == *b) { + a++; + b++; + } + return (unsigned char)*a - (unsigned char)*b; +} + +int strncmp(const char *a, const char *b, size_t n) +{ + for (; n && *a && *a == *b; n--, a++, b++) + ; + if (n == 0) + return 0; + return (unsigned char)*a - (unsigned char)*b; +} + +char *strcpy(char *dst, const char *src) +{ + char *d = dst; + while ((*d++ = *src++)) + ; + return dst; +} + +char *strncpy(char *dst, const char *src, size_t n) +{ + char *d = dst; + while (n && *src) { + *d++ = *src++; + n--; + } + while (n--) + *d++ = '\0'; + return dst; +} + +void *memcpy(void *restrict dst, const void *restrict src, size_t n) +{ + unsigned char *d = dst; + const unsigned char *s = src; + while (n--) + *d++ = *s++; + return dst; +} + +void *memmove(void *dst, const void *src, size_t n) +{ + unsigned char *d = dst; + const unsigned char *s = src; + + if ((size_t)(d - s) >= n) { + /* No overlap (or exact): copy forward. */ + while (n--) + *d++ = *s++; + } else { + /* Overlap: copy backward. */ + d += n; + s += n; + while (n--) + *--d = *--s; + } + return dst; +} + +int memcmp(const void *a, const void *b, size_t n) +{ + const unsigned char *x = a; + const unsigned char *y = b; + while (n--) { + if (*x != *y) + return *x - *y; + x++; + y++; + } + return 0; +} + +void *memset(void *dst, int c, size_t n) +{ + unsigned char *d = dst; + while (n--) + *d++ = (unsigned char)c; + return dst; +} diff --git a/src/syscall.c b/src/syscall.c new file mode 100644 index 0000000..32e2b3d --- /dev/null +++ b/src/syscall.c @@ -0,0 +1,51 @@ +/* + * syscall.c — the raw syscall entry point. + * + * This is the ONLY file in nulsl-libc that executes the `syscall` + * instruction. Every other function that reaches the kernel goes + * through here (see docs/syscalls.md). + * + * ABI: on error the kernel returns -errno in rax. We follow the glibc + * convention for syscall(): return -1 and set errno. (The -4095 bound is + * the documented Linux range for negative errno values.) + */ + +#include +#include +#include + +long syscall(long number, ...) +{ + va_list ap; + long a1, a2, a3, a4, a5, a6; + long ret; + + va_start(ap, number); + a1 = va_arg(ap, long); + a2 = va_arg(ap, long); + a3 = va_arg(ap, long); + a4 = va_arg(ap, long); + a5 = va_arg(ap, long); + a6 = va_arg(ap, long); + va_end(ap); + +#if defined(__x86_64__) + /* System V AMD64 ABI: number in rax, args in rdi rsi rdx r10 r8 r9. */ + register long r10 __asm__("r10") = a4; + register long r8 __asm__("r8") = a5; + register long r9 __asm__("r9") = a6; + __asm__ volatile("syscall" + : "=a"(ret) + : "a"(number), "D"(a1), "S"(a2), "d"(a3), "r"(r10), + "r"(r8), "r"(r9) + : "rcx", "r11", "memory"); +#else +#error "nulsl-libc: no syscall ABI for this architecture yet — see src/arch/" +#endif + + if (ret < 0 && ret >= -4095) { + errno = (int)-ret; + return -1; + } + return ret; +} diff --git a/src/unistd.c b/src/unistd.c new file mode 100644 index 0000000..1a0b57c --- /dev/null +++ b/src/unistd.c @@ -0,0 +1,47 @@ +/* + * unistd.c — thin wrappers around raw syscalls. + * + * Each function here is a one-liner on purpose: the kernel is the API + * (project guideline #6), and a wrapper that does more than translate + * arguments is a wrapper that can lie. Error translation (kernel -errno + * -> errno) happens inside syscall() itself. + */ + +#include +#include +#include + +ssize_t read(int fd, void *buf, size_t count) +{ + return (ssize_t)syscall(SYS_read, fd, buf, count); +} + +ssize_t write(int fd, const void *buf, size_t count) +{ + return (ssize_t)syscall(SYS_write, fd, buf, count); +} + +int close(int fd) +{ + return (int)syscall(SYS_close, fd); +} + +pid_t getpid(void) +{ + return (pid_t)syscall(SYS_getpid); +} + +void _exit(int status) +{ + syscall(SYS_exit, status); + for (;;) + ; /* unreachable */ +} + +/* Stub: trivially a one-liner once wanted (SYS_unlink = 87 on x86_64). */ +int unlink(const char *path) +{ + (void)path; + errno = ENOSYS; + return -1; +}