3

For this C source code:

int add(int a, int b) { return a + b; }

, the Watcom C Compiler for 8086 (wcc -s -ms -os -0 prog.c) generates the following machine code (hex): 01 D0 C3, disassembling to add ax, dx (01 D0) + ret (C3).

For this assembly source code:

PUBLIC  add_
EXTRN   _small_code_:BYTE
_TEXT SEGMENT BYTE PUBLIC USE16 'CODE'
add_: add ax, dx
      ret
_TEXT ENDS
END

, the Watcom Assembler (WASM, wasm -ms -0 prog.wasm) generates the following machine code (hex): 03 C2 C3, disassembling to add ax, dx (03 C2) + ret (C3).

Thus they generate a different binary encoding of the same 8086 assembly instruction add ax, dx.

FYI If I implement the the function in Watcom C inline assembly, then the machine code output will be the same as with WASM.

A collection of different instruction encodings:

  • add ax, dx. wcc: 01 D0; wasm: 03 C2.
  • mov bx, ax. wcc: 89 C3; wasm: 8B D8.
  • add ax, byte 9. wcc: 05 09 00; wasm: 83 C0 09.

How can I make the Watcom C compiler (for C code) and WASM generate the instructions with the same binary encoding? Is there a command-line flag or some other configuration option for either? I wasn't able to find any.

The reason why I need it is that I'd like to reproduce an executable program file written in Watcom C by writing WASM source only, and I want the final output be bit-by-bit identical to the original.

Michael Petch
  • 46,082
  • 8
  • 107
  • 198
pts
  • 80,836
  • 20
  • 110
  • 183
  • 3
    Tell the compiler to generate an .asm file, and then pass it to wasm. – Raymond Chen Oct 26 '22 at 14:24
  • 1
    The interesting part are the different opcodes for the "same" instruction. Did you check this with the 8086's manual? Anyway, different compilers or even assemblers may choose different opcodes, in case of ambiguous mnemonics, if the result is equivalent. Because of this I would not try what you want to achieve. – the busybee Oct 26 '22 at 14:34
  • @thebusybee there doesn't appear to be any real difference based on the intel manual, ostensibly `03` could be faster because the destination can only be a register and thus on pentium and later it can take advantage of renaming potentially. But most likely on anything even remotely modern they are effectively the same. I don't have manuals for the 8086-386 so I can't say if there are differences there. I assume there potentially are. – Mgetz Oct 26 '22 at 14:37
  • @Mgetz My comment was more a note and a hint to the OP, not a missing knowledge of my own. Somewhere on my disk is a manual... but it is not my task to check that. ;-) Thanks anyway! – the busybee Oct 26 '22 at 14:39
  • 1
    @thebusybee: There are two ways to encode most standard 8086 `op reg,reg`, one using the `op r/m, r` opcode and the other using the `op r, r/m` opcode. ([x86 XOR opcode differences](https://stackoverflow.com/q/50336269)). Some assemblers have overrides for which form to pick, like GAS `{load} xor %eax, %ecx` vs. `{store} xor %eax, %ecx` - see my answer on that linked question. In this case, `01` and `03` are opposite "directions" of the same 16-bit ALU `add` operation (https://www.felixcloutier.com/x86/add), and the ModRM differences look reasonable. – Peter Cordes Oct 26 '22 at 17:35
  • 1
    They have exactly equal performance in all known cases, including on Pentium, @Mgetz. P5 Pentium doesn't do register-renaming; it's an in-order dual-issue pipeline. No CPUs care that a register destination *could* have been memory, only that it's actually a register. Decoding happens before hazard / dependency analysis, or before issue/rename in an out-of-order pipeline. – Peter Cordes Oct 26 '22 at 17:36
  • @RaymondChen: Feeding the assembly output of *wcc* to *wasm* is not possible directly, because *wcc* cannot generate an assembly file. However, `wdis -a` can be used to generate an assembly file from the .obj file generated by *wcc*. However, `wdis -a` doesn't dump all data in the .obj file, so it may cause differences in some unpredictable way, thus it's not a drop-in replacement. – pts Oct 26 '22 at 17:58
  • @PeterCordes I figured, but I've seen stupider things on the 8086 occasionally. – Mgetz Oct 26 '22 at 19:33

1 Answers1

2

This answer is inspired by a comment by @RaymondChen.

Here is a cumbersome, multistep way to change the machine code emitted by wcc to match the output of wasm:

  1. Compile the C source code witm wcc (part of OpenWatcom) to .obj file as usual.

  2. Use dmpobj (part of OpenWatcom) to extract the machine code bytes of the _TEXT segment.

  3. Use ndisasm (part of NASM, ndisasm -b 16 file.obj) to disassemble the machine code bytes.

  4. Write and run custom source text filter to keep the assembly instructions only and convert them WASM syntax.

  5. Use wasm (part of OpenWatcom) to generate the 2nd .obj file.

  6. Use dmpobj to extract the machine code bytes of the _TEXT segment of the 2nd .obj file.

  7. Write and run a custom binary filter to replace the machine code bytes in the _TEXT segment of the 1st .obj file from the equivalent bytes extracted from the 2nd .obj file, using the offsets in the outputs of the dmpobj invocations.

These steps avoid using wdis -a (conversion from .obj to assembly source), because that's lossy (it doesn't include everything in the .obj file), which can potentially make unwanted changes, causing problems later.

pts
  • 80,836
  • 20
  • 110
  • 183
  • Most disassemblers other than `ndisasm` understand object-file metadata and will only disassemble the `.text` section. Some modern disassemblers don't understand 16-bit DOS `.obj` format, though. e.g. GNU Binutils `objdump -drwC -Mintel` doesn't understand `nasm -fobj` output. (I don't have Watcom tools). Agner Fog's `objconv -fnasm foo.obj dis.asm` understands the file format, but doesn't actually disassemble, thinking `section .text` is just data. Maybe it's just a matter of knowing the right section name for that format. – Peter Cordes Oct 26 '22 at 19:54
  • @PeterCordes: Extracting the machine code bytes is not enough, but we also want to replace them at the same offset later. Thus we also need offsets, which *dmpobj* can give us, and most other tools I know can't. – pts Oct 26 '22 at 20:50
  • 1
    Oh I see, you were already extracting the machine code to a flat binary before `ndisasm`. If you disassemble the `_TEXT` segment and re-assemble that into a flat binary (`nasm -fbin`), you could presumably still do the final replacement using the right offsets. That seems to me slightly simpler, but it's still the same overall strategy: disassemble and reassemble the machine code of the `_TEXT` segment of a `.exe` or `.obj`, keeping the numeric constants for absolute addresses, then overwriting in place back into the executable or object. – Peter Cordes Oct 26 '22 at 21:08
  • @PeterCordes: Indeed, this answer explains this in more detail. – pts Oct 26 '22 at 23:49
  • 1
    Right, I was just summarizing and talking through *why* it works, even without symbols to track the addresses of things in the `.data` section. Presumably even symbol relocations will still go in the same places. (Or I guess segment relocations for things like `mov ax, @data`.) So metadata that refers to things inside the code will still work, as long as the *only* change is in the opcodes and ModRM bytes, no changes to instruction length. – Peter Cordes Oct 27 '22 at 00:23