1

In MASM, I've always inserted a standalone break instruction

00007ff7`63141120 cc              int     3

However, replacing that instruction with the MSVC DebugBreak function generates

KERNELBASE!DebugBreak:
00007ff8`6b159b90 6690            xchg    ax,ax
00007ff8`6b159b92 cc              int     3
00007ff8`6b159b93 c3              ret

I was surprised to see the xchg instruction prior to the break instruction

xchg    ax,ax

As noted from another S.O. article:

Actually, xchg ax,ax is just how MS disassembles "66 90". 66 is the operand size override, so it supposedly operates on ax instead of eax. However, the CPU still executes it as a nop. The 66 prefix is used here to make the instruction two bytes in size, usually for alignment purposes.

MSVC, like most compilers, aligns functions to 16 byte boundaries.

Question What is the purpose of that xchg instruction?

Alex Guteniev
  • 12,039
  • 2
  • 34
  • 79
vengy
  • 1,548
  • 10
  • 18

1 Answers1

3

MSVC generates 2 byte nop before any single-byte instruction at the beginning of a function (except ret in empty functions1). I've tried __halt, _enable, _disable intrinsics and seen the same effect.

Apparently it is for patching. /hotpatch option gives the same change for x86, and /hotpatch option is not recognized on x64. According to the /hotpatch documentation, it is expected behavior (emphasis mine):

Because instructions are always two bytes or larger on the ARM architecture, and because x64 compilation is always treated as if /hotpatch has been specified, you don't have to specify /hotpatch when you compile for these targets;

So hotpatching support is unconditional for x64, and its result is seen in DebugBreak implementation.

See here: https://godbolt.org/z/1G737cErf

See this post on why it is needed for hotpatching: Why do Windows functions all begin with a pointless MOV EDI, EDI instruction?. Looks like that currently hotpatching is smart enough to use any two bytes or more instruction, not just MOV EDI, EDI, still it cannot use single-byte instruction, as two-byte backward jump may be written at exact moment when the instruction pointer points at the second instruction.


1 As discussed in comments, empty functions have three-byte ret 0, although it is not apparent from MSVC assembly output, as it is represented there as just ret)

Alex Guteniev
  • 12,039
  • 2
  • 34
  • 79
  • 1
    *MSVC generates 2 byte nop before any single-byte instruction* - actually only at the top of a function. If you do something else first in a function that will use a longer instruction, like store to a global, there's no `nop` before an `int3` or `cli`/`sti`. https://godbolt.org/z/jae5oT5Mj. In fact your own link already showed this with back-to-back sti/cli with no padding between them. And there isn't padding before the single-byte `ret`. – Peter Cordes Oct 27 '21 at 14:45
  • 1
    Interestingly, MSVC compiles an empty function with no padding before the single-byte `ret`. I guess hotpatching could use the space after the `ret`, before the next function. (`ret` is an unconditional branch, so there will never be a situation where RIP = the byte after the `ret`, which would become the middle of a new instruction like `jmp rel8` after storing those new machine code bytes, which is the reason for avoiding single-byte instructions normally.) – Peter Cordes Oct 27 '21 at 14:51
  • 4
    @PeterCordes In my experience, MSVC encodes it as a three-byte "ret 0" if it is the only instruction in the function. – Raymond Chen Oct 27 '21 at 15:02
  • @RaymondChen, indeed. I don't know of a way to make it visible on Godbolt's but I see `c2 00 00` in the debugger. – Alex Guteniev Oct 27 '21 at 15:10
  • @RaymondChen: Ah, that makes sense, I guess, if they don't want to rely on alignment of the next function to leave 15 bytes of padding. Optimizing empty functions usually isn't important anyway so the extra uop will rarely matter. And because of aligning functions, there's no code-size saving. Even the uop-cache won't benefit because unconditional branches like `ret` always end a "way" of Sandybridge-family's uop cache. And yeah, Compiler Explorer doesn't support "binary mode" for MSVC, not even for the WINE versions that run on the Linux server instead of redirecting to another server. – Peter Cordes Oct 27 '21 at 15:30