Completely rewrite stack management.

Now we always use the stack instead of keeping a TOS register.
This is very inefficient, but I'll worry about register
allocation later.

The new block model is inspired by x86's `enter` and `leave`
instructions. I intend to support nested procedures at some point
in the future.
master
James T. Martin 2022-09-06 19:47:46 -07:00
parent 4e06f8d00f
commit 57aa667000
Signed by: james
GPG Key ID: D6FB2F9892F9B225
9 changed files with 231 additions and 234 deletions

View File

@ -6,7 +6,7 @@
#include <stddef.h>
#include <stdint.h>
typedef size_t ip;
typedef uint32_t ip;
extern ip here;
/// Jump to a known address.
@ -15,17 +15,4 @@ void inst_jump(ip there);
/// Jump to an unresolved address.
ip inst_jump_unresolved(void);
void inst_jump_resolve(ip disp, ip there);
void inst_mov_imm(reg reg, uint64_t imm);
void inst_mov_imm_i64(reg reg, int64_t imm);
void inst_syscall(void);
void inst_push(reg reg);
void inst_pop(reg reg);
void inst_mov(reg dest, reg src);
void inst_mov_from(reg dest, reg base);
void inst_mov_from_disp(reg dest, reg base, int32_t disp);
void inst_mov_to(reg base, reg src);
void inst_mov_to_disp(reg base, reg src, int32_t disp);
#endif

View File

@ -22,7 +22,7 @@ static const char* outfile_name;
FILE* infile;
FILE* outfile;
// HACK: "here" tracking should be handled by the assembler, not IO.
size_t here = 0;
uint32_t here = 0;
#ifdef __linux__
void open_files(const char* infile_name, const char* outfile_name_) {

View File

@ -6,7 +6,7 @@
extern FILE* outfile;
extern FILE* infile;
extern size_t here;
extern uint32_t here;
void open_files(const char* infile_name, const char* outfile_name);
void close_files(void);

277
src/ir.c
View File

@ -12,174 +12,80 @@
#include <stdlib.h>
#include <string.h>
struct fixups {
struct fixups* next;
ip disp;
#define MAX_STACK_FRAMES 32
#define MAX_LABELS 256
#define MAX_FIXUPS 256
struct stack_frame {
uint32_t depth;
uint32_t label_depth;
};
struct label_info {
const struct frame* frame;
size_t argc;
struct label {
uint32_t frame;
uint32_t argc;
ip definition;
struct fixups* fixups;
uint32_t fixupc;
ip fixups[MAX_FIXUPS];
};
struct labels {
struct labels* prev;
struct label_info info;
};
static uint32_t stack_depth = 0;
static uint32_t stack_frame = 0;
static struct stack_frame stack_frames[MAX_STACK_FRAMES];
static uint32_t label_depth;
static struct label labels[MAX_LABELS];
struct frame {
struct frame* prev;
size_t depth;
struct labels* labels;
};
static struct frame* current_frame;
struct storage {
enum { STORE_REG, STORE_STACK } type;
union { reg reg; size_t off; };
};
static size_t top_of_stack;
// Enter a new stack frame.
static void enter(void) {
struct frame* next = malloc(sizeof(struct frame));
next->prev = current_frame;
next->depth = top_of_stack;
current_frame = next;
void init(var* argc, var* argv, var* env) {
assert(stack_depth == 0);
x86_inst_mov_r64_r64(BP, SP);
// TODO: replace with add, once I implement add
x86_inst_add_r64_imm8(BP, 8 * 3);
*env = stack_depth++;
*argv = stack_depth++;
*argc = stack_depth++;
}
// Leave the current stack frame.
static void leave(void) {
struct frame* next = current_frame;
current_frame = next->prev;
top_of_stack = next->depth;
struct labels* labels = next->labels;
free(next);
while (labels != NULL) {
struct labels* next_label = labels->prev;
free(labels);
labels = next_label;
}
label enter(uint32_t retc) {
assert(stack_frame < MAX_STACK_FRAMES);
struct stack_frame frame = { stack_depth, label_depth };
stack_frames[stack_frame] = frame;
stack_frame++;
return declare(retc);
}
// Allocate registers or stack space for the arguments.
static void reserve(size_t argc) {
top_of_stack += argc;
void leave(var* args) {
assert(stack_frame > 0);
struct stack_frame frame = stack_frames[stack_frame];
define(frame.label_depth, args);
stack_depth = frame.depth;
label_depth = frame.label_depth;
}
static var new_var(void) {
var var = top_of_stack;
top_of_stack++;
x86_inst_push_r64(RA);
return var;
label declare(uint32_t argc) {
assert(label_depth < MAX_LABELS);
struct label label = { stack_frame, argc, (ip) -1, 0, 0 };
labels[label_depth] = label;
return label_depth++;
}
static struct storage storage(var var) {
struct storage storage;
if (var == top_of_stack - 1) {
storage.type = STORE_REG;
storage.reg = RA;
} else {
storage.type = STORE_STACK;
storage.off = -var * 8 - 16;
}
return storage;
}
static void move(var dest, var src) {
if (dest == src) return;
struct storage ds = storage(dest);
struct storage ss = storage(src);
if (ds.type == STORE_REG && ss.type == STORE_REG) {
x86_inst_mov_r64_r64(ds.reg, ss.reg);
} else if (ds.type == STORE_STACK && ss.type == STORE_REG) {
x86_inst_mov_m64_r64_disp(RB, ss.reg, ds.off);
} else if (ds.type == STORE_REG && ds.type == STORE_STACK) {
x86_inst_mov_r64_m64_disp(ds.reg, RB, ss.off);
} else {
// FIXME: DI is scratch register?
x86_inst_mov_r64_m64_disp(DI, RB, ss.off);
x86_inst_mov_m64_r64_disp(RB, DI, ds.off);
}
}
static void exchange(var x, var y) {
if (x == y) return;
assert(0); // UNIMPLEMENTED
}
// Restore the stack and registers to a previous frame,
// in preparation for a jump out of the current frame.
//
// This involves loading spilled variables into registers,
// restoring the stack pointer,
// spilling variables onto the stack to make space for arguments,
// and relocating arguments to the correct registers.
static void restore(const struct frame* frame, size_t argc, var* args) {
for (size_t i = 0; i < argc; i++) {
var arg = args[i];
size_t depth = frame->depth + i;
if (arg == depth) continue;
size_t conflict = (size_t) -1;
for (size_t j = i + 1; j < argc; j++) {
if (depth == args[j]) {
conflict = j;
break;
}
}
if (conflict == (size_t) -1) {
move(depth, args[i]);
} else {
// TODO: an algorithm which produces fewer exchanges
exchange(args[conflict], args[i]);
args[conflict] = args[i];
}
}
}
label declare_label(size_t argc) {
struct labels* labels = malloc(sizeof(struct labels));
labels->prev = current_frame->labels;
labels->info.frame = current_frame;
labels->info.argc = argc;
labels->info.definition = (ip) -1;
current_frame->labels = labels;
return &labels->info;
}
void define_label(label label, var* args) {
assert(label->frame == current_frame);
void define(label l, var* args) {
struct label* label = &labels[l];
label->definition = here;
struct fixups* fixups = label->fixups;
while (fixups != NULL) {
struct fixups* fixup = fixups;
inst_jump_resolve(fixup->disp, here);
fixups = fixup->next;
free(fixup);
while (label->fixupc > 0) {
label->fixupc--;
inst_jump_resolve(label->fixups[label->fixupc], here);
}
enter();
reserve(label->argc);
}
void queue_fixup(label label, ip disp) {
struct fixups* fixup = malloc(sizeof(struct fixups));
fixup->next = label->fixups;
fixup->disp = disp;
label->fixups = fixup;
}
void jump(label label, var* args) {
restore(label->frame, label->argc, args);
if (label->definition != (ip) -1) {
inst_jump(label->definition);
void jump(label l, var* args) {
struct label* label = &labels[l];
if (label->definition == (ip) -1) {
assert(label->fixupc < MAX_FIXUPS);
label->fixups[label->fixupc] = inst_jump_unresolved();
label->fixupc++;
} else {
ip disp = inst_jump_unresolved();
queue_fixup(label, disp);
inst_jump(label->definition);
}
leave();
}
void jump_table(size_t branches, label* labels, var index, var* args) {
@ -190,34 +96,59 @@ void jump_if(label label, var cond, var* args) {
assert(0); // UNIMPLEMENTED
}
static void save(var* vars) {
void load_var(reg reg, var var) {
// the stack grows downward, so the bottom of the stack, BP, points to nothing;
// subtracting 8 causes it to point to the first variable, 0.
// (each variable is 8 bytes.)
x86_inst_mov_r64_m64_disp(reg, BP, -(var * 8) - 8);
}
var lit(uint64_t x) {
var var = new_var();
struct storage stg = storage(var);
assert(stg.type == STORE_REG);
x86_inst_mov_r64_imm(stg.reg, x);
return var;
var push_var(reg reg) {
x86_inst_push_r64(reg);
return stack_depth++;
}
void syscall(size_t argc, var* args) {
assert(argc > 0);
// rax already populated by top of stack
// FIXME: this won't work forever
// FIXME: save args in case we don't want to sysexit
// FIXME: save other registers
if (argc > 1)
x86_inst_mov_r64_m64_disp(DI, RB, -args[1] * 8 - 16);
if (argc > 2)
x86_inst_mov_r64_m64_disp(SI, RB, -args[2] * 8 - 16);
if (argc > 3)
x86_inst_mov_r64_m64_disp(RD, RB, -args[3] * 8 - 16);
var lit(uint64_t lit) {
x86_inst_mov_r64_imm(AX, lit);
x86_inst_push_r64(AX);
return stack_depth++;
}
var sub(var subtrahend, var minuend) {
// TODO: use modr/m
load_var(AX, subtrahend);
load_var(BX, minuend);
x86_inst_sub_r64_r64(AX, BX);
return push_var(AX);
}
// Linux system call: https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/
var syscall(size_t argc, var* args) {
assert(argc > 0 && argc <= 7);
switch(argc) {
case 7:
load_var(R9, args[6]);
__attribute__((fallthrough));
case 6:
load_var(R8, args[5]);
__attribute__ ((fallthrough));
case 5:
load_var(R10, args[4]);
__attribute__ ((fallthrough));
case 4:
load_var(DX, args[3]);
__attribute__ ((fallthrough));
case 3:
load_var(SI, args[2]);
__attribute__ ((fallthrough));
case 2:
load_var(DI, args[1]);
__attribute__ ((fallthrough));
case 1:
// the system call number, not an argument
load_var(AX, args[0]);
}
// NOTE: syscall clobbers rcx and r11.
x86_inst_syscall();
}
void init(void) {
x86_inst_mov_r64_r64(RB, SP);
return push_var(AX);
}

View File

@ -1,49 +1,71 @@
#ifndef _ASSEMBLER_H
#define _ASSEMBLER_H
#ifndef _IR_H
#define _IR_H
#include <stddef.h>
#include <stdint.h>
typedef size_t var;
typedef struct label_info* label;
typedef uint32_t var;
typedef uint32_t label;
/// Declare a new label.
///
/// A label is the destination of a jump,
/// located with a fixed stack context and fixed argument types.
label declare_label(size_t args);
/// Call this at the beginning of execution.
/// It performs initialization and stuff.
void init(var* argc, var* argv, var* env);
/// Define a previously-declared label.
/// Enter a new block.
///
/// The label must be defined in the same stack context
/// which it was declared in.
/// All labels defined in this block will have access to all variables
/// which are in scope as of calling `enter`. You will be able to jump
/// to any label which is defined in this block from here
/// to the symmetric `leave`.
///
/// The definition of the label is... here, which is to say
/// whatever code you proceed to generate after this.
///
/// A label is implicitly terminated by an unconditional jump or exit.
/// However, it may exit at multiple locations via unconditional jumps.
void define_label(label label, var* args);
/// This also generates a new label corresponding with the end of the block,
/// which will be automatically defined when you call `leave`.
label enter(uint32_t retc);
/// Jump to label, unconditionally. Terminates a block.
/// Leave a block.
///
/// It is only possible to jump to a label in a parent or adjacent stack frame
/// (you can't jump *deeper* into the stack).
/// This will restore the context to how it was when `enter` was called,
/// plus the return values declared by the call to `enter`.
void leave(var* args);
/// Declare a new label in the innermost block.
///
/// This label can only be called from the block or nested blocks.
/// This label must be called with the given number of arguments.
label declare(uint32_t argc);
/// Define a label in the innermost block, automatically terminating
/// any previous labels.
///
/// All variables defined prior to the beginning of this block will be in scope.
/// The arguments associated with the label will be in scope.
/// Variables defined *after* the beginning of the block but *prior* to this label
/// will *not* be in scope.
///
/// From this label you can jump to any label in the enclosing block
/// or any parent block.
void define(label label, var* args);
/// Jump to label, unconditionally; never returns.
void jump(label label, var* args);
/// Jump to label in table; never returns. Terminates a block.
/// Jump to `index`th label in table; never returns.
///
/// All labels must be at the same depth and accept the same arguments.
/// `index` must not be out of bounds.
void jump_table(size_t branches, label* labels, var index, var* args);
/// Jump to label if `cond` is not zero. Does not terminate a block.
/// Jump to label if `cond` is not zero.
void jump_if(label label, var cond, var* args);
var lit(uint64_t x);
/// Integer literal.
var lit(uint64_t lit);
void syscall(size_t argc, var* args);
/// Subtraction.
var sub(var subtrahend, var minuend);
void init(void);
/// Perform a system call.
var syscall(size_t argc, var* args);
#endif

0
src/lex.c Normal file
View File

View File

@ -14,10 +14,13 @@
#define ELF_HEADER_SIZE 0xb0
size_t compile(void) {
init();
var code = lit(42);
var call = lit(60);
var args[2] = { call, code };
var argc, argv, env;
init(&argc, &argv, &env);
var a = lit(52);
var b = lit(10);
var exit_code = sub(a, b);
var sys_exit = lit(60);
var args[2] = { sys_exit, exit_code };
syscall(2, args);
return ELF_HEADER_SIZE;
}

View File

@ -36,6 +36,12 @@ static void x86_rexwr(reg reg) {
emit_u8(rex);
}
static void x86_rexwb(reg b) {
uint8_t rex = REX | REX_W;
if (b >= R8) rex |= REX_B;
emit_u8(rex);
}
static void x86_rexwrb(reg r, reg b) {
uint8_t rex = REX | REX_W;
if (r >= R8) rex |= REX_R;
@ -59,6 +65,10 @@ static void x86_modrm32(reg r, reg b) {
emit_u8(MODRM_RM32 | (REG(r) << 3) | REG(b));
}
static void x86_modxm(uint8_t ext, reg b) {
emit_u8(MODRM_RR | (ext << 3) | REG(b));
}
static void x86_enc_opr(uint8_t op, reg reg) {
x86_opt_rexr(reg);
emit_u8(op + REG(reg));
@ -123,6 +133,28 @@ static void x86_enc_rexw_modrmd(uint8_t op, reg r, reg b, int32_t disp) {
}
}
static void x86_enc_rexw_modxm_imm8(uint8_t op, uint8_t ext, reg m, uint8_t imm) {
x86_rexwb(m);
emit_u8(op);
x86_modxm(ext, m);
emit_u8(imm);
}
static void x86_enc_rexw_modxm_imm32(uint8_t op, uint8_t ext, reg m, uint32_t imm) {
x86_rexwb(m);
emit_u8(op);
x86_modxm(ext, m);
emit_u32(imm);
}
static void x86_enc_rexw_modxm_imm(uint8_t op, uint8_t ext, reg m, uint32_t imm) {
if (imm <= UINT8_MAX) {
x86_enc_rexw_modxm_imm8(op, ext, m, (uint8_t) imm);
} else {
x86_enc_rexw_modxm_imm32(op, ext, m, imm);
}
}
static void x86_enc_disp8(uint8_t op, int8_t disp) {
uint8_t buf[2] = { op, (uint8_t) disp };
emit(buf, 2);
@ -142,7 +174,7 @@ void x86_inst_mov_r64_imm32(reg dest, uint32_t imm) {
}
void x86_inst_mov_r64_imm(reg dest, uint64_t imm) {
// TODO: xor if 0, use inc and dec, 16-bit and 8-bit immediates
// TODO: xor if 0, use inc and dec, 16-bit and 8-bit immediates, sign extension
if (imm <= UINT32_MAX) {
x86_inst_mov_r64_imm32(dest, (uint32_t) imm);
} else {
@ -150,15 +182,6 @@ void x86_inst_mov_r64_imm(reg dest, uint64_t imm) {
}
}
void x86_inst_mov_r64_imms(reg dest, int64_t imm) {
// TODO: sign-extend
if (imm >= 0) {
x86_inst_mov_r64_imm(dest, imm);
} else {
x86_inst_mov_r64_imm64(dest, (int64_t) imm);
}
}
void x86_inst_mov_r64_r64(reg dest, reg src) {
x86_enc_rexw_modrr(0x8b, dest, src);
}
@ -220,6 +243,28 @@ void x86_inst_jmp_disp(int32_t disp) {
}
}
// TODO: special instructions for AX
void x86_inst_sub_r64_imm8(reg dest, int8_t imm) {
x86_enc_rexw_modxm_imm8(0x83, 5, dest, (uint8_t) imm);
}
void x86_inst_sub_r64_imm32(reg dest, int32_t imm) {
x86_enc_rexw_modxm_imm32(0x81, 5, dest, (uint32_t) imm);
}
void x86_inst_sub_r64_imm(reg dest, int32_t imm) {
x86_enc_rexw_modxm_imm(0x81, 5, dest, (uint32_t) imm);
}
void x86_inst_sub_r64_r64(reg dest, reg src) {
x86_enc_rexw_modrr(0x2B, dest, src);
}
// TODO: in case of -128, choice of add vs. sub matters!
void x86_inst_add_r64_imm8(reg dest, int8_t imm) {
x86_enc_rexw_modxm_imm8(0x83, 0, dest, (uint8_t) imm);
}
void x86_inst_syscall(void) {
const uint8_t buf[2] = { 0x0f, 0x05 };
emit(buf, 2);

View File

@ -7,10 +7,10 @@
// The specific register size (e.g. al/ax/eax/rax) depends on the instruction.
// All registers are valid for all instructions; we will perform exchanges if necessary.
typedef enum reg {
RA = 0, // rax, eax, ax, al
RC = 1, // rcx, ecx, cx, cl
RD = 2, // rdx, edx, dx, dl
RB = 3, // rbx, ebx, bx, bl
AX = 0, // rax, eax, ax, al
CX = 1, // rcx, ecx, cx, cl
DX = 2, // rdx, edx, dx, dl
BX = 3, // rbx, ebx, bx, bl
SP = 4, // rsp, esp, sp, spl (we do not use ah)
BP = 5, // rbp, ebp, bp, bpl (we do not use ch)
SI = 6, // rsi, esi, si, sil (we do not use dh)
@ -29,7 +29,6 @@ typedef enum reg {
void x86_inst_mov_r64_imm64(reg dest, uint64_t imm);
void x86_inst_mov_r64_imm32(reg dest, uint32_t imm);
void x86_inst_mov_r64_imm(reg dest, uint64_t imm);
void x86_inst_mov_r64_imms(reg dest, int64_t imm);
void x86_inst_mov_r64_r64(reg dest, reg src);
void x86_inst_mov_r64_m64(reg dest, reg src);
void x86_inst_mov_r64_m64_disp8(reg dest, reg src, int8_t disp);
@ -39,13 +38,23 @@ void x86_inst_mov_m64_r64(reg dest, reg src);
void x86_inst_mov_m64_r64_disp8(reg dest, reg src, int8_t disp);
void x86_inst_mov_m64_r64_disp32(reg dest, reg src, int32_t disp);
void x86_inst_mov_m64_r64_disp(reg dest, reg src, int32_t disp);
void x86_inst_push_r64(reg reg);
void x86_inst_pop_r64(reg reg);
#define X86_JMP_DISP8_SIZE 2
void x86_inst_jmp_disp8(int8_t disp);
#define X86_JMP_DISP32_SIZE 5
void x86_inst_jmp_disp32(int32_t disp);
void x86_inst_jmp_disp(int32_t disp);
void x86_inst_sub_r64_imm8(reg dest, int8_t imm);
void x86_inst_sub_r64_imm32(reg dest, int32_t imm);
void x86_inst_sub_r64_imm(reg dest, int32_t imm);
void x86_inst_sub_r64_r64(reg dest, reg src);
void x86_inst_add_r64_imm8(reg dest, int8_t imm);
void x86_inst_syscall(void);
#endif