#include "x86.h" #include #include /* ------------------------------------------------------------------------- * * i386 code generation * * The frame, from EBP downwards: * * [ebp + 8 + 4k] incoming argument k * [ebp + 4] return address * [ebp] saved ebp * [ebp - ...] parameters, copied in from the argument area * [ebp - ...] locals * [ebp - ...] one slot per temporary * * Parameters are copied into the frame rather than read in place so that a * parameter and a local are the same thing to everything below. * ------------------------------------------------------------------------- */ /* Which register a temporary lives in, or none. Only ebx, esi and edi are handed out: eax, ecx and edx are the scratch this emitter computes in, and the three that are left survive a call without being saved. */ #define REG_NONE 0 #define REG_COUNT 3 static const char *const REGS[REG_COUNT] = { "ebx", "esi", "edi" }; typedef struct Frame { const FeIrFunc *f; long *local_off; /* [ebp + off] for each local */ long temp_base; /* first temporary slot */ long size; /* bytes to subtract from esp */ /* 0 means the temporary lives in its stack slot. */ unsigned char *temp_reg; } Frame; static long align_up(long v, long a) { long r = v % a; return r ? v + a - r : v; } static unsigned long slot_bytes(const FeIrLocal *l) { switch (l->type) { case FE_IR_I8: return 1; case FE_IR_I16: return 2; case FE_IR_I32: return 4; case FE_IR_PTR: return 4; case FE_IR_MEM: return l->size ? l->size : 1; default: return 4; } } /* Every temporary is four bytes: a temporary only ever holds something that fits in a register, and narrower values are kept zero- or sign-extended. */ #define TEMP_SLOT 4L static void frame_layout(Frame *fr, const FeIrFunc *f, long *storage) { unsigned i; long off = 0; fr->f = f; fr->local_off = storage; for (i = 0; i < f->local_count; ++i) { unsigned long size = slot_bytes(&f->locals[i]); long a = (long)f->locals[i].align; if (a < 1) a = 1; if (a > 4) a = 4; off = align_up(off + (long)size, a); storage[i] = -off; } off = align_up(off, 4); fr->temp_base = -off; off += (long)f->temp_count * TEMP_SLOT; fr->size = align_up(off, 4); } /* Which temporaries a value reads. Returns how many it wrote into `used`. */ static unsigned reads_of(const FeIrValue *v, unsigned *used) { unsigned n = 0; unsigned i; switch (v->op) { case FE_IR_CONST: break; case FE_IR_LOAD: case FE_IR_ADDR: if (v->place.base == FE_PLACE_TEMP) used[n++] = v->place.index; break; case FE_IR_STORE: if (v->place.base == FE_PLACE_TEMP) used[n++] = v->place.index; used[n++] = v->a; break; case FE_IR_COPY: if (v->place.base == FE_PLACE_TEMP) used[n++] = v->place.index; if (v->place2.base == FE_PLACE_TEMP) used[n++] = v->place2.index; break; case FE_IR_CAST: used[n++] = v->a; break; case FE_IR_CALL: for (i = 0; i < v->arg_count && n < 18; ++i) used[n++] = v->args[i]; break; default: used[n++] = v->a; used[n++] = v->b; break; } return n; } /* Give registers to the temporaries that can hold one. A temporary that is defined in one block and read in another has to go through memory: this walks one block at a time and knows nothing about the others. Lowering does produce such temporaries -- a bounds check splits a block between computing an index and using it -- so eligibility is decided over the whole function first, and the scan inside a block only considers what survived that. */ static void allocate_registers(Frame *fr, const FeIrFunc *f) { unsigned n = f->temp_count; unsigned char *single; /* 1 while the temporary stays in one block */ unsigned *home; /* the block it was defined in */ unsigned *last; /* the last instruction in that block to read it */ const FeIrBlock *b; const FeIrValue *v; unsigned used[20]; unsigned i, k, at; if (!n) { fr->temp_reg = 0; return; } fr->temp_reg = (unsigned char *)calloc(n, 1); single = (unsigned char *)calloc(n, 1); home = (unsigned *)calloc(n, sizeof(unsigned)); last = (unsigned *)calloc(n, sizeof(unsigned)); if (!fr->temp_reg || !single || !home || !last) { free(single); free(home); free(last); return; } for (i = 0; i < n; ++i) { single[i] = 1; home[i] = 0xFFFFFFFFU; } for (b = f->first; b; b = b->next) { for (v = b->first; v; v = v->next) { if (v->has_dest) { if (home[v->dest] != 0xFFFFFFFFU) single[v->dest] = 0; home[v->dest] = b->id; } k = reads_of(v, used); for (i = 0; i < k; ++i) if (used[i] < n && home[used[i]] != b->id) single[used[i]] = 0; } if (b->term == FE_IR_BR && b->cond < n && home[b->cond] != b->id) single[b->cond] = 0; if (b->term == FE_IR_RET && b->has_ret_value && b->ret_value < n && home[b->ret_value] != b->id) single[b->ret_value] = 0; } for (b = f->first; b; b = b->next) { unsigned char busy[REG_COUNT]; unsigned owner[REG_COUNT]; for (i = 0; i < REG_COUNT; ++i) { busy[i] = 0; owner[i] = 0; } /* When each temporary is last read in this block. */ at = 0; for (v = b->first; v; v = v->next, ++at) { k = reads_of(v, used); for (i = 0; i < k; ++i) if (used[i] < n && single[used[i]]) last[used[i]] = at; } if (b->term == FE_IR_BR && b->cond < n && single[b->cond]) last[b->cond] = at; if (b->term == FE_IR_RET && b->has_ret_value && b->ret_value < n && single[b->ret_value]) last[b->ret_value] = at; at = 0; for (v = b->first; v; v = v->next, ++at) { /* Free whatever was read for the last time before this. */ for (i = 0; i < REG_COUNT; ++i) if (busy[i] && last[owner[i]] < at) busy[i] = 0; if (!v->has_dest || !single[v->dest]) continue; /* A call clobbers the scratch registers but not these three, so a result can still be kept in one across the call that made it. */ for (i = 0; i < REG_COUNT; ++i) if (!busy[i]) { busy[i] = 1; owner[i] = v->dest; fr->temp_reg[v->dest] = (unsigned char)(i + 1); break; } } } free(single); free(home); free(last); } static long temp_off(const Frame *fr, unsigned t) { return fr->temp_base - (long)(t + 1) * TEMP_SLOT; } static const char *word_of(FeIrType t) { switch (t) { case FE_IR_I8: return "byte ptr"; case FE_IR_I16: return "word ptr"; default: return "dword ptr"; } } static const char *reg_of(FeIrType t, int which) { /* which: 0 -> a, 1 -> c, 2 -> d */ switch (t) { case FE_IR_I8: return which == 0 ? "al" : which == 1 ? "cl" : "dl"; case FE_IR_I16: return which == 0 ? "ax" : which == 1 ? "cx" : "dx"; default: return which == 0 ? "eax" : which == 1 ? "ecx" : "edx"; } } /* Write the effective address of a place into `buf`. A place is a base plus a constant, and the only base that is not already an address is a temporary, which holds a pointer. */ static void place_addr(const Frame *fr, const FeIrPlace *p, char *buf) { switch (p->base) { case FE_PLACE_LOCAL: sprintf(buf, "[ebp%+ld]", fr->local_off[p->index] + p->offset); break; case FE_PLACE_GLOBAL: if (p->offset) sprintf(buf, "[%s%+ld]", p->name, p->offset); else sprintf(buf, "[%s]", p->name); break; case FE_PLACE_TEMP: sprintf(buf, "[edx%+ld]", p->offset); break; } } /* A temporary-based place needs its pointer in a register first. */ static void load_temp(const Frame *fr, unsigned t, const char *reg, FILE *out); static void load_place_base(const Frame *fr, const FeIrPlace *p, FILE *out) { if (p->base != FE_PLACE_TEMP) return; /* Through load_temp, not straight from the slot: the pointer may be living in a register, in which case the slot was never written. */ load_temp(fr, p->index, "edx", out); } static void load_temp(const Frame *fr, unsigned t, const char *reg, FILE *out) { if (fr->temp_reg && fr->temp_reg[t]) { const char *from = REGS[fr->temp_reg[t] - 1]; if (strcmp(from, reg) != 0) fprintf(out, " mov %s, %s\n", reg, from); return; } fprintf(out, " mov %s, [ebp%+ld]\n", reg, temp_off(fr, t)); } static void store_temp(const Frame *fr, unsigned t, const char *reg, FILE *out) { if (fr->temp_reg && fr->temp_reg[t]) { const char *to = REGS[fr->temp_reg[t] - 1]; if (strcmp(to, reg) != 0) fprintf(out, " mov %s, %s\n", to, reg); return; } fprintf(out, " mov [ebp%+ld], %s\n", temp_off(fr, t), reg); } /* The register a temporary lives in, or null when it lives in its slot. */ static const char *reg_home(const Frame *fr, unsigned t) { if (!fr->temp_reg || !fr->temp_reg[t]) return 0; return REGS[fr->temp_reg[t] - 1]; } /* Something an instruction can take as its right-hand operand: a register, or the temporary's slot read in place. */ static void operand_of(const Frame *fr, unsigned t, char *buf) { const char *r = reg_home(fr, t); if (r) strcpy(buf, r); else sprintf(buf, "dword ptr [ebp%+ld]", temp_off(fr, t)); } static const char *simple_op(FeIrOp op) { switch (op) { case FE_IR_ADD: return "add "; case FE_IR_SUB: return "sub "; case FE_IR_AND: return "and "; case FE_IR_OR: return "or "; case FE_IR_XOR: return "xor "; case FE_IR_MUL: return "imul"; default: return 0; } } static const char *cmp_set(FeIrOp op, int is_unsigned) { switch (op) { case FE_IR_EQ: return "sete"; case FE_IR_NE: return "setne"; case FE_IR_LT: return is_unsigned ? "setb" : "setl"; case FE_IR_LE: return is_unsigned ? "setbe" : "setle"; case FE_IR_GT: return is_unsigned ? "seta" : "setg"; case FE_IR_GE: return is_unsigned ? "setae" : "setge"; default: return "sete"; } } static void emit_binary(const Frame *fr, const FeIrValue *v, FILE *out) { int is_cmp = v->op >= FE_IR_EQ && v->op <= FE_IR_GE; FeIrType t = is_cmp ? (FeIrType)v->imm : v->type; const char *a = reg_of(t, 0); const char *c = reg_of(t, 1); /* When the result has a register of its own and the operation is one that can work on any register, the whole thing happens there: no trip through the scratch register and no trip through memory. Only the full-width operations qualify. esi and edi have no byte halves, so a narrow operation still goes through eax, where they do. */ if (!is_cmp && v->has_dest && (t == FE_IR_I32 || t == FE_IR_PTR) && simple_op(v->op)) { const char *d = reg_home(fr, v->dest); const char *rb = reg_home(fr, v->b); if (d && !(rb && strcmp(rb, d) == 0)) { char right[64]; load_temp(fr, v->a, d, out); operand_of(fr, v->b, right); fprintf(out, " %s %s, %s\n", simple_op(v->op), d, right); return; } } /* A full-width comparison can read both sides where they already are; the answer still has to come out of `al`, which is why it lands in eax when the result has no register of its own. */ if (is_cmp && (t == FE_IR_I32 || t == FE_IR_PTR)) { const char *left = reg_home(fr, v->a); const char *d = reg_home(fr, v->dest); char right[64]; if (!left) { load_temp(fr, v->a, "eax", out); left = "eax"; } operand_of(fr, v->b, right); fprintf(out, " cmp %s, %s\n", left, right); fprintf(out, " %s al\n", cmp_set(v->op, v->is_unsigned)); fprintf(out, " movzx %s, al\n", d ? d : "eax"); if (!d) store_temp(fr, v->dest, "eax", out); return; } load_temp(fr, v->a, "eax", out); load_temp(fr, v->b, "ecx", out); if (is_cmp) { fprintf(out, " cmp %s, %s\n", a, c); fprintf(out, " %s al\n", cmp_set(v->op, v->is_unsigned)); fprintf(out, " movzx eax, al\n"); store_temp(fr, v->dest, "eax", out); return; } switch (v->op) { case FE_IR_ADD: fprintf(out, " add %s, %s\n", a, c); break; case FE_IR_SUB: fprintf(out, " sub %s, %s\n", a, c); break; case FE_IR_MUL: fprintf(out, " imul %s, %s\n", a, c); break; case FE_IR_AND: fprintf(out, " and %s, %s\n", a, c); break; case FE_IR_OR: fprintf(out, " or %s, %s\n", a, c); break; case FE_IR_XOR: fprintf(out, " xor %s, %s\n", a, c); break; case FE_IR_SHL: fprintf(out, " shl %s, cl\n", a); break; case FE_IR_SHR: fprintf(out, " %s %s, cl\n", v->is_unsigned ? "shr" : "sar", a); break; case FE_IR_DIV: case FE_IR_MOD: /* The divide instructions use edx:eax, so the operands have to be widened to 32 bits whatever the declared width is. */ if (v->is_unsigned) fprintf(out, " xor edx, edx\n"); else fprintf(out, " cdq\n"); fprintf(out, " %s ecx\n", v->is_unsigned ? "div " : "idiv"); if (v->op == FE_IR_MOD) fprintf(out, " mov eax, edx\n"); break; default: break; } store_temp(fr, v->dest, "eax", out); } static void emit_value(const Frame *fr, const FeIrValue *v, FILE *out) { char addr[128]; unsigned i; switch (v->op) { case FE_IR_CONST: { const char *d = reg_home(fr, v->dest); fprintf(out, " mov %s, %ld\n", d ? d : "eax", v->imm); if (!d) store_temp(fr, v->dest, "eax", out); break; } case FE_IR_LOAD: { const char *d = reg_home(fr, v->dest); const char *into = d ? d : "eax"; load_place_base(fr, &v->place, out); place_addr(fr, &v->place, addr); if (v->type == FE_IR_I8) fprintf(out, " movzx %s, byte ptr %s\n", into, addr); else if (v->type == FE_IR_I16) fprintf(out, " movzx %s, word ptr %s\n", into, addr); else fprintf(out, " mov %s, dword ptr %s\n", into, addr); if (!d) store_temp(fr, v->dest, "eax", out); break; } case FE_IR_STORE: { const char *from = reg_home(fr, v->a); load_place_base(fr, &v->place, out); place_addr(fr, &v->place, addr); /* A full-width value already in a register goes straight out; a narrow one needs a byte or word half, which only eax has here. */ if (from && (v->type == FE_IR_I32 || v->type == FE_IR_PTR)) { fprintf(out, " mov %s %s, %s\n", word_of(v->type), addr, from); break; } load_temp(fr, v->a, "eax", out); fprintf(out, " mov %s %s, %s\n", word_of(v->type), addr, reg_of(v->type, 0)); break; } case FE_IR_ADDR: { const char *d = reg_home(fr, v->dest); load_place_base(fr, &v->place, out); place_addr(fr, &v->place, addr); fprintf(out, " lea %s, %s\n", d ? d : "eax", addr); if (!d) store_temp(fr, v->dest, "eax", out); break; } case FE_IR_CAST: load_temp(fr, v->a, "eax", out); /* Narrowing is free once everything is kept in a 32-bit slot; widening has to say whether the top bits are copies of the sign. */ if (v->type == FE_IR_I8) fprintf(out, " %s eax, al\n", v->is_unsigned ? "movzx" : "movsx"); else if (v->type == FE_IR_I16) fprintf(out, " %s eax, ax\n", v->is_unsigned ? "movzx" : "movsx"); store_temp(fr, v->dest, "eax", out); break; case FE_IR_CALL: /* cdecl: arguments pushed right to left, the caller pops them. */ for (i = v->arg_count; i > 0; --i) { load_temp(fr, v->args[i - 1], "eax", out); fprintf(out, " push eax\n"); } fprintf(out, " call %s\n", v->callee); if (v->arg_count) fprintf(out, " add esp, %u\n", v->arg_count * 4U); if (v->has_dest) store_temp(fr, v->dest, "eax", out); break; case FE_IR_COPY: { char dst[128]; char src[128]; /* Both addresses are worked out in the scratch registers first, and only then does the block copy take over esi and edi -- which may be holding temporaries, so it hands them back. */ if (v->place2.base == FE_PLACE_TEMP) { load_temp(fr, v->place2.index, "eax", out); if (v->place2.offset) fprintf(out, " add eax, %ld\n", v->place2.offset); } else { place_addr(fr, &v->place2, src); fprintf(out, " lea eax, %s\n", src); } if (v->place.base == FE_PLACE_TEMP) { load_temp(fr, v->place.index, "edx", out); if (v->place.offset) fprintf(out, " add edx, %ld\n", v->place.offset); } else { place_addr(fr, &v->place, dst); fprintf(out, " lea edx, %s\n", dst); } fprintf(out, " push esi\n"); fprintf(out, " push edi\n"); fprintf(out, " mov esi, eax\n"); fprintf(out, " mov edi, edx\n"); fprintf(out, " mov ecx, %ld\n", v->imm); fprintf(out, " cld\n"); fprintf(out, " rep movsb\n"); fprintf(out, " pop edi\n pop esi\n"); break; } default: emit_binary(fr, v, out); break; } } static void emit_func(const FeIrModule *m, const FeIrFunc *f, FILE *out) { Frame fr; long *storage; const FeIrBlock *b; const FeIrValue *v; unsigned i; long arg = 8; if (f->is_extern || !f->first) return; /* One offset per local, however many there are. A fixed array here would silently stop emitting a function that had too many. */ storage = (long *)malloc((size_t)(f->local_count ? f->local_count : 1) * sizeof(long)); if (!storage) return; frame_layout(&fr, f, storage); allocate_registers(&fr, f); fprintf(out, "\npublic %s\n", f->name); fprintf(out, "%s proc near\n", f->name); fprintf(out, " push ebp\n"); fprintf(out, " mov ebp, esp\n"); if (fr.size) fprintf(out, " sub esp, %ld\n", fr.size); fprintf(out, " push ebx\n push esi\n" " push edi\n"); /* Copy the incoming arguments into the frame. */ for (i = 0; i < f->param_count; ++i) { fprintf(out, " mov eax, [ebp+%ld]\n", arg); fprintf(out, " mov %s [ebp%+ld], %s\n", word_of(f->locals[i].type), storage[i], reg_of(f->locals[i].type, 0)); arg += 4; } for (b = f->first; b; b = b->next) { fprintf(out, "L%s_%u:\n", f->name, b->id); for (v = b->first; v; v = v->next) emit_value(&fr, v, out); switch (b->term) { case FE_IR_JMP: fprintf(out, " jmp L%s_%u\n", f->name, b->target); break; case FE_IR_BR: load_temp(&fr, b->cond, "eax", out); fprintf(out, " test eax, eax\n"); fprintf(out, " jnz L%s_%u\n", f->name, b->target); fprintf(out, " jmp L%s_%u\n", f->name, b->target_else); break; case FE_IR_RET: if (b->has_ret_value) load_temp(&fr, b->ret_value, "eax", out); fprintf(out, " pop edi\n pop esi\n" " pop ebx\n"); fprintf(out, " mov esp, ebp\n pop ebp\n"); fprintf(out, " ret\n"); break; case FE_IR_TRAP: fprintf(out, " push %lu\n", b->trap_line); fprintf(out, " push offset FE_FILE_%u\n", b->trap_file); fprintf(out, " push %u\n", (unsigned)b->trap); fprintf(out, " call fe_trap\n"); fprintf(out, " add esp, 12\n"); break; } } fprintf(out, "%s endp\n", f->name); free(storage); free(fr.temp_reg); (void)m; } static void emit_string(const char *s, FILE *out) { int in = 0; fputs(" db ", out); for (; s && *s; ++s) { unsigned char c = (unsigned char)*s; if (c >= 32 && c < 127 && c != '\'' && c != '"') { if (!in) { fputc('\'', out); in = 1; } fputc(c, out); } else { if (in) { fputs("',", out); in = 0; } fprintf(out, "%u,", c); } } if (in) fputc('\'', out); else fputc('0', out); if (in) fputs(",0", out); fputc('\n', out); } void fe_x86_emit(const FeIrModule *m, FILE *out) { const FeIrFunc *f; const FeIrGlobal *g; int any_trap = 0; const FeIrBlock *b; unsigned i; for (f = m->funcs; f && !any_trap; f = f->next) for (b = f->first; b; b = b->next) if (b->term == FE_IR_TRAP) { any_trap = 1; break; } fputs(".386\n.model flat\n\n", out); for (f = m->funcs; f; f = f->next) if (f->is_extern || !f->first) fprintf(out, "extern %s : near\n", f->name); /* Anything called but not defined here lives somewhere else -- the runtime, or a library. Lowering emits such calls directly (allocating, writing, trapping), so the names are collected from the calls themselves rather than from a list that would have to be kept in step. */ { const char *seen[64]; unsigned count = 0; const FeIrValue *v; const FeIrFunc *g; unsigned i; for (f = m->funcs; f; f = f->next) for (b = f->first; b; b = b->next) for (v = b->first; v; v = v->next) { if (v->op != FE_IR_CALL || !v->callee) continue; for (g = m->funcs; g; g = g->next) if (!strcmp(g->name, v->callee)) break; if (g) continue; for (i = 0; i < count; ++i) if (!strcmp(seen[i], v->callee)) break; if (i < count || count >= 64) continue; seen[count++] = v->callee; fprintf(out, "extern %s : near\n", v->callee); } } if (any_trap) fputs("extern fe_trap : near\n", out); fputs("\n_DATA segment dword public 'DATA'\n", out); /* One name per file a trap can come from. A build is many units in one module, and a trap that names the wrong file is worse than one that names none. */ for (i = 0; i < m->file_count; ++i) { fprintf(out, "public FE_FILE_%u\nFE_FILE_%u label byte\n", i, i); emit_string(m->files[i], out); } for (g = m->globals; g; g = g->next) { unsigned long i; fprintf(out, "public %s\n%s label byte\n", g->name, g->name); if (!g->init) { fprintf(out, " db %lu dup(0)\n", g->size ? g->size : 1UL); continue; } for (i = 0; i < g->size; ) { unsigned r; unsigned long j; for (r = 0; r < g->reloc_count; ++r) if (g->relocs[r].at == i) break; if (r < g->reloc_count) { /* A hole the linker fills with an address. */ fprintf(out, " dd offset %s\n", g->relocs[r].symbol); i += 4; continue; } fputs(" db ", out); j = 0; while (i < g->size && j < 16) { unsigned q; for (q = 0; q < g->reloc_count; ++q) if (g->relocs[q].at == i) break; if (q < g->reloc_count) break; fprintf(out, "%s%u", j ? "," : "", g->init[i]); ++i; ++j; } fputc('\n', out); } if (!g->size) fputs(" db 0\n", out); } fputs("_DATA ends\n", out); fputs("\n_TEXT segment dword public 'CODE'\n", out); for (f = m->funcs; f; f = f->next) emit_func(m, f, out); /* The runtime's entry stub calls one fixed name, so point it here. */ if (m->entry_main) fprintf(out, "\npublic fe_main_\nfe_main_ proc near\n" " jmp %s\nfe_main_ endp\n", m->entry_main); fputs("\n_TEXT ends\n\nend\n", out); }