diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | 02_process_init/Makefile | 18 | ||||
-rw-r--r-- | 02_process_init/README.md | 294 | ||||
-rw-r--r-- | 02_process_init/entry.S | 27 | ||||
-rw-r--r-- | 02_process_init/entry.c | 75 | ||||
-rw-r--r-- | include/elf.h | 26 | ||||
-rw-r--r-- | include/fmt.h | 125 | ||||
-rw-r--r-- | include/syscall.h | 56 | ||||
-rw-r--r-- | test/Makefile | 10 | ||||
-rw-r--r-- | test/checker.cc | 68 | ||||
-rw-r--r-- | test/test_helper.h | 73 |
11 files changed, 774 insertions, 0 deletions
@@ -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; \ + } \ + } |