I am new to C and I have a question about pointers

Viewed 119

The following lines of code work as you'd expect

#include <stdio.h>

int main(void)
{
  int n;
  int a[5];
  int *p;

  a[2] = 1024;
  p = &n;
  /*
   * write your line of code here...
   * Remember:
   * - you are not allowed to use a
   * - you are not allowed to modify p
   * - only one statement
   * - you are not allowed to code anything else than this line of code
   */
  
  /* ...so that this prints 98\n */
  printf("a[2] = %d\n", a[2]);
  return (0);
}

This prints out a[2] = 1024

Now I was asked to modify this code so that a[2] = 98 gets printed instead. There were a ton of constraints. I couldn't use the variable a anywhere else in the code again and a couple other things. I found a solution online but I don't understand it at all.

#include <stdio.h>

int main(void)
{
  int n;
  int a[5];
  int *p;

  a[2] = 1024;
  p = &n;
  /*
   * write your line of code here...
   * Remember:
   * - you are not allowed to use a
   * - you are not allowed to modify p
   * - only one statement
   * - you are not allowed to code anything else than this line of code
   */
  p[5] = 98;
  /* ...so that this prints 98\n */
  printf("a[2] = %d\n", a[2]);
  return (0);
}

So, setting p[5] = 98; results in a[2] = 98 being printed, which is the intended result. I'm fairly new to C programming and pointers in general but I have absolutely no idea why this works the way it does.

4 Answers

This is all about the layout of the stack, the memory where the local variables in your method are stored. Variables are pushed onto the stack, so first the variable n is pushed to the stack, let us assume that it ends up at address 1000 (this is just a fictional address). Since it is an integer it takes up the space of an integer (4 bytes if integers are 32 bits). Then you push the array a to the stack. It will be located next to n.

Since n was placed at address 1000 and took up 4 bytes of memory, then a will be placed at address 1004 (1000 + 4). a is an array of 5 integers, each taking up 4 bytes. So a takes up the space from 1004 to 1024.

Your variable p is an integer pointer, and you set it to point to n. That means that p points to the address 1000 which is the address of n. You then write p[5] in C that is equivalent to the expression *(p + 5). Which basically means take value of the address p + 5. And since p is an integer pointer and each integer takes up 4 bytes, you are essentially asking for the value of address: 1000 + (5 * 4) = 1020

In the array a you stored the value 1024 at index 2, that corresponds to address: 1004 + 2 * 4 = 1012, so when you print the value of a[2] you are printing the value of address 1012. This means that the value you are setting to 98, is not a[2] but a[4].

The reason why I am mentioning this, is that in my case it did not print 98 but 1024. As people have already mentioned you are working with undefined behavior, and although it might work on some setups, it might not work on all.

I can't tell you the answer to your problem. But I can write a program which might, maybe, find the answer to your problem.

Try running this program:

#include <stdio.h>

int main2(int off)
{
    int n;
    int a[5];
    int *p;

    a[2] = 1024;
    p = &n;
    p[off] = 98;
    return a[2];
}

int main()
{
    int i;
    for(i = -5; i <= 5; i++)
        if(main2(i) == 98)
            printf("the magic offset is %d\n", i);
}

On my computer, with one of my compilers, today, this program prints

the magic offset is 4

That tells me that (on my computer, with that compiler, today) the "solution" to your ridiculous problem would be

#include <stdio.h>

int main()
{
    int n;
    int a[5];
    int *p;

    a[2] = 1024;
    p = &n;
    p[4] = 98;
    printf("a[2] = %d\n", a[2]);
}

I'm not even going to try to explain why this works, because the reasons are so obscure, unrepeatable, and meaningless. (See this question's other good answers for more details.) Basically the first program automates the search for a magic offset from p, more or less as you discovered.

And, in fact, under the first compiler I tried it, it didn't even work. Despite using the magic number 4 that the first program discovered, the second program printed a[2] = 1024. That's not too surprising: the relative positions of variables like a, p, and n are not specified by any standard. They're totally up to the compiler. The compiler is perfectly within its rights to arrange them one way in function main2 in my first program, and a completely different way in function main in my second program.

I tried my first program under a different compiler, and it printed

the magic offset is -3

and then crashed with a segmentation fault. But then, under that compiler, when I changed the relevant line in the second program to

p[-3] = 98;

it "worked", printing a[2] = 98 as required.

(And then I tried turning up the optimization level, and it stopped working.)

To be perfectly clear, the fact that my approach did not work under that first compiler, because it failed to arrange things in a consistent or predictable way, does not mean there's anything wrong with that compiler! Quite the contrary: the fault is entirely in the broken programs I wrote, and the broken assignment of yours that motivated them.


Here is an alternative exercise which will teach you something useful about arrays and pointers, without requiring that you "learn" false, unrepeatable facts about how variables are or aren't guaranteed to be arranged in stack frames.

#include <stdio.h>

int main(void)
{
    int a[5];
    int *p;

    a[2] = 1024;
    p = &a[4];
    /*
     * write your line of code here...
     * Remember:
     * - you are not allowed to use a
     * - you are not allowed to modify p
     * - only one statement
     * - you are not allowed to code anything else than this line of code
     */
    
    /* ...so that this prints 98\n */
    printf("a[2] = %d\n", a[2]);
}

This problem has a similar solution — you can easily work it out — but the solution is unique and guaranteed to work, because it depends on well-defined properties of arrays and pointer arithmetic in C, not on accidental details of the stack layout.

I assume that you have got this task because you are studying how to do some kind of hacking by writing outside allowed memory to change other variables.

The way to achieve the goal depends on compiler, compiler flags, systems, etc.

So to solve it on your system start by adding these lines:

printf("size int: %zu\n", sizeof(int));
printf("n: %p\n", (void*)&n);
printf("a: %p\n", (void*)a);

On my system I get:

size int: 4
n: 0x7fff387c7934
a: 0x7fff387c7920

From this I can see that a is placed in memory hex 0x14 or decimal 20 bytes before n. Since size of int is 4 we can also say that a is placed 5 ints (i.e. 20/4 = 5) before n.

Now p points to n. To reach the start of a (aka a[0]) using p we need to go 5 ints backwards in memory. That is done using p[-5].

Now you want to change a[2] so we need to add that offset and access p[-5+2] or p[-3]. In other words:

p[-3] = 98;

will do the trick on my system.

note: Accessing p[-3] is undefined behavior, i.e. something you should never do in real programs.

Just tried another system - see https://ideone.com/AAeaX3

Here the solution was

p[3] = 98;

because n and a was placed differently in memory.

It "works" by accident. It relies on n, p, and a being laid out in memory in a specific order (each box represents 4 bytes):

Address        Item
-------    --------
              +---+       
0x8000     n: |   |       p[0]
              +---+
0x8004     p: |   |       p[1]
              +---+
0x8008        |   |       p[2]
              +---+
0x800c     a: |   | a[0]  p[3]
              +---+
0x8010        |   | a[1]  p[4]
              +---+
0x8014        |   | a[2]  p[5]
              +---+
0x8018        |   | a[3]  p[6]
              +---+
0x801c        |   | a[4]  p[7]
              +---+

Some background:

A pointer is any expression whose value is the location of an object or function in a running program's execution environment - essentially, an address. A variable of pointer type stores an address value. However, pointers have associated type semantics - a pointer to int is a different type than a pointer to double, which is a different type than a pointer to struct foo, which is a different type than a pointer to an array of char, etc.

When you add 1 to a pointer value, the result is a pointer to the next object of the pointed-to type immediately following:

char  *cp = &some_char;
short *sp = &some_short;
long  *lp = &some_long;

           +---+                       +---+                       +---+
some_char: |   | <-- cp    some_short: |   |  <-- sp    some_long: |   | <-- lp
           +---+                       |   |                       |   |
           |   | <-- cp + 1            |   |                       |   |
           +---+                       +---+                       |   |
           |   | <-- cp + 2            |   | <-- sp + 1            |   |
           +---+                       |   |                       |   |
           |   | <-- cp + 3            |   |                       |   |
           +---+                       +---+                       +---+
           |   | <-- cp + 4            |   | <-- sp + 2            |   | <-- lp + 1
           +---+                       |   |                       |   |
            ...                         ...                         ...

This is exactly how array subscripting works - the array subscript operation a[i] is defined as *(a + i) - given a starting address a, offset i elements (not bytes!) from that address and deference the result. Arrays are not pointers; rather, array expressions "decay" to pointers to their first element under most circumstances. But this means you can use the [] subscript operator on pointer variables as well, so if you set p to point to n with

int *p = &n;

then you can apply the [] operator to p and treat it as though it was an array. So, if p == 0x8000 (the address of n), then p + 5 == 0x8014, which is the address of a[2]. Thus, *(p + 5) == p[5] == a[2] == *(a + 2).

But...

This behavior is undefined - it may work, it may not. It may result in garbled output, it may branch into some random subroutine, it may invoke Rogue. Neither the compiler nor the runtime environment are required to handle it in any particular way - any result is equally correct as far as the language is concerned.

We're pretending n is the first element of an array of int when it really isn't, so we're indexing out of bounds with p[1], p[2], etc. We're assuming objects are laid out in a specific order, but the compiler is under no obligation to lay variables out that way. The compiler may optimize things such that p is stored in a register, rather than on the stack.

This is a horrible way to teach pointers. It's unsafe, it's unportable, it's bad practice, it's confusing, it's an atypical use case, it doesn't explain why we use pointers. Whoever gave you this code shouldn't be teaching anyone how to program in C. If they write C for a living they are a menace.

Related