aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--02_process_init/Makefile18
-rw-r--r--02_process_init/README.md294
-rw-r--r--02_process_init/entry.S27
-rw-r--r--02_process_init/entry.c75
-rw-r--r--include/elf.h26
-rw-r--r--include/fmt.h125
-rw-r--r--include/syscall.h56
-rw-r--r--test/Makefile10
-rw-r--r--test/checker.cc68
-rw-r--r--test/test_helper.h73
11 files changed, 774 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index a11eb0a..c44fb23 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
*.so
*.o
main
+entry
+checker
diff --git a/02_process_init/Makefile b/02_process_init/Makefile
new file mode 100644
index 0000000..6e1f512
--- /dev/null
+++ b/02_process_init/Makefile
@@ -0,0 +1,18 @@
+# Copyright (c) 2020 Johannes Stoelp
+
+show: entry
+ #gdb -q --batch -ex 'starti' -ex 'x/g $$rsp' -ex 'x/s *(char**)($$rsp+8)' ./$^
+ ./entry 1 2 3 4
+
+entry: entry.S entry.c
+ gcc -o $@ \
+ -I ../include \
+ -Wall -Wextra \
+ -static \
+ -nostartfiles -nodefaultlibs \
+ -g -O0 \
+ $^
+
+
+clean:
+ rm -f entry
diff --git a/02_process_init/README.md b/02_process_init/README.md
new file mode 100644
index 0000000..5e4e7b3
--- /dev/null
+++ b/02_process_init/README.md
@@ -0,0 +1,294 @@
+# Process Initialization
+
+Before starting to implement a minimal dynamic linker the first step is to
+understand the `process initialization` in further depth.
+Which is important because when starting a new process
+- the dynamic linker must setup the execution environment for the user program
+ (eg load dependencies, pass command line arguments)
+- the control is first passed to the dynamic linker (interpreter) by
+ the Linux Kernel as mentioned in
+ [01_dynamic_linking](../01_dynamic_linking/README.md)
+- the dynamic linker must be a stand-alone executable with no dependencies
+
+Before transferring control to a new user process the Linux Kernel provides some
+data on the `stack` with the format following the specification in the
+[`SystemV x86-64 ABI`][sysv_x86_64] chapter _Initial Stack and Register State_.
+
+## Stack state on process entry
+
+On process startup after `execve(2)` the stack looks as follows
+```text
+ +------------+ High Address
+ | .. |
+ | ENV strs |<-+
+ +->| ARG strs | |
+ | | .. | |
+ | +------------+ |
+ | | .. | |
+ | +------------+ |
+ | | AT_NULL | |
+ | +------------+ |
+ | | AUXV | |
+ | +------------+ |
+ | | 0x0 | |
+ | +------------+ |
+ | | ENVP |--+
+ | +------------+
+ | | 0x0 |
+ | +------------+
+ +--| ARGV |
+ +------------+
+ $rsp ->| ARGC |
+ +------------+ Low Address
+
+
+ | Offset (in bytes) | Type | Description
+-----+-----------------------+------------------------+--------------------
+AUXV | &ENVP + 8*#ENVP + 8 | struct { uint64_t[2] } | Auxiliary Vector
+ 0x0 | &ENVP + 8*#ENVP | | 0 terinator (ENVP)
+ENVP | &ARGV + 8*ARGC + 8 | const char* [] | Environment ptrs
+ 0x0 | &ARGV + 8*ARGC | | 0 terinator (ARGV)
+ARGV | $rsp + 8 | const char* [] | Argument ptrs
+ARGC | $rsp | uint64_t | Argument count
+```
+
+Where `ARGV` is an array of pointers to strings holding the command line
+arguments passed to the user program and `ARGC` the number of arguments passed
++1 as `ARGV[0]` holds the path of the program started. Similar `ENVP` is an
+array of pointers to strings holding the environment variables as seen by this
+process.
+The `AUXV` is the auxiliary vector and holds additional information as for
+example the `entry point` or the `program header` of the program. Entries in
+`AUXV` are encoded as given
+in `AuxvEntry`.
+```c
+struct AuxvEntry {
+ uint64_t tag;
+ uint64_t val;
+};
+```
+The [`x86-64 System V ABI`][sysv_x86_64] chapter _Auxiliary Vector_ specifies
+the following tags
+```text
+AT_NULL = 0
+AT_IGNORE = 1
+AT_EXECFD = 2
+AT_PHDR = 3
+AT_PHENT = 4
+AT_PHNUM = 5
+AT_PAGESZ = 6
+AT_BASE = 7
+AT_FLAGS = 8
+AT_ENTRY = 9
+AT_NOTELF = 10
+AT_UID = 11
+AT_EUID = 12
+AT_GID = 13
+AT_EGID = 14
+```
+Where `AT_NULL` is used to indicate the end of `AUXV`.
+
+## Register state on process entry
+
+Regarding the state of general purpose registers on process entry the
+[`x86-64 SystemV ABI`][sysv_x86_64] states that all registers except the ones listed
+below are in an unspecified state:
+- `$rbp`: content is unspecified, but user code should set it to zero to mark
+ the deepest stack frame
+- `$rsp`: points to the beginning of the data block provided by the Kernel and
+ is guaranteed to be 16-byte aligned at process entry
+- `$rdx`: function pointer that the application should register with
+ `atexit(BA_OS)`.
+> Not sure here if clearing `$rbp` is strictly required as frame-pointer
+> chaining is optional and can be omitted (eg `gcc -fomit-frame-pointer`).
+
+## Hands-on the first instruction
+
+Before exploring and visualizing the data passed by the Linux Kernel on the
+stack there is one more question to answer:
+**How to run the first instruction in a process?**
+
+Typically when building a `C` program the users entry point is the `main`
+function, however this won't contain the first instruction executed after the
+process entry. This can be seen by extracting the `entry point` from the ELF
+header and checking against the symbols in the program. Here the entry point is
+`0x1020` which belongs to the symbol `_start` and not `main`.
+```bash
+readelf -h main | grep Entry
+ Entry point address: 0x1020
+
+nm main | grep '1020\|main'
+ 0000000000001119 T main
+ 0000000000001020 T _start
+```
+
+This is because by default the `static linker` adds some extra code & libraries
+to the program like for example the `libc` and the `C-runtime (crt)` which
+contains the `_start` symbol and hence the first instruction executed.
+
+Passing `--trace` down to the `static linker` it sheds some light onto which
+input files the static linker actually processes.
+```bash
+echo 'void main() {}' | gcc -x c -o /dev/null - -Wl,--trace
+/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../lib/Scrt1.o
+/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../lib/crti.o
+/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/crtbeginS.o
+/tmp/ccjZdjYx.o
+/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/libgcc.a
+/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../lib/libgcc_s.so
+/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../lib/libc.so
+/usr/lib/ld-linux-x86-64.so.2
+/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/crtendS.o
+/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../lib/crtn.o
+```
+> `/tmp/ccjZdjYx.o` is a temporary file created by the compiler containing the
+> code echoed.
+
+The static linker can be explicitly told to not include any default files by
+using the `gcc -nostdlib` argument.
+```bash
+echo 'void _start() {}' | gcc -x c -o /dev/null - -Wl,--trace -nostdlib
+/tmp/ccbfkCoZ.o
+```
+Quoting `man gcc`
+> `-nostdlib` Do not use the standard system startup files or libraries when linking.
+
+## Examining the data from the Kernel
+
+With the capability to control the first instruction executed after process
+entry we finally can visualize the data passed by the Linux Kernel on the stack.
+
+First we provide the symbol `_start` (default entry point) which saves a
+pointer to the Kernel data in `$rdi` and jumps to a function called `entry`.
+The pointer is saved in `$rdi` because that's the register for the first
+argument of class `INTEGER` ([SystemV ABI Function Arugments][sysv_x86_64_fnarg]).
+```asm
+.section .text, "ax", @progbits
+.global _start
+_start:
+ // Clear $rbp.
+ xor rbp, rbp
+
+ // Load ptr to Kernel data.
+ lea rdi, [rsp]
+
+ call entry
+ ...
+```
+The full source code of the `_start` function is available in [entry.S](./entry.S).
+
+The pointer passed to the `entry` function can be used to compute `ARGC`,
+`ARGV` and `ENVP` accordingly.
+```c
+void entry(long* prctx) {
+ long argc = *prctx;
+ const char** argv = (const char**)(prctx + 1);
+ const char** envv = (const char**)(argv + argc + 1);
+ ...
+```
+
+To collect the `AUXV` entries we first need to count the number of environment
+variables as follows.
+```c
+// entry
+ ...
+ int envc = 0;
+ for (const char** env = envv; *env; ++env) {
+ ++envc;
+ }
+
+ uint64_t auxv[AT_MAX_CNT];
+ for (unsigned i = 0; i < AT_MAX_CNT; ++i) {
+ auxv[i] = 0;
+ }
+
+ const uint64_t* auxvp = (const uint64_t*)(envv + envc + 1);
+ for (unsigned i = 0; auxvp[i] != AT_NULL; i += 2) {
+ if (auxvp[i] < AT_MAX_CNT) {
+ auxv[auxvp[i]] = auxvp[i + 1];
+ }
+ }
+ ...
+```
+
+Finally the data can be printed as
+```c
+// entry
+ ...
+ dynld_printf("Got %d arg(s)\n", argc);
+ for (const char** arg = argv; *arg; ++arg) {
+ dynld_printf("\targ = %s\n", *arg);
+ }
+
+ const int max_env = 10;
+ dynld_printf("Print first %d env var(s)\n", max_env - 1);
+ for (const char** env = envv; *env && (env - envv < max_env); ++env) {
+ dynld_printf("\tenv = %s\n", *env);
+ }
+
+ dynld_printf("Print auxiliary vector\n");
+ dynld_printf("\tAT_EXECFD: %ld\n", auxv[AT_EXECFD]);
+ dynld_printf("\tAT_PHDR : %p\n", auxv[AT_PHDR]);
+ dynld_printf("\tAT_PHENT : %ld\n", auxv[AT_PHENT]);
+ dynld_printf("\tAT_PHNUM : %ld\n", auxv[AT_PHNUM]);
+ dynld_printf("\tAT_PAGESZ: %ld\n", auxv[AT_PAGESZ]);
+ dynld_printf("\tAT_BASE : %lx\n", auxv[AT_BASE]);
+ dynld_printf("\tAT_FLAGS : %ld\n", auxv[AT_FLAGS]);
+ dynld_printf("\tAT_ENTRY : %p\n", auxv[AT_ENTRY]);
+ dynld_printf("\tAT_NOTELF: %lx\n", auxv[AT_NOTELF]);
+ dynld_printf("\tAT_UID : %ld\n", auxv[AT_UID]);
+ dynld_printf("\tAT_EUID : %ld\n", auxv[AT_EUID]);
+ dynld_printf("\tAT_GID : %ld\n", auxv[AT_GID]);
+ dynld_printf("\tAT_EGID : %ld\n", auxv[AT_EGID]);
+ ...
+```
+The full source code of the `entry` function is available in [entry.c](./entry.c).
+
+Running the program as `./entry 1 2 3 4` it yields following output:
+```text
+Got 5 arg(s)
+ arg = ./entry
+ arg = 1
+ arg = 2
+ arg = 3
+ arg = 4
+Print first 9 env var(s)
+ env = I3SOCK=/run/user/1000/i3/ipc-socket.1200
+ env = LC_NAME=en_US.UTF-8
+ env = LC_NUMERIC=en_US.UTF-8
+ env = WINDOWID=46221701
+ env = LC_ADDRESS=en_US.UTF-8
+ env = GDM_LANG=en_US.utf8
+ env = PWD=/home/johannst/dev/dynld/02_process_init
+ env = MAIL=/var/spool/mail/johannst
+ env = XDG_SESSION_PATH=/org/freedesktop/DisplayManager/Session env = LANG=en_US.utf8
+Print auxiliary vector
+ AT_EXECFD: 0
+ AT_PHDR : 0x400040
+ AT_PHENT : 56
+ AT_PHNUM : 5
+ AT_PAGESZ: 4096
+ AT_BASE : 0
+ AT_FLAGS : 0
+ AT_ENTRY : 0x401000
+ AT_NOTELF: 0
+ AT_UID : 1000
+ AT_EUID : 1000
+ AT_GID : 1000
+ AT_EGID : 1000
+```
+
+## Things to remember
+- On process entry the Linux Kernel provides data on the stack as specified in
+ the `SystemV ABI`
+- By default the `static linker` adds additional code which contains the
+ `_start` symbol being the default process `entry point`
+
+## References & Source Code
+- [x86-64 SystemV ABI][sysv_x86_64]
+- [x86-64 SystemV ABI - Passing arguments to functions][sysv_x86_64_fnarg]
+- [entry.S](./entry.S)
+- [entry.c](./entry.c)
+
+[sysv_x86_64]: https://www.uclibc.org/docs/psABI-x86_64.pdf
+[sysv_x86_64_fnarg]: https://johannst.github.io/notes/arch/x86_64.html#passing-arguments-to-functions
diff --git a/02_process_init/entry.S b/02_process_init/entry.S
new file mode 100644
index 0000000..50425ba
--- /dev/null
+++ b/02_process_init/entry.S
@@ -0,0 +1,27 @@
+// Copyright (c) 2020 Johannes Stoelp
+
+#include <asm/unistd.h>
+
+.intel_syntax noprefix
+
+.section .text, "ax", @progbits
+.global _start
+_start:
+ // $rsp is guaranteed to be 16-byte aligned.
+
+ // Clear $rbp as specified by the SysV AMD64 ABI.
+ xor rbp, rbp
+
+ // Load pointer to process context prepared by execve(2) syscall as
+ // specified in the SysV AMD64 ABI.
+ // Save pointer in $rdi which is the arg0 (int/ptr) register.
+ lea rdi, [rsp]
+
+ // Stack frames must be 16-byte aligned before control is transfered to the
+ // callees entry point.
+ call entry
+
+ // Call exit(0) syscall.
+ mov rdi, 0
+ mov rax, __NR_exit
+ syscall
diff --git a/02_process_init/entry.c b/02_process_init/entry.c
new file mode 100644
index 0000000..a6b0918
--- /dev/null
+++ b/02_process_init/entry.c
@@ -0,0 +1,75 @@
+// Copyright (c) 2020 Johannes Stoelp
+
+#include <asm/unistd.h>
+#include <elf.h>
+#include <fmt.h>
+#include <stdint.h>
+#include <syscall.h>
+
+#if !defined(__linux__) || !defined(__x86_64__)
+# error "Only supported in linux(x86_64)!"
+#endif
+
+int dynld_printf(const char* fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ char buf[64];
+ int ret = dynld_vsnprintf(buf, sizeof(buf), fmt, ap);
+ va_end(ap);
+ syscall3(__NR_write, 1 /* stdout */, buf, ret);
+ return ret;
+}
+
+void entry(long* prctx) {
+ // Interpret data on the stack passed by the OS kernel as specified in the
+ // x86_64 SysV ABI.
+
+ long argc = *prctx;
+ const char** argv = (const char**)(prctx + 1);
+ const char** envv = (const char**)(argv + argc + 1);
+
+ int envc = 0;
+ for (const char** env = envv; *env; ++env) {
+ ++envc;
+ }
+
+ uint64_t auxv[AT_MAX_CNT];
+ for (unsigned i = 0; i < AT_MAX_CNT; ++i) {
+ auxv[i] = 0;
+ }
+
+ const uint64_t* auxvp = (const uint64_t*)(envv + envc + 1);
+ for (unsigned i = 0; auxvp[i] != AT_NULL; i += 2) {
+ if (auxvp[i] < AT_MAX_CNT) {
+ auxv[auxvp[i]] = auxvp[i + 1];
+ }
+ }
+
+ // Print for demonstration
+
+ dynld_printf("Got %d arg(s)\n", argc);
+ for (const char** arg = argv; *arg; ++arg) {
+ dynld_printf("\targ = %s\n", *arg);
+ }
+
+ const int max_env = 10;
+ dynld_printf("Print first %d env var(s)\n", max_env - 1);
+ for (const char** env = envv; *env && (env - envv < max_env); ++env) {
+ dynld_printf("\tenv = %s\n", *env);
+ }
+
+ dynld_printf("Print auxiliary vector\n");
+ dynld_printf("\tAT_EXECFD: %ld\n", auxv[AT_EXECFD]);
+ dynld_printf("\tAT_PHDR : %p\n", auxv[AT_PHDR]);
+ dynld_printf("\tAT_PHENT : %ld\n", auxv[AT_PHENT]);
+ dynld_printf("\tAT_PHNUM : %ld\n", auxv[AT_PHNUM]);
+ dynld_printf("\tAT_PAGESZ: %ld\n", auxv[AT_PAGESZ]);
+ dynld_printf("\tAT_BASE : %lx\n", auxv[AT_BASE]);
+ dynld_printf("\tAT_FLAGS : %ld\n", auxv[AT_FLAGS]);
+ dynld_printf("\tAT_ENTRY : %p\n", auxv[AT_ENTRY]);
+ dynld_printf("\tAT_NOTELF: %lx\n", auxv[AT_NOTELF]);
+ dynld_printf("\tAT_UID : %ld\n", auxv[AT_UID]);
+ dynld_printf("\tAT_EUID : %ld\n", auxv[AT_EUID]);
+ dynld_printf("\tAT_GID : %ld\n", auxv[AT_GID]);
+ dynld_printf("\tAT_EGID : %ld\n", auxv[AT_EGID]);
+}
diff --git a/include/elf.h b/include/elf.h
new file mode 100644
index 0000000..7e279fe
--- /dev/null
+++ b/include/elf.h
@@ -0,0 +1,26 @@
+// Copyright (c) 2020 Johannes Stoelp
+
+#pragma once
+
+#include <bits/stdint-uintn.h>
+#include <stdint.h>
+
+enum eAuxvTag {
+ AT_NULL = 0, /* ignored */
+ AT_IGNORE = 1, /* ignored */
+ AT_EXECFD = 2, /* val */
+ AT_PHDR = 3, /* ptr */
+ AT_PHENT = 4, /* val */
+ AT_PHNUM = 5, /* val */
+ AT_PAGESZ = 6, /* val */
+ AT_BASE = 7, /* ptr */
+ AT_FLAGS = 8, /* val */
+ AT_ENTRY = 9, /* ptr */
+ AT_NOTELF = 10, /* val */
+ AT_UID = 11, /* val */
+ AT_EUID = 12, /* val */
+ AT_GID = 13, /* val */
+ AT_EGID = 14, /* val */
+
+ AT_MAX_CNT,
+};
diff --git a/include/fmt.h b/include/fmt.h
new file mode 100644
index 0000000..c74ac4c
--- /dev/null
+++ b/include/fmt.h
@@ -0,0 +1,125 @@
+// Copyright (c) 2020 Johannes Stoelp
+
+#pragma once
+
+#include <stdarg.h>
+
+#define ALLOW_UNUSED __attribute__((unused))
+
+ALLOW_UNUSED
+static const char* num2dec(char* buf, unsigned long len, unsigned long long num) {
+ char* pbuf = buf + len - 1;
+ *pbuf = '\0';
+
+ if (num == 0) {
+ *(--pbuf) = '0';
+ }
+
+ while (num > 0 && pbuf != buf) {
+ char d = (num % 10) + '0';
+ *(--pbuf) = d;
+ num /= 10;
+ }
+ return pbuf;
+}
+
+ALLOW_UNUSED
+static const char* num2hex(char* buf, unsigned long len, unsigned long long num) {
+ char* pbuf = buf + len - 1;
+ *pbuf = '\0';
+
+ if (num == 0) {
+ *(--pbuf) = '0';
+ }
+
+ while (num > 0 && pbuf != buf) {
+ char d = (num & 0xf);
+ *(--pbuf) = d + (d > 9 ? 'a' - 10 : '0');
+ num >>= 4;
+ }
+ return pbuf;
+}
+
+ALLOW_UNUSED
+static int dynld_vsnprintf(char* buf, unsigned long len, const char* fmt, va_list ap) {
+ unsigned i = 0;
+
+#define put(c) \
+ { \
+ char _c = (c); \
+ if (i < len) { \
+ buf[i] = _c; \
+ } \
+ ++i; \
+ }
+
+#define puts(s) \
+ while (*s) { \
+ put(*s++); \
+ }
+
+ char scratch[16];
+ int l_cnt = 0;
+
+ while (*fmt) {
+ if (*fmt != '%') {
+ put(*fmt++);
+ continue;
+ }
+
+ l_cnt = 0;
+
+ continue_fmt:
+ switch (*(++fmt /* constume '%' */)) {
+ case 'l':
+ ++l_cnt;
+ goto continue_fmt;
+ case 'd': {
+ long val = l_cnt > 0 ? va_arg(ap, long) : va_arg(ap, int);
+ if (val < 0) {
+ val *= -1;
+ put('-');
+ }
+ const char* ptr = num2dec(scratch, sizeof(scratch), val);
+ puts(ptr);
+ } break;
+ case 'x': {
+ unsigned long val = l_cnt > 0 ? va_arg(ap, unsigned long) : va_arg(ap, unsigned);
+ const char* ptr = num2hex(scratch, sizeof(scratch), val);
+ puts(ptr);
+ } break;
+ case 's': {
+ const char* ptr = va_arg(ap, const char*);
+ puts(ptr);
+ } break;
+ case 'p': {
+ const void* val = va_arg(ap, const void*);
+ const char* ptr = num2hex(scratch, sizeof(scratch), (unsigned long long)val);
+ put('0');
+ put('x');
+ puts(ptr);
+ } break;
+ default:
+ put(*fmt);
+ break;
+ }
+ ++fmt;
+ }
+
+#undef puts
+#undef put
+
+ if (buf) {
+ i < len ? (buf[i] = '\0') : (buf[len - 1] = '\0');
+ }
+ return i;
+}
+
+ALLOW_UNUSED
+static int dynld_snprintf(char* buf, unsigned long len, const char* fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ int ret = dynld_vsnprintf(buf, len, fmt, ap);
+ va_end(ap);
+ return ret;
+}
diff --git a/include/syscall.h b/include/syscall.h
new file mode 100644
index 0000000..0460ede
--- /dev/null
+++ b/include/syscall.h
@@ -0,0 +1,56 @@
+// Copyright (c) 2020 Johannes Stoelp
+
+#pragma once
+
+#if !defined(__linux__) || !defined(__x86_64__)
+# error "Only supported on linux(x86_64)!"
+#endif
+
+// Inline ASM
+// Syntax:
+// asm asm-qualifiers (AssemblerTemplate : OutputOperands : InputOperands : Clobbers)
+//
+// Output operand constraints:
+// = | operand (variable) is written to by this instruction
+// + | operand (variable) is written to / read from by this instruction
+//
+// Input/Output operand constraints:
+// r | allocate general purpose register
+//
+// Machine specific constraints (x86_64):
+// a | a register (eg rax)
+// d | d register (eg rdx)
+// D | di register (eg rdi)
+// S | si register (eg rsi)
+//
+// Local register variables:
+// In case a specific register is required which can not be specified via a
+// machine specific constraint.
+// ```c
+// register long r12 asm ("r12") = 42;
+// asm("nop" : : "r"(r10));
+// ```
+//
+// Reference:
+// https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
+// https://gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html#Machine-Constraints
+// https://gcc.gnu.org/onlinedocs/gcc/Local-Register-Variables.html
+
+// Linux syscall ABI
+// x86-64
+// #syscall: rax
+// ret : rax
+// instr : syscall
+// args : rdi rsi rdx r10 r8 r9
+//
+// Reference:
+// syscall(2)
+
+#define argcast(A) ((long)(A))
+#define syscall3(n,a1,a2,a3) _syscall3(n, argcast(a1), argcast(a2), argcast(a3))
+
+static inline long _syscall3(long n, long a1, long a2, long a3) {
+ long ret;
+ asm volatile("syscall" : "=a"(ret) : "a"(n), "D"(a1), "S"(a2), "d"(a3) : "memory");
+ return ret;
+}
diff --git a/test/Makefile b/test/Makefile
new file mode 100644
index 0000000..0efa3fc
--- /dev/null
+++ b/test/Makefile
@@ -0,0 +1,10 @@
+# Copyright (c) 2020 Johannes Stoelp
+
+check: build
+ ./checker
+
+build: checker.cc
+ g++ -o cHecker -g -O2 -I ../include -Wall -Wextra $^
+
+clean:
+ rm -f checker
diff --git a/test/checker.cc b/test/checker.cc
new file mode 100644
index 0000000..2b5bc8a
--- /dev/null
+++ b/test/checker.cc
@@ -0,0 +1,68 @@
+// Copyright (c) 2020 Johannes Stoelp
+
+#include "test_helper.h"
+
+#include <cstdio>
+#include <fmt.h>
+
+void check_dec() {
+ char have[16];
+ int len = dynld_snprintf(have, sizeof(have), "%d %d", 12345, -54321);
+
+ ASSERT_EQ("12345 -54321", have);
+ ASSERT_EQ(12, len);
+ ASSERT_EQ('\0', have[len]);
+}
+
+void check_hex() {
+ char have[16];
+ int len = dynld_snprintf(have, sizeof(have), "%x %x", 0xdeadbeef, 0xcafe);
+
+ ASSERT_EQ("deadbeef cafe", have);
+ ASSERT_EQ(13, len);
+ ASSERT_EQ('\0', have[len]);
+}
+
+void check_ptr() {
+ char have[16];
+ int len = dynld_snprintf(have, sizeof(have), "%p %p", (void*)0xabcd, (void*)0x0);
+
+ ASSERT_EQ("0xabcd 0x0", have);
+ ASSERT_EQ(10, len);
+ ASSERT_EQ('\0', have[len]);
+}
+
+void check_null() {
+ int len = dynld_snprintf(0, 0, "%s", "abcd1234efgh5678");
+
+ ASSERT_EQ(16, len);
+}
+
+void check_exact_len() {
+ char have[8];
+ int len = dynld_snprintf(have, sizeof(have), "%s", "12345678");
+
+ ASSERT_EQ("1234567", have);
+ ASSERT_EQ(8, len);
+ ASSERT_EQ('\0', have[7]);
+}
+
+void check_exceed_len() {
+ char have[8];
+ int len = dynld_snprintf(have, sizeof(have), "%s", "123456789abcedf");
+
+ ASSERT_EQ("1234567", have);
+ ASSERT_EQ(15, len);
+ ASSERT_EQ('\0', have[7]);
+}
+
+int main() {
+ TEST_INIT;
+ TEST(check_dec);
+ TEST(check_hex);
+ TEST(check_ptr);
+ TEST(check_null);
+ TEST(check_exact_len);
+ TEST(check_exceed_len);
+ return TEST_FAIL_CNT;
+}
diff --git a/test/test_helper.h b/test/test_helper.h
new file mode 100644
index 0000000..43cfce9
--- /dev/null
+++ b/test/test_helper.h
@@ -0,0 +1,73 @@
+// Copyright (c) 2020 Johannes Stoelp
+
+#pragma once
+#include <cstring>
+#include <exception>
+#include <iostream>
+
+/* Extremely trivial helper, just to get some tests out. */
+
+struct TestFailed : std::exception {};
+
+/* Requirements
+ * T1: comparable + printable (stream operator)
+ * T2: comparable + printable (stream operator)
+ */
+
+template<typename T1, typename T2>
+void ASSERT_EQ(T1 expected, T2 have) {
+ if (expected != have) {
+ std::cerr << "ASSERT_EQ failed:\n"
+ << " expected: " << expected << "\n"
+ << " have : " << have << "\n"
+ << std::flush;
+ throw TestFailed{};
+ }
+}
+
+template<typename T1, typename T2>
+void ASSERT_EQ(T1* expected, T2* have) {
+ ASSERT_EQ(*expected, *have);
+}
+
+template<>
+void ASSERT_EQ(const char* expected, const char* have) {
+ if (std::strcmp(expected, have) != 0) {
+ std::cerr << "ASSERT_EQ failed:\n"
+ << " expected: " << expected << "\n"
+ << " have : " << have << "\n"
+ << std::flush;
+ throw TestFailed{};
+ }
+}
+
+template<>
+void ASSERT_EQ(const char* expected, char* have) {
+ ASSERT_EQ(expected, static_cast<const char*>(have));
+}
+
+template<>
+void ASSERT_EQ(char* expected, const char* have) {
+ ASSERT_EQ(static_cast<const char*>(expected), have);
+}
+
+void ASSERT_EQ(char* expected, char* have) {
+ ASSERT_EQ(static_cast<const char*>(expected), static_cast<const char*>(have));
+}
+
+#define TEST_INIT unsigned fail_cnt = 0;
+#define TEST_FAIL_CNT fail_cnt
+
+#define TEST(fn) \
+ { \
+ try { \
+ fn(); \
+ std::cerr << "SUCCESS " #fn << std::endl; \
+ } catch (TestFailed&) { \
+ ++fail_cnt; \
+ std::cerr << "FAIL " #fn << std::endl; \
+ } catch (...) { \
+ ++fail_cnt; \
+ std::cerr << "FAIL " #fn << "(caught unspecified exception)" << std::endl; \
+ } \
+ }