views:

327

answers:

4
+9  Q: 

memory layout hack

i have been following this course in youtube and it was talking about how some programmers can use there knowledge of how memory is laid to do clever things.. one of the examples in the lecture was something like that

#include <stdio.h>
void makeArray();
void printArray();
int main(){
        makeArray();
        printArray();
        return 0;
}
void makeArray(){
    int array[10];
    int i;
    for(i=0;i<10;i++)
        array[i]=i;
}
void printArray(){
    int array[10];
    int i;  
    for(i=0;i<10;i++)
        printf("%d\n",array[i]);
}

the idea is as long as the two function has the same activation record size on the stack segment it will work and print numbers from 0 to 9 ... but actually it prints something like that

134520820
-1079626712
0
1
2
3
4
5
6
7

there are always those two values at the begging ... can any one explain that ??? iam using gcc in linux

the exact lecture url starting at 5:15

A: 

Seemingly random data like that is what happens when you address your variables without initializing them.

This happens because of how the processor relates things in memory, for example, returning from the function call and entering into the next call while after releasing the variables in scope of makeArray.

When you entered printArray, the starting address for pointing variables has changed.

the array is initialized in the make array method ... and when it loses the scope (as far as i know) it will just move the pointer in the stack segment ... so why there are only two variables that get corrupted ? ... i need more details...
Ahmed Kotb
The poster seems to think that the array in both functions should end up in the same area on the stack, so that the array in printArray should have in it what he put into the array in makeArray(). This "clever" trick turns out not to work in this case. This is why good programmers avoid such "clever" tricks.
Tim Schaeffer
+1 for good programmers avoid clever tricks.
glowcoder
@Ahmed: the variables aren't corrupted. The array in `printArray()` is placed lower in the stack than the array in `makeArray()` and is picking up some of the activation record left by `makeArray()`, or some other junk.
Tim Schaeffer
@glowcoder i know that and ofcourse i am not going to use this in programming something...i just want to understand why it didnt work as the lecturer said ...@tim : obviously they didn't take the same place in memory thats why it wasn't the same (there is a 2 byte shift) and i wanted an explanation for this
Ahmed Kotb
It didn't work as your lecture said because your lecture was written in the 1950s when there was only one program running on the computer at a time.
@Ahmed: It's actually an 8 byte shift (2 x sizeof(int)). Also, I agree that its an interesting exercise to try and figure out why intuition fails in this case.
torak
@user257493: How does a different program, running in its own address space, affect the stack layout of this program?
torak
@Ahmed: I understand. Your best bet is to generate the assembly code for this and follows the bouncing stack pointer.
Tim Schaeffer
@tork: yes i meant that...thanks.
Ahmed Kotb
@user 257493, that's quite funny (the 1950s comment) but C wasn't started until '69 so, unless Stanford actually perfected time travel, your timeline is a bit off :-)
paxdiablo
@paxdiablo if anyone can travel back in time, it's early 70's Stanford.
glowcoder
+3  A: 

Probably GCC generates code that does not push the arguments to the stack when calling a function, instead it allocates extra space in the stack. The arguments to your 'printf' function call, "%d\n" and array[i] take 8 bytes on the stack, the first argument is a pointer and the second is an integer. This explains why there are two integers that are not printed correctly.

X-N2O
+1 very clever..
Tomas
Nice idea, but most certainly wrong. The params would be pushed immediately before printf is called. If they were anywhere else, printf wouldn't know how to find them. And because printf uses variable args, they cant be passed in registers.
Roddy
Well, normally gcc would use push/pop, like this: http://pastebin.com/bpgX0AVMBut there are cases where it would allocate extra space for the arguments in the function prolog: http://pastebin.com/HD0aAk5x
X-N2O
Well, you're correct in that they're not pushed using PUSH instructions, but the SUB ESP,n ... MOV [ESP],value results (as is MUST) in an identical stack layout to the PUSH instructions. It just saves a cycle or so.
Roddy
+22  A: 

I'm sorry but there's absolutely nothing clever about that piece of code and people who use it are very foolish.


Addendum:

Or, sometimes, just sometimes, very clever. Having watched the video linked to in the question update, this wasn't some rogue code monkey breaking the rules. This guy understood what he was doing quite well.

It requires a deep understanding of the underlying code generated and can easily break (as mentioned and seen here) if your environment changes (like compilers, architectures and so on).

But, provided you have that knowledge, you can probably get away with it. It's not something I'd suggest to anyone other than a veteran but I can see it having its place in very limited situations and, to be honest I've no doubt occasinally been somewhat more ... pragmatic ... than I should have been in my own career :-)

Now back to your regular programming ...


It's non-portable between architectures, compilers, releases of compilers, and probably even optimisation levels within the same release of a compiler, as well as being undefined behaviour (reading uninitialised variables).

Your best bet if you want to understand it is to examine the assembler code output by the compiler.

But your best bet overall is to just forget about it and code to the standard.


For example, this transcript shows how gcc can have different behaviour at different optimisation levels:

pax> gcc -o qq qq.c ; ./qq
0
1
2
3
4
5
6
7
8
9

pax> gcc -O3 -o qq qq.c ; ./qq
1628373048
1629343944
1629097166
2280872
2281480
0
0
0
1629542238
1629542245

At gcc's high optimisation level (what I like to call its insane optimisation level), this is the makeArray function. It's basically figured out that the array is not used and therefore optimised its initialisation out of existence.

_makeArray:
        pushl   %ebp            ; stack frame setup
        movl    %esp, %ebp

                                ; heavily optimised function

        popl    %ebp            ; stack frame tear-down

        ret                     ; and return

I'm actually slightly surprised that gcc even left the function stub in there at all.

Update: as Nicholas Knight points out in a comment, the function remains since it must be visible to the linker - making the function static results in gcc removing the stub as well.

If you check the assembler code at optimisation level 0 below, it gives a clue (it's not the actual reason - see below). Examine the following code and you'll see that the stack frame setup is different for the two functions despite the fact that they have exactly the same parameters passed in and the same local variables:

subl    $48, %esp     ; in makeArray
subl    $56, %esp     ; in printArray

This is because printArray allocates some extra space to store the address of the printf format string and the address of the array element, four bytes each, which accounts for the eight bytes (two 32-bit values) difference.

That's the most likely explanation for your array in printArray() being off by two values.

Here's the two functions at optimisation level 0 for your enjoyment :-)

_makeArray:
        pushl   %ebp                     ; stack fram setup
        movl    %esp, %ebp
        subl    $48, %esp
        movl    $0, -4(%ebp)             ; i = 0
        jmp     L4                       ; start loop
L5:
        movl    -4(%ebp), %edx
        movl    -4(%ebp), %eax
        movl    %eax, -44(%ebp,%edx,4)   ; array[i] = i
        addl    $1, -4(%ebp)             ; i++
L4:
        cmpl    $9, -4(%ebp)             ; for all i up to and including 9
        jle     L5                       ; continue loop
        leave
        ret
        .section .rdata,"dr"
LC0:
        .ascii "%d\12\0"                 ; format string for printf
        .text

_printArray:
        pushl   %ebp                     ; stack frame setup
        movl    %esp, %ebp
        subl    $56, %esp
        movl    $0, -4(%ebp)             ; i = 0
        jmp     L8                       ; start loop
L9:
        movl    -4(%ebp), %eax           ; get i
        movl    -44(%ebp,%eax,4), %eax   ; get array[i]
        movl    %eax, 4(%esp)            ; store array[i] for printf
        movl    $LC0, (%esp)             ; store format string
        call    _printf                  ; make the call
        addl    $1, -4(%ebp)             ; i++
L8:
        cmpl    $9, -4(%ebp)             ; for all i up to and including 9
        jle     L9                       ; continue loop
        leave
        ret

Update: As Roddy points out in a comment. that's not the cause of your specific problem since, in this case, the array is actually at the same position in memory (%ebp-44 with %ebp being the same across the two calls). What I was trying to point out was that two functions with the same argument list and same local parameters did not necessarily end up with the same stack frame layout.

All it would take would be for printArray to swap the location of its local variables (including any temporaries not explicitly created by the developer) around and you would have this problem.

paxdiablo
It's probably portable between architectures, but in no way is it guaranteed to work since it relies on undefined behavior and common habits of C compilers. There are situations where crazy things like this are good to know.
Adam Shiemke
This, this, a million times THIS. I don't even want to THINK about the inevitable bugs when some developer thinks they're "clever" and actually uses this in real code. This is the ultimate in "it works on my machine!" nonsense.
Nicholas Knight
I KNOW that it is not something we meet every day ... the lecturer (who was in Stanford stuff and working in facebook now) said it was useful in coding a sound driver ...any way i think it is not wrong to know why does it behave like that...just for educational purpose...
Ahmed Kotb
@Ahmed: It's _not_ useful in coding _anything_. You _can't_ know why it behaves like that without knowing _exactly_ how your particular compiler and platform work. Not all implementations are going to behave the same way, as you've already discovered, and there's no meaningful rhyme or reason to it. That the person perpetrating this crime on computer science was a university professor does not surprise me. That he's at facebook surprises me even less. These facts do nothing to give him credibility.
Nicholas Knight
although you were very aggressive but .. i have accepted your answeri have tried to do some operations in the makeArray function so gcc want remove it during optimization and it works :)... and i learned something new so calm down...
Ahmed Kotb
@paxdiablo: The stub being there is easy enough to explain, the only safe place to eliminate the stub would be in the linker, which I doubt is affected by GCC's -On flag.
Nicholas Knight
Damn good catch, @Nicholas, making the function static removes even the stub, since it no longer needs to be visible to the linker.
paxdiablo
@Ahmed, I wasn't specifically targeting you but, when you've been in the industry for 30 years and had to clean up this sort of problem so many time, cynical as I you may become, young Padawan :-) See the update as to the specific likely cause, more stack used in one of the functions. You really _can_ learn a lot by examining the assembler files (gcc -S for example).
paxdiablo
+1 @paxdiablo. Great followup investigation/explanation.
quixoto
Re. the 48 vs 56 bytes, that shouldn't matter because array[0] is at -44(%EBP) in BOTH cases, and EBP = ESP *before* the 48/56 is subtracted from ESP. So, nice guess, but no cigar...?
Roddy
I learned something here....I just assumed all the values would be rubbish when he started printing an uninitialized array.
Rokujolady
@paxdiablo : no problem ... and thanks for watching the video :)
Ahmed Kotb
In the lecturer's defense, he was not advocating using code like this in a real application but was just showing examples of how functions map to memory. This stack to code relationship is really the point of the whole lecture.
Nick
Hence my addendum :-)
paxdiablo
+4  A: 

Never, ever, ever, ever, ever, ever do anything like this. It will not work reliably. You will get odd bugs. It is far from portable.

Ways it can fail:

.1. The compiler adds extra, hidden code

DevStudio, in debug mode, adds calls to functions that check the stack to catch stack errors. These calls will overwrite what was on the stack, thus losing your data.

.2. Someone adds an Enter/Exit call

Some compilers allow the programmer to define functions to be called on function entry and function exit. Like (1) these use stack space and will overwrite what's already there, losing data.

.3. Interrupts

In main(), if you get an interrupt between the calls to makeArray and printArray, you will lose your data. The first thing that happens when processing an interrupt is to save the state of the cpu. This usually involves pushing the CPU registers and flags onto the stack, and yes, you guessed it, overwrite your data.

.4. Compilers are clever

As you've seen, the array in makeArray is at a different address to the one in printArray. The compiler has placed it's local variables in different positions on the stack. It uses a complex algorithm to decide where to put variable - on the stack, in a register, etc and it's really not worth trying to figure out how the compiler does it as the next version of the compiler might do it some other way.

To sum up, these kind of 'clever tricks' aren't tricks and are certainly not clever. You would not lose anything by declaring the array in main and passing a reference/pointer to it in the two functions. Stacks are for storing local variables and function return addresses. Once your data goes out of scope (i.e. the stack top shrinks past the data) then the data is effectively lost - anything can happen to it.

To illustrate this point more, your results would probably be different if you had different function names (I'm just guessing here, OK).

Skizz
Ah, bitten by the infamous "all lists start at 1 no matter what number you put there" SO misfeature :-)
paxdiablo
Whoa, wasn't aware of that one. Fixed it (sort of).
Skizz
+1 for mentioning interrupts!
Roddy