0

In the following assembly:

mov     dx, word ptr [ebp+arg_0]
mov     [ebp+var_8], dx

Thinking of this as an assembled C function, how many bits wide is (the argument to the C function) arg_0? How many bits wide is (the local C variable) var_8? That is to say, is it a short, an int, etc.

From this, it appears that var_8 is 16 bits, since dx is a 16-bit register. But I'm not sure about arg_0.

If the assembly also contains this line:

ecx, [ebp+arg_0]

Would that imply that arg_0 is a 32-bit value?

ineedahero
  • 715
  • 1
  • 5
  • 10
  • The bare instruction mnemonics with no final `x` are the original 8086 instructions, which generally have a width given by their operand types. (Though there are quirky exceptions like `mul` and `div`.) `dx` is a 16-bit wide register. Additionally a memory reference labeled `word ptr` refers to a 16-bit wide chunk of memory. The registers with a leading `e` are indeed 32 bits. You can get this information by reading an x86 instruction reference. I recommend you do that. – Gene May 08 '18 at 00:30
  • hmm... I read this after adding my answer to your previous question, can you please check it first, to see, if it helps? It's basically also answering this, if you get the grasp of the principle of information in computer being encoded in bits, and in this case the information stored at address `ebp+arg_0` is used at one time as 16 bit and other as 32 bit integer value, i.e. unless there's bug in the code, the source informations was most likely 32 bit wide, to cover both usage cases (and using something like `(short)` cast in one case to truncate it to 16b). – Ped7g May 08 '18 at 06:16

1 Answers1

4

There are three principles to understand in order to tackle this question.

  1. The assembler must be able to infer the correct length.
    Though the Intel's syntax is not using a size suffix like the AT&T syntax the assembler still need a way to find the size of the operands.
     
    The ambiguous instruction mov [var], 1 is written as movl $1, var in AT&T syntax, if the size of the store is 32-bit (note the suffix l), so it is easy to tell the size of the immediate operand.
    The assembler that accepts the Intel syntax needs a way to infer this size, there are four widely used options:

    • It is inferred from the other operand.
      This is the case when a register is involved, for example.
      E.g. mov [var], dx is a 16-bit store.
    • It is stated explicitly.
      mov WORD [var], dx
      MASM-syntax assemblers need a PTR after the size, because their size specifiers are only allowed on memory operands, not immediates or anywhere else.
      This is the form I prefer because it is clear, it stands out and it is a bit less error-prone (mov WORD [var], edx is invalid).
    • It is inferred from the context.

       var db 0
      
       mov [var], 1   ; MASM/TASM only.   associate sizes with labels 
      

      MASM-syntax assemblers can infer that since var is declared with db its size is 8-bit and so is the store (by default).
      This is the form I don't like because it makes the code harder to read (one good thing about assembly is the "locality" of the semantics of the instructions) and mix high-level concepts like types with low-level concepts like store sizes. That's why NASM's syntax doesn't support magical / non-local size association.

    • There is only one correct size the vast majority of times
      This is the case with push, branches and all the instructions where their operand size depends on the memory model or code size.
      The actual size used can be overridden for some instructions, but the default is a sensible choice. (e.g. push word 123 vs. push 123)

     
    To put it short, there must be a way for the assembler to tell the size, otherwise it will reject the code. (Or some low quality assemblers like emu8086 have a default operand size for ambiguous cases.)

    If you are looking at a disassembled code, disassemblers usually take the safe side and always state the size explicitly.
    If not, you must resort to manual inspection of the opcode, if the disassembler won't show the opcodes, it is time to change it.
    The disassembler has no trouble finding out the size of the operand as the binary code it is disassembling is the same executed by the CPU and the instructions opcodes encode the operand size.
     

  2. The C language is intentionally loose on how C types map to the number of bits
     
    It's not futile to try to infer the type of a variable from the disassembly but one must take into consideration the platform too, not only the architecture.
    The main models used are discussed here:

    Datatype    LP64    ILP64   LLP64   ILP32   LP32
    char        8       8       8       8       8
    short       16      16      16      16      16
    _int32      32          
    int         32      64      32      32      16
    long        64      64      32      32      32
    long long                   64      [64]                    
    pointer     64      64      64      32      32
    

    Windows on x86_64 uses LLP64. Other OSes on x86-64 typically use the x86-64 System V ABI, an LP64 model.

  3. Assembly doesn't have types and programmers can exploit that
     
    Even compilers can exploit that.
     
    In the case linked a bar variable of type long long (64-bit) is ORed with 1, clang spares a REX prefix by ORing only the low byte. This causes a store-forwarding stall if the variable is reloaded again right away with two dword loads or one qword, so it's probably not a good choice, especially in 32-bit mode where or dword [bar], 1 is the same size and it's likely to be reloaded as two 32-bit halves.
    If one would look at the disassembled code incautiously they could infer that bar is 8-bit.
    This kind of tricks, where a variable or an object, are accessed partially are common.
     
    In order to correctly guess the size of a variable it takes a bit of expertise.
    For example, structures members are usually padded, so there is unused space between them that may fool the inexperienced user into thinking that each member is bigger than it is.
    The stack has precise alignment requirements that also may make widen the parameters size.
     
    The rule of thumb is that compilers generally prefer to keep the stack 16-byte aligned, and naturally-align all variables. Multiple narrow variables are packed into a single dword. When passing function args via the stack, each one is padded to 32 or 64-bit, but that doesn't apply to the layout of locals on the stack.

To finally answer your question

Yes, from the first snippet of code you can assume that the value of arg_0 is 16-bit wide.
Note that since it's a function arg passed on the stack, it is actually 32-bit but the upper 16 bits are not used.

If a mov ecx, [ebp+arg_0] appeared later in the code than you would have to revisit your guess about the size of the value of arg_0, it is certainly at least 32-bit.
It is unlikely that it is 64-bit (64-bit type are rare in 32-bit code, we can make this bet) so we can conclude it is 32-bit.
Evidently, the first snippet was one of those tricks that only uses a part of a variable.

That's how you deal with reverse engineering a size of a var, you make a guess, verify it is consistent with the rest of the code, revisit it if not, repeat.
With time you'll make mostly good guesses that need no revision at all.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Margaret Bloom
  • 41,768
  • 5
  • 78
  • 124
  • I think there is some confusion in this answer between the size of the operand and the size of the address. If the destination register is 16-bit, this only means that the size of the operand fetched from memory is 16-bit (as indicated by the `word ptr` part), but the address `[ebp+arg_0]` is not necessarily 16-bit in size. Note that `arg_0` is just a displacement and can be 8-bit, 16-bit, or 32-bit, depending on whether the code is 16-bit, 32-bit, 64-bit, the address size override prefix, and how the assembler handles address overflows. – Hadi Brais May 08 '18 at 17:58
  • 1
    @HadiBrais It was not my intention to involve the size of the address. `arg_0` is syntactically just a displacement but, with an abuse of notation, I `arg_0` to refer to the value. – Margaret Bloom May 08 '18 at 18:26
  • It seems to me that the first two sections of the answer are about the operand size. Then at the end it says "you can assume that arg_0 is 16-bit wide". But why? Then it says that if there is `[ebp+arg_0]` then `arg_0` is at least 32-bit. But again why? Then it says "It is unlikely that it is 64-bit" But the displacement can never be 64-bit. – Hadi Brais May 08 '18 at 18:37
  • Although the assembler might play a role here. For example, if `arg_0` is 64-bit then the assembler might decide to truncate it silently or otherwise. I'm not sure if any of the assemblers do that or just emit an error. If they do, then `arg_0` can be 64-bit, but the actual displacement can be different after it gets truncated. Th OP did not specify what assembler is being used. – Hadi Brais May 08 '18 at 19:00
  • 1
    @HadiBrais Formally, I use `arg_0` to mean `[arg_0]`. This is an abuse of notation but I thought it was clear from the context, like when one says "`var1` is *x*". hmm, maybe I should clarify that section. – Margaret Bloom May 08 '18 at 20:12
  • I think it's confusing to say *The assembler or the CPU must be able to infer the correct length.* Only the assembler is involved in looking at asm source syntax and figuring out what operand-size to using in the machine code. When the CPU decodes the machine code, the operand-size is given by the opcode, the current mode, and REX or operand-size prefixes. i.e. for a given mode (e.g. 32-bit), operand-size is well-defined for every instruction. And this process is totally unrelated to assemble-time memory operand size being implied by a register operand or given explicitly. – Peter Cordes May 10 '18 at 00:05
  • 2
    @PeterCordes They both need to infer the correct length, in different ways but they both have to. The code the OP posted looks like a disassembler output, that's why I included the CPU as the subject. I'm editing to make it clearer. – Margaret Bloom May 10 '18 at 10:55
  • Good edit. Yeah, decoding machine code is sort of inferring operand-size from the current mode and prefixes (except with 8-bit operand-size where it's just the opcode), but there's no way for it to be ambiguous or to make it "more" explicit (other than REX prefixes). – Peter Cordes May 10 '18 at 17:29
  • 1
    BTW, the `clang -m32` godbolt link with `or byte ptr[bar], 1` is actually a pessimization, not clang being clever. `or dword ptr [bar], 1` is the same code-size, and avoids a store-forwarding stall assuming that the next access to `int64_t bar` loads its halves into 32-bit integer registers. If the next access loads it with `movq` then you're screwed either way, but a dword RMW is never worse and can be better on all modern CPUs. Clang really likes 8-bit operand-size, and doesn't seem to know about partial-register stall either even with `-march=core2`. – Peter Cordes May 10 '18 at 17:34