about summary refs log tree commit diff stats
path: root/subx
diff options
context:
space:
mode:
Diffstat (limited to 'subx')
-rw-r--r--subx/010---vm.cc37
-rw-r--r--subx/011run.cc31
-rw-r--r--subx/012elf.cc28
-rw-r--r--subx/013direct_addressing.cc308
-rw-r--r--subx/014indirect_addressing.cc334
-rw-r--r--subx/015immediate_addressing.cc526
-rw-r--r--subx/017jump_disp8.cc54
-rw-r--r--subx/019functions.cc19
-rw-r--r--subx/020syscalls.cc19
-rw-r--r--subx/031check_operands.cc4
-rw-r--r--subx/034compute_segment_address.cc2
-rw-r--r--subx/040---tests.cc3
-rw-r--r--subx/050_write.subx11
-rw-r--r--subx/057write.subx2
-rw-r--r--subx/060read.subx2
-rw-r--r--subx/062write-stream.subx22
-rw-r--r--subx/066print-int.subx (renamed from subx/066print-byte.subx)122
-rwxr-xr-xsubx/apps/assortbin21961 -> 22287 bytes
-rwxr-xr-xsubx/apps/crenshaw2-1bin19120 -> 19446 bytes
-rwxr-xr-xsubx/apps/crenshaw2-1bbin19679 -> 20005 bytes
-rwxr-xr-x[-rw-r--r--]subx/apps/dquotesbin23219 -> 24275 bytes
-rw-r--r--subx/apps/dquotes.subx231
-rwxr-xr-xsubx/apps/factorialbin18036 -> 18362 bytes
-rwxr-xr-xsubx/apps/handlebin18863 -> 19189 bytes
-rwxr-xr-xsubx/apps/hexbin22129 -> 22455 bytes
-rwxr-xr-xsubx/apps/packbin36721 -> 37047 bytes
-rwxr-xr-xsubx/build2
-rwxr-xr-xsubx/test_layers6
28 files changed, 1499 insertions, 264 deletions
diff --git a/subx/010---vm.cc b/subx/010---vm.cc
index 31e5608f..18f69035 100644
--- a/subx/010---vm.cc
+++ b/subx/010---vm.cc
@@ -62,7 +62,10 @@ put_new(Help, "registers",
   "- the sign flag (SF): usually set if an arithmetic result is negative, or\n"
   "  reset if not.\n"
   "- the zero flag (ZF): usually set if a result is zero, or reset if not.\n"
-  "- the overflow flag (OF): usually set if an arithmetic result overflows.\n"
+  "- the carry flag (CF): usually set if an arithmetic result overflows by just one bit.\n"
+  "  Useful for operating on unsigned numbers.\n"
+  "- the overflow flag (OF): usually set if an arithmetic result overflows by more than one bit.\n"
+  "  Useful for operating on signed numbers.\n"
   "The flag bits are read by conditional jumps.\n"
   "\n"
   "For complete details on how different instructions update the flags, consult the IA-32\n"
@@ -78,36 +81,10 @@ put_new(Help, "registers",
 // the subset of x86 flag registers we care about
 bool SF = false;  // sign flag
 bool ZF = false;  // zero flag
+bool CF = false;  // carry flag
 bool OF = false;  // overflow flag
 :(before "End Reset")
-SF = ZF = OF = false;
-
-//: how the flag registers are updated after each instruction
-
-:(before "End Includes")
-// Combine 'arg1' and 'arg2' with arithmetic operation 'op' and store the
-// result in 'arg1', then update flags.
-// beware: no side-effects in args
-#define BINARY_ARITHMETIC_OP(op, arg1, arg2) { \
-  /* arg1 and arg2 must be signed */ \
-  int64_t tmp = arg1 op arg2; \
-  arg1 = arg1 op arg2; \
-  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << arg1 << end(); \
-  SF = (arg1 < 0); \
-  ZF = (arg1 == 0); \
-  OF = (arg1 != tmp); \
-}
-
-// Combine 'arg1' and 'arg2' with bitwise operation 'op' and store the result
-// in 'arg1', then update flags.
-#define BINARY_BITWISE_OP(op, arg1, arg2) { \
-  /* arg1 and arg2 must be unsigned */ \
-  arg1 = arg1 op arg2; \
-  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << arg1 << end(); \
-  SF = (arg1 >> 31); \
-  ZF = (arg1 == 0); \
-  OF = false; \
-}
+SF = ZF = CF = OF = false;
 
 //:: simulated RAM
 
@@ -374,7 +351,7 @@ void dump_registers() {
     if (i > 0) out << "; ";
     out << "  " << i << ": " << std::hex << std::setw(8) << std::setfill('_') << Reg[i].u;
   }
-  out << " -- SF: " << SF << "; ZF: " << ZF << "; OF: " << OF;
+  out << " -- SF: " << SF << "; ZF: " << ZF << "; CF: " << CF << "; OF: " << OF;
   trace(Callstack_depth+1, "run") << out.str() << end();
 }
 
diff --git a/subx/011run.cc b/subx/011run.cc
index 6b18ca06..236401b8 100644
--- a/subx/011run.cc
+++ b/subx/011run.cc
@@ -35,7 +35,7 @@ put_new(Help, "syntax",
 cerr << "  syntax\n";
 
 :(code)
-void test_add_imm32_to_eax() {
+void test_add_imm32_to_EAX() {
   // At the lowest level, SubX programs are a series of hex bytes, each
   // (variable-length) instruction on one line.
   run(
@@ -65,19 +65,18 @@ void test_add_imm32_to_eax() {
       // opcode        ModR/M                    SIB                   displacement    immediate
       // instruction   mod, reg, Reg/Mem bits    scale, index, base
       // 1-3 bytes     0/1 byte                  0/1 byte              0/1/2/4 bytes   0/1/2/4 bytes
-      "  05            .                         .                     .               0a 0b 0c 0d\n"  // add 0x0d0c0b0a to EAX
+      "  b8            .                         .                     .               0a 0b 0c 0d\n"  // copy 0x0d0c0b0a to EAX
       // The periods are just to help the eye track long gaps between columns,
       // and are otherwise ignored.
   );
   // This program, when run, causes the following events in the trace:
   CHECK_TRACE_CONTENTS(
-      "load: 0x00000001 -> 05\n"
+      "load: 0x00000001 -> b8\n"
       "load: 0x00000002 -> 0a\n"
       "load: 0x00000003 -> 0b\n"
       "load: 0x00000004 -> 0c\n"
       "load: 0x00000005 -> 0d\n"
-      "run: add imm32 0x0d0c0b0a to reg EAX\n"
-      "run: storing 0x0d0c0b0a\n"
+      "run: copy imm32 0x0d0c0b0a to EAX\n"
   );
 }
 
@@ -350,18 +349,30 @@ void parse_and_load(const string& text_bytes) {
 //:: run
 
 :(before "End Initialize Op Names")
-put_new(Name, "05", "add imm32 to EAX (add)");
+put_new(Name, "b8", "copy imm32 to EAX (mov)");
 
 //: our first opcode
+
 :(before "End Single-Byte Opcodes")
-case 0x05: {  // add imm32 to EAX
-  int32_t arg2 = next32();
-  trace(Callstack_depth+1, "run") << "add imm32 0x" << HEXWORD << arg2 << " to reg EAX" << end();
-  BINARY_ARITHMETIC_OP(+, Reg[EAX].i, arg2);
+case 0xb8: {  // copy imm32 to EAX
+  const int32_t src = next32();
+  trace(Callstack_depth+1, "run") << "copy imm32 0x" << HEXWORD << src << " to EAX" << end();
+  Reg[EAX].i = src;
   break;
 }
 
 :(code)
+void test_copy_imm32_to_EAX() {
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  b8                                 0a 0b 0c 0d \n"  // copy 0x0d0c0b0a to EAX
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: copy imm32 0x0d0c0b0a to EAX\n"
+  );
+}
+
 // read a 32-bit int in little-endian order from the instruction stream
 int32_t next32() {
   int32_t result = read_mem_i32(EIP);
diff --git a/subx/012elf.cc b/subx/012elf.cc
index d0a3fbd2..0ae0b108 100644
--- a/subx/012elf.cc
+++ b/subx/012elf.cc
@@ -90,6 +90,13 @@ void load_elf_contents(uint8_t* elf_contents, size_t size, int argc, char* argv[
 
 void push(uint32_t val) {
   Reg[ESP].u -= 4;
+  if (Reg[ESP].u < STACK_SEGMENT) {
+    raise << "The stack overflowed its segment. "
+          << "Maybe SPACE_FOR_SEGMENT should be larger? "
+          << "Or you need to carve out an exception for the stack segment "
+          << "to be larger.\n" << end();
+    exit(1);
+  }
   trace(Callstack_depth+1, "run") << "decrementing ESP to 0x" << HEXWORD << Reg[ESP].u << end();
   trace(Callstack_depth+1, "run") << "pushing value 0x" << HEXWORD << val << end();
   write_mem_u32(Reg[ESP].u, val);
@@ -134,18 +141,17 @@ void load_segment_from_program_header(uint8_t* elf_contents, int segment_index,
 //   code:  0x09000000 -> 0x09ffffff (specified in ELF binary)
 //   data:  0x0a000000 -> 0x0affffff (specified in ELF binary)
 //   --- heap gets mmap'd somewhere here ---
-//   stack: 0x7dffffff -> 0x7d000000 (downward; not in ELF binary)
-//   argv hack: 0x7f000000 -> 0x7fffffff (not in ELF binary)
+//   stack: 0xbdffffff -> 0xbd000000 (downward; not in ELF binary)
+//   argv hack: 0xbf000000 -> 0xbfffffff (not in ELF binary)
 //
-// For now we avoid addresses with the most significant bit set; SubX doesn't
-// support unsigned comparison yet (https://github.com/akkartik/mu/issues/30)
-// Once we do, we can go up to 0xc0000000; higher addresses are reserved for
-// the Linux kernel.
-const int CODE_SEGMENT      = 0x09000000;
-const int DATA_SEGMENT =      0x0a000000;
-const int STACK_SEGMENT     = 0x7d000000;
-const int AFTER_STACK       = 0x7e000000;
-const int ARGV_DATA_SEGMENT = 0x7f000000;
+// Addresses above 0xc0000000 are reserved for the Linux kernel.
+const uint32_t CODE_SEGMENT      = 0x09000000;
+const uint32_t DATA_SEGMENT      = 0x0a000000;
+const uint32_t START_HEAP        = 0x0b000000;
+const uint32_t END_HEAP          = 0xbd000000;
+const uint32_t STACK_SEGMENT     = 0xbd000000;
+const uint32_t AFTER_STACK       = 0xbe000000;
+const uint32_t ARGV_DATA_SEGMENT = 0xbf000000;
 // When updating the above memory map, don't forget to update `mmap`'s
 // implementation in the 'syscalls' layer.
 :(before "End Dump Info for Instruction")
diff --git a/subx/013direct_addressing.cc b/subx/013direct_addressing.cc
index c2dfa911..2914e0dd 100644
--- a/subx/013direct_addressing.cc
+++ b/subx/013direct_addressing.cc
@@ -25,12 +25,75 @@ case 0x01: {  // add r32 to r/m32
   uint8_t modrm = next();
   uint8_t arg2 = (modrm>>3)&0x7;
   trace(Callstack_depth+1, "run") << "add " << rname(arg2) << " to r/m32" << end();
-  int32_t* arg1 = effective_address(modrm);
-  BINARY_ARITHMETIC_OP(+, *arg1, Reg[arg2].i);
+  int32_t* signed_arg1 = effective_address(modrm);
+  int32_t signed_result = *signed_arg1 + Reg[arg2].i;
+  SF = (signed_result < 0);
+  ZF = (signed_result == 0);
+  int64_t signed_full_result = static_cast<int64_t>(*signed_arg1) + Reg[arg2].i;
+  OF = (signed_result != signed_full_result);
+  // set CF
+  uint32_t unsigned_arg1 = static_cast<uint32_t>(*signed_arg1);
+  uint32_t unsigned_result = unsigned_arg1 + Reg[arg2].u;
+  uint64_t unsigned_full_result = static_cast<uint64_t>(unsigned_arg1) + Reg[arg2].u;
+  CF = (unsigned_result != unsigned_full_result);
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
+  *signed_arg1 = signed_result;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << *signed_arg1 << end();
   break;
 }
 
 :(code)
+void test_add_r32_to_r32_signed_overflow() {
+  Reg[EAX].i = 0x7fffffff;  // largest positive signed integer
+  Reg[EBX].i = 1;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  01     d8                                    \n" // add EBX to EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add EBX to r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: SF=1; ZF=0; CF=0; OF=1\n"
+      "run: storing 0x80000000\n"
+  );
+}
+
+void test_add_r32_to_r32_unsigned_overflow() {
+  Reg[EAX].u = 0xffffffff;  // largest unsigned number
+  Reg[EBX].u = 1;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  01     d8                                    \n" // add EBX to EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add EBX to r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: SF=0; ZF=1; CF=1; OF=0\n"
+      "run: storing 0x00000000\n"
+  );
+}
+
+void test_add_r32_to_r32_unsigned_and_signed_overflow() {
+  Reg[EAX].u = Reg[EBX].u = 0x80000000;  // smallest negative signed integer
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  01     d8                                    \n" // add EBX to EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add EBX to r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: SF=0; ZF=1; CF=1; OF=1\n"
+      "run: storing 0x00000000\n"
+  );
+}
+
+:(code)
 // Implement tables 2-2 and 2-3 in the Intel manual, Volume 2.
 // We return a pointer so that instructions can write to multiple bytes in
 // 'Mem' at once.
@@ -111,18 +174,82 @@ case 0x29: {  // subtract r32 from r/m32
   const uint8_t modrm = next();
   const uint8_t arg2 = (modrm>>3)&0x7;
   trace(Callstack_depth+1, "run") << "subtract " << rname(arg2) << " from r/m32" << end();
-  int32_t* arg1 = effective_address(modrm);
-  BINARY_ARITHMETIC_OP(-, *arg1, Reg[arg2].i);
+  int32_t* signed_arg1 = effective_address(modrm);
+  int32_t signed_result = *signed_arg1 - Reg[arg2].i;
+  SF = (signed_result < 0);
+  ZF = (signed_result == 0);
+  int64_t signed_full_result = static_cast<int64_t>(*signed_arg1) - Reg[arg2].i;
+  OF = (signed_result != signed_full_result);
+  // set CF
+  uint32_t unsigned_arg1 = static_cast<uint32_t>(*signed_arg1);
+  uint32_t unsigned_result = unsigned_arg1 - Reg[arg2].u;
+  uint64_t unsigned_full_result = static_cast<uint64_t>(unsigned_arg1) - Reg[arg2].u;
+  CF = (unsigned_result != unsigned_full_result);
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
+  *signed_arg1 = signed_result;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << *signed_arg1 << end();
   break;
 }
 
+:(code)
+void test_subtract_r32_from_r32_signed_overflow() {
+  Reg[EAX].i = 0x80000000;  // smallest negative signed integer
+  Reg[EBX].i = 0x7fffffff;  // largest positive signed integer
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  29     d8                                    \n"  // subtract EBX from EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: subtract EBX from r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: SF=0; ZF=0; CF=0; OF=1\n"
+      "run: storing 0x00000001\n"
+  );
+}
+
+void test_subtract_r32_from_r32_unsigned_overflow() {
+  Reg[EAX].i = 0;
+  Reg[EBX].i = 1;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  29     d8                                    \n"  // subtract EBX from EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: subtract EBX from r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: SF=1; ZF=0; CF=1; OF=0\n"
+      "run: storing 0xffffffff\n"
+  );
+}
+
+void test_subtract_r32_from_r32_signed_and_unsigned_overflow() {
+  Reg[EAX].i = 0;
+  Reg[EBX].i = 0x80000000;  // smallest negative signed integer
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  29     d8                                    \n"  // subtract EBX from EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: subtract EBX from r/m32\n"
+      "run: r/m32 is EAX\n"
+      "run: SF=1; ZF=0; CF=1; OF=1\n"
+      "run: storing 0x80000000\n"
+  );
+}
+
 //:: multiply
 
 :(before "End Initialize Op Names")
 put_new(Name, "f7", "negate/multiply/divide rm32 (with EAX and EDX if necessary) depending on subop (neg/mul/idiv)");
 
 :(code)
-void test_multiply_eax_by_r32() {
+void test_multiply_EAX_by_r32() {
   Reg[EAX].i = 4;
   Reg[ECX].i = 3;
   run(
@@ -148,7 +275,7 @@ case 0xf7: {
   switch (subop) {
   case 4: {  // mul unsigned EAX by r/m32
     trace(Callstack_depth+1, "run") << "subop: multiply EAX by r/m32" << end();
-    const uint64_t result = Reg[EAX].u * static_cast<uint32_t>(*arg1);
+    const uint64_t result = static_cast<uint64_t>(Reg[EAX].u) * static_cast<uint32_t>(*arg1);
     Reg[EAX].u = result & 0xffffffff;
     Reg[EDX].u = result >> 32;
     OF = (Reg[EDX].u != 0);
@@ -179,19 +306,27 @@ void test_multiply_r32_into_r32() {
       // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
   );
   CHECK_TRACE_CONTENTS(
-      "run: multiply r/m32 into EBX\n"
+      "run: multiply EBX by r/m32\n"
       "run: r/m32 is EAX\n"
       "run: storing 0x00000008\n"
   );
 }
 
 :(before "End Two-Byte Opcodes Starting With 0f")
-case 0xaf: {  // multiply r32 into r/m32
+case 0xaf: {  // multiply r32 by r/m32
   const uint8_t modrm = next();
-  const uint8_t arg2 = (modrm>>3)&0x7;
-  trace(Callstack_depth+1, "run") << "multiply r/m32 into " << rname(arg2) << end();
-  const int32_t* arg1 = effective_address(modrm);
-  BINARY_ARITHMETIC_OP(*, Reg[arg2].i, *arg1);
+  const uint8_t arg1 = (modrm>>3)&0x7;
+  trace(Callstack_depth+1, "run") << "multiply " << rname(arg1) << " by r/m32" << end();
+  const int32_t* arg2 = effective_address(modrm);
+  int32_t result = Reg[arg1].i * (*arg2);
+  SF = (Reg[arg1].i < 0);
+  ZF = (Reg[arg1].i == 0);
+  int64_t full_result = static_cast<int64_t>(Reg[arg1].i) * (*arg2);
+  OF = (Reg[arg1].i != full_result);
+  CF = OF;
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
+  Reg[arg1].i = result;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << Reg[arg1].i << end();
   break;
 }
 
@@ -253,7 +388,7 @@ void test_negate_can_overflow() {
 
 //:: divide with remainder
 
-void test_divide_eax_by_rm32() {
+void test_divide_EAX_by_rm32() {
   Reg[EAX].u = 7;
   Reg[EDX].u = 0;
   Reg[ECX].i = 3;
@@ -280,13 +415,14 @@ case 7: {  // divide EDX:EAX by r/m32, storing quotient in EAX and remainder in
   assert(divisor != 0);
   Reg[EAX].i = dividend/divisor;  // quotient
   Reg[EDX].i = dividend%divisor;  // remainder
+  // flag state undefined
   trace(Callstack_depth+1, "run") << "quotient: 0x" << HEXWORD << Reg[EAX].i << end();
   trace(Callstack_depth+1, "run") << "remainder: 0x" << HEXWORD << Reg[EDX].i << end();
   break;
 }
 
 :(code)
-void test_divide_eax_by_negative_rm32() {
+void test_divide_EAX_by_negative_rm32() {
   Reg[EAX].u = 7;
   Reg[EDX].u = 0;
   Reg[ECX].i = -3;
@@ -305,7 +441,7 @@ void test_divide_eax_by_negative_rm32() {
   );
 }
 
-void test_divide_negative_eax_by_rm32() {
+void test_divide_negative_EAX_by_rm32() {
   Reg[EAX].i = -7;
   Reg[EDX].i = -1;  // sign extend
   Reg[ECX].i = 3;
@@ -324,7 +460,7 @@ void test_divide_negative_eax_by_rm32() {
   );
 }
 
-void test_divide_negative_edx_eax_by_rm32() {
+void test_divide_negative_EDX_EAX_by_rm32() {
   Reg[EAX].i = 0;  // lower 32 bits are clear
   Reg[EDX].i = -7;
   Reg[ECX].i = 0x40000000;  // 2^30 (largest positive power of 2)
@@ -569,8 +705,16 @@ case 0x21: {  // and r32 with r/m32
   const uint8_t modrm = next();
   const uint8_t arg2 = (modrm>>3)&0x7;
   trace(Callstack_depth+1, "run") << "and " << rname(arg2) << " with r/m32" << end();
-  int32_t* arg1 = effective_address(modrm);
-  BINARY_BITWISE_OP(&, *arg1, Reg[arg2].u);
+  // bitwise ops technically operate on unsigned numbers, but it makes no
+  // difference
+  int32_t* signed_arg1 = effective_address(modrm);
+  *signed_arg1 &= Reg[arg2].i;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << *signed_arg1 << end();
+  SF = (*signed_arg1 >> 31);
+  ZF = (*signed_arg1 == 0);
+  CF = false;
+  OF = false;
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
   break;
 }
 
@@ -601,8 +745,16 @@ case 0x09: {  // or r32 with r/m32
   const uint8_t modrm = next();
   const uint8_t arg2 = (modrm>>3)&0x7;
   trace(Callstack_depth+1, "run") << "or " << rname(arg2) << " with r/m32" << end();
-  int32_t* arg1 = effective_address(modrm);
-  BINARY_BITWISE_OP(|, *arg1, Reg[arg2].u);
+  // bitwise ops technically operate on unsigned numbers, but it makes no
+  // difference
+  int32_t* signed_arg1 = effective_address(modrm);
+  *signed_arg1 |= Reg[arg2].i;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << *signed_arg1 << end();
+  SF = (*signed_arg1 >> 31);
+  ZF = (*signed_arg1 == 0);
+  CF = false;
+  OF = false;
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
   break;
 }
 
@@ -633,8 +785,16 @@ case 0x31: {  // xor r32 with r/m32
   const uint8_t modrm = next();
   const uint8_t arg2 = (modrm>>3)&0x7;
   trace(Callstack_depth+1, "run") << "xor " << rname(arg2) << " with r/m32" << end();
-  int32_t* arg1 = effective_address(modrm);
-  BINARY_BITWISE_OP(^, *arg1, Reg[arg2].u);
+  // bitwise ops technically operate on unsigned numbers, but it makes no
+  // difference
+  int32_t* signed_arg1 = effective_address(modrm);
+  *signed_arg1 ^= Reg[arg2].i;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << *signed_arg1 << end();
+  SF = (*signed_arg1 >> 31);
+  ZF = (*signed_arg1 == 0);
+  CF = false;
+  OF = false;
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
   break;
 }
 
@@ -680,13 +840,13 @@ void test_compare_r32_with_r32_greater() {
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
-      "  39     d8                                    \n"  // compare EBX with EAX
+      "  39     d8                                    \n"  // compare EAX with EBX
       // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
   );
   CHECK_TRACE_CONTENTS(
-      "run: compare EBX with r/m32\n"
+      "run: compare r/m32 with EBX\n"
       "run: r/m32 is EAX\n"
-      "run: SF=0; ZF=0; OF=0\n"
+      "run: SF=0; ZF=0; CF=0; OF=0\n"
   );
 }
 
@@ -694,32 +854,84 @@ void test_compare_r32_with_r32_greater() {
 case 0x39: {  // set SF if r/m32 < r32
   const uint8_t modrm = next();
   const uint8_t reg2 = (modrm>>3)&0x7;
-  trace(Callstack_depth+1, "run") << "compare " << rname(reg2) << " with r/m32" << end();
-  const int32_t* arg1 = effective_address(modrm);
-  const int32_t arg2 = Reg[reg2].i;
-  const int32_t tmp1 = *arg1 - arg2;
-  SF = (tmp1 < 0);
-  ZF = (tmp1 == 0);
-  const int64_t tmp2 = *arg1 - arg2;
-  OF = (tmp1 != tmp2);
-  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; OF=" << OF << end();
+  trace(Callstack_depth+1, "run") << "compare r/m32 with " << rname(reg2) << end();
+  const int32_t* signed_arg1 = effective_address(modrm);
+  const int32_t signed_difference = *signed_arg1 - Reg[reg2].i;
+  SF = (signed_difference < 0);
+  ZF = (signed_difference == 0);
+  const int64_t signed_full_difference = static_cast<int64_t>(*signed_arg1) - Reg[reg2].i;
+  OF = (signed_difference != signed_full_difference);
+  // set CF
+  const uint32_t unsigned_arg1 = static_cast<uint32_t>(*signed_arg1);
+  const uint32_t unsigned_difference = unsigned_arg1 - Reg[reg2].u;
+  const uint64_t unsigned_full_difference = static_cast<uint64_t>(unsigned_arg1) - Reg[reg2].u;
+  CF = (unsigned_difference != unsigned_full_difference);
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
   break;
 }
 
 :(code)
-void test_compare_r32_with_r32_lesser() {
+void test_compare_r32_with_r32_lesser_unsigned_and_signed() {
   Reg[EAX].i = 0x0a0b0c07;
   Reg[EBX].i = 0x0a0b0c0d;
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
-      "  39     d8                                    \n"  // compare EBX with EAX
+      "  39     d8                                    \n"  // compare EAX with EBX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare r/m32 with EBX\n"
+      "run: r/m32 is EAX\n"
+      "run: SF=1; ZF=0; CF=1; OF=0\n"
+  );
+}
+
+void test_compare_r32_with_r32_lesser_unsigned_and_signed_due_to_overflow() {
+  Reg[EAX].i = 0x7fffffff;  // largest positive signed integer
+  Reg[EBX].i = 0x80000000;  // smallest negative signed integer
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  39     d8                                    \n"  // compare EAX with EBX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare r/m32 with EBX\n"
+      "run: r/m32 is EAX\n"
+      "run: SF=1; ZF=0; CF=1; OF=1\n"
+  );
+}
+
+void test_compare_r32_with_r32_lesser_signed() {
+  Reg[EAX].i = 0xffffffff;  // -1
+  Reg[EBX].i = 0x00000001;  // 1
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  39     d8                                    \n"  // compare EAX with EBX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare r/m32 with EBX\n"
+      "run: r/m32 is EAX\n"
+      "run: SF=1; ZF=0; CF=0; OF=0\n"
+  );
+}
+
+void test_compare_r32_with_r32_lesser_unsigned() {
+  Reg[EAX].i = 0x00000001;  // 1
+  Reg[EBX].i = 0xffffffff;  // -1
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  39     d8                                    \n"  // compare EAX with EBX
       // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
   );
   CHECK_TRACE_CONTENTS(
-      "run: compare EBX with r/m32\n"
+      "run: compare r/m32 with EBX\n"
       "run: r/m32 is EAX\n"
-      "run: SF=1; ZF=0; OF=0\n"
+      "run: SF=0; ZF=0; CF=1; OF=0\n"
   );
 }
 
@@ -729,13 +941,13 @@ void test_compare_r32_with_r32_equal() {
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
-      "  39     d8                                    \n"  // compare EBX with EAX
+      "  39     d8                                    \n"  // compare EAX and EBX
       // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
   );
   CHECK_TRACE_CONTENTS(
-      "run: compare EBX with r/m32\n"
+      "run: compare r/m32 with EBX\n"
       "run: r/m32 is EAX\n"
-      "run: SF=0; ZF=1; OF=0\n"
+      "run: SF=0; ZF=1; CF=0; OF=0\n"
   );
 }
 
@@ -971,7 +1183,8 @@ put_new(Name, "57", "push EDI to stack (push)");
 
 :(code)
 void test_push_r32() {
-  Reg[ESP].u = 0x64;
+  Mem.push_back(vma(0xbd000000));  // manually allocate memory
+  Reg[ESP].u = 0xbd000008;
   Reg[EBX].i = 0x0000000a;
   run(
       "== 0x1\n"  // code segment
@@ -980,7 +1193,7 @@ void test_push_r32() {
   );
   CHECK_TRACE_CONTENTS(
       "run: push EBX\n"
-      "run: decrementing ESP to 0x00000060\n"
+      "run: decrementing ESP to 0xbd000004\n"
       "run: pushing value 0x0000000a\n"
   );
 }
@@ -1015,9 +1228,9 @@ put_new(Name, "5f", "pop top of stack to EDI (pop)");
 
 :(code)
 void test_pop_r32() {
-  Reg[ESP].u = 0x02000000;
-  Mem.push_back(vma(0x02000000));  // manually allocate memory
-  write_mem_i32(0x02000000, 0x0000000a);  // ..before this write
+  Mem.push_back(vma(0xbd000000));  // manually allocate memory
+  Reg[ESP].u = 0xbd000008;
+  write_mem_i32(0xbd000008, 0x0000000a);  // ..before this write
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
@@ -1028,7 +1241,7 @@ void test_pop_r32() {
   CHECK_TRACE_CONTENTS(
       "run: pop into EBX\n"
       "run: popping value 0x0000000a\n"
-      "run: incrementing ESP to 0x02000004\n"
+      "run: incrementing ESP to 0xbd00000c\n"
   );
 }
 
@@ -1054,5 +1267,6 @@ uint32_t pop() {
   trace(Callstack_depth+1, "run") << "popping value 0x" << HEXWORD << result << end();
   Reg[ESP].u += 4;
   trace(Callstack_depth+1, "run") << "incrementing ESP to 0x" << HEXWORD << Reg[ESP].u << end();
+  assert(Reg[ESP].u < AFTER_STACK);
   return result;
 }
diff --git a/subx/014indirect_addressing.cc b/subx/014indirect_addressing.cc
index f591f0cf..a281921f 100644
--- a/subx/014indirect_addressing.cc
+++ b/subx/014indirect_addressing.cc
@@ -59,11 +59,84 @@ case 0x03: {  // add r/m32 to r32
   const uint8_t modrm = next();
   const uint8_t arg1 = (modrm>>3)&0x7;
   trace(Callstack_depth+1, "run") << "add r/m32 to " << rname(arg1) << end();
-  const int32_t* arg2 = effective_address(modrm);
-  BINARY_ARITHMETIC_OP(+, Reg[arg1].i, *arg2);
+  const int32_t* signed_arg2 = effective_address(modrm);
+  int32_t signed_result = Reg[arg1].i + *signed_arg2;
+  SF = (signed_result < 0);
+  ZF = (signed_result == 0);
+  int64_t signed_full_result = static_cast<int64_t>(Reg[arg1].i) + *signed_arg2;
+  OF = (signed_result != signed_full_result);
+  // set CF
+  uint32_t unsigned_arg2 = static_cast<uint32_t>(*signed_arg2);
+  uint32_t unsigned_result = Reg[arg1].u + unsigned_arg2;
+  uint64_t unsigned_full_result = static_cast<uint64_t>(Reg[arg1].u) + unsigned_arg2;
+  CF = (unsigned_result != unsigned_full_result);
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
+  Reg[arg1].i = signed_result;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << Reg[arg1].i << end();
   break;
 }
 
+:(code)
+void test_add_mem_at_r32_to_r32_signed_overflow() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0x7fffffff;  // largest positive signed integer
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  03     18                                    \n" // add *EAX to EBX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 1
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add r/m32 to EBX\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: effective address contains 1\n"
+      "run: SF=1; ZF=0; CF=0; OF=1\n"
+      "run: storing 0x80000000\n"
+  );
+}
+
+void test_add_mem_at_r32_to_r32_unsigned_overflow() {
+  Reg[EAX].u = 0x2000;
+  Reg[EBX].u = 0xffffffff;  // largest unsigned number
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  03     18                                    \n" // add *EAX to EBX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add r/m32 to EBX\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: effective address contains 1\n"
+      "run: SF=0; ZF=1; CF=1; OF=0\n"
+      "run: storing 0x00000000\n"
+  );
+}
+
+void test_add_mem_at_r32_to_r32_unsigned_and_signed_overflow() {
+  Reg[EAX].u = 0x2000;
+  Reg[EBX].u = 0x80000000;  // smallest negative signed integer
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  03     18                                    \n" // add *EAX to EBX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "00 00 00 80\n"  // smallest negative signed integer
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add r/m32 to EBX\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: effective address contains 80000000\n"
+      "run: SF=0; ZF=1; CF=1; OF=1\n"
+      "run: storing 0x00000000\n"
+  );
+}
+
 //:: subtract
 
 :(code)
@@ -114,11 +187,84 @@ case 0x2b: {  // subtract r/m32 from r32
   const uint8_t modrm = next();
   const uint8_t arg1 = (modrm>>3)&0x7;
   trace(Callstack_depth+1, "run") << "subtract r/m32 from " << rname(arg1) << end();
-  const int32_t* arg2 = effective_address(modrm);
-  BINARY_ARITHMETIC_OP(-, Reg[arg1].i, *arg2);
+  const int32_t* signed_arg2 = effective_address(modrm);
+  const int32_t signed_result = Reg[arg1].i - *signed_arg2;
+  SF = (signed_result < 0);
+  ZF = (signed_result == 0);
+  int64_t signed_full_result = static_cast<int64_t>(Reg[arg1].i) - *signed_arg2;
+  OF = (signed_result != signed_full_result);
+  // set CF
+  uint32_t unsigned_arg2 = static_cast<uint32_t>(*signed_arg2);
+  uint32_t unsigned_result = Reg[arg1].u - unsigned_arg2;
+  uint64_t unsigned_full_result = static_cast<uint64_t>(Reg[arg1].u) - unsigned_arg2;
+  CF = (unsigned_result != unsigned_full_result);
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
+  Reg[arg1].i = signed_result;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << Reg[arg1].i << end();
   break;
 }
 
+:(code)
+void test_subtract_mem_at_r32_from_r32_signed_overflow() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0x80000000;  // smallest negative signed integer
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  2b     18                                    \n"  // subtract *EAX from EBX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "ff ff ff 7f\n"  // largest positive signed integer
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: subtract r/m32 from EBX\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: effective address contains 7fffffff\n"
+      "run: SF=0; ZF=0; CF=0; OF=1\n"
+      "run: storing 0x00000001\n"
+  );
+}
+
+void test_subtract_mem_at_r32_from_r32_unsigned_overflow() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  2b     18                                    \n"  // subtract *EAX from EBX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 1
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: subtract r/m32 from EBX\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: effective address contains 1\n"
+      "run: SF=1; ZF=0; CF=1; OF=0\n"
+      "run: storing 0xffffffff\n"
+  );
+}
+
+void test_subtract_mem_at_r32_from_r32_signed_and_unsigned_overflow() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  2b     18                                    \n"  // subtract *EAX from EBX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "00 00 00 80\n"  // smallest negative signed integer
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: subtract r/m32 from EBX\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: effective address contains 80000000\n"
+      "run: SF=1; ZF=0; CF=1; OF=1\n"
+      "run: storing 0x80000000\n"
+  );
+}
+
 //:: and
 :(code)
 void test_and_r32_with_mem_at_r32() {
@@ -168,8 +314,16 @@ case 0x23: {  // and r/m32 with r32
   const uint8_t modrm = next();
   const uint8_t arg1 = (modrm>>3)&0x7;
   trace(Callstack_depth+1, "run") << "and r/m32 with " << rname(arg1) << end();
-  const int32_t* arg2 = effective_address(modrm);
-  BINARY_BITWISE_OP(&, Reg[arg1].u, *arg2);
+  // bitwise ops technically operate on unsigned numbers, but it makes no
+  // difference
+  const int32_t* signed_arg2 = effective_address(modrm);
+  Reg[arg1].i &= *signed_arg2;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << Reg[arg1].i << end();
+  SF = (Reg[arg1].i >> 31);
+  ZF = (Reg[arg1].i == 0);
+  CF = false;
+  OF = false;
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
   break;
 }
 
@@ -223,8 +377,16 @@ case 0x0b: {  // or r/m32 with r32
   const uint8_t modrm = next();
   const uint8_t arg1 = (modrm>>3)&0x7;
   trace(Callstack_depth+1, "run") << "or r/m32 with " << rname(arg1) << end();
-  const int32_t* arg2 = effective_address(modrm);
-  BINARY_BITWISE_OP(|, Reg[arg1].u, *arg2);
+  // bitwise ops technically operate on unsigned numbers, but it makes no
+  // difference
+  const int32_t* signed_arg2 = effective_address(modrm);
+  Reg[arg1].i |= *signed_arg2;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << Reg[arg1].i << end();
+  SF = (Reg[arg1].i >> 31);
+  ZF = (Reg[arg1].i == 0);
+  CF = false;
+  OF = false;
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
   break;
 }
 
@@ -277,8 +439,16 @@ case 0x33: {  // xor r/m32 with r32
   const uint8_t modrm = next();
   const uint8_t arg1 = (modrm>>3)&0x7;
   trace(Callstack_depth+1, "run") << "xor r/m32 with " << rname(arg1) << end();
-  const int32_t* arg2 = effective_address(modrm);
-  BINARY_BITWISE_OP(|, Reg[arg1].u, *arg2);
+  // bitwise ops technically operate on unsigned numbers, but it makes no
+  // difference
+  const int32_t* signed_arg2 = effective_address(modrm);
+  Reg[arg1].i |= *signed_arg2;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << Reg[arg1].i << end();
+  SF = (Reg[arg1].i >> 31);
+  ZF = (Reg[arg1].i == 0);
+  CF = false;
+  OF = false;
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
   break;
 }
 
@@ -312,15 +482,15 @@ void test_compare_mem_at_r32_with_r32_greater() {
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
-      "  39     18                                    \n"  // compare EBX with *EAX
+      "  39     18                                    \n"  // compare *EAX with EBX
       // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
       "== 0x2000\n"  // data segment
       "0d 0c 0b 0a\n"  // 0x0a0b0c0d
   );
   CHECK_TRACE_CONTENTS(
-      "run: compare EBX with r/m32\n"
+      "run: compare r/m32 with EBX\n"
       "run: effective address is 0x00002000 (EAX)\n"
-      "run: SF=0; ZF=0; OF=0\n"
+      "run: SF=0; ZF=0; CF=0; OF=0\n"
   );
 }
 
@@ -331,15 +501,15 @@ void test_compare_mem_at_r32_with_r32_lesser() {
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
-      "  39     18                                    \n"  // compare EBX with *EAX
+      "  39     18                                    \n"  // compare *EAX with EBX
       // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
       "== 0x2000\n"  // data segment
       "07 0c 0b 0a\n"  // 0x0a0b0c0d
   );
   CHECK_TRACE_CONTENTS(
-      "run: compare EBX with r/m32\n"
+      "run: compare r/m32 with EBX\n"
       "run: effective address is 0x00002000 (EAX)\n"
-      "run: SF=1; ZF=0; OF=0\n"
+      "run: SF=1; ZF=0; CF=1; OF=0\n"
   );
 }
 
@@ -350,15 +520,15 @@ void test_compare_mem_at_r32_with_r32_equal() {
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
-      "  39     18                                    \n"  // compare EBX with *EAX
+      "  39     18                                    \n"  // compare *EAX and EBX
       // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
       "== 0x2000\n"  // data segment
       "0d 0c 0b 0a\n"  // 0x0a0b0c0d
   );
   CHECK_TRACE_CONTENTS(
-      "run: compare EBX with r/m32\n"
+      "run: compare r/m32 with EBX\n"
       "run: effective address is 0x00002000 (EAX)\n"
-      "run: SF=0; ZF=1; OF=0\n"
+      "run: SF=0; ZF=1; CF=0; OF=0\n"
   );
 }
 
@@ -374,15 +544,15 @@ void test_compare_r32_with_mem_at_r32_greater() {
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
-      "  3b     18                                    \n"  // compare *EAX with EBX
+      "  3b     18                                    \n"  // compare EBX with *EAX
       // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
       "== 0x2000\n"  // data segment
-      "07 0c 0b 0a\n"  // 0x0a0b0c0d
+      "07 0c 0b 0a\n"  // 0x0a0b0c07
   );
   CHECK_TRACE_CONTENTS(
-      "run: compare r/m32 with EBX\n"
+      "run: compare EBX with r/m32\n"
       "run: effective address is 0x00002000 (EAX)\n"
-      "run: SF=0; ZF=0; OF=0\n"
+      "run: SF=0; ZF=0; CF=0; OF=0\n"
   );
 }
 
@@ -390,59 +560,118 @@ void test_compare_r32_with_mem_at_r32_greater() {
 case 0x3b: {  // set SF if r32 < r/m32
   const uint8_t modrm = next();
   const uint8_t reg1 = (modrm>>3)&0x7;
-  trace(Callstack_depth+1, "run") << "compare r/m32 with " << rname(reg1) << end();
-  const int32_t arg1 = Reg[reg1].i;
-  const int32_t* arg2 = effective_address(modrm);
-  const int32_t tmp1 = arg1 - *arg2;
-  SF = (tmp1 < 0);
-  ZF = (tmp1 == 0);
-  int64_t tmp2 = arg1 - *arg2;
-  OF = (tmp1 != tmp2);
-  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; OF=" << OF << end();
+  trace(Callstack_depth+1, "run") << "compare " << rname(reg1) << " with r/m32" << end();
+  const int32_t* signed_arg2 = effective_address(modrm);
+  const int32_t signed_difference = Reg[reg1].i - *signed_arg2;
+  SF = (signed_difference < 0);
+  ZF = (signed_difference == 0);
+  int64_t full_signed_difference = static_cast<int64_t>(Reg[reg1].i) - *signed_arg2;
+  OF = (signed_difference != full_signed_difference);
+  const uint32_t unsigned_arg2 = static_cast<uint32_t>(*signed_arg2);
+  const uint32_t unsigned_difference = Reg[reg1].u - unsigned_arg2;
+  const uint64_t full_unsigned_difference = static_cast<uint64_t>(Reg[reg1].u) - unsigned_arg2;
+  CF = (unsigned_difference != full_unsigned_difference);
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
   break;
 }
 
 :(code)
-void test_compare_r32_with_mem_at_r32_lesser() {
+void test_compare_r32_with_mem_at_r32_lesser_unsigned_and_signed() {
   Reg[EAX].i = 0x2000;
   Reg[EBX].i = 0x0a0b0c07;
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
-      "  3b     18                                    \n"  // compare *EAX with EBX
-      // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
+      "  3b     18                                    \n"  // compare EBX with *EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
       "== 0x2000\n"  // data segment
       "0d 0c 0b 0a\n"  // 0x0a0b0c0d
   );
   CHECK_TRACE_CONTENTS(
-      "run: compare r/m32 with EBX\n"
+      "run: compare EBX with r/m32\n"
       "run: effective address is 0x00002000 (EAX)\n"
-      "run: SF=1; ZF=0; OF=0\n"
+      "run: effective address contains a0b0c0d\n"
+      "run: SF=1; ZF=0; CF=1; OF=0\n"
+  );
+}
+
+void test_compare_r32_with_mem_at_r32_lesser_unsigned_and_signed_due_to_overflow() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0x7fffffff;  // largest positive signed integer
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  3b     18                                    \n"  // compare EBX with *EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "00 00 00 80\n"  // smallest negative signed integer
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare EBX with r/m32\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: effective address contains 80000000\n"
+      "run: SF=1; ZF=0; CF=1; OF=1\n"
+  );
+}
+
+void test_compare_r32_with_mem_at_r32_lesser_signed() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0xffffffff;  // -1
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  3b     18                                    \n"  // compare EBX with *EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "01 00 00 00\n"  // 1
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare EBX with r/m32\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: effective address contains 1\n"
+      "run: SF=1; ZF=0; CF=0; OF=0\n"
+  );
+}
+
+void test_compare_r32_with_mem_at_r32_lesser_unsigned() {
+  Reg[EAX].i = 0x2000;
+  Reg[EBX].i = 0x00000001;  // 1
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  3b     18                                    \n"  // compare EBX with *EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+      "== 0x2000\n"  // data segment
+      "ff ff ff ff\n"  // -1
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare EBX with r/m32\n"
+      "run: effective address is 0x00002000 (EAX)\n"
+      "run: effective address contains ffffffff\n"
+      "run: SF=0; ZF=0; CF=1; OF=0\n"
   );
 }
 
-:(code)
 void test_compare_r32_with_mem_at_r32_equal() {
   Reg[EAX].i = 0x2000;
   Reg[EBX].i = 0x0a0b0c0d;
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
-      "  3b     18                                    \n"  // compare *EAX with EBX
+      "  3b     18                                    \n"  // compare EBX with *EAX
       // ModR/M in binary: 00 (indirect mode) 011 (src EAX) 000 (dest EAX)
       "== 0x2000\n"  // data segment
       "0d 0c 0b 0a\n"  // 0x0a0b0c0d
   );
   CHECK_TRACE_CONTENTS(
-      "run: compare r/m32 with EBX\n"
+      "run: compare EBX with r/m32\n"
       "run: effective address is 0x00002000 (EAX)\n"
-      "run: SF=0; ZF=1; OF=0\n"
+      "run: SF=0; ZF=1; CF=0; OF=0\n"
   );
 }
 
 //:: copy (mov)
 
-:(code)
 void test_copy_r32_to_mem_at_r32() {
   Reg[EBX].i = 0xaf;
   Reg[EAX].i = 0x60;
@@ -502,8 +731,8 @@ void test_jump_mem_at_r32() {
       // op     ModR/M  SIB   displacement  immediate
       "  ff     20                                    \n"  // jump to *EAX
       // ModR/M in binary: 00 (indirect mode) 100 (jump to r/m32) 000 (src EAX)
-      "  05                                 00 00 00 01\n"
-      "  05                                 00 00 00 02\n"
+      "  b8                                 00 00 00 01\n"
+      "  b8                                 00 00 00 02\n"
       "== 0x2000\n"  // data segment
       "08 00 00 00\n"  // 0x00000008
   );
@@ -512,9 +741,9 @@ void test_jump_mem_at_r32() {
       "run: jump to r/m32\n"
       "run: effective address is 0x00002000 (EAX)\n"
       "run: jumping to 0x00000008\n"
-      "run: 0x00000008 opcode: 05\n"
+      "run: 0x00000008 opcode: b8\n"
   );
-  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000003 opcode: 05");
+  CHECK_TRACE_DOESNT_CONTAIN("run: 0x00000003 opcode: b8");
 }
 
 :(before "End Op ff Subops")
@@ -531,7 +760,8 @@ case 4: {  // jump to r/m32
 :(code)
 void test_push_mem_at_r32() {
   Reg[EAX].i = 0x2000;
-  Reg[ESP].u = 0x14;
+  Mem.push_back(vma(0xbd000000));  // manually allocate memory
+  Reg[ESP].u = 0xbd000014;
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
@@ -542,7 +772,7 @@ void test_push_mem_at_r32() {
   CHECK_TRACE_CONTENTS(
       "run: push r/m32\n"
       "run: effective address is 0x00002000 (EAX)\n"
-      "run: decrementing ESP to 0x00000010\n"
+      "run: decrementing ESP to 0xbd000010\n"
       "run: pushing value 0x000000af\n"
   );
 }
@@ -563,20 +793,20 @@ put_new(Name, "8f", "pop top of stack to rm32 (pop)");
 :(code)
 void test_pop_mem_at_r32() {
   Reg[EAX].i = 0x60;
-  Reg[ESP].u = 0x2000;
+  Mem.push_back(vma(0xbd000000));  // manually allocate memory
+  Reg[ESP].u = 0xbd000000;
+  write_mem_i32(0xbd000000, 0x00000030);
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  8f     00                                    \n"  // pop stack into *EAX
       // ModR/M in binary: 00 (indirect mode) 000 (pop r/m32) 000 (dest EAX)
-      "== 0x2000\n"  // data segment
-      "30 00 00 00\n"  // 0x00000030
   );
   CHECK_TRACE_CONTENTS(
       "run: pop into r/m32\n"
       "run: effective address is 0x00000060 (EAX)\n"
       "run: popping value 0x00000030\n"
-      "run: incrementing ESP to 0x00002004\n"
+      "run: incrementing ESP to 0xbd000004\n"
   );
 }
 
diff --git a/subx/015immediate_addressing.cc b/subx/015immediate_addressing.cc
index 18cd5334..4210c024 100644
--- a/subx/015immediate_addressing.cc
+++ b/subx/015immediate_addressing.cc
@@ -1,6 +1,78 @@
 //: instructions that (immediately) contain an argument to act with
 
 :(before "End Initialize Op Names")
+put_new(Name, "05", "add imm32 to EAX (add)");
+
+:(before "End Single-Byte Opcodes")
+case 0x05: {  // add imm32 to EAX
+  int32_t signed_arg2 = next32();
+  trace(Callstack_depth+1, "run") << "add imm32 0x" << HEXWORD << signed_arg2 << " to EAX" << end();
+  int32_t signed_result = Reg[EAX].i + signed_arg2;
+  SF = (signed_result < 0);
+  ZF = (signed_result == 0);
+  int64_t signed_full_result = static_cast<int64_t>(Reg[EAX].i) + signed_arg2;
+  OF = (signed_result != signed_full_result);
+  // set CF
+  uint32_t unsigned_arg2 = static_cast<uint32_t>(signed_arg2);
+  uint32_t unsigned_result = Reg[EAX].u + unsigned_arg2;
+  uint64_t unsigned_full_result = static_cast<uint64_t>(Reg[EAX].u) + unsigned_arg2;
+  CF = (unsigned_result != unsigned_full_result);
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
+  Reg[EAX].i = signed_result;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << Reg[EAX].i << end();
+  break;
+}
+
+:(code)
+void test_add_imm32_to_EAX_signed_overflow() {
+  Reg[EAX].i = 0x7fffffff;  // largest positive signed integer
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  05                                 01 00 00 00 \n" // add 1 to EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add imm32 0x00000001 to EAX\n"
+      "run: SF=1; ZF=0; CF=0; OF=1\n"
+      "run: storing 0x80000000\n"
+  );
+}
+
+void test_add_imm32_to_EAX_unsigned_overflow() {
+  Reg[EAX].u = 0xffffffff;  // largest unsigned number
+  Reg[EBX].u = 1;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  05                                 01 00 00 00 \n" // add 1 to EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add imm32 0x00000001 to EAX\n"
+      "run: SF=0; ZF=1; CF=1; OF=0\n"
+      "run: storing 0x00000000\n"
+  );
+}
+
+void test_add_imm32_to_EAX_unsigned_and_signed_overflow() {
+  Reg[EAX].u = 0x80000000;  // smallest negative signed integer
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  05                                 00 00 00 80 \n" // add 0x80000000 to EAX
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: add imm32 0x80000000 to EAX\n"
+      "run: SF=0; ZF=1; CF=1; OF=1\n"
+      "run: storing 0x00000000\n"
+  );
+}
+
+//:
+
+:(before "End Initialize Op Names")
 put_new(Name, "81", "combine rm32 with imm32 based on subop (add/sub/and/or/xor/cmp)");
 
 :(code)
@@ -10,7 +82,7 @@ void test_add_imm32_to_r32() {
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  81     c3                          0a 0b 0c 0d\n"  // add 0x0d0c0b0a to EBX
-      // ModR/M in binary: 11 (direct mode) 000 (add imm32) 011 (dest EBX)
+      // ModR/M in binary: 11 (direct mode) 000 (subop add) 011 (dest EBX)
   );
   CHECK_TRACE_CONTENTS(
       "run: combine imm32 with r/m32\n"
@@ -25,15 +97,29 @@ void test_add_imm32_to_r32() {
 case 0x81: {  // combine imm32 with r/m32
   trace(Callstack_depth+1, "run") << "combine imm32 with r/m32" << end();
   const uint8_t modrm = next();
-  int32_t* arg1 = effective_address(modrm);
-  const int32_t arg2 = next32();
-  trace(Callstack_depth+1, "run") << "imm32 is 0x" << HEXWORD << arg2 << end();
+  int32_t* signed_arg1 = effective_address(modrm);
+  const int32_t signed_arg2 = next32();
+  trace(Callstack_depth+1, "run") << "imm32 is 0x" << HEXWORD << signed_arg2 << end();
   const uint8_t subop = (modrm>>3)&0x7;  // middle 3 'reg opcode' bits
   switch (subop) {
-  case 0:
+  case 0: {
     trace(Callstack_depth+1, "run") << "subop add" << end();
-    BINARY_ARITHMETIC_OP(+, *arg1, arg2);
+    int32_t signed_result = *signed_arg1 + signed_arg2;
+    SF = (signed_result < 0);
+    ZF = (signed_result == 0);
+    int64_t signed_full_result = static_cast<int64_t>(*signed_arg1) + signed_arg2;
+    OF = (signed_result != signed_full_result);
+    // set CF
+    uint32_t unsigned_arg1 = static_cast<uint32_t>(*signed_arg1);
+    uint32_t unsigned_arg2 = static_cast<uint32_t>(signed_arg2);
+    uint32_t unsigned_result = unsigned_arg1 + unsigned_arg2;
+    uint64_t unsigned_full_result = static_cast<uint64_t>(unsigned_arg1) + unsigned_arg2;
+    CF = (unsigned_result != unsigned_full_result);
+    trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
+    *signed_arg1 = signed_result;
+    trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << *signed_arg1 << end();
     break;
+  }
   // End Op 81 Subops
   default:
     cerr << "unrecognized subop for opcode 81: " << NUM(subop) << '\n';
@@ -42,6 +128,61 @@ case 0x81: {  // combine imm32 with r/m32
   break;
 }
 
+:(code)
+void test_add_imm32_to_r32_signed_overflow() {
+  Reg[EBX].i = 0x7fffffff;  // largest positive signed integer
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     c3                          01 00 00 00\n"  // add 1 to EBX
+      // ModR/M in binary: 11 (direct mode) 000 (subop add) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: imm32 is 0x00000001\n"
+      "run: subop add\n"
+      "run: SF=1; ZF=0; CF=0; OF=1\n"
+      "run: storing 0x80000000\n"
+  );
+}
+
+void test_add_imm32_to_r32_unsigned_overflow() {
+  Reg[EBX].u = 0xffffffff;  // largest unsigned number
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     c3                          01 00 00 00\n"  // add 1 to EBX
+      // ModR/M in binary: 11 (direct mode) 011 (subop add) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: imm32 is 0x00000001\n"
+      "run: subop add\n"
+      "run: SF=0; ZF=1; CF=1; OF=0\n"
+      "run: storing 0x00000000\n"
+  );
+}
+
+void test_add_imm32_to_r32_unsigned_and_signed_overflow() {
+  Reg[EBX].u = 0x80000000;  // smallest negative signed integer
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     c3                          00 00 00 80\n"  // add 0x80000000 to EBX
+      // ModR/M in binary: 11 (direct mode) 011 (subop add) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: r/m32 is EBX\n"
+      "run: imm32 is 0x80000000\n"
+      "run: subop add\n"
+      "run: SF=0; ZF=1; CF=1; OF=1\n"
+      "run: storing 0x00000000\n"
+  );
+}
+
 //:
 
 :(code)
@@ -51,7 +192,7 @@ void test_add_imm32_to_mem_at_r32() {
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  81     03                          0a 0b 0c 0d \n"  // add 0x0d0c0b0a to *EBX
-      // ModR/M in binary: 00 (indirect mode) 000 (add imm32) 011 (dest EBX)
+      // ModR/M in binary: 00 (indirect mode) 000 (subop add) 011 (dest EBX)
       "== 0x2000\n"  // data segment
       "01 00 00 00\n"  // 0x00000001
   );
@@ -70,7 +211,7 @@ void test_add_imm32_to_mem_at_r32() {
 put_new(Name, "2d", "subtract imm32 from EAX (sub)");
 
 :(code)
-void test_subtract_imm32_from_eax() {
+void test_subtract_imm32_from_EAX() {
   Reg[EAX].i = 0x0d0c0baa;
   run(
       "== 0x1\n"  // code segment
@@ -85,22 +226,79 @@ void test_subtract_imm32_from_eax() {
 
 :(before "End Single-Byte Opcodes")
 case 0x2d: {  // subtract imm32 from EAX
-  const int32_t arg2 = next32();
-  trace(Callstack_depth+1, "run") << "subtract imm32 0x" << HEXWORD << arg2 << " from EAX" << end();
-  BINARY_ARITHMETIC_OP(-, Reg[EAX].i, arg2);
+  const int32_t signed_arg2 = next32();
+  trace(Callstack_depth+1, "run") << "subtract imm32 0x" << HEXWORD << signed_arg2 << " from EAX" << end();
+  int32_t signed_result = Reg[EAX].i - signed_arg2;
+  SF = (signed_result < 0);
+  ZF = (signed_result == 0);
+  int64_t signed_full_result = static_cast<int64_t>(Reg[EAX].i) - signed_arg2;
+  OF = (signed_result != signed_full_result);
+  // set CF
+  uint32_t unsigned_arg2 = static_cast<uint32_t>(signed_arg2);
+  uint32_t unsigned_result = Reg[EAX].u - unsigned_arg2;
+  uint64_t unsigned_full_result = static_cast<uint64_t>(Reg[EAX].u) - unsigned_arg2;
+  CF = (unsigned_result != unsigned_full_result);
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
+  Reg[EAX].i = signed_result;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << Reg[EAX].i << end();
   break;
 }
 
+:(code)
+void test_subtract_imm32_from_EAX_signed_overflow() {
+  Reg[EAX].i = 0x80000000;  // smallest negative signed integer
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  2d                                 ff ff ff 7f \n"  // subtract largest positive signed integer from EAX
+      // ModR/M in binary: 00 (indirect mode) 101 (subop subtract) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: subtract imm32 0x7fffffff from EAX\n"
+      "run: SF=0; ZF=0; CF=0; OF=1\n"
+      "run: storing 0x00000001\n"
+  );
+}
+
+void test_subtract_imm32_from_EAX_unsigned_overflow() {
+  Reg[EAX].i = 0;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  2d                                 01 00 00 00 \n"  // subtract 1 from EAX
+      // ModR/M in binary: 00 (indirect mode) 101 (subop subtract) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: subtract imm32 0x00000001 from EAX\n"
+      "run: SF=1; ZF=0; CF=1; OF=0\n"
+      "run: storing 0xffffffff\n"
+  );
+}
+
+void test_subtract_imm32_from_EAX_signed_and_unsigned_overflow() {
+  Reg[EAX].i = 0;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  2d                                 00 00 00 80 \n"  // subtract smallest negative signed integer from EAX
+      // ModR/M in binary: 00 (indirect mode) 101 (subop subtract) 011 (dest EBX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: subtract imm32 0x80000000 from EAX\n"
+      "run: SF=1; ZF=0; CF=1; OF=1\n"
+      "run: storing 0x80000000\n"
+  );
+}
+
 //:
 
-:(code)
 void test_subtract_imm32_from_mem_at_r32() {
   Reg[EBX].i = 0x2000;
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  81     2b                          01 00 00 00 \n"  // subtract 1 from *EBX
-      // ModR/M in binary: 00 (indirect mode) 101 (subtract imm32) 011 (dest EBX)
+      // ModR/M in binary: 00 (indirect mode) 101 (subop subtract) 011 (dest EBX)
       "== 0x2000\n"  // data segment
       "0a 00 00 00\n"  // 0x0000000a
   );
@@ -116,20 +314,96 @@ void test_subtract_imm32_from_mem_at_r32() {
 :(before "End Op 81 Subops")
 case 5: {
   trace(Callstack_depth+1, "run") << "subop subtract" << end();
-  BINARY_ARITHMETIC_OP(-, *arg1, arg2);
+  int32_t signed_result = *signed_arg1 - signed_arg2;
+  SF = (signed_result < 0);
+  ZF = (signed_result == 0);
+  int64_t signed_full_result = static_cast<int64_t>(*signed_arg1) - signed_arg2;
+  OF = (signed_result != signed_full_result);
+  // set CF
+  uint32_t unsigned_arg1 = static_cast<uint32_t>(*signed_arg1);
+  uint32_t unsigned_arg2 = static_cast<uint32_t>(signed_arg2);
+  uint32_t unsigned_result = unsigned_arg1 - unsigned_arg2;
+  uint64_t unsigned_full_result = static_cast<uint64_t>(unsigned_arg1) - unsigned_arg2;
+  CF = (unsigned_result != unsigned_full_result);
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
+  *signed_arg1 = signed_result;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << *signed_arg1 << end();
   break;
 }
 
+:(code)
+void test_subtract_imm32_from_mem_at_r32_signed_overflow() {
+  Reg[EBX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     2b                          ff ff ff 7f \n"  // subtract largest positive signed integer from *EBX
+      // ModR/M in binary: 00 (indirect mode) 101 (subop subtract) 011 (dest EBX)
+      "== 0x2000\n"  // data segment
+      "00 00 00 80\n"  // smallest negative signed integer
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: effective address is 0x00002000 (EBX)\n"
+      "run: effective address contains 80000000\n"
+      "run: imm32 is 0x7fffffff\n"
+      "run: subop subtract\n"
+      "run: SF=0; ZF=0; CF=0; OF=1\n"
+      "run: storing 0x00000001\n"
+  );
+}
+
+void test_subtract_imm32_from_mem_at_r32_unsigned_overflow() {
+  Reg[EBX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     2b                          01 00 00 00 \n"  // subtract 1 from *EBX
+      // ModR/M in binary: 00 (indirect mode) 101 (subop subtract) 011 (dest EBX)
+      "== 0x2000\n"  // data segment
+      "00 00 00 00\n"  // 0
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: effective address is 0x00002000 (EBX)\n"
+      "run: effective address contains 0\n"
+      "run: imm32 is 0x00000001\n"
+      "run: subop subtract\n"
+      "run: SF=1; ZF=0; CF=1; OF=0\n"
+      "run: storing 0xffffffff\n"
+  );
+}
+
+void test_subtract_imm32_from_mem_at_r32_signed_and_unsigned_overflow() {
+  Reg[EBX].i = 0x2000;
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  81     2b                          00 00 00 80 \n"  // subtract smallest negative signed integer from *EBX
+      // ModR/M in binary: 00 (indirect mode) 101 (subop subtract) 011 (dest EBX)
+      "== 0x2000\n"  // data segment
+      "00 00 00 00\n"  // 0
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: combine imm32 with r/m32\n"
+      "run: effective address is 0x00002000 (EBX)\n"
+      "run: effective address contains 0\n"
+      "run: imm32 is 0x80000000\n"
+      "run: subop subtract\n"
+      "run: SF=1; ZF=0; CF=1; OF=1\n"
+      "run: storing 0x80000000\n"
+  );
+}
+
 //:
 
-:(code)
 void test_subtract_imm32_from_r32() {
   Reg[EBX].i = 10;
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  81     eb                          01 00 00 00 \n"  // subtract 1 from EBX
-      // ModR/M in binary: 11 (direct mode) 101 (subtract imm32) 011 (dest EBX)
+      // ModR/M in binary: 11 (direct mode) 101 (subop subtract) 011 (dest EBX)
   );
   CHECK_TRACE_CONTENTS(
       "run: combine imm32 with r/m32\n"
@@ -338,7 +612,7 @@ void test_shift_right_logical_negative_r32_with_imm8() {
 put_new(Name, "25", "EAX = bitwise AND of imm32 with EAX (and)");
 
 :(code)
-void test_and_imm32_with_eax() {
+void test_and_EAX_with_imm32() {
   Reg[EAX].i = 0xff;
   run(
       "== 0x1\n"  // code segment
@@ -353,9 +627,17 @@ void test_and_imm32_with_eax() {
 
 :(before "End Single-Byte Opcodes")
 case 0x25: {  // and imm32 with EAX
-  const int32_t arg2 = next32();
-  trace(Callstack_depth+1, "run") << "and imm32 0x" << HEXWORD << arg2 << " with EAX" << end();
-  BINARY_BITWISE_OP(&, Reg[EAX].i, arg2);
+  // bitwise ops technically operate on unsigned numbers, but it makes no
+  // difference
+  const int32_t signed_arg2 = next32();
+  trace(Callstack_depth+1, "run") << "and imm32 0x" << HEXWORD << signed_arg2 << " with EAX" << end();
+  Reg[EAX].i &= signed_arg2;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << Reg[EAX].i << end();
+  SF = (Reg[EAX].i >> 31);
+  ZF = (Reg[EAX].i == 0);
+  CF = false;
+  OF = false;
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
   break;
 }
 
@@ -368,7 +650,7 @@ void test_and_imm32_with_mem_at_r32() {
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  81     23                          0a 0b 0c 0d \n"  // and 0x0d0c0b0a with *EBX
-      // ModR/M in binary: 00 (indirect mode) 100 (and imm32) 011 (dest EBX)
+      // ModR/M in binary: 00 (indirect mode) 100 (subop and) 011 (dest EBX)
       "== 0x2000\n"  // data segment
       "ff 00 00 00\n"  // 0x000000ff
   );
@@ -384,7 +666,15 @@ void test_and_imm32_with_mem_at_r32() {
 :(before "End Op 81 Subops")
 case 4: {
   trace(Callstack_depth+1, "run") << "subop and" << end();
-  BINARY_BITWISE_OP(&, *arg1, arg2);
+  // bitwise ops technically operate on unsigned numbers, but it makes no
+  // difference
+  *signed_arg1 &= signed_arg2;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << *signed_arg1 << end();
+  SF = (*signed_arg1 >> 31);
+  ZF = (*signed_arg1 == 0);
+  CF = false;
+  OF = false;
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
   break;
 }
 
@@ -397,7 +687,7 @@ void test_and_imm32_with_r32() {
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  81     e3                          0a 0b 0c 0d \n"  // and 0x0d0c0b0a with EBX
-      // ModR/M in binary: 11 (direct mode) 100 (and imm32) 011 (dest EBX)
+      // ModR/M in binary: 11 (direct mode) 100 (subop and) 011 (dest EBX)
   );
   CHECK_TRACE_CONTENTS(
       "run: combine imm32 with r/m32\n"
@@ -414,7 +704,7 @@ void test_and_imm32_with_r32() {
 put_new(Name, "0d", "EAX = bitwise OR of imm32 with EAX (or)");
 
 :(code)
-void test_or_imm32_with_eax() {
+void test_or_EAX_with_imm32() {
   Reg[EAX].i = 0xd0c0b0a0;
   run(
       "== 0x1\n"  // code segment
@@ -429,9 +719,17 @@ void test_or_imm32_with_eax() {
 
 :(before "End Single-Byte Opcodes")
 case 0x0d: {  // or imm32 with EAX
-  const int32_t arg2 = next32();
-  trace(Callstack_depth+1, "run") << "or imm32 0x" << HEXWORD << arg2 << " with EAX" << end();
-  BINARY_BITWISE_OP(|, Reg[EAX].i, arg2);
+  // bitwise ops technically operate on unsigned numbers, but it makes no
+  // difference
+  const int32_t signed_arg2 = next32();
+  trace(Callstack_depth+1, "run") << "or imm32 0x" << HEXWORD << signed_arg2 << " with EAX" << end();
+  Reg[EAX].i |= signed_arg2;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << Reg[EAX].i << end();
+  SF = (Reg[EAX].i >> 31);
+  ZF = (Reg[EAX].i == 0);
+  CF = false;
+  OF = false;
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
   break;
 }
 
@@ -444,7 +742,7 @@ void test_or_imm32_with_mem_at_r32() {
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  81     0b                          0a 0b 0c 0d \n"  // or 0x0d0c0b0a with *EBX
-      // ModR/M in binary: 00 (indirect mode) 001 (or imm32) 011 (dest EBX)
+      // ModR/M in binary: 00 (indirect mode) 001 (subop or) 011 (dest EBX)
       "== 0x2000\n"  // data segment
       "a0 b0 c0 d0\n"  // 0xd0c0b0a0
   );
@@ -460,7 +758,15 @@ void test_or_imm32_with_mem_at_r32() {
 :(before "End Op 81 Subops")
 case 1: {
   trace(Callstack_depth+1, "run") << "subop or" << end();
-  BINARY_BITWISE_OP(|, *arg1, arg2);
+  // bitwise ops technically operate on unsigned numbers, but it makes no
+  // difference
+  *signed_arg1 |= signed_arg2; \
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << *signed_arg1 << end(); \
+  SF = (*signed_arg1 >> 31); \
+  ZF = (*signed_arg1 == 0); \
+  CF = false; \
+  OF = false; \
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end(); \
   break;
 }
 
@@ -471,7 +777,7 @@ void test_or_imm32_with_r32() {
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  81     cb                          0a 0b 0c 0d \n"  // or 0x0d0c0b0a with EBX
-      // ModR/M in binary: 11 (direct mode) 001 (or imm32) 011 (dest EBX)
+      // ModR/M in binary: 11 (direct mode) 001 (subop or) 011 (dest EBX)
   );
   CHECK_TRACE_CONTENTS(
       "run: combine imm32 with r/m32\n"
@@ -488,7 +794,7 @@ void test_or_imm32_with_r32() {
 put_new(Name, "35", "EAX = bitwise XOR of imm32 with EAX (xor)");
 
 :(code)
-void test_xor_imm32_with_eax() {
+void test_xor_EAX_with_imm32() {
   Reg[EAX].i = 0xddccb0a0;
   run(
       "== 0x1\n"  // code segment
@@ -503,9 +809,17 @@ void test_xor_imm32_with_eax() {
 
 :(before "End Single-Byte Opcodes")
 case 0x35: {  // xor imm32 with EAX
-  const int32_t arg2 = next32();
-  trace(Callstack_depth+1, "run") << "xor imm32 0x" << HEXWORD << arg2 << " with EAX" << end();
-  BINARY_BITWISE_OP(^, Reg[EAX].i, arg2);
+  // bitwise ops technically operate on unsigned numbers, but it makes no
+  // difference
+  const int32_t signed_arg2 = next32();
+  trace(Callstack_depth+1, "run") << "xor imm32 0x" << HEXWORD << signed_arg2 << " with EAX" << end();
+  Reg[EAX].i ^= signed_arg2;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << Reg[EAX].i << end();
+  SF = (Reg[EAX].i >> 31);
+  ZF = (Reg[EAX].i == 0);
+  CF = false;
+  OF = false;
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
   break;
 }
 
@@ -518,7 +832,7 @@ void test_xor_imm32_with_mem_at_r32() {
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  81     33                          0a 0b 0c 0d \n"  // xor 0x0d0c0b0a with *EBX
-      // ModR/M in binary: 00 (indirect mode) 110 (xor imm32) 011 (dest EBX)
+      // ModR/M in binary: 00 (indirect mode) 110 (subop xor) 011 (dest EBX)
       "== 0x2000\n"  // data segment
       "a0 b0 c0 d0\n"  // 0xd0c0b0a0
   );
@@ -534,7 +848,15 @@ void test_xor_imm32_with_mem_at_r32() {
 :(before "End Op 81 Subops")
 case 6: {
   trace(Callstack_depth+1, "run") << "subop xor" << end();
-  BINARY_BITWISE_OP(^, *arg1, arg2);
+  // bitwise ops technically operate on unsigned numbers, but it makes no
+  // difference
+  *signed_arg1 ^= signed_arg2;
+  trace(Callstack_depth+1, "run") << "storing 0x" << HEXWORD << *signed_arg1 << end();
+  SF = (*signed_arg1 >> 31);
+  ZF = (*signed_arg1 == 0);
+  CF = false;
+  OF = false;
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
   break;
 }
 
@@ -545,7 +867,7 @@ void test_xor_imm32_with_r32() {
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  81     f3                          0a 0b 0c 0d \n"  // xor 0x0d0c0b0a with EBX
-      // ModR/M in binary: 11 (direct mode) 110 (xor imm32) 011 (dest EBX)
+      // ModR/M in binary: 11 (direct mode) 110 (subop xor) 011 (dest EBX)
   );
   CHECK_TRACE_CONTENTS(
       "run: combine imm32 with r/m32\n"
@@ -562,49 +884,96 @@ void test_xor_imm32_with_r32() {
 put_new(Name, "3d", "compare: set SF if EAX < imm32 (cmp)");
 
 :(code)
-void test_compare_imm32_with_eax_greater() {
+void test_compare_EAX_with_imm32_greater() {
   Reg[EAX].i = 0x0d0c0b0a;
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
-      "  3d                                 07 0b 0c 0d \n"  // compare 0x0d0c0b07 with EAX
+      "  3d                                 07 0b 0c 0d \n"  // compare EAX with 0x0d0c0b07
   );
   CHECK_TRACE_CONTENTS(
-      "run: compare EAX and imm32 0x0d0c0b07\n"
-      "run: SF=0; ZF=0; OF=0\n"
+      "run: compare EAX with imm32 0x0d0c0b07\n"
+      "run: SF=0; ZF=0; CF=0; OF=0\n"
   );
 }
 
 :(before "End Single-Byte Opcodes")
 case 0x3d: {  // compare EAX with imm32
-  const int32_t arg1 = Reg[EAX].i;
-  const int32_t arg2 = next32();
-  trace(Callstack_depth+1, "run") << "compare EAX and imm32 0x" << HEXWORD << arg2 << end();
-  const int32_t tmp1 = arg1 - arg2;
-  SF = (tmp1 < 0);
-  ZF = (tmp1 == 0);
-  const int64_t tmp2 = arg1 - arg2;
-  OF = (tmp1 != tmp2);
-  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; OF=" << OF << end();
+  const int32_t signed_arg1 = Reg[EAX].i;
+  const int32_t signed_arg2 = next32();
+  trace(Callstack_depth+1, "run") << "compare EAX with imm32 0x" << HEXWORD << signed_arg2 << end();
+  const int32_t signed_difference = signed_arg1 - signed_arg2;
+  SF = (signed_difference < 0);
+  ZF = (signed_difference == 0);
+  const int64_t full_signed_difference = static_cast<int64_t>(signed_arg1) - signed_arg2;
+  OF = (signed_difference != full_signed_difference);
+  const uint32_t unsigned_arg1 = static_cast<uint32_t>(signed_arg1);
+  const uint32_t unsigned_arg2 = static_cast<uint32_t>(signed_arg2);
+  const uint32_t unsigned_difference = unsigned_arg1 - unsigned_arg2;
+  const uint64_t full_unsigned_difference = static_cast<uint64_t>(unsigned_arg1) - unsigned_arg2;
+  CF = (unsigned_difference != full_unsigned_difference);
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
   break;
 }
 
 :(code)
-void test_compare_imm32_with_eax_lesser() {
-  Reg[EAX].i = 0x0d0c0b07;
+void test_compare_EAX_with_imm32_lesser_unsigned_and_signed() {
+  Reg[EAX].i = 0x0a0b0c07;
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
-      "  3d                                 0a 0b 0c 0d \n"  // compare 0x0d0c0b0a with EAX
+      "  3d                                 0d 0c 0b 0a \n"  // compare EAX with imm32
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
   );
   CHECK_TRACE_CONTENTS(
-      "run: compare EAX and imm32 0x0d0c0b0a\n"
-      "run: SF=1; ZF=0; OF=0\n"
+      "run: compare EAX with imm32 0x0a0b0c0d\n"
+      "run: SF=1; ZF=0; CF=1; OF=0\n"
   );
 }
 
-:(code)
-void test_compare_imm32_with_eax_equal() {
+void test_compare_EAX_with_imm32_lesser_unsigned_and_signed_due_to_overflow() {
+  Reg[EAX].i = 0x7fffffff;  // largest positive signed integer
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  3d                                 00 00 00 80\n"  // compare EAX with smallest negative signed integer
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare EAX with imm32 0x80000000\n"
+      "run: SF=1; ZF=0; CF=1; OF=1\n"
+  );
+}
+
+void test_compare_EAX_with_imm32_lesser_signed() {
+  Reg[EAX].i = 0xffffffff;  // -1
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  3d                                 01 00 00 00\n"  // compare EAX with 1
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare EAX with imm32 0x00000001\n"
+      "run: SF=1; ZF=0; CF=0; OF=0\n"
+  );
+}
+
+void test_compare_EAX_with_imm32_lesser_unsigned() {
+  Reg[EAX].i = 0x00000001;  // 1
+  run(
+      "== 0x1\n"  // code segment
+      // op     ModR/M  SIB   displacement  immediate
+      "  3d                                 ff ff ff ff\n"  // compare EAX with -1
+      // ModR/M in binary: 11 (direct mode) 011 (src EBX) 000 (dest EAX)
+  );
+  CHECK_TRACE_CONTENTS(
+      "run: compare EAX with imm32 0xffffffff\n"
+      "run: SF=0; ZF=0; CF=1; OF=0\n"
+  );
+}
+
+void test_compare_EAX_with_imm32_equal() {
   Reg[EAX].i = 0x0d0c0b0a;
   run(
       "== 0x1\n"  // code segment
@@ -612,39 +981,38 @@ void test_compare_imm32_with_eax_equal() {
       "  3d                                 0a 0b 0c 0d \n"  // compare 0x0d0c0b0a with EAX
   );
   CHECK_TRACE_CONTENTS(
-      "run: compare EAX and imm32 0x0d0c0b0a\n"
-      "run: SF=0; ZF=1; OF=0\n"
+      "run: compare EAX with imm32 0x0d0c0b0a\n"
+      "run: SF=0; ZF=1; CF=0; OF=0\n"
   );
 }
 
 //:
 
-:(code)
 void test_compare_imm32_with_r32_greater() {
   Reg[EBX].i = 0x0d0c0b0a;
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  81     fb                          07 0b 0c 0d \n"  // compare 0x0d0c0b07 with EBX
-      // ModR/M in binary: 11 (direct mode) 111 (compare imm32) 011 (dest EBX)
+      // ModR/M in binary: 11 (direct mode) 111 (subop compare) 011 (dest EBX)
   );
   CHECK_TRACE_CONTENTS(
       "run: combine imm32 with r/m32\n"
       "run: r/m32 is EBX\n"
       "run: imm32 is 0x0d0c0b07\n"
-      "run: SF=0; ZF=0; OF=0\n"
+      "run: SF=0; ZF=0; CF=0; OF=0\n"
   );
 }
 
 :(before "End Op 81 Subops")
 case 7: {
   trace(Callstack_depth+1, "run") << "subop compare" << end();
-  const int32_t tmp1 = *arg1 - arg2;
+  const int32_t tmp1 = *signed_arg1 - signed_arg2;
   SF = (tmp1 < 0);
   ZF = (tmp1 == 0);
-  const int64_t tmp2 = *arg1 - arg2;
+  const int64_t tmp2 = static_cast<int64_t>(*signed_arg1) - signed_arg2;
   OF = (tmp1 != tmp2);
-  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; OF=" << OF << end();
+  trace(Callstack_depth+1, "run") << "SF=" << SF << "; ZF=" << ZF << "; CF=" << CF << "; OF=" << OF << end();
   break;
 }
 
@@ -655,13 +1023,13 @@ void test_compare_imm32_with_r32_lesser() {
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  81     fb                          0a 0b 0c 0d \n"  // compare 0x0d0c0b0a with EBX
-      // ModR/M in binary: 11 (direct mode) 111 (compare imm32) 011 (dest EBX)
+      // ModR/M in binary: 11 (direct mode) 111 (subop compare) 011 (dest EBX)
   );
   CHECK_TRACE_CONTENTS(
       "run: combine imm32 with r/m32\n"
       "run: r/m32 is EBX\n"
       "run: imm32 is 0x0d0c0b0a\n"
-      "run: SF=1; ZF=0; OF=0\n"
+      "run: SF=1; ZF=0; CF=0; OF=0\n"
   );
 }
 
@@ -672,13 +1040,13 @@ void test_compare_imm32_with_r32_equal() {
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  81     fb                          0a 0b 0c 0d \n"  // compare 0x0d0c0b0a with EBX
-      // ModR/M in binary: 11 (direct mode) 111 (compare imm32) 011 (dest EBX)
+      // ModR/M in binary: 11 (direct mode) 111 (subop compare) 011 (dest EBX)
   );
   CHECK_TRACE_CONTENTS(
       "run: combine imm32 with r/m32\n"
       "run: r/m32 is EBX\n"
       "run: imm32 is 0x0d0c0b0a\n"
-      "run: SF=0; ZF=1; OF=0\n"
+      "run: SF=0; ZF=1; CF=0; OF=0\n"
   );
 }
 
@@ -689,7 +1057,7 @@ void test_compare_imm32_with_mem_at_r32_greater() {
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  81     3b                          07 0b 0c 0d \n"  // compare 0x0d0c0b07 with *EBX
-      // ModR/M in binary: 00 (indirect mode) 111 (compare imm32) 011 (dest EBX)
+      // ModR/M in binary: 00 (indirect mode) 111 (subop compare) 011 (dest EBX)
       "== 0x2000\n"  // data segment
       "0a 0b 0c 0d\n"  // 0x0d0c0b0a
   );
@@ -697,7 +1065,7 @@ void test_compare_imm32_with_mem_at_r32_greater() {
       "run: combine imm32 with r/m32\n"
       "run: effective address is 0x00002000 (EBX)\n"
       "run: imm32 is 0x0d0c0b07\n"
-      "run: SF=0; ZF=0; OF=0\n"
+      "run: SF=0; ZF=0; CF=0; OF=0\n"
   );
 }
 
@@ -708,7 +1076,7 @@ void test_compare_imm32_with_mem_at_r32_lesser() {
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  81     3b                          0a 0b 0c 0d \n"  // compare 0x0d0c0b0a with *EBX
-      // ModR/M in binary: 00 (indirect mode) 111 (compare imm32) 011 (dest EBX)
+      // ModR/M in binary: 00 (indirect mode) 111 (subop compare) 011 (dest EBX)
       "== 0x2000\n"  // data segment
       "07 0b 0c 0d\n"  // 0x0d0c0b07
   );
@@ -716,7 +1084,7 @@ void test_compare_imm32_with_mem_at_r32_lesser() {
       "run: combine imm32 with r/m32\n"
       "run: effective address is 0x00002000 (EBX)\n"
       "run: imm32 is 0x0d0c0b0a\n"
-      "run: SF=1; ZF=0; OF=0\n"
+      "run: SF=1; ZF=0; CF=0; OF=0\n"
   );
 }
 
@@ -728,7 +1096,7 @@ void test_compare_imm32_with_mem_at_r32_equal() {
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
       "  81     3b                          0a 0b 0c 0d \n"  // compare 0x0d0c0b0a with *EBX
-      // ModR/M in binary: 00 (indirect mode) 111 (compare imm32) 011 (dest EBX)
+      // ModR/M in binary: 00 (indirect mode) 111 (subop compare) 011 (dest EBX)
       "== 0x2000\n"  // data segment
       "0a 0b 0c 0d\n"  // 0x0d0c0b0a
   );
@@ -736,14 +1104,14 @@ void test_compare_imm32_with_mem_at_r32_equal() {
       "run: combine imm32 with r/m32\n"
       "run: effective address is 0x00002000 (EBX)\n"
       "run: imm32 is 0x0d0c0b0a\n"
-      "run: SF=0; ZF=1; OF=0\n"
+      "run: SF=0; ZF=1; CF=0; OF=0\n"
   );
 }
 
 //:: copy (mov)
 
 :(before "End Initialize Op Names")
-put_new(Name, "b8", "copy imm32 to EAX (mov)");
+// b8 defined earlier to copy imm32 to EAX
 put_new(Name, "b9", "copy imm32 to ECX (mov)");
 put_new(Name, "ba", "copy imm32 to EDX (mov)");
 put_new(Name, "bb", "copy imm32 to EBX (mov)");
@@ -765,7 +1133,6 @@ void test_copy_imm32_to_r32() {
 }
 
 :(before "End Single-Byte Opcodes")
-case 0xb8:
 case 0xb9:
 case 0xba:
 case 0xbb:
@@ -824,7 +1191,8 @@ put_new(Name, "68", "push imm32 to stack (push)");
 
 :(code)
 void test_push_imm32() {
-  Reg[ESP].u = 0x14;
+  Mem.push_back(vma(0xbd000000));  // manually allocate memory
+  Reg[ESP].u = 0xbd000014;
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
@@ -832,7 +1200,7 @@ void test_push_imm32() {
   );
   CHECK_TRACE_CONTENTS(
       "run: push imm32 0x000000af\n"
-      "run: ESP is now 0x00000010\n"
+      "run: ESP is now 0xbd000010\n"
       "run: contents at ESP: 0x000000af\n"
   );
 }
diff --git a/subx/017jump_disp8.cc b/subx/017jump_disp8.cc
index 22ae6567..35cc1331 100644
--- a/subx/017jump_disp8.cc
+++ b/subx/017jump_disp8.cc
@@ -135,7 +135,8 @@ void test_jne_rel8_fail() {
 //:: jump if greater
 
 :(before "End Initialize Op Names")
-put_new(Name, "7f", "jump disp8 bytes away if greater, if ZF is unset and SF == OF (jcc/jg/jnle)");
+put_new(Name, "7f", "jump disp8 bytes away if greater (signed), if ZF is unset and SF == OF (jcc/jg/jnle)");
+put_new(Name, "77", "jump disp8 bytes away if greater (unsigned), if ZF is unset and CF is unset (jcc/ja/jnbe)");
 
 :(code)
 void test_jg_rel8_success() {
@@ -158,9 +159,17 @@ void test_jg_rel8_success() {
 }
 
 :(before "End Single-Byte Opcodes")
-case 0x7f: {  // jump rel8 if !SF and !ZF
+case 0x7f: {  // jump rel8 if SF == OF and !ZF
   const int8_t offset = static_cast<int>(next());
-  if (!ZF && SF == OF) {
+  if (SF == OF && !ZF) {
+    trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
+    EIP += offset;
+  }
+  break;
+}
+case 0x77: {  // jump rel8 if !CF and !ZF
+  const int8_t offset = static_cast<int>(next());
+  if (!CF && !ZF) {
     trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
     EIP += offset;
   }
@@ -190,7 +199,8 @@ void test_jg_rel8_fail() {
 //:: jump if greater or equal
 
 :(before "End Initialize Op Names")
-put_new(Name, "7d", "jump disp8 bytes away if greater or equal, if SF == OF (jcc/jge/jnl)");
+put_new(Name, "7d", "jump disp8 bytes away if greater or equal (signed), if SF == OF (jcc/jge/jnl)");
+put_new(Name, "73", "jump disp8 bytes away if greater or equal (unsigned), if CF is unset (jcc/jae/jnb)");
 
 :(code)
 void test_jge_rel8_success() {
@@ -212,7 +222,7 @@ void test_jge_rel8_success() {
 }
 
 :(before "End Single-Byte Opcodes")
-case 0x7d: {  // jump rel8 if !SF
+case 0x7d: {  // jump rel8 if SF == OF
   const int8_t offset = static_cast<int>(next());
   if (SF == OF) {
     trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
@@ -220,6 +230,14 @@ case 0x7d: {  // jump rel8 if !SF
   }
   break;
 }
+case 0x73: {  // jump rel8 if !CF
+  const int8_t offset = static_cast<int>(next());
+  if (!CF) {
+    trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
+    EIP += offset;
+  }
+  break;
+}
 
 :(code)
 void test_jge_rel8_fail() {
@@ -243,7 +261,8 @@ void test_jge_rel8_fail() {
 //:: jump if lesser
 
 :(before "End Initialize Op Names")
-put_new(Name, "7c", "jump disp8 bytes away if lesser, if SF != OF (jcc/jl/jnge)");
+put_new(Name, "7c", "jump disp8 bytes away if lesser (signed), if SF != OF (jcc/jl/jnge)");
+put_new(Name, "72", "jump disp8 bytes away if lesser (unsigned), if CF is set (jcc/jb/jnae)");
 
 :(code)
 void test_jl_rel8_success() {
@@ -266,7 +285,7 @@ void test_jl_rel8_success() {
 }
 
 :(before "End Single-Byte Opcodes")
-case 0x7c: {  // jump rel8 if SF and !ZF
+case 0x7c: {  // jump rel8 if SF != OF
   const int8_t offset = static_cast<int>(next());
   if (SF != OF) {
     trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
@@ -274,6 +293,14 @@ case 0x7c: {  // jump rel8 if SF and !ZF
   }
   break;
 }
+case 0x72: {  // jump rel8 if CF
+  const int8_t offset = static_cast<int>(next());
+  if (CF) {
+    trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
+    EIP += offset;
+  }
+  break;
+}
 
 :(code)
 void test_jl_rel8_fail() {
@@ -298,7 +325,8 @@ void test_jl_rel8_fail() {
 //:: jump if lesser or equal
 
 :(before "End Initialize Op Names")
-put_new(Name, "7e", "jump disp8 bytes away if lesser or equal, if ZF is set or SF != OF (jcc/jle/jng)");
+put_new(Name, "7e", "jump disp8 bytes away if lesser or equal (signed), if ZF is set or SF != OF (jcc/jle/jng)");
+put_new(Name, "76", "jump disp8 bytes away if lesser or equal (unsigned), if ZF is set or CF is set (jcc/jbe/jna)");
 
 :(code)
 void test_jle_rel8_equal() {
@@ -341,7 +369,7 @@ void test_jle_rel8_lesser() {
 }
 
 :(before "End Single-Byte Opcodes")
-case 0x7e: {  // jump rel8 if SF or ZF
+case 0x7e: {  // jump rel8 if ZF or SF != OF
   const int8_t offset = static_cast<int>(next());
   if (ZF || SF != OF) {
     trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
@@ -349,6 +377,14 @@ case 0x7e: {  // jump rel8 if SF or ZF
   }
   break;
 }
+case 0x76: {  // jump rel8 if ZF or CF
+  const int8_t offset = static_cast<int>(next());
+  if (ZF || CF) {
+    trace(Callstack_depth+1, "run") << "jump " << NUM(offset) << end();
+    EIP += offset;
+  }
+  break;
+}
 
 :(code)
 void test_jle_rel8_greater() {
diff --git a/subx/019functions.cc b/subx/019functions.cc
index 7f45167b..00da8397 100644
--- a/subx/019functions.cc
+++ b/subx/019functions.cc
@@ -5,7 +5,8 @@ put_new(Name, "e8", "call disp32 (call)");
 
 :(code)
 void test_call_disp32() {
-  Reg[ESP].u = 0x64;
+  Mem.push_back(vma(0xbd000000));  // manually allocate memory
+  Reg[ESP].u = 0xbd000064;
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
@@ -14,7 +15,7 @@ void test_call_disp32() {
   );
   CHECK_TRACE_CONTENTS(
       "run: call imm32 0x000000a0\n"
-      "run: decrementing ESP to 0x00000060\n"
+      "run: decrementing ESP to 0xbd000060\n"
       "run: pushing value 0x00000006\n"
       "run: jumping to 0x000000a6\n"
   );
@@ -36,7 +37,8 @@ case 0xe8: {  // call disp32 relative to next EIP
 
 :(code)
 void test_call_r32() {
-  Reg[ESP].u = 0x64;
+  Mem.push_back(vma(0xbd000000));  // manually allocate memory
+  Reg[ESP].u = 0xbd000064;
   Reg[EBX].u = 0x000000a0;
   run(
       "== 0x1\n"  // code segment
@@ -47,7 +49,7 @@ void test_call_r32() {
   CHECK_TRACE_CONTENTS(
       "run: call to r/m32\n"
       "run: r/m32 is EBX\n"
-      "run: decrementing ESP to 0x00000060\n"
+      "run: decrementing ESP to 0xbd000060\n"
       "run: pushing value 0x00000003\n"
       "run: jumping to 0x000000a3\n"
   );
@@ -66,7 +68,8 @@ case 2: {  // call function pointer at r/m32
 
 :(code)
 void test_call_mem_at_r32() {
-  Reg[ESP].u = 0x64;
+  Mem.push_back(vma(0xbd000000));  // manually allocate memory
+  Reg[ESP].u = 0xbd000064;
   Reg[EBX].u = 0x2000;
   run(
       "== 0x1\n"  // code segment
@@ -79,7 +82,7 @@ void test_call_mem_at_r32() {
   CHECK_TRACE_CONTENTS(
       "run: call to r/m32\n"
       "run: effective address is 0x00002000 (EBX)\n"
-      "run: decrementing ESP to 0x00000060\n"
+      "run: decrementing ESP to 0xbd000060\n"
       "run: pushing value 0x00000003\n"
       "run: jumping to 0x000000a3\n"
   );
@@ -92,7 +95,9 @@ put_new(Name, "c3", "return from most recent unfinished call (ret)");
 
 :(code)
 void test_ret() {
-  Reg[ESP].u = 0x2000;
+  Mem.push_back(vma(0xbd000000));  // manually allocate memory
+  Reg[ESP].u = 0xbd000064;
+  write_mem_u32(Reg[ESP].u, 0x10);
   run(
       "== 0x1\n"  // code segment
       // op     ModR/M  SIB   displacement  immediate
diff --git a/subx/020syscalls.cc b/subx/020syscalls.cc
index 6b9faa2c..eb8ebcce 100644
--- a/subx/020syscalls.cc
+++ b/subx/020syscalls.cc
@@ -111,16 +111,19 @@ void check_mode(int reg) {
 
 :(before "End Globals")
 // Very primitive/fixed/insecure mmap segments for now.
-// For now we avoid addresses with the most significant bit set; SubX doesn't
-// support unsigned comparison yet (https://github.com/akkartik/mu/issues/30)
-// Once we do, we can go up to 0xc0000000; higher addresses are reserved for
-// the Linux kernel.
-uint32_t Next_segment = 0x7c000000;
+uint32_t Segments_allocated_above = END_HEAP;
 const uint32_t SPACE_FOR_SEGMENT = 0x01000000;
 :(code)
+// always allocate multiples of the segment size
 uint32_t new_segment(uint32_t length) {
-  uint32_t result = Next_segment;
-  Mem.push_back(vma(Next_segment, Next_segment+length));
-  Next_segment -= SPACE_FOR_SEGMENT;
+  assert(length > 0);
+  uint32_t result = (Segments_allocated_above - length) & 0xff000000;
+  if (result <= START_HEAP) {
+    raise << "Allocated too many segments; the VM ran out of memory. "
+          << "Maybe SPACE_FOR_SEGMENT can be smaller?\n" << end();
+    exit(1);
+  }
+  Mem.push_back(vma(result, result+length));
+  Segments_allocated_above = result;
   return result;
 }
diff --git a/subx/031check_operands.cc b/subx/031check_operands.cc
index 9590979f..aad84da6 100644
--- a/subx/031check_operands.cc
+++ b/subx/031check_operands.cc
@@ -152,8 +152,12 @@ void init_permitted_operands() {
 
   // jump
   put(Permitted_operands, "eb", 0x04);
+  put(Permitted_operands, "72", 0x04);
+  put(Permitted_operands, "73", 0x04);
   put(Permitted_operands, "74", 0x04);
   put(Permitted_operands, "75", 0x04);
+  put(Permitted_operands, "76", 0x04);
+  put(Permitted_operands, "77", 0x04);
   put(Permitted_operands, "7c", 0x04);
   put(Permitted_operands, "7d", 0x04);
   put(Permitted_operands, "7e", 0x04);
diff --git a/subx/034compute_segment_address.cc b/subx/034compute_segment_address.cc
index a1b7482d..47311219 100644
--- a/subx/034compute_segment_address.cc
+++ b/subx/034compute_segment_address.cc
@@ -14,7 +14,7 @@ void test_segment_name() {
       "load: 0x09000056 -> 0b\n"
       "load: 0x09000057 -> 0c\n"
       "load: 0x09000058 -> 0d\n"
-      "run: add imm32 0x0d0c0b0a to reg EAX\n"
+      "run: add imm32 0x0d0c0b0a to EAX\n"
       "run: storing 0x0d0c0b0a\n"
   );
 }
diff --git a/subx/040---tests.cc b/subx/040---tests.cc
index d35cc711..e5949bbd 100644
--- a/subx/040---tests.cc
+++ b/subx/040---tests.cc
@@ -16,7 +16,8 @@ Transform.push_back(create_test_function);
 
 :(code)
 void test_run_test() {
-  Reg[ESP].u = 0x100;
+  Mem.push_back(vma(0xbd000000));  // manually allocate memory
+  Reg[ESP].u = 0xbd000100;
   run(
       "== 0x1\n"  // code segment
       "main:\n"
diff --git a/subx/050_write.subx b/subx/050_write.subx
index 083adad6..0d6b8152 100644
--- a/subx/050_write.subx
+++ b/subx/050_write.subx
@@ -32,6 +32,9 @@ _write:  # fd : int, s : (address array byte) -> <void>
     # . syscall
     b8/copy-to-EAX  4/imm32/write
     cd/syscall  0x80/imm8
+    # if (EAX < 0) abort
+    3d/compare-EAX-with  0/imm32
+    0f 8c/jump-if-lesser  $_write:abort/disp32
 $_write:end:
     # . restore registers
     5b/pop-to-EBX
@@ -43,4 +46,12 @@ $_write:end:
     5d/pop-to-EBP
     c3/return
 
+$_write:abort:
+    # can't write a message here for risk of an infinite loop, so we'll use a special exit code instead
+    # . syscall(exit, 255)
+    bb/copy-to-EBX  0xff/imm32
+    b8/copy-to-EAX  1/imm32/exit
+    cd/syscall  0x80/imm8
+    # never gets here
+
 # . . vim:nowrap:textwidth=0
diff --git a/subx/057write.subx b/subx/057write.subx
index 3135003b..455146ac 100644
--- a/subx/057write.subx
+++ b/subx/057write.subx
@@ -27,7 +27,7 @@ write:  # f : fd or (address stream), s : (address array byte) -> <void>
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
     # if (f < 0x08000000) _write(f, s) and return  # f can't be a user-mode address, so treat it as a kernel file descriptor
     81          7/subop/compare     1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         0x08000000/imm32  # compare *(EBP+8)
-    7d/jump-if-greater-or-equal  $write:fake/disp8
+    73/jump-if-greater-unsigned-or-equal  $write:fake/disp8
     # . . push args
     ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
     ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
diff --git a/subx/060read.subx b/subx/060read.subx
index cedafbf5..d377a1ad 100644
--- a/subx/060read.subx
+++ b/subx/060read.subx
@@ -51,7 +51,7 @@ read:  # f : fd or (address stream), s : (address stream) -> num-bytes-read/EAX
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
     # if (f < 0x08000000) return _read(f, s)  # f can't be a user-mode address, so treat it as a kernel file descriptor
     81          7/subop/compare     1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         0x08000000/imm32  # compare *(EBP+8)
-    7d/jump-if-greater-or-equal  $read:fake/disp8
+    73/jump-if-greater-unsigned-or-equal  $read:fake/disp8
     # . . push args
     ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
     ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
diff --git a/subx/062write-stream.subx b/subx/062write-stream.subx
index 73766fe3..92c67dc2 100644
--- a/subx/062write-stream.subx
+++ b/subx/062write-stream.subx
@@ -21,7 +21,7 @@ write-stream:  # f : fd or (address stream), s : (address stream) -> <void>
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
     # if (f < 0x08000000) _write-stream(f, s), return  # f can't be a user-mode address, so treat it as a kernel file descriptor
     81          7/subop/compare     1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         0x08000000/imm32  # compare *(EBP+8)
-    7d/jump-if-greater-or-equal  $write-stream:fake/disp8
+    73/jump-if-greater-unsigned-or-equal  $write-stream:fake/disp8
     # . . push args
     ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           0xc/disp8       .                 # push *(EBP+12)
     ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
@@ -102,6 +102,11 @@ _write-stream:  # fd : int, s : (address stream) -> <void>
     # . . syscall
     b8/copy-to-EAX  4/imm32/write
     cd/syscall  0x80/imm8
+    # if (EAX < 0) abort
+    3d/compare-EAX-with  0/imm32
+    0f 8c/jump-if-lesser  $_write-stream:abort/disp32
+    # s->read += EAX
+    01/add                          1/mod/*+disp8   6/rm32/ESI    .           .             .           0/r32/EAX   4/disp8         .                 # add EAX to *(ESI+4)
     # . restore registers
     5f/pop-to-EDI
     5e/pop-to-ESI
@@ -114,6 +119,21 @@ _write-stream:  # fd : int, s : (address stream) -> <void>
     5d/pop-to-EBP
     c3/return
 
+$_write-stream:abort:
+    # . _write(2/stderr, error)
+    # . . push args
+    68/push  "_write-stream: failed to write to file"/imm32
+    68/push  2/imm32/stderr
+    # . . call
+    e8/call  _write/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # . syscall(exit, 1)
+    bb/copy-to-EBX  1/imm32
+    b8/copy-to-EAX  1/imm32/exit
+    cd/syscall  0x80/imm8
+    # never gets here
+
 test-write-stream-single:
     # setup
     # . clear-stream(_test-stream)
diff --git a/subx/066print-byte.subx b/subx/066print-int.subx
index 80a29a0c..0715b414 100644
--- a/subx/066print-byte.subx
+++ b/subx/066print-int.subx
@@ -1,10 +1,17 @@
-# Print the (hex) textual representation of the lowest byte of a number.
+# Print the (hex) textual representation of numbers.
 
 == code
 #   instruction                     effective address                                                   register    displacement    immediate
 # . op          subop               mod             rm32          base        index         scale       r32
 # . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
 
+#? Entry:  # run a single test, while debugging
+#?     e8/call test-print-int32-buffered/disp32
+#?     # syscall(exit, Num-test-failures)
+#?     8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           3/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
+#?     b8/copy-to-EAX  1/imm32/exit
+#?     cd/syscall  0x80/imm8
+
 append-byte-hex:  # f : (address stream), n : int -> <void>
     # . prolog
     55/push-EBP
@@ -76,6 +83,7 @@ test-append-byte-hex:
     # . end
     c3/return
 
+# print the hex representation for the lowest byte of a number
 print-byte-buffered:  # f : (address buffered-file), n : int -> <void>
     # . prolog
     55/push-EBP
@@ -163,4 +171,116 @@ test-print-byte-buffered:
     # . end
     c3/return
 
+print-int32-buffered:  # f : (address buffered-file), n : int -> <void>
+    # pseudocode:
+    #  ECX = 28
+    #  while true
+    #    if (ECX < 0) break
+    #    EAX = n >> ECX
+    #    EAX = EAX & 0xf
+    #    write-byte-buffered(f, AL)
+    #    ECX -= 4
+    #
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # . save registers
+    50/push-EAX
+    51/push-ECX
+    # ECX = 28
+    b9/copy-to-ECX  0x1c/imm32
+$print-int32-buffered:loop:
+    # if (ECX < 0) break
+    81          7/subop/compare     3/mod/direct    1/rm32/ECX    .           .             .           .           .               0/imm32           # compare ECX
+    7c/jump-if-lesser  $print-int32-buffered:end/disp8
+    # EAX = n >> ECX
+    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           0/r32/EAX   0xc/disp8       .                 # copy *(EBP+12) to EAX
+    d3/>>ECX    5/subop/pad-zeroes  3/mod/direct    0/rm32/EAX    .           .             .           .           .               .                 # shift EAX right by ECX bits, padding zeroes
+    # EAX = to-hex-char(AL)
+    25/and-EAX  0xf/imm32
+    e8/call  to-hex-char/disp32
+    # write-byte-buffered(f, AL)
+    # . . push args
+    50/push-EAX
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+    # . . call
+    e8/call  write-byte-buffered/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # ECX -= 4
+    81          5/subop/subtract    3/mod/direct    1/rm32/ECX    .           .             .           .           .               4/imm32           # subtract from ECX
+    eb/jump  $print-int32-buffered:loop/disp8
+$print-int32-buffered:end:
+    # . restore registers
+    59/pop-to-ECX
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-print-int32-buffered:
+    # - check that print-int32-buffered prints the hex textual representation
+    # setup
+    # . clear-stream(_test-stream)
+    # . . push args
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # print-int32-buffered(_test-buffered-file, 0x8899aa)
+    # . . push args
+    68/push  0x8899aa/imm32
+    68/push  _test-buffered-file/imm32
+    # . . call
+    e8/call  print-int32-buffered/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # flush(_test-buffered-file)
+    # . . push args
+    68/push  _test-buffered-file/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+#?     # dump line {{{
+#?     # . write-stream(2/stderr, line)
+#?     # . . push args
+#?     68/push  _test-stream/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write-stream/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # . write(2/stderr, "$\n")
+#?     # . . push args
+#?     68/push  "$\n"/imm32
+#?     68/push  2/imm32/stderr
+#?     # . . call
+#?     e8/call  write/disp32
+#?     # . . discard args
+#?     81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+#?     # }}}
+    # check-stream-equal(_test-stream, "008899aa", msg)
+    # . . push args
+    68/push  "F - test-print-int32-buffered"/imm32
+    68/push  "008899aa"/imm32
+    68/push  _test-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . end
+    c3/return
+
 # . . vim:nowrap:textwidth=0
diff --git a/subx/apps/assort b/subx/apps/assort
index fb24828b..1cd0b6cf 100755
--- a/subx/apps/assort
+++ b/subx/apps/assort
Binary files differdiff --git a/subx/apps/crenshaw2-1 b/subx/apps/crenshaw2-1
index 2ffc8a84..03d3d0c3 100755
--- a/subx/apps/crenshaw2-1
+++ b/subx/apps/crenshaw2-1
Binary files differdiff --git a/subx/apps/crenshaw2-1b b/subx/apps/crenshaw2-1b
index 6144d591..60521cb0 100755
--- a/subx/apps/crenshaw2-1b
+++ b/subx/apps/crenshaw2-1b
Binary files differdiff --git a/subx/apps/dquotes b/subx/apps/dquotes
index a91f829d..c180c114 100644..100755
--- a/subx/apps/dquotes
+++ b/subx/apps/dquotes
Binary files differdiff --git a/subx/apps/dquotes.subx b/subx/apps/dquotes.subx
index e6b24698..5f98d295 100644
--- a/subx/apps/dquotes.subx
+++ b/subx/apps/dquotes.subx
@@ -112,6 +112,8 @@ convert:  # in : (address buffered-file), out : (address buffered-file) -> <void
     #         process-string-literal(word-slice, out, new-data-segment)
     #       else
     #         write-slice-buffered(out, word-slice)
+    #       write(out, " ")
+    #     write(out, "\n\n")
     #   write-stream-data(out, new-data-segment)
     #   flush(out)
     #
@@ -1223,9 +1225,227 @@ emit-metadata:  # out : (address buffered-file), word : (address slice)
     55/push-EBP
     89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
     # . save registers
+    50/push-EAX
+    51/push-ECX
+    52/push-EDX
+
+    # PSEUDOCODE
+    # ECX = (char *) word->start
+    # while true:
+    #   if ECX == word->end: return
+    #   if *(ECX++) == '/': break
+    # write-slice-buffered(out, {ECX, word->end})
+
+    # ECX = word
+    8b/copy/word                    1/mod/*+disp8   5/rm32/EBP    .           .             .           1/r32/ECX   0xc/disp8       .                 # copy *(EBP+12) to ECX
+    # EDX = word->end
+    8b/copy/word->end               1/mod/*+disp8   1/rm32/ECX    .           .             .           2/r32/EDX   4/disp8         .                 # copy *(ECX+4) to EDX
+    # ECX = word->start
+    8b/copy/word->start             0/mod/indirect  1/rm32/ECX    .           .             .           1/r32/ECX   .               .                 # copy *ECX to ECX
+
+    # clear out EAX
+    b8/copy-to-EAX 0/imm32
+    # while *start != '/':
+$skip-datum-loop:
+    # . start == end?
+    39/compare-ECX-and              3/mod/direct    2/rm32/EDX    .           .             .           1/r32/ECX   .               .                 # EDX == ECX
+    # . if so, return from function (it's only datum, or empty)
+    74/jump-if-equal  $emit-metadata:end/disp8
+
+    # . start++
+    41/increment-ECX                                                                                                                                  # ECX++
+
+    # . EAX = *start
+    8a/copy-byte                    0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy *ECX to EAX
+
+    # . EAX != '/'?
+    3d/compare-EAX-and  0x2f/imm32
+    # . if so, continue looping
+    75/jump-if-not-equal  $skip-datum-loop/disp8
+    # end
+
+    # write-slice-buffered(out, &{start, end})
+    # . push end
+    52/push-EDX
+    # . push start
+    51/push-ECX
+    # . push &{start, end}
+    54/push-ESP
+
+    # . push out
+    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
+
+    e8/call  write-slice-buffered/disp32
+    # . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           0x10/imm32      .                 # add 16 to ESP
+
 $emit-metadata:end:
-    # . reclaim locals
     # . restore registers
+    5a/pop-to-EDX
+    59/pop-to-ECX
+    58/pop-to-EAX
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-emit-metadata:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # var slice/ECX = "abc/def"
+    68/push  _test-slice-word-end/imm32
+    68/push  _test-slice-word/imm32/start
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # emit-metadata(_test-output-buffered-file, slice)
+    # . . push args
+    51/push-ECX
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  emit-metadata/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # flush(_test-output-buffered-file)
+    # . . push args
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-stream-equal(_test-output-stream, "/def", msg)  # important that there's no leading space
+    # . . push args
+    68/push  "F - test-emit-metadata"/imm32
+    68/push  "/def"/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-emit-metadata-none:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # var slice/ECX = "abc"
+    68/push  _test-slice-word-datum-end/imm32
+    68/push  _test-slice-word/imm32/start
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # emit-metadata(_test-output-buffered-file, slice)
+    # . . push args
+    51/push-ECX
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  emit-metadata/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # flush(_test-output-buffered-file)
+    # . . push args
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-stream-equal(_test-output-stream, "", msg)
+    # . . push args
+    68/push  "F - test-emit-metadata-none"/imm32
+    68/push  ""/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+    # . epilog
+    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+    5d/pop-to-EBP
+    c3/return
+
+test-emit-metadata-multiple:
+    # . prolog
+    55/push-EBP
+    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+    # setup
+    # . clear-stream(_test-output-stream)
+    # . . push args
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # . clear-stream(_test-output-buffered-file+4)
+    # . . push args
+    b8/copy-to-EAX  _test-output-buffered-file/imm32
+    05/add-to-EAX  4/imm32
+    50/push-EAX
+    # . . call
+    e8/call  clear-stream/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # var slice/ECX = "abc/def/ghi"
+    68/push  _test-slice-word-end2/imm32
+    68/push  _test-slice-word/imm32/start
+    89/copy                         3/mod/direct    1/rm32/ECX    .           .             .           4/r32/ESP   .               .                 # copy ESP to ECX
+    # emit-metadata(_test-output-buffered-file, slice)
+    # . . push args
+    51/push-ECX
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  emit-metadata/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
+    # flush(_test-output-buffered-file)
+    # . . push args
+    68/push  _test-output-buffered-file/imm32
+    # . . call
+    e8/call  flush/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+    # check-stream-equal(_test-output-stream, "/def/ghi", msg)  # important that there's no leading space
+    # . . push args
+    68/push  "F - test-emit-metadata-multiple"/imm32
+    68/push  "/def/ghi"/imm32
+    68/push  _test-output-stream/imm32
+    # . . call
+    e8/call  check-stream-equal/disp32
+    # . . discard args
+    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
     # . epilog
     89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
     5d/pop-to-EBP
@@ -1746,4 +1966,13 @@ _test-slice-a-dquote-b:
   22/dquote 61/a 5c/backslash 22/dquote 62/b 22/dquote  # "a\"b"
 _test-slice-a-dquote-b-end:
 
+# abc/def/ghi
+_test-slice-word:
+  61/a 62/b 63/c  # abc
+_test-slice-word-datum-end:
+  2f/slash 64/d 65/e 66/f  # /def
+_test-slice-word-end:
+  2f/slash 67/g 68/h 69/i  # /ghi
+_test-slice-word-end2:
+
 # . . vim:nowrap:textwidth=0
diff --git a/subx/apps/factorial b/subx/apps/factorial
index d1bd5f17..c18b8bcc 100755
--- a/subx/apps/factorial
+++ b/subx/apps/factorial
Binary files differdiff --git a/subx/apps/handle b/subx/apps/handle
index b77ebba6..4fe1e044 100755
--- a/subx/apps/handle
+++ b/subx/apps/handle
Binary files differdiff --git a/subx/apps/hex b/subx/apps/hex
index dde157e3..59170942 100755
--- a/subx/apps/hex
+++ b/subx/apps/hex
Binary files differdiff --git a/subx/apps/pack b/subx/apps/pack
index 1154be78..f2149b59 100755
--- a/subx/apps/pack
+++ b/subx/apps/pack
Binary files differdiff --git a/subx/build b/subx/build
index 614b07d8..f29518ac 100755
--- a/subx/build
+++ b/subx/build
@@ -22,7 +22,7 @@ UNTIL_LAYER=${2:-zzz}
 test "$CXX" || export CXX=c++
 test "$CC" || export CC=cc
 test "$CFLAGS" || export CFLAGS="-g -O3"
-export CFLAGS="$CFLAGS -Wall -Wextra -ftrapv -fno-strict-aliasing"
+export CFLAGS="$CFLAGS -Wall -Wextra -fno-strict-aliasing"
 
 # return 1 if $1 is older than _any_ of the remaining args
 older_than() {
diff --git a/subx/test_layers b/subx/test_layers
index a2ed65c6..688a5a99 100755
--- a/subx/test_layers
+++ b/subx/test_layers
@@ -21,11 +21,11 @@ CFLAGS=$CFLAGS ./build  # build optimized by default since we'll be running it r
 for f in [0-9]*.subx
 do
   echo "=== $f"
-  ./subx translate $(../enumerate/enumerate --until $f |grep '\.subx$') -o foo  &&  ./subx run foo
+  ./subx translate $(../enumerate/enumerate --until $f |grep '\.subx$') -o a.elf  &&  ./subx run a.elf
   echo
   test `uname` = 'Linux'  &&  {
-    chmod +x foo
-    ./foo
+    chmod +x a.elf
+    ./a.elf
     echo
   } || true
 done