Configuring Build Automation and Core Standard Library Support

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:

  1. The target run depends on intermediate compilation targets (e.g., all).
  2. all resolves dependencies on specific source files and image binaries.
  3. Eventually, the abstract-machine/scripts/platform/nemu.mk script 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.

Tags: NEMU RISC-V Abstract-Machine C-Library systems-programming

Posted on Mon, 13 Jul 2026 17:28:02 +0000 by Pellefant