2
votes

I'm trying to understand this assembly code, can anybody help me with writting it in C/C++ language ?

this is the code:

 loc_1C1D40:             ; unsigned int
 push    5
 call    ??_U@YAPAXI@Z   ; operator new[](uint)
 mov     [ebp+esi*4+var_14], eax
 add     esp, 4
 inc     esi
 mov     byte ptr [eax+4], 0
 cmp     esi, 4
 jl      short loc_1C1D40

As I understand, the first two lines just call to "operator new" which return an address in eax. after this, the "mov [ebp+esi*4+var_14], eax" means that the address is being save in some sort of an array probably. The reason why esi is being incremented is pretty obvious. but why do we add 4 to esp?

1
It's the calling convention. See "cdecl". - Margaret Bloom
Note that there is no language called C/C++. - fuz
add esp,4 is balances the push inside the loop. Maybe more efficient to push once outside the loop, and then mov dword [esp], 5 / call, but worse for code-size and not worth it for a tiny loop. Pretty obvious this is a loop that allocates arrays, storing the pointers in another array. (And storing a 0 into each array). - Peter Cordes
thank you my friend, although i did not understand the first part about balancing the array. I'll do some homework about it :) @PeterCordes - jony
Balancing the stack, not the array. - Alexey Frunze

1 Answers

2
votes

Start by doing a line-by-line analysis to figure out what the code does.

push    5

This instruction pushes the constant value "5" onto the stack. Why? Well, because...

call    ??_U@YAPAXI@Z   ; operator new[](uint)

This instruction calls operator new[], which takes a single uint parameter. That parameter is evidently passed on the stack in whatever calling convention is used by this code. So, clearly, so far we have called operator new[] to allocate an array that is 5 bytes in size.

In C++, that would be written as:

BYTE* eax = new BYTE[5];

The call to operator new[] returns its value (the pointer to the beginning of the allocated memory block) in the EAX register. This is a general rule for all x86 calling conventions—functions always return their result in the EAX register.

mov     [ebp+esi*4+var_14], eax

The above code stores (moves) the resulting pointer (the one that is returned in EAX) into the memory location addressed by EBP + (ESI * 4) + var_14. In other words, it scales the value in the ESI register by 4 (presumably, the size of a uint), adds the offset from the EBP register, and then adds the offset of the constant var_14.

This is roughly equivalent to the following pseudo-C++ code:

void* address = (EBP + (ESI * 4) + var_14);
*address = eax;
add     esp, 4

This cleans the stack, effectively undoing the initial push 5 instruction.

push pushed a 32-bit (4 byte) value onto the stack, which decremented the stack pointer, which is maintained in the ESP register (note that the stack grows downward on x86). This add instruction increments the stack pointer (again, the ESP register) by 4 bytes.

Balancing the stack in this way is an optimization. You could equivalently have written pop eax, but that would have the added side-effect of clobbering the value in the EAX register.

There is no direct C++ equivalent of this instruction, as it's just doing bookkeeping work that would normally be hidden from you by a high-level language.

inc     esi

This increments the value of the ESI register by 1. It is equivalent to:

esi += 1;
mov     byte ptr [eax+4], 0

This stores the constant value 0 in the BYTE-sized memory block at EAX + 4. It corresponds to the following pseudo-C++:

BYTE* ptr = (eax + 4);
*ptr = 0;
cmp     esi, 4

This compares the value of the ESI register to the constant value 4. The CMP instruction actually sets the flags as if a subtraction was done.

Therefore, the subsequent instruction:

jl      short loc_1C1D40

conditionally jumps if the value of the ESI register is less than 4.

The compare-and-jump is a hallmark of a looping construct in a higher-level language, like a for or while loop.


Putting it all together, you have something like:

void Foo(char** var_14)
{
    for (int esi = 0; esi < 4; ++esi)
    {
        var_14[esi] = new char[5];
        var_14[esi][4] = 0;
    }
}

That's not exactly right, of course. Reconstituting the original C or C++ code from the compiled assembly is a lot like reconstituting the original cow from a ground-beef patty.

But it's pretty good. In fact, if you compile the above function in MSVC, optimizing for speed and targeting 32-bit x86, you get the following assembly generated:

void Foo(char**) PROC
        push    esi
        push    edi
        mov     edi, DWORD PTR _var_14$[esp+4]
        xor     esi, esi
$LL4@Foo:
        push    5
        call    void * operator new[](unsigned int)  ; operator new[]
        mov     DWORD PTR [edi+esi*4], eax
        add     esp, 4
        inc     esi
        mov     BYTE PTR [eax+4], 0
        cmp     esi, 4
        jl      SHORT $LL4@Foo
        pop     edi
        pop     esi
        ret     0
void Foo(char**) ENDP

That's pretty much exactly the same as what you had in the question, assuming you ignore the prologue and epilogue (which you didn't show in the question anyway).

The major difference is that the compiler is applying a fairly obvious loop-hoisting optimization to the MOV instruction. Instead of the original code's:

mov   [ebp + esi * 4 + var_14], eax

it instead pre-computes esp + var_14 in the prologue, caching the result in the free EDI register:

mov   edi, DWORD PTR _var_14$[esp + 4]

allowing the loading instruction inside the loop to be simply:

mov   DWORD PTR [edi + esi * 4], eax

I have no idea why your code doesn't do this, or why it's using EBP to hold the offset.