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 <[email protected]>
This commit is contained in:
2026-08-30 04:09:45 -04:00
co-authored by Sisyphus
parent aca40a69b0
commit b9f8d9bf19
8 changed files with 440 additions and 0 deletions
+40
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
/*
* errno.c — the errno global.
*
* Single-threaded for now; see include/errno.h for the threading note.
*/
#include <errno.h>
int errno = 0;
+18
View File
@@ -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 <sys/syscall.h>
/*
* 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 */
+85
View File
@@ -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 <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#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;
}
+85
View File
@@ -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 <errno.h>
#include <stdlib.h>
#include <unistd.h>
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;
}
+105
View File
@@ -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 <string.h>
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;
}
+51
View File
@@ -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 <errno.h>
#include <stdarg.h>
#include <sys/syscall.h>
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;
}
+47
View File
@@ -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 <errno.h>
#include <sys/syscall.h>
#include <unistd.h>
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;
}