D
D
Daniil Romanov2020-05-05 02:57:00
C++ / C#
Daniil Romanov, 2020-05-05 02:57:00

Accessing private fields via pointer arithmetic?

Task text: all fields of this class are private, your task is to implement several functions that give full access to these fields (see code template), despite the fact that they are private.

/*
 * Класс Cls определен точно таким образом:
 *
 * struct Cls {
 * Cls(char c, double d, int i);
 * private:
 *     char c;
 *     double d;
 *     int i;
 * };
 *
 */

// Эта функция должна предоставить доступ к полю c объекта cls.
// Обратите внимание, что возвращается ссылка на char, т. е.
// доступ предоставляется на чтение и запись.
char &get_c(Cls &cls) {
    char* ptr;
    ptr = (char*)&cls;
    return *ptr;
}

// Эта функция должна предоставить доступ к полю d объекта cls.
// Обратите внимание, что возвращается ссылка на double, т. е.
// доступ предоставляется на чтение и запись.
double &get_d(Cls &cls) {
    double* ptr;
    ptr = (double*)( (char*)(&cls) ) + 1;
    return *ptr; 
}

// Эта функция должна предоставить доступ к полю i объекта cls.
// Обратите внимание, что возвращается ссылка на int, т. е.
// доступ предоставляется на чтение и запись.
int &get_i(Cls &cls) {
    double* p;
    int* ptr;
    p = (double*)( (char*)(&cls) ) + 1;
    ptr = (int*) (p + 1);
    
    return *ptr;
}


Can you please explain how exactly all this is arranged in memory?
The moment when we receive a field char c is clear; Let's convert the reference to the object as a pointer of type char.
However, then it is not entirely clear what happens and why. What does the conversion (double*)( (char*)(&cls) ) + 1 give us? After all, char takes 1 byte, and we will convert it to double. It is clear that we need to take 8 bytes after char to get the double field.
Getting a field of type int raises even more questions. For example, if ptr = (int*) (p) + 1, then this will produce an incorrect result. Why is this happening?

Answer the question

In order to leave comments, you need to log in

4 answer(s)
J
jcmvbkbc, 2020-05-05
@chattydude1

Can you please explain how exactly all this is arranged in memory?

This is usually arranged so that the fields go one after the other in memory. But besides the size, the fields have alignment. For example, uint32_t is 4-byte aligned, and uint64_t is 8-byte aligned. Therefore, there can be holes between successive fields of different types.
In the above example, double is the field with the most alignment, the alignment of the object will be 8, the field cwill be at offset 0 in the object, the field will dbe at offset 8, and the field will ibe at offset 16. With this in mind, pointer games acquire meaning.
Task text: all fields of this class are private, your task is to implement several functions that give full access to these fields (see code template), despite the fact that they are private.

Please don't ever do that.

M
mayton2019, 2020-05-05
@mayton2019

Formally, from the point of view of OOP, this cannot be done. I do not know the product tasks where such a malicious hack would take place.
And the teacher who came up with this - you need to tear off the balls.

K
k-morozov, 2020-05-05
@k-morozov

if there are no virtual methods, then we can assume that the data goes in a row. in fact, you need to correctly access the desired memory location. Read about memory alignment.

O
oleghab, 2020-05-07
@oleghab

In reality, if you need to bypass access restrictions, then this can be done, for example, like this:

/*
 * Класс Cls определен точно таким образом:
 *
 * struct Cls {
 * Cls(char c, double d, int i);
 * private:
 *     char c;
 *     double d;
 *     int i;
 * };
 *
 */

namespace {
    // Cls_Double - точная копия Cls в смысле расположения данных.
    // Важный момент - данные в Cls НЕ скрыты, но видимы с ограничением доступа (что разные вещи)
    // Это позволяет делать дубликат класса всегда
    struct Cls_Double {
        char c;
        double d;
        int i;
    };
}

// Эта функция должна предоставить доступ к полю c объекта cls.
// Обратите внимание, что возвращается ссылка на char, т. е.
// доступ предоставляется на чтение и запись.
char &get_c(Cls &cls) {
    return reinterpret_cast<Cls_Double&>(cls).c;
}

// Эта функция должна предоставить доступ к полю d объекта cls.
// Обратите внимание, что возвращается ссылка на double, т. е.
// доступ предоставляется на чтение и запись.
double &get_d(Cls &cls) {
    return reinterpret_cast<Cls_Double&>(cls).d;
}

// Эта функция должна предоставить доступ к полю i объекта cls.
// Обратите внимание, что возвращается ссылка на int, т. е.
// доступ предоставляется на чтение и запись.
int &get_i(Cls &cls) {
    return reinterpret_cast<Cls_Double&>(cls).i;
}

A couple of notes:
1) casting through pointer arithmetic and reinterpret_cast both violate strict aliasing, so it doesn’t matter what to use
2) if there are internal torments, then for at least relative peace of mind, you can use a double static_cast instead of reinterpret_cast, it will also violate strict aliasing, so that's it equals
3) Perhaps, if this is an academic assignment, manual computation via pointer arithmetic, in the style of
return *(char *)((std::uint8_t*)&cls);
return *(double *)((std::uint8_t*)&cls + sizeof(char));
return *(int *)((std::uint8_t*)&cls + sizeof(char) + sizeof(double));

But never do this in production, because you can have a lot of problems with supporting such calculations, including if suddenly Cls actually inherits from a class with vptr, which can change from the outside. Then Cls_Double should be identically inherited, and the compiler will provide correct addressing.
Another option is data packing issues, which the compiler can also solve for us for free.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question