1

I'm currently looking into assembly and disassembled some code(x86) and there I tried to recreate the C-Code from it. I know at least the basics of the registers and programming in general. But I can't make sense of the usage of ESI in the case of:

mov esi, [esp+10h]
lea esi, [esi+0]

I don't know for sure what esp+10h is in this case, but I don't think it matters, since it should just be some random 32-bit-value, right?

From what I know is that mov esi, [esp+10h] will copy the data(the actual value) from the adress esp+10h and store it in ESI. So esi should be some kind of data right now. But then afterwards lea esi, [esi+0] comes and interprets esi+0, which is data, as an adress and writes this in esi.

In another thread on stackoverflow I read, that lea esi, [esi+0] is actually is a nop(What is the meaning of lea 0x0(%esi),%esi). That makes sense since it shouldn't modify the address. But that's confusing when esi is data on usage. So my C-code for it looks something like this:

int esi = *(esp + 0x10); //move data from esp+10h to esi
int* esi = &esi;         //write the address of esi+0 to esi which is
                         //actually an int and not int*

So my question is if there is any distinction between data and adress on an register-level or if it's just interpreted as it is since it looks the same(32-bit register). My other question is what this code now actually does and what ESI is at the end? Does mov always copy data? Does lea always copy an address of doesn't it matter anyways?

As you might have guessed by now, I'm pretty confused and maybe I'm just thinking too far. Please have mercy with me :D

Community
  • 1
  • 1
SkryptX
  • 813
  • 1
  • 9
  • 24
  • 1
    `mov esi, [esp+10h]` gets the fourth (DWORD/4-byte) argument of an (assumed) function and copies it to `ESI`. `lea esi, [esi+0]` is indeed a NOP, similar to `mov esi, esi`. – zx485 Apr 28 '16 at 12:42
  • @zx485 : You are assuming the stack hasn't had anything placed on it after the return address. Maybe the code has prologue/epilogue code? There isn't actually enough information in the question to say what is at `esp+10h` unless the OP updated the question with the rest of the code they are looking at. – Michael Petch Apr 28 '16 at 12:47
  • The second statement should read `esi = esi + 0;`. and the first one is slightly incorrect, it is not `esp + 0x10` but `esp + 0x10 / sizeof(*esp)`; that is the assembler displacements are in bytes/char, but in C the displacements/indexes are in number of elements. The meaning of `lea` is that it can be used to calculate the address that would be used if the same kind of operand was given to `mov` opcode. – Antti Haapala -- Слава Україні Apr 28 '16 at 12:50
  • 3
    This is just a guess, but usually nop's like `lea esi, [esi+0]` are indicative that the compiler was likely attempting to align a piece of code to a boundary for purposes of performance. Usually you'll see do nothing instruction just before the top of a loop to align it on a particular boundary (often 16-bytes). If you showed us all the code you are converting we'd be able to make a more accurate determination about what the code is actually doing. – Michael Petch Apr 28 '16 at 13:06
  • So based on your answers I'm guessing that assembly actually doesn't care if an adress or data is in the memory, right? And I'm not sure if the whole code is any more useful... but yeah the `lea` is actually right in front of a loop – SkryptX Apr 28 '16 at 13:18
  • You can store data or memory addresses in a register (or memory). The meaning of the data is dependent on what instructions you use. – Michael Petch Apr 28 '16 at 14:21
  • 1
    For CPU there's no difference between having an address or value in ESI, both are integer numbers. With `lea` it is important to note, that while the instruction itself is abbreviation of "load effective address", it is really only calculating the address, not fetching the actual value. That's the reason why it's ok to use it as NOP. If it would reach for the actual memory value, you would risk crash because of illegal address access (as value of ESI is undetermined and may point to invalid memory area). – Ped7g Apr 28 '16 at 15:12
  • ok now I understand it better and thanks for all the comments :) maybe someone could write an answer off of the comments so I can mark it as answer, or else I'll just do it myself sometime ^^ – SkryptX Apr 28 '16 at 15:58
  • `lea` is a shift-and-add instruction using the syntax and encoding of an effective-address. There's no way it could figure out where the old contents were loaded from, so the `&` operator in your C is where you went wrong. I'll leave it for @Ped7g to actually post an answer, since he commented first, and I don't want to ninja his chance to get some rep. – Peter Cordes Apr 28 '16 at 16:18
  • @PeterCordes ; Seems it doesn't matter now, someone else decided to answer before Ped7g. – Michael Petch Apr 28 '16 at 16:51
  • IIRC `lea esi, [esi+0]` is a 3-byte nop, while for a 2-byte nop `mov %esi,%esi` tends to get used. LEA is sometimes used to do strength reduction, e.g: the compiler may convert a multiplication by 100 into 2 LEAs and a SHL: http://stackoverflow.com/a/10524461/371250 – ninjalj Apr 28 '16 at 18:04
  • FWIW it's not uncommon to see `lea reg, [reg+0]` as a 2 or 3 byte no-op to align loop targets so that small loops begin on some specific power-of-two boundary. Both MSVC and GCC may do this – user1354557 Apr 28 '16 at 21:47
  • But why is it beneficial that it's a power-of-two? What optimization can be done when it's like this? And is it worth the nop? – SkryptX Apr 28 '16 at 22:54
  • Logic in memory chips (especially L0/1/2/.. cache) likes to work with chunks of memory (it tracks modification of whole chunk: less data to track. And fetches whole chunk: when the upper level of memory finally does set itself to read desired byte, it can produce sequence of following ones much faster). The only reasonable boundary between those chunks is any power-of-two, so the cache can calculate number of chunk easily just by shifting the bits of address right. IIRC, x86 CPU L0 caches are often 4kiB (4096 bytes) oriented. A code loop starting at L0 cache boundary makes it simpler for CPU. – Ped7g Apr 29 '16 at 13:07
  • "simpler for CPU": also the memory bus on 64b PC is 64b (or maybe more nowadays?), so the aligned memory access (to fetch first instruction opcode of loop, rest of it is pre-fetched in sequential reading) is faster than accessing unaligned memory... "is it worth"? C compiler's optimizations are things which generally work, so "yes". But don't go crazy about it, profiling is essential to evaluate any particular optimization. For example many modern C compilers can work with profiler data (after you run your code few times) to figure out which optimizations are worth of it. – Ped7g Apr 29 '16 at 13:17

1 Answers1

4

CPUs don't have data types like higher-level languages do. To the CPU, all data is just bits; the interpretation of those bits as an integer / bitmask / address / floating-point number exists only in the programmer's mind. It's the programmer's responsibility to know "this 32-bit value represents the address of a character array, so I should only do things with it that make sense in that context". But if you do something less sensible, like shifting an address, the machine won't stop you.

The first instruction takes the value in the esp register (which usually contains the address of the top of the stack), adds 10h, fetches a 32-bit value from that address, and loads it into the esi register. The value at this address is probably a local variable or function argument, and it's being loaded into esi in preparation for some further computations.

The second instruction takes the value in the esi register, adds 0, and loads that value into esi. (Note it doesn't fetch anything from memory, despite the "indirect" appearance of the instruction; this is an oddity of the lea instruction, which essentially computes an address but then doesn't dereference it. You can think of it as somewhat like C's unary &.) As you say, this is a no-op; yes, there was data in esi, but it gets "replaced" by itself.

Note that there is only one esi register in the machine, so your code which declares it as two different variables is misleading. Think of esi as a global variable.

Since C does have data types, to get a similar effect in C, you would have to typecast all over the place. I would express the code like this:

int esi, esp;

esi = *(int *)(esp + 0x10);
esi = (int)&*(int *)esi; // does nothing

(You have to assume that you can cast integers to pointers and back without changing their values.)

Nate Eldredge
  • 48,811
  • 6
  • 54
  • 82