Automating Execution Environments
In systems programming projects involving emulators like NEMU (Non-Architecture-based Machine Emulator), developers often face repetitive manual input when launching test cases. While interactive debugging tools (such as sdb) require user intervention, automated regression testing should not depend on manual keystrokes. The Abstract Machine (AM) framework provides a batch execution mode to streamline this process.
To enable batch mode execution via the build system, the Makefile configuration must be adjusted. When initiating a test suite under am-kernels/tests/cpu-tests, the typical command structure is:
make ARCH=$ISA-nemu ALL=dummy run
This command triggers a dependency chain. Initially, Makefile.dummy is generated dynamically, effectively defining target variables such as NAME and SRCS. This configuration is included in the top-level Abstract Machine Makefile. The dependency graph resolves as follows:
- The target
rundepends on intermediate compilation targets (e.g.,all). allresolves dependencies on specific source files and image binaries.- Eventually, the
abstract-machine/scripts/platform/nemu.mkscript is invoked.
The critical step occurs within the NEMU execution definition. By default, the Makefile executes the binary. To suppress interactive prompts, the binary invocation arguments must include the batch mode flag (typically -b). The relevant section in the platform script invokes the emulator within its own directory:
run: image
$(MAKE) -C $(NEMU_HOME) ISA=$(ISA) run ARGS="$(NEMUFLAGS)" IMG=$(IMAGE).bin
The underlying Makefile logic in the emulator passes these arguments to the executable. Specifically, ensuring the NEMU_EXEC variable includes the appropriate flags ensures the program loads the image and runs immediately without waiting for a 'continue' command. This setup allows the build system to verify functionality fully automatically.
Implementing Standard Library Primitives
Before complex applications can run on the emulated environment, fundamental C standard library functions must be implemented within the kernel library (klib). These implementations reside typically at abstract-machine/klib/src/.
Memory and String Manipulation
The following implementations replace the standard headers with custom variants optimized for the target architecture. Pointers are managed carefully to handle potential memory overlaps and type safety.
#include <klib.h>
#include <stdint.h>
#if !defined(__ISA_NATIVE__) || defined(__NATIVE_USE_KLIB__)
size_t str_len(const char *s) {
const char *cursor = s;
while (*cursor != 0) {
cursor++;
}
return cursor - s;
}
char *copy_str(char *dest, const char *source) {
char *end_dest = dest;
while ((*end_dest++ = *source++) != 0);
return dest;
}
char *safe_copy_str(char *dest, const char *source, size_t count) {
char *write_ptr = dest;
// Copy up to `count` characters or until null terminator
while (count > 0 && *source) {
*write_ptr++ = *source++;
count--;
}
// Null-terminate if space remains
while (count > 0) {
*write_ptr++ = 0;
count--;
}
return dest;
}
int compare_strings(const char *lhs, const char *rhs) {
while (*lhs && (*lhs == *rhs)) {
lhs++;
rhs++;
}
return *(unsigned char *)lhs - *(unsigned char *)rhs;
}
void *set_memory(void *target, int value, size_t bytes) {
unsigned char *byte_cursor = (unsigned char *)target;
unsigned char fill_val = (unsigned char)value;
for (size_t i = 0; i < bytes; i++) {
byte_cursor[i] = fill_val;
}
return target;
}
void *move_memory(void *target, const void *source, size_t bytes) {
unsigned char *out = (unsigned char *)target;
const unsigned char *in = (const unsigned char *)source;
// Detect overlapping regions
// If destination is before source OR ranges do not overlap, forward copy
if (out < in || out >= in + bytes) {
for (size_t i = 0; i < bytes; i++) {
out[i] = in[i];
}
} else {
// Overlap detected: iterate backwards to prevent corruption
for (size_t i = bytes; i > 0; i--) {
out[i-1] = in[i-1];
}
}
return target;
}
#endif
Key considerations for these functions include handling the memmove logic correctly when source and destination pointers overlap, which requires direction-dependent copying loops. Additionally, string comparisons treat data as unsigned char to insure consistent behavior across signedness variations.
Formatted Output Generation
Supporting formatted strings allows for flexible logging and debugging. The implementation focuses on parsing format specifiers manually to minimize dependency size. The logic handles integers, hexadecimal values, and character sequences.
#include <stdarg.h>
#include <klib-macros.h>
// Helper: Convert signed integer to string representation
static int convert_int(int value, char *buffer, int base) {
char *ptr = buffer;
char sign_marker = '\0';
if (base == 10 && value < 0) {
sign_marker = '-';
value = -value;
}
// Reverse order generation
char tmp[32];
int idx = 0;
unsigned int temp = (unsigned int)value;
if (temp == 0) tmp[idx++] = '0';
while (temp) {
tmp[idx++] = "0123456789abcdef"[temp % base];
temp /= base;
}
// Write to output with optional sign
if (sign_marker) *ptr++ = sign_marker;
while (idx > 0) *ptr++ = tmp[--idx];
*ptr = 0;
return ptr - buffer;
}
int format_print(char *output, const char *format, va_list args) {
char *original_pos = output;
char num_buffer[64];
while (*format) {
if (*format == '%') {
format++;
switch (*format) {
case 'd':
case 'i':
convert_int(va_arg(args, int), num_buffer, 10);
break;
case 'x':
case 'X':
convert_int(va_arg(args, unsigned int), num_buffer, 16);
if (*format == 'X') {
char *p = num_buffer;
while (*p) {
if (*p >= 'a' && *p <= 'f') *p -= 32;
p++;
}
}
break;
case 's':
{
char *str = va_arg(args, char *);
if (!str) str = "(null)";
while (*str) *output++ = *str++;
break;
}
case 'c':
*output++ = (char)va_arg(args, int);
break;
case '%':
*output++ = '%';
break;
default:
*output++ = '%';
*output++ = *format;
break;
}
// Append converted numbers
if (*format == 'd' || *format == 'i' || *format == 'x' || *format == 'X') {
char *num_p = num_buffer;
while(*num_p) *output++ = *num_p++;
}
} else {
*output++ = *format;
}
format++;
}
*output = '\0';
return output - original_pos;
}
int print_snprintf(char *buf, size_t n, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
int len = format_print(buf, fmt, ap);
va_end(ap);
return len;
}
The print_snprintf variant adds bounds checking to ensure the buffer is not exceeded. The helper function iterates through the format string, dispatching to specific conversion routines based on the specifier encountered. Integer bases other than 10 are handled by converting values into hex representations and adjusting character casing for uppercase options. This approach minimizes external dependencies while providing sufficient functionality for testing kernels.