1

I was working a Lisp interpreter with a 'threaded' design, where it compiles to a VM where each instruction is a C function that calls the next instruction — if the compiler does tail call elimination, then it should only need a jmp between instructions.

It's a stack-oriented VM, with an Imm instructions that pushes an immediate to the stack.

When I manually call the first Imm instruction, it runs fine. However, once the Imm instruction jumps to the next Imm instruction, it seems the call stack becomes corrupted as none of the parameter values are valid anymore. However, this only occurs when I use clang with __attribute__((musttail)). In fact, gcc -O3 and even clang -O3 compile it to a jmp just fine (but clang without musttail compiles to a call). Anyone know what could be going on here?

A stripped-down version of the code that exhibits this behavior follows:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

typedef enum { TypeInt = 0 } Tag;

typedef struct {
    int n; /* item count */
    int64_t *val; /* pointer */
} List;

typedef struct Block {
    struct Ins *ins;
    int n;
    struct Fn *fn;
    struct Scope *locals;
    struct Scope *mod;
} Block;

typedef struct Scope {
    int n;
    const char **names;
    int64_t *vals;
    Block first;
} Scope;

typedef struct Ins {
    char (*op)(int, Block);
    int64_t imm;
} Ins;

typedef struct Fn {
    Scope locals;
    Scope *mod;
} Fn;


int64_t *stack;
uint32_t sc; /* stack counter */
uint32_t fc; /* frame counter */
uint32_t cap = 128 * sizeof(int64_t);

#define F_INS(x) char x(int ic, Block b)
#define MUSTTAIL __attribute__((musttail))
#define INSRET MUSTTAIL return b.ins[ic + 1].op(ic + 1, b);

#define N_BUILTINS 1
const char *builtins[N_BUILTINS] = { "+" };

void vm_init(void) { stack = malloc(cap); }
F_INS(Done) { return 0; }

void stack_push(int64_t v)
{
    if (++sc >= cap) stack = realloc(stack, cap *= 2);
    stack[sc - 1] = v;
}

F_INS(Imm)
{
    printf("IC: %u\n", ic);
    stack_push(b.ins[ic].imm);
    INSRET;
}

F_INS(Add)
{
    int64_t y = stack[--sc];
    int64_t x = stack[--sc];
    stack_push(x + y);
    INSRET;
}


int main(int argc, char **argv)
{
    Block b = {0};
    vm_init();
    b.ins = realloc(b.ins, ++b.n * sizeof(Ins));
    b.ins[b.n - 1].op = &Imm;
    b.ins[b.n - 1].imm = 3;
    b.ins = realloc(b.ins, ++b.n * sizeof(Ins));
    b.ins[b.n - 1].op = &Imm;
    b.ins[b.n - 1].imm = 4;
    b.ins = realloc(b.ins, ++b.n * sizeof(Ins));
    b.ins[b.n - 1].op = &Add;
    b.ins = realloc(b.ins, ++b.n * sizeof(Ins));
    b.ins[b.n - 1].op = &Done;

    (*b.ins[0].op)(0, b);
}

When I remove the structs for Fn and Scope, or when I turn optimizations on, it doesn't crash anymore. Bizarrely, it works fine with fsanitize=memory.

The full code is at https://git.sr.ht/~euclaise/trent/tree/

clang version 14.0.6
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
Josh Pritsker
  • 46
  • 1
  • 5

0 Answers0